文章目录 信号入门什么是linux信号?信号处理的常见方式查看系统定义的信号列表 产生信号通过终端按键产生信号调用系统函数向进程发送信号由软件条件产生信号硬件异常产生信号 阻塞信号阻塞
我们输入命令,在shell下启动一个进程迎来循环打印一个字符串。
int main(){while (1){printf("i am a process,i am waiting signal!\n");sleep(1);}return 0;}
我们可以使用kill -2 命令终止该进程。
我们可以通过kill -l命令查看linux中定义的信号列表,其中,1 - 31号信号为普通信号,34 - 64号信号为实时信号。
当然,我们可以使用 man 7 signal 查看各个信号的默认处理行为。
[yzh@yzh test1]$ kill -l
当我们执行以下死循环程序,我们可以通过CTRL + C来终止该进程。
int main(){while (1){printf("i am a process,i am waiting signal!\n");sleep(1);}return 0;}
信号时如何被进程保存?
如果一个进程接受到该信号,那么该信号是保存在该进程的PCB(进程控制块)中的信号位图字段中。
其中,信号的位置代表普通信号的编号,比特位0 or 1 代表信号是否被保存。以上图示中代表进程保存了3号信号。
信号发送的本质?
当一个进程收到信号,本质上修改PCB(进程控制块)中指定的位图结构,进而完成发送信号的过程。
使用signal函数捕捉信号
sighandler_t signal(int signum, sighandler_t handler);
参数:
注意:
signal函数仅仅是修改进程特定信号的后续处理动作,不是直接调用对应的处理动作。
void catchSig( int signum ){ cout << "进程捕捉到了一个信号,正在处理中: " << signum << " pid: " << getpid() << endl;}int main(){ signal(SIGINT,catchSig); //捕捉2号信号 while( true ) { cout << "我是一个进程,我正在运行... Pid: " << getpid() << endl; sleep(1); } return 0;}
注意:
核心转储
在云服务器中,核心转储默认是关闭的,当时我们可以使用 ulimit -a 命令查看当前资源配置情况。
如果第一行中core文件的大小为0,代表该云服务器的核心转储是关闭的。
我们可以使用 ulimit - c 命令来设置核心转储文件的大小。
运行.signal可执行程序,使用 kill -8 PID 命令终止目标进程即可在当前路径下生成对应的core文件。
core dump标记位
我们知道大,对于waipid函数,status可以用于获取子进程的退出状态。其中status不能简单的以一个整形判断,status的比特位便代表着一些退出信息。
以下代码中,当子进程出现野指针问题,OS便会想子进程发送SIGFPE信号终止子进程,并且在core dump标志设置为1,留下core dump文件记录相关进程信息。
int main(){if (fork() == 0){//子进程cout << " 子进程正在运行 " << endl;int *p = NULL;*p = 100;exit(0);}//父进程int status = 0;waitpid(-1, &status, 0);cout << " coreDump:%d ",(status >> 7) & 1 << endl;return 0;}
结果如下:
利用核心转储进行调试
当进程出现异常的时候,OS会将当前进程在内存中的相关核心数据,转存在磁盘当中,也就是core文件,所以,我们可以利用core文件进行调试。
void catchSig( int signum ){ cout << "进程捕捉到了一个信号,正在处理中: " << signum << endl;}int main(){ signal(SIGINT,catchSig); //回调函数。 while( true ) { cout << "我是一个进程,我正在运行... Pid: " << getpid() << endl; int a = 100;a /= 0; //除0错误 sleep(1); } return 0;}
首先,我们可以使用 gdb 可执行文件命令文件进行调试,然后再通过core file 命令加载core文件,既可以判断该进程在终止时收到了8号信号,并且定位到了程序产生错误时的一段代码。
注意:
如果普通信号全部被捕捉,如何终止进程?
我们可以通过以下代码,将1 ~ 31号信号全部捕捉,如果收到其中信号,OS按理来说会执行我们所自定义的函数。
void handler(int signal){printf("get a signal:%d pid:%d ", signal,getpid());printf("\n");}int main(){int sign;for (sign = 1; sign <= 31; sign++){signal(sign, handler);}while (1){sleep(1);}return 0;}
当我们使用一系列kill命令终止目标进程,发现OS接收到信号之后执行用户定义的自定义函数而无法终止进程。但是,如果使用 kill -9 命令,即时9号命令被捕捉,OS还是执行系统默认处理动作,终止目标进程。
所以有些信号无法被捕捉,例如Linux中的9号信号,因为如果所有信号都可以被捕捉的话,那么操作系统也将无法被终止。
在Shell中,实际上kill命令也是在调用了kill函数实现的。
kill函数可以给一个指定的进程发送指定的信号。
返回值: 调用成功返回0,失败返回-1。
int kill(pid_t pid, int sig);
所以,我们可以通过kill函数实现一个简单的kill命令。
static void Usage( char* proc){cout << "USage %s : " << proc << endl;}int main(int arGC,char* argv[] ){if( argc != 3 ){Usage(argv[0]);exit(1);}pid_t pid = atoi(argv[1]);int signal = atoi(argv[2]);kill(pid,signal);return 0;}
结果如下:
模拟实现的”kill“命令将sleep进程成功终止。
raise
raise函数可以给当前进程发送指定的信号(自己给自己发送6号信号 )。
返回值: 成功返回0,失败返回-1。
int raise(int sig);
当我们使用signal将6号信号捕捉,调用abort函数时。
static void hander( int number ){cout << " get a signal " << number << endl;}int main(){ signal( 6,hander ); while( true ) {cout << "我开始运行了" << endl;sleep(1);abort(); // = raise(6) = kill(pid,6) } return 0; }
结果如下:
即使我们捕捉了6号信号,进程执行了我们自定义的函数,但是进程依旧被终止了。
注意:
abort的作用是使当前进程收到信号而异常终止,生成core dump文件,exit是让当前进程正常终止。
如何理解调用系统接口?
用户调用系统接口 --> OS执行OS对应的系统调用代码 --> OS提取参数,或者设置特定的数值 --> OS向目标进程写信号–> 修改对应进程PCB中位图的标记位–进程后续处理信号–> 执行对应的处理方法。
SIGPIPE
调用一个alarm函数可以设定一个闹钟,也就是告诉操作系统在seconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作为终止当前进程。
unsigned int alarm(unsigned int seconds);
返回值:
我们通过alarm闹钟设置1秒时count计算的总次数。
int main(){ alarm(1); int count = 0; while( true ) { cout <<" count: " << count++ << endl; } return 0; }
结果如下:
我们发现在通过vscode远程连接云服务器一秒钟计累加70163次。可是,实际上云服务器1秒钟累加次数远远大于该值,那么现在的结果远小于实际结果的原因是什么?
怎么单纯计算云服务器计算能力?
我们在通过ALARM函数设定1秒闹钟后,通过signal函数捕捉SIGALRM信号,即在收到SIGALRM信号之后进程将执行catchSig即获取1秒内count的累加值。
int count = 0;void catchSig( int signum ){ cout << "final count " << count << endl; }int main(){ alarm(1); signal( SIGALRM,catchSig ); while( true ) { count++; } return 0; }
结果如下:
此时云服务器中count累加大概为5亿次,并且alarm闹钟在触发一次就被移除了。
那么,如果我们想周期性1秒内计算count累加值,如何处理?
我们可以在自定义函中又设置一次alarm闹钟,进程执行完该函数后,又累加count,1秒后便再次触发闹钟执行该自定义函数,周期性不断循环。
long long int count = 0;void catchSig( int signum ){ cout << "final count " << count << endl; alarm(1);}int main(){ alarm(1); signal( SIGALRM,catchSig ); while( true ) { count++; } return 0; }
结果如下:
//定时器待定。
如何理解软件条件给进程发送信号
当我们对SIGFPE(8号信号)进行捕捉,当遇到除零错误时,进程执行自定义函数。
void handler( int signum ){ sleep(1); std::cout << "获得了一个2号信号: " << signum << std::endl; // exit(0); //应该及时退出。} int main( ){ signal(SIGFPE,handler); int a = 100; a /= 0; while(1) sleep(1); return 0;}
结果如下:
可是,进程为什么会循环执行handler自定义函数呢?
我们先来理解除0错误。
综合以上结论,进程之所以循环执行自定义函数,因为寄存器中的异常一直没有被解决,当当前进程执行时切换到其他进程时,当前进程的上下文是保存在寄存器中的。当寄存器又重新切到当前进程,又识别到异常错误,操作系统就会不断重复得发送8号信号。所以一般有该异常并捕捉之后,一般需要及时退出。
如何理解野指针或者越界问题?
综合以上情况,无论是硬件问题还是软件问题擦还是产生信号,所有信号都有它的来源,但最终都是由OS所识别,解释,并发送的。
sigset_t类型对于每种信号用于一个bit表示”有效“或者”无效“状态,至于在这个类型内部如何存储这些bit则依赖于系统的实现,从使用者的并不关心。使用者只能调用以下函数来操作sigset_t变量。
#include int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset (sigset_t *set, int signo);int sigdelset(sigset_t *set, int signo);int siGISmember(const sigset_t *set, int signo);
函数sigempty初始化set所指向的信号集,使其中所有的信号的对应bit位清零,表示该信号集不包含任何有效信号。
函数sigfilset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号的有效信号包括系统所支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
sigismember是一个布尔函数,用于判断一个信号集的有效符号是否包含某种有效信号,若包含则返回1,不包含则返回0,出错返回-1。这四个函数都是成功返回0,出错返回-1。
调用函数sigprocmask可以读取或更改进程的信号屏蔽子(阻塞信号集)。
#include int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
how | explantion |
---|---|
SIG_BLOCK | set包含了我们所希望添加到当前信号屏蔽字的信号,相当于mask = mask |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask =set |
sigpending函数用于读取进程的未决信号集,并通过set参数传出。
该函数调用成功返回0,出错返回-1。
int sigpending(sigset_t *set);
实践代码:
static void showPending( sigset_t &pending ) {for( int sig = 1; sig <= 31; ++sig ){if( sigismember(&pending,sig)) cout << "1";else cout << "0";}cout << endl;}int main(){ //定义信号集对象;sigset_t bset,obset; sigset_t pending; //用于保存信号集 //2.初始化sigemptyset( &bset );sigemptyset( &obset );sigemptyset( &pending );//3.添加要屏蔽的信号----2号信号 sigaddset(&bset,2); //其实只是在栈上作了修改。//4.设置set到内核中对应的进程内。(默认情况下进程不会对任何信号进行block) int n = sigprocmask( SIG_BLOCK,&bset,&obset ); assert( n == 0 ); (void)n; cout << "block 2 号信号成功 ...get: pid "<< getpid() << endl; while( true ){//5.1获取当前进程的pending信号集。sigpending( &pending);//5.2显示pending信号集。showPending( pending );、sleep(1); // signal( 2,handler ); } return 0;}
我们运行该可执行程序,如果该进程没有收到2号信号,那么penging信号集2号比特位就为0,当我们使用kill命令向该进程发送2号信号时,由于该进程的2号信号被屏蔽,所以该进程便一直处于pending(未决)状态。
如果我们想看到2号信号递达之后pending表的变化情况,我们设置20秒之后,将自动解除该进程的2号信号屏蔽状态,此时2号信号便会立即递达。并且我们再对2号进程进行捕捉。当处理2号信号时,进程会转而执行用户所写的自定义函数。
static void showPending( sigset_t &pending ) {for( int sig = 1; sig <= 31; ++sig ){if( sigismember(&pending,sig)) cout << " 1 ";else cout << " 0 ";}cout << endl;}static void handler( int signum ){ cout << "捕捉 信号: " << signum << endl;}int main(){ signal(2,handler);//定义信号集对象;sigset_t bset,obset; sigset_t pending; //2.初始化sigemptyset( &bset );sigemptyset( &obset );sigemptyset( &pending );//3.添加要屏蔽的信号 sigaddset(&bset,2); //其实只是在栈上作了修改。//4.设置set到内核中对应的进程内。(默认情况下进程不会对任何信号进行block) int n = sigprocmask( SIG_BLOCK,&bset,&obset ); assert( n == 0 ); (void)n; cout << "block 2 号信号成功 ...get: pid "<< getpid() << endl;//重复打印当前进程的pending信号集。int count = 0; while( true ){//5.1获取当前进程的pending信号集。sigpending( &pending);//5.2显示pending信号集中没有被递达的信号。showPending( pending );sleep(1);++count;if( count == 20 ){sigprocmask(SIG_SETMASK,&obset,nullptr); //恢复该进程的屏蔽字。cout << "开始解除对2号信号的屏蔽" << endl;}} return 0; }
我们可以看到,当进程收到2号信号后,一段时间内该进程处于阻塞状态,如果解除对2号信号的屏蔽,此时2号信号便会立刻递达,转而执行我们的自定义方法。pending表中对应的比特位也由1变成了0。
每一个进程都含有进程地址空间,进程地址空间由用户地址空间和内核地址空间构成。
用户地址空间:用户缩写的代码和数据存储于用户空间,通过用户级页表与物理内存建立映射关系。
内核地址空间:操作系统的代码数据存储与OS内核空间,通过内核级页表与物理内存建立映射关系。
在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是相同的。
内核态和用户态是如何进行转换的?
用户态与内核态之间互相主要为3种情况:
用户凭什么执行OS的代码?
CPU的寄存器有2套,一套可见,一套不可见。
其中便有一套CR3寄存器 ---- 表示当前CPU的执行权限, 其中有一个位图结构,例如 1 表示内核态,3表示用户态。
例如:当我们调用open系统函数时,有一行汇编指令 int 80 将用户态改为内核态,此时,因为CPU中保存了改进程的内核地址空间和用户地址空间及其对应的页表地址,进程便能通过CPU找到内核地址空间和内核级页表来执行OS代码和数据。
内核如何捕捉信号?
当我们执行主控制流程大的时候,某条命令可能因为某些情况进入内核(转换为内核态),当内核处理完异常情况前准备返回用户态时,就必须查看pengding表。
如果OS查看 pending表对应比特位由0变1,再查看block位图对应的比特位,如果为0,则执行OS默认终止进程操作不用转换为用户态,如果为1,则直接忽略该信号动作,并且转换为用户,从主控制流程中上次被中断的地方继续向下执行。
如果待处理信号为自定义捕捉时,。信号处理完毕之后调用系统sigreturn再次转换为内核态,再将pending表中对应的比特位清除,最后再返回用户态从上次被中断的地方继续向下执行。
当捕捉信号动作为自定义函数时,内核态能不能直接处理用户地址空间的代码?
可以,因为内核态是一种权限非常高的状态,但是OS不能这样设计。
因为OS如果用户在用户地址空间中写了一些非法操作,比如 rm命令删除内核数据时,这是非常危险的,进程并不能确保用户所写的代码是否安全合法,所以基础南横不能以内核态的状态执行与用户的代码。
我们可以利用以下例图巧记:
该图形与直线有几个交点就说明有该进程有几次状态切换,箭头的方向代表进程状态的切换方向,两个椭圆的交点(红色)表示内核信号检测,也就是pending表的检测。
sigaction函数可以读取并修改与指定信号相关联的处理动作。
返回值:
调用成功则返回0,出错则返回- 1。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数:
signo:表示指定信号的编号
act: 如果act指针非空,那么根据act修改该信号的处理动作。
oact: 如果oact指针非空,那么根据oact传出该信号原来的处理动作。
其中,参数act和oldact都是结构体变量,结构体定义如下:
struct sigaction {void(*sa_handler)(int);void(*sa_sigaction)(int, siginfo_t *, void *); //不作解释sigset_t sa_mask;int sa_flags; //一般赋为0void(*sa_restorer)(void); //不做解释};
sa_handler
注意:
用户所写的自定义函数,返回值必须为void,参数为int,我们可以通过参数来获取当前被捕捉信号的编号。
sa_mask
例如:我们使用sigaction函数捕捉2号信号,并自动屏蔽3,4,5号信号,在进程处理自定义函数时打印信号集。
void showPending( sigset_t* pending ){ for( int sig = 1; sig <= 31; ++sig ) { if( sigismember( pending ,sig)) cout << " 1 "; else cout << " 0 "; } cout << endl;}void handler( int num ){ cout << " 捕捉2号信号成功 pid:" << getpid() << endl; cout << " 捕捉2号信号成功 pid:" << getpid() << endl; cout << " 捕捉2号信号成功 pid:" << getpid() << endl; cout << " 捕捉2号信号成功 pid:" << getpid() << endl; sigset_t pending; int c = 20; while( true ) { sigpending(&pending); showPending(&pending); c--; if( !c ) break; sleep(1); }}int main( ){ //内核数据,用户栈定义的。 struct sigaction act,oact; act.sa_flags = 0; sigemptyset(&act.sa_mask); act.sa_handler = handler; sigaddset(&act.sa_mask,3); sigaddset(&act.sa_mask,4); sigaddset(&act.sa_mask,5); sigaction(2,&act,&oact); while( true ) sleep(1); //设置进当前调用进程的pcb当中。 return 0;}
当进程捕捉2号信号时,自定义函数还有一段时间持续在返回。此时,我们通过kill命令向目标进程发送3,4,5号信号,此时便可以通过pending发现3,4,5号信号在该时间段内已经被屏蔽了。但是,一段时间后,因为自动恢复原来的信号屏蔽字,3,4,5信号之一开始递达终止进程。
如果我们在main函数中调用insert函数将结点1插入链表,但是insert函数共分为两步。当进程插入函数执行到第一步的时候(并没有执行完),
因为某些中断,异常到了等原因切换到内核态,再次返回时由检查pending表发现还有信号处理,所以进程执行sighandler函数。
当进程执行完sighandler函数,也是将node2结点头插到链表中。
但是,当进程执行完自定义函数并返回为用户态执行mian函数中insert调用异常时继续向下执行,则继续执行insert函数中的第一,二步执行完毕,完成头插。
但是,最终只有node1完成了头插,而node2因为结点地址丢失而找不到了进而无法处理,导致了内存泄露。
像这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称之为重入,insert访问一个全局链表,有可能因为重入而导致错乱,像这样的函数称为: 不可重入函数,反之的,如果一个函数只访问自己的局部变量或参数,则称为:可重入函数。
如果一个函数符合以下条件之一则是不可重入的:
volatile作用: 保持内存的可见性,告知编译器,该被关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
以下代码中,我们在标准情况下,将2号信号进行捕捉后执行handler函数时让全局变量flag由0变1,当执行完毕后继续执行main函数中的代码此时应该跳出循环并打印flag值。
#include #include int flag = 0;void handler(int signo){cout << "change flag: " << flag; flag = 1; cout << " -> " << flag << endl;}int main(){signal(2, handler);while (!flag);cout << " 进程正常退出后 " << flag << endl; return 0;}
结果如下:
但是在gcc中,也有对应的优化机制,当我们使用最高优化级别”O3“进行编译,并运行该可执行程序时.
此时,尽管在handler函数中全局变量flag由0变成了1,但是在主函数中while循环依旧符合条件。
因为,在gcc优化之前,每一次访问全局变量flag都会从物理内存读取flag到寄存器中检测。可是编译器优化过后,编译器认为main函数中的全局变量flag并没有被修改。所以编译器在第一次加载flag时,便读取到寄存器edx中,而在handler修改flag时只是在内存中修改,之后,进程执行到mian函数中的flag那一行代码,便会从寄存器中检测。
所以,对全局变量以volatile关键字修饰,让编译器对其保持对内存的可见性。
#include #include volatile int flag = 0;void handler(int signo){cout << "change flag: " << flag; flag = 1; cout << " -> " << flag << endl;}int main(){signal(2, handler);while (!flag);cout << " 进程正常退出后 " << flag << endl;return 0;}
注意:
OS不关心执行代码,编译器优化在编译期间便把代码优化编译了,CPU按照编译后的代码执行。
用户为了避免僵尸进程,父进程需要使用wait和waitpid来处理僵尸进程,但是有两种处理方式:
父进程fork出子进程,子进程调用exit(0)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用waitpid以非阻塞的方式获得子进程的退出状态并打印。
void handler( int signum ){ pid_t id = 0; cout << ": 子进程退出" << signum << " father " << getpid() << endl; while( id = waitpid(-1,NULL,WNOHANG) > 0 ) { cout << "wait child success " << endl; }}int main(){ signal( SIGCHLD,handler ); if( fork() == 0 ) { cout << "child pid: " << getpid() << endl; sleep(1); exit(0); } while( true ) { sleep(3); cout << " 正在执行父进程 " << endl; }}
结果如下:
由于OS并不知道有多少个子进程退出,所以我们需要采用while循环调用waitpid函数以非阻塞方式清理僵尸进程,如果以阻塞方式等待,那么父进程再下一轮查询子进程状态时该子进程恰好不退出的话,那么父进程就会阻塞等待,无法继续执行下面的代码。
如果我们不想等待子进程,并且还想让子进程退出之后自动释放僵尸进程,我们可以通过signal函数默认对子进程忽略。
int main(){signal(SIGCHLD,SIG_IGN); if( fork() == 0 ) { cout << "child pid: " << getpid() << endl; sleep(5); exit(0); } while( true) { cout << "parent 正在运行 "<< endl; sleep(1); }}
这种忽略的含义就是用户告诉OS退出并默认清理僵尸进程。
来源地址:https://blog.csdn.net/m0_63300413/article/details/132453677
--结束END--
本文标题: Linux进程信号
本文链接: https://www.lsjlt.com/news/397112.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-03-01
2024-03-01
2024-03-01
2024-03-01
2024-03-01
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0