目录

Linux进程信号

信号的种类

信号的产生

键盘发信号

输入系统命令发信号

使用函数发信号

kill

raise

abort

alarm

硬件异常产生信号

除0异常

野指针访问异常

子进程退出向父进程发信号

信号的保存

概念共识

表的相关操作

信号集相关的函数

表相关的函数

操作block表

操作pending表

表的代码操作

信号的处理

总结:用户态和内核态的切换

用户模式进入内核模式

内核准备返回前检查信号

跳转到用户模式执行信号处理函数

信号处理函数返回时进入内核

恢复主控制流程继续执行


只有认知的突破💫才能带来真正的成长💫编程技术的学习💫没有捷径💫一起加油💫 

🍁感谢各位的观看🍁欢迎大家留言🍁咱们一起加油🍁努力成为更好的自己🍁

Linux进程信号

对于信号的学习,无非就是搞明白如下图所示的过程。

信号的种类

ubuntu@VM-4-17-ubuntu:~$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

1~31是分时系统信号,34~64为实时系统信号,我们只关注分时系统信号即可。

信号的产生

信号的产生方式,有如下几种方式。

键盘发信号

通过键盘的快捷键方式,向进程发送信号。比如,Ctrl+c终止进程。

输入系统命令发信号

通过kill命令发送信号,比如,kill -9 4244

使用函数发信号

kill

系统kill命令,底层就是调用kill函数实现的。通过kill函数给指定进程发送信号。

函数:int kill(pid_t pid,int sig)头文件:<sys/types.h> ,<signal.h>。发送成功,返回0,否则-1。

参数:

  • pid:目标进程的pid。

  • sig:向目标进程发送某种信号。

如下所示的代码——实现自己的kill命令。

#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(int argc,char*argv[])
{

    int signal=std::stoi(argv[1]);//发送的信号
    pid_t pid=std::stoi(argv[2]);   //目标进程的pid
    kill(pid,signal);   //向目标进程发送某种信号
    return 0;
}

raise

函数:int raise(int sig)。这个函数用于进程自己,自己给自己发信号。

如下所示的代码。

#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(int argc,char*argv[])
{
    int i=0;
    while(1)
    {
        std::cout<<i<<std::endl;
        i++;
        if(i==5)
        {
            std::cout<<"啊,我死了\n";
            raise(9);
        }
        sleep(1);
    }
    return 0;
}

abort

函数:void abort(void)。只要调用这个函数,它就会向该进程发送异常终止信号,和exit()一样的效果。

如下所示的代码。

#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(int argc,char*argv[])
{
    int i=0;
    while(1)
    {
        i++;
        std::cout<<i<<std::endl;
        if(i==5)
        {
            std::cout<<"啊,我死了,我调用了abort函数\n";
            abort();
        }
        sleep(1);
    }
    return 0;
}

alarm

函数:unsigned int alarm(unsigned int seconds)。它是一个闹钟函数,在程序运行之前,给这个程序设置一个闹钟,时间一到,就会向该进程发送一个SIGALRM信号,该信号的默认处理动作是终止当前进程。

参数

  • seconds:定的时间。

  • 返回值:如果超时则返回0。未超时的前提下,返回的是上一个闹钟的剩余时间。

如下所示的代码。

#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

void fun(int signal)
{
    std::cout<<"收到一个信号"<<signal<<std::endl;
}
int main(int argc,char*argv[])
{
    signal(SIGALRM,fun);
    alarm(5);  //10秒钟一到,就会向该进程发信号,被signal捕捉,被fun处理
    int i=0;
    while(1)
    {
        i++;
        std::cout<<i<<std::endl;
        sleep(1);
    }
    return 0;
}

硬件异常产生信号

除0异常

除0异常,OS会向该进程发SIGFPE信号,终止该进程。

如下所示的代码。

#include <iostream>
#include <signal.h>
void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    signal(SIGFPE, handler); // 8) SIGFPE
    sleep(1);
    int a = 10;
    a /= 0;
    while (1)
        ;
    return 0;
}

野指针访问异常

#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}
int main()
{
    signal(SIGSEGV, handler);
    sleep(1);
    int *p = NULL;
    *p = 100;
    while (1)
        ;
    return 0;
}

子进程退出向父进程发信号

#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
int main()
{
    if (fork() == 0)
    {
        sleep(1);
        int a = 10;
        a /= 0;     //子进程除0异常退出
        exit(0);
    }
    int status = 0;
    waitpid(-1, &status, 0);
    printf("exit signal: %d, core dump: %d\n", status & 0x7F, (status >> 7) & 1);
    return 0;
}

信号的保存

对于信号的处理并不是立即的,而是先保存起来,而这个信号被保存在pending表中。进程块中,有三张表,分别是block,pending和handler表。如下图所示。


//进程块中的3张表
struct task_struct { 
    ... 
    /* signal handlers */ 
    struct sighand_struct *sighand; 
    sigset_t blocked 
    struct sigpending pending; 
    ... 
}

概念共识

有几个概念需要知道。

  • 实际执行信号的处理动作,称为信号递达(Delivery)

  • 信号从产生到递达之间的状态,称为信号未决(Pending)。

  • 进程可以选择阻塞(Block)某个信号。

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

表的相关操作

信号集相关的函数
  • sigset_t:信号集。它就是一个整形变量,采用位图给它的位设值。

  • int sigemptyset(sigset_t *set):清空信号集——全部置0。

  • int sigfillset(sigset_t *set):填满信号集——全部置1。

  • int sigaddset(sigset_t *set, int signo):给信号集中的某个信号置1——给指定信号置1。

  • int sigdelset(sigset_t *set, int signo):给信号集中的某个信号置0——给指定信号置0。

  • int sigismember(const sigset_t *set, int signo):判断指定信号是否在信号集中被设置了。

  • 注意:在使用sigset_ t类型的变量之前,⼀定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加(置1)或删除(置0)某种有效信号。

  • sigemptyset,sigfillset,sigaddset,sigdelset这四个函数都是成功返回0,出错返回-1。

  • sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

表相关的函数
操作block表
  • int sigprocmask(int how, const sigset_t *set, sigset_t *oset)。 成功返回0,否则-1。

参数:how就直接使用SIG_SETMASK。

这个函数用来操作bolck表的,bolck表==屏蔽字。把设置好的sigset_t信号集,通过这个函数给设置到屏蔽字中。

操作pending表
  • int sigpending(sigset_t *set)。成功返回0,否则-1。

这个函数是用来获取pending表中的信号,通过输出,传给自己设置的sigset_t信号集。对于pending的信号设置,一般都是OS自动设置,用户无需设置。当然也可以用户设置——信号的产生,便是用户自己设置的过程。

表的代码操作

如下所示的代码。

#include <iostream>
#include <functional>
#include <vector>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "[pid: " << getpid() << "] " << "sigpending list: ";
    // 右->左, 低->高 , 0000 0000
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))    //判断信号是否存在(被设置)
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << "\r\n";
}

int main()
{
    // 1. 屏蔽2号信号
    // 1.1 用户层面,设置位图
    sigset_t block, oblock;    //设置屏蔽字新,旧信号集
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigaddset(&block, SIGINT); // 这里的时候,我们有没有设置当前进程的信号屏蔽字?没有!!!!

    // 1.2 设置内核的信号屏蔽字
    sigprocmask(SIG_SETMASK, &block, &oblock);

    int cnt = 15;
    while (true)
    {
        sigset_t pending;    //设置pending信号集
        sigemptyset(&pending);
        // 2.1 获取当前进程的pending信号集
        sigpending(&pending);

        // 2.2 不断打印所有的pending信号集中的信号
        PrintPending(pending);    //带出来

        cnt--;
        if (cnt == 0)
        {
            // 解除对2号的屏蔽
            std::cout << "解除对2号的屏蔽啦!" << std::endl;
            // 怎么做?
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }
        sleep(1);
    }
}

信号的处理

typedef void (*sighandler_t)(int); // 信号处理函数的类型定义
sighandler_t signal(int signum, sighandler_t handler);

这个函数就是操作handler表,把我们自己定义的操作函数,给设置到handler表,接收到有效的信号后,就会执行自定义的函数。

类型别名 sighandler_t:表示信号处理函数的指针,该函数接收一个int 类型的信号编号作为参数,无返回值。

  • signum:要处理的信号编号(如 SIGINTSIGTERM 等)。

  • handler:信号的处理方式,有 3 种可选值:

    • 自定义函数:信号触发时执行该自定义函数(最常用)。

    • SIG_IGN:忽略该信号(内核直接丢弃,进程无感知)。

    • SIG_DFL::恢复信号的默认处理行为(如 SIGINT 默认是终止进程,SIGSEGV 默认是段错误并终止)

总结:用户态和内核态的切换

信号被触发,到被处理,需要从用户态到内核态再到用户态的转化,如下图所示。

  1. 用户模式进入内核模式

  • 进程在用户模式下执行主控制流程的指令时,会因为中断、异常或系统调用(比如readsleep)进入内核模式。

  • 这是信号处理的起点,因为内核只会在从内核模式返回用户模式的 “间隙” 里检查和处理信号。


  1. 内核准备返回前检查信号

  • 内核在处理完中断 / 异常 / 系统调用、准备返回用户模式之前,会调用do_signal()函数。

  • 这个函数会检查当前进程的信号队列,判断是否有可以递送的信号。


  1. 跳转到用户模式执行信号处理函数

  • 如果检测到需要处理的信号,并且该信号的处理方式是自定义函数(不是默认或忽略),内核会修改进程的上下文。

  • 它会让进程回到用户模式时,不继续执行原来的主控制流程,而是直接跳转到用户定义的信号处理函数(比如图中的sighandler)。


  1. 信号处理函数返回时进入内核

  • 当信号处理函数执行完毕后,不会直接回到主控制流程,而是会触发一个特殊的系统调用:sigreturn

  • 这个系统调用会再次让进程进入内核模式,由内核完成收尾工作。


  1. 恢复主控制流程继续执行

  • 内核通过sys_sigreturn()函数,恢复进程在第 1 步被中断时的上下文。

  • 最后返回用户模式,让进程从上次被中断的指令位置继续向下执行主控制流程。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐