Linux应用进程间通信六(信号)
一、概述
1.1、信号的概念
信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。通常信号是由一个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程。一个信号的产生叫生成,接收到一个信号叫捕获。
1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被 取消时才被传递给进程。
1.2、用户进程对信号的响应方式
1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
3)执行缺省操作:Linux对每种信号都规定了默认操作
1.3、信号的来源
在Linux操作系统中,信号可以由多种来源产生,包括但不限于:
- 用户操作:用户可以通过键盘产生信号,如按下Ctrl+C通常发送SIGINT(中断信号)。
- 软件生成:程序可以通过kill系统调用或raise函数发送信号给其他进程或自身。
- 硬件异常:当程序执行非法操作(如除零、内存访问违规)时,硬件会触发信号,如SIGFPE(浮点异常)或SIGSEGV(段错误)。
- 系统条件:系统在特定条件下也会发送信号,如SIGHUP(挂起信号)可能在控制终端关闭时发送。
- 定时器超时:使用alarm、setitimer或timer等定时器函数设置的定时器超时后,会发送如SIGALRM或SIGVTALRM信号。
1.4、信号的种类
Linux中定义了许多种类的信号,每种信号都有其特定的用途和默认行为。以下是一些常见的信号:
- SIGINT:中断信号,通常由用户通过按下Ctrl+C产生,用于中断正在运行的程序。
- SIGTERM:终止信号,用于请求程序自己终止,可以被捕获或忽略。
- SIGKILL:杀死信号,用于立即终止程序,无法被捕获或忽略。
- SIGSTOP:停止信号,用于停止进程的执行,无法被捕获、忽略或由用户生成。
- SIGCHLD:子进程结束信号,当子进程结束时发送给父进程。
- SIGCONT:继续信号,用于唤醒一个被停止的进程。
此外,还有SIGSEGV(段错误信号)、SIGFPE(浮点异常信号)、SIGILL(非法指令信号)等,分别用于处理不同的异常和事件。
二、实现相关函数
2.1、信号的发送函数
-
kill函数
- 原型:
int kill(pid_t pid, int sig);
- 功能:向指定进程发送信号。其中,
pid
指定目标进程的ID,sig
指定要发送的信号。 - 参数:
pid
:目标进程的ID。取值有以下情况:- 大于0:将信号发送给指定进程ID的进程。
- 等于0:将信号发送给当前进程组中的所有进程。
- 小于-1:将信号发送给进程组号为
pid
绝对值的进程组中的所有进程。
sig
:要发送的信号编号,推荐使用信号宏定义。
- 返回值:成功返回0,失败返回-1。
- 原型:
-
raise函数
- 原型:
int raise(int sig);
- 功能:向当前进程发送信号。相当于
kill(getpid(), sig);
。 - 参数:
sig
为要发送的信号编号。 - 返回值:成功返回0,失败返回非0值。
- 原型:
-
abort函数
- 原型:
void abort(void);
- 功能:向当前进程发送异常终止信号SIGABRT,并产生core文件。相当于
kill(getpid(), SIGABRT);
。
- 原型:
-
alarm函数
- 原型:
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器,指定
seconds
秒后,内核给当前进程发送SIGALRM信号。进程收到该信号,默认终止进程。 - 参数:
seconds
为定时器超时时间,单位为秒。 - 返回值:返回上次alarm设置的剩余秒数。若设置为0,则表示取消定时器,并返回旧闹钟剩余秒数。
- 原型:
-
setitimer函数
- 原型:
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
- 功能:设置定时器,可替代alarm函数,精度更高(微秒级),可实现周期定时。
- 参数:
which
:定时方式,有以下选项:- ITIMER_REAL:计算自然时间,发送SIGALRM信号。
- ITIMER_VIRTUAL:只计算用户空间占用的CPU时间,发送SIGVTALRM信号。
- ITIMER_PROF:计算用户空间和内核空间占用的CPU时间之和,发送SIGPROF信号。
new_value
:指向itimerval结构的指针,指定定时器的超时时间和周期。old_value
:指向itimerval结构的指针,用于存储旧的定时器值,通常设为NULL。
- 原型:
2.2、信号的接收与等待函数
-
pause函数
- 原型:
int pause(void);
- 功能:让调用者进入休眠状态,直到进程收到信号并被唤醒。
- 返回值:唤醒后返回-1。
- 原型:
-
sleep函数
- 原型:
unsigned int sleep(unsigned int seconds);
- 功能:让调用者进入休眠状态指定的秒数,期间可被信号唤醒。
- 返回值:剩余的休眠时间。
- 原型:
2.3、信号的处理函数
-
signal函数
- 原型:
sighandler_t signal(int signum, sighandler_t handler);
- 功能:向内核注册一个信号处理函数。
- 参数:
signum
:要处理的信号编号。handler
:自定义的信号处理函数的指针。
- 返回值:设置成功返回以前的信号处理程序的地址,设置失败返回SIG_ERR。
- 原型:
-
sigaction函数
- 原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 功能:设置信号处理函数,比signal函数更灵活,可用于实时信号。
- 参数:
signum
:要处理的信号编号。act
:指向sigaction结构的指针,指定新的信号处理函数和其他选项。oldact
:指向sigaction结构的指针,用于存储旧的信号处理函数信息,通常设为NULL。
- 原型:
2.4、信号的屏蔽与解除屏蔽函数
-
sigprocmask函数
- 原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 功能:设置要屏蔽的信号。
- 参数:
how
:指定屏蔽信号的方式,有以下选项:- SIG_BLOCK:添加
set
中的信号到当前屏蔽集。 - SIG_UNBLOCK:从当前屏蔽集中删除
set
中的信号。 - SIG_SETMASK:用
set
替换当前的屏蔽集。
- SIG_BLOCK:添加
set
:指向sigset_t结构的指针,指定要屏蔽或解除屏蔽的信号集。oldset
:指向sigset_t结构的指针,用于存储旧的屏蔽集信息,通常设为NULL。
- 原型:
三、实例应用
3.1、kill 函数发送杀死pid指向的进程的信号,raise 发送杀死自己的信号
来看如何使用kill
函数:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
exit(EXIT_FAILURE);
}
pid_t pid = atoi(argv[1]);
if (kill(pid, SIGKILL) == -1) {
perror("kill");
exit(EXIT_FAILURE);
}
printf("Sent SIGKILL to process %d\n", pid);
return 0;
}
在这个例子中,程序接受一个命令行参数,即要终止的进程的PID。然后,它使用kill
函数向该进程发送SIGKILL
信号。如果kill
调用失败,程序将打印错误信息并退出。
接下来,看如何使用raise
函数:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main() {
printf("Sending SIGKILL to myself...\n");
if (raise(SIGKILL) == -1) {
perror("raise");
// Note: If raise fails, it's highly unlikely we'll get here,
// because SIGKILL is uncatchable and usually terminates the process immediately.
// But for the sake of completeness, we include this check.
exit(EXIT_FAILURE);
}
// This line will never be reached because the process will be terminated by SIGKILL.
printf("This will not be printed.\n");
return 0; // This return statement will never be executed.
}
在这个例子中,程序直接调用raise
函数向自己发送SIGKILL
信号。由于SIGKILL
信号是无法被捕获或忽略的,因此程序将立即终止。因此,raise
调用之后的任何代码都不会被执行。
注意:在实际使用中,发送SIGKILL
信号应该谨慎进行,因为它会强制终止进程,不给进程任何清理资源或保存状态的机会。通常,应该首先尝试发送SIGTERM
信号,让进程有机会进行清理工作,然后再考虑使用SIGKILL
。
另外,编译和运行这些程序需要适当的权限。特别是,终止其他用户的进程通常需要超级用户权限。在编译这些程序时,可以使用gcc
编译器,例如:
gcc -o kill_example kill_example.c | |
gcc -o raise_example raise_example.c |
然后,以适当的权限运行它们:
./kill_example <pid> # 替换<pid>为目标进程的PID | |
./raise_example # 注意,这将立即终止当前运行的程序 |
3.2、定时器的使用
在Linux应用进程间通信中,信号定时器(timer)是一种常用的机制,用于在指定的时间间隔后或在指定的时间点向进程发送信号。这通常通过alarm
函数或更灵活的setitimer
函数来实现。下面是一个使用setitimer
函数设置定时器的简单示例。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
// 信号处理函数
void timer_handler(int signum) {
static int count = 0;
printf("timer expired %d times\n", ++count);
}
int main() {
// 设置信号处理函数
struct sigaction sa;
sa.sa_handler = &timer_handler; // 指定处理函数
sa.sa_flags = 0; // 默认标志
sigemptyset(&sa.sa_mask); // 清空信号集
if (sigaction(SIGVTALRM, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
// 设置定时器
struct itimerval timer;
timer.it_value.tv_sec = 2; // 初始延迟2秒
timer.it_value.tv_usec = 0; // 微秒部分
timer.it_interval.tv_sec = 1; // 每隔1秒触发一次
timer.it_interval.tv_usec = 0; // 微秒部分
if (setitimer(ITIMER_VIRTUAL, &timer, NULL) == -1) {
perror("setitimer");
exit(EXIT_FAILURE);
}
// 主循环,等待信号
while (1) {
pause(); // 等待信号
}
// 注意:由于使用了pause(),这个return语句实际上永远不会被执行。
// 进程将在收到SIGVTALRM信号时被唤醒,并调用timer_handler函数。
return 0;
}
在这个例子中,设置了一个虚拟定时器(ITIMER_VIRTUAL
),它将在初始延迟2秒后首次触发,并且之后每隔1秒触发一次。定时器触发时,将向进程发送SIGVTALRM
信号,该信号由我们自定义的timer_handler
函数处理。
timer_handler
函数简单地打印出一个计数器,每次定时器触发时递增。
main
函数中的while (1)
循环和pause()
调用确保进程在等待信号时不会退出。pause()
函数会使进程进入休眠状态,直到它接收到一个信号。一旦收到信号,pause()
将返回,进程将继续执行(在这个例子中,实际上会再次进入pause()
调用,等待下一个信号)。
请注意,由于使用了pause()
和无限循环,这个程序将一直运行,直到被外部方式终止(例如,通过发送SIGKILL
信号)。
编译和运行这个程序:
gcc -o timer_example timer_example.c | |
./timer_example |
应该会看到每隔一秒打印一次的计数器输出,表明定时器正在按预期工作。