- Linux設備驅動開發詳解(第2版)
- 華清遠見嵌入式培訓中心 宋寶華編著
- 280字
- 2018-12-27 10:06:16
第6章字符設備驅動
本章導讀
在整個Linux設備驅動的學習中,字符設備驅動較為基礎。本章將講解Linux字符設備驅動程序的結構,并解釋其主要組成部分的編程方法。
6.1節講解了 Linux 字符設備驅動的關鍵數據結構 cdev 及 file_operations 結構體的操作方法,并分析了 Linux 字符設備的整體結構,給出了簡單的設計模板。
6.2節描述了本章及后續各章節所基于的globalmem虛擬字符設備,第6~9章都將基于該虛擬設備實例進行字符設備驅動及并發控制等知識的講解。
6.3節依據6.1節的知識講解globalmem設備的驅動編寫方法,對讀寫函數、seek()函數和 I/O 控制函數等進行了重點分析。該節的最后改造globalmem的驅動程序以利用文件私有數據。
6.4節給出了6.3節的globalmem設備驅動在用戶空間的驗證。
6.1 Linux字符設備驅動結構
6.1.1 cdev結構體
在Linux2.6內核中,使用cdev結構體描述一個字符設備,cdev結構體的定義如代碼清單6.1。
代碼清單6.1 cdev結構體
1 struct cdev { 2 struct kobject kobj; /* 內嵌的kobject對象 */ 3 struct module *owner; /*所屬模塊*/ 4 struct file_operations *ops; /*文件操作結構體*/ 5 struct list_head list; 6 dev_t dev; /*設備號*/ 7 unsigned int count; 8 };
cdev結構體的dev_t成員定義了設備號,為32位,其中12位主設備號,20位次設備號。使用下列宏可以從dev_t獲得主設備號和次設備號:
MAJOR(dev_t dev) MINOR(dev_t dev)
而使用下列宏則可以通過主設備號和次設備號生成dev_t:
MKDEV(int major, int minor)
cdev 結構體的另一個重要成員 file_operations 定義了字符設備驅動提供給虛擬文件系統的接口函數。
Linux2.6內核提供了一組函數用于操作cdev結構體:
void cdev_init(struct cdev *, struct file_operations *); struct cdev *cdev_alloc(void); void cdev_put(struct cdev *p); int cdev_add(struct cdev *, dev_t, unsigned); void cdev_del(struct cdev *);
cdev_init()函數用于初始化cdev的成員,并建立cdev和file_operations之間的連接,其源代碼如代碼清單6.2所示。
代碼清單6.2 cdev_init()函數
1 void cdev_init(struct cdev *cdev, struct file_operations *fops) 2 { 3 memset(cdev, 0, sizeof *cdev); 4 INIT_LIST_HEAD(&cdev->list); 5 kobject_init(&cdev->kobj, &ktype_cdev_default); 6 cdev->ops = fops; /*將傳入的文件操作結構體指針賦值給cdev的ops*/ 7 }
cdev_alloc()函數用于動態申請一個cdev內存,其源代碼如代碼清單6.3所示。
代碼清單6.3 cdev_alloc()函數
1 struct cdev *cdev_alloc(void) 2 { 3 struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); 4 if (p) { 5 INIT_LIST_HEAD(&p->list); 6 kobject_init(&p->kobj, &ktype_cdev_dynamic); 7 } 8 return p; 9 }
cdev_add()函數和cdev_del()函數分別向系統添加和刪除一個cdev,完成字符設備的注冊和注銷。對cdev_add()的調用通常發生在字符設備驅動模塊加載函數中,而對cdev_del()函數的調用則通常發生在字符設備驅動模塊卸載函數中。
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結構體目前已經比較龐大,它的定義如代碼清單6.4所示。
代碼清單6.4 file_operations結構體
1 struct file_operations { 2 struct module *owner; 3 /* 擁有該結構的模塊的指針,一般為THIS_MODULES */ 4 loff_t(*llseek)(struct file *, loff_t, int); 5 /* 用來修改文件當前的讀寫位置*/ 6 ssize_t(*read)(struct file *, char __user *, size_t, loff_t*); 7 /* 從設備中同步讀取數據 */ 8 ssize_t(*write)(struct file *, const char __user *, size_t, loff_t*); 9 /* 向設備發送數據*/ 10 ssize_t(*aio_read)(struct kiocb *, char __user *, size_t, loff_t); 11 /* 初始化一個異步的讀取操作*/ 12 ssize_t(*aio_write)(struct kiocb *, const char __user *, size_t, loff_t); 13 /* 初始化一個異步的寫入操作*/ 14 int(*readdir)(struct file *, void *, filldir_t); 15 /* 僅用于讀取目錄,對于設備文件,該字段為 NULL */ 16 unsigned int(*poll)(struct file *, struct poll_table_struct*); 17 /* 輪詢函數,判斷目前是否可以進行非阻塞的讀取或寫入*/ 18 int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); 19 /* 執行設備I/O控制命令*/ 20 long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); 21 /* 不使用BLK的文件系統,將使用此種函數指針代替ioctl */ 22 long(*compat_ioctl)(struct file *, unsigned int, unsigned long); 23 /* 在64位系統上,32位的ioctl調用,將使用此函數指針代替*/ 24 int(*mmap)(struct file *, struct vm_area_struct*); 25 /* 用于請求將設備內存映射到進程地址空間*/ 26 int(*open)(struct inode *, struct file*); 27 /* 打開 */ 28 int(*flush)(struct file*); 29 int(*release)(struct inode *, struct file*); 30 /* 關閉*/ 31 int (*fsync) (struct file *, struct dentry *, int datasync); 32 /* 刷新待處理的數據*/ 33 int(*aio_fsync)(struct kiocb *, int datasync); 34 /* 異步fsync */ 35 int(*fasync)(int, struct file *, int); 36 /* 通知設備FASYNC標志發生變化*/ 37 int(*lock)(struct file *, int, struct file_lock*); 38 ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int); 39 /* 通常為NULL */ 40 unsigned long(*get_unmapped_area)(struct file *,unsigned long, unsigned long, 41 unsigned long, unsigned long); 42 /* 在當前進程地址空間找到一個未映射的內存段 */ 43 int(*check_flags)(int); 44 /* 允許模塊檢查傳遞給fcntl(F_SETEL...)調用的標志 */ 45 int(*dir_notify)(struct file *filp, unsigned long arg); 46 /* 對文件系統有效,驅動程序不必實現*/ 47 int(*flock)(struct file *, int, struct file_lock*); 48 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, 49 unsigned int); /* 由VFS調用,將管道數據粘接到文件 */ 50 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, 51 unsigned int); /* 由VFS調用,將文件數據粘接到管道 */ 52 int (*setlease)(struct file *, long, struct file_lock **); 53 };
下面我們對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字符設備驅動的組成
在Linux中,字符設備驅動由如下幾個部分組成。
1.字符設備驅動模塊加載與卸載函數
在字符設備驅動模塊加載函數中應該實現設備號的申請和 cdev 的注冊,而在卸載函數中應實現設備號的釋放和cdev的注銷。
工程師通常習慣為設備定義一個設備相關的結構體,其包含該設備所涉及的cdev、私有數據及信號量等信息。常見的設備結構體、模塊加載和卸載函數形式如代碼清單6.5所示。
代碼清單6.5 字符設備驅動模塊加載與卸載函數模板
1 /* 設備結構體 2 struct xxx_dev_t { 3 struct cdev cdev; 4 ... 5 } xxx_dev; 6 /* 設備驅動模塊加載函數 7 static int __init xxx_init(void) 8 { 9 ... 10 cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化cdev */ 11 xxx_dev.cdev.owner = THIS_MODULE; 12 /* 獲取字符設備號*/ 13 if (xxx_major) { 14 register_chrdev_region(xxx_dev_no, 1, DEV_NAME); 15 } else { 16 alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME); 17 } 18 19 ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注冊設備*/ 20 ... 21 } 22 /*設備驅動模塊卸載函數*/ 23 static void __exit xxx_exit(void) 24 { 25 unregister_chrdev_region(xxx_dev_no, 1); /* 釋放占用的設備號*/ 26 cdev_del(&xxx_dev.cdev); /* 注銷設備*/ 27 ... 28 }
2.字符設備驅動的file_operations結構體中成員函數
file_operations結構體中成員函數是字符設備驅動與內核的接口,是用戶空間對Linux進行系統調用最終的落實者。大多數字符設備驅動會實現read()、write()和ioctl()函數,常見的字符設備驅動的這3個函數的形式如代碼清單6.6所示。
代碼清單6.6 字符設備驅動讀、寫、I/O控制函數模板
1 /* 讀設備*/ 2 ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, 3 loff_t*f_pos) 4 { 5 ... 6 copy_to_user(buf, ..., ...); 7 ... 8 } 9 /* 寫設備*/ 10 ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, 11 loff_t *f_pos) 12 { 13 ... 14 copy_from_user(..., buf, ...); 15 ... 16 } 17 /* ioctl函數 */ 18 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, 19 unsigned long arg) 20 { 21 ... 22 switch (cmd) { 23 case XXX_CMD1: 24 ... 25 break; 26 case XXX_CMD2: 27 ... 28 break; 29 default: 30 /* 不能支持的命令 */ 31 return - ENOTTY; 32 } 33 return 0; 34 }
設備驅動的讀函數中,filp是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,count 是要讀的字節數,f_pos是讀的位置相對于文件開頭的偏移。
設備驅動的寫函數中,filp是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,count 是要寫的字節數,f_pos是寫的位置相對于文件開頭的偏移。
由于內核空間與用戶空間的內存不能直接互訪,因此借助了函數 copy_from_user()完成用戶空間到內核空間的拷貝,以及copy_to_user()完成內核空間到用戶空間的拷貝,見代碼第6行和第14行。
完成內核空間和用戶空間內存拷貝的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(),如:
int val; /* 內核空間整型變量 ... get_user(val, (int *) arg); /* 用戶→內核,arg是用戶空間的地址 ... put_user(val, (int *) arg); /* 內核→用戶,arg是用戶空間的地址
讀和寫函數中的_ _user是一個宏,表明其后的指針指向用戶空間,這個宏定義為:
#ifdef __CHECKER__ # define __user __attribute__((noderef, address_space(1))) #else # define __user #endif
I/O控制函數的cmd參數為事先定義的I/O控制命令,而arg為對應于該命令的參數。例如對于串行設備,如果SET_BAUDRATE是一道設置波特率的命令,那后面的arg就應該是波特率值。
在字符設備驅動中,需要定義一個file_operations 的實例,并將具體設備驅動的函數賦值給file_operations的成員,如代碼清單6.7所示。
代碼清單6.7字符設備驅動文件操作結構體模板
1 struct file_operations xxx_fops = { 2 .owner = THIS_MODULE, 3 .read = xxx_read, 4 .write = xxx_write, 5 .ioctl = xxx_ioctl, 6 ... 7 };
上述xxx_fops在代碼清單6.5第10行的cdev_init(&xxx_dev.cdev, &xxx_fops)的語句中被建立與cdev的連接。
圖6.1所示為字符設備驅動的結構、字符設備驅動與字符設備以及字符設備驅動與用戶空間訪問該設備的程序之間的關系。

圖6.1 字符設備驅動的結構
6.2 globalmem虛擬設備實例描述
從本章開始,后續的數章都將基于虛擬的globalmem設備進行字符設備驅動的講解。globalmem意味著“全局內存”,在globalmem字符設備驅動中會分配一片大小為GLOBALMEM_SIZE(4KB)的內存空間,并在驅動中提供針對該片內存的讀寫、控制和定位函數,以供用戶空間的進程能通過Linux系統調用訪問這片內存。
實際上,這個虛擬的globalmem設備幾乎沒有任何實用價值,僅僅是一種為了講解問題的方便而憑空制造的設備。當然,它也并非百無一用,由于globalmem可被兩個或兩個以上的進程同時訪問,其中的全局內存可作為用戶空間進程進行通信的一種蹩腳的手段。
本章將給出globalmem設備驅動的雛形,而后續章節會在這個雛形的基礎上添加并發與同步控制等復雜功能。
6.3 globalmem設備驅動
6.3.1 頭文件、宏及設備結構體
在globalmem字符設備驅動中,應包含它要使用的頭文件,并定義globalmem設備結構體及相關宏。
代碼清單6.8 globalmem設備結構體和宏
1 #include <linux/module.h> 2 #include <linux/types.h> 3 #include <linux/fs.h> 4 #include <linux/errno.h> 5 #include <linux/mm.h> 6 #include <linux/sched.h> 7 #include <linux/init.h> 8 #include <linux/cdev.h> 9 #include <asm/io.h> 10 #include <asm/system.h> 11 #include <asm/uaccess.h> 12 13 #define GLOBALMEM_SIZE 0x1000 /*全局內存大小:4KB*/ 14 #define MEM_CLEAR 0x1 /*清零全局內存*/ 15 #define GLOBALMEM_MAJOR 250 /*預設的globalmem的主設備號*/ 16 17 static int globalmem_major = GLOBALMEM_MAJOR; 18 /*globalmem設備結構體*/ 19 struct globalmem_dev { 20 struct cdev cdev; /*cdev結構體*/ 21 unsigned char mem[GLOBALMEM_SIZE]; /*全局內存*/ 22 }; 23 24 struct globalmem_dev dev; /*設備結構體實例*/
從第19~22行代碼可以看出,定義的globalmem_dev設備結構體包含了對應于globalmem字符設備的cdev、使用的內存 mem[GLOBALMEM_SIZE]。當然,程序中并不一定要把mem[GLOBALMEM_SIZE]和cdev包含在一個設備結構體中,但這樣定義的好處在于,它借用了面向對象程序設計中“封裝”的思想,體現了一種良好的編程習慣。
6.3.2 加載與卸載設備驅動
globalmem設備驅動的模塊加載和卸載函數遵循代碼清單6.5的類似模板,其實現的工作與代碼清單6.5完全一致,如代碼清單6.9所示。
代碼清單6.9 globalmem設備驅動模塊加載與卸載函數
1 /*globalmem設備驅動模塊加載函數*/ 2 int globalmem_init(void) 3 { 4 int result; 5 dev_t devno = MKDEV(globalmem_major, 0); 6 7 /* 申請字符設備驅動區域*/ 8 if (globalmem_major) 9 result = register_chrdev_region(devno, 1, "globalmem"); 10 else { 11 /* 動態獲得主設備號 */ 12 result = alloc_chrdev_region(&devno, 0, 1, "globalmem"); 13 globalmem_major = MAJOR(devno); 14 } 15 if (result < 0) 16 return result; 17 18 globalmem_setup_cdev(); 19 return 0; 20 } 21 22 /*globalmem設備驅動模塊卸載函數*/ 23 void globalmem_exit(void) 24 { 25 cdev_del(&dev.cdev); /*刪除cdev結構*/ 26 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);/*注銷設備區域*/ 27 }
第18行調用的globalmem_setup_cdev()函數完成cdev的初始化和添加,如代碼清單6.10所示。
代碼清單6.10 初始化并添加cdev結構體
1 /*初始化并添加cdev結構體*/ 2 static void globalmem_setup_cdev() 3 { 4 int err, devno = MKDEV(globalmem_major, 0); 5 6 cdev_init(&dev.cdev, &globalmem_fops); 7 dev.cdev.owner = THIS_MODULE; 8 err = cdev_add(&dev.cdev, devno, 1); 9 if (err) 10 printk(KERN_NOTICE "Error %d adding globalmem", err); 11 }
在cdev_init()函數中,與globalmem的cdev關聯的file_operations結構體如代碼清單6.11所示。
代碼清單6.11 globalmem設備驅動文件操作結構體
1 static const struct file_operations globalmem_fops = { 2 .owner = THIS_MODULE, 3 .llseek = globalmem_llseek, 4 .read = globalmem_read, 5 .write = globalmem_write, 6 .ioctl = globalmem_ioctl, 7 };
6.3.3 讀寫函數
globalmem 設備驅動的讀寫函數主要是讓設備結構體的mem[]數組與用戶空間交互數據,并隨著訪問的字節數變更返回給用戶的文件讀寫偏移位置。讀和寫函數的實現分別如代碼清單6.12和6.13所示。
代碼清單6.12 globalmem設備驅動讀函數
1 static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t count, 2 loff_t *ppos) 3 { 4 unsigned long p = *ppos; 5 int ret = 0; 6 7 /*分析和獲取有效的讀長度*/ 8 if (p >= GLOBALMEM_SIZE) /* 要讀的偏移位置越界 9 return 0; 10 if (count > GLOBALMEM_SIZE - p)/* 要讀的字節數太大 11 count = GLOBALMEM_SIZE - p; 12 13 /*內核空間→用戶空間*/ 14 if (copy_to_user(buf, (void*)(dev.mem + p), count)) 15 ret = - EFAULT; 16 else { 17 *ppos += count; 18 ret = count; 19 20 printk(KERN_INFO "read %d bytes(s) from %d\n", count, p); 21 } 22 23 return ret; 24 }
代碼清單6.13 globalmem設備驅動寫函數
1 static ssize_t globalmem_write(struct file *filp, const char __user *buf, 2 size_t count, loff_t *ppos) 3 { 4 unsigned long p = *ppos; 5 int ret = 0; 6 7 /*分析和獲取有效的寫長度*/ 8 if (p >= GLOBALMEM_SIZE) /* 要寫的偏移位置越界 9 return 0; 10 if (count > GLOBALMEM_SIZE - p) /* 要寫的字節數太多 11 count = GLOBALMEM_SIZE - p; 12 13 /*用戶空間→內核空間*/ 14 if (copy_from_user(dev.mem + p, buf, count)) 15 ret = - EFAULT; 16 else { 17 *ppos += count; 18 ret = count; 19 20 printk(KERN_INFO "written %d bytes(s) from %d\n", count, p); 21 } 22 23 return ret; 24 }
6.3.4 seek函數
seek()函數對文件定位的起始地址可以是文件開頭(SEEK_SET,0)、當前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),globalmem支持從文件開頭和當前位置相對偏移。
在定位的時候,應該檢查用戶請求的合法性,若不合法,函數返回- EINVAL,合法時返回文件的當前位置,如代碼清單6.14。
代碼清單6.14 globalmem設備驅動seek()函數
1 static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) 2 { 3 loff_t ret; 4 switch (orig) { 6 case 0: /*從文件開頭開始偏移*/ 7 if (offset < 0) { 8 ret = - EINVAL; 9 break; 10 } 11 if ((unsigned int)offset > GLOBALMEM_SIZE) { 12 ret = - EINVAL; 13 break; 14 } 15 filp->f_pos = (unsigned int)offset; 16 ret = filp->f_pos; 17 break; 18 case 1: /*從當前位置開始偏移*/ 19 if ((filp->f_pos + offset) > GLOBALMEM_SIZE) { 20 ret = - EINVAL; 21 break; 22 } 23 if ((filp->f_pos + offset) < 0) { 24 ret = - EINVAL; 25 break; 26 } 27 filp->f_pos += offset; 28 ret = filp->f_pos; 29 break; 30 default: 31 ret = - EINVAL; 32 } 33 return ret; 34 }
6.3.5 ioctl函數
1.globalmem設備驅動的ioctl()函數
globalmem設備驅動的ioctl()函數接受MEM_CLEAR命令,這個命令會將全局內存的有效數據長度清0,對于設備不支持的命令,ioctl()函數應該返回- EINVAL,如代碼清單6.15所示。
代碼清單6.15 globalmem設備驅動的I/O控制函數
1 static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned 2 int cmd, unsigned long arg) 3 { 4 switch (cmd) { 5 case MEM_CLEAR: 6 /* 清除全局內存 7 memset(dev->mem, 0, GLOBALMEM_SIZE); 8 printk(KERN_INFO "globalmem is set to zero\n"); 9 break; 10 11 default: 12 return - EINVAL; /* 其他不支持的命令 13 } 14 return 0; 15 }
在上述程序中,MEM_CLEAR 被宏定義為 0x01,實際上并不是一種值得推薦的方法,簡單地對命令定義為0x0、0x1、0x2等類似值會導致不同的設備驅動擁有相同的命令號。如果設備A、B都支持0x0、0x1、0x2這樣的命令,假設用戶本身希望給A發0x1命令,可是不經意間發給了B,這個時候B 因為支持該命令,它就會執行該命令。因此,Linux 內核推薦采用一套統一的ioctl()命令生成方式。
2.ioctl()命令
Linux建議以如圖6.2所示的方式定義ioctl()的命令。

圖6.2 I/O控制命令的組成
命令碼的設備類型字段為一個“幻數”,可以是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個宏的通用定義如代碼清單6.16所示。
代碼清單6.16 _IO()、_IOR()、_IOW()和_IOWR()宏定義
1 #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) 2 #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),\ 3 (_IOC_TYPECHECK(size))) 4 #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),\ 5 (_IOC_TYPECHECK(size))) 6 #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr), \ 7 (_IOC_TYPECHECK(size))) 8 /*_IO、_IOR等使用的_IOC宏*/ 9 #define _IOC(dir,type,nr,size) \ 10 (((dir) << _IOC_DIRSHIFT) | \ 11 ((type) << _IOC_TYPESHIFT) | \ 12 ((nr) << _IOC_NRSHIFT) | \ 13 ((size) << _IOC_SIZESHIFT))
由此可見,這幾個宏的作用是根據傳入的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()系統調用發生時自動關閉打開的文件。
(2)FIONCLEX:即File IOctl Not CLose on Exec,與FIOCLEX標志相反,清除由FIOCLEX命令設置的標志。
(3)FIOQSIZE:獲得一個文件或者目錄的大小,當用于設備文件時,返回一個ENOTTY錯誤。(4)FIONBIO:即File IOctl Non-Blocking I/O,這個調用修改在 filp->f_flags中的O_NONBLOCK標志。
FIOCLEX、FIONCLEX、FIOQSIZE和FIONBIO這些宏的定義為:
#define FIONCLEX 0x5450 #define FIOCLEX 0x5451 #define FIOQSIZE 0x5460 #define FIONBIO 0x5421
6.3.6 使用文件私有數據
6.3.1~6.3.5節給出的代碼完整地實現了預期的globalmem雛形,在其代碼中,為globalmem設備結構體globalmem_dev定義了全局實例dev(見代碼清單6.7第25行),而globalmem的驅動中read()、write()、ioctl()、llseek()函數都針對dev進行操作。
實際上,大多數Linux驅動工程師遵循一個“潛規則”,那就是將文件的私有數據private_data指向設備結構體,在read()、write()、ioctl()、llseek()等函數通過private_data訪問設備結構體。
這個時候,我們要將各函數進行少量的修改,為了讓讀者朋友建立字符設備驅動的全貌視圖,代碼清單6.17列出了完整的使用文件私有數據的globalmem的設備驅動,本程序位于虛擬機/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6目錄。
代碼清單6.17使用文件私有數據的globalmem的設備驅動
1 #include <linux/module.h> 2 #include <linux/types.h> 3 #include <linux/fs.h> 4 #include <linux/errno.h> 5 #include <linux/mm.h> 6 #include <linux/sched.h> 7 #include <linux/init.h> 8 #include <linux/cdev.h> 9 #include <asm/io.h> 10 #include <asm/system.h> 11 #include <asm/uaccess.h> 12 13 #define GLOBALMEM_SIZE 0x1000 /*全局內存最大4KB*/ 14 #define MEM_CLEAR 0x1 /*清零全局內存*/ 15 #define GLOBALMEM_MAJOR 250 /*預設的globalmem的主設備號*/ 16 17 static int globalmem_major = GLOBALMEM_MAJOR; 18 /*globalmem設備結構體*/ 19 struct globalmem_dev { 20 struct cdev cdev; /*cdev結構體*/ 21 unsigned char mem[GLOBALMEM_SIZE]; /*全局內存*/ 22 }; 23 24 struct globalmem_dev *globalmem_devp; /*設備結構體指針*/ 25 /*文件打開函數*/ 26 int globalmem_open(struct inode *inode, struct file *filp) 27 { 28 /*將設備結構體指針賦值給文件私有數據指針*/ 29 filp->private_data = globalmem_devp; 30 return 0; 31 } 32 /*文件釋放函數*/ 33 int globalmem_release(struct inode *inode, struct file *filp) 34 { 35 return 0; 36 } 37 38 /* ioctl設備控制函數 */ 39 static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned 40 int cmd, unsigned long arg) 41 { 42 struct globalmem_dev *dev = filp->private_data;/*獲得設備結構體指針*/ 43 44 switch (cmd) { 45 case MEM_CLEAR: 46 memset(dev->mem, 0, GLOBALMEM_SIZE); 47 printk(KERN_INFO "globalmem is set to zero\n"); 48 break; 49 50 default: 51 return - EINVAL; 52 } 53 54 return 0; 55 } 56 57 /*讀函數*/ 58 static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, 59 loff_t *ppos) 60 { 61 unsigned long p = *ppos; 62 unsigned int count = size; 63 int ret = 0; 64 struct globalmem_dev *dev = filp->private_data;/*獲得設備結構體指針*/ 65 66 /*分析和獲取有效的寫長度*/ 67 if (p >= GLOBALMEM_SIZE) 68 return 0; 69 if (count > GLOBALMEM_SIZE - p) 70 count = GLOBALMEM_SIZE - p; 71 72 /*內核空間→用戶空間*/ 73 if (copy_to_user(buf, (void *)(dev->mem + p), count)) { 74 ret = - EFAULT; 75 } else { 76 *ppos += count; 77 ret = count; 78 79 printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p); 80 } 81 82 return ret; 83 } 84 85 /*寫函數*/ 86 static ssize_t globalmem_write(struct file *filp, const char __user *buf, 87 size_t size, loff_t *ppos) 88 { 89 unsigned long p = *ppos; 90 unsigned int count = size; 91 int ret = 0; 92 struct globalmem_dev *dev = filp->private_data;/*獲得設備結構體指針*/ 93 94 /*分析和獲取有效的寫長度*/ 95 if (p >= GLOBALMEM_SIZE) 96 return 0; 97 if (count > GLOBALMEM_SIZE - p) 98 count = GLOBALMEM_SIZE - p; 99 100 /*用戶空間→內核空間*/ 101 if (copy_from_user(dev->mem + p, buf, count)) 102 ret = - EFAULT; 103 else { 104 *ppos += count; 105 ret = count; 106 107 printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p); 108 } 109 110 return ret; 111 } 112 113 /* seek文件定位函數 */ 114 static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) 115 { 116 loff_t ret = 0; 117 switch (orig) { 118 case 0: /*相對文件開始位置偏移*/ 119 if (offset < 0) { 120 ret = - EINVAL; 121 break; 122 } 123 if ((unsigned int)offset > GLOBALMEM_SIZE) { 124 ret = - EINVAL; 125 break; 126 } 127 filp->f_pos = (unsigned int)offset; 128 ret = filp->f_pos; 129 break; 130 case 1: /*相對文件當前位置偏移*/ 131 if ((filp->f_pos + offset) > GLOBALMEM_SIZE) { 132 ret = - EINVAL; 133 break; 134 } 135 if ((filp->f_pos + offset) < 0) { 136 ret = - EINVAL; 137 break; 138 } 139 filp->f_pos += offset; 140 ret = filp->f_pos; 141 break; 142 default: 143 ret = - EINVAL; 144 break; 145 } 146 return ret; 147 } 148 149 /*文件操作結構體*/ 150 static const struct file_operations globalmem_fops = { 151 .owner = THIS_MODULE, 152 .llseek = globalmem_llseek, 153 .read = globalmem_read, 154 .write = globalmem_write, 155 .ioctl = globalmem_ioctl, 156 .open = globalmem_open, 157 .release = globalmem_release, 158 }; 159 160 /*初始化并注冊cdev*/ 161 static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) 162 { 163 int err, devno = MKDEV(globalmem_major, index); 164 165 cdev_init(&dev->cdev, &globalmem_fops); 166 dev->cdev.owner = THIS_MODULE; 167 err = cdev_add(&dev->cdev, devno, 1); 168 if (err) 169 printk(KERN_NOTICE "Error %d adding globalmem %d", err, index); 170 } 171 172 /*設備驅動模塊加載函數*/ 173 int globalmem_init(void) 174 { 175 int result; 176 dev_t devno = MKDEV(globalmem_major, 0); 177 178 /* 申請設備號*/ 179 if (globalmem_major) 180 result = register_chrdev_region(devno, 1, "globalmem"); 181 else { /* 動態申請設備號 */ 182 result = alloc_chrdev_region(&devno, 0, 1, "globalmem"); 183 globalmem_major = MAJOR(devno); 184 } 185 if (result < 0) 186 return result; 187 188 /* 動態申請設備結構體的內存*/ 189 globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL); 190 if (!globalmem_devp) { /*申請失敗*/ 191 result = - ENOMEM; 192 goto fail_malloc; 193 } 194 195 memset(globalmem_devp, 0, sizeof(struct globalmem_dev)); 196 197 globalmem_setup_cdev(globalmem_devp, 0); 198 return 0; 199 200 fail_malloc: 201 unregister_chrdev_region(devno, 1); 202 return result; 203 } 204 205 /*模塊卸載函數*/ 206 void globalmem_exit(void) 207 { 208 cdev_del(&globalmem_devp->cdev); /*注銷cdev*/ 209 kfree(globalmem_devp); /*釋放設備結構體內存*/ 210 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);/*釋放設備號*/ 211 } 212 213 MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>"); 214 MODULE_LICENSE("Dual BSD/GPL"); 215 216 module_param(globalmem_major, int, S_IRUGO); 217 218 module_init(globalmem_init); 219 module_exit(globalmem_exit);
除了在globalmem_open()函數中通過filp->private_data = globalmem_devp語句(見第29行)將設備結構體指針賦值給文件私有數據指針并在 globalmem_read()、globalmem_write()、globalmem_llseek()和globalmem_ioctl()函數中通過struct globalmem_dev *dev = filp->private_data語句獲得設備結構體指針并使用該指針操作設備結構體外,代碼清單6.17與代碼清單6.7~6.15的程序基本相同。
讀者朋友們,這個時候,請您翻回到本書的第1章,再次閱讀代碼清單1.4,即Linux下LED的設備驅動,是否豁然開朗?
代碼清單6.17僅僅作為使用private_data的范例,實際上,在這個程序中使用private_data沒有任何意義,直接訪問全局變量globalmem_devp會更加結構清晰。如果globalmem不只包括一個設備,而是同時包括兩個或兩個以上的設備,采用private_data的優勢就會集中顯現出來。
在不對代碼清單6.17中的globalmem_read()、globalmem_write()、globalmem_ioctl()等重要函數及 globalmem_fops 結構體等數據結構進行任何修改的前提下,只是簡單地修改 globalmem_init()、globalmem_exit()和globalmem_open(),就可以輕松地讓globalmem驅動中包含兩個同樣的設備(次設備號分別為0和1),如代碼清單6.18所示。
代碼清單6.18支持2個globalmem設備的globalmem驅動
1 /*文件打開函數*/ 2 int globalmem_open(struct inode *inode, struct file *filp) 3 { 4 /*將設備結構體指針賦值給文件私有數據指針*/ 5 struct globalmem_dev *dev; 6 7 dev = container_of(inode->i_cdev,struct globalmem_dev,cdev); 8 filp->private_data = dev; 9 return 0; 10 } 11 12 /*設備驅動模塊加載函數*/ 13 int globalmem_init(void) 14 { 15 int result; 16 dev_t devno = MKDEV(globalmem_major, 0); 17 18 /* 申請設備號*/ 19 if (globalmem_major) 20 result = register_chrdev_region(devno, 2, "globalmem"); 21 else { /* 動態申請設備號 */ 23 result = alloc_chrdev_region(&devno, 0, 2, "globalmem"); 24 globalmem_major = MAJOR(devno); 25 } 26 if (result < 0) 27 return result; 28 29 /* 動態申請兩個設備結構體的內存*/ 30 globalmem_devp = kmalloc(2*sizeof(struct globalmem_dev), GFP_KERNEL); 31 if (!globalmem_devp) { /*申請失敗*/ 33 result = - ENOMEM; 34 goto fail_malloc; 35 } 36 memset(globalmem_devp, 0, 2*sizeof(struct globalmem_dev)); 37 38 globalmem_setup_cdev(&globalmem_devp[0], 0); 39 globalmem_setup_cdev(&globalmem_devp[1], 1); 40 return 0; 41 42 fail_malloc: unregister_chrdev_region(devno, 1); 43 return result; 44 } 45 46 /*模塊卸載函數*/ 47 void globalmem_exit(void) 48 { 49 cdev_del(&(globalmem_devp[0].cdev)); 50 cdev_del(&(globalmem_devp[1].cdev)); /* 注銷cdev */ 51 kfree(globalmem_devp); /*釋放設備結構體內存*/ 52 unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); /*釋放設備號*/ 53 } /* 其他代碼同清單6.16 */
代碼清單6.18第7行調用的container_of()的作用是通過結構體成員的指針找到對應結構體的指針,這個技巧在Linux內核編程中十分常用。在container_of(inode->i_cdev,struct globalmem_dev, cdev)語句中,傳給container_of()的第1個參數是結構體成員的指針,第2個參數為整個結構體的類型,第3個參數為傳入的第1個參數即結構體成員的類型,container_of()返回值為整個結構體的指針。
6.4 globalmem驅動在用戶空間的驗證
在對應目錄通過“make”命令編譯globalmem的驅動,得到globalmem.ko文件。運行:
lihacker@lihacker-laptop: ~ /develop/svn/ldd6410-read-only/training/kernel/drivers/ globalmem/ch6$ sudo su root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d rivers/globalmem/ch6# insmod globalmem.ko
命令加載模塊,通過“lsmod”命令,發現globalmem模塊已被加載。再通過“cat /proc/devices”命令查看,發現多出了主設備號為250的“globalmem”字符設備驅動:
root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d
rivers/globalmem/ch6# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
188 ttyUSB
189 usb_device
216 rfcomm
226 drm
250 globalmem
接下來,通過命令:
root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d
rivers/globalmem/ch6# mknod /dev/globalmem c 250 0
創建“/dev/globalmem”設備節點,并通過“echo 'hello world' > /dev/globalmem”命令和“cat/dev/globalmem”命令分別驗證設備的寫和讀,結果證明“hello world”字符串被正確地寫入globalmem字符設備:
root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d rivers/globalmem/ch6# echo "hello world" > /dev/globalmem root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d rivers/globalmem/ch6# cat /dev/globalmem hello world
如果啟用了sysfs文件系統,將發現多出了/sys/module/globalmem目錄,該目錄下的樹型結構為:
|-- refcnt '-- sections |-- .bss |-- .data |-- .gnu.linkonce.this_module |-- .rodata |-- .rodata.str1.1 |-- .strtab |-- .symtab |-- .text '-- __versions
refcnt記錄了globalmem模塊的引用計數,sections下包含的數個文件則給出了globalmem所包含的BSS、數據段和代碼段等的地址及其他信息。
對于代碼清單6.18給出的支持兩個globalmem設備的驅動,在加載模塊后需創建兩個設備節點,/dev/globalmem0對應主設備號globalmem_major,次設備號0,/dev/globalmem1對應主設備號 globalmem_major,次設備號1。分別讀寫/dev/globalmem0 和/dev/globalmem1,發現都讀寫到了正確的對應的設備。
6.5 總結
字符設備是3大類設備(字符設備、塊設備和網絡設備)中較簡單的一類設備,其驅動程序中完成的主要工作是初始化、添加和刪除cdev結構體,申請和釋放設備號,以及填充file_operations結構體中的操作函數,實現 file_operations 結構體中的read()、write()和 ioctl()等函數是驅動設計的主體工作。