百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

Linux信号深度解析:系统编程中的关键通信手段

sinye56 2024-11-24 21:30 12 浏览 0 评论

为什么需要信号?

Linux 计算机系统中有许多处于不同状态的进程。这些进程要么属于用户应用程序,要么属于操作系统。我们需要一种机制来协调内核和这些进程的活动。其中一种方法就是让进程在发生重要事件时通知其他进程。这就是为什么我们需要信号。

信号基本上是一种单向通知。信号可以由内核发送给一个进程,也可以由一个进程发送给另一个进程,还可以由一个进程发送给自己。

Linux 信号起源于 Unix 信号。在后来的 Linux 版本中,加入了实时信号。信号是一种简单、轻量级的进程间通信方式,因此非常适合嵌入式系统。

关于 Linux 信号一些基本知识

共有 31 个标准信号,编号为 1-31。每个信号都以 "SIG "命名,后跟一个后缀。

在 macOS 的命令行中执行 man 3 signal ,可以看到:

从 2.2 版开始,Linux 内核支持 33 种不同的实时信号。这些信号的编号为 32-64,但程序员应该使用 SIGRTMIN+n 符号。标准信号有特定用途,但 SIGUSR1 和 SIGUSR2 的使用可由应用程序定义。实时信号也由应用程序定义。

为什么没有 0 信号?

0 号信号(POSIX.1 将其称为空信号)一般不使用,但 kill 函数将其作为特例使用。不会发送任何信号,但可以使用它(相当不可靠)来检查进程是否仍然存在

以下是一个使用 C 语言编写的示例代码,展示了如何使用kill函数和 0 号信号来检查进程是否存在:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
        return 1;
    }

    pid_t pid = atoi(argv[1]);
    if (kill(pid, 0) == 0) {
        printf("Process with PID %d exists.\n", pid);
    } else {
        perror("kill");
        printf("Process with PID %d does not exist or you don't have permission to signal it.\n", pid);
    }

    return 0;
}

请注意,这种方法并不是百分之百可靠的,因为在一个进程终止和其 PID 被重新分配给另一个进程之间的短暂时间内,可能会出现检查结果不准确的情况。此外,如果目标进程是由不同用户运行的,并且没有适当的权限,kill函数也可能返回错误。因此,这种方法应该只作为一种粗略的检查手段,而不是作为严格的进程存在的验证。

推荐使用 sigaction 而不是 signal 函数来处理信号

Linux 实现的信号完全符合 POSIX 标准。较新的实现应优先使用 sigaction,而不是传统的信号接口。

signal 函数是一个较旧的函数,用于设置一个信号的处理函数。它的原型如下:

void (*signal(int sig, void (*func)(int)))(int);

你可以使用 signal 函数来为特定的信号设置一个处理函数。例如,下面的代码为 SIGINT(通常是用户按 Ctrl+C 时发送的信号)信号设置了一个处理函数:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handle_sigint(int sig) {
    printf("Caught signal %d\n", sig);
}

int main() {
    signal(SIGINT, handle_sigint);
    while (1) {
        printf("Hello, World!\n");
        sleep(1);
    }
    return 0;
}

我们来看看 sigaction 函数。这个函数提供了更多的方式来控制信号的行为。它的原型如下:

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

sigaction 函数使用一个 sigaction 结构体来指定信号的处理方式,这个结构体包含了信号处理函数、信号掩码和其它选项。下面是使用 sigaction 的一个例子

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handle_sigint(int sig, siginfo_t *si, void *uc) {
    printf("Caught signal %d\n", sig);
}

int main() {
    struct sigaction sa;
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handle_sigint;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    while (1) {
        printf("Hello, World!\n");
        sleep(1);
    }
    return 0;
}

在这个例子中,我们使用 sigaction 来为 SIGINT 设置信号处理函数 handle_sigint。我们设置了 SA_SIGINFO 标志,这意味着信号处理函数可以接收额外的信息,如信号编号和发送信号的进程信息。

为什么推荐使用 sigaction 而不是 signal 呢?原因是:

  1. 功能强大:sigaction 允许你更精细地控制信号的处理,例如设置信号掩码来在信号处理函数执行期间阻塞特定的信号。
  2. 可靠性:signal 在某些系统上可能不是线程安全的,而 sigaction 则没有这个问题。
  3. 向后兼容:虽然 sigaction 是 POSIX 标准引入的,但它也提供了与 signal 的向后兼容性。通过适当的使用 sa_flags 参数,可以模拟 signal 的行为。
  4. 符合标准:使用 sigaction 可以确保代码的便携性,因为它遵循了 POSIX 标准,这意味着在所有遵循 POSIX 标准的操作系统中,sigaction 的行为都是一致的。

硬中断与软中断

正如硬件子系统可以中断处理器一样,信号也可以中断进程的执行。因此,它们被视为软件中断。中断处理程序处理硬件中断,信号处理程序处理信号。

一些信号映射到特定的按键输入:

  • SIGINT 对应 ctrl+c
  • SIGSTOP 对应 ctrl+z
  • SIGQUIT 对应 ctrl+\

信号如何影响 Linux 进程的状态?

  • 有些信号会终止接收进程:sighup、sigint、sigterm、sigkill
  • 有一些信号在终止进程的同时会产生内核转储,以帮助程序员调试出错的原因:SIGABRT(终止信号)、SIGBUS(总线错误)、SIGILL(非法指令)、SIGSEGV(无效内存引用)、SIGSYS(不良系统调用)
  • 有些信号会停止进程:SIGSTOP、SIGTSTP

程序可以覆盖默认行为。例如,可以编写一个交互式程序来忽略SIGINT(由ctrl+c输入生成)。两个值得注意的例外是SIGKILL和SIGSTOP信号,它们不能以这种方式被忽略、阻止或覆盖

下面的例子中将用到以下信号:

  • SIGSTOP: 这个信号导致接收它的进程停止,但不会终止。进程保持在停止状态,直到收到 SIGCONT 信号。
  • SIGCHLD: 当子进程改变其状态(例如,停止、继续或终止)时,父进程会收到这个信号。
  • SIGCONT: 这个信号会使停止的进程继续执行

我们通过一个示例来演示父进程和子进程之间的这些信号交互:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handle_sigchld(int sig) {
    printf("Parent received SIGCHLD\n");
}

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // Child process
        printf("Child process is stopping itself...\n");
        kill(getpid(), SIGSTOP);
        printf("Child process is continuing...\n");
        sleep(1); // Simulate some work
        printf("Child process is exiting...\n");
        exit(0);
    } else if (pid > 0) {
        // Parent process
        signal(SIGCHLD, handle_sigchld);
        printf("Parent process is waiting for child to stop...\n");
        pause(); // Wait for SIGCHLD
        printf("Parent is signaling child to continue...\n");
        kill(pid, SIGCONT);
        printf("Parent is waiting for child to exit...\n");
        wait(NULL); // Wait for child to exit
        printf("Parent process is done.\n");
    } else {
        // Fork failed
        perror("fork");
        exit(1);
    }

    return 0;
}
  1. 父进程创建子进程:父进程使用 fork() 系统调用创建一个子进程。
  2. 子进程发送 SIGSTOP 给自己:子进程使用 kill() 系统调用向自己发送 SIGSTOP 信号,导致子进程停止。
  3. 父进程收到 SIGCHLD 信号:因为子进程的状态发生了变化,父进程会收到一个 SIGCHLD 信号。
  4. 父进程发送 SIGCONT 给子进程:父进程可以使用 kill() 系统调用来发送 SIGCONT 信号给子进程,使其从停止状态继续执行。
  5. 子进程继续执行:子进程收到 SIGCONT 信号后,会从停止状态恢复,并继续执行。
  6. 父进程再次收到 SIGCHLD 信号:因为子进程的状态再次发生了变化(从停止到继续),父进程会再次收到一个 SIGCHLD 信号。
  7. 子进程退出:子进程完成后,它会退出。这会向父进程发送另一个 SIGCHLD 信号。
  8. 父进程处理子进程的退出:父进程需要调用 wait() 或 waitpid() 系统调用来获取子进程的状态信息,并防止子进程成为僵尸进程。

以下为流程示意图:

信号与异常类似吗?

有些编程语言可以使用 try-throw-catch 等结构来处理异常。信号与异常并不相似。相反,失败的系统或库调用会返回非零的退出代码。当进程被终止时,它的退出代码将是 128 加上信号号。例如,被 SIGKILL 杀死的进程将返回 137 (128+9)

Linux信号是同步的还是异步的?

Linux信号可以是同步的也可以是异步的,这取决于信号的触发方式和发送时机。

同步信号是由于指令导致了不可恢复的错误(如非法地址访问)而产生的。这些信号会发送给导致错误的线程。比如

  • 当进程执行了一个非法操作(如访问非法内存、除以零等)时,内核会同步地发送一个信号(如 SIGSEGV)给该进程。
  • 当进程接收到一个系统调用(如 read、write)的请求时,如果该请求不能立即完成,进程可能会被同步地挂起,直到请求完成或超时。

这些信号也被称为陷阱,因为它们也会向内核陷阱处理程序发送陷阱信号。

异步信号是当前执行上下文的外部信号(是由其他进程或内核在某个事件发生时发送给目标进程的)从另一个进程发送 SIGKILL 就是一个例子。这些信号也称为软件中断。比如:

  • 当一个进程想要通知另一个进程某个事件已经发生时,它会发送一个信号(如 SIGUSR1)给目标进程。这是一种典型的异步通信方式。
  • 当子进程改变其状态(例如,停止、继续或终止)时,内核会异步地发送 SIGCHLD 信号给父进程。父进程可以注册一个信号处理函数来处理这个信号,例如,获取子进程的状态信息或者防止子进程成为僵尸进程。

信号的典型生命周期是怎样的?

信号经过三个阶段:

  1. 生成:信号可由内核或任何进程生成。无论谁生成信号,都会将其发送给特定进程。信号用数字表示,没有额外的数据或参数。因此,信号是轻量级的。不过,POSIX 实时信号可以传递额外的数据。可以产生信号的系统调用和函数包括 raise、kill、killpg、pthread_kill、tgkill 和 sigqueue
  2. 传递:一个信号在被传递之前被称为待处理信号。通常情况下,内核会尽快向进程发送信号。但是,如果进程阻塞了信号,它将一直处于待处理状态,直到被解除阻塞为止
  3. 处理:信号一旦发出,就会以多种方式之一进行处理。
  4. 每个信号都有一个相关的默认操作:忽略信号;
  5. 或终止进程,有时会进行核心转储;
  6. 或停止/继续进程。
  7. 对于非默认行为,可以调用处理函数。具体会发生哪种情况,可通过 sigaction 函数指定

阻塞和解除阻塞信号

信号会中断程序的正常执行流程。当进程正在执行一些关键代码或更新与信号处理器共享的数据时,这种情况是不可取的。阻塞可以解决这个问题。但代价是信号处理会延迟

每个进程都可以指定是否要阻止某个特定信号。如果被阻止,而信号确实发生了,操作系统将把信号作为待处理信号保留。一旦进程解除阻塞,信号就会发送。当前阻塞信号的集合称为信号掩码。

无限期地阻塞信号是没有意义的。因此,进程可以在信号发送后忽略它。

一个进程屏蔽的信号不会影响其他进程,其他进程可以正常接收信号。

信号屏蔽可以使用 sigprocmask(单线程)或 pthread_sigmask(多线程)设置。当一个进程有多个线程时,可以按线程阻塞信号。信号将传递给任何一个未阻塞它的线程。

信号处理器以进程为单位,信号屏蔽以线程为单位。

一个进程可以有多个待发信号吗?

是的,一个进程可以有许多标准信号待处理。但是,特定信号类型只能有一个实例处于待处理状态。这是因为信号的挂起和阻塞是作为位掩码实现的,每个信号类型只有一个位。

例如,我们可以同时挂起 SIGALRM 和 SIGTERM 信号,但不能挂起两个 SIGALRM 信号。即使 SIGALRM 信号被多次触发,进程也只会收到一个 SIGALRM 信号

对于实时信号,信号可以与数据一起排队,这样每个信号实例都可以单独传递和处理。

POSIX 并未规定标准信号的传送顺序,也未说明如果标准信号和实时信号都待处理会发生什么情况。Linux 优先处理标准信号。对于实时信号,编号较低的信号先发送,如果一个信号类型有多个队列,则最早的一个先发送。

相关推荐

程序员:JDK的安装与配置(完整版)_jdk的安装方法

对于Java程序员来说,jdk是必不陌生的一个词。但怎么安装配置jdk,对新手来说确实头疼的一件事情。我这里以jdk10为例,详细的说明讲解了jdk的安装和配置,如果有不明白的小伙伴可以评论区留言哦下...

Linux中安装jdk并配置环境变量_linux jdk安装教程及环境变量配置

一、通过连接工具登录到Linux(我这里使用的Centos7.6版本)服务器连接工具有很多我就不一一介绍了今天使用比较常用的XShell工具登录成功如下:二、上传jdk安装包到Linux服务器jdk...

麒麟系统安装JAVA JDK教程_麒麟系统配置jdk

检查检查系统是否自带java在麒麟系统桌面空白处,右键“在终端打开”,打开shell对话框输入:java–version查看是否自带java及版本如图所示,系统自带OpenJDK,要先卸载自带JDK...

学习笔记-Linux JDK - 安装&amp;配置

前提条件#检查是否存在JDKrpm-qa|grepjava#删除现存JDKyum-yremovejava*安装OracleJDK不分系统#进入安装文件目...

Linux新手入门系列:Linux下jdk安装配置

本系列文章是把作者刚接触和学习Linux时候的实操记录分享出来,内容主要包括Linux入门的一些理论概念知识、Web程序、mysql数据库的简单安装部署,希望能够帮到一些初学者,少走一些弯路。注意:L...

测试员必备:Linux下安装JDK 1.8你必须知道的那些事

1.简介在Oracle收购Sun后,Java的一系列产品就被整合到Oracle官网中,打开官网乍眼一看也不知道去哪里下载,还得一个一个的摸索尝试,而且网上大多数都是一些Oracle收购Sun前,或者就...

Linux 下安装JDK17_linux 安装jdk1.8 yum

一、安装环境操作系统:JDK版本:17二、安装步骤第一步:下载安装包下载Linux环境下的jdk1.8,请去官网(https://www.oracle.com/java/technologies/do...

在Ubuntu系统中安装JDK 17并配置环境变量教程

在Ubuntu系统上安装JDK17并配置环境变量是Java开发环境搭建的重要步骤。JDK17是Oracle提供的长期支持版本,广泛用于开发Java应用程序。以下是详细的步骤,帮助你在Ubuntu系...

如何在 Linux 上安装 Java_linux安装java的步骤

在桌面上拥抱Java应用程序,然后在所有桌面上运行它们。--SethKenlon(作者)无论你运行的是哪种操作系统,通常都有几种安装应用程序的方法。有时你可能会在应用程序商店中找到一个应用程序...

Windows和Linux环境下的JDK安装教程

JavaDevelopmentKit(简称JDK),是Java开发的核心工具包,提供了Java应用程序的编译、运行和开发所需的各类工具和类库。它包括了JRE(JavaRuntimeEnviro...

linux安装jdk_linux安装jdk软连接

JDK是啥就不用多介绍了哈,外行的人也不会进来看我的博文。依然记得读大学那会,第一次实验课就是在机房安装jdk,编写HelloWorld程序。时光飞逝啊,一下过了十多年了,挣了不少钱,买了跑车,娶了富...

linux安装jdk,全局配置,不同用户不同jdk

jdk1.8安装包链接:https://pan.baidu.com/s/14qBrh6ZpLK04QS8ogCepwg提取码:09zs上传文件解压tar-zxvfjdk-8u152-linux-...

运维大神教你在linux下安装jdk8_linux安装jdk1.7

1.到官网下载适合自己机器的版本。楼主下载的是jdk-8u66-linux-i586.tar.gzhttp://www.oracle.com/technetwork/java/javase/downl...

window和linux安装JDK1.8_linux 安装jdk1.8.tar

Windows安装JDK1.8的步骤:步骤1:下载JDK打开浏览器,找到JDK下载页面https://d.injdk.cn/download/oraclejdk/8在页面中找到并点击“下载...

最全的linux下安装JavaJDK的教程(图文详解)不会安装你来打我?

默认已经有了linux服务器,且有root账号首先检查一下是否已经安装过java的jdk任意位置输入命令:whichjava像我这个已经安装过了,就会提示在哪个位置,你的肯定是找不到。一般我们在...

取消回复欢迎 发表评论: