🔥博客主页: 我要成为C++领域大神
🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】
❤️感谢大家点赞👍收藏⭐评论✍️本博客致力于知识分享,与更多的人进行学习交流
如何触发信号
信号是Linux下的经典技术,一般操作系统利用信号杀死违规进程,典型进程干预手段,信号除了杀死进程外也可以挂起进程
kill -l
查看系统支持的信号
32,33号信号预留给线程库NTPL
两种信号,经典信号/实时信号
1-31是unix经典信号,软件开发工程师使用,例如进程通信,信号捕捉等。
34-64是自定义信号,一般驱动开发使用,偏底层。
1、终端组合按键触发信号
Ctrl+/(SIGQUIT/3)
系统向唯一的前台进程发送2号信号,目标进程被杀死
Ctrl+C(SIGINT/2)
系统向唯一的前台进程发送2号信号,目标进程被杀死
Ctrl+Z(SIGTSTP/20)
系统向唯一的前台进程发送20号信号,目标进程被挂起
终端组合按键触发的信号会发给唯一的前台进程
写一个无法退出的死循环,使用组合键杀死这个进程
按下Ctrl+C成功杀死这个死循环进程
2、命令发送信号
kill -signo pid
此命令可以向任意进程发送任意信号
kill命令成功杀死进程
3、函数发送信号
使用信号函数需要包含头文件 signal.h
常见信号函数:
kill(pid_t,int signo)
向任意进程发送任意信号
raise(int signo)
向自身进程发送任意信号
void abort(void)
向自身发送SIGABRT/6信号
关于kill函数的使用:
kill函数的第一个参数是进程ID,第二个参数是信号编号。这与kill命令正好相反,所以为了符合使用逻辑,在传入命令行参数时,将命令行第二个参数作为kill
函数的第一个参数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>
int main(int argc,char **argv)
{
if(argc<3){
printf("参数数量错误\n");
exit(0);
}
kill(atoi(argv[2]),atoi(argv[1]));
return 0;
}
自定义kill
命令杀死进程
4、硬件异常产生信号
1)对只读内存进行写操作,属于违规操作硬件,系统向违规进程发送SIGSEGV(11)信号,段错误,杀死违规进程。
例如:当我们尝试修改常量区内存时:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
int main()
{
char *str="this is const";
str[1]='d';
return 0;
}
2)SIGBUS(总线错误),越界访问,无效访问内存,系统向违规进程发送SIGBUS(7)信号,杀死违规进程。
3)SIGFPE(浮点数例外),CPU违规运算,运算异常,系统向违规进程发送SIGFPE(8),杀死进程。
5、软条件触发信号
软条件触发信号(Soft Condition Trigger Signal),指的是某些条件满足时触发相应的处理操作。例如,文件描述符变为可读时触发读操作;设置一个定时程序,定时器结束后触发操作;管道读端结束,写端向管道写数据(触发软条件),系统向写端进程发送SIGPIPE13信号杀死写端进程。
信号的三大行为,与五种默认处理动作
默认行为
信号处置进程后,可以通过结果分析信号的默认动作。
默认处理动作:
TERM:直接杀死目标进程,SIGKILL,SIGINT
CORE:直接杀死进程,但是转储核心处理文件(dump core),SIGQUIT,SIGSEGV SIGFPE,SIGBUS
IGN:通知回收信号 SIGCHLD(唯一的忽略信号),忽略信号,发到进程不会影响进程
STOP:挂起进程SIGSTP,SIGTSTP
CONT:唤醒进程,SIGCONT
忽略行为
忽略行为没有处理动作,直接丢弃,不会影响进程。忽略行为的优先级比动作要高。
捕捉行为
捕捉行为可以实现,信号绑定自定义任务。信号触发,执行捕捉函数,执行自定义任务。捕捉技术在开发中普遍使用,例如Qt的信号与槽机制。
Dump Core
如果进程因为硬件异常被系统杀死,那么会(Dump Core),错误原因 xxx(核心已转储)
例如:对常量空间的非法访问。
Dumb Cor中存放的是错误信息,但是系统不会生产core文件,我们可以进行修改:
ulimit -a
查看系统限制
ulimit -c 4096
生成一个块大小的core,让进程产生的错误生成core文件
后续调试可以通过gdp ./可执行文件名 core
快速定位错误位置,无需逐步调试,节省编译时间
让信号失效的三种方式:屏蔽、忽略、捕捉
屏蔽(Block):延迟处理信号,直到信号解除屏蔽。
忽略(Ignore):完全忽略信号,不对信号作出任何响应。
捕捉(Catch):使用自定义的信号处理程序响应信号。
1. 屏蔽(Block)
屏蔽信号指的是将信号加入到进程的信号屏蔽集(blocked signal set)中。当信号被屏蔽时,即使信号被发送给进程,它也不会立即处理该信号,而是将其放入未决信号集(pending signal set),直到信号被解除屏蔽时才会处理。
2. 忽略(Ignore)
忽略信号指的是将信号处理程序设置为SIG_IGN
,使得进程完全忽略某个特定的信号。当信号被忽略时,进程不会对该信号作出任何响应。
3. 捕捉(Catch)
捕捉信号指的是将信号处理程序设置为自定义函数,当信号到达时执行该函数。捕捉信号允许进程在信号到达时执行特定的操作而不会执行本来的功能。
如果所有的信号都被屏蔽、忽略、捕捉,那么不就会导致病毒无处可寻吗?
系统保留高权级信号,这类信号无法被屏蔽,捕捉和忽略,服务于内核,只要发出必然抵达。
SIGKILL(9)
,无法被屏蔽、捕捉、忽略,只要发出必然杀死
SIGSTOP(19)
,无法被屏蔽,捕捉忽略,只要发出必然挂起
信号的传递过程
当某个事件发生(例如用户按下Ctrl+C
),内核会生成一个SIGINT
信号,传递给目标进程虚拟内存中3-4G内核层的PCB,PCB中存有信号处理信息。同时更新目标进程的PCB中的未决信号集。如果发送信号的行为是屏蔽,那么也会更新目标进程PCB中的屏蔽信号集,将对应信号位设置1, 可以实现阻塞信号的效果。当屏蔽解除或者信号不被屏蔽时,未决信号集中的信号将被处理。处理过程是由信号处理程序(Handler)来执行的。
未决信号集:是一个位图,每一位代表一个特定的信号。位值为1表示该信号是未决的。
当内核发送一个信号给进程时,它会将对应信号的位设置为1,表示此信号正在传递,还未处理。
屏蔽信号集:用于表示哪些信号当前被屏蔽(阻塞)。每个信号对应屏蔽信号集中的一位,位值为1表示该信号被屏蔽。
未决信号集和屏蔽信号集处理UNIX经典信号不支持信号排队处理。(因为一个信号即可达到目的)
自定义信号可以实现排队序列,多个相同信号触发也可以排队依次处理。(例如各种家电的遥控器,当连续按下多个按键时,可以连续执行。还有音乐播放器的切歌功能)
信号屏蔽的实现
#include <signal.h>
sigset_t
是一个数据类型,用于表示信号集。
sigemptyset(sigset_t set);
初始化信号集set
,将其设置为空,即不包含任何信号。
sigfillset(sigset_t set);
初始化信号集set
,将其设置为满,即包含所有信号。
sigaddset(sigset_t set, int signo);
将信号signo
添加到信号集set
中。
sigdelset(sigset_t set, int signo);
从信号集set
中删除信号signo
。
int sigismember(const sigset_t set, int signo);
检查信号signo
是否是信号集set
的成员。如果是,返回 1;否则,返回 0。
sigprocmask(int how, const sigset_t newset, sigset_t oldset);
检查并更改当前线程的信号屏蔽字。
how
可以是:
SIG_SETMASK
(将信号屏蔽字设置为 newset
)
SIG_BLOCK
(将 newset
中的信号添加到当前信号屏蔽字)
SIG_UNBLOCK
(从当前信号屏蔽字中移除 newset
中的信号)
下面通过一个demo程序屏蔽SIGINT
信号:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>
int main(void)
{
sigset_t set,oldset;
sigemptyset(&set);//将信号屏蔽集全部置0
sigaddset(&set,SIGINT);//将SIGINT加入屏蔽集
sigprocmask(SIG_SETMASK,&set,&oldset);
while(1) sleep(1);
return 0;
}
结果:无论是哪种方式发送信号都无法实现
再添加SIGQUIT
和SIGKILL
为屏蔽信号
sigaddset(&set,SIGQUIT);//将SIGQUIT加入屏蔽集
sigaddset(&set,SIGKILL);//将SIGKILL加入屏蔽集(高优先级信号无法屏蔽)
SIGKILL
无法被屏蔽,因为是高优先级信号
查看信号的屏蔽情况
信号已经被发出,抵达进程,进程中被屏蔽,要观察这种已触发被屏蔽的信号集只能查看未决信号集(只有读的权限)
获取进程的未决信号集,而后输出未决的每一位,0 or 1,查看信号屏蔽
sigpending(&pset)
调用这个函数,系统会将进程的未决信号集传出到pset中
使用遍历循环结合sigismember
,查看每一位的情况并输出。
查看未决信号集的demo程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>
//通过打印未决信号集,查看被屏蔽的信号
void print_sigpending(sigset_t pset){
int i=1;//信号是从1开始的
for(i;i<32;++i){
if(sigismember(&pset,i)){
putchar('1');
}else
putchar('0');
}
putchar('\n');
}
int main(void)
{
sigset_t set,oldset,pset;
sigemptyset(&set);//将信号屏蔽集全部置0
sigaddset(&set,SIGINT);//将SIGINT加入屏蔽集
sigaddset(&set,SIGQUIT);//将SIGQUIT加入屏蔽集
sigprocmask(SIG_SETMASK,&set,&oldset);
while(1){
sigpending(&pset);
print_sigpending(pset);
sleep(1);
}
return 0;
}
运行结果:
当键盘按下组合键发送SIGINT
和SIGQUIT
后,未决信号集被置1
信号行为修改
使用struct sigaction
结构体对行为进行操作:
结构体成员:
act.sa_handler
:
可以设置为: SIG_DFL
(默认信号处理方式)SIG_IGN
(忽略信号)。也可以指向自定义信号处理函数的指针
act.sa_flags
:
标志位,用于控制信号处理行为。如果使用 sa_sigaction
处理函数,则将 flags
设置为 SA_SIGINFO
。
常用值: 0
(默认,不使用附加选项)
act.sa_mask
:
类型为 sigset_t
的信号集。用于在信号处理期间临时阻塞的信号。通常使用 sigemptyset
初始化。
函数 sigaction
:用 newact
替换进程的信号处理行为,并将原有的信号处理行为保存在 oldact
中。
sigaction(int signo, struct sigaction *newact, struct sigaction *oldact);
signo
:要处理的信号编号。
newact
:指向包含新信号处理方式的 struct sigaction
结构体的指针。
oldact
:指向用于保存原有信号处理方式的 struct sigaction
结构体的指针(可以为 NULL
)。
下面写一个设置自定义的信号处理demo程序:当收到 SIGINT
信号时,会调用自定义信号捕捉函数SIG_CATCH
并打印收到的信号编号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>
//act.sa_handler的类型是void (*fun)(int)
//是一个自定义函数指针类型,自定义的捕捉函数也要是这种类型
void SIG_CATCH(int signo){
printf("SIGINT %d 捕捉信号\n",signo);
}
int main()
{
struct sigaction act,oldact;
act.sa_handler=SIG_CATCH;//自定义的函数,用来输出捕捉到的信号编号
act.sa_flags=0;
sigemptyset(&act.sa_mask);//信号集类型
sigaction(SIGINT,&act,&oldact);//替换信号行为成自定义的捕捉行为,并且将原来的行为保存到oldact中
while(1)
sleep(1);
return 0;
}
当前进程收到SIGINT
信号,触发捕捉函数
经典信号临时屏蔽
经典信号临时屏蔽是指在信号处理函数执行期间,临时阻塞某些信号,以避免这些信号在处理当前信号时再次被递送。 两个相同信号触发,可以最大排队一次。
在信号处理的上下文中,当信号被捕捉时,同一信号在默认情况下最多只能排队一次。
如果允许任意数量的信号排队,系统资源的消耗可能会显著增加。这种资源限制有助于系统避免因信号排队导致的资源枯竭。
信号排队行为
在这段代码中,如果按下 Ctrl+C
发送 SIGINT
信号,信号处理程序会执行,并在执行期间再次按下 Ctrl+C
发送的 SIGINT
信号将不会被排队处理,只能在当前信号处理完成后再处理一次。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>
//act.sa_handler的类型是void (*fun)(int)
//是一个自定义函数指针类型,自定义的捕捉函数也要是这种类型
void SIG_CATCH(int signo){
int flag=2;
while(flag--){
printf(" flag%d\n",flag);
sleep(1);
}
}
int main()
{
struct sigaction act,oldact;
act.sa_handler=SIG_CATCH;//自定义的函数,用来输出捕捉到的信号编号
act.sa_flags=0;
sigemptyset(&act.sa_mask);//信号集类型
sigaction(SIGINT,&act,&oldact);//替换信号行为成自定义的捕捉行为,并且将原来的行为保存到oldact中
while(1)
sleep(1);
return 0;
}
不管我们发送多少次SIGINT
信号,也最多触发两次
捕捉函数的冲突
为了避免不同信号绑定相同的捕捉函数,引发冲突。
在进程处理某一个信号时,可以使用sa_mask
临时屏蔽其他信号,等信号处理完再解除屏蔽,避免不同信号调用相同的捕捉冲突。
使用信号回收僵尸进程
使用wait
和waitpid
回收僵尸进程的操作都是主动回收,无论是阻塞回收还是非阻塞回收都会花费大量的时间片和资源。
操作系统在每次子进程结束后,都会发送SIGCHLD
信号给父进程,这个信号默认处理行为会忽略,起到通知父进程的作用。那么我们就可以使用捕捉技术,绑定SIGCHLD
信号与回收函数。当发送SIGCHLD信号后,自动调用回收函数,杀死僵尸进程。
下面是实现这一机制的demo程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>
#include <sys/wait.h>
void SIG_KILLZOMB(int signo){
int zpid;
while((zpid=waitpid(-1,NULL,WNOHANG))>0){
printf("已成功回收一个进程\n");
}
}
int main()
{
struct sigaction act,oldact;
act.sa_handler=SIG_KILLZOMB;//自定义的回收捕捉函数
act.sa_flags=0;
sigemptyset(&act.sa_mask);//将信号集类型全部置0
sigaction(SIGCHLD,&act,&oldact);//替换捕捉行为为自定义的函数,并且将原来的函数保存到oldset中
pid_t pid;
pid=fork();
if(pid>0){
printf("Parent PID:%d is working\n",getpid());
while(1) sleep(1);
}else if(pid==0){
printf("Child PID:%d is Exiting\n",getpid());
exit(0);
}
return 0;
}
捕捉函数实现的流程
main函数先执行,执行过程中产生信号,系统执行捕捉函数,捕捉函数执行完,稍后回到主函数继续执行
内核切换到用户层执行捕捉函数,使用进程本身资源(时间片或内存)
1、进程在用户层执行特定指令
2、系统发出信号到内核层(等待处理)
当系统中某个事件(如硬件中断、软件异常等)发生时,会产生一个信号,这个信号会被传递到内核层进行处理。
3、进程状态发生转换,从用户空间切换到内核空间:
用户进程的执行发生中断,CPU 从用户空间切换到内核空间,以便处理信号。
这种转换通常是通过系统调用中断指令实现的。
4、切换到内核层后完成调用:
内核捕获信号后,根据信号的类型和处理程序完成相应的处理。内核找到对应的信号处理函数,并准备调用它。
5、完成调用后,返回用户空间,检测是否有未处理的信号,有则处理:
内核处理完信号后,恢复进程的执行上下文,并返回用户空间。返回用户空间前,内核会检查是否有未处理的信号,如果有则继续处理。
6、如果信号的处理行为为捕捉行为,调用用户层捕捉函数,携高权限切换到用户层:
如果信号有用户定义的处理函数,内核会将信号处理行为转换为调用用户层的捕捉函数。内核会提升权限,切换到用户空间以执行用户层的捕捉函数。
7、捕捉到信号:
捕捉函数执行具体的信号处理逻辑。
8、系统调用完毕,返回用户空间:
捕捉函数执行完毕后,系统调用
sigreturn
指令返回内核空间,恢复进程的执行上下文。
9、从 main
被中断的位置继续执行:
最后,进程从
main
函数中断的位置继续执行。
到底什么是内核层与用户层?
内核层与用户层就是不同级别的CPU访问权限。
可以参考 Intel单核处理器的特权级别(Privilege Levels),也称为保护环(Protection Rings)。
这种模式在操作系统中也存在,用于管理和限制不同层次的软件对硬件资源的访问权限。防止低权限的代码(如用户进程)直接访问高权限的资源(如操作系统内核),减少系统被恶意攻击。
Level 0(环0):
特权级别:最高级别的CPU权限,内核层(Kernel Mode)。
访问权限:可以访问所有的硬件资源和执行所有的CPU指令。
操作系统内核运行在这一层,包括设备驱动程序、硬件抽象层等关键系统组件。
Level 3(环3):
特权级别:最低级别的CPU权限,用户层(User Mode)。
访问权限:受限,无法直接访问大多数硬件资源,只能通过系统调用访问操作系统提供的服务。
应用程序和用户进程运行在这一层,以确保系统的稳定性和安全性。
捕捉函数的可重入和不可重入
捕捉函数的可重入性(reentrancy)和不可重入性(non-reentrancy)是指函数在被中断后重新进入执行时的行为特性。
可重入函数
可重入函数指的是能够在被中断后,安全地再次被调用的函数。即使在另一个调用还没有完成的情况下,也不会出现数据不一致或其他问题。
int add(int a, int b) {
return a + b;
}
add
函数是可重入的,因为它不依赖于任何共享状态或资源。
不可重入函数
不可重入函数是指在被中断后,再次调用可能会导致数据不一致或程序崩溃,通常因为它们依赖于静态或全局状态。
char* get_message() {
static char message[] = "Hello, World!";
return message;
}
get_message
函数是不可重入的,因为它返回了一个指向静态变量的指针。如果该函数在中断过程中被重新调用,会导致多个调用共享同一个静态变量,可能会引发数据不一致的问题。
捕捉函数中的可重入和不可重入
在信号处理(捕捉函数)过程中,假设信号处理程序调用了一个不可重入函数,而这个函数在被信号中断之前已经在运行,这时重新进入这个函数可能会导致程序崩溃或数据损坏。
例如在链表中间插入节点,插入操作进行一般时,调用了捕捉函数,主函数中断。而在捕捉函数中插入了S1节点,捕捉函数处理完毕后,M1节点的next指向了end。
虽然这种情况不会影响对链表的遍历,但是会产生垃圾节点和数据。
在编写信号处理程序时,应尽量使用可重入函数,避免使用不可重入的函数或在信号处理程序中调用不可重入的函数。