信号(Signal)

信号的基本概念

信号被认为是一种软件中断(区别于硬件中断),信号机制提供了一种在单进程/线程下处理异步事件的方法。

每个信号都有一个编号和一个宏定义名称 ,这些宏定义可以在 signal.h 中找到。

使用kill -l命令查看系统中定义的信号列表: 1-31是普通信号; 34-64是实时信号

信号的产生

1.用户在终端按下某些键时,终端驱动程序会发送信号给前台程序

例如:Ctrl-c产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-z产生SIGTSTP信号

2.硬件异常产生信号

这类信号由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

例如:当前进程执行除以0的指令,CPU的运算单元会产生异常,内核将这个进程解释为SIGFPE信号发送给当前进程。

           当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

3.一个进程调用kill(2)函数可以发送信号给另一个进程

可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的

如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。

kill

int kill(pid_t pid, int sig);

    pid>0 此时正式最普通的一种情况,pid是要目标进程的pid。

    pid=0  那么kill()会将信号发送给调用进程同组的所有进程,也包括他自己。

    pid=-1 那么信号将被发送至所有它具有权限发送信号的每一个进程(init进程和调用进程除外)。

    pid<-1 信号会发送sig信号到组id等于该pid绝对值的进程组中的每一个进程。

    如果pid在以上四种情况之外,无法匹配到目标进程,那么就会返回-1,errno被设置为ESRCH。当没有权限发送时kill()也将失败返回-1,errno会被设置为EPERM。

killpg

    killpg()函数向某一进程组的所有成员发送一个信号。

    int killpg(pid_t pid, int sig);

raise

    发送信号的函数是raise(),它只接受一个参数signal,然后把该信号传递给调用进程(自己给自己发信号):

    int raise(int sig);//成功返回0,失败返回-1

4.由软件条件产生信号

例如通过alarm()函数来产生信号

unsigned int alarm(unsigned int seconds);

alarm函数的返回值是0或上次设置闹钟剩余的时间。

int main()

{

         int count=0;

         alarm(1);

         while(1)

         {

                 printf("%d\n",count);

                 count++;

         }

         return 0;

}

信号的处理过程

进程收到一个信号后不会被立即处理,而是在恰当 时机进行处理!

什么是适当的时候呢?比如说中断返回的时候,或者内核态返回用户态的时候(这个情况出现的比较多)。

信号不一定会被立即处理,操作系统不会为了处理一个信号而把当前正在运行的进程挂起(切换进程),挂起(进程切换)的话消耗太大了,如果不是紧急信号,是不会立即处理的。操作系统多选择在内核态切换回用户态的时候处理信号,这样就利用两者的切换来处理了(不用单独进行进程切换以免浪费时间)。

总归是不能避免的,因为很有可能在睡眠的进程就接收到信号,操作系统肯定不愿意切换当前正在运行的进程,于是就得把信号储存在进程唯一的PCB(task_struct)当中。

具体过程是当进程运行到某处,接受到一个信号,保留“现场”,响应信号(注意这里的响应是一种宏观意义上的响应,对信号的忽略(SIG_IGN)也被以为是一种响应),在返回到刚刚保存的地方继续运行。


信号的处理流程


信号的处理流程--2

处理信号的整个过程是这样的:进程由于 系统调用或者中断 进入内核,完成相应任务返回用户空间的前夕,检查信号队列,如果有信号,则根据信号向量表找到信号处理函数,设置一些参数后,跳到用户态执行信号处理函数。信号处理函数执行完毕后,返回内核态,设置一些参数,再返回到用户态继续执行程序。

信号的阻塞与未决

信号递达(Delivery) :实际执行信号的处理动作;

信号未决(Pending) :信号从产生到递达之间的状态;

进程可以选择阻塞(Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后 可选的一种处理动作。

信号的阻塞与未决

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。

信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,

在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态,表明信号是否已经产生。

在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略

在上图的例子中,

  1. SIGHUP信号 未阻塞也未产生过,当它递达时执行默认处理动作。

  2. SIGINT信号 产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没 有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  3. SIGQUIT信号 未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

信号产生但是不立即处理,前提条件是要把它保存在pending表中,表明信号已经产生

信号集

在处理信号相关的函数时,我们时常需要一种的特殊的数据结构来表示一组信号的集合,这样的集合我们称之为信号集,其数据类型表示为sigset_t,sigset_t类型对于每种信号用一个bit表示 "有效"或者"无效" ,通常是用位掩码的形式来实现的。

sigset_t同时也提供了一组函数(实际上用宏来实现的,感兴趣可以查阅sigset.h),用以实现对sigset_t类型数据的操作。其原型如下:

其原型如下:

int sigemptyset(sigset_t *set);

初始化set所指向的信号集,使其中所有信号的对应的bit清零,表示该信号集不包含任何有效信号.

int sigfillset(sigset_t *set);

初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号机的有效信号包括系统支持的所有信号.

int sigaddset(sigset_t *set,int signo);

在该信号集中添加某种有效信号.

int sigdelset(sigset_t *set,int signo);

在该信号集中删除某种有效信号

int sigismemeber(const sigset_t *set,int signo);

是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含贼返回1,不包含则返回0,出错返回-1

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

读取或更改进程的信号屏蔽字(阻塞信号集)如果成功返回0 失败返回-1

int sigpending(sigset_t *set);

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

信号屏蔽

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

如果在进程在阻塞某信号时,该信号产生过多次,Liunx这样实现的:常规信号在抵达之前产生多次只计一次,而实时信号在递达之前产生多可以依次放在一个队列里. 每个信号只有一个bit的未决标志,非0既1,不记录该信号产生了多少次,阻塞标志也是这样表示的. 因此呢,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t为信号集,这个类型可以表示每个信号的"有效"或"无效“状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中类似. 阻塞信号集也叫做当前进程的信号屏蔽字. 屏蔽这样理解 "是阻塞,不是忽略"

如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对 待处理信号的阻塞时,待处理信号就会立刻被处理。

一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中 一个信号递达。

sigprocmask()函数可以检测和更改当前进程的信号屏蔽字。其原型:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数解析:

set表示新设置的信号屏蔽字,oset表示当前信号屏蔽字

处理方式:

      set 非空, oset 为NULL :按照how指示的方法更改set指向信号集的信号屏蔽字。

      set 为NULL,oset 非空:读取oset指向信号集的信号屏蔽字,通过oset参数传出。

      set 和 oset 都非空 :现将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

当oldset是一个非空指针的话,调用sigprocmask之后,oldset便返回了之前的信号屏蔽字。set参数会结合how参数对当前的信号屏蔽字做出修改。(有两个特殊的信号,你不可以屏蔽它们是:SIGKILL和SIGSTOP)具体规则是:

how                                                            行为

-------------------------------------------------------------------------------------------

SIG_BLOCK                 设置进程的信号屏蔽字为当前信号屏蔽字和set的并集。set是新增的要屏蔽的信号集。(添加到block表当中去)

SIG_UNBLOCK            设置当前进程的信号屏蔽字为当前信号屏蔽字和set补集的交集,也就是当前信号屏蔽字减去set中的要解除屏蔽的信号                                            集。set中是要解除屏蔽的信号集。(从block表中删除)

SIG_SETMASK            设置当前进程的信号屏蔽字为set信号集。(设置block表 设置当前信号屏蔽字为set所指向的值)

-------------------------------------------------------------------------------------------

然而当set指向一个NULL时,那么how也就没有作用了。通常我们让set设置为NULL时,通过oldset获取当前的信号屏蔽字。

如果某个或多个信号在进程屏蔽了该信号的期间来到过一次或者多次,我们称这样的信号叫做未决的(pending)信号。那么在调用sigprocmask()解除这个信号屏蔽之后,该信号会在sigprocmask ()返回之前,递送给(SUSv3 规定至少传递一个信号)当前进程。

进程维护了一个数据结构来保存未决的信号,我们可以通过sigpending()来获取哪些信号是未决的:

int sigpending(sigset_t *set);//return 0 on success,or -1 on error

set参数返回的便是未决的信号集。之后便可以通过使用sigismember()来判断,set中包含哪些信号。

这是一个输出型参数,会把当前进程的pending表打印到传入的set集中。

Linux上signal()注册的信号处理函数在执行时,会自动的将当前的信号添加到进程的信号屏蔽字当中。当信号处理函数返回时,会恢复之前的信号屏蔽字。这意味着,当信号处理函数执行时,它不会递归的中断自身。

信号处理的接口

对于大部分的信号,Linux系统都有默认的处理方式。而大部分默认的处理方式是终止程序并转储core文件

signal()

要处理信号,Linux系统处理信号的接口有两个sigaction(),signal(),较简单的是signal()函数,其形式如下:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

siganl()函数有两个参数其中有一个int的参数便是要处理的信号,诸如SIGINT的宏。另一个参数类型为sighandler_t的函数指针,handler指针对应的函数我们称之为:信号处理函数(signal-handler function)。可见signal()的第二个参数是一个信号处理函数,返回值也是一个信号处理函数,失败返回宏SIG_ERR(SIGKILL和SIGSTOP的默认行为分别是杀死和停止一个进程,任何试图改变这两个信号的处理方式的行为都将返回错误)。

通俗一点来说就是当signum信号到来时,进程会保存当前的进程栈,转去执行siganl()中指定的handler函数。之前提到过,信号的响应方式有多种,因此handler不仅可以是一个函数指针也可以是ISO C为我们定义的宏:SIG_IGN,SIG_DEL,和他们的名字一样SIG_IGN是忽略这个信号,SIG_DEL是保持这个信号的默认处理方式

sigaction()

sigaction()系统调用之前我们已经解除了signal()函数,sigaction()是另外一种选择,它功能更加强大,兼容性更好,任何时候我们都应优先考虑使用sigaction(),即使signal()更加简单灵活。其函数原型:

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);//Return 0 on success,or -1 on error

与sigprocmask类似地,oldact返回之前的信号设置,act用来设置新的信号处理。

signum自然不用解释,这是要处理的信号。

这个函数的关键之处就是struct sigaction这个和函数同名的结构体。当然要使用sigaction()还是得从struct sigaction入手,它的定义:

        struct sigaction { 

                 union { 

                         void (*sa_handler)(int); //信号处理方式 

                         void (*sa_sigaction)(int, siginfo_t *, void *); //实时信号的处理方式 

                 } __sigaction_handler; //Address of handler 

                 sigset_t sa_mask; //Signals blocked during the handler invocation,额外屏蔽的信号 

                 int sa_flags; //Flags controlling handler invocation 

                 void (*sa_restorer)(void); //Restore,not use

        };

signum是指定信号的编号

sa_mask是一组信号集,当调用信号处理函数之前会将这组信号集添加到进程的信号屏蔽字中,直到信号处理函数返回。

利用sa_mask参数,我们可以指定一组信号,让我们的信号处理函数不被这些信号打断。

与前面的signal()一样,默认还是会把引发信号处理函数的信号自动的添加到进程的信号屏蔽字中的。

sa_flags参数,这是一组选项,下面就来看看这组选项是什么意思:

sa_flags                                                  说明

SA_INTERRUPT                   由此信号中断的系统调用不会自动重启。

SA_NOCLDSTOP                 当signum为SIGCHLD时,当因接受一信号的子进程停止或者恢复时,将不会产生此信号(有点绕).

                                              但是子进程终止时,仍会产生此信号。(If sig is SIGCHLD, don’t generate this signal when a child process is                                                            stopped or resumed as a consequence of receiving a signal.)

SA_NOCLDWAIT                  当signum为SIGCHLD时,子进程终止时不会转化为僵尸进程。

                                              此时调用wait(),则阻塞到所有子进程都终止,才返回-1,errno被视之为ECHILD。

SA_NODEFER                     捕获该信号的时候,不会在执行信号处理函数之前将该信号自动添加到进程的信号屏蔽字中。

SA_ONSTACK                     调用信号处理函数时,使用sigaltstack()安装的备用栈。

SA_RESETHAND                当捕获该信号时,会在调用信号处理函数之前将信号处理函数设置为默认值SIG_DFL,并清除SA_SIGINFO标志。

SA_RESTART                      被此信号中断的系统调用,会自动重启。

SA_SIGINFO                        调用信号处理函数时附带了额外的数据要处理,具体见下文。

sa_restorer和名字一样为保留参数,不需要使用。

最后我们要看的是__sigaction_handler,这是一个联合体。sa_handler和sa_sigaction都是信号处理函数的指针,所以一次只能选择两者中的一个。

如果sa_mask中设置了SA_SIGINFO位那么就按照void (*sa_sigaction)(int, siginfo_t *, void *)的形式的函数调用信号处理函数,否则使用 void (*sa_handler)(int)这样的函数。

下面我们再来看一看sa_sigaction这个函数:

void sa_sigaction(int signum, siginfo_t* info, void* context);

siginfo_t是一个结构体,其结构和实现相关

siginfo_t {

    int si_signo; /* Signal number */

    int si_errno; /* An errno value */

    int si_code; /* Signal code */

    int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */

    pid_t si_pid; /* Sending process ID */

    uid_t si_uid; /* Real user ID of sending process */

    int si_status; /* Exit value or signal */

    clock_t si_utime; /* User time consumed */

    clock_t si_stime; /* System time consumed */

    sigval_t si_value; /* Signal value */

    int si_int; /* POSIX.1b signal */

    void *si_ptr; /* POSIX.1b signal */

    int si_overrun; /* Timer overrun count; POSIX.1b timers */

    int si_timerid; /* Timer ID; POSIX.1b timers */

    void *si_addr; /* Memory location which caused fault */

    long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */

    int si_fd; /* File descriptor */

    short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */

}

每个字段的含义后边都加了清晰的注释,但是还有一个参数使我们需要特别注意的,其中si_value字段用来接收伴随着信号发送过来的数据,其类型是一个sigval_t的联合体,其定义

# define __have_sigval_t 1

/* Type for data associated with a signal. */

typedef union sigval

{

    int    sival_int;

    void* sival_ptr;

}  sigval_t;

#endif

在实际编程中,到底选择sival_int还是sival_ptr字段,还是取决于你的应用程序。但是由于指针的作用范围只能在进程的内部,如果发送一个指针到另一个进程一般没有什么实际的意义。

sigaction的处理方式

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

        1、若act指针非空,则根据act结构体中的信号处理函数来修改该信号的处理动作。

        2、若oact指针非 空,则通过oact传出该信号原来的处理动作。

        3、现将原来的处理动作备份到oact里,然后根据act修改该信号的处理动作。

        (注:后两个参数都是输入输出型参数!)

将sa_handler三种可选方式

    1、赋值为常数SIG_IGN传给sigaction表示忽略信号;

    2、赋值为常数SIG_DFL表示执行系统默认动作;

    3、赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。

        (注:这是一个回调函数,不是被main函数调用,而是被系统所调用)

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。

使用sigqueue()

发送实时信号时可以附带数据,kill(),raise()等函数的参数注定他们无法附带更多的数据,这里我们要认识一个新的函数sigqueue()专门用于在发送信号的时候,附加传递额外的数据。

int sigqueue(pid_t pid, int sig, const union sigval value);//Return 0 on success ,or -1 on error

前两个参数和kill()一致,但是不同于kill(),这里不能将pid只能是单个进程,而不像kill()那样丰富的用法。value的类型便是在上边提及的sigval_t,于是就清晰了:发送进程在这里发送的value在接受进程中通过信号处理函数sa_sigaction中的siginfo_t info参数就可以拿到了。

一个处理实时信号信号简单的demo

处理信号端代码catch.c:

void sighandler(int sig,siginfo_t* info,void* context)

{

        printf("Send process pid = %ld,receive a data :%d\n",info->si_pid,info->si_value.sival_int);

}


int main()

{

        printf("pid = %ld\n",(long)getpid());

        struct sigaction act;

        act.sa_flags = SA_SIGINFO;

        sigemptyset(&act.sa_mask);

        act.sa_sigaction = sighandler;

        if ( sigaction(SIGRTMIN+5,&act,0 )  ==  -1)     exit(-1);

        pause();

}

发送信号端send.c:

int main(int argc,char* argv[])

{

        printf("Send process pid = %ld\n",(long) getpid() );

        union sigval value;

        value.sival_int = 5435620;

        pid_t pid = (pid_t)atol( argv[1] );

        sigqueue(pid,SIGRTMIN + 5,value);

}


举个例子

1、定义一个闹钟,约定times秒后,内核向该进程发送一个SIGALRM信号;

2、调用pause函数将进程挂起,内核切换到别的进程运行;

3、times秒后,内核向该进程发送SIGALRM信号,发现其处理动作是一个自定义函数,于是切回用户态执行该自定义处理函数;

4、进入sig_alrm函数时SIGALRM信号被自动屏蔽,从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行特殊的系统调用sigreturn再次进入内核,之后再返回用户态继续执行进程的主控制流程(main函数调用的mytest函数)。

5、pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理 动作。

void sig_alarm(int signum)

{

        printf("I am a custom handler!\n");

}


void mysleep(unsigned int times)

{

        //注册两个信号处理动作

        struct sigaction new,old;

        new.sa_handler=sig_alarm; //信号处理函数

        sigemptyset(&new.sa_mask);//不屏蔽任何信号屏蔽字

        new.sa_flags=0;

        //对SIGALRM 信号的默认处理动作修改为自定义处理动作

        sigaction(SIGALRM,&new,&old);

        alarm(times);

        pause(); //挂起等待

        alarm(1);

        sleep(2);

        alarm(0); //取消闹钟

        //恢复SIGALRM 信号到默认处理动作

        sigaction(SIGALRM,&old,NULL);

        alarm(1);

        sleep(2);

}

int main()

{

        while(1)

        {

                mysleep(2);

                printf("many seconds passed\n");

                printf("###################\n");

        }

        return 0;

}

推荐阅读更多精彩内容