进程、轻量级进程(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相关:
- 内存相关:
- 进程地址空间
- 锁住内存大小
- 进程页数 (只有记录,没有限制)
- 堆大小,栈大小
- 资源相关:
- 文件:
- core dump大小
- 最大文件大小
- 打开文件个数
- 进程同步与通信
- 相关数据结构 和 处理流程
- pd->sigal->rlim 是一个表示进程资源使用情况以及限制的结构 的数组。
- 表示进程资源使用情况以及限制的结构:包含当前值,最大值两个数值。
- 只有超级用户才能增大资源限制。
- 一般用户登陆时:
- kernel创建root进程,减少limit,
- 建一个 shell子进程,继承limit.
- 把shell进程的用户,改成登陆的那个用户
-
- 进程状态(state)
- 运行,TASK_RUNNING
- 阻塞
- 可中断阻塞,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里去取。
|