操作系统复习-进程与线程
进程
写在前面
本系列是操作系统期末复习笔记,主要参考资料是王道《2019年操作系统考研复习指导》与《操作系统-精髓与设计原理:第七版》。
本文是本系列的第一篇,介绍进程与线程,是操作系统中的重中之中。对应《操作系统精髓》的第三章与第四章。
进程的定义
Q1:进程和程序是两个概念:程序是代码+数据,进程是代码+数据+堆栈+PCB;程序是永存、静态的,进程是暂时的、动态的。一个程序可对应多个进程,一个进程可以执行一个或几个程序。
进程的状态转换
进程状态 |
状态描述 |
运行态 |
进程正在处理机上运行,单处理器情况下一次最多一个进程处于这个状态 |
就绪态 |
进程处于准备运行的状态,获得了除处理机之外的资源 |
阻塞/等待态 |
等待事件如I/O操作而暂停运行,即使处理器空闲,该资源也不能运行 |
新建态 |
进程控制块已经创建但是还没加载到内存中的新进程 创建:申请PCB->填写PCB信息->分配资源->转入就绪状态 |
退出态 |
进程正常结束或是中断运行,设置为退出态后,进一步的处理资源释放和回收工作 |
挂起态 |
当内存中所有进程都阻塞的时候,操作系统将进程挂起,转移到磁盘中(调入其他进程) |
阻塞/挂起态 |
进程在外存中并等待一个事件 |
就绪/挂起态 |
进程在外存中,但是只要被载入内存就能执行 |
N1:区分就绪状态和阻塞态的原因主要在于把处理机资源从其他资源中独立出来,是因为在分时系统的时间片轮转机制中,每个时间片只有若干ms,进程的切换是十分频繁的且时间短,其他资源的使用分配对应的时间比较长而且频率较低,因此区分成生命周期的两个状态,能够方便进行管理。
N2: 阻塞和就绪态都能直接到退出状态,由于父进程终止的时候,子进程也会被终止。
Q2 阻塞态为什么不能进入运行态?就绪态为什么不能直接进入阻塞态?
N3: 阻塞->就绪:当进程等待的事件到来的时候,中断程序必须把相应程序从阻塞状态转换为就绪状态。
Q3:为什么需要挂起态?为什么需要两个挂起态?在一个没有虚拟内存的系统中,每个被执行的进程都必须完全载入内存。但是由于处理器处理速度比I/O快,很有可能出现所有进程都在等待I/O的情况。因此,即使是多道程序设计,处理器仍然有可能处于空闲状态。解决方法一种是扩充内容,但成本高昂。另一种合理的方法是进行交换:把内存中某个进程的一部分或全部转移到磁盘中。操作系统把阻塞的进程换出到磁盘中的挂起队列,之后取出挂起队列中的另一个进程或者接受一个新进程的请求,将其纳入内存运行。(交换是一个I/O操作,因此也可能使问题恶化,但是由于磁盘I./O一般是系统中最快的IO,因此往往能提高性能)。
当操作系统执行换出操作时候,有两种换进内存的选择:第一种是接入新创建的进程,第二种是调入一个以前挂起的进程。通常调入之前的进程,而不去增加系统的负载总数。但是以前挂起的进程有可能仍处于阻塞,再次调入就毫无意义,所以需要设计阻塞和就绪挂起来调度进程。
如果存在虚拟内存,可能会执行到只有一部分内容在内存中的进程,如果访问的进程地址不在内存中,则进程的相应部分被调入内存中,可以消除显式交换的需要。但是有可能导致虚拟内存崩溃,所以仍需要显式的交换。
运行->就绪/挂起态:超时之后转换到就绪态,如果如果这个进程被优先级高的抢占之后,就可以直接转换到就绪挂起队列中,并释放一些内存空间。
各种状态->退出:完成、错误、父进程终止
N4:挂起的原因:交换、os原因:挂起后台进程或工具程序进程或是怀疑有问题的进程、交互式用户请求、定时:周期性的进程,在两次使用之间应该被换出、父进程请求:检查或修改挂起的进程,协调不同后代进程的行为
进程的控制
N5:进程切换与处理器模式切换有何不同?模式切换时,处理器逻辑上可能还处在同一进程中。如果进程因中断或异常进入到核心态运行,执行完后又回到用户态,那么操作系统只需要恢复进程进入内核时所保存的CPU现场,无需改变当前进程的环境信息。但若要切换进程,则进程的环境信息也需要改变。
N6:调度和切换的区别?调度是指决定资源分配给哪个进程的行为,是一种决策行为;切换是指实际分配的行为,是执行行为。也就是说,先有资源的调度,然后才有进程的切换。
进程的组织
进程的通信
进程通信是指进程之间的信息交换,PV操作是低级通信方式,高级通信方式以较高的效率传输大量的数据,主要有以下三类:
线程的概念
Q4:为什么要引入线程?引入进程的目的是为了更好地使多道程序并发执行,以提高资源利用量和系统吞吐量,增加并发程度,(满足功能需求)。引入线程,则是为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。引入线程后,进程的内涵发生改变,只作为除CPU以外系统资源的分配单元,线程则作为处理器的分配单元。有了线程之后,线程切换时,有可能候会发生进程切换,也有可能不发生进程切换,平均下来,每次切换所需要的开销就小了,让更多的线程参与并发,也不会影响到响应时间的问题,提高系统并发性。
N7:线程的用户级/内核级线程示意图
用户级线程相对内核级的优点:(1):所有线程管理数据结构都在一个进程的用户地址空间中,线程切换不需要内核态特权,进程不需要为了线程管理而切换到内核态,节省了两次状态转换的开销;(2):调度可以是一个用程序相关的,为应用程序量身定做调度算法而不扰乱底层的调度程序;(3):用户级线程可以在任何操作系统中运行,不需要对底层内核进行修改。线程库是一组供所有应用程序共享的应用程序级别的函数。
用户级线程相对于内核级的缺点:(1):在典型的操作系统中,许多系统调用都会引起阻塞。当一个线程阻塞,整个进程阻塞(2):一个多线程应用程序不能利用多处理技术。
栈和堆的区别
(1)程序内存布局场景下,堆与栈表示的是两种内存管理方式;
(2)数据结构场景下,堆与栈表示两种常用的数据结构。
(1)管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
(2)空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小64bits的Windows默认1MB,64bits的Linux默认10MB;
(3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
(4)分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
(5)分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
(6)存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。
无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。