操作系统(五)网络 I/O
Linux五种IO模型
- 阻塞I/O: 这是最常见的I/O模型。在此模式中,当应用程序执行I/O操作时,如果数据还没有准备好,应用程序就会被阻塞(挂起),直到数据准备好为止。这期间,应用程序不能做其他事情。
- 非阻塞I/O:在此模式中,如果I/O操作的数据还没有准备好,操作会立即返回一个错误,而不是阻塞应用程序。应用程序可以继续执行其他操作,也可以反复尝试该I/O操作。
- I/O多路复用:也常称为事件驱动I/O。在此模式中,应用程序可以同时监控多个I/O描述符(比如,socket),当任何一个I/O描述符准备好数据时,应用程序就可以对其进行处理。这可以在一个单独的进程或线程中同时处理多个I/O操作,并且不需要阻塞或轮询。select、poll、epoll都是这种模型的实现。
- 信号驱动:在此模型中,应用程序可以向操作系统注册一个信号处理函数,当数据准备好时,操作系统会发送一个信号,应用程序可以在接收到信号时读取数据。这种模式避免了阻塞和轮询,但是编程复杂性较高。
- 异步I/O:在此模型中,应用程序发起I/O操作后,可以立即开始做其他事情,当数据准备好时,操作系统会将数据复制到应用程序的缓冲区,并通知应用程序。这种模型的优点是应用程序不需要等待I/O操作的完成,缺点是编程复杂性较高。
5 种 I/O 模型主要是阻塞 I/O、非阻塞 I/O、信号驱动式 I/O、I/O 多路复用、异步 I/O,其中前四个都属于同步I/O模型。
阻塞同步I/O模型和非阻塞同步I/O模型的区别在于:进程发起系统调用后,是会被挂起直到收到数据后在返回、还是立即返回成功或错误。
同步I/O 和异步I/O 的区别在于:将数据从内核复制到用户空间时,用户进程是否会阻塞,如果用户进程会阻塞,则是同步I/O,如果不会阻塞就是异步 I/O。
阻塞IO和非阻塞IO的应用场景问题,有一个计算密集型的场景,和一个给用户传视频的场景,分别应该用什么io?
计算密集型的场景,需要消耗的是 CPU 资源,用阻塞 IO 会比较好,如果用非阻塞 IO,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
用户传视频的场景,瓶颈不是 CPU 资源,用非阻塞 IO 比较好,使用非阻塞IO可以避免阻塞在传输函数上,提高程序的并发性和响应时间。
谈谈你对 I/O 多路复用的理解
如果不使用 I/O 多路复用,服务端要并发处理多个客户端的 I/O 事件的话,需要通过创建子进程或者线程的方式来实现,也就是针对每一个连接的 I/O 事件要需要一个子进程或者线程来处理,但是随着客户端越来越多,意味着服务端需要创建更多的子进程或者线程,这样对系统的开销太大了。
那么有了 I/O 多路复用就可以解决这个问题,I/O 多路复用可以实现是多个I/O复用一个进程,也就是只需要一个进程就能并发处理多个客户端的 I/O 事件, 进程可以通过select、poll、epoll这类 I/O 多路复用系统调用接口从内核中获取有事件发生的 socket 集合,然后应用程序就可以遍历这个集合,对每一个 socket 事件进行处理。
Redis 单线程也能做到高性能的原因,也跟 I/O 多路复用有关系。
select、poll、epoll 有什么区别?
select 和 poll 内部都是使用「线性结构」来存储进程关注的 Socket 集合,在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。 很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,epoll 通过两个方面解决了 select/poll 的问题。
epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,通过 epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait()
函数时,只会返回有事件发生的文件描述符链表表,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
select、poll、epoll 适合哪些应用场景?
在连接数较少并且都十分活跃的情况下,选择 select 或者 poll 会比 epoll 性能好,因为 epoll 中的所有描述符都存储在内核中,必须使用epoll_ctl来添加到内核的红黑树中,这就意味着每一个新的连接都需要两次系统调用(epoll_ctl+epoll_wait),而在select 和poll中只需要一次,频繁系统调用降低效率。
在连接数较多并且有很多的不活跃连接时,epoll 会比 select 和 poll 性能好,因为 epoll 在内核用红黑树来关注所有待检测的 socket,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配,再加上 epoll 有就绪队列,也不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。
epoll ET 模式和 LT 模式有什么区别?哪一个更高效?
ET 模式和 LT 模式 区别如下:
- 边缘触发(ET,Edge Trigger):当描述符从未就绪变为就绪时,只会通知一次,之后不会再通知,因此我们程序要保证一次性将事件处理完。
水平触发(LT,Level Trigger):当文件描述符就绪时,会触发通知,如果用户程序没有一次性把数据读/写完,下次还会发出可读/可写信号进行通知。
边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数。
零拷贝技术了解过吗?说一下原理
传统文件传输:
- 共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
- 其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的。
sendfile:
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
- 首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。
- 其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。
零拷贝:
对于支持网卡支持 SG-DMA 技术的情况下, sendfile()
系统调用的过程发生了点变化,具体过程如下:
- 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
- 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝,这个过程之中,只进行了 2 次数据拷贝;
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
所以零拷贝技术主要是为了提升文件传输的效率,Kafka 消息队列 I/O 的吞吐量高的原因,也是因为使用了零拷贝技术。
reactor 模式有哪些方案?
常见的 Reactor 实现方案有三种。
第一种方案单 Reactor 单进程 / 线程,不用考虑进程间通信以及数据同步的问题,因此实现起来比较简单,这种方案的缺陷在于无法充分利用多核 CPU,而且处理业务逻辑的时间不能太长,否则会延迟响应,所以不适用于计算机密集型的场景,适用于业务处理快速的场景,比如 Redis(6.0之前 ) 采用的是单 Reactor 单进程的方案。
第二种方案单 Reactor 多线程,通过多线程的方式解决了方案一的缺陷,但它离高并发还差一点距离,差在只有一个 Reactor 对象来承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
第三种方案多 Reactor 多进程 / 线程,通过多个 Reactor 来解决了方案二的缺陷,主 Reactor 只负责监听事件,响应事件的工作交给了从 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案,Nginx 则采用了类似于 「多 Reactor 多进程」的方案。
proactor 和 reactor 模式有什么区别?
- Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
- Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
因此,Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。
参考: