文章從上層應用訪問字符設備驅動開始,一步步地深刻分析Linux字符設備的軟件層次、組成框架和交互、如何編寫驅動、設備文件的建立和mdev原理,對Linux字符設備驅動有全面的講解。本文整合以前發表的《Linux字符設備驅動剖析》和《 Linux 設備文件的建立和mdev》兩篇文章,基於linux字符設備驅動的全部相關知識給讀者一個完整的呈現。node
1、從最簡單的應用程序入手linux
1.很簡單,open設備文件,read、write、ioctl,最後close退出。以下:android
2、/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/rcSsocket
Mount –a 會讀取/etc/fstab的內容來掛載,其內容以下:
4. /dev/NULL和/dev/console是在製做根文件系統的時候靜態建立的,其餘設備文件都是系統加載根文件系統和各類驅動初始化過程當中自動建立的,固然也能夠經過命令行手動mknod設備文件。
3、設備文件的建立
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)。
4、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後再回頭來分析這個問題。
5、字符設備驅動的註冊
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表明字符設備號和範圍。
6、再述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數據結構。
7、設備操做
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設備的控制。
8、再論字符設備驅動的初始化
綜上所述,字符設備的初始化包括兩個主要環節:
1) 字符設備驅動的註冊,即經過cdev_add向系統註冊cdev數據結構,提供file_operations操做集和設備號等信息,最終file_operations存放在全局指針變量cdev_map指向的Hash表中,其能夠經過設備號索引並遍歷獲得。
2) 經過device_create(設備類, 設備號, 設備名)在sys/class/設備類中建立設備屬性文件併發送uevent事件,而mdev利用該信息自動調用mknod在/dev目錄下建立對應的設備文件,以便應用程序訪問。
那麼如何經過經過device_create來建立設備文件呢,mdev的原理又是什麼呢?咱們接着分析。
9、設備類相關知識
設備類是虛擬的,並無直接對應的物理實物,只是爲了更好地管理同一類設備導出到用戶空間而產生的目錄和文件。整個過程涉及到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的子成員。
10、兩種建立設備文件的方式
在設備驅動中cdev_add將struct file_operations和設備號註冊到系統後,爲了可以自動產生驅動對應的設備文件,須要調用class_create和device_create,並經過uevent機制調用mdev(嵌入式linux由busybox提供)來調用mknod建立設備文件。固然也能夠不調用這兩個接口,那就手工經過命令行mknod來建立設備文件。
11、設備類和設備相關數據結構
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向用戶層提供信息。
12、建立設備類目錄文件
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目錄。
十3、建立設備目錄和設備屬性文件
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」
十4、建立設備文件
輪到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、物聯網、汽車自動駕駛等領域原創技術分享請關注微信公衆號: