内存到哪里去了
原创|X侦探所事件簿|内存到哪里去了 https://mp.weixin.qq.com/s/3ofmJhRwIZD-GV8TYxVLGw
原创|X侦探所事件簿|内存到哪里去了
提示:公众号展示代码会自动折行,建议横屏阅读
「第一部分 前言」
我们都知道,程序的运行离不开内存。很多人都有这种直接朴素的想法,内存越大程序的运行速度越快。对于数据库来说,如果数据都能加载到内存中,不需要从磁盘读取,那速度肯定是杠杠的。但是,对于现在的应用来说,几十GB乃至TB级别的数据,都是常见的情况,但内存多是十几GB。所以,内存就是就是珍贵的资源,要精打细算的使用,那么这次我们就探究一下和内存相关的知识。这些知识将从两个方面着手,一是操作系统方面,从该方面我们讲述内存在这个层面是怎么分布的;二是MySQL方面,从该方面我们了解在运行中MySQL的各个模块的内存申请情况。以及最后,这两方面结合看所产生的一些问题。本章,我们从操作系统来了解一下有哪些内存概念,以及通过一些实验来验证这些概念。
1.1 运维中的内存
大多数时候,我们是从运维角度来了解内存的,看看系统有多少内存,程序占用多少内存,这里我们通过工具以及操作系统信息,简单了解程序运行时内存使用情况。
1.2 TOP
查看内存的第一个方法是使用top命令来查看程序的运行情况。如下图所示
图片中展示的PID是进程在操作系统中运行时ID编号,通过这个ID唯一标识一个进程。输出信息中,内存相关最重要的是VIRT和RES这两个指标,其中VIRT是使用的虚拟内存空间,RES是实际占用的常驻内存空间。
在这里面要解释一下虚拟内存空间和常驻内存空间。虚拟内存空间是通过mmap/malloc/new等方式,向操作系统声明要使用的空间,但是这些操作所获取的内存空间并没有被读取或者写入,此时没有使用内存条上物理空间;常驻内存空间是实际占用的内存空间,也就是通过读取或者写入内容,真正意义上占用了内存条上物理空间。
1.3 /proc/[pid]/status
进一步细致地查看内存的方式是通过/proc/[pid]目录下的status文件,可以看到内存的使用情况。如下图所示,通过ps获取进程的PID,并展示/proc目录下该PID的status的内容。
在该文件中,更详细的展示程序运行中内存使用情况,不同内核版本输出的信息不尽相同,所以需要根据实际环境查找对应手册。其中VmSize和top中的VIRT是相同含义,VmRSS和top中的RES是相同含义;在输出的内容中的内存使用还包括了VmExe等信息。这里VmExe是程序的代码段所需要的虚拟空间,VmData是程序运行时的虚拟数据空间的大小,VmExe是程序代码段的大小,VmLib是共享库代码空间的大小。
1.4 size
上面引入的VmExe等概念可以通过size命令静态的了解执行文件在内存中的相关信息,如下图所示
其中text是代码(也就是CPU指令)所占用的空间对应VmExe,data是初始值的全局变量空间,bss是没有初始值的全局变量的空间,这些内存空间是在程序运行启动时,就需要分配给进程的。
由size命令,引入代码段等概念,我们需要介绍一下Linux运行时,程序的内存空间分布,其情况如下图所示
所有代码的代码段都是从0x400000的虚拟地址空间开始的,从这里开始,以此向上存储的是代码空间,初始化全局变量空间,未初始化全局变量的空间。特殊说明一下,在不同的版本下面虚拟内存映射可能会有些差异,需要结合特定版本的手册来准确定位这些信息。
1.5 /proc/[pid]/maps
更进一步的查看内存空间的使用情况需要查看/proc/[pid]/maps文件,该文件的功能是记录了内存申请的虚拟地址(如malloc等内存分配函数)。如下所示,在maps文件中有如下内容
用于加在代码段的0x400000地址起始的代码空间:
主线程的堆栈空间:
子线程的堆栈空间:
所使用的动态链接库的地址空间:
所有类似如上空间的总和,构成了程序在内存中运行的全部虚拟空间。但这里要注意,这些空间很多可能没有实际存储数据,因此不会使用到内存条上的物理空间。在Linux下,只有当读写这些虚拟内存,触发了page fault中断,才会将虚拟内存和物理内存建立映射关系。这时的物理内存将会被统计到VmRSS(status文件)和RES(top命令)。
如上我们从运维的角度了解到内存的一些知识,下面将会通过简单的代码,验证一下关于mmap,malloc以及thread stack 局部变量和status文件中VmRSS等信息的关系。
「第二部分 代码看内存」
static int stop_thread = 0;
int sleep_in_second(int n)
{
struct timespec time_to_sleep, time_real_sleep;
time_to_sleep.tv_sec = n;
time_to_sleep.tv_nsec = 0;
nanosleep(&time_to_sleep, &time_real_sleep);
}
static void* thread_func(void *arg)
{
int temp = (int)((long long)arg);
printf("thread %d\n", temp);
if (temp == 1)
{
//thread 1
//local variables memory test
const int local_size = 4*1024*1024;
char buf[local_size];
memset(buf, 0, local_size);
//malloc memory test
const int malloc_size(8*1024*1024);
char *p = (char*)malloc(malloc_size);
for (int i = 0; i < malloc_size; i++)
{
p[i] = (i % 26) + 'a';
}
//mmap memory test;
const int mmap_size = 16*1024*1024;
char *p2 = nullptr;
p2 = (char*)mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
for (int i = 0; i < mmap_size; i++)
{
p2[i] = (i % 26) + 'a';
}
}
else
{
//thread 2
}
while (1)
{
if (stop_thread)
break;
sleep_in_second(1);
}
return 0;
}
int
main(int argc, char *argv[])
{
pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
const int stack_16m = 16*1024*1024;
pthread_attr_setstacksize(&attr, stack_16m);
//default 8M thread stack size
pthread_create(&thread,&attr,&thread_func,(void*)1);
//16M thread stack size
pthread_create(&thread,NULL,&thread_func,(void*)2);
getchar();
stop_thread = 1;
return 0;
}
//g++ mem_test.cc -o mem_test -lrt -lpthread -std=c++11
2.1 测试1
没有线程及相关代码,基线测试
size命令
status文件
2.2 测试2
创建线程,并定义本地变量未初始化/初始化
未初始化本地变量
size命令
status文件
初始化本地变量
size命令
status文件
结论1
通过对该测试的VmRSS进行观察,可以得出一个结论,线程本地stack上定义的变量,如果没有初始化,则不会产生实际的空间占用。初始化后,将会占用实际的物理空间。
2.3 测试3
malloc和mmap分配,未初始化/初始化
未初始化
size命令
status文件
初始化
size命令
status文件
以上是简单的实验,来验证内存的一些情况,并未涵所有的情况,比如在在线程函数内定义int类型的变量会是什么情况?对应的maps文件中内存的分配是什么情况?这些都留给读者自己去尝试,去验证。
「第三部分 结束语」
内存是程序运行的核心,在MySQL的运行中内存的使用是云用户关注的重要指标之一,这涉及到购买资源是否足够。在下一篇中,我们将会从MySQL的角度介绍内存是如何被MySQL所管理的。
「第四部分 参考文献」
https://man7.org/linux/man-pages/man5/proc.5.html
https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/size.html