一、阻塞访问和非阻塞访问的定义及区别
在实际开发过程中,可能遇到对同一个设备驱动进行多次访问的情况,根据访问方式的不同,可以分为阻塞和非阻塞访问,这两种访问的区别体现在设备繁忙时,对于当前进程的处理方式不同。
阻塞访问:
当执行设备操作时,无法获得资源,则将该进程挂起,等到获得可以执行设备操作资源的时候再执行,等待期间处于休眠状态。一般通过等待队列来实现处理的先后顺序。
非阻塞访问:
当执行设备操作时,无法获得资源,但不会将进程挂起,而是不断轮询直到可以操作为止。其中read,write,open等文件操作会收到非阻塞访问的影响。
二、基本概念和函数的介绍
1、等待队列
等待队列是阻塞访问实现唤醒进程的手段。
a、 定义
等待队列由等待队列头和等待队列项构成,他们之间由双向循环列表连接,用一个list_head类型的域作为连接件。
struct __wait_queue_head {
spinlock_t lock; /* 保护等待队列的原子锁 (自旋锁),在对task_list与操作的过程中,使用该锁实现对等待队列的互斥访问*/
struct list_head task_list; /* 等待队列,双向循环链表,存放等待的进程 */
};
/*__wait_queue,该结构是对一个等待任务的抽象。每个等待任务都会抽象成一个wait_queue,并且挂载到wait_queue_head上。该结构定义如下:*/
struct __wait_queue {
unsigned int flags;#define WQ_FLAG_EXCLUSIVE 0x01
void *private; /* 通常指向当前任务控制块 */
wait_queue_func_t func;
struct list_head task_list; /* 挂入wait_queue_head的挂载点 */
};typedef struct __wait_queue wait_queue_t;
/* 任务唤醒操作方法,该方法在内核中提供,通常为auto remove_wake_function */
b、操作
使用等待队列时,通常在设备结构体里面定义一个等待队列头。
定义等待队列头:
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
也可以使用宏定义将两步合成一步:
DECLARE_WAIT_QUEUE_HEAD (my_queue);
定义等待队列项:
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可
用的时候就要将这些进程对应的等待队列项添加到等待队列里面。
DECLARE_WAITQUEUE(name,tsk); //该等待队列名为name,private为tsk
name 就是等待队列项的名字,tsk 表示这个等待队列项属于哪个任务(进程),一般设置为
current , 在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏
DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。
从等待队列头里添加/删除等待队列
当进程不可访问时,将其添加进等待队列
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)
q :等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
等待唤醒
当设备可以使用时,可以主动唤醒处于休眠期的进程
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
等待事件
可以设置当满足某个事件的时候,自主唤醒进程
函数 | 描述 |
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,可以被信号打断。 |
2、轮询
a、定义
轮询用于非阻塞访问。分为select、poll、epoll。
select
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout)
fd_set 类型变量的每一个位都代表了一个文件描述符
readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。
writefds 和 readfs 类似,只是 writefs 用于监视这些文件是否可以进行写操作。
exceptfds 用于监视这些文件的异常。
可以使用下面的宏对fd_set类型的变量进行操作
void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)
FD_ZERO 用于将 fd_set 变量的所有位都清零,FD_SET 用于将 fd_set 变量的某个位置 1,
也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符。FD_CLR 用户将 fd_set
变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符。FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符。
poll
相比于select,poll函数可以监视的文件描述符数量没有限制。
int poll(struct pollfd *fds,nfds_t nfds,int timeout)
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,并且 revents
返回 0。events 是要监视的事件,可监视的事件类型如下所示:
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
epoll
epoll一般用于网络编程,这里就不介绍了。
三、使用方法
1、使用等待队列实现阻塞访问
fd = open(filename, O_RDWR); //应用程序使用阻塞的方式打开文件
①在设备结构体定义等待队列头
struct imx6uirq_dev{
.............
wait_queue_head_t r_wait;
}
②初始化等待队列头
init_waitqueue_head(&imx6uirq.r_wait);
前两步可以参考前面的宏定义方法,一步完成
③定义一个等待队列并实现满足条件时移除
DECLARE_WAITQUEUE(wait, current); /* 定义一个等待队列 */
add_wait_queue(&dev->r_wait, &wait); /* 添加到等待队列头 */
__set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */
schedule(); /* 进行一次任务切换 */
if(signal_pending(current)) { /* 判断是否为信号引起的唤醒 */
.........................
__set_current_state(TASK_RUNNING); /* 设置为运行状态 */
remove_wait_queue(&dev->r_wait, &wait); /* 将等待队列移除 */
也可以使用等待事件和主动唤醒
ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
wake_up_interruptible(&dev->r_wait);
2、非阻塞访问打开文件
fd = open("dev_xxx", O_RDWR | O_NONBLOCK);
在驱动中判断为非阻塞访问的方法:
if (filp->f_flags & O_NONBLOCK)
3、select的使用
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);
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;
4、poll的使用
fd = open(filename, O_RDWR | O_NONBLOCK);
fds.fd = fd;
fds.events = POLLIN;
ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
if (ret) { /* 数据有效 */
/* 读取数据 */
} else if (ret == 0) { /* 超时 */
......
} else if (ret < 0) { /* 错误 */
......
}
}
5、设备驱动中的poll函数
在应用程序中,无论调用select还是poll,都会出发设备驱动中的poll函数。设备中的poll函数定义在file operations操作函数集合里面,我们需要对其进行定义。
static struct file_operations imx6uirq_fops = {
.poll = imx6uirq_poll,
}
//函数原型
unsigned int (*poll) (struct file *, struct poll_table_struct *);
其在file_operations结构体中。参数file是file结构体指针,第二个参数是轮询表指针 。
在poll函数中,主要调用poll_wait函数,将对应的等待队列头添加到poll_table。最后返回表示是否能对设备进行无阻塞读、写访问的掩码。
例子:
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
poll_wait(filp, &dev->r_wait, wait); /* 将等待队列头添加到poll_table中 */
if(atomic_read(&dev->releasekey)) { /* 按键按下 */
mask = POLLIN | POLLRDNORM; /* 返回PLLIN */
}
return mask;
}
参考链接:linux中的阻塞机制及等待队列 - touchcode - 博客园
Linux设备驱动开发-linux驱动中的非阻塞访问方式_浩瀚之水的专栏-CSDN博客
正点原子I.MX6U 嵌入式 Linux 驱动开发指南