漫谈计算机操作系统.docx
《漫谈计算机操作系统.docx》由会员分享,可在线阅读,更多相关《漫谈计算机操作系统.docx(23页珍藏版)》请在冰豆网上搜索。
![漫谈计算机操作系统.docx](https://file1.bdocx.com/fileroot1/2023-1/1/ce3fbc25-a224-4294-9942-9530ed1a705c/ce3fbc25-a224-4294-9942-9530ed1a705c1.gif)
漫谈计算机操作系统
操作系统基础知识一:
进程调度(上)
进程是个什么玩意
双击一个exe文件,就启动了一个进程。
成天说进程进程,这进程到底是个啥?
按照书上说的,进程就是正在执行的程序。
听了还是有些糊涂。
让我们先从cpu说起。
CPU
Cpu,自始至终在干一件事,一件什么事呢?
那就是:
从内存中取指令,然后执行,接着,取下一条指令,执行,再取..,永不停息!
这个设计思想,是著名的数学家,物理学家,经济学家冯·诺依曼提出的。
冯·诺依曼
指令指针寄存器
接下来有一个问题:
cpu从内存哪里取指令
先说一个概念:
寄存器。
Cpu里有很多寄存器,寄存器是什么呢?
它是一个数字电路,里面可以存储信息,而cpu可以对寄存器中的数据进行加减乘除,比较等运算。
Cpu会把内存中的数据读取到寄存器中,然后进行运算,有可能还会把数据写回内存。
Cpu中(拿我们天天都要用到的x86cpu),有一个特殊的寄存器,叫IP,指令指针寄存器。
还有一个寄存器,叫CS,代码段寄存器。
CS会“决定”(如何决定,以后再说,实模式和保护模式下各不相同)一个基地址:
base。
Ip里面存放着一个确切的地址。
Cpu会从内存中的base+ip这个地址取指令(在打开了分页模式的情况下,这个还不是最终的物理地址。
什么叫分页模式,这个以后再说)。
很多情况下,CS“决定”的基地址就是0,也就是说,cpu会从IP指向的地址取指令。
在vc下调试,可以看到,IP寄存器总是指向下一条要执行的指令。
指令译码
一段程序,就分代码和数据,都在内存里放着,cpu根据代码的指示,来对数据进行操作。
它怎么区分代码和数据呢?
内存里面的东西,只有零和一,cpu也就认识这俩数,够笨的了。
他只认一个死理,那就是IP指向的地方,它就认为这里放着的是代码,它就会按照代码的指示来工作。
IP指向了一个地址,这个地址只有一个字节,但是,计算机指令都是精心设计的,cpu只要一看到这个开头的数字,它就知道这个指令是什么,有多长。
Cpu里面有个指令译码器。
它就会干这件事。
Cpu执行完了这条指令,IP就会自动增加一个长度,这个长度,就是刚执行完毕的指令的长度,这样就自动指向了下一条指令,cpu就会接着去取那条指令执行了。
也就是说,如果没有遇上跳转指令,CPU会把内存,从头跑到尾。
接下来还有一个问题,CPU如何取指令。
CPU通过主板上的总线和其他设备相连,内存里的数据,就会通过总线传输到CPU中,供CPU执行。
实际情况是,CPU连接到北桥芯片,再连接到内存。
CPU获取到了内存里的数据,就可以开始处理了。
cpu里有一个cache,就是缓存(缓存还分一级缓存二级缓存),它会一口气从内存里读一大段数据进来,放到缓存中。
然后每次从这个里取,这样就不用每次跑到内存了,效率会快很多。
但是一段程序会有很多跳转指令,很可能cpu发现下一条指令在cache里没有,没办法,cache没有命中,它只好重新回内存中取了。
进程的运行
好了,说了这么多,可以开始说进程是个什么玩意了。
一个exe程序中有很多信息,其中就有代码。
Exe在硬盘上放着。
运行这个exe时,操作系统会把exe中的代码加载到内存中,(具体加载到哪里,由操作系统决定)然后把ip指针,指向代码的开头,接下来,就像放鞭炮一样,这段代码就执行下去了。
这样,一个进程就开始运行了。
多进程
咱们的操作系统,可以同时运行好几个进程,聊QQ,迅雷下载,看网页,编程序。
但是,CPU只有一块(我们先讨论单核的情况),却有很多进程要执行。
操作系统如何做到这一点呢?
答案就是分时!
什么叫分时呢,就是操作系统会轮流执行这些进程,每个时刻,只有一个进程被执行。
每个进程只运行很短的时间,然后马上换另一个进程。
但是切换的速度非常快,给人感觉,这些进程都是同时在被运行的。
操作系统会把时间分成很多时间片(什么叫时间片,以后再说)。
给每个进程都分配了。
一个进程的时间片用完了,就会换下一个进程运行。
大伙轮流使用CPU。
就好比厕所就一个坑,大伙都想去,只好轮蹲。
就算这个厕所有俩坑,人还是多,还是轮流使用。
肯定不能给每人都挖个坑,一是成本太大,二是人是动态的,不可能知道要几个坑。
只能运行一个进程的操作系统也有,比如DOS。
现在还有谁用这个?
例如,一共有三个进程,操作系统分别把它们加载到内存中,分别对应了红绿蓝三个区域。
一开始,IP指针指向了绿色进程的开头,该进程得到执行。
见下图:
等绿色进程执行一会后,它的时间片用完了。
准备进程切换。
见下图:
操作系统把IP指针指向了红色进程的开头,红色进程得到执行。
见下图,注意,操作系统要“记住”,绿色进程运行到了哪里。
再次注意!
!
操作系统要能够“记住”!
怎么记住?
以后再说。
红色进程运行一会后,也会切换到蓝色进程;等蓝色进程运行一阵后,又会切换回绿色进程。
此时,IP指针指向了上次运行到的地址,绿色进程得到继续执行,这个“断茬”就接起来了。
绿色进程好像根本没有被打断一样。
它自己也不知道自己被操作系统打断了。
进程控制块
操作系统的一大重要任务就是,管理运行的进程。
为了管理这些进程,它在内存里给每个进程都分配了一个数据结构,专门用来保存用来管理这个进程的相关信息,名字好像叫这个,我记不清了。
有两点要注意。
1这个数据结构很大很复杂。
2这个进程只有操作系统能用,平时咱们的应用程序是用不了的。
这个东西也不是给应用程序用的。
当然,要是这个操作系统写的不好,有些病毒搞破坏,这就另说了。
这个数据结构中,有两个部分我们要留意。
1进程的状态。
一个整数,标记这个进程是什么状态(linux中就有,可执行态,睡眠态,僵尸态,停止态,什么叫可执行态,睡眠态,僵尸态,这些状态是用来干什么的,以后再说)
2上下文。
一个结构体,用来“记住”一个进程运行到哪里。
进程调度程序
一个进程的时间片用完了,就要切换进程,操作系统就会从所有的进程控制块中,选一个最应该执行的进程,然后把CPU的使用权交给它,这样这个进程就得以运行。
当这个进程的时间片用完了,操作系统就会再挑一个进程。
这么挑?
这就是进程调度算法要干的事情了。
进程状态和上下文是干什么用的
先说进程状态。
操作系统中运行着很多进程,但是并不是每个进程都在干活的,例如,咱们的Bam,给它发命令的时候,才需要它干活,没事的时候,就不该运行,不能让它白占宝贵的CPU资源。
一个进程为“可执行态”,表示的意思是,这个进程可以被操作系统执行(注意,这不是说这个进程正在被执行,而是“可以”,当然操作系统的进程调度算法,会保证公平)。
操作系统在进行进程调度的时候,只会挑选那些状态为可执行态的进程来执行。
也就是说,操作系统有一套标准来衡量哪个进程最应该被执行,但是他一看这个进程的状态为“睡眠态”,转身就走,压根就不考虑。
也就是说,很多进程,没事干的时候,它的状态就是睡眠态。
操作系统就不会去调度它。
注意,其实应该说是很多线程的状态。
我们这里不区分进程和线程,这些东西以后再说。
我们这篇文章属科普,很多东西只是说明基本原理,和实际的操作系统还有差异,以后再说。
注意我们的WaitFroSingleObject函数,还有SetEvent函数,ReleaseMutex函数。
还是以后再说。
睡眠态和可执行态的转换,后面会继续说明。
再说上下文。
操作系统应该能够“记住”一个进程运行到了哪里。
当一个进程需要切换的时候,操作系统就会把这个进程的“快照”保存在上下文中。
或者也可以理解为“存档”。
等下次又调度到这个进程的时候,又会“读档”。
举个例子,你在打游戏,玩了三个游戏。
当然,一次只能玩一个。
恶魔城玩腻了,就存档,然后换盘,最终幻想,这个也玩腻了,存档,换忍龙,玩一会腻了,再存档,这时候,又想玩恶魔城了。
这时候,读档!
就可以接着玩了。
(这仨游戏都不是一个平台上的,按说是不会出现这种情况的,凑合着理解吧。
)
操作系统运行三个进程,它每次切换进程的时候,也会存档,读档。
存档读档都有哪些信息
回到当初CPU的原理,CPU会不断把内存中的信息,读取到内部的寄存器中,然后进行处理,如有必要,会写回内存。
每个进程,在内存里都有自己的领地,一般,注意,是一般不会去占别人的地盘(什么时候会“不一般”,以后再说)。
换句话说,内存都是自己的(咱们那个共享内存是什么玩意,以后再说。
),公用的东西,就是CPU!
也就是说,我们存档的时候,必须把CPU的“快照”保存下来。
保存到内存里面。
等要恢复一个进程的时候,就会从内存中把CPU的“快照”取出来!
让CPU恢复。
CPU的快照是什么
就是CPU中应用程序会用到的所有的寄存器的值!
其中就包括IP寄存器的值。
中断
操作系统每隔一段时间,就会进行进程切换。
现在的问题就是,应用程序好端端的运行着,怎么就切换了。
这要说到CPU的一个特性,中断!
什么叫中断。
拿x86的CPU来说,它有倆引脚,一个叫不可屏蔽中断引脚,一个叫可屏蔽中断引脚。
咱们说可屏蔽中断引脚,每执行完一条指令,CPU就要去检查这个引脚有无信号(你可能会问,这不累吗?
这个问题就不要担心了,这是由硬件完成的,和咱们软件编码没有关系)
接下来问题是,什么时候中断会来。
电脑上有很多外设,例如鼠标,键盘,每按一个按键,就会给CPU发送一个键盘中断。
外设连接在中断控制器上,中断控制器又连接到cpu。
咱们要重点说的是:
时钟中断。
这个时钟,不是bios上那个用纽扣电池的时钟,而是一个时钟芯片。
这个时钟芯片,每隔一定时间,就会给CPU发送一个时钟中断。
接下来的问题是,中断来了,会怎么样。
CPU每收到一个中断,如果这个中断没有被屏蔽,那么,它会立刻扔下手头的工作,根据这个中断号(中断号咋来的?
由中断控制器发给CPU的),跳到对应的中断处理程序(中断处理程序是怎么来的?
以后再说)执行完了,再中断返回到之前被中断的地方,重新运行。
比如,时钟中断来了,操作系统就会进入时钟中断对应的程序,键盘中断来了,就去对应的键盘中断程序…
在实模式下(什么叫实模式,以后再说)就相当于进行了一次函数调用,而在保护模式下(什么叫保护模式,以后再说),就会进入一扇中断门(什么叫中断门,以后再说)然后再到相应的中断处理程序。
注意,虽然都干了一件和函数调用一样的事情,但是这两个有本质区别,后者进行了内核态堆栈和用户态堆栈的转换(什么叫内核态堆栈和用户态堆栈,以后再说),还进行了特权级变换(以后再说吧)。
进程切换,就是发生在时钟中断中的。
Linux早期时候,一秒一百次时钟中断,后来硬件好了,一秒一千次。
我们最终要使用的是应用程序,操作系统这些进程调度处理对我们来说,完全是无用的。
中断多了必然浪费,但是多了,系统“灵敏度”就提高了。
时间片
时钟中断每隔周期时间就会发生,每两个时钟中断中间就是一个时间片。
每来一个时钟中断,叫一个滴答。
进程调度算法
只举一个简单的算法。
进程控制块中还有一个成员很重要,是一个整数,就是操作系统给它分配的时间片,十个,几十个,都有可能,视不同进程而定。
每次来一个时钟中断,在时钟处理程序里,首先把当前正在运行的进程的时间片减一。
然后看是否到零了。
如果没有,说明这个进程“阳寿未尽”于是调度程序就退出了。
这个进程得以继续运行。
一旦发现这个进程时间片用完了。
那么立刻执行调度程序,挑另外一个进程运行。
把cpu的使用权交给它。
如何挑选呢?
遍历一遍所有的进程,看谁的剩余时间片最多,就把谁挑出来。
让它去运行。
这种思想是照顾最困难的那个。
注意,操作系统只会从状态为可执行态的进程中挑选,一旦发现某个进程的状态是睡眠态,立刻就无视。
就这样,当所有的进程的时间片都用完了,这时候所有人进行“转世投胎”,重新给他们分配时间片。
如何分配呢?
进程控制块里还有一个成员很重要,就是进程的特权级。
分配时间片的算法思想是,谁的级别高,就给谁分配更多的时间片。
也就是说,不保证级别高的进程比级别低的进程先运行,但是级别高的进程的运行时间一定比级别低的进程运行的时间长。
这种算法比较简单,不支持抢占(什么叫抢占,以后再说)。
进程状态的转换
以后再说吧。
小结
就说到这里吧,理论需要结合实践.。
下次咱们开始在虚拟机上实现最最简单的进程调度。
操作系统基础知识二:
进程调度(下)
进程状态的转换
从“锁”说起
锁是干嘛用的,进程同步要用的!
注意,在这篇文章中,我们不区分进程和线程的概念,但凡说到,可以认为这是一个概念。
等以后说到虚拟内存时,再进行区分
比如两个进程都会去访问一个变量,两个要是都在读,倒是好办了,君子动口不动手。
要是一个正在读,一个正在写,或者俩都想写,可能就出事了。
出啥事呢?
啥子事呢?
我们来看这个例子,算了,例子也甭举了,但凡知道多线程的,都该知道我想举的例子是个啥,随便翻看一本介绍多线程编程的书,都有介绍。
这个东西,地球人都知道的。
恕我在这里不啰嗦了。
比如说,当一个进程正在对一个全局数组写的时候,只写了一半,还有另一半要写,这时候,时钟中断发生了!
发生了!
而且很不巧,在中断处理程序中,进行了进程切换!
更不巧的是,新切换到的进程又去读了这个数组,那边还没写完呢,这边就直接用里面的数据了。
嗯,后面会怎么样呢?
谁知道!
反正凶多吉少了。
我们要知道,当时钟中断发生的时候,我们不可能知道我们的程序运行到了哪里。
顺便说一下,linux早些时候,硬件不行,一秒一百次时钟中断,后来条件好了,一秒一千次时钟中断。
所以,这里就需要一把“锁”了。
锁是个啥玩意?
没锁会咋样?
打个比方,厕所就一个坑,你蹲那里了,这时候又来了一个人,他也要,你正往里面写呢,他破门而入,一把把你领子一揪,拖出来换他了!
你能咽下这口气吗?
今天饿死事小,失节事大,非打起来不可。
写程序的时候,出现这种情况,很可能就出了些莫名其妙的错。
所以,我们就需要一把锁了。
打个比方吧,还是厕所,就一个坑,你蹲那里了,但是这次不同的是,这回你蹲的是五星级厕所,有锁了!
你进去,先把锁锁上,这时候又来了一个人,他进不来了!
不管你在里面多久,他都得等!
等你完了,你解锁,提裤子走人,然后他才能进去,他进去后,又会加锁,这样,别人就没法抢了。
或者也可以这样打比方。
开会,大家都想发言,但是一次只能有一个人发言,不然大家都说,啥也听不见了。
这时候就要有一个话筒,就一个。
谁想发言,就申请这个话筒,申请到了,就发言,发言结束,释放话筒。
而后面,等这个话筒的人,还有一大堆,他们又会去抢这个话筒。
当同时有多个人去抢话筒时,最后只能有一个人抢到。
没有话筒,就不能发言。
也就是说,我们需要这种机制,“锁”机制!
两个进程都会访问同一个对象,而对这个对象的访问过程中,很可能会出现问题,所以需要有一种机制来保证,从一个进程访问这个对象开始,到访问结束,其他任何进程,不能对这个对象进行访问。
再具体一些就是,有一个锁,任意一个进程在访问这个对象前,都要去申请这把锁,如果申请到了,就加锁,然后继续往下走,如果没有申请到,那么,就等,直到申请到这把锁的进程访问结束,释放这把锁,然后自己拿这把锁。
申请锁的时候,有三个步骤需要注意:
(1) 进程首先要检查这把锁的状态,是否能申请到
(2) 申请到了,就加锁,然后继续
(3) 申请不到,就等待,直到这把锁可用(也有可能只等一段时间)
释放锁的时候,比较简单,就是把锁的状态置为解锁的状态。
说的很罗嗦呀!
我们自己写一个锁
intlock=0;//定义一个全局变量,初始值为0,表示没有加锁
//需要加锁保护时候,就调用这个函数
voidlock()
{
Begin_lock:
if(lock==0) //如果没有加锁
lock=1; //加锁!
else //否则
gotoBegin_lock; //返回最开始,重新去检查锁是否可用,若果等不到//这把锁,这个函数就会一直循环,不会返回
}
voidunlock()
{
lock=0; //解锁
}
我们写了一把自旋锁。
(什么叫自旋锁,马上就说;什么叫互斥锁,以后说)
自旋锁,就是发现这个锁不可用的时候,马上回去死等!
这个进程会一直消耗cpu,它的时间片,会用得干干净净。
回想上面的三个步骤,我们的函数都做到了。
思考,我们写的这把锁,管用吗?
答案是,不管用!
还是那句话,当时钟中断发生的时候,我们不可能知道我们的程序运行到了哪里。
假设进程一调用lock函数,执行if(lock==0)时,发现了没有加锁,好,他继续,准备加锁,这时候IP寄存器指向了lock=1,准备加锁,但是此时这条语句还没有执行。
lock变量并没有变成1。
就在这时候,切换到了进程二。
进程二也调用了lock函数,注意,此时全局变量lock还是0!
执行if(lock==0),他也可以通过!
我们期望的是,两个进程同时调用lock函数时,最后只能有一个通过,另一个必须等,但是现在俩进程都通过了!
这把锁根本没有起到作用!
这是什么原因呢?
因为,这个锁函数,不是原子操作!
原子操作
我们先说一下原子是个什么东西。
原子
世界万物,大到日月星辰,小到虫蚁草芥,由什么组成的呢?
你加班到深夜,仰望星空,浩瀚宇宙,难道没有想过这个问题吗?
毛主席,少年时就思索救国之路;周恩来,少年时就知道为中华崛起而读书。
伟人从小就立志高远,思索这些大问题。
我们也应该严格要求自己。
这个古人早早就在思索。
有很多观念,比如古希腊人认为万物是有土气火水组成的,不过亚里士多德认为,这四种还不能解释很多现象,他认为还应有一种神秘的第五元素。
中国古代也有认为,万物由金木水火土组成。
在古人的这些思想中,有一个值得注意,那就是古希腊的德莫克利特。
他认为,万物都由不可分割的微粒组成,他管这种微粒叫原子。
这个思想很了不起,遗憾的是,那时候科技不发达,也没有放大镜什么的,他没法验证自己的理论。
然后,历史在前进,科学在发展,出现一位牛人,英国的著名科学家艾萨克·牛顿。
人如其名呀,牛到什么地步?
死的那天,欧洲贵族能来的都来了。
这边老婆和娘掉河里了,都不管了,牛顿那边还等我去抬棺材呢!
到了现场,才发现,好多人抬棺材都轮不上!
跟火炬手一样,传到你了,才能抬。
正抬着呢,手机响了,家里着火了,不管了!
房子没了还能重盖,我走了,人埋了还能挖出来让我重新埋么?
回家了就吹牛呗,儿子,知道你爸爸今天干啥去了不?
牛顿坟上那土就是我拿铁锨拍实的!
我这还有那天的孝服呢,限量版!
人家也不是浪得虚名,万有引力,光学研究,三大运动定律,那个年代要是有诺贝尔奖,拿去参选,不带评的,直接就内定了。
发明微积分,菲尔茨奖也得有一份,其他天文地理更是样样精通。
伯努利当年全欧洲悬赏两个问题,六个月没人敢揭榜,牛顿听说后,一顿饭功夫就做出来了。
一个字,牛!
不过这人气量不咋大,和莱布尼茨争谁发明微积分,牛顿雇了一批五毛去骂街,后来嫌这些水军不给力,干脆披了马甲,亲自上阵。
大科学家就这素质。
也不奇怪,杨振宁和李政道争功后来不也闹翻了。
晚年时候牛顿醉心化学研究,但是没啥成绩。
按说这么大的腕,多少留点东西吧。
原因是他选错方向了。
晚年牛顿当了不列颠铸币厂厂长。
食君之禄,担君之忧,和某些干部上班就是喝茶看报不一样,人家可不白吃干饭。
一刻没停研究,研究啥呢?
西方经久不衰的炼金术!
(也是东方经久不衰的)
一门心思想把铅铜等金属变成黄金,这要是成功了,GDP那得翻跟头上涨,三年之内赶英超美,共产主义跑步实现了,多爽!
资本主义要发展,必须要四样东西:
技术,劳动力,资本和市场。
技术有了,瓦特改良蒸汽机,工业革命,这个没问题。
劳动力,欧洲少呀!
资本呢?
欧洲没有黄金呀!
不过后来都解决了,到非洲抓黑奴,然后运到南美洲,开矿挖金子,再运回总部。
资本主义完成原始积累,然后就到亚洲找市场了。
当然,这是后话了,黄金,很重要的东西。
欧洲没有怎么办?
牛顿那年代有骨气,想自力更生,刻苦钻研。
不过,结果不用我说你也知道了。
他不知道,咱可知道,原子是化学变化中的最小微粒,化学方法,是无法分割原子的。
到死也成功不了。
不过话说回来,金子要是这么容易就造出来,连痰盂都是金的了,那也就不值钱了。
说了这么多,就为了说明原子很不好分割,连这么伟大的科学家都没辙。
人类几百万年文明,从钻木取火那天开始,一直就拿他没辙。
当然了,科学在发展,历史在前进,汤姆生发现电子,卢瑟福完善原子模型,质子又是由夸克组成,圣斗士爆发小宇宙连原子都能击碎等诸多科学发现都表明原子其实是可以再分的。
但是,我们这里,提到原子操作,还是用了原子最初的含义。
回到原子操作
话扯远了。
我们说到原子操作,简单来说,就是一个进程从这个操作开始到结束,执行期间不会切换到另一个进程。
什么样的操作是原子操作呢,首先可以肯定,一条汇编语句就是一个原子操作。
因为中断只可能发生在每条汇编指令结束之后。
Cpu是每执行完一条语句,就去检查中断的。
现在的问题是,加锁函数需要以下三个步骤:
(1) 进程首先要检查这把锁的状态,是否能申请到
(2) 申请到了,就加锁,然后继续
(3) 申请不到,就等待,直到这把锁可用(也有可能只等一段时间)
这个操作无法用一条汇编语句实现,嗯,至少目前intel还没有这样的指令(就拿x86来说),注意,这里面可是有ifelse这样的关系。
也就是说,我们要用一种方法。
保证这三步不会被中断。
有一个最简单的方法,既然害怕被中断,那么干脆一开始就把中断关了,等这三步做完,再把中断打开。
有贼心又有贼胆,但是贼却没了,这样就不怕中断了。
(这种方法只对单核CPU有效,双核的你关了一个核的中断,另一个可没有关,双核就要借助硬件帮忙了,以后再说)
加锁的过程中,会把锁的状态置为“加锁状态”
也就是说,当大家都去要这把锁的时候,首先都要去执行关中断的命令,指令都是一条一条执行的,不论挑到了哪个进程,反正总会有一个,率先执行了关中断,一旦中断被关,那么其他进程就再没机会执行了。
这样,就可以保证,很多进程“同时”去要这把锁,最后只能有一个进程获得。
总之,我们可以保证加锁这个过程,是原子操作。
尝试关中断
在vc08中,输入以下代码,vc6一样。
_asm表示,这是一条汇编语