Linux信号大揭秘-从中断到控制进程,一步步掌握进程通信利器!

news2025/1/15 16:31:41

在Linux环境下,信号(Signal)是一种软件中断,用于通知进程发生了某些重要事件。无论你是在编写命令行工具、服务程序,还是开发图形界面应用,都离不开对信号的处理。本文将全面解析信号的工作原理,并通过实例代码让你彻底掌握在C++程序中使用信号的技巧。


一、信号基本概念

信号是由内核发送给进程的一种通知机制,通常源于硬件异常、外部设备中断或软件事件发生。比如:

  • 硬件异常:除零错误(SIGSEGV)、非法内存访问(SIGSEGV)等

    • 当硬件(如内存、CPU)检测到了一个错误并通知到内核,而后内核再发送相应的信号给对应的进程 。

    • 比如最常见的除
0 操作(CPU),引用了无法访问的内存区域(内存),后者我们可能会经常看到,信号类型为
SIGSEGV,即段错误。

    • 以前的内存是分段的,比如数据段、代码段等,当进程访问错误内存地址时,就会抛出段错误的信息:

    int main
    { 
    		int*ptr=NULL; *ptr=1024;
    } 
    
    Process finished with exit code 139(interrupted by signal 11:SIGSEGV) 
    

  • 外部中断:用户按下Ctrl+C(SIGINT)

    • 用户通过键盘或者其他设备键入了能够产生信号的特殊字符,例如最为常用的
Ctrl-C,中断当前进程的运行。
  • 软件事件:定时器到期(SIGALRM)、子进程退出(SIGCHLD)等

    • 比如进程设置的定时器到期,进程的某个子进程退出等等,都会有信号的产生和发生。

    • 定时器到期会发送一个
SIGALRM 的信号,也就是
Singal
Alarm。

    • 进程的某个子进程退出会发送一个
SIGCHLD 信号,也就是
Singal
Child 从信号的名字上我们能大致的猜到这个信号是干啥的。


无论何时,只要内核检测到相应事件发生,就会向对应的进程发送信号,促使其执行特定的处理逻辑。


在C++中,信号处理通常涉及到<csignal><signal.h>库。可以使用signalsigaction函数来设置信号处理函数。


下面是一个简单的示例,展示如何捕获SIGSEGV信号:

#include <iostream>
#include <csignal>
#include <cstring>

void signalHandler(int signum) {
    // 打印信号编号,并退出
    std::cout << "Caught signal: " << signum << std::endl;
    exit(signum);
}

int main() {
    // 设置SIGSEGV信号的处理函数
    signal(SIGSEGV, signalHandler);

    // 故意触发一个SIGSEGV信号
    int *ptr = nullptr;
    std::cout << "Accessing null pointer..." << std::endl;
    *ptr = 1; // 这将触发SIGSEGV信号

    return 0;
}

在这个示例中,我们定义了一个signalHandler函数,它将被调用以处理SIGSEGV信号。

我们使用signal函数来设置这个处理函数。

main函数中,我们故意访问一个空指针来触发SIGSEGV信号,这将导致操作系统调用我们的信号处理函数。


二、处理信号的方式

面对进程收到的信号,我们一般有三种处理方式:

1、忽略信号

忽略信号意味着当信号发生时,进程不会采取任何行动。在C++中,可以使用signalsigaction函数来设置信号的处理行为为忽略。

#include <iostream>
#include <csignal>
#include <unistd.h> // 用于sleep函数

void ignoreSignal(int signum) {
    // 这个函数实际上什么也不做,信号被忽略
}

int main() {
    // 忽略SIGINT信号(通常由Ctrl+C产生)
    signal(SIGINT, ignoreSignal);

    std::cout << "Process will ignore SIGINT signals. Press Ctrl+C to test." << std::endl;
    while (true) {
        sleep(1); // 让进程休眠,等待信号
    }
    return 0;
}

在这个示例中,我们通过设置signal函数的第二个参数为ignoreSignal函数来忽略SIGINT信号。ignoreSignal函数什么也不做,因此当用户尝试使用Ctrl+C中断程序时,程序不会响应。


2、阻塞(暂时屏蔽)信号

阻塞信号意味着暂时阻止信号的传递,直到进程再次准备接受该信号。

在C++中,可以使用sigprocmask函数来阻塞或解除阻塞信号。

#include <iostream>
#include <csignal>
#include <cerrno>
#include <cstring>
#include <unistd.h> // 用于sleep函数
#include <sys/types.h>
#include <signal.h>

void signalHandler(int signum) {
    std::cout << "Caught signal: " << strsignal(signum) << std::endl;
}

int main() {
    // 设置信号处理函数
    signal(SIGINT, signalHandler);

    // 创建一个信号集,包含SIGINT
    sigset_t signal_set;
    if (sigemptyset(&signal_set) == -1) {
        std::perror("sigemptyset");
        return 1;
    }
    if (sigaddset(&signal_set, SIGINT) == -1) {
        std::perror("sigaddset");
        return 1;
    }

    // 阻塞SIGINT信号
    if (sigprocmask(SIG_BLOCK, &signal_set, NULL) == -1) {
        std::perror("sigprocmask");
        return 1;
    }

    std::cout << "SIGINT is blocked for 5 seconds. Press Ctrl+C to test." << std::endl;
    sleep(5); // SIGINT will be blocked during this period

    // 解除SIGINT信号的阻塞
    if (sigprocmask(SIG_UNBLOCK, &signal_set, NULL) == -1) {
        std::perror("sigprocmask");
        return 1;
    }

    std::cout << "SIGINT is unblocked. Press Ctrl+C to test again." << std::endl;
    while (true) {
        sleep(1); // 让进程休眠,等待信号
    }
    return 0;
}

在这个示例中,我们首先设置了SIGINT信号的处理函数。

然后,我们创建了一个信号集,并将SIGINT信号添加到这个集合中。

使用sigprocmask函数与SIG_BLOCK标志来阻塞SIGINT信号。

sleep(5)调用期间,SIGINT信号被阻塞,这意味着如果用户尝试使用Ctrl+C中断程序,程序不会立即响应

5秒后,我们解除了SIGINT信号的阻塞,此时如果用户再次按下Ctrl+C,程序将能够捕捉到信号并调用信号处理函数。

请注意,信号处理和阻塞是操作系统级的机制,需要谨慎使用,以避免潜在的竞态条件和不可预见的行为。


3、编写信号处理程序

绝大多数情况下,我们都会选择第三种方式:为进程注册一个针对特定信号的处理函数。

下面就让我们通过代码示例,感受一下信号处理程序的魅力。

#include <iostream>
#include <csignal>
#include <unistd.h>

void signalHandler(int signum) {
    std::cout << "Caught signal " << signum << std::endl;
    // 进行一些清理工作
    // ...
    exit(signum);
}

int main() {
    // 注册SIGINT(Ctrl+C)的信号处理程序
    signal(SIGINT, signalHandler);

    while (true) {
        std::cout << "程序正在运行..." << std::endl;
        sleep(1);
    }

    return 0;
}

在上述示例中,我们通过signal函数注册了SIGINT(用户按下Ctrl+C时产生)信号的处理程序signalHandler。

当程序运行时,用户按下Ctrl+C,内核会将该中断事件转换为SIGINT信号,并调用我们编写的signalHandler函数。

在函数内部,我们可以执行任何所需的操作,比如打印信息、进行清理工作或退出进程等。

三、常用信号及其作用


下面简单介绍几个在Linux编程中常用的信号,以及最佳处理方式。

1、SIGINT: 中断进程执行

通常由用户通过Ctrl+C产生,可以通过捕获并优雅地处理来实现程序的中断。

#include <iostream>
#include <csignal>
#include <unistd.h>

void handleSigInt(int signum) {
    std::cout << "SIGINT received, exiting gracefully." << std::endl;
    _exit(0); // 使用_exit直接退出,避免再次触发信号处理程序
}

int main() {
    signal(SIGINT, handleSigInt);
    while (true) {
        std::cout << "Running... Press Ctrl+C to interrupt." << std::endl;
        sleep(1);
    }
    return 0;
}

2、SIGTERM:终止进程,kill命令的默认信号

通常由kill命令产生,可以通过捕获并执行清理操作来优雅地终止程序。

复制
void handleSigTerm(int signum) {
    std::cout << "SIGTERM received, cleaning up and exiting." << std::endl;
    // 执行清理工作
    _exit(0);
}

int main() {
    signal(SIGTERM, handleSigTerm);
    // 主程序逻辑
}

3、SIGKILL:必杀信号,进程无法捕获或忽略

4、SIGCHLD:子进程退出时发送给父进程

通常用于处理子进程的退出状态。

#include <sys/wait.h> // 等待子进程

void handleSigChld(int signum) {
    int status;
    while (waitpid(-1, &status, WNOHANG) > 0) {
        // 处理子进程退出状态
        std::cout << "Child process exited with status " << status << std::endl;
    }
}

int main() {
    signal(SIGCHLD, handleSigChld);
    // 启动子进程的代码
}

5、SIGSEGV:非法访问内存区域,如指针使用错误

段错误,C++ 中数组访问越界,指针访问不存在的内存区域等等,内核都会发送该信号给进程。

通常由可以通过捕获来避免程序崩溃。

void handleSigSegv(int signum) {
    std::cout << "SIGSEGV received, handling segmentation fault." << std::endl;
    // 可以记录日志,进行内存检查等
    _exit(1);
}

int main() {
    signal(SIGSEGV, handleSigSegv);
    // 可能触发SIGSEGV的代码
}

6、SIGALRM:定时器超时

可以通过alarmsetitimer设置,用于定时执行操作。

#include <iostream>
#include <csignal>
#include <unistd.h>

void handleSigAlrm(int signum) {
    std::cout << "SIGALRM received, timer expired." << std::endl;
    // 执行定时任务
}

int main() {
    signal(SIGALRM, handleSigAlrm);
    alarm(5); // 设置5秒后触发SIGALRM
    while (true) {
        sleep(1);
    }
    return 0;
}

7、暂停/恢复
  • SIGSTOP 这是一个暂停信号,进程无法阻塞、捕获或者是忽略该信号,所以总是能够停止程序的运行,有点像打断点一样。
  • SIGCONT 使停止的进程继续执行,也就是恢复进程的调度属性。

8、终端 SIGHUP

当终端断开时,将发送该信号给终端控制进程。


四、信号处理注意事项


1、信号处理流程

内核调用信号处理函数可能会发生在任意时刻,并且完全有可能打断系统调用的执行。

在这里插入图片描述


2、在编写信号处理程序时,需要注意以下几个要点:
  • 信号处理程序应该尽可能简短,不能执行复杂或耗时的操作
  • 信号处理程序内部必须只调用可重入函数或系统调用,如printf、malloc等不可重入函数可能导致进程行为异常
  • 如果在执行信号处理程序时收到同一信号,则该信号会被阻塞并与已有信号合并为一个信号
  • signal函数在多线程环境中可能不够安全,可以考虑使用sigaction函数来设置信号处理行为。

3、关于信号问题的解答
  • 如果当前的
SIGINT
处理函数还在执行,此时又来了一个或多个
SIGINT
信号会发生什么?
  • POSIX 标准将保证当前同一个信号将会被阻塞,也就是说,不会中断信号处理函数,而是等在那里。如果此时有多个相同信号到达,那么多个信号将会并合并成一个向进程发送。

  • 什么是可重入函数?信号处理函数为什么需要是可重入的?
    • 可重入函数,可以认为是线程安全的函数,也就是即使多个线程乱序调用某一个函数,依然能够得到预期的结果。
    • 使用不可重入的函数可能会导致进程执行混乱,甚至是陷入到休眠状态,失去进程的控制诸如
printf()、malloc() 等函数都是不可重入的。

五、更多信号应用场景


除了中断进程和定时器超时,信号在Linux编程中还有很多应用场景,比如:

1、父子进程间通信和控制
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <csignal>

void sigchld_handler(int signum) {
    // 子进程退出时,父进程接收SIGCHLD信号
    while (waitpid(-1, NULL, WNOHANG) > 0);
    std::cout << "Child process has terminated." << std::endl;
}

int main() {
    // 设置SIGCHLD信号处理函数
    signal(SIGCHLD, sigchld_handler);

    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "Fork failed" << std::endl;
        return 1;
    } else if (pid == 0) {
        // 子进程
        std::cout << "Child process running" << std::endl;
        exit(0);
    } else {
        // 父进程
        std::cout << "Parent process waiting for child to terminate" << std::endl;
        wait(NULL); // 等待子进程退出
    }
    return 0;
}

2、实现Unix域Socket和管道间的信号驱动I/O

信号驱动I/O允许一个进程在I/O操作准备好时接收信号。

以下是使用sigaction设置信号驱动I/O的示例。

#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <csignal>
#include <fcntl.h>

void sigio_handler(int signum) {
    std::cout << "SIGIO received" << std::endl;
    // 处理I/O操作
}

int main() {
    struct sockaddr_un server_addr, client_addr;
    int server_fd, client_fd;
    socklen_t client_len;
    char buffer[1024];

    // 创建Unix域socket
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, "/tmp/server_socket");
    unlink(server_addr.sun_path); // 删除旧的socket文件
    bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(server_fd, 5);

    // 设置信号驱动I/O
    struct sigaction sa;
    sa.sa_handler = NULL;
    sa.sa_flags = SA_RESTART | SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sigio_handler;
    sigaction(SIGIO, &sa, NULL);
    fcntl(server_fd, F_SETOWN, getpid());
    fcntl(server_fd, F_SETFL, O_ASYNC);

    while (true) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd >= 0) {
            std::cout << "Client connected" << std::endl;
            // 读取数据
            while (read(client_fd, buffer, sizeof(buffer)) > 0) {
                // 处理接收到的数据
            }
            close(client_fd);
        }
    }

    close(server_fd);
    unlink(server_addr.sun_path);
    return 0;
}

在这个示例中,我们创建了一个Unix域socket服务器,并设置了一个信号驱动I/O。当有数据可读时,服务器将接收到SIGIO信号,并调用sigio_handler函数。


请注意,信号驱动I/O的使用相对复杂,需要对系统调用和信号处理有深入的理解。此外,不同的操作系统和编译器可能有不同的实现和限制。


你是否已经体会到信号作为一种低层次进程通信机制的强大功能?想要进一步学习信号在网络编程等领域的应用吗?如果有任何疑问,欢迎在评论区留言交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1791162.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实用软件分享---简单菜谱 0.3版本 几千种美食(安卓)

专栏介绍:本专栏主要分享一些实用的软件(Po Jie版); 声明1:软件不保证时效性;只能保证在写本文时,该软件是可用的;不保证后续时间该软件能一直正常运行;不保证没有bug;如果软件不可用了,我知道后会第一时间在题目上注明(已失效)。介意者请勿订阅。 声明2:本专栏的…

重生之 SpringBoot3 入门保姆级学习(14、内容协商基础简介)

重生之 SpringBoot3 入门保姆级学习&#xff08;14、内容协商基础简介&#xff09; 3.3 内容协商3.3.1 基础简介3.3.2 演示效果 3.3 内容协商 3.3.1 基础简介 默认规则 基于请求头的内容协商&#xff08;默认开启&#xff09; 客户端向服务器发送请求&#xff0c;携带 HTTP 标…

大归纳!!教你使用<string.h>的字符函数与字符串函数!!☑

这篇博客为你归纳了所有的字符函数和最常用的字符串函数&#xff0c;以及对应的模拟实现&#xff01;&#xff01;你可以直接循着目录跳到你需要的段落哦&#xff01;&#xff01;&#x1f60d; 目录 字符函数 字符分类 字符判断函数 islower——判断小写字母 isupper——…

上位机图像处理和嵌入式模块部署(f407 mcu中的udp server开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 既然lwip已经port到407上面了&#xff0c;接下来其实就可以做一些测试了。本身lwip支持tcp、udp&#xff0c;也支持client和server&#xff0c;既然…

Qt信号槽与函数直接调用性能对比

1. 测试方法 定义一个类Recv&#xff0c;其中包含一个成员变量num和一个成员函数add()&#xff0c;add()实现num的递增。 另一个类Send通过信号槽或直接调用的方法调用Recv的add函数。 单独开一个线程Watcher&#xff0c;每秒计算num变量的增长数值&#xff0c;作为add函数被调…

STL中vector动态二维数组理解(杨辉三角)

题目链接&#xff1a;118.杨辉三角 题目描述&#xff1a; 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 题目指要&#xff1a; 本题的主要目的是理解vector<vector<int&…

编译原理总结

编译器构成 1. 前端分析部分 1.1 词法分析 确定词性&#xff0c;输出为token序列 1.2 语法分析 识别短语 1.3 语义分析 分析短语在句子中的成分 IR中间代码生成 2. 机器无关代码优化 3. 后端综合部分 目标代码生成 机器相关代码优化 4. 其他 全局信息表 异常输出

电影推荐系统配置运行

电影推荐系统配置运行 代码地址项目介绍&#xff08;引自原文&#xff09; 环境创建新环境激活环境安装包创建管理员用户(可选)启动 代码地址 movie 项目介绍&#xff08;引自原文&#xff09; 本推荐系统采用的是分层模型设计思想&#xff0c;第一层为前端页面模型设计&…

2024.6.5

1、react原理学习&#xff0c; hook、fiber 2、瀑布流组件完善 3、代码随想录二刷

【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【06】【商品服务】接口文档地址_三级分类_SPU_SKU

持续学习&持续更新中… 学习态度&#xff1a;守破离 【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【06】【商品服务】接口文档地址_三级分类_SPU_SKU 接口文档地址三级分类效果图建表后台组建数据的树形结构在人人(后台管理系统)中实现管理商品的三级分类路径规则使用…

DRIVEN|15分的CNN+LightGBM怎么做特征分类,适用于转录组

说在前面 今天分享一篇做深度学习模型的文章&#xff0c;这是一篇软硬结合的研究&#xff0c;排除转换实体产品&#xff0c;我们做生信基础研究的可以学习模仿这个算法&#xff0c;适用且不局限于临床资料&#xff0c;转录组数据&#xff0c;GWAS数据。 今天给大家分享的一篇文…

创新入门|营销中的视频内容:不可或缺的策略

视频在营销中日益重要。你是否也发现,视频内容最近似乎无处不在?它占据着社交媒体的推文、网站首页,甚至电子邮件中的位置。事实上,并不是你一个人有这样的感受。在过去十年中,视频作为一种营销手段日益成熟和强大。这是因为,人类天生就是视觉动物。我们大脑处理视觉信息的速度…

补上缺失的一环----一种数据库系统主动对外推送表的增删改实时变动数据的实践

在实践中&#xff0c;一些应用程序或模块需要实时获取某些数据库表的增删改变动数据。 对此需求&#xff0c;常见的方案有: 1、应用程序通过轮循查询数据库方式获取数据库表的增删改变动数据. 2、应用程序在把数据写入数据库表之前&#xff0c;通过事件方式向外通知数据库表的增…

安徽某高校数据挖掘作业6

1 根据附件中year文件&#xff0c;编辑Python程序绘制年销售总额分布条形图和年净利润分布条形图&#xff0c;附Python程序和图像。 2 根据附件中quarter和quarter_b文件&#xff0c;编辑Python程序绘制2018—2020年销售额和净利润折线图&#xff0c;附Python程序和图像。 3 …

城市之旅:使用 LLM 和 Elasticsearch 简化地理空间搜索(二)

我们在之前的文章 “城市之旅&#xff1a;使用 LLM 和 Elasticsearch 简化地理空间搜索&#xff08;一&#xff09;”&#xff0c;在今天的练习中&#xff0c;我将使用本地部署来做那里面的 Jupyter notebook。 安装 Elasticsearch 及 Kibana 如果你还没有安装好自己的 Elasti…

C#-foreach循环语句

foreach循环语句 语法&#xff1a; foreach(数据类型 变量名 in 数组或集合对象) { 语句块; } foreach 会在每次循环的过程中&#xff0c;依次从数组或集合对象中取出一个新的元素放foreach( )里定义的变量中&#xff0c;直到所有元素都成功取出后退出循环。 foreach循环…

[AI资讯·0605] GLM-4系列开源模型,OpenAI安全疑云,ARM推出终端计算子系统,猿辅导大模型备案……

AI资讯 1毛钱1百万token&#xff0c;写2遍红楼梦&#xff01;国产大模型下一步还想卷什么&#xff1f;AI「末日」突然来临&#xff0c;公司同事集体变蠢&#xff01;只因四大聊天机器人同时宕机OpenAI员工们开始反抗了&#xff01;AI手机PC大爆发&#xff0c;Arm从软硬件到生态…

搭建大型分布式服务(三十九)SpringBoot 整合多个kafka数据源-支持Aware模式

系列文章目录 文章目录 系列文章目录前言一、本文要点二、开发环境三、原项目四、修改项目五、测试一下五、小结 前言 本插件稳定运行上百个kafka项目&#xff0c;每天处理上亿级的数据的精简小插件&#xff0c;快速上手。 <dependency><groupId>io.github.vipjo…

【成品设计】基于物联网技术的养老院老人健康监控系统的设计与实现

《基于物联网技术的养老院老人健康监控系统的设计与实现》 老年人监护系统的核心任务是分析老年人在日常生活中是否存在意外事件和异常行为并提供相关帮助。正确分析出老年人的突发意外和行为异常&#xff0c;不仅可以有效地降低老年人因意外事故造成的生命风险&#xff0c;还…

【一步一步了解Java系列】:重磅多态

看到这句话的时候证明&#xff1a;此刻你我都在努力 加油陌生人 个人主页&#xff1a;Gu Gu Study专栏&#xff1a;一步一步了解Java 喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹 喜欢的话可以点个赞谢谢了。 作者&#xff1a;小闭…