Linux信号处理:基础与实践
1. 常见信号介绍
在Linux系统中,信号是一种用于进程间通信和系统通知的机制。以下是几种常见信号的详细说明:
-
SIGVTALRM
:当使用
ITIMER_VIRTUAL
标志创建的定时器到期时,
setitimer()
函数会发送此信号。
-
SIGWINCH
:当终端窗口大小发生变化时,内核会为前台进程组中的所有进程发出此信号。默认情况下,进程会忽略该信号,但如果进程关注终端窗口大小,它可以选择捕获并处理此信号。例如,
top
命令就是一个捕获该信号的程序,你可以在其运行时调整窗口大小,观察它的响应。
-
SIGXCPU
:当进程超过其软处理器时间限制时,内核会发出此信号。内核会每秒继续发出此信号,直到进程退出或超过其硬处理器时间限制。一旦超过硬限制,内核会向进程发送
SIGKILL
信号。
-
SIGXFSZ
:当进程超过其文件大小限制时,内核会发出此信号。默认操作是终止进程,但如果捕获或忽略此信号,会导致文件大小超过限制的系统调用将返回 -1,并将
errno
设置为
EFBIG
。
2. 基本信号管理
信号管理是Linux编程中的重要部分,其中最简单和最古老的接口是
signal()
函数。以下是关于
signal()
函数的详细介绍:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal (int signo, sighandler_t handler);
-
功能
:成功调用
signal()函数会移除当前接收到信号signo时采取的动作,并使用handler指定的信号处理程序来处理该信号。 - 参数说明 :
-
signo:是前面讨论过的信号名称之一,例如SIGINT或SIGUSR1。需要注意的是,进程不能捕获SIGKILL和SIGSTOP信号,因此为这两个信号设置处理程序是没有意义的。 -
handler:是一个函数指针,指向信号处理函数。该函数必须返回void,并且接受一个整数参数,该参数是正在处理的信号的标识符。 - 特殊值 :
-
SIG_DFL:将信号signo的行为设置为默认行为。例如,对于SIGPIPE,进程将终止。 -
SIG_IGN:忽略信号signo。 -
返回值
:
signal()函数返回信号的先前行为,可能是信号处理程序的指针、SIG_DFL或SIG_IGN。出错时,函数返回SIG_ERR,并且不会设置errno。
3. 等待信号
pause()
系统调用是一个非常有用的函数,它可以让进程进入睡眠状态,直到接收到一个被处理或终止进程的信号。
#include <unistd.h>
int pause (void);
-
功能
:
pause()函数只有在接收到信号时才会返回,此时信号会被处理,pause()返回 -1,并将errno设置为EINTR。如果内核发出一个被忽略的信号,进程不会唤醒。 -
原理
:在Linux内核中,
pause()是最简单的系统调用之一。它只执行两个操作:首先将进程置于可中断睡眠状态,然后调用schedule()来调用Linux进程调度器,以找到另一个要运行的进程。由于进程实际上没有等待任何东西,因此除非接收到信号,否则内核不会唤醒它。
4. 信号处理示例
以下是两个简单的信号处理示例:
-
示例一:处理
SIGINT
信号
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
/* handler for SIGINT */
static void sigint_handler (int signo)
{
/*
* Technically, you shouldn't use printf() in a
* signal handler, but it isn't the end of the
* world. I'll discuss why in the section
* "Reentrancy."
*/
printf ("Caught SIGINT!\n");
exit (EXIT_SUCCESS);
}
int main (void)
{
/*
* Register sigint_handler as our signal handler
* for SIGINT.
*/
if (signal (SIGINT, sigint_handler) == SIG_ERR) {
fprintf (stderr, "Cannot handle SIGINT!\n");
exit (EXIT_FAILURE);
}
for (;;)
pause ();
return 0;
}
-
示例二:处理
SIGTERM和SIGINT信号
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
/* handler for SIGINT and SIGTERM */
static void signal_handler (int signo)
{
if (signo == SIGINT)
printf ("Caught SIGINT!\n");
else if (signo == SIGTERM)
printf ("Caught SIGTERM!\n");
else {
/* this should never happen */
fprintf (stderr, "Unexpected signal!\n");
exit (EXIT_FAILURE);
}
exit (EXIT_SUCCESS);
}
int main (void)
{
/*
* Register signal_handler as our signal handler
* for SIGINT.
*/
if (signal (SIGINT, signal_handler) == SIG_ERR) {
fprintf (stderr, "Cannot handle SIGINT!\n");
exit (EXIT_FAILURE);
}
/*
* Register signal_handler as our signal handler
* for SIGTERM.
*/
if (signal (SIGTERM, signal_handler) == SIG_ERR) {
fprintf (stderr, "Cannot handle SIGTERM!\n");
exit (EXIT_FAILURE);
}
/* Reset SIGPROF's behavior to the default. */
if (signal (SIGPROF, SIG_DFL) == SIG_ERR) {
fprintf (stderr, "Cannot reset SIGPROF!\n");
exit (EXIT_FAILURE);
}
/* Ignore SIGHUP. */
if (signal (SIGHUP, SIG_IGN) == SIG_ERR) {
fprintf (stderr, "Cannot ignore SIGHUP!\n");
exit (EXIT_FAILURE);
}
for (;;)
pause ();
return 0;
}
5. 信号的执行与继承
在进程创建和执行过程中,信号的行为会发生继承和变化,具体规则如下表所示:
| 信号行为 | 跨
fork
| 跨
exec
|
| ---- | ---- | ---- |
| 忽略 | 继承 | 继承 |
| 默认 | 继承 | 继承 |
| 处理 | 继承 | 不继承 |
| 待处理信号 | 不继承 | 继承 |
当shell在后台执行进程时,新执行的进程应该忽略中断和退出字符。因此,在shell执行后台进程之前,应该将
SIGINT
和
SIGQUIT
设置为
SIG_IGN
。以下是一个示例代码:
/* handle SIGINT, but only if it isn't ignored */
if (signal (SIGINT, SIG_IGN) != SIG_IGN) {
if (signal (SIGINT, sigint_handler) == SIG_ERR)
fprintf (stderr, "Failed to handle SIGINT!\n");
}
/* handle SIGQUIT, but only if it isn't ignored */
if (signal (SIGQUIT, SIG_IGN) != SIG_IGN) {
if (signal (SIGQUIT, sigquit_handler) == SIG_ERR)
fprintf (stderr, "Failed to handle SIGQUIT!\n");
}
6. 信号编号与字符串的映射
在编程中,有时需要将信号编号转换为字符串表示。以下是几种实现方法:
-
使用
sys_siglist
:
sys_siglist
是一个字符串数组,包含系统支持的信号名称,通过信号编号进行索引。
extern const char * const sys_siglist[];
-
使用
psignal():psignal()函数会将你提供的msg参数字符串打印到stderr,后面跟着一个冒号、一个空格和信号signo的名称。
#include <signal.h>
void psignal (int signo, const char *msg);
-
使用
strsignal():strsignal()函数返回一个指向信号signo描述的指针。如果signo无效,返回的描述通常会说明这一点。
#define _GNU_SOURCE
#include <string.h>
char * strsignal (int signo);
使用
sys_siglist
是一个不错的选择,以下是重写的信号处理函数示例:
static void signal_handler (int signo)
{
printf ("Caught %s\n", sys_siglist[signo]);
}
7. 发送信号
在Linux系统中,可以使用
kill()
系统调用从一个进程向另一个进程发送信号。
#include <sys/types.h>
#include <signal.h>
int kill (pid_t pid, int signo);
- 参数说明 :
-
pid:指定接收信号的进程或进程组。-
如果
pid大于0,kill()将信号signo发送给pid标识的进程。 -
如果
pid等于0,signo将发送给调用进程的进程组中的每个进程。 -
如果
pid等于 -1,signo将发送给调用进程有权限发送信号的每个进程,但不包括自身和init进程。 -
如果
pid小于 -1,signo将发送给进程组-pid。
-
如果
-
signo:指定要发送的信号。 -
返回值
:成功时,
kill()返回0。只要发送了一个信号,该调用就被视为成功。失败时(没有发送信号),调用返回 -1,并将errno设置为以下值之一: -
EINVAL:signo指定的信号无效。 -
EPERM:调用进程缺乏向任何请求的进程发送信号的足够权限。 -
ESRCH:pid表示的进程或进程组不存在,或者在进程的情况下是僵尸进程。
8. 发送信号的权限
为了向另一个进程发送信号,发送进程需要适当的权限。具有
CAP_KILL
能力的进程(通常是由
root
拥有的进程)可以向任何进程发送信号。没有此能力时,发送进程的有效或真实用户ID必须等于接收进程的真实或保存用户ID。简单来说,用户只能向自己拥有的进程发送信号。不过,Unix系统(包括Linux)为
SIGCONT
定义了一个例外:一个进程可以向同一会话中的任何其他进程发送此信号,用户ID不需要匹配。
9. 发送信号的示例
以下是发送信号的示例代码:
-
发送
SIGHUP
信号
int ret;
ret = kill (1722, SIGHUP);
if (ret)
perror ("kill");
这个代码片段实际上等同于以下
kill
命令:
$ kill -HUP 1722
- 检查发送信号的权限
int ret;
ret = kill (1722, 0);
if (ret)
; /* we lack permission */
else
; /* we have permission */
10. 向自己发送信号
raise()
函数是一个进程向自己发送信号的简单方法:
#include <signal.h>
int raise (int signo);
raise(signo)
调用等同于
kill(getpid(), signo)
调用。调用成功时返回0,失败时返回非零值,并且不会设置
errno
。
11. 向整个进程组发送信号
killpg()
函数可以方便地向给定进程组中的所有进程发送信号:
#include <signal.h>
int killpg (int pgrp, int signo);
killpg(pgrp, signo)
调用等同于
kill(-pgrp, signo)
调用。即使
pgrp
为0,
killpg()
也会将信号
signo
发送给调用进程组中的每个进程。成功时,
killpg()
返回0;失败时,返回 -1,并将
errno
设置为以下值之一:
-
EINVAL
:
signo
指定的信号无效。
-
EPERM
:调用进程缺乏向任何请求的进程发送信号的足够权限。
-
ESRCH
:
pgrp
表示的进程组不存在。
12. 信号处理的可重入性
当内核发出信号时,进程可能正在执行任何代码。信号处理程序无法知道信号到达时进程正在执行什么代码,因此信号处理程序在执行操作和处理数据时必须非常小心。特别是在修改全局(即共享)数据时,必须格外谨慎。
可重入函数是指可以安全地从自身内部(或从同一进程的另一个线程)调用的函数。为了符合可重入性,函数不能操作静态数据,只能操作栈分配的数据或调用者提供的数据,并且不能调用任何不可重入的函数。
13. 保证可重入的函数
在编写信号处理程序时,必须假设被中断的进程可能正在执行不可重入的函数。因此,信号处理程序必须只使用可重入的函数。POSIX.1 - 2003和Single UNIX Specification规定了一组在所有兼容平台上保证可重入和信号安全的函数,如下表所示:
| 函数名 | 函数名 | 函数名 |
| ---- | ---- | ---- |
|
abort()
|
accept()
|
access()
|
|
aio_error()
|
aio_return()
|
aio_suspend()
|
|
alarm()
|
bind()
|
cfgetispeed()
|
|
cfgetospeed()
|
cfsetispeed()
|
cfsetospeed()
|
|
chdir()
|
chmod()
|
chown()
|
|
clock_gettime()
|
close()
|
connect()
|
|
creat()
|
dup()
|
dup2()
|
|
execle()
|
execve()
|
_Exit()
|
|
_exit()
|
fchmod()
|
fchown()
|
|
fcntl()
|
fdatasync()
|
fork()
|
|
fpathconf()
|
fstat()
|
fsync()
|
|
ftruncate()
|
getegid()
|
geteuid()
|
|
getgid()
|
getgroups()
|
getpeername()
|
|
getpgrp()
|
getpid()
|
getppid()
|
|
getsockname()
|
getsockopt()
|
getuid()
|
|
kill()
|
link()
|
listen()
|
|
lseek()
|
lstat()
|
mkdir()
|
|
mkfifo()
|
open()
|
pathconf()
|
|
pause()
|
pipe()
|
poll()
|
|
posix_trace_event()
|
pselect()
|
raise()
|
|
read()
|
readlink()
|
recv()
|
|
recvfrom()
|
recvmsg()
|
rename()
|
|
rmdir()
|
select()
|
sem_post()
|
|
send()
|
sendmsg()
|
sendto()
|
|
setgid()
|
setpgid()
|
setsid()
|
|
setsockopt()
|
setuid()
|
shutdown()
|
|
sigaction()
|
sigaddset()
|
sigdelset()
|
|
sigemptyset()
|
sigfillset()
|
sigismember()
|
|
signal()
|
sigpause()
|
sigpending()
|
|
sigprocmask()
|
sigqueue()
|
sigset()
|
|
sigsuspend()
|
sleep()
|
socket()
|
|
socketpair()
|
stat()
|
symlink()
|
|
sysconf()
|
tcdrain()
|
tcflow()
|
|
tcflush()
|
tcgetattr()
|
tcgetpgrp()
|
|
tcsendbreak()
|
tcsetattr()
|
tcsetpgrp()
|
|
time()
|
timer_getoverrun()
|
timer_gettime()
|
|
timer_settime()
|
times()
|
umask()
|
|
uname()
|
unlink()
|
utime()
|
|
wait()
|
waitpid()
|
write()
|
虽然还有更多的函数是安全的,但Linux和其他符合POSIX标准的系统只保证这些函数的可重入性。
14. 信号集操作
在信号处理中,有时需要操作信号集,例如进程阻塞的信号集或待处理的信号集。以下是一些信号集操作函数:
#include <signal.h>
int sigemptyset (sigset_t *set);
int sigfillset (sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset (sigset_t *set, int signo);
int sigismember (const sigset_t *set, int signo);
-
sigemptyset():初始化set指定的信号集,将其标记为空(所有信号都被排除在集合之外)。 -
sigfillset():初始化set指定的信号集,将其标记为包含所有信号。 -
sigaddset():将信号signo添加到set指定的信号集中。 -
sigdelset():从set指定的信号集中删除信号signo。 -
sigismember():检查信号signo是否是set指定的信号集的成员。
通过这些信号集操作函数,可以方便地管理进程的信号集,从而更好地控制信号的处理。
Linux信号处理:基础与实践
15. 信号集操作示例
以下是一个简单的示例,展示如何使用信号集操作函数:
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t set;
// 初始化信号集为空
if (sigemptyset(&set) == -1) {
perror("sigemptyset");
return 1;
}
// 添加SIGINT信号到信号集
if (sigaddset(&set, SIGINT) == -1) {
perror("sigaddset");
return 1;
}
// 检查SIGINT是否在信号集中
if (sigismember(&set, SIGINT)) {
printf("SIGINT is a member of the set.\n");
} else {
printf("SIGINT is not a member of the set.\n");
}
// 从信号集中删除SIGINT信号
if (sigdelset(&set, SIGINT) == -1) {
perror("sigdelset");
return 1;
}
// 再次检查SIGINT是否在信号集中
if (sigismember(&set, SIGINT)) {
printf("SIGINT is a member of the set.\n");
} else {
printf("SIGINT is not a member of the set.\n");
}
return 0;
}
在这个示例中,我们首先使用
sigemptyset
初始化一个空的信号集,然后使用
sigaddset
将
SIGINT
信号添加到信号集中,接着使用
sigismember
检查
SIGINT
是否在信号集中,之后使用
sigdelset
从信号集中删除
SIGINT
信号,最后再次使用
sigismember
进行检查。
16. 信号阻塞与解除阻塞
在某些情况下,我们可能需要暂时阻塞某些信号的传递,以避免信号处理程序干扰正在进行的关键操作。可以使用
sigprocmask
函数来实现信号的阻塞和解除阻塞。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 参数说明 :
-
how:指定如何修改当前的信号掩码,有以下三个可选值:-
SIG_BLOCK:将set中的信号添加到当前信号掩码中,即阻塞这些信号。 -
SIG_UNBLOCK:从当前信号掩码中移除set中的信号,即解除这些信号的阻塞。 -
SIG_SETMASK:将当前信号掩码设置为set中的信号。
-
-
set:指向一个信号集,指定要操作的信号。 -
oldset:如果不为NULL,则会将原来的信号掩码保存到oldset中。 -
返回值
:成功时返回0,失败时返回 -1,并设置
errno。
以下是一个信号阻塞和解除阻塞的示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signo) {
printf("Caught SIGINT!\n");
}
int main() {
sigset_t set, oldset;
// 初始化信号集,添加SIGINT信号
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 注册SIGINT信号处理函数
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("signal");
return 1;
}
// 阻塞SIGINT信号
if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is blocked. Press Ctrl+C to test...\n");
sleep(5);
// 解除SIGINT信号的阻塞
if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is unblocked. Press Ctrl+C to test...\n");
pause();
return 0;
}
在这个示例中,我们首先初始化一个信号集并添加
SIGINT
信号,然后注册
SIGINT
信号的处理函数。接着使用
sigprocmask
函数阻塞
SIGINT
信号,在阻塞期间按下
Ctrl+C
不会触发信号处理函数。之后使用
sigprocmask
函数解除
SIGINT
信号的阻塞,此时按下
Ctrl+C
会触发信号处理函数。
17. 待处理信号检查
可以使用
sigpending
函数来检查当前进程中哪些信号被阻塞且待处理。
#include <signal.h>
int sigpending(sigset_t *set);
- 参数说明 :
-
set:指向一个信号集,用于存储当前被阻塞且待处理的信号。 -
返回值
:成功时返回0,失败时返回 -1,并设置
errno。
以下是一个检查待处理信号的示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signo) {
printf("Caught SIGINT!\n");
}
int main() {
sigset_t set, pending_set;
// 初始化信号集,添加SIGINT信号
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 注册SIGINT信号处理函数
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("signal");
return 1;
}
// 阻塞SIGINT信号
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is blocked. Press Ctrl+C to send a signal...\n");
sleep(5);
// 检查待处理信号
if (sigpending(&pending_set) == -1) {
perror("sigpending");
return 1;
}
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT is pending.\n");
} else {
printf("SIGINT is not pending.\n");
}
// 解除SIGINT信号的阻塞
if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
return 0;
}
在这个示例中,我们首先阻塞
SIGINT
信号,然后提示用户按下
Ctrl+C
发送信号。接着使用
sigpending
函数检查
SIGINT
信号是否被阻塞且待处理,最后解除
SIGINT
信号的阻塞。
18. 信号处理的流程图
graph TD;
A[进程正常执行] --> B{收到信号};
B -- 信号被忽略 --> A;
B -- 信号未被阻塞且有处理函数 --> C[调用信号处理函数];
C --> A;
B -- 信号被阻塞 --> D[信号变为待处理状态];
D -- 信号解除阻塞 --> C;
这个流程图展示了信号处理的基本流程。当进程正常执行时,如果收到信号,会根据信号的处理方式进行不同的操作。如果信号被忽略,则进程继续正常执行;如果信号未被阻塞且有处理函数,则调用信号处理函数;如果信号被阻塞,则信号变为待处理状态,直到信号解除阻塞后再调用处理函数。
19. 信号处理的注意事项
在进行信号处理时,需要注意以下几点:
-
可重入性
:信号处理程序必须只使用可重入的函数,避免操作静态数据,以防止出现数据不一致的问题。
-
异步信号安全
:信号处理程序可能会在任何时候被调用,因此需要确保处理程序不会干扰正在进行的关键操作。
-
信号阻塞与解除阻塞
:合理使用信号阻塞和解除阻塞机制,以避免信号处理程序干扰关键操作,同时确保待处理的信号能够得到及时处理。
-
错误处理
:在使用信号相关的系统调用时,需要进行错误检查,以确保程序的健壮性。
20. 总结
信号处理是Linux系统中非常重要的一部分,它为进程间通信和系统通知提供了一种有效的机制。通过本文的介绍,我们了解了常见信号的含义、基本信号管理函数(如
signal
、
pause
等)、信号的执行与继承规则、信号编号与字符串的映射方法、发送信号的函数(如
kill
、
raise
、
killpg
等)、信号处理的可重入性以及信号集操作等内容。同时,我们还学习了信号阻塞与解除阻塞、待处理信号检查等高级用法。在实际编程中,需要根据具体需求合理使用这些信号处理机制,并注意信号处理的注意事项,以确保程序的正确性和健壮性。
希望本文能够帮助你更好地理解和应用Linux信号处理机制,如果你在实际使用中遇到任何问题,欢迎留言交流。
超级会员免费看
1153

被折叠的 条评论
为什么被折叠?



