linux设备驱动中的阻塞,非阻塞I/O,异步通知
阻塞操作
在执行设备操作时, 若不能获得资源则进程睡眠,让出CPU,当满足可操纵条件后,内核唤醒进程继续执行;
由于阻塞的进程会进入睡眠状态,因此需要适时的唤醒它,否则进程就”一睡不醒了”.
在linux驱动中, 采用等待队列(wait queue)的方式进行唤醒睡眠的进程.
等待队列的驱动编程:(在已有的设备驱动基本框架上继续)
这里,假设一个设备要实现读/写.
首先就要考虑: 读的前提是要有数据可读,如果没有就需要阻塞等待,直到忘里面写入数据,然后才能读.(这里暂时不考虑设备空/满等造成的死锁);
其次, 当设备满了, 想要继续写,就要阻塞等待读操作把数据取走,然后才能继续写.
好, 接下来看看具体的驱动编程实现:
在模块加载函数
xxx_init();
中添加初始化等待队列头.(有几个添加几个)init_waitqueue_head (q_head);
其中, q_head是在设备外新建的一个等待队列头,当然也可以将它封装到一个设备结构体中.
wait_queue_head_t q_head;
在读操作中进入睡眠状态, 等待唤醒
wait_event_interruptible(q_head, condition);
当前进程进入睡眠等待的可打断状态, 直到条件condition满足时才被唤醒. (这里条件就是文件可读,这个条件可根据实际情况来设置,如网卡有数据/串口有数据等等)
在写操作完成后, 发出唤醒的信号
wake_up_interruptible(wait_queue_head_t *q_head);
当写的操作完成后, 设备可读了, 然后就发出wake_up_interruptible信号,唤醒等待队列q_head中的进程. 这样读操作自动被唤醒执行.
非阻塞操作:
进程在不能进行设备操作是并不睡眠,而是直接返回结果;(使用较多.)
实现过程:
这样要实现设备的状态的监控,如果使用while(1)
循环的轮询,那么会占用大量的资源.不合适.
因此应用层通过select() / poll() /epoll() 则可妥善的处理这样的问题.把轮询交给kernel(sys_poll)进行监控处理(其实linux操作系统的调度算法才是核心, 这里linux提供了简单的接口,减轻了咱们的难度).然后轮询时间有kernel来控制, 它最终时刻调用驱动程序中的.poll, 通过它的返回值便可查询硬件的状态,从而返回给应用层.
驱动编程实现:
应用层:
调用select/poll/epoll
;(略)驱动层(kernel):
在file_operation
结构体中实现一下.poll
即可.unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait_table);
其中: wait_table是轮询表指针. 该函数进行2项工作:
对可能引起设备文件状态变化的等待队列q_head 调用
poll_wait
()函数, 将对应的等待队列头添加到poll_table;bool poll_wait (struct file * filp,wait_queue_head_t * wait_address,poll_table * wait_table);
Note: 这里的wait与wait_event意义是不同的, 这里不会引起阻塞.它的目的就是将当前进程添加到参数wait_table等待列表中而已.
返回表示能否进行无阻塞访问(读/写)的掩码.(bool是内核定义的0/1二值整形类型)
编程典型模板;
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(..condition..) //readable
{
mask |= POLLIN | POLLRDNORM; //标示数据可读取
...
}
if(...condition..) //writable
{
mask |= POLLOUT | POLLWRNORM; //标示数据可写入
}
return mask;
}
异步通知:
一旦设备就绪主动通知应用程序, 这样应用程序不需要查询设备状态,类似”中断”,即”信号驱动的异步I/O”.
异步通知的发出是 驱动主动 发出的.
编程实现:
应用层:
启动信号驱动机制signal(SIGIO, input_handler); //将信号SIGIO与处理函数链接起来 fcntl(STDIN_FILENO, F_SETOWN, getpid()); //把当前进程作为标准输入文件STDIN_FILENO的拥有者, 这样内核就知道把信号发送给哪个进程了 int oflags = fcntl(STDIN_FILENO, F_GETFL); fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC); //这两步启用异步通知机制,并对设备设置FASYNC标志.
综上, 为了在user应用空间中处理一个设备释放的信号,需要完成:
- 通过F_SETOWN这个IO控制命令设置设备文件的owner为本进程,这样设备驱动放出的信号才能被本进程接收到;
- 通过F_SETFL命令设置文件支持FASYNC, 异步通知模式;
- signal()函数连接信号和信号处理函数;
驱动层:(驱动主动释放信号)
为了使设备支持异步通知机制,驱动程序中需要完成:- 支持F_SETOWN, 能够在这个控制命令中处理中设置flip->f_owner为对应进程ID.(内核已完成,驱动无需处理);
- 支持F_SETFL命令(设置文件标志),每当文件的FASYNC改变时, 驱动程序中的fasync()函数将能够得到执行.因此驱动需要实现fasync()函数;
在设备资源可获得是,调用kill_fasync()函数激发相应的信号;
—-驱动中的这3项工作与应用程序中的3项工作是一一对应的.
定义异步通知的结构体
struct fasync_struct *async_queue;
在file_operation结构体声明具体函数
.fsync = xxx_fasync,
确定在什么地方发出信号,一边kernel接受驱动信号并给应用发信号.
kill_fasync()
使用场景:
使用不很很频繁的时候.
end