C预处理器将.c文件翻译成.i文件(ASCII码)
处理所有#define,#include等带井号的预编译符号,删除注释(除了#pragma编译器指令),添加行号和文件标示符以便提示错误警告等
C编译器将.i文件翻译成一个.s文件(汇编)
词法分析,语法分析,语义分析,源代码优化,目标代码生成,目标代码优化
汇编器将.s文件翻译成.o文件(可重定位目标文件)
链接器将相关的所以.o文件以及一些必要的系统目标文件组合起来生成一个可执行目标文件,同时要负责符号的解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号最终的内存地址,并修改对那些目标的引用
静态链接:编译阶段将静态库加入可执行文件,文件大,简而言之就是可执行文件包含运行时所需的全部代码,不易于维护,只要库变了,整个可执行文件都得重新链接
动态链接:链接阶段紧紧加入描述信息,运行时在将相应的动态库(也称共享库)加载到内存,只要调用接口不变,库和代码本身就是相互独立的,动态链接还可以分为装载时链接和运行时链接,区别简单来说就是编译器是否知道进程要调用的DLL模块(动态链接库),编译动态库必须是以位置无关代码方式编译-fpic
加载器(exec函数家族来调用)将可执行目标文件中的代码段和数据段从磁盘复制到内存中,然后跳转到入口点(ELF头中)来运行程序
程序运行时内存映像(进程地址空间)—图7-15 栈位于内核内存(不可见)之下,栈的增长是伴随着地址减小,堆的增长伴随着地址的增大,中间还有一块区域是共享库的内存映射区域
打桩机制就是截获动态库函数的调用,转而执行年自己的代码,你可以创建该函数的包装函数,以此可以方便追踪信息
每个进程执行逻辑控制流的一部分,然后被抢占(暂时挂起),然后轮到其他进程执行(时间分片),但看上去是在独占的使用处理器
两个逻辑流在时间上重叠,那么他们就是并发的,当两个流并发的运行在不同处理器核上,那么称他们为并行流
进程为每个程序提供它自己的私有地址空间,和这个空间中某个地址相关联的那个内存自己是不能被其他进程读或者写的
程序初始时都在用户模式,用户模式的进程不允许使用特权指令,进程从用户模式变为内核模式的方法是中断,故障或诸如陷入系统调用这样的异常,异常发生时,控制传递到异常处理程序,处理器从用户模式变为内核模式,处理程序运行在内核模式中,当他返回应用代码时,处理器从内核模式变回用户模式
内核抢占当前进程并重新开始先前被抢占的进程,这中决策叫做调度,由内核中的调度器处理。在内核调度了一个新的进程运行后他就抢占当前进程(旧的),并使用上下文切换机制将控制转移到新的进程,保存当前进程的上下文
如果系统调用因为等待事件发生而阻塞,内核可以让当前进程休眠切换到另一进程,就发生了上下文切换 图8-14
主机中的所有进程共享主存(内存),这就伴随这很多问题的出现,比如进程太多需要太多主存时,许多进程实际上就不能得到有效的运行。内存容易被破坏,如果多个进程一起工作时,某个进程王另一个进程的内存中写入了东西,则会另一个进程就会感觉发生了莫名其妙的错误,为了解决多个进程使用内存出现的这些问题,就要用到虚拟内存
虚拟内存机制将主存看成是一个存储在磁盘上的地址空间的高速缓存,主存只保存活动区域,根据需要在磁盘和主存之间来回传递数据,这样便高效的使用了主存,并且他为每个进程提供了一致的地址空间,从而简化了内存管理,保护了每个进程的地址空间不被其他进程破坏
页面替换算法 :
最佳置换算法(OPT)
先进先出(FIFO)页面置换算法
最近最久未使用(LRU)置换算法
时钟(CLOCK)置换算法
内存维护一个页表,在物理内存和虚拟内存之间扮演一个桥梁的作用,页表中的每个页可能不为空也可能为空,为空表示该虚拟页还未被分配,而每个虚拟页还有一个有效位,当页不为空时同时标记为1时,该页的地址字段就表示主存中相应的物理页的起始地址,如果未被标记,则地址字段就表示虚拟内存(磁盘)中相应的起始地址 ——–图9-4
如果发生缺页(主存不命中),则将虚拟内存中的相应的条目替换到主存中并将页表中对应的页标记为1
分配页面时将页表中某个地址位null的页的地址替换为其在虚拟内存中的起始地址
整个程序在运行过程中需要引用的不同页面很可能超过物理内存的总大小,这也许会导致缺页的频繁发生,导致效率变低,但是有个原则叫做局部性原则,它保证了在任意时刻,程序将趋向与在一个较小的活动页面集合上工作,这个集合叫做工作集(常驻集合)
每个进程都有一个独立的虚拟地址空间,并且不同虚拟地址空间中的页可以映射同一个物理页,这个页叫做共享页面
如果我们仅仅是使用一个单独的页表来进行地址翻译,那么有时将会导致页表的占用太多的内存空间(页表总是驻留在内存中),假设地址空间为32位,页面位4kb,每个页表条目4字节,那将会需要2^32 / 2^12 * 4字节也就是4mb的页表,然而如果是64位呢,页表将会更大,于是我们引入了多级页表一级页表映射多个二级页表,二级页表中的二级页表中的PTE映射页面,此时只有一级页表是总驻留在主存中的,根据需要再创建,调入或调出2级页表,当然最经常使用的二级页表是会驻留在主存中的
(Linux)虚拟内存区域与一个磁盘上的对象(普通文件或匿名文件)关联起来,以初始化虚拟内存区域的内容,这个过程叫做内存映射
当运行是需要额外虚拟内存时可以使用mmap和munmap来创建和删除虚拟内存,也可以使用方便的动态内存分配器来分配管理,动态内存分配器可以分为显示分配器(C的malloc和free等,c++的new和delete等)和隐式分配器(也叫垃圾收集器,代表:JAVA)
分配器必须立即响应分配请求,不能为了提高效率重新排列或者缓冲请求
碎片分为内部碎片和外部碎片
内部碎片 是指分配的区域大于其有效载荷,比如申请了8个字的空间而实际上8个字的空间并没有完全用上,可能是因为使用者没有正确估量自己所需要的实际大小,也可能是因为需要内存对齐等导致有些空间用不上
外部碎片 常常是因为一个并不大的空间被申请了一个更小的空间,而导致这个并不大的空间剩下的区域已经不能满足分配需要,导致这块剩下的小区域就没用了,除非其前面或后面的区域被释放,然后这块小区域和刚释放的区域合并,至于如何合并就又要引入一个概念叫 边界标记
一块连续的堆空间中,以分配的块是用链表链接起来的,合并下一个块很简单,将指向下一个块头部的指针指向下下个块头部就行,那么上一个块需要合并呢,于是便需要引入边界标记 图9-39
堆空间中空闲的区域总是零散的,并且有大有小,并不能保证当前空闲区域大小等于所需要申请的大小,如果盲目的分配则会导致碎片等,如果挨个比较选取最优的那么效率又会很低,如何抉择呢?
隐式空闲链表
——–首次适配
——–下一次适配
——–最佳适配
——–最坏适配
显示空闲链表 将空闲块组织成显式数据结构–双向链表来维护
——–后进先出的顺序维护,将新释放的块放置在链表开始处,结合首次适配使用
——–按地址顺序维护,链表每个块的地址都小于其后继块的大小
分离存储的空闲链表 单向空闲链表的分配效率常常跟空闲块数量成线性相关,如果我们将大小大致相同的空闲块归为同一个大小类,每个大小类用一个单向链表维护,然后用一个数组来维护多个空闲链表
垃圾收集器 像java之类的语言使用叫做垃圾收集器的动态内存分配器来分配内存,他严格精确的集iang内存视为一张有向图,并区分出可达节点和不可达节点,而不可达节点将被视为垃圾,从而精确的回收所有垃圾,像C语言这样的语言也有垃圾收集器,不过它并不能像JAVA之类的语言维持可达图的精确表示,这样的垃圾收集器叫做 保守的垃圾收集器 所以还是需要我们手动的控制