momo zone

调核人的blog

关于0号和1号进程堆栈的疑惑

这一部分困惑了很久,但这个不是理解kernel的一个重点。

先看看大牛的解释:

由于创建新进程的过程是通过完全复制父进程代码段和数据段的方式实现的,因此在首次使用fork()创建新进程init时,为了确保新进程用户态堆栈没有进程0的多余信息,要求进程0在创建首个新进程之前不要使用用户态堆栈,也即要求任务0不要调用函数。因此在main.c主程序移动到任务0执行后,任务0中的代码fork()不能以函数形式进行调用。程序中实现的方法是采用gcc函数内嵌(内联)的形式来执行这个系统调用。参见下面程序第23行。虽然其中的系统中断调用还是避免不了使用,但是系统调用使用任务的内核态栈,而每个任务都有自己独立的内核态栈,因此系统调用不会影响这里讨论的用户态栈。
   
另外,在创建新进程init的过程中,系统对其进行了一些特殊处理。在为新进程init复制其父进程(进程0)的页目录和页表项时并没有为它们处于内核区
的代码和数据执行写时复制(Copy on Wirte)操作
,进程0和进程init实际上同时使用着内核代码区内(小于1MB的物理内存)相同的代码和数据物理内存页面,只是执行的代码不在一处,因此实际上它们也
同时使用着相同的用户堆栈区。为了不出现冲突问题,就必须要求任务0在整个执行过程中禁止使用到用户堆栈区域,而让进程init能单独使用堆栈。因此pause()也必须采用内嵌函数形式来实现。
    当系统中一个进程(例如init进程的子进程,进程2)执行过execve()调用后,进程2的代码和数据区会位于系统的主内存区中,因此系统可以利用写时复制技术来处理其他新进程的创建和执行。
    对于Linux来说,所有任务都是在用户模式下运行的,包括很多系统应用程序,如shell程序、网络子系统程序等。内核源代码lib/目录下的库文件就是专门为这里新创建的进程提供支持函数的。

看完后可以明确以下几点:
1.进程1是通过fork()复制进程0得到的,和进程2以及后续创建的进程不同的是没有接着使用execve(),而是“手动”地进行了一些特殊处理。
2.因为1,所以进程0和1的数据段和代码断指向相同物理地址。
3.又因为2,所以用户堆栈段也是一样的。

其实关键的就是因为第一点,没有使用execve()
不过即使这样如果真的在创建进程1之前往进程0的用户堆栈写东西,也不会导致系统崩溃,因为新创建的进程仅仅复制了这些无用的堆栈信息,但永远也不会去读它(写黑客程序除外)。

留下评论