您的位置:首页 > 运维架构 > Linux

linux信号-------初涉

2016-01-30 00:23 405 查看
一.信号的本质

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息

二.信号的分类

1) 可靠信号与不可靠信号:

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在 支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送 函数kill()。

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

非可靠信号一般都有确定的用途及含义, 可靠信号则可以让用户自定义使用

2)实时信号与非实时信号;

早期Unix系统只定义了32种信号,前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL+C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

三.信号的生命周期(处理流程)

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个阶段:

信号诞生

信号在进程中注册

信号的执行和注销

3.1 信号的诞生

信号时间的发生有两个来源:

硬件来源:我们按下键盘上由终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号,比如ctrl+c、ctrl+\; 其它硬件故障。

软件来源:进程执行出错时,如非法段存取、内存访问违规、浮点数溢出、除0运算,内核会给进程发送一个信号;一个进程可以通过系统调用给另一个进程发送信号(实现进程间的通信),最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数。

3.2 信号在目标进程中注册

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。如果发送给一个处于可运行状态的进程,则只置相应的域即可。

进程的task_struct结构中有关于本进程中未决信号的数据成员:struct sigpending pending:

struct sigpending{

        struct sigqueue *head, *tail;/*第一、第二个成员分别指向一个sigqueue类型的结构链(“未决信号链表”)的首尾,

链表中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构*/

sigset_t signal;              /* 第三个成员是进程中所有未决信号集 */
}   


struct sigqueue{
struct sigqueue *next;
siginfo_t info;//siginfo_t结构体见后面信号登记
}



未决信号:所谓未决信号,是指被阻塞的信号,等待被递送的信号


信号在进程中注册指的是信号值加入到未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信 号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配 一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)。

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。

一个非实时信号诞生后

1)如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失

2)如果进程的未决信号中没有相同的信号,则在进程中注册自己。

总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)

3.3 信号的安装

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()只有两个参数,不支持信号传递信息、主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(有两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与sigqueue()系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。

--->signal()

#include<signal.h>

void (*signal (int signum, void (*handler)(int)))(int);

//另一种声明方法
typedef void(*sighandler_t)(int); //命名一个函数指针类型
sighandler_t signal(int signum, sighandler_t handler);

//返回值:若成功,返回最后一次为安装信号signum而调用signal()时的handler的值(处理函数的地址);若出错,则返回SIG_ERR


第一个参数指定信号的值;

第二个参数指定针对前面信号量的处理:

1)参数设定为SIG_IGN,忽略该信号;

2) 参数设定为SIG_DFL,采用系统默认方式处理信号;

3)自己实现处理方式(参数指定一个函数地址);

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号

---->sigaction()

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);


sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些信号等等。

struct sigaction{
void (*sa_handler)(int);    //老类型的信号处理函数指针,类似signal
void (*sa_sigaction)(int, siginfo_t *, void *);  //新类型的信号处理函数指针
sigset_t sa_mask; //
int  sa_flags;//
}


1、前两行的函数指针指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。

2、由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

siginfo_t {
int      si_signo;  /* 信号值,对所有信号有意义*/
int      si_errno;  /* errno值,对所有信号有意义*/
int      si_code;   /* 信号产生的原因,对所有信号有意义*/
union{  /* 联合数据结构,不同成员适应不同信号 */
//确保分配足够大的存储空间
int _pad[SI_PAD_SIZE];
//对SIGKILL有意义的结构
struct{
...
}...
... ...
... ...
//对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
struct{
...
}...
... ...
}


前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是siginfo_t联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。

3.sa_mask是一个包含信号集合的结构体(sigset_t),该结构体内的信号表示在进行信号处理时,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。

针对信号集sigset_t结构体,有一组专门的函数对它进行处理,请看后面的小节。

注:请注意sa_mask指定的信号阻塞的前提条件是:在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。

4.sa_flags是一组掩码的合成值。(|)

SA_NODEFER

在处理信号时,如果又发生了其它的信号,则立即进入其它信号的处理,等其它信号处理完毕后,再继续处理当前的信号。即(入栈)递归地处理,如果sa_flags包含了该掩码,则结构体sigaction的sa_mask将无效!sa_mask在缺省情况下,当前信号本身也不被阻塞,信号可以嵌套发送。

SA_NOMASK 待测试,貌似和SA_NODEFER一样

SA_SIGINFO

指定结构体的信号处理函数指针是哪个有效,如果sa_flags包含该掩码,则sa_sigaction指针有效,否则是sa_handler指针有效。当设定了该标志位时,表示信号附带额参数可以被产地到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)

SA_RESETHAND

处理完毕要捕捉的信号后,将自动撤消信号处理函数的注册,即必须再重新注册信号处理函数,才能继续处理接下来产生的信号。该选项不符合一般的信号处理流程,现已经被废弃。

SA_RESTART

如果在发生信号时,程序正阻塞在某个系统调用,例如调用read()函数,则在处理完毕信号后,接着从阻塞的系统返回。如果不指定该参数,中断处理完毕之后,read函数读取失败。

4.信号的执行和注销

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。

对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。否则待该信号的所有sigqueue处理完毕后再在进程的未决信号集中删除该信号。

当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

5.信号的发送

发送信号的主要函数有:kill()、raise、sigqueue()、alarm()、setitimer()以及abort()。

5.1 kill() raise()

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signum);

raise(int signum);//向进程本身发送信号

//调用raise(signum);等价于调用 kill(getpid(), signum);
//两个函数返回值:若成功,返回0;若出错,返回-1


该系统调用可以用来想任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0

将该信号发送给进程ID为pid的进程。

pid==0

将该信号发送给与发送进程属于同一进程组的所有进程(这些进程的进程组ID等于发送进程的进程组ID),而且发送进程具有权限想这些进程发送信号。这里用的术语“所有进程”不包括实现定义的系统进程集。对于大多数UNIX系统,系统进程集包括内核进程和init(pid为1)。

pid<0

将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。如前所述,所有进程不包括系统进程集中的进程。

pid==-1

将该信号发送给发送进程有权限向他们发送信号的所有进程。如前所述,所有进程不包括系统进程集中的进程。

signum是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。

EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

5.2 sigqueue()发送排队信号

#include<sys/types.h>
#include<signal.h>

int sigqueue(pid_t pid, int signum, const union sigval value);

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


第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值、

typedef union sigval{
int sical_int;
void *sival_ptr;
}sigval_t


信号的信息嵌入在siginfo结构中。除了系统提供的信息,应用程序还可以向信号处理程序传递整数或者指向包含更多信息的缓冲区指针。

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到对应sigaction()注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

使用排队信号必须做一下几个操作:

1)使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志。

2)在sigaction结构的sa_sigaction成员中(而不是通常的sa_handler字段)提供信号处理程序。程序可能允许用户使用sa_handler字段,但不能获取sigqueue函数发送出来的额外信息。

3)使用sigqueue函数发送信号。

5.3 alarm() pause()

#include<unistd>

unsigned int alarm(unsigned int seconds);
//返回值:0或以前设置的闹钟时间的余留秒数

int pause(void);
//返回值:-1,erron设置为EINTR


系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。如果指定的参数seconds为0,则不再发送 SIGALRM信号。如果忽略或不捕获此信号,则其默认动作时终止调用该alarm函数的进程。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

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

alarm+pause 可以实现 sleep的功能(后续)

5.4 abort

#include <stdlib.h>

void abort(void);


向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

5.5 setitimer()

现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下

#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);


linux为每个进程维护3个计时器:

ITIMER_REAL:真实计时器计算的是程序运行的实际时间;---直接

ITIMER_VIRTUAL:虚拟计时器计算的是程序运行在用户态时所消耗的时间(可认为是实际时间减掉(系统调用和程序睡眠所消耗)的时间);---需要了解内核

ITIMER_PROF:实用计时器计算的是程序处于用户态和处于内核态所消耗的时间之和。---常用

例如:有一程序运行,在用户态运行了5秒,在内核态运行了6秒,还睡眠了7秒,则真实计算器计算的结果是18秒,虚拟计时器计算的是5秒,实用计时器计算的是11秒。

用指定的初始间隔和重复间隔时间为进程设定好一个计时器后,该计时器就会定时地向进程发送时钟信号。3个计时器发送的时钟信号分别为:SIGALRM,SIGVTALRM和SIGPROF。

1) int getitimer(int which, struct itimerval *curr_value);

参数whitch指定哪个计时器;

参数curr_value为一结构体的传出参数,用于传出该计时器的初始间隔和重复间隔时间;

返回值:成功0,否则-1

2)int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

参数which指定哪个计时器;

参数value为一结构体的传入参数,指定该计时器的初始间隔时间和重复间隔时间

参数ovalue为一结构体传出参数,用于传出以前的计时器时间设置

返回值:成功0;否则-1

struct itimerval {
struct timeval it_interval; /* next value */        //重复间隔
struct timeval it_value;  /* current value */        //初始间隔
};

struct timeval {
long tv_sec;            /* seconds */            //时间的秒数部分
long tv_usec;           /* microseconds */    //时间的微秒部分
};


6.信号集及信号集操作函数:

信号集被定义为一种数据类型:

typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t


信号集用来描述信号的集合,每个信号占用一位。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

#include <signal.h>

int sigemptyset(sigset_t *set)           //初始化由set指定的信号集,信号集里面的所有信号被清空;
int sigfillset(sigset_t *set)            //调用该函数后,set指向的信号集中将包含linux支持的64种信号;
int sigaddset(sigset_t *set, int signum)     //在set指向的信号集中加入signum信号;
int sigdelset(sigset_t *set, int signum)     //在set指向的信号集中删除signum信号;
int sigismember(const sigset_t *set, int signum);    //判断signum是否包含在set中(是:返回1,否:0)
int sigpending(sigset_t *set);         //将被阻塞的信号集合由参数set指针返回(挂起信号)


7.信号阻塞与信号未决

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:

#include <signal.h>

int  sigprocmask(int  how, const sigset_t *set, sigset_t *oldset));//若成功返回0;出错,返回-1

int sigpending(sigset_t *set));      //若成功返回0;出错,返回-1

int sigsuspend(const sigset_t *mask));  //返回值:-1,并将erron设置为EINTR


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

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

函数sigprocmask是全程阻塞,在sigprocmask中设置 了阻塞集合后,被阻塞的信号将不能再被信号处理函数捕捉,直到重新设置阻塞信号集合。

参数how的值为如下3者之一:

a:SIG_BLOCK ,将参数2的信号集合添加到进程原有的阻塞信号集合中

b:SIG_UNBLOCK ,从进程原有的阻塞信号集合移除参数2中包含的信号

c:SIG_SETMASK,重新设置进程的阻塞信号集为参数2的信号集

若oldset是非空指针,那么进程的当前信号屏蔽字通过oldset返回。

若set是一个非空指针,how只是如何修改当前信号屏蔽字。SIG_BLOCK是或操作,而SIG_SETMASK是赋值操作。注意,不能阻塞SIGKILL和SIGSTOP信号。

若set是一个空指针,则不改变该进程的信号屏蔽字,how的值也无意义。

2)int sigpending(sigset_t *set)); 

测试信号~

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

3) int sigsuspend(const sigset_t *mask));

(待研究)



(未完待续)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: