dpdk用户态协议栈-手动实现Epoll

在之前的协议栈中,无论是tcp还是udp,都只能是单连接,无法并发地接收数据。想要解决这个问题,有以下几个方案:

  1. 开多个线程

  2. IO多路复用

下面我会分别实现一下Linux环境下上面三种做法,最后选择在REPStack中我们选择哪一种。这里直接用我以前写的代码了。

多线程做法

#define BUFFER_LENGTH       1024

//线程函数
void *client_thread(void *arg)
{
    int clientfd = *(int*)arg;

    while(1)
    {
        char buffer[BUFFER_LENGTH] = {0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);

        if(ret == 0)
        {
            close(clientfd);
            break;
        }
        printf("ret: %d, buffer: %s\n", ret, buffer);

        send(clientfd, buffer, ret, 0);
    }
}

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    while(1)
    {
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        pthread_t threadid;
        //将clientfd昨晚参数传入线程
        pthread_create(&threadid, NULL, client_thread, &clientfd);
    }
}

IO多路复用做法

#define BUFFER_LENGTH       1024

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    int epfd = epoll_create(1);

    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

    struct epoll_event events[1024] = {0};

    while(1)
    {
        //需要遍历的io数量
        int nready = epoll_wait(epfd, events, 1024, -1);
        printf("nready: %d\n", nready);
        if(nready < 0)
            continue;

        int i = 0;
        //遍历IO
        for(int i = 0; i < nready; i++)
        {
            int connfd = events[i].data.fd;

            if(sockfd == connfd)
            {
                int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
                if(clientfd < 0)
                    continue;

                printf("clientfd: %d\n", clientfd);

                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = clientfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
            }
            else if(events[i].events & EPOLLIN)
            {
                char buffer[BUFFER_LENGTH] = {0};
                short len = 0;
                recv(connfd, &len, 2, 0);
                len = ntohs(len);

                int n = recv(connfd, buffer, BUFFER_LENGTH, 0);
                if(n > 0)
                {
                    printf("recv: %s\n", buffer);
                    send(connfd, buffer, n, 0);
                }
                else if(n == 0)
                {
                    printf("close\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);

                    close(connfd);
                }
            }
        }
    }
}

原理和其他实现方式参考我写的这篇文章

EPOLL的实现

Epoll是Linux IO多路复用的一种IO管理机制。内核的实现代码在Linux内核源码的fs/eventpoll.c中。是比select和poll更高性能的一种IO管理机制。

在实现epoll之前,要先了解内核epoll的运行原理。内核的epoll可以从四方面来理解。

  1. Epoll 的数据结构,rbtree 对的存储,ready 队列存储就绪 io。

  2. Epoll的线程安全,SMP的运行,以及防止死锁。

  3. Epoll内核回调

  4. Epoll的LT和ET

Epoll数据结构

Epoll 主要由两个结构体:eventpoll 与 epitem。Epitem 是每一个 IO 所对应的的事件。比如

epoll_ctl EPOLL_CTL_ADD 操作的时候,就需要创建一个 epitem。Eventpoll 是每一个 epoll 所

对应的的。比如 epoll_create 就是创建一个 eventpoll。

  • Epitem定义

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240817225445.png

  • Eventpoll定义

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240817225536.png

数据结构示意图

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240817230146.png

List用来存储就绪的IO。当内核IO准备就绪的时候就会执行epoll_event_callback的回调函数,将epitem添加到List中。当epoll_wait激活重新运行的时候,将list的epitem逐一copy到events参数中。

RBtree用来存储所有io数据,方便快速通过io_fd查找。也从insert与remove来讨论。

当 App 执行 epoll_ctl EPOLL_CTL_ADD 操作,将 epitem 添加到 rbtree 中。当 App 执行 epoll_ctl EPOLL_CTL_ADD 操作,将 epitem 从rbtree中删除。

Epoll锁机制

// 获取自旋锁    
pthread_spin_lock(&ep->lock);
// epitem的rdy置1,代表epitem在就绪队列重,后续触发相同事件只需要修改event
epi->rdy = 1;
// 添加到list中
LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);
// 将eventpoll的rdnum加1
ep->rdnum ++;
// 释放spinlock
pthread_spin_unlock(&ep->lock);

Epoll回调

Epoll 的回调函数何时执行,此部分需要与 Tcp 的协议栈一起来阐述。Tcp 协议栈的时序图如

下图所示,epoll 从协议栈回调的部分从下图的编号 1,2,3,4。具体 Tcp 协议栈的实现,后续

从另外的文章中表述出来。下面分别对四个步骤详细描述

编号 1:是 tcp 三次握手,对端反馈 ack 后,socket 进入 rcvd 状态。需要将监听 socket 的

event 置为 EPOLLIN,此时标识可以进入到 accept 读取 socket 数据。

编号 2:在 established 状态,收到数据以后,需要将 socket 的 event 置为 EPOLLIN 状态。

编号 3:在 established 状态,收到 fin 时,此时 socket 进入到 close_wait。需要 socket 的 event 置为 EPOLLIN。读取断开信息。

编号 4:检测 socket 的 send 状态,如果对端 cwnd>0 是可以,发送的数据。故需要将 socket

置为 EPOLLOUT。

所以在此四处添加 EPOLL 的回调函数,即可使得 epoll 正常接收到 io 事件。

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/%E6%9F%A5%E7%9C%8B%E8%AF%84%E8%AE%BASequence.png

有点尴尬,这个图等我有空了再重新画一遍,我不知道他要vip才能无水印。

LT和ET

LT(水平触发)与 ET(边沿触发)是电子信号里面的概念。不清楚可以 man epoll 查看的。 如下图所示:

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240818002132.png

比如:event = EPOLLIN | EPOLLLT,将 event 设置为 EPOLLIN 与水平触发。只要 event 为 EPOLLIN

时就能不断调用 epoll 回调函数。

比如: event = EPOLLIN | EPOLLET,event 如果从 EPOLLOUT 变化为 EPOLLIN 的时候,就会触

发。在此情形下,变化只发生一次,故只调用一次 epoll 回调函数。关于水平触发与边沿触

发放在 epoll 回调函数执行的时候,如果为 EPOLLET(边沿触发),与之前的 event 对比,如

果发生改变则调用 epoll 回调函数,如果为 EPOLLLT(水平触发),则查看 event 是否为 EPOLLIN,

即可调用 epoll 回调函数。

效果演示

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240818004353.png

参考资料:https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值