进程

第三章


进程


进程是任何多道程序设计的操作系统中的基本概念。通常把进程定义为程序执行的一个实例,因此,如果16个用户同时运行vi,那么就有16个独立的进程。在Linux源代码中,常把进程称为任务或线程。


进程、轻量级进程和线程


术语“进程”在使用中常有几个不同的含义。这里的定义为:进程是程序执行时的一个实例。你可以把它看作充分描述程序已经执行到何种程度的数据结构的汇集。


进程类似于人类:它们被产生,有或多或少有效的生命,可以产生一个或多个子进程,最终都要死亡。一个微小的差异是进程之间没有性别差异-------每个进程只有一个父亲。


从内核观点看,进程的目的就是相当分配系统资源的实体。


当一个进程创建时,它几乎与你进程相同。它接受父进程地址空间的一个拷贝,并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享有程序代码的页,但是它们各有独立的数据拷贝(栈和堆),因此子进程对一个内存单元的修改对父进程是不可见的。


尽管早其Unix内核使用了这种简单模式,但是,现代Unix系统并没有如此使用,它们支持多线程应用程序--------拥有很多相对独立执行流的用户程序共享应用程序的大部分的大部分数据结构。在这样的系统中,一个进程由几个用户线程组成。每个线程都代表进程的一个执行流。现在,大部分多线程应用程序都是用pthread库的标准库函数集编写的。


Linux内核的早期版本没有提供多线程应用的支持。从内核观点看,多线程应用程序仅仅是一个普通进程。多线程应用程序多个执行流的创建、处理、调度调整都是在用户态进行的。


但是,这种多线程应用程序的实现方式不那么令人满意。例如,假设一个象棋程序使用两个线程:其中一个控制图形化棋盘,等待人类选手的移动并显示计算机的移动,而另一个思考棋的下一步移动。尽管第一个线程等待选手移动时,第二个线程应当继续运行,以此利用选手的思考时间。但是,如果象棋程序仅是一个单独的进程。每一个线程就不能简单地发出等待用户行为的阻塞系统调用;否则,第二个线程也被阻塞。相反,第一个线程必须使用复杂的非阻塞技术来确保进程仍然是可运行的。


Linux使用轻量级进程对多线程应用程序提供更好的支持。两个轻量级进程基本上可以共享一些资源,诸如地址空间、打开文件等等。只要其中一个修改共享资源,另一个就立即查看这种修改。当然,当两个线程访问共享资源时就必須同步它们自己。


实现多线程应用程序的一个简单方式就是把轻量级进程与每个线程关联起来。这样,线程之间就可以通过简单地共享同一内存地址空间、同一打开文件集等来访问相同的应用程序数据结构集;同时,每个线程都可以由内核独立调度,以便一个睡眠的同时另一个仍然是可以运行的。


POSIX兼容的多线程应用程序由支持“线程组”的内核来处理最好不过。在Linux中,一个线程组基本上就是实现了多线程应用的一组轻量级进程,对于像getpid()kill(),和_exit()这样的一些系统调用,它像一个组织,起整体的作用。


进程描述符


为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。例如,内核必须知道进程的优先级,它是正在CPU上运行还是因某些事件而被阻塞,给它分配了什么样的地址空间,允许它访问哪个文件等等。这正是进程描述符的作用-------进程描述符都是task_struct数据结构,它的字段包含了与一个进程相关的所有信息。因为进程描述符中存放了那么多信息,所以它是相当复杂的。它不仅包含了很多进程属性的字段,而且一些字段还包括了指向其它数据结构的指针。依些类推,图3-1示意性地描述了Linux的进程描述符。


图右边的六个数据结构涉及进程所拥有的特殊资源,这些资源将在以后的章节中涉及。本章集中讨论两种字段:进程的状态和进程的父/子关系。


进程的状态


顾名思议,进程描述符中的state字段描述了进程当前所处的状态。它由一组标志组成,其中每个标志描述一种可能的进程状态。在当前的Linux版本中,这些状态是互斥的。因为,严格意义上说,只能设置一种状态;其余的标志被清除。下面是进程可能的状态:


可运行状态

进程要么在CPU上执行,要么准备执行。

可中断的等待状态

进程被挂起,直到某个条件变为真。产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号都是可以唤醒进程的条件。

不可中断的等待状态

与可中断的等待状态类似,但有一个例外,把信号传递到睡眠进程不能改变它的状态。这种状态很少用到,但在一些特定的情况下,这种状态是很有用的。例如,当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备进会用到这种状态。探测完成以前,设备驱动程序不能被中断,否则,硬件设备会处于不可预知的状态。

暂停状态

进程的执行被暂停。当进程接收到SIGSTOPSIGTSTPSIGTIINSIGTTOU信号后,进入暂停状态。

跟踪状态

进程的执行已由debugger程序暂停。当一个进程被另一个进程监控时,任何信号都可以把这个进程置于TASK_TRACED状态。


还有两个进程状态是既可以存放在进程描述符的state字段中,也可以存放在exit_state字段中。从这两个字段的名称可以看出,只有当进程的执行被终止时,进程的状态才会变为这两种状态中的一种:


僵死状态

进程的执行被终止,但是,父进程还没有发布wait4()waitpid()系统调用来返回有关死亡进程的信息。发布wait()类系统调用前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能还需要它。


僵死撤消状态

最终状态:由于父进程则发出wait4()waitpid()系统调用,因而进程由系统删除。为了防止其它执行线程在同一个进程上也执行wait()类系统调用,而把进程的状态由僵死撤消状态。


state字段的值通常用一个简单的赋值语句设置。例如:


p->state= TASK_RUNNING

内核也使用set_task_stateset_current_state宏;它们分别设置指定进程的状态和当前执行进程的状态。此外,这些宏确保编译程序或CPU控制单元不把赋值操作与其它指令混合。混合指令的顺序有时会导致灾难性的后果。


标识一个进程


一般来说,能被独立调度的每个执行上下文都必须拥有它自己的进程描述符;因此,即使共享内核大部分数据结构的轻量级进程,也有它们自己的task_struct结构。


进程和进程描述符之间有非常严格的一一对应关系,这使得用32位进程描述符地址标识进程成为一种方便的方式。进程描述符指针指向这些地址,内核对进程的大部分引用是通过进程描述符指针进行的。


另一方面,类Unix操作系统允许用户使用一个叫做进程标识符processId的数来标识进程,PID存放在进程描述符的pid字段中。PID被顺序编号,新创建进程的PID通常是前一个进程的PID1。不过,PID的值有一个上限,当内核使用的PID达到这个上限值的时候就必须循环使用已闲置的小PID号。在缺省情况下,最大的PID号是32767;系统管理员可以通过往/proc/sys/kernel/pid_max这个文件中写入一个更小的值来减少PID的上限值,使PID的上限小于32767。在64位体系结构中,系统管理员可以把PID的上限扩大到4194303


由于循环使用PID编号,内核必须通过管理一个pidmap-array位图来表示当前已分配的PID号和闲置的PID号。因为一个页框包含32768个位。所以在32位体系结构中pidmap-array位图存放在一个单独的页中。然而,在64位体系结构中,当内核分配了超过当前位图大小的PID号时,需要为PID位图增加更多的页。系统会一直保存这些页不被释放。


Linux把不同的PID与系统中每个进程或轻量级进程相关联。这种方式能提供最大的灵活性,因为系统中每个执行上下文都可以被唯一地识别。


另一方面,Unix程序员希望同一组中的线程有共同的PID。例如,把指定PID的信号发送给所有线程。事实上,POSIX1003.1c标准规定一个多线程应用程序中的所有线程都必須有相同的PID


遵照这个标准,Linux引入线程组的表示。一个线程组中的所有线程使用和该线程组的领头线程有相同的PID,也就是该组中第一个轻量级进程的PID,它被存入进程描述符的tgid字段中。getpid()系统调用返回当前进程的tgid值而不是pid的值,因此,一个多线程应用的所有线程共享相同的PID。绝大多数进程都属于一个线程组,包含单一的成员;线程组的领头线程其tgid的值与pid的值相同,因而getpid()系统调用对这类进程所起的作用和一般进程是一样的。


下面,我们将向你说明如何从进程的PID中有效地导出它的描述符指针。效率至关重要,因为像kill()这样的很多系统调用使用PID表示所操作的过程。


进程描述符处理


进程是动态实体,其生命周期范围从几毫秒到几个月。因此,内核必须能够同时处理很多进程,并把进程描述符存在动态内存中,而不是放在永久分配给内核的存储区。对每个进程来说,Linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨进程描述符的小数据结构thread_info,叫做线程描述符,这块存储区域的大小通常为8192个字节。考虑到效率的因素,内核让这8K空间占据连续的两个页框并让第一个页框的起始地址是213次方的倍数。当几乎没有可用的动态内存时,就会很难找到这样的两个连续页框,因为空闲空间可能存在大量碎片。因此,在80x86体系结构中,在编译时可以进行设置,以使内核栈和线程描述符跨越一个单独的页框。内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所有的栈。因为内核控制路径使用很少的栈,因此只需要几千个字节的内核态堆栈。所以,对栈和thread_info结构来说,8KB足够了。不过,当使用一个页框存放内核态堆栈和thread_info结构时,内核要采用一些额外的栈以防止中断和异常的深度嵌套而引起的溢出。


3-2显示了在2页内存区中存放两种数据结构的方式。线程描述符驻留于这个内存区的开始,而栈从末端向下增长。该图还显示了分别通过taskthread_info字段使thread_info结构与task_struct结构互相关联。


esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的,因此,esp寄存器指向这个栈的顶端。


一旦数据写入堆栈,esp的值就递减。因为thread_info结构是52个字节长,因此,内核栈能扩展到8140个字节。


C语言使用下列的联合结构方便地表示一个进程的线程描述符和内核栈:

unionthread_union{

structthread_info thread_info;

unsignedlong stack[2048];

};

如图3-2所示,thread_info结构从0x015fa000地址开始存放,而栈从0x015fc000地址开始存放。Esp寄存器的值指向地址为0x015fa878的当前栈顶。


内核使用alloc_thread_infofree_thread_info宏分配和释放存储thread_info结构和内核栈的内存区。


标识当前进程


从效率的观点来看,刚才所讲的thread_info结构与内核态堆栈之间的紧密结合提供的主要好处是:内核很容易从esp寄存器的值获得当前在CPU上正在运行的进程的thread_info结构的地址。事实上,如果thread_union结构长度是8K,则内核屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址;而如果thread_union结构长度是4K,内核需要屏蔽掉esp的低12位有效位。这项工作由current_thread_info()函数来完成,它产生如下一些汇编指令:


movl$0xffffe000,%ecx

andl%esp,%ecx

movl%ecx,p

这三条指令执行后,p就包含在执行指令的CPU上运行的进程的thread_info结构的指针。


进程最常用的是进程描述符的地址不是thread_info结构的地址。为了获得当前在CPU上运行进程的描述符指针,内核要调用current宏。该宏本质上等价于current_thread_info()->task,它产生如下汇编语言指令:


movl$0xffffe000,%ecx

andl%esp,%ecx

movl(%ecx),p


因为task字段在thread_info结构中的偏移量为0,所以执行完这三条指令之后,p就包含在CPU上运行进程的描述符指针。


current宏经常作为进程描述符字段的前缀出现在内核代码中,例如,current->pid返回在CPU上正在执行的进程的PID


用栈存放进程描述符的另一个优点体现在多处理器系统上:如前所述,对于每个硬件处理器,仅通过检查栈就可以获得当前正确的进程。早先的Linux版本没有把内核栈与进程描述符放在一起,而是强制引入全局静态变量current来标识正在运行进程的描述符。在多处理器系统上,有必要把current定义为一个数组,每一个元素对应一可用CPU


双向链表


在继续阐述内核跟踪系统中各种进程的细节之前,先着重说明实现双向链表的特殊数据结构的作用。


对每个链表,必須实现一组原语操作:初始化链表,插入和删除一个元素,扫描链表等。这可能既浪费开发人员的精力,也因为对每个不同的链表都要重复相同的原语操作而造成存储空间的浪费。


因此,Linux内核定义了list_head数据结构,字段nextprev分别表示通过双向链表向前和向后的指针元素,不过,值得特别关注是,list_head字段的指针中存放的是另一个list_head字段的地址,而不是含有list_head结构的整个数据结构地址。


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


有几个实现原语的函数和宏,如表3-1所示。


Linux2.6内核支持另一种双向链表,其与list_head有着明显的区别,因为它不是循环链表,主要用于散列表,对散列表而言重要的是空间而不是固定的时间内找到表中的最后一个元素。表头存放在hlist_head数据结构中,该结构只不过是指向表的第一个元素的指针。每个元素都是hlist_node类型的数据结构,它的next指针指向下一个元素,pprev指针指向前一个元素的next字段。因为不是循环链表,所以第一个元素的pprev字段和最后一个元素的next字段都置为NULL。对这种表可以用类似表3-1中的函数和宏来操纵。


进程链表


我们首先介绍双向链表的第一个例子------进程链表,进程链表把所有进程的描述符链接起来。每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prevnext字段分别指向前面和后面的task_struct元素。


进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。init_tasktasks.prev字段指向链表中最后插入的进程描述符的tasks字段。


SET_LINKSREMOVE_LINKS宏分别用于从进程链表中插入和删除一个进程描述符。这些宏考虑了进程间的父子关系。


还有一个很有用的宏就是for_each_process,它的功能是打描事个进程链表,其定义如下:


这个宏是循环控制语句,内核开发都利用它提供循环。注意init_task进程描述符是如何起到链表头作用的。这个宏从指向init_task的指针开始,把指针移动下一个任务,然后继续,直到又到init_task为止。在每一次循环时,传递给这个宏的参变量中存放的是当前被打描进程描述符的地址,这与list_entry宏的返回值一样。


TASK_RUNNING状态的进程链表


当内核寻找一个新进程在CPU上运行时,必须只考虑可运行进程。


早先的Linux版本把所有的可运行进程都放在同一个叫做运行队列的链表中,由于维持链表中的进程按优先级排序开销过大,因此,早期的调度程序不得不为选择“最佳”可运行进程而扫描整个队列。


Linux2.6实现的运行队列有所不同。其目的是让调度程序能在固定的时间内选出”最佳”可运行进程,与队列中可运行的进程数无关。


提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先权对应一个不同的链表,每个task_struct描述符包含一个list_head类型的字段run_list,如果进程的优先权等于krun_list字段把该进程链入优先权为k的可运行进程的链表中。此外,在多处理器系统中,每个CPU都有它自己的运行队列,即它自己的进程链表集。这是一个通过使数据结构更复杂来改善性能的典型例子:调度程序的操作效率的确提高了,但运行队列的链表却为此而被拆分成140个不同的队列!


正如我们将看到的,内核必须为系统中每个运行队列保存大量的数据,不过运行队列的主要数据结构还是组成运行队列的进程描述符链表,所有这些链表都由一个单独的prio_array_t数据结构来实现,其字段说明如表3-2所示。


enqueue_task(p,array)函数把进程描述符插入某个运行队列的链表,其代码本质上等同于:


进程描述符prio字段存放进程的动态优先权,而array字段是一个指针,指向当前运行队列的prio_array_t数据结构。类似地,dequeue_task(p,array)函数从运行队列的链表中删除一个进程描述符。


进程间的关系


程序创建的进程具有父/子关系。如果一个进程创建多个子进程时,则子进程之间具有兄弟关系。在进程描述符中引入几个字段来表示这些关系,表示给定进程P的这些字段列在表3-3中。进程0和进程1由内核创建的:稍后我们将看到,进程1是所有进程的祖先。


3-4显示了一组进程间的亲属关系。进程P0接连还创建了P1P2,和P3。进程P3创建了P4


特别要说明的是,进程之间还存在其它关系:一个进程可能是一个进程组或登录会话的领头进程,也可能是一个线程组的领头进程,它还可能跟踪其它进程的执行。表3-4列出进程描述符中的一些字段,这些字段建立起了进程P和其它进程之间的关系。


Pidhash表及链表


在几种情况下,内核必須能从进程的PID导出对应的进程描述符指针。例如,为kill()系统调用时就会发生这种情况:当进程P1希望向另一进程P2发送一个信号时,P1调用kill()系统调用,其参数为P2PID,内核从这个PID导出其对应的进程描述符,然后从P2的进程描述符中取出记录挂起信号的数据结构指针。


顺序扫描进程链表并检查进程描述符的pid字段是可行但相当低效的。为了加速查找。引入了4个散列表。需要4个散列是因为进程描述符包含了表示不同类型PID的字段,而且每种类型的PID需要它自己的散列表。


内核初始化期间动态地为4个散列表分配空间,并把它们的地址存入pid_hash数组。一个散列表就被存在4个页框中,可以拥有2048个表项。


pid_hashfn宏把PID转化为表索引,pid_hashfn宏展开为:

#definepid_hashfn(x) hash_long((unsigned long)x,pidhash_shift)

变量pidhash_shitf用来存放表索引的长度。很多散列函数都使用hash_long(),在32位体系结构中它基本等价于


因为在我们的例子中pidhash_shift等于11,所以pid_hashfn的值范围是0211次减1


正如计算机科学的基础课程所阐述的那样,散列函数并不总能确保PID与表的索引一一对应。两个不同的PID散列到相同的表索引称为冲突。


Linux利用链表来处理冲突的PID,每一个表项是由冲突的进程描述符组成的双向链表。图3-5显示了具有两个链表的PID散列表。进程号为289029384的两个进程散列到这个表的第200个元素,而进程号为29385的进程散列到这个表的第1466个元素。


具有链表的散列法比从PID到表索引的线性转换更优越,这是因为在任何给定的实例中,系统中的进程数总是远远小于32768。如果在任何给定的实例中大部分表项都不使用的话,那么把表定义为32768项会是一种浪费。


由于需要跟踪进程间的关系,PID散列表中使用的数据结构非常复杂。看一个例子:假设内核必须回收一个指定线程组中的所有进程,这意味着这些进程的tgid的值是相同的,都等于一个给定值。如果根据线程组号找散列表,只能返回一个进程描述符,就是线程组领头进程的描述符。为了能快速返回组中其它所有进程,内核就必须为每个线程组保留一个进程链表。在查找给定登录会话或进程组的进程时也会有同样的情形。


PID散列表的数据结构解决了所有这些难题,因为它们可以为包含在一个散列表中的任何PID号定义进程链表。最主要的数据结构是四个pid结构的数组。它在进程描述符的pid字段中,表3-6显示了pid结构的字段。


3-6给出了PIDTYPE_TGID类型散列表的例子。pid_hash数组的第二个元素存放散列表的地址,也就是用hlist_head结构的数组表示链表的头。在散列表第71项为起点形成的链表中,有两个PID号为2464351的进程描述符。PID的值存放在pid结构的nr字段中,而pid结构在进程描述符中。我们考虑线程组4351PID链表:散列表中的进程描述符的pid_list字段中存放链表的头,同时每个PID链表中指向前一个元素和后一个元素的指针也存放在每个链表元素的pid_list字段中。


下面是处理PID散列表的函数和宏:


do_eash_task_pid(nr,type,task)

while_each_task_pid(nr,type,task)

标记do-while循环的开始和结束,循环作用在PID值等于nrPID链表上,链表的类型由参数type给出,task参数指向当前被扫描的元素的进程描述符。

find_task_by_pid_type(type,nr)

type类型的散列表中查找PID等于nr的进程,该函数返回所匹配的进程描述指针,若没有匹配的进程,函数返回NULL

find_task_by_pid(nr)

find_task_by_pid_type(type,nr)相同。

attach_pid(task,type,nr)

task指向的PID等于nr的进程描述符插入type类型的散列表中。如果一个PID等于nr的进程描述符已经在散列表中,这个函数就只把task插入已有的PID进程链表中。

detach_pid(task,type)

type类型的PID进程链表中删除task所指向的进程描述符。如果删除后PID进程链表没有变成空,则函数终止,否则,该函数还要从type类型的散列表中删除进程描述符。最后,如果PID的值没有出现任何其它的散列表中,为了这个值能够被反复使用,该函数还必須清除PID位图中的相应们。

next_thread(task)

返回PIDTYPE_TGID类型的散列链表中task指示的下一个轻量级进程的进程描述符。由于散列链表是循环的,若应用于传统的进程,那么该宏返回进程本身的描述符地址。


如何组织进程

 

运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起。当要把其它状态的进程分组时,不同的状态要求不同的处理,Linux选择了下列方式之一:

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

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

 

等待队列

等待队列在内核中有很多用途,尤其用在中断处理、进程同步及定时。因为这些主题将在以后的章节中讨论,所以我们中在这里面说明,进程必须经常等待某些事件的发生,例如,等待一个磁盘操作的终止,等待释放系统资源,或等待时间经过固定的间隔。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。

 

等待队列由双向链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头,等待队列头是一个类型为wait_queue_head_t的数据结构:

struct __wait_queue_head{

       spinlock_t lock;

       struct list_head task_list;

};

typedef struct __wait_queue_head wait_queue_head_t;;

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

等待队列链表中的元素类型为wait_queue_t:

struct __wait_queue {

       unsigned int flags;

       struct task_struct *task;

       wait_queue_func_t func;

       struct      list_head task_list

};

typedef struct __wait_queue wait_queue_t;

 

等待队列链表中的每个元素代表一个睡眠进程,该进程等待某一事件的发生:它的描述符地址存放在task字段中。task_list字段中包含的是指针,由这个指针把一个元素链接到等待相同事件的进程链表中。

 

然而,要唤醒等待队列中所有睡眠的进程有时并不方便。例如,如果两个或多个进程正在等待互斥访问某一要释放的资源,仅唤醒等待队列中的一个进程才有意义。这个进程占有资源,而其它进程继续睡眠。

 

因此,有两种睡眠进程:互斥进程由内核有选择地唤醒,而非互斥进程总是由内核在事件发生时唤醒。等待访问临界资源的进程就是互斥进程的典型例子。等待相关事件的进程是非互斥的。例如,我们考虑等待磁盘传输结束的一组进程:一但磁盘完成,所有等待的进程都会被唤醒。正如我们将在下面所看到的那样,等待队列元素func字段用来表示等待队列中睡眠进程应该有什么方式唤醒。

 

等待队列的操作

 

可以用DECLARE_WAIT_QUEUE_HEAD(name)宏定义一个新等待队列的头,它静态地声明一个叫name的等待队列的头变量并对该变量的lock和task_list字段进行初始化。函数init_waitqueue_head()可以用来初始化动态分配的等待队列的头变量。

 

函数init_waitqueue_entry(q,p)如下所示初始化wait_queue_t结构的变量q:

q->flags = 0;

q->task = p;

q->func = default_wake_function;

非互斥进程p将由default_wake_function()唤醒,default_wake_function()是在第七章中要讨论的try_to_wake_up()函数的一个简单的封装。

 

也可以选择DEFINE_WAIT宏声明一个wait_queue_t类型的新变量,并用CPU上运行的当前进程的描述符和唤醒函数autoremove_wake_function()的地址初始化这个新变量。这个调用default_wake_function()来唤醒睡眠进程,然后从等待队列的链表中删除对应的元素。最后,内核开发都可以通过init_waitqueue_func_entry()函数来自定义唤醒函数,该函数负责初始化等待队列的元素。

 

一但定义了一个元素,必须把它插入等待队列。add_wait_queue()函数把一个非互斥进程插入等待队列链表的第一个位置。add_wait_queue_exclusive()函数把一个互斥进程插入等待队列链表的最后一个位置。remove_wait_queue()函数从等待队列链表中删除一个进程。waitqueue_active()函数检查一个给定的等待队列是否为空。

 

要等待特定条件的进程可以调用如下列表中的任何一个函数。

(1)   sleep_on()对当前进程进行操作:

void sleep_on(wait_queue_head_t *wq)

{

        wait_queue_t wait

        init_waitqueue_entry(&wait,current);

        current->state = TASK_UNINTERRUPTIBLE;

        add_wait_queue(wq,&wait);

        schedule();

        remove_wait_queue(wq,&wait);

}

该函数把当前进程的状态设置为TASK_UNINTERRUPTIBLE,并把它插入到特定的等待队列。然后,它调用调度程序,而调度程序重新开始另一个程序的执行。当睡眠进程被唤醒时,调度程序重新开始执行sleep_on()函数,把该进程从等待队列中删除。

(2)   interruptible_sleep_on()与sleep_on()函数是一样的,但稍有不同,前者把当前进程的状态设置为TASK_INERRUPTIBLE而不是TASK_UNINTERRUPTIBLE,因此,接受一个信号就可以唤醒当前进程。

(3)   sleep_on_timeout()和interruptible_sleep_on_timeout()与前面函数类似,但它们允许调用都定义一个时间间隔,过了这个间隔以后,进程交由内核唤醒。为了做到这点,它们调用shedule_timeout()函数而不是schedule()函数。

(4)   在Linux 2.6中引入的prepare_to_wait() prepare_to_wait_exclusive()和finish_wait()函数提供了另外一种途径来使当前进程在一个等待队列中睡眠。它们的典型应用如下:

DEFINE_WAIT(wait);

prepare_to_wait_exclusive(&wq,&wait,TASK_INTERRUPTIBLE);

if(!condition)

schedule();

finish_wait(&wq,&wait);

函数prepare_to_wait()和prepare_to_wait_exclusive()用传递的第三个参数设置进程的状态,然后把等待队列元素的互斥标志flag分别设置为0或1,最后,把等待元素wait插入到以wq为头的等待队列的链表中。

进程一但被唤醒就执行 finish_wait()函数,它把进程的状态再次设置为TASK_RUNNING,并从等待队列中删除等待元素。

(5)   wait_event 和 wait_event_interruptible宏使它们的调用进程在等待队列上睡眠,一直到修改了给定条件为止。例如,宏wait_event(wq,condition)本质上实现下面的功能

DEFINE_WAIT(__wait);

for(;;){

prepare_to_wait(&wq,&__wait,TASK_UNINTERRUPTIBLE);

if(condition)

        break;

        schedule();

}

finish_wait(&wq,&__wait);

对上面列出的函数做一些说明:sleep_on()类函数在以下条件下不能使用,那就是必须测试条件并且当条件还没有得到验证时又紧接着让进程去睡眠;由于那些条件是众所周知的竞争条件产生的根源,所以不鼓励这样使用。此外,为了把一个互斥进程插入等待队列,内核必须使用prepare_to_wait_exclusive()。

所有其它的相关函数把进程当作非互斥进程来插入。最后,除非使用DEFINE_WAIT或finish_wait(),否则内核必须在唤醒等待进程后从等待队列中删除对应的等待队列元素。

 

内核通过下面的任何一个宏唤醒等待队列中的进程并把它们的状态置为TASK_RUNNING;wake_up,wake_up_nr,wake_up_all,wake_up_interruptible,wake_up_interruptible_nr,wake_up_interruptible_all,wake_up_interruptible_sync和wake_up_locked.从每个宏的名字我们可以明白其功能。

(1)    所有宏都考虑到处于TASK_INTERRUPTIBLE状态的睡眠进程;如果宏的名字中不含有字符串”interruptible”,那么处于TASK_UNINTERRUPTIBLE状态的睡眠进程也被考虑到。

(2)    所有宏都唤醒具有请求状态的所有非互斥进程。

(3)    名字中含有nr字符串的宏唤醒给定数的具有请求状态的互斥进程;这个数字是宏的一个参数。名字中含有all字符串的宏只唤醒具有请求状态的所有互斥进程。最后,名字中不含nr或all字符吕的宏只唤醒具有请求状态的一个互斥进程。

(4)    名字中不含有sync字符串的宏检查被唤醒进程的优先级是否高于系统中正在运行进程的优先级,并在必要时高用schedule().这些检查并不是名字中含有sync字符串的宏进行的,造成的结果是高优先级进程的执行稍有延迟。

(5)    wake_up_locked宏和wake_up宏相类似,仅有的不同是当wait_queue_head_t中的自旋乐已经被持有进要调用wak_up_locked。

进程资源限制


每个进程都有一组相关的资源限制,限制指定了进程能使用的系统资源数量。这些限制避免用户过分使用系统资源。Linux承认以下表3-7中的资源限制。


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


Structrlimit{

unsignedlong rlim_cur;

unsignedlong rlim_max;

};


rlim_cur字段是资源的当前资源限制。例如,current->signal->rlim[RLIMIT_CPU].rlim_cur表示正运行进程所占用CPU时间的当前限制。

rlim_max字段是资源限制所允许的最大值。利用getrlimit()setrlimit()系统调用,用户总能把一些资源的rlim_cur限制增加到rlim_max。然而,只有超级用户才能改变rlim_max字段,或把rlim_cur字段设置成大于相应rlim_max字段的一个值。


大多数资源限制包含值RLIMIT_INFINITY,它意味着没有对相应的资源施加用户限制。然而,系统管理员可以给一些资源选择施加更强的限制。只要用户注册进系统,内核就创建一个由超级用户拥有的进程,超级用户能调用setrlimit()以减少一个资源rlim_maxrlim_cur字段的值。随后,同一进程执行一个loginshell。该进程就变为由用户拥有。由用户创建的每个进程都继承其父进程rlim数组的内容。因此,用户不能忽略系统强加的限制。



 



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值