目前正在通读《Linux内核设计与实现》一书,本文是对第三章的总结。 初次接触Linux的内容,难免是囫囵吞枣,一知半解。从android的角度来说,本章的主要内容有:
1.进程的创建,fork的含义
在跟读罗升阳大神的android源码解析时,在看到创建新进程那一段时,都会很懵逼,为啥调用了Process.fork之后,新进程也能接着执行fork语句之后的代码?
这里摘一下书中的原话,供大家解惑和品读:
进程在创建它的时刻才开始存活。在Linux系统中,这通常是调用fork()系统的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork的进程称为父进程,新产生的进程称为子进程。在该调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。在现代Linux内核中,fork()实际上是由clone()系统调用实现的。
那fork系统是如何做到这种返回两次逻辑的呢?这一章里还并未给出具体实现的解析,得另寻书籍进行解答。姑且就先接受这个既定事实,对android来说已经够用了。
创建进程(spawn)的机制是:首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。
Unix的特别之处是:它把这个步骤分解到两个函数中去执行:fork()和exec()。
fork()通过拷贝当前进程创建一个子进程。子进程和父进程的区别仅仅在于PID(进程ID)、PPID(父进程ID)和某些资源和统计量。因此,fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
exec()函数负责读取可执行文件并将其载入地址空间开始运行。
Linux的fork()使用写时拷贝(copy-on-write)页实现。(备注:写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。这种技术使地址空间上的页拷贝被推迟到实际发生写入的时候才进行,在此之前,只是以只读方式共享。地址空间里常常包含数十兆的数据。)
Linux内核通常把进程也叫做任务(task)。
Linux中的进程列表是通过队列来存放的。
task_struct类型的结构,也称为进程描述符
Linux通过slab分配器来分配task_struct结构。
进程描述符是以thread_info结构存放在进程的内核栈尾端的。
2.进程和线程的关系
Linux把所有的线程都当做进程来实现。从内核角度来说,它并没有线程这个概念。线程仅仅被视为一个与其他进程共享某些资源的进程,如地址空间。
3.进程的家族体系
Unix系统的进程之间存在一个明显的继承关系,在Linux系统中也是如此。
所有的进程都是PID为1的init进程的后代。
内核在系统启动的最后阶段启动init进程。
系统中每个进程都必有一个父进程,也可以拥有零个或者多个子进程。
同父的进程被称为兄弟。
4.进程终结
进程终结时所需的清理工作和进程描述符的删除是分开执行的。
4.1第一阶段
释放进程相关联的所有资源,进入EXIT_ZOMBIE退出状态
主要靠do_exit()完成。
在释放资源后的最后阶段调用exit_notify()向父进程发送信号,同时想办法托孤。
调用schedule()切换到新的进程。
此时进程所剩的内存就是内核栈、thread_info结构和task_struct结构,残喘的唯一目的就是向它的父进程提供信息。
托孤的策略是:在所在的线程组里找一个线程作为养父,如果没有,就让init作为子进程的养父。
4.2 第二阶段
由父进程删除该进程的进程描述符
在第一阶段之后,该进程已经处于僵死不能再运行状态。
父进程通过wait()系统调用来收集子进程的信息。
在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。