我们都知道进程是Linux内核中最为重要的一个抽象概念,那么我们平时在fork一个进程时,该进程究竟是怎么产生的呢?
本篇推送会浅谈一下在进程创建过程中扮演着重要角色的do_fork函数。
内核如何来抽象一个进程
内核通过一个叫做task_struct的结构体来抽象一个进程,该结构体的定义(以内核2.6为例)在include/linux.sched.h中。
截取部分task_struct如下:
上述task_struct属性是我节选出的部分其结构体中的属性,我们从中可以大致了解到标识一个进程的属性大致会有该用以表示该进程所处的状态,进程的标志,以及进程是否被其他进程跟踪,进程锁的深度,进程的优先级,进程的pid,进程的父母,进程的孩子链表,进程所打开的文件描述符表,进程所处的文件系统,进程的信号。。。。等等一堆我们平时可能遇到的和进程相关的东西。
do_fork简单分析
接触linuxC编程的人都知道,创建一个进程我们需要调用fork函数,fork其实又是调用了clone函数来实现的,而clone函数中最关键的函数就是do_fork函数。
在分析do_fork前我们脑海中可以大致想象一下,进程究竟是如何被创建出来的,假如让你来创建一个进程你会咋么做?
我们可以这样去分析,既然原来的进程被抽象成一个task_struct,那么新进程也是一个task_struct只不过它里面的一些属性会不同与原来的task_struct,那么创建一个新进程所要做的工作就是赋值一个与原来进程一样都的task_struct结构,然后然后将新进程的task_struct不同于原来task_struct的属性进行修改即可。
do_fork定义在kernel/fork.c文件中。
在分析该函数之前我们先来分析一下它的函数的各个参数。
参数如下:
1.clone_flags:该参数是此函数中最重要的一个参数,该值中的每个位都代表对子进程task_struct中的每种属性的设置;
2.stack_start:子进程用户态堆栈的开始地址;
3.regs:当系统发生系统调用时,需从用户态切换到内核态,此结构体用来保存此时用户态进程中的通用寄存器中的值,并被存放在内核态堆栈中;
4.stack_size:目前未被使用,通常设为0;
5.parent_tidptr:父进程在用户态下pid的地址;
6.child_tidptr:子进程在用户态下pid的地址;
其中clone_flags的标志位宏定义如下:
举个简单的例子当我们的参数中设置了CLONE_VM这个宏,那么就以为这我们新创建的进程和其父进程要共享VM,当我们设置了CLONE_FILES时意味这父子进程之间共享打开的文件描述符。
do_fork开始执行后首先做的就是为子进程定义一个新的task_struct指针:
struct task_struct *p;
在下来会检查一些clone_flags所不允许的位组合,例如:
- if (clone_flags & CLONE_NEWUSER) { if (clone_flags & CLONE_THREAD) return -EINVAL;
- }
上述中不允许同时既设置了CLONE_NEWUSER标志,还设置CLONE_THREAD标志,这样就会产生错误。
类似上面当一系列的安全检查完毕之后,copy_process函数就登场了,copy_process函数工作流程具体如下:
1)调用dup_task_struct函数为新的进程创建一个内核栈,thread_info结构和task_struct等,当然此时的值都是和父进程完全一样的
dup_task_struct函数定义如下:
2)检查并确保新创建该子进程后,当前用户所拥有的进程数没有超出给它分配的资源限制,代码如下:
3)子进程着手使自己与父进程区别开来,从父进程那继承过来的许多属性都要被清0或设置一个初始值,但task_struct中的大多数数据还是未被修改,部分代码如下:
4)给子进程分配一个CPU,代码如下:
- sched_fork(p, clone_flags);
5) 接着就是子进程拷贝父进程的一些资源,具体如下,调用copy_files函数拷贝父进程打开的文件描述符:
调用copy_fs继承父进程所属的文件系统。
调用copy_signal函数拷贝并设置新的signal_struct,signal_struct包含了大量的进程运行的信息,调用copy_mm函数处理与新进程的内存问题。
调用copy_io函数拷贝父进程的I/O情况:
还有调用copy_namespaces 和 copy_thread等,这里就不在赘述。
6)调用alloc_pid为新进程分配一个pid。
pid = alloc_pid(p->nsproxy->pid_ns);
7)copy_process做一些收尾工作,并返回新进程的task_struct指针,此时再次回到了do_fork,新创建的子进程被唤醒,并让其先投入运行。
总结
关于进程创建的源码理解,我感觉主要抓住俩点即可。***进程被内核抽象成了啥?它的数据结构是咋样的(task_struct)这点我们必须有所认识,第二创建进程最主要的其实就是拷贝父进程的task_struct里的属性,但是关键点是拷贝哪些,哪些又是子进程和父进程所不同的,很简单我们只需要把握住进程创建函数里的clone_flags参数就可以知道怎么拷贝了。