Linux内核设备模型与总线
- 内核版本 linux Kernel 2.6.34, 与 Robert.Love的《Linux Kernel Development》(第三版)所讲述的内核版本一样
- 源代码下载路径: https://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2
1. Linux内核设备模型分析
1) kobject对象的设计思想
- 对于没有接触过Java、Python等高级面向对象编程语言工程师,第一次看到struct kobject对象,都会对它的作用感到困惑,不知道为什么这么多Linux内核对象结构体中,都要有一个看起来没什么用的struct kobject。
- 对于接触过JAVA、python等面向对象编程语言的工程师,对object对象肯定不陌生。在JAVA、Python中,object对象是所有对象的基类,所有的对象最终都会继承到object对象。关于JAVA中object对象及其相关方法的描述,可以参阅JDK在线文档。
- 在Linux内核设备模型中,也是借鉴了JAVA、Python中的做法,让struct kobject对象作为所有内核设备的基类,kobject是一个最高层次的抽象基类,这样Linux内核才能够通过一个继承树体系,管理到系统里的每一个设备。
- 在Linux内核中,虽然kobject对象可以作为所有设备对象的基类,但是类比于JAVA、Python,我们一般不直接使用kobject这种最高抽象层次的基类作为实际需要开发的设备程序的的直接基类,原因见图1。kobject类比于物质在自然界继承体系里的概念,物质是一个抽象的概念,所有的生物和非生物都继承自物质,即他们和物质都是IS-A的关系。狗 IS-A 物质,水IS-A 物质。但是我们真正研究狗的时候,一般是从其犬科动物或者动物等比较具体的基类开始研究,研究它动物属性,研究它发出叫声的特性,很少研究狗的物质属性(除非是唯物主义哲学家),但是狗确实是物质。
- 同理,在研究一般具体的Linux设备驱动,如视频设备V4L2驱动的代码,一般都从其上层基类struct video_device研究起,或者再抽血一些,研究struct video_device 的基类struct cdev, 很少直接使用最上层的kobject基类。但是V4L2设备驱动,确实 IS-A kobject。
Figure 1 自然界继承体系,所有对象都继承自物质
2) kobject对象的特性
- kobjet对象的声明在与相关的API在include/linux/kobject.h文件中。
- kobject 对象有对象引用计数(kref),父子object索引(parent)等基本属性。
- kobject提供了sysfs文件系统相关的节点描述,属性与函数,使得Linux系统可以通过特殊的sysfs文件系统,以树形继承的关系来访问Linux内核中的每个具体的kobject对象。实际上,kobject最初的设计目的就是为了在sysfs中模仿windows的设备管理器,提供一个类似树形的,可以管理额访问系统所有设备的接口。
- kobject对象还提供了Linux系统设备中hotplug热插拔相关的事件与函数方法,使得内核设备可以支持热插拔机制。
- 每个继承了object的Linux设备对象,在调用者获得kobject基类对象实例之后,可以通过container_of()函数,一层一层转换,最终获得具体子类对象。有了kobject,就可以实现通过基类来访问子类对象的机制。
3) Linux内核中继承kobject的主要基础类设备模型
- Kobject类似于JAVA中的object类,一般不作为内核设备对象的直接基类。但是类似于JDK中有不少对象直接继承自object对象,JDK中直接继承自object的对象,一般都是最为基础类对象,提供给开发者使用。同样,Linux内核中也有不少继承自kobject的基础类对象,Linux内核驱动开发者可以使用这些直接继承自kobject的基础类设备,来构建实际的Linux设备驱动。
- kset是一个集合容器,用于管理一类object子类对象的集合,继承自某个基类kobject的所有基类的kobject对象都可以用一个kset容器来管理。
- Linux内核在继承自kobject的重要基础类对象如图2所示。
Figure 2 Linux内核中直接继承kobject的重要基础类对象
- device类对象一般用于Linux各种总线设备(platform虚拟平台总线、USB总线等)的基类,下一节详细介绍。
- module对象是用于模块管理接口(就是上一篇文章中单继承与接口一节的接口),接口本身也是一个对象(JAVA中的interface也是对象,继承object),实现了module接口的对象,可以通过模块的方式,动态装载、卸载代码块。
- class对象是用于设备分类管理的相关接口,通过class接口可以将内核中各种设备类型的信息导出到用户态。
- cdev对象就是典型的字符设备基类对象,所有的字符设备最终都会继承到cdev对象。cdev对象同时制定了字符设备标准的访问接口函数方法。
- 总之,拥有了以上这些基础类,内核开发者就能开发自己特殊的设备驱动,并且通过这些类和接口,将驱动程序集成到Linux内核中。
3). Linux内核中是怎么管理维护这些继承类对象
- 之前的内容讲了一堆面向对象的概念,描述了一堆与Linux设备驱动有关的对象之间的关系,可Linux内核毕竟是C语言写的,内核中如何维护这些基础类对象呢?
- 在Linux内核drivers/base/ 目录下,有很多重要的代码,目录命名为base/,可见基础类对象这个 名词还是有来源依据的,Linux设备驱动里的对象基本都是继承自drivers/base/里面的对象。
- drivers/base/base.h中声明了base的一些私有对象属性,以及API,其中很多API如devices_init(),buses_init(),classes_init(),platform_bus_init()等初始化函数都会在Linux内核init函数的driver_init()中被调用,因此可见,Linux内核在启动时通过base.h中的这些初始化*_init()函数使得Linux内核中整个驱动系统相关的基础类组件对象都能进入工作状态。
- 查看devices_init()函数实现代码如下,我们发现实际上,内核在初始化devices的时候,使用kobject对象创建了dev_kobj作为所有device子类对象的基类。并且创建了相应的devices_kset来管理这些子类。
- int __init devices_init(void)
- {
- devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
- if (!devices_kset)
- return -ENOMEM;
- dev_kobj = kobject_create_and_add("dev", NULL);
- if (!dev_kobj)
- goto dev_kobj_err;
- sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);
- if (!sysfs_dev_block_kobj)
- goto block_kobj_err;
- sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);
- if (!sysfs_dev_char_kobj)
- goto char_kobj_err;
- return 0;
- char_kobj_err:
- kobject_put(sysfs_dev_block_kobj);
- block_kobj_err:
- kobject_put(dev_kobj);
- dev_kobj_err:
- kset_unregister(devices_kset);
- return -ENOMEM;
- }
- 最后,我们实现device的子类对象,并通过device_register()函数将其注册的时候(Linux内核还有很多类似的register函数,例如register_chrdev_region,注册函数意图大同小异,都是让基类能够获取注册后的子类对象),用户就可以通过基类访问注册后的子类对象了。
- 在device_register()函数中,我们看到注册的device子类会和基类的kset容器以及关联起来,最终系统可以通过基类device对象实例dev_kobj所关联的kset容器来访问deivce的子类对象(一般会通过container_of()函数获取子类对象)。
- 实际上,Linux内核中,cdev,device等基础类对象,都会在初始化时init()相关的全局变量,Linux内核需要维护这些全局变量以及相关的容器(kset、数组、list都可以看成容器)。
- 由此可见,面向对象思想里面,继承,多态,虚函数的实现并不神秘,还是通过精巧的设计,用全部变量加容易的方法来管理这些关系。由于有这些由Linux内核维护的全局变量和相关容器,所以在开发设备驱动模块子类时,需要通过注册函数(register)才能让基类能够关联到子类的对象。当有了面向对象的观点,我们可以在更高层次理解Linux内核这些对象的关系,从而设计并改进我们的系统。
2. Linux内核总线、设备与驱动
1). 计算机系统总线模型
- Linux内核总线的思想来源于如图3所示的计算机系统总线模型。
- 计算机系统总线控制着外部设备与计算机CPU的通信,任意CPU N都可以通过总线访问到任意外部设备。
- 一般情况下,外部设备数量都会大于CPU的数量,有了总线,无需为每个外部设备都配备一个CPU。只有外部设备需要CPU来访问读取处理数据,发送控制信号时,一个空闲的CPU才会通过总线控制器与某个外部设备建立通信连接。
- 一旦CPU处理完某个外部设备的数据之后,CPU可以通过总线控制器,断开和某个外部设备的连接,去处理其它外部设备的访问需求。
- 总之,计算机系统中的总线模型为数量较多的外部设备提供了一种共享数量较少的CPU的控制访问机制。
Figure 3 计算机系统中的总线
2). Linux内核中的与总线
- Linux内核之后有各种各样的软件总线,系统中所支持的所有总线型驱动设备在/sys/bus/ 目录下可以看到。主要的总线型设备驱动有USB、platform、I2C、SPI、SCSI、mmc等。
- Linux内核中所有总线接口的的基类以及相关的API都是在include/linux/device.h中声明。其中一个总线接口包括三个核心的基类对象:struct bus_type、struct device 与struct device_driver。这些基类对象与Linux内核中实际的USB、platform、I2C总线接口的继承关系如图4所示。
Figure 4 Linux内核中,各种总线设备与总线接口基类的对应关系
- Linux 内核总线驱动实际上是模仿计算机系统总线的机制,在一个具体类型的总线上(例如I2C、USB、platform)多数的device设备共享少数的device_driver提供了一种管理机制。
- 通过总线,将一个设备驱动中,逻辑功能部分(device_driver)与硬件具体资源bsp相关的部分(device)分隔开来,使得同一种类型的多数设备实例(device),能够共享同一个驱动程序逻辑代码(device_driver)。
- struct bus_type 对象与struct device 对象、structdevice_driver对象构成了一个设备实例化管理接口,对象之间的行为模式如图5所示,类似于抽象共产,将每个device与device_driver装配起来,构造出真正的设备实例。
- struct bus_type对象在 match()函数方法中,通过对比新发现的device 与 device_driver 的 Id(Id可以是name也可以是dts的描述节点或者实际总线自己定义的匹配Id号都可以匹配),为新加入的device设备找到合适的Id匹配的device_driver,然后调用device_driver的probe()函数方法,进行构造实例化,最终产生实例化的设备驱动并且作为节点挂载在/dev目录下。
- struct bus_type对象的 remove()函数则处理设备卸载析构的行为
- bus_type对象与device对象、device_driver对象除了用于实例化的函数方法,还有suspend()、resume()、shutdown()等函数方法,用于实现设备的休眠、唤醒等电源管理相关的功能。
- struct bus_type对象的uevent()函数方法提供了热插拔事件的相关通信机制,通过该函数接口,可以给用户态发送热插拔相关的异步事件。
Figure 5 bus_type 与 device、device_driver对象的行为模式
3).platform总线与设备简介
- 在Linux 内核中,USB、I2C、SPI等总线都是实际存在的总线,都有对应的相关的外部硬件电路以及相关的标准化通信协议最终以电信号为载体与实际的I2C、USB等实际硬件设备通信。
- USB、I2C等总线设备,可以通过真正的硬件热插拔,从而触发具体的bus_type总线进行driver与device匹配,最终在/dev/*目录下构造实例化设备驱动。
- 而struct platform_bus_type对象在Linux内核中代表一个虚拟的平台设备总线。即在系统硬件中,不存在与platform_bus_type对应的硬件电路,在SOC中也不存在对应的总线控制器(USB、I2C等模组在SOC芯片中都有相关的硬件控制器)。
- 虽然struct platform_bus_type不存在真正的总线,但是我们在处理各种杂七杂八的驱动时(比如LED、智能卡、杂项设备、framebuffer等),也有把device_driver的驱动实现逻辑代码和device硬件bsp相关的代码分离出来的需求, 这样使得同样类型但是占用不同端口或者资源(比如 LED1、 LED2都是LED设备,但是一般会占用不同的GPIO口)的device能够共享同一份device_driver的逻辑代码,不需要为每一个LED设备都写一份驱动(维护量无比巨大)。
- 因而Linux内核采用 struct platform_bus_type、structplatform_device 与struct platform_driver三个对象继承了总线设备相关的基类对象,模仿系统总线的行为模式,通过struct platform_bus_type来管理 structplatform_driver与struct platform_device的设备匹配与设备构造实例化。
- 虽然platform虚拟平台总线不像usb、I2C等总线接口有真正的硬件设备插拔事件。但是struct platform_driver与struct platform_device对象都实现了module接口,可以编译成module进行insmod/rmmod等动态装载于卸载。那么struct platform_driver与struct platform_device对象在在作为module动态地装载与卸载时,相当于模拟了总线的热插拔事件,那么可以通过insmod/rmmod模拟总线设备的热插拔,来触发struct platform_bus_type对象进行driver与device匹配,并在/dev/*目录下构造实例化真正的设备驱动。