网络协议栈实现分析6--Wait_queue等待队列

来源:互联网 发布:linux 对文件进行排序 编辑:程序博客网 时间:2024/04/29 07:12

                         等待队列 

   进程转入休眠状态等待某个特定事件,该事件发生时该进程就会被唤醒。实现这个技术的是把该事件和等待队列联系起来。需要将转入休眠状态的进程插入队列中。当事件发生后,内核遍历该队列,唤醒休眠任务让他投入运行状态,任务负责将自己从等待队列中删除。 等待队列在内核中广泛存在。

Wait _queue结构

Include/linux/wait.h

struct wait_queue {

struct task_struct * task;

struct wait_queue * next;

};

Task:task_struct结构的指针,它代表的是一个进程。

Next:它指向的是下一个wait_queue进程。

Sched.h

struct task_struct {

/* these are hardcoded - don't touch */

volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */

long counter;

long priority;

unsigned long signal;

unsigned long blocked; /* bitmap of masked signals */

unsigned long flags; /* per process flags, defined below */

int errno;

int debugreg[8];  /* Hardware debugging registers */

struct exec_domain *exec_domain;

/* various fields */

struct linux_binfmt *binfmt;

struct task_struct *next_task, *prev_task;

struct sigaction sigaction[32];

unsigned long saved_kernel_stack;

unsigned long kernel_stack_page;

int exit_code, exit_signal;

unsigned long personality;

int dumpable:1;

int did_exec:1;

int pid,pgrp,tty_old_pgrp,session,leader;

int groups[NGROUPS];

/* 

 * pointers to (original) parent process, youngest child, younger sibling,

 * older sibling, respectively.  (p->father can be replaced with 

 * p->p_pptr->pid)

 */

struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;

struct wait_queue *wait_chldexit; /* for wait4() */

unsigned short uid,euid,suid,fsuid;

unsigned short gid,egid,sgid,fsgid;

unsigned long timeout;

unsigned long it_real_value, it_prof_value, it_virt_value;

unsigned long it_real_incr, it_prof_incr, it_virt_incr;

long utime, stime, cutime, cstime, start_time;

struct rlimit rlim[RLIM_NLIMITS]; 

unsigned short used_math;

char comm[16];

/* file system info */

int link_count;

struct tty_struct *tty; /* NULL if no tty */

/* ipc stuff */

struct sem_undo *semundo;

struct sem_queue *semsleeping;

/* ldt for this task - used by Wine.  If NULL, default_ldt is used */

struct desc_struct *ldt;

/* tss for this task */

struct thread_struct tss;

/* filesystem information */

struct fs_struct fs[1];

/* open file information */

struct files_struct files[1];

/* memory management info */

struct mm_struct mm[1];

};

wait_event 

16840:通过使用这个宏,内核代码能够使当前执行的进程在等待队列 wq 中等待直至给定

condition(可能是任何的表达式)得到满足。 

16842:如果条件已经为真,当前进程显然也就无需等待了。 

16844:否则,进程必须等待给定条件转变为真。这可以通过调用__wait_event来实现(16824

行),我们将在下一节介绍它。由于  __wait_event 已经同 wait_event 分离,已知条

件为假的部分内核代码可以直接调用__wait_queue,而不用通过宏来进行冗余的(特

别是在这些情况下)测试,实际上也没有代码会真正这样处理。更为重要的是,如

果条件已经为真,wait_event 会跳过将进程插入等待队列的代码。 

  注意 wait_event 的主体是用一个比较特殊的结构封闭起来的: 

 do { 

    /* … */ 

  } while (0)        

  使我惊奇的是,这个小技巧并没有得到应有的重视。这里的主要思路是使被封闭的代码

能够像一个单句一样使用。考虑下面这个宏,该宏的目的是如果 是一个非空指针,则调用

free#define FREE1(p)  if  (p)  free (p) 

除非你在如下所述的情况下使用 FREE1,否则所有调用都是正确有效的: 

  if  (expression) 

  FREE1(p) 

 else 

  printf(“expression was false./n”) ;   

 

FREE1 经扩展以后,else就和错误的 if――FREE1的 if――联系在一起。 

  我曾经发现有些程序员通过如下途径解决这种问题: 

  #define FREE2(p)    if (p) { free(p); } 

  #define FREE3(p)    { if (p) { free(p); } } 

这两种方法都不尽人意,程序员在调用宏以后自然而然使用的分号会把扩展信息弄乱。以

FREE2 为例,在宏展开之后,为了使编译器能更准确的识别,我们还需要进行一定的缩进

调节,最终代码如下所示:

if (expression) 

    if (p) { free(p);} 

else 

    printf(“expression was false./n”); 

 

这样就会引起语法错误――else 和任何一个 if 都不匹配。FREE3 从本质上讲也存在同样的

问题。而且在研究问题产生原因的同时,你也能够明白为什么宏体里是否包含 if 是无关紧

要的。不管宏体内部内容如何,只要你使用一组括号来指定宏体,你就会碰到相同的问题。  

  这里是我们能够引入 do/while(0)技巧的地方。现在我们可以编写 FREE4,它能够克服

前面所出现的所有问题。 

 

#define FREE4(P)  / 

do {             / 

if (p)        / 

  free(p);    /  

while (0) 

  

将 FREE4 和其它宏一样插入相同代码之后,宏展开后其代码如下所示(为清晰起见,

我们再次调整了缩进格式): 

if (expression) 

  do { 

if (p)         

  free(p);    

    } while (0);      /* “;” following macro.*/ 

 

这段代码当然可以正确执行。编译器能够优化这个伪循环,舍弃循环控制,因此执行代

码并没有速度的损失,我们也从而得到了能够实现理想功能的宏。 

  虽然这是一个可以接受的解决方案,但是我们不能不提到的是编写函数要比编写宏好得

多。不过如果你不能提供函数调用所需的开销,那么就需要使用内联函数。这种情况虽然在

内核中经常出现,但是在其它地方就要少得多。(无可否认,当使用 C++gcc 或者任何实

现了将要出现的修正版 ISO 标准 的编译器时,这种方案只是一种选择,就是最后为 

加内联函数。) 

__wait_event 

16842__wait_event 使当前进程在等待队列 wq 中等待直至 condition为真。 

16829:通过调用 add_wait_queue16791 行),局部变量__wait 可以被链接到队列上。注

__wait 是在堆栈中而不是在内核堆中分配空间,这是内核中常用的一种技巧。在

宏运行结束之前,__wait 就已经被从等待队列中移走了,因此等待队列中指向它的

指针总是有效的。 

16830:重复分配 CPU给另一个进程直至条件满足,这一点将在下面几节中讨论。 

16831:进程被置为 TASK_UNINTERRUPTIBLE 状态(16190 行)。这意味着进程处于休

眠状态,不应被唤醒,即使是信号量也不能打断该进程的休眠。信号量在第 章中

介绍,而进程状态则在第 章中介绍。 

16832:如果条件已经满足,则可以退出循环

请注意如果在第一次循环时条件就已经满足,那么前面一行的赋值就浪费了(因为

在循环结束之后进程状态会立刻被再次赋值)。__wait_event假定宏开始执行时条件

还没有得到满足。而且,这种对进程状态变量 state 的延迟赋值也并没有什么害处。

在某些特殊情况下,这种方法还十分有益。例如当__wait_event 开始执行时条件为

假,但是在执行到 16832 行时就为真了。这种变化只有在为有关进程状态的代码计

算 condition变量值时才会出现问题。但是在代码中这种情况我一处也没有发现。 

:调用schedule26686 行,在第 章中讨论)将 CPU转移给另一个进程。直到进程

再次获得 CPU时,对 schedule 的调用才会返回。这种情况只有当等待队列中的进程

被唤醒时才会发生。 

:进程已经退出了,因此条件必定已经得到了满足。进程重置 TASK_RUNNING 的状

态(16188 行) ,使其适合CPU运行。 

:通过调用 remove_wait_queue16814 行)将进程从等待队列中移去。

wait_event_interruptible__wait_event_interruptible (分别参见16868行和16847

基本上与 wait_event __wait_event 相同,但不同的是它们允许休眠的进程可以被

信号量中断。如前所述,信号量将在第 章中介绍。 

请注意 wait_event 是被如下结构所包含的。 

 ({ 

   /* … */ 

 }) 

和 do/while(0)技巧一样,这样可以使被封闭起来的代码能够像一个单元一样运行。

这样的封闭代码就是一个独立的表达式,而不是一个独立的语句。也就是说,它可

以求值以供其它更复杂的表达式使用。发生这种情况的原因主要在于一些不可移植

的 gcc 特有代码的存在。通过使用这类技巧,一个程序块中的最后一个表达式的值

将定义为整个程序块的最终值。当在表达式中使用 wait_event_interruptible 时,执

wait_event_interruptible

行宏体后赋__ret的值为宏体的值(参看 16873行)。对于有Lisp 背景知识的程序员

来说,这是个很常见的概念。但是如果你仅仅了解一点 C和其它一些相关的过程性

程序设计语言,那么你可能就会觉得比较奇怪。 

_wake_up 

6829:该函数用来唤醒等待队列中正在休眠的进程。它由wake_upwake_up_interruptible

调用(请分别参看 16612 行和 16614 行)。这些宏提供 mode 参数,只有状态满足

mode 所包含的状态之一的进程才可能被唤醒。 

6833:正如将在第 10 章中详细讨论的那样,锁(lock)是用来限制对资源的访问,这在 SMP

逻辑单元中尤其重要,因为在这种情况下当一个 CPU在修改某数据结构时,另一个

CPU可能正在从该数据结构中读取数据,或者也有可能两个 CPU同时对同一个数据

结构进行修改,等等。在这种情况下,受保护的资源显然是等待队列。非常有趣的

是所有的等待队列都使用同一个锁来保护。虽然这种方法要比为每一个等待队列定

义一个新锁简单得多,但是这就意味着 SMP逻辑单元可能经常会发现自己正在等待

一个实际上并不必须的锁。 

6838:本段代码遍历非空队列,为队列中正确状态的每一个进程调用 wake_up_process

26356 行)。如前所述,进程(队列节点)在此可能并没有从队列中移走。这在很

大程度上是由于即使队列中的进程正在被唤醒,它仍然可能希望继续存在于等待队

列中,这一点正如我们在__wait_event中发现的问题一样。

参考linux 完全注释 

 

<基于www.hacktao.com上的作品创作,转载请注明!>

原创粉丝点击