Linux信号

一、预备:中断当前正在做的事情,是一种事件的异步通知机制,当前正在做的事情为进程,信号到来时进程就要中断正在做的事情,信号就是一种给进程发送的用来进行事件的异步通知机制。

信号的产生相对于进程的运行是异步的,各做各的互不干扰

基本结论:

1.信号处理在信号产生的时候就知道信号要如何处理了。

2.信号的处理不是立即处理,可以在合适的时候进行信号的处理。

3.人能处理信号是被提前教育过的,所以进程是OS程序员设计的,早已内置了对信号的识别和处理机制。

4.信号源多

二、产生信号:

1.信号产生:

键盘产生信号

ctrl+c对于相当一部分进程来说就是终止

系统调用产生信号

函数产生信号

由软件条件产生信号

异常产生信号

2收到信号处理方式:

默认处理动作

自定义处理动作

忽略处理

3.4.更改进程的默认处理动作:

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

void handlerSig(int sig)
{
    std::cout << "获得了一个信号" << sig << std::endl;
}

int main()
{
    signal(SIGINT, handlerSig);//收到2号信号时转为handlerSig函数

    int cnt = 0;
    while (true)
    {
        std::cout << "hello world " << cnt++ << std::endl;
        sleep(1);
    }

    return 0;
}

ctrl+c时不会停下来了,是因为./XXX& 表示后台进程,ctrl+c无法处理键盘产生的信号,键盘产生的信号只能发给前台进程,后台进程无法获取从标准输入流中的内容,但是都可以向标准输出打印。键盘只有一个,输入数据一定是要给一个确定的进程的。后台进程可以有多个,前台进程的本质就是为了从键盘获取数据。

前后台移动:

jobs:查看所以后台任务。

fg:任务号,特定的进程,提到前台。

ctrl+z:进程切换到后台。

bg+任务号:让后台进行任务回复运行。

4.信号记录:

信号产生不是立即处理就要记录下来。

记录在哪?如何记录?

信号发给进程,在PCB里定义一个unsigend int sigs,用整数记录这个信号,但如果是要发好多个,一个整数不够记录,就把这个整数当作位图结构,一共32个比特位,规定比特位的位置表示信号编号,比特位的内容表示是否收到,比如收到2号信号,就把第二位比特位由0置1,记录在进程的task_struct里。所以发送信号本质是向目标进程写入信号。task_struct属于操作系统内的数据结构,修改位图本质是修改内核数据。不管信号怎么产生,发送信号在底层都必须让OS发。kill底层调用了系统调用。

写一个kill命令向当前进程发信号:

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

void handlerSig(int sig)
{
    std::cout << "获得了一个信号" << sig << std::endl;
}

int main()
{
    signal(SIGINT, handlerSig); // 收到2号信号时转为handlerSig函数

    int cnt = 0;
    while (true)
    {
        std::cout << "hello world " << cnt++ << " , pid: " << std::endl;
        sleep(1);
    }

    return 0;
}
cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char *argv[]) // 需要有命令行参数,一个是信号编号signum,一个是目标进程target
{
    if (argc != 3)
    {
        std::cout << "./mykill signumber pid" << std::endl;
        return 1;
    }
    int signum = std::stoi(argv[1]); // 由字符串转为整数
    pid_t target = std::stoi(argv[2]);

    int n = kill(target, signum); // 成功返回0,错误返回-1
    if (n == 0)
    {
        std::cout << "send" << signum << "to" << target << "success.";
    }

    return 0;
}

9号信号无法被自定义捕捉,防止有恶意程序。

异常产生信号,进程收到8号信号产生错误崩溃。

6.alarm信号:告诉内核数秒后向进程发送14号信号,默认作用是终止当前进程

基于闹钟写一个程序,一开始啥也不做,有了闹钟之后醒来一次就执行某种行为

cpp 复制代码
void handlerSig(int sig)
{
    std::cout << "获得了一个信号" << sig << std::endl;
}

int main()
{
    signal(SIGALRM, handlerSig); // 收到14号信号时转为handlerSig函数
    alarm(1);

    while (true)
    {
        std::cout << "." << std::endl;
        sleep(1);
    }

    return 0;
}

过了一秒钟收到14号信号但是之后就没有闹钟了。

解决方法:

cpp 复制代码
void handlerSig(int sig)
{
    std::cout << "获得了一个信号" << sig << "pid: " << getpid() << std::endl;
    alarm(1);
}

在进行信号处理的时候还是同一个进程,一秒钟后收到一个闹钟执行handlerSig函数再收到一个闹钟,就会再执行handlerSig函数,然后又可以收到一个闹钟,依次类推。

这样子就可以让进程每隔一秒就收到一个信号,执行依次自定义捕捉。

pause,等待一个信号,只有当信号被捕捉才会返回

cpp 复制代码
void handlerSig(int sig) // 捕捉信号
{
    std::cout << "获得了一个信号" << sig << "pid: " << getpid() << std::endl;
    alarm(1);
}

int main()
{
    signal(SIGALRM, handlerSig); // 收到14号信号时转为handlerSig函数
    alarm(1);

    while (true)
    {
        // std::cout << ".  " << "pid: " << getpid() << std::endl;
        // sleep(1);

        // 让进程什么都不做,保持暂停状态
        // 一旦来一个信号,就被唤醒一次执行方法
        pause();
    }

    return 0;
}

这样子根据alarm闹钟的信号就可以实现每隔一秒执行我们想要的代码。

cpp 复制代码
// 每隔一秒,完成一些任务
////////func()具体的方法////////
void Sched()
{
    std::cout << "我是进程调度" << std::endl;
}
void MemManger()
{
    std::cout << "我是周期性内存管理,正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
    std::cout << "我是刷新程序,我在定期刷新内存数据到磁盘" << std::endl;
}
///////////////////////////////

using func_t = std::function<void()>; // 定义一种新的函数类型,等价于typedefine
std::vector<func_t> funcs;

void handlerSig(int sig) // 捕捉信号
{
    // std::cout << "获得了一个信号" << sig << "pid: " << getpid() << std::endl;
    std::cout << "##############################" << std::endl;
    for (auto f : funcs)
        f();
    std::cout << "##############################" << std::endl;
    alarm(1);
}

int main()
{
    funcs.push_back(Sched);
    funcs.push_back(MemManger);
    funcs.push_back(Fflush);
    signal(SIGALRM, handlerSig); // 收到14号信号时转为handlerSig函数
    alarm(1);

    while (true)
    {
        // std::cout << ".  " << "pid: " << getpid() << std::endl;
        // sleep(1);

        // 让进程什么都不做,保持暂停状态
        // 一旦来一个信号,就被唤醒一次执行方法
        pause();
    }

    return 0;
}

这也是操作系统的原理,受信号的指令而驱动。

操作系统可能同时存在很多闹钟,所以要管理闹钟,先描述再组织

三、保存:

cpp 复制代码
int main()
{

    signal(2, SIG_IGN); // 忽略信号

    while (true)
    {
        sleep(1);
        std::cout << ". " << std::endl;
    }

    return 0;
}

写一个样例:屏蔽2号信号,然后不断获取当前进程的pending信号集,在没有获取2号信号时打印应该是全0,突然发一个2号,因为2号信号不会被抵达,所以应该肉眼看到pending里面的一个比特位由0变1

cpp 复制代码
void PrintPending(sigset_t &pending)
{
    printf("我是一个进程(%d),pending:", getpid());

    for (int signo = 31; signo >= 1; signo--) // 信号从第31个开始一直到第1个全部遍历一遍
    {
        if (sigismember(&pending, signo)) // 特定的信号在这个集合里就说明被置为1了
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << std::endl;
}

int main()
{
    // 1.屏蔽2号信号
    sigset_t block, oblock; // 要对2号信号进行屏蔽首先要把位图定义出来然后清空
    sigemptyset(&block);    // 清空位图
    sigemptyset(&oblock);
    sigaddset(&block, SIGINT);                         // 将2号信号添加进位图,此时还没有屏蔽
    int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 把用户空间的位图结构设置到当前调用进程的block表里
    (void)n;                                           // 防止n被定义但没有被使用
    // 4.重复获取打印过程
    while (true)
    {
        // 2.获取pending信号集
        sigset_t pending;
        int m = sigpending(&pending); // 将pending添加进当前调用进程的pending表里

        // 3.打印
        PrintPending(pending);

        sleep(1);
    }

    return 0;
}

四、处理:

捕捉信号的过程:用户态执行代码发现错误异常跳转到内核态处理当前进程可以递送的信号,然后去查pending表,看看有没有signal的调用,如果是自定义就回到用户态进行自定义的信号处理,如果不是就返回代码被中断的地方继续执行。如果是自定义的信号处理,执行完后进行系统调用回到内核态,再回到代码被中断执行的地方继续执行。

内核态和用户态:

五、SIGCHLD信号(了解) :

进程⼀章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了,采用第⼆种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外⼀种办法:父进程调用sigaction将 SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是⼀个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

相关推荐
weixin_446260851 小时前
n8n 工作流集合:解锁自动化新体验!
运维·自动化
淼_@淼1 小时前
pytest简介
运维·服务器·pytest
赖small强1 小时前
【Linux C/C++开发】第26章:系统级综合项目理论
linux·c语言·c++
SCandL1521 小时前
安全上下文的修改实验
linux
ragnwang1 小时前
Ubuntu /home 分区安全扩容教程
linux·运维·ubuntu
Azure++2 小时前
Centos安装clickhouse
linux·clickhouse·centos
濊繵2 小时前
Linux网络--应用层自定义协议与序列化
linux·服务器·网络
zt1985q2 小时前
本地部署 Jupyter 并实现外部访问(Windows 版本)
运维·服务器·windows
fpcc2 小时前
跟我学C++中级篇——重载问题分析之函数模板重载的问题
c++