从入门到精通:进程信号

引言

在操作系统的世界里,信号是一种用于进程间通信和控制的重要机制。信号能够在不同的进程之间传递异步事件,通知进程发生了某种情况。在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. 信号处理的可重入性 :信号处理函数应该是可重入的,即信号处理函数不应该调用不可重入的函数,如mallocprintf等。

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;
}

在这个示例中,我们使用volatilesig_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 竞态条件和可重入函数

竞态条件是指多个进程或线程并发执行时,由于缺乏同步机制,导致程序的运行结果不确定的情况。在信号处理函数中,避免使用不可重入函数,如mallocfree和标准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函数

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

  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 什么是可重入函数

可重入函数是指可以在任何时刻被中断,并且在中断后可以正确地继续执行的函数。可重入函数通常不使用全局变量或静态变量,不依赖于不可重入的函数,如mallocfree和标准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;
}

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

9.3 常见的不可重入函数

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

  1. 动态内存分配函数 :如malloccallocfree
    2

. 标准I/O库函数 :如printfsprintffgets

  1. 时间和日期函数 :如ctimelocaltimegmtime

  2. 环境变量函数 :如getenvputenv

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

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 竞态条件和信号处理

竞态条件是指多个进程或线程并发执行时,由于缺乏同步机制,导致程序的运行结果不确定的情况。在信号处理函数中,避免使用不可重入函数,如mallocfree和标准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系统中的信号机制,包括信号的生成、阻塞、捕捉和处理。通过具体的示例代码展示了信号的各种应用场景和处理方式。信号是进程间通信和控制的重要工具,理解和掌握信号机制可以帮助你更好地控制和管理进程,提高程序的健壮性和可靠性。希望通过本文的学习,你能更加深入地理解和掌握信号机制,为开发高效、可靠的程序打下坚实的基础。

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

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

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

相关推荐
N1cez31 分钟前
linux conda 安装 配置
linux·conda
chuanshan23433 分钟前
2024.9.26C++作业
c++
GOTXX36 分钟前
【计算机网络】初识Socket编程,揭秘Socket编程艺术--UDP篇
linux·开发语言·网络·计算机网络·php·socket·套接字
蓝天扶光37 分钟前
初识Linux以及Linux的基本命令
linux·运维·服务器
cyt涛42 分钟前
WEB服务器——Tomcat
运维·服务器·http·servlet·tomcat·web·jsp
2401_8543910843 分钟前
Spring Boot影院管理系统:小徐的创新
服务器·数据库·spring boot
promise5241 小时前
MySQL实现按分秒统计数据量
linux·运维·数据库·sql·mysql·shell
想躺平的做题家1 小时前
Linux高级编程_26_shell
linux·运维·服务器·c
玉树临风江流儿1 小时前
Linux驱动开发(速记版)--驱动基础
linux·运维·服务器
Leaf Ye1 小时前
Android常用C++特性之std::unique_lock
c++