joy keeps flowin'

Cow and Hprof

xx
目次

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() 之后,内核会把父进程的所有内存页都标记为只读。一旦其中一个进程尝试写入某个内存页,就会触发一个保护故障(缺页异常),此时会陷入内核。

内核拦截写入,并为尝试写入的进程创建这个页面的一个新副本,恢复这个页面的可写权限,然后重新执行这个写操作,这时就可以正常执行了。

image

内核会保留每个内存页面的引用数。每次复制某个页面后,该页面的引用数减少一;如果该页面只有一个引用,就可以跳过分配,直接修改。

这种分配过程对于进程来说是透明的,能够确保一个进程的内存更改在另一进程中不可见。

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格式 #

image Java Hprof格式

整体分为HeaderRecord数组两部分。

记录hprof的元信息 image Hprof-Header

Header总共占了18 + 4 + 4 + 4 = 32byte

Record数组 #

记录各个类型对应的数据 image Hprof-Record

单条Record总共占了1 + 4 + 4 + LENGTH byte

支持的TAG类型 #

一级Tag #

二级Tag #

主要位于HEAP_DUMPHEAP_DUMP_SEGMENT

Android Hprof格式 #

image Android Hprof格式

Header #

格式与Java的Header一致

Record数组 #

格式与Java的Record一致

支持的TAG类型 #

一级Tag #

二级Tag #

主要位于HEAP_DUMP_SEGMENT

#

完整Android hprof #

image
标签:
Categories: