网络虚拟化场景下网络包的发送过程

网络虚拟化有和存储虚拟化类似的地方,例如,它们都是基于 virtio 的,因而在看网络虚拟化的过程中,会看到和存储虚拟化很像的数据结构和原理。但是,网络虚拟化也有自己的特殊性。例如,存储虚拟化是将宿主机上的文件作为客户机上的硬盘,而网络虚拟化需要依赖于内核协议栈进行网络包的封装与解封装。

当网络包经过客户机的协议栈到达 virtio_net 驱动的时候,按照 net_device_ops 的定义,start_xmit 会被调用。

static const struct net_device_ops virtnet_netdev = {
  .ndo_open            = virtnet_open,
  .ndo_stop          = virtnet_close,
  .ndo_start_xmit      = start_xmit,
  .ndo_validate_addr   = eth_validate_addr,
  .ndo_set_mac_address = virtnet_set_mac_address,
  .ndo_set_rx_mode     = virtnet_set_rx_mode,
  .ndo_get_stats64     = virtnet_stats,
  .ndo_vlan_rx_add_vid = virtnet_vlan_rx_add_vid,
  .ndo_vlan_rx_kill_vid = virtnet_vlan_rx_kill_vid,
  .ndo_xdp    = virtnet_xdp,
  .ndo_features_check  = passthru_features_check,
};

接下来的调用链为:start_xmit->xmit_skb-> virtqueue_add_outbuf->virtqueue_add,将网络包放入队列中,并调用 virtqueue_notify 通知接收方。

static netdev_tx_t start_xmit(struct sk_buff *skb, struct net_device *dev)
{
  struct virtnet_info *vi = netdev_priv(dev);
  int qnum = skb_get_queue_mapping(skb);
  struct send_queue *sq = &vi->sq[qnum];
  int err;
  struct netdev_queue *txq = netdev_get_tx_queue(dev, qnum);
  bool kick = !skb->xmit_more;
  bool use_napi = sq->napi.weight;
......
  /* Try to transmit */
  err = xmit_skb(sq, skb);
......
  if (kick || netif_xmit_stopped(txq))
    virtqueue_kick(sq->vq);
  return NETDEV_TX_OK;
}

bool virtqueue_kick(struct virtqueue *vq)
{
  if (virtqueue_kick_prepare(vq))
    return virtqueue_notify(vq);
  return true;
}

写入一个 I/O 会使得 qemu 触发 VM exit,这个逻辑我们在解析 CPU 的时候看到过。

接下来,那会调用 VirtQueue 的 handle_output 函数。前面我们已经设置过这个函数了,其实就是 virtio_net_handle_tx_bh。

static void virtio_net_handle_tx_bh(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIONet *n = VIRTIO_NET(vdev);
    VirtIONetQueue *q = &n->vqs[vq2q(virtio_get_queue_index(vq))];

    q->tx_waiting = 1;

    virtio_queue_set_notification(vq, 0);
    qemu_bh_schedule(q->tx_bh);
}

virtio_net_handle_tx_bh 调用了 qemu_bh_schedule,而在 virtio_net_add_queue 中调用 qemu_bh_new,并把函数设置为 virtio_net_tx_bh。

virtio_net_tx_bh 函数调用发送函数 virtio_net_flush_tx。

static int32_t virtio_net_flush_tx(VirtIONetQueue *q)
{
    VirtIONet *n = q->n;
    VirtIODevice *vdev = VIRTIO_DEVICE(n);
    VirtQueueElement *elem;
    int32_t num_packets = 0;
    int queue_index = vq2q(virtio_get_queue_index(q->tx_vq));

    for (;;) {
        ssize_t ret;
        unsigned int out_num;
        struct iovec sg[VIRTQUEUE_MAX_SIZE], sg2[VIRTQUEUE_MAX_SIZE + 1], *out_sg;
        struct virtio_net_hdr_mrg_rxbuf mhdr;

        elem = virtqueue_pop(q->tx_vq, sizeof(VirtQueueElement));
        out_num = elem->out_num;
        out_sg = elem->out_sg;
......
        ret = qemu_sendv_packet_async(qemu_get_subqueue(n->nic, queue_index),out_sg, out_num, virtio_net_tx_complete);
    }
......
    return num_packets;
}

virtio_net_flush_tx 会调用 virtqueue_pop。这里面,我们能看到对于 vring 的操作,也即从这里面将客户机里面写入的数据读取出来。

然后,我们调用 qemu_sendv_packet_async 发送网络包。接下来的调用链为:qemu_sendv_packet_async->qemu_net_queue_send_iov->qemu_net_queue_flush->qemu_net_queue_deliver。

在 qemu_net_queue_deliver 中,我们会调用 NetQueue 的 deliver 函数。前面 qemu_new_net_queue 会把 deliver 函数设置为 qemu_deliver_packet_iov。它会调用 nc->info->receive_iov。

static NetClientInfo net_tap_info = {
    .type = NET_CLIENT_DRIVER_TAP,
    .size = sizeof(TAPState),
    .receive = tap_receive,
    .receive_raw = tap_receive_raw,
    .receive_iov = tap_receive_iov,
    .poll = tap_poll,
    .cleanup = tap_cleanup,
    .has_ufo = tap_has_ufo,
    .has_vnet_hdr = tap_has_vnet_hdr,
    .has_vnet_hdr_len = tap_has_vnet_hdr_len,
    .using_vnet_hdr = tap_using_vnet_hdr,
    .set_offload = tap_set_offload,
    .set_vnet_hdr_len = tap_set_vnet_hdr_len,
    .set_vnet_le = tap_set_vnet_le,
    .set_vnet_be = tap_set_vnet_be,
};

根据 net_tap_info 的定义调用的是 tap_receive_iov。他会调用 tap_write_packet->writev 写入这个字符设备。

在内核的字符设备驱动中,tun_chr_write_iter 会被调用。

static ssize_t tun_chr_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
  struct file *file = iocb->ki_filp;
  struct tun_struct *tun = tun_get(file);
  struct tun_file *tfile = file->private_data;
  ssize_t result;

  result = tun_get_user(tun, tfile, NULL, from,
            file->f_flags & O_NONBLOCK, false);

  tun_put(tun);
  return result;
}

当我们使用 writev() 系统调用向 tun/tap 设备的字符设备文件写入数据时,tun_chr_write 函数将被调用。它会使用 tun_get_user,从用户区接收数据,将数据存入 skb 中,然后调用关键的函数 netif_rx_ni(skb) ,将 skb 送给 tcp/ip 协议栈处理,最终完成虚拟网卡的数据接收。

把网络虚拟化场景下网络包的发送过程总结一下。

  • 在虚拟机里面的用户态,应用程序通过 write 系统调用写入 socket。
  • 写入的内容经过 VFS 层,内核协议栈,到达虚拟机里面的内核的网络设备驱动,也即 virtio_net。
  • virtio_net 网络设备有一个操作结构 struct net_device_ops,里面定义了发送一个网络包调用的函数为 start_xmit。
  • 在 virtio_net 的前端驱动和 qemu 中的后端驱动之间,有两个队列 virtqueue,一个用于发送,一个用于接收。然后,我们需要在 start_xmit 中调用 virtqueue_add,将网络包放入发送队列,然后调用 virtqueue_notify 通知 qemu。
  • qemu 本来处于 KVM_RUN 的状态,收到通知后,通过 VM exit 指令退出客户机模式,进入宿主机模式。发送网络包的时候,virtio_net_handle_tx_bh 函数会被调用。
  • 接下来是一个 for 循环,我们需要在循环中调用 virtqueue_pop,从传输队列中获取要发送的数据,然后调用 qemu_sendv_packet_async 进行发送。
  • qemu 会调用 writev 向字符设备文件写入,进入宿主机的内核。
  • 在宿主机内核中字符设备文件的 file_operations 里面的 write_iter 会被调用,也即会调用 tun_chr_write_iter。
  • 在 tun_chr_write_iter 函数中,tun_get_user 将要发送的网络包从 qemu 拷贝到宿主机内核里面来,然后调用 netif_rx_ni 开始调用宿主机内核协议栈进行处理。
  • 宿主机内核协议栈处理完毕之后,会发送给 tap 虚拟网卡,完成从虚拟机里面到宿主机的整个发送过程。

此文章为12月Day2学习笔记,内容来源于极客时间《趣谈Linux操作系统》,推荐该课程。

你可能感兴趣的:(Linux,linux)