1.定义“等待队列头”。
wait_queue_head_t my_queue;
2.初始化“等待队列头”。
init_waitqueue_head(&my_queue);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头的“快捷方式”。
DECLARE_WAIT_QUEUE_HEAD (name)
3.定义等待队列。
DECLARE_WAITQUEUE(name, tsk)
该宏用于定义并初始化一个名为name 的等待队列。
4.添加/移除等待队列。
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
add_wait_queue() 用于将等待队列wait 添加到等待队列头q 指向的等待队列链表中, 而
remove_wait_queue()用于将等待队列wait 从附属的等待队列头q 指向的等待队列链表中移除。
5.等待事件。
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
等待第一个参数queue 作为等待队列头的等待队列被唤醒,而且第二个参数condition 必须满足,否则阻塞。wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout后的宏意味着阻塞等待的超时时间,以jiffy 为单位,在第三个参数的timeout 到达时,不论condition 是否满足,均返回。wait()的定义如代码1.1所示,从其源代码可以看出,当condition 满足时,wait_event()会立即返回,否则,阻塞等待condition 满足。
代码清单1.1 wait_event()函数
#define wait_event(wq, condition) \
do { \
if (condition) /*条件满足立即返回*/ \
break; \
__wait_event(wq, condition);/*添加等待队列并阻塞*/
} while (0)
#define __wait_event(wq, condition) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
if (condition) \
break; \
schedule();/*放弃CPU*/ \
} \
finish_wait(&wq, &__wait); \
} while (0)
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 finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING);//恢复当前进程的状态为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);
}
}
DEFINE_WAIT(__wait); /* 创建等待队列 __wait*/
6.唤醒队列。
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
上述操作会唤醒以queue 作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。wake_up() 应与wait_event() 或wait_event_timeout() 成对使用, 而wake_up_interruptible() 则应与wait_event_interruptible() 或wait_event_interruptible_timeout() 成对使用。wake_up() 可唤醒处于TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE 的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE 的进程。
7.在等待队列上睡眠。
sleep_on(wait_queue_head_t *q );
interruptible_sleep_on(wait_queue_head_t *q );
sleep_on()函数的作用就是将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q 引导的等待队列被唤醒。interruptible_sleep_on() 与sleep_on() 函数类似, 其作用是将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q 引导的等待队列被唤醒或者进程收到信号。
sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与wake_up_interruptible()成对使用。
代码清单1.2和代码清单1.3分别列出了sleep_on()和interruptible_sleep_on()函数的源代码。
代码清单1.2 sleep_on()函数
void fastcall _ _sched interruptible_sleep_on(wait_queue_head_t *q)
{
SLEEP_ON_VAR
/* #define SLEEP_ON_VAR \
unsigned long flags; \
wait_queue_t wait; \
init_waitqueue_entry(&wait, current); */
current->state = TASK_UNINTERRUPTIBLE;//改变当前进程状态
SLEEP_ON_HEAD
/* #define SLEEP_ON_HEAD \
spin_lock_irqsave(&q->lock,flags); \
_ _add_wait_queue(q, &wait); \
spin_unlock(&q->lock);*/
schedule();//放弃CPU
SLEEP_ON_TAIL
/* #define SLEEP_ON_TAIL \
spin_lock_irq(&q->lock); \
_ _remove_wait_queue(q, &wait); \
spin_unlock_irqrestore(&q->lock, flags); */
}
代码清单1.3 interruptible_sleep_on()函数
void fastcall _ _sched interruptible_sleep_on(wait_queue_head_t *q)
{
SLEEP_ON_VAR
current->state = TASK_INTERRUPTIBLE;//改变当前进程状态
SLEEP_ON_HEAD
schedule();//放弃CPU
SLEEP_ON_TAIL
}
上面分别列出了sleep_on()和interruptible_sleep_on()函数的源代码。
从上面可以看出,不论是sleep_on()还是interruptible_sleep_on(),其流程都如下所示。
(1)定义并初始化一个等待队列,将进程状态改变为TASK_UNINTERRUPTIBLE(不能被信号打断)或TASK_INTERRUPTIBLE(可以被信号打断),并将等待队列添加到等待队列头。
(2)通过schedule()放弃CPU,调度其他进程执行。
(3)进程被其他地方唤醒,将等待队列移出等待队列头。
在内核中使用 set_current_state()函数或_ _add_wait_queue()函数来实现目前进程状态的改变,直接采用current->state = TASK_UNINTERRUPTIBLE 类似的赋值语句也是可行的。通常而言,set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于_ _add_ wait_queue()。因此,很多设备驱动中,并不调用sleep_on()或interruptible_sleep_on(),而是亲自进行进程的状态改变和切换,如代码清单1.4所示。
代码清单1.4 在驱动程序中改变进程状态并调用schedule()
static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos)
{
...
DECLARE_WAITQUEUE(wait, current); //定义等待队列
_ _add_wait_queue(&xxx_wait, &wait); //添加等待队列
ret = count;
/* 等待设备缓冲区可写 */
do
{
avail = device_writable(...);
if (avail < 0)
_ _set_current_state(TASK_INTERRUPTIBLE);//改变进程状态
if (avail < 0)
{
if (file->f_flags &O_NONBLOCK) //非阻塞
{
if (!ret)
ret = - EAGAIN;
goto out;
}
schedule(); //调度其他进程执行
if (signal_pending(current))//如果是因为信号唤醒
{
if (!ret)
ret = - ERESTARTSYS;
goto out;
}
}
}while (avail < 0);
/* 写设备缓冲区 */
device_write(...)
out:
remove_wait_queue(&xxx_wait, &wait);//将等待队列移出等待队列头
set_current_state(TASK_RUNNING);//设置进程状态为TASK_RUNNING
return ret;
}
8.支持阻塞操作的globalfifo设备驱动
现在我们给 globalfifo 增加这样的约束:把globalfifo中的全局内存变成一个FIFO,只有当FIFO中有数据的时候(即有进程把数据写到这个FIFO而且没有被读进程读空),读进程才能把数据读出,而且读取后的数据会从 globalfifo的全局内存中被拿掉;只有当FIFO非满时(即还有一些空间未被写,或写满后被读进程从这个FIFO 中读出了数据),写进程才能往这个FIFO中写入数据。现在,将globalfifo重命名为“globalfifo”,在globalfifo中,读FIFO将唤醒写FIFO,而写FIFO也将唤醒读FIFO。首先,需要修改设备结构体,在其中增加两个等待队列头,分别对应于读和写,如代码清单1.5所示。
代码清单1.5 globalfifo设备结构体
struct globalfifo_dev
{
struct cdev cdev; /*cdev 结构体*/
unsigned int current_len; /*fifo 有效数据长度*/
unsigned char mem[GLOBALFIFO_SIZE]; /*全局内存*/
struct semaphore sem; /*并发控制用的信号量*/
wait_queue_head_t r_wait; /*阻塞读用的等待队列头*/
wait_queue_head_t w_wait; /*阻塞写用的等待队列头*/
};
与globalfifo设备结构体的另一个不同是增加了current_len成员用于表征目前FIFO中有效数据的长度。这个等待队列需在设备驱动模块加载函数中调用init_waitqueue_head()被初始化,新的设备驱动模块加载函数如代码清单1.6所示。
代码清单1.6 globalfifo 设备驱动模块加载函数
int globalfifo_init(void)
{
int ret;
dev_t devno = MKDEV(globalfifo_major, 0);
/* 申请设备号*/
if (globalfifo_major)
ret = register_chrdev_region(devno, 1, "globalfifo");
else /* 动态申请设备号 */
{
ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo");
globalfifo_major = MAJOR(devno);
}
if (ret < 0)
return ret;
/* 动态申请设备结构体的内存*/
globalfifo_devp = kmalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
if (!globalfifo_devp) /*申请失败*/
{
ret = - ENOMEM;
goto fail_malloc;
}
memset(globalfifo_devp, 0, sizeof(struct globalfifo_dev));
globalfifo_setup_cdev(globalfifo_devp, 0);
init_MUTEX(&globalfifo_devp->sem); /*初始化信号量*/
init_waitqueue_head(&globalfifo_devp->r_wait); /*初始化读等待队列头*/
init_waitqueue_head(&globalfifo_devp->w_wait); /*初始化写等待队列头*/
return 0;
fail_malloc: unregister_chrdev_region(devno, 1);
return ret;
}
设备驱动读写操作需要被修改,在读函数中需增加等待globalfifo_devp->w_wait 被唤醒的语句,而在写操作中唤醒globalfifo_devp->r_wait,如代码清单1.7所示。
代码清单1.7增加等待队列后的globalfifo 读写函数
/*globalfifo 读函数*/
static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count, 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 非空 */
if (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;
}
/*globalfifo 写操作*/
static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
struct globalfifo_dev *dev = filp->private_data; //获得设备结构体指针
int ret;
DECLARE_WAITQUEUE(wait, current); //定义等待队列
down(&dev->sem); //获取信号量
add_wait_queue(&dev->w_wait, &wait); //进入写等待队列头
/* 等待FIFO 非满 */
if (dev->current_len == GLOBALFIFO_SIZE)
{
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 > GLOBALFIFO_SIZE - dev->current_len)
count = GLOBALFIFO_SIZE - dev->current_len;
if (copy_from_user(dev->mem + dev->current_len, buf, count))
{
ret = - EFAULT;
goto out;
}
else
{
dev->current_len += count;
printk(KERN_INFO "written %d bytes(s),current_len:%d\n", count, dev
->current_len);
wake_up_interruptible(&dev->r_wait); //唤醒读等待队列
ret = count;
}
out:
up(&dev->sem); //释放信号量
out2:
remove_wait_queue(&dev->w_wait, &wait); //从附属的等待队列头移除
set_current_state(TASK_RUNNING);
return ret;
}
代码清单1.7处理了等待队列进出和进程切换的过程。是否可以把读函数中一大段用于等待dev->current_len != 0 的内容直接用wait_event_interruptible(dev->r_wait,dev->current_len != 0)替换,把写函数中一大段用于等待dev->current_len != GLOBALFIFO_SIZE 的代码用wait_event_ interruptible(dev->w_wait, dev->current_len != 0)替换呢?
实际上,就控制等待队列非空和非满的角度而言,wait_event_interruptible ( dev->r_wait, dev->current_len != 0)和第13~32 行代码的功能完全一样,wait_event_interruptible(dev->w_wait,dev->current_len != 0)和第73~93 行代码的功能完全一样。细微的区别体现在第13~32 行代码和第73~93 行代码在进行schedule()即切换进程前,通过up(&dev->sem)释放了信号量。这一细微的动作意义重大,非如此,则死锁将不可避免。
如图 1.1(a)所示,假设目前的FIFO 为空,即dev->current_len 为0,此时如果有一个读进程,它会先获得信号量,因为条件不满足,它将因为wait_event_interruptible(dev->r_wait, dev-> current_len != 0)而阻塞,而释放dev->r_wait 等待队列及让dev->current_len != 0 的操作又需要在写进程中进行,写进程在执行写操作前又必须等待读进程释放信号量,造成互相等待对方资源的矛盾局面,从而死锁。
如图 1.1(b)所示,假设目前的FIFO 为满即dev->current_len 为GLOBALFIFO_SIZE,此时如果有一个写进程, 它先获得信号量, 因为条件不满足, 它将因为wait_event_interruptible ( dev->w_wait,dev->current_len != GLOBALFIFO_SIZE)而阻塞,而释放dev->w_wait 等待队列及让dev->current_ len !=GLOBALFIFO_SIZE 的操作又需要在读进程中进行,读进程在执行读操作前又必须等待写进程释放信号量,造成互相等待对方资源的矛盾局面,从而死锁。
所谓死锁,就是多个进程循环等待它方占有的资源而无限期地僵持下去的局面。如果没有外力的作用,那么死锁涉及的各个进程都将永远处于封锁状态。因此,驱动工程师一定要注意:当多个等待队列、信号量等机制同时出现时,谨防死锁。
现在回过来看一下代码清单1.7的第15行和第75行,发现在设备驱动的read()、write()等功能函数中,可以通过filp->f_flags 标志获得用户空间是否要求非阻塞访问。驱动中可以依据此标志判断用户究竟要求阻塞还是非阻塞访问,从而进行不同的处理。
图1.1 等待队列、信号量等引起的死锁