可执行文件
在UNIX中,任何文件都可以通过简单的 chmod +x 命令标记为可执行文件。但是不能保证文件可执行,这个标记只是告诉操作系统内核将这个文件读入内存,然后寻找一个头签名,又称为“魔数(magic)”,据此可以确定精确的可执行格式。
OS X支持的可执行格式:
- 解释器脚本格式:一种特殊形式的二进制文件格式,这种文件指向的文件才是真正得到执行的文件
- 通用二进制格式:这种文件包含一个简单的文件头,文件头后面依次拷贝了每一种支持架构的二进制文件,因此通用二进制往往非常庞大,因此又叫“胖二进制”
- Mach-O二进制格式:OS X独有的二进制格式。Mach-O文件头的主要功能在于加载命令(load command)。加载命令紧跟在文件头后,文件头中的两个字段:ncmds和sizeofncmds 是用于解析加载命令
动态库
Mach-O镜像中有很多对外部的库和符号的引用。由动态链接器来绑定,动态链接器是内核执行LC_DYLINKER 加载命令时启动的,通常使用的是/usr/lib/dyld 作为动态链接器,链接器接管刚创建的进程的控制权,因为内核将进程的入口点设置为链接器的入口点
共享库缓存
共享库缓存(shard library cache)是dyld 支持的另外一个机制。共享库缓存指的是一些库经过预先链接,然后保存在磁盘上的一个文件中。共享缓存在iOS 中尤为重要,在iOS 中大部分常用库都被缓存了
库的运行时加载
OS X 中提供的运行时动态库加载API类似于POSIX提供的API,但是OS X 的实现完全不同:
- dlopen(const char *path)用于寻找和加载指定路径的库或bundle
- dlopen_preflight(const char *path)是Leopard之后OS X提供的扩展,用于模拟dlopen( )的加载过程,但不是真正加载任何东西
- dlsym(void *handle, char *sym)用于定位之前通过dlopen( ) 打开的句
柄中的符号 - dladdr(char *addr, DL_Info *info)通过bundle名称或地址addr处的库填充DL_Info结构体,这个函数和GNU扩展一致
- 在其他函数发送错误时,dlerror( )用于提供一条错误信息
Cocoa 和 Carbon 为dl* 系列函数提供了高级包装,以及CFBundle/NSBundle 对象,用于加载Mach-O bundle文件
dyld 的特性
- 两级名称空间:指的是符号名称还包含其所在库的信息
- 函数拦截:DYLD_INTERPOSE宏定义允许一个库将其函数替换一个函数的实现
进程
和其他任何抢占式的多任务操作系统一样,进程作为一个正在执行的程序的是实例是UNIX的一个基本概念。这个实例可以通过Progress ID(进程ID,即PID)来唯一辨识。进程还会将其和父进程的亲属关系保存在父进程ID(Parent Progress ID, PPID)中。父进程可以通过fork(或通过posix_spawn)创建子进程,并且预期子进程会消亡。UNIX进程的生母意义就是运行,然后在运行结束之后返回一个整数。子进程返回的整数由其父进程手机。进程将要返回的值传递给exit( 2)系统调用(或者重main( )函数中返回)
进程的生命周期
僵尸状态:当生命周期超过子进程,但是仍然抛弃自己的子进程转移到其他事情的父进程会使得其子进程处于半死不活的僵尸状态
进程地址空间
用户态的一个优点在于虚拟内存的隔离,进程独享一个私有的地址空间,在iOS 上范围2~3GB,在32为OS X 上为4GB,在64位的OS X 上则为不可想象的16EB
-
进程入口点
和所有标准的C语言程序一样,OS X 也有一个标准的入口点,默认为“main”。不过除了三个标准参数:argc、argv 和envp外,Mach-O 还接受第四个参数名为“Apple”的char **。Cocoa 应用程序的标准入口也是C main( ),不过常见做法是将main( ) 实现为NSApplicationMain( )的包装,通过NSApplicationMain( )进入Objective-C的编程模型 -
地址空间布局随机化
进程在自己私有的虚拟内存空间中启动。现在在大部分操作系统中都采用了一种称为地址空间布局随机化(ASLR)技术,进程每一次启动时,地址空间都会被简单地随机化,只是偏移基本的布局,程序文本、数据结构仍然是一样的,实现方法是通过内核将Mach-O 的段“平移”某个随机系数 -
32位地址空间(Intel)
32位地址空间在4GB大小(232字节)然而和其他操作系统的不同之处在于整个4GB的地址空间都是在用户空间访问的,没有预留的内核空间。 -
64位地址空间
64位地址空间允许高达16EB(16GGB)的巨大地址空间,但是大部分硬件架构只支持48位到52位寻址,由于虚拟地址到物理地址转换的开销,Intel 架构只使用了48位的虚拟地址,因为用户内存空间可以访问的区域最高到0x7FFF FFFF FFFF。64位地址空间和传统的OS X 模型的不同之处在于传统的OS X 内存模型中内核有自己的地址空间,但是新的地址空间允许更快速的用户态/内核态的切换(共享CR3寄存器,CR3寄存器包含了页表指针的控制寄存器) -
32位地址空间(iOS)
iOS 的地址空间32位Intel地址空间更为受限,首先和32位 OS X 不同的地方在于iOS 的内核映射在用户地址空间的0xC000 0000(iOS 3) 或 0x8000 0000(iOS 4 和 5),占用1~2GB的空间。此外,0x3000 0000 以上的空间预留给各种库和框架
进程内存分配(用户态)
- 基于栈的内存分配:通常由编译器处理,因为栈中填充的通常是程序的自动变量
- 基于堆的内存分配:用于动态内存分配,只限于用户态使用,在内层面,既没有用户对也没有栈的存在。
alloca( )
尽管栈在传统上一直都是保存自动变量,但是在某些情况下,程序员也可以选择用栈来动态分配内存,方法是使用鲜为人知的alloca( ) 这个函数的原型和malloc( )是一样的,区别在于这个函数返回的指针是栈上的地址而不是堆中的地址。
从实现角度,alloca( )从两方面优于malloc( )
- 在栈中非配空间只不过是简单的修改栈指针寄存器,时间消耗低,不用担心页面错误
- 当分配空间的函数返回时,栈中分配的空间会自动释放,解决内存地址泄露问题
但是栈空间通常比堆空间受限很多,所以alloca( )非常适合名称较短的函数中对小空间的分配
堆分配
堆是由C语言运行时维护的用户态数据结构,通过堆的使用,程序可以不用直接在页面的层次处理内存分配。Darwin的libC 采用了一个基于分配区域(allocation zone)的特殊分配算法
虚拟内存(系统管理员角度)
- 页面生命周期
页面状态 | 意义 |
---|---|
Free(空闲) | 物理页面没有被任何虚拟内存页面使用,如果有的话,可以立即回收 |
Acitve(活跃) | 物理页面当前正用于一个虚拟内存页面,而且最近被引用过。这个页面不太可能被交换出去,除非不再有任何非活跃的物理页面存在了。如果这个页面近期不会被引用,则会被置于非活跃的状态 |
Inactive(非活跃) | 物理页面当前正用于一个虚拟内存页面,但是最近没有被任何进程引用过。这个页面有可能会在有需要时被交换出去,另外,如果这个页面在任何时刻被引用了,那么会被重新置于活跃状态 |
Speculative(投机) | 页面被投机映射。通常产生这个状态的原因是针对可能的内存需求做了一次猜测的分配,但是还没有处于活跃状态(也不是非活跃状态,因为有可能很快被访问) |
Wired down(联动) | 物理页面当前正用于一个虚拟内存页面。但是不能被交换出去,不论引用状况如何 |
vm_stat( ):实用工具,显示内核内部的虚拟内存计数器
Translation faults:页错误计数
Pages copy-on-write:因为cow错误引发的页面复制的次数
Pages zero filled:被分配且初始化的页面数
Pageins:从交换空间提取页面的次数
Pageouts:将页面推出到交换空间的次数
sysctl( )
sysctl( )命令是用于查看和修改内核变量的标准UNIX命令,这条命令也支持管理虚拟内存的设置dynamic-pager()
OS X 有一个来自Mach的独特之处在于交换空间不是直接在内核层次管理的而是由专用的用户进程dynamic-pager处理所有的交换技术,dynamic-pager负责管理磁盘上的交换空间
线程
线程作为最大化利用进程时间片的方法,应运而生:通过使用多个线程,程序的指向可以分割表面上看上去并发执行的子任务。线程之间切换的开销比较小,只要保存和恢复寄存器即可。多核处理器更是特别和适合线程,因为多个处理器核心共享同样的cache和ARM,这位线程间的共享虚拟内存提供了基础
-
POSIX 线程
POSIX 线程模型实际上是除了Windows(Windows 坚持提供Win32 线程 API)之外所有系统使用的线程的API。 OS X 和 iOS 不仅仅是pthread -
Grand Central Dispatch
苹果宣称这一套API是对线程的替换,这一套API引入了编程范式的变化,不要从线程和线程函数的角度思考,而是鼓励开发者从功能块(functional block)的角度思考。GCD自己维护了一个底层的线程库实现,以支持并发和异步的执行模型,减轻开发者处理并发的负担以及减少类似于死锁之类的潜在错误。这种机制也可以处理其他异步的通知类型。GCD还支持异步 I/O。 GCD的另外一项优势是能够自动地随着逻辑处理器的个数而扩展。
函数块中的工作由一下几个分发队列中的某一个完成:
- 全局分发队列:应用程序通过dispatch_get_global_queue( )并指定请求的优先级(DISPATCH_QUEUE_PRIORITY_DEFAULT、_LOW、或_HIGH)既可以得到这个队列
- 主分发队列:和Cocoa 应用程序的运行循环整合在一起。通过dispatch_get_main_queue( )可以获得此队列
- 自定义队列:通过调用 dispatch_queue_create( )手工创建队列,可以用于更好地控制分发行为。自定义队列既可以是串行队列(任务以FIFO的顺序进行)也可以是并发队列
GCD的API都声明在<dispatch/dispatch.h>头文件中,代码实现在libDispatch.dylib库中,这是一个libSystem的内部库。这些API本身都是基于preeathed_workqueue API实现的,而XNU通过workq系统调用也支持这个API。要注意的是:Objective-C 进一步封装这些API,暴露给用户的是NSOperation 相关的对象