引言
在操作系统的世界里,信号是一种用于进程间通信和控制的重要机制。信号能够在不同的进程之间传递异步事件,通知进程发生了某种情况。在Linux系统中,信号的使用是非常普遍且重要的,尤其是在处理进程控制、异常处理和进程间通信时。本文将带你深入了解Linux系统中的信号机制,从基本概念到高级应用,全面覆盖信号的生成、阻塞、捕捉和处理。通过对信号的深入理解和实际操作,你将能够更好地控制和管理进程,提高程序的健壮性和可靠性。
1. 信号入门
1.1 生活角度的信号
为了更好地理解信号,我们可以从生活中的例子开始。假设你在网上购买了很多商品,并且正在等待快递的到来。你知道快递员什么时候到达,但你不需要时刻守在门口。相反,当快递员到达时,他会给你打电话或者发送短信通知你。这时候,你可能正在做其他事情,比如玩游戏。你会在方便的时候去取快递,而不是立即去取。这就像信号处理一样,信号的产生是异步的,你可以在适当的时候处理它们。
- 快递通知:当快递员到达时,他会通知你,这就像操作系统向进程发送信号。信号是操作系统提供的一种异步通知机制,可以在任何时刻通知进程发生了某种事件。
- 延迟处理:你可以在适当的时候处理快递,而不是立即去取,就像信号可以被阻塞,直到进程准备好处理它们。信号的处理可以延迟到进程空闲时,保证进程的重要任务不被中断。
- 处理快递:你可以选择不同的处理方式,比如打开快递(默认处理)、转送给别人(自定义处理)或者忽略它(忽略信号)。同样地,信号的处理也有多种方式,可以执行默认动作、捕捉处理或忽略信号。
1.2 技术应用角度的信号
在技术应用中,信号是一种软中断机制,用于通知进程发生了特定的事件。例如,当你在Shell中运行一个前台进程并按下Ctrl-C
时,会产生一个SIGINT信号,通知该进程应该终止。
信号的技术应用主要包括以下几个方面:
- 进程控制:信号可以用于控制进程的执行,例如终止进程、暂停进程和恢复进程等。常见的信号包括SIGINT(中断进程)、SIGTERM(终止进程)和SIGSTOP(暂停进程)。
- 异常处理:信号可以用于处理进程运行时的异常情况,例如内存访问错误、除零错误和非法指令等。常见的信号包括SIGSEGV(段错误)、SIGFPE(浮点异常)和SIGILL(非法指令)。
- 进程间通信:信号可以用于进程间的简单通信,例如通知子进程或父进程完成某个任务。常见的信号包括SIGCHLD(子进程状态变化)和SIGUSR1、SIGUSR2(用户定义信号)。
以下是一个简单的程序示例,它会一直等待信号:
c
#include <stdio.h>
#include <unistd.h>
int main() {
while (1) {
printf("I am a process, I am waiting for a signal!\n");
sleep(1);
}
return 0;
}
当你在终端中运行这个程序,并按下Ctrl-C
时,程序会收到一个SIGINT信号并终止。信号的产生和处理是异步的,这意味着进程在运行过程中可以随时接收到信号,并根据设定的处理方式进行处理。
1.3 注意事项
在使用信号时,有一些重要的注意事项需要牢记:
- 前台与后台进程 :
Ctrl-C
产生的信号只能发送给前台进程。如果你将命令放到后台运行,Shell不会等待进程结束。 - 异步性:信号相对于进程的控制流程是异步的,进程在运行时随时可能接收到信号。
- 信号丢失:如果多个相同的信号在进程未处理完第一个信号时到达,除实时信号外,其他信号可能会丢失。
- 信号处理的可重入性 :信号处理函数应该是可重入的,即信号处理函数不应该调用不可重入的函数,如
malloc
、printf
等。
1.4 信号的概念
信号是进程之间事件异步通知的一种方式,属于软中断。每个信号都有一个编号和一个宏定义名称,如SIGINT、SIGTERM等。信号可以被进程捕捉并处理,也可以被进程忽略或采用默认处理动作。常见的信号包括:
- SIGINT (2):中断进程,通常由用户按
Ctrl-C
产生。 - SIGTERM (15):终止进程,可以由
kill
命令发送。 - SIGKILL(9):强制终止进程,无法被捕捉或忽略。
- SIGSTOP(19):暂停进程,无法被捕捉或忽略。
- SIGCONT(18):恢复暂停的进程。
- SIGSEGV(11):段错误,通常由于非法内存访问引起。
- SIGPIPE(13):写入一个没有读端的管道时产生。
2. 信号的产生
信号可以通过多种方式产生,包括终端按键、系统函数和软件条件等。
2.1 通过终端按键产生信号
在Shell中,用户可以通过按下特定的键来产生信号。例如,Ctrl-C
会产生SIGINT信号,Ctrl-\
会产生SIGQUIT信号。以下是一个简单的程序,验证这两个信号的默认处理动作:
c
#include <stdio.h>
#include <unistd.h>
int main() {
while (1) {
printf("Running... Press Ctrl-C or Ctrl-\\ to test signal handling\n");
sleep(1);
}
return 0;
}
运行这个程序并按下Ctrl-C
,你会看到程序被终止。同样,按下Ctrl-\
会使程序终止并生成一个核心转储文件(core dump)。
2.2 调用系统函数向进程发信号
你可以使用kill
命令或kill
函数向进程发送信号。以下是一个示例,展示如何在后台运行一个程序并使用kill
命令发送信号:
sh
$ ./sig & # 将程序放到后台运行
$ kill -SIGTERM <pid> # 向程序发送SIGTERM信号
kill
命令实际上调用了kill
函数,发送信号到指定的进程。使用kill
函数可以在程序中实现同样的功能:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
while (1) {
printf("Child process running...\n");
sleep(1);
}
} else {
// 父进程
sleep(3);
printf("Sending SIGTERM to child process\n");
kill(pid, SIGTERM);
wait(NULL);
}
return 0;
}
在这个示例中,父进程在3秒后发送SIGTERM信号给子进程,子进程接收到信号后终止。
2.3 由软件条件产生信号
软件条件也可以产生信号,例如管道破裂(SIGPIPE)和定时器超时(SIGALRM)。以下是一个示例,使用alarm
函数设置一个定时器,触发SIGALRM信号:
c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void alarm_handler(int sig) {
printf("Alarm signal received\n");
}
int main() {
signal(SIGALRM, alarm_handler);
alarm(2); // 设置2秒的定时器
while (1) {
printf("Waiting for alarm...\n");
sleep(1);
}
return 0;
}
在这个示例中,程序会在2秒后接收到SIGALRM信号,并调用自定义的信号处理函数。
2.4 硬件异常产生信号
硬件异常也会产生信号,例如除以零(SIGFPE)和非法内存访问(SIGSEGV)。以下是一个示
例,模拟非法内存访问并捕捉SIGSEGV信号:
c
#include <stdio.h>
#include <signal.h>
void segv_handler(int sig) {
printf("Segmentation fault captured\n");
_exit(1);
}
int main() {
signal(SIGSEGV, segv_handler);
int *p = NULL;
*p = 100; // 触发非法内存访问
return 0;
}
在这个示例中,程序会触发一个段错误,并调用自定义的信号处理函数。
3. 信号捕捉
信号捕捉是指定义一个信号处理函数,在信号递达时执行该函数。通过信号捕捉,可以自定义信号的处理方式,例如记录日志、清理资源等。
3.1 使用signal
函数捕捉信号
signal
函数用于设置信号处理程序。以下是一个示例,展示如何使用signal
函数捕捉SIGINT信号:
c
#include <stdio.h>
#include <signal.h>
void sigint_handler(int sig) {
printf("Caught SIGINT signal\n");
}
int main() {
signal(SIGINT, sigint_handler);
while (1) {
printf("Press Ctrl-C to generate SIGINT signal\n");
sleep(1);
}
return 0;
}
在这个示例中,当你按下Ctrl-C
时,程序会捕捉到SIGINT信号并调用自定义的信号处理函数。
3.2 使用sigaction
函数捕捉信号
sigaction
函数提供了更强大的信号捕捉功能,可以设置额外的选项,如信号屏蔽和实时信号处理。以下是一个示例,展示如何使用sigaction
函数捕捉SIGINT信号:
c
#include <stdio.h>
#include <signal.h>
#include <string.h>
void sigint_handler(int sig) {
printf("Caught SIGINT signal\n");
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigint_handler;
sigaction(SIGINT, &sa, NULL);
while (1) {
printf("Press Ctrl-C to generate SIGINT signal\n");
sleep(1);
}
return 0;
}
在这个示例中,我们使用sigaction
函数设置SIGINT信号的处理程序,并且可以指定更多的选项,如信号屏蔽和处理标志。
3.3 可重入函数
在信号处理函数中,使用可重入函数是非常重要的。可重入函数是指可以在任何时刻中断的函数,通常不使用全局变量或静态变量。以下是一个示例,展示如何编写可重入的信号处理函数:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void sigint_handler(int sig) {
flag = 1;
}
int main() {
signal(SIGINT, sigint_handler);
while (!flag) {
printf("Waiting for SIGINT signal\n");
sleep(1);
}
printf("SIGINT signal received, exiting\n");
return 0;
}
在这个示例中,我们使用volatile
和sig_atomic_t
关键字确保信号处理函数是可重入的。
4. 信号阻塞与未决
4.1 信号阻塞
信号阻塞是指进程暂时不处理某些信号,而是将它们保持在未决状态,直到解除阻塞。通过信号阻塞,可以防止在关键代码段中断进程。
使用sigprocmask
函数阻塞信号
以下是一个示例,展示如何使用sigprocmask
函数阻塞SIGINT信号:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("Caught SIGINT signal\n");
}
int main() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
signal(SIGINT, sigint_handler);
// 阻塞SIGINT信号
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT signal blocked\n");
sleep(5);
// 解除阻塞SIGINT信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("SIGINT signal unblocked\n");
while (1) {
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个示例中,程序会在开始时阻塞SIGINT信号,5秒后解除阻塞。期间按下Ctrl-C
不会终止程序,而是在解除阻塞后立即处理该信号。
4.2 信号未决
信号未决是指信号已产生但未被递达的状态。当信号被阻塞时,进入未决状态。可以使用sigpending
函数查看未决信号。
使用sigpending
函数查看未决信号
以下是一个示例,展示如何使用sigpending
函数查看未决信号:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("Caught SIGINT signal\n");
}
int main() {
sigset_t set, pending;
sigemptyset(&set);
sigaddset(&set, SIGINT);
signal(SIGINT, sigint_handler);
// 阻塞SIGINT信号
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT signal blocked\n");
sleep(5);
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("SIGINT signal is pending\n");
}
// 解除阻塞SIGINT信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("SIGINT signal unblocked\n");
while (1) {
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个示例中,程序在阻塞SIGINT信号后查看未决信号,并在解除阻塞后处理该信号。
5. 信号的高级处理
5.1 信号的默认处理动作
每个信号都有一个默认的处理动作,包括终止进程、忽略信号和生成核心转储文件等。你可以通过自定义信号处理函数改变信号的处理方式。
信号的默认处理动作分为以下几种:
- 终止进程:如SIGINT、SIGTERM和SIGKILL信号,默认会终止进程。
- 生成核心转储文件:如SIGSEGV和SIGABRT信号,默认会终止进程并生成核心转储文件。
- 忽略信号:如SIGCHLD和SIGURG信号,默认会被忽略。
- 暂停进程:如SIGSTOP和SIGTSTP信号,默认会暂停进程。
- 恢复进程:如SIGCONT信号,默认会恢复暂停的进程。
5.2 核心转储文件
核心转储文件(core dump)是指当进程异常终止时,将进程的内存状态保存到磁盘上的文件。核心转储文件用于调试进程异常终止时的情况,可以帮助开发者查找和修复程序中的错误。
可以使用ulimit
命令设置允许生成核心转储文件的大小。例如,设置允许生成最大为1024KB的核心转储文件:
sh
$ ulimit -c 1024
以下是一个示例,展示如何生成核心转储文件:
c
#include <stdio.h>
#include <signal.h>
int main() {
int *p = NULL;
*p = 100; // 触发段错误
return 0;
}
运行这个程序会生成一个核心转储文件,可以使用调试器(如gdb)检查该文件以查找错误原因:
sh
$ gdb ./a.out core
(gdb) bt
5.3 竞态条件和可重入函数
竞态条件是指多个进程或线程并发执行时,由于缺乏同步机制,导致程序的运行结果不确定的情况。在信号处理函数中,避免使用不可重入函数,如malloc
、free
和标准I/O库函数,因为它们可能会引发竞态条件。
以下是一个示例,展示竞态条件的发生:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int count = 0;
void handler(int sig) {
count++;
printf("Signal received, count = %d\n", count);
}
int main() {
signal(SIGINT, handler);
while (count < 5) {
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个示例中,如果信号
处理函数在count
变量的修改过程中被中断,可能会导致count
值不正确。
5.4 使用volatile
关键字
volatile
关键字用于告诉编译器变量的值可能随时发生变化,避免编译器对变量进行优化。在信号处理函数中,使用volatile
关键字可以确保变量的正确性。
以下是一个示例,展示如何使用volatile
关键字:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void sigint_handler(int sig) {
flag = 1;
}
int main() {
signal(SIGINT, sigint_handler);
while (!flag) {
printf("Waiting for SIGINT signal\n");
sleep(1);
}
printf("SIGINT signal received, exiting\n");
return 0;
}
在这个示例中,我们使用volatile
关键字确保flag
变量的正确性。
6. SIGCHLD信号
当子进程终止时,会向父进程发送SIGCHLD信号。父进程可以通过捕捉SIGCHLD信号处理子进程的终止,避免产生僵尸进程。
6.1 使用SIGCHLD信号处理子进程终止
以下是一个示例,展示如何使用SIGCHLD信号处理子进程的终止:
c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
void sigchld_handler(int sig) {
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("Child process %d terminated\n", pid);
}
}
int main() {
signal(SIGCHLD, sigchld_handler);
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
printf("Child process %d\n", getpid());
sleep(2);
exit(EXIT_SUCCESS);
} else {
printf("Parent process %d\n", getpid());
while (1) {
printf("Parent process is doing something\n");
sleep(1);
}
}
return 0;
}
在这个示例中,父进程通过捕捉SIGCHLD信号处理子进程的终止,避免产生僵尸进程。
7. SIGCHLD信号的处理机制
7.1 wait和waitpid函数
在处理子进程终止时,父进程通常会使用wait
或waitpid
函数等待子进程的结束并获取其退出状态。以下是这两个函数的基本用法:
- wait函数 :
wait
函数会阻塞父进程,直到有子进程结束为止。
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- waitpid函数 :
waitpid
函数提供了更灵活的等待方式,可以指定等待特定的子进程,并且可以选择阻塞或非阻塞等待。
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
在信号处理函数中,通常使用waitpid
函数并指定WNOHANG
选项,以非阻塞方式等待所有结束的子进程。
7.2 自动清理子进程
除了使用SIGCHLD信号处理子进程的终止,还可以通过设置SIGCHLD信号的默认处理动作为忽略,自动清理子进程,避免产生僵尸进程。
以下是一个示例,展示如何通过设置SIGCHLD信号的默认处理动作为忽略,自动清理子进程:
c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction(SIGCHLD, &sa, NULL);
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
printf("Child process %d\n", getpid());
sleep(2);
exit(EXIT_SUCCESS);
} else {
printf("Parent process %d\n", getpid());
while (1) {
printf("Parent process is doing something\n");
sleep(1);
}
}
return 0;
}
在这个示例中,父进程通过设置SIGCHLD信号的默认处理动作为忽略,自动清理子进程,避免产生僵尸进程。
8. 阻塞信号
8.1 信号的阻塞与未决
信号阻塞是指进程暂时不处理某些信号,而是将它们保持在未决状态,直到解除阻塞。信号未决是指信号已产生但未被递达的状态。
8.2 使用sigprocmask函数
sigprocmask
函数用于设置进程的信号屏蔽字,可以阻塞或解除阻塞信号。以下是一个示例,展示如何使用sigprocmask
函数阻塞和解除阻塞信号:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("Caught SIGINT signal\n");
}
int main() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
signal(SIGINT, sigint_handler);
// 阻塞SIGINT信号
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT signal blocked\n");
sleep(5);
// 解除阻塞SIGINT信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("SIGINT signal unblocked\n");
while (1) {
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个示例中,程序会在开始时阻塞SIGINT信号,5秒后解除阻塞。期间按下Ctrl-C
不会终止程序,而是在解除阻塞后立即处理该信号。
8.3 使用sigpending函数查看未决信号
sigpending
函数用于查看进程的未决信号。以下是一个示例,展示如何使用sigpending
函数查看未决信号:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("Caught SIGINT signal\n");
}
int main() {
sigset_t set, pending;
sigemptyset(&set);
sigaddset(&set, SIGINT);
signal(SIGINT, sigint_handler);
// 阻塞SIGINT信号
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT signal blocked\n");
sleep(5);
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("SIGINT signal is pending\n");
}
// 解除阻塞SIGINT信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("SIGINT signal unblocked\n");
while (1) {
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个示例中,程序在阻塞SIGINT信号后查看未决信号,并在解除阻塞后处理该信号。
9. 可重入函数
9.1 什么是可重入函数
可重入函数是指可以在任何时刻被中断,并且在中断后可以正确地继续执行的函数。可重入函数通常不使用全局变量或静态变量,不依赖于不可重入的函数,如malloc
、free
和标准I/O库函数。
9.2 编写可重入函数
在编写信号处理函数时,确保函数是可重入的非常重要。以下是一个示例,展示如何编写可重入的信号处理函数:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void sigint_handler(int sig) {
flag = 1;
}
int main() {
signal(SIGINT, sigint_handler);
while (!flag) {
printf("Waiting for SIGINT signal\n");
sleep(1);
}
printf("SIGINT signal received, exiting\n");
return 0;
}
在这个示例中,我们使用volatile
和sig_atomic_t
关键字确保信号处理函数是可重入的。
9.3 常见的不可重入函数
在信号处理函数中,应该避免使用以下不可重入函数:
- 动态内存分配函数 :如
malloc
、calloc
和free
。
2
. 标准I/O库函数 :如printf
、sprintf
和fgets
。
-
时间和日期函数 :如
ctime
、localtime
和gmtime
。 -
环境变量函数 :如
getenv
和putenv
。
这些函数通常使用全局数据结构或静态数据结构,不适合在信号处理函数中使用。
10. 信号处理的高级话题
10.1 核心转储文件
核心转储文件(core dump)是指当进程异常终止时,将进程的内存状态保存到磁盘上的文件。核心转储文件用于调试进程异常终止时的情况,可以帮助开发者查找和修复程序中的错误。
以下是一个示例,展示如何生成核心转储文件:
c
#include <stdio.h>
#include <signal.h>
int main() {
int *p = NULL;
*p = 100; // 触发段错误
return 0;
}
运行这个程序会生成一个核心转储文件,可以使用调试器(如gdb)检查该文件以查找错误原因:
sh
$ gdb ./a.out core
(gdb) bt
10.2 竞态条件和信号处理
竞态条件是指多个进程或线程并发执行时,由于缺乏同步机制,导致程序的运行结果不确定的情况。在信号处理函数中,避免使用不可重入函数,如malloc
、free
和标准I/O库函数,因为它们可能会引发竞态条件。
以下是一个示例,展示竞态条件的发生:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int count = 0;
void handler(int sig) {
count++;
printf("Signal received, count = %d\n", count);
}
int main() {
signal(SIGINT, handler);
while (count < 5) {
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个示例中,如果信号处理函数在count
变量的修改过程中被中断,可能会导致count
值不正确。
10.3 使用volatile关键字
volatile
关键字用于告诉编译器变量的值可能随时发生变化,避免编译器对变量进行优化。在信号处理函数中,使用volatile
关键字可以确保变量的正确性。
以下是一个示例,展示如何使用volatile
关键字:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void sigint_handler(int sig) {
flag = 1;
}
int main() {
signal(SIGINT, sigint_handler);
while (!flag) {
printf("Waiting for SIGINT signal\n");
sleep(1);
}
printf("SIGINT signal received, exiting\n");
return 0;
}
在这个示例中,我们使用volatile
关键字确保flag
变量的正确性。
11. 信号的实际应用
11.1 使用信号实现定时任务
信号可以用于实现定时任务,例如定时器。以下是一个示例,展示如何使用SIGALRM信号实现定时任务:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler(int sig) {
printf("Timer expired\n");
alarm(2); // 重新设置定时器
}
int main() {
signal(SIGALRM, alarm_handler);
alarm(2); // 设置2秒的定时器
while (1) {
printf("Waiting for timer...\n");
sleep(1);
}
return 0;
}
在这个示例中,程序会每隔2秒打印一次"Timer expired",实现定时任务的功能。
11.2 使用信号处理进程间通信
信号可以用于进程间的简单通信,例如通知子进程或父进程完成某个任务。以下是一个示例,展示如何使用SIGUSR1信号实现进程间通信:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigusr1_handler(int sig) {
printf("Received SIGUSR1 signal\n");
}
int main() {
signal(SIGUSR1, sigusr1_handler);
pid_t pid = fork();
if (pid == 0) {
// 子进程
sleep(2);
kill(getppid(), SIGUSR1);
} else {
// 父进程
pause(); // 等待信号
}
return 0;
}
在这个示例中,子进程在2秒后发送SIGUSR1信号给父进程,父进程接收到信号后调用自定义的信号处理函数。
11.3 使用信号处理多线程程序
在多线程程序中,信号处理需要特别注意,因为信号默认会发送到整个进程,而不是特定的线程。以下是一个示例,展示如何在多线程程序中使用信号:
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
void *thread_func(void *arg) {
printf("Thread %d running\n", *(int *)arg);
while (1) {
sleep(1);
}
return NULL;
}
void sigint_handler(int sig) {
printf("Caught SIGINT signal\n");
}
int main() {
pthread_t threads[2];
int thread_ids[2] = {1, 2};
signal(SIGINT, sigint_handler);
for (int i = 0; i < 2; i++) {
if (pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
}
for (int i = 0; i < 2; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
在这个示例中,主线程设置了SIGINT信号的处理函数,当按下Ctrl-C
时,信号处理函数会被调用。信号处理函数在多线程程序中的使用需要小心,避免使用不可重入的函数。
结论
本文详细介绍了Linux系统中的信号机制,包括信号的生成、阻塞、捕捉和处理。通过具体的示例代码展示了信号的各种应用场景和处理方式。信号是进程间通信和控制的重要工具,理解和掌握信号机制可以帮助你更好地控制和管理进程,提高程序的健壮性和可靠性。希望通过本文的学习,你能更加深入地理解和掌握信号机制,为开发高效、可靠的程序打下坚实的基础。
嗯,就是这样啦,文章到这里就结束啦,真心感谢你花时间来读。
觉得有点收获的话,不妨给我点个赞吧!
如果发现文章有啥漏洞或错误的地方,欢迎私信我或者在评论里提醒一声~