Linux驱动开发--阻塞、非阻塞I/O
2. 阻塞、非阻塞I/OIO 指的是 Input/Output,也就是输入/输出,是应用步调对驱动设备的输入/输出操作。当应用步调对设备驱动举行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用步调对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用步调对应的线程不会挂起,它要么一直轮询等候,直到设备资源可以利用,要么就直接放弃。
在阻塞访问时,不能获取资源的历程将进入休眠,它将CPU资源“礼让”给其他历程。因为阻塞的历程会进入休眠状态,所以必须确保有一个地方能够叫醒休眠的历程,否则,历程就真的“寿终正寝”了。叫醒历程的地方最大可能发生在中断里面,因为在硬件资源获得的同时往往陪伴着一个中断。而非阻塞的历程则不断实行,直到可以举行I/O.
https://i-blog.csdnimg.cn/direct/61feaf96221a42b08ab846d1684163e7.png
对于设备驱动文件的默认读取方式就是阻塞式的,
// 代码清单 8.1 阻塞地读串口一个字符
char buf;
fd = open("/dev/ttyS1", O_RDWR);
...
res = read(fd, &buf, 1);/* 当串口上有输入时才返回 */
if(res == 1)
printf("%c\n", buf);
// 代码清单 8.2 非阻塞地读串口一个字符
char buf;
fd = open("/dev/ttyS1", O_RDWR | O_NONBLOCK);
...
while(read(fd, &buf, 1) != 1)
continue;/* 串口上无输入也返回,因此要循环尝试读取串口 */
printf("%c\n", buf);
除了在打开文件时可以指定阻塞还是非阻塞方式以外,在文件打开后,也可以通过ioctl()和fcntl()改变读写的方式,如从阻塞变更为非阻塞或者从非阻塞变更为阻塞。例如,调用 fcntl(fd.F_SETFL,O_NONBLOCK)可以设置 fd 对应的 I/0 为非阻塞。
2.1 阻塞–等候队列(休眠、叫醒)
在 Linux 驱动步调中,可以利用等候队列(Wait Queue)来实现阻塞历程的叫醒。等候队列很早就作为一个基本的功能单位出现在 Linux 内核里了,它以队列为基础数据结构,与历程调度机制精密结合,可以用来同步对体系资源的访问,第 7 章中所讲述的信号量在内核中也依赖等候队列来实现。
Linux 内核提供了如下关于等候队列的操作:
①定义并初始化“等候队列头部”
等候队列头利用结构体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 函数初始化等候队列头,函数原型如下:
extern void __init_waitqueue_head(wait_queue_head_t *q, const char *name, struct lock_class_key *);
#define init_waitqueue_head(q) \
do { \
static struct lock_class_key __key; \
\
__init_waitqueue_head((q), #q, &__key); \
} while (0)
//参数 q 就是要初始化的等待队列头。
也可以利用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等候队列头的定义的初始化。
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.task_list = { &(name).task_list, &(name).task_list } }
②定义等候队列元素
等候队列头就是一个等候队列的头部,每个访问设备的历程都是一个队列项,当设备不可用的时候就要将这些历程对应的等候队列项添加到等候队列里面。结构体 wait_queue_t 表示等候队列项,结构体内容如下:
typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
利用宏 DECLARE_WAITQUEUE(name, tsk) 定义并初始化一个等候队列项,宏的内容如下: name就是等候队列项的名字, tsk 表示这个等候队列项属于哪个任务(历程),一样寻常设置为current , 在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏DECLARE_WAITQUEUE 就是给当前正在运行的历程创建并初始化了一个等候队列项。
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.task_list = { NULL, NULL } }
③添加/移除等候队列
当设备不可访问的时候就须要将历程对应的等候队列项添加到前面创建的等候队列头中,只有添加到等候队列头中以后历程才气进入休眠态。当设备可以访问以后再将历程对应的等候队列项从等候队列头中移除即可 。
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);
add_wait_queue() 用于将等候队列元素 wait 添加到等候队列头部 q 指向的双向链表中,而 remove_wait_queue() 用于将等候队列元素 wait 从由 q 头部指向的链表中移除。下面简朴看一下添加过程:
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
//头插法
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}
④叫醒队列
https://i-blog.csdnimg.cn/direct/6c7c8252edf4494db5b9913a6f719bd4.png
上述操作会叫醒以 queue 作为等候队列头部的队列中所有的历程。(但是现在应该改成上表中的状态了!!!)
[*] wake_up() 应该与 wait_event() 或 wait_event_timeout() 成对利用;
[*] wake_up_interruptible() 则应与 wait_event_interruptible() 或 wait_event_interruptible_timeout() 成对利用。
[*] wake_up() 可叫醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 的历程,而 wake_up_interruptible() 只能叫醒处于 TASK_INTERRUPTIBLE 的历程。
⑤等候事件
除了自动叫醒以外,也可以设置等候队列等候某个事件,当这个事件满意以后就自动叫醒等候队列中的历程,和等候事件有关的 API 函数如下 所示:
https://i-blog.csdnimg.cn/direct/f18e6036cd1e4b3d9ae9dbc2f5b5d1f9.png
等候第 1 个参数 queue 作为等候队列头部的队列被叫醒,而且第 2 个参数 condition 必须满意,否则继承阻塞。wait_event() 和 wait_event_interruptible() 的区别在于后者可以被信号打断,而前者不能。加上 _timeout 后的宏意味着阻塞等候的超时时间,以 jiffies 为单位,在第 3 个参数的 timeout 到达时,不论 condition 是否满意,均返回。
Linux 内核利用全局变量 jiffies 来记录体系从启动以来的体系节拍数,体系启动的时候会将 jiffies 初始化为 0;在编译 Linux 内核的时候可以通过图形化界面设置体系节拍率 。比如 1000Hz, 100Hz 等等 。
⑦在等候队列上睡眠
sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);
sleep_on() 函数的作用就是将现在历程的状态置成 TASK_UNINTERRUPTIBLE,并定义一个等候队列元素,之后把它挂到等候队列头部 q 指向的双向链表,直到资源可获得,q 队列指向链接的历程被叫醒。
interruptible_sleep_on() 与 sleep_on() 函数类似,其作用是将现在历程的状态置成 TASK_INTERRUPTIBLE,并定义一个等候队列元素,之后把它附属到 q 指向的队列,直到资源可获得(q 指引的等候队列被叫醒)或者历程收到信号。
sleep_on() 函数应该与 wake_up() 成对利用,interruptible_sleep_on() 应该与 wake_up_interruptible() 成对利用。
再设备驱动中利用等候队列–模板:
static size_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos)
{
...
DECLARE_WAITQUEUE(wait, current);
/* 添加元素到等待队列 */
add_wait_queue(&xxx_wait, &wait);
/* 等待设备缓冲区可写 */
do {
avail = device_writable(...);
if (avail < 0) {
if (file->f_flags & O_NONBLOCK) { /* 非阻塞 */
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态 */
schedule(); /* 调度其他进程执行 */
if (signal_pending(current)) { /* 如果是因为信号唤醒 */
ret = -ERESTARTSYS;
goto out;
}
}
} while (avail < 0);
/* 写设备缓冲区 */
device_write(...)
out:
remove_wait_queue(&xxx_wait, &wait); /* 将元素移出 xxx_wait 指引的队列 */
set_current_state(TASK_RUNNING); /* 设置进程状态为 TASK_RUNNING */
return ret;
}
1)如果是非阻塞访问(O_NONBLOCK 被设置),设备忙时,直接返回“-EAGAIN”。
2)对于阻塞访问,会调用 __set_current_state(TASK_INTERRUPTIBLE) 举行历程状态切
换并体现通过“schedule()”调度其他历程实行。
3)醒来的时候要注意,由于调度出去的时候,历程状态是 TASK_INTERRUPTIBLE,
即浅度睡眠,所以叫醒它的有可能是信号,因此,我们起首通过 signal_pending(current) 了解
是不是信号叫醒的,如果是,立即返回“-ERESTARTSYS”。
DECLARE_WAITQUEUE、add_wait_queue 这两个动作加起来完成的效果如图 8.2 所示。在 wait_queue_head_t 指向的链表上,新定义的 wait_queue 元素被插入,而这个新插入的元素绑定了一个 task_struct(当前做 xxx_write 的 current,这也是DECLARE_WAITQUEUE 利用 “current” 作为参数的缘故起因)。
内核定义了 task_struct 结构体表示一个历程.
https://i-blog.csdnimg.cn/direct/90b84025df684b2b8dc73807840dde05.png
2.2 实行示例
2.3 非阻塞–轮询(POLL、Select机制)
如果用户应用步调以非阻塞的方式访问设备,设备驱动步调就要提供非阻塞的处置惩罚方式,也就是轮询。 poll、 epoll 和 select 可以用于处置惩罚轮询,应用步调通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用步调调用 select、 epoll 或 poll 函数的时候设备驱动步调中的 poll 函数就会实行,因此须要在设备驱动步调中编写 poll 函数。
2.3.1 select 函数
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);
此中 readfds、writefds、exceptfds 分别是被 select() 监视的读、写和非常处置惩罚的文件描述符集合,numfds 的值是须要检查的号码最高的 fd 加 1。readfds 文件集中的任何一个文件变得可读,select() 返回;同理,writefds 文件集中的任何一个文件变得可写,select 也返回。
如图 8.3 所示,第一次对 n 个文件举行 select() 的时候,若任何一个文件满意要求,select() 就直接返回;第 2 次再举行 select() 的时候,没有文件满意读写要求,select() 的历程阻塞且睡眠。由于调用 select() 的时候,每个驱动的 poll() 接口都会被调用到,现实上实行select() 的历程被挂到了每个驱动的等候队列上,可以被任何一个驱动叫醒。如果 FD 变得可读写,select() 返回。
https://i-blog.csdnimg.cn/direct/53fed3dd93b54a0bb63bd88bcc3187bf.png
imeout:超时时间,当我们调用 select 函数等候某些文件描述符可以设置超时时间,超时时间利用结构体 timeval 表示,结构体定义如下所示:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
}
当 timeout 为 NULL 的时候就表示无穷期的等候。 此时就是编程阻塞IO了!
返回值: 0,表示的话就表示超时发生,但是没有任何文件描述符可以举行操作; -1,发生错误;其他值,可以举行操作的文件描述符个数。
比如我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set 变量,这个变量要传递给参数 readfds。当我们定义好一个 fd_set 变量以后可以利用如下所示几个宏举行操作,下列操作用来设置、扫除、判断文件描述符集合:
操作函数原型描述扫除文件描述符集合FD_ZERO(fd_set *set)扫除一个文件描述符集合添加文件描述符FD_SET(int fd, fd_set *set)将一个文件描述符参加文件描述符集合中扫除文件描述符FD_CLR(int fd, fd_set *set)将一个文件描述符从文件描述符集合中扫除判断文件描述符FD_ISSET(int fd, fd_set *set)判断文件描述符是否被置位 利用 select 函数对某个设备驱动文件举行读非阻塞访问的操作示例如下所示:
void main(void)
{
int ret, fd;
fd_set readfds;
struct timeval timeout;
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 函数读取数据 */
.
.
/* 使用 read 函数读取数据 */
}
break;
}
}
引出下面POLL,在单个线程中, select 函数能够监视的文件描述符数量有最大的限定,一样寻常为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低服从!
#undef __FD_SETSIZE
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits;
} __kernel_fd_set;
2.3.2 POLL函数
在单个线程中, select 函数能够监视的文件描述符数量有最大的限定,一样寻常为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低服从!这个时候就可以利用 poll 函数, poll 函数本质上和 select 没有太大的差异,但是 poll 函数没有最大文件描述符限定, Linux 应用步调中 poll 函数原型如下所示:
poll() 的功能和实现原理与 select() 相似,其函数原型为:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
[*]fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd范例的, pollfd 结构体如下所示:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
[*]fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,而且 revents返回 0。 events 是要监视的事件,可监视的事件范例如下所示:
事件范例描述POLLIN有数据可以读取。POLLPRI有紧急的数据须要读取。POLLOUT可以写数据。POLLERR指定的文件描述符发生错误。POLLHUP指定的文件描述符挂起。POLLNVAL无效的请求。POLLRDNORM等同于 POLLIN。
[*] revents 是返回参数,也就是返回的事件, 由 Linux 内核设置具体的返回事件。
[*] nfds: poll 函数要监视的文件描述符数量。
[*] timeout: 超时时间,单位为 ms。
[*] 返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量; 0,超时; -1,发生错误,而且设置 errno 为错误范例。
利用 poll 函数对某个设备驱动文件举行读非阻塞访问的操作示例如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
void main(void)
{
int ret;
int fd1, fd2;
struct pollfd fds; // 用于存储两个设备文件的轮询信息
// 打开两个设备文件,设置为非阻塞模式
fd1 = open("device1", O_RDWR | O_NONBLOCK);
if (fd1 < 0) {
perror("open device1 failed");
exit(EXIT_FAILURE);
}
fd2 = open("device2", O_RDWR | O_NONBLOCK);
if (fd2 < 0) {
perror("open device2 failed");
close(fd1); // 关闭已打开的文件描述符
exit(EXIT_FAILURE);
}
// 构造轮询结构体
fds.fd = fd1;
fds.events = POLLIN; // 监视设备1是否可读
fds.fd = fd2;
fds.events = POLLIN; // 监视设备2是否可读
// 轮询两个设备文件,超时时间设置为1000ms
ret = poll(fds, 2, 1000);
if (ret > 0) {
// 检查设备1是否可读
if (fds.revents & POLLIN) {
printf("Device1 is readable\n");
// 在这里可以对设备1进行读操作
// 示例代码省略读操作部分
}
// 检查设备2是否可读
if (fds.revents & POLLIN) {
printf("Device2 is readable\n");
// 在这里可以对设备2进行读操作
// 示例代码省略读操作部分
}
} else if (ret == 0) {
printf("Poll timeout\n");
} else {
perror("poll failed");
}
// 关闭文件描述符
close(fd1);
close(fd2);
}
2.3.3 epoll函数
当多路复用的文件数量巨大、I/O 流量频繁的时候,一样寻常不太适合利用 select() 和 poll(),此种情况下,select() 和 poll() 的性能体现较差,我们宜利用 epoll。epoll 的最大好处是会随着 fd 的数量增长而降低服从,select() 则会随着 fd 的数量增大性能降落显着。
与 epoll 相干的用户空间编程接口包罗:
int epoll_create(int size);
epoll 就是为处置惩罚大并发而准备的,一样寻常常常在网络编程中利用 epoll 函数。
2.3.4 Linux 驱动下的 poll 操作函数
当应用步调调用 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 函数。
[*]返回值:返回设备资源的可获取状态,即 POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL 等宏的位“或”效果。每个宏的寄义都表明设备的一种状态,如POLLIN(定义为 0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为 0x0004)意味着设
备可以无阻塞地写。可以返回的资源状态如下:
事件范例描述POLLIN有数据可以读取。POLLPRI有紧急的数据须要读取。POLLOUT可以写数据。POLLERR指定的文件描述符发生错误。POLLHUP指定的文件描述符挂起。POLLNVAL无效的请求。POLLRDNORM等同于 POLLIN,平凡数据可读 这个函数应该举行两项工作:
1)对可能引起设备文件状态变化的等候队列调用 poll_wait() 函数,将对应的等候队列头部添加到 poll_table 中。
2)返回表示是否能对设备举行无阻塞读、写访问的掩码。
https://i-blog.csdnimg.cn/direct/c15b0680ef384997acd74aa24d4b13d9.png
这里肯定注意我们的视角从前面的应用端APP,转换到了一个具体的设备驱动。
用于向 poll_table 注册等候队列的关键 poll_wait() 函数的原型如下:
void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table * wait);
poll_wait() 函数的名称非常轻易让人产生误会,以为它和 wait_event() 等一样,会阻塞地等候某事件的发生,实在这个函数并不会引起阻塞。poll_wait 函数不会引起阻塞, poll_wait() 函数所做的工作是把当前历程--应用步调添加到 wait 参数指定的等候列表(poll_table)中,现实作用是让叫醒参数 queue 对应的等候队列可以叫醒因 select() 而睡眠的历程。
struct poll_table_struct;
/*
* structures and helpers for f_op->poll implementations
*/
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
/*
* Do not touch the structure directly, use the access functions
* poll_does_not_wait() and poll_requested_events() instead.
*/
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
//poll_table 结构用于管理 poll 系统调用的等待队列。
这里具体看一下这里的逻辑:
poll_table 是一个用于管理多个设备等候队列的结构。它的主要作用是将多个设备的等候队列头(wait_queue_head_t)集中管理起来,以便在调用 poll 函数时能够高效地处置惩罚多个设备的等候队列。
关于 poll_table 的管理机制
[*]当应用步调调用 poll 或 select 体系调用时,内核会调用设备驱动步调中定义的 poll 函数。unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);此中,wait 是一个指向 poll_table_struct 的指针,它是一个内核管理的结构,用于保存当前设备的等候队列头。
[*]在 poll 函数中,驱动步调须要利用 poll_wait 宏来将当前设备的等候队列头(wait_queue_head_t)添加到 poll_table 中。
[*]poll_table 是动态管理的。当一个设备的等候队列头被添加到 poll_table 后,内核会负责维护这些等候队列头。如果设备有数据可读或可写,内核会叫醒对应的等候队列中的任务。
poll_table 是由内核管理的,它用于集中管理多个设备的等候队列头。
[*]每个设备驱动步调通常会定义自己的等候队列头(wait_queue_head_t)。例如,在一个 LED 驱动中,可能会定义一个等候队列头,用于等候 LED 状态变化的通知。而在一个按键驱动中,也会定义一个等候队列头,用于等候按键事件。
[*]当应用步调调用 poll 函数时,内核会创建一个 poll_table,并将所有相干设备的等候队列头添加到这个 poll_table 中。这样,内核可以同一管理这些等候队列头,而不须要每个驱动步调单独管理。
通过以上分析,可得出设备驱动中poll函数的典范模板:
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 (data_available)
mask |= POLLIN | POLLRDNORM; /* 可读 */
/* 标示数据可获得(对用户可读)*/
if (...)
mask |= POLLOUT | POLLWRNORM; /* 可写 */
/* 标示数据可写入 */
...
return mask;
}
2.4 实行示例
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]