一. 阻塞和非阻塞简介
-
IO指的是输入/输出的意思,即应用程序对驱动程序的输入/输出操作
-
若应用程序不能获取到设备资源
- 阻塞式IO会将应用程序对应的线程挂起,直到获取到设备资源
- 非阻塞式IO则不会挂起对应线程,而是一直轮询等待,直到获取到设备资源或直接放弃
-
阻塞IO示意图
-
非阻塞IO示意图
-
应用程序阻塞式和非阻塞式访问驱动程序
/* 对于驱动设备,默认是以阻塞的方式打开的 */ fd = open("/dev/xxx_dev", O_RDWR); /* 若想以非阻塞式方式打开,需要加入相关参数 */ fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);
二. 等待队列
-
阻塞访问的好处是,当设备文件不可操作时,应用程序可以进入阻塞态让出CPU资源。当设备文件可操作时必须唤醒进程,一般在中断服务函数中唤醒。Linux内核提供等待队列(wait queue)来实现阻塞进程的唤醒工作。
-
等待队列头,即一个等待队列的头部,使用在include/linux/wait.h文件下的wait_queue_head_t结构体定义
struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t; /* 创建并初始化队列头,参数q为要初始化的队列头 */ void init_waitqueue_head(wait_queue_head_t *q);
-
等待队列项,每个访问设备的进程都是一个等待队列项;当进程阻塞时,则将其添加到等待队列中;用结构体wait_queue_t表示等待队列项
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,表示当前进程*/ DECLARE_WAITQUEUE(name, tsk);
-
添加/移除等待队列项:当设备不可访问时,要将进程对应的等待队列项插入等待队列中,进程才可以进入休眠状态;当设备可以访问时,再将其移除出等待队列。
/* 参数q为要加入的等待队列(头),参数wait为要加入的等待队列项 */ /* 将等待队列项添加至等待队列 */ 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);
-
主动唤醒等待队列
TASK_INTERRUPTIBLE状态表示可被信号(硬件中断/软件信号)唤醒;TASK_INTERRUPTIBLE状态则不能被信号打断。
/* 参数q为唤醒的等待队列头 */ // 唤醒等待队列中所有的所有处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程 void wake_up(wait_queue_head_t *q); // 唤醒等待队列中所有的所有处于TASK_INTERRUPTIBLE状态的进程 void wake_up_interruptible(wait_queue_head_t *q);
-
等待事件到来,唤醒等待队列中的进程
/* 将进程设置为TASK_UNINTERRUPTIBLE状态 参数wq是以等待队列(头) 参数condition,为真时进程被唤醒,否则一直阻塞*/ #define wait_event(wq, condition) /* timeout为超时时间,单位为jiffies(系统时钟中断次数) 当condition为真时,返回1 当condition为假且timeout到了,返回0*/ #define wait_event_timeout(wq, condition, timeout) /* 将进程设置为TASK_INTERRUPTIBLE状态,即可以被信号打断*/ #define wait_event_interruptible(wq, condition) #define wait_event_interruptible_timeout(wq, condition, timeout)
-
设置进程状态
__set_current_state()
三. 轮询
-
以非阻塞方式访问设备,设备驱动程序需要提供非阻塞的处理方式,即轮询。
-
select函数:能够监视的文件描述符最大数量为1024,并且加大数量会使效率降低
int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout );
nfds:所要监视的三类文件描述符集中,最大文件描述符**+1**
fd_set:fd_set 类型变量(文件描述符集合)的每一个位都代表了一个文件描述符
readfds、writefds 和exceptfds:这三个fd_set类型指针指向描述符集合
- readfds 用于监视指定描述符集的读变化,当集合中有一个文件可以读取,则selsect返回一个大于0的数。
- writefds用于监视指定描述符集的写变化
- exceptfds用于监视指定描述符集的异常
fd_set类型定义
typedef __kernel_fd_set fd_set; #undef __FD_SETSIZE #define __FD_SETSIZE 1024 typedef struct { unsigned long fds_bits[ __FD_SETSIZE / (8 * sizeof(long)) ]; } __kernel_fd_set;
操作fd_set变量
void FD_ZERO(fd_set *set); //清零fd_set变量,即清除所有文件描述符 void FD_SET(int fd, fd_set *set); //向fd_set添加set文件描述符 void FD_CLR(int fd, fd_set *set); //将set文件描述符从fd_set中删除 int FD_ISSET(int fd, fd_set *set); //判断set文件描述符是否属于fd_set,1为匹配
timeout:超时时间,当为NULL时,表示无限期等待
struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微妙 */ };
返回值:0表示超时;-1表示发生错误;其他值表示可以进行操作文件个数
-
举例
int main() { int flag,fd; //要监听的文件描述符 fd_set readfds; //读操作文件描述符集 struct timeval timeout;//超时时间 fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); //以非阻塞形式打开文件 FD_ZERO(&readfds); //将fd加入读文件操作符集 FD_SET(fd,&readfds); timeout.tv_sec = 0; //设置timeout时间 timeout.tv_usec = 50000;//50ms flag = select(fd+1,&readfds,NULL,NULL,&timeout); //轮询文件是否可操作 if(flag == 0) printf("timeout\r\n"); else if(flag == -1) printf("err\r\n"); else { if(FD_ISSET(fd,&readfds)){ /*属于fd文件描述符*/ //读fd文件操作 } } return 0; }
-
poll函数:与select函数相同,只是监视的文件描述符没有限制
int poll(struct pollfd *fds , nfds_t nfds , int timeout);
fds:要监视的文件描述符及事件的集合,类型为pollfd
struct pollfd { int fd; /* 文件描述符 */ short events; /* 请求的事件 */ short revents; /* 返回的事件 */ }; /* events为监听的事件,可以为如下类型 */ POLLIN 有数据可以读取。 POLLPRI 有紧急的数据需要读取。 POLLOUT 可以写数据。 POLLERR 指定的文件描述符发生错误。 POLLHUP 指定的文件描述符挂起。 POLLNVAL 无效的请求。 /* revents为返回参数,即返回的事件 */
nfds:要监视的文件描述符数量
timeout:超时时间
返回值:0超时;-1错误;返回可操作文件的数量
-
举例
int main() { int flag,fd; //要监听的文件描述符 pollfd readfds; //读操作文件描述符 fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); //以非阻塞形式打开文件 readfds.fd = fd; //文件描述符是否可读取 readfds.events = POLLIN; flag = poll(&readfds,1,1000); //轮询文件是否可操作 if(flag == 0) printf("超时\r\n"); else if(flag == -1) printf("错误\r\n"); else //对fd文件进行读操作 return 0; }
- epoll函数:上述两个函数,都会随着所监听的fd数量的增多,而出现效率低下;epoll就是为了解决这个问题的,为处理大并发准备的,一般在网络编程中会使用。
四. Linux驱动下的poll操作函数
-
当应用程序调用select和poll函数对驱动程序进行非阻塞访问时,就会调用file_operations操作集下的poll函数,所以驱动程序需要编写poll函数
/* filp参数为要打开的设备文件 wait为poll_table_struct结构体指针,由应用程序传递进来 */ unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait); /*返回值为设备或资源的状态*/ POLLIN 有数据可以读取。 POLLPRI 有紧急的数据需要读取。 POLLOUT 可以写数据。 POLLERR 指定的文件描述符发生错误。 POLLHUP 指定的文件描述符挂起。 POLLNVAL 无效的请求。 0 超时
-
在poll函数中,调用poll_wait函数;该函数不会引起阻塞,只是将应用程序添加到poll_table中
/* wait_address为要添加到poll_table中的等待队列头 p为poll_table,即为poll函数中的wait参数*/ void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);
-
当APP使用poll和select系统调用时,会先调用poll_initwait(&table),初始化poll_table;然后调用file_operations->poll函数,然后使用poll_wait函数,将等待队列头加入poll_table(将current添加到等待队列头);随后调用poll_schedule_timeout (),到时后,返回运行file_operations->poll函数,最后完成判断事件操作后,返回结果至APP,同时调用poll_freewait(&table)。
/* file_operations->poll函数示例 */ static unsigned int Dev_poll(struct file *filp, struct poll_table_struct *wait) { struct LedDev_t *dev = filp->private_data; /* 将等待队列头添加到poll_table中 */ poll_wait(filp, &queue_head, wait); if() //判断是否可以读取 return POLLIN; else if() //判断是否可以写 return POLLOUT; else return 0; //timeout }
五.小结
- 阻塞IO
- 创建等待队列头
- 当某个条件/事件未达到,为当前进程创建等待队列项;设置进程状态,将进程等待队列项插入等待队列头(进入休眠)
- 进行一次任务调度
- 在某个事件到来时(如中断),唤醒等待队列
- 将进程等待队列项移出等待队列头,设置进程状态,进程继续运行
- 非阻塞IO
- 以非阻塞的形式打开文件
- App使用poll和select函数,轮询文件是否可以操作,设置轮询时间(根据这个轮询时间,可以大大减小cpu占用)
- 对应驱动层,则实现poll函数
以上是我在学习过程中的总结,不当之处请在评论区指出。