Linux内核之进程管理

一、认识进程
  进程就是处于执行期的程序(目标代码存放在某种存储介质上),进程的另一个名字是任务(task),Linux内核通常把进程也叫做任务;但进程并不仅仅局限于一段可执行程序代码(Unix称其为代码段,text section)。通常进程还要包含其他的资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),当然还包括用来存放全局变量的数据段等。实际上,进程就是正在执行的程序代码的实时结果。内核需要有效而又透明的管理所有的细节。
  执行线程,简称线程(thread),是在进程中活动的对象;进程是操作系统分配资源的基本单位,而线程是处理执行的基本单位。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的Unix系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程已经司空见惯了。Linux系统对进程和线程并不特别区分,对Linux而言,线程只不过是一种特殊的进程而已。
  在现代的操作系统中,进程提供了两种虚拟机制:虚拟处理器和虚拟内存,虽然实际上可能是许多个进程正在分享一个处理器,但是虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器;而虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。
  程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称,实际上,完全可能存在两个或多个不同的进程执行的是同一个程序。并且两个或两个以上并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。
  进程具有的特征:
    1)动态性:进程是程序的一次执行过程,是临时的,有生命的,是动态产生,动态消亡的;
    2)并发性:任何进程都可以同其他进程一起并发执行;
    3)独立性:进程是系统进行资源分配和调度的一个独立单位;
    4)结构性:进程由程序、数据和进程控制块三部分组成;

二、进程描述符
  内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中,链表的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构中,进程描述符中包含一个具体进程的所有信息。每一个进程都有一个进程描述符,记录以下重要信息:进程标识符、进程当前状态、栈地址空间、内存地址空间、文件系统、打开的文件、信号量等。
  进程描述符在内存中的存放位置比较有特点,由于系统需要频繁的获取当前进程描述符的地址,为了提高效率,Linux巧妙的实现该功能,使用curren宏可以快速得到当前进程地址;在x86体系中,通过SP寄存器可以快速获取当前进程栈的位置;Linux在栈末端存放了一个特殊的数据结构thread_info,thread_info中存放了指向task_struct的指针,根据这个原理,首先当前进程通过SP寄存器获取栈的位置,然后根据栈大小(一般为1-2页)获取thread_info的地址,最后通过thread_info获取当前进程的地址;基于以上分析,进程的内核栈与thread_info存放在同一个页内,thread_info与内核栈共享了页,从源代码中可以看出,在分配内核栈的同时也分配了thread_info。

Linux内核之进程管理
Task_Struct结构体信息:
  标识符:描述本进程的唯一标识符;
  状态:任务状态、退出代码、退出信息等;
  优先级:相对于其他进程的优先级;
  程序计数器:程序中被执行的下一条指令的地址;
  内存地址:包括程序代码和进程相关的数据的指针,还有其他进程共享内存块的指针;
  上下文数据:进程执行时处理的寄存器中的数据;
  I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
  记账信息:包括处理器时间总和,使用时钟总和、时间限制、记账信息等;

三、进程状态
  进程描述符中的state域描述了进程的当前状态信息,系统中的每个进程都必然处于五种状态中的一种,该域的值也必为下列五种状态标志之一:
  1)TASK_RUNNING(运行)—进程是可执行的;它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态,这种状态也可以应用到内核空间中正在执行的进程;
  2)TASK_INTERRUPTIBLE(可中断)—进程正在睡吗(也就是说它被阻塞),等待某些条件的达成,一旦这些条件达成了,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒随时准备投入运行;
  3)TASK_UNINTERRUPTIBLE(不可中断)—除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可中断状态相同,这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现;由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用的得较少;
  4)_TASK_TRACED—被其他进程跟踪的进程,例如通过ptrace对调试程序进程跟踪;
  5)_TASK_STOPPED(停止)—进程停止执行;进程没有投入运行也不能投入运行,通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态;

Linux内核之进程管理

四、进程创建
  Unix的进程创建很特别,许多其他的操作系统都提供了产生(spawn)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix采用了与众不同的实现方式,它把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(进程号,每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。
  Linux内核之进程管理
  写时复制:是一种可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程的地址空间,而是让父进程和子进程共享一个拷贝,只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行,在页根本不会被写入的情况下,它们就无须复制了;fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据,由于Unix强调进程快速执行的能力,所以这个优化是很重要的。