Linux阻塞與非阻塞IO
當應用程式對裝置驅動進行操作的時候,如果不能擷取到裝置資源,那麼阻塞式IO就會将應用程式對應的線程挂起,直到裝置資源可以才做為止。對于非阻塞IO,應用程式對應的線程不會挂起,它要麼一直輪詢等待, 直到裝置資源可以使用,要麼就直接放棄。
阻塞IO示意圖
非阻塞IO示意圖
等待隊列
阻塞通路最大的好處就是當裝置檔案不可操作的時候程序可以進入休眠态,這樣可以将CPU 資源讓出來,那麼當裝置檔案可以操作的時候要怎麼才能喚醒線程呢?
Linux核心提供了等待隊列機制。
Linux核心的等待隊列是以雙循環連結清單為基礎資料結構,與程序排程機制緊密結合,能夠用于實作核心的異步事件通知機制。它有兩種資料結構:等待隊列頭(wait_queue_head_t)和等待隊列項(wait_queue_t)。
等待隊列頭
等待隊列頭使用結構體wait_queue_head_t 表示, wait_queue_head_t 結構體定義在檔案include/linux/wait.h 中,結構體内容如下:
struct __wait_queue_head {
spinlock_t lock; /* 保護等待隊列的原子鎖
(自旋鎖),在對task_list與操作的過程中,使用該鎖實作對等待隊列的互斥通路*/
struct list_head task_list; /* 等待隊列,雙向循環連結清單,存放等待的程序 */
};
等待隊列項
等待隊列頭就是一個等待隊列的頭部,每個通路裝置的程序都是一個隊列項,當裝置不可用的時候就要将這些程序對應的等待隊列項添加到等待隊列裡面。結構體wait_queue_t表示等待隊列項,結構體内容如下:
/*__wait_queue,該結構是對一個等待任務的抽象。每個等待任務都會抽象成一個wait_queue,
并且挂載到wait_queue_head上。該結構定義如下:*/
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private; /* 通常指向目前任務控制塊 */
wait_queue_func_t func;
struct list_head task_list; /* 挂入wait_queue_head的挂載點 */
};
typedef struct __wait_queue wait_queue_t;
/* 任務喚醒操作方法,該方法在核心中提供,通常為auto remove_wake_function */
等待隊列相關操作
- 定義并初始化等待隊列頭
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue); //會将自旋鎖初始化為未鎖,等待隊列初始化為空的雙向循環連結清單。
//宏名用于定義并初始化,相當于"快捷方式"
DECLARE_WAIT_QUEUE_HEAD (my_queue);
- 定義一個等待隊列項
DECLARE_WAITQUEUE(name, tsk);
name 就是等待隊列項的名字, tsk 表示這個等待隊列項屬于哪個任務(程序),一般設定為current,在Linux核心中current相當于一個全局變量。表示目前程序。是以宏DECLARE_WAITQUEUE就是給目前正在運作的程序建立并初始化了一個等待隊列項。
- 從等待隊列頭中添加或删除等待隊列項
void add_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
//在等待的資源或事件滿足時,程序被喚醒,使用該函數被從等待頭中删除。
void remove_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
- 等待喚醒
當裝置可以使用的時候就要喚醒進入休眠狀态的程序。
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
這兩個函數的差別在于:wake_up 函數可以喚醒處于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 狀态的程序,而 wake_up_interruptible 函數隻能喚醒處于 TASK_INTERRUPTIBLE 狀态的程序
5.等待事件
處于休眠中的程序,除了主動喚醒,也可以設定等待隊列等待某個事件,當這個事件滿足以後就自動喚醒等待隊列中的程序。
wait_event(queue,condition);//等待以queue為等待隊列頭等待隊列被喚醒,condition必須滿足,否則阻塞
wait_event_interruptible(queue,condition);//可被信号打斷
wait_event_timeout(queue,condition,timeout);//阻塞等待的逾時時間,時間到了,不論condition是否滿足,都要傳回
wait_event_interruptible_timeout(queue,condition,timeout)
非阻塞的處理方式——輪詢
poll、 epoll 和 select 可以用于處理輪詢,應用程式通過 select、 epoll 或 poll 函數來查詢裝置是否可以操作,如果可以操作的話就從裝置讀取或者向裝置寫入資料。
應用程式下的輪詢函數
select函數
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout)
readfds、writefds、exiceptfds這三個指針指向檔案描述符的集合,都是fd_set類型。
fd_set的每一個位都代表一個檔案描述符。readfds用于監視指定描述符集的讀變化,也就是監視這些檔案是否可以讀取,隻要這些集合裡面有一個檔案可以讀取那麼seclect就會傳回一個大于 0 的值表示檔案可以讀取。
fd_set的操作宏
void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)
timeout表示輪詢時間片,多長時間輪詢一次
struct timeval結構體
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
}
poll函數
select函數能夠監視的檔案描述符有最大數量限制,一般為1024,并且監控的檔案描述符越多效率越低。這時可以使用poll函數
int poll(struct pollfd *fds,
nfds_t nfds, /*檔案描述符的個數*/
int timeout) /*逾時時間機關ms*/
struct pollfd
{
int fd; //檔案描述符
short events; //注冊的事件,要監視的事件
short revents //實際發生的事件,由核心填充
}
events代表要監視的事件,可以有以下幾種,用宏定義表示
宏名 | 說明 |
---|---|
POLLIN | 有資料可以讀取。 |
POLLPRI | 有緊急的資料需要讀取。 |
POLLOUT | 可以寫資料。 |
POLLERR | 指定的檔案描述符發生錯誤。 |
POLLHUP | 指定的檔案描述符挂起。 |
POLLNVAL | 無效的請求。 |
POLLRDNORM | 等同于 POLLIN |
epoll函數
select函數和poll函數都會随着要監視的fd數量的增加而效率降低,而且poll函數需要周遊所有的檔案描述符檢測就緒的描述符。epoll函數就為并發而生的
Linux 驅動下的 poll 操作函數
當應用程式調用 select 或 poll 函數來對驅動程式進行非阻塞通路的時候,驅動程式file_operations 操作集中的 poll 函數就會執行。是以驅動程式的編寫者需要提供驅對應的 poll 函數
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
這個struct poll_table_struct 結構體是使用者空間傳來的一般是給 poll_wait的參數
傳回值 | 描述 |
---|---|
POLLIN | 有資料可以讀取。 |
POLLPRI | 有緊急的資料需要讀取。 |
POLLOUT | 可以寫資料。 |
POLLERR | 指定的檔案描述符發生錯誤。 |
POLLHUP | 指定的檔案描述符挂起。 |
POLLNVAL | 無效的請求。 |
POLLRDNORM | 等同于 POLLIN,普通資料可讀 |
在驅動程式的 poll函數中調用poll_wait函數,poll_wait函數不會引起阻塞,隻是将應用程式添加到 poll_table中
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)