Docker 网络基础 | 虚拟网络设备对(veth)原理

290次阅读  |  发布于3年以前

在容器化大行其道的今天,Docker 可谓是容器界的宠儿。比起笨重的虚拟机,Docker 可谓是身轻如燕。当然,本文不是介绍虚拟机与 Docker 之间的优缺点,而是介绍 Docker 网络中重要的组成部分之一:

虚拟网络设备对:veth

在介绍 veth 前,我们先来介绍一下 网络命名空间(network namespace)

网络命名空间

网络命名空间 是 Linux 内核用来隔离不同容器间的网络资源(每个 Docker 容器都拥有一个独立的网络命名空间),网络命名空间主要隔离的资源包括:

如下图所示,当系统中拥有 3 个网络命名空间:

由于不同的网络命名空间之间是相互隔离的,所以不同的网络命名空间之间并不能直接通信。比如在 网络命名空间A 配置了一个 IP 地址为 172.17.42.1 的设备,但在 网络命名空间B 里却不能访问,如下图所示:

就好比两台电脑,如果没有任何网线连接,它们之间是不能通信的。所以,Linux 内核提供了 虚拟网络设备对(veth) 这个功能,用于解决不同网络命名空间之间的通信。

虚拟网络设备对(veth)

虚拟网络设备对 用于解决不同网络命名空间之间的通信,可以将其看成是两块有网线连接的网卡。只要将其中一块网卡放置到网络命名空间A,另外一块网卡放置到网络命名空间B,那么两个不同的网络命名空间就能够通信,如下图所示:

如上图所示,veth0veth1 组成一个虚拟网络设备对。虚拟网络设备对 就像管道一样,只要向其中一端发送数据,就可以从另外一端接收到数据。

Docker 就是使用 虚拟网络设备对 来实现不同容器之间的通信,其原理如下图:

从上图可以看出,每个容器之间并不是直接通过 虚拟网络设备对 来进行连接的,而是在主机上创建一个名为 docker0网桥,然后通过 虚拟网络设备对 来将各个容器连接到 网桥 上。网桥 有将多个 网络设备 连接起来的能力,就如现实中的 交换机 一样。

当然,本文的主题是 veth 的实现,而不是 网桥 的现实,所以对 网桥 的介绍就此结束,有兴趣可以参考《[Linux网桥工作原理与实现] 》一文。

虚拟网络设备对实现

在 Linux 内核中,使用 net_device 对象来表示一个网络设备。由于 veth 提供双向通信的功能,所以需要使用两个 net_device 对象来实现。由于 net_device 对象比较庞大,所以这里只列出本文相关的字段:

struct net_device
{
    char name[IFNAMSIZ];
    ...
    const struct net_device_ops *netdev_ops;
    ...
}

下面介绍一下这两个字段的作用:

由于 veth 由两个 net_device 对象组成的,所以这两个 net_device 对象应该有指向对方的指针。但通过查阅代码,并没发现有指向对方的指针,那么内核是怎么实现 veth 的呢?

虽然 Linux 内核使用 net_device 对象来表示一个网络设备,但由于不同厂商的网络设备可能存在各种差异,所以为了让 Linux 内核能够适应各种网络设备,故为不同的网络设备提供私有数据的存储空间。

也就是说,一个网络设备除了拥有 net_device 部分外,还有其私有数据部分。不同的网络设备其私有数据部分不同,而网络设备的私有数据部分存一般放在 net_device 对象的结束位置,如下图所示:

上图展示了 PCMCIA网卡RTL-8139网卡 对应的私有数据部分存储的位置,PCMCIA网卡 的私有数据部分对应的是 pcnet_dev_t 结构,而 RTL-8139网卡 的私有数据部分对应的是 rtl8139_private 结构。

回到我们的主题,虚拟网络设备对 的私有数据部分由 veth_priv 结构表示,其定义如下:

struct veth_priv {
    struct net_device *peer;
    struct veth_net_stats *stats;
    ...
};

下面介绍一下 veth_priv 结构各个字段的作用:

veth_priv 结构可以看出,虚拟网络设备对 所属的两个设备对象是由 peer 字段来关联起来的,如下图所示:

1. 创建虚拟网络设备对

当使用 ip 命令创建一对 虚拟网络设备对 时,会触发调用 veth_newlink 函数来完成创建工作,其实现如下:

static int
veth_newlink(struct net_device *dev, struct nlattr *tb[], struct nlattr *data[])
{
    int err;
    struct net_device *peer;
    struct veth_priv *priv;
    char ifname[IFNAMSIZ];
    ...

    // 由于虚拟网络设备对是由两个网络设备组成,
    // dev 是虚拟网络设备对的其中一个网络设备,
    // 所以需要调用 rtnl_create_link() 函数创建的另外一个网络设备并保存到 peer 变量中.
    peer = rtnl_create_link(dev_net(dev), ifname, &veth_link_ops, tbp);
    ...

    priv = netdev_priv(dev);  // 获取 dev 的私有数据部分
    priv->peer = peer;        // 将其 peer 字段指向 peer

    priv = netdev_priv(peer); // 获取 peer 的私有数据部分
    priv->peer = dev;         // 将其 peer 字段指向 dev

    return 0;
}

上面代码经过精简后,保留了主要逻辑,所以 veth_newlink 主要完成以下工作:

就这样,一对 虚拟网络设备对 的创建就完成了。

2. 初始化虚拟网络设备对

当然,在创建 虚拟网络设备对 时还需要对其进行初始化,初始化过程由 veth_setup 函数完成,其实现如下:

static const struct net_device_ops veth_netdev_ops = {
    ...
    .ndo_start_xmit = veth_xmit,
    ...
};

static void veth_setup(struct net_device *dev)
{
    ...
    dev->netdev_ops = &veth_netdev_ops;
    ...
}

在初始化 虚拟网络设备对 时,最重要的是设置其操作函数集。而 net_device_ops 结构是网络设备的操作函数集结构,当向设备发送数据时,将会触发调用设备操作函数集的 ndo_start_xmit 方法。

veth_setup 函数将此方法设置为 veth_xmit,也就是说,当向 虚拟网络设备对 的其中一端发送数据时,将会调用 veth_xmit 函数来发送数据。

3. 向虚拟网络设备对发送数据

当向 虚拟网络设备对 的其中一端发送数据时,将会调用 veth_xmit 函数来完成发送过程,其实现如下:

static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct net_device *rcv = NULL;
    struct veth_priv *priv, *rcv_priv;
    ...

    // 获取发送数据设备的对端设备
    priv = netdev_priv(dev); 
    rcv = priv->peer;
    ...

    skb->tstamp.tv64 = 0;
    skb->pkt_type = PACKET_HOST;
    // 将数据包的接收设备设置为对端设备
    skb->protocol = eth_type_trans(skb, rcv);
    ...

    // 将数据包上送给内核协议栈
    netif_rx(skb);

    return NETDEV_TX_OK;
}

我们先来介绍一下 veth_xmit 函数各个参数的意义:

veth_xmit 函数的实现比较简单,主要完成以下工作:

我们通过下图来展示发送数据的过程:

如上图所示,当一个数据包从 虚拟网络设备对 的一端发送出去,会从其另外一端被接收,并上送到内核协议栈处理。

总结

由于 虚拟网络设备对 的出现,解决了容器间的通信问题。而本文主要分析了 虚拟网络设备对 的实现原理,但是有些细节并没有详细分析,如果有不懂的地方可以加我微信一起探讨。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8