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

来源:互联网 发布:无主之地前传改枪软件 编辑:程序博客网 时间:2024/05/16 04:17

重点:信号意义、几种常见信号、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 + CSIGKILL杀死任一进程SIGQUIT按退出键 Ctrl + \SIGSTOP停止一个进程SIGTERMkill命令发送SIGTSTP停止信号,发送给前台
进程组的所有进程SIGUSR1用户定义的信号,可用
于应用程序SIGUSR2同SIGUSR1SIGTTIN后台进程组的进程试图
读控制终端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 0
原创粉丝点击