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    → 挂
相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式