一、基本概念介绍
这里的 IO 指的是Input/Output,也就是输入和输出,可以理解为应用程序对驱动设备的读写操作。阻塞IO是指:当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,应用程序会被系统挂起,系统会先去执行其他进程,直到设备资源可以获取为止。非阻塞IO是指:如果应用程序不能访问到设备驱动的资源,也不会被挂起,而是一直占用CPU轮询访问直到有效访问或出错。 实际上,区分阻塞或非阻塞重点在于满足上面的概念,而不是具体实现方法,下面的笔记内容只是展示了典型的实现方案而已!实现阻塞IO典型方案是使用在设备驱动中使用等待队列;实现非阻塞IO的典型方案则是在设备驱动中实现poll_wait。 异步通知机制可以实现设备驱动层通过信号主动通知应用程序。这种机制逻辑上与硬件中断类似,系统可以去执行其他用户进程直到设备驱动发出信号唤醒指定的用户程序;可以理解为软件中断。 |
/
二、阻塞IO的原理与实现
Linux内核提供了wait_queue来实现进程的阻塞与唤醒工作。 函数接口与结构体: ① 等待队列头结构体wait_queue_head_t:<linux/wait.h> struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t; ② #define init_waitqueue_head(wait_queue_head_t *q); //初始化等待队列头结构体的函数接口,初始化结构体的自旋锁和队列头 #include <linux/wait.h> ③ #define DECLARE_WAIT_QUEUE_HEAD(name) //宏函数一次性初始化等待队列头并返回,name就是需要初始化的对象 #include <linux/wait.h> ④ 等待队列项结构体wait_queue_t:<linux/wait.h> 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; ⑤ #define DECLARE_WAITQUEUE(name,tsk); //宏函数给当前进程(tsk=current)创建一个等待项并初始化(name=wait) #include <linux/wait.h> ⑥ void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); //将等待项wait加入到等待队列q中,链表队列头插 #include <linux/wait.h> ⑦ void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); //将等待项wait从等待队列q中移除 #include <linux/wait.h> ⑧ void wake_up(wait_queue_head_t *q); //调用等待队列里所有等待项的func函数接口,逐一循环唤醒 #include <linux/wait.h> ⑨ void wake_up_interruptible(wait_queue_head_t *q); //唤醒处于interruptible休眠模式的进程 #include <linux/wait.h> ⑩ #define wait_event(wq,condition); //根据条件condition是否满足(=1),自动唤醒wq队列头;否则一直阻塞 #include <linux/wait.h> 11、#define wait_event_timeout(wq,condition,timeout); //定时等待condition条件满足,则自动唤醒wq队列,返回1表示条件满足,0=超时 #include <linux/wait.h> 参数解释: timeout:阻塞等待超时时间,单位:jiffies / 标准基本的实现流程: ① 调用函数init_waitqueue_head初始化一个等待队列的头 ② 在目标函数位置,判断条件满足则调用函数wake_up_interrupt或wake_up来唤醒等待队列里的所有等待的项 ③ 应用程序读写接口,调用函数DECLARE_WAITQUEUE为当前进程创建一个等待项并调用函数add_wait_queue加入等待队列 ④ 通过函数: __set_current_state(TASK_INTERRUPTIBLE); //设置任务状态 schedule(); //当前进程进入休眠状态 if(signal_pending(current)){ //判断是否是信号引起的唤醒 retvalue = -ERESTARTSYS; goto err0; } __set_current_state(TASK_RUNNING); //设置进程状态为运行状态 将进程进入休眠模式,等待被唤醒 ⑤ 被唤醒后,调用函数remove_wait_queue将等待项从队列中移除 / |
/
三、非阻塞IO的原理与实现
应用程序的非阻塞访问驱动设备,其实就是轮询请求;典型的应用层轮询访问函数有poll、epoll和select,这三种轮询方法访问的都是字符驱动设备的poll函数。因此需要在设备驱动程序中去实现poll函数接口。 Linux设备驱动下的poll操作函数实现: file_operations结构体的poll函数原型:unsigned int (*poll) (struct file *, struct poll_table_struct *); 参数解释: struct file *:要打开的设备文件(文件描述符) struct poll_table_struct:由应用程序传递进来的,一般将此参数传递给poll_wait函数。 返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下: poll_wait函数原型:#include <linux/poll.h> void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p); //将应用程序添加到poll_table中 参数解释: *wait_address:需要添加到poll_table中的等待队列头; *p:就是file_operations中poll函数的wait传入参数; 标准的非阻塞IO实现流程: ① 字符驱动设备实现poll函数接口,根据设备状态返回符合规定的状态值(poll_wait函数其实无需调度使用); ② 在应用程序处,先使用poll、epoll或select函数轮询检查设备是否可操作; |
/
四、异步通知的原理与实现
异步通知也可以理解为Linux设备驱动对应用程序产生的软件中断(信号通知)。驱动可以通过主动向应程序发送双方约定的信号的方式来告知自己可以被访问了,应用程序则可以在收到信号后,才从设备驱动中读取数据或写入数据。目前linux内核所支持的异步通知信号如下:#include <asm/signal.h> 应用程序所使用的函数接口: ① sighandler_t signal(int signum,sighandler_t handler); //设定指定信号的处理函数,返回的是上一个信号处理函数地址或SIG_ERR #include <signal.h> 参数解释: signum:需要监听并处理的信号 handler:信号处理函数接口地址,格式:typedef void (*sighandler_t)(int); ② int fcntl(int fd, int cmd, ... /* arg */); //对打开的目标字符驱动设备执行cmd指令所命令的操作,在这里主要是为了将进程状态信息告知设备驱动 #include <unistd.h> #include <fcntl.h> 参数解释: fd:当前打开字符驱动返回的文件操作对象 cmd:具体的操作指令,这里会用到F_SETOWN、F_GETFL、F_SETFL 设备驱动所使用的函数接口: ① int (*fasync)(int fd, struct file *filp ,int on); //这是file_operations里的函数,应用层通过fcntl函数来调用它注册到设备驱动来告知内核需要信号 参数解释: fd:调用fcntl函数的应用进程文件描述符; filp: on:=1表示注册,=0表示需要注销 ② int fasync_helper(int fd, struct file *filp, int on, struct fasync_struct **fapp); //向内核为具体应用进程申请异步信号通知,一般在前面的fasync函数接口实现中被调用 #include <linux/fcntl.h> 参数解释: fd:需要注册启动异步信号通知的应用程序文件描述ID *filp: **fapp:一个描述异步信号通知的结构体对象struct fasync_struct;这个是返回值,返回具体的对象; 结构体fasync_struct原型:<linux/fs.h> struct fasync_struct { spinlock_t fa_lock; int magic; int fa_fd; struct fasync_struct *fa_next; /* singly linked list */ struct file *fa_file; struct rcu_head fa_rcu; }; ③ void kill_fasync(struct fasync_struct **fp, int sig, int band); //向指定的应用进程发送指定的信号 #include <linux/fcntl.h> 参数解释: fp:要操作的目标fasync_struct对象; sig:目标发送的信号; band:设备可读则设置为POLL_IN,设备可写则设备为POLL_OUT; 异步信号通知IO功能的基本配置流程: ① 在设备驱动内,实现file_operations的fasync函数,在里面调用函数fasync_helper函数注册应用程序的异步信号; ② 在设备驱动内,实现file_operations的release函数,在里面调用fasync(-1,filp,0)注销掉异步信号对象; ③ 在设备驱动内,适当的位置,通过调用函数kill_fasync函数向目标进程对象发送信号; ④ 在应用程序里,使用signal函数为约定好的指定信号注册对应的信号处理函数; ⑤ 在应用程序里,使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核; ⑥ 在应用程序里,使用flags = fcntl(fd, F_GETFL)获取当前的进程状态; ⑦ 在应用程序里,使用fcntl(fd, F_SETFL, flags | FASYNC)(FASYNC会触发设备驱动的fasync函数)开启当前进程的异步通知功能 异步通知方案存在的问题与解决方案(理论): 问题1:多个应用程序采用异步通知的方法操作同一个设备驱动? 答:在设备驱动里管理一个fasync_struct结构体对象的数组,每个应用进程对应数组里的一个成员。当符合信号触发条件时,循环向数组内有效成员发送信号。 问题2:一个应用程序打开了多个设备,且都采用了异步通知的方案? 答:配合select或poll这种非阻塞IO方式,在信号中断产生后,通过非阻塞IO进一步确认发出信号的设备。 |