linux suspend to disk

  1. lock_system_sleep();

该函数设置当前进程为不可冻结,并获取pm系统的互斥锁,防止hibernate期间被冻结,因为后面会调用freeze_process函数冻结所有进程,这其中不包含它自己。

snapshot_device_available是一个原子操作类型的变量,表示snapshot设备是否可用,这个设备应该是hibernate用到的swap分区。这个变量初始化为1,atomic_add_unless函数的作用是自动给v累加a,直到v等于u。用到这里,就是将snapshot_device_available设置为零,保证此时swap分区不可用。

  1. pm_prepare_console

hibernate阶段,将kernel 的console切换为SUSPEND_CONSOLE,并保存之前的console号,等唤醒时,再切回来:

发送通知链信息,通知其它注册要监听该消息的模块,现在要进行hibernate了。其它消息还包括:

例如在MMC驱动中会接收这个消息:

mmc收到这个消息后,就会停止扫描mmc卡的插入动作。

  1. sys_sync

该函数同步所有缓存数据到块设备上。

  1. freeze_processes()

进程冻结技术(freezing of tasks)是指在系统hibernate或者suspend的时候,将用户进程和部分内核线程置于“可控”的暂停状态。

    假设没有冻结技术,进程可以在任意可调度的点暂停,而且直到cpu_down才会暂停并迁移。这会给系统带来很多问题:

  1. 有可能破坏文件系统。在系统创建hibernate image到cpu down之间,如果有进程还在修改文件系统的内容,这将会导致系统恢复之后无法完全恢复文件系统;
  2. 有可能导致创建hibernation image失败。创建hibernation image需要足够的内存空间,但是在这期间如果还有进程在申请内存,就可能导致创建失败;
  3. 有可能干扰设备的suspend和resume。在cpu down之前,device suspend期间,如果进程还在访问设备,尤其是访问竞争资源,就有可能引起设备suspend异常;
  4. 有可能导致进程感知系统休眠。系统休眠的理想状态是所有任务对休眠过程无感知,睡醒之后全部自动恢复工作,但是有些进程,比如某个进程需要所有cpu online才能正常工作,如果进程不冻结,那么在休眠过程中将会工作异常。

冻结的对象是内核中可以被调度执行的实体,包括用户进程、内核线程和work_queue。用户进程默认是可以被冻结的,借用信号处理机制实现;内核线程和work_queue默认是不能被冻结的,少数内核线程和work_queue在创建时指定了freezable标志,这些任务需要对freeze状态进行判断,当系统进入freezing时,主动暂停运行。

kernel threads可以通过调用kthread_freezable_should_stop来判断freezing状态,并主动调用__refrigerator进入冻结;work_queue通过判断max_active属性,如果max_active=0,则不能入队新的work,所有work延后执行。

  1. 首先先禁止新的usermodehelper被创建使能,usermodehelper是一种在内核空间调用用户空间代码的方法。
  2. 设置标志位,确保本进程不会被冻结。
  3. 调用try_to_freeze_tasks函数,该函数是核心,它实现了具体的动作。

  1. 设置超时时间,默认是20秒,如果超时就返回错误,停止suspend。
  2. 如果是在freeze_kerenl_processes函数中调用的这个函数,那么就会去冷冻workqueue。
  3. 接下来轮询各个进程,给其发送信号:

freeze_task返回false,表示此进程已经被冷冻或者不需要被冷冻,返回true,表示已经给该进程发送了信号,如下所示:

正常的能够待机成功且不超时的情况,freeze_task会返回false,当然这个过程可能要反复几次,也就是while循环要执行几次,直到todo变为0。例如一个正常的用户进程,一开始这里的freeze_task会返回true,那么就会跑if(!freezer_should_skip)这个语句,肯定为真,那么todo就会被加1。那么什么时候freeze_task会返回false呢?就是等到_refrigerator函数执行后,将该进程的状态设为PF_FROZEN后,等到下次while循环再执行到freeze_task函数时,就会返回false。

fake_signal_wake_up函数巧妙的利用了信号处理机制,只设置任务的TIF_SIGPENDING位,但不传递任何信号,然后唤醒任务;这样任务在返回用户态时会进入信号处理流程,检查系统的freeze状态,并做相应处理。

当在用户进程中调用诸如open、read、poll等系统调用时,会触发一个SVC异常,从而使进程进入内核态。arm64架构处理SVC异常的代码如下:

它会去查找sys_call_table,找到用户调用的函数,然后去执行。这里我们重点看它从内核态返回时的过程,其执行了ret_fast_syscall:

do_notify_resume函数用来处理未尽事宜,主要调用do_signal函数去处理信号。调用get_signal去获取信号,此时会调用try_to_freeze函数去判断是否要进入冷冻状态:

system_freezing_cnt变量在freeze_processes函数被加一。

真正冷冻进程的函数是:

设置进程状态为UNINTERRUPTIBLE,设置标志位为PF_FROZEN。

整个冷冻进程的过程如下所示:

  1. lock_device_hotplug

获取设备热插拔的锁,禁止处理设备热插拔事件

  1. create_basic_memory_bitmaps
  2. swsusp_arch_suspend

该结构体数据用来保存当前cpu的状态。其地址位于swsusp_arch_suspend的堆栈中,在__cpu_suspend_enter函数中赋值。该数据需要在cpu_resume被调用之前一直保持不变。该结构体数据描述了被保存的CPU状态的大小及布局。

将CPU状态保存在sleep_stack_data_area,然后将其地址放到sleep_save_stash中供cpu_resume使用。cpu_resume将重新加载这些保存的状态,然后返回。因为链接寄存器是在这里保存的,因此cpu_resume返回时就好像从这里返回的,即cpu_resume会返回到swsusp_arch_suspend。

__cpu_suspend_enter返回1,而cpu_resume返回零,swsusp_arch_suspend函数可以根据这两个不同的返回值来确认是在suspend流程还是resume流程。

ARM64系统的寄存器分类如下:

其中X19-X30保存的是被调用函数的相关信息。因此__cpu_suspend_enter函数首先保存了callee相关寄存器的值,接下来:

将SP寄存器保存到cpu_suspend_ctx中。

sleep_save_stash是一个指针数组的头地址。指向sleep_stack_data。

软件可以根据MPIDR_EL1寄存器的内容看出自己当前是在哪个核心运行。当前CPU内大部分都包含了多个核心,例如MSD938芯片,包含2个A72,2个A53,它们分别属于两个簇clusters。这个寄存器就是告诉软件当前自己是在哪个簇,簇里哪个核运行的。通常的,affinity 0代表核心在簇内的索引,而affinity 1代表簇的索引。

MPIDR hash: aff0[0] aff1[7] aff2[14] aff3[30] mask[0x101] bits[2]

linux内核的代码入口在head.S文件中:

可以看到内核刚开始运行时,MMU和cache都是没有打开的,X0中保存的是FDT的地址。这里需要对data cache和instruction cache多说几句。我们知道,具体实现中的ARMv8处理器的cache是形成若干个level,一般而言,可能L1是分成了data cache和instruction cache,而其他level的cache都是unified cache。上面定义的D-cache off并不是说仅仅disable L1的data cache,实际上是disable了各个level的data cache和unified cache。同理,对于instruction cache亦然。

此外,在on/off控制上,MMU和data cache是有一定关联的。在ARM64中,SCTLR, System Control Register用来控制MMU icache和dcache,虽然这几个控制bit是分开的,但是并不意味着MMU、data cache、instruction cache的on/off控制是彼此独立的。一般而言,这里MMU和data cache是绑定的,即如果MMU 是off的,那么data cache也必须要off。因为如果打开data cache,那么要设定memory type、sharebility attribute、cachebility attribute等,而这些信息是保存在页表(Translation table)的描述符中,因此,如果不打开MMU,如果没有页表翻译过程,那么根本不知道怎么来应用data cache。当然,是不是说HW根本不允许这样设定呢?也不是了,在MMU OFF而data cache是ON的时候,这时候,所有的memory type和attribute是固定的,即memory type都是normal Non-shareable的,对于inner cache和outer cache,其策略都是Write-Back,Read-Write Allocate的。

上面是head.S中一开始的代码,是mstar加上的,前面一段是用来给STR用的,判断是否处于STR模式。后面一段是保存ramdisk和cmdline地址的。

上面一段代码是head.S原来的样子,在__primary_switch函数执行到最后,会跳转到start_kernel。我们挨个分析这段代码涉及到的几个函数。

  1. preserve_boot_args

boot_args定义如下:

由于MMU = off, D-cache = off,因此写入boot_args变量的操作都是略过data cache的,直接写入了RAM中(前面说过了,这里的D-cache并不是特指L1的data cache,而是各个level的data cache和unified cache),为了安全起见(也许bootloader中打开了D-cache并操作了boot_args这段memory,从而在各个level的data cache和unified cache有了一些旧的,没有意义的数据),需要将boot_args变量对应的cache line进行清除并设置无效。在调用__inval_cache_range之前,x0是boot_args这段memory的首地址,x1是末尾的地址(boot_args变量长度是4x8byte=32byte,也就是0x20了)。

为何要保存x0~x3这四个寄存器呢?因为ARM64 boot protocol对启动时候的x0~x3这四个寄存器有严格的限制:x0是dtb的物理地址,x1~x3必须是0(非零值是保留将来使用)。在后续setup_arch函数执行的时候会访问boot_args并进行校验:

还有一个小细节是如何访问boot_args这个符号的,这个符号是一个虚拟地址,但是,现在没有建立好页表,也没有打开MMU,如何访问它呢?这是通过adr_l这个宏来完成的。这个宏实际上是通过adrp这个汇编指令完成,通过该指令可以将符号地址变成运行时地址(通过PC relative offset形式),因此,当运行的MMU OFF mode下,通过adrp指令可以获取符号的物理地址。不过adrp是page对齐的(adrp中的p就是page的意思),boot_args这个符号当然不会是page size对齐的,因此不能直接使用adrp,而是使用adr_l这个宏进行处理:

最后,我们来解释一下dmb sy这一条指令。在ARM ARM文档中,有关于数据访问指令和 data cache指令之间操作顺序的约定,原文如下:

All data cache instructions, other than DC ZVA, that specify an address can execute in any order relative to loads or stores that access any address with the Device memory attribute,or with Normal memory with Inner Non-cacheable attribute unless a DMB or DSB is executed between the instructions.

因此,在Non-cacheable的情况下,必须要使用DMB来保证stp指令在dc ivac指令之前执行完成。

  1. el2_setup

  1. 当前的exception level保存在PSTATE中,程序可以通过MRS或者MSR来访问PSTATE,当然需要传递一个Special-purpose register做为参数,CurrentEL就是获取PSTATE中current exception level域的特殊寄存器。
  2. sctlr_el2也是一个可以通过MRS/MSR指令访问的寄存器,当CPU处于EL2状态的时候,该寄存器可以控制整个系统的行为。当然,这里仅仅是设定EL2下的数据访问和地址翻译过程中的endianess配置,也就是EE bit[25]。根据配置,CPU_BE和CPU_LE包围的指令只会保留一行。对于little endian而言,实际上就是将sctlr_el2寄存器的EE(bit 25)设定为0。顺便说一下,这个bit不仅仅控制EL2数据访问的endianess以及EL2 stage 1的地址翻译过程中的endianess(当然,EL2只有stage 1),还可以控制EL1和EL0 stage 2地址翻译的过程的endianess(这时候有两个stage的地址翻译过程)。
  3. 执行到这里说明CPU处于EL1,这种状态下没有权限访问sctlr_el2,只能是访问sctlr_el1。sctlr_el1可以通过EE和E0E来控制EL1和EL0状态下是little endian还是big endian。EE bit控制了EL1下的数据访问以及EL1和EL0 stage 1地址翻译的过程的endianess。E0E bit用来控制EL0状态下的数据访问的endianess。此外,需要注意的是:由于修改了system control register(设定endianess状态),因此需要一个isb来同步(具体包括两部分的内容,一是确认硬件已经执行完毕了isb之前的所有指令,包括修改system control寄存器的那一条指令,另外一点是确保isb之后的指令从新来过,例如取指,校验权限等)。
  4. 执行到这里说明CPU处于EL2,首先设定的是hcr_el2寄存器,Hypervisor Configuration Register。该寄存器的大部分bit 值在reset状态的时候就是0值,只不过bit 31(Register Width Control)是implementation defined,因此这里set 31为1,确保Low level的EL1也是Aarch64的
  5. 这一段代码是对Generic timers进行配置。要想理解这段代码,我们需要简单的了解一些ARMv8上Generic timer的运作逻辑。一个全局范围的system counter、各个PE上自己专属的local timer以及连接这些组件之间的bus或者信息传递机制组成了Generic Timer。对于PE而言,通过寄存器访问,它能看到的是physical counter(实际的system counter计数)、virtual counter(physical counter基础上的offset)、physical timer、virtual timer等。NTHCTL_EL2,Counter-timer Hypervisor Control register,用来控制系统中的physical counter和virutal counter如何产生event stream以及在EL1和EL0状态访问physical counter和timer的硬件行为的。在EL1(EL0)状态的时候访问physical counter和timer有两种配置,一种是允许其访问,另外一种就是trap to EL2。这里的设定是:不陷入EL2(对应的bit设置为1)。更详细的信息可以参考ARMv8 ARM文档。cntvoff_el2是virtual counter offset,所谓virtual counter,其值就是physical counter的值减去一个offset的值(也就是cntvoff_el2的值了),这里把offset值清零,因此virtual counter的计数和physical counter的计数是一样的。
  6. 这一段代码是对GIC V3进行配置。ID_AA64PFR0_EL1,AArch64 Processor Feature Register 0,该寄存器描述了PE实现的feature。GIC bits [27:24]描述了该PE是否实现了system register来访问GIC,如果没有(GIC bits 等于0)那么就略过GIC V3的设定。ICC_SRE_EL2,Interrupt Controller System Register Enable register (EL2),该寄存器用来(在EL2状态时候)控制如何访问GIC CPU interface模块的,可以通过memory mapped方式,也可以通过system register的方式。将SRE bit设定为1确保通过system register方式进行GIC interface cpu寄存器的访问。将enable bit设定为1确保在EL1状态的时候可以通过ICC_SRE_EL1寄存器对GIC进行配置而不是陷入EL2。

    1. midr_el1和mpidr_el1都属于标识该PE信息的read only寄存器。MIDR_EL1,Main ID Register主要给出了该PE的architecture信息,Implementer是谁等等信息。MPIDR_EL1,Multiprocessor Affinity Register,该寄存器保存了processor ID。vpidr_el2和vmpidr_el2是上面的两个寄存器是对应的,只不过是for virtual processor的。
    2. 这段代码实际上是将0x33d00800(BE)或者0x30d00800(LE)写入sctlr_el1寄存器。BE和LE的设定和上面第一段代码中的描述是类似的,其他bit的设定请参考ARMv8 ARM文档
    3. PMCR_EL0,Performance Monitors Control Register,该寄存器的[15:11]标识了支持的Performance Monitors counter的数目,并将其设定到MDCR_EL2(Monitor Debug Configuration Register (EL2))中。MDCR_EL2中其他的bit都设定为0,其结果就是允许EL0和EL1进行debug的操作(而不是trap to EL2),允许EL1访问Performance Monitors counter(而不是trap to EL2)。
    4. 当系统发生了异常并进入EL2,SPSR_EL2,Saved Program Status Register (EL2)会保存处理器状态,ELR_EL2,Exception Link Register (EL2)会保存返回发生exception的现场的返回地址。这里是设定SPSR_EL2和ELR_EL2的初始值。w20寄存器保存了cpu启动时候的Eexception level ,因此w20被设定为BOOT_CPU_MODE_EL2。
    5. eret指令是用来返回发生exception的现场。实际上,这个指令仅仅是模拟了一次异常返回而已,SPSR_EL2和ELR_EL2都已经设定OK,执行该指令会使得CPU返回EL1状态,并且将SPSR_EL2的值赋给PSTATE,ELR_ELR就是返回地址(实际上也恰好是函数的返回地址)。

完成了el2_setup这个函数分析之后,我们再回头思考这样的问题:为何是el2_setup?为了没有el3_setup?当一个SOC的实现在包括了EL3的支持,那么CPU CORE缺省应该进入EL3状态,为何这里只是判断EL2还是EL1,从而执行不同的流程,如果是EL3状态,代码不就有问题了吗?实际上,即便是由于SOC支持TrustZone而导致cpu core上电后进入EL3,这时候,接管cpu控制的一定不是linux kernel(至少目前来看linux kernel不会做Secure monitor),而是Secure Platform Firmware(也就是传说中的secure monitor),它会进行硬件平台的初始化,loading trusted OS等等,等到完成了secure world的构建之后,把控制权转交给non-secure world,这时候,CPU core多半处于EL2(如果支持虚拟化)或者EL1(不支持虚拟化)。因此,对于linux kernel而言,它感知不到secure world(linux kernel一般也不会做Trusted OS),仅仅是在non-secure world中呼风唤雨,可以是Hypervisor或者rich OS。

  1. set_cpu_boot_mode_flag

在进入这个函数的时候,有一个前提条件:w20寄存器保存了cpu启动时候的Eexception level。由于系统启动之后仍然需要了解cpu启动时候的Eexception level(例如判断是否启用hyp mode),因此,有一个全局变量__boot_cpu_mode用来保存启动时候的CPU mode。

    1. 本质上我们希望系统中所有的cpu在初始化的时候处于同样的mode,要么都是EL2,要么都是EL1,有些EL2,有些EL1是不被允许的(也许只有那些精神分裂的bootloader才会这么搞)。
    2. 所有的cpu core在启动的时候都处于EL2 mode表示系统支持虚拟化,只有在这种情况下,kvm模块可以顺利启动。
    3. set_cpu_boot_mode_flag和el2_setup这两个函数会在各个cpu上执行。
    4. 变量__boot_cpu_mode定义如下:

如果cpu启动的时候是EL1 mode,会修改变量__boot_cpu_mode A域,将其修改为BOOT_CPU_MODE_EL1。如果cpu启动的时候是EL2 mode,会修改变量__boot_cpu_mode B域,将其修改为BOOT_CPU_MODE_EL2。

__PHYS_OFFSET,定义为:

#define __PHYS_OFFSET    (KERNEL_START - TEXT_OFFSET)

#define KERNEL_START    _text

KERNEL_START是kernel开始运行的虚拟地址,更确切的说是内核正文段开始的虚拟地址。 在链接脚本文件中(参考arch/arm64/kernel下的vmlinux.lds.S),KERNEL_START被设定为:

. = PAGE_OFFSET + TEXT_OFFSET;

.head.text : { 
    _text = .; 
    HEAD_TEXT 
}

因此,KERNEL_START的值和PAGE_OFFSET以及TEXT_OFFSET这两个offset的设定有关。TEXT_OFFSET标识了内核正文段的offset,其实如果该宏被定义为KERNEL_TEXT_OFFSET会更好理解。我们知道,操作系统运行在内核空间,应用程序运行在用户空间,假设内核空间的首地址是x(一般也是RAM的首地址),那么是否让kernel运行在x地址呢?对于arm,在内核空间的开始有32kB(0x00008000)的空间用于保存内核的页表(也就是进程0的PGD)以及bootloader和kernel之间参数的传递,对于ARM64,在其Makefile中定义了这个offset是512KB(0x00080000)。

ifeq ($(CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET), y) 

TEXT_OFFSET := $(shell awk 'BEGIN {srand(); printf "0x%03x000\n", int(512 * rand())}') 
else 
TEXT_OFFSET := 0x00080000 
endif

kernel image的开始部分包括了一个ARM64 image header的内容,这个header定义了bootloader如何来copy kernel image。ARM64 image header中有一个域(text_offset)就是告知bootloader,它应该按照多大的偏移来copy kernel image。当然了,也许有些bootloader不鸟这些,对于ARM64平台,反正大家一直都是固定为0x80000,因此,bootloader也没有什么太大的动力来修改支持这个特性。怎么破?虽然目前ARM64的kernel的TEXT_OFFSET就是固定为0x80000,但是也许将来内核会修改这个offset啊。在这种情况下,内核的开发者提供了一个CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET选项,在编译内核的时候可以randomize内核的TEXT_OFFSET值,以此来测试bootloader是否能够正确的copy kernel image到正确的内存偏移位置上去。通过这样一个配置项,可以尽快的暴露问题,确保了整个系统(bootloader + kernel)稳定的运行。

搞定了TEXT_OFFSET,我们再来看看PAGE_OFFSET,在arch/arm64/include/asm/memory.h中,PAGE_OFFSET被定义为:

#define VA_BITS       (CONFIG_ARM64_VA_BITS) 
#define PAGE_OFFSET     (UL(0xffffffffffffffff) << (VA_BITS - 1))

VA_BITS定义了虚拟地址空间的bit数(该值也就是定义了用户态程序或者内核能够访问的虚拟地址空间的size),假设VA_BITS被设定为39个bit,那么PAGE_OFFSET就是0xffffffc0-00000000。PAGE_OFFSET的名字也不好(个人观点,可能有误),OFFSET表明的是一个偏移,内核空间被划分成一个个的page,PAGE_OFFSET看起来应该是定义以page为单位的偏移。但是,以什么为基准的偏移呢?PAGE_OFFSET的名字中没有给出,当然实际上,这个符号是定义以整个address space的起始地址(也就是0)为基准。另外,虽然这个地址是要求page对齐,但是实际上,这个符号仍然定义的是虚拟地址的offset(而不是page的offset)。根据上面的理由,我觉得定义成KERNEL_IMG_OFFSET会更好理解一些。一句话总结:PAGE_OFFSET定义了将kernel image安放在虚拟地址空间的哪个位置上。

OK,经过漫长的说明之后,__PHYS_OFFSET实际上就是kernel 空间的首地址(并不是__PHYS_OFFSET的位置开始就是真实的kernel image,实际上从__PHYS_OFFSET开始,首先是TEXT_OFFSET的保留区域,然后才是真正的kernel image)。实际上,__PHYS_OFFSET定义的是一个虚拟地址而不是物理地址,这里的PHYS严重影响了该符号的含义,实际上adrp这条指令可以将一个虚拟地址转换成物理地址(在没有打开MMU的时候)。

KERNEL_START

kernel代码段开始的内核空间虚拟首地址

PAGE_OFFSET

内核空间的虚拟首地址

TEXT_OFFSET

kernel代码段相对内核空间虚拟地址的偏移

__PHYS_OFFSET

内核空间虚拟首地址

PHYS_OFFSET

系统内存的起始物理地址。在系统初始化的过程中,会把PHYS_OFFSET开始的物理内存映射到PAGE_OFFSET的虚拟内存上去。

上图中蓝色区域表示映射到物理内存的虚拟地址,而白色区域表示未映射的部分。可以看出,Firefox使用了相当多的虚拟地址空间,因为它占用内存较多。

Linux进程标准的内存段布局,如下图所示,地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。记住,这些段只是简单的虚拟内存地址空间范围。

进程地址空间中最顶部的段是栈,大多数编程语言将之用于存储函数参数和局部变量。调用一个方法或函数会将一个新的栈帧(stack frame)压入到栈中,这个栈帧会在函数返回时被清理掉。由于栈中数据严格的遵守LIFO的顺序,这个简单的设计意味着不必使用复杂的数据结构来追踪栈中的内容,只需要一个简单的指针指向栈的顶端即可,因此压栈(pushing)和退栈(popping)过程非常迅速、准确。进程中的每一个线程都有属于自己的栈。

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

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

在栈的下方,是我们的内存映射段。内核将文件的内容直接映射到内存。任何应用程序都可以通过Linux的mmap()系统调用或者Windows的CreateFileMapping()/MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式,所以它被用来加载动态库。创建一个不对应于任何文件的匿名内存映射也是可能的,此方法用于存放程序的数据。在Linux中,如果你通过malloc()请求一大块内存,C运行库将会创建这样一个匿名映射而不是使用堆内存。“大块”意味着比MMAP_THRESHOLD还大,缺省128KB,可以通过mallocp()调整。

接下来的一块内存空间是堆。与栈一样,堆用于运行时内存分配;但不同的是,堆用于存储那些生存期与函数调用无关的数据。大部分语言都提供了堆管理功能。在C语言中,堆分配的接口是malloc()函数。如果堆中有足够的空间来满足内存请求,它就可以被语言运行时库处理而不需要内核参与,否则,堆会被扩大,通过brk()系统调用来分配请求所需的内存块。堆管理是很复杂的,需要精细的算法来应付我们程序中杂乱的分配模式,优化速度和内存使用效率。处理一个堆请求所需的时间会大幅度的变动。实时系统通过特殊目的分配器来解决这个问题。堆在分配过程中可能会变得零零碎碎,如下图所示:

最后,我们看看底部的内存段:BSS,数据段,代码段。

在C语言中,BSS和数据段保存的都是静态(全局)变量的内容。区别在于BSS保存的是未被初始化的静态变量内容,他们的值不是直接在程序的源码中设定的。BSS内存区域是匿名的,它不映射到任何文件。如果你写static intcntActiveUsers,则cntActiveUsers的内容就会保存到BSS中去。而数据段则保存在源代码中已经初始化的静态变量的内容。数据段不是匿名的,它映射了一部分的程序二进制镜像,也就是源代码中指定了初始值的静态变量。所以,如果你写static int cntActiveUsers=10,则cntActiveUsers的内容就保存在了数据段中,而且初始值是10。尽管数据段映射了一个文件,但它是一个私有内存映射,这意味着更改此处的内存不会影响被映射的文件。

你可以通过阅读文件/proc/pid_of_process/maps来检验一个Linux进程中的内存区域。记住:一个段可能包含许多区域。比如,每个内存映射文件在mmap段中都有属于自己的区域,动态库拥有类似BSS和数据段的额外区域。有时人们提到“数据段”,指的是全部的数据段+BSS+堆。

你还可以通过nm和objdump命令来察看二进制镜像,打印其中的符号,它们的地址,段等信息

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值