Linux驱动进阶(二)——设备驱动中的阻塞和同步机制

文章目录

  • 前言
  • 阻塞与非阻塞
  • 等待队列
    • 等待队列概述
    • 等待队列的实现
    • 等待队列的使用
  • 同步机制实验
    • 同步机制设计
    • 实验验证
  • 小结


前言

阻塞和非阻塞是设备访问的两种基本方式。使用这两种方式,驱动程序可以灵活地支持阻塞与非阻塞访问。在写阻塞与非阻塞的驱动程序时,经常用到等待队列,所有本章将对等待队列进行简要介绍。

阻塞与非阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上它们是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的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_INTERRUPTIBLETASK_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;  //链尾指针

每个事件由一个链表组成,每个链表中包含了等待这个事件的等待队列。这个结构下图所示。
Linux驱动进阶(二)——设备驱动中的阻塞和同步机制_第1张图片
为了实现实验的设计,定义了一个函数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 
#include 
#include 
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 
#include 
#include 
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 
#include 
#include 
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 
#include 
#include 
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操作暂时不能进行时,立刻返回。这两种方式各有优劣,在实际应用中,应该有选择地使用。由于阻塞和非阻塞也是由等待队列来实现的,所以本章也概要地讲解了一些等待队列的用法。

你可能感兴趣的:(Linux驱动开发,linux,驱动开发)