学习目标:
掌握Linux信号的基本概念
掌握信号产生的一般方式
理解信号未决和阻塞的概念,原理。
掌握信号捕捉的一般方式。
了解可重入函数的概念。
Linux信号基本概念
进程信号是一种软件中断,用于通知进程发生的某些事件或请求特定的操作。当进程接收到一个信号时,它可以选择忽略信号、执行默认操作或者捕捉信号并执行相应的处理函数。
进程可以通过使用系统调用(如
kill
)向其他进程发送信号,也可以通过调用signal
或sigaction
等函数来设置对特定信号的处理方式。处理方式包括默认操作(如终止进程)、忽略信号或自定义信号处理函数。进程还可以使用sigprocmask
函数来设置或修改信号掩码,以决定在接收信号时是否阻塞其他信号的传递。通过信号,进程可以实现诸如进程终止、中断处理、定时器、子进程状态变化等功能。信号在多进程编程、进程间通信和进程控制等方面起着重要的作用。
1、信号编号
每个信号都有一个唯一的标识符,称为信号编号,用整数表示。
可以使用命令 man 7 signal 在终端上查看完整的信号列表及其作用。
常用的信号编号:
SIGINT (2): 中断信号,通常由终端按下 Ctrl+C 产生,用于请求进程中断运行。
SIGTERM (15): 终止信号,用于请求进程正常终止。
SIGKILL (9): 杀死信号,用于强制终止进程。该信号不能被忽略、阻塞或捕捉。
SIGSTOP (19): 停止信号,用于暂停进程的执行。
SIGCONT (18): 继续信号,用于恢复一个被停止的进程的执行。
SIGCHLD (17): 子进程状态变化信号,用于通知父进程子进程的终止或停止或继续。
SIGHUP (1): 终端挂起信号,通常在终端关闭或连接丢失时发送给进程。
SIGUSR1 (10) 和 SIGUSR2 (12): 用户自定义信号,可由进程根据需要自由使用。
SIGPIPE (13): 管道破裂信号,用于通知进程写入已关闭的管道。
SIGSEGV (11): 段错误信号,用于指示进程访问了无效的内存地址。
SIGALRM (14): 定时器信号,用于向进程发送定时器事件。
SIGTERM (16): 堆栈溢出信号,用于指示进程的堆栈空间已耗尽。
2、信号的内核结构
在Linux内核中,信号以数据结构的形式进行管理,存储在
struct task_struct
的结构体。通过修改这些结构中的字段,内核可以进行信号的传递、阻塞、解除阻塞以及选择性地处理信号。
发送信号也可以理解为写入信号
进程描述符中包含了与信号相关的字段,如下所示:
pending:一个用于管理未决信号的位图,表示当前进程正在等待处理的信号集合。
block:一个用于管理阻塞信号的位图,表示当前进程阻塞的信号集合。
hander:一个指向信号处理函数集合的指针,包含了进程的信号处理函数信息。
sigaction:一个结构体,用于指定信号处理函数及其行为的配置选项,包括信号处理函数的地址、标志和屏蔽字。
3、实时信号与标准信号
1)实时信号(Real-time signals)
每个进程可以使用实时信号的范围通常为
SIGRTMIN
到SIGRTMAX
(包括这两个值)之间的一组信号。实时信号具有优先级(使用较低的编号表示较高的优先级)。
实时信号可以排队传递,即多个实时信号可以按顺序排队等待进程处理。
2)标准信号(Standard signals)
标准信号编号范围通常为1到31。
标准信号的传递是不可靠的,即发送信号后,无法保证进程会立即接收到并处理。
信号产生
1、用户层产生信号的方式
1)终端产生信号
在终端中按下特定的组合键,如Ctrl+C(SIGINT)可以产生中断信号,Ctrl+\(SIGQUIT)可以产生退出信号。这些信号会被发送给终端关联的前台进程组。
在终端中,可以使用
jobs
命令查看当前正在运行的前台和后台进程列表。还可以使用fg
命令将一个后台进程切换到前台运行,或使用bg
命令将一个前台进程切换到后台运行。也可以使用
kill
命令可以向指定的进程发送信号。例如,使用kill -SIGTERM <PID>
可以发送终止信号(SIGTERM)给进程
2)系统调用
通过调用特定的系统调用函数,如
kill()
、raise()``abort()
等,用户程序可以请求操作系统帮助产生信号。
kill()
函数:
函数原型:
int kill(pid_t pid, int sig);
功能:向指定的进程或进程组发送信号。
参数:
pid
:要发送信号的目标进程的进程ID或进程组ID。
sig
:要发送的信号编号。返回值:成功返回0,失败返回-1。
raise()
函数:
函数原型:
int raise(int sig);
功能:向当前进程发送信号。
参数:
sig
:要发送的信号编号。返回值:成功返回0,失败返回-1。
abort()
函数:
函数原型:void abort(void);
功能:触发异常终止进程的操作。
通常会生成一个核心转储文件,可以使用调试工具对该文件进行分析,以确定引发异常终止的原因。
3)闹钟定时器(Alarm Timer)
使用
alarm()
或setitimer()
系统调用,用户程序可以设置闹钟定时器,当定时器到期时,会产生一个SIGALRM信号。
alarm()
函数
函数原型:unsigned int alarm(unsigned int seconds); 功能:设置闹钟定时器,在指定秒数后触发SIGALRM信号。 参数:
seconds:要设置的定时器触发时间,以秒为单位。 返回值:
前一次设置的闹钟剩余时间,如果之前没有设置闹钟,则返回0。
4)硬件异常
硬件异常是由计算机硬件引起的异常事件,例如内存访问错误、除以零、非法指令等。当这些异常事件发生时,处理器会立即中断当前执行的指令,并跳转到异常处理程序。
2、core dump:
Core dump是指在程序运行过程中发生严重错误或异常情况时,操作系统将当前进程的内存映像以二进制文件的形式保存下来的过程和结果。该文件通常被称为"core文件"或"core dump文件"。
它提供了一种快照,可以用于事后分析、调试和重现出错的场景。在Linux系统中,可以使用调试器(如GDB)来分析core dump文件,并查找程序崩溃的原因。
一般情况下,core dump文件的生成是由操作系统处理的,但可以通过设置特定的环境变量(如
ulimit -c
)或通过编程手段来控制是否生成core dump文件,以及core dump文件的大小限制。但一般会被关闭,占用空间资源
信号状态
1、信号阻塞
信号阻塞(Signal Pending)是指被阻塞的信号集合。
通过阻塞信号,进程可以延迟对特定信号的处理或防止其被中断。当信号被阻塞时,即使接收到该信号,进程也不会立即处理它,而是将其排队等待解除阻塞后再处理。
本质上是将内核结构中位图block对应信号的状态置1
1)sigprocmask函数
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
功能:该函数用于设置和修改进程的信号掩码,即设置进程的信号阻塞集合。它可以用来阻塞或解除阻塞特定信号的传递给进程
参数:
how
:控制信号掩码的操作类型
SIG_BLOCK
:将set
中的信号添加到当前信号掩码中,即阻塞指定的信号。
SIG_UNBLOCK
:从当前信号掩码中移除set
中的信号,即解除对指定信号的阻塞。
SIG_SETMASK
:将当前信号掩码设置为set
,即用set
中的信号替换当前信号掩码。
set
:指向要设置的信号集合的指针,用于指定要添加、移除或替换的信号。
oldset
:可选参数,用于保存之前的信号掩码。如果不为 NULL,则之前的信号掩码将被保存到oldset
所指向的sigset_t
变量中。
2、信号未决
信号未决(Signal Pending)是指进程接收到但尚未处理的信号集合。当进程接收到一个信号时,该信号会被添加到未决信号集中,表示该信号正在等待处理。
本质上是将内核结构中位图pending对应信号的状态置1
1)sigpending函数
函数原型:int sigpending(sigset_t *set)
功能:该函数用于获取当前进程的未决信号集,将结果存储在参数
set
所指向的sigset_t
类型变量中
3、sigset_t
sigset_t
是一个数据类型,用于表示信号集(Signal Set)。它是一个位图,每个位代表一个信号的状态,用于指示信号的阻塞、未决或允许传递给进程。
这些函数是与 sigset_t
相关的系统调用函数,用于设置和操作信号集。
int sigemptyset(sigset_t *set)
:该函数将信号集set
初始化为空集,即清空所有信号位。它将信号集中的所有位设置为 0。
int sigfillset(sigset_t *set)
:该函数将信号集set
设置为包含系统中所有的信号,即将所有信号位设置为 1。
int sigaddset(sigset_t *set, int signo)
:该函数将信号signo
添加到信号集set
中,将对应的信号位设置为 1。
int sigdelset(sigset_t *set, int signo)
:该函数将信号signo
从信号集set
中删除,将对应的信号位设置为 0。
int sigismember(const sigset_t *set, int signo)
:该函数检查信号signo
是否在信号集set
中。如果信号存在于集合中,返回非零值;否则,返回 0。
信号递达
1、信号捕捉
1)进程内核态和用户态
用户态(User Mode)当进程在用户态执行时,它只能访问受限的资源和执行受限的操作。
内核态(Kernel Mode):当进程在内核态执行时,它拥有完全的访问权限,可以执行任意的指令并访问系统的所有资源,包括底层硬件资源。
2)内核对信号的检查
内核对信号的检查发生在用户态和内核态的切换,如执行系统调用(System Call)、异常、中断处理
3)莫比乌斯环
2、信号处理方式
1)默认操作(Default Action)
可以将信号处理函数设置为
SIG_IGN
,表示接收到的信号实现默认操作。默认操作可能是终止进程、忽略信号或终止进程并生成核心转储文件等。
2)忽略信号(Ignore)
可以将信号处理函数设置为
SIG_IGN
,表示忽略接收到的信号。当信号被忽略时,进程不会对该信号做任何处理,直接忽略它。忽略某些信号可能会有安全风险或导致意外行为,所以只有少数信号是可以安全忽略的。
3)自定义信号处理函数(Custom Handler)
可以为信号设置自定义的信号处理函数。这种方式允许进程在接收到信号时执行自定义的操作,而不是使用默认操作。自定义信号处理函数可以是用户定义的函数,当接收到信号时会调用该函数进行处理。
3、用于捕捉的系统调用:
1)signal
函数原型:
#include <signal.h> void (*signal(int signum, void (*handler)(int)))(int);
参数说明:
signum
:表示要设置处理函数的信号编号。可以是预定义的信号常量,如SIGINT
、SIGTERM
等,或者是自定义的信号编号。
handler
:表示要设置的信号处理函数的指针。可以是一个函数指针,也可以是预定义的信号处理选项之一。常见的信号处理选项包括:
SIG_DFL
:默认操作,即执行信号的默认操作。
SIG_IGN
:忽略信号,即不对接收到的信号进行任何操作。
SIG_ERR
:表示设置信号处理函数失败。
返回值:
返回值是一个指向之前信号处理函数的指针,如果之前没有设置过信号处理函数,则返回
SIG_DFL
(默认操作)或SIG_IGN
(忽略信号)。
以下是 sigal()
函数的使用示例:
#include <stdio.h> #include <signal.h> void sig_handler(int signum) { printf("Received signal: %d\n", signum); // 执行自定义的信号处理操作 } int main() { signal(SIGINT, sig_handler); // 设置 SIGINT 的处理函数为 sig_handler while (1) { // 执行一些操作 } return 0; }
2)sigaction
函数原型:
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明:
signum
:要设置处理函数的信号编号。
act
:一个指向struct sigaction
结构的指针,包含了设置信号处理的详细信息,如处理函数的地址、信号处理的标志等。
oldact
:一个可选的指向struct sigaction
结构的指针,用于存储之前的信号处理配置。
struct sigaction
结构定义如下:
struct sigaction { void (*sa_handler)(int); // 信号处理函数的地址 void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数的地址(扩展形式) //`sa_handler` 和 `sa_sigaction` 二选一 sigset_t sa_mask; // 信号掩码,用于阻塞其他信号 int sa_flags; // 信号处理的标志 void (*sa_restorer)(void); // 由旧的 BSD 信号机制使用,现已不再使用 };
以下是 sigaction()
函数的使用示例:
#include <stdio.h> #include <signal.h> void sig_handler(int signum) { printf("Received signal: %d\n", signum); } int main() { struct sigaction act; act.sa_handler = sig_handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, NULL); // 设置 SIGINT 的处理函数为 sig_handler while (1) { // 执行一些操作 } return 0; }
可重入函数:
可重入函数(Reentrant Function)是指在多个线程同时调用时,能够保证函数执行的正确性和可预期性,不会产生竞态条件或数据不一致的问题。可重入函数的设计要考虑线程安全性和数据共享的问题。
volatile:
volatile
是一个关键字,用于在编程中声明一个变量是易变的(Volatile)。它告诉编译器不要对该变量进行优化,以确保每次访问变量时都从内存中读取最新的值,并在每次写入变量时将其立即写回内存。保持内存的可见性
SIGCHLD:
父进程的等待方案:信号丢失,阻塞
忽略处理(忽略的特例),让子进程退出,不要给父进程发送信号