- 信号概念
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号
可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
- 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C
通常会给进程发送一个中断信号。
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给
相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的
内存区域。
系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU
时间超限,或者该进程的某个子进程退出。
运行 kill 命令或调用 kill 函数。
- 使用信号的两个主要目的是:
让进程知道已经发生了一个特定的事情。
强迫进程执行它自己代码中的信号处理程序。
- 信号的特点:
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
查看系统定义的信号列表:kill --l
前 31 个信号为常规信号,其余为实时信号。
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson25$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
信号的 5 种默认处理动作
查看信号的详细信息:man 7 signal
◼ 信号的 5 中默认处理动作
Term 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件(保存进程异常退出的错误信息)
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程
◼ 信号的几种状态:产生、未决、递达
◼ SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作
c
#include <stdio.h>
#include <string.h>
int main() {
char * buf; //指针没有指向合法内存
strcpy(buf, "hello");
return 0;
}
信号相关函数
kill
在编程中,kill
函数通常用于向进程发送信号,通知它进行某种操作,如终止或暂停。这个函数最常见于类 Unix 操作系统,如 Linux 和 macOS,它的功能是通过发送信号来控制进程的行为。
函数定义
在 C 语言中,kill
函数定义如下:
c
#include <signal.h>
int kill(pid_t pid, int sig);
pid_t pid
: 指定要发送信号的进程 ID。如果pid
大于 0,信号将发送给具有该 ID 的进程。如果pid
等于 0,信号将发送给与发送者同一进程组的所有进程。int sig
: 要发送的信号。例如,SIGTERM
是请求终止进程的标准信号,SIGKILL
是强制终止进程的信号。
返回值
- 成功时,
kill
函数返回 0。 - 失败时,返回 -1,并且
errno
被设置为错误的具体原因,比如EINVAL
表示一个无效的信号,或EPERM
表示没有足够的权限来发送信号。
示例
下面是一个使用 kill
函数的简单示例,它向进程发送一个 SIGTERM
信号:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid = 1234; // 假设进程ID为1234
int result = kill(pid, SIGTERM);
if (result == 0) {
printf("Signal sent successfully\n");
} else {
perror("Failed to send signal");
}
return 0;
}
这个函数在系统编程中非常重要,用于进程间通信和进程控制,尤其是在需要清理僵尸进程或优雅地终止服务时非常有用。
在编程中,raise
函数通常用于在当前进程中生成一个信号。这个函数是C语言标准库中的一部分,定义在 <signal.h>
头文件中。它允许程序在其自身的执行流中发送信号,这可以用于各种目的,如触发信号处理程序或模拟外部事件的影响。
raise
函数定义
在 C 语言中,raise
函数的定义如下:
c
#include <signal.h>
int raise(int sig);
int sig
: 要发送的信号代码。例如,SIGINT
通常用于表示中断信号(如Ctrl+C触发的),而SIGTERM
用于请求终止程序。
返回值
- 成功时,
raise
函数返回 0。 - 失败时,返回非0值。
示例
下面是一个使用 raise
函数的简单示例,它向自己发送一个 SIGTERM
信号:
c
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void signal_handler(int signal) {
if (signal == SIGTERM) {
printf("Received SIGTERM, exiting...\n");
exit(0);
}
}
int main() {
if (signal(SIGTERM, signal_handler) == SIG_ERR) {
printf("Error: Cannot set signal handler.\n");
return 1;
}
printf("Raising SIGTERM...\n");
int result = raise(SIGTERM);
if (result != 0) {
perror("Failed to raise signal");
return 1;
}
return 0; // 这行代码永远不会执行,因为signal_handler中已经调用了exit()
}
在这个例子中,raise
函数用于发送 SIGTERM
信号给程序自身,当信号被触发时,通过安装的信号处理程序 signal_handler
捕获并处理这个信号,随后程序退出。
使用场景
raise
函数常用于测试信号处理代码、在特定情况下自我终止程序、或者实现程序内部的通信机制。它提供了一种有效的方式来模拟信号发送,帮助开发者在单个程序内部测试和处理各种信号相关的逻辑。
abort
在编程中,abort
函数用于立即终止当前进程的执行,通常用于处理无法恢复的错误或异常情况。abort
是 C 语言标准库中的一个函数,定义在 <stdlib.h>
头文件中。当调用 abort
时,程序会生成一个 SIGABRT
(abort signal,终止信号)信号,这通常会导致程序异常终止。
函数定义
在 C 语言中,abort
函数的定义如下:
c
#include <stdlib.h>
void abort(void);
- 该函数没有参数。
- 函数声明为
void
返回类型,意味着它不返回任何值。
行为描述
- 终止进程 :
abort
函数会立即终止调用它的进程。 - 信号生成 :调用
abort
通常会生成SIGABRT
信号,该信号的默认行为是终止进程。 - 内存和资源:由于进程是突然终止的,操作系统负责回收任何由进程使用的内存和系统资源。
- 核心转储 :在许多系统上,默认情况下,
abort
的调用会产生一个核心转储文件,这可以用于后续的调试和分析。
示例
下面是一个使用 abort
函数的简单示例,展示了如何在检测到某些错误条件时终止程序:
c
#include <stdlib.h>
#include <stdio.h>
int main() {
printf("Something went horribly wrong!\n");
abort(); // 终止程序
// 下面的代码永远不会执行
printf("This line will never be executed.\n");
return 0;
}
在这个示例中,调用 abort()
会立即终止程序。由于 abort
导致程序异常终止,"This line will never be executed."
这一行将永远不会执行。
使用场景
abort
函数通常在以下情况下使用:
- 错误处理 :在检测到无法恢复的错误或严重异常情况时,使用
abort
来立即终止程序。 - 调试 :在开发过程中,当程序到达不应该到达的代码路径时,使用
abort
来强制停止程序并生成核心转储,便于开发者调试。 - 安全性 :在关键安全验证失败时,使用
abort
以防止潜在的安全漏洞被利用。
abort
是一个强大的工具,用于处理那些需要立即停止程序执行的情况,但它应该谨慎使用,因为它不提供进行任何清理操作的机会,如关闭文件句柄或释放动态分配的内存。
c
/*
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
- 参数:
- pid :
> 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid=某个进程组的ID取反 (-12345)
- sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
kill(getppid(), 9);
kill(getpid(), 9);
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig : 要发送的信号
- 返回值:
- 成功 0
- 失败 非0
kill(getpid(), sig); 实现同样功能
void abort(void);
- 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
kill(getpid(), SIGABRT);
*/
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if(pid == 0) {
// 子进程
int i = 0;
for(i = 0; i < 5; i++) {
printf("child process\n");
sleep(1);
}
} else if(pid > 0) {
// 父进程
printf("parent process\n");
sleep(2);
printf("kill child process now\n");
kill(pid, SIGINT);
}
return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out
parent process
child process
child process
kill child process now
alarm
在编程中,alarm
函数用于设置一个计时器(闹钟),在指定的秒数后发送 SIGALRM
信号到当前进程。这个函数是 Unix 系统的标准部分,主要用于在进程需要在一定时间后接收到通知时创建简单的时间延迟或超时。
函数定义
在 C 语言中,alarm
函数定义在 <unistd.h>
头文件中,其函数原型如下:
c
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
参数:
unsigned int seconds
: 指定在多少秒后产生SIGALRM
信号。如果seconds
为0,则取消任何之前设置的闹钟。
-
返回值:
- 返回值是之前设置的闹钟时间的剩余秒数,如果之前没有设置闹钟,则返回0。
行为描述
- 信号发送 :当
alarm
函数设置的计时器到期时,将向进程发送SIGALRM
信号。如果进程没有捕获或忽略此信号,其默认行为是终止进程。 - 闹钟取消:通过传递参数0可以取消之前设置的计时器。
- 闹钟覆盖 :如果在之前的
alarm
调用之后再次调用alarm
,新的调用将覆盖旧的设置。
示例
下面是一个使用 alarm
函数的示例,该示例设置一个5秒后的计时器,并捕获 SIGALRM
信号:
c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handle_sigalrm(int sig) {
printf("Received SIGALRM - alarm went off!\n");
}
int main() {
// 设置 SIGALRM 的处理函数
signal(SIGALRM, handle_sigalrm);
// 设置 alarm 为5秒
alarm(5);
printf("Alarm set for 5 seconds...\n");
// 等待信号到来,通常这里会做一些其他的工作
pause(); // 使进程挂起直到捕获到一个信号
printf("Continuing execution...\n");
return 0;
}
在这个程序中,通过 alarm
函数设定一个5秒后的计时器,并通过 signal
函数设定 SIGALRM
的处理函数 handle_sigalrm
。程序执行 pause()
函数后会挂起直到收到信号。一旦信号 SIGALRM
被接收,相应的处理函数将被调用,然后程序继续执行。
使用场景
alarm
函数常用于以下场景:
- 实现超时:在执行可能长时间运行的操作时,用于强制超时。
- 周期性任务 :虽然
alarm
只能设置一个定时器,它可以用于触发周期性执行的任务。 - 资源管理:在一段时间后自动释放或检查资源状态。
这个函数提供了一个简单但有效的机制来处理与时间相关的事件,尤其是在需要在指定时间后执行特定操作的场景中非常有用。
c
/*
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
函数会给当前的进程发送一个信号:SIGALARM
- 参数:
seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
取消一个定时器,通过alarm(0)。
- 返回值:
- 之前没有定时器,返回0
- 之前有定时器,返回之前的定时器剩余的时间
- SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
alarm(10); -> 返回0
过了1秒
alarm(5); -> 返回9
alarm(100) -> 该函数是不阻塞的
*/
#include <stdio.h>
#include <unistd.h>
int main() {
int seconds = alarm(5);
printf("seconds = %d\n", seconds); // 0
sleep(2);
seconds = alarm(2); // 不阻塞
printf("seconds = %d\n", seconds); // 3
while(1) {
}
return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out
seconds = 002multiPr
seconds = 3
Alarm clock
c
// 1秒钟电脑能数多少个数?
#include <stdio.h>
#include <unistd.h>
/*
实际的时间 = 内核时间 + 用户时间 + 消耗的时间
进行文件IO操作的时候比较浪费时间
定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/
int main() {
alarm(1);
int i = 0;
while(1) {
printf("%i\n", i++);
}
return 0;
}
setitimer
setitimer
函数是 alarm
函数的更灵活和强大的版本,提供了对进程定时器的精细控制。它允许设置可以重复的定时器,并且具有更高的时间精度。setitimer
在 Unix-like 系统中使用广泛,通常定义在 <sys/time.h>
头文件中。
函数定义
在 C 语言中,setitimer
的定义如下:
c
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
- 参数 :
int which
: 定时器的类型。可选值包括ITIMER_REAL
(减少真实时间,到期发送SIGALRM
)、ITIMER_VIRTUAL
(减少进程虚拟时间,到期发送SIGVTALRM
)、ITIMER_PROF
(减少进程运行时间加上系统时间,到期发送SIGPROF
)。const struct itimerval *new_value
: 指向itimerval
结构的指针,该结构指定新的定时器值。struct itimerval *old_value
: 可选,用于存储之前定时器的值。如果不关心旧值,可以设置为NULL
。
struct itimerval
结构定义如下:
c
struct itimerval {
struct timeval it_interval; // 下一次定时器触发的间隔时间
struct timeval it_value; // 定时器首次触发的延迟时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
it_interval
设置为非零值时,setitimer
会重复触发信号。it_value
设置为零时,定时器被禁用。
返回值
- 成功时,
setitimer
返回0
。 - 失败时,返回
-1
并设置errno
以指示错误原因(如EINVAL
表示参数无效)。
示例
以下是一个使用 setitimer
来每隔一定时间发送 SIGALRM
的示例:
c
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
void timer_handler(int signum) {
static int count = 0;
printf("Timer expired %d times\n", ++count);
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = &timer_handler;
sigaction(SIGALRM, &sa, NULL);
struct itimerval timer;
timer.it_value.tv_sec = 1; // 初始延迟1秒
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 2; // 之后每2秒触发一次
timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &timer, NULL);
while (1) {
sleep(1); // 让程序保持运行以持续接收信号
}
return 0;
}
在这个程序中,定时器首次设置为1秒后触发,之后每2秒重复触发。信号处理函数 timer_handler
在每次信号到来时被调用,记录并打印触发次数。
使用场景
setitimer
常用于需要高精度定时或需要定时器重复触发的场合,如:
- 性能监控 :使用
ITIMER_PROF
来监测程序的性能。 - 资源管理:定期检查和管理系统资源。
- 实时应用:在需要精确控制时间的实时应用中,比如多媒体处理或游戏。
这个函数提供了对 Unix 系统中进程定时器的高级控制,但也需要注意使用 setitimer
可能会引入复杂性,特别是在涉及信号和多线程
时。
c
/*
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
- 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
- 参数:
- which : 定时器以什么时间计时
ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM 常用
ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF
- new_value: 设置定时器的属性
struct itimerval { // 定时器的结构体
struct timeval it_interval; // 每个阶段的时间,间隔时间
struct timeval it_value; // 延迟多长时间执行定时器
};
struct timeval { // 时间的结构体
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒
};
过10秒后,每个2秒定时一次
- old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
- 返回值:
成功 0
失败 -1 并设置错误号
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
// 过3秒以后,每隔2秒钟定时一次
int main() {
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
定时器开始了...
Alarm clock
信号捕捉函数
signal
在 C 语言中,signal
函 方式。它是 Unix 和类 Unix 系统中的标准部分,用于定义当特定信号被传递给进程时应执行的行为。signal
函数定义在 <signal.h>
头文件中。
函数定义
signal
函数的原型如下:
c
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
- 参数 :
int sig
: 要处理的信号代码,如SIGINT
(中断信号,通常由 Ctrl+C 产生),SIGTERM
(终止信号),SIGALRM
(由alarm
函数产生的定时信号),等等。void (*func)(int)
: 指向信号处理函数的指针。该函数必须接受一个整型参数(即信号编号)。此参数可以是一个指向函数的指针,用于处理信号;也可以是SIG_IGN
忽略信号,或SIG_DFL
使用信号的默认处理方式。
返回值
- 返回一个指向之前关联到指定信号的处理函数的指针。如果调用失败,返回
SIG_ERR
并设置errno
来指示错误。
示例
下面是一个使用 signal
函数来设置信号处理程序的示例,该示例中处理 SIGINT
信号:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void (int signal) {
printf("Received SIGINT, exiting...\n");
exit(0);
}
int main() {
// 设置 SIGINT 的处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
printf("Failed to set signal handler.\n");
return 1;
}
printf("Waiting for SIGINT...\n");
// 让程序保持运行,等待信号
while(1) {
sleep(1);
}
return 0;
}
在这个程序中,当用户按 Ctrl+C 时,将不会执行默认的终止操作,而是调用 signal_handler
函数,打印一条消息并退出程序。
注意事项和限制
- 可移植性问题 :
signal
函数的行为在不同的系统或库实现之间可能会有所不同。特别是在信号被处理时程序的行为(如信号是否自动重置为默认行为)可能不一致。 - 推荐使用
sigaction
:由于signal
函数的这些可移植性问题,建议在需要精确控制信号处理行为的情况下使用更现代且功能更全的sigaction
函数。sigaction
提供了更详细的控制,包括阻止在信号处理函数执行时送达的其他信号等功能。
signal
函数是处理 Unix 信号的基本工具之一,适用于简单的信号处理需求,但在复杂应用中,更稳定且功能丰富的 sigaction
往往是更好的选择。
c
/*
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 参数:
- signum: 要捕捉的信号
- handler: 捕捉到信号要如何处理
- SIG_IGN : 忽略信号
- SIG_DFL : 使用信号默认的行为
- 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
- 返回值:
成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
失败,返回SIG_ERR,设置错误号
SIGKILL SIGSTOP不能被捕捉,不能被忽略。
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
// 注册信号捕捉
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_DFL);
// void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
signal(SIGALRM, myalarm);
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out
定时器开始了...
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
信号集
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为
信号集的数据结构来表示,其系统数据类型为 sigset_t。
◼ 在 PCB 中有两个非常重要的信号集。一个称之为 "阻塞信号集" (阻塞信号被处理),另一个称之为
"未决信号集" 。这两个信号集都是内核使用位图(二进制位 )机制来实现的。但操作系统不允许我
们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数
来对 PCB 中的这两个信号集进行修改。
◼ 信号的 "未决" 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
◼ 信号的 "阻塞" 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
◼ 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,
所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
信号集相关函数
sigemptyset
在 C 语言中,sigemptyset
函数用于初始化一个信号集,将其清空,即从该集合中移除所有信号。这个函数是信号处理相关函数的一部分,定义在 <signal.h>
头文件中,并且通常与其他信号操作函数一起使用,以控制信号的阻塞、忽略或处理行为。
函数定义
sigemptyset
函数的定义如下:
c
#include <signal.h>
int sigemptyset(sigset_t *set);
- 参数 :
sigset_t *set
: 指向sigset_t
类型的指针,该类型用于存储信号集。
返回值
- 成功时,
sigemptyset
返回0
。 - 失败时,通常情况下,此函数不应失败,因此如果出现非零返回值,应检查代码其他部分是否有错误。
示例
以下是一个如何使用 sigemptyset
函数来初始化信号集的示例:
c
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t signalSet;
int result;
// 初始化信号集为空
result = sigemptyset(&signalSet);
if (result == 0) {
printf("Signal set successfully initialized as empty.\n");
} else {
printf("Failed to initialize signal set.\n");
}
// 现在可以向此空集中添加信号,或用它来设置信号掩码等操作
return 0;
}
在这个示例中,首先声明一个 sigset_t
类型的变量 signalSet
,然后使用 sigemptyset
函数初始化它,确保该信号集不包含任何信号。这是处理信号时设置信号集的通常做法,尤其是在设置新的信号掩码或在多线程应用中为特定线程配置信号之前。
使用场景
sigemptyset
通常在下列情况中使用:
- 在设置新的信号处理或信号掩码之前,确保信号集中不含任何未预期的信号。
- 清除现有信号集以重新配置程序的信号处理行为。
- 在多线程程序中,为不同线程设置不同的信号掩码。
这个函数是信号处理编程中的基础,允许程序员精确地控制哪些信号应该被阻塞或处理,从而提高程序的可靠性和响应性。
sigfillset
在 C 语言中,sigfillset
函数用于初始化一个信号集,并将所有已定义的信号添加到这个集合中。这个函数是信号处理相关函数的一部分,定义在 <signal.h>
头文件中,并且通常与其他信号操作函数一起使用,以控制信号的阻塞、忽略或处理行为。
函数定义
sigfillset
函数的定义如下:
c
#include <signal.h>
int sigfillset(sigset_t *set);
- 参数 :
sigset_t *set
: 指向sigset_t
类型的指针,该类型用于存储信号集。
返回值
- 成功时,
sigfillset
返回0
。 - 失败时,通常情况下,此函数不应失败,因此如果出现非零返回值,应检查代码其他部分是否有错误。
示例
以下是一个如何使用 sigfillset
函数来初始化并填充信号集的示例:
c
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t signalSet;
int result;
// 初始化信号集并添加所有信号
result = sigfillset(&signalSet);
if (result == 0) {
printf("Signal set successfully initialized and filled with all signals.\n");
} else {
printf("Failed to initialize and fill the signal set.\n");
}
// 可以用此信号集来阻塞所有信号
if (sigprocmask(SIG_SETMASK, &signalSet, NULL) == 0) {
printf("All signals are now blocked.\n");
} else {
perror("sigprocmask");
}
return 0;
}
在这个示例中,首先使用 sigfillset
函数初始化并填充 sigset_t
类型的变量 signalSet
,使其包含所有可用信号。然后使用 sigprocmask
函数设置信号掩码,阻塞所有信号,这在需要确保某代码段不被信号打断时非常有用。
使用场景
sigfillset
通常在下列情况中使用:
- 当需要阻塞进程中的所有信号以执行关键代码段时,可以使用此函数填充信号集,并通过
sigprocmask
设置为当前的信号掩码。 - 在需要清理或重置信号掩码到最初状态时,可以使用此函数以确保所有信号都被考虑到。
这个函数对于需要精细控制信号行为的程序是非常重要的,它允许开发者确保在特定时刻不会有任何外部信号干扰程序的执行。
sigdelset
在 C 语言中,sigdelset
函数用于从一个信号集中删除一个特定的信号。这个函数是 Unix 和类 Unix 系统中信号处理操作的一部分,定义在 <signal.h>
头文件中。使用 sigdelset
可以修改信号集,这对于精确控制哪些信号应该被阻塞或忽略是非常有用的。
函数定义
sigdelset
函数的定义如下:
c
#include <signal.h>
int sigdelset(sigset_t *set, int signum);
- 参数 :
sigset_t *set
: 指向sigset_t
类型的指针,该类型用于存储信号集。int signum
: 指定要从信号集中删除的信号。
返回值
- 成功时,
sigdelset
返回0
。 - 失败时,返回
-1
并设置errno
来指示错误。常见的错误原因是signum
不是有效的信号编号。
示例
以下是一个使用 sigdelset
函数来从信号集中删除一个信号的示例:
c
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t signalSet;
int result;
// 初始化信号集并添加所有信号
sigfillset(&signalSet);
// 从信号集中删除 SIGINT
result = sigdelset(&signalSet, SIGINT);
if (result == 0) {
printf("SIGINT successfully removed from the signal set.\n");
} else {
perror("sigdelset");
}
// 现在可以使用修改后的信号集来阻塞其他所有信号,而不阻塞 SIGINT
if (sigprocmask(SIG_SETMASK, &signalSet, NULL) == 0) {
printf("All signals except SIGINT are now blocked.\n");
} else {
perror("sigprocmask");
}
return 0;
}
在这个示例中,首先使用 sigfillset
函数初始化并填充 sigset_t
类型的变量 signalSet
,使其包含所有可用信号。然后使用 sigdelset
函数从信号集中删除 SIGINT
(通常是 Ctrl+C 生成的中断信号)。之后,通过 sigprocmask
设置信号掩码,阻塞除 SIGINT
之外的所有信号。
使用场景
sigdelset
通常在以下情况中使用:
- 在需要确保某些信号不被阻塞时,比如在执行关键任务时需要响应中断信号(如
SIGINT
)。 - 修改现有的信号集,以便在多线程应用中为特定线程配置信号处理或阻塞策略。
- 在信号处理程序中动态调整信号掩码,以便在处理某些信号时允许或禁止其他信号的递送。
这个函数使得信号处理在程序中更加灵活,有助于提高程序对外部事件的响应能力和可控性。
sigismember
在 C 语言中,sigismember
函数用于检查特定信号是否为给定信号集的成员。这个函数是 Unix 类操作系统中 <signal.h>
头文件提供的信号处理函数集的一部分。它常用于确定特定信号是否已被添加到信号集中,这对于配置应用程序中的信号处理行为非常关键。
函数定义
sigismember
函数的定义如下:
c
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
- 参数 :
const sigset_t *set
:指向之前通过sigemptyset
、sigfillset
初始化或通过sigaddset
、sigdelset
修改过的信号集的指针。int signum
:要检测其成员资格的信号编号。
返回值
- 0:该信号不是信号集的成员。
- 1:该信号是信号集的成员。
- -1 :发生错误,此时
errno
被设置为具体的错误代码(例如,如果signum
是无效的信号编号,可能会设置EINVAL
)。
示例
下面是使用 sigismember
函数的一个示例,该示例演示了如何检查信号 SIGINT 是否在信号集中:
c
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t signalSet;
sigemptyset(&signalSet); // 初始化信号集为空
sigaddset(&signalSet, SIGINT); // 向信号集中添加 SIGINT
// 检查 SIGINT 是否在集合中
if (sigismember(&signalSet, SIGINT)) {
printf("SIGINT is a member of the set.\n");
} else {
printf("SIGINT is not a member of the set.\n");
}
return 0;
}
使用场景
sigismember
函数通常在以下情况中使用:
- 信号掩码管理:在设置或修改进程的信号掩码之前检查某个信号是否已经在信号集中,以决定是否需要添加或删除。
- 信号处理逻辑:在信号处理函数中判断某个信号是否应被当前处理逻辑处理。
- 多线程应用:在多线程程序中,为特定线程设置或检查信号掩码时确定某信号的状态。
通过这种方式,sigismember
功能使程序能够更精确地控制和响应信号,提高了程序对信号的管理能力和灵活性。
c
/*
以下信号集相关的函数都是对自定义的信号集进行操作。
int sigemptyset(sigset_t *set);
- 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);
- 功能:将信号集中的所有的标志位置为1
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigaddset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置不阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signum);
- 功能:判断某个信号是否阻塞
- 参数:
- set:需要操作的信号集
- signum:需要判断的那个信号
- 返回值:
1 : signum被阻塞
0 : signum不阻塞
-1 : 失败
*/
#include <signal.h>
#include <stdio.h>
int main() {
// 创建一个信号集
sigset_t set;
// 清空信号集的内容
sigemptyset(&set);
// 判断 SIGINT 是否在信号集 set 里
int ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 判断SIGINT是否在信号集中
ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out
SIGINT 不阻塞
SIGINT 阻塞
SIGQUIT 阻塞
SIGQUIT 不阻塞
sigprocmask
在 C 语言中,sigprocmask
函数用于检查和更改进程的信号掩码。信号掩码是指定哪些信号将被阻塞(即暂时不递送给进程)的信号集。sigprocmask
是 Unix 和类 Unix 系统中信号处理的一个重要工具,定义在 <signal.h>
头文件中。通过使用这个函数,可以在运行时动态地更改哪些信号被阻塞,从而控制程序的行为。
函数定义
sigprocmask
函数的原型如下:
c
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 参数 :
int how
: 指定如何修改当前信号掩码。可能的值有:SIG_BLOCK
: 将set
指定的信号添加到当前信号掩码中,即阻塞这些信号。SIG_UNBLOCK
: 从当前信号掩码中移除set
指定的信号,即不再阻塞这些信号。SIG_SETMASK
: 将当前信号掩码设置为set
指定的信号集。
const sigset_t *set
: 指向信号集的指针,这个信号集指定了要阻塞或解除阻塞的信号。如果为NULL
,则how
参数没有效果,但如果oldset
不是NULL
,当前信号掩码仍然可以被存储。sigset_t *oldset
: 如果不为NULL
,则当前信号掩码在函数修改前将被存储在oldset
指定的位置。
返回值
- 成功时,返回
0
。 - 失败时,返回
-1
并设置errno
以指示错误原因(例如,EINVAL
表示how
参数无效)。
示例
以下是一个使用 sigprocmask
函数来修改信号掩码的示例:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
sigset_t newSet, oldSet;
// 初始化新信号集,并添加 SIGINT
sigemptyset(&newSet);
sigaddset(&newSet, SIGINT);
// 阻塞 SIGINT 信号
if (sigprocmask(SIG_BLOCK, &newSet, &oldSet) < 0) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is now blocked.\n");
// 模拟一些工作,延时 5 秒
sleep(5);
// 恢复之前的信号掩码
if (sigprocmask(SIG_SETMASK, &oldSet, NULL) < 0) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is no longer blocked.\n");
return 0;
}
使用场景
sigprocmask
函数常用于以下场景:
- 控制信号的阻塞:在执行关键代码段时,可能需要阻塞某些信号,以防止信号处理函数中断这些操作。
- 动态信号管理:根据程序的状态更改哪些信号应当被阻塞或接收。
- 信号掩码的查询 :通过传递
NULL
为set
参数,可以查询当前的信号掩码状态。
这个函数在多线程程序和复杂的信号处理逻辑中尤其有用,它允许程序精确控制在何时应该处理特定的信号。
sigpending
在 Unix 和类 Unix 系统中,sigpending
函数用于检查当前阻塞且未决的信号集,即那些已被发送到进程但由于当前的信号掩码而被阻止的信号。此函数是信号处理功能的一部分,定义在 <signal.h>
头文件中。
函数定义
sigpending
函数的定义如下:
c
#include <signal.h>
int sigpending(sigset_t *set);
- 参数 :
sigset_t *set
: 指向sigset_t
类型的指针,该指针用于输出当前进程的未决信号集。
返回值
- 成功时,返回
0
。 - 失败时,返回
-1
并设置errno
以指示错误原因。
示例
以下是使用 sigpending
函数来检查和输出当前阻塞但未决的信号的示例:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
sigset_t newSet, pendingSet;
// 初始化新信号集并添加 SIGINT 和 SIGTERM
sigemptyset(&newSet);
sigaddset(&newSet, SIGINT);
sigaddset(&newSet, SIGTERM);
// 阻塞 SIGINT 和 SIGTERM 信号
if (sigprocmask(SIG_BLOCK, &newSet, NULL) < 0) {
perror("sigprocmask");
return 1;
}
// 发送 SIGINT 信号给自己
raise(SIGINT);
// 检查未决的信号
if (sigpending(&pendingSet) < 0) {
perror("sigpending");
return 1;
}
// 检查 SIGINT 是否在未决信号集中
if (sigismember(&pendingSet, SIGINT)) {
printf("SIGINT is pending.\n");
}
// 检查 SIGTERM 是否在未决信号集中
if (sigismember(&pendingSet, SIGTERM)) {
printf("SIGTERM is pending.\n");
} else {
printf("SIGTERM is not pending.\n");
}
return 0;
}
在这个示例中,首先将 SIGINT 和 SIGTERM 添加到新的信号集中并阻塞这些信号。然后通过 raise
函数向进程自己发送 SIGINT 信号。由于 SIGINT 被阻塞,它变成未决状态。随后,使用 sigpending
检查哪些信号是未决的,并通过 sigismember
检查 SIGINT 和 SIGTERM 是否在未决信号集中。
使用场景
sigpending
函数通常在需要了解哪些信号正在等待被处理的情况下使用,特别是在:
- 复杂的信号处理逻辑中,需要确定当前哪些信号由于阻塞掩码而未被处理。
- 调试多线程应用程序中的信号处理问题时。
- 在长时间运行的服务中,程序可能需要定期检查是否有重要信号等待处理。
这个函数帮助开发者更好地理解和控制程序的行为,尤其是在涉及信号阻塞和处理时。
c
/*
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
- 参数:
- how : 如何对内核阻塞信号集进行处理
SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
假设内核中默认的阻塞信号集是mask, mask | set
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
mask &= ~set
SIG_SETMASK:覆盖内核中原来的值
- set :已经初始化好的用户自定义的信号集
- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
- 返回值:
成功:0
失败:-1
设置错误号:EFAULT、EINVAL
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集
- 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
*/
// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set);
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int num = 0;
while(1) {
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
// 遍历前32位
for(int i = 1; i <= 31; i++) {
if(sigismember(&pendingset, i) == 1) {
printf("1");
}else if(sigismember(&pendingset, i) == 0) {
printf("0");
}else {
perror("sigismember");
exit(0);
}
}
printf("\n");
sleep(1);
if(num == 10) {
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
^\0110000000000000000000000000000
0110000000000000000000000000000
sigaction
在 Unix 和类 Unix 系统中,sigaction
函数用于检查和更改信号的处理方式。这是信号处理的一个高级工具,提供比 signal
函数更详细的控制,允许程序定义如何处理特定的信号,包括设置信号处理函数、指定信号处理过程中哪些信号应被阻塞等。
函数定义
sigaction
函数定义在 <signal.h>
头文件中,其原型如下:
c
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 参数 :
int signum
: 要操作的信号编号,除了SIGKILL
和SIGSTOP
,因为这两个信号不能被捕获或忽略。const struct sigaction *act
: 指向sigaction
结构的指针,该结构指定了新的信号处理设置。struct sigaction *oldact
: 如果不为NULL
,则存储旧的信号处理设置。
struct sigaction
结构定义如下:
c
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler
: 指向信号处理函数的指针,或者可以是SIG_IGN
(忽略信号)或SIG_DFL
(恢复默认行为)。sa_sigaction
: 一个可选的信号处理函数,用于接收额外的信号信息,只有在sa_flags
中设置了SA_SIGINFO
时才使用。sa_mask
: 在信号处理函数执行期间需要阻塞的附加信号。sa_flags
: 用于修改信号处理的行为,例如SA_RESTART
使被信号中断的系统调用自动重启。sa_restorer
: 这是一个过时的、不再使用的字段,用于在早期的系统实现中恢复信号上下文。
返回值
- 成功时,返回
0
。 - 失败时,返回
-1
并设置errno
以指示错误原因。
示例
以下是一个使用 sigaction
设置信号处理函数的示例:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handle_sigint(int signum) {
printf("Caught signal %d\n", signum);
}
int main() {
struct sigaction sa;
sa.sa_handler = handle_sigint;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("Press Ctrl+C\n");
sleep(10); // Wait for signal
return 0;
}
在这个例子中,我们设置了 SIGINT
信号的处理函数为 handle_sigint
。当用户按下 Ctrl+C(通常发送 SIGINT
)时,程序将捕获并处理该信号。
使用场景
sigaction
是信号处理中非常强大的工具,常用于:
- 定制复杂的信号处理逻辑。
- 在处理信号时阻塞其他信号。
- 处理需要从信号处理程序中获取额外信息的高级场景。
这个函数提供了对信号处理的全面控制,使得应用程序能够更稳定和安全地运行,尤其是在多线程环境和需要精细控制信号处理的应用中。
c
/*
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
- 功能:检查或者改变信号的处理。信号捕捉
- 参数:
- signum : 需要捕捉的信号的编号或者宏值(信号的名称)
- act :捕捉到信号之后的处理动作
- oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
- 返回值:
成功 0
失败 -1
struct sigaction {
// 函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);
// 不常用
void (*sa_sigaction)(int, siginfo_t *, void *);
// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
sigset_t sa_mask;
// 使用哪一个信号处理对捕捉到的信号进行处理
// 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
int sa_flags;
// 被废弃掉了
void (*sa_restorer)(void);
};
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
sigemptyset(&act.sa_mask); // 清空临时阻塞信号集
// 注册信号捕捉
sigaction(SIGALRM, &act, NULL);
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
// getchar();
while(1);
return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out
定时器开始了...
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
内核实现信号捕捉的过程
在 Unix 和类 Unix 系统中,信号是操作系统用来通知进程某些事件已经发生的一种机制。信号可以由操作系统因为外部事件(比如硬件异常、终端用户操作)触发,或者由应用程序(如调用 kill
或 raise
)显式生成。下面是操作系统内核实现信号捕捉的基本过程:
- 信号的生成
信号可以通过多种方式生成:
- 外部事件 :例如,用户按下 Ctrl+C 在终端生成
SIGINT
信号。 - 程序错误 :如除零操作产生
SIGFPE
(浮点异常信号)。 - 显式调用 :程序调用
kill
、raise
等函数直接发送信号。
- 信号的传递
一旦信号生成,内核将其添加到目标进程的信号队列中。每个进程都有一个信号队列和一个或多个信号掩码(决定哪些信号当前被阻塞)。如果信号未被阻塞,它被标记为"待处理"(pending)。
- 信号的捕捉和处理
当进程从内核模式返回到用户模式前,内核检查待处理的信号:
- 信号掩码检查:内核检查进程的信号掩码,看是否有待处理的信号被阻塞。
- 信号处理分配:如果信号未被阻塞,内核将查找与该信号关联的处理动作。这些动作可以是默认动作(比如终止进程)、忽略信号或者是一个指向用户定义函数的指针(信号处理函数)。
- 用户态信号处理函数的调用
如果为信号指定了用户定义的处理函数(通过如 sigaction
设置):
- 上下文保存:在调用信号处理函数前,内核保存当前的处理上下文(如CPU寄存器状态),以便信号处理完成后能恢复执行。
- 设置信号栈 :如果指定了信号处理栈(通过
sigaltstack
),内核会切换到这个栈。 - 调用处理函数:内核修改程序计数器(PC),让其指向信号处理函数的入口,然后返回用户空间执行该函数。
- 信号处理完毕
- 处理函数返回:信号处理函数执行完毕后,从处理函数返回。
- 上下文恢复:内核恢复在信号处理前保存的上下文,进程继续执行信号到达前被中断的代码。
- 信号处理的后续动作
根据信号处理的结果,进程可能继续正常执行,或者因为某些信号的默认行为(如 SIGTERM
)而终止。
这个过程涉及到复杂的内核机制,包括中断处理、上下文切换和用户空间与内核空间之间的交互。操作系统的信号处理机制是理解进程管理和操作系统响应外部事件能力的重要部分。在多任务和多用户环境中,这种机制尤为重要,因为它允许操作系统控制和协调不同进程的行为。
SIGCHLD信号
SIGCHLD信号产生的条件
子进程终止时
子进程接收到 SIGSTOP 信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时
◼ 以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号
c
/*
SIGCHLD信号产生的3个条件:
1.子进程结束
2.子进程暂停了
3.子进程继续运行
都会给父进程发送该信号,父进程默认忽略该信号。
使用SIGCHLD信号解决僵尸进程的问题。
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
void myFun(int num) {
printf("捕捉到的信号 :%d\n", num);
// 回收子进程PCB的资源,这样的话父进程没有办法做事情
// while(1) {
// wait(NULL);
// }
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);//回收所有子进程,且非阻塞
if(ret > 0) {
printf("child die , pid = %d\n", ret);
} else if(ret == 0) {
// 说明还有子进程活着
break;
} else if(ret == -1) {
// 没有子进程
break;
}
}
}
int main() {
// 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
// 创建一些子进程
pid_t pid;
for(int i = 0; i < 20; i++) {
pid = fork();
//避免子进程再生成子进程
if(pid == 0) {
break;
}
}
if(pid > 0) {
// 父进程
// 捕捉子进程死亡时发送的SIGCHLD信号,进行回收
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun; //指定回调函数
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1) {
printf("parent process pid : %d\n", getpid());
sleep(2);
}
} else if( pid == 0) {
// 子进程
printf("child process pid : %d\n", getpid());
}
return 0;
}
解决僵尸进程问题
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ gcc sigchld.c
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out
child process pid : 8662
child process pid : 8663
child process pid : 8666
child process pid : 8665
child process pid : 8664
child process pid : 8667
child process pid : 8669
child process pid : 8668
child process pid : 8670
child process pid : 8673
child process pid : 8671
child process pid : 8674
child process pid : 8672
child process pid : 8675
child process pid : 8676
child process pid : 8677
捕捉到的信号 :17
child process pid : 8678
child process pid : 8681
child process pid : 8679
child process pid : 8680
child die , pid = 8662
child die , pid = 8663
child die , pid = 8664
child die , pid = 8665
child die , pid = 8666
child die , pid = 8667
child die , pid = 8668
child die , pid = 8669
child die , pid = 8670
child die , pid = 8671
child die , pid = 8672
child die , pid = 8673
child die , pid = 8674
child die , pid = 8675
child die , pid = 8676
child die , pid = 8677
child die , pid = 8678
child die , pid = 8679
child die , pid = 8680
child die , pid = 8681
捕捉到的信号 :17
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661