目录
一、阻塞和非阻塞IO
二、应用程序阻塞与非阻塞方式
三、等待队列
1、等待队列头
2、等待队列项
3、队列项添加 / 移除等待队列头
4、等待唤醒
5、等待事件
四、轮询
1、select函数
2、poll函数
3、epoll函数
五、Linux驱动下的 poll操作函数
六、阻塞式IO程序编写示例
1、等待事件的方法
2、添加等待队列项的方式
七、非阻塞式IO程序编写示例
这里的IO指的是Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。 Linux内核提供了等待队列 (wait queue)来实现阻塞进程的唤醒工作
阻塞方式:
/*open*/
fd = open(filename, O_RDWR);
if (fd < 0){
printf("open %s failed!!\r\n", filename);
return -1;
}
ret = read(fd, &data, sizeof(data));
非阻塞方式:
/*open*/
fd = open(filename, O_RDWR | O_NONBLOCK);
if (fd < 0){
printf("open %s failed!!\r\n", filename);
return -1;
}
ret = read(fd, &data, sizeof(data));
在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue_head_t 表示, wait_queue_head_ t结构体定义在文件 include/linux/wait.h中,结构体内容如下所示:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
定义好等待队列头以后需要初始化, 使用 init_waitqueue_head 函数初始化等待队列头
void init_waitqueue_head(wait_queue_head_t *q)
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
name就是等待队列项的名字, tsk表示这个等待队列项属于哪个任务 (进程 ),一般设置 为current,在 Linux内核中 current相当于一个全局变量,表示当前进程。因此宏 DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可。
(1)添加等待队列项API:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
q: 等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
(2)移除等待队列项 API:
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)
参数q就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE状态的进程,而 wake_up_interruptible函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程,和等待事件有关的API函数如下:
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,可以被信号打断。 |
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。 poll、epoll和 select可以用于处理轮询,应用程序通过 select、epoll或 poll函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、epoll或 poll函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll函数。我们先来看一下应用程序中使用的 select、epoll或 poll函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
nfds: 所要监视的这三类文件描述集合中, 最大文件描述符加 1。
readfds、 writefds和 exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是 fd_set类型的, fd_set类型变量的每一个位都代表了一个文件描述符。 readfds用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect就会返回一个大于 0的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。 writefds和 readfs类似,只是 writefs用于监视这些文件是否可以进行写操作。 exceptfds用于监视这些文件的异常。
比如我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set变量,这个变量要传递给参数 readfds。当我们定义好一个 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)
在单个线程中,select函数能够监视的文件描述符 数量有最大的限制,一般为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll函数,poll函数本质上和 select没有太大的差别,但是 poll函数没有最大文件描述符限制
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
fds 要监视的文件描述符集合以及要监视的事件 ,为一个数组,数组元素都是结构体
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
events是要监视的事件,可监视的事件类型如下所示:
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
nfds: poll函数要监视的文件描述符数量。
timeout :超时时间,单位为 ms。
传统的 selcet和 poll函数都会随着所监听的 fd数量的增加,出现效率低下的问题,而且poll函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此, epoll应运而生epoll就是为处理大并发而准备的,一般常常在网络编程中使用 epoll函数。应用程序需要先使用 epoll_create函数创建一个 epoll句柄
int epoll_create(int size)
当应用程序调用select或 poll函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations操作集中的poll函数就会执行。所以驱动程序的编写者需要提供对应的 poll函数, poll函数原型如下所示:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
filp :要打开的设备文件 (文件描述符 )。
wait :结构体 poll_table_struct类型指针, 由 应用程序传 递进来的。一般将此参数传递给poll_wait函数。
https://github.com/denghengli/linux_driver/tree/master/15_blockio
#include
#include
/*定时器回调函数,在设备资源可用的时候唤醒等待队列中的线程*/
static void timer_func(unsigned long arg)
{
struct key_dev *dev = (struct key_dev*)arg;
...
wake_up(&dev->r_wait);/*唤醒等待队列中的线程*/
}
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
...
/*等待等待队列中的 reles_value 事件,使访问的线程进入阻塞*/
wait_event_interruptible(dev->r_wait, reles_value);
...
return ret;
}
static int key_open(struct inode *inode, struct file *filp)
{
...
/*初始化等待队列头*/
init_waitqueue_head(&dev->r_wait);
return 0;
}
#include
#include
/*定时器回调函数,在设备资源可用的时候唤醒等待队列中的线程*/
static void timer_func(unsigned long arg)
{
struct key_dev *dev = (struct key_dev*)arg;
...
wake_up(&dev->r_wait);/*唤醒等待队列中的线程*/
}
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
...
/*定义并初始化一个等待队列项*/
DECLARE_WAITQUEUE(wait, current);
if (reles_value == 0){
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); /*将等待队列移除 */
return -ERESTARTSYS;
}
/*可用设备唤醒*/
__set_current_state(TASK_RUNNING); /*设置为运行状态 */
remove_wait_queue(&dev->r_wait, &wait); /*将等待队列移除 */
}
...
return ret;
}
static int key_open(struct inode *inode, struct file *filp)
{
...
/*初始化等待队列头*/
init_waitqueue_head(&dev->r_wait);
return 0;
}
https://github.com/denghengli/linux_driver/tree/master/16_noblockio