isware

linux多线程及信号处理

linux信号种类

1、可靠信号和不可靠信号

      "不可靠信号"
  
    Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在 早期机制上的信号叫做"不可靠信号",信号值小于SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。这就是"不可靠信号"的来源。他的主要问题是:
  
    • 进程每次处理信号后,就将对信号的响应配置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户假如不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
  
   • 信号可能丢失,后面将对此周详阐述。
  
   因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应连同信号可能丢失。
  
    Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。
  
    "可靠信号"
      随着时间的发展,实践证实了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现"可靠信 号"。由于原来定义的信号已有许多应用,不好再做改变,最终只好又新增加了一些信号,并在一开始就把他们定义为可靠信号,这些信号支持排队,不会丢失。同 时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。POSIX.4对可靠信号机制做了标准 化。但是,POSIX只对可靠信号机制应具备的功能连同信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。
      信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装 函数sigation()连同信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
      注:不要有这样的误解:由sigqueue()发送、sigaction安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值 位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。信号的可靠和不可靠只和信号值有关,和信号的发送及安装 函数无关。现在linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾 也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。
      对于现在linux的两个信号安装函数:signal()及sigaction()来说,他们都不能把SIGRTMIN以前的信号变成可靠信号 (都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安 装的信号都能传递信息给信号处理函数(对任何信号这一点都成立),而经过signal安装的信号却不能向信号处理函数传递信息。对于信号发送函数来说也是 相同的。

2、实时信号和非实时信号

      早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信 号已有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这确保了发送的多个实时信号都 被接收。实时信号是POSIX标准的一部分,可用于应用进程。
      非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

Linux的信号的种类有60多种。可以用kill -l命令查看所有的信号,每个信号的含义如下:

1) SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程

2)SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止里程。

3)SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。

4)SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件

5)SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。

6 ) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。

7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。

8)SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。

9)SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。

10)SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。

11)SIGSEGV:指示进程进行了无数内存访问。默认动作为终止进程并产生core文件。

12)SIGUSR2:这是另外一个用户自定义信号 ,程序员可以在程序中定义 并使用该信号。默认动作为终止进程。1

13)SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。

14 ) SIGALRM:定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。

15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。

16)SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。

17)SIGCONT:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为终止进程。

18)SIGTTIN:停止进程的运行,但该信号可以被处理和忽略。按下<ctrl+z>组合键发出灾个信号。默认动作为暂停进程。

19)SIGTSTP:停止进程的运行,可该信号可以被处理可忽略。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。

21)SIGTTOU:该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。

22)SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。默认动作为忽略该信号。

23)SIGXFSZ:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。

24)SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。

25)SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。

26)SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。

27)SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。

28)SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。

29)SIGPWR:关机。默认动作为终止进程。

30)SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。

31)SIGRTMIN~(64)SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。


进程对信号的响应

 

   进程能够通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP; (2)捕获信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作,周详情况请参考 [2]连同其他资料。注意,进程对实时信号的缺省反应是进程终止。
   Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。

信号的发送


发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()连同abort()。
1、kill()
#include
#include
int kill(pid_t pid,int signo)
参数pid的值 信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的任何进程
pid=-1 除发送进程自身外,任何进程ID大于1的进程
Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,连同当前进程是否具备 向目标发送信号的权限(root权限的进程能够向任何进程发送信号,非root权限的进程只能向属于同一个session或同一个用户的进程发送信 号)。
Kill()最常用于pid>0时的信号发送,调用成功返回 0; 否则,返回 -1。注:对于pid<0时的情况,对于哪些进程将接受信号,各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c即可,上 表中的规则是参考red hat 7.2。
2、raise()
#include
int raise(int signo)
3、sigqueue()
#include
#include
int sigqueue(pid_t pid, int sig, const union sigval val)
调用成功返回 0;否则,返回 -1。
sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,和函数sigaction()配合使用。
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。假如 signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性连同当前进程是否有权限向目标进程发送信号。
在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指的是信号处理函数由 sigaction安装,并设定了sa_sigaction指针,稍后将阐述)的siginfo_t结构中,这样信号处理函数就能够处理这些信息了。由于 sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。
注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数; sigqueue()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的任何相同信号,都被合并为一个信号。
4、alarm()
#include
unsigned int alarm(unsigned int seconds)
专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm()调用都将无效。假如参数seconds为零,那么进程内将不再包含任何闹钟时间。
5、setitimer()
#include
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
setitimer()比alarm功能强大,支持3种类型的定时器:
• ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
• ITIMER_VIRTUAL 设定程式执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
• ITIMER_PROF 设定进程执行连同内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;
Setitimer()第一个参数which指定定时器类型(上面三种之一);第二个参数是结构itimerval的一个实例,结构itimerval形式见附录1。第三个参数可不做处理。
Setitimer()调用成功返回0,否则返回-1。
6、abort()
#include
void abort(void);
向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程配置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

信号安装

假如进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。
linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现, 是库函数。他只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现: sys_signal连同sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来和 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。
1、signal()
#include
void (*signal(int signum, void (*handler))(int)))(int);
假如该函数原型不容易理解的话,能够参考下面的分解方式来理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,能够忽略该信号(参数设为SIG_IGN);能够采用系统默认方式处理信号(参数设为SIG_DFL);也能够自己实现处理方式(参数指定一个函数地址)。
假如signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。
2、sigaction()
#include
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,能够为除SIGKILL及SIGSTOP外的任何一 个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构 sigaction的实例中,指定了对特定信号的处理,能够为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存原来对相应信号 的处理,可指定oldact为NULL。假如把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。
sigaction结构定义如下:

struct sigaction {
union{
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int,struct siginfo *, void *);
}_u
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
}

其中,sa_restorer,已过时,POSIX不支持他,不应再被使用。
1、联合数据结构中的两个元素_sa_handler连同*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了能够是用户自定义的处理函数外,还能够为SIG_DFL(采用缺省的处理方式),也能够为SIG_IGN(忽略信号)。
2、由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由_sa_sigaction是 指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),他指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使 用(posix没有规范使用该参数的标准),第二个参数是指向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{
...
}...
... ...
}
}

注:为了更便于阅读,在说明问题时常把该结构表示为附录2所表示的形式。
siginfo_t结构中的联合数据成员确保该结构适应任何的信号,比如对于实时信号来说,则实际采用下面的结构形式:

typedef struct {
int si_signo;
int si_errno;
int si_code;
union sigval si_value;
} siginfo_t;

结构的第四个域同样为一个联合数据结构:

union sigval {
int sival_int;
void *sival_ptr;
}
采用联合数据结构,说明siginfo_t结构中的si_value要么持有一个4字节的整数值,要么持有一个指针,这就构成了和信号相关的数 据。在信号的处理函数中,包含这样的信号相关数据指针,但没有规定具体如何对这些数据进行操作,操作方法应该由程式研发人员根据具体任务事先约定。
前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数 据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就能够让信号传递一些附加信息。信号能够传递信息对程式研发是很有意义 的。
信号参数的传递过程可图示如下:

3、sa_mask指定在信号处理程式执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或SA_NOMASK标志位。
注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。
4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是 SA_SIGINFO,当设定了该标志位时,表示信号附带的参数能够被传递到信号处理函数中,因此,应该为sigaction结构中的 sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,配置该标志变得毫无意义。即使为 sa_sigaction指定了信号处理函数,假如不配置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信 息的访问都将导致段错误(Segmentation fault)。
注:很多文献在阐述该标志位时都认为,假如配置了该标志位,就必须定义三参数信号处理函数。实际不是这样的,验证方法很简单:自己实现一个单一 参数信号处理函数,并在程式中配置该标志位,能够察看程式的运行结果。实际上,能够把该标志位看成信号是否传递参数的开关,假如配置该位,则传递参数;否 则,不传递参数。

Linux信号阻塞和信号未决

1. 信号掩码——被阻塞的信号集
  每个进程都有一个用来描述哪些信号传送来将被阻塞的信号集,如果某种信号在某个进程的阻塞信号集中,则传送到该进程的此种信号将会被阻塞。当前被进程阻塞的信号集也叫信号掩码,类型为sigset_t。每个进程都有自己 的信号掩码,且创建子进程时,子进程会继承父进程的信号掩码。
2. 信号阻塞和忽略的区别
  阻塞的概念与忽略信号是不同的:操作系统在信号被进程解除阻塞之前不会将信号传递出去,被阻塞的信号也不会影响进程的行为,信号只是暂时被阻止传递;当进程忽略一个信号时,信号会被传递出去,但进程将信号丢弃。
3. 信号集的操作
  信号集可以由以下几个函数操作:
  int sigemptyset(sigset_t *set); //清空信号集
  int sigfillset(sigset_t *set); //将所有信号填充进set中
  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); //判断signnum是不是包含在set中,在返回1,不在返回0
  初始化往往可以用sigemptyset()将信号集清空,再用sigaddset()向信号集中添加信号;或者可以使用sigfillset()将所有信号添加到信号集,再用sigdelset()将某信号从中删除掉。
4. sigprocmask()介绍
  可以使用函数sigprocmask()来检查或者修改进程的信号掩码。函数信息如下:
  #include <signal.h>
  int sigprocmask ( int how, const sigset_t *restrict set,
  sigset_t *restrict old );
  参数how 是一个整数,说明信号掩码的修改方式:
  SIG_BLOCK --- 将set指向的信号集中的信号添加到当前阻塞信号集中;
  SIG_UNBLOCK --- 从当前阻塞信号集中移除set指向的信号集中的信号;
  SIG_SETMASK --- 指定set所指向的信号集为当前阻塞信号集。
  此外,如果参数set 为NULL, 说明不需要修改,如果old 为NULL,sigprocmask会将修改之前的信号集放在*old 之中返回。
5.sigaction()回顾
  在前面有用过sigaction()函数:
  include <signal.h>
  int sigaction(int signum,const struct sigaction *act,
  const struct sigaction *oldact);
  该函数是用于注册一个信号处理函数。参数结构体sigaction与函数同名,具体信息如下:
  struct sigaction {
  void (*sa_handler)(int); //老类型的信号处理函数指针
  void (*sa_sigaction)(int, siginfo_t *, void *);//新类型的信号处理函数指针
  sigset_t sa_mask; //将要被阻塞的信号集合
  int sa_flags; //信号处理方式掩码
  void (*sa_restorer)(void); //保留
  }
  5.1 sa_handler:一个函数指针,用于指向原型为void handler(int)的信号处理函数地址(老类型的信号处理函数);
  5.2 sa_sigaction:也是一个函数指针,用于指向原型为:
  void handler(int (新类型的信号处理函数);
  三个参数的含义为:
  iSignNum:传入的信号
  pSignInfo:与该信号相关的一些信息,它是个结构体
  pReserved:保留,现没用
   5.3 sa_handler和sa_sigaction只应该有一个生效,如果想采用老的信号处理机制,就应该让sa_handler指向正确的信号处理函数; 否则应该让sa_sigaction指向正确的信号处理函数,并且让字段sa_flags包含SA_SIGINFO选项。
  5.4 sa_mask是一个包含信号集合的结构体,该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。该信号集可以用前面标题3提到的5个函数来进行操作。
  5.5 字段sa_flags是一组掩码的合成值,指示信号处理时所应该采取的一些行为,各掩码的含义为:
  (1)SA_RESETHAND ---处理完毕要捕捉的信号后,将自动撤消信号处理函数的注册,即必须再重新注册信号处理函数,才能继续处理接下来产生的信号。
  (2)SA_NODEFER ---在处理信号时,如果又发生了其它的信号,则立即进入其它信号的处理,等其它信号处理完毕后,再继续处理当前的信号,即递规地处理。如果sa_flags包含了该掩码,则结构体sigaction的sa_mask将无效;
  (3)SA_RESTART--- 如果在发生信号时,程序正阻塞在某个系统调用,例如调用read()函数,则在处理完毕信号后,接着从阻塞的系统返回。该掩码符合普通的程序处理流程,所以一般来说,应该设置该掩码,否则信号处理完后,阻塞的系统调用将会返回失败;
  (4)SA_SIGINFO ---指示结构体的信号处理函数指针是哪个有效,如果sa_flags包含该掩码,则sa_sigactiion指针有效,否则是sa_handler指针有效。
  需要注意的是:
   函数sigprocmask是全程阻塞,在sigprocmask中设置了阻塞集合后,被阻塞的信号将不能再被信号处理函数捕捉,直到重新设置阻塞信号 集合。而在sigaction()注册信号处理函数时,选择阻塞的信号集只是在处理捕捉的信号时,才对指定的其他信号进行阻塞。
6、信号未决
sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的任何信号,在set指向的信号集中返回结果。
sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno配置为EINTR。

计时器与信号

睡眠函数
Linux下有两个睡眠函数,原型为:
#include <unistd.h>
   unsigned int sleep(unsigned int seconds);
   void usleep(unsigned long usec);
函数sleep让进程睡眠seconds秒,函数usleep让进程睡眠usec毫秒。
sleep睡眠函数内部是用信号机制进行处理的,用到的函数有:
   #include <unistd.h>
   unsigned int alarm(unsigned int seconds); //告知自身进程,要进程在seconds秒后自动产生一个//SIGALRM的信号,
   int pause(void);                       //将自身进程挂起,直到有信号发生时才从pause返回

示例:模拟睡眠3秒:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void SignHandler(int iSignNo)
{
    printf("signal:%d\n",iSignNo);
}

int main()
{
    signal(SIGALRM,SignHandler);
    alarm(3);
    printf("Before pause().\n");
    pause();
    printf("After pause().\n");
    return 0;
}
注意:因为sleep在内部是用alarm实现的,所以在程序中最好不要sleep与alarm混用,以免造成混乱。

时钟处理
Linux为每个进程维护3个计时器,分别是真实计时器、虚拟计时器和实用计时器。
真实计时器计算的是程序运行的实际时间;
虚拟计时器计算的是程序运行在用户态时所消耗的时间(可认为是实际时间减掉(系统调用和程序睡眠所消耗)的时间);
实用计时器计算的是程序处于用户态和处于内核态所消耗的时间之和。
例如:有一程序运行,在用户态运行了5秒,在内核态运行了6秒,还睡眠了7秒,则真实计算器计算的结果是18秒,虚拟计时器计算的是5秒,实用计时器计算的是11秒。
用指定的初始间隔和重复间隔时间为进程设定好一个计时器后,该计时器就会定时地向进程发送时钟信号。3个计时器发送的时钟信号分别为:SIGALRM,SIGVTALRM和SIGPROF。
用到的函数与数据结构:
#include <sys/time.h>

//获取计时器的设置
//which指定哪个计时器,可选项为ITIMER_REAL(真实计时器)、ITIMER_VITUAL(虚拟计时器、ITIMER_PROF(实用计时器))
//value为一结构体的传出参数,用于传出该计时器的初始间隔时间和重复间隔时间
//如果成功,返回0,否则-1
int getitimer(int which, struct itimerval *value);

//设置计时器
//which指定哪个计时器,可选项为ITIMER_REAL(真实计时器)、ITIMER_VITUAL(虚拟计时器、ITIMER_PROF(实用计时器))
//value为一结构体的传入参数,指定该计时器的初始间隔时间和重复间隔时间
//ovalue为一结构体传出参数,用于传出以前的计时器时间设置。
//如果成功,返回0,否则-1
int setitimer(int which, const struct itimerval *value, struct itimer val *ovalue);

struct itimerval {
struct timeval it_interval; /* next value */   //重复间隔
struct timeval it_value;    /* current value */ //初始间隔
};
struct timeval {
long tv_sec;                /* seconds */    //时间的秒数部分
long tv_usec;               /* microseconds */     //时间的微秒部分
};

示例:启用真实计时器的进行时钟处理
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

void TimeInt2Obj(int imSecond,timeval *ptVal)
{
    ptVal->tv_sec=imSecond/1000;
    ptVal->tv_usec=(imSecond%1000)*1000;
}

void SignHandler(int SignNo)
{
    printf("Clock\n");
}

int main()
{
    signal(SIGALRM,SignHandler);
    itimerval tval;
    TimeInt2Obj(1,&tval.it_value);   //设初始间隔为1毫秒,注意不要为0
    TimeInt2Obj(1500,&tval.it_interval); //设置以后的重复间隔为1500毫秒
    setitimer(ITIMER_REAL,&tval,NULL);
    while(getchar()!=EOF);
    return 0;
}


信号生命周期

从信号发送到信号处理函数的执行完毕
对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,能够分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。

下面阐述四个事件的实际意义:
1. 信号"诞生"。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时连同调用信号发送函数kill()或sigqueue()等)。
2. 信号在目标进程中"注册";进程的task_struct结构中有关于本进程中未决信号的数据成员:
3. struct sigpending pending:
4. struct sigpending{
5. struct sigqueue *head, **tail;
6. sigset_t signal;
7. };
第三个成员是进程中任何未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已知道这些信号的 存在,但还没来得及处理,或该信号被进程阻塞。
注:
当一个实时信号发送给一个进程时,不管该信号是否已在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着 同一个实时信号能够在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为他分配一个结构来登记该信号信息,并把 该结构添加在未决信号链尾,即任何诞生的实时信号都会在目标进程中注册);
当一个非实时信号发送给一个进程时,假如该信号已在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这 意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)、假如发现相同的信号已在目标结构 中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)、假如进程的未决信号中没有相同信号,则在进程中注册自己)。
8. 信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。假如存在未决信号等待处理且该 信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时和非实时信号 是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信 号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:假如只 占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除 该信号(信号注销完毕)。
进程在执行信号相应处理函数之前,首先要把信号在进程中注销。
9. 信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响完全结束。
注:
1)信号注册和否,和发送信号的函数(如kill()或sigqueue()等)连同信号安装函数(signal()及sigaction()) 无关,只和信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被 注册)。
2)在信号被注销到相应的信号处理函数执行完毕这段时间内,假如进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。
信号编程注意事项

1. 防止不该丢失的信号丢失。假如对八中所提到的信号生命周期理解深刻的话,很容易知道信号会不会丢失,连同在哪里丢失。
2. 程式的可移植性
考虑到程式的可移植性,应该尽量采用POSIX信号函数,POSIX信号函数主要分为两类:
o POSIX 1003.1信号函数: Kill()、sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、 sigismember()、sigpending()、sigprocmask()、sigsuspend()。
o POSIX 1003.1b信号函数。POSIX 1003.1b在信号的实时性方面对POSIX 1003.1做了扩展,包括以下三个函数: sigqueue()、sigtimedwait()、sigwaitinfo()。其中,sigqueue主要针对信号发送,而 sigtimedwait及sigwaitinfo()主要用于取代sigsuspend()函数,后面有相应实例。
o #include
o int sigwaitinfo(sigset_t *set, siginfo_t *info).
该函数和sigsuspend()类似,阻塞一个进程直到特定信号发生,但信号到来时不执行信号处理函数,而是返回信号值。因此为了避免执行相应的信号处理函数,必须在调用该函数前,使进程屏蔽掉set指向的信号,因此调用该函数的典型代码是:
sigset_t newmask;
int rcvd_sig;
siginfo_t info;

sigemptyset(&newmask);
sigaddset(&newmask, SIGRTMIN);
sigprocmask(SIG_BLOCK, &newmask, NULL);
rcvd_sig = sigwaitinfo(&newmask, &info)
if (rcvd_sig == -1) {
..
}
调用成功返回信号值,否则返回-1。sigtimedwait()功能相似,只但是增加了一个进程等待的时间。
3. 程式的稳定性。
为了增强程式的稳定性,在信号处理函数中应使用可重入函数。
信号处理程式中应当使用可再入(可重入)函数(注:所谓可重入函数是指一个能够被多个任务调用的过程,任务在调用时不必担心数据是否会出错)。因为进程在 收到信号后,就将跳转到信号处理函数去接着执行。假如信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样 进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。不可再入函数在信号处理函数中被视为不安全函数。
满足下列条件的函数多数是不可再入的:(1)使用静态的数据结构,如getlogin(),gmtime(),getgrgid(), getgrnam(),getpwuid()连同getpwnam()等等;(2)函数实现时,调用了malloc()或free()函数;(3)实现 时使用了标准I/O函数的。The Open Group视下列函数为可再入的:
_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、 cfsetospeed()、chdir()、chmod()、chown()、close()、creat()、dup()、dup2()、 execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、 geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、 kill()、link()、lseek()、mkdir()、mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、setgid ()、setpgid()、setsid()、setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、 sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、 stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、 tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。
即使信号处理函数使用的都是"安全函数",同样要注意进入处理函数时,首先要保存errno的值,结束时,再恢复原值。因为,信号处理过程中,errno 值随时可能被改变。另外,longjmp()连同siglongjmp()没有被列为可再入函数,因为不能确保紧接着两个函数的其他调用是安全的。
信号应用实例

信号的安装(配置信号关联动作)
linux下的信号应用并没有想象的那么恐怖,程式员所要做的最多只有三件事情:
1. 安装信号(推荐使用sigaction());
2. 实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);
3. 发送信号,推荐使用sigqueue()。
实际上,对有些信号来说,只要安装信号就足够了(信号处理方式采用缺省或忽略)。其他可能要做的无非是和信号集相关的几种操作。
实例一:信号发送及处理
实现一个信号接收程式sigreceive(其中信号安装由sigaction())。
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
int sig;
sig=atoi(argv[1]);

sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
act.sa_sigaction=new_op;

if(sigaction(sig,&act,NULL) < 0)
{
printf("install sigal error\n");
}

while(1)
{
sleep(2);
printf("wait for the signal\n");
}
}
void new_op(int signum,siginfo_t *info,void *myact)
{
printf("receive signal %d", signum);
sleep(5);
}
说明,命令行参数为信号值,后台运行sigreceive signo &,可获得该进程的ID,假设为pid,然后再另一终端上运行kill -s signo pid验证信号的发送接收及处理。同时,可验证信号的排队问题。
注:能够用sigqueue实现一个命令行信号发送程式sigqueuesend
实例二:信号传递附加信息
主要包括两个实例:
1. 向进程本身发送信号,并传递指针参数;
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
union sigval mysigval;
 int i;
 int sig;
 pid_t pid;
 char data[10];
 memset(data,0,sizeof(data));
 for(i=0;i < 5;i )
 data[i]='2';
 mysigval.sival_ptr=data;

 sig=atoi(argv[1]);
 pid=getpid();

 sigemptyset(&act.sa_mask);
 act.sa_sigaction=new_op;//三参数信号处理函数
 act.sa_flags=SA_SIGINFO;//信息传递开关
 if(sigaction(sig,&act,NULL) < 0)
 {
 printf("install sigal error\n");
 }
 while(1)
 {
 sleep(2);
 printf("wait for the signal\n");
 sigqueue(pid,sig,mysigval);//向本进程发送信号,并传递附加信息
 }

 }

 void new_op(int signum,siginfo_t *info,void *myact)//三参数信号处理函数的实现
 {
 int i;
 for(i=0;i<10;i )
 {
 printf("%c\n ",(*( (char*)((*info).si_ptr) i)));
 }
 printf("handle signal %d over;",signum);
 }
这个例子中,信号实现了附加信息的传递,信号究竟如何对这些信息进行处理则取决于具体的应用。
2、 不同进程间传递整型参数:把1中的信号发送和接收放在两个程式中,并且在发送过程中传递整型参数。
信号接收程式:
#include
#include
#include
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
int sig;
pid_t pid;

pid=getpid();
sig=atoi(argv[1]);

sigemptyset(&act.sa_mask);
act.sa_sigaction=new_op;
act.sa_flags=SA_SIGINFO;
if(sigaction(sig,&act,NULL)<0)
{
printf("install sigal error\n");
}
while(1)
{
sleep(2);
printf("wait for the signal\n");
}

}
void new_op(int signum,siginfo_t *info,void *myact)
{
printf("the int value is %d \n",info->si_int);
}
信号发送程式:命令行第二个参数为信号值,第三个参数为接收进程ID。
main(int argc,char**argv)
{
pid_t pid;
int signum;
union sigval mysigval;

signum=atoi(argv[1]);
pid=(pid_t)atoi(argv[2]);
mysigval.sival_int=8;//不代表具体含义,只用于说明问题

if(sigqueue(pid,signum,mysigval)==-1)
printf("send error\n");
sleep(2);
}
实例三:信号阻塞及信号集操作
#include "signal.h"
#include "unistd.h"
static void my_op(int);
main()
{
sigset_t new_mask,old_mask,pending_mask;
struct sigaction act;

sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
act.sa_sigaction=(void*)my_op;
if(sigaction(SIGRTMIN 10,&act,NULL))
printf("install signal SIGRTMIN 10 error\n");

sigemptyset(&new_mask);
sigaddset(&new_mask,SIGRTMIN 10);
if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))
printf("block signal SIGRTMIN 10 error\n");

sleep(10);
printf("now begin to get pending mask and unblock SIGRTMIN 10\n");
if(sigpending(&pending_mask)<0)
printf("get pending mask error\n");
if(sigismember(&pending_mask,SIGRTMIN 10))
printf("signal SIGRTMIN 10 is pending\n");

if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)
printf("unblock signal error\n");
printf("signal unblocked\n");

sleep(10);
}
static void my_op(int signum)
{
printf("receive signal %d \n",signum);
}

编译该程式,并以后台方式运行。在另一终端向该进程发送信号(运行kill -s 42 pid,SIGRTMIN 10为42),查看结果能够看出几个关键函数的运行机制,信号集相关操作比较简单。
注:在上面几个实例中,使用了printf()函数,只是作为诊断工具,pringf()函数是不可重入的,不应在信号处理函数中使用。

用sigqueue实现的命令行信号发送程式sigqueuesend,命令行第二个参数是发送的信号值,第三个参数是接收该信号的进程ID,能够配合实例一使用:
int main(int argc,char**argv)
{
pid_t pid;
int sig;
sig=atoi(argv[1]);
pid=atoi(argv[2]);
sigqueue(pid,sig,NULL);
sleep(2);
}

posted on 2011-07-27 16:28 艾斯维亚 阅读(10125) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理