上篇文章:介紹了linux中的五種I/O模型,本篇,就來使用阻塞式I/O和非用阻塞式I/O兩種方式進行按鍵的讀取實驗,并對比之前使用輸入捕獲和中斷法檢測的按鍵程序,查看CPU的使用率是否降低。
1 阻塞I/O方式的按鍵檢測
1.1 阻塞I/O之等待隊列
阻塞訪問最大的好處就是當設備文件不可操作的時候進程可以進入休眠態,這樣可以將CPU資源讓出來。但是,當設備文件可以操作的時候就必須喚醒進程,一般在中斷函數里面完成喚醒工作。Linux 內核提供了等待隊列(wait queue)來實現阻塞進程的喚醒工作。
等待隊列頭使用結構體wait_queue_head_t 表示:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
使用 init_waitqueue_head 函數初始化等待隊列頭:
/**
* q: 要初始化的等待隊列頭
* return: 無
*/
void init_waitqueue_head(wait_queue_head_t *q)
當設備不可用的時, 將這些進程對應的等待隊列項(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)
當設備不可訪問的時候就需要將進程對應的等待隊列項添加到前面創建的等待隊列頭中:
/**
* q: 要加入的等待隊列頭
* wait:要加入的等待隊列項
* return: 無
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
當設備可以訪問以后再將進程對應的等待隊列項從等待隊列頭中刪除即可:
/**
* q: 要刪除的等待隊列頭
* wait:要刪除的等待隊列項
* return: 無
*/
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)
1.2 阻塞I/O程序編寫
這里僅介紹與之前按鍵程序的主要區別。
1.2.1驅動程序
阻塞讀取邏輯如下,首先要定義一個等待隊列,當按鍵沒有按下時,就要阻塞等待了(將等待隊列添加到等待隊列頭),然后進行行一次任務切換,交出CPU的使用權。等待有按鍵按下時,會有信號喚醒該等待,并將按鍵值返回給應用層的程序。
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;
/* 定義一個等待隊列 <-------------------------- */
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)
{
//printk("releasekey!\r\n");
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;
}
按鍵的定時器去抖邏輯中的,讀取到按鍵后,觸發喚醒,這里以其中的一個按鍵為例,其邏輯如下:
void timer1_function(unsigned long arg)
{
unsigned char value;
struct irq_keydesc *keydesc;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
keydesc = &dev->irqkeydesc[0];
value = gpio_get_value(keydesc->gpio); /* 讀取IO值 */
if(value == 1) /* 按下按鍵 */
{
printk("get key1: high\r\n");
atomic_set(&dev->keyvalue, keydesc->value);
}
else /* 按鍵松開 */
{
printk("key1 release\r\n");
atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
atomic_set(&dev->releasekey, 1); /* 標記松開按鍵,即完成一次完整的按鍵過程 */
}
/* 喚醒進程 */
if(atomic_read(&dev->releasekey))
{
wake_up_interruptible(&dev->r_wait);
}
}
1.2.2 應用程序
應用程序不需要修改,還使用之前的輪詢讀取的方式,為了在測試時看出阻塞與非阻塞方式的區別,在read函數前后添加打印,如果程序運行正常,會先打印read前一句的打印,直到有按鍵按下后,read函數才被接觸阻塞,read后一句的打印才會打印出。
/* 循環讀取按鍵值數據! */
while(1)
{
printf("[APP] read begin...\r\n");
read(fd, &keyvalue, sizeof(keyvalue));
printf("[APP] read end\r\n");
if (keyvalue == KEY1VALUE)
{
printf("[APP] KEY1 Press, value = %#X\r\n", keyvalue);
}
else if (keyvalue == KEY2VALUE)
{
printf("[APP] KEY2 Press, value = %#X\r\n", keyvalue);
}
}
1.2 實驗
和之前一樣,使用Makefile編譯驅動程序和應用程序,并復制到nfs根文件系統中。
開始測試,按如下圖,當沒有按鍵按下時,應用程序被阻塞:
按鍵程序在后臺運行,此時使用top指令開查看CPU的使用率,可以發現阻塞式按鍵驅動這種方式,CPU的暫用率幾乎為0,雖然按鍵應用程序中仍實現循環讀取的方式,但因平時讀取不到按鍵值,按鍵應用程序被阻塞住了,CPU的使用權被讓出,自然CPU的使用率就降下來了。
2 非阻塞I/O方式的按鍵檢測
按鍵應用程序以非阻塞的方式讀取,按鍵驅動程序也要以非阻塞的方式立即返回。應用程序可以通過select、poll或epoll函數來 查詢設備是否可以操作,驅動程序使用poll函數。
2.1 非阻塞I/O之select/poll
select函數原型:
/**
* nfs: 所要監視的這三類文件描述集合中,最大文件描述符加1
* readfds: 用于監視指定描述符集的讀變化
* writefds: 用于監視文件是否可以進行寫操作
* exceptfds: 用于監視文件的異常
* timeout: 超時時間
* return: 0 超時發生, -1 發生錯誤, 其他值 可以進行操作的文件描述符個數
*/
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout)
其中超時時間使用結構體timeval表示:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
當timeout為NULL的時候就表示無限等待。
poll函數原型:
/**
* fds: 要監視的文件描述符集合以及要監視的事件,為一個數組
* nfds: 監視的文件描述符數量
* timeout: 超時時間,單位為 ms
* return: 0 超時發生, -1 發生錯誤, 其他值 可以進行操作的文件描述符個數
*/
int poll(struct pollfd *fds,
nfds_t nfds,
nt timeout)
2.2 非阻塞I/O程序編寫
2.2.1 驅動程序
poll函數處理部分:
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_table中 */
poll_wait(filp, &dev->r_wait, wait);
/* 按鍵按下 */
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,
};
read函數處理部分:
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)
{
/* 沒有按鍵按下,返回-EAGAIN */
if(atomic_read(&dev->releasekey) == 0)
{
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)
{
//printk("releasekey!\r\n");
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;
}
2.2.2 應用程序
2.2.2.1 poll方式讀取
注意open函數的參數是O_NONBLOCK,即非阻塞訪問,并且為了在測試時看出阻塞讀取與非阻塞讀取的區別,在poll函數前后添加打印,如果程序正常運行,poll函數則不會被阻塞,500ms超時未讀取到按鍵值后會再次循環讀取,實際效果就是可以看打一直有打印輸出。
filename = argv[1];
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞訪問 */
if (fd < 0)
{
printf("[APP] Can't open file %s\r\n", filename);
return -1;
}
/* 構造結構體 */
fds.fd = fd;
fds.events = POLLIN;
while(1)
{
printf("[APP] poll begin... \r\n", data);
ret = poll(&fds, 1, 500);
printf("[APP] poll end \r\n", data);
/* 數據有效 */
if (ret > 0)
{
ret = read(fd, &data, sizeof(data));
if(ret < 0)
{
/* 讀取錯誤 */
}
else
{
if(data)
{
printf("[APP] key value = %d \r\n", data);
}
}
}
/* 超時 */
else if (ret == 0)
{
/* 用戶自定義超時處理 */
}
/* 錯誤 */
else
{
/* 用戶自定義錯誤處理 */
}
}
2.2.2.2 select方式讀取
select方式讀取與poll方式類似,都是非阻塞讀取,程序類似:
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;
}
}
2.3 實驗
2.3.1 poll方式讀取
和之前一樣,使用Makefile編譯驅動程序和應用程序,并復制到nfs根文件系統中。
開始測試,按如下圖,當沒有按鍵按下時,應用程序也沒有被阻塞,從不斷的打印就可以看出應用程序在循環運行。當有按鍵按下時,能夠讀取到對應的按鍵值。
按鍵程序在后臺運行,此時使用top指令開查看CPU的使用率,可以發現非阻塞式按鍵驅動這種方式,CPU的暫用率也幾乎為0,雖然按鍵應用程序中仍實現循環讀取的方式,但poll函數有500ms的超時設置,在超時等待的時間里,CPU的使用權也是被讓出,所以CPU的使用率也降下來了。
2.3.2 select方式讀取
select方式讀取與poll方式讀取的效果一樣。
使用ps指令查看poll方式的按鍵進行號,使用kill殺帶該進程,再運行select方式的按鍵應用程序:
select非阻塞讀取的方式,CPU的暫用率也幾乎為0:
3 總結
本篇使用兩種I/O模型進行按鍵讀?。?strong>阻塞式I/O和非用阻塞式I/O,通過實際的實驗,對比兩者方式的實際運行效果與主要區別,并查看CPU的占用率,兩種方式的CPU使用率都幾乎為0。
-
嵌入式
+關注
關注
5082文章
19104瀏覽量
304811 -
驅動
+關注
關注
12文章
1838瀏覽量
85262 -
Linux
+關注
關注
87文章
11292瀏覽量
209328 -
i.MX6
+關注
關注
1文章
37瀏覽量
16299
發布評論請先 登錄
相關推薦
評論