第3章 进程管理
进程是Unix操作系统抽象概念中最基本的一种,进程管理是操作系统的心脏所在。
3.1 进程
- 进程:处于执行期的程序以及相关的资源的总称。
- 线程:在进程中活动的对象,拥有独立的程序计数器、进程栈和一组进程寄存器。
内核调度的对象是线程而不是进程。
存在包含多个线程的多线程程序
存在两个或多个不同进程执行同一程序,并且可以共享资源
现代操作系统中,进程提供两种虚拟机制:
虚拟存储器:给进程假象好像自己在独享处理器,实际是很多进程在分享一个处理器。
-
虚拟内存:让进程觉得自己拥有整个系统的内存资源
注意:线程之间可以共享虚拟内存,但每个都拥有自己的虚拟处理器。
创建进程:
fork()系统调用:通过复制一个现有进程来创建一个全新的进程。(进程在创建它的时候开始存活)
父进程调用fork()产生子进程
在返回点:父进程恢复执行,子进程开始执行
fork()返回两次:一次回到父进程,一次回到子进程
exec():创建新的地址空间,把新的程序载入其中。
exit():退出执行,终结程序并将其占用的资源释放掉。
wait4():父进程调用它查询子进程是否终结。
3.2 进程描述符及任务结构
进程列表存放在任务队列(双向循环链表)中,每一项都是进程描述符(类型为task_struct),包含了一个进程的所有信息:
- 它打开的文件
- 进程的地址空间
- 挂起的信号
- 进程的状态
- 其他更多信息
分配进程描述符
以前:各个进程的task_struct存放在它们内核栈的尾端,只通过栈指针就能计算出它的位置,避免使用额外的寄存器。
现在:用slab分配器动态生成task_struct,在内核栈的尾端创建一个新的结构struct thread_info,thread_info有一个指向进程描述符的指针(task域中存放指向实际task_struct的指针)
进程描述符的存放
进程唯一的标识值PID(类型pid_t,实际是int类型),PID最大值默认设置为32768(short int型最大值,最大值可更改),PID最大值也表示系统中允许同时存在的进程的最大数目。
内核访问任务的速度关键在于找到当前进程的进程描述符task_struct,通过current宏实现:
current宏针对不同硬件体系结构:
寄存器富余的直接拿出一个专门的寄存器存放指向当前进程task_struct的指针。
寄存器不富余的,在内核栈尾端创建thread_info结构,计算偏移间接找到task_struct。
x86中,current把栈指针的后13位屏蔽掉来计算thread_info结构的偏移,通过current_thread_info()函数实现:
movl $-8192,%eax //栈大小8KB,4KB->4096
andl $esp,%eax //计算偏移
current_thread_info() ->task //current从thread_info的task域中提取task_struct地址
进程状态
进程描述符stat域描述了进程当前状态,进程状态只有五种,必为其一:
TASK_RUNNING(运行):进程正在执行/在运行队列中等待执行。
TASK_INTERRUPTIBLE(可中断):进程正在睡眠(被阻塞),等待条件达成就变为执行状态,也可被信号提前唤醒。
TASK_UNINTERRUPTIBLE(不可中断):对信号不作响应,其余与可中断状态相同。
__TASK_TRACED:被其他进程跟踪的进程。
__TASK_STOPPED:进程停止执行
设置当前进程状态
set_task_state(task,state); //将task状态设置为state
必要时会设置内存屏障来强制其他处理器作重新排序,否则等价于:task->state = state
进程上下文
可执行程序代码是进程的重要组成部分,从可执行文件载入到进程的地址空间执行。
陷入内核:内核代表进程执行,从用户空间到内核,此时内核处于进程上下文,current在此上下文中是有效的,内核退出->恢复上下文。
系统调用和异常处理程序是对内核明确定义的接口
进程家族树
进程之间存在明显的继承关系,所有进程都是init进程(PID为1)的后代,系统启动的最后阶段就是启动init进程。
每个进程必有一个父进程,也有0或多个子进程,进程间的关系存放在进程描述符中(task_struct):
parent指针:指向父进程tast_struct
children:子进程链表
init进程的进程描述符是作为init_task静态分配的。
struct tsak_struct *task;
for (task = current;task != &init_task;task = task->parent)
//一直找寻当前进程的父进程,直到找到init进程
实际上从系统的任意一个进程出发都能找到任意指定的其它进程。
获取任务队列的下一个进程:list_entry(task->tasks.next,struct task_struct,tasks)
获取任务队列的前一个进程:list_entry(task->tasks.prev,struct task_struct,tasks)
//通过next_task(task)宏和prev_task(task)宏实现
for_each_process(task)宏可依次访问整个任务队列,每次访问都指向链表中的下一个元素,但遍历所有进程代价大。
3.3 进程创建
一般产生进程的机制:
- 在新的地址空间里创建进程
- 读入可执行文件
- 开始执行
UNIX中:
fork():通过拷贝当前进程来创建一个新的子进程。子父进程区别:PID(进程唯一)、PPID(父进程进程号)、某些资源统计量。
exec():读取可执行文件并将其载入新的地址空间开始运行。
写时拷贝
传统的fork()系统调用直接把所有资源复制给新进程,这样效率低。
Linux使用写时拷贝页:推迟甚至免除拷贝数据,父进程和子进程共享一个拷贝(只读),在需要写入的时候,数据才会被复制。
fork()的实际开销:
复制父进程的页表
给子进程创建唯一的进程描述符
fork()
clone()系统调用通过一系列参数标志来指明父子进程需要共享的资源
创建进程:大部分靠do_fork()
fork()、vfork()、_clone()库函数根据各自需要的参数标志去调用clone()->do_fork()完成创建的大部分工作->copy_process()函数返回指向子进程的指针->回到do_fork(),子进程被唤醒先执行。
vfork()
除了不拷贝父进程的页表项,与fork()功能相同。
子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程堵塞直到子进程退出或执行exec().
子进程不能向地址空间写入,理想情况下,最好不用vfork()。
3.4 线程在Linux中的实现
线程机制:现代编程技术中常用的一种抽象概念,提供在同一程序内共享内存地址空间运行的一组线程,还可共享打开的问价和其他资源。支持并发程序设计技术。
从内核角度来说,并没有线程的概念,都是进程,只是线程可以和其他进程共享资源,每个线程都有自己的task_struct。
创建线程
线程创建和进程创建类似,只是在调用clone()时需要传递一些参数标志来指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0);
父子共享地址空间、文件系统资源、文件描述符和信号处理程序。
普通fork()实现:clone(SIGCHLD,0);
vfork()实现:clone(CLONE_VFORK | CLONE_VM | SIGCHLD,0);
传递的参数标志决定了新创建进程的行为方式和父子进程之间共享资源的种类。书上有具体参数标志的含义。
内核线程
内核线程:独立运行在内核空间的标准进程,没有独立的地址空间,只在内核空间运行,可以被调度或抢占。
ps -ef //看到内核线程
内核线程只能由内核线程创建,都是从kthreadd内核线程中衍生出来的新内核线程。
创建新内核线程:kthread_create() 新进程处于不可运行状态
明确唤醒进程,否则不会主动运行:wake_up_process()
让线程运行起来:kthread_run()
线程自己调用退出:do_exit()
其他调用使线程退出:kthread_stop() 传递的参数为kthread_create()函数返回的task_struct结构的地址
3.5 进程终结
进程终结时:内核释放它占有的资源并告知其父进程。
1、清理工作
进程的析构:进程调用exit():
- 显示调用
- 隐式的从某个程序的主函数返回
- 当进程接受到它既不能处理也不能忽略的信号或异常时,可能被动的终结。
进程终结:大部分靠do_exit()
do_exit()永不返回
释放掉与进程相关联的资源,进程不可运行并处于EXIT_ZOMBIE退出状态
占用的所有内存:内核栈、thread_info结构和tast_struct结构。
此时进程存在唯一目的就是向父进程提供信息。
2、删除进程描述符
do_exit()后,系统还保留了它的进程描述符,唯一目的就是向父进程提供信息,当父进程获得已终结的子进程的信息后或者通知内核那是无关信息后,子进程的进程描述符才被释放。
wait()这一族函数都是通过唯一的系统调用wait4()实现的,标准动作是将调用它的进程挂起,直到其中一个子进程退出。
会返回该子进程PID,调用该函数时提供的指针会包含子函数退出时的退出代码。
释放进程描述符:release_task()
释放进程内核栈、thread_info结构和tast_struct结构
孤儿进程造成的进退维谷
如果父进程在子进程之前退出,必须给子进程找到一个新的父亲,否则这些孤儿进程在退出时将永远处于僵死状态,耗费内存。
解决:寻父:
do_exit() -> exit_notify() -> forgrt_original_parent() -> find_new_reaper()
如果不行就让init做它们的父进程
遍历子进程为它们设置新的父进程:
子进程链表
子进程被跟踪时,父进程设定为调试进程,此时父进程退出了,要为它和它的兄弟新找一个父进程
ptrace子进程链表:在其中搜索相关兄弟进程
init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。