目錄
聲明
1. 阻塞和非阻塞簡介
2. 等待隊列
2.1 等待隊列頭
2.2 等待隊列項
2.3?將隊列項添加/移除等待隊列頭
2.4?等待喚醒
2.5 等待事件
3.?輪詢
3.1 select函數
3.2 poll函數
3.3 epoll函數
4.?Linux 驅動下的 poll 操作函數
聲明
本博客所記錄的關于正點原子i.MX6ULL開發板的學習筆記,(內容參照正點原子I.MX6U嵌入式linux驅動開發指南,可在正點原子官方獲取正點原子Linux開發板 — 正點原子資料下載中心 1.0.0 文檔),旨在如實記錄我在學校學習該開發板過程中所遭遇的各類問題以及詳細的解決辦法。其初衷純粹是為了個人知識梳理、學習總結以及日后回顧查閱方便,同時也期望能為同樣在學習這款開發板的同學或愛好者提供一些解決問題的思路和參考。我盡力保證內容的準確性和可靠性,但由于個人知識水平和實踐經驗有限,若存在錯誤或不嚴謹之處,懇請各位讀者批評指正。
責任聲明:雖然我力求提供有效的問題解決辦法,但由于開發板使用環境、硬件差異、軟件版本等多種因素的影響,我的筆記內容不一定適用于所有情況。對于因參考本筆記而導致的任何直接或間接損失,我不承擔任何法律責任。使用本筆記內容的讀者應自行承擔相關風險,并在必要時尋求專業技術支持。
1. 阻塞和非阻塞簡介
當應用程序對設備驅動進行操作的時候,如果不能獲取到設備資源,那么阻塞式 IO (IO 指的是 Input/Output,也就是輸入/輸出,是應用程序對驅動設備的輸入/輸出操作)就會將應用程序對應的線程掛起,直到設備資源可以獲取為止。對于非阻塞 IO,應用程序對應的線程不會掛起,它要么一直輪詢等待,直到設備資源可以使用,要么就直接放棄。阻塞式 IO 如圖所示:
應用程序調用 read 函數從設備中讀取數據,當設備不可用或數據未準備好的時候就會進入到休眠態。等設備可用的時候就會從休眠態喚醒,然后從設備中讀取數據返回給應用程序。
非阻塞 IO 如圖所示:
應用程序使用非阻塞訪問方式從設備讀取數據,當設備不可用或數據未準備好的時候會立即向內核返回一個錯誤碼,表示數據讀取失敗。應用程序會再次重新讀取數據,這樣一直往復循環,直到數據讀取成功。
2. 等待隊列
2.1 等待隊列頭
阻塞訪問最大的好處就是當設備文件不可操作的時候進程可以進入休眠態,這樣可以將CPU 資源讓出來。當設備文件可以操作的時候就必須喚醒進程,一般在中斷函數里面完成喚醒工作。 Linux 內核提供了等待隊列(wait queue)來實現阻塞進程的喚醒工作,如果我們要在驅動中使用等待隊列,必須創建并初始化一個等待隊列頭,等待隊列頭使用結構體wait_queue_head_t 表示, wait_queue_head_t 結構體定義在文件 include/linux/wait.h 中,結構體內容如下所示:
// 定義一個等待隊列頭結構體
// 等待隊列頭用于管理等待隊列,等待隊列常用于實現進程的睡眠和喚醒操作
struct __wait_queue_head {// 自旋鎖,用于保護等待隊列相關的操作// 自旋鎖是一種用于多處理器環境下的鎖機制,當一個線程獲取自旋鎖時,如果鎖已被其他線程持有,該線程會不斷循環嘗試獲取鎖,而不是進入睡眠狀態spinlock_t lock; // 任務列表,用于存儲等待在該等待隊列上的任務(進程)// 這里使用鏈表來管理等待的任務,每個等待的任務以節點的形式存儲在該鏈表中struct list_head task_list;
};// 為結構體 __wait_queue_head 定義一個別名 wait_queue_head_t
// 這樣在后續使用時可以更方便地聲明該結構體類型的變量
typedef struct __wait_queue_head wait_queue_head_t;
定義好等待隊列頭以后 使用 init_waitqueue_head 函數初始化等待隊列頭,函數原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
參數 q 就是要初始化的等待隊列頭。
也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 來一次性完成等待隊列頭的定義的初始化
2.2 等待隊列項
等待隊列頭就是一個等待隊列的頭部,每個訪問設備的進程都是一個隊列項,當設備不可用的時候就要將這些進程對應的等待隊列項添加到等待隊列里面。結構體 wait_queue_t 表示等待隊列項,結構體內容如下:
?
// 等待隊列結構體
struct __wait_queue {// 標志位,用于存儲一些與等待隊列相關的狀態信息,例如等待的條件是否滿足等unsigned int flags;// 私有數據指針,通常用于指向與等待隊列相關的特定數據,例如等待的進程相關的額外信息等void *private;// 等待隊列函數指針,指向一個函數,該函數通常用于處理等待隊列上的事件,比如喚醒等待的任務等wait_queue_func_t func;// 任務列表,用于將等待隊列上的任務以鏈表的形式組織起來,方便管理和操作等待的任務struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
使用宏 DECLARE_WAITQUEUE 定義并初始化一個等待隊列項,宏的內容如下:
DECLARE_WAITQUEUE(name, tsk)
name 就是等待隊列項的名字, tsk 表示這個等待隊列項屬于哪個任務(進程),一般設置為current , 在 Linux 內 核 中 current 相 當 于 一 個 全 局 變 量 , 表 示 當 前 進 程 。 因 此 宏DECLARE_WAITQUEUE 就是給當前正在運行的進程創建并初始化了一個等待隊列項。
2.3?將隊列項添加/移除等待隊列頭
當設備不可訪問的時候就需要將進程對應的等待隊列項添加到前面創建的等待隊列頭中,只有添加到等待隊列頭中以后進程才能進入休眠態。當設備可以訪問以后再將進程對應的等待隊列項從等待隊列頭中移除即可,
等待隊列項添加 API 函數如下:
void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
函數參數和返回值含義如下:
q: 等待隊列項要加入的等待隊列頭。
wait:要加入的等待隊列項。
返回值:無。
等待隊列項移除 API 函數如下:
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
函數參數和返回值含義如下:
q: 要刪除的等待隊列項所處的等待隊列頭。
wait:要刪除的等待隊列項。
返回值:無。
2.4?等待喚醒
當設備可以使用的時候就要喚醒進入休眠態的進程,喚醒可以使用兩個函數:
void wake_up(wait_queue_head_t *q)void wake_up_interruptible(wait_queue_head_t *q)
參數 q 就是要喚醒的等待隊列頭,這兩個函數會將這個等待隊列頭中的所有進程都喚醒。wake_up 函數可以喚醒處于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 狀態的進程,而 wake_up_interruptible 函數只能喚醒處于 TASK_INTERRUPTIBLE 狀態的進程。
2.5 等待事件
當這個事件滿足以后就自動喚醒等待隊列中的進程,和等待事件有關的 API 函數如表 52.1.2.1 所示:
3.?輪詢
如果用戶應用程序以非阻塞的方式訪問設備,設備驅動程序就要提供非阻塞的處理方式,也就是輪詢。 poll、 epoll 和 select 可以用于處理輪詢,應用程序通過 select、 epoll 或 poll 函數來查詢設備是否可以操作,如果可以操作的話就從設備讀取或者向設備寫入數據。當應用程序調用 select、 epoll 或 poll 函數的時候設備驅動程序中的 poll 函數就會執行。
3.1 select函數
select 函數原型:
// nfds 表示被檢查的描述符集合中最大描述符加1
// 即它用于指定需要檢查的文件描述符的范圍,范圍是從0到nfds - 1的文件描述符都會被檢查
int select(int nfds, // readfds 是一個指向fd_set類型的指針,fd_set 是一個用于存儲文件描述符集合的類型// readfds 用于指定需要檢查可讀性的文件描述符集合,集合中的文件描述符會被檢查是否有數據可讀fd_set *readfds, // writefds 是一個指向fd_set類型的指針// writefds 用于指定需要檢查可寫性的文件描述符集合,集合中的文件描述符會被檢查是否可寫fd_set *writefds, // exceptfds 是一個指向fd_set類型的指針// exceptfds 用于指定需要檢查異常情況的文件描述符集合,集合中的文件描述符會被檢查是否有異常發生fd_set *exceptfds, // timeout 是一個指向struct timeval類型的指針// struct timeval 是一個結構體,包含秒和微秒兩個成員,用于表示時間// timeout 用于設置select函數的超時時間,如果設置為NULL,則select函數會一直阻塞直到有文件描述符滿足條件或發生錯誤;如果設置了具體的時間值,select函數會在超時時間到達時返回struct timeval *timeout)
{// 函數體部分省略,實際的select函數實現會根據傳入的參數檢查文件描述符集合// 并返回滿足條件的文件描述符數量,或者在超時或發生錯誤時返回相應的值
}
select 函數的返回值有以下幾種情況:
大于 0:表示有文件描述符滿足了可讀、可寫或異常的條件,返回值是滿足條件的文件描述符的數量。
等于 0:表示在指定的超時時間內沒有文件描述符滿足條件(當 timeout 不為 NULL 時)。
小于 0:表示發生了錯誤,具體的錯誤類型可以通過 errno 變量獲取
nfds: 所要監視的這三類文件描述集合中, 最大文件描述符加 1。
readfds、 writefds 和 exceptfds:這三個指針指向描述符集合,這三個參數指明了關心哪些描述符、需要滿足哪些條件等等,這三個參數都是 fd_set 類型的, fd_set 類型變量的每一個位都代表了一個文件描述符。 readfds 用于監視指定描述符集的讀變化,也就是監視這些文件是否可以讀取,只要這些集合里面有一個文件可以讀取那么 seclect 就會返回一個大于 0 的值表示文件可以讀取。如果沒有文件可以讀取,那么就會根據 timeout 參數來判斷是否超時。可以將 readfs設置為 NULL,表示不關心任何文件的讀變化。 writefds 和 readfs 類似,只是 writefs 用于監視這些文件是否可以進行寫操作。 exceptfds 用于監視這些文件的異常。
當我們定義好一個 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)
FD_ZERO 用于將 fd_set 變量的所有位都清零。
FD_SET 用于將 fd_set 變量的某個位置 1,也就是向 fd_set 添加一個文件描述符,參數 fd 就是要加入的文件描述符。
FD_CLR 用于將 fd_se變量的某個位清零,也就是將一個文件描述符從 fd_set 中刪除,參數 fd 就是要刪除的文件描述符。
FD_ISSET 用于測試一個文件是否屬于某個集合,參數 fd 就是要判斷的文件描述符
timeout:超時時間,當我們調用 select 函數等待某些文件描述符可以設置超時時間,超時時間使用結構體 timeval 表示,結構體定義如下所示:
// 定義一個表示時間值的結構體 timeval
struct timeval {// tv_sec 成員表示秒數,是一個長整型數據// 用于存儲時間值中的秒部分long tv_sec; /* 秒 */// tv_usec 成員表示微秒數,也是一個長整型數據// 用于存儲時間值中的微秒部分,微秒是秒的百萬分之一long tv_usec; /* 微秒 */
};
當 timeout 為 NULL 的時候就表示無限期的等待。
返回值: 0,表示的話就表示超時發生,但是沒有任何文件描述符可以進行操作; -1,發生錯誤;其他值,可以進行操作的文件描述符個數。
使用 select 函數對某個設備驅動文件進行讀非阻塞訪問的操作示例如下所示:
?
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fcntl.h>// 主函數
void main(void)
{int ret, fd; // 定義變量 ret 用于存儲 select 函數的返回值,fd 用于存儲文件描述符fd_set readfds; // 定義文件描述符集 readfds,用于存儲需要監視的讀操作文件描述符struct timeval timeout; // 定義超時結構體 timeout,用于設置 select 函數的超時時間// 以讀寫且非阻塞的方式打開文件 "dev_xxx",將返回的文件描述符存儲在 fd 中fd = open("dev_xxx", O_RDWR | O_NONBLOCK); // 清空文件描述符集 readfds,將所有位都設置為 0FD_ZERO(&readfds); // 將文件描述符 fd 添加到文件描述符集 readfds 中,表示要監視 fd 的讀操作FD_SET(fd, &readfds); // 設置超時結構體的秒數為 0timeout.tv_sec = 0; // 設置超時結構體的微秒數為 500000,即 500 毫秒timeout.tv_usec = 500000; // 調用 select 函數,監視 readfds 集合中的文件描述符的讀操作,// 第二個參數為 NULL 表示不監視寫操作,第三個參數為 NULL 表示不監視異常情況,// 最后一個參數為 timeout 表示設置的超時時間,將返回滿足條件的文件描述符數量或錯誤值ret = select(fd + 1, &readfds, NULL, NULL, &timeout); // 根據 select 函數的返回值進行處理switch (ret) {case 0: // 如果返回值為 0,表示超時printf("timeout!\r\n");break;case -1: // 如果返回值為 -1,表示發生錯誤printf("error!\r\n");break;default: // 其他情況表示有文件描述符滿足可讀條件if(FD_ISSET(fd, &readfds)) { // 檢查文件描述符 fd 是否在 readfds 集合中且滿足可讀條件// 這里可以添加使用 read 函數讀取數據的代碼,目前僅作為示例,實際使用中需要根據需求編寫讀取數據的邏輯// read(fd, buffer, sizeof(buffer)); // 假設 buffer 是用于存儲讀取數據的緩沖區}break;}
}
注:在單個線程中, select 函數能夠監視的文件描述符數量有最大的限制,一般為 1024,可以修改內核將監視的文件描述符數量改大,但是這樣會降低效率!這個時候就可以使用 poll 函數。
3.2 poll函數
poll 函數本質上和 select 沒有太大的差別,但是 poll 函數沒有最大文件描述符限制, Linux 應用程序中 poll 函數原型如下所示:
?
int poll(struct pollfd *fds,nfds_t nfds,int timeout)
fds: 要監視的文件描述符集合以及要監視的事件,為一個數組,數組元素都是結構體 pollfd類型的, pollfd 結構體如下所示:
// pollfd 結構體用于描述一個被監視的文件描述符及其相關事件
struct pollfd {// 文件描述符:需要監視的目標文件描述符// 設置為 -1 時表示忽略該條目(events 字段會被忽略,revents 會被置零)int fd; // 請求的事件:bitmask,指定要監視的事件類型// 常用事件標志包括:// POLLIN - 有數據可讀(包括普通數據和優先帶外數據)// POLLOUT - 可以寫入數據(不會阻塞)// POLLPRI - 有緊急數據可讀(如 TCP 帶外數據)// POLLERR - 發生錯誤(僅在 revents 中返回,不可設置)// POLLHUP - 發生掛起(如管道關閉、連接斷開)// POLLNVAL - 文件描述符無效(如未打開)short events; // 返回的事件:bitmask,由內核填充,指示實際發生的事件// 包含與 events 相同的標志位,另外還可能包含:// POLLERR - 文件描述符發生錯誤// POLLHUP - 文件描述符被掛起// POLLNVAL - 文件描述符無效short revents;
};
fd 是要監視的文件描述符,如果 fd 無效的話那么 events 監視事件也就無效,并且 revents
返回 0。 events 是要監視的事件。返回值:返回 revents 域中不為 0 的 pollfd 結構體個數,也就是發生事件或錯誤的文件描述符數量; 0,超時; -1,發生錯誤,并且設置 errno 為錯誤類型。
使用 poll 函數對某個設備驅動文件進行讀非阻塞訪問的操作示例如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>int main(void)
{int ret;int fd; /* 要監視的文件描述符 */struct pollfd fds; /* 定義pollfd結構體,用于poll系統調用 */// 以讀寫和非阻塞模式打開指定文件// 非阻塞模式意味著read/write操作不會阻塞進程fd = open("filename", O_RDWR | O_NONBLOCK); if (fd < 0) {perror("open failed");return -1;}/* 配置pollfd結構體 */fds.fd = fd; /* 設置要監視的文件描述符 */fds.events = POLLIN; /* 監視POLLIN事件:表示有數據可讀 *//* 可選事件還包括:POLLOUT(可寫)、POLLERR(錯誤)等 *//* 調用poll函數監視文件描述符 */// 參數1:指向pollfd數組的指針(這里只有一個元素)// 參數2:數組中元素的數量// 參數3:超時時間(毫秒),-1表示無限等待,0表示立即返回ret = poll(&fds, 1, 500); /* 輪詢文件是否有可讀數據,超時時間500ms *//* 處理poll返回值 */if (ret > 0) { /* 有事件發生 */if (fds.revents & POLLIN) { /* 檢查是否是可讀事件 *//* 讀取數據的代碼 */char buffer[1024];int bytes = read(fd, buffer, sizeof(buffer));if (bytes > 0) {printf("Read %d bytes\n", bytes);/* 處理讀取到的數據 */} else if (bytes == 0) {printf("EOF detected\n");} else {perror("read error");}} else if (fds.revents & POLLERR) { /* 檢查是否有錯誤發生 */printf("Poll error on fd %d\n", fd);}} else if (ret == 0) { /* 超時,沒有事件發生 */printf("Poll timeout after 500ms\n");} else { /* 錯誤處理 */perror("poll failed");close(fd);return -1;}close(fd); /* 關閉文件描述符 */return 0;
}
3.3 epoll函數
selcet 和 poll 函數都會隨著所監聽的 fd 數量的增加,出現效率低下的問題,而且poll 函數每次必須遍歷所有的描述符來檢查就緒的描述符,這個過程很浪費時間。
epoll 就是為處理大并發而準備的,一般常常在網絡編程中使用 epoll 函數。應用程序需要先使用 epoll_create 函數創建一個 epoll 句柄, epoll_create 函數原型如下:
int epoll_create(int size)
size: 從 Linux2.6.8 開始此參數已經沒有意義了,隨便填寫一個大于 0 的值就可以。
返回值: epoll 句柄,如果為-1 的話表示創建失敗。
epoll 句柄創建成功以后使用 epoll_ctl 函數向其中添加要監視的文件描述符以及監視的事件, epoll_ctl 函數原型如下所示:
/*** epoll_ctl - 控制epoll實例,添加、修改或刪除監視的文件描述符* @epfd: epoll實例的文件描述符(由epoll_create創建)* @op: 操作類型,可選值:* - EPOLL_CTL_ADD: 向epoll實例中添加新的監視文件描述符* - EPOLL_CTL_MOD: 修改已存在的監視文件描述符的事件掩碼* - EPOLL_CTL_DEL: 從epoll實例中刪除監視的文件描述符* @fd: 要操作的目標文件描述符(如socket、管道等)* @event: 指向epoll_event結構體的指針,指定要監視的事件類型和關聯數據* 當op為EPOLL_CTL_DEL時,event參數可以為NULL* * 返回值:* 成功時返回0,失敗時返回-1并設置errno*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數 struct epoll_event *event?
struct epoll_event {uint32_t events; /* 要監視的事件類型(位掩碼) */epoll_data_t data; /* 與文件描述符關聯的用戶數據 */
};typedef union epoll_data {void *ptr; /* 指向用戶自定義數據的指針 */int fd; /* 文件描述符 */uint32_t u32; /* 32位整數 */uint64_t u64; /* 64位整數 */
} epoll_data_t;
?常用事件類型:
常用事件類型(events 字段):
EPOLLIN - 有數據可讀(包括普通數據和優先帶外數據)
EPOLLOUT - 可以寫入數據(不會阻塞)
EPOLLPRI - 有緊急數據可讀(如 TCP 帶外數據)
EPOLLERR - 文件描述符發生錯誤(自動觸發,無需設置)
EPOLLHUP - 文件描述符被掛起(自動觸發,無需設置)
EPOLLET - 設置為邊沿觸發模式(Edge Triggered,默認是水平觸發)
EPOLLONESHOT - 一次性觸發,事件觸發后自動從監視列表移除,當監視完成以后還需要再次監視某個 fd,那么就需要將fd 重新添加到 epoll 里面。
返回值: 0,成功; -1,失敗,并且設置 errno 的值為相應的錯誤碼。
一切都設置好以后應用程序就可以通過 epoll_wait 函數來等待事件的發生,類似 select 函數。 epoll_wait 函數原型如下所示:
int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout)
函數參數和返回值含義如下:
epfd: 要等待的 epoll。
events: 指向 epoll_event 結構體的數組,當有事件發生的時候 Linux 內核會填寫 events,調
用者可以根據 events 判斷發生了哪些事件。
maxevents: events 數組大小,必須大于 0。
timeout: 超時時間,單位為 ms。
返回值: 0,超時; -1,錯誤;其他值,準備就緒的文件描述符數量。
epoll 更多的是用在大規模的并發服務器上,因為在這種場合下 select 和 poll 并不適合。當
設計到的文件描述符(fd)比較少的時候就適合用 selcet 和 poll。
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>int main() {// 創建epoll實例,參數size已被棄用但必須為正數(內核2.6.8后被忽略)// 返回值是一個文件描述符,用于后續的epoll操作int epfd = epoll_create(1);if (epfd == -1) {perror("epoll_create");exit(EXIT_FAILURE);}// 創建TCP套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket");close(epfd); // 清理已創建的資源exit(EXIT_FAILURE);}// 配置服務器地址(實際應用中需要bind、listen等操作)struct sockaddr_in server_addr = {.sin_family = AF_INET,.sin_port = htons(8080),.sin_addr.s_addr = INADDR_ANY};// 綁定套接字(示例中省略錯誤處理)if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 監聽連接(示例中省略錯誤處理)if (listen(sockfd, 5) == -1) {perror("listen");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 設置epoll_event結構體,配置監聽事件struct epoll_event ev;// 監視可讀事件并使用邊沿觸發模式// 邊沿觸發模式要求:// 1. 必須使用非阻塞I/O// 2. 必須處理完所有數據(否則不會再次觸發)ev.events = EPOLLIN | EPOLLET;// 存儲與事件關聯的數據(可在epoll_wait返回時獲取)ev.data.fd = sockfd;// 將監聽套接字添加到epoll實例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {perror("epoll_ctl: add");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 后續可能需要修改監聽事件(例如添加可寫事件)// 注意:修改事件時需重新設置整個ev.events字段ev.events = EPOLLIN | EPOLLOUT | EPOLLET; // 同時監聽可讀和可寫事件if (epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev) == -1) {perror("epoll_ctl: mod");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 當不再需要監聽某個文件描述符時,從epoll實例中刪除// 第三個參數為需要刪除的文件描述符// 第四個參數在刪除時可以為NULL(內核不使用該參數)if (epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL) == -1) {perror("epoll_ctl: del");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 關閉資源close(sockfd);close(epfd);return 0;
}
4.?Linux 驅動下的 poll 操作函數
當應用程序調用 select 或 poll 函數來對驅動程序進行非阻塞訪問的時候,驅動程序file_operations 操作集中的 poll 函數就會執行。所以驅動程序的編寫者需要提供對應的 poll 函數, poll 函數原型如下所示:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
函數參數和返回值含義如下: filp: 要打開的設備文件(文件描述符)。
wait: 結構體 poll_table_struct 類型指針, 由應用程序傳遞進來的。一般將此參數傳遞給poll_wait 函數。
返回值:向應用程序返回設備或者資源狀態,可以返回的資源狀態如下:
POLLIN 有數據可以讀取。
POLLPRI | 有緊急的數據需要讀取。 |
POLLOUT | 可以寫數據。 |
POLLERR | 指定的文件描述符發生錯誤。 |
POLLHUP | 指定的文件描述符掛起。 |
POLLNVAL | 無效的請求。 |
POLLRDNORM | 等同于 POLLIN,普通數據可讀 |
在驅動程序的 poll 函數中調用 poll_wait 函數, poll_wait 函數不會引起阻塞,只是將應用程序添加到 poll_table 中, poll_wait 函數原型如下:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
參數 wait_address 是要添加到 poll_table 中的等待隊列頭,參數 p 就是 poll_table,就是file_operations 中 poll 函數的 wait 參數。
使用示例:
/*** my_driver_poll - 實現設備驅動的輪詢方法,用于I/O多路復用* @filp: 指向被操作文件的結構體指針,包含文件狀態和私有數據* @wait: 用于注冊等待隊列的poll_table結構體指針,由內核提供* * 返回值:* 位掩碼,表示當前設備的就緒狀態(POLLIN/POLLOUT等)* * 功能說明:* 1. 注冊等待隊列,使當前進程可以在設備狀態變化時被喚醒* 2. 檢查設備的讀寫緩沖區狀態,設置相應的就緒標志* 3. 立即返回當前狀態,不阻塞進程*/
static unsigned int my_driver_poll(struct file *filp, struct poll_table_struct *wait)
{// 從文件的私有數據中獲取設備結構體實例struct my_device *dev = filp->private_data;unsigned int mask = 0; // 初始化就緒狀態掩碼/* 注冊等待隊列 - 這是實現非阻塞輪詢的關鍵 */// 將當前進程添加到讀操作的等待隊列中// 當設備有新數據可讀時,會喚醒該隊列中的進程poll_wait(filp, &dev->read_wait_queue, wait);// 將當前進程添加到寫操作的等待隊列中// 當設備有足夠空間可寫入時,會喚醒該隊列中的進程poll_wait(filp, &dev->write_wait_queue, wait);/* 檢查讀緩沖區狀態 - 決定是否返回可讀標志 */// 如果接收緩沖區不為空(有數據可讀)if (!list_empty(&dev->rx_buffer)) {// 設置可讀標志:// POLLIN - 有數據可讀(包括普通數據和優先數據)// POLLRDNORM - 有普通數據可讀(與POLLIN類似,具體取決于設備類型)mask |= POLLIN | POLLRDNORM;}/* 檢查寫緩沖區狀態 - 決定是否返回可寫標志 */// 如果發送緩沖區有足夠空間(大于最小寫入空間閾值)if (dev->tx_buffer_space > MIN_WRITE_SPACE) {// 設置可寫標志:// POLLOUT - 可以寫入數據(不會阻塞)// POLLWRNORM - 可以寫入普通數據(與POLLOUT類似,具體取決于設備類型)mask |= POLLOUT | POLLWRNORM;}// 返回當前設備的就緒狀態掩碼// 內核會根據這個掩碼通知用戶空間程序哪些操作可以立即執行return mask;
}
?