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

概述

内核的PCI子系统(即PCI层)提供了不同设备一些通用的功能,以便简化各种设备驱动程序。

PCI层重要结构体如下:

pci_device_id

设备标识,根据PCI标志定义的ID,而不是Linux本地的。

pci_dev

类似于网络设备的net_device。每个PCI会被分配一个net_dev实例。

pci_driver

PCI层和设备驱动程序之间的接口。主要由一些函数指针组成。如下所示:

struct pci_driver {
    struct list_head node;
    char *name; //驱动程序名字
    const struct pci_device_id *id_table;   /* ID向量,内核用于把设备关联到此驱动程序 */
    int  (*probe)  (struct pci_dev *dev, const struct pci_device_id *id);   /* New device inserted */
    void (*remove) (struct pci_dev *dev);   /* Device removed (NULL if not a hot-plug capable driver) */
    int  (*suspend) (struct pci_dev *dev, pm_message_t state);  /* Device suspended */
    int  (*suspend_late) (struct pci_dev *dev, pm_message_t state);
    int  (*resume_early) (struct pci_dev *dev);
    int  (*resume) (struct pci_dev *dev);                   /* Device woken up */
    void (*shutdown) (struct pci_dev *dev);
    struct pci_error_handlers *err_handler;
    struct device_driver    driver;
    struct pci_dynids dynids;
};

PCI NIC设备的注册

PCI设备由 pci_device_id (的成员共同)唯一标识。

struct pci_device_id {
    __u32 vendor, device;       /* Vendor and device ID or PCI_ANY_ID*/ //通常 vendor, device就足以标识设备
    __u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID *///很少用到
    __u32 class, class_mask;    /* (class,subclass,prog-if) triplet *///设备所属的类,如network类
    kernel_ulong_t driver_data; /* Data private to the driver *///不属于PCI标识部分,而是驱动私有参数
};

每一个设备驱动程序会注册一个pci_device_id
实例的向量(即一系列的pci_device_id 实例),这个向量包含了该驱动程序所能处理的设备的ID。

下面是设备驱动程序的注册和删除的函数:

int __pci_register_driver(struct pci_driver *drv, struct module *owner, const char *mod_name)
void pci_unregister_driver(struct pci_driver *drv)
pci_module_init()//在一些驱动程序上作为__pci_register_driver别名

内核根据设备ID查询设备的驱动程序,这是一种探测机制。探测机制有两种方式:静态和动态。

 静态:

给定一个PCI的ID,内核根据该ID从id_table向量查询出对应的驱动程序

动态:

根据用户手动配置的ID,比较少用到,要求内核编译时支持热插拔。

电源管理和网络唤醒

PCI的电源管理事件有pci_driver的suspend和resume进行。这两个函数分别负责PCI状态的保存和恢复。如果遇到NIC的的情况还需要分别进行下面步骤:

suspend:停止设备出口队列,使得该设备无法再传输:

resume:重启出口队列,是设备可以继续传输。

网络唤醒功能允许NIC在接收到某种特殊帧是唤醒系统。这个功能通常是被禁用的,但是此功能可以用pci_enable_wake打开或关上。关于这部分我发现linux-3.12.36里好像没有这个函数了,可能有使用了其它网络唤醒方法,留着以后再补充了。

PCI NIC驱动程序注册范例

以Intel PRO/100 Ethernet驱动程序说明NIC设备驱动程序的注册,源文件为drivers/net/e100.c。

初始化pci_device_id内容:

#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 }

/****************************************************************************************/
#define DEFINE_PCI_DEVICE_TABLE(_table) \
    const struct pci_device_id _table[] __devinitconst
/***************************************************************************************/

static DEFINE_PCI_DEVICE_TABLE(e100_id_table) = {
    INTEL_8255X_ETHERNET_DEVICE(0x1029, 0),
    INTEL_8255X_ETHERNET_DEVICE(0x1030, 0),
    INTEL_8255X_ETHERNET_DEVICE(0x1031, 3),
    INTEL_8255X_ETHERNET_DEVICE(0x1032, 3),
    INTEL_8255X_ETHERNET_DEVICE(0x1033, 3),
    INTEL_8255X_ETHERNET_DEVICE(0x1034, 3),
    INTEL_8255X_ETHERNET_DEVICE(0x1038, 3),
    INTEL_8255X_ETHERNET_DEVICE(0x1039, 4),
    INTEL_8255X_ETHERNET_DEVICE(0x103A, 4),
    INTEL_8255X_ETHERNET_DEVICE(0x103B, 4),
    INTEL_8255X_ETHERNET_DEVICE(0x103C, 4),
    INTEL_8255X_ETHERNET_DEVICE(0x103D, 4),
    INTEL_8255X_ETHERNET_DEVICE(0x103E, 4),
    INTEL_8255X_ETHERNET_DEVICE(0x1050, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1051, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1052, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1053, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1054, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1055, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1056, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1057, 5),
    INTEL_8255X_ETHERNET_DEVICE(0x1059, 0),
    INTEL_8255X_ETHERNET_DEVICE(0x1064, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x1065, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x1066, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x1067, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x1068, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x1069, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x106A, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x106B, 6),
    INTEL_8255X_ETHERNET_DEVICE(0x1091, 7),
    INTEL_8255X_ETHERNET_DEVICE(0x1092, 7),
    INTEL_8255X_ETHERNET_DEVICE(0x1093, 7),
    INTEL_8255X_ETHERNET_DEVICE(0x1094, 7),
    INTEL_8255X_ETHERNET_DEVICE(0x1095, 7),
    INTEL_8255X_ETHERNET_DEVICE(0x10fe, 7),
    INTEL_8255X_ETHERNET_DEVICE(0x1209, 0),
    INTEL_8255X_ETHERNET_DEVICE(0x1229, 0),
    INTEL_8255X_ETHERNET_DEVICE(0x2449, 2),
    INTEL_8255X_ETHERNET_DEVICE(0x2459, 2),
    INTEL_8255X_ETHERNET_DEVICE(0x245D, 2),
    INTEL_8255X_ETHERNET_DEVICE(0x27DC, 7),
    { 0, }
};

在模块的初始化和卸载接口中完成PCI设备驱动程序的注册和注销:

static struct pci_driver e100_driver = {
    .name =         DRV_NAME,
    .id_table =     e100_id_table,
    .probe =        e100_probe,
    .remove =       __devexit_p(e100_remove),
#ifdef CONFIG_PM
    /* Power Management hooks */
    .suspend =      e100_suspend,
    .resume =       e100_resume,
#endif
    .shutdown =     e100_shutdown,
    .err_handler = &e100_err_handler,
};
static int __init e100_init_module(void)
{
    if (((1 << debug) - 1) & NETIF_MSG_DRV) {
        pr_info("%s, %s\n", DRV_DESCRIPTION, DRV_VERSION);
        pr_info("%s\n", DRV_COPYRIGHT);
    }
    return pci_register_driver(&e100_driver);
}
static void __exit e100_cleanup_module(void)
{
    pci_unregister_driver(&e100_driver);
}
module_init(e100_init_module);
module_exit(e100_cleanup_module);

其中的一些函数指针原型:

#define DRV_NAME        "e100"
static int __devinit e100_probe(struct pci_dev *pdev,  const struct pci_device_id *ent)
{
    struct net_device *netdev;
    struct nic *nic;
    int err;
    if (!(netdev = alloc_etherdev(sizeof(struct nic)))) {
        if (((1 << debug) - 1) & NETIF_MSG_PROBE)
            pr_err("Etherdev alloc failed, aborting\n");
        return -ENOMEM;
    }
                         ……
                         ……
}

PCI子系统总览

(a)在系统引导时,会建立一个数据库,把每个总线都关联到一份已侦测到而使用该总线的设备列表。PCI总线的描述符处理其他参数外,还包括一个已侦测PCI设备的列表。

(b)当驱动程序被加载,调用pci_register_driver注册pci_driver到PCI层时,PCI会使用pci_driver结构中的PCI设备ID参数id_table与已侦测到的PCI设备列表匹配,若匹配到就会建立该驱动程序的设备列表。对于每个匹配到的设备,PCI层会调用相匹配的驱动程序中的pci_driver结构中的probe函数,建立并注册相关联的网络设备。

/proc/pci文件包含了已注册的PCI设备的信息。pciutils套件中的lspci命令会输出有关本地PCI设备的信息,其中有些信息取自/sys。

时间: 2024-08-02 10:41:04

深入理解Linux网络技术内幕——PCI层和网络接口卡的相关文章

深入理解Linux网络技术内幕——L4层协议与Raw IP的处理

我们简单了解下L4层协议和Raw IP是如何与IP层进行交互的. L4层协议 L4层协议可以通过静态编译和模块配置两种方式加入内核. 比较重要的协议如TCP.UDP.ICMP通常是静态编译至内核. 一些不常用的或者比较特殊的协议,则是通过内核配置加入内核.如IGMP,SCTP,IPIP等等. L4层协议的注册 L4层协议有net_protocol结构定义: /* This is used to register protocols. */ struct net_protocol { int (*

深入理解Linux网络技术内幕——IPv4 报文的传输发送

报文传输,指的是报文离开本机,发往其他系统的过程. 传输可以由L4层协议发起,也可以由报文转发发起. 在深入理解Linux网络技术内幕--IPv4 报文的接收(转发与本地传递)一文中,我们可以看到,报文转发最后会调用dst_output与邻居子系统进行交互,然后传给设备驱动程序. 这里,我们从L4层协议发起的传输,最后也会经历这一过程(调用dst_output).本文讨论的是L4层协议发起的传输,在IPv4协议处理(IP层)中的一些环节. 大蓝图 我们先看下传输环节的大蓝图,以便对传输这一过程有

深入理解Linux网络技术内幕——网络设备初始化

概述 内核的初始化过程过程中,与网络相关的工作如下所示: 内核引导时执行start_kernel,start_kernel结束之前会调用rest_init,rest_init初始化内核线程init(在Linux3-12中为kernel_init). asmlinkage void __init start_kernel(void) { ... parse_early_param();//间接调用parse_args parse_args(...); //处理内核引导程序(boot loader)

《深入理解Linux网络技术内幕》阅读笔记 --- 路由

一.Linux内核中路由相关的主要数据结构 struct fib_result:对路由表查找后返回该结构,它的内容并不是简单的包含下一跳信息,而且包含其他特性,例如策略路由所需的更多参数. struct fib_rule:表示由策略路由在路由流量时选择路由表的规则 struct fib_node:一条路由表项.例如,该数据结构用于存储由route add或ip route add命令添加一条路由时生成的信息. struct fn_zone:一个zone表示子网掩码长度相同的一组路由 struct

深入理解Linux网络技术内幕——设备的注册于初始化(一)

副标题:设备注册相关的基本结构的原理框架 设备注册与删除时间 设备在下列两种情况下进行注册: 1)加载NIC驱动时 2)插入热插拔设备时 这里NIC与热插拔设备有些不同.a.对于非热插拔NIC来说,NIC的注册是伴随着其驱动的发生的,而NIC可以内建到内核,也可以作为模块载入,如果内建入内核,则NIC设备和初始化均发生在引导时,如果NIC作为模块加载,则NIC的注册和驱动初始化均发生在模块加载时.b. 对于热插拔NIC设备来说,其驱动已经加载,因此设备的注册发生在插入设备,内核通知关联驱动时.

深入理解Linux网络技术内幕——帧的接收与传输

帧的接收 NAPI与netif_rx(非NAPI) Linux内核获取网络帧到达通知的方式有两中:中断和轮询.(中断值设备向内核发出中断,轮询指linux内核主动轮询设备) 在早起的linux内核中,网络帧主要以中断的方式通知linux内核帧的到达.这是非NAPI方式. 现在的操作系统中,linux内核使用NAPI方式, 获取帧到达的消息.NAPI混合使用了中断和轮询. netif_rx(非NAPI): 每一个帧接收完毕时,设备向内核发送一个中断.(在低流量负载的情况下,这种方式对比轮询优势明显

深入理解Linux网络技术内幕——路由子系统的概念与高级路由

本文讨论IPv4的路由子系统.(IPv6对路由的处理不同). 基本概念 路由子系统工作在三层,用来转发入口流量. 路由子系统主要设计 路由器.路由.路由表等概念. 路由器: 配备多个网络接口卡(NIC),并且能利用自身网络信息进行入口流量转发的设备. 路由: 流量转发,决定目的地的过程 路由表:转发信息库,该库中储存路由需要本地接收还是转发的信息, 以及转发流量时所需要的信息.(即,信息库用来判断,要不要转发,如果要转发,向哪里转发). 我们了解,路由器有多个网卡,但是多个NIC的设备不一定就是

深入理解Linux网络技术内幕——IPv4 报文的接收(转发与本地传递)

我们知道,报文经过网卡驱动处理后,调用net_receive_skb传递给具体的协议处理函数,对于IPv4报文来说,其协议处理函数就是ip_rcv了,ip_rcv在进行一些健康检查等操作后,会调用ip_rcv_finish来处理报文.这也是IPv4协议对报文接收处理的开始. 我们先看下ip_rcv_finish源代码: ip_rcv_finish: //ip数据报文的主要处理程序(ip_rcv仅仅只是对ip数据报做一些健康性检查) //ip_rcv_finish 其实是进行路由表查询,,决定报文

深入理解Linux网络技术内幕——协议处理函数

网络帧在进入网络层时,需要区分不同的网络协议进行处理,这就需要涉及协议处理函数. 首先我们从驱动接收到一个数据帧,分析数据帧在协议栈中自下而上的传输流程. 设备驱动程序在接收到一个数据帧时,会将其保存在一个sk_buff缓冲区数据结构,并对其进行初始化. struct sk_buff { ...... __be16 protocol:16; ...... } 在这个缓冲区结构体中,有一个protocol字段,用于标识网络层的协议. 我们知道网络帧在设备驱动程序中处理后,设备驱动程序会调用neti