怎么做到高性能网络IO?

简介: 怎么做到高性能网络IO?

为什么要做高性能网络IO。主要是解决c10,c10M问题


最开始的时候我们走的内核协议栈,走内核协议栈其实性能比较低,因为我们之前介绍的时候需要拷贝两次


但是我们采用用户态协议栈可以少拷贝一次,可以大大提高效率,

4.png

步骤:


1)客户端请求数据,先经过网卡,服务器需要从网卡copy数据到内核协议栈(tcp/bsd)。


2)再从内核协议栈copy数据到应用程序。


由此可见,客户端与应用程序之间的数据交互,多了两次数据拷贝的操作,在大量数据并发的情况下,必将会严重影响性能。


优化思路:可以跳过内核协议栈,去除拷贝操作,数据直接从网卡到应用程序,这种方式称为零拷贝。


但是我们这个在做完用户态协议栈在配合reactor的时候,会出现一个问题,是什么问题呢?


问题是:我们的epoll并不会通知这个事件

那么我们是怎么看待这个问题的呢,我们如果不了解epoll的原理和底层的话,我们一下也不知道为什么epoll不会通知,其实是因为epoll的通知是由内核通知的但是我们旁路之后,不走内核协议栈,那么内核协议栈就不会通知数据了


图例:(我们是在内核里创建了红黑树和fd的一些结构体信息,然后提供系统调用给用户)


协议栈解析出有数据来,通知到epoll中。应用程序操作epoll

5.png

所以我们在走用户态协议栈的时候,就不能用系统自带的epoll了,需要自己再用户态实现一个


epoll,进行管理


我们怎么设计epoll呢,我们采用红黑树结构是最好的,排除掉哈希表,优先队列,链表


就是红黑树最好了,为什么,因为红黑树这个数据结构更适合增删改查,效率也高,并且有


带有二叉搜索树的性质,所以很好用


需要查找性能高的数据结构,可选的数据结构有


  • hash:fd 数量不确定,创建 hash 消耗大量的内存。若 fd 数量较少时,内存浪费多,性能低


  • b/b+ 树:查找性能低于红黑树,降低树高,用于磁盘 io


  • rbtree:查找性能高,效率稳定,这里选用红黑树


还有一点就是epoll 监听的是系统 fd。而在自定义用户态协议栈的过程中,我们定义的 fd 只是个 int 值,并不指向内核打开文件表中对应的 i-node 结点


epoll 通过 fd 检测协议栈中的 tcb 有无事件发生,并对这些 fd 进行管理。

6.png7.png

自定义epoll 的主要结构体有


  • epitem:存储每个 io 对应的事件,每个注册到 epoll 池的 fd 对应1个 epitem
// 自定义的 epitem
struct epitem {
  RB_ENTRY(epitem) rbn;   // 红黑树的结点
  LIST_ENTRY(epitem) rdlink;  // 就绪队列,双向链表结点
  int rdy;           // 是否在就绪队列中
    int sockfd;          // 事件对应的sockfd
  struct epoll_event event; // 注册事件的类型 
};

eventpoll:用于管理1个 epoll 对象

// 自定义的 eventpoll
struct eventpoll {
  int fd;       // epfd
  ep_rb_tree rbr; // 红黑树的根结点
  int rbcnt;      
  LIST_HEAD( ,epitem) rdlist; // 就绪队列头结点
  int rdnum;          
  int waiting;  // epoll_wait判断是否正在等待
  pthread_mutex_t mtx;   //rbtree update
  pthread_spinlock_t lock; //rdlist update
  pthread_cond_t cond;  //block for event,用于epoll_wait的超时等待
  pthread_mutex_t cdmtx;  //mutex for cond
};

红黑树和双向链表共用结点 epitem。


双向链表采用的是就绪队列,在处理事件的时候,可以按先来先服务策略进行处理时间

8.png

2、epoll 锁机制


考虑两个公共资源:红黑树和就绪队列。


  • 红黑树:mutex,互斥锁


  • 就绪队列:spinlock,采用自旋锁,避免 SMP 体系下,多核竞争。


我们的红黑树的删除和修改和插入都是采用互斥锁的,因为不用锁的的话会发生线程安全问题,比如我们将epoll交给多个线程管理,那么当事件就绪的时候就会有惊群效应,如果此时


不加锁的话,那么多个线程会同时去处理这个事件,那么就会出现线程安全问题


3、epoll 用户态接口


epoll 为用户态提供的接口有:epoll_create, epoll_ctl, eoll_wait

 

3.1、epoll_create 的实现


功能: 创建 eventpoll 结构体

int epoll_create(int size) {
  if (size <= 0) return -1;
  // 从位图中获取新的fd,fd从3开始依次递增  
  int epfd =get_fd_frombitmap();
  struct eventpoll *ep = (struct eventpoll*)rte_calloc("eventpoll",1, sizeof(struct eventpoll), 0);
  if (!ep) {
    // 创建失败,将fd从位图中删除
    set_fd_frombitmap(epfd);
    return -1;
  }
  // 初始化红黑树和就绪队列
  ep->rbcnt = 0;
  RB_INIT(&ep->rbr);
  LIST_INIT(&ep->rdlist);
  if (pthread_mutex_init(&ep->mtx, NULL)) {
    rte_free(ep);
    set_fd_frombitmap(epfd);
    return -2;
  }
  if (pthread_mutex_init(&ep->cdmtx, NULL)) {
    pthread_mutex_destroy(&ep->mtx);
    rte_free(ep);
    set_fd_frombitmap(epfd);
    return -2;
  }
  if (pthread_cond_init(&ep->cond, NULL)) {
    pthread_mutex_destroy(&ep->cdmtx);
    pthread_mutex_destroy(&ep->mtx);
    rte_free(ep);
    set_fd_frombitmap(epfd);
    return -2;
  }
  if (pthread_spin_init(&ep->lock, PTHREAD_PROCESS_SHARED)) {
    pthread_cond_destroy(&ep->cond);
    pthread_mutex_destroy(&ep->cdmtx);
    pthread_mutex_destroy(&ep->mtx);
    rte_free(ep);
    set_fd_frombitmap(epfd);
    return -2;
  }
  return epfd;
}
3.2、epoll_ctl 的实现


功能:对红黑树进行增添,修改、删除。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) {
    // 通过fd查找到协议栈中对应的tcb连接,返回 eventpoll 对象,fd -> host
  struct eventpoll *ep = (struct eventpoll *)get_hostinfo_fromfd(epfd);
  // 若ep对象为空,或没有要设置的事件(del除外)
  if (!ep || (!event && op != EPOLL_CTL_DEL)) {
    errno = -EINVAL;
    return -1;
  }
  ///1、ADD 操作
  if (op == EPOLL_CTL_ADD) {
    pthread_mutex_lock(&ep->mtx);
    struct epitem tmp;
    tmp.sockfd = fd;
         // 在红黑树查找该结点 
    struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
    // 若红黑树已经存在该结点,返回
    if (epi) {
      pthread_mutex_unlock(&ep->mtx);
      return -1;
    }
    // 不存在,则创建 epitem 结点,并为其添加sockfd和事件
    epi = (struct epitem*)rte_calloc("epitem",1, sizeof(struct epitem), 0);
    if (!epi) {
      pthread_mutex_unlock(&ep->mtx);
      errno = -ENOMEM;
      return -1;
    } 
    epi->sockfd = fd;
    memcpy(&epi->event, event, sizeof(struct epoll_event));
    // 插入到红黑树中
    epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);
    assert(epi == NULL);
    // 红黑树结点数量增加
        ep->rbcnt ++;
    pthread_mutex_unlock(&ep->mtx);
  } 
  // 2、DEL 操作
  else if (op == EPOLL_CTL_DEL) {
    pthread_mutex_lock(&ep->mtx);
    struct epitem tmp;
    tmp.sockfd = fd;
    struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
        // 若红黑树中不存在该结点,直接返回
    if (!epi) {
      pthread_mutex_unlock(&ep->mtx);
      return -1;
    }
    // 存在该结点,则从红黑树中删除
    epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
    if (!epi) {
      pthread_mutex_unlock(&ep->mtx);
      return -1;
    }
    // 红黑树结点数量减少
    ep->rbcnt --;
         // 释放结点空间
    rte_free(epi);
    pthread_mutex_unlock(&ep->mtx);
  } 
    // 3、MOD 操作
    else if (op == EPOLL_CTL_MOD) {
    struct epitem tmp;
    tmp.sockfd = fd;
    struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
    // 该结点存在,则修改
    if (epi) {
      epi->event.events = event->events;
      epi->event.events |= EPOLLERR | EPOLLHUP;
    } 
    // 不存在,返回-1
    else {
      errno = -ENOENT;
      return -1;
    }
  } 
    // 4、非法操作
    else {
    assert(0);
  }
  return 0;
}
3.3、epoll_wait的实现


功能:等待 fd 就绪,监控就绪队列,若有数据,从内核拷贝数据到用户空间;若没有数据,阻塞。


等待的实现方法


等待规定的时间,条件变量 + pthread_cond_timedwait


一直等待(阻塞),条件变量 + pthread_cond_wait

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) {
    // 通过fd查找到协议栈中对应的tcb连接,返回 eventpoll 对象,fd -> host
  struct eventpoll *ep = (struct eventpoll *)get_hostinfo_fromfd(epfd);
  // 若ep对象为空,或没有要设置的事件(del除外)
  if (!ep || (!event && op != EPOLL_CTL_DEL)) {
    errno = -EINVAL;
    return -1;
  }
  ///1、ADD 操作
  if (op == EPOLL_CTL_ADD) {
    pthread_mutex_lock(&ep->mtx);
    struct epitem tmp;
    tmp.sockfd = fd;
         // 在红黑树查找该结点 
    struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
    // 若红黑树已经存在该结点,返回
    if (epi) {
      pthread_mutex_unlock(&ep->mtx);
      return -1;
    }
    // 不存在,则创建 epitem 结点,并为其添加sockfd和事件
    epi = (struct epitem*)rte_calloc("epitem",1, sizeof(struct epitem), 0);
    if (!epi) {
      pthread_mutex_unlock(&ep->mtx);
      errno = -ENOMEM;
      return -1;
    } 
    epi->sockfd = fd;
    memcpy(&epi->event, event, sizeof(struct epoll_event));
    // 插入到红黑树中
    epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);
    assert(epi == NULL);
    // 红黑树结点数量增加
        ep->rbcnt ++;
    pthread_mutex_unlock(&ep->mtx);
  } 
  // 2、DEL 操作
  else if (op == EPOLL_CTL_DEL) {
    pthread_mutex_lock(&ep->mtx);
    struct epitem tmp;
    tmp.sockfd = fd;
    struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
        // 若红黑树中不存在该结点,直接返回
    if (!epi) {
      pthread_mutex_unlock(&ep->mtx);
      return -1;
    }
    // 存在该结点,则从红黑树中删除
    epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
    if (!epi) {
      pthread_mutex_unlock(&ep->mtx);
      return -1;
    }
    // 红黑树结点数量减少
    ep->rbcnt --;
         // 释放结点空间
    rte_free(epi);
    pthread_mutex_unlock(&ep->mtx);
  } 
    // 3、MOD 操作
    else if (op == EPOLL_CTL_MOD) {
    struct epitem tmp;
    tmp.sockfd = fd;
    struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
    // 该结点存在,则修改
    if (epi) {
      epi->event.events = event->events;
      epi->event.events |= EPOLLERR | EPOLLHUP;
    } 
    // 不存在,返回-1
    else {
      errno = -ENOENT;
      return -1;
    }
  } 
    // 4、非法操作
    else {
    assert(0);
  }
  return 0;
}

4、epoll 回调


4.1、epoll 回调函数的实现


当内核 io 准备就绪的时候,执行 epoll 回调函数,将 epitem 添加到 rdlist 中,唤醒 epoll_wait。当 epoll_wait 被激活重新运行的时候,将 rdlist 的 epitem 逐一拷贝到 events 中,同时删除 rdlist 中对应的结点。换句话说, epoll_callback 是生产者,放入结点,唤醒 epoll_wait;epoll_wait 是消费者,消费结点。

// 从协议栈回调到epoll,把fd和对应的事件拷贝到应用程序
static int nepoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {
  struct epitem tmp;
  tmp.sockfd = sockid;
  // 在红黑树中查找 epitem
  struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
  if (!epi) {
    return -1;
  }
  // 已经在就绪队列中,只添加事件
  if (epi->rdy) {
    epi->event.events |= event;
    return 1;
  } 
  // 不在就绪队列,则将结点加入到就绪队列
  pthread_spin_lock(&ep->lock);
  epi->rdy = 1;
  LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);
  ep->rdnum ++;
  pthread_spin_unlock(&ep->lock);
  pthread_mutex_lock(&ep->cdmtx);
  // 就绪队列中增加结点,唤醒epoll_wait
  pthread_cond_signal(&ep->cond);
  pthread_mutex_unlock(&ep->cdmtx);
  return 0;
}
4.2、epoll 回调的时机


触发 epoll 回调4个时机,需要在这些地方添加 epoll 回调函数,使得 epoll 可以正常接收数据。


三次握手中,在 syn-rcvd 状态,对端返回 ack 后,tcb 结点放入到全连接队列,将对应的 sockfd 的置为 EPOLLIN 状态,等待 accept 取出,触发 epoll 回调。

if (stream->status == TCP_SYN_RCVD) {
    // 进入到 ESTABLISHED 状态
    stream->status = TCP_STATUS_ESTABLISHED;
  // 设置 epoll 回调函数,等待 accept
}

在 established 状态,收到数据后,将 sockfd 置为 EPOLLIN 状态,等待读取数据,触发epoll 回调

if (tcphdr->tcp_flags & TCP_PSH_FLAG) {
    // 建立连接后,push 接收数据,设置 epoll 回调函数
} 

在 established 状态,收到 fin 时,进入到 close_wait 状态。将 sockfd 的 event 置为 EPOLLIN,读取断开信息,触发 epoll 回调

if (tcphdr->tcp_flags & TCP_FIN_FLAG) {
    // 收到 fin,进入到 CLOSE_WAIT 状态
    stream->status = TCP_STATUS_CLOSE_WAIT; 
    // 设置 epoll 回调函数,读取断开信息
}
  • 检测 socket 的 send 状态,如果对端 cwnd>0, 可以发送数据,将 sockfd 置为 EPOLLOUT,等待发送数据


5、epoll 事件通知机制


水平触发(LT),有事件,则一直触发;边缘触发(ET),只触发一次,关注的是 io 状态的变化。


实现的关键是内核 io 就绪时,epoll 回调函数的执行次数。


LT,检测 recvbuffer 有数据则调用 epoll 回调函数


ET,从协议栈中检测到recvbuffer中接收数据就调用 epoll 回调函数

 

我们后面还可以用io_uring来处理,先不介绍了

相关文章
|
25天前
|
大数据 云计算
中国网络大会专题论坛 | 下一代超大规模高性能公共云网络
中国计算机学会ChinaNet上,阿里云洛神云网络将与知名学术届代表一起共话下一代超大规模高性能公共云网络的关键技术。
|
13天前
|
消息中间件 编解码 网络协议
Netty从入门到精通:高性能网络编程的进阶之路
【11月更文挑战第17天】Netty是一个基于Java NIO(Non-blocking I/O)的高性能、异步事件驱动的网络应用框架。使用Netty,开发者可以快速、高效地开发可扩展的网络服务器和客户端程序。本文将带您从Netty的背景、业务场景、功能点、解决问题的关键、底层原理实现,到编写一个详细的Java示例,全面了解Netty,帮助您从入门到精通。
48 0
|
1月前
|
网络协议 前端开发 Java
网络协议与IO模型
网络协议与IO模型
网络协议与IO模型
|
24天前
|
网络协议 物联网 API
Python网络编程:Twisted框架的异步IO处理与实战
【10月更文挑战第26天】Python 是一门功能强大且易于学习的编程语言,Twisted 框架以其事件驱动和异步IO处理能力,在网络编程领域独树一帜。本文深入探讨 Twisted 的异步IO机制,并通过实战示例展示其强大功能。示例包括创建简单HTTP服务器,展示如何高效处理大量并发连接。
40 1
|
25天前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
208 2
|
15天前
|
大数据 云计算
2024 CCF中国网络大会专题论坛丨下一代超大规模高性能公共云网络 精彩回顾
中国计算机学会ChinaNet上,阿里云洛神云网络将与知名学术届代表一起共话下一代超大规模高性能公共云网络的关键技术。
|
1月前
|
缓存 Java Linux
硬核图解网络IO模型!
硬核图解网络IO模型!
|
23天前
|
网络协议 调度 开发者
Python网络编程:Twisted框架的异步IO处理与实战
【10月更文挑战第27天】本文介绍了Python网络编程中的Twisted框架,重点讲解了其异步IO处理机制。通过反应器模式,Twisted能够在单线程中高效处理多个网络连接。文章提供了两个实战示例:一个简单的Echo服务器和一个HTTP服务器,展示了Twisted的强大功能和灵活性。
32 0
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
下一篇
无影云桌面