XV6操作系统入门系列(2024)-04-第一个进程

287 阅读6分钟

XV6操作系统入门系列(2024)

04-第一个进程

进程是操作系统管理程序的基本单元。一个程序最基本的组成部分包含代码、数据和硬件资源。那么,XV6操作系统启动的第一个进程是什么呢?我的文章风格是从这个问题出发,引出一系列相关的知识点。

资料链接:

  • XV6操作系统教材
  • 官方实验代码 git clone git://g.csail.mit.edu/xv6-labs-2024

从内核初始化开始找线索

我前面的文章详细介绍了XV6如何利用RiscV处理器的硬件机制,启动自己的。对接下来的内容没什么用,大家也不需要浪费时间翻前面的内容。现在我们只需要知道,RiscV会执行XV6源代码中的 kernel/main.c 的代码。要想搞明白XV6操作系统的第一个进程是怎么来的,我们唯一的线索就在这个代码中。 打开main.c 文件,注释里清清楚楚地写着first user process——第一个用户进程是由 userinit() 产生的。

用户进程初始化函数 userinit

我先把源代码贴在这里,读这段代码的时候,我们会碰到一些黑盒性质的概念,我们可以先接受,后面再慢慢理解。

从代码上看userinit() 做了这么几件事情:

  • 调用allocproc() 分配一个进程,返回一个结构体,肯定保存了一些进程需要的数据,allocproc()内部做了什么我们还看不到;
  • 给申请到的进程的页表pagetable写入一段二进制代码initcode,这段代码是干什么的我们也不清楚;
  • 给进程的trapframe->epc设置为0,代码注释说用于内核态返回用户态,具体怎么做到的我们也不清楚。 我们遇到了三个新的问题,不用慌,我们一个一个解决。

第一个问题 allocproc 是分配进程资源的

allocproc 逻辑我给大家总结一下:

  • • 遍历一个全局变量数组 proc,是内核中表示所有进程的数据结构,有 NPROC=64 个。
  • • 从中找到第一个没有被使用的进程插槽拿出来。 这个简单的过程就叫做分配进程。如果在此之前,你听说过操作系统创建进程,并且觉得这个过程非常高大上,那现在可以对它祛媚了,实际上指的就是这么个简单的过程。

第二个问题 initcode 是用来装载第一个用户程序

在源代码里,initcode是一段恐怖的数字数组。注释里写着它是从 user/initcode.S 代码中编译来的可执行文件。我们来看一下这段代码。

如果你对汇编完全不熟悉,我给你总结一下,这段代码等于 exec("init", ["init"]) 。调用内核里的函数 exec ,运行程序 init。这里的 init 指的是 user/init.c 这个程序。这个程序核心是运行 sh 命令行工具。 到这里,我们已经能够回答这篇文章的核心问题:XV6里运行的第一个进程的代码部分涉及 initcode.S + init.c 这两个文件。

我想你已经晕了,我这里再总结成这样一张图。 蓝色部分是当前处理器正在执行的代码,调用userinit()allocprocuvmfirst 三个函数来为第一个进程做准备。绿色部分是还没有运行的代码,由 initcode.Sinit.c 两部分组成。

那么绿色程序究竟是如何真正执行起来的呢?

上一小节我们分析了处理器正在执行的程序,由蓝色部分表示。它们装载了一个准备运行的程序,由绿色部分表示。那么绿色部分什么时候运行起来的呢? 在操作系统初始化阶段的最后,main.c调用了一个进程调度函数 scheduler()。它的流程就是循环遍历所有的进程,找到下一个可以运行的进程,然后调用

  swtch(&c->context, &p->context);

它的作用是将当前cpu中各个寄存器的值保存到 c->context 上,然后将 p->context 中的值加载到cpu的各个寄存器中。

因为 swtch 内部修改了 ra 寄存器(return address),swtch 内部的ret指令不会再返回执行c->proc这条代码了,而是会进入 p->context->ra 这个地方继续运行。 那么,第一个进程的 p->context->ra 是什么呢?答案在前面的allocproc函数中,所以新进程的 p->context->ra 都设置成了 forkret 函数。也就是说,swtch 执行后会跳到 forkret 函数继续执行,不会返回来执行 c->proc这个跳转非常隐蔽,你必须了解汇编指令ret返回到的指令地址是寄存器ra的值

我们来验证一下这个结论。打开代码调试界面,我们输入指令添加几个断点:

  • b proc.c:469swtch 执行处添加中断。
  • b swtch:40swtch 内部的ret指令处打上断点。
  • b forkretforkret 处打上断点。 接着,我们输入 c 代码停在第一个断点处。然后我们按下两个c,代码停在了第三个断点处。完全符合预期,程序进入到了forkret中运行。

第三个问题trapframe 是干什么的?

要搞明白这个问题,需要我们紧接着上一个问题继续分析源码。 从forkret的源码我们可以知道,它调用了 usertrapret() 这个函数。而 usertrapret() 这个函数。而 usertrapret() 的代码也很特殊,需要一定的汇编指令的知识,它最终跳转到了 trampoline.S:userret 这个汇编指令。汇编指令 trampoline.S:userret 里使用到了一个特殊的指令 sret。这是 RiscV 处理器专门用来从内核态跳转到用户态的指令。在这个指令之前,我们看到的所有指令都是运行在内核态的。到 sret 之后,我们才接触到了第一个用户态的指令。

那么,处理器是怎么知道 sret 究竟要跳转到哪行代码继续执行呢?答案就在 trapframe 中。还记得我们要解决的第三个问题吗?这个命令 p->trapframe->epc=0 就是用来设置sret要跳转回的命令地址,因为这里设置为0,也就是跳转到用户程序的开头。

总结

这篇文章详细分析了XV6的第一个应用程序是怎么运行起来的。当然我写的还不够详细,关键还是需要你亲自读源码体会。如果你遇到解决不了的问题,欢迎在评论区留言。有时候别人只是一句话,自己折腾一下午。 如果你真的弄明白了第一个应用程序的运行机制,后续做XV6的实验题目会容易很多,不至于摸不着头脑,无从下手。