意义是:返璞归真,我们只是在重复着历史的循环。提高IO效率的根本方向只有2个:(1)让内核足够小;(2)让内核足够大。
大背景:随着网络的发展,IO密集型业务的增长,操作系统的内核逐渐从“好用的工具”变成“碍事的管家”,于是行业开始思考独立于内核之外实现更高效率的IO。
以下为方便理解的比喻、简化模型,不是严谨的说明。
思考两个问题:
- 如果没有操作系统,如何写一个超简单的Request-Response服务?
- 如果DPDK平台上要跑多个第三方应用,如何公平运行这些应用?如何防止“不靠谱”的恶意应用搞破坏?
我们先只考虑1,如果没有操作系统的话,我们会怎么写服务器。
STEP 1:时光退回50年前,假设我们有台单核服务器,上面只跑1个服务,这个服务非常轻量、通信量充分小、计算负载充分低,而且不存在信任与安全的问题。
最简单的做法是,当网卡收到请求抛出硬件中断时,CPU(假设DMA还没有发明出来)将网卡缓冲区的IP包数据读出,解析请求,执行请求,再将响应数据写入网卡缓冲区,发送。
STEP 2:业务开始变复杂,CPU处理请求的时间已经不能忽略不计了,但是硬件中断必须马上响应完,否则有可能丢失请求。
为了解决这个问题,我们先写了一个基础框架:让CPU可以交替执行2个程序A和B,如果网卡有数据请求过来,就保存CPU现场立即强制切换到执行A。
程序A的功能是:收数据包但不立即处理请求,而是把收到的数据缓存到内存
程序B的功能是:从内存里取出缓存的数据包,执行处理请求
只要我们的基础框架和程序A的性能不拉跨,基本不用担心丢失请求了。
现在开始考虑问题2。
STEP 3:业务更复杂了,我们需要在这台服务器上跑3个程序A、B、C。A还是上面那个框架,B是我们自研的应用,C是外包给外包商开发的程序,而且这个开发商好像还有点不太靠谱。
为了解决这个问题,我们先把内存分成两半,一半只允许程序A使用,另一半再劈两半,分别允许B和C使用。然后再改造一下CPU,让程序B和C只能访问各自段的段内内存,如果想跨段访问,必须调用程序A的接口进行申请,由A来代理访问。
由于开发程序B的外包商不太靠谱,我们还要防止程序B“偷听”本来要发给A的请求。于是,我们给IP消息编个号加到头部,发给B的消息是1号,发给C的消息是2号,再规定所有消息只能由程序A接收。程序B和C向程序A申请提取消息时,程序A会核对编号;程序B和C定时向程序A询问是否有发给自己的消息,如果有,就把消息从程序A复制到自己的内存,然后再处理执行。我们管这种带编号的消息起名叫UDP报文。
STEP 4:通信量越来越大,程序A光从网卡取数据就能把CPU累死,于是我们又制造了一个硬件DMA,协助CPU做枯燥无味的拷贝工作。
STEP 5:通信量又大了。消息从程序A拷贝到程序B和C的开销已经大到不可忽视。程序B和C都觉得程序A“太碍事了”“管得太宽,手伸得太长”,抗议A“滥权”的呼声越来越高。于是程序A改造了一下,决定“放权”。程序A允许B向A申请一段共享内存,程序A把消息往共享内存里转圈写入,B就可以不从A拷贝数据直接看到内容了;同理,B想发什么数据,直接往共享内存里写入,程序A会转圈处理程序B写入的消息。发明这个机制的人姓U,所以人们把这种机制命名为“uring”。
STEP 6:通信量更大了,而且还更频繁。而且人们发现,程序A、B、C之间互相通信的开销也非常大。于是程序B和C合计了一下,“A还是太碍事了,咱们把A踢了单干如何?走,去问问网卡同不同意!”