【进程间通信】信号

来源:互联网 发布:贴源数据是什么意思 编辑:程序博客网 时间:2024/04/19 20:41

信号(signal)机制是在软件层次上对中断机制的一种模拟;从概念上说,一个进程收到一个信号与一个处理器接收到一个中断请求是一样的;而另一个进程可以向另一个(或另一组)进程发送信号;也跟在多处理器中一个处理器可以向其他处理器发送中断请求一样,中断源可以是本处理器本身,也可能是各种外设设备;信号,也不一定来自其他进程,可以来自不同的来源,还可以来自本进程的执行;处理器在执行一段程序并不需要停下来等待中断的发生,也不知道何时发生;信号也是一样,一个进程并不需要通过一个什么操作来等待信号的到达,也不知道什么时候就有信号到达;


(1)事实上,在所有的进程间通信机制中只有信号是异步的;中断和信号二者之间的这种相似和类比不仅仅是概念上的,也体现在它们的实现上;就像中断机制中有一个中断向量表一样,在每一个进程的task_struct结构中都有一个指针sig,指向一个signal_struct结构,即为信号向量表;在中断机制中,对每个中断请求都可以加以屏蔽而不让处理器对之响应,在信号机制中也有类似的手段;但是信号总归是软件上来实现的,中断是软硬件结合实现的;

(2)早期的信号机制比较简单和原始,是不可靠的;Linux内核的信号机制符合POSIX.4的规定;每一个进程的task_struct中有一个指针sig,指向一个signal_struct(action[]是一个信号向量表,数组中每一个元素就相当于一个信号向量,确定了当进程接收到一个具体的信号时应该采取的行动,不过它还有SIG_DFL和SIG_IGN分别是默认处理和忽略处理,但是信号向量表中处理程序一般是在用户空间的);对信号的检测和响应总是发生在系统空间,当前进程处于系统调用,中断或异常而进入系统空间以后,从系统空间返回用户空间前夕;也可以是当前进程在内核中进入睡眠以后刚被唤醒的时候,由于信号的存在而提前返回到用户空间;当有信号要响应时,处理器的执行路线如下:


(3)早期的信号向量表中,每一个向量都是函数指针,如今是一个k_sigaction,其中_sa_handler和_sa_sigaction仍是函数指针,sa_mask是一个位图,其中每一位对应着一种信号;如果位图中的某一位为0,变瘦执行当前信号的处理程序的相应信号暂时被屏蔽了;不管位图中相应位是否为1,当前信号总是自动屏蔽的,使得对同一中信号的处理不会嵌套发生,除非sa_mask中的SA_NODEFER或SA_NOMASK标志位1;显然,这正是借鉴中断服务程序关闭中断防止嵌套的经验,这是将不可靠信号改成可靠信号的关键一步;当我么按下ctrl+C时,会使当前运行的程序流产,内核会向相应的进程发一个SIGINT,而这个信号默认处理就是do_exit(),可为该信号绑定处理程序,早期的实现机制,会让该设置流产,这里的屏蔽并不是将信号丢弃,已经到达的信号仍旧存在;sa_mask值sigset类型,模拟中断控制器中的中断请求寄存器,以及中断屏蔽寄存器;而sa_flags,有序到标识,其中SA_SIGINFO表示信号处理程序有三个参数,其中一个指向siginfo_t数据结构的指针(通信双方可传递的信息,还可外加一个void *),否则只有一个即SIGINT;

(4)以前的进程task_struct中有两个位图sigset_t,即signal(现在在sigpending中pending)和blocked,分别用来模拟中断控制器的中断请求寄存器以及中断屏蔽寄存器;每当有中断请求到来时,就将对应的中断控制转台寄存器的相应位置1,等到中断处理程序读出这个寄存器又被清0,但是会因为连续的请求会被合并,中断检测某个中断通道中中断请求就会轮询连接通道中所有的中断源,因此合并只是形式上的,不是实质上的;但是信号机制就不同了,无法轮询所有可能的信号来源,解决办法就是为信号准备一个队列,每产生一个信号就把它挂入这个队列中,在sigpending中(signal和队列);task_struct中有一个整型的sigpending表示这个进程是否有信号在等待处理,sas_ss_sp记录当前进程在用户空间执行信号处理程序时的堆栈位置,另一个是sas_ss_size,那就是堆栈大小;

(5)在sigaction()中,signum是信号编号,而newact表示新的,待设置的向量sigaction,oldaction用来返回老向量的数据结构;在signal()中只有信号编号和对应的处理程序;

(6)首先是在sys_signal()中,是按传统来设置的,在k_signaction(new_sa设置成SA_ONESHOT一次性,使用完了就设置成SIG_DFL和SA_NOMSK不使用信号屏蔽);在sys_rt_sigaction,首先使用new_sa从用户空间复制act;而在sys_sigaction中,由于成分次序不同,只好将各个成分依次复制过来了;这三个函数最终都熬调用do_sigacion;

(7)在do_sigaction()中,系统对信号SIGKILL和SIGSTOP的响应是不允许改变的,所以要放在开头时加以检查;同时这两个信号也是不允许屏蔽的,所以要在k->sa.sa_mask将这两个信号对应的屏蔽位清除;同时要使用oact保存原始的k_sigaction,并设置新的k_sigactions;当新设置的向量为SIG_IGN或SIG_DFL,但是涉及的信号SIGCONT,SIGCHLD和SIGWINCH之一时,如果已经有一个或这样的信号等待处理,则posix要求将这些已达到的信号丢弃,所以通过rm_sig_from_queue()将已经到达的信号丢弃;然后还要将等到处理的总标志sigpending重新算一下;

(8)跟信号向量有关的系统调用还有一些:sigpromask()改变本进程task_struct结构中信号屏蔽位图blocked;这与具体的向量的k_sigaction的屏蔽位图sa_mask是不一样的,它只在执行相应的处理程序才起作用,而blocked是一直起作用的,屏蔽是指暂时阻止已经到达的信号做出响应,一旦屏蔽取消,这些已经达到的信号还是可会得到处理的;sigpending()检查哪些信号已经达到但是还未处理;sigsuspend()暂时改变本进程的信号屏蔽位图,并使进程进入睡眠,等待任何一个未被屏蔽的信号达到;

(9)向一个进程发送信号,同样地,老版本是通过kill()来完成的,当pid为0时,发送给当前进程组中所有的进程,当pid为-1时,发送给系统中的所有进程;而sigqueue()发送的信号sig本身外还有附加的信息val,此外,它只能将信号发送给指定的进程;raise()是发送给自己信号的函数;

(9.1)在sys_kill()中,首先准备一个siginfo结构,保存sig信号,设置错误码为0,以及code,本进程号,以及uid号,然后调用kill_something_info(),首先根据pid来判断确定是要将信号发送给一个特定进程,还是进程组,还是所有进程;

(9.2)在sys_rt_sigqueueinfo()只允许将信号发送给一个特定的进程,并且随同信号发送的siginfo结构也是用户进程自己设置的;

(10)在kill_proc_info()是将统一信号发送给特定进程;而kill_pg_info最终也是逐个地找到同一进程组中所有进程的task_struct,并最终调用send_sig_info()将信号发送给它们;

(11)在send_sig_info()中,首先使用bad_signal对参数进行检查,在bad_signal,使用si_code来区分7种不同的信号源,而在sys_rt_sigqueueinfo()中,随同siginfo结构来自进程的用户空间,且是一个负数;信号一般只发送给属于同一个session以及同一个用户的进程,因此是不具有特权用户发送信号的权限的,使用capable(CAP_KILL)来检查它们是否有特权权限;使用handle_stop_signal来屏蔽一些特定信号的后续信号,使用ignored_signal来优化,如果信号向量表中所投递信号的响应是SIG_IGN,并且不在跟踪模式中,也没有加以屏蔽,那就不用投递了;否则,就绪投递,对于老编制的信号,所谓投递也就是将目标进程的接受信号位图signal的相应标志位设成1,而无需将信号挂入队列中,但是这样将在短期中接受的多个同种信号合并成一个,因此对于老编制的信号,也要挂入队列,不过只会挂入一次;如果到达的信号是新编制,就将接收位图中相对标志位设为0,通过deliver_signal()投递信号;

(11.1)在deliver_signal中,主体是send_signal,首先将siginfo信息复制到sigqueue中,并将这个sigqueue挂入队列中,siginfo若为整数0,1(如,页面异常无法恢复时,强制使用force_sig发送信号SIGBUS),说明发生的信号来自系统空间,自身补内容;还要用sigaddset将接收位图中相应标志位设成0;在deliver_signal中,还要检查目标进程是否屏蔽了该信号,若该进程处于睡眠状态,且没有屏蔽该信号,则调度该进程;到这边,信号投递已经完成了;

(12)在中断机制中,处理器的硬件在每条指令结束时都要检测中断是否有发生;而信号是在系统调用,中断处理,异常处理返回用户空间前夕,还有就是进程从睡眠中唤醒(一定是在系统调用中);用户空间的进程,即使信号到了也不会立即引起对信号的反应,只有该进程进入内核,然后返回用户空间时才反应;对于信号来自异常处理程序时,由于进程已经在内核中进行,在返回用户空间时就会反应了;

(13)对信号做出反应是通过do_signal()完成的;首先如果regs->xcs的最低两位不等于3的话,说明本次中断或异常发生于系统空间,处理器并不是出于用户空间的前夕,并不需要对信号做出反应;否则继续;在每一轮循环中,从进程的信号队列使用dequeue_signal取出一个未加屏蔽的信号加以处理,知道信号队列中不存在信号为止,或信号向量是SIG_DFL(且默认反应是让接受到信号的进程exit)或执行一个由用户设置的信号处理程序;当收到SIGCHLD信号的进程要调用sys_wait4()来检查其所有的子进程,只要找到一个已结束的子进程就为其释放最后的资源;其中pid为1的进程对所有的信号都不处理,继续continue;由此可见,当信号向量为SIG_IGN或SIG_DFL时,对信号的反应都是在系统空间中完成的,无需返回到用户空间;如果信号向量指向一个信号处理程序,那么就要使用handle_signal完成了;当for循环运行完后,意义是很大的,说明,已经过了好多坎,通常是SIGCHLD,SIGCONT,SIGWINCH等信号,要求自动重新执行失败的系统调用;此时利用regs寄存器中的内容,更新eip即减2,重新执行系统调用;

(14)若用户空间设置了 信号处理程序,要通过handle_signal()准备好对处理程序的执行,大体步骤如下;

(14.1)在用户空间堆栈中为信号处理程序的执行预先创建一个frame,frame中包含局部变量的数据结构,并把系统空间堆栈中的原始框架保存到这个数据结构中;

(14.2)在信号处理程序中出入对系统调用sigreturn();

(14.3)将系统空间堆栈中原始frame修改成执行信号处理程序的框架;

(14.4)返回到用户空间,执行信号处理程序;

(14.5)信号处理程序执行完毕后,通过sireturn()返回系统空间;

(14.6)在系统空间sigreturn()中,从用户空间恢复原始frame;

(14.7)再返回到用户空间,继续执行原先的用户程序;

0 0