Linux进和创建——写时拷贝机制

所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程会读取系统的初始化脚本(initscript)并执行其他相关的程序,最终完成整个系统的启动过程。

内核把进程(在Linux中进程又称任务)存放在任务队列中。任务队列是双向循环链表。链表中的每一项数据的类型都是task_struct,task_struct就是所谓的进程描述符的结构。进程描述符中包含一个进程的所有信息。进程描述符所包含的数据能完整地描述一个正在执行的程序(程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称)如它打开的文件,进程的地址空间,挂起的信号,进程的状态等等。

可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,它就会陷入内核空间,此时,内核“代表进程执行”并处于进程上下文中。在此期间除非有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出时,程序恢复,在用户空间会继续执行。系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行。对内核的所有访问都必须通过这些接口。

系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程称为兄弟。进程间的关系存放在进程描述符中。每个task_struct(进程描述符结构)都包含一个指向其父进程task_struct的名为parent的指针,还包含一个称为children的子进程链表。

init进程的进程描述符是作为init_task静态分配的

许多操作系统在创建新进程时,都会首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。而LInux则不是,它将这两个步骤分解到两个单独的函数中去执行:fork()和exec(),首先fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID(每个进程唯一),PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID),和某些资源和统计量。exec()函数负责读取可执行文件并将其载入地址空间开始运行。

创建子进程是通过fork()方法完成的,而fork()是使用写时拷贝(copy-on-write)机制。这是一种可以推迟甚至免除拷贝数据的技术。当执行fork()方法创建进程时,内核并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。

只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。即是说,资源的复制只有在需要写入时才进行,在此之前,都是以只读方式共享。
如果fork()之后立即调用exec()那么页就根据无须复制了,这种情况下,页根本不会被写入。

fork()的实际开销就是复制父进程的页表和给子进程创建唯一的进程描述符。

Linux通过clone()系统调用实现fork()。clone系统调用会通过一系列的参数标志来指明父、子进程需要共享的资源。clone()会去调用do_fork()。
do_fork会完成创建中的大部分工作,它会调用copy_process()函数,然后让进程开始运行。copy_process()函数完成以下工作:
(1)首先调用dup_task_struct为新进程创建一个内核栈、thread_info结构和task_struct(进程描述符的结构),这些值与当前进程的值相同。此时的子进程与父进程的描述符是完全相同的。
(2)检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
(3)子进程开始着手使自己与父进程区别开来。子进程描述符内的许多成员都被清0或设为初始值。那些不是继承而来的描述符成员,主要是统计信息。此时task_struct中仍然有很多数据都依然未被修改。
(4)将子进程的状态设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
(5)copy_process()调用copy_flags()以更新task_struct的flags 成员,表明进程是否有超级用户权限的PF_SUPERPRIV标志被清0,表明进程没有调用exec()函数的PF_FORKNOEXEC标志被设置。
(6)调用alloc_pid为新进程分配一个有效的PID。
(7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则这些资源对每个进程是不同的,因此被拷贝到这里。
(8)最后,copy_process()做收尾工作并返回一个指向子进程的指针。再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程会被唤醒并让其投入运行。

一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。

谢谢阅读。

猜你喜欢

转载自blog.csdn.net/weixin_40763897/article/details/90734876