linux内核与设计实现-进程
目录
介绍
以linux内核与设计实现第三版为主线,网上资料和书籍为辅助,进行断续整理,参考文章放在后面。整理的目的,当然就是为了对整个计算机的体系提高理解程度,但是并不会在这里抠细节。时间越久,越会发现这些还是很重要的。
程序
我是从第三章开始看的,这里面说到了一个程序概念,我就首先拿出来整理了。
程序是什么,程序就是指令的集合,或者说是指令序列。通过这个指令的序列,去告诉计算机要做什么,执行的步骤是什么。
指令
而每个计算机都有指令系统,包含了计算机能够做的所有基础操作。比如说,算术运算(加减),逻辑运算,数据传送,移位,条件转移等。而指令本质就是一组二进制的代码。
通过这组二级制信息,我们需要指出数据的来源,操作结果的去向以及所执行的操作是什么:
- 操作码:我要做什么操作
- 操作数的地址
- 操作结果的存储位置
- 下条指令的地址:执行程序时,大多数指令按顺序依次从主存中取出执行,只有在遇到转移指令时,程序的执行顺序才会改变。
程序计数器
而为了压缩指令的长度,可以用一个程序计数器(Program Counter,PC)存放指令地址。每执行一条指令,PC 的指令地址就自动 +1(设该指令只占一个主存单元),指出将要执行的下一条指令的地址。当遇到转移指令时,则用转移地址修改 PC 的内容。由于使用了 PC,指令中就不必明显地给出下一条将要执行指令的地址。
工作流程如图所示,我直接copy别人博客的图:
指令执行大致过程
将指令指针IP(指令计数器IC,程序计数器PC)中存储的地址传送给存储器,而后取出指令存储到指令寄存器中IR中,然后通过地址计算取出所需的操作数据,放到cpu中的寄存器中。然后通过指令译码得知操作是什么,从而发出信号进行执行操作。最后将PC+1,或者从指令中得到目标地址更新PC。
寻址方式
前面图中地址计算的过程就是寻址。寻址分为立即寻址,寄存器寻址,直接寻址,寄存器间接寻址,寄存器相对寻址。
(1) 立即寻址:操作数就在指令里面,无需访存;
(2) 寄存器寻址和寄存器间接寻址:操作数就在寄存器里面,通过寄存器的编码去寻址;间接寻址就是寄存器存储的是操作数的地址,要通过寄存器拿到地址去访存拿到数据
(3) 相对寻址:根据当前的PC地址加上指令中的形式地址形成有效地址;
(4) 基址寻址:有效地址是将cpu中的基址寄存器加上指令中的形式地址A。基址寄存器中的内容由OS决定,并且在执行的过程中不可变。这样在多道程序中,用户就可以只要关心自己的地址空间就行了,不必关心实际的地址。它的好处就是可以扩大寻址的范围,因为基址寄存器的位数可以大于形式地址A的位数;
(5) 变址寻址:有效地址是将CPU中变址寄存器IX的内容加上指令字中有效地址A。其中形式地址是作为一个基准地址,而变址寄存器中的地址是由用户设定,会根据情况发生变化。主要用于解决循环问题。比如一个几百万次的循环,我们只需将变址寄存器中的值自增1就可以访问了。
无论哪种寻址都有是为了更快,更高效的执行指令。所以我们可以说,程序是通过一个个基础的,具有一定顺序的指令组成,然后按照一定的顺序一一执行,如果要跳转那指令里面都有说明,计算机只要按照说明操作就行了。
指令流水线
而为了提高CPU执行指令的效率,使用了指令流水线的技术。它将一条指令分为多个不同的阶段:取值(IF),译码(ID),访存(MEM),执行(EX),写回(WB)5个阶段。
想象一下工厂的流水线,第一个工人在完成某个件事情后并不会等待后面的工人完成在继续下一个操作,而是一件接着一件。指令流水线和这个类似,当第一条指令完成IF后,第二条指令就可以开始IF了,重复利用使得多条指令同时执行,大大提高效率。
指令乱序
说到指令流水线就想到指令乱序,指令乱序的目的就是为了进一步提高执行的效率,减少流水线阻塞。
流水线的阻塞有三种情况:
- 结构相关:资源冲突,如都要使用某个部件。
- 数据相关:后一个指令需要前一个指令的执行结果。
- 控制相关:涉及到跳转,分支指令。
所以,如果像这样有依赖关系的指令如果挨得很近,后一条指令必定会因为等待前一条执行的结果,而在流水线中阻塞很久,占用流水线的资源。而CPU的乱序,作为优化的一种手段,则试图通过指令重排将这样的两条指令拉开距离, 以至于后一条指令进入CPU的时候,前一条指令结果已经得到了,那么也就不再需要阻塞等待了。这里的意思是将不相关的指令插入相关指令的中间,以达到减少阻塞时间的目的。
指令乱序虽然是一种优化手段,但是也会带来一些问题,比如说某个变量虽然看起来是前后无关的,但是会和其他线程的操作有着隐含的前后关系,这样就会出现线程安全的问题。
而为了解决这类问题,在JVM中提供了内存屏障和锁来防止乱序。而除了乱序,它同时也有一个功能那就是保证缓存的一致性。这样就说到了缓存一致性协议。
程序编译
联系到java,JVM本身也是一个虚拟的计算机,它里面也定义了基础的指令操作,也就是字节码指令,这个我们在编译之后就可以发现。而class本身也是一个二级制的文件,JVM通过C++去识别这个class里面的指令,然后通过PC寄存器来模拟计算机中的程序计数器,来记录指令执行的位置。而每个线程都有自己的PC寄存器。
不过不管怎么样,最终,JVM本身的指令也会被翻译成机器指令,通过计算机来执行。而这个翻译就涉及到了编译。编译的原因就是,因为计算机并不识别JVM中定义的字节码指令。
而java是如果变成机器语音,详情可以看Java代码到底是如何编译成机器指令的,这里进行部分的转载操作:
里面他将.java文件编译成.class文件称为前端编译;将.class文件翻译成机器指令称为后端编译。
在后端编译中,JVM会一条条执行指令字节码指令,其实也就是一条条将字节码指令通过解释器翻译成机器码指令执行。这样一边翻译一边执行速度当然会比可执行的二进制程序要慢,所以就引入了JIT技术。
JIT技术会将频繁执行的热点代码翻译成机器码进行缓存使用。
当 JVM 执行代码时,它并不立即开始编译代码。首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。第二个原因是最优化,当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化。
该章节中所参考或者部分转载的文章
进程
我们现在知道程序是一堆二级制文本,它本身自己不会运行。想要运行就只能让操作系统将它加载到内存中,而后为他分配资源。所以可以有多个进程执行同一个程序。
系统资源
我们经常会看到:
进程是资源分配的最小单位,而线程是调度的最小单位。
这里的资源有很多,各种各样的软硬件资源。归纳起来分为:处理器,存储器,IO设备。
处理器分配涉及到调度策略;存储器包括主存,辅存,寄存器;IO设备包括键盘,屏幕,打印机,磁盘等。
而操作系统是按照进程来分配资源的,比如说一个进程打开了一个文件,那么操作系统会给他一个文件句柄,而进程里面的线程都可以使用这个句柄,但是如果你想要另外打开这个文件,那就不行,会被提示该文件正在被另外一个程序使用。
进程描述符