900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > Linux I/O多路复用整理

Linux I/O多路复用整理

时间:2023-01-31 04:11:39

相关推荐

Linux I/O多路复用整理

前言

服务端一般为了读数据,要经过如下几个流程:

listenfd = socket(); // 打开一个网络通信端口bind(listenfd); // 绑定listen(listenfd);// 监听while(1) {connfd = accept(listenfd); // 阻塞建立连接int n = read(connfd, buf); // 阻塞读数据doSomeThing(buf); // 利用读到的数据做些什么close(connfd);// 关闭连接,循环等待下一个连接}

需要注意的是,accept(listenfd)read(connfd, buf)两个操作是会阻塞的

其中,accept我们容易理解,就是在等待客户端的连接请求。

而read负责读取客户端即用户发来的数据,主要分为以下两步:

用户数据从网卡拷贝到内核缓冲区用户数据从内核缓冲区再拷贝到用户缓冲区

这个read阻塞问题很大,如果用户一直没有发数据,是会一直阻塞在这里的。

这种情况下就叫做阻塞I/O

怎么办呢?

一种解决方案是为read操作申请个线程,让线程去等,但这种情况仍是阻塞的,还是一种阻塞I/O。

另外一种解决方案就是非阻塞I/O,也叫无阻塞I/O,这种方式寄希望于系统能够提供一种原语(系统调用),来实现非阻塞的I/O。

这个 read 函数的效果是,如果没有数据到达时(到达网卡并拷贝到了内核缓冲区),立刻返回一个错误值(-1),而不是阻塞地等待。

操作系统提供了这样的功能,只需要在调用 read 前,将文件描述符设置为非阻塞即可。

fcntl(connfd, F_SETFL, O_NONBLOCK);int n = read(connfd, buffer) != SUCCESS);

这样,为每个连接的请求分配一个线程,之后该用户线程循环调用系统提供的原语read,直到返回值不为 -1,再开始处理业务,这就实现了非阻塞。

上述这样就完全没问题了吗?当然不是

上面的方案还是给每个客户端请求分配了一个线程,资源占用极大。

那么就有新的一种优化思路:I/O多路复用

当然还有个聪明的办法,我们可以每 accept 一个客户端连接后,将这个文件描述符(connfd)放到一个数组里。

fdlist.add(connfd);

然后弄一个新的线程去不断遍历这个数组,调用每一个元素的非阻塞 read 方法。

while(1) {for(fd <-- fdlist) {if(read(fd) != -1) {doSomeThing();}}}

这样,我们就成功用一个线程处理了多个客户端连接。

I/O多路复用已经被封装实现了,其目前主流实现方式有SelectPollEpoll,I/O多路复用是单个线程记录跟踪每一个I/O流的状态来同时管理多个I/O流,一旦发现有I/O流( 表现为文件描述符 )可以进行无阻塞I/O访问时候,就进行数据处理。

值得注意的是io_during是一种新的多路复用方式。

什么叫可以进行无阻塞I/O访问呢?

I/O多路复用程序监听多个Socket,并将Socket产生的事件放入队列中排队,有个叫事件分配器的每次从队列中拿出来一个事件进行处理,当这个事件处理完之后,就可以进行无阻塞I/O访问了。

这可以避免来一个请求就分配一个线程的弱点。

接下来介绍下SelectPollEpoll之间有何不同

Select

函数原型和参数说明

函数原型

int select(int n,fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout);

参数说明

int n: 集合中所有文件描述符的范围,select函数只会监视小于n(大于等于0)的文件描述符。应填写所有文件描述符的最大值加1fd_set * readfds: 这个指针作为参数传入函数时,包含了要监视是否可以读取数据的文件描述符,函数返回时将可以读取的文件描述符对应的bit保持置1,否则置0fd_set * writefds: 这个指针作为参数传入函数时,包含了要监视是否可以写入数据的文件描述符,函数返回时将可以写入的文件描述符对应的bit保持置1,否则置0fd_set * exceptfds: 同上,用来监视文件错误异常文件timeval * timeout: 超时时间,如果达到此时间没有监控到符合要求的文件描述符,则会强制函数返回0

返回值:int类型,代表可读文件描述符的数量

值得注意的是,方法参数中有三个fd数组,我们在调用select函数的时候,如果3类监控条件中有无需监控的,可以直接传入立即数0,例如:

retval = select(2, &readable_fds, 0, 0, &timeout);

说明

select 会修改传入的参数数组,并且select 调用需要传入fd数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的select 只会告诉你可读文件描述符的数量,不会告诉你具体对应的文件描述符是哪个I/O流有数据,你得自己轮询, 也因此select函数调用的时间复杂度是O(n)select只能监视1024个连接,因为内核原因定义#define __FD_SETSIZE 1024select不是线程安全的select同时支持windows和linux
为什么select中定义最大连接数为1024?

因为进程的文件描述符上限默认是1024,但比较有意思的是,进程的文件描述符上限是可以改的,但要想扩大Select的支持,只能重新编译内核

Poll

函数原型

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

参数说明

timeout参数是超时事件,如果为-1,则代表无限阻塞等待,如果0,则立即返回

fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件,pollfd结构体的定义如下:

struct pollfd{int fd; //指定文件描述符 short events;//关心的事件 short revents; //实际发生的事件,由内核来填充}

poll与select一样,也是采用轮询的方式来检查文件描述符是否处于就绪态,也因此poll的时间复杂度也是O(n)没有1024限制,因为poll采用链表来进行文件描述符的存储,没有最大限制与Select一样,不是线程安全的poll只有Linux支持

Epoll

Epoll调用由三个函数组成:

int epoll_create(int size):创建一个epoll句柄,size用来告诉内核监听的fd数目有多大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):类似于select调用,等待事件的产生,下面是epoll_wait的过程 Epoll 是线程安全的epoll只有Linux支持它所支持的FD上限是最大可以打开文件的数目epoll的核心是3个API,核心数据结构是:红黑树和链表epoll支持边缘触发(edge trigger,ET)或水平触发(level trigger,LT)两种模式,默认采用水平触发,那么Epoll可以看做是增强版的poll;如果采用边缘触发,那么Epoll可以告诉你具体是哪个I/O流有数据,通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程。也因此Epoll的时间复杂度是O(1)

一、水平触发(EPOLLLT)

水平触发:只要缓冲区有数据,epoll_wait就会一直被触发,直到缓冲区为空;

水平触发(EPOLLLT)是epoll默认的工作模式,其优缺点如下:

优点:保证了数据的完整输出;缺点:当数据较大时,需要不断从用户态和内核态切换,消耗了大量的系统资源,影响服务器性能;

应用场景:应用较少,一般用于连接请求较少及客户端发送的数据量较少的服务器,可一次性接收所有数据。此时,若使用边沿触发,会多调用一次accept/read等来判断数据是否为空。

二、边沿触发(EPOLLET)

边沿触发:只有所监听的事件状态改变或者有事件发生时,epoll_wait才会被触发;

epoll边沿触发时,假设一个客户端发送100字节的数据,而服务器设定read每次读取20字节,那么一次触发只能读取20个字节,然后内核调用epoll_wait直到下一次事件发生,才会继续从剩下的80字节读取20个字节,由此可见,这种模式其工作效率非常低且无法保证数据的完整性,因此边沿触发不会单独使用。

边沿触发通常与非阻塞IO一起使用,其工作模式为:epoll_wait触发一次,在while(1)循环内非阻塞IO读取数据,直到缓冲区数据为空(保证了数据的完整性),内核才会继续调用epoll_wait等待事件发生。

拾遗

虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

关于Redis中的I/O多路复用

Redis 内部使用单线程的文件事件处理器file event handler

采用IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理分发器会把事件分发给具体的处理器:

连接应答处理器:对连接服务器的各个客户端进行应答。命令请求处理器:接收客户端传来的命令请求。命令回复处理器:向客户端返回命令的执行结果复制处理器:当主服务器和从服务器进行复制操作时, 主从服务器都需要关联此处理器。

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