子进程信号继承;kill+raise+alarm+pause+信号发生接收和处理+信号屏蔽

发布于:2022-11-15 ⋅ 阅读:(383) ⋅ 点赞:(0)

子进程对父进程信号继承情况

fork创建子进程,但子进程没有exec

在fork子进程之前:

如果父进程调用signal设置了某个信号的处理方式的话,那么fork出的子进程会继承父进程对该信号设置的处理

强调:只有在fork之前,父进程所设置的信号处理方式,才会被子进程继承

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<signal.h>
//pid_t fork(void);
int main(void){
    pid_t ret=0;
    /*在创建子进程前,设置信号忽略*/
    signal(SIGINT,SIG_IGN);
    ret=fork();
    if(ret>0){//父进程

    }
    else if(ret==0){//子进程

    }
    while(1);//防止进程太早结束
    return 0;
}

该信号被忽略后,父子进程都忽略

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<signal.h>
void signal_fun(int sig){
   printf("pid=%d\t,singo=%d\n",getpid(),sig);
}
int main(void){
    pid_t ret=0;
    /*在创建子进程前,设置信号忽略*/
    signal(SIGINT,signal_fun);
    ret=fork();
    if(ret>0){//父进程

    }
    else if(ret==0){//子进程

    }
    while(1);//防止进程太早结束
    return 0;
}
^Cpid=5992      ,singo=2
pid=5993        ,singo=2
^Cpid=5992      ,singo=2
pid=5993        ,singo=2
 //父子进程都会设置该信号
  • 为什么捕获函数在子进程里面依然有效

  • 因为子进程复制了父进程的代码和数据,子进程自然也会包含信号处理函数的代码,所在子进程中依然有效。

父子进程各自设置信号

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<signal.h>
void singnal_fun1(int sig){
    printf("1---Pid=%d\n",getpid());
}
void singnal_fun2(int sig){
    printf("2---Pid=%d\n",getpid());
}
int main(void){
    pid_t ret=0;
    /*在创建子进程前,设置信号忽略*/
    signal(SIGINT,SIG_DFL);
    ret=fork();
    if(ret>0){//父进程
       signal(SIGINT,singnal_fun1);
    }
    else if(ret==0){//子进程
       signal(SIGINT,singnal_fun2);//这里的只设置子进程
    }
    while(1);//防止进程太早结束
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ gcc fork_signal.c -o exe
guojiawei@ubantu-gjw:~/Desktop/signal$ ./exe
^C1---Pid=6766
2---Pid=6767

有调用exec加载新程序

fork之前,父进程设置的处理方式是忽略或默认

exec加载新程序后,忽略和默认设置依然有效

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main(int argc,char** argv,char** environ){
    pid_t ret=0;
    signal(SIGINT,SIG_IGN);

    ret=fork();
    if(ret>0){//父进程
      
    }
    else if(ret==0){//子进程
       execve("./child",argv,environ);
    }

    while(1);//防止进程太早结束
    return 0;
}


ctrl+c对父子进程均不起作用

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main(int argc,char** argv,char** environ){
    pid_t ret=0;
    signal(SIGINT,SIG_DFL);

    ret=fork();
    if(ret>0){//父进程
      
    }
    else if(ret==0){//子进程
       execve("./child",argv,environ);
    }

    while(1);//防止进程太早结束
    return 0;
}


默认:父子进程均有效

fork之前,父进程设置处理方式是捕获时
  • 新程序的代码会覆盖子进程中原有的父进程的代码,信号捕获函数的代码也会被覆盖

  • 既然捕获函数已经不存在了,捕获处理方式自然也就没有意义了,所以信号的处理方式会被还原为默认处理方式。

如果子进程所继承的信号处理方式是捕获的话,exec加载新程序后,捕获处理方式会被还原为默认处理方式。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void signal_fun(int sigo){
    printf("hello world!\n");
}
int main(int argc,char** argv,char** environ){
    pid_t ret=0;
    signal(SIGINT,signal_fun);

    ret=fork();
    if(ret>0){//父进程
      
    }
    else if(ret==0){//子进程
       execve("./child",argv,environ);
    }

    while(1);//防止进程太早结束
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ gcc fork.c -o main
guojiawei@ubantu-gjw:~/Desktop/signal$ ./main
^Chello world!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K7HeHS95-1668522074854)(/home/guojiawei/.config/Typora/typora-user-images/image-20221115160041547.png)]
父进程捕获,但是子进程被杀死了

子程序只能在自己的程序定义捕获

#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
void signal_fun(int sigo){
    printf("child\n");
}
int main(){
    signal(SIGINT,signal_fun);
    while(1);
    return 0;
}
//编译成./child
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void signal_fun(int sigo){
    printf("\nfather\n");
}
int main(int argc,char** argv,char** environ){
    pid_t ret=0;
    signal(SIGINT,signal_fun);

    ret=fork();
    if(ret>0){//父进程
      
    }
    else if(ret==0){//子进程
       execve("./child",argv,environ);
    }

    while(1);//防止进程太早结束
    return 0;
}

总结

1.仅fork时
			 子进程会继承父进程fork之前所设置的信号处理方式。
2.当有exec加载新程序时
			(1)子进程继承的处理方式是忽略或默认处理方式时,exec新程序后设置依然有	
			(2)如果子进程继承是捕获处理方式时,exec新程序后将被还原为默认处理方式

kill(),raise()

#include <sys/types.h>
#include <signal.h>
//kill:向PID所指向的进程发送指定的信号
int kill(pid_t pid, int sig);
//成功返回0,失败返回-1,errno被设置
#include <signal.h>
//raise:向当前进程发送指定信号
int raise(int sig);
//成功返回0,失败返回非0
kill(getpid(),SIGUSR1);
raise(SIGKILL);

当多个进程协同工作时,kill函数有时还是会用到的

比如向其它进程发送某信号,通知其某件事情发生了,其它进程收到这个信号后,就会调用信号处理函数进行相应的处理,以实现协同工作

alarm(),pause()

#include <unistd.h>
//设置一个定时时间,当所设置的时间到后,内核会向调用alarm的进程发送SIGALRM信号
//SIGALRM的默认处理方式是终止
unsigned int alarm(unsigned int seconds);
/*返回值:返回上一次调用alarm时所设置时间的剩余值*/
#include <unistd.h>
//调用该函数的进程会永久挂起(阻塞或者休眠),直至被信号(任意一个信号)唤醒为止
int pause(void);
/*返回值:
当被信号唤醒后会返回-1,表示失败了,errno的错误号被设置EINTR(表示函数被信号中断)*/
void signal_fun(int sigo){
    printf("time up!\n");
}
int main(){
    signal(SIGALRM,signal_fun);
    alarm(5);//5秒
    while(1);
    return 0;
}

返回值

int main(){
    unsigned int res=0;
    alarm(4);//5秒
    sleep(2);
    res=alarm(3);//上次剩余时间
    printf("res=%d\n",res);
    while(1);
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ ./main
res=2
闹钟
//因为4秒睡眠2秒,还剩3秒结束
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void signal_fun(int signo){}
int main(){
    unsigned int res=0;
    signal(SIGINT,signal_fun);
    pause();
    printf("\nhello\n");
    while(1);
    return 0;
}
/*
guojiawei@ubantu-gjw:~/Desktop/signal$ gcc fork.c -o main
guojiawei@ubantu-gjw:~/Desktop/signal$ ./main
^C
hello
^\退出 (核心已转储)
*/
  • 在开发中往往会使用pause()函数来帮助调试
  • 不想继续休眠时使用信号唤醒即可
abort()~raise(SIGABRT)

使用信号唤醒休眠函数

调用sleep、pause等函数时,这些函数会使进程进入休眠状态

如果不想继续休眠时,可以使用信号将其唤醒

  • 唤醒的方法-----给信号登记一个空捕获函数即可

唤醒的过程
当信号发送给进程后,会中断当前休眠的函数,然后去执行捕获函数,捕获函数执行完毕返回后,不再调用休眠函数,而是执行休眠函数之后的代码,这样函数就被唤醒了

#include <signal.h>
void signal_fun(int signo){
    printf("execute signal_fun\n");
}
int main(){
    signal(SIGINT,signal_fun);
    pause();//sleep(10);
    printf("pause code!\n");
    while(1);
    return 0;
}

既要捕获信号,又想继续执行pause或sleep

手动重启

pause()返回值

​ pause() returns only when a signal was caught and the
​ signal-catching function returned.

In this case, pause()returns -1, and errno is set to EINTR.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void signal_fun(int signo){
    printf("execute signal_fun\n");
}
int main(){
    int res=0;
    signal(SIGINT,signal_fun);
    lable: res=pause();
    /*手动重启*/
    if(res==-1&&errno==EINTR){
        goto lable;
    }
    printf("pause code!\n");//这句话总是执行不出
    while(1);
    return 0;
}
/*
guojiawei@ubantu-gjw:~/Desktop/signal$ ./main
^Cexecute signal_fun
^Cexecute signal_fun
^Cexecute signal_fun
^Cexecute signal_fun
^\退出 (核心已转储)
*/

sleep()返回值
1. Zero if the requested time has elapsed

  1. the number of seconds left to sleep, if the call was interrupted by a signal handler.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void signal_fun(int signo){
    printf("execute signal_fun\n");
}
int main(){
    int res=0;
    signal(SIGINT,signal_fun);
      /*手动重启*/
    res=10;
    lable_sleep: res=sleep(res);
    if(res!=0){
        printf("res=%d\n",res);
        goto lable_sleep;
    }
    printf("pause code!\n");//这句话总是执行不出
    while(1);
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ gcc fork.c -o mmm
guojiawei@ubantu-gjw:~/Desktop/signal$ ./mmm
^Cexecute signal_fun
res=8
^Cexecute signal_fun
res=6
^Cexecute signal_fun
res=5
^Cexecute signal_fun
res=4
^Cexecute signal_fun
res=3
^Cexecute signal_fun
res=1
pause code!

休眠函数自启动

使用read从键盘读取数据,当键盘没有输入任何数据时,read会休眠,不过函数被信号唤醒后,会自动重启read的调用

​ read函数读数据时,并不一定会休眠

  • 读硬盘上的普通文件时,不管文件有没有数据,read都不会休眠,而是会返回继续向下运行
  • 如果read读的是键盘的话,如果键盘没有数据时read就会休眠。
void signal_fun(int signo){
    printf("execute signal\n");
}
int main(){
    int res=0;
    signal(SIGINT,signal_fun);
    char buf[100]={0};
    read(0,buf,sizeof(buf));
    printf("have read!\n");
    while(1);
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ ./mmm
^Cexecute signal
^Cexecute signal
mmmm
have read!
^\退出 (核心已转储)

也就是有些休眠函数自动启动,有些休眠函数手动启动

信号发生,接收和处理过程

信号屏蔽字

作用

用来屏蔽信号的

每个进程能够接收的信号有62种,信号屏蔽字的每一位记录了每个信号是被屏蔽的还是被打开的

  • 如果是打开的就立即处理
  • 如果是屏蔽的就暂不处理
屏蔽字放在了哪里

每一个进程都有一个信号屏蔽字,它被放在了进程表(task_struct结构体变量)中

什么是屏蔽字
  • 简单地认为屏蔽字就是一个64位的unsigned int数,每一位对应着一个信号
    • 如果这一位为0,表示信号可以被立即处理
    • 如果为1表示该信号被屏蔽了,暂不处理
	1   2   3   4   5          61  62  63  64             
    *   *   *   *   *  ......   *   *   *   * 
    ------------------------------------------
<EX>第1位:对应编号为1(SIGHUP)的信号,该位为
		1)0:表示1(SIGHUP)这个信号是打开的,可以被立即处理
		2)1:表示信号被屏蔽了,暂时不能处理 

有对应的API用于修改信号屏蔽字的-----实现某个信号的打开和屏蔽

在默认情况下,信号屏蔽字中所有的位都为0

未处理信号集

作用:

跟屏蔽字一样,也一个64位的无符号整形数,专门用于记录未处理的信号

“未处理信号集”同样也是被放在了进程的进程表中(task_struct)

什么时候会记录

​ 信号来了,当进程的信号处理机制,检查该信号在屏蔽字中的对应位时发现是1,表示该信号被屏蔽了,暂时不能被处理,此时就会将“未处理信号集”中该信号编号所对应的位设置为1,这个记录就表示,有一个信号未被处理。

​ 如果该信号发送了多次,但是每一次都因为被屏蔽了而无法处理的话,在“未处理信号集”中只记录一次

什么时候处理未处理信号

当屏蔽字中该信号的位变成0时(被打开了),此时就回去检查“未处理信号”,处理表中的信号

信号处理的完整过程

屏蔽字+未处理信号集

  • 内核接收信号,首先去查看信号处理方式这张表,看是捕获默认还是终止
    • 如果是忽视,那么直接丢弃信号
    • 如果是捕获或默认或终止,会查看信号屏蔽字这张表
  • 检查相应的信号屏蔽字是0还是1
    • 如果是0,然后把0设置成1,就执行相应的捕获默认终止的操作
    • 如果是1,表名正在执行信号,就记录到未处理信号集
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void signal_fun(int signo){
    printf("hello\n");
    sleep(3);
    printf("world\n");
}
int main(){
    pid_t ret=0;
    signal(SIGINT,signal_fun);
    while(1);
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ ./mmm
^Chello
^C^C^C^C^Cworld
hello
world
^Chello
^C^C^C^C^C^C^C^C^C^C^Cworld
hello
world
//按下很多次,但只记录了一次

修改信号屏蔽字

修改的原理

  1. 定义一个64位的与屏蔽字类似的变量

  2. 将该变量设置为想要的值

    将某信号对应的位设置为0或者为1

  3. 使用这个变量中的值来修改屏蔽字

    1. 完全的替换
      使用变量的值去完全替换掉屏蔽字

      ​ 比如:屏蔽字 = 变量(1111111…11111)

    2. 使用|操作,将对应的位设置为1,只屏蔽某个或者某两个信号

      ​ 比如:屏蔽字 = 屏蔽字 | 0000…10

    3. 第三种:使用位&操作,将对应的位清0,打开信号

      ​ 比如:屏蔽字 = 屏蔽字 & (~0000…10)
      ​ 屏蔽字 = 屏蔽字 & 1111…01

SIGKILL和SIGSTOP信号是不能被屏蔽,就算在屏蔽字中它们对应的位设置为了1,也不会起到屏蔽的作用

设置变量API

       #include <signal.h>
       //将变量set的64位全部设置为0
       int sigemptyset(sigset_t *set);
       //将变量set的64位全部设置为1
       int sigfillset(sigset_t *set);
       //将变量set中,signum(信号编号)对应的那一位设置为1,其它为不变
       int sigaddset(sigset_t *set, int signum);
       //将变量set的signum(信号编号)对应的那一位设置为0,其它位不变
       int sigdelset(sigset_t *set, int signum);
/*	调用成功返回0,失败返回-1,并且errno被设置*/

设置变量修改屏蔽字API

       #include <signal.h>
       /* Prototype for the glibc wrapper function */
//使用设置好的变量set去修改信号屏蔽字
       int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
1)how:修改方式,前面说过有三种修改方式
		(a)SIG_BLOCK:屏蔽某个信号----屏蔽字=屏蔽字 | set
		(b)SIG_UNBLOCK:对屏蔽字的某位进行清0操作-----屏蔽字=屏蔽字&(~set)
		(c)SIG_SETMASK:直接使用set的值替换掉屏蔽字
2)set:set的地址
3)oldset:保存修改之前屏蔽字的值,如果写为NULL的话,就表示不保存

代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void signal_fun(int signo){
    sigset_t set;
    printf("hello\n");
    /*得到:010000*/
    sigemptyset(&set);//取地址
    sigaddset(&set,SIGINT);
    /*修改屏蔽字*/
    //SIG_UNBLOCK:取反相与
    sigprocmask(SIG_UNBLOCK,&set,NULL);//不保存旧值
    sleep(3);
    printf("world\n");
}
int main(){
    pid_t ret=0;
    signal(SIGINT,signal_fun);
    while(1);
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/signal$ ./mm
^Chello
^Chello
^Chello
^Chello
^Chello
^Chello
world
world
world
world
world
world