Linux信号深度解析:系统编程中的关键通信手段
sinye56 2024-11-24 21:30 1 浏览 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 呢?原因是:
- 功能强大:sigaction 允许你更精细地控制信号的处理,例如设置信号掩码来在信号处理函数执行期间阻塞特定的信号。
- 可靠性:signal 在某些系统上可能不是线程安全的,而 sigaction 则没有这个问题。
- 向后兼容:虽然 sigaction 是 POSIX 标准引入的,但它也提供了与 signal 的向后兼容性。通过适当的使用 sa_flags 参数,可以模拟 signal 的行为。
- 符合标准:使用 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;
}
- 父进程创建子进程:父进程使用 fork() 系统调用创建一个子进程。
- 子进程发送 SIGSTOP 给自己:子进程使用 kill() 系统调用向自己发送 SIGSTOP 信号,导致子进程停止。
- 父进程收到 SIGCHLD 信号:因为子进程的状态发生了变化,父进程会收到一个 SIGCHLD 信号。
- 父进程发送 SIGCONT 给子进程:父进程可以使用 kill() 系统调用来发送 SIGCONT 信号给子进程,使其从停止状态继续执行。
- 子进程继续执行:子进程收到 SIGCONT 信号后,会从停止状态恢复,并继续执行。
- 父进程再次收到 SIGCHLD 信号:因为子进程的状态再次发生了变化(从停止到继续),父进程会再次收到一个 SIGCHLD 信号。
- 子进程退出:子进程完成后,它会退出。这会向父进程发送另一个 SIGCHLD 信号。
- 父进程处理子进程的退出:父进程需要调用 wait() 或 waitpid() 系统调用来获取子进程的状态信息,并防止子进程成为僵尸进程。
以下为流程示意图:
信号与异常类似吗?
有些编程语言可以使用 try-throw-catch 等结构来处理异常。信号与异常并不相似。相反,失败的系统或库调用会返回非零的退出代码。当进程被终止时,它的退出代码将是 128 加上信号号。例如,被 SIGKILL 杀死的进程将返回 137 (128+9)
Linux信号是同步的还是异步的?
Linux信号可以是同步的也可以是异步的,这取决于信号的触发方式和发送时机。
同步信号是由于指令导致了不可恢复的错误(如非法地址访问)而产生的。这些信号会发送给导致错误的线程。比如
- 当进程执行了一个非法操作(如访问非法内存、除以零等)时,内核会同步地发送一个信号(如 SIGSEGV)给该进程。
- 当进程接收到一个系统调用(如 read、write)的请求时,如果该请求不能立即完成,进程可能会被同步地挂起,直到请求完成或超时。
这些信号也被称为陷阱,因为它们也会向内核陷阱处理程序发送陷阱信号。
异步信号是当前执行上下文的外部信号(是由其他进程或内核在某个事件发生时发送给目标进程的)从另一个进程发送 SIGKILL 就是一个例子。这些信号也称为软件中断。比如:
- 当一个进程想要通知另一个进程某个事件已经发生时,它会发送一个信号(如 SIGUSR1)给目标进程。这是一种典型的异步通信方式。
- 当子进程改变其状态(例如,停止、继续或终止)时,内核会异步地发送 SIGCHLD 信号给父进程。父进程可以注册一个信号处理函数来处理这个信号,例如,获取子进程的状态信息或者防止子进程成为僵尸进程。
信号的典型生命周期是怎样的?
信号经过三个阶段:
- 生成:信号可由内核或任何进程生成。无论谁生成信号,都会将其发送给特定进程。信号用数字表示,没有额外的数据或参数。因此,信号是轻量级的。不过,POSIX 实时信号可以传递额外的数据。可以产生信号的系统调用和函数包括 raise、kill、killpg、pthread_kill、tgkill 和 sigqueue
- 传递:一个信号在被传递之前被称为待处理信号。通常情况下,内核会尽快向进程发送信号。但是,如果进程阻塞了信号,它将一直处于待处理状态,直到被解除阻塞为止
- 处理:信号一旦发出,就会以多种方式之一进行处理。
- 每个信号都有一个相关的默认操作:忽略信号;
- 或终止进程,有时会进行核心转储;
- 或停止/继续进程。
- 对于非默认行为,可以调用处理函数。具体会发生哪种情况,可通过 sigaction 函数指定
阻塞和解除阻塞信号
信号会中断程序的正常执行流程。当进程正在执行一些关键代码或更新与信号处理器共享的数据时,这种情况是不可取的。阻塞可以解决这个问题。但代价是信号处理会延迟
每个进程都可以指定是否要阻止某个特定信号。如果被阻止,而信号确实发生了,操作系统将把信号作为待处理信号保留。一旦进程解除阻塞,信号就会发送。当前阻塞信号的集合称为信号掩码。
无限期地阻塞信号是没有意义的。因此,进程可以在信号发送后忽略它。
一个进程屏蔽的信号不会影响其他进程,其他进程可以正常接收信号。
信号屏蔽可以使用 sigprocmask(单线程)或 pthread_sigmask(多线程)设置。当一个进程有多个线程时,可以按线程阻塞信号。信号将传递给任何一个未阻塞它的线程。
信号处理器以进程为单位,信号屏蔽以线程为单位。
一个进程可以有多个待发信号吗?
是的,一个进程可以有许多标准信号待处理。但是,特定信号类型只能有一个实例处于待处理状态。这是因为信号的挂起和阻塞是作为位掩码实现的,每个信号类型只有一个位。
例如,我们可以同时挂起 SIGALRM 和 SIGTERM 信号,但不能挂起两个 SIGALRM 信号。即使 SIGALRM 信号被多次触发,进程也只会收到一个 SIGALRM 信号
对于实时信号,信号可以与数据一起排队,这样每个信号实例都可以单独传递和处理。
POSIX 并未规定标准信号的传送顺序,也未说明如果标准信号和实时信号都待处理会发生什么情况。Linux 优先处理标准信号。对于实时信号,编号较低的信号先发送,如果一个信号类型有多个队列,则最早的一个先发送。
相关推荐
- RHEL8和CentOS8怎么重启网络
-
本文主要讲解如何重启RHEL8或者CentOS8网络以及如何解决RHEL8和CentOS8系统的网络管理服务报错,当我们安装好RHEL8或者CentOS8,重启启动网络时,会出现以下报错:...
- Linux 内、外网双网卡路由配置
-
1.路由信息的影响Linux系统中如果有多张网卡的情况下,如果路由信息配置不正确,...
- Linux——centos7修改网卡名
-
修改网卡名这个操作可能平时用不太上,可作为了解。修改网卡默认名从ens33改成eth01.首先修改网卡配置文件名(建议将原配置文件进行备份)...
- CentOS7下修改网卡名称为ethX的操作方法
-
?Linux操作系统的网卡设备的传统命名方式是eth0、eth1、eth2等,而CentOS7提供了不同的命名规则,默认是基于固件、拓扑、位置信息来分配。这样做的优点是命名全自动的、可预知的...
- Linux 网卡名称enss33修改为eth0
-
一、CentOS修改/etc/sysconfig/grub文件(修改前先备份)为GRUB_CMDLINE_LINUX变量增加2个参数(net.ifnames=0biosdevname=0),修改完成...
- CentOS下双网卡绑定,实现带宽飞速
-
方式一1.新建/etc/sysconfig/network-scripts/ifcfg-bond0文件DEVICE=bond0IPADDR=191.3.60.1NETMASK=255.255.2...
- linux 双网卡双网段设置路由转发
-
背景网络情况linux双网卡:网卡A(ens3)和网卡B(...
- Linux-VMware设置网卡保持激活
-
Linux系统只有在激活网卡的状态下才能去连接网络,进行网络通讯。修改配置文件(永久激活网卡)...
- VMware虚拟机三种网络模式
-
01.VMware虚拟机三种网络模式由于linux目前很热门,越来越多的人在学习linux,但是买一台服务放家里来学习,实在是很浪费。那么如何解决这个问题?虚拟机软件是很好的选择,常用的虚拟机软件有v...
- 2023年最新版 linux克隆虚拟机 解决网卡uuid重复问题
-
问题描述1、克隆了虚拟机,两台虚拟机里面的ip以及网卡的uuid都是一样的2、ip好改,但是uuid如何改呢?解决问题1、每台主机应该保证网卡的UUID是唯一的,避免后面网络通信有问题...
- Linux网卡的Vlan配置,你可能不了解的玩法
-
如果服务器上连的交换机端口已经预先设置了TRUNK,并允许特定的VLAN可以通过,那么服务器的网卡在配置时就必须指定所属的VLAN,否则就不通了,这种情形在虚拟化部署时较常见。例如在一个办公环境中,办...
- Centos7 网卡绑定
-
1、切换到指定目录#备份网卡数据cd/etc/sysconfig/network-scriptscpifcfg-enp5s0f0ifcfg-enp5s0f0.bak...
- Linux搭建nginx+keepalived 高可用(主备+双主模式)
-
一:keepalived简介反向代理及负载均衡参考:...
- Linux下Route 路由指令使用详解
-
linuxroute命令用于显示和操作IP路由表。要实现两个不同子网之间的通信,需要一台连接两个网络的路由器,或者同时位于两个网络的网关来实现。在Linux系统中,设置路由通常是为了解决以下问题:该...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle忘记用户名密码 (59)
- oracle11gr2安装教程 (55)
- mybatis调用oracle存储过程 (67)
- oracle spool的用法 (57)
- oracle asm 磁盘管理 (67)
- 前端 设计模式 (64)
- 前端面试vue (56)
- linux格式化 (55)
- linux图形界面 (62)
- linux文件压缩 (75)
- Linux设置权限 (53)
- linux服务器配置 (62)
- mysql安装linux (71)
- linux启动命令 (59)
- 查看linux磁盘 (72)
- linux用户组 (74)
- linux多线程 (70)
- linux设备驱动 (53)
- linux自启动 (59)
- linux网络命令 (55)
- linux传文件 (60)
- linux打包文件 (58)
- linux查看数据库 (61)
- linux获取ip (64)
- linux进程通信 (63)