我从哪儿来?先问一个原始问题,程序是如何运行的?
先唠叨一下程序和进程的区别:
程序编译成机器代码
程序运行
由图可知程序会先由编译器编译成机器指令,运行之前先把程序放入内存,在内存中创建一个进程实体。一个进程实体(进程映像)由PCB、程序段、数据段组成。然后CPU从内存中取出指令,来运行程序。
进程实体的组成进程实体的组成
同时挂三个QQ号,会对应三个QQ 进程,它们的PCB、数据段各不相 同,但程序段的内容都是相同的 (都是运行着相同的QQ程序)
PCB 是给操作系统用的; 程序段、数据段是给进程自己用的。
引入进程实体的概念后,可把进程定义为: 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位
进程的组织Linux进程使用 struct task_struct 来定义管理进程,源码字段如下:
structtask_struct{
/*
*offsetsofthesearehardcodedelsewhere-touchwithcare
*/
volatilelongstate;/*-1unrunnable,0runnable,>0stopped*/
unsignedlongflags;/*perprocessflags,definedbelow*/
intsigpending;
mm_segment_taddr_limit;/*threadaddressspace:
0-0xBFFFFFFFforuser-thead
0-0xFFFFFFFFforkernel-thread
*/
structexec_domain*exec_domain;
volatilelongneed_resched;
unsignedlongptrace;
intlock_depth;/*Lockdepth*/
/*
*offset32beginshereon32-bitplatforms.Wekeep
*allfieldsinasinglecachelinethatareneededfor
*thegoodness()loopinschedule().
*/
longcounter;
longnice;
unsignedlongpolicy;
structmm_struct*mm;
intprocessor;
...
}
进程组织
Linux 把所有的进程使用双向链表连接起来。
进程的状态与转换进程的状态
进程状态的转换
进程之间的转换, 就绪运行和阻塞三种状态之间的转换,大部分人总是记不住,其实很简答,死背硬记肯定是不行的,只需要记住下面几条,这个转换关系自己就能在脑海中画出来。
注意点:
就绪态、阻塞态、运行态本质区别:
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。
如何实现进程的控制?答:用“原语实现”。
进程控制相关的原语
计算机系统的层次结构
原语的执行具有“原子性”,一气呵成。那么,为何进程控制(状态转换)的过程要“一气呵成”?
举个栗子:假设PCB中的变量 state 表示进程当前所处状态,1表示就绪态,2表示阻塞态...
就绪阻塞队列
假设此时进程2等待的事件发生了,则操作系统中,负责进程控制的内核程序至少需要做这样两件事:
完成了第一步后收到中断信号,那么PCB 2 的 state=1,但是它却被放在阻塞队列里,主要原因就是第一,第二步操作不是一个原子操作。
那么,这个原语厉害呀,lua脚本也可以实现这种原子操作redis,那么这个原语的原子性是怎么实现的呢?
原语的实现?中断机制中断机制
再说这个之前,我们有必要先了解一下中断机制。
关中断和开中断其实就是像我们生活中的开关一样。关中断是为了保护一些不能中途停止执行的程序而设计的,计算机的CPU进行的是时分复用,即每个时钟周期内,CPU只能执行一条指令。
在多道程序设计的环境下(就是我们通常所说的多个程序同时运行时),CPU是不断地交替地将这些程序的指令一条一条的分别执行,这样从宏观上看我们就感觉多个程序是在同时执行,但从微观上看则是CPU在不同的时间段(极短)内执行着不同程序的单条指令。
而CPU在这些指令之间的切换就是通过中断来实现的。关中断就是为了让CPU在一段时间内执行同一程序的多条指令而设计的,比如在出现了非常事件后又恢复正常时,CPU就会忙于恢复非常事件出现之前计算机的工作环境(通常叫做恢复现场),在恢复现场的时候,CPU是不允许被其他的程序打扰的,此时就要启动关中断,不再相应其他的请求。当现场恢复完毕后,CPU就启动开中断,其他等待着的程序的指令就开始被CPU执行,计算机恢复正常。
中断的分类这里就不列举了,感兴趣的宝宝可以自己去搜索一下。
原语实现
可以用 “关中断指令”和“开中断指令”这两个特权指令实现原子性。
正常情况:CPU每执行完一条指令都会例行检查是否有中断信号需要处理,如果有,则暂停运行当前这段程序,转而执行相应的中断处理程序。CPU执行了关中断指令之后,就不再例行 检查中断信号,直到执行开中断指令之后 才会恢复检查。
原语实现
以下四张图是进程创建,终止,阻塞和唤醒,切换时候的原语操作
进程的创建
进程的终止
进程的阻塞和唤醒
进程的切换
无论哪个进程控制原语,要做的无非三类事情:
进程通信
进程通信分为共享存储,消息传递,管道通信。
顾名思义,进程通信就是指进程之间的信息交换。 进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。
共享内存内存地址空间独立
为了保证安全,一个进程不能直接访问另一个进程的地址空间。但是进程之间的信息交换又是必须实现的。那么该怎么共享呢?如果看过我前几期分享内存管理和文件系统的,应该会有思路。
开辟共享内存
注意点:
共享存储分为两种方式:
管道通信
“管道”是指用于连接读写进程的一个共享文件,又名pipe 文件。其实就是在内存中开辟 一个大小固定的缓冲区。
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。
消息包括消息头和消息体,消息头包括:发送进程ID、接受进程ID、消息类型、消息长 度等格式化的信息(计算机网络中发送的“报文”其实就是一种格式化的消息)。
消息直接挂到接收进程的消息缓冲队列上, 消息要先发送到中间实体(信箱)中,因此也称“信箱通信方式”。Eg:计网中的电子邮件系统。
消息传递
小结
进程通信
线程
线程属性
线程的属性又不了解的可以看看上面的图。
线程
三种线程模型每一种线程模型的实现我们都围绕四个话题展开:
历史背景:早期的操作系统(如:早期UNIX)只支持进程,不支持线程。当时的“线程”是由线程库实现的。
早期线程实现方式
多对一模型:多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。
线程模拟实现代码如下:
模拟线程实现
从代码的角度看,线程其实就是一段代码逻辑。 上述三段代码逻辑上可以看作三个“线程”。 while 循环就是一个最弱智的“线程库”,线程库完成了对线程的管理工作(如调度)。
很多编程语言提供了强大的线程库,可以实现应用线程的创建、销毁、调度等功能。
多对一模型特点:
注意:操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位。
一对一模型大多数现代操作系统都实现了内核级线程,如 应用 Windows、Linux。
一对一模型
一对一模型: 一个用户级线程映射到一个内核态核级线程。每个用户进程有与用户级线程同数量的内核级线程。
一对一模型特点:
值得注意的是,一对一模型由于每创建一个用户线程就要创建一个相应的内核线程,由于创建内核线程的开销会影响应用程序的性能,所以这种模型的大多数实现限制了系统支持的线程数量。Linux,还有 Windows 操作系统的家族,都实现了一对一模型。
Java使用的就是一对一线程模型,它的一个线程对应于一个内核线程,调度完全交给操作系统来处理,所以切换线程的代价很大,线程数调参是Java工程里面重要的一个环节。
多对多模型多对多模型
多对多模型:n个用户级线程映射到m个内核级线程(n >= m)。每个用户进程对应 m 个内核级线程。
内核级线程才是处理机分配的单位。例如:多核CPU环境下,左边这个进程最多能被分配两个核。
多对多模型特点:
这个是GO语言这些年这么火热的基础,Go语言中的协程goroutine调度器就是采用的这种实现方案,在Go语言中一个进程可以启动成千上万个goroutine,goroutine非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。
golang的GMP调度器模型
小结
线程小结
了解这些线程模型,对每种语言的机制,以及对那些高并发机制,优缺点,其实就会有更加深刻的理解。
进程调度调度这个地方由于和开发比较远,不需要太深入,我这边浅浅的讲一下调度。
当有一堆任务要处理,但由于资源有限,这些事情没法同时处理。这就需要确定某种规则来决定处理这些任务的顺序,这就是“调度”研究的问题。
进程调度
先关注三个问题, 进程调度的时机,进程的切换与过程,进程调度方式。
进程调度的时机进程调度(低级调度),就是按照某种算法从就绪队列中选择一个进程为其分配处理机。
需要进行进程调度和切换的情况
不能进行进程调度与切换的情况
这个地方,不能调度大多数是不能被外力打断的情况下,需要原子操作,但是进程在普通临界区中是可以进行调度、切换的。
解释一下两个名词:
临界资源:一个时间段内只允许一个进程使用的资源。各进程需要互斥地访问临界资源。
临界区:访问临界资源的那段代码。
进程的切换与过程广义的进程调度包含了选择一个进程和进程切换两个步骤。 进程切换的过程主要完成了:
非剥夺调度方式,又称非抢占方式。即,只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态。
实现简单,系统开销小但是无法及时处理紧急任务,适合于早期的批处理系统.
剥夺调度方式,又称抢占方式。当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程。
可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中 断)。适合于分时操作系统、实时操作系统.
进程调度
调度算法的评价指标
进程调度算法评价指标
很多指标看名称就能知道,这个地方我重点说一下CPU利用率和系统吞吐量,这两个指标也是现在很多架构并发,或者 java 垃圾回收器主要考虑的两个指标。
「CPU利用率」
CPU利用率: 指CPU “忙碌”的时间占总时间的比例。
利用率 = 忙碌的时间 / 总时间
Eg:某计算机只支持单道程序,某个作业刚开始需要在CPU上运行5秒, 再用打印机打印输出5秒,之后再执行5秒,才能结束。在此过程中, CPU利用率=(5 5)/(5 5 5)
「系统吞吐量」
对于计算机来说,希望能用尽可能少的时间处理完尽可能多的作业。系统吞吐量:单位时间内完成作业的数量
系统吞吐量= 总共完成了多少道作业 / 总共花了多少时间
Eg:某计算机系统处理完10道作业,共花费100秒,则系统吞吐量为? 10/100 = 0.1 道/秒.
没有最好的调度算法,只有最合适的算法,实际应用中取什么算法,主要根据自身场景是更加追求响应时间,还是系统吞吐量。
例如:垃圾回收器中,CMS是响应时间有优先,以获取最小停顿时间为目的,为了减少STW,牺牲了一定的吞吐量。在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景;UseParalleGC UseParalleoldGC 垃圾回收器是吞吐量优先,但是需要长时间的STW。
调度算法Tips:各种调度算法的学习思路
「适合早起批处理系统」
进程调度算法
这几种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。因此这三种算法一般适合用于早期的批处理系统,当然,FCFS算法也常结合其他的算法使用,在现在也扮演着很重要的角色。
「适合交互式系统」
进程调度算法
比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括 分时操作系统、实时操作系统等)更注重系统的响应时间、公平性、平衡性等指标。而这几种算法恰好也能较好地满足交互式系统的需求。因此这三种算法适合用于交互式系统。(比如UNIX使用的就是多级反馈队列调度算法)
具体的算法这里不展开,如果后期宝宝们反馈多的话再考虑展开。
这个是上篇,下一篇着重讲进程同步互斥,下一篇的目录预告如下:
进程同步互斥
有收获的欢迎点赞,分享,在看,喜欢的话可以关注我公主号,文章首发均在这~~~
❝
往期推荐:
1.操作系统之内存管理,高能预警!!!
2.操作系统之文件管理,一切皆文件!!!
3.一文弄懂什么是DevOps,妈妈语气讲解❞
文章参考:王道老师操作系统
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved