网络编程5:epoll模型用法和原理

epoll模型详解

epoll_wait和poll的区别

  • epoll_wait 函数调用完之后,我们可以直接在 event 参数中拿到所有有事件就绪的 fd,直接处理即可(event 参数仅仅是个出参);
  • poll 函数的事件集合调用前后数量都未改变,只不过调用前我们通过 pollfd 结构体events 字段设置待检测事件,调用后我们需要通过 pollfd 结构体的 revents 字段去检测就绪的事件( 参数 fds 既是入参也是出参)。
  • 一般在 fd 数量比较多,但某段时间内,就绪事件 fd 数量较少的情况下,epoll_wait 才会体现出它的优势,也就是说 socket 连接数量较大时而活跃连接较少时 epoll 模型更高效。

函数签名

#include <sys/epoll.h>

//创建epoll句柄
int epoll_create(int size);// 返回一个epoll句柄
//参数size在Linux2.6以后不在使用,但是必须设置为一个>0的数
成功返回一个非负的epollfd句柄,失败返回-1

// 将待检测事件的句柄绑定到epollfd上
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); 
参数一:epollfd
参数二:操作选项:取值有 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL,分别表示向epollfd 
上添加、修改和移除一个其他 fd(socketfd),当取值为EPOLL_CTL_DEL时,第四个参数忽略不计,
可以设置为NULL
参数三:被操作fd,socketfd
参数四:结构体地址
成功返回0,失败返回-1

//检测句柄事件
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数一:epollfd
参数二:结构体输出参数,调用成功后,events 中存放的是与就绪事件相关 epoll_event 结构体数组
参数三:参数二数组中元素的最大个数
参数四:超时时间,单位微秒
成功返回有件事的fd数量,失败返回-1,返回0表示超时

epoll_event结构体:

struct epoll_event
{
    uint32_t     events;      /* 需要检测的 fd 事件,取值与 poll 函数一样 */
    epoll_data_t data;        /* 用户自定义数据 */ data联合体对象,64位系统上,8字节大小
};
typedef union epoll_data  // 
{
    void*		 ptr;
    int          fd; // 这里设置fd
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;
关于联合体,可以存储不同的数据类型,但是只能同时存储其中一种的数据类型,任何两个成员不会同时生效,
节省空间
tip:联合体所占空间字节数,必须满足能容纳最大字段的字节,并且可以被所有成员字节整除

水平模式和边缘模式

水平模式(默认模式)

一个事件只要有,就会一直触发

  • socket可读事件在水平模式触发条件
    • socket上没有数据->socket上有数据
    • socket上处于有数据状态
  • socket可写事件在水平模式触发条件
    • socket可写->socket可写
      socket不可可写->socket可写
边缘模式(需要添加事件宏EPOLLET)

只有一个事件从无到有才会触发

  • socket触发可写
    • socket不可写->可写
  • socket触发可读
    • socket上无数据->socekt有数据
    • socket上又新来一次数据
关于socket读写事件状态
  • 对于写事件:
    • 正常情况下,网络通信双方,socket是一直可写,所谓socket可写就是TCP缓冲区足够大,允许你把数据发出去。
    • 所以如果是水平模式注册了写事件,就会一直触发。一般对于发数据都建议先发,发不完再注册写事件,发完了取消写事件。
    • 对于边缘模式,一般也是先发数据,如果数据没发完,就注册可写时间,数据发完了不需要取消可写时间。
  • 对于读事件:
    • 考虑到边缘模式只会触发一次,所以注册的读事件触发之后,一般建议一次性收完,通过while死循环直到recv返回EWOULDBLOCK错误码,表示没有数据可收了。
    • 而对于水平模式,注册了读事件,如果socket缓冲区一直有数据,会一直触发,所以水平模式可以按需收取字节数,收不完没事。
  • 关于水平模式和边缘模式效率问题
    • 一般没有所谓的效率高低,只是根据两个模式特点,编码方式不同,需要根据业务来定;
    • 对于写事件
      • 水平模式编码简单,但是触发次数多;
      • 边缘模式编码复杂,触发次数少。
    • 对于读事件:
      • 边缘模式触发次数少,就需要while循环不断去收,如果对端不停发数据,一个线程的边缘模式就会卡在一个socket的收数据循环,从而无法处理其他socket上的事件。
      • 水平模式不会丢失数据或者信息,如果数据没读完,内核会一直上报,虽然每次读数据都会系统调用一次,但是可以照顾多个连接的公平性(实时性),而且水平模式的写法可以跨平台。

epoll底层数据结构(以后要修改)

  • 通过epoll_create创建一个epoll文件描述符的时候,会创建一个eventpoll对象,这个对象有两个重要的成员变量,一个是指向红黑树的根节点,一个指向就绪双向链表的跟节点。
  • 红黑树里面存放的是所有挂载到epollfd上的所要监测的socketfd信息,双向链表存储的是事件就绪的sockefd信息。
  • epoll_ctl会通过传入的op操作,操作红黑树上相关的socketfd的节点信息(新增、修改、删除)。
  • epoll_wait是判断双向链表里面有没有就绪节点,如果有信息就会取出来填充到epoll_waitd的传入参数中。如果双向链表没有信息,epoll_wait会阻塞设定的超时时间,如果这段时间有事件就绪,内核会将发送事件的socketfd信息插入到双向链表中,并且被epoll_wait填充到传出参数()中。

优点

  • 相比select没有连接数限制;
  • 效率提升,应用程序不需要轮询,不会随着句柄数量增加而降低效率,也就是只关心活跃的连接,epoll_wait返回的从内核携带出来的只有发送了事件的结构体;
  • 每次调用epoll_wait的时候,不想poll和select传入一个结构体到内核中,而是通过传入的epoll句柄复用内核中的epoll实例结构体,减小了系统开销。
  • epoll底层有个红黑树和双向链表,当有事件发生了,内核可以很快的遍历红黑树o(logn),并将相应的socket节点信息添加到双向链表中,不想poll和select的是循环遍历O(n)。

小总结:

  • LT 模式下,读事件触发后,可以按需收取想要的字节数,不用把本次接收到的数据收取干净(即不用循环到 recv 或者 read 函数返回 -1,错误码为 EWOULDBLOCK 或 EAGAIN);ET 模式下,读事件必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使有机会,也可能存在上次没读完的数据没有及时处理,造成客户端响应延迟。
  • LT 模式下,不需要写事件一定要及时移除,避免不必要的触发,浪费 CPU 资源;ET 模式下,写事件触发后,如果还需要下一次的写事件触发来驱动任务(例如发上次剩余的数据),你需要继续注册一次检测可写事件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值