APUE读书笔记---第10章 信号

APUE读书笔记—第10章 信号

1. 信号概念

  • 信号是软件中断,信号提供了一种处理异步事件的方法。
  • 信号名在 < signal.h > 都被定义为正整数常量
  • 不存在编号为0的信号。

1.1 产生信号的条件:

  1. 当用户按某写终端按键时,引发终端产生的信号。如:Ctrl+C产生SIGINT信号。
  2. 硬件异常产生信号。如除零错,无效的内存应用产生SIGSEGV信号。
  3. 进程调用kill函数可将任一信号发送给另一个进程或进程组。
  4. 用户可用kill命令将信号发送给其他进程。
  5. 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。如SIGPIPE在管道的读进程已终止后,一个进程写此管道。

1.2 信号的处理

  • 忽略此信号。SIGKILL和SIGSTOP不能被忽略
  • 捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数
  • 执行默认的动作。Linux对每种信号都规定了默认操作。默认的动作共分为5种情况:(man 7 singal)
    • Term表示终止当前进程
    • Core表示终止当前进程并且Core Dump(Core Dump 用于gdb调试).
    • Ign表示忽略该信号.
    • Stop表示停止当前进程.
    • Cont表示继续执行先前停止的进程.

2. 信号集处理函数

2.1 信号集(signal set)

一个能表示多个信号的数据类型。

#include <signal.h>

int sigemptyset(sigset_t *set);
//初始化由set指向的信号集,清除其中所有信号
int sigfillset(sigset_t *set);
//初始化由set指向的信号集,使其包括所有信号。
int sigaddset(sigset_t *set, int signum);
//将一个信号添加到已有的信号集
int sigdelset(sigset_t *set, int signum);
//从信号集删除一个信号
//4个函数返回值:若成功,返回0,若出错,返回-1

int sigismember(const sigset_t *set, int signum);
//测试signum是否在set信号集中
//返回值:若真,返回0,若假,返回0

2.2 函数sigprocmask

调用sigprocmask可以检测或更改或同时进行检测和更改进程的信号屏蔽字。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//返回值:若成功,返回0,若出错,返回-1
  • oldset是非空指针,那么进程的当前信号屏蔽字通过oldset返回。
  • set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
  • sigprocmask函数仅为单线程进程定义。
how说明
SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

2.3 函数sigpending

sigpending函数返回一个信号集,对于调用进程而言,其中的各信号是阻塞不能传递的,因而也一定是当前未决的。

#include <signal.h>

int sigpending(sigset_t *set);
//返回值:若成功,返回0,若出错,返回-1

2.4 信号集处理函数的例子

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

void printsigset(const sigset_t *set)
{
    int i;
    for(i = 1; i < 32; i++){
        if(sigismember(set, i) == 1){   //测试set的第i为是否为1
            putchar('1');   
        }else{
            putchar('0');   
        }   
    }
    puts("");   //换行
}

int main()
{
    sigset_t s, p;
    int i = 0;
    sigemptyset(&s);             //init
    sigaddset(&s, SIGINT);       //add SIGINT to sigset
    sigaddset(&s, SIGQUIT);         //add SIGQUIT to sigset
    sigprocmask(SIG_BLOCK, &s, NULL);   //block SIGINT 
    printf("64bit sigset_t类型的大小为:%ld\n", sizeof(s));

    while(1){
        sigpending(&p);         //get pending signals
        printsigset(&p);        //print pending sigset
        if(i == 10){
            sigdelset(&s, SIGQUIT); //删除SIGQUIT=0信号,只有SIGINT=1信号
            sigprocmask(SIG_UNBLOCK, &s, NULL);//不阻塞SIGINT信号,进程被SIGQUIT信号结束
        }
        i++;
        sleep(1);
    }

    return 0;
}

首先构造一个信号屏蔽字,然后添加要阻塞的信号,读出当前进程的未决信号集,调用sigprocmask函数更改进程的信号屏蔽字
运行结果:

➜  SIGNAL ./sigset 
64bit sigset_t类型的大小为:128
0000000000000000000000000000000
0000000000000000000000000000000     //未决信号集为空。
^C0100000000000000000000000000000   //Ctrl + C 发送SIGINT信号,SIGINT信号被阻塞,并打印未决信号集,下标为1的位被置1
0100000000000000000000000000000
0100000000000000000000000000000
^\0110000000000000000000000000000   //Ctrl + \ 发送SIGQUIT信号,SIGQUIT信号被阻塞,并打印未决信号集,下标为2的位被置1
0110000000000000000000000000000
0110000000000000000000000000000
0110000000000000000000000000000
0110000000000000000000000000000
0110000000000000000000000000000
//10s之后SIGQUIT信号被清除,SIGINT进程被阻塞,进程因SIGQUIT信号而退出

3. 信号捕捉设定

函数sigaction的功能是检查、修改与指定信号相关联的处理动作

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//返回值:若成功,返回0,若出错,返回-1
  • 参数signo是要检测或修改其具体动作的信号编号。
  • oldact返回信号的上一个动作,act指定要修改的动作。

sigaction的结构如下:

struct sigaction {
    void     (*sa_handler)(int);    //捕捉函数
    void     (*sa_sigaction)(int, siginfo_t *, void *); //新添加的捕捉函数,可以传参 , 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数
    sigset_t   sa_mask; //在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的
阻塞信号集
    int        sa_flags;    //SA_SIGINFO 或者 0
};

3.1 信号捕捉例子

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

void do_sig(int num)
{
    int n = 5;
    printf("I am do_sig\n");
    while(n--) {
        printf("num = %d\n", num);
        sleep(1);
    }
}

int main()
{
    struct sigaction act;

//  act.sa_handler = SIG_DFL;   //SIG_DFL执行默认动作
//  act.sa_handler = SIG_IGN;   //SIG_IGN执行忽略动作
    act.sa_handler = do_sig;

    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT);
    act.sa_flags = 0;

    sigaction(SIGINT, &act, NULL);
    while(1){
        printf("**********\n"); 
        sleep(1);
    }

    return 0;
}

运行结果:

➜  SIGNAL ./sigaction
**********
^CI am do_sig   //Ctrl + C 发送SIGINT信号
num = 2
^C^C^Cnum = 2   //连续多次发送SIGINT信号
^\num = 2       //Ctrl + \ 发送一次SIGQUIT信号
num = 2
num = 2
I am do_sig
num = 2
num = 2
num = 2
num = 2
num = 2
[1]    1485 quit (core dumped)  ./sigaction

程序分析

  1. 定义一个sigaction的变量,并且将信号捕捉函数的首地址赋值给sa_handler成员。
  2. 初始化一个信号集,并且添加SIGQUIT信号,因此如果在执行捕捉函数时会阻塞SIGQUIT信号,并设置捕捉的信号为SIGINT。
  3. 当程序运行起来,当按下Ctrl+C发送一个SIGINT信号时,系统从用户态进入内核态,当内核处理完异常
    处理递送的信号时,需要进入用户态调用捕捉函数
  4. 在调用捕捉函数的执行n–的5秒期间,又发送三次SIGINT信号和一次SIGQUIT信号,但是在执行捕捉函数的期间因为之前设置的信号集会阻塞这些信号。
  5. 当执行完捕捉函数后,会执行特殊的系统调用sigreturn,再次陷入内核态,然后又从内核态返回上次被中断的地方(while(1)的死循环)继续执行,接着信号屏蔽字会恢复为原先值。
  6. 因为信号屏蔽字的恢复,上次阻塞的信号再次引发中断,但当同一个信号多次发生,通常不会加入队列当被解除阻塞后,信号处理函数只被调用一次。因此,在执行完第一个I am do_sig后即使多次发送SIGINT但也只会在执行一次I am do_sig。
  7. 当执行完第二次SIGINT后,又会以相同的方式处理SIGQUIT信号,因为信号集恢复为原先值,因此,进程被SIGQUIT信号终止。

4. C标准库信号处理函数

4.1 signal函数

signal函数由ISO C定义,不最好使用sigaction代替该函数。

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//返回值:若成功,返回以前的信号处理配置,若出错,返回SIG_ERR
  • 第一个参数指定信号的值,第二个参数指定针对前面信号值的处理handler可以是SIG_IGN(忽略该信号)、SIG_DFL(系统默认动作)或者是接到此信号后要调用的函数地址。我们称这种处理为捕捉该信号,称此函数为信号处理程序信号捕捉函数
  • 如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR

4.2 signal函数的使用

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

void do_sig(int n)
{
    printf("SIGINT\n");
}

int main(void)
{
    signal(SIGINT, do_sig);

    while(1) {
        printf("**************\n");
        sleep(1);
    }

    return 0;
}

运行结果:

➜  SIGNAL ./signal 
**************
**************
^CSIGINT        //Ctrl + C 发送SIGINT信号
**************
**************
^CSIGINT        //Ctrl + C 发送SIGINT信号
**************
^\[1]    12618 quit (core dumped)  ./signal //发送SIGQUIT退出程序

4.2system函数

system函数执行一条shell命令

#include <stdlib.h>

int system(const char *command);

5.可重入函数

进程捕捉的信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时终端,它首先执行该信号处理程序中的指令,如果从信号处理程序返回,则继续执行在捕捉的信号时进程正在执行的正常指令序列。但是在返回控制时不会出现任何错误,就叫可重入函数。而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

例如:

strtok_r函数

strtok_r函数是strtok函数的可重入版本,并且线程安全。用来分解一个字符串为一组字符串。

#include <string.h>

char *strtok(char *str, const char *delim);

char *strtok_r(char *str, const char *delim, char **saveptr);
  • str是被分割的字符串地址
  • delim函数的为分隔符字符串地址
  • saveptr是指向char *类型的指针变量,用来保存strtok_r内部保存切分时的上下文,以应对连续调用分解相同源字符串。
#include <stdio.h>
#include <string.h>

int main(void)
{
    char str[] = "everthing will be OK!";

    char *saveptr = str, *p;

    while((p = strtok_r(saveptr, " ", &saveptr)) != NULL)
        printf("%s\n", p);

    return 0;
}

运行结果:

SIGNAL ./strtok_r 
everthing
will
be
OK!

可重入函数见man 7 signal

6. 时序竟态

#include <unistd.h>
int pause(void);
//返回-1,并设置errno
  • pause函数使调用进程挂起,直到有信号递达,如果递达信号是忽略,则继续挂起。

pause函数可以在逻辑上实现简易的sleep函数

1. signal(SIGALRM, do_sig);
2. alarm(n);
3. pause();

先设置一个SIGALRM信号,当alarm定时了n秒之后,pause接收到SIGALRM信号,然后返回。
但是,这存在着BUG,因为形成了竟态条件(race condition),会涉及时序竟态的问题。

竞态条件(race condition),从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。

当程序执行完第二步alarm函数,此时如果有优先级更高的进程抢占了CPU的使用权,此时该进程就会处于就绪状态,当该进程被拉起时,n秒已经过去,SIGALRM信号已经丢失,那么该进程则会一直处于pause状态。

为了使信号不丢失,就可是设置信号屏蔽字阻塞。

1. signal(SIGALRM, do_sig);
2. 阻塞SIGALRM
3. alarm(n);
4. 解除阻塞SIGALRM
5. pause();

但是在第4和第5步中间,仍然会形成竟态条件。因此需要一个原子操作先恢复信号屏蔽字,然后使进程休眠。而sigsuspend函数提供此功能,因此,就有了sigsuspend函数。

#include <signal.h>
int sigsuspend(const sigset_t *mask);
//返回-1,并将errno设置为EINTR

sigsuspend函数的行为如下(原子操作):

  1. 通过参数mask解除对某个信号的屏蔽
  2. 挂起进程
  3. 当被信号唤醒,sigsuspend返回,进程的信号屏蔽字恢复为原来的值。

6.1 实现sleep函数

使用sigaction代替signal,使用sigsuspend代替pause

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

void sig_arm() { } 

unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    //设置信号捕捉函数,保存之前信号动作
    newact.sa_handler = sig_arm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    //阻塞SIGALRM信号,保存当前信号屏蔽字
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);

    alarm(nsecs);

    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);
    //参数suspmask设置为取消阻塞SIGALRM信号,保证sigsuspend一定不阻塞SIGALRM,并且休眠进程,同时等待信号发生
    sigsuspend(&suspmask);

    //设置未睡眠够秒数,恢复之前的信号动作
    unslept = alarm(0); 
    sigaction(SIGALRM, &oldact, NULL);

    //进程恢复为原来的信号屏蔽字
    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return unslept;
}

int main(void)
{
    int n = 5;
    while(n--) {
        mysleep(1); 
        printf("1 sec passed\n");
    }
}

运行结果:

➜  SIGNAL ./mysleep1
1 sec passed
1 sec passed
1 sec passed
1 sec passed
1 sec passed

7. SIGCHLD信号处理

7.1 SIGCHLD信号的产生条件

  1. 子进程终止时
  2. 子进程接收到SIGSTOP信号停止时
  3. 子进程处在停止态,接受到SIGCONT后唤醒时

7.2 使用信号机制通知父进程回收子进程

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>

void do_sig_child(int signo)
{
    pid_t pid;
    int status;
    while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status))
            printf("child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status)) 
            printf("child %d signal %d\n", pid, WTERMSIG(status));
    }
}

int main(void
{
    pid_t pid;
    int i;
    for(i = 0; i < 10; i++) {
        if ((pid = fork()) == 0) {
            break;  
        } else if (pid < 0) {
            perror("fork err"); 
            exit(1);
        }   
    }

    if(pid == 0) {
        int n = 8;
        while(n--) {
            printf("child ID:%d\n", getpid());
            sleep(1);
        }
    } else if (pid > 0) {
        struct sigaction act;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);

        while(1) {
            printf("Parent ID:%d\n\n\n", getpid()); 
            sleep(1);
        }   
    }

    return 100;
}
//WIFEXITED(status):子进程正常exit终止,返回真
//WEXITSTATUS(status)返回子进程正常退出值
//WIFSIGNALED(status):子进程被信号终止,返回真
//WTERMSIG(status)返回终止子进程的信号值

运行结果:

child ID:25348
child ID:25347
child ID:25349
child 25340 exit 100    //子进程退出值为进程return 的 100
child 25345 exit 100
child 25346 exit 100
Parent ID:25339


child 25343 exit 100
child 25341 exit 100
Parent ID:25339


child 25344 exit 100
Parent ID:25339


child 25342 exit 100
Parent ID:25339


child 25347 exit 100
child 25348 exit 100
Parent ID:25339


child 25349 exit 100
Parent ID:25339


Parent ID:25339
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值