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

第一章:温故而知新

站得高,望的远

开发工具和应用程序属于同一个层次---操作系统应用程序编程接口

应用程序接口的提供者是运行库

运行库使用操作系统提供的系统调用接口,系统调用接口在实现中往往以软件中断的方式提供

操作系统内核层对于硬件层来说是硬件接口的使用者,而硬件是接口的定义者,硬件接口决定了操作系统内核,具体来说就是驱动程序如何操作硬件,如何与硬件进行通信

操作系统做什么

操作系统的一个功能就是提供抽象的接口,另外一个主要功能是管理硬件资源

不要让CPU打盹

多道程序:

当某个程序暂时无须使用CPU时,监控程序就把另外的正在等待CPU资源的程序启动,使得CPU能够充分利用起来

分时系统:

每个程序运行一段时间以后都主动让出CPU给其他程序,使得一段时间内每个程序都有机会运行一小段时间

多任务系统:

操作系统接管了所有的硬件资源,并且本身运行在一个受硬件保护的级别。所有的应用程序都以进程的方式运行在比操作系统权限更低的级别,每个进程都有自己独立的地址空间,使得进程之间的地址空间相互隔离。CPU由操作系统统一进行分配,每个进程根据进程优先级的高低都有机会得到CPU,但是,如果运行时间超出一定的时间,操作系统会暂停该进程,将CPU资源分配给其他等待运行的进程。这种CPU的分配方式即所谓的抢占式,操作系统可以强制剥夺CPU资源并且分配给他认为目前最需要的进程。如果操作系统分配给每个进程的时间都很短,即CPU在多个进程间快速切换,从而造成了很多进程都在同时运行的假象

设备驱动

操作系统作为硬件层的上层,它是对硬件的管理和抽象。对于操作系统上面的运行库和应用程序来说,它们希望那个看到的是要给统一的硬件访问模式

这些繁琐的硬件细节全部都交给了操作系统,具体地讲是操作系统中的驱动程序来完成。驱动程序可以看作操作系统的一部分,他往往跟操作系统内核一起运行在特权级,但它又与操作系统之间有一定的独立性,使得驱动程序有比较好的灵活性

操作系统的开发者为硬件厂商提供一系列的接口和框架,凡是按照这个接口和框架开发的驱动程序都可以在该操作系统上使用

内存不够怎么办

为什么需要虚拟地址?

 1)地址空间不隔离 

所有程序都直接访问物理地址,程序所使用的内存空间不是相互隔离的,恶意的程序可以很容易改写其他程序的内部数据,以达到破坏的目的

有些非恶意的程序可能不小心修改了其他程序的数据,就会使其他程序也可能崩溃

用户希望他是使用计算机的时候,其中一个任务失败了,至少不会影响其他任务

2)内存使用效率低

内存空间不够,将其他程序的数据暂时写到磁盘里,等到需要用的时候再读回来,整个过程中有大量的数据在换入换出,导致效率十分低下

3)程序运行的地址不确定

程序每次需要装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域的位置是不确定的,这给程序的编写造成了一定的麻烦,因为在程序编写时,他访问数据和指令跳转时的目标地址很多都是固定的

妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另外一个程序相互不重叠,以达到地址空间隔离的效果

关于隔离

物理地址空间是实实在在的,存在于计算机中,而且对于每一台计算机来说只有唯一的一个,可以把物理空间想象成物理内存

虚拟地址空间是指虚拟的,人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟地址空间,而且每个进程只能访问自己的地址空间,这样就有效地做到了进程的隔离

分段

分段基本思路是把一段与程序所需的内存空间大小的虚拟空间映射到某个地址空间。假设有一个地址从0x00000000到0x00A00000的假象空间也就是虚拟空间,然后从实际的物理内存中分配一个相同大下的物理地址,然后再把这两块相同大小的地址空间一一映射,即虚拟空间中的每个字节对应于物理空间中的每个字节,这个映射由软件来设置。实际的地址转换由硬件完成,CPU会将这个地址转换成实际的物理地址

分段解决了地址空间隔离 和 程序运行的地址不确定 这两个问题

分页

进程的虚拟地址空间按页分割,把常用的数据和代码页装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘里取出来即可

虚拟空间的有些页被映射到同一个物理页就可实现内存共享

虚拟页不在内存中,但是进程需要用到这个页,硬件会捕获这个消息,就是所谓的页错误,然后操作系统接管进程,负责将需要的页从磁盘中读出来并且装入内存,然后将内存的这个页与虚拟页之间建立映射关系

CPU发出的是虚拟地址经过MMU转换后就变成了物理地址。一般MMU都集成在CPU内部中,不会以独立部件出现

分页解决了内存效率低的问题

众人拾柴火焰高

线程基础

线程,有时被称为轻量级进程,是程序执行流的最小单位。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。一个进程由一个或多个线程组成,各个线程之间共享程序的内存空间(包括代码段,数据段,堆等)及一些进程级的资源(如打开文件和信号)

多个线程可以互不干扰地并发执行,并共享进程地全局变量和堆的数据

使用多线程的原因

1)某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程可以有效利用等待的时间

2)某个操作(常常是计算)会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会中断。多线程可以让一个线程负责交互,另一个线程负责计算

3)程序逻辑本身就要求并发操作

4)多CPU或多核计算机,本身具备同时执行多个线程的能力,因此单线程程序无法全面发挥计算机的全部计算能力

5)相对于多进程应用,多线程在数据共享方面效率要高很多

线程的访问权限

线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈,但实际情况也拥有自己的私有存储空间

1)栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)

2)线程局部存储(TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量

3)寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有

线程调度

在单处理器对应多线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多线程程序轮流执行,每次执行一小段时间,这样每个线程就看起来在同时执行。不断在处理器上切换不同的线程的行为称为线程调度

线程的状态

运行:此时线程正在执行

就绪:此时线程可以立刻运行,但CPU已经被占用

等待:此时线程正在等待某一事件(通常是I\O或同步)发生,无法执行

处于运行中线程拥有一段可以执行的时间,这段时间被称为时间片,当时间片用尽的时候,该线程将进入就绪状态。如果在时间片用完之前进程就开始等待某事件,那么它将进入等待状态。每当一个线程离开运行状态时,调度系统就会选择一个其他的就绪线程继续执行。在一个处于等待状态的线程所等待的事件发生之后,该线程将进入就绪状态

线程的调度方式

轮转法

让各个线程轮流执行一小段时间的方法,这决定了线程之间交错执行的特点

优先级调度

决定了线程按照什么顺序轮流执行,在具有优先级调度的系统中,线程都拥有各自的线程优先级,具有高优先级的线程会更早地执行,而低优先级的线程常常要等待到系统中已经没有高优先级的可执行的线程存在时才能执行

线程的优先级不仅可以由用户手动设置,系统还会根据不同线程的表现自动调整优先级,使得调度更有效率

IO密集型线程总是比CPU密集型线程容易得到优先级的提升

优先级改变的三种方式

1)用户指定优先级

2)根据进入等待状态的频繁程度提升或降低优先级

3)长时间得不到执行而被提升优先级

可抢占线程和不可抢占线程

抢占:线程在用尽时间片之后会被强制剥夺继续执行的权力,而进入就绪状态,之后别的线程抢占了当前线程

在不可抢占线程中,线程主动放弃执行无非两种情况

1)当线程试图等待某事件时(I\O等)

2)线程主动放弃时间片

在不可线程执行执行时,有一个显著的特点那就是线程的调度时机是确定的,线程调度只会在线程主动放弃执行或线程等待某事件时。这样可以避免一些因抢占式线程里调度时机不确定的问题

Linux的多线程

fork函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数里返回

在fork函数调用后,新的任务将启动并和本任务一起从fork函数返回,但不同的是本任务fork将返回新任务pid,而新任务的fork将返回0

fork产生新任务的速度非常快,因为fork并不复制原任务的内存空间,而是和原任务一起共享一个写时拷贝的内存空间。所谓写时拷贝,指的是两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响其他的任务使用

fork只能够产生本任务的镜像,因此需用exec配合才能启动别的新任务。exec可以用新的可执行映像替换当前的可执行映像,因此在fork产生一个新任务之后,新任务可以调用exec来执行新的可执行文件

使用clone可以产生一个新的任务,从指定的位置开始执行,并且共享当前进程的内存空间和文件等。如此就可以在实际效果上产生一个线程

线程安全

多线程程序处于一个多变的环境中,可访问的全局变量和堆数据随时都可以被其他的线程改变,因此多线程程序在并发时数据的一致性变得非常重要

自增操作在多线程环境下会出现错误是因为这个操作被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码。单指令的操作称为原子的,因为无论如何,单条指令的执行是不会被打断的

原子操作指令仅适用于比较简单特定的场合,在复杂的场合下,原子操作指令就力不从心了

同步与锁

所谓同步,即是在一个线程访问数据末结束的时候,其他线程不得对同一个数据进行访问,如此,对数据的访问就被原子化了

同步的最常见的方法是使用锁。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁。在锁已被占用的时候试图获取锁,线程会等待,直到锁重新可用

二元信号量

二元信号量是最简单的一种锁,它只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放

对于允许多个线程并发访问的资源,多元信号量简称信号量,它是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:

1)将信号量的 -1

2)如果信号量的值 < 0,则进入等该状态,否则继续执行

访问完资源之后,线程释放信号量,进行如下操作:

1)将信号量的值+1

2)如果信号量的值 < 1,唤醒一个等待中的线程

同一个信号量可以被系统中的一个线程获取之后,由另一个线程释放

互斥量

互斥量要求那个线程获得互斥量,那个线程就要负责释放这个锁,其他线程越俎代庖去释放互斥量是无效的

临界区

临界区是比互斥量更加严格的同步手段。把临界区的锁的获取称之为进入临界区,而把锁的释放称之为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的,就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质

读写锁

信号量,互斥量,临界区中任何一种来进行同步,尽管可以保证程序正确,但对于读取频繁,而仅仅偶尔写入的情况,会显得效率低效,读写锁可以避免这个问题

条件变量

条件变量作为一种同步手段,对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次,线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行

可重入与线程安全

一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部因素,又一次进入该函数执行

1)多个线程同时执行这个函数

2)函数自身(可能是经过多层调用之后)调用自身

一个函数要成为可重入的,必须具有以下特点:

1)不使用任何(局部)静态或全局的非const变量

2)不返回任何(局部)静态或全局的非const变量的指针

3)仅依赖于调用方提供的参数

4)不依赖于单个资源的锁

5)不调用任何不可重入的函数

可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用

过度优化

CPU发展出了动态调度,在执行程序的时候为了提高效率有可能会交换指令的顺序,同样,编译器在进行优化的时候,也可能为了效率而交换毫不相干的两条指令的执行顺序

使用volatile关键字试图阻止过度优化

1)阻止编译器为了提高速度将一个变量缓存到寄存器内不写回

2)阻止编译器调整操作volatile变量的指令顺序

阻止CPU换序可使用barrier指令

多线程内部情况

一对一模型

对于直接支持线程的系统,一对一模型是最为简单的模型。对一对一模型来说,一个用户使用的线程是唯一对应一个内核使用的线程(反过来不一定)

这样用户线程就具有了和内核线程一致的优点,线程之间的并发是真正的并发,一个线程因为某原因阻塞时,其他线程不会受到影响。一对一模型可以让多线程程序在多处理器系统上有更好的表现

一般直接使用API或系统调用创建的线程均为一对一的线程

缺点:

1)由于许多操作系统限制了内核线程的数量,因此一对一线程会让用户的线程数量受到限制

2)许多操作系统内核线程调度时,上下文切换的开销比较大,导致用户线程的执行效率下降

多对一模型

多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此相对于一对一模型,多对一模型的线程切换要快速许多

缺点:

1)其中一个用户线程阻塞,那么所有的线程都将无法执行,因为此时内核里的线程也随之阻塞了

2)在多处理系统上,处理器的增多对多对一模型的线程性能也不会有明显的帮助

多对一模型的好处是高效的上下文切换和几乎无限制的线程数量

多对多模型

多对多模型结合了多对一模型和一对一模型,将多个用户线程映射到少数但不止一个内核线程上

在多对多模型上,一个用户线程阻塞并不会使得所有的用户线程阻塞,因为此时还有别的线程可以被调度来执行。多对多模型对用户模型的数量没有什么限制,在多处理系统上,多对多模型的线程也能得到一定的性能提升,不过提升的幅度不如一对一模型高

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值