这个函数是将进程/线程加入队列
它的解释是:通过DECLARE_EAITQUEUE宏将等待队列项初始化成对应的任务结构,并且用于连接的相关指针设置为空,其中加入了调试相关代码
进程通过执行下面步骤将自己加入到一个等待队列中:
1.调用DECLARE_WAITQUEUE()红创建一个等待队列的项
2.调用add_wait_queue()把自己加入到等待队列中。该队列会在进程等待的条件满足时唤醒 它,在其他地方写相关代码,在事件发生时,对等待队列执行wake_up操作
3.将进程状态变更为:TASK_INTERRUPTIBLR or TASK_UNINTERRUPTIBLE.
4.如果状态被设置为TASK_INTERRUPTIBLE,则信号唤醒进程,即为伪唤醒(唤醒不是因 为事件的发生),因此检查并处理信号
5.检查comdition是否为真,为真则没必要休眠,如果不为真,则调用scheduled().
6.当进程被唤醒的时候,它会再次检查田间是否为真,真就退出循环,否则再次调用 schedules()并一直重复这部操作
7.condition满足后,进程将自己设置为TASK_RUNNING 并通过remove_wait_queue()退出
add_wait_queue() | add_wait_queue_exclusive()
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 标志
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()函数,只是第二和参数和第三个参数与原来的增加到队列头的参数调换的一下顺序
wait_event_interruptible()。该函数修改task的状态为TASK_INTERRUPTIBLE,意味着改进程将不会继续运行直到被唤醒,然后被添加到等待队列wq中。
在wait_event_interruptible()中首先判断condition是不是已经满足,如果是则直接返回0,否则调用__wait_event_interruptible(),并用__ret来存放返回值
以下部分是自己学习过程中遇到的困惑
这里借用一下别人FIFO驱动例程
/*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()相关函数,这里的睡眠,等待队列,信号量有什么关系
首先应从总体上把握程序的思想,信号量在这儿涉及到两个函数:此处你给出的读函数,还有一个你没有给出的写函数(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);
}
原来这里有两种睡眠:一种是产生竞态时要休眠;另一种是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;
}