【linux】驅動-13-阻塞與非阻塞

李柱明發表於2021-06-21


前言

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 引數。
  • 建議:找個例程看看就明白了。

相關文章