温故而知新——程序员的自我修养

搭配计算机体系结构基础一起理解计算机

硬件

对于程序员,计算机中最为关键的部件是中央处理器(CPU)、内存、I/O控制芯片。

SMP&内核

一个计算机可能有多个CPU,最常见的是对称多处理器(SMP),即每个CPU在系统中所处的位置以及所发挥的功能是一样的、相互对称的。注意不可以说CPU数量多运算速度就快,因为程序并非都可以分解成若干不相干的子问题。只有在请求相互独立时作用大,如:大型数据库、网络服务器这些商务服务器和需要大量计算的环境,它们需要同时处理大量请求且请求间相互独立。

多核处理器将多个处理器“打包起来”,这些处理器之间共享较昂贵的缓存部件,只保留多个核心。实际上就是SMP的简化版,只是在多核和SMP在缓存共享等方面有细微的差别。

系统软件

一般将管理计算机本身的软件称为系统软件。分为平台性的(如操作系统内核、驱动程序、运行库、系统工具)和程序开发的(如编译器、汇编器、链接器等开发工具和开发库)。温故而知新——程序员的自我修养_第1张图片

计算机系统软件体系结构是按照层次的结构组织设计的。每个层次之间要互相通信,要通信就要有一个通信的协议,一般将其称为接口

  • 接口下层为接口提供者
  • 上层为接口使用者。

理论上层次之间遵循这个接除了硬件和应用程序,每个层都可以被修改或替换。
除了硬件和应用程序,其他层都是中间层,每隔中间层都是对它下面那层的包装和拓展。【中间层使应用程序和硬件之间保持相对独立】
软件体系中,开发软件和应用程序属于同一个层次,它们使用同一个接口:应用程序编程接口(API)
API的提供者是运行库。
运行库使用操作系统提供的系统调用接口,系统调用接口在实现中常以软件中断方式提供。
操作系统内核层对硬件层使硬件接口的使用者,硬件是接口的定义者【定义解决操作系统内核,驱动程序如何操纵硬件/与硬件进行通信】这种接口称为硬件规格,由生产厂商提供。

操作系统

功能

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

如何使计算机运行起来更有效率,在时间段内处理更多的任务?计算机中资源主要分:CPU、存储器、IO设备这三方面,可以从这三个方面入手。

CPU的充分利用

多道程序
当某个程序暂时不需要使用CPU时,监控器就把另外正在等待CPU资源的程序启动,使CPU充分利用起来。
缺陷:程序间调度策略粗糙,程序间不分轻重缓急如果有的程序急需使用CPU(如用户交互任务),但响应时间无法确定。
分时系统
程序运行模式变成协作模式。在一段时间内让每个程序都可以运行一小段时间。
缺陷:如果有程序在进行很耗时的计算,霸占CPU不放,其他程序只能等着,使得系统像死机一样。
多任务系统
操作系统接管所有的硬件资源且本身运行在一个受硬件保护的级别。
所有的应有程序以进程的方式运行在比操作系统权限低的级别,且每个进程有自己的独立地址空间。CPU由操作系统统一进行分配,每个进程根据其优先级进行分配,但如果某个进程运行时间超出了一定时间,操作系统会暂停该程序,将CPU资源分配给其他等待运行的程序这种方法为:抢占式即操作系统可以强制剥夺CPU资源分配给它认为目前最需要的进程。
CPU在多个进程间迅速切换从而造成进程同时运行的假象,使每个进程从逻辑上看可以独占计算机的资源。

设备驱动

操作系统作为硬件层的上层,对硬件管理和抽象。对于操作系统的运行库和应用程序,需要统一的硬件访问方式。对于繁琐的硬件细节交给了操作系统中的硬件驱动程序完成。
硬件驱动程序常与操作系统内核一起运行在特权级,与操作系统内核又有一定的独立性,使驱动程序有灵活性。【驱动程序开发由生产厂商完成】
驱动程序要考虑硬件的状态、调度和分配各个请求从而达到更高的性能。

内存不够怎么办

早期计算机中,程序都是直接在物理内存上运行的,运行时访问的是物理地址。为了充分利用硬件,我们必须同时运行多个程序,那么问题就在于如何把计算机有限的物理内存分配给多个程序使用。
所有程序直接分配物理内存进行访问是最简单的分配,但是问题也很多:

  • 地址空间不隔离
    恶意的程序或是有BUG的程序可能会改写其他程序的(内存)数据,使得程序崩溃
  • 内存使用效率低
    没有有效的内存管理机制,程序执行时监控程序会将整个程序装入内存中开始执行。如果忽然要运行一个程序,由于程序运行需要连续的内存,需要进行大量的数据交换,导致效率低下。
  • 程序运行的地址不确定
    由于程序运行时需要分配一块足够大的空闲区域,这块空闲区域位置不确定,会出现程序的重定义问题。

解决方法:增加中间层即间接的地址访问法。把程序给出的地址看作虚拟地址,通过映射到实际的物理地址,从而保证程序间物理内存不会重叠,从而使地址空间隔离。

隔离

除程序间通信部分外,程序需要简单的执行环境、单一的地址空间、有自己的CPU就好像一个进程一个计算机,进程间完全独立运行。
地址分为虚拟地址空间、物理地址空间。其中,物理地址空间是实实在在存在的,每个计算机只有一个;虚拟地址空间是虚拟的、想象出来的地址空间,每个进程都有并且只能访问自己的虚拟地址空间【使进程隔离】。

分段

最开始时使用。就是使程序需要的虚拟地址空间与物理地址空间一一对应映射。
虽然解决了地址空间不隔离、程序运行地址不确定,但是对于内存使用进行优化,如果内存不足,被换入换出的是整个程序从而造成大量磁盘访问操作,影响运行速度。

分页

根据程序的局部性原理,一个时间段内可能只是频繁使用一小块数据,可以使用更小粒度的分割、映射方法:分页。
分页的基本方法:将地址空间分为固定大小的页,每一页由硬件决定,或是硬件支持多种页大小、操作系统决定。
虚拟空间的页:虚拟页(VP:Virtual Page)
物理内存的页:物理页(PP:Physical Page)
磁盘中的页:磁盘页(DP:Disk Page)
温故而知新——程序员的自我修养_第2张图片
上图中VP2和VP3不存在内存中,若进程用到这两页时硬件就会捕获到这个消息,这种情况称为页错误
解决:操作系统接管进程将VP2和VP3从磁盘读出装入内存,再将装入内存中的这两页与VP2、VP3重新建立映射关系。
虚拟存储的实现需要硬件的支持,不同CPU是不同的,但有一个必要的部件MMU(一般集中在CPU内部不以独立部件存在)进行页映射。
温故而知新——程序员的自我修养_第3张图片

线程

基础
概念

线程,可被称为轻量级进程,是程序执行流的最小单元。标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。
通常,一个进程由一个/多个线程组成,线程之间共享程序的内存空间(代码段、数据段、堆…)及一些进程级的资源(如打开文件和信号)
温故而知新——程序员的自我修养_第4张图片
大多软件应用中,多个线程互不干扰的并发执行,且共享进程的全局变量、堆的数据。与单线程相比使用多线程的原因:

  • 某个操作可能陷入长时间的等待,等待的线程进入睡眠状态,无法继续执行;多线程可以有效利用等待时间。eg:等待网络响应。
  • 某个操作(eg计算)消耗大量时间,若仅有一个进程,程序与用户间的交互会中断;多线程可以让一个线程负责交互,一个线程负责该操作。
  • 程序逻辑本身要求并发操作(eg:多端下载软件)
  • 多CPU/多核计算机,本身具备同时执行多个线程的能力,单线程程序无法全面发挥计算机的能力。
  • 相对于多进程应用,多线程在数据共享方面更高效。
线程的访问权限

线程可以访问进程中的所有数据,甚至其他线程的堆栈,不过实际运用中也有私有的存储空间,包括:

  • 栈(并非完全被其他线程访问,可认为是私有的数据)
  • 线程的局部存储(TSL),是某些操作系统为线程单独提供的,容量有限。
  • 寄存器(含PC寄存器),寄存器是执行流的基本数据,为线程私有。
    温故而知新——程序员的自我修养_第5张图片
线程调度与优先级

在单处理器对应多线程的情况下,并发是模拟出来的状态,操作系统让线程轮流执行、每个线程仅执行一小段时间【轮转法】。这样不断在处理器上切换不同线程的行为叫线程调度。线程调度中线程有三种状态:

  • 运行【running】:正在执行
  • 就绪【ready】:可以立刻执行但CPU被占用
  • 等待【wait】:线程在等待某事件(IO或同步)发生,无法执行。

状态转换:
温故而知新——程序员的自我修养_第6张图片
注:时间片:处于运行中线程的一段可以执行的时间。
线程都拥有各自的线程优先级,优先级高的先执行,优先级低的需要等待系统中没有高优先级且可执行的线程存在时才能执行。
频繁进入等待状态的线程(常只占用很少时间)——IO密集型线程
很少进入等待状态的线程——CPU密集型线程
IO密集型线程更容易得到优先级提升。
饿死:一个线程优先级较低,在它执行前总有优先级高的线程试图执行。
线程优先级改变方式

  1. 用户指定优先级
  2. 根据进入等待状态的频繁程度提升或降低优先级
  3. 长时间得不到执行而被提升优先权(一个线程只要等待时间足够长,其优先级可以提高到足够执行的程度,防止饿死)
线程安全

什么是线程安全?

  • 线程安全就是说多线程访问同一代码,不会产生不确定的结果。
  • 编写线程安全的代码是依靠线程同步
  • 线程安全的问题都是由全局变量及静态变量引起的

判断:当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
确保线程安全的方法有这几个:

  • 竞争与原子操作
    多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。
    因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。

  • 同步与锁
    为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
    同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
    二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。

  • 可重入
    一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。
    一个函数要想可重入,需满足:
    温故而知新——程序员的自我修养_第7张图片

  • 过度优化
    在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。
    可以使用volatile关键字试图阻止过度优化,它可以做两件事:
    ● 第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;
    ● 第二,阻止编译器调整操作volatile变量的指令顺序。
    在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。

多线程内部情况

实际用户实际使用的线程不是内核线程,而是存在于用户态的用户线程。用户态多线程库的实现方式:

一对一模型

一个用户使用的线程唯一对应一个内核使用的线程(反过来不一定)。
缺点

  1. 由于许多操作系统限制了内核线程的数量,一对一线程会让用户的线程数量受到限制
  2. 很多操作系统内核线程调度时上下文切换开销大,使用户线程执行效率下降。
多对一模型

多个用户线程映射到一个内核线程上,线程间切换由用户态代码实现,所以线程的切换更快。
问题:一个用户线程被阻塞,所有线程无法执行,内核线程也堵塞
好处:高效的上下文切换和几乎无限制的线程数量

多对多模型

将多个用户线程映射到少数但不止一个内核线程上。解决前两个模型的问题且得到一定的性能提升(没有一对一提升的多)

————————————
淦!以后一定分节慢慢整理

你可能感兴趣的:(#,笔记)