開發可統計單詞個數的Android驅動程式(2)
八、 指定回呼函數
本節講的內容十分關鍵。不管Linux驅動程式的功能多麼複雜還是多麼“酷”,都必須允許使用者空間的應用程式與核心空間的驅動程式進行互動才有意義。而最 常用的互動方式就是讀寫裝置檔案。通過file_operations.read和file_operations.write成員變數可以分別指定讀寫 裝置檔案要調用的回呼函數指標。
在本節將為word_count.c添加兩個函數:word_count_read和word_count_write。這兩個函數分別處理從裝置檔案讀 資料和向裝置檔案寫資料的動作。本節的例子先不考慮word_count要實現的統計單詞數的功能,先用word_count_read和 word_count_write函數做一個讀寫裝置檔案資料的實驗,以便讓讀者瞭解如何與裝置檔案互動資料。本節編寫的word_count.c檔案是 一個分支,讀者可在word_count/read_write目錄找到word_count.c檔案。可以用該檔案覆蓋word_count目錄下的同 名檔案測試本節的例子。
本例的功能是向裝置檔案/dev/wordcount寫入資料後,都可以從/dev/wordcount裝置檔案中讀出這些資料(只能讀取一次)。下面先看看本例的完整的代碼。
#include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> #define DEVICE_NAME "wordcount" // 定義裝置檔案名稱 static unsigned char mem[10000]; // 儲存向裝置檔案寫入的資料 static char read_flag = 'y'; // y:已從裝置檔案讀取資料 n:未從裝置檔案讀取資料 static int written_count = 0; // 向裝置檔案寫入資料的位元組數 // 從裝置檔案讀取資料時調用該函數 // file:指向裝置檔案、buf:儲存可讀取的資料 count:可讀取的位元組數 ppos:讀取資料的位移量 static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { // 如果還沒有讀取裝置檔案中的資料,可以進行讀取 if(read_flag == 'n') { // 將核心空間的資料複製到使用者空間,buf中的資料就是從裝置檔案中讀出的資料 copy_to_user(buf, (void*) mem, written_count); // 向日誌輸出已讀取的位元組數 printk("read count:%d", (int) written_count); // 設定資料已讀狀態 read_flag = 'y'; return written_count; } // 已經從裝置檔案讀取資料,不能再次讀取資料 else { return 0; } } // 向裝置檔案寫入資料時調用該函數 // file:指向裝置檔案、buf:儲存寫入的資料 count:寫入資料的位元組數 ppos:寫入資料的位移量 static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { // 將使用者空間的資料複製到核心空間,mem中的資料就是向裝置檔案寫入的資料 copy_from_user(mem, buf, count); // 設定資料的未讀狀態 read_flag = 'n'; // 儲存寫入資料的位元組數 written_count = count; // 向日誌輸出已寫入的位元組數 printk("written count:%d", (int)count); return count; } // 描述與裝置檔案觸發的事件對應的回呼函數指標 // 需要設定read和write成員變數,系統才能調用處理讀寫裝置檔案動作的函數 static struct file_operations dev_fops = { .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write }; // 描述裝置檔案的資訊 static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops }; // 初始化Linux驅動 static int word_count_init(void) { int ret; // 建立裝置檔案 ret = misc_register(&misc); // 輸出日誌資訊 printk("word_count_init_success\n"); return ret; } // 卸載Linux驅動 static void word_count_exit(void) { // 刪除裝置檔案 misc_deregister(&misc); // 輸出日誌資訊 printk("word_init_exit_success\n"); } // 註冊初始化Linux驅動的函數 module_init( word_count_init); // 註冊卸載Linux驅動的函數 module_exit( word_count_exit); MODULE_AUTHOR("lining"); MODULE_DESCRIPTION("statistics of word count."); MODULE_ALIAS("word count module."); MODULE_LICENSE("GPL");
編寫上面代碼需要瞭解如下幾點。
1. word_count_read和word_count_write函數的參數基本相同,只有第2個參數buf稍微一點差異。 word_count_read函數的buf參數類型是char*,而word_count_write函數的buf參數類型是const char*,這就意味著word_count_write函數中的buf參數值無法修改。word_count_read函數中的buf參數表示從裝置文 件讀出的資料,也就是說,buf中的資料都可能由裝置檔案讀出,至於可以讀出多少資料,取決於word_count_read函數的傳回值。如果 word_count_read函數返回n,則可以從buf讀出n個字元。當然,如果n為0,表示無法讀出任何的字元。如果n小於0,表示發生了某種錯誤 (n為錯誤碼)。word_count_write函數中的buf表示由使用者空間的應用程式寫入的資料。buf參數前有一個“__user”宏,表示 buf的記憶體地區位於使用者空間。
2. 由於核心空間的程式不能直接存取使用者空間中的資料,因此,需要在word_count_read和word_count_write函數中分別使用 copy_to_user和copy_from_user函數將資料從核心空間複製到使用者空間或從使用者空間複製到核心空間。
3. 本例只能從裝置檔案讀一次資料。也就是說,寫一次資料,讀一次資料後,第二次無法再從裝置檔案讀出任何資料。除非再次寫入資料。這個功能是通過 read_flag變數控制的。當read_flag變數值為n,表示還沒有讀過裝置檔案,在word_count_read函數中會正常讀取資料。如果 read_flag變數值為y,表示已經讀過裝置檔案中的資料,word_count_read函數會直接返回0。應用程式將無法讀取任何資料。
4. 實際上word_count_read函數的count參數表示的就是從裝置檔案讀取的位元組數。但因為使用cat命令測試word_count驅動時。直 接讀取了32768個位元組。因此count參數就沒什麼用了(值總是32768)。所以要在word_count_write函數中將寫入的位元組數儲存, 在word_count_read函數中直接使用寫入的位元組數。也就是說,寫入多少個位元組,就讀出多少個位元組。
5. 所有寫入的資料都儲存在mem數組中。該數組定義為10000個字元,因此寫入的資料位元組數不能超過10000,否則將會溢出。
為了方便讀者測試本節的例子,筆者編寫了幾個Shell指令檔,允許在UbuntuLinux、S3C6410開發板和Android模擬器上測試 word_count驅動。其中有一個負責調度的指令檔build.sh。本書所有的例子都會有一個build.sh指令檔,執行這個指令檔就會要 求使用者選擇將原始碼編譯到那個平台,選擇菜單6-11所示。使用者可以輸入1、2或3選擇編譯平台。如果直接按斷行符號鍵,預設值會選擇第1個編譯平台 (UbuntuLinux)。
build.sh指令檔的代碼如下:
source /root/drivers/common.sh # select_target是一個函數,用語顯示圖6-11所示的選擇菜單,並接收使用者的輸入 # 改函數在common.sh檔案中定義 select_target if [ $selected_target == 1 ]; then source ./build_ubuntu.sh # 執行編譯成Ubuntu Linux平台驅動的指令檔 elif [ $selected_target == 2 ]; then source ./build_s3c6410.sh # 執行編譯成s3c6410平台驅動的指令檔 elif [ $selected_target == 3 ]; then source ./build_emulator.sh # 執行編譯成Android模擬器平台驅動的指令檔 fi
在build.sh指令檔中涉及到了3個指令檔(build_ubuntu.sh、build_s3c6410.sh和 build_emulator.sh),這3個指令檔的代碼類似,只是選擇的Linux核心版本不同。對於S3C6410和Android模擬器平台, 編譯完後Linux驅動,會自動將編譯好的Linux驅動檔案(*.so檔案)上傳到相應平台的/data/local目錄,並安裝Linux驅動。例 如,build_s3c6410.sh指令檔的代碼如下:
source /root/drivers/common.sh # S3C6410_KERNEL_PATH變數是適用S3C6410平台的Linux核心原始碼的路徑, # 該變數以及其它類似變數都在common.sh指令檔中定義 make -C $S3C6410_KERNEL_PATH M=${PWD} find_devices # 如果什麼都選擇,直接退出 if [ "$selected_device" == "" ]; then exit else # 上傳驅動程式(word_count.ko) adb -s $selected_device push ${PWD}/word_count.ko /data/local # 判斷word_count驅動是否存在 testing=$(adb -s $selected_device shell lsmod | grep "word_count") if [ "$testing" != "" ]; then # 刪除已經存在的word_count驅動 adb -s $selected_device shell rmmod word_count fi # 在S3C6410開發板中安裝word_count驅動 adb -s $selected_device shell "insmod /data/local/word_count.ko" fi
使用上面的指令檔,需要在read_write目錄建立一個Makefile檔案,內容如下:
obj-m := word_count.o
現在執行build.sh指令檔,選擇要編譯的平台,並執行下面的命令向/dev/word_count裝置檔案寫入資料。
# echo ‘hello lining’ > /dev/wordcount
然後執行如下的命令從/dev/word_count裝置檔案讀取資料。
# cat /dev/wordcount
如果輸出“hello lining”,說明測試成功。
注意:如 果在S3C6410開發板和Android模擬器上測試word_count驅動,需要執行shell.sh指令檔或adb shell命令進入相應平台的終端。其中shell.sh指令碼在/root/drivers目錄中。這兩種方式的區別是如果有多個Android裝置和 PC相連時,shell.sh指令碼會出現一個類似圖6-11所示的選擇菜單,使用者可以選擇進入哪個Android裝置的終端,而adb shell命令必須要加-s命令列參數指定Android裝置的ID才可以進入相應Android裝置的終端。
九、實現統計單詞數的演算法
本節開始編寫word_count驅動的商務邏輯:統計單詞數。本節實現的演算法將由空格、定位字元(ASCII:9)、斷行符號符(ASCII:13)和分行符號 (ASCII:10)分隔的字串算做一個單詞,該演算法同時考慮了有多個分隔字元(空格符、定位字元、斷行符號符和分行符號)的情況。下面是word_count驅 動完整的代碼。在代碼中包含了統計單詞數的函數get_word_count。
#include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> #define DEVICE_NAME "wordcount" // 定義裝置檔案名稱 static unsigned char mem[10000]; // 儲存向裝置檔案寫入的資料 static int word_count = 0; // 單詞數 #define TRUE -1 #define FALSE 0 // 判斷指定字元是否為空白格(包括空格符、定位字元、斷行符號符和分行符號) static char is_spacewhite(char c) { if(c == ' ' || c == 9 || c == 13 || c == 10) return TRUE; else return FALSE; } // 統計單詞數 static int get_word_count(const char *buf) { int n = 1; int i = 0; char c = ' '; char flag = 0; // 處理多個空格分隔的情況,0:正常情況,1:已遇到一個空格 if(*buf == '\0') return 0; // 第1個字元是空格,從0開始計數 if(is_spacewhite(*buf) == TRUE) n--; // 掃描字串中的每一個字元 for (; (c = *(buf + i)) != '\0'; i++) { // 只由一個空格分隔單詞的情況 if(flag == 1 && is_spacewhite(c) == FALSE) { flag = 0; } // 由多個空格分隔單詞的情況,忽略多餘的空格 else if(flag == 1 && is_spacewhite(c) == TRUE) { continue; } // 當前字元為空白格時單詞數加1 if(is_spacewhite(c) == TRUE) { n++; flag = 1; } } // 如果字串以一個或多個空格結尾,不計數(單詞數減1) if(is_spacewhite(*(buf + i - 1)) == TRUE) n--; return n; } // 從裝置檔案讀取資料時調用的函數 static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { unsigned char temp[4]; // 將單詞數(int類型)分解成4個位元組儲存在buf中 temp[0] = word_count >> 24; temp[1] = word_count >> 16; temp[2] = word_count >> 8; temp[3] = word_count; copy_to_user(buf, (void*) temp, 4); printk("read:word count:%d", (int) count); return count; } // 向裝置檔案寫入資料時調用的函數 static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { ssize_t written = count; copy_from_user(mem, buf, count); mem[count] = '\0'; // 統計單詞數 word_count = get_word_count(mem); printk("write:word count:%d", (int)word_count); return written; } // 描述與裝置檔案觸發的事件對應的回呼函數指標 static struct file_operations dev_fops = { .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write }; // 描述裝置檔案的資訊 static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops }; // 初始化Linux驅動 static int word_count_init(void) { int ret; // 建立裝置檔案 ret = misc_register(&misc); // 輸出日誌資訊 printk("word_count_init_success\n"); return ret; } // 卸載Linux驅動 static void word_count_exit(void) { // 刪除裝置檔案 misc_deregister(&misc); // 輸出日誌資訊 printk("word_init_exit_success\n"); } // 註冊初始化Linux驅動的函數 module_init( word_count_init); // 註冊卸載Linux驅動的函數 module_exit( word_count_exit); MODULE_AUTHOR("lining"); MODULE_DESCRIPTION("statistics of word count."); MODULE_ALIAS("word count module."); MODULE_LICENSE("GPL");
編寫word_count驅動程式需要瞭解如下幾點。
1. get_word_count函數將mem數組中第1個為“\0”的字元作為字串的結尾符,因此在word_count_write函數中將 mem[count]的值設為“\0”,否則get_word_count函數無法知道要統計單詞數的字串到哪裡結束。由於mem數組的長度為 10000,而字串最後一個字元為“\0”,因此待統計的字串最大長度為9999。
2. 單詞數使用int類型變數儲存。在word_count_write函數中統計出了單詞數(word_count變數的值),在 word_count_read函數中將word_count整型變數值分解成4個位元組儲存在buf中。因此,在應用程式中需要再將這4個位元組組合成 int類型的值。
十、編譯、安裝、卸載Linux驅動程式
在上一節word_count驅動程式已經全部編寫完成了,而且多次編譯測試該驅動程式。安裝和卸載word_count驅動也做過多次。 word_count驅動與read_write目錄中的驅動一樣,也有一個build.sh和3個與平台相關的指令檔。這些指令檔與6.3.5節的 實作類別似,這裡不再詳細介紹。現在執行build.sh指令檔,並選擇要編譯的平台。然後執行下面兩行命令查看日誌輸出資訊和word_count驅動 模組(word_count.ko)的資訊。
# dmesg |tail -n 1
# modinfo word_count.ko
如果顯示6-12所示的資訊,表明word_count驅動工作完全正常。
本書的指令檔都是使用insmod命令安裝Linux驅動的,除了該命令外,使用modprobe命令也可以安裝Linux驅動。insmod和 modprobe的區別是modprobe命令可以檢查驅動模組的依賴性。如A模組依賴於B模組(裝載A之前必須先裝載B)。如果使用insmod命令裝 載A模組,會出現錯誤。而使用modprobe命令裝載A模組,B模組會現在裝載。在使用modprobe命令裝載驅動模組之前,需要先使用depmod 命令檢測Linux驅動模組的依賴關係。
# depmod /root/drivers/ch06/word_count/word_count.ko
depmod命令實際上將Linux驅動模組檔案(包括其路徑)添加到如下的檔案中。
/lib/modules/3.0.0-16-generic/modules.dep
使用depmod命令檢測完依賴關係後,就可以調用modprobe命令裝載Linux驅動。
# modprobe word_count
使用depmod和modprobe命令需要注意如下幾點:
1. depmod命令必須使用Linux驅動模組(.ko檔案)的絕對路徑。
2. depmod命令會將核心模組的依賴資訊寫入當前正在使用的核心的modules.dep檔案。例如,筆者的Ubuntu Linux使用的是Linux3.0.0.16,所以應到3.0.0-16-generic目錄去尋找modules.dep檔案。如果讀者使用了其他 Linux核心,需要到相應的目錄去尋找modules.dep檔案。
3. modprobe命令只需使用驅動名稱即可,不需要跟.ko。
在Android模擬器和Ubuntu上測試Linux驅動
本文節選至《Android深度探索(卷1):HAL與驅動開發》,
接下來幾篇文章將詳細闡述如何開發ARM架構的Linux驅動,並分別利用android程式、NDK、可執行檔測試Linux驅動。可在ubuntu
Linux、Android模擬器和S3C6410開發板(可以選購OK6410-A開發板,需要刷Android)