程序员的自我修养-链接、装载与库-1 计算机层次结构
第一部分 简介
计算机的层次结构
中间层:除了硬件和应用程序,其他都是所谓的中间层。
每个中间层都是对他下面一层的包装和拓展。
运行库(Runtime Library):为应用程序提供接口。LInux下GLibc库提供POSIX的API,Windows的运行库提供Windows API(Win32)
硬件:
在UNIX中硬件设备的访问形式跟普通的文件一样;
在Windows中,图形硬件被抽象成了GDI,声音和多媒体设备被抽象成了DirectX对象;磁盘被抽象成了文件系统;
这些东西都交给了操作系统中的硬件驱动程序完成
隔离&地址空间:
我们希望不同程序在运行时,彼此之间内存地址没有重叠的情况,给他一个单一的地址空间好像整个程序独占有整个计算机而不用关心其他的程序(当然程序间的通信部分除外)。地址空间
可以想象成一个很大的数组32位的地址空间:2^32 ,即4GB,地址空间有效的地址是:0~4294967295用十六进制表示就是0x00000000~0xFFFFFFFF。
地址空间:物理地址和虚拟地址空间。比如你的计算机是32位的,它有地址线32条(实际是36条),那么物理空间就是4GB。但是你的计算机只装了512MB的内存,那么其实际的物理地址空间真正有效的部分只有0x00000000~0x1FFFFFFF,其他部分都是无效的。虚拟地址空间是指虚拟的,人们想象出来的 ,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,这样就做到了隔离
分段:
按照上面的分段机制,我们可以将A和B程序的代码空间完全隔离,当A访问的地址超过了0x00A00000的时候硬件就会判断这是一个非法访问,拒绝这个地址请求。
问题:程序局部性原理,当一个程序在运行时,只有一部分数据被频繁的使用,所以可以使用更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,这就是分页(Paging)
分页:
基本方法是把空间分为固定大小的页。比如4KB对于整个操作系统来说,页的大小是固定的。4Gb可以分成1048576个页。
虚拟空间的页叫虚拟页;物理内存中的页叫物理页;磁盘中的页叫磁盘页;
当虚拟空间的页映射到同一物理页的时候,就实现了内存共享。
几乎所有的硬件都是用一个叫MMU(Memory Management Unit)的部件来进行页映射。
线程
线程的是私有空间:栈、线程局部存储(Thread Local Storatge, TLS), 寄存器
当线程个数<CPU核数时,是真正的并行。当多于核数时至少有一个核要执行不止一个线程。这时候就涉及到了线程调度算法:优先级调度和轮转法。
CPU密集型线程 的优先级 低于 IO密集型线程。
Windows上有一系列的线程创建函数,Linux下并不存在真正意义上的线程而是Task。都称为任务。每一个任务概念上都类似于一个单线程的进程。fork可以复制当前进程 exec使用新的可执行印象覆盖当前可知性映像 clone创建子进程并从指定位置开始执行。
fork并不复制原任务内存空间,而采用的是写时复制。
线程安全
原子操作具有很棒的安全性。使用各种锁的操作,就是想让操作编程原子操作。
同步与锁:
每个线程在访问资源前需要先获取锁然后在访问结束之后释放锁。
信号量(Semaphore):一个初始值为N的信号量允许N个线程共享数据(二元信号量:只允许1个线程访问资源)
互斥量(Mutex):类似二元信号。首先是只允许一个线程访问资源。互斥量要求必须由获取资源的线程释放资源。而释放信号量可以由任意线程完成。
临界区(Critical Section):更加严格的同步手段。临界区的作用范围仅限于本进程,其他进程无法获取该锁。
读写锁(Read-Write Lock):依据获取之后“读”or“写”将锁分类。对于同一个锁有两种获取方式:共享的or独占的。
条件变量(Condition Variable):对于条件变量,进程可以:1等待条件变量 2唤醒条件变量。条件变量可以让许多进程一起等待某个事件的发生,当事件发生时所有的线程可以一起回复执行。
过度优化:
因为编译器的优化可能会出现一些线程不安全。比如 寄存器变量 和 指令乱序。
一个指令乱序的eg:单例模式下:
class A{ A* getInstance(){ if(p == NULL){ tmp = new A; barrier(); p = tmp; } return p; } };
p= new A包含三部:1)内存上分配地址 2)调用构造函数 3)给p赋值为地址 其中2,3两步是可能会发生乱序的。放置的办法是 使用 barrier来阻止乱序。
线程模型:
用户定义的线程不一定在内核中对应一个内核态的线程。
1.一对一模型:1个用户使用的线程就唯一对应一个内核使用的的线程(反过来,1个内核里的线程在用户态不一定有对应的线程存在)。
缺点:上下文切换低效,线程数量有限制。
2.多对一模型:将多个用户线程映射到一个内核线程上。
缺点:1个用户线程阻塞导致所有线程都无法执行。 优点:高效的上下文切换和 几乎无限制的线程数量
3.多对多模型:将多个用户线程映射到少数但不止一个内核线程上。