进程起源:
道生一(start_kernel,内核启动函数),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)。
fork.c
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main(int argc,char * argv[]) { int pid; /* fork another process */ pid = fork(); // fork是用于创建子进程的系统调用 if (pid < 0) { /* error occurred */ fprintf(stderr,"ForkFailed!"); exit(-1); } else if (pid == 0) { /* child process */ printf("This is ChildProcess!\n"); } else { /* parent process */ printf("This is ParentProcess!\n"); /* parent will wait for the child tocomplete*/ wait(NULL); printf("Child Complete!\n"); } }
编译、运行fork.c:
cgh@ubuntu:~/test$gcc fork.c #编译 cgh@ubuntu:~/test$./a.out #运行 This is ParentProcess! This is ChildProcess! Child Complete!
fork.c中有一个if else的条件判断语句,main函数执行时if else中只能有一个满足执行条件。
1. 当pid < 0时屏幕打印"Fork Failed!"。
2. 当pid == 0时屏幕打印"This is Child Process!\n"。
3. 其他情况屏幕打印"This is Parent Process!\n"和"ChildComplete!\n"
正常情况下只能打印以上三种情况中的一种,然而运行fork.c时屏幕上同时打印了2、3两种情况。
fork.c打破了if else条件分支的结构?当然不是,fork系统调用在父进程和子进程各返回一次。Fork之后这段代码同时在两个进程中执行了。
在子进程中fork()的返回值是0,pid = 0,执行了else if中的代码。在父进程中fork()的返回值是子进程的pid(> 0),执行了else中的代码。
接下来分析创建一个新进程在内核中的执行过程(下面的看不懂可以跳过,第三讲有详细分析)
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork这个内核函数来实现进程创建的;
Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:
复制一个PCB——task_struct
1. err = arch_dup_task_struct(tsk, orig);
要给新进程分配一个新的内核堆栈
1. ti = alloc_thread_info_node(tsk, node);
2. tsk->stack = ti;
3. setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
要修改复制过来的进程数据,比如pid、进程链表等等都要改改吧,见copy_process内部。
从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?在copy_process中的copy_thread设定。
1. *childregs = *current_pt_regs(); //复制内核堆栈
2. childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
3. p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
4. p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址