Chinaunix首页 | 论坛 | 博客
  • 博客访问: 327124
  • 博文数量: 100
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 665
  • 用 户 组: 普通用户
  • 注册时间: 2015-02-02 12:43
文章分类

全部博文(100)

文章存档

2015年(100)

我的朋友

分类: LINUX

2015-06-08 13:49:42

信号的角色
  • 信号作用
    •   通知进程某个事件发生了
    •   使进程执行信号处理函数
  • 特点:
    •   普通信号和实时信号:
      •    0:不是有效信号,只用于检查是当前进程否有发送信号的权限,并不真正发送。也就无所无处理了
      •    1 - 32: 普通信号,按位存储。当多次发送某个信号,而且来不及处理时,只记录一个
      •    32 - 64:实时信号,列队存储。相同信号,可以发送多个。
    •   信号可阻塞:不接受某个信号。
      •    要处理某个信号前,先阻塞自身,直到处理结束。这样使信号处理函数不必具有可重入性(因为用户可以设定信号处理函数,不能要求太高)
    •   只能在内核态向用户态切换时,检查并处理信号。
      •    即中断或异常处理结束时,而且没有嵌套KCP(可以检查保存的cs寄存器的权限位)。

  • 收到信号的动作
    •   忽略该信号
    •   默认处理
      •    终止进程、dump(终止,并产生core文件)
      •    忽略
        •     最终是否忽略的条件:
          •      进程没有被trace
          •      没有阻塞该信号
          •      显式指明忽略,或默认处理是忽略
      •    停止、继续
    •   被指定的信号处理函数捕获
    •   SIGKILL、SIGSTOP永远不能被忽略、捕获或阻塞,只能用默认处理。
      •    致命信号:默认处理是终止进程,而且不能被捕获的信号(例如SIGKILL)。
  • 多线程程序中的信号
    •   共享:
      •    信号处理函数共享
      •    致命信号杀死所有线程(不论是发给某个线程, 还是发给整个进程。)
    •   不共享:
      •    悬挂信号,信号阻塞位 不共享。
      •    信号最终只送到某一个线程(即使是发给整个进程的,也会选择一个线程接收,接收线程修改共享的数据结构)

  • 数据结构:pd里和信号相关的域:(pd可以是LWP)
    •   悬挂信号:
      •    LWP私有的悬挂信号:pending(struct sigpending)
        •     列队头,struct sigpending(含内嵌链表):
          •      信号悬挂位:signal(sigset_t),那类信号已经到达了。
        •     信号列队节点 struct sigqueue(含内嵌链表):
          •      互斥锁
          •      信号的信息:info (siginfo_t)
            •       谁(哪些函数、模块)发出的信号: si_code
              •        系统调用:发给进程(SI_USER);  发给线程(SI_TKILL)
              •        普通的kernel函数:SI_KERNEL
              •        定时器发出的:SI_TIMER
              •        I/O完成后发出的:SI_SYNCIO
            •       产生该信号时的错误值:  si_errno(一般程序出错后,都会设置错误值,然后给自己信号)。若为零,说明没有错误。
            •       信号值:si_signo
            •       存储与信号相关的数据:si_fields (union)
          •      flags: (列队节点的属性)
          •      进程的用户:  user (struct user_struct)
      •    整个进程共享的悬挂信号:signal (struct signal_struct - 信号描述符)
        •     计数:引用计数、活着的线程数
        •     悬挂信号: shared_pending(struct sigpending)
        •     停止、终止整个线程组,选择接收线程需要的统计数据。
        •     等待该进程死亡的等待列队:wait_chldexit
    •   信号阻塞:阻塞 - 发送信号时会挂上列队,但处理信号时不处理。
      •    信号阻塞位:blocked(sigset_t)
      •    实时信号阻塞位:real_blocked (sigset_t)
    •   信号处理函数相关:
      •    一组信号处理函数:sighand (struct sighand_sruct)
        •     引用计数 count
        •     互斥锁 (锁:信号描述符和信号处理描述符)
        •     64元素数组action (struct k_sigaction)
          •      信号处理函数sa_handler
            •       SIG_DFL(0): 默认处理
            •       SIG_IGN(1): 显式忽略
            •       其他:信号处理的函数指针
          •      处理这个信号时要阻塞的信号:sa_mask
          •      怎么处理信号的flag: sa_flags
            •       是否给父进程发SIGCHLD:
              •        SA_NOCLDSTOP:指明儿子停止时不要给自己发SIGCHLD,儿子检查该位
              •        SA_NOCLDWAIT: 子进程终止时,不僵尸(FIXME: :不给自己发SIGCHLD?)自己不关心儿子的僵尸状态。
            •       信号处理函数相关的:
              •        SA_SIGINFO:给信号处理函数的附加信息
              •        SA_ONSTACK:信号处理函数用可选栈
            •       执行信号处理前后相关的:
              •        SA_NODEFER,SA_NOMASK:执行信号处理时,不阻塞自己
              •        SA_RESETHAND, SA_ONESHOT:执行完信号处理,恢复默认处理
            •       阻塞时被信号唤醒的系统调用:
              •        SA_RESTART:重新启动被信号打断的系统调用
      •    信号处理用的可选栈的地址:sas_ss_sp (ulong)
      •    栈长度: sas_ss_size (size_t)
    •   设备驱动阻塞信号的动作相关:
      •    设备驱动用的回调函数,用来阻塞信号:notifier (int (*)(void*))
      •    通用指针,作为函数参数: notifier_data (void*)
      •    设备驱动的信号阻塞位:notifier_mask (sigset_t *)

  • 数据结构的操作函数
    •   sigset_t的位操作
      •    指定的一个、几个位置0, 置1
      •    设置低32位,高32位都为0或1
    •   检查进程的悬挂信号
      •    检查TIF_SIGPENDING标志位
      •    根据有无未阻塞的悬挂信号(共享的和私有的),设置TIF_SIGPENDING
    •   删除信号
      •    从信号列队删除信号(mask指定删除哪几个,或都删除)
      •    删除整个进程的信号(两个列队)

信号产生
  • 给线程发信号
    •   内核函数选择:
      •    是否有额外信息:siginfo_t
      •    是否是不能阻塞,不能忽略的信号
      •    通过pid发送: sys_tkill, sys_tgkill (系统调用tkill, tgkill的处理函数)
    •   最终调用specific_end_sig_info()完成
      •    参数:信号值(sig), siginfo_t的指针(info), LWP的进程描述符的指针(t)
        •     siginfo_t == 0:用户模式进程发的
        •     siginfo_t == 1:内核发的
        •     siginfo_t == 2:内核发的,而且是 SIGSTOP或SIGKILL
      •    步骤:(执行该函数前提是:禁中断,锁上进程的信号互斥锁)
        •     检查是否忽略 (忽略:返0);如果是普通信号,检查是否已存在 (已存在,返0)
        •     调用send_signal插入信号。send_signal: 参数比specific_end_sig_info多了一个列队指针
          •      info=2(sig=SIGSTOP或SIGKILL),或者进程悬挂的信号过多,或者没有内存分配列队节点
            •       如果sig<=32(普通信号),或 info==0, 或 info==1 或 info->si_code==SI_USER(kill发的); 设置sigset, 返回0;
            •       否则返回 -EAGAIN(错误码)。
          •      否则,分配节点,设置节点内数据,挂上私有悬挂信号列队
        •     如果成功插入,且未被阻塞,调signal_wake_up通知进程。 signal_wake_up:
          •      try_wake_up(): 如果目标进程在可中断睡眠,或停止状态,就可被唤醒
          •      如果上步返0,说明处于运行状态。
            •       如果正在某个CPU运行,给那个CPU发中断消息,强制调度。
            •       否则,不用管。(那个CPU调度到目标进程时,自然会检查信号)
        •     返回1

  • 给进程(线程组)发信号
    •   内核函数选择:
      •    给进程、进程组发信号
      •    是否有额外信息:siginfo_t
    •   最终调用group_send_sig_info()完成
      •    参数:信号值, siginfo_t, LWP的进程描述符
      •    返回值:成功返0, 失败返回错误值
      •    步骤:
        •     检查信号的值和发送者的权限(以下检查条件满足一个就可)。
          •      发送进程的用户有权限
          •      如果信号是SIGCOUT,目的进程与发送进程属于同一个会话
          •      发送进程和目的进程属于同一个用户
        •     锁互斥锁,禁中断
        •     调用handle_stop_signal()解决信号冲突(stop信号(SIGSTOP、SIGTSTP、SIGTTIN、SIGTOUT)和contine信号(SIGOUT)冲突)
          •      如果进程已被杀死,什么都不用做。
          •      从线程组所有列队中删除与新来的信号冲突的信号。
            •       如果新来的是SIGCONT,要唤醒所有线程(try_wake_up)
        •     检查ingnore,是否是已经存在的普通信号。如果满足一个,返0
        •     __group_complete_signal()选择一个线程接收,并唤醒它
          •      选择条件(都满足):
            •       线程未阻塞该信号
            •       线程没有进入僵尸,退出,停止,追踪状态。没有要被kill
            •       要么正占有CPU,要么TIF_SIGPENDING位为0 (都说明这个线程没有信号要处理)
          •      选择顺序:
            •       如果参数pd满足,选择参数线程
            •       否则,从最近一次收到信号的进程开始。沿环链表寻找
          •     选到何时线程:
            •       致命信号:杀死所有线程(直接处理)
            •       否则,signal_wake_up()唤醒找到的合适进程。
        •     打开互斥锁,使能中断

信号的传递
  • 传递 do_signal(): 处理所有的非阻塞悬挂信号
    •   参数:
      •    regs: 内核栈内存放用户模式寄存器的地址
      •    oldset: 传址返回进程的阻塞信号
    •   返回值:
      •    1:还有信号(本次没处理,或处理完一个捕获的就返回了)。
      •    0:没有可处理信号了
    •   步骤:
      •    检查是不是要恢复到用户模式(regs里存的cs的权限==3)
      •    不断循环摘除非阻塞的悬挂信号,直到没有了或碰到一个捕获的信号
        •     检查该进程有没有被trace,如果有,调用notify_parent_cldstop唤醒debugger;该进程再次被调度后,continue。
          •      notify_parent_cldstop的动作:
            •       本身进入traced状态(停止状态的一种)
            •       唤醒父进程,调度。
        •     默认忽略的:continue (显示忽略的根本就没有实际发送)
        •     默认停止的:睡眠,调度,continue
          •      如果是孤儿进程,忽略非SIGSTOP的“默认停止”信号(没有其他原因的停止)
          •      进程睡眠动作:调用do_signal_stop(),当自己是第一个要停止的线程时才执行具体动作
            •       设置group_stop_count位正值,唤醒所有线程
              •        线程醒后检查该值,停止自己
            •       如果父进程没有SA_NOCLDSTOP标志,给父进程发SIGCHLD信号
        •     默认是dump的,生成core文件,结束整个进程(do_group_exit)
        •     捕获的:break; 处理捕获的信号handle_signal(每次捕获一个信号并处理后就退出循环。这样每一次do_signal处理一个捕获的信号,使实时信号可以按照合适的顺序来处理)
          •      模式切换:
            •       handler是在用户空间定义并执行的。
            •       执行前必须切到用户模式,执行后要切回内核模式。
            •       切回内核模式时,内核栈就被清空。所以必须在用户栈保存原来的内核栈。
          •      执行流程:
            •       在用户空间建立内核栈帧(setup_frame),设置栈内iret恢复寄存器(cs eip ss esp)为handle的环境
              •        栈帧:
                •         返回地址:__kernel_sigreturn标签
                •         信号处理的参数:信号值
                •         内核栈保存的硬件寄存器、和其他信息
                •         帧的签名,以使调试器识别出该栈帧(2.6内核)
              •        修改内核栈内保存的寄存器,使之iret返回时,进入新建的栈帧
              •        修改阻塞位,计算pending
            •       do_signal返回后,内核切回到用户模式后,开始执行信号handle
            •       信号handle栈帧的返回地址(__kernel_sigreturn)是: __NR_sigreturn号系统调用。
              •        系统调用的handle是:从用户栈中读数据,
                •         恢复信号阻塞位(并重新计算pending),
                •         恢复内核栈(信号处理之前的用户寄存器)。
                •         异常处理完后,恢复到用户模式,正好是信号处理前的状态。
      •    检查有没有捕获信号检查有没有被信号打断阻塞的系统调用,设置重发。
        •     附加说明:
          •      处理中断前期:SAVE_ALL之前,先把中断、异常、系统调用号的值编码后入栈。SAVE_ALL的寄存器与该值一起组成regs结构(该值的位置就是:regs->orig_eax)。编码方式:
          •      中断(包含以上三种情况)处理完成后。返回值放在:regs->eax
        •     do_signal检查并重发:
          •      检查时机:在处理准备处理捕获信号前,或do_signal结束之前(没有捕获信号时)
          •      检查内容:
            •       检查处理完的是否是异常:regs->orig_eax >=0。
            •       检查处理后的返回值
              •        -EINTR:表示不重发,该值直接返回给用户模式
              •        -ERESTART...: 表示异常处理被信号打断,需要根据情况决定是否重发。
          •      设置重发:
            •       内核栈内保存的用户寄存器,指向刚刚过去的int 0x80或sysenter: regs->eip -= 2 ((FIXME: sysenter,原书为sysreturn是不是有错?))
              •        这样切换到用户模式后,马上又会int 0x80重发系统调用。
            •       调整系统调用参数重发:regs->eax = __NR_restart_syscall,仅用于时间相关的系统调用。例如nanosleep
              •        nanosleep的服务例程会在thread_info->restart_block设定一个特殊函数,它用来调整参数,调用nanosleep原来的服务例程
              •        __NR_restart_syscall,的服务例程仅是执行thread_info->restart_block。

信号相关的系统调用
  • 发信号
    •   给进程发信号:kill (pid)
      •    参数:   指定进程(pid>0),当前进程(0),所有进程(-1),当前进程同组的所有进程(pid<-1)
      •    实现:
        •     某个进程: 用kill_proc_info,
        •     一组进程: kill_pg_info,
        •     系统内所有进程:一次次用group_send_sig_info给所有进程发。
    •   给线程发信号(tkill, tgkill)
      •    tgkill指定:tgid组内的线程pid。防止当线程不断的创建,删除时,某个pid不同时出现在不同的线程组中(发送错误)

  • 改变action、阻塞位
    • 改变sigaction (sigaction(sig, act, old_act))
      •   SIGKILL,SIGSTOP不允许该
      •   最终改成ignore(不论是默认还是显式指定ignore),放弃所有悬挂信号
    • 修改阻塞位sigprocmask,
      •   只能修改非实信号
      • 修改完了要重新计算pending位

  • 检查阻塞的悬挂:sigpending()

  • 睡眠等待信号:sigsuspend。 允许合适的信号后;睡眠,不断被唤醒,睡眠。直到有捕获的信号
    •   do_signal返回1后,该函数返回。
      •    do_signal收到一个捕获信号时,返回1。
      •    不是在返回用户模式时,处理新哈,也返回1(有错误发生)。
      •    其他改变进程状态的信号,不起作用。
    •   如果分成改block位,sleep两个系统调用。中间可能会有信号发过来
      •    可能放行一个信号,(处理信号),然后睡眠(打断睡眠的信号已经处理了,没有信号在唤醒进程)

  • 实时信号:相同功能的加上tr_前缀
    •   rt_sigqueueinfo()
      •    发送实时信号。
    •   rt_sigtimedwait ()
      •    拆除阻塞的悬挂信号,不处理它,仅返回值。
      •    如果没有,等一会儿
阅读(1404) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~