Linux底層驅動開發需要學習哪些內容想必這是很多學習Linux的朋友十分頭疼的問題,今天就讓我來告訴大家我們到底該學習哪些內容呢?
1. 要會一些硬件知識,比如Arm接口編程
2. 學會寫簡單的makefile
3. 編一應用程序,可以用makefile跑起來
4. 學會寫驅動的makefile
5. 寫一簡單char驅動,makefile編譯通過,可以insmod, lsmod, rmmod. 在驅動的init函數里打印hello world, insmod后應該能夠通過dmesg看到輸出。
6. 寫一完整驅動, 加上read, write, ioctl, polling等各種函數的驅動實現。 在ioctl里完成從用戶空間向內核空間傳遞結構體的實現。
7. 寫一block驅動, 加上read,write,ioctl,poll等各種函數實現。
8. 簡單學習下內存管理, 這個是難的,明白各種memory alloc的函數實現細節。這是Linux開發的基本功。
9. 學習鎖機制的應用,這個不是難的但是容易犯錯的,涉及到很多同步和并發的問題。
10. 看內核中實際應用的驅動代碼。 你會發現基本的你已經知道了, 大的框架都是一樣的, 無非是read, write, ioctl等函數的實現, 但里面包含了很多很多細小的實現細節是之前不知道的。 這時候就要考慮到很多別的問題而不僅僅是基本功能的實現。 推薦您看2.6.20中integrated的一個驅動 kvm, 記得是在driver/lguest下,很好玩的, 就是Linux下的虛擬機驅動, 代碼不長,但功能強大。有能力的可以自己寫一操作系統按照要求做成磁盤鏡像加載到虛擬機中, 然后客戶機可以有自己的4G虛擬地址空間。
11. 看完驅動歡迎您進入Linux kernel學習中來。 簡單的方法,跟著ldd(Linux devive driver)做一遍。
1、 Makefile 是如何編寫
eg:
# 這是上面那個程序的 Makefile 文件
main:main.o mytool1.o mytool2.o
gcc -o main main.o mytool1.o mytool2.o
main.o:main.c mytool1.h mytool2.h
gcc -c main.c
mytool1.o:mytool1.c mytool1.h
gcc -c mytool1.c
mytool2.o:mytool2.c mytool2.h
gcc -c mytool2.c
分析:
在 Makefile 中也#開始的行都是注釋行.Makefile 中重要的是描述文件的依賴關系的說
明.一般的格式是: Linux 操作系統 C 語言編程入門
target: components //表示的是依賴關系
TAB rule //規則
main:main.o mytool1.o mytool2.o 表示我們的目標(target)main 的依賴對象(components)是 main.o mytool1.o mytool2.o 當倚賴的對象在目標修改后修改的話,就要去執行規則一行所指定的命令.就象我們的上
面那個 Makefile 第3行所說的一樣要執行 gcc -o main main.o mytool1.o mytool2.o
(注意規則一行中的 TAB表示那里是一個 TAB 鍵)
Makefile 有三個非常有用的變量.分別是$@,$^,$<代表的意義分別是:
$@--目標文件; $^--所有的依賴文件; $<--第一個依賴文件。
1、 字符設備驅動
Linux字符設備驅動的關鍵數據結構cdev及file_operations結構體的操作方法,并分析了Linux字符設備的整體結構,給出了簡單的設計模板.
2.1、驅動結構
1) cdev結構體(cdev結構體描述字符設備)
定義:
1 struct cdev {
3 struct kobject kobj; /* 內嵌的kobject對象 */
4 struct module *owner; /*所屬模塊*/
5 struct file_operations *ops; /*文件操作結構體*/
6 struct list_head list;
7 dev_t dev; /*設備號*/ 定義了設備號
8 unsigned int count;
9 };
dev_t 成員定義了設備號,為 32 位,其中高 12 位為主設備號,低20位為次設備號。使用下列宏可以從dev_t獲得主設備號和次設備號:
MAJOR(dev_t dev) //主設備號
MINOR(dev_t dev) //次設備號
而使用下列宏則可以通過主設備號和設備號生成 dev_t:
MKDEV(int major, int minor)
file_operations 定義了字符設備驅動提供給虛擬文件系統的接口函數
Linux 2.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 之間的連接。
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 cdev->kobj.ktype = &ktype_cdev_default;
6 kobject_init(&cdev->kobj);
7 cdev->ops = fops; /*將傳入的文件操作結構體指針賦值給cdev的ops*/
8 }
cdev_alloc()函數用于動態申請一個cdev內存
1 struct cdev *cdev_alloc(void)
2 {
3 struct cdev *p=kmalloc(sizeof(struct cdev),GFP_KERNEL); /*分配cdev的內存*/
4 if (p) {
5 memset(p, 0, sizeof(struct cdev));
6 p->kobj.ktype = &ktype_cdev_dynamic;
7 INIT_LIST_HEAD(&p->list);
8 kobject_init(&p->kobj);
9 }
10 return p;
11 }
cdev_add()函數和 cdev_del()函數分別向系統添加和刪除一個cdev,完成字符設備的注冊和注銷。對 cdev_add()的調用通常發生在字符設備驅動模塊加載函數中,而對cdev_del()函數的調用則通常發生在字符設備驅動模塊卸載函數中。
2) 分配和釋放設備號
在 調用 cdev_add() 函 數 向系統注冊 字符 設備 之前 , 應首先調用register_chrdev_region()或 alloc_chrdev_region()函數向系統申請設備號。register_chrdev_region() 函 數 用 于 已 知 起 始 設 備的 設備 號 的 情 況; 而alloc_chrdev_region()用于設備號未知,向系統動態申請未被占用的設備號的情況,相反地 ,在 調用 cdev_del() 函 數 從系 統 注銷 字符設備 之 后,unregister_chrdev_region()應該被調用以釋放原先申請的設備號。
3)file_operations結構體
1 struct file_operations
2 {
3 struct module *owner; // 擁有該結構的模塊的指針,一般為THIS_MODULES
5 loff_t(*llseek)(struct file *, loff_t, int); // 用來修改文件當前的讀寫位置
7 ssize_t(*read)(struct file *, char _ _user *, size_t, loff_t*); // 從設備中同步讀取數據
9 ssize_t(*aio_read)(struct kiocb *, char _ _user *, size_t, loff_t); // 初始化一個異步的讀取操作
11 ssize_t(*write)(struct file *, const char _ _user *, size_t, loff_t*); // 向設備發送數據
13 ssize_t(*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t); // 初始化一個異步的寫入操作
15 int(*readdir)(struct file *, void *, filldir_t); // 僅用于讀取目錄,對于設備文件,該字段為 NULL
17 unsigned int(*poll)(struct file *, struct poll_table_struct*); // 輪詢函數,判斷目前是否可以進行非阻塞的讀取或寫入
19 int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); // 執行設備I/O控制命令
21 long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); // 不使用BLK文件系統,將使用此種函數指針代替ioctl
23 long(*compat_ioctl)(struct file *, unsigned int, unsigned long); // 在64位系統上,32位的ioctl調用將使用此函數指針代替
25 int(*mmap)(struct file *, struct vm_area_struct*); // 用于請求將設備內存映射到進程地址空間
27 int(*open)(struct inode *, struct file*); // 打開
29 int(*flush)(struct file*);
30 int(*release)(struct inode *, struct file*); / 關閉
32 int(*synch)(struct file *, struct dentry *, int datasync); // 刷新待處理的數據
34 int(*aio_fsync)(struct kiocb *, int datasync); // 異步fsync
36 int(*fasync)(int, struct file *, int); // 通知設備FASYNC標志發生變化
38 int(*lock)(struct file *, int, struct file_lock*);
39 ssize_t(*readv)(struct file *, const struct iovec *, unsigned long, loff_t*);
40 ssize_t(*writev)(struct file *, const struct iovec *, unsigned long, loff_t*); // readv和writev:分散/聚集型的讀寫操作
42 ssize_t(*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void*); // 通常為NULL
44 ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int); // 通常為NULL
46 unsigned long(*get_unmapped_area)(struct file *,unsigned long, unsigned long, unsigned long, unsigned long); // 在進程地址空間找到一個將底層設備中的內存段映射的位置
49 int(*check_flags)(int); // 允許模塊檢查傳遞給fcntl(F_SETEL...)調用的標志
51 int(*dir_notify)(struct file *filp, unsigned long arg); // 僅對文件系統有效,驅動程序不必實現
53 int(*flock)(struct file *, int, struct file_lock*);
54 };
llseek()函數用來修改一個文件的當前讀寫位置,并將新位置返回,在出錯時,這個函數返回一個負值
read()函數用來從設備中讀取數據,成功時函數返回讀取的字節數,出錯時返回一個負值。
write()函數向設備發送數據,成功時該函數返回寫入的字節數。如果此函數未被實現,當用戶進行write()系統調用時,將得到-EINVAL返回值。
readdir()函數僅用于目錄,設備節點不需要實現它。
ioctl()提供設備相關控制命令的實現 (既不是讀操作也不是寫操作) , 當調用成功時,返回給調用程序一個非負值。內核本身識別部分控制命令,而不必調用設備驅動中的
ioctl()。如果設備不提供ioctl()函數,對于內核不能識別的命令,用戶進行ioctl()系統調用時將獲得-EINVAL返回值。
mmap()函數將設備內存映射到進程內存中,如果設備驅動未實現此函數,用戶進行 mmap()系統調用時將獲得-ENODEV返回值。 這個函數對于幀緩沖等設備特別有意義。
3)字符設備驅動的組成
A、字符設備驅動模塊加載與卸載函數
字符設備驅動模塊加載函數中應該實現設備號的申請和cdev的注冊, 而在卸載函數中應實現設備號的釋放和 cdev的注銷常見的設備結構體、模塊加載和卸載函數形式如代碼清單:
1 //設備結構體
2 struct xxx_dev_t
3 {
4 struct cdev cdev;
5 ...
6 } xxx_dev;
7 //設備驅動模塊加載函數
8 static int _ _init xxx_init(void)
9 {
10 ...
11 cdev_init(&xxx_dev.cdev, &xxx_fops); //初始化cdev
12 xxx_dev.cdev.owner = THIS_MODULE; //獲取字符設備號
14 if (xxx_major)
15 {
16 register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
17 }
18 else
19 {
20 alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
21 }
22
23 ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); //注冊設備
24 ...
25 }
26 //設備驅動模塊卸載函數
27 static void _ _exit xxx_exit(void)
28 {
29 unregister_chrdev_region(xxx_dev_no, 1); //釋放占用的設備號
30 cdev_del(&xxx_dev.cdev); //注銷設備
31 ...
32 }
B、字符設備驅動的file_operations 結構體中成員函數
file_operations 結構體中成員函數是字符設備驅動與內核的接口,是用戶空間對Linux進行系統調用終的落實者。 大多數字符設備驅動會實現read()、 write()和 ioctl()函數,常見的字符設備驅動的這3個函數的形式如代碼清單
1 /* 讀設備*/
2 ssize_t xxx_read(struct file *filp, char _ _user *buf, size_t count, loff_t*f_pos)
4 {
5 ...
6 copy_to_user(buf, ..., ...);
7 ...
8 }
設備驅動的讀函數中,filp是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,count是要讀的字節數,f_pos是讀的位置相對于文件開頭的偏移。
9 /* 寫設備*/
10 ssize_t xxx_write(struct file *filp, const char _ _user *buf, size_t count, loff_t *f_pos)
12 {
13 ...
14 copy_from_user(..., buf, ...);
15 ...
16 }
設備驅動的寫函數中,filp是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,count是要寫的字節數,f_pos是寫的位置相對于文件開頭的偏移
17 /* ioctl函數 */
18 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
unsigned long arg)
20 {
21 ...
22 switch (cmd)
23 {
24 case XXX_CMD1:
25 ...
26 break;
27 case XXX_CMD2:
28 ...
29 break;
30 default: /* 不能支持的命令 */
32 return - ENOTTY;
33 }
34 return 0;
35 }
I/O 控制函數的cmd參數為事先定義的I/O 控制命令, 而 arg為對應于該命令的參數。例如對于串行設備,如果SET_BAUDRATE 是一個設置波特率的命令,那后面的arg就應該是波特率值。
讀和寫函數中的_ _user 是一個宏,表明其后的指針指向用戶空間
在字符設備驅動中,需要定義一個 file_operations 的實例,并將具體設備驅動的函數賦值給file_operations的成員,如代碼清單:
1 struct file_operations xxx_fops =
2 {
3 .owner = THIS_MODULE,
4 .read = xxx_read,
5 .write = xxx_write,
6 .ioctl = xxx_ioctl,
7 ...
8 };