Net/3 的接口层位于协议栈的最底部,负责和设备驱动交互,并为上层应用屏蔽物理层的差异。

接口层为上层提供 best effort 的服务。

ifnet

每个 ifnet 对应了一个网络设备,由于网络设备可能有多个地址,因此 ifnet 中的 ifaddr 也是是一个单向链表:

Diagram
Figure 1. ifnet-ifaddr

ifnet 结构

ifnet 的定义如下:

net/if.h
struct ifnet {
    char *if_name;              /* name, e.g. ``en'' or ``lo'' */ (1)
    struct ifnet *if_next;      /* all struct ifnets are chained */
    struct ifaddr *if_addrlist; /* linked list of addresses per if */ (2)
    int if_pcount;              /* number of promiscuous listeners */
    caddr_t if_bpf;             /* packet filter structure */
    u_short if_index;           /* numeric abbreviation for this if  */
    short if_unit;              /* sub-unit for lower level driver */
    short if_timer;             /* time 'til if_watchdog called */ (3)
    short if_flags;             /* up/down, broadcast, etc. */
    struct if_data {
        /* generic interface information */
        u_char ifi_type;               /* ethernet, tokenring, etc */
        u_char ifi_addrlen;            /* media address length */
        u_char ifi_hdrlen;             /* media header length */
        u_long ifi_mtu;                /* maximum transmission unit */
        u_long ifi_metric;             /* routing metric (external only) */
        u_long ifi_baudrate;           /* linespeed */
                                       /* volatile statistics */
        u_long ifi_ipackets;           /* packets received on interface */
        u_long ifi_ierrors;            /* input errors on interface */
        u_long ifi_opackets;           /* packets sent on interface */
        u_long ifi_oerrors;            /* output errors on interface */
        u_long ifi_collisions;         /* collisions on csma interfaces */
        u_long ifi_ibytes;             /* total number of octets received */
        u_long ifi_obytes;             /* total number of octets sent */
        u_long ifi_imcasts;            /* packets received via multicast */
        u_long ifi_omcasts;            /* packets sent via multicast */
        u_long ifi_iqdrops;            /* dropped on input, this interface */
        u_long ifi_noproto;            /* destined for unsupported protocol */
        struct timeval ifi_lastchange; /* last updated */
    } if_data;
    /* procedure handles */
    int(*if_init) /* init routine */
        __P((int));
    int(*if_output) /* output routine (enqueue) */
        __P((struct ifnet *, struct mbuf *, struct sockaddr *, struct rtentry *));
    int(*if_start) /* initiate output routine */
        __P((struct ifnet *));
    int(*if_done)              /* output complete routine */
        __P((struct ifnet *)); /* (XXX not used; fake prototype) */
    int(*if_ioctl)             /* ioctl routine */
        __P((struct ifnet *, u_long, caddr_t));
    int(*if_reset)
        __P((int));   /* new autoconfig will permit removal */
    int(*if_watchdog) /* timer routine */
        __P((int));
    struct ifqueue {
        struct mbuf *ifq_head;
        struct mbuf *ifq_tail;
        int ifq_len;
        int ifq_maxlen;
        int ifq_drops;
    } if_snd; /* output queue * (4)
};
#define if_mtu if_data.ifi_mtu (5)
#define if_type if_data.ifi_type (6)
#define if_addrlen if_data.ifi_addrlen
#define if_hdrlen if_data.ifi_hdrlen
#define if_metric if_data.ifi_metric (7)
#define if_baudrate if_data.ifi_baudrate
#define if_ipackets if_data.ifi_ipackets
#define if_ierrors if_data.ifi_ierrors
#define if_opackets if_data.ifi_opackets
#define if_oerrors if_data.ifi_oerrors
#define if_collisions if_data.ifi_collisions
#define if_ibytes if_data.ifi_ibytes
#define if_obytes if_data.ifi_obytes
#define if_imcasts if_data.ifi_imcasts
#define if_omcasts if_data.ifi_omcasts
#define if_iqdrops if_data.ifi_iqdrops
#define if_noproto if_data.ifi_noproto
#define if_lastchange if_data.ifi_lastchange
  1. 现在假设有两个以太网接口,则:

    • 两个以太网接口的 if_name 均为 ens。

    • 第一个以太网接口的 if_unit 为 0,第二个以太网接口的 if_unit 为 1.

      因此在 ifconfig 的显示中,第一个以太网接口显示为 ens0,第二个以太网接口显示为 ens1。
    • if_index 在内核中唯一地标示了接口。

  2. if_addr 链表。

  3. 以秒为单位记录时间,内核为此接口调用 if_watchdog 后归零。此函数用于收集接口统计或者是复位硬件。

  4. 接口分组输出队列。

  5. mtu。对于以太网时 1500。

  6. if_type 表明了接口支持的硬件地址类型:

    Table 1. if_types.h
    if_type作用

    IFT_ETHER

    CSMA/CD 以太网

    IFT_IOS88023

    IEEE 802.3 以太网

    IFT_ISO88025

    IEEE 802.5 令牌环网

    IFT_LOOP

    环回接口

  7. if_metric 通常为零。更大的值不利于通过从接口。

在 Net/3 中注释 /* XXX */ 用于提醒读者这段代码时易混淆的。包括不明确的副作用,或者一个更难问题的解决方案。

if_flags

if_flags 的详细定义如下:

#define IFF_UP          0x1             /* interface is up */
#define IFF_BROADCAST   0x2             /* broadcast address valid */
#define IFF_DEBUG       0x4             /* turn on debugging */
#define IFF_LOOPBACK    0x8             /* is a loopback net */
#define IFF_POINTOPOINT 0x10            /* interface is point-to-point link */
#define IFF_NOTRAILERS  0x20            /* obsolete: avoid use of trailers */
#define IFF_RUNNING     0x40            /* resources allocated */
#define IFF_NOARP       0x80            /* no address resolution protocol */
#define IFF_PROMISC     0x100           /* receive all packets */
#define IFF_ALLMULTI    0x200           /* receive all multicast packets */
#define IFF_OACTIVE     0x400           /* transmission in progress */
#define IFF_SIMPLEX     0x800           /* can't hear own transmissions */
#define IFF_LINK0       0x1000          /* per link layer defined bit */
#define IFF_LINK1       0x2000          /* per link layer defined bit */
#define IFF_LINK2       0x4000          /* per link layer defined bit */
#define IFF_ALTPHYS     IFF_LINK2       /* use alternate physical connection */
#define IFF_MULTICAST   0x8000          /* supports multicast */
在现代 Linux 中,需要区别 IFF_UP 和 IFF_RUNNING。IFF_UP 代表这个端口是可用的,而 IFF_RUNNING 代表这个端口是可用的且已经连接了网线。一个有意思的内容是 docker 网卡只设置了 IFF_UP 而没有设置 IFF_RUNNING。

ifaddr 结构

ifaddr 结构的定义要简洁很多:

net/if.h
struct ifaddr {
    struct sockaddr *ifa_addr;    /* address of interface */
    struct sockaddr *ifa_dstaddr; /* other end of p-to-p link */
#define ifa_broadaddr ifa_dstaddr /* broadcast address interface */
    struct sockaddr *ifa_netmask; /* used to determine subnet */
    struct ifnet *ifa_ifp;        /* back-pointer to interface */
    struct ifaddr *ifa_next;      /* next address for interface */
    void (*ifa_rtrequest)();      /* check or clean routes (+ or -)'d */
    u_short ifa_flags;            /* mostly rt_flags for cloning */
    short ifa_refcnt;             /* extra to malloc for link info */ (1)
    int ifa_metric;               /* cost of going out this interface */
#ifdef notdef
    struct rtentry *ifa_rt; /* XXXX for ROUTETOIF ????? */
#endif
};
  1. 引用计数

ifnet-ifaddr 中曾经提到过 ifaddr 是一个链表结构。

sockaddr 结构及其特化

一个接口的地址信息不仅包括了主机地址。Net/3 使用通用的 sockaddr 保存主机地址、广播地址和网络掩码。通过使用一个通用结构,将硬件和协议专用地址细节相对接口层隐藏起来。

下面时 sockaddr 的定义:

sys/socket.h
/*
 * Structure used by kernel to store most
 * addresses.
 */
struct sockaddr {
    u_char sa_len;    /* total length */ (1)
    u_char sa_family; /* address family */ (2)
    char sa_data[14]; /* actually longer; address value */ (3)
};
  1. 结构体总大小。

  2. 地址族类型:

    sa_family协议

    AF_INET

    Internet

    AF_ISO, AF_OSI

    OSI

    AF_UNIX

    Unix 域

    AF_ROUTE

    路由表

    AF_LINK

    数据链路

    AF_UNSPEC

  3. 如果地址长度大于 14B,则 sa_data 最高可以拓展到 253 字节(255 - sa_len - sa_family)。

    在 C 语言中,如果最后一个成员为 buffer 对象,则可以将其视为不定长对象。

sockaddr 本是是一个通用结构。此后,每个协议对其进行特化,创建自己需要的 sockaddr 对象。

网络初始化

下面演示了内核启动时网络初始化相关的步骤:

kern/init_main.c
void main(void *framep) {
    // ...
    cpu_startup(); (1)
    // ...

    /* Attach pseudo-devices. */
    for (pdev = pdevinit; pdev->pdev_attach != NULL; pdev++) (2)
        (*pdev->pdev_attach)(pdev->pdev_count);

    /*
     * Initialize protocols.  Block reception of incoming packets
     * until everything is ready.
     */
    s = splimp();
    ifinit(); (3)
    domaininit(); (4)
    splx(s);

    // ...
    /* The scheduler is an infinite loop. */
    scheduler();
    /* NOTREACHED */
}
  1. 查找并初始化所有硬件设备。

  2. pdevinit 中包含了 loopback 这样的伪接口。其完全由软件实现。

  3. 初始化网络接口。

  4. 初始化协议。

环回初始化

下面是环回接口的初始化过程:

net/if_loop.c
#define LOMTU (1024 + 512)

void loopattach(int n) {
    struct ifnet *ifp = &loif;

    ifp->if_name = "lo";
    ifp->if_mtu = LOMTU; (1)
    ifp->if_flags = IFF_LOOPBACK | IFF_MULTICAST; (2)
    ifp->if_ioctl = loioctl;
    ifp->if_output = looutput;
    ifp->if_type = IFT_LOOP;
    ifp->if_hdrlen = 0; (3)
    ifp->if_addrlen = 0; (4)
    if_attach(ifp);
    bpfattach(&ifp->if_bpf, ifp, DLT_NULL, sizeof(u_int)); (5)
}
  1. 将 mtu 设置为 1536B。

  2. 环回接口允许多播。

  3. loopback 没有链路首部。

  4. loopback 没有硬件地址。

  5. 向 bfp 注册 loopback 接口。

if_attach

if_attach 源码如下:

net/if.c
struct ifaddr **ifnet_addrs = NULL;

void if_attach(struct ifnet *ifp) {
    unsigned socksize, ifasize;
    int namelen, unitlen, masklen, ether_output();
    char workbuf[12], *unitname;
    register struct ifnet **p = &ifnet;
    register struct sockaddr_dl *sdl;
    register struct ifaddr *ifa;
    static int if_indexlim = 8;
    extern void link_rtrequest();

    while (*p)
        p = &((*p)->if_next);
    *p = ifp;
    ifp->if_index = ++if_index;
    if (ifnet_addrs == NULL || if_index >= if_indexlim) { (1)
        unsigned n = (if_indexlim <<= 1) * sizeof(ifa);
        struct ifaddr **q = (struct ifaddr **)malloc(n, M_IFADDR, M_WAITOK);
        if (ifnet_addrs) {
            bcopy((caddr_t)ifnet_addrs, (caddr_t)q, n / 2);
            free((caddr_t)ifnet_addrs, M_IFADDR);
        }
        ifnet_addrs = q;
    }
    /*
     * create a Link Level name for this device
     */
    unitname = sprint_d((u_int)ifp->if_unit, workbuf, sizeof(workbuf)); (2)
    namelen = strlen(ifp->if_name);
    unitlen = strlen(unitname);
#define _offsetof(t, m) ((int)((caddr_t) & ((t *)0)->m))
    masklen = _offsetof(struct sockaddr_dl, sdl_data[0]) +
              unitlen + namelen;
    socksize = masklen + ifp->if_addrlen;
#define ROUNDUP(a) (1 + (((a)-1) | (sizeof(long) - 1)))
    socksize = ROUNDUP(socksize);
    if (socksize < sizeof(*sdl))
        socksize = sizeof(*sdl);
    ifasize = sizeof(*ifa) + 2 * socksize;
    if (ifa = (struct ifaddr *)malloc(ifasize, M_IFADDR, M_WAITOK)) { (3)
        bzero((caddr_t)ifa, ifasize);
        sdl = (struct sockaddr_dl *)(ifa + 1);
        sdl->sdl_len = socksize;
        sdl->sdl_family = AF_LINK;
        bcopy(ifp->if_name, sdl->sdl_data, namelen); (4)
        bcopy(unitname, namelen + (caddr_t)sdl->sdl_data, unitlen);
        sdl->sdl_nlen = (namelen += unitlen);
        sdl->sdl_index = ifp->if_index;
        sdl->sdl_type = ifp->if_type;
        ifnet_addrs[if_index - 1] = ifa;
        ifa->ifa_ifp = ifp;
        ifa->ifa_next = ifp->if_addrlist;
        ifa->ifa_rtrequest = link_rtrequest; (5)
        ifp->if_addrlist = ifa;
        ifa->ifa_addr = (struct sockaddr *)sdl;
        sdl = (struct sockaddr_dl *)(socksize + (caddr_t)sdl);
        ifa->ifa_netmask = (struct sockaddr *)sdl;
        sdl->sdl_len = masklen;
        while (namelen != 0)
            sdl->sdl_data[--namelen] = 0xff;
    }
    /* XXX -- Temporary fix before changing 10 ethernet drivers */
    if (ifp->if_output == ether_output)
        ether_ifattach(ifp);
}
  1. 如果 ifnet_addrs 没有初始化,则对其进行初始化。如果容量不足,则分配二倍大小的 buffer,并将旧数据复制到新数组中。

    这里的 malloc 和 free 并不是 libc 中的函数。而是内核专用的。
  2. 将接口的 unit 值转为字符串。然后计算 if_name + unit 串的长度。

  3. 如果分配内存,忽略代码。

  4. 依次拼接 if_name 和 unit,并将其放在 sdl_data 中,将长度放在 sdl_nlen 中。接口的索引放在 sdl_index 中。

  5. 以太网使用 arp_rtrequest 取代 link_rtrequest,loopback 使用 loop_rtrequest。

ifinit

接口结构被初始化并链接在一起后,main 函数调用 ifinit:

net/if.c
void ifinit() {
    register struct ifnet *ifp;

    for (ifp = ifnet; ifp; ifp = ifp->if_next)
        if (ifp->if_snd.ifq_maxlen == 0) (1)
            ifp->if_snd.ifq_maxlen = ifqmaxlen;
    if_slowtimo(0); (2)
}
  1. 如果接口的 maxlen 没有初始化,则设置为 50。

  2. 启动接口的 watchdog 定时器。如果定时器到期,内核回调用这个接口的监视定时器函数。

以太网输入

以太网处理输入数据帧的流程如下:

Diagram

以太网接受发送向它的单播地址和以太网广播地址的帧。当一个完整的帧可用时,接口会产生一个中断,然后内核调用 leintr。

leintr 会调用 leread 借助 m_devget 将数据读到 mbuf 中。

Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx