APUE读书笔记-第十章 信号

今天开始写写APUE读书笔记,又不是从第一章开始尴尬。好了,书归正传,APUE这本书非常好,不过有些内容过于老旧,因此结合书本知识与动手实践,做一点读书笔记

第十章主要对信号进行分析,既然谈到信号首先让我们来看看什么是信号,这里书中并没有给出太明确的答案,APUE中只是说:“首先,每个信号都有一个名字”,我认为这个说法并没有交代清楚信号的本质,仅是强调了信号类似于某种“宏定义”。在这里给大家还是先分享一篇blog:http://blog.chinaunix.net/uid-24774106-id-4061386.html

这是一个系列blog共四篇,这四篇blog对signal机制的源码进行了一定程度的分析与对比,我本人的水平还没有达到分析源码的程度,所以就站在前人的肩膀,进一步学习,在此也向我曾经引用的博主们致以我最真诚的感谢(今天怎么这么感性)。又跑题了,上述博客中对于信号的理解是“  信号是一种机制,是在软件层次对中断机制的一种模拟,内核让某进程意识到某特殊事情发生了。

通过上面的分析,我们可以知道,信号首先代表着某种事件的发生,根据APUE,信号共包括以下四种情况:

  1. 当用户按某些终端键时,引发终端产生的信号。这里一个例子就是,在程序运行过程中,按下ctrl+c可产生终端信号。
  2. 硬件异常产生信号:除数为0、无效的内存引用等。例如,对执行一个无效内存引用的进程产生SIGSEGV信号。
  3. 进程调用kill函数可将任意信号发送给另一个进程或进程组。用户可用kill命令(注意此处是命令)将信号发送给其他进程。
  4. 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。

对于以上四种情况的信号,共有三种处理方法,分别是:

  1. 忽略此信号。大多数信号都可使用这种方式处理,但有两种信号却决不能被忽略,SIGKILL、SIGSTOP,以上两种信号是内核和超级用户使进程终止的可靠方法,因此不能被忽略。
  2. 捕捉信号。要捕获一个信号,即是要程序显示的通知内核,某一信号发生时采取某种处理方式,要显示通知内核就要使用函数signal(后文会详细分析)。
  3. 执行默认动作。

好了分析到此,就产生了一个问题,我的系统中到底支持多少种信号?

对于以上问题可通过两种方法,一种是分析程序源码,另一种是通过命令。首先来看通过命令的方法,通过“kill -l”命令可查看当前系统中所支持的信号的种类。

当前系统中所支持的信号又可以分为两类分别为“实时信号”与“非实时信号”。

  1. 当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。
  2. 当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。

以上两点内容中所谈到的注册不是指通过singal函数在信号值与信号处理函数间建立映射关系的过程,(前半句看不懂可以忽略)此处的注册是指在进程控制块中对尚未处理的信号值进行记录。

将我参考的博客分享给大家http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html

再进一步已经知道了信号的种类,那么每种信号的默认操作是什么?在此就不给大家做一一讲解了,分享一篇blog吧http://www.jb51.net/LINUXjishu/173601.html

最后给大家补充一点关于kill的知识http://www.cnblogs.com/peida/archive/2012/12/20/2825837.html

好了以上就是通过命令的方法,接下来看看通过程序源码的方法,由于我现在还没有安装内核源码树,所以只能看有关于头文件中的内容,在我以前的博客中已经阐述过如何通过头文件查找所需要的内容(安利一下自己的博客)。这里直接给出结果,有关于信号值的定义位于/usr/include/x86_64-linux-gnu/bits/signum,不过以上内容来自于glibc,并非来自于linux kernel。

分析了这么多,接下来要真正开始使用signal函数了,signal定义位于/usr/include/signal.h中,函数定义如下:

/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);

/* The X/Open definition of `signal' specifies the SVID semantic.  Use
   the additional function `sysv_signal' when X/Open compatibility is
   requested.  */
extern __sighandler_t __sysv_signal (int __sig, __sighandler_t __handler)
     __THROW;
#ifdef __USE_GNU
extern __sighandler_t sysv_signal (int __sig, __sighandler_t __handler)
     __THROW;
#endif

/* Set the handler for the signal SIG to HANDLER, returning the old
   handler, or SIG_ERR on error.
   By default `signal' has the BSD semantic.  */
__BEGIN_NAMESPACE_STD
#ifdef __USE_MISC
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
     __THROW;
#else
/* Make sure the used `signal' implementation is the SVID version. */
# ifdef __REDIRECT_NTH
extern __sighandler_t __REDIRECT_NTH (signal,
				      (int __sig, __sighandler_t __handler),
				      __sysv_signal);
# else
#  define signal __sysv_signal
# endif
#endif
__END_NAMESPACE_STD

#ifdef __USE_XOPEN
/* The X/Open definition of `signal' conflicts with the BSD version.
   So they defined another function `bsd_signal'.  */
extern __sighandler_t bsd_signal (int __sig, __sighandler_t __handler)
     __THROW;
#endif

#ifdef __USE_MISC
/* SVID names for the same things.  */
extern __sighandler_t ssignal (int __sig, __sighandler_t __handler)
     __THROW;

可以看到除了标准的定义形式外:

extern __sighandler_t signal (int __sig, __sighandler_t __handler)

还有多种不同的定义形式。其中__handler的值是常量SIG_IGN、SIG_DFL或接到此信号后要调用的函数的地址。首先让我们来看看signal函数的原型,signal 函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值。这里signal的返回值是一个函数地址,该函数有一个整型参数(即最后的int)。这里要区分开一件事:首先signal是一个函数,既然是函数就要有函数参数与返回值,其中参数部分就是一个整型数与一个函数指针,该函数指针指向信号处理程序;而signal函数的返回值同样也是一个函数指针,但该指针所指向的函数仅需要一个函数(也就是最后的int),并且无返回值(注意与signal函数的返回值相区别);最后signal函数所返回的函数指针就是参数中的信号处理程序。

其中对于常量的定义如下:

/* Fake signal functions.  */
#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

同样是位于signums.h中。

再来看看signal.c文件,signal.c来自于glibc源码,相关内容如下:

__sighandler_t
__bsd_signal (int sig, __sighandler_t handler)
{
  struct sigaction act, oact;

  /* Check signal extents to protect __sigismember.  */
  if (handler == SIG_ERR || sig < 1 || sig >= NSIG)
    {
      __set_errno (EINVAL);
      return SIG_ERR;
    }

  act.sa_handler = handler;
  if (__sigemptyset (&act.sa_mask) < 0
      || __sigaddset (&act.sa_mask, sig) < 0)
    return SIG_ERR;
  act.sa_flags = __sigismember (&_sigintr, sig) ? 0 : SA_RESTART;
  if (__sigaction (sig, &act, &oact) < 0)
    return SIG_ERR;

  return oact.sa_handler;
}
weak_alias (__bsd_signal, bsd_signal)
weak_alias (__bsd_signal, signal)
weak_alias (__bsd_signal, ssignal)

又是弱别名方法,其中signal、ssignal、bsd_signal函数在signal.h中出现过。因此我们基本可以确定signal的实现是“__bsd_signal”,同时通过“act.sa_handler = handler;”这一句可以得知信号处理函数先赋给了act.sa_handler,这一赋值语句在一定程度上反映了信号处理程序的安装过程。而“__bsd_signal”主要是调用了__sigaction函数,__sigaction在glibc中有多个实现,由于我还没有编译glibc,因此就无法确定链接了哪个实现。但可以确定的是若函数正确返回,则函数指针指向信号处理函数。以上就是对signal函数的一点简单分析。

这里还有两点问题需要注意,这两点分别是有关于exec、fork函数与信号处理函数之间的关系:

  1. 当执行一个程序时,所有信号的状态都是系统默认或忽略。通常信号都被设置为他们的默认动作,除非调用exec的进程忽略该信号。确切的将exec将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变。一个进程原先要捕捉的信号,当其执行一个新程序后,就不能在捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义。
  2. 当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

以上内容就是对10.1到10.3节内容的总结。

接下来看10.4这一小节,本小节主要分析了早期的unix信号机制存在缺陷,但glibc中已经对存在的问题进行了修正,我分享的第一篇博客中就对这一部分进行了详细分析。

总结起来glibc的优势主要体现在以下三点:

1. 传统的signal系统调用,他的信号处理函数是一次性的,执行过后,该信号的信号处理函数就变成了SIG_DFL。而glibc中信号处理函数一次安装之后可多次利用。传统的signal系统调用之所以会在第二次信号到来时采取默认方式是由于signal系统调用首先设置了标志位“SA_ONESHOT| SA_NOMASK”,而“SA_ONESHOT”的作用就是指明信号处理函数一旦被调用过就恢复到默认的信号处理函数去

源码如下(这是我直接摘抄第一篇博客中贴出的源码,正确性我还没有验证):

   if (ka->sa.sa_flags & SA_ONESHOT)
                ka->sa.sa_handler = SIG_DFL;
可以看到若已经置位SA_ONESHOT,则将信号处理函数变为默认方式。

2.早期的signal函数,没有屏蔽正在处理的信号。早期的signal函数在调用时会设置SA_NOMASK|SA_ONESHOT标志,这两个标志位一定要结合起来看才有意义,首先SA_NOMASK 表示不启用位图屏蔽信号,即这个时候可以运行信号嵌套(所谓信号嵌套是指信号处理函数结束前,无法中断当前的信号处理函数而对新一次到来的信号进行处理,类似于中断嵌套)。若设置了SA_NOMASK 则所处理的信号的另一次出现,可以中断信号处理程序,而对于新信号的处理方式由于设置了SA_ONESHOT标志位,因此信号函数在此时已变为默认方式。这也就解释了第一篇博客中给出的实例,当中断信号再次来临时程序为什么会中断而不再输出。而glibc并未设置SA_NOMASK,则不屏蔽相应的信号,内核采取的处理方式是自动阻塞这个信号,并将上述信号加入等待队列中,直到一次信号处理程序结束,才从等待队列中取出下一个要处理的信号(这句话其实并不准确,对于实时信号,则相同的信号再次到来时,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次)。

3. 早期的signal,会中断系统调用。APUE中所写的内容是“如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行”,此处的低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:

  1. 如果某些类型文件(如读管道、终端设备和网络设备)的数据不存在,则读操作可能会使调用者永远阻塞;
  2. 如果这些数据不能被相同的类型文件立即接受,则写操作可能会使调用者永远阻塞;
  3. pause函数和wait函数;
  4. 某些ioctl操作;
  5. 某些进程间通信函数。

原因在于没有SA_RESTART标志位,根据APUE中给出的说法“POSIX.1要求只有中断信号的SA_RESTART标志有效时,实现才重启系统调用”。

以上就是APUE中10.4到10.5节的内容,总结起来就是:glibc主要解决了kernel中信号机制的缺陷,主要体现在以下三点:glibc 中的信号安装并不是一次性的;glibc 屏蔽了正在处理的信号,对于相同信号的另一次出现,glibc 给出的做法阻塞该信号,待进程准备好后再进行下一次处理;glibc 对于中断的系统调用可重启。

针对以上第一点与第三点的内容在此给出一个我写的测试程序,源码如下:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signo)
{
    if(signo==SIGUSR1)
        printf("received SIGUSR1\n");
    else if(signo==SIGUSR2)
        printf("received SIGUSR2\n");
    else fprintf(stderr,"unknown no\n");
}

int main()
{
    char temp[10];
    if(signal(SIGUSR1,signal_handler)==SIG_ERR)
        fprintf(stderr,"can't catch SIGUSR1\n");
    if(signal(SIGUSR2,signal_handler)==SIG_ERR)
        fprintf(stderr,"can't catch SIGUSR2\n");
    read(STDIN_FILENO,temp,10);        

    return 0;
}

调用read函数从标准输入读入,则进程处于阻塞状态,发送SIGUSR1后中断系统调用,但glibc中signal可以重启系统调用,则收到信号后,read进程能够重启,则进程在此进入阻塞状态,此时再次发送SIGUSR2信号,测试中断处理程序是否是一次性的。运行结果如下:

窗口一运行结果:

./test_signal_1
received SIGUSR1
received SIGUSR2
 
此时程序尚未退出,等待输入。

窗口二运行结果,连续发送两个信号。

 kill -10 4966
 kill -12 4966

写到这里要给上面的内容做一点补充:在我分享的第一篇blog中给出这样一个例子,我稍加修改把循环次数增大了十倍,代码如下:

 #include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

#define MSG "OMG , I catch the signal SIGINT\n"
#define MSG_END "OK,finished process signal SIGINT\n"
int do_heavy_work()
{
    int i ;
    int k;
    srand(time(NULL));

    for(i = 0 ; i < 1000000000;i++)
    {
        k = rand()%1234589;
    }

}

void signal_handler(int signo)
{
    write(2,MSG,strlen(MSG));
    do_heavy_work();
    write(2,MSG_END,strlen(MSG_END));
}

int main()
{
    char input[1024] = {0};

#if defined TRADITIONAL_SIGNAL_API
    if(syscall(SYS_signal ,SIGINT,signal_handler) == -1)
#elif defined SYSTEMV_SIGNAL_API
    if(sysv_signal(SIGINT,signal_handler) == -1)
#else
    if(signal(SIGINT,signal_handler) == SIG_ERR)
#endif

    {
        fprintf(stderr,"signal failed\n");
        return -1;
    }

    printf("input a string:\n");
    if(fgets(input,sizeof(input),stdin)== NULL)
    {
        fprintf(stderr,"fgets failed(%s)\n",strerror(errno));
        return -2;
    }
    else
    {
        printf("you entered:%s",input);
    }

    return 0;

    
} 

运行结果如下:

input a string:
^COMG , I catch the signal SIGINT
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^COK,finished process signal SIGINT
OMG , I catch the signal SIGINT
OK,finished process signal SIGINT
a
you entered:a

一开始我并没有仔细的分析运行结果,直到我将循环次数增加了十倍,发现在一次中断信号处理程序运行过程中,发出再多次的SIGINT信号,中断处理程序都只在运行一次,其实这一点已经证明了对于非实时信号,信号是不排队的,即只对第一次来到的信号进行处理。

那么问题又来了为什么在一次信号处理程序运行过程中,还能对一个信号进行处理,还是在网上找到的答案:

http://blog.sina.com.cn/s/blog_7a9cae0101010hth.html

这其实与处理信号的机制有关,根据上述博文,信号是先在进程中注销,而后再调用信号处理函数,所以对于上面的例子,第一次发出SIGINT信号,信号经历产生->在进程中注册->在进程中销毁,才开始执行信号处理函数,而此时SIGINT已经处于未注册状态。因此在第一个信号处理函数运行过程中,再次产生SIGINT信号,又会经历上述流程,直到在进程中销毁这一步(为什么?后面的中断信号不响应就说明前面的没销毁)。那么此时问题又来了,为什么进程运行到此时又不销毁了?这回我真不知道答案害羞。我分享的那篇blog中金给出了结论:在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。其实这个问题的关键在于何时对是否存在信号需要进行处理进行判断。进行判断后才会对已经注册的信号进行销毁操作。

再来补充一个有关于第二点的例子,代码如下:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signo)
{
	if(signo==SIGUSR1){
		printf("received SIGUSR1 start pause()\n");
		pause();
		printf("received SIGUSR1 start pause()\n");
	}
	else if(signo==SIGUSR2){
		printf("received SIGUSR2 start pause()\n");
		pause();
		printf("received SIGUSR2 start pause()\n");
	}
	else fprintf(stderr,"unknown no\n");
}

int main()
{
	int n;
	char temp[10];
	if(signal(SIGUSR1,signal_handler)==SIG_ERR)
		fprintf(stderr,"can't catch SIGUSR1\n");
	if(signal(SIGUSR2,signal_handler)==SIG_ERR)
		fprintf(stderr,"can't catch SIGUSR2\n");
	while((n=read(STDIN_FILENO,temp,10))!=0){
		printf("%s",temp);
	}		

	return 0;
}

净是些疯狂的想法啊,在中断处理函数中调用pause()函数,运行结果如下:

窗口一:

received SIGUSR1 start pause()

窗口二:

kill -10 6210
kill -10 6210

发送再多,窗口一也不会有反映,为什么?

让我们从已知的结论出发:glibc 会屏蔽当前正在处理的信号,因此当一次信号处理程序调用pause()函数使进程进入睡眠状态,内核认为此时信号仍处于处理过程中,因此阻塞后来的信号;另一方面,根据APUE,pause函数只有执行了一个信号处理程序并从其返回时,pause才返回,也就是才输出。但现在没有办法使信号处理程序返回,则pause无法返回;pause不返回,则信号处理程序无法返回,注意此时已构成死锁。信号处理程序的无法返回造成了后来的信号无法被处理。以上程序即使换成实时信号也不行,原因可采用同样的思路分析。

最后说一点,以上分析都是我根据网上与书上的内容自己分析总结的,因此有不对的地方欢迎大家批评。对于有关于程序的内容,最权威的资料就是源码,不过很可惜,本人的水平还没有达到。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值