OS56.【Linux】理解信号: 信号的产生(1) 键盘输入和kill命令
目录
之后的文章将按这个顺序来:
信号的产生→信号的保存→信号的捕捉
1首先说明: 信号和信号量没有任何关系!
注: 信号量在OS56.【Linux】理解信号量文章讲过
生活中的例子
从生活中的例子理解信号: 信号弹、上下课铃声、闹钟等等都可以认为是信号
那为什么能认为"信号弹、上下课铃声、闹钟"是信号呢?
答: 有人提前教过我们,我们我记住了这些就是信号,可以推出: 即使是现在没有信号产生,我们也知道信号产生之后应该干什么,例如红灯停,绿灯行 → 识别信号,知道信号的处理方法
产生信号后,可能不会立即处理
信号产生了,由于某些原因(例如当前处理的事情非常重要,无法暂停),我们可能并会不立即处理这个信号,在合适的时候才会处理
所以,信号产生后到信号处理这个时间窗口内,进程必须记住信号的到来
结论
1.进程必须识别且能够处理信号: 即使信号没有产生,也要具备处理信号的能力,信号的处理能力属于进程内置功能的一部分
2.当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,合适的时候才会去执行对应的动作,那么进程在这个时间窗口需要具有临时保存哪些信号已经发生了的能力
2.从Ctrl+C来看信号的产生
新建以下文件:
test_signal/ ├── makefile ├── send_signal.sh └── test_signal.cppmakefile写入:
test_signal.out:test_signal.cpp g++ -o $@ $^ -g -std=c++11 .PHONY:clean clean: rm -f test_signal.outtest_signal.cpp写入无限向终端打印hello world的程序:
#include <unistd.h> #include <iostream> using namespace std; int main() { for (;;) { std::cout<<"Hello World!"<<std::endl; sleep(1); } return 0; }启动:
/test_signal.out 运行结果: 按下Ctrl+C,进程停止执行

为什么 Ctrl+C 可以杀死进程?
上方运行的结果可以看到: 输入任何bash命令都不会起作用,但是Ctrl+C杀死了这个进程
Linux中,一次登陆分配一个终端,一般会配上一个bash,每一个登陆,只允许一个进程是前台进程,可以允许多个进程是后台进程
那么前台进程和后台进程的区别是: 只有前台进程能获取键盘输入
那么无限向终端打印hello world的进程为前台进程,bash为后台进程,这样bash无法获取键盘输入
如果将无限向终端打印hello world的进程为启动为后台进程:
/test_signal.out &运行结果: Ctrl+C无法结束无限向终端打印hello world的进程为启动的后台进程

Bash内部对Ctrl+C做了特殊处理
那Ctrl+C为什么没有杀死bash? 因为Bash内部对Ctrl+C做了特殊处理,Ctrl+C是向前台进程发送了2号信号来终止前台进程的(这个后面的文章再解释)
在bash-5.3的根目录下有一个sig.c文件,注释说明了原因:
#define NULL_HANDLER (SigHandler *)SIG_DFL /* The list of signals that would terminate the shell if not caught. We catch them, but just so that we can write the history file, and so forth. */ static struct termsig terminating_signals[] = { //...... #ifdef SIGINT { SIGINT, NULL_HANDLER, 0 }, #endif //...... };注释里面说的很清楚: 这些信号如果不捕获(捕获这个概念在本文 4.形象理解信号的处理方式 提到了)的话,会终止shell
附: bash-5.3的国内下载链接https://mirrors.nju.edu.cn/gnu/bash/bash-5.3.tar.gz
结论: Ctrl+C 可以杀死前台进程,Ctrl+C向前台进程发送2号信号SIGINT
3.信号的种类
kill -l命令查看系统的各个信号:

细节1: 信号的编号
仔细看的话,从1到64,没有32号和33号信号(历史原因,这里不讲),一共62个信号
普通信号
1号~31号
实时信号
34号~64号
实时信号的特点: 实时信号产生了必须立即处理,这里不学
细节2: 信号的名称
信号的名称的字母都是大写的,其实它们在Linux内核中被定义为宏,信号的名称的宏对应的数字就是信号的编号
x86平台下,Linux内核定义在/arch/x86/include/uapi/asm/signal.h中
*注: uapi这个目录下存储用户空间api头文件
#define SIGHUP 1 #define SIGINT 2 #define SIGQUIT 3 #define SIGILL 4 #define SIGTRAP 5 #define SIGABRT 6 #define SIGIOT 6 #define SIGBUS 7 #define SIGFPE 8 #define SIGKILL 9 #define SIGUSR1 10 #define SIGSEGV 11 #define SIGUSR2 12 #define SIGPIPE 13 #define SIGALRM 14 #define SIGTERM 15 #define SIGSTKFLT 16 #define SIGCHLD 17 #define SIGCONT 18 #define SIGSTOP 19 #define SIGTSTP 20 #define SIGTTIN 21 #define SIGTTOU 22 #define SIGURG 23 #define SIGXCPU 24 #define SIGXFSZ 25 #define SIGVTALRM 26 #define SIGPROF 27 #define SIGWINCH 28 #define SIGIO 29 #define SIGPOLL SIGIO /* #define SIGLOST 29 */ #define SIGPWR 30 #define SIGSYS 31 #define SIGUNUSED 31 /* These should not be considered constants from userland. */ #define SIGRTMIN 32 #define SIGRTMAX _NSIG4.形象理解信号的处理方式
操作系统为进程发送信号,进程需要处理信号,只有3种方式,而且只能3选1
默认动作: 手机响了,接听电话(这是大多数人默认的动作)
从上面的实验的运行结果,可以得出: 进程收到2号信号的默认动作,就是终止自己
忽略: 手机响了,选择静音,不接听,忽略这个电话
自定义动作: 手机响了,但不想接,于是挂断电话并回复短信(自定义动作)
自定义动作被称为信号的捕获(或捕捉),对于bash而言,不能执行2号信号的默认动作,需要捕获该信号做进一步处理,否则影响用户体验
下面测试信号的捕捉
5.测试信号的捕捉
验证Ctrl+C向进程发送了2号信号,那么就要使用自定义方法捕获2号信号,这里使用signal系统调用
signal系统调用

signal是修改特定进程对于信号的处理动作的
signum: 信号的编号
handler: 函数指针,手册给出:
typedef void (*sighandler_t)(int);sighandler_t为函数指针类型,这个函数执行捕获信号后的自定义动作,其返回值为void,只接受一个int类型的信号编号
为什么myhandler需要传参?
答: 区分不同的信号,做不同的事情,例如:
void myhandler(int signo) { if (signo==1) { //...... } else if (signo==2) { //...... } else if (...) //...... } int main() { signal(1,my_hander); signal(2,my_hander); //...... }test_signal.cpp:无限循环正常执行,只不过用户键入Ctrl+C后需要执行自定义动作
#include <unistd.h> #include <iostream> #include <signal.h> void myhandler(int signo) { std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl; } int main() { signal(SIGINT,myhandler); for (;;) { std::cout<<"Hello World!"<<std::endl; sleep(1); } return 0; }注: 1.signal(SIGINT,my_hander);只需要设置一次,后面都有效
2.只有收到2号信号时(例如Ctrl+C或kill -2 pid),myhandler才会调用,调用signal不会触发myhandler的执行
运行结果:test_signal.out进程遇到2号信号不会执行默认动作,而是执行打印任务

反思: 能否捕获所有信号?
如果将系统中所有信号全部捕捉,执行自定义动作,例如自定义动作为无限循环,那么是不是test_signal.out进程无法被杀死?
这里测试1~31号普通信号:
#include <unistd.h> #include <iostream> #include <signal.h> #include <sys/types.h> void myhandler(int signo) { std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl; } int main() { for (int i=1;i<=31;i++) signal(i,myhandler); std::cout<<"本进程的pid为: "<<getpid()<<std::endl; for (;;) { std::cout<<"Hello World!"<<std::endl; sleep(1); } return 0; }启动一个脚本,批量向该进程发送信号:
send_signal.sh写入:
#!/usr/bin/bash if [ "$1" = "" ] || [ "$1" = " " ] then echo "需要提供进程的pid,用法: ./send_signal.sh pid" exit -1 fi pid=$1 for ((i=1; i<=31; i++)) do kill -${i} ${pid} sleep 0.2 done运行结果: 9号信号SIGKILL无法捕获

从10号信号开始:
#!/usr/bin/bash if [ "$1" = "" ] || [ "$1" = " " ] then echo "需要提供进程的pid,用法: ./send_signal.sh pid" exit -1 fi pid=$1 for ((i=10; i<=31; i++)) do kill -${i} ${pid} sleep 0.2 done运行结果: 19号信号SIGSTOP无法捕获

从20号信号开始:
#!/usr/bin/bash if [ "$1" = "" ] || [ "$1" = " " ] then echo "需要提供进程的pid,用法: ./send_signal.sh pid" exit -1 fi pid=$1 for ((i=20; i<=31; i++)) do kill -${i} ${pid} sleep 0.2 done运行结果: 20~31号信号可全部被捕获

结论: 1~31号普通信号,只有9号信号SIGKILL、19号信号SIGSTOP无法捕获
反思: 为什么进程无法捕获9和19号信号?
如果进程捕获了所有信号但是不退出,这样的进程十分危险,它一直占用操作系统的资源,而且可能执行一些危险的动作
为了避免这样的事情发生,Linux中设置: 9和19号信号无法捕获(可以理解为这两个信号是操作系统留的"后门"),分别用于强制终止进程和强制暂停进程,否则系统将彻底失去对进程的最终控制权
结论: 内核必须在任何情况下都能无条件终止或停止进程,以保证系统的可控性和稳定性
Linux内核源码验证: 为什么进程无法捕获9和19号信号
linux 6.18.6在/kernel/signal.c中定义:
static bool sig_task_ignored(struct task_struct *t, int sig, bool force) { void __user *handler; handler = sig_handler(t, sig); /* SIGKILL and SIGSTOP may not be sent to the global init */ if (unlikely(is_global_init(t) && sig_kernel_only(sig))) return true; if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) && handler == SIG_DFL && !(force && sig_kernel_only(sig))) return true; /* Only allow kernel generated signals to this kthread */ if (unlikely((t->flags & PF_KTHREAD) && (handler == SIG_KTHREAD_KERNEL) && !force)) return true; return sig_handler_ignored(handler, sig); } 注释明确说明:
/* SIGKILL and SIGSTOP may not be sent to the global init */sig_kernel_only在include/linux/signal.h中定义:
#define sigmask(sig) (1UL << ((sig) - 1)) #if SIGRTMIN > BITS_PER_LONG #define rt_sigmask(sig) (1ULL << ((sig)-1)) #else #define rt_sigmask(sig) sigmask(sig) #endif #define SIG_KERNEL_ONLY_MASK (\ rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP)) #define siginmask(sig, mask) \ ((sig) > 0 && (sig) < SIGRTMIN && (rt_sigmask(sig) & (mask))) #define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)x86下,SIGRTMIN在/arch/x86/include/uapi/asm/signal.h中定义:
#define SIGRTMIN 32BITS_PER_LONG在/include/asm-generic/bitsperlong.h中定义,和具体架构有关:
#ifdef CONFIG_64BIT #define BITS_PER_LONG 64 #else #define BITS_PER_LONG 32 #endif /* CONFIG_64BIT */Intel 32位下:
sig_kernel_only(sig) == siginmask(sig, SIG_KERNEL_ONLY_MASK) == siginmask(sig, (rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))) == siginmask(sig, ((1ULL << ((SIGKILL)-1)) | (1ULL << ((SIGSTOP)-1))))