阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

带你了解什么是内核总线架构

33次阅读
没有评论

共计 5963 个字符,预计需要花费 15 分钟才能阅读完成。

导读 对于内核总线来说,大家应该会对 Platform 总线比较熟悉,知道在 Platform 总线上会有两个链表,一条链表用来把该总线上的所有设备链接起来,一条链表用来把该总线上的驱动链接起来。

当设备或者驱动添加到链表时,会触发总线的 match 函数。那么,您有没有深入去研究过内核总线呢? 在本文中,我们来深入探讨一下内核中的总线,主要涉及到以下问题:

带你了解什么是内核总线架构

businessman having questions and concrete wall

1. 内核中是如何部署总线的。
2. 设备和驱动是如何挂载到总线上的。
3. 设备和其对应的驱动是如何通过总线进行匹配的。

1. 总线部署

我们从函数 start_kernel 来分析总线的部署,实际上在函数 start_kernel 调用之前,会有汇编代码来处理启动参数,启动模式,创建内核空间页表,准备好堆栈等。由于这些同总线部署关系不大,暂且就认为 start_kernel 就是内核的 main 函数。start_kernel 内部会调用 rest_init,rest_init 函数内部创建内核线程 kernel_init,而 kernel_init 中有如下的函数调用过程:

kernel_init-->do_basic_setup->driver_init—>buses_init 和 platform_bus_init

此处的 buses_init 和 platform_bus_init 就是总线的部署函数,也是本小节的重点,且 buses_init 必须在 platform_bus_init 前面调用。因为 Platform 总线是挂载在 bus 总线下的,接下来我们详细分析下这两个过程。

buses_init

内核中所有的对象如 bus,都是一个 kobject,而把相同类型的 kobject 集合到一起就组成了一个 kset,而函数 buses_init 内部就是通过 bus_kset = kset_create_and_add(“bus”, &bus_uevent_ops, NULL)来注册 bus 总线对应的 kset,其最后 bus_kset 如下图 1 所示:

带你了解什么是内核总线架构

图 1 bus_kset 结构

至此算是准备好了 bus_kset,我们继续往下看一下其他类型的总线是如何和 bus 进行关联的。

platform_bus_init

该函数主要完成两个功能,其函数如下:

struct bus_type platform_bus_type = {
    .name       = "platform",
    .dev_attrs  = platform_dev_attrs,
    .match      = platform_match,
    .uevent     = platform_uevent,
    .pm     = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);
int __init platform_bus_init(void)
{
    int error;

    early_platform_cleanup();

    error = device_register(&platform_bus);
    if (error)
        return error;
    error =  bus_register(&platform_bus_type);
    if (error)
        device_unregister(&platform_bus);
    return error;
}

device_register 是用来注册一个 device,并添加到系统中,最后会在 /sys/devices/ 目录下建立 platform 目录对应的设备对象,其路径是 /sys/devices/platform/。

bus_register 是将 Platform bus 总线注册进系统,其实内部就是创建了对应的 kset 和 kobject 等,且主要完成以下三项工作:

初始化必须的结构体,struct subsys_private 和对应的 kobkect。
同 bus 总线建立关系,kobject.parent 设置为上一步已经创建好的 bus_kset.kobj, kobject.kset 设置为 bus_kset, 把对应的 kobject.ktype 设置为 bus_ktype。
把对应的 kobjet 添加到对应的 kset 的链表中,对总线来说,就是添加到 bus_kset 中的链表中。
下面是 bus_register 函数的实现(删除了创建失败退出时 free 内存等的操作),且我在代码中增加了注释,方便大家查阅:

/**
 * bus_register - register a bus with the system.
 * @bus: bus.
 *
 * Once we have that, we registered the bus with the kobject
 * infrastructure, then register the children subsystems it has:
 * the devices and drivers that belong to the bus.
 */
int bus_register(struct bus_type *bus)
{
    int retval;
    //step: 创建并分配,初始化 struct subsys_private 结构体指针
    struct subsys_private *priv;
    priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    priv->bus = bus;
    bus->p = priv;
    BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);
    retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
    if (retval)
        goto out;

//step2:同上面创建的 bus_kset 进行关联
//kset_register 时,会设置对应 priv->subsys。Kobject->parent = bus_kset.kobj
    priv->subsys.kobj.kset = bus_kset;
    priv->subsys.kobj.ktype = &bus_ktype;
    priv->drivers_autoprobe = 1;
    retval = kset_register(&priv->subsys); // 一会添加到 bus_kset 链表中
    if (retval)
        goto out;
    //step3:创建对应的属性文件
    retval = bus_create_file(bus, &bus_attr_uevent);
    if (retval)
        goto bus_uevent_fail;

    priv->devices_kset = kset_create_and_add("devices", NULL,
                         &priv->subsys.kobj);
    if (!priv->devices_kset) {
        retval = -ENOMEM;
        goto bus_devices_fail;
    }

    priv->drivers_kset = kset_create_and_add("drivers", NULL,
                         &priv->subsys.kobj);
    if (!priv->drivers_kset) {
        retval = -ENOMEM;
        goto bus_drivers_fail;
    }
    //step4: 初始化两个比较重要的链表,后面内容中会提到这两个链表
    klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
    klist_init(&priv->klist_drivers, NULL, NULL);
    
    //step5: 添加探针文件
    retval = add_probe_files(bus);
    if (retval)
        goto bus_probe_files_fail;

    retval = bus_add_attrs(bus);
    if (retval)
        goto bus_attrs_fail;

    pr_debug("bus:'%s': registered\n", bus->name);
    return 0;
    ……
    return retval;
}
EXPORT_SYMBOL_GPL(bus_register);

对于其他总线 (如 IIC 等),也是通过 bus_register 进行注册的,比如 bus_register(&i2c_bus_type) 和 bus_register(&mmc_bus_type)等,其原理同上面一样,在此就不挨个介绍了。

通过上面的分析,我们知道了 bus 总线,且其他总线挂载在 bus 总线下,等总线部署完成后,不同设备会挂载在对应的总线下面。对于 SPI,IIC 等设备,他们都可以挂载在对应的总线下同 CPU 进行数据交互。但在嵌入式系统中,有些设备是不属于这些常见的总线,因此引入了虚拟的 Platform 总线,本小节正是通过虚拟的 Platform 总线来说明总线部署的。

2. 设备和驱动的挂载

我们依然采用 Platform 总线来说明设备和驱动的挂载问题。

设备挂载

对于 Platform 总线来说,可以通过函数 platform_device_register 来挂载 (有的地方称之为注册) 设备,也可以通过设备树来挂载,在内核启动时,会进行设备树的解析,本文中不涉及设备树,主要介绍 platform_device_register 的方式。

该函数原型如下:

int platform_device_register(struct platform_device *pdev)
{device_initialize(&pdev->dev);
    return platform_device_add(pdev);
}

函数在执行的过程中,有如下调用关系:

platform_device_add—-> 设置 struct platform_device 中的总线类型及其他参数 —>device_add—>bus_add_device—->klist_add_tail

这个调用过程省略了一些属性和节点等的处理,我关注的重点在函数 klist_add_tail,该函数是把当前设备添加到 platform_bus 中的一个链表中,这个链表在 Platform 总线部署时就初始化完成了,其初始化函数就是函数 bus_register 中的 step4,可以翻阅上一个小节来查看。

驱动挂载

对于 Platform 总线来说,可以通过函数 platform_driver_register 来挂载 (有的地方称之为注册) 设备,其函数原型如下:

int platform_driver_register(struct platform_driver *drv)
{
    drv->driver.bus = &platform_bus_type;
    if (drv->probe)
        drv->driver.probe = platform_drv_probe;
    if (drv->remove)
        drv->driver.remove = platform_drv_remove;
    if (drv->shutdown)
        drv->driver.shutdown = platform_drv_shutdown;

    return driver_register(&drv->driver);
}
EXPORT_SYMBOL_GPL(platform_driver_register);

函数在执行的过程中,有如下调用关系:

driver_register—> 设置对应的参数等 —->driver_find—> bus_add_driver—-> klist_add_tail

相对于设备挂载,多了一个函数 driver_find 的调用,该函数主要目的就是判断驱动是否已经挂载上了,其余处理方式同设备挂载相同。最为重要的依然是 klist_add_tail,把该驱动添加到了 platform_bus 中的一个链表中。

其他类型的总线设备和驱动相同,也会存在两个链表,设备和驱动均挂载到相应的链表中。

3. 设备和驱动的匹配

从第 2 小节中,我们知道 Platform 总线下有两个链表,我采用下面的图来具体化这两个链表,图左边的设备链表,图中仅呈现 3 个设备,实际上会有很多,图右边为驱动链表。不管是左边的设备还是右边的驱动,均有 name 字段(通常情况下是 compatible),这是个非常重要的字段,后面我们会用到它。

带你了解什么是内核总线架构

图 2Platform 总线的两个链表

针对匹配问题,我依然采用 Platform 总线来阐述,我们已经知道在进行驱动挂载时,会调用函数 bus_add_driver,该函数内核实际上还会调用一个函数 driver_attach(针对设置 drivers_autoprobe 的情况),下面是函数 driver_attach 的调用情况:

driver_attach
--->bus_for_each_dev(drv->bus, NULL, drv, __driver_attach)
---> klist_iter_init_node(&bus->p->klist_devices, &i, (start ? &start->p->knode_bus : NULL));
   while ((dev = next_device(&i)) && !error)
      error = fn(dev, data);
   --->driver_match_device
     ---> drv->bus->match
       ---> platform_match

从上面代码过程可以看出,当挂载驱动时,会遍历图 2 中左边的链表,最后调用 Platform 总线的 match 函数 platform_match (match 函数是在 struct bus_type platform_bus_type 中设置的,在总线部署时阶段调用 platform_bus_init 就设置好了)来进行设备和驱动的匹配。每个总线都会有自己的 match 函数,且 match 函数里面会通过多种方式匹配,如常见的 compitable,name 或者 id_table,只要有一个能匹配上,则认为驱动和设备匹配成功。

总结

本文主要采用 Platform 来说明了内核中总线部署,设备和驱动挂载,及设备和驱动的匹配问题,实际上其他总线也是采用相同的方式,在我的描述过程中,重点在于总线,忽略了一些 sysfs 节点,引用计数,kobject,kset 等,但这些在内核架构中也是比较重要的环节,希望大家在了解总线架构后,也能有时间去深入查看内核总线的各个处理细节。

特别说明:不同的内核,可能使用到的函数,或者函数的实现同文章中介绍的存在出入,但其原理及架构相同,可以作为参考。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-25发表,共计5963字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中