Linux 进程信号(产生/注册/安装/阻塞/销毁/处理)

 

目录

信号(signal)

kill -l 查看Linux中的 62种信号

生命周期(事件发生->注册安装->注销->处理)

信号的产生(触发信号的事件发生 )

硬件产生     软件产生

kill 命令   kill()   sigqueue()   raise()   abort()   alarm()   setitimer()

信号的注册安装

非可靠信号与可靠信号

信号的注册

扫描二维码关注公众号,回复: 9655626 查看本文章

信号的安装

signal()   sigaction()

信号的阻塞

sigprocmask()

信号的注销和处理

注销

处理

信号捕捉及进程响应信号的时机

可重入函数

用户处理信号的时机为什么要在内核态切换到用户态的时候?

信号在防止僵尸进程中的应用


信号(signal)

信号是进程间通信机制中唯一的异步通信机制,  是在软件层次(包括操作系统)上对中断机制的一种模拟 .

Linux的信号是系统或进程发出的给一个进程的通知, 它的主要作用是用来 激活 信号接收者 的一段程序(信号处理程序), 除此之外, 也可以携带少量信息.  从实现方式上来看,  它是一种用软件构建的中断系统, 只不过接收及处理中断请求的不是处理器而是进程. 与外设向处理器的硬中断请求一样, 信号是一种异步通信方式. 异步体现在一个进程不必通过任何操作来等待信号的到达, 事实上, 进程也不知道信号到底什么时候到达. 进程之间可以互相通过系统调用kill发送软中断信号. 内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件.  也就是说,信号就是一个携带少量信息的通知 .

系统中的很多事件都可以产生一个信号. Linux为可能产生的信号都进行了命名和编号, 并为接收信号的进程提供了默认服务, 即每个信号都有一个与之对应的信号默认服务程序. 也就是说,进程在接受到某个信号时, 默认会执行与信号相对应的系统提供的默认服务程序. 但如果用户提供了信号处理函数, 就不会进行默认处理. 此时, 内核会在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号 .

kill -l 查看Linux中的 62种信号

 明明是64种为怎么说是62 ? 仔细看其实是没有编号为32 和 33的信号的 .

其中1~31是非可靠信号, 34~64是可靠信号 .  非可靠信号和可靠信号下面说



生命周期(事件发生->注册安装->注销->处理)

从信号发送到信号处理函数的执行完毕 . 一个信号是否可以完整的过完一生, 与其可靠性有关 .

触发信号的事件发生 --> 进程中注册安装 --> 进程中注销 -->执行相应的信号处理函数(生命终止)

来依次看一下这几个过程.


信号的产生(触发信号的事件发生 )

 信号可以由硬件产生, 也可以有软件产生

硬件产生

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

  • 终端按键产生: 如Ctrl+c (对应2号信号SIGINT), Ctrl+z(对应19号信号SIGSTOP) , Ctrl+\(对应3号信号SIGOUIT) 等
     
  • 硬件异常产生的信号: 如访问非法内存(内存越界), 除0等 

来看一下野指针导致的硬件异常发出的信号,  其中的signal()为信号捕捉函数, hander()是自定义的处理方法, 信号捕捉后面说.

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void hander(int sig){
    printf("段错误(吐核)\n");
    printf("捕捉到的信号为 %d 号\n", sig);
    raise(SIGKILL);//raise(9);
}
int main(){
    //signal(SIGSEGV, hander);
    int * p = NULL;
    *p = 10;
    return 0;
}

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void hander(int sig){
    printf("段错误(吐核)\n");
    printf("捕捉到的信号为 %d 号\n", sig);
    raise(SIGKILL);//raise(9);
}
int main(){
    signal(SIGSEGV, hander);
    int * p = NULL;
    *p = 10;
    return 0;
}


软件产生

  • kill 命令终止一个进程
     
  • 进程使用kill()函数将一个信号发送给一个进程或一个进程组

    PS : 这样操作是有限制的, 即如不是root用户, 接收信号的进程和发送信号的进程的所有者必须相同.
     
  • sigqueue()函数,raise()函数, abort()函数, setitimer()函数, alarm()函数等

kill 命令

使用方式 : kill  -signum  PID 

其中signum就是前面的kill -l 显示的信号的编号, 其中最常用的是kill -9 , 9号是SIGKILL信号, 其作用是杀死一个进程(中止信号)

Shell在解析kill命令时是调用kill()函数实现的 . 

示例 :
刚在Linux下敲代码时, 段错误让人头大, 提示段错误是由11号信号SIGSEGV发出的. 来看下面代码.

#include<stdio.h>
int main(){
    while(1);
    return 0;
}

虽然这段代码会陷入死循环, 但确实是没有错的, 我们编译运行如下.

10601是while1进程的id. 之所以要两次回车才显示段错误提示 , 第一次回车是执行kill命令, 第二次回车是因为在10601进程终止掉之前, 已经回到了Shell提示符等待用户输入下一条命令, Shell不希望段错误信息和用户的输入交错在一起, 所以等用户再次输入命令之后才显示. 
以往遇到的段错误都是由非法内存访问产生的,  而这个程序本身没错,  是由我们手动发送信号SIGSEGV产生的段错误.


kill()

头文件:#include <signal.h>

原型  : int kill(pid_t pid, int sig);

功能 : 向指定的进程或进程组发送任何信号

参数 :  pid > 0, 则将信号sig发送到进程ID为 pid的进程.
           pid = 0, 则将sig发送到调用进程的进程组中的每个进程 .
           pid = -1, 则将sig发送到调用进程有权发送信号的每个进程, 但不包括1号进程
           pid < -1, 则将sig发送到进程组中ID为-pid的每个进程 .
           sig = 0 ,  则不发送任何信号,但仍执行错误检查,  这可用于检查是否存在进程ID或进程组ID.

返回值:  成功时( 至少发送了一个信号) , 返回0. 出现错误时, 将返回-1, 并设置errno .


sigqueue()

头文件:#include <signal.h>

原型  : int sigqueue(pid_t pid, int sig, const union sigval value);

功能 : 向指定的进程发送任何信号(可以带附加信息), 是比较新的发送信号的函数, 主要是针对可靠信号提出的 (当然也支持前31

          种), 支持信号带有参数, 与函数sigaction()配合使用 (sigaction()函数下面说) .

          sigqueue()比kill()传递了更多的附加信息, 但sigqueue()只能向一个进程发送信号,而不能发送信号给进程组 

参数 :  pid : 指定接收信号的进程ID

           sig : 要发送的信号, 如果signo=0, 将会执行错误检查, 不发送任何信号, 即检查pid的有效性以及当前进程是否有权限向目标

                   进程发送信号.

           value联合体类型union sigval, 指定向信号传递的参数. 在调用sigqueue时, 这个联合体中的信息会拷贝到信号处理函数

                       的siginfo_t结构中, 这样信号处理函数就可以处理这些信息了 .

联合体定义如下 :

typedef union sigval {
    int  sival_int;
    void *sival_ptr;
}sigval_t;

返回值:  成功时, 返回0. 出现错误时, 将返回-1, 并设置errno .


raise()

头文件 : #include <signal.h>

原型int raise(int sig);

参数 :sig : 要发送的信号

功能 : 给自己发送任意信号. 即向调用进程或线程发送一个信号, 在单线程程序中,它相当于kill(getpid(), sig), 即向自己发送信号.

          在多线程程序中,它相当于 pthread_kill(pthread_self( ),  sig);

          如果信号导致调用处理程序, 则raise()函数在信号处理程序返回之后才返回 .

返回值 : raise()成功时返回0,失败时返回非0.


abort()

头文件 : #include <stdlib.h>

原型 :     void abort(void);

功能:  abort函数使当前进程接收到信号而异常终止 (abort()内部会调用raise()). abort()首先解除SIGABRT信号的阻塞, 然后为调用

           进程发出该信号. 会导致所调用进程的异常终止,  除非SIGABRT信号被捕获并且信号处理程序不返回. 

参数 : 无

返回值 : 无, 像exit()函数一样, abort函数总会成功的, 所以不需要返回值.


alarm()

头文件:  #include <unistd.h>

原型 :    unsigned int alarm(unsigned int seconds);

功能 : 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作   

           是终止当前进程. sleep()函数内不就是调用了alarm() .参数: 设定的秒数. 如果seconds为0, 则之前设置的闹钟被取消, 返回 

           时返回之前闹钟被取消时还剩余的秒数.

返回值 : 没有被取消正常结束返回值是0, 或者是之前设定的闹钟时间还余下的秒数.


现在系统中很多程序不再使用alarm调用, 而是使用setitimer调用来设置定时器, 用getitimer()来得到定时器的状态, getitimer()的

声明格式:int getitimer(int which, struct itimerval *value);

setitimer()

头文件 :  #include <sys/time.h>

原型 : int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

参数 : which:这个系统调用给进程提供了三个定时器, 它们各自有其独有的计时域, 当其中任何一个到达, 就发送一个相应的信号

                      给进程, 并使得计时器重新开始。三个计时器由参数which指定, 如下所示:

                      TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。

                      ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。

                      ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时. 与ITIMER_VIRTUAL是一对, 该定时器经常

                      用来统计进程在用户态和内核态花费的时间. 计时到达将发送SIGPROF信号给进程.

           new_value: 用来指明定时器的时间, 其结构如下:

struct itimerval {
    struct timeval it_interval; 下一次的取值 
    struct timeval it_value;    本次的设定值 
};

                        该结构中的timeval结构定义如下:

struct timeval {
    long tv_sec;  秒
    long tv_usec; 微秒,1秒 = 1000000 微秒
};

          old_value: 如果不为空,则其中保留的是上次调用设定的值. 定时器将it_value递减到0时, 产生一个信号, 并将it_value的值

                            设定为it_interval的值, 然后重新开始计时, 如此往复. 当it_value设定为0时, 计时器停止, 或者当它计时到期 ,

                            it_interval为0时停止.

返回值 :调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

错误码 : EFAULT:参数new_value或old_value是无效的指针 .

              EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。



信号的注册安装

如果进程要处理某一信号, 那么就要在进程中注册(或称之为安装)该信号. 所谓信号的注册,  就是把信号编号及其对应的处理

函数加入到进程控制块task_struct中. 

说到信号的注册, 就需要区分一下信号的种类, 再来看具体怎么注册的. 从可靠性角度 : 分为可靠信号非可靠信号.


非可靠信号与可靠信号

前面说到62种信号中, 1~31是非可靠信号, 34~64是可靠信号. 

非可靠信号(也称之为非实时信号): 

Linux的信号继承自早期的Unix信号,非可靠信号算是Unix信号的缺陷 . 其中Linux中的1~31号就是集成于Unix.

非可靠信号的生命周期可能不完整 .

那非可靠信号到底哪儿靠不住了 ?

非可靠信号的主要问题是信号可能会丢失, 分为两种情况 .

1. 非可靠信号采用的是位图来进行注册,而没有采用队列, 所以当位图中已有这个信号时, 再来一个相同的信号注册, 后来的信号并不会注册, 其生命周期不完整,  这就造成了信号的丢失 .

2. 进程处理一个非可靠信号, 信号处理函数执行完毕, 信号处理函数会恢复成默认处理方式.  因此,进程在下次接收到这个信号后, 信号执行的是默认服务, 而在一些情况下, 这个默认处理并不是我们所希望的所以, 常常需要在信号处理程序中,  重新安装该信号, 但这种做法很不靠谱, 极易造成信号的丢失(即有信号没有得到用户预期的处理, 也可以看作一种丢失).

可靠信号(也称之为实时信号) :

Linux既继承了Unix的非可靠信号, 也有自己独有的一套可靠信号, 就是34~64号

这个可靠, 肯定是相对于非可靠来说的,  非可靠可能会造成信号丢失, 那么可靠信号则不会.

可靠信号是 通过信号排队的方法实现的,  即在多个信号集中到达时, 内核用信号队列记住这些信号, 这样就从根本上解决了信号丢失的问题 .

即当一个信号发送给一个进程时, 不管该信号是否已经在进程中注册, 都会被再注册一次, 并加入到可靠信号未决信号队列, 这意味着同一个信号可以在同一个进程的未决信号队列中占有多个sigqueue结构. 所以可靠.

总结: 可靠性是指信号是否会丢失, 即该信号是否支持排队. 如果支持排队就是可靠的, 不支持排队就是不可靠的.

区分完了这两种信号, 回过头再来看它们各自是如何注册的 .


信号的注册

当有事件发生时, 如检测到硬件异常, 以及调用信号发送函数kill()等, 即会诞生相应的信号. 此时在接收信号的进程中维护着一个

号队列, 这个队列是一个sigqueue结构的链表.  sigqueue结构定义如下:

struct sigqueue {
    struct sigqueue * next;         指向下一个
    siginfo_t info;
};

其队列头是一个sigpending结构, 如下:

struct sigpending
    struct sigqueue * head;  链表头指针
    struct sigqueue * tail;  链表尾指针
    sigset_t signal;
};

在这个结构中, 除了用队列头尾两个指针之外还有一个sigset_t类型的位图 signal.  系统中的每个信号都在位图中占有一个固定的

位置,  在每个信号的固定位置以1或0表示这个信号的状态.  所以结构sigpending中的这个signal位图就用其中各个位的值来表示对

应的信号的到来情况, 即用1表示信号已经到来并且还未被进程处理 ;  用0表示该位对应的信号未到或已处理完毕.  因此,这个

signal位图也叫做进程的未决信号位图 .  task_struct 中就有成员 struct sigpending pending;

非可靠信号的注册

当一个非可靠信号发送给一个进程时, 如过该信号在位图中对应位置为0, 则置1, 将这个信号加入信号队列末尾, 如果该信号在位

图中对应位置为1, 对位图不修改, 直接忽略掉这个信号. 这也就意味着同一个非可靠信号可以在同一个进程的未决信号信息链中最

多只有一个sigqueue结构 . 

可靠信号的注册

当一个可靠信号发送给一个进程时, 如过该信号在位图中对应位置为0, 则置1, 将这个信号加入信号队列末尾, 如果该信号在位图

中对应位置为1, 对位图不修改, 直接将这个信号加入信号队列末尾. 所以不管该信号是否已经在进程中注册,  都会被再注册一次,

也就是会加入到信号队列. 因此, 可靠信号相比于非可靠信号不会在注册时丢失.这也就意味着同一个可靠信号可以在同一个进程

的未决信号信息链中占有多个sigqueue结构 (进程每收到一个可靠信号, 都会为它分配一个结构来登记该信号信息, 并把该结构添

加在未决信号链尾, 即所有诞生的可靠信号都会在目标进程中册 ).


信号的安装

如果进程要处理某一信号, 那么就要在进程中安装该信号. 所谓信号的安装, 就是把信号编号及其对应的处理函数加入到进程

控制块中 .

安装信号主要用来确定信号值及进程针对该信号值的动作(处理函数)之间的映射关系,  即在进程中注册的信号在执行时, 该

执行何种操作.

Linux用来安装信号的函数有两个:signal()sigaction() .其中, signal()是在系统调用sys_signal()基础上实现的库函数, signal()

只有两个参数, 不支持信号传递信息, 主要是用于前31种非可靠信号的安装 ; 而sigaction()是较新的函数(由两个系统调用实现:

sys_signal以及sys_rt_sigaction), 有三个参数, 支持信号传递信息, 主要用来与 sigqueue() 系统调用配合使用, 当然, sigaction()同

样支持非实时信号的安装. sigaction()优于signal()主要体现在支持信号带有参数 , 这两个函数下面说.

进程对信号管理的结构如下图所示:


signal()

头文件 : #include<signal.h>

原型 : void (*signal(int signum, void (*handler))(int)))(int);

原型不容易理解, 我们用时可以直接用下面的形式

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);  //直接用这个形式

功能 : 改变进程接收到特定信号后的行为.

signal()的行为在不同的Unix版本中有所不同, 在不同的Linux不同版本中也不尽相同, 所以推荐使用sigaction()

参数signum : 指定信号的值.

          handler : signum信号的的处理方法, 参数设为SIG_IGN时忽略该信号; 参数设为SIG_DFL时采用系统默认方式处理方法;   

          还可以自己传一个自定义的处理函数(自定义函数的地址).

返回值 : 调用成功, 返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR .


sigaction()

头文件 : #include<signal.h>

原型 : int sigaction(int signum, const struct sigaction *act,  struct sigaction *oldact);

功能 : 改变进程接收到特定信号后的行为.

参数signum :指定信号的值,  除SIGKILL和SIGSTOP外的任一有效的信号 ( 因为这两个信号是非阻塞的,如果给他两定义自己                             的处理函数, 将导致信号安装错误)  (信号阻塞下面说)

          act : 指向结构sigaction实例的一个指针, 在结构sigaction的实例中, 指定了对特定信号的处理, 可以为空, 进程会以缺省方   

                  式对信号处理 .

         oldact : 指向的对象用来 保存并返回 原来对相应信号的处理, 不关心则可指定oldact为NULL. 如果把第2, 第3个参数都设为

                       NULL, 那么这个函数可用于检查信号的有效性 .

第2个参数act 最为重要, 其中包含了对指定信号的处理, 信号所传递的信息, 信号处理函数执行过程中应屏蔽掉哪些信号等等 .

返回值 : 当函数调用成功后,返回0,否则返回-1 .

函数中所使用的sigaction结构如下

struct sigaction {
	__sighandler_t	sa_handler;      //信号处理函数指针
	unsigned long	sa_flags;        //信号处理函数行为方式标志
	sigset_t	sa_mask;	//屏蔽位图 
};

 其中sa_handler是一个联合体成员, 联合体如下.

union {
	__sighandler_t sa_handler;
	void (*sa_sigaction) (int, siginfo_t *, void *);
}__sigaction_handler;

所以用户既可以指定信号处理函数, 还可以采用缺省的默认处理方式 (用SIG_DFL)或忽略该信号(用SIG_IGN). 其中sa_sigaction

是自定义的信号处理函数指针, 这个信号处理函数带有三个参数, 是为可靠信号而设的(当然同样支持非可靠信号), 第 1 个参数为

信号值, 第 3 个参数没有使用, 第 2 个参数是指向siginfo_t结构实例的指针,  结构中包含信号携带的数据值, 参数等 .

sigaction结构体中的sa_mask 是一个sigset_t类型的变量, 和前面在信号注册时说的未决信号位图同种类型, 使用原理也相同, 但

在此处却含义不同,  sa_mask在此处表示信号屏蔽位图(或 阻塞信号集, 信号阻塞掩码等, 都是一个意思). 该类型定义如下:

typedef struct {
    unsigned long sig[_NSIG_WORDS];
} sigset_t;

在sigaction结构中sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞.

注:请注意sa_mask指定的信号阻塞的前提条件是,在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞


信号的阻塞

当信号在进程中注册后, 有时并进程并不希望这个信号马上处理, 所以才有信号的阻塞, 被阻塞的信号暂不处理, 一直处于未

决状态, 直到解除阻塞 .

每个进程都有一个用来描述 "哪些信号递送到进程时将被阻塞"的信号集,该信号集中的所有信号在递送到进程后都将被阻塞.

先来统一下概念 :

  • 实际执行信号的处理动作(忽略, 默认, 自定义) 称为信号递达 
     
  • 信号从产生到递达之间的状态, 叫做信号未决
     
  • 进程可以选择阻塞某个信号, 也就是选择屏蔽某个信号, 阻塞也叫屏蔽. 被阻塞的信号产生时,将保持在未决状态,直至进程取消

    对该信号的阻塞,才执行递达的动作

注意:阻塞和忽略是不同的. 只要信号阻塞就不会被递达, 而忽略是信号在递达之后的一种处理方式.

在前面信号注册安装时, 我们知道信号在task_struct中有一个pending位图(未决信号集), 其实信号在task_struct中还有一个block

位图(阻塞信号集或屏蔽位图) . 信号产生时,  内核在task_struct中的pending中设置该信号的未决标志, 直至信号递达才清除该标

志. 但在信号递达前, 信号可能被阻塞, 致使信号一直处于未决状态.  过程如下 :

图片来源于网络

注意: 有两个信号不能被阻塞, 在处理时不能忽略, 不能自定义处理函数,  就是9号信号SIGKILL和19号进程SIGSTOP.

SIGKILL : 强行结束某个进程  SIGSTOP : 强行暂停某个进程

信号的阻塞可以是系统阻塞或自定义阻塞 :

系统自动阻塞 :在某个信号的处理函数正在执行时, 该信号将被阻塞, 直到信号处理函数执行完毕, 该阻塞将会解除. 这种机制的作

                        用主要是避免信号的嵌套处理.

自定义阻塞 : 通过sigaction()sigprocmask()实现信号的自定义阻塞 .

int sigaction(int signum, const struct sigaction *act,  struct sigaction *oldact);

在使用sigaction安装信号时, 如果设置了sa_mask阻塞信号(信号阻塞掩码)集, 则该信号集中的信号在其信号对应的处理函数

执行期间将会被阻塞. 这种情况下进行信号阻塞的主要原因是: 一个信号处理函数在执行过程中, 可能会有其他信号到来. 此

时, 当前的信号处理函数就会被中断.  而这往往是不希望发生的. 此时, 可以通过sigaction系统调用的信号阻塞掩码sa_mask

对相关信号进行阻塞. 通过这种方式阻塞的信号, 在信号处理函数执行结束后就会解除.

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

可以通过sigprocmask系统调用指定阻塞某个或者某几个信号. 这种情况下进行信号阻塞的原因较多, 一个典型的情况是:某

个信号的处理函数与进程某段代码都要对某个共享数据区进行读写. 如果当进程正在读写共享数据区的过程中, 一个信号产

生, 则进程的读写过程将被中断转而执行信号处理函数, 而信号处理函数也要对该共享数据区进行读写, 这样共享数据区就可

能会发生混乱. 这种情况下, 需要在进程读写共享数据区前阻塞该信号,  在读写完成后再解除该信号的阻塞 .

sigprocmask()

头文件 : #include<signal.h>

原型 : int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能 : 指定阻塞某个或者某几个信号

参数how : 输入型参数, 设置要对block进行的操作.有3种方式

                     SIG_BLOCK --  向block中添加set中的信号  block = block | set

                     SIG_UNBLOCK -- 解除set在block中的信号 block = block & ~(set)

                     SIG_SETMASK。 -- 将block设置为set   block =  set

          set : 输入型参数, 阻塞信号集

          oldset : 输出型参数, 原阻塞信号集. 不关心则置空

返回值 : 若成功, 返回0, 若失败,返回-1.

系统还提供了以下与阻塞相关的函数 :

int sigemptyset(sigset_t * set);                  初始化函数,清除信号集中的所有信号
int sigfillset(sigset_t * set);                   初始化函数,使信号集包括所有信号
int sigaddset(sigset_t * set, int signo);         添加信号
int sigdelset(sigset_t * set, int signo);         删除信号
int sigismember(const sigset_t * set, int signo); 检测signor信号在set中有没有


信号的注销和处理

进程在处理信号之前(运行相应的信号处理函数前), 会在未决信号队列(链表)中删除信号对应的sigqueue节点 .

注销

非可靠信号的注销: 

对与非可靠信号来说, 由于在未决信号队列中最多只占用一个sigqueue节点, 所以只需要在删除对应sigqueue结点后, 将位图中对

应位置 置为0即可.

可靠信号的注销 :

对于可靠信号来说, 某一个可靠信号在未决信号队列中可能不止一个, 所以在删除对应的sigqueue结点后, 还需要检测队列中是否

还有相同的节点, 如果没有, 则将位图中对应位置置为0, 如果还有, 则不对位图进行操作.

处理

进程在进行信号处理时, 有以下三种处理方式 :

1. 忽略此信号(忽略处理)。大多数信号都可使用这种方式进行处理, 但有两种信号却不能被忽略:SIGKILL和SIGSTOP. 另外,如

     果忽略某些由硬件异常产生的信号(例如非法内存访问或除以0), 则进程的行为是未定义的;

     例如 : signal(SIGINT, SIG_IGN); 就是忽略信号SIGINT

2. 执行该信号的默认处理动作(默认处理)(对于大多数信号而言, 系统默认的操作是中止该进程)

     例如 : signal(SIGINT, SIG_DFL); 就是执行SIGINT的默认处理函数

3. (自定义处理)   必须提供一个信号处理函数, 内核在处理该信号时切换到用户态执行这个处理函数, 这种方式称为捕捉(Catch)一

    个信号 .例如 : signal(SIGINT, sighandler ); 就是执行SIGINT的自定义处理函数sighandler.

下面详细来看一下信号捕捉的过程. 


信号捕捉及进程响应信号的时机

 小问题: 进程如何从用户态转向内核态?   答 : 中断, 异常, 系统调用 .

内核信号捕捉处理过程及响应信号的实际如下图所示 :

上图很好的说明了信号捕捉时用户态和内核态的切换(处理信号最好的时机是程序从内核态切换至用户态的时候). 下面就上图的一

系列操作作以解释说明:

注; sighandler()为自定义信号处理函数(图中写错成sighand了, 太麻烦了就不改了...)

(1)当前正在执行main函数时发生中断, 异常或者系统调用切换至内核态.

(2)在中断处理完毕后要返回用户态的main函数之前, 调用do_signal()检查是否有待处理信号.

(3)内核决定返回用户态后时, 如果do_signal()检测到有待处理信号, 这种情况不是直接返回用户态恢复main函数的上下文信息

        继续执行,而是返回用户态执行sighandler(), sighandler()和main函数使用不同的堆栈空间, 两者之间不存在调用和被调用的

        关系, 它们属于两个独立的控制流程.

(4)sighandler函数返回后自动执行特殊的系统调用, 即调用sigreturn再次进入内核态.

(5)进入内核态后还是用do_signal()检测是否有待处理信号, 如果没有, 这次再返回用户态就是恢复main函数的上下文继续向下

         执行. 如果有继续转入用户态执行sighandler(), 然后到(4), 这样直到没有待处理信号时再返回用户态就是恢复main函数的上

         下文继续向下执行.

至此, 一个信号就结束了它的一生.   写的我好累啊...


可重入函数

函数的重入: 多个执行流中进入同一个函数运行, 比如说就是当前调用某个函数, 当执行这个函数到一半的时候, 进程时间片用完

                    另一个进程开始运行, 有调用这个函数执行 . 就说这个函数就被重入.

不可重入函数 : 当函数重入发生时, 会造成数据二义性, 逻辑混乱等问题的函数, 称之为不可重入函数

可重入函数: 与不可重入函数相反, 当函数重入发生时, 不会造成数据二义性, 逻辑混乱等问题的函数, 称之为可重入函数

1. 自定义信号处理函数应为可重入函数

2. 函数不可重入的原因有:

   (1).  malloc/free函数都为不可重入函数, 在多个执行流中进行操作时要格外注意

   (2). 函数中静态变量/全局变量

   (3). 标准I/O函数

所以说, 在设计信号处理函数时, 要考虑到函数的可重入性.


用户处理信号的时机为什么要在内核态切换到用户态的时候?

原因是:信号不一定会被立即处理, 操作系统不会为了处理一个信号而挂起当前正在运行的进程,这样产生的消耗太大( 当然紧

急信号除外, 如9号和19号进程 ) 操作系统选择在内核态切换至用户态的时候去处理信号, 是因为这样就不用单独为了处理信号而

进行进行内核与用户之间的切换, 从而浪费时间.

但是有时候一个正在睡眠的进程突然收到信号, 操作系统肯定不愿意切换当前正在运行的进程, 预示着就将该信号存在此进程的

PCB的信号字段中 , 此时这个信号被阻塞.



信号在防止僵尸进程中的应用

在子进程先于父进程退出时, 要是父进程没有关注到子进程的退出, 没有获取子进程的退出状态, 则子进程就会处于僵死状态, 子进

程称为僵尸进程, 造成资源泄露等危害. 在之前我们用wait()和waitpid()来解决这个问题, 即父进程调用wait()一直阻塞等待子进程

退出(父进程此时处于可中断睡眠状态), 然后父进程获取子进程退出状态, 然后子进程完全释放. waitpid()非阻塞函数, 所以需要循

环等待子进程退出(此时父进程处于运行状态). 但不管是wait()还是waitpid(), 在等待子进程退出时都不能做其他的事, 这显然是不

合理的. 此时就可以利用信号机制来很好地处理这个问题了 .

17 -- SIGCHLD:  在子进程结束时, 内核会给父进程发送这个信号. 

这样, 我们就可以自定义SIGCHLD 信号的处理函数, 在自定义处理函数中调用waitpid(), 这样就不需要父进程一直等待了. 

等等, 还没结束, SIGCHLD编号是17, 是一个非可靠信号, 那么问题就来了, 前面说过, 非可靠信号在未决信号队列中只能有一个,

当多个子进程同时退出时, 就会有多个SIGCHLD信号同时发送给父进程, 那么父进程也只会处理处理一个SIGCHLD信号, 其他信

号会丢失, 对应的, 也只回调了一次SIGCHLD自定义处理函数, 只能处理一个僵尸进程. 但非可靠信号的丢失问题是没有办法避免

的, 这是设计之初就有的缺陷, 所以只能在一次回调了一次SIGCHLD自定义处理函数时处理完所有的变成僵尸进程的子进程.

这样就可以在自定义处理函数中用 while(waitpid(-1, NULL, WNOHANG) > 0). 循环是为了在一次调用SIGCHLD自定义处理函

数时完成对所有此刻退出的子进程处理 .

wait() / waitpid()详解 戳链接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103302883

僵死状态/僵尸进程 : 戳链接( ̄︶ ̄)↗ https://blog.csdn.net/qq_41071068/article/details/103231310

发布了223 篇原创文章 · 获赞 639 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/103659853