Linux进程通信-信号

  1. 信号概念

信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号

可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

  1. 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C

通常会给进程发送一个中断信号。

 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给

相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的

内存区域。

 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU

时间超限,或者该进程的某个子进程退出。

 运行 kill 命令或调用 kill 函数。

  1. 使用信号的两个主要目的是:

 让进程知道已经发生了一个特定的事情。

 强迫进程执行它自己代码中的信号处理程序。

  1. 信号的特点:

 简单

 不能携带大量信息

 满足某个特定条件才发送

 优先级比较高

查看系统定义的信号列表:kill –l

前 31 个信号为常规信号,其余为实时信号。
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson25$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

image-20240505220458667

image-20240505220510108

image-20240505220520571

信号的 5 种默认处理动作

查看信号的详细信息:man 7 signal

◼ 信号的 5 中默认处理动作

 Term 终止进程

 Ign 当前进程忽略掉这个信号

 Core 终止进程,并生成一个Core文件(保存进程异常退出的错误信息)

 Stop 暂停当前进程

 Cont 继续执行当前被暂停的进程

◼ 信号的几种状态:产生、未决、递达

◼ SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作

#include <stdio.h>
#include <string.h>

int main() {

    char * buf; //指针没有指向合法内存

    strcpy(buf, "hello");

    return 0;
}

信号相关函数

kill

在编程中,kill 函数通常用于向进程发送信号,通知它进行某种操作,如终止或暂停。这个函数最常见于类 Unix 操作系统,如 Linux 和 macOS,它的功能是通过发送信号来控制进程的行为。

函数定义

在 C 语言中,kill 函数定义如下:

#include <signal.h>
int kill(pid_t pid, int sig);
  • pid_t pid: 指定要发送信号的进程 ID。如果 pid 大于 0,信号将发送给具有该 ID 的进程。如果 pid 等于 0,信号将发送给与发送者同一进程组的所有进程。
  • int sig: 要发送的信号。例如,SIGTERM 是请求终止进程的标准信号,SIGKILL 是强制终止进程的信号。
返回值
  • 成功时,kill 函数返回 0。
  • 失败时,返回 -1,并且 errno 被设置为错误的具体原因,比如 EINVAL 表示一个无效的信号,或 EPERM 表示没有足够的权限来发送信号。
示例

下面是一个使用 kill 函数的简单示例,它向进程发送一个 SIGTERM 信号:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    pid_t pid = 1234; // 假设进程ID为1234
    int result = kill(pid, SIGTERM);

    if (result == 0) {
        printf("Signal sent successfully\n");
    } else {
        perror("Failed to send signal");
    }

    return 0;
}

这个函数在系统编程中非常重要,用于进程间通信和进程控制,尤其是在需要清理僵尸进程或优雅地终止服务时非常有用。

在编程中,raise 函数通常用于在当前进程中生成一个信号。这个函数是C语言标准库中的一部分,定义在 <signal.h> 头文件中。它允许程序在其自身的执行流中发送信号,这可以用于各种目的,如触发信号处理程序或模拟外部事件的影响。

raise
函数定义

在 C 语言中,raise 函数的定义如下:

#include <signal.h>
int raise(int sig);
  • int sig: 要发送的信号代码。例如,SIGINT 通常用于表示中断信号(如Ctrl+C触发的),而 SIGTERM 用于请求终止程序。
返回值
  • 成功时,raise 函数返回 0。
  • 失败时,返回非0值。
示例

下面是一个使用 raise 函数的简单示例,它向自己发送一个 SIGTERM 信号:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void signal_handler(int signal) {
    if (signal == SIGTERM) {
        printf("Received SIGTERM, exiting...\n");
        exit(0);
    }
}

int main() {
    if (signal(SIGTERM, signal_handler) == SIG_ERR) {
        printf("Error: Cannot set signal handler.\n");
        return 1;
    }

    printf("Raising SIGTERM...\n");
    int result = raise(SIGTERM);

    if (result != 0) {
        perror("Failed to raise signal");
        return 1;
    }

    return 0; // 这行代码永远不会执行,因为signal_handler中已经调用了exit()
}

在这个例子中,raise 函数用于发送 SIGTERM 信号给程序自身,当信号被触发时,通过安装的信号处理程序 signal_handler 捕获并处理这个信号,随后程序退出。

使用场景

raise 函数常用于测试信号处理代码、在特定情况下自我终止程序、或者实现程序内部的通信机制。它提供了一种有效的方式来模拟信号发送,帮助开发者在单个程序内部测试和处理各种信号相关的逻辑。

abort

在编程中,abort 函数用于立即终止当前进程的执行,通常用于处理无法恢复的错误或异常情况。abort 是 C 语言标准库中的一个函数,定义在 <stdlib.h> 头文件中。当调用 abort 时,程序会生成一个 SIGABRT(abort signal,终止信号)信号,这通常会导致程序异常终止。

函数定义

在 C 语言中,abort 函数的定义如下:

#include <stdlib.h>
void abort(void);
  • 该函数没有参数。
  • 函数声明为 void 返回类型,意味着它不返回任何值。
行为描述
  • 终止进程abort 函数会立即终止调用它的进程。
  • 信号生成:调用 abort 通常会生成 SIGABRT 信号,该信号的默认行为是终止进程。
  • 内存和资源:由于进程是突然终止的,操作系统负责回收任何由进程使用的内存和系统资源。
  • 核心转储:在许多系统上,默认情况下,abort 的调用会产生一个核心转储文件,这可以用于后续的调试和分析。
示例

下面是一个使用 abort 函数的简单示例,展示了如何在检测到某些错误条件时终止程序:

#include <stdlib.h>
#include <stdio.h>

int main() {
    printf("Something went horribly wrong!\n");
    abort(); // 终止程序
    // 下面的代码永远不会执行
    printf("This line will never be executed.\n");
    return 0;
}

在这个示例中,调用 abort() 会立即终止程序。由于 abort 导致程序异常终止,"This line will never be executed." 这一行将永远不会执行。

使用场景

abort 函数通常在以下情况下使用:

  • 错误处理:在检测到无法恢复的错误或严重异常情况时,使用 abort 来立即终止程序。
  • 调试:在开发过程中,当程序到达不应该到达的代码路径时,使用 abort 来强制停止程序并生成核心转储,便于开发者调试。
  • 安全性:在关键安全验证失败时,使用 abort 以防止潜在的安全漏洞被利用。

abort 是一个强大的工具,用于处理那些需要立即停止程序执行的情况,但它应该谨慎使用,因为它不提供进行任何清理操作的机会,如关闭文件句柄或释放动态分配的内存。

/*  
    #include <sys/types.h>
    #include <signal.h>

    int kill(pid_t pid, int sig);
        - 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
        - 参数:
            - pid :
                > 0 : 将信号发送给指定的进程
                = 0 : 将信号发送给当前的进程组
                = -1 : 将信号发送给每一个有权限接收这个信号的进程
                < -1 : 这个pid=某个进程组的ID取反 (-12345)
            - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

        kill(getppid(), 9);
        kill(getpid(), 9);
        
    int raise(int sig);
        - 功能:给当前进程发送信号
        - 参数:
            - sig : 要发送的信号
        - 返回值:
            - 成功 0
            - 失败 非0
        kill(getpid(), sig); 实现同样功能  

    void abort(void);
        - 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
        kill(getpid(), SIGABRT);
*/

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

int main() {

    pid_t pid = fork();

    if(pid == 0) {
        // 子进程
        int i = 0;
        for(i = 0; i < 5; i++) {
            printf("child process\n");
            sleep(1);
        }

    } else if(pid > 0) {
        // 父进程
        printf("parent process\n");
        sleep(2);
        printf("kill child process now\n");
        kill(pid, SIGINT);
    }

    return 0;
}

daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out 
parent process
child process
child process
kill child process now
alarm

在编程中,alarm 函数用于设置一个计时器(闹钟),在指定的秒数后发送 SIGALRM 信号到当前进程。这个函数是 Unix 系统的标准部分,主要用于在进程需要在一定时间后接收到通知时创建简单的时间延迟或超时。

函数定义

在 C 语言中,alarm 函数定义在 <unistd.h> 头文件中,其函数原型如下:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 参数:

    • unsigned int seconds: 指定在多少秒后产生 SIGALRM 信号。如果 seconds 为0,则取消任何之前设置的闹钟。
  • 返回值:

    • 返回值是之前设置的闹钟时间的剩余秒数,如果之前没有设置闹钟,则返回0。
行为描述
  • 信号发送:当 alarm 函数设置的计时器到期时,将向进程发送 SIGALRM 信号。如果进程没有捕获或忽略此信号,其默认行为是终止进程。
  • 闹钟取消:通过传递参数0可以取消之前设置的计时器。
  • 闹钟覆盖:如果在之前的 alarm 调用之后再次调用 alarm,新的调用将覆盖旧的设置。
示例

下面是一个使用 alarm 函数的示例,该示例设置一个5秒后的计时器,并捕获 SIGALRM 信号:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handle_sigalrm(int sig) {
    printf("Received SIGALRM - alarm went off!\n");
}

int main() {
    // 设置 SIGALRM 的处理函数
    signal(SIGALRM, handle_sigalrm);

    // 设置 alarm 为5秒
    alarm(5);

    printf("Alarm set for 5 seconds...\n");
    // 等待信号到来,通常这里会做一些其他的工作
    pause();  // 使进程挂起直到捕获到一个信号

    printf("Continuing execution...\n");

    return 0;
}

在这个程序中,通过 alarm 函数设定一个5秒后的计时器,并通过 signal 函数设定 SIGALRM 的处理函数 handle_sigalrm。程序执行 pause() 函数后会挂起直到收到信号。一旦信号 SIGALRM 被接收,相应的处理函数将被调用,然后程序继续执行。

使用场景

alarm 函数常用于以下场景:

  • 实现超时:在执行可能长时间运行的操作时,用于强制超时。
  • 周期性任务:虽然 alarm 只能设置一个定时器,它可以用于触发周期性执行的任务。
  • 资源管理:在一段时间后自动释放或检查资源状态。

这个函数提供了一个简单但有效的机制来处理与时间相关的事件,尤其是在需要在指定时间后执行特定操作的场景中非常有用。

/*
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
        - 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
                函数会给当前的进程发送一个信号:SIGALARM
        - 参数:
            seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
                    取消一个定时器,通过alarm(0)。
        - 返回值:
            - 之前没有定时器,返回0
            - 之前有定时器,返回之前的定时器剩余的时间

    - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
        alarm(10);  -> 返回0
        过了1秒
        alarm(5);   -> 返回9

    alarm(100) -> 该函数是不阻塞的
*/

#include <stdio.h>
#include <unistd.h>

int main() {

    int seconds = alarm(5);
    printf("seconds = %d\n", seconds);  // 0

    sleep(2);
    seconds = alarm(2);    // 不阻塞
    printf("seconds = %d\n", seconds);  // 3

    while(1) {
    }

    return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out 
seconds = 002multiPr
seconds = 3
Alarm clock
// 1秒钟电脑能数多少个数?
#include <stdio.h>
#include <unistd.h>

/*
    实际的时间 = 内核时间 + 用户时间 + 消耗的时间
    进行文件IO操作的时候比较浪费时间

    定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/

int main() {    

    alarm(1);

    int i = 0;
    while(1) {
        printf("%i\n", i++);
    }

    return 0;
}
setitimer

setitimer 函数是 alarm 函数的更灵活和强大的版本,提供了对进程定时器的精细控制。它允许设置可以重复的定时器,并且具有更高的时间精度。setitimer 在 Unix-like 系统中使用广泛,通常定义在 <sys/time.h> 头文件中。

函数定义

在 C 语言中,setitimer 的定义如下:

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
  • 参数:
    • int which: 定时器的类型。可选值包括 ITIMER_REAL(减少真实时间,到期发送 SIGALRM)、ITIMER_VIRTUAL(减少进程虚拟时间,到期发送 SIGVTALRM)、ITIMER_PROF(减少进程运行时间加上系统时间,到期发送 SIGPROF)。
    • const struct itimerval *new_value: 指向 itimerval 结构的指针,该结构指定新的定时器值。
    • struct itimerval *old_value: 可选,用于存储之前定时器的值。如果不关心旧值,可以设置为 NULL

struct itimerval 结构定义如下:

struct itimerval {
    struct timeval it_interval; // 下一次定时器触发的间隔时间
    struct timeval it_value;    // 定时器首次触发的延迟时间
};

struct timeval {
    long tv_sec;  // 秒
    long tv_usec; // 微秒
};
  • it_interval 设置为非零值时,setitimer 会重复触发信号。
  • it_value 设置为零时,定时器被禁用。
返回值
  • 成功时,setitimer 返回 0
  • 失败时,返回 -1 并设置 errno 以指示错误原因(如 EINVAL 表示参数无效)。
示例

以下是一个使用 setitimer 来每隔一定时间发送 SIGALRM 的示例:

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>

void timer_handler(int signum) {
    static int count = 0;
    printf("Timer expired %d times\n", ++count);
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = &timer_handler;
    sigaction(SIGALRM, &sa, NULL);

    struct itimerval timer;
    timer.it_value.tv_sec = 1; // 初始延迟1秒
    timer.it_value.tv_usec = 0;

    timer.it_interval.tv_sec = 2; // 之后每2秒触发一次
    timer.it_interval.tv_usec = 0;

    setitimer(ITIMER_REAL, &timer, NULL);

    while (1) {
        sleep(1);  // 让程序保持运行以持续接收信号
    }

    return 0;
}

在这个程序中,定时器首次设置为1秒后触发,之后每2秒重复触发。信号处理函数 timer_handler 在每次信号到来时被调用,记录并打印触发次数。

使用场景

setitimer 常用于需要高精度定时或需要定时器重复触发的场合,如:

  • 性能监控:使用 ITIMER_PROF 来监测程序的性能。
  • 资源管理:定期检查和管理系统资源。
  • 实时应用:在需要精确控制时间的实时应用中,比如多媒体处理或游戏。

这个函数提供了对 Unix 系统中进程定时器的高级控制,但也需要注意使用 setitimer 可能会引入复杂性,特别是在涉及信号和多线程

时。

/*
    #include <sys/time.h>
    int setitimer(int which, const struct itimerval *new_value,
                        struct itimerval *old_value);
    
        - 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
        - 参数:
            - which : 定时器以什么时间计时
              ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM   常用
              ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
              ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF

            - new_value: 设置定时器的属性
            
                struct itimerval {      // 定时器的结构体
                struct timeval it_interval;  // 每个阶段的时间,间隔时间
                struct timeval it_value;     // 延迟多长时间执行定时器
                };

                struct timeval {        // 时间的结构体
                    time_t      tv_sec;     //  秒数     
                    suseconds_t tv_usec;    //  微秒    
                };

            过10秒后,每个2秒定时一次
           
            - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
        
        - 返回值:
            成功 0
            失败 -1 并设置错误号
*/

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

// 过3秒以后,每隔2秒钟定时一次
int main() {

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;


    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    if(ret == -1) {
        perror("setitimer");
        exit(0);
    }

    getchar();

    return 0;
}
定时器开始了...
Alarm clock

信号捕捉函数

signal

在 C 语言中,signal 函 方式。它是 Unix 和类 Unix 系统中的标准部分,用于定义当特定信号被传递给进程时应执行的行为。signal 函数定义在 <signal.h> 头文件中。

函数定义

signal 函数的原型如下:

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
  • 参数:
    • int sig: 要处理的信号代码,如 SIGINT(中断信号,通常由 Ctrl+C 产生),SIGTERM(终止信号),SIGALRM(由 alarm 函数产生的定时信号),等等。
    • void (*func)(int): 指向信号处理函数的指针。该函数必须接受一个整型参数(即信号编号)。此参数可以是一个指向函数的指针,用于处理信号;也可以是 SIG_IGN 忽略信号,或 SIG_DFL 使用信号的默认处理方式。
返回值
  • 返回一个指向之前关联到指定信号的处理函数的指针。如果调用失败,返回 SIG_ERR 并设置 errno 来指示错误。
示例

下面是一个使用 signal 函数来设置信号处理程序的示例,该示例中处理 SIGINT 信号:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void  (int signal) {
    printf("Received SIGINT, exiting...\n");
    exit(0);
}

int main() {
    // 设置 SIGINT 的处理函数
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        printf("Failed to set signal handler.\n");
        return 1;
    }

    printf("Waiting for SIGINT...\n");
    // 让程序保持运行,等待信号
    while(1) {
        sleep(1);
    }

    return 0;
}

在这个程序中,当用户按 Ctrl+C 时,将不会执行默认的终止操作,而是调用 signal_handler 函数,打印一条消息并退出程序。

注意事项和限制
  • 可移植性问题signal 函数的行为在不同的系统或库实现之间可能会有所不同。特别是在信号被处理时程序的行为(如信号是否自动重置为默认行为)可能不一致。
  • 推荐使用 sigaction:由于 signal 函数的这些可移植性问题,建议在需要精确控制信号处理行为的情况下使用更现代且功能更全的 sigaction 函数。sigaction 提供了更详细的控制,包括阻止在信号处理函数执行时送达的其他信号等功能。

signal 函数是处理 Unix 信号的基本工具之一,适用于简单的信号处理需求,但在复杂应用中,更稳定且功能丰富的 sigaction 往往是更好的选择。

/*
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
        - 功能:设置某个信号的捕捉行为
        - 参数:
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN : 忽略信号
                - SIG_DFL : 使用信号默认的行为
                - 回调函数 :  这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
                回调函数:
                    - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                    - 不是程序员调用,而是当信号产生,由内核调用
                    - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

        - 返回值:
            成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号
            
    SIGKILL SIGSTOP不能被捕捉,不能被忽略。
*/

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num) {
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() {  

    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
    signal(SIGALRM, myalarm);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    if(ret == -1) {
        perror("setitimer");
        exit(0);
    }

    getchar();

    return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out 
定时器开始了...
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14

信号集

许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为

信号集的数据结构来表示,其系统数据类型为 sigset_t。

◼ 在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” (阻塞信号被处理),另一个称之为

“未决信号集” 。这两个信号集都是内核使用位图(二进制位 )机制来实现的。但操作系统不允许我

们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数

来对 PCB 中的这两个信号集进行修改。

◼ 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。

◼ 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

◼ 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,

所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

image-20240507172721278

信号集相关函数

sigemptyset

在 C 语言中,sigemptyset 函数用于初始化一个信号集,将其清空,即从该集合中移除所有信号。这个函数是信号处理相关函数的一部分,定义在 <signal.h> 头文件中,并且通常与其他信号操作函数一起使用,以控制信号的阻塞、忽略或处理行为。

函数定义

sigemptyset 函数的定义如下:

#include <signal.h>
int sigemptyset(sigset_t *set);
  • 参数:
    • sigset_t *set: 指向 sigset_t 类型的指针,该类型用于存储信号集。
返回值
  • 成功时,sigemptyset 返回 0
  • 失败时,通常情况下,此函数不应失败,因此如果出现非零返回值,应检查代码其他部分是否有错误。
示例

以下是一个如何使用 sigemptyset 函数来初始化信号集的示例:

#include <stdio.h>
#include <signal.h>

int main() {
    sigset_t signalSet;
    int result;

    // 初始化信号集为空
    result = sigemptyset(&signalSet);
    if (result == 0) {
        printf("Signal set successfully initialized as empty.\n");
    } else {
        printf("Failed to initialize signal set.\n");
    }

    // 现在可以向此空集中添加信号,或用它来设置信号掩码等操作
    return 0;
}

在这个示例中,首先声明一个 sigset_t 类型的变量 signalSet,然后使用 sigemptyset 函数初始化它,确保该信号集不包含任何信号。这是处理信号时设置信号集的通常做法,尤其是在设置新的信号掩码或在多线程应用中为特定线程配置信号之前。

使用场景

sigemptyset 通常在下列情况中使用:

  • 在设置新的信号处理或信号掩码之前,确保信号集中不含任何未预期的信号。
  • 清除现有信号集以重新配置程序的信号处理行为。
  • 在多线程程序中,为不同线程设置不同的信号掩码。

这个函数是信号处理编程中的基础,允许程序员精确地控制哪些信号应该被阻塞或处理,从而提高程序的可靠性和响应性。

sigfillset

在 C 语言中,sigfillset 函数用于初始化一个信号集,并将所有已定义的信号添加到这个集合中。这个函数是信号处理相关函数的一部分,定义在 <signal.h> 头文件中,并且通常与其他信号操作函数一起使用,以控制信号的阻塞、忽略或处理行为。

函数定义

sigfillset 函数的定义如下:

#include <signal.h>
int sigfillset(sigset_t *set);
  • 参数:
    • sigset_t *set: 指向 sigset_t 类型的指针,该类型用于存储信号集。
返回值
  • 成功时,sigfillset 返回 0
  • 失败时,通常情况下,此函数不应失败,因此如果出现非零返回值,应检查代码其他部分是否有错误。
示例

以下是一个如何使用 sigfillset 函数来初始化并填充信号集的示例:

#include <stdio.h>
#include <signal.h>

int main() {
    sigset_t signalSet;
    int result;

    // 初始化信号集并添加所有信号
    result = sigfillset(&signalSet);
    if (result == 0) {
        printf("Signal set successfully initialized and filled with all signals.\n");
    } else {
        printf("Failed to initialize and fill the signal set.\n");
    }

    // 可以用此信号集来阻塞所有信号
    if (sigprocmask(SIG_SETMASK, &signalSet, NULL) == 0) {
        printf("All signals are now blocked.\n");
    } else {
        perror("sigprocmask");
    }

    return 0;
}

在这个示例中,首先使用 sigfillset 函数初始化并填充 sigset_t 类型的变量 signalSet,使其包含所有可用信号。然后使用 sigprocmask 函数设置信号掩码,阻塞所有信号,这在需要确保某代码段不被信号打断时非常有用。

使用场景

sigfillset 通常在下列情况中使用:

  • 当需要阻塞进程中的所有信号以执行关键代码段时,可以使用此函数填充信号集,并通过 sigprocmask 设置为当前的信号掩码。
  • 在需要清理或重置信号掩码到最初状态时,可以使用此函数以确保所有信号都被考虑到。

这个函数对于需要精细控制信号行为的程序是非常重要的,它允许开发者确保在特定时刻不会有任何外部信号干扰程序的执行。

sigdelset

在 C 语言中,sigdelset 函数用于从一个信号集中删除一个特定的信号。这个函数是 Unix 和类 Unix 系统中信号处理操作的一部分,定义在 <signal.h> 头文件中。使用 sigdelset 可以修改信号集,这对于精确控制哪些信号应该被阻塞或忽略是非常有用的。

函数定义

sigdelset 函数的定义如下:

#include <signal.h>
int sigdelset(sigset_t *set, int signum);
  • 参数:
    • sigset_t *set: 指向 sigset_t 类型的指针,该类型用于存储信号集。
    • int signum: 指定要从信号集中删除的信号。
返回值
  • 成功时,sigdelset 返回 0
  • 失败时,返回 -1 并设置 errno 来指示错误。常见的错误原因是 signum 不是有效的信号编号。
示例

以下是一个使用 sigdelset 函数来从信号集中删除一个信号的示例:

#include <stdio.h>
#include <signal.h>

int main() {
    sigset_t signalSet;
    int result;

    // 初始化信号集并添加所有信号
    sigfillset(&signalSet);

    // 从信号集中删除 SIGINT
    result = sigdelset(&signalSet, SIGINT);
    if (result == 0) {
        printf("SIGINT successfully removed from the signal set.\n");
    } else {
        perror("sigdelset");
    }

    // 现在可以使用修改后的信号集来阻塞其他所有信号,而不阻塞 SIGINT
    if (sigprocmask(SIG_SETMASK, &signalSet, NULL) == 0) {
        printf("All signals except SIGINT are now blocked.\n");
    } else {
        perror("sigprocmask");
    }

    return 0;
}

在这个示例中,首先使用 sigfillset 函数初始化并填充 sigset_t 类型的变量 signalSet,使其包含所有可用信号。然后使用 sigdelset 函数从信号集中删除 SIGINT(通常是 Ctrl+C 生成的中断信号)。之后,通过 sigprocmask 设置信号掩码,阻塞除 SIGINT 之外的所有信号。

使用场景

sigdelset 通常在以下情况中使用:

  • 在需要确保某些信号不被阻塞时,比如在执行关键任务时需要响应中断信号(如 SIGINT)。
  • 修改现有的信号集,以便在多线程应用中为特定线程配置信号处理或阻塞策略。
  • 在信号处理程序中动态调整信号掩码,以便在处理某些信号时允许或禁止其他信号的递送。

这个函数使得信号处理在程序中更加灵活,有助于提高程序对外部事件的响应能力和可控性。

sigismember

在 C 语言中,sigismember 函数用于检查特定信号是否为给定信号集的成员。这个函数是 Unix 类操作系统中 <signal.h> 头文件提供的信号处理函数集的一部分。它常用于确定特定信号是否已被添加到信号集中,这对于配置应用程序中的信号处理行为非常关键。

函数定义

sigismember 函数的定义如下:

#include <signal.h>
int sigismember(const sigset_t *set, int signum);
  • 参数
    • const sigset_t *set:指向之前通过 sigemptysetsigfillset 初始化或通过 sigaddsetsigdelset 修改过的信号集的指针。
    • int signum:要检测其成员资格的信号编号。
返回值
  • 0:该信号不是信号集的成员。
  • 1:该信号是信号集的成员。
  • -1:发生错误,此时 errno 被设置为具体的错误代码(例如,如果 signum 是无效的信号编号,可能会设置 EINVAL)。
示例

下面是使用 sigismember 函数的一个示例,该示例演示了如何检查信号 SIGINT 是否在信号集中:

#include <stdio.h>
#include <signal.h>

int main() {
    sigset_t signalSet;
    sigemptyset(&signalSet);  // 初始化信号集为空
    sigaddset(&signalSet, SIGINT);  // 向信号集中添加 SIGINT

    // 检查 SIGINT 是否在集合中
    if (sigismember(&signalSet, SIGINT)) {
        printf("SIGINT is a member of the set.\n");
    } else {
        printf("SIGINT is not a member of the set.\n");
    }

    return 0;
}
使用场景

sigismember 函数通常在以下情况中使用:

  • 信号掩码管理:在设置或修改进程的信号掩码之前检查某个信号是否已经在信号集中,以决定是否需要添加或删除。
  • 信号处理逻辑:在信号处理函数中判断某个信号是否应被当前处理逻辑处理。
  • 多线程应用:在多线程程序中,为特定线程设置或检查信号掩码时确定某信号的状态。

通过这种方式,sigismember 功能使程序能够更精确地控制和响应信号,提高了程序对信号的管理能力和灵活性。

/*
    以下信号集相关的函数都是对自定义的信号集进行操作。

    int sigemptyset(sigset_t *set);
        - 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigfillset(sigset_t *set);
        - 功能:将信号集中的所有的标志位置为1
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigaddset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigdelset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置不阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigismember(const sigset_t *set, int signum);
        - 功能:判断某个信号是否阻塞
        - 参数:
            - set:需要操作的信号集
            - signum:需要判断的那个信号
        - 返回值:
            1 : signum被阻塞
            0 : signum不阻塞
            -1 : 失败

*/

#include <signal.h>
#include <stdio.h>


int main() {

    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集 set 里
    int ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGINT 阻塞\n");
    }

    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 判断SIGINT是否在信号集中
    ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGINT 阻塞\n");
    }

    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGQUIT 阻塞\n");
    }

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGQUIT 阻塞\n");
    }

    return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out 
SIGINT 不阻塞
SIGINT 阻塞
SIGQUIT 阻塞
SIGQUIT 不阻塞

image-20240507193710572

sigprocmask

在 C 语言中,sigprocmask 函数用于检查和更改进程的信号掩码。信号掩码是指定哪些信号将被阻塞(即暂时不递送给进程)的信号集。sigprocmask 是 Unix 和类 Unix 系统中信号处理的一个重要工具,定义在 <signal.h> 头文件中。通过使用这个函数,可以在运行时动态地更改哪些信号被阻塞,从而控制程序的行为。

函数定义

sigprocmask 函数的原型如下:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 参数:
    • int how: 指定如何修改当前信号掩码。可能的值有:
      • SIG_BLOCK: 将 set 指定的信号添加到当前信号掩码中,即阻塞这些信号。
      • SIG_UNBLOCK: 从当前信号掩码中移除 set 指定的信号,即不再阻塞这些信号。
      • SIG_SETMASK: 将当前信号掩码设置为 set 指定的信号集。
    • const sigset_t *set: 指向信号集的指针,这个信号集指定了要阻塞或解除阻塞的信号。如果为 NULL,则 how 参数没有效果,但如果 oldset 不是 NULL,当前信号掩码仍然可以被存储。
    • sigset_t *oldset: 如果不为 NULL,则当前信号掩码在函数修改前将被存储在 oldset 指定的位置。
返回值
  • 成功时,返回 0
  • 失败时,返回 -1 并设置 errno 以指示错误原因(例如,EINVAL 表示 how 参数无效)。
示例

以下是一个使用 sigprocmask 函数来修改信号掩码的示例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t newSet, oldSet;

    // 初始化新信号集,并添加 SIGINT
    sigemptyset(&newSet);
    sigaddset(&newSet, SIGINT);

    // 阻塞 SIGINT 信号
    if (sigprocmask(SIG_BLOCK, &newSet, &oldSet) < 0) {
        perror("sigprocmask");
        return 1;
    }

    printf("SIGINT is now blocked.\n");

    // 模拟一些工作,延时 5 秒
    sleep(5);

    // 恢复之前的信号掩码
    if (sigprocmask(SIG_SETMASK, &oldSet, NULL) < 0) {
        perror("sigprocmask");
        return 1;
    }

    printf("SIGINT is no longer blocked.\n");

    return 0;
}
使用场景

sigprocmask 函数常用于以下场景:

  • 控制信号的阻塞:在执行关键代码段时,可能需要阻塞某些信号,以防止信号处理函数中断这些操作。
  • 动态信号管理:根据程序的状态更改哪些信号应当被阻塞或接收。
  • 信号掩码的查询:通过传递 NULLset 参数,可以查询当前的信号掩码状态。

这个函数在多线程程序和复杂的信号处理逻辑中尤其有用,它允许程序精确控制在何时应该处理特定的信号。

sigpending

在 Unix 和类 Unix 系统中,sigpending 函数用于检查当前阻塞且未决的信号集,即那些已被发送到进程但由于当前的信号掩码而被阻止的信号。此函数是信号处理功能的一部分,定义在 <signal.h> 头文件中。

函数定义

sigpending 函数的定义如下:

#include <signal.h>
int sigpending(sigset_t *set);
  • 参数:
    • sigset_t *set: 指向 sigset_t 类型的指针,该指针用于输出当前进程的未决信号集。
返回值
  • 成功时,返回 0
  • 失败时,返回 -1 并设置 errno 以指示错误原因。
示例

以下是使用 sigpending 函数来检查和输出当前阻塞但未决的信号的示例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t newSet, pendingSet;

    // 初始化新信号集并添加 SIGINT 和 SIGTERM
    sigemptyset(&newSet);
    sigaddset(&newSet, SIGINT);
    sigaddset(&newSet, SIGTERM);

    // 阻塞 SIGINT 和 SIGTERM 信号
    if (sigprocmask(SIG_BLOCK, &newSet, NULL) < 0) {
        perror("sigprocmask");
        return 1;
    }

    // 发送 SIGINT 信号给自己
    raise(SIGINT);

    // 检查未决的信号
    if (sigpending(&pendingSet) < 0) {
        perror("sigpending");
        return 1;
    }

    // 检查 SIGINT 是否在未决信号集中
    if (sigismember(&pendingSet, SIGINT)) {
        printf("SIGINT is pending.\n");
    }

    // 检查 SIGTERM 是否在未决信号集中
    if (sigismember(&pendingSet, SIGTERM)) {
        printf("SIGTERM is pending.\n");
    } else {
        printf("SIGTERM is not pending.\n");
    }

    return 0;
}

在这个示例中,首先将 SIGINT 和 SIGTERM 添加到新的信号集中并阻塞这些信号。然后通过 raise 函数向进程自己发送 SIGINT 信号。由于 SIGINT 被阻塞,它变成未决状态。随后,使用 sigpending 检查哪些信号是未决的,并通过 sigismember 检查 SIGINT 和 SIGTERM 是否在未决信号集中。

使用场景

sigpending 函数通常在需要了解哪些信号正在等待被处理的情况下使用,特别是在:

  • 复杂的信号处理逻辑中,需要确定当前哪些信号由于阻塞掩码而未被处理。
  • 调试多线程应用程序中的信号处理问题时。
  • 在长时间运行的服务中,程序可能需要定期检查是否有重要信号等待处理。

这个函数帮助开发者更好地理解和控制程序的行为,尤其是在涉及信号阻塞和处理时。

/*
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
        - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
        - 参数:
            - how : 如何对内核阻塞信号集进行处理
                SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
                    假设内核中默认的阻塞信号集是mask, mask | set
                SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
                    mask &= ~set
                SIG_SETMASK:覆盖内核中原来的值
            
            - set :已经初始化好的用户自定义的信号集
            - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
        - 返回值:
            成功:0
            失败:-1
                设置错误号:EFAULT、EINVAL

    int sigpending(sigset_t *set);
        - 功能:获取内核中的未决信号集
        - 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
*/

// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    // 设置2、3号信号阻塞
    sigset_t set;
    sigemptyset(&set);
    // 将2号和3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;

    while(1) {
        num++;
        // 获取当前的未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);

        // 遍历前32位
        for(int i = 1; i <= 31; i++) {
            if(sigismember(&pendingset, i) == 1) {
                printf("1");
            }else if(sigismember(&pendingset, i) == 0) {
                printf("0");
            }else {
                perror("sigismember");
                exit(0);
            }
        }

        printf("\n");
        sleep(1);
        if(num == 10) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }

    }


    return 0;
}

daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out 
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
^\0110000000000000000000000000000
0110000000000000000000000000000

sigaction

在 Unix 和类 Unix 系统中,sigaction 函数用于检查和更改信号的处理方式。这是信号处理的一个高级工具,提供比 signal 函数更详细的控制,允许程序定义如何处理特定的信号,包括设置信号处理函数、指定信号处理过程中哪些信号应被阻塞等。

函数定义

sigaction 函数定义在 <signal.h> 头文件中,其原型如下:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 参数:
    • int signum: 要操作的信号编号,除了 SIGKILLSIGSTOP,因为这两个信号不能被捕获或忽略。
    • const struct sigaction *act: 指向 sigaction 结构的指针,该结构指定了新的信号处理设置。
    • struct sigaction *oldact: 如果不为 NULL,则存储旧的信号处理设置。

struct sigaction 结构定义如下:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int      sa_flags;
    void     (*sa_restorer)(void);
};
  • sa_handler: 指向信号处理函数的指针,或者可以是 SIG_IGN(忽略信号)或 SIG_DFL(恢复默认行为)。
  • sa_sigaction: 一个可选的信号处理函数,用于接收额外的信号信息,只有在 sa_flags 中设置了 SA_SIGINFO 时才使用。
  • sa_mask: 在信号处理函数执行期间需要阻塞的附加信号。
  • sa_flags: 用于修改信号处理的行为,例如 SA_RESTART 使被信号中断的系统调用自动重启。
  • sa_restorer: 这是一个过时的、不再使用的字段,用于在早期的系统实现中恢复信号上下文。
返回值
  • 成功时,返回 0
  • 失败时,返回 -1 并设置 errno 以指示错误原因。
示例

以下是一个使用 sigaction 设置信号处理函数的示例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int signum) {
    printf("Caught signal %d\n", signum);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    printf("Press Ctrl+C\n");
    sleep(10); // Wait for signal

    return 0;
}

在这个例子中,我们设置了 SIGINT 信号的处理函数为 handle_sigint。当用户按下 Ctrl+C(通常发送 SIGINT)时,程序将捕获并处理该信号。

使用场景

sigaction 是信号处理中非常强大的工具,常用于:

  • 定制复杂的信号处理逻辑。
  • 在处理信号时阻塞其他信号。
  • 处理需要从信号处理程序中获取额外信息的高级场景。

这个函数提供了对信号处理的全面控制,使得应用程序能够更稳定和安全地运行,尤其是在多线程环境和需要精细控制信号处理的应用中。

/*
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act,
                            struct sigaction *oldact);

        - 功能:检查或者改变信号的处理。信号捕捉
        - 参数:
            - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
            - act :捕捉到信号之后的处理动作
            - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
        - 返回值:
            成功 0
            失败 -1

     struct sigaction {
        // 函数指针,指向的函数就是信号捕捉到之后的处理函数
        void     (*sa_handler)(int);
        // 不常用
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        sigset_t   sa_mask;
        // 使用哪一个信号处理对捕捉到的信号进行处理
        // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
        int        sa_flags;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };

*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num) {
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() {

    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask);  // 清空临时阻塞信号集
   
    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    if(ret == -1) {
        perror("setitimer");
        exit(0);
    }

    // getchar();
    while(1);

    return 0;
}
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out 
定时器开始了...
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx

内核实现信号捕捉的过程

在 Unix 和类 Unix 系统中,信号是操作系统用来通知进程某些事件已经发生的一种机制。信号可以由操作系统因为外部事件(比如硬件异常、终端用户操作)触发,或者由应用程序(如调用 killraise)显式生成。下面是操作系统内核实现信号捕捉的基本过程:

  1. 信号的生成

信号可以通过多种方式生成:

  • 外部事件:例如,用户按下 Ctrl+C 在终端生成 SIGINT 信号。
  • 程序错误:如除零操作产生 SIGFPE(浮点异常信号)。
  • 显式调用:程序调用 killraise 等函数直接发送信号。
  1. 信号的传递

一旦信号生成,内核将其添加到目标进程的信号队列中。每个进程都有一个信号队列和一个或多个信号掩码(决定哪些信号当前被阻塞)。如果信号未被阻塞,它被标记为“待处理”(pending)。

  1. 信号的捕捉和处理

当进程从内核模式返回到用户模式前,内核检查待处理的信号:

  • 信号掩码检查:内核检查进程的信号掩码,看是否有待处理的信号被阻塞。
  • 信号处理分配:如果信号未被阻塞,内核将查找与该信号关联的处理动作。这些动作可以是默认动作(比如终止进程)、忽略信号或者是一个指向用户定义函数的指针(信号处理函数)。
  1. 用户态信号处理函数的调用

如果为信号指定了用户定义的处理函数(通过如 sigaction 设置):

  • 上下文保存:在调用信号处理函数前,内核保存当前的处理上下文(如CPU寄存器状态),以便信号处理完成后能恢复执行。
  • 设置信号栈:如果指定了信号处理栈(通过 sigaltstack),内核会切换到这个栈。
  • 调用处理函数:内核修改程序计数器(PC),让其指向信号处理函数的入口,然后返回用户空间执行该函数。
  1. 信号处理完毕
  • 处理函数返回:信号处理函数执行完毕后,从处理函数返回。
  • 上下文恢复:内核恢复在信号处理前保存的上下文,进程继续执行信号到达前被中断的代码。
  1. 信号处理的后续动作

根据信号处理的结果,进程可能继续正常执行,或者因为某些信号的默认行为(如 SIGTERM)而终止。

这个过程涉及到复杂的内核机制,包括中断处理、上下文切换和用户空间与内核空间之间的交互。操作系统的信号处理机制是理解进程管理和操作系统响应外部事件能力的重要部分。在多任务和多用户环境中,这种机制尤为重要,因为它允许操作系统控制和协调不同进程的行为。

SIGCHLD信号

SIGCHLD信号产生的条件

 子进程终止时

 子进程接收到 SIGSTOP 信号停止时

 子进程处在停止态,接受到SIGCONT后唤醒时

◼ 以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号

/*
    SIGCHLD信号产生的3个条件:
        1.子进程结束
        2.子进程暂停了
        3.子进程继续运行
        都会给父进程发送该信号,父进程默认忽略该信号。
    
    使用SIGCHLD信号解决僵尸进程的问题。
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>

void myFun(int num) {
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源,这样的话父进程没有办法做事情
    // while(1) {
    //     wait(NULL); 
    // }
    while(1) {
       int ret = waitpid(-1, NULL, WNOHANG);//回收所有子进程,且非阻塞
       if(ret > 0) {
           printf("child die , pid = %d\n", ret);
       } else if(ret == 0) {
           // 说明还有子进程活着
           break;
       } else if(ret == -1) {
           // 没有子进程
           break;
       }
    }
}

int main() {

    // 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 创建一些子进程
    pid_t pid;
    for(int i = 0; i < 20; i++) {
        pid = fork();
        //避免子进程再生成子进程
        if(pid == 0) {
            break;
        }
    }

    if(pid > 0) {
        // 父进程

        // 捕捉子进程死亡时发送的SIGCHLD信号,进行回收
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun; //指定回调函数
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);

        // 注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while(1) {
            printf("parent process pid : %d\n", getpid());
            sleep(2);
        }
    } else if( pid == 0) {
        // 子进程
        printf("child process pid : %d\n", getpid());
    }

    return 0;
}
解决僵尸进程问题
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ gcc sigchld.c 
daic@daic:~/Linux/linuxwebserver/part02multiProgress/lesson26$ ./a.out 
child process pid : 8662
child process pid : 8663
child process pid : 8666
child process pid : 8665
child process pid : 8664
child process pid : 8667
child process pid : 8669
child process pid : 8668
child process pid : 8670
child process pid : 8673
child process pid : 8671
child process pid : 8674
child process pid : 8672
child process pid : 8675
child process pid : 8676
child process pid : 8677
捕捉到的信号 :17
child process pid : 8678
child process pid : 8681
child process pid : 8679
child process pid : 8680
child die , pid = 8662
child die , pid = 8663
child die , pid = 8664
child die , pid = 8665
child die , pid = 8666
child die , pid = 8667
child die , pid = 8668
child die , pid = 8669
child die , pid = 8670
child die , pid = 8671
child die , pid = 8672
child die , pid = 8673
child die , pid = 8674
child die , pid = 8675
child die , pid = 8676
child die , pid = 8677
child die , pid = 8678
child die , pid = 8679
child die , pid = 8680
child die , pid = 8681
捕捉到的信号 :17
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661
parent process pid : 8661

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/600104.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32——基础篇

技术笔记&#xff01; 一、初识STM32 1.1 ARM内核系列 A 系列&#xff1a;Application缩写。高性能应用&#xff0c;比如&#xff1a;手机、电脑、电视等。 R 系列&#xff1a;Real-time缩写。实时性强&#xff0c;汽车电子、军工、无线基带等。 M 系列&#xff1a;Microcont…

资料总结分享:SAM,bam,bed文件格式

目录 sam文件 bam文件 bed 文件 sam文件 SAM&#xff08;Sequence Alignment/Map&#xff09;文件是存储测序数据比对结果的一种常见格式。SAM文件通常用于存储DNA或RNA测序数据在参考基因组上的比对结果。 SAM文件由多行文本组成&#xff0c;每一行代表一个比对结果。SAM文…

nginx 负载均衡、反向代理实验

nginx 负载均衡、反向代理实验 实验目的 理解概念&#xff1a;明确反向代理和负载均衡的基本概念及其在网络架构中的作用。 掌握技能&#xff1a;学习如何配置Nginx以实现反向代理和负载均衡功能。 实践应用&#xff1a;通过实际操作&#xff0c;体验Nginx如何提升Web服务的可…

不上班,我靠这5份赚钱副业养活了自己

在这个快节奏的社会里&#xff0c;很多人都在为生活奔波忙碌。今天&#xff0c;就让我来跟大家分享一下我的“躺平”秘籍吧&#xff01; 这一个月来&#xff0c;我没有上班&#xff0c;但好在有副业养活自己。有时候&#xff0c;我真的觉得有一份自己喜欢的自媒体副业挺好的。…

PE文件(四)FileBuffer-ImageBuffer作业

C语言实现如下功能 2.编写一个函数&#xff0c;将RVA的值转换成FOA 将文件加载到内存时&#xff0c;已知一个数据在内存中的地址&#xff0c;将此地址转化成文件在硬盘上时的相对于文件起始地址的文件偏移地址。即将虚拟内存偏移地址转换成文件偏移地址。 说明&#xff1a;这里…

div组件作为按钮点击时显示小手样式

最近开发的时候发现用div来做按钮比直接使用el-button方便很多&#xff0c;调整样式也方便&#xff0c;但是如果直接使用div做按钮&#xff0c;鼠标放上面时只显示一个竖着的光标&#xff0c;很不美观&#xff0c;并且不会让用户知道可以点击 <template><div class&qu…

栈数据结构

1,概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈 顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则。 压栈(push)&#x…

VS Code中PlatformIO IDE的安装并开发Arduino

VS Code中PlatformIO IDE的安装并开发Arduino VS Code的安装 略 PlatformIO IDE的安装 PlatformIO IDE是是什么 PlatformIO IDE 是一个基于开源的跨平台集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门用于嵌入式系统和物联网&#xff08;IoT&#xff09;开发。…

Mybatis-Plus大批量插入数据到MySQL

MyBatis-Plus的saveBatch方法 GetMapping("/save1") public void save1() {// 数据准备List<MallOrder> orderList getMallOrderList();// mybatis-pluslong start System.currentTimeMillis();mallOrderService.saveBatch(orderList);System.out.println(&…

【Linux】HTTPS

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 &#x1f3e0;个人专栏&#xff1a;Linux 目录 &#x1f449;&#x1f3fb;HTTPS协议概念&#x1f449;&#x1f3fb;加密为什么要进行加密 &#x1f449;&#x1f3fb;常见的加密方式对称加密…

Backblaze发布2024 Q1硬盘故障质量报告-1

作为一家在2021年在美国纳斯达克上市的云端备份公司&#xff0c;Backblaze一直保持着对外定期发布HDD和SSD的故障率稳定性质量报告&#xff0c;给大家提供了一份真实应用场景下的稳定性分析参考数据。 截至2024年第一季度末&#xff0c;Backblaze在其全球数据中心的云存储服务器…

15.计算机网络

1.物理层的互联设备 中继器 和 集线器 2.集线器可以看做特殊的多路中继器 集线器 不可以做到自动寻址的功能 3.数据链路层 网桥 和 交换机 4.交换机是多端口网桥 5.网络层 路由器 6.应用层 网关 7.广播域 网络层 可以形成多个广播域 冲突域 网络层数据链路层 可以形成多个冲突域…

Codeforces Round 942 (Div. 2) A-D1

题目&#xff1a; Codeforces Round 942 (Div. 2) D2有缘再补吧… A. Contest Proposal 题意 两个升序&#xff08;不降&#xff09;的序列a和b&#xff0c;可以在a的任意位置插入任意数&#xff08;要保持升序&#xff09;&#xff0c;使对任意i&#xff0c;有a[i] < b[…

shell脚本编写-测试同一网段内主机是否在线

除了可以使用ansible自动化运维工具判断主机是否在线以外&#xff0c;还可以通过编写Shell脚本来实现。 1、编写脚本 #! /bin/bash #测试192.168.81.0/24网段中哪些主机处于开机状态&#xff0c;哪些主机处于关机状态# #方法一&#xff1a;使用for循环判断 # for i in {1..25…

红海云OA存在任意文件上传漏洞【附poc】

漏洞复现 1、fofa poc见文末 body"RedseaPlatform" 打开burp进行抓包发送到repeater&#xff0c;如下图所示&#xff1a; 打入poc&#xff08;文末获取&#xff09;&#xff0c;成功上传。 「你即将失去如下所有学习变强机会」 学习效率低&#xff0c;学不到实战内…

聊聊 ASP.NET Core 中间件(三):如何创建自己的中间件?

前言 本质上&#xff0c;中间件类也是一个普通的 .NET 类&#xff0c;它不需要继承任何父类或者实现任何接口。 但是有几个约定&#xff1a; 需要有一个构造方法构造方法至少要有一个 RequestDelegate 类型的参数&#xff0c;用来指向下一个中间件。需要定义一个名字为 Invo…

后仿中必须读懂的User-defined primitives(UDP)

一 UDP定义规则 UDP&#xff0c;全名&#xff1a;User-defined primitives。 用户自己定义的原语。 UDP可分为&#xff1a;combinational UDP&#xff08;组合逻辑&#xff09;和 sequential UDP&#xff08;时序逻辑&#xff09;。 1.1 组合逻辑UDP combinational UDP用于…

软件系统测试方案书(测试计划-Word原件)

2 引言 2.1 编写目的 2.3 测试人员 2.4 项目背景 2.5 测试目标 2.6 简写和缩略词 2.7 参考资料 2.8 测试提交文档 2.9 测试进度 3 测试环境 3.1 软硬件环境 4 测试工具 5 测试策略 5.1 测试阶段划分及内容 5.1.1 集成测试 5.1.2 系统测试 5.1.2.1 功能测试 5.…

Autosar NvM配置-手动配置Nvblock及使用-基于ETAS软件

文章目录 前言NvDataInterfaceNvBlockNvM配置SWC配置RTE Mapping使用生成的接口操作NVM总结前言 NVM作为存储协议栈中最顶层的模块,是必须要掌握的。目前项目基本使用MCU带的Dflash模块,使用Fee模拟eeprom。在项目前期阶段,应该充分讨论需要存储的内容,包括应用数据,诊断…

在Ubuntu上安装docker

一、安装docker 更新系统包列表&#xff1a; sudo apt-get update安装必要的依赖软件包&#xff0c;使apt可以通过HTTPS使用repository。 sudo apt-get install apt-transport-https ca-certificates curl software-properties-common添加Docker的阿里云GPG密钥&#xff1a;…