第3章 进程管理
3.1 进程
1、进程
进程就是处于执行期的程序。
进程包括:
- 可执行程序代码
- 打开的文件
- 挂起的信号
- 内核内部数据
- 处理器状态
- 一个或多个具有内存映射的内存地址空间
- 一个或多个执行线程
- 用来存放全局变量的数据段
- ……
实际上,进程就是正在执行的程序代码的实时结果
2、执行线程
- 简称线程,是在进程中活动的对象。
- 每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。
- 内核调度的对象是线程,而不是进程。
进程提供两种虚拟机制:
虚拟处理器和虚拟内存。
在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。
3、fork系统调用 该系统调用通过复制一个现有进程来创建一个全新的进程。 在返回点的相同位置上 fork()实际上是由clone()系统调用实现的。
3.2 进程描述符及任务结构
1、内核把进程的列表存放在叫做任务队列的双向循环链表中。
链表中的每项都是类型为task_ struct称为进程描述符的结构,该结构定义在<linux/sched.h>文件中。
2、进程描述符中包含一个具体进程的所有信息。
3、进程描述符中包含:
- 它打开的文件
- 进程的地址空间
- 挂起的信号
- 进程的状态
- 其他更多信息
3.2.1 分配进程描述符
1、Linux以通过slab分配器分配task_ struct结构,这样能达到对到对象复用和缓存着色的目的。
2、struct_ thread_ info在文件<asm/thread_ info.h>中定义。
3、每个任务的thread_info结构在他的内核栈的尾端分配。
3.2.2 进程描述符的存放
1、内核通过一个唯一的进程标识值或PID来标识每个进程。
- PID最大值的限制在<linux/threads.h>中定义
- 系统管理员可以通过修改/proc/sys/kernel/pid_max来提高上限。
2、硬件体系结构不同,该宏的实现也不同。
- 可以拿出―个专门寄存器来存放指向当前进程task_ struct的指针,用于加快访问速度。
- 在内核的尾端创建thread_ info结构,通过计算偏移间接地查找task_ struct结构。
3.2.3 进程状态
进程描述符中state域描述了进程的当前状态。
五种进程状态:
- TASK_ RUNNING (运行):
进程是可执行;它或者正在执行,或者在运行队列中等待执行——进程在用户空间中执行的唯一可能的状态。- TASK_ INTERRUPTIBLE (可中断):
进程正在睡眠,等待某些条件达、- TASK_ UNINTERRUPTIBLE (不可中断):
另一种阻塞状态,处于该状态的进程只有当资源有效时被唤醒,不能通过信号或定时中断唤醒。- TASK_ STOPPED (停止):
进程停止执行;进程没有投入运行也不能投入运行,常见信号:SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU。- TASK_ TRACED:
被其他进程跟踪的进程。
3.2.4 设置当前进程状态
set_ task_ state(task,state)函数:
- 该函数将指定的进程设置为指定的状态,会设置内存屏障来强制其他处理器作重新排序。
- set_ current_ state(task,state)和set_ task_ state(task,state)含义是等同的。
3.2.5 进程上下文
- 内核“代表进程执行”并处于进程上下文中——当一个程序调执行了系统调用或者触发了某个异常,它就陷入了内核空间。
- 在此上下文中current宏是有效的。
- 系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口。
3.2.6 进程家族树
1、Unⅸ系统的进程之间存在—个明显的继承关系。
- 所有的进程都是PID为1的init进程的后代。
- 内核在系统启动的最后阶段启动init进程。
- 该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。
2、init进程的进程描述符是作为init_task静态分配的。
3、for_ each_ process(task)宏提供了依次访问整个任务队列的能力。
3.3 进程创建
产生进程的机制:
其他操作系统中:
- 首先在新的地址空间里创建进程,
- 读入可执行文件
- 最后开始执行。
在linux系统中——使用fork()和exec()函数
- fork()函数:通过拷贝当前进程创建一个子进程。
- exec()函数:负责读取可执行文件并将其载入地址空间开始运行。
3.3.1 写时拷贝
1、Linux的fork()使用写时拷贝页实现——让父进程和子进程共享同一个拷贝。
- 写时拷贝是一种可以推迟甚至免除拷贝数据的技术。
- 资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
3.3.2 fork()
1、通过clone()系统调用实现fork(),clone()去调用do_ fork()。
do_ fork完成创建中大量工作,定义在kernel/fork.c文件中。
2、copy_ process()完成的工作:
- 调用duptaskstruct()为新进程创建一个内核栈、threadinfo结构和taskstruct,这些值与当前进程的值相同,此时,子进程和父进程的描述符是完全相同的。
- 检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
- 子进程着手使自己与父进程区别开来,进程描述符内的许多成员都要被清0或设为初始值,那些不是继承而来的进程描述符成员,主要是统计信息,task_struct中的大多数数据都依然未被修改。
- 子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。
- copyprocess()调用copyflags以更新task_struct的flags成员。
- 调用alloc_pid()为新进程分配一个有效的PID。
- 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
- 最后,copy_ process()做扫尾工作并返回一个指向子进程的指针。
3.3.3 vfork()
1、vfork()与fork()很类似,实现是通过向clone()系统调用传递一个特殊标志来进行。
2、好处是不用拷贝父进程的页表项,不过理想情况下,系统最好不要调用vfork()。
3.4 线程在Linux中的实现
1、线程机制提供了在同―程序内共享内存地址空间运行的―组线程,线程机制支持并发程序设计技术。
2、不同系统中线程机制的比较:
在其他系统中: - 线程被抽象成一种消耗较少的资源,运行迅速的执行单元。 在linux系统中: - 线程只是一种进程间共享资源的手段。
3.4.1 创建线程
1、线程的创建在调用clone()时需要传递一些参数标志来指明所需要共享的资源。
2、clone()参数标志决定新创建进程的行为方式和父子进程之间共享的资源种类。
3.4.2 内核线程
1、内核线程——独立运行在内核空间的标准进程。
2、内核线程和普通的进程间的区别在于内核线程没有独立的地址空间。
3、在Linux系统上运行ps -ef命令,你可以看到内核线程。
4、内核是通过从kthreadd内核进程中衍生出所有新的内核线程来自动处理这一点的,在<linux/kthreadd>中申明有接口。
新的任务是由kthread内核进系统调用程通过clone()而创建的。
5、内核退出机制:
- 内核线程启动后一直运行知道调用do_exit()退出.
- 内核其它部分调用kthread_stop()退出。
3.5 进程终结
1、进程的析构是自身引起的。
2、进程终结会调用do_ exit函数,该函数永不返回。
3、僵死进程存在的唯一目的就是向它的父进程提供信息。
3.5.1 删除进程描述符
1、在linux系统中,进程终结时所需要的清理工作和进程描述符的删除被分开执行。
2、调用release_task函数——最终释放进程描述符。
3.5.2 孤儿进程造成的进退维谷
1、如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态。
2、解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。在doexit()中会调用exitnotify(),该函数会调用forgetoriginalparent(),而后者会调用findnewreaper()来执行寻父。
小结
本章的主要内容——进程
- 进程为何如此重要
- 进程与线程之间的关系
- 进程的一般特性
- Linux如何存放和表示进程
- 如何创建进程
- 如何把新的执行映像装入到地址空间
- 如何表示进程的层次关系
- 父进程又是如何收集其后代的信息
- 进程最终如何消亡。
问题
1、什么是slab分配器?
- slab是Linux操作系统的一种内存分配机制。
- slab分配器是基于对象进行管理的,相同类型的对象归为一类,每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中。
- slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当以后又要请求新的对象时,就可以从内存直接获取而不用重复初始化。
- 对象高速缓存的内存区被划分为多个slab,每个slab由一个或多个连续的页框组成,这些页框中既包含已分配的对象,也包含空闲的对象。
- 在cache和object中加入slab分配器,是在时间和空间上的折中方案。
2、什么是析构?
- 析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,以区别于构造函数。
- 它不能带任何参数,也没有返回值(包括void类型)。
- 只能有一个析构函数,不能重载。
- 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作。所以许多简单的类中没有用显式的析构函数
参考资料
1、slab百度百科
2、析构函数百度百科
3、《Linux内核设计与实现》第三章