Linux 设备驱动中的阻塞与非阻塞 I/O

阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。
阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能不停地查询,这反而会无谓地耗费 CPU 资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将 CPU 资源让给其他进程。因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
下面以两个实例演示阻塞和非阻塞的读取串口一个字符:

  • 阻塞地读取串口一个字符
char buf; 
fd = open("/dev/ttyS1", O_RDWR); 
... 
res = read(fd,&buf,1); //当串口上有输入时才返回
if(res==1) 
	printf("%c\n", buf); 
  • 非阻塞地读取串口一个字符
char buf; 
fd = open("/dev/ttyS1", O_RDWR| O_NONBLOCK); 
... 
while(read(fd,&buf,1)!=1); //串口上无输入也返回,所以要循环尝试读取串口
printf("%c\n", buf);

1、等待队列(阻塞)

在 Linux 驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。wait queue 很早就作为一个基本的功能单位出现在 Linux 内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,信号量在内核中也依赖等待队列来实现。

  • 定义等待队列头:wait_queue_head_t my_queue;
  • 初始化等待队列头:init_waitqueue_head(&my_queue);
  • 定义并初始化等待队列头:DECLARE_WAIT_QUEUE_HEAD (name)
  • 定义并初始化等待队列name:DECLARE_WAITQUEUE(name, tsk)
  • 添加等待队列: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);
  • 阻塞等待事件(condition)发生:
wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);	//time以jiffy 为单位
wait_event_interruptible_timeout(queue, condition, timeout);
  • 唤醒队列:
void wake_up(wait_queue_head_t *queue); 
void wake_up_interruptible(wait_queue_head_t *queue); 

wake_up() 可 唤 醒 处 于TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 的进程, 而wake_up_interruptible()只能唤醒处于 TASK_INTERRUPTIBLE 的进程。

  • 在等待队列上睡眠
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。

  • 改变进程状态:
set_current_state();
__add_wait_queue();
current->state = TASK_UNINTERRUPTIBLE/TASK_RUNNING/TASK_INTERRUPTIBLE	//对进程状态直接赋值

set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于__add_ wait_queue()。因此,在许多设备驱动中,并不调用 sleep_on()interruptible_sleep_on(),而是亲自进行进程的状态改变和切换。

  • 以下为在驱动程序中改变进程状态并调用 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; 
} 

2、轮询操作(非阻塞)

使用非阻塞 I/O 的应用程序通常会使用 select()和 poll()系统调用查询是否可对设备进行无阻塞的访问。select()和 poll()系统调用最终会引发设备驱动中的 poll()函数被执行。

2.1 应用程序中的轮询编程

应用程序中最广泛用到的是 BSD UNIX 中引入的 select()系统调用,其原型如下:

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中 readfds、writefds、exceptfds 分别是被 select()监视的读、写和异常处理的文件描述符集合,numfds 的值是需要检查的号码最高的文件描述符加 1。timeout 参数是一个指向 struct timeval 类型的指针,它可以使 select()在等待 timeout 时间后若没有文件描述符准备好则返回。struct timeval 数据结构的定义如下代码:

struct timeval 
{ 
 	int tv_sec; /* 秒 */ 
 	int tv_usec; /* 微妙 */ 
}; 

FD_ZERO(fd_set *set):清除一个文件描述符集。
FD_SET(int fd,fd_set *set):将一个文件描述符加入文件描述符集中。
FD_CLR(int fd,fd_set *set) :将一个文件描述符从文件描述符集中清除。
FD_ISSET(int fd,fd_set *set) :判断文件描述符是否被置位。

2.2 驱动程序中的轮询编程

设备驱动中 poll()函数的原型如下:

unsigned int(*poll)(struct file * filp, struct poll_table* wait); 

第一个参数为 file 结构体指针,第二个参数为轮询表指针。这个函数应该进行以下两项工作。

  • 对可能引起设备文件状态变化的等待队列调用 poll_wait()函数,将对应的等待队列头添加到poll_table。
  • 返回表示是否能对设备进行无阻塞读、写访问的掩码。
    关键的用于向 poll_table 注册等待队列的 poll_wait()函数的原型如下:
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait); 

poll_wait()函数的名称非常容易让人误会它会阻塞地等待某事件的发生,但这个函数并不会引起阻塞。poll_wait()函数所做的工作是把当前进程添加到 wait 参数指定的等待列表(poll_table)中。
驱动程序 poll()函数应该返回设备资源的可获取状态,即 POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL 等宏的位“或”结果。每个宏的含义都表明设备的一种状态,如 POLLIN(定义为 0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为 0x0004)意味着设备可以无阻塞地写。
通过以上分析,可得出设备驱动中 poll()函数的典型模板:

static unsigned int xxx_poll(struct file *filp, poll_table *wait) 
{ 
	unsigned int mask = 0; 
	struct xxx_dev *dev = filp->private_data; /*获得设备结构体指针*/ 

	... 
	poll_wait(filp, &dev->r_wait, wait);//加读等待队列头
	poll_wait(filp, &dev->w_wait, wait);//加写等待队列头 

	if (...)//可读
	{ 
		mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/ 
	} 

	if (...)//可写
	{ 
		mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/ 
	} 

	... 
	return mask; 
	}
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值