> 引子
本文是嵌入式企鵝圈開篇--《linux字符設備驅動剖析》的姐妹篇,在上述文章里面我們詳細描述了字符設備驅動框架涉及的驅動注冊、通過設備文件來訪問驅動等知識,并明確通過device_create接口并結合mdev來創建設備文件,但沒有展開這個知識點。本文將從代碼級去理解Linux設備類和設備文件的創建過程。通過這兩篇文章,我們將可以對linux字符設備驅動的機制和脈絡有全面的認識。
以下程序分析沒有縮進,編輯了好幾次都不行,耐心點才能跟蹤完整個代碼:-)
> 一、設備類相關知識
設備類是虛擬的,并沒有直接對應的物理實物,只是為了更好地管理同一類設備導出到用戶空間而產生的目錄和文件。整個過程涉及到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的子成員。
本文無意對sysfs和linux設備驅動模型進行展開,以后再另寫文章進行分析。
> 二、兩種創建設備文件的方式
在設備驅動中cdev_add將struct file_operations和設備號注冊到系統后,為了能夠自動產生驅動對應的設備文件,需要調用class_create和device_create,并通過uevent機制調用mdev(嵌入式linux由busybox提供)來調用mknod創建設備文件。當然也可以不調用這兩個接口,那就手工通過命令行mknod來創建設備文件。
> 三、設備類和設備相關數據結構
~~~
## 1include/linux/kobject.h
struct kobject {
const char *name;//名稱
struct list_head entry;//kobject鏈表
struct kobject *parent;//即所屬kset的kobject
struct kset *kset;//所屬kset
struct kobj_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.
## 2include/linux/device.h
~~~
struct class {//設備類
const char *name; //設備類名稱
struct module *owner;//創建設備類的module
struct class_attribute *class_attrs;//設備類屬性
struct device_attribute *dev_attrs;//設備屬性
struct kobject *dev_kobj;//kobject再sysfs中代表一個目錄
….
struct class_private *p;//設備類得以注冊到系統的連接件
};
~~~
## 3drivers/base/base.h
~~~
struct class_private {
//該設備類同樣是一個kset,包含下面的class_devices;同時在class_subsys填充父kset
struct kset class_subsys;
struct klist class_devices;//設備類包含的設備(kobject)
…
struct class *class;//指向設備類數據結構,即要創建的本級目錄信息
};
~~~
## 4include/linux/device.h
~~~
struct device {//設備
struct device *parent;//sysfs/devices/中的父設備
struct device_private *p;//設備得以注冊到系統的連接件
struct kobject kobj;//設備目錄
const char *init_name;//設備名稱
struct bus_type *bus;//設備所屬總線
struct device_driver *driver; //設備使用的驅動
struct klist_node knode_class;//連接到設備類的klist
struct class *class;//所屬設備類
const struct attribute_group **groups;
…
}
~~~
## 5drivers/base/base.h
~~~
struct device_private {
struct klist klist_children;//連接子設備
struct klist_node knode_parent;//加入到父設備鏈表
struct klist_node knode_driver;//加入到驅動的設備鏈表
struct klist_node knode_bus;//加入到總線的鏈表
struct device *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);
struct class_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);
//還會增加總線相關的一些屬性環境變量等等。
#if defined(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.上述設備屬性文件
~~~
static struct 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){{
return print_dev_t(buf, dev->devt);//即返回設備的設備號
}
~~~
6.devices設備目錄響應uevent事件的操作
~~~
static const 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/目錄下創建了設備文件。
更多原創技術分享敬請關注微信公眾號:嵌入式企鵝圈
