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

在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的使用相对复杂,需要对系统调用和信号处理有深入的理解。此外,不同的操作系统和编译器可能有不同的实现和限制。

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

相关推荐
yaoxin5211238 分钟前
第二十七章 TCP 客户端 服务器通信 - 连接管理
服务器·网络·tcp/ip
内核程序员kevin10 分钟前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
sinat_384241094 小时前
使用 npm 安装 Electron 作为开发依赖
服务器
朝九晚五ฺ5 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream5 小时前
Linux的桌面
linux
xiaozhiwise5 小时前
Makefile 之 自动化变量
linux
Kkooe6 小时前
GitLab|数据迁移
运维·服务器·git
久醉不在酒6 小时前
MySQL数据库运维及集群搭建
运维·数据库·mysql
意疏7 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker