APUE学习:第十章信号

信号以SIG开头

eg:

SIGABRT——夭折信号,进程调用abort时产生这种信号

SIGLARM——闹钟信号,alarm设置的定时器超时后产生

信号在头文件 signal.h中定义

信号产生的条件:

1.用户按某些终端键

eg:Ctrl+C产生中断信号(SIGINT)

2.硬件异常产生信号:除数为0、无效的内存引用等。

3.进程调用kill函数将任意信号发送给另一个进程或进程组

4.用户用kill命令(shell)将信号发送给其他进程

5.当软件条件发生时,产生信号

eg:

在网络上传来带外的数据——SIGURG

对于信号的处理:

1.忽略

两种信号不能被忽略:SIGKILL、SIGSTOP,它们为内核和超级用户提供了使进程终止或停止的可靠方法 ,硬件信号也不行

2.捕捉

SIGKILL和SIGSTOP不能捕捉

3.执行系统默认动作

终止+core  这个core是什么意思???

表示在当前工作目录的core文件中复制了该进程的内存映像。

函数signal

 

signp参数是图10-1中的信号名;

func值有三种选择:

SIG_IGN——表示忽略此信号(SIGKILL和SIGSTOP信号不能忽略)

SIG_DFL——表示收到信号后执行系统默认动作

函数地址——当收到信号后执行此函数

代码示例: 

#include"apue.h"

static void sig_usr(int);/*信号处理函数*/

int main()
{
    if(signal(SIGUSR1,sig_usr)==SIG_ERR)
        err_sys("can't catch SIGUSR1");
    if(signal(SIGUSR2,sig_usr)==SIG_ERR)
        err_sys("can't catch SIGUSR2");
    for(;;)
        pause();
    
}

static void sig_usr(int signo)
{
    if(signo==SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo==SIGUSR2)
        printf("received SIGUSR2\n");
    else 
        err_dump("received signal %d\n",signo);
}

输出结果:

可以调用kill命令将信号发送给进程

kill -USR1 5106 表示给5106发送USR1信号 

kill 5106向该进程发送SIGTERM(用于请求进程正常终止)

进程用exec启动程序文件,新的程序文件会将所有信号都设置为它们的默认动作,除非新的程序忽略这些信号。

一个进程调用fork时,其子进程继承父进程的信号处理方式

不可靠的信号(远古时代的宝贝)

可重入函数

在信号处理程序中要保证调用安全的函数。

如果你不确定一个函数是不是可重入的可以问问chatGPT

可靠信号术语和语义

信号递送:

当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。

在信号产生和递送之间的时间间隔内,称信号是未决的。

函数kill和raise

pid对应4种情况:

pid>0——发送给进程ID为pid的进程

pid==o——发送给属于同一进程组的所有进程(有权限的前提下)

pid<0——发送给进程组ID等于pid绝对值的所有进程

pid==-1 发送给发送进程有权限发送的所有进程 

上述四种情况的所有进程不包括系统进程集(不包括现定义的系统进程集,包括内核进程和init进程)中的进程。

权限问题:

1.超级用户可将信号发送给任一进程

2.非超级用户,发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID

3.若发送的信号为SIGCONT,则可将它发送给属于同一会话的任一其他进程

kill(pid,0);

signo为0表示空信号,用来判断进程ID为pid的进程是否存在。

如果向一个并不存在的进程发送空信号,则kill返回-1,errno被设置为ESRCH

注意:kill函数不是原子操作,等函数返回时,原本存在的进程可能已经终止,所以并无多大价值.

函数alarm和pause

注意:可以用signal函数捕捉闹钟到时的SIGALRM信号,但是signal函数一定要在闹钟到时之前调用,否者程序会直接终止。

注意:如果没有设置信号处理程序,则进程会执行信号的默认动作,然后pause函数返回。

用alarm、pause函数实现有缺陷的sleep函数,代码如下:

#include"apue.h"
#include<signal.h>
#include<unistd.h>
static void sig_alarm(int signo)
{
    /*nothing to do,just return to wake up the pause*/
}
unsigned int sleep1(unsigned int seconds)
{
    if(signal(SIGALRM,sig_alarm) == SIG_ERR)
        return seconds;
    alarm(seconds);
    pause();
    return alarm(0);/*turn off timer*/
        
}
int main()
{
    sleep1(5);
    return 0;
}

存在以下3个问题:

 1.在调用sleep1之前已经设置了闹钟,则它会被sleep1中第一次调用alarm时移除

2.修改了对SIGALRM的配置,sleep1函数对SIGALRM信号进行捕获并执行响应程序

3.第一次执行alarm和pause有竞争条件,如果执行alarm(seconds)后进程进行了切换,且在pause()执行之前时间结束,则进程会结束。

对sleep1的改良这里不作赘述

信号集

定义:

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

sigismember测试一个指定的位

函数sigprocmask

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

oset保存旧的信号屏蔽字

eg:

sigprocmask(SIG_BLOCK,&newmask,&oldmask);

将newmask和旧信号屏蔽字的并集作为进程的新信号屏蔽字,将旧信号屏蔽字存入oldmask

可以用sigprocmask函数来获取进程的屏蔽字集合

sigset_t sigset;
sigprocmask(0,NULL,&sigset);

进程的屏蔽字集合存入sigset

 打印调用进程信号屏蔽字中的信号名  代码示例:

#include"apue.h"
#include<errno.h>
void pr_mask(const char * str)
{
    sigset_t sigset;
    int errno_save;/*用来保存errno值*/
    errno_save = errno;
    if(sigprocmask(0,NULL,&sigset)<0)
        err_ret("sigprocmask error");
    else
    {
        printf("%s",str);
        if(sigismember(&sigset,SIGINT))
        printf("SIGINT");
        if(sigismember(&sigset,SIGQUIT))
        printf("SIGQUIT");
        if(sigismember(&sigset,SIGUSR1))
        printf("SIGUSR1");
        if(sigismember(&sigset,SIGALRM))
        printf("SIGALRM");
        printf("\n");

    }
    errno=errno_save;
}

函数sigpending

作用:

用于获取当前被阻塞(pending)的信号集合,即已经发送给进程但尚未被处理的信号集合。

阻塞信号是指暂时阻止信号递送给进程,而未决信号是指已经发送给进程但尚未处理的信号。

对之前很多信号功能的代码示例:

#include"apue.h"
static void sig_quit(int);
int main()
{
    sigset_t newmask,oldmask,pendmask;
    if(signal(SIGQUIT,sig_quit)) /*为SIGQUIT信号设置信号执行程序*/
        err_sys("can't catch SIGQUIT");
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGQUIT);/*阻塞信号SIGQUIT*/
    if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)/*将新信号屏蔽集赋予进程*/
        err_sys("SIG_BLOCK error");
    sleep(5);/*屏蔽SIGQUIT睡眠*/
    if(sigpending(&pendmask)<0)
        err_sys("sigpending error");
    if(sigismember(&pendmask,SIGQUIT))
        printf("\nSIGQUIT pending\n");
    if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        err_sys("SIG_SETMASK error");
    printf("SIGQUIT unblocked\n");
    sleep(5);
    exit(0);

}

static void sig_quit(int signo)
{
    printf("caught SIGQUIT\n");
    if(signal(SIGQUIT,SIG_DFL)<0)
        err_sys("can't reset SIGQUIT");
}

流程分析:

1.

signal(SIGQUIT,sig_quit)

为SIGQUIT信号设置信号执行程序
2.        

sigemptyset(&newmask);
    sigaddset(&newmask,SIGQUIT);/*阻塞信号SIGQUIT*/
    if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)/*将新信号屏蔽集赋予进程*/
        err_sys("SIG_BLOCK error");

为进程设置新的信号屏蔽字,对SIGQUIT信号进行屏蔽,并将旧信号屏蔽字进行保存,方便后面回复状态

3.

sleep(5);/*屏蔽SIGQUIT睡眠*/

进程进入第一次睡眠,在此期间屏蔽SIGQUIT信号

4.

if(sigpending(&pendmask)<0)
        err_sys("sigpending error");
    if(sigismember(&pendmask,SIGQUIT))
        printf("\nSIGQUIT pending");

获取未决信号和屏蔽信号,并判断SIGQUIT是否是屏蔽信号或者未决信号

这里SIGQUIT是屏蔽信号,终端输出SIGQUIT pending

5.

if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        err_sys("SIG_SETMASK error");
    printf("SIGQUIT unblocked\n");
    sleep(5);

回复进程的信号屏蔽字(即取消对SIGQUIT信号的屏蔽)

则屏蔽队列中的SIGQUIT立马变成了未决信号,信号执行程序sig_quit立马调用,输出

caught SIGQUIT

然后才输出SIGQUIT unblocked

解除屏蔽的瞬间屏蔽信号就变成了未决信号,并调用信号执行程序

输出结果:

注意到caught SIGQUIT的输出是在SIGQUIT unblocked之前的

信号处理程序在解除屏蔽的瞬间就执行了

观察到第二次运行代码,产生了10次SIGQUIT信号,只调用了一次信号处理程序。

当解除对信号的阻塞时,操作系统会检查是否有相同类型的多个未决信号等待处理。如果有多个相同类型的未决信号,通常只会调用一次信号处理程序来处理这些信号。这样可以确保信号处理程序不会被重复调用,避免出现重复处理相同信号的情况。

函数sigaction(我感觉十分复杂,究极套娃)

功能:检查或修改(或检查并修改)与指定信号相关联的处理动作。

signo变量表示要处理的信号编号,比如SIGINT、SIGTERM等。

act参数用于指定信号处理函数以及一些其他选项

oact(old act)参数用于保存旧的act值

eg:

sigaction(SIGINT,act,oact);

将对于SIGINT的信号处理函数和一些其他选项(后面详述)设置成act指定的一些值,并把旧的处理函数和一些其他选项保存记录在oact中

sigaction结构体定义如下:

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

sa_handler字段包含信号处理函数的地址

当sa_handler设置为函数地址时,sa_mask用来设置执行函数时的屏蔽字

sa_mask保证了信号处理函数在执行过程中不被其他信号打断

sa_flags对应信号进行处理的各个选项,可选如下:

sa_sigaction字段

1.是一个替代的信号处理程序

2.sa_sigaction和sa_handler二选一

3.当sa_flags为SA_SIGINFO时,选用sa_sigation字段,按下列方式调用信号处理程序:

void handler(int signo,siginfo_t * info,void *context)

下面解释info、context两个参数:

info参数

siginfo结构体参数如下: 

struct siginfo{
    int si_signo;/*信号编号*/
    int si_errno;/*如果信号是由于某个系统调用失败而产生的,这里将包含对应的错误号。*/
    int si_code;/*额外信息(取决于信号)*/
    pid_t si_pid;/*发送进程的进程ID*/
    uid_t si_uid;/*发送进程的实际用户ID*/
    void *si_addr;/*造成故障的地址*/
    int si_status;/*返回值或信号编码*/
    union sigval si_value;/*特殊应用值*/




};

si_code根据信号的不同有不同的值,具体如下:

context参数

ucontext_t *uc_link; /*该指针指向一个将在当前上下文返回后被恢复的上下文*/
sigset_t uc_sigmask; /*当前上下文活跃时被阻塞的信号集*/
stack_t uc_stack;/*当前上下文使用的栈*/
mcontext_t uc_mcontext;/*以特定于某台机器或某种平台的方式保存的上下文信息*/

 

私了个募这个sigaction函数,东西一大堆,真的看不明白,人绕晕了!!!!

下面介绍几个sigaction应用示例来熟悉一下

 sigaction应用示例:实现signal函数

#include"apue.h"
#include<signal.h>
Sigfunc *signal(int signo,Sigfunc * func)
{
    struct sigaction act,oact;/*用来存放从sigaction函数返回的信息*/
    act.sa_handler=func;
    sigemptyset(&act.sa_mask);
    act.sa_flags=0;
    if(signo == SIGALRM)
    {
        #ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
        #endif
    }
    else
    act.sa_flags |= SA_RESTART;
    if(sigaction(signo,&act,&oact)<0)
        return SIG_ERR;
    return oact.sa_handler;
    
}
static void sig_quit(int signo)
{
    printf("\ncaught SIGQUIT\n");
}
int main()
{

    signal(SIGQUIT,sig_quit);
    pause();
    return 0;
}

函数sigsetjmp和siglongjmp

作用:在信号处理程序中进行非局部转移。

代码示例:

#include"apue.h"
#include<setjmp.h>
#include<time.h>
#include"test_sigprocmask.c"
static void sig_usr1(int);
static void sig_alrm(int);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjmp;
int main()
{

    if(signal(SIGUSR1,sig_usr1) == SIG_ERR)
        err_sys("signal (SIGUSR1) error");
    if(signal(SIGALRM,sig_alrm) == SIG_ERR)
        err_sys("signal (SIGALRM) error");
    pr_mask("starting main: ");
    if(sigsetjmp(jmpbuf,1))
    {
        pr_mask("ending main: ");
        exit(0);
    }
    canjmp=1; /*执行了sigsetjmp之后才将canjmp设置成1*/
    for(;;)
    pause();
    

}

static void sig_usr1(int signo)
{
    time_t starttime;
    if(canjmp == 0)/*如果canjmp为0则返回*/
    return ; 
    pr_mask("starting sig_usr1");
    alarm(3);/*计时3s⏰*/
    starttime = time(NULL);
    for(;;)
    if( time(NULL) > starttime+5 )
        break;
    pr_mask("finishing sig_usr1");
    canjmp = 0;/*在一次longjmp之前把canjmp清0*/
    siglongjmp(jmpbuf,1);

}

static void sig_alrm(int signo)
{
    pr_mask("in sig_alrm: ");
}

代码分析:

1.创建了易失变量canjmp来限制siglongjmp只能在sigsetjmp之后调用,别切在执行一次longjmp之前将canjmp清0(保证了执行sigsetjmp之后才能longjmp)

2.并且canjmp的类型为sig_atomic_t,保证了对canjmp的写为原子操作

输出结果:

main函数中调用的sigsetjmp参数为jmpbuf和1,savemask值非0,会保存当前屏蔽字,所以当从sig_usr1或者sig_alrm函数longjmp回来时,信号屏蔽字为空,输出ending main:

另外执行信号处理程序会屏蔽相应的信号:

左边没有执行信号处理程序时,屏蔽字为空,中间进入SIGUSR1的信号处理程序,屏蔽字为SIGUSR1,右边在执行sig_usr1时被SIGALRM信号中断,转而执行sig_alrm,此时中断屏蔽字为SIGUSR1和SIGALRM

函数sigsuspend

目的:

为了解决在解除信号屏蔽和pause()之间产生的信号带来的问题。

功能:

sigsuspend函数的作用是挂起进程,直到收到指定信号为止。当调用sigsuspend函数时,进程会将自身的信号屏蔽字设置为一个指定的信号集,然后进入睡眠状态,直到收到信号集中的信号为止。收到信号后,进程会恢复原来的信号屏蔽字,然后继续执行。这个函数通常用于实现信号的同步处理。

suspend可以保护代码临界区,使其不被特定信号中断

下面的代码就保护临界区不受SIGINT信号中断 

代码示例:

#include"apue.h"
#include"test_sigprocmask.c"
static void sig_int(int);
int main()
{
    sigset_t newmask,oldmask,waitmask;
    pr_mask("program start:");
    if(signal(SIGINT,sig_int) == SIG_ERR)
        err_sys("signal(SIGINT) error");
    sigemptyset(&waitmask);
    sigaddset(&waitmask,SIGUSR1);
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGINT);
    if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
        err_sys("SIG_BLOCK error");
    /*临界区代码在这*/
    pr_mask("in critical region:");
    if(sigsuspend(&waitmask)!=-1)
        err_sys("sigsuspend error");
    pr_mask("after return from sigsuspend: ");
    if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        err_sys("SIG_SETMASK error");
    pr_mask("program exit: ");

    exit(0);
}

static void sig_int(int signo)
{
    pr_mask("\nin sig_int: ");
}

输出结果:

分析:程序一开始屏蔽字为空

1.if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
        err_sys("SIG_BLOCK error");

这里newmask中屏蔽信号为SIGINT,然后进入临界区域(critical region)

所以临界区的屏蔽字屏蔽了SIGINT信号,输出in critical region:SIGINT

2.然后调用suspend函数 

if(sigsuspend(&waitmask)!=-1)
        err_sys("sigsuspend error");

waitmask集合中只包含SIGUSR1信号,进程进入等待除SIGUSR1外的信号的状态

终端键入Ctrl+C 发送SIGINT信号,进程接收SIGINT信号,进入信号处理函数sig_int

由于sigsuspend函数屏蔽了SIGUSR1信号,且SIGINT的信号处理函数自动屏蔽SIGINT信号,所以sig_int函数输出

in sig_int:SIGINT SIGUSR1

3.从sigsuspend函数返回,屏蔽字集合恢复到调用sigsuspend函数之前,即只屏蔽了SIGINT信号

输出

after return from sigsuspend: SIGINT

4.

if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        err_sys("SIG_SETMASK error");

将屏蔽字恢复为程序开始时的状态,即屏蔽字为空

输出:

program exit: 

suspend函数的另一种应用:等待一个信号处理程序设置一个全局变量

下面的代码用于捕捉中断信号和退出信号,希望仅当捕捉到退出信号时,才唤醒主例程。

代码示例如下:

#include"apue.h"
volatile sig_atomic_t quitflag;
static void sig_int(int signo)
{
    if(signo == SIGINT)
        printf("\ninterrupt\n");
    else if(signo == SIGQUIT)
        quitflag=1;
    printf("quitflag的值为%d\n",quitflag);
}

int main()
{
    sigset_t newmask,oldmask,zeromask;
    if(signal(SIGINT,sig_int) == SIG_ERR)/*为SIGINT和SIGQUIT信号设置同一处理函数*/
        err_sys("signal (SIGNINT) error");
    if(signal(SIGQUIT,sig_int) == SIG_ERR)
        err_sys("signal (SIGQUIT) error");
    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGQUIT);
    if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
        err_sys("SIG_BLOCK error");
    while(quitflag == 0) /*只有在sig_int将quitflag设置成1后,主例程才能继续*/
    {
        sigsuspend(&zeromask);
        printf("在while循环中,quitflag的值为%d\n",quitflag);
    }
    printf("跳出了循环!");
    quitflag = 0;
    if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        err_sys("SIG_SETMASK error");
    exit(0);
}

程序会卡在主函数while循环处,zeromask为空,进程会接收所有信号

while(quitflag == 0) /*只有在sig_int将quitflag设置成1后,主例程才能继续*/
    {
        sigsuspend(&zeromask);
        printf("在while循环中,quitflag的值为%d\n",quitflag);
    }

但是只有SIGQUIT信号能使得quitflag的值变为1,使得程序继续

PS.我自己运行的时候第二次键入Ctrl+C程序就退出了,我不知道为什么。 

sigsuspend函数可以实现父、子进程间的同步

TELlWAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT和WAIT_CHILD实现如下:

#include "apue.h"
static volatile sig_atomic_t sigflag;
static sigset_t newmask,oldmask,zeromask;
static void sig_usr(int signo)
{
    sigflag = 1;
}

/*完成两件任务:
1.给SIGUSR1、SIGUSR2设置信号处理函数
2.屏蔽SIGUSR1、SIGUSR2信号,并存储原屏蔽字
*/
void TELL_WAIT(void)
{
    if(signal(SIGUSR1,sig_usr) == SIG_ERR)
        err_sys("signal SIGUSR1 error");
    if(signal(SIGUSR2,sig_usr) == SIG_ERR)
        err_sys("signal SIGUSR2 error");
    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGUSR1);
    sigaddset(&oldmask,SIGUSR2);
    if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
        err_sys("SIG_BLOCK error");
}
/*
向父进程发送USR2信号,告诉父进程自己准备完毕
*/
void TELL_PARENT(pid_t pid)
{
    kill(pid,SIGUSR2);
}

void WAIT_PARENT(void)
{
    while(sigflag == 0) /*等待父进程将sigflag设置为0*/
        sigsuspend(&zeromask);
    sigflag=0;
    /*还原信号屏蔽字*/
    if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        err_sys("SIG_SETMASK error");
    
    
}

/*向子进程发送SIGUSR1信号,告诉自己已经完成*/
void TELL_CHILD(pid_t pid)
{
    kill(pid,SIGUSR1);
}

void WAIT_CHILD(void)
{
    /*等待子进程将sigflag设置为0*/
    while(sigflag == 0)
        sigsuspend(&zeromask);
    sigflag=0;
    if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        err_sys("SIG_SETMASK error");

}



代码解析:

1.代码中用SIGUSR1表示父进程发送给子进程的信号,SIGUSR2表示子进程发送给父进程的信号

2.TELL_WAIT函数相当于一个初始化函数

3.父子进程之间的TELL通过发送USR信号来触发信号处理函数的执行从而修改sigflag的值来进行

函数abort——使程序异常终止

 原理:向调用进程发送SIGABRT信号

一些细节:

1.调用abort会向主机发送一个未成功终止的通知,具体是调用raise函数

2.abort将SIGABRT信号发送给进程,进程执行信号处理程序返回后,不会返回调用abort的进程

3.进程在收到SIGABRT信号后,要在终止之前由其执行所需的清理操作

按POSIX.1实现的abort代码如下:

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

void abort(void)
{
    sigset_t mask;
    struct sigaction action;
    /*调用abort的进程不能忽略SIGABRT信号,如果该进程忽略了,则取消屏蔽*/
    sigaction(SIGABRT,NULL,&action);/*把对于SIGABRT信号的相关处理设置保存在action中*/
    if(action.__sigaction_handler.sa_handler == SIG_IGN)/*如果对于SIGABRT的处理为忽略则改成默认*/
      {
        action.__sigaction_handler.sa_handler = SIG_DFL;
        sigaction(SIGABRT,&action,NULL);
      }  

    if(action.__sigaction_handler.sa_handler == SIG_DFL)
        fflush(NULL);/*冲洗所有的标准输入输出流*/
      
    sigfillset(&mask);/*将所有信号屏蔽字设为1*/
    sigdelset(&mask,SIGABRT);/*不屏蔽SIGABRT信号*/
    sigprocmask(SIG_SETMASK,&mask,NULL);/*屏蔽除了SIGABRT之外的所有信号*/
    kill(getpid(),SIGABRT);
    
    fflush(NULL);/*冲洗所有打开的标准输入输出流*/
    action.__sigaction_handler.sa_handler = SIG_DFL;
    sigaction(SIGABRT,&action,NULL);
    sigprocmask(SIG_SETMASK,&mask,NULL);
    kill(getpid(),SIGABRT);
    exit(1);

}

解析:

sigaction(SIGABRT,NULL,&action);/*把对于SIGABRT信号的相关处理设置保存在action中*/
    if(action.__sigaction_handler.sa_handler == SIG_IGN)/*如果对于SIGABRT的处理为忽略则改成默认*/
      {
        action.__sigaction_handler.sa_handler = SIG_DFL;
        sigaction(SIGABRT,&action,NULL);
      }  

这段代码判断进程对于SIGABRT信号是否是忽略的,若是忽略的则设置成默认状态

为什么代码中对于标准输入输出流进行了两次冲洗?

第一次冲洗是在给进程发送SIGABRT信号(也就是执行信号处理程序)之前,

在信号处理程序中输入输出缓存中可能产生新数据

所以在从信号处理程序返回后要进行再次冲洗

函数system

函数sleep、nanosleep和clock_nanosleep

reqtp指向的timespec结构体用秒和纳秒指定需要休眠的长度

remtp指向的timespec结构体存放未休眠完的时间长度

clock_id可选如下:

  1. CLOCK_REALTIME
    • 这是基于系统实时时间的时钟。它反映了墙上时钟的时间,并受到系统时间调整(如用户手动设置时间或NTP调整)的影响。
    • 当使用 CLOCK_REALTIME 与 clock_nanosleep 时,休眠的结束时间将基于系统的实时时间。
  2. CLOCK_MONOTONIC
    • 这是一个从某个固定点(通常是系统启动)开始计算的时钟,它不受系统时间调整的影响。
    • CLOCK_MONOTONIC 通常用于测量经过的时间,因为它不会由于系统时间的更改而受到影响。
    • 使用 CLOCK_MONOTONIC 与 clock_nanosleep 时,休眠的结束时间将基于从某个固定点开始的单调时间。
  3. CLOCK_PROCESS_CPUTIME_ID
    • 这个时钟测量当前进程所消耗的CPU时间。
    • 它不包括其他进程或系统活动所使用的时间,只计算当前进程在用户模式和内核模式下实际使用的时间。
  4. CLOCK_THREAD_CPUTIME_ID
    • 类似于 CLOCK_PROCESS_CPUTIME_ID,但它测量的是当前线程所消耗的CPU时间。

函数sigqueue——对信号进行排队

使用排队信号前置条件:

sigqueue函数与kill函数类似,但是sigqueue函数可以使用value参数向信号处理程序传递整数和指针值。

注意:

1.信号不能无限排队,收到SIGQUEUE_MAX限制,达到相应限制后,sigqueue会失败,将errno设置为EAGAIN

2.独立信号集,编号在SIGRTMIN~SIGRTMAX之间,默认行为是终止进程

作业控制信号

作业控制信号之间有某些交互:

1.一个进程产生4种停止信号(SIGTSTP、SIGSTOP、SIGttin或SIGTTOU)中的任意一种时,未决SIGCONT会被丢弃

2.对一个进程产生SIGCONT信号时,未决停止信号(SIGTSTP、SIGSTOP、SIGttin或SIGTTOU)将会被丢弃

SIGCONT只对于停止进程有效(使得进程继续),否则直接忽略。

尽管有的停止进程忽略或者阻塞SIGCONT信号,但是收到SIGCONT信号的停止进程还是会继续。

示例代码如下,改程序将标准输入复制到标准输出,并在SIGTSTP信号处理函数中管理屏幕刷新:

#include"apue.h"
#define BUFFSIZE 1024
/*SIGTSTP信号的处理函数*/
static void sig_tstp(int signo)
{
    sigset_t mask;
    /*....将光标移动至左下角,重置tty模式*/

    /*解除对于SIGTSTP信号的阻塞,因为在我们处理该信号的时候,它被阻塞了*/
    sigemptyset(&mask);
    sigaddset(&mask,SIGTSTP);
    sigprocmask(SIG_UNBLOCK,&mask,NULL);
    signal(SIGTSTP,SIG_DFL);/*对于SIGTSTP信号的处理设置为默认*/
    kill(getpid(),SIGTSTP);
    /*直到进程继续之前不会从kill函数返回*/
    /*.. 重新设置tty模式,重新绘制屏幕..*/


}

int main()
{
    int n;
    char buf[BUFFSIZE];
    /*只有在我们用用作业控制shell运行时,才能捕捉SIGTSTP信号*/
    if(signal(SIGTSTP,SIG_IGN) == SIG_DFL)
     signal(SIGTSTP,sig_tstp);
    while((n = read(STDIN_FILENO,buf,BUFFSIZE))>0)
        if(write(STDOUT_FILENO,buf,n)!=n)
            err_sys("write error");
    if(n<0)
        err_sys("read error");
        
    return 0;
}

对于信号处理函数sig_tstp:

static void sig_tstp(int signo)
{
    sigset_t mask;
    /*....将光标移动至左下角,重置tty模式*/

    /*解除对于SIGTSTP信号的阻塞,因为在我们处理该信号的时候,它被阻塞了*/
    sigemptyset(&mask);
    sigaddset(&mask,SIGTSTP);
    sigprocmask(SIG_UNBLOCK,&mask,NULL);
    signal(SIGTSTP,SIG_DFL);/*对于SIGTSTP信号的处理设置为默认*/
    kill(getpid(),SIGTSTP);
    /*直到进程继续之前不会从kill函数返回*/
    /*.. 重新设置tty模式,重新绘制屏幕..*/


}

1.该函数使得进程收到SIGTSTP信号之后,将光标移动至左下角,并重置tty模式,方便用户重新输入

2.然后会取消对于SIGTSTP信号的屏蔽(进入SIGTSTP信号的处理函数后系统会自动屏蔽SIGTSTP信号),并将对于SIGSTSTP信号的处理程序设置为默认(只将进程挂起)

3.然后调用kill函数对该进程发送SIGTSTP信号,使得进程挂起

4.进程收到SIGCONT信号后才会从kill函数返回,重新设置tty模式,绘制屏幕

信号名和编号之间的映射

psignal函数

使用psignal函数可移植地打印与信号编号对应的字符串。

 

psiginfo函数

用来获取sigaction函数信号处理程序中siginfo的信息(除信号编号以外的更多信息)

习题

10.1

删除图 10-2 程序中的 for(;;) 语句,结果会怎样?为什么?

图10-2代码如下:

 

 

修改后的代码如下:

#include"apue.h"

static void sig_usr(int);/*信号处理函数*/

int main()
{
    if(signal(SIGUSR1,sig_usr)==SIG_ERR)
        err_sys("can't catch SIGUSR1");
    if(signal(SIGUSR2,sig_usr)==SIG_ERR)
        err_sys("can't catch SIGUSR2");
        pause();
    
}

static void sig_usr(int signo)
{
    if(signo==SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo==SIGUSR2)
        printf("received SIGUSR2\n");
    else 
        err_dump("eceived signal %d\n",signo);
}

收到USR1信号后,进程就结束了

没有那个for循环,pause的时候收到USR1信号,进程跳出pause去执行USR1的信号处理程序,然后进程就结束了

10.2

实现 10.22 节中说明的 sig2str 函数

 

 我的原来的思路是用psignal函数来实现sig2str函数,这是不可取的,我对于psignal函数的作用还不够清晰

psignal函数是一个在Unix和类Unix系统(包括Solaris)中用于将信号编号转换为描述性字符串并打印到标准错误输出(stderr)的函数。

eg:

 

注意输出的为Interrupt而不是SIGINT,输出的为该信号的作用

而sig2str(SIGINT,str)

则str只存放的SIGINT,表明将一个int型的信号变为对应的信号名存入str

参考Github上的答案:

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

#define s2s(signo, str, signal)                \
  {                                            \
    if (signo == signal) strcpy(str, #signal); \
  }

void sig2str(int signo, char *str) {
  strcpy(str, "UNKNOWN");
  s2s(signo, str, SIGKILL);
  s2s(signo, str, SIGHUP);
  s2s(signo, str, SIGINT);
  s2s(signo, str, SIGQUIT);
  s2s(signo, str, SIGILL);
  s2s(signo, str, SIGTRAP);
  s2s(signo, str, SIGABRT);
  s2s(signo, str, SIGIOT);
  s2s(signo, str, SIGBUS);
  s2s(signo, str, SIGFPE);
  s2s(signo, str, SIGKILL);
  s2s(signo, str, SIGUSR1);
  s2s(signo, str, SIGSEGV);
  s2s(signo, str, SIGUSR2);
  s2s(signo, str, SIGPIPE);
  s2s(signo, str, SIGALRM);
  s2s(signo, str, SIGTERM);
  s2s(signo, str, SIGSTKFLT);
  s2s(signo, str, SIGCHLD);
  s2s(signo, str, SIGCONT);
  s2s(signo, str, SIGSTOP);
  s2s(signo, str, SIGTSTP);
  s2s(signo, str, SIGTTIN);
  s2s(signo, str, SIGTTOU);
  s2s(signo, str, SIGURG);
  s2s(signo, str, SIGXCPU);
  s2s(signo, str, SIGXFSZ);
  s2s(signo, str, SIGVTALRM);
  s2s(signo, str, SIGPROF);
  s2s(signo, str, SIGWINCH);
  s2s(signo, str, SIGIO);
  s2s(signo, str, SIGPWR);
  s2s(signo, str, SIGSYS);
  s2s(signo, str, SIGRTMIN);
}

int main() {
  int n;
  char str[1024];
  while (1) {
    scanf("%d", &n);
    sig2str(n, str);
    printf("%s\n", str);
  }
}

重点是这里的宏定义,感觉自己对宏定义还是不太熟悉:

#define s2s(signo, str, signal)                \
  {                                            \
    if (signo == signal) strcpy(str, #signal); \
  }

我只知道基本的宏定义,参考了

【C进阶】之宏定义的扩展_c++的宏扩展-CSDN博客

进行更深入的了解。

10.3

画出运行图 10-9 程序时的栈帧情况

见 p745 ,图 C-11

10.4

图 10-11 程序中利用 setjmplongjmp 设置 I/O 操作的超时,下面的代码也常见用于侧重目的:

signal(SIGALRM, sig_alrm);
alarm(60);
if (setjmp(env_alrm) != 0) {
    /* handle timeout */
}

这段代码有什么错误?

可能会导致longjmp在setjmp前调用

eg:

1.signal函数设置完信号处理程序

2.alarm函数设置闹钟

3.发生进程切换

4.60s后进程收到SIGALRM信号执行信号处理函数sig_alrm,longjmp在setjmp调用之前调用,发生错误

Github:

在第一次调用 alarmsetjmp 之间有一次竞争条件。如果进程在调用 alarmsetjmp 之间被内核阻塞了,闹钟时间超过后就调用信号处理程序,然后调用 longjmp 。但是由于没有调用过 setjmp ,所以没有设置 env_alrm 缓冲区。如果 longjmp 的跳转缓冲区没有被 setjmp 初始化,则说明 longjmp 的操作是未定义的。

10.5

仅适用一个定时器(alarm 或较高精度的 setitimer),构造一组函数,使得进程在该单一定时器基础上可以设置任意数量的定时器。

先复习一下alarm函数:

这里只说说我的思路(只说思路,C语言实现小根堆太麻烦了):

1.创建一个小根堆用于存放各计时器的剩余时间

2.只对剩余时间最小的那个计时器进行计时,将未进行计时的计时值存入小根堆中

3.每次my_alarm函数和sig_alrm函数都对小根堆中的计时器剩余时间值进行更新

#include"unistd.h"

/*用来改造alarm函数,来实现多个定时闹钟*/
unsigned int my_alarm(unsigned int secs)
{
    int res_secs = alarm(secs);/*获取剩余的秒数*/
    int pas_time = 堆顶元素值 - res_secs;/*得到自从上一次更新堆流逝时间*/
    堆中所有元素值-=pas_time;/*更新堆中时间*/
    if(res_secs == 0)/*没有正在计时的闹钟*/
    {
        将secs插入堆中;
        return 0;
    }
    if(res_secs < secs)
    {
        alarm(res_secs);
    }
    else
    {
        alarm(secs);
    }
    
    将secs插入堆中;
    

}

static void sig_alrm(int signo)
{
    if(signo != SIGALRM)
    {
        printf("发生了错误!!!\n");
        exit(0);
    }
    int past_time = 堆顶元素;/*计算自从上一次更新堆的流逝时间*/
    删除堆顶元素;
    堆中所有元素值-=pas_time;/*更新堆中时间*/
    alarm(新的堆顶元素);

}


int main()
{
    
    return 0;
}

缺陷:感觉精度不够,没有考虑my_alarm和sig_alrm函数在执行时的时间流逝,有误差。

Github答案:

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

struct timer_t {
  unsigned int started_at;  // 设置定时器时的 UNIX 时间戳
  unsigned int ended_at;    // 设置的定时器应该触发的 UNIX 时间戳
  void (*proc)();           // 超时执行的函数
  struct timer_t *next;     // 指向下一个定时器
};

struct timer_t *head = NULL;

/**
 * 向队列中添加一个定时器
 * 返回最近一个要触发的闹钟时间的余留秒数
 */
unsigned int push_to_timer_list(struct timer_t *node) {
  unsigned int cur_time;
  time((time_t *)&cur_time);

  struct timer_t *cur = head;

  /* 若当前没有待触发的定时器 */
  if (cur == NULL) {
    head = node;
    alarm(node->ended_at - cur_time);
    return (head->ended_at - cur_time);
  } else {
    /* 如果添加的定时器先于之前的第一个定时器 */
    if (head->ended_at > node->ended_at) {
      node->next = head;
      head = node;
      alarm(node->ended_at - cur_time);
      return (node->ended_at - cur_time);
    } else {
      /* 移动游标 */
      while (cur->next && cur->next->ended_at < node->ended_at) {
        cur = cur->next;
      }
      node->next = cur->next;
      cur->next = node;
      return (head->ended_at - cur_time);
    }
  }
}

/**
 * 触发指定的事件并启动下一个定时器
 */
void alarm_proc(int signo) {
  struct timer_t *cur;
  cur = head;
  cur->proc();

  if (cur->next) {
    alarm(cur->next->ended_at - cur->ended_at);
    free(head);
    head = cur->next;
  } else {
    free(head);
    head = NULL;
  }
}

/**
 * 类似于 JavsScript 的 settimeout 函数
 * 参数是定时器的时间以及超时后执行的函数
 */
void settimeout(unsigned int seconds, void (*proc)()) {
  /* 如果时间为 0 ,则直接执行 */
  if (seconds == 0) {
    proc();
    return;
  }

  struct timer_t *t = (struct timer_t *)malloc(sizeof(struct timer_t));
  time((time_t *)&(t->started_at));
  t->ended_at = t->started_at + seconds;
  t->next = NULL;
  t->proc = proc;

  if (signal(SIGALRM, alarm_proc) == SIG_ERR) {
    printf("signal (SIGALRM) error\n");
    exit(0);
  }

  push_to_timer_list(t);
}

void print() { printf("Hello World\n"); }

void print2() { printf("Hello MeiK\n"); }

int main() {
  settimeout(2, print);
  settimeout(1, print);
  settimeout(5, print);
  settimeout(3, print2);
  printf("Hello main\n");
  for (;;) {
    pause();
  }
  return 0;
}

.......未完待续

  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值