目录
4、alarm 函数(闹钟函数)(一个进程只能设置一个闹钟)
3、sigaction 函数和 sigaction 结构体(配合信号集使用)
4、sigprocmask 函数(用于设置block图,配合信号集使用)
5、sigpending 函数(用于查看待处理/阻塞信号的位图)
Linux 信号详解
在 Linux 系统中,信号是一种用于进程间通信的异步通知机制,它可以用于通知进程发生了某种事件,进程可以根据信号的类型做出相应的处理。下面我们将从多个方面对 Linux 信号进 行详细的讲解。
一、信号是什么
1、是什么
信 号是进程之间事件异步通知的⼀种⽅式,属于软中断。
2、查看信号
3、信号处理流程
二、发出信号的方式(信号的产生)
1、终端按键产生信号:比如按下 Ctrl+C 产生 SIGINT 信号,按下 Ctrl+\ 产生 SIGQUIT 信号,用于强制终止进程。
2、系统函数调用:通过kill、raise、abort等函数向进程发送信号。
3、软件条件产生:例如,当进程执行除 0 操作时,会产生 SIGFPE(浮点异常)信号;当进程访问非法内存地址时,会产生 SIGSEGV(段错误)信号。
4、硬件异常产生:如内存越界、除零操作等硬件错误会引发相应的信号。
下面介绍系统函数调用来产生信号
1、kill 函数
#incldue<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
pid:指定接收信号的进程 ID。如果pid > 0,信号将发送给指定 ID 的进程;
如果pid = 0,信号将发送给与当前进程同组的所有进程;
如果pid < 0,信号将发送给进程组 ID 为-pid的所有进程;
如果pid = -1,信号将发送给系统内的所有进程(除了进程 1 和一些特殊进程)。
sig:指定要发送的信号编号或宏定义名称。
返回值:成功时返回 0,出错时返回 - 1。
用例 一个父进程用 kill 函数杀死子进程
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t id = fork();
if (id == 0) {
// 子进程
printf("Child process is running, pid: %d\n", getpid());
while (1) {
sleep(1);
}
} else if (child_pid > 0) {
// 父进程
sleep(3);
// 向子进程发送SIGTERM信号
if (kill(child_pid, SIGTERM) == 0) {
printf("Sent SIGTERM signal to child process\n");
} else {
perror("kill");
}
} else {
perror("fork");
}
return 0;
}
在这个例子中,父进程通过fork创建子进程,然后在 3 秒后使用kill函数向子进程发送 SIGTERM 信号,子进程收到信号后会按照默认方式终止。
2、raise 函数
#include<signal.h>
int raise(int sig);
- 参数说明:sig指定要发送的信号编号或宏定义名称,该函数用于杀死自己
- 返回值:成功时返回 0,出错时返回非零值。
用例:
#include <stdio.h>
#include <signal.h>
int main() {
// 向自身发送SIGINT信号
raise(SIGINT);
return 0;
}
在这个例子中,我们先设置了 SIGINT 信号的处理函数,然后使用raise函数向自身发送 SIGINT 信号,来用SIGINT信号来杀死自己。
3、abort 函数
#inlude<stdlib.h>
void abort(void);
功能说明:该函数用于向当前进程发送 SIGABRT 信号,使进程异常终止,并产生核心转储文件(如果系统允许)。
- 用例
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Before abort...\n");
// 向自身发送SIGABRT信号
abort();
printf("This line will not be executed.\n");
return 0;
}
在这个例子中,调用abort函数后,程序会收到 SIGABRT 信号,按照默认处理方式异常终止,后面的打印语句不会被执行。
4、alarm 函数(闹钟函数)(一个进程只能设置一个闹钟)
#inlude<stdlib.h>
void abort(void);
参数说明:seconds指定经过多少秒后向当前进程发送 SIGALRM 信号。如果seconds为 0,则取消之前设置的闹钟。
返回值:返回之前设置的闹钟剩余的秒数,如果之前没有设置闹钟,则返回 0。
用例
#include <stdio.h>
#include <unistd.h>
void handler(int signum) {
printf("Received SIGALRM signal, time's up!\n");
exit(0);
}
int main() {
signal(SIGALRM, handler);
// 设置5秒后发送SIGALRM信号
alarm(5);
printf("Waiting for 5 seconds...\n");
while (1) {
sleep(1);
}
return 0;
}
在这个例子中,我们设置了 5 秒后发送 SIGALRM 信号,并自定义了该信号的处理函数。5 秒后,程序会收到 SIGALRM 信号,执行处理函数后退出。
三、捕捉信号的方式
流程
如果信号的处理动作是⽤⼾⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。由于信号处理函数的代码是在⽤⼾空间的,处理过程⽐较复杂,举例如下:
⽤⼾程序注册了 SIGQUIT 信号的处理函数 sighandler 。
当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态。
在中断处理完毕后要返回⽤⼾态的 main 函数之前检查到有信号 • 内核决定返回⽤⼾态后不是恢复 数, sighandler 和 SIGQUIT 递达。 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 函 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个 独⽴的控制流程。
s ighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态。
如果没有新的信号要递达,这次再返回⽤⼾态就是恢复 main 函数的上下⽂继续执⾏了。
简化图
用户态和内核态详解:
用户态(User Mode)和内核态(Kernel Mode)是操作系统中两种不同的CPU运行状态,用于区分程序执行的权限级别和资源访问能力12。
一、用户态和内核态是什么
在 Linux 系统中,为了保护操作系统的关键资源和确保系统稳定运行,将处理器的执行模式划分为用户态和内核态 。
- 用户态:是应用程序运行的模式。处于用户态的程序受到诸多限制,只能访问有限的内存空间,并且不能直接执行一些敏感的操作,如访问硬件设备、修改系统关键数据等。应用程序在用户态下执行的指令集是经过筛选的,不具备直接控制硬件和操作系统核心功能的能力。例如,我们日常使用的文本编辑器、浏览器等应用程序,在运行时都处于用户态。
- 内核态:是操作系统内核运行的模式。在内核态下,程序拥有最高的权限,可以访问系统的所有资源,包括硬件设备、内存的所有区域,并且能够执行任何指令,如控制 CPU、磁盘、网卡等硬件设备,进行进程调度、内存管理等核心操作。内核态下的代码直接与硬件交互,负责处理系统的各种底层事务,保障系统的正常运行。
二、用户态和内核态的切换方式
用户态和内核态之间的切换是 Linux 系统实现资源管理和保护的重要机制,主要通过以下几种方式实现:
- 系统调用:这是应用程序主动从用户态切换到内核态的最常见方式。当应用程序需要使用操作系统提供的服务,如文件读写、网络通信、进程创建等功能时,会通过系统调用陷入内核态。例如,当应用程序调用open函数打开一个文件时,会触发系统调用,CPU 会切换到内核态,由内核中的文件系统模块来处理文件打开的具体操作,完成后再返回用户态并将结果返回给应用程序。系统调用通过软中断(在 x86 架构上通常是int 0x80或更现代的syscall指令)来实现,它会保存用户态的当前执行环境(如寄存器值、程序计数器等),然后跳转到内核态的系统调用处理函数执行相应操作。
- 异常:当 CPU 在执行用户态程序时遇到一些异常情况,如除零错误、内存访问越界、非法指令等,会自动触发异常机制,导致 CPU 切换到内核态。内核会根据异常的类型进行相应的处理,例如,当发生内存访问越界异常时,内核可能会终止该进程或者向进程发送一个SIGSEGV信号。在处理完异常后,根据具体情况决定是恢复用户态程序的执行还是终止程序。
- 外部中断:外部设备(如键盘、鼠标、网卡等)产生的中断信号也会使 CPU 从用户态切换到内核态。当外部设备完成某项操作(如键盘按键按下、网卡接收到数据)时,会向 CPU 发送一个中断请求,CPU 在响应中断时,会暂停当前用户态程序的执行,保存现场,切换到内核态执行中断处理程序。中断处理程序会处理设备的请求,如读取键盘输入的数据、接收网卡数据等,处理完成后恢复用户态程序的执行。
三、用户态和内核态的数据交互
用户态和内核态之间的数据交互是实现系统功能的重要环节,主要有以下几种方式:
- 参数传递:在进行系统调用时,应用程序需要将相关的参数传递给内核。这些参数可以通过寄存器传递,也可以在用户空间和内核空间之间共享内存区域来传递。例如,在调用write函数向文件写入数据时,应用程序会将文件描述符、要写入的数据缓冲区地址以及数据长度等参数传递给内核,内核根据这些参数完成数据写入操作。
- 共享内存:为了提高数据传输的效率,用户态和内核态之间可以通过共享内存的方式进行数据交互。内核可以分配一段物理内存,并将其映射到用户空间和内核空间,使得用户态程序和内核态代码都可以访问该内存区域。这种方式常用于需要频繁进行大量数据传输的场景,如某些高性能的网络应用程序与内核网络模块之间的数据交互。
- 内核缓冲区:内核通常会维护一些缓冲区,用于暂存数据。例如,在文件系统中,内核会使用缓冲区来缓存从磁盘读取的数据或准备写入磁盘的数据。当用户态程序进行文件读写操作时,数据会先在用户态缓冲区和内核缓冲区之间进行传输,然后再由内核根据具体情况进行磁盘 I/O 操作。
四、处理信号的几种方式(信号的处理-处理)
1、前提
默认处理:每个信号都有一个默认的处理方式,如终止进程、忽略信号、产生核心转储文件等。例如,SIGKILL 信号的默认处理方式是立即终止进程,且该信号不能被捕获和忽略。
忽略信号:进程可以选择忽略某些信号,使其不产生任何效果。其中忽略也是一种动作
自定义处理:进程可以自定义信号处理函数,当收到特定信号时,执行自定义的处理逻辑。
2、signal 函数
#include<signal.h>
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:指定要处理的信号编号或宏定义名称。
handler:指定信号的处理方式,可以是SIG_IGN(忽略信号)、SIG_DFL(使用默认处理方式)或给他传一个自定义函数指针。
返回值:成功时返回上一次该信号的处理函数指针,出错时返回SIG_ERR。
用例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum) {
printf("hello\n");
exit(0);
}
int main() {
// 设置SIGINT信号的自定义处理函数
signal(SIGINT, sigint_handler);
while (1) {
sleep(1);
}
return 0;
}
在这个例子中,我们通过signal函数自定义了 SIGINT 信号的处理函数sigint_handler,当程序收到 SIGINT 信号(按下 Ctrl+C)时,会执行sigint_handler函数,打印提示信息后退出程序。
3、sigaction 函数和 sigaction 结构体(配合信号集使用)
sigaction函数用途于signal函数类似,但它提供了比传统
signal
函数更强大、更灵活的信号处理机制。
sigaction函数(函数的主要作用是检查或修改与指定信号相关联的处理动作)
#include<signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明:
signum:指定要设置处理方式的信号编号或宏定义名称。
act:指向struct sigaction结构体的指针,用于指定新的信号处理方式。(输出型参数)
oldact:如果不为 NULL,用于保存原来的信号处理方式。(输出型参数)
返回值:成功时返回 0,出错时为-1
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); // 已废弃字段
};
使用时,将自定义处理方式的函数赋值给sa_handler。
用 sigemptyset() 函数清空sa_mask,清空阻塞集
将sa_flags赋值为 0
五、保存信号的方式(信号的处理-待处理/阻塞)
1、前提介绍:
在 Linux 系统中,信号的保存涉及到几个重要的位图:
- pending 位图:用于记录当前进程收到的所有信号,每一位对应一个信号,若该位为 1,表示进程收到了相应的信号。
- block 位图:用于记录当前被阻塞的信号,被阻塞的信号不会立即被处理,而是处于挂起状态,直到该信号的阻塞被解除。
- handler 位图:记录了每个信号对应的处理方式(默认、忽略、自定义处理函数)。
2、信号阻塞和信号忽略
信号阻塞:是指进程可以将某些信号设置为阻塞状态,被阻塞的信号即使到达进程也不会立即被处理,而是处于挂起状态。可以使用sigprocmask函数来设置信号的阻塞状态。
信号忽略:是指进程明确表示不处理某些信号,使其不产生任何效果。可以通过将信号的处理方式设置为SIG_IGN来实现信号忽略,例如signal(SIGINT, SIG_IGN);。
3、sigemptyset 相关函数
sigset_t是一个信号集,是一个位图,下面是用来改变此位图的相关函数
(1)、sigemptyset函数
#include<signal.h> int sigemptyset(sigset_t *set);
参数说明:set是指向sigset_t类型变量的指针,该函数用于清空信号集set,即将其中所有信号的对应位都设置为 0。
返回值:成功时返回 0,出错时返回 - 1。
(2)、sigfillset函数
#include<signal.h> int sigfillset(sigset_t *set);
参数说明:set是指向sigset_t类型变量的指针,该函数用于将信号集set中的所有位都设置为 1,即包含所有信号。
返回值:成功时返回 0,出错时返回 - 1。
(3)、 sigaddset函数
#include<signal.h> int sigaddset(sigset_t *set, int signum);
参数说明:set是指向sigset_t类型变量的指针,signum指定要添加到信号集中的信号编号或宏定义名称。
返回值:成功时返回 0,出错时返回 - 1。
(4)、sigdelset函数
#include<signal.h> int sigdelset(sigset_t *set, int signum);
参数说明:set是指向sigset_t类型变量的指针,signum指定要删除此信号集中的信号编号或宏定义名称。
返回值:成功时返回 0,出错时返回 - 1。
(5)、sigismember函数
#include<signal.h> int sigismember(sigset_t *set, int signum);
参数说明:set是指向sigset_t类型变量的指针,signum是一个信号编号或者宏定义名称,
该函数用于检查signum信号是否在信号集中。
返回值:成功时返回 0,出错时返回 - 1。
4、sigprocmask 函数(用于设置block图,配合信号集使用)
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
how:指定信号屏蔽操作的方式,有以下几种取值:
SIG_BLOCK:将set中的信号添加到当前进程的信号屏蔽字中,即阻塞这些信号。
SIG_UNBLOCK:将set中的信号从当前进程的信号屏蔽字中移除,解除对这些信号的阻塞。
SIG_SETMASK:将当前进程的信号屏蔽字设置为set中的信号集合。
set:指向要操作的信号集。
oldset:如果不为 NULL,用于保存原来的信号屏蔽字(输出型参数)。
返回值:成功时返回 0,出错时返回 - 1。
用例
#include <stdio.h>
#include <unistd.h>
void sigalrm_handler(int signum) {
printf("Received SIGALRM signal, time's up!\n");
exit(0);
}
int main() {
signal(SIGALRM, sigalrm_handler);
// 设置5秒后发送SIGALRM信号
alarm(5);
printf("Waiting for 5 seconds...\n");
while (1) {
sleep(1);
}
return 0;
}
在这个例子中,我们先阻塞了 SIGINT 信号,此时按下 Ctrl+C 不会立即终止程序,10 秒后解除对 SIGINT 信号的阻塞,再次按下 Ctrl+C 就会执行自定义的信号处理函数。
5、sigpending 函数(用于查看待处理/阻塞信号的位图)
#include<signal.h>
int sigpending(sigset_t *set);
参数说明:set是指向sigset_t类型变量的指针,用于保存当前进程中处于挂起状态(已收到但未处理)的信号集合。是一个输出型参数
返回值:成功时返回 0,出错时返回 - 1。
用例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
sigset_t pending_set;
// 获取当前进程中处于挂起状态的信号集合
if (sigpending(&pending_set) == 0) {
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT signal is pending.\n");
} else {
printf("SIGINT signal is not pending.\n");
}
} else {
perror("sigpending");
}
return 0;
}
在这个例子中,我们使用sigpending函数获取当前进程中处于挂起状态的信号集合,并检查 SIGINT 信号是否在其中。
6、pause函数
pause
函数的主要作用是:
- 使调用进程进入睡眠状态,直到捕获到一个信号
- 在信号处理程序执行完毕后,
pause
才会返回 - 如果信号导致进程终止,则
pause
不会返回
总结:
1、如何理解信号处理
信号处理是进程对收到的信号做出响应的过程。当进程收到一个信号时,系统会根据信号的处理方式进行相应的操作。
如果是默认处理,系统会按照预定义的行为处理信号;
如果是忽略信号,系统会直接丢弃该信号;
如果是捕捉信号,系统会暂停当前进程的正常执行流程,转而执行用户自定义的信号处理函数, 处理完信号后,再回到原来被中断的地方继续执行。
2、如何记录信号
前面提到的 pending 位图就是用于记录信号的一种方式。当进程收到一个信号时,系统会将 pending 位图中对应的位置为 1,表示该信号已收到。此外,系统还会维护一些与信号相关的内核数据结构,用于记录信号的详细信息,如信号的来源、发送时间等。
3、如何执行信号
当信号的阻塞被解除且信号处于 pending 状态时,系统会根据信号的处理方式执行相应的操作。如果是默认处理或忽略信号,系统会直接按照预定义的规则执行;如果是捕捉信号,系统会调用用户自定义的信号处理函数来执行相应的逻辑。
补充——前台程序和后台程序
1、前台程序
前台程序是指在终端中当前正在运行,并且占据终端输入输出的程序。当终端有输入时,输入会被发送到前台程序;
我们平时运行的一般为前台程序,可以通过ctrl+c来终止。
2、后台程序
后台程序是指在终端中启动后,不占据终端输入输出,可以在后台继续运行的程序。通常可以通过在命令后加上&符号将程序放入后台运行。
此时运行的程序为后台程序,后台程序无法通过ctrl+c来杀死。后台程序默认不会收到一些终端产生的信号,但可以通过kill命令向其发送信号。
-----------------------------------------------------------------------------------------------------------------------------