一、硬中断软中断
我们知道系统为了安全,在调用一些指令时需要进行用户态和内核态转换,如只有在内核态才能执行读写命令,这样保证了系统的安全。
注:程序里的read()函数和系统指令read()是两个东西,第二个才是内核态执行的东西,第一个只是进入内核态
为了能进行用户态和内核态状态转换,需要进行cpu中断,才能从用户态进行内核态,而中断分为硬中断和软中断:
- 硬中断:外部设备产生,可能发生在任意时间。比如网卡接收到报文,报文被DMA(网卡上的内嵌设备)映射报文到内存的网卡缓冲区,网卡向CPU发起中断信号IRQ iterrupt request。cpu挂起正在执行的进行,进入内核态,去处理网卡的中断程序。执行完后cpu从内核态切回用户态,根据进程描述符加载被挂起的进程继续执行
- 挂起是保存到进程描述符。寄存器里保存的比如代码段入口、行号、堆栈地址、操作数1、操作数2。
- 软中断:如程序运行过程中自己产生的一些中断
- 如要进行系统调用system_call,则发起
0X80中断 - 如程序执行碰到除0异常
- 如要进行系统调用system_call,则发起
这里说说80中断
系统调用:80中断
系统调用指的是:比如用户想要读取硬盘上的文件,发起read调用,这个read只是内核态的库函数api,该api会发起系统调用中断后去调用真正的底层read。
因为系统调用system_call函数所对应的中断指令编号是0X80(十进制就是8×16=128),而该指令编号对应的就是系统调用程序的入口,所以我们称系统调用未80中断。
其他中断程序映射可以见下表
系统调用流程
- 在cpu寄存器里存一个系统调用号,(表示哪个系统函数,比如read)。
- 将cpu的当前寄存器信息都保存到thread_info中(恢复到用户态时用)
- 把CPU寄存器信息保存到进程描述符PCB里
- 把CPU堆栈地址的指向指向内核空间。非的持续性
- 然后执行80中断处理程序,找到刚刚存的系统调用号(比如read),先检查缓存中有没有对应的数据,没有就去磁盘中加载到内核缓冲区,然后从内核缓冲区拷贝到用户空间,
- 然后恢复到用户态,恢复现场,用户态就知道从哪继续执行。
用户内核态切换
- 用户空间:用户代码、用户堆栈
- 内核空间:内核代码、内核调度程序、进程描述符(内核堆栈、thread_info进程描述符)
- 进程描述符和用户的进程是一一对应的
- SYS_API系统调用:,如read、write。系统调用就是0X80中断
- 进程描述符pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息,
- 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器。。。方便程序从内核态切换回用户态时恢复现场。
- 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配,

进程空间

二、早期IO
进行数据处理和网络数据处理时,考虑到硬件读取速度慢,都不能直接在硬件上处理,所以需要在内存中进行处理。
主要流程为:
- 读:用户态切换到内核态,先看内核态缓冲区有没有,有就直接读到,没有就交给DMA去读。DMA控制器从磁盘、网卡、其他IO设备中读。CPU在这个期间可以执行其他进程。DMA加载到内核缓冲区后告诉CPU,CPU把数据拷贝到用户态,CPU把该进程从阻塞队列移到到运行队列。
- 写:缓存区满了之后,写操作阻塞,缓冲区有一个等待队列,记录阻塞的进程(java的轻量级进程),DMA把缓冲区数据写到网卡后告诉CPU,中断CPU,把该进程移动到运行队列。
三、DMA直接内存拷贝
DMA( direct memory access) :直接内存拷贝
特点:减少使用CPU
定义:DMA的出现就是为了解决批量数据的输入/输出问题。DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。这样数据的传送速度就取决于存储器和外设的工作速度。
场景:要把内存数据发送到网卡然后发出去时:
- 没有DMA时候怎么办:CPU读内存数据到CPU的高速缓存,再写到网卡。这样就把CPU的速度拉低到和网卡一个速度。
- 有了DMA:把内存数据读到socket内核缓存区(CPU复制),CPU就不管了,告诉DMA开始接管。DMA开始把内核缓冲区的数据写到网卡。DMA读socket缓冲区,读到DMA缓冲区,然后写到网卡中。不停写到网卡。
补充知识:kernel buffer和socket buffer的区别
kernel buffer和socket buffer的区别
kernel buffer —>socket buffer
但从操作系统的角度来看,这已经是零拷贝,因为没有数据从内核空间复制到用户空间。 内核需要复制的原因是因为通用硬件DMA访问需要连续的内存空间(因此需要缓冲区)。 但是,如果硬件支持scatter-and-gather,这是可以避免的。
引入DMA后的传统IO
DMA相关阅读:https://blog.youkuaiyun.com/z69183787/article/details/104334247
通常系统总线是由CPU管理的,在DMA方式时,就希望CPU把这些总线让出来,即CPU连到这些总线上的线处于第三态(高阻状态),而由DMA控制器接管,控制传送的字节数,判断DMA是否结束,以及发出DMA结束信号。因此DMA控制器必须有以下功能:
1、能向CPU发出系统保持(HOLD)信号,提出总线接管请求;
2、当CPU发出允许接管信号后,负责对总线的控制,进入DMA方式;
3、能对存储器寻址及能修改地址指针,实现对内存的读写;
4、能决定本次DMA传送的字节数,判断DMA传送是否借宿。
5、发出DMA结束信号,使CPU恢复正常工作状态。

DMA传输将从一个地址空间复制到另外一个地址空间。当CPU初始化这个传输动作,传输动作本身是由DMA控制器来实行和完成。 典型例子—移动一个外部内存的区块(硬盘)到芯片内部更快的内存区(内核缓冲区)。
DMA接收数据
DMA方式是一种完全由硬件进行组信息传送的控制方式,具有中断方式的优点,即在数据准备阶段,CPU与外设并行工作。
一个完整的DMA传输过程必须经历DMA请求、DMA响应、DMA传输、DMA结束4个步骤。
①CPU读指定的数据:
- 先检查内核缓冲区里是否有指定的数据
- 如果有:直接就可以读
- 如果没有:CPU就交给DMA,DMA负责把硬盘读到缓冲区
②DMA读完数据后:
- 读完后DMA发起CPU中断(硬中断),告诉CPU移动完了,这样CPU就知道socket内核缓冲区又空出来了
- CPU从用户态切换到内核态,执行中断处理程序,将socket缓冲区阻塞的进程移回到运行队列。
- 比如要发送的数据是100k,但是内核缓冲区就50k,这样第二次50k也能发出去了。
对于实现DMA传输,它是由DMA控制器直接掌管总线(地址总线、数据总线和控制总线),因此,存在一个总线控制权转移问题
DMA发送数据
四、传统IO
初学 Java 时,我们在学习 IO 和 网络编程时,会使用以下代码:
需求:调用 read 方法读取 index.html 的内容—— 变成字节数组,然后调用 write 方法,将 index.html 字节流写到 socket 中
// 打开文件流
File file = new File("index.html");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
// 把流读到字节数组
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
// 把数组把给socket
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
那么,我们调用这两个方法,在 OS 底层发生了什么呢?我这里借鉴了一张其他文字的图片,尝试解释这个过程。

上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作。下面说说他们的步骤:
- \1. read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。
- \2. 发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
- \3. 发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
- \4. 第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
- \5. write 方法返回,再次从内核态切换到用户态。
如你所见,复制拷贝操作太多了。如何优化这些流程?零拷贝技术
场景:消息队列持久化后,有消费者来了,要进行消费,那么我们需要将消息从硬盘读取到网卡中发送出去。
进程去读,先看内核缓冲区有没有,没有就告诉DMA去读到内核缓冲区,然后把进程放到内核的阻塞队列中,DMA读好后发起硬件中断告诉CPU,CPU唤醒阻塞进程,从内核缓冲区读到用户数据缓冲区,然后再切换到内核态进程写操作,写到socket缓冲区后,告诉DMA把socket缓冲区的数据写到网卡。复制了4次,进程切换了4次。
切换具体流程:
- JVM发出read() 系统调用。
- OS上下文切换到内核模式(第一次上下文切换)并将数据从网卡或硬盘等通过DMA读取到内核空间缓冲区。(第一次拷贝:hardware网卡 ----> kernel buffer内核缓存区)
- OS内核然后将数据复制到用户空间缓冲区(第二次拷贝: kernel buffer内核缓存区 ——> user buffer用户缓冲区),然后之前的read系统调用返回。而系统调用的返回又会导致一次内核空间到用户空间的上下文切换(第二次上下文切换)。
- JVM处理代码逻辑并发送write()系统调用。
- OS上下文切换到内核模式(第三次上下文切换)并从用户空间缓冲区复制数据到内核空间缓冲区(第三次拷贝: user buffer ——> kernel buffer)。
- write系统调用返回,导致内核空间到用户空间的再次上下文切换(第四次上下文切换)。将内核空间缓冲区中的数据写到hardware(第四次拷贝: kernel buffer ——> hardware)
数据流转:
传统IO总结
总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。显然在这个用例中,从内核空间到用户空间内存的复制是完全不必要的,因为除了将数据转储到不同的buffer之外,我们没有做任何其他的事情。所以,我们能不能直接从hardware读取数据到kernel buffer后,再从kernel buffer写到目标地点不就好了。为了解决这种不必要的数据复制,操作系统出现了零拷贝的概念。
另外一张图
五、零拷贝
注意,不同的操作系统对零拷贝的实现各不相同。在这里我们介绍linux下的零拷贝实现。
零拷贝基本介绍:
- 零拷贝是网络编程的关键,很多性能优化都离不开。
- 在 Java 程序中,常用的零拷贝有
mmap(内存映射)和sendFile。 - 另外我们看下 NIO 中如何使用零拷贝
零拷贝是为了不要在内核缓冲区和用户空间之间拷贝
1、mmap零拷贝
mmap(Memory Mapped Files)
思想:内核空间和用户空间共享内存,省去用户态和内核态之间的拷贝
内核空间就是内核缓存冲


有别的地方这么说,我觉得不对:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
原理:直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(DMA操作系统在适当的时候)。
DMA把磁盘上的文件映射到内存,用户空间和内核空间共享同一块物理地址,这样就无需进程用户空间和内核空间的来回复制。写到网卡的时候,共享空间的内容拷贝到socket缓冲区(CPU复制),然后告诉DMA发送到网卡。3次复制(2次DMA,一次CPU复制)
缺陷:不可靠,落盘时,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。(很多中间件都是这么做的,不直接刷盘,而是系统根据规则决定干什么时候冲刷内核缓冲区到磁盘)
根据上面的描述,cpu都不用管理进行拷贝了,从硬盘到内核空间有DMA进行操作,而内核空间到用户空间有共享内存作为支撑无需拷贝。
mmap流程
第一步:mmap系统调用使DMA引擎将文件内容(硬盘)复制到内核缓冲区中。然后与用户进程共享缓冲区,而无需在内核和用户内存空间之间执行任何复制。
第二步:写系统调用使内核将数据从原始内核缓冲区kernel buffer复制到与套接字关联的内核缓冲区socket buffer中。
第三步:第三份复制发生在DMA引擎将数据从内核套接字缓冲区传递到协议引擎时。
- 发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件(或网卡)中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
- mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。
- 发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。
- write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。
mmap 示意图

如上图,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 Socket 缓冲区。
现在,你只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。
3次拷贝 3次切换
NIO使用mmap
Java NIO,提供了一个 MappedByteBuffer 类可以用来实现mmap内存映射。
MappedByteBuffer只能通过调用FileChannel.map()取得,再没有其他方式。
FileChannel.map()是抽象方法,具体实现是在 FileChannelImpl.c 可自行查看JDK源码,其map0()方法就是调用了Linux内核的mmap的API。
应用案例见下面
kafka使用mmap
kafka的落盘技术使用了mmap,所以Kafka提供了一个参数——producer.type来控制是不是主动flush;如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。
| 落盘 | 消费 | |
|---|---|---|
| kafka | mmap | sendfile |
| RocketMQ | mmap |
2、send-file
磁盘文件通过网络发送(Broker 到 Consumer)
定义:打开文件的文件描述符fd+socket的fd告诉sendfile,也是经过和上面一样的3次复制。不过只进行了2次用户态和内核态的切换
原理:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换

如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用,然后掉一共 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为在一个用户空间。
最后,数据从 Socket 缓冲区进入到协议栈。
此时,数据经过了 3 次拷贝,3 次上下文切换。
那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?
实际上,Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
sendfile改进
而 Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIO Buffer(socket buffer),无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示。
引进了sendfile2.4之后,sendfile实现了更简单的方式,不同之处在于,文件到达内核缓冲区后,不必再将数据全部复制到socket buffer缓冲区,而只将记录数据位置和长度相关的数据保存到socket buffer(代替复制),而数据实际由DMA模块直接发送给协议相关引擎,再次降低了复制操作。
Linux 在 2.4 版本中,做了一些修改,避免了从 内核缓冲区kernel buffer到 Socket buffer 的拷贝,直接从内核缓冲区拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结:
- 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
- 没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket buffer缓冲区当中。该描述符包含了两方面的信息:
- a) kernel buffer的内存地址;
- b) kernel buffer的偏移量。
- sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。
kernel buffer拷贝到socket buffer只拷贝少量信息
- 这里其实有 一次 cpu 拷贝:
kernel buffer -> socket buffer
但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略
现在,index.html 要从文件进入到网络协议栈,只需 2


最低0.47元/天 解锁文章
826

被折叠的 条评论
为什么被折叠?



