《Tsinghua os mooc》第11~14讲 进程和线程
第十一讲 进程和线程
-
进程 vs 程序
- 程序 = 文件 (静态的可执行文件)
- 进程 = 执行中的程序 = 程序 + 执行状态
- 进程的组成包括程序、数据和进程控制块
- 同一个程序的多次执行过程对应为不同进程
-
三状态进程模型:就绪、运行、等待
-
挂起(Suspend):把一个进程从内存转到外存。
-
线程
- 为什么需要引入线程?因为需要并行执行而又能共享资源的场景。
- 线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单元,是CPU调度的基本单位。
- 线程允许进程内多个执行流并行运行,并且能共享。
-
进程 vs 线程
- 进程是资源分配单位,线程是CPU调度单位。进程由一组相关资源构成,包括地址空间(代码段、数据段)、打开的文件等各种资源。线程描述在进程资源环境中的指令流执行状态。
- 进程拥有一个完整的资源平台,而线程只独享指令流执行的必要资源,如寄存器和栈。
- 线程具有就绪、等待和运行三种基本状态和状态间的转换关系。
- 线程能减少并发执行的时间和空间开销。线程的创建时间比进程短,线程的终止时间比进程短,同一进程内的线程切换时间比进程短。而且由于同一进程的各线程间共享内存和文件资源,可不通过内核进行直接通信。
-
进程控制块(PCB)包含三种信息:
- 进程标识信息。如pid,name
- 处理机现场保存。如context和tf
- 进程控制信息
- 调度和状态信息。如state,runs, need_resched,flags
- 进程间通信信息。如parent,cptr,yptr,optr
- 存储管理信息。如kstack, mm, cr3
- 进程所用资源。
- 有关数据结构连接信息。如list_link, hask_link
第十二讲 进程控制
-
内核为每个进程维护了对应的进程控制块(PCB),并且将相同状态的进程的PCB放置在同一队列,比如有就绪队列、I/O等待队列和僵尸队列。
-
同一个等待队列里的各个进程等待的事件相同吗?为什么发生事件后此队列的所有进程都被唤醒?
-
进程创建
- Windows进程创建API: CreateProcess(filename)
- Unix进程创建系统调用:fork/exec
- exec: 允许进程“加载”一个完全不同的程序,并从main开始执行(即_start);允许进程加载时指定启动参数(argc, argv);exec调用成功时,它是相同的进程,但是运行了不同的程序;代码段、堆栈和堆(heap)等完全重写。
-
进程等待:wait()系统调用用于父进程等待子进程的结束。具体而言,子进程结束时通过exit()向父进程返回一个值,父进程通过wait()接受并处理返回值。父进程调用wait时的具体行为如下:
- 有子进程存活时,父进程进入等待状态,等待子进程的返回结果。当某子进程调用exit()时,唤醒父进程,将exit()返回值作为父进程中wait的返回值。
- 有僵尸子进程等待时,wait()立即返回其中一个值。
- 无子进程存活时,wait()立刻返回。
-
进程终止:进程结束执行时调用exit(),完成进程资源回收。exit系统调用的行为:
- 将调用参数作为进程的“结果”
- 关闭所有打开的文件等占用资源
- 释放内存
- 释放大部分进程相关的内核数据结构
- 检查是否父进程是存活着的。如存活,保留结果的值直到父进程需要它,进入僵尸(zombie/defunct)状态;如果没有,它释放所有的数据结构,进程结束。
- 清理所有等待的僵尸进程
-
其他进程控制系统调用
- nice()指定进程的初始优先级
- Unix系统中进程优先级会随执行时间而衰减
- ptrace()允许一个进程控制另一个进程的执行,设置断点和查看寄存器等
- sleep()可以让进程在定时器的等待队列中等待指定
第十三讲 实验四 内核线程管理
- 对x86系统而言,进程/线程上下文就是CPU内部的一堆寄存器的信息。
struct context {
uint32_t eip;
uint32_t esp;
uint32_t ebx;
uint32_t ecx;
uint32_t edx;
uint32_t esi;
uint32_t edi;
uint32_t ebp;
};
- trap_frame:用于保存前一个被(中断或异常)打断的进程的状态信息。
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
第十四讲 实验五 用户进程管理
-
ELF文件加载到内存:加载各个section、分配BSS段、分配堆栈空间。
-
如何理解ucore_os_docs lab 5的这句话?有了文件系统后会怎么加载?直接读取obj/__user_exit.out文件,然后写到指定内存空间吗?
“而到了与文件系统相关的实验后,ucore会提供一个简单的文件系统,那时所有的用户程序就都不再用这种方法进行加载了,而可以用大家熟悉的文件方式进行加载了。”
-
ucore把用户进程的虚拟地址空间分了两块,一块与内核线程一样,是所有用户进程都共
享的内核虚拟地址空间,映射到同样的物理内存空间中,这样在物理内存中只需放置一份内
核代码,使得用户进程从用户态进入核心态时,内核代码可以统一应对不同的内核程序;另
外一块是用户虚拟地址空间,虽然虚拟地址范围一样,但映射到不同且没有交集的物理内存空间中。这样当ucore把用户进程的执行代码(即应用程序的执行代码) 和数据(即应用程序的全局变量等) 放到用户虚拟地址空间中时,确保了各个进程不会“非法”访问到其他进程的物理内存空间。 -
当进程执行完它的工作后,就需要执行退出操作,释放进程占用的资源。ucore分了两步来完成这个工作,首先由进程本身完成大部分资源的占用内存回收工作,然后由此进程的父进程完成剩余资源占用内存的回收工作。为何不让进程本身完成所有的资源回收工作呢?这是因为进程要执行回收操作,就表明此进程还存在,还在执行指令,这就需要内核栈的空间不能释放,且表示进程存在的进程控制块不能释放。所以需要父进程来帮忙释放子进程无法完成的这两个资源回收工作。
-
“硬”构造出第一个用户进程:建立用户代码/数据段 -> 创建内核线程 -> 创建用户进程“壳” -> 填写用户进程“肉” -> 执行用户进程 -> 完成系统调用 -> 结束用户进程