【Linux系统编程】18. Linux进程信号(上)

文章目录

一、初识信号

1、一个样例

信号是一种给进程发送的,用来进行事件异步通知的机制。

样例:

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

int main()
{
    while(true)
    {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果:

我们按下Ctrl+C后,键盘输⼊产⽣⼀个硬件中断,被OS获取、解释成信号、发送给⽬标前台进程。 前台进程因为收到信号,根据信号做出相应的处理,这里的处理是进程退出

2、signal 函数

Ctrl+C 是向前台进程发送 SIGINT(2 )信号。

证明如下:

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

void handler(int sig)
{
    std::cout << "我是: " << getpid() << ",我获得了一个信号: " << sig << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGINT, handler);

    while(true)
    {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果:

3、信号处理

我们可以通过以下指令查看信号:kill -l

收到信号后的处理动作有以下三种:忽略处理(SIG_IGN)、默认处理(SIG_DFL)、自定义处理(handler)

这里以Ctrl+C产生的SIGINT信号举例:

  • SIGINT的默认处理是SIG_DFL,也就是说当我们按下Ctrl+C,默认进程就会被终止。
  • 如果将SIGINT的处理动作设置为SIG_IGN,那么当我们按下Ctrl+C,不会对进程产生任何影响,也就是忽略这个信号的意思。
  • 如果将SIGINT的处理动作设置为自定义处理,我们就需要自己定义一个处理函数,例如handler,当我们按下Ctrl+C时,系统就会默认执行handler这个函数,这个特性很重要!!!

二、产⽣信号

产生信号大体分为4种方式,分别是键盘按键、系统调用、软件条件、硬件异常。

1、键盘按键产生信号

1)基本操作

  • Ctrl+C(SIGINT 2):终止进程
  • Ctrl+\(SIGQUIT 3):终止进程并⽣成core dump⽂件。
  • Ctrl+Z(SIGTSTP 20):终止进程并将当前进程挂到后台。

这里测试一下Ctrl+Z:

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

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGTSTP, SIG_DFL); 
    while (true)
    {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果:

当我们按下Ctrl+Z后,进程就被挂到后台了。

一些前后台有关的指令:

  • jobs:查看所有后台任务
  • fg 任务号:把任务切到前台运行
  • bg 任务号:让挂起的任务在后台继续运行

2)键盘产生信号的过程

当键盘按下的时候,会向CPU发送硬件中断,然后CPU会识别到自身针脚具有硬件中断信息,接着CPU就会执行OS中处理键盘数据的代码。

2、系统调用产生信号

1)kill 函数

我们可以封装一个简单的kill指令:

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

// ./kill -9 1234
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cerr << "Usage: " << "./kill -signumber pid" << std::endl;
        return 1;
    }

    int signumber = std::stoi(argv[1] + 1); 
    pid_t pid = std::stoi(argv[2]);
    int n = kill(pid, signumber);
    
    return 0;
}

执行一个死循环:

再通过封装的的kill指令发送9号信号来杀掉这个进程:

2)raise 函数

示例:一秒向自己发送一个2号信号

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

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

int main()
{
    signal(2, handler); 
    while (true)
    {
        sleep(1);
        raise(2); 
    }
    return 0;
}

结果:

3)abort 函数

示例:一秒执行一次abort函数

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

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

int main()
{
    signal(SIGABRT, handler); 
    while (true)
    {
        sleep(1);
        abort(); 
    }
    return 0;
}

结果:

3、软件条件产生信号

1)alarm 函数

样例:通过alarm函数体会IO效率

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

int main()
{
    int count = 0;
    alarm(1); // 1秒钟
    while(true)
    {
        std::cout << "count: " << count << std::endl; // IO
        count++;
    }

    return 0;
}

运行结果:

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

int count = 0;

void handler(int signumber)
{
    std::cout << "count: " << count << std::endl;
    exit(0);
}

int main()
{
    signal(SIGALRM, handler); 
    alarm(1);                 
    while (true)
    {
        count++;
    }

    return 0;
}

运行结果:

可以得出结论:IO会极大的影响效率

2)pause 函数

样例:实现一个基于 SIGALRM 信号的周期性任务调度器

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

using func_t = std::function<void()>; // 函数类型
int gcount = 0;                       // 被唤醒的次数
std::vector<func_t> gfuncs;           // 任务列表

void handler(int sig)
{
    // 遍历执行所有任务
    for (auto &f : gfuncs)
    {
        f();
    }
    std::cout << "fcout: " << gcount << std::endl;
    int n = alarm(1); // 重设1s闹钟
    std::cout << "剩余时间: " << n << std::endl;
}

int main()
{
    std::cout << "我的进程pid: " << getpid() << std::endl;
    gfuncs.push_back(
        []()
        {
            std::cout << "我是一个内核刷新操作" << std::endl;
        });
    gfuncs.push_back(
        []()
        {
            std::cout << "我是一个检测进程时间片的操作,如果时间片到了,我会切换进程" << std::endl;
        });
    gfuncs.push_back(
        []()
        {
            std::cout << "我是一个内存管理操作,定期清洁操作系统内部的内存碎片" << std::endl;
        });

    alarm(1); // 1s闹钟
    signal(SIGALRM, handler);

    while (true)
    {
        pause(); 
        std::cout << "我醒来了..." << std::endl;
        gcount++;
    }

    return 0;
}

运行结果:

3)什么是软件条件

软件条件就是:由软件层面的状态变化或操作触发信号的场景

比如定时器超时(alarm 函数设定时间到),向已关闭的管道写数据(触发 SIGPIPE 信号),或是 pause 函数让进程阻塞后被信号唤醒。满足这些条件时,系统会给对应进程发信号,提醒进程处理。

4)什么是系统闹钟

系统闹钟就是:操作系统提供的定时能力,让用户可以设置 "在未来某个时间点触发某个操作"

定时器需要被管理:先描述,在组织。
先描述 :内核用 struct timer_list 结构体来描述一个定时器。

cpp 复制代码
struct timer_list
{
    struct list_head entry;
    unsigned long expires; // 超时时间点
    
    void (*function)(unsigned long); // 超时后要执行的回调函数
    unsigned long data;
    
    struct tvec_t_base_s *base;
};

再组织 :为了高效管理大量定时器,Linux 内核实际使用时间轮结构,可以把它抽象成一个最小堆,堆顶就是超时时间最短的那个定时器。这样内核可以快速找到 "下一个要触发的闹钟",并在它到期时执行对应的处理函数。

4、硬件异常产生信号

硬件异常就是:硬件检测到错误,通知内核,内核向当前进程发送对应信号

比如:
进程除零 :CPU 运算异常,内核发送 SIGFPE
非法访存 :MMU 异常,内核发送 SIGSEGV

1)模拟除0

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

int main()
{
    signal(SIGFPE, SIG_DFL);
    int a = 10;
    a /= 0;
    while(1);
    
    return 0;
}

运行结果:

2)模拟野指针

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

int main()
{
    signal(SIGSEGV, SIG_DFL);
    int *p = NULL;
    *p = 100;
    while (1);

    return 0;
}

运行结果:

3)子进程异常退出 core dump

子进程模拟除零:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
    if (fork() == 0)
    {
    	// 子进程
        sleep(1);
        int a = 10;
        a /= 0;
        exit(0);
    }

    // 父进程
    int status = 0;
    waitpid(-1, &status, 0); 
    printf("exit signal: %d, core dump: %d\n", status & 0x7F, (status >> 7) & 1);
    return 0;
}

运行结果:

由于子进程除零异常,产生的SIGFPE信号默认会终止进程并触发Core Dump

注意:系统默认禁用core文件,可通过 ulimit 命令解限制。示例:ulimit -c 1024

针对上面生成的core文件我们就可以来进行事后调试:gdb ./test core

这样我们就可以直接定位到代码中出错的那一行。

注意 :调试时需要在代码编译的时候带-g选项。

相关推荐
舰长1151 小时前
Windows服务器修改默认远程端口3389
运维·服务器
minji...2 小时前
Linux 线程同步与互斥(五) 日志,线程池
linux·运维·服务器·开发语言·c++·算法
埃伊蟹黄面2 小时前
数据链路层
服务器·网络
华清远见IT开放实验室2 小时前
嵌入式系统化课程 学习内容与服务说明
linux·stm32·学习·嵌入式·全栈·虚拟仿真·测评中心
圆山猫2 小时前
[Linux] Ubuntu 26.04 换阿里云镜像源(最新方法)
linux·ubuntu·阿里云
Dillon Dong2 小时前
【系列主题】从 Docker 构建失败看依赖隔离:多阶段构建的“隐形陷阱”
运维·docker·容器
云飞云共享云桌面2 小时前
精密机械制造工厂研发部门使用SolidWorks和ug,三维设计云桌面如何选择?
大数据·运维·服务器·网络·数据库·人工智能·制造
小心我捶你啊3 小时前
VPS的主要用途,与其它方式的区别
服务器·网络协议·tcp/ip
网络小白不怕黑3 小时前
1.2 VMware部署Rocky Linux 9 (MBR分区表,图形化安装)
linux·运维·服务器