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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM