目錄
實驗程序編寫
blockio.c
blockioApp.c
Makefile 文件
運行測試
在之前的文章里,Linux阻塞和非阻塞 IO(上),我們學習了Linux應用程序了兩種操作方式:阻塞和非阻塞 IO。
在Linux 中斷實驗中,Linux 中斷實驗,我們直接在應用程序中通過 read 函數不斷的讀取按鍵狀態,當按鍵有效的時候就打印出按鍵值。缺點就是:imx6uirqApp 這個測試應用程序擁有很高的 CPU 占用率。
本節實驗,我們使用阻塞 IO 的方式,實現同樣的功能,但大大降低CPU的占用率。
實驗程序編寫
在中斷實驗代碼的基礎上修改,主要是對其添加阻塞訪問相關的代碼。
blockio.c
代碼如下:
#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>#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 0imx6uirq.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;}
#endifDECLARE_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;
}/* 設備操作函數 */
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);gpio_free(imx6uirq.irqkeydesc[i].gpio);}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");
關鍵代碼分析如下:
設備文件名字為“blockio”,當驅動程序加載成功以后就會在根文件系統中出現一個名為“/dev/blockio”的文件。
#define IMX6UIRQ_NAME "blockio" /* 名字 */
在imx6uirq 設備結構體中,添加一個等待隊列頭 r_wait,因為在 Linux 驅動中處理阻塞 IO需要用到等待隊列。
wait_queue_head_t r_wait; /* 讀等待隊列頭 */
timer_function函數里,定時器中斷處理函數執行,表示有按鍵按下,先判斷一下是否是一次有效的按鍵,如果是的話就通過 wake_up 或者 wake_up_interruptible 函數來喚醒等待隊列r_wait。
/* 喚醒進程 */if(atomic_read(&dev->releasekey)) { /* 完成一次按鍵過程 *//* wake_up(&dev->r_wait); */wake_up_interruptible(&dev->r_wait);}
keyio_init函數,調用 init_waitqueue_head 函數初始化等待隊列頭 r_wait。
/* 初始化等待隊列頭 */init_waitqueue_head(&imx6uirq.r_wait);
imx6uirq_read函數,采用等待事件來處理 read 的阻塞訪問, wait_event_interruptible 函數等待releasekey 有效,也就是有按鍵按下。
如果按鍵沒有按下的話進程就會進入休眠狀態,因為采用了 wait_event_interruptible 函數,因此進入休眠態的進程可以被信號打斷。
#if 0/* 加入等待隊列,等待被喚醒,也就是有按鍵按下 */ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); if (ret) {goto wait_error;}
#endif
imx6uirq_read函數,使用等待隊列實現阻塞訪問的關鍵代碼:
- 首先使用 DECLARE_WAITQUEUE 宏定義一個等待隊列,
- 如果沒有按鍵按下的話,就使用 add_wait_queue 函數將當前任務的等待隊列,添加到等待隊列頭 r_wait 中。
- 隨后調用__set_current_state 函數,設置當前進程的狀態為 TASK_INTERRUPTIBLE,也就是可以被信號打斷。
- 接下來調用 schedule 函數進行一次任務切換,當前進程就會進入到休眠態。如果有按鍵按下,那么進入休眠態的進程就會喚醒,然后接著從休眠點開始運行。
- 通過 signal_pending 函數,判斷一下進程是不是由信號喚醒的,如果是由信號喚醒的話就直接返回-ERESTARTSYS 這個錯誤碼。
- 如果不是由信號喚醒的(也就是被按鍵喚醒的),那么就調用__set_current_state 函數將任務狀態設置為 TASK_RUNNING,然后調用 remove_wait_queue 函數將進程從等待隊列中刪除。
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); /* 將對應的隊列項從等待隊列頭刪除 */}
總結一下,使用等待隊列實現阻塞訪問的步驟:
- 將任務或者進程加入到等待隊列頭,
- 在合適的點喚醒等待隊列,一般都是中斷處理函數里面。
blockioApp.c
測試app的代碼和中斷實驗的代碼一致,代碼如下:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"/** @description : main主程序* @param - argc : argv數組元素個數* @param - argv : 具體參數* @return : 0 成功;其他 失敗*/
int main(int argc, char *argv[])
{int fd;int ret = 0;char *filename;unsigned char data;if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if (fd < 0) {printf("Can't open file %s\r\n", filename);return -1;}while (1) {ret = read(fd, &data, sizeof(data));if (ret < 0) { /* 數據讀取錯誤或者無效 */} else { /* 數據讀取正確 */if (data) /* 讀取到數據 */printf("key value = %#X\r\n", data);}}close(fd);return ret;
}
Makefile 文件
makefile文件只需要修改?obj-m 變量的值,改為blockio.o。
內容如下:
KERNELDIR := /home/huax/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_gaCURRENT_PATH := $(shell pwd)
obj-m := blockio.obuild: kernel_modules
kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
運行測試
編譯代碼:
make -j32 //編譯makefile文件
arm-linux-gnueabihf-gcc blockioApp.c -o blockioApp //編譯測試app
編譯成功以后,就會生成一個名為“blockio.ko”的驅動模塊文件,和blcokioApp 這個應用程序。
將編譯出來 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 按鍵,結果如圖:
當按下 KEY0 按鍵以后 blockioApp 這個測試 APP 就會打印出按鍵值。
輸入“top”命令,查看 blockioAPP 這個應用 APP 的 CPU 使用率,如圖:
可以看出,當我們在按鍵驅動程序里面加入阻塞訪問以后, blockioApp 這個應用程序的 CPU 使用率從 99.6%降低到了 0.0%。
我們可以使用“kill”命令關閉后臺運行的應用程序,比如我們關閉掉 blockioApp 這個后臺運行的應用程序。先查看 blockioApp 這個應用程序的 PID:
使用如下命令可“殺死”指定 PID 的進程:
kill -9 149
“./blockioApp /dev/blockio”這個應用程序已經被“殺掉”了。
再輸入“ps”命令查看當前系統運行的進程,會發現 blockioApp 已經不見了。