加载中…
正文 字体大小:

暑假--学习笔记4

(2014-08-01 09:24:09)
标签:

it

信号:

信号是软件中断,是一种处理异步事件的典型方法。


    1.不可靠的信号:

    信号可能会丢失,一个信号发生了,但进程却可能一直不知道这一点。并且早期的信号机制使进程对信号的控制能力很差。比如说,不具备阻塞信号的能力(换句话说,想实现当某个信号发生时,不要忽略该信号,在其发生时记住它,然后在进程做好了准备时再通知它的功能)。其次,早期的不可靠信号机制中,还存在当进程每次接到信号对其进行处理时,随即该信号动作重置为默认值。

早期的信号机制是这样的:


//首先声明一个信号处理函数:

int sig_int();

//接着,注册信号捕捉函数

signal(SIGINT, sig_int);

//定义信号处理函数

sig_int()

{

signal(SIGINT, sig_int);

}

 

它就存在上述存在的两个问题:一是不能阻塞信号,二是“在信号发生之后到信号处理程序调用signal函数之间有一个时间窗口。如果在此段时间中,可能会发生另一次中断信号。第二个信号会造成默认动作,而对中断信号的默认动作是终止该进程”


在说可靠的信号机制之前,先介绍了两个概念:一个是中断的系统调用,另一个是可重入函数。


 

2.中断的系统调用:

  什么是系统调用?“又称为系统呼叫,指运行在使用者空间的程序向操作系统内核请求需求要更高权限运行的服务。系统调用了用户程序与操作系统之间的接口。系统调用是运行在内核空间的。”

系统调用的一种分类方式,分为低速系统调用和其他系统调用。而低速系统调用可能会使进程永远阻塞的一类系统调用。包括:


“某些类型文件的数据不存在,则读操作可能会使调用者永远阻塞”

“如果这些数据不能被相同的类型文件立即接受,则写操作可能会使调用者永远阻塞”

“在某种条件发生之前打开某些类型文件,可能会发生阻塞”

“pause函数和wait函数”

“某些ioctl函数”

“某些进程间通信函数”


其中也有一个例外:与磁盘I/O有关的系统调用。虽然读、写一个磁盘文件可能暂时阻塞调用者,但是除非发生硬件错误,I/O操作总会很快返回,使调用者不再处于阻塞状态。”


一般信号的发生是难以估计的,它可能会发生在系统调用时。在早期的信号机制中,如果在系统调用时,发生了相应信号,那么必须立即去处理它,则此时的系统调用就会被中断。


一般的,当系统调用被中断后,会出错返回,并设置errno值,让调用者去进行出错判断与下一步操作。系统调用被中断后,返回给调用者去判断有时是麻烦的或者不必处理的,所以又出现了一种处理方法,叫做“自动重启动”。


什么是自动重启动?自动重启动就是当系统调用被中断时,并不出错返回,而是暂时阻塞系统调用返回,在信号处理程序完成后继续没完成的系统调用。因为有时候开启“自动重启”可能会带来问题,所以者成为了一个可以设置的选项。


3.可重入函数:

     什么是可重入函数?“可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数。可以在这个函数执行的任何时刻中断它,转OS调用去执行另外一段代码,而返回控制时不会出现什么错误。它意味着除了使用自己栈上的变量以外不依赖于任何环境。”


可重入函数一般不满足以下三点:

1.使用静态数据结构

2.调用malloc或free

3.是标准I/O函数。


     为什么这么说呢,比如在修改一个静态变量值的时候,发生了信号,在早期的信号机制中,此时必须去执行信号处理函数,所以此时的修改就被中断了。当从信号处理函数返回时,静态变量的值是否改变,是否还是一个可靠可用的值就不得而知了,如果其他函数也要使用该变量,那么可能会导致程序发生错误。所以使用静态数据结构的函数不是一个可重入函数。

 

        之后,就介绍了可靠的信号处理机制。


       首先,介绍了一些可靠的信号术语与语义。


      信号未决:在信号产生和向进程递送信号之间的时间间隔内,称信号是未决的。

      信号阻塞:让系统暂时保留信号留待以后发送(不是不发送,而是延迟发送),一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

      信号掩码(信号屏蔽字 signal mask):它规定了当前阻塞递送到该进程的信号集。每个进程有自己的信号掩码,创建子进程时将继承父进程的信号掩码。我们可以通过修改当前的信号掩码来改变信号的阻塞情况。

       信号集:一个能表示多个信号的数据结构。

 

         “如果进程产生了一个阻塞的信号,而且对该信号的动作时系统默认动作或捕捉该信号,则为该进程将此信号保持在为未决状态,直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。内核在递送一个原来被阻塞的信号给进程时,才决定对它的处理。于是进程在信号递送给它之前仍可改变对该信号的动作。”

 

 

          然后,介绍了建立在可靠信号的机制的一些函数。

          首先是,5个处理信号集的函数。



       暑假--学习笔记4
  

  

对于信号的初始化必须使用sigempty或者setfillset函数,不能直接采用赋值语句初始化。


在初始化了一个信号集后,我们就可以设置信号屏蔽字了。

用函数sigprocmask(仅为单线程定义)设置信号屏蔽字。

#include 

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

关于参数的说明:


 

暑假--学习笔记4

 sigset_t *restrict oset指进程原来的信号集。

         如果只需要改变信号阻塞情况而不需要不关系原本的值,则可以将oset设为NULL;

         如果希望什么也不做,只是想获得当前信号掩码的信息,则可以将set设为NULL;如果set不为NULL,则可以通过how指示如何修改当前信号屏蔽字。


         注意:不能阻塞SIGKILL和SIGSTOP信号,如果设置了,sigprocmask也会忽略。

 

        设置了信号屏蔽字之后,我们可以用sigpending来读取当前的未决信号。

        #include 

        int  sigpending(sigset_t *set);

        若成功返回0,若出错,返回-1。当前的未决信号集是存储在set中的。

 

        在设置了信号屏蔽字和知道怎样读取未决信号后,我们可以采用sigactin来检查或修改(或检查并修改)与指定信号相关联的处理动作。

         #include 

         int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

         这个函数的参数看起来有些复杂,但是返回值非常简单呢。成功就返回0,出错就返回-1.


         signo:指定的信号。后面两个参数都是struct sigaction结构体。


     struct sigaction{

void   (*sa_handler)(int);  //指向返回值为void,参数为int的信号处理函数或者SIG_IGN或者是SIG_DFL

sigset_t  sa_mask;  //用来指定在信号处理函数执行期间需要屏蔽的信号

int   sa_flags;  //设置一些选项标志。比如是否开启自动重启是否提供附加信息

void  (*sa_sigaction)(int, siginfo_t *, void *); //可以对信号处理程序提供更多附加信号。指向一个返回为void,带有三个参数的信号处理信号。

};


      第一字段和第二个字段一般是一次只能使用一个,因为可能在有的系统中,si_handler和sa_sigaction实现可能使用同一个存储区


    通常是使用第一个字段调用信号处理程序:

    void handler(int signo);

    如果sa_flags设置了SA_SIGINFO标志,那么就需要按

    void handler(int signo, siginfo_t *info, void *context);来调用信号处理程序。

    act:设置新的信号处理方式。

    oact:原来的信号处理方式。


    siginfo结构体:


    struct siginfo{

int  si_signo;

int  si_errno;

int  si_code;  //额外的信息

int  si_pid;

int  si_uid;  //实际用户ID

int  si_addr;     //包含造成故障的根源地,该地址可能不准确

int si_status;   //exit值或信号编号

union sigval  si_value;  //应用程序详细的一些值

      

};

  

union sigval{

int  sival_int;    //应用程序在递送信息时,传递整型数

void *sival_ptr;                 //应用程序在递送信息时,传递一个指针值

};

 

         

        在建立可靠的信号机制,还有需要sigsuspend函数。为什么这么说呢,因为在这样的一个情况下,一开始想要阻塞某个信号,之后希望对该信号解除阻塞,然后pause以等待以前被阻塞的信号发生。在信号解除阻塞和pause之间,如果发生了以前被阻塞的信号,则该信号很容易发生丢失。因为,很有可能之后不会再发生这个信号。因为该信号刚好被解除阻塞,并且pause函数还没有其作用并且之后也可能不会再发生该信号,所以pause很有可能会一直阻塞下去。为了避免这种情况的发生,所以我们就需要一个原子操作:先恢复信号屏蔽字,然后使进程休眠。sigsuspend函数就可以提供这种功能。


      #include 

     int sigsuspend(const sigset_t *sigmask)

     信号可以实现保护代码临界区,使其不被特定信号中断的。也可以等待一个信号处理程序设置一个全局变量,也可以实现父、子进程之间的同步。


       sigaction函数的一个小小例子:


     #include

    #include

    #include

    #include


    //信号处理函数

   static void sig_usr(int signum)

  {

if (signum == SIGUSR1){

printf("SIGUSR1 receive\n");

}else if (signum == SIGUSR2){

printf("SIGUSR2 receive\n");

}else {

printf("signal %d receive\n", signum);

}

 }


  int main(void)

  {

char buf[512];

int n;

struct sigaction sa_usr;


//sa_flag设置为开启自动重启功能

sa_usr.sa_flags = SA_RESTART;

//将sa_mask信号集清空

sigemptyset(&sa_usr.sa_mask);

//注册信号处理函数sig_usr___

sa_usr.sa_handler = sig_usr;


//调用sigaction函数

sigaction(SIGUSR1, &sa_usr, NULL);

sigaction(SIGUSR2, &sa_usr, NULL);

 

printf("My pid = %d\n", getpid());


while (1){

if ((n = read(STDOUT_FILENO, buf, 511)) == -1){

if (errno == EINTR){

printf("read is interrupted bu signal\n");

}

}else {

buf[n] = '\0';

printf("%d bytes read: %s\n", n, buf);

}

}

return 0;

 }


   运行结果:

   

       暑假--学习笔记4


    如果将sa_flag不设置为自动重启 ,那么运行结果为:

   

       暑假--学习笔记4


         

     用信号保护临界区代码的例子:

     #include

     #include

     #include

     #include

     #include "../lib/error.c"


     static void sig_int(int);


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

   {

sigset_t newmask, oldmask, waitmask;


//用来打印此时的进程的屏蔽的信号

pr_mask("program start: ");


//用signal函数安装sig_int函数.现在一般不用signal函数了。

//严谨做法就是在开头将安装信号处理函数,防止信号处理函数

//还没由安装时,就发生信号,导致信号处理发生错误,不合预想。

if (signal(SIGINT, sig_int) == SIG_ERR){

err_sys("signal(SIGINT) error");

}


//初始化waitmask信号集.清除waitmask信号集里的所有信号

sigemptyset(&waitmask);

//将SUGUSR1信号加入到waitmask信号集中

sigaddset(&waitmask, SIGUSR1);



//初始化newmask信号集,清除newmask信号集里的所有信号

sigemptyset(&newmask);

//将SIGINT信号加入到newmask信号集中

sigaddset(&newmask, SIGINT);


//将当前进程的信号屏蔽字设置为newmask和当前信号屏蔽字的并集

//并且当前信号屏蔽字存储在oldmask中

//此时进程的信号屏蔽字中包含SIGINT信号

if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0){

err_sys("SIG_BLOCK error");

}


 


//打印此时的信号屏蔽字

pr_mask("in critical region: ");


//设置新的信号屏蔽字阻塞当前的进程,将SIGUSR1加入到信号屏蔽字中

//收到SIGUSR1信号,阻塞,程序继续挂起;

//收到SIGINT信号,调用信号处理函数sig_int

//待信号处理函数返回,sigsuspend返回,并且在sigsuspend函数返回时,将信号

//屏蔽字设置为调用它之前的值

//sigsuspend将捕捉信号和信号处理函数集成到一起了

if (sigsuspend(&waitmask) != -1){

err_sys("sigsuspend error");

}


//打印此时进程的信号屏蔽字

pr_mask("after return from sigsuspend: ");


//恢复进程一开始的信号屏蔽字

if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0){

err_sys("SIG_SETMASK error");

}


//打印此时的信号屏蔽字

pr_mask("program exit: ");


exit(0);

}


   static void sig_int(int signo)

 {

pr_mask("\n in sig_int: ");

 }


    void pr_mask(const char *str)

   {

sigset_t sigset;

int errno_save;


errno_save = errno;

if (sigprocmask(0, NULL, &sigset) < 0){

err_ret("sigpromask error");

}else {

printf("%s", str);

if (sigismember(&sigset, SIGINT)){

printf(" SIGINT");

}

if (sigismember(&sigset, SIGQUIT)){

printf(" SIGQUIT");

}

if (sigismember(&sigset, SIGUSR1)){

printf(" SIGUSR1");

}

if (sigismember(&sigset, SIGALRM)){

printf(" SIGALRM");

}

if (sigismember(&sigset, SIGUSR1)){

printf(" SIGUSR1");

}



printf("\n");


}


errno = errno_save;

 }


         运行结果:

       暑假--学习笔记4


      一般在信号处理函数中,会先将errno值复制,在返回到调用者之前,再将errno值复原,防止局部的信号处理函数使errno发生变化。有时此变化外部时不需要知道,内部解决即可的。如果不复原errno值的话,可能会使外部的errno值产生变化,使程序出现一些错误判断。        


涉及信号时是要精细周到的考虑。

    
     绿色斜体字均为直接引用APUE或者百科资料。

0

阅读 评论 收藏 转载 喜欢 打印举报
已投稿到:
  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 不良信息反馈 电话:4006900000 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有