rt_sigaction系统调用及示例

我们来学习一下 Linux 中与实时信号(Real-time signals)相关的系统调用,通常以 rt_sig* 命名。这些系统调用提供了对信号处理更强大和精确的控制,特别是处理实时信号(信号编号从 SIGRTMIN 到 SIGRTMAX)。

1. 函数介绍

rt_sig* 系列系统调用是 Linux 内核提供的一组用于处理信号的底层接口。它们是标准信号处理函数(如 signalsigactionkillsigprocmask 等)在内核层面的实现,并且扩展了对实时信号的支持。

与传统的非实时信号相比,实时信号具有以下特点:

  • 排队(Queuing): 多个相同的实时信号可以排队,确保每个信号都被传递。而非实时信号可能会丢失(合并)。
  • 伴随数据(伴随数据(伴随数据(伴随数据(Associated Data)))): 发送实时信号时,可以附带一个整数值(sigval)或一个联合体(union),接收方可以获取这个数据。
  • 优先级(Priority): 实时信号的编号范围是 SIGRTMIN 到 SIGRTMAX,编号越小优先级越高。

rt_sig* 系列调用包括:

  • rt_sigaction: 类似于 sigaction,用于检查或修改信号的处理方式(处理函数、掩码、标志)。
  • rt_sigprocmask: 类似于 sigprocmask,用于检查或修改当前进程的信号屏蔽字(阻塞哪些信号)。
  • rt_sigpending: 类似于 sigpending,用于检查当前有哪些被阻塞且待处理的信号。
  • rt_sigtimedwait: 类似于 sigsuspend,但允许指定超时时间,并可以获取信号附带的数据。
  • rt_sigqueueinfo: 用于向进程发送信号并附带 siginfo_t 结构的数据(比 sigqueue 更底层)。
  • rt_sigsuspend: 类似于 sigsuspend,临时替换信号掩码并挂起进程等待信号。

这些系统调用通常由标准 C 库(glibc)封装成更易用的函数(如 sigactionsigqueue 等)供应用程序调用。直接使用这些底层系统调用比较复杂,主要用于库实现或特殊需求。

2. 函数原型

这些是内核系统调用的原型,用户空间程序通常不直接调用它们,而是通过 glibc 提供的封装函数。

// 注意:这些是内核系统调用的签名,通常在用户空间不可见或需要通过 syscall() 调用
// 并且参数类型和含义可能与 glibc 封装函数略有不同。

long sys_rt_sigaction(int sig, const struct sigaction *act,
                      struct sigaction *oact, size_t sigsetsize);

long sys_rt_sigprocmask(int how, sigset_t *nset,
                        sigset_t *oset, size_t sigsetsize);

long sys_rt_sigpending(sigset_t *uset, size_t sigsetsize);

long sys_rt_sigtimedwait(const sigset_t *uthese, siginfo_t *uinfo,
                         const struct timespec *uts, size_t sigsetsize);

long sys_rt_sigqueueinfo(pid_t pid, int sig, siginfo_t *uinfo);

long sys_rt_sigsuspend(sigset_t *unewset, size_t sigsetsize);
  • sigsetsize: 这是 sigset_t 类型的大小(以字节为单位),内核用它来确保用户空间和内核空间对信号集大小的理解一致。

3. 功能

  • rt_sigaction: 检查或修改特定信号的处理动作(disposition),包括处理函数、信号屏蔽字、标志(如 SA_RESTARTSA_SIGINFO 等)。
  • rt_sigprocmask: 检查或修改当前进程的信号屏蔽字(signal mask),控制哪些信号被阻塞(暂时不递送)。
  • rt_sigpending: 获取当前进程中所有被阻塞且已产生但尚未递送的信号集合。
  • rt_sigtimedwait: 原子地临时解除对指定信号集的阻塞,并挂起进程等待其中一个信号到来,或者等待超时。可以获取信号的详细信息(siginfo_t)。
  • rt_sigqueueinfo: 向指定进程发送一个信号,并允许发送者指定 siginfo_t 结构中的详细信息(比 kill 或 sigqueue 更灵活但也更危险,因为它可以伪造信号来源)。
  • rt_sigsuspend: 用指定的信号集替换当前进程的信号屏蔽字,并挂起进程直到捕获一个信号。返回时恢复原来的信号屏蔽字。

4. 参数

参数因具体调用而异,但通常涉及:

  • sigint 类型。信号编号。
  • act / nset / uthese / unewset: 指向 sigaction 结构体、sigset_t 信号集、siginfo_t 结构体或 timespec 结构体的指针,包含要设置或使用的数据。
  • oact / oset / uinfo / uts: 指向用于存放旧值或获取信息的缓冲区的指针。
  • pidpid_t 类型。目标进程的进程 ID。
  • sigsetsizesize_t 类型。sigset_t 的大小(字节),用于内核验证。
  • howint 类型。指定 rt_sigprocmask 的操作类型(SIG_BLOCKSIG_UNBLOCKSIG_SETMASK)。

5. 返回值

  • 成功:
    • rt_sigactionrt_sigprocmaskrt_sigpendingrt_sigsuspend: 通常返回 0。
    • rt_sigtimedwait: 返回被捕获的信号编号。
  • 失败: 返回 -1,并设置 errno

6. 相似函数或关联函数

  • 用户空间封装函数:
    • sigaction (封装 rt_sigaction)
    • sigprocmask (封装 rt_sigprocmask)
    • sigpending (封装 rt_sigpending)
    • sigtimedwait (封装 rt_sigtimedwait)
    • sigwaitinfo (封装 rt_sigtimedwait)
    • sigqueue (封装 rt_sigqueueinfo 或类似机制)
    • sigsuspend (封装 rt_sigsuspend)
  • 信号相关类型:
    • sigset_t: 信号集类型。
    • struct sigaction: 定义信号处理动作。
    • siginfo_t: 包含信号产生的详细信息。
    • struct timespec: 用于指定时间。
  • 实时信号常量:
    • SIGRTMINSIGRTMAX: 定义实时信号的编号范围。
  • 相关头文件:
    • <signal.h>: 包含信号处理函数和常量。
    • <sys/ucontext.h>: 有时与信号上下文相关。

7. 示例代码

下面的示例演示如何使用用户空间的封装函数(它们内部调用 rt_sig* 系统调用)来处理实时信号,并传递附加数据。

#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

// 实时信号处理函数,使用 SA_SIGINFO 标志
// sa_sigaction 函数指针接收额外的 siginfo_t 和 void* 参数
void rt_signal_handler(int sig, siginfo_t *info, void *context) {
    printf("Received real-time signal %d\n", sig);

    // 检查信号来源和附加数据
    if (info->si_code == SI_QUEUE) {
        // 信号是通过 sigqueue 发送的
        printf("  Signal sent by sigqueue()\n");
        printf("  Sender PID: %d\n", info->si_pid);
        // 打印附加的整数值
        printf("  Attached integer value: %d\n", info->si_value.sival_int);
        // 或者如果是通过指针传递的 (较少见)
        // printf("  Attached pointer value: %p\n", info->si_value.sival_ptr);
    } else if (info->si_code == SI_USER) {
        // 信号是通过 kill() 发送的
        printf("  Signal sent by kill()\n");
        printf("  Sender PID: %d\n", info->si_pid);
    } else {
         printf("  Signal sent by other means (code: %d)\n", info->si_code);
    }
}

int main() {
    pid_t pid;
    struct sigaction sa;
    sigset_t block_mask;
    union sigval value_to_send;
    int rt_sig_num;

    // 1. 选择一个实时信号 (使用编号最小的,优先级最高)
    rt_sig_num = SIGRTMIN;
    if (rt_sig_num > SIGRTMAX) {
        fprintf(stderr, "No real-time signals available.\n");
        exit(EXIT_FAILURE);
    }
    printf("Using real-time signal number: %d\n", rt_sig_num);

    // 2. 设置实时信号处理函数
    memset(&sa, 0, sizeof(sa));
    // 使用 sa_sigaction 而不是 sa_handler 来获取 siginfo_t
    sa.sa_sigaction = rt_signal_handler;
    sigemptyset(&sa.sa_mask); // 不在处理函数执行时阻塞其他信号
    sa.sa_flags = SA_SIGINFO; // 必须设置此标志才能获取 siginfo_t

    if (sigaction(rt_sig_num, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }
    printf("Set up handler for signal %d\n", rt_sig_num);

    // 3. 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // --- 子进程 ---
        printf("Child process (PID: %d) running...\n", getpid());

        // 4. 子进程阻塞一段时间,等待父进程发送信号
        printf("Child sleeping for 2 seconds...\n");
        sleep(2);

        // 5. 子进程向父进程发送带数据的实时信号
        value_to_send.sival_int = 42; // 要发送的整数值
        printf("Child sending real-time signal %d with value %d to parent (PID: %d)...\n",
               rt_sig_num, value_to_send.sival_int, getppid());

        if (sigqueue(getppid(), rt_sig_num, value_to_send) == -1) {
            perror("sigqueue (child)");
            exit(EXIT_FAILURE);
        }
        printf("Child finished sending signal.\n");
        exit(EXIT_SUCCESS);

    } else {
        // --- 父进程 ---
        int status;

        printf("Parent process (PID: %d) waiting for signal...\n", getpid());

        // 6. 父进程可以执行其他任务...
        // 这里我们简单地等待信号处理函数被调用
        // 或者使用 sigwaitinfo/sigtimedwait 等同步等待信号

        // 7. 等待子进程结束
        if (waitpid(pid, &status, 0) == -1) {
            perror("waitpid");
            exit(EXIT_FAILURE);
        }

        if (WIFEXITED(status)) {
            printf("Child exited with status %d\n", WEXITSTATUS(status));
        } else {
            printf("Child did not exit normally.\n");
        }

        printf("Parent process continuing...\n");

        // 8. 父进程也可以发送信号给自己或测试排队
        printf("Parent sending two more instances of signal %d to itself...\n", rt_sig_num);
        value_to_send.sival_int = 100;
        if (sigqueue(getpid(), rt_sig_num, value_to_send) == -1) {
            perror("sigqueue (parent 1)");
        }
        value_to_send.sival_int = 200;
        if (sigqueue(getpid(), rt_sig_num, value_to_send) == -1) {
            perror("sigqueue (parent 2)");
        }
        printf("Parent sent two signals. They should be queued and delivered one by one.\n");

        // 9. 给一点时间处理信号
        sleep(1);

        // 10. 演示 sigtimedwait: 等待信号或超时
        sigset_t wait_mask;
        siginfo_t sig_info;
        struct timespec timeout = {2, 0}; // 2 秒超时

        sigemptyset(&wait_mask);
        sigaddset(&wait_mask, rt_sig_num);

        printf("Parent calling sigtimedwait for signal %d (timeout 2s)...\n", rt_sig_num);
        int sig_received = sigtimedwait(&wait_mask, &sig_info, &timeout);
        if (sig_received == -1) {
            if (errno == EAGAIN) {
                printf("sigtimedwait timed out.\n");
            } else {
                perror("sigtimedwait");
            }
        } else {
            printf("sigtimedwait caught signal %d\n", sig_received);
            if (sig_info.si_code == SI_QUEUE) {
                printf("  Value from sigtimedwait: %d\n", sig_info.si_value.sival_int);
            }
        }

        printf("Parent process exiting.\n");
    }

    return 0;
}

编译和运行:

# 假设代码保存在 rt_sig_example.c 中
gcc -o rt_sig_example rt_sig_example.c

# 运行
./rt_sig_example

这个示例展示了:

  1. 如何设置处理实时信号的函数 (sigaction + SA_SIGINFO)。
  2. 如何使用 sigqueue 发送带附加数据的实时信号。
  3. 实时信号的排队特性(父进程给自己发送两个信号)。
  4. 如何使用 sigtimedwait 同步等待信号并获取其信息。

这些功能底层都依赖于 rt_sig* 系统调用。

好的,我们来深入学习 rt_sigaction 系统调用,从 Linux 编程小白的角度出发。

1. 函数介绍

在 Linux 系统中,信号(Signal)是一种软件中断机制,用于通知进程发生了某个事件(例如用户按下 Ctrl+C、子进程终止、定时器到期等)。当一个信号发送给进程时,进程需要知道如何“响应”这个信号。

rt_sigaction(通常通过用户空间的 sigaction 函数调用)就是用来指定进程如何处理特定的信号。你可以告诉系统:“当收到 SIGINT(通常是 Ctrl+C)信号时,请调用我写的这个函数 my_handler 来处理”,或者“请忽略 SIGPIPE 信号”,或者“收到 SIGTERM 信号时,请执行默认操作(通常是终止进程)”。

简单来说,sigaction 是你和操作系统之间关于“如何处理信号”的一个协议或约定。它比老式的 signal 函数更强大、更可靠。

2. 函数原型

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

3. 功能

检查或修改与指定信号 signum 相关联的处理动作(disposition)。你可以设置一个新的处理动作,也可以查询当前的处理动作。

4. 参数

  • signum:
    • int 类型。
    • 你想要设置或查询的信号的编号。常见的信号有 SIGINT(中断,Ctrl+C)、SIGTERM(终止)、SIGUSR1/SIGUSR2(用户自定义信号)、SIGALRM(定时器)等。
  • act:
    • const struct sigaction * 类型。
    • 一个指针,指向一个 struct sigaction 结构体。这个结构体定义了你希望为 signum 信号设置的处理方式。如果你只是想查询当前设置而不修改它,可以传 NULL
  • oldact:
    • struct sigaction * 类型。
    • 一个指针,指向一个 struct sigaction 结构体。函数调用成功后,会把 signum 信号原来的处理方式复制到这个结构体中。如果你不关心旧的设置,可以传 NULL

5. 返回值

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如,signum 是无效的信号号)。

6. 相似函数或关联函数

  • signal: 一个更早、更简单的设置信号处理函数的方式,但功能有限且行为在不同系统上可能不一致。
  • struct sigaction: 这是 sigaction 函数的核心。你需要填充这个结构体来指定信号处理方式。它的主要成员包括:
    • sa_handler 或 sa_sigaction: 指向信号处理函数的指针。可以是 SIG_DFL(默认处理)、SIG_IGN(忽略信号)或你自定义的函数地址。
    • sa_mask: 类型为 sigset_t。当信号处理函数正在执行时,这个信号集中的信号会被临时阻塞(屏蔽),防止处理函数被其他信号中断。
    • sa_flags: 一些标志位,用于修改信号处理的行为。常用的有:
      • SA_RESTART: 如果一个系统调用(如 readwrite)被该信号中断,在信号处理函数返回后,系统调用会自动重新开始,而不是返回错误。
      • SA_SIGINFO: 如果设置了这个标志,你应该使用 sa_sigaction 成员而不是 sa_handler,并且你的处理函数签名需要是 void handler(int sig, siginfo_t *info, void *context),这样可以获取更多关于信号的信息。

7. 示例代码

下面是一个简单的例子,展示如何使用 sigaction 来处理 SIGINT(Ctrl+C)信号。

#define _GNU_SOURCE // 启用 GNU 扩展,以便使用 SA_RESTART 等
#include <stdio.h>
#include <stdlib.h>  // 包含 exit
#include <unistd.h>  // 包含 sleep
#include <signal.h>  // 包含 sigaction 相关
#include <string.h>  // 包含 memset

// 定义一个信号处理函数
// 这个函数的签名必须是 void function_name(int signum)
void handle_sigint(int sig) {
    // 注意:在信号处理函数内部,应该只调用异步信号安全(async-signal-safe)的函数
    // printf 通常不是异步信号安全的,但 write 是
    // 这里为了简单和可读性使用 printf,但在生产代码中推荐用 write
    printf("\nCaught signal %d (SIGINT, usually Ctrl+C)\n", sig);
    printf("Performing cleanup...\n");
    // 这里可以做一些清理工作,比如关闭文件、释放内存等
    // ...
    printf("Cleanup done. Exiting gracefully.\n");
    exit(EXIT_SUCCESS); // 优雅退出程序
}

int main() {
    struct sigaction sa_new;   // 用于设置新的信号处理动作
    struct sigaction sa_old;   // 用于保存旧的信号处理动作

    // 1. 初始化 sigaction 结构体
    // 使用 memset 将结构体清零,确保没有垃圾数据
    memset(&sa_new, 0, sizeof(sa_new));

    // 2. 设置处理函数
    sa_new.sa_handler = handle_sigint; // 指定我们自定义的处理函数

    // 3. 设置在处理函数执行期间要临时阻塞的信号
    // sigemptyset 初始化信号集为空
    sigemptyset(&sa_new.sa_mask);
    // 如果你想在处理 SIGINT 时也阻塞 SIGTERM,可以这样添加:
    // sigaddset(&sa_new.sa_mask, SIGTERM);

    // 4. 设置标志
    // SA_RESTART: 被信号中断的系统调用自动重启
    sa_new.sa_flags = SA_RESTART;

    // 5. 调用 sigaction 设置 SIGINT 的处理方式
    printf("Setting signal handler for SIGINT (Ctrl+C)...\n");
    if (sigaction(SIGINT, &sa_new, &sa_old) == -1) {
        // 如果 sigaction 调用失败
        perror("sigaction"); // perror 会打印错误信息
        exit(EXIT_FAILURE);  // 退出程序
    }

    // 6. 检查并打印旧的处理方式 (可选)
    printf("Old handler for SIGINT was: ");
    if (sa_old.sa_handler == SIG_DFL) {
        printf("Default action (terminate)\n");
    } else if (sa_old.sa_handler == SIG_IGN) {
        printf("Ignored\n");
    } else {
        printf("A custom function (address: %p)\n", (void*)sa_old.sa_handler);
    }

    // 7. 程序主体逻辑:进入一个循环,等待信号
    printf("Program is running. Press Ctrl+C to trigger the signal handler.\n");
    printf("Or try sending SIGTERM with 'kill %d' in another terminal.\n", getpid());
    while (1) {
        printf("Working... (Press Ctrl+C to stop)\n");
        sleep(2); // 睡眠2秒,模拟工作
        // 如果在 sleep 期间按下 Ctrl+C,handle_sigint 会被调用
    }

    // 程序正常流程不会执行到这里,因为 Ctrl+C 会调用 exit
    return 0;
}

编译和运行:

# 假设代码保存在 sigaction_example.c 中
gcc -o sigaction_example sigaction_example.c

# 运行程序
./sigaction_example

# 在程序运行时,按 Ctrl+C,观察输出
# 或者在另一个终端,使用 kill 命令发送信号
# kill -TERM <PID>  (其中 <PID> 是上面程序输出的 PID)

这个例子展示了如何使用 sigaction 来捕获 SIGINT 信号,并在信号处理函数中执行清理操作后优雅地退出程序。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注