Linux的IO模型介绍-epoll、poll、select

同步与异步IO

这个概念其实很简单,A让B去做一件事情,然后B先回复了一声“好的”,再转而去做这件事情,那么这就是异步。如果B先做完了这件事情,然后再回复A“我做好了”,那么这就是同步。
所以说,同步还是异步,那是针对被调用者B来说的。

阻塞与非阻塞IO

这个概念就更简单了,A让B去做一件事情,如果在B做事情这段时间,A会把自己挂起(其实就是把自己搞到等待队列中,直到被B唤醒)。如果A不做这种无意义的等待,而是继续搞自己的其他事情,等B的通知来了就来了,那么这就是非阻塞。
所以说,阻塞还是非阻塞是针对调用者A来说的。

这俩类根本就不是一回事情

Linux的IO模型

其实Linux的IO模型非常简单。
举个例子,当用户调用read准备读取某个IO的东西时:
1.相关IO的数据会被拷贝到内核态内存缓冲区中;
2.将内核态的内存缓冲区内容拷贝到用户态的进程空间中。
这么一来,四种基于此的IO模式就产生了。

1.阻塞IO

很简单,就是当用户调用这种类型的read方法尝试通过系统调用获取IO数据时,在一开始的时候由于需要准备数据,加上多次的内存拷贝,所以调用者就会把自己挂起来,直到read操作完成了,才会被唤醒。(很显然,上述两个步骤都被blocked了)
在这里插入图片描述

非阻塞IO

非阻塞IO就很直白了,当调用者调用了非阻塞类型的read操作时,由于很有可能上述两步操作没有完成,于是read操作会直接当时立刻马上就得到一个false的操作结果。read的调用者一看,卧槽,false,于是就知道了,IO根本就没有准备好数据!那咋办?很简单,调用者就会while一直问,“IO啊,你操作完了么?”就这么一直问,一直问。
在这里插入图片描述

异步IO

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
在这里插入图片描述

多路复用IO(其实这就是同步IO!!!)

有人也称之为“事件驱动型IO”,等一下你就明白这是什么道理了。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

select

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

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

poll

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

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
poll和select其实就是引进的一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。

epoll

epoll是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll操作过程需要三个接口,分别如下:

  1. int epoll_create(int size);
    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
    当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    函数是对指定描述符fd执行op操作。或者咱们直白的说,就是往epoll对象中增加/删除某一个流的某一个事件作为监控对象。

  • epfd:是epoll_create()的返回值。
  • op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
  • fd:是需要监听的fd(文件描述符)
  • epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
那么完整且简洁的epoll用法是什么呢?

while true {
	active_stream[] = epoll_wait(epollfd)
	for i in active_stream[] {
		read or write untill unavailable
	}
}

是不是非常简单?

epoll的两种工作模式:电平触发与边沿触发

卧槽,这么像FPGA?
边沿触发就很好理解,ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。
为啥是非阻塞呢?因为你要是阻塞了,那么万一把别的IO就绪上升沿给一起阻塞看不到了,咋整???
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。

电平触发就更好理解了。
它同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

总结

epoll就很棒,它不用遍历文件描述符,而是通过监听回调的的机制。咱们找点时间分析一下epoll的底层实现,听说好像和mmap有关系。

猜你喜欢

转载自blog.csdn.net/weixin_44039270/article/details/106878667