在 Linux 系统中,信号(Signals) 是一种进程间通信(IPC)机制,用于通知进程发生了某种事件或请求进程执行特定操作。
- 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
- 信号产⽣之后,进程知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗? 知道。所以,信号的处理方法,在信号产⽣之前,已经准备好了。
- 处理信号,立即处理吗?进程可能正在做优先级更高的事情,可能不会立即处理?什么时候?合适的时候。
- 信号到来 | 信号保存 | 信号处理
- 怎么进行信号处理啊?a.默认行为 b.忽略信号 c.⾃定义行为, 后续都叫做信号捕捉。
前台进程和后台进程
./precess 在前台启动的前台进程 关闭方法:ctrl+c 杀死前台进程
./process & 在后台启动的后台进程
两种关闭方法: 1.再启动一个窗口 ,查询process的pid ,使用kill -9 pid杀死进程process
2.首先启动时使用 nohup ./process & 是将process的输出写在在nohup.out文件中,但也是后台启动
之后使用 fg 1 (1 是process的作业号)是将process改为前台进程
最后使用ctrl +c
上面的ctrl+c 实际上是OS发送2号信号SIGINT给进程process ,2号信号是终止进程
ctrl+\ 是发送3号信号SIGQUIT ,也是终止进程
查看信号
kill -l //查看linux中的常见信号
1号信号到32号信号都是普通信号(我们要学的)
查看信号的具体作用
man 7 signal //查看信号手册
其中Core和Term都是退出
Core VS Trem
Trem是正常终止进程(允许进程自行清理)
优雅终止(graceful termination):通知进程自行清理资源后退出。
Core被称为核心转储,终止进程的同时生成了 core dump 文件(记录进程崩溃时的内存状态,用于后续调试)
但是云服务器默认关闭了这个功能 ,需要系统配置才能打开此功能。
打开Core file的方法
前面进程等待时,子进程的退出时的退出信息,[7]就是core dump标志
如果子进程退出是因为执行了core且core被打开 ,这个标志位会被置为1
使用Core file
当代码量太多了,找不到哪里出了问题 core file 可以帮助我们调试程序崩溃原因
使用Core file过程
1.先将core file文件打开
2.编译可执行程序时加上 -g
3.运行可执行程序,结果崩了
4.使用gdb调试
gdb 可执行程序名称
5.在gdb中 输入 core-file core文件名
信号捕捉和更改信号的处理动作
信号也可以被捕捉,使用系统调用signal
signal只需要设置一次 ,但凡有对应的信号输入 就会被捕捉到.
捕捉到对应信号后 , 会执行第二个参数的方法,相当于改变了原先信号的执行方法
31个信号中 ,有几个信号无法被捕捉 如:9号信号
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:信号编号
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法
signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调⽤!!
前台进程在运⾏过程中⽤⼾随时可能按下 Ctrl-C ⽽产⽣⼀个信号,也就是说该进程的⽤ ⼾空间代码执⾏到任何地⽅都有可能收到 SIGINT 信号⽽终⽌,所以信号相对于进程的控 制流程来说是异步(Asynchronous)的。
使用信号捕捉,验证上面ctrl+c OS是发送的信号是2号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
std::cout << "我是" << getpid() << "捕捉到信号:" << signum << std::endl;
}
int main()
{
::signal(2,handler); //signal设置一次即可
while (true)
{
std::cout << "hello linux signal" << std::endl;
sleep(1);
}
return 0;
}
信号的发送和接受
- 一切信号都是由OS发送给进程 因为OS是进程的唯一管理者
- 信号的接受方都是进程
- 常见信号有31个 ,进程的PCB中有一个位图,进程收到n号信号就将第n位位图由0置1 ,且进程还有一个信号函数指针数组,
- 两者对应关系: 第n信号的执行方法 = 信号函数指针数组[n-1]
信号的产生
1.键盘输入产生信号
例如:ctrl+c 就是从键盘产生2号信号 ,OS将信号发送给进程
问题: 但是OS怎么知道键盘输入数据了?
硬件中断
硬件中断(Hardware Interrupt) 是由硬件设备触发、通过硬件电路实现的机制,但其处理过程需要软件(操作系统内核)配合
硬件中断: 键盘被按下后 ,键盘发送了硬件中断给CPU ,CPU收到后会通知OS向键盘拷贝键盘数据
2.系统指令产生信号
kill -9 pid //产生了9号信号
3.系统调用产生信号
能发信号的系统调用 kill raise abort
kill不仅仅是指令 ,也是系统调用
raise 给调用此系统调用的进程发信号 相当于 kill (getpid() , signo)
abort 给进程发送6号信号 相当于 kill(getpid() ,6)
abort()
函数会向当前调用进程发送SIGABRT
信号(6号信号),默认行为是终止进程并生成核心转储(core dump)
4.软件条件产生信号
eg: 管道读端关闭 ,就产生13号信号 ,由OS发送给写端进程,杀死写端进程
eg: alarm 闹钟时间到了 ,OS发送14号进程给调用alarm的程序
alarm()
是 Linux/Unix 系统中的一个系统调用,用于设置一个定时器,在指定的秒数后向当前进程发送SIGALRM
(14号信号)。默认情况下,SIGALRM
会终止进程,但可以通过信号处理函数自定义行为。返回值:
返回之前定时器的剩余时间 ,响了返回0,没响返回剩余几秒
alarm(0)是取消闹钟 ,返回值是剩余几秒
#include <iostream>
#include <unistd.h>
#include <signal.h>
int number = 0;
void die(int signumber)
{
printf("get a sig : %d, count : %d\n", signumber, number);
exit(0);
}
int main()
{
// 统计我的服务器1S可以将计数器累加多少!
alarm(1); // 我自己,会在1S之后收到一个SIGALRM信号
signal(SIGALRM, die);
while (true)
{
number++;
}
}
执行结果
取消闹钟 alarm(0)
#include <iostream>
#include <unistd.h>
#include <signal.h>
int number = 0;
void die(int signumber)
{
printf("get a sig : %d, count : %d\n", signumber, number);
exit(0);
}
int main()
{
// 统计我的服务器1S可以将计数器累加多少!
alarm(10); // 我自己,会在10S之后收到一个SIGALRM信号
sleep(4);
int n = alarm(0); // 0:取消闹钟
std::cout << "n : " << n << std::endl;
signal(SIGALRM, die);
while (true)
{
number++;
}
}
这里已经取消闹钟,进程不会收到14号信号,会一直执行
5.异常产生信号
野指针访问 /=0操作 都会引发信号的产生
下面演示/=0导致异常发送信号
#include <iostream> #include <unistd.h> #include <signal.h> #include<vector> #include<functional> void handler(int signum) { std::cout << "我是" << getpid() << "捕捉到信号:" << signum << std::endl; } int main() { signal(8 ,handler); int a =10; a/= 0; while(true); }
这个执行结果为什么是死循环?
上面的代码,将8号信号捕捉了,自定义的行为也没有退出进程 ,进程还在继续 ,为什么一直打印呢?
因为有众多其他进程等着执行 ,此进程时间片结束后OS将此进程在cpu寄存器上的信息记录切换其它进程 ,等OS把这个进程切回来一看 ,cpu中的寄存器还是溢出状态就还发送8号信号,一直如此
信号保存
知识补充:
信号捕捉到后三种处理结果
- signal(signum ,SIG_DFL) 使用信号默认行为 不用signal捕捉 ,信号就是默认行为
- signal(signum ,SIG_IGN) 使用忽略信号行为 捕捉到信号,忽略信号(什么也不做)
- signal(signum ,handler ) 使用自定义行为
SIG_DFL SIG_IGN是宏
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态(也就是产生了还未实现),称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 阻塞也可以称为屏蔽
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞 , 才执行递达的动作.(信号产生 ,就会将信号保存(pending),如果信号被阻塞 ,就不会递达,直到阻塞解除)
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的⼀种处理动作。
在内核中的表示
信号在内核中的示意图
进程的PCB中有三张表(位图)
三个位图的比特位的位置对应的就是信号的编号(暂时只关心1 到31号信号)
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有⼀个函数指针表示处理动作。
block表: 信号是否阻塞表 1/0 代表该信号是/否阻塞
pending表:信号是否保存表 1/0 代表进程是/否收到该信号
handler表: 信号函数指针表 1/0 代表该信号的函数指针
signal本质就是改变了进程中handler表中的函数指针(SIG_DFL 或 SIG_IGN 或 自定义函数指针) 例如:signal(2 ,handler)
- 信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上 图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
- SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤⼾⾃定义函数 sighandler。
如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?
POSIX.1允许系统递送该信号⼀次或多次。
Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。这里不讨论实时信号。
这张图是linux内核的代码 ,从中我们可以看出进程的PCB中关于信号的三张表最底层都是sigset_t
sigset_t(信号集)
从上图来看,每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产⽣了多少次,阻塞标志也是这样表示的。
因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储 ,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号 是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask) 这⾥的“屏蔽” 应该理解为阻塞⽽不是忽略。
信号集操作函数(对sigset_t进行增删查改)
上面的三张表属于进程的内核数据结构,OS不允许用户直接使用,OS提供了系统调用接口用来访问,但是我们发现三张表本质都是信号集,我们想改变三张表,需要先使用信号集操作函数将我们想要的信号集创建 ,再使用修改三张表的sys call去改变三张表
(原因是三张表的sys call函数参数需要传sigset_t ,是将表中的sigset_t 整体改变,我们要先将sigset_t设置好)
构建sigset_t 的函数
sigemptyset 功能:初始化一个空的信号集(清空所有信号)
sigset_t set; sigemptyset(&set); // 清空set,不包含任何信号
sigfillset 功能:初始化一个包含所有信号的信号集(填充所有支持的信号)
sigset_t set; sigfillset(&set); // set包含所有系统支持的信号
sigaddset 功能:向信号集中添加一个指定的信号
sigaddset(&set, SIGINT); // 将SIGINT添加到set中
sigdelset 功能:从信号集中删除一个指定的信号。
sigismember 功能:检查某个信号是否在信号集中。
返回值:
1
:信号在集合中。0
:信号不在集合中。-1
:错误(如无效信号)。
注意:在使⽤sigset_t类型的变量之前,⼀定要调⽤sigemptyset或sigfillset做初始化,使信号集处于 确定的状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删 除某种有效信号。
更改进程关于信号的内核数据结构(三张表)的函数
sigprocmask 读取或更改进程的block表
调⽤函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
假设当前的信号屏蔽字(block表)为mask,下表说明了 how参数的可选值。
set是我们创建好的sigset_t oldset是老的sigset_t
如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀ 个信号递达。
sigpending 获取进程的pending表
读取当前进程的未决信号集,通过set参数传出。 调⽤成功则返回0,出错则返回-1
set为输出型参数