读书笔记--《程序员的自我修养》第一章:简介

一、计算机软件体系结构

二、操作系统和CPU
1、操作系统的功能:提供抽象的接口,管理硬件资源。

2、充分利用CPU的方法
(1)多道程序利用
编写一个监控程序,当程序暂时无法使用CPU时,监控程序就把另外的正在等待CPU资源的程序启动,充分利用CPU。
缺点:程序调度策略太粗糙
(2)分时系统
每个程序运行一段时间后都主动让出CPU给其他程序,使得一段时间内每个程序都有机会运行一小段时间。尤其适合交互式的任务。
(3)多任务系统
操作系统接管所有的硬件资源,并且本身运行在一个受硬件保护的级别。所有的应用程序都以进程的方式运行在比操作系统权限更低的级别。CPU由操作系统统一进行分配,每个进程根据进程优先级的高低都有机会得到CPU,但是如果运行时间超出了一定的时间,操作系统会暂停该进程,将CPU资源分配给其他等待运行的进程。这种CPU分配方式称为抢占式,OS可以强制剥夺CPU资源并且分配给它认为目前最需要的进程。如果OS分配给每个进程的时间都很短,即CPU在多个进程间快速切换,从而造成很多进程都同时运行的假象。

三、设备驱动
1、驱动程序可以看作是OS的一部分,和OS内核运行在特权级,但它又与OS内核之间有一定的独立性,使得驱动程序有比较好的灵活性。

2、linux文件系统ext2、ext3

3、硬盘的基本存储单位是扇区,每个扇区一般为512字节。每个硬盘往往有多个盘片,每个盘片分两面,每面按照同心圆划分为若干个磁道,每个磁道划分为若干个扇区。比如一个硬盘有2个盘片,每个盘片有65536个磁道,每个磁道有1024个扇区,则该硬盘容量为2*2*65536*1024*512=128GB。由于每个盘片上同心圆周长不一样,如果按照每个磁道都拥有相同数量的扇区,那么靠近盘面外围的磁道密度肯定比内圈更加稀疏,这样是比较浪费空间的。但是如果不同的磁道扇区数量不同,计算会比较复杂。为了屏蔽这些复杂的硬件细节,现代硬盘普遍使用一种叫做LBA的方式,即整个硬盘中所有的扇区从0开始编号,一直到最后一个扇区,这个扇区叫做逻辑扇区号。

四、内存分配
1、内存分配问题
(1)地址空间不隔离
所有程序直接访问物理地址,程序使用的内存空间不是隔离的,恶意程序会很容易改写其他程序的内存数据,达到破坏
(2)内存使用效率低
运行一个程序时,需要将整个程序装入内存中然后开始执行。如果要突然运行一个程序,但是内存空间已经不够了,这时候我们就需要将其他程序重新装入磁盘,等到用的时候再读回来。这样大量数据换入换出,导致效率低下。
(3)程序运行地址不确定
因为程序每次需要装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空虚区域的位置是不确定的,给程序的编写造成了一定的麻烦。这将涉及到重定位问题

2、解决问题
(1)针对这个问题,引入虚拟地址的概念。
(2)分段
这里写图片描述
分段有效解决了(1)(3)两个问题,因为它不需要关心分配到物理地址的哪一个区域,只需要按照虚拟地址0x,,,~0x…来编写程序即可。但是其无法解决效率低的问题,因为如果内存不足,被换入到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度。事实上,根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序地很多数据其实在一个时间段内都是不会被用到的!因此人们想到了粒度更小的内存分割和映射方法,使得程序的局部性原理得到充分利用,大大提高了内存的使用率。这种方法就是分页。

(3)分页
每一页的大小由硬件决定,或硬件支持多种大小的页,由OS选择决定页的大小。目前几乎所有的PC上的OS都使用4KB大小的页。
虚拟存储的实现需要依靠硬件的支持,对于不同的CPU来说是不同的。但是几户所有的硬件都采用一个叫做MMU的部件来进行页映射。
这里写图片描述
在页映射下,CPU发出的是virtual address,即我们的程序看到的是虚拟地址,经过MMU转换以后就变成了physical address。一般MMU集成在CPU内部了,不会以独立的部件存在。

五、线程
1、线程,或轻量级进程(LWP)是程序执行流的最小单元。通常,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码段、数据段和堆段)及一些进程级的资源(如打开文件和信号)。

2、线程的访问权限
线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,那么这就是很少见的情况),但实际中线程也有自己的私有存储空间。包括栈、线程局部存储(TLS)、寄存器(是执行流的基本数据,因此为线程私有)
这里写图片描述

3、线程的调度和优先级
当线程数量小于处理器数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此不相干。但是对于线程数量大于处理器数量的情况,线程的并发会受到一些阻碍,因为此时至少有一个处理器会运行多个线程。这样也给不断在处理器上切换不同的线程的行为称之为线程调度。
这里写图片描述
我们一般把频繁等待的线程称之为IO密集型线程,把很少等待的线程称为CPU密集型线程。
在优先级调度下,存在一种饿死的现象。线程的优先级改变一般有三种方式。
(1)用户指定优先级
(2)根据进入等待状态的频繁程度提升或降低优先级
(3)长时间得不到执行而被提升优先级

4、Linux多线程
Linux内核中并不存在真正意义上的线程概念。Linux将所有的执行实体都称为任务。每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux下不同任务之间可以选择共享内存空间,因此共享了同一个内存空间的多个任务构成了一个进程,这些任务也就成了这个进程里的线程。
在Linux下,用以下方法可以创建一个新任务。
这里写图片描述
fork函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数里返回。其中本任务的fork将返回新任务的pid,而新任务的fork将返回0。
这里写图片描述

5、线程安全
为了避免多个线程同时读写同一数据而产生不可预料的后果,需要将各个线程对同一个数据的访问同步。同步既是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。这样对数据的访问被原子化了。
同步最常见的方法是使用锁。
(1)二元信号量
是一种最简单的锁,只有两种状态:占用和非占用。它适合只能被唯一一个线程独占的资源。当其处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将其置为占用状态,此后其他所有试图获取该二元信号量的线程将会等待,直到该锁被释放。

(2)互斥量
与二元信号量类似,资源仅同时允许一个线程访问,但区别是互斥量要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,而信号量不是。

(3)临界区
是比互斥量更加严格的同步手段。它和互斥量和信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的,即,一个进程创建了一个互斥量或信号量,另一个进程试图获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他进程无法获得该锁。

(4)读写锁
适于读取频繁偶尔写入的情况。对于同一个锁,读写锁有两种获取方式,共享的或独占的。
当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态;
当锁处于共享的状态时,其他线程以共享的方式获取锁仍然会成功,此时这个锁分配给了多个线程。然而,当其他线程以独占的方式获取时,它将必须等待锁被所有的线程释放。
当锁处于独占状态时,将阻止任何其他线程获取该锁,不论他们试图以哪种方式获取。
这里写图片描述

(5)条件变量
作为一种同步手段,作用类似于一个栅栏。对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次线程可以唤醒条件变量,此时某个或所有等待条件变量的线程都会被唤醒并继续支持。

6、多线程内部情况
(1)一对一模型
一个用户使用的线程唯一对应一个内核使用的线程(反过来不一定)。这时,线程之间的并发是真正的并发。

(2)多对一模型
多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行。因此相对一对一模型,多对一模型的线程切换要快速的多。缺点:如果也给用户线程阻塞,那么所有的线程都无法执行,因此此时内核里的线程也阻塞了。

(3)多对多模型
多对多模型结合了多对一模型和一对一模型的特点,将多个用户线程映射到少数但不止一个内核线程上。在此模型中,一个用户线程阻塞并不会使得所有的用户线程阻塞,因为此时还有别的线程可以被调度来执行。另外,多对多模型对用户线程的数量也没有限制,在多处理器系统上,多对多模型的线程也能得到一定的性能提升,不过提升的幅度不如一对一模型高。

猜你喜欢

转载自blog.csdn.net/qq_15727809/article/details/82592862