进程间通信之信号(1)

信号(1)

信号的基本概念

  1. 信号是事件发生时对进程的通知机制。或者说是软中断(硬中断的软件模拟),收到信号的时间是无法准确知道。

  2. 信号分为两个类型,通过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
    

    其中1~31号信号是标准信号,剩下的称为实时信号

    实时信号主要作用是弥补标准信号的使用限制的!对比如下:

    1. 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的。而标准信号给用户自定义使用的信号只有两个,SIGUSR1和SIGUSR2

    2. 对实时信号所采用的是队列化管理,比如将某一个实时信号多次发送给一个进程,那么会多次传递信号。反之,如果给一个进程多次发送标准信号(假如此时进程正在处理该信号,后续又来了相同的信号),信号最多会出现2次!

    3. 发送实时信号时,可以为信号指定伴随数据(整数或者指针)

      union sigval {
                     int   sival_int;
                     void *sival_ptr;
                 };
      
    4. 不同的实时信号的传递顺序可以得到保障。顺序如下:

      1. 如果是不同的信号同时处于等待状态,那么会率先传递最小编号的信号
      2. 如果是相同的信号同时处于等待状态,那么传递顺序按照信号发送来的顺序保持一致
    5. 对于开发人员使用实时信号时需要注意的是:不同的系统的实时信号的编号可能不同,所以代码中填写实时信号的编号,不是一个好的做法,最好的做法直接写宏名

  3. 信号因某些事件而产生,信号产生后,会于稍后被传递给某一个进程,而进程也会采取某些措施来相应信号。在产生和到达期间,信号处于等待(pending)状态。

  4. 通常一旦系统将要调度该进程运行,等待的信号会马上送达,或者此时进程正在运行,则会立刻传递该信号!

  5. 进程收到信号后,有如下默认操作(操作为其中之一)(可以通过man 7 signal查看详细英文介绍):

    1. 忽略信号
    2. 终止进程(异常终止,非主动调用exit())
    3. 产生core,同时终止进程(产生的core文件可以使用gdb调试,查看进程终止时的状态)
    4. 停止进程
    5. 恢复停止的进程,继续执行
  6. 当然进程收到信号后,除了可以按照上面五种信号的默认行为处理之外,还可以自定义改变信号到达时的处理行为。程序可以对信号设置的行为如下:

    1. 采用默认行为。适用于撤销之前对信号的行为做的修改、恢复其默认处理场景
    2. 忽略信号。适用于默认行为为终止进程的信号(可以通过man 7 signal查看不同信号的默认行为)。
    3. 执行信号处理程序(自定义的函数)
  7. 一些常见的信号描述

    名称信号值描述默认行为
    SIGINT2终端中断,如ctrl Cterm
    SIGQUIT3终端退出,如ctrl \core
    SIGBUS7发生内存访问错误core
    SIGFPE8算数异常(如除0操作)core
    SIGKILL9"必杀"信号(该信号不可以被捕捉设置)term
    SIGSEGV11【非常常见】对内存的无效引用(如引用的页不存在、
    更新只读内存、用户态访问内核态内存、解引用未初始化的指针等)
    core
    SIGPIPE13写读端关闭的管道时产生的错误term
    SIGALRM14使用alarm或者setitimer设置的定时器到期term
    SIGSTOP19必停信号(该信号不可以被捕捉设置)stop

信号的设置

  1. 有两个函数可以改变信号的设置,分别是signal()和sigaction()函数。

    1. sigaction函数具备signal函数所不具备的功能,但是signal函数使用方式简单
    2. signal函数不同系统的实现方式可能不太相同,所以它的移植性得不到保证,所以要追求可移植性,首选sigaction函数
  2. signal()函数

    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    //signum:需要设置的信号的编号
    //handler:是一个函数指针,指向信号处理函数(注意信号处理函数的返回值void与参数int类型)
    //返回值:成功返回之前的信号处理(函数指针),失败返回SIG_ERR,并置错误码
    //SIG_ERR的定义:#define	SIG_ERR	 ((__sighandler_t) -1)
    

    代码示例:

    #include <stdio.h>
    #include <signal.h>
    
    void newHandler(int sig)
    {
        /* 对信号做处理 */
    }
    
    int main(int argc, char **argv)
    {
        void (*oldHandler)(int);  
    
        //对SIGINT信号进行捕捉并处理,处理行为在newHandler函数中定义
        oldHandler = signal(SIGINT, newHandler);
        if(oldHandler == SIG_ERR){
            printf("error\n");
            return -1;
        }
    
        /* 此处假如对SIGINT信号设置为其它处理方式,那么可以在
         * 处理完成后恢复旧的处理方式 */
    
        //恢复信号的旧的行为
        signal(SIGINT, oldHandler);
        
        //按照信号的默认行为处理
        /* signal(SIGINT, SIG_DFL); */
    
        //忽略该信号
        /* signal(SIGINT, SIG_IGN); */
        return 0;
    }
    
  3. 程序在执行的过程中,如果收到了信号,那么就会打断主程序,内核转而执行信号的处理函数,当信号处理函数执行完成后,主程序会在打断的位置恢复执行。

  4. 代码示例1:捕捉2号信号

    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    
    void sigHandler(int sig)
    {
        printf("sig %d is comming\n", sig);
    }
    
    int main(int argc, char **argv)
    {
    
        if(SIG_ERR == signal(SIGINT, sigHandler)){
            printf("error\n");
            return -1;
        }
    
        //使用pause函数等待signal函数捕捉信号,并执行信号处理函数
        //当信号处理函数执行完成后pause函数才会返回,失败返回-1,并置错误码
        pause();    
    
        return 0;
    }
    

    代码示例2:同时捕捉2、3号信号,只捕捉两次

    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    
    void sigHandler(int sig)
    {
        printf("sig %d is comming\n", sig);
    }
    
    int main(int argc, char **argv)
    {
    
        if(SIG_ERR == signal(SIGINT, sigHandler)){
            printf("error\n");
            return -1;
        }
    
        if(SIG_ERR == signal(SIGQUIT, sigHandler)){
            printf("error\n");
            return -1;
        }
    
        //使用pause函数等待signal函数捕捉信号,并执行信号处理函数
        //当信号处理函数执行完成后pause函数才会返回,失败返回-1,并置错误码
        pause();    
        pause();    
    
        return 0;
    }
    //想要给进程发送2、3号信号,很简单ctrl c发送2号信号、ctrl \发送3号信号,
    //或者使用kill命令kill -2 pid, kill -3 pid; 查看本进程pid也很简单,pidof + 进程名,
    //或者ps -e|grep + 进程名
    

发送信号

  1. 上面的例子中提到主动给某个进程发送信号可以使用kill命令完成。其实还有一个kill()函数可以实现发送信号

    int kill(pid_t pid, int sig);
    //sig:指定要发送给pid的信号编号
    

    kill函数的pid有四种填法:

    1. pid > 0:发送信号给pid指定的进程
    2. pid = 0:发送信号给调用进程同组的每一个进程,包含调用者本身
    3. pid = -1:给系统中除init和本身两个进程外的所有进程发送信号(慎用),该信号也可以称为广播信号
    4. pid < -1:会向组id等于该pid绝对值的进程组内所有下属进程发送信号

    代码示例:

    #include <signal.h>
    #include <stdlib.h>
    
    int main(int argc, char **argv)
    {
        //给进程发送2号信号,进程pid通过传参传入
        kill(atoi(argv[1]), 2);
    
        //给本进程所属的进程组内的每一个进程发送2号信号,
        //可以通过ps -ajx命令查看进程组id
        kill(0, 2);
    
        //向组id = |-128|的进程组所有进程发送2号信号
        kill(-128, 2);
    
        //给系统中除init和本身两个进程外的所有进程发送2号信号
        kill(-1, 2);
    
        return 0;
    }
    
  2. 如果kill函数的sig参数填0,表示发送空信号给指定进程,如果发送失败就证明目标进程不存在!

  3. 使用raise()函数发送信号

    int raise(int sig);
    //raise() returns 0 on success, and nonzero for failure
    

    该函数可以给调用者自身(进程或者线程)发送信号。

    在单线程程序中,调用raise函数类似于调用kill函数如下操作:

    kill(getpid(), sig);
    

    在多线程程序中,调用raise函数等效操作如下:

    pthread_kill(pthread_self(), sig);
    
  4. 使用killpg()函数向某一个进程组所有成员发送信号

    int killpg(int pgrp, int sig);
    

    调用killpg()函数相对于kill函数的如下操作:

    kill(-pgrp, sig)
    

    如果pgrp的值为0,那么会向调用者所属的进程组所有的进程发送信号。

显示信号的描述

每一个信号都有一串与它相关的打印说明,这写描述保存在sys_siglist数组中。除sys_siglist数组外还有两个函数也可以获取信号的描述,分别是:

  1. #include <string.h>
    
    char *strsignal(int sig);
    //sig:需要获取信息的信号
    //成功返回一个char*,指向对该信号描述的字符串,失败返回NULL
    
  2. void psignal(int sig, const char *s);
    //s:用户给定的字符串,后面跟一个冒号,随后是对应sig的描述
    

三种方式代码示例:

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

int main(int argc, char **argv)
{
    //1.使用数组打印的方式(不推荐)
    printf("mode1: %s\n", sys_siglist[2]);

    //2.使用strsignal方式
    char *p = strsignal(2);
    printf("mode2: %s\n", p);
    
    //3.使用psignal的方式
    psignal(2, "mode3");

    return 0;
}
//运行结果
mode1: Interrupt
mode2: Interrupt
mode3: Interrupt

信号第一部分整理完成,未完待续,敬请期待第二部分!
本人能力有限,如有错误望各位大佬不吝指正,原创不易,转载请注明出处!

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值