Linux字元裝置驅動剖析
一、先看看裝置應用程式
1.很簡單,open裝置檔案,read、write、ioctl,最後close退出。如下:
intmain(int argc ,char *argv[]){
unsigned char val[1] = 1;
int fd =open("/dev/LED",O_RDWR);//開啟裝置
write(fd,val,1);//寫入裝置,這裡代表LED全亮
close(fd);//關閉裝置
return 0;
}
二、/dev目錄與檔案系統
2./dev是根檔案系統下的一個目錄檔案,/代表根目錄,其掛載的是根檔案系統的yaffs格式,通過讀取/根目錄這個檔案,就能分析list出其包含的各個目錄,其中就包括dev這個子目錄。即在/根目錄(也是一個檔案,其真實存在於flash介質)中有一項這樣的資料:
檔案屬性 檔案偏移 檔案大小 檔名稱 等等
ls/ 命令即會使用/掛載的yaffs檔案系統來讀取出根目錄檔案的內容,然後list出dev(是一個目錄)。即這時還不需要去讀取dev這個目錄檔案的內容。Cd dev即會分析dev掛載的檔案系統的超級塊的資訊,superblock,而不再理會在flash中的dev目錄檔案的資料。
3./dev在根檔案系統構建的時候會掛載為tmpfs. Tmpfs是一個基於虛擬記憶體的檔案系統,主要使用RAM和SWAP(Ramfs只是使用實體記憶體)。即以後讀寫dev這個目錄的操作都轉到tmpfs的操作,確切地講都是針對RAM的操作,而不再是通過yaffs檔案系統的讀寫函式去訪問flash介質。Tmpfs基於RAM,所以在掉電後回消失。因此/dev目錄下的裝置檔案都是每次linux啟動後建立的。
掛載過程:/etc/init.d/rcS
Mount –a 會讀取/etc/fstab的內容來掛載,其內容如下:
4./dev/NULL和/dev/console是在製作根檔案系統的時候靜態建立的,其他裝置檔案都是系統載入根檔案系統和各種驅動初始化過程中自動建立的,當然也可以通過命令列手動mknod裝置檔案。
三、裝置檔案的建立
5./dev目錄下的裝置檔案基本上都是通過mdev來動態建立的。mdev是一個使用者態的應用程式,位於busybox工具箱中。其建立過程包括:
1)驅動初始化或者匯流排匹配後會呼叫驅動的probe介面,該介面會呼叫device_create(裝置類, 裝置號, 裝置名);在/sys/class/裝置類目錄生成唯一的裝置屬性檔案(包括裝置號和裝置名等資訊),並且傳送uvent事件(KOBJ_ADD和環境變數,如路徑等資訊)到使用者空間(通過socket方式)。
2)mdev是一個work_thread執行緒,收到事件後會分析出/sys/class/裝置類的對應檔案,最終呼叫mknod動態來建立裝置檔案,而這個裝置檔案內容主要是裝置號(這個裝置檔案對應的inode會記錄檔案的屬性是一個裝置(其他屬性還包括目錄,一般檔案,符號連結等))。應用程式open(device_name,…)最重要的一步就是通過檔案系統介面來獲得該裝置檔案的內容—裝置號。
6.如果初始化過程中沒有呼叫device_create介面來建立裝置檔案,則需要手動通過命令列呼叫mknod介面來建立裝置檔案,方可在應用程式中訪問。
7.mknod介面分析,通過系統呼叫後對應呼叫sys_mknod,其是vfs層的介面。
Sys_mknod(裝置名, 裝置號)
vfs通過逐一路徑link_path_walk,分析出dev掛載了tmpfs,所以呼叫tmpfs->mknod=shmem_mknod
shmem_mknod(structinode *dir, struct dentry *dentry, int mode, dev_t dev)
inode = shmem_get_inode(dir->i_sb,dir, mode, dev, VM_NORESERVE);
inode = new_inode(sb);
switch (mode & S_IFMT) {
default:
inode->i_op =&shmem_special_inode_operations;
init_special_inode(inode,mode, dev);
break;
case S_IFREG://file
case S_IFDIR://DIR
case S_IFLNK:
//dentry填入inode資訊,這時對應的dentry和inode都已經存在於記憶體中。
d_instantiate(dentry, inode);
8. 可見,tmpfs的目錄和檔案都是像ramfs一樣一般都存在於記憶體中。通過ls命令來獲取目錄的資訊則由dentry資料結構的內容來獲取,而檔案的資訊由inode資料結構的內容來提供。Inode包括裝置檔案的裝置號i_rdev,檔案屬性(i_mode: S_ISCHR),inode操作集i_fop(對於裝置檔案來說就是如何open這個inode)。
四、open裝置檔案
9. open裝置檔案的最終目的是為了獲取到該裝置驅動的file_operations操作集,而該介面集是struct file的成員,open返回file資料結構指標:
struct file {
conststruct file_operations *f_op;
unsignedint f_flags;//可讀,可寫等
…
};
以下是led裝置驅動的操作介面。open("/dev/LED",O_RDWR)就是為了獲得led_fops。
static conststruct file_operations led_fops = {
.owner =THIS_MODULE,
.open =led_open,
.write = led_write,
};
10. 仔細看應用程式int fd =open("/dev/LED",O_RDWR),open的返回值是int,並不是file,其實是為了作業系統和安全考慮。fd位於應用層,而file位於核心層,它們都同屬程序相關概念。在Linux中,同一個檔案(對應於唯一的inode)可以被不同的程序開啟多次,而每次開啟都會獲得file資料結構。而每個程序都會維護一個已經開啟的file陣列,fd就是對應file結構的陣列下標。因此,file和fd在程序範圍內是一一對應的關係。
11. open介面分析,通過系統呼叫後對應呼叫sys_open,其是vfs層的介面
Sys_open(/dev/led)
SYSCALL_DEFINE3(open,const char __user *, filename, int, flags, int, mode)
do_sys_open(AT_FDCWD,/dev/tty, flags, mode);
fd = get_unused_fd_flags(flags);
struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);
//path_init返回時nd->dentry即為搜尋路徑檔名的起點
path_init(dfd, pathname, LOOKUP_PARENT, &nd);
//link_path_walk一步步建立開啟路徑的各個目錄的dentry和inode
link_path_walk(pathname, &nd);
do_last(&nd, &path, open_flag, acc_mode, mode, pathname);
//先處理..父目錄和.當前目錄
//通過inode節點建立file
filp = nameidata_to_filp(nd);
__dentry_open()
//inode->i_fop=&def_chr_fops
f->f_op =fops_get(inode->i_fop);
if (!open && f->f_op)
open = f->f_op->open;
if (open) {
//呼叫def_chr_fops->open
error = open(inode, f);
其中inode->i_fop在mknod的init_special_inode呼叫中被賦值為def_chr_fops。以下該變數的定義,因此, open(inode, f)即呼叫到chrdev_open。其可以看出是字元裝置所對應的檔案系統介面,我們姑且稱其為字元裝置檔案系統。
conststruct file_operations def_chr_fops = {
.open = chrdev_open,
};
繼續分析chrdev_open:
Kobj_lookup(cdev_map,inode->i_rdev, &idx)即是通過裝置的裝置號(inode->i_rdev)在cdev_map中查詢裝置對應的操作集file_operations.關於如何查詢,我們在理解字元裝置驅動如何註冊自己的file_operations後再回頭來分析這個問題。
五、字元裝置驅動的註冊
12. 字元裝置對應cdev資料結構:
struct cdev {
struct kobject kobj; // 每個 cdev 都是一個 kobject
struct module*owner; // 指向實現驅動的模組
const structfile_operations *ops; // 操縱這個字元裝置檔案的方法
struct list_headlist; //對應的字元裝置檔案的inode->i_devices 的連結串列頭
dev_t dev; // 起始裝置編號
unsigned intcount; // 裝置範圍號大小
};
13. led裝置驅動初始化和裝置驅動註冊
-
cdev_init是初始化cdev結構體,並將led_fops填入該結構。
-
cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
-
cdev_map是一個全家指標變數,型別如下:
-
kobj_map使用hash散列表來儲存cdev資料結構。通過註冊裝置的主裝置號major來獲得cdev_map->probes陣列的索引值i(i = major % 255),然後把一個型別為struct probe的節點物件加入到probes[i]所管理的連結串列中,probes[i]->data即是cdev資料結構,而probes[i]->dev和range代表字元裝置號和範圍。
六、再述open裝置檔案
14. 通過第五步的字元裝置的註冊過程,應該對Kobj_lookup查詢led_ops是很容易理解的。至此,已經獲得led裝置驅動的led_ops。接著立刻呼叫file->f_ops->open即呼叫了led_open,在該函式中會對led用到的GPIO進行ioremap並設定GPIO方向、上下拉等硬體初始化。
15. 最後,chrdev_open一步步返回,最後到
do_sys_open的struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);返回。
Fd_install(fd, f)即是在當前程序中將存有led_ops的file指標填入程序的file陣列中,下標是fd。最後將fd返回給使用者空間。而使用者空間只要傳入fd即可找到對應的file資料結構。
七、裝置操作
15. 這裡以裝置寫為例,主要是控制led的亮和滅。
write(fd,val,1)系統呼叫後對應sys_write,其對應所有的檔案寫,包括目錄、一般檔案和裝置檔案,一般檔案有位置偏移的概念,即讀寫之後,當前位置會發生變化,所以如要跳著讀寫,就需要fseek。對於字元裝置檔案,沒有位置的概念。所以我們重點跟蹤vfs_write的過程。
1)fget_light在當前程序中通過fd來獲得file指標
2)vfs_write
3) 對於led裝置,file->f_op->write即是led_write。
在該介面中實現對led裝置的控制。
八、再論字元裝置驅動的初始化
綜上所述,字元裝置的初始化包括兩個主要環節:
1)字元裝置驅動的註冊,即通過cdev_add向系統註冊cdev資料結構,提供file_operations操作集和裝置號等資訊,最終file_operations存放在全域性指標變數cdev_map指向的Hash表中,其可以通過裝置號索引並遍歷得到。
2)通過device_create(裝置類, 裝置號, 裝置名)在sys/class/裝置類中建立裝置屬性檔案併發送uevent事件,而mdev利用該資訊自動呼叫mknod在/dev目錄下建立對應的裝置檔案,以便應用程式訪問。
注:
device_create和mdev的程式碼分析請留意後續文章。本文涉及的vfs虛擬檔案系統知識(如vfs框架、dentry,inode資料結構等內容)也由後續文章詳細講述。
微信公眾號:嵌入式企鵝圈
blog: http://blog.csdn.net/yueqian_scut
我們追求:
1.忠於Linux原始碼,百分百原創。
2.從上電第一行程式碼、系統第一行程式碼、模組第一行程式碼、應用第一行程式碼,深入講解嵌入式軟體生命週期。
3 深刻理解硬體體系, 聚焦軟體層次設計、模組設計和框架設計。