Linux 进程执行过程分析
编号411,本文参考孟宁老师 Github 项目 https://github.com/mengning/linuxkernel
进程的基本要素
由于系统进程有一定的特殊性,这里主要分析普通用户进程。一般来讲,Linux 系统下的进程有几个基础要素:
-
可执行代码
可执行代码是进程的基本要素,这部分包含表示程序功能的进程私有代码和共享的链接库代码。
-
系统专用系统堆栈空间
进程专用的系统堆栈空间
-
进程控制块
即task_stuct数据结构,一方面,进程控制块包含的内容为内核调度提供了数据,另一方面,这个结构体记录了该进程的私有资源。
-
独立存储空间
独立的存储空间,即表示该进程拥有专有的用户空间(用户空间堆栈)。
除了以上的基本要素外,为了理解进程的执行过程,我们还需要理解以下基本内容:
- 进程的生命周期
一个进程被fork出来后,进入就绪态;当被调度到获得CPU执行时,进入执行态;如果时间片用完或被强占时,进入就绪态;资源得不到满足时,进入睡眠态(深度睡眠或浅度睡眠),比如一个网络程序,在等对方发包,此时不能占着CPU,进入睡眠态,当包发过来时,进程被唤醒,进入就绪态;如果被暂停,进入停止态;执行完成后,资源释放,此时父进程wait4还未收到它的信号,进入僵死态。即整个周期可能会涉及的状态有:就绪态,执行态,僵死态,停止态,睡眠态。
-
进程执行相关的系统调用
- fork() 调用
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
-
execve() 调用
- 预处理
首先在内核空间分配一个物理页面,然后调用do_getname()从用户空间拷贝文件名字符串。
- 预处理
-
调用主体函数do_execve()
-
我们既然要执行参数中给的二进制文件,首先需要打开文件,获取文件句柄file
-
然后我们需要一个linux_binprm结构体去保存函数具体的参数信息,包括文件名,argv,envp,还会将文件前128字节读到linux_binprm.buf中。
-
因为可执行文件的种类很多,比如elf,a.out等格式。我们需要从内核全局linux_binfmt队列中找到一个能够处理参数中所给的可执行文件的linux_binfmt结构,具体就是依次试用linux_binfmt结构中各自的load_binary()函数。
-
-
可执行文件的装载和投运(a.out为例)
-
与过去决裂,释放用户空间。
既然是要执行参数中给定的二进制文件,就需要放弃可能从父进程继承下来的用户空间,而使用本进程自己的用户空间。因此,需要检查是否与父进程通过指针共享用户空间,还是之前复制父进程用户空间。如果通过指针共享,说明本进程本身没有自己的用户空间,之前称为“进程”不合适,应该称作线程,就直接申请进程用户空间。如果复制父进程的用户空间,这是就需要全部释放。 -
装载可执行文件数据段代码段
这时可以将可执行文件装入进程的用户空间了,这时分两种情况:-
可执行文件不是"纯代码",需要通过do_brk()扩展数据段+代码段大小的空间,然后通过read()读取文件内容到用户空间
-
否则,如果文件系统提供mmap(),并且数据段和代码段长度与页面大小对齐,直接通过文件映射读取到用户空间,否则,通过1方法读取。
-
-
装载可执行文件堆栈段和bss段
用户空间堆栈区顶部当然是用户虚存空间顶部,即TASK_SIZE,为3GB,虚存地址为0xC000 0000的位置。
这里主要是设置用户堆栈区,包括envp[],argv[]以及argc
- start_thread()
-
代码实测
-
编写 fork() 和 execve() 的测试代码并进行测试
编写以下两个文件
- helloworld 文件
// file helloword.c #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { printf("Hello world!\n"); pid_t pid = getpid(); printf("pid of helloworld from helloword is %d\n", pid); return 0; } // end helloword.c
- start_process 文件
// file start_process.c #include<stdio.h>