DECLARE_WAITQUEUE 将进程加入等待队列

来源:互联网 发布:4g网络优化工程师招聘 编辑:程序博客网 时间:2024/05/17 01:12
#define   DECLARE_WAITQUEUE(name,   tsk)       / wait_queue_t   name     =__WAITQUEUE_INITIALIZER(name,   tsk) #define   __WAITQUEUE_INITIALIZER(name,tsk)   {task:  tsk,  task_list:  {  NULL,NULL},  __WAITQUEUE_DEBUG_INI(name)} 


参考资料:http://blog.csdn.net/hzn407487204/article/details/5489507

                    http://www.360doc.com/content/10/1009/17/1317564_59632874.shtml
它的解释是:
通过DECLARE_WAITQUEUE宏将等待队列项初始化成对应的任务结构,并且用于连接的相关指针均设置为空。其中加入了调试相关代码。


进程通过执行下面步骤将自己加入到一个等待队列中:
1) 调用DECLARE_WAITQUEUE()创建一个等待队列的项;
2) 调用add_wait_queue()把自己加入到等待队列中。该队列会在进程等待的条件满足时唤醒它。在其他地方写相关代码,在事件发生时,对等的队列执行wake_up()操作。
3) 将进程状态变更为: TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE。
4) 如果状态被置为TASK_INTERRUPTIBLE ,则信号唤醒进程。即为伪唤醒(唤醒不是因为事件的发生),因此检查并处理信号。
5) 检查condition是否为真,为真则没必要休眠,如果不为真,则调用scheduled()。
6) 当进程被唤醒的时候,它会再次检查条件是否为真。真就退出循环,否则再次调用scheduled()并一直重复这步操作。
7) condition满足后,进程将自己设置为TASK_RUNNING 并通过remove_wait_queue()退出。

 

 

加入等待队列 | add_wait_queue() | add_wait_queue_exclusive()

内核:2.6.24
add_wait_queue() 用来将一个进程添加到等待队列,该函数在获得必要的自旋锁后,使用 __add_wait_queue() 函数来完成队列添加工作。

__add_wait_queue() 定义为:

static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new){    list_add(&new->task_list, &head->task_list);//list_add() 是标准的建立队列双向链表函数。}

另外还有一个  add_wait_queue_exclusive() 函数,它的工作方式和 add_wait_queue() 一样,但是将进程插入到队列尾部,同时还设置了 WQ_EXCLUSIVE 标志。
add_wait_queue_exclusive() 定义为:

void fastcall add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait){    unsigned long flags;    wait->flags |= WQ_FLAG_EXCLUSIVE;    spin_lock_irqsave(&q->lock, flags);    __add_wait_queue_tail(q, wait);    spin_unlock_irqrestore(&q->lock, flags);}

__add_wait_queue_tail() 函数定义为:

static inline void __add_wait_queue_tail(wait_queue_head_t *head,                        wait_queue_t *new){    list_add_tail(&new->task_list, &head->task_list);}


 

list_add_tail() 定义为:/** * list_add_tail - add a new entry * @new: new entry to be added * @head: list head to add it before * * Insert a new entry before the specified head. * This is useful for implementing queues. */static inline void list_add_tail(struct list_head *new, struct list_head *head){    __list_add(new, head->prev, head);}


由此可见,添加到队列尾也是调用 __list_add() 函数,只是第 2 个参数和第 3 个参数与原来的增加到队列头的参数调换了一下顺序。

 

wait_event_interruptible()。该函数修改task的状态为TASK_INTERRUPTIBLE,意味着改进程将不会继续运行直到被唤醒,然后被添加到等待队列wq中。

在wait_event_interruptible()中首先判断condition是不是已经满足,如果是则直接返回0,否则调用__wait_event_interruptible(),并用__ret来存放返回值

---------------------------------------------------------------

#define wait_event_interruptible(wq, condition)          /

({                                                       /

    int __ret = 0;                                       /

    if (!(condition))                                    /

       
__wait_event_interruptible(wq, condition, __ret);/

    __ret;                                               /

})

wait_event_interruptible()--> __wait_event_interruptible()

__wait_event_interruptible()首先定义并初始化一个wait_queue_t变量__wait,其中数据为当前进程current,并把__wait入队。

   
在无限循环中,__wait_event_interruptible()将本进程置为可中断的挂起状态,反复检查condition是否成立,如果成立
则退出,如果不成立则继续休眠;条件满足后,即把本进程运行状态置为运行态,并将__wait从等待队列中清除掉,从而进程能够调度运行。如果进程当前有
异步信号(POSIX的),则返回-ERESTARTSYS。



接下来是一个案例:
 

自己在看globalfifo驱动的时候看得不是很明白,有些地方没搞懂,今天有幸看到论坛上一个帖子,把我没搞懂的都搞懂了,摘录下来供参考!

=============================我是分割线==============================

LZ:

/*globalfifo读函数*/
static ssize_t globalfifo_read(structfile*filp,char __user *buf,size_tcount,
  loff_t *ppos)
{
  int ret;
  struct globalfifo_dev*dev= filp->private_data;//获得设备结构体指针

  DECLARE_WAITQUEUE(wait, current);//定义等待队列


  down(&dev->sem);//此函数包含了进入睡眠的动作吗?

  add_wait_queue(&dev->r_wait,&wait);//为何要在获得信号量后进入读等待队列头?


  /* 等待FIFO非空 */
  while(dev->current_len== 0)
  {
    if(filp->f_flags&O_NONBLOCK)//?

    {
      ret =- EAGAIN;//?

      goto out;
    }
    __set_current_state(TASK_INTERRUPTIBLE);//改变进程状态为睡眠

    up(&dev->sem);

    schedule();//调度其他进程执行

    if(signal_pending(current))//?

    //如果是因为信号唤醒

    {
      ret =- ERESTARTSYS;
      goto out2;
    }

    down(&dev->sem);
  }

  /* 拷贝到用户空间 */
  if(count> dev->current_len)
    count= dev->current_len;

  if(copy_to_user(buf, dev->mem,count))
  {
    ret =- EFAULT;
    goto out;
  }
  else
  {
    memcpy(dev->mem, dev->mem +count, dev->current_len-count);//fifo数据前移

    dev->current_len-=count;//有效数据长度减少

    printk(KERN_INFO"read %d bytes(s),current_len:%d\n",count, dev->current_len);
     
    wake_up_interruptible(&dev->w_wait);//唤醒写等待队列

    
    ret =count;
  }
  out: up(&dev->sem);//释放信号量

  out2:remove_wait_queue(&dev->w_wait,&wait);//从附属的等待队列头移除

  set_current_state(TASK_RUNNING);
  return ret;
}

本人对阻塞操作还是不能结合这个实际例子加以理解。
我只能理解:从进入睡眠wait_event()到睡眠唤醒wake_up()
可这里没有wait_event()相关函数,为什么?
另外,这里的睡眠、等待队列,和信号量又有什么关系??看书都看晕了!!

===================================我是分割线====================================

回复1:

首先应从总体上把握程序的思想,信号量在这儿涉及到两个函数:此处你给出的读函数,还有一个你没有给出的写函数(globalfifo_write)。globalfifo_dev 结构表示底层被驱动的硬件(此处可能只是表示软件模拟的一个环形FIFO),这个结构显然定义了两个进程睡眠队列:r_wait,
w_wait,即一个读数据的进程睡眠队列和一个写数据进程的睡眠队列。由于读写进程操作的是同一块区域,所以这块区域就成为我们通常所说的“临界区”,读写进程不可同时访问这块区域,否则很有可能会造成系统状态的不一致,这一点我想应该容易理解,那么dev->sem信号量的目的就是创建这个“临界区”,无论读写进程在进入到共享区域进行操作时(读或写),必须获取独享访问的权限,这就是如下语句的目的:
down(&dev->sem);  //此函数包含了进入睡眠的动作吗?
正如你所问的,这个down函数可以将当前进程置于睡眠状态,当有一个写进程正在操作这块共享区域时,此时读进程(在down调用下)会被挂起(即进入睡眠)。

该进程的唤醒由写进程负责,正如你给出的globalfifo_read函数后的如下语句:
wake_up_interruptible(&dev->w_wait);
如果你查看对应的globalfifo_write函数,那么在globalfifo_write函数中将会存在如下的语句:
wake_up_interruptible(&dev->r_wait); (个人理解这里应该是由up(&dev->sem)唤醒进程,wake_up应该是唤醒sleep_on导致的进程睡眠,欢迎拍砖)
即当一个写进程完成数据的写入后,其唤醒可能等待读取的进程,此时globalfifo_read函数
将继续从如下语句执行。
add_wait_queue(&dev->r_wait, &wait); //为何要在获得信号量后进入读等待队列头?
正如你所问的,为何此时要将当前进程提前挂入到r_wait队列,因为后面我们需要明确编码
将进程置于睡眠(当无数据可读取时),这与调用down函数时由内核将当前进程置于睡眠状态基本是一致的,不同的是,调用down时是由内核在资源不可用时将进程置于睡眠,而此处我们
根据有无数据主动编码将进程置于睡眠状态,因为当无数据读取时,而用户又没有设置NON_BLOCK标志位,我们不能继续占用CPU,需要让出CPU,从而让写进程有可能向共享区域中写数据,所以一方面你会看出在主动让进程置于睡眠时,还必须要调用up函数释放信号量,从而使得写进程可以进入到“临界区”写数据;另一方面执行如下代码将进程明确置于睡眠:
    __set_current_state(TASK_INTERRUPTIBLE); //改变进程状态为可中断睡眠
    up(&dev->sem);

      schedule(); //调度其他进程执行

schedule函数调度其他进程执行,注意当前进程被设置为TASK_INTERRUPTIBLE状态,即可中断睡眠,这个状态的进程被排除在进程调度资格之外,直到被唤醒(即进入TASK_RUNNING状态),前面已提出,这个唤醒将执行由globalfifo_write函数(或者某个中断函数)的进程唤醒。
除了被globalfifo_write函数唤醒外,还有另外一种被唤醒的可能,即进程接收到一个外部中断,
如下代码即检查这种情况,如用户等不及了,使用Ctrl+C进行中断读进程操作,那么对Ctrl+C的捕捉就在此处完成:

if (signal_pending(current))               //被其他中断唤醒,如Ctrl+C,则直接跳出
    {
      ret =  - ERESTARTSYS;
      goto out2;
    }
========上面这几句之前一直没弄明白,一直站在应用层的角度去思考============

博主补充资料:

signal_pending( current )―――》检查当前进程是否有信号处理,返回不为0表示有信号需要处理。
-ERESTARTSYS表示信号函数处理完毕后重新执行信号函数前的某个系统调用。
也就是说,如果信号函数前有发生系统调用,在调度用户信号函数之前,内核会检查系统调用的返回值,看看是不是因为这个信号而中断了系统调用.如果返回值-ERESTARTSYS,并且当前调度的信号具备-ERESTARTSYS属性,系统就会在用户信号函数返回之后再执行该系统调用。

=========================================================================

注意函数最后如下语句:
remove_wait_queue(&dev->w_wait, &wait); //原文有误
remove_wait_queue(&dev->r_wait, &wait);  //这才是正确的写法

在函数退出之前,将当前进程从r_wait队列中删除,这正如前面的如下语句形成对称:
add_wait_queue(&dev->r_wait, &wait);

其他应无难理解之处,以上代码有两处进程睡眠之处:
1>为进入“临界区”获取信号量时可能进入睡眠(即调用down函数时),此处的睡眠动作由内核负责(即down函数的底层实现)。
2>在无可读数据时,自己编码实现读进程睡眠。如下的这段代码在很多驱动程序中都是比较典型的:

DECLARE_WAITQUEUE(wait, current);
  down(&dev->sem);
  add_wait_queue(&dev->r_wait,&wait);

  while(conditionnot meet)
  {
   
      __set_current_state(TASK_INTERRUPTIBLE);//改变进程状态为睡眠

    up(&dev->sem);

      schedule();//调度其他进程执行

    if(signal_pending(current)){
      ret =- ERESTARTSYS;
      goto out2;
     }

     down(&dev->sem);
  }

============================我是分割线==============================

LZ再问:

原来这里有两种睡眠:一种是产生竞态时要休眠;另一种是FIFO为空时要休眠。

不好意思,最后再问几个问题:
1.从哪处醒来?
down函数中的睡眠,在醒来后,进程是否还是从down函数开始继续往下?
  __set_current_state(TASK_INTERRUPTIBLE)
睡眠在醒来后,进程从哪里开始?

2.while (dev->current_len == 0)
  {
    if (filp->f_flags &O_NONBLOCK)     
    {
      ret =  - EAGAIN;                     
      goto out;
    }
    __set_current_state(TASK_INTERRUPTIBLE);
    up(&dev->sem);

    schedule();
    if (signal_pending(current))              
    {
      ret =  - ERESTARTSYS;
      goto out2;
    }

    down(&dev->sem);    为何这里会有down函数?它的作用。。。。
  }

3.else
  {
    memcpy(dev->mem, dev->mem + count, dev->current_len - count); //fifo数据前移
    dev->current_len -= count; //有效数据长度减少
    printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev->current_len);
     
    wake_up_interruptible(&dev->w_wait); //唤醒写等待队列
   
    ret = count;
  }
这里为何没有up函数?前面有down函数的啊。

=========================我是分割线================================

回复:

1.进程被唤醒而调度进入运行时,从down语句的下一条语句执行:即代码中如下语句开始执行:
add_wait_queue(&dev->r_wait, &wait);
另外注意你对如下代码理解有误:
    __set_current_state(TASK_INTERRUPTIBLE); //改变进程状态为睡眠

这条语句只是改变了进程的状态,并没有将进程置于睡眠,此时进程仍然占用CPU运行,直到如下调用shedule函数,进程才被明确置入睡眠等待状态,因为睡眠的目的在于等待写进程写入数据,所以在调度schedule函数之前需要释放信号量,放弃对“临界区”的“霸占”,这就是up函数的作用。
    up(&dev->sem);
      schedule(); //调度其他进程执行

在调用schedule函数退出CPU后,下次唤醒后进入运行时将从schedule语句的下一条语句开始,即if (signal_pending(current)) 语句。

2.注意到dev->current_len 也是一个共享变量,读写进程可能会同时访问该变量,所以在检查该变量值之前也要进入“临界区”。很多代码中将down语句放在此处while循环的外面,在
99%的时候程序都不会出现异常,但是却是不规则不正确的编码方式。
前一个帖子中,我说到如下代码的用途,如果如下代码不成立,那么这个读进程的唤醒应是
对应写进程的功劳,此时表示极有可能又有可读数据了,那么为了读取数据,必须进入“临界区”,程序为了保险起见,再次对dev->current_len进行检查,以避免多个读进程同时等待,而同时被唤醒的极端情况。dev->current_len又是一个共享变量,所以才有此处down语句调用,可以说,此处的调用是非常关键的,也是极其正确的编码方式。
if (signal_pending(current))              
    {
      ret =  - ERESTARTSYS;
      goto out2;
    }

原创粉丝点击