本文是上一博文 Linux 内核学习笔记:进程 2 的创建及执行(第 1 部分)的续篇续篇。
注:比较完整的进程加载程序案例可参考《Linux 内核设计的艺术》(第 2 版)第 6.3 节“一个用户进程从创建到退出的完整过程”。这个小节写的相当不错!
进程 1 创建进程 2
在打开标准输出、标准错误输出设备文件完毕后,进程 1 继续执行 init 函数,创建进程 2:
1 | // init/main.c --------------------------------- |
进程 1 创建进程 2 的过程跟进程 0 创建进程 1 的过程一样,请参照之前博文 Linux 内核学习笔记:进程 1 的创建及执行(第 1 部分)。其中有一点需要注意,跟进程 1 只复制了进程 0 页表的前 160 项不同,进程 2 复制了进程 1 页表的前 1024 项:
1 | // mm/memory.c --------------------------------- |
进程 1 继续执行
在进程 1,fork 函数返回进程 2 的进程号(注意不是任务号)2,开始执行以下代码:
1 | // init/main.c --------------------------------- |
上述代码表明 wait 函数实际调用的是 waitpid 函数,而 waitpid 函数又通过系统调用总入口 _system_call 来调用具体的服务程序 sys_waitpid 函数:
1 | // kernel/exit.c ------------------------------ |
现在已经调度到进程 2 执行。
进入进程 2 执行
进程 2 开始执行以下代码:
1 | // init/main.c --------------------------------- |
调用 execve
execve 函数执行流程可参考之前博文 Linux 内核学习笔记:预备知识之“加载及虚拟地址空间”“execve 系统调用”部分。其定义如下:
1 | // init/main.c --------------------------------- |
以上代码表明 execve 函数实际调用的是 do_execve 函数。
上述代码有一点不好理解的是 lea EIP(%esp),%eax
这一句。在 kernel/system_call.s 文件开头,作者就对系统调用时,内核栈的分布情况有了一个说明:
1 | * Stack layout in 'ret_from_system_call': |
上述代码可用图表示如下:
所以 lea EIP(%esp),%eax
这一句就相当于让 eax 的值等于被压栈的原 eip 的首地址。而且 eax 还被作为 do_execve 函数的第一个参数 eip。所以有:
1 | eip[0] = 原 eip |
检测 shell 文件
检测 i 节点属性
这部分代码如下:
1 | // fs/exec.c ----------------------------------- |
检测文件头属性
这部分代码如下:
1 | // fs/exec.c ----------------------------------- |
为 shell 程序执行做准备
加载参数和环境变量
这部分代码如下:
1 | // fs/exec.c ----------------------------------- |
其中,对于 p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
这一句的理解可参照下图:
图片来源:《Linux 内核完全注释》
copy_strings 函数
copy_strings 函数定义如下:
1 | // fs/exec.c ----------------------------------- |
理解 copy_strings 可参考下图:
图片来源:《Linux 内核完全注释》
create_tables 函数
create_tables 函数定义如下:
1 | // fs/exec.c ----------------------------------- |
create_tables 函数执行结果如下图所示:
图片来源:《Linux 内核完全注释》
综上,以上函数执行完后的最终效果如下图所示:
图片来源:《Linux 内核完全注释》
调整进程 2 管理结构
这部分代码如下:
1 | // fs/exec.c ----------------------------------- |
free_page_tables 函数
free_page_tables 函数定义如下:
1 | // mm/memory.c --------------------------------- |
change_ldt 函数
change_ldt 函数定义如下:
1 | // fs/exec.c ----------------------------------- |
调整 eip 和 esp
这部分代码如下:
1 | // fs/exec.c ----------------------------------- |
到这里,do_execve 函数执行完毕。
执行 shell 程序
缺页中断
除了参数和环境变量的页面管理指针数组,shell 程序的线性地址空间对应的程序并未加载到物理内存,所以当 shell 程序开始执行时,在页表目录或页表不能查询到 shell 程序所在物理页面的表项,这样会引发“页异常”中断。此中断会进一步调用“缺页中断”处理程序来分配一个页面,并加载一页 shell 程序。
执行代码如下:
1 | // mm/page.s ----------------------------------- |
调用 do_no_page
do_no_page 函数定义如下:
1 | // mm/memory.c --------------------------------- |