【linux操作系统】信号

目录

信号产生

信号处理

signal

[Core Dump](#Core Dump)

信号捕捉

信号保存

sigset_t

sigprocmask

sigpending

sigaction

可重入函数

volatile


生活中处处有信号,上课铃,闹钟等,当我们收到信号我们都知道该去做这个信号对应的事情,进程也是,操作系统也是,cpu也是。

信号产生

各信号对应的编码。1到31是常规信号,34到64是实时信号。

各信号对应的做法用man 7 signal查看

产生信号的做法

1.kill指令,如kill -9 pid。给当前进程发送9号信号。

2.键盘,ctrl c,ctrl \,ctrl z等分别是给进程发送2号,3号,19号

3.发生除0,野指针访问等行为时,os会检测到硬件异常,给进程写信号。

4.kill系统调用

给指定进程发送指定信号

参数1:进程pid

参数2:信号编码

一个实现kill的demo

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <cstring>

void Usage()
{
    std::cout << "Usage : ./Process signumber pid" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        exit(1);
    }
    int signumber = std::stoi(argv[1]);
    int pid = std::stoi(argv[2]);
    int n = kill(pid, signumber);

    return 0;
}

5.raise

给当前进程发送指定信号

6.abort

abort 函数使当前进程接收到信号SIG_ABRT而异常终止。

7.由软件产生的信号

如管道信号SIG_PIPE,闹钟alarm信号SIG_ALRM等。

alarm函数可以设定一个闹钟,参数指定闹钟的秒数,设为0表示取消上一个闹钟。

返回值返回上一个闹钟的剩余秒数。

信号处理

信号的处理方法有三种,分别是忽略,默认,自定义。

signal

函数的功能是捕捉指定信号,并定义其行为。

参数1:待捕捉的信号编码;

参数2:行为,可以是自定义,可以是忽略SIG_IGN,可以是默认SIG_DFL;

sighandler为函数指针类型,表示返回值为void,参数为int类型的函数。

Core Dump

信号的退出行为分为正常退出Term,异常退出Core。

当进程异常退出的时候会发生核心转储,生成一个core.xxx的文件,保存异常信息便于调试。

当发生核心转储后,退出信息的core dump标志就被设为1。

这是一段测试代码

cpp 复制代码
//testsig.cc

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
// typedef void (*sighandler_t)(int);

int count = 0;
void handler(int signumber)
{
    // int num = alarm(0);
    //  int n = alarm(3);
    // std::cout << "我是:" << getpid() << ",收到信号:" << signumber << std::endl;
    // std::cout << count << std::endl;
    //  std::cout << n << std::endl;

    // std::cout << num << std::endl;
    //  exit(1);

    std::cout << "catch a sig : " << signumber << std::endl;
    exit(0);
}

// int main()
// {
//     // std::cout << "我是进程: " << getpid() << std::endl;

//     // 处理信号
//     // 自定义捕捉
//     signal(SIGINT, handler);
//     // 忽略信号
//     // signal(SIGINT, SIG_IGN);
//     // 默认处理
//     // signal(SIGINT, SIG_DFL);
//     //  while(true)
//     //  {
//     //      //std::cout << "I am a process, waiting a signal!!" << std::endl;
//     //      sleep(1);
//     //      abort();
//     //      //raise(2);
//     //  }

//     alarm(100);
//     // int n = 1;

//     while (true)
//     {
//         // std::cout << n << std::endl;
//         count++;
//         sleep(1);
//     }

//     return 0;
// }

int main()
{
    // signal(SIGFPE, handler);
    // int a = 10;
    // a /= 0;

    int id = fork();
    if (id == 0)
    {
        // int a = 10;
        // a /= 0;
        char *ptr = nullptr;
        *ptr = 'a';
    }
    else
    {
        int status = 0;
        waitpid(id, &status, 0);
        std::cout << "exit code: " << ((status >> 8) & 0xFF) << " exit sig code: "
                  << (status & 0x7f) << " core dump: " << ((status >> 7) & 0x1) << std::endl;
    }
    return 0;
}

信号捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

  1. 用户程序注册了SIGQUIT信号的处理函数sighandler

  2. 当前正在执行main函数,这时发生中断异常用户态切换到内核态

  3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。

  4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

  5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。

  6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

  7. 简记为∞符号

信号保存

信号并不是一产生就要被处理的,我们收到快递员的电话,并不是第一时间就要去取快递,而是选择一个合适的时间点。那么在产生到处理的过程中,信号保存在哪里呢。要搞清楚这个问题首先得知道三张表。pending-未决信号集,block-阻塞信号集(也称信号屏蔽字),handler表。

信号在内核中的表示示意图

每个信号都有两个标志位分别表示阻塞(block)未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。一旦解除阻塞,信号将会被立刻递达。

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用戶自定义函数sighandler。

sigset_t

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

cpp 复制代码
#include <signal.h>

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);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_t类型的变量之前,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。

初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。

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

sigprocmask

参数1:表示如何更改。how参数的含义

参数2:你要设定的block表

参数3:原进程的block表,输出型参数。

返回值:若成功则为0,若出错则为-1

sigpending

读取当前进程的未决信号集,通过set参数传出。

返回值:调用成功则返回0,出错则返回-1。

cpp 复制代码
#include <signal.h>

int sigpending(sigset_t *set);

sigaction

cpp 复制代码
#include <signal.h>

int sigaction(int signo, const struct sigaction *act, struct sigaction*oact);

sigaction函数可以读取和修改与指定信号相关联的处理动作。

调用成功则返回0,出错则返回-1。

signo是指定信号的编号。

act指针非空,则根据act修改该信号的处理动作。

oact指针非空,则通过oact传出该信号原来的处理动作。

actoact指向sigaction结构体:

sa_handler也为回调函数。和sighandler 类似。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释。

下面是一段测试代码

cpp 复制代码
//SignalSave.cc

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


void PrintPending()
{
    sigset_t set;
    // sigemptyset(&set);
    sigpending(&set);
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(&set, i))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}
void handler(int signo)
{
    std::cout << "im is proc : " << getpid() << std::endl;
    std::cout << "我收到了信号:" << signo << std::endl;
    //在处理之前2号信号就被置为0了
    // std::cout << "-------------------------------" << std::endl;
    // PrintPending();
    // std::cout << "-------------------------------" << std::endl;
}
int main()
{
    std::cout << "im is proc : " << getpid() << std::endl;

    // sigset_t set;
    // sigemptyset(&set);
    // sigpending(&set);
    signal(SIGINT, handler);

    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set,SIGINT);//设置屏蔽2号信号,此时内核还未改变
    sigaddset(&block_set,SIGQUIT);//3
    sigaddset(&block_set,SIGILL);//4
    sigaddset(&block_set,SIGTRAP);//5
    sigprocmask(SIG_BLOCK, &block_set, &old_set);
    int cnt = 15;

    while (1)
    {
         
        PrintPending();
        cnt--;
        if(cnt < 0)
        {
            sigprocmask(SIG_SETMASK, &old_set,&block_set);//解除信号立即跳转到handler
            PrintPending();
        }
         sleep(1);
    }
    return 0;
}

可重入函数

信号的执行流和主控制流程是异步的,当两者访问相同的全局资源,就有可能出现冲突。二者不存在调用和被调用的关系,并且使用不同的堆栈空间。

函数被不同控制流程使用,在第一次调用的时候还未返回,就开始第二次调用,这称为重入。如果函数因为重入而造成全局函数混乱,则称为不可重入函数 ,反之,函数只访问自己的局部变量和参数,则成为可重入函数

不可重入函数

volatile

这是一个关键字告诉编译器,其修饰的变量,不允许被优化,对该变量的任何操作都必须在真实的内存中进行。因为编译器会存在优化的行为,在某些情况,直接在寄存器中读取数据。

这是一段测试代码

我发现我不在循环里面设置代码,编译器就不在内存检查flag;我在程序里面设置输出流,编译器就检查,我给信号就能正常退出。原因是当在循环里加上 printf(或任何函数调用、I/O 操作)时,编译器不敢再做这种激进优化(因为函数调用可能有副作用)。它被迫每次循环都去内存重新读取 flag 的值。

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
//int flag = 0;
 volatile int flag = 0;

void handler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
}

int main()
{
    signal(2, handler);
    while (!flag);
    // {
    //     //printf("im an test proc!!   %d\n", flag);
    //     printf("im an test proc!!   \n");
        
    //     //sleep(1);
    // }
    

    printf("process quit normal\n");
    return 0;
}

完。。。

相关推荐
SuperEugene1 小时前
VXE-Table 4.x 实战规范:列配置 + 合并单元格 + 虚拟滚动,避坑卡顿 / 错乱 / 合并失效|表单与表格规范篇
开发语言·前端·javascript·vue.js·前端框架·vxetable
xushichao19891 小时前
高性能密码学库
开发语言·c++·算法
小涛不学习1 小时前
Java面试全攻略(基础 + 集合 + 并发 + JVM + 框架)
java·开发语言
m0_518019481 小时前
C++代码混淆与保护
开发语言·c++·算法
skd89991 小时前
MicroSIP助手,智慧语音V3.2.3版本,MicroSIP自动拨号助手
服务器
m0_569881471 小时前
C++中的智能指针详解
开发语言·c++·算法
源远流长jerry1 小时前
RDMA 传输服务详解:可靠性与连接模式的深度剖析
linux·运维·网络·tcp/ip·架构
IT二叔1 小时前
Git Flow03-发布流程
git
SmartBrain1 小时前
Spring Boot 中常用注解总结(AI工程化)
java·人工智能·spring boot·后端