Linux -- 进程信号

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;
}
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

脚踏车(crush)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值