第三章 进程管理

来源:互联网 发布:sql语句exit 编辑:程序博客网 时间:2024/05/07 22:58

1、  进程是Unix操作系统最基本的抽象之一。一个进程就是处于执行期的程序(目标码存放在某种存储介质上)。通常进程还要包含其他资源、像打开的文件、挂起的信号、内核内部数据、处理器状态、地址空间及一个或多个线程,当然还包括用来存放全局变量的数据段等。

2、  线程是在进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。Linux系统的线程实现很特别——它对线程和进程并不特别区分。对Linux而言,线程不过是一种特殊的进程罢了。

3、  在现代操作系统中,进程提供俩种虚拟机制:虚拟处理器和虚拟内存。

虚拟处理器:让进程觉得自己在独享处理器,实际上是多个进程在分享一个处理器。

虚拟内存:让进程在获取和使用内存时觉得自己拥有整个系统的所有内存资源。

 

有趣的是,在线程之间(这里包括在同一个进程中的线程)可以共享虚拟内存,但拥有各自的虚拟处理器。

 

 

一、进程描述符及任务结构

内核把进程存放在任务队列(task list)的双向循环链表。

链表中的每一项都是类型为task_struct、称为进程描述符的结构。

进程描述符包含一个具体进程的所有信息。

1、  分配进程描述符

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

2、  进程描述符的存放

内核通过一个唯一的进程标识符或PID来标识每个进程。PID是一个数,最大值默认为32768

在内核中,访问任务通常需要获得指向其task_struct指针。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要。

方法:

     有的硬件体系结构,如PowerPC可以拿出一个专门的寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。

     而有些像X86这样的体系结构(其寄存器并不富裕),就只能在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。

3、  进程状态

·TASK_RUNNING(运行)——进程是可执行的;它或者正在执行,或者在运行队列中等待执行

·TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。

一旦条件达成,或者接收到信号,内核都会把此进程设为运行状态。

·TASK_UNINTERRUPTIBLE(不可中断)——除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可中断相同。

·TASK_ZOMBIE(僵死)——该进程已经结束了,但其父进程还没有调用wait4()系统调用。

·TASK_STOPPED(停止)——进程停止,进程没有投入运行也不能投入运行。

 

4、设置当前进状态

内核经常需要使用某个进程状态,这时最好使用set_task_state(taskstate)

该函数将指定的进程设置为指定的状态

5、进程上下文

可执行程序代码是进程的重要组成部分。这些代码从可执行文件载入到进程的地址空间执行。

一般程序在用户空间执行,当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。

1、  进程家族树

Linux中,所有的进程都是PID1init进程的后代,内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并指向其他的相关程序,最终完成系统启动的整个过程。

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

 

二、 进程创建

Unix系统进程的创建分俩步fork()exec()

·fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅在于PIDPPID和某些资源和统计量(如挂起的信号,它没有必要被继承)

·exec()函数负责读取可执行文件并将其载入地址空间开始运行。

1、  写时拷贝

写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不赋值整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入时候,数据才会被复制,从而使各个进程拥有各自的拷贝。

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

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

2、  Fork()

Linux通过clone()系统调用实现fork(),这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。Fork()vfork()_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()

过程:

3、  vfork()

vfork()系统调用和fork()功能相同,除了不拷贝父进程的页表项。

三、 线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象,该机制提供了在同一程序内共享内存地址空间运行的一组线程,这些线程还可以共享打开的文件和其他资源。

         Linux实现线程的机制非常独特,它把所有的线程都当做进程来实现。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是该进程和其他一些进程共享某些资源,如地址空间)

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

线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:

Clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND,0)

上面 的代码产生的结果和调用fork()差不多,只是 父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。

1、  内核线程

内核经常需要在后台执行一些操作,这种任务可以通过内核线程完成——独立运行在内核空间的标准进程。内核线程和普通的进程的区别在于内核线程没有独立的地址空间(实际上它的mm指针被设置为NULL),它们只在内核空间运行,从来不切换到用户空间去,内核线程和普通进程一样,可以被调度,也可以被抢占。

 

四、 进程终止

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

         一般说来,进程的析构发生在它调用exit()之后,既可能显示地调用这个系统调用,也可能隐式地从某个程序的主函数返回。该任务大部分靠do_exit()来完成,要做的工作:

至此,与进程相关联的所有资源都被释放掉了(假设进程是这些资源的唯一使用者)。进程不可运行(实际上也没有地址空间让它运行)并处于TASK_ZOMBIE状态,它占用的所有资源就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内核被释放,归还给系统使用。

1、  删除进程描述符

在调用了do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此进程终结时需要的清理工作(即上述的do_exit函数)和进程描述符的删除被分开。

在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。

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

当最终需要释放进程描述符时,release_task()会被调用,用以完成以下工作:

至此,进程描述符和所有进程独享的资源就全部放掉了。

2、  孤儿进程

如果父进程在子进程之前退出,必须有机制来保证子进程找到一个新的父亲,否则这些成为孤儿进程就会在退出时永远处于僵死状态,白白的耗费内存。

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