程序员的自我修养 学习笔记(4)
可执行文件只有装载到内存以后才能被CPU执行。程序就将是菜谱,CPU就像是厨师,计算机的其他硬件就像是厨具,整个炒菜的过程就是一个进程。同样的一份菜谱,不同人可以做出来不同的味道。这个类比真是巧妙。
Linux下面,进程最大使用3G的虚拟空间
Windows下面,进程最大使用2G的虚拟空间
现在计算机,配置超过4G的内存的电脑已经不是不可能了,在这种情况下,32位CPU能够访问到大于4G的空间吗?如果此空间指的是虚拟地址空间,由于32位CPU的指针只能是32位,最大寻址范围是0~4GB。如果此空间指的是实际内存空间,Intel的Pentium Pro CPU采用36位物理地址,可以访问高达64G的物理内存,这种机制叫做PAE(Physical Address Extension)。
覆盖装入(Overlay):程序员必须手工将程序分割成若干块,然后编写一个小的辅助代码来管理这些模块何时应该驻留内存、何时应该被替换掉。
页映射(Paging):是虚拟存储机制的一部分。
创建一个进程:
1. 创建一个独立的虚拟地址空间
2. 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
3. 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
静态链接存在空间浪费和更新困难两个因素。
1. 如果每个程序都静态链接,那么在多个程序执行时,就会存在很多冗余的副本
2 如果静态链接库中有某一个模块更新了,那么所有基于此库的程序都要重新编译、下载更新一次,对用户来说非常不方便。
解决这个问题的方法,是动态链接。要做到动态链接,就要把程序的模块相互分割开来,形成相互之间较独立的文件,而不再将它们静态地链接在一起,简单的讲,在程序编译时,不进行链接,在实际运行时,才进行链接。
共享对象在编译时,不能假设自己在进程虚拟地址空间中的位置,可执行文件基本可以确定自己在进程虚拟空间地址中的起始位置,因为可执行文件往往是第一个被加载的文件,它可以选择一个固定空闲的地址,比如Linux下,一般是0x8040_0000,WIndows下面一般是0x0040_0000
malloc申请的内存位于堆空间内。malloc是怎么实现的呢?从简单的思路来想,把进程的内存管理全部交给操作系统内核来处理,操作系统提供系统调用,供程序申请堆内存使用。简单是简单,考虑到堆申请操作比较频繁,系统调用开销较大,效率不高,因此,需要改进。怎么改进呢?
书中给出了一个很好的类比,程序向操作系统”批发”一块较大的堆空间,由运行库来管理“批发来的”堆空间,对于同一个商品,只能暂时借给一个客人,存放商品的货柜是一一对应的,哪儿借出的商品,归还时,就还到哪里去(从1号仓库中借出的商品,归还时要还到1号仓库中去),一个借出对应一个归还,不能乱套,不能有借无换。
Linux下的堆管理,系统提供了两个系统调用接口,brk和mmap。
brk的原型为: int brk(void* end_data_segment)
实际作用是设置进程数据段的结束地址(可以扩大和缩小数据段),增大结束地址,增大的那部分空间就可以被我们作为堆空间来使用。
mmap,向操作系统申请一段虚拟地址空间,这段空间可以映射到某个文件,此时,称为文件映射。如果映射到其他地方,则称为匿名空间,可以作为堆使用。
mmap原型:void *mmap(
void* start, //申请空间的起始地址(设为0,则自动选择)
size_t length, //申请空间的长度
int prot.. //申请空间的权限
int flags, //映射类型(文件映射、匿名空间等)
int fd, //用于文件映射的指定文件描述符和文件偏移
off_t offset);
Windows的进程,将地址空间分配给了各种EXE、DLL文件、堆、栈。每个线程的栈都是独立的,一个进程中有多少个线程,就应该有多少个对应的栈。对于WIndows来说,每个线程默认的栈大小是1MB。
堆管理算法:空闲链表、位图分配,对象池(假设每次请求的都是一个固定的大小)。在很多现实应用行,堆分配算法往往是采取多种算法复合而成的。比如,对于glibc来说,小于64个字节的空间,采用的类似于对象池的方法,对大于512字节的空间申请,采用的最佳适配算法,对于大于64个字节而小于512个字节的,它会根据情况采取最佳折中策略。对于大于128K的申请,会使用mmap机制向操作系统来申请。