linux驱动开发(21)-Linux设备驱动模型(一)

sysfs文件系统

我们先讨论sysfs文件系统 。Linux设备模型是个极其复杂的结构,为了构建它,除了基本的元素外(这就是接下来会谈到的kobject、kset等基础类数据结构)​,尚需要一种机制,来向外部(用户空间的程序)展示内部的构造,并且通过文件接口的方式实现与外界的沟通与互动。sysfs文件系统就充当了这种角色,它不但在各种基础的数据结构之间建立彼此的互联层次关系,而且向外界提供了与数据结构进行互动的文件接口。这种形象的比喻反映到Linux系统,我们可以看到sysfs文件除了在内核空间所展现的合纵连横作用外,而且以文件目录层次结构的形式向用户空间提供了系统硬件设备间的一个拓扑图,这种文件形式 的接口也让用户空间与内核空间的数据对象的交互成为可能。如果熟悉proc文件系统的话,应当知道sysfs文件系统实际上取代了proc文件系统的功能,当然取代proc文件系统只是sysfs文件系统一小部分的功能而已。这种数据对象间的交互的一个具体的例子就是,透过sysfs文件系统可以取代ioctl的功能:如果向一个设备文件发送ioctl命令的话,需要首先打开该设备文件 ,然后再通过ioctl函数向设备发出命令,很显然需要一个完整(虽然代码可能很简单)的应用程序来做这件事,现在有了sysfs文件系统,一个很简单的shell命令也许就可以完成前面所说的工作。

sysfs文件系统的初始化发生在Linux系统的启动阶段:

cpp 复制代码
<fs/sysfs/mount.c>
int __init sysfs_init(void)
{
    ...
    err = register_filesystem(&sysfs_fs_type);
    if (!err) {
        sysfs_mount = kern_mount(&sysfs_fs_type);
    } else
        goto out_err;
    ...
}

函数将向系统注册一个类型为sysfs_fs_type的文件系统,sysfs_fs_type的定义为:

cpp 复制代码
<fs/sysfs/mount.c>
static struct file_system_type sysfs_fs_type = {
    .name    ="sysfs",
    .get_sb         =sysfs_get_sb,
    .kill_sb        =sysfs_kill_sb,
};

sysfs_fs_type中的get_sb成员,它指向函数sysfs_get_sb,它在内核空间创造了一棵独立的VFS树​,内核创建这棵VFS树主要用来沟通系统中总线、设备与驱动,同时向用户空间提供接口及展示系统中各种设备的拓展视图等,事实上它并不用来作为其他实际文件系统的挂载点。

sysfs_get_sb函数用来产生sysfs文件系统的超级块,其内部调用的最主要的函数是sysfs_fill_super,后者再经过一系列的函数调用链进入到sysfs_init_inode函数,这里之所以重点强调这个函数,是因为在接下来谈到内核对象的属性问题时会看到用户空间和内核对象的沟通问题,这种文件接口形式的交互发生在内核空间和用户空间,所以我们需要知道这条沟通的通道是如何建立起来的。在sysfs_init_inode中,函数将为sysfs文件系统中每个文件或目录所对应的inode赋予一个新的操作对象:

cpp 复制代码
<fs/sysfs/inode.c>
static void sysfs_init_inode(struct sysfs_dirent *sd, struct inode *inode)
{
    struct bin_attribute *bin_attr;
    inode->i_private = sysfs_get(sd);
    inode->i_mapping->a_ops = &sysfs_aops;
    inode->i_mapping->backing_dev_info = &sysfs_backing_dev_info;
    inode->i_op = &sysfs_inode_operations;
    set_default_inode_attr(inode, sd->s_mode);
    sysfs_refresh_inode(sd, inode);
    /* initialize inode according to type */
    switch (sysfs_type(sd)) {
    case SYSFS_DIR:
        inode->i_op = &sysfs_dir_inode_operations;
        inode->i_fop = &sysfs_dir_operations;
        break;
    case SYSFS_KOBJ_ATTR:
        inode->i_size = PAGE_SIZE;
        inode->i_fop = &sysfs_file_operations;
        break;
    case SYSFS_KOBJ_BIN_ATTR:
        bin_attr = sd->s_bin_attr.bin_attr;
        inode->i_size = bin_attr->size;
        inode->i_fop = &bin_fops;
        break;
    case SYSFS_KOBJ_LINK:
        inode->i_op = &sysfs_symlink_inode_operations;
        break;
    default:
        BUG();
    }
    unlock_new_inode(inode);
}

sysfs文件系统是个基于RAM实现的文件系统,如果编译内核时指定了CONFIG_SYSFS选项,那么这个文件系统就会包含到内核中。对于用户进程中的文件系统来说,sysfs的标准挂载点是"/sys"目录,将sysfs文件系统挂载到用户进程的"/sys"目录的命令为:

cpp 复制代码
mount -t sysfs sysfs /sys

如此,所有内核层面的对sysfs文件树的操作,都将一成不变地显示在用户空间的"/sys"目录下。

kobject和kset

我们将讨论Linux实现设备驱动模型的底层数据结构kobject和kset,在后面讨论总线、设备与驱动时,会经常看到对这些底层数据结构的操作。kobject和kset可以看作后面讨论的元素的父类。

kobject

Linux内核用kobject来表示一个内核对象,它在源码中的定义为:

cpp 复制代码
<include/linux/kobject.h>
struct kobject {
    const char        *name;
    struct list_head  entry;
    struct kobject        *parent;
    struct kset         *kset;
    struct kobj_type    *ktype;
    struct sysfs_dirent *sd;
    struct kref       kref;
    unsigned int state_initialized:1;
    unsigned int state_in_sysfs:1;
    unsigned int state_add_uevent_sent:1;
    unsigned int state_remove_uevent_sent:1;
    unsigned int uevent_suppress:1;
};

在此简单解释一下其每位成员的作用:

cpp 复制代码
const char *name

用来表示该内核对象的名称。如果该内核对象加入系统,那么它的name将会出现在sysfs文件系统中(表现形式是一个新的目录名)​。

cpp 复制代码
struct list_head entry

用来将一系列的内核对象构成链表。

cpp 复制代码
struct kobject *parent

指向该内核对象的上层节点。通过引入该成员构建内核对象之间的层次化关系

cpp 复制代码
struct kset *kset

当前内核对象所属的kset对象的指针。kset对象代表一个subsystem,其中容纳了一系列同类型的kobject对象。

cpp 复制代码
struct kref kref

其核心数据是一原子型变量,用来表示内核对象的引用计数。内核通过该成员追踪内核对象的生命周期。

cpp 复制代码
struct kobj_type *ktype

定义了该内核对象的一组sysfs文件系统相关的操作函数和属性。显然不同类型的内核对象会有不同的ktype,用以体现kobject所代表的内核对象的特质。通过该成员,C中的struct数据类型具备了C++中class类型的某些特点,这里体现了基于C的面向对象设计思想。同时,内核通过ktype成员将kobject对象的sysfs文件操作与其属性文件关联起来。

cpp 复制代码
struct sysfs_dirent *sd

用来表示该内核对象在sysfs文件系统中对应的目录项的实例。

cpp 复制代码
unsigned int state_initialized

表示该kobject所代表的内核对象初始化的状态,1表示对象已被初始化,0表示尚未初始化。

cpp 复制代码
unsigned int state_in_sysfs

表示该kobject所代表的内核对象有没有在sysfs文件中建立一个入口点。

cpp 复制代码
unsigned int uevent_suppress

如果该kobject对象隶属于某一kset,那么它的状态变化可以导致其所在的kset对象向用户空间发送event消息。成员uevent_suppress用来表示当该kobject状态发生变化时,是否让其所在的kset向用户空间发送event消息。值1表示不让kset发送这种event消息。

kobject数据结构最通用的用法是嵌在表示某一对象的数据结构中,比如内核中定义的字符型设备对象cdev中就嵌入了kobject结构:

cpp 复制代码
<include/linux/cdev.h>
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

下面介绍内核中定义的对kobject对象上的一些常用的操作函数。

kobject_set_name

该函数用来设定kobject中的name成员,函数原型为

cpp 复制代码
int kobject_set_name(struct kobject *kobj, const char *fmt, ...)

kobject_init

该函数用来初始化一个内核对象的kobject结构,其核心功能代码为(去除了一些参数检查等的代码)​:

cpp 复制代码
<lib/kobject.c>
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
{
    ...
    kobject_init_internal(kobj);
    kobj->ktype = ktype;
    return;
}

除了为kobj指定ktype成员外,真正的初始化工作发生在kobject_init_internal中:

cpp 复制代码
<lib/kobject.c>
static void kobject_init_internal(struct kobject *kobj)
{
    if (!kobj)
        return;
    kref_init(&kobj->kref);
    INIT_LIST_HEAD(&kobj->entry);
    kobj->state_in_sysfs = 0;
    kobj->state_add_uevent_sent = 0;
    kobj->state_remove_uevent_sent = 0;
    kobj->state_initialized = 1;
}

函数中的kref_init(&kobj->kref)用于将kobject的引用计数refcount初始化为1,state_initialized置为1表示该内核对象已被初始化,state_in_sysfs置为0表示该内核对象尚未出现在sysfs文件树中。

kobject_add

函数原型为:

cpp 复制代码
int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...)

对于kobject来讲,这是个非常重要的函数。因为内核源码中这个函数调用链比较烦琐,所以此处不再列出其源码,我们解释该函数的主要功能,然后给出作为示范性质的代码片段。kobject_add的主要功能有两个,一是建立kobject对象间的层次关系,二是在sysfs文件系统中建立一个目录。在将一个kobject对象通过kobject_add函数调用加入系统前,kobject对象必须已被初始化。

关于这两个功能的实现细节,kobject_add首先将参数parent赋值给kobj的parent成员kobj->parent = parent,然后调用kobject_add_internal(kobj)函数。在kobject_add_internal函数内部,如果调用kobject_add时parent是一NULL指针,那么要看该kobj是否在一个kset对象中:如果是就把该kset中的kobject成员作为kobj的parent;否则该kobj对象在sysfs文件树中就将处于根目录的位置。

cpp 复制代码
<lib/kobject.c>
static int kobject_add_internal(struct kobject *kobj)
{
    ...
    parent=kobject_get(kobj->parent);
    //在kobj有所属的kset的情况下
    if(kobj->kset){
        //如果调用kobject_add时,传入的parent参数是一NULL指针
        if(!parent)
            //就把kobj所在的kset中的kobj作为它的parent
            parent=kobject_get(&kobj->kset->kobj);
        //将kobj加入到所属kset链表的末尾
        kobj_kset_join(kobj);
        kobj->parent=parent;
    }
    //在kobj没有所属的kset的情况下,如果调用kobject_add时parent为NULL
    //那么kobj->parent也将为NULL
    ...
}

kobject_add接下来会调用sysfs_create_dir在sysfs文件树中创建目录:

cpp 复制代码
<fs/sysfs/dir.c>
int sysfs_create_dir(struct kobject * kobj)
{
    ...
    if (kobj->parent)
        parent_sd = kobj->parent->sd;
    else
        parent_sd = &sysfs_root;
    ...
    error = create_dir(kobj, parent_sd, type, ns, kobject_name(kobj), &sd);
    if (!error)
        kobj->sd = sd;
    return error;
}

可以看到,如果kobj->parent为NULL(刚刚在kobject_add_internal函数中讨论过这种情况)​,调用create_dir在sysfs文件树中为当前kobj创建目录时,parent_sd =&sysfs_root,否则parent_sd = kobj->parent->sd。parent_sd = &sysfs_root意味着在sysfs文件树的根目录下为kobj创建一个新的目录,否则就是在parent_sd对应的目录底下创建新目录。如果kobj在sysfs中成功创建了一个新目录,自然应该将kobj->state_in_sysfs置为1。在sysfs文件系统中,目录对应的数据结构为struct sysfs_dirent,函数中用sd表示该类型的一个实例,在将kobj对象加入sysfs文件树之后,kobj->sd = sd。

kobject_init_and_add

函数原型为:

cpp 复制代码
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
                    struct kobject *parent, const char *fmt, ...)

该函数实际的工作是将kobject_init和kobject_add两个函数的功能合并到了一起。

kobject_create

该函数用来分配并初始化一个kobject对象:

cpp 复制代码
<lib/kobject.c>
struct kobject *kobject_create(void)
{
    struct kobject *kobj;
    kobj = kzalloc(sizeof(*kobj), GFP_KERNEL);
    if (!kobj)
        return NULL;
    kobject_init(kobj, &dynamic_kobj_ktype);
    return kobj;
}

如果调用kobject_create来产生一个kobject对象,那么调用者将无法为该kobject对象另行指定kobj_type。kobject_create为产生的kobject对象指定了一个默认的kobj_type对象dynamic_kobj_ktype,这个行为将影响kobject对象上的sysfs文件操作。如果调用者需要明确指定一个自己的kobj_type对象给该kobject对象,那么还应该使用其他函数,比如调用kobject_init_and_add函数。

kobject_del

函数的实现代码为:

cpp 复制代码
<lib/kobject.c>
void kobject_del(struct kobject *kobj)
{
    if (!kobj)
        return;
    sysfs_remove_dir(kobj);
    kobj->state_in_sysfs = 0;
    kobj_kset_leave(kobj);
    kobject_put(kobj->parent);
    kobj->parent = NULL;
}

函数将在sysfs文件树中把kobj对应的目录删除,另外如果kobj隶属于某一kset的话,将其从kset的链表中删除。

接下来看个具体的sample。

cpp 复制代码
//先声明一个kobject对象指针parent
static struct kobject*parent=NULL;
static int __init kobj_demo_init(void)
{
    ...
    parent = kobject_create_and_add("pa_obj", NULL);
    ...
}
module_init(kobj_demo_init);

在把上述模块加载到系统之后,就可在/sys目录下看到一名为"pa_obj"的新目录:

cpp 复制代码
/sys# ls -l
total 0
...
drwxr-xr-x 2rootroot0Jun1819:03pa_obj
drwxr-xr-x 2rootroot0Jun1815:04power

如果在调用kobject_create_and_add时指定了parent参数,那么新kobject对象所对应的目录将建立在parent目录之下。比如把上面的代码改成:

cpp 复制代码
//声明两个kobject对象,层次结构为父子关系
static struct kobject*parent=NULL;
static struct kobject*child=NULL;
static int__init  kobj_demo_init(void)
{
    ...
    parent=kobject_create_and_add("pa_obj",NULL);
    //指定child kobj的parent
    child=kobject_create_and_add("cld_obj",parent);
    ...
}

加载上面的代码将会在sysfs树中生成两个新的目录,体现在用户空间的/sys目录下便是/sys/pa_obj目录和/sys/pa_obj/cld_obj目录。如果想试验上面的代码,记得要在模块的退出函数中调用kobject_del(child)和kobject_del(parent)将child与parent所指向的对象删除,这样/sys目录下的"cld_obj"和"pa_obj"目录才会消失,否则在删除这个新目录时会遇上麻烦,因为sysfs文件系统没有为用户进程提供删除目录的接口。

将一个kobject对象添加进系统或者从系统中删除,主要是围绕sysfs文件系统展开的,对应的结果反映到/sys目录中就是一个新目录的诞生或者是一个已存在目录的消亡。这种对sysfs文件树的操作的现实意义除了向用户空间展示不同kobject对象之间的层次关系外,还在于用户空间的程序可以通过文件系统的接口配置内核空间kobject对象的某些属性,这是下一节要讨论的主题。

总结:

kobject、kset是linux内核设备驱动模型的基础类数据结构。

sysfs向外部(用户空间的程序)展示设备模型内部的构造(层次关系),并且通过文件接口的方式实现与外界的沟通与互动。

Linux内核用kobject来表示一个内核对象。

kobject->kset->user space(uevent)

相关推荐
虾..1 小时前
Linux 软硬链接和动静态库
linux·运维·服务器
Evan芙1 小时前
Linux常见的日志服务管理的常见日志服务
linux·运维·服务器
hkhkhkhkh1233 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
HZero.chen4 小时前
Linux字符串处理
linux·string
张童瑶4 小时前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功1234 小时前
什么是SELinux
linux
石小千4 小时前
Linux安装OpenProject
linux·运维
柏木乃一4 小时前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程
Lime-30904 小时前
制作Ubuntu 24.04-GPU服务器测试系统盘
linux·运维·ubuntu
百年渔翁_肯肯5 小时前
Linux 与 Unix 的核心区别(清晰对比版)
linux·运维·unix