《深入理解Linux内核第四版》学习笔记——第三章进程(一)进程描述符

一、进程,轻量级进程,线程

        进程是程序执行时的一个实例,可以看做充分描述程序已经执行到何种程度的数据结构的汇集。一个进程可以有多个子进程,但是只能有一个父进程。以内核观点看,进程的目的就是担当分配系统资源的实体

        进程被创建时,接收父进程地址空间的一个逻辑拷贝,但是他们有独立的堆栈,父子进程对内存单元的修改互不可见。

        进程:用户线程=1:N,每个线程都是进程的一个执行流

        Linux采用轻量级进程对多线程应用提供更好的支持。两个轻量级进程基本上可以共享一些资源,只要其中一个修改共享资源,另一个就立即查看这种修改。在Linux中,一个线程组基本上就是实现了多线程应用的一组轻量级进程。也就是说:线程组A=[轻量级进程A1,轻量级进程A2..]

二、进程描述符

        进程描述符是一个字段包含了与一个进程相关的所有信息的task_struct类型结构。不仅包含了很多进程属性字段,而且一些字段还包括了指向其他数据结构的指针

 1.进程状态

        进程状态由一组标志组成,由进程描述符中的state字段描述,每个标志只能描述一个互斥状态。共有下面7种状态,最后两种既可以存放在state字段中,又可以存放在exit_state字段中:

        1.可运行状态(TASK_RUNNING),进程要么在CPU上执行,要么准备执行。

        2.可中断的等待状态(TASK_INTERRUPTIBLE).进程挂起,直到某条件符合产生硬件中断释放进程正等待的系统资源,或传递一个信号都可以唤醒进程,后状态变为可运行状态。

        3.不可中断的等待状态(TASK_UNINTERRUPTIBLE),与可中断等待状态类似,但是把信号传递到睡眠进程不能改变它的状态。在进程必须等待,直到一个不能被中断的事件发生时,这种状态很有用。

        4.暂停状态(TASK_STOPPED),进程执行被暂停,当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号后,进入暂停状态。

        5.跟踪状态(TASK_TRACED),进程的执行已由debugger程序暂停。

        6.僵死状态(EXIT_AOMBIE),进程执行被终止,但是父进程还没有发布wait4()或waitpid()系统调用来返回有关死亡进程的信息。且在发布wait()类系统调用前,内核不能丢弃死进程描述符中的数据。

        7.僵死撤销状态(EXIT_DEAD),最终状态,父进程刚发出wait()类系统调用,进程由系统删除,为了防止其他执行线程在同一个进程上也执行wait类,把僵死状态改为僵死撤销状态。

        通过p->state=TASK_RUNNING方式赋值state字段,内核也使用set_task_state和set_current_state宏分别设置指定进程状态和当前执行进程的状态。

2.标识一个进程

        32位(仅仅是一个逻辑地址的偏移量部分)进程描述符地址标识进程。进程描述符指针指向这些地址,内核对进程的大部分引用通过进程描述符的指针进行。

        通过进程标识符PID数来标识进程,PID存放在进程描述符的pid字段中。PID被顺序编号,逐个+1,当达到最大值时要循环使用闲置的小PID号。缺省状态下PID最大值是32767(PID_MAX_DEFAULT-1),可以通过/proc/sys/kernel/pid_max文件更改减小PID上限值,在64位结构中,可以把PID上限扩大到4194303。

        内核通过pidmap-array位图表示当前已分配的和闲置的PID号。一个页框包含32768个位,在32位体系结构中pidmap-array位图存放在一个单独的页中。64位体系结构中,内存分配超过当前位图大小的PID号时,为PID位图增加更多页。系统会一直保存这些页不被释放。

        因为一个多线程应用中的所有线程都必须有相同的PID,Linux引入线程组,一个组中的所有线程使用该组领头线程的PID,被存入线程描述符的tgid字段中,getpid()返回该字段。

进程描述符处理

        进程是动态实体,内核把进程描述符存放在动态内存中。对于每个进程来说,Linux把内核态的进程堆栈和紧挨进程描述符的线程描述符(thread_info)存放在一个单独为进程分配的存储区域。这块区域大小通常为8192字节(2页框),且占据两个连续的页框,并让区域起始位置是2^{13}的倍数。为防止因存在大量碎片而没有可用连续内存时,在80x86体系结构中,编译时可以设置使得内核栈和线程描述符跨越一个单独的页框

        线程描述符驻留于内存区的开始,栈从末端向下增长。esp寄存器是CPU栈指针,存放栈顶单元的地址,一旦数据写入堆栈,esp值递减。因为thread_info是52结构字节长,因此内核栈能扩展到8140字节。内核使用alloc_thread_info和free_thread_info宏分配和释放存储thread_info结构和内核栈的内存区

标识当前进程

        内核栈和线程描述符紧密结合的好处:内核很容易从esp寄存器的值获得当前在CPU上正在运行进程的thread_info结构的地址。如果thread_union结构长度是8K,则内核屏蔽掉esp最低13位有效位就可以获得thread_info的基地址;如果thread_union结构长度是4k,要屏蔽esp低12位有效位。通过current_thread_info()函数完成。汇编指令如下(不加括号:寄存器里的值,加括号:寄存器表示的地址上的值):

        进程常用进程描述符地址,内核调用current宏获得当前CPU上运行进程的描述符指针。

         因为task字段在thread_info结构中的偏移量为0,所以执行完这三条指令之后,p就包含在CPU上运行的进程的描述符的指针。current宏经常作为进程描述符字段的前缀出现在内核代码中。例如current->pid返回在CPU上正在执行的进程的PID。

双向链表

        Linux内核定义了list_head数据结构,字段next和prev分别表示通用双向链表向前和向后的指针元素。list_head字段的指针中存放的是另一个list_head字段的地址,而不是含有list_head结构的整个数据结构地址。

         新链表用LIST_HEAD(list_name)宏创建,它申明类型为list_head的新变量list_name,此变量作为新链表头的占位符,是一个哑元素。LIST_HEAD(list_name)宏还初始化list_head数据结构的prev和next字段,让它们指向list_name变量本身。

        Linux2.6内核还支持一种不同于list_head的用于散列表的非循环双向链表,对散列表而言重要的是空间。表头存放在hlist_head中,是指向表的第一个元素的指针。每个元素都是hlist_node类型的数据结构,next指针指向下一个元素,pprev指向前一个,第一个元素的pprev和最后一个元素的next指向NULL。

进程链表

        进程链表是双向链表,把所有进程的描述符连接起来。每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev和next字段分别指向前面和后边的task_struct元素。

        进程链表的头是init_task描述符,他是0进程或swapper进程的进程描述符。init_task的prev字段指向最后插入的进程描述符的tasks字段。

TASK_RUNNING状态的进程链表

        内核只寻找TASK_RUNNING状态的进程进入CPU运行。通过建立多个不同优先级的可运行进程链表提高调度程序运行速度。每个task_struct描述符包含一个list_head类型的字段run_list,如果进城优先权为k(0-139),run_list字段把该进程链入优先权为k的可运行进程的链表中。每个CPU都有自己的进程链表集

        运行队列的主要数据结构还是进程描述符链表,进程描述符的prio字段存放进程的动态优先权,所有这些链表都由一个单独的prio_array_t数据结构实现,字段如下:

3.进程间的关系

         进程描述符中表示进程亲属关系的字段如下图所示,进程1(init)是所有进程的祖先:

        下图显示了一组进程间的亲属关系,进程P0创建了P1,P2,P3。进程P3又创建了P4。进程之间存在除下图中的其他关系:一个进程可能是一个进程组或登录会话的领头进程,也可能是一个线程组的领头进程,还可能跟踪其他进程的执行

        建立非亲属关系的进程描述符字段如下所述:

pidhash表及链表

         在某些情况下,内核必须能从进程的PID导出对应的进程描述符指针。顺序扫描进程链表并检查进程描述符的pid字段相当低效。因为进程描述符包含了表示不同类型PID的字段,每种类型的PID都需要他自己的散列表,因此引入4个散列表加速查找,散列表和进程描述符中相关字段说明如下:

         内核初始化期间动态地为4个散列表分配空间,并把它们的地址存入pid_hash数组。一个散列表的长度依赖于可用RAM的容量。如果一个系统有512MB的RAM,那么每个散列表就被存在4个页框中,可以拥有2048个表项

        用pid_hashfn宏把PID转化为表索引:

        #define pid_hashfn(x) hash_long((unsigned  long)x,pidhash_shift)

        hash_long()在32位体系结构中基本等价于:

        变量pidhash_shift用来存放表索引的长度(以位为单位,例子:11),所以pid_hashfn的取值范围是0到2^{11}-1=2047。pidhash表及链表如下图所示:

        两个不同PID可能会散列到相同的表索引而冲突。Linux利用双向链表来处理冲突的PID

        PID散列表可以为包含在一个散列表中的任何PID号定义进程链表。上图中的PID结构的字段如下所示:

         下图给出PIDTYPE_TGID类型的散列表的例子。pid_hash数组的第二个元素存放的事散列表的地址,也就是用hlist_head结构的数组表示的链表的头。线程组4351的PID链表:散列表中的进程描述符的pid_list字段中存放链表的头,同时每个PID链表中指向前一个和后一个元素的指针也放在每个链表元素的pid_list字段中。

4.如何组织进程

        运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起,当要把其他状态的进程分组时,不同状态的处理要求不同:

        没有为处于TASK_STOPPED、EXIT_ZOMBIE、EXIT_DEAD状态的进程建立专门的链表。由于对处于暂停、僵死、死亡状态进程的访问比较简单,或者通过PID,或者通过特定父进程的子进程链表,所以不必对这三种状态进程分组。

等待队列

        进程必须经常等待某些事件的发生,例如等待释放系统资源,等待一个磁盘操作的停止。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒他们。

        等待队列由双向链表实现,每一个等待队列都有一个等待队列头wait_queue_head_t以及多个队列元素wait_queue_t:

         因为等待队列是由终端处理程序和主要内核函数修改的,因此必须对其双向链表进行保护以免其进行同时访问。同步是通过等待队列头中的lock自旋锁达到的。task_list字段是等待进程链表的头。

       wait_queue_t字段:task存放进程等待的事件的描述符地址;task_list字段包含的是把元素连接到等待相同事件的进程链表中的指针;flags表示进程是互斥进程(flags=1)还是非互斥进程(flags=0);func字段表示登台队列中睡眠进程的唤醒方式。

【P102-P105是等待队列的操作代码】

进程资源限制

        每个进程都有一组相关的资源限制,限制制定了进程能使用的系统资源数量。

        对当前进程的资源限制存放在current->signal->rlim字段,即进程的信号描述符的一个字段。该字段是类型为rlimit结构的数组,每个资源限制对应一个元素:

         rlim_cur字段是资源的当前资源限制。例如:current->signal->rlim[RLIMIT_CPU].rlim_cur表示正运行进程所占用CPU时间的当前限制。rlim_max字段是资源限制所允许的最大值。超级用户可以进行资源限制的修改。资源限制字段如下所示。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值