Linux网络编程————多路复用

文章目录

    • 引言:
      • select 的工作过程如下:
      • poll 的工作过程如下:
      • select/poll 的缺陷
      • epoll 的工作过程如下:
      • epoll 的优点

引言:

多路复用模型是五种常见I/O模型之一,使用 select/poll 实现的多路复用 I/O 模型是使用最为广泛的事件驱动 I/O 模型,但是由于 select/poll 实现的不完善,这种 I/O 模型的缺陷也逐渐暴露出来。

select 的工作过程如下:

  1. 调用者初始化自己关心的可读、可写和异常的描述符集。比如对希望在
    可读时接到通知的描述符,就将其加入 readfds。描述符集的结构是
    fd_set,在 Linux 中它有如下定义:
typedef struct 
{
      
	#ifdef __USE_XOPEN 
    		__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; 
	# define __FDS_BITS(set) ((set)->fds_bits) 
	#else 
     		__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; 
	# define __FDS_BITS(set) ((set)->__fds_bits) 
	#endif 
} fd_set; 

所谓将描述符加入描述符集就是将描述符所对应的位置位

  1. 调用者将描述符集传给 select,如果对某一类描述符不感兴趣,将 NULL传给对应的描述符指针,select函数原型如下:
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout); 

第一个参数 max_fd设置为三个描述符集中被置 1 的位中最大的一个的数组索引加 1。
最后一个参数表示当经过一定的时间后,如果没有描述符准备好,select 超时返回。如果需要 select 无限期等待,也传 NULL 即可。
中间三个参数就是我们关心的文件描述符集readset、writeset、exceptset,不关心的我们可以设置为NULL
select函数的返回值:
成功就绪描述符的数目
超时时返回0
出错返回-1

  1. 进入系统调用后,描述符集被从用户空间拷贝到内核空间。内核扫描描述符集,根据结果为相应的描述符所对应的 struct file 结构创建轮询表项struct poll_table_entry 并加入轮询表 struct poll_table_struct 中,然后再把进程放入对应的等待队中。

  2. 当描述符状态发生改变时,进程被唤醒,内核再次扫描描述符集,根据轮询表状态将描述符集中活动的描述符(即描述符按照用户所关心的方式发生了改变)对应的位置 1,然后将描述符集拷贝回用户空间。

  3. 应用程序用 FD_ISSET 宏扫描返回的描述符集以决定某个描述符是否发生了改变,然后采取相应的动作常用的其他宏扫描还有如下(函数的使用都可以去看man手册):

       void FD_CLR(int fd, fd_set *set);//清除某个位时可以使用
       int  FD_ISSET(int fd, fd_set *set);//测试某个位是否被置位
       void FD_SET(int fd, fd_set *set);//设置变量的某个位置位
       void FD_ZERO(fd_set *set);//一个 fd_set类型变量的所有位都设为 0

poll 的工作过程如下:

工作过程和select差不多,但相对 select 而言,poll 的情况稍好一些,首先没有文件描述符数量的限制,而且拷贝问题没那么严重。poll 只把用户关心的描述符所对应的 struct pollfd 结构拷贝到内核,而不是整个描述符集。但是在 poll 返回的时候,所有的这些 struct pollfd 结构都被拷贝回内核空间,而非仅仅活动的描述符所对应的结构,所以依然有不必要的拷贝操作。

  1. 用户通过设置结构体的内容将自己要监听的文件描述符、监听fd 上哪些事件通过fds结构体保存起来,当调用epoll时将结构体内容从用户空间拷贝到内核
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

第一个参数:用来指向一个struct pollfd类型的数组

struct pollfd
{
     
	int   fd;         /* 文件描述符 */
        short events;     /* 关心的事件类型 */
        short revents;    /* 实际返回的事件类型 */
};

第二个参数:指定数组中监听的元素个数

第三个参数:timeout指定等待的毫秒数,无论I/O是否准备好 ,poll都会返回
为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;
为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件,一旦选举出来,立即返回。

  1. 内核在规定的时间内对文件描述符进行轮询查找事件,来测试其中是否有就绪的,当有文件描述符上发生了事件后,内核通过一系列事件按位或,修改revents用于返回给用户空间告知用户相应的事件发生了
  2. 用户通过将从poll返回的revents与相应的掩码做与运算来判断自己希望检测的事件是否发生了,后面调用相应的处理函数
    常用的用于检测的事件掩码有:
EPOLLIN:表示对应的文件描述符可以读; 
EPOLLOUT:表示对应的文件描述符可以写; 
EPOLLPRI:表示对应的文件描述符有紧急的数据可读; 
EPOLLERR:表示对应的文件描述符发生错误; 
EPOLLHUP:表示对应的文件描述符被挂断; 
EPOLLET:  将 EPOLL 设为边缘触发 ET 模式,这是相对于水平触发 LT 来说的。 
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续

select/poll 的缺陷

select/poll 存在着必须遍历整个监听的描述符集才能获取事件的问题,也使得它必定会在高并发的场景落幕
影响 select 性能的主要因素是两次用户/内核之间的拷贝和三次扫描(轮询)。
扫描的范围是索引从 0 到 n-1 的位置,这意味着当描述符的范围增加时,扫描带来的开销也随之增加。同样,在大多数情况下,大部分描述符处于非活动状态,扫描是对 CPU 时间的极大浪费。并且,当描述符状态发生改变时,内核经过扫描以后已经知道哪些描述符是活动的,但是当描述符集被拷贝回用户空间以后应用程序还要再扫描一次或多次来确定某个描述符是否在活动的描述符集中。这种重复的工作对于一个同时有几千个甚至更多打开描述符的服务器将会带来急剧的性能下降,而且单个进程能够监视的文件描述符的数量存在最大限制一般是1024个 。

相对 select 而言,poll 的情况稍好一些,没有了文件描述符个数的限制,拷贝的量也没那么大,但是在从内核拷贝到用户空间时所以依然有不必要的拷贝操作

epoll 的工作过程如下:

poll不像前面的两个一样,它并不是一个单独的函数而是由一组函数组成,它有3个系统调用

  1. epoll_create用于创建一个 epoll 的句柄有的也叫监听池,虽然简单的说是创建一个epoll句柄,但是实际上是通过内核把一段磁盘上的空间地址映射到进程的虚拟地址空间,在进程的虚拟地址空间创建一个虚拟文件系统,用于存储套接字描述符,并在虚拟文件系统中创建一个文件节点,创建的函数定义如下,参数size用于定义内核监听的套接字数目的大小,实际上epoll可以监听的文件描述符个数是与内存空间成线性关系的,内存空间越大,可以监听的描述符越多,这里要注意一下的是;当 Epoll 句柄创建好后会首先占用一个 fd 值的,所以每次在使用完 Epoll 后,必须记得调用 close()函数来关闭这个 fd,否则可能因为fd 未被释放而导致 fd 被耗尽
int epoll_create(int size);
若成功返回文件描述符,若出错返回-1 

  1. 前面我们为epoll创建了空间用来存放文件描述符,那么这里epoll_ctl就是向epoll句柄(epfd)中通过参数op注册、删除、修改要被监听的文件描述符fd,并通过epoll_event结构体指定需要监听fd上的那些事件,调用该函数内核会在用户的虚拟地址空间创建红黑树用来存储加入进来的文件描述符,同时通过红黑树来查找和删除也是非常快的,由于内核实现中epoll是根据每个描述符上面的callback函数实现的,所以当被监听的文件描述符发生变化时内核检测到活跃的文件描述符,并把它们加入到就绪态list链表,后面的epoll_wait就是从这里就绪态链表中取数据的
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
若成功返回0,若出错返回-1。 


op对应三个宏如下:
EPOLL_CTL_ADD:注册事件,将新的fd加入到epoll专用描述符epfd中; 
EPOLL_CTL_MOD:修改事件,修改已注册的	fd的监听事件; 
EPOLL_CTL_DEL:删除时间,将一个fd从epoll专用描述符 epfd 中删除;

struct epoll_event 结构体内容如下:
struct epoll_event 
{
      
     __uint32_t events; /*  监听的 Epoll 事件*/ 
     epoll_data_t data; /*  用户可以传递的变量*/ 
}; 

data是一个联合结构体,定义如下:
typedef union epoll_data 
{
      
     void *ptr;   /*  指向需要传递的数据*/ 
     int fd;      /*  套接字描述符*/ 
     __uint32_t u32; 
     __uint64_t u64; 
} epoll_data_t; 

events由下面几个宏组成:
EPOLLIN:表示对应的文件描述符可以读; 
EPOLLOUT:表示对应的文件描述符可以写; 
EPOLLPRI:表示对应的文件描述符有紧急的数据可读; 
EPOLLERR:表示对应的文件描述符发生错误; 
EPOLLHUP:表示对应的文件描述符被挂断; 
EPOLLET:  将 EPOLL 设为边缘触发 ET 模式,这是相对于水平触发 LT 来说的,这种模式下内核会不断的通知进程让它去处理监听的结果。 
EPOLLONESHOT:只监听一次事件,内核默认通知你一次之后你已经知道了就不会一直通知你,当监听完这次事件之后,如果还需要继续
监听这个 socket 的话,需要重新把这个 socket 加入到 EPOLL 队列里 

  1. 经过上面操作后就差等待事件了,epoll_wait就是等待事件的发生,当有事件发生的时候就把就绪态list中的事件拷贝到epoll_event数组中,maxevents 参数定义内核每次能处理的最大事件数,参数 timeout 是超时时间(单位为毫秒),如果设置为 0会立即返回,设置-1 将是永久阻塞,一般用-1 即可
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
成功:返回整个epoll_wait函数的返回值是就绪态事件个数
如果在timeout超时间隔内没有任何文件描述符处于就绪态:返回0
出错:返回-1并在errno中设定错误码以表示错误原因。 


epoll 的优点

  1. select能监听的文件描述符最大数量只能1024对吧!,那么epoll则是,连接上限与系统内存有关,即内存越大能并发的连接就越多

  2. 前面的select各种缺陷,内核与用户间大量的文件描述符拷贝是吧!,那么epoll则是,通过内核与用户空间mmap同一块内存实现的,避免了不必要的内存拷贝。

  3. select为了监听描述符集中发生的事情是轮询查找是吧!,那么epoll则是,只会对“活跃”的描述符进行操作,这是因为在内核实现中epoll是根据每个描述符上面的callback函数实现的,只有活跃的描述符才会主动去调用callback函数,调用之后内核检测到活跃的文件描述符,为活跃的文件描述符创建就绪态list链表,当调用epoll_wait()时就是通过直接查看就绪态链表是否为空来完成就绪态文件描述符信息返回的,如果在链表中查找到有事件就绪时,将就绪事件填写到传入到epoll_wait()中的数组中并返回,没有数据就阻塞timeout时间,然后epoll_wait()返回.

没错epoll就是如此优秀就是为了高并发场景而量身定制的,

你可能感兴趣的:(Linux)