iOS 进程的内存布局和生命周期

本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢 !

需要了解的小小知识点

程序:

全称 计算机程序(Computer Program),是一组计算机能识别和执行的指令,又称计算机软件
是指为了得到某种结果而可以由计算机等具有信息处理能力的装置执行的代码化指令序列,用某些程序设计语言编写,如:C、C++、OC等;它运行于电子计算机上

进程:

是计算机中的程序关于某数据集合上的一次运行活动
是独立运行、独立分配资源和独立接受调度的基本单位
是操作系统结构的基础
在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

一个合法的APP在iPhone上安装成功后,会有一个代表APP的图标显示在桌面,点击图标就可以打开APP愉快的玩耍!这个打开的过程就是进程的调度过程。

进程是怎么运行的呢 ?

  • 进程有三个基本状态:就 绪执 行睡 眠

    • 就绪状态

    当进程已分配到除CPU以外的所有必要资源后,只需要再获得CPU便可以立即执行,这时的状态称为就绪状态;在系统中处于就绪状态的进程往往会有多个,这些进程被存放在一个队列中,称为就绪队列。

    • 执行状态

      进程已获得CPU, 进入执行状态

    • 睡眠状态(阻塞状态)
      正在执行的进程由于某些事件暂时无法继续执行,便放弃CPU占用转入暂停;阻塞状态的进程也会排入队列中,现代操作系统会根据阻塞原因的不同将处于阻塞状态的进程排入多个队列;导致阻塞的事件有:请求I/O、申请缓冲空间

在iOS中进程通过Progress ID(进程ID,即PID)来唯一辨识。进程还会将其和父进程的亲属关系保存在父进程ID(Parent Progress ID, PPID)中。父进程可以通过fork(或通过posix_spawn)创建子进程,并且预期子进程会消亡。子进程返回的整数由其父进程收集。

iOS进程生命周期

进程状态切换 —— 图片引用自《深入解析Mac OS X & iOS 操作系统》 .png

对进程有一个了解后,就来看看进程的内存布局

进程的内存布局.png

简单理解图中关键词

  • 保留区:该区域是系统分配出来的一块空闲区域;用来处理紧急情况下的任务

  • 代码段:编译之后的指令代码

  • 全局数据段:包含常量区和全局区,全局区:已初始化数据、未初始化数据

  • 常量区:

  • 已初始化数据:简称 DATA(data segment),用来存放程序中已初始化的全局变量、静态变量等数据的一块内存区域;比如 全局变量 int a = 10,NSString *strS = @"123456789";

  • 未初始化数据:简称 BSS(Block Started by Symbol),用来存放程序中未初始化的全局变量、静态变量等数据的一块内存区域;比如 全局变量 int a 、, 并未赋值的变量a就被认为是未初始化数据

  • 堆区:通过alloc、malloc、calloc等调用动态分配的一块内存区域,特点:内存分配由低到高不连续分配

  • 内存映射段:mmap 文件映射和匿名映射,磁盘文件的数据映射到内存,通过修改内存就能修改磁盘文件

  • 栈区:函数调用开销,比如局部变量TaggedPointer指针。内存分配由高到低连续分配

  • 内核区:可以认为是内核代码区,iOS的核心是XNU内核;XNU内核是一个混合内核,其核心是一个叫Mach的微内核

  • 图中由进程箭头指向的代码段、全局数据段是在进程就绪状态时通过编译器装载进来的

  • 堆区、栈区、内存映射区域是在进程进入执行状态(即运行时)后向CPU(处理器)申请分配

  • 进程结束这些区域会随着销毁,保留区和内核区除外

  • 堆和栈是可增长的,它们的增长方向相对,堆向上增长,栈向下增长。

---------------Linux中进程的内存段布局---------------

Linux进程内存布局图.png

上图是一张32位x86架构上运行的Linux中进程的内存段布局, 通过该图从上至下的简要分析Linux中进程的内存分布情况和各自的大致用途:

内核空间和用户空间:

  • 对于32位X86架构上运行的Linux而言, 其虚拟地址空间的寻址范围从0 ~ 4G,内核将这块空间划分为两个部分,

  • 将最高的1G字节0xC0000000 ~ 0xFFFFFFFF称为“内核空间”, 顾名思义是提供给内核使用;

  • 将较低3G字节0x00000000 ~ 0xBFFFFFFF称为“用户空间”,即提供给各个运行的进程使用。

  • 理论上,每个进程都是可以访问全部能寻址的4G虚拟内存空间的,
    但是系统为了防止内核空间被用户进程有意或无意的破坏,所以采用了
    分级保护措施;

  • 将内核定为0级,将用户进程定为3级, 这样用户进程便无法直接访问内核的虚拟内存空间,仅能通过系统调用来进入内核态,从而来访问被限定的部分内核空间地址。

  • 同时,由于访问权限的机制,不同的进程间也都拥有独立的用户空间。这样非对称的访问机制使得Linux系统运行更加的安全稳定。

  • PS: 每个进程都运行在一个属于自己的内存沙盒里, 这个沙盒即虚拟地址空间,这些虚拟地址再通过页表(page table)来映射到物理内存上, 页表由操作系统维护并被CPU所引用。所以用户空间地址的映射是动态变化的;

  • 而内核空间则是持续存在的,在每个进程中都映射到相同的物理内存中,这样便于寻址以应对随时出现的中断和系统调用。

  • 另外,用户进程也是无法访问0x00000000 ~ 0x08048000这一段虚拟内存地址的,在这段地址上有诸多例如C库,动态加载器如ld.soVDSO等的映射地址。 如果用户进程访问到该区间会返回段错误。

用户空间内存布局

  • 用户空间中最顶部的段叫做栈,他被用于存放函数参数和动态局部变量。调用一个方法或函数会将一个新的栈帧(stack frame)压入到栈中,这个栈帧会在函数返回时被清理掉。

  • 在运行过程中,进程通过函数的调用和返回使得控制权在各个函数间转移,在新函数调用时,原函数的栈帧状态保持不变,并为新的函数开辟其所需的帧空间;

  • 当调用函数返回时,该函数的运行空间随着栈帧被弹出而清空,这次进程回到原函数的栈帧环境中继续执行。进程中的每一个线程都有属于自己的栈。

通过不断向栈中压入数据,超出其容量就会耗尽栈所对应的内存区域,
这将触发一个页故障(page fault),而被Linuxexpand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。
如果栈的大小低于RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情。这是一种将栈扩展到所需大小的常规机制。
如果达到了最大栈空间的大小,就会栈溢出(stack overflow),程序收到一个段错误(segmentation fault)。

注: 动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

mmap内存映射

  • 在栈段的低一段便是mmap端,mmap是一种高效便捷的文件I/O方式,内核将文件内容映射在此段内存中,常见情形便是加载动态链接库
  • 在Linux中, 如果你通过malloc申请一块大于MMAP_THRESHOLD(通常默认为128KB, 可用mallopt()修改)大小的堆空间时, glibc会返回一块匿名的mmap内存块而非一块堆内存。

  • 在mmap段下面便是堆段了,堆段同栈段一样,都是为进程运行提供动态的内存分配,但是其和栈的区别在于堆上内存的生命期和执行分配的函数的生命期不一致
  • 堆上分配的内存只有在对应进程通过系统调用主动释放或进程结束后才会释放。所以,内存泄露这个经典的问题便由此产生。
  • 由于堆内存的反复申请和释放,也不可避免的会造成堆段碎片化。这种情况可以使用“对象池”的设计手段来避免。
image.png
  • 堆段再往下便是BSS段和DATA段这两个静态内存区域,这两段都是用来存储静态局部或静态全局变量,其在编译期间便决定了虚拟内存的消耗。

  • 区别是: DATA段存放的是已经初始化的变量,其映射自程序镜像中包含对应静态变量的文件;而BSS段则存放的是未初始化的变量,他不映射自任何一个执行文件。

  • 根据C语言标准规定,未初始化的静态成员变量的初始值必须为0,所以内核在加载二进制文件后执行程序前会将BSS段清0。

代码段

BSS和DATA段下是代码段(TEXT),这段中存有程序的指令代码。Text段是通过只读的方式加载到内存中的,他在多个进程中是可以被安全共享的。

参考文章:

Linux进程内存布局

你可能感兴趣的:(iOS 进程的内存布局和生命周期)