进程、轻量级进程(LWP)、线程

进程、轻量级进程(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地址。
      •    每进程有自己的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数据结构——哈希表管理 (利用id找到所用相关的pd,方便)。
        •     一个哈希表数组(pid_hash),存放四个哈希表, 每一个表代表一类id (pid, tgid, pgrp, session)
        •     每个哈希表的由数组(索引为哈希值)和二维链表(嵌入到进程描述符内的pids中)实现
          •      第一维链表:哈希冲突链表。
          •      第二维链表:要查找的值相同的链表, 叫per-PID list(同一组的所有线程,同一组的所有进程,同一会话的所有进程);
      •    进程组ID(pgrp), 回话ID(session)在共享信号的数据结构里。因为同一进程内的所有LWP,这两个ID都是一样的
  •  
    •   家族关系:由pd里的链表(下级)和pd指针(上级)实现
      •    关系:
        •     亲生父亲:创建自己的进程,或是托孤进程(创建自己的进程死了)。
        •     父亲:自己死时要发信号告知的。一般是亲生父亲,有时是监控自己的进程 (调用ptrace)
        •     孩子:
        •     兄弟:
      •    监控(自己起的名字,类似于监护。由于管理方式相同,也归为家族关系)
        •     监控的进程列表:ptrace_children
        •     被监控的其他进程:ptrace_list (类似于被监控的兄弟)
      •    在链表里为了管理方便:
        •     最大儿子的兄弟是父亲
        •     最小儿子的弟弟也是父亲
        •     父亲保管最大儿子,和最小儿子
  •  
    •   进程资源及资源限制:
      •    CPU相关:
        •     占用CPU总时间
        •     用户的最大进程数
      •    内存相关:
        •     进程地址空间
        •     锁住内存大小
        •     进程页数 (只有记录,没有限制)
        •     堆大小,栈大小
      •    资源相关:
        •     文件:
          •      core dump大小
          •      最大文件大小
          •      打开文件个数
        •     进程同步与通信
          •      锁数目,
          •      悬挂信号数据
          •      在消息列队中占的大小
      •    相关数据结构 和 处理流程
        •     pd->sigal->rlim 是一个表示进程资源使用情况以及限制的结构 的数组。
        •     表示进程资源使用情况以及限制的结构:包含当前值,最大值两个数值。
      •    只有超级用户才能增大资源限制。
      •    一般用户登陆时:
        •     kernel创建root进程,减少limit,
        •     建一个 shell子进程,继承limit.
        •     把shell进程的用户,改成登陆的那个用户
  •  
    •   进程状态(state)
      •    运行,TASK_RUNNING
        •     组织pd的结构:就绪进程链:
          •      一个CPU一组链表,每个链表表示一种优先级。
      •    阻塞
        •     可中断阻塞,TASK_INTERRUPTIBLE
          •      可被硬件中断,“释放资源”事件,信号唤醒。
        •     不可中断阻塞,TASK_UNINTERRUPTIBLE
          •      可被硬件中断,“释放资源”事件,唤醒。
          •      但不能被信号唤醒。可用于驱动程序中。
        •     组织pb的结构:等待列队: 每一类事件一个列队,用内嵌链表实现(虽然没列出内嵌链表节点)
          •      列队头:
            •       自旋锁:防止有一个主函数和中断函数同时操作列队。
          •      列队节点:
            •       独占标志:表示该进程是否要独占资源 (不再唤醒别的进程)
            •       指向pd的指针
            •       用于唤醒进程的回调函数。(提供进程的执行机会,是否操作等待列队由用户决定)
      •    停止
        •     停止TASK_STOPPED
          •      被信号停止
        •     追踪TASK_TRACED
          •      该进程被一个调试进程监控以后,收到任何一个信号就进入该状态
        •     组织pb的结构:FIXME: 信号的等待列队?
      •    退出
        •     退出_僵尸EXIT_ZOMBIE
          •      进程终止,资源没有被回收(父进程要用,没有调wait系列函数)
        •     退出_死亡EXIT_DEAD
          •      进程终止,资源正在被回收(父进程要用,没有调wait系列函数)。
          •      一旦资源回收完成,进程描述符也就被回收了。
          •      它防止该进程再次被wait.
        •     组织pb的结构:不挂到队列上,只在家族关系中,等待父进程收回资源
 程控制 :
  • 阻塞(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)可以检查值。
        •     与用硬件指令保存时间差不多。
    •   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
        •     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)
          •      返回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取的
    •    区别在于:
      •     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.
  • 具体实现函数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
        •     设置调度信息(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_stopped)或被trace(pd->ptrace里有PT_PTRACE位)的进程, 悬挂一个SIGSTOP信号.
          •      只有debugger发出SIGCONT信号后, 才能进入运行状态
        •     设状态,入列队:如果有clone_stopped位, 子进程设为stopped状态; 否则调用wake_up_new_task(), 把子进程加入就绪列队:
          •      调整父进程和子进程的调度参数 (主要是时间片)
          •      如果父子在同一CPU上运行, 且页表不同享, 子进程在插在父进程前
            •       子进程很可能exec, 不与父进程共享页. 这样防止父进程无用的copy on write.
          •      如果不同CPU上运行, 或者共享页表, 子进程放在列队最后
      •    如果父进程处于被调试状态, 程通知调试器
        •     当前进程给debugger进程发信号, 告知自己创建了子进程; 并停止自己(进入traced状态), 使debugger运行.
          •      子进程的pid保存在current->ptrace_message中, 供debugger用
          •      调试器发信号, 使父进程继续后, 再进行下一步; 否则父进程一直处于traced状态
      •    设置父进程状态
        •     如果有clone_vfork, 把自己放到一个等待列队.
          •      内核处理完系统调用后, 会执行调度, 这样就阻塞父进程了.
          •      直到子进程释放了它的内存地址空间, 即子进程终止或exec新程序, 用信号唤醒父进程.
    •   返回子进程的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)
  • 内核线程:
    •   只运行于kernel模式,只能访问大于3G的空间。而普通进程在内核模式时,能访问整个4G空间
    •   创建方法, 类似于clone
      •    准备返回地址fn: 构造一个regs. 里面有fn, args, __KERNEL_CS等. regs->eip是汇编函数kernel_thread_helper
      •    do_fork (flags|CLONE_VM|clone_untraced, 0, &regs, 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脏的磁盘缓存
进程销毁
  • 进程终止
    •   系统调用
      •    整个进程终止: 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, 使本线程退出
    •   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: 待验证)
      •    调度. 调度函数会忽略僵尸进程, 但会减少僵尸进程的pd的使用计数; 会检查PF_DEAD位, 把它变成exit_dead状态
进程移除 TODO:
posted on 2010-01-06 15:26  Parry Nee  阅读(1892)  评论(0编辑  收藏  举报