第一章
- 接口:
- 应用程序使用的是操作系统应用程序编程接口,由运行库提供,例如Linux下Glibc的POSIX的API,Windows提供的WIndows API
- 运行库使用OS提供的系统调用接口,在实现层面由软件中断提供,如Linux使用0x80号中断作为系统调用接口,Windows使用0x2E号中断作为系统调用接口
- OS内核与硬件层的接口为硬件规格
- 操作系统的两个主要功能是提供接口和管理资源
- 现代OS是一种多任务系统,OS管理所有硬件资源,本身运行在一个受硬件保护的级别。所有应用程序都以进程的形式运行在一个比OS更低的级别,CPU由OS统一分配
- 硬件驱动与OS一起运行在特权级,但是于内核有一定独立性。例如在UNIX中,硬件被抽象为普通的文件;Windows中,图形硬件被抽象为GDI,声音和多媒体设备被抽象为DirectX对象,磁盘被抽象为文件系统
- 分段和分页
- 线程 Thread
- 一个标准的线程由线程ID、当前指令指针PC、寄存器集合和堆栈构成
- - 线程可以访问进程在内存里的所有数据,包括其他线程的堆栈
- 单处理器运行多线程时,需要线程调度。一个线程可以有三种状态
- 运行
- 就绪
- 等待
运行状态的线程拥有一个时间片,用尽后进入就绪状态,若未用尽就在等待某事件,则进入的等待状态。每当一个线程离开运行状态,调度系统就会选择一个其他的就绪线程继续执行。线程拥有自己的优先级,某些OS会根据线程优先级来优先调度高优先级线程进入运行状态。通常,I/O密集型线程比CPU密集型线程优先级高。一个长事件得不到调度的线程称为饿死 Starvation
- 抢占与不可抢占:
在可抢占系统中,线程用尽时间片被强制剥夺执行权,进入就绪状态这一过程即抢占 Preemption
在不可抢占系统中,线程主动放弃执行只有两种情况- 线程等待I/O
- 线程主动放弃时间片
- Linux的多线程
Linux将所有执行实体(进程和线程)都称之为任务 Task,每个任务都类似一个单线程进程,具有内存空间,执行实体,文件资源等。但Linux下不同任务可以共享内存空间,多个共享同一内存空间的多个任务就构成了一个进程。
Linux中管理任务的方式如下:
- fork:产生一个和当前进程完全一样的新进程,老进程和新进程一块从fork返回,但老进程返回新任务的pid,新任务返回0
- exec:使新的可执行映像覆盖当前的可执行映像,fork新任务之后,可以用exec执行新的可执行文件
- clone:创建子进程并从指定位置开始执行
- 线程安全
- 原子操作
- 同步与锁:
- 锁
- 信号量
- 互斥量
- 临界区
- 条件变量:对于条件变量,线程有两种操作:等待和唤醒。一个条件变量可以被多个线程等待,唤醒会使所有等待在该条件变量上的线程全部唤醒
- 读写锁:有两种获取方式,共享Shared和独占Exclusive,在共享锁已被获取的状态下,其他线程依然可以以独占方式获得锁;但是想以独占方式获得锁,只能等待锁被所有线程释放
读写锁状态 | 以共享方式获取 | 以独占方式获取 |
---|---|---|
自由 | 成功 | 成功 |
共享 | 成功 | 等待 |
独占 | 等待 | 等待 |
- 可重入 Reentrant
重入:一个函数被重入,表示该函数没有执行完成,由于外部因素或内部调用,又一次进入该函数。函数被重入的情况:- 多个线程同事执行这个函数
- 函数递归调用自身
一个函数是可重入的代表这个函数被重入后不会产生任何不良后果,可重入函数包含以下几个特点: - 不适用任何局部静态或全局非const变量
- 不返回任何局部静态或全局的非const变量
- 仅依赖于调用方提供的参数
- 不依赖任何单个资源的锁
- 不调用任何不可重入的函数
可重入函数可以在多线程环境下使用
- 三种线程模型
- 内核线程由多核CPU或调度算法实现并发,而用户线程不一定与内核线程有一对一关系,例如可能三个用户线程对于内核来说只有一个线程
- 一对一模型:
一个用户态线程对应一个内核态线程。此模型下用户态线程可以做到真正的并发。Linux中利用clone产生的线程就是一对一线程 - 多对一模型
多个用户态线程被映射到同一个内核线程,线程间的切换由用户态代码进行,其中一个用户态线程阻塞,其他线程都无法运行,因为内核线程也被阻塞了 - 多对多模型
多个用户态线程被映射少数但不止一个内核态线程上,其中一个用户态线程的阻塞不会影响其他线程
第二章
- 编译的4个步骤
- 预处理
- 删除所有#define,并且展开所有宏定义
- 处理所有条件预编译指令,诸如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”
- 处理所有#include预编译指令,将头文件插入到预编译指令的位置(递归进行,也会处理头文件里的头文件)
- 删除所有注释 “//”、“/* */”
- 添加行号,便于编译器调试、报错
- 保留所有#pragma编译器质量你个
- 编译
- 将预处理完的文件进行一系列词法分析、语法分析、语义分析、优化,生成相应的汇编代码
- 汇编
- 将汇编代码转为机器码
- 链接
- 编译器是一个将高级语言翻译成机器语言的一个工具,从源代码到目标代码,可以看作经历了6步:扫描、语法分析、语义分析、源代码优化、代码生成核目标代码优化
- 词法分析:源代码输入扫描器,运用一种类似有限状态机的算法将源代码字符序列分割为一系列记号 Token,大致分为几类:关键字、标识符、字面量(数字、字符串 etc.)、特殊符号(加号、等号 etc.)
- 语法分析:对Token进行语法分析,产生语法树,采用上下文无关语法的分析手段。语法树是以表达式为节点的树
- 语义分析:由语义分析器完成,编译器能分析的语义是静态语义,是编译期可以确定的语义,与之对应的是动态语义,即运行期才能确定的语义。静态语义通常包括声明和类型匹配、类型转换
- 中间语言生成:源码级优化器在源码级别进行优化,将语法树转换为中间代码
- 目标代码生成和优化,使用代码生成器和目标代码优化器
- 模块之间的互相通信有两种方式:模块间的函数调用和模块间的变量访问
- 静态链接
链接的过程主要包括地址和空间分配、符号决议、重定位等
基本的静态链接:目标文件.o或.obj文件和库一起链接形成最终的可执行文件,最常见的库是运行时库,库其实是一组目标文件的包
第三章
- 可执行文件、目标文件、静态链接库、动态链接库都可以按照可执行文件格式存储,均为Linux的**ELF (Executable Linkable Format)**格式文件
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件 Relocatable File | 静态链接库、目标文件 | .o(Linux)、.obj(Windows) |
可执行文件 Executable File | 无扩展名 可直接执行 | bash文件、.exe(Windows) |
共享目标文件 Shared Object File | 一种是链接器可以使用这种文件链接其他文件 另一种是动态链接器可以将几个这种目标文件和可执行文件结合,作为进程映像的一部分来运行 | .so(Linux)、DLL(Windows) |
核心转储文件 Core Dump File | 进程意外终止时,系统可以将该进程地址空间的内容和其他信息转储到这种文件 | core dump(Linux) |
- 目标文件
目标文件中包含编译后的机器指令代码、数据和链接所需的信息,一般这些信息以节 Section或段 Segment存储。
源代码编译后的机器指令码被放在代码段 Code Segment,全局变量和局部静态变量被放在数据段 Data Segment。
指令和数据分段存储的优势:
- 程序装载后,数据和指令被映射到两个虚拟内存区域,数据区域是可读写的,指令区域是只读的,防止指令被改写
- CPU的缓存 Cache体系被设计为数据缓存和指令缓存分离,程序的指令和数据分离可以提高缓存命中率
- OS中运行多个程序的副本时,指令只需要保存一份
- 段表 Section Header Table
保存段的基本属性:段名、段长、偏移、读写权限、其他属性 - 重定位表 Relocation Table
链接器在处理目标文件时,某些部位必须被重定位,这些信息记录在重定位表里 - 字符串表
- 链接与符号
例如,目标函数B要用到目标函数A中的函数foo,则A 定义 Define了foo,B 引用 Reference了foo,变量同理。链接中,变量和函数统称为符号 Symbol,他们的名字成为符号名 Symbol Name
链接过程需要基于符号来完成,每个目标文件都有一个符号表,记录了目标文件所用到的所有符号,每个定义的符号都有一个对应的符号值 Symbol Value,对于符号,符号值就是他们的地址,符号可以分为以下几类:
- 定义在本目标文件的全局符号,可以被其他文件引用
- 在本目标文件中引用的全局符号,未定义在本目标文件中:外部符号 Extern Symbol
- 段名
- 局部符号,仅在编译单元内部可见
- 行号
- 符号修饰与函数签名
Unix下的C语言规定,C语言源代码文件中所有全局变量和函数编译后,对应的符号名前面会加上下划线“_”,以区别其他语言编写的文件,但是无法解决同一种语言多个模块间的冲突,C++针对此引入了命名空间 Namespace
C++的符号修饰:符号修饰 Name Decoration、符号改编 Name Mangling
C++允许函数静态重载,也支持命名空间,即在不同名程空间可以拥有多个同名符号,所引入了函数签名 Function Signature,函数签名包含一个函数的信息:函数名、参数类型、所在的类和名称空间。编译器与链接器在处理符号时,使用名称修饰方法,使得每个函数签名对应一个修饰后名程 Decorated Name。所以C++源代码编译后的目标文件使用的都是修饰后名称,编译器和链接器对于不同函数签名的函数,无论函数名是否相同,都看作不同的函数。 - extern “C”
C++为了兼容C语言,引入了一个关键字“extern C”,在其后的大括号里的代码会被当做C语言处理,在这里,C++名称修饰机制将不起作用
例如:
extern C{
int func(int);
int var;
}
声明了一个函数func,定义了一个变量var。在Visual C++下,会将这两个符号名前加上下划线"_"进行修饰,Linux的GCC下编译器不会修饰。
一个头文件中声明了C语言的函数和全局变量,这个头文件可能会被C语言代码和C++代码包含,例如C库string.h中的memset函数,原型如下:
void *memset(void *,int,size_t)
若不加任何处理,C程序包含string.h头文件时,可以正确处理,C++则不然,必须用extern C声明memset,但因为C不支持extern C语法,则需要使用宏“__cplusplus”,C++编译器在编译C++时会默认定义这个宏,来表征这是一个C++文件,所以我们可以用条件宏来判断是否C++代码:
#ifdef __cplusplus
extern C{
#end if
void *memset(void *,int,size_t)
#ifdef __cplusplus
}
#endif
- 弱符号与强符号
例如在目标文件A、B中都定义全局整型变量,则链接器链接A、B时会报错,这种符号即强符号。C/C++编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为若符号 。链接器的处理规则如下:
- 不允许强符号多次定义
- 若一个符号在某个目标文件中为强符号,其他目标文件中为若符号,则选择强符号
- 若一个符号在所有目标文件中均为弱符号,选择占用空间最大的一个
强引用与弱引用: