1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > IO多路复用:select poll epoll

IO多路复用:select poll epoll

时间:2022-08-01 05:24:23

相关推荐

IO多路复用:select poll epoll

文章目录

前言IO multiplexing - IO复用IO多路复用的优势selectselect函数select的使用select的缺点select的优势 pollepollepoll函数epoll_createepoll_ctlepoll_wait 工作模式 总结Reference

前言

I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

select,poll,epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者,本文重点讲解网络IO。

对于一个network IO,它会涉及到两个系统对象:

application:调用这个IO的进程kernel:系统内核

它们经历的两个交互过程是:

阶段1: wait for data,等待数据准备阶段2: copy data from kernel to user,将数据从内核拷贝到用户进程中

IO multiplexing - IO复用

I/O多路复用(multiplexing)是网络编程中最常用的模型,最常用的selectpollepoll都属于这种模型。以select为例:

看起来它与blocking I/O很相似,两个阶段都阻塞。但它与blocking I/O的一个重要区别就是它可以等待多个数据报就绪(datagram ready),即可以处理多个连接。这里的select相当于一个"代理",调用select以后进程会被select阻塞,这时候在内核空间内select会监听指定的多个datagram (如socket连接),如果其中任意一个数据就绪了就返回。此时程序再进行数据读取操作,将数据拷贝至当前进程内。由于select可以监听多个socket,可以用它来处理多个连接。

select模型中每个socket一般都设置成non-blocking,虽然等待数据阶段仍然是阻塞状态,但是它是被select调用阻塞的,而不是直接被I/O阻塞的。select底层通过轮询机制来判断每个socket读写是否就绪。

select也有一些缺点,比如底层轮询机制会增加开销、支持的文件描述符数量过少等。为此,Linux引入了epoll作为select的改进版本。

IO多路复用的优势

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销

select

select函数

函数原型

#include <sys/select.h>#include <sys/time.h>int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

函数参数

maxfdp1:指定待测试的描述符个数

readset、writeset、exceptset:指定要让内核监控的可写、可读、异常的描述符,如果对某一个的条件不感兴趣,可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

void FD_ZERO(fd_set *fdset);:清空集合void FD_SET(int fd, fd_set *fdset);:将一个给定的文件描述符加入集合之中void FD_CLR(int fd, fd_set *fdset);:将一个给定的文件描述符从集合中删除int FD_ISSET(int fd, fd_set *fdset);:检查集合中指定的文件描述符是否可以读写

timeout:告知内核等待所指定描述字中的任何一个就绪可花多少时间

struct timeval{long tv_sec; //secondslong tv_usec; //microseconds};

这个参数有三种可能: 永远等待下去:仅在有一个描述符准备好时才返回。为此,把该参数设置为空指针NULL等待一段固定时间:在有一个描述符准备好时返回,否则超时返回根本不等待:检查描述符后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

返回值

若有就绪描述符返回其数目,若超时则为0,若出错则为-1

select函数监控3类文件描述符,调用select函数后会阻塞,直到描述符fd准备就绪(有数据可读、可写、异常)或者超时,函数便返回。 当select函数返回后,可通过遍历描述符集合,找到就绪的描述符。

select的使用

SOCKADDR_IN addrSrv;int reuse = 1;SOCKET sockSrv,connsock;SOCKADDR_IN addrClient;pool pool;int len=sizeof(SOCKADDR);/*创建TCP*/sockSrv=socket(AF_INET,SOCK_STREAM,0);/*地址、端口的绑定*/addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);addrSrv.sin_family=AF_INET;addrSrv.sin_port=htons(port);if(bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR))<0){fprintf(stderr,"Failed to bind");return ;}if(listen(sockSrv,5)<0){fprintf(stderr,"Failed to listen socket");return ;}setsockopt(sockSrv,SOL_SOCKET,SO_REUSEADDR,(const char*)&reuse,sizeof(reuse));init_pool(sockSrv,&pool);while(1){/*通过selete设置为异步模式*/pool.ready_set=pool.read_set;pool.nready=select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);if(FD_ISSET(sockSrv,&pool.ready_set)){connsock=accept(sockSrv,(SOCKADDR *)&addrClient,&len);//loadDeal()/*连接处理*/add_client(connsock,&pool);//添加到连接池}/*检查是否有事件发生*/check_client(&pool);}

上面是一个服务器代码的关键部分,设置为异步的模式,然后接受到连接将其添加到连接池中。监听描述符上使用select,接受客户端的连接请求,在check_client函数中,遍历连接池中的描述符,检查是否有事件发生。

select的缺点

每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)

select的优势

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的

poll

#include <sys/poll.h>/*** fds:要监视的描述符* nfds:fds中描述符的总数量* 返回值:返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;**/int poll (struct pollfd *fds, unsigned int nfds, int timeout);// 表示监视的描述符struct pollfd {int fd; /* file descriptor */short events; /* requested events to watch 监视的请求事件 */short revents; /* returned events witnessed 已发生的事件 */};

poll的使用和select基本类似,相比selectpoll解决了单个进程能够打开的文件描述符数量有限制这个问题,和select函数一样,当poll函数返回后,可以通过遍历描述符集合,找到就绪的描述符。

selectpoll共同具有的一个很大的缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd(文件描述符)数量增多而线性增大。

此外,selectpoll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其性能会线性下降。

epoll

目前,epoll是Linux2.6下最高效的IO复用方式,也是Nginx、Node的IO实现方式。epoll的出现,解决了selectpoll的缺点:

基于事件驱动的方式,避免了每次都要把所有fd(文件描述符)都扫描一遍epoll_wait只返回就绪的fd(文件描述符)epoll使用nmap内存映射技术避免了内存复制的开销epoll的fd(文件描述符)数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关,通常远大于1024

epoll函数

#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);

epoll_create

创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1

epoll_ctl

注册要监听的事件类型,参数解释如下:

epfd表示epoll句柄

op表示fd操作类型,有如下3种

EPOLL_CTL_ADD 注册新的fd到epfd中EPOLL_CTL_MOD 修改已注册的fd的监听事件EPOLL_CTL_DEL 从epfd中删除一个fd

fd是要监听的描述符

event表示要监听的事件

struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */};typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64;} epoll_data_t;

epoll_wait

等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。

epfd是epoll句柄events表示从内核得到的就绪事件集合maxevents告诉内核events的大小timeout表示等待的超时事件

工作模式

水平触发(Level Triggered,LT):当就绪的fd(文件描述符)未被用户进程处理,下一次查询依旧会返回,这也是select和poll的触发方式

边缘触发(Edge Triggered,ET):无论就绪的fd(文件描述符)是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发

总结

epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超selectpoll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。

Reference

一文读懂阻塞、非阻塞、同步、异步

IO多路复用的三种机制

select/poll/epoll对比分析

select、poll、epoll之间的区别总结

IO多路复用之select总结

IO多路复用之poll总结

IO多路复用之epoll总结

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。