文章目录

  • 问题描述
  • 问题分析
    • 针对问题1 的猜测:
    • 针对问题2 的猜测:
  • 原理追踪
  • 总结

问题描述

事情开始于一段内存问题,通过gperf工具抓取进程运行过程中的内存占用情况。
分析结果时发现一个有趣的事情,top看到的实际物理内存只有几兆,但是pprof统计的内存信息却达到了几个G(其实这个问题用gperf heap profiler的选项也能很好的验证想法,但是还是想探索一番)。

很明显是创建线程时产生的内存分配,且最终的分配函数是__pthread_create_2_1,这是当前版本glibc创建线程时的实现函数,且在该函数内进行线程空间的分配。

查看进程代码,发现确实有大量的线程创建,我们知道线程是有自己独立的栈空间,top的 RES统计的是当前进程占用物理内存的情况,也就是当用户进程想要申请物理内存的时候会发出缺页异常,进程切换到内核态,由内核调用对应的系统调用取一部分物理内存加入页表交给用户态进程。这个时候,使用的物理内存的大小才会被计算到RES之中。

回到top数据和pprof抓取的内存数据对不上的问题,难道单独线程的创建并不会占用物理内存?

到现在为止可以梳理出以下几个问题:

  1. 线程的创建消耗的内存在哪里? (猜测可能在栈上,因为top的VIRT确实很大)
  2. 消耗的内存大小 是如何判断的?(目前还不太清楚,不过以上进程代码是创建了800个线程,算下来平均每个线程的大小是10M了)

问题分析

  1. 为了单独聚焦线程创建时的内存分配问题,编写如下的简单测试代码,创建800个线程:

    #include <cstdio>
    #include <cstdlib>
    #include <thread>void f(long id) {fprintf(stdout, "create thread %ld\n",id);sleep(10000);}int main()
    {long thread_num = 800;                    // client thread numstd::vector<std::thread> v;for (long id = 0;id < thread_num; ++id ) {std::thread t(f,id); t.detach();fprintf(stdout, "exit ...\n");}printf("\n");sleep(4000);  return 0;
    }
    

    单纯的创建线程,并不做其他的内存分配操作。

  2. 为了抓取该进程的内存分配过程,我们加入gperf工具来运行查看。

    #当前shell的环境变量中加入tcmalloc动态库的路径
    #如果没有tcmalloc,则yum install gperftools即可
    env LD_PRELOAD="/usr/lib/libtcmalloc.so"#编译加入链接tcmalloc的选项
    g++ -std=c++11 test.cpp -pthread -ltcmalloc#使用会生成heap profile的方式启动进程
    #开启只监控mmap,mremap,sbrk的系统调用分配内存的方式,并且ctrl+c停止运行时生成heap文件
    HEAPPROFILESIGNAL=2  HEAP_PROFILE_ONLY_MMAP=true HEAP_PROFILE_INUSE_INTERVAL=1024 HEAPPROFILE=./thread ./a.out
    
  3. 进程运行的过程中我们使用pmap查看进程内存空间的分配情况
    pmap -X PID
    输出信息如下

    其中:
    address为进程的虚拟地址
    size为当前字段分配的虚拟内存的大小,单位是KB
    Rss为占用的物理内存的大小
    Mapping为内存所处的区域

统计了一下size:10240KB 的区域刚好是800个,显然该区域为线程空间。所处的进程内存区域也不在heap上,占用的物理内存大小大小也就是一个指针的大小,8B
使用pmap PID再次查看发现线程的空间都分布在anno区域上,即使用的匿名页的方式

匿名页的描述信息如下:

The amount of anonymous memory is reported for each mapping. Anonymous memory shared with other address spaces is not included, unless the -a option is specified.
Anonymous memory is reported for the process heap, stack, for ‘copy on write’ pages with mappings mapped with MAP_PRIVATE.

即匿名页是使用mmap方式分配的,且会将使用的内存叶标记为MAP_PRIVATE,即仅为进程用户空间独立使用。

针对问题1 的猜测:

到现在为止我们通过工具发现了线程的内存分配貌似是通过mmap,使用匿名页的方式分配出来的,因为匿名页能够和其他进程共享内存空间,所以不会被计入当前进程的物理内存区域。
关于进程的内存分布可以参考进程内存分布,匿名页是在堆区域和栈区域之间的一部分内存区域,pmap的输出我们也能看出来mmapping的那一列。

针对问题2 的猜测:

那为什么会占用10M的虚拟内存呢(size那一列),显然也很好理解了。因为线程是独享自己的栈空间的,所以需要为每个线程开辟属于自己的函数栈空间来保存函数栈帧和局部变量。
ulimit -a能够看到stack size 那一行是属于当前系统默认的进程栈空间的大小。

这里可以通过ulimit -s 2048 将系统的默认分配的栈的大小设置为2M,再次运行程序会发现线程的虚拟内存占用变为了2M

是不是很有趣。
到了这里,我们仅仅是使用工具进行了线程内存的占用分析,但问题并没有追到底层。

原理追踪

我们上面使用了gperf的heap proflie运行了程序,此时我们ctrl+c终端进程之后会在当前目录下生成很多个.heap文件,使用pprof 的svg选项将文件内容导出
pprof --svg a.out thread.0001.heap > thread.svg
将导出的thread.svg放入浏览器中可以看到线程内存占用的一个calltrace,如下(如果程序中链入了glibc以及内核的静态库,估计calltrace会庞大很多):

也就是线程创建时的栈空间的分配最终是由函数__pthread_create_2_1分配的。

PS:这里的calltrace 仅仅包括mmap,mremap,sbrk的分配,因为我们在进程运行的时候指定了HEAP_PROFILE_ONLY_MMAP=true 选项,如果各位仅仅想要确认malloc,calloc,realloc等在堆上分配的内存大小可以去掉该选项来运行进程。
输出svg的时候增加pprof的--ignore选项来忽略mmap,sbrk的分配内存,这样的calltrace就没有他们的内存占用了,仅包括堆上的内存占用
pprof --ignore='DoAllocWithArena|SbrkSysAllocator::Alloc|MmapSysAllocator::Alloc' --svg a.out thread.0001.heap > thread.svg

查看glibc的线程创建源码pthread_create.c
函数__pthread_create_2_1 调用ALLOCATE_STACK为线程的数据结构pd分配内存空间。

versioned_symbol (libpthread, __pthread_create_2_1, pthread_create, GLIBC_2_1)int
__pthread_create_2_1 (newthread, attr, start_routine, arg)pthread_t *newthread;const pthread_attr_t *attr;void *(*start_routine) (void *);void *arg;
{......struct pthread *pd = NULL;int err = ALLOCATE_STACK (iattr, &pd);if (__builtin_expect (err != 0, 0)......
}

ALLOCATE_STACK函数实现入下allocatestack.c
分配的空间大小会优先从用户设置的pthread_attr属性 attr.stacksize中获取,如果用户进程没有设置stacksize,就会获取系统默认的stacksize的大小。

接下来会调用get_cached_stack函数来获取栈上面可以获得的空间大小size以及所处的虚拟内存空间的地址mem。

最后通过mmap将当前线程所需要的内存叶标记为MAP_PRIVATE和MAP_ANONYMOUS表示当前内存区域仅属于用户进程且被用户进程共享。

详细实现如下:

static int
allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,ALLOCATE_STACK_PARMS)
{....../* Get the stack size from the attribute if it is set.  Otherwise weuse the default we determined at start time.  */size = attr->stacksize ?: __default_stacksize;......void *mem;....../* Try to get a stack from the cache.  */reqsize = size;pd = get_cached_stack (&size, &mem);if (pd == NULL){/* To avoid aliasing effects on a larger scale than pages weadjust the allocated stack size if necessary.  This wayallocations directly following each other will not havealiasing problems.  */#if MULTI_PAGE_ALIASING != 0if ((size % MULTI_PAGE_ALIASING) == 0)size += pagesize_m1 + 1;#endif/*mmap分配物理内存,并进行内存区域的标记*/mem = mmap (NULL, size, prot,MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);if (__builtin_expect (mem == MAP_FAILED, 0)){if (errno == ENOMEM)__set_errno (EAGAIN);return errno;}

总结

glibc用户态的调用到最后仍然还是内核态进行实际的物理操作。
至此,关于线程创建时的内存分配追踪就到这里了。我们会发现操作系统的博大精深和环环相扣,使用一个个工具验证自己的猜测, 再从原理发掘前人的设计,这样就会对整个链路有了一个更加深刻的理解。

至于更加底层的内核实现,如何将物理内存与用户进程进行隔离且互不影响,这又是一段庞大复杂的设计链路。有趣的事情很多,慢慢来~

Linux创建线程时 内存分配的那些事相关推荐

  1. 32位linux进程线程在内存中的样子

    1.线程诞生史 1.1 线程诞生的原因 早期是没有线程概念的,只有进程的概念,操作系统以进程为调度单位.--可以这么来理解:早期进程相当于现在的单线程的进程(只有一个线程的进程,创建进程时,里面有一个 ...

  2. 创建线程时,需要创建的内容

    请参看文献:线程调度,这样就能明白为什么需要有TCB,栈等: 创建线程时,需要初始化的参数: void ThreadCreate(A){TCB *tcb=malloc(); //申请一段内存作为TCB ...

  3. linux创建新进程就分配空间,linux几种创建进程的方法

    在Linux中主要提供了fork.vfork.clone三个进程创建方法. 在linux源码中这三个调用的执行过程是执行fork(),vfork(),clone()时,通过一个系统调用表映射到sys_ ...

  4. Linux内核中常见内存分配函数

    1.      原理说明 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示.四级页表分 ...

  5. linux 创建线程函数吗,[笔记]linux下和windows下的 创建线程函数

    linux下和windows下的 创建线程函数 #ifdef __GNUC__ //Linux #include #define CreateThreadEx(tid,threadFun,args) ...

  6. linux c 指针和内存分配内存,Linux C语言指针与内存学习笔记

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 环境准备 Ubuntu 操作系统.VIM 编辑器.GCC 编译器.GDB调试器. 初始指针 通过两个数的交换引入指针指针 ...

  7. linux c 指针和内存分配内存,linux-C基础系列-内存管理(野指针).md

    野指针概述 野指针通常指的是指针变量中保存的值不会死一个合法的内存地址,但又对其访问.需要注意的是野指针不是空指针,而是指向内存不可用的指针. C语言中对于空指针(NULL)是可以判断出来的,但是野指 ...

  8. 【kernel 中内存分配那点事】

    首先呢作为车载bsp开发人员,写大量的内核代码是不现实的事情,多数都是修修改改,但是要有内核代码阅读浏览理解的能力,毕竟linux kernel 还是很nb 的,所有技术人员深入研究内核代码是必须的, ...

  9. Linux 进程资源分配,linux 进程管理和内存分配

    1.进程相关概念 进程:正在运行中的程序 内核功用:进程管理.文件系统.网络功能.内存管理.驱动程序.安全功能等 Process:运行中的程序的一个副本,是被载入内存的一个指令集合 进程 ID(Pro ...

最新文章

  1. sgn matlab,matlab中.*的问题
  2. Linux下搭建Tomcat服务器
  3. Redis源码剖析(七)监视功能
  4. ActionTileViewController.js
  5. classmethod 继承_让人眼花缭乱的类继承
  6. 前端emojs_Emoji-Chat emoji表情包发送及显示兼容web端、移动端
  7. CSDN-markdown编辑器语法说明
  8. iOS 获取APP名称 版本等
  9. 良心啊,做电商要是早点在这几个网站学习,也不至于被黑产坑啊
  10. gnss到底是什么呢
  11. 网络的高可用性(一)
  12. P2E游戏+保护濒危动物是否值得一玩,链游Pettoverse全面分析
  13. JS将任意格式的时间转为Date对象
  14. 如何说服老板页面兼容IE9+
  15. 删除数组中的重复项(保留最后一次出现的重复元素并保证数组的原有顺序)
  16. 荣耀note10无缘鸿蒙,赵明确认荣耀NOTE10 真机参数疑似全曝光!
  17. MATLAB——根轨迹原理及其Matlab绘制
  18. oracle 表空间转换,Oracle表空间数据文件移动的方法
  19. 目前已完成linux适配的软件,WPS Linux版与国产统一操作系统UOS完成适配:符合国人使用习惯...
  20. 货车进货路线问题java代码实现_货车出行路线规划-出行路线规划-开发指南-Android 地图SDK | 高德地图API...

热门文章

  1. poj 2559 Largest Rectangle in a Histogram 栈
  2. npm start 作用
  3. C#_Socket网络编程实现的简单局域网内即时聊天,发送文件,抖动窗口。
  4. eclipse设置保护色非原创
  5. MVC 中的 ViewModel
  6. C# 异步读取数据库里面的数据与绑定UI的解决办法
  7. recover 没有捕获异常_GO语言异常处理机制panic和recover分析
  8. 码云nacos下载_nacos安装,配置以及持久化
  9. java变量存储位置_java 中变量存储位置的区别
  10. 如何用burp抓取手机的流量_用企业微信SCRM如何搭建流量新体系