Linux系统编程之信号进阶

本章节继续介绍关于Linux系统下信号相关的知识点,介于之前介绍了信号的基础,了解了信号的产生、缘由以及工作流程,这里就对信号的相关函数进行统一的分析,之后在用实例进行说明。

信号产生函数

kill

kill 函数可以给一个特定的进程发送指定的信号

#include <signal.h>

int kill(pid_t pid, int sig) 

pid > 0     sig发送给ID为pid的进程 
pid == 0    sig发送给与发送进程同组的所有进程 
pid < 0     sig发送给组ID为|-pid|的进程,并且发送进程具有向其发送信号的权限 
pid == -1   sig发送给发送进程有权限向他们发送信号的系统上的所有进程 

sig为发送的信号值,当sig为0时,用于检测特定为pid进程是否存在,如不存在,返回-1。

raise

raise函数可以给当前进程发送指定的信号(自己也可以给自己发送信号)

#include <signal.h>

int raise(int sig);

abort

abort函数通过向当前进程发送信号值 SIGABRT,使当前进程接收到信号而异常终止,但是abort会认为进程不安全。

#include <stdlib.h> 

void abort(void);

类似于exit函数一样,abort函数总是成功的,因此没有返回值。

alarm

由软件条件产生信号,进程可以通过调用alarm向它自己发送SIGALRM信号

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

参数seconds:alarm函数安排内核在seconds秒内发送一个SIGALRM信号给调用进程,如果soconds等于0,那么不会调度新的闹钟(alarm)

返回值:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0

在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

setitimer

现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

#include <sys/time.h>

int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:

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

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

ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIRTUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

定时器中的参数value用来指明定时器的时间,其结构如下:

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

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

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

在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

EFAULT:参数value或ovalue是无效的指针。
EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>

int sec;
void sigroutine(int signo) {

    switch (signo) {
        case SIGALRM:
            printf("Catch a signal -- SIGALRM\n");
            break;
        case SIGVTALRM:
            printf("Catch a signal -- SIGVTALRM\n");
            break;
    }
    return;

}

int main()
{
    struct itimerval value,ovalue,value2;
    sec = 5;
    printf("process id is %d\n",getpid());

    signal(SIGALRM, sigroutine);
    signal(SIGVTALRM, sigroutine);

    value.it_value.tv_sec = 1;
    value.it_value.tv_usec = 0;
    value.it_interval.tv_sec = 1;
    value.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &value, &ovalue);

    value2.it_value.tv_sec = 0;
    value2.it_value.tv_usec = 500000;
    value2.it_interval.tv_sec = 0;
    value2.it_interval.tv_usec = 500000;
    setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

    for (;;) ;
}

程序运行结果如下:

$ ./app 
process id is 104501 
Catch a signal -- SIGVTALRM
Catch a signal -- SIGALRM
Catch a signal -- SIGVTALRM
Catch a signal -- SIGVTALRM
Catch a signal -- SIGALRM
Catch a signal -- SIGVTALRM
Catch a signal -- SIGVTALRM
...

sigqueue

sigqueue是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction配合使用,后续详细介绍。

信号集

信号集被定义为一种数据类型:

typedef struct {

    unsigned long sig[_NSIG_WORDS];

} sigset_t

信号集用来描述信号的集合,每个信号占用一位。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。

sigset_t 信号集

下面是为信号集 sigset_t 操作定义的相关函数:

int sigemptyset(sigset_t *set)  初始化由set指定的信号集,信号集里面的所有信号被清空;
int sigfillset(sigset_t *set)   调用该函数后,set指向的信号集中将包含linux支持的64种信号;
int sigaddset(sigset_t *set, int signo) 在set指向的信号集中加入signum信号;
int sigdelset(sigset_t *set, int signo) 在set指向的信号集中删除signum信号;
int sigismember(const sigset_t *set, int signo) 判定信号signum是否在set指向的信号集中。 

信号集模型

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解 为阻塞而不是忽略。下图展示了PCB中信号集模型:

  1. PCB进程控制块中有信号屏蔽状态字(block),信号未决状态字(pending)还有是否忽略标识
  2. 信号屏蔽状态字(block):1代表阻塞,0代表不阻塞;信号未决状态字(pending):1代表未决,0代表信号递达
  3. 向进程发送SIGINT,内核首先判断信号屏蔽状态字是否阻塞,如果信号屏蔽状态字阻塞,信号未决状态字(pengding)相应位置1;
    若阻塞解除,信号未决状态字(pending)相应位置0,表示信号可以递达了。
  4. block状态字,pending状态都是64bit,分别代表Linux系统中的64个信号。例如SIGINT是2号信号,对应block状态字中的第二位
  5. block状态字用户可以读写,pending状态字用户只能读,这是新号的设计机制。

内核将信号传递到PEND集合中,置位相应的未决态1,之后,经过用户设置过的阻塞信号集屏蔽字处理,如果信号被阻塞,那么未决集内对应位还是1,如果没有阻塞,信号成为递达,经过handler处理,默认、忽略以及捕捉,此时未决集中对应的标志位转为0.

信号集操作函数

在信号集中,用户可通过sigprocmask设置阻塞信号集,通过sigpending获取未决态信号集

sigprocmask

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

#include <signal.h>

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

成功返回0,出错返回-1

如果oset是非空指针,则读取进程的当前信号屏蔽状态字通过oset参数传出,如果set是非空指针,则更改进程的信号屏蔽状态字,参数how控制如何更改。

如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

参数 how 的含义

--SIG_BLOCK    set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set(位或运算)
--SIG_UNBLOCK    set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask^set(位异或运算)
--SIG_SETMASK    设置当前信号屏蔽字为set所指向的值,相当于mask=set

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

sigpending

sigpending函数用来读取当前进程的信号未决集,通过set参数传出。

#include <signal.h>

int sigpending(sigset_t *set);

成功返回0,出错返回-1。

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

信号集代码示例

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

void printsigset(const sigset_t *set){
    int i;
    for(i =1; i<32;i++)
    {
        if(sigismember(set, i)==1)
            putchar('1');
        else
            putchar('0');
    }
    puts("");
}

int main(void){

    sigset_t s,p;
    int i = 0;
    //  清空信号集
    sigemptyset(&s);
    // 捕捉信号屏蔽字
    sigaddset(&s, SIGINT); // ctrl + c
    sigaddset(&s, SIGQUIT); // ctrl + z
    sigaddset(&s, SIGTSTP); // ctrl + \
    // 补充设置部分信号屏蔽字
    sigprocmask(SIG_BLOCK, &s, NULL);
    while(1)
    {
        // 获取未决信号集
        sigpending(&p);
        printsigset(&p);
        if (i==10)
        {
            // 解除信号屏蔽字 SIGQUIT
            sigdelset(&s, SIGQUIT);
            sigprocmask(SIG_UNBLOCK, &s, NULL);
        }
        sleep(1);
        i++;
    }
    return 0;
}

注意,SIGKUILL 和 SIGSTOP 两个信号屏蔽字 无法捕捉、阻塞以及忽略。

程序执行结果:

Allies:signal rememberme$ ./sigprocmask
  0000000000000000000000000000000
^Z0000000000000000010000000000000
  0000000000000000010000000000000
^C0100000000000000010000000000000
^\0110000000000000010000000000000
  0110000000000000010000000000000
  ...

信号捕捉

sigaction

注册信号捕捉函数sigaction,POSIX标准定义了sigaction函数,它允许像Linux和Solaris这样与POSIX兼容的系统上的用户,明确地指出它们想要的信号处理语义。

sigaction函数可以读取或者指定信号相关联的处理动作,signal与其功能类似,但signal是标准C的信号接口,对不同的操作系统有不同的行为,所以一般尽量不使用signal,取而代之的是sigaction函数。函数原型如下:

#include <signal.h>

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

signum: 指定信号的编号(利用kill -l命令可以查看);
*act: 若act指针非空,则根据act修改该信号的处理动作;
*oldact: 若oldact指针非空,则通过oldact传出该信号原来的处理动作;

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

对于其中的结构体 struct sigaction 的定义如下:

struct sigaction {
    void void sigset_t int
    void
    (*sa_handler)(int);
    (*sa_sigaction)(int, siginfo_t *, void *);
    sa_mask;
    sa_flags; (*sa_restorer)(void);
};

sa_handler : 早期的捕捉函数,SIG_DEF(默认),SIG_IGN(忽略),自定义函数指针
sa_sigaction : 新添加的捕捉函数,可以传参 , 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数 
sa_mask : 在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的 阻塞信号集
sa_flags : SA_SIGINFO 或者 0
sa_restorer : 保留,已过时

捕捉代码示例

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

// 捕捉函数处理,接受捕捉的信号值
void sigHandler(int signum){
    printf("\nsigHandler: %d\n",signum);
}

int main(void){

    // 声明结构体 sigaction
    struct  sigaction sa;
    // 使用 sa_handler 函数指针捕捉处理,参数 0
    sa.sa_flags = 0;
    // 设置函数指针指向函数体
    sa.sa_handler = sigHandler;
    // sa.sa_handler = SIG_DFL;
    // sa.sa_handler = SIG_IGN;
    // 清空函数捕捉时的 mask 值
    sigemptyset(&sa.sa_mask);
    // 注册捕捉函数
    sigaction(SIGINT, &sa, NULL);

    while(1){
        printf("......\n");
        sleep(1);
    }
    return 0;
}

程序运行结果:

Allies:signal rememberme$ ./sigaction
......
......
......
^C
sigHandler: 2
......
......
^C
sigHandler: 2
......
......

注意:子进程继承了父进程的信号屏蔽字和信号处理动作。

库函数 signal 原型如下:

typedef void (*sighandler_t)(int)

sighandler_t signal(int signum, sighandler_t handler)

int system(const char *command) 
集合fork,exec,wait一体

信号捕捉传参

通常的sigaction结构体中 sa.sa_flags 设置为 时,信号捕捉只能够得到捕捉的信号值,然而向进程本身发送信号,并传递指针参数则需要修改标志位 sa.sa_flags 为 SA_SIGINFO。发送信号时候就用之前提到的函数 sigqueue 添加而外信息。先来回顾一下信号捕捉流程

sigqueue 函数原型

sigqueue 功能和 kill 相同,都可以向制定进程发送信号,其次还可以额外夹杂数据。

#include <sys/types.h>
#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

调用成功返回 0;否则,返回 -1。

sigqueue 是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

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

sigqueue 比kill 传递了更多的附加信息,但 sigqueue 只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用 sigqueue 时,sigval_t 指定的信息会拷贝到对应 sig 注册的3参数信号处理函数的 siginfo_t 结构中,这样信号处理函数就可以处理这些信息了。由于 sigqueue 系统调用支持发送带参数信号,所以比 kill 系统调用的功能要灵活和强大得多。

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)
{
    struct sigaction act;  
    union sigval mysigval;
    int i;
    int sig;
    pid_t pid;         
    char data[10];
    memset(data,0,sizeof(data));

    for(i=0;i < 5;i++)
            data[i]='2';

    mysigval.sival_ptr=data;
    sig=atoi(argv[1]);

    pid=getpid();

    sigemptyset(&act.sa_mask);
    act.sa_sigaction=new_op;//三参数信号处理函数
    act.sa_flags=SA_SIGINFO;//信息传递开关,允许传说参数信息给new_op

    if(sigaction(sig,&act,NULL) < 0)
    {
            printf("install sigal error\n");
    }

    while(1)
    {
            sleep(2);
            printf("wait for the signal\n");
            sigqueue(pid,sig,mysigval);//向本进程发送信号,并传递附加信息
    }
}

void new_op(int signum,siginfo_t *info,void *myact)//三参数信号处理函数的实现
{
    int i;
    for(i=0;i<10;i++)
    {
            printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));
    }
    printf("handle signal %d over;",signum);
}

程序运行结果

$ ./sigqueue 7
wait for the signal
S
S
S
S
S
handle signal 7 over;wait for the signal
S
S
S
S
S
handle signal 7 over;wait for the signal
S
S
...

sigqueue 也可以夸进程通信,但是其传递的数据一半以整型等具体的基本数据。不同进程间收发信号,不在同一地址空间,不适合传地址。

SIGCHLD 信号处理

产生条件

SIGCHLD的产生条件有一下三点:

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

处理方式

父进程回收子进程时,对status的处理方式

pid_t waitpid(pid_t pid, int *status, int options) 

options
    1. WNOHANG 没有子进程结束,立即返回
    2. WUNTRACED   如果子进程由于被停止产生的SIGCHLD, waitpid则立即返回
    3. WCONTINUED  如果子进程由于被SIGCONT唤醒而产生的SIGCHLD, waitpid则立即返回

获取status 
    1. WIFEXITED(status) 子进程正常exit终止,返回真 WEXITSTATUS(status)返回子进程正常退出值
    2. WIFSIGNALED(status) 子进程被信号终止,返回真
    3. WTERMSIG(status)    返回终止子进程的信号值 
    4. WIFSTOPPED(status)  子进程被停止,返回真 WSTOPSIG(status)返回停止子进程的信号值
    5. WIFCONTINUED(status)    子进程由停止态转为就绪态,返回真

SIGCHLD 代码示例

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

void do_sig(int n){
    int status;
    pid_t pid;
    // 收到子进程发送的 SIGCHILD 信号,捕捉函数至此
    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 received signal %d\n",pid,WTERMSIG(status));

    }
}

int main(void){

    pid_t pid;
    int i;
    // 父子进程均设置 SIGCHLD 信号屏蔽字,方便捕捉
    sigset_t newmask,oldmask;
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGCHLD);
    // 修改信号屏蔽字
    sigprocmask(SIG_BLOCK,&newmask,&oldmask);
    // fork子进程5个,全部捕捉 SIGCHLD 信号值
    for (i = 0; i < 5; i++)
    {
        if((pid = fork() )== 0){
            break;
        }
        else if(pid < 0){
            perror("fork error!");
            exit(1);
        }
    }
    // 子进程 10 次循环打印, 10s 后结束
    if(pid == 0){
        int n = 10;
        while(n--){
            printf("child ID %d\n", getpid());
            sleep(1);
        }
        return i;
    } else if(pid > 0){
        // 父进程添加捕追函数
        struct sigaction act;
        act.sa_handler = do_sig;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        // 注册信号捕捉函数
        sigaction(SIGCHLD,&act, NULL);
        // 恢复初始信号屏蔽字的状态
        sigprocmask(SIG_SETMASK,&oldmask,NULL);
        while(1){
            printf("Parent ID %d\n",getpid());
            sleep(5);
        }
    }
    return 0;
}

程序运行结果:

...
child ID 1421
child ID 1422
child ID 1423
child ID 1420
Parent ID 1418
child ID 1421
child ID 1422
child ID 1419
child 1423 exit 4
child 1422 exit 3
child 1421 exit 2
child 1420 exit 1
child 1419 exit 0
Parent ID 1418
...

此时通过 kill -9 ? 向指定的子进程(?)发信号杀死子进程,那么打印log便会将指定的子进程退出信号值打印出来(9)。

总结

本章节继前面章节,系统从函数方面阐述了信号的发送,拦截以及捕捉。对于父子进程而言,信号屏蔽字,信号集等在父子进程中都会的到继承,因此再处理父子进程间信号时,需要优先设置相应的信号屏蔽字后,再fork子进程,然后在父进程中进行监听。

参考链接1
参考链接2

邢文鹏Linux系统教学资料

坚持原创技术分享,您的支持将鼓励我继续创作!