进程、轻量级进程(LWP)、线程
- 进程:程序执行体,有生命期,用来分配资源的实体
- 线程:分配CPU的实体。
- 用户空间实现,一个线程阻塞,所有都阻塞。
- 内核实现,不会所用相关线程都阻塞。用LWP实现,用线程组表示这些线程逻辑上所属的进程。
进程描述符
- 进程描述符(简称pd, process descriptors),结构体是:task_struct
- 数据较多,存放在kenerl的动态内存空间。
- pd的引用放在thread_info中,
- thread_info与内核栈,放在一个8K空间(它的地址8K对齐)。内核程序使用的栈空间很小。
- thread_info在底部,内核栈在顶部向下增长。
- 好处:多CPU时方便,每个CPU根据自己的栈指针就可以找到当前的pd (以后用current表示当前CPU运行的进程描述符)。
- esp(内核栈指针)低8位置零,就是thread_info地址。
- 好处:多CPU时方便,每个CPU根据自己的栈指针就可以找到当前的pd (以后用current表示当前CPU运行的进程描述符)。
- 每进程有自己的thread_info, (分配释放函数: alloc_thread_info, free_thread_info)
- 描述符的内容
- 相关的ID (一个4元素数组)
- 进程ID (PID)
- PID按创建顺序连续增长,到最大值后从最小值开始。
- 0号进程:交换进程(swapper)
- 有PID可用位图,表示那一个PID可用,至少占一个页。
- 线程组ID(tgid),用LWP实现多线程支持
- 多进程时,进程id,就是线程组id, 也就是组长的pid(LWP)。 getpid() 取的是线程组的id(tgid), 也是组长的pid.
- 单线程时,pid = gid。所以getpid,也是真正的pid.
- 进程组ID(pgrp)。
- 回话的ID(session).
- 组ID,都是组长的PID。FIXME: 但pb也有各组长的PID
- 线程组长:tgid
- 进程组长:signal->pgrp ,
- 会话长:signal->session
- 组ID,都是组长的PID。FIXME: 但pb也有各组长的PID
- 管理ID数据结构——哈希表管理 (利用id找到所用相关的pd,方便)。
- 一个哈希表数组(pid_hash),存放四个哈希表, 每一个表代表一类id (pid, tgid, pgrp, session)
- 每个哈希表的由数组(索引为哈希值)和二维链表(嵌入到进程描述符内的pids中)实现
- 第一维链表:哈希冲突链表。
- 第二维链表:要查找的值相同的链表, 叫per-PID list(同一组的所有线程,同一组的所有进程,同一会话的所有进程);
- 进程组ID(pgrp), 回话ID(session)在共享信号的数据结构里。因为同一进程内的所有LWP,这两个ID都是一样的
- 进程ID (PID)
- 相关的ID (一个4元素数组)
-
- 家族关系:由pd里的链表(下级)和pd指针(上级)实现
- 关系:
- 亲生父亲:创建自己的进程,或是托孤进程(创建自己的进程死了)。
- 父亲:自己死时要发信号告知的。一般是亲生父亲,有时是监控自己的进程 (调用ptrace)
- 孩子:
- 兄弟:
- 监控(自己起的名字,类似于监护。由于管理方式相同,也归为家族关系)
- 监控的进程列表:ptrace_children
- 被监控的其他进程:ptrace_list (类似于被监控的兄弟)
- 在链表里为了管理方便:
- 最大儿子的兄弟是父亲
- 最小儿子的弟弟也是父亲
- 父亲保管最大儿子,和最小儿子
- 关系:
- 家族关系:由pd里的链表(下级)和pd指针(上级)实现
-
- 进程资源及资源限制:
- CPU相关:
- 占用CPU总时间
- 用户的最大进程数
- 内存相关:
- 进程地址空间
- 锁住内存大小
- 进程页数 (只有记录,没有限制)
- 堆大小,栈大小
- 资源相关:
- 文件:
- core dump大小
- 最大文件大小
- 打开文件个数
- 进程同步与通信
- 锁数目,
- 悬挂信号数据
- 在消息列队中占的大小
- 文件:
- 相关数据结构 和 处理流程
- pd->sigal->rlim 是一个表示进程资源使用情况以及限制的结构 的数组。
- 表示进程资源使用情况以及限制的结构:包含当前值,最大值两个数值。
- 只有超级用户才能增大资源限制。
- 一般用户登陆时:
- kernel创建root进程,减少limit,
- 建一个 shell子进程,继承limit.
- 把shell进程的用户,改成登陆的那个用户
- CPU相关:
- 进程资源及资源限制:
-
- 进程状态(state)
- 运行,TASK_RUNNING
- 组织pd的结构:就绪进程链:
- 一个CPU一组链表,每个链表表示一种优先级。
- 组织pd的结构:就绪进程链:
- 阻塞
- 可中断阻塞,TASK_INTERRUPTIBLE
- 可被硬件中断,“释放资源”事件,信号唤醒。
- 不可中断阻塞,TASK_UNINTERRUPTIBLE
- 可被硬件中断,“释放资源”事件,唤醒。
- 但不能被信号唤醒。可用于驱动程序中。
- 组织pb的结构:等待列队: 每一类事件一个列队,用内嵌链表实现(虽然没列出内嵌链表节点)
- 列队头:
- 自旋锁:防止有一个主函数和中断函数同时操作列队。
- 列队节点:
- 独占标志:表示该进程是否要独占资源 (不再唤醒别的进程)
- 指向pd的指针
- 用于唤醒进程的回调函数。(提供进程的执行机会,是否操作等待列队由用户决定)
- 列队头:
- 可中断阻塞,TASK_INTERRUPTIBLE
- 停止
- 停止TASK_STOPPED
- 被信号停止
- 追踪TASK_TRACED
- 该进程被一个调试进程监控以后,收到任何一个信号就进入该状态
- 组织pb的结构:FIXME: 信号的等待列队?
- 停止TASK_STOPPED
- 退出
- 退出_僵尸EXIT_ZOMBIE
- 进程终止,资源没有被回收(父进程要用,没有调wait系列函数)
- 退出_死亡EXIT_DEAD
- 进程终止,资源正在被回收(父进程要用,没有调wait系列函数)。
- 一旦资源回收完成,进程描述符也就被回收了。
- 它防止该进程再次被wait.
- 组织pb的结构:不挂到队列上,只在家族关系中,等待父进程收回资源
- 退出_僵尸EXIT_ZOMBIE
- 运行,TASK_RUNNING
- 进程状态(state)
进 程控制 :
- 阻塞(current阻塞到某个列队上):
- 基本流程
- 临时生成一个列队节点,初始化。
- 改变current的状态,放入节点,挂到列队上。
- 调度 (=====》至此,阻塞完成。 一旦被别的进程唤醒====》从调度函数中返回)
- 从等待列队上摘除节点。
- 变化:
- 将挂列队、调度、从列队删除三步拆开,便于灵活处理。
- 可中断的、限时、独占的函数类似。只不过进程状态、调度函数、独占标志不同。
- 非独占的从列队开始添加,独占的从末尾添加。(但一个列队内既有独占的,又有非独占的等待进程,很少见)
- 基本流程
- 唤醒:
- 基本流程
- 唤醒一个进程:调用节点里的回调函数
- 唤醒的时候从列队开头依次唤醒,直到唤醒一个独占的后停止。
- 变化
- 是否只唤醒可中断的进程. (_interruptible后缀)
- 唤醒的独占进程的数目(1个,多个(_nr后缀),所有(_all后缀))
- 唤醒后是否不检查优先级,马上给予CPU (有_sync的不检查优先级)。
- 基本流程
- 进程切换
- 切换pgd (全局页目录),此章不讨论。
- 切换内核栈,硬件上下文
- 硬件上下文,就是CPU的寄存器。
- 一部分(大多数CPU寄存器(除了通用寄存器))在pd中保存(task_struct->thread, 类型是thread_struct),
- 一部分(通用寄存器)保存在内核栈中.
- 原来用硬件指令()保存CPU信息。后来改成软件(一个个MOV指令)
- 容易控制,可以挑选信息保存,便于优化。不保存的做其他用(如:进程间传递)
- far jmp:跳至目标进程的TSSD。而linux是每个CPU一个TSS,不是每进程一个
- 对于一些寄存器(ds、es)可以检查值。
- 与用硬件指令保存时间差不多。
- 容易控制,可以挑选信息保存,便于优化。不保存的做其他用(如:进程间传递)
- 硬件上下文,就是CPU的寄存器。
- switch_to 宏
- 三个参数:
- prev: 要换走的进程,一般是当前进程
- next: 要换到的进程。
- last: 传出参数。当前进程再次被换到时,最后一个占用CPU的进程。(prev指向的进程 就是 next指向的进程 的last)
- 步骤:
- 栈切换, 完成后就是在新进程的上执行了:
- 保存prev(放在eax)
- eflags,ebp入内核栈;
- 保存并装载新的esp (旧的esp放到prev->thread.esp,新的esp是next->thread.esp)
- 此时current就是新的esp所指的thread_info内的task指针
- 设置返回地址:
- prev进程以后得到执行时的__switch_to的返回地址: __switch_to后的第一条指令, 放入prev->thread.eip,
- 准备next进程的从__switch_to返回的地址: next->thread.eip入栈.
- 调用__switch_to ()函数,该函数动作如下:
- 更新CPU的相关信息(tss和gdt):
- 存next->thread.esp0(内核栈低)到本地TSS.esp0中。
- 所在CPU的全局段表里的TLS段, 设成next进程的.
- 更新tss的I/O位图.
- 更新CPU的寄存器(pd->thread (tss) 与 CPU寄存器交换数据):
- 保存FPU, MMX, XMM寄存器, 先不装载以后需要时通过中断装载(TODO: )
- 保存prev的fs, gs寄存器. 装载next的
- 装载next的debug寄存器(debug寄存器一个8个, 进程切换时只需6个)
- 返回
- prev放入eax (prev就是新进程的last)
- ret
- 更新CPU的相关信息(tss和gdt):
- ret返回的地址: (__switch_to之前被存入栈中, __switch_to ret时进入eip)
- 如果是next新进程, next->thread.eip是iret_from_fork.
- 如果next不是新进程:
- 弹出ebp, elfags
- 把eax放入last变量 (prev就是next进程的last)
- 栈切换, 完成后就是在新进程的上执行了:
- 三个参数:
任务状态段(一个存CPU状态的数组,tss_struct init_tss[])
- 每个CPU用段上的一个元素。(FIXME: 用于:用户模式要进入内核模式时,设置相应寄存器)
- TSS上存内核栈地址。CPU上的程序从用户模式转到内核模式,设置esp。
- TSS存I/O端口许可位图。用户模式程序用到I/O时,检查有无权限
- 所以,进程切换时,要保存的寄存器在pd->thread中。
- thread_struct不是thread_info。thread_info中只有少量的数据或指针, 用于通过esp快速定位数据
- 进程切换时,更新TSS上的信息。
- CPU控制单元再从TSS上取需要的信息。
- 即反应了CPU的当前进程情况,又不需要维护所有进程的状态数据。
- TSS的描述符在GDT里。
- TSSD:任务状态段描述符 (其实应该叫任务状态描述符,每个TSSD,表示一个CPU的状态, FIXME: :具体以源码为准)
- CPU原始设计,每个进程一个TSS元素。
- linux设计,每个CPU一个TSS元素。
- cpu里的tr寄存器,保存着自己的TSSD(即init_ttss[cpu_id]),不用总上gdt里去取。 进程创建: clone, fork, vfork系统调用
- clone系统调用
- 参数:
- 执行函数(fn), 参数(arg)
- flags|死亡时给父进程发的信号 (clone_flags): 以下介绍clone_flags
- 资源共享
- 段,页,打开文件共享:
- 页表(不是页, CLONE_VM),
- 打开文件(clone_files),
- 建一个新tls段(clone_settls)
- 路径和权限设置:
- clone_fs: 共享根目录, 当前目录, 创建文件初始权限.
- clone_newns: 新的根路径, 自己的视野看文件系统
- 线程通信
- clone_sighand: 信号处理action, 阻塞和悬挂的信号
- clone_sysvsem: 共享undoable信号量操作
- 段,页,打开文件共享:
- 进程关系
- 同父: clone_parent 创建进程与新进程是兄弟 (同父), 新进程不是创建进程的子进程
- 为了方便期间, 以下讨论暂时不考虑这一因素(它很容易实现), 认为创建进程就是父进程
- 同一个线程组: clone_thread. 属于同一个进程(线程组)
- 都被trace: clone_ptrace
- 子进程不被trace: clone_untrace (内核设置, 覆盖clone_ptrace)
- 同父: clone_parent 创建进程与新进程是兄弟 (同父), 新进程不是创建进程的子进程
- 返回tid
- 向父进程返回tid: clone_parent_settid
- 向子进程返回tid: clone_child_settid
- 子进程的状态:
- 子进程开始就stop: clone_stopped
- 进程死亡或exec通知:
- 启动内核机制: 如果子进程死亡或exec, 它自己空间内的tid(*ctid)清零, 并唤醒等待子进程死亡的进程.
- 资源共享
- 赋给子进程的资源
- 子进程的栈(父进程alloc的内存地址)
- 线程局部仓库段(tls)
- 返回子进程tid的地址
- 父进程用户空间内的地址
- 子进程用户空间的地址
- 参数:
- clone, fork, vfork实现方式
- 大致相同:
- 系统调用服务例程sys_clone, sys_fork, sys_vfork三者最终都是调用do_fork函数完成.
- do_fork的参数与clone系统调用的参数类似, 不过多了一个regs(内核栈保存的用户模式寄存器). 实际上其他的参数也都是用regs取的
- 系统调用服务例程sys_clone, sys_fork, sys_vfork三者最终都是调用do_fork函数完成.
- 区别在于:
- clone:
- clone的API外衣, 把fn, arg压入用户栈中, 然后引发系统调用. 返回用户模式后下一条指令就是fn.
- sysclone: parent_tidptr, child_tidptr都传到了 do_fork的参数中
- sysclone: 检查是否有新的栈, 如果没有就用父进程的栈 (开始地址就是regs.esp)
- fork, vfork:
- 服务例程就是直接调用do_fork, 不过参数稍加修改
- clone_flags:
- sys_fork: SIGCHLD|0;
- sys_vfork: SIGCHLD| (clone_vfork | clone_vm)
- 用户栈: 都是父进程的栈.
- parent_tidptr, child_ctidptr都是NULL.
- clone:
- 大致相同:
- 具体实现函数do_fork() (内核函数)的工作流程:
- 分配PID, 确定子进程到底是否traced.
- 分配空闲的PID
- 确定clone_ptrace位. (确定子进程到底要不要被trace, 而不是参数所说的希望被trace)
- 设置该位: 参数已设该位, 且创建线程被trace中
- 清除该位: 父进程没有被trace, 或 clone_untrace已经设置.
- 复制进程描述符(copy_process)
- 检查clone_flags是否兼容, 是否安全
- clone_newns 与 clone_fs 互斥
- clone_sighand 是 clone_thread 的必要条件: 线程必须共享信号处理
- clone_vm 是 clone_sighand 的必要条件 : 共享信号处理, 首先要共享信号处理的代码(在进程页面里)
- 附加的安全检查: security_task_create(clone_flags)
- 复制进程描述符
- 在父进程的thread_info里保存浮点寄存器: __unlazy_fpu()
- 分配新的进程pd(alloc_task_struct), 并拷贝父进程pd
- 分配新的thread_info(alloc_thread_info), 并拷贝父进程的thread_info.
- 新的thread_info和新分配的pd 相互引, 新pd的引用计数设为2 (表示:新pd有用, 且不是僵尸进程)
- 相关计数加1: (此处先相关计数检查, 都通过后再都加1)
- 检查并增加: 用户拥有进程数, 系统总共进程数.
- 一般来说, 所有进程的thread_info总和, 不超过物理内存的1/8
- 新进程的可执行格式的引用计数(FIXME: pd里标有可执行个数吗)
- 系统执行fork总数.
- 检查并增加: 用户拥有进程数, 系统总共进程数.
- 进程pd的关键域的设置(顺序与源码可能不一致):
- 进程关系
- 设置父子关系 (parent, real_parent, 考虑被trace的情况)
- 设置新pd的PID
- 设置tgid, 线程组长的pd(pd->group_leader). (根据是不是线程组长, 即clone_thread位是否为0)
- 加入PID哈希表(pid, tgid, 如果是进程组长加入pgid和sid表),(调attach_pid())
- 拷贝tid到父进程的用户空间(parent_tidptr)
- 拷贝资源(如果clone_flags没标明共享):
- 文件,目录,内存:copy_files, copy_mm, copy_namespace,
- 进程通信: copy_signal, copy_sighand, copy_semundo
- 设置子进程的内核栈(thread_info), 内核态相关寄存器(thread_struct, 不知道这个结构的具体用处): copy_thread()
- 子进程的thread_struct:
- esp, esp0 - 内核栈顶, 内核栈底
- eip - ret_from_fork()的地址 (用户态切到内核态的第一条指令)
- I/O许可位图 - 如果父进程有, 就拷贝一份过来
- TLS - 如果用户空间提供了TLS段, 拷贝过来
- 设置子进程的内核栈:
- child_regs.esp = 传入的栈地址参数;
- child_regs.eax = 0, 给用户态的返回值是0
- 清除thread_info中的, TIF_SYSCALL_TRACE位, 防止运行ret_from_fork时, 系统通知调试进程
- 设置子进程的thread_info的cpuid
- 子进程的thread_struct:
- 设置调度信息(sched_fork())
- 设置task_running状态,
- 初始化调度参数(时间片),
- 子进程禁止内核抢占(thread_info.preempt_cout = 1)
- 其他:
- 如果没有被trace,pd->ptrace = 0;
- 设置pd->exit_signal:
- 有clone_thread位: 设为参数clone_flags中的退出信号
- 没有clone_thread位: 设为-1 (表示进程终止时, 该LWP不给父进程发信号)
- pd->flags: 清除PF_SUPERPRIV , 设置PF_FORKNOEXEC
- 大内核锁 pd->lock_depth = -1
- exec次数: pd->did_exec = 0
- 拷贝child_tidptr到pd->set_child_tid. 以备子进程开始执行时, 把tid放到自己内存空间的child_tidptr
- 进程关系
- 返回pd
- 检查clone_flags是否兼容, 是否安全
- 设置父子进程的运行状态, 调度信息
- 设置子进程的状态.
- 挂信号: 如果创建出来的是停止(clone_stopped)或被trace(pd->ptrace里有PT_PTRACE位)的进程, 悬挂一个SIGSTOP信号.
- 只有debugger发出SIGCONT信号后, 才能进入运行状态
- 设状态,入列队:如果有clone_stopped位, 子进程设为stopped状态; 否则调用wake_up_new_task(), 把子进程加入就绪列队:
- 调整父进程和子进程的调度参数 (主要是时间片)
- 如果父子在同一CPU上运行, 且页表不同享, 子进程在插在父进程前
- 子进程很可能exec, 不与父进程共享页. 这样防止父进程无用的copy on write.
- 如果不同CPU上运行, 或者共享页表, 子进程放在列队最后
- 挂信号: 如果创建出来的是停止(clone_stopped)或被trace(pd->ptrace里有PT_PTRACE位)的进程, 悬挂一个SIGSTOP信号.
- 如果父进程处于被调试状态, 程通知调试器
- 当前进程给debugger进程发信号, 告知自己创建了子进程; 并停止自己(进入traced状态), 使debugger运行.
- 子进程的pid保存在current->ptrace_message中, 供debugger用
- 调试器发信号, 使父进程继续后, 再进行下一步; 否则父进程一直处于traced状态
- 当前进程给debugger进程发信号, 告知自己创建了子进程; 并停止自己(进入traced状态), 使debugger运行.
- 设置父进程状态
- 如果有clone_vfork, 把自己放到一个等待列队.
- 内核处理完系统调用后, 会执行调度, 这样就阻塞父进程了.
- 直到子进程释放了它的内存地址空间, 即子进程终止或exec新程序, 用信号唤醒父进程.
- 如果有clone_vfork, 把自己放到一个等待列队.
- 设置子进程的状态.
- 返回子进程的pid.
- 子进程被调度后,执行pd.thread.eip(ret_from_fork). 调用关系(=>): ret_from_fork=>schedule_tail=>finish_task_switch.
- schedule_tail的另一件事就是: 把pid保存到地址pd->set_child_tid (创建进程使的parent_tidptr)
- finish_task_switch的动作是: 装载内核栈保存的寄存器(regs->eax为0),返回到用户态。系统调用返回值就是eax(0)
- 分配PID, 确定子进程到底是否traced.
- 内核线程:
- 只运行于kernel模式,只能访问大于3G的空间。而普通进程在内核模式时,能访问整个4G空间
- 创建方法, 类似于clone
- 准备返回地址fn: 构造一个regs. 里面有fn, args, __KERNEL_CS等. regs->eip是汇编函数kernel_thread_helper
- do_fork (flags|CLONE_VM|clone_untraced, 0, ®s, 0, NULL, NULL)
- 创建线程, 与父进程共享页. 用上步构造的regs初始化新程的内核栈
- 新线程被调度后. 由ret_from_fork, 用regs恢复寄存器, 开始执行kernel_thread_helper
- kernel_thread_helper: 把args压入栈, call fn(args, fn都寄存器中)
- 典型的内核线程:
- 进程0: 所有进程的祖先
- 编译时存在.
- pd, 内核栈: init_task, init_thread_union
- 资源: init_mm, init_files, init_fs. 信号: init_signals, init_sighand
- 页表: swapper_gd_dir
- 功能
- 初始化系统数据,
- 多CPU系统中, 开始时BIOS禁用其他CPU.
- 初始化系统数据后, 进程0拷贝自己到其他CPU的调度列队上, 启动其他CPU, 所有的PID都是0.
- 使能中断
- 创建内核线程1, (函数是init)
- 进入idle
- 初始化系统数据,
- 编译时存在.
- 进程1:
- init函数 exec可执行文件init, 使内核线程变成了普通进程.
- 管理其他进程, 称为托孤进程
- 其他内核线程:
- 执行工作列队:
- ksoftirqd: 执行 softlets
- kblockd: 执行工作列队 kblockd_workqueue, 定期激活块设备驱动
- keventd (又叫events): 处理工作列队 keventd_wq
- 管理资源:
- kapmd: 电源管理
- kswapd: 交换进程, 用于回收内存资源
- pdflush: flush脏的磁盘缓存
- 执行工作列队:
- 进程0: 所有进程的祖先
进程销毁
- 进程终止
- 系统调用
- 整个进程终止: exit_group(), 由do_group_exit处理系统调用. c函数 exit()也是用的这系统调用
- 某个线程终止: _exit(), 由do_exit处理. C函数中用到此系统调用的API: pthread_exit
- do_group_exit流程: (整个组内至少有一个线程调用它, 用于整组协调)
- 检查线程组的退出过程是否启动: 检查signal_group_exit(线程组内的公共数据)是否非零. 如果没有启动, 执行一下操作来启动退出过程:
- 设置启动标志signal_group_exit.
- 存储终止码(exit_group的参数), 在current->signal->group_exit_cold
- 向其他线程发SIG_KILL信号, (它们收到信号后, 调do_exit())
- 调用do_exit, 使本线程退出
- 检查线程组的退出过程是否启动: 检查signal_group_exit(线程组内的公共数据)是否非零. 如果没有启动, 执行一下操作来启动退出过程:
- do_exit流程:
- 设置线程的终止标志, 退出码
- 设置PF_EXITING, 标明要被终止
- 设置pd->exit_code
- 系统调用参数
- 或是内核提供的错误码, 表示异常终止
- 释放资源:
- 删除该进程的定时器
- 去除对资源的引用:
- exit_mm, __exit_files;
- __exit_fs(root路径,工作路径, 创建文件权限), exit_namespace(挂载的文件系统的视野);
- exit_thread(thread_struct), exit_sem,
- 如果这个线程的函数实现了一种可执行格式, 可执行格式数的引用计数--; FIXME: 还没看到这块儿, 凑合翻译的不一定对
- 改变父子关系, 并向父进程发信号, 改变自己的状态(exit_notify)
- 托付终止线程创建的子进程:
- 如果终止线程还有同组线程: 终止线程创建的子进程, 作为与同组线程的子进程.
- 否则: 终止线程创建的子进程, 作为孤儿进程, 由init进程托管
- 向父进程发信号
- exit_signal有意义 && 最后线程 : 发exit_signal
- 否则:
- 被trace : 发SIGCHLD
- 没被trace : 不发信号
- 僵尸自己或直接死亡, 并设置PF_DEAD位
- exit_signal没意义 && 没被trace : 直接死亡 (这种情况没有发信号)
- 变成EXIT_DEAD状态,
- release_task() (后面介绍). pd引用计数变为1, 不会马上释放
- 否则: 僵尸
- exit_signal有意义 || 被trace : 僵尸
- 整理"僵尸"与"发临僵尸信号"的关系:
- 将发信号的条件中"最后线程"去掉, 可简化为(exit_signal有意义)||(被trace) == (发信号)
- 可得出后: (发信号) == (僵尸)
- 又可推出: (没有trace && exit_signal有意义 && 不是最后进程) == (僵尸了,但没法信号) , 这种情况在移除死进程时, 会给其父进程发信号 (FIXME: 待验证)
- exit_signal没意义 && 没被trace : 直接死亡 (这种情况没有发信号)
- 托付终止线程创建的子进程:
- 调度. 调度函数会忽略僵尸进程, 但会减少僵尸进程的pd的使用计数; 会检查PF_DEAD位, 把它变成exit_dead状态
- 设置线程的终止标志, 退出码
- 系统调用
进程移除 TODO: