Linux | Linux 驱动模型(1)

Linux 核心开发团队在开发2.5内核的过程中,引入了Linux驱动模型(Linux Driver Model),有时也被称为Linux设备模型(Linux Device Model),主要的目的是解决之前版本存在的以下问题:

  • 没有一种统一的机制表达驱动和设备之间的关系;

  • 没有通用的热插拔机制;

  • 没有通用的电源管理机制;

  • procfs文件系统过度混乱,包含了许多不是进程的信息。

outside_default.png

Linux内核基于kobject内核对象机制将系统中的总线类型、设备和驱动分别用bus_type、device和device_driver等对象描述,并将其组织成一个层次结构的系统,统一管理各种类别(class)的设备及其接口(class_interface),同时借助sysfs文件系统将所见设备系统展示给用户空间,提供了一个完全层次结构的用户视图。

在上一篇文章里,我们就了解到了,要把一个磁盘挂载到系统,kobject、sysfs这些概念都是绕不开的东西,不了解这些东西,似乎不太好往下做了,所以我们还是应该继续巩固这方面的基础知识。

Linux 驱动模型的核心内容可以综合如下:

  • 以内核对象为基础。内核对象用kobject表示,相当于其他对象的基类,是构建Linux驱动模型的关键。具有相同类型的内核对象构成内核对象集kset,内核对象集也包含自己的内核对象,从而组织成层次化的结构。

  • 用sysfs文件系统导出到用户空间内核中的所有内核对象组织成树状,以对象属性为叶子,通过sysfs文件系统,将用户空间对文件的读/写操作转化为内核对象属性的显示和保存方法。从而导出内核对象信息,并提供配置接口。

  • 将Linux子系统表达为总线类型/驱动/设备/类/接口的关系,分别用bus_type、device、device_driver、class和class_interface结构表示。每个子系统有自己的总线类型,它有一条驱动链表和一条设备链表,用来链接已经加载的驱动和已发现的设备,驱动加载和设备发现的顺序可以是任意的。每个设备最多绑定一个驱动,被绑定了驱动的设备可以正常工作。除此以外,每个设备可以属于某个唯一类,类上包含多个接口,接口的方法被作用于设备,不管是先添加接口还是先发现设备。

引用计数

软件开发常常面对将一个分配好内存的对象多次传递,并在多处使用的场景,我们需要在程序动态运行的现实下,明确知道这些对象使用完毕的时机,以便释放它所用的内存。为此,需要引用计数:

  • 防止内存泄漏:确保已分配的对象最终会被释放;

  • 防止访问已释放的内存:确保不会使用已经被释放的对象。

数据结构kref来完成了这个任务,它就是单纯的做原子加减。

//common/include/linux/kref.h:19
struct kref {
  refcount_t refcount;
};
//common/tools/include/linux/refcount.h:52
typedef struct refcount_struct {
  atomic_t refs;
} refcount_t;
//common/tools/include/linux/lockdep.h:58
#define atomic_t unsigned long

对atomic_t的实现不必过多了解,我们只需要知道对它的操作是原子性的,由硬件CPU直接保证。

一些对kref的基础操作:

static inline void kref_init(struct kref *kref)
初始化对象并设引用计数为1


static inline unsigned int kref_read(const struct kref *kref)
读取引用计数


static inline void kref_get(struct kref *kref)
使引用计数加1,使用前必须init过


static inline int kref_put(struct kref *kref, void (*release)(struct kref *kref))
使引用计数减一,减为0了就调用传入的release函数

所以,假如我们有某struct包含kref结构,那么可以struct->kref找到kref指针,然后使用该指针可以操控引用计数器,但是假如我们有了kref结构指针,想要找到该struct指针,kernel提供了一个宏定义函数来干这个事儿。

//common/include/linux/kernel.h:497
#define container_of(ptr, type, member) ({        \
  void *__mptr = (void *)(ptr);          \
  BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&  \
       !__same_type(*(ptr), void),      \
       "pointer type mismatch in container_of()");  \
  ((type *)(__mptr - offsetof(type, member))); })

*struct = container_of(kref-pointer, type, member)

反解struct指针,这利用了kref指针在类中的偏移是固定的这个特性,这有时是很有用的。

如果一个struct包含了引用计数器,那么在初始化该struct时也必须要初始化它的引用计数器(通过调用kref_init),设置这个init的结构也得必须调用一次kref_put来最终释放这个引用。之后在使用这个struct之前,要递增引用计数,使用完之后,要递减引用计数。通常来说,为了放置程序员忘了,不如再封装一次,调用这个struct时,去调用封装函数。

在计数减到0时,会去调用传入的释放函数,这个函数需要完成内存释放的工作,但是这个函数入参是kref的指针,需要使用我们之前说的container_of找到结构体指针,不要释放错了。

释放函数例子:

void foo_release(struct kref *kref)
{
  struct foo *foo;
  foo = container_of(foo, struct foo, kref);
  kfree(foo);
}

如果release被调用过了,那么这个结构体就不应该再被使用了,因为相关内存可能已经被释放。

内核对象及集合

Linux驱动模型的基础是内核对象。它将总线类型、设备、驱动等都看作是内核对象,使用kobject结构来表示。我们先看看这个结构是如何被定义的:

//common/include/linux/kobject.h:65
struct kobject {
  const char    *name; // kobject 的名字
  struct list_head  entry; // 将kobject连接到kset的连接件
  struct kobject    *parent; // 指向kobject的父对象的指针
  struct kset    *kset; // 如果kobject连接到了kset,则指向
  struct kobj_type  *ktype; // kobject的类型
  struct kernfs_node  *sd; /* 指向sysfs内部树中的节点 */
  struct kref    kref; // 前文中介绍的引用计数
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
  struct delayed_work  release;
#endif
  unsigned int state_initialized:1; // 初始化标记
  unsigned int state_in_sysfs:1; // 添加到内核sysfs关系树标记
  unsigned int state_add_uevent_sent:1; // 已经发送添加事件标记
  unsigned int state_remove_uevent_sent:1; // 已经发送删除事件标记
  unsigned int uevent_suppress:1; // 抑制标记
};

细心的同学可能会发现有一个数据结构叫list_head,这个结构用来构建一种与类型无关的双向循环链表。

这个结构的定义如下:

//common/include/linux/types.h:178
struct list_head {
  struct list_head *next, *prev;
};

这个定义中,只有两个指向list_head的指针,这个指针是通用的,不需要因为特定的结构而去做特别的定义,这能做到它的通用性,而且使用前文中介绍的container_of宏定义,能轻松地找到包含当前list_head指针的结构体指针。

因此,在Linux中,双循环链表具有以下特性:

  • 链表结构被作为一个成员嵌入到宿主数据结构内;

  • 该嵌入连接件可以被放在宿主结构内的任何地方;

  • 可以为链表结构取任何的名字;

  • 宿主结构可以有多个链表链接见。

我们还可以看到,定义中有一个kobj_type结构体,下面来分析这个结构体的作用。

//common/include/linux/kobject.h:144
struct kobj_type {
  void (*release)(struct kobject *kobj);// 内核对象的释放销毁方法
  const struct sysfs_ops *sysfs_ops;// 指向操作表结构的指针,其中实现内核对象的属性读写方法
  struct attribute **default_attrs;  /* 这种类型的内核对象的默认属性 */
  const struct attribute_group **default_groups; // 这种类型的内核对象的默认属性组
  const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
  const void *(*namespace)(struct kobject *kobj);
  void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
};

对于相同类型的kobject,kobj_type收敛了一部分它们所共有的成员和方法,例如release方法,在前面介绍过,kref被put为0的时候,它将用来释放内核对象。

某种程度上kset看上去像是kobj_type的扩展,它表示内核对象的集合。kobj_type关注于对象的类型,kset关注于内核对象的聚合,如果我没有理解错的话,相同的kobj_type的内核对象,可以位于不同的kset中。

kset定义:

struct kset {
  struct list_head list; // 用于循环遍历set中的object的双向链表
  spinlock_t list_lock; // 遍历时的自旋锁
  struct kobject kobj; // 这个kset的内嵌kobject
  const struct kset_uevent_ops *uevent_ops; // 这个kset的uevent操作集
}

可以看到即使是kset里面也同样内嵌了一个kobject,完全可以将kset本身作为kobject来对待,kset将里面所包含的object组织为一个链表,list域为表头,对应于kobject里面的entry域,kset的指针也被包含在kobject的kset域。需要注意可能存在不包含于kset的kobject。

kset的好处是又将一堆有着相同模式操作的kobject聚合在一起,kset定义了一个uevent操作表,对于一个层次在它之下的kobject,并且层次路径上没有其他的kset,如果这个kobject上发生了某种事件,就会调用操作表中相应的函数,来通知用户空间。

kset层次下包含的kobject类型也可以不同,比如devices_kset下可以包含dynamic_kobj_ktype的kobject(virtual)、kset_ktype的kset(system)、device_ktype的kobject(PCI总线)等。

总结一下,kset具有如下功能:

  • 作为包含一组对象的容器,kset可以被内核用来跟踪“所有块设备”或者“所有PCI设备驱动”;

  • 作为一个目录级别的“粘合剂”,将设备模型中的内核对象(以及sysfs)粘在一起。每个kset都内嵌一个kobject,它可以作为其他kobject的parent,通过这样的方式来建立设备模型层次结构;

  • kset里面包含uevent操作,使得可以支持kobject的“热插拔”。

内核提供了许多函数用于管理内核对象:

//common/include/linux/kobject.h:89
int kobject_set_name(struct kobject *kobj, const char *name, ...);


int kobject_set_name_vargs(struct kobject *kobj, const char *fmt,
         va_list vargs);


static inline const char *kobject_name(const struct kobject *kobj)
{
  return kobj->name;
}
// 初始化函数,需要在已经为kobject完成空间分配后调用
extern void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
// 向sysfs注册这个kobject
int kobject_add(struct kobject *kobj, struct kobject *parent,
    const char *fmt, ...);
// 顾名思义,前面的二合一
int kobject_init_and_add(struct kobject *kobj,
       struct kobj_type *ktype, struct kobject *parent,
       const char *fmt, ...);
// 执行与add相反的动作,从sysfs移除
extern void kobject_del(struct kobject *kobj);
// 动态的生成一个kobject结构,里面包含了init过程
extern struct kobject * __must_check kobject_create(void);
extern struct kobject * __must_check kobject_create_and_add(const char *name,
            struct kobject *parent);
// 重命名
extern int __must_check kobject_rename(struct kobject *, const char *new_name);
// 移动kobject到另外的parent
extern int __must_check kobject_move(struct kobject *, struct kobject *);
// 引用计数相关
extern struct kobject *kobject_get(struct kobject *kobj);
extern struct kobject * __must_check kobject_get_unless_zero(
            struct kobject *kobj);
extern void kobject_put(struct kobject *kobj);
// 返回namespace tag
extern const void *kobject_namespace(struct kobject *kobj);
extern void kobject_get_ownership(struct kobject *kobj,
          kuid_t *uid, kgid_t *gid);
extern char *kobject_get_path(struct kobject *kobj, gfp_t flag);


/**
 * kobject_has_children - Returns whether a kobject has children.
 */
static inline bool kobject_has_children(struct kobject *kobj)
{
  WARN_ON_ONCE(kref_read(&kobj->kref) == 0);


  return kobj->sd && kobj->sd->dir.subdirs;
}

具体细节这里不做分析,总之,在之前的实践中我们也可以知道,add了一个kobject之后,对应的路径下就会生成相应的文件夹。生成有如下规则:

  • parent定义,生成在parent下面;

  • parent没定义,kset定义,kset的kobject赋值为parent,生成在parent下面;

  • parent和kset都没定义,生成于sys根目录。

同样有许多函数用于管理kset:

//common/include/linux/kobject.h:215
// 初始化内核对象集
extern void kset_init(struct kset *kset);
// 初始化内核对象集并一次性将它添加到sysfs,调用前kset空间必须已申请
extern int __must_check kset_register(struct kset *kset);
extern void kset_unregister(struct kset *kset);
// 分配空间、初始化、并一次性将它添加到sysfs
extern struct kset * __must_check kset_create_and_add(const char *name,
            const struct kset_uevent_ops *u,
            struct kobject *parent_kobj);
// container_of转换一下
static inline struct kset *to_kset(struct kobject *kobj)
{
  return kobj ? container_of(kobj, struct kset, kobj) : NULL;
}
// 引用计数相关
static inline struct kset *kset_get(struct kset *k)
{
  return k ? to_kset(kobject_get(&k->kobj)) : NULL;
}


static inline void kset_put(struct kset *k)
{
  kobject_put(&k->kobj);
}


static inline struct kobj_type *get_ktype(struct kobject *kobj)
{
  return kobj->ktype;
}
// 找某个kobject
extern struct kobject *kset_find_obj(struct kset *, const char *);

内核uevent相关函数:

//common/include/linux/kobject.h:255
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
// 实际完成发送的函数,比较关键,但太长,就不贴了
//impl:common/lib/kobject_uevent.c:457
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
      char *envp[]);
int kobject_synth_uevent(struct kobject *kobj, const char *buf, size_t count);
// 添加一对键值关系到环境
int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...);

环境数据为Linux系统支持设备热插拔至关重要,因为设备热插拔需要内核与用户空间配合完成,内核表示环境数据的结构是kobj_uevent_env,其结构定义如下:

//common/include/linux/kobject.h:159
struct kobj_uevent_env {
  char *argv[3];
  char *envp[UEVENT_NUM_ENVP]; // 环境变量/值数组,最多UEVENT_NUM_ENVP(=64))项
  int envp_idx; // 当前索引
  char buf[UEVENT_BUFFER_SIZE]; // 环境数据缓冲区,最多UEVENT_BUFFER_SIZE(=2048)字节
  int buflen; // 缓冲区长度
};

记录的原理是将键值以"variable=value\0"的形式记录在buf缓冲区中,buflen指向还未使用的第一个字符位置,每一条记录的起始位置指针存放于envp数组中,envp_idx记录当前空闲的记录指针数组位置。

outside_default.png

然后该结构体将会作为入参,传入到配置的kset_uevent_ops定义的回调函数中,我们来看看kset_uevent_ops里面有些什么:

//common/include/linux/kobject.h:167
struct kset_uevent_ops {
  int (* const filter)(struct kset *kset, struct kobject *kobj);
  const char *(* const name)(struct kset *kset, struct kobject *kobj);
  int (* const uevent)(struct kset *kset, struct kobject *kobj,
          struct kobj_uevent_env *env);
};

里面包含一个过滤回调函数,返回0表示不需要报告,一个名字回调函数,返回子系统名字,和一个真正的用于写入通知用户空间的信息的回调函数,可以发现该函数里有一个入参就是kobj_uevent_env,调用回调函数后,该入参结构体会完成写入动作。

紧接着,环境数据将被发送到用户空间,从代码中可以看出,这个操作的完成可以使用socket通信机制或者user_helper机制,这里就不深究了。

sysfs文件系统

从内部看,sysfs是一种表示内核对象、对象属性以及对象关系的一种机制。sysfs核心将内核输出的对象、对象属性以及对象关系组织成树状形式,称为sysfs内部树;从外部看,sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构(sysfs外部树?),向用户空间导出内核的设备和驱动信息,并且为内核的设备和驱动提供配置接口。

sysfs核心负责为内核中的内部表示和用户空间的外部呈现之间建立对应关系,也被称为sysfs映射。

  • 内核对象被映射为用户空间的目录;

  • 对象属性被映射为用户空间的常规文件;

  • 对象关系被映射为用户空间的符号链接。

根据sysfs的特性,sysfs代码提供了两方面的API,一个是内核编程接口,用于向其他内核模块提供构建内部树的API,另一个是系统文件接口,使用户空间可以查看并操作对应的内核对象。

outside_default.png

在我所研究的Android内核代码中使用的数据结构已经和Linux2.6内核存在许多不同,采用了kernfs这个新的东西(经查询 ,kernfs在内核3.14版本被合入),kernfs拆分了sysfs使用的部分内部逻辑。

经过不断查询,了解到kernfs将原本的sysfs_dirent数据结构重新设计为kernfs_node,相关操作重新设计为kernfs_ops,使得sysfs成为了链接内核对象和(二进制)属性的一个包装层。这样改进的目标是运行用户使用sysfs的核心功能而不是滚动用户自己的伪文件系统实现(通常无法处理文件关闭、锁定与vfs层的分离等)。(虽然还是不怎么理解,不过知道这里进行了一次优化就好),大概是现在变成了这种:

outside_default.png

感觉是把sysfs拆分成了sys和fs,之前在kobject中有些匪夷所思的gid和uid也大概知道是什么了,因为kernfs不再与kobject直接关联,所以它们采用了一种新的映射方式去寻找kobject。

既然已经发生了变化,之前的一些思路也不太适用于现在,关于sysfs的介绍就到这里。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值