内核程序通过进程表对进程进行管理,每个进程在进程表中占有一项。在Linux系统中,进程表项是一个task_struct任务结构指针。任务数据结构定义在头文件include/linux/sched.h中。有些书上称其为进程控制块(Process Control Block,PCB)或进程描述符(Processor Descriptor,PD)。其中保存着用于控制和管理进程的所有信息。主要包括进程当前运行的状态信息、信号、进程号、父进程号、运行时间累计值、正在使用的文件和本任务的局部描述符以及任务状态段信息。该结构每个字段的具体含义如下所示。
struct task_struct {
long state; // 任务的运行状态(-1不可运行,0可运行(就绪),>0已停止)。
long counter; // 任务运行时间计数(递减)(滴答数),运行时间片。
long priority; // 优先数。任务开始运行时counter=priority,越大运行越长。
long signal; // 信号位图,每个位代表一种信号,信号值=位偏移值+1。
struct sigaction sigaction[32]; // 信号执行属性结构,对应信号将要执行的操作和标志信息。
long blocked; // 进程信号屏蔽码(对应信号位图)。
int exit_code; // 任务停止执行后的退出码,其父进程会来取。
unsigned long start_code; // 代码段地址。
unsigned long end_code; // 代码长度(字节数)。
unsigned long end_data; // 代码长度 + 数据长度(字节数)。
unsigned long brk; // 总长度(字节数)。
unsigned long start_stack; // 堆栈段地址。
long pid; // 进程标识号(进程号)。
long pgrp; // 进程组号。
long session; // 会话号。
long leader; // 会话首领。
int groups[NGROUPS]; // 进程所属组号。一个进程可属于多个组。
task_struct *p_pptr; // 指向父进程的指针。
task_struct *p_cptr; // 指向最新子进程的指针。
task_struct *p_ysptr; // 指向比自己后创建的相邻进程的指针。
task_struct *p_osptr; // 指向比自己早创建的相邻进程的指针。
unsigned short uid; // 用户标识号(用户id)。
unsigned short euid; // 有效用户id。
unsigned short suid; // 保存的用户id。
unsigned short gid; // 组标识号(组id)。
unsigned short egid; // 有效组id。
unsigned short sgid; // 保存的组id。
long alarm; // 报警定时值(滴答数)。
long utime; // 用户态运行时间(滴答数)。
long stime; // 系统态运行时间(滴答数)。
long cutime; // 子进程用户态运行时间。
long cstime; // 子进程系统态运行时间。
long start_time; // 进程开始运行时刻。
struct rlimit rlim[RLIM_NLIMITS]; // 进程资源使用统计数组。
unsigned int flags; // 各进程的标志,在下面第149行开始定义(还未使用)。
unsigned short used_math; // 标志:是否使用了协处理器。
int tty; // 进程使用tty终端的子设备号。-1表示没有使用。
unsigned short umask; // 文件创建属性屏蔽位。
struct m_inode * pwd; // 当前工作目录i节点结构指针。
struct m_inode * root; // 根目录i节点结构指针。
struct m_inode * executable; // 执行文件i节点结构指针。
struct m_inode * library; // 被加载库文件i节点结构指针。
unsigned long close_on_exec; // 执行时关闭文件句柄位图标志。(参见include/fcntl.h)
struct file * filp[NR_OPEN]; // 文件结构指针表,最多32项。表项号即是文件描述符的值。
struct desc_struct ldt[3]; // 局部描述符表。0-空,1-代码段cs,2-数据和堆栈段ds&ss。
struct tss_struct tss; // 进程的任务状态段信息结构。
};
(1)long state 字段含有进程的当前状态代号。如果进程正在等待使用CPU或者进程正被运行,那么state的值是TASK_RUNNING。如果进程正在等待某一事件的发生因而处于空闲状态,那么state的值就是TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。这两个值含义的区别在于处于TASK_INTERRUPTIBLE状态的进程能够被信号唤醒并激活,而处于TASK_UNINTERRUPTIBLE状态的进程则通常是在直接或间接地等待硬件条件的满足因而不会接受任何信号。TASK_STOPPED状态用于说明一个进程正处于停止状态。例如进程在收到一个相关信号时(如SIGSTOP、SIGTTIN或SIGTTOU等)或者当进程被另一个进程使用ptrace系统调用监控并且控制权在监控进程中时。TASK_ZOMBIE状态用于描述一个进程已经被终止,但其任务数据结构项仍然存在于任务结构表中。一个进程在这些状态之间的转换过程见下节说明。
(2)long counter 字段保存着进程在被暂时停止本次运行之前还能执行的时间滴答数,即在正常情况下还需要经过几个系统时钟周期才切换到另一个进程。调度程序会使用进程的counter值来选择下一个要执行的进程,因此counter可以看作是一个进程的动态特性。在一个进程刚被创建时counter的初值等于priority。
(3)long priority 用于给counter赋初值。在Linux 0.12中这个初值为15个系统时钟周期时间(15个嘀嗒)。当需要时调度程序会使用priority的值为counter赋一个初值,参见sched.c程序和fork.c程序。当然,priority的单位也是时间滴答数。
(4)long signal 字段是进程当前所收到信号的位图,共32位,每位代表一种信号,信号值=位偏移值+1。因此Linux内核最多有32个信号。在每个系统调用处理过程的最后,系统会使用该信号位图对信号进行预处理。
(5)struct sigaction sigaction[32] 结构数组用来保存处理各信号所使用的操作和属性。数组的每一项对应一个信号。
(6)long blocked 字段是进程当前不想处理的信号的阻塞位图。与signal字段类似,其每一位代表一种被阻塞的信号。
(7)int exit 字段是用来保存程序终止时的退出码。在子进程结束后父进程可以查询它的这个退出码。
(8)unsigned long start_code字段是进程代码在CPU线性地址空间中的开始地址,在Linux 0.1x内核中其值是64MB的整数倍。
(9)unsigned long end_code字段保存着进程代码的字节长度值。
(10)unsigned long end_data 字段保存着进程的代码长度 + 数据长度的总字节长度值。
(11)unsigned long brk 字段也是进程代码和数据的总字节长度值(指针值),但是还包括未初始化的的数据区bss,参见图13-6。这是brk在一个进程开始执行时的初值。通过修改这个指针,内核可以为进程添加和释放动态分配的内存。这通常是通过调用malloc()函数并通过brk系统调用由内核进行操作。
(12)unsigned long start_stack 字段值指向进程逻辑地址空间中堆栈的起始处。同样请参见图13-6中的堆栈指针位置。
(13)long pid 是进程标识号,即进程号。它被用来唯一地标识进程。
(14)long pgrp 是指进程所属进程组号。
(15)long session 是进程的会话号,即所属会话的进程号。
(16)long leader 是会话首进程号。有关进程组和会话的概念请参见第7章程序列表后的说明。
(17)int groups[NGROUPS] 是进程所属各个组的组号数组。一个进程可属于多个组。
(18)task_struct *p_pptr 是指向父进程任务结构的指针。
(19)task_struct *p_cptr 是指向最新子进程任务结构的指针。
(20)task_struct *p_ysptr 是指向比自己后创建的相邻进程的指针。
(21)task_struct *p_osptr 是指向比自己早创建的相邻进程的指针。以上4个指针的关系参见图5-20。在Linux 0.11内核的任务数据结构中专门有一个父进程号字段father,但是0.12内核中已经不用。此时我们可以使用进程的pptr->pid来取得父进程的进程号。
(22)unsigned short uid 是拥有该进程的用户标识号(用户id)。
(23)unsigned short euid 是有效用户标识号,用于指明访问文件的权力。
(24)unsigned short suid 是保存的用户标识号。当执行文件的设置用户ID标志(set-user-ID)置位时,suid中保存着执行文件的uid;否则,suid等于进程的euid。
(25)unsigned short gid 是用户所属组标识号(组id)。指明了拥有该进程的用户组。
(26)unsigned short egid 是有效组标识号,用于指明该组用户访问文件的权限。
(27)unsigned short sgid 是保存的用户组标识号。当执行文件的设置组ID标志(set-group-ID)置位时,sgid中保存着执行文件的gid;否则,sgid等于进程的egid。有关这些用户号和组号的描述请参见第5章sys.c程序前的概述。
(28)long timeout 内核定时超时值。
(29)long alarm 是进程的报警定时值(滴答数)。如果进程使用系统调用alarm()设置过该字段值(alarm()在kernel/sched.c第370行开始处。内核会把该函数以秒为单位的参数值转换成滴答值,加上系统当前时间滴答值之后保存在该字段中),那么此后当系统时间滴答值超过了alarm字段值时,内核就会向该进程发送一个SIGALRM信号。默认时该信号会终止程序的执行。当然我们也可以使用信号捕捉函数(signal()或sigaction())来捕捉该信号进行指定的操作。
(30)long utime 是累计进程在用户态运行的时间(滴答数)。
(31)long stime 是累计进程在系统态(内核态)运行的时间(滴答数)。
(32)long cutime 是累计进程的子进程在用户态运行的时间(滴答数)。
(33)long cstime 是累计进程的子进程内核态运行的时间(滴答数)。
(34)long start_time 是进程生成并开始运行的时刻。
(35)struct rlimit rlim[RLIM_NLIMITS] 进程资源使用统计数组。
(36)unsigned int flags 各进程的标志,0.12内核还未使用。
(37)unsigned short used_math 是一个标志,指明本进程是否使用了协处理器。
(38)int tty 是进程使用tty终端的子设备号。-1表示没有使用。
(39)unsigned short umask 是进程创建新文件时所使用的16位属性屏蔽字(每位表示文件的一种属性),即新建文件所设置的访问属性。若屏蔽字某位被置位,则表示对应的属性被禁止(屏蔽)掉。该属性屏蔽字会与创建文件时给出的属性值一起使用(mode &~umask)以作为新建文件的实际访问属性。有关屏蔽字和文件属性各位的具体含义请参见文件include/fcntl.h和include/sys/state.h。
(40)struct m_inode * pwd 是进程的当前工作目录i节点结构。每个进程都有一个当前工作目录,用于解析相对路径名,并且可以使用系统调用chdir来改变之。
(41)struct m_inode * root 是进程自己的根目录i节点结构。每个进程都可有自己指定的根目录,用于解析绝对路径名。只有超级用户能通过系统调用chroot来修改这个根目录。
(42)struct m_inode * executable 是进程运行的执行文件在内存中的i节点结构指针。系统可根据该字段来判断系统中是否还有另一个进程在运行同一个执行文件。如果有的话那么这个内存中i节点引用计数值executable->i_count会大于1。在进程被创建时该字段被赋予和父进程同一字段相同的值,即表示正在与父进程运行同一个程序。当在进程中调用exec()类函数而去执行一个指定的执行文件时,该字段值就会被替换成exec()函数所执行程序的内存i节点指针。当进程调用exit()函数而执行退出处理时该字段所指内存i节点的引用计数会被减1,并且该字段将被置空。该字段的主要作用体现在memory.c程序的share_page()函数中。该函数代码根据进程的executable所指节点的引用计数可判断系统中当前运行的程序是否有多个副本存在(至少2个)。若是,则在它们之间尝试页面共享操作。
在系统初始化时,在第1次调用执行execve()函数之前,系统创建的所有任务的executable都是0。这些任务包括任务0、任务1以及任务1直接创建的没有执行过execve()的所有任务,即代码直接包含在内核代码中的所有任务的executable都是0。因为任务0的代码包含在内核代码中,它不是由系统从文件系统上加载运行的执行文件,因此内核代码中固定设置它的executable值为0。另外,创建新进程时,fork()会复制父进程的任务数据结构,因此任务1的executable也是0。但在执行了execve()之后,executable就被赋予了被执行文件的内存i节点的指针。此后所有任务的该值就均不会为0了。
(43)unsigned long close_on_exec 是一个进程文件描述符(文件句柄)位图标志。每个位代表一个文件描述符,用于确定在调用系统调用execve()时需要关闭的文件描述符(参见include/fcntl.h)。当一个程序使用fork()函数创建了一个子进程时,通常会在该子进程中调用execve()函数加载执行另一个新程序。此时子进程将完全被新程序替换掉,并在子进程中开始执行新程序。若一个文件描述符在close_on_exec中的对应位是置位状态,那么在子进程执行execve()调用时对应打开着的文件描述符将被关闭,即在新程序中该文件描述符被关闭;否则,该文件描述符将始终处于打开状态。
(44)struct file * filp[NR_OPEN] 是进程使用的所有打开文件的文件结构指针表,最多32项。文件描述符的值即是该结构中的索引值。其中每一项用于文件描述符定位文件指针和访问文件。
(45)struct desc_struct ldt[3] 是该进程局部描述符表结构。定义了该任务在虚拟地址空间中的代码段和数据段。其中数组项0是空项,项1是代码段描述符,项2是数据段(包含数据和堆栈)描述符。
(46)struct tss_struct tss 是进程的任务状态段(Task State Segment,TSS)信息结构。在任务从执行中被切换出时tss_struct结构保存了当前处理器的所有寄存器值。当任务又被CPU重新执行时,CPU就会利用这些值恢复到任务被切换出时的状态,并开始执行。
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换(switch)至另一个进程时,它就需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。在Linux中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。