本章知识点

        进程是Unix操作系统抽象概念中最基本的一种。其中涉及进程的定义以及相关概念,比如线程;讨论Linux内核如何管理每个进程:它们在内核中如何被列举,如何创建,如何消亡。拥有操作系统就是为了运行用户程序,进程管理是所有操作系统的心脏所在,Linux也不例外。

3.1 进程

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

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

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

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

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

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

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

        注意:Linux内核通常把进程叫做任务。

3.2 进程描述符以及任务结构

        内核把进程的列表存放在任务队列的双向循环链表中。链表中的每一项都是类型为task_struct称为进程描述符的结构,定义在linux/sched.h中。进程描述符中包含一个具体进程的所有信息。

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

        第3章 进程管理(一)

1、分配进程描述符

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

        在x86上,struct thread_info在文件include/asm/thread_info.h中定义如下:

第3章 进程管理(一)

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

2、进程描述符的存放

        内核通过一个唯一的进程标识值或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上是一个int类型。为了与老版本的Unix和Linux兼容,PID的最大值默认设置为32768。内核把每个进程的PID存放在各自的进程描述符中。

        这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。尽管32768对于一般的桌面系统足够用了,但是大型服务器可能需要更多的进程。这个值越小,转一圈就越快,本来数值大的进程比数值小的进程迟运行,但这样就破坏了这一原则。如果确实需要,由系统管理员通过修改/proc/sys/kernel/pid_max来提高上限。

        在内核中,访问任务通常需要获得指向task_struct的指针。内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度显得尤为重要。硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放执行当前进程task_struct的指针,用于加快访问速度。而有些像x86的体系结构,就只能在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。

3、进程的状态

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

        第3章 进程管理(一)

        TASK_RUNNING(运行):进程是可执行的;它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的进程。

        TASK_INTERRUPTIBLE(可中断):进程正在睡眠(阻塞),等待某些条件的达成。一旦这些条件达成,内核会把进程状态设置为运行。处于该状态的进程也会接收到信号而提前被唤醒并随时准备投入运行。

        TASK_UNINTERRUPTIBLE(不可中断):除了接收到信号也不会被唤醒或者准备投入运行外,这个状态与可中断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于该状态的任务对信号不做响应,所以较之可中断状态,使用得较少。

        __TASK_TRACED:被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。

       __TASK_STOPPED:进程停止执行;进程没有投入运行也不能投入运行。通常,这种状态发生在接收到SIGSTOP等信号时。在调试期间接收到任何信号,都会使得进程进入该状态。

4、设置当前进程状态

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

include/linux/sched.h

#define set_task_state(tsk, state_value)                \
        set_mb((tsk)->state, (state_value))

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

该函数将指定的进程设置为指定的状态。必要时,它会设置内存屏障来强制其他处理器作重新排序。否则,它等价于:

task->state = state;

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

#define set_current_state(state_value)          \
        set_mb(current->state, (state_value))

5、进程上下文

        可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间。称内核代表进程执行并处于进程上下文中。在此上下文中current宏是有效的。

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

6、进程家族树

        Unix系统的进程之间存在继承关系,在Linux系统中也是如此。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。

第3章 进程管理(一)

        系统中的每个进程必有一个父进程,每个进程也可以有零个或多个子进程。有同一个父进程的所有进程称为兄弟。进程之间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程task_struct的指针,还包含一个称为children的子进程链表。对于当前的进程,可以通过如下代码获得其父进程的进程描述符:

struct task_struct *parent= current->parent;

也可以按照以下方式依次访问子进程:

struct task_struct *task;

struct list_head *list;

//遍历链表

list_for_each(list, &current->children) {

        task=list_entry(list, struct task_struct, sibling);

        /*task现在指向当前的某个子进程*/

}

init进程的进程描述符是作为init_task静态分配的。下面的代码演示所有进程之间的关系:

struct task_struct *task;

for(task = current; task != &init_task; task = task->parent)

        /*task现在指向init*/

实际上,可以通过这种继承体系从系统的任何一个进程出发查找到任意指定的其它进程。但大多数时候,只需要通过简单的重复方式就可以遍历系统中的有进程。因为任务队列是一个双向的循环链表。对于给定的进程,获取链表中的下一个进程:

list_entry(task->tasks.next, struct task_struct, tasks);

 

获取链表中的上一个进程:

list_entry(task->tasks.prev, struct task_struct, tasks);

这两个例子分别通过next_task(task)宏和prev_task(task)宏实现。实际上,for_each_process(p) 宏提供了依次访问整个任务队列的能力。每次访问,任务指针都指向链表中的下一个元素。

struct task_struct *task;

for_each_process(p) {

        printk("%s[%d]\n", p->comm, p->pid);

}

备注:

在一个拥有大量进程的系统中通过重复遍历所有的进程代价是很大的。如果没有充足的理由,别这样做。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>

struct task_struct *task;

static __init int hello_init(void)
{

for_each_process(task) { // 遍历系统中所有的进程
                printk("%s[%d]\n", task->comm, task->pid);
}

return 0;
}

static __exit void hello_exit(void)
{
printk(KERN_INFO  "hello_exit");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("Dual BSD/GPL");

 

相关文章: