第三章 进程管理

3.1 进程

进程就是处于执行期的程序

进程就是正在执行的程序代码的实时结果

线程:在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。

内核调度的对象是线程,不是进程,对于Linux而言,线程只是一种特殊的进程。

进程提供两种虚拟机制:虚拟处理器和虚拟内存

虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。

虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。

在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器

进程是处于执行器的程序以及相关资源的总称

该系统调用通过复制一个现有进程来创建一个全新的进程

Fork()系统调用返回两次:一次回到父进程,一次回到新产生的子进程

通过exec()这组函数可以创建新的地址空间,把新的程序载入其中

通过exit()退出执行,会终结进程并将其占用的资源放掉。

父进程通过wait4系统调用调查子进程是否终结,使进程有了等待特定进程执行完毕的能力

进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()

3.2 进程描述符及任务结构

内核把进程的列表存放在叫做任务队列的双向循环链表中,链表中的每一项都是类型为task_struct、称为进程描述符的结构,定义在<linux/sched.h>文件中

进程描述符中包含的数据能完整地描述一个正在执行的程序

3.2.1 分配进程描述符

Linux通过slab分配器分配task_struct结构

目的:达到对象复用和缓和着色

在栈底或栈顶创建一个新的结构struct_thread_info,在<asm/thread_info.h>下定义

每个任务的thread_info结构在它的内核栈的尾端分配,结构中task域中存放的是指向该任务实际task_struct的指针

3.2.2 进程描述符的存放

内核通过一个唯一的进程标志值或PID来标识每个进程

PID是一个数,表示为pid_t隐含类型,实际上是一个int类型,最大值默认为32768

内核把每一个进程的PID存放在它们各自的进程描述符中

最大值:系统允许同时存在的进程的最大数目,通过修改/proc/sys/kernel/pid_max来提高上限

访问任务通常需要获得指向其task_struct的指针,内核中大部分处理进程的代码都是直接通过task_struct进行的

通过current宏查找到当前正在运行的进程的进程描述符

Current把栈指针的后13个有效位屏蔽掉,用来计算thread_info的偏移,通过current_thread_info()函数来完成

Current再从thread_info的task域中提取并返回task_struct的地址

3.2.3 进程状态

系统中的每个进程都必然处于五个状态的一种:运行、可中断、不可中断、被其他进程跟踪的进程、停止

3.2.4 设置当前进程状态

使用set_task_state(task,state)函数,该函数将指定的进程设置为指定的状态

3.2.5 进程上下文

可执行程序代码:从一个可执行文件载入到进程的地址空间执行

当一个进程执行了系统调用或触发了某个异常,就陷入内核空间,我们称其“代表进程执行”并处于进程上下文中,在此上下文current宏是有效的

系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过接口

3.2.6 进程家族树

所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本,完成系统调用的整个过程

每个进程必须有一个父进程,每个进程都可以拥有零个或多个子进程,拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct、叫做parent的指针,还包含一个称为children的子进程链表

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

任务队列:双向循环链表

获取链表下一个进程:next_task(task)

获取链表前一个进程:prev_task(task)

依次访问整个任务队列的能力:for_each_precess(task),每次访问,任务指针都指向链表中的下一个元素

3.3 进程创建

两个函数:fork() evec()

Fork()通过拷贝当前进程创建一个子进程

子进程和父进程的区别:PID、PPID和某些资源和统计量

Exec函数负责读取可执行文件并将其载入地址空间开始运行

3.3.1 写时拷贝

Fork()使用写时拷贝页实现,可以推迟甚至免除拷贝数据,内核让父进程和子进程共享同一个拷贝

资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享,使地址空间上的页的拷贝被推迟到实际发生写入的时候进行

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

3.3.2 fork()

通过clone()系统调用实现fork()

Fork(),vfork(),_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()

Do_fork()定义在kernel/fork.c文件中,调用copy_precess函数,让进程开始运行

3.3.3  vfork()

除了不拷贝父进程的页表外,功能与fork()相同,通过向clone()系统调用传递一个特殊标志来进行

3.4 线程在Linux中的实现

线程机制:提供了在同一程序内共享 内存地址空间运行的一组线程,可以共享打开的文件和其他资源,在多处理器系统上,能够保证真正的并行处理

线程仅仅被视为一个与其他进程共享某些资源的进程,每个线程都拥有唯一隶属自己的task_struct,看起来就像一个普通的进程

对于Linux来说,它只是一种进程间共享资源的手段

3.4.1 创建线程

在调用clone()的时候需要传递一些参数标志来指明需要共享的资源

父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序

传递给clone()的参数标志决定了 新创建进程的行为方式和父子进程之间共享的资源种类,在<linux/sched.h>中定义

3.4.2 内核线程

内核线程没有独立的地址空间,指向地址空间的mm指针被设置为NULL,它只在内核空间中运行,从来不切换到用户空间去,可以被调度,可以被抢占

运行ps –ef命令,可以看到内核线程

内核线程只能由其他内核线程创建,kthreadd内核进程中衍生出所有新的内核线程,在<linux/kthread.h>中申明有接口

新的任务是由kthread内核进程通过clone()系统调用而创建的。新的进程将运行threadfn函数,给其传递的参数为data。进程被命名为namefmt

新创建的进程处于不可运行状态,如果不通过wake_up_process()唤醒它,它不会运行

创建一个进程并让它运行,可以通过kthread_run()来达到

内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数为kthread_create函数返回的task_struct结构的地址

3.5 进程终结

当一个进程终结时,内核必须释放它所占有的资源并告知其父进程

进程的终结发生在进程调用exit()系统调用时。当进程接受到它既不能处理也不能忽略的信号或异常时,它还可能被动地终结

终结大部分靠do_exit()来完成(定义于kernel/exit.c)

Do_exit()永不返回

之后,进程不可运行并处于EXIT_ZOMBIE退出状态,存在的唯一目的就是向它的父进程提供新消息,父进程检索到信息后,由进程所持有的剩余内存被释放

3.5.1 删除进程描述符

系统还保留了进程描述符,这样做可以让系统有办法在子进程终结后仍能获得它的信息

进程终结时所需的清理工作和进程描述符的删除被分开执行。

在父进程获得已终结的子进程信息后,子进程的task_struct才被释放

Wait():通过唯一的系统调用wait4()来实现,它的标准动作是挂起调用它的进程,直到其中一个子进程退出,此时函数返回该子进程的PID,调用该函数时提供的指针会包含子函数退出时的退出代码

当最终需要释放进程描述符时,release_task()会被调用

3.5.2 孤儿进程造成的进退维谷

父进程在子进程之前退出,必须保证子进程找到新的父进程,否则这些孤儿进程会永远处于僵死状态,白白的耗费内存

解决方法:给子进程在当前进程组内找到一个线程作为父亲,如果不行,就让init做它们的父进程。

在do_exit()中会调用exit_notify(),该函数会调用forget_original_parents(),而后者调用find_new_reaper()来执行寻父过程

遍历所有子进程并为它们设置新的父进程

遍历了两个链表:子进程链表和ptrace子进程链表

解决孤儿问题的方法:在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程

一旦系统为进程成功的找到和设置了新的父进程,就不会再有出现驻留僵死进程的危险了,init进程会例行调用wait()来检查子进程,清除所有与其相关的僵死进程。

04-21 13:08