cdev結構體及其相關函數
1、在 linux 2.6內核中,使用 cdev結構體描述字符設備,cdev 的定義在 <linux/cdev.h> 中可找到,其定義如下:
[cpp]
1. /*include/linux/cdev.h*/
[cpp]
1. struct cdev {
2. struct kobject kobj; //內嵌的kobject對象
3. struct module *owner; //所屬模塊
4. const struct file_operations *ops;
5. //文件操作結構,在寫驅動時,其結構體內的大部分函數要被實現
6. struct list_head list;
7. dev_t dev; //設備號,int 類型,高12位爲主設備號,低20位爲次設備號
8. unsigned int count;
9. };
可以使用如下宏調用來獲得主、次設備號:
MAJOR(dev_t dev)
MINOR(dev_t dev)
MKDEV(int major,int minor) //通過主次設備號來生成dev_t
以上宏調用在內核源碼中如此定義:
[cpp]
1. #define MINORBITS 20
2. #define MINORMASK ((1U << MINORBITS) - 1)
3. //(1<<20 -1) 此操作後,MINORMASK宏的低20位爲1,高12位爲0
4.
5. #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
6. #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
7. #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2、下面一組函數用來對cdev結構體進行操作:
[cpp]
1. void cdev_init(struct cdev *, const struct file_operations *);//初始化,建立cdev和file_operation 之間的連接
2.
3. struct cdev *cdev_alloc(void); //動態申請一個cdev內存
4.
5. void cdev_put(struct cdev *p); //釋放
6.
7. int cdev_add(struct cdev *, dev_t, unsigned); //註冊設備,通常發生在驅動模塊的加載函數中
8.
9. void cdev_del(struct cdev *);//註銷設備,通常發生在驅動模塊的卸載函數中
3、在調用cdev_add()函數向系統註冊字符設備之前,應首先調用register_chrdev_region()或alloc_chrdev_region()來向系統申請設備號:
[cpp]
1. int register_chrdev_region(dev_t from,unsigned count,const char *name)
[cpp]
1. int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
函數代替,他們之間的區別在於:register_chrdev_region()函數用於已知起始設備的設備號時,而alloc_chrdev_region()設備號未知,向系統動態申請未被佔用的設備號的情況,函數調用成功後,會把得到的設備號放入第一個參數dev中,優點在於不會造成設備號重複的衝突。
在調用cdev_del函數從系統註銷字符設備之後,應調用unregister_chrdev_region():
[cpp]
1. void unregister_chrdev_region(dev_t from,unsigned count)
函數釋放原先申請的設備號。
他們之間的順序關係如下:
register_chrdev_region()-->cdev_add() //此過程在加載模塊中
cdev_del()-->unregister_chrdev_region() //此過程在卸載模塊中
有兩個方法可以分配並初始化 cedv 結構。如果希望在運行時動態的獲得一個獨立的 cdev 結構,可以如下這麼做:
[cpp]
1. struct cdev *my_cdev = cdev_alloc();
2. my_cdev->ops = &my_fops;
cdev_alloc(void) 函數的代碼爲(對 cdev 結構體操作的系列函數可在 fs/char_dev.c 中找到):
[cpp]
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_alloc() 的源代碼可能由於內核版本號的不同而有差別(上面的代碼爲 2.6.30)
有時可能希望就把 cdev 結構內嵌在自己的特定設備結構裏,那麼在分配好 cdev 結構後,就用 cdev_init() 函數對其初始化:
[cpp]
1. void cdev_init (struct cdev *cdev, struct file_operations *fops)
cdev_init() 函數代碼爲:
[cpp]
1. void cdev_init(struct cdev *cdev, const 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 中的 owner 要設置爲 THIS_MOULE 。
一旦 cdev 結構體設置完畢,最後一步就是要把這事告訴給內核,使用下面的函數:
[cpp]
1. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
cdev_add() 對應的代碼爲:
[cpp]
1. /**
2. * cdev_add() - add a char device to the system
3. * @p: the cdev structure for the device
4. * @dev: the first device number for which this device is responsible
5. * @count: the number of consecutive minor numbers corresponding to this
6. * device
7. *
8. * cdev_add() adds the device represented by @p to the system, making it
9. * live immediately. A negative error code is returned on failure.
10. */
11. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
12. {
13. p->dev = dev;
14. p->count = count;
15. return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
16. }
參數 p 是 cdev 結構體的指針;
參數 dev 是設備響應的第一個設備號;
參數 count 和設備相關聯的設備號的數目。
一般的,count 的值爲 1,但是有些情形也可能是大於 1 的數。比如 SCSI 磁帶機,它通過給每個物理設備安排多個此設備號來允許用戶在應用程序裏選擇操作模式(比如密度)。
cdev_add 如果失敗了,那麼返回一個負值,表明驅動無法加載到系統中。然而它一般情況下都會成功,一旦 cdev_add 返回,設備也就 “活” 了起來,於是所對應的操作方法(file_operations 結構裏所定義的各種函數)也就能爲內核所調用。
從系統中移除一個字符設備,可以調用:
[cpp]
1. void cdev_del(struct cdev *p)
老版本的字符設備註冊與註銷
在許多驅動程序代碼裏,會看到許多字符設備驅動並沒有用 cdev 這個接口。這是一種老式的方法,但新寫的代碼應該使用 cdev 接口。
用於註冊字符設備驅動程序的老式函數 register_chrdev() 函數定義如下:
[cpp]
1. int register_chardev (unsigned int major, const char *name, struct file_operations *fops)
利用該函數註冊時,應先定義好主設備號、設備驅動程序的名稱、file_operations 結構體的變量。
應用程序中利用設備文件搜索設備驅動程序的時候使用主設備號 (major) 。
在內核中表示 proc 文件系統或錯誤代碼時,使用設備驅動程序名稱。
另外,利用 unregister_chrdev() 函數註銷字符設備驅動程序時,可以作爲區分標誌。註冊函數中關鍵的地方是定義 file_operations 結構體變量的地址。
所謂註冊字符設備驅動程序,應理解爲在內核中註冊與主設備號相關的 file_operations 結構體。
register_chrdev() 函數註冊完設備驅動程序,把定義主設備號的 major 設置爲 0,返回註冊的主設備號(動態分配),把已知的主設備號設爲 major 值時,返回 0 (人工指定)。註冊失敗時,返回負值
從內核中註銷字符設備驅動程序的 unregister_chrdev() 函數形式如下:
[cpp]
1. int unregister_chrdev (unsigned int major, const char *name)
該函數中使用主設備號(major) 和設備驅動程序名稱 (name) 與 register_chrdev 函數中使用的值相同,因爲內核會把這些參數作爲註銷字符設備驅動程序的基準對比兩個設定內容。從內核成功註銷了字符設備驅動程序時,返回 0 ,失敗則返回負值。
file_operations結構體分析
Linxu驅動中的設備文件註冊的操作方法結構體,也是向用戶層提供操作接口的方法體,我的版本爲3.1.10。原型在內核源碼 /include/linux/fs.h中定義:
1. struct file_operations {
2. struct module *owner;
3. //第一個 file_operations 成員根本不是一個操作; 它是一個指向擁有這個結構的模塊的指針. 這個成員用來在它的操作還在被使用時阻止模塊被卸載. 幾乎所有時間中, 它被簡單初始化爲 THIS_MODULE, 一個在 <linux/module.h> 中定義的宏.
4. loff_t (*llseek) (struct file *, loff_t, int);
5. //llseek 方法用作改變文件中的當前讀/寫位置, 並且新位置作爲(正的)返回值. loff_t 參數是一個"long offset", 並且就算在 32位平臺上也至少 64 位寬. 錯誤由一個負返回值指示. 如果這個函數指針是 NULL, seek 調用會以潛在地無法預知的方式修改 file 結構中的位置計數器( 在"file 結構" 一節中描述).
6. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
7. //用來從設備中獲取數據. 在這個位置的一個空指針導致 read 系統調用以 -EINVAL("Invalid argument") 失敗. 一個非負返回值代表了成功讀取的字節數( 返回值是一個 "signed size" 類型, 常常是目標平臺本地的整數類型).
8. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
9. //發送數據給設備. 如果 NULL, -EINVAL 返回給調用 write 系統調用的程序. 如果非負, 返回值代表成功寫的字節數.
10. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
11. //初始化一個異步讀 -- 可能在函數返回前不結束的讀操作. 如果這個方法是 NULL, 所有的操作會由 read 代替進行(同步地).
12. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
13. //初始化設備上的一個異步寫.
14. int (*readdir) (struct file *, voidvoid *, filldir_t);
15. //對於設備文件這個成員應當爲 NULL; 它用來讀取目錄, 並且僅對文件系統有用.
16. unsigned int (*poll) (struct file *, struct poll_table_struct *);
17. //poll 方法是 3 個系統調用的後端: poll, epoll, 和 select, 都用作查詢對一個或多個文件描述符的讀或寫是否會阻塞. poll 方法應當返回一個位掩碼指示是否非阻塞的讀或寫是可能的, 並且, 可能地, 提供給內核信息用來使調用進程睡眠直到 I/O 變爲可能. 如果一個驅動的 poll 方法爲 NULL, 設備假定爲不阻塞地可讀可寫.
18. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
19. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
20. // 2.6.35 之後的版本只有這兩個ioctl,少了inode參數,功能不變 表示系統調用提供了發出設備特定命令的方法
21. int (*mmap) (struct file *, struct vm_area_struct *);
22. //mmap 用來請求將設備內存映射到進程的地址空間. 如果這個方法是 NULL, mmap 系統調用返回 -ENODEV.
23. int (*open) (struct inode *, struct file *);
24. //儘管這常常是對設備文件進行的第一個操作, 不要求驅動聲明一個對應的方法. 如果這個項是 NULL, 設備打開一直成功, 但是你的驅動不會得到通知.
25. int (*flush) (struct file *, fl_owner_t id);
26. //flush 操作在進程關閉它的設備文件描述符的拷貝時調用; 它應當執行(並且等待)設備的任何未完成的操作. 這個必須不要和用戶查詢請求的 fsync 操作混淆了. 當前, flush 在很少驅動中使用; SCSI 磁帶驅動使用它, 例如, 爲確保所有寫的數據在設備關閉前寫到磁帶上. 如果 flush 爲 NULL, 內核簡單地忽略用戶應用程序的請求.
27. int (*release) (struct inode *, struct file *);
28. //在文件結構被釋放時引用這個操作. 如同 open, release 可以爲 NULL.
29. int (*fsync) (struct file *, loff_t, loff_t, int datasync);
30. //這個方法是 fsync 系統調用的後端, 用戶調用來刷新任何掛着的數據. 如果這個指針是 NULL, 系統調用返回 -EINVAL.
31. int (*aio_fsync) (struct kiocb *, int datasync);
32. //這是 fsync 方法的異步版本.
33. int (*fasync) (int, struct file *, int);
34. //這個操作用來通知設備它的 FASYNC 標誌的改變. 異步通知是一個高級的主題, 在第 6 章中描述. 這個成員可以是NULL 如果驅動不支持異步通知.
35. int (*lock) (struct file *, int, struct file_lock *);
36. //lock 方法用來實現文件加鎖; 加鎖對常規文件是必不可少的特性, 但是設備驅動幾乎從不實現它.
37. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
38. //sendpage 是 sendfile 的另一半; 它由內核調用來發送數據, 一次一頁, 到對應的文件. 設備驅動實際上不實現 sendpage.
39. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
40. //這個方法的目的是在進程的地址空間找一個合適的位置來映射在底層設備上的內存段中. 這個任務通常由內存管理代碼進行; 這個方法存在爲了使驅動能強制特殊設備可能有的任何的對齊請求. 大部分驅動可以置這個方法爲 NULL.
41. int (*check_flags)(int);
42. //這個方法允許模塊檢查傳遞給 fnctl(F_SETFL...) 調用的標誌.
43. int (*flock) (struct file *, int, struct file_lock *);
44. //文件加鎖
45. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
46. //由VFS調用,將管道數據粘接到文件。
47. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
48. //由VFS調用,將文件數據粘接到管道
49. //用於從管道傳輸文件數據,目前只用於splice2系統調用;
50. int (*setlease)(struct file *, long, struct file_lock **);
51. // 設置一個打開標誌到open的file
52. long (*fallocate)(struct file *file, int mode, loff_t offset,
53. loff_t len);
54. // 通過設置mode標誌FA_ALLOCATE或FA_DEALLOCATE,表示preallocation and deallocation of preallocated blocks respectively,由sys_fallocate調用。
55. };
Linux字符設備驅動的組成
在Linux中,字符設備驅動由以下幾個部分組成:
1、字符設備驅動模塊加載於卸載函數
在字符設備驅動模塊加載函數中應該實現設備號的申請和cdev的註冊,而在卸載函數中應實現設備號的釋放和cdev的註銷
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 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_operation體中的成員函數
file_operation結構體中成員函數是字符設備驅動與內核的接口,是用戶空間對Linux進行系統調用最終的落實者。大多數字符設備驅動會實現read()、write()、ioctl()函數。
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_ops是讀/寫的位置相對於文件開頭的偏移。
由於內核空間和用戶空間不能直接互訪,故借用函數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; |
若要複製的內存是簡單類型,如:char,int,long等,則可以使用簡單的put_user()和get_user()。
int val; /*內核空間整形變量 ... get_user(val, (int *) arg); /* 用戶到內核,arg是用戶空間的地址 ... put_user(val, (int *) arg); /*內核到用戶,arg是用戶空間的地址 |
在字符設備驅動中,要定義一個file_operations的實例,並將具體設備驅動的函數賦值給file_operations的成員。
1 struct file_operations xxx_fops = { 2 .owner = THIS_MODULE, 3 .read = xxx_read, 4 .write = xxx_write, 5 .ioctl = xxx_ioctl, 6 ... 7 }; |
在字符設備驅動模塊加載與卸載函數模板中的cdev_init(&xxx_dev.cdev,&xxx_fops);語句可建立與cdev的連接。
字符設備驅動的結構如下:
設備驅動程序編寫流程:
設備驅動程序可以使用模塊的方式加載的方式加載到內核中去,驅動開發時時沒有main()函數,模塊在調用insmod命令時被加載,此時的入口點式init_module()函數,通常在該函數中完成設備的註冊。同樣,模塊在調用rmmod命令時被卸載,此時的入口點是cleanup_module()函數,在該函數中完成設備的卸載。在設備完成註冊加載之後,用戶的應用程序就可以對該設備進行一定的操作,如open()、read()、write()等,而驅動程序就是用於實現這些操作,在用戶應用程序調用相應入口函數時執行相關的操作。
設備功能是由file_operation()函數定義的。
proc文件系統
/proc文件系統是一個僞文件系統,它是一種內核和內核模塊用來向進程發送信息的機制。這個僞文件系統讓用戶可以和內核內部數據結構進行交互,獲取有關係統和進程的有用信息,在運行時通過改變內核參數來改變設置。與其他文件系統不同,/proc存在於內存之中而不是在硬盤之中。/proc文件系統體現了內核及進程運行的內容,在加載模塊成功後,讀者可以通過查看/proc/device文件獲得相關設備的主設備號
globalmem字符設備驅動
globalmem流程圖:
globalmem設備是一個虛擬設備,同時意味着“全局內存”,在globalmem字符設備驅動中會分配一片大小GLOBALMEM_SIZE(4KB)的內存空間,並在驅動中提供針對這片內存的讀寫、控制和定位函數,以供用戶空間的進程能通過Linux系統調用訪問這片內存。
globalmem可被兩個或兩個以上的進程同時訪問,其中全局內存可作爲用戶空間進程進行通信的一種蹩腳手段。
源碼:
globalmen模塊
[cpp]
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<linux/slab.h>
10. #include<linux/kdev_t.h>
11. #include<linux/device.h>
12. #include <asm/io.h>
13. #include<asm/system.h>
14. #include<asm/uaccess.h>
15.
16. #define DEVICE_NAME "globalmem"
17. #defineGLOBALMEM_SIZE 0x1000 /*全局內存最大4K字節*/
18. #define MEM_CLEAR0x1 /*清0全局內存*/
19. #define GLOBALMEM_MAJOR241 /*預設的globalmem的主設備號*/
20.
21. static intglobalmem_major = GLOBALMEM_MAJOR;
22. /*globalmem設備結構體*/
23. struct globalmem_dev
24. {
25. struct cdev cdev; /*cdev結構體*/
26. unsigned char mem[GLOBALMEM_SIZE];/*全局內存*/
27. };
28.
29. struct globalmem_dev*globalmem_devp; /*設備結構體指針*/
30.
31. static struct class*globalmem0_class;
32. static struct class*globalmem1_class;
33.
34. /*文件打開函數*/
35. int globalmem_open(structinode *inode, struct file *filp)
36. {
37. /*將設備結構體指針賦值給文件私有數據指針*/
38. struct globalmem_dev *dev;
39.
40. dev = container_of(inode->i_cdev,structglobalmem_dev,cdev);
41. filp->private_data = dev;
42. return 0;
43. }
44. /*文件釋放函數*/
45. intglobalmem_release(struct inode *inode, struct file *filp)
46. {
47. return 0;
48. }
49.
50. /* ioctl設備控制函數 */
51. static intglobalmem_ioctl(struct inode *inodep, struct file *filp, unsigned
52. int cmd, unsigned long arg)
53. {
54. struct globalmem_dev *dev =filp->private_data;/*獲得設備結構體指針*/
55.
56. switch (cmd)
57. {
58. case MEM_CLEAR:
59. memset(dev->mem, 0,GLOBALMEM_SIZE);
60. printk(KERN_INFO "globalmem is setto zero\n");
61. break;
62.
63. default:
64. return - EINVAL;
65. }
66. return 0;
67. }
68.
69. /*讀函數*/
70. static ssize_tglobalmem_read(struct file *filp, char __user *buf, size_t size,
71. loff_t *ppos)
72. {
73. unsigned long p = *ppos;
74. unsigned int count = size;
75. int ret = 0;
76. struct globalmem_dev *dev =filp->private_data; /*獲得設備結構體指針*/
77.
78. /*分析和獲取有效的寫長度*/
79. if (p >= GLOBALMEM_SIZE)
80. return count ? - ENXIO: 0;
81. if (count > GLOBALMEM_SIZE - p)
82. count = GLOBALMEM_SIZE - p;
83.
84. /*內核空間->用戶空間*/
85. if (copy_to_user(buf, (void*)(dev->mem +p), count))
86. {
87. ret = - EFAULT;
88. }
89. else
90. {
91. *ppos += count;
92. ret = count;
93.
94. printk(KERN_INFO "read %d bytes(s)from %d\n", count, p);
95. }
96.
97. return ret;
98. }
99.
100. /*寫函數*/
101. static ssize_tglobalmem_write(struct file *filp, const char __user *buf,
102. size_t size, loff_t *ppos)
103. {
104. unsigned long p = *ppos;
105. unsigned int count = size;
106. int ret = 0;
107. struct globalmem_dev *dev =filp->private_data; /*獲得設備結構體指針*/
108.
109. /*分析和獲取有效的寫長度*/
110. if (p >= GLOBALMEM_SIZE)
111. return count ? - ENXIO: 0;
112. if (count > GLOBALMEM_SIZE - p)
113. count = GLOBALMEM_SIZE - p;
114.
115. /*用戶空間->內核空間*/
116. if (copy_from_user(dev->mem + p, buf,count))
117. ret = - EFAULT;
118. else
119. {
120. *ppos += count;
121. ret = count;
122.
123. printk(KERN_INFO "written %d bytes(s)from %d\n", count, p);
124. }
125.
126. return ret;
127. }
128.
129. /* seek文件定位函數 */
130. static loff_tglobalmem_llseek(struct file *filp, loff_t offset, int orig)
131. {
132. loff_t ret = 0;
133. switch (orig)
134. {
135. case 0: /*相對文件開始位置偏移*/
136. if (offset < 0)
137. {
138. ret = - EINVAL;
139. break;
140. }
141. if ((unsigned int)offset >GLOBALMEM_SIZE)
142. {
143. ret = - EINVAL;
144. break;
145. }
146. filp->f_pos = (unsigned int)offset;
147. ret = filp->f_pos;
148. break;
149. case 1: /*相對文件當前位置偏移*/
150. if ((filp->f_pos + offset) >GLOBALMEM_SIZE)
151. {
152. ret = - EINVAL;
153. break;
154. }
155. if ((filp->f_pos + offset) < 0)
156. {
157. ret = - EINVAL;
158. break;
159. }
160. filp->f_pos += offset;
161. ret = filp->f_pos;
162. break;
163. default:
164. ret = - EINVAL;
165. break;
166. }
167. return ret;
168. }
169.
170. /*文件操作結構體*/
171. static const structfile_operations globalmem_fops =
172. {
173. .owner = THIS_MODULE,
174. .llseek = globalmem_llseek,
175. .read = globalmem_read,
176. .write = globalmem_write,
177. .ioctl = globalmem_ioctl,
178. .open = globalmem_open,
179. .release = globalmem_release,
180. };
181.
182. /*初始化並註冊cdev*/
183. static voidglobalmem_setup_cdev(struct globalmem_dev *dev, int index)
184. {
185. int err, devno = MKDEV(globalmem_major,index);
186.
187. cdev_init(&dev->cdev,&globalmem_fops);
188. dev->cdev.owner = THIS_MODULE;
189. dev->cdev.ops = &globalmem_fops;
190. err = cdev_add(&dev->cdev, devno, 1);
191. if (err)
192. printk(KERN_NOTICE "Error %d addingLED%d", err, index);
193. }
194.
195. /*設備驅動模塊加載函數*/
196. int globalmem_init(void)
197. {
198. int result;
199. dev_t devno = MKDEV(globalmem_major, 0);
200.
201. /* 申請設備號*/
202. if (globalmem_major)
203. result = register_chrdev_region(devno, 2,DEVICE_NAME);
204. else /* 動態申請設備號 */
205. {
206. result = alloc_chrdev_region(&devno, 0,2, DEVICE_NAME);
207. globalmem_major = MAJOR(devno);
208. }
209. if (result < 0)
210. return result;
211.
212. /* 動態申請2個設備結構體的內存*/
213. globalmem_devp = kmalloc(2*sizeof(structglobalmem_dev), GFP_KERNEL);
214. if (!globalmem_devp) /*申請失敗*/
215. {
216. result = - ENOMEM;
217. goto fail_malloc;
218. }
219. memset(globalmem_devp, 0, 2*sizeof(structglobalmem_dev));
220.
221. globalmem_setup_cdev(&globalmem_devp[0],0);
222. globalmem_setup_cdev(&globalmem_devp[1],1);
223.
224. //註冊一個類,使mdev可以在/dev/下面建立設備節點
225. globalmem0_class =class_create(THIS_MODULE, "globalmem0");
226. if( IS_ERR(globalmem0_class) )
227. {
228. printk(KERN_NOTICE, "creatglobalmem0_class failed!");
229. return -1;
230. }
231.
232. //創建一個設備節點,節點名字爲globalmem0
233. device_create(globalmem0_class, NULL,MKDEV(globalmem_major, 0), NULL, "globalmem0");
234. printk(KERN_NOTICE, "globalmem0initialized!");
235.
236. globalmem1_class =class_create(THIS_MODULE, "globalmem1");
237. if( IS_ERR(globalmem1_class) )
238. {
239. printk(KERN_NOTICE, "creatglobalmem1_class failed!");
240. return -1;
241. }
242.
243. //創建一個設備節點,節點名字爲globalmem1
244. device_create(globalmem1_class, NULL,MKDEV(globalmem_major, 1), NULL, "globalmem1");
245. printk(KERN_NOTICE, "globalmem1initialized!");
246.
247. return 0;
248.
249. fail_malloc: unregister_chrdev_region(devno,2);
250. return result;
251. }
252.
253. /*模塊卸載函數*/
254. void globalmem_exit(void)
255. {
256. cdev_del(&(globalmem_devp[0].cdev));
257. cdev_del(&(globalmem_devp[1].cdev)); /*註銷cdev*/
258. kfree(globalmem_devp); /*釋放設備結構體內存*/
259. unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); /*釋放設備號*/
260. class_destroy(globalmem0_class);
261. class_destroy(globalmem1_class);
262. }
263.
264. MODULE_AUTHOR("SongBaohua");
265. MODULE_LICENSE("DualBSD/GPL");
266.
267. module_param(globalmem_major,int, S_IRUGO);
268.
269. module_init(globalmem_init);
270. module_exit(globalmem_exit);
測試程序:
[cpp]
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>
4. #include <unistd.h>
5. #include<linux/fcntl.h>
6.
7. int main()
8. {
9. int fd;
10. char buf1[]="testglobalmen data!";//寫入memdev設備的內容
11. char buf_read[4096]; //memdev設備的內容讀入到該buf中
12.
13. if((fd=open("/dev/globalmem0",O_RDWR))==-1)//打開memdev設備
14. {
15. printf("open globalmem0 WRONG!\n");
16. return 0;
17. }
18.
19. int count = 0;
20. while(count<=100)
21. {
22. printf("openglobalmem0 SUCCESS!\n");
23. printf("buf is %s\n",buf1);
24. write(fd,buf1,sizeof(buf1));//把buf中的內容寫入memdev設備
25. lseek(fd,0,SEEK_SET); //把文件指針重新定位到文件開始的位置
26. read(fd,buf_read,sizeof(buf1));//把memdev設備中的內容讀入到buf_read中
27. printf("buf_read is %s\n",buf_read);
28. count++;
29. }
30.
31. close(fd);
32. return 0;
33. }
34.
分析:
一.MKDEV
[cpp]
1. /linux/kdev_t.h
2. #define _LINUX_KDEV_T_H
3. 3#ifdef __KERNEL__
4. 4#define MINORBITS 20
5. 5#define MINORMASK ((1U << MINORBITS) - 1)
6. 6
7. 7#define MAJOR(dev) ((unsigned int) ((dev) >>MINORBITS))
8. 8#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
9. 9#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
當編譯內核的時候,__KERNEL__在命令行被定義
When you compile your kernel, __KERNEL__ isdefined on the command line.
User-space programs need access tothe kernel headers, but some of the info in kernel headers is intended only forthe kernel. Wrapping some statements in an #ifdef __KERNEL__/#endif blockensures that user-space programs don't see those statements.
主設備號高12位,次設備號低20位
二.kmalloc和vmalloc的區別
1.kmalloc
1>kmalloc內存分配和malloc相似,除非被阻塞否則他執行的速度非常快,而且不對獲得空間清零.在用kmalloc申請函數後,要對起清零.用memset()函數對申請的內存進行清零。
2>kamlloc函數原型:
#include<linux/slab.h>
void *kmalloc(size_t size, intflags);
(1)第一個參數是要分配的塊的大小
(2)第二個參數是分配標誌(flags),他提供了多種kmalloc的行爲。
(3)第三個最常用的GFP_KERNEL;
A.表示內存分配(最終總是調用get_free_pages來實現實際的分配;這就是GFP前綴的由來)是代表運行在內核空間的進程執行的。使用GFP_KERNEL容許kmalloc在分配空閒內存時候如果內存不足容許把當前進程睡眠以等待。因此這時分配函數必須是可重入的。如果在進程上下文之外如:中斷處理程序、tasklet以及內核定時器中這種情況下current進程不該睡眠,驅動程序該使用GFP_ATOMIC.
B.GFP_ATOMIC
用來從中斷處理和進程上下文之外的其他代碼中分配內存. 從不睡眠.
C.GFP_KERNEL
內核內存的正常分配. 可能睡眠.
D.GFP_USER
用來爲用戶空間頁來分配內存; 它可能睡眠.
E.GFP_HIGHUSER
如同 GFP_USER, 但是從高端內存分配, 如果有. 高端內存在下一個子節描述.
F.GFP_NOFS,GFP_NOIO
這個標誌功能如同 GFP_KERNEL, 但是它們增加限制到內核能做的來滿足請求. 一個 GFP_NOFS 分配不允許進行任何文件系統調用, 而 GFP_NOIO 根本不允許任何 I/O 初始化. 它們主要地用在文件系統和虛擬內存代碼, 那裏允許一個分配睡眠, 但是遞歸的文件系統調用會是一個壞注意.
上面列出的這些分配標誌可以是下列標誌的相或來作爲參數, 這些標誌改變這些分配如何進行:
__GFP_DMA
這個標誌要求分配在能夠 DMA 的內存區. 確切的含義是平臺依賴的並且在下面章節來解釋.
__GFP_HIGHMEM
這個標誌指示分配的內存可以位於高端內存.
__GFP_COLD
正常地, 內存分配器盡力返回"緩衝熱"的頁 -- 可能在處理器緩衝中找到的頁. 相反, 這個標誌請求一個"冷"頁, 它在一段時間沒被使用. 它對分配頁作 DMA 讀是有用的, 此時在處理器緩衝中出現是無用的. 一個完整的對如何分配 DMA 緩存的討論看"直接內存存取"一節在第 1 章.
__GFP_NOWARN
這個很少用到的標誌阻止內核來發出警告(使用 printk), 當一個分配無法滿足.
__GFP_HIGH
這個標誌標識了一個高優先級請求, 它被允許來消耗甚至被內核保留給緊急狀況的最後的內存頁.
Ø __GFP_REPEAT
__GFP_NOFAIL
__GFP_NORETRY
這些標誌修改分配器如何動作, 當它有困難滿足一個分配. __GFP_REPEAT 意思是" 更盡力些嘗試" 通過重複嘗試 -- 但是分配可能仍然失敗. __GFP_NOFAIL 標誌告訴分配器不要失敗; 它盡最大努力來滿足要求. 使用__GFP_NOFAIL 是強烈不推薦的; 可能從不會有有效的理由在一個設備驅動中使用它. 最後, __GFP_NORETRY告知分配器立即放棄如果得不到請求的內存.
Ø 內存區段
__GFP_DMA和__GFP_HIGHMEM的使用與平臺相關,Linux把內存分成3個區段:可用於DMA的內存、常規內存、以及高端內存。X86平臺上ISA設備DMA區段是內存的前16MB,而PCI設備無此限制。
內存區後面的機制在 mm/page_alloc.c 中實現, 而內存區的初始化在平臺特定的文件中, 常常在 arch 目錄樹的 mm/init.c。
3>kamlloc的使用方法:
Linux 處理內存分配通過創建一套固定大小的內存對象池. 分配請求被這樣來處理, 進入一個持有足夠大的對象的池子並且將整個內存塊遞交給請求者. 驅動開發者應當記住的一件事情是, 內核只能分配某些預定義的, 固定大小的字節數組.
如果你請求一個任意數量內存, 你可能得到稍微多於你請求的, 至多是 2 倍數量. 同樣, 程序員應當記住kmalloc 能夠處理的最小分配是 32 或者 64 字節, 依賴系統的體系所使用的頁大小. kmalloc 能夠分配的內存塊的大小有一個上限. 這個限制隨着體系和內核配置選項而變化. 如果你的代碼是要完全可移植, 它不能指望可以分配任何大於 128 KB. 如果你需要多於幾個 KB, 但是, 有個比 kmalloc 更好的方法來獲得內存。在設備驅動程序或者內核模塊中動態開闢內存,不是用malloc,而是kmalloc ,vmalloc,或者用get_free_pages直接申請頁。釋放內存用的是kfree,vfree,或free_pages. kmalloc函數返回的是虛擬地址(線性地址). kmalloc特殊之處在於它分配的內存是物理上連續的,這對於要進行DMA的設備十分重要. 而用vmalloc分配的內存只是線性地址連續,物理地址不一定連續,不能直接用於DMA.
注意kmalloc最大隻能開闢128k-16,16個字節是被頁描述符結構佔用了。kmalloc用法參見khg.
內存映射的I/O口,寄存器或者是硬件設備的RAM(如顯存)一般佔用F0000000以上的地址空間。在驅動程序中不能直接訪問,要通過kernel函數vremap獲得重新映射以後的地址。
另外,很多硬件需要一塊比較大的連續內存用作DMA傳送。這塊內存需要一直駐留在內存,不能被交換到文件中去。但是kmalloc最多隻能開闢大小爲32XPAGE_SIZE的內存,一般的PAGE_SIZE=4kB,也就是128kB的大小的內存。
3.kmalloc和vmalloc的區別
• vmalloc()與 kmalloc()都可用於分配內存
• kmalloc()分配的內存處於3GB~high_memory之 間,這段內核空間與物理內存的映射一一對應
•vmalloc()分配的內存在 VMALLOC_START~4GB之間,這段非連續內 存區映射到物理內存也可能是非連續的
• 在內核空間中調用kmalloc()分配連續物理空間,而調用vmalloc()分配非物理連續空間。
• 把kmalloc()所分配內核空間中的地址稱爲內核邏輯地址
• 把vmalloc()分配的內核空間中的地址稱 爲內核虛擬地址
• vmalloc()在分配過程中須更新內核頁表
總結:
1.kmalloc和vmalloc分配的是內核的內存,malloc分配的是用戶的內存
2.kmalloc保證分配的內存在物理上是連續的,kmalloc()分配的內存在0xBFFFFFFF-0xFFFFFFFF以上的內存中,driver一般是用它來完成對DS的分配,更適合於類似設備驅動的程序來使用;
3.vmalloc保證的是在虛擬地址空間上的連續,vmalloc()則是位於物理地址非連續,虛地址連續區,起始位置由VMALLOL_START來決定,一般作爲交換區、模塊的分配。
3.kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相對較大(因爲vmalloc還可以處理交換空間)。
4.內存只有在要被DMA訪問的時候才需要物理上連續,vmalloc比kmalloc要慢
5.vmalloc使用的正確場合是分配一大塊,連續的,只在軟件中存在的,用於緩衝的內存區域。不能在微處理器之外使用。
6.vmalloc 中調用了 kmalloc (GFP—KERNEL),因此也不能應用於原子上下文。
7.kmalloc和 kfree管理內核段內分配的內存,這是真實地址已知的實際物理內存塊。
8.vmalloc對應於vfree,分配連續的虛擬內存,但是物理上不一定連續。
9.kmalloc分配內存是基於slab,因此slab的一些特性包括着色,對齊等都具備,性能較好。物理地址和邏輯地址都是連續的
三.module_param
1.爲什麼引入
在用戶態下編程可以通過main()來傳遞命令行參數,而編寫一個內核模塊則可通過module_param()來傳遞命令行參數.
2.module_param宏是Linux 2.6內核中新增的,該宏被定義在include/linux/moduleparam.h文件中,具體定義如下:
[cpp] view plain copy print?
1. /* Helper functions: type is byte, short, ushort, int, uint, long,
2. ulong,charp, bool or invbool, or XXX if you define param_get_XXX,
3. param_set_XXX and param_check_XXX.
4. */
5. #define module_param_named(name, value, type,perm)
6. param_check_##type(name,&(value));
7. module_param_call(name, param_set_##type, param_get_##type, &value,perm);
8. __MODULE_PARM_TYPE(name, #type)
9. #define module_param(name, type,perm)
10. module_param_named(name, name, type, perm)
由此可知module_param的實現是通過module_param_named(name, name, type, perm)的。
3.module_param使用了3個參數:變量名,它的類型,以及一個權限掩碼用來做一個輔助的sysfs入口。
這個宏定義應當放在任何函數之外,典型地是出現在源文件的前面。
eg:static char *whom="world"
static int tige=1;
module_param(tiger,int,S_IRUGO);
module_param(whom,charp,S_IRUGO);
4.模塊參數支持許多類型:
bool
invbool
一個布爾型(true 或者 false)值(相關的變量應當是 int 類型). invbool 類型顛倒了值, 所以真值變成 false, 反之亦然.
charp:一個字符指針值. 內存爲用戶提供的字串分配, 指針因此設置.
int
long
short
uint
ulong
ushort
基本的變長整型值.以 u 開頭的是無符號值.
5.數組參數,用逗號間隔的列表提供的值, 模塊加載者也支持。
聲明一個數組參數,使用:
module_param_array(name,type,num,perm);
這裏 name是你的數組的名子(也是參數名),
type是數組元素的類型,
num是一個整型變量,
perm是通常的權限值.
如果數組參數在加載時設置,num 被設置成提供的數的個數. 模塊加載者拒絕比數組能放下的多的值.
Tiger-John說明:
perm參數的作用是什麼?
最後的module_param 字段是一個權限值,表示此參數在sysfs文件系統中所對應的文件節點的屬性。你應當使用 <linux/stat.h>中定義的值. 這個值控制誰可以存取這些模塊參數在 sysfs 中的表示.當perm爲0時,表示此參數不存在 sysfs文件系統下對應的文件節點。 否則,模塊被加載後,在/sys/module/ 目錄下將出現以此模塊名命名的目錄, 帶有給定的權限.。
權限在include/linux/stat.h中有定義
比如:
#defineS_IRWXU 00700
#defineS_IRUSR 00400
#defineS_IWUSR 00200
#defineS_IXUSR 00100
#defineS_IRWXG 00070
#defineS_IRGRP 00040
#defineS_IWGRP 00020
#defineS_IXGRP 00010
#defineS_IRWXO 00007
#defineS_IROTH 00004
#defineS_IWOTH 00002
#defineS_IXOTH 00001
使用S_IRUGO 參數可以被所有人讀取, 但是不能改變; S_IRUGO|S_IWUSR 允許 root 來改變參數. 注意, 如果一個參數被 sysfs修改, 你的模塊看到的參數值也改變了, 但是你的模塊沒有任何其他的通知. 你應當不要使模塊參數可寫, 除非你準備好檢測這個改變並且因而作出反應.
四.file文件結構
struct file, 定義於<linux/fs.h>, 是設備驅動中第二個最重要的數據結構. 注意 file 與用戶空間程序的 FILE 指針沒有任何關係. 一個 FILE定義在 C 庫中, 從不出現在內核代碼中. 一個 struct file, 另一方面, 是一個內核結構, 從不出現在用戶程序中.
文件結構代表一個打開的文件.(它不特定給設備驅動; 系統中每個打開的文件有一個關聯的 struct file 在內核空間). 它由內核在 open 時創建,並傳遞給在文件上操作的任何函數, 直到最後的關閉. 在文件的所有實例都關閉後, 內核釋放這個數據結構.
在內核源碼中, struct file的指針常常稱爲 file 或者 filp("file pointer"). 我們將一直稱這個指針爲 filp 以避免和結構自身混淆.因此, file 指的是結構, 而 filp 是結構指針.
struct file 的最重要成員在這展示.如同在前一節, 第一次閱讀可以跳過這個列表. 但是, 在本章後面, 當我們面對一些真實 C 代碼時, 我們將更詳細討論這些成員.
mode_tf_mode;
文件模式確定文件是可讀的或者是可寫的(或者都是),通過位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函數中檢查這個成員的讀寫許可,但是你不需要檢查讀寫許可, 因爲內核在調用你的方法之前檢查. 當文件還沒有爲那種存取而打開時讀或寫的企圖被拒絕, 驅動甚至不知道這個情況.
loff_tf_pos;
當前讀寫位置.loff_t 在所有平臺都是 64 位( 在 gcc 術語裏是 long long ). 驅動可以讀這個值, 如果它需要知道文件中的當前位置,但是正常地不應該改變它; 讀和寫應當使用它們作爲最後參數而收到的指針來更新一個位置, 代替直接作用於 filp->f_pos. 這個規則的一個例外是在llseek 方法中, 它的目的就是改變文件位置.
unsignedint f_flags;
這些是文件標誌,例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驅動應當檢查 O_NONBLOCK 標誌來看是否是請求非阻塞操作(我們在第一章的"阻塞和非阻塞操作"一節中討論非阻塞 I/O ); 其他標誌很少使用. 特別地, 應當檢查讀/寫許可, 使用 f_mode而不是 f_flags. 所有的標誌在頭文件 <linux/fcntl.h> 中定義.
structfile_operations *f_op;
和文件關聯的操作.內核安排指針作爲它的 open 實現的一部分, 接着讀取它當它需要分派任何的操作時. filp->f_op 中的值從不由內核保存爲後面的引用;這意味着你可改變你的文件關聯的文件操作, 在你返回調用者之後新方法會起作用. 例如, 關聯到主編號 1 (/dev/null, /dev/zero, 等等)的open 代碼根據打開的次編號來替代 filp->f_op 中的操作. 這個做法允許實現幾種行爲, 在同一個主編號下而不必在每個系統調用中引入開銷.替換文件操作的能力是面向對象編程的"方法重載"的內核對等體.
void*private_data;
open系統調用設置這個指針爲 NULL, 在爲驅動調用 open 方法之前. 你可自由使用這個成員或者忽略它; 你可以使用這個成員來指向分配的數據,但是接着你必須記住在內核銷燬文件結構之前, 在 release 方法中釋放那個內存. private_data 是一個有用的資源, 在系統調用間保留狀態信息,我們大部分例子模塊都使用它.
structdentry *f_dentry;
關聯到文件的目錄入口(dentry )結構. 設備驅動編寫者正常地不需要關心 dentry 結構, 除了作爲 filp->f_dentry->d_inode 存取inode 結構.
真實結構有多幾個成員,但是它們對設備驅動沒有用處. 我們可以安全地忽略這些成員, 因爲驅動從不創建文件結構; 它們真實存取別處創建的結構.
五.inode結構
inode 結構由內核在內部用來表示文件. 因此, 它和代表打開文件描述符的文件結構是不同的.可能有代表單個文件的多個打開描述符的許多文件結構, 但是它們都指向一個單個 inode 結構.
inode 結構包含大量關於文件的信息. 作爲一個通用的規則, 這個結構只有 2 個成員對於編寫驅動代碼有用:
dev_ti_rdev;
對於代表設備文件的節點,這個成員包含實際的設備編號.
structcdev *i_cdev;
structcdev 是內核的內部結構, 代表字符設備; 這個成員包含一個指針, 指向這個結構, 當節點指的是一個字符設備文件時.
i_rdev 類型在 2.5 開發系列中改變了, 破壞了大量的驅動. 作爲一個鼓勵更可移植編程的方法, 內核開發者已經增加了 2 個宏,可用來從一個 inode 中獲取主次編號:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
爲了不要被下一次改動抓住, 應當使用這些宏代替直接操作 i_rdev.
六.container_of()
container_of()的作用是通過結構體成員的指針找到對應
結構體的指針,這個技巧在Linux內核編程中十分常用。在container_of
(inode->i_cdev,structglobalmem_dev,cdev)語句中,傳給container_of()的第1個參數是結
構體成員的指針,第2個參數爲整個結構體的類型,第3 個參數爲傳入的第1 個參數即
結構體成員的類型,container_of()返回值爲整個結構體的指針。
七.ioctl函數
首先說明在2.6.36以後ioctl函數已經不再存在了,而是用unlocked_ioctl和compat_ioctl兩個函數實現以前版本的ioctl函數。同時在參數方面也發生了一定程度的改變,去除了原來ioctl中的struct inode參數,同時改變了返回值。
但是驅動設計過程中存在的問題變化並不是很大,同樣在應用程序設計中我們還是採用ioctl實現訪問,而並不是unlocked_ioctl函數,因此我們還可以稱之爲ioctl函數的實現。
ioctl函數的實現主要是用來實現具體的硬件控制,採用相應的命令控制硬件的具體操作,這樣就能使得硬件的操作不再是單調的讀寫操作。使得硬件的使用更加的方便。
ioctl函數實現主要包括兩個部分,首先是命令的定義,然後纔是ioctl函數的實現,命令的定義是採用一定的規則。
ioctl的命令主要用於應用程序通過該命令操作具體的硬件設備,實現具體的操作,在驅動中主要是對命令進行解析,通過switch-case語句實現不同命令的控制,進而實現不同的硬件操作。
ioctl函數的命令定義方法:
int (*unlocked_ioctl)(struct file*filp,unsigned int cmd,unsigned long arg)
雖然其中沒有指針的參數,但是通常採用arg傳遞指針參數。cmd是一個命令。每一個命令由一個整形數據構成(32bits),將一個命令分成四部分,每一部分實現具體的配置,設備類型(幻數)8bits,方向2bits,序號8bits,數據大小13/14bits。命令的實現實質上就是通過簡單的移位操作,將各個部分組合起來而已。
一個命令的分佈的大概情況如下:
|---方向位(31-30)|----數據長度(29-16)----------------|---------設備類型(15-8)------|----------序號(7-0)----------|
|----------------------------------------------------------------------------------------------------------------------------------------|
其中方向位主要是表示對設備的操作,比如讀設備,寫設備等操作以及讀寫設備等都具有一定的方向,2個bits只有4種方向。
數據長度表示每一次操作(讀、寫)數據的大小,一般而已每一個命令對應的數據大小都是一個固定的值,不會經常改變,14bits說明可以選擇的數據長度最大爲16k。
設備類型類似於主設備號(由於8bits,剛好組成一個字節,因此經常採用字符作爲幻數,表示某一類設備的命令),用來區別不同的命令類型,也就是特定的設備類型對應特定的設備。序號主要是這一類命令中的具體某一個,類似於次設備號(256個命令),也就是一個設備支持的命令多達256個。
同時在內核中也存在具體的宏用來定義命令以及解析命令。
但是大部分的宏都只是定義具體的方向,其他的都需要設計者定義。
主要的宏如下:
[cpp]
1. #include<linux/ioctl.h>
2. _IO(type,nr) 表示定義一個沒有方向的命令,
3. _IOR(type,nr,size) 表示定義一個類型爲type,序號爲nr,數據大小爲size的讀命令
4. _IOW(type,nr,size) 表示定義一個類型爲type,序號爲nr,數據大小爲size的寫命令
5. _IOWR(type,nr,size) 表示定義一個類型爲type,序號爲nr,數據大小爲size的寫讀命令
通常的type可採用某一個字母或者數字作爲設備命令類型。
是實際運用中通常採用如下的方法定義一個具體的命令:
[cpp]
1. //頭文件
2. #include<linux/ioctl.h>
3. /*定義一系列的命令*/
4. /*幻數,主要用於表示類型*/
5. #define MAGIC_NUM 'k'
6. /*打印命令*/
7. #define MEMDEV_PRINTF _IO(MAGIC_NUM,1)
8. /*從設備讀一個int數據*/
9. #define MEMDEV_READ _IOR(MAGIC_NUM,2,int)
10. /*往設備寫一個int數據*/
11. #define MEMDEV_WRITE _IOW(MAGIC_NUM,3,int)
12. /*最大的序列號*/
13. #define MEM_MAX_CMD 3
還有對命令進行解析的宏,用來確定具體命令的四個部分(方向,大小,類型,序號)具體如下所示:
[cpp]
1. /*確定命令的方向*/
2. _IOC_DIR(nr)
3. /*確定命令的類型*/
4. _IOC_TYPE(nr)
5. /*確定命令的序號*/
6. _IOC_NR(nr)
7. /*確定命令的大小*/
8. _IOC_SIZE(nr)
上面的幾個宏可以用來命令,實現命令正確性的檢查。
ioctl的實現過程主要包括如下的過程:
1、命令的檢測
2、指針參數的檢測
3、命令的控制switch-case語句
1、命令的檢測主要包括類型的檢查,數據大小,序號的檢測,通過結合上面的命令解析宏可以快速的確定。
[cpp]
1. /*檢查類型,幻數是否正確*/
2. if(_IOC_TYPE(cmd)!=MAGIC_NUM)
3. return -EINVAL;
4. /*檢測命令序號是否大於允許的最大序號*/
5. if(_IOC_NR(cmd)> MEM_MAX_CMD)
6. return -EINVAL;
2、主要是指針參數的檢測。指針參數主要是因爲內核空間和用戶空間的差異性導致的,因此需要來自用戶空間指針的有效性。使用copy_from_user,copy_to_user,get_user,put_user之類的函數時,由於函數會實現指針參量的檢測,因此可以省略,但是採用__get_user(),__put_user()之類的函數時一定要進行檢測。具體的檢測方法如下所示:
[cpp]
1. if(_IOC_DIR(cmd) & _IOC_READ)
2. err = !access_ok(VERIFY_WRITE,(void *)args,_IOC_SIZE(cmd));
3. else if(_IOC_DIR(cmd) & _IOC_WRITE)
4. err = !access_ok(VERIFY_READ,(void *)args,_IOC_SIZE(cmd));
5. if(err)/*返回錯誤*/
6. return -EFAULT;
當方向是讀時,說明是從設備讀數據到用戶空間,因此要檢測用戶空間的指針是否可寫,採用VERIFY_WRITE,而當方向是寫時,說明是往設備中寫數據,因此需要檢測用戶空間中的指針的可讀性VERIFY_READ。檢查通常採用access_ok()實現檢測,第一個參數爲讀寫,第二個爲檢測的指針,第三個爲數據的大小。
3、命名的控制:
命令的控制主要是採用switch和case相結合實現的,這於window編程中的檢測各種消息的實現方式是相同的。
[cpp]
1. /*根據命令執行相應的操作*/
2. switch(cmd)
3. {
4. case MEMDEV_PRINTF:
5. printk("<--------CMD MEMDEV_PRINTF Done------------>\n\n");
6. ...
7. break;
8. case MEMDEV_READ:
9. ioarg = &mem_devp->data;
10. ...
11. ret = __put_user(ioarg,(int *)args);
12. ioarg = 0;
13. ...
14. break;
15. case MEMDEV_WRITE:
16. ...
17. ret = __get_user(ioarg,(int *)args);
18. printk("<--------CMD MEMDEV_WRITE Done ioarg = %d--------->\n\n",ioarg);
19. ioarg = 0;
20. ...
21. break;
22. default:
23. ret = -EINVAL;
24. printk("<-------INVAL CMD--------->\n\n");
25. break;
26. }
這只是基本的框架結構,實際中根據具體的情況進行修改。這樣就實現了基本的命令控制。
文件操作支持的集合如下:
[cpp]
1. /*添加該模塊的基本文件操作支持*/
2. static const struct file_operations mem_fops =
3. {
4. /*結尾不是分號,注意其中的差別*/
5. .owner = THIS_MODULE,
6. .llseek = mem_llseek,
7. .read = mem_read,
8. .write = mem_write,
9. .open = mem_open,
10. .release = mem_release,
11. /*添加新的操作支持*/
12. .unlocked_ioctl = mem_ioctl,
13. };
需要注意不是ioctl,而是unlocked_ioctl。