虚拟内存
计算机的CPU很早就运行在保护模式下,为每个进程提供独立的虚拟内存空间。在32位系统上这个空间大小是4GiB(2的32次方),在64位系统上这个空间就非常大以至于也不用关心它的大小,肯定是远远大于物理内存的大小。每个进程的虚拟内存地址从高到低分为内核区域和用户进程区域。进程区域按地址从高到低包含:用户栈(stack)、共享库映射区域、运行时堆(heap)、程序相关信息。其中程序的变量占用的内存在用户栈上,动态分配的内存(malloc等函数)在堆上。进程打开的文件所用的内存不在虚拟内存区域,不算这个进程的内存。从这个角度看 hcache 根据进程查看进程使用的 PageCache 大小 是对的。hcache 里统计的是进程虚拟内存中映射到进程里的文件在 Page Cache 里的缓存情况。.so 文件是动态链接库。进程直接用内存映射的方式将这些链接库映射到进程的虚拟内存中。这是进程读写文件的另外一种高效的方式。要求是映射文件的起始地址和偏移都是内存页大小4KiB对齐。这些动态链接库大小都不大,这里继续忽略。这两笔记录字段分别是:对应vma地址的start和end地址(总地址是48位)、内存读写权限(r读w写s共享p私有)、文件偏移(如果是匿名映射默没有文件,偏移默认为0)、映射的文件 inode 。所以这两笔记录都是共享访问,是共享内存相关。先看第一个是deleted的文件。再看看进程的smap文件里这个 SYSV0bae0001 信息。这个通过删除的映射文件实现的共享内存总大小是68651384 kB(65 GB)(注:内存映射并不会实际分配物理内存,只有在访问到具体的页发生缺页中断内核才会实际分配物理内存),其中由该进程postmaster读写访问导致驻留内存的大小是1360532 kB(1GB)。这个共享内存总大小正好对应的是PostgreSQL的shared buffers 大小。postgresql的checkpoint进程访问该共享内存更多一些。这里还有一个Pss字段表示是该进程独自使用该共享内存的内存大小。用下面的命令可以看所有PostgreSQL 进程对这个共享内存实际使用内存,可以看出大小不一。ps -u postgres o pid= | sed 's#.*#/proc/&/smaps#' |xargs grep SYSV0bae0001 -A3
复制
同样的方法查看另外一个文件 /dev/shm/PostgreSQL.2199562942 内存情况,结果还不大一样,Size 很小。
这两个文件表现形式不一样,是因为对应的是PostgreSQL中的两个共享内存,并且默认实现方法还不一样。当然要看进程的Pss 也不需要这么麻烦。使用 smem 命令可以查看所有进程的Rss和Pss 、Uss内存。smem -U postgres -s uss -r
复制
sRSS(Resident set size):单个进程实际占用的物理内存大小。如果把所有进程的RSS相加会超过物理内存总大小,因为这里面包含了各进程之间共享的内存。共享的链接库也被计算多次。
sPSS(Proportional set size):单个进程实际占用的物理内存大小,共享库的内存会被多个进程分摊计算(分摊算法有点复杂,这里不深究)。
sUSS(Unique set size):进程独自占用的物理内存(私有内存,不包括共享库等内存)。
共享内存
共享内存的实现方法通常有四种:system v 共享内存、posix共享内存、mmap内存映射、windows共享内存。windows共享内存是windows系统里用法。这里我们重点看前面三个的特性。SYSTEM V
- 调用shmgt生成共享内存块,返回shmid。这个在进程的maps里可能看到。
通过 ipcs -m 命令可以查看使用SYSTEM V生成的共享内存。比如说上面这个 PostgreSQL 实例。bytes 是共享内存最大大小,70299017216 Byte是 65.5GB,比 shared_buffers 略大一点。 nattch表示挂载到该共享内存的进程数(lsof 命令查看是哪些进程)。上面是没有访问的时候,下面是有压测的时候。由此可以确定 PostgreSQL 的 shared buffer 对应的共享内存是使用 system V 这种实现方式。这一点也通过 PostgreSQL的参数 shared_memory_type 确认了。shared_memory_type 参数是PG 12版本新增的参数用于启动时指定shared memory的类型,有三种值:smmap
ssysv
swindows
该参数在linux上默认值在 PG 9.3 版本后是mmap,在9.3之前是sysv。上面参数是被我修改过。通常新版本的参数肯定有其好的原因,使用 mmap 共享内存方式就不需要设置内核参数 kernel.shmmax 和 kernel.shmall 。使用这种方式的共享内存在进程的maps文件里映射记录特征就是映射到文件 /SYSVXXXXXXXX(deleted)。文件创建映射后就被从文件系统解除链接,但是文件的 inode 和 data还在被 PostgreSQL 主进程使用,所以不会实际删除,只是外部人员无法再访问和映射这个文件,而 PostgreSQL 的子进程(包括客户端会话进程)会从主进程或者子进程里继承这个内存映射。最后,除了能在 ipcs 命令里看出这个共享内存的大小,在free 命令的 shared 字段也能看到这种共享内存大小。同时这个大小也是计入 cache 数据中。当清理掉 PageCache 中的文件缓存后,cache 大小就跟 shared 大小一致。由此可见使用system V 创建的共享内存不能从 PageCache 里清理,也不能回收,所以不计入free 命令的 available 字段。在 atop 命令的 shmem 和 shrss 也可以看到这个共享内存的大小最后再说一个 SYSTEM V 跟 mmap 方式的区别。SYSTEM V 方式产生的共享内存,必须显示的销毁才会释放物理内存。如果 PostgreSQL 进程异常退出,这部分内存就无法释放了。此时可以重启主机,或者使用 ipcrm 命令删除共享内存。OK。再回到最初的进程的 maps 文件,里面还有另外一个内存映射文件 /dev/shm/PostgreSQL.2199562942 。这个是 PG 中另外一个动态共享内存的映射文件,它默认实现方式是 posix 。参数 dynamic_shared_memory_type 的值有三种:ssysv
sposix
smmap
接下来我们就看看 posix 实现的共享内存的特点。POSIX
POSIX 本质上跟内存映射文件 mmap 是一样的,区别在于 POSIX 映射的是tmpfs文件系统。这是Linux的临时文件系统,将内存的一部分空间拿来当文件系统使用,可以直接通过读写文件来读写该部分内存。使用 tmpfs 的有多个目录,我们只看 /dev/shm 这个目录。默认大小是内存的一半。跟前面说的内存映射一样,在文件系统挂载的时候并不会实际分配物理内存,只有读写到具体的内存页发生缺页中断才会分配物理内存。通过 df 倒是能看出实际使用空间就是占用的物理内存大小。上面因为动态共享内存选择的是 POSIX 方式,所以映射文件在 /dev/shm,如果选择的是 mmap 方式,则映射文件在 PG的 data 目录下的pg_dynshmem 下。由于我没有碰到动态共享内存占用内存问题,并且这个文件大小也很小,我就略过它。可惜 shared_memory_type 不支持 POSIX,只好改回 mmap 看。MMAP
当 PG 的共享内存使用 mmap 方式时,进程 maps 记录里多了一个文件 /dev/zero 的映射,并且也是 deleted 状态。这里不解的是还有文件 /SYSV0bae0001 (deleted),不过好在 Size 只有 4KB 就忽略了。而 /dev/zero 文件的Size有65.5 GiB,应是 shared_buffer 对应的共享内存无疑。这个大小也能在 free 命令的 shared 字段看出,同时也是计入 cache 中。当清理PageCache中文件缓存时,这个共享内存也是不能清理和回收的。不过我猜测,内存映射文件的内存虽然不能回收,但是在主机可用内存不足的时候应该是能被换出(swap out)。感觉好像跟 SYSTEM V 一样。不过看 atop 命令就能看出区别。atop 的shmem 字段是共享内存的大小,shrss 却不包含。同时 ipcs -m 里也没有这个 shared buffer 对应的共享内存。当 PostgreSQL 进程异常退出时,这部分共享内存会很快释放掉。这个跟 SYSTEM V 方式有很大不同。
总结
共享内存三种方法最底层的系统调用依然是mmap,只不过处理细节不一样。PG的shared_buffer 支持 system v 和 mmap。mmap 将文件 /dev/zero 映射到内存中作为共享内存,其内存会体现在 free 的 cache 和 shared 字段里,以及 atop 的 shmem 里但shrss 里没有。system v 实现的共享内存会体现在 free 的 cache 和 shared 字段里,以及atop 的 shmem 和 shrss 里。posix 实现的共享内存是将内存映射到 tmpfs 文件系统,可以方便的调整共享内存大小,所以 PG 用它来实现动态共享内存。截止目前的测试,内存页表的大小都是 4KiB 。当物理内存很大以及进程共享内存很大,进程又很多时,页表的空间管理成本很高。所以Linux还提供了大页技术(HugeTable),后来又有了透明大页。ORACLE 和 PG 都支持大页,都建议关闭透明大页。使用大页时共享内存特点又是如何呢?写不完了,只好再开一篇文章再说。除此之外,ORACLE 共享内存也有两种实现方案,是否跟 PG 表现一样我们也下篇文章再看。