一、堵塞和非堵塞IO
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂
起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
1、堵塞访问基本流程
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将
CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完
成唤醒工作。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。
(1)创建并初始化等待队列头
等待队列头使用结构体wait_queue_head_t 表示,可以在设备结构体中进行定义
struct imx6uirq_dev{
dev_t devid;
struct cdev cdev;
.......wait_queue_head_t r_wait;
};
在驱动入口函数中使用init_waitqueue_head()函数进行初始化,参数为&等待队列头
init_waitqueue_head(&imx6uirq.r_wait);
(2)等待队列项
每个访问设备的进程都是一个队列项,使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
name 就是等待队列项的名字
tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current
(3)将队列项添加等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。
等待队列项添加 API 函数如下:
void add_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
q:等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
(4)等待唤醒
唤醒方式有两种,一种是主动唤醒:
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。
wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进
程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
相关代码如下
DECLARE_WAITQUEUE(wait, current); /* 定义一个等待队列 */
if(atomic_read(&dev->releasekey) == 0) { /* 没有按键按下 */
add_wait_queue(&dev->r_wait, &wait); /* 添加到等待队列头 */
__set_current_state(TASK_INTERRUPTIBLE); /* 设置为可被打断的状态 */
schedule(); /* 进行一次任务切换 */
if(signal_pending(current)) { /* 判断是否为信号引起的唤醒 */
ret = -ERESTARTSYS;
goto wait_error;
}
__set_current_state(TASK_RUNNING); /*设置为运行状态 */
remove_wait_queue(&dev->r_wait, &wait); /*将等待队列移除 */
}
其中signal_pending()函数用于判断当前进程是否有信号处理,返回值不为0的话表示有信号需要处理
另一种唤醒方式是事件唤醒,置等待队列等待某个事件,当这个事件满足以后就自动唤醒
等待队列中的进程。和等待事件有关的 API 函数:
wait_event_interruptible(wq, condition)
等待以 wq 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞
(5)移除等待队列头
当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,相关函数如下:
void remove_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
q:要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项。
同时还要设置任务为运行态
__set_current_state(TASK_RUNNING);
remove_wait_queue(&dev->r_wait, &wait);
2、非堵塞访问
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,
也就是轮询。poll、epoll 和 select 可以用于处理轮询
(具体代码后续完善)
二、异步通知
驱动程序可以通过向应用程序主动发送不同的信号来实现不同的功能,而不是应用程序向驱动循环询问,这就是异步通知。
1、驱动程序对异步通知的处理
(1)fasync_struct 结构体
首先我们需要在驱动程序中定义一个 fasync_struct 结构体指针变量, fasync_struct 结构体内
容如下:
struct fasync_struct {
spinlock_t fa_lock;
int magic;
int fa_fd;
struct fasync_struct *fa_next;
struct file *fa_file;
struct rcu_head fa_rcu;
};
一般将 fasync_struct 结构体指针变量定义到设备结构体中,具体如下
struct imx6uirq_dev{
......
struct fasync_struct *async_queue;
};
(2)fasync 函数
如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此
函数格式如下所示:
int (*fasync) (int fd, struct file *filp, int on)
fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体
指针,fasync_helper 函数原型如下:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化
的 fasync_struct 结构体指针变量。
对应的操作函数fasync 函数代码如下:
static int imx6uirq_fasync(int fd, struct file *filp, int on){
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
return fasync_helper(fd, filp, on, &dev->async_queue);
}
(3)kill_fasync 函数
当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。 kill_fasync
函数负责发送指定的信号,kill_fasync 函数原型如下所示:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
fp:要操作的 fasync_struct。
sig:要发送的信号。
band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT。
具体代码如下:
if(atomic_read(&dev->releasekey)) {
if(dev->async_queue) /* 一次完整的按键过程 */
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}
(4)释放操作函数
在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct
static int imx6uirq_release(struct inode *inode, struct file *filp){
return imx6uirq_fasync(-1, filp, 0);
}
2、应用程序对异步通知的处理
(1)注册信号处理函数
应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来
设置信号的处理函数。
signal(SIGIO, sigio_signal_func);
(2)开启异步通知
使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核并开启异步通知
int flags = 0;
fcntl(fd, F_SETOWN, getpid()); 将当前进程的进程号告诉给内核
flags = fcntl(fd, F_GETFD); 获取当前的进程状态
fcntl(fd, F_SETFL, flags | FASYNC); 设置进程启用异步通知功能
(3)编写signal函数
static void sigio_signal_func(int signum){
.......
}