前言
13. 阻塞与非阻塞
本章内容为驱动基石之一。
驱动只提供功能,不提供策略。
阻塞与非阻塞 都是应用程序主动访问的。从应用角度去解读阻塞与非阻塞。
原文:https://www.cnblogs.com/lizhuming/p/14912496.html
13.1 阻塞与非阻塞
阻塞:
- 指在执行设备操作时,若不能获得资源,则挂起进程,直至满足操作的条件后再继续执行。
非阻塞:
- 指在执行设备操作时,若不能获得资源,则不挂起,要么放弃,要么不停查询,直至设备可操作。
实现阻塞的常用技能包括:(目的其实就是阻塞)
- 休眠与唤醒机制(和等待队列相辅相成)。
- 等待队列(和休眠与唤醒机制相辅相成)。
- poll机制。
13.2 休眠与唤醒
若需要实现阻塞式访问,可以使用休眠与唤醒机制。
相关函数其实在 等待队列 小节有说明了,现在只是函数汇总。
13.2.1 内核休眠函数
内核源码路径:include\linux\wait.h。
13.2.2 内核唤醒函数
内核源码路径:include\linux\wait.h。
13.3 等待队列(阻塞)
等待队列:
- 其实就是内核的一个队列功能单位&API。
- 在驱动中,可以使用等待队列来实现阻塞进程的唤醒。
使用方法:
- 定义等待队列头部。
- 初始化等待队列头部。
- 定义等待队列元素。
- 添加/移除等待队列。
- 等待事件。
- 唤醒队列。
另外一种使用方法就是 在等待队列上睡眠。
等待队列头部结构体:
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
等待队列元素结构体:
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
13.3.1 定义等待队列头部
定义等待队列头部方法:wait_queue_head_t my_queue;
13.3.2 初始化等待队列头部
初始化等待队列头部源码:void init_waitqueue_head(wait_queue_head_t *q);
或
定义&初始化等待队列头部:使用宏 DECLARE_WAIT_QUEUE_HEAD。
13.3.3 定义等待队列元素
定义等待队列元素源码:#define DECLARE_WAITQUEUE(name, tsk);
- name:该等待队列元素的名字。
- tsk:该等待队列元素归属于哪个任务进程。
13.3.4 添加/移除等待队列元素
添加等待队列元素源码:void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);
- wq_head:等待队列头部。
- wq_entry:等待队列。
移除等待队列元素源码:void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);
- wq_head:等待队列头部。
- wq_entry:等待队列。
13.3.5 等待事件
睡眠,直至事件发生:wait_event(wq_head, condition)
- wq_head:等待队列头。
- condition:事件。当其为真时,跳出。
/**
* wait_event - sleep until a condition gets true
* @wq_head: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_UNINTERRUPTIBLE) until the
* @condition evaluates to true. The @condition is checked each time
* the waitqueue @wq_head is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*/
#define wait_event(wq_head, condition) \
do { \
might_sleep(); \
if (condition) \
break; \
__wait_event(wq_head, condition); \
} while (0)
- TASK_INTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接等等),可被信号中断唤醒。可被 信号 和 wake_up() 唤醒。
- TASK_UNINTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接等等),但会忽略信号、不可以被中断唤醒。即是只能由 wake_up() 唤醒。
睡眠,直至事件发生或超时:wait_event_timeout(wq_head, condition, timeout)
等待事件发生,且可被信号中断唤醒:wait_event_interruptible(wq_head, condition)
等待事件发生或超时,且可被信号中断唤醒:wait_event_interruptible_timeout(wq_head, condition, timeout)
io_wait_event():
/*
* io_wait_event() -- like wait_event() but with io_schedule()
*/
#define io_wait_event(wq_head, condition) \
do { \
might_sleep(); \
if (condition) \
break; \
__io_wait_event(wq_head, condition); \
} while (0)
13.3.6 唤醒队列
以下两个函数对应等待事件使用:
- 唤醒队列:
void wake_up(wait_queue_head_t *queue);
- 唤醒队列,信号中断可唤醒:
void wake_up_interruptible(wait_queue_head_t *queue);
13.3.7 在等待队列上睡眠
函数源码:
sleep_on(wait_queue_head_t *q)
interruptible_sleep_on(wait_queue_head_t *q)
- sleep_on():
- 把当前进程状态设置为 TASK_INTERRUPTIBLE,并定义一个等待队列元素,并添加到 q 中。
- 直到资源可用或 q 队列指向链接的进程被唤醒。
- 与 wake_up() 配套使用。interruptible_sleep_on() 与 wake_up_interruptible() 配套使用。
13.4 轮询
当用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式。
poll、epoll 和 select 可以用于处理轮询。这三个 API 均在 应用层 使用。
注意,轮询也是在APP实现轮询的。
13.4.1 select 函数
select():
- 函数原型:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- numfds:需要检查的 fd 中最大的 fd + 1。
- readfds:读 文件描述符集合。NULL 不关心这个。
- writefds:写 文件描述符集合。NULL 不关心这个。
- exceptfds:异常 文件描述符集合。NULL 不关心这个。
- timeout:超时时间。NULL 时为无限等待。
- 时间结构体:
struct timeval{
long tv_sec; // 秒
long tv_usec; // 微妙
};
- 返回:
- 0:超时。
- -1:错误。
- 其他值:可进行操作的文件描述符个数。
- 原理:fd_set 为一个 N 字节类型,需要操作的 fd 值在对应比特上置为 1 即可。若 fd 的值为 6,需要检查读操作,则把 readfds 第 6 个 bit 置 1。调用该函数后,先把对应 fd_set 清空,再检查、标记可操作情况。Linux 提供以下接口操作:
FD_CLR(int fd, fd_set *set); // 把 fd 对应的 set bit 清空。
FD_ISSET(int fd, fd_set *set); // 查看 d 对应的 set bit 是否被置 **1**。
FD_SET(int fd, fd_set *set); // 把 fd 对应的 set bit 置 **1**。
FD_ZERO(fd_set *set); // 把 set 全部清空。
fd_set 是有限制的,可以查看源码,修改也可。但是改大会影响系统效率。
13.4.2 poll 函数
由于 fd_set 是有限制的,所以当需要监测大量文件时,便不可用。
这时候,poll() 函数就应运而生。
poll() 和 select() 没什么区别,只是前者没有最大文件描述符限制。
- 函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
- fds:要监视的文件描述符集合。
- nfds:要监视的文件描述符数量。
- timeout:超时时间。单位 ms。
- 返回:
- 0:超时。
- -1:发生错误,并设置 error 为错误类型。
- 其它:返回 revent 域值不为 0 的 pollfd 个数。即是发生事件或错误的文件描述符数量。
被监视的文件描述符格式:
struct pollfd{
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的时间 */
}
可请求的事件 events:
13.4.3 epoll 函数
select() 和 poll() 会随着监测的 fd 数量增加,而出现效率低下的问题。
poll() 每次监测都需要历遍所有被监测的描述符。
epoll() 函数就是为大量并大而生的。在网络编程中比较常见。
epoll() 使用方法:
- 创建一个 epoll 句柄:
- 函数原型:
int epoll_creat(int size);
- size:随便大于 0 即可。 Linux2.6.8 后便不再维护了。
- 返回:
- epoll 句柄。
- -1:创建失败。
- 函数原型:
- 向 epoll 添加要监视的文件及监测的事件。
- 函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
。 - epfd:epoll 句柄。
- op:操作标识。
- EPOLL_CTL_ADD:向 epfd 添加 fd 表示的描述符。
- EPOLL_CTL_MOD:修改 fd 的 event 时间。
- EPOLL_CTL_DEL:从 epfd 中删除 fd 描述符。
- fd:要监测的文件。
- event:要监测的事件类型。
- 函数原型:
- 等待事件发生。
- 函数原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
。 - epfd:epoll 句柄。
- events:指向 epoll_event 结构体数组。
- maxevents:events 数组大小,必须大于 0。
- timeout:超时时间。
- 函数原型:
epoll_event 结构体:
struct epoll_event{
uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户数据 */
}
可请求的事件 events:
13.5 驱动中的 poll 函数
当应用程序调用 select() 函数和 poll() 函数时,驱动程序会调用 file_operations 中的 poll。
- 函数原型:
unsigned int(*poll)(struct file *filp, struct poll_table_struct *wait)
- file:file 结构体。
- wait:轮询表指针。主要传给 poll_wait 函数。
- 该函数主要工作:
- 对可能引起设备文件状态变化的等待队列调用 poll_wait() 函数,将对应的等待队列头部添加到 poll_table 中。
- 返回表示是否能对设备进行无阻塞读、写访问的掩码。可以返回以下值:
- POLLIN:有数据可读。
- POLLPRI:有紧急的数据需要读取。
- POLLOUT:可以写数据。
- POLLERR:指定的文件描述符发生错误。
- POLLHUP:指定的文件描述符挂起。
- POLLNVAL:无效的请求。
- POLLRDNORM:等同于 POLLIN,普通数据可读。
poll_wait()
- 函数原型:
void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
- 该函数不会阻塞进程,只是将当前进程添加到 wait 参数指定的等待列表中。
- filp:要操作的设备文件描述符。
- wait_address:要添加到 wait 轮询表中的等待队列头。
- p:file_operations 中 poll 的 wait 参数。
- 建议:找个例程看看就明白了。