35、Linux信号处理:基础与实践

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信号处理机制,如果你在实际使用中遇到任何问题,欢迎留言交流。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值