《程序员的自我修养》读书笔记——第一章

259 阅读6分钟

一、三个关键部件

CPU、内存、I/O控制芯片

二、发展史

  • 早期,CPU和内存同频,直接连接在总线上,I/O设备等低速设备通过I/O控制器与总线相连。
  • CPU核心频率远超内存,CPU采用倍频的方式和系统总线通信。系统总线和内存同频。
  • 北桥连接高速组件(内存、图形设备),南桥连接键盘、鼠标、USB等低速设备。
  • SMP:每个CPU在系统相互独立,地位功能完全一致。
  • 多核处理器:多个核心共享缓存。

三、计算机系统软件体系结构

"Any problem in computer science can be solved by another layer of indirection."

  • 层次之间相互通信的协议称为接口。
  • 系统调用接口以软件中断的方式提供。
  • 硬件是接口的定义者,操作系统内核层是接口的使用者。

四、操作系统

两大功能

  • 提供抽象接口
  • 管理硬件资源(CPU、存储器、I/O设备)

CPU调度

  1. 多道程序:
    • CPU闲置时分配给别的程序。
    • 程序调度之间无优先级。
    • 重要任务可能无法得到快速响应。
  2. 分时系统:
    • 每个程序运行一段时间后主动让出CPU。
    • 依赖程序主动让出CPU。
  3. 多任务系统:
    • 操作系统接管所有硬件资源。
    • 操作系统拥有最高权限,CPU由操作系统统一分配。
    • 程序以进程的方式运行,每个进程拥有独立的地址空间。
    • 进程之间有优先级,操作系统可以强制剥夺CPU权限分配给高优先级进程。

硬件抽象

  • 操作系统为开发者提供统一的接口。
  • 硬件生成厂商按照操作系统提供的框架提供硬件驱动程序。

内存分配

  1. 物理内存直接分配:

    1. 地址空间不隔离
    2. 内存使用效率低: 需要将整个程序装入内存。 会有大量的内存换入换出。
    3. 程序运行内存地址不确定
  2. 虚拟地址映射

    每个进程有自己独立的虚拟地址空间。 真实的物理地址映射由操作系统管理。

虚拟地址映射方法

  1. 分段
    • 把程序所需的内存空间映射到等大的物理空间。
    • 地址空间隔离、虚拟空间地址固定都是从0x00000000开始。
    • 无法解决换入换出,内存不足情况依然需要整块换出。
  2. 分页
    • 地址空间(虚拟地址和物理地址)人为的划分为等大的页(4KB或4MB)。 32位虚拟地址空间为4GB,划分为1 048 5764KB的页。
    • 以页为单位在虚拟页、物理页、磁盘页之间交换和存取数据。

MMU(Memory Management Unit)复制处理虚拟地址的映射。

线程

线程是程序执行流的最小单元。线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。 一个进程的线程共享程序的内存空间(包括代码段、数据段、堆等)以及一些进程级的资源(打开的文件等)。

线程私有线程之间共享(进程所有)
* 局部变量全局变量
* 函数的参数堆上的数据
* 线程局部存储(TLS)函数里的静态变量
* 寄存器(执行流的基本数据)程序代码
打开的文件

线程调度

线程状态:

  • 运行
  • 就绪
  • 等待

调度算法: 优先级调度轮转法 高优先级的线程会更早的执行。IO密集型线程会比CPU密集型更容易得到优先级的提升。 IO密集型线程会频繁的进入等待状态,让出CPU时间。 饿死:一个线程的优先级较低,始终无法得到执行。 为了避免饿死,调度系统会提升那些等待了很长时间得不到执行的线程。线程在时间片用尽之后会被强制剥夺继续执行的权利,称为抢占

Linux的多线程

Linux将所有的执行实体(无论线程还是进程)都称为任务。 fork产生一个新的任务,新任务和原任务执行相同的任务镜像,同时读取一段内存,在任意一个任务对内存发生修改时复制一份以单独使用(写时复制)。 新任务通过调用exec来执行新的可执行文件。

线程安全

一个操作被编译为汇编代码后不止一条指令,它在执行的时候就可能会被线程调度打断,导致共享的全局变量和堆数据可能被其他线程改变。 例如:i++被clang编译器翻译为三条指令。 原子操作:单指令的操作,不会被打断。

同步与锁

多个线程同时读取并不会导致线程安全问题,只有非原子型操作对数据进行修改时才需要同步机制来避免出错。 线程在访问数据之前需要先获取锁,访问结束后释放锁。在锁被占用的时候,线程会等待,直到锁重新可用。

  1. 信号量:
    • 获取信号量,信号量值-1。
    • 如果信号量的值小于0,线程阻塞,否则继续执行。
    • 访问资源结束,释放信号。
    • 信号量的值+1。
    • 信号量的值大于1,唤醒一个阻塞的线程。
    • 信号量可以由一个线程获取另一个线程释放。
  2. 互斥量:
    • 类似于二元信号量,资源只允许被一个线程访问。
    • 哪个线程获取互斥量,哪个线程释放。
  3. 临界区:
    • 信号量和互斥量进程间可见。
    • 临界量作用范围仅限于本进程,其他进程无法获取该锁。
  4. 读写锁:
    • 自由状态:可以被获取并进入共享或独占状态。
    • 共享状态:可以以共享方式被获取,等到所有其他线程都释放了可以被独占获取并进入独占状态。
    • 独占状态:阻止任何其他线程获取该锁。
  5. 条件变量:
    • 线程可以等待条件变量。
    • 线程可以唤醒条件变量。
    • 条件变量被唤醒,所有等待该条件变量的线程都恢复执行。

DCL, Double Check Lock Singleton

volatile T* pInst = 0;      # volatile 阻止编译器将变量缓存到寄存器而不写回
T* GetInstance()
{
    if(pInst == NULL)
    {
        lock();             # 只在初始化的时候加锁
        if(pInst == NULL)
            pInst = new T;
        unlock();
    }
    return pInst;
}

new 操作做了什么

  • (1)为对象分配内存

  • (2)调用构造函数初始化对象

  • (3)返回内存地址 编译器优化后,步骤(2)和步骤(3)的执行顺序会颠倒,所以DCL单例也不是线程安全的。

  • [1] DDJ_Jul_Aug_2004_revised