930 likes | 1.15k Views
Linux 操作系统分析与实践 第三讲:进程管理. 《 Linux 操作系统分析与实践 》 课程建设小组 北京大学 二零零八年春季 *致谢:感谢 Intel 对本课程项目的资助. 本讲主要内容. Linux 中的进程 Linux 进程控制 Linux 的进程调度 Linux 源代码阅读示例 : 进程调度 schedule 部分的阅读. 一、 Linux 中的进程. 进程 是程序执行时的一个实例 从内核的观点来看,进程的目的是担当分配系统资源 (CPU 时间,存储器等 ) 的实体 Linux 中的关于进程的代码大部分是如何管理进程的代码
E N D
Linux操作系统分析与实践第三讲:进程管理 《Linux操作系统分析与实践》课程建设小组 北京大学 二零零八年春季 *致谢:感谢Intel对本课程项目的资助
本讲主要内容 • Linux中的进程 • Linux进程控制 • Linux的进程调度 • Linux源代码阅读示例: • 进程调度schedule部分的阅读
一、Linux中的进程 • 进程 是程序执行时的一个实例 • 从内核的观点来看,进程的目的是担当分配系统资源(CPU 时间,存储器等)的实体 • Linux中的关于进程的代码大部分是如何管理进程的代码 • 每个进程运行的是程序的代码
轻量级进程 • 线程代表进程的一个执行流,内核无法感知 • Linux使用轻量级进程对多线程应用程序提供更好的支持 • 轻量级进程可以共享资源 • 通过将轻量级进程与线程相关联,内核可以独立调度线程
进程描述符(续) Task_struct结构的描述: • 进程标识 • 进程状态(State) • 进程调度信息和策略 • 标识号(Identifiers) • 进程通信有关的信息(IPC) • 进程链接信息(Links) • 时间和定时器信息(Times and Timers) • 文件系统信息(Files System) • 处理器相关的上下文信息
进程描述符(续) • Linux中每一个进程由一个task_struct数据结构来描述(进程控制块PCB) • 进程描述符放在动态内存中而且和内核态的进程栈放在一个独立的8KB的内存区中 • 好处:通过esp就能引用进程描 述符
current宏 • current宏 • current宏获取当前正在运行的进程描述符的指针,current宏经常作为进程描述符出现在内核代码里,例如current->pid返回当前正在运行的进程的PID值
进程的状态 • task_struct 中的state 表示进程当前的状态 • Linux中的进程有5个状态:
进程链表 • task_struct中的 struct task_struct *next_task, *prev_task;
TASK_RUNNING状态的进程链表 • task_struct中的 struct list_head run_list; • list_head是Linux内核当中定义的一个数据结构用来实现双向链表,Linux内核中使用上百个向链表来存放各种数据结(include\list.h)
进程PID hash • task_struct中的pid • 为了快速的从pid值获得进程描述符。需要有hash表 • hash_pid(),unhashpid()在pidhash表中分别插入和删除一个进程 • find_task_by_pid()查找散列表并返回给定PID的进程描述符指针
进程之间的父子关系 • task_struct中的 struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; • p_opptr : original parent (process 1 或者创建它的父进程) • p_pptr: parent (父进程,有时候是调试时的调试监管进程) • p_cptr: child (指向自己最年轻的子进程) • p_ysptr:指向比自己年轻的兄弟进程 • p_osptr:指向比自己老的兄弟进程
进程之间的父子关系(续) • Linux中的0号进程,通常称为swapper进程,是所有进程的祖先。由它执行cpu_idle()函数,当没有其他进程处于TASK_RUNNING的时候,调度程序会选择0号进程运行 • 0号进程创建1号进程,通常称为init进程。它创建和监控其他进程的活动
进程的地址空间 • Linux把进程的线性地址空间组织为一个个线性区 • 每一个线性区对应一组连续的页 • 线性区之间不重叠
进程的地址空间(续) • task_struct struct mm_struct *mm; 内存描述符 mm_struct 里面有一个字段mmp 指向内存线性区链表的首部。
进程的地址空间(续) • 每个线性区有一定的访问权限 • 在增加或删除线性区时,Linux尽量合并访问权限相同且相邻的线性区
进程堆的管理 • 每个进程都拥有一个特殊的线形区:堆 • 内存描述符里面的start_brk和brk字段限定了这个区的开始地址和结束地址 • 常用的C库函数:malloc(),free() • 系统调用brk()用于直接修改堆大小
二、LINUX进程控制 • Linux进程的创建和执行 • 相关的数据结构和系统调用 • 进程的创建 • 程序的执行 • Linux进程的撤消
相关的数据结构 • 系统创建进程时,Linux为新进程分配一task_struct结构,进程结束时收回其task_struct结构 • Linux在内存空间中分配了一块空间来存放进程的task_struct结构,并将所有的task_struct结构的指针放在一个task数组中,该数组是在操作系统内核中专门开辟的一块区域,数组大小也就是系统中所能容纳的进程的数目
相关的数据结构(续) • Task数组的结构: struct task_struct *task[NR_TASKS] ={&init_task}; NR_TASKS是数组的大小,默认是512
相关的数据结构(续) • Task_struct结构的描述: • 进程标识 • 进程状态(State) • 进程调度信息和策略 • 标识号(Identifiers) • 进程通信有关的信息(IPC) • 进程链接信息(Links) • 时间和定时器信息(Times and Timers) • 文件系统信息(Files System) • 处理器相关的上下文信息
相关的系统调用 • Fork() 通过复制调用进程来建立新的进程,是最基本的进程建立过程 • Exec 包括一系列系统调用,它们都是通过用一个新的程序覆盖原来的内存空间,实现进程的转变 • Wait() 提供初级的进程同步措施,能使一个进程等待,直到另外一个进程结束为止。 • Exit() 该系统调用用来终止一个进程的运行
如何区分父进程和子进程的功能? • 父子进程怎样被调度执行? • 父子进程在内存中如何存放? • 子进程如何继承父进程的资源?
进程的创建过程 新的进程通过克隆旧的进程来建立,当前进程是通过fork()系统调用来建立新的进程 当系统调用结束时,内核在系统的物理内存中为新进程分配新的task_struct结构,并为新进程要使用的堆栈分配物理页和进程标志符 父进程和子进程共享打开的文件 fork()的简单流程
Fork调用执行示意 如上图所示,分别使fork调用前后的两个部分 PC指向当前执行的语句,fork之前,它指向第一个printf语句,fork调用之后进程A、B一起运行,A是父进程,B是子进程-进程A的副本,执行与A一样的程序。两个pc都指向第二个printf语句。也就是AB从程序的相同点开始执行
Fork——内存的变动 Fork()执行的内存变动如下: • 分配1页给task_struct结构 • 分配1页给内核堆栈 • 分配1页给pg_dir并且给page_tables分配一些页
Fork——硬件相关的变化 硬件相关的变化 • SS被置为内核堆栈(0x10) • ESP被置为新分配栈的顶端(kernel_stack_page) • CR3指向新分配的页目录 (由copy_page_table()完成) • Idt=_LDT(task_nr)建立新的局部描述符 • 为新的任务状态段(tss)和局部描述符表(ldt[])装入gdt • 从父进程继承剩下的寄存器
Fork系统调用 Pid_t fork(void); 由fork创建的新进程称为子进程。该函数被调用一次,会返回两次。给子进程的返回值是0,给父进程的返回值是子进程的进程ID。然后子进程和父进程继续执行fork之后的指令。子进程拥有父进程数据空间,堆和栈的拷贝,但是它们并不是共享这些存储空间。这里就用到了 “写时复制”技术
写时复制 • 写时复制技术(copy_on_write) Linux通过写时复制技术来调入执行的程序 Linux将可写虚拟内存页的页表项标志为只读,当进程向该内存页写入数据时,处理器会发现内存访问中的问题(向只读页中写入),然后会导致操作系统可以捕获的页故障,由操作系统来完成内存页的复制 一个给定的物理页面可以代表多个逻辑页面,当这个页被一个进程从另一个进程处得到共享时,它是逻辑上的拷贝 如前面的fork调用,逻辑拷贝整个进程的地址空间,仅当试图修改页面(产生写错误)才真正的拷贝
程序的执行 • 用fork创建子进程之后,为了让父进程和子进程执行不同的任务,经常需要调用一种exec函数以执行另一个程序。当进程调用exec函数时,该进程完全由新程序替代。新程序从main开始执行 • exec并不创建新进程,前后进程ID是不变的。它是用另外一个程序替代了当前进程的正文,数据,堆和栈
exec函数 exec执行时的内存变化 • 1页分配给可执行文件头 • 1页或者多页分配给堆栈 硬件相关的变化: • clear_page_tables()移去旧页 • 在新的LDT[]中设置描述符 • 包含argv和envp的“脏”页被分配 • 设置调用者的指针 • 设置调用者的堆栈指针指向建立的堆栈 • 更新内存段的边界
Fork VS Exec 因为fork只能建立相同程序的副本,如果它是程序员唯一可以使用的建立进程的手段,会影响linux的性能 exec系列系统调用把新进程装入调用进程的地址空间,改变调用进程的代码。如果exec成功,调用者进程将被覆盖,从新进程的入口地址开始执行。exec只用新进程取代了原来的进程。并且没有返回数据
进程的撤销 • 撤销时机 • 主动撤销:执行完代码,通知内核释放进程的资源 • 被动:内核有选择地强迫进程死掉。e.g内核代表进程运行时在内核态产生不可恢复的异常
进程的撤销 • 进程可能已死,但必须保存它的描述符,在你进程得到通知后才可以删除 • 僵死状态:表明进程已死,但需要等待父进程删除 • 撤销过程分为 • 进程终止:释放进程占有的大部分资源 • 进程删除:彻底删除进程的所有数据结构
系统调用_exit() • C编译程序总是把exit()插入到main()的最后一条语句之后,exit()调用_exit()系统调用 • _exit() 调用do_exit()释放进程所占资源,终止进程
进程终止:do_exit() • 删除内核对终止进程的大部分引用: • 信号量队列中的进程描述符 • 删除进程描述符中与分页、文件系统、打开文件描述符和信号处理相关数据结构 • 减小进程所用模块的引用计数 • 将进程描述符的exit_code字段设置为终止代号 • 更新父子进程的亲属关系,强制将自己的子进程作为其它某个进程的子进程,以等待该父进程进行删除 • 调用schedule()进行调度
进程删除 • 父进程调用wait()类系统调用检查子进程是否终止 • 若子进程包含终止代号,则父进程通过release()释放僵死进程的描述符 • 释放进程id • 从进程链表中删除进程描述符 • 释放存放进程描述符的内存区
三、Linux的进程调度 • schedule() 决定是否进行进程切换,若要切换,切换到哪个进程 • Linux的调度时机 • 调度策略
1、进程调度的依据 • 选择一个权值(weight)最大的进程 • 权值的计算goodness(): 综合policy,priority,rt_priority和counter四项计算 • 步骤: 1、区分实时进程和普通进程 实时进程的权值:rt_priority 普通进程的权值:与counter有关 2、权值: 普通进程:weight = p->counter 实时进程:weight = 1000 + rt_priority
普通进程 SCHED_OTHER 实时进程 SCHED_RR policy SCHED_FIFO priority task_struct rt_priority counter
普通进程 • 动态优先调度 • 周期性地修改进程的优先级(避免饥饿) • 根据进程的counter值 • 实时进程的优先级是静态优先级
counter • 剩余的时间片 • 时间单位:时钟滴答(10ms) • 初值200ms • do_fork()同时设置父进程和子进程的counter域 • 把父进程的剩余时间片分成相等的两份,父子进程各占一份 • 对于普通进程,counter起作用 • 与priority的关系
当时间片用完后,用priority重新对counter赋值 • 何时重新赋值——所有处于可运行状态的普通进程的时间片都用完 • 时间片确定 • CPU时间的两个层次:时间段,时间段中时间片 • 基本时间片
need_resched 此标志若为1,调用调度程序schedule() • 选择下一个应该运行的进程 • 调用ret_from_sys_call()恢复所选进程的环境 • 时钟中断:最频繁的调度时机