【并发程序设计】13.信号机制

时间:2024-06-02 19:07:15

13.信号机制

概念

信号机制是Unix、类Unix以及其他POSIX兼容的操作系统中的一种进程间通讯方式,它允许进程在发生特定事件时接收通知

信号机制是操作系统中的一个重要概念,它提供了一种异步的通知机制,用于在进程之间传递消息。信号可以被看作是一种软中断,它们可以在任何时间被发送给一个进程,以通知该进程发生了某个特定的事件。信号的本质是在软件层次上模拟硬件中断的行为,但它完全由软件控制,因此被称为“软中断”。

信号的处理过程通常涉及以下几个步骤:

  1. 信号的产生:当某个事件发生时,如用户按下Ctrl+C,或者程序访问了非法内存地址,操作系统会产生一个信号。
  2. 信号的传递:产生的信号会被操作系统传递给目标进程。这个过程是异步的,意味着信号可以在任何时间点到达。
  3. 信号的接收:进程通过注册信号处理函数来接收和处理信号。当信号到达时,如果进程已经为该信号注册了处理函数,操作系统会调用该函数。
  4. 信号的处理:在信号处理函数中,进程可以决定如何处理信号。常见的处理方式包括忽略信号、采取默认行为(如终止进程)或执行自定义的操作。
  5. 信号的屏蔽:进程可以选择暂时屏蔽某些信号,这样即使在信号产生时也不会被立即处理。这通常用于避免在某些关键操作中被信号中断。

在Linux系统中,信号机制是通过内核实现的。内核负责管理信号的发送和接收,并通过软中断的方式通知进程。进程可以通过系统调用来设置信号处理函数,从而定义对不同信号的响应方式。

常用信号

信号名 代号 含义 默认操作
SIGHUP 1 该信号在用户终端关闭时产生,通常是发给和该 终端关联的会话内的所有进程 终止
SIGINT 2 该信号在用户键入INTR字符(Ctrl-C)时产生,内核发送此信号送到当前终端的所有前台进程 终止
SIGQUIT 3 该信号和SIGINT类似,但由QUIT字符(通常是 Ctrl-)来产生 终止
SIGILL 4 该信号在一个进程企图执行一条非法指令时产生 终止
SIGSEV 5 该信号在非法访问内存时产生,如野指针、缓 冲区溢出 终止
SIGPIPE 13 当进程往一个没有读端的管道中写入时产生,代 表“管道断裂” 终止
SIGKILL 9 该信号用来结束进程,并且不能被捕捉和忽略 终止
SIGSTOP 19 该信号用于暂停进程,并且不能被捕捉和忽略 暂停进程
SIGTSTP 20 该信号用于暂停进程,用户可键入SUSP字符( 通常是Ctrl-Z)发出这个信号 暂停进程
SIGCONT 18 该信号让进程进入运行态 继续运行
SIGALRM 14 该信号用于通知进程定时器时间已到 终止
SIGUSR1/2 10/12 该信号保留给用户程序使用 终止
SIGCHLD 17 是子进程状态改变发给父进程的。 忽略

kill -l 命令查看所有信号

在这里插入图片描述

用到的命令

Kill 命令

  1. 格式kill [参数] [进程号]
  2. 功能:Linux中的kill命令的功能是向指定进程发送信号以终止该进程的运行
  3. 参数
    • -[信号名或代号],例 kill -9 pid 结束进程pid
    • -l(小写L):列出所有可用的信号名称。如果不加信号编号,使用此参数会显示全部信号。
    • -s:指定要发送的信号的名称或编号。这允许用户选择发送不同的信号到进程。
    • -a:当处理当前进程时,不限制命令名和进程号的对应关系。这在批量脚本中尤其有用。
    • -p:指定kill命令只打印相关进程的进程号,而不发送任何信号。
    • -u:指定用户。这个参数用来限定只向特定用户的进程发送信号

killall 命令

  1. 格式killall [参数] [进程名]
  2. 功能结束所有与给定名称匹配的运行中的进程。这可以简化操作,因为用户不需要先查找进程ID(PID),再使用kill命令来终止进程。
  3. 参数
    • -e | --exact:要求进程名与指定名称完全匹配。
    • -I | --ignore-case:在匹配进程名时忽略大小写。
    • -g | --process-group:结束整个进程组而不仅仅是单个进程。
    • -i | --interactive:在结束进程前询问用户确认,实现交互式操作。
    • -l | --list:列出所有已知的信号名称。
    • -q | --quiet:在进程没有结束时不输出任何信息。
    • -s | --signal:发送指定的信号到进程,默认为SIGTERM。
    • -v | --verbose:报告信号是否成功发送到每个匹配的进程。
    • -w | --wait:等待每个被发送信号的进程终止

发送信号的函数

kill 函数

  1. 原型

    #include <signal.h>
    int kill(pid_t pid, int sig)
    
  2. 功能:指定的进程或进程组发送信号

  3. 参数

    • pid:指定要接收信号的进程或进程组
      • pid > 0:信号将被发送到进程ID为pid的进程。
      • pid = 0:信号将被发送到与调用kill()的进程属于同一个进程组的所有进程。
      • pid = -1:信号将被广播发送到系统中所有调用进程有权发送信号的进程,除了进程1(init)。
      • pid < -1:信号将被发送到以-pid为进程组标识的所有进程。
    • int sig:指定要发送的信号的编号。不同的信号编号代表不同的信号,例如SIGTERM代表终止信号,SIGKILL代表强制终止信号。
  4. 返回值

    • 成功,返回0
    • 失败,返回非零数

alarm 函数

  1. 原型

    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
    
  2. 功能:设置一个定时器,在定时器到期时向调用进程发送SIGALRM信号

  3. 参数seconds:指定定时器的数。如果seconds为0,则取消之前设置的定时器,并返回剩余的时间片。如果seconds非零,则设置一个新的定时器,定时器到期时,将向调用alarm函数的进程发送SIGALRM信号。

  4. 返回值

    • 上一个定时器的剩余时间(以秒为单位)
    • 如果没有设置过定时器,则返回值为0
  5. 注意:如果在定时器到期前再次调用alarm函数设置了新的定时器,那么原来的定时器会被取消,新的定时器将从当前时间开始计时。

pause 函数

  1. 原型

    #include <stdlib.h>
    int pause(void);
    
  2. 功能暂停程序执行,直到接收到一个信号

  3. 参数:无

  4. 返回值

    • 如果成功,则返回-1
    • 如果发生错误,则返回-1并设置errno为相应的错误代码

ualarm 函数

  1. 原型

    #include <unistd.h>
    int ualarm(unsigned int seconds, int interval);
    
  2. 功能:设置一个定时器,在定时器到期时向进程发送SIGALRM信号

  3. 参数

    • seconds:指定定时器的秒数。如果seconds为0,则取消之前设置的定时器。
    • interval:指定间隔时间(以jiffies为单位)。如果interval为0或负数,则表示只执行一次定时器。
  4. 返回值

    • 成功,返回0
    • 失败,返回-1
  5. 注意ualarm函数已经被标记为已废弃(deprecated),因为它的行为在不同的系统和平台上可能不一致。在新的代码中,建议使用setitimer函数来代替ualarm函数,以实现更可靠和可移植的定时器功能。

setitimer函数

  1. 原型

    #include <sys/time.h>
    int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
    
  2. 功能:设置一个定时器,该定时器在到期时会向调用进程发送一个SIGALRM信号

  3. 参数

    • which:指定定时器类型,可以是以下三种之一:
      • ITIMER_REAL:基于真实的时间间隔(Real timer),即按照日历时间来计算。
      • ITIMER_VIRTUAL:基于虚拟的时间间隔(Virtual timer),即按照进程在用户态消耗的CPU时间来计算。
      • ITIMER_PROF:基于进程时间(Profiling timer),即按照进程在用户态和内核态消耗的CPU时间来计算。
    • new_value:指向struct itimerval结构的指针,该结构定义了定时器的间隔时间和总时间。如果这个指针为NULL,则定时器被取消。
    • old_value:指向struct itimerval结构的指针,用于存储之前的定时器设置。如果这个指针为NULL,则不保存之前的定时器设置。
  4. 结构体

    • struct itimerval {
          struct timeval it_interval; /* 定时器触发的时间间隔 */
          struct timeval it_value;    /* 第一次触发定时器的时间 */
      };
      
    • struct timeval {
          time_t tv_sec; // 秒数
          suseconds_t tv_usec; // 微秒数
      };
      
  5. 返回值

    • 成功,返回0
    • 失败,返回-1
  6. :可以通过调用gettimeofday(&t_start, NULL)来获取当前时间,并将其存储在timeval结构体变量t_start中。

捕获信号

捕获流程

在这里插入图片描述

signal函数

  1. 原型

    #include <signal.h>
    typedef void (*sighandler_t)(int);` 
    sighandler_t signal(int signum, sighandler_t handler);
    
  2. 功能:设置指定信号的处理函数

  3. 参数

    • signum:指定要处理的信号的编号。
    • handler:指向信号处理函数的指针。如果设置为SIG_IGN,则忽略该信号;如果设置为SIG_DFL,则采用系统默认处理方式。
  4. 返回值:返回先前为指定信号设置的处理函数指针,或者如果之前没有设置处理函数,则返回SIG_DFL

  5. 注:typedef void (*sighandler_t)(int);

    • (*sighandler_t) 表示定义了一个名为 sighandler_t 的函数指针类型。这个函数指针类型指向的函数返回值为 void,即没有返回值,同时接受一个整型参数 int,通常用于传递信号编号
  6. 示例

    捕捉SIGINT信号,打印"I cath the SIGINT \n"

    程序执行后按ctrl +c 打印“I cath the SIGINT \n”后信号功能复原,再按一次ctrl +c,程序退出

    #include <signal.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <linux/posix_types.h>
    
    typedef void (*sighandler_t)(int);
    
    sighandler_t oldact;
    
    void handle(int sig){
        printf("I cath the SIGINT \n");
        signal(SIGINT,oldact);//回复信号原来的功能
    }
    
    int main(){
    
        oldact = signal(SIGINT,handle);//设定信号执行的函数,并将原来的处理信号的函数指针赋值给oldact
        while(1){
            sleep(1);
        }
    
    

sigaction函数

  1. 原型

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

    • sigaction 函数用于设置指定信号 signum 的信号处理函数、信号集和标志。
    • 它允许更细致的控制信号的行为,比 signal 函数提供了更多的选项。
  3. 参数

    • signum:要处理的信号编号。
    • act:指向 sigaction 结构体的指针,该结构体定义了新的信号处理行为。
    • oldact:指向 sigaction 结构体的指针,用于存储原来的信号处理行为。如果不需要保存旧的行为,可以设置为 NULL
  4. 返回值

    • 成功时返回 0
    • 失败时返回 -1
  5. sigaction 结构体

    struct sigaction {
        void (*sa_handler)(int);      // 信号处理函数
        void (*sa_sigaction)(int, siginfo_t *, void *); // 带有额外信息的信号处理函数(POSIX标准)
        sigset_t sa_mask;             // 信号集,指定在处理信号时阻塞哪些信号
        int sa_flags;                 // 影响信号处理行为的标志
    };
    
    • sa_flags 标志
      • SA_NOCLDSTOP:如果子进程被终止,父进程不会成为停止状态。
      • SA_ONSTACK:使用用户栈中的额外空间执行信号处理函数。
      • SA_RESTART:在信号处理完成后,重启被中断的系统调用。
      • SA_RESETHAND:在信号处理结束后,将信号处理函数重置为默认行为。
      • SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数,以提供额外的信号信息。
  6. 注意

    • 不同的操作系统可能对信号的支持不同,因此在使用前应检查操作系统的文档。
    • 在多线程环境中,信号处理可能会被任何线程接收,因此信号处理函数应设计为异步安全的。
    • 当一个信号被捕获时,它的默认行为会被禁止,除非在 sa_handlersa_sigaction 中明确指定。

sigemptyset函数

  1. 原型

    #include <signal.h>
    int sigemptyset(sigset_t *set);
    
  2. 功能:初始化或清空信号集

  3. 参数

    • set:指向要清空的信号集的指针。
  4. 返回值

    • 成功时返回0
    • 失败时返回-1

示例

使用信号回收子进程

  • 信号 17)SIGCHLD 是子进程状态改变发给父进程的。

  • SIGCHLD的产生条件

    1. 子进程终止时

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

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

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

// 信号处理函数
void handle(int sig){
    wait(NULL); // 等待子进程结束
    printf("Get sig =%d\n",sig); // 输出接收到的信号编号
}

int main()
{
    pid_t pid; // 用于存储fork()函数的返回值
    struct sigaction myact; // 用于存储信号处理结构体
    myact.sa_handler = handle; // 设置信号处理函数为handle
    myact.sa_flags = 0; // 设置信号处理标志为0

    sigemptyset(&myact.sa_mask); // 清空信号集

    pid = fork(); // 创建子进程

    // 父进程部分
    if(pid>0)
    {
        //wait(NULL);
        sigaction(SIGCHLD,&myact,NULL); // 设置SIGCHLD信号的处理函数为handle
        while(1)
        {
            printf("this is father process\n"); // 输出信息
            sleep(1); // 休眠1秒
        }
    }

    // 子进程部分
    else if(pid==0)
    {
        sleep(5); // 休眠5秒
        exit(0); // 退出子进程
    }
}

信号阻塞和信号集

有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。这种情况可以通过阻塞信号实现。

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

信号的状态:

  • 信号递达(Delivery ):实际信号执行的处理过程(3种状态:忽略,执行默认动作,捕获)
  • 信号未决(Pending):从产生到递达之间的状态

信号集

信号集是一组用于表示信号的集合,它包含了一组信号的状态,通常用来表示这些信号是否已经发生或被处理

信号集操作函数

  1. sigset_t set; 自定义信号集。 是一个32bit 64bit 128bit的数组。
  2. sigemptyset(sigset_t *set); 清空信号集
  3. sigfillset(sigset_t *set); 全部置1
  4. sigaddset(sigset_t *set, int signum); 将第signum个信号添加到集合中
  5. sigdelset(sigset_t *set, int signum); 将第signum个信号从集合中移除
  6. sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。

信号屏蔽函数

sigprocmask函数
  1. 原型

    #include <signal.h>
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    
  2. 功能:设定对信号集内的信号的处理方式(阻塞或不阻塞)

  3. 参数

    • how:指定函数的行为,可以是以下值之一:

      • SIG_BLOCK:将参数set中的信号加入到进程的信号屏蔽字中。

      • SIG_UNBLOCK:从进程的信号屏蔽字中移除参数set中的信号。

      • SIG_SETMASK:将参数set设置为进程的信号屏蔽字。

      • SIG_MASK:清空信号集,然后加入原来的信号屏蔽字。

      • SIG_BLOCKSET:类似于SIG_BLOCK,但不会改变原来的信号屏蔽字。

      • SIG_UNBLOCKSET:类似于SIG_UNBLOCK,但不会改变原来的信号屏蔽字。

    • set:指向sigset_t类型的指针,包含了要添加到信号屏蔽字中的信号集合。

    • oldset:指向sigset_t类型的指针,用于存放调用前的信号屏蔽字。

  4. 返回值

    • 成功时返回0,
    • 失败时返回-1,并设置errno为错误码。

示例

SIGINT信号的处理和屏蔽

在刚开始运行时的5秒内按下ctrl+c终端没有反应,五秒结束后信号屏蔽解除,终端打印"I get sig=%d\n"

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

void handle(int sig)  // 定义一个信号处理函数,参数为接收到的信号值
{
    printf("I get sig=%d\n", sig);  // 打印接收到的信号值
}

int main()  // 主函数
{
    struct sigaction act;  // 定义一个sigaction结构体变量act
    act.sa_handler = handle;  // 设置信号处理函数为handle
    sigemptyset(&act.sa_mask);  // 初始化清空信号集
    act.sa_flags = 0;  // 设置标志位为0
    sigaction(SIGINT, &act, NULL);  // 将SIGINT信号的处理方式设置为act

    sigset_t set;  // 定义一个信号集变量set
    sigemptyset(&set);  // 初始化清空信号集
    sigaddset(&set, SIGINT);  // 将SIGINT信号添加到信号集中

    sigprocmask(SIG_BLOCK, &set, NULL);  // 阻塞信号集set中的信号(即禁止接收SIGINT信号)
    sleep(5);  // 暂停执行5秒
    sigprocmask(SIG_UNBLOCK, &set, NULL);  // 解除对信号集中的信号的阻塞(即允许接收SIGINT信号)

    while (1) 
    {
        sleep(1);  
    }
}

信号阻塞函数

pause函数
  1. 原型

    #include <unistd.h>
    int pause(void);
    
  2. 功能pause函数使当前进程进入睡眠状态,直到收到一个信号为止。在睡眠期间,进程不会占用CPU资源。

  3. 返回值

    • 如果成功,则返回-1,并将errno设置为EINTR(表示被中断)。
    • 如果失败,则返回-1,并将errno设置为错误码。
sigsuspend函数
  1. 原型

    #include <signal.h>
    int sigsuspend(const sigset_t *mask);
    
  2. 功能

    • sigsuspend函数使当前进程进入睡眠状态,直到收到一个信号为止。在睡眠期间,进程不会占用CPU资源。
    • pause函数不同,sigsuspend函数可以指定要阻塞的信号集,即在等待信号时,哪些信号应该被阻塞。
  3. 参数

    • mask:指向sigset_t类型的指针,表示要阻塞的信号集。如果为NULL,则不阻塞任何信号
  4. 返回值

    • 如果成功,则返回-1,并将errno设置为EINTR(表示被中断)
    • 如果失败,则返回-1,并将errno设置为错误码

示例

展示两个方法阻塞信号

  • 法一:通过sigprocmask和pause实现
  • 法二:通过sigsuspend实现
#include <signal.h>  // 引入信号处理相关的头文件
#include <stdio.h>   // 引入标准输入输出相关的头文件
#include <stdlib.h>  // 引入标准库函数相关的头文件
#include <unistd.h>  // 引入Unix系统调用相关的头文件

void handle(int sig)  // 信号处理函数
{
    printf("I get sig=%d\n", sig);  // 打印接收到的信号值
}

void mytask()  // 任务函数
{
    printf("My task start\n");  // 打印任务开始信息
    sleep(3);  // 暂停3秒
    printf("My task end\n");  // 打印任务结束信息
}

int main()  // 主函数
{
    struct sigaction act;  // 定义一个sigaction结构体变量act
    act.sa_handler = handle;  // 设置信号处理函数为handle
    act.sa_flags = 0;  // 设置标志位为0
    sigemptyset(&act.sa_mask);  // 初始化清空信号集
    sigaction(SIGINT, &act, NULL);  // 将SIGINT信号的处理方式设置为act
    sigaction(SIGHUP, &act, NULL);  // 将SIGHUP信号的处理方式设置为act

    sigset_t set, set2;  // 定义两个信号集变量set和set2
    sigemptyset(&set2);  // 初始化清空set2信号集
    sigaddset(&set, SIGHUP);  // 将SIGHUP信号添加到信号集set中
    sigaddset(&set, SIGINT);  // 将SIGINT信号添加到信号集set中

    pause();  // 暂停执行,等待信号的到来

    while (1)  // 无限循环
    {
        sigprocmask(SIG_BLOCK, &set, NULL);  // 阻塞信号集中的信号(即禁止接收SIGHUP和SIGINT信号)
        mytask();  // 执行自定义的任务函数
        /*法一*/
        //sigprocmask(SIG_UNBLOCK,&set,NULL);  // 解除对信号集中的信号的阻塞(即允许接收SIGHUP和SIGINT信号)
        //pause();//暂停执行,等待信号的到来
        /*法二*/
        sigsuspend(&set2);  // 挂起进程,直到收到信号为止
    }

    printf("After pause\n");  // 打印"After pause"信息

    while (1)  // 无限循环
    {
        sleep(1);  // 每次循环暂停执行1秒
    }
}

具体分析:

  • sigprocmask:这个函数用于修改进程的信号屏蔽字,即它可以阻止某些信号的传递。当设置了信号屏蔽字后,指定的信号将不会传递给进程,直到取消屏蔽。这种方法的缺点是,如果在取消屏蔽和调用pause之间有信号发生,那么这个信号可能会丢失。
  • pause:这个函数会使进程进入睡眠状态,直到收到一个信号。如果在使用sigprocmask解除信号屏蔽之后调用pause,在这两个调用之间的时间窗口内发生的信号可能会导致pause永远挂起,因为pause只有在接收到信号后才会返回。
  • sigsuspend:这个函数结合了sigprocmaskpause的功能,它在解除信号屏蔽的同时使进程进入睡眠状态。这是一个原子操作,意味着它保证了在信号解除屏蔽和等待信号之间不会有时间窗口,从而避免了信号丢失的问题。因此,如果需要等待某个信号,建议使用sigsuspend而不是单独使用sigprocmaskpause

总的来说,sigprocmask主要用于改变信号屏蔽字,pause用于等待任何信号的到来,而sigsuspend则是在等待特定信号时使用的更为安全的方法,因为它可以保证在等待期间不会错过任何信号