lesson7:进程信号

1. 进程信号 

1.1 什么是Linux信号 

  • 本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,但不是需要立即处理,你可以在后续进行处理

结合进程->信号结论

  1.  进程要处理信号,必须具备信号"识别"的能力(看到 + 处理动作)
  2. 为什么进程能够"识别"信号呢?程序员做的
  3. 信号产生是随机的,进程可能正在忙自己的事情,所以信号的后续处理,可能不是立即处理的!
  4. 信号会临时的记录下对应的信号,方便后续进行处理
  5. 在什么时候处理呢?合适的时候
  6. 一般而言,信号的产生相对于进程而言是异步的(两者同时发生)

1.2 信号如何产生

  • ctrl+c:本质就是通过键盘组合键向目标进程发送2号信号 -> 进程退出了
  • OS解释组合键->查找进程列表->前台运行的进程->OS写入对应的信号到进程的内部的位图结构中

信号是如何记录的?

实际上,当一个进程接收到某种信号后,该信号是被记录在该进程的进程控制块当中的。我们都知道进程控制块本质上就是一个结构体变量,而对于信号来说我们主要就是记录某种信号是否产生,因此,我们可以用一个32位的位图来记录信号是否产生。 

 

  • 就是进程的PCB内部保存了信号的位图字段

如何理解信号发送的本质

  • 信号位图是在task_struct->task_struct内核数据结构->OS
  • OS向目标进程写信号,OS直接修改PCB中的指定的位图结构,完成"发送"信号的过程

1.3 信号处理的常见方式:

  1. 默认(进程2自带的,程序员写好的逻辑)
  2. 忽略(也是信号处理的一种方式)
  3. 自定义动作(捕捉信号) 

1.4 常见信号->kill -l

  •  kill -l查看进程,[1,31]都是普通信号,[34,64]都是实时信号

1.5 案例:模拟实现信号

 

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
using namespace std;
 void catchSig(int signum)
 {
     cout << "进程捕捉到了一个信号,正在处理中: " << signum <<"Pid: " << getpid()<<endl;
 }
int main()
{
    // signal(2, fun);
    // 键盘
    signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个
    while(true)
    {
        cout << "我是一个进程,我正在运行..., Pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

 1.6 对系统调用接口的理解

  • 用户调用系统接口->执行OS对应的系统调用代码->OS提取参数,或者设置特定的数值
  • ->OS向目标进程写信号->修改对应进程的信号标记位->进程后续会处理信号->执行对应的处理动作!

1.7 由软件条件产生信号

  • 如果管道的读端不光不读而且还关闭了,写端一直在写,
  • 就会导致写没有意义,OS会自动终止对应的写端进程,通过发送信号的方式,SIGPIPE 13
  •  理解: a. OS先识别到某种软件条件触发或者不满足 b.OS 构建信号,发送给指定的进程

1.8 硬件异常产生信号

理解除0错误

  1. 进行计算的是CPU,这个硬件
  2. CPU内部是有寄存器的,状态寄存器(位图),有对应的状态标记位,溢出标记位,OS会自动进行计算完毕之后的检测,
    如果溢出标记位是1,OS里面识别到有溢出问题,立即找到当前谁在运行提取PID,OS完成信号发送的过程,进程会在合适的时候进行处理

一旦出现硬件异常,进程不一定会退出(虽然一般默认是退出的)

一个程序中发生了死循环,主要是因为寄存器中的异常一直没有被解决

 1.9 理解野指针或者越界问题

  1. 都必须通过地址,找到目标位置
  2. 我们语言上面的地址,全部都是虚拟地址
  3. 将虚拟地址转换成物理地址
  4. 页表+MMU(Memory Manager Unit,硬件!!)
  5. 野指针越界 -> 非法地址 -> MMU转化的时候,一定会报错

对信号的总结: 所有的信号,都有他的来源,但最终全部都是被OS识别,解释,并发送的! 

1.10 理解信号处理的合适时候

  •  合适的时候:从内核态返回用户态的时候

用户级页表 && 内核级页表 

  •  进程地址空间映射到物理内存中的页表是有2种的
  • 一种是用户级页表,一种是内核级页表
  • 通过是处于内核态,还是用户态,我们可以执行OS的代码,比如执行进程切换的时候,
    • CPU的寄存器有2套,一套可见,一套CPU不可见,自用
    • CR3 -> 表示当前CPU的执行权限 1内核,3用户态

1.11 两种不能被忽略 && 杀死的信号

  •  SIGKILLSIGSTOP

2. 阻塞信号

2.1 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意: 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2.2 在内核中的表示

2.3 理解sigset_t类型

  • sigset_t是一个系统类型,就像pid_t一样
  •  sigset_t---> 不允许用户自己进行位操作,OS给我们提供了对应的操纵位图的方法
  • sigset_t---> user是可以直接使用该类型,和用内置类型 && 自定义类型没有任何差别
  • sigset_t---> 一定需要对应的系统接口,来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象

 2.4 案例

2.4.1 问题一: 对所有信号都进行了自定义捕捉,那我们是不是就写了一个不会被异常或者用户杀掉的进程??

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

void catchSig(int signum)
{
    std::cout << "获取一个信号: " << signum << std::endl;
}

int main()
{
    // 9号进程无法被屏蔽或者阻塞
    for(int sig = 1; sig <= 31; sig++) signal(sig, catchSig);

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

  •  9号进程无法被屏蔽或者阻塞,所以不存在一个无法被用户杀死的进程

2.4.2 问题二:将2号信号block,并且不断的获取并打印当前进程的pending信号集,然后再突然发送一个2号信号,会发生什么?? 

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void handler(int signum)
{
    std::cout << "捕捉 信号: " << signum << std::endl;
    // 不要终止进程,exit
}

static void showPending(sigset_t& pending)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(&pending, sig))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << std::endl;
}

int main()
{
    // 0. 方便测试,捕捉2号信号,不要退出
    signal(2, handler);

    // 1. 定义信号集对象
    sigset_t bset, obset;
    sigset_t pending;

    // 2. 初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);

    // 3. 添加要进行屏蔽的信号
    sigaddset(&bset, 2 /*SIGINT*/);

    // 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
    int n = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(n == 0);
    (void)n;
    std::cout << "block 2 号信号成功...., pid: " << getpid() << std::endl;
   
    // 5. 重复打印当前进程的pending信号集
    int count = 0;
    while (true)
    {
        // 5.1 获取当前进程的pending信号集
        sigpending(&pending);
        // 5.2 显示pending信号集中的没有被递达的信号
        showPending(pending);
        sleep(1);
        count++;
        if (count == 20)
        {
            // 默认情况下,恢复对于2号信号的block的时候,确实会进行递达
            // 但是2号信号的默认处理动作是终止进程!
            // 所以需要对2号信号进行捕捉
            std::cout << "解除对于2号信号的block" << std::endl;
            int n = sigprocmask(SIG_SETMASK, &obset, nullptr);
            assert(n == 0);
            (void)n;
        }
    }
    return 0;
}

2.4.3 问题三:对所有信号都进程block,我们是不是就写了一个不会被异常或者用户杀掉的进程??

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void showPending(sigset_t& pending)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(&pending, sig))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << std::endl;
}

static void blockSig(int sig)
{
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, sig);
    int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
    assert(n == 0);
    (void)n;
}

int main()
{
     for (int sig = 1; sig <= 31; sig++)
    {
        blockSig(sig);
    }
    sigset_t pending;
    while (true)
    {
        sigpending(&pending);
        showPending(pending);
        sleep(1);
    }
    return 0;
}
#!/bin/bash

i=1
id=$(pidof signal)
while [ $i -le 31 ]
do
    if [ $i -eq 9 ];then
        let i++
        continue
    fi
    if [ $i -eq 19 ];then
        let i++
        continue
    fi
    kill -$i $id
    echo "kill -$i $id"
    let i++
    sleep 1
done

 

  •   9号进程无法被屏蔽或者阻塞
  • 19号进程(SIGSTOP)也无法被屏蔽

3. 捕捉信号 

 

  

 

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;

void showPending(sigset_t *pending)
{
    for(int sig = 1; sig <= 31; sig++)
    {
        if(sigismember(pending, sig)) cout << "1";
        else cout << "0";
    }
    cout << endl;
}

void handler(int signum)
{
    cout << "获取了一个信号: " << signum << endl;

    sigset_t pending;
    int c = 20;
    while(true)
    {
        sigpending(&pending);
        showPending(&pending);
        c--;
        if(!c) break;
        sleep(1);
    }
}

int main()
{
    // signal(2, SIG_IGN);
    cout << "getpid: " << getpid() << endl; 
    // 内核数据类型,用户栈定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;

    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaddset(&act.sa_mask, 6);
    sigaddset(&act.sa_mask, 7);

    // 设置进当前调用进程的pcb中
    sigaction(2, &act, &oact);

    cout << "default action : " << (oact.sa_handler) << endl;
    while(true) sleep(1);
    return 0;
}

  •  处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,OS将会block

 4. 核心转储 && 验证进程等待中的core dump标记位

4.1  获取子进程status

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
int main()
{
    
    pid_t id = fork();
     if(id == 0)
     {
         sleep(1);
         int a = 100;
         a /= 0;
         exit(0);
     }

     int status = 0;
     waitpid(id, &status, 0);
     cout << "父进程:" << getpid() << " 子进程:" << id << \ 
     " exit sig: " << (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl;
    return 0;
}

  •  子进程的status中的core dump标志,是否发生了核心转储
  • 当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中!
  • core主要引用在调式的时候,还会形成一个新文件(保存核心数据),这里我不好演示
  • 一般而言,云服务器(生产环境)的核心转储功能是被关闭的

5. 使用alarm信号闹钟验证1s之内会进行多少次count++ 

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

using namespace std;

 typedef function<void ()> func;
 vector<func> callbacks;

 uint64_t count = 0;

 void showCount()
 {
     // cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
     cout << "final count : " << count << endl;
 }
 void showLog()
 {
     cout << "这个是日志功能" << endl;
 }
 void logUser()
 {
     if(fork() == 0)
     {
         execl("/usr/bin/who", "who", nullptr);
         exit(1);
     }
     wait(nullptr);
 }

 // 定时器功能
 // sig: 
 void catchSig(int signum)
 {
     for(auto &f : callbacks)
     {
         f();
     }
     //alarm(1);
 }

 static void Usage(string proc)
 {
     cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
 }

void handler(int signum)
{
    sleep(1);
    cout << "获得了一个信号: " << signum << endl;
     exit(1);
}

int main(int argc, char* argv[])
{
     signal(SIGALRM, catchSig);
     alarm(1); // 设定了一个闹钟,这个闹钟一旦触发,就自动移除了

     callbacks.push_back(showCount);
     callbacks.push_back(showLog);
     callbacks.push_back(logUser);
     while(true) count++;

    return 0;
}
  • 如果只计算到1w多,那么主要是因为cout + 网络发送 = IO

6. 可重入函数&不可重入函数

可重入函数:

  • 在多线程的程序当中,如果一个函数可能同一时刻被多个线程访问,没有导致程序的结果产生二义性,则该函数被称之为可重入函数

 不可重入函数

  • 在多线程的程序当中,如果一个函数可能同一时刻被多个线程访问,从而导致程序的结果产生二义性,则该函数被称之为不可重入函数

6.1 理解volatile类型

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;

int flag = 0;

void changeFlag(int signum)
{
    (void)signum;
    cout <<"change flag: "<< flag;
    flag = 1;
    cout << "->" << flag << endl;
}

int main()
{
    signal(2, changeFlag);
    while(!flag);
    cout << "进程正常退出后:" << flag << endl;
}

  • 没加优化之前,CPU都是在内存中拿数据
  • 加了优化之后,内存中的数据会放在edx中,之后CPU只向edx里面拿数据
    就导致每次拿的都是1无法正常退出,
volatile int flag = 0;
  • 保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量
    的任何操作,都必须在真实的内存中进行操作

7. SIGCHLD信号(了解)

7.1 证明子进程退出会向父进程发送信号

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;

void handler(int signum)
{
    cout << "子进程退出: " << signum << " fater: " << getpid() << endl;
}

// 证明 子进程退出,会想父进程发送信号
int main()
{
    signal(SIGCHLD, handler);
    if (fork() == 0)
    {
        cout << "child pid: " << getpid() << endl;
        sleep(1);
        exit(0);
    }

    while (true) sleep(1);

}

7.2 在子进程退出后,不等待子进程,让它自动释放僵尸进程

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;

// 如果我们不想等待子进程,并且我们还想让子进程退出之后,自动释放僵尸子进程
int main()
{
    // OS 默认就是忽略的
    signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略

    if(fork() == 0)
    {
        cout << "child: " << getpid() << endl;
        sleep(5);
        exit(0);
    }

    while(true)
    {
        cout << "parent: " << getpid() << " 执行我自己的任务!" << endl;
        sleep(1);
    }
}
  • signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值