UNIX环境编程(c语言)--多路复用select、poll、epoll

基础概念

前言
如果一个程序需要同时监听网络、设备、标准输入的描述符时,而每个描述符都可能需要阻塞,单进程单线程程序阻塞在网络描述符时就无法监听其他描述符,可想而知这样子的程序是有问题的

但是如果使用多线程或多进程的模式的话,创建进程或线程都是需要时间开销的,如果设备性能不太够时这种代价就很大了。现在还有一种实现方式多路复用,能够同时监听多个描述符,有事件发送就通知,并告知是哪个发生了什么事情

同步、异步

同步和异步的概念描述的是用户线程与内核的交互方式

同步:指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行

异步:指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数

阻塞、非阻塞

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式

阻塞是指IO操作在没有接收完数据或者没有得到结果之前不会返回,需要彻底完成后才返回到用户空间

非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成

五种io模块

  1. 同步阻塞io:即传统的IO模型,在linux中默认情况下所有的socket都是阻塞模式。在这种模式下,内核要等待足够的数据到来,否则整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除阻塞的状态。一般这种模式下,还需要在服务器端使用多线程(或多进程),多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
  2. 同步非阻塞:这种模式下不再阻塞等待数据的到来,但是如果一直没有读取到需要的数据,将不断发起io请求,直到数据到达。不断的轮询请求,消耗大量的CPU资源,所以这种模式一般比较少用。
  3. io多路复用:避免了同步非阻塞IO模型中轮询等待的问题,可以同时监听多个描述符,从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。
  4. 信号驱动io:调用sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程
    再把数据从内核读入到用户空间,这一步是阻塞的
  5. 异步io:也称为异步非阻塞IO。“真正”的异步IO需要操作系统更强的支持。当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

相比于IO多路复用模型,信号驱动IO和异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式。

三种io复用select、poll、epoll对比

  • select:支持的系统较多,有最大监听值限制(通常是1024,可以增加但是越多性能越差)
  • poll:unix系统都有的,没有最大值限制(越多同样性能越差)
  • epoll:linux特有,性能最好

目前最常用的是epoll,也是性能最好的

在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术,在大数据、高并发、集群等一些名词唱得火热之年代,select和poll的用武之地越来越有限,风头已经被epoll占尽。

select多路复用

select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。

原型

int select(int maxfdpl, fd_set *readset, fd_set *write, fd_set *exceptset
			, const struct timeval *timeout);
//    有就绪描述符返回个数,超时返回0,出错返回-1

参数 readsetwriteexceptset分别是监听读、写、出错的描述符集合,数据类型是fd_set 其内部实现的细节我们不需要了解,假如我们需要监听描述符fd是否有读数据,就调用相关函数使其加入readset集合中,再select即可,其他两个也是类似。当然如果我们不关心哪个集合的通知可以设为NULL

关于fd_set 类型的使用,相关函数如下

void FD_ZERO(fd_set *fdset);  // 初始化fd_set 
void FD_SET(int fd, fd_set *fdset);  //让fd 加入fdset集合
void FD_CLR(int fd, fd_set *fd_set);  //从fdset 集合中删除 fd 
void FD_ISSET(int fd, fd_set *fd_set);    //判断调用select之后,fd是否有事件发生

描述符集合的初始化是必须要完成的,一个自动变量分配的描述符集合没有初始化的话,内部的值不确定,可能会发生莫名其妙的事情。

select函数在返回前会修改readsetwriteexceptset三个集合,未就绪的描述符会被置0,所以我们可以所以FD_ISSET来判断。这也意味着,我们在每次调用select之前,都需要重新把我们关心的描述符重新加入集合。

参数maxfdpl,是所监听的描述符里面值最大的一个再加1的值,注意不是描述符的个数,而是最大值加1,如果我们监听三个描述符,1,3,6,那么maxfdpl的值应该为7.

最后一个参数timeout,这个参数也是一个结构,这个参数用于指定超时时间

struct timeval {
	long tv_sec;    // 单位为秒
	long tv_usec;   //单位为微秒
}

有三种情况:

  1. 将timeout 设置为NULL,将永不超时一直阻塞,直到有描述符就绪才返回
  2. 等待固定一个时间,超过timeout 指定的秒+微秒后,依然没有描述符就绪,将超时返回
  3. 不等待,将timeout的秒和微秒都设置为0,相当于轮询

在某些系统下,如果还没有到超时时间就返回(描述符已经就位),将修改timeout的值,所以出于移植的考虑,最好在每次select之前,都对timeout初始化

这里还有一个小妙用,如果我们将readsetwriteexceptset都设置为NULL就得到了一个比sleep更精准的定时器,因为timeout可以指定时间到微秒级

poll多路复用

poll()的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

原型

int poll(struct pollfd *fdarrary, unsigned long nfds, int timeout);
// 成功返回就绪描述符的数目,超时返回0,失败返回-1

第一个参数fdarrary,应该填struct pollfd结构体数组的第一个元素的指针,就是每个待监听描述符都建立一个struct pollfd结构体,组成一个结构体数组,然后将首地址作为实参

struct pollfd{
	int 		fd;    // 描述符
	short 		events;//等待的事件
	short		revents; // 实际发生了的事情
}

events和revents可取值如下
在这里插入图片描述

第二个参数 nfds 指定数组中监听的元素个数,就是监听的描述符个数

第三个参数 timeout指定超时时间,单位是毫秒,当timeout为负数时,表示永远不超时,为0时不等待,相当于轮询

epoll多路复用

epoll能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

select()、poll()模型都是水平触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

  • LT(level triggered 水平触发)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
  • ET (edge-triggered 边缘触发)是高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。

epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3个部分:

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  2. 调用epoll_ctl向epoll对象中添加需要连接的套接字
  3. 调用epoll_wait收集发生的事件的连接

创建epoll:epoll_create()

原型

int epoll_create(int size);

系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功返回文件描述符,若出错返回-1。从Linux2.6.8版以来,size参数被忽略不用,但是需要给一个大于0的值

返回值,epoll_create()返回了代表新创建的epoll实例的文件描述符。这个文件描述符在其他几个epoll系统调用中用来表示epoll实例。当这个文件描述符不再需要时,应该通过close来关闭。当所有与epoll实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返还给系统。从2.6.27版内核以来,Linux支持了一个新的系统调用epoll_create1()。该系统调用执行的任务同epoll_create()一样,但是去掉了无用的参数size,并增加了一个可用来修改系统调用行为的flags参数。目前只支持一个flag标志EPOLL_CLOEXEC,它使得内核在新的文件描述符上启动了执行即关闭标志。

修改epoll的兴趣列表:epoll_ctl()

epoll_ctl()能够修改epoll实例中的兴趣列表。
原型

typedef union epoll_data
{
 void *ptr; /* Pointer to user-defind data */
 int fd; /* File descriptor */
 uint32_t u32; /* 32-bit integer */
 uint64_t u64; /* 64-bit integer */
} epoll_data_t;
struct epoll_event
{
 uint32_t events; /*需要检测的fd事件,取值与poll函数一样*/
 epoll_data_t data; /* 用户自定义数据 */
};


int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
//若成功返回0,若出错返回-1


第一个参数epfd是epoll_create()的返回值

第二个参数op用来指定需要执行的操作,它可以是如下几种值:

  • EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误
  • EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误;
  • EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev(设置为NULL)。如果我们试图移除一个不在epfd的兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。

关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除

第三个参数fd 指明了要修改兴趣列表中的哪一个文件描述符的设定。就是被操作的fd;
第四个参数ev是指向结构体epoll_event的指针,结构体的定义如上

事件等待:epoll_wait()

epoll_wait()返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。调用成功后epoll_wait()返回数组evlist中的元素个数,如果在timeout超时间隔内没有任何文件描述符处于就绪态的话就返回0,出错时返回-1并在errno中设定错误码以表示错误原因。

原型

int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

第一个参数epfd是epoll_create()的返回值;
第二个参数evlist是一个epoll_event结构数组的首地址,这是一个输出参数,函数调用成功后,events中存放的是与就绪事件相关epoll_event结构体数组
第三个参数maxevents指定所evlist数组里包含的元素个数;
第四个参数timeout超时时间,单位毫秒

  • timeout等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止。
  • timeout等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件。
  • timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。

数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。注意,data字段是唯一可获知同这个事件相关的文件描述符的途径。因此,当我们调用epoll_ctl()将文件描述符添加到感兴趣列表中时,应该要么将ev.date.fd设为文件描述符号,要么将ev.date.ptr设为指向包含文件描述符号的结构体。

在这里插入图片描述

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GuanFuXinCSDN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值