• 深入理解Linux网络技术内 幕(六)——PCI层和网络接口卡



    前言

    内核中的PCI子系统(也称PCI层)提供各种PCI设备驱动程序共同的所有通用功能。这个子系统让程序员减少了很多必须对各种设备所做的事,让驱动程序能以更简明的方式编写,使内核更易于收集和维护有关各种设备的信息,如描述信息和统计数据。

    涉及的数据结构

    在此列出的PCI层使用的一些关键数据结构类型。还有很多其他类型,但是下列几种是必须知道的。第一个结构定义在include/linux/mod_devicetale.h中,而另外两个定义在include/linux/pci.h

    • pci_device_id

      • 设备标识符。这不是Linux所使用的本地ID,而是根据PCI标准所定义的ID。后面一节会说明此ID的定义
    • pci_dev

      • 每个PCI设备都会被分派一个pci_dev实例,如同网络设备都会被分派net_device实例一样。这个结构由内核使用,已引用一个PCI设备。
    • pci_driver

      • 定义PCI层和设备驱动程序之间的接口。这个结构主要由函数指针组成。所有PCI设备都会使用这个结构。后面一节会进行介绍。

    PCI设备驱动程序pci_driver结构的实例定义。以下是其主要字段说明,特别注意NIC设备的情况。函数指针会由驱动程序初始化为该驱动程序内适当的函数。

    • char *name

      • 驱动程序的名字
    • const struct pci_device_id *id_table

      • 这是一个ID向量,内核用于把一些设备关联到此驱动程序,”PCI NIC“驱动程序注册范例一节会展示一个实例。
    • int (*probe)(struct pci_dev *dev, const pci_device_id *id)

      • 当PCI层发现它正在搜寻驱动程序的设备ID与前面所提到的id_table匹配时,就会调用该函数。此函数应该开启硬件,分配net_device结构,初始化并注册新设备。此函数中,驱动程序会分配正确工作所需的所有数据结构(如传输或接收时所用的缓冲区)。
    • void (*remove)(struct pci_dev *dev)

      • 当驱动程序从内核除名时,或者当个可热插拔设备被删除时,PCI层就会调用此函数。此函数为probe函数配对函数,用于清理任何数据结构和状态。网络设备使用此函数来释放已分配的I/O端口和I/O内存,为设备除名,释放net_device数据结构以及其他由设备驱动程序在probe函数内所分配的辅助数据结构。
    • int (*suspend)(struct pci_dev *dev, pm_message_t state)

    • int (*resume)(struct pci_dev *dev)

      • 当系统进入挂起模式以及重新继续时,PCI层就会调用这些函数。后面一节介绍
    • int (*enable_wake)(struct pci_dev *dev, u32 state, int enable)

      • 利用这个函数,驱动程序可以通过产生特定的电源管理事件信号,开启或关闭设备唤醒系统的能力。
    • struct pci_dynids dynids

      • 动态ID

    PCI NIC设备驱动程序的注册

    PCI设备独一无二识别方式是通过一些参数的组合,包括开发商以及模型等。这些参数由内核存储在pci_device_id类型的数据结构中,定义如下:

    struct pci_device_id{
        unsigned int vendor, device;
        unsigned int subvendor, suddevice;
        unsigned int class, class_mask;
        unsigned long driver_data;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    vendor和device通常就足以识别设备。subvendor和subdevice很少用到,通常设置为通配符(PCI_ANY_ID)。class和class_mask代表该设备所属的类,而NETWORK就是我们所要讨论的设备所属类。driver_data不是PCI ID的一部分,而是由驱动程序所使用的一个私有参数。

    PCI设备驱动程序分别用pci_register_driver和pci_unregister_driver向内核注册和除名。这些函数定义在include/pci/pci.c中。除此之外,还有一个pci_module_init,是pci_register_driver的别名。因此pci_register_driver之前,旧版本内核中所用的函数名为pci_module_init,有些驱动程序依然使用它。

    pci_register_driver需要一个pci_driver数据结构作为自变量。借助于pci_driverid_table向量,内核知道该驱动程序可以处理那些设备,此外,也因为pci_driver中所有虚拟函数,使内核有一种机制,可以与此驱动程序的任何相关联的设备彼此交互。

    PCI的优点之一是,其之处寻找IRQ和每个设备所需要的其他资源的探测方式相当优雅。模块可以加载期间接收一些输入参数,以告知该如何配置其所负责的所有设备。但是,有些时候让驱动程序自行检查系统上的设备,然后为其负责的那些设备做配置会比较简答一点。必要时,用户依然可以退回到手动配置。

    /sys文件系统输出有关系统总线(PCI,USB等等)的信息,包括各种设备及各种设备之间的关系。/sys也允许管理员为特定的设备驱动程序定义新的ID,使得除了驱动程序通过其pci_driver结构的id_table向量注册的静态ID之外,内核还能使用由用户所配置的参数。

    这里不会说明内核根据设备ID查询驱动程序所采用的探测机制。然而值得一提的是,探测方式有两种:

    • 静态

      • 给定一个设备PCI ID,内核就能根据is_table向量查询出正确的PCI驱动程序(也就是pci_driver实例_。)这称为静态探测方式
    • 动态

      • 这种查询是根据用户手动配置ID,这种情况在实际中很少见,但是,在调试的情况下偶尔也使用。动态指的是系统管理员可以新增ID的能力,而不是指ID本身可自行变动。

      • 由于动态ID是在运行中的系统上配置的,只有当内核被编译成支持热插拔才能使用。

    电源管理和网络唤醒

    PCI电源管理事件由pci_driver数据结构的sudpendresume函数处理。除了分别负责PCI状态的保存和恢复之外,这些函数遇到NIC的情况时还需要采取特殊步骤:

    • suspend主要停止设备出口队列,使得该设备无法再传输。

    • resume重启出口队列,使得该设备得以再次传输。

    网络唤醒(Wake-on-lan,WOL)是一种功能,允许NIC在接收到一种特殊类型的帧时唤醒处于待命模式的系统。WOL通常默认是关闭的。此功能用pci_enable_wake打开或关上。

    当首次引入WOL功能时,只有一种帧可以唤醒系统:“魔术封包”这类特殊帧有两个主要特征:

    • 目的MAC地址属于正在接收的NIC(无论该地址是单播、多播还是广播)。

    • 帧中的某处(任何地方)会设置一段48位序列,后面再接NIC MAC地址,在一行中至少连续重复16此。

    现在也有可能允许其他类型的帧唤醒系统。有少量设备可以根据在模块加载期间设置的一个参数开启或关闭WOL功能。

    主要一个开启WOL功能的功能识别一个帧,其类型为可唤醒系统,就会产生一个电源管理通知信息,去做相应的工作。

    PCI NIC驱动程序注册范例

    drivers/net/e100.c中的Intel PRO/100 Ethernet驱动程序来说明驱动程序的注册:

    #define INTEL_8255X_ETHERNET_DEVICE(device_id, ich){\
            PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID, \
            PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00, ich}
    static struct pci_device_id e100_id_table[] = {
        INTEL_8255X_ETHERNET_DEVICE{0x1029, 0},
        INTEL_8255X_ETHERNET_DEVICE{0x1030, 0},
        ....     
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    PCI NIC设备驱动程序会把一个pci_device_id结构体向量注册给内核,该结构向量列出了其所能处理的那些设备。例如,e100_id_table就是e100.c驱动程序所使用的结构,注意:

    • 第一个字段(相当于此结构定义中的vendor)为固定值PCI_VENDOR_ID_INTEL,它为指定给Intel初始化的开发商ID。

    • 第三个字段和第四个字段(subvendor和subdevice)通常初始化为通用值PCI_ANY_ID,因为前两个字段(vendor和device)足以识别那些设备。

    • 许多设备都会使用那张设备表使用宏__devinitdata,用来标记初始化数据,不过e100_id_table没有。

    此模块由module_init宏指定的e100_init_module初始化。当此函数在引导期间或者模块加载期间由内核执行时,就会调用pci_moudule_init。此函数就会注册该驱动程序,并间接注册所有相关的NIC,后面一节进行介绍。

    下面是e100驱动程序与PCI层接口相关的关键部分:

    #define NAME "e100"
    static int __devinit e100_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
    {
        ...
    }
    static void __devexit e100_remove(struct pci_dev *pdev)
    {
        ...
    }
    #ifdef CONFIG_PM
    static int e100_suspend(struct pci_dev *pdev, u32 state)
    {
        ...
    }
    static int e100_resume(struct pci_dev *pdev)
    {
        ...
    }
    #endif
    
    static struct pci_driver e100_driver = {
        .name = NAME,
        .id_table = e100_id_table,
        .probe = e100_probe,
        .remove = __devexit_p(e100_remove),
    #ifdef CONFIG_PM
        .suspend = e100_suspend,
        .resume = e100_resume,
    #endif
    };
    
    static int __init e100_init_module(void)
    {
        ...
        return pci_module_init(&e100_driver);
    }
    static void __exit e100_cleanup_module(void)
    {
        pci_unregister_driver(&e100_driver);
    }
    module_init(e100_init_module);
    module_exit(e100_cleanup_module);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    此外,注意:

    • 只有当内核支持电源管理时,才会对suspend和resume做初始化,所以只有当该条件为真时,e100_suspend和e100_resume这两个函数才会被包括在映像中。

    • pci_driver的remove字段用宏__devexit_p标记,而e100_remove用__devexit标记。

    • e100_probe用__devinit标记。

    大蓝图

    我们把前几节所了解的综合来看,采用PCI总线的系统以及一些PCI设备在引导期间会发生什么事。

    当系统引导时,会建立一种数据库,把每个总线都关联一份已侦测到而使用该总线的设备列表。例如,PCI总线的描述符除了其他参数外,还包括一个已侦测PCI设备的列表。如“PCI NIC设备驱动程序的注册”一节所见,每个pci设备都可由pci_device_id结构中的一个大的字段集合唯一地识别,不过通常只需要几个字符就足够了。我们也知道PCI设备驱动程序如何定义一个pci_driver实例,以及如何用pci_register_driver与PCI层注册。设备驱动程序加载时,内核已建好其数据库,我们以配有三个PCI设备的图为例,说明当设备驱动程序A和B加载时会发生什么事。

    在这里插入图片描述

    当设备驱动程序A被加载时,会调用pci_register_driver并提供pci_driver实例而与PCI层注册。pci_driver结构有内含一个此驱动程序能驱动的PCI设备ID的向量。接着,PCI层使用该表去查看已侦测的PCI设备列表中与那些设备匹配。于是,就会建立该驱动程序的设备列表(如上图b所示)。此外,对每个匹配的设备而言,PCI层会调用相匹配的驱动程序中的pci_driver结构中锁提供的probe函数。probe函数会建立并注册相关联的网络设备。就此而言,设备Dev3就会被分派给驱动程序B。图c所示就是加载此去程程序后的结果。

    当驱动程序于稍后卸载时,该模块的module_exit函数就会调用pci_unregister_driver。接着,由于其数据库,使得PCI层能够遍历所有与该驱动程序相关联的设备,并启用该驱动程序的remove函数。此函数就会将此网络设备除名。

    通过/proc文件系统调整

    /proc/pci文件可用于倾卸有关已注册的PCI设备的信息。pciutils套件中的lspci命令也可用于打印出有关于本地PCI设备的有用信息,但其信息取自/sys

    本章涉及的函数和变量

    名称描述
    函数和宏
    pci_register_driver注册PCI驱动程序
    pci_unregister_driver除名PCI驱动程序
    pci_module_init初始化PCI驱动程序
    数据结构
    struct pci_driver定义PCI驱动程序(多数都是虚拟回调函数)
    struct pci_device_id存储PCI设备相关联的通用ID
    struct pci_dev结构代表内核空间中的PCI设备

    涉及的文件和目录

    在这里插入图片描述

  • 相关阅读:
    Ubuntu 22.04 更换内地源
    MyBtais的SQL映射文件(元素,查询,映射,动态SQL)
    Opengl之混合
    【数据结构】栈
    戴尔PowerEdge服务器R450 RAID配置步骤
    openstack 遇到的error
    java八股文面试[数据库]——分库分表
    js — 原生轮播图的制作
    Mybatis的二级缓存 (默认方式)
    机器学习(19)---XGBoost入门
  • 原文地址:https://blog.csdn.net/m0_56145255/article/details/127601181