【C++高并发服务器WebServer】-6:信号

在这里插入图片描述

信号的概念

信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

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

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程 发送一个中断信号。
  • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
  • 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
  • 运行 kill 命令或调用 kill 函数。

使用信号主要是两个目的,一个是让进程知道已经发生了一个特定的事情,强迫进程执行它自己代码中的信号处理程序。

信号的特点有:简单、不能携带大量信息、满足某个特定条件才能发送、优先级比较高。

查看系统定义的信号列表:kill -l,前31个信号为常规信号,其余为实时信号。

在这里插入图片描述
在这里插入图片描述
下面是所有进程对应的事件表。
在这里插入图片描述
信号的5种默认处理动作有:Term终止进程、Ign当前进程忽略这个信号、Core终止进程 并且生成一个Core文件(保存进程异常退出的错误信息)、Stop暂停当前进程、Cont继续执行当前被暂停的进程。

信号的三种状态:产生、未决、递达。其中比较特殊的是SIGKILL和SIGSTOP,信号不能 被捕捉、阻塞或者忽略,只能执行默认操作。

1.1 core文件

我们 创建一个简单的代码,就是如下的代码。然后试着编译运行,会发现会报错:段错误(核心已转储)

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

int main() {
    char * buf;//
    strcpy(buf, "hello");
    return 0;
}

通过命令ulimit -a可以查看到对应的文件大小,但是core file size这是0,所以我们可以进行对应的修改。
在这里插入图片描述
通过命令ulimit -c 1024我们可以进行修改,修改之后可以再进行对应的查看。

在这里插入图片描述
通过编译上面的代码为core.c然后运行,会发现生成的core文件很大,如下所示:
在这里插入图片描述
通过命令gdb a.out进入到调试界面,然后输入命令core-file core可以查看对应的core文件信息。

看到core中写着:程序通过SIGSEGV(访问了非法内存)信号终止了,后面也有对应的出错代码位置。

在这里插入图片描述

1.2 kill命令

#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); 这个命令等同于raise(9);

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

通过下面的demo可以模拟对应的kill命令的过程。

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

1.3 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) { //设置while循环观察alarm(2)的效果
    }

    return 0;
}

定时器与进程的状态无关(就算进程中有sleep),alarm也会继续,alarm采用的时自然定时法。

进程实际运行的时间 = 内核时间+用户时间+消耗的时间(IO等)

1.4 setitimer调用

首先,struct itimerval是一个结构体,用于定义定时器的参数。它的定义如下:

struct itimerval {
    struct timeval it_interval; // 定时器的间隔时间
    struct timeval it_value;    // 定时器的初始时间
};

其中struct timeval的定义为,也就是这个itimerval是多级嵌套的结构体:

struct timeval {
    time_t tv_sec;  // 秒
    suseconds_t tv_usec; // 微秒
};
#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;    //  微秒    
            };
       
        - 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会立即执行。
    printf("定时器开始了...\n");

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

1.5 signal捕捉信号

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

1.6 信号集

许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。

在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。

信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

进程的内核区有PCB,PCB中有PID、PPID、文件描述符等,同时也有未决信号集、阻塞信号集等这些东西。

在这里插入图片描述

来简述一下未决信号集和阻塞信号集的过程:

第一段:信号的产生与未决状态

用户通过键盘按下 Ctrl + C,会触发系统产生编号为2的信号 SIGINT。然而,信号产生后并未立即被处理,而是处于“未决”状态。在内核中,所有未被处理的信号会被存储在一个特殊的集合中,称为“未决信号集”。对于 SIGINT 信号,其状态被记录在未决信号集的第二个标志位上。如果该标志位的值为 0,则表示信号当前不是未决状态;如果值为 1,则表示信号处于未决状态,等待被处理。

第二段:信号处理与阻塞机制

在处理未决信号之前,需要将其与另一个信号集——“阻塞信号集”进行比较。阻塞信号集默认情况下不阻塞任何信号,但用户可以通过调用系统的API来设置某些信号的阻塞状态。在处理信号时,系统会查询阻塞信号集中对应的标志位,判断该信号是否被设置为阻塞。如果未被阻塞,信号将被正常处理;如果被阻塞,则信号继续保持未决状态,直到阻塞解除后,信号才会被处理。

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

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>
#include <bits/types/sigset_t.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;
}

运行上面的代码,可以看到下面的结果。

在这里插入图片描述

1.7 内核实现信号捕捉的过程

在这里插入图片描述

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

1.9 sigchld

SIGCHLD信号在三种情况下会被发送给父进程:当子进程结束、子进程暂停(例如被信号暂停执行)或子进程从暂停状态恢复运行时。然而,默认情况下,父进程会忽略SIGCHLD信号。通过正确处理SIGCHLD信号,父进程可以在子进程结束时及时调用wait或waitpid函数来收集子进程的状态信息并清理其资源,从而避免子进程变成僵尸进程。这种方法是解决僵尸进程问题的有效手段。

下面的demo是创建多个子进程,并在父进程中处理子进程结束时产生的SIGCHLD信号。为了避免在信号处理函数注册完成之前子进程就已经结束并发送SIGCHLD信号,代码首先将SIGCHLD信号阻塞,直到信号处理函数注册完成后再解除阻塞。父进程会持续运行并打印其进程ID,而子进程会在创建后立即退出。

首先初始化信号集并阻塞SIGCHLD信号:使用sigset_t类型定义一个信号集set,并通过sigemptyset将其初始化为空。
用sigaddset将SIGCHLD信号添加到信号集中。调用sigprocmask,将SIGCHLD信号添加到当前进程的信号屏蔽集中,从而阻塞SIGCHLD信号,防止其在信号处理函数注册完成之前被处理。

使用fork函数创建多个子进程。在for循环中,每次调用fork都会创建一个子进程。子进程在创建后立即退出,而父进程会继续运行。子进程退出时会向父进程发送SIGCHLD信号,但由于信号已经被阻塞,因此不会立即触发信号处理函数。

在父进程中,定义一个struct sigaction结构体act,并设置其sa_handler为自定义的信号处理函数myFun。
使用sigemptyset清空act.sa_mask,确保信号处理函数执行时不会被其他信号中断。
调用sigaction将SIGCHLD信号的处理函数设置为myFun。
调用sigprocmask,将SIGCHLD信号从信号屏蔽集中移除,解除阻塞,允许信号被处理。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
#include <bits/types/sigset_t.h>
#include <bits/sigaction.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);

    // 这是一个函数,用于设置当前进程的信号屏蔽集。
    // 信号屏蔽集决定了哪些信号会被暂时阻塞(即不会被立即处理)。
    // SIG_BLOCK:这是一个常量,表示将信号集中的信号添加到当前进程的信号屏蔽集中,即阻塞这些信号。
    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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值