Linux-驱动架构简析

45次阅读

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

这篇文章不是驱动开发教程,只不过作者读过内核源码后,想对知识做一个梳理,从源码的角度分析一下 Linux 的驱动架构的实现。行文也不大讲究,希望可以把问题说清楚。本文使用的 kernel 源码版本是 3.13.3。

学习 kernel 虽然没有捷径,但是有合理的方法。

1) 首先,需要熟悉操作系统的设计与实现,推荐大家看 MINIX 作者的那部书,同时把 MINIX 的 kernel 代码研读一下。不然,你不知道操作系统都有哪些模块,不知道操作系统要做些什么事情,提供什么功能。简单地说,操作系统首先要驱动 CPU,然后提供那几大管理(进程,内存,文件),实现一两百个系统呼叫,提供驱动接口,用户态与内核之间进行切换。

2) 去 intel 的官网,找一下‘Intel® 64 and IA-32 Architectures Software Developer’s Manual’,了解一下 CPU 的架构,工作模式,底层编码。否则,你不知道 gdt,ldt,page table, 实地址,保护模式,定时器中断都是什么东西,为什么操作系统要这样来设置寄存器。这块基本上全汇编语言,对 CPU 的初始化,寄存器设置,手册上面都有严格的时需要求。哪些操作需要屏蔽中断,哪些需要在一个指令周期完成等等。有了上面的基础后,大概知道一个操作系统大概要做些什么事情,如何驱动底层的 CPU,这个时候阅读 linux 的 kernel 代码,事半功倍。

kernel 分为两个模块:一个是 core: cpu,中断,进程,内存几大管理,提供系统呼叫。另一个是 driver。linux 的 driver 都是有架构的,不需要从底层做起。各类架构称为‘子系统’:如,block 子系统,net 子系统,usb 子系统等。别看操作系统的代码量大,其实,driver 占了估计 80% 的代码量,这些都是不需要去看的。驱动是否放在内核,就是微内核与宏内核的区别。

阅读源码过程中,观其大略即可,主要了解整个结构,以及程序的流程。如:系统呼叫的调用,追一个就可以了—— 看看操作系统如何捕捉软中断,根据中断号,dispatch 到相应的服务程序,如何保存现场,完成后,又如何回到用户态。系统呼叫调用,核心就是 dispatch 的流程。追完一支系统呼叫,其它的大概就知道怎么回事了。driver 也就一样的,找个简单的驱动看看,从驱动层一直到驱动的架构,流程清楚就可以了。如字符设备驱动,追一下注册后,驱动框架如何把该设备放入 list,当有用户请求的时候,它又如何查找到相应的设备,调用相应的操作函数,一路追下来,流程大概就清楚了。

不建议一开始就阅读“linux 内核源码分析”之类的书,会让读者一头雾水。正确的方法应该是,先了解相应的背景知识后,再来阅读源码。Driver 框架的源码的位于 drivers/base/,它是整个驱动模式的基础框架,相当于 OO 语言的里面的 Object 对象。其实,Linux 的驱动框架就是一个 OO 的结构,core 模块定义数据结构,函数接口,实现各种通用的功能——相当于 OO 里面的基类。各模块的设备驱动程序则只需要实现 core 模块里面定义的接口即可。

bus, driver, device 框架

linux 的外围设备驱动,都是通过 bus + driver + device 来管理的,其实也好理解,外设都是通过总线来与 cpu 通讯的。kernel 会实现各种总线的规范以及设备管理(设备检测,驱动绑定等),驱动程序只需要注册自己的驱动,实现对设备的读写控制即可。

这类驱动通常是 2 个层次:总线子系统 + 驱动模块,它的流程大概是:

1) bus_register(xx)

kernel 里面的各 bus 子系统(如:serio, usb, pci, …)会使用该函数来注册自己。

2) driver_register(xx)

驱动模块使用它来向总线系统注册自己,这样驱动模块只需要关注相应 driver 接口的实现。通常,bus 子系统会对 driver_register 来进行封装,如:

  • serio 提供 serio_register_driver()
  • usb 提供 usb_register_driver()
  • pci 提供 pci_register_driver()

3) registe_device(xx)

各总线除了管理 driver 外,还管理 device,通常会提供一支 API 来添加设备,如:input_register_device, serio_add_port. 实现上都是通过一个链表对设备进行管理,通常是在初始化或者 probe 的时候,添加设备。

设备 (device) 指的是具体实现总线协议的物理设备,如对 serio 总线而言,i8042 就是它的一个设备,而该总线连接的设备 (鼠标,键盘) 则是一个 serio driver。

注册

bus.c 和 driver.c 分别对 bus,driver 和 device 进行管理,提供注册 bus, driver 和查找 device 功能。

bus_register(*bus) 这个函数会生成两个 list,用来保存设备和驱动。

INIT_LIST_HEAD(&priv->interfaces);
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
klist_init(&priv->klist_drivers, NULL, NULL);

* priv 是 struct subsys_private 定义在 driver/base/base.h

driver_register(*drv) 实际上就是调用 bus_add_driver(*drv) 把 drv 添加到 klist_drivers:

klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);

同理注册 device,也是通过 bus_add_device(*dev),添加到 klist_devices:

klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);

以 hid_bus_type 为例,执行 bus_register(&hid_bus_type) 后,hid_bus_type->p->klist_devices 和 hid_bus_type->p->klist_klist_drivers 这两个 list 会被初始化,为后面的 driver 和 device 注册做准备,driver 数据结构如下:

       static struct hid_driver tpkbd_driver = {
        .name = "lenovo_tpkbd",
        .id_table = tpkbd_devices,
        .input_mapping = tpkbd_input_mapping,
        .probe = tpkbd_probe,
        .remove = tpkbd_remove,
    };

注册 driver 时, 它先经过 __hid_register_driver(&tpkbd_driver), 设置一些基本参数。

hdrv->driver.bus = &hid_bus_type;
.....    
driver_register(&hdrv->driver);

设置 ’driver.bus’ 字段后,driver 和 bus 的对应关系就建立起来了。然后,经过 driver_register 后,hid_bus_type->p->list_drivers 保存了 tpkbd_driver.

Q: driver 模块是不知道 hid_driver 这个数据结构的,它如何能把它的指针放到 list 里面呢?

答案是”不能”,list_drivers 是不能保存 hid_driver 指针的。driver 模块提供了一个接口: ‘struct device_driver’ , hid_driver 这个结构里面需要包含该结构。

      struct hid_driver {
              const struct hid_device_id *id_table;
                /* private: */  
                 struct device_driver driver;
      }

注册的时候,取的是 driver 字段的地址,也就是 hid_driver.driver 的指针,driver_register(&hdrv->driver); 当从 driver 模块 callback 到 hid-core 模块的时候,如

 static int hid_bus_match(struct device *dev, struct device_driver *drv)
 {struct hid_driver *hdrv = container_of(drv, struct hid_driver, driver);
         struct hid_device *hdev = container_of(dev, struct hid_device, dev);

         return hid_match_device(hdev, hdrv) != NULL;
 }

使用 container_of 就把 hid_driver.driver 的指针转换成了 hid_driver 的指针--这个方法类似 OO 编程里面使用基类指针指向派生类对象。Linux 普通使用这个方法,来构建框架。

device 和 driver 绑定

当增加新 device 的时候,bus 会轮循它的驱动列表来找到一个匹配的驱动,它们是通过 device id 和 driver 的 id_table 来进行”匹配”的,主要是在 driver_match_device()[drivers/base/base.h] 通过 bus->match() 这个 callback 来让驱动判断是否支持该设备,一旦匹配成功,device 的 driver 字段会被设置成相应的 driver 指针 :

really_probe()
{
    dev->driver = drv;
    if (dev->bus->probe) {ret = dev->bus->probe(dev);
        ...
    } else if (drv->probe) {ret = drv->probe(dev);
        ...
    }
}

然后 callback 该 driver 的 probe 或者 connect 函数,进行一些初始化操作。

同理,当增加新的 driver 时,bus 也会执行相同的动作,为驱动查找设备。因此,绑定发生在两个阶段:

1: 驱动找设备,发生在 driver 向 bus 系统注册自己时候,函数调用链是:

driver_register –> bus_add_driver –> driver_attach() [dd.c] -- 将轮循 device 链表,查找匹配的 device。

2: 设备查找驱动,发生在设备增加到总线的的时候,函数调用链是:

device_add –> bus_probe_device –> device_initial_probe –> device_attach -- 将轮循 driver 链表,查找匹配的 driver。

匹配成功后,系统继续调用 driver_probe_device() 来 callback ‘drv->probe(dev)’ 或者 ‘bus->probe(dev) –>drv->connect(),在 probe 或者 connect 函数里面,驱动开始实际的初始化操作。因此,probe() 或者 connect() 是真正的驱动 ’ 入口 ’。

对驱动开发者而言,最基本是两个步骤:

  • 定义 device id table.
  • probe()或 connect()开始具体的初始化工作。

(driver 和 device 注册流程图)

实例分析:atkbd 键盘驱动

Serio Bus 主要是支持 PS/2,串口等串行设备协议,物理上可以通过 i8042 控制芯片来连接 PS/ 2 的鼠标或键盘,它的架构是:

  • serio.c 实现总线框架。
  • serio_register_port 注册底层读写设备-- port 就是 serio 的底层通讯设备,它执行 serio 总线的底层读写。
  • serio_register_driver 注册驱动,与 port 进行绑定,利用 port 进行底层的读写通讯。

atkbd 驱动注册的时侯,需指定它支持的 port 类型。

serio->id.type        = SERIO_8042; // 表明驱动需要 8042 的支持。serio_register_driver()     // 注册自己,根据设备 id 绑定相应的 port(本例中是 8042)。

作为 serio 的 port,i8042 通过 serio_register_port 来注册,生成 serio 对象,这样驱动程序就可以通过 serio->wirte/read 来调用 i8042 进行底层的通讯。数据流程框图如下:

driver 通过 bus 匹配 port,通过 port 与外设通讯。

我们可以在 sys 接口 [/sys/bus/serio/] 目录下找到设备和驱动的相关信息,里面的内容可以通过 DEVICE_ATTR_XX 系列宏定义来添加。port 的命令规则是 serio0, serio1, serioN 是自动增加的。

dev_set_name(&serio->dev, "serio%ld", (long)atomic_inc_return(&serio_no) - 1);

atkbd 注册 serio 驱动后,还需要注册 input 设备,它需要实现 input 子系统的接口,作为一个 input 设备工作。input.c 定义了 callback 和公用接口, 子模块实现相应接口。

keybord:  drivers/input/keyboard/atkbd.c

注册设备
atkbd.c: atkbd_connect  → input_register_device()

Input 子系统通过 event 字符设备来与应用程序进行通讯。

evdev.c: evdev_init → input_register_handler → ....
cdev_init(xx, fops) // 处理  /dev/input/eventX 的读写操作。

atkdb 通过注册 Serio Bus 驱动和 Input device 来打通从应用程序到外设通讯链路:

application –> /dev/input/eventX –> Input 子系统 –> atkdb 驱动 –> serio bus –> i8042 port –> 物理键盘。

我们可以看到字符设备主要是用来提供应用层接口,Bus 框架则用来管理外设驱动。

Input 提供了 proc 文件接口,可以查看相应的信息。

cat /proc/bus/input/devices:可以得到某个设备的 event number.
cat /proc/bus/input/handlers

通过 cat eventX 可以得到按键产生 input_event,查看 event 里面的原始数据

sudo cat /dev/input/eventXX | hexdump
XX: event number.

参考资料

1: Linux 3.13 源码。

2: Linux Device Drivers。


欢迎大家来我的网站交流: 般若程序蝉

正文完
 0