前言
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。
- 在驅動中,可以使用等待佇列來實現阻塞程式的喚醒。
使用方法:
- 定義等待佇列頭部。
- 初始化等待佇列頭部。
- 定義等待佇列元素。
- 新增/移除等待佇列。
- 等待事件。
- 喚醒佇列。
另外一種使用方法就是 在等待佇列上睡眠。
等待佇列頭部結構體:
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() 使用方法:
- 建立一個 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:
巨集 | 說明 |
---|---|
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 引數。
- 建議:找個例程看看就明白了。