操作系统笔记三
进程
把一个静态程序通过OS在内存中让cpu执行起来的动态执行过程叫进程
写代码都是用户态,而进程在执行过程中需要完成特定的功能,这些功能呢只有操作系统能提供,比如说读写文件,读写文件的过程是与硬盘打交道,这个过程需要操作系统来完成,所以说进程只需要给操作系统发出请求,操作系统代表进程在内核中执行,那么这个时候进程处于核心态
特点
动态性:动态创建,结束进程
并发性:可以被独立调度并占用cpu运行。可能一段时间有多个进程在执行,给人一种并行的感觉(一个时刻多个进程,只有多核cpu能实现),实际为并发
独立性:不同进程的工作不相互影响。通过页表使不同程序访问不同地址空间,越过地址空间会缺页,产生异常。OS给不同进行分配不同的页表,让其各自在独立的空间中运行
制约性:不同进程间可能有交互,或者一个进程需等待另一进程执行到一定阶段才能继续执行。因访问共享数据/资源或进程间同步而产生制约
程序 = 算法 + 数据结构
描述进程的数据结构:进程控制块PCB
OS为每个进程维护一个PCB,保存与进程有关的各种状态信息
PCB
OS管理进程运行所用的信息集合。OS用PCB描述进程基本情况及运行变化过程。PCB是进程存在的唯一标志
进程创建:生成一个PCB
进程终止:回收它的PCB
根据进程的个数是否固定会否频繁插入删除选择不同类型
进程状态
生命周期管理 :
创建:3种事件:1.系统初始化时。2.用户请求创建一个新进程。3.正在运行的进程执行了创建进程的系统调用
运行:内核选择一个就绪进程,占用cpu并执行
等待/阻塞:3种情况:1.请求并等待系统服务,无法马上完成。2.启动某种操作,无法马上完成。3.需要的数据没有到达。
进程只能阻塞自己,因为只有进程自身知道何时需要等待某种事件发生
唤醒:原因:1.被阻塞进程所需的资源可被满足。2.等待的事件到达。3.将该进程的PCB插入就绪队列。
进程只能被别的进程或OS唤醒
结束:1.正常退出,自愿。2.错误退出,自愿。3.致命错误,强制。4.被其他进程所杀,强制
三种基本状态:
运行状态:正在cpu上运行
就绪状态:获得除cpu外的一切所需资源,一旦得到cpu即可运行
等待状态:等待某一事件而暂停运行。如等待资源,等待输入/输出完成
Null -->New:产生新进程来执行一个程序
New -->Ready:进程创建完成并初始化后,一切准备就绪。不会很久
Ready -->Running:进程被OS调度,分配到cpu上运行
Running -->Exit:进程完成或因出错,由OS作结束处理
Running -->Ready:由于分配它的cpu时间片用完而让出cpu。由OS来完成
Running -->Blocked: 请求某样东西且必须等待
Blocked -->Ready:进程等待的事件到达
进程挂起
挂起:意味着进程没有占有内存空间,处在挂起状态的进程映像在磁盘上
OS看到的是一系列进程,需要选择哪个进程去执行,以及哪个进程要切换状态
线程
读硬盘的速度远低于cpu运行速度,可能要等半天才能解压
因此需要一种新的实体,实体间可以并发执行,共享相同的地址空间(与进程不同,进程间地址空间是独立的)。这就是线程
线程分为两部分:一部分是资源管理,包含地址空间,打开的文件、访问的网络等等。一部分是执行功能,现在这个执行状态交由线程管理
代码在进程这个资源管理平台上的一条执行的流程,认为是线程
线程控制块TCB:只负责管理跟执行流程相关的一系例信息。包含程序计数器PC,堆栈SP,寄存器信息(有不同的执行流,控制流,需要通过一系列的寄存器表示控制流的执行状态)。它的内存空间(堆空间),数据空间,代码段是所有线程所共享的
在高频计算里面很强调性能,而且它所执行的代码相对统一,不会出现一个代码出现不同的控制流程,强调性能的需要使用线程来完成。
另外一方面,比如浏览器,打开一个网页就可以用线程的方式来实现,打开不同的网页都很快,如果用线程的机制,可能有的网页有恶意代码或者有bug导致这个线程崩溃了,就有可能引起整个进程的崩溃,你可能打开很多网页,这些网页都崩溃。这也是早期浏览器很多采用线程实现,现在的浏览器都采用进程来实现。这个适合进程
线程是具有同一个地址空间的,属于同一个进程的线程拥有同一个页表,切换的时候不要切换页表。
而对于进程而言,我们要切换进程的话,需要把页表也切换掉,而这个切换页表的开销是比较大的,因为这里面涉及到它们访问的地址空间不一样,它里面的很多cache信息,很多TLB信息,硬件的信息,都会失效,需要重新加载。那么这个开销比较大。
线程切换的时候,由于共享页表,重用,不用做失效处理,效率会很高
线程的实现
3种实现方式:
用户线程:在用户空间实现。OS看不到的线程,由专门的用户线程库来进行管理
内核线程:在内核中实现。由OS管理起来的线程
轻量级进程:在内核中实现,支持用户线程
用户线程与内核线程的对应关系
多对一:多个用户线程对应一个内核线程
一对一:一个用户线程对应到一个内核线程
多对多:n个用户线程对应到m个内核线程
线程控制块TCB在库里面实现,OS看不到TCB,只能看到整个进程的信息。进程里的线程信息是由管理库来实现
在用户空间实现线程机制,不依赖于OS的内核,由一组用户级的线程库函数来完成线程的管理。包括线程的创建、终止、同步和调度等
用户线程的维护由相应进程完成(通过线程库函数),不需要OS内核了解用户线程存在,可用于不支持线程技术的多进程操作系统
每个进程都需要自己私有的TCB列表,用来跟踪记录各个线程的状态信息(PC,栈指针,寄存器)。
用户线程切换也由线程库函数完成,无须用户态/核心态切换,所以速度很快
运行每个进程有自定义的线程调度算法
缺点:
如果一个线程发起系统调用而阻塞,整个进程将等待。其他线程也会被等待
当一个线程开始运行后,除非主动交出cpu使用权,否则其进程里的其他线程将无法运行。用户态线程库没法主动打断当前用户线程的执行。OS可以,OS会管理中断,一旦产生中断,整个cpu控制权会回到OS手里,OS对中断可以进行进一步处理。特别是时钟中断。
时间片是分配给进程,因此与其他进程相比,多线程执行时,每个线程得到的时间片会更少一些,执行会较慢
所有属于一个进程的线程都被对应PCB统一管理,具体调度时由TCB完成。
只要完成一次切换,就会有一次用户态/内核态的变换。开销更大
上下文切换
各个进程共享cpu资源,不同时候需要切换不同进程占用cpu执行。切换过程称为上下文切换。
进程上下文切换,切换的是进程所用到的寄存器,寄存器是和CPU紧密相连的,意味着在操作系统里面实现进程切换的话,要关注操作系统所载的CPU,它到底具有哪些寄存器,以及这些寄存器哪些是被进程所使用的
进程运行时需要的寄存器:
程序寄存器,它指出了进程执行到什么地方
栈指针也是寄存器,可以知道调用关系以及相应的变量位于什么位置。
等等
这些信息在做进程切换的时候,需要把这些信息保存到进程控制块中的某个位置,保存之后呢,当运行另外一个进程的时候,需要把这个寄存器从那个进程控制块中取出来,甚至context这么上下文取出来,然后恢复到CPU里面去,相当于把寄存器的资源重新赋好,这使得我们接下来这个进程可以继续在CPU上执行
进程必须在切换之前存储许多部分的进程上下文,并且能够在之后恢复他们,同时速度要快
OS将PCB放在不同的队列里:就绪队列,等待I/O队列(每个设备的队列),僵尸队列
进程创建
unix是用fork/exec 这两个系统调用来完成新进程的创建
所执行的程序是一样的,但是变量有一个地方不一样就是id
父进程是原来执行的id,子进程是新分配的id
完成复制之后,由exec加载一个新的程序加载到内存当中
fork完之后,当前系统就有两个进程,并且这两个进程的当前指令都指向fork完之后的那一行,父进程和子进程执行fork的返回值不一样,子进程返回0,父进程中返回值是子进程pid则跳过括号中的代码
除了返回pid不一样,其他完全一样,根据返回值不同执行不同进程
进程加载/执行
printf(“Why would I execute?”),放在这是做一个提醒作用, 如果执行了exec这个系统调用之后,当前这个子进程的地址空间里面代码段已经被覆盖了,也意味接下来这条语句(printf(“Why would I execute?”))代码的内容已经被新的程序程序覆盖了,这句话根本执行不到如果执行到,肯定是出了什么问题,比如exec失败了
最后有一个wait,后面会讲到,等待子进程结束,因为这个pid,大于0,其实就是代表子进程的pid号,假设wait返回了,也就意味着子进程结束了
执行fork之后,代码和数据都复制了一份
ork是要把父进程的地址空间完全复制一份到子进程中来,这里有一个内存的大量拷贝,需要把代码段数据段全都拷贝到另一进程地址空间中去,但是接下来执行exec的时候,刚才所做的拷贝全都没有用,因为要去还在新的程序,要把代码段,数据段给重新覆盖掉,所以前面做的fork的拷贝是多余的
优化:
1.vfork,早期unix系统提供的一种手段,做fork的时候只是复制y一小部分,j绝大部分都没有复制,因为它知道调完vfork之后,我们的程序会紧接着调用exec,使得效率高。但是这个增加了编程人员的开销,增加了一个fork和vfork
2.编程人员还是只有fork一个系统调用就行了,而不需要考虑什么时候后面程序是执行exec还是不执行exec的系统调用好。操作系统里面,各个子系统之间是相互支持相互帮助的,我们通过虚存管理,就可以实现一个高效的fork实现机制,这里面有个技术叫copy on write (COW). 就是写的时候再进行复制。当父进程创建子进程的时候,如果是采用COW技术,我们在做实际的子进程地址空间复制的时候,并没有像之前一样把整个地址空间真实的复制,而只是复制,父进程地址空间所需要的元数据,这些元数据其实就是页表,他们指向同一块内存空间,当父进程或者子进程对某一个地址单元进行写操作的时候,会触发一个异常,使得无论是父进程还是子进程,要完成一个操作,就是把触发异常的这个页给他复制成两份,使得我们父进程和子进程分别有不同的地址。通过这种方式,可以实现按需的一种情况。按需写的一种情况复制,如果你全部做只读,我确实没必要复制。只有当写的时候,这个父进程和子进程有不同的页表
进程等等/终止
父进程就可以帮助子进程完成最后一步,把它在内存中的资源释放掉,最主要的就是PCB.
调度
在某个时刻决定选择那个就绪进程去占用CPU执行。
目标:使得系统效率更高
什么时候调度:从一个状态到另一个状态变化的时候触发一次调度。因为这个都是否考虑让新的进程执行,或者让当前进程从CPU上撤下来,换一个进程去执行,这实际上就是调度时机,或者称为调度点
用户态抢占和非抢占
非抢占式:进程启动后,别的进程不能打断,导致别的进程必须等待这个进程执行完毕才去执行。效率不高
抢占式:用户进程在它的执行过程中,它感知不到,操作系统会决定,在某个时刻打断当前用户进程的执行,让操作系统去选择另外一个进程执行(操作系统认为更值得去执行的进程)称之为抢占式调用。现在操作系统常用调度策略
内核里面也存在是否可以抢占,
内核态非抢占:当一个用户进程执行系统调用,如果这个系统调用在内核中不会导致这个进程处于等待状态,它还是执行状态,这个时候,当这个系统调用返回的时候,一定会返回到发起这个系统调用的进程中去继续执行,意味着内核中不会出现抢占现象,不会在内核中切换,切换到另一个线程去执行,只要你这个进程在内核中执行,这个流程中它自身不会出现从运行态到阻塞态的变化,就确保回去的时候一定是,发起请求的进程继续执行。
内核态抢占:如果内核中也允许去做抢占,就是说有可能正在执行的系统调用,在内核里面执行的系统调用,某一个进程发起的,结果由于特殊事件产生,需要把当前内核完成一次切换,切到另一个优先级更高的进程去执行,一旦之前那个系统调用返回的时候,在内核那一点,是从内核态返回用户态的时候,可能变成另一个进程去,这种情况称之为抢占式的内核
调度原则
调度算法
来了新的执行时间更短的进程
1 新进程继续放到就绪队列里面去,排在就绪队列最前面,但是它不会打断当前正在运行的进程Pw,这种非抢占方式。
2 抢占式的,就是说,当Pw在执行的时候,比如执行完一个时间片,还剩余执行事件8(之前是9),这是来了一个新进程,执行时间是5,比较下正在执行的剩余时间和这个新的进程执行时间,新进程执行时间更小,它剩余的时间只有5,虽然它没有被执行,这个时候就要完成一次抢占,当前正在运行的进程从运行态-》就绪态,重新放回就绪队列里面去,让新进程占用CPU继续执行
p4p5各少了一个p3的执行时间,但是p3多了p4和p5的执行时间
根据过去执行时间预估未来执行时间
轮循调度算法
设置时间片20,轮流分享时间片,每20的时候让出cpu
q是时间片
多级反馈队列
公平共享调度
实时调度
实时调度首先面向的系统不一样。就是 real time system 实时系统
实时系统更多的是用在工业控制,如火车或者一些机床,或者一些嵌入式工厂里面,控制环境里面需要确保某一些任务需要能够在规定时间之内必须完成,这个规定时间就是real time一个体现,要在将来某段时间内必须完成某件事情,这个时间是确定的
静态:在任务执行前确定优先级
动态:任务随着执行过程中优先级会有变化
多处理器调度
1 放到哪个CPU执行,2确保系统是一个负载平衡的状态