linux 驱动 rtc

linux rtc 驱动 rtc_device 中包括cdev 和 device 所以从file operations 能拿到cdev 然后可以拿到device

cpp 复制代码
struct rtc_device {
    struct device dev;           // 设备模型核心结构体
    struct rtc_class_ops *ops;   // RTC操作函数集
    struct mutex ops_lock;
    struct cdev char_dev;        // 字符设备结构体
    int irq_freq;
    struct rtc_time alarm;
    // ... 其他成员
};

关键成员说明

  • char_devstruct cdev类型,表示字符设备接口,用于注册到字符设备系统,使用户可以通过/dev/rtcX访问

  • devstruct device类型,Linux设备模型核心结构体,用于sysfs、设备树、电源管理等

cpp 复制代码
static int rtc_dev_open(struct inode *inode, struct file *file)
{
    // 从inode获取cdev,然后通过container_of获取rtc_device
    struct rtc_device *rtc = container_of(inode->i_cdev, 
                                         struct rtc_device, 
                                         char_dev);
    // ...
    file->private_data = rtc;  // 保存rtc_device供后续操作使用
    return 0;
}
cpp 复制代码
err = cdev_device_add(&rtc->char_dev, &rtc->dev);
该函数会:
将cdev添加到字符设备系统,使其可通过设备号访问
将device添加到设备模型,创建sysfs节点
建立cdev与device之间的关联(通过cdev_set_parent(cdev, &dev->kobj)设置父子关系)

int cdev_device_add(struct cdev *cdev, struct device *dev)
{
    int rc = 0;

    if (dev->devt) {
        cdev_set_parent(cdev, &dev->kobj);  /* 在这里调用 */
        
        rc = cdev_add(cdev, dev->devt, 1);
        if (rc)
            return rc;
    }

    rc = device_add(dev);
    if (rc)
        cdev_del(cdev);

    return rc;
}

传统普通字符注册

cpp 复制代码
// 1. 单独注册字符设备
cdev_add(&mydev->cdev, devno, 1);

// 2. 单独注册设备模型
device_add(&mydev->dev);

在分开调用 cdev_add() 和 device_add() 的情况下,不会自动调用 cdev_set_parent()。这是两种调用方式的关键区别之一。

传统驱动如何处理?

大多数不调用 cdev_set_parent() 的传统驱动采用以下策略:

方案1:将cdev嵌入自定义结构体

cpp 复制代码
struct my_device {
    struct cdev cdev;
    struct device dev;
    // 其他成员
};

通过container_of管理生命周期:

cpp 复制代码
static int my_open(struct inode *inode, struct file *filp)
{
    struct my_device *mydev = container_of(inode->i_cdev, 
                                          struct my_device, cdev);
    filp->private_data = mydev;  // 后续通过private_data访问
}

方案2:手动调用(罕见)

如果确实需要sysfs层次关系,可以手动调用

复制代码
cdev_add(&mydev->cdev, devno, 1);
cdev_set_parent(&mydev->cdev, &mydev->dev.kobj);  // 手动建立关联
device_add(&mydev->dev);

但这种方式不推荐,因为:

  • cdev_device_add() 已封装了最佳实践

  • 手动调用顺序容易出错

  • 没有充分利用现代内核API

何时需要关心这个问题?

必须调用 cdev_set_parent() 的场景

  1. 子系统要求严格的sysfs层次结构(如RTC子系统)

  2. 需要参与复杂的设备模型事件通知

  3. 设备有复杂的电源管理依赖关系

可以忽略的场景

  1. 简单的字符设备,仅需基本功能

  2. 驱动完全控制两个对象的生命周期

  3. 不需要精细的sysfs组织

结论

答案 :传统分开调用不会 自动调用 cdev_set_parent()。但这不意味着传统方式错误,只是:

  • cdev_device_add():现代、简洁、自动管理

  • 分开调用:传统、灵活、需手动管理(通常通过结构体嵌入实现)

对于大多数普通字符驱动,不调用 cdev_set_parent() 也完全可行 ,因为在 open() 中通过 container_of() 获取驱动私有数据后,cdev和device的生命周期可以通过驱动逻辑自行保证

核心机制:kobject + kset

Linux 设备模型通过 kobjectkset 实现层次化管理,而非单一全局链表:

cpp 复制代码
struct device {
    struct kobject kobj;          // 嵌入的kobject,提供链表基础
    struct device *parent;        // 父设备指针(层次关系)
    struct kset *kobj_parent_kset; // 所属的kset
    // ...
};

各类设备的链表归属

1. 总线设备(bus devices)
cpp 复制代码
// 注册到全局总线链表
int bus_register(struct bus_type *bus)
  -> bus->p->subsys.kobj.kset = bus_kset;  // 全局bus_kset
  • 所有总线都挂载在 全局 bus_kset

  • 路径:/sys/bus/

  1. 类设备(class devices)
cpp 复制代码
// 注册到全局类链表
int class_register(struct class *cls)
  -> cls->p->subsys.kobj.kset = class_kset; // 全局class_kset
3. 平台设备(platform devices)
cpp 复制代码
int platform_device_register(struct platform_device *pdev)
  -> device_add(&pdev->dev)  // 最终会挂载到platform_bus_type
2. SPI/I2C设备
cpp 复制代码
spi_register_device()
  -> device_add(&spi->dev);  // 挂载到spi_master的设备下
3. PCI设备
  • 挂载在PCI总线层次下

  • 路径:/sys/devices/pci0000:00/0000:00:xx.x/

4. 普通字符设备
复制代码
// 通过cdev_device_add注册
cdev_device_add(&mydev->cdev, &mydev->dev);
  • 不会 加入全局 device 链表

  • 加入其父设备(如pdev)的子设备链表

  • 通过 cdev_set_parent() 建立层次关系,但cdev本身有独立链表

关键链表结构

1. 全局设备链表(仅维护kobject关系)

cpp 复制代码
// 所有kobject都通过其父kset链入链表
struct kobject {
    struct list_head entry;   // 链入parent kset的成员链表
    struct kobject *parent;   // 父对象
    struct kset *kset;        // 所属的kset
};

// 字符设备有自己的哈希表管理

static struct kobj_map *cdev_map; // 全局字符设备映射

  • 通过设备号快速查找cdev

  • 与device链表独立 ,但通过 cdev_set_parent() 关联

设备和驱动

cpp 复制代码
struct bus_type {
    struct kset *drivers_kset;  // 该总线上的所有驱动
    struct kset *devices_kset;  // 该总线上的所有设备
    // ...
};

注册流程

cpp 复制代码
device_add(&mydev->dev);  // 设备注册
  -> bus_add_device()     // 将设备链入 bus->devices_kset
  -> klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);

driver_register(&mydrv->driver);  // 驱动注册
  -> bus_add_driver()            // 将驱动链入 bus->drivers_kset
  -> klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);

Driver 的管理方式

同理,驱动也是按总线分类管理

cpp 复制代码
// 所有注册到PCI总线的驱动
struct bus_type pci_bus_type = {
    .name = "pci",
    .drivers_kset = &pci_drivers_kset,
    .devices_kset = &pci_devices_kset,
};

// 所有注册到I2C总线的驱动
struct bus_type i2c_bus_type = {
    .name = "i2c",
    .drivers_kset = &i2c_drivers_kset,
    .devices_kset = &i2c_devices_kset,
};

如何遍历所有设备/驱动?

方法1:通过sysfs(用户空间)

cpp 复制代码
# 查看所有PCI设备
ls /sys/bus/pci/devices/

# 查看所有I2C驱动
ls /sys/bus/i2c/drivers/

# 查看所有设备类
ls /sys/class/

方法2:通过内核API遍历

cpp 复制代码
// 遍历某总线的所有设备
void bus_for_each_dev(struct bus_type *bus, ...)
{
    klist_iter_init_node(&bus->p->klist_devices, ...);
    // 遍历 devices_kset
}

// 遍历某总线的所有驱动
void bus_for_each_drv(struct bus_type *bus, ...)
{
    klist_iter_init_node(&bus->p->klist_drivers, ...);
    // 遍历 drivers_kset
}

类(class)的独立管理

除了总线,class也维护独立的链表:

cpp 复制代码
struct class {
    struct kset *dev_kset;  // 该类中的所有设备
    struct subsys_private *p;
};

// 注册到类
device_add(&mydev->dev);
  -> device_add_class_symlinks()
  -> class_dev_kobj_add()  // 链入 class->dev_kset

关键数据结构关系

cpp 复制代码
全局kset(顶层)
├── bus_kset(所有总线)
│   ├── pci_bus_type.p->subsys.kset
│   ├── i2c_bus_type.p->subsys.kset
│   └── ...
└── class_kset(所有类)
    ├── input_class.p->subsys.kset
    ├── rtc_class.p->subsys.kset
    └── ...
cpp 复制代码
bus->devices_kset
    ├── device1->kobj
    ├── device2->kobj
    └── ...

bus->drivers_kset
    ├── driver1->p->kobj
    ├── driver2->p->kobj
    └── ...
cpp 复制代码
全局设备模型(顶层)
    ├── bus_kset(所有总线类型)  ← 并列
    │   ├── pci_bus_type
    │   ├── i2c_bus_type
    │   └── usb_bus_type
    │
    └── class_kset(所有设备类)  ← 并列
        ├── block_class
        ├── net_class
        ├── input_class
        └── rtc_class
cpp 复制代码
全局设备模型(顶层)
    │
    ├── bus_kset(所有总线类型)
    │   ├── pci_bus_type
    │   │   ├── drivers_kset(驱动链表)
    │   │   │   ├── e1000_driver.kobj (驱动对象)
    │   │   │   └── xhci_hcd_driver.kobj
    │   │   └── devices_kset(设备链表)
    │   │       ├── 0000:00:1f.2_dev.kobj (PCI设备)
    │   │       │   ├── 触发probe() → 绑定到ahci_driver
    │   │       │   └── 创建子设备 → scsi_host
    │   │       └── 0000:00:14.0_dev.kobj
    │   │           └── 创建子设备 → usb1, usb2...
    │   │
    │   ├── i2c_bus_type
    │   │   ├── drivers_kset
    │   │   └── devices_kset
    │   └── usb_bus_type
    │
    ├── class_kset(所有设备类)
    │   ├── block_class
    │   │   └── dev_kset
    │   │       └── sda_dev.kobj → 链接到 scsi_device->dev
    │   ├── net_class
    │   │   └── eth0_dev.kobj → 链接到 pci_device->dev
    │   ├── input_class
    │   └── rtc_class
    │       └── rtc0_dev.kobj (平台RTC设备)
    │           └── 通过cdev_device_add()关联 → rtc0_cdev
    │
    └── cdev_map(字符设备全局哈希表,独立管理)← 关键
        ├── [主设备号=1, 次设备号=8] → mem_cdev
        ├── [主设备号=10, 次设备号=0] → ptmx_cdev
        └── [主设备号=249, 次设备号=0] → rtc0_cdev
            ├── cdev.kobj.parent = &rtc0_dev.kobj  ← cdev_set_parent()
            ├── cdev.ops = &rtc_dev_fops
            └── 用户open() → 通过container_of()找到rtc_device

一个设备的双重归属

同一个设备可以同时属于某个 bus 和某个 class

cpp 复制代码
// PCI 网卡设备
struct device dev = {
    .bus = &pci_bus_type,      // 属于 PCI 总线
    .class = &net_class,       // 同时属于网络设备类
};

核心理解bus内核驱动开发视角class用户空间操作视角,两者从不同维度管理同一批设备,因此必须并列设计才能提供最大灵活性。

在 Linux 内核驱动里,"动词在前" 还是 "动词在后" 并不是随便写的,它背后有一条 "谁拥有资源" 的约定。

记住一句话:

"动词在前"表示"我要新建并返回一个对象";
"动词在后"表示"别人已经把对象造好,我只是拿它做后续动作"。

复制代码
rtc_device_create        → create → 生
device_create            → create → 生
cdev_alloc               → alloc  → 生
kobject_create_and_add   → create → 生(and add 只是附加动作)
register_chrdev_region   → register → 生(legacy)
cdev_add                 → add    → 挂
device_add               → add    → 挂
rtc_register_device      → register → 挂(对象已存在)
kobject_add              → add    → 挂
相关推荐
python百炼成钢1 小时前
53.Linux regmap驱动框架
linux·运维·服务器·驱动开发
python百炼成钢1 小时前
54.Linux IIO驱动框架
linux·运维·服务器·驱动开发
纷飞梦雪1 小时前
ubuntu22开启root
linux·运维·ubuntu
Konwledging1 小时前
linux debug工具集合
linux
星哥说事1 小时前
恶意团伙利用 PHP-FPM 未授权访问漏洞发起大规模攻击
linux·服务器
Evan芙1 小时前
shell编程求10个随机数的最大值与最小值
java·linux·前端·javascript·网络
再睡一夏就好1 小时前
进程调度毫秒之争:详解Linux O(1)调度与进程切换
linux·运维·服务器·c++·算法·哈希算法
BS_Li2 小时前
【Linux系统编程】库制作与原理
linux·运维·服务器