第三章 进程管理
-
进程
-
进程描述符及任务结构
3.1进程
进程就是处于执行期的程序。但进程也不局限于一段可执行的代码段。还需要例如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个可执行线程,还包括用来存放全局变量的数据段等。
可执行线程,简称线程,是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调用对象是线程而不是进程。对Linux而言,线程只不过是以中特殊的进程。
在现代操作系统中,进程提供了两种虚拟机制:虚拟处理器和虚拟内存。
虚拟处理器给进程一种假象,每个进程都觉得自己在独享处理器。虚拟内存让进程觉得自己拥有系统所有的内存资源。有一点值得注意,(同一个进程中的线程)线程间可共享虚拟内存,但每个都拥有各自的虚拟处理器。
在Linux系统中,通常通过调用fork()创建进程,新产生的进程称为子进程。该调用结束的时候,在返回这个相同位置上,父进程恢复执行,子进程开始执行。fork()函数返回两次:一次父进程,一次子进程。通常,为了创建的新进程可以立即执行新的不同的程序,可直接调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。在现代Linux内核中,fork()实际上是clone()系统调用实现的。
最终程序可通过exit()系统调用退出执行,最终会终结进程并将其占用的资源释放掉。父进程通过 wait4()系统调用查询子进程是否终结,这其实是的进程拥有了等待特定进程执行完毕的能力。进程退出后被设置为僵死状态,直到他的父进程调用wait()或waitpid()为止。
注意一点:从内核的角度来看,进程的另一个名字叫做任务(task)
3.2 进程描述符及任务结构
内核把进程的列表存放在叫任务队列的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构。进程描述符中包含一个具体进程的所有信息。
task_struct 相对较大,32位机上约1.7KB。进程描述符中包含的数据能完整地描述一个正在执行地程序:它打开地文件,进程的地址空间,挂起的信号,进程的状态,还有其他信息。
3.2.1分配进程描述符
Linux通过slab分配器分配task_struct 结构,这样就可以达到对象复用和缓存着色的目的。(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗,印证了第一章所说的Unix的一个特点就是进程创建迅速。)
3.2.2进程描述符的存放
内核通过一个唯一的进程标识或PID来表示每个进程。PID最大默认值设置为32768。这个最大值很重要,它实际上就是系统中允许同时存在的进程的最大数目。系统管理员可通过 /proc/sys/kernell/pid_max 来提高上限。
3.2.3进程状态
进程描述符中的state域描述了进程状态的当前状态。系统中的每个进程都必然处于五个状态中的一个。
· TASK_RUNNING(运行)
· TASK_INTERRUPTIBLE(可中断)
· TASK_UNINTERRUPTIBLE(不可中断)
· _TASK_TRACED
· _TASK_STOPPED(停止)
· TASK_RUNNING(运行)——进程是可执行的;他或许正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的过程。
· TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于这种状态的进程也会因为接收到信号儿提前被唤醒并随时准备投入运行。
· TASK_UNINTERRUPTIBLE(不可中断)——除了就算是接收到信号也不会被唤醒或者准备投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不做出响应,所以较之可中断状态,使用的较少。
· _TASK_TRACED——被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。
· _TASK_STOPPED(停止)——进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接受到任何信号,都会使进程进入到这种状态。
3.2.4 设置当前进程状态
内核需要经常调整进程状态。这时最好使用 set_task_state(task,state)函数:
set_task_state(task,state); /* 将任务 task 的状态设置为 state */
该函数将指定的进程设置为指定的状态。必要的时候,他会设置内存屏障来强制其他处理器最重新排序。否则,它等价于:
task->state = state;
set_current_state(state)和set_task_sate(current,state)含义是等价的。
3.2.5 进程上下文
可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序调执行了系统调用或者出发了某个异常,他就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中 current 宏是有效的。除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间会继续执行。
系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口。
3.2.6 进程家族树
Linux系统的进程之间存在一个明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。
系统中每个进程都必须有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程都被称为兄弟。进程间的关系存放在进程描述符中。每个task_struct 都包含一个指向其父进程 task_struct、叫做parent的指针,还包含一个称为children的子进程链表。所以,对于当前进程,可以通过下面的代码获得其父进程的进程描述符:
struct task_struct *my_parent = current->parent;
同样的,也可以按以下方式依次访问子进程:
struct task_struct *task;
struct list_head *list;
list_for_each(list,¤t->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(task)宏提供了依次访问整个任务队列的能力。每次访问,任务指针都指向链表中的下一个元素
struct task_struct *task;
for_each_process(task){
/* 他打印出每一个任务的名称和PID*/
printk("%s[%d]\n",task->comm,task->pid);
}
特别提醒: 在一个拥有大量进程的系统中通过重复来遍历所有的进程代价是很大的。因此,如果没有充足的理由(或者别无他法)别这样做。
3.3 进程创建
未完待续……