1 信号的概念
查看信号的详细信息:
man 7 signal
- 信号处理的能力属于进程内置功能的一部分;进程即使没有收到信号,也能知道那些信号该怎么处理
- 当进程收到一个信号时,可能不会立即处理这个信号
- 进程具有保存哪些信号已经发生了的能力
1.1 前台进程和后台进程的概念
- 前台进程:不接受其他指令,前台进程只能有一个
- 后台进程:可以接收其他指令,在
./xxx
之后加上&
可指定后台进程, 后台进程可以有多个
ctrl+C只能杀死前台进程,一次登录、一个终端、会配置一个bash
问1:linux前台进程为什么不能接收其他指令?
答1:**在Linux中,当一个进程在前台运行时,它会占用终端,这意味着终端不能接收或执行其他命令,直到当前运行的程序完成。**这是因为前台进程和终端之间存在一个输入和输出的关系,前台进程会读取终端的输入,并将输出发送到终端。因此,当一个进程在前台运行时,终端的所有输入都会被该进程接收,这就阻止了终端接收其他命令。
相反,当一个进程在后台运行时,它不会占用终端,这意味着终端可以继续接收并执行其他命令。这是因为后台进程与终端的输入和输出关系被切断,因此它们不会互相干扰。这就是为什么我们可以在一个程序在后台运行的同时,在终端中执行其他命令。
问2:所以说前台进程能接收ctrl+c信号,而后台不能嘛?
答2:是的,你的理解是正确的。在Linux中,
Ctrl+C
产生的信号只能发送给前台进程。这是因为前台进程是与用户交互的进程,它接收并处理来自键盘的信号。当你按下Ctrl+C
时,前台进程会接收到一个SIGINT
(中断)信号,通常会导致进程终止。然而,后台进程则不接收来自键盘的信号。这意味着如果你在一个进程在后台运行时按下
Ctrl+C
,这个信号不会被发送到后台进程。这就是为什么后台进程可以在你使用终端执行其他命令时继续运行。
总结:
- 前台进程占用终端,接收处理来自键盘的信号
- 后台进程不占用终端,此时终端可以执行其他命令,并且不接受键盘信号,信号不会发送到后台进程
1.2 信号的处理方式
- 默认 ——
SIG_DFL
- 忽略 ——
SIG_IGN
- 信号的捕捉 ——
signal
或sigaction
1.3 signal
—— 信号自定义捕捉
不是所有信号都能被signal
捕捉的!!例如:9)SIGKILL
、 19)SIGSTOP
- 当接收到信号时函数才执行!!!(设置信号的处理方式)
#include <signal.h> //signal
#include <iostream>
#include <unistd.h> //sleep
using namespace std;
int main() {
signal(SIGINT, [](int sigNO) -> void{ //只需要设置一次,往后都有效
cout << "get signal: " << sigNO << endl;
});
while(1) {
cout << "i am a process" << endl;
sleep(1);
}
}
1.4 ctrl+c 如何变为信号的
- 用户按
Ctrl + C
键盘输入产生硬件中断,被操作系统获取,解释成信号。发送给目标前台进程
1.4.1 硬件中断
问:linux操作系统如何知道键盘输入了数据?
答:在Linux操作系统中,键盘输入的数据是通过一系列的硬件和软件交互来处理的。当你在键盘上按下一个键时,键盘会生成一个中断信号,这个信号被发送到计算机的中断控制器。中断控制器会通知CPU有一个新的输入需要处理
在软件层面,Linux内核有一个叫做**
input
的子系统**,它负责处理来自各种输入设备(如键盘、鼠标等)的事件。当键盘事件发生时,input
子系统会生成一个事件,并将其放入一个队列中。然后,这些事件会被传递给用户空间的应用程序进行处理。它提供了一个统一的接口,使得应用程序可以方便地处理各种输入设备的事件。问:中断向量表的处理过程具体是怎样的呢?
答:
- 中断发生:当一个中断发生时,CPU会首先接收到一个中断请求。这个请求可能来自于外部硬件设备,比如键盘或鼠标,也可能来自于内部,比如定时器或者软件中断。
- 查询中断向量表:CPU会根据接收到的中断请求的类型,查询中断向量表以找到对应的中断服务程序的入口地址。
- 跳转到中断服务程序:一旦找到了中断服务程序的入口地址,CPU会将控制转移到该地址,开始执行中断服务程序。在执行中断服务程序之前,CPU会首先保存当前的执行环境,也就是所谓的“保护现场”。
- 执行中断服务程序:中断服务程序会处理中断请求,执行必要的操作。这可能包括读取或写入硬件设备,修改内存或寄存器的值,或者触发其他的软件操作。
- 返回到原来的程序:一旦中断服务程序完成,控制会返回到中断发生前的程序。CPU会恢复之前保存的执行环境,然后继续执行被中断的程序。
硬件-》中断号-》中断单元-》CPU-》中断向量表-》处理中断的方法
1.5 信号的产生和进程运行时异步的 —— “软中断”
2 信号的产生
2.1 键盘组合键
ctrl + c
: 2)SIGINT
ctrl + \
: 3)SIGQUIT
ctrl + z
: 19)SIGSTOP
2.2 kill命令
2.3 系统调用
2.3.1 kill
向目标进程(pid
)发送信号(sig
)
2.3.2 raise
给当前进程发送信号(sig
)
2.3.2 abort
给自己发送6号信号,终止进程
与直接kill -6 [pid]
不同,用kill不会终止进程
2.4 硬件异常产生信号
除0或者野指针问题,系统会给进程发信号,进程收到信号终止进程
问:OS如何知道进程的除零异常?
答:
当一个进程试图进行除零操作时,CPU会产生除法错误异常。相应的异常处理程序会发送**
SIGFPE
信号给当前进程,然后由其采取必要的步骤,恢复还是中止(如果该信号没有对应的处理程序,则中止)。这就是操作系统如何知道进程的除零异常的。这个过程是由硬件电路完成的,而不是操作系统。对于大多数处理器(RISC-V处理器除外),当遇到“divide by zero”时都会引发异常(FPU也有状态标志,ALU和FPU是并行的),如果是浮点除法,则不会终止进程运行,并且会返回确定的结果inf。以x86架构cpu为例,cpu通过8位的中断类型码通过中断向量表**(IDT)找到对应的中断处理程序的入口地址,随即控制权交由操作系统内核进行故障处理。所以,当一个进程试图进行除零操作时,处理器会触发一个异常,然后操作系统会接管,处理这个异常。这就是操作系统如何知道进程的除零异常的。通过改变CPU寄存器的值,从而被操作系统识别,通过不同类别的异常给进程发送不同的信号
2.5 软件条件产生信号
例如: 管道读端关闭,写端正常时,操作系统检测,系统给进程发送SIGPIPE
,终止进程
2.5.1 alarm
—— 14)SIGALRM
- 返回值:alarm调用时的剩余时间
2.6 Core Dump
- 如果这些Action为Core的信号发出之后,且core dump功能打开时,会生成core文件
2.6.1 wait
的status
参数
其中第7位core dump
标志存放信号的Action
- 打开core dump功能:
ulimit -c [number]
,默认被关闭,即number为0
2.6.2 核心存储(core dump)
打开之后,当进程出现异常,操作系统会将进程在内存中的运行信息存储到但钱目录新城core.[pid]
文件
- 发送kill -3信号
int main() {
pid_t id = fork();
if (id == 0) {
int cnt = 50;
while(cnt--) {
cout << "i am a process" << getpid() << endl;
sleep(1);
}
exit(0);
}
int status = 0;
pid_t rid = waitpid(id, &status,0);
if (rid == id)cout << ((status>>8)&0xFF) << ' ' << ((status>>7)&1) << ' ' << (status&0x7f);
}
- 查看具体异常错误信息
int main() {
int a = 10;
int b = 0;
int c = a / b;
return 0;
}
3 信号的发送与保存
3.1 概念
- 信号发送给进程的PCB,PCB中维护一个
int signal
,对于1-31的不同信号,每一个对应一个位(pending
表) - 所以说发信号的本质时OS去修改
task_struct
中signal
对应位的值(1或0)
实际执行信号的处理动作称为信号递达(Delivery)(
handler
)
信号从产生到递达之间的状态,称为信号未决(Pending
)。
进程可以选择阻塞 (Block
)某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作(若信号被忽略,则信号依然可以添加到未决信号集合中)。
三张“表”:
- block表:位图,0表示不屏蔽,1表示屏蔽
- pending表:位图,记录当前进程收到了哪些信号
- handler表:函数指针数组,存放对应信号的处理方法
- block表和pending表在内核中的数据结构均为
sigset_t
3.2 信号集操作函数
sigset_t
系统给用户层提供的数据结构
//屏蔽信号函数 —— 对block表操作
//通过下列函数操作一个sigset_t类型的变量,将该变量传入sigprocmask函数改变系统中的block表
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
3.2.1 sigprocmask
how
:决定传入的set
与oldset
的操作
set
:重新设置表oldset
:保存之前的block表(输出型参数)
3.2.2 sigpending
获取当前进程的pending表,返回值小于0表示失败
3.2.1 使用函数进行信号阻塞
- 与捕捉信号一样,有些信号无法被捕捉例如:9,19
//阻塞ctrl+c发送的2号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void printpending(sigset_t& pending) {
for (int signo = 31; signo >= 1; signo--) {
cout << (sigismember(&pending, signo) ? "1" : "0");
}
cout << endl;
sleep(1);
}
int main() {
// cout << getpid() << endl;
sigset_t bst, oldset; //栈区
sigemptyset(&bst);
sigaddset(&bst, 2); //此时并没有设置到进程的task_struct中
sigprocmask(SIG_SETMASK, &bst, &oldset); //此时已完成屏蔽
sigset_t pending;
int cnt = 10;
while (cnt--) {
int n = sigpending(&pending);
if (n < 0) continue;
printpending(pending);
if (cnt == 0) {
sigprocmask(SIG_SETMASK, &oldset, nullptr);
}
}
}
4 信号的捕捉和处理
4.1 用户态与内核态
- 当进程从内核态返回到用户态的时候,进行信号的检测和处理!!!
- 调用系统调用函数时,操作系统自动从用户态切换到内核态
- 汇编语句:
int 80
,从用户态陷入内核态:CPU中的寄存器ecs
低两位00表示内核态,11表示用户态
-
mm_struct
:- 1G内核空间 ->内核级页表 -> 物理内存
- 3G用户空间 -> 页表 +
MMU
-> 物理内存
-
进程视角:当调度系统中的方法,就在自己的地址空间(内核空间)中执行
-
操作系统的本质:基于时钟中断的一个死循环
问:如何区分操作系统的内核态和用户态,他们是怎么转换的
答:
操作系统的内核态和用户态是对CPU功能的一种封装抽象,主要体现在特权级别和数据隔离上。
- 内核态:当CPU处于保护模式下,且
CPU.CS
=系统代码段时(CS.CPL=0
),即为内核态。在内核态下,程序可以访问操作系统内核数据结构和程序,具有对硬件的所有操作权限,可以执行所有CPU指令集,访问任意地址的内存。内核态的程序主要包括访问磁盘、内存分配、网卡、声卡等敏感操作。- 用户态:当
CPU.CS
=用户代码段时(CS.CPL=3
),即为用户态。用户态下的程序不能直接访问操作系统内核数据结构和程序,只有操控部分寄存器的权限。用户态和内核态之间的转换主要有以下几种方式:
- 系统调用:用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,例如fork()就是一个创建新进程的系统调用。
- 异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
- 外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停当前执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。
- 当进程的时间片消耗完毕时,①进入内核态,保存CPU上下文,切换到其他进程,②进入用户态,当再次切换到该进程时,③进入内核态,读取上下文,④进入用户态,执行进程程序代码和数据
4.2 信号的处理
//以程序中的除零异常为例
int main() {
signal(SIGFPE, [](int) {
cout << "除零异常" << endl;
sleep(1);
});
int a = 10;
int b = 0;
int c = a / b; //产生异常
return 0;
}
4.3 信号的捕捉——signal
&sigaction
- 参数1:信号编号
- 参数2:输入参数,传入新的
sigaction
结构体 - 参数3:输出参数,得到旧的
sigaction
结构体
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用**sa_mask
**字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
5 SIGCHLD
信号
- 子进程在退出的时候,会主动向父进程发送17号信号(
SIGCHLD
信号), 该信号的默认处理动作是忽略,
5.1 考虑多个子进程同时退出——WNOHANG
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void handler(int signo) {
sleep(1);
pid_t rid;
//非阻塞轮询等待
while((rid = waitpid(-1, nullptr, WNOHANG)) > 0) {
cout << "i am process: " << getpid() << ", signo: " << signo
<< ", child process quit: " << rid << endl;
}
}
int main() {
signal(SIGCHLD, handler);
for (int i = 0; i < 5; i++) {
pid_t id = fork();
if (id == 0) {
while(true) {
cout << "i am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
}
while(true) {
cout << "i am father process" << getpid() << endl;
sleep(1);
}
}
5.2 忽略子进程退出
- 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用
sigaction
将SIGCHLD
的处理动作置为**SIG_IGN
**,这样fork
出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction
函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。请编写程序验证这样做不会产生僵尸进程。
//子进程退出后,子进程资源自动被释放
signal(SIGCHLD, SIG_IGN);
hile(true) {
cout << “i am father process” << getpid() << endl;
sleep(1);
}
}
## 5.2 忽略子进程退出
- 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用`sigaction`将`SIGCHLD`的处理动作置为**`SIG_IGN`**,**这样`fork`出来的子进程在终止时会自动清理掉,不会产生僵尸进程**,也不会通知父进程。系统默认的忽略动作和用户用`sigaction`函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。请编写程序验证这样做不会产生僵尸进程。
```cpp
//子进程退出后,子进程资源自动被释放
signal(SIGCHLD, SIG_IGN);