前言
阻塞和非阻塞是设备访问的两种基本方式。使用这两种方式,驱动程序可以灵活地支持阻塞与非阻塞访问。在写阻塞与非阻塞的驱动程序时,经常用到等待队列,所有本章将对等待队列进行简要介绍。
阻塞与非阻塞
阻塞调用
是指调用结果返回之前,当前线程会被挂起。函数只有得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上它们是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select()
就是这样的一个例子。下面是调用select()
函数进入阻塞的一个例子。
void main()
{
FILE *fp;
struct fd_set fds;
struct timeval timeout={4, 0}; //select()函数等待4s,4s后轮询
char buffer[256]={0}; //256字节的缓冲区
fp = fopen(....); //打开文件
while(1)
{
FD_ZERO(&fds); //清空集合
FD_SET(fp, &fds); //同上
maxfdp=fp+1; //描述符最大值加1
switch(select(maxfdp, &fds, &fds, NULL, &timeout)) //select函数使用
{
case -1:
exit(-1);
break; //select()函数错误,退出程序
case 0:
break; //再次轮询
default:
if(FD_ISSET(fp, &fds)) //判断是否文件中有数据
{
read(fds, buffer, 256, ...); //接受文件数据
if(FD_ISSET(fp, &fds)) //测试文件是否可写
fwrite(fp, buffer...); //写入文件buffer清空
}
}
}
}
等待队列
本节将介绍驱动程序编程中常用的等待队列机制。这种机制使等待的进程暂时睡眠,当等待的信号到来时,便唤醒等待队列中进程继续执行。本节将详细介绍等待队列的内容。
等待队列概述
在Linux驱动程序中,阻塞进程可以使用等待队列(Wait Queue)
来实现。由于等待队列很有用,在Linux2.0的时代,就已经引入了等待队列机制。等待队列的基本数据结构是一个双向链表
,这个链表存储睡眠的进程。等待队列也与进程调度机制紧密结合,能够用于实现内核中异步事件通知机制。等待队列可以用来同步对系统资源的访问。例如,当完成一项工作之后,才允许完成另一项工作。
在内核中,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合。可以使用等待队列实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问等。
等待队列的实现
根据不同的平台,其提供的指令代码有所不同,所以等待队列的实现也有所不同。在Linux中,等待队列的定义如下代码所示。
struct __wait_queue_head{
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
下面详细介绍该结构体中的各个成员变量。
1.lock自旋锁
lock自旋锁的功能很简答,用来对task_list
链表起保护作用。当要向task_lsit
链表中加入或者删除元素时,内核内部就会锁定lock锁,当修改完成后,会释放lock锁。也就是说,lock自旋锁在对task_lsit
与操作的过程中,实现了对等待队列的互斥访问。
2.task_list变量
task_list
是一个双向循环链表,用来存放等待的进程。
等待队列的使用
在Linux中,等待队列的类型为struct wait_queue_head_t
。内核提供了一系列的函数对struct wait_queue_head_t
进行操作。下面将对等待队列的操作方法进行简要的介绍。
1.定义和初始化等待队列头
在Linux中,定义等待队列的方法和定义普通结构体的方法相同,定义方法如下:
struct wait_queue_head_t wait;
一个等待队列必须初始化才能被使用,init_waitqueue_head()
函数用来初始化一个等待队列,其代码形式如下:
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITALIZER(name)
2.定义等待队列
Linux内核中提到了一个宏用来定义等待队列,该宏的代码如下:
#define DECLARE_WAITQUEUE(name, tsk) \wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
该宏用来定义并且初始化一个名为name
的等待队列。
3.添加和移除等待队列
Linux内核中提供了两个函数用来添加和移除队列,这两个函数的定义如下:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
add_wait_queue()
函数用来将等待队列元素wait添加到等待队列头q所指向的等待队列链表中。与其相反的函数是remove_wait_queue()
,该函数用来将队列元素wait从等待队列q所指向的等待队列中删除。
4.等待事件
Linux内核中提供一些宏来等待相应的事件,这些宏的定义如下:
#define wait_event(wq, condition)
#define wait_event_timeout(wq, condition, ret)
#define wait_event_interruptible(wq, condition, ret)
#define wait_event_interruptible_timeout(wq, condition, ret)
wait_event
宏的功能是,在等待队列中睡眠直到condition
为真。在等待的期间进程会被置为TASK_UNINTERRUPTIBLE
进入睡眠,直到condition
变量为真。每次进程被唤醒的时候都会检查condition
的值。wait_event_timeout
宏与wait_event
类似,不过如果所给的睡眠时间为负数则立即返回。如果在睡眠期间被唤醒,且condition
为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回0。wait_event_interruptible
宏与wait_event
的区别是,调用该宏在等待的过程中当前进程会被设置为TASK_INTERRUPTIBLE
状态。在每次被唤醒的时候,首先检查condition
是否为真,如果为真则返回;否则检查如果进程是被信号唤醒,会返回-ERESTARTSYS
错误码。如果condition
为真,则返回0。wait_event_interruptible_timeout
宏与wait_event_timeout
宏类似,不过如果睡眠期间被信号打断则返回ERESTARTSYS
错误码。
5.唤醒等待队列
Linux内核中提供一些宏用来唤醒相应的队列中的进程,这些宏的定义如下:
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
wake_up
宏唤醒等待队列,可唤醒处于TASK_INTERRUPTIBLE
和TASK_UNINTERRUPTIBLE
状态的进程,这个宏和wait_event/wait_event_timeout
成对使用。wake_up_interruptible
宏和wake_up()
唯一的区别是,它只能唤醒TASK_INTERRUPTIBLE
状态的进程。这个宏可以唤醒使用wait_event_interruptible、wait_event_interruptible_timeout
宏睡眠的进程。
同步机制实验
本节将讲解一个使用等待队列实现的同步机制的实验,通过本节的实验,读者可以对Linux中的同步机制有一个较深的了解。
同步机制设计
进程同步机制的设计首先需要一个等待队列,所有等待一个事件完成的进程都挂接在这个等待队列中,一个包含队列的数据结构可以实现这种意图。这个数据结构的定义代码如下:
struct CustomEvent{
int eventNum; //事件号
wait_queue_head_t *p; //系统等待队列首指针
struct CustomEvent *next; //队列链指针
};
下面对这个结构体进行简要的解释:
- 2行的
eventNum
表示进程等待的事件号 - 3行,是一个等待队列,进程在这个等待队列中等待。
- 4行,是连接这个结构体的指针。
为了实现实验的意图,设计了两个指针分别表示事件链表的头部和尾部,这两个结构的定义如下代码所示。
CustomEvent *lpevent_head = NULL; //链头指针
CustomEvent *lpevent_end = NULL; //链尾指针
每个事件由一个链表组成,每个链表中包含了等待这个事件的等待队列。这个结构下图所示。
为了实现实验的设计,定义了一个函数FindEventNum()
从一个事件链表中找到某个事件对应的等待链表,这个函数的代码如下:
CustomEvent *FindEventNum(int eventNum, CustomEvent **prev)
{
CustomEvent *tmp = lpevent_head;
*prev = NULL;
while(tmp)
{
if(tmp->eventNum == eventNum)
return tmp;
*prev = tmp;
tmp = tmp->next;
}
return NULL;
}
下面对这个函数进行简要的介绍:
- 1行,函数接收两个参数,第1个参数
eventNum
是事件的序号,第2个参数是返回事件的前一个事件。该函数找到所要的事件则返回,否则返回NULL。 - 3行,将
tmp
赋值为事件链表的头部。 - 4行,将
prev
指向NULL。 - 5~11行,是一个
while()
循环,找到所要事件的结构体指针。 - 7行,判断
tmp
所指向的事件号是否与eventNum
相同,如果相同则返回,表示找到,否则继续沿着链表查找。 - 10行,将
tmp
向后移动。 - 12行,如果没有找到,则返回NULL值。
为了实现实验的设计,定义了一个系统调用函数sys_CustomEvent_open()
,该函数新分配了一个事件,并返回新分配事件的事件号,其函数的定义如下:
asmlinkage int sys_CustomEvent_open(int eventNum)
{
CustomEvent *new;
CustomEvent *prev;
if(eventNum)
if(!FindEentNum(eventNum, &prev))
return -1;
else
return eventNum;
else
{
new = (CustomEvent *)kmalloc(sizeof(CustomEvent), GFP_KERNEL);
new->p = (wait_queue_head_t *)kmalloc(sizeof(wait_queue_head_t), GFP_KERNEL);
new->next = NULL;
new->p->task_list.next = &new->p->task_list;
new->p->task_list.prev = &new->p->task_list;
if(!lpevent_head)
{
new->eventNum = 2; //从2开始按偶数递增事件号
lpevent_end->next = lpevent_end = new;
return new->eventNum;
}
else
{
//事件队列不为空,按偶数递增一个事件号
new->eventNum = lpevent_end->eventNum + 2;
lpevent_end->next = new;
lpevent_end = new;
}
return new->eventNum;
}
return 0;
}
下面对该函数进行简要的介绍:
- 1行,该函数用来建立一个新的事件,参数为新建立的事件号。
- 3、4行,定义了两个事件的指针。
- 5行,判断事件是否为0,如果为0,则重新创建一个事件。
- 6~9行,根据事件号查找事件,如果找到返回事件号,如果没有找到返回-1。
FindEventNum()
函数根据事件号查找相应的事件。 - 12~31行,用来重新分配一个事件。
- 12行,调用
kmalloc()
函数新分配一个事件。 - 13行,分配该事件对应的等待队列,将等待队列的任务结构体链接指向自己。
- 17~22行,如果没有事件链表头,则将新分配的事件赋给事件链表头,并返回新分配的事件号。
- 25~28行,如果已经没有事件链表头,则将新分配的事件连接到链表中。
- 30行,返回新分配的事件号。
下面定义了一个将进程阻塞到一个事件的系统调用函数,直到等待的事件被唤醒时,事件才退出。该函数的代码如下:
asmlinkage int sys_CustomEvent_wait(int eventNum)
{
CustomEvent *tmp;
CustomEvent *prev = NULL;
if((tmp = FindEventNum(eventNum, &prev)) != NULL)
{
DEFINE_WAIT(wait); //初始化一个wait_queue_head
//当前进程进入阻塞队列
prepare_to_wait(tmp->p, &wait, TASK_INTERRUPTIBLE);
schedule(); //重新调度
finish_wait(tmp->p, &wait); //进程被唤醒从阻塞队列退出
return eventNum;
}
return -1;
}
下面对该函数进行简要的介绍:
- 1行,函数实现了一个等待队列等待的系统调用。
- 3,4行,定义了两个事件的指针。
- 5行,通过
eventNum
找到事件结构体,如果查找失败,则返回-1。 - 7行,定义并初始化一个等待队列。
- 8行,将当前进程放入等待队列中。
- 9行,重新调度新的进程。
- 10行,当进程被唤醒时,进程从等待队列中退出。
- 11行,返回事件号。
有使进程睡眠的函数,就有使进程唤醒的函数。唤醒等待特定事件的函数是sys_CustomEvent_signal()
,该函数的代码如下:
asmlinkage int sys_CustomEvent_signal(int eventNum)
{
CustomEvent *tmp = NULL;
CustomEvent *prev = NULL;
if(!(tmp = FindEventNum(eventNum, &prev)))
return 0;
wake_up(tmp->p); //唤醒等待事件的进程
return -1;
}
下面对该函数进行简要的介绍:
- 1行,函数接收一个参数,这个参数是要唤醒的事件的事件号,在这个事件上等待的函数,都将被唤醒。
- 1行,函数接收一个参数,这个参数是要唤醒的事件的事件号,在这个事件上等待的函数,都将被唤醒。
- 2、3行,定义了两个结构体指针。
- 5行,如果没有发现事件,则返回。
- 7行,唤醒等待队列上的所有进程。
- 8行,返回1,表示成功。
定义了一个关闭事件的函数,该函数先唤醒事件上的等待队列,然后清除事件占用的空间。函数的代码如下:
asmlinkage int sys_CustomEvent_close(int eventNum)
{
CustomEvent *prev=NULL;
CustomEvent *releaseItem;
if(releaseItem = FindEventNum(eventNum, &prev))
{
if(releaseItem == lpevent_end)
lpevent_end = prev;
else if(releaseItem == lpevent_head)
lpevent_head = lpevent_head->next;
else
prev->next = releaseNum->next;
sys_CustomEvent_signal(eventNum);
if(releaseNum){
kfree(releaseNum);
}
return releasNum;
}
return 0;
}
下面对该函数进行简要的介绍:
- 1行,函数表示关闭事件。如果关闭失败返回0,否则返回关闭的事件号
- 3、4行,定义了两个结构体指针。
- 5行,找到需要关闭的事件。
- 7行,如果是链表的最后一个事件,那么将
lpevent_end
指向前一个事件。 - 9行,如果是链表中的第一个事件,那么将
lpevent_head
指向第二个事件。 - 10行,如果事件是中间的事件,那么将中间的事件去掉,用指针连接起来。
- 13行,唤醒需要关闭的事件。
- 14行,清空事件占用的内存。
- 18行,返回事件号。
实验验证
将以上的代码编译进内核,并用新内核启动系统。那么系统中就存在了4个新的系统调用。这4个新的系统调用分别是__NR_CustomEvetn_open、__NR_CustomEvent_wait、__NR_CustomEvent_signal和__NR_myevent_close
。分别使用这4个系统调用编写程序来验证同步机制。
首先需要打开一个事件,完成这个功能的代码如下,该段代码打开看一个事件号为2的函数,然后退出。
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int CustomEvent_open(int flag){
return syscall(__NR_CustomEvent_open, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_open(atoi(argv[1]));
printf("%d\n",i);
return 0;
}
打开一个事件号为2的函数后,就可以在这个事件上将多个进程置为等待状态。将一个进程置为等待状态的代码如下,多次执行下面的代码,并传递参数2,会将进程放入事件2的等待队列中。
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int CustomEvent_wait(int flag){
return syscall(__NR_CustomEvent_wait, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_wait(atoi(argv[1]));
printf("%d\n",i);
return 0;
}
如果执行了上面的操作,那么会将多个进程置为等待状态,这时候调用下面的代码,并传递参数2,来唤醒多个等待事件2的进程。
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int CustomEvent_wait(int flag){
return syscall(__NR_CustomEvent_signal, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_signal(atoi(argv[1]));
printf("%d\n",i);
return 0;
}
当不需要一个事件时,可以删除这个事件,那么在这个事件上等待的所有进程,都会返回并执行,完成该功能的代码如下:
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int myevent_close(int flag){
return syscall(__NR_CustomEvent_close, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_close(atoi(argv[1]));
printf("%d\n", i);
return 0;
}
小结
阻塞和非阻塞在驱动程序中经常用到。阻塞在I/O操作暂时不能进行时,让进程进入等待队列。后者在I/O操作暂时不能进行时,立刻返回。这两种方式各有优劣,在实际应用中,应该有选择地使用。由于阻塞和非阻塞也是由等待队列来实现的,所以本章也概要地讲解了一些等待队列的用法。