目錄
1.阻塞和非阻塞IO
1)阻塞和非阻塞簡介
2)等待隊列
3)輪詢
2.阻塞IO實驗
1)實驗程式編寫
2)運作測試
3.非阻塞IO實驗
1)實驗程式編寫
2)運作測試
阻塞和非阻塞IO是Linux驅動開發裡面很常見的兩種裝置通路模式,在編寫驅動的時候一定要考
慮到阻塞和非阻塞。本章我們就來學習一下阻塞和非阻塞IO,以及如何在驅動程式中處理阻塞
與非阻塞,如何在驅動程式使用等待隊列和poll機制。
1.阻塞和非阻塞IO
1)阻塞和非阻塞簡介
這裡的“IO”并不是我們學習STM32或者其他單片機的時候所說的“GPIO”(也就是引腳)。這
裡的IO指的是Input/Output,也就是輸入/輸出,是應用程式對驅動裝置的輸入/輸出操
作。當應用程式對裝置驅動進行操作的時候,如果不能擷取到裝置資源,那麼阻塞式IO就
會将應用程式對應的線程挂起,直到裝置資源可以擷取為止。對于非阻塞IO,應用程式對
應的線程不會挂起,它要麼一直輪詢等待,直到裝置資源可以使用,要麼就直接放棄。阻
塞式IO如圖1所示:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL6lkaNVTTq50MFpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL5kzM5UjNwgTMxMzNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
圖1 阻塞IO通路示意圖
圖1中應用程式調用read函數從裝置中讀取資料,當裝置不可用或資料未準備好的時候就
會進入到休眠态。等裝置可用的時候就會從休眠态喚醒,然後從裝置中讀取資料傳回給應
用程式。非阻塞IO如圖2所示:
圖2 非阻塞IO通路示意圖
從圖2可以看出,應用程式使用非阻塞通路方式從裝置讀取資料,當裝置不可用或資料未
準備好的時候會立即向核心傳回一個錯誤碼,表示資料讀取失敗。應用程式會再次重新讀
取資料,這樣一直往複循環,直到資料讀取成功。應用程式可以使用如下所示示例代碼來
實作阻塞通路:
1 int fd;
2 int data = 0;
3
4 fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打開 */
5 ret = read(fd, &data, sizeof(data)); /* 讀取資料 */
從以上代碼可以看出,對于裝置驅動檔案的預設讀取方式就是阻塞式的,是以我們前面所
有的例程測試APP都是采用阻塞IO。如果應用程式要采用非阻塞的方式來通路驅動裝置文
件,可以使用如下所示代碼:
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”,表
示以非阻塞方式打開裝置,這樣從裝置中讀取資料的時候就是非阻塞方式的了。
2)等待隊列
1、等待隊列頭
阻塞通路最大的好處就是當裝置檔案不可操作的時候程序可以進入休眠态,這樣可以
将CPU資源讓出來。但是,當裝置檔案可以操作的時候就必須喚醒程序,一般在中斷函
數裡面完成喚醒工作。Linux核心提供了等待隊列(wait queue)來實作阻塞程序的喚醒
工作,如果我們要在驅動中使用等待隊列,必須建立并初始化一個等待隊列頭,等待隊
列頭使用結構體wait_queue_head_t表示,wait_queue_head_t結構體定義在檔案
include/linux/wait.h中,結構體内容如下所示:
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表示等待隊列項,結構體内容如下:
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函數如圖3所示:
圖3 等待事件API函數
3)輪詢
如果使用者應用程式以非阻塞的方式通路裝置,裝置驅動程式就要提供非阻塞的處理方式,
也就是輪詢。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函數對某個裝置驅動檔案進行讀非阻塞通路的操作示例如下所示:
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函數對某個裝置驅動檔案進行讀非阻塞通路的操作示例如下所示:
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這兩個函數。
當應用程式調用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參數。
2.阻塞IO實驗
在上一章Linux中斷實驗中,我們直接在應用程式中通過read函數不斷的讀取按鍵狀态,當按
鍵有效的時候就列印出按鍵值。這種方法有個缺點,那就是imx6uirqApp這個測試應用程式擁
有很高的CPU占用率,大家可以在開發闆中加載上一章的驅動程式子產品imx6uirq.ko,然後以
背景運作模式打開imx6uirqApp這個測試軟體,指令如下:
./imx6uirqApp /dev/imx6uirq &
測試驅動是否正常工作,如果驅動工作正常的話輸入“top”指令檢視imx6uirqApp這個應用程式
的CPU使用率,結果如圖4所示:
圖4 CPU使用率
從圖4可以看出,imx6uirqApp這個應用程式的CPU使用率竟然高達99.6%,這僅僅是一個讀取
按鍵值的應用程式,這麼高的CPU使用率顯然是有問題的!原因就在于我們是直接在while循環
中通過read函數讀取按鍵值,是以imx6uirqApp這個軟體會一直運作,一直讀取按鍵值,CPU
使用率肯定就會很高。最好的方法就是在沒有有效的按鍵事件發生的時候,imx6uirqApp這個
應用程式應該處于休眠狀态,當有按鍵事件發生以後imx6uirqApp這個應用程式才運作,列印
出按鍵值,這樣就會降低CPU使用率,本小節我們就使用阻塞IO來實作此功能。
1)實驗程式編寫
1、驅動程式編寫
将“13_irq”實驗中的imx6uirq.c複制到14_blockio檔案夾中,并重命名為blockio.c。接下
來我們就修改blockio.c這個檔案,在其中添加阻塞相關的代碼,完成以後的blockio.c内
容如下所示(因為是在上一章實驗的imx6uirq.c檔案的基礎上修改的,為了減少篇幅,
下面的代碼有省略):
1 #include <linux/types.h>
2 #include <linux/kernel.h>
......
18 #include <asm/mach/map.h>
19 #include <asm/uaccess.h>
20 #include <asm/io.h>
21 /***************************************************************
22 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
23 檔案名 : block.c
24 作者 : 左忠凱
25 版本 : V1.0
26 描述 : 阻塞 IO 通路
27 其他 : 無
28 論壇 : www.openedv.com
29 日志 : 初版 V1.0 2019/7/26 左忠凱建立
30 ***************************************************************/
31 #define IMX6UIRQ_CNT 1 /* 裝置号個數 */
32 #define IMX6UIRQ_NAME "blockio" /* 名字 */
33 #define KEY0VALUE 0X01 /* KEY0 按鍵值 */
34 #define INVAKEY 0XFF /* 無效的按鍵值 */
35 #define KEY_NUM 1 /* 按鍵數量 */
36
37 /* 中斷 IO 描述結構體 */
38 struct irq_keydesc {
39 int gpio; /* gpio */
40 int irqnum; /* 中斷号 */
41 unsigned char value; /* 按鍵對應的鍵值 */
42 char name[10]; /* 名字 */
43 irqreturn_t (*handler)(int, void *); /* 中斷服務函數 */
44 };
45
46 /* imx6uirq 裝置結構體 */
47 struct imx6uirq_dev{
48 dev_t devid; /* 裝置号 */
49 struct cdev cdev; /* cdev */
50 struct class *class; /* 類 */
51 struct device *device; /* 裝置 */
52 int major; /* 主裝置号 */
53 int minor; /* 次裝置号 */
54 struct device_node *nd; /* 裝置節點 */
55 atomic_t keyvalue; /* 有效的按鍵鍵值 */
56 atomic_t releasekey; /* 标記是否完成一次完成的按鍵 */
57 struct timer_list timer; /* 定義一個定時器*/
58 struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按鍵 init 述數組 */
59 unsigned char curkeynum; /* 目前 init 按鍵号 */
60
61 wait_queue_head_t r_wait; /* 讀等待隊列頭 */
62 };
63
64 struct imx6uirq_dev imx6uirq; /* irq 裝置 */
65
66 /* @description : 中斷服務函數,開啟定時器
67 * 定時器用于按鍵消抖。
68 * @param - irq : 中斷号
69 * @param - dev_id : 裝置結構。
70 * @return : 中斷執行結果
71 */
72 static irqreturn_t key0_handler(int irq, void *dev_id)
73 {
74 struct imx6uirq_dev *dev = (struct imx6uirq_dev*)dev_id;
75
76 dev->curkeynum = 0;
77 dev->timer.data = (volatile long)dev_id;
78 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
79 return IRQ_RETVAL(IRQ_HANDLED);
80 }
81
82 /* @description : 定時器服務函數,用于按鍵消抖,定時器到了以後
83 * 再次讀取按鍵值,如果按鍵還是處于按下狀态就表示按鍵有效。
84 * @param - arg : 裝置結構變量
85 * @return : 無
86 */
87 void timer_function(unsigned long arg)
88 {
89 unsigned char value;
90 unsigned char num;
91 struct irq_keydesc *keydesc;
92 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
93
94 num = dev->curkeynum;
95 keydesc = &dev->irqkeydesc[num];
96
97 value = gpio_get_value(keydesc->gpio); /* 讀取 IO 值 */
98 if(value == 0){ /* 按下按鍵 */
99 atomic_set(&dev->keyvalue, keydesc->value);
100 }
101 else{ /* 按鍵松開 */
102 atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
103 atomic_set(&dev->releasekey, 1);
104 }
105
106 /* 喚醒程序 */
107 if(atomic_read(&dev->releasekey)) { /* 完成一次按鍵過程 */
108 /* wake_up(&dev->r_wait); */
109 wake_up_interruptible(&dev->r_wait);
110 }
111 }
112
113 /*
114 * @description : 按鍵 IO 初始化
115 * @param : 無
116 * @return : 無
117 */
118 static int keyio_init(void)
119 {
120 unsigned char i = 0;
121 char name[10];
122 int ret = 0;
......
163 /* 建立定時器 */
164 init_timer(&imx6uirq.timer);
165 imx6uirq.timer.function = timer_function;
166
167 /* 初始化等待隊列頭 */
168 init_waitqueue_head(&imx6uirq.r_wait);
169 return 0;
170 }
171
172 /*
173 * @description : 打開裝置
174 * @param – inode : 傳遞給驅動的 inode
175 * @param - filp : 裝置檔案,file 結構體有個叫做 private_data 的成員變量
176 * 一般在 open 的時候将 private_data 指向裝置結構體。
177 * @return : 0 成功;其他 失敗
178 */
179 static int imx6uirq_open(struct inode *inode, struct file *filp)
180 {
181 filp->private_data = &imx6uirq; /* 設定私有資料 */
182 return 0;
183 }
184
185 /*
186 * @description : 從裝置讀取資料
187 * @param - filp : 要打開的裝置檔案(檔案描述符)
188 * @param - buf : 傳回給使用者空間的資料緩沖區
189 * @param - cnt : 要讀取的資料長度
190 * @param - offt : 相對于檔案首位址的偏移
191 * @return : 讀取的位元組數,如果為負值,表示讀取失敗
192 */
193 static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
194 {
195 int ret = 0;
196 unsigned char keyvalue = 0;
197 unsigned char releasekey = 0;
198 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
199
200 #if 0
201 /* 加入等待隊列,等待被喚醒, 也就是有按鍵按下 */
202 ret = wait_event_interruptible(dev->r_wait,atomic_read(&dev->releasekey));
203 if (ret) {
204 goto wait_error;
205 }
206 #endif
207
208 DECLARE_WAITQUEUE(wait, current); /* 定義一個等待隊列 */
209 if(atomic_read(&dev->releasekey) == 0) { /* 沒有按鍵按下 */
210 add_wait_queue(&dev->r_wait, &wait); /* 添加到等待隊列頭 */
211 __set_current_state(TASK_INTERRUPTIBLE);/* 設定任務狀态 */
212 schedule(); /* 進行一次任務切換 */
213 if(signal_pending(current)) { /* 判斷是否為信号引起的喚醒 */
214 ret = -ERESTARTSYS;
215 goto wait_error;
216 }
217 __set_current_state(TASK_RUNNING); /* 設定為運作狀态 */
218 remove_wait_queue(&dev->r_wait, &wait); /* 将等待隊列移除 */
219 }
220 keyvalue = atomic_read(&dev->keyvalue);
221 releasekey = atomic_read(&dev->releasekey);
......
234 return 0;
235
236 wait_error:
237 set_current_state(TASK_RUNNING); /* 設定任務為運作态 */
238 remove_wait_queue(&dev->r_wait, &wait); /* 将等待隊列移除 */
239 return ret;
240
241 data_error:
242 return -EINVAL;
243 }
244
245 /* 裝置操作函數 */
246 static struct file_operations imx6uirq_fops = {
247 .owner = THIS_MODULE,
248 .open = imx6uirq_open,
249 .read = imx6uirq_read,
250 };
251
252 /*
253 * @description : 驅動入口函數
254 * @param : 無
255 * @return : 無
256 */
257 static int __init imx6uirq_init(void)
258 {
259 /* 1、建構裝置号 */
260 if (imx6uirq.major) {
261 imx6uirq.devid = MKDEV(imx6uirq.major, 0);
262 register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT,IMX6UIRQ_NAME);
263 } else {
264 alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT,IMX6UIRQ_NAME);
265 imx6uirq.major = MAJOR(imx6uirq.devid);
266 imx6uirq.minor = MINOR(imx6uirq.devid);
267 }
......
284
285 /* 5、始化按鍵 */
286 atomic_set(&imx6uirq.keyvalue, INVAKEY);
287 atomic_set(&imx6uirq.releasekey, 0);
288 keyio_init();
289 return 0;
290 }
291
292 /*
293 * @description : 驅動出口函數
294 * @param : 無
295 * @return : 無
296 */
297 static void __exit imx6uirq_exit(void)
298 {
299 unsigned i = 0;
300 /* 删除定時器 */
301 del_timer_sync(&imx6uirq.timer); /* 删除定時器 */
......
310 class_destroy(imx6uirq.class);
311 }
312
313 module_init(imx6uirq_init);
314 module_exit(imx6uirq_exit);
315 MODULE_LICENSE("GPL");
第32行,修改裝置檔案名字為“blockio”,當驅動程式加載成功以後就會在根檔案系統
中出現一個名為“/dev/blockio”的檔案。
第61行,在裝置結構體中添加一個等待隊列頭r_wait,因為在Linux驅動中處理阻塞IO
需要用到等待隊列。
第107~110行,定時器中斷處理函數執行,表示有按鍵按下,先在107行判斷一下是否
是一次有效的按鍵,如果是的話就通過wake_up或者
wake_up_interruptible函數來喚醒等待隊列r_wait。
第168行,調用init_waitqueue_head函數初始化等待隊列頭r_wait。
第200~206行,采用等待事件來處理read的阻塞通路,wait_event_interruptible函數等
待releasekey有效,也就是有按鍵按下。如果按鍵沒有按下的話程序就
會進入休眠狀态,因為采用了wait_event_interruptible函數,是以進入
休眠态的程序可以被信号打斷。
第208~218行,首先使用DECLARE_WAITQUEUE宏定義一個等待隊列,如果沒有按
鍵按下的話就使用add_wait_queue函數将目前任務的等待隊列添加到
等待隊列頭r_wait中。随後調用__set_current_state函數設定目前進
程的狀态為TASK_INTERRUPTIBLE,也就是可以被信号打斷。接下
來調用schedule函數進行一次任務切換,目前程序就會進入到休眠
态。如果有按鍵按下,那麼進入休眠态的程序就會喚醒,然後接着從
休眠點開始運作。在這裡也就是從第213行開始運作,首先通過
signal_pending函數判斷一下程序是不是由信号喚醒的,如果是由信
号喚醒的話就直接傳回-ERESTARTSYS這個錯誤碼。如果不是由信号
喚醒的(也就是被按鍵喚醒的)那麼就在217行調用__set_current_state
函數将任務狀态設定為TASK_RUNNING,然後在218行調用
remove_wait_queue函數将程序從等待隊列中删除。使用等待隊列實
現阻塞通路重點注意兩點:
1、将任務或者程序加入到等待隊列頭;
2、在合适的點喚醒等待隊列,一般都是中斷處理函數裡面。
2、編寫測試
APP本節實驗的測試APP直接使用之前所編寫的imx6uirqApp.c,将imx6uirqApp.c複
制到本節實驗檔案夾下,并且重命名為blockioApp.c,不需要修改任何内容。
2)運作測試
1、編譯驅動程式和測試APP
1.編譯驅動程式
編寫Makefile檔案,本章實驗的Makefile檔案和之前實驗基本一樣,隻是将obj -m變
量的值改為blockio.o,Makefile内容如下所示:
1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-
rel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := blockio.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第4行,設定obj-m變量的值為blockio.o。
輸入如下指令編譯出驅動子產品檔案:
make -j32
編譯成功以後就會生成一個名為“blockio.ko”的驅動子產品檔案。
2.編譯測試APP
輸入如下指令編譯測試noblockioApp.c這個測試程式:
arm-linux-gnueabihf-gcc blockioApp.c -o blockioApp
編譯成功以後就會生成blcokioApp這個應用程式。
2、運作測試
将上一小節編譯出來blockio.ko和blockioApp這兩個檔案拷貝到
rootfs/lib/modules/4.1.15目錄中,重新開機開發闆,進入到目錄lib/modules/4.1.15中,輸
入如下指令加載blockio.ko驅動子產品:
depmod //第一次加載驅動的時候需要運作此指令
modprobe blockio.ko //加載驅動
驅動加載成功以後使用如下指令打開blockioApp這個測試APP,并且以背景模式運
行:
./blockioApp /dev/blockio &
按下開發闆上的KEY0按鍵,結果如圖5所示:
圖5 測試APP運作測試
當按下KEY0按鍵以後blockioApp這個測試APP就會列印出按鍵值。輸入“top”指令,查
看blockioAPP這個應用APP的CPU使用率,如圖5所示:
圖5 應用程式CPU使用率
從圖5可以看出,當我們在按鍵驅動程式裡面加入阻塞通路以後,blockioApp這個應用
程式的CPU使用率從圖5中的99.6%降低到了0.0%。大家注意,這裡的0.0%并不是說
blockioApp這個應用程式不使用CPU了,隻是因為使用率太小了,CPU使用率可能為
0.00001%,但是圖5隻能顯示出小數點後一位,是以就顯示成了0.0%。
我們可以使用“kill”指令關閉背景運作的應用程式,比如我們關閉掉blockioApp這個後
台運作的應用程式。首先輸出“Ctrl+C”關閉top指令界面,進入到指令行模式。然後使
用“ps”指令檢視一下blockioApp這個應用程式的PID,如圖6所示:
圖6 目前系統所有程序的ID
從圖6可以看出,blockioApp這個應用程式的PID為149,使用“kill -9 PID”即可“殺死”指
定PID的程序,比如我們現在要“殺死”PID為149的blockioApp應用程式,可是使用如下
指令:
kill -9 149
輸入上述指令以後終端顯示如圖7所示:
圖7 kill指令輸出結果
從圖7可以看出,“./blockioApp/dev/blockio”這個應用程式已經被“殺掉”了,在此輸入
“ps”指令檢視目前系統運作的程序,會發現blockioApp已經不見了。這個就是使用kill
指令“殺掉”指定程序的方法。
3.非阻塞IO實驗
1)實驗程式編寫
1、驅動程式編寫
本章實驗我們在之前的“14_blockio”實驗的基礎上完成,上一小節實驗我們已經在驅動
中添加了阻塞IO的代碼,本小節我們繼續完善驅動,加入非阻塞IO驅動代碼。建立名
為“15_noblockio”的檔案夾,然後在15_noblockio檔案夾裡面建立vscode工程,工作
區命名為“noblockio”。将“14_blockio”實驗中的blockio.c複制到15_noblockio檔案夾
中,并重命名為noblockio.c。接下來我們就修改noblockio.c這個檔案,在其中添加非
阻塞相關的代碼,完成以後的noblockio.c内容如下所示(因為是在上一小節實驗的
blockio.c檔案的基礎上修改的,為了減少篇幅,下面的代碼有省略):
1 #include <linux/types.h>
2 #include <linux/kernel.h>
......
18 #include <linux/wait.h>
19 #include <linux/poll.h>
20 #include <asm/mach/map.h>
21 #include <asm/uaccess.h>
22 #include <asm/io.h>
23 /***************************************************************
24 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
25 檔案名 : noblock.c
26 作者 : 左忠凱
27 版本 : V1.0
28 描述 : 非阻塞 IO 通路
29 其他 : 無
30 論壇 : www.openedv.com
31 日志 : 初版 V1.0 2019/7/26 左忠凱建立
32 ***************************************************************/
31 #define IMX6UIRQ_CNT 1 /* 裝置号個數 */
32 #define IMX6UIRQ_NAME "noblockio" /* 名字 */
......
187 /*
188 * @description : 從裝置讀取資料
189 * @param - filp : 要打開的裝置檔案(檔案描述符)
190 * @param - buf : 傳回給使用者空間的資料緩沖區
191 * @param - cnt : 要讀取的資料長度
192 * @param - offt : 相對于檔案首位址的偏移
193 * @return : 讀取的位元組數,如果為負值,表示讀取失敗
194 */
195 static ssize_t imx6uirq_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
196 {
197 int ret = 0;
198 unsigned char keyvalue = 0;
199 unsigned char releasekey = 0;
200 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
201
202 if (filp->f_flags & O_NONBLOCK) { /* 非阻塞通路 */
203 if(atomic_read(&dev->releasekey) == 0) /* 沒有按鍵按下 */
204 return -EAGAIN;
205 } else { /* 阻塞通路 */
206 /* 加入等待隊列,等待被喚醒,也就是有按鍵按下 */
207 ret = wait_event_interruptible(dev->r_wait,atomic_read(&dev->releasekey));
208 if (ret) {
209 goto wait_error;
210 }
211 }
......
229 wait_error:
230 return ret;
231 data_error:
232 return -EINVAL;
233 }
234
235 /*
236 * @description : poll 函數,用于處理非阻塞通路
237 * @param - filp : 要打開的裝置檔案(檔案描述符)
238 * @param - wait : 等待清單(poll_table)
239 * @return : 裝置或者資源狀态,
240 */
241 unsigned int imx6uirq_poll(struct file *filp,struct poll_table_struct *wait)
242 {
243 unsigned int mask = 0;
244 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
245
246 poll_wait(filp, &dev->r_wait, wait);
247
248 if(atomic_read(&dev->releasekey)) { /* 按鍵按下 */
249 mask = POLLIN | POLLRDNORM; /* 傳回 PLLIN */
250 }
251 return mask;
252 }
253
254 /* 裝置操作函數 */
255 static struct file_operations imx6uirq_fops = {
256 .owner = THIS_MODULE,
257 .open = imx6uirq_open,
258 .read = imx6uirq_read,
259 .poll = imx6uirq_poll,
260 };
261
262 /*
263 * @description : 驅動入口函數
264 * @param : 無
265 * @return : 無
266 */
267 static int __init imx6uirq_init(void)
268 {
......
298 keyio_init();
299 return 0;
300 }
301
302 /*
303 * @description : 驅動出口函數
304 * @param : 無
305 * @return : 無
306 */
307 static void __exit imx6uirq_exit(void)
308 {
309 unsigned i = 0;
310 /* 删除定時器 */
311 del_timer_sync(&imx6uirq.timer); /* 删除定時器 */
......
320 class_destroy(imx6uirq.class);
321 }
322
323 module_init(imx6uirq_init);
324 module_exit(imx6uirq_exit);
325 MODULE_LICENSE("GPL");
第32行,修改裝置檔案名字為“noblockio”,當驅動程式加載成功以後就會在根檔案系
統中出現一個名為“/dev/noblockio”的檔案。
第202~204行,判斷是否為非阻塞式讀取通路,如果是的話就判斷按鍵是否有效,也
就是判斷一下有沒有按鍵按下,如果沒有的話就傳回-EAGAIN。
第241~252行,imx6uirq_poll函數就是file_operations驅動操作集中的poll函數,當應
用程式調用select或者poll函數的時候imx6uirq_poll函數就會執行。
第246行調用poll_wait函數将等待隊列頭添加到poll_table中,第
248~250行判斷按鍵是否有效,如果按鍵有效的話就向應用程式傳回
POLLIN這個事件,表示有資料可以讀取。
第259行,設定file_operations的poll成員變量為imx6uirq_poll。
2、編寫測試APP
建立名為noblockioApp.c測試APP檔案,然後在其中輸入如下所示内容:
1 #include "stdio.h"
2 #include "unistd.h"
3 #include "sys/types.h"
4 #include "sys/stat.h"
5 #include "fcntl.h"
6 #include "stdlib.h"
7 #include "string.h"
8 #include "poll.h"
9 #include "sys/select.h"
10 #include "sys/time.h"
11 #include "linux/ioctl.h"
12 /***************************************************************
13 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
14 檔案名 : noblockApp.c
15 作者 : 左忠凱
16 版本 : V1.0
17 描述 : 非阻塞通路測試 APP
18 其他 : 無
19 使用方法 :./blockApp /dev/blockio 打開測試 App
20 論壇 : www.openedv.com
21 日志 : 初版 V1.0 2019/9/8 左忠凱建立
22 ***************************************************************/
23
24 /*
25 * @description : main 主程式
26 * @param - argc : argv 數組元素個數
27 * @param - argv : 具體參數
28 * @return : 0 成功;其他 失敗
29 */
30 int main(int argc, char *argv[])
31 {
32 int fd;
33 int ret = 0;
34 char *filename;
35 struct pollfd fds;
36 fd_set readfds;
37 struct timeval timeout;
38 unsigned char data;
39
40 if (argc != 2) {
41 printf("Error Usage!\r\n");
42 return -1;
43 }
44
45 filename = argv[1];
46 fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞通路 */
47 if (fd < 0) {
48 printf("Can't open file %s\r\n", filename);
49 return -1;
50 }
51
52 #if 0
53 /* 構造結構體 */
54 fds.fd = fd;
55 fds.events = POLLIN;
56
57 while (1) {
58 ret = poll(&fds, 1, 500);
59 if (ret) { /* 資料有效 */
60 ret = read(fd, &data, sizeof(data));
61 if(ret < 0) {
62 /* 讀取錯誤 */
63 } else {
64 if(data)
65 printf("key value = %d \r\n", data);
66 }
67 } else if (ret == 0) { /* 逾時 */
68 /* 使用者自定義逾時處理 */
69 } else if (ret < 0) { /* 錯誤 */
70 /* 使用者自定義錯誤處理 */
71 }
72 }
73 #endif
74
75 while (1) {
76 FD_ZERO(&readfds);
77 FD_SET(fd, &readfds);
78 /* 構造逾時時間 */
79 timeout.tv_sec = 0;
80 timeout.tv_usec = 500000; /* 500ms */
81 ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
82 switch (ret) {
83 case 0: /* 逾時 */
84 /* 使用者自定義逾時處理 */
85 break;
86 case -1: /* 錯誤 */
87 /* 使用者自定義錯誤處理 */
88 break;
89 default: /* 可以讀取資料 */
90 if(FD_ISSET(fd, &readfds)) {
91 ret = read(fd, &data, sizeof(data));
92 if (ret < 0) {
93 /* 讀取錯誤 */
94 } else {
95 if (data)
96 printf("key value=%d\r\n", data);
97 }
98 }
99 break;
100 }
101 }
102
103 close(fd);
104 return ret;
105 }
第52~73行,這段代碼使用poll函數來實作非阻塞通路,在while循環中使用poll函數不
斷的輪詢,檢查驅動程式是否有資料可以讀取,如果可以讀取的話就調
用read函數讀取按鍵資料。
第75~101行,這段代碼使用select函數來實作非阻塞通路。
2)運作測試
1、編譯驅動程式和測試APP
1.編譯驅動程式
編寫Makefile檔案,本章實驗的Makefile檔案和之前實驗基本一樣,隻是将obj-m變
量的值改為noblockio.o,Makefile内容如下所示:
KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-
rel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := no blockio.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第4行,設定obj-m變量的值為noblockio.o。
輸入如下指令編譯出驅動子產品檔案:
make -j32
編譯成功以後就會生成一個名為“noblockio.ko”的驅動子產品檔案。
2.編譯測試APP
輸入如下指令編譯測試noblockioApp.c這個測試程式:
arm-linux-gnueabihf-gcc noblockioApp.c -o noblockioApp
編譯成功以後就會生成noblcokioApp這個應用程式。
2、運作測試
将上一小節編譯出來noblockio.ko和noblockioApp這兩個檔案拷貝到
rootfs/lib/modules/4.1.15目錄中,重新開機開發闆,進入到目錄lib/modules/4.1.15中,輸
入如下指令加載blockio.ko驅動子產品:
depmod //第一次加載驅動的時候需要運作此指令
modprobe noblockio.ko //加載驅動
驅動加載成功以後使用如下指令打開noblockioApp這個測試APP,并且以背景模式運
行:
./noblockioApp /dev/noblockio &
按下開發闆上的KEY0按鍵,結果如圖8所示:
圖8 測試APP運作測試
當按下KEY0按鍵以後noblockioApp這個測試APP就會列印出按鍵值。輸入“top”命
令,檢視noblockioAPP這個應用APP的CPU使用率,如圖9所示:
圖9 應用程式CPU使用率
從圖9可以看出,采用非阻塞方式讀處理以後,noblockioApp的CPU占用率也低至
0.0%,和之前中的blockioApp一樣,這裡的0.0%并不是說noblockioApp這個應用程
序不使用CPU了,隻是因為使用率太小了,而圖中隻能顯示出小數點後一位,是以就
顯示成了0.0%。如果要“殺掉”處于背景運作模式的noblockioApp這個應用程式,可以
參考之前講解的方法。