文章從上層應用訪問字元設備驅動開始,一步步地深入分析Linux字元設備的軟體層次、組成框架和交互、如何編寫驅動、設備文件的創建和mdev原理,對Linux字元設備驅動有全面的講解。本文整合之前發表的《Linux字元設備驅動剖析》和《 Linux 設備文件的創建和mdev》兩篇文章,基於linux字元 ...
文章從上層應用訪問字元設備驅動開始,一步步地深入分析Linux字元設備的軟體層次、組成框架和交互、如何編寫驅動、設備文件的創建和mdev原理,對Linux字元設備驅動有全面的講解。本文整合之前發表的《Linux字元設備驅動剖析》和《 Linux 設備文件的創建和mdev》兩篇文章,基於linux字元設備驅動的所有相關知識給讀者一個完整的呈現。
一、從最簡單的應用程式入手
1.很簡單,open設備文件,read、write、ioctl,最後close退出。如下:
二、/dev目錄與文件系統
2. /dev是根文件系統下的一個目錄文件,/代表根目錄,其掛載的是根文件系統的yaffs格式,通過讀取/根目錄這個文件,就能分析list出其包含的各個目錄,其中就包括dev這個子目錄。即在/根目錄(也是一個文件,其真實存在於flash介質)中有一項這樣的數據:
是否目錄 偏移 大小 名稱 -- --
1 0xYYYY 0Xmmm dev -- --
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(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);
可見,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);
//path_init返回時nd->dentry即為搜索路徑文件名的起點
//link_path_walk一步步建立打開路徑的各個目錄的dentry和inode
其中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設備驅動初始化和設備驅動註冊
1) cdev_init是初始化cdev結構體,並將led_fops填入該結構。
2) cdev_add
3) cdev_map是一個全家指針變數,類型如下:
4) 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數據結構。
七、設備操作
16. 這裡以設備寫為例,主要是控制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的原理又是什麼呢?我們接著分析。
九、設備類相關知識
設備類是虛擬的,並沒有直接對應的物理實物,只是為了更好地管理同一類設備導出到用戶空間而產生的目錄和文件。整個過程涉及到sysfs文件系統,該文件系統是為了展示linux設備驅動模型而構建的文件系統,是基於ramfs,linux根目錄中的/sysfs即掛載了sysfs文件系統。
Struct kobject數據結構是sysfs的基礎,kobject在sysfs中代表一個目錄,而linux的驅動(struct driver)、設備(struct device)、設備類(struct class)均是從kobject進行派生的,因此他們在sysfs中都對應於一個目錄。而數據結構中附屬的struct device_attribute、driver_attribute、class_attribute等屬性數據結構在sysfs中則代表一個普通的文件。
Struct kset是struct kobject的容器,即Struct kset可以成為同一類struct kobject的父親,而其自身也有kobject成員,因此其又可能和其他kobject成為上一級kset的子成員。
十、兩種創建設備文件的方式
在設備驅動中cdev_add將struct file_operations和設備號註冊到系統後,為了能夠自動產生驅動對應的設備文件,需要調用class_create和device_create,並通過uevent機制調用mdev(嵌入式linux由busybox提供)來調用mknod創建設備文件。當然也可以不調用這兩個介面,那就手工通過命令行mknod來創建設備文件。
十一、設備類和設備相關數據結構
1. include/linux/kobject.h
struct kobject {
constchar *name;//名稱
structlist_head entry;//kobject鏈表
structkobject *parent;//即所屬kset的kobject
structkset *kset;//所屬kset
structkobj_type *ktype;//屬性操作介面
…
};
struct kset {
struct list_head list;//管理同屬於kset的kobject
struct kobject kobj;//可以成為上一級父kset的子目錄
const struct kset_uevent_ops *uevent_ops;//uevent處理介面
};
假設Kobject A代表一個目錄,kset B代表幾個目錄(包括A)的共同的父目錄。
則A.kset=B; A.parent=B.kobj.
2.include/linux/device.h
struct class {//設備類
const char *name;//設備類名稱
struct module *owner;//創建設備類的module
structclass_attribute *class_attrs;//設備類屬性
struct device_attribute *dev_attrs;//設備屬性
struct kobject *dev_kobj;//kobject再sysfs中代表一個目錄
….
struct class_private *p;//設備類得以註冊到系統的連接件
};
3.drivers/base/base.h
struct class_private {
//該設備類同樣是一個kset ,包含下麵的class_devices;同時在class_subsys填充父kset
struct kset class_subsys;
structklist class_devices;//設備類包含的設備(kobject)
…
structclass *class;//指向設備類數據結構,即要創建的本級目錄信息
};
4.include/linux/device.h
structdevice {//設備
structdevice *parent;//sysfs/devices/中的父設備
structdevice_private *p;//設備得以註冊到系統的連接件
structkobject kobj;//設備目錄
constchar *init_name;//設備名稱
structbus_type *bus;//設備所屬匯流排
structdevice_driver *driver; //設備使用的驅動
structklist_node knode_class;//連接到設備類的klist
structclass *class;//所屬設備類
conststruct attribute_group **groups;
…
}
5. drivers/base/base.h
struct device_private {
structklist klist_children;//連接子設備
structklist_node knode_parent;//加入到父設備鏈表
structklist_node knode_driver;//加入到驅動的設備鏈表
structklist_node knode_bus;//加入到匯流排的鏈表
structdevice *device;//對應設備結構
};
6. 解釋
class_private是class的私有結構,class通過class_private註冊到系統中;device_private是device的私有結構,device通過device_private註冊到系統中。註冊到系統中也是將相應的數據結構加入到系統已經存在的鏈表中,但是這些鏈接的細節並不希望暴露給用戶,也沒有必要暴露出來,所以才有private的結構。而class和device則通過sysfs向用戶層提供信息。
十二、創建設備類目錄文件
1. 在驅動通過cdev_add將struct file_operations介面集和設備註冊到系統後,即利用class_create介面來創建設備類目錄文件。
led_class = class_create(THIS_MODULE,"led_class");
__class_create(owner, name,&__key);
cls->name = name;//設備類名
cls->owner= owner;//所屬module
retval =__class_register(cls, key);
structclass_private *cp;
//將類的名字led_class賦值給對應的kset
kobject_set_name(&cp->class_subsys.kobj,"%s", cls->name);
// 填充class_subsys所屬的父kset:ket:sysfs/class.
cp->class_subsys.kobj.kset= class_kset;
//填充class屬性操作介面
cp->class_subsys.kobj.ktype= &class_ktype;
cp->class = cls;//通過cp可以找到class
cls->p = cp;//通過class可以找到cp
//創建led_class設備類目錄
kset_register(&cp->class_subsys);
//在led_class目錄創建class屬性文件
add_class_attrs(class_get(cls));
2. 繼續展開kset_register
kset_register(&cp->class_subsys);
kobject_add_internal(&k->kobj);
// parent即class_kset.kobj, 即/sysfs/class對應的目錄
parent =kobject_get(kobj->parent);
create_dir(kobj);
//創建一個led _class設備類目錄
sysfs_create_dir(kobj);
該介面是sysfs文件系統介面,代表創建一個目錄,不再展開。
3. 上述提到的class_kset 在class_init被創建
class_kset= kset_create_and_add("class", NULL, NULL);
第三個傳參為NULL,代表預設在/sysfs/創建class目錄。
十三、創建設備目錄和設備屬性文件
1.利用class_create介面來創建設備類目錄文件後,再利用device_create介面來創建具體設備目錄和設備屬性文件。
led_device =device_create(led_class, NULL, led_devno, NULL, "led");
device_create_vargs
dev->devt = devt;//設備號
dev->class= class;//設備類led_class
dev->parent =parent;//父設備,這裡是NULL
kobject_set_name_vargs(&dev->kobj,fmt, args)//設備名”led”
device_register(dev)註冊設備
2. 繼續展開device_register(dev)
device_initialize(dev);
dev->kobj.kset= devices_kset;//設備所屬/sysfs/devices/
device_add(dev)
device_private_init(dev)//初始化device_private
dev_set_name(dev,"%s", dev->init_name);//賦值dev->kobject的名稱
setup_parent(dev,parent);//建立device和父設備的kobject的聯繫
//kobject_add在/sysfs/devices/目錄下創建設備目錄led,kobject_add是和kset_register相似的介面,只不過前者針對kobject,後者針對kset。
kobject_add(&dev->kobj,dev->kobj.parent, NULL);
kobject_add_varg
kobj->parent= parent;
kobject_add_internal(kobj)
create_dir(kobj);//創建設備目錄
//在剛創建的/sysfs/devices/led目錄下創建uevent屬性文件,名稱是”uevent”
device_create_file(dev,&uevent_attr);
//在剛創建的/sysfs/devices/led目錄下創建dev屬性文件,名稱是”dev”,該屬性文件的內容就是設備號
device_create_file(dev,&devt_attr);
//在/sysfs/class/led_class/目錄下建立led設備的符號連接,所以打開/sysfs/class/led_class/led/目錄也能看到dev屬性文件,讀出設備號。
device_add_class_symlinks(dev);
//創建device屬性文件,包括設備所屬匯流排的屬性和attribute_group屬性
device_add_attrs()
bus_add_device(dev)//將設備加入匯流排
//觸發uevent機制,並通過調用mdev來創建設備文件。
kobject_uevent(&dev->kobj,KOBJ_ADD);
//匹配設備和匯流排的驅動,匹配成功就調用驅動的probe介面,不再展開
bus_probe_device(dev);
3. 展開kobject_uevent(&dev->kobj, KOBJ_ADD);
kobject_uevent_env(kobj,action, NULL);
kset= top_kobj->kset;
uevent_ops = kset->uevent_ops; //即device_uevent_ops
//subsystem即設備所屬的設備類的名稱”led_class”
subsystem= uevent_ops->name(kset, kobj);
//devpath即/sysfs/devices/led/
devpath= kobject_get_path(kobj, GFP_KERNEL);
//添加各種環境變數
add_uevent_var(env,"ACTION=%s", action_string);
add_uevent_var(env,"DEVPATH=%s", devpath);
add_uevent_var(env,"SUBSYSTEM=%s", subsystem);
uevent_ops->uevent(kset,kobj, env);
add_uevent_var(env,"MAJOR=%u", MAJOR(dev->devt));
add_uevent_var(env,"MINOR=%u", MINOR(dev->devt));
add_uevent_var(env,"DEVNAME=%s", name);
add_uevent_var(env,"DEVTYPE=%s", dev->type->name);
//還會增加匯流排相關的一些屬性環境變數等等。
#ifdefined(CONFIG_NET)//如果是PC的linux會通過socket的方式嚮應用層發送uevent事件消息,但在嵌入式linux中不啟用該機制。
#endif
argv [0] = uevent_helper;//即/sbin/mdev
argv [1] = (char *)subsystem;//”led_class”
argv [2] = NULL;
add_uevent_var(env,"HOME=/");
add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
4. 上述提到的devices_kset在devices_init被創建
devices_kset= kset_create_and_add("devices", &device_uevent_ops, NULL);
第三個傳參為NULL,代表預設在/sysfs/創建devices目錄
5. 上述設備屬性文件
staticstruct device_attribute devt_attr =
__ATTR(dev, S_IRUGO, show_dev, NULL);
static ssize_t show_dev(struct device*dev, struct device_attribute *attr,
char *buf){{
returnprint_dev_t(buf, dev->devt); //即返回設備的設備號
}
6.devices設備目錄響應uevent事件的操作
staticconst struct kset_uevent_ops device_uevent_ops = {
.filter = dev_uevent_filter,
.name = dev_uevent_name,
.uevent = dev_uevent,
};
7.call_usermodehelper是從內核空間調用用戶空間程式的介面。
8. 對於嵌入式系統來說,busybox採用的是mdev,在系統啟動腳本rcS 中會使用命令
echo /sbin/mdev >/proc/sys/kernel/hotplug
uevent_helper[]數組即讀入/proc/sys/kernel/hotplug文件的內容,即 “/sbin/mdev”
十四、創建設備文件
輪到mdev出場了,以上描述都是在sysfs文件系統中創建目錄或者文件,而應用程式訪問的設備文件則需要創建在/dev/目錄下。該項工作由mdev完成。
Mdev的原理是解釋/etc/mdev.conf文件定義的命名設備文件的規則,併在該規則下根據環境變數的要求來創建設備文件。Mdev.conf由用戶層指定,因此更具靈活性。本文無意展開對mdev配置腳本的分析。
Busybox/util-linux/mdev.c
int mdev_main(int argc UNUSED_PARAM, char**argv)
xchdir("/dev");
if (argv[1] &&strcmp(argv[1], "-s")//系統啟動時mdev –s才會執行這個分支
else
action= getenv("ACTION");
env_path= getenv("DEVPATH");
G.subsystem= getenv("SUBSYSTEM");
snprintf(temp, PATH_MAX,"/sys%s", env_path);//到/sysfs/devices/led目錄
make_device(temp,/*delete:*/ 0);
strcpy(dev_maj_min,"/dev");
//讀出dev屬性文件,得到設備號
open_read_close(path,dev_maj_min + 1, 64);
….
mknod(node_name,rule->mode | type, makedev(major, minor))
最終我們會跟蹤到mknod在/dev/目錄下創建了設備文件。
我們追求:
1.從上電第一行代碼、系統第一行代碼、模塊第一行代碼、應用第一行代碼,深入講解嵌入式軟體生命周期。
2 深刻理解硬體體系,以面向對象思維剖析各種匯流排和驅動框架。
3 聚焦軟體層次設計和框架設計
4 知其然,知其所以然
更多的嵌入式linux和android、物聯網、汽車自動駕駛等領域原創技術分享請關註微信公眾號: