linux内核--进程管理(一)

时间:2022-06-01 06:39:11

进程

进程就是处于执行期的程序(目标码存放在某种存储介质上)。但进程并不仅仅局限于可执行程序代码(Unix称其为代码段,text section)。通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),当然还包括用来存放全局变量的数据段等。实际上,进程就是正在执行的程序的实时结果。内核需要有效而又透明地管理所有细节。

执行线程,简称线程(thread),是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的unix系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程程序司空见惯。 Linux系统的线程实现非常特别:它对线程和进程并不特别区分。对linux而言,线程只不过是一种特殊的进程罢了。

在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上可能是许多进程正在分享一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。 在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。

程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总体。实际上,完全可能存在两个或多个不同的进程执行的是同一个程序。并且两个或两个以上并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。

进程在创建它的时刻开始存活。在Linux系统中,这通常是调用fork()系统的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时,再返回这个相同的位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核而返回两次:一次回到父进程,另一次回到新产生的子进程。

通常,创建新的进程都是为了立即执行新的,不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。在现代Linux内核中,fork()实际上是由clone()系统调用实现的。

最终,程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,知道他的父进程调用wait()或waitpid()为止。

进程描述符及任务结构

内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct,称为进程描述符(process descriptor)的结构,该结构定义在文件中。该描述符中包含一个具体进程的所有信息。

task_struct 相对较大,在32位机器上,它大约有1.7KB。但如果考虑到该结构内包含了内核管理一个进程所需的所有信息,那么它的大小也算相当小了。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息。

分配进程描述符

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。在2.6以前的内核中,各个进程的task_struct存放在它们内核栈的尾端。这样做是为了让那些像x86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专门记录。由于现在用slab分配器动态生成task_sruct,所以只需在栈底(对于向下增长的栈来说)或栈顶(也对于向上增长的栈来说)创建一个新的结构struct thread_info。
在x86上,struct thread_info在文件中定义如下:

struct thread_info {     
    struct task_struct *task;     
    struct exec_domain *exec_domain;     
    __u32  flags;     
    __u32  status;     
    __u32  cpu;     
    int    preempt_count;     
    mm_segment_t addr_limit;     
    struct restart_block restart_block;     
    void   *sysenter_return;     
};  

进程描述符和内核栈的关系如下图所示:
linux内核--进程管理(一)

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

进程描述符的存放

内核通过一个唯一的进程标识符(process identification value)或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。为了与老版本的unix和linux兼容 ,PID的最大默认值设为32768(short int短整型的最大值),尽管这个值也可以以增加到高达400万(这受

movl $-8192,%eax
andl %esp,%eax

这里假定栈的大小为8KB,当4KB的栈启用时,就要用4096,而不是8192。
最后,current 再从thread_info的 task 域中提取并返回 task_struct 的地址:

current_thread_info()->task;

对比一下这部分在powerpc上的实现(IBM基于RISC的现代微处理器),我们可以发现ppc当前的task_struct是保持在一个寄存器中的,也就是说,在ppc上,current宏只需把r2寄存器中的值返回就行了。与x86不一样,ppc有足够多的寄存器,所以它的实现有这样选择的余地。而访问进程描述符是一个重要的频繁操作,所以ppc的内核开发者觉得完全有必要为此使用一个专门的寄存器。

进程状态
进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然处于五中进程状态中的一种。该域的值也比为下列五种状态标志之一:

  1. TASK_RUNNING(运行)——进程是可执行的;它或者正在执行,或者在运行队列中等待运行。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的进程。
  2. TASK_INTERRUPTIBLE(可中断)——进程正在书面(也就是说它被阻塞),等待某些条件的到达。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
  3. TASK_UNINTERRUPTIBLE(不可中断)——除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须等待时不受干扰或等待时间很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用的较少。
  4. __TASK_TRACED——被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。
  5. __TASK_STOPPED(停止)——进程停止执行;进程没有投入运行也不能投入运行。

    进程状态转化图如下所示:
    linux内核--进程管理(一)

设置当前进程状态

内核经常需要调整某个进程的状态。这是最好使用set_task_state(task,state)函数:

set_task_state(task,state); /*将任务task的状态置为state*/

该函数将制定的进程设置为指定的状态。必要的时候,他会设置内存屏蔽来强制其他处理器作重新排序。(一般只有在SMP系统中有此必要。)否则,它等价于:

task->state=state;

set_current_state(state)和set_task_state(current,state)含义是等同的。