APUE------信号

信号概念

信号是软件中断,很多比较重要的应用程序都需要处理信号,信号提供了一种处理异步事件的方法。

有很多条件可以产生信号:
当用户按某些终端键时,引发终端产生的信号。
硬件异常产生信号。
进程调用kill(2)函数可以将任意信号发送给另一个进程或进程组。
用户可以调用kill(1)命令将信号发送给其他进程。
当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。

当某个信号产生,我们可以有以下3中方式处理:
1. 忽略此信号。但是有两种信号不能被忽略,它们是SIGKILL和SIGSTOP。这两个信号不能被忽略的原因是:它们向内核和超级用户提供了是进程终止或停止的可靠方法。
2. 捕捉信号。调用一个用户函数。
3. 执行系统默认动作。

函数signal

unix系统信号机制最键的接口是signal函数。

#include <signal.h>
void (*signal(int signo , void(*func)(int)))(int));
//返回值:若成功,返回以前的信号处理配置;若出错,返回SIG_ERR

signo参数是信号名。func值是常量SIG_IGN(忽略此信号)、SIG_DFL(系统默认动作)、或当接到此信号后要调用的函数的地址(信号处理函数signal handler或信号捕捉函数signal-catching function)。

signal函数原型太复杂了,如果使用下面的typedef,则可使其简单一些。
typedef void Sigfunc(int);
然后可将signal函数原型写成:
Sigfunc signal(int , Sigfunc );

这个typedef包括在apue.h中,系统不自带。

这是一个简单的程序,它捕捉两个用户定义的信号并打印信号编号。

#include "apue.h"

static void sig_usr(int);   //one handler for both signals

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

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

当执行一个程序时,所有信号的状态都是系统默认或忽略。exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变

当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

可重入函数

如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。如果进程正在执行malloc,被信号中断,这时会发生什么?

Single UNIX Specification说明了在信号处理程序中保证调用安全的函数。这些函数是可重入的并被称为异步信号安全的(async-signal safe)。除了可重入以外,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送。

这段程序从信号处理程序my_alarm调用非可重入函数getpwnam,而my_alarm每秒钟都被调用一次。

#include "apue.h"
#include <pwd.h>

static void my_alarm(int signo)
{
    struct passwd *rootptr;

    printf("in signal handler\n");
    if((rootptr = getpwnam("root")) == NULL)
        err_sys("getpwnam(root)error");
    alarm(1);
}

int main()
{
    struct passwd *ptr;

    signal(SIGALARM, my_alarm);
    alarm(1);
    for(;;)
    {
        if((ptr = getpwnam("sar")) == NULL)
            err_sys("getpwnam error");
        if(strcmp(ptr->pw_name, "sar") != 0)
            printf("return value corrupted!, pw_name = %s\n",
            ptr->pw_name);
    }
}

可靠信号术语和语义

当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。当对信号采取了这种动作时,我们说向进程递送了一个信号。在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)。

每一个进程都有一个信号屏蔽字 (signal mask),它规定了当前要阻塞递送到该进程的信号集。

函数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参数有以下4种不同的情况:
pid > 0 将该信号发送给进程ID为pid的进程。
pid = 0 发送给与发送进程同一进程组的所有进程,而且发送线程具有权限向这些进程发送信号。
pid < 0 以绝对值来计算pid
pid == -1 将该信号发送给发送进程有权限向它们发送信号的所有进程。

函数alarm和pause

alarm函数可以设置一个定时器,在将来的某个时刻该定时器会超时。当定时器超时,产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值:0或以前设置的闹钟时间的预留秒数

参数seconds的值是产生信号SIGALRM需要经过的时钟秒数。当这一时刻达到时,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一个时间间隔。

每个进程只能有一个闹钟时间。
如果有以前注册的尚未超时的闹钟时间,而且本次调用的seconds值是0,则取消以前的闹钟时间,其余值仍作为alarm函数的返回值。

pause函数使调用进程挂起直至捕捉到一个信号。

#include <unistd.h>
int pause(void);
//返回值:-1,errno设置为EINTR

只有执行了一个信号处理程序并从其返回时,pause才返回。

使用longjmp ,带时间限制调用read

#include "apue.h"
#include <setjmp.h>

static void sig_alrm(int);
static jmp_buf  env_alrm;

int main()
{
    int n;
    char line[MAXLINE];

    if(signal(SIGALRM, sig_alrm) == SIG_ERR)
        err_sys("signal(SIGALRM) error");
    if(setjmp(env_alrm) != 0)
        err_quit("read timeout");
    alarm(10);
    if((n = read(STDIN_FILENO, lin, MAXLINE)) < 0)
        err_sys("read error");
    alarm(0);

    write(STDOUT_FILENO, line, n);
    exit(0);
}

static void
sig_alrm(int signo)
{
    longjmp(env_alrm, 1);
}

信号集(signal set)

#include <signal.h>
int sigemptyset(sigset_t *set);         //清空
int sigfillset(sigset_t *set);          //填满
int sigaddset(sigset_t *set, int signo);    //增加
int sigdelset(sigset_t *set, int signo);    //删除
//4个函数返回值:若成功,返回0;若出错,返回-1
int sigismember(const sigset_t *set, int signo);//判断
//返回值:若真,返回1;若假,返回0

函数sigprocmask

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

#include <signal.h>
int sigprocmask(int how, const siget_t *restrict set,
                siget_t *restrict oset);
//返回值:若成功,返回0;若出错,返回-1

首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。

how 说明
SIG_BLOCK 当前信号屏蔽字加上set
SIG_UNBLOCK 当前信号屏蔽字去掉set
SIG_SETMASK set替换当前信号屏蔽字

函数sigpending

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

#include <signal.h>
int sigpending(sigset_t *set);
//返回值:若成功,返回0;若出错,返回-1

函数sigaction

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

#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act 
              ,struct sigaction *restrict oact);
//返回值:若成功,返回0;若出错,返回-1

其中,参数signo是要检测或修改其具体动作的信号编号。若act指针非空,则要修改其动作。如果oact指针非空,则系统经由oact指针返回该信号的上一个动作。

此函数使用下列结构:

struct sigaction{
    void (*sa_handler)(int);/* addr of signal handler,*/
                            /*or SIG_IGN, OR SIG_DFL*/
    sigset_t sa_mask;       /*additional signals to block*/
    int sa_flags;           /*signal options, Figure 10.16*/
    /*alternate handler*/
    void (sa_sigaction)(int, siginfo_t *, void *);
}

如果sa_handler字段包含一个信号捕捉函数的地址(不是常亮SIG_IGN或SIG_DFL),则sa_mask字段说明雷了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。

act结构的sa_flags字段指定对信号进行处理的各个选项。

sa_sigaction字段是一个替代的信号处理程序,在sigaction结构中使用了SA_SIGINFO标志时,使用该信号处理程序。对于sa_sigaction字段和sa_handler字段两者,实现可能使用同一存储区,所以应用只能一次使用这两个字段中的一个。

通常,按下列方式调用信号处理程序:
void handler(int signo);
但是,如果设置了AS_SIGINFO标志,那么按下列方式调用信号处理程序:
void handler(int signo, siginfo_t *info, void *context);

siginfo结构包含了信号产生原因的有关信息

struct siginfo{
    int   si_signo; /*signal number*/
    int   si_errno; /*if nonzero,errno value from<errno.h>*/
    int   si_code;  /*additional info (depends on signal)*/
    pid_t si_pid;   /*sending process ID*/
    uid_t si_uid;   /*sending process real user ID*/
    void *si_addr;  /*addres that caused the fault*/
    int   si_status; /*exit value or signal number*/
    union sigval si_value;/*application-specific value*/
    /*possibly other fields also*/
}
union sigval
{
    int sival_int;
    void *sival_ptr;
}

用sigaction实现的signal函数

#include "apue.h"

/* Reliable version of signal(),using POSIX sigaction().*/
signal(int signo , Sigfunc *func)
{
    struct sigaction act, oact;
    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);
}

函数sigsetjmp和siglongjmp

非局部转移的setjmp和longjmp函数。

#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
//返回值:若直接调用,返回0;若从siglongjmp调用返回,则返回非0
void siglongjmp(sigjmp_buf env, int val);

这两个函数和setjmp、longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0savemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。

信号屏蔽、sigsetjmp和siglongjmp实例

#include "apue.h"
#include <setjmp.h>
#include <time.h>

static void                     sig_usr1(int);
static void                     sig_alrm(int);
static sigjmp_buf               jmpbuf;
static volatile sig_atomic_t    canjump;

int main(void)
{
    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: ");       /*Figure 10.14*/

    if(sigsetjmp(jmpbuf, 1)){
        pr_mask("ending main: ");
        exit(0);
    }
    canjump = 1;                      /*now sigsetjmp() is OK*/

    for(;;)
        pause();
}

static void sig_usr1(int signo)
{
    time_t starttime;

    if(canjump == 0)
        return;            /*unexpected signal, ignore*/

    pr_mask("starting sig_usr1: ");

    alarm(3);                  /*SIGALRM in 3 seconds*/
    starttime = time(NULL);
    for(;;)                    /*busy wait for 5 seconds*/
        if(time(NULL) > starttime + 5)
            break;

    pr_mask("finishing sig_usr1:");

    canjump = 0;
    siglongjmp(jmpbuf, 1);  /*jump back to main,don`t return*/
}

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

函数sigsuspend

目的是保护代码临界区,使其不被特定信号中断
在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由
sigsuspend函数提供的

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

进程的信号屏蔽字设置为sigmask指向的值。在捕捉到一个信号或发生了一个终止该进程的信号之前,该进程挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。

保护临界区不被信号中断

#include "apue.h"
static void sig_int(int)

int main(void)
{
    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);

    /* * Block SIGINT and save current signal mask. */
    if(sigpromask(SIG_BLOCK, &newmask, &oldmask) < 0)
        err_sys("SIG_BLOCK error");

    /* *Critical region of code. */
    pr_mask("in critical region:");

    /* *Pause, allowing all signals excecpt SIGUSR1. */
    if(sigsuspend(&waitmask) != -1)
        err_sys("sigsuspend error");

    pr_mask("after return from sigsuspend:");

    /* * Reset signal mask which unblocks SIGINT. */
    if(sigpromask(SIG_SETMASK, &oldmask, NULL) < 0)
        err_sys("SIG_SETMASK error");

    /* * And continue processing··· */
    pr_mask("program exit: ");

    exit(0);
}

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

利用sigsusupend还可以实现等待一个信号处理程序设置一个全局变量。

可以用用心实现父,子进程之间的同步

如果在等待信号期间希望调用其他系统函数可以使用多线程,可专门安排一个线程处理信号。

函数abort

abort函数的功能是使程序异常终止

#include <stdlib.h>
void abort(void);
//此函数不返回值

此函数将SIGABRT信号发送给调用进程(进程不应该忽略此信号)。
abort()函数导致所有的流被关闭和冲洗。

函数system

在程序中执行一串命令字符串。

注意system的返回值,它是shell的终止状态,但shell的终止状态并不总是执行命令字符串进程的终止状态。在信号方面是最后要加上一个信号编号。

仅当shell本身异常终止时,system的返回值才报告一个异常终止。

在编写使用system函数的程序时,一定要正确地解释返回值,如果直接调用fork、exec和wait,终止状态与调用system是不同的。

sleep、nanosleep和clock_nanosleep

#include <unistd.h>
unsigned int sleep(unsigned int seconds);
//返回值:0或为休眠完的秒数

此函数使调用进程挂起直到满足下面两个条件之一。
1. 已经过了seconds所指定的墙上时钟时间。
2. 调用进程捕捉到一个信号并从信号处理程序返回。

nanosleep函数与sleep函数类似,但提供了纳秒级的精度

#include <time.h>
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);
//返回值:若休眠到要求的时间,返回0;若出错,返回-1

reqpt参数用秒和纳秒指定需要休眠的时间长度。如果某个信号中断了休眠间隔,程序并没有终止,remtp参数指向的timespec结构就会被设置为未休眠完的时间长度。如果对为休眠完的时间并不感兴趣,可以把该参数置为NULL;

随着多个系统时钟的引入,需要使用相对于特定时钟的延迟时间来挂起调用线程。clock_nanosleep函数提供了这种功能

#include <time.h>
int clock_nanosleep(clockid_t clock_id, int flags,
                    const struct timespec *reqtp, 
                    struct timespec *remtp);
//返回值:若休眠要求的时间,返回0;如出错,返回错误号

clock_id参数指定了计算延迟时间基于的时钟。flags参数用于控制延迟是相对的还是绝对的。flags为0时表示休眠时间是相对的(希望休眠的时间长度),如果flags值设置为TIMER_ABSTIME,表示休眠时间是绝对的(希望休眠到时钟到达某个特定的时间)。

注意,除了出错返回,调用clock_nanosleep和调用nanosleep的效果是相同的。使用相对休眠的问题是有些应用对休眠长度有精度要求,相对休眠时间会导致实际休眠时间比要求的长。

函数sigqueue

使用排队信号必须做以下几个操作:
1. 使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志。如果没有给出这个标志,信号会延迟,但信号是否进入队里要取决于具体实现。
2. 在sigaction结构的sa_sigaction成员中提供信号处理程序,实现可能允许用户使用sa_handler字段,但不能获取sigqueue函数发送出来的额外信息。
3. 使用sigqueue函数发送信号。

#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval value);
//返回值:若成功,返回0;若出错,返回-1

sigqueue函数只能把信号发送给单个进程,可以使用value参数向信号处理程序传递整数和指针值,除此之外,sigqueue函数与kill函数类似。

信号不能被无限排队,达到相应的限制(SIGQUEUE_MAX)以后,sigqueue就会失败,将errno设为EAGAIN。

随着实时信号的增强,引入了用于应用程序的独立信号集。这些信号的编号在SIGRTMIN~SIGRTMAX之间,包括这个限制值。注意,这些信号的默认行为是终止进程。

信号名和编号

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

#include <signal.h>
void psignal(int signo, const char *msg);

字符串msg输出到标准错误文件,后面跟随一个冒号和一个空格,再后面对该信号的说明,最后是一个换行符。如果msg为NULL,只有信号说明部分输出到标准错误文件。

如果在sigaciton信号处理程序中有siginfo结构,可以使用psiginfo函数打印信号信息。

#include <signal.h>
void psiginfo(const siginfo_t *info, const char *msg); 

如果只需要信号的字符描述部分,也不需要把它写到标准错误文件中,可以使用strsignal函数。

#include <string.h>
char *strsignal(int signo);
//返回值:指向描述该信号的字符串的指针

你可能感兴趣的:(APUE------信号)