凡是单纯讲史的章节我全部略去。
本节讲的主要是由CPU、内存和I/O之间速度不匹配而设计的硬件架构及其发展。
这个就不用细说了CPU最快,内存次之,I/O更慢。由于CPU和内存速度还算接近,所以把CPU和内存算作一类,I/O单独算作一类。当然这里说的I/O是指I/O设备,并不是操作。
随着发展CPU频率越来越高,处理速度越来越快,内存跟不上节奏了,它们之间的I/O也出现了速度不匹配的问题。
因为I/O设备可分为高速设备和低速设备两种,所以为高速搭配北桥,低速搭配南桥。
它们之间的关系可用下图表示:
CPU的频率只能达到4GHz无法提升,这是由CPU制造工艺决定的,是个瓶颈,目前还无法突破。
一个CPU能力有限,那就让多个CPU共同工作提升效率。但是这样的CPU阵列各部件利用率不高,于是,发展出了多核心,其他部件共享的多核CPU设计。说白了,原来的CPU里面每个CPU一个核心,除此之外还有围绕这个核的其他部件。但是现在多核CPU除了核心彼此独立外,其他的部件是共享的。
这一节就这么点内容。
从下图可以看出计算机的结构大概是这样的:
最底层是硬件,它提供硬件规格描述。再往上是操作系统内核,它提供系统调用。再往上是运行库,它提供各种系统API。再往上就是各种系统软件了。这种设计具有上层屏蔽下层,上层提供接口的特点。
这一节对接口的解释非常好。作者说接口是一种协议,协议二字比较贴切。当然这个协议不是计算机网络中的protocol。
有二。1、提供抽象接口。2、管理硬件。
操作系统经历了从多道程序设计、分时操作系统、到多任务操作系统等阶段。
多道程序设计是指CPU空闲的时候出让CPU以提高CPU利用率的设计;分时是指给每个程序固定的时间片执行,时间片一到就停止的设计,不过这个时间片是轮转着用的,不是一个程序用完了就没了;多任务就是现在操作系统设计了,程序以进程的方式存在。
抢占:OS对程序执行具有绝对的控制权,OS依据一定标准判断该剥夺哪个程序的执行就剥夺,想让哪个程序执行就让哪个程序执行。
GDI和directX等都是硬件的抽象,是一个中间层,它们屏蔽了硬件的具体细节,提供了通用的操作接口。
LBA(Logical Block
Address):因为硬盘结构复杂,概念繁多,寻找一个扇区要经过很多步骤,这个比较麻烦。与其如此,不如干脆为每个扇区配置一个逻辑编号,这样找扇区就好像是哈希算法一样快。
程序在内存中的地址空间是需要相互隔离的。这是为了防止一个程序在无意间修改其他程序造成意料之外的结果,另外,这也是为了信息安全。
内存利用率要高,要不然程序在内存和硬盘之间进行I/O操作所花费的时间可就多了。
程序运行的地址应该是确定的。因为多数程序指令跳转的目标地址是固定的,如果运行地址不确定就不能保证每次都在目标地址上运行,这就需要重定向进行调整,浪费时间。
解决上述问题的办法是使用中间层,即把程序的运行地址与目标地址建立一种映射关系。
我们平时说的什么32位,64位CPU啥的都是指CPU的处理能力,从硬件的角度讲,即,计算机的地址总线的条数。从CPU的设计上讲就是CPU一次能够处理的二进制位数,而这个位数还有一个学名叫字长。
内存的物理地址空间就是真实的内存空间,虚拟地址空间则是应用于进程的逻辑地址空间。
我在想如何从16进制的差值一下推断出地址空间的大小?
以下是我的想法。1位16进制数字代表4位2进制数字,换句话说16进制数字转换为2进制数字是以24为单位进行换算的。那么根据某个16进制数字所在位置乘以当前权值就可以得到该位置上的16进制数字所代表的2进制数字。而16进制某位的权值等于低一位的权值乘以24,并且16进制最低位的权值是20,因此可以根据这个规律换算出相应的2进制数字。
来看个例子。书上说从0X00000000到0X00A00000的地址空间大小就等于|0x00A00000-0x00000000|=|A00000|因为A是10所以其等价于|1000000|,现在按照上述规律进行换算。
10×220+0×216+0×212+0×28+0×24+0×20=10M(byte)。
分段的方法可以使各进程彼此隔离,并且可以使程序运行的地址确定。
分段的缺点就是它以程序为单位进行处理,但是根据程序运行的局部性原理,程序通常情况下只有一少部分需要常驻内存,因此以程序为单位换进换出严重影响了内存的利用率和处理速度。
页面有3种:1、虚拟页;2、内存页;3、磁盘页。
MMU(Memory Management
Unit)负责把虚拟地址转换成物理地址。
使用线程的好处?
1、多线程可以有效利用等待时间。因为某线程陷入等待状态后别的线程可以继续执行;
2、多线程不会使与用户的交互中断。因为可以一个线程负责与用户交互,另一个线程负责计算;
3、能够实现程序内部并发执行操作;
4、多核CPU等硬件的潜力只有多线程才能使其充分发挥;
5、在数据共享方面更高效。
线程的私有存储空间?
1、栈;2、线程局部存储(Thread Local Storage,TLS);3、寄存器。
线程真正的并发执行和非真正并发执行?
在同一时间只有处理器核心数量大于等于执行线程数量的时候才是真并发执行,除此之外都是模拟出来的。
线程调度:在同一时间处理器的核心数量小于执行线程的数量时就需要在同一核心不断切换来执行线程。
改变线程优先级的3种方式
1、用户指定优先级;2、根据等待状态的频繁程度调整优先级;3、长时间得不到执行而被提升优先级。
可抢占执行线程和不可抢占执行线程:线程的各种状态完全由操作系统来控制这就叫可抢占,就像某线程的时间片用完进入就绪态一样,这就是由操作系统来控制的。除此之外的就是不可抢占线程。
不可抢占线程主动放弃执行的时机:1、线程等待某事件发生时。2、线程主动放弃时间片。因为就这俩条件所以不可抢占线程调度的时机是确定的。
Linux下的多线程:不像Windows那样把线程和进程分得那样清楚,Linux是以任务为单位的,如果某几个任务的执行是做同一件事的各个部分,那么这几个任务就可以看成是线程,而这件事就可以看成是进程。所以Linux下的线程和进程是动态的概念。
Linux下的fork函数:fork是叉子的意思,我不知道为啥Linux用它来给函数命名。它的作用就是复制任务,新任务和原任务共享同一块内存空间,并且是写时复制。所谓写时复制就是写的时候才从内存空间里面复制出一块给你写,原内存空间内容不变。读的时候新旧任务读同一块内存空间。
Linux下的exec函数:fork产生的是本任务的镜像,也就是复制品。两个同样的任务完成同样的功能是浪费啊,所以fork是个半成品函数,必须搭配别的函数才有用,这个函数就是exec函数。Exec函数用来执行别的可执行文件,换句话说就是干别的事。所以可以把fork理解成在一块内存空间上创造出个接口给exec执行新任务。
Linux下的clone函数:我对它的理解就是fork和exec二合一,clone的作用就是产生新线程。
要知道线程安全就得知道啥叫线程不安全。所谓线程不安全就是指多个线程同时访问共享数据造成结果的不确定性。
原子操作:绝对不会被打断的操作。因为原子是化学反应中的最小微粒不可再分所以拿这个来比拟原子操作。它适用于简单应用环境。
解决线程不安全的通用方法是锁。
线程同步:一开始我还以为是多个线程一起访问某个资源呢,其实不然,线程同步是解决线程访问同一数据资源的解决方式,保证了同一时间只有同一线程访问数据资源,从而保证了线程安全。
锁——二元信号量:最简单的锁机制。只允许一个线程独占,一旦有线程占用,锁就呈现占用状态,其他线程无法访问资源。否则,非占用状态,可以接受线程。
锁——多元信号量:就是它允许多个线程同步访问资源,比二元信号量高能一些。我感觉信号量就像管道。一个线程想访问资源它就必须首先获取一个管道,这样原来的管道数就少1,于是信号量首先减1。但是如果信号量减1以后成为负值,说明原来的管道数为0,即原来就已经没有管道了,那么此时信号量机制就只能让该线程等待了,这就是P原语。而如果一个线程用完了资源想要释放,那么它必须归还它所使用的管道,那么管道总数应该加1,即信号量加1。正因为信号量已经加1,如果此时的信号量值为小于1,那说明在加1之前管道总量就已经透支了,而且先前那些因为没有获得管道的线程还在那等着呢。正好有个线程归还了管道,V原语赶紧从那些等待的线程中找一个出来把管道给它,这就是在信号量值小于1的情况下唤醒线程的意思。
锁——互斥量(Mutex):信号量与互斥量的区别是一个信号量可以被一个线程获取并释放给另一个线程使用,正如V原语的操作。而互斥量始终都是一个线程,上锁是这个线程,这个线程不执行完就不解锁。
锁——临界区:获取临界区的锁为进入临界区,释放锁为离开临界区。它的作用对象是某一位以进程,一旦某进程进入临界区,其他进程就无法进入。除此之外,临界区与互斥量相同。
锁——读写锁:互斥量、临界区和信号量适用于读写都非常频繁的场合,而读写锁适用于读频繁而写不频繁的场合。它的工作规律可用下表表示:
锁写锁状态
以共享方式获取
以独占方式获取
自由
成功
成功
共享
成功
等待
独占
等待
等待
锁——条件变量:相当于一个开关,它可以让等待它的线程继续等待也可以让它们继续执行。而这个开关需要一些其他的线程打开或关闭它。
可重入函数:一个函数没有执行完全,但是由于内部因素或者外部调用,又一次开始执行该函数。它不产生任何不良后果。
产生可重入的条件:1、多线程共同执行该函数。2、函数自己直接或者间接调用自身。
可重入函数的特点:1、不使用任何(局部)静态或全局的非const变量。因为如果使用的话它就涉嫌操纵共享数据,这样会导致线程不安全。2、不返回任何(局部)静态或全局的非const变量的指针。因为这同样涉及到共享数据。3、仅依赖于调用方提供的参数。因为这样可以把函数的执行过程局限在局部。4、不依赖于任何单个资源的锁。单个资源的锁不允许被中断,这不符合可重入函数的定义。5、不调用任何不可重入函数。这个没啥好说的,如果调用了,可重入函数就成了不可重入函数。可重入性质是并发安全的强力保证可在多线程环境下大胆使用。
过度优化:P53这个例子就是说本来2个x++结果是2,但是经过上锁以后却是1,这证明即使通过锁机制也不能完全保障计算正确,这是计算机内部工作机制造成的线程不安全。
CPU对程序的优化可能导致线程不安全,因为它会调整程序语句执行顺序以达到CPU所谓的优化,这有时候很麻烦。Volatile关键字可以阻止这种优化。1、它阻止编译器为提高程序执行速度将一个变量缓存到寄存器内而不写回。2、它阻止编译器调整语句执行顺序。这两件事就是volatile所做的具体工作。但是,volatile能管住编译器管不了CPU,CPU还是能对指令进行动态调整。
P54举了一个double-check的例子,虽然现在我对这个没有多深的理解,但是从这个例子中我看到作者是怎么分析的。它是将各个语句内部实际所进行的操作都列出来进行分析的,这个值得我学习。
虽然volatile管不了CPU,但是CPU有CPU相当于volatile的指令,一般这个指令叫做barrier。
线程分为内核级线程和用户级线程,内核级线程是用户直接接触不到的,用户只能接触到用户级线程。
3种内核级线程与用户级线程的模型。
1、一对一模型:就是每个用户级线程都对应一个内核级线程,但反过来不是,因为内核级线程可能没有用户级线程与之对应。一般直接使用API或者系统调用创建的线程均为一对一模型。
它的优点:真正实现线程的并发执行,线程之间彼此互不影响。
它的缺点:1、许多操作系统限制了内核级线程的数量导致用户级线程数量受限。2、许多操作系统用在内核级线程调度上的开销较大,主要为上下文切换开销,致使用户级线程执行效率低下。
2、多对一模型:多个用户级线程对应同一个内核级线程,线程的切换由用户级代码决定。作者说多处理器对提升处理速度没有明显帮助,这是当然的了,CPU处理的是内核级线程,而这个模型就在那摆着,CPU也只能按照这个模式来处理。再说了,一个线程只能在一个核上跑,你再多给几个核也没用啊。
它的优点:它比一对一模型快,还有高效的上下文切换和近似无限制的线程数量。
它的缺点:只要有一个线程阻塞,对应于同一个内核级线程的其他线程也无法执行,该内核级线程也阻塞,这很好理解,因为只有一条通路。
3、多对多模型:是上面二者的合体。很显然它能克服上述二者的缺点,同理多处理器也无法显著提升它的执行效率。