zoukankan      html  css  js  c++  java
  • 信号

    一、信号的产生:
    1.用户在终端按下某些键时,终端驱动程序会发送信号给前台进程
    例如:
    Ctrl-C产生SIGINT信号
    Ctrl-产生SIGQUIT信号
    Ctrl-Z产生SIGTSTP信号
     
    2.硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
    例如:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给程。
    再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
     
    3.由软件条件产生信号
    (1)SIGPIPE是一种由软件条件产生的信号
    (2)alarm函数 和SIGALRM信号。
             #include <unistd.h>
              unsigned int alarm(unsigned int seconds);
    调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发 SIGALRM信号, 该信号的默认处理动作是终止当前进程。这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
     

     
     
     
    4.调用相关的函数来向进程发送信号;
    例如:
    kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
    raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
    #include <signal.h>
    int kill(pid_t pid, int signo);  
    int raise(int signo);               
    这两个函数都是成功返回0,错误返回-1
     
    abort函数使当前进程接收到SIGABRT信号⽽而异常终止。
    #include <stdlib.h>
    void abort(void);
    就像exit函数一样,abort函数总是会成功的,所以没有返回值。
    (kill -l 可以察看系统定义的信号列表)
     
    二、当进程接收到信号后可选的处理动作有三种:
    1.忽略此信号。
    2.执行该信号的默认处理动作(大部分默认的动作是终止进程)。
    3.提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。(下文有详细讲解)
     
     
    三、阻塞信号
    1.信号在内核中的表示:
    实际执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞, 才执行递达的动作。
       信号在内核中的表⽰示⽰示意图:
    每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产⽣生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
    在上图的例子中,
    1. SIGHUP信号未阻塞也未产生过,当它递达时执⾏行默认处理动作。
    2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
    3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler
     
     
    如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
    POSIX.1允许系统递送该信号一次或多次。
    Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在⼀一个队列里。从上图来看,每个信号只有一 个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集, 这个类型可以表⽰示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该 信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这⾥里的“屏蔽”应该理解为阻塞而不是忽略。
     
     
     
    2. 信号集操作函数
    sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,这些bit依赖于系统实现,使用者只能调用以下函数来操作 sigset_t变量,而不应该对它的内部数据做任何解释,比如:利用位运算来获取未决信号集中的bit位信息,用printf直接打印sigset_t变量是没 有意义的。
     信号集操作函数:
     
    #include <signal.h>(前四个成功返回0,出错返回-1)
    (1)int sigemptyset(sigset_t *set);u2028 //初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集 不包含任何有效信号。
    (2)int sigfillset(sigset_t *set);u2028   //初始化set所指向的信号集,使其中所有信号的对 应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
    //注意:在使用sigset_t类型 的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
    (3)int sigaddset(sigset_t *set, int signo);u2028  //添加某种有效信号
    (4)int sigdelset(sigset_t *set, int signo);u2028  //删除某种有效信号
    (5)int sigismember(const sigset_t *set, int signo);  //判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0, 出错返回-1。
     
    3.屏蔽字(阻塞信号集)操作函数
     
    #include<signal.h>
    int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
     
    返回值:若成功则为0,若出错则为-1
     
    参数how指示如何更改(假设当前的信号屏蔽字为mask)
     
    如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
    如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
     
    4.读取当前进程中的未决信号集
     
    #include <signal.h>
    int sigpending(sigset_t *set);
     
    sigpending读取当前进程的未决信号集,通过set参数传出。
    调⽤用成功则返回0,出错则返回 -1。
     
     
    void catch ( int i)
    {
              printf( "catch %d SIG " ,i);
    }
     
    void printsigset(sigset_t* p)
    {
               int i;
               for (i=1;i<32;++i)
              {
                        if (sigismember(p,i))
                       {
                                 printf( "1" );
                       }
                        else
                       {
                                 printf( "0" );
                       }
              }
              printf( " " );
    }
    int main()
    {
               int i;
              sigset_t n,o,t;
              sigemptyset(&n);             //初始化信号集
              sigemptyset(&o);
               for (i=1;i<32;++i)                   //将31个常规信号设置为阻塞状态
              {
                       signal(i, catch );
                       sigaddset(&n,i);             //添加信号
                       sigprocmask(SIG_BLOCK,&n,NULL);   //设置阻塞字
              }
     
               int j=0;
               while (1)
              {
                       sigpending(&t);
                       printsigset(&t);
                       usleep(500000);
     
                        if (j < 32)
                       {
                                 kill(getpid(),j);      //给当前进程发送 i 号信号
                                 j++;
                                  if (j == 9 )
                                          j++;
                                  if (j == 19)
                                          j++;
                       }
                        else if (j++ == 40)
                                  break ;
              }
     
               for (i=1;i<32;++i)                   //将31个常规信号设置为阻塞状态
              {
                       sigaddset(&o,i);
                  sigprocmask(SIG_UNBLOCK,&o,NULL);   //设置阻塞字
                       printf( "释放%d 号信号 " ,i);
                       usleep(500000);
              }
    }
     
     
     
     
    四、捕捉信号
    1.内核实现信号的捕捉
    如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程⽐比较复杂,举例如下:
    1. 用户程序注册了SIGQUIT信号的处理函数sighandler。 
    2. 当前正在执行main函数,这时发生中断或异常切换到内核态。 
    3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
     4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系, 是两个独立的控制流程。
    5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 
    6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
     
    捕捉的过程(∞): 
     
     
     
    信号注册是使用的函数
    #include <signal.h>
    int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
    sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则 返回- 1。
    signo是指定信号的编号。
    若act指针非空,则根据act修改该信号的处理动作。
    若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结 构体:
     
    struct sigaction {
    void (*sa_handler)(int);
    //将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,
       赋值为常数SIG_DFL 表示执行系统默认动作,赋值为一个函数指针表⽰示⽤用⾃自定义函数捕捉信号
    void (*sa_sigaction)(int, siginfo_t *, void *);
    //sa_sigaction是实时信号的处理函数
    sigset_t sa_mask;
    //sa_mask字段说明这些需要额外屏蔽的信号,
    int sa_flags;
    //默认为0
    }
     
     
    void catch ( int sig)
    {
               //
    }
     
    int my_sleep(int time)
    {
               struct sigaction new ,old;
               new .sa_handler = catch ;    //初始化 sigaction 结构体
              sigemptyset(& new .sa_mask);
               new .sa_flags = 0;
     
              sigaction(SIGALRM,& new ,&old);  //注册函数
              alarm(time);
              pause();     //挂起进程函数
     
              sigaction(SIGALRM,&old,NULL);
              alarm(0);
     
    }
     
    1. 调用sigaction注册了SIGALRM信号的处理函数  sig_alrm。 
    2. 调用alarm(time)设定闹钟。         
    3. 调用pause等待,内核切换到别的进程运行。         
    4. time秒之后,闹钟超时,内核发SIGALRM给这个进程。   
    5. 从内核态返回这个进程的用户态之前处理未决信号,发现有SIGALRM信号,其处理函数是catch。 
    6. 切换到用户态执行catch函数,进入sig_alrm函数时SIGALRM信号被自动屏蔽,从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行系统调用 sigreturn再次进入内核,再返回用户态继续执行进程的主控制流程(main函数调用 的mysleep函数)。 
    7. pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号  以前的处理 动作。
     
    三个问题:
    1、信号处理函数catch什么都没干,为什么还要注册它作为SIGALRM的处理函数?不注册信号处理函数可以吗? (SIGALRM会终止进程?)
     pause函数使调用进程挂起直到有信号递达,如果不注册SIGALRM处理函数,当有信号SIGALRM信号产生时会执行默认动作,终止进程,达不到sleep(1)的效果了;
    2、为什么在mysleep函数返回前要恢复SIGALRM信号原来的sigaction? (想想不恢复 会怎样?)
     因为要模仿sleep函数,sleep函数在sleep(time)之后不会对SIGALRM 信号进行修改的,将SIGALRM 不恢复会使alarm()失效;
    3、mysleep函数的返回值表示什么含义?什么情况下返回非0值?
    mysleep的返回值是在信号SIGALRM信号传来时闹钟还剩余的秒数;当闹钟结束前有其他信号发送给该进程,并该进程对其进行了相关的处理时,alarm(0)取消闹钟会使返回值非零;
     
     
     
    五、可重入函数
    当捕捉到信号时,不论进程的主控制流程当前执行到哪⼉儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。信号处理函数是一个单独的控制流程,因为它 和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。引入了 信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局 变量、硬件资源等),就有可能出现冲突,如下面的例子所示。 如下是不可重入的函数:
     
     
     
     main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完 第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于 是切换 到sighandler函数,sighandler也调⽤用insert函数向同一个链表head中插入节 点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从 main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二 步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。    
    像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次 进入该函 数,这称为重入,insert函数访问一个全局链表,有可能因为重入⽽而造成错乱,像这样 的函数称为 不可重⼊入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入 (Reentrant) 函数。
    不可重入的函数的条件:
    调用了malloc或free,因为malloc也是用全局链表来管理堆的。
    调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
     
     
    六、sig_atomic_t类型与volatile限定符
    1.C标准定义了⼀一个类型sig_atomic_t,在不同平台的C语⾔言库中取不同的类型,例如在32 位机 上定义sig_atomic_t为int类型。
     2.编译器在进行词法语法分析时如果判断出某个变量使用频繁,并且不被更改时会对这个变量进行相关的优化,变为寄存器变量,优化之后省去了每次循环读内存的操作,效率⾮非常⾼高。
        编译器优化的缺点:编译器只是对代码到二进制的转换,对逻辑的正确性不能做出判断,并且编译器无法识别程序中存在多个执行流程。
    例如:下图中(编译器将 a 优化为寄存器变量),信号处理函数中对全局变量中的a的修改(内存中)不能在主执行流中(寄存器中)体现,这就造成了逻辑的错误!
     
     
     
    3.C语⾔言提供了volatile限定符,如果将 上述变量定义为volatile sig_atomic_t a=0;那么即使指定了优化选项,编译器也不会优化掉对变 量a内存单元的读写。 对于程序中存在多个执⾏行流程访问同一全局变量的情况,volatile限定符是必要的,
    此外,虽 然程 序只有单一的执⾏行流程,但是变量属于以下情况之一的,也需要volatile限定:
    1. 变量的内存单元中的数据不需要写操作就可以⾃己发生变化,每次读上来的值都可能不一 样
    2. 即使多次向变量的内存单元中写数据,只写不读,也并不是在无用功,而是有特殊意义的 什么样的内存单元会具有这样的特性呢?肯定不是普通的内存,而是映射到内存地址空间的硬 件寄存器,例如串⼜口的接收寄存器属于上述第一种情况,而发送寄存器属于上述第二种情况。
    sig_atomic_t类型的变量应该总是加上volatile限定符,因为要使用sig_atomic_t类 型的理由也正 是要加volatile限定符的理由。
     
     
    七、竞态条件与sigsuspend函数
    现在重新审视“mysleep”程序,
    设想这样的时序:
    1. 注册SIGALRM信号的处理函数。         
    2. 调⽤用alarm(nsecs)设定闹钟。         
    3. 内核调度优先级更⾼高的进程取代当前进程执⾏行,并且优先级更⾼高的进程有很多个,每个都要执行很长时间 
    4. nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态。   
    5. 优先级更高的进程执行完了,内核要调度回这个进程执行。SIGALRM信号递达,执行处理函 数sig_alrm之后再次进入内核。 
    6. 返回这个进程的主控制流程,alarm(nsecs)返回,调用pause()挂起等待。         
    7. 可是SIGALRM信号已经处理完了,还等待什么呢?      
           
    出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。 虽然alarm(nsecs)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用 alarm(nsecs)之 后的nsecs秒之内被调⽤用。由于异步事件在任何时候都有可能发生(这里 的异步事件指出现更高优 先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题 而导致错误,这叫做竞态条件 (Race Condition)。
     
    sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题
    #include <signal.h>
    int sigsuspend(const sigset_t *sigmask);
    和pause一样,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后 sigsuspend才返回,返回值为-1,errno设置为EINTR
    调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时 解除对某 个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
     
     
     
     
     
     
  • 相关阅读:
    如何动态加载控件以及插件编程思想(C#)
    Web Services的魅力
    Remoting和Webservice的区别
    用Asp.Net c#写的采集小例子
    .net remoting范例
    C# 实现屏幕键盘 (ScreenKeyboard)
    初识用.NET Remoting来开发分布式应用(转)
    把页面以流的形式输出 在另外一个页面显示
    webservice的应用
    【表单】鼠标经过时获得焦点,获得焦点时选中文本
  • 原文地址:https://www.cnblogs.com/shihaochangeworld/p/5722090.html
Copyright © 2011-2022 走看看