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


前言

13. 阻塞與非阻塞

本章內容為驅動基石之一。
驅動只提供功能,不提供策略。

阻塞與非阻塞 都是應用程式主動訪問的。從應用角度去解讀阻塞與非阻塞。

原文://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 輪詢

當用戶應用程式以非阻塞的方式訪問設備,設備驅動程式就要提供非阻塞的處理方式。
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:

宏 說明
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);。
    • epfd:epoll 句柄。
    • op:操作標識。
      • EPOLL_CTL_ADD:向 epfd 添加 fd 表示的描述符。
      • EPOLL_CTL_MOD:修改 fd 的 event 時間。
      • EPOLL_CTL_DEL:從 epfd 中刪除 fd 描述符。
    • fd:要監測的文件。
    • event:要監測的事件類型。
  3. 等待事件發生。
    • 函數原型: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:

宏 說明
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)
  • 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 參數。
  • 建議:找個常式看看就明白了。