从入门到精通:进程信号

引言

在操作系统的世界里,信号是一种用于进程间通信和控制的重要机制。信号能够在不同的进程之间传递异步事件,通知进程发生了某种情况。在Linux系统中,信号的使用是非常普遍且重要的,尤其是在处理进程控制、异常处理和进程间通信时。本文将带你深入了解Linux系统中的信号机制,从基本概念到高级应用,全面覆盖信号的生成、阻塞、捕捉和处理。通过对信号的深入理解和实际操作,你将能够更好地控制和管理进程,提高程序的健壮性和可靠性。

1. 信号入门

1.1 生活角度的信号

为了更好地理解信号,我们可以从生活中的例子开始。假设你在网上购买了很多商品,并且正在等待快递的到来。你知道快递员什么时候到达,但你不需要时刻守在门口。相反,当快递员到达时,他会给你打电话或者发送短信通知你。这时候,你可能正在做其他事情,比如玩游戏。你会在方便的时候去取快递,而不是立即去取。这就像信号处理一样,信号的产生是异步的,你可以在适当的时候处理它们。

  1. 快递通知:当快递员到达时,他会通知你,这就像操作系统向进程发送信号。信号是操作系统提供的一种异步通知机制,可以在任何时刻通知进程发生了某种事件。
  2. 延迟处理:你可以在适当的时候处理快递,而不是立即去取,就像信号可以被阻塞,直到进程准备好处理它们。信号的处理可以延迟到进程空闲时,保证进程的重要任务不被中断。
  3. 处理快递:你可以选择不同的处理方式,比如打开快递(默认处理)、转送给别人(自定义处理)或者忽略它(忽略信号)。同样地,信号的处理也有多种方式,可以执行默认动作、捕捉处理或忽略信号。

1.2 技术应用角度的信号

在技术应用中,信号是一种软中断机制,用于通知进程发生了特定的事件。例如,当你在Shell中运行一个前台进程并按下Ctrl-C时,会产生一个SIGINT信号,通知该进程应该终止。

信号的技术应用主要包括以下几个方面:

  1. 进程控制:信号可以用于控制进程的执行,例如终止进程、暂停进程和恢复进程等。常见的信号包括SIGINT(中断进程)、SIGTERM(终止进程)和SIGSTOP(暂停进程)。
  2. 异常处理:信号可以用于处理进程运行时的异常情况,例如内存访问错误、除零错误和非法指令等。常见的信号包括SIGSEGV(段错误)、SIGFPE(浮点异常)和SIGILL(非法指令)。
  3. 进程间通信:信号可以用于进程间的简单通信,例如通知子进程或父进程完成某个任务。常见的信号包括SIGCHLD(子进程状态变化)和SIGUSR1、SIGUSR2(用户定义信号)。

以下是一个简单的程序示例,它会一直等待信号:

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    while (1) {
        printf("I am a process, I am waiting for a signal!\n");
        sleep(1);
    }
    return 0;
}

当你在终端中运行这个程序,并按下Ctrl-C时,程序会收到一个SIGINT信号并终止。信号的产生和处理是异步的,这意味着进程在运行过程中可以随时接收到信号,并根据设定的处理方式进行处理。

1.3 注意事项

在使用信号时,有一些重要的注意事项需要牢记:

  1. 前台与后台进程 :Ctrl-C产生的信号只能发送给前台进程。如果你将命令放到后台运行,Shell不会等待进程结束。
  2. 异步性:信号相对于进程的控制流程是异步的,进程在运行时随时可能接收到信号。
  3. 信号丢失:如果多个相同的信号在进程未处理完第一个信号时到达,除实时信号外,其他信号可能会丢失。
  4. 信号处理的可重入性 :信号处理函数应该是可重入的,即信号处理函数不应该调用不可重入的函数,如malloc、printf等。

1.4 信号的概念

信号是进程之间事件异步通知的一种方式,属于软中断。每个信号都有一个编号和一个宏定义名称,如SIGINT、SIGTERM等。信号可以被进程捕捉并处理,也可以被进程忽略或采用默认处理动作。常见的信号包括:

  1. SIGINT (2):中断进程,通常由用户按Ctrl-C产生。
  2. SIGTERM (15):终止进程,可以由kill命令发送。
  3. SIGKILL(9):强制终止进程,无法被捕捉或忽略。
  4. SIGSTOP(19):暂停进程,无法被捕捉或忽略。
  5. SIGCONT(18):恢复暂停的进程。
  6. SIGSEGV(11):段错误,通常由于非法内存访问引起。
  7. SIGPIPE(13):写入一个没有读端的管道时产生。

2. 信号的产生

信号可以通过多种方式产生,包括终端按键、系统函数和软件条件等。

2.1 通过终端按键产生信号

在Shell中,用户可以通过按下特定的键来产生信号。例如,Ctrl-C会产生SIGINT信号,Ctrl-\会产生SIGQUIT信号。以下是一个简单的程序,验证这两个信号的默认处理动作:

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    while (1) {
        printf("Running... Press Ctrl-C or Ctrl-\\ to test signal handling\n");
        sleep(1);
    }
    return 0;
}

运行这个程序并按下Ctrl-C,你会看到程序被终止。同样,按下Ctrl-\会使程序终止并生成一个核心转储文件(core dump)。

2.2 调用系统函数向进程发信号

你可以使用kill命令或kill函数向进程发送信号。以下是一个示例,展示如何在后台运行一个程序并使用kill命令发送信号:

sh 复制代码
$ ./sig &        # 将程序放到后台运行
$ kill -SIGTERM <pid>   # 向程序发送SIGTERM信号

kill命令实际上调用了kill函数,发送信号到指定的进程。使用kill函数可以在程序中实现同样的功能:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        while (1) {
            printf("Child process running...\n");
            sleep(1);
        }
    } else {
        // 父进程
        sleep(3);
        printf("Sending SIGTERM to child process\n");
        kill(pid, SIGTERM);
        wait(NULL);
    }
    return 0;
}

在这个示例中,父进程在3秒后发送SIGTERM信号给子进程,子进程接收到信号后终止。

2.3 由软件条件产生信号

软件条件也可以产生信号,例如管道破裂(SIGPIPE)和定时器超时(SIGALRM)。以下是一个示例,使用alarm函数设置一个定时器,触发SIGALRM信号:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void alarm_handler(int sig) {
    printf("Alarm signal received\n");
}

int main() {
    signal(SIGALRM, alarm_handler);
    alarm(2);  // 设置2秒的定时器
    while (1) {
        printf("Waiting for alarm...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,程序会在2秒后接收到SIGALRM信号,并调用自定义的信号处理函数。

2.4 硬件异常产生信号

硬件异常也会产生信号,例如除以零(SIGFPE)和非法内存访问(SIGSEGV)。以下是一个示

例,模拟非法内存访问并捕捉SIGSEGV信号:

c 复制代码
#include <stdio.h>
#include <signal.h>

void segv_handler(int sig) {
    printf("Segmentation fault captured\n");
    _exit(1);
}

int main() {
    signal(SIGSEGV, segv_handler);
    int *p = NULL;
    *p = 100;  // 触发非法内存访问
    return 0;
}

在这个示例中,程序会触发一个段错误,并调用自定义的信号处理函数。

3. 信号捕捉

信号捕捉是指定义一个信号处理函数,在信号递达时执行该函数。通过信号捕捉,可以自定义信号的处理方式,例如记录日志、清理资源等。

3.1 使用signal函数捕捉信号

signal函数用于设置信号处理程序。以下是一个示例,展示如何使用signal函数捕捉SIGINT信号:

c 复制代码
#include <stdio.h>
#include <signal.h>

void sigint_handler(int sig) {
    printf("Caught SIGINT signal\n");
}

int main() {
    signal(SIGINT, sigint_handler);
    while (1) {
        printf("Press Ctrl-C to generate SIGINT signal\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,当你按下Ctrl-C时,程序会捕捉到SIGINT信号并调用自定义的信号处理函数。

3.2 使用sigaction函数捕捉信号

sigaction函数提供了更强大的信号捕捉功能,可以设置额外的选项,如信号屏蔽和实时信号处理。以下是一个示例,展示如何使用sigaction函数捕捉SIGINT信号:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <string.h>

void sigint_handler(int sig) {
    printf("Caught SIGINT signal\n");
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigint_handler;
    sigaction(SIGINT, &sa, NULL);
    while (1) {
        printf("Press Ctrl-C to generate SIGINT signal\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,我们使用sigaction函数设置SIGINT信号的处理程序,并且可以指定更多的选项,如信号屏蔽和处理标志。

3.3 可重入函数

在信号处理函数中,使用可重入函数是非常重要的。可重入函数是指可以在任何时刻中断的函数,通常不使用全局变量或静态变量。以下是一个示例,展示如何编写可重入的信号处理函数:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t flag = 0;

void sigint_handler(int sig) {
    flag = 1;
}

int main() {
    signal(SIGINT, sigint_handler);
    while (!flag) {
        printf("Waiting for SIGINT signal\n");
        sleep(1);
    }
    printf("SIGINT signal received, exiting\n");
    return 0;
}

在这个示例中,我们使用volatile和sig_atomic_t关键字确保信号处理函数是可重入的。

4. 信号阻塞与未决

4.1 信号阻塞

信号阻塞是指进程暂时不处理某些信号,而是将它们保持在未决状态,直到解除阻塞。通过信号阻塞,可以防止在关键代码段中断进程。

使用sigprocmask函数阻塞信号

以下是一个示例,展示如何使用sigprocmask函数阻塞SIGINT信号:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    printf("Caught SIGINT signal\n");
}

int main() {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    signal(SIGINT, sigint_handler);

    // 阻塞SIGINT信号
    sigprocmask(SIG_BLOCK, &set, NULL);
    printf("SIGINT signal blocked\n");
    sleep(5);

    // 解除阻塞SIGINT信号
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("SIGINT signal unblocked\n");

    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,程序会在开始时阻塞SIGINT信号,5秒后解除阻塞。期间按下Ctrl-C不会终止程序,而是在解除阻塞后立即处理该信号。

4.2 信号未决

信号未决是指信号已产生但未被递达的状态。当信号被阻塞时,进入未决状态。可以使用sigpending函数查看未决信号。

使用sigpending函数查看未决信号

以下是一个示例,展示如何使用sigpending函数查看未决信号:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    printf("Caught SIGINT signal\n");
}

int main() {
    sigset_t set, pending;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    signal(SIGINT, sigint_handler);

    // 阻塞SIGINT信号
    sigprocmask(SIG_BLOCK, &set, NULL);
    printf("SIGINT signal blocked\n");

    sleep(5);
    sigpending(&pending);

    if (sigismember(&pending, SIGINT)) {
        printf("SIGINT signal is pending\n");
    }

    // 解除阻塞SIGINT信号
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("SIGINT signal unblocked\n");

    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,程序在阻塞SIGINT信号后查看未决信号,并在解除阻塞后处理该信号。

5. 信号的高级处理

5.1 信号的默认处理动作

每个信号都有一个默认的处理动作,包括终止进程、忽略信号和生成核心转储文件等。你可以通过自定义信号处理函数改变信号的处理方式。

信号的默认处理动作分为以下几种:

  1. 终止进程:如SIGINT、SIGTERM和SIGKILL信号,默认会终止进程。
  2. 生成核心转储文件:如SIGSEGV和SIGABRT信号,默认会终止进程并生成核心转储文件。
  3. 忽略信号:如SIGCHLD和SIGURG信号,默认会被忽略。
  4. 暂停进程:如SIGSTOP和SIGTSTP信号,默认会暂停进程。
  5. 恢复进程:如SIGCONT信号,默认会恢复暂停的进程。

5.2 核心转储文件

核心转储文件(core dump)是指当进程异常终止时,将进程的内存状态保存到磁盘上的文件。核心转储文件用于调试进程异常终止时的情况,可以帮助开发者查找和修复程序中的错误。

可以使用ulimit命令设置允许生成核心转储文件的大小。例如,设置允许生成最大为1024KB的核心转储文件:

sh 复制代码
$ ulimit -c 1024

以下是一个示例,展示如何生成核心转储文件:

c 复制代码
#include <stdio.h>
#include <signal.h>

int main() {
    int *p = NULL;
    *p = 100;  // 触发段错误
    return 0;
}

运行这个程序会生成一个核心转储文件,可以使用调试器(如gdb)检查该文件以查找错误原因:

sh 复制代码
$ gdb ./a.out core
(gdb) bt

5.3 竞态条件和可重入函数

竞态条件是指多个进程或线程并发执行时,由于缺乏同步机制,导致程序的运行结果不确定的情况。在信号处理函数中,避免使用不可重入函数,如malloc、free和标准I/O库函数,因为它们可能会引发竞态条件。

以下是一个示例,展示竞态条件的发生:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int count = 0;

void handler(int sig) {
    count++;
    printf("Signal received, count = %d\n", count);
}

int main() {
    signal(SIGINT, handler);
    while (count < 5) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,如果信号

处理函数在count变量的修改过程中被中断,可能会导致count值不正确。

5.4 使用volatile关键字

volatile关键字用于告诉编译器变量的值可能随时发生变化,避免编译器对变量进行优化。在信号处理函数中,使用volatile关键字可以确保变量的正确性。

以下是一个示例,展示如何使用volatile关键字:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t flag = 0;

void sigint_handler(int sig) {
    flag = 1;
}

int main() {
    signal(SIGINT, sigint_handler);
    while (!flag) {
        printf("Waiting for SIGINT signal\n");
        sleep(1);
    }
    printf("SIGINT signal received, exiting\n");
    return 0;
}

在这个示例中,我们使用volatile关键字确保flag变量的正确性。

6. SIGCHLD信号

当子进程终止时,会向父进程发送SIGCHLD信号。父进程可以通过捕捉SIGCHLD信号处理子进程的终止,避免产生僵尸进程。

6.1 使用SIGCHLD信号处理子进程终止

以下是一个示例,展示如何使用SIGCHLD信号处理子进程的终止:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

void sigchld_handler(int sig) {
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        printf("Child process %d terminated\n", pid);
    }
}

int main() {
    signal(SIGCHLD, sigchld_handler);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        printf("Child process %d\n", getpid());
        sleep(2);
        exit(EXIT_SUCCESS);
    } else {
        printf("Parent process %d\n", getpid());
        while (1) {
            printf("Parent process is doing something\n");
            sleep(1);
        }
    }

    return 0;
}

在这个示例中,父进程通过捕捉SIGCHLD信号处理子进程的终止,避免产生僵尸进程。

7. SIGCHLD信号的处理机制

7.1 wait和waitpid函数

在处理子进程终止时,父进程通常会使用wait或waitpid函数等待子进程的结束并获取其退出状态。以下是这两个函数的基本用法:

  1. wait函数 :wait函数会阻塞父进程,直到有子进程结束为止。
c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
  1. waitpid函数 :waitpid函数提供了更灵活的等待方式,可以指定等待特定的子进程,并且可以选择阻塞或非阻塞等待。
c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

在信号处理函数中,通常使用waitpid函数并指定WNOHANG选项,以非阻塞方式等待所有结束的子进程。

7.2 自动清理子进程

除了使用SIGCHLD信号处理子进程的终止,还可以通过设置SIGCHLD信号的默认处理动作为忽略,自动清理子进程,避免产生僵尸进程。

以下是一个示例,展示如何通过设置SIGCHLD信号的默认处理动作为忽略,自动清理子进程:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    struct sigaction sa;
    sa.sa_handler = SIG_IGN;
    sigaction(SIGCHLD, &sa, NULL);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        printf("Child process %d\n", getpid());
        sleep(2);
        exit(EXIT_SUCCESS);
    } else {
        printf("Parent process %d\n", getpid());
        while (1) {
            printf("Parent process is doing something\n");
            sleep(1);
        }
    }

    return 0;
}

在这个示例中,父进程通过设置SIGCHLD信号的默认处理动作为忽略,自动清理子进程,避免产生僵尸进程。

8. 阻塞信号

8.1 信号的阻塞与未决

信号阻塞是指进程暂时不处理某些信号,而是将它们保持在未决状态,直到解除阻塞。信号未决是指信号已产生但未被递达的状态。

8.2 使用sigprocmask函数

sigprocmask函数用于设置进程的信号屏蔽字,可以阻塞或解除阻塞信号。以下是一个示例,展示如何使用sigprocmask函数阻塞和解除阻塞信号:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    printf("Caught SIGINT signal\n");
}

int main() {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    signal(SIGINT, sigint_handler);

    // 阻塞SIGINT信号
    sigprocmask(SIG_BLOCK, &set, NULL);
    printf("SIGINT signal blocked\n");
    sleep(5);

    // 解除阻塞SIGINT信号
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("SIGINT signal unblocked\n");

    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,程序会在开始时阻塞SIGINT信号,5秒后解除阻塞。期间按下Ctrl-C不会终止程序,而是在解除阻塞后立即处理该信号。

8.3 使用sigpending函数查看未决信号

sigpending函数用于查看进程的未决信号。以下是一个示例,展示如何使用sigpending函数查看未决信号:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    printf("Caught SIGINT signal\n");
}

int main() {
    sigset_t set, pending;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    signal(SIGINT, sigint_handler);

    // 阻塞SIGINT信号
    sigprocmask(SIG_BLOCK, &set, NULL);
    printf("SIGINT signal blocked\n");

    sleep(5);
    sigpending(&pending);

    if (sigismember(&pending, SIGINT)) {
        printf("SIGINT signal is pending\n");
    }

    // 解除阻塞SIGINT信号
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("SIGINT signal unblocked\n");

    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,程序在阻塞SIGINT信号后查看未决信号,并在解除阻塞后处理该信号。

9. 可重入函数

9.1 什么是可重入函数

可重入函数是指可以在任何时刻被中断,并且在中断后可以正确地继续执行的函数。可重入函数通常不使用全局变量或静态变量,不依赖于不可重入的函数,如malloc、free和标准I/O库函数。

9.2 编写可重入函数

在编写信号处理函数时,确保函数是可重入的非常重要。以下是一个示例,展示如何编写可重入的信号处理函数:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t flag = 0;

void sigint_handler(int sig) {
    flag = 1;
}

int main() {
    signal(SIGINT, sigint_handler);
    while (!flag) {
        printf("Waiting for SIGINT signal\n");
        sleep(1);
    }
    printf("SIGINT signal received, exiting\n");
    return 0;
}

在这个示例中,我们使用volatile和sig_atomic_t关键字确保信号处理函数是可重入的。

9.3 常见的不可重入函数

在信号处理函数中,应该避免使用以下不可重入函数:

  1. 动态内存分配函数 :如malloc、calloc和free。
    2

. 标准I/O库函数 :如printf、sprintf和fgets。

  1. 时间和日期函数 :如ctime、localtime和gmtime。

  2. 环境变量函数 :如getenv和putenv。

这些函数通常使用全局数据结构或静态数据结构,不适合在信号处理函数中使用。

10. 信号处理的高级话题

10.1 核心转储文件

核心转储文件(core dump)是指当进程异常终止时,将进程的内存状态保存到磁盘上的文件。核心转储文件用于调试进程异常终止时的情况,可以帮助开发者查找和修复程序中的错误。

以下是一个示例,展示如何生成核心转储文件:

c 复制代码
#include <stdio.h>
#include <signal.h>

int main() {
    int *p = NULL;
    *p = 100;  // 触发段错误
    return 0;
}

运行这个程序会生成一个核心转储文件,可以使用调试器(如gdb)检查该文件以查找错误原因:

sh 复制代码
$ gdb ./a.out core
(gdb) bt

10.2 竞态条件和信号处理

竞态条件是指多个进程或线程并发执行时,由于缺乏同步机制,导致程序的运行结果不确定的情况。在信号处理函数中,避免使用不可重入函数,如malloc、free和标准I/O库函数,因为它们可能会引发竞态条件。

以下是一个示例,展示竞态条件的发生:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int count = 0;

void handler(int sig) {
    count++;
    printf("Signal received, count = %d\n", count);
}

int main() {
    signal(SIGINT, handler);
    while (count < 5) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,如果信号处理函数在count变量的修改过程中被中断,可能会导致count值不正确。

10.3 使用volatile关键字

volatile关键字用于告诉编译器变量的值可能随时发生变化,避免编译器对变量进行优化。在信号处理函数中,使用volatile关键字可以确保变量的正确性。

以下是一个示例,展示如何使用volatile关键字:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t flag = 0;

void sigint_handler(int sig) {
    flag = 1;
}

int main() {
    signal(SIGINT, sigint_handler);
    while (!flag) {
        printf("Waiting for SIGINT signal\n");
        sleep(1);
    }
    printf("SIGINT signal received, exiting\n");
    return 0;
}

在这个示例中,我们使用volatile关键字确保flag变量的正确性。

11. 信号的实际应用

11.1 使用信号实现定时任务

信号可以用于实现定时任务,例如定时器。以下是一个示例,展示如何使用SIGALRM信号实现定时任务:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler(int sig) {
    printf("Timer expired\n");
    alarm(2);  // 重新设置定时器
}

int main() {
    signal(SIGALRM, alarm_handler);
    alarm(2);  // 设置2秒的定时器
    while (1) {
        printf("Waiting for timer...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,程序会每隔2秒打印一次"Timer expired",实现定时任务的功能。

11.2 使用信号处理进程间通信

信号可以用于进程间的简单通信,例如通知子进程或父进程完成某个任务。以下是一个示例,展示如何使用SIGUSR1信号实现进程间通信:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigusr1_handler(int sig) {
    printf("Received SIGUSR1 signal\n");
}

int main() {
    signal(SIGUSR1, sigusr1_handler);

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        sleep(2);
        kill(getppid(), SIGUSR1);
    } else {
        // 父进程
        pause();  // 等待信号
    }
    return 0;
}

在这个示例中,子进程在2秒后发送SIGUSR1信号给父进程,父进程接收到信号后调用自定义的信号处理函数。

11.3 使用信号处理多线程程序

在多线程程序中,信号处理需要特别注意,因为信号默认会发送到整个进程,而不是特定的线程。以下是一个示例,展示如何在多线程程序中使用信号:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>

void *thread_func(void *arg) {
    printf("Thread %d running\n", *(int *)arg);
    while (1) {
        sleep(1);
    }
    return NULL;
}

void sigint_handler(int sig) {
    printf("Caught SIGINT signal\n");
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    signal(SIGINT, sigint_handler);

    for (int i = 0; i < 2; i++) {
        if (pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    for (int i = 0; i < 2; i++) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

在这个示例中,主线程设置了SIGINT信号的处理函数,当按下Ctrl-C时,信号处理函数会被调用。信号处理函数在多线程程序中的使用需要小心,避免使用不可重入的函数。

结论

本文详细介绍了Linux系统中的信号机制,包括信号的生成、阻塞、捕捉和处理。通过具体的示例代码展示了信号的各种应用场景和处理方式。信号是进程间通信和控制的重要工具,理解和掌握信号机制可以帮助你更好地控制和管理进程,提高程序的健壮性和可靠性。希望通过本文的学习,你能更加深入地理解和掌握信号机制,为开发高效、可靠的程序打下坚实的基础。

嗯,就是这样啦,文章到这里就结束啦,真心感谢你花时间来读。

觉得有点收获的话,不妨给我点个赞吧!

如果发现文章有啥漏洞或错误的地方,欢迎私信我或者在评论里提醒一声~

相关推荐
aixingkong9211 小时前
Nvidia为例 XPU的Multi-Host、Socket Direct 、GPUDirect、RDMA 技术点分析
服务器·网络·硬件架构·硬件工程
雯宝3 小时前
mnt_init 函数
linux
UIU1144 小时前
递归算法与汉诺塔
c++·学习·算法·c#·递归
Fruit_Caller4 小时前
static
开发语言·c++
律宏阔9 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔9 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴9 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千9 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei379 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei379 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化