Hasen的linux设备驱动开发学习之旅--阻塞与非阻塞I/O

/**
 * Author:hasen
 * 参考 :《linux设备驱动开发详解》
 * 简介:android小菜鸟的linux
 * 	         设备驱动开发学习之旅
 * 主题:阻塞与非阻塞I/O
 * Date:2014-11-05
 */
		阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被
	挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能
	进行设备操作时,并不挂起,它或者放弃,或者不停地查询,直到条件满足以进行操作为止。
		驱动程序通常需要提供这样的能力,当应用程序进行read()、write()等系统调用时,若设备的资源不能
	获取,而用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的xxx_read()、xxx_write()等操作中将
	进程阻塞直到资源可以获取,此后,应用程序的read()、write()等调用才返回,整个过程仍然进行了正确的
	设备访问,用户没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的
	xxx_read()、xxx_write()等操作应该立即返回,read()、write()等系统调用也随即被返回。
		阻塞的进程会进入休眠状态,需要有一个地方来唤醒休眠的进程。唤醒进程的最大可能是在中断里面。

		下面的代码段分别演示了以阻塞和非阻塞方式读取一个字符的代码。实际的串口编程中,若使用非阻塞模
		式,还可借助信号(sigaction)以异步方式访问串口以提高CPU的利用率。

		代码段1:阻塞地读串口一个字符

		char buf ;
		fd = open("/dev/ttyS1",O_RDWR) ; /*以阻塞的方式打开串口*/
		...
		res = read(fd,&buf,1) ; /*当串口上有输入时才返回*/
		if(res == 1)
			printf("%c\n",buf) ;

		代码段2:非阻塞地读串口一个字符

		char buf ;
		fd = open("/dev/ttyS1",O_RDWR|O_NONBLOCK) ;/*以非阻塞的方式打开串口*/
		...
		while(read(fd,&buf,1) == 1)
			continue ; /*串口上无输入也返回,所以要循环尝试读取串口*/
		printf("%c\n",buf) ;

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

	等待队列的操作如下:
		(1)定义“等待队列头”
			wait_queue_head_t my_queue ;
		(2)初始化“等待队列头”
			init_waitqueue_head(&my_queue) ;
		用下面的宏可以实现定义和初始化“等待队列头”
			DECLARE_WAIT_QUEUE_HEAD(name)
		(3)定义等待队列
			该宏用于定义并初始化一个名为name的等待队列
			DECLARE_WAITQUEUE(name,tsk)
		(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是否满足,均返回。

		(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和TASH_UNINTERRUPTIBLE的进程,而wait_event_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()函数的作用是把目前的进程状态置成TASK_UNINTERRUPTIBLE,并定
			义一个等待队列,之后把它附属到等待队列q,直到资源可或获得,q引导的等待队列被唤醒或者进
			程收到信号。

		在许多的linux驱动程序之中,并不调用sleep_on()或者interruptible_sleep_on(),而是直接
		进行进程状态的改编和切换。

		代码示例:在驱动代码中改变进程状态并调用schedule()

			static int xxx_write(struct file *filp,const char *buffer,
					size_t count,loff_t *ppos)
			{
				...
				DECLARE_WAITQUEUE(wait,current) ;/*定义等待队列*/
				add_wait(&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_NONOBLOCK){ /*非阻塞*/
							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_RUNNNIG*/
			}

		上述的代码示例对理解进程切换非常重要,所以应该读几遍直至完全领悟,下面是几个要点:
			(1)如果是非阻塞访问(O_NONOBLOCK被设置),设备忙时,直接返回"-EAGAIN" .
			(2)对于阻塞访问,会进行状态的切换并显式通过"schedule()"调度其他进程执行。
			(3)醒来时要注意,由于调度出去的时候,进程的状态是TASK_INTERRUPTIBLE,即浅度的睡眠,因
				此唤醒它的有可能是信号,因此,我们首先通过"signal_pending(current)"了解是不是信
				号唤醒的,如果是,立即返回"-ERESTARTSYS" 。

	轮询操作
			在用户程序中,select()和poll()也是设备阻塞与非阻塞访问息息相关的论题。使用非阻塞I/O的
		应用程序通常会使用select()和poll()系统调用查询是否可以对设备进行无阻塞的访问。select()和
		poll()系统调用最终会引发设备驱动中的poll()函数被执行。epoll()是扩展的poll() .
			应用程序中最广泛的应用的是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) ;/*判断文件描述符是否被置位*/

		设备驱动中的轮询编程

		设备驱动中poll()函数的原型是:
		unsigned int (*poll) (struct poll_table *wait) ;
		第一个参数为file结构体指针,第二个参数为轮询表指针。这个函数进行两项工作:

		(1)对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加
			到poll_table.
		(2)返回表示是否能对设备进行无阻塞读、写访问的掩码。

		关键的用于向poll_table注册等待队列的poll_wait()函数的原型如下:
		void poll_wait(struct file *filp,wait_queue_head_t *queue,poll_table *wait) ;
			poll_wait()函数的名字非常容易让人产生误会,以为它和wait_event()等一样,会阻塞地
		等待某时间发生,其实这个函数并不会引起阻塞。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 ;
			}

	总结:
		阻塞与非阻塞访问是I/O操作的两种不同模式,前者在操作暂时不可进行时会让进程睡眠,后者则不然。
		在设备驱动中阻塞I/O一般基于等待队列来说实现,等待队列可用于同步驱动中事件发生的先后顺序。
	使用非阻塞I/O的应用程序也可借助轮询函数来查询设备是否能立即被访问,用户空间调用select()和poll()
	接口,设备驱动提供poll()函数。设备驱动的poll()本身不会阻塞,但是poll()和select()系统调用则会阻
	塞地等待文件描述符集合中的至少一个可访问或超时。

时间: 2024-11-13 04:08:48

Hasen的linux设备驱动开发学习之旅--阻塞与非阻塞I/O的相关文章

Hasen的linux设备驱动开发学习之旅--支持轮询操作的设备驱动

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:支持轮询操作的设备驱动 * Date:2014-11-07 */ 在globalfifo的poll()函数中,首先将设备结构体中的r_wait和w_wait等待队列头添加到等待列表, 然后通过判断dev->current_len是否等于0来获取设备的可读状态,通过判断dev->current_len是否等于 GLOBALF

Hasen的linux设备驱动开发学习之旅--异步通知

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:异步通知 * Date:2014-11-05 */ 一.异步通知的概念和作用 阻塞和非阻塞访问.poll()函数提供了较好地解决设备访问的机制,但是如果有了异步通知整套机制就更 加完整了. 异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这 一点非常类似于硬件上"中断"的概

Hasen的linux设备驱动开发学习之旅--时钟

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:时钟 * Date:2014-11-15 */ 一.内核定时器 1.内核定时器编程 软件意义上的定时器最终依赖硬件定时器来是实现,内核在时钟中断发生后执行检测各定时器是否到期, 到期后的定时器处理函数将作为软中断在底半部执行.实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ 软中断,运行当前处理器上到期的所有定时器. Linu

Hasen的linux设备驱动开发学习之旅--异步I/O

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:异步I/O * Date:2014-11-11 */ linux中最常用的输入/输出(I/O)模型是同步I/O.在这个模型中,请求发出后,应用就会阻塞,知道请求满足 为止.但是在某些情况下,I/O请求可能需要与其他的进程进行交叠.可移植操作系统接口(POSIX)异步I/O(AIO) 应用程序接口(API)就提供了这种功能. AIO基本

Hasen的linux设备驱动开发学习之旅--中断

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:中断 * Date:2014-11-13 */ 一.中断和定时器 所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停执行当前的程序, 转而去处理突发事件,处理完毕后CPU又返回原程序被中断的位置并继续执行. 下图是中断的分类 嵌入式系统以及X86 PC中大多包含可编程中断控制器(PIC),许多MCU内部就

Hasen的linux设备驱动开发学习之旅--linux设备驱动中的并发与竞态

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:linux设备驱动中的并发与竞态 * Date:2014-11-04 */ 1.并发与竞态 并发(concurrency)指的是多个执行单元同时.并行被执行,而并发的执行单元对共享资源(软件上的全 局变量,静态变量等)的访问则很容易导致竞态(race conditions). 主要的竞态发生在以下几种情况: (1)对称多处理(SMP)

Linux设备驱动开发学习(1):前言

虽然网络上已经有很多Linux设备驱动开发学习的文章和博客,更是有很多经典的Linux设备驱动开 发的书籍,写这些博文似乎意义不大,但把自己的学习过程.学习心得记录下来,一方面有着强化巩固的 意义,另一方面也是把所学知识转化为自己所得的必要途径之一,这是我写这些的博客的原始动力.

Linux设备驱动开发学习(3):构造和运行模块(未完)

从本章开始引入所有关于模块和内核编程的基本概念,并编写一个完整模块来实践这些基本的概 念. 3.1 搭建测试环境 由于所有测测试代码都是基于Ubuntu 14.04.2 Desktop的3.16.0-30-generic内核,所以最好是到 Linux官方网站去下载一份该版本内核的源代码.另外,建议在虚拟机里面安装你的Ubuntu桌面环境,这 样避免因为误操作造成硬件损坏或者重要数据丢失.更多测试环境搭建细节可参考博文: Ubuntu 14.04.2 + Vmware搭建Linux驱动开发环境 3

Linux设备驱动开发学习(2):Linux设备驱动简介(未完)

(未完待续......)