【嵌入式Linux驅動開發】十六、Linux中的阻塞IO與非阻塞IO

   有志不在年高,羅成少年統兵馬。
  得道何須深山,老君立地曉陰陽


一、阻塞和非阻塞IO

  阻塞和非阻塞 IO 是 Linux 驅動開發裏面很常見的兩種設備訪問模式。需要強調的是,這裏的 IO 指的是 Input/Output,也就是輸入/輸出,是應用程序對驅動設備的輸入/輸出操作。應用程序對設備驅動進行操作的時候,如果不能獲取到設備資源時,阻塞IO和非阻塞IO動作是不同的,具體動作如下所示:

  • 阻塞式 IO 就會將應用程序對應的線程掛起,直到設備資源可以獲取爲止
    • 阻塞方式讀IO:open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打開 */
    • 阻塞訪問最大的好處就是當設備文件不可操作的時候進程可以進入休眠態,這樣可以將CPU 資源讓出來。
  • 非阻塞 IO,應用程序對應的線程不會掛起,它要麼一直輪詢等待,直到設備資源可以使用,要麼就直接放棄。
    • 非阻塞方式讀IO:open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打開 */

1.1 等待隊列 - 阻塞IO

  阻塞訪問最大的好處就是當設備文件不可操作的時候進程可以進入休眠態,這樣可以將CPU 資源讓出來。但是,當設備文件可以操作的時候就必須喚醒進程,一般在中斷函數裏面完成喚醒工作。Linux 內核提供了等待隊列(wait queue)來實現阻塞進程的喚醒工作

  • 等待隊列頭

  如果我們要在驅動中使用等待隊列,必須創建並初始化一個等待隊列頭,等待隊列頭使用結構體 wait_queue_head_t 定義。定義好等待隊列頭以後需要初始化, 使用 init_waitqueue_head 函數初始化等待隊列頭。

當然了,也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 來一次性完成等待隊列頭的定義的初始化。

  • 等待隊列項

  等待隊列頭就是一個等待隊列的頭部,每個訪問設備的進程都是一個隊列項,當設備不可用的時候就要將這些進程對應的等待隊列項添加到等待隊列裏面。使用結構體 wait_queue_t 定義等待隊列項,然後使用 init_waitqueue_head 函數初始化等待隊列頭

使用宏 DECLARE_WAITQUEUE(name, tsk) 定義並初始化一個等待隊列項。
其中,name 就是等待隊列項的名字, tsk 表示這個等待隊列項屬於哪個任務(進程),,一般設置爲current , 在 Linux 內 核 中 current 相 當 於 一 個 全 局 變 量 , 表 示 當 前 進 程 。 因 此 宏DECLARE_WAITQUEUE 就是給當前正在運行的進程創建並初始化了一個等待隊列項。

  • 將隊列項添加/移除等待隊列頭

  當設備不可訪問的時候就需要將進程對應的等待隊列項添加到前面創建的等待隊列頭中,只有添加到等待隊列頭中以後進程才能進入休眠態。當設備可以訪問以後再將進程對應的等待隊列項從等待隊列頭中移除即可,等待隊列項添加/移除 API 函數如下:

//q: 等待隊列項要加入的等待隊列頭,wait:要加入的等待隊列項。
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)
  • 等待喚醒

  當設備可以使用的時候就要喚醒進入休眠態的進程,喚醒可以使用如下兩個函數,這兩個函數會將這個等待隊列頭中的所有進程都喚醒。wake_up 函數可以喚醒處於 TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE 狀態的進程,而 wake_up_interruptible 函數只能喚醒處於TASK_INTERRUPTIBLE 狀態的進程。

//參數 q 就是要喚醒的等待隊列頭
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
  • 等待事件

  除了等待喚醒以外,也可以設置等待隊列等待某個事件,當這個事件滿足以後就自動喚醒等待隊列中的進程。

函數 描述
wait_event(wq, condition) 等待以 wq 爲等待隊列頭的等待隊列被喚醒,前提是 condition 條件必須滿足(爲真),否則一直阻塞 。 此 函 數 會 將 進 程 設 置 爲TASK_UNINTERRUPTIBLE 狀態
wait_event_timeout(wq, condition, timeout) 功能和 wait_event 類似,但是此函數可以添加超時時間,以 jiffies 爲單位。此函數有返回值,如果返回 0 的話表示超時時間到,而且 condition爲假。爲 1 的話表示 condition 爲真,也就是條件滿足了。
wait_event_interruptible(wq, condition) 與 wait_event 函數類似,但是此函數將進程設置爲 TASK_INTERRUPTIBLE,就是可以被信號打斷。
wait_event_interruptible_timeout(wq,condition, timeout) 與 wait_event_timeout 函數類似,此函數也將進程設置爲 TASK_INTERRUPTIBLE,可以被信號打斷。

1.2 輪詢 - 非阻塞IO

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

1.2.1 非阻塞訪問 - select函數

函數原型:

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

nfds

  • 要操作的文件描述符個數。

readfds、 writefds 和 exceptfds

  • 這三個指針指向描述符集合( fd_set 類型)
  • fd_set 類型變量的每一個位都代表了一個文件描述符。 【下面有介紹如何添加描述到描述符集fd_set中】
  • readfds - 監視指定描述符集的讀變化(是否可讀)。
    • 若可讀select返回大於0的值;若不可讀根據timeout參數判斷是否超時;若設置readfds爲NULL則表示不關係是否可讀。
  • writefds - 監視指定描述符集的寫變化(是否可寫)。
  • exceptfds - 監視指定描述符集的異常。

timeout

  • 超時時間,當我們調用 select 函數等待某些文件描述符可以設置超時時間,超時時間使用結構體 timeval 表示。
  • 當 timeout 爲 NULL 的時候就表示無限期的等待。
  • 結構體定義如下所示:
struct timeval {
	long tv_sec; /* 秒 */
	long tv_usec; /* 微妙 */
};

select返回值

  • 0,表示超時,但是沒有任何文件描述符可以進行操作;
  • -1,表示發生錯誤;
  • 其他值,表示可以進行操作的文件描述符個數。

fd_set

  • 若要從一個設備文件中讀取數據,那麼就可以定義一個 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_set 的某個位是否置 1,也就是判斷某個文件是否可以進行操作,參數 fd 就是要判斷的文件描述符

select 函數讀非阻塞訪問示例

void main(void)
{
	int ret, fd; /* 要監視的文件描述符 */
	fd_set readfds; /* 讀操作文件描述符集 */
	struct timeval timeout; /* 超時結構體 */
	
	fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */
	
	FD_ZERO(&readfds); /* 清除 readfds */
	FD_SET(fd, &readfds); /* 將 fd 添加到 readfds 裏面 */
	
	/* 構造超時時間 */
	timeout.tv_sec = 0;
	timeout.tv_usec = 500000; /* 500ms */
	
	ret = select(fd + 1, &readfds, NULL, NULL, &timeout);//1個文件描述符,只檢測讀變化
	switch (ret) {
		case 0: /* 超時 */
			printf("timeout!\r\n");
			break;
		case -1: /* 錯誤 */
			printf("error!\r\n");
			break;
		default: /* 可以讀取數據 */
			if(FD_ISSET(fd, &readfds)) { /* 判斷是否爲 fd 文件描述符 */
				/* 使用 read 函數讀取數據 */
			}
			break;
	}
}

1.2.1 非阻塞訪問 - poll函數

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

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

fds

  • 要監視的文件描述符集合以及要監視的事件,類型是一個pollfd類型的結構體數組
  • pollfd 結構體如下所示,參數說明如下
    • fd 是要監視的文件描述符,如果 fd 無效的話那麼 events 監視事件也就無效,並且 revents返回 0。
    • events 是要監視的事件,可監視的事件類型如下表。
    • revents 是返回的事件,由 Linux 內核設置具體的返回事件
struct pollfd {
	int fd; /* 文件描述符 */
	short events; /* 請求的事件 */
	short revents; /* 返回的事件 */
};
事件類型 描述
POLLIN 有數據可以讀取。
POLLPRI 有緊急的數據需要讀取。
POLLOUT 可以寫數據。
POLLERR 指定的文件描述符發生錯誤。
POLLHUP 指定的文件描述符掛起。
POLLNVAL 無效的請求。
POLLRDNORM 等同於 POLLIN

nfds

  • poll 函數要監視的文件描述符數量。

timeout

  • 超時時間,單位爲 ms。

poll返回值

  • 0,表示超時發生
  • -1,表示發生錯誤,並且設置 errno 爲錯誤類型。
  • 其他值,表示revents 域中不爲 0 的 pollfd 結構體個數

poll 函數讀非阻塞訪問示例

void main(void)
{
	int ret;
	int fd; /* 要監視的文件描述符 */
	struct pollfd fds;
	
	fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */
	
	/* 構造結構體 */
	fds.fd = fd;
	fds.events = POLLIN; /* 監視數據是否可以讀取 */
	ret = poll(&fds, 1, 500); /* 輪詢文件是否可操作,超時 500ms */
	if (ret) { /* 數據有效 */
		......
		/* 讀取數據 */
		......
	} else if (ret == 0) { /* 超時 */
		......
	} else if (ret < 0) { /* 錯誤 */
		......
	}
}

1.2.1 非阻塞訪問 - epoll函數

  傳統的 selcet 和 poll 函數都會隨着所監聽的 fd 數量的增加,出現效率低下的問題,而且poll 函數每次必須遍歷所有的描述符來檢查就緒的描述符,這個過程很浪費時間。爲此, epoll因運而生, epoll 就是爲處理大併發而準備的,一般常常在網絡編程中使用 epoll 函數。

使用流程:epoll_create -> epoll_ctl -> epoll_wait

  應用程序需要先使用 epoll_create 函數創建一個 epoll 句柄, epoll_create 函數原型如下:

int epoll_create(int size)

size

  • 從 Linux2.6.8 開始此參數已經沒有意義了,隨便填寫一個大於 0 的值就可以。
  • 返回值: epoll 句柄,如果爲-1 的話表示創建失敗。

  poll 句柄創建成功以後使用 epoll_ctl 函數向其中添加要監視的文件描述符以及監視的事件, epoll_ctl 函數原型如下所示:

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

epfd

  • 要操作的 epoll 句柄,也就是使用 epoll_create 函數創建的 epoll 句柄。

op

  • 表示要對epoll 句柄(epfd)進行的操作,可以設置的參數如下表所示。
參數 含義
EPOLL_CTL_ADD 向 epfd 添加文件參數 fd 表示的描述符。
EPOLL_CTL_MOD 修改參數 fd 的 event 事件。
EPOLL_CTL_DEL 從 epfd 中刪除 fd 描述符。

fd

  • 要監視的文件描述符。

event

  • 要監視的事件類型,爲 epoll_event 結構體類型指針, epoll_event 結構體類型如下所示。
  • 結構體 epoll_event 的 events 成員變量表示要監視的事件,可選的事件如下表所示,這些事件可以進行“或”操作,也就是說可以設置監視多個事件。
struct epoll_event {
	uint32_t events; /* epoll 事件 */
	epoll_data_t data; /* 用戶數據 */
};
參數 含義
EPOLLIN 有數據可以讀取。
EPOLLOUT 可以寫數據。
EPOLLPRI 有緊急的數據需要讀取。
EPOLLERR 指定的文件描述符發生錯誤。
EPOLLHUP 指定的文件描述符掛起。
EPOLLET 設置 epoll 爲邊沿觸發,默認觸發模式爲水平觸發。
EPOLLONESHOT 一次性的監視,當監視完成以後還需要再次監視某個 fd,那麼就需要將fd 重新添加到 epoll 裏面。

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,初學先了解select和poll函數即可!

1.3 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,普通數據可讀

二、阻塞IO實驗

  在上一節第一個按鍵實驗中,我們直接在應用程序中通過 read 函數不斷的讀取按鍵狀態,當按鍵有效的時候就打印出按鍵值。這種方法有個缺點,那就是 imx6uirqApp 這個測試應用程序擁有高達99.6% CPU 佔用率。

  原因就在於我們是直接在 while 循環中通過 read 函數讀取按鍵值,因此 imx6uirqApp 這個軟件會一直運行,一直讀取按鍵值, CPU 使用率肯定就會很高。最好的方法就是在沒有有效的按鍵事件發生的時候,imx6uirqApp 這個應用程序應該處於休眠狀態,當有按鍵事件發生以後 imx6uirqApp 這個應用程序才運行,打印出按鍵值,這樣就會降低 CPU 使用率,本小節我們就使用阻塞 IO 來實現此功能。

驅動程序

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: block.c
作者	  	: 左忠凱
版本	   	: V1.0
描述	   	: 阻塞IO訪問
其他	   	: 無
論壇 	   	: www.openedv.com
日誌	   	: 初版V1.0 2019/7/26 左忠凱創建
***************************************************************/
#define IMX6UIRQ_CNT		1			/* 設備號個數 	*/
#define IMX6UIRQ_NAME		"blockio"	/* 名字 		*/
#define KEY0VALUE			0X01		/* KEY0按鍵值 	*/
#define INVAKEY				0XFF		/* 無效的按鍵值 */
#define KEY_NUM				1			/* 按鍵數量 	*/

/* 中斷IO描述結構體 */
struct irq_keydesc {
	int gpio;								/* gpio */
	int irqnum;								/* 中斷號     */
	unsigned char value;					/* 按鍵對應的鍵值 */
	char name[10];							/* 名字 */
	irqreturn_t (*handler)(int, void *);	/* 中斷服務函數 */
};

/* 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;	/* 讀等待隊列頭 */
};

struct imx6uirq_dev imx6uirq;	/* irq設備 */

/* @description		: 中斷服務函數,開啓定時器		
 *				  	  定時器用於按鍵消抖。
 * @param - irq 	: 中斷號 
 * @param - dev_id	: 設備結構。
 * @return 			: 中斷執行結果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev*)dev_id;

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定時 */
	return IRQ_RETVAL(IRQ_HANDLED);
}

/* @description	: 定時器服務函數,用於按鍵消抖,定時器到了以後
 *				  再次讀取按鍵值,如果按鍵還是處於按下狀態就表示按鍵有效。
 * @param - arg	: 設備結構變量
 * @return 		: 無
 */
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);
	}
}

/*
 * @description	: 按鍵IO初始化
 * @param 		: 無
 * @return 		: 無
 */
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;
}

/*
 * @description		: 打開設備
 * @param - inode 	: 傳遞給驅動的inode
 * @param - filp 	: 設備文件,file結構體有個叫做private_data的成員變量
 * 					  一般在open的時候將private_data指向設備結構體。
 * @return 			: 0 成功;其他 失敗
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq;	/* 設置私有數據 */
	return 0;
}

 /*
  * @description     : 從設備讀取數據 
  * @param - filp    : 要打開的設備文件(文件描述符)
  * @param - buf     : 返回給用戶空間的數據緩衝區
  * @param - cnt     : 要讀取的數據長度
  * @param - offt    : 相對於文件首地址的偏移
  * @return          : 讀取的字節數,如果爲負值,表示讀取失敗
  */
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;
		}
	}
	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;
}

/* 設備操作函數 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
};

/*
 * @description	: 驅動入口函數
 * @param 		: 無
 * @return 		: 無
 */
static int __init imx6uirq_init(void)
{
	/* 1、構建設備號 */
	if (imx6uirq.major) {
		imx6uirq.devid = MKDEV(imx6uirq.major, 0);
		register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
	} else {
		alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
		imx6uirq.major = MAJOR(imx6uirq.devid);
		imx6uirq.minor = MINOR(imx6uirq.devid);
	}

	/* 2、註冊字符設備 */
	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
	cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

	/* 3、創建類 */
	imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.class)) {	
		return PTR_ERR(imx6uirq.class);
	}

	/* 4、創建設備 */
	imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.device)) {
		return PTR_ERR(imx6uirq.device);
	}
		
	/* 5、始化按鍵 */
	atomic_set(&imx6uirq.keyvalue, INVAKEY);
	atomic_set(&imx6uirq.releasekey, 0);
	keyio_init();
	return 0;
}

/*
 * @description	: 驅動出口函數
 * @param 		: 無
 * @return 		: 無
 */
static void __exit imx6uirq_exit(void)
{
	unsigned i = 0;
	/* 刪除定時器 */
	del_timer_sync(&imx6uirq.timer);	/* 刪除定時器 */
		
	/* 釋放中斷 */	
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
	}
	cdev_del(&imx6uirq.cdev);
	unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
	device_destroy(imx6uirq.class, imx6uirq.devid);
	class_destroy(imx6uirq.class);
}
	
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
	
	

應用程序

同第一次按鍵

需要說明的

1、幾個函數
__set_current_state 函數

  • 設置當前進程的狀態爲 TASK_INTERRUPTIBLE,也就是可以被信號打斷

schedule 函數

  • 進行一次任務切換,當前進程就會進入到休眠態

signal_pending 函數

  • 判斷一下進程是不是由信號喚醒的

2、使用等待隊列實現阻塞訪問重點注意兩點:

  • ①、將任務或者進程加入到等待隊列頭。
  • ②、在合適的點喚醒等待隊列,一般都是中斷處理函數裏面。

三、非阻塞IO實驗

驅動程序

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: noblock.c
作者	  	: 左忠凱
版本	   	: V1.0
描述	   	: 非阻塞IO訪問
其他	   	: 無
論壇 	   	: www.openedv.com
日誌	   	: 初版V1.0 2019/7/26 左忠凱創建
***************************************************************/
#define IMX6UIRQ_CNT		1			/* 設備號個數 	*/
#define IMX6UIRQ_NAME		"noblockio"	/* 名字 		*/
#define KEY0VALUE			0X01		/* KEY0按鍵值 	*/
#define INVAKEY				0XFF		/* 無效的按鍵值 */
#define KEY_NUM				1			/* 按鍵數量 	*/

/* 中斷IO描述結構體 */
struct irq_keydesc {
	int gpio;								/* gpio */
	int irqnum;								/* 中斷號     */
	unsigned char value;					/* 按鍵對應的鍵值 */
	char name[10];							/* 名字 */
	irqreturn_t (*handler)(int, void *);	/* 中斷服務函數 */
};

/* 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;	/* 讀等待隊列頭 */
};

struct imx6uirq_dev imx6uirq;	/* irq設備 */

/* @description		: 中斷服務函數,開啓定時器		
 *				  	  定時器用於按鍵消抖。
 * @param - irq 	: 中斷號 
 * @param - dev_id	: 設備結構。
 * @return 			: 中斷執行結果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev*)dev_id;

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定時 */
	return IRQ_RETVAL(IRQ_HANDLED);
}

/* @description	: 定時器服務函數,用於按鍵消抖,定時器到了以後
 *				  再次讀取按鍵值,如果按鍵還是處於按下狀態就表示按鍵有效。
 * @param - arg	: 設備結構變量
 * @return 		: 無
 */
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);
	}
}

/*
 * @description	: 按鍵IO初始化
 * @param 		: 無
 * @return 		: 無
 */
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;
}

/*
 * @description		: 打開設備
 * @param - inode 	: 傳遞給驅動的inode
 * @param - filp 	: 設備文件,file結構體有個叫做private_data的成員變量
 * 					  一般在open的時候將private_data指向設備結構體。
 * @return 			: 0 成功;其他 失敗
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq;	/* 設置私有數據 */
	return 0;
}

 /*
  * @description     : 從設備讀取數據 
  * @param - filp    : 要打開的設備文件(文件描述符)
  * @param - buf     : 返回給用戶空間的數據緩衝區
  * @param - cnt     : 要讀取的數據長度
  * @param - offt    : 相對於文件首地址的偏移
  * @return          : 讀取的字節數,如果爲負值,表示讀取失敗
  */
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 (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;
		}
	}

	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:
	return ret;
data_error:
	return -EINVAL;
}

 /*
  * @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;
}

/* 設備操作函數 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.poll = imx6uirq_poll,
};

/*
 * @description	: 驅動入口函數
 * @param 		: 無
 * @return 		: 無
 */
static int __init imx6uirq_init(void)
{
	/* 1、構建設備號 */
	if (imx6uirq.major) {
		imx6uirq.devid = MKDEV(imx6uirq.major, 0);
		register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
	} else {
		alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
		imx6uirq.major = MAJOR(imx6uirq.devid);
		imx6uirq.minor = MINOR(imx6uirq.devid);
	}

	/* 2、註冊字符設備 */
	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
	cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

	/* 3、創建類 */
	imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.class)) {	
		return PTR_ERR(imx6uirq.class);
	}

	/* 4、創建設備 */
	imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.device)) {
		return PTR_ERR(imx6uirq.device);
	}
		
	/* 5、始化按鍵 */
	atomic_set(&imx6uirq.keyvalue, INVAKEY);
	atomic_set(&imx6uirq.releasekey, 0);
	keyio_init();
	return 0;
}

/*
 * @description	: 驅動出口函數
 * @param 		: 無
 * @return 		: 無
 */
static void __exit imx6uirq_exit(void)
{
	unsigned i = 0;
	/* 刪除定時器 */
	del_timer_sync(&imx6uirq.timer);	/* 刪除定時器 */
		
	/* 釋放中斷 */	
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
	}
	cdev_del(&imx6uirq.cdev);
	unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
	device_destroy(imx6uirq.class, imx6uirq.devid);
	class_destroy(imx6uirq.class);
}	
	
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
	
	

應用程序

#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;
}

需要說明的:

  • 第 52~73 行,這段代碼使用 poll 函數來實現非阻塞訪問,在 while 循環中使用 poll 函數不斷的輪詢,檢查驅動程序是否有數據可以讀取,如果可以讀取的話就調用 read 函數讀取按鍵數據。
  • 第 75~101 行,這段代碼使用 select 函數來實現非阻塞訪問
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章