Linux 进程创建笔记
一般的操作系统如果想要 spwan 一个进程,一般会经过几个步骤,先在新的地址空间中创建进程,然后读入可执行文件,最后开始执行这个可执行文件。Linux 下,这几个阶段分别由两个函数独立完成,fork() 负责创建一个子进程。exec() 则负责读入可执行文件并开始运行。
fork() 的作用就进行进程复制。Linux 下一共实现了三种类似的功能,分别是 fork(), vfork() 和 clone () 。
Fork()
Fork() 函数是一个重量级的进程复制,在逻辑上而言,子进程在最初应该具有和父进程一样的内存页,建立父进程完整的副本信息。然而基于 “创建子进程后,大概率会执行同父进程完全不同的应用程序” 这个现象后,通常对子进程都是用 copy on write 技术,即内核此时并不会复制整个进程的地址空间,而是让父进程和子进程共享同一个拷贝,子进程拥有了一个同父进程一样的页表 ,这样,子进程和父进程就拥有了一样的虚拟地址空间和实际物理地址空间的对应关系。他们指向同样的物理内存页。不过,由于他们只是短暂的共享了数据,所以他们并不能修改彼此之间的页。
Vfork()
vfork() 同 fork() 类似,但他显式的没有创建父进程的副本,子进程和父进程之间可以直接进行数据共享。 vfork() 的设计是为了子进程生成后直接进行 exec() 系统调用设计的。显然这种方式快于 fork() ,但是,由于 COW 的出现,fork() 和 vfork() 在速度上已经没有明显的区别。在 vfork() 生成子进程后,父进程被阻塞,直到子进程退出或者执行 exec() 调用。由于 fork() 已经实现了 COW 技术,所以现在基本上都在使用 fork()。
Clone()
clone()主要用于产生线程,可以对父子进程之间进行数据共享,复制。做到精确的控制。
fork() vfork() clone() 他们的实现的具体细节各有不同,但最后负责进程复制的函数都是一样的,为 do_fork() 函数
do_fork() 函数
do_fork 函数先调用 copy_progress() 函数用来生成新的进程,copy_progress() 根据标志指定需要重用的父进程的数据,生成新的子进程。在子进程生成以后,需要对子进程获得其 PID。如果是使用 vfork() 函数进行调用,那么父进程会进入睡眠状态,直到子进程调用 exec() 或者退出。通过这样的方式,保证了父进程和子进程彼此之间对共享的变量互不干扰。然后子进程把其自身的 task_struct 添加到调度器队列,进行进程调度。对于新fork 的子进程,调度器会让其有较高的几率先运行。如果子进程在父进程前进行运行,那么会降低复制内存页带来的额外开销。
copy_progress() 用来进行资源的复制,生成子进程的过程。它首先为新的子进程创建一个自己的内核栈,thread_info和 task_struct 等信息,这些值同父进程的值相同,在这个时候,父进程和子进程的描述符完全相同。
然后检查需要分配的资源数量,确保没有超出资源限制,不会出现资源不足。如果出现了资源不足的现象,那么会调用 sched_fork() 函数,让调度器重新对新的进程进行设置。
接着,为了区别开子进程和父进程,子进程开始对进程描述符内的信息进行修改。
设置子进程的状态为 UNINTERRUPTIBLE 类型,确保其现在不会投入运行中。
子进程继续复制父进程的一些内核资源,对于这些资源,都有相应的标志位用来表示资源状态。如果标志位为1,那么表示两个进程共享这些资源,反之,说明子进程拥有了自己的资源信息
当做完这些工作后,会进行一些扫尾工作,然后返回一个指向子进程的指针。
以上,就是 Linux 进程创建的大概过程和一些细节步骤。