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_dev:struct cdev类型,表示字符设备接口,用于注册到字符设备系统,使用户可以通过/dev/rtcX访问 -
dev:struct 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() 的场景:
-
子系统要求严格的sysfs层次结构(如RTC子系统)
-
需要参与复杂的设备模型事件通知
-
设备有复杂的电源管理依赖关系
可以忽略的场景:
-
简单的字符设备,仅需基本功能
-
驱动完全控制两个对象的生命周期
-
不需要精细的sysfs组织
结论
答案 :传统分开调用不会 自动调用 cdev_set_parent()。但这不意味着传统方式错误,只是:
-
cdev_device_add():现代、简洁、自动管理
-
分开调用:传统、灵活、需手动管理(通常通过结构体嵌入实现)
对于大多数普通字符驱动,不调用 cdev_set_parent() 也完全可行 ,因为在 open() 中通过 container_of() 获取驱动私有数据后,cdev和device的生命周期可以通过驱动逻辑自行保证
核心机制:kobject + kset
Linux 设备模型通过 kobject 和 kset 实现层次化管理,而非单一全局链表:
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/
- 类设备(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 → 挂