Linux驅動開發一.字元裝置框架——4.驅動測試
在前面的三章裡我們完成了驅動的框架、應用程式的編寫,但是並沒有實現檔案的實際讀寫功能(只是通過核心打印出了除錯資訊)。這一章我們著重實現檔案實際的讀寫效果。
由於沒有實際資料IO,我們只是在驅動中定義一個數據傳遞給應用程式;在應用程式中定義個使用者資料用來傳遞給核心(驅動)。
核心空間和使用者空間的資料互動
因為使用者和驅動之間的資料是分別存在在核心空間和使用者空間中,我們需要在核心空間和資料空間之間做資料互動。這就要用到下面核心裡的兩個函式(uaccess.h中):
static inline unsigned long __must_check copy_from_user(void *to, constvoid __user *from, unsigned long n) { if (access_ok(VERIFY_READ, from, n)) n = __copy_from_user(to, from, n); else /* security hole - plug it */ memset(to, 0, n); return n; } static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned longn) { if (access_ok(VERIFY_WRITE, to, n)) n = __copy_to_user(to, from, n); return n; }
從函式名字就可以看出來,一個是從使用者空間(user)拷貝資料(copy_from_user)另一個是向用戶空間複製資料(copy_to_user)。通過這兩個函式,就可以實現資料在核心空間和使用者空間進行互動。
驅動程式編寫
驅動程式的編寫是在上一章的驅動上完成的,只是修改了dev_write和dev_read兩個函式,還新增了兩個變數用來儲存資料。
#include <linux/module.h> #include<linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #define DEV_MAJOR 200 //裝置號 #define DEV_NAME "DEVICE_TEST" //裝置名稱 static char kerneldata[] = {"test data from kernel!"}; //測試用核心資料 static char writebuf[100]; //寫快取 // /** // * @brief 開啟裝置檔案 // * // * @return int // */ static int dev_open(struct inode *inode, struct file *filp) { printk("dev open!\r\n"); return 0; } /** * @brief 關閉裝置檔案 * * @return int */ static int dev_release(struct inode *inode, struct file *filp) { printk("dev release!\r\n"); return 0; } /** * @brief 讀裝置檔案資料 * * @param filp * @param buf * @param count * @param ppos * @return ssize_t */ static ssize_t dev_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos) { int ret = 0; printk("dev read data!\r\n"); ret = copy_to_user(buf,kerneldata,sizeof(kerneldata)); //向用戶空間寫入資料 if (ret == 0){ return 0; } else{ printk("kernel read data error!"); return -1; } } /** * @brief 裝置檔案資料寫入 * * @param file * @param buf * @param count * @param ppos * @return ssize_t */ static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { int ret = 0; printk("dev write data!\r\n"); ret = copy_from_user(writebuf,buf,count); //從使用者空間獲取資料寫入核心空間(writebuf) if (ret == 0){ printk("get data from APP:%s\r\n",writebuf); return 0; } else{ printk("kernelwrite err!\r\n"); return -1; } } /** * @brief 檔案操作結構體 * */ static struct file_operations testDev_fops= { .owner = THIS_MODULE, .open = dev_open, .release = dev_release, .read = dev_read, .write = dev_write, }; /** * @brief 初始化 * * @return int */ static int __init dev_init(void) { int ret = 0; printk("device init!\r\n"); //字元設備註冊 ret = register_chrdev(DEV_MAJOR, DEV_NAME, &testDev_fops); if(ret < 0 ){ printk("device init failed\r\n"); } return 0; } /** * @brief 解除安裝 * */ static void __exit dev_exit(void) { //字元設備註銷 unregister_chrdev(DEV_MAJOR,DEV_NAME); printk("device exit\r\n"); } module_init(dev_init); //模組載入 module_exit(dev_exit); //模組解除安裝 MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
整個過程先放上面,在後面結合應用程式來大概講一下。主要就是看下讀和寫兩個函式。
應用程式
應用程式修改的地方大一些
/** * @file testAPP.c * @author your name ([email protected]) * @brief * @version 0.1 * @date 2022-04-02 * * @copyright Copyright (c) 2022 * */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> /** * @brief * * @param argc //引數個數 * @param argv //引數 * @return int */ int main(int argc,char *argv[]) { char *filename; //檔名 filename = argv[1]; //檔名為命令列後第二個引數(索引值為1) int ret = 0; //初始化操作返回值 int f = 0; //初始化檔案控制代碼 int action = atoi(argv[2]); //讀寫標誌:0為讀,1為寫 char readbuf[100]; //初始化讀資料快取 char writebuf[100]; //初始化寫資料快取 char testdata[] = {"data from user Application!"}; //測試資料,準備寫入核心的資料 if(argc != 3){ //輸入引數數量!=3,提示輸入格式錯誤! printf("input format error!"); } f = open(filename, O_RDWR); //開啟檔案 if(f < 0){ printf("open file %s failed!\r\n", filename); } /** * @brief 引數為0,進行讀操作 * */ if(action == 0){ ret = read(f, readbuf , 100); if (ret < 0) { printf("read err!"); return -1; } else{ printf("read data from kernel:%s\r\n",readbuf); /*sleep1秒,等待printf函式列印完成*/ sleep(1); ret = close(f); return 0; } } /** * @brief 引數為1,進行寫操作 * */ else if(action == 1){ ret = write(f, testdata, sizeof(testdata)); if(ret <0){ printf("write err!"); return -1; } else{ close(f); return 0; } } else{ close(f); return 0; } }
因為我們通過應用檔案操作驅動檔案主要的方式就是讀和寫,所以方便起見命令的格式為
./APPtest /dev/testDev 0
一共3個引數,第一個是應用程式名稱,第二個是裝置節點檔案,第三個是讀寫標誌位。在函式一開始會判斷引數數量是否和要求格式一致(3個),不一致時提示格式錯誤。正常的話開啟檔案進行後續操作。
檔案讀寫操作
這一章主要就是講一下怎麼實現檔案的讀寫操作。
讀資料
當我們需要獲取硬體的資料時(例如獲取感測器資訊)就要使用到讀操作。在掛載模組並載入裝置檔案節點以後通過命令列輸入如下命令
./testAPP /dev/testDev 0
第三個引數為0(argv[2]),在APP裡action變數值就為0。通過if據判斷呼叫read函式。read函式中的引數為檔案控制代碼f,讀取快取readbuf,讀取的資料長度100。在執行read函式時,核心執行檔案操作結構體內read對應的函式dev_read。dev_read裡最主要的功能就是呼叫下面的函式
ret = copy_to_user(buf,kerneldata,sizeof(kerneldata)); //向用戶空間寫入資料
就是將kerneldata變數裡的值傳遞給使用者空間。使用者空間在拿到值以後通過printf函式打印出來。
這裡有個bug:
printf函式和printk函式的優先順序不知道是不是有什麼關係,在列印readbuf裡的內容時,在列印了一半的資訊後關閉檔案的提示資訊會被printk打印出來。然後再繼續列印(類似中斷的效果)。所以我在這裡加了個sleep,等了1秒鐘,等訊息被列印完全了再關閉檔案。
寫資料
寫資料和讀資料的過程基本一致,在命令列內輸入下面的命令
./testAPP /dev/testDev 1
通過第3個引數1,程式呼叫if判斷執行write函式,在write函式裡我們將testdata給到buf內,核心中使用
ret = copy_from_user(writebuf,buf,count); //從使用者空間獲取資料寫入核心空間(writebuf) if (ret == 0){ printk("get data from APP:%s\r\n",writebuf); return 0; }
copy_from_user函式,將資料讀出至writebuf內,如果無異常就將writebuf裡的資料通過printk打印出來,而這裡的列印資訊和關閉的列印資訊都是通過核心的printk列印的,所以不存在前面的bug。這樣整個驅動就完成了!