全部博文(436)
分类: LINUX
2013-03-14 10:57:47
进程的静态特性
1. 进程 & 轻量级进程 & 线程
?定义: 进程是程序执行时的一个实例,是充分描述程序已经执行到何种程度的数据结构的汇集。
每个线程代表进程的一个执行流。
?轻量级进程基本上可以共享一些资源,诸如地址空间、打开的文件等,所以只要其一
个修改资源,另一个就立即查看这种修改。
?进程的目的(内核角度):担当分配资源系统的实体。
?子进程可与父进程共享含有函数代码的页,但是各自有独立的数据拷贝(栈和堆)。
在现代Unix系统当中,支持多线程应用程序,多线程应用程序多个执行流的创建、处理、
调度整个都是在用户态进行的。
?实现多线程应用程序的一个简单方式就是把轻量级进程与每个线程关联起来。
?简单多线程(用户态 进程级调度) à 结合轻量级进程(可在内核态独立调度)
为什么实现多线程应用程序一定要把轻量级进程与每个线程关联起来?即使是单单的
线程之间也能共享统一内存空间,同一打开文件集,独立调度。
2. 进程描述符(task_struct结构)
为了管理进程,内核必须清楚描述每一个进程所做的事情。(eg. 优先级、状态、地址空间,访问文件的权限、进程间关系……)
3. 进程状态(state字段)
设置状态字段:
p-> state = TASK_RUNNING
宏:
set_task_state set_current_state
由一组标志位组成(进程可能状态):
状态 |
|
|
可运行状态 |
TASK_RUNNING |
正在执行或准备执行 |
可中断的等待状态 |
TASK_INTERRUPTIBLE |
挂起、睡眠 |
不可中断的等待… |
TASK_UNINTERRUPTIBLE |
|
暂停状态 |
TASK_STOPPED |
SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU暂停信号 |
跟踪状态 |
TASK_TRACED |
进程的执行已由degugger程序暂停 |
僵死状态 |
EXIT_ZOMBIE |
进程被终止,但父进程还未返回信息 |
僵死撤消状态 |
EXIT_DEAD |
最终状态 |
为什么要设置不可中断等待?有什么用?
4. 标识一直进程
描述符; PID编号(循环使用,通过pidmap_array位图来管理PID号)
?在/proc/sys/kernel/pid_max中修改PID的上限值
(/proc是一个特殊文件系统的安装点)
?PID与进程或轻量级进程一一对应提供了灵活性,系统中每个执行的上下文都可以被唯一的标识。
?一个线程组中的所有线程有共同的PID,都使用领头线程的PID,存在描述符的tgid中,getpid()系统调用返回的也是tgig,而不是pid。
5. 进程描述符处理
?进程描述符存放在动态内存中。
?通常把thread_info,线程描述符和内核态的进程堆栈放到一起,一个单独为进程分配的存储区域。(内核很容易能从eap寄存器中获得当前CPU正在运行进程的thread_info结构的地址)
?线程描述符存放在内存区域的开始,而堆栈从尾部开始,往开始的方向增长,esp永远指向栈顶的地址
?通过线程描述符和进程描述符分别通过task和thread_info字段互相关联
?内核通过alloc_thread_info和free_thread_info宏分配和释放存储thread_info结构和内核栈的内存区。
? 内核态的进程访问内核数据段的栈
6. 标识当前进程
?进程最常用的是进程描述符的地址而不是thread_info结构的地址。
获得当前在CPU上运行进程的描述符指针 current_thread_info() –> task:
movl $0xffffe000, %ecx // (或者用于4K的堆栈 0xfffff000)
andl %esp, %ecx
movl (%ecx), p //P得到描述符指针
?用栈存放进程描述符的另一个优点体现在仅通过检查栈就可以获得当前正确的进程
?在多处理器系统上,有必要把current定义成一个数组,每个元素对应一个可用的CPU
7. 双向链表
?list_head结构 创建 LIST_HEAD(list_name)
?Linux2.6中,双向链表不是循环链表,用于散列表,散列表更重要的是空间而不是在固定的时间内找到表中的最后一个元素。
8. 进程链表
?进程链表把所有进程的描述符链接起来,每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev, next字段分别指向前面和后面的task_struct元素。
?进程链表的头是init_task描述符
?for_each_process(p)扫描整个进程链表,每次参数变量中存放的是当前被扫描进程描述符的地址。
9. TASK_RUNNING状态的进程链表
?当内核寻找一个新进程到CPU上运行时,必须只考虑可运行进程(TASK_RUNNNING)
?提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先权对应不同的链表。
?prio_array_t数据结构:
int nr_active 链表中进程描述符的数量
unsigned long[5] bitmap 优先权位图:当且仅当某个优先权的进程链表不为空时设置对应标志位
struct list_head[140] 140个优先权队列的头结点
10. 进程间的关系
兄弟关系???
11. Pidhash表及链表(不懂)
?在几种情况下,内核必须能从进程的PID导出对应的进程描述符指针。
Eg. 进程P1希望向另一个进程P2发送一个信号时
1) P1调用kill() 系统调用,参数为P2的PID
2) 内核从参数的PID导出对应的进程描述符
3) 从P2的进程描述符中取出记录挂起信号的数据结构指针
?为了加速查找,引入了4个散列表,分别包含不同类型PID字段(内核初始化期间分配空间,并将它们地址存入pid_hash数组)
PIDTYPE_PID(进程PID) PIDTYPE_TGID(线程组领头进程的PID)
PIDTYPE_PGID(进程组领头进程的PID) PIDTYPE_SID(回话领头进程的PID)
每个散列表长度依赖于可用RAM的容量???如何换算??
如何用pid_hashfn宏把PID转化成表索引???
?两个不同的PID散列(hash)到相同的表索引称为冲突。
Linux利用链表来处理冲突的PID:每一个表项是由冲突的进程描述符组成的
具有链表的散列法比从PID到表索引的线性转换更优越。
Pid散列表的数据结构:
Int nr pid的数值
Struct hlist_head pid_chain 链接散列链表的下一个和前一个元素
Struct list_head pid_list 每个pid的进程链表头
12. 如何组织进程
为TASK_RUNNING状态分一个组;
TASK_STOPPED, EXIT_ZOMBIE,EXIT_DEAD状态进程的访问比较简单,通过PID或者父进程的子进程链表访问,不单独分组;
根据不同特殊事件把TASK_INTERRUPTIBLE, UNINTERRUPTIBLE状态的进程细分为许多类
13. 等待队列(双向链表)
?等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒他们。
为防止中断处理程序和主要内核函数对双向链表进行同时访问,在等待队列头中有一个lock自旋锁
?睡眠进程: 互斥进程(内核选择唤醒) & 非互斥进程(事件触发唤醒)
14. 等待队列的操作
非互斥插入队首,互斥插入队尾
1) autoremove_wake_function()调用唤醒函数,唤醒睡眠进程
2) 唤醒函数default_wake_function()
3) 设置进程状态为TASK_RUNNING
4) 从等待队列中删除对应的元素(指向睡眠进程的描述符的指针)
5) 内核开发者可以通过init_waitqueue_func_entry()函数来自定义唤醒函数
函数:
不可中断 可中断
Sleep_on() interruptible_sleep_on()
sleep_on_timeout() interruptible_sleep_on_timeout()
wait_event() wait_event_interruptible()
linux2.6
prepare_to_wait() prepare_to_wait_exclusive()
finish_wait()
等待队列的具体结构?为什么非互斥和互斥插入方式不同?
15. 进程资源限制
每个进程都有一组相关的资源限制,限制制定了进程能使用的系统资源数量。避免用户过分使用系统资源,对当前进程的资源限制存放在current->signal->rlim,即进程的信号描述符的一个字段。
Struct rlimit{
Unsigned long rlim_cur;
Unsigned long rlim_max; //只有超级用户才能改变这个值
};
进程切换
挂起正在CPU上运行的进程,并恢复以前挂起的进程
16. 硬件上下文
?进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。在Linux中,硬件上下文的一部分存放在TSS段,剩余部分存放在内核态堆栈中。
早起版本通过far jmp指令切换上下文,linux2.6使用软件执行进程切换(通过一组remove指令逐步执行切换)
?进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器的内容都以保存在内核态堆栈上。
17. 任务状态段
?TSS Task State Segment 任务状态段
TSS反映了当前进程的特权级
?Linux强制为系统中每个不同的CPU创建一个TSS:
用户态切换到内核态时,从TSS中获取内核态堆栈的地址
用户态进程试访问I/O端口时,访问TSS得到I/O许可权位图
每次进程切换时,被替换的进程的上下文保存在进程描述符的thread字段。
18. 执行进程切换
本质上分为两步:
1) 切换页全局目录以安装一个新的地址空间;
2) 切换内核态堆栈和硬件上下文
?Switch_to宏
参数: prev 被替换进程在内存中的位置
Next 新进程描述符的地址在内存中的位置
Last 输出参数,记录被替换的进程?
执行过程:
1) 在eax,edx中分别保存prev和next的值
2) 把eflags和ebp寄存器的内容分别保存在prev内核栈中
3) 把esp的内容保存到prev->thread.esp中,指向prev内核栈的栈顶
4) 把next->thread.esp装入esp中,此时内核开始在next的内核栈上操作,这条指令实际上完成了从prev到next的切换
5) 把标记为1的地址存入prev->thread.eip(当被替换的进程恢复执行时,进程将执行被标记为1的那条指令)
6) 宏把next->thead.eip的值压入next的内核栈
7) 跳到__switch_to()c函数
8) 被替换的进程再次获得CPU,执行一些保存eflags和ebp寄存器内容的指令,同时这两条指令的第一条指令被标记为1
9) 拷贝eax寄存器的内容到switch_to宏的第三个参数last内(即prev)
?__switch_to()函数
函数大多数执行与switch_to()宏的进程切换,作用于prev_p和next_p两个参数(分别从eax和edx中取值)
执行过程:
1) 执行由__unlazy_fpu()宏产生的代码,以有选择地保存prev_p进程的FPU.MMX及MM寄存器的内容。
2) 执行smp_processor_id()宏获得本地CPU的下标(从当前进程的thread_info结构的cpu字段获得下标)
3) 把next_p->thread.esp0装入对应于本地CPU的TSS的esp0字段。(以后任何由sysenter汇编指令产生的从用户态到内核态的特级权转换将把这个地址拷贝到esp寄存器中)
4) 把next_p进程使用的线程局部存储(TLS)段装入本地CPU的全局描述符表,三个段选择符保存在进程描述符内的tls_array数组中
5) 把fs和gs段寄存器的内容分别存放在prev_p->thread.fs
或next_p->thread.gs中
6) 把fs和gs段寄存器已经被prev_p或next_p进程中的任意一个使用,则将next_p进程的thread_struct描述符中保存的值装入这些寄存器中。
7) 用next_p->thread.debugeg数组的内容装载dr0, … ,dr7中的 6个调试寄存器值时。只有在next_p被挂起时正在使用调试寄存器,这种操作才能进行(???)
8) 如果有必有,更新TSS中的I/O位图。
9) 终止。
19. 保存和加载FPU, MMX和XMM寄存器
FPU floating-point unit 浮点运算单元
为了维持与旧模式兼容,浮点运算函数用ESCAPE指令来执行,这些指令作用于包含在CPU中的浮点寄存器集,如果一个进程正在使用ESCAPE指令,那么浮点寄存器的内容就属于他的硬件上下文,并且应该被保存。
在最近的pentium模型中,Intel在它的微处理器中引入一个新的汇编指令集,叫MMX指。