信号

Linux里信号概念和作用

概念

信号 描述
SIGHUP 1 终端挂起或控制进程终止
SIGINT 2 中断(Interrupt)信号,通常由Ctrl+C触发
SIGQUIT 3 退出(Quit)信号,通常由Ctrl+\触发,导致进程终止并生成核心转储
SIGILL 4 非法指令(Illegal Instruction)
SIGTRAP 5 跟踪陷阱(Trace Trap)
SIGABRT 6 由abort函数产生的信号
SIGBUS 7 总线错误(Bus Error)
SIGFPE 8 浮点异常(Floating Point Exception),如除以零
SIGKILL 9 杀死(Kill)信号,强制终止进程
SIGUSR1 10 用户定义信号1
SIGSEGV 11 段错误(Segmentation Fault),非法内存访问
SIGUSR2 12 用户定义信号2
SIGPIPE 13 管道破裂(Broken Pipe),写入无读端的管道
SIGALRM 14 闹钟(Alarm)信号,由alarm函数触发
SIGTERM 15 终止(Termination)信号,软件终止(可捕获)
SIGCHLD 17 子进程停止或终止
SIGCONT 18 继续(Continue)信号,使暂停的进程继续执行
SIGSTOP 19 停止(Stop)信号,暂停进程(不可捕获和忽略)
SIGTSTP 20 终端停止(Terminal Stop)信号,通常由Ctrl+Z触发,暂停前台进程
SIGTTIN 21 后台进程尝试从终端读数据
SIGTTOU 22 后台进程尝试向终端写数据

信号

信号是进程处理紧急情况所用的一种方式,它没有特别复杂的数据结构,就是用一个代号一样的数字

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。

  • Linux 提供了几十种信号,分别代表不同的意义。我们可以通过kill -l命令查看信号。

  • 信号在进程的整个生命周期里都有效。

  • 信号的产生和代码运行的过程是异步的。

    • 因此收到信号之后,可能不会立即处理这个信号,即pengding(例如进程正在进行IO),因此需要先将信号保存起来。

中断

中断又分为软中断和硬中断:

  • 上半部直接处理硬件请求,也就是硬中断。硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序。

  • 信号与进程之间,属于软中断。软中断(下半部)是以内核线程的方式执行,并且每一个 CPU 都对应一个软中断内核线程,比如 0号 CPU 对应的软中断内核线程的名字是ksoftirqd/0

产生信号

  • 信号发送来源广泛,有可能来自于用户态,有可能来自于硬件,也有可能来自于内核。
    • 来自键盘组合键 : Ctrl+C 产生 SIGINT 信号,Ctrl+Z 产生 SIGTSTP 信号。
    • 来自kill命令 :kill -9 pid 可以发送信号给一个进程,杀死它。
    • 来自系统调用 :有的时候,硬件异常也会产生信号。例如:当执行了除以零的指令时,CPU会产生一个异常(状态标志位会进行标记),然后发送SIGFPE(浮点异常)信号给进程。

会话(session) 前/后台进程

在我们登录Linux服务器时,会产生一个会话,对于一个会话,一般只会配上一个bash(默认前台进程)。

  • 当我们执行一个进程时,如果这个进程变为了前台进程bash则会变为后台进程,不再提供代码解释。

每一个终端会话只能有一个前台进程组,但可以有多个后台进程,并且我们的键盘信号只能发送给前台进程。

  • 这也就是为什么我们在终端中运行./可执行程序之后,在同一终端中无法使用bash指令。因为此时该程序会占用前台(阻塞当前的bash),此时bash会等待这个前台进程完成。

  • 你想在运行可执行程序的同时继续使用bash,可以使用&将该程序放到后台运行

    • ./可执行程序 &。这样,该程序会在后台运行,而bash会继续在前台等待你的输入。

其中区分前后台进程,可以使用谁获取键盘输入,谁就是前台进程这样的方法。

操作 作用
jobs 查看后台进程
gf -任务号 将某一个任务提为前台进程
^Z 暂停一个前台进程
bg 将暂停(挂起)状态的作业继续在后台运行

信号的处理

  • 执行默认操作。

  • 捕捉信号(并非所有信号都能被捕捉):提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数。

    • 如果没有收到信号,那么这个方法不会被调用。
  • 忽略信号。

常见的函数

函数 描述 函数原型
signal 设置一个函数来处理一个信号。 void (*signal(int signum, void (*handler)(int)))(int);
raise 发送信号给调用进程。 int raise(int sig);
kill 向进程或进程组发送信号。 int kill(pid_t pid, int sig);
sigaction 更改或检查指定信号的处理程序。 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigemptyset 初始化一个空的信号集。 int sigemptyset(sigset_t *set);
sigfillset 初始化一个包含所有信号的信号集。 int sigfillset(sigset_t *set);
sigaddset 将指定信号添加到信号集中。 int sigaddset(sigset_t *set, int signum);
sigdelset 从信号集中删除指定信号。 int sigdelset(sigset_t *set, int signum);
sigprocmask 更改进程的信号屏蔽字(阻塞信号)。 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigsuspend 暂停进程直到接收到一个信号。 int sigsuspend(const sigset_t *mask);
sigpending 检查当前未决的信号(进程正在等待处理的信号)。 int sigpending(sigset_t *set);
alarm 在指定秒数后发送SIGALRM信号给进程。 unsigned int alarm(unsigned int seconds);
pause 使进程暂停执行,直到接收到信号。 int pause(void);
sigwait 等待一个或多个信号。 int sigwait(const sigset_t *set, int *sig);
sigqueue 向进程发送一个信号和数据。 int sigqueue(pid_t pid, int sig, const union sigval value);

当设置了多个alarm时,本次调用alarm的返回值是上次调用的剩余时间。判断alarm是否超时可以使用堆,到时只用判断堆顶就知道

普通信号[1-31号]如何保存的?

进程的task_struct(PCB)内,存在一个signal(32-bit),利用它的比特位来表示一个信号。即位图管理。所谓的发信号,实际就是操作系统去修改对应进程pcb的信号位图。

阻塞-未决信号与信号方法

  • 每个信号都有两个标志位分别表示**阻塞(block) 未决(pending),还有一个函数指针数组标(handler)**表示处理动作。

    • block位图用来记录是否屏蔽(阻塞)某个信号。

    • pending位图用来记录是否收到某个信号。

    • 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志

block-pending-handler

具体流程

  1. 信号送达:信号到达进程时,对应的pending位图位被设置为 1。

  2. 检查阻塞表:如果该信号未被阻塞,信号处理函数将被调用。

  3. 清除pending位图:在信号处理函数执行之前,相应的pending位图位被清除(从 1 变为 0)。

  4. 添加到block表:在信号处理函数执行期间,该信号被自动添加到block表中,防止信号处理函数被嵌套调用。

  5. 信号处理完毕:信号处理函数执行完毕后,该信号从block表中移除,恢复之前的阻塞状态。

sigset_t数据类型

  • 每个信号只有一个bit的未决标志,非01,不记录该信号产生了多少次,阻塞标志也是这样表示的。

  • 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集。

信号集操作函数

函数 原型 描述
sigemptyset int sigemptyset(sigset_t *set); 初始化一个信号集为空集
sigfillset int sigfillset(sigset_t *set); 初始化一个信号集,并将所有的信号加入到此信号集中
sigaddset int sigaddset(sigset_t *set, int signo); 将指定的信号加入到信号集中
sigdelset int sigdelset(sigset_t *set, int signo); 从信号集中删除指定的信号
sigismember int sigismember(const sigset_t *set, int signo); 检查指定的信号是否在信号集中
sigprocmask int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 检查或更改当前阻塞的信号集
sigsuspend int sigsuspend(const sigset_t *mask); 暂时替换进程的信号掩码,并挂起进程直到接收到一个信号
sigpending int sigpending(sigset_t *set); 获取当前未决信号集
sigaction int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 检查或修改指定信号的处理行为
sigwait int sigwait(const sigset_t *set, int *sig); 等待信号集中的一个信号,并将其从信号集中删除
sigwaitinfo int sigwaitinfo(const sigset_t *set, siginfo_t *info); 等待信号集中的一个信号,并返回附加信息
sigtimedwait int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout); 等待信号集中的一个信号,带有超时时间

信号处理和捕获

当进程从内核态返回到用户态的时候,进行信号的检测和处理。

操作系统会自动的完成身份切换。

基于信号的等待

  • 条件

    1. 子进程在退出的时候,会发出17号信号。
    2. 父进程要保持一直在运行。
    3. 最后还是需要使用wait()/waitpid()这样的接口来解决子进程的僵尸状态
  • 因此,我们可以完成基于信号捕捉等待。如果是多个子进程,那么在信号捕获的基础上采用循环非阻塞(非阻塞:应对只退出一半子进程的情况)

    1
    while((rid = waitpid(-1,nullptr,WNOHANG))>0)...

特殊情况

  • SIGCHLD信号:
    • 在一些Unix系统(包括Linux)中,如果父进程将SIGCHLD信号的处理动作设置为SIG_IGN,则子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。不过,这种方法在所有的Unix系统上并不完全保证可用。

信号
https://weihehe.top/2024/07/23/信号/
作者
weihehe
发布于
2024年7月23日
许可协议