Linux_信号

一个进程退出有两种情况:1.正常执行完毕。2.程序执行中异常退出。第一种情况可以通过进程退出码来获取进程执行结果,第二种情况需要通过信号来判断进程异常退出原因。那么进程在什么样的条件下会产生信号,进程又是怎样处理产生的信号呢?
我们可以类比生活中的信号,比如接收到了取快递的短信,而你正在打游戏,等你打完游戏后在处理。这个短信就是一个信号,当你接收到短信时,你有可能在忙,所以需要记住这个短信(信号保存),当你处理这个信号时,有可能会自己取快递(默认动作),有可能忽略这个短信(忽略动作),也有可能让别人取(自定义动作)。综上所述,要学习信号,就要从信号的产生,信号的保存,信号的处理三个方面着手。
Linux_信号_第1张图片
在Linux中,可以用kill -l 查看信号列表,man 7 signal 可以查看7号手册中的信号信息。
Linux_信号_第2张图片

SIGHUP 是宏,它的值为1,其余类似。我们学的是1-31的信号,34-64属于实时信号。

一.信号的产生

信号的产生方式有五种:1.键盘 2. 系统调用 3.命令行 4.软件条件 5.硬件异常。
为了便于验证,这里提前引入一个系统调用signal,该函数的功能是将指定信号的处理动作修改为自定义行为。handler函数即为信号的自定义行为,它是一个回调函数
Linux_信号_第3张图片


void handler(int signo)
{
    //  自定义行为
}

int main()
{
    signal(2, handler);

    ///.....
    return 0;
}

1.1 键盘
  • **ctrl + c **:给前台进程发送一个SIGINT信号,这个信号的默认处理动作是退出。可以用man 7 signal来查看。
  • Linux_信号_第4张图片


Linux_信号_第5张图片

  • **ctrl + \ ** :给前台进程发送SIGQUIT信号,默认处理动作也是退出。Core与Term的区别后面会讲,但它们都会让进程退出。

验证程序:
当执行下面程序时,用键盘输入ctrl c ctrl \ 不会执行默认处理动作(退出),而是执行handler函数打印信号编号。

void handler(int signo)
{
    cout << signo << endl;
}
int main()
{
    signal(SIGINT, handler);
    signal(SIGQUIT, handler);

    while (1) ;
    return 0;
}

1.2 命令行
  • kill signo pid 给指定pid的进程发送signo信号

int main()
{
    while (1)
    {
        std::cout << "我是一个进程 %d:" << getpid() << std::endl;
        sleep(1);
    }
    
    return 0;
}

Linux_信号_第6张图片

1.3 系统调用
  • kill:给指定进程发送sig信号

    在这里插入图片描述

#include 
#include 
#include 
#include 

int main()
{
    int cnt = 5;
    while (cnt)
    {
        sleep(1);
        std::cout << cnt-- << std::endl;
    }

    kill(getpid(), SIGINT);

    std::cout << "begin -----" << std::endl;
    while (1)
    {
        ;
    }

    return 0;
}

这个程序在运行5秒后会立即退出,不会打印begin,因为它自己给自己发送2号信号 Linux_信号_第7张图片

  • raise:谁调用这个函数,给谁发送信号

    image.png

#include 
#include 
#include 
#include 

int main()
{
    int cnt = 5;
    while (cnt)
    {
        sleep(1);
        std::cout << cnt-- << std::endl;
    }

    raise(SIGINT);

    std::cout << "begin -----" << std::endl;
    while (1)
    {
        ;
    }

    return 0;
}
  • abort:谁调用这个函数,给谁发送**SIGABRT**信号,终止当前进程。自定义捕捉后也会退出。

Linux_信号_第8张图片
image.png

#include 
#include 
#include 
#include 

int main()
{
    int cnt = 5;
    while (cnt)
    {
        sleep(1);
        std::cout << cnt-- << std::endl;
    }

    //这是一个c库函数,内部封装了系统调用,不论你捕不捕捉SIGABRT信号,调用该函数程序都会退出
    abort();

    std::cout << "begin -----" << std::endl;
    while (1)
    {
        ;
    }

    return 0;
}

1.4 软件条件

image.png

  • 功能:在seconds秒后,给当前进程发送一个SIGALRM信号
  • 返回值:返回上一个闹钟剩余的秒数,当一个闹钟

Linux_信号_第9张图片

#include 
#include 
#include 
#include 

using std::cout;
using std::endl;

void handler(int signo)
{
    cout << signo << endl;
}

int main()
{

    signal(SIGALRM, handler);

   alarm(3);
   
   sleep(1);

   cout << alarm(1) << endl;       //此时打印的是2 ,因为创建alarm(3)这个闹钟后
                                   // 休眠了1秒,所以创建alarm(1)闹钟时,它的返回值是上一个
                                   //闹钟的剩余时间,2秒

   sleep(1);

    return 0;
}

计算机内部有一个计时器,它每时每刻都在运行,电脑时间也是根据它来确定的,并且它每隔一定时间都会给os发送一个时钟中断。
由于系统中的闹钟不只一个,所以操作系统为了管理闹钟而创建了一个结构体,结构体内部有一个timestamp(currtime+seconds)字段,保存的是这个闹钟什么时候唤醒。操作系统会遍历这个闹钟队列,当检测到哪个闹钟的timestamp和当前时间相同,便唤醒这个闹钟。

1.5 硬件异常

当程序中出现除0错误时,操作系统会给进程发送SIGFPE信号。OS是怎样知道程序中有除0错误的呢?当a /= 0 这样的语句被cpu执行时,cpu识别到除数为0时,会将状态寄存器中的溢出位置1,然后操作系统识别到状态寄存器中的值,会根据cpu中的寄存器找到当前进程的task_struct并写入信号SIGFPE
Linux_信号_第10张图片
虚拟地址是通过页表映射到内存当中的,其中从虚拟地址到物理地址的转换是由mmu硬件来完成的,mmu转化的时候有两种情况:1.页表中没有映射关系,mmu直接报错。2.页表中有映射关系,但是没有访问权限,mmu也会报错。操作系统识别到mmu报错,则会向当前进程发送SIGSEGV信号,表明错误原因是段错误。
Linux_信号_第11张图片
上述两种情况,看似是由软件引发的错误,但实际上是硬件异常而引发的操作系统向进程发送信号的过程。

1.6 Core与Term

Linux_信号_第12张图片

之前研究进程执行情况时,需要获取进程退出码和退出信号,其中有一个字段core dump这个字段的作用是表示核心转储,即将程序的数据都转储到磁盘。当程序异常退出时,如果默认退出动作为Core则会将core dump设置为1,然后在当前路径下创建一个core.pid的转储文件,在gdb中可以使用core-file指令导入这个转储文件,方便定位异常原因。如果默认退出动作为Term则不会创建转储文件。

生产环境(云服务器)默认不开启核心转储,所以core 也不会生产转储文件。

  • ulimit -a 可以打印服务器的资源上线
  • Linux_信号_第13张图片
  • 从上图可以看出,core file size =0 ,使用ulimit -c 10024 将core file size 修改为 10024即可开始转储功能

二.信号的保存

信号可能随时产生,也就是说信号的产生和进程的执行是异步的。当一个信号产生时,程序有可能会执行更加重要的任务,比如IO,所以不能立即处理信号,于是就需要将信号保存到一个地方,以便于后续处理。1-31的信号,短时间内我们只需要保存有无产生即可,故可以用位图来保存信号。下面介绍三个概念:

  1. 信号递达(Delivery):执行信号的处理动作就叫做信号递达
  2. 信号未决(Pending):从信号的产生到信号递达之间的状态就叫做信号未决
  3. 阻塞(block):当一个信号被阻塞时,它将永远保持未决状态,直到解除阻塞,才会递达。

在每一个进程控制块中,有三张表:block,pending,handler。block和pending表是位图结构,handler表是函数指针数组,存放的是信号处理行为(signal函数修改的就是这个表)。block表也叫做信号屏蔽字(阻塞信号集)。
Linux_信号_第14张图片
block和pending由于只需要保存两种状态,故此用位图来表示block表和pending表即可。在内核中,这种结构叫做sigset_t也叫信号集。

2.1 信号集函数
  • int sigemptyset(sigset_t *set);
    • 功能:将set信号集置0
  • int sigfillset(sigset_t *set);
    • 功能:将set信号集全部置1
  • int sigaddset (sigset_t *set, int signo);
    • 功能:给set信号集添加signo信号
  • int sigdelset(sigset_t *set, int signo);
    • 功能:在set信号集中删除signo信号
  • int sigismember(const sigset_t *set, int signo);
    • 功能:signo信号是否在set信号集中
int main()
{
    sigset_t set;
    sigemptyset(&set);
    sigfillset(&set);

    sigdelset(&set, 4);
    sigaddset(&set, 4);

    if (sigismember(&set, 4)) 
    {
        /// ...
    }
    return 0;
}
上述代码只是在栈区修改信号集(局部变量),还没有将信号添加到内核中,所以需要调用系统调用将信号添加到内核中。

2.2 block表修改函数
  • sigprocmask:

Linux_信号_第15张图片

  • how:以何种方式添加
    • SIG_BLOCK:block |= set 将set中的信号添加到block表中
    • SIG_UNBLOCK: block &= ~set 在block表中删除set中的信号
    • SIG_SETMASK:block = set 将block表改为set表
  • oldset:输出型参数,保存的是上一次信号屏蔽字。
  • 返回值:成功返回0,失败返回-1
#include 
#include 
#include 
#include 

using std::cout;
using std::endl;

int main()
{
    sigset_t set, oset;

    sigemptyset(&set);
    sigemptyset(&oset);

    // 给set添加3号信号
    sigaddset(&set, 3);

    // 阻塞三号信号
    sigprocmask(SIG_SETMASK, &set, &oset);

    while (1) ;

    return 0;
}
在上面程序中,由于阻塞了三号信号,所以`ctrl \`不会导致程序退出。

2.3 pending查看函数

操作系统不允许用户修改pending信号集,只能查看pending表

  • sigpending:

image.png

#include 
#include 
#include 
#include 

using std::cout;
using std::endl;

void printPending(const sigset_t& set)
{
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&set, i)) cout << "1";
        else cout << "0";
    }
    cout << endl;
}
int main()
{
    sigset_t set;
    sigemptyset(&set);

    sigpending(&set);

    while (1)
    {
        sleep(1);
        printPending(set);
    }

    return 0;
}

2.4 handler 表修改函数

信号的处理动作有三种,默认(SIG_DFL),忽略(SIG_IGN) ,自定义。
修改信号处理动作的函数有两个:
1.signal :
Linux_信号_第16张图片
2.sigaction:
Linux_信号_第17张图片

  • 参数中sigaction又是一种数据结构,它的字段有如下图

Linux_信号_第18张图片
我们使用这个数据结构时,只需要初始化sa_handler, sa_mask,sa_flags即可

  • sa_handler:自定义处理函数
  • sa_mask:当你正在执行某一个信号的处理函数时,该信号自动被os阻塞,防止出现递归调用的情况,如果你想在处理handler函数时屏蔽其他信号,就需要设置这个参数。
  • sa_flags:初始化0即可
#include 
#include 
#include 
#include 

using std::cout;
using std::endl;

void handler(int signo)
{
    cout << signo << endl;
}
int main()
{
    sigset_t set;
    sigemptyset(&set);

    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    act.sa_mask = set;

    //将3号信号的处理动作自定义为handler函数,oact保存上一次该信号的属性。
    sigaction(3, &act, &oact);

    while(1) ;

    return 0;
}    

三.信号的处理

当进程接收到一个信号时,会在合适的时间处理,那么什么是合适的时候呢?当进程执行状态由内核态转变为用户态的时候,就会处理信号。

用户态:执行用户程序的状态叫做用户态。
内核态:执行操作系统程序的状态叫做内核态。

  • 一个进程时间片到了,需要切换为内核态
  • 一个进程调用系统调用,需要切换为内核态

在cpu中,有一个CR3寄存器,当值为3时,表明当前进程为用户态,当值为0时,表明为内核态。

3.1 进程地址空间

在32位系统中,进程地址空间占4GB,其中1G是内核空间,3G是用户空间。内核空间保存的是操作系统的代码和数据,当进程调用系统调用时,便会跳转到内核空间中,此时进程的状态由用户态切换为内核态。一个进程中的页表分为内核级页表和用户级页表,由于操作系统只有一份,需要保证每一个进程看到同一个操作系统,所以在内存中,只有一份内核级页表和操作系统代码,所有进程共享这个资源。
Linux_信号_第19张图片

一个进程是如何被调度?
当时钟硬件发送时钟中断时,os检查当前执行进程的时间片,如果时间片到了,操作系统就会调用
schedule函数,保存当前进程的上下文,然会切换另一个进程。

3.2 信号处理原理

当程序执行系统调用时,状态由用户态变为内核态。当执行完系统调用后,os会访问进程pcb中的三个有关信号的表:block,pending,handler,如果一个信号在pending表中并且没有被阻塞,那么os会暂时将block置1,然后去调用handler表中的自定义函数,从内核态又变为用户态,执行完这个信号处理函数后,又会调用sigreturn函数从用户态切换为内核态,将block表中的阻塞信号置0,调用sys_sigreturn()接口,恢复上下文,返回一开始的系统调用处。
Linux_信号_第20张图片
根据上图可知,每一次调用系统,状态改变了4次。信号检测时机在交点处。

四.可重入函数

Linux_信号_第21张图片
当一个函数被重复进入,没有任何问题时,该函数便是可重入函数;当出现问题时,该函数便是不可重入函数。使用了全局变量的函数大部分是不可重入函数,如输入输出函数,STL容器,库,malloc/free等都是不可重入函数。
例子如上图,当链表插入被重复进入,就会导致一个节点丢失,内存泄漏。

五.SIGCHLD

当父进程创建子进程后,父进程可以调用wait/waitpid回收子进程,这样父进程会时刻关注子进程的状态。如果我们不想用这种方法回收子进程,也可以接收子进程退出时给父进程发送的信号SIGCHLD,这种信号的处理动作是默认SIG_DFL但行为是啥都不做,所以可以用信号的方式回收子进程。

#include 
#include 
#include 
#include 
#include 

using std::cout;
using std::endl;

pid_t id = 0;
void waitProcess(int signo)
{
    sleep(3);
    while (1)
    {
        int n = waitpid(-1, nullptr, WNOHANG);
        if (n > 0)
        {
            cout << id << " wait success !" << endl;
        }
        else
        {
            break;
        }
    }
}

int main()
{

    signal(SIGCHLD, waitProcess);
    for (int i = 0; i < 10; ++i)
    {
        id = fork();
        if (id == 0)
        {
            // 子进程
            sleep(5);
            exit(1);
        }
    }

    sleep(10);

    return 0;
}
  • 除了上面这种方法,在Linux中,还可以使用signal(SIGCHLD, SIG_IGN)表明父进程不想回收子进程,子进程可以直接退出。
#include 
#include 
#include 
#include 

int main()
{
    //特殊组合,操作系统识别到这种组合会直接退出子进程。
    signal(SIGCHLD, SIG_IGN);
    
    for (int i = 0; i < 10; ++i)
    {
        id = fork();
        if (id == 0)
        {
            // 子进程
            sleep(5);
            exit(1);
        }
    }

    sleep(10);

    return 0;
}

你可能感兴趣的:(Linux,linux,服务器)