uc_10_信号

1  信号基础

1.1  什么是信号

        信号是提供异步事件处理机制的软件中断

        这些异步事件可能来自硬件设备,如用户同时按下了Ctrl和C键;也可能来自系统内核,如试图访问尚未映射的虚拟内存;又或者来自用户进程,如尝试计算 int / 0表达式。

        进程之间可以相互发送信号,这使信号成为一种进程间通信(Inter Process Communication, IPC)的基本手段。

        信号的异步特性不仅表现为它的产生是异步的,对它的处理同样也是异步的:

        程序的设计者不可能也不需要精确地预见什么时候触发什么信号,也同样无法预见该信号究竟在什么时候会被处理。一切都在内核的操控下,异步地运行。

        信号是在软件层面对中断机制的一种模拟。

1.2  信号的名称与编号

        信号是很短的信息,本质就是一个整数,用以代表不同事件。

        signum.h头文件中用一组名字前缀为SIG的宏来标识信号,即为信号的名字。

        kill  -l 命令可查看所有62个信号

        生成转储文件,俗称吐核

        其中前31个信号为不可靠的非实时信号,后31个为可靠的实时信号。

31个不可靠、非实时信号
编号 名称 说明 默认操作
1 SIGHUP 进程的控制终端关闭(用户登出) 终止           
2 SIGINT 用户产生中断符(Crtl + C) 终止           
3 SIGQUIT 用户产生退出符(Ctrl + \) 终止 + 转储
4 SIGILL 进程试图执行非法指令 终止 + 转储
5 SIGTRAP 进入断点 终止 + 转储
6 SIGABRT abort()函数产生 终止 + 转储
7 SIGBUS 硬件或对齐错误 终止 + 转储
8 SIGFPE 算术异常(除以0 ...) 终止 + 转储
9 SIGKILL 不能被捕获或忽略的进程终止信号 终止           
10 SIGUSR1 进程自定义的信号 终止           
11 SIGSEGV 无效内存访问 终止 + 转储
12 SIGUSR2 进程自定义的信号 终止           
13 SIGPIPE 向读端已关闭的管道写入 终止           
14 SIGALRM alarm()函数产生/真实定时器到期 终止           
15 SIGTERM 可以被捕获或忽略的进程终止信号 终止           
16 SIGSTKFLT 协处理器栈错误 终止           
17 SIGCHLD 子进程终止            忽略
18 SIGCONT 进程由停止状态恢复运行            忽略
19 SIGSTOP 不能被捕获或忽略的进程终止信号            停止
20 SIGTSTP 用户产生停止符(Ctrl + Z)            停止
21 SIGTTIN 后台进程读控制终端            停止
22 SIGTTOU 后台进程写控制终端            停止
23 SIGURG 紧急I/O未处理            忽略
24 SIGXCPU 进程资源超限 终止 + 转储
25 SIGXFSZ 文件资源超限 终止 + 转储
26 SIGVTALRM 虚拟定时器到期 终止           
27 SIGPROF 实用定时器到期 终止           
28 SIGWINCH 控制终端窗口大小改变            忽略
29 SIGIO 异步I/O事件 终止           
30 SIGPWR 断电 终止           
31 SIGSYS 进程试图执行无效系统调用 终止 + 转储

         9和19都是不能被捕获或忽略的进程终止信号。

        02是Ctrl+C,终止;

        20是Ctrl+Z,停止。

 2  信号处理(忽略、默认、捕获)

        忽略:什么也不做,SIGKILL(9)和SIGSTOP(19)不能被忽略

        默认:在没有人为设置的情况,系统缺省的处理行为

        捕获:接收到信号的进程会暂停执行,转而执行一段事先编写好的处理代码,执行完毕后再

                   从暂停执行的地方继续运行。

                   信号捕获一旦开启,就由内核形成捕获机制:循环捕获,啥时候来啥时候捕获。

2.1  signal()

        #include

        typedef void(*sighandler_t)(int);

        sighandler_t   signal(int signum,   sighandler_t handler); 

                功能:设置调用进程针对特定信号的处理方式

                signum:信号编号

                handler:信号的处理方式,可以取以下值

                                SIG_IGN                  忽略

                                SIG_DFL                  默认

                                信号处理函数指针    捕获  //信号处理函数,由内核负责调用

                返回值:成功(一般不关心)返回原信号处理方式,如果之前未处理过则返回NULL 

                               失败返回SIG_ERR !!!

//signal.c   信号处理:忽略、捕获、恢复默认
#include
#include
#include

//信号处理函数,由内核负责调用!!!
void sigfun(int signum){
    printf("%d进程:捕获到%d号信号\n",getpid(),signum);
}

int main(void){

    //对2号信号进行忽略处理(此开启了一种机制,这机制是忽略)
    if(signal(/*2*/SIGINT,SIG_IGN) == SIG_ERR){  //注意是宏SIG_ERR
        perror("signal");
        return -1;
    }//忽略以下代码,保留for(;;),编译执行,按Ctrl+C发现被忽略

    //对2号信号进行捕获处理
    if(signal(SIGINT,sigfun) == SIG_ERR){
        perror("signal");
        return -1;
    }//忽略以下代码,保留for(;;),编译执行,按Ctrl+C发现被捕获

    //对2号信号进行默认处理
    if(signal(SIGINT,SIG_DFL) == SIG_ERR){
        perror("signal");
        return -1;
    }//编译执行,按Ctrl+C,发现恢复为SIGINT
    
    for(;;);
    return 0;
}

        主控制流程、信号处理流程、内核处理流程:

        uc_10_信号_第1张图片

        当有信号到来时,内核会保存当前进程的栈帧,然后再执行信号处理函数。

        当信号处理函数结束后,内核会恢复之前保存的进程的栈帧,使之继续运行。 

2.2  太平间信号(17)

        前述博文中的wait()也好,waitpid()也好,阻塞也好,非阻塞也好,都不如太平间信号收尸来的完美(把收尸的事放到17信号处理函数中)。

        如前所述,无论一个进程是正常终止还是异常终止,都会通过系统内核向其父进程发送SIGCHLD(17)信号,对17号信号默认处理方式是:忽略。

        父进程完全可以在针对SIGCHLD(17)信号的信号处理函数中,异步地回收子进程的僵尸,简洁高效(子进程死了发送17,父就及时收尸,收尸后不影响父进程干自己的事):

void sigchld (int signum){

    pid_t pid = wait(NULL);

    if(pid == -1){

        perror("wait");

        exit(EXIT_FAILURE);

        }

    printf("%d子进程终止\n,pid);

}

       上述方式是收到1个17号信号,父进程收1个尸。

        这样处理存在一个潜在的风险,就是在sigchld()信号处理函数执行过程中,又有多个子进程终止,由于SIGCHLD(17)信号不可靠,可能会丢失(信号集,信号屏蔽uc_11),形成漏网僵尸,因此有必要在一个信号处理函数调用过程中循环waitpid(),回收尽可能多的僵尸:

        5个子进程同时死,发出5个17号信号。第1个17号信号过来,调用信号处理函数,此时丢失3个信号。此时若还是按照1个17号信号,收1个尸的话,会产生3个僵尸进程,ps aux命令查看。

        解决方案:在每次调用信号处理函数的时候,不再只收1个子进程尸体,而是循环多次收尸,一次把5个尸体收干净:

for(;;){

    pid_t pid = waitpid(-1,NULL,WNOHANG);
    if(-1 == pid){
        if(ECHILD == errno){
            printf("%d进程:没有子进程可回收了\n",getpid());
            break;
        }else{
            perror("waitpid");
            return;
        }
    }else if(0 == pid){
        printf("%d进程:子进程在运行,没法回收\n",getpid());
        break;
    }else{
        printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
    }
}
//sigchld.c  17号信号/太平间信号
#include
#include
#include
#include
#include
//信号处理函数,负责收尸
void sigchild(int signum){
    printf("%d进程:捕获到%d号信号\n",getpid(),signum);
    sleep(2);//假装信号处理函数比较耗时

    //waitpid()循环收尸,非阻塞方式,最完美
    for(;;){
        pid_t pid = waitpid(-1,NULL,WNOHANG);
        if(pid == -1){
            if(errno == ECHILD){
                printf("%d进程:没有子进程了\n",getpid());
                break;
            }else{
                perror("waitpid");
                return;
            }
        }else if(pid == 0){
            printf("%d进程:子进程在运行,收不了\n",getpid());
            break; //老六老也不死,退出循环,啥时候死,啥时候内核再调用本函数
        }else{
            printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
        }    
    }
    
    //wait()循环收尸。wait()是阻塞函数,天然缺陷(老六一直不死,父进程不能就干等着吧)
    /*for(;;){
        pid_t pid = wait(NULL);//NULL,不存子进程终止状态
        if(pid == -1){
            if(errno == ECHILD){
                printf("%d进程:没有子进程了\n",getpid());
                break;
            }else{
                perror("wait");
                return;
            }
        }
        printf("%d进程:回收了%d号进程\n",getpid(),pid);
    }*/
}

int main(void){
    //对17号信号进行捕获处理,把收尸的事放在信号处理函数这里处理,完美
    if(signal(SIGCHLD,sigchild)==SIG_ERR){
        perror("signal");
        return -1;
    }
    //创建多个子进程
    for(int i = 0;i < 5;i++){
        pid_t pid = fork();
        if(pid == -1){
            perror("fork");
            return -1;
        }
        if(pid == 0){
            printf("%d进程:我是子进程\n",getpid());
            //sleep(i + 1);  //一个一个死,17号信号一个一个来,都收到
            sleep(1); //同时来5个不可靠的17号信号,只收到2个,其余丢弃(信号屏蔽uc_11)
                      //所以顶部代码中,每次17->调用信号处理函数,就循环多次收尸。
            return 0;//!!!!!!! 子进程代码勿忘return 0;
        }
    }
    //创建老六
    pid_t oldsix = fork();
    if(oldsix == -1){
        perror("oldsix");
        return -1;
    }
    if(oldsix == 0){
        printf("%d进程:我是老六,就不结束\n",getpid());
        sleep(15);
        return 0;
    }
    
    //父进程该干啥就干啥
    for(;;);
    return 0;
}
//编译执行

        编译执行,结果如下,好好理解下~

        uc_10_信号_第2张图片

        综上,上述最完美代码解决了以下痛点:

        为了避免子进程同时死,同时发送多个17,信号丢失,处理方式是每次收到17号信号,调用信号处理函数时都循环多次收尸;

        为了避免子进程老也不死,不建议父进程wait()阻塞干等着,而是用waitpid(),不耽误父进程干别的事。

2.3  信号发送

        信号发送,共3种方式:kill命令、kill()、raise()。

2.3.1  kill命令

        kill  [-信号]  PID   //在终端手动敲kill命令

        若不指明具体信号,缺省发送SIGTERM(15)信号(可被捕获或忽略的进程终止信号)。

        若要指明具体信号,可用信号编号,也可用信号名称,且前缀SIG可省略不写。如:

                kill -9 1234                             //终止,终止1个进程

                kill -SIGKILL 1234 5678        //终止,终止多个进程

                kill -KILL -1                            //终止,终止所有进程

        root用户可以发给任何进程,普通用户只能发给自己的进程。

2.3.2  kill()

        #include

        int kill(pid_t pid,  int signum);

                功能:向指定的进程发送信号  //注意,进程死后,进程仍存在,是僵尸进程,需收尸

                pid:可以取以下值

                        -1:向系统中的所有进程发送信号

                        >0:向特定进程(由PID标识)发送信号

                signum:信号编号,取0可用于检查pid进程是否存在,

                                如不存在,kill()返回-1,且errno为ESRCH

                返回值:成功(至少发出去一个信号)返回0,失败返回-1 

//kill.c  kill()函数演示
#include
#include
#include
#include
#include

int main(void){
    //父进程创建子进程
    pid_t pid = fork();
    if(pid == -1){
        perror("fork");
        return -1;
    }
    //子进程执行
    if(pid == 0){
        printf("%d进程:我是子进程\n",getpid());
        for(;;);
        return 0;
    }
    //父进程给子进程发送信号
    getchar();
    if(kill(pid,SIGINT) == -1){
        perror("kill");
        return -1;
    }
    getchar();
    if(kill(pid,0) == -1){
        printf("子进程不存在\n");
    }else{
        printf("子进程存在\n"); //子进程通过kill死了,子进程仍存在,是尸体,需收尸
    }
    
    if(wait(NULL) == -1){
        perror("wait");
        return -1;
    }

    getchar();
    if(kill(pid,0) == -1){
        printf("子进程不存在\n");
    }else{
        printf("子进程存在\n");
    }
    return 0;
}

2.3.3  raise()

        #include

        int raise (int signum); 

                功能:向调用进程自己发送信号

                signum:信号编号

                返回值:成功返回0,失败返回非0  

        本函数等效于:

         kill (getpid(), signum);

3  信号处理的继承和恢复.

        fork()函数创建的子进程会继承父进程的信号处理方式:

                父进程中被忽略的信号,在子进程中依然被忽略。

                父进程中被捕获的信号,在子进程中依然被捕获。

        exec家族函数创建的新进程对信号的处理方式和原进程稍有不同:

                原进程中被忽略的信号,在新进程中依然被忽略。

                进程中被捕获的信号,在进程中被默认处理。

       

        代码验证:编写代码,父子进程都无限循环,父进程忽略2号,捕获3号,编译执行。

                          另起终端窗口,kill  -2  25543命令,ps aux查看。

                          验证结束,kill -9 25543杀死。

uc_10_信号_第3张图片

        

你可能感兴趣的:(uc,unix)