Linux 阻塞和非阻塞 IO 實驗學習

Bathwind_W發表於2024-06-21

Linux 阻塞和非阻塞 IO 實驗學習

IO 指的是 Input/Output,也就是輸入/輸出,是應用程式對驅動裝置的輸入/輸出操作。當應用程式對裝置驅動進行操作的時候,如果不能獲取到裝置資源,那麼阻塞式 IO 就會將應用程式對應的執行緒掛起,直到裝置資源可以獲取為止。對於非阻塞 IO,應用程式對應的執行緒不會掛起,它要麼一直輪詢等待,直到裝置資源可以使用,要麼就直接放棄。阻塞式 IO 如圖 52.1.1.1所示:
在這裡插入圖片描述
應用程式呼叫 read 函式從裝置中讀取資料,當裝置不可用或資料未準備好的時候就會進入到休眠態。等裝置可用的時候就會從休眠態喚醒,然後從裝置中讀取資料返回給應用程式。
非阻塞 IO 如圖 52.1.2 所示:
在這裡插入圖片描述
應用程式使用非阻塞訪問方式從裝置讀取資料,當裝置不可用或資料未準備好的時候會立即向核心返回一個錯誤碼,表示資料讀取失敗。應用程式會再次重新讀取資料,這樣一直往復迴圈,直到資料讀取成功。

示例程式碼 52.1.1.1 應用程式阻塞讀取資料
1 int fd;
2 int data = 0;
3 4
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式開啟 */
5 ret = read(fd, &data, sizeof(data)); /* 讀取資料 */

如果應用程式要採用非阻塞的方式來訪問驅動裝置檔案,可以使用如下所示程式碼:

示例程式碼 52.1.1.2 應用程式非阻塞讀取資料
1 int fd;
2 int data = 0;
3 4
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式開啟 */
5 ret = read(fd, &data, sizeof(data)); /* 讀取資料 */
第 4 行使用 open 函式開啟“/dev/xxx_dev”裝置檔案的時候新增了引數“O_NONBLOCK”,
表示以非阻塞方式開啟裝置,這樣從裝置中讀取資料的時候就是非阻塞方式的了。

等待佇列

1、等待佇列頭

阻塞訪問最大的好處就是當裝置檔案不可操作的時候程序可以進入休眠態,這樣可以將CPU 資源讓出來。但是,當裝置檔案可以操作的時候就必須喚醒程序,一般在中斷函式里面完成喚醒工作。 Linux 核心提供了等待佇列(wait queue)來實現阻塞程序的喚醒工作,如果我們要在驅動中使用等待佇列,必須建立並初始化一個等待佇列頭,等待佇列頭使用結構體wait_queue_head_t 表示, wait_queue_head_t 結構體定義在檔案 include/linux/wait.h 中,結構體內容如下所示:

示例程式碼 52.1.2.1 wait_queue_head_t 結構體
39 struct __wait_queue_head {
40 spinlock_t lock;
41 struct list_head task_list;
42 };
43 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、等待佇列項

等待佇列頭就是一個等待佇列的頭部,每個訪問裝置的程序都是一個佇列項,當裝置不可用的時候就要將這些程序對應的等待佇列項新增到等待佇列裡面。結構體 wait_queue_t 表示等待佇列項,結構體內容如下:

示例程式碼 52.1.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 就是給當前正在執行的程序建立並初始化了一個等待佇列項。

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:要刪除的等待佇列項。
返回值:無。

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 狀態的程序。

5、等待事件

除了主動喚醒以外,也可以設定等待佇列等待某個事件,當這個事件滿足以後就自動喚醒等待佇列中的程序,和等待事件有關的 API 函式如表 52.1.2.1 所示:
在這裡插入圖片描述
在這裡插入圖片描述

輪詢

如果使用者應用程式以非阻塞的方式訪問裝置,裝置驅動程式就要提供非阻塞的處理方式,也就是輪詢。 poll、 epoll 和 select 可以用於處理輪詢,應用程式透過 select、 epoll 或 poll 函式來查詢裝置是否可以操作,如果可以操作的話就從裝置讀取或者向裝置寫入資料。當應用程式呼叫 select、 epoll 或 poll 函式的時候裝置驅動程式中的 poll 函式就會執行,因此需要在裝置驅動程式中編寫 poll 函式。我們先來看一下應用程式中使用的 select、 poll 和 epoll 這三個函式。

1、 select 函式

select 函式原型如下:

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout)

nfds: 所要監視的這三類檔案描述集合中, 最大檔案描述符加 1。
readfds、 writefds 和 exceptfds:這三個指標指向描述符集合,這三個引數指明瞭關心哪些描述符、需要滿足哪些條件等等,這三個引數都是 fd_set 型別的, fd_set 型別變數的每一個位都代表了一個檔案描述符。readfds 用於監視指定描述符集的讀變化,也就是監視這些檔案是否可以讀取,只要這些集合裡面有一個檔案可以讀取那麼 seclect 就會返回一個大於 0 的值表示檔案可以讀取。如果沒有檔案可以讀取,那麼就會根據 timeout 引數來判斷是否超時。可以將 readfs設定為 NULL,表示不關心任何檔案的讀變化。 writefds 和 readfs 類似,只是 writefs 用於監視這些檔案是否可以進行寫操作。 exceptfds 用於監視這些檔案的異常。
我們現在要從一個裝置檔案中讀取資料,那麼就可以定義一個 fd_set 變數,這個變數要傳遞給引數 readfds。當我們定義好一個 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_set變數的某個位清零,也就是將一個檔案描述符從 fd_set 中刪除,引數 fd 就是要刪除的檔案描述符。 FD_ISSET 用於測試一個檔案是否屬於某個集合,引數 fd 就是要判斷的檔案描述符。
timeout:超時時間,當我們呼叫 select 函式等待某些檔案描述符可以設定超時時間,超時時間使用結構體 timeval 表示,結構體定義如下所示:

struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};

當 timeout 為 NULL 的時候就表示無限期的等待。
返回值: 0,表示的話就表示超時發生,但是沒有任何檔案描述符可以進行操作; -1,發生
錯誤;其他值,可以進行操作的檔案描述符個數。
使用 select 函式對某個裝置驅動檔案進行讀非阻塞訪問的操作示例如下所示:

示例程式碼 52.1.3.1 select 函式非阻塞讀訪問示例
1 void main(void)
2 {
3 int ret, fd; /* 要監視的檔案描述符 */
4 fd_set readfds; /* 讀操作檔案描述符集 */
5 struct timeval timeout; /* 超時結構體 */
6 7
fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */
8 9
FD_ZERO(&readfds); /* 清除 readfds */
10 FD_SET(fd, &readfds); /* 將 fd 新增到 readfds 裡面 */
11
12 /* 構造超時時間 */
13 timeout.tv_sec = 0;
14 timeout.tv_usec = 500000; /* 500ms */
15
16 ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
17 switch (ret) {
18 case 0: /* 超時 */
19 printf("timeout!\r\n");
20 break;
21 case -1: /* 錯誤 */
22 printf("error!\r\n");
23 break;
24 default: /* 可以讀取資料 */
25 if(FD_ISSET(fd, &readfds)) { /* 判斷是否為 fd 檔案描述符 */
26 /* 使用 read 函式讀取資料 */
27 }
28 break;
29 }
30 }

2、 poll 函式

在單個執行緒中, select 函式能夠監視的檔案描述符數量有最大的限制,一般為 1024,可以修改核心將監視的檔案描述符數量改大,但是這樣會降低效率!這個時候就可以使用 poll 函式,poll 函式本質上和 select 沒有太大的差別,但是 poll 函式沒有最大檔案描述符限制, Linux 應用程式中 poll 函式原型如下所示:

int poll(struct pollfd *fds,nfds_t nfds,int timeout)

函式引數和返回值含義如下:
fds: 要監視的檔案描述符集合以及要監視的事件,為一個陣列,陣列元素都是結構體 pollfd
型別的, pollfd 結構體如下所示:

struct pollfd {
int fd; /* 檔案描述符 */
short events; /* 請求的事件 */
short revents; /* 返回的事件 */
};

fd 是要監視的檔案描述符,如果 fd 無效的話那麼 events 監視事件也就無效,並且 revents返回 0。 events 是要監視的事件,可監視的事件型別如下所示:

POLLIN 有資料可以讀取
POLLPRI 有緊急的資料需要讀取。
POLLOUT 可以寫資料。
POLLERR 指定的檔案描述符發生錯誤。
POLLHUP 指定的檔案描述符掛起。
POLLNVAL 無效的請求。
POLLRDNORM 等同於 POLLIN
revents 是返回引數,也就是返回的事件, 由 Linux 核心設定具體的返回事件。
nfds: poll 函式要監視的檔案描述符數量。
timeout: 超時時間,單位為 ms。
返回值:返回 revents 域中不為 0 的 pollfd 結構體個數,也就是發生事件或錯誤的檔案描述
符數量; 0,超時; -1,發生錯誤,並且設定 errno 為錯誤型別。

使用 poll 函式對某個裝置驅動檔案進行讀非阻塞訪問的操作示例如下所示:

示例程式碼 52.1.3.2 poll 函式讀非阻塞訪問示例
1 void main(void)
2 {
3 int ret;
4 int fd; /* 要監視的檔案描述符 */
5 struct pollfd fds;
6 7
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */
8 9
/* 構造結構體 */
10 fds.fd = fd;
11 fds.events = POLLIN; /* 監視資料是否可以讀取 */
12
13 ret = poll(&fds, 1, 500); /* 輪詢檔案是否可操作,超時 500ms */
14 if (ret) { /* 資料有效 */
15 ......
16 /* 讀取資料 */
17 ......
18 } else if (ret == 0) { /* 超時 */
19 ......
20 } else if (ret < 0) { /* 錯誤 */
21 ......
22 }
23 }

3、 epoll 函式

傳統的 selcet 和 poll 函式都會隨著所監聽的 fd 數量的增加,出現效率低下的問題,而且poll 函式每次必須遍歷所有的描述符來檢查就緒的描述符,這個過程很浪費時間。為此, epoll應運而生, epoll 就是為處理大併發而準備的,一般常常在網路程式設計中使用 epoll 函式。應用程式需要先使用 epoll_create 函式建立一個 epoll 控制代碼, epoll_create 函式原型如下:

int epoll_create(int size)
函式引數和返回值含義如下:
size: 從 Linux2.6.8 開始此引數已經沒有意義了,隨便填寫一個大於 0 的值就可以。
返回值: epoll 控制代碼,如果為-1 的話表示建立失敗。

epoll 控制代碼建立成功以後使用 epoll_ctl 函式向其中新增要監視的檔案描述符以及監視的事件, epoll_ctl 函式原型如下所示:

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)

函式引數和返回值含義如下:

epfd: 要操作的 epoll 控制代碼,也就是使用 epoll_create 函式建立的 epoll 控制代碼。
op: 表示要對 epfd(epoll 控制代碼)進行的操作,可以設定為:
EPOLL_CTL_ADD 向 epfd 新增檔案引數 fd 表示的描述符。
EPOLL_CTL_MOD 修改引數 fd 的 event 事件。
EPOLL_CTL_DEL 從 epfd 中刪除 fd 描述符。
fd:要監視的檔案描述符。
event: 要監視的事件型別,為 epoll_event 結構體型別指標, epoll_event 結構體型別如下所
示:
struct epoll_event {
	uint32_t events; /* epoll 事件 */
	epoll_data_t data; /* 使用者資料 */
};
結構體 epoll_event 的 events 成員變數表示要監視的事件,可選的事件如下所示:
EPOLLIN 有資料可以讀取。
EPOLLOUT 可以寫資料。
EPOLLPRI 有緊急的資料需要讀取。
EPOLLERR 指定的檔案描述符發生錯誤。
EPOLLHUP 指定的檔案描述符掛起。
EPOLLET 設定 epoll 為邊沿觸發,預設觸發模式為水平觸發。
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,本章我們就使用 sellect 和 poll 這兩個函式。

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 引數。

阻塞 IO 實驗

在這裡插入圖片描述
imx6uirqApp 這個應用程式的 CPU 使用率竟然高達 99.6%,這僅僅是一個讀取按鍵值的應用程式,這麼高的 CPU 使用率顯然是有問題的!原因就在於我們是直接在 while 迴圈中透過 read 函式讀取按鍵值,因此imx6uirqApp 這個軟體會一直執行,一直讀取按鍵值, CPU 使用率肯定就會很高。最好的方法就是在沒有有效的按鍵事件發生的時候,imx6uirqApp 這個應用程式應該處於休眠狀態,當有按鍵事件發生以後 imx6uirqApp 這個應用程式才執行,列印出按鍵值,這樣就會降低 CPU 使用率,
下面開始阻塞實驗的編寫:
首先裝置結構體的編寫,裡面新增佇列頭變數。

/* imx6uirq裝置結構體 */
struct imx6uirq_dev{
	dev_t devid;			/* 裝置號 	 */	
	struct cdev cdev;		/* cdev 	*/                 
	struct class *class;	/* 類 		*/
	struct device *device;	/* 裝置 	 */
	int major;				/* 主裝置號	  */
	int minor;				/* 次裝置號   */
	struct device_node	*nd; /* 裝置節點 */	
	atomic_t keyvalue;		/* 有效的按鍵鍵值 */
	atomic_t releasekey;	/* 標記是否完成一次完成的按鍵,包括按下和釋放 */
	struct timer_list timer;/* 定義一個定時器*/
	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按鍵init述陣列 */
	unsigned char curkeynum;				/* 當前init按鍵號 */

	wait_queue_head_t r_wait;	/* 讀等待佇列頭 */
};

如果我們要在驅動中使用等待佇列,必須建立並初始化一個等待佇列頭,所以在裝置結構體資訊新增這個變數:wait_queue_head_t r_wait
接下來我們要考慮初始化這個佇列頭啥的。

static int keyio_init(void)
{
	unsigned char i = 0;
	char name[10];
	int ret = 0;
	
	imx6uirq.nd = of_find_node_by_path("/key");
	if (imx6uirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} 

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
		if (imx6uirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,並且設定成中斷模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(name));	/* 緩衝區清零 */
		sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);		/* 組合名字 */
		gpio_request(imx6uirq.irqkeydesc[i].gpio, name);
		gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);	
		imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
#if 0
		imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
#endif
	}

	/* 申請中斷 */
	imx6uirq.irqkeydesc[0].handler = key0_handler;
	imx6uirq.irqkeydesc[0].value = KEY0VALUE;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 建立定時器 */
     init_timer(&imx6uirq.timer);
     imx6uirq.timer.function = timer_function;

	/* 初始化等待佇列頭 */
	init_waitqueue_head(&imx6uirq.r_wait);
	return 0;
}

主要新增內容如上面程式碼所示,也就是這一句init_waitqueue_head(&imx6uirq.r_wait);
接下來一個重要的函式就是讀函式。讀函式如下列所示:

static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char releasekey = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

#if 0
	/* 加入等待佇列,等待被喚醒,也就是有按鍵按下 */
 	ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
	if (ret) {
		goto wait_error;
	} 
#endif

	DECLARE_WAITQUEUE(wait, current);	/* 定義一個等待佇列 */
	if(atomic_read(&dev->releasekey) == 0) {	/* 沒有按鍵按下 */
		add_wait_queue(&dev->r_wait, &wait);	/* 將等待佇列新增到等待佇列頭 */
		__set_current_state(TASK_INTERRUPTIBLE);/* 設定任務狀態 */
		schedule();							/* 進行一次任務切換 */
		if(signal_pending(current))	{			/* 判斷是否為訊號引起的喚醒 */
			ret = -ERESTARTSYS;
			goto wait_error;
		}
		__set_current_state(TASK_RUNNING);      /* 將當前任務設定為執行狀態 */
	    remove_wait_queue(&dev->r_wait, &wait);    /* 將對應的佇列項從等待佇列頭刪除 */
	}

	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* 有按鍵按下 */	
		if (keyvalue & 0x80) {
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		} else {
			goto data_error;
		}
		atomic_set(&dev->releasekey, 0);/* 按下標誌清零 */
	} else {
		goto data_error;
	}
	return 0;

wait_error:
	set_current_state(TASK_RUNNING);		/* 設定任務為執行態 */
	remove_wait_queue(&dev->r_wait, &wait);	/* 將等待佇列移除 */
	return ret;

data_error:
	return -EINVAL;
}

其中核心邏輯重要的地方如下:

DECLARE_WAITQUEUE(wait, current);	/* 定義一個等待佇列 */
	if(atomic_read(&dev->releasekey) == 0) {	/* 沒有按鍵按下 */
		add_wait_queue(&dev->r_wait, &wait);	/* 將等待佇列新增到等待佇列頭 */
		__set_current_state(TASK_INTERRUPTIBLE);/* 設定任務狀態 */
		schedule();							/* 進行一次任務切換 */
		if(signal_pending(current))	{			/* 判斷是否為訊號引起的喚醒 */
			ret = -ERESTARTSYS;
			goto wait_error;
		}
		__set_current_state(TASK_RUNNING);      /* 將當前任務設定為執行狀態 */
	    remove_wait_queue(&dev->r_wait, &wait);    /* 將對應的佇列項從等待佇列頭刪除 */
	}
	wait_error:
	set_current_state(TASK_RUNNING);		/* 設定任務為執行態 */
	remove_wait_queue(&dev->r_wait, &wait);	/* 將等待佇列移除 */
	return ret;

在Linux核心中,程序或執行緒可能需要等待某些條件滿足才能繼續執行。這種情況在裝置驅動程式中尤其常見,比如等待硬體事件(如按鍵輸入)。在這個程式碼段中,使用了一系列標準的核心函式和宏來實現程序的阻塞和喚醒,這些操作是為了安全和有效地處理程序等待和喚醒,具體包括以下幾個步驟:

  1. __set_current_state(TASK_INTERRUPTIBLE);
    這行程式碼將當前程序的狀態設定為TASK_INTERRUPTIBLE,即可中斷的睡眠狀態。在這種狀態下,程序不會執行,從而不會消耗CPU資源,但它可以被訊號等中斷喚醒。這是一種資源節約和高效的等待方式。

  2. schedule();
    schedule()函式是程序排程器的核心功能,它會停止當前程序的執行,並選擇另一個程序繼續執行。噹噹前程序呼叫schedule()後,它會在排程器找到下一個要執行的程序之前進入睡眠狀態,直到被喚醒。在我們的場景中,這意味著程序將等待直到按鍵事件發生(即等待佇列被喚醒)。

  3. if(signal_pending(current))
    在從schedule()返回後,這行程式碼檢查是否有未處理的訊號需要當前程序處理。在Linux中,程序可以透過訊號與其他程序互動,這些訊號可以是使用者透過鍵盤產生的(如Ctrl-C),也可以是其他程序或系統傳送的。如果存在待處理的訊號,這意味著程序被一個外部事件(而非按鍵事件)喚醒,這通常需要特殊處理。
    特殊處理主要是以下程式碼:

  4. -ERESTARTSYS
    如果檢測到訊號,函式返回-ERESTARTSYS。這個特殊的錯誤碼告訴核心如果可能的話應該重新啟動系統呼叫。這是對訊號的一種響應方式,允許使用者空間程式優雅地處理中斷。

  5. __set_current_state(TASK_RUNNING);
    在確認不是由於訊號喚醒後,將程序狀態設定回TASK_RUNNING,表示程序現在是活躍的,並準備繼續執行。

  6. remove_wait_queue(&dev->r_wait, &wait);
    最後,從等待佇列中移除當前程序。這是必要的清理步驟,以確保等待佇列只包含仍需要等待的程序。

接下來就是定時回撥函式,因為我們是按鍵按下經過定時器延時消抖。所以喚醒任務程序應該定義在定時器回撥函式中。

void timer_function(unsigned long arg)
{
	unsigned char value;
	unsigned char num;
	struct irq_keydesc *keydesc;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio); 	/* 讀取IO值 */
	if(value == 0){ 						/* 按下按鍵 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按鍵鬆開 */
		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
		atomic_set(&dev->releasekey, 1);	/* 標記鬆開按鍵,即完成一次完整的按鍵過程 */
	}               

	/* 喚醒程序 */
	if(atomic_read(&dev->releasekey)) {	/* 完成一次按鍵過程 */
		/* wake_up(&dev->r_wait); */
		wake_up_interruptible(&dev->r_wait);
	}
}

測試APP與上一節內容基本類似。

非阻塞IO實驗

非阻塞IO實驗只需要在阻塞IO實驗的基礎上稍微修改即可。需要再裝置讀函式中新增:

if (filp->f_flags & O_NONBLOCK)	{ /* 非阻塞訪問 */
	if(atomic_read(&dev->releasekey) == 0)	/* 沒有按鍵按下,返回-EAGAIN */
		return -EAGAIN;
} else {							/* 阻塞訪問 */
	/* 加入等待佇列,等待被喚醒,也就是有按鍵按下 */
	ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
	if (ret) {
		goto wait_error;
	}
}

與上面阻塞實驗編寫的阻塞函式略有不同,這裡用到的是等待事件函式。
在這裡插入圖片描述
在這裡插入圖片描述

區別分析:

讓我們更詳細地對比一下手動實現等待和喚醒的程式碼段與使用 wait_event_interruptible() 的差異。透過對每個步驟的詳細分析,我們可以更清楚地看到兩種方法的優缺點。

手動實現等待和喚醒

手動實現的程式碼段如下:

if (atomic_read(&dev->releasekey) == 0) {
    add_wait_queue(&dev->r_wait, &wait);
    __set_current_state(TASK_INTERRUPTIBLE);
    schedule();
    if (signal_pending(current)) {
        ret = -ERESTARTSYS;
        goto wait_error;
    }
}
詳細步驟分析
  1. 新增到等待佇列

    • 使用 add_wait_queue() 將當前程序新增到 dev->r_wait 等待佇列。這需要手動建立和初始化等待佇列元素 wait
  2. 設定程序狀態

    • 使用 __set_current_state(TASK_INTERRUPTIBLE) 直接設定程序的狀態。這種方式較為低階,直接操作程序狀態,可能導致競態條件,如果在設定狀態和呼叫 schedule() 之間發生中斷,可能會錯過喚醒事件。
  3. 排程其他任務

    • 呼叫 schedule() 讓出CPU,使當前程序掛起,直到被喚醒。
  4. 檢查喚醒原因

    • 檢查是否由訊號喚醒,如果是,則設定返回值 -ERESTARTSYS 並跳轉到錯誤處理程式碼。
缺點
  • 複雜性:需要手動管理等待佇列和程序狀態,容易出錯。
  • 競態條件:在設定程序狀態和排程之間可能發生競態條件。
  • 錯誤處理:需要手動編寫程式碼來處理訊號喚醒的情況。
使用 wait_event_interruptible() 的方法

wait_event_interruptible() 的典型用法如下:

if (wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey) != 0)) {
    return -ERESTARTSYS;
}
詳細步驟分析
  1. 等待條件

    • wait_event_interruptible() 將當前程序新增到等待佇列,並在給定的條件(這裡是 atomic_read(&dev->releasekey) != 0)不滿足時進入休眠。只有當條件滿足或接收到訊號時才會返回。
  2. 自動處理程序狀態和排程

    • 這個函式內部自動設定程序狀態為 TASK_INTERRUPTIBLE 並在必要時呼叫 schedule()。這減少了競態條件的風險。
  3. 自動錯誤處理

    • 如果由於訊號而喚醒,函式返回非零值,通常是 -ERESTARTSYS,這簡化了錯誤處理程式碼。

接下來就是非阻塞要用的poll函式。

 /*
  * @description     : poll函式,用於處理非阻塞訪問
  * @param - filp    : 要開啟的裝置檔案(檔案描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 裝置或者資源狀態,
  */
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

	poll_wait(filp, &dev->r_wait, wait);	/* 將等待佇列頭新增到poll_table中 */
	
	if(atomic_read(&dev->releasekey)) {		/* 按鍵按下 */
		mask = POLLIN | POLLRDNORM;			/* 返回PLLIN */
	}
	return mask;
}

接下來也要在裝置操作函式中關聯poll函式。

/* 裝置操作函式 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.poll = imx6uirq_poll,
};

接下來就是編寫測試APP函式。我們需要在測試APP中用poll函式去反覆查詢裝置是否可用。

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "poll.h"
#include "sys/select.h"
#include "sys/time.h"
#include "linux/ioctl.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
檔名		: noblockApp.c
作者	  	: 左忠凱
版本	   	: V1.0
描述	   	: 非阻塞訪問測試APP
其他	   	: 無
使用方法	:./blockApp /dev/blockio 開啟測試App
論壇 	   	: www.openedv.com
日誌	   	: 初版V1.0 2019/9/8 左忠凱建立
***************************************************************/

/*
 * @description		: main主程式
 * @param - argc 	: argv陣列元素個數
 * @param - argv 	: 具體引數
 * @return 			: 0 成功;其他 失敗
 */
int main(int argc, char *argv[])
{
	int fd;
	int ret = 0;
	char *filename;
	struct pollfd fds;
	fd_set readfds;
	struct timeval timeout;
	unsigned char data;

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR | O_NONBLOCK);	/* 非阻塞訪問 */
	if (fd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

#if 0
	/* 構造結構體 */
	fds.fd = fd;
	fds.events = POLLIN;
		
	while (1) {
		ret = poll(&fds, 1, 500);
		if (ret) {	/* 資料有效 */
			ret = read(fd, &data, sizeof(data));
			if(ret < 0) {
				/* 讀取錯誤 */
			} else {
				if(data)
					printf("key value = %d \r\n", data);
			} 	
		} else if (ret == 0) { 	/* 超時 */
			/* 使用者自定義超時處理 */
		} else if (ret < 0) {	/* 錯誤 */
			/* 使用者自定義錯誤處理 */
		}
	}
#endif

	while (1) {	
		FD_ZERO(&readfds);
		FD_SET(fd, &readfds);
		/* 構造超時時間 */
		timeout.tv_sec = 0;
		timeout.tv_usec = 500000; /* 500ms */
		ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
		switch (ret) {
			case 0: 	/* 超時 */
				/* 使用者自定義超時處理 */
				break;
			case -1:	/* 錯誤 */
				/* 使用者自定義錯誤處理 */
				break;
			default:  /* 可以讀取資料 */
				if(FD_ISSET(fd, &readfds)) {
					ret = read(fd, &data, sizeof(data));
					if (ret < 0) {
						/* 讀取錯誤 */
					} else {
						if (data)
							printf("key value=%d\r\n", data);
					}
				}
				break;
		}	
	}

	close(fd);
	return ret;
}

主要來看下while(1)裡面這段非阻塞處理的程式。
首先:FD_ZERO 宏用於初始化檔案描述符集,將其清空,確保沒有任何檔案描述符被設定在集合中。這是使用 select 函式前的必要步驟,因為 select 函式會檢查傳入的檔案描述符集來決定需要監聽哪些檔案描述符的狀態。如果不先清空檔案描述符集,那麼可能會包含一些無效或不想監聽的檔案描述符,從而導致不可預測的行為。
FD_SET 宏用於向檔案描述符集中新增一個特定的檔案描述符。這意味著您告訴 select 函式,您對這個特定的檔案描述符(fd)感興趣,希望監聽它的讀狀態(是否有資料可讀)。每次呼叫 FD_SET 都會將一個檔案描述符加入到集合中。
在使用 select 函式時,第一個引數需要特別注意,它是 nfds,這個引數指定了要檢查的檔案描述符集中的最高檔案描述符加一。這是因為 select 函式使用這個值來確定需要檢查的檔案描述符範圍,從檔案描述符 0 到 nfds-1。

相關文章