纳秒级网络库【四】接口设计

代码发布在GitHub - QuarkCloud/quark-daemon: 低延迟网络库

写在正文之前,很多人不理解,为什么要专门提到接口设计。关键原因在于三点:

1、因为要控制延迟,所以并没有太多的腾挪空间。接口设计已经很大程度体现出系统底层设计思路。

2、操作系统底层差异性并不小。而应用接口还要保持一致,确保应用层无需修改。

3、应用接口一旦被广泛应用,时间越长就越难改。windows下,不少函数至少都有一个对应的Ex版本;Posix规范也有若干_r版本函数。

回到正题,我们知道网络库的API,大体可以分为两类。

1、创建类,比如accept、connect、close,单次调用耗时很长,但调用次数极少,在整个生命周期,基本只会被调用一次。对于这类函数,我倾向于url格式。比如tcp://1.2.3.4:5678或者udp://1.2.3.4:5678。调用方使用起来很方便,对于实现方,信息也足够,可以将无关内容隐藏起来。

2、IO类,比如send/recv,调用频次极高,实现方式直接影响到最终延迟大小,是本文讨论重点。


在之前的文章中,我们曾经讨论过,关于IOCP和EPOLL之间的差异。EPOLL是在系统底层可读可写时,通知应用层。IOCP是让应用层预先提供内存区,在读写完成后,通知应用层已经完成的字节数。IOCP的实现原理,有利于减少拷贝次数,这也是liburing的原理。

从send/wirte来说,发起方是由应用层主动发起的,所以分配内存的时机,是应用层可控的,只需要最终完成多少字节就可以了。

从recv/read来说,情况比较复杂。因为发起方是由对端,所以分配内存的时机是不可控的,也无法确定最终可读的字节数是多少。

从操作系统网络栈的实现逻辑来看,我们研究一下,数据包的接收过程。

1、网卡接收到网络帧,验证。这一步是必须执行,没有办法省略,这是物理层的范畴,通常不计入拷贝次数。网卡通过DMA拷贝数据到驱动层的环形队列,这是第一次复制,从网卡到驱动的RingBuffer的复制。

2、驱动层从环形队列中取出数据包,转换成内核层的sk_buff格式,再进入网络协议栈处理。这是第二次复制,从驱动层到网络协议栈。

3、协议栈这里处理协议头,必然存在一个缓冲区。linux在各个协议层都使用同一个结构sk_buff,因为已经预留足够空间,所以后续各个协议层,都可以共享这个结构。

4、从内核层拷贝到应用层,这一步常规网络库都要执行。这是第三次复制,但不是必须的。类似sendfile这样的零拷贝技术,主要就是为了绕过这一步,也避免用户层到内核层的切换。

从上面网络协议栈接收过程,我们可以看到,通常存在三次复制。不论是IOCP或者EPOLL,他们实际上只是在内核层到用户层的数据拷贝方式不同,并没有优化延迟。由于内核层本身已经有缓冲区,所以,无论recv什么时候分配内存,都不会丢失数据。但是,如果内核层没有缓冲区的情况下,就可能导致底层无法写入数据而丢失。

假设我们使用XDP技术,网卡支持Offload模式。XDP的native模式是运行在驱动层,性能要差点。对于offload模式,将数据直接从网卡拷贝到用户层。这时候,必须保证用户有类似驱动层的环形队列一直存在,见网络接收过程的第一步。如果在接收接口设计中,在用户层发起recv调用时,才指定内存,在网卡拷贝数据时,就存在这时候用户层还没有被调用的情况,于是数据就丢失了。

于是,我们在设计recv接口时,不能由外部指定缓冲区,而在底层确保接收缓冲区一直存在。

网络发送过程,和接收过程相反。发送过程是应用层主动发起的,我们无需确保底层缓冲区一直存在,而只需保证,在发送完成之前,外部提供的数据缓冲区要一直存在就可以了。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值