pause系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 pause 函数,它是一个非常简单的系统调用,功能是使调用它的进程(或线程)进入睡眠(阻塞)状态,直到该进程接收到一个信号(signal)为止。


1. 函数介绍

pause 是一个 Linux 系统调用,它的作用非常直接:挂起调用它的进程,使其进入可中断的睡眠状态(interruptible sleep state)。进程会一直保持睡眠,不消耗 CPU 时间,直到发生以下两种情况之一:

1. 接收到信号: 进程被一个信号中断。这可以是任何信号,例如 SIGINT (Ctrl+C), SIGTERM (终止), SIGUSR1 (用户自定义信号) 等。
2. 进程被杀死: 例如收到 SIGKILL 信号,但这通常不会让 pause 返回,因为进程直接被终止了。
当进程因信号而被唤醒时,pause 调用会返回

pause 通常用于那些需要无限期等待某个外部事件(通过信号来通知)的程序中。它提供了一种简单、高效(不占用 CPU)的等待机制。

你可以把它想象成一个人在等待电话。他什么也不做,只是静静地坐着(睡眠),直到电话铃响(收到信号),他才会起身去接电话(pause 返回)。


2. 函数原型

#include <unistd.h> // 必需

int pause(void);

3. 功能

  • 进入睡眠: 调用 pause 的进程会立即放弃 CPU,并被放入内核的等待队列中。
  • 等待信号: 进程进入睡眠状态,直到有任何信号递达(delivered)到该进程。
  • 被信号中断: 当信号被递达时(并且该信号没有被忽略或导致进程终止),进程会从 pause 调用中返回

4. 参数

  • voidpause 函数不接受任何参数。

5. 返回值

  • 总是返回 -1pause 调用永远不会成功返回一个非负值。
  • 总是设置 errno: 当 pause 因接收到信号而返回时,它会将 errno 设置为 EINTR (Interrupted system call)。

重要pause 的返回唯一原因就是被信号中断。因此,检查返回值和 errno 通常是确认 pause 是因信号返回的标准做法。


6. 相似函数,或关联函数

  • sleepnanosleep: 这些函数使进程睡眠指定的时间。pause 是无限期睡眠,直到信号。
  • sigsuspend: 这是一个更高级、更安全的用于等待信号的函数。它允许在等待信号的原子性操作中临时替换进程的信号掩码(blocked signals set)。这可以避免在设置信号掩码和调用 pause 之间收到信号的竞态条件(race condition)。
  • 信号处理函数 (signalsigaction): 用于设置当进程收到特定信号时应执行的操作。
  • sigprocmask: 用于检查或修改进程的信号掩码(哪些信号被阻塞)。
  • waitwaitpid: 使进程等待子进程状态改变(结束、停止等),这也是一种阻塞等待。

7. 示例代码

示例 1:基本的 pause 使用和信号处理

这个例子演示了如何使用 pause 使进程等待信号,并通过信号处理函数来响应信号。

#include <unistd.h>   // pause
#include <stdio.h>    // printf, perror
#include <stdlib.h>   // exit
#include <signal.h>   // signal, SIGINT, SIGTERM
#include <errno.h>    // errno
#include <string.h>   // memset

// 全局标志,用于在信号处理函数和主循环间通信
volatile sig_atomic_t signal_received = 0;
volatile int last_signal = 0;

// 信号处理函数
void signal_handler(int sig) {
    printf("\nSignal handler called for signal %d\n", sig);
    signal_received = 1;
    last_signal = sig;
    // 注意:在信号处理函数中,应只调用异步信号安全的函数
    // printf 通常被认为是安全的,但严格来说不是 100% 可靠
    // 更安全的做法是只设置标志位,然后在主循环中检查
}

int main() {
    printf("Process PID: %d\n", getpid());
    printf("Try sending signals using 'kill %d' or pressing Ctrl+C\n", getpid());
    printf("Send SIGTERM (kill %d) or SIGINT (Ctrl+C) to exit.\n", getpid());

    // 1. 设置信号处理函数
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        perror("signal SIGINT");
        exit(EXIT_FAILURE);
    }
    if (signal(SIGTERM, signal_handler) == SIG_ERR) {
        perror("signal SIGTERM");
        exit(EXIT_FAILURE);
    }
    // 忽略 SIGUSR1,但它仍然会中断 pause
    if (signal(SIGUSR1, SIG_IGN) == SIG_ERR) {
        perror("signal SIGUSR1");
        exit(EXIT_FAILURE);
    }

    printf("Entering main loop with pause()...\n");

    // 2. 主循环
    while (1) {
        // 3. 调用 pause 进入睡眠
        printf("Going to sleep... (waiting for a signal)\n");
        int result = pause(); // 进程在此处挂起

        // 4. pause 返回(唯一原因是被信号中断)
        if (result == -1 && errno == EINTR) {
            printf("pause() was interrupted by a signal (errno=EINTR).\n");
            
            // 5. 检查是哪个信号
            if (signal_received) {
                printf("Handled signal %d in signal handler.\n", last_signal);
                if (last_signal == SIGINT || last_signal == SIGTERM) {
                    printf("Received exit signal. Cleaning up and exiting.\n");
                    break; // 退出主循环
                }
                // 为下一次循环重置标志
                signal_received = 0;
            }
        } else {
            // 这理论上不应该发生,因为 pause 总是返回 -1 和 EINTR
            printf("Unexpected return from pause(): result=%d, errno=%d (%s)\n",
                   result, errno, strerror(errno));
        }
    }

    printf("Main loop exited. Performing cleanup...\n");
    // 这里可以执行一些清理工作

    printf("Program exiting normally.\n");
    return 0;
}

代码解释:

1. 定义了两个 volatile sig_atomic_t 类型的全局变量 signal_received 和 last_signalvolatile 确保编译器不会优化对它们的访问,sig_atomic_t 是一种推荐用于信号处理函数中修改的整数类型,保证了原子性。
2. 定义了一个信号处理函数 signal_handler。当进程收到 SIGINT (Ctrl+C) 或 SIGTERM 时,该函数会被调用。它打印一条消息,并设置全局标志。
3. 在 main 函数中,使用 signal() 函数为 SIGINT 和 SIGTERM 注册了处理函数。对于 SIGUSR1,设置为忽略 (SIG_IGN),但请注意,即使是被忽略的信号,也能中断 pause
4. 进入一个无限循环 while(1)
5. 在循环内部调用 pause()。进程在此处进入睡眠状态。
6. 当进程收到信号时,pause() 调用返回,并将 errno 设置为 EINTR
7. 检查 pause 的返回值和 errno。如果符合预期(-1 和 EINTR),则继续处理。
8. 检查全局标志 signal_received,确定是哪个信号导致了 pause 返回,并根据信号类型决定是否退出循环。
9. 如果收到 SIGINT 或 SIGTERM,则跳出循环,执行清理工作并退出程序。

编译和运行:

gcc -o pause_example pause_example.c
./pause_example
# 在另一个终端:
# kill -USR1 <PID>  # 发送 SIGUSR1 (会被忽略,但会中断 pause)
# kill <PID>        # 发送 SIGTERM (默认信号,会退出)
# kill -INT <PID>   # 发送 SIGINT (等同于 Ctrl+C)

示例 2:使用 pause 等待子进程结束 (不推荐,仅作演示)

虽然 wait/waitpid 是等待子进程结束的标准方法,但这个例子演示了如何(不推荐地)使用 pause 和 SIGCHLD 信号来实现类似功能。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>

volatile sig_atomic_t child_done = 0;

void sigchld_handler(int sig) {
    // 在信号处理函数中,通常只应设置标志位
    // 实际的 wait 操作应在主循环中进行,以避免特定的竞争条件
    // 这里简化处理
    printf("SIGCHLD received.\n");
    child_done = 1;
}

int main() {
    pid_t pid;

    // 1. 设置 SIGCHLD 信号处理函数
    // SIGCHLD 在子进程状态改变时发送给父进程
    if (signal(SIGCHLD, sigchld_handler) == SIG_ERR) {
        perror("signal SIGCHLD");
        exit(EXIT_FAILURE);
    }

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

    if (pid == 0) {
        // --- 子进程 ---
        printf("Child process (PID %d) started.\n", getpid());
        sleep(5); // 模拟工作
        printf("Child process (PID %d) finished.\n", getpid());
        _exit(EXIT_SUCCESS);
    } else {
        // --- 父进程 ---
        printf("Parent process (PID %d) created child (PID %d).\n", getpid(), pid);

        // 3. 等待子进程结束
        printf("Parent entering loop with pause() to wait for child...\n");
        while (!child_done) {
            printf("Parent is waiting (paused)...\n");
            pause(); // 等待信号 (期望是 SIGCHLD)
            printf("Parent woke up from pause().\n");
            
            if (child_done) {
                printf("Parent detected child is done via signal flag.\n");
                // 清理僵尸进程
                int status;
                pid_t waited_pid = wait(&status);
                if (waited_pid == -1) {
                    perror("wait");
                } else {
                    printf("Parent reaped child PID %d with status %d.\n", waited_pid, status);
                }
            }
        }
        printf("Parent process finished.\n");
    }

    return 0;
}

代码解释:

  1. 定义了一个全局标志 child_done
  2. 定义了 SIGCHLD 信号的处理函数 sigchld_handler,当子进程结束时,内核会向父进程发送 SIGCHLD 信号,该处理函数会设置 child_done 标志。
  3. 在 main 函数中,为 SIGCHLD 注册处理函数。
  4. 使用 fork 创建子进程。
  5. 子进程: 睡眠 5 秒后退出。
  6. 父进程:
    • 进入一个循环,循环条件是 child_done 为假。
    • 在循环中调用 pause(),使父进程睡眠。
    • 当子进程结束,内核发送 SIGCHLD 信号,sigchld_handler 被调用,设置 child_done 为真。
    • pause() 返回,循环检查 child_done,发现为真,于是调用 wait() 清理子进程(收割僵尸进程)并退出循环。

重要提示与注意事项:

1. sigsuspend vs pause: 直接使用 pause 等待信号时,可能会遇到竞态条件。例如,你可能想在等待信号前先阻塞某些信号。如果在阻塞信号和调用 pause 之间信号到达,信号会被挂起,但 pause 会立即返回(因为信号已挂起)。sigsuspend 可以原子性地设置新的信号掩码并挂起进程,避免了这种竞态条件,是更推荐的方式。
2. 信号安全: 在信号处理函数中,应只调用异步信号安全(async-signal-safe)的函数。printfwrite (到 stderr) 通常被认为是安全的,但最好还是限制在修改 volatile sig_atomic_t 变量等简单操作。
3. SIGCHLD 处理: 示例 2 中的 SIGCHLD 处理方式是简化的。在实际应用中,一个信号处理函数可能需要处理多个子进程的退出,且 wait 可能需要在一个循环中调用直到没有更多子进程结束。使用 waitpid 通常更精确。
4. pause 的局限性pause 只能等待任何信号。如果你只想等待特定信号,pause 本身无法做到,需要结合信号处理函数和全局标志来间接实现。

总结:

pause 是一个简单但重要的系统调用,用于使进程高效地(不消耗 CPU)等待信号。理解其工作原理以及与信号处理机制的结合使用是掌握 Linux 进程控制和同步的基础。在需要等待异步事件时,它是一个非常有用的工具,尽管在某些复杂场景下,sigsuspend 可能是更安全的选择。

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

发表回复

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