阻塞和非阻塞IO

阻塞访问

应用程序调用read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。

../../_images/image-20200605084032365.png

非阻塞访问

应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。

../../_images/image-20200605084158960.png

应用程序示例

fd = open("/dev/xxx_dev", O_RDWR);   /* 阻塞方式打开,也是默认的方式 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */



fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); 			/* 读取数据 */

// 参数:O_NONBLOCK
// 表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了

阻塞方式访问 等待队列

  1. 等待队列头

    // 结构体定义在文件 include/linux/wait.h中 
    struct __wait_queue_head {
    	spinlock_t lock;
    	struct list_head task_list;
    };
    typedef struct __wait_queue_head wait_queue_head_t;
    void init_waitqueue_head(wait_queue_head_t *q) //等待队列头初始化
    DECLARE_WAIT_QUEUE_HEAD(name)                  //一次性完成等待队列头创建和初始化,名字为name
    
  2. 等待队列项 等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个等待队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。 队列项结构体定义如下:

    struct __wait_queue { 
    	unsigned int flags; 
    	void *private; 
    	wait_queue_func_t func; 
    	struct list_head task_list;
    };
    typedef struct __wait_queue wait_queue_t;
    

    创建并初始化队列项宏如下

    /*
    *功能:定义并初始化一个等待队列项
    *@name:队列项的名字
    *@tsk:这个等待队列项属于哪个任务 (进程 ),一般设 置 为current,在 Linux内核中 current相当于一个全局变量,表示当前进程
    */
    DECLARE_WAITQUEUE(name, tsk) 
    
  3. 将队列项添加 /移除等待队列头

    /*
    *功能:将队列项添加到队列头
    *@q   :要加入到哪个等待队列头。
    *@wait:要加入的等待队列项
    */
    void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
    
    /*
    *功能:将队列项添加到队列头
    *@q   :要从哪个等待队列头删除。
    *@wait:要删除的等待队列项
    */
    void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
    
  4. 等待唤醒

    /*
    *功能:当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数,
    *      这个等待队列头中的所有进程都唤醒
    *@q   :要唤醒的等待队列头指针。
    */
    void wake_up(wait_queue_head_t *q) // TASK_INTERRUPTIBLE和 TASK_UNINTERRUPTIBLE状态的进程
    void wake_up_interruptible(wait_queue_head_t *q)//只能唤醒处于 TASK_INTERRUPTIBLE状态的进程
    
  5. 等待事件 除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程 和等待事件有关的 API函数所示:

函数 功能
wait_event(wq, condition) 等待以wq为等待队列头的等待队列被唤醒,前提是 condition条件必须满足 (为真 ),否则一直阻塞。此函数会将进程设置为TASK_UNINTERRUPTIBLE状态
wait_event_timeout(wq, condition, timeout) 功能和wait_event类似,但是此函数可以添加超时时间,以 jiffies为单位。此函数有返回值,如果返回0的话表示超时时间到,而且 condition为假。为 1的话表示 condition为真,也就是条件满足了。
wait_event_interruptible(wq, condition) 与wait_event函数类似,但是此函数将 进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断。
wait_event_interruptible_timeout(wq, condition, timeout) 与wait_event_timeout函数类似,此函数也 将 进程设置为 TASK_INTERRUPTIBLE,可以被信号打断。

非阻塞访问 轮询

非阻塞方式访问 当应用程序调用select、 epoll或 poll函数的时候设备驱动程序中的 poll函数就会执行,因此需要在设备驱动程序中编写poll函数epoll更多的是用在大规模的并发服务器上,因为在这种场合下 select和 poll并不适合。当设计到的文件描述符 (fd)比较少的时候就适合用 selcet和 poll

select 函数

/*
*@功能     :
*@nfds     :所要 监视的这三类文件描述集合中, 最大文件描述符加 1
*@readfds  :都代表了一个文件描述符。 readfds用于监视指定描述符集的读变化,
*            也就是监视这些文件是否可以读取,
*            只要这些集合里面有一个文件可以读取那么 seclect就会返回一个
*            大于 0的值表示文件可以读取。如果没有文件可以读取,
*            那么就会根据 timeout参数来判断是否超时。可以将 readfs设置为 NULL,
*            表示不关心任何文件的读变化。
*@writefds :writefds和 readfs类似,只是 writefs用于监视这些文件是否可以进行写操作。
*@exceptfds:用于监视这些文件的异常。
*@timeout  :超时时间 当 timeout为 NULL的时候就表示无限期的等待。
*            struct timeval { 
*            long tv_sec;  秒  
*            long tv_usec; 微妙  
*            };
*@返回值:0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作 ;
*            -1,发生错误;
*            其他值,可以进行操作的文件描述符个数。
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
函数 功能
void FD_ZERO(fd_set *set) fd_set变量的所有位都清零
void FD_SET(int fd, fd_set *set) fd_set 变量的某个位置 1也就是向 fd_set添加一个文件描述符
void FD_CLR(int fd, fd_set *set) fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除
int FD_ISSET(int fd, fd_set *set) FD_ISSET用于 测试一个文件是否属于某个集合

在应用程序使用示例:

void main(void)
{
	int ret, fd; 			/* 要监视的文件描述符 */
	fd_set readfds; 		/* 读操作文件描述符集 */
	struct timeval timeout; /* 超时结构体 */
	fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
	FD_ZERO(&readfds); 		/* 清除readfds */
	FD_SET(fd, &readfds);   /* 将fd添加到readfds里面 */
	/* 构造超时时间 */
	timeout.tv_sec = 0;
	timeout.tv_usec = 500000; /* 500ms */
	ret = select(fd + 1, &readfds, NULL, NULL, &timeout);  /* 在500ms内会一直轮询查询 注:不会挂起线程,cpu被占着*/
	switch (ret) {
		case 0: /* 超时 */
		printf("timeout!\r\n");
		break;
		case -1: /* 错误 */
		printf("error!\r\n");
		break;
		default: /* 可以读取数据 */
			if(FD_ISSET(fd, &readfds)) { /* 判断是否为fd文件描述符 */
			/* 使用read函数读取数据 */
			}
		break;
	}
}

poll 函数

在单个线程中, select函数能够监视的文件描述符 数量有最大的限制,一般为 1024,可以 修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll函数, poll函数本质上和 select没有太大的差别,但是 poll函数没有最大文件描述符限制,

/*
*@功能     :
*@fds      :要监视的文件描述符集合以及要监视的事件 是数组,数组元素都是结构体 pollfd类型的,
*            struct pollfd { 
*               int fd; 	   //要监视的文件描述符,如果 fd无效 events监视事件也就无效,且 revents返回0
*				short events;  //请求的事件 是要监视的事件,可监视的事件类型如下所示:
*				short revents; //返回的事件由 Linux内核设置具体的返回事件。
*			};
*           events 		类型如下:
*			POLLIN 		有数据可以读取。
*			POLLPRI 	有紧急的数据需要读取。
*			POLLOUT 	可以写数据。
*			POLLERR 	指定的文件描述符发生错误。
*			POLLHUP 	指定的文件描述符挂起。
*			POLLNVAL 	无效的请求。
*			POLLRDNORM 	等同于 POLLIN
*

*@nfds     :poll函数要监视的文件描述符数量
*@timeout  :超时时间超时时间,单位为 ms
*@返回值   :revents域中不为 0的 pollfd结构体个数,也就是发生事件或错误的文件描述符数量; 
*            0,超时 ;
*            -1,发生错误,并且设置 errno为错误类型。
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout)

非阻塞式访问示例:

void main(void)
{
	int ret;
	int fd; 								/* 要监视的文件描述符 */
	struct pollfd fds;
	fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
	/* 构造pollfd结构体 */
	fds.fd = fd;
	fds.events = POLLIN; 	   /* 监视数据是否可以读取 */
	ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时500ms */
	if (ret) { 				/* 数据有效 */
		// ......
		/* 读取数据 */
		// ......
	} else if (ret == 0) { /* 超时 */
		// ......
	} else if (ret < 0) { /* 错误 */
		// ......
}

epoll 函数

传统的 selcet和 poll函数都会随着所监听的 fd数量的增加,出现效率低下的问题,而且poll函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll应运而生 epoll就是为处理大并发而准备的,一般常常在网络编程中使用epoll函数。

创建 epoll句柄

/*
*@功能	:创建一个 epoll句柄,
*@size	:无意义,随便填个大于0的值即可
*@返回值:epoll句柄,如果为 -1的话表示创建失败。
*/
int epoll_create(int size)

添加监视的文件描述符

/*
*@功能	:添加监视的文件描述符以及监视的事件
*@epfd	:要操作的 epoll句柄 epoll_create函数返回值
*@op	:表示要对 epfd(epoll句柄 )进行的操作,可以设置为:
*		  EPOLL_CTL_ADD 向 epfd添加文件参数 fd表示的描述符 
*		  EPOLL_CTL_MOD 修改参数 fd的 event事件 
*		  EPOLL_CTL_DEL 从 epfd中删除 fd描述符 
*@fd	:要监视的文件描述符
*@event	:要监视的事件类型,
*		  struct epoll_event { 
*				uint32_t events;   // epoll事件 
*				epoll_data_t data; // 用户数据  
*		  };
*         events 这些事件可以进行“或”操作,也就是说可以设置监视多个事件 类型如下:
*         EPOLLIN 有数据可以读取
*         EPOLLOUT 可以写数据
*         EPOLLPRI 有紧急的数据需要读取
*         EPOLLERR 指定的文件描述符发生错误
*         EPOLLHUP 指定的文件描述符挂起
*         EPOLLET 设置 epoll为边沿触发,默认触发模式为水平触发。
*         EPOLLONESHOT 一次性的监视,当监视完成以后还需要再次监视某个fd,那么就需要将fd重新添加到 epoll里面。
*@返回值:0,成功 ;-1,失败,并且设置 errno的值为 相应的错误码。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

函数来等待事件的发生

/*
*@功能		:函数来等待事件的发生,类似 select函数。
*@epfd		:要等待的 epoll
*@events	:指向 epoll_event结构体的数组,当有事件发生的时候 Linux内核会填写 events,
*             调用者可以根据 events判断发生了哪些事件。
*@maxevents	:events数组大小,必须大于 0
*@timeout	:超时时间,单位为 ms
*@返回值:,超时 ;-1,错误;其他值,准备就绪的文件描述符数量。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

Linux驱动下的 poll操作函数

当应用程序调用 select或 poll函数来对驱动程序进行非阻塞访问的时候,驱动程序 file_operations操作集中的 poll函数就会执行。所以驱动程序的编写者需要提供对应的 poll函 数, poll函数原型如下所示:

/*
*@功能 :
*@filp :要打开的设备文件 (文件描述符 )。
*@wait :结构体 poll_table_struct类型指针, 由 应用程序传 递进来的。一般将此参数传递给poll_wait函数。
*@返回值 :向应用程序返回设备或者资源状态,可以返回的资源状态如下:
*		POLLIN 有数据可以读取。
*		POLLPRI 有紧急的数据需要读取。
*		POLLOUT 可以写数据。
*		POLLERR 指定的文件描述符发生错误。
*		POLLHUP 指定的文件描述符挂起。
*		POLLNVAL 无效的请求。
*		POLLRDNORM 等同于 POLLIN,普通数据可读
*/
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

我们需要在驱动程序的 poll函数中调用 poll_wait函数, poll_wait函数不会引起阻塞,只是将应用程序添加到 poll_table中。

 // poll_wait函数原型如下:
 /*
*@功能 :
*@filp 			:要打开的设备文件 (文件描述符 )。
*@wait_address 	:是要添加到 poll_table中的等待队列头
*@p:			:就是file_operations中 poll函数的 wait参数
*/

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)