Linux操作系统中断
举个通俗易懂的例子:比如你现在正在打游戏,快要打完了,boss
就只剩下一丁点的血量了,这时候你的外卖到了,外卖小哥都在敲门了,此时你就把游戏进行存档,去拿外卖,外卖小哥敲门就相当于一个中断请求IRQ
,此时会通过中断处理程序将游戏进行存档(存档就是中断处理程序),并且将吃东西的进程唤醒,并成运行状态。(咱们把打游戏和吃东西都当作是一个一个的任务,)
中断处理程序的作用:将打游戏进行存档,接着将一开始处于睡眠状态的吃东西进程唤醒,使它从睡眠状态变成运行状态。
从以上可以看出中断对于操作系统性能来说太重要了,比如一开始你正在打游戏,即打游戏进程处于运行状态,此时吃东西进程应该处于睡眠状态,因为外卖还没有到,不应该让吃东西进程一直处于运行状态(相当于你一直张着嘴),我们可以先将不处于运行状态,即睡眠状态的任务或者进程先放进等待队列里面去,当设备的数据,比如外卖来了之后,设备再给CPU
发起一个中断请求IRQ
(外卖小哥敲门就是相当于一个中断请求),接着CPU
就会执行中断处理程序了,但是执行中断处理程序不应该影响上一个运行的任务或者进程执行的进度,即需要将其存档,接下来中断处理程序就会将数据放入到响应的设备缓冲区当中,接下来就会把之前因为设备缓冲区中缺少数据而等待的任务或者进程进行唤醒,让它转移到运行队列里面去,处于运行状态,中断处理程序执行结束之后就会恢复之前挂起的打游戏任务或者进程,此时CPU
不仅仅只能执行吃东西这个任务或者进程了,还可以一边吃东西一边打游戏了,多个任务并行执行。
每一个中断都会有对应的中断处理程序
1.什么是系统中断?(软中断/硬中断)
-
硬中断:硬件发起的中断,硬中断是比较典型的中断,一般都是异步中断,是由外部设备产生的中断,可以发生在任意时间。例子:比如当前计算机的网卡设备就受到一组报文之后,会被DMA设备(网卡上的一块内嵌设备)直接转移到内存条的一个空间内,叫做网卡缓冲区,然后网卡就会向
CPU
发起一个中断请求IRQ
,CPU
接着就会执行网卡中断对应的中断处理程序。异步中断与当前占用CPU
的进程没有直接关系的,更像是一种事件驱动模型,去驱动关联的进程,可以进行下一步的工作。 -
软中断:由
CPU
产生的中断。例子:比如CPU
正在执行一个代码块(10/0),我们都知道0是不能作为除数的,此时CPU
检测到这行代码之后,就会发起一个软中断,就会让当前进程从用户态切换到内核态,会保留一些数据,当应用程序恢复到用户态之后,会检查某个寄存器的区域,会发现产生错误了,应用程序就有机会该错误或者输出异常信息或者直接结束。
系统调用就是借助软中断完成的,即0x80中断
,0x80中断(128中断)
也是一种软中断。即系统调用的中断处理程序是0x80中断
。
2.系统中断,内核会做什么事情?
每一个中断都会有对应的中断处理程序,例子:比如当前计算机的网卡设备就受到一组报文之后,会被DMA设备(网卡上的一块内嵌设备)直接转移到内存条的一个空间内,叫做网卡缓冲区,然后网卡就会向CPU
发起一个中断请求IRQ
,此时可能进程C
正在占用着CPU
,此时进程C
肯定有一个运行状态,比如进程C
已经在多个寄存器中存储数据,正在准备下一步计算呢,进程C
为了不延误中断处理程序的处理需要做一件事情,会将进程C
使用的寄存器保存到进程描述符中(每一个进程都有一个进程描述符,进程描述符的很多字段会保留用户态下进程瞬时的运行状态数据,以便中断处理程序执行结束之后,恢复进程C
为运行状态时可以接着执行),然后接着进程CPU
会从用户态切换到内核态了(为什么要切换呢?因为接下来执行的程序与进程C
用户程序就没有关系了),我们知道每个进程都会有两个堆栈,即(用户栈/内核栈),用户栈中存储比如你代码声明的变量,这些都是存储在用户态空间中的数据。而当我们需要进行系统调用(比如申请系统资源)的时候就需要从用户态切换到内核态,内核栈中也存储类似声明变量等数据。接着来CPU
就不管进程C
用户态的代码了,,CPU
会执行网卡中断对应的中断处理程序了,执行完中断处理程序之后,那么CPU
又会从内核态切换回用户态,怎么切回呢?就是从进程C
的进程描述符中加载进程C
的上一个运行状态。
3.硬件中断触发的过程(ps:8259芯片中断控制器的工作流程)
硬件中断触发的过程如下图所示
INTR
:中断引脚。用于接收中断信号的,8259A中断控制器
:用于连接硬件设备的,我们知道咱们的硬件设备都是直接插到主板上的,然后插口可以通过一根电线连接到这个中断控制器来,当电线中的电流发生变化之后就相当于插槽中的硬件产生中断了,中断控制器就会经过一一系列的处理完成中断请求。8259A中断控制器
有8个接口即IR0~IR7
,但是咱们的设备不只有8个,8259A中断控制器
是可以通过级联的方式,即IR2
接口。中断请求寄存器
: 存储各个设备的中断信号。比如假设它用8位表示,哪个位数上的至值为1,则表示此时对应的设备上面有中断信号。比如00000001
表示此时键盘上有中断信号。优先级别解析器
:因为中断信号也是有中断优先级的。优先级别解析器
会根据IRQ
后面的序号来判断哪个先执行,序号越小优先级越高,此时IRQ0
的优先级别最高,先执行。正在服务寄存器
:保存当前CPU
正在处理的中断请求序号,比如此时正在执行的中断请求时硬盘设备引发的中断,即IRQ1
,则此时保存的值为1
。当CPU
执行该中断请求之后会将正在服务寄存器
中的值清空,8259A中断控制器
知道正在服务寄存器
中的值清空之后,就会按照中断请求优先级将后面的等待的中断请求依次执行,CPU
会通过数据总线把下一个执行的中断请求序号写进正在服务寄存器
。IRQ中断程序入口映射表
:系统在启动的时候就会将中断请求的序号(即向量)跟代码段地址绑定起来。CPU
通过INTR
中断引脚只能知道中断请求的序号,CPU
需要通过序号即向量找到对应的代码段地址,然后CPU
会到对应的地址执行对应的中断处理程序代码。
Socket基础
Java Demo
Server
端的代码如下:
/**
* @Auther: huangshuai
* @Date: 2021/12/26 20:03
* @Description:Socket Demo
* @Version:
*/
public class TcpServer {
public static void main(String[] args) {
try{
ServerSocket ss = new ServerSocket(6666);
while (true) {
Socket s = ss.accept();
System.out.println("A client connected!");
DataInputStream dis = new DataInputStream(s.getInputStream());
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
String str = null;
if ((str = dis.readUTF()) != null) {
System.out.println(str);
System.out.println("from " + s.getInetAddress() + ", port #" + s.getPort());
}
dos.writeUTF("Hello," + s.getInetAddress() + ", port" + s.getPort());
dis.close();
dos.close();
s.close();
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
Client
端的代码如下:
/**
* @Auther: huangshuai
* @Date: 2021/12/26 20:10
* @Description:
* @Version:
*/
public class TcpClient {
public static void main(String[] args) {
try {
Socket s = new Socket("127.0.0.1", 6666);
OutputStream os = s.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF("Hello, Server!");
DataInputStream dis = new DataInputStream(s.getInputStream());
System.out.println(dis.readUTF());
dos.flush();
dos.close();
dis.close();
s.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
Socket读写缓冲区工作机制
APP
在用户态中想要实现两端的数据传输,首先需要从用户态切换到内核态中的Socket
套接字实例,上图中的两个客户端都是Socket
套接字实例,发送或者接收数据时最终都会映射到内核中的write/send
函数(或者读操作最终会映射到内核中的read/recv
函数),先将数据放入到输出缓冲区当中,然后再将数据传输到对端,接收数据时,也是先通过Socket
套接字实例的read/recv
系统调用从输入缓冲区中读取数据到内核空间,接着将内核空间中的数据拷贝到用户空间中。
Socket两种模式
BIO阻塞式IO
写操作
- 当发送数据时,会先检查输出缓冲区中的可用空间大小是否小于你发送的数据大小,如果小于的话,那么这次写操作的系统调用会被阻塞,一直阻塞到输出缓冲区中的可用空间大小大于你要发送数据的大小。
- 当
TCP/IP
协议正在操作输出缓冲区中的数据时,也会加锁,如果此时你要发送数据的话也会阻塞,直到TCP/IP
操作结束释放锁。 - 当你发送的数据很大的时候,比如10MB,但是假设整个输出缓冲区的大小为5mb,此时系统调用会把10mb分两份,会先将5mb的数据写进输出缓冲区当中,接着当前进程同样也会被阻塞挂起。当输出缓冲区中的5mb数据被
TCP/IP
协议传输完时,会将当前进程唤醒,接着将剩下的5mb数据再放入到输出缓冲区当中,直接返回结束。
读操作
- 当进行读操作时,如果此时输入缓冲区没有数据地话,当前进程会阻塞,一直等待直到输入缓冲区中有数据。当输入缓冲区有数据时,会通过中断处理程序将当前读操作地进程唤醒。
- 当进行读操作时,如果此时你想要读取大小为50mb的数据,但是此时输入缓冲区中的数据有100mb,此时你只能将输入缓冲区中的50mb数据读取出来,如果你想要读取完整个输入缓冲区中的数据,需要再次读取。
NIO非阻塞IO
写操作
当发送数据时,同样也是先将用户态的数据拷贝到内核空间中,先检查输出缓冲区的可用空间大小是否够用,假设此时发送数据大小为2mb,输出缓冲区的大小为1mb,此时会先将1mb的数据放入输出缓冲区中,接着直接返回到用户态,并且告诉用户态此时已经将1mb的数据写入到输出缓冲区中,还有1mb的数据没有处理。如果你想要发送完的话,需要再次进行系统调用将剩下的1mb数据写入到输出缓冲区中。NIO
是一种非阻塞式,会不断地重试写数据写不进去地话不会阻塞,假设此时输出缓冲区没有空间了,系统调用会直接返回-1
到用户态,-1
表示这次写数据失败了,原因是空间满了。
读操作
当进行读操作时,如果此时输入缓冲区没有数据的话,会直接返回而不会将当前进程阻塞。
面试题
如果Socket
套接字的Server
端一直抢占不到CPU
,那么Socket
套接字Client
端一直发送过来数据会不会有问题?
Socket
套接字的Server
端一直抢占不到CPU
,那么Socket
套接字Client
端一直发送过来数据就会导致Client
端的输出缓冲区和Server
端的输入缓冲区打满。再者我们知道Socket
底层是是使用TCP
协议将Client
端中的输出缓冲区数据传输到Server
端中的输入缓冲区中。
TCP
能够保证数据不丢(即Client
端写入输出缓冲区中的数据不会丢失);TCP
能够保证传输数据顺序性。我们知道从Client
端的输出缓冲区发送数据到Server
端的输入缓冲区的过程是不受应用程序控制的,而是由OS
上的TCP
协议实现程序控制的。这个TCP
协议实现程序里面有拥塞控制
,滑动窗口
和重试机制等等的保证数据不丢失和顺序性,例如当从Client
端的输出缓冲区发送数据到Server
端失败时,会进行重试,比如隔个5秒钟再发一次,还失败的话,再隔10秒钟再发.....。Server
端的输入缓冲区接收到数据之后会发送ACK
进行确认。那么当Socket
套接字Client
端一直发送过来数据就会导致Client
端的输出缓冲区和Server
端的输入缓冲区打满时出现什么情况呢?
-
BIO模式下
:在Client
端的APP应用进程尝试写数据到输出缓冲区中,但是此时输出缓冲区已经满了,当前进程就会阻塞被挂起,直到输出缓冲区中有空间可写。 -
NIO模式下
:在Client
端的APP应用进程尝试写数据到输出缓冲区中,但是此时输出缓冲区已经满了,当前进程就会返回错误信息而不是等待,不断地尝试写,直到输出缓冲区中有空间可写。
系统调用,用户态和内核态
1.为什么要有这两种状态?(用户栈/内核栈)
一个进程被创建出来时,系统会给当前进程分配两个空间,分别是用户栈(用户空间)和内核栈(内核空间),
系统调用的原理图如下:
2.什么时候进程会切换至内核态
- 当用户程序需要系统调用的时候。比如当执行到用户程序某行代码时,需要申请资源或者访问硬件或者访问硬盘等等操作,用户代码肯定是不能直接去访问硬件的,这时候就需要进行系统调用,切换至内核态。(系统调用)
- 外部设备数据就绪了,设备会给
CPU
发起一个硬中断请求,CPU
不管当前线程正在干什么,会立马响应设备中断,当前的进程就会从用户态切换到内核态,然后使用内核堆栈去执行中断处理程序。(硬中断) - 当前用户程序的某一行代码出现了错误,比如执行10/0时,也会从用户态切换到内核态。(软中断)
3.进程状态切换时,都要做什么事情?
当处于进程的用户态时,如下图所示
当需要将用户态切换到内核态时,需要考虑的是后面切换回用户态的时候数据状态不丢,所以需要在切换到内核态之前把一些数据存储到进程描述符fd
中,以便后面恢复用户态运行。如下图所示
- 将用户态的寄存器的瞬时数据保存到进程
fd
中。 - 将堆栈地址由原来的指向用户堆栈改变指向为内核堆栈。那么它是怎么知道用户堆栈地址和内核堆栈地址的呢》是从进程描述符
fd
中读取的. - 代码地址由原来指向用户代码改变指向为内核代码。行号归零
BIO通信底层原理
BIO通信底层原理,直接看图很清晰,如下:
BIO
的缺点:一个进程只能监听一个Socket
,使用BIO
很难实现c10k,c100k等
,c10k
表示服务端要支持一万个客户端连接,要想实现只能通过开启一万个进程的方式,但是这是不现实的。
显然只能通过实现一个进程只能监听多个Socket
才能实现。,这个技术就是多路复用
Linux select 多路复用函数
在linux中,我们可以通过使用select
函数实现I/O
端口的复用,传递给select
函数的参数会告诉内核:
- 我们所关心的文件描述符,即想要监听哪些
Socket
。 - 对每个描述符,我们所关心的状态,即哪些描述符就绪了。
- 我们要等待多长时间。即
select
要阻塞多长时间呢?传null
表示某个Socket
就绪了之后就会返回,也可以传0
表示这是一个非阻塞的select
调用,如果没有Socket
就绪的话直接返回-1
,说明当前没有Socket
就绪。传入大于0
的话,如果在这个时间内还没有Socket
就绪的话直接返回,即这个参数表示select
最大的等待时间。
从select
函数返回后,内核告诉我们以下信息
- 对我们的要求已经做好准备的描述符的个数。即就绪的
Socket
数量。 - 对于三种条件哪些描述符已经做好准备(读、写、异常)。
有了这些返回信息后,我们可以调用合适的
I/O
函数(通常是read
或者write
),并且这些函数不会再阻塞。因为此时Socket
读缓冲区里面已经有数据了。
select
函数的定义如下图所示
select
函数返回值为做好准备的文件描述符的个数,超时返回为0,错误返回-1。
重要参数说明如下:
maxfdp1
表示本次调用select
的bitmap
中的最大有效位数到哪里,比如5的话,后面一大堆的0,因为一共有1024
位,如果每次都是检查全部的1024
位太费性能了。fd_set
是一个bitmap
结构,长度为1024
是使用final
修饰的常量,不能修改的。但是能使用的也就1021
个,也就是说只能同时监听1021
个socket
。哪个位上为1
表示对应的socket
已经就绪了。readset
参数表示你关心的socket
读就绪的编号,比如此时编号为5,6,7
的socket
都就绪的话,这个readset
参数传00000111000.......
。writeset
参数表示你关心的socket
写就绪的编号,比如此时编号为5,6,7
的socket
都就绪的话,这个writeset
参数传00000111000.......
。timeout
表示此次调用select
函数最大允许等待的时长,我们一般传null
表示有socket
就绪了之后才返回,否则一直阻塞等待;传0
表示一开始检查是否有已经就绪的socket
,如果没有直接返回不会阻塞;传大于0
表示在这个时间内等待直到有socket
就绪,如果超过这个时间都没有socket
就绪的话,不再等待直接返回。timeval
的数据结构如下:
struct timeval {
long tv_sec;/*秒*/
long tv_usec;/*微秒*/
}
相关的函数说明如下图所示
使用c语言演示select
函数的demo
如下:
从代码中可以看出,select
系统调用后,返回了一个置位后的&rset
,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket
需要read
数据,有效提高了效率。
问题
-
1、
bitmap
最大1024
位,一个进程最多只能处理1024
个客户端 -
2、
&rset
不可重用,每次socket
有数据就相应的位会被置位 -
3、文件描述符数组拷贝到了内核态(只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)),仍然有开销。
select
调用需要传入fd
数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制) -
4、
select
并没有通知用户态哪一个socket
有数据,仍然需要O(n)
的遍历。select
仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
select
方式,既做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次 select
的系统调用 + N
次就绪状态的文件描述符的 read
系统调用。(read
系统调用不再阻塞。只可能会在select
函数中阻塞)
select底层原理分析
直接看图如下:
Linux poll 多路复用函数
poll
函数的定义如下:
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
pollfd
的结构如下:
struct pollfd {
int fd;/*file descriptor*/
short events;/*requested events*/
short revents;/*return events*/
}
优点
-
1、
poll
使用pollfd
数组来代替select
中的bitmap
,数组没有1024的限制,可以一次管理更多的client
。它和select
的主要区别就是,去掉了select
只能监听1024
个文件描述符的限制。 -
2、当
pollfds
数组中有事件发生,相应的revents
置位为1
,遍历的时候又置位回零,实现了pollfd
数组的重用
问题
poll
解决了select
缺点中的前两条,其本质原理还是select
的方法,还存在select
中原来的问题
- 1、
pollfds
数组拷贝到了内核态,仍然有开销 - 2、
poll
并没有通知用户态哪一个socket
有数据,仍然需要O(n)
的遍历
Linux epoll 多路复用函数
epoll
是在2.6
内核中提出的,是之前的select
和poll
的增强版本,相对于select
和poll
来说,epoll
更加灵活,没有描述符的限制,epoll
使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放在一个事件表中,这样用户空间和内核空间的copy
只需一次。
epoll
操作过程中需要三个接口,分别如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* event, int maxevents, int timeout);
epoll_create
函数是一个系统函数,函数将在内核空间内开辟一块新的空间,可以理解为epoll
结构空间,返回值为epoll
的文件描述符编号,方便后续操作使用。epoll_ctl
是epoll
的事件注册函数,epoll
与select
不同,select
函数是调用指定需要监听的描述符和事件,epoll
先将用户感兴趣的描述符事件注册到epoll
空间内,此函数是非阻塞函数,作用仅仅是增删改epoll
空间内的描述符信息。- 参数一:
epfd
,很简单,epoll
结构的进程fd
编号,函数将依靠该编号找到对应的epoll
结构。 - 参数二:
op
表示当前请求类型,有三个宏定义:(EPOLL_CTL_ADD
:注册新的fd
到epfd
中、(EPOLL_CTL_MOD
:修改已经注册的fd
监听事件)、(EPOLL_CTL_DEL
:从epfd
中删除一个fd
)。 - 参数三:
fd
表示需要监听的文件描述符,一般指的是socket_fd
。 - 参数四:
event
告诉内核对该fd
资源感兴趣的事件。
- 参数一:
epoll_wait
等待事件的产生,类似于select()
调用,根据参数timeout
来决定是否阻塞。- 参数一:
epfd
指定感兴趣的epoll
事件列表。 - 参数二:
*events
是一个指针,必须指向一个epoll_event
结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组中! - 参数三:
maxevents
标明参数二epoll_event
数组最多能够接收的数据量,即本次操作最多能获取多少就绪数据。 - 参数四:
timeout
单位为毫秒,0
值时表示立即返回,非阻塞调用;-1
值表示阻塞调用,直到有用户噶感兴趣的事件就绪为止;>0
表示阻塞调用,阻塞指定事件内如果有事件就绪则提前返回,否则等到指定时间后返回。 - 返回值:本次就绪的
fd
个数。
- 参数一:
epoll
对文件描述符的操作有两种模式:LT(水平触发)
和ET(边缘触发)
,LT模式
是默认模式,两者区别如下:
LT(水平触发)
:事件就绪后,用户可以选择处理或者不处理,如果用户本次未处理,那么下次调用epoll_wait
时仍然会将未处理的事件打包给你。ET(边缘触发)
:事件就绪后,用户必须处理,因为内核不给你兜底了,内核把就绪的事件打包给你后,就把对应的就绪事件清楚掉了。
三步调用
epoll_create
:创建一个epoll
句柄epoll_ctl
:向内核添加、修改或删除要监控的文件描述符epoll_wait
:类似发起了select()
调用
事件通知机制
-
1、当有网卡上有数据到达了,首先会放到
DMA
(内存中的一个buffer
,网卡可以直接访问这个数据区域)中 -
2、网卡向
cpu
发起中断,让cpu
先处理网卡的事 -
3、中断号在内存中会绑定一个回调,哪个
socket
中有数据,回调函数就把哪个socket
放入就绪链表中
epoll底层原理分析
直接看图如下: