1

多路复用

关于什么是I/O多路复用,在知乎上有个很好的回答,可以参考罗志宇前辈的回答
记录下自己的理解:忘记这个坑爹的中文翻译。记住I/O multiplexing
              图片描述

I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流.
I/O多路复用这一技术。简单来说,就是一个线程追踪多条io流(读,写,异常),但不使用轮询,而是由设备本身告知程序哪条流可用了,这样一来就解放了cpu,也充分利用io资源,下文主要讲解如何实现这一技术,linux下这一技术有三个实现,select,poll,epoll。

阻塞I/O模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下,我们可以通过循环把集合中的流从头到尾问一遍,这样就可以处理多个流了,但这样的做法显然不好,因为如果所有流都没有数据,那么只会白白浪费CPU.
为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流

fcntl I/O多路复用

此种方法与select相似,不作介绍

I/O Multiplexing --> select

#include <sys/select.h>  
/**
 *select将更新这个集合,把其中不可读的套节字去掉只保留符合条件的套节字在这个集合里面    
 */
int select(int nfds, fd_set *readfds, fd_set *writefds,  
           fd_set *exceptfds, struct timeval *timeout); 

参数readfds、writefds、exceptfds都是指向文件描述符的指针,数据类型为fd_set。而readfds是用来检测输入的,writefds是用来检测输出的,exceptfds使用检测是否异常的。有关fd_set通常有四个宏供我们操作:FD_ZERO、FD_SET、FD_CLR、FD_ISSET。

  • FD_ZERO(fd_set *set); //fd_set所指向的集合清空
  • FD_SET(int fd,fd_set *set);//文件描述符fd添加到fd_set所指向的集合中
  • FD_CLR(int fd,fd_set *set);//文件描述符fd从fd_set所指向的集合中移除
  • int FD_ISSET(int fd,fd_set *set);//文件描述符fd是fd_set所指向的集合中的成员,则FD_ISSET返回true,否则返回false

文件描述符集合有一个最大容量限制,由常量FD_SETSIZE来决定,在Linux上,该常量值为1024。
参数timeout为超时时间。

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

int main(void)
{
        int bytes_read, ready;
        char buffer[128];
        fd_set readfds;
        struct timeval timeout;

        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds);

        timeout.tv_sec = 10;
        timeout.tv_usec = 0;

        ready = select(STDIN_FILENO+1, &readfds, NULL, NULL, &timeout);

        if (ready) {
                bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
                if (buffer[bytes_read - 1] == '\n')
                        buffer[bytes_read - 1] = '\0';
                printf("%s\n", buffer);
        } else {
                printf("No data to read\n");
        }

        return 0;
}

I/O Multiplexing --> poll

由于I/O Multiplexing->select存在以下问题

  • select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
  • select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。
  • select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
  • select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.

因此14年后(1997)出现了POLL,修复select的以下问题

  • poll 去掉了1024个链接的限制
  • poll 从设计上来说,不再修改传入数组
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);  

struct pollfd{  
    int fd;         //文件描述符  
    short events;   //等待的事件  
    short revents;  //实际发生的事件  
};  
  • 第一个参数:

 每个pollfd结构体指定了一个被监视的文件描述符。第一个参数是一个数组,即poll函数可以监视多个文件描述符。每个结构体的events是监视该文件描述符的事件掩码,由用户来设置。revents是文件描述符的操作结果事件,内核在调用返回时设置。events中请求的任何事件都可能在revents中返回
图片描述

  • 第二个参数nfds:
    要监视的描述符的数目。
  • 第三个参数timeout
    指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回.

图片描述


I/O Multiplexing -->epoll

问题:使用select,我们有O(n)的无差别轮询复杂度,随着处理的流越多,无差别轮询时间就越长
此时epoll产生,epoll可以理解为event poll,不同于(select/poll)无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。


如果觉得我的文章对你有用,请随意赞赏
载入中...