进程间通信(四)信号

信号的基本概念

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

产生信号的事件

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

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

  • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域

  • 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU时间超限,或者该进程的某个子进程退出。

  • 运行 kill 命令或调用 kill 函数

信号的目的与特点

  使用信号有两个目的:第一,让进程知道已经发生了一个特定的事件;第二,强迫进程执行它自己代码中的信号处理程序。

信号的四个特点

  • 简单:使用简单,但内核实现复杂
  • 不能大量信息:仅能携带信号值,一般不用做进程通信
  • 满足某个特定条件才能发送
  • 优先级较高

各种信号

查看系统定义的信号列表

kill -l

signal
其中,前31个信号为常规信号,其余为实时信号。

编号信号名称对应事件默认动作
1SIGHUP用户退出shell时,由该shell启动的所有进程将收到这个信号终止进程
2SIGINT当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号终止进程
3SIGQUIT用户按下<Ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号终止进程
4SIGILLCPU检测到某进程执行了非法指令终止进程并产生core文件
5SIGTRAP该信号由断点指令或其他 trap指令产生终止进程并产生core文件
6SIGABRT调用abort函数时产生该信号终止进程并产生core文件
7SIGBUS非法访问内存地址,包括内存对齐出错终止进程并产生core文件
8SIGFPE在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误终止进程并产生core文件
9SIGKILL无条件终止进程。该信号不能被忽略,处理和阻塞终止进程,可以杀死任何进程
10SIGUSE1用户定义的信号。即程序员可以在程序中定义并使用该信号终止进程
11SIGSEGV**指示进程进行了无效内存访问(段错误) **终止进程并产生core文件
12SIGUSR2另外一个用户自定义信号,程序员可以在程序中定义并使用该信号终止进程
13SIGPIPEBroken pipe向一个没有读端的管道写数据终止进程
14SIGALRM定时器超时,超时的时间 由系统调用alarm设置终止进程
15SIGTERM程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号终止进程
16SIGSTKFLTLinux早期版本出现的信号,现仍保留向后兼容终止进程
17SIGCHLD子进程结束时,父进程会收到这个信号忽略这个信号
18SIGCONT如果进程已停止,则使其继续运行继续/忽略
19SIGSTOP停止进程的执行。信号不能被忽略,处理和阻塞为终止进程
20SIGTSTP停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号暂停进程
21SIGTTIN后台进程读终端控制台暂停进程
22SIGTTOU该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生暂停进程
23SIGURG套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达忽略该信号
24SIGXCPU进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程终止进程
25SIGXFSZ超过文件的最大长度设置终止进程
26SIGVTALRM虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间终止进程
27SGIPROF类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间终止进程
28SIGWINCH窗口变化大小时发出忽略该信号
29SIGIO此信号向进程指示发出了一个异步IO事件忽略该信号
30SIGPWR关机终止进程
31SIGSYS无效的系统调用终止进程并产生core文件
34~64SIGRTMIN~SIGRTMAXLINUX的实时信号,它们没有固定的含义(可以由用户自定义)终止进程

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

信号的状态及默认处理动作

信号的5中处理动作

  • Trem :终止进程
  • Ign :当前进程忽略掉这个信号
  • Core :默认动作是终止进程,并产生一个 Core文件(核心转储文件)
  • Stop :暂停当前进程
  • Cont :继续执行当前被暂停的进程

  信号有三种状态:产生、未决、递达。与传送快递方式相同,首先产生一个快递订单,状态为产生;然后在传送过程中为 未决 状态;当快递到达时,状态为 递达状态。

信号相关函数

kill(), raise(), abort()

  1. 函数原型
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
  1. 功能:给任何进程或者进程组,发送指定信号。

  2. 参数

  • pid :需要发送的进程id。pid > 0 :将信号发送给指定进程。pid = 0:将信号发送给当前的进程的进程组所有进程。pid = -1:将信号发送给每一个有权限接受这个信号的进程。pid < -1:进程组id 取反。
  • sig :发送信号的编号 / 宏值,0 表示不发送任何信号

  1. 函数原型
#include <signal.h>

int raise(int sig);
  1. 功能:给当前进程发送信号

  2. 参数:要发送的信号

  3. 返回值:成功返回0,失败返回非0值。

  4. 实现:kill(getpid(), sig)


  1. 函数原型
#include <stdlib.h>

void abort(void);
  1. 功能:发送SIGABRT 信号给当前进程,默认杀死当前进程

  2. 实现:kill(getpid(), SIGABRT


kill 示例

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


int main(){
    pid_t pid = fork();
    if (pid == 0){
        for (int i = 0; i < 5; i++){
            printf("child process\n");
            sleep(1);
        }
    }
    else if (pid > 0){
        printf("parent process\n");
        sleep(2);
        
        printf("kill child pricess now\n");
        kill(pid, SIGINT);
    }

    return 0;
}

子进程未执行结束,父进程将其kill
kill

alarm() 定时器

  1. 函数原型
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  1. 功能:设计定时器。函数调用开始倒计时,倒计时为0时,函数会给当前进程发送一个信号:SIGALARM

  2. 参数:

  • seconds :倒计时的市场,单位为秒。如果参数为0,定时器无效。取消一个定时器,使用 alarm(0)
  1. 返回值:之前没有定时器,返回0;之前有定时器,返回之前的定时器剩余的时间
  • SIGALARM :默认终止当前的进程,每一个进程有且只有唯一的一个定时器。
alarm(10);			返回0
// 过1s
alarm(5);			返回9

示例2:alarm 定时器

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


int main(){
    int seconds = alarm(5);
    printf("seconds = %d\n", seconds);      // 第一次调用返回0

    sleep(3);
    seconds = alarm(1);
    printf("seconds = %d\n", seconds);      // 非第一次调用返回上一个倒计时的剩余时间

    while (1){}
    return 0;
}

在第一次调用时,返回值为0,在接下来的调用时,返回值为上一次的倒计时剩余时间。可以看到 alarm 是非阻塞的,在计时结束后,发送 SIGALARM 信号,终止进程。
alarm
  

setitimer() 定时器

  1. 函数原型
#include <sys/time.h>

int setitimer(int which, const struct itimerval *new_value,
                     struct itimerval *old_value);
  1. 功能:设置定时器,可以替代alarm(),精度更高。可以实现周期定时。

  2. 参数

  • which :定时器以什么时间计时。ITIMEER_REAL :现实时间,时间到达后,发送 SIGALRMITIMER_VIRUTAL :用户时间,时间到达后,发送 SIGVTALRMITIMER_PROF 以该进程在用户态和内核态所消耗的时间来计算,时间到达,发送 SIGPROF信号。
  • new_value :设置定时器的属性。
 truct itimerval {
    struct timeval it_interval; // 定时器时间间隔
    struct timeval it_value;    // 定时器执行的延迟时间
};

struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

  • old_value :记录上次的定时的时间参数,一般不使用,指定为 NULL
  1. 返回值
  • 成功返回0
  • 失败返回 -1 ,设置 errorno

示例3:setitimer()


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


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

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

    // 设置延迟时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞
    printf("clock start\n");
    
    if (ret == -1){
        perror("setitimer");
        exit(0);
    }

    getchar();
    return 0;
}

程序在过 3s 后,发送 SIGALARM 程序结束。
setitimer

信号捕捉函数(signal(), sigaction())

  1. 函数原型
#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  1. 函数功能:设置某个信号的捕捉行为。SIGKILLSIGSTOP 不能被捕捉,不能被忽略。

  2. 参数

  • signum :要捕捉的信号,一般使用宏值
  • handler :捕捉到信号的处理函数。SIG_IGN 表示忽略信号;SIG_DFL 表示信号默认的行为;回调函数:该函数由内核调用,该函数用于捕捉到信号后,系统如何去处理该信号。
  1. 返回值
  • 成功返回上一次注册的信号处理函数的地址,第一次调用返回NULL
  • 失败返回SIG_ERR ,设置错误号

回调函数:需要程序员实现,函数类型根据实际需求,看函数指针的定义。在信号产生后,由内核调用。
函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置即可。

示例3:捕捉信号 SIGALRM

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


void myalarm(int num){// 表示捕捉的信号的值
    printf("Get signal value : %d\n", num);
    printf("----------------------\n");
}

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

    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    signal(SIGALRM, myalarm); 

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

    // 设置延迟时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞
    printf("clock start\n");
    
    if (ret == -1){
        perror("setitimer");
        exit(0);
    }

    getchar();
    return 0;
}

首先注册信号捕捉SIGALRM 函数,在延迟时间 3s 后,捕捉信号,程序继续开始间隔2s发送 SIGALRM 信号,通过回调函数捕捉信号。
signal()


  1. 函数原型
#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
  1. 函数功能:检查或者修改的信号的处理。

  2. 参数

  • signum :需要捕捉的信号的编号或者宏值
  • act :捕捉到信号后的处理动作
  • oldact :上一次对信号捕捉相关的设置,一般不使用,传递 NULL
struct sigaction {
	// 函数指针,指向的函数就是信号捕捉到之后的处理函数			
    void     (*sa_handler)(int);
    // 不常用,
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号
    sigset_t   sa_mask;
    // 使用哪一个信号处理函数。0表示使用 `sq_handler` ,`SA_SIGINFO` 表示使用 sa_sigaction
    int        sa_flags;
    void     (*sa_restorer)(void);		// 无用
};

  1. 返回值:成功返回0,失败返回-1,设置 errorno

示例4:sigaction() 信号捕捉函数

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


void myalarm(int num){// 表示捕捉的信号的值
    printf("Get signal value : %d\n", num);
    printf("----------------------\n");
}



// 过3s后,每隔2s定时一次
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;

    // 设置延迟时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞
    printf("clock start\n");
    
    if (ret == -1){
        perror("setitimer");
        exit(0);
    }

    // getchar();
    while (1);
    return 0;
}

sigaction

内核信号捕捉过程
信号捕捉过程

SIGCHLD 信号

  SIGCHLD 信号产生的条件:

  1. 子进程终止时
  2. 子进程接收到 SIGSTOP 信号停止时
  3. 子进程处在停止态时,接收到 SIGCONT 后唤醒。
    以上上中条件都会向父进程发送 sigchld 信号,父进程默认会忽略该信号。

  在子进程执行结束后,父进程未回收子进程资源,使得子进程为僵尸进程。父进程可以通过捕捉 SIGCHLD 信号,释放子进程资源来解决僵尸进程的问题。

示例5:利用 SIGCHLD 回收子进程资源

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

// 回收子进程PCB资源
void myFun(int num){
    printf("get signal: %d\n", num);

    // 第一个子进程死亡后,发送SIGCHLD信号,未决信号集中 17 位设为1,在其他子进程死亡后,也相继发送17号信号,
    // 但信号集只能保存一个
    wait(NULL);         
    
    // 可以释放所有子进程的资源,但是父进程阻塞在这里,无法向下执行
    // while(1){        
    //     wait(NULL);
    // }

    // 父进程不会阻塞,可以释放所有子进程资源,
    // 但可能发生:信号没有注册成功,所有子进程已经结束,会 core dump
    // 解决方案:提前设置好阻塞信号集
    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(){
    // 提前设置好信号阻塞
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, 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){  // 父进程
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);      

        // 捕捉子进程结束时,发送的 SIGCHLD 信号
        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;
}


信号集

未决信号集与阻塞信号集

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

  • 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
  • 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
  • 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

  阻塞信号集与未决信号集在信号处理时如何共同操作信号?

  1. 用户通过键盘 Ctrl + C ,产生2号信号 SIGINT (信号被创建)
  2. 信号产生但是没有被处理(未决)
  • 在内核中将所有没有被处理的信号存储在一个集合中(未决信号集
  • SIGINT 信号被存储在第2个标志位,该标志位的值为0,说明信号不是未决状态;该标志位为1,说明信号集处于未决状态
  1. 未决状态的信号处理之前,需要与另一个信号集(阻塞信号集)进行比较。
  • 阻塞信号集默认不阻塞任何信号
  • 如需阻塞某些信号,可通过系统调用设置阻塞信号
  1. 在处理的时候和阻塞信号集中的标志位进行查询,查看是否对该信号设置阻塞
  • 如果没有阻塞,则处理该信号
  • 如果阻塞了,则喜好继续处于未决状态,直至阻塞接触,处理该信号

自定义信号集相关函数

  1. 函数原型
int sigemptyset(sigset_t *set);		// 将信号集所有标志位设为0
int sigfillset(sigset_t *set);		// 将信号集所有标志位设为1
int sigaddset(sigset_t *set, int signum);	// 将某一个信号对应标志位设为1,阻塞该信号
int sigdelset(sigset_t *set, int signum);	// 将某一个信号对应标志位设为0,不阻塞该信号

// 给出信号集中某个信号是否阻塞,返回1 表示 signum 阻塞,返回 0 表示signum 不阻塞
int sigismember(const sigset_t *set, int signum);	
  1. 功能:清空信号集中的数据,将信号集中的所有标志位置为0

  2. 参数:传出参数,需要操作的信号集

  3. 返回值

  • 成功返回0,失败返回-1,设置errorno

示例6:信号集使用


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

int main(){
    // 创建一个信号集
    sigset_t set;

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

    // 判断 SIGINT 是否在信号集 set 中
    int ret = sigismember(&set, SIGINT);
    if (ret == 1){
        printf("SIGINT block\n");
    }else if (ret == 0){
        printf("SIGINT no block\n");
    }
    else{
        perror("sigismember");
        exit(0);
    }

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

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

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);
    // 判断是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if (ret == 1){
        printf("SIGQUIT block\n");
    }else if (ret == 0){
        printf("SIGQUIT no block\n");
    }
    else{
        perror("sigismember");
        exit(0);
    }

    return 0;
}

  首先对自定义信号集 set 置0,然后将 SIGINTSIGQUIT 设为 1。执行结果如下
sigset


系统信号集操作函数

  1. 函数原型
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  1. 函数功能:查看或者修改阻塞信号集,将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)

  2. 参数

  • how :如何对内核阻塞信号集处理
    SIG_BLOCKmask | set):将用户设置的默认信号集添加到内核中,内核中原来的数据不变。
    SIG_UNBLOCKmask &= ~set):根据用户设置的数据,对内核中的数据进行解除阻塞。
    SIG_SETMASK :覆盖内核中原来的值

  • set :初始化好的用户自定义的信号集

  • oldset :保存设置之前的内核中的阻塞信号集的状态,一般设为 NULL

  1. 返回值
  • 成功返回0
  • 失败返回-1,设置错误号

  1. 函数原型
int sigpending(sigset_t* set);		// 获取内核中的未决信号

示例7:设置并查看内核阻塞信号

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


int main(){
    sigset_t set;
    sigemptyset(&set);

    // 将2 号 3号 信号添加到信号集
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

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

    int n = 0;
    while (1){
       sigset_t pendingset;
       sigemptyset(&pendingset);
       sigpending(&pendingset);

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

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

  首先将2号信号与3号信号设置为阻塞,所以在接受到 Ctrl +cCtrl + \ ,第2个和第三个标志位为 1,信号被阻塞,处于未决状态。循环10次后,解除阻塞,处理SIGINT信号,退出程序。
sigprocmask

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值