Cow and Hprof
目次
COW(copy on write) #
Linux通过fork()和exec()函数族创建新进程。
为什么通过这种方式,而不是从头开始创建新进程? #
Of course, one big question you might have: why would we build such an odd interface to what should be the simple act of creating a new process? Well, as it turns out, the separation of fork() and exec() is essential in building a UNIX shell, because it lets the shell run code after the call to fork() but before the call to exec(); this code can alter the environment of the about-to-be-run program, and thus enables a variety of interesting features to be readily built. The shell is just a user program4 . It shows you a prompt and then waits for you to type something into it. You then type a command (i.e., the name of an executable program, plus any arguments) into it; in most cases, the shell then figures out where in the file system the executable resides, calls fork() to create a new child process to run the command, calls some variant of exec() to run the command, and then waits for the command to complete by calling wait(). When the child completes, the shell returns from wait() and prints out a prompt again, ready for your next command. The separation of fork() and exec() allows the shell to do a whole bunch of useful things rather easily. For example: prompt> wc p3.c > newfile.txt In the example above, the output of the program wc is redirected into the output file newfile.txt (the greater-than sign is how said redirection is indicated). The way the shell accomplishes this task is quite simple: when the child is created, before calling exec(), the shell (specifically, the code executed in the child process) closes standard output and opens the file newfile.txt. By doing so, any output from the soonto-be-running program wc is sent to the file instead of the screen (open file descriptors are kept open across the exec() call, thus enabling this behavior [SR05]).
–威斯康大学出的 Operating System: Three Easy Pieces 的第一章 第三节
微软的研究
fork()最早出现在Genie分时操作系统,允许父进程描述新进程的内存地址和机器上下文。默认情况下子进程和父进程共享内存,可以选择给子进程提供另一块完全不同的内存执行不同的程序。
UNIX可以shell实现 prompt> wc p3.c > newfile.txt 这类功能。
为什么用Copy on Write #
在Unix诞生早期,fork简单。不需要参数,为新进程提供了继承自父进程的默认值。后面随着进程的尺寸越来越大,需要复制文件锁、定时器、异步IO操作、跟踪等。此外,许多系统调用标志控制了fork在内存映射标志 MADV_DONTFORK/DOFORK/WIPEONFORK等)、文件描述符和线程都需要复制给子进程这些状态。操作耗时,且执行exec
后会覆盖整个子进程的虚拟地址空间,浪费大量物理内存。因此才有了COW来减少这个开销。
内核不会复制进程的整个地址空间,而是只复制其页表(即指向与父进程相同的内存页),fork
之后的父子进程的地址空间指向同样的物理内存页。
复制文件锁:访问共享文件时只有一个进程可以进程写操作
定时器:确保每个进程都有机会执行
异步IO操作:发起I/O请求的线程不等I/O操作完成,就继续执行随后的代码,I/O结果用其他方式通知发起I/O请求的程序
跟踪:在进程切换过程中对进程状态和执行情况进行监控和记录,以便分析和调试
Copy on Write机制 #
进程创建之后,操作系统分配一系列虚拟内存页,开始对所有可以访问内存页的进程标记为可读的,当一个进程尝试修改只读页的时候,操作系统创建一个独占的原始页的副本,其他的进程仍然指向原始的只读内存页。只有对副本的修改最对修改的一个进程可见。
fork()
之后,内核会把父进程的所有内存页都标记为只读。一旦其中一个进程尝试写入某个内存页,就会触发一个保护故障(缺页异常),此时会陷入内核。
内核拦截写入,并为尝试写入的进程创建这个页面的一个新副本,恢复这个页面的可写权限,然后重新执行这个写操作,这时就可以正常执行了。
内核会保留每个内存页面的引用数。每次复制某个页面后,该页面的引用数减少一;如果该页面只有一个引用,就可以跳过分配,直接修改。
这种分配过程对于进程来说是透明的,能够确保一个进程的内存更改在另一进程中不可见。
fork 之后可以不执行exec ,直接执行其他方法
KOOM Copy on Write #
Linux SuspendAll方法不会让执行SuspendAll的线程suspend(保留着一个线程)
父进程等待子进程执行完成
为什么先suspend再fork #
如果去掉 fork()
前额外添加的虚拟机暂停请求,直接让 Debug.dumpHprofData()
在子进程暂停虚拟机线程,会发现暂停虚拟机线程的调用将被一直阻塞。
因为 fork() 得出的子进程仅会保留调用 fork()
的唯一线程,而虚拟机线程暂停需要当前线程等待其它线程到达 Suspend Check Point 以通知原线程。但子进程中的其它线程已经不复存在了,等待便不会再有任何回音,因此通过在原进程提前暂停虚拟机线程,欺骗子进程对虚拟机线程状态的检测,才能保证逻辑的正常运行。
hprof #
Hprof文件格式有明确组织方式,Android在Java的基础上新增了部分Tag。
Java Hprof格式 #
整体分为Header
和Record数组
两部分。
Header #
记录hprof的元信息 Hprof-Header
格式名和版本号:JAVA PROFILE 1.0.2 (
18byte
)标识符大小:
4byte
高位时间戳:
4byte
地位时间戳:
4byte
Header
总共占了18 + 4 + 4 + 4 = 32byte
Record数组 #
记录各个类型对应的数据 Hprof-Record
类型(
TAG
):表示Record对应的类型(1Byte
)时间戳(
TIME
):发生时间(4byte
)长度(
LENGTH
):记录数据的长度(4byte
)记录数据(
BODY
):记录的数据(${LENGTH}Byte
)
单条Record
总共占了1 + 4 + 4 + LENGTH byte
支持的TAG类型 #
一级Tag #
STRING_IN_UTF8 = 0x01
LOAD_CLASS = 0x02
UNLOAD_CLASS = 0x03
STACK_FRAME = 0x04
STACK_TRACE = 0x05
ALLOC_SITES = 0x06
HEAP_SUMMARY = 0x07
START_THREAD = 0x0a
END_THREAD = 0x0b
HEAP_DUMP = 0x0c - 堆内容
CPU_SAMPLES = 0x0d
CONTROL_SETTINGS = 0x0e
HEAP_DUMP_SEGMENT = 0x1c - 堆内容
HEAP_DUMP_END = 0x2c
二级Tag #
主要位于HEAP_DUMP
或HEAP_DUMP_SEGMENT
中
ROOT_UNKNOWN = 0xff
ROOT_JNI_GLOBAL = 0x01
ROOT_JNI_LOCAL = 0x02
ROOT_JAVA_FRAME = 0x03
ROOT_NATIVE_STACK = 0x04
ROOT_STICKY_CLASS = 0x05
ROOT_THREAD_BLOCK = 0x06
ROOT_MONITOR_USED = 0x07
ROOT_THREAD_OBJECT = 0x08
CLASS_DUMP = 0x20
INSTANCE_DUMP = 0x21
OBJECT_ARRAY_DUMP = 0x22
PRIMITIVE_ARRAY_DUMP = 0x23:占据到80%以上
Android Hprof格式 #
Header #
格式与Java的Header一致
Record数组 #
格式与Java的Record一致
支持的TAG类型 #
一级Tag #
STRING_IN_UTF8 = 0x01
LOAD_CLASS = 0x02
STACK_FRAME = 0x04
STACK_TRACE = 0x05
HEAP_DUMP_SEGMENT = 0x1c
HEAP_DUMP_END = 0x2c
二级Tag #
主要位于HEAP_DUMP_SEGMENT
中
Java所有的二级Tag
HEAP_DUMP_INFO = 0xfe:存储的是
堆空间的类型
,主要有以下三种HEAP_ZYGOTE = 0x5A
Z
系统堆空间HEAP_APP = 0x41
A
应用堆空间HEAP_IMAGE = 0x49
I
图片堆空间
ROOT_INTERNED_STRING = 0x89
ROOT_FINALIZING = 0x8a
ROOT_DEBUGGER = 0x8b
ROOT_REFERENCE_CLEANUP = 0x8c
ROOT_VM_INTERNAL = 0x8d
ROOT_JNI_MONITOR = 0x8e
ROOT_UNREACHABLE = 0x90
PRIMITIVE_ARRAY_NODATA = 0xc3