为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。例如,内核必须知道进程的优先级,它是正在CPU上运行还是因某些事件而被阻塞,给它分配了什么样的地址空间,允许它访问哪个文件等等。
这些正是进程描述符的作用——进程描述符都是task_struct数据结构,它的字段包含了与一个进程相关的所有信息。因为进程描述符存放了那么多的信息,所以它是相当复杂滴。。。。不过别怕,我们解决主要矛盾,其他的部分就会迎刃而解,不攻自破的。下图就显示了Linux的进程描述符的主要矛盾:
本博,我们将集中讨论进程的基本信息、进程的状态以及进程间关系。其他的我们会在以后博文中涉及到。
1、进程的基本信息
1.1 标识一个进程——PID
每个进程都必须拥有它自己的进程描述符;因此,即使共享内核大部分数据结构的轻量级进程(后面会提到),也有它们自己的task_struct结构。进程和进程描述符之间有非常严格的一一对应关系,所以我们可以方便地使用32位进程描述符地址标识进程。进程描述符指针(task_struct*)指向这些地址。内核对进程的大部份引用都是通过进程描述符指针进行的。
另一方面,类Unix橾作系统允许用户使用一个叫做进程标识符processID (PID)的数来标识进程,PID存放在task_struct的pid字段中。PID被顺序编号,新创建进程的PID通常是前一个进程的PID加1。不过,PID的值有一个上限,当内核使用的PID达到这个峰值的时候,就必须开始循环使用已闲置的小PID号。在缺省情况下,最大的PID号是32767
系统管理员可以通过往/proc/sys/kernel/pid_max 这个文件中写入一个更小的值来减小PID的上限值,使PID的上限小于32767。在64位体系结构中,系统管理员可以把PID的上限扩大到4194304。
由于循环使用PID编号,内核必须通过管理一个pidmap_array位图来表示当前已分配的PID和闲置的PID号。因为一个页框包含32768个位(4*1024*8),所以在32位体系结构中pidmap_array位图正好存放在一个单独的页中。系统会一直保存这些页而不释放的。
再告诉你一个知识,Linux只支持轻量级进程,不支持线程,但为了弥补这样的缺陷,Linux引入线程组的概念。一个线程组中的所有线程使用和该线程组的领头线程相同的PID,也就是该组中第一个轻量级进程的PID,它被存入进程描述符的tgid字段中。getpid()系统调用返回当前进程的tgid值而不是pid值,因此,一个多线程应用的所有线程共享相同的PID。绝大多数进程都属于一个线程组;而线程组的领头线程其tgid与pid的值相同,因而getpid()系统调用对这类进程所起的作用和一般进程是一样的。
所以,我们得出一个重要的结论,Linux虽不支持线程,但是它有具备支持线程的操作系统的所有特性,后面讲解轻量级进程的概念中还会详细讨论。
1.2 进程描述符定位
进程是动态实体,其生命周期范围从几毫秒到几个月。因此,内核必须能够同时处理很多进程,并把对应的进程描述符存放在动态内存中,而不是放在永久分配给内核的内存区(3G之上的线性地址)。
那么,怎么找到被动态分配的进程描述符呢?我们需要在3G之上线性地址的内存区为每个进程设计一个块——thread_union。对每个进程来说,我们需要给其分配两个页面,即8192个字节的块。Linux把两个不同数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。
考虑到效率问题,内核让这8k的空间占据连续两个页框并让第一个页框的起始地址是2^13的倍数。当几乎没有可用的动态内存空间时,就会很难找到这样的两个连续页框,因为空闲空间可能存在大量的碎片(注意,这里是物理空间,见“伙伴系统算法”博文)。因此,在80x86体系结构中,在编译时可以进行设置,以使内核栈和线程描述符跨越一个单独的页框(因为主要存在的单页的碎片)。在“Linux中的分段”的博文中我们已经知道,内核态的进程访问处于