进程间的信号 --------详解(通过信号去控制进程的运行状态)

(1)概述
  1.信号是一种软件中断,用来处理异步事件
  2.信号的本质是一种进程间的通信,一个进程向另一个进程发送信号
  3.执行kill -l可查看系统所有的信号
  4.作用:ctl+c时用来做一些收尾工作:
     1.删除管道.删除共享内存.删除信号量.删除消息队列..
     2.进程间通信

(2)信号的生命周期
  进程之间约定好:如果发生了某件事情T ( trigger ),就向目标进程( destination   process )
  发送某特定信号 X ,而目标进程看到 X ,就意识到 T 事件发生了,目标进程就会执行相应的动作 A ( action )
  1.Linux 内核收到了产生的信号,然后就在目标进程的进程描述符里记录了一笔:收到信号一枚。
       2.Linux 内核会在适当的时机,将信号递送( deliver )给进程。
  3.在内核收到信号,但是还没有递送给目标进程的这一段时间里,信号处于挂起状态,也称为未决信号。
  4.内核将信号递送给进程,进程就会暂停当前的控制流,转而去执行信号处理函数。
  5.实际情况还应该考虑的问题
    1.目标进程正在执行关键代码,不能被信号中断,需要阻塞某些信号
    2.如何处理重复的信号,排队还是丢弃?
    3.已有多个不同的信号被挂起,应该优先递送哪个信号?
    4.对于多线程的进程,如果向该进程发送信号,应该由哪个线程来负责响应?


(3)信号的产生
  [1] 硬件异常
    1.硬件检测到了错误并通知内核,由内核发送相应的信号给相关进程
    2.常见硬件异常的信号
      SIGBUS: 总线异常
      SIGFPE:    算数错误
      SIGILL: 非法及其指令
      SIGSEGV: 段错误
        进程访问未初始化的指针或 NULL 指针指向的地址
        进程在用户态访问内核部分的地址    
        进程修改只读的内存地址

  [2] 终端相关的信号
    ·Ctrl+C :产生 SIGINT 信号。
    ·Ctrl+\ :产生 SIGQUIT 信号。
    ·Ctrl+Z :产生 SIGTSTP 信号。
    键入这些信号生成字符,相当于向前台进程组发送了对应的信号。

  [3] 软件事件相关的信号
    · 子进程退出,内核可能会向父进程发送 SIGCHLD 信号。
    · 父进程退出,内核可能会给子进程发送信号。
    · 定时器到期,给进程发送信号。

(4)信号默认处理函数
  [1]信号的默认操作
    · 显式地忽略信号:ignore
    · 终止进程:terminate
    · 生成核心转储文件并终止进程(用于调试):core
    · 停止进程(暂停进程):stop
    · 恢复进程的执行: continue

(5)信号的分类
  [1]不可靠信号    
        1.信号值在 [1,31] 之间的所有信号,都被称为不可靠信号
        2.不可靠信号是从传统的 Unix 继承而来的
        3.不可靠信号如果收到某不可靠信号,内核发现已经存在该信号处于未决状态,
          就会简单地丢弃该信号。因此发送不可靠信号,信号可能会丢失,

  [2]可靠信号
    1.在 [SIGRTMIN,SIGRTMAX] 之间的信号,被称为可靠信号
    2.内核内部有队列来维护,如果多次收到可靠信号,内核会将信号挂到相应的队列中,因此不会丢失。


(6)传统信号(System V风格)
  [1]在相同的 Linux 平台上,由于 glibc 版本的差异,提供的 signal 函数的语义也有差异。
        在早期的 libc4 和 libc5 中, signal 函数的语义是 Syetem V 风格的。因此,从可移植的角度来看,不应该使用 signal 函数。

  [2]信号执行时屏蔽自身的特性
    1.对于传统的 System V 信号机制,在信号处理期间,不会屏蔽对应的信号,而这就会引起信号处理函
     数的重入。
    2.System V 风格的信号,在其信号处理期间没有屏蔽任何信号,换句话说,执行信号处理函数期间,
     处理流程可以被任意信号中断,包括正在处理的信号。

  [3]信号中断系统调用的重启特性
    1.系统调用在执行期间,很可能会收到信号,此时进程可能不得不从系统调用中返回,去执行信号处理函数
    2.对于执行时间比较久的系统调用(如 wait 、 read 等)被信号中断的可能性会大大增加。
    3.系统调用被中断后,一般会返回失败,并置错误码为 EINTR
    4.如果程序员希望处理完信号之后,被中断的系统调用能够重启,则需要通过判断 errno 的值来解决,
     即如果发现错误码是 EINTR ,就重新调用系统调用。

(7)signal---kill.raise.alarm(信号安装和发送)
  [1]signal函数
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    返回值: sighandler_t类型的函数指针
    形参:   int, sighandler_t类型函数指针
      void (*signal(int signum, sighandler_t handler))(int);
    使用例子1:
        int sig_int(int sig) {printf("sig_int");}
        signal(SIGINT, sig_int);
    使用例子2:
          signal(SIGINT, SIG_IGN);    // 忽略SIGINT信号
         signal(SIGINT, SIG_DFL);    // 对于SIGINT信号,使用默认处理函数
    ==>写法二: void (*signal(int signum, void (*handler)(int)))(int);  

  [2]kill函数(和signal搭配)
      int kill(pid_t pid, int sig);
       ·pid > 0 :发送信号给进程 ID 等于 pid 的进程。
       ·pid = 0 :发送信号给调用进程所在的同一个进程组的每一个进程。
       ·pid = -1 :有权限向调用进程发送信号的所有进程发出信号, init 进程和进程自身除外。
       ·pid < -1 :向进程组 -pid 发送信号。
       当函数成功时,返回 0 ,失败时,返回 -1 ,并置 errno
       1. kill 函数不仅可以向特定进程发送信号,也可以向特进程组发送信号
       2. 所有信号值都是>0的。 若第二个参数 signo的值为0,这种情况下,来检测目标进程或进程组是否存在,
        如果 kill 函数返回 -1 且 errno 为 ESRCH ,则可以断定我们关注的进程或进程组并不存在

    [3]raise函数
        int raise(int sig);
       向进程自身发送信号的接口
      int raise(int sig);
      1.单线程的程序而言
        相当于:kill(getpid(),sig)
      2.对于多线程的程序而言
       相当于:pthread_kill(pthread_self(),sig) // 给当前线程发信号

    [4]alarm函数(闹钟)
        unsigned int alarm(unsigned int seconds);
        描述: 每隔5s给当前进程发送一个SIGALRM信号。
            alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。
            可以设置忽略或者不捕获此信号,默认动作是终止调用该alarm函数的进程。
        成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
        出错:-1


(8)sigaction---sigqueue(信号的发送和安装)
    [1]sigqueue(发送信号)
        int sigqueue(pid_t pid, int sig, const union sigval value);
        参数: pid:     要发送信号的进程ID
              sig:      要发送的信号
              value: 发送的伴随数据,该参数的数据类型是联合体
                union sigval {
                    int sival_int;
                    void *sival_ptr;    // 几乎不用(每个进程都有独立的地址空间)
                };
            //考虑到不同的进程有各自独立的地址空间,传递指针到另一个进程几乎没有任何意义。因此 sigqueue 函数很少传递指针( sival_ptr ),大多是传递整型( sival_int )。    
        1.传统的信号多用 signal/kill 这两个函数搭配
        2.signal函数的表达力有限,控制不够精准;所以引入了sigqueue函数来完成实时信号的发送
        3.sigqueue函数也可以发送空信号(信号0)来检查进程是否存在。
        4.和 kill 函数不同的地方在于,它不能通过将pid指定为负值而向整个进程组发送信号。


    [2]    sigaction(安装信号)
        int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
        1.参数: signum: 信号编号
            1.struct sigaction {    // 第二个参数
                union {
                    void (*sa_handler)(int);//sa_flags里没有设置SA_SIGINFO标记的信号处理函数
                    //sa_flags里设置了SA_SIGINFO标志, 的信号处理函数
                    void handle(int signum, siginfo_t *info, void *ucontext);
                    // 收到的额外数据保存在info->si_value中的si_int和si_ptr中
                }
                sigset_t sa_mask;            // 阻塞信号集
                int sa_flags;                // 标志
                void (*sa_restorer)(void);    // 恢复处理程序
            };        
            2.siginfo_t {     // handle信号处理函数的第二个参数    
                int si_signo; // 信号的值
                int si_code;  // 信号来源:SI_USER.SI_TKILL.SI_QUEUE.. 
                pid_t si_pid;    // 信号发送进程的进程 ID 。
                uid_t si_uid;   //信号发送进程的真实用户 ID 。
                union sigval si_value; //sigqueue 函数发送信号时所带的伴随数据。
                ...
            }
            3.ucontext是 void* 类型的,其实它是一个 ucontext_t 类型的变量。
                这个结构体提供了进程上下文的信息,用于描述进程执行信号处理函数之前进程所处的状态。
                通常情况下信号处理函数很少会用到这个变量
            4. sa_flags的含义
                1.SA_NOCLDSTOP
                    一旦父进程为SIGCHLD信号设置了这个标志位,那么子进程停止和子进程恢复这两件事情,就不会向父进程发送SIGCHLD信号了
                        但是子进程切换为SIGCONT时还是会给父进程发送SIGCHLD信号。
                2.SA_NOCLDWAIT
                    如果父进程为SIGCHLD设置了SA_NOCLDWAIT 标志位,那么子进程退出时,就不会进入僵尸状态,而是直接自行了断。
                    对于Linux而言,子进程转换切换为SIGSTOP.SIGCONT.SIGKILL时都会给父进程发送SIGCHLD信号。这点和上面的 SA_NOCLDSTOP 略有不同。
                3.SA_ONESHOT 和 SA_RESETHAND
                    这两个标志位的本质是一样的,表示信号处理函数是一次性的,信号递送出去之后,信号处理函数便恢复成默认值 SIG_DFL 。
                4.SA_NODEFER 和 SA_NOMASK
                    这两个标志位的作用是一样的,在信号处理函数执行期间,不阻塞当前信号。
                5.SA_RESTART
                    这个标志位表示,如果系统调用被信号中断,则不返回错误,而是自动重启系统调用
                6.SA_SIGINFO
                    没有设置SA_SIGINFO:
                        跟signal使用方法相同, 使用一个参数的信号处理函数
                        void (*sa_handler)(int);
                    设置了SA_SIGINFO:
                        1.这个标志位表示信号发送者会提供伴随数据。这时使用带3个参数的信号处理函数
                            void handle(int, siginfo_t *info, void *ucontext);    
                        2.能获取到发送进程的PID、UID.信号来源.及发送的额外信息...
        2.注意
            1.对SIGKILL 和 SIGSTOP,不可以为它们安装信号处理函数,也不能屏蔽掉这些信号。
                若通过 sigaction 强行给 SIGKILL 或 SIGSTOP 注册信号处理函数,则会返回-1,并置errno为EINVAL。 

  (9)等待信号
    [1]pause函数
      int pause(void);
      作用:使调用进程(线程)进入休眠状态(就是挂起);直到接收到信号且信号函数成功返回
        pause函数才会返回
      返回值:始终返回-1

(10)线程的阻塞信号集      
    [1]概述
        1.每个线程都拥有独立的阻塞信号掩码。
        2.开会时关闭手机是一种比较极端的例子。更合理的做法是暂时屏蔽部分人的电话。对于某些重要的电话,比如儿子老师的电话、父母的电
            话或老板的电话,是不希望被屏蔽的。信号也是如此。进程在执行某些操作的时候,可能只需要屏蔽一部分信号,而不是所有信号。
        3.信号集: 数据类型为 sigset_t,sigset_t 的类型是位掩码,每一个比特代表一个信号。    
        4.SIGKILL 信号和 SIGSTOP 信号不能被阻塞。(设置信号集时会被内核剔除)(避免出现神仙进程)
        5.对于多线程的进程而言,每一个线程都有自己的阻塞信号集。

    [2]常用API
        int sigemptyset(sigset_t *set);    // 初始化信号集set中的信号为空
        int sigfillset(sigset_t *set);  // 将所有信号添加进信号集set
        int sigaddset(sigset_t *set, int signum); // 在信号集中添加signum信号
        int sigdelset(sigset_t *set, int signum); // 在信号集中删除signum信号
        int sigismember(const sigset_t *set, int signum); // 判断signum是否存在于set信号集中
        int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 设置信号集
            oldset: 如果oldset为非NULL,则信号掩码的先前值存储在oldset中,故一般设置为NULL。
            how的选项:
                SIG_BLOCK:   在当前阻塞信号集中增加set信号集中的信号
                SIG_UNBLOCK:在当前阻塞信号集中删除set信号集中的信号
                SIG_SETMASK:阻塞信号集被设置为set信号集。

    [3]pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
        1.为了更显式地设置线程的阻塞信号掩码,线程库提供了 pthread_sigmask 函数来设置线程的阻塞信号掩码。
        2.事实上 pthread_sigmask 函数和 sigprocmask 函数的行为是一样的。
            pthread_sigmask函数将调用sigprocmask函数(内核源码)

    [4]注意
        1.如果阻塞了某个信号A, 然后调用pause。在程序的执行过程中如果一直给进程发送
            信号A,pause函数将不会返回, 因为发送的信号都被阻塞。
        2.对于信号集中阻塞的不可靠信号a, 在阻塞过程中, 发送多个信号a时,之前挂起的信号a会被抛弃;
           解除阻塞后, 最终发送到目标进程的信号a只有一个。
        3.对于信号集中阻塞的可靠信号b,在阻塞过程中, 发送多个信号a时,会创建一个队列来管理阻塞的信号;
           解除阻塞后, 最终发送到目标进程的信号b = 信号b的发送次数。
        4.SIGKILL 信号和 SIGSTOP 信号不能被阻塞。(设置信号集时会被内核剔除)

(11)SIGCHID信号
    1.父进程可以监测子进程的以下三种事件; 每次状态改变,子进程会发SIGCHID给父进程
        · 子进程终止(即子进程死亡)
        · 子进程停止(即子进程暂停)
        · 子进程恢复(即子进程从暂停中恢复执行)

    2.若使用sigaction---sigqueue(信号的发送和安装)
        1.sigaction使用了宏SA_NOCLDSTOP:
            一旦父进程为SIGCHLD信号设置了这个标志位,那么子进程停止和子进程恢复这两件事情,就不会向父进程发送SIGCHLD信号了。
                但是子进程切换为SIGCONT时还是会给父进程发送SIGCHLD信号。
        2.sigaction使用了宏SA_NOCLDWAIT:
            如果父进程为SIGCHLD设置了SA_NOCLDWAIT 标志位,那么子进程退出时,就不会进入僵尸状态,而是直接自行了断。对于Linux而言,子进程转换切换为SIGSTOP.SIGCONT.SIGKILL时都会给父进程发送SIGCHLD信号。这点和上面的 SA_NOCLDSTOP 略有不同。

(12)信号的练习
    信号示例: (通过信号模拟 <司机--售票员>)
    1.平常司机在车上休息,售票员在观察上车人数
    2.售票员发现车上人满了就提醒司机发车
    3.中途停两个站: 9km. 15km处, 这时售票员要提醒司机
    6.总里程20公里
    7.到终点站后司机提醒售票员让所有乘客下车
    8.售票员退出后, 司机才能退出


(13)信号与线程的关系
    POSIX 标准: 

   0.给线程发送信号用pthread_kill()
    1.信号处理函数必须在多线程进程的所有线程之间共享,但是每个线程要有自己的挂起信号集合和阻塞信号掩码。
    2.POSIX 函数 kill/sigqueue 必须面向进程,而不是进程下的某个特定的线程。
    3.每个发给多线程应用的信号仅递送给一个线程,这个线程是由内核从不会阻塞该信号的线程中随意选出来的。
    4. 如果发送一个致命信号到多线程,那么内核将杀死该应用的所有线程,而不仅仅是接收信号的那个线程。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值