中断
中断基本原理
中断定义
中断机制:CPU 在执行指令时,收到某个中断信号转而去执行预先设定好的代码,然后再返回到原指令流中继续执行,这就是中断机制。
中断作用
**外设异步通知 CPU:**外设发生了什么事情或者完成了什么任务或者有什么消息要告诉 CPU,都可以异步给 CPU 发通知。例如,网卡收到了网络包,磁盘完成了 IO 任务,定时器的间隔时间到了,都可以给 CPU 发中断信号。
**CPU 之间发送消息:**在 SMP 系统中,一个 CPU 想要给另一个 CPU 发送消息,可以给其发送 IPI(处理器间中断)。
**处理 CPU 异常:**CPU 在执行指令的过程中遇到了异常会给自己发送中断信号来处理异常。例如,做整数除法运算的时候发现被除数是 0,访问虚拟内存的时候发现虚拟内存没有映射到物理内存上。
**实现系统调用:**早期的系统调用就是靠中断指令来实现的,后期虽然开发了专用的系统调用指令,但是其基本原理还是相似的。
中断产生
中断信号的产生有以下 4 个来源:
**1.外设。**外设产生的中断信号是异步的,一般也叫做硬件中断 (注意硬中断是另外一个概念)。硬件中断按照是否可以屏蔽分为可屏蔽中断和不可屏蔽中断。例如,网卡、磁盘、定时器都可以产生硬件中断。
**2.CPU。**这里指的是一个 CPU 向另一个 CPU 发送中断,这种中断叫做 IPI(处理器间中断)。IPI 也可以看出是一种特殊的硬件中断,因为它和硬件中断的模式差不多,都是异步的
**3.CPU 异常。**CPU 在执行指令的过程中发现异常会向自己发送中断信号,这种中断是同步的,一般也叫做软件中断 (注意软中断是另外一个概念)。CPU 异常按照是否需要修复以及是否能修复分为 3 类:1.陷阱 (trap),不需要修复,中断处理完成后继续执行下一条指令,2.故障 (fault),需要修复也有可能修复,中断处理完成后重新执行之前的指令,3.中止 (abort),需要修复但是无法修复,中断处理完成后,进程或者内核将会崩溃。例如,缺页异常是一种故障,所以也叫缺页故障,缺页异常处理完成后会重新执行刚才的指令。
**4.中断指令。**直接用 CPU 指令来产生中断信号,这种中断和 CPU 异常一样是同步的,也可以叫做软件中断。例如,中断指令 int 0x80 可以用来实现系统调用。
中断信号的 4 个来源正好对应着中断的 4 个作用。前两种中断都可以叫做硬件中断,都是异步的;后两种中断都可以叫做软件中断,都是同步的。很多书上也把硬件中断叫做中断 (异步中断),把软件中断叫做异常 (同步中断)。
内核对象
中断处理
有了中断之后,CPU 就分为两个执行场景了,进程执行场景 (process context) 和中断执行场景 (interrupt context)。进程的执行是进程执行场景,同步中断的处理也是进程执行场景,异步中断的处理是中断执行场景。可能有的人会对同步中断的处理是进程执行场景感到疑惑,但是这也很好理解,因为同步中断处理是和当前指令相关的,可以看做是进程执行的一部分。而异步中断的处理和当前指令没有关系,所以不是进程执行场景。
进程执行场景和中断执行场景有两个区别:一是进程执行场景是可以调度、可以休眠的,而中断执行场景是不可以调度不可用休眠的;二是在进程执行场景中是可以接受中断信号的,而在中断执行场景中是屏蔽中断信号的。所以如果中断执行场景的执行时间太长的话,就会影响我们对新的中断信号的响应性,所以我们需要尽量缩短中断执行场景的时间。
由于同步中断是软件产生的因此可以看做是进程执行的一部分,但是硬件中断在执行中断处理程序的时候会屏蔽其他中断序号,因此需要①立即快速处理。②如果比较耗时可以先预处理然后再完全处理。
中断向量号
不同的中断信号需要有不同的处理方式,那么系统是怎么区分不同的中断信号呢?是靠中断向量号。每一个中断信号都有一个中断向量号,中断向量号是一个整数。CPU 收到一个中断信号会根据这个信号的中断的向量号去查询中断向量表,根据向量表里面的指示去调用相应的处理函数。
中断信号和中断向量号是如何对应的呢?对于 CPU 异常来说,其向量号是由 CPU 架构标准规定的。对于外设来说,其向量号是由设备驱动动态申请的。对于 IPI 中断和指令中断来说,其向量号是由内核规定的。
中断框架结构
中断流程
保存现场
CPU 收到中断信号后会首先把一些数据 push 到内核栈上,保存的数据是和当前执行点相关的,这样中断完成后就可以返回到原执行点。如果 CPU 当前处于用户态,则会先切换到内核态,把用户栈切换为内核栈再去保存数据
查找向量表
保存完被中断程序的信息之后,就要去执行中断处理程序了。CPU 会根据当前中断信号的向量号去查询中断向量表找到中断处理程序。CPU 是如何获得当前中断信号的向量号的呢,如果是 CPU 异常可以在 CPU 内部获取,如果是指令中断,在指令中就有向量号,如果是硬件中断,则可以从中断控制器中获取中断向量号。那 CPU 又是怎么找到中断向量表呢,是通过 IDTR 寄存器。如下
CPU 现在已经把被中断的程序现场保存到内核栈上了,又得到了中断向量号,然后就根据中断向量号从中断向量表中找到对应的门描述符,对描述符做一番安全检查之后,CPU 就开始执行中断处理函数。
中断处理
硬中断 (hardirq)
硬件中断的中断处理和软件中断有一部分是相同的,有一部分却有很大的不同。对于 IPI 中断和 per CPU 中断,其设置是和软件中断相同的,都是一步到位设置到具体的处理函数。但是对于余下的外设中断,只是设置了入口函数,并没有设置具体的处理函数,而且是所有的外设中断的处理函数都统一到了同一个入口函数。然后在这个入口函数处会调用相应的 irq 描述符的 handler 函数,这个 handler 函数是中断控制器设置的。中断控制器设置的这个 handler 函数会处理与这个中断控制器相关的一些事物,然后再调用具体设备注册的 irqaction 的 handler 函数进行具体的中断处理。
对于外设中断为什么要采取这样的处理方式呢?有两个原因,1 是因为外设中断和中断控制器相关联,这样可以统一处理与中断控制器相关的事物,2 是因为外设中断的驱动执行比较晚,有些设备还是可以热插拔的,直接把它们放到中断向量表上比较麻烦。有个 irq_desc 这个中间层,设备驱动后面只需要调用函数 request_irq 来注册 ISR,只处理与设备相关的业务就可以了,而不用考虑和中断控制器硬件相关的处理。
软中断 (softirq)
软中断是把中断处理程序分成了两段:前一段叫做硬中断,执行驱动的 ISR,处理与硬件密切相关的事,在此期间是禁止中断的;后一段叫做软中断,软中断中处理和硬件不太密切的事物,在此期间是开中断的,可以继续接受硬件中断。软中断的设计提高了系统对中断的响应性。下面我们先说软中断的执行时机,然后再说软中断的使用接口。
软中断也是中断处理程序的一部分,是在 ISR 执行完成之后运行的,在 ISR 中可以向软中断中添加任务,然后软中断有事要做就会运行了。有些时候当软中断过多,处理不过来的时候,也会唤醒 ksoftirqd/x 线程来执行软中断。
所有软中断的处理函数都是在系统启动的初始化函数里面用 open_softirq 接口设置的。raise_softirq 一般是在硬中断或者软中断中用来往软中断上 push work 使得软中断可以被触发执行或者继续执行。
Linux 零拷贝技术
splice( ) 函数
tee( ) 函数
硬件中断、软件中断,硬中断、软中断是不同的概念,分别指的是中断的来源和中断的处理方式。
零拷贝(Zero-Copy)是一种 I/O 操作优化技术,可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间。其在 FTP 或者 HTTP 等协议中可以显著地提升性能。
概述:
如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间的复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。其过程如下图所示:
可以想想一下这个过程。服务器读从磁盘读取文件的时候,发生一次系统调用,产生用户态到内核态的转换,将磁盘文件拷贝到内核的内存中。然后将位于内核内存中的文件数据拷贝到用户的缓冲区中。用户应用缓冲区需要将这些数据发送到 socket 缓冲区中,进行一次用户态到内核态的转换,复制这些数据。此时这些数据在内核的 socket 的缓冲区中,在进行一次拷贝放到网卡上发送出去。
所以整个过程一共进行了四次拷贝,四次内核和用户态的切换。这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
零拷贝原理
零拷贝主要是用来解决操作系统在处理 I/O 操作时,频繁复制数据的问题。关于零拷贝主要技术有 mmap+write、sendfile 和 splice 等几种方式。
DMA 技术很容易理解,本质上,DMA 技术就是我们在主板上放一块独立的芯片。在进行内存和 I/O 设备的数据传输的时候,我们不再通过 CPU 来控制数据传输,而直接通过 DMA 控制器(DMA Controller,简称 DMAC)。这块芯片,我们可以认为它其实就是一个协处理器(Co-Processor)。DMAC 的价值在如下情况中尤其明显:当我们要传输的数据特别大、速度特别快,或者传输的数据特别小、速度特别慢的时候。
看完下图会发现其实零拷贝就是少了 CPU 拷贝这一步,磁盘拷贝还是要有的
- mmap/write 方式
把数据读取到内核缓冲区后,应用程序进行写入操作时,直接把内核的 Read Buffer 的数据复制到 Socket Buffer 以便写入,这次内核之间的复制也是需要 CPU 的参与的。
- sendfile 方式
可以看到使用 sendfile 后,没有用户空间的参与,一切操作都在内核中进行。但是还是需要 1 次拷贝
- 带有 scatter/gather 的 sendfile 方式
Linux 2.4 内核进行了优化,提供了带有 scatter/gather 的 sendfile 操作,这个操作可以把最后一次 CPU COPY 去除。其原理就是在内核空间 Read BUffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中,这样就不需要复制。其本质和虚拟内存的解决方法思路一致,就是内存地址的记录。
由来:
- splice 方式
其实就是 CPU 在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline)直接把数据传过去了,不去要 CPU 复制了
常见的零拷贝实现
Mmap + Write
mmap 将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
sendfile()
通过 sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝,sendfile 调用中 I/O 数据对用户空间是完全不可见的,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
splice()
在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的 CPU 拷贝操作,2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝。
写时复制
通过尽量延迟产生私有对象中的副本,写时复制最充分地利用了稀有的物理资源。
写时拷贝底层原理
在 Linux 系统中,调用 fork 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 写时复制 机制。如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。
传统的 fork 系统调用直接把所有资源复制给新创建的进程,这种实现过于简单并且效率低下,如果父进程中有很多数据的话依次复制个子进程,那么 fork 函数肯定是非常慢的。因此使用写时拷贝会快很多。
注意: sendfile 适用于文件数据到网卡的传输过程,并且用户程序对数据没有修改的场景;
首先要了解一下内存共享机制。不同进程的 虚拟内存地址 映射到相同的 物理内存地址,那么就实现了共享内存的机制。我们可以用用这种思想来实现写时拷贝。fork() 之后,kernel 把父进程中所有的内存页的权限都设为 read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU 硬件检测到内存页是 read-only 的,于是触发页异常中断(page-fault),陷入 kernel 的一个中断例程。中断例程中,kernel 就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。这样父进程和子进程都有了属于自己独立的页。
子进程可以执行 exec() 来做自己想要的功能。
什么是 DMA?
冯.诺依曼结构
冯.诺依曼提出了计算机“存储程序”的计算机设计理念,即将计算机指令进行编码后存储在计算机的存储器中,需要的时候可以顺序地执行程序代码,从而控制计算机运行,这就是冯.诺依曼计算机体系的开端。
核心设计思想主要体现在如下三个方面:
-
程序、数据的最终形态都是二进制编码,程序和数据都是以二进制方式存储在存储器中的,二进制编码也是计算机能够所识别和执行的编码。(可执行二进制文件:.bin 文件)
-
程序、数据和指令序列,都是事先存在主(内)存储器中,以便于计算机在工作时能够高速地从存储器中提取指令并加以分析和执行。
-
确定了计算机的五个基本组成部分:运算器、控制器、存储器、输入设备、输出设备
Linux 内存管理
CPU 访问内存的过程
Linux 任务调度
https://ty-chen.github.io/linux-kernel-schedule/