一、目的
本文将介绍linux下创建进程的过程,不同于其他操作系统,linux创建进程的实现方式有自己的显著特点,具体的实现代码可以在kernel/fork.c中找到。
linux没有线程和进程的区别,但是父进程在创建子进程时,可以根据参数标志让子进程选择性的继承父进程的资源;如果指定子进程继承父进程的地址空间、打开的文件、文件系统、信号量等资源,那么此时的子进程就相当于父进程的“线程”。虽然linux不直接提供线程,但是通过参数标志的方式间接提供被称为“轻量级进程”的线程,显得更加灵活和简单。
此外,由于采用了写时拷贝技术,避免了大量页的拷贝操作,所以linux下创建进程的代价很小。
二、系统调用接口
进程创建的系统调用接口有fork()、vfork()、clone();创建内核进程的接口是kernel_thread()。这些接口函数最终都是调用do_fork()实现具体的进程创建工作,区别在于参数标志不同。
fork()没有设置参数标志,相当于指定了默认参数标志;vfork()设置了CLONE_VFORK、CLONE_VM参数标志,指定父进程共享地址空间(注意与写时拷贝的区别),且子进程创建完成后,父进程睡眠,子进程优先运行;子进程运行完毕后通知父进程继续运行,由于写时拷贝技术的发明,vfork()的优点已经不是很明显了;clone()原型声明中给出了参数标志clone_flags,由用户设置父进程共享的资源;kernel_thread()是内核接口,只能在内核编程中使用,并且设置了CLONE_VM、CLONE_UNTRACED参数标志;由于内核进程在内核态运行,并且只能由内核进程创建,所以内核进程不需要独立的地址空间,因此内核进程指定了CLONE_VM参数标志。
由于这些接口都是调用do_fork()函数实现,差异仅在于参数标志,并且上面已经给出了差异分析,所以下面就不一一讲述各个接口的具体实现。fork()使用了默认的参数标志,属于创建子进程的标准操作,所以下面讲述fork()的实现过程。
三、参数标志
参数标志指定了父子进程之间共享的资源,这些资源之间可能存在冲突,也可能存在依赖关系。例如:CLONE_NEWUSER和CLONE_FS之间冲突,不能既要求为子进程创建新的命名空间,又要求子进程继承父进程的文件系统信息。CLONE_THREAD和CLONE_SIGHAND之间存在依赖关系,如果指定父子进程在同一线程组,那么父子进程必须共享信号处理函数。
下面列出存在冲突的参数标志组合:
CLONE_NEWNS和CLONE_FS:指定子进程拥有新的命名空间,那么就不能继承父进程的文件系统信息。
CLONE_NEWUSER和CLONE_FS:指定子进程拥有新的用户空间,那么就不能继承父进程的文件系统信息。该组合会导致一个系统漏洞,可以让一个普通用户窃取到root的权限。
下面列出存在依赖关系的参数标志组合:
CLONE_THREAD依赖CLONE_SIGHAND:父子进程在同一进程组,那么需要共享信号处理函数。
CLONE_SIGHAND依赖CLONE_VM:父子共享信号处理函数,那么需要共享地址空间。
四、fork()实现过程
fork()->do_fork()首先调用copy_process()创建新进程,然后调用wake_up_new_task()将进程放入运行队列中并启动新进程。
copy_process()主要完成以下工作:
1、参数标志检测,处理存在冲突或者依赖关系的参数标志组合;
2、不能为init进程创建兄弟进程,防止这些兄弟进程在退出时变为僵尸进程(进程0不会处理僵尸进程)。如果当前进程为init,则不能指定CLONE_PARENT参数标志;
3、调用dup_task_struct()为新进程创建一个task_struct结构、一个thread_info结构和内核栈(内核栈的末端就是thread_info结构,所以这两者共享了页结构),此时父子进程的描述符、内核栈内容完全一致(除了thread_info相关的内容);在内核栈与thread_info之间写入“内核栈防越界魔数”STACK_END_MAGIC(0x57AC6E9D);
4、判断当前用户所拥有的进程数是否超出最大值;
5、调用copy_flags()设置新进程的flags成员,表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0,表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置;
6、初始化新进程的孩子链表和兄弟链表指针指向新进程自己(即无孩子和兄弟进程);
7、设置新进程的时间为0;设置进程创建时间为当前系统时间;
8、进行调度程序相关的设置,设置运行状态为TASK_RUNNING,继承父进程的优先级值;
9、根据参数标志拷贝或共享文件系统、信号量、信号处理函数、打开的文件、地址空间、命名空间和cpu寄存器组等资源;默认情况下,子进程拷贝父进程的资源;
10、调用alloc_pid()为新进程分配一个有效的PID;
11、根据参数标志,设置子进程与父进程的关系以及与进程组的关系;
12、返回新进程的进程描述符。
总而言之,fork()的作用就是“复制”一份父进程及其资源,搭建好了一个进程运行时所需的必要环境;当调用exec()函数族时,才会将进程地址空间的内容替换为子进程的可执行文件。
五、总结
由于采用了参数标志指定父子进程之间共享的资源,所以linux没有刻意区分进程和线程,实现方式上更加灵活和简便;并且采用了写时拷贝技术,创建进程的代价也大大减小了。基于以上特性,linux创建进程的方式显得更加“优美”。
版权声明:
原创作品,如非商业性转载,请注明出处;如商业性转载出版,请与作者联系。