程序员的自我修养读书笔记-1

前序:作为一个马上就要工作的非科班本科生,前段时间为了找工作,有针对性的学习了一些编程语言,数据结构,网络方面的知识,学的非常浅,非常杂乱,存粹是为了应对找工作。现在空下来了,想着以后应该就是走程序员这条道路了,就决定学习一些计算相关的基础课程(像那句老话说的,出来混的,总是还要还的hhhhh)有好多人给我推荐了这本《程序员的自我修养》,我翻了翻,大体内容更加偏向于底层关于程序的链接,装载与动态静态库的一些原理。刚刚好能弥补我底层方面知识不足,因此开始认真研读这本书,并将每章中的内容以我自己的语言做成笔记,以此来记录自己的学习历程。

计算机基本软硬件

1.硬件组成

1. 1早期的计算机硬件结构

早期的计算机没有复杂的图形设备,大多数的显示设备是只能输出字符的终端,而CPU的核心频率也不高,和内存的频率基本一样,这些外部I/O设备(显示设备,键盘,软盘,磁盘)与CPU,内存都是直接连接在一个**总线(Bus)**上面。但是这些I/O设备的速度与CPU,内存相比来说还是差很大,因此为了协调I/O设备与总线之间的速度,也为了能让CPU能够与I/O设备进行通信,一般每个设备会有一个相应的I/O控制器。
在这里插入图片描述

1.2 现代硬件结构框架

1.2.1 随着CPU核心频率的不断提升,内存的速度更不上CPU的速度,为了摆脱其他设备(内存,I/O设备)对CPU频率提高的限制,于是采用了与内存频率一致的系统总线,在系统总线上的外部设备可以工作在较低的频率上,而CPU采用倍频技术,使得CPU内部工作频率变为外部频率的倍数
1.2.2 接着图形化操作系统和软件的普及,使得图像芯片需要跟CPU和内存之间大量交换数据,原来的慢速I/O总线已经无法满足需求。为了协调CPU,内存和高速图形设备,人们专门设计了一个高速的北桥芯片(Northbridge,PCI Bridge),来完成他们间告诉的数据传输。

PCI 的缩写为,Peripheral Component Interconnect,外部的设备互相连接

但是北桥的运行速度太快了,假如所有的相对低速的设备也接在北桥上,那么北桥既须处理高速设备,又需要处理低速设备,设计就会非常的复杂。于是人们又设计了专门处理低速设备的南桥(Southbridge)

20世纪90年代的PC机的系统总线采用PCI结构,而低速设备采用ISA总线。下图为PCI/ISA南北桥硬件设计图。
在这里插入图片描述
1.2.3
CPU频率最开始的几十kHz到现在的4GHz,提升零 数十万倍,但是自从2004之后,CPU的频率就在也没有发生质的提高。CPU的工艺方面已经达到了物理极限。
因此人们只能从其他方面提高CPU的频率,比如增加CPU的数量。一个最常见的形式就是对称多处理器(SMP,Symmetrical Multi-Processing),常用于大型的数据库,网络服务器上。

**多核处理器,(Muti-core Processor)**其实就是SMP的一种简化,存粹的SMP对于个人计算机来说价格高,性价比低,而多核采用的是多个处理器共享比较昂贵的缓存部件,只保留多个核心,并且以一个处理器的外包进行出售。

2. 软件组成

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。

2.1 软件组成结构

软件系统结构图:
在这里插入图片描述
应用层与开发工具:这一层通过调用运行库接口(OS API,比如Window API,Linux的POSIX API)完成功能
运行库:OS API 的提供者,通过封装调用
系统内核提供的系统调用
来完成功能
系统内核:调用的提供者,系统调用一般以软件中断的方式提供,与硬件驱动程序(也可以看作是操作系统的一部分,往往和操作系统一起运行在特权级)一起工作,完成对硬件的控制。
硬件:最底层

2.2 操作系统的功能

  1. 提供了抽象的接口
  2. 管理硬件资源

2.3 CPU调度方式

  1. 分时系统:每个程序运行一段时间以后都主动让出CPU给其他程序
    2.多任何系统:操作系统接管了所有的硬件资源,并且本身运行在一个受硬件保护的级别。所有应用程序以进程的方式运行,且级别低于操作系统。每个进程拥有自己的独立地址空间。CPU由操作系统统一分配(采用抢占式分配),每个进程根据进程优先级高低都有机会得到CPU。如果某个进程运行时间超出了一定时间,操作系统会暂停该进程,将CPU分配给其他正在等待的进程。

2.4 虚拟地址

虚拟地址:每个进程拥有自己的独立虚拟地址空间,虚拟地址可以通过操作系统完成到物理空间的映射(实际上这个过程由硬件完成,MMU),隔离了不同进程的地址。
映射策略选择:
分段:最初人们采取这种策略,把程序需要的整个虚拟空间映射到物理地址。
优点:完成不同进程地址的隔离,并且程序无需关心自己运行的地址,不用考虑重定位
缺点:内存使用效率低
分页:把虚拟地址空间按照固定大小的页进行分割,把常用的数据和代码页和数据页转载到内存中,把不常用的保存在磁盘中。

2.5线程

2.5.1 线程:又被称为轻量级进程,程序执行的最小单元。由线程ID,当前指令指针(PC),寄存器组合和堆栈组成。

和进程之间的区别:通常一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(代码段,数据段,堆等)以及一些进程级的资源。
线程进程关系图:
在这里插入图片描述

同一个进程内线程之间数据关系图:
在这里插入图片描述

2.5.2 线程的调度

线程调度的概念:在单处理器对应多线程的情况下,操作系统会让这些多线程程序轮流执行,每次仅执行一小段时间,这样的一个不断在处理器上切换不同线程的行为称之为线程调度

线程的状态:运行,就绪(可以运行,但cpu被占用),等待(线程在等待某一事件发生,通常是I/O同步)

线程状态的切换
在这里插入图片描述

线程调度主要策略:
优先级调度:线程运行有相应的优先级,高优先级的线程会更早的执行。
线程优先级改变方式:1. 用户指定优先级
2. 根据进入等待状态的频繁程度提升或降低优先级
3.长时间得不到执行而被提升优先级
论转法:即上文提到的时间片论转

####2.5.3 Linux的多线程
fork:复制当前进程,与原任务共享一个写时复制(Copy on Write,COW)

所谓写时复制,指的是两个任务可以同时自由读取内存,但是任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响其他的任务使用。
在这里插入图片描述

fork只能够产生本任务的镜像,因此必须要使用exec配合才能启动别的新的任务。

exec:可以用新的可执行映像替换当前的可执行映像。
clone:可以产生一个新线程,从指定的位置开始执行,并且可以选择是否共享当前进程的内存空间和文件等。

2.5.4 线程安全

原因:程序可访问的全局变量和堆数据随时都可能被其他的线程改变。
原子操作:被汇编代码翻译后只有一条指令,不会被调度系统打断,能保证线程安全的操作。

但是原子操作仅适用于一些比较简单的环境,一旦遇到复杂的场合,比如保证一个复杂数据结构更该的原子性,原子操作就力不从心了。

保证线程安全的通用手段:同步与锁

  1. 二元信号量:**它适合只能被唯一一个线程独占访问的资源。**当处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量重置为占用状态,此后试图获取该二元信号量的线程将会等待,直到该锁被释放。
  2. 信号量:适用于允许多个线程并发访问d的资源。通过计数的方式来决定是否给予资源使用权。
  3. 互斥量:与信号量相似,但是信号量可以被整个系统的任意线程获取释放,互斥量要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁。
  4. 临界区:与互斥量相似,但是临界区范围仅限于本进程,其他进程无法获取该锁,而互斥量则是在系统任意进程都是可见的。
  5. 读写锁:对于频繁读取,偶尔写入的情况,上述方式会显得低效,读写锁则可以避免这个问题。
    读写锁有两种状态:共享与独占。当读写锁处于自由状态时,试图以任何一种方式获取锁都能成功,并且将锁置于这个状态。如果处于共享状态,其他锁以共享的方式获取这个锁都会成功,以独占的方式获取这个共享的锁,那它会一直等待锁被所有的线程释放。
    相应的,处于独占状态的锁将阻止任何其他线程获取该锁,不论以何种方式获取。
读写锁状态共享方式获取独占方式获取
自由成功成功
共享成功等待
独占等待等待

编译器过度优化导致的问题:

  1. 编译器为了提高速度,把某个共享的变量提前放到了寄存器中,但是各个线程的寄存器都是独立的,这样修改变量后,会使得变量的结果出现不确定性。
    解决方式:可以在变量前添加volatile试图阻止优化。
  2. CPU为提高效率动态交换指令的顺序。
    例子:单例模式
volatile T* pInst = 0;
T* GetInstance()
{
    if(pInst == NULL)
    {
        lock();
        if(pInst == NULL)
            pInst = new T;
        unlock();
    }
    return pInst;
}

pInst = new T;,实际包含了三个操作

  1. 分配内存
    2.在内存位置上调用构造函数
    3.将内存地址赋值给pInst

对于CPU来说,2.3步骤的顺序是可以颠倒的,也就是说可能会出现这样的一种状况,操作系统分配了内存后,程序先将内存地址分配给了pInst,这时对象还没构造完成,假如这时候出现了另外一个对GetInstance的并发调用,会直接将这个未完成构造的对象直接返回给用户使用,就很有可能造成程序的崩溃。

解决方式:调用CPU提供的阻止换顺指令barrier,不过不同CPU的名称都各不相同,例如POWERPC提供的为lwsync.

volatile T* pInst = 0;
T* GetInstance()
{
    if(pInst == NULL)
    {
        lock();
        if(pInst == NULL)
        {
                T* temp = new T;
                barrier();
                pInst = temp;
        }  
        unlock();
    }
    return pInst;
}
2.5.4 线程模型
一对一线程模型:一个用户使用的线程就唯一对应一个内核使用的线程

优点:

  1. 一个线程阻塞,其他线程不受影响,是真正的并发
  2. 在多处理器的系统上表现更好
    缺点:
  3. 用户线程数量受到内核线程数量的影响
    2.内核线程调度时候,上下问切换开销大,用户线程效率下降
    在这里插入图片描述
多对一模型:将多个用户线程映射到一个内核线程上,线程之间的切换由用户态代码来完成。

优点:

  1. 线程切换效率高
    2.几乎无限制的线程数量

缺点:

  1. 如果一个线程阻塞,所有的线程都无法执行
    2.多处理器系统中,处理器的增加对线程性能不会由明显提高
    在这里插入图片描述
多对多模型:将多个用户线程映射到少数但不止一个线程上

优点:

  1. 一个线程阻塞不会影响其他线程的执行
    2.对线程数量没有什么限制
    3.在多处理器系统上,多对对模型的线程的性能也能获的一定的提高。
    在这里插入图片描述
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值