当前位置: 首页 > news >正文

(done) 补充:xv6 的一个用户程序 init 是怎么启动的 ?它如何启动第一个 bash ?

先看 main.c
从函数名来看,比较相关的就 userinit() 和 scheduler()

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"volatile static int started = 0;// start() jumps here in supervisor mode on all CPUs.
void
main()
{if(cpuid() == 0){consoleinit();printfinit();printf("\n");printf("xv6 kernel is booting\n");printf("\n");kinit();         // physical page allocatorkvminit();       // create kernel page tablekvminithart();   // turn on pagingprocinit();      // process tabletrapinit();      // trap vectorstrapinithart();  // install kernel trap vectorplicinit();      // set up interrupt controllerplicinithart();  // ask PLIC for device interruptsbinit();         // buffer cacheiinit();         // inode tablefileinit();      // file tablevirtio_disk_init(); // emulated hard diskuserinit();      // first user process__sync_synchronize();started = 1;} else {while(started == 0);__sync_synchronize();printf("hart %d starting\n", cpuid());kvminithart();    // turn on pagingtrapinithart();   // install kernel trap vectorplicinithart();   // ask PLIC for device interrupts}scheduler();        
}

看 userinit
1.使用 allocproc() 分配一个进程 (分配内核栈,以及 scheduler 调度后跳转到的地址 – forkret)
2.使用 uvmfirst 为 proc 映射一个内存页,该内存页储存 initcode (initcode.S 的汇编结果)。映射到用户空间虚拟地址 0 上
3.设置进程当前目录为 根目录 “/” (该目录在 mkfs.c 中创建)
4.设置进程状态为 RUNNABLE,方便后续调度

// a user program that calls exec("/init")
// assembled from ../user/initcode.S
// od -t xC ../user/initcode
// 汇编自 initcode.S 的二进制指令
uchar initcode[] = {0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00
};// Load the user initcode into address 0 of pagetable,
// for the very first process.
// sz must be less than a page.
// 分配一页内存,映射到页表里,再把 src 的内容拷贝进那页内存
void
uvmfirst(pagetable_t pagetable, uchar *src, uint sz)
{char *mem;if(sz >= PGSIZE)panic("uvmfirst: more than a page");mem = kalloc();memset(mem, 0, PGSIZE);mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);memmove(mem, src, sz);
}// Set up first user process.
void
userinit(void)
{struct proc *p;p = allocproc();initproc = p;// allocate one user page and copy initcode's instructions// and data into it.// 分配一页内存,映射到页表里,再把 initCode 的内容拷贝进那页内存uvmfirst(p->pagetable, initcode, sizeof(initcode));p->sz = PGSIZE;// prepare for the very first "return" from kernel to user.// 初始化用户程序的栈顶和 pcp->trapframe->epc = 0;      // user program counterp->trapframe->sp = PGSIZE;  // user stack pointer// 给第一个进程命名safestrcpy(p->name, "initcode", sizeof(p->name));// 名为 "/" 的 inode 是在 mkfs.c 中被创建的p->cwd = namei("/");// 标记该进程状态为 RUNNABLEp->state = RUNNABLE;release(&p->lock);
}

看 initcode.S
从汇编源码来看,就做两个事情:
1.调用系统调用 exec(“/init”, “/init”, NULL)
2.调用系统调用 exit(0)

# Initial process that execs /init.
# This code runs in user space.#include "syscall.h"# exec(init, argv)
.globl start
start:la a0, initla a1, argvli a7, SYS_exececall# for(;;) exit();
exit:li a7, SYS_exitecalljal exit# char init[] = "/init\0";
init:.string "/init\0"# char *argv[] = { init, 0 };
.p2align 2
argv:.long init.long 0

接下来看 scheduler
1.开中断
2.查找 RUNNABLE process
3.上下文切换过去

void
scheduler(void)
{struct proc *p;struct cpu *c = mycpu();c->proc = 0;for(;;){// The most recent process to run may have had interrupts// turned off; enable them to avoid a deadlock if all// processes are waiting.intr_on();for(p = proc; p < &proc[NPROC]; p++) {acquire(&p->lock);if(p->state == RUNNABLE) {// Switch to chosen process.  It is the process's job// to release its lock and then reacquire it// before jumping back to us.p->state = RUNNING;c->proc = p;swtch(&c->context, &p->context);// Process is done running for now.// It should have changed its p->state before coming back.c->proc = 0;}release(&p->lock);}}
}

根据 allocproc() 的代码,我们知道所有内核进程一开始跳转到的函数是 forkret,来看看 forkret
如果 forkret 是被第一次调用的,那么它会执行 fsinit(ROOTDEV) 初始化内核的文件系统。否则,调用 usertrapret

// A fork child's very first scheduling by scheduler()
// will swtch to forkret.
void
forkret(void)
{static int first = 1;// Still holding p->lock from scheduler.release(&myproc()->lock);if (first) {// File system initialization must be run in the context of a// regular process (e.g., because it calls sleep), and thus cannot// be run from main().fsinit(ROOTDEV);first = 0;// ensure other cores see first=0.__sync_synchronize();}usertrapret();
}

看 usertrapret
这里会做一系列设置,但最终会使用

  uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);((void (*)(uint64))trampoline_userret)(satp);

这种方式跳转到 trampoline.S 汇编代码里,这个代码最后一个指令是 sret,也就是跳转到用户态,同时 PC 指向之前在 userinit() 中设置的 0x0

//
// return to user space
//
void
usertrapret(void)
{struct proc *p = myproc();// we're about to switch the destination of traps from// kerneltrap() to usertrap(), so turn off interrupts until// we're back in user space, where usertrap() is correct.intr_off();// send syscalls, interrupts, and exceptions to uservec in trampoline.Suint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);w_stvec(trampoline_uservec);// set up trapframe values that uservec will need when// the process next traps into the kernel.p->trapframe->kernel_satp = r_satp();         // kernel page tablep->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stackp->trapframe->kernel_trap = (uint64)usertrap;p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()// set up the registers that trampoline.S's sret will use// to get to user space.// set S Previous Privilege mode to User.unsigned long x = r_sstatus();x &= ~SSTATUS_SPP; // clear SPP to 0 for user modex |= SSTATUS_SPIE; // enable interrupts in user modew_sstatus(x);// set S Exception Program Counter to the saved user pc.w_sepc(p->trapframe->epc);// tell trampoline.S the user page table to switch to.uint64 satp = MAKE_SATP(p->pagetable);// jump to userret in trampoline.S at the top of memory, which // switches to the user page table, restores user registers,// and switches to user mode with sret.uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);((void (*)(uint64))trampoline_userret)(satp);
}

最后回顾 initcode.S,它调用了系统调用 exec(/init, /init, NULL)。我们来看看文件 /init 是啥。
在 Makefile 中,可以看到 _init,说明它以文件形式存在于 xv6 文件系统里
在这里插入图片描述

看 user/init.c
从代码来看,它打开 console,随后调用 fork 来启动 sh 程序,接着等待 sh 程序退出,若 sh 程序退出,则再次开启一个新的 sh

// init: The initial user-level program#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/spinlock.h"
#include "kernel/sleeplock.h"
#include "kernel/fs.h"
#include "kernel/file.h"
#include "user/user.h"
#include "kernel/fcntl.h"char *argv[] = { "sh", 0 };int
main(void)
{int pid, wpid;if(open("console", O_RDWR) < 0){mknod("console", CONSOLE, 0);open("console", O_RDWR);}dup(0);  // stdoutdup(0);  // stderrfor(;;){printf("init: starting sh\n");pid = fork();if(pid < 0){printf("init: fork failed\n");exit(1);}if(pid == 0){exec("sh", argv);printf("init: exec sh failed\n");exit(1);}for(;;){// this call to wait() returns if the shell exits,// or if a parentless process exits.wpid = wait((int *) 0);if(wpid == pid){// the shell exited; restart it.break;} else if(wpid < 0){printf("init: wait returned an error\n");exit(1);} else {// it was a parentless process; do nothing.}}}
}

再看 sh.c
sh.c 的代码复杂很多,但核心代码就下面几行,循环读取用户输入的命令,随后调用 fork + exec 去执行 (runcmd 是基于 exec 实现的)

  // Read and run input commands.while(getcmd(buf, sizeof(buf)) >= 0){if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){// Chdir must be called by the parent, not the child.buf[strlen(buf)-1] = 0;  // chop \nif(chdir(buf+3) < 0)fprintf(2, "cannot cd %s\n", buf+3);continue;}if(fork1() == 0)runcmd(parsecmd(buf));wait(0);}

到这里,就知道 xv6 第一个程序是 init.c,以及它是怎么启动第一个 bash 的了。

相关文章:

  • ARM64内核内存空间布局
  • The 2024 Sichuan Provincial Collegiate Programming Contest部分题解(L,H,E,B,I)
  • Ethereum Pectra 的升级
  • TWASandGWAS中GBS filtering and GWAS(1)
  • 《Flutter社交应用暗黑奥秘:模式适配与色彩的艺术》
  • 使用PhpStudy搭建Web测试服务器
  • 每日一题洛谷P8662 [蓝桥杯 2018 省 AB] 全球变暖c++
  • Ubuntu20.04 搭建Kubernetes 1.28版本集群
  • WSL 安装 Debian 12 后,Linux 如何安装 redis ?
  • C#WPF里不能出现滚动条的原因
  • SysAid On-Prem XML注入漏洞复现(CVE-2025-2776)
  • 栈和队列复习(C语言版)
  • Java笔记4
  • Go语言即时通讯系统 开发日志day1
  • OpenCV CUDA 模块中在 GPU 上对图像或矩阵进行 翻转(镜像)操作的一个函数 flip()
  • beyond compare 免密钥进入使用(删除注册表)
  • 信息安全模型全解:从机密性到完整性的理论基石
  • OpenCVCUDA 模块中在 GPU 上对图像或矩阵进行 边界填充(padding)函数copyMakeBorder()
  • 994. 腐烂的橘子
  • MiMo-7B-RL调研
  • 社恐也能嗨起来,《孤独摇滚》千人观影齐舞荧光棒
  • 成就彼此,照亮世界:“中欧建交50周年论坛”在沪成功举行
  • 交涉之政、交涉之学与交涉文献——《近代中外交涉史料丛书》第二辑“总序”
  • 印称一名高级官员在巴基斯坦发动的袭击中死亡
  • 国家统计局:4月份居民消费价格同比下降0.1%
  • 体坛联播|郑钦文收获红土赛季首胜,国际乒联公布财报