【Linux篇】理解信号:如何通过信号让程序听从操作系统的指令

信号的悄然到来:当操作系统发出'警告'时

  • 一.信号
    • [1.1 基本概念](#1.1 基本概念)
    • [1.2 产生信号方式](#1.2 产生信号方式)
      • [1.2.1 键盘产生信号](#1.2.1 键盘产生信号)
      • [1.2.2 系统调用产生信号](#1.2.2 系统调用产生信号)
        • [1.2.2.1 kill](#1.2.2.1 kill)
        • [1.2.2.2 raise](#1.2.2.2 raise)
        • [1.2.2.3 abort](#1.2.2.3 abort)
      • [1.2.3 调用系统命令](#1.2.3 调用系统命令)
      • [1.2.4 异常](#1.2.4 异常)
      • [1.2.5 软件条件产生信号](#1.2.5 软件条件产生信号)
        • [1.2.5.1 pause](#1.2.5.1 pause)
        • [1.2.5.2 alarm](#1.2.5.2 alarm)
  • [二. 最后](#二. 最后)

信号的产生是现代通信系统中的基础,它涉及将信息从一个地方传递到另一个地方。在通信过程中,信号扮演着至关重要的角色,它是信息的载体。信号的生成可以通过不同的方式实现,包括电磁波的调制、音频信号的转换等。理解信号的产生过程有助于我们掌握如何高效地传递信息。在此基础上,我们能够进一步探讨信号的处理与优化,确保信息能够准确、迅速地到达目的地。

💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!

👍点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!

🚀分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对Linux OS感兴趣的朋友,让我们一起进步!

一.信号

1.1 基本概念

信号是操作系统用于通知进程特定事件的一种机制。例如,当某个事件发生时,操作系统会向进程发送一个信号,以提示它采取某种响应。信号可以是硬件中断(如按键事件、鼠标点击等)或软件异常(如除零错误、非法内存访问等)。信号可以看作是一种轻量级的异步通信方式,通常用于让操作系统或其他进程向目标进程传递信息或指示某种操作的发生。

结论:信号的处理不是立即处理,而是在合适的时候处理它。合适的时候指程序发生异常,中断,或者系统调用等。

1.2 产生信号方式

1.2.1 键盘产生信号

下面将结合代码来展示键盘如何产生信号的,思路:我们先将每个信号的行为改为我们想要的然后进行合理性分析。

示例代码:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber <<
std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGINT/*2*/, handler);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}

结果:

SIGINT/2/信号是用来默认的行为是终止进程的。通过上述图片可以看出Ctrl + c发送的是2号信号。

Ctrl + z是用来暂停进程的,将当前前台进程挂起到后台等。

关于前台和后台进程的理解。

  • 前台进程:可以从键盘标准输入中获取,前台进程有且仅有一个,因为键盘只有一个,输入的数据必须给一个确定的进程。
  • 后台进程:无法从键盘标准输入中获取内容,后台进程可以有多个。
  • 相似点:两个进程都是向标准输出中打印内容。

前台进程的"无响应"本质是 Shell 被阻塞,等待进程释放控制权,而 Ctrl+C 通过信号机制强制终止进程,恢复终端的可用性。如ls,pwd必须给sheel指令进行执行(当前进程),而Ctrl+c是给子进程进行的。不需要给sheel执行。

补充:关于前后台进程移动的指令。

  1. jobs:jobs 命令用于列出当前 shell 会话中管理的所有后台作业和暂停的作业。
  2. fg:fg (foreground)[任务号],将一个后台或者暂停的作业调回前台继续运行。
  3. bg:bg (background)[任务号],将暂停(停止)的作业放到后台,继续运行。

信号如何存的???

  • 信号是一个int类型的变量,位数表示信号的编号,1表示有,0表示无,发送信号的本质就是修改内核的数据。

1.2.2 系统调用产生信号

1.2.2.1 kill
  • 功能:
    作用:发送信号给进程,触发相应的信号处理动作。

原型如下:

int kill(pid_t pid, int sig);

参数:

pid:目标进程ID。

sig:信号编号,如 SIGTERM、SIGKILL、SIGUSR1 等。

1.2.2.2 raise
  • 功能:
    用于向调用自己的进程发送信号,让进程自己产生一个信号,通常用于测试信号处理或者程序内部触发异常行为。
    原型如下:

int raise(int sig);

参数 sig 为信号编号,如 SIGINT、SIGTERM、SIGABRT 等。

返回值:成功返回0,失败返回非0。

1.2.2.3 abort
  • 功能:

它会向进程发送 SIGABRT 信号,默认行为是使程序产生异常终止并生成核心转储(core dump),便于调试。

功能特点:

    1. 调用 abort() 后,程序不会执行 return 或 exit,而是直接异常结束。
    1. 若程序注册了对 SIGABRT 的信号处理器,则先执行信号处理函数,再终止程序。
    1. 一般用于当程序检测到无法恢复的错误时,立即停止运行。

1.2.3 调用系统命令

下面给出带代码来演示这个行为。

示例代码:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
    while (true)
    {
        sleep(1);
    }
}

该例子无法退出,可以使用kill -[信号编号或信号] [进程pid值]。

kill -9 153032

该命令会将指定的进程终止。

1.2.4 异常

通常发生段错误,除零错误,分别对应8号信号(SIGFPE),11号信号(SIGSEGV),信号都通过操作系统发送给指定发生异常的进程。

示例代码:

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

void handler(int sig)
{
    std::cout << "获得一个信号:" << sig << std::endl;
    exit(13);//没有此句,会造成程序死循环,因为程序发行异常,处理完异常恢复上下文,程序计数器(PC)会被恢复为触发信号时的指令地址(即a /= 0这一行的地址)。就会继续调用,造成死循环。
}

int main()
{
    for (int i = 1; i < 32; i++)
        signal(i, handler);

        sleep(1);
        int a = 10;
        a /= 0;

    return 0;
}

输出结果:

获得一个信号:8

1.2.5 软件条件产生信号

1.2.5.1 pause
  • 功能:
    通常用于程序等待信号的场景,比如等待某种异步事件发生。

程序执行到 pause() 时进入睡眠状态,不消耗CPU,直到信号唤醒。

  • 函数原型:

int pause(void);

  • 返回值

pause() 在被信号唤醒后返回 -1,同时 errno 设置为 EINTR (被信号中断)。

1.2.5.2 alarm

作用原理:

  1. 调用 alarm(seconds) 后,计时器开始倒计时。

  2. 时间到后,向进程发送 SIGALRM 信号(程序需要设置信号处理函数或使用默认动作)。

  3. 如果 alarm() 被再次调用,会取消之前的定时器,重新启动新的计时器。

    函数原型

unsigned int alarm(unsigned int seconds);

  • 返回值:

返回第一个闹钟剩余的秒数。

下面展示通过定时器超时(SIGALRM)产生信号。

示例代码:

cpp 复制代码
#include <iostream>
#include <vector>
#include <functional>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
//硬件中断
void Sched()
{
    std::cout << "我是进程调度" << std::endl;
} 

void MemManger()
{
    std::cout << "我是周期性内存管理,正在检查内存有没有问题" <<std::endl; 
}

void Fflush()
{
    std::cout << "我是刷新程序,我正在定期刷新内存数据,到磁盘" << std::endl;
}

void handlerSig(int sig)
{
    std:: cout << "########################################" << std::endl;
    for(auto f: funcs)
        f();
    std:: cout << "########################################" << std::endl;
    int n = alarm(1);
}

int main()
{
    funcs.push_back(Sched);
    funcs.push_back(MemManger);
    funcs.push_back(Fflush);


    signal(SIGALRM,handlerSig);
    alarm(1);

    while(true)//这就是操作系统
    {
        pause();
    }

    return 0;
}

该实例每1秒产生SIGALRM信号,执行handler函数,完成任务。

输出结果:

########################################

我是进程调度

我是周期性内存管理,正在检查内存有没有问题

我是刷新程序,我正在定期刷新内存数据,到磁盘

########################################
结论:操作系统发送信号都是修改位图,通过进程pid和信号编号等。

二. 最后

本文系统阐述了操作系统信号机制的核心概念与应用。信号作为进程间异步通信手段,用于通知事件(如硬件中断、软件异常)。产生方式包括:1)键盘输入(Ctrl+C/Z触发SIGINT/SIGTSTP);2)系统调用(kill/raise/abort发送信号);3)命令行工具(kill命令终止进程);4)程序异常(除零触发SIGFPE,段错误触发SIGSEGV);5)软件条件(alarm定时器触发SIGALRM)。信号处理非即时,需注册回调函数,但需注意异步安全(如避免非重入函数)。关键案例包括:信号驱动的周期任务(结合alarm与pause)、异常处理中的死循环规避(通过exit终止或标志位控制)。最后强调信号本质为内核位图操作,合理设计处理逻辑可实现进程控制、资源管理等功能。

相关推荐
꧁坚持很酷꧂3 分钟前
Linux Ubuntu18.04下安装Qt Craeator 5.12.9(图文详解)
linux·运维·qt
凉、介20 分钟前
PCI 总线学习笔记(五)
android·linux·笔记·学习·pcie·pci
电鱼智能的电小鱼1 小时前
EFISH-SBC-RK3588无人机地面基准站项目
linux·网络·嵌入式硬件·机器人·无人机·边缘计算
电鱼智能的电小鱼1 小时前
基于 EFISH-SBC-RK3588 的无人机环境感知与数据采集方案
linux·网络·嵌入式硬件·数码相机·无人机·边缘计算
小诸葛的博客2 小时前
详解Linux中的定时任务管理工具crond
linux·运维·chrome
一默19912 小时前
CentOS 7.9升级OpenSSH到9.9p2
linux·运维·centos
keep intensify2 小时前
Linux常用指令
linux·服务器·php
带电的小王2 小时前
sherpa-ncnn:Linux(x86/ARM32/ARM64)构建sherpa-ncnn --语音转文本大模型
linux·语音识别·实时音视频·sherpa-ncnn
沧浪之水!3 小时前
【Linux网络】:套接字之UDP
linux·网络·udp
BranH3 小时前
Linux系统中命令设定临时IP
linux·运维·服务器