概念
进程和线程
进程可以属于进程组,进程组的主要作用是让用户可以同时控制多个进程——通常向一个进程组发送信号的方式控制这些进程。
线程只不过是一组寄存器的状态,一个进程中可以存在多个线程,一个进程内的多有线程都共享虚拟内存空间、文件描述符和文件句柄。进程的抽象以一个或多个线程的容器的形式保存下来。
进程生命周期
进程会尽可能保存在SRUN状态(即正在运行或可运行状态),除非要等待一些资源(I/O相关资源、同步对象互斥体和锁)。进程正在等待时,就没有必要占用CPU了,甚至没有必要存在于运行队列中。这个进程置于“睡眠”状态,即SSLEEP状态,进程会一直睡直到等待的资源变得可用。当然睡眠的进程也会被信号唤醒。
多线程的一个主要优点是一个线程的状态可以独立于其他线程,但给一个线程子在睡眠时,另一个线程可以被调度在CPU上执行。
通过一个特殊的信号TSTOP可以使一个进程停止执行。相当于冻结进程,挂起这个进程的所有线程。只有发送另一个CONT信号才能恢复进程。
当一个进程通过从main()函数返回或通过调用exit(2)完成执行时,这个进程被从内存中清理干净,从而完全终止。终止一个进程会同时终止其所有线程,但是在进程完成终止之前,会短暂地处于僵尸状态。
僵尸状态
父进程通过wait()等待子进程,收集子进程返回值。当父进程抛弃了子进程时,子进程会处于僵尸状态,僵尸状态的资源都释放了,只是占用着PID,当父进程通过wait()收集子进程返回值时,子进程才会真正死掉。
UNIX信号
信号是一种软件中断,表示进程自己发生了异常,或者发生了外部的事件。信号指发送给程序的异步通知,其中不包含数据。信号由操作系统发送给进程,用于表示发生了某种条件,而这种条件通常是因为某类硬件错误或程序异常而产生的。
OSX定义了31个信号,定义在<sys/signal.h>
中
来源:
- HW:硬件异常或错误
- OS:操作系统,内核代码中的某个地方
- TTY:终端驱动程序
- User:用户通过kill命令发出(用户可通过这个命令模拟所有其他信号)
默认处理
- C——SA_CORE:除非另外指定,进程会产生核心转储
- I_SA_IGNORE:忽略掉信号
- K_SA_KILL:进程会终止
- S_SA_STOP:进程暂停运行
- T_SA_TTYSTOP
信号原本是设计为发送给进程的,不过POSIX允许向单个线程发送信号。
可执行文件
可执行文件都包含一个头签名,通常是一个“魔数”,据此可以精确判定可执行文件的格式,这个标志就告诉操作系统内核将这个文件读入内存。
OSX支持三种:解释器脚本格式、通用二进制格式、Mach-O格式。
通用二进制格式
通用二进制格式只不过是其支持的各种架构的二进制文件的打包文件。
Mach-O 二进制文件
同一种二进制文件可用于多种目标文件类型:
- 可执行文件
- 库文件
- 核心转储文件
- 内核扩展文件
filetype表示目标文件的类型。
加载命令
Mach-O文件头包含了非常详细的指令,指导如何设置并加载二进制数据。一些命令是由内核加载器直接使用,其他命令由动态链接器处理。
内核加载器:bsd/kern/mach_oader.c
加载过程在内核的部分负责新进程的基本设置:
- 分配虚拟内存
- 创建主线程
- 处理代码签名、加密
对于动态链接的可执行文件(大部分可执行文件都是动态链接),真正的库加载和符号解析的工作都是通过 LC_LOAD_DYLINKER 命令指定的动态链接器在用户态完成。控制权会转交给链接器,链接器进而接着处理文件头中的其他加载命令。
加载命令:
- LC_SEGMENT :进程虚拟内存设置
- 对于每一个段,将文件中相应的内容加载到内存中
- LC_UNIXTHREAD
- 当所有库都完成加载之后,dyld的工作也完成了,之后由LC_UNIXTHREAD命令负责启动二进制程序的主线程
- LC_THREAD
- 用户核心转储文件
- LC_MAIN
- 设置程序主线程的入口地址和栈大小
- LC_CODE_SIGNATURE
- LC_CODE_SIGNATURE包含了Mach-O二进制文件的代码签名,如果签名和代码本身不匹配,内核立即给进程发送了一个SIGKILL信号
动态库
可执行文件很小是独立的,除了极少数的一些静态链接的可执行文件。大部分可执行文件都是动态链接的,都依赖一些预先存在的库,既可能是操作系统提供的,也可能是第三方的库。
启动时库的加载
仅有很少量的进程只需要内核加载器就可以完成加载,OSX上几乎所有进程都是动态链接的,所以Mach-O镜像中有很多“空洞”,即对外部的库和符号引用,这些空洞需要在启动时填补。这项工作就需要由动态链接器完成,也称为符号绑定。
动态链接器是内核执行 LC_DYLINKER 加载命令时启动的,通常情况使用/usr/lib/dyld
作为动态链接器。
dyld是一个用户态进程,不属于内核的一部分,作为单独的开源项目由苹果进行维护。从内核角度看,dyld是一个可插入的组件,可以替换为第三方的链接器。
共享库缓存
共享库缓存是dyld支持的另外一个机制,指的是一些库经过预先连链接,然后保存在磁盘上的一个文件中。在iOS中,大部分常用的库都被缓存了。
库的运行时加载
通常情况,开发者通过 #include
包含一些头文件,头文件中声明了他们想要的库和符号,有时候还会通过链接器的-l参数指定额外的库。通过这种方式构建的可执行文件只有在解决了所有依赖条件之后才能加载执行。
另一种方法,通过<dlfcn.h>
头文件中提供的函数在运行时加载库,得到更高程度的灵活性,需要在编译时确定或指导库的名称,开发者可以准备多个库,在运行时根据功能或需求加载最合适的库。如果库加载失败,程序还可以通过返回的错误代码进行错误处理。
弱符号:将符号定义为“weak”。抢符号必须在执行之前解析,如果解析失败,程序运行失败。弱符号发生解析错误不会引起程序链接错误。动态链接器将这个符号设置为NULL,允许程序恢复这一错误或采用其他处理措施。
dyld的特性
- 两级名称空间
- 函数拦截
- 允许一个库将其函数实现替换为另一个函数的实现
进程地址空间
用户态的一个优点在于虚拟内存的隔离,进程独享一个私有的地址空间。
进程内存分配(用户态)
基于栈的内存分配通常由编译器处理。动态内存分配一般在堆上进行。这些仅限于用户态,内核层面,既没有用户堆也没有栈存在。
alloca()
尽管传统上栈是保存自动变量的,但某些情况,也可以使用栈动态分配内存,方法就是alloca,和malloc区别在于函数返回的指针是栈上的地址,而不是堆上的地址。
堆分配
堆是由C语言运行时库维护的用户态数据结构,通过堆的使用,程序可以不用直接在页面的层次处理内存分配。