Skip to content

《面渣逆袭》操作系统 篇 · 第 6/6 章。原版 PDF(下载 / 打印)

零拷贝了解吗?

假如需要文件传输,使用传统I /O ,数据读取和写入是用户空间到内核空间来回赋值,而内核空间的数据是通过操作系统的I /O 交叉从磁盘读取或者写入,这期间发生了多次用户态和内核态的上下文切换,以及多次数据拷贝。

配图

为了提升I /O 性能,就需要减少用户态与内核态的上下文切换内存拷贝的次数

这就用到了我们零拷贝的技术,零拷贝技术实现主要有两种:

mmap+write

mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷⻉操作。

配图

sendfile

在 Linux 内核版本 2.1 中,提供了一个专⻔发送文件的系统调用函数 sendfile() 。

首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。

其次,该系统调用,可以直接把内核缓冲区里的数据拷⻉到 socket 缓冲区里,不再拷⻉到用户态,这样就只有 2 次上下文切换,和 3 次数据拷⻉。

配图

很多开源项目如Kafka 、RocketMQ 都采用了零拷贝技术来提升IO效率。

聊聊阻塞与非阻塞 I/O 、 同步与异步 I/O?

阻塞I /O

先来看看阻塞****I/O,当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷⻉到应用程序的缓冲区中,当拷⻉过程完成, read 才会返回。

注意,阻塞等待的是内核数据准备好和数据从内核态拷到用户态这两个过程

配图

非阻塞I /O

非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷⻉到应用程序缓冲区, read 调用才可以获取到结果。

配图

基于非阻塞的I /O 多路复用

我们上面的非阻塞I /O 有一个问题,什么问题呢?应用程序要一直轮询,这个过程没法干其它事情,所以引入了I/O****多路复用技术。

当内核数据准备好时,以事件通知应用程序进行操作。

配图

注意:无论是阻塞 I/O 、还是非阻塞 I/O 、非阻塞I /O 多路复用,都是同步调用。因为它们在read 调用时,内核将数据从内核空间拷⻉到应用程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷⻉效率不高,read 调用就会在这个同步过程中等待比较⻓的时间。

异步I /O

真正的异步****I/O内核数据准备好数据从内核态拷到用户态这两个过程都不用等待。

发起 aio_read 之后,就立即返回,内核自动将数据从内核空间拷⻉到应用程序空间,这个拷⻉过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷⻉动作。

配图

拿例子理解几种I /O 模型老三关注了很多UP主,有些UP主是老鸽子,到了更新的时间:

阻塞I /O 就是,老三不干别的,就干等着,盯着UP的更新。

非阻塞I /O 就是,老三发现UP没更,就去喝个茶什么的,过一会儿来盯一次,一直等到UP更新。

基于非阻塞的 I/O 多路复用好比,老三发现UP没更,就去干别的,过了一会儿B 站推送消息了,老三一看,有很多条,就去翻动态,看看等的UP是不是更新了。

异步I /O 就是,老三说UP你该更了,UP赶紧爆肝把视频做出来,然后把视频亲自呈到老三面前,这个过程不用等待。

配图

详细讲一讲I/O多路复用?

我们先了解什么是I /O 多路复用?

我们在传统的I /O 模型中,如果服务端需要支持多个客户端,我们可能要为每个客户端分配一个进程/ 线程。

不管是基于重一点的进程模型,还是轻一点的线程模型,假如连接多了,操作系统是扛不住的。

所以就引入了I/O 多路复用技术。

简单说,就是一个进程/ 线程维护多个Socket ,这个多路复用就是多个连接复用一个进程/ 线程。

配图

我们来看看I /O 多路复用三种实现机制:

select

select 实现多路复用的方式是:

将已连接的 Socket 都放到一个文件描述符集合fd_set ,然后调用 select 函数将f d_set 集合拷⻉到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历f d_set 的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个f d_set 拷⻉回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket ,再对其处理。

select 使用固定⻓度的 BitsMap ,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024 ,只能监听 0~1023 的文件描述符。

select 机制的缺点:

(1 )每次调用select ,都需要把f d_set 集合从用户态拷贝到内核态,如果f d_set 集合很大时,那这个开销也很大,比如百万连接却只有少数活跃连接时这样做就太没有效率。

(2 )每次调用select 都需要在内核遍历传递进来的所有f d_set ,如果f d_set 集合很大时,那这个开销也很大。

(3 )为了减少数据拷贝带来的性能损坏,内核对被监控的f d_set 集合大小做了限制,一般为1 024 ,如果想要修改会比较麻烦,可能还需要编译内核。

(4 )每次调用select 之前都需要遍历设置监听集合,重复工作。

poll

poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了

select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

但是 poll 和 select 并没有太大的本质区别,都是使用线性结构存储进程关注的Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的Socke ,时间复杂度为O (n) ,而且也需要在用户态与内核态之间拷⻉文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增⻓。

epoll

epoll 通过两个方面,很好解决了 select/poll 的问题。

第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过

epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是

O(logn) ,通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket集合,只需要传入一个待检测的 socket ,减少了内核和用户空间大量的数据拷和内存分配

第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数,内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

配图

epoll 的方式即使监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。因而,epoll****被称为解决

C10K****问题的利器

没有什么使我停留— — 除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。

系列内容

面渣逆袭 JavaSE 篇!

面渣逆袭 Java 集合框架篇!

面渣逆袭 Java 并发编程篇!

面渣逆袭 JVM 篇!

面渣逆袭 Spring 篇!

面渣逆袭 Redis 篇!

面渣逆袭 MyBatis 篇!

面渣逆袭 MySQL 篇!

面渣逆袭操作系统篇!

面渣逆袭计算机网络篇!

图文详解操作系统面试高频题,这次吊打面试官,我觉得稳了(手动 dog )。整理:沉默王二,戳转载链接,作者:三分恶,戳原文链接。

本文整理自三分恶《面渣逆袭》系列的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传