精华内容
下载资源
问答
  • sigaction捕捉信号
    2020-07-14 21:22:46

    signal捕捉信号存在一个缺点:
    当捕捉到信号 之后,执行对应的回调函数,但是在执行期间,如果又来了一个信号,就会把当前函数入栈,执行完新的回调函数之后,在继续执行旧信号对应的回调函数。
    但是有时候我们需要不能打断当前回调函数,即当前信号要比其他信号优先级高,需要使用sigaction函数来捕捉信号。
    sigaction(int 捕捉的信号编号,const struct sigaction * handler, struct sigaction * oldhandler传出参数,此信号旧的回调函数);
    sigaction和signal的区别在于signal直接使用回调函数名进行绑定,而sigaction使用sigaction结构体进行绑定。
    struct sigaction {
    void (*sa_handler)(int);回调函数名
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;在执行信号的回调函数时,sigset_t信号集被置1的信号被屏蔽。
    int sa_flags;通常设置为0
    void (*sa_restorer)(void);
    };

    更多相关内容
  • 详细介绍了Linux下的信号以及怎么捕捉信号,本文首先介绍了信号的基本概念和处理过程,接着介绍了信号捕捉的步骤与捕捉信号实例,有需要的小伙伴们可以参考学习。下面跟着小编一起来看看。
  • Linux下捕捉信号

    2021-05-17 11:51:27
    信号由三种处理方式:忽略执行该信号的默认处理动作捕捉信号如果信号的处理动作是用户自定义函数,在信号递达时就调用这个自定义函数,这称为捕捉信号。进程收到一个信号后不会被立即处理,而是在恰当时机进行处理!...

    信号由三种处理方式:

    忽略

    执行该信号的默认处理动作

    捕捉信号

    如果信号的处理动作是用户自定义函数,在信号递达时就调用这个自定义函数,这称为捕捉信号。

    进程收到一个信号后不会被立即处理,而是在恰当时机进行处理!即内核态返回用户态之前 !

    但是由于信号处理函数的代码在用户空间,所以这增加了内核处理信号捕捉的复杂度。

    内核实现信号捕捉的步骤:

    用户为某信号注册一个信号处理函数sighandler。

    当前正在执行主程序,这时候因为中断、异常或系统调用进入内核态。

    在处理完异常要返回用户态的主程序之前,检查到有信号未处理,并发现该信号需要按照用户自定义的函数来处理。

    内核决定返回用户态执行sighandler函数,而不是恢复main函数的上下文继续执行!(sighandler和main函数使用的是不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程)

    sighandler函数返回后,执行特殊的系统调用sigreturn从用户态回到内核态

    检查是否还有其它信号需要递达,如果没有 则返回用户态并恢复主程序的上下文信息继续执行。

    ce7594e5c6aa9b9fc7e7363ff75ee9fa.png

    signal

    给某一个进程的某一个信号(标号为signum)注册一个相应的处理函数,即对该信号的默认处理动作进行修改,修改为handler函数指向的方式;

    signal函数接受两个参数:一个整型的信号编号,以及一个指向用户定义的信号处理函数的指针。

    此外,signal函数的返回值是一个指向调用用户定义信号处理函数的指针。

    sigaction

    sigaction函数可以读取和修改与指定信号相关联的处理动作。

    signum是指定信号的编号。

    处理方式:

    若act指针非空,则根据act结构体中的信号处理函数来修改该信号的处理动作。

    若oact指针非 空,则通过oact传出该信号原来的处理动作。

    现将原来的处理动作备份到oact里,然后根据act修改该信号的处理动作。

    (注:后两个参数都是输入输出型参数!)

    将sa_handler三种可选方式:

    赋值为常数SIG_IGN传给sigaction表示忽略信号;

    赋值为常数SIG_DFL表示执行系统默认动作;

    赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。

    (注:这是一个回调函数,不是被main函数调用,而是被系统所调用)

    当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。

    pause

    pause函数使调用进程挂起直到有信号递达!

    处理方式:

    如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;

    如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;

    如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR。

    所以pause只有出错的返回值(类似exec函数家族)。错误码EINTR表示“被信号中断”。

    举个栗子

    定义一个闹钟,约定times秒后,内核向该进程发送一个SIGALRM信号;

    调用pause函数将进程挂起,内核切换到别的进程运行;

    times秒后,内核向该进程发送SIGALRM信号,发现其处理动作是一个自定义函数,于是切回用户态执行该自定义处理函数;

    进入sig_alrm函数时SIGALRM信号被自动屏蔽,从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行特殊的系统调用sigreturn再次进入内核,之后再返回用户态继续执行进程的主控制流程(main函数调用的mytest函数)。

    pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理 动作。

    1798b16bbd8522afba6a879489063c4b.png

    定义一个闹钟并挂起等待,收到信号后执行自定义处理动作,在没有恢复默认处理动作前,收到SIGALRM信号都会按照其自定义处理函数来处理。恢复自定义处理动作之后收到SIGALRM信号则执行其默认处理动作即终止进程!

    0b1331709591d260c1c78e86d0c51c18.png

    展开全文
  • 信号的种类、捕捉信号->signal函数、sigaction函数 信号产生的方式、保存信号、信号在内核中的结构、信号集操作函数、处理信号、处理信号的时机、用户态与内核态、用户态与内核态的转化、处理信号的过程


    在这里插入图片描述
    思维导图获取地址
    提取码: 72tf


    1. 信号的理解

     信号在我们的生活中十分常见,红绿灯🚦会给我发送信号,我们在识别到红绿灯的信号后也会做出相应的处理动作,前进或者停住。信号在程序中其实也十分常见,程序的异常终止往往都是因为接收到了信号。我们将信号的生命周期分为3个阶段:信号产生之前、信号产生时、信号处理方式。

    1.1 信号产生之前

     信号在还没有产生时,对于进程来说,进程也一定知道在接收到信号之后该怎么做。
    //进程内部一定能够识别“信号”,程序员在设计进程时,已经内置了处理方案。
     其实信号与进程的关系就是一种“异步关系”!
    · 异步关系
    进程在等待某件事的过程中,继续做自己的事情,不需要等待这一件事完成后再工作。
    (与同步关系相反,同步关系是:在等待某件事的过程中,停下自己正在做的事情,直到等待的事情做完,再继续做自己做的事情)

    1.2 信号产生时

     进程可能因为正在处理更重要的事情,而不会立即处理信号,它会等合适的时候去处理。
    //这里的合适的时候我会在后文解释。由于不会立即处理信号,此时则需要将信号保存记录下来

    1.3 处理信号

    处理信号的方式有3个
     <1> 采用默认行为
     <2> 自定义行为
     <3> 忽略信号(非常规,但也算是处理了信号)

    问题:为什么要有信号?
     为了让进程具有解决突发事件的能力。(比如程序遇到野指针、数组越界、除零问题)

    2. 信号的种类

    在这里插入图片描述
     Linux中一共有62个信号。
    从时间上分为:非实时信号和实时信号
    从可靠性上分为:不可靠信号和可靠信号
      [1, 31] 这31个信号是“非实时信号”,也是“不可靠信号”
      [34, 64] 这31个信号是“实时信号”,也是“可靠信号”

    //“可靠信号”与“不可靠信号”的区别在于:可靠信号克服了信号可能丢失的问题,并且它支持排队。不可靠信号就是不支持排队,所以可能会丢失。

    这里介绍几种常用的信号:

    (1) SIGHUP
     本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    2) SIGINT
     程序终止(interrupt)信号,可以通过输入Ctrl + c发送该信号,用于通知前台进程组终止进程。//注意!无法终止后台进程
    3) SIGQUIT
     进程退出信号,可以通过输入Ctrl + \ 来发送该信号。
    4) SIGILL
     执行了非法指令。通常是因为可执行文件本身出现错误,或者试图执行数据段。 堆栈溢出时也有可能产生这个信号。
    6) SIGABRT
     调用abort函数生成的信号。
    8) SIGFPE
     在发生致命的算术运算错误时发出。 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
    9) SIGKILL
    本信号不能被捕捉、阻塞和忽略,用来杀死任意一个运行中的进程
    11) SIGSEGV
     程序发生段错误时,就是接收到了这个信号。试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
    13) SIGPIPE
     管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
    14) SIGALRM
     时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
    15) SIGTERM
     程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
    17) SIGCHLD
     子进程结束时, 父进程会收到这个信号。
    18) SIGCONT
     让一个停止(stopped)的进程继续执行。 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作。 例如, 重新显示提示符
    19) SIGSTOP
    停止(stopped)进程的执行。本信号不能被捕捉, 阻塞和忽略。注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行.
    20) SIGTSTP
     停止进程的运行,但该信号可以被处理和忽略。 可以通过输入Ctrl-Z来发送这个信号。


    3. 捕捉信号

    3.1 signal函数

    	typedef void (*sighandler_t) (int);
    	//sighandler_t是个函数指针类型
    
    	sighandler_t signal(int signum, sighandler_t handler);
    

     signal函数是用来“注册”信号的,将signum对应的信号的处理操作改为handler函数。这个过程就是注册!也有种说法叫做“捕获信号”,只有传过来的信号满足你signal函数传入的信号时,才会调用你signal函数中注册该信号时所写的函数方法。
    //也就是说,如果传入该进程的信号不是signal注册的,就会正常执行这个信号本来的功能。
    参数:
     signum:代表传入的信号的值(几号信号)
     handler:handler是个函数指针,它的函数体内实现了signum对应的处理信号的操作。
    作用:
     在使用signal函数去注册完某一个信号后,以后遇到该信号就会去调用signal函数中的handler方法了。但是有一些信号是不能被注册/捕获的。比如:9号信号,9号信号的作用就是去杀死进程,所以它是不能被捕获的。 #SIGKILL、SIGSTOP不能被捕捉、阻塞和忽略!
    演示

     #include <iostream>                                                                               
     #include <unistd.h>    
     #include <signal.h>    
     using namespace std;    
     //信号处理函数
     void handler(int signal)    
     {    
       cout << "signal: " << signal << endl;    
     }    
         
     int main()    
     {    
       signal(2, handler);    //捕捉2号信号,通过Ctrl + c可以发送2号信号SIGINT
       while(1);    //死循环等待信号的发送
       return 0;    
     } 
    

    在这里插入图片描述

    3.2 sigaction函数

    	int sigaction(int signo, const struct sigaction* act, struct sigaction* oact);
    

    参数:
     signo:和signal函数的signum一样,都是表示要捕获的信号。
     act:类型是个struct sigaction。
    在这里插入图片描述
    sa_handler就是信号捕捉后的处理方法。
    sa_mask:它是一个屏蔽信号集(我会在下文讲),在调用该信号捕捉函数之前,这一信号集要加进进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值。
    sa_flags:一般设置为0。下面是sa_flags的一些标志位设置。

    SA_INTERRUPT 由此信号中断的系统调用不会自动重启
    SA_RESTART 由此信号中断的系统调用会自动重启
    SA_SIGINFO 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针

    sa_sigaction:实时信号的参数。
    sa_restorer:这个参数是个已经废弃的数据域,不要使用!
     oact:输出型参数,用于保存原来的信号处理动作。
    //sigaction和signal功能基本一样,就是参数多一些。能用signal的地方就用signal就好了。
    演示

    #include <iostream>    
    #include <unistd.h>    
    #include <signal.h>    
    using namespace std;    
        
    void handler(int signal)    
    {    
      cout << "signal: " << signal << endl;    
    }    
        
    int main()    
    {                                                                              
      struct sigaction newact;    
      newact.sa_handler = handler;    
      newact.sa_flags = 0;    
        
      sigaction(2, &newact, NULL);    
      while(1);    
      return 0;    
    } 
    

    在这里插入图片描述

    4. 信号产生的方式

    ①:键盘 (Ctrl + c、Ctrl + z、Ctrl + \)
    ②:系统调用 (kill、raise、abort)
    ③:程序异常 (最终会表现在硬件层面上)
    ④:软件条件 (SIGPIPE、SIGALRM)
    //虽然产生信号的方式有很多种,但是最终它们都会经由OS去发送信号

    ①:键盘
    键盘上的有很多组合键能够发出对应的信号
    Ctrl + C --> 2号信号 —> SIGINT
    Ctrl + \ --> 3号信号 —> SIGQUIT
    Ctrl + Z --> 20号信号—> SIGSTP

    ②:系统调用
    <1>:kill函数

    	int kill(pid_t pid, int sig);
    

    给pid号进程发送sig信号。
    参数:pid和sig往往通过命令行参数的形式传入程序中
    在这里插入图片描述
    在这里插入图片描述
    <2>:raise函数

    int raise(int sig);      <==>    kill(getpid(), sig);    //这两者等价
    

    给自己(当前进程)发送sig信号。
    <3>:abort函数

    void abort(void);
    

    给自己发送6号信号SIGABRT。作用是让进程终止(Terminal)。
    //abort函数总是能够成功的,所以它没有返回值!

    ③:程序异常
     程序异常最终都会表现在硬件层面。OS在检测到硬件异常后,会找到发生异常的进程,然后给该进程发送信号。例如:当前进程执行了除零的指令,CPU的计算单元会产生异常,OS将这个异常解释为SIGFPE信号发送给出现错误的进程;再比如当前进程非法访问内存地址,此时MMU会产生异常,OS将这个异常解释为SIGSEGV信号发送给该进程。

    ④:软件条件
     由于当前“软件条件”不成熟,而产生了信号。
    例如:SIGPIPE就是因为读端被关闭,所以OS给写端发送了SIGPIPE信号,将其杀死。还有SIGALRM
    14) SIGALRM

    	unsigned alarm(unsigned seconds);    //seconds中写的是秒数
    

    几秒后会发送一个alarm信号给自己,该信号的默认处理动作是终止当前进程。
     如果没到秒数之前,进程就接收到了alarm信号,那么alarm函数的返回值就是剩余的秒数。如果正常运行完了,那么返回值就是0.

    5. 保存信号

     我们在前面提到过,非实时信号是从1号~31号。这里面一共有31个信号,面对这个数字是不是有些想法了?没错,信号就是利用比特位去存储的,也就是使用到了位图!(位图就是一种借助比特位去存储数据的数据结构,它的主要功能就是判断某个位置对应的值是否存在,因为每个位置只有0和1这两种状态)
    问题:如何理解一个进程接收到了信号?
     一个进程的task_struct中有指向信号位图的指针,信号位图是与进程相关联的。一个进程接收信号的本质其实就是将该进程task_struct所对应的信号位图的对应位置由数字0改为数字1,这样就表示该进程接收到了某个信号。

    在理解信号详细的存储结构之前,我们先了解一下下面一组概念:
    信号递达:实际执行信号的处理动作,被称为信号递达(Delivery)
    信号未决:信号从产生到递达之间的状态,被称为信号未决(Pending)
    信号阻塞:信号被阻塞了(blocking)

    5.1 信号在内核中的结构

    在这里插入图片描述
    注意:
     对于非实时信号[1, 31],由于信号位图只能表示某个信号到底存不存在 (非0即1),而不能统计次数。所以当一个信号在递达之前产生了多次,那么只计一次!
     对于实时信号[34, 64],它们在递达之前产生多次的话,全都会记录下来,依次放在一个队列中。

    5.2 信号集操作函数

    · 信号集
     sigset_t就是信号集,它是一种类型,也就是说它可以像int一样去定义变量。
     从上面的信号的结构图来看,对于阻塞block和未决pending位图只有在含义上有所不同,因此,我们可以使用sigset_t类型去存储它们的信息,去表示某个信号是“有效”还是“无效”。
     对于阻塞block位图来说,sigset_t可以表示某个信号“被阻塞”还是“没被阻塞”。
     对于未决pending位图来说,sigset_t可以表示某个信号“处于未决状态”还是“不处于未决状态”。
    //sigset_t被称为信号集,从而衍生出了阻塞信号集和未决信号集。阻塞信号集也可以被称为信号屏蔽字。

    ※注意※:
     sigset_t可以理解为无符号整形。但是不要直接对sigset_t类型的数据直接使用按位运算!
     因为不同的平台中OS对sigset_t的实现可能不同。我们要使用信号集操作函数去控制sigset_t类型的数据。

    · 信号集操作函数

    //包含于<signal.h>
    int sigemptyset(sigset_t* set);
    int sigfillset(sigset_t* set);
    int sigaddset(sigset_t* set, int signo);          //向信号集set中添加signo信号
    int sigdelset(sigset_t* set, int signo);          //删除set信号集中的signo信号
    int sigismember(const sigset_t* set, int signo);  //判断set信号集中的signo信号是否有效
    
    • 在使用sigset_t的数据前,要先用sigemptyset去初始化set所指向的信号集,使其中所有的信号对应的bit位全部清零,表示该信号集不包含任何 有效信号。
    • sigfillset函数,初始化set所指向的信号集,使其中的所有信号的对应bit位置变为1,表示该信号集的有效信号包括系统支持的所有信号。
    • 在使用sigset_t类型的变量之前,一定要确保调用过sigemptyset或sigfillset做初始化!
      注意(细节):

     信号集操作函数,实际上操作的只是你在程序中定义的sigset类型的变量,并没有对进程中的阻塞标志位图或未决位图进行修改。我们接下来还需要使用系统调用接口将信号集变量set设置进 进程中!

    · 阻塞信号集的操作

    int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
    

    返回值:
     成功返回0;失败返回-1
    参数:
     how:表示如何更改信号屏蔽字(又叫阻塞信号集),how有如下3种可选值:
    SIG_BLOCK: set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask | set
    SIG_UNBLOCK: set包含了我们希望从信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set
    SIG_SETMASK: 设置当前信号屏蔽字的值为set所指向的值,相当于mask = set #就是赋值
     set:指向现在的信号集,如果只想读取现在的信号集,可以设置为NULL。
     oset:oset是个输出型参数,不想使用可以设置为NULL。它用于获取你想要更改的set,也就是我们能通过这个参数备份要修改的set值,如果下次想要复原,可以使用保存的这个oset值。

    · 获取未决信号集

    	int sigpending(sigset_t* set);
    

     set也是个输出型参数,用于读取当前的未决信号集。
     未决信号集是通过发送信号进而改变里面的值的,所以我们只能去获取未决信号集。

    代码演示

    //代码的功能:
    //先给把2号信号加入到当前进程的信号屏蔽字。
    //然后创建出一个子进程,让子进程在5s后解除对于2号信号的屏蔽
    //与此同时,父进程在反复给子进程发送2号信号(并且进行非阻塞式的进程等待)
    #include <iostream>                                                                                                                                                                                            
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <signal.h>
    using namespace std;
    
    int main()
    {
      sigset_t set;
      sigemptyset(&set);
      sigaddset(&set, 2); //将2号信号加入屏蔽集
      sigprocmask(SIG_BLOCK, &set, NULL); //给当前进程设置信号集
    
      pid_t id = fork();
      if(id == 0)
      {
        int cnt = 0;
        while(1)
        {
          ++cnt;
          if(cnt == 5)
          {
            //5s后解除子进程对2号信号的屏蔽
            //sigdelset(&set, 2);//这里可以不用删除,直接在sigpromask中使用SIG_UNBLOCK
            sigprocmask(SIG_UNBLOCK, &set, NULL);
          }
    
          cout << "child# " << endl;
          sleep(1);
        }
      }
      else 
      {
        int status = 0;
        while(1)
        {
          cout << "Father sent signal to child" << endl;
          kill(id, 2);  //给子进程发送2号信号
          sleep(1);
          
          waitpid(id, &status, WNOHANG);	//非阻塞式的等待子进程
          if(status)
          {
            cout << "Child signal: " << (status & 0x7F) << endl;  //打印子进程接收到的信号
            break;
          }
        }
      }
      return 0;
    } 
    

    在这里插入图片描述


    6. 处理信号

    信号的处理方式一共有3种:

    1. SIG_DFL  //采用默认行为
    2. SIG_IGN  //采用忽略处理
    3. 自定义方法handler

    在使用signal/sigaction函数中,可以向handler参数的位置传这三种参数。

    6.1 处理信号的时机

    我们前面提到了,处理信号是要等到“合适”的时候,那么"合适的时候"是什么时候?
    由“内核态”切换到 “用户态”的时候,进行信号的处理。

    那么什么是内核态,什么是用户态呢?

    6.1.1 用户态与内核态

    内核态(Kernal Mode):通常用来执行操作系统的代码,是一种权限非常高的状态
    用户态(User Mode) :用来执行普通用户的代码,是一种受监管的普通状态

    · 特权级
    Intel x86架构的CPU一共有0~4 四个特权级,其中0级最高,3级最低。对于UNIX/LINUX来说,只使用了0级特权级和3级特权级。
    当程序运行在3级特权级的时候,就是处于用户态
    当程序运行在0级特权级上,就是处于内核态

    6.1.2 用户态与内核态的转化

    “用户态”—> “内核态”
    a. 进行系统调用
    b. 时间片到了导致进程切换
    c. 程序异常
    d. 外围设备的中断

    “内核态” —> “用户态”
    a. 系统调用返回
    b. 进程切换完毕
    c. 处理完异常、中断等问题

    6.2 处理信号的过程

    在这里插入图片描述
    面对这张图,我们可能不太容易理解从③到④这个操作,也就是为什么信号的处理为自定义行为的时候必须要切换回用户态呢?内核态的权限比用户态高,在用户态能执行的代码内核态也一定可以,为什么还要特地切回用户态呢?
     理论上是内核态可以去访问用户态的代码,但是在信号的处理中,绝对不能直接去访问用户态的代码!
     因为如果用户态的代码涉及到一些非法越权操作,在内核态时就会执行这种非法操作,这样就出问题了!所以必须切换到用户态去执行用户态的代码,这样出现越权操作就会报错,而不会去执行这种非法操作了。

    展开全文
  • 捕捉信号一. 阻塞信号1. 信号的常见其他概念 实际执行信号的处理动作(3种)称为信号递达; 信号从产生到递达之间的状态,叫做信号未决; 进程可以选择阻塞某个信号; 被阻塞的信号产生时,将保持在未决状态,...

    阻塞信号&捕捉信号


    一. 阻塞信号

    1. 信号的常见其他概念

        实际执行信号的处理动作(3种)称为信号递达;
        信号从产生到递达之间的状态,叫做信号未决;
        进程可以选择阻塞某个信号;
        被阻塞的信号产生时,将保持在未决状态,直至进程取消对该信号的阻塞,才执行递达的动作;
    注意:阻塞和忽略是不同的。只要信号阻塞就不会被递达;而忽略是信号在递达之后的一种处理方式。

    2. 在内核中的表示
        信号在内核中的表示示意图:

        每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针(handler)表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直至信号递达才清除该标志。 操作系统向进程发送信号就是将pending位图中的该信号对应状态位由0变为1。

        如上图,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作;SIGINT信号产生过,但是正在被阻塞,所以暂时递达不了。虽然它的处理动作是忽略,但是未解除阻塞时不能忽略该信号,因为经常仍有机会改变处理动作后再解除阻塞;SIGQUIT信号未产生过,但是它是阻塞的,所以一旦该信号产生它就被阻塞无法递达,它的处理动作也是用户自定义函数。
        在Linux下,如果进程解除某信号的阻塞之前,该信号产生了很多次,它的处理方法是:若是常规信号,在递达之前多次产生只计一次;若是实时信号,在递达之前产生多次则可以放在一个队列里。 本文只讨论常规信号,下面提到的信号都是常规信号。


    3. 信号集
            
            从上图我们可以知道,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次。同样的,阻塞标志也是这样表示的。所以阻塞和未决标志我们可以采用相同的数据类型sigset_t来存储,sigget_t称为 信号集

            这个类型可以表示每个信号的“有效”、“无效”状态。在未决信号集中,“有效”、 “无效”表示该信号是否处于未决状态; 在阻塞信号集中,“有效”、 “无效” 表示该信号是否被阻塞。阻塞信号集也叫做当前进程的信号屏蔽字

    4. 信号集操作函数

            sigget_t类型对每一种信号用一个bit表示“有效”或者“无效”状态,至于这个类型内部是怎样储存这些bit则依赖系统实现,从使用者的角度是不用关心的。使用者只用调用以下函数来对sigget_t变量进行操作,而不用对它的内部数据进行任何解释。

        其中,前四个函数都是成功返回0,出错返回-1;最后一个sigismember函数包含返回1,不包含返回0,出错返回-1。

    注意: 在使用sigget_t类型的变量之前,一定要调用sigemptyset函数或者sigfillset函数做初始化,使信号集处于确定的状态。

    5. sigprocmask

    调用 sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。
    (1)函数原型:

    (2)参数:
            how:有三个可取值(如下图,假设当前信号屏蔽字为mask)
        set:指向一个信号集的指针
        oldset:用于备份原来的信号屏蔽字,不想备份时可设置为NULL

    1)若set为非空指针,则根据参数how更改进程的信号屏蔽字;
    2)若oldset是非空指针,则读取进程当前的信号屏蔽字通过oldset参数传出;
    3)若set、oldset都非空,则将原先的信号屏蔽字备份到oldset中,然后根据set和how参数更改信号屏蔽字

    (3)返回值:成功返回0,出错返回-1

    说明:若调用sigprocmask解除了若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。


    6. sigpending

    (1)函数原型:

    (2)函数功能:读取当前进程的未决信号集,通过set参数传出
    (3)参数:输出型参数,传出数据
    (4)返回值:成功返回0,出错返回-1


    7.利用以上所介绍函数编写代码使用:
    #include <stdio.h>                                                                        
    #include <signal.h>
    #include <unistd.h>
    
    void printsigset(sigset_t* set)//打印pending表
    {
        int i = 0;
        for(i=1; i<=32; ++i)
        {   
            if(sigismember(set,i))//当前信号在信号集中
                putchar('1');
            else//当前信号不在信号集中
                putchar('0');
        }   
        puts("");//printf("\n");
    }
    
    int main()
    {
        sigset_t s,p;
        sigemptyset(&s);//初始化信号集
        sigaddset(&s,2);//将2号信号设置为有效信号
        sigprocmask(SIG_BLOCK, &s, NULL);//屏蔽2号信号
        int i = 10;
        while(i--)
        {
            sigpending(&p);//获取当前进程的未决信号集
            printsigset(&p);//打印未决信号集
            if(i==7)
            {
                raise(2);//向本进程发送2号信号
            }
            if(i == 5)
            {
                sigprocmask(SIG_UNBLOCK, &s, NULL);//解除对2号信号的屏蔽
                printf("recober block bitmap\n");
            }
            sleep(1);
        }
        return 0;
    }                       

    运行结果为:
            可以看到,开始运行时未决信号集中没有有效信号。过3秒后我们向进程发送了一个2号信号,此时未决信号集对应的第二个bit变为1,说明进程有2号信号。到第5秒时,我们解除了对2号信号的屏蔽,此时第三秒产生的2号信号被递达,它的默认处理动作是终止进程,所以此时进程直接终止。

    二. 捕捉信号


    1. 内核如何实现对信号的捕捉
            在前面的介绍中我们可以知道,当信号的处理动作是自定义函数的时候,在信号递达时就调用这个函数,这称为捕捉信号。内核对信号的捕捉详细过程见下图(进行了四次用户态与内核态的切换):
    说明:信号处理函数与main函数使用不同的堆栈空间,它们之间不存在调用与被调用的关系,是两个独立的控制流程。


    2. signal函数
    (1)函数原型
    (2)函数功能: 修改signum信号的处理动作为handler指向的函数
    (3)参数:

            signum:表示要捕捉的信号序号
            handler:是一个回调函数,若信号signum产生并且没有被阻塞时,当该信号被递达时,就去执行该回调函数,该回调函数有一个整型参数,表示对哪个信号进行处理。
    (4)代码实现:
    #include <stdio.h>                                                              
    #include <signal.h>
    #include <unistd.h>
    
    void handler(int num)//捕捉信号
    {
        printf("signo is %d\n",num);
        return;
    }
    
    int main()
    {
        signal(2,handler);//捕捉2号信号
        signal(3,handler);//捕捉3号信号
    
        while(1)
        {   
            printf("this is youngmay\n");
            sleep(1);
        }   
        return 0;
    }
    运行结果:
    可以看到,我们由键盘向进程发送的2号和3号信号都被捕捉,而发送的20号信号并未被捕捉,而是执行了它的默认处理动作,让进程暂停了。

    3. sigaction函数

    (1)函数原型

    (2)函数功能:读取和修改与指定信号相关联的处理动作。 如果正在执行该信号的处理动作时,又发来了该信号,系统会自动屏蔽该信号,直到执行结束才解除屏蔽

    (3)参数:
            signum:指定信号的编号
            act:若非空,则根据它修改该信号的处理动作
            oldact:若非空,则通过它传出该信号原来的处理动作
    说明:act、oldact指针指向sigaction结构体,sigaction结构体如下:
    struct sigaction {
                   void     (*sa_handler)(int);//信号的处理动作
                   void     (*sa_sigaction)(int, siginfo_t *, void *);
                   sigset_t   sa_mask;//当正在执行信号处理动作时,希望屏蔽的信号。当处理结束后,自动解除屏蔽
                   int        sa_flags;//一般为0
                   void     (*sa_restorer)(void);
               };

    (4)返回值:成功返回0,出错返回-1

    (5)说明
            将sahandler赋值为常数SIGIGN传给sigsction表示忽略信号;赋值为常数SIG_DFL表示执行系统默认处理动作;赋值为一个函数指针表示用户自定义函数捕捉信号,或者说向内核注册了一个信号处理函数。
    (6)代码实现:
    #include <stdio.h>                                                              
    #include <signal.h>
    #include <unistd.h>
    
    void handler(int num)
    {
        printf("signo is %d\n",num);
        return;
    }
    
    int main()
    {
        struct sigaction act,oact;
        act.sa_handler = handler;//将信号处理动作设置为信号捕捉
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask,3);//屏蔽3号信号
        sigaction(2,&act,&oact);//自定义2号信号的处理动作
        sigaction(3,&act,&oact);//自定义3号信号的处理动作
        int i = 10; 
        while(i--)
        {
            printf("this is youngmay\n");
            if(i == 5)
            {
                sigaction(2,&oact,NULL);//恢复2号信号的默认处理动作
                oact.sa_handler = SIG_IGN;//将3号信号的处理动作设为忽略
                sigaction(3,&oact,NULL);
            }
            sleep(1);
        }
        return 0;
    }                

                          
    运行结果:
            可以看到,前五秒内,我们可进程发送2号和3号信号都执行了自定义函数,打印出一句话;在五秒之后,因为3号信号的处理动作设置为了忽略,而2号信号的处理动作设为默认处理动作,所以我们再向进程发送3号信号时无任何反应,而发送2号信号执行了2号信号的默认处理动作直接终止了进程。


    4. pause函数
    (1)函数原型:
    (2)函数功能:使进程挂起等待直到有信号递达。

    (3)返回值: 只有出错的返回值,返回-1
        若信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;若信号的处理动作是忽略,则进程继续处于挂起等待状态,pause不返回;若信号的处理动作是捕捉,则调用了信号处理函数之后,pause返回-1,errno设置为EINTR(该错误码表示“被信号中断”)。
    (4)利用pause函数实现sleep

            我们想实现sleep几秒,我们可以先让进程挂起等待,在几秒之后,向进程发送信号使得它递达,从而让pause出错返回-1(这里信号的处理动作必须设置为捕捉信号,否则pause函数不会返回),从而达到sleep的功能。其中,在几秒之后向进程发送信号使得pause函数出错返回,我们需要用到之前的alarm函数,设置一个闹钟,在几秒之后,让它向进程发送SIGALRM信号。所以,实现我们的mysleep函数,具体步骤如下:
       
         1)将SIGALRM信号的处理动作设置为自定义动作;
         2)调用alarm函数设置闹钟;
         3)调用pause函数等待;
         4)alarm(0)取消闹钟(在(3)中pause可能被其他信号唤醒,所以要取消闹钟返回闹钟剩余的秒数);
         5)恢复SIGALRM信号的原有处理动作;
         6)返回剩余的秒数;

    实现代码如下:
    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    
    void handler(int num)
    {                                                                               
        ;   
    }
    
    unsigned int mysleep(unsigned int t)
    {
        struct sigaction act,oact;//act为要设置闹钟信号的相关信息,oact保存闹钟信号>的原有相关信息
        act.sa_handler = handler;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);//处理闹钟信号时,不屏蔽其他信号
        sigaction(SIGALRM,&act,&oact);//捕捉闹钟信号
            
        alarm(t);//t秒之后向进程发送闹钟信号
        pause();//使进程挂起等待
        int ret = alarm(0);//取消闹钟,返回闹钟剩余秒数
        sigaction(SIGALRM,&oact,NULL);//恢复闹钟的默认处理动作
        return ret;//返回闹钟剩下的时间
    }
    
    int main()
    {
        while(1)
        {
            printf("hi,i am youngmay\n");
            mysleep(1);
        }
        return 0;
    }                            

    运行结果为:
            程序跑起来之后,每隔一秒,都会打印出一句话。说明我们自己实习的mysleep函数实现了sleep函数的功能。

    我们可以验证一下mysleep函数的返回值是否为闹钟剩下的秒数,修改代码如下:
    #include <stdio.h>                                                              
    #include <signal.h>
    #include <unistd.h>
    
    void handler(int num)
    {
        printf("signo is %d\n",num);
        return;
    }
    
    unsigned int mysleep(unsigned int t)
    {
        struct sigaction act,oact;//act为要设置闹钟信号的相关信息,oact保存闹钟信号>的原有相关信息
        act.sa_handler = handler;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);//处理闹钟信号时,不屏蔽其他信号
        sigaction(SIGALRM,&act,&oact);//捕捉闹钟信号
            
        alarm(t);//t秒之后向进程发送闹钟信号
        pause();//使进程挂起等待
        int ret = alarm(0);//取消闹钟,返回闹钟剩余秒数
        sigaction(SIGALRM,&oact,NULL);//恢复闹钟的默认处理动作
        return ret;//返回闹钟剩下的时间
    }
    
    int main()
    {
        signal(2,handler);//将2号信号的处理动作改为自定义函数
        printf("hello\n");
        unsigned int ret = mysleep(20);
        printf("return ret is %d\n",ret);
        return 0;
    }                      

    运行结果如下:
            在程序运行起来过两秒,我们由键盘给进程发送ctrl+c命令,即向进程发送2号信号,可以看到mysleep函数的返回值为18即闹钟剩余的时间。

    说明:

    (1)在mysleep程序中注册SIGALRM的自定义处理函数,是为了使pause出错返回,使进程从挂起状态醒来。因为如果不注册该函数,SIGALRM信号的默认处理动作是终止进程,所以在几秒后,进程会直接终止而不是醒来继续执行后面的语句。而在处理函数中什么都没做,是因为sleep函数在让进程挂起等待时也什么都没做。

    (2)mysleep中再返回前恢复SIGALRM信号的原有处理动作,是因为在mysleep结束后,进程结束前,如果再次发送SIGALRM信号本意是想终止进程,但因为SIGALRM信号是自定义处理动作,导致进程不终止,这样达不到我们的预期。我们也可以将恢复SIGALRM信号的默认处理动作认为是:你借了别人的东西用,在用完之后,你要将东西给人家还回去,不能影响其他人的使用。

    (3)mysleep函数的返回值与sleep函数的返回值作用一致。

    展开全文
  • 操作系统 — 捕捉信号

    千次阅读 2018-01-19 12:50:57
    捕捉信号从上篇浅析信号中我们了解到信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作为软中断. 从它的命名可以看出来它的实质和使用和中断非常相似,所以,信号也可以说是进程控制的一...
  • trap捕捉信号(附信号表说明)

    千次阅读 2018-12-20 16:11:25
    trap捕捉信号有三种形式 第一种: trap “commands” signal-list 当脚本收到signal-list清单内列出的信号时,trap命令执行双引号中的命令. 例1 #!/bin/bash trap "echo 123" 15 while true do echo abc ...
  • python 异常终止并捕捉信号

    千次阅读 2020-08-28 19:27:30
    基本信号: import signal signal.SIGABORT signal.SIGHUP # 连接挂断 signal.SIGILL # 非法指令 signal.SIGINT # 连接中断 signal.SIGKILL # 终止进程(此信号不能被捕获或忽略) signal.SIGQUIT # 终端退出 ...
  • 如何捕捉信号的上升沿下降沿

    千次阅读 2020-04-26 10:22:57
    思路:设计两个或多个一位的寄存器,用来接收被检测的信号,系统时钟来一次记一次输入信号,如果用了两个寄存器直接异或就可以了;使用高频的时钟对信号进行采样,因此要实现上升沿检测,时钟频率至少要在信号最高...
  • 捕捉信号

    2014-04-13 19:54:52
    捕捉信号 4.1. 内核如何实现信号的捕捉 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信 号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 1. 用户程序...
  • linux进程如何捕捉信号

    千次阅读 2017-09-29 19:11:43
    linux中一共有32种信号,在/usr/include/bits/signum.h 头文件中可以看到 #define SIGHUP 1 /* Hangup (POSIX). */ #define SIGINT 2 /* Interrupt (ANSI). */ #define SIGQUIT 3 /* Q
  • } //3秒后每隔2秒定时 int main(){ //注册捕捉信号 //signal(SIGALRM, SIG_IGN);//捕捉之后忽略信号 //signal(SIGALRM, SIG_DFL);//捕捉不作处理,只是捕捉了一下 signal(SIGALRM, myalarm); struct itimerval new_...
  • signal() 捕捉信号

    千次阅读 2016-08-15 14:52:06
     * 7: 当按hello^C 时字符并没有输出来,而是捕捉信号,打印捕捉调用的函数  * 信号与输入交替进行时会把输入给清掉  * (糸统调用执行过程中来信号)输入阻塞过程中来信号{  清除缓冲区  }  *  ...
  • 文章目录编程环境:未决信号集:阻塞信号集:自定义信号集:sigprocmask() 函数:sigpending() 函数:写一个小的例子:信号捕捉:signal() 函数:Unix 中的 sigaction() 函数:Linux 中的 sigaction() 函数:下载...
  • 捕捉信号的总结

    2017-05-01 20:46:11
    这次我要说的是捕捉信号,那么什么是捕捉信号呢?以及它如何实现和使用?这是我总结的用意。 1. 捕捉信号的概念:如果信号的处理动作是用户自定义函数,在信号传递时就调用这个函数,这称为捕捉信号。 这是别人...
  • Linux下的信号(二):阻塞信号一,什么是捕捉信号?1,捕捉信号:信号处理方式三种方式中的一种,意思是既不忽略该信号,又不执行信号默认的动作,而是让信号执行自定义动作。捕捉信号要使用signal函数,为了做到这...
  • 用signal函数捕捉信号SIGINT

    千次阅读 2020-01-06 23:12:38
    用系统调用函数fork( )创建两个子进程,再用系统调用函数signal()让父进程捕捉信号SIGINT(用kill命令来触发),当捕捉到中断信号后,父进程用系统调用函数kill()向两个子进程发出信号,子进程捕捉到父进程发来的...
  • 信号捕捉过程 我们现在知道了信号在产生之后不是被立即处理的,而是在合适的时候才进行处理,那么什么时候是合适的时候呢?信号又是怎么被捕捉的呢? 合适的时候是指:从内核态切换为用户态时进行信号捕捉。 ...
  • 阻塞信号和捕捉信号

    千次阅读 2016-07-27 00:10:59
    一 阻塞信号 1 概念: 信号递达:实际执行信号的处理动作称为信号递达(Delivery)。 信号未决:信号从产生到递达之间的状态,称为信号未决(Pending)。 进程可以选择阻塞(Block )某个信号。被阻塞的信号产生时将保持在...
  • golang信号捕捉与处理

    2019-08-27 01:00:58
    notify方法原型: funcNotify(c chan<- os.Signal, sig ...os.Signal) 第一个参数表示接收信号的管道 第二个及后面的参数表示设置...1. 捕捉信号的简单示例 1.1 捕捉所有信号 package main import ( "fmt...
  • 不要说我水,我只是在慢慢积累。
  • 1.signal捕捉信号 #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); \qquadsignal(SIGALRM,myfunc)函数会捕捉SIGALRM信号,捕捉之后会执行...
  • 信号捕捉,说白了就是抓到一个信号后,执行我们指定的函数,或者执行我们指定的动作。下面详细介绍两个信号捕捉操作参数:signal和sigaction。 ##signal函数 函数原型: sighandler_t signal(int signum, ...
  • 前面介绍了信号的基本信息和信号的发送,这里总结一下Linux中进程对信号捕捉和处理,进程收到信号一般会设置一个信号处理函数来专门执行接收到信号后的操作,类似于中断一样。 在信号处理函数中,可以根据信号的...
  • trap捕捉信号有三种形式 第一种: trap "commands" signal-list 当脚本收到signal-list清单内列出的信号时,trap命令执行双引号中的命令. 例1 #!/bin/bash trap "echo 123" 15 while true do echo abc sleep 5 ...
  • 进程信号捕捉

    千次阅读 2018-04-14 17:10:43
    如果信号的处理是自定义的,当信号递达时就调用某个用户自定义函数,这就是信号捕捉。 当然,必须要用系统调用通过内核来实现信号捕捉信号捕捉的整个过程: 要捕捉某个信号,首先要注册这个信号的处理函数,...
  • (1)捕捉信号:trap命令 常用信号 ctrl+c(终止进程) ctrl+z(暂停进程,打入后台) [root@server ~]# trap &amp;quot;echo westos&amp;quot; 2 [root@server ~]# ^Cwestos (2)列出中断信号与...
  • 信号(七)信号捕捉

    2018-04-12 22:26:39
    一、利用signal函数来简单捕捉信号#include &lt;signal.h&gt; typedef void (*sighandler_t)(int); //定义一个函数指针类型,其中函数的返回值为void,函数参数为int;其实一个函数的函数名就是函数指针 ...
  • #!/bin/bash trap 'echo "sorry, interupt by ctrl-c"' SIGTERM SIGINT echo begin test program count=1 while [ $count -lt 10 ] ...shell脚本捕捉程序退出 #!/bin/bash trap 'echo "exit program"' EXIT .

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 52,903
精华内容 21,161
关键字:

捕捉信号