欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux进程信号

程序员文章站 2022-07-12 12:34:53
...

信号的基本概念

  首先,我们可以用kill -l命令查看系统中定义的信号列表:
Linux进程信号

  每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2。编号34以上的是实时信号,34以下的信号是普通信号。而这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明,在命令行上输入man 7 signal:
Linux进程信号

产生信号的方式:

  1. ⽤户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产⽣SIGINT信号,Ctrl-\产⽣SIGQUIT信号,Ctrl-Z产⽣SIGTSTP信号(可使前台进程停⽌,这个信号将在后⾯课程详细解释)。

  2. 硬件异常产⽣信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令,CPU的运算单元会产⽣异常,内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址,,MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。

  3. ⼀个进程调⽤kill(2)函数可以发送信号给另⼀个进程。可以⽤kill(1)命令发送信号给某个进程,kill(1)命令也是调⽤kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终⽌进程。当内核检测到某种软件条件发⽣时也可以通过信号通知进程,例如闹钟超时产⽣SIGALRM信号,向读端已关闭的管道写数据时产⽣SIGPIPE信号。如果不想按默认动作处理信号,⽤户程序可以调⽤sigaction(2)函数告诉内核如何处理某种信号.

  4. 软件条件产⽣

进程收到信号后,常见的信号处理动作有以下三种:

  1. 忽略此信号。

  2. 执⾏行该信号的默认处理动作(终止该信号)。

  3. 提供⼀个信号处理函数(自定义动作),要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

前台进程与后台进程

//sig.c
#include <stdio.h>

int main(){
    while(1);
    return 0;
}

1.⼀个命令后⾯加个&可以放到后台运⾏,用命令查看时,发现后台进程STAT状态栏是R,所以有+表示前台进程,无+表示后台进程。
Linux进程信号

2.Ctrl-C产生的信号只能发给前台进程,不能发给后台进程。
前台进程:
Linux进程信号

后台进程:
Linux进程信号

3.后台进程使Shell不必等待进程结束就可以接受新的命令。而前台进程运行时占用SHELL,它运行的时候SHELL不能接受其他命令。
Linux进程信号

4.Shell可以同时运行一个前台进程和任意多个后台进程。
Linux进程信号

5.前台进程在运⾏过程中⽤户随时可能按下Ctrl+C⽽产⽣⼀个信号,也就是说该进程的⽤户空间代码执⾏到任何地⽅都有可能收到SIGINT信号⽽终⽌,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。后台进程不是任一进程都能做,要看实际情况。一般来说,如果某进程不需从键盘输入输出(交互少的)或者执行所需时间较长的话,就比较合适做后台进程。

产生信号

产生信号的方式在前面已经提到,简单来说可以归结为以下4点:

  1. 通过终端按键(组合键)产生信号

  2. 硬件异常产生的信号

  3. 调用系统函数向进程发信号

  4. 由软件条件产生信号

1.通过终端按键(组合键)产生信号:
Core Dump:

  概念:当⼀个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。也叫核心转储,帮助开发者进行调试,在程序崩溃时把内存数据dump到硬盘上,让gdb识别 。
Linux进程信号

  一个进程允许产生多大的core文件取决于进程的 Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。

  用ulimit -a命令查看系统中的软硬件资源限制
Linux进程信号

  在开发调试阶段可以用ulimit命令改变这个限制, 允许产生core文件:ulimit -c 1024,允许core⽂件最大为1024K
Linux进程信号

  修改core文件默认大小之后,表示可以产生core文件,此时再发送ctrl+\信号,会产生core dump文件
Linux进程信号

  进程异常终止通常是因为有 Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做 Post-mortem Debug(事后调试)。
Linux进程信号

2.调用系统函数向进程发信号:

kill:给某个进程发送某个信号
函数原型:

#include <signal.h>
int kill(pid_t pid, int signo);
//成功返回0,错误返回-1。

Linux进程信号

模拟实现kill:

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

void usage(const char* proc){
    printf("Usage: %s signo pid\n", proc);
}

int main(int argc, char* argv[]){
    if(argc != 3){
        usage(argv[0]);
        return 0;
    }
    int signo = atoi(argv[1]);
    int pid = atoi(argv[2]);
    kill(pid, signo);
    return 0;
}

Linux进程信号

raise:给自己发送某个信号
函数原型:

#include <signal.h>
int raise(int signo);
//成功返回0,错误返回-1。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

int main(){
    while(1){
        sleep(1);
        raise(3);
    }
    return 0;
}

执行1秒后:
Linux进程信号
也就是该进程执行1秒后,遇到raise函数,发出3号信号,导致程序异常终止。

abort:给自己发送 SIGABRT 信号
函数原型:

#include <stdlib.h>
void abort(void);
//就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

int main(){
    while(1){
        sleep(1);
        abort();
    }
    return 0;
}

执行1秒后,遇到abort函数,发出 SIGABRT 信号,导致程序异常终止
Linux进程信号

3.由软件条件产生信号:

闹钟信号:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//调⽤alarm函数可以设定⼀个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终⽌当前进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <signal.h>

void MyHandler(int sig, uint64_t count){
    (void) sig;
    printf("count = %lu\n", count);
    exit(0);
}

int main(){
    uint64_t count = 0;
    signal(SIGALRM, MyHandler);
    alarm(3);
    while(1){
        ++count;
    }
    return 0;
}

捕捉信号

Linux内核实现信号捕捉流程:
Linux进程信号

特点:

  1. 整体执行顺序∞
  2. 信号处理函数与原有的main函数是两个不同的执行流
  3. 信号处理未执行完以前,原有的main函数一直挂起等待

有关捕捉信号的函数:
pause:

#include <unistd.h>
int pause(void);

  pause函数使调⽤进程挂起直到有信号递达。如果信号的处理动作是终⽌进程,则进程终⽌,pause函数没有机会返回;如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调⽤了信号处理函数之后pause返回-1,errno设置为EINTR,所以pause只有出错的返回值。

模拟实现sleep函数:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void MyHandler(int sig){
    (void) sig;
}

void MySleep(int second){
    //1.捕捉 SIGALRM 信号,并且要把原来的信号处理方法备份下来
    __sighandler_t old_handler = signal(SIGALRM, MyHandler);
    //2.通过 alarm 注册一个闹钟
    alarm(second);
    //3.需要借助一个函数来等待闹钟信号的到来
    //  在闹钟信号到来之前,挂起等待
    //  一旦闹钟信号来了,继续往后执行
    pause();
    //4.恢复 SIGALRM 的信号处理方式
    //  如果不恢复的话,就可能影响到其他信号
    signal(SIGALRM, old_handler);
    return;
}

int main(){
    printf("before sleep...\n");
    MySleep(3);
    printf("after sleep...\n");
    return 0;
}

阻塞信号

1.信号其他相关常⻅概念

  • 实际执⾏信号的处理动作称为信号递达(Delivery)
  • 信号从产⽣到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block )某个信号。
  • 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
  • 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。
    Linux进程信号

2.信号集操作函数

#include <signal.h>
//初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含任何有效信号
int sigemptyset(sigset_t *set); 
//初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰该信号集的有效信号包括系统⽀持的所有信号。   
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); 

//这四个函数都是成功返回0,出错返回-1。
//sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

3.sigprocmask
功能:调⽤函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1

  如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指针,则更改进程的信号屏蔽字,参数how指⽰如何更改。如果oset和set都是⾮空指针,则先将原来的信号屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
Linux进程信号

如果调⽤sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀个信号递达。

4.sigpending

#include <signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。调⽤成功则返回0,出错则返回-1。

利用以上函数,进行简单的测试:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void PrintSigset(sigset_t* set){
    int i = 1;
    for(; i < 32; ++i){
        if(sigismember(set, i)){   //判断指定信号是否在目标集合中
            printf("1");
        }
        else{
            printf("0");
        }
    }
    printf("\n");
}

void MyHandler(int sig){
    printf("sig = %d\n", sig);
}

int main(){
    //1.捕捉 SIGINT 信号
    signal(SIGINT, MyHandler);
    //2.把 SIGINT 信号屏蔽掉
    sigset_t set;
    sigset_t oset;
    //  清空初始化
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    //  设置阻塞信号集,阻塞SIGINT信号
    sigprocmask(SIG_BLOCK, &set, &oset);
    //3.循环读取未决信号集
    int count = 0;
    while(1){
        ++count;
        if(count >= 6){
            count = 0;
            //解除信号屏蔽字,再设置上
            printf("解除信号屏蔽字!\n");
            sigprocmask(SIG_SETMASK, &oset, NULL);
            sleep(1);
            printf("再次设置信号屏蔽字!\n");
            sigprocmask(SIG_BLOCK, &set, &oset);
        }
        sigset_t pending_set;
        //获取未决信号集
        sigpending(&pending_set);
        PrintSigset(&pending_set);
        sleep(1);
    }
    return 0;
}

实现的效果如下:
Linux进程信号

可重⼊函数

同一进程中的同一个函数被不同执行流调用称为重入。

判断依据:是否有逻辑问题
同一个函数在多个执行流中同时调用,没有逻辑问题,称为可重入。
同一个函数在多个执行流中同时调用,有逻辑问题,称为不可重入。

如果⼀个函数符合以下条件之⼀则是不可重⼊的:(仅供参考)

  • 调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
  • 调用了其他不可重入函数。
  • 使用了非常量的全局变量/静态变量。
  • 调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。

注意:如果信号处理函数为不可重入函数,就可能出现逻辑问题,导致程序出现错误甚至崩溃。

如何编写可重入函数 :
1、不能在函数内部使用静态或全局数据
2、不能返回静态或全局数据,所有数据都有函数的调用者提供
3、使用本地数据、或通过制作全局数据的本地拷贝来保护全局数据
4、如果必须访问全局变量,利用互斥机制来保护全局变量
5、不在可重入函数内部调用不可重入函数
6、不调用malloc和free函数
7、不调用标准I/O函数

volatile

作用:

  1. 每次从内存中取值,不从缓存中取值。保证了用volatile修饰的共享变量,每次的更新对于其他线程都是可见的。
  2. volatile保证了其他线程的立即可见性,没有保证原子性。
  3. 由于有些时候对volatile的操作,不会被保存,说明不会造成阻塞。不可用于多线程下的计数器。

  被volatile修饰的变量,编译器对操作该变量的代码不再进行优化,而是从对应的内存中读取。

竞态条件与sigsuspend函数

现在重新审视“mysleep”程序,设想这样的时序:

  1. 注册SIGALRM信号的处理函数。
  2. 调⽤alarm(nsecs)设定闹钟。
  3. 内核调度优先级更⾼的进程取代当前进程执⾏,并且优先级更⾼的进程有很多个,每个都要执⾏很⻓时间
  4. nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态。
  5. 优先级更⾼的进程执⾏完了,内核要调度回这个进程执⾏。SIGALRM信号递达,执⾏处理函数sig_alrm之后再次进⼊内核。
  6. 返回这个进程的主控制流程,alarm(nsecs)返回,调⽤pause()挂起等待。
  7. 可是SIGALRM信号已经处理完了,还等待什么呢?

  出现这个问题的根本原因是系统运⾏的时序(Timing)并不像我们写程序时所设想的那样。虽然alarm(nsecs)紧接着的下⼀⾏就是pause(),但是⽆法保证pause()⼀定会在调⽤alarm(nsecs)之后的nsecs秒之内被调⽤。由于异步事件在任何时候都有可能发⽣(这⾥的异步事件指出现更⾼优先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题⽽导致错误,这叫做竞态条件(Race Condition)。

“解除信号屏蔽”和“挂起等待信号”这两步合并成⼀个原⼦操作的函数:

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);

  和pause⼀样,sigsuspend没有成功返回值,只有执⾏了⼀个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。调⽤sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void MyHandler(int sig){
    (void) sig;
}

//这个版本的函数来解决竞态条件问题
void MySleep(int second){
    //1.捕捉 SIGALRM 信号,并且要把原来的信号处理方法备份下来
    __sighandler_t old_handler = signal(SIGALRM, MyHandler);

    //2.屏蔽闹钟信号
    sigset_t set, oset;
    sigemptyset(&set);
    sigaddset(&set, SIGALRM);
    sigprocmask(SIG_BLOCK, &set, &oset);

    //3.通过 alarm 注册一个闹钟
    alarm(second);

    //4.需要借助一个函数来等待闹钟信号的到来
    //  在闹钟信号到来之前,挂起等待
    //  一旦闹钟信号来了,继续往后执行
    //  此处不能使用 pause, 而要使用 sigsuspend 来进行替换
    //  sigsuspend 能够做到原子的进行解除屏蔽+等待
    sigset_t tmp_set = oset;
    //  此处我们基于oset备份了一份tmp_set
    //  但是 tmp_set 里面可能也包含了对 SIGALRM 的屏蔽
    sigdelset(&tmp_set, SIGALRM);
    sigsuspend(&tmp_set);

    //5.恢复 SIGALRM 的信号处理方式
    //  如果不恢复的话,就可能影响到其他信号
    signal(SIGALRM, old_handler);

    //6.恢复信号屏蔽字
    sigprocmask(SIG_SETMASK, &oset, NULL);
    return;
}

int main(){
    printf("before sleep...\n");
    MySleep(3);
    printf("after sleep...\n");
    return 0;
}

SIGCHLD信号

  ⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以⾃定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>

void MyHandler(int sig){
    (void) sig;
    //int ret = wait(NULL);      //若用wait,会因wait数量不匹配而导致产生僵尸进程
    //printf("MyHandler:%d\n", ret);
    while(1){
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret > 0){
            printf("waitpid %d\n", ret);
            continue;
        }
        else if(ret == 0){
            //子进程还存在,没执行完,直接让信号处理函数返回
            //如果后续的子进程再结束,还会再次发送 SIGCHLD 信号
            break;
        }
        else{
            //子进程都结束
            break;
        }
    }
}

int main(){
    signal(SIGCHLD, MyHandler);
    int i = 0;
    for(; i < 20; ++i){
        pid_t ret = fork();
        if(ret < 0){
            perror("fork");
            return 1;
        }
        if(ret == 0){
            //child
            printf("child %d\n", getpid());
            sleep(3);
            exit(0);
        }
    }
    while(1){
        printf("father working\n");
        sleep(1);
    }
    return 0;
}

Linux进程信号

相关标签: Linux 信号

上一篇: [Linux]进程信号

下一篇: 47全排列II