在详细讲解运行进程的组织及调度的一些列课题前,先把Linux内核中如何组织非运行状态进程的那些机制梳理一遍。
运行队列链表rq把处于TASK_RUNNING状态的所有进程组织在一起。当要把其他状态的进程分组时,不同的状态要求不同的处理,Linux选择了下列方式之一:
(1) 没有为处于TASK_STOPPED、EXIT_ZOMBIE 或 EXIT_DEAD状态的进程建立专门的链表。由于处于暂停、僵死、死亡状态进程的访问比较简单,或者通过PID,或者通过特定父进程的子进程链表,所以不必对这三种状态进程分组。
(2) 对于处于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态的进程将会被划分进若干的类别中,每个类别代表了一个指定的事件。在这里,进程状态并没有提供足够的信息来迅速地访问到进程,所以有必要引入一个额外的进程链表。它们叫做等待队列,我们下面将会着重讨论。
等待队列在内核中有很多用途,尤其用在中断处理、进程同步及定时。因为这些主题将在以后博文中讨论,所以我们只在这里说明,进程必须经常等待某些事件发生,例如,等待一个磁盘操作的终止,等待释放系统资源,或等待时间经过固定的间隔。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。
等待队列由双向链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头,等待队列头 是一个类型为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字段中包含的是指针,由这个指针把一个元素链接到等待相同事件的进程链表中 。
然而,要唤醒等待队列中所有睡眠的进程有时并不方便。例如,如果两个或多个进程正在等待互斥访问某一要释放的资源,仅唤醒等待队列中的一个进程才有意义。这个进程占有资源,而其他进程继续睡眠。
因此,有两种睡眠进程:互斥进程(队列元素的flags字段为1 )由内核有选择地唤醒,而非互斥进程(flags值为0 )总是由内核在事件发生时全部唤醒。等待访问临界资源的进程就是互斥进程的典型例子。等待相关事件的进程是非互斥的。例如,我们考虑等待磁盘传输结束的一组进程:一旦磁盘传输完成,所有等待的进程都会被唤醒。下面,我们将详细讲解,等待队列元素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( )的地址初始化这个新变量的func字段。这个函数调用default_wake_function()函数来唤醒进程,然后从等待队列的链表中删除对应的元素(每个等待队列链表中的一个元素其实就是指向睡眠进程描述符的指针)。最后,内核开发者可以通过init_waitqueue_func_entry( )函数来自定义唤醒函数,该函数负责初始化等待队列的元素。
static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func)
{
q->flags = 0;
q->private = NULL;
q->func = func;
}
一旦定义了一个元素,必须把它插入等待队列。add_wait_queue( )函数把一个非互斥进程 插入等待队列链表的第一个位置。
add_wait_queue_exclusive( )函数把一个互斥进程 插入等待队列链表的最后一个位置(跟上个函数的区别仅仅是flags字段)。
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); /* wq points to the wait queue head */
schedule( );
remove_wait_queue(wq, &wait);
}
该函数把当前进程的状态设置为TASK_UNINTERRUPTIBLE,并把它插入到特定的等待队列wq中去。然后,它调用调度程序,而调度程序重新开始另一个程序的执行。当睡眠进程被唤醒时,调度程序重新开始执行sleep_on()函数,把该进程从等待队列中删除。
(2)interruptible_sleep_on():与sleep_on()函数是一样的,但稍有不同,前者把当前进程的状态设置为TASK_INTERRUPTIBLE而不是TASK_UNINTERRUPTIBLE,因此,接受一个信号就可以唤醒当前进程:
void fastcall __sched interruptible_sleep_on(wait_queue_head_t *q)
{
SLEEP_ON_VAR
SLEEP_ON_BKLCHECK
current->state = TASK_INTERRUPTIBLE;
SLEEP_ON_HEAD
schedule();
SLEEP_ON_TAIL
}
(3)sleep_on_timeout() 和 interruptible_sleep_on_timeout()与前面函数类似,但它们允许调用者定义一个时间间隔,过了这个间隔以后,进程将由内核唤醒。为了做到这点,它们调用schedule_timeout()而不是schedule()函数:
long fastcall __sched sleep_on_timeout(wait_queue_head_t *q, long timeout)
{
SLEEP_ON_VAR
SLEEP_ON_BKLCHECK
current->state = TASK_UNINTERRUPTIBLE;
SLEEP_ON_HEAD
timeout = schedule_timeout(timeout);
SLEEP_ON_TAIL
return timeout;
}
long fastcall __sched interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout)
{
SLEEP_ON_VAR
SLEEP_ON_BKLCHECK
current->state = TASK_INTERRUPTIBLE;
SLEEP_ON_HEAD
timeout = schedule_timeout(timeout);
SLEEP_ON_TAIL
return timeout;
}
在Linux 2.6中引入的prepare_to_wait( )、prepare_to_wait_exclusive( )和finish_wait( )函数提供了另外一种途径来使当前进程在一个等待队列中睡眠(再2.6.18以后已经不使用sleep_on了)。它们的典型应用如下:
DEFINE_WAIT(wait);
prepare_to_wait_exclusive(&wq, &wait, TASK_INTERRUPTIBLE);
/* wq is the head of the wait queue */
...
if (!condition)
schedule();
finish_wait(&wq, &wait);
函数prepare_to_wait( )和prepare_to_wait_exclusive( )用传递的第三个参数设置进程的状态,然后把等待队列元素的互斥标识flag分别设置为0(非互斥)或1(互斥),最后,把等待元素wait插入到以wq为头的等待队列的链表中。
void fastcall prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait);
if (is_sync_wait(wait))
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
void fastcall prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue_tail(q, wait);
if (is_sync_wait(wait))
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
进程一旦被唤醒就执行finish_wait()函数,它把进程的状态再次设置为TASK_RUNNING(仅发生在schedule()之前,唤醒条件变为真的情况下),并从等待队列中删除等待元素(除非这个工作已经由唤醒函数完成)。
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING);
if (!list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
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()函数(或者只是直接调用add_wait_queue_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”字符串的宏进行的,造成的结果是高优先级进程的执行稍有延迟。
wake_up_locked宏和wake_up宏相类似,仅有的不同是当wait_queue_head_t中的自旋锁已经被持有时要调用wake_up_locked。
侧如,wake_up宏等价于下列代码片段:
void wake_up(wait_queue_head_t *q)
{
struct list_head *tmp;
wait_queue_t *curr;
list_for_each(tmp, &q->task_list) {
curr = list_entry(tmp, wait_queue_t, task_list);
if (curr->func(curr, TASK_INTERRUPTIBLE|TASK_UNINTERRUPTIBLE,
0, NULL) && curr->flags)
break;
}
}
List_for_each宏扫描双向链表q->task_list中的所有项,即等待队列中的所有进程。对每一项,list_entry宏都计算wait_queue_t变量对应的地址。这个变量的func字段存放唤醒函数的地址,它试图唤醒由等待队列元素的task字段标识的进程。如果一个进程已经被有效地唤醒(函数返回1)并且进程是互斥的(curr->flags等于1),循环结束。因为所有的非互斥进程总是在双向链表的开始位置,而所有的互斥进程在双向链表的尾部,所以函数总是先唤醒非互斥进程然后再唤醒互斥进程,如果有进程存在的话。
每个进程都有一组相关的资源限制(resource limit),限制指定了进程能使用的系统资源数量。这些限制避免用户过分使用系统资源(CPU、磁盘空间等)。Linux承认以下表中的资源限制:
<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"Cambria Math"; panose-1:2 4 5 3 5 4 6 3 2 4; mso-font-charset:0; mso-generic-font-family:roman; mso-font-pitch:variable; mso-font-signature:-1610611985 1107304683 0 0 159 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-unhide:no; mso-style-qformat:yes; mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman","serif"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} tt {mso-style-unhide:no; mso-ansi-font-size:12.0pt; mso-bidi-font-size:12.0pt; font-family:宋体; mso-ascii-font-family:宋体; mso-fareast-font-family:宋体; mso-hansi-font-family:宋体; mso-bidi-font-family:宋体;} p.doctext, li.doctext, div.doctext {mso-style-name:doctext; mso-style-unhide:no; mso-margin-top-alt:auto; margin-right:0cm; mso-margin-bottom-alt:auto; margin-left:0cm; mso-pagination:widow-orphan; font-size:12.0pt; font-family:宋体; mso-bidi-font-family:宋体;} span.docemphbold {mso-style-name:docemphbold; mso-style-unhide:no;} .MsoChpDefault {mso-style-type:export-only; mso-default-props:yes; font-size:10.0pt; mso-ansi-font-size:10.0pt; mso-bidi-font-size:10.0pt; mso-ascii-font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-hansi-font-family:"Times New Roman"; mso-font-kerning:0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.Section1 {page:Section1;} -->
字段名 |
说明 |
RLIMIT_AS |
进程地址空间的最大数(以字节为单位)。当进程使用malloc() 或相关函数扩大它的地址空间时,内核检查这个值 |
RLIMIT_CORE |
内存信息转储文件的大小(以字节为单位)。当一个进程异常终止时,内核在进程的当前目录下创建内存信息转储文文件之前检查这个值。如果这个限制为0 ,那么,内核就不创建这个文件 |
RLIMIT_CPU |
进程使用CPU 的最长时间(以秒为单位)。如果进程超过了这个限制,内核就向它发一个SIGXCPU 信号,然后如果进程还不终止,再发一个SIGKILL 信号 |
RLIMIT_DATA |
堆大小的最大值(以字节为单位)。在扩充进程的堆之前,内核检查这个值 |
RLIMIT_FSIZE |
文件大小的最大值(以字节为单位)。如果进程试图把一个文件的大小扩充到大于这个值,内核就给这个进程发SIGXFSZ 信号 |
RLIMIT_LOCKS |
文件锁的最大值(目前是非强制的) |
RLIMIT_MEMLOCK |
非交换内存的最大值(以字节为单位)。当进程试图通交mlock() 或mlockall() 系统调用锁住一个页框时,内核检查这个值 |
RLIMIT_MSGQUEUE |
POSIX 消息队列中的最大字节数 |
RLIMIT_NOFILE |
打开文件描述符的最大数。当打开一个新文件或复制一个文件描述符时,内核检查这个值 |
RLIMIT_NPROC |
用户能拥有的进程最大数 |
RLIMIT_RSS |
进程所拥有的页框最大数(目前是非强制的) |
RLIMIT_SIGPENDING |
进程挂起信号的最大数 |
RLIMIT_STACK |
栈大小的最大值(以字节为单位)。内核在扩充进程的用户态堆栈之前检查这个值 |
对当前进程的资源限制存放在current->signal->rlim字段,即进程的信号描述符的一个字段。该字段是类型为rlimit结构的数组,每个资源限制对应一个元素:
struct rlimit {
unsigned long rlim_cur;
unsigned long rlim_max;
};
rlim_cur字段是资源的当前资源限制。例如:current->signal->rlim[RLIMIT_CPU].rlim_cur表示正运行进程所占用CPU时间的当前限制。
rlim_max字段是资源限制所允许的最大值。利用getrlimit()和setrlimit()系统调用,用户总能把一些资源的rlim_cur限制增加到rlim_max。然而,只有超级用户(或更确切地说,具有CAP_SYS_RESOURCE权能的用户)才能改变rlim_max字段,或把rlim_cur字段设置成大于相应rlim_max字段的一个值。
大多数资源限制包含值RLIMIT_INFINITY(Oxffffffff),它意味着没有对相应的资源施加用户限制(当然,由于内核设计上的限制,可用RAM、可用磁盘空间等,实际的限制还是存在的)。然而,系统管理员可以给一些资源选择施加更强的限制。只要用户注册进系统,内核就创建一个由超级用户拥有的进程,超级用户能调用setrlimit()以减少一个资源rlim_max和rlim_cur字段的值。随后,同一进程执行一个login shell,该进程就变为由用户拥有。由用户创建的每个新进程都继承其父进程rlim数组的内容,因此,用户不能忽略系统强加的限制。