• I/O复用--浅谈epoll


    我们聊了聊select和poll知道:

    • 它们都是采取轮询的方式查找是否有就绪描述符。
    • 都有数据结构从用户态拷贝到内核态,内核态拷贝到用户态这个过程。

    为了针对许多大量连接,高并发的的场景下大量的资源消耗,效率低的问题,这一节就浅浅来聊一下epoll,epoll是之前的select和poll的增强版本,是linux操作系统独有的I/O复用技术。

    对于epoll来说他更灵活,解决了select和poll的弊端,使用起来也更加方便顺手,他不像select和poll那样只提供了一个方法,epoll提供了一组方法。

    本节呢就是聊聊epoll的使用和一些优点,对于epoll的两种触犯机制ET和LT的探讨会放在下一节去聊聊,注意select和poll只是LT。

    好了~言归正传

    目录

    epoll的工作原理:

    epoll的API:

    epoll的特点:

    用epoll实现tcp服务器的并发


    epoll的工作原理:

    • 1. 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。
    • 2. 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
    • 3. 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

    用大白话去理解就是,比如你是老师,你给同学们布置了作业,你在班里要检查作业的时候 不是你一个个去问,“啊,你写完了没有啊?”,而是你坐在讲台上,谁写完了,谁把作业拿上来给你,比方说有三个人写完了,这三个人就把作业交给你,你就会知道“哦,有三个人写完了”,至于剩下没交给你的那就是没写完的,你也不需要去问~  

    大概就是这样了,关键就是不用你去一个个问,而是谁完了,谁通知你

    前面说了epoll提供了一组api,方便我们去使用,下来我们看看~

    epoll的API:

    epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表组成。

    【1. 创建内核事件表:】

    函数原型为:

    1. #include
    2. int epoll_create(int size)
    3. //成功返回内核事件表的标识符,失败返回-1

    功能:该函数 创建内核事件表用于存放描述符和关注的事件。调用这个函数的时候,在内核cache里建立了红黑树struct rb_root用于存储以后epoll_ctl传来的socket,也就是内核事件表;还建立了一个双向链表struct list_headrdllidt用于存储就绪事件。 当epoll_wait调用时,仅仅观察这个双向链表里有没有数据即可,有数据表示有就绪事件,直接返回。
    size参数:表示创建的事件表需要多大,记住最后要close关闭,如果不关闭,那么就会导致fd被耗尽

    注意:

    size参数告诉内核这个epoll对象会处理的事件⼤致数量,⽽不是能够处理的事件的最⼤数(同时,size不要传0,会报invalid argument错误)。
    在现在linux版本中,这个size参数已经没有意义了;
    返回: epoll对象句柄;之后针对该epoll的操作需要通过该句柄来标识该epoll对象;

    【2. 管理内核事件表:】

    可以对要监听的事件进行操作:注册,删除,修改

    返回: 0表示成功, -1表示错误,根据errno错误码判断错误类型。

    原型如下:

    1. #include
    2. int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
    3. //成功返回0,失败返回-1.

     参数:

    • epfd:内核事件表的标识符。
    • op:标识操作,有:添加,删除,修改。不用自己写了,根据第二个op参数来选择
    1. EPOLL_CTL_ADD //注册新的文件描述符到内核事件表epfd中
    2. EPOLL_CTL_DEL //从内核事件表中删除文件描述符
    3. EPOLL_CTL_MOD //修改文件描述符
    • fd:要操作的文件描述符。
    • events:保存事件类型,用户填充告诉内核要对哪种事件进行操作。所以需要先定义event结构体,然后再进行操作,表示对何种事件类型进行何种操作
    1. struct epoll_event
    2. {
    3. short events;//事件类型:在每一个poll的事件类型标识前加个‘E’
    4. Union epoll_data_t data(联合体,用到其中的fd成员即文件描述符,其他的不用)
    5. }
    6. typedef union epoll_data {
    7. void *ptr; /* 指向用户自定义数据 */
    8. int fd; /* 注册的文件描述符 */
    9. uint32_t u32; /* 32-bit integer */
    10. uint64_t u64; /* 64-bit integer */
    11. } epoll_data_t;

    event.events 取值:

    1. EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包; FIN包就是read 返回 0
    2. EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起⾮阻塞tcp连接,连接建⽴成功事件相当于可写事件)
    3. EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接
    4. EPOLLPRI 表示连接上有紧急数据需要读
    5. EPOLLERR 表示连接发⽣错误
    6. EPOLLHUP 表示连接被挂起
    7. EPOLLET 将触发⽅式设置为边缘触发,系统默认为⽔平触发
    8. EPOLLONESHOT 表示该事件只处理⼀次,下次需要处理时需重新加⼊epoll

    假如现在我们将监听文件描述符添加到内核事件表中:

    1. struct epoll_event event;
    2. event.events=EPOLLIN;//监听读事件
    3. event.data.fd=listenfd;//被监听的文件描述符
    4. int res=epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&event);//将event结构体中存储的事件类型添加到内核事件表中。

    【3. 开始监听内核事件表的事件】

    1. int epoll_wait(int epfd,struct epoll_event events[],int maxevents,int timeout)
    2. // 成功返回就绪个数,失败-1,超时0

    参数:

    • epfd:内核事件表;
    • events[]:events的数据是 由内核在epoll_wait返回时填充的,有事件就绪的文件描述符和就绪的事件类型;
    • maxevents:数组的长度,表示一次epoll_wait最多返回多少个就绪的文件描述符。
    • timeout:监听时间。

    收集 epoll 监控的事件中已经发⽣的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待timeout 毫秒后返回。
    返回:表示当前发⽣的事件个数
    返回0表示本次没有事件发⽣;
    返回-1表示出现错误,需要检查errno错误码判断错误类型。

    epoll的特点:

    【1. 速度快:】

    • select中为fd_set,poll 为 struct pollfd fds[],它们都是用户创建的,用户态存在,每调用一次select或者poll,都会存在两次用户态->内核,内核->用户的数据拷贝,一次是在调用时,一次是在返回时。这样数据结构越大,则越慢。
    • epoll直接就在内核中记录,将其都记录在内核事件表上,在监听时不需要进行拷贝,所以速度比他俩快.

    【 2. 查找时间复杂度低:】

    • select,poll返回就绪事件的个数,但不知道在哪,所以要轮询找,每次用户检索就绪事件的时间复杂度为O(N)。
    • epoll直接返回就绪事件链表,链表中有值就有就绪事件,所以每次都是就绪的不用找,每次用户检索就绪事件的时间复杂度为O(1)。

    【3. 采取回调函数方式处理就绪事件】

    • select,poll采用轮询方式检测是否有事件发生,循环检测是否有事件就绪,直到找不到。适合关注的文件描述每次都有很大的机率就绪,1000个有900多个就绪,那么就很适合,但是1000个只有一个事件发生,还是要进行轮询1000多次,函数栈帧空间频繁,影响效果;
    • epoll的内核采用的是回调的方式检测文件描述符是否有事件发生。假设1000个,把文件描述符添加到事件表上,文件描述符在红黑树上挂载,除了值,事件类型,还带有一个回调函数,如果文件描述符有事件发生,它就调用回调函数,回调函数的作用是有事件就绪时就把文件描述符添加到双向链表中,返回时把链表的值拷贝到用户态。1000个一个事件发生只会触发一次回调,不用多次循环。适合很多文件描述符,就绪事件描述符少。

    用epoll实现tcp服务器的并发

    因为epoll封装了很多函数,所以操作起来比起selet和poll代码简洁了很多

    服务器代码如下:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. //网络头文件
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #define MAX 10//定义最大连接数为10个
    13. int InitSocket()
    14. {
    15. int sockfd = socket(AF_INET,SOCK_STREAM,0);
    16. if(sockfd == -1) return -1;
    17. struct sockaddr_in ser;//指明地址信息,是一种通用的套接字地址
    18. memset(&ser,0,sizeof(ser));
    19. ser.sin_family = AF_INET;//设置地址家族
    20. ser.sin_port = htons(6000);//设置端口
    21. ser.sin_addr.s_addr = inet_addr("127.0.0.1");//设置地址
    22. int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));//绑定端口号和地址
    23. if(res == -1) return -1;
    24. res = listen(sockfd,5);
    25. if(res == -1) return -1;
    26. return sockfd;
    27. }
    28. void epoll_add(int epfd,int fd)
    29. {
    30. struct epoll_event ev;
    31. ev.events=EPOLLIN;//读
    32. ev.data.fd=fd;
    33. if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
    34. {
    35. printf("epoll add failed\n");
    36. }
    37. }
    38. void epoll_del(int epfd,int fd)
    39. {
    40. if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
    41. {
    42. printf("epoll del failed\n");
    43. }
    44. }
    45. void accept_client(int epfd,int sockfd)
    46. {
    47. struct sockaddr_in caddr;
    48. int len=sizeof(caddr);
    49. int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
    50. if(c<0)
    51. {
    52. return ;
    53. }
    54. printf("accpet c=%d ip=%s\n",c,inet_ntoa(caddr.sin_addr));
    55. epoll_add(epfd,c);
    56. }
    57. void recv_data(int epfd,int c)
    58. {
    59. char buff[128]={0};
    60. int num=recv(c,buff,127,0);
    61. if(num<=0)//如果num==0说明客户端结束了描述符号
    62. {
    63. epoll_del(epfd,c);//移除改客户端对应的描述符
    64. close(c);
    65. printf("client close\n");
    66. return ;
    67. }
    68. printf("buff (%d)=%s\n",c,buff);
    69. send(c,"ok",2,0);
    70. }
    71. int main()
    72. {
    73. int sockfd = InitSocket();//监听套接字,有客户端链接时就会触发读事件。
    74. assert(sockfd != -1);
    75. //创建内核事件表
    76. int epfd=epoll_create(MAX);//底层,红黑树
    77. if(epfd==-1)
    78. {
    79. exit(1);
    80. }
    81. epoll_add(epfd,sockfd);//将监听套接子添加到内核事件表
    82. struct epoll_event evs[MAX];//用来接收就绪的文件描述符
    83. while(1)
    84. {
    85. int n=epoll_wait(epfd,evs,MAX,5000);
    86. if(n==-1)
    87. {
    88. printf("err\n");
    89. }
    90. else if(n==0)
    91. {
    92. printf("time out\n");
    93. }
    94. else
    95. {//前n个元素是数据就绪的
    96. for(int i=0;i
    97. {
    98. int fd=evs[i].data.fd;
    99. if(evs[i].events&EPOLLIN)//看读事件是不是就绪
    100. {
    101. if(fd==sockfd)
    102. {
    103. accept_client(epfd,sockfd);
    104. }
    105. else
    106. {
    107. recv_data(epfd,fd);
    108. }
    109. }
    110. //if(evs[i].events&EPOLLOUT)
    111. }
    112. }
    113. }
    114. close(sockfd);
    115. }

    客户端的代码都是一样的,这里我就不粘了

    ヾ(◍°∇°◍)ノ゙

  • 相关阅读:
    6.3 - 常见协议及对应的端口号
    1000套web前端期末大作业 HTML+CSS+JavaScript网页设计实例 企业网站制作【建议收藏】
    学习大数据,所必需的java基础(6)
    经典文献阅读之--EGO-Planner(无ESDF的四旋翼局部规划器)
    nodejs入门及常用模块(http、fs、path)
    【UniApp】-uni-app-内置组件
    【面试题】throws 与 throw 声明和抛出异常
    Labs‘Codes review(AVR)(3)
    二、【React-Router5】路由的基本使用
    loj 10078 / 一本通 1500 / 洛谷 P5764【最短路】【dfs枚举排列】
  • 原文地址:https://blog.csdn.net/weixin_51609435/article/details/127416352