Linux virtio-net driver

1,基本概念

virtio 是对半虚拟化 hypervisor 中的一组通用模拟设备的抽象。它允许 hypervisor 导出一组通用的模拟设备,并通过一个通用的应用编程接口(API)让它们变得可用。右图展示了为什么这很重要。有了半虚拟化 hypervisor 之后,客户操作系统能够实现一组通用的接口,在一组后端驱动程序之后采用特定的设备模拟。后端驱动程序不需要是通用的,因为它们只实现前端所需的行为。

注意,在现实中(尽管不需要),设备模拟发生在使用 QEMU 的空间,因此后端驱动程序与 hypervisor 的用户空间交互,以通过 QEMU 为 I/O 提供便利。QEMU 是一个系统模拟器,它不仅提供客户操作系统虚拟化平台,还提供整个系统(PCI 主机控制器、磁盘、网络、视频硬件、USB 控制器和其他硬件元素)的模拟。

2,概念层次

2.1,模块层次

virtio实际上可以被分为3个层次,从上到下依次为:

device specific layer:也就是上图的最上层,如virtio-net, virtio-blk等,是具体的设备驱动层。

virtio layer:上图的中间层,实现virtio queue及在其之上的buffer传输。

transport layer:上图的最下层,具体的物理传输层,比如pci, mmio, channel io。

2.2,对象层次

从客户操作系统的角度来看,对象层次结构 的定义如上图所示。在顶级的是 virtio_driver,它在客户操作系统中表示前端驱动程序。与该驱动程序匹配的设备由 virtio_device(设备在客户操作系统中的表示)封装。这引用 virtio_config_ops 结构(它定义配置 virtio 设备的操作)。virtio_device 由 virtqueue 引用(它包含一个到它服务的 virtio_device 的引用)。最后,每个 virtqueue 对象引用 virtqueue_ops 对象,后者定义处理 hypervisor 的驱动程序的底层队列操作。

virtio_driver 结构定义上层设备驱动程序、驱动程序支持的设备 ID 的列表、一个特性表单(取决于设备类型)和一个回调函数列表。当 hypervisor 识别到与设备列表中的设备 ID 相匹配的新设备时,将调用 probe 函数(由 virtio_driver 对象提供)来传入 virtio_device 对象。将这个对象和设备的管理数据缓存起来(以独立于驱动程序的方式缓存)。可能要调用 virtio_config_ops 函数来获取或设置特定于设备的选项,例如,为 virtio_blk 设备获取磁盘的 Read/Write 状态或设置块设备的块大小,具体情况取决于启动器的类型。

virtio_device 不包含到 virtqueue 的引用(但 virtqueue 确实引用了 virtio_device)。要识别与该 virtio_device 相关联的 virtqueue,需要结合使用 virtio_config_ops 对象和 find_vq 函数。该对象返回与这个 virtio_device 实例相关联的虚拟队列。find_vq 函数还允许为 virtqueue 指定一个回调函数(查看左 图中的 virtqueue 结构)。

virtqueue 是一个简单的结构,它识别一个可选的回调函数(在 hypervisor 使用缓冲池时调用)、一个到 virtio_device 的引用、一个到 virtqueue 操作的引用,以及一个引用要使用的底层实现的特殊 priv 引用。虽然 callback 是可选的,但是它能够动态地启用或禁用回调。

virtio缓冲池:客户操作系统(前端)驱动程序通过缓冲池与 hypervisor 交互。对于 I/O,客户操作系统提供一个或多个表示请求的缓冲池。例如,你可以提供 3 个缓冲池,第一个表示 Read 请求,后面两个表示响应数据。该配置在内部被表示为一个散集列表(scatter-gather),列表中的每个条目表示一个地址和一个长度。缓冲区的格式、顺序和内容仅对前端和后端驱动程序有意义。内部传输(当前实现中的连接点)仅移动缓冲区,并且不知道它们的内部表示。

该层次结构的核心是 virtqueue_ops,它定义在客户操作系统和 hypervisor 之间移动命令和数据的方式:

第一个函数 add_buf 用于向 hypervisor 提供请求。如前面所述,该请求以散集列表的形式存在。对于 add_buf,客户操作系统提供用于将请求添加到队列的 virtqueue、散集列表(地址和长度数组)、用作输出条目(目标是底层 hypervisor)的缓冲池数量,以及用作输入条目(hypervisor 将为它们储存数据并返回到客户操作系统)的缓冲池数量。当通过 add_buf 向 hypervisor 发出请求时,客户操作系统能够通过 kick 函数通知 hypervisor 新的请求。为了获得最佳的性能,客户操作系统应该在通过 kick 发出通知之前将尽可能多的缓冲池装载到 virtqueue。

第二个函数get_buf 处理自 hypervisor 的响应。客户操作系统仅需调用该函数或通过提供的 virtqueue callback 函数等待通知就可以实现轮询。当客户操作系统知道缓冲区可用时,调用 get_buf 返回完成的缓冲区。

最后两个函数enable_cb 和 disable_cb用来启用或禁用回调进程(通过在 virtqueue 中由 virtqueue 初始化的 callback 函数)。注意,该回调函数和 hypervisor 位于独立的地址空间中,因此调用通过一个间接的 hypervisor 来触发(比如 kvm_hypercall)。

3,virtio 驱动框架

Linux设备模型是由总线(bus_type),设备(device),驱动(device_driver)这三大数据结构来描述。在设备模型中,所有的设备都通过总线来连接。即使有些设备没有连接到一根物理上的总线,Linux也为其设置了一个内部的虚拟的总线,来维持总线,驱动,设备三者的关系。总线是处理器与一个或者多个设备之间的通道。

在这里有2层总线:pci总线和virtio总线。virtio设备物理上连接在pci物理总线上,逻辑上连接在virtio虚拟总线。做为pci设备便于资源分配与配置,逻辑设备模型中,便于管理与组织。

pci总线上挂接的设备对应了struct pci_dev结构,而对于pci总线上的virtio-pci设备,virtio提供了自己的驱动即struct pci_driver virtio_pci_driver。当virtio-pci设备挂到pci总线上或者virtio_pci_driver注册到pci总线上时,最终会调用virtio_pci_probe探测函数,这个函数会通过struct virtio_pci_device把struct pci_dev转化为struct virtio_device,再挂接到virtio总线上去。

virtio总线上挂接的设备对应了struct virtio_device结构,而对于virtio总线上的virtio-net设备,virtio-net提供了自己的驱动即struct virtio_driver virtio_net_driver。当virtio-net设备挂到virtio总线上或者virtio_net_driver注册到virtio总线上时,首先调用virtio bus的探测函数virtio_dev_probe找到驱动探测函数virtnet_probe。这个函数最终会通过register_netdev()把网络设备注册到linux网络协议栈。

3.1,virtio_pci_probe()

vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL);

pci_set_drvdata(pci_dev, vp_dev);
vp_dev->vdev.dev.parent = &pci_dev->dev;
vp_dev->vdev.dev.release = virtio_pci_release_dev;
vp_dev->pci_dev = pci_dev;
INIT_LIST_HEAD(&vp_dev->virtqueues);
spin_lock_init(&vp_dev->lock);

rc = pci_enable_device(pci_dev);           #开启bar空间的IO/Memory映射,开启INTx或MSI-X中断,进入D0电源状态等。
rc = virtio_pci_modern_probe(vp_dev);
pci_set_master(pci_dev);    #设置pci配置空间中的command.master位,表示此pci设备可作为主设备发起pci总线读写请求。
rc = register_virtio_device(&vp_dev->vdev);

struct virtio_pci_device这个结构的主要目的是充当struct pci_dev和struct virtio_device之间的桥梁。通过struct pci_dev *pdev->dev.driver_data可以找到struct virtio_pci_device,而通过struct virtio_pci_device *vp_dev->vdev可以找到struct virtio_device。进而三个数据结构彼此联系。

virtio_pci_modern_probe() 的主要作用是找到common_cfg, ISR status, notify base, device specific cfg这4个pci capability在pci配置空间中偏移以及所使用的bar空间,然后将它们映射进内核虚拟地址空间,分别对应到 vp_dev->common,vp_dev->isr,vp_dev->notify_base,vp_dev->device 4个起始地址位置,这样后续就可以直接以内存形式访问了。注意这些地址空间对应的是pci设备的内存,是不可cache的,对它们的读写会立即触发pci设备的响应行为,与一般的DDR内存不一样。另外此函数还会做如下初始化:

vp_dev->vdev.config = &virtio_pci_config_ops;    #操作pci配置空间的接口
vp_dev->config_vector = vp_config_vector;        #设备配置变化时应该使用的MSI-X中断向量
vp_dev->setup_vq = setup_vq;                     #创建队列的接口
vp_dev->del_vq = del_vq;                         #删除队列的接口

setup_vq实际上可以分为2各部分:第一部分是驱动创建队列的部分,这部分通过调用vring_create_virtqueue()实现,包括为队列分配descriptor table, avail ring, used ring。第二部分是设备创建队列的部分,这部分主要是操作pci配置空间common capability的queue_select,queue_size,queue_msix_vector,queue_enable,queue_notify_off,queue_desc,queue_avail,queue_used告诉设备关于队列的size, msix_vector, notify base, desc table, avail ring, used ring。

register_virtio_device() 的主要作用是将virtio device注册至virtio bus。设备挂接总线前做了一些初始化动作:首先是声明设备属于virtio总线。其次调用dev->config->reset()将pci配置空间common_cfg.device_status置为0即重新初始化设备。接着调用dev->config->set_status()在pci配置空间的common_cfg.device_status上设置ACKNOWLEDGE位,表示驱动已将设备识别为合法的设备。

dev->dev.bus = &virtio_bus;
device_initialize(&dev->dev);

dev_set_name(&dev->dev, "virtio%u", dev->index);
spin_lock_init(&dev->config_lock);
dev->config_enabled = false;
dev->config_change_pending = false;

dev->config->reset(dev);
virtio_add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);

INIT_LIST_HEAD(&dev->vqs);
spin_lock_init(&dev->vqs_list_lock);

err = device_add(&dev->dev);

最终通过调用device_add()将virtio设备挂接到virtio总线上。device_add是设备挂接总线的标准通用流程,所有总线都一样。挂接总线会有一个探测驱动的过程,即遍历注册到这个总线的所有驱动,调用总线的match函数,看设备的vendor和device是否匹配驱动声明的vendor和device,若匹配则调用驱动的probe函数进一步探测。

3.2,struct virtio_config_ops virtio_pci_config_ops

  • .get()/.set(): 读写device specific configuration,即struct virtio_pci_device *vp_dev->device指向的内存。
  • .get_status() / .set_status() :读写common configuration中的device status。
  • .reset() :将common configuration中的device status写为0以触发device reset。
  • .get_features() / .finalize_features() :读写common configuration中的device_feature和driver_feature。
  • .get_vq_affinity() / .set_vq_affinity() :修改中断的cpu亲和性,它实际上会去修改MSI-X table中的message address字段,从而改变MSI-X消息所投递到的cpu。
  • .find_vqs() -> vp_modern_find_vqs:创建virtqueue队列,并为每个队列分配中断,最后enable 所有queue,即将common cfg的queue_enable置1。
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
vp_find_vqs(vdev, nvqs, vqs, callbacks, names, ctx, desc);

list_for_each_entry(vq, &vdev->vqs, list)
	vp_modern_set_queue_enable(&vp_dev->mdev, vq->index, true);

vp_find_vqs()实际调用了vp_find_vqs_msix():它首先创建virtqueue队列,再为每个队列分配中断:

vp_dev->vqs = kcalloc(nvqs, sizeof(*vp_dev->vqs), GFP_KERNEL);

vp_request_msix_vectors(vdev, nvectors, per_vq_vectors, desc);

for (i = 0; i < nvqs; ++i) {
	vqs[i] = vp_setup_vq(vdev, queue_idx++, callbacks[i], names[i], ctx[i], msix_vec);

	request_irq(pci_irq_vector(vp_dev->pci_dev, msix_vec), vring_interrupt, 0,
				vp_dev->msix_names[msix_vec], vqs[i]);
}

vp_request_msix_vectors() -> pci_alloc_irq_vectors_affinity():这里主要是为每个队列申请一个中断号:这包括一个命令控制队列,n x (rx queue, tx queue),除了上述队列外,还为 设备配置变化申请了一个中断号,所有中断号都是msi-x中断号,其最终使用pci_enable_msix_range()接口申请。每个msi-x中断都可以指定cpu亲和性,见msi-x table的message address和message data。中断的cpu亲和性由参数struct irq_affinity *desc描述。命令控制队列和收发包队列的中断处理函数都是vring_interrupt。设备配置变化时会有一个中断,vp_request_msix_vectors()中为这个中断绑定一个中断处理函数vp_config_changed()。同时这个中断号也会通过vp_dev->config_vector()接口写入到common cfg capability的msix_config中去。

vp_setup_vq() -> setup_vq():它主要是调用vring_create_virtqueue创建struct virtqueue,然后把queue相关的信息写入common cfg capability:vp_dev->common->queue_size/queue_desc/queue_avail/ queue_used/queue_msix_vector。

学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂
更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,久学习,或点击这里加qun免费
领取,关注我持续更新哦! ! 

for (; num && vring_size(num, vring_align) > PAGE_SIZE; num /= 2) {
	queue = vring_alloc_queue(vdev, vring_size(num, vring_align), &dma_addr,
				GFP_KERNEL|__GFP_NOWARN|__GFP_ZERO);
}

vring_init(&vring, num, queue, vring_align);

vq = __vring_new_virtqueue(index, vring, vdev, weak_barriers, context,
				notify, callback, name);

to_vvq(vq)->split.queue_dma_addr = dma_addr;
to_vvq(vq)->split.queue_size_in_bytes = queue_size_in_bytes;
to_vvq(vq)->we_own_ring = true;

与队列相关的内存可以分为共享区和非共享区:struct vring中指向的desc table, avail ring, used ring称之为共享区,这一片内存既可以被guest os侧的前端驱动看到,也可以被host os侧的后端device看到。对这一块内存的操作必须遵照DMA原则,即它可能像IO空间一样随时变化,需注意其一致性。struct vring_virtqueue及其中的struct virtqueue称之为非共享区,它是guest os侧前端驱动所使用的辅助数据,并不会被后端device看到。对它们的访问无需考虑一致性问题。

上面代码中的vring_alloc_queue为desc table, avail ring, used ring所在的共享区分配内存。而vring_init则是根据上面分配的内存初始化struct vring中desc, avail, used的偏移。上面的__vring_new_virtqueue则是为队列的非共享区struct vring_virtqueue分配内存并初始化。

4,执行流

5,数据结构 

6,virtio_net_driver 

7,net_device_ops 

virtnet_open:在avail ring中填充用于接收报文的empty buffer,enable rx queue对应的napi。

for (i = 0; i < vi->max_queue_pairs; i++) {
	if (!try_fill_recv(vi, &vi->rq[i], GFP_KERNEL))
		schedule_delayed_work(&vi->refill, 0);

	virtnet_napi_enable(vi->rq[i].vq, &vi->rq[i].napi);
	virtnet_napi_tx_enable(vi, vi->sq[i].vq, &vi->sq[i].napi);
}

virtnet_set_mac_address:virtnet_send_command(vi, VIRTIO_NET_CTRL_MAC, CTRL_MAC_ADDR_SET, &sg, NULL),通过command queue向后端发送mac地址修改的命令。

virtnet_set_rx_mode:virtnet_send_command(vi, VIRTIO_NET_CTRL_RX, CTRL_RX_PROMISC | CTRL_RX_ALLMULTI | CTRL_MAC_TABLE_SET, sg, NULL),向后端发送命令进入混杂模式,接收所有组播报文, 并设置MAC地址过滤表。

virtnet_vlan_rx_add_vid:virtnet_send_command(vi, VIRTIO_NET_CTRL_VLAN, CTRL_VLAN_ADD, sg, NULL)向后端发送添加vlan的命令。

virtnet_select_queue:发送报文时,根据vi->vq_index为每个报文选择发送队列实际上是每个cpu对应一个tx queue,这个是在初始化时就确定好了的。

8,初始化:virtnet_probe

8.1,virtio_cread_feature(vdev, VIRTIO_NET_F_MQ, &max_queue_pairs),从virtio device specific配置空间内读取max_virtqueue_pairs,即使用多少个virtio queue。

8.2,创建net_device,设置dev->netdev_ops和dev->ethtool_ops。

8.3,决定 dev->features 和 dev->vlan_features。

8.4,如果支持从配置空间内读取MAC地址,则从配置空间内读取MAC地址到dev->dev_addr。如果不支持从配置空间内读取MAC地址,则生成一个随机的MAC地址。

8.5,INIT_WORK(&vi->config_work, virtnet_config_changed_work); 启动一个work监控link状态,若为down则停掉所有发送queue,若为up则启动所有发送queue。

8.6,根据协商的features决定是否以mergeable的形式收包,如果是(vi->mergeable_rx_bufs = true)则相应的virtio net header长度由10字节变为12字节。

8.7,决定最大队列数和当前队列数

8.8,init_vqs(vi):分配和初始化rx/tx queue

8.8.1,virtnet_alloc_queues(vi)

为收发队列分配内存,初始化refill的worker: refill_work,添加每个rx queue对应的napi。

8.8.2,virtnet_find_vqs(vi)

这一步主要是调用vi->vdev->config->find_vqs()创建队列。这个接口需要知道:1,要创建多少个队列vqs:包括命令控制队列,接收队列,发送队列。2,每个接收队列的接收完成中断函数,每个发送队列的发送完成中断函数:callbacks。3,每个队列的名称names。最终创建完成的收发队列存放在 vi->rq[i].vq 和 vi->sq[i].vq,命令控制队列存放在vi->cvq。

8.8.3,virtnet_set_affinity(vi), 实际是调用了vdev->config->set_vq_affinity()设置中断的cpu亲和性。

8.9,向操作系统注册virtio网络设备

8.10,virtio_device_ready(vdev),通知后端VM驱动已经ready。

8.11,try_fill_recv(见9.3),在avail ring中填充用于接收报文的empty buffer, vi->rq[i].num表示填充了多少个。

8.12,注册cpu热插拔时的回调函数

8.13,通过command queue通知后端当前enable的队列数 

8.14,从config读取link status 

如果支持VIRTIO_NET_F_STATUS则表示virtio device能够主动通知链路状态,所以先把设备设为link down状态netif_carrier_off(dev),然后启动一个工作队列监控virtio device的链路状态变化。schedule_work(&vi->config_work)。如果不支持VIRTIO_NET_F_STATUS则直接把设备设为link up状态netif_carrier_on(dev)。

9,收包:virtnet_poll

9.1,virtqueue_get_buf():从rx queue的used ring里取一个描述符,并返回其关联的收到的skb报文。

9.2,receive_buf():根据描述符信息填充skb,并将skb投递至TCP/IP协议栈。

9.2.1 通过描述符信息构建skb,(参考)

如果vi->mergeable_rx_bufs开启则调用receive_mergeable(): 当vi->mergeable_rx_bufs开启开启时第一个描述符内的内容一定是virtio_net_hdr,读取其中的hdr->mhdr.num_buffers获知描述符链表的总长度。读取描述符链表的每一个:page = virtqueue_get_buf(rq->vq, &len)。将其放入skb_shinfo(skb)->frags [ x ]中:set_skb_frag(skb, page, 0, &len)。

如果vi->big_packets开启则调用receive_big():skb = page_to_skb(rq, page, 0),分配一个skb,注意其数据缓冲区长度只有128字节,用于存放virt_net_hdr。由于第一个描述符内的内容是virtio_net_hdr,所以将virtio_net_hdr拷贝到skb中。如果支持GSO/GRO即vi->big_packets开启则此处收到的描述符是一个链表,链表中每个描述符对应一个页,这些页用page->private串联起来了。见add_recvbuf_big()。将包长度cover的所有描述符或页都放入skb_shinfo(skb)->frags [ x ]中。

如果上述2者都没有开启则调用receive_small():因为分配的时候就是分配的skb,所以这里直接将描述符缓冲区转换为skb,只不过要剪除前面的virtio_net_hdr头部。

9.2.2 根据virtio net header中csum相关字段初始化skb->ip_summed。

9.2.3 skb->protocol = eth_type_trans(skb, dev)。

9.2.4 根据virtio net header中GSO相关字段初始化skb_shinfo(skb)->gso_type/gso_size/gso_segs

9.2.5 napi_gro_receive(&rq->napi, skb) 将报文送至网络协议栈。

9.3,try_fill_recv():往rx queue的avail ring里填充用于接收新报文的empty buffer。

如果接收队列的empty buffer数量少于总队列长度的一半,则给接收队列填充empty buffer:try_fill_recv()。如果填充失败则启动工作队列过一会再填充schedule_delayed_work(&vi->refill, 0)。

如果前后端协商出mergeable的报文组织形式,则前端每次往avail ring填一个page(一个描述符)作为empty buffer。后端发送报文给前端时,在第一个描述符(即第一个page)的前面填入virtio net header,第一个描述符的剩余部分填报文payload,如果一页不够,再取一个描述符(另一页)继续填入报文payload。virtio net header->num_buffers指明了总共有几个缓冲区(virtio net header * 1+ payload * N)。第一个描述符中除virtio net header外的其余的报文payload被放入skb->data即线性区,后续描述符中的报文payload被放入skb->frags即非线性区。

如果前后端协商出big jumbo帧的报文组织形式,则前端每次往avail ring填17个page作为empty buffer,第一个page的前面sizeof(virtio_net_header)使用一个描述符,第一个page的剩余部分再使用一个描述符,后续16个page各使用一个描述符,总共使用18个描述符,这些描述符用desc->next串起来形成一个大的逻辑描述符。后端发送报文给前端时,在第一个描述符(即第一个page)的前面填入virtio net header,desc->next指向的后续描述符填报文payload,virtio net header加报文payload总共占用一个链式的逻辑描述符。前端收到报文时分配一个skb,将第一页的前sizeof(virtio net header)部分内容(即逻辑描述符的第一片内容)拷贝到skb的headroom内,再将第一页的剩余内容(即逻辑描述符的第二片内容)拷贝到skb->data即skb的线性区。后续页(即逻辑描述符的第三片及后续片)内容放入skb->frags即skb的非线性区。

如果前后端协商出small的报文组织形式,则前端每次分配一个skb,skb的headroom作为存放virtio net header的区域(对应逻辑描述符的第一片),skb->data作为存放报文payload的区域(对应逻辑描述符的第二片),所以总共会往avail ring填1个包含2片的逻辑描述符(用desc->next串起来)作为empty buffer。后端发送报文给前端时,在第一个描述符(即第一个page)的前面填入virtio net header,desc->next指向的后续描述符填报文payload,virtio net header加报文payload总共占用一个链式的逻辑描述符(仅有2片)。前端收到报文时将buffer转为skb,则skb->data就是报文payload。

给接收队列填充完empty buffer后调用virtqueue_kick敲doorbell通知后端设备。

9.4,napi_complete_done:如果没有报文可以接收了则打开中断disable qx queue对应的napi。

10,发包:start_xmit

qnum = skb_get_queue_mapping(skb);
struct netdev_queue *txq = netdev_get_tx_queue(dev, qnum);

free_old_xmit_skbs(sq, false);

xmit_skb(sq, skb);

if (sq->vq->num_free < 2+MAX_SKB_FRAGS) {
	netif_stop_subqueue(dev, qnum);
	free_old_xmit_skbs(sq, false);
}

if (kick || netif_xmit_stopped(txq)) {
	virtqueue_kick_prepare(sq->vq);
	virtqueue_notify(sq->vq);
}

10.1 free_old_xmit_skbs(sq):从tx queue的used ring取出已发送完毕的描述符,并释放描述符关联的skb。

10.2 xmit_skb(sq, skb)

如果virtio net header可以和报文共用一个描述符则在skb->data前面插入virtio net header,并设置can_push = true。

 在virtio net header中设置 csum offload 相关域。当需要做csum offload时virtio描述符里的csum_start指向校验和计算的起始位置,csum_offset指向校验和的存放位置,注意csum_offset指向的位置必须已经预先放好一个伪头部校验和,所以它是partial csum。

 在virtio net header中设置 GSO 相关域。当需要做GSO offload时virtio描述符里的hdr_len指明了截止到TCP payload的前面L2+L3+L4的头部的长度,这个头部在切片时需要copy至每个报文。而gso_size就是对TCP payload进行切片的尺寸。

 

如果virtio net header可以和报文共用一个描述符则在skb->data前面插入virtio net header,之后用skb->data 及 skb->frags 来初始化scatter_list sq->sg。

最后调用virtqueue_add_outbuf()发送包,如果有多个分片则这些分片会用多个描述符发送,这些描述符会用desc->next链接起来。

10.3,netif_stop_subqueue:如果发送队列里的空闲描述符数小于总队列长度的一半则停止这个发送队列,并调用free_old_xmit_skbs()释放更多buffer。

10.4,virtqueue_notify:通知virtio device。

11,中断

11.1,skb_recv_done,收报中断,表示有新报文收到

 

先调用virtqueue_disable_cb(rvq)关闭收包中断,再调用__napi_schedule(&rq->napi)调度rxq对应的napi。

11.2,skb_xmit_done,发包中断,表示报文发送完毕

 先调用virtqueue_disable_cb(rvq)关闭发包中断,再调用netif_wake_subqueue(vi->dev, vq2txq(vq))启动发送队列。

原文链接:https://zhuanlan.zhihu.com/p/540370469

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值