进程
进程就是处于执行期的程序(目标代码存放在某种存储介质上)。但进程并不仅仅局限于可执行程序代码(Unix称其为代码段,text section)。通常进程还包括其他资源,比如:打开的文件,挂起的信号,内核的内部数据,处理器状态,一个或多个具有内存映射的内存地址空间,一个或多个执行线程(thread of execution),以及用来存放全局变量的数据段等。实际上,进程就是程序执行时的实时反馈。
执行线程,简称线程(thread),是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的unix系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程程序已经司空见惯。 Linux系统线程的实现非常特别:它对线程和进程并不特别区分。对linux而言,线程只不过是一种特殊的进程罢了。
在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上可能是许多进程正在分享一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。 在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。
程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总体。实际上,存在两个或多个不同的进程执行同一程序。并且两个或两个以上并存的进程还可以共享打开的文件,以及地址空间等资源。
进程在创建它的时刻开始存活。在Linux系统中,这通常是调用fork()的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时,再返回这个相同的位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核而返回两次:一次回到父进程,另一次回到新产生的子进程。
通常,创建新的进程都是为了执行不同的程序,因此接着调用exec()函数就可以创建新的地址空间,并把新的程序载入其中。
最终,程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()为止。
进程描述符及任务结构
内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct,称为进程描述符(process descriptor)的结构,该结构定义在sched.h文件中。该描述符中包含一个具体进程的所有信息,进程描述符中包含的数据能完整地描述一个正在执行的程序,比如:打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息。
分配进程描述符
Linux通过内核实际所采用的内存管理机制(比如:slab内存分配机制)分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。在2.6以前的内核中,各个进程的task_struct存放在它们内核栈的尾端。这样做的目的是为了让寄存器较少的硬件体系结构只需要通过栈指针就能计算出它的位置,从而避免使用额外的寄存器专门记录。由于现在使用内存管理机制动态生成task_sruct,所以需在栈底(对于向下增长的栈来说)或栈顶(也对于向上增长的栈来说)创建一个新的结构,即struct thread_info来与task_struct进行关联。
在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;
};
进程描述符和内核栈的关系如下图所示:
每个进程的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。
进程描述符的存放
内核通过一个唯一的进程标识符(process identification value)或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。内核把每个进程的PID存放在它们各自的进程描述符中。
PID的最大默认值设为32768(short int短整型的最大值),尽管这个值可以增加到高达400万(这受限与<linux/threads.h>中所定义的PID最大值)。这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。尽管32768对于一般的桌面系统足够用了,但是大型服务器可能需要更多进程。这个值越小,转一圈就越快,本来数值大的进程比数值小的进程迟运行,但这样一来就破坏了这一原则。如果确实需要的话,可以不考虑与老式系统的兼容,有系统管理员通过修改/proc/sys/kernel/pid_max来提高上限。
在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要。硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。而有些像x86这样的体系结构(其寄存器并不富余),就只能在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。
在x86系统上,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移。该操作是通过current_thread_info()函数来完成的。汇编代码如下:
movl $-8192,%eax
andl %esp,%eax
这里假定栈的大小为8KB,当4KB的栈启用时,就要用4096,而不是8192。
最后,current 再从thread_info的 task 域中提取并返回 task_struct 的地址:
current_thread_info()->task;
对比一下这部分在PowerPC上的实现(IBM基于RISC的现代微处理器),可以发现PowerPC当前的task_struct是保持在一个寄存器中的,也就是说,在PowerPC上,current宏只需把r2寄存器中的值返回就行了。与X86不一样,PowerPC有足够多的寄存器,所以它的实现有这样选择的余地。而访问进程描述符是一个重要的频繁操作,所以PowerPC的内核开发者觉得完全有必要为此使用一个专门的寄存器。
进程状态
进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然处于五中进程状态中的一种。该域的值也比为下列五种状态标志之一:
- TASK_RUNNING(运行)——进程是可执行的;它或者正在执行,或者在运行队列中等待运行。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的进程。
- TASK_INTERRUPTIBLE(可中断)——进程正在书面(也就是说它被阻塞),等待某些条件的到达。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
- TASK_UNINTERRUPTIBLE(不可中断)——除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须等待时不受干扰或等待时间很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用的较少。
- __TASK_TRACED——被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。
- __TASK_STOPPED(停止)——进程停止执行;进程没有投入运行也不能投入运行。
进程状态转化图如下所示:
设置当前进程状态
内核经常需要调整某个进程的状态。使用set_task_state(task,state)函数:
set_task_state(task,state); /*将任务task的状态置为state*/
该函数将制定的进程设置为指定的状态。必要的时候,它会设置内存屏蔽来强制其他处理器作重新排序。(一般只有在SMP系统中有此必要。)否则,它等价于:
task->state=state;