Linux設備驅動核心理論(一)

4.Linux內核模塊

        4.1 Linux內核模塊簡介

                如果把所有需要的功能都編譯到Linux內核。這回導致兩個問題,一是生成的內核會很大,二是如果我們要在現有的內核中新增或刪除功能,將不得不重新編譯內核。

                現在我們需要的是一種機制使得編譯出的內核本身並不需要包含所有功能,而在這些功能需要被使用的時候,其對應的代碼被動態地加載到內核中。

                Linux提供了這樣的一種機制,這種機制被稱爲模塊(Module)。模塊具有這樣的特點:

                        模塊本身不被編譯如內核映像,從而控制內核的大小。

                        模塊一旦被加載,它就和內核中的其他部分完全一樣。

                內核模塊編譯後會生成*.ko目標文件,通過“insmod ./*.ko”命令可以加載內核模塊,通過“rmmod *”命令可以卸載它。內核模塊中用於輸出的函數是內核空間的printk()而非用戶空間的printf()。printk()的用法和printf基本相似,但前者可定義輸出級別。printk可作爲一種最基本的內核調試手段。

                在linux中,使用lsmod命令可以獲得系統中加載了的所有模塊以及模塊間的依賴關係。lsmod命令實際上讀取並分析“/proc/modules”文件。

                內核中已加載模塊的信息也存在於/sys/module目錄下,加載hello.ko後,內核中將包含/sys/module/hello目錄,該目錄下包含一個refcnt文件和一個sections目錄。

                modprobe命令比insmod命令要強大,它在加載某模塊時,會同時加載該模塊所依賴的其他模塊。使用modprobe命令加載的模塊若以“modprobe -r filename”的方式卸載將同時卸載其依賴的模塊。

                使用modinfo<模塊名>命令可以獲得模塊的信息,包括模塊作者、模塊說明、模塊所支持的參數以及vermagic。

        4.2 Linux內核模塊程序結構

                一個Linux內核模塊主要由如下幾個部分組成。

                (1)模塊加載函數(一般需要)

                        當通過insmod或modprobe命令加載內核模塊時,模塊的加載函數會自動被內核執行,完成本模塊的相關初始化工作。

                (2)模塊卸載函數(一般需要)

                        當通過remod命令卸載某模塊時,模塊的卸載函數會自動被內核執行,完成與模塊卸載函數相反的功能。

                (3)模塊許可證聲明(必須)

                        許可證(LICENSE)聲明描述內核模塊的許可權限,如果不聲明LICENSE,模塊被加載時,將受到內核被污染(kernel tainted)的警告。

                        在Linux2.6內核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、"Dual MPL/GPL"和“Proprietary”。

                        大多數情況下,內核模塊遵循GPL兼容許可證。Linux 2.6內核模塊最常見是以MODULE_LICENSE("Dual BSD/GPL")語句聲明模塊採用BSD/GPL雙LICENSE。

                (4)模塊參數(可選)

                        模塊參數是模塊被加載的時候可以被傳遞給他的值,它本身對應模塊內部的全局變量。

                (5)模塊導出符號(可選)

                        內核模塊可以導出符號(symbol,對應於函數和變量),這樣其他模塊可以使用本模塊中的變量或函數。

                 (6)模塊作者等信息聲明(可選)

                        語句聲明塊是:MODEULE_AUTHOR、MODULE_LICENSE、MODULE_DESCRIPTION、MODULE_ALIAS。

        4.3 模塊加載函數

                Linux內核模塊加載函數一般以__init標識聲明。典型的模塊加載函數的形式如下下:

                        static int __init initialization_function(void)

                        {

                                /*=初始化代碼*/

                        }

                        module_init(initialization_function);

                模塊加載函數必須以“module_init(函數名)”的形式被指定。它返回整數值,若初始化成功,應返回0。而在初始化失敗時,應該返回錯誤編碼。在Linux內核裏,錯誤編碼是一個負值,在<linux/errno.h>中定義,包含-ENODEV、-ENOMEM之類的符號值。總是返回相應的錯誤編碼是種非常好的習慣,因爲只有這樣,用戶纔可以利用perror等方法把它們轉化成有意義的錯誤信息字符串。

                在Linux 2.6內核中,可以利用request_module(const char *fmt, ...)函數加載內核模塊,驅動開發人員可以通過request_module(module_name)或request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev))這種靈活的方式加載其他內核模塊。

                在Linux中,所有標識爲__init的函數在連接的時候都放在.init.text這個區段內,此外,所有的__init函數在區段.initcall.init中還保存了一份指針,在初始化時內核會通過這些函數指針調用這些__init函數,並在初始化完成後,釋放init區段(包括.init.text、.initcall.init等)。

        4.4 模塊卸載函數

                Linux內核模塊卸載函數一般以__exit標識聲明。典型的模塊卸載函數的形式如下:

                        static void __exit cleanup_function(void)

                        {

                                /*釋放代碼*/

                        }

                        module_exit(cleanup_function);

                模塊卸載函數在模塊卸載的時候執行,不返回任何值,必須以“module_exit(函數名)”的形式來指定。通常來說,模塊卸載要完成與模塊加載相反的功能:

                        若模塊加載函數註冊了XXX,模塊卸載函數應註銷XXX。

                        若模塊加載函數動態申請了內存,則模塊卸載函數應釋放該內存。

                        若模塊加載函數申請了硬件資源(中斷、DMA通道、I/O端口和I/O內存等)的佔用,則模塊卸載函數應釋放這些硬件資源。

                        若模塊加載函數開啓了硬件,則卸載函數中一般要關閉之。

                和__init一樣,__exit也可以使對應函數的運行完成後自動回收內存。實際上,__init和__exit都是宏,其定義分別爲:

                        #define __init        __attribute__((__section__(".init.text")))

                        #ifdef MODULE

                        #define __exit       __attribute__((__section__(".exit.text")))

                        #else

                        #define __exit       __attribute_used__attribute__((__section__(".exit.text")))

                        #endif

                數據也可以被定義爲__initdata和__exitdata,這兩個宏分別爲:

                        #define __initdata        __attribute__((__section__(".init.data")))

                        #define __exitdata       __attribute__((__section__(".exit.data")))

        4.5 模塊參數

                我們可以用“module_param(參數名,參數類型,參數讀/寫權限)”爲模塊定義一個參數:

                        static char *book_name="dissecting Linux Device Driver";

                        static int num=4000;

                        module_param(num, int, S_IRUGO);

                        module_param(book_name, charp, S_IRUGO);

                在裝載內核模塊時,用戶可以向模塊傳遞參數,形式爲”insmode(或modprobe)模塊名 參數名=參數值“,如果不傳遞,參數將使用模塊內定義的缺省值。

                參數類型可以是byte、short、ushort、int、uint、long、ulong、charp(字符類型)、bool或invbool(布爾的反),在模塊被編譯時會將module_param中聲明的類型與變量定義的類型進行比較,判斷是否一致。

                模塊被加載後,在/sys/module/目錄下將出現以此模塊名命名的目錄。當“參數讀/寫權限”爲0時,表示此參數不存在sysfs文件系統下對應的文件節點。如果此模塊存在“參數讀/寫權限”不爲0的命令行參數,在此模塊的目錄下還將出現paramters目錄,包含一系列以參數名命名的文件節點,這些文件的權限值就是傳入module_parame()的“參數讀/寫權限”,而文件的內容爲參數的值。

                除此之外,模塊也可以擁有參數數組,形式爲“module_param_array(數組名,數組類型,數組長,參數讀/寫權限)”從2.6.0~2.6.10版本,需將數組長變量名賦給“數組長”,從2.6.10版本開始,需將數組長變量的指針賦給“數組長”,當不需要保存實際輸入的數組元素個數時,可以設置“數組長”爲NULL。

                運行insmod或modprobe命令時,應使用逗號分隔輸入的數組元素。

                通過查看“/var/log/messages”日誌文件可以看到內核的輸出。

        4.6 導出符號

                Linux 2.6的“/proc/kallsyms”文件對應着內核符號表,它記錄了符號以及符號所在的內存地址。

                模塊可以使用如下宏導出符號到內核符號表:

                        EXPORT_SYMBOL(符號名);

                        EXPORT_SYMBOL_GOL(符號名);

                導出的符號將可以被其他模塊使用,使用前聲明一下即可。EXPORT_SYMBOL_GPL()只適合用於包含GPL許可權的模塊。

        4.7 模塊聲明與描述

                在Linux內核模塊中,我們可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分別聲明模塊的作者、描述、版本、設備表和別名。

                對於USB、PCI等設備驅動,通常會創建一個MODULE_DEVICE_TABLE,表明該驅動模塊所支持的設備。

        4.8 模塊的使用計數

                Linux 2.4內核中,模塊自身通過MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏來管理自己的使用計數。

                Linux 2.6內核提供了模塊計數管理接口try_module_get(&module)和module_put(&module)。從而取代Linux 2.4內核中的模塊使用計數管理宏。模塊的使用計數一般不必由模塊自身管理,而且模塊計算管理還考慮了SMP與PREEMPT機制的影響。

                        int try_module_get(struct module *module);

                該函數用於增加模塊使用計數;若返回爲0,表示調用失敗,希望使用的模塊沒有被加載或正在被卸載中。

                        void module_put(struct module *module);

                該函數用於減少模塊使用計數。

                try_module_get()與module_put()的引入與使用與Linux 2.6內核下的設備模型密切相關。Linux 2.6內核爲不同類型的設備定義了struct module *owner域,用來指向管理此設備的模塊。當開始使用某個設備時,內核使用try_module_get(dev->owner)去增加管理此設備的owner模塊的使用計數;當不再使用此設備時,內核使用module_put(dev->owner)減少對管理此設備的owner模塊的使用計數。這樣,當設備在使用時,管理此設備的模塊將不能被卸載。只有當設備不再被使用時,模塊才允許被卸載。

                在Linux 2.6內核下,對於設備驅動工程師而言,很少需要親自調用try_module_get()與module_put(),因此此時開發人員所寫的驅動通常爲支持某具體設備的owner模塊,對此設備owner模塊的計數管理由內核裏更底層的代碼如總線驅動或是此類設備共用的核心模塊來實現,從而簡化了設備驅動開發。

        4.9 模塊的編譯

                我們可以爲代碼清單的模板編寫一個簡單的Makefile:

                        KVERS=$(shell uname -r)

                        #Kernel module

                        obj-m+=hello.o

                        #Specify flags for the module compilation

                        #EXTRA_CFLAGS=-G -O0

                        build:kernel_modules

                        kernel_modules:make -C /lib/module/$(KVERS)/build M=$(CURDIR) modules

                        clean: make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

                該Makefile文件應該與源代碼hello.c位於同一個目錄,開啓其中的EXTRA_CFAGS=-g -O0可以得到包含調試信息的hello.ko模塊。運行make命令得到的模塊可直接在PC上運行。

                如果一個模塊包括多個.c文件(如file1.c、file2.c),則應該以如下方式編寫Makefile:

                        obj-m:=modulename.o

                        modulename-objs:=file1.o file2.o

        4.10 使用模塊繞開GPL

                對於企業自己編寫的驅動等內核代碼,如果不編譯爲模塊則無法繞開GPL,編譯爲模塊後企業在產品中使用模塊,則公司對外不再需要提供對應的源代碼,爲了使公司產品所使用的Linux操作系統支持模塊,需要完成如下工作:

                        在內核編譯時應該選上“可以加載模塊”,嵌入式產品一般不需要動態卸載模塊,所以“可以卸載模塊”不用選。

                        將我們編譯的內核模塊.ko文件按應該放置在目標文件系統的相關目錄下。

                        產品的文件系統中應該包含了支持新內核的insmod、lsmod、rmmod等工具,由於嵌入式產品中一般不需要建立模塊間依賴關係,所以modprobe可以不要,一般也不需要卸載模塊,所以rmmod也可以不要。

                        在使用中用戶可使用insmod命令手動加載模塊,如insmod xxx.ko。

                        但是一般而言,產品在啓動過程中應該加載模塊,在嵌入式產品Linux的啓動過程中,加載企業自己的模塊的最簡單的方法是修改啓動過程的rc腳本,增加insmod /.../xxx.ko這樣的命令。用busybox做出的文件系統,通常修改etc/init.d/rcS文件。

                嚴格意義上講,不使用GPL許可權的驅動模塊不宜使用標準的驅動架構,如V4L2、ALSA、Framebugger等,否則仍然可能存在license問題。

5.Linux文件系統與設備文件系統

        5.1 Linux文件操作

                5.1.1 文件操作系統調用

                        Linux的文件操作系統調用(在Windows編程領域,習慣稱操作系統提供的接口爲API)涉及創建、打開、讀寫和關閉文件。

                        1.創建

                                 int creat(const char *filename, mode_t mode);

                                 參數mode指定新建文件的存取權限,它同umake一起決定文件的最終權限(mode&umask),其中umask代表了文件在創建時需要去掉的一些存取權限。unask可以通過系統調用umask()來改變。

                                 int unask(int newmask);

                                 該調用將umask設置爲newmask,然後返回舊的umask,它隻影響讀、寫和執行權限。

                         2.打開

                                 int open(const char *pathname, int flags);

                                 int open(const char *pathname, int flags, mode_t mode);

                                 open函數有兩個形式,其中pathname是我們要打開的文件名(包含路徑名稱,缺省是認爲在當前路徑下面)。flags可以是圖中的一個值或者是幾個組合:

                                        

                                 O_RDONLY、O_WRONLY、O_RDWR三個標誌只能使用任意的一個。

                                 如果使用了O_CREATE標誌,使用的函數是int open(const char* pathname, int flags, mode_t mode);還要指定mode標誌,用來表示文件的訪問權限。mode可是是下圖中的值的組合:

                                       

                                 除了可以通過上述宏進行“或”邏輯產生標誌以外,我們也可以自己用數字來表示,Linux用5個數字來表示文件的各種權限;第一位表示設置用戶ID;第二位表示設置組ID;第三位表示用戶自己的權限位;第四位表示組的權限;最後一位表示其他人的權限。

                                open("test", 10705);等價於open("test", O_CREAT, S_IRWXU|S_IROTH|S_IXOTH|S_ISUID);

                                如果文件打開成功,open函數會返回一個文件描述符,以後對該文件的所有操作就可以通過對這個文件描述符進行操作來實現。

                        3.讀寫

                                在文件打開以後,我們纔可以對文件進行讀寫,Linux中提供文件讀寫的系統調用時read、write函數:

                                        int read(int fd, const void *buf, size_t length);

                                        int write(int fd, const void *buf, size_t length);

                                其中參數buf爲指向緩存區的指針,length爲緩存區的大小(以字節外單位)。函數read實現從文件描述符fd所指定的文件中讀取length個字節到buf所指向的緩存區中,返回值爲實際讀寫的字節數。函數write實現將把length個字節從buf指向的緩存區中寫到文件描述符fd所指向的文件中,返回值爲實際寫入的字節數。

                                以O_CREAT爲標誌的open實際實現了文件創建的功能,下面的函數等同creat函數:int open(pathname, O_CREAT|O_WRONLY|O_TRUNC, mode);

                        4.定位

                                對於隨機文件,我們可以隨機地制定位置讀寫,使用如下函數進行定位:

                                       int lseek(int fd, offset_t offset, int whence);

                               lseek將文件讀寫指針相對whence移動offset個字節。操作成功時,返回文件指針相對於文件包的位置。參數whence可使用下述值:

                                       SEEK_SET:相對文件開頭。

                                       SEEK_CUR:相對文件讀寫指針的當前位置。

                                       SEEK_END:相對文件末尾

                               offset可取負值,表示向前移動。

                               由於lseek函數的返回值爲文件指針相對文件頭的位置,因此下列調用的返回值就是文件的長度:lseek(fd, 0, SEEK_END);

                       5.關閉

                               當我們操作完成後,我們要關閉文件了,只要調用close就可以了,其中fd是我們要關閉的文件描述符:

                                       int close(int fd);

                5.1.2 C庫文件操作

                        C庫函數的文件操作實際上是獨立於具體的操作系統平臺的,不管是DOS、Windows、Linux還是在VxWorks中都是這些函數。

                        1.創建和打開

                                 FILE *fopen(const char *path, const char *mode);

                                 C庫函數中支持的打開模式如下:

                                        

                                其中b用於區分二進制文件和文本文件,這一點在DOS、Windows系統中是有區分的,但Linux不區分二進制文件和文本文件。

                        2.讀寫

                                C庫函數支持以字符、字符串等爲單位,支持按照某種格式進行文件的讀寫,這一組函數爲:

                                        int fgetc(FILE *stream);

                                        int fputc(int c, FILE *stream);

                                        char *fgets(char *s, int n, FILE *stream);

                                        int fputs(const char *s, FILE *stream);

                                        int fprintf(FILE *stream, const char *format, ...);

                                        int fscanf(FILE *stream, const char *format, ...);

                                        size_t fread(void *ptr, size_t size, size_t n, FILE *stream);

                                        size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);

                                read()實現從流stream中讀取加n個字段,每個字段爲size字節,並將讀取的字段放入ptr所指的字符數組中,返回實際已讀取的字段數。在讀取的字段數小於num時,可能是在函數調用時出現錯誤,也可能是讀到文件結尾。所以要通過調用feof()和ferror()來判斷。

                                write()實現從緩存區ptr所指的數組中把n個字段寫到流stream中,每個字段長爲size個字節,返回實際寫入的字段數。

                                另外,C庫函數還提供了讀寫過程中的定位能力,這些函數包括:

                                        int fgetpos(FILE *stream, fpos_t *pos);

                                        int fsetpos(FILE *stream, const fpos_t *pos);

                                        int fseek(FILE *stream, long offset, int whence);

                        3.關閉

                                利用C庫函數關閉文件依然是很簡單的操作:

                                        int fclose(FILE *stream);

        5.2 Linux文件系統

                5.2.1 Linux文件系統目錄結構

                        進入Linux根目錄(即“/”,Linux文件系統的入口,也是出於最高一級的目錄)運行“ls -l”命令,看到Linux包含以下目錄。

                        /bin:包含基本命令,如ls、cp、mkdir等,這個目錄中的文件都是可執行的。

                        /sbin:包含系統命令,如modprobe、hwclock、ifconfig等,大多是涉及系統管理的命令,這個目錄中的文件都是可執行的。

                        /dev:設備文件存儲目錄,應用程序通過對這些文件的讀寫和控制就可以訪問實際的設備。

                        /etc:系統配置文件的所在地,一些服務器的配置文件也在這裏,如用戶帳號及密碼配置文件。busybox的啓動腳本也存放在該目錄。

                        /lib:系統庫文件存放目錄,如LDD6410包含libc-2.6.1.so、libpthread-2.6.1.so、libthread-db-1.0.so等。

                        /mnt:一般是用於存放掛在存儲設備的掛在目錄的。可以參看/etc/fstab的定義。有時候我們讓系統開機自動掛載文件系統,把掛載點放在這裏也是可以的。

                        /opt:opt是”可選“的意思,有些軟件包會被安裝在這裏。

                        /proc:操作系統運行時,進程及內核信息(比如CPU、硬盤分區、內存信息等)存放在這裏。/proc目錄爲僞文件系統proc的掛載目錄,proc並不是真正的文件系統,它存在於內存之中。

                        /tmp:有時用戶運行程序的時候,會產生臨時文件,/tmp就用來存放臨時文件的。

                        /usr:是系統存放程序的目錄,比如用戶命令、用戶庫等。

                        /var:var表示的是變化的意思,這個目錄的內容經常變動,如/var的/var/log目錄被用來存放系統日誌。

                        /sys:Linux 2.6內核所支持的sysfs文件系統被影射在此目錄。Linux設備驅動模型中的總線、驅動和設備可以在sysfs文件系統中找到對應的節點。當內核檢測到在系統中出現了新的設備後,內核會在sysfs文件系統中爲該設備生成一項新的記錄。

                5.2.2 Linux文件系統與設備驅動

                        下圖爲Linux中虛擬文件系統、磁盤文件(存放於Ramdisk、Flash、ROM、SD卡、U盤等文件系統中的文件也屬於此列)及一般設備文件與設備驅動程序之間的關係:

                               

                        應用程序與VFS之間的接口是系統調用,而VFS語磁盤文件系統以及普通設備之間的接口是file_operations結構體成員函數,這個結構體包含對文件進行打開、關閉、讀寫、控制的一系列成員函數。

                        由於字符設備的上層沒有磁盤文件系統,所以字符設備的file_operations成員函數就直接由設備驅動提供了。

                        而對於塊存儲設備而言,ext2、fat、jffs2等文件系統中會實現針對VFS的file_operations成員函數,設備驅動層將看不到file_operations的存在。磁盤文件系統和設備驅動會將對磁盤上文件的訪問最終轉換成對磁盤上柱面和扇區的訪問。

                       設備驅動程序的設計中,一般而言,會關心file和inode着兩個結構體。

                       1.file結構體

                                file結構體代表一個打開的文件(設備對應於設備文件),系統中每個打開的文件在內核空間都有一個關聯的struct file。它由內核在打開文件使創建,並傳遞給在文件上進行操作的任何函數。在文件的所有實例都關閉後,內核釋放這個數據結構。在內核和驅動源代碼中,struct file的指針通常被命名爲file或filp(file pointer)。

                               struct file

                               {

                                       union{

                                               struct list_head fu_list;

                                               struct rcu_head fu_rcuhead;

                                       }f_u;

                                       struct dentry *f_dentry;    /*與文件關聯的目錄入口(dentry)結構*/

                                       struct vfsmount *f_vfsmnt;

                                       struct file_operations *f_op;  /*和文件關聯的操作*/

                                       atomic_t f_count;

                                       unsigned int f_flags;   /*文件標誌,如O_RDONLY、O_NONBLOCK、O_SYNC*/

                                       mode_t f_mode;  /*文件讀/寫模式, FMODE_READ和FMODE_WRITE*/

                                       loff_t f_pos; /*當前讀寫位置*/

                                       struct fown_struct f_owner;

                                       unsigned int f_uid, f_gid;

                                       struct file_ra_state f_ra;

                                       unsigned long f_version;

                                       void *f_security;

                                       /*tty驅動需要,其他的也許需要*/

                                       void *private_data; /*文件私有數據*/

                                       ...

                                       struct address_space *f_mapping;

                               };

                               文件讀/寫模式mode、標誌f_flags都是設備驅動關心的內容,而私有數據指針private_data在設備驅動中被廣泛應用,大多數指向設備驅動自定義用於描述設備的結構體。

                               驅動程序中經常會使用如下類似的代碼來檢測用戶打開文件的讀寫方式:

                                       if(file->f_mode & FMODE_WRITE){/*用戶要求可寫*/

                                       }

                                       if(file->f_mode & FMODE_WRITE){/*用戶要求可讀*/

                                       }

                               下面的代碼可以用於判斷以阻塞還是非阻塞方式打開設備文件:

                                       if(file->f_flags & O_NONBLOCK)     /*非阻塞*/

                                               pr_debug("open:non-bolcking\n");

                                       else                                                  /*阻塞*/

                                               pr_debug("open:blocking\n");

                        2.inode結構體

                                VFS inode包含文件訪問權限、屬主、組、大小、生成時間、訪問時間、最後修改時間等信息。它是Linux管理文件系統的最基本單位,也是文件系統連接任何子目錄、文件的橋樑,inode結構體的定義如下:

                                struct inode{

                                        ...

                                        unode_t i_mode;  /*inode的權限*/

                                        uid_t i_uid;  /*inode擁有者的id*/ 

                                        gid_t i_gid; /*inode所屬的羣組id*/

                                        dev_t i_rdev; /*若是設備文件,此字段記錄設備的設備號*/

                                        loff_t i_size;  /*inode所代表的文件大小*/

                                        struct timespec i_atime;  /*inode最近一次的存取時間*/

                                        struct timespec i_mtime; /*inode最近一次的修改時間*/

                                        struct timespec i_ctime;  /*inode的產生時間*/

                                        unsigned long i_blksize;  /*inode在做I/O時的區塊大小*/

                                        unsigned long i_blocks;  /*inode所使用的block數,一個block爲512byte*/

                                        struct block_device *i_bdev;   /*若是塊設備,爲其對應的block_device結構體指針*/                       

                                        struct cdev *i_dev;  /*若是字符設備,爲其對應的cdev結構體指針*/

                                        ...

                                };

                                對於表示設備文件的inode結構,i_rdev字段包含設備編號。Linux2.6設備編號分爲主設備編號和次設備編號,前者爲dev_t的高12位,後者爲dev_t的低20位。下列操作用於從一個inode中獲得主設備號和次設備號:

                                       unsigned int iminor(struct inode *inode);

                                       unsigned int imajor(struct inode *inode);

                               查看/proc/devices文件可以獲知系統中註冊的設備,第一列爲主設備號,第二列爲設備號。

                               查看/dev目錄可以獲知系統中包含的設備文件,日期的前兩列給出了對應設備的主設備號和次設備號。

                               主設備號是與驅動對應的概念,同一類設備一般使用相同的主設備號,不同類的設備一般使用不同的主設備號(但是也排除在同一主設備號下包含有一定差異的設備)。因爲同一驅動可支持多個同類設備,因此用次設備號來描述使用該驅動的設備的序號,序號一般從0開始。

                               內核Documents目錄下的devices.txt文件描述了Linux設備號的分配情況。

        5.3 devfs設備文件系統

                devfs(設備文件系統)是由Linux 2.4內核引入的,引入是被許多工程師給予了高度評價,它的出現使得設備驅動程序能自主管理它自己的設備文件。具體來說,devgs具有如下優點:

                        (1)可以通過程序在設備初始化時在/dev目錄下創建設備文件,卸載設備時將它刪除。

                        (2)設備驅動程序可以指定設備名、所有者和權限位,用戶空間程序仍可以修改所有者和權限位。

                        (3)不需要爲設備驅動程序分配主設備號及次設備號,在程序中直接給register_chrdev()傳遞0主設備號獲得可用的主設備號,在devfs_register指定次設備號。

                創建設備目錄:devfs_handle_t devfs_mk_dir(devfs_handle_t dir, const *name, void *info);

                創建設備文件:devfs_handle_t devfs_register(devfs_handle_t dir, const char* name, unsigned int flags, unsigned int major, unsigned int minor, umode_t mode, void* ops, void *info);

                撤銷設備文件:void devfs_unregister(devfs_handle_t de);

                在Linux 2.4的設備驅動編程中,分別在模塊加載和卸載函數中創建和撤銷設備文件是被普遍採用並值得大力推薦的好方法。

                使用的register_chrdev()和unregister_chrdev在Linux 2.6內核中雖然仍然被支持,但這是過時的做法。

        5.4 udev設備文件系統

                5.4.1 udev與devfs的區別

                        儘管devfs有這樣和那樣的優點,但是在Linux 2.6內核中,devfs被認爲是過時的方法,並最終被拋棄,udev取代了它。Linux VFS內核維護者AI Viro指出了幾點udev取代devfs的原因:

                                devfs所做的工作被確信可以在用戶態來完成。

                                devfs被加入內核時,大家寄望它的質量可以迎頭趕上。

                                devfs被發現了一些可修復和無法修復的bug。

                                對於可修復的bug,幾個月前就已經被修復,其維護者認爲一切良好。

                                對於不可修復的bug,同樣是相當長一段時間以來沒有改觀了。

                                devfs的維護者和作者對它感到失望並且已經停止了對代碼的維護工作。

                        udev完全在用戶態工作,利用設備加入或移除時內核所發出的熱插拔事件(hotplug event)來工作。在熱插拔時,設備的詳細信息會由內核輸出到位於/sys的sysfs文件系統。udev的設備命名策略、權限控制和事件處理都是在用戶態下完成的,它利用sysfs中的信息來進行創建設備文件節點等工作熱插拔時輸出到sysfs中的設備的詳細信息就是相親對象的資料(外貿、年齡、性格、籍貫等),設備命名策略等就是擇偶標準。devfs是個蹩腳的婚姻介紹所,它直接指定了誰和誰談戀愛,而udev則聰明地多,它只是把資料交給了客戶,讓客戶根據這些資料去選擇和誰談戀愛。

                        由於udev根據系統中硬件設備的狀態動態更新設備文件,進行設備文件的創建和刪除等,進行設備文件的創建和刪除等,因此,在使用udev後,/dev目錄下就會只包含系統中真正存在的設備了。

                        devfs與udev的另一個顯著區別在於:採用devfs,當一個並不存在的/dev節點被打開的時候,devfs能自動加載對應的驅動,而udev則不這麼做。這是因爲udev的設計者認爲Linux應該在設備被發現的時候加載驅動模塊,而不是當它被訪問的時候。udev的設計者認爲devfs所提供的打開/dev節點時自動加載驅動的功能對於一個配置正確的計算機是多餘的。系統中所有的設備都應該產生熱插拔事件並加載恰當的驅動,而udev能注意到這點並且爲它創建對應的設備節點。

                5.4.2 sysfs文件系統與Linux設備模型

                        Linux 2.6的內核引入了sysfs文件系統,sysfs被看成是與proc、devfs和devpty同類別的文件系統,該文件系統是一個虛擬的文件系統,它可以產生一個包含所有系統硬件的層級視圖,與提供進程和狀態信息的proc文件系統十分類似。

                        sysfs把鏈接在系統上的設備和總線組織成爲一個分級的文件,它們可以由用戶空間存取,向用戶空間導出內核數據結構以及它們的屬性。sysfs的一個目的就是展示設備驅動模型中各組件的層次關係,其頂級目錄包括block、device、bus、drivers、class、power和firmware。

                        block目錄包含所有的塊設備:devices目錄包含系統所有的設備,並根據設備掛在的總線類型組織層次結構;bus目錄包含系統中所有的總線類型;drivers目錄包含內核中所有已註冊的設備驅動程序;class目錄包含系統中的設備類型(如網卡設備、聲卡設備、輸入設備等)。在/sys目錄運行tree會得到一個相當長的樹形目錄。

                        在/sys/bus的pci等子目錄下,又會再分出drivers和devices目錄,而devices目錄中的文件是對/sys/devices目錄中文件的符號鏈接。同樣地,/sys/class目錄下也包含對許多對/sys/devices下文件的鏈接。下圖所示,這與設備、驅動、總線和類的現實狀況是直接對應的,也正符合Linux 2.6的設備模型。

                       

                         隨着技術的不斷進步,系統的拓撲結構越來越複雜,對智能電源管理、熱插拔以及即插即用的支持要求越來越高,Linux 2.4內核已經難以滿足這些需求。爲適應這種形勢的需要,Linux 2.6內核開發了上述全新的設備、總線、類和驅動環環相扣的設備模型。下圖表示了Linux驅動模型中設備、總線和類之間的關係。

                               

                       大多數情況下,Linux 2.6內核中的設備模型代碼會作爲“幕後黑手”處理好這些關係,內核中總線級和其他內核子系統會完成與設備模型的交互,這是的驅動工程師幾乎不需要關心設備模型。

                       Linux內核中,分別使用bus_type、device_driver和device來描述總線、驅動和設備。這3個結構體定義於include/linux/device.h頭文件中。

                       device_driver和device分別表示驅動和設備,而這兩者都必須依附於一種總線,因此都包含struct bus_type指針。在Linux內核中,設備和驅動是分開註冊的,註冊1個設備的時候並不需要驅動已經存在,而1個驅動被註冊的時候,也不需要對應的設備已經被註冊。設備和驅動各自涌向內核,而每個設備和驅動涌入的時候都會尋找自己的另一半。茫茫人海,何處覓蹤?正是bus_type的match()成員函數將兩者捆綁在一起,簡單地說,設備和驅動就是紅塵漂浮的男女,而bus_type的match()則是牽引紅線的月老,它可以識別什麼設備與什麼驅動可以配對。

                        注意,總線、驅動和設備都最終會落實爲sysfs中的一個目錄,因爲進一步追蹤代碼會發現,它們實際上都可以認爲是kobject的派生類(device結構體直接包含了kobject kobj成員,而bus_type和device_driver則透過bus_type_private、driver_private間接包含kobject),kobject可看作所有總線、設備和驅動的抽象基類,一個kobject對應sysfs中的一個目錄。

                        總線、設備和驅動中的各個attribute則直接落實爲sysfs中的一個文件,attribute會伴隨show()和store()這兩個函數,分別用於讀和寫該attribute對應的sysfs文件節點。

                        事實上,udev規則中各信息的來源實際上就是bus_type、device_driver、device以及attribute等所對應的sysfs節點。

                5.4.3 udev的組成

                        udev的主頁位於:http;//www/kernek.org/pub/linux/utils/kenel/hotplug/udev.html,上面包含可關於udev的詳細介紹,從http;/www.us.kernel.org/pub/linux/utils/kernek/hotplug上下載罪行的udev包。udev的設計目標如下:

                                在用戶空間執行。

                                動態建立/刪除設備文件。

                                允許每個人都不用關心主/次設備號。

                                提供LSB標準名稱。

                                如果需要,可提供固定的名稱。

                        爲了提供這些功能,udev以3個分割的子計劃發展:namedev、libsysfs和udev。namedev爲設備命名子系統,libsvsfs提供訪問sysfs文件系統從中獲取信息的標準接口,udev提供/dev設備節點文件的動態創建和刪除策略。udev程序揹負與namedev和libsysfs庫交互的任務,當/sbin/hotplug程序被內核調用時,udev將被運行。udev的工作過程如下:

                                (1)當內核檢測到系統中出現了新設備後,內核會在sysfs文件系統中爲該設備生成新紀錄並導出一些設備特定的信息及所發生的事件。

                                (2)udev獲取內核導出的信息,它調用namedev決定應該給該設備指定的名稱,如果是新插入設備,udev將調用libsysfs決定應該爲該設備的設備文件指定的主/次設備號,並用分析獲得的設備名稱和主/次設備號創建/dev中的設備文件;如果是設備移除,則之間已經被創建的/dev文件將被刪除。

                        在namedev中使用5步序列來決定指定設備的命名:

                                (1)標籤(label)/序號(serial):這一步檢查設備是否有唯一的識別記號,例如USB設備有唯一的USB序號,SCSI有唯一的UUID。如果namedev找到與這種唯一編號相對應的規則,它將使用該規則提供名稱。
                                (2)設備總線號:這一步會檢查總線設備編號,對於不可熱插拔的環境,這一步足以辨別設備。例如,PCI總線編號在系統的使用期間內很少變更。如果namedev找到相對應的規則,規則中的名稱就會被使用。

                                 (3)總線上的拓撲:當設備在總線上的位置匹配用戶指定的規則時,就會使用該規則指定指定名稱。

                                 (4)替換名稱:當內核提供的名稱匹配指定的替代字符串時,就會使用替代字符串指定的名稱。

                                 (5)內核提供的名稱:這一步“保羅萬象”,如果以前的幾個步驟的沒有被提供,默認的內核將被指定給該設備。

                5.4.4 udev規則文件

                        udev的規則文件以行爲單位,以“#”開頭的行代表註釋行。其餘的每一行代表一個規則。每個規則分成一個或多個匹配和賦值部分。匹配部分用匹配專用的關鍵字來表示,相應的賦值部分用賦值專用的關鍵字來表示。匹配關鍵字包括:ACTION(行爲)、KERNEL(匹配內核設備名)、BUS(匹配總線類型)、SYSFS(匹配從sysfs得到的信息,比如label、vendor、USB序列號)、SUBSYSTEM(匹配子系統名)等,賦值關鍵字包括:NAME(創建的設備文件名)、SYMLINK(符號創建鏈接名)、OWNER(設置設備的所有者)、GROUP(設置設備的組)、IMPORT(調用外部程序)等。

                        例如,如下規則:

                                SUBSYSTEM==“net”,ACTION==“add”,SYSFS(address)==“00:0d:87:f6:59:f3”,IMPORT=“/sbin/rename_netiface %k eth0”

                       其中的“匹配”部分有3項,分別是SUBSYSTEM、ACTION和SYSFS。而“賦值”部分有一項,是IMPORT。這個規則的意思是:當系統中出現的新硬件屬於net子系統範疇,系統對該硬件採取的動作是加入這個硬件,切這個硬件在sysfs文件系統中的“address”信息等於“00:0d:87:f6:59:f3”時,這個硬件在udev層次施行的動作是調用外部程序/sbin/rename_netiface,並傳遞給該程序兩個參數,一個是“%k”,代表內核對該新設備定義的名稱,另一個是“eth0”。

                        多個設備採用基於它們的序列號或者其他標識信息的辦法來進行確定的映射,使用下面的規則可以做到:

                                BUS=“usb”,SYSFS(serial)="HXOLL0012202323480",NAME="lp_epson",SYMLINK="printers/epson_stylus"

                        該規則中的匹配項目有BUS和SYSFS,賦值項目爲NAME和SYMLINK,它意味着當一臺USB打印機的序列號爲“HXOLL0012202323480”的USB打印機不管何時被插入,對應的設備名都是/dev/lp_epson,而devfs顯然無法實現設備的這種固定命名。

                        udev規則的寫法非常靈活,在匹配部分,可以通過“*”、"?"、[a~c]、[1~9]等shell通配符來靈活匹配多個項目。*類似於shell中的*通配符,代替任意長度的任意字符串,?代替一個字符,[x~y]是訪問定義,此外,%k就是KENEL,%n則是設備的KERNEL序號(如存儲設備的分區號)。

                        可以藉助udev中的udevinfo工具查找規則文件可以利用的信息。如運行“udevinfo -a -p/sys/block/sda”。

                5.4.5 創建和配置mdev

                        在嵌入式系統中,通常可以用udev的輕量級版本mdev,mdev集成與busybox中。在busybox的源代碼目錄運行,make menuconfig,進入“LInux System Utilities”子選項,選中mdev相關項目。

                        LDD6410根文件系統中/etc/init.d/rcS包含的如下內容即是爲了使用mdev的功能:

                                /bin/mount -t sysfs sysfs /sys

                                /bin/mount -t tmpfs mdev /dev

                                echo /bin/mdev > /proc/sys/kernel/hotplug

                                mdev -s

                        其中“mdev -s”的含義是掃描/sys中所有的類設備目錄,如果在目錄中含有名爲“dev”的文件,切文件中包含的是設備號,則mdev就利用這些信息爲該設備在/dev下創建設備節點文件。

                        “echo /sbin/mdev > /proc/sys/kernel/hotplug”的含義是當有熱插拔事件產生時,內核就會調用位於/sbin目錄的mdev。這時mdev通過環境變量中的ACTION和DEVPATH,來確定此次熱插拔事件的動作以及影響了/sys中的那個目錄。接着會看看這個目錄中是否有“dev”的屬性文件,如果有就利用這些信息爲這個設備在/dev下創建設備節點文件。

                        若要修改mdev的規則,可通過修改/etc/mdev.cong文件實現。

        5.5 LDD6410的SD和NAND文件系統

                LDD6410的SD卡分爲兩區,其中的第二個分區爲ext3文件系統,存在LDD6410的文件數據,其製作方法如下:

                        (1)在安裝了Linux的PC機上通過fdisk給一張空的SD卡分爲2個區(如果SD卡中本身已經包含,請通過fdisk的“d”命令全部刪除)

                        (2)格式化SD卡的分區1和分區2:mkfs.vfat /dev/sdb1     mkfs.ext3 /dev/sdb2        fsck.ext3 /dev/sdb2

                        (3)通過moviNAND Fusing_Tool.exe燒寫SD卡的U-BOOT和zImage。

                更新NAND中U-BOOT的方法如下:

                        (1)通過tftp或nfs等方式獲取新的U-BOOT,如:

                                #tftp -r u-boot-movi.bin -g 192.168.1.111

                        (2)運行:

                                #flashcp u-boot-movi.bin /dev/mtd0

                更新NAND中zImage的方法如下:

                        (1)通過tftp或nfs等方式獲取新的zImage,如:

                                #tftp -r zImage-fix -g 192.168.1.111

                        (2)運行:

                                #flashcp zImage-fix /dev/mtd1

                更新NAND中文件系統的方法如下:

                        在PC上將做好的新的根文件系統拷貝到SD卡或NFS的某個目錄,下面我們以<new rootfs_dir>指代該目錄

                                以SD卡或NFS爲根文件系統啓動系統,運行如下命令擦除/dev/mtd2分區:#flash_eraseall /dev/mtd2

                                然後將NAND的該分區mount到/mnt:#mount /dev/mtdblock2 -t yaffs2 /mnt/

                                將新的文件系統拷貝到/mnt:#cp -fa <new_rootfs_dir> /mnt

6.字符設備驅動

        6.1 Linux字符設備驅動結構

                6.1.1 cdev結構體

                        在Linux 2.6內核中,使用cdev結構體描述一個字符設備,cdev結構體的定義如下:

                                struct cdev{

                                        struct kobject kobj;        /*內嵌的kobject對象*/

                                        struct module *owner;        /*所屬模塊*/

                                        struct file_operations * ops;        /*文件操作結構體*/

                                        struct list_head list;

                                        dev_t dev;        /*設備號*/

                                        unsigned int count;

                                };

                        cdev結構體的dev_t成員定義了設備號,爲32位,其中12位主設備號,20位次設備號。使用下列宏可以從dev_t獲得主設備號和次設備號:

                                MAJOR(dev_t dev)

                                MANOR(dev_t dev)

                        而使用下列宏則可以通過主設備號和次設備號生成dev_t:

                                MKDEV(int major, int minor)

                        cdev結構體的另一個重要成員file_operations定義了字符設備驅動提供給虛擬文件系統的接口函數。Linux 2.6內核提供了一組函數用於操作cdev結構體:

                                void cdev_init(struct cdev *cdev, struct file_operations *fops)    /*用於初始化cdev的成員,並建立cdev和file_operations之間的連接*/

                                {

                                        memset(cdev, 0, sizeof *cdev);

                                        INIT_LIST_HEAD(&cdev->list);

                                        kobject_init(&cdev->kobj, &ktype_cdev_default);

                                        cdev->ops=fops;        /*將傳入的文件操作系統結構體指針賦值給cdev的ops*/

                                }

                                struct cdev *cdev_alloc(void)        /*用語動態申請一個cdev內存*/

                                {

                                        struct cdev *p=kzalloc(sizeof(struct cdev), GFP_KERNEL);

                                        if(p){

                                                INIT_LIST_HEAD(&p->list);

                                                kobject_init(&p->kobj, &ktype_cdev_dynamic);

                                        }

                                        return p;

                                }

                                void cdev_put(struct cdev *p);

                                int cdev_add(struct cdev*, dev_t, unsigned);        /*向系統添加一個cdev並完成設備的註冊,調用通常發生在字符設備驅動模塊加載函數中*/

                                void cdev_del(struct cdev*);        /*向系統刪除一個cdev並完成設備的註銷,調用通常發生在字符設備驅動模塊卸載函數中*/

                6.1.2 分配和釋放設備號

                        在調用cdev_add()函數向系統註冊字符設備之前,應首先調用register_chrdev_region()或alloc_chrdev_region()函數向系統申請設備號,函數原型:

                                int register_chrdev_region(dev_t from, unsigned count, const char *name)

                                int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

                        register_chrdev_region()函數用於已知起始設備的設備號的情況,而alloc_chrdev_region()用於設備號未知,向系統動態申請未被佔用的設備號的情況,函數調用成功之後,會把得到的設備號放在第一個參數dev中。alloc_chrdev_region()與register_chrdev_region()對比的優點在於它會自動避開設備號重複的衝突。

                        相反地,在調用cdev_del()函數從系統註銷字符設備之後,unregister_chrdev_region()應該被調用以釋放原先申請的設備號,函數的原型:

                                void unregister_chrdev_region(dev_t from, unsigned count);

                6.1.3 file_operations結構體

                        file_operations結構體中的成員函數是字符設備驅動程序設計的主體內容,這些函數實際會在應用程序進行Linux的open()、write()、read()、close()等系統調用時最終被調用。file_operations結構體目前已經比較龐大,定義如下:

                                struct file_operations{

                                        struct module *owner;        /*擁有該結構的模塊的指針,一般爲THIS_MODULES*/

                                        loff_t(*llseek)(struct file *, loff_t, int);        /*用來修改文件當前的讀寫位置*/

                                        ssize_t(*read)(struct file *, char __user*, size_t, loff_t*);        /*從設備中同步讀取數據*/

                                        ssize_t(*write)(struct file *, const char __user*, size_t, loff_t*);        /*向設備發送數據*/

                                        ssize_t(*aio_read)(struct file *, char __user*, size_t, loff_t);        /*初始化一個異步的讀取操作*/

                                        ssize_t(*aio_write)(struct file *, const char __user*, size_t, loff_t);        /*初始化一個異步的寫入操作*/

                                        int (*readdir)(struct file*, void*, filldir_t);        /*僅用於讀取目錄,對於設備文件,該字段爲NULL*/

                                        unsigned int(*poll)(struct file*, struct_poll_table_struct*);        /*輪訓函數,判斷目前是否可以進行非阻塞的讀寫或寫入*/

                                        int(*ioctl)(struct inode*, struct file*, unsigned int, unsigned long);        /*執行設備I/O控制命令*/

                                        long(*unlocked_ioctl)(struct file*, unsigned int, unsigned long);        /*不使用BLK的文件系統,將使用此種函數指針代替ioctl*/

                                        long(*compat_ioctl)(struct file*, unsigned int, unsigned long);        /*在64位系統上,32位的ioctl調用將使用此函數指針代替*/

                                        int(*mmp)(struct file*, struct vm_area_struct*);        /*用於請求將設備內存映射到進程地址空間*/

                                        int(*open)(struct inode*, struct file*);        /*打開*/

                                        int(*flush)(struct file*);

                                        int(*release)(struct inode*, struct file*);        /*關閉*/

                                        int(*fsync)(struct file*, struct dentry*, int datasync);        /*刷新待處理的數據*/

                                        int(*aio_fsync)(struct kiocb*, int datasync);        /*異步fsync*/

                                        int(*fasync)(int, struct file*, int);        /*通知設備FASYNC標誌發生變化*/

                                        int(*lock)(struct file*, int, struct file_lock*)

                                        ssize_t(*sendpage)(struct file*, struct page*, int, size_t, loff_t*, int);        /*通常爲NULL*/

                                        unsigned long(*get_unmapped_area)(struct file*,unsigned long,unsigned long,unsigned long,unsigned long);/*當前進程地址空間找1未映射個內存段*/

                                        int(*check_flags)(int);        /*允許模塊檢查傳遞給fcntl(F_SETEL...)調用的標誌*/

                                        int(*dir_notify)(struct file *filp, unsigned long arg);        /*對文件系統有效,驅動程序不必實現*/

                                        int(*flock)(struct file*, int, struct file_lock*); 

                                        ssize_t(*splice_write)(struct pipe_inode_info*, struct file*, loff_t*, size_t, unsigned int);        /*由VFS調用,將管道數據粘接到文件*/

                                        ssize_t(*splice_read)(struct file*, loff_t*, struct pipe_inode_info*, size_t, unsigned int);        /*由VFS調用,將文件數據粘接到管道*/

                                        int(*setlease)(struct file*, long, struct file_lock**);

                                };

                        下面我們對file_operations結構體中的主要成員進行分析:

                                llseek函數用來修改一個文件的當前讀寫位置,並將新位置返回,在出錯時,這個函數返回一個負值。

                                read函數用來從設備中讀取數據,成功時函數返回讀取的字節數,出錯時返回一個負值。

                                write函數想設備發送數據,成功是該函數返回寫入的字節數,如果此函數未被實現,當用戶write系統調用時,將得到-EINVAL返回值。

                                readdir函數僅用於目錄,設備節點不需要實現它。

                                ioctl提供設備相關控制命令的實現(既不是讀操作也不是寫操作),當調用成功時,返回給調用程序一個非負值。

                                mmap函數將設備內存映射到進程內存中,如果設備驅動未實現此函數,當mmap系統調用時獲得-ENODEV返回值。此函數對於幀緩衝等設備特別有意義。

                                當用戶空間調用LInux API函數open打開設備文件時,設備驅動的open函數最終被調用。驅動程序可以不實現這個函數,在這種情況下,設備的打開操作永遠成功。與open函數對應的是release函數。

                                poll函數一般用於詢問設備是否可被非阻塞地立即讀寫。當詢問的條件未觸發時,用戶空間進行select和poll系統調用將引起進程的阻塞。

                                aio_read和aio_write函數分別對與文件描述符對應的設備進行異步讀、寫操作。設備實現這兩個函數後,用戶空間可以對該設備文件描述符調用aio_read、aio_write等系統調用進行讀寫。

                6.1.4 Linux字符設備驅動的組成

                        1.字符設備驅動模塊加載和卸載函數

                                 在字符設備驅動模塊加載函數中應該實現設備號的申請和cdev的註冊,而在卸載函數中應實現設備號的釋放和cdev的註銷。

                                 工程師通常習慣爲設備定義一個設備相關的結構體,其包含該設備所涉及的cdev、私有數據及信號量等信息。常見的設備結構體、模塊加載和卸載函數形式如下:

                                         /*設備結構體*/

                                         struct xxx_dev_t{

                                                 struct cdev cdev;

                                                 ...

                                         }

                                         /*設備驅動模塊加載函數*/

                                         static int __init xxx_init(void)

                                         {

                                                 cdev_init(&xxx_dev.cdev, &xxx_fops);        /*初始化cdev*/

                                                 xxx_dev.cdev.owner=THIS_MODULE;

                                                 /*獲取字符設備號*/

                                                 if(xxx_major){

                                                         register_chrdev_region(xxx_dev_no, 1, DEV_NAME);

                                                 }else{

                                                         alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);

                                                 }

                                                 ret=cdev_add(&xxx_dev.cdev, xxx_dev_no, 1);        /*註冊設備*/

                                         }

                                         /*設備驅動模塊卸載函數*/

                                         static void __exit xxx_exit(void)

                                         {

                                                 unregister_chrdev_region(xxx_dev_no, 1);        /*釋放佔用的設備號*/

                                                 cdev_del(&xxx_dev.cdev);        /*註銷設備*/

                                         }

                         2.字符設備驅動的file_operations結構體中成員函數

                                 file_operations結構體中成員函數是字符設備驅動與內核的接口,是用戶空間對Linux進行系統調用最終的落實者。大多數字符設備驅動會read()、write()、ioctl()函數,常見的字符設備驅動的這3個函數的形式如下實現:

                                         /*讀設備*/

                                         ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)

                                         {

                                                 ...

                                                 copy_to_user(buf, ..., ...);

                                                 ...

                                         }

                                         /*寫設備*/

                                         ssize_t xxx_write(struct file *flip, const char __user *buf, size_t count, loff_t *f_pos)

                                         {

                                                 ...

                                                 copy_from_user(..., buf, ...);

                                                 ...

                                         }

                                         /*ioctl函數*/

                                         int xxx_ioctl(struct inode *inode, struct file *flip, unsigned int cmd, unsigned long arg)

                                         {

                                                 ...

                                                 switch(cmd){

                                                 case XXX_CMD1:

                                                         ...

                                                         break;

                                                 case XXX_CMD2:

                                                         ...

                                                         break;

                                                 default:        /*不能支持的命令*/

                                                         return -ENOTTY;

                                                  }

                                                  return 0;

                                         }

                                 設備驅動的讀函數中,filp是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,count是要讀的字節數,f_pos是讀的位置相對於文件開頭的偏移。

                                 設備驅動的寫函數中,filp是文件結構體指針,buf是用戶空間內存的地址,該地址的內核空間不能直接讀寫,count是要寫的字節數,f_pos是寫的位置相對於文件開頭的偏移。

                                 由於內核空間與用戶空間的內存不能直接互訪,因此藉助了函數copy_from_user()完成用戶空間到內核空間的拷貝,以及copy_to_user()完成內核空間到用戶空間的拷貝。原型分別爲:

                                         unsigned long copy_from_user(void* to, const void __user *from, unsigned long count);

                                         unsigned long copy_to_user(void __user * to, const void *from, unsigned long count);

                                 上述函數均返回不能被複制的字節數,因此,如果完全複製成功,返回值爲0。

                                 如果要複製的內存是簡單的類型,如char、int、long等,則可以使用簡單的put_user()和get_user()。

                                 讀和寫函數中的__user是一個宏,表明其後的指針指向用戶空間,這個宏定義爲:

                                         #ifdef __CHECKER__

                                         #degine __user        __attribute__((noderef, address_space(1)))

                                         #else

                                         #define __user

                                         #endif

                                 I/O控制函數的cmd參數爲事先定義的I/O控制命令,而arg爲對應於該命令的參數。例如對於串行設備,如果SET_BAUDRATE是一道設置波特率的命令,那後面的arg就應該是一個波特率值。

                                 在字符設備驅動中,需要定義一個file_operations的實例,並將具體設備驅動的函數賦值給file_operations的成員,如下代碼實現:

                                         struct file_operations xxx_fops={

                                                 .owner=THIS_MODULE,

                                                 .read=xxx_read,

                                                 .write=xxx_write,

                                                 .ioctl=xxx_ioctl,

                                                 ...

                                         };

                                 通過cdev_init(&xxx_dev.cdev, &xxx_fops)語句被建立與cdev的連接。

                                 下圖所示爲字符設備驅動的結構、字符設備驅動與字符設備以及字符設備驅動與用戶空間訪問該設備的程序之間的關係。

                                

        6.2 golbalmem虛擬設備實例描述

                從本章開始,後續的數章都將基於虛擬的globalmem設備進行字符設備驅動的講解。globalmem意味着“全局內存”,在globalmem字符設備驅動中會分配一片大小爲GLOBALMEM_SIZE(4KB)的內存空間,並在驅動中提供針對該片內存的讀寫、控制和定位函數,以供用戶空間的進程能通過Linux系統調用訪問這片內存。

                實際上,這個虛擬的globalmem設備幾乎沒有任何實用價值,僅僅是一種爲了講解問題的方便而憑空製造的設備。當然,它也並非百無一用,由於global可被兩個或兩個以上的進程同時訪問,其中的全局內存可作爲用戶空間進程進行通信的一種蹩腳的手段。

        6.3 globalmem設備驅動

                6.3.1 頭文件、宏及設備結構體

                        在globalmem字符設備驅動中,應包含它要使用的頭文件,並定義globalmem設備結構體及相關宏。

                6.3.2 加載與卸載設備驅動

                6.3.3 讀寫函數

                        globalmem設備驅動的讀寫函數主要是讓那個設備結構體的mem[]數組與用戶空間交互數據,並隨着訪問的字節數變更返回給用戶的文件讀寫偏移位置。

                6.3.4 seek函數

                        seek函數對文件定位的起始死值可以是文件開頭(SEEK_SET, 0)、當前位置(SEEK_CUR,1)和文件尾(SEEK_END, 2)globalmem支持從我呢間開頭和當前位置相對偏移。

                        在定位的時候,應該檢查用戶請求的合法性,若不合法,函數返回-EINVAL,合法時返回文件的當前位置。

                6.3.5 ioctl函數

                        1.globalmem設備驅動的ioctl()函數

                                 globalmem設備驅動的ioctl函數接受MEM_CLEAR命令,這個命令會將全局內存的有效數據長度清0,對於設備不支持的命令,ioctl函數應該返回-EINVAL。

                        2.ioctl命令

                                Linux建議如下圖所示的方式定義ioctl的命令。

                               

                                命令嗎的設備類型字段爲一個“幻數”,可以是0~0xff之間的值,內核中的ioctl-number.txt給出了一些推薦的和已經被使用的“幻數”,新設備驅動定義“幻數”的時候要避免與其衝突。

                                命令碼的序列號也是8位寬。

                                命令碼的方向字段爲2位,該字段表示數據傳送的方向,可能的值是_IOC_NONE(無數據傳輸)、_IOC_READ(讀)、_IOC_WRITE(寫)和_IOC_READ|_IOC_WRITE(雙向)。數據傳送的方向是從應用程序的角度來看的。

                                命令碼的數據長度字段表示涉及的用戶數據的大小,這個成員的寬度依賴於體系結構,通常是13或者14位。

                                內核還定義了_IO()、_IOR()、_IOW()和_IOWR()這4個宏來輔助生成命令,這4個宏的通用定義如下:

                                        #define _IOC(dir, type, nr, size) (((dir)<< _IOC_DIRSHIFT)|((type)<<_IOC_TYPESHIFT)|((nr<<_IOC_NRSHIFT)|((size)<<_IOC_SIZESHIFT))

                                        #define _IO(type, nr)        _IOC(_IOC_NONE,(type),(nr),0)

                                        #define _IOR(type, nr, size)    _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

                                        #define _IOW(type, nr, size)    _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

                                        #define _IOWR(type, nr, size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr), (_IOC_TYPECHECK(size)))

                                由此可見,這幾個宏的作用是根據傳入的type(設備類型字段)、nr(序列號字段)和size(數據長度字段)和宏名隱含的方向字段移位組合生成命令碼。

                                由於globalmem的MEM_CLEAR命令不涉及數據傳輸,因此它可定義爲:

                                        #define GLOBALMEM_MAGIC ...

                                        #define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)

                        3.預定義命令

                                內核中預定義了一些I/O控制命令,如果某設備驅動中包含了與預定義命令一樣的命令碼,這些命令會被當作預定義命令被內核處理而不是被設備驅動處理,如定義命令有如下4種。

                                (1)FIOCLEX:即File IOctl Close on Exec,對文件設置專用標誌,通知內核當exec()系統調用發生時自動關閉打開的文件。#define FIONCLEX 0x5450

                                (2)FIONCLEX:即File IOctl Not Close on Exec,,與FIOCLEX標誌相反,清除由FIOCLEX命令設置的標誌。#define FIOCLEX 0x5451

                                (3)FIOQSIZE:獲得一個文件或者目錄的大小,當用於設備文件時,返回一個ENOTTY錯誤。#define FIOQSIZE 0x5460

                                (4)FIONBIO:即File IOctl Non-Blocking I/O,這個調用修改在filp->f_flags中的O_NONBLOCK標誌。#define FIONBIO 0x5421

                6.3.6 使用文件私有數據

                        大多數Linux驅動工程師遵循一個“潛規則”,那就是將文件的私有數據private_data指向設備結構體,在read()、write()、ioctl()、llseek()等函數通過private_data訪問設備結構體。

                        container_off()的作用是通過結構體成員的指針找到對應結構體的指針,這個技巧在Linux內核編程中十分常用。在container_of(inode->i_cdev, struct globalmem_dev, cdev)語句中,傳給container_of()的第一個參數是結構體成員的指針,第2個參數爲整個結構體的類型,第3個參數爲傳入的第1個參數即結構體成員的類型,container_of返回值爲整個結構體的指針。

        6.4 globalmem驅動在用戶空間的驗證

                在對應目錄通過make命令編譯globalmem的驅動,得到globalmem.ko文件。運行:

                        ~/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6$sudo su

                        ~/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6$ insmod globalmem.ko

                命令加載模塊,通過lsmod命令,發現globalmem模塊已被加載。再通過cat/proc/devices命令查看,發現多出了主設備號爲250的globalmem字符設備驅動。

                接下來,通過命令:

                        /home/lihacker/develop/svn/ldd6410-read-only/trainling/kernel/drivers/globalmem/ch6$mkmod /dev/globalmem c 250 0

                創建/dev/globalmem設備節點,並通過echo 'hello world'>/dev/globalmem命令和cat /dev/globalmem命令分別驗證設備的寫和讀,結果證明hello world字符串被正確地寫入globalmem字符設備:

                        /home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6#echo "hello world" > /dev/globalmem

                        /home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6#cat /dev/globalmem

                如果啓用了sysfs文件系統,將發現多出了/sys/module/globalmem目錄。

                refcnt記錄了globalmem模塊的引用計數,sections下包含的數個文件則給出了globalmem所包含的BSS、數據段和代碼段等的地址及其他信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章