<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="https://www.gentoo-zh.org/extern.php?action=feed&amp;tid=479&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / linux源码解读（三十二）：dpdk原理概述（一）]]></title>
		<link>https://www.gentoo-zh.org/viewtopic.php?id=479</link>
		<description><![CDATA[linux源码解读（三十二）：dpdk原理概述（一） 最近发表的帖子。]]></description>
		<lastBuildDate>Wed, 12 Oct 2022 02:49:54 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[linux源码解读（三十二）：dpdk原理概述（一）]]></title>
			<link>https://www.gentoo-zh.org/viewtopic.php?pid=486#p486</link>
			<description><![CDATA[<p>1、操作系统、计算机网络诞生已经几十年了，部分功能不再能满足现在的业务需求。如果对操作系统做更改，成本非常高，所以部分问题是在应用层想办法解决的，比如前面介绍的协程、quic等，都是在应用层重新开发的框架，简单回顾如下：</p><p>&#160; &#160; 协程：server多线程通信时，如果每连接一个客户端就要生成一个线程去处理，对server硬件资源消耗极大！为了解决多线程以及互相切换带来的性能损耗，应用层发明了协程框架：单线程人为控制跳转到不同的代码块执行，避免了cpu浪费、线程锁/切换等一系列耗时的问题！<br />&#160; &#160; quic协议：tcp协议已经深度嵌入了操作系统，更改起来难度很大，所以同样也是在应用层基于udp协议实现了tls、拥塞控制等，彻底让协议和操作系统松耦合！</p><p>&#160; &#160;除了上述问题，操作还有另一个比较严重的问题：基于os内核的网络数据IO！传统做网络开发时，接收和发送数据用的是操作系统提供的receive和send函数，用户配置一下网络参数、传入应用层的数据即可！操作系统由于集成了协议栈，会在用户传输的应用层数据前面加上协议不同层级的包头，然后通过网卡发送数据；接收到的数据处理方式类似，按照协议类型一层一层拨开，直到获取到应用层的数据！整个流程大致如下：</p><p>&#160; 网卡接受数据-----&gt;发出硬件中断通知cpu来取数据-----&gt;os把数据复制到内存并启动内核线程<br />&#160; &#160; &#160; &#160; &#160; ---&gt;软件中断---&gt;内核线程在协议栈中处理包---&gt;处理完毕通知用户层</p><p>&#160; 大家有没有觉得这个链条忒长啊？这么长的处理流程带来的问题：</p><p>&#160; &#160; “中间商”多，整个流程耗时；数据进入下一个环节时容易cache miss<br />&#160; &#160; 同一份数据在内存不同的地方存储（缓存内存、内核内存、用户空间的内存），浪费内存<br />&#160; &#160; 网卡通过中断通知cpu，每次硬中断大约消耗100微秒，这还不算因为终止上下文所带来的Cache Miss（L1、L2、TLB等cpu的cache可能都会更新）<br />&#160; &#160; 用户到内核态的上下文切换耗时<br />&#160; &#160; 数据在内核态用户态之间切换拷贝带来大量CPU消耗，全局锁竞争<br />&#160; &#160; 内核工作在多核上，为保障全局一致，即使采用Lock Free，也避免不了锁总线、内存屏障带来的性能损耗</p><p>&#160; &#160; &#160;这一系列的问题都是内核处理网卡接收到的数据导致的。大胆一点想象：如果不让内核处理网卡数据了？能不能避免上述各个环节的损耗了？能不能让3环的应用直接控制网卡收发数据了？</p><p>&#160; &#160; &#160; &#160;2、如果真的通过3环应用层直接读写网卡，面临的问题：</p><p>&#160; &#160; &#160; &#160;用户空间的内存要映射到网卡，才能直接读写网卡<br />&#160; &#160; &#160; &#160;驱动要运行在用户空间</p><p>&#160; &#160; &#160;（1）这两个问题是怎么解决的了？这一切都得益于linux提供的UIO机制！ UIO 能够拦截中断，并重设中断回调行为（相当于hook了，这个功能还是要在内核实现的，因为硬件中断只能在内核处理），从而绕过内核协议栈后续的处理流程。这里借用别人的一张图：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220321181333273-1395979647.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;UIO 设备的实现机制其实是对用户空间暴露文件接口，比如当注册一个 UIO 设备 uioX，就会出现文件 /dev/uioX（用于读取中断，底层还是要在内核处理，因为硬件中断只能发生在内核），对该文件的读写就是对设备内存的读写（通过mmap实现）。除此之外，对设备的控制还可以通过 /sys/class/uio 下的各个文件的读写来完成。所以UIO的本质：</p><p>&#160; &#160; &#160;让用户空间的程序拦截内核的中断，更改中断的handler处理函数，让用户空间的程序第一时间拿到刚从网卡接收到的“一手、热乎”数据，减少内核的数据处理流程！由于应用程序拿到的是网络链路层（也就是第二层）的数据，这就需要应用程序自己按照协议解析数据了！说个额外的：这个功能可以用来抓包！</p><p>&#160; 简化后的示意图如下：原本网卡是由操作系统内核接管的，现在直接由3环的dpdk应用控制了！<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220321211919637-1452026464.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;这就是dpdk的第一个优点；除了这个，还有以下几个：</p><p>&#160; &#160;（2）Huge Page 大页：传统页面大小是4Kb，如果进程要使用64G内存，则64G/4KB=16000000（一千六百万）页，所有在页表项中占用16000000 * 4B=62MB；但是TLB缓存的空间是有限的，不可能存储这么多页面的地址映射关系，所以可能导致TLB miss；如果改成2MB的huge Page，所需页面减少到64G/2MB=2000个。在TLB容量有限的情况下尽可能地多在TLB存放地址映射，极大减少了TLB miss！下图是采用不同大小页面时TLB能覆盖的内存对比！<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202203/2052730-20220323110657814-1540225820.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;（3）mempool 内存池：任何网络协议都要处理报文，这些报文肯定是存放在内存的！申请和释放内存就需要调用malloc和free函数了！这两个是系统调用，涉及到上下文切换；同时还要用buddy或slab算法查找空闲内存块，效率较低！dpdk 在用户空间实现了一套精巧的内存池技术，内核空间和用户空间的内存交互不进行拷贝，只做控制权转移。当收发数据包时，就减少了内存拷贝的开销！</p><p> （4）Ring 无锁环：多线程/多进程之间互斥，传统的方式就是上锁！但是dpdk基于 Linux 内核的无锁环形缓冲 kfifo 实现了自己的一套无锁机制，支持多消费者或单消费者出队、多生产者或单生产者入队；</p><p>&#160; &#160;（5）PMD poll-mode网卡驱动：网络IO监听有两种方式，分别是</p><p>&#160; &#160; 事件驱动，比如epoll：这种方式进程让出cpu后等数据；一旦有了数据，网卡通过中断通知操作系统，然后唤醒进程继续执行！这种方式适合于接收的数据量不大，但实时性要求高的场景；<br />&#160; &#160; 轮询，比如poll：本质就是用死循环不停的检查内存有没有数据到来！这种方式适合于接收大块数据，实时性要求不高的场景；</p><p>&#160; 总的来说说：中断是外界强加给的信号，必须被动应对，而轮询则是应用程序主动地处理事情。前者最大的影响就是打断系统当前工作的连续性，而后者则不会，事务的安排自在掌握！</p><p>&#160; dpdk采用第二种轮询方式：直接用死循环不停的地检查网卡内存，带来了零拷贝、无系统调用的好处，同时避免了网卡硬件中断带来的上下文切换（理论上会消耗300个时钟周期）、cache miss、硬中断执行等损耗！</p><p>&#160; &#160; &#160;（6）NUMA：dpdk 内存分配上通过 proc 提供的内存信息，使 CPU 核心尽量使用靠近其所在节点的内存，避免了跨 NUMA 节点远程访问内存的性能问题；其软件架构去中心化，尽量避免全局共享，带来全局竞争，失去横向扩展的能力</p><p>&#160; （7）CPU 亲和性： dpdk 利用 CPU 的亲和性将一个线程或多个线程绑定到一个或多个 CPU 上，这样在线程执行过程中，就不会被随意调度，一方面减少了线程间的频繁切换带来的开销，另一方面避免了 CPU L1、L2、TLB等缓存的局部失效性，增加了 CPU cache的命中率。</p><p>&#160; &#160;3、一个简单的数据接收demo，主要是对网络中的数据进行一层一层解包：</p><div class="codebox"><pre class="vscroll"><code>int main(int argc, char *argv[])
{
    
    // 初始化环境，检查内存、CPU相关的设置，主要是巨页、端口的设置
    if (rte_eal_init(argc, argv) &lt; 0)
    {
    
        rte_exit(EXIT_FAILURE, &quot;Error with EAL init\n&quot;);
    }

    // 内存池初始化，发送和接收的数据都在内存池里
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
        &quot;mbuf pool&quot;, NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
    if (NULL == mbuf_pool)
    {
    
        rte_exit(EXIT_FAILURE, &quot;Could not create mbuf pool\n&quot;);
    }

    // 启动dpdk
    ng_init_port(mbuf_pool);

    while (1)
    {
    
        // 接收数据
        struct rte_mbuf *mbufs[BURST_SIZE];
        unsigned num_recvd = rte_eth_rx_burst(gDpdkPortId, 0, mbufs, BURST_SIZE);
        if (num_recvd &gt; BURST_SIZE)
        {
    
            // 溢出
            rte_exit(EXIT_FAILURE, &quot;Error receiving from eth\n&quot;);
        }

        // 对mbuf中的数据进行处理
        unsigned i = 0;
        for (i = 0; i &lt; num_recvd; i++)
        {
    
            // 得到以太网中的数据
            struct rte_ether_hdr *ehdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
            // 如果不是ip协议
            if (ehdr-&gt;ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4))
            {
    
                continue;
            }
            struct rte_ipv4_hdr *iphdr =
                rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));

            // 接收udp的数据帧
            if (iphdr-&gt;next_proto_id == IPPROTO_UDP)
            {
    
                struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);

                uint16_t length = ntohs(udphdr-&gt;dgram_len);
                // udp data copy to buff
                uint16_t udp_data_len = length - sizeof(struct rte_udp_hdr) + 1;
                char buff[udp_data_len];
                memset(buff, 0, udp_data_len);
                --udp_data_len;
                memcpy(buff, (udphdr + 1), udp_data_len);

                //源地址
                struct in_addr addr;
                addr.s_addr = iphdr-&gt;src_addr;
                printf(&quot;src: %s:%d, &quot;, inet_ntoa(addr), ntohs(udphdr-&gt;src_port));

                //目的地址+数据长度+数据内容
                addr.s_addr = iphdr-&gt;dst_addr;
                printf(&quot;dst: %s:%d, %s\n&quot;,
                       inet_ntoa(addr), ntohs(udphdr-&gt;dst_port), buff);

                // 用完放回内存池
                rte_pktmbuf_free(mbufs[i]);
            }
        }
    }
}</code></pre></div><p> </p><p> </p><p> </p><p> </p><p>参考：</p><p>1、https://cloud.tencent.com/developer/article/1198333&#160; 一文看懂dpdk<br />2、https://www.cnblogs.com/bakari/p/8404650.html dpdk全面分析<br />3、https://www.bilibili.com/video/BV1VR4y1W7zy?spm_id_from=333.337.search-card.all.click&#160; dpdk原理<br />4、https://lwn.net/Articles/232575/&#160; uio机制<br />5、https://chowdera.com/2021/12/202112162035343569.html dpdk示例<br />6、https://cloud.tencent.com/developer/article/1736535 dpdk内存池</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 12 Oct 2022 02:49:54 +0000</pubDate>
			<guid>https://www.gentoo-zh.org/viewtopic.php?pid=486#p486</guid>
		</item>
	</channel>
</rss>
