【linux】驱动-13-阻塞与非阻塞


前言

13. 阻塞与非阻塞

本章内容为驱动基石之一
驱动只提供功能,不提供策略

阻塞与非阻塞 都是应用程序主动访问的。从应用角度去解读阻塞与非阻塞。

原文:https://www.cnblogs.com/lizhuming/p/14912496.html

13.1 阻塞与非阻塞

阻塞

  • 指在执行设备操作时,若不能获得资源,则挂起进程,直至满足操作的条件后再继续执行。

非阻塞

  • 指在执行设备操作时,若不能获得资源,则不挂起,要么放弃,要么不停查询,直至设备可操作。

实现阻塞的常用技能包括:(目的其实就是阻塞)

  • 休眠与唤醒机制和等待队列相辅相成)。
  • 等待队列和休眠与唤醒机制相辅相成)。
  • poll机制

13.2 休眠与唤醒

若需要实现阻塞式访问,可以使用休眠与唤醒机制。

相关函数其实在 等待队列 小节有说明了,现在只是函数汇总。

13.2.1 内核休眠函数

内核源码路径:include\linux\wait.h。

函数名 描述
wait_event(wq, condition) 休眠,直至 condition 为真;休眠期间不能被打断。
wait_event_interruptible(wq, condition) 休眠,直至 condition 为真;休眠期间可被打断,包括信号。
wait_event_timeout(wq, condition, timeout) 休眠,直至 condition 为真或超时;休眠期间不能被打断。
wait_event_interruptible_timeout(wq, condition, timeout) 休眠,直至 condition 为真或超时;休眠期间可被打断,包括信号。

13.2.2 内核唤醒函数

内核源码路径:include\linux\wait.h。

函数名 描述
wake_up_interruptible(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,只唤醒其中的一个线程
wake_up_interruptible_nr(x, nr) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,只唤醒其中的 nr 个线程
wake_up_interruptible_all(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,唤醒其中的所有线程
wake_up(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中的一个线程
wake_up_nr(x, nr) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中 nr 个线程
wake_up_all(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,唤醒其中的所有线程

13.3 等待队列(阻塞)

等待队列

  • 其实就是内核的一个队列功能单位&API。
  • 在驱动中,可以使用等待队列来实现阻塞进程的唤醒。

使用方法

  1. 定义等待队列头部。
  2. 初始化等待队列头部。
  3. 定义等待队列元素。
  4. 添加/移除等待队列。
  5. 等待事件。
  6. 唤醒队列。

另外一种使用方法就是 在等待队列上睡眠

等待队列头部结构体

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 轮询

当用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式。
pollepollselect 可以用于处理轮询。这三个 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,需要检查读操作,则把 readfds6 个 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 域值不为 0pollfd 个数。即是发生事件或错误的文件描述符数量。

被监视的文件描述符格式

struct pollfd{
    int fd; /* 文件描述符 */
    short events; /* 请求的事件 */
    short revents; /* 返回的时间 */
}

可请求的事件 events

说明
POLLIN 有数据可读
POLLPRI 有紧急的数据需要读取
POLLOUT 可以写数据
POLLERR 指定的文件描述符发生错误
POLLHUP 指定的文件描述符被挂起
POLLNVAL 无效的请求
POLLRDNORM 等同于 POLLIN

13.4.3 epoll 函数

select()poll() 会随着监测的 fd 数量增加,而出现效率低下的问题。
poll() 每次监测都需要历遍所有被监测的描述符。

epoll() 函数就是为大量并大而生的。在网络编程中比较常见。

epoll() 使用方法:

  1. 创建一个 epoll 句柄:
    • 函数原型:int epoll_creat(int size);
    • size:随便大于 0 即可。 Linux2.6.8 后便不再维护了。
    • 返回
      • epoll 句柄。
      • -1:创建失败。
  2. epoll 添加要监视的文件及监测的事件。
    • 函数原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    • epfdepoll 句柄。
    • op:操作标识。
      • EPOLL_CTL_ADD:向 epfd 添加 fd 表示的描述符。
      • EPOLL_CTL_MOD:修改 fdevent 时间。
      • EPOLL_CTL_DEL:从 epfd 中删除 fd 描述符。
    • fd:要监测的文件。
    • event:要监测的事件类型。
  3. 等待事件发生。
    • 函数原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    • epfdepoll 句柄。
    • events:指向 epoll_event 结构体数组。
    • maxeventsevents 数组大小,必须大于 0。
    • timeout:超时时间。

epoll_event 结构体

struct epoll_event{
    uint32_t events; /* epoll 事件 */
    epoll_data_t data; /* 用户数据 */
}

可请求的事件 events

说明
EPOLLIN 有数据可读
EPOLLPRI 有紧急的数据需要读取
EPOLLOUT 可以写数据
EPOLLERR 指定的文件描述符发生错误
EPOLLHUP 指定的文件描述符被挂起
EPOLLET 设置 epoll 为边沿触发,默认触发模式为水平触发
EPOLLONESHOT 一次性的监视,当监视完成后,还需要监视某个 fd,那就需要把 fd 重新添加到 epoll

13.5 驱动中的 poll 函数

当应用程序调用 select() 函数和 poll() 函数时,驱动程序会调用 file_operations 中的 poll

  • 函数原型:unsigned int(*poll)(struct file *filp, struct poll_table_struct *wait)
  • filefile 结构体。
  • 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 参数。
  • 建议:找个例程看看就明白了。
posted @ 2021-06-21 12:37  李柱明  阅读(552)  评论(0编辑  收藏  举报