Linux内核编程——Linux设备模型

引言

在上一章学习了 Linux 驱动基础之后,本章将介绍 Linux 设备模型(LDM)的结构。GNU/Linux 内核进行了标准化,旨在简化内核及驱动的维护和扩展,方便开发者进行开发和管理。本章将帮助你更好地理解这些基础知识。

结构

本章涵盖以下内容:

  • Linux 设备模型的数据结构
  • 总线模型
  • 设备模型
  • 驱动模型
  • 类(Class)
  • 关于 procfs
  • 关于设备树(DTB)

目标

Linux 设备模型是内核中的一个框架,提供了一种标准化、统一的方式来表示和管理各种硬件设备。它抽象了与不同设备交互的细节,使内核和驱动程序能够通过一致的接口协同工作。我们将逐一探讨这些接口,以加深理解。

Linux 设备模型数据结构

设备模型简化了驱动程序的编写和维护,同时促进模块化及设备管理的便利性,主要包括:

  • 总线模型(Bus model) :设备被组织在总线中,总线代表系统中的物理或逻辑互联。如 PCI、USB、I2C 及平台总线等。

  • 设备模型(Device model) :设备被表示为模型中的对象,每个设备拥有唯一标识符及一组属性。设备按层级结构组织,反映设备间的物理或逻辑关系。

  • 设备节点(Device nodes) :设备关联于 /sys 文件系统中的设备节点,这些节点提供了可读写的接口供用户与应用程序访问设备属性。通过读写对应节点,用户可查询和配置设备。内核通过虚拟文件系统 sysfs 在用户空间中呈现其设备模型,通常挂载于 /sys 目录,包含以下子目录:

    • block:系统中的所有块设备(磁盘、分区等)
    • bus:物理设备所连接的总线类型(pci、ide、usb 等)
    • class:驱动类(net、sound、usb 等)
    • devices:设备的层级结构
    • firmware:系统固件信息(高级配置与电源管理接口)
    • fs:已挂载文件系统信息
    • kernel:内核状态信息(登录用户、热插拔等)
    • module:当前加载的模块列表
    • power:能源管理子系统相关信息
  • 类模型(Class model) :设备根据功能或类型被分类,如块设备、网络设备等。每个类定义了一组设备应遵循的共同属性和行为。

  • 驱动模型(Driver model) :定义设备驱动与设备模型之间的接口。驱动程序向模型注册自身,并与特定设备类型关联。检测到兼容设备时,内核会将相应驱动绑定到该设备。

  • 驱动绑定(Driver binding) :将设备与合适驱动程序关联的过程。内核根据设备标识符(如 PCI ID)匹配设备与驱动,确保设备被正确驱动。

  • 驱动核心(Driver core) :设备模型的核心部分,负责驱动和设备的注册与注销。主要功能是匹配设备和驱动,确保双方通信顺畅。

  • 设备树(Device tree) :在使用设备树的架构(如 ARM)中,设备树是一种描述硬件拓扑和属性的数据结构,用于在内核启动时实例化设备并配置设备模型。

Linux 设备模型提供了一个灵活且可扩展的框架,支持各种硬件架构和设备。它实现了硬件细节与驱动逻辑的分离,简化了多样设备驱动的开发和维护。

Linux 内核驱动通常通过驱动模型接口与设备模型交互,完成设备注册、资源管理以及与内核其他部分的通信。设备模型是 Linux 内核的基础,使其支持多样化的硬件生态系统。

sysfs 文件包含一些标准属性,这些属性由同名文件或目录表示,主要包括:

  • dev :主设备号和次设备号,用于自动创建 /dev 目录下的条目
  • device:指向包含设备的目录的符号链接,用于发现提供特定服务的硬件设备(如以太网 PCI 卡)
  • driver :指向驱动目录的符号链接(位于 /sys/bus/*/drivers

kobject 结构体通常嵌入在更大的结构体中。它集成了一组功能,这些功能将在 Linux 设备模型层次结构中的更高抽象层次上使用。

例如,cdev 结构体定义如下:

arduino 复制代码
struct cdev {
        struct kobject kob;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
};

我们可以看到该结构体中包含一个类型为 kobject 的字段,而 kobject 结构体定义如下:

arduino 复制代码
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;
};

因此,kobject 结构体是层次化的:一个对象有父对象,并包含一个 kset 成员,而 kset 包含同级别的对象集合。

该结构需要通过 kobject_init() 函数进行初始化。同时,在初始化过程中需要设置 kobject 的名称(该名称会显示在 sysfs 中),通过 kobject_set_name() 函数完成。

kobject 的任何操作都通过调用 kobject_get() 来增加其内部计数器,或当对象不再使用时调用 kobject_put() 减少计数器。只有当内部计数器为 0 时,kobject 才会被释放。

为了释放包含 kobject 结构体的设备结构相关资源(例如 cdev),需要一个通知方法,该方法称为 release,并通过 ktype 字段(类型为 struct kobj_type)关联到对象。

kobject 是 Linux 设备模型的基础结构。设备模型中更高级别的结构包括 struct bus_typestruct devicestruct device_driver

关于 Linux 设备模型(LDM)的更多帮助资料,可参考以下链接:

总线模型

总线是指将各种硬件设备连接到系统的逻辑或物理互联。总线模型是一种抽象,提供了在内核中表示和管理不同类型总线的标准化方式。

这一抽象对于处理硬件架构的多样性及计算机系统中可能存在的大量总线至关重要。

Linux 总线模型是内核中的一个框架,系统性地表示和管理总线------即不同硬件组件和设备之间的通信通道或互联。总线在连接处理器、内存、外设及其他子系统时起着关键作用,使它们能相互通信并协同工作。

Linux 总线模型的特性和概念包括:

  • 总线类型(Bus type) :定义了不同的总线类型,每种类型代表一种特定的互联技术或架构。例如外设组件互联(PCI)、通用串行总线(USB)、内部集成电路总线(I2C)、串行外设接口(SPI)等。
  • 总线子系统(Bus subsystem) :每种总线类型在内核中作为一个子系统实现,负责管理连接到该总线类型的设备。
  • 总线设备(Bus devices) :属于某个总线的设备被表示为总线设备。每个总线设备都与特定总线类型关联,且具有该总线类型共有的特性。
  • 总线属性(Bus attributes) :描述总线特性的属性,如速度、地址范围等,通常通过 /sys 文件系统暴露给用户空间。
  • 总线注册(Bus registration) :总线和总线设备向内核注册,使内核能够动态发现并管理连接在这些总线上的硬件。
  • 总线枚举(Bus enumeration) :内核在启动过程中或热插拔新设备时,会枚举每个总线上的设备,发现设备、初始化并使其可用。
  • 设备连接(Device attachment) :设备连接到总线,总线模型帮助组织系统中设备的层级结构,层级反映设备与总线间的物理或逻辑连接关系。
  • 热插拔支持(Hotplug support) :总线模型支持热插拔,允许设备动态添加或移除,无需重启系统,尤其适用于支持热插拔的总线和设备,如 USB、PCI 等。

通过为总线及其设备提供标准化框架,Linux 总线模型促进了内核的模块化、可扩展性和灵活性。它简化了驱动开发,为处理总线和设备提供了通用基础设施,不论底层硬件架构如何。

/sys 文件系统中的 /sys/bus 目录是访问与总线相关信息和配置的用户空间入口。

Linux 设备模型提供了多个数据结构,确保硬件设备与设备驱动间的交互。该模型基于 kobject 结构体,通过以下结构体构建层级关系:

  • struct bus_type
  • struct device
  • struct device_driver

下图展示了驱动记录中数据结构的层级关系:

Linux 总线模型涵盖了多种总线类型,每种总线都有其特定用途。常见的总线类型包括:

  • 外设组件互连(PCI) :用于将扩展卡连接到主板。
  • 通用串行总线(USB) :常用于连接外围设备和外部设备。
  • 内部集成电路总线(I2C) :用于板上集成电路之间的通信。
  • 平台总线(Platform bus) :表示集成在系统主板上的设备。

记录一个总线涉及多个步骤,主要操作如下:

  • 总线类型注册:每种总线类型需向 Linux 内核注册,表示内核支持连接到该总线类型的设备。注册过程中需要提供一组函数,定义该总线上的设备如何枚举和管理。
  • 总线特定操作:定义如何发现、探测和初始化特定总线上的设备。此类操作针对总线特性设计,可能包括设备枚举、读取配置信息和资源分配等方法。
  • 总线设备枚举:总线模型协助枚举特定总线上的设备,提供统一方式发现和表示连接设备,帮助内核识别硬件配置。
  • 设备关系:总线上的设备以层级方式组织,反映设备间关系。例如,PCI 总线有多个插槽,每个插槽可包含一个设备。此层级结构体现在设备模型中。
  • /sys 文件系统表示 :总线及其设备信息通过 /sys 文件系统暴露给用户空间。用户和应用程序可通过 /sys/bus 下的相关文件和目录查询和配置设备。

总线是处理器与输入输出设备之间的通信通道。为了保证模型的通用性,所有 I/O 设备均通过总线连接到处理器(即使该总线可能是虚拟的,无物理硬件)。

当添加一个系统总线时,它会出现在 /sys/bus 的 sysfs 文件系统中。和 kobject 一样,总线也可以组织成层级结构,并在 sysfs 中被表示。

Linux 设备模型中的总线由结构体 struct bus_type 表示:

arduino 复制代码
struct bus_type {
        const char              *name;
        const char              *dev_name;
        struct device           *dev_root;
        struct bus_attribute    *bus_attrs;
        struct device_attribute *dev_attrs;
        struct driver_attribute *drv_attrs;
        struct subsys_private   *p;

        int (*match)(struct device *dev, struct device_driver *drv);
        int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
        int (*probe)(struct device *dev);
        int (*remove)(struct device *dev);
        //...
};

每个总线都有名称、一组默认属性、若干特定函数以及私有驱动数据。其中,uevent 函数(前称 hotplug)用于热插拔设备。

总线的主要操作包括:

  • 记录(register)
  • 实现 struct bus_type 结构体中描述的操作函数
  • 迭代(iteration)
  • 检查(inspection)

连接到总线的设备

总线通过 bus_register() 注册,通过 bus_unregister() 注销。

实现示例:

arduino 复制代码
#include <linux/device.h>

// 定义总线类型
struct bus_type my_bus_type = {
  .name   = "mybus",
  .match  = my_match,
  .uevent = my_uevent,
};

static int __init my_bus_init(void)
{
  int err;
  // ...
  err = bus_register(&my_bus_type);
  if (err)
    return err;
  // ...
}

static void __exit my_bus_exit(void)
{
  // ...
  bus_unregister(&my_bus_type);
  // ...
}

bus_type 结构体中通常需要初始化的函数是 matchuevent

arduino 复制代码
#include <linux/device.h>
#include <linux/string.h>

// 设备与驱动匹配函数,简单地通过名字比较实现
static int my_match(struct device *dev, struct device_driver *driver)
{
  return !strncmp(dev_name(dev), driver->name, strlen(driver->name));
}

// 处理热插拔用户事件,添加环境变量 DEV_NAME
static int my_uevent(struct device *dev, struct kobj_uevent_env *env)
{
  add_uevent_var(env, "DEV_NAME=%s", dev_name(dev));
  return 0;
}

match 函数在新设备或驱动添加到总线时调用,其作用是比较设备 ID 和驱动 ID。uevent 函数在用户空间生成热插拔事件前调用,负责添加环境变量。

对总线的其他操作包括遍历连接的驱动和设备。虽然不能直接访问(驱动和设备列表存储在驱动私有数据 subsys_private *p 字段中),但可使用宏 bus_for_each_devbus_for_each_drv 进行迭代。

LDM 接口允许为相关对象创建属性,这些属性对应 sysfs 总线子目录下的文件。

与总线关联的属性由 bus_attribute 结构体描述:

c 复制代码
struct bus_attribute {
         struct attribute attr;
         ssize_t (*show)(struct bus_type *, char *buf);
         ssize_t (*store)(struct bus_type *, const char *buf, size_t count);
};

属性通过宏 BUS_ATTR 定义,然后使用 bus_create_file()bus_remove_file() 函数在总线结构体中添加或移除属性。

my_bus 定义属性的示例:

arduino 复制代码
#define MY_BUS_DESCR "BPB my bus"

// 导出简单总线属性
static ssize_t my_show_bus_descr(struct bus_type *bus, char *buf)
{
        return snprintf(buf, PAGE_SIZE, "%s\n", MY_BUS_DESCR);
}
/*
 * 定义属性 - 属性名为 descr;
 * 完整名称为 bus_attr_descr;
 * sysfs 入口应为 /sys/bus/mybus/descr
 */
BUS_ATTR(descr, 0444, my_show_bus_descr, NULL);

// 在模块初始化函数中指定属性
static int __init my_bus_init(void)
{
        int err;
        // ...
        err = bus_create_file(&my_bus_type, &bus_attr_descr);
        if (err) {
                /* 处理错误 */
        }
        // ...
}

static void __exit my_bus_exit(void)
{
        // ...
        bus_remove_file(&my_bus_type, &bus_attr_descr);
        // ...
}

总线同时表示一个 bus_type 对象和一个设备对象(总线本身也是一个设备)。

驱动模型

Linux 驱动模型有助于在核心内核功能与各类设备驱动之间保持清晰的分离,使内核更加模块化、可扩展且易于维护。

Linux 驱动模型包括以下内容:

  • 设备驱动注册与初始化

    • struct device_driver:每个设备驱动由 struct device_driver 结构体表示,包含驱动名称、支持的设备、初始化和清理的回调函数等信息。
    • module_driver():用于注册驱动到内核的宏,将驱动与特定内核模块关联。
  • 设备注册与管理

    • struct device:系统中每个设备由 struct device 结构体表示,包含设备名称、类型及其关联驱动等信息。
    • device_create():创建设备并与驱动关联的函数。
    • device_destroy():销毁先前创建的设备。
  • 驱动与设备匹配

    • 驱动匹配:根据设备类型、厂商ID、产品ID等条件匹配设备与驱动。
    • 驱动绑定:当检测到设备时,内核尝试将其绑定到合适的驱动。
  • 驱动操作

    • struct file_operations:包含驱动可实现的各种文件操作指针,如打开、读取、写入、释放等。
    • struct platform_driverstruct pci_driver:分别为平台设备和 PCI 设备的专用驱动结构。
  • 内核对象模型 :设备、驱动及相关实体均表示为内核对象,按层级组织,通过 /sys 文件系统访问。

  • sysfs 接口 :Linux 驱动模型在 /sys 文件系统中暴露接口,方便用户空间工具和应用查询与配置驱动及设备信息。

  • 热插拔与热换支持:驱动模型支持热插拔和热换操作,使设备可动态添加或移除而无需重启系统。

  • 设备属性与特性:提供标准化方式表示和查询设备属性,方便用户空间应用和脚本与设备交互。

通过采用 Linux 驱动模型,开发者能编写遵循标准接口的驱动,更便于将新硬件集成到 Linux 内核中。驱动模型的模块化和可扩展性提升了内核的整体稳定性和可维护性。

LDM 用于实现系统设备与驱动的简单关联,驱动可独立于物理设备导出信息。

在 sysfs 中,驱动信息不局限于单一子目录,而是散布于不同路径:

  • 已加载模块位于 /sys/module
  • 每个设备对应的驱动在 /sys/devices
  • 属于同一类的驱动在 /sys/class
  • 与总线相关的驱动在 /sys/bus

设备驱动由 struct device_driver 结构体表示,定义如下:

c 复制代码
struct device_driver {
         const char              *name;
         struct bus_type         *bus;
         struct driver_private   *p;
         struct module           *owner;
         const char              *mod_name;     /* 用于内置模块 */
         int     (*probe)        (struct device *dev);
         int     (*remove)       (struct device *dev);
         void    (*shutdown)     (struct device *dev);
         int     (*suspend)      (struct device *dev, pm_message_t state);
         int     (*resume)       (struct device *dev);
};

该结构包含驱动名(在 sysfs 中显示)、驱动所工作的总线以及设备运行过程中调用的各种函数。

还有类似于之前的 driver_register()driver_unregister() 函数用于注册和注销驱动。用于属性操作的有 struct driver_attribute 结构体、DRIVER_ATTR 宏及 driver_create_file()driver_remove_file() 函数,分别用来添加和删除驱动属性。

类似于设备,struct device_driver 通常被集成进更专用的结构体以适应特定总线类型,如 PCI 或 USB。示例如下:

scss 复制代码
// 自定义驱动类型
struct my_driver {
  struct module *module;
  struct device_driver driver;
};

#define to_my_driver(drv) container_of(drv, struct my_driver, driver);

int my_register_driver(struct my_driver *driver)
{
  int err;

  driver->driver.bus = &my_bus_type;
  err = driver_register(&driver->driver);
  if (err)
    return err;
  return 0;
}

void my_unregister_driver(struct my_driver *driver)
{
  driver_unregister(&driver->driver);
}

/* 导出注册和注销驱动函数 */
EXPORT_SYMBOL(my_register_driver);
EXPORT_SYMBOL(my_unregister_driver);

驱动的注册与注销操作被导出以供其他模块使用。驱动操作在总线初始化时定义,并导出给驱动自身调用。

当实现一个使用总线连接设备的驱动时,会调用 my_register_drivermy_unregister_driver 函数来关联总线。

在驱动实现中使用这些函数时,需要声明一个 my_driver 类型的结构体,初始化成员,然后通过总线提供的函数注册它,示例如下:

ini 复制代码
static struct my_driver mydriver = {
  .module = THIS_MODULE,
  .driver = {
    .name = "bpbdriver",
  },
};
//...

// 注册驱动
int err;
err = my_register_driver(&mydriver);
if (err < 0) {
  /* 错误处理 */
}
// ...

// 注销驱动
my_unregister_driver(&mydriver);

类(Classes)

类是 Linux 设备模型的高级视角,简化了实现细节的呈现。虽然有针对 SCSI、ATA 等的具体驱动,但它们都属于磁盘类。类根据设备的功能对设备进行分组,而非连接方式或工作原理。类在 /sys/class 中有对应的表示。

描述类的两个主要结构体是 struct classstruct devicestruct class 描述通用类,而 struct device 描述与设备关联的类。它们的初始化、反初始化以及属性添加相关函数定义在 include/linux/device.h 中。

类的主要优势在于配合用户空间的 udev 二进制程序工作,udev 会基于类信息自动在 /dev 目录中创建设备节点。

一个通用类由如下的 class 结构体定义:

arduino 复制代码
struct class {
         const char              *name;
         struct module           *owner;
         struct kobject          *dev_kobj;
         struct subsys_private   *p;
         struct class_attribute          *class_attrs;
         struct class_device_attribute   *class_dev_attrs;
         struct device_attribute         *dev_attrs;

         int     (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
         void    (*class_release)(struct class *class);
         void    (*dev_release)(struct device *dev);
         //...
};

使用 class_register()class_unregister() 分别初始化和注销类,示例如下:

csharp 复制代码
static struct class my_class = {
        .name = "myclass",
};

static int __init my_init(void)
{
        int err;
        //...
        err = class_register(&my_class);
        if (err < 0) {
                /* 处理错误 */
        }
        //...
}

static void __exit my_cleanup(void)
{
        //...
        class_unregister(&my_class);
        //...
}

device_create() 函数初始化设备结构体,将通用类结构和传入的设备参数关联,同时会创建一个名为 dev 的类属性,包含设备的主设备号和次设备号(minor:major)。

用户空间的 udev 程序可读取该属性文件中的信息,通过调用 mknod/dev 目录下创建相应的设备节点。

初始化示例:

ini 复制代码
struct device* my_classdev;
struct cdev cdev;
struct device dev;

// 为设备 cdev.dev 初始化类
my_classdev = device_create(&my_class, NULL, cdev.dev, &dev, "myclass0");

// 销毁设备 cdev.dev 的类
device_destroy(&my_class, cdev.dev);

当检测到新设备时,设备会被分配到特定类,并在 /dev 目录下生成对应节点。例如上例中会创建 /dev/myclass0 节点。

关于 procfs

与其对应的 /sys 文件系统类似,/proc 是一个允许内核与用户空间交换信息的虚拟文件系统。

示例代码中包含了如下头文件:

arduino 复制代码
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
#include <linux/minmax.h>
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif

定义了一些常量和变量:

arduino 复制代码
#define PROCFS_MAX_SIZE 2048UL
#define PROCFS_ENTRY_FILENAME "buffer"
#define PROCFS_ENTRY_FOLDER   "bpb"

static struct proc_dir_entry *parent;
static struct proc_dir_entry *proc_file;
static char procfs_buffer[PROCFS_MAX_SIZE];
static unsigned long procfs_buffer_size = 0;

实现了读取函数 procfs_read(),它将缓冲区数据复制到用户空间,并处理读取偏移量,确保一次性读取:

arduino 复制代码
static ssize_t procfs_read(struct file *filp, char __user *buffer,
                           size_t length, loff_t *offset)
{
    if (*offset || procfs_buffer_size == 0) {
        pr_debug("procfs_read: END\n");
        *offset = 0;
        return 0;
    }
    procfs_buffer_size = min(procfs_buffer_size, length);
    if (copy_to_user(buffer, procfs_buffer, procfs_buffer_size))
        return -EFAULT;
    *offset += procfs_buffer_size;
    pr_debug("procfs_read: read %lu bytes\n", procfs_buffer_size);
    return procfs_buffer_size;
}

实现打开和关闭函数,分别增加和减少模块使用计数:

arduino 复制代码
static int procfs_open(struct inode *inode, struct file *file)
{
    try_module_get(THIS_MODULE);
    return 0;
}
static int procfs_close(struct inode *inode, struct file *file)
{
    module_put(THIS_MODULE);
    return 0;
}

根据内核版本,定义文件操作结构:

ini 复制代码
#ifdef HAVE_PROC_OPS
static struct proc_ops file_ops_proc_file = {
    .proc_read    = procfs_read,
    .proc_open    = procfs_open,
    .proc_release = procfs_close,
};
#else
static const struct file_operations file_ops_proc_file = {
    .read    = procfs_read,
    .open    = procfs_open,
    .release = procfs_close,
};
#endif

模块初始化时创建 /proc/bpb/buffer 目录和文件:

scss 复制代码
static int __init procfs_init(void)
{
    parent = proc_mkdir(PROCFS_ENTRY_FOLDER,NULL);    
    if( parent == NULL )
    {
            pr_info("Error creating proc entry");
            goto r_device;
    }
    proc_file = proc_create(PROCFS_ENTRY_FILENAME, 0644, parent,
                                &file_ops_proc_file);
    if (proc_file == NULL) {
        pr_debug("Error: Could not initialize /proc/%s\n",
                 PROCFS_ENTRY_FILENAME);
        return -ENOMEM;
    }
    proc_set_size(proc_file, 80);
    proc_set_user(proc_file, GLOBAL_ROOT_UID, GLOBAL_ROOT_GID);
    pr_debug("/proc/%s created\n", PROCFS_ENTRY_FILENAME);
    return 0;
    
r_device:    
    return -1;
}

static void __exit procfs_exit(void)
{
    proc_remove(parent); // 删除 /proc/bpb
    pr_debug("/proc/%s removed\n", PROCFS_ENTRY_FILENAME);
}

使用宏注册模块初始化和退出函数,并声明模块许可证:

scss 复制代码
module_init(procfs_init);
module_exit(procfs_exit);
MODULE_LICENSE("GPL");

注意:示例代码仅实现了只读的 procfs 条目。读写版本可在本书对应的 GitHub 仓库下载。

主要函数简介

  • proc_mkdir()

    用于在 /proc 文件系统中创建目录或子目录。

    原型:

    c 复制代码
    struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent);

    参数 name 为目录名,parent 指向父目录,若为 NULL,则创建于根目录。

  • proc_create()

    用于在 /proc 创建文件或目录条目,允许内核向用户空间暴露信息和功能。

    原型:

    c 复制代码
    struct proc_dir_entry *proc_create(const char *name, umode_t mode,
                                       struct proc_dir_entry *parent,
                                       const struct file_operations *proc_fops);

    参数说明:

    • name:条目名称
    • mode:权限模式(如 0644)
    • parent:父目录指针
    • proc_fops:文件操作指针
  • remove_proc_entry()

    用于删除 /proc 目录中的条目。

    原型:

    arduino 复制代码
    void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
  • proc_remove()

    用于递归删除文件夹及其子条目。

    原型:

    arduino 复制代码
    void proc_remove(struct proc_dir_entry *parent);

用户空间与内核空间数据交换关键函数

  • copy_from_user():从用户空间复制数据到内核空间。

    arduino 复制代码
    long copy_from_user(void *to, const void __user *from, unsigned long n);

    返回未成功复制的字节数,返回0表示复制成功。

  • copy_to_user():从内核空间复制数据到用户空间。

    arduino 复制代码
    long copy_to_user(void __user *to, const void *from, unsigned long n);

    返回未成功复制的字节数,返回0表示复制成功。

编译示例

使用上一章定义的 Makefile,执行:

go 复制代码
$ make clean && make

编译过程示例省略。

使用 modinfo 查看模块信息,加载模块,并验证其存在:

shell 复制代码
$ sudo insmod ./procfs.ko
$ lsmod | grep procfs
procfs                 20480  0

$ tree /proc/bpb
/proc/bpb
└── buffer

$ ls -alh /proc/bpb/buffer
-rw-r--r-- 1 root root 80 nov. 14 20:46 buffer

条目权限为 0644,意味着 root 用户可读写,其他用户只读。

操作示例

读取初始空缓冲区:

shell 复制代码
$ cat /proc/bpb/buffer

尝试写入字符串(非 root 用户无权限):

javascript 复制代码
$ sudo echo "hello bpb" > /proc/bpb/buffer
zsh: permission denied: /proc/bpb/buffer

切换为 root 后写入:

shell 复制代码
$ sudo su
# echo "hello bpb" > /proc/bpb/buffer

再读取缓冲区内容:

shell 复制代码
# cat /proc/bpb/buffer
hello bpb

额外说明

云计算的弹性优势在于按需使用资源,节省成本。工作负载常见场景如:周末办公关闭,负载低,资源可缩减;月末工资结算时,数据库和服务器负载高,需扩容。持续监控资源使用与成本,发现异常时采取自动化措施,既保障资源合理配置,也能优化成本。

此外,安全监控中,异常负载可能预示攻击行为,可用算法自动检测并报警,协助运维团队及时应对。

关于 DTB 设备树

设备树二进制(Device Tree Blob,DTB)是硬件配置数据的二进制表示,用于那些需要动态描述硬件的系统上的 Linux 内核,比如嵌入式系统或片上系统(SoC)。它为内核提供了一种结构化的方式来理解系统硬件,而无需将硬件信息硬编码在内核中。

设备树是一种描述硬件的数据结构,起源于 PowerPC 平台,但现已广泛应用于嵌入式系统。它有助于将硬件相关细节与操作系统或软件分离。

设备树使用一种称为设备树源(Device Tree Source,DTS)的可读格式编写,然后编译成二进制格式 DTB,供 Linux 内核使用。

目的

  • 描述硬件组件及其互连关系(例如 CPU、内存、总线、GPIO 等)。
  • 使 Linux 内核能在尽量少修改的情况下支持多种硬件。
  • 提供一种机制,将设备特定的数据传递给操作系统。

设备树由节点(nodes)和属性(properties)组成:

  • 节点(Nodes):代表设备或硬件组件。节点可包含子节点,形成层次化的硬件描述(如总线及其连接的设备)。
  • 属性(Properties):节点关联的键值对,描述属性如内存地址、中断请求号(IRQ)、兼容性字符串等。

下图展示了 Linux 访问设备树的示意:

图 4.3 描述了一个设备树源(DTS)示例:

ini 复制代码
/dts-v1/;

{
    compatible = "my-board,example";
    model = "My Custom Board";
    memory {
        device_type = "memory";
        reg = <0x80000000 0x20000000>; // 起始地址,大小
    };
    soc {
        compatible = "simple-bus";
        ranges;
        uart0: serial@1000 {
            compatible = "ns16550";
            reg = <0x1000 0x100>;
            interrupt-parent = <&intc>;
            interrupts = <5>;
        };
        gpio: gpio@2000 {
            compatible = "gpio-controller";
            reg = <0x2000 0x100>;
            gpio-controller;
            #gpio-cells = <2>;
        };
    };
};

关键组成部分说明:

  • /(根节点):顶层节点,描述整个系统。
  • compatible:用于标识设备或开发板的字符串,内核根据此字段匹配驱动。
  • reg:指定设备的内存映射寄存器地址。
  • interrupts:描述设备所使用的中断线。
  • 标签(如 uart0):用于在 DTS 中其他地方引用该节点。

设备树的使用流程是将 DTS 源文件通过设备树编译器(Device Tree Compiler,DTC)编译成二进制的 DTB 文件。

编译命令

编译命令示例:

perl 复制代码
$ dtc -I dts -O dtb -o my-device-tree.dtb my-device-tree.dts

参数说明:

  • -I dts:输入格式为 DTS。
  • -O dtb:输出格式为 DTB。
  • -o:指定输出文件名。

生成的 DTB 文件会在 Linux 内核启动时传递给内核。

启动流程如下:

  • 引导加载程序(如 U-Boot):

    • 加载内核并提供 DTB 文件。
    • DTB 可以嵌入内核,也可以单独加载。
  • 内核

    • 读取 DTB 文件以获取硬件信息。
    • 根据 DTB 中的 compatible 属性将驱动与硬件匹配。
  • 动态配置

    • 设备树允许无需重新编译内核即可进行动态配置。
    • 通过设备树覆盖(Overlay,附加的 DT 片段)在运行时修改硬件描述(例如启用新设备)。

使用 DTB 的主要优点:

  • 硬件抽象:内核开发者无需将硬件细节硬编码到内核中。
  • 可移植性:只需更换 DTB 文件,同一内核二进制可以运行于不同开发板。
  • 灵活性:支持通过覆盖实现运行时修改。

实际应用示例:

系统中有 UART 和 GPIO 控制器,DTS 定义这些设备,内核通过 DTB 文件初始化硬件。

使用 U-Boot 加载 DTB 的步骤

  1. 将 DTB 文件放置到启动分区。

  2. 更新启动加载程序配置:

    perl 复制代码
    perl
    复制
    setenv fdtfile my-device-tree.dtb
    saveenv
  3. 启动内核:

    bash 复制代码
    nginx
    复制
    bootz ${kernel_addr} - ${fdt_addr}
  4. 当系统无法启动或设备无法识别时:

    a. 使用 dtc 工具将 DTB 反编译回可读的 DTS:

    css 复制代码
    mathematica
    复制
    $ dtc -I dtb -O dts -o output.dts my-device-tree.dtb

    b. 查看内核日志(dmesg)中有关设备初始化的错误。

    c. 确保 compatible 字符串与内核驱动中预期的值匹配。

设备树覆盖(Overlays) 允许在运行时添加或修改设备树,适用于模块化硬件设计或条件配置。

示例:

ini 复制代码
/dts-v1/;
/plugin/;
/ {
    fragment@0 {
        target = <&gpio>;
        __overlay__ {
            new-led {
                compatible = "gpio-led";
                gpios = <&gpio 5 GPIO_ACTIVE_HIGH>;
                label = "user-led";
            };
        };
    };
};

此覆盖为 GPIO 控制器添加了一个 LED 设备。

DTB 是 Linux 内核硬件抽象机制的重要组成部分,尤其在嵌入式系统中尤为关键。理解其结构、生成和使用方式,将大大简化针对多样硬件配置的内核开发和调试工作。

总结

本章让我们再次进入了 GNU/Linux 内核的世界,详细介绍了多年来为简化内核的可维护性和开发而构建的基础设施。

结合到目前为止所学习的内容,下一章我们将着重讲解字符设备驱动的开发。

相关推荐
数据智能老司机10 分钟前
Linux内核编程——字符设备驱动程序
linux·架构·操作系统
DemonAvenger13 分钟前
Go网络编程基础:网络模型与协议栈概述
网络协议·架构·go
lyx 弈心36 分钟前
I/O 进程 7.2
linux·算法·io
舒克起飞了1 小时前
linux系统编程——Makefile、GDB调试
linux·运维·服务器
背影疾风1 小时前
C++之路:类基础、构造析构、拷贝构造函数
linux·开发语言·c++
m0_694845572 小时前
服务器如何配置防火墙规则开放/关闭端口?
linux·服务器·安全·云计算
风铃喵游3 小时前
构建引擎: 打造小程序编译器
前端·小程序·架构
阿巴~阿巴~3 小时前
Linux基本命令篇 —— alias命令
linux·服务器·bash
筏.k3 小时前
C++ 网络编程(14) asio多线程模型IOThreadPool
网络·c++·架构
好名字更能让你们记住我4 小时前
Linux多线程(十二)之【生产者消费者模型】
linux·运维·服务器·jvm·windows·centos