在 Linux 内核中,net_device 数据结构用来代表网络设备,包含设备的硬件和软件配置信息,是网络驱动层最核心的数据结构。

结构体解析

通用字段

设备名称、next 指针指向下一个 net_device、设备状态 state、网络设备索引值 ifindex(用来标志网络设备以便快速定位,设备被创建后由 dev_get_index 函数分配)、refcnt 表示网络设备的引用次数。

1
2
3
4
5
6
struct net_device {
    char            name[IFNAMSIZ];  // 设备名称,如果被驱动程序设置的名称中包含 %d 格式化字符串,register_netdev 将使用一个数字替换它
    unsigned long        state;     // 设备状态,包含若干标识,驱动程序通常无需直接操作这些标识,内核提供了一组工具函数
    void            *priv;    /* pointer to private data    */
  /* ... */
};

硬件信息

内存共享字段(描述网络适配器与内核共享的内存空间,指定发送包和接受包所在的区域)、I/O 基地址(用于驱动程序搜索设备)、设备使用的中断号 irq、分配给设备的 DMA 通道号、多端口设备使用的不同端口 if_port(网络介质类型决定)。

1
2
3
4
5
6
7
8
9
struct net_device {
    unsigned long        mem_end;    /* shared mem end    */
    unsigned long        mem_start;    /* shared mem start    */
    unsigned long        base_addr;    /* device I/O address    */
    unsigned int        irq;        /* device IRQ number    */
    unsigned char        if_port;    /* Selectable AUI, TP,..*/
    unsigned char        dma;        /* DMA channel        */
  /* ... */
};

物理层数据字段

指定 2 层协议头部长度、最大传输单元 mtu(以太网 1500byte)、网络设备输出队列的最大长度、网络设备类型 type、地址字段(广播地址、多播地址表等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct net_device {
    unsigned        mtu;    /* interface MTU value        */
    unsigned short        type;    /* interface hardware type    */
    unsigned short        hard_header_len;    /* hardware hdr length    */
    unsigned int        flags;    /* interface flags (a la BSD)    */

  /* Interface address info used in eth_type_trans() */
    unsigned char        dev_addr[MAX_ADDR_LEN];    /* hw address, (before bcast because most packets are unicast) */
    unsigned char        broadcast[MAX_ADDR_LEN];    /* hw bcast add    */

    unsigned long        tx_queue_len;    /* Max frames per queue allowed */

  /* ... */
};

设备方法

 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
43
44
45
46
47
48
49
50
struct net_device {
  // 打开接口,在 ifconfig 激活接口时,接口将被打开。open 函数应该注册所有的系统资源(I/O端口、IRQ、DMA等等),打开硬件,并对设备执行其他所需的设置
    int            (*open)(struct net_device *dev);
  // 停止接口,当设备终止时应该被停止,其执行操作与open相反
    int            (*stop)(struct net_device *dev);

  // 执行数据包的发送
  int            (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);

    // 在 hard_start_xmit 前被调用,将传入的信息组织成硬件头
  int            (*hard_header) (struct sk_buff *skb,
                        struct net_device *dev,
                        unsigned short type,
                        void *daddr,
                        void *saddr,
                        unsigned len);

  // 该函数用来在传输数据包之前,完成 ARP 解析之后,重新建立硬件头
    int            (*rebuild_header)(struct sk_buff *skb);

  // 改变接口配置,是配置驱动程序的入口点,可在运行中改变设备的I/O地址和中断号
  int            (*set_config)(struct net_device *dev, struct ifmap *map);

  // 如果数据包的传输在合理的时间段内失败,则假定丢失了中断或接口被锁住
  // 这时网络代码将调用该方法,它负责解决问题并重新开始数据包的传输
    void            (*tx_timeout) (struct net_device *dev);

  // 当应用程序需要获得接口的统计信息时,将调用该函数
  struct net_device_stats* (*get_stats)(struct net_device *dev);


  /* 下面是可选设备操作 */

  // NAPI兼容驱动程序提供这个方法,在禁止中断时,以轮询模式操作接口
  int            (*poll) (struct net_device *dev, int *quota);

    // 当设备的组播列表发生改变,或者设备标识位发生改变时,将调用该方法
    void            (*set_multicast_list)(struct net_device *dev);
     // 如果接口支持硬件地址的改变,则可实现该方法
    int            (*set_mac_address)(struct net_device *dev, void *addr);

  // 执行接口的 ioctl 命令
    int            (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);

  // 在接口的 MTU 改变时,该函数负责采取相应的动作。
  // 如果驱动程序在用户改变MTU时需要完成某些特定工作,则应该声明自己的函数,否则默认的函数可正确实现相关处理
    int            (*change_mtu)(struct net_device *dev, int new_mtu);

    /* ... */
};

网络设备的创建和注册

一个网络设备被使用前,需要先被创建成为一个 struct net_device 并注册。下面描述注册过程:

和其他所有内核数据结构一样,net_device 包含了一个 kobject 和引用计数,并且通过 sysfs 导出信息。由于与其他结构相关,因此它必须被动态分配。用来执行分配的内核函数是 alloc_netdev,其原型如下:

1
struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *));

函数参数定义如下:

  • sizeof_priv 是驱动程序的私有数据区的大小,这个区成员和 net_device 结构一同分配给网络设备。
  • name 是接口的名字,在用户空间可见,可以使用类似 printf%d 的格式
  • setup 是一个初始化函数,用来设置 net_device 结构剩余的部分

使用示例如下,注意必须要检查函数返回值,以确定分配工作完成:

1
2
3
4
struct net_device snull;
snull = alloc_netdev(sizeof(struct snull_priv), "sn%d", snull_init);
if (snull == NULL)
  goto out;

网络子系统针对 alloc_netdev 函数,为不同种类的接口封装了许多函数,最常用的是 alloc_etherdev

1
struct net_device *alloc_etherdev(int sizeof_priv);

该函数使用 eth%d 的形式指定分配给网络设备的名字,它提供了自己的初始化函数 ether_setup,用正确的值为以太网设备设置 net_device 中的许多成员。因此在驱动程序中没有为 alloc_etherdev 提供初始化函数。

一旦 net_device 结构体被初始化后,剩余的工作就是将该结构体传递给 register_netdev 函数:

1
int        register_netdev(struct net_device *dev);

网络设备的开启与关闭

net/core/dev.c

开启网络设备函数 dev_open(struct net_device *dev)。如果网络设备已经激活或者它尚未被注册,函数返回错误信息。

  • 判断设备是否激活;
  • 使用 set_bit 函数修改设备状态为**__LINK_STATE_START**,调用 net_device 中的 open 指向函数设置该设备;
  • 激活网络设备的队列和调度器;
  • 将事件(NETDEV_UP)登记到通知链:notifier_call_chain(&netdev_chain,事件,dev)。

关闭网络设备函数 dev_close(struct net_device *dev)

  • 如果网络设备未被激活,则不需要关闭;
  • 将事件(NETDEV_GOING_DOWN)登记到通知链:notifier_call_chain(&netdev_chain,事件名,dev);
  • 删除包调度器中的相应信息:dev_deactivate;
  • 清除设备的活动状态:clear_bit;
  • 调用 net_device 中的 stop 指向函数,执行停止操作。

参考资料

  • Understanding Linux Kernel Internals:Part II