当前位置: 首页 > 科技观察

你需要知道的内核总线架构

时间:2023-03-15 14:43:38 科技观察

当一个设备或驱动程序被添加到链表中时,就会触发总线的匹配函数。那么,你深入研究过内核总线吗?在本文中,我们将深入讨论内核中的总线,主要涉及以下几个问题:如何在内核中部署总线。设备和驱动程序如何安装在总线上。设备及其对应的驱动程序如何通过总线进行匹配。1.总线部署我们从函数start_kernel来分析总线部署。其实在调用start_kernel函数之前,会有汇编代码处理启动参数、启动模式、创建内核空间页表、准备栈等,由于这些和总线部署关系不大,所以暂且start_kernel被认为是内核的主要功能。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是总线的部署函数,而也是本节的重点,buss_init必须在platform_bus_init之前调用。因为Platform总线是挂载在bus总线下面的,所以我们来详细分析一下这两个过程。buss_init内核中的所有对象,比如bus,都是kobject,相同类型的kobject组合起来形成一个kset,函数内部使用bus_kset=kset_create_and_add("bus",&bus_uevent_ops,NULL)来注册busbuss_init总线对应的kset,最后的bus_kset如下图1所示:图1bus_kset结构体为bus_kset准备好了,我们继续看其他类型的总线是如何关联到总线上的。platform_bus_init这个函数主要完成两个功能,其作用如下:structbus_typeplatform_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__initplatform_bus_init(void){int错误;early_platform_cleanup();error=device_register(&platform_bus);如果(错误)返回错误;error=bus_register(&platform_bus_type);device_register用于注册设备并将其添加到系统中。最后会在/sys/devices/目录下创建一个对应平台目录的设备对象,其路径为/sys/devices/platform/。bus_register是将Platform总线注册到系统中。实际上,内部创建了相应的kset和kobject,主要完成以下三个工作:初始化必要的结构体,structsubsys_private和相应的kobkect。建立与总线的关系,设置kobject.parent为上一步创建的bus_kset.kobj,kobject.kset设置为bus_kset,对应的kobject.ktype设置为bus_ktype。将对应的kobjet添加到对应kset的链表中。对于总线,它被添加到bus_kset中的链表中。下面是bus_register函数的实现(创建失败退出时删除freememory的操作),为了方便大家,我在代码中加了注释:/***bus_register-向系统注册一个总线。*@bus:公共汽车。**一旦我们有了它,我们就用kobject*基础设施注册总线,然后注册它拥有的子子系统:*属于总线的设备和驱动程序。*/intbus_register(structbus_type*bus){intretval;//步骤:创建并分配,初始化structsubsys_private结构体指针structsubsys_private*priv;priv=kzalloc(sizeof(structsubsys_private),GFP_KERNEL);如果(!priv)返回-ENOMEM;私有->总线=总线;总线->p=priv;BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);retval=kobject_set_name(&priv->subsys.kobj,"%s",bus->name);if(retval)gotoout;//step2:同上当创建的bus_kset与//kset_register关联时,会设置对应的priv->subsys。kobject->parent=bus_kset.kobjpriv->subsys.kobj.kset=bus_kset;priv->subsys.kobj.ktype=&bus_ktype;priv->drivers_autoprobe=1;retval=kset_register(&priv->subsys);//如果(retval)gotoout,一个将被添加到bus_kset列表中;//step3:创建对应的属性文件retval=bus_create_file(bus,&bus_attr_uevent);如果(retval)转到bus_uevent_fail;priv->devices_kset=kset_create_and_add("设备",NULL,&priv->subsys.kobj);如果(!priv->devices_kset){retval=-ENOMEM;转到bus_devices_fail;}priv->drivers_kset=kset_create_and_add("drivers",NULL,&priv->subsys.kobj);if(!priv->drivers_kset){retval=-ENOMEM;转到bus_drivers_fail;}//step4:初始化两个重要的链表,在后面的内容中会提到klist_init(&priv->klist_drivers,空,空);//step5:添加探测文件retval=add_probe_files(bus);如果(retval)转到bus_probe_files_fail;retval=bus_add_attrs(总线);如果(retval)转到bus_attrs_fail;pr_debug("bus:'%s':registered\n",bus->name);返回0;...返回retval;}EXPORT_SYMBOL_GPL(bus_register);对于其他总线(如IIC等),也是通过bus_register进行注册的,如bus_register(&i2c_bus_type)和bus_register(&mmc_bus_type)等,原理同上,就不一一介绍了在这里。通过上面的分析,我们知道了总线总线,总线总线下挂载了其他总线。总线部署完成后,相应的总线下方会挂载不同的设备。对于SPI、IIC等设备,都可以挂载在相应的总线上,与CPU进行数据交互。但是在嵌入式系统中,有些设备并不属于这些公共总线,因此引入了虚拟Platform总线。本节使用虚拟平台总线来说明总线部署。2.设备和驱动的挂载我们还是用Platform总线来说明设备和驱动的挂载。设备挂载对于Platform总线,可以通过函数platform_device_register(有的地方叫注册)挂载设备,也可以通过设备树挂载。内核启动时,会分析设备树。本文不涉及设备树,主要介绍platform_device_register的方式。函数原型如下:intplatform_device_register(structplatform_device*pdev){device_initialize(&pdev->dev);returnplatform_device_add(pdev);}函数执行过程中有如下调用关系:platform_device_add---->setstructplatform_device中的总线类型等参数--->device_add--->bus_add_device---->klist_add_tail这个调用过程省略了一些属性和节点的处理。我的重点是函数klist_add_tail,就是把当前的设备添加到platform_bus中的一个链表中。这个链表在部署平台总线时被初始化。它的初始化函数是函数bus_register中的step4。可以参考上一节查看。驱动挂载对于平台总线,设备可以通过函数platform_driver_register挂载(有时称为注册)。函数原型如下:intplatform_driver_register(structplatform_driver*drv){drv->driver.bus=&platform_bus_type;如果(drv->probe)drv->driver.probe=platform_drv_probe;如果(drv->remove)drv->driver.remove=platform_drv_remove;如果(drv->shutdown)drv->driver.shutdown=platform_drv_shutdown;returndriver_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个设备。其实会有很多,图中右侧是驱动器列表。不管是左边的device还是右边的driver,都有一个name字段(一般是compatible),这是一个很重要的字段,我们后面会用到。图2Platform总线的两个链表对于匹配问题,我还是用Platform总线来说明。我们已经知道在挂载驱动的时候会调用函数bus_add_driver,而函数kernel实际上会调用一个函数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总线的匹配函数platform_match(匹配函数在structbus_typeplatform_bus_type中设置,部署在总线上的时间阶段调用platform_bus_init来设置)匹配设备和驱动。每条总线都会有自己的匹配函数,匹配函数会通过多种方式进行匹配,比如常见的compitable、name或者id_table,只要能匹配到一个就认为驱动和设备匹配成功。小结本文主要通过Platform来讲解内核中的总线部署,设备和驱动的挂载,以及设备和驱动的匹配。其实其他公交车也是用同样的方法。在我的描述过程中,重点是总线,忽略了一些sysfs节点、引用计数、kobject、kset等,但这些也是内核架构中比较重要的环节。希望大家在了解了总线架构之后,也能有时间深入了解内核总线的各种处理细节。特别说明:不同的内核,可能用到的功能,或者功能的实现与文中介绍的有所不同,但是它们的原理和架构是相同的,可以作为参考。作者介绍社区编辑赵庆尧,从事驱动开发多年。他的研究兴趣包括安全操作系统和网络安全,并发表了与网络相关的专利。