Linux系统之----信号中断(下)
1.用户态和内核态
用户态(User Mode)和内核态(Kernel Mode)是操作系统中两种不同的执行模式,主要用于区分操作系统和用户程序的权限和执行环境。
1.1 用户态
1)权限限制:用户态是操作系统为用户程序提供的执行环境,具有较低的权限。用户程序在用户态下运行,不能直接访问硬件资源,也不能执行一些需要高权限的操作。
2)信号处理:在用户态下,程序可以定义信号处理函数(如signal
函数),当程序接收到信号时,操作系统会调用这些函数来处理信号。但是,信号处理函数的执行仍然是在用户态下进行的。
3)系统调用:用户程序需要通过系统调用(如syscall
)进入内核态,以请求操作系统执行一些需要更高权限的操作,如文件操作、网络通信等。
1.2 内核态
1)高权限:内核态是操作系统内核运行的环境,具有最高的权限。在内核态下,程序可以直接访问硬件资源,执行所有系统操作。
2)中断处理:内核态负责处理硬件中断和异常,如时钟中断、I/O中断等。当硬件设备需要通知操作系统时,会触发中断,操作系统在内核态下处理这些中断。
3)系统调用处理:当用户程序通过系统调用请求操作系统服务时,操作系统在内核态下执行相应的系统调用处理程序,完成用户请求的操作。
1.3用户态和内核态的转换
1.从用户态到内核态:用户程序通过系统调用进入内核态。系统调用通常涉及从用户态切换到内核态,这涉及到保存用户态的上下文,加载内核态的上下文,然后执行内核态的代码。
2.从内核态到用户态:内核态代码执行完毕后,需要将控制权返回给用户态。这涉及到保存内核态的上下文,恢复用户态的上下文,然后切换回用户态。
每个交点都代表一次切换~
2.硬件中断
1)定义:硬件中断是由外部设备(如键盘、鼠标、磁盘驱动器等)或内部事件(如时钟中断)触发的,用于通知CPU有紧急任务需要处理。
2)触发:当外部设备或内部事件需要CPU的注意时,它们会向CPU发送一个中断信号,请求CPU暂停当前正在执行的用户态代码,转而处理中断请求。
3)处理流程:
中断请求:外部设备或内部事件发送中断请求信号。
中断响应:CPU检测到中断请求后,保存当前的程序状态(如程序计数器、寄存器等),并跳转到中断处理程序。
中断处理:执行中断处理程序,处理中断请求。
中断返回:中断处理完成后,CPU恢复之前保存的程序状态,继续执行被中断的用户态代码。
4)优点:硬件中断允许CPU在处理用户态代码的同时,响应外部设备或内部事件的请求,提高了系统的响应速度和效率。
3.中断向量表
1)定义:中断向量表是一个数据结构,用于存储不同中断类型的中断处理程序的入口地址。每个中断类型对应一个唯一的中断向量。
2)结构:中断向量表通常是一个数组,每个元素包含一个指向中断处理程序的指针。中断向量表的大小和结构取决于具体的硬件架构。
3)使用:当CPU响应中断时,它会根据中断类型从中断向量表中查找对应的中断处理程序的入口地址。CPU跳转到该地址,执行中断处理程序。
4)操作系统的作用:操作系统负责初始化中断向量表,将每个中断类型映射到相应的中断处理程序。操作系统还可以动态修改中断向量表,以支持不同的中断处理策略或加载新的驱动程序。
这里有一点要注意!!!
硬件中断和信号,没有任何关系!!两套技术体系。信号是一种,软件方式,模拟中断行为的!!
OS的运行,是在时钟源的中断下,一直触发中断处理,执行的操作系统代码!!!
4.软中断
4.1 定义
软中断是程序运行过程中,通过执行特定的指令(如 INT
指令)来触发中断处理程序的过程。它是一种在软件层面模拟硬件中断的方式,主要用于实现操作系统中的系统调用、异常处理等功能。
4.2 软中断的工作原理
软中断的工作原理可以分为以下几个步骤:
触发软中断:程序通过执行特定的指令(如
int
指令)来触发软中断。这个指令会告诉CPU当前需要执行一个中断处理程序。保存当前状态:当CPU接收到软中断指令时,它会暂停当前的程序执行,并保存当前的程序状态,包括程序计数器(PC)、寄存器状态等。这一步是为了在中断处理完成后能够恢复到中断发生前的状态。
查找中断向量表(IDT):CPU会查找中断向量表(IDT)来找到对应的中断处理程序。IDT是一个数据结构,它包含了所有中断处理程序的入口地址。每个中断都有一个唯一的中断号,这个中断号在IDT中用作索引。
执行中断处理程序:CPU根据中断号在IDT中找到对应的中断处理程序后,它会跳转到这个程序的入口地址并开始执行。中断处理程序可以执行各种任务,比如处理系统调用、处理硬件中断、执行异常处理等。
恢复现场:中断处理程序执行完成后,CPU会恢复之前保存的程序状态,然后继续执行被中断的程序。
4.3 系统调用基于软中断
下图列出了多个系统调用号(如_NR_exit
、_NR_fork
等),这些系统调用号是操作系统定义的,用于标识不同的系统调用。每个进程都有自己的用户级页表,但所有进程共享一个内核级页表。
因此,我们得出结论:
1.无论进程怎么切换,怎么调度,每一个进程都可以找到同一个内核!,CPU也随时能找到内核!!!
2. 用户要访问OS,只有一种途径,就是系统调用!!!你的进程,是如何看到系统调用的???通过虚拟地址空间!!!我们还可以推论出:所有的函数调用,未来全部都可以理解成为在我自己的虚拟地址空间中完成!!!!
4.4 内核角度分析系统调用表
首先,我们要明白一点,就是我们之前用的函数,比如fork()函数,open()函数,其本质不是这样的,只是系统给我们提供了一个调用的接口!实际上,fork()函数在系统里是sys_fork()函数,open()函数在系统里是sys_open()函数,很多函数都是这样,因此,内核中形成了一个表,叫做系统调用表~如下图所示:
该表是一个数组,其中的每个元素都是一个函数指针,指向实际执行系统调用操作的内核函数。系统调用号用作索引来查找这个表,从而找到对应的处理函数。
而当用户程序需要执行系统调用时,它会触发一个从用户态到内核态的上下文切换。这通常通过执行一个特殊的指令来完成,如在x86架构中使用int 0x80
或syscall
指令。
4.5 当前特权级别(CPL)
4.5.1 什么是CPL
CPL(Current Privilege Level,当前特权级别)是x86架构CPU中用来标识当前执行代码的特权级别的一个字段,位于CS(Code Segment)寄存器中。CPL主要有两个级别:
Ring 0(内核态):最高特权级别,操作系统内核代码运行在这个级别。在这个级别,代码可以访问所有的硬件资源和执行所有的指令。
Ring 3(用户态):最低特权级别,用户程序运行在这个级别。在这个级别,代码的权限受到限制,不能直接访问硬件资源,也不能执行某些特权指令。
4.5.2 修改CPL的方法
4.5.2.1使用中断或异常
当一个中断或异常发生时,CPU会自动切换到Ring 0,并从中断向量表(IDT)中获取对应的中断处理程序的地址来执行。这是用户程序进入内核空间的一种常见方式
4.5.2.2 使用系统调用指令
在x86系统中,可以通过int 0x80
(Linux)或syscall
(较新的系统)指令来触发一个软中断,从而从Ring 3切换到Ring 0。这些指令会保存当前的寄存器状态,然后跳转到内核中的系统调用处理程序
4.6 操作系统如何知道硬件报错
当硬件发生错误时,它会触发一个中断信号给CPU。这个中断信号会导致CPU执行一个特定的中断处理程序,这个处理程序是操作系统的一部分。操作系统通过这种方式来检测和响应硬件错误。
之后进行中断处理:
硬件中断:由外部硬件设备触发的中断,例如键盘输入或硬盘读写错误。
软件中断:由CPU执行特定指令(如int 0x80
或syscall
)触发的中断,用于执行系统调用。
异常:由CPU内部错误触发的中断,例如除零错误或非法指令。
4.7 总结
操作系统的定义:操作系统是一个基于中断工作的软件集合,它形成了一个死循环,不断地处理各种中断和异常。
操作系统的作用:操作系统通过中断处理程序来响应硬件错误和用户请求,从而管理硬件资源和提供服务给用户程序。
4.8 设置信号处理的系统调用函数
4.8.1 sigaction()函数
这个函数是sigaction,下面我们来看一下它:
参数说明:
signum
:指定要设置处理方式的信号编号。
act
:指向一个 sigaction
结构的指针,该结构包含了信号处理的信息,即如何处理信号。
oldact
:可选参数,如果提供,它将被填充为信号之前的处理设置。
实际上,它原型是一个结构体,叫做struct sigaction,我们也看一下:
struct sigaction {void (*sa_handler)(int); /* 指向信号处理函数的指针 */void (*sa_sigaction)(int, siginfo_t *, void *); /* 指向信号处理函数的指针,允许传递额 外信息 */sigset_t sa_mask; /* 信号集,指定在信号处理函数执行期间应该被阻塞的信号 */int sa_flags; /* 信号处理选项 */void (*sa_restorer)(void); /* 保留,用于信号处理函数结束后恢复信号处理 */
};
4.8.2 关键结论
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么,它会被阻塞到当前处理结束为止。这样可以防止信号递归处理!如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用 sa_mask 字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags 字段包含一些选项,本章的代码都把 sa_flags 设为0,sa_sigaction 是实时信号的处理函数,下面我们证明一下:
首先我们看这样一段代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{std::cout << "获取到一个信号: " << signo << std::endl;while (1){sigset_t pending;sigemptyset(&pending); // 先给置为空sigpending(&pending);for (int i = 31; i > 0; i--){if (sigismember(&pending, i))std::cout << "1";elsestd::cout << "0";}std::cout << std::endl;sleep(1);}
}
int main()
{struct sigaction act, oact;act.sa_handler = handler;act.sa_flags = 0;// 信号集置空sigemptyset(&(act.sa_mask)); // 我们现在有没有设置到内核?没有sigaction(2, &act, &oact);while (true){std::cout << "我是一个进程: " << getpid() << std::endl;sleep(1);}return 0;
}
运行:
当我们只把2号信号加入到信号集时,当我们连续发2号信号时,第一次kill会导致其被捕捉,导致程序在handler函数里面循环,而在我们第二次kill后,由于信号集里面已经有2号信号了,所以对应的位置被置为1,而其他信号没被加入信号集,因此当我们输入三号信号时,进程会被杀死!
下面我们在加入几个信号,进一步验证一下:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{std::cout << "获取到一个信号: " << signo << std::endl;while (1){sigset_t pending;sigemptyset(&pending); // 先给置为空sigpending(&pending);for (int i = 31; i > 0; i--){if (sigismember(&pending, i))std::cout << "1";elsestd::cout << "0";}std::cout << std::endl;sleep(1);}
}
int main()
{struct sigaction act, oact;act.sa_handler = handler;act.sa_flags = 0;// 信号集置空sigemptyset(&(act.sa_mask)); // 我们现在有没有设置到内核?没有sigaddset(&(act.sa_mask),3);sigaddset(&(act.sa_mask),4);sigaddset(&(act.sa_mask),5);sigaddset(&(act.sa_mask),6);sigaction(2, &act, &oact);// 将2号信号的捕捉方法,设置到内核中!while (true){std::cout << "我是一个进程: " << getpid() << std::endl;sleep(1);}return 0;
}
运行: