Linux_进程信号

理解信号

    1.用户输入命令,在shell下启动一个前台进程

    2.用户按下Ctrl-c,这个键盘产生一个硬件中断。

    3.如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断

    4.终端驱动程序将Ctrl-C解释一个SIGNINT信号,记载该进程的PCB中(也可以说发送了一个SIGNINT信号给该进程)。

    5.当某个时刻要从内核态返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGNINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回他的用户空间代码执行。

注意

    1.Ctrl-c产生的信号只能发送给前台进程。一个命令 后面加个&可以放到后台运行,这样shell不必等待进程结束就可以接受新的命令,启动新的进程。

    2.Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到想Ctrl-c这样的控制键产生的信号。

    3.   前台进程在运行过程中用户随时都可能按下Ctrl-c而产生一个信号,也就是说该进程的用户代码空间执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的

kill -l命令可以查看系统定义的信号列表

[root@dreame ~]# 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

每个信号都有一个编号和一个宏定义名称,这些宏定义可以再signal.h中找到。

产生信号的方式概览:

    1.用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-c产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-z产生SIGTSTP(可使前台进程停止)

    2.硬件异常产生信号,这些条件由硬件检测到并通知到内核,然后内核会发送适当的信号。例如当前进程执行了除0的指令,cpu的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。或者当前进程访问了非法地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号再发给进程。

    3.一个进程调用kill(2)函数可以发送信号给另一个进程。可以用kill(1)命令发送某个信号给某个进程,kill(1)命令也是调用kill(2)函数实现的。如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。如果不想按默认动作处理信号,用户程序可以调用sigaction(2)函数告诉内核应该如何处理某种信号。

    4.软件条件产生

信号处理常见方式概览:可选的处理动作有以下三种:

    1.忽略该信号

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

    3.提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。

产生信号

1.通过终端按键产生信号    

    SIGINT的默认处理动作时终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

2.调用系统函数向进程发信号

    首先在后台执行死循环程序,然后用kill命令给他发SIGSEGV信号。

    kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发的信号)。

#include <signal.h>
int kill(pid_t pid,int signo);
int raise(int signo);

这两个函数都是成功返回0,错误返回-1.

abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);

3.由软件条件产生信号

    SIGPIPE是一种由软件条件产生的信号,在管道里曾使用到这个信号。

#include <unistd.h>
unsigned int alarm(unisigned int seconds);

    调用alarm函数可以设定一个闹钟,也就是告诉内核seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。函数的返回值是0或者以前设定的闹钟还余下的秒数。

例:

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

int main(){
    int count = 14;
    alarm(1);
    for(;1;++count){
        printf("count = %d\n",count);
    }
    return 0;
}

计数一秒钟,一秒钟到,程序被SIGALRM信号终止。

阻塞信号

1.信号其他相关常见概念

    实际执行信号的处理动作称为信号递达(Delivery)

    信号从产生到递达之间的状态,称为信号未决(Pending)

    进程可以选择阻塞(Block)某个信号。

    被阻塞的信号产生式将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

    阻塞和忽略是不同的,信号只要被阻塞就不会递达,而忽略是在信号递达之后,可选择的一种处理信号的动作。

2.在内核中的表示

示意图


    每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图中,SIGHUP信号未阻塞也没有产生过,当它产生并递达时执行默认处理动作。SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然他的处理动作默认是忽略,但在没有解除阻塞之前不能忽略这个信号。因为进程仍然有机会改变处理动作之后再解除阻塞。SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,他的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,虽然操作系统允许递达某信号一次或多次,但Linux下常规信号在递达之前产生一次或多次只记一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

3.sigset_t

    从示意图可以看出,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标记也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset来存储,sigset称为信号集,这个类型可以表示每个信号的"有效",或者"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号集是否被阻塞,而在未决信号集中的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Single Mask),这里的屏蔽是阻塞而不是忽略。

4.信号集操作函数

    sigset类型对于每种信号用一个bit表示"有效"或者"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,如printf打印sigset_t变量是没有任何意义的。

#include <singal.h>
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);

    函数sigemptyset初始化set所指向的信号集,使其中所有的信号对应的bit清零,表示该信号集不包含任何有效信号。

    函数sigfillset初始化set指向的信号集,使其中的所有信号对应bit置位,表示该信号集的有效信号包括系统所支持的所有信号。

    在使用sigset_t类型的变量值钱,一定要调用sigmptyset或sigfillset初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,失败返回-1.sigismember是一个布尔函数,用于判断一个信号集中的有效信号是否包含某种信号,若包含返回1,不包含返回0,出错返回-1.

sigprocmask

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

#include <signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
返回值:成功0,出错为-1

    如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how提示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字假设当前的信号屏蔽字为mask,那么how参数的可选值有:


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

sigpending

   读取当前进程的未决信号集,通过set参数传出。调用成功返回0,失败返回-1。

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

void printsigset(sigset_t *set){
    int i = 0;
    for(;i<32;i++){
        if(sigismember(set,i)){//判定指定信号是否在目标集合中
            putchar('1');
        }else{
            putchar('0');
        }
    }
    puts("");
}

int main(){
    sigset_t s,p;//定义信号集对象,并清空初始化
    sigemptyset(&s);
    sigaddset(&s,SIGINT);//SIGINT即ctrl_c
    sigprocmask(SIG_BLOCK,&s,NULL);//阻塞信号集,ctrl_c
被阻塞    while(1){
        sigpending(&p);//获取未决信号集
        printsigset(&p);
        sleep(1);
    }
    return 0;
}
10000000000000000000000000000000
10000000000000000000000000000000
10000000000000000000000000000000
^C10100000000000000000000000000000//ctrl_c已经无法终止程序,因为被阻塞
^C10100000000000000000000000000000
^C10100000000000000000000000000000
^C10100000000000000000000000000000
^C^C10100000000000000000000000000000
^C10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
^\退出(吐核)                     //ctrl_\还可以退出,未被阻塞,可以被抵达并处理
程序运行时,每秒钟打印一遍各信号的未决状态,由于我们阻塞了SIGINT信号,按Ctrl_c将会使SIGINT信号处于未决状态。

猜你喜欢

转载自blog.csdn.net/qq_40425540/article/details/80054142