【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选项。

相关推荐
大树8820 分钟前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠23 分钟前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质1 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush41 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz1 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工2 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智2 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩2 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_2 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化