Linux 设备管理(linux网络协议栈笔记)

来源:互联网 发布:淘宝购物如何付款 编辑:程序博客网 时间:2024/06/11 14:02

Linux 设备管理

设备初始化是我们要分析的第三和第四个大步骤,这个部分要涉及到一些设备驱动的背景知识。

设备管理的目标是能对所有的外设进行良好的读、写、控制等操作。但是如果众多设备没有一个统 一的接口,则不利于开发人员的工作。因此 Linux 采用了类似 UNIX 的方法,使用设备文件来实现这个 统一接口。由此可见,设备文件的相关概念是设备管理的最基础部分。
要让操作系统感知到设备的存在,必须提供一个注册机制,使操作系统能识别设备对应的驱动程序, 目前采用基于主、次设备号的方式来管理设备。Linux 习惯上将设备文件放在目录/dev 或其子目录之下, 设备文件名通常由两部分组成,第一部分通常较短,可能只有 2 或 3 个字母组成,例如普通硬盘如 IDE 接口的为“hd”,SCSI 硬盘为“sd”,软盘为“fd”,并口为“lp”,第二部分通常为数字或字母,用来区 别设备实例。例如/dev/hda/dev/hdb/dev/hdc 表示第一、二、三块硬盘;而/dev/hda1/dev/hda2/dev/hda3表示第一硬盘的第一、第二、第三分区。

这种机 制就 是 2.4.的版本中的注册与管理方式——devfs 管理方式,如果在编译内核时选中 CONFIG_DEVFS_FS,那么就可以利用这种设备管理方式。devfs 挂载于/dev 目录下,提供了一种类似于 文件的方法来管理位于/dev 目录下的所有设备,我们知道/dev 目录下的每一个文件都对应的是一个设备, 至于当前该设备存在与否先且不论,而且这些特殊文件是位于根文件系统上的,在制作文件系统的时候 我们就已经建立了这些设备文件,因此通过操作这些特殊文件,可以实现与内核进行交互。但是 devfs 文件系统有一些缺点,例如:不确定的设备映射,有时一个设备映射的设备文件可能不同,例如我的 U 盘可能对应 sda 有可能对应 sdb;没有足够的主/辅设备号,当设备过多的时候,显然这会成为一个问题; /dev 目录下文件太多而且不能表示当前系统上的实际设备;命名不够灵活,不能任意指定,容易造成设 备之间冲突而不能正常初始化驱动。 本人在构思这本书的时候 devfs 管理方式还在大行其道,但是时隔 3 年,内核源代码中已经不采用这 种技术,转而采用 sysfs 技术。引入了一个新的文件系统 sysfs,它挂载于/sys 目录下,跟 devfs 一样它也 是一个虚拟文件系统,也是用来对系统的设备进行管理的,它把实际连接到系统上的设备和总线组织成一个分级的文件,用户空间的程序同样可以利用这些信息以实现和内核的交互,该文件系统是当前系统 上实际设备树的一个直观反应,sysfs 的工作就是把系统的硬件配置视图导出给用户空间的进程。在 2.6.18 内核中,必须选中 General SetupConfigure Standard Kernel features(For small systems),在 File SystemPseudo filesystems 菜单内部出现“sysfs file system support”。

不管如何进步,其中心思想是建立一种分层的体制,块设备是一种 class,字符设备是一种 class,而 网络设备也是一种 class,驱动程序开发者如果知道自己的设备属于哪一种 class,那么就把其驱动程序挂 到相应的 class 上,让内核为驱动程序分配名字和设备号,如果不确定是哪种 class,还可以自己建立 class 类别。Linux 用户可以到/sys 下观察一下系统中有哪些内核模块及驱动,然后和/dev 下的文件做一个对比 就发现,/sys 目录确实将各种设备进行了归类,条理清晰的多。用户空间的工具 udev 就是利用了 sysfs 提供的信息来实现所有 devfs 的功能的,但不同的是udev 运行在用户空间中,而 devfs 却运行在内核空 间,而且udev不存在 devfs 那些先天的缺陷。很显然,sysfs 将是未来发展的方向。
那么 sysfs 是怎么认出系统中存在的设备以及应该使用什么设备号呢?对于已经编入内核的驱动程 序,当被内核检测到的时候,会直接在 sysfs 中注册其对象;对于编译成模块的驱动程序,当模块载入 的时候才会这样做。一旦挂载了 sysfs 文件系统(挂载到 /sys),内建的驱动程序在 sysfs 注册的数据就可 以被用户空间的进程使用,并提供给 udev 以创建设备节点。
关于设备管理的内容,我只想说这么多,因为要牵扯到许多的配置文件和环境变量,由于每个版本 的 Linux 的设备管理在配置文件和环境变量的设置上多少有些不同,它们的进化又非常快,我觉得只要 不影响本文的理解,我们就先跨过这部分内容吧。
下面我们开始进入内核设备管理系统。
Linux 在设备驱动程序的实现上又分为两层:

  • 抽象设备层(又叫核心模块)
  • 特定设备驱动程序

抽象硬件层: 这一层主要提供一些设备无关的处理流程,也提供一些公用的函数给底层的 device driver 调用。 它为网络协议提供统一的发送、接收接口。这主要是通过net_device结构。是上层的、与设备无关的, 这部分根据输入输出请求,通过特定设备驱动程序接口,来与设备进行通信。
特定设备驱动程序:是一种下层的、与设备有关的,常称为设备驱动程序,它直接与相应设备打 交道,并且向上层提供一组访问接口; 当一个网络设备的初始化程序被调用时,它返回一个状态指 示它所驱动的控制器是否有一个实例。
那么第三个大步骤就是抽象设备层的初始化。
这由net_dev_init函数完成,此函数由下面的宏修饰, 如下:
subsys_initcall(net_dev_init);这个宏定义请参见前面说的 init.h,它被定义为:define_initcall("4",fn)所以它是在 core_initcallfs_initcall之后被调用的。
在 Linux2.4 内核中net_dev_init就是对实际底层网络设备的初始化例程。但是我们要注意的是现在的 这个函数在 Linux2.6 内核中已经不对特定设备进行初始化了,只是为网络设备设置一些基础功能。比如 proc 文件系统、sysfs 系统、全局设备和索引表、设置软中断回调等。不过我个人觉得最重要的是对 queue 的各项成员的初始化。
内核网络初始化函数net_dev_init 分析:

/*设备处理层的初始化函数*/static int __init net_dev_init(void){         int i, rc = -ENOMEM;         /*没有被初始化*/         BUG_ON(!dev_boot_phase);         /*该函数在/proc目录下创建三个文件,主要用于读取网络相关统计数据         正如我们看到的,/proc下的文件基本都为只读的,这里提供的三个文件         都没有写操作*/         if (dev_proc_init())                   goto out;         /*在/sysfs设备文件系统的class中注册net节点*/         if (netdev_sysfs_init())                   goto out;         /*初始化网络处理函数链表和散列表,这些函数是用来处理接收到的不同         协议族报文*/         INIT_LIST_HEAD(&ptype_all);         for (i = 0; i < 16; i++)                   INIT_LIST_HEAD(&ptype_base[i]);         /*下面为初始化存放网络设备的散列表*/         /*散列表关键字由设备名称计算获得*/         for (i = 0; i < ARRAY_SIZE(dev_name_head); i++)                   INIT_HLIST_HEAD(&dev_name_head[i]);         /*散列表关键字由设备接口索引计算获得*/         for (i = 0; i < ARRAY_SIZE(dev_index_head); i++)                   INIT_HLIST_HEAD(&dev_index_head[i]);         /*          *     Initialise the packet receive queues.          */         /*初始化与CPU相关的数据接收队列*/         for_each_possible_cpu(i) {                   struct softnet_data *queue;                   queue = &per_cpu(softnet_data, i);                   skb_queue_head_init(&queue->input_pkt_queue);                   queue->completion_queue = NULL;                   INIT_LIST_HEAD(&queue->poll_list);                   set_bit(__LINK_STATE_START, &queue->backlog_dev.state);                   queue->backlog_dev.weight = weight_p;                   queue->backlog_dev.poll = process_backlog;                   atomic_set(&queue->backlog_dev.refcnt, 1);         }         /*注册网络DMA客户端*/         netdev_dma_register();         /*标志已经初始化*/         dev_boot_phase = 0;         /*注册两个软件中断用于数据接收和发送*/         open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);         open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);         /*在通知链表中注册一个回调函数,用于相应CPU热插拔事件         由回调函数可以看出,一旦接到通知,CPU输入队列中的包逐一         交由netif_rx()处理*/         hotcpu_notifier(dev_cpu_callback, 0);         /*初始化目的路由缓存,通知链的方式*/         dst_init();         /*初始化网络链路层的组播模块,在/proc/net下创建文件dev_mcast         用来存放内核中网络设备与IP组播相关的参数*/         dev_mcast_init();         rc = 0;out:         return rc;}

分析图:
这里写图片描述
这个 queue 之所以重要,在于我们将来在网络接收报文的章节中要对它大书特书,不然,我们就不知道 Linux 网络接收过程的整个环节,不过,在此处,读者只需记住 2 点:
1. 这个 queue 有一个名叫 backlog_dev的设备,其 poll 函数指针指向了一个叫做 process_backlog 的函数
2. 我们设置的接收软中断(RX_SOFTIRQ)将要和这个 queue 以及 back_log设备打交道。

底层 PCI 模块的初始化

虽然我们不必太关心设备管理是如何实现的,但是我们要知道我们的驱动程序是如何与设备搭上关 系。在目前的主机上,PCI 总线是用得最广泛的总线技术,而基于 PCI 总线的网卡设备已经是市场主流, 我们就研究一下 PCI 网卡是如何被操控的,以此可以推断出在不同总线技术下驱动程序的实现基础。
我们已经知道万事都得有头,驱动程序也不例外。在之前提到的 4 个大的步骤里,驱动程序开发人 员使用 module_init宏来修饰自己驱动程序的第一个函数,促使初始化函数放在第 6 段.initcall 段中。
驱动程序必须得遵守一套开发框架,如果是 PCI 驱动,那么为了做 到这一点必须调用一个函数:pci_module_init,它就是整个驱动程序开始工作的第一步,它也能替你完成 底层关于 PCI 操作。不过在 2.6 中它已经变成 include/linux 目录下的 pci.h 中的宏,它被定义成 pci_register_driver。其参数是一个 pci_driver{}类型的结构体,此结构由驱动开发人员定义。我们等一会 还能见到它。
这里写图片描述

根据上图顺藤摸瓜,最终会找到driver_attach 函数。先仔细分析它:

这里写图片描述
这里写图片描述

我们就没有必要去翻箱倒柜的研究bus_macth 内部做了什么了,要把它说明白就偏离了分析网络协议栈的主 线。我们只给出这个函数的大致流程
这里写图片描述
bus_match 函数最后会调用__pci_device_probe,这个函数非常重要,和所有驱动程序有非常 大的关系:
这里写图片描述
不管是哪一种 probe,都最终调用 drvprobe,这是一个函数指针,它是由驱动程序的开发者设置 的。下面就以网络设备的驱动程序为例,看看此 probe 是如何被指定的,它完成了什么工作。下图表示 dev probe 可以被两个函数调用,分别是 pci_device_probe_staticpci_device_probe_dynamic函数,它们先分别调用 pci_match_devicepci_match_one_device 函数,最后才会调用它。
这里写图片描述

__pci_register_driver调用 driver_register之 后会调用 pci_create_newid_file函数 ,它会调用 sysfs_create_file函数为当前的驱动程序创建相应的文件,文件放在/sys 目录下。

网络设备接口初始化例程

net_dev_init 为我们准备好了网络设备的基础功能部件,那特定的设备驱动程序在哪被初始化呢? 先别急,我们得先知道驱动程序是如何被装入内存的。我们前篇曾分析过 ELF 格式,每个驱动 程序编写者通过设置自己的驱动程序入口函数——比如 xxx_init_module——为 module_init类型的,那么 在编译以后入口函数会放在特殊的 text 段中以至于系统在启动的时候能找到这个入口函数,就这样,系 统遍历并执行各个入口函数,让这些驱动程序完成基本的加载。下图中就是驱动程序被装入内存中的实 现过程,其中包括我们熟知的 loopback 接口,它也作为一种设备,在这个阶段被装入内存,而且,注意, 它还主动完成了其它驱动程序没有做的事——register_netdev :
这里写图片描述
设备驱动程序被装入系统的步骤如上图所示。每个驱动程序调用 pci_module_init函数,传入一个 pci_driver{}结构,比如:

    static struct pci_driver aaa_driver = {       .name = "aaa", /* 驱动模块的名字,可任由开发者定义 */      .id_table = aaa_pci_tbl, /* 这是一个 pci_device_id{}结构的数组,必须和设备的硬件信息一致 */        .probe = aaa_init_one, /* 回调函数,由 PCI 模块调用 */       }; 

不过要注意的是这只是加载,并不是对设备进行初始化。在 Linux 初学者会有错觉。一个明证就是: 我们在配置内核的编译时,比如在配置网络设备一节时,会看到多种网络设备驱动被编译到内核中,但 是实际上能起作用的就只是与主机网口真正匹配的驱动程序能工作。也就是说,那些没有相应设备的驱 动程序根本就找不到自己的设备,也就无所谓“初始化”了。举个例子,本人主机网卡是 Ether ExpressPro100,但在在配置编译选项时,如果选择同时编译 Ether ExpressPro100 和 Intel Pro /100+并都是 内嵌入内核时,系统会先初始化前者,而后者只执行到 pci_bus_match就返回了 0——没有找到相应的设 备!。
那么真正的初始化在哪呢? 结合上一节介绍的底层 PCI 模块的初始化一节中,我们知道每个驱动程序必须设置代码中的device id{}结构。驱动程序把这个结构传入 PCI 数据库,当 PCI 开始工作以后,它会扫描总线和设备的 device id, 然后查找数据库中的每个驱动,如果与之匹配,那么就会调用之前注册到 PCI 库中的 devprobe()函 数。每个驱动程序要自己写 probe()回调函数,完成检查寄存器和真正初始化设备的工作。其通用的工 作流程如下图
这里写图片描述
dev_new_index 中分配一个 ifindex 给设备,所谓分配,其实就是一个 static 整数不断往上加 1, 来一个设备就加 1。 在 2.6 早期版本中用dev_base数组来串联每个设备,但是这种方法在查找上不方便,而且扩展 性差(当时只有 8 个设备),随着 Linux 在路由器和交换机设备上的应用,这种方法不能适应这 种类型设备多接口的特性,转而采用dev_name_head 来记录。此全局变量是一个 hash 表,以设 备的名字作为输入,再通过一系列 hash 变换得到一个 key,来查找和增删某设备,如果碰到接 口特别多的情况比如有 128 个接口,或者有 4095 个 VLAN,这样的 hash 查找就比较快了。(Linux 下 VLAN 也是一种特殊的“设备”),当然系统中还有使用 dev_base的地方,比如要搜索一个接 口但并不知道其名字和接口,只知道接口的 ip 地址,那么就只好从这个表找——咦,这不就是 路由查找的本质吗?
static struct hlist_head dev_name_head[1<<NETDEV_HASHBITS],也就是说该 hash 数组有 15 个
单元,比以前的 8 个多,如果发生 key 冲突可以用链表来挂接。 同样的道理,网口的 ifindex 也用 hash 表来存储了,名字叫 dev_index_head
这里写图片描述这里写图片描述这里写图片描述

下面这个结构就是刚才一直说的net_device,它显得非常庞大,这其实是 Linux 内核目前不适合用作 高性能由器操作系统的地方。
这里写图片描述
这里写图片描述
这里写图片描述这里写图片描述 这里写图片描述这里写图片描述

有一类设备,在协议栈里非常特殊,在大多数的 TCP/IP 实现里,都实现了这样一个设备,不过,它不是真正的设备,而只是一个虚拟的,用来做调试的接口。它就是 loopback设备。在协议栈初始化的时候,这个接口必然要创建,而且一直存在。现在,我们来看看这个接口是如何初始化的,刚才提到了,它在被装入的时候,主动调用了register_netdev,而不是由 PCI 模块回调它,原因很简单,没有实际设备的 id 与 loopback 设备相符,PCI 自然不会调用它,所以注册的事情就必须自己动手,丰衣足食啦。与 2.4 内核不同的是,在 2.6 内核中,loopback 接口设备的初始化被移到 net_olddevs_init中。当网卡是以模块动态加载方式初始化的时候,netdev_boot_base 返回 0。因为 net_olddevs_init在各模块加载之前执行当网卡以嵌入内核代码方式初始化的时候,netdev_boot_base回返回 1。因为嵌入模块已经被初始化了,所以在 net_olddevs_init 函数执行的时候可以扫描到设备的存在。
下面是 loopback 设备的定义:
这里写图片描述

net_device{}结构中有 2 个非常重要的指针:ip_ptrprivip_ptr指向的是更高层次的数据结构,而priv 指向的是设备底层的私有数据。因为在 Linux 内核维护人员来看,大部分网络设备具有的共性,和与物理硬件无关的数据可以归纳为一个数据结构,这是设备抽象层维护的结构,而硬件自己特有的数据比如寄存器和硬件缓冲区由驱动开发人员维护,但也不确定这些数据的大小和类型,于是使用一个 void 指 针来指向。要注意的是 net_device和设备私有数据是放在连续的空间的,请读者自行去参考 alloc_netdev函数,这减少了申请内存的次数,也不会导致 2 次释放内存。 而且net_device{}这个结构也不必被上层的协议栈操作,比如设备可能有 IP 地址,也可能没有 IP 地 址,更重要的是,该设备也不应该只和 IP 协议打叫道,可能还有 PPP 协议、SLIP、X25 协议,所以应该 再找一个跟此结构相关但却和协议相关的“代理结构”来记录这些信息。由于并不肯定是和 IP 协议栈交 互,那么指向这个“代理结构”的指针类型还是 void 类型。当然按照命名规矩,在 IP 协议栈的框架内, 提出了一个in_device{}的结构,in 就是ip network的意思啦。虽然目前net_device{}列出了ip_ptr和其他 网络的指针,但将来这个几个指针会用“联合”来代替,除非该设备不仅工作在 IP 网络上还工 作在其他类型的网络上。
这里写图片描述

这个结构和 net_device 的关系如下图,这个关系在 inetdev_init函数中确定:
这里写图片描述

设备无关层采用in_device{}数据结构保存 IP 地址和邻居信息——虽然是间接的网络抽象层采用 net_device{}数据结构保存设备的名字、编号、地址等共性设备特定层的数据则有设备驱动开发人员自己定义,一般有硬件发送、接收缓冲区、芯片寄存器的信息等等。这片内存区一般是紧跟在 net_device{}后面,由驱动程序在创建net_device{}的时候顺带把这块内存也创建了。当然还是用 priv 指针指向,以方便访问。设备已经被注册了,那么是否就可以工作了呢?不是的,还得靠用户把这些网卡激活,当然,一般都有脚本在系统初始化的时候干这样的活。网卡被激活的时候,它要完成几个非常重用的事情:
1. 挂接中断处理函数(ISR),如果不能为驱动程序申请到中断,那说明要么网卡没插好,要么和 其他设备发生了冲突,结果就是设备根本不能用。
2. 创建驱动程序内部接收环和发送缓冲区,网卡一般都要“环”的方式来存放报文。
3. 挂接接口状态扫描定时器,以 poll 的方式轮询接口是否真正 up 或 down。
4. 进一步打开设备特点寄存器,使其可以开始收发报文了
这里将这个过程画在下面这幅图中:
这里写图片描述
看到这几幅图,读者一定都看到了那几个红色小红旗及方框,其中写明了此时代码要完成的基本任 务。现在可以对驱动程序的初始化做一个基本的概括,即 Linux2.6 下网络驱动程序的初始化分为 4 个基 本步骤:
第1步. 系统把驱动程序装入内存
第2步. PCI 为设备选择正确的驱动程序,并分配相应内存数据结构
第3步. 指定驱动程序如何处理报文格式
第4步. 用户打开设备使其可以真正工作起来

关于底层驱动的架构已经基本介绍完了,在这里我还得提醒读者,在笔记中,设备就是接口,接口 就是设备,它们在一般情况下是同等意义。也许有读者提出一块网卡可能有 2 个接口,那么这算多少设 备呢?可以这样解答:设备驱动程序可以只有一份,但是你创建的 net_device{}结构必须要 2 个,也就是 说,Linux 内核本来不是用来做路由器的,现在你要赶鸭子上架硬是要实现路由器级别的 Linux,那么你 得忍受内核中一些编码风格——那个 net_device 实际可以改名为 net_interface等。所以,本书中“接口” 和“设备”是换着使用,它们的含义基本上是接近“接口”概念的。

1 0
原创粉丝点击