ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

不知道怎么入门Selet、Poll、Epoll?看这篇文章就够了!

2021-06-16 14:03:34  阅读:165  来源: 互联网

标签:set struct epoll int Epoll 就够 fd Selet select


前置知识:

  1. 用户空间和内核空间
  2. 进程切换
  3. 进程的阻塞
  4. 文件描述符
  5. 缓存 IO
  6. 中断

如果对以上概念还有不清楚的地方的话,可以先手动百科下再继续往下看~

I/O多路复用

首先我们来了解一下文件描述符:在Unix哲学中:“Everything is a file."Socket、pipe都可以看做文件,并且有自己独一无二的文件描述符(缩写为fd),而文件常常面临着读数据和写数据。

然后我们来了解一下I/O的概念:I/O分别是input和output的说写,也就是读数据和写数据,我们通过write可以往文件中写数据,而通过read从文件中读取数据。

那阻塞I/O是什么意思呢?如果在文件描述符中的数据还没有准备好的时候:例如在还没有任何数据的时候,发送了read()调用,进程就会堵塞,

那有没有什么好办法?我们可以同时监听多个fd,在有fd就绪的时候进行通知,没有的时候就堵塞?有,那就是I/O多路复用!

I/O多路复用就像是公司的收发室小哥,会替你处理中通、顺丰、韵达、申通…等等各家快递公司的快递,等哪家的快递到了,再打电话通知你拿~这个时候你就有很多时间在办公室愉快地摸鱼啦!

那在操作系统里,该如何设计这样的收发室小哥呢?

  1. 当有一个fd就绪时进行通知(等待小哥给你发短信)
  2. 都不可用?在有可用的文件描述符之前一直处于堵塞状态(专心在工位摸鱼)
  3. 唤醒:哪个fd可用了?(收到短信,下楼拿快递)
  4. 处理完所有的I/O就绪的fd,没有堵塞(拿完所有的快递,没有摸鱼)
  5. 返回第一步重新开始(上楼,回到工位,继续摸鱼)

UNIX公司里提供了select、poll、epoll、kqueue这几位年纪、性格和能力都有所差异的收发室小哥,让我们分别介绍下这几位小哥的工作特点吧!

select

select是最早出现在UNIX里的多路复用有关的系统调用:

#include<sys/select.h>
int selcet( int n,
            fd_set *readfds,
            fd_set *writefds,
            fd_set *exceptfds,
            struct timeval *timeout
            );

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);

readfds、writefds、exceptfds分别表示select所监视的不同类型的文件描述符,分别等待不同的事件:监视是否有数据可读、监视是否有数据可写、是否有异常etc。第一个参数n呢,等于所有集合中最大的文件描述符的值加一。当selet成功返回时,readfds、writefds、exceptfds都会写改为只包含对应类型的I/O就绪的文件描述。举个例子,readfds对应的是中通快递,而显示有编号1、2、3的快递在发往我们收发室,在select检查快递的时候,只有2到了,因此readfds集合就只返回编号为2的快递。参数timeout是只想timeval结构体的指针:

#include<sys/time.h>

struct timeval{
    long tv_sec; 
    long tv_usec; 

}

如果该参数不是NULL,在tv_sec秒tv_usec毫秒过后select系统调用就会返回。

FD_ZERO(fd_set *fdset):清空fdset与所有文件描述符的联系。

FD_SET(int fd, fd_set *fdset):建立文件描述符fd与fdset的联系。

FD_CLR(int fd, fd_set *fdset):清除文件描述符fd与fdset的联系。

FD_ISSET(int fd, fd_set *fdset):检查fd_set联系的文件描述符fd是否可读写,>0表示可读写。

typedef __kernel_fd_set     fd_set;
#undef __FD_SETSIZE
#define __FD_SETSIZE    1024

typedef struct {
    unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

select()的机制中提供一种fd_set的数据结构,fd_set是一个struct, 其内部只有一个 由16个元素组成的unsigned long数组,其实就是常见的bitmap,这个数组一共可以表示16 × 64 = 1024位, 每一位用来表示一个 fd, 这也就是 select针对读,定或异常每一类最多只能有 1024个fd 限制的由来。当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

select机制的问题

1.单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,这个大小是由宏控制的,进行改变很麻烦(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024),并且由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;

2.内核 / 用户空间内存拷贝问题:select需要复制大量的句柄数据结构,产生巨大的开销;

3.select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

poll

早期淘宝购物并不发达,所以收发室的快件并不是很多,select轮询上限1024个快件的工作方式也够用了。但随着网络购物的发展,收发室的快递越来越多,select模式下fd_set的长度限制就开始成为了致命缺陷。

吸取了select的教训,新引入的poll就不再采用定长数组来保存所监控的fd信息了,而是采用可以改变长度的链表,没有了连接数目的限制,poll可以支持高并发的需求。除此以外,poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

poll用来保存事件的数据结构:

#include <poll.h>

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

使用poll函数进行操作:

#include <poll.h>

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

fds:一个pollfd队列的队头指针,我们先把需要监视的文件描述符和他们上面的事件放到这个队列中

nfds:队列的长度

timeout:事件操作,设置指定正数的阻塞事件,0表示非阻塞模式,-1表示永久阻塞。

epoll:

设想一个场景,双十一到了,公司总共有一万份快递要来,而实际上每一段时间只会有几十个或者几百个快递到达收发室,也就是说,每一时刻工作人员只需要处理一万份快递中的一小部分快递,那么如何才能高效的处理这种场景呢?如果继续采用select和poll轮询的方法,收发室工作人员一定会累坏吧。实际上,在Linux 2.4版本以前, unix公司就是这么做的。Linux 2.4版中,引入了epoll,它在Linux内核中申请了一个简易的文件系统,把原先的一个select或poll调用分成了3部分,相当于把select和poll的工作分给三个人来做:

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  1. 调用epoll_create建立一个epoll对象(在epoll文件系统中给这个句柄分配资源);

  2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字;

  3. 调用epoll_wait收集发生事件的连接。

这样只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就会非常高,因为调用epoll_wait时并没有向它传递这100万个连接,内核也不需要去遍历全部的连接。

当进程调用epoll_create方法的时候,Linux内核就会创建一个eventpoll结构体,对于这个结构体我们主要需要关注两个成员:

struct eventpoll {
...
  struct rb_root rbr;

  struct list_head rdllist;

...
};

我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。

在epoll中,对于每一个事件都会建立一个epitem结构体:

struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。

因此当我们调用epoll_wait检查是否有就绪的事件时,只是检查eventpoll对象中的rdllist双向链表中是否有epitem, 如果非空的话,就将这里的时间复制到用户态内存中,同时返回时间的数量。因此epoll_wait的效率很高。而epoll_ctl对epoll对象中的事件进行增删查改时,由于使用了红黑树,因此查找事件也是十分高效的。

总结

Linux系统中存在着select、poll、epoll三种系统调用,针对高并发,且任一时间只有少数socket是活跃的场景epoll的性能要好很多。 但是如果在并发量低,socket都比较活跃的情况下,select/poll就不见得比epoll慢了(就像我们常常说快排比插入排序快,但是在特定情况下这并不成立)。并且由于历史原因(先来后到),select因为兼容性比较好,仍然在被大量应用中。

标签:set,struct,epoll,int,Epoll,就够,fd,Selet,select
来源: https://blog.csdn.net/Java_Caiyo/article/details/117956907

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有