ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DPDK实现epoll

DPDK实现epoll 双方在发送数据之后对方会回复一个window窗口大小这个window的数据表示还可以接收多少数据那么下次对方发送的数据就不会超过这个window的大小。双方都需要维护两个window一个是自身的window表示自身还可以接收多少的数据另一个是对方的window表示这次最多可以发送多少数据也就是对方最多可以接受多少数据。tcp是双工的不能确保先发先到怎么确保对方接收的数据不会乱序通过序号seq与确认号ack初步判断是否乱序是否有丢失的数据。通过延迟确认收到信息后先不进行确认而是延迟一段时间再进行确认。还可以看一下有没有接收到更多的数据可以一起确认。如果我发送四个协议包0-200200-400400-600600-800.而对方接收到了0200、200-400、600-800没有接收到400-600的数据包。那么对方在回复确认信息时只会将ack400让对端重发后面两个包。发包有三个状态分别对应 已发送已确认、已发送未确认、未发送未确认。在数据缓冲区中有两个指针用来表示已发送已确认、未发送未确认。这两个指针之间的数据就是已发送未确认的数据。这两个指针会动态的前进形成一个窗口就是滑动窗口。延迟确认用作接收。滑动窗口用作发送。在发送数据的起始阶段发送数据的大小是呈指数增加的这个指数增加的阶段称之为慢启动。到了一个阈值之后就会线性增长增长到另一个阈值就会降低再线性增长称之为拥塞控制。慢启动和拥塞控制是发送方自己维护的。发送数据时间到接收到该数据确认的时间差称之为RTT。通过往返的时间RTT判断数据的发送情况如果RTT的值比较小说明通道顺畅下一次就可以多发送一些数据如果RTT的值比较大说明通道不通畅下一次就少发送一些数据。dpdk实现的tcp协议如何实现并发使用一请求一线程。主函数中已经开辟了三个线程再加上main线程一共四个线程。rte_eal_remote_launch(pkt_process, mbuf_pool, lcore_id); rte_eal_remote_launch(udp_server_entry, mbuf_pool, lcore_id); rte_eal_remote_launch(tcp_server_entry, mbuf_pool, lcore_id);dpdk实现的tcp网络协议栈有一个mainloop还有两个数据缓冲区rx_burst,tx_burst只需要从rx_burst中把数据取出来放进队列再从队列中取出数据放进tx_burst。是直接在网卡中原始数据放进rx_burst。pkt_process负责解析网络协议栈解析出来真正的数据。每一个连接都有一个sendbuf和recvbuf。使用一连接一线程的方式实现io并发。在用户态实现epoll从而实现io并发。思考题怎么实现多个epoll共存如果存在多个服务端dns、webserver、tcpserver、ssh就需要为每一种服务提供一个epoll所以需要多个epoll。epoll为什么选择红黑树epoll管理的io是不确定的也是不固定的。会频繁的查找所以需要的数据结构的查找性能必须良好。hash可以根据地址进行查找实现O(1)的时间复杂度但是需要提前开辟内存不适合数量不确定的情况。如果内存不够再分配内存的话是呈几何级增长的。为什么是几何级别这个不是靠用户自己实现的吗线性增长是不是也可以线性增长是来一个元素申请一块内存理论上hash也可以不过行呢个会退化到O(n)。Hash为了维护O(1)查找需要预留额外的空间。B树的查找性能不如红黑树而且B树的每一个节点是一块内存而不是一个元素。跳表实现起来更复杂。所以综合查找性能内存线性增长实现的复杂度选择了红黑树。红黑树查找性能良好、内存线性增长、允许不连续的内存。内核中的网络skbuff就是红黑树。hash适合业务简单但是数量极大的情况。比如大量的邮件通过邮件ID查找邮件。数据库索引使用hash建立索引机制。DNS域名与IP键值对的存储。epoll_create,epoll_ctl,epoll_wait接口的具体实现。见最下面。协议栈如何通知epoll模块通过回调函数epoll_event_callback通知epoll。什么条件从整集到就绪队列在epoll_event_callback中将epitem从整集中添加到就绪队列。epoll_event_callback什么时候触发协议栈接收到数据后需要通知epoll通过epoll_event_callback回调函数通知epollepoll_event_callback的功能就是将触发事件的socked加入就绪队列。epoll就可以通过epollwait在就绪队列中获取触发事件的socket。协议栈线程负责处理原始数据的协议解析与组包。应用线程负责获取解析好的数据进行业务处理。整集的数据结构就是红黑树将节点添加到就绪队列中时不是从整集中去除该元素再添加到就绪队列而是直接在原本的基础上加入到就绪队列这个元素可以既在整集中又在就绪队列中。对于epollwait(epfd,event,length,timeout)timeout0 一直阻塞timeout0 立即返回timeout0 阻塞指定的时间看到timeout立马联想到条件变量cond_wait有等待就有激活cond_single。当等待队列不为空的时候就cond_single把等待队列中的所有事件处理完成。timeout0就使用cond_wait然后通过sond_single唤醒。协议栈通过回调函数通知epoll。epoll一共四个函数31三个对外的接口面向用户一个对内的接口面向协议栈。epoll处于协议栈与用户层之间是内核文件系统的一部分。3epoll_create,epoll_ctl,epoll_wait1epoll_event_callbackepoll的线程安全加锁就是红黑树的线程安全使用大锁对整个红黑树进行加锁。main线程中的mainloop从网卡中批量获取原始数据rte_eth_rx_burst放进ring-in把ring-out中数据批量传输到网卡rte_eth_tx_burst检查是否有定时器到期有就执行定时器回调协议线程入口函数int pkt_process(void *arg)中的process loopptkpacket 数据包批量获取ring-in中的数据包进行协议栈(以太网、IP、UDP、TCP等)解析检查UDP、TCP的sendbuffer如果有需要发送的包就构造并放入ring-outudp和tcp的数据是自己解析非udp非tcp的数据通过kni交给内核处理。pkt_process不接触网卡只负责操作ring的in与out 和 socket队列。if (ehdr-ether_type rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) { struct rte_ipv4_hdr *iphdr rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr)); #if 1 // arp table ng_arp_entry_insert(iphdr-src_addr, ehdr-s_addr.addr_bytes); #endif if (iphdr-next_proto_id IPPROTO_UDP) { udp_process(mbufs[i]); // 53 -- // } else if (iphdr-next_proto_id IPPROTO_TCP) { ng_tcp_process(mbufs[i]); } else { rte_kni_tx_burst(global_kni, mbufs, num_recvd); //printf(tcp/udp -- rte_kni_handle_request\n); } } else { // ifconfig vEth0 192.168.0.119 up rte_kni_tx_burst(global_kni, mbufs, num_recvd); //printf(ip -- rte_kni_handle_request\n); }epoll一共四个函数31三个对外的接口面向用户一个对内的接口面向协议栈。3epoll_create(),epoll_ctl(),epoll_wait()1epoll_event_callback()epoll中有两个数据结构一个是红黑树一个是就绪队列。红黑树存放所有的io结点就绪队列存放event就绪的io。每一个fd对应一个epitem。首先理解两个结构体。struct eventpoll { // 一个 epoll 实例 int fd; // epoll 自己的 fd ep_rb_tree rbr; // 红黑树存所有被监听的 fd int rbcnt; // 红黑树节点数 LIST_HEAD(, epitem) rdlist; // 就绪链表有事件的 fd int rdnum; // 就绪数量 int waiting; // 是否有线程在 wait pthread_mutex_t mtx; // 保护红黑树 pthread_spinlock_t lock; // 保护就绪链表 pthread_cond_t cond; // 条件变量有事件时唤醒 wait pthread_mutex_t cdmtx; // 配合 cond 的互斥锁 }; struct epitem { // 一个被监听的 fd 对应一个 epitem RB_ENTRY(epitem) rbn; // 红黑树节点 LIST_ENTRY(epitem) rdlink; // 就绪链表节点 int rdy; // 是否已在就绪链表中 int sockfd; // 被监听的 socket fd struct epoll_event event; // 关注的事件 用户数据 };epoll实例与epollitem。一个epoll一个eventpoll。一个fd一个epitem。epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event)将sockid的event事件进行触发加入到就绪队列中。根据sockid查找epitem将这个epitem加入就绪队列唤醒nepoll_wait的线程查找后怎么设置的epitem的event没找到相关代码啊只有如果该epitem在就绪队列中就合并事件没有单独处理epitem的event。如果epitem在就绪链表上就合并事件如果不在就忽略event这个参数只把epitem加入就绪队列。epitem的event是在nepoll_ctl中设置的。nepoll_createint nepoll_create(int size)初始化一个epoll实例。分配epfd分配epollevent结构体挂到全局TCP表上初始化字段红黑树就绪链表等初始化四个原语操作为什么挂到全局TCP表上TCP表是什么有什么用TCP 表ng_tcp_table是一个全局单例里面存放着一些全局变量比如TCP连接的所有状态、epfd实例等。int nepoll_ctl(int epfd, int op, int sockid, struct epoll_event *event)ADD加锁查找是否已经在红黑树中分配epitem配置epitem的字段sockid、event等插入红黑树解锁DEL加锁查找是否已经在红黑树中从红黑树中移除释放该epitem解锁MOD在红黑树中查找该epitem修改事件并强制加上错误事件为什么强制加上错误事件为什么MOD不用加锁Linux内核定义的必须加上这两个状态。MOD也应该加锁。int nepoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)通过epfd找到对应的实例eventpoll加条件变量的互斥锁while (ep-rdnum 0 timeout ! 0)等待循环就绪队列为空且timeout不等于0等待标识置一ep-waiting 1如果timeout大于0等待指定的时间获取当前时间线程挂起pehtead_cond_timeouttimeout置零如果timeout小于0pthread_cond_wait一直等待等待标识置零ep-waiting 0条件变量解锁加自旋锁while循环判断就绪的事件从就绪队列中取出元素将事件epi-event加入到events数组中自旋锁解锁返回就绪的fd数量为什么使用while可能存在虚假唤醒single唤醒了多个线程所以被唤醒后需要重新检查条件。如果有5个线程都在工作此时来了新任务进行single唤醒但是没有线程被唤醒sigle丢失。率先处理完任务的线程会率先返回外部的大循环的顶部此时在wait前重新判断条件。综合wait前后都需要判断所以使用while。零如果timeout小于0pthread_cond_wait一直等待等待标识置零ep-waiting 0条件变量解锁加自旋锁while循环判断就绪的事件从就绪队列中取出元素将事件epi-event加入到events数组中自旋锁解锁返回就绪的fd数量为什么使用while可能存在虚假唤醒single唤醒了多个线程所以被唤醒后需要重新检查条件。如果有5个线程都在工作此时来了新任务进行single唤醒但是没有线程被唤醒sigle丢失。率先处理完任务的线程会率先返回外部的大循环的顶部此时在wait前重新判断条件。综合wait前后都需要判断所以使用while。文章参考零声教育的C/Clinux服务期高级架构系统教程学习:[0voice · GitHub
返回列表