在介绍网络IO模型,我们先来看一下同步和异步,以及阻塞和非阻塞的概念。
同步和异步关注的是结果消息的通信机制
- 同步:同步的意思就是调用方需要主动等待结果的返回
- 异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。
阻塞和非阻塞主要关注的是等待结果返回调用方的状态
- 阻塞:是指结果返回之前,当前线程被挂起,不做任何事
- 非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。
然后我们就来了解一些基本的网络IO模型
- 阻塞I/O(blocking I/O)
- 非阻塞I/O (nonblocking I/O)
- I/O复用(select 、poll和epoll) (I/O multiplexing)
- 信号驱动I/O (signal driven I/O (SIGIO))
- 异步I/O (asynchronous I/O )
阻塞I/O模型
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待,知道数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
当调用recvfrom()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用recvfrom()函数时,未必用户空间就已经存在数据,那么此时recvfrom()函数就会处于等待状态。
上述说到了数据从内核拷贝到用户空间的问题,这里我们在Java内存区域就曾经介绍过了,如下:
非阻塞IO模型
我们把一个Socket接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。上述模型绝不被推荐。
把Socket设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字多次调用recvfrom()函数的过程。前三次调用recvfrom()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recvfrom()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recvfrom()函数返回成功指示,应用程序开始处理数据。
IO复用模型
简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO模型并没有什么优越性;关键是能实现同时对多个IO端口进行监听
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
当用户进程调用了select,那么整个进程会被block;而同时,kernel会“监视”所有select负责的socket;当任何一个socket中的数据准备好了,select就会返回。这个时候,用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
这里我们主要来看一下select、poll、epoll的区别?
1、支持一个进程所能打开的最大连接数
方法 | 描述 |
---|---|
select | 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),可以对进行修改,然后重新编译内核,但是性能可能会受到影响。 |
poll | poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。 |
epoll | 连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。 |
2、FD剧增后带来的IO效率问题
方法 | 描述 |
---|---|
select | 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 |
poll | 同上 |
epoll | 因为epoll内核中实现是根据每个FD上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 |
3、消息传递方式
方法 | 描述 |
---|---|
select | 内核需要将消息传递到用户空间,都需要内核拷贝动作。 |
poll | poll同上 |
epoll | epoll通过内核和用户空间共享一块内存来实现的。 |
信号驱动IO模型
简介:两次调用,两次返回
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
异步IO模型
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。
I/O模型的比较
最后提一下 Level_triggered(水平触发) 和 Edge_triggered(边缘触发)
-
Level_triggered(水平触发)
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。 -
Edge_triggered(边缘触发)
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。
来源:CSDN
作者:kami0107
链接:https://blog.csdn.net/newbie0107/article/details/103792305