《APUE》笔记-第十章-信号

重点:信号意义、几种常见信号、signal、信号的阻塞和未决、sigprocmask、sigpending、sigaction、sigsuspend、竞争条件

1.信号

信号是软件中断,信号提供了一种处理异步事件的方法:产生信号的事件是随机出现的,需要告诉内核当什么信号发生时该执行什么操作。

定义在<signal.h>里(本机实际位置:/usr/include/bits/signum.h),形式:“#define 信号名  信号编号” ,如下图,不存在编号为0的信号。


信号处理动作:1.忽略;2.捕捉;3.执行系统默认动作(大多数是终止该进程)

几种常见信号及其产生原因:

SIGKILL和SIGSTOP两种信号不能被忽略也不能被捕捉

信号名说明(产生原因)
SIGABRT调用abort( ),异常终止
SIGALRM定时器超时alarm( )
SIGCHLD进程终止或停止时,
发送给其父进程
SIGCONT发送给需要继续运行,
但出于停止状态的进程
SIGHUP终端检测到连接断开,
则将此信号发送给控制
进程
SIGINT按中断键 Ctrl + C
SIGKILL杀死任一进程
SIGQUIT按退出键 Ctrl + \
SIGSTOP停止一个进程
SIGTERMkill命令发送
SIGTSTP停止信号,发送给前台
进程组的所有进程
SIGUSR1用户定义的信号,可用
于应用程序
SIGUSR2同SIGUSR1
SIGTTIN后台进程组的进程试图
读控制终端
SIGTTOUT后台进程组的进程试图
写控制终端
  

2.signal

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);

成功:返回以前的信号配置;出错,返回SIG_ERR

第二个参数:SIG_IGN、SIG_DFL、或信号捕捉函数的函数名。

从函数定义可看出,对signal来说,不改变信号处理方式就不能确定信号的当前处理方式,sigaction可对此作出改善。

因为子进程在开始时复制了父进程的内存映像,所以子进程继承父进程的信号处理方式。

练习程序:

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

static void sig_usr(int);

int main()
{
        if (signal(SIGUSR1, sig_usr) == SIG_ERR)
                printf("can't catch signal SIGUSR1\n");
        if (signal(SIGUSR2, sig_usr) == SIG_ERR)
                printf("can't catch signal SIGUSR2\n");
        for (; ;)
                pause();//在接到信号前,一直挂起
        return 0;
}

static void sig_usr(int signo)
{
        if (signo == SIGUSR1)
                printf("received SIGUSR1\n");
        if (signo == SIGUSR2)
                printf("received SIGUSR2\n");
}
结果:


分析:

1.pause函数会导致进程在接到一个信号前,会一直处于挂起状态。

2.调用不带参数的kill,默认会发送SIGTERM信号,而对该信号的默认处理方式是终止该进程。

3.不可靠信号

不可靠指的是:

信号可能会丢失:一个信号发生了,但进程可能会不知道。

对信号控制能力差:无法阻塞信号

进程每次接收到信号对其进行处理时,随即将该信号的动作重置为默认值

不能关闭信号

4.中断的系统调用

低速系统调用和其他系统调用

低速系统调用:可能会使进程永远阻塞的一类系统调用

早起Unix:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR

为使应用程序不必处理被中断的系统调用,BSD引进了某些被中断系统调用的自动重启动。自动重启动的系统调用有:ioctl、read、readv、write、writev、wait、waitpid

自动重启动可能会引起问题,所以允许禁用此功能。

引入自动重启动理由是:不必每次对读、写系统调用进行是否出错的测试,如果是被中断的,则再调用读、写系统调用。

5.可重入函数

进程捕捉到信号并对其进行处理,进程正在执行的指令序列就被信号处理程序临时中断,首先执行该信号处理程序中的指令,如果从信号处理程序返回,则继续在捕捉到信号时进程正在执行的正常指令中返回。在信号处理程序中,不能判断捕捉到信号时进程在何处执行,这样不能保证在中断处理结束后能够正确返回到进程的执行指令中。为了保证进程在处理完中断后能够正确返回,需要保证调用的是可重入的函数。

不可重入函数包括:(1)使用静态数据结构,(2)调用malloc或free,(3)标准I/O函数。

信号处理程序中调用一个不可重入的函数,则结果是不可预测的。例如getpwnam函数是个不可重入的,因为其结果存放在静态存储单元中,信号处理程序调用后,返回给正常调用者的信息可能是被返回给信号处理程序的信息覆盖。

6.kill、 raise

kill向进程或进程组发送信号,raise向自身发送信号

#include <signal.h>

int kill(pid_t pid, int signo)

int raise(int signo)

返回值:成功,返回0;出错,返回-1

raise(signo)<=>kill(getpid(), signo)

kill的pid:

pid>0:发送给pid

pid<0:发送给|pid|

pid=0:发送给同一进程组所有进程

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

练习程序:

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

static void sig_usr1(int signo)
{
        printf("received signal SIGUSR1\n");
}

static void sig_chld(int signo)
{
        printf("received signal SIGCHLD\n");
}

int main()
{
        pid_t pid;
        if ((pid = fork()) < 0)
        {
                printf("fork() error\n");
                exit(-1);
        }
        if (pid == 0)
        {
                signal(SIGUSR1, sig_usr1);
                raise(SIGUSR1);//子进程向自身发送信号
                pause();//等待父进程发送信号
        }
        else
        {
                sleep(1);//等待子进程执行
                signal(SIGCHLD, sig_chld);
                kill(pid, SIGKILL);//杀死子进程
                sleep(2);//等待子进程终止,捕捉信号SIGCHLD
        }
        exit(0);
}
结果:


分析都在代码注释里

7.alarm、 pause

#include <unistd.h>

//设置一个定时器

unsigned int alarm(unsigned int seconds);

返回值:0或以前设置的闹钟时间的余留秒数

若seconds=0,则取消以前设置的闹钟时间,余留秒数仍作为返回值


//使调用进程挂起,直到捕捉到一个信号

int pause(void);

返回值:-1,errno设置为EINTR

程序练习:

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

#define ERR_EXIT(m) \
        {\
                perror(m);\
                exit(EXIT_FAILURE);\
        }

void sig_alrm(int signo)
{
        printf("received SIGALRM\n");
}

int main()
{
        signal(SIGALRM, sig_alrm);
        int time_origin = alarm(0);
        alarm(2);
        int time_now = alarm(5);
        printf("origin time is: %d\n", time_origin);
        printf("now time is: %d\n", time_now);
        pause();
        printf("after pause()\n");
        exit(0);
}

结果:


8.信号集

信号集是一个能表示多个信号的数据类型,用sigset_t定义一个信号集

int sigemptyset(sigset_t *set);//初始化,清除所有信号

int sigfillset(sigset_t *set);//初始化,包括所有信号

int sigaddset(sigset_t *set, int signo);

int sigdelset(sigset_t *set, int signo);

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

int sigismember(sigset_t *set, int signo);

//若真,返回1;若假,返回0

所有应用程序在使用信号集之前,要对该信号集调用sigemptyset或sigfillset一次

程序练习:

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

void printsig(const sigset_t *set)
{
        //未列出全部信号
        if (sigismember(set, SIGINT))
                printf("SIGINT\n");
        if (sigismember(set, SIGQUIT))
                printf("SIGQUIT\n");
        if (sigismember(set, SIGALRM))
                printf("SIGALRM\n");
        if (sigismember(set, SIGUSR1))
                printf("SIGUSR1\n");
        int i;
        for (i = 1; i < NSIG; i++)//NSIG:最大信号编号
        {
                if (sigismember(set, i))
                        putchar('1');
                else
                        putchar('0');
        }
        printf("\n");
}

int main()
{
        sigset_t set;
        sigemptyset(&set);
        printsig(&set);
        sigaddset(&set, SIGINT);
        sigaddset(&set, SIGUSR1);
        printsig(&set);
        sigdelset(&set, SIGINT);
        printsig(&set);

        exit(0);
}
结果:

9.信号的阻塞和未决

参考文章:信号的阻塞和未决(写的很好)

实际执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号,SIGKILL 和 SIGSTOP 不能被阻塞。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

概括如下:


若sigprocmask将一个信号集设置为信号屏蔽字时,当该信号集中的信号发生后,在递送到信号处理程序过程中,因为被阻塞所以出于未决状态。此时,通过sigpending就可取出处于未决状态的信号集。

10.sigprocmask、 sigpending

//将信号集中的信号设为信号屏蔽字

int sigprocmask(int how, sigset_t *restrict set, sigset_t *restrict oset);

how:

SIG_BLOCK:set包含我们希望加到当前信号屏蔽字的信号,相当于mask=mask|set

SIG_UNBLOCK:set包含我们希望从当前信号屏蔽字删除的信号,相当于mask=mask& ~set

SIG_SETMASK:设置当前信号屏蔽字为set,相当于mask=set

oset为非空指针,则当前信号屏蔽字通过oset返回

//取出处于未决状态的信号集

int sigpending(sigset_t *set);

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

程序练习:

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

#define ERR_EXIT(m)\
        {\
                perror(m);\
                exit(EXIT_FAILURE);\
        }

//打印全部信号编号
static void print_sig(const sigset_t *set)
{
        int i;
        for (i = 1; i < NSIG; i++)
        {
                if (sigismember(set, i))
                        putchar('1');
                else
                        putchar('0');
        }
        printf("\n");
}

//信号处理程序
static void sig_quit(int signo)
{
        printf("caught SIGQUIT\n");
        if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
                ERR_EXIT("signal error");
}

int main()
{
        if (signal(SIGQUIT, sig_quit) == SIG_ERR)
                ERR_EXIT("signal error");
        sigset_t newmask;
        sigset_t oldmask;
        sigset_t pendmask;

        sigemptyset(&newmask);
        sigaddset(&newmask, SIGQUIT);
        sigprocmask(SIG_BLOCK, &newmask, &oldmask);

        sigpending(&pendmask);
        print_sig(&pendmask);
        sleep(5);//此期间产生退出信号
        sigpending(&pendmask);
        print_sig(&pendmask);
        if (sigismember(&pendmask, SIGQUIT))
                printf("SIGQUIT pending\n");

        sigprocmask(SIG_SETMASK, &oldmask, NULL);
        sleep(5);//再次产生退出信号

        exit(0);
}
结果:


分析:

通过第一次调用sigprocmask后就调用sigpending来获取信号集并打印,可知,sigpending返回的是处于未决状态的信号集,而不是信号屏蔽字。只有一个信号产生了并被阻塞处于未决状态时,sigpending才返回该信号集

另:系统不会对信号进行排队,即阻塞期间产生多次SIGQUIT,但只向进程递送一次SIGQUIT

11.sigaction

//功能:检查或修改(或检查并修改)与指定信号相关联的处理动作,取代早期的signal函数

int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

struct sigaction

{

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

    sigset_t sa_mask;

    int sa_flags;

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

};

sa_handler:信号捕捉函数的地址;

sa_mask:调用信号捕捉函数前,该信号集被加到信号屏蔽字中,从而在调用信号捕捉函数时,能阻塞某些信号。

sa_flags:SA_INTERRUPT:中断的系统调用不自动重启动;SA_RESTART:中断的系统调用自动重启动;......

sa_sigaction:替代的信号处理程序,一次只能使用sa_handler和sa_sigaction中的一个。

说明:同一信号多次发生,并不将它们加入队列;如:某种信号阻塞时发生了5次,解除阻塞后,信号处理函数只调用一次。

linux下默认是不自动重启动的。

用sigaction实现signal,程序如下:

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

typedef void Sigfunc(int);

Sigfunc *signal(int signo, Sigfunc *func)
{
        printf("now in my signal\n");
        struct sigaction act, oact;
        act.sa_handler = func;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        #ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
        #endif
        sigaction(signo, &act, &oact);
        return(oact.sa_handler);
}

void sig_func(int signo)
{
        printf("catched signal\n");
}

int main()
{
        signal(SIGALRM, sig_func);
        alarm(1);
        pause();
        exit(0);
}
结果:


12.sigsetjmp、 siglongjmp

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savemask);

void siglongjmp(sigjmp_buf env, int val);

若savemask非0,则sigsetjmp在env中保存了进程当前信号屏蔽字。当调用siglongjmp时,如果带非0的sigsetjmp已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。

调用信号处理函数时,再次产生一个信号,并通过siglongjmp返回,程序如下:

#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <time.h>
#include <errno.h>

static void sig_usr1(int);
static void sig_alarm(int);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjump;
void pr_mask(const char* str);

int main()
{
        signal(SIGUSR1, sig_usr1);
        signal(SIGALRM, sig_alarm);
        pr_mask("starting main:");
        if (sigsetjmp(jmpbuf,1))
        {
                pr_mask("ending main:");
                exit(0);
        }
        canjump = 1;
        for (; ;)
                pause();
}

static void sig_usr1(int signo)
{
        time_t starttime;

        if (canjump == 0)
                return;
        pr_mask("starting sig_usr1: ");
        alarm(3);
        sleep(5);
        pr_mask("finishing sig_usr1: ");
        canjump = 0;
        siglongjmp(jmpbuf, 1);
}
static void sig_alarm(int signo)
{
        pr_mask("in sig_alrm: ");
}

void pr_mask(const char *str)
{
        sigset_t sigmask;
        sigprocmask(0, NULL, &sigmask);
        printf("%s", str);
        if (sigismember(&sigmask, SIGINT))
                printf(" SIGINT");
        if (sigismember(&sigmask, SIGALRM))
                printf(" SIGALRM");
        if (sigismember(&sigmask, SIGUSR1))
                printf(" SIGUSR1");
        if (sigismember(&sigmask, SIGQUIT))
                rintf(" SIGQUIT");
        printf("\n");
}
结果:


分析:

1.打印信号屏蔽字pr_mask("starting main:"),

2.调用sigsetjmp,在jmpbuf中保存进程当前的信号屏蔽字

3.发送SIGUSR1信号给进程

4.接收到信号,进入信号处理程序,打印此时的信号屏蔽字,pr_mask("starting sig_usr1"); 

注:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。 SIGUSR1  

5. 3秒后,产生信号SIGALRM,进入另一个信号处理程序,打印此时信号屏蔽字pr_mask("in sig_alrm:");

注:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。 SIGUSR1  SIGALRM

6.返回上一层信号处理程序,打印信号屏蔽字,pr_mask("ending sig_usr1");

注:当从信号处理程序返回时,恢复原来的信号屏蔽字 SIGUSR1

7.调用siglongjmp,返回sigsetjmp位置,siglongjmp从jmpbuf中恢复之前的信号屏蔽字。打印此时信号屏蔽字 pr_mask("ending main:"),无

注:当从信号处理程序返回时,恢复原来的信号屏蔽字 无

本程序还利用了canjump为1从而确保sigsetjmp已经将当前的信号屏蔽字保存到jmpbuf中了;否则不执行信号处理程序,直接返回。

13.sigsuspend

sigsuspend将解除信号屏蔽字和使进程挂起结合成一个原子操作。这样可以解决竞争条件,见第14节。在原子操作中,先恢复信号屏蔽字,然后使进程休眠。进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。

int sigsuspend(const sigset_t *sigmask);

返回值:-1,并将errno设置为EINTR

程序如下:

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

static void pr_mask(const char *str);
static void sig_int(int signo);

int main()
{
        signal(SIGINT, sig_int);
        pr_mask("starting main: ");

        sigset_t newmask, oldmask, waitmask;
        sigemptyset(&newmask);
        sigemptyset(&waitmask);
        sigaddset(&newmask, SIGINT);
        sigaddset(&waitmask, SIGUSR1);

        sigprocmask(SIG_BLOCK, &newmask, &oldmask);
        pr_mask("after newmask: ");
        sigsuspend(&waitmask);//恢复之前的信号屏蔽字,现在的信号屏蔽字设置为SIGUSR1,进程休眠。调用sig_int后返回
        pr_mask("after return from sigsuspend(): ");
        sigprocmask(SIG_SETMASK, &oldmask, NULL);
        pr_mask("ending main: ");
        return 0;
}

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

static void pr_mask(const char *str)
{
        sigset_t sigmask;
        sigprocmask(0, NULL, &sigmask);
        printf("%s", str);
        if (sigismember(&sigmask, SIGINT))
                printf(" SIGINT");
        if (sigismember(&sigmask, SIGALRM))
                printf(" SIGALRM");
        if (sigismember(&sigmask, SIGUSR1))
                printf(" SIGUSR1");
        if (sigismember(&sigmask, SIGQUIT))
                printf(" SIGQUIT");
        printf("\n");
}
结果:


分析:

调用sigsuspend(&waitmask)后,进程原先的信号屏蔽字(SIGINT)被恢复,现在的信号屏蔽字被设置为waitmask(SIGUSR1);

sigsuspend捕捉到一个信号并从信号处理程序返回时,sigsuspend返回。所以中断键引起sig_int被调用,sig_int返回时,sigsuspend返回,并将信号屏蔽字设置为之前的信号屏蔽字(SIGINT)

14.竞争条件和sleep函数

使用alarm和pause函数可实现sleep函数

程序如下:

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

static void sig_alrm(int signo)
{

}

unsigned int sleep1(unsigned int seconds)
{
        if (signal(SIGALRM, sig_alrm) == SIG_ERR)
                return(seconds);
        alarm(seconds);
        pause();
        return(alarm(0));
}

int main()
{
        printf("now sleep 3 seconds\n");
        sleep1(3);
        printf("end.\n");
        exit(0);
}
该简单实现有3个问题:

1.如果在调用sleep1之前,调用者已设置了闹钟,则它被sleep1函数中的第一次alarm调用擦除

2.改程序中修改了对SIGALRM的配置

3.在第一次调用alarm和pause之间有一个竞争条件。在一个繁忙的系统中, 可能alarm在调用pause之前超时,并调用了信号处理程序。如果发生了这种情况,则在调用pause后,如果没有捕捉到其他信号,调用者将永远被挂起。虽然alarm的下一行就是pause,但无法保证pause在调用alarm后的seconds秒后一定会被调用,由于时序而导致的错误,叫做竞争条件

sleep:

unsigned int sleep(unsigned int seconds);

返回值:0或未休眠完的秒数

此函数使调用进程挂起直到满足下面两个条件之一:

(1)已经过了seconds所指定的墙上时钟时间

(2)调用进程捕捉到一个信号并从信号处理程序返回

使用alarm实现的sleep函数,该函数可靠的处理信号,避免了竞争条件,程序如下:

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

static void sig_alrm(int signo)
{

}

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

        newact.sa_handler = sig_alrm;
        sigemptyset(&newact.sa_mask);
        newact.sa_flags = 0;
        sigaction(SIGALRM, &newact, &oldact);

        sigemptyset(&newmask);
        sigaddset(&newmask, SIGALRM);
        sigprocmask(SIG_BLOCK, &newmask, &oldmask);

        alarm(seconds);
        suspmask = oldmask;

        sigdelset(&suspmask, SIGALRM);
        sigsuspend(&suspmask);

        unslept = alarm(0);

        sigaction(SIGALRM, &oldact, NULL);
        sigprocmask(SIG_SETMASK, &oldmask, NULL);
        return unslept;
}

int main()
{
        printf("sleep 5 seconds:\n");
        int i;
        for (i = 0; i < 5; i++)
        {
                sleep(1);
                printf("%d\n", i+1);
        }
        printf("end\n");
        return 0;
}
结果:


分析:

因为sigprocmask先将SIGALRM设置为信号屏蔽字,则就算调用alarm后内核转而去执行其他进程而超时,但由于被屏蔽,所以无法执行信号处理程序。而sigsuspend将解除信号屏蔽和挂起等待合为一个原子操作,则不可能存在解除信号后又去执行其他进程,而原来的进程一直处于等待的情况。所以能够确保解除信号后执行信号处理程序,然后一定会返回,不会使调用进程一直处于阻塞状态。sigprocmask和sigsuspend一起使用就解决了原来的sleep的pause后可能一直阻塞的问题。



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值