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 深刻理解硬件體系, 聚焦軟件層次設計、模塊設計和框架設計。


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