Linux -- 进程信号

news2024/11/23 19:53:59

文章目录

  • 1. 信号量
    • 1.1 进程互斥概念
    • 1.2 认识信号量
  • 2. 信号入门
    • 2.1 信号概念
    • 2.2 见一见
    • 2.3 signal()系统调用
    • 2.4 宏定义信号
  • 3. 信号产生方式
    • 3.1 键盘产生信号
    • 3.2 系统调用产生信号
    • 3.3 软件条件产生信号
    • 3.4 硬件异常产生信号
    • 3.5 Core dump
  • 4. 阻塞信号
    • 4.1 相关概念
    • 4.2 信号在内核中的示意图
    • 4.3 函数操作pending和block表
      • 4.3.1 sigset_t信号集
      • 4.3.2 函数
      • 4.3.3 sigpending未决信号集
  • 5. 捕捉信号
    • 5.1 引出
    • 5.2 信号捕捉
    • 5.3 sigaction
  • 6. 其他知识
    • 6.1 可重入函数
    • 6.2 volatile关键字
    • 6.3 SIGCHLD信号

1. 信号量

1.1 进程互斥概念

  1. 两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域,否则可能发生与时间有关的错误,这种现象被称作进程互斥· 也就是说,一个进程正在访问临界资源,另一个要访问该资源的进程必须等待(任何时刻,都只允许一个进程在进行共享资源的访问)
  2. 任何时刻都只允许一个进程在进行访问的共享资源叫做临界资源
  3. 临界资源都是通过代码访问的,凡是访问临界资源的代码就叫做临界区
  4. 一个程序,它要么完整的被执行,要么完全不执行的特性就叫原子性

1.2 认识信号量

  1. 信号量又是什么呢?

    • 本质就是一个计数器,用来给资源计数
    • 任何一个进程想访问临界资源中的一个子资源的时候都不能直接访问,必须先申请信号量资源;如果有信号量资源,进程在对应的临界区访问临界资源就会申请对应的信号量,类似:count–;使用完后就会释放信号量,类似:count++
  2. 信号量是不是共享资源呢?

    • 是的,因为进行需要申请信号量,那么进程就必须先看到对应的信号量,那么信号量就是共享资源
  3. 什么来保证信号量的资源呢?

    • 信号量必须保证++和–操作是原子性的
  4. 接口认识(不具体说,这里信号量只是做个了解)

    • int semget(key_t key, int nsems, int semflg); —> 获取一个信号量标识符
    • int semctl(int semid, int semnum, int cmd, …); —> 信号量控制操作

2. 信号入门

2.1 信号概念

  1. 生活中的信号到进程信号:

红绿灯、闹钟、下课铃、倒计时、电话等等,这些都是我们生活中的信号。当发生信号的时候,我们就会有对应的行为,比如当红灯亮的时候,我们会停止下来等待,当然信号没有发生的时候,我们也会有知道怎么来处理它。那么我们能处理信号的原因是因为我们可以识别到这些信号。那么进程就相当于是我,信号就相当于一个数字,进程在没有收到信号的时候其实它就已经知道怎么来处理这个信号了!为了能够知道信号处理,那么就需要识别这些信号,那么这些信号怎么来识别呢,操作系统中已经对每个信号进行了设置,如下:(1-31:普通信号,34-64:实时信号,不关心实时信号)

还有一个问题就是,生活中电话这个信号是不是可能随时就来了,但是我们如果正在和老板谈重要会议呢,那么就不会立马处理,但是这个电话挂断后,我们会记住有个电话之前打来过,这里这个信号就被保存到我们的大脑中,此时的过程就是信号产生 -> 信号保存 -> 信号处理,进程也是如此,当一个信号来了的时候,可能这个进程在执行一个优先级很高的任务,那么此时这个信号就会被进程记录下来,等任务执行完后,再对此信号做出处理

  1. 进程如何记录对应产生的信号?怎么保存这些信号呢?

用结构体对信号进行描述,然后用数据结构对信号管理起来;进程task_struct结构体中存在位图结构来保存信号

2.2 见一见

代码:

#include <iostream>
#include <unistd.h>

int main()
{
    while(true)
    {
        std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl;
        sleep(2);
    }
    
    return 0;
}

演示:

  1. 通过发送信号杀掉进程:
  1. 前台进程直接ctrl + c终止进程:
  1. 后台进程只能通过发送信号杀掉进程:

2.3 signal()系统调用

选项内容
作用Signal()将信号信号的处置设置为handler
头文件#include <signal.h>
函数声明sighandler_t signal(int signum, sighandler_t handler);
  1. 回顾回调函数
#include <iostream>
int add(int x, int y)
{
    return x + y;
}

void calc(int (*add_fun)(int, int))
{
    int a = 10;
    int b = 20;
    int result = add_fun(a, b);
    std::cout << "result: " << result << std::endl;
}

int main()
{
    calc(add);
    return 0;
}
//最终输出结果:
//result: 30
  1. signal()系统调用获取对信号最处理
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signum)
{
    std::cout << "get a signal: " << signum << std::endl;
}

int main()
{
    signal(2, handler);  //handler函数就是回调函数

    while(true)
    {
        std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl;
        sleep(2);
    }
    
    return 0;
}

演示:

观察现象:当Ctrl + c时,会执行会执行对应的handler方法

  1. (9)号信号的特殊

代码:

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

void handler(int signum)
{
    std::cout << "get a signal: " << signum << std::endl;
}

int main()
{
    for(int i = 1; i <= 31; ++i)
    {
        signal(i, handler); 
    }
    

    while(true)
    {
        std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl;
        sleep(3);
    }
    
    return 0;
}

演示:

(9)号信号是管理员信号,是不可被定义的,所以可以直接杀掉进程

2.4 宏定义信号

3. 信号产生方式

3.1 键盘产生信号

当我们命令上按Ctrl+c时就表示终止进程,键盘如何发送这里请查阅资料

3.2 系统调用产生信号

  • kill()系统调用
选项内容
作用发送信号给进程
头文件#include <sys/types.h> #include <signal.h>
函数声明int kill(pid_t pid, int sig);
返回值成功返回0,失败返回-1

代码:

systemcall.cc文件:

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

void manual(std::string process)
{
    std::cout << "manual: \n\t";
    std::cout << process << " <number> <process>\n" << std:: endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3) //必须带两个选项
    {
        manual(argv[0]);
    }

    int signum = atoi(argv[1]); //atoi():字符串转整数
    pid_t id = atoi(argv[2]);
    int ret = kill(id, signum);  //kill():发送信号给进程
    assert(ret == 0);
    if(ret != -1){
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }

    return 0;
}

test.cc文件:

#include <iostream>
#include <unistd.h>

int main()
{
    while(true)
    {
        std::cout << "I am a process, is excuting ..... , PID: " << getpid() << std::endl;
        sleep(2);
    }
    return 0;
}

演示:

  • raise()C语言接口
选项内容
作用发送信号给调用者
头文件#include <signal.h>
函数声明int raise(int sig);
返回值成功返回0,失败返回非0
  • abort()C语言接口
选项内容
作用导致进程异常终止
头文件#include <stdlib.h>
函数声明void abort(void);
返回值无返回值

3.3 软件条件产生信号

  • alarm()C语言接口
选项内容
作用设置一个告警信号
头文件#include <unistd.h>
函数声明unsigned alarm(unsigned seconds);
返回值如果在剩余时间内有先前的alarm()请求,则alarm()应返回一个非零值,表示距离前一个的秒数请求将生成一个SIGALRM信号。否则,报警()应返回0
  • 验证IO效率
#include <iostream>
#include <unistd.h>
#include <signal.h>

int count = 0;

int main()
{
    alarm(1); //1秒后发信号
    
    while(true)
    {
        std::cout << "count: " << count++ << std::endl;
    }

    return 0;
}
//大概count等于20000左右
#include <iostream>
#include <unistd.h>
#include <signal.h>

int count = 0;

void handler(int signum)
{
    std::cout << "get a signal: " << signum << " count:" << count << std::endl;
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1); //1秒后发信号
    
    while(true)
    {
        ++count;
    }

    return 0;
}
//大概count等于500000000左右
  • alarm()返回值理解
#include <iostream>
#include <unistd.h>
#include <signal.h>

int count = 0;

void handler(int signum)
{
    std::cout << "get a signal: " << signum << " count:" << count << std::endl;
    int ret = alarm(10);
    std::cout << "ret: " << ret << std::endl;
}

int main()
{
    std::cout << "PID: " << getpid() << std::endl;
    signal(SIGALRM, handler);
    alarm(10); //1秒后发信号
    
    while(true)
    {
        ++count;
    }

    return 0;
}

返回的就是上次设置闹钟时间到给闹钟发信号的时间的时间差。也就是假如我设置了一个20分钟的闹钟,此时我的小猫把房间里面的东西给我吵醒了,我起床一看我只睡了10分钟,那么剩余的10分钟就是这里的时间差。这个闹钟是个信号,那么对应的操作系统中就会有对应的闹钟结构体对其描述和管理。

3.4 硬件异常产生信号

test.cc

#include <iostream>

int main()
{
    int a = 10;
    int b = 0;      
    int c = a / b;
    
    std::cout << "division operation" << std::endl;
    return 0;
}
//运行结果如下:
  • 这里其实是硬件异常导致产生的信号,运行会通过内存把数据加载到CPU来运算,CPU中有一种寄存器是用来记录是否数据溢出的,这里除0操作会溢出,那么这个寄存器就会被置为对应的数值来表示这个状态,最后CPU检测到异常然后给到操作系统,操作就会给该signal进程发送信号(8)号信号SIGFPE。
  • 这里可不可以不让这个进程因为SIGFPE信号退出呢?可以的
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int signalnum)
{
    printf("PID:%d received signal:%d crash\n", getpid(),signalnum);
    //exit(1);
}

int main()
{
    signal(SIGFPE, handler); //捕捉信号

    int a = 10;
    int b = 0;      
    int c = a / b;
    
    std::cout << "division operation" << std::endl;
    return 0;
}
//这段代码的现象:循环打印printf内容,为什么呢?
//原因是CPU中这个溢出状态检测寄存器检测到溢出后操作系统接受到错误给该进程发送信号,但是操作系统并没有修复CPU溢出检测寄存器,所以操作系统不断就给进程发送信号,这里进程不断捕捉信号(自定义行为),就死循环了。怎么改正呢?很简单绶捕捉信号后终止掉进程
  • 野指针问题同样是硬件异常产生信号
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int signalnum)
{
    printf("PID:%d received signal:%d crash\n", getpid(),signalnum);
    //exit(1);
}

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

    int* p1 = nullptr;
    //p1 = (int*)100;
    *p1 = 100; //野指针访问写入

    std::cout << "wild pointer!" << std::endl;
    
    return 0;
}
//输出结果:segmentation fault (11号信号:SIGSEGV)
//为什么会出现这种错误呢?程序运行会变成进程,进程由操作系统管理,虚拟内存会通过页表建立key/value关系映射到物理内存,硬件上MMU主要完成虚拟地址到物理地址的映射,所以虚表是实现MMU的手段,这里的页表中不仅仅有kv关系,同时也有读写权限,p1=(void*)0,也就是0号地址,这里有两种可能导致异常,可能p1虚拟地址并没有物理地址,也可能p1虚拟地址有映射的物理地址但是并没有写入或者读取权限。,这两种可能都会导致报错。
//这里为什么会出现死循环呢?原因还是因为操作系统并没有修复MMU硬件错误,使得进程不断捕获信号,导致死循环打印。解决方法:捕获后终止进程

3.5 Core dump

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。虚拟机上是可以直接看到的,但是云服务器上此功能是默认关闭的,使用ulimit -a命令查看:

  • 如何打开呢?

使用ulimit -c size(大小)使得这个磁盘核心转储有大小就是把它打开了。

  • 如何验证core file size的存在?
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int signalnum){
    printf("PID:%d received signal:%d termination\n",getpid(), signalnum);
    exit(1);
}

int main()
{
    while(true){
        std::cout << "PID:" << getpid() << " doing ....." << std::endl;
        sleep(1);
    }
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aglLGiPi-1684114794264)(https://typora130.oss-cn-nanjing.aliyuncs.com/QQ截图20230508191812.png)]

又上述观察得到一个结果:Term就是普通终止,没有任何操作;Core终止会先进行核心转储再终止进程,如何验证?

  • 核心转储有什么用?方便异常后进行调试
#include <iostream>

int main()
{
    std::cout << "wild pointer!" << std::endl;
    std::cout << "wild pointer!" << std::endl;
    std::cout << "wild pointer!" << std::endl;
    
    int* p1 = nullptr;
    *p1 = 100; //野指针访问写入

    std::cout << "wild pointer!" << std::endl;
    
    return 0;
}
  • 这里有了这个core dump那么调试就很轻松,那为什么云服务器会关闭core dump呢,它有什么坏处呢?

它会占用磁盘空间,一是本身程序就很大,出错后形成core dump文件很大;二是每次程序瓜重启都会形成core dump文件,如果很多次重启程序就会导致形成很多个core dump文件。如何关闭呢?ulimit -c 0命令。

另外,进程退出时可以获取信号,也可以获取退出码,也可以知道core dump是否发生(依靠core dump标记比特位):

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;

        int* p = nullptr;
        *p = 100;

        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;
        exit(0);
    }

    int status = 0;
    waitpid(id, &status, 0); //阻塞等待
    printf("exit code: %d | exit signal: %d | core dump flag: %d\n",  \
    ((status >> 8) & 0xFF), status & 0x7F, (status >> 7) & 0x1);

    return 0;
}
//运行结果如下:

4. 阻塞信号

4.1 相关概念

  1. 信号递达(delivery):执行信号的处理动作(默认处理:终止进程(Term,Core)、signal函数:自定义处理)
  2. 信号未决(pending):信号从产生到递达之间的状态(暂时保存)
  3. 阻塞信号(block):被阻塞的信号产生时将保持在未决状态,直到进程解出对此信号的阻塞才执行递达动作(阻塞和忽略是不同的,信号被阻塞就不会递达,忽略是递达后的一种处理动作(什么都不做的动作))

4.2 信号在内核中的示意图

进程维护三张表:

  1. pending表:位图结构;比特位的位置表示哪一个信号,比特位的内容表示是否收到信号
  2. block表:位图结构;比特位的位置表示哪一个信号,比特位的内容表示是否被阻塞
  3. handler表:函数指针数组;数组下标表示信号编号,数组下标对应的内容表示递达动作

如何理解:第一行中,block是0,pending是0,默认处理动作。第二行中block是1,pending是1,忽略来处理。第三行中block是1,pending是0,捕获信号自定义处理。

4.3 函数操作pending和block表

4.3.1 sigset_t信号集

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态**。阻塞信号集也叫做当**
前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。sigset_t来控制block和pending两个位图。

# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct
 {
 	unsigned long int __val[_SIGSET_NWORDS];
 } __sigset_t;

4.3.2 函数

  1. int sigemptyset(sigset_t *set); //初始化set给出的信号集为空,并从该集合中排除所有信号
  2. int sigfillset(sigset_t *set); //初始化set为full,包括所有信号
  3. int sigaddset(sigset_t *set, int signum); //添加信号符号
  4. int sigdelset(sigset_t *set, int signum); //删除信号符号
  5. int sigismember(const sigset_t *set, int signum); //测试sgn是否是集合的成员
  6. int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); //检查和改变阻塞信号
  • sigprocmask函数

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

how参数可选值功能
SIG_BLOCK添加信号屏蔽字信号,相当于mask = mask | set
SIG_UNBLOCK删除信号屏蔽字信号,相当于mask = mask & ~set
SIG_SETMASK设置信号屏蔽字为set所指向的值,相当于mask = set
  • 使用
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void showNew(sigset_t* newSet)
{
    int signalnum = 1;
    std::cout << "newSet:";
    for(; signalnum <= 31; ++signalnum)
    {
        if(sigismember(newSet, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

void showOld(sigset_t* oldSet)
{
    int signalnum = 1;
    std::cout << "oldSet:";
    for(; signalnum <= 31; ++signalnum)
    {
        if(sigismember(oldSet, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

int main()
{
    //栈操作-->并没有设置进进程
    sigset_t newSet, oldSet;
    sigemptyset(&newSet); //初始化
    sigemptyset(&oldSet);

    sigaddset(&newSet, 2); //将2号信号添加到newSet信号集
    //设置进进程
    sigprocmask(SIG_SETMASK, &newSet, &oldSet); //阻塞信号集被设置为参数集
    int time = 0;
    while(true)
    {
        showNew(&newSet); //这里打印是一直不变的,因为newSet和oldSet并没有改变
        showOld(&oldSet);
        ++time;
        if(time == 10) //不再屏蔽2号信号
        {
            sigprocmask(SIG_SETMASK, &oldSet, &newSet); //把old信号集设置到进程
            
        }
        sleep(1);
    }

    return 0;
}

4.3.3 sigpending未决信号集

  1. 函数:int sigpending(sigset_t *set);//检测未决信号(set参数是输出型参数)
#include <iostream>
#include <signal.h>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>


//任务:屏蔽二号信号不断获取进程pending信号集并不断打印,发送二号信号观察pending信号集变化并解出二号信号阻塞,递达处理动作是自定义动作

static void printtPending(const sigset_t &pending)
{
    std::cout << "PID:" << getpid() << " pending: ";
    for(int signalnum = 1; signalnum <= 31; ++signalnum)
    {
        if(sigismember(&pending, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

static void handler(int signalnum)
{
    std::cout << "catched:" << signalnum << std::endl;
}

int main()
{
    sigset_t newSet, oldSet;
    //初始化
    sigemptyset(&newSet);
    sigemptyset(&oldSet);
    //信号集中设置2信号
    sigaddset(&newSet, SIGINT); 
    //信号屏蔽字设置进进程中
    sigprocmask(SIG_BLOCK, &newSet, &oldSet);
    //获取进程pending信号集并打印
    int count = 0;
    signal(SIGINT, handler); //2号信号捕捉后执行自定义动作
    while(true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        //获取pending信号集
        int ret = sigpending(&pending);
        assert(ret == 0);
        (void)ret;
        //打印
        printtPending(pending);
        //解出对2号信号的屏蔽
        if(count++ == 10) 
        {
            std::cout << "SIGINT signal unblocked!" << std::endl;
            sigprocmask(SIG_SETMASK, &oldSet, nullptr); //对2号信号解出阻塞后,默认递达动作时终止进程
        }
        sleep(1);
    }

    return 0;
}

运行结果

现象描述:给进程发送2号信号后,并没有采取信号默认处理方式而是处于信号未决状态,也就是本来没发送信号,此时发送2号信号后,屏蔽信号集中2号信号被设置,所以进程收到2号信号时被阻塞了,也就是处于未决状态没有递达,所以此时pending信号集的第2个比特位变成了1,过了10秒后信号递达先是解除2号信号阻塞,然后执行自定义处理动作,随后打印解出2号信号后的pending信号集。

5. 捕捉信号

5.1 引出

生活中,当我们和某个人说的十分重要的事的时候突然来了个电话,我们不会去立即处理,当和这个人说完事后再回电话处理。那么信号会被立即处理吗?也可能不会,但是当一个信号解除了阻塞状态时,就会立即递达。这里需要引出的问题就是,什么时候合适解出阻塞状态呢?正是进程从内核态用户态的时候,进程会在OS指导下进行信号的检测和处理(处理三种方式:默认、忽略、自定义行为处理)。用户态是执行用户的代码进程所处的状态,内核态是执行内核的代码进程所处的状态,这句话什么意思呢?我们在linux上写代码的时候往往会调用系统调用接口,这些系统接口是Linux操作系统中的代码来封装得到的,那么当我们写代码的时候就会会执行内核中的代码。 那么再回顾地址空间:

  1. 所有进程的虚拟地址空间[0GB, 3GB]是不同的,每个进程都有自己的用户级页表
  2. 所有进程的虚拟地址空间[3GB, 4GB]是不同的,每个进程都有相同的内核级页表
  3. OS运行的本质:都是在进程的虚拟地址空间运行
  4. 系统调用的本质:在进程自身地址空间中进行函数跳转并返回即可
  5. OS本质?1.OS是软件,是systemd进程,只是这个进程是死循环 2. OS时钟每个很短时间给OS发送时钟中断,OS执行对应中断处理方法来检测当前进程时钟中断。进程如何被调度?时间片到了,进程对应的上下文等等保存并切换,选择合适用的进程(进程调度就是一个系统函数schedule()来完成的)
  • 问题:既然进程中包含内核地址空间和用户地址空间,那么一个进程不就可以随意访问内核中的代码和数据吗?

这里为了解决这个问题就有了内核态和用户态的出现,怎么来识别身份的呢?CPU中有CR3寄存器,其中3表示用户态,0表示内核态,这里身份切换并不是我们用户来完成的,用户无法更改,所以,OS中的系统调用内部中会修改执行级别,这样就能进行访问内核中的代码了。

5.2 信号捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码
是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行
main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号
SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler
和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返
回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复
main函数的上下文继续执行了。信号捕捉中用户态和内核态状态转换有四次转换

5.3 sigaction

  • 函数:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //检测和更改信号,如果act不为nullptr,signum被设置到act中,如果oldact不为nullptr,之前的act会被保存到oldact中

oldact是输出型参数,act是输入型参数。其中struct sigaction结构体:

struct sigaction {
	void     (*sa_handler)(int);
    //sa_handler指定与signum相关联的操作,默认操作可以是SIG_DFL,忽略该信号的SIG_IGN,或者指向信号的指针处理函数。
	void     (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;
    //sa_mask指定在execu‐期间应该被阻塞的信号的掩码(即,添加到调用信号处理程序的线程的信号掩码中)信号处理程序的连接。此外,触发处理程序的信号将被阻塞,除非使用了SA_NODEFER标志。
	int        sa_flags;
    //sa_flags指定一组修改信号行为的标志
	void     (*sa_restorer)(void);
};
  • 使用
#include <iostream>
#include <cstring>
#include <csignal>
#include <cassert>
#include <unistd.h>

static void printtPending(const sigset_t &pending)
{
    std::cout << "PID:" << getpid() << " pending: ";
    for(int signalnum = 1; signalnum <= 31; ++signalnum)
    {
        if(sigismember(&pending, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

//现象:2/3/4/5号信号都被block了
//第一次发送2号信号,此时2号信号正在被自定义处理,在此期间如果再发送2号信号,此时再发送的2号信号就会被暂存处于pending状态,3/4/5号信号也会暂存
//pending信号集是在执行handler之前被置零的

static void handler(int signalnum)
{
    printf("PID:%d catched signalnum:%d\n", getpid(), signalnum);
    int time = 30;
    while(time--)
    {
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);
        printtPending(pending);
        sleep(2);
    }
}

int main()
{
    std::cout << "Process PID: " << getpid() << std::endl;
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act)); //初始化
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = handler; //对2信号递达后采用自定义处理动作
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask); //初始化

    sigaddset(&act.sa_mask, SIGQUIT); //3号信号屏蔽
    sigaddset(&act.sa_mask, SIGILL); //4号信号屏蔽
    sigaddset(&act.sa_mask, SIGTRAP); //5号信号屏蔽

    int ret = sigaction(SIGINT, &act, &oldact); //检测2号信号 --> 等价于signla(SIGINT)
    assert(ret == 0);
    (void)ret;
    while(true)
    {
        sleep(1);
    }
}

运行截图

6. 其他知识

6.1 可重入函数

#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signalnum);

typedef struct singleLinkListNode
{
    struct singleLinkListNode* _next;
    int _val;

    singleLinkListNode(const int& val)
        :_val(val)
    {
        _next = nullptr;
    }
}node;

node* head = new node(0);
node node1(1), node2(2);

void printLink(node* phead)
{
    node* cur = phead;
    while(cur)
    {
        printf("node:%d->", cur->_val);
        cur = cur->_next;
    }
    std::cout << "nullptr" << std::endl;
}

void insert(node* newnode)
{
    newnode->_next = head;
    std::cout << "wait signal......." << std::endl;
    sleep(10);
     //10秒期间发送2号信号让其递达执行自定义处理动作
    head = newnode;
}

void handler(int signalnum)
{
    printf("PID:%d, call handler!\n", getpid());
    insert(&node2);
}

int main()
{   
    printf("Process PID:%d\n", getpid());
    signal(SIGINT, handler);
    insert(&node1);
    
    std::cout << "head: ";
    printLink(head);
    std::cout << "node1: ";
    printLink(&node1);
    std::cout << "node2: ";
    printLink(&node2);

    return 0;
}
  1. 画图理解

运行结果:

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

  • 符合以下条件之一则是不可重入
  1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的
  2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

6.2 volatile关键字

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

int quit = 0;

void handler(int signalnum)
{
    printf("change quit form 0 to 1\n");
    quit = 1;
}

int main()
{
    printf("Process PID:%d\n", getpid());
    signal(SIGINT, handler);

    while(!quit); //欺骗编译器

    printf("normal exit!\n");

    return 0;
}

//makefile
valatile_keyword:valatile_keyword.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f valatile_keyword

运行结果:

image-20230510200542633

其实gcc编译器有很多优化选项:-O1、-O2、-O3、-O0 (man gcc查找):

下面换用-O1优化选项进行编译,运行截图:

  • 为什么这里-O2选项优化后发送2号信号并不会终止进程呢?

首先要知道上面代码哪里优化了,其实这里while(!quit)这个语句时别优化了,如何优化呢?CPU执行运算的时候,quit初始值为0,那么0就被Load到寄存器中,此时寄存器就是0值,当发送2号信号,quit被赋值变成1,但是这里寄存器中的值并随之改变,所以一直死循环。这里就是一个内存位置不可见的问题,怎么来解决这个问题呢?告诉编译器保证每次检测都要从内存中读取数据,不要让内存数据不可见。解决方法:变量前加上volatile关键字。volatile关键字作用:保证内存可见性

6.3 SIGCHLD信号

引出:子进程退出,父进程如何得知的呢?父进程阻塞式等待或者非阻塞式等待都需要父进程主动检测,其实子进程退出的时候会向父进程发送SIGCHLD信号,父进程收到信号采用的是忽略的处理方式。验证SIGCHLD信号:

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

pid_t id;

void handler(int signalnum)
{
    sleep(1);
    printf("catched a signal:%d, who:%d\n", signalnum, getpid());
    pid_t result = waitpid(-1, nullptr, 0); //等待任意子进程
    
    if(result > 0)
    {
        printf("wait success!, result:%d, id:%d\n", result, id);
    }
}

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

    id = fork();
    if(id == 0)
    {
        int time = 5;
        while(time--)
        {
            printf("child process, PID:%d, PPID:%d\n", getpid(), getppid());
            sleep(1);
        }

        exit(1);
    }

    while(true)
    {
        sleep(1);
    }

    return 0;
}

运行结果:(监控脚本:examine.sh,使用:bash examine.sh)

场景:假如如果有多个子进程同时退出呢?多个子进程同时退出会发送多个SIGCHLD信号,但是这里父进程的信号集中的SIGCHLD信号只有一个比特位来标记,所以此时就需要循环等待子进程来回收所有子进程(基于信号回收进程):

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

pid_t id;

void handler(int signalnum)
{
    sleep(1);
    printf("catched a signal:%d, who:%d\n", signalnum, getpid());
    while (true) //循环回收
    {
        pid_t result = waitpid(-1, nullptr, WNOHANG); //等待回收子进程

        if (result > 0)
        {
            printf("wait success!, result:%d, id:%d\n", result, id);
        }
        else
        {
            break;
        }
    }
    printf("handler done!\n");
}

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

    for (int i = 0; i < 5; ++i) //创建5个子进程
    {
        id = fork();
        if (id == 0)
        {
            int time = 5;
            while (time--)
            {
                printf("child process, PID:%d, PPID:%d\n", getpid(), getppid());
                sleep(1);
            }

            exit(1);
        }
    }
    while (true)
    {
        sleep(1);
    }
    return 0;
}

优雅的处理僵尸进程,直接让操作系统回收,而不是父进程等待回收(只保证Linux下有效):

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

pid_t id;

int main()
{
    signal(SIGCHLD, SIG_IGN); //收到SIGCHLD信号默认处理动作为忽略

    for (int i = 0; i < 5; ++i)
    {
        id = fork();
        if (id == 0)
        {
            int time = 5;
            while (time--)
            {
                printf("child process, PID:%d, PPID:%d\n", getpid(), getppid());
                sleep(1);
            }

            exit(1);
        }
    }
    while (true)
    {
        sleep(1);
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/527130.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Raspberry Pi OS 2023-05-03 版本发布

导读树莓派 Raspberry Pi Foundation 今天发布了用于 Raspberry Pi 计算机的官方 Raspberry Pi OS 发行版新版本&#xff0c;带来了更新的组件、错误 Bug 修复和各种性能改进。 Raspberry Pi OS 2023-05-03 版本最大变化是内核从长期支持的 Linux 5.15 LTS 到长期支持的 Linux …

【为什么可以相信一个HTTPS网站】

解决信用&#xff0c;仅仅有加密和解密是不够的。加密解密解决的只是传输链路的安全问题&#xff0c;相当于两个人说话不被窃听。可以类比成你现在生活 的世界——货币的信用&#xff0c;是由政府在背后支撑的&#xff1b;购房贷款的信用&#xff0c;是由银行在背后支撑的&…

### Cause: dm.jdbc.driver.DMException: 列[URI]长度超出定义

### Cause: dm.jdbc.driver.DMException: 列[URI]长度超出定义 报错信息&#xff1a; ### Cause: dm.jdbc.driver.DMException: 列[URI]长度超出定义 ; 列[URI]长度超出定义; nested exception is dm.jdbc.driver.DMException: 列[URI]长度超出定义at org.springframework.jdb…

计算机毕业论文内容参考|基于Python的互联网金融产品交易平台的设计与实现

文章目录 导文摘要前言绪论课题背景国内外现状与趋势课题内容相关技术与方法介绍技术分析需求分析技术分析技术设计系统架构技术实现产品管理模块订单管理模块支付管理模块总结与展望导文 基于Python的互联网金融产品交易平台的设计与实现 摘要 本文提出并实现了一种基于Pytho…

《计算机网络—自顶向下方法》 Wireshark实验(六):IP 协议分析

IP 协议&#xff08;Internet Protocol&#xff09;&#xff0c;又译为网际协议或互联网协议&#xff0c;是用在 TCP/IP 协议簇中的网络层协议。主要功能是无连接数据报传送、数据报路由选择和差错控制。IP 协议是 TCP/IP 协议族的核心协议&#xff0c;其主要包含两个方面&…

Vue入门学习笔记:TodoList(三):实例中的数据、事件和方法

目录&#xff1a; Vue入门学习笔记&#xff1a;TodoList&#xff08;一&#xff09;&#xff1a;HelloWorld Vue入门学习笔记&#xff1a;TodoList&#xff08;二&#xff09;&#xff1a;挂载点、模板、实例 Vue入门学习笔记&#xff1a;TodoList&#xff08;三&#xff09;&a…

有关浪涌保护器的基础知识

浪涌和瞬态电压浪涌是电路上电压和电流的临时上升。它们的电压范围大于 2000 伏&#xff0c;电流范围大于 100 安培。典型的上升时间在1到10微秒范围内。瞬态或浪涌是最常见的电源问题&#xff0c;其瞬时破坏性会造成重大损坏&#xff0c;例如电气或电子设备故障、频繁停机、数…

MySQL正则表达式与存储过程

一、正则表达式&#xff08;REGEXP&#xff09; 1、正则表达式匹配符 字符解释举列^匹配文本的开始字符’ ^aa ’ 匹配以 aa 开头的字符串$匹配文本的结束字符’ aa$ ’ 匹配以aa结尾的字符串.匹配任何单个字符’ a.b 匹配任何a和b之间有一个字符的字符串*匹配零个或多个在它…

Webpack4 核心概念

文章目录 Webpack4 核心概念概述安装webpack简单使用配置webpack.config.js配置package.json打包 核心概念modeentry 和 outputentry配置说明output配置说明配置单入口配置多入口 loader安装babel、babel-loader、core-js配置.babelrc配置webpack.config.js配置package.json编写…

107.(cesium篇)cesium椎体上下跳动

听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行完整代码包,运行如有问题,可“私信”博主。 效果如下所示: 下面献上完整代码,代码重要位置会做相应解释 <html lang="en"> <

等保2.0常见问题汇总

等保2.0发布后&#xff0c;整个网络安全行业积极学习并按照新规范进行网络系统的部署。然而许多人仍然对等保制度的来历、发展、演变以及贯彻重点存在疑问。等保2.0依然在整个实施流程上由五个标准环节构成&#xff1a;定级、备案、建设整改、等级测评、监督检查五个方面。本文…

Science文章复现(Python):图1 - Aircraft obs(机载的观测 CO2)

之前有写过science文章后处理的复现Science文章复现&#xff08;Python&#xff09;&#xff1a;在机载观测中明显的强烈南大洋碳吸收 在这里是针对图细节的理解&#xff1a; 首先需要下载这个项目 https://github.com/NCAR/so-co2-airborne-obs 这里的环境配置会比较麻烦 con…

Java设计模式 13-命令模式

命令模式 一、智能生活项目需求 看一个具体的需求 1)我们买了一套智能家电&#xff0c;有照明灯、风扇、冰箱、洗衣机&#xff0c;我们只要在手机上安装 app 就可以控制对这些家电工作。 2)这些智能家电来自不同的厂家&#xff0c;我们不想针对每一种家电都安装一个 App&…

怎样实现RPC框架

随着微服务架构的盛行&#xff0c;远程调用成了开发微服务必不可少的能力&#xff0c;RPC 框架作为微服务体系的底层支撑&#xff0c;也成了日常开发的必备工具。当下&#xff0c;RPC 框架已经不仅是进行远程调用的基础工具&#xff0c;还需要提供路由、服务发现、负载均衡、容…

两种QGraphicsItem方式实现橡皮擦功能(矩形选中框)

方法一&#xff1a;继承QGraphicsItem实现橡皮擦功能。&#xff08;gif中红色矩形框&#xff09; 方法二&#xff1a;继承QGraphicsRectItem实现的橡皮擦功能。&#xff08;gif中蓝色矩形框&#xff09; 通过以上GIF可以看出两款橡皮擦都具有位置拖动和大小拖动的功能&#xff…

Nacos必知必会:这些知识点你一定要掌握!

前言 Nacos 是一个开源的服务发现、配置管理和服务治理平台&#xff0c;是阿里巴巴开源的一款产品。 Nacos 可以帮助开发者更好地管理微服务架构中的服务注册、配置和发现等问题&#xff0c;提高系统的可靠性和可维护性。 本文将介绍 Nacos 的必知必会知识点&#xff0c;包括…

Zookeeper基础和简单使用

安装与配置 概念 基于观察者模式设计的分布式服务管理框架&#xff0c;负责存储和管理大家都关心数据&#xff0c;然后接受观察者的注册&#xff0c;一单这些数据的这状态发生了变化&#xff0c;Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应 特点…

当JS遇到加密:解密你的JS代码!

作为一名程序员&#xff0c;我们经常会遇到各种加密算法&#xff0c;比如常见的AES、RSA、MD5等等&#xff0c;但是今天我想和大家聊一聊一个日常生活中比较常见的加密方式——JavaScript加密。 在我们日常浏览网页时&#xff0c;经常会看到一些网站的JavaScript代码经过加密&…

【Redis】布隆过滤器原理与应用

文章目录 原理应用实战总结 布隆过滤器&#xff08;Bloom Filter&#xff09;是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。 它的优点是空间效率和查询时间都比一般的算法要好的多&#xff…

AJAX实现搜索联想 自动补全

分析: 1.想实现联想搜索需要数据库的数据支撑,需要进行模糊查询,搜索出所有包含用户输入的关键字信息,并将这些信息都反馈到前端,简化用户输入,从而提高用户的体验。 2.为了提高用户的使用体验&#xff0c;整个页面不能全部刷新&#xff0c;需要局部刷新&#xff0c;为此需要…