进程概述和内存分配

本文是作者阅读TLPI(The Linux Programer Interface的总结),为了突出重点,避免一刀砍,我不会过多的去介绍基本的概念和用法,我重点会去介绍原理和细节。因此对于本文的读者,至少要求读过APUE,或者是实际有写过相关代码的程序员,因为知识有点零散,所以我会尽可能以FAQ的形式呈现给读者。

进程

一个进程的内存布局是什么样的?

每个进程所所分配的内存由很多部分组成,通常我们称之为段,一般会有如下段:

  • 文本段 包含了进程执行的程序机器语言指令,文本段具有只读属性,以防止进程通过错误指针意外修改自身的指令。
  • 初始化数据段包含了显示初始化的全局变量和静态变量,当程序加载到内存时,从可执行文件中读取这些变量的值
  • 未初始化数据段包含了未进行显式初始化的全局变量和静态变量,程序启动之前,系统将本段内所有的内存初始化为0。
  • 栈段是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,栈帧中存储了函数的具备变量,实参,和返回值。
  • 堆段是可在运行时动态进程内存分配的一块区域,堆顶端称作program break

注: 为什么要区分初始化数据段,和未初始化数据段呢?,未初始化数据段简称为BSS段,有何含义BSS全称为Block Started by Symbol,其主要原因在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间,相反,可执行文件只需要记录未初始化数据段的位置和所需大小即可。直到运行时才分配内存空间。通过size命令可以显示可执行文件的文本段,初始化数据段,未初始化数据段的段大小信息。

如何知道进程的文本段,初始化数据段和非初始化数据段的结束位置?

大多数UNIX实现中C语言编程环境提供了三个全局符号:etext,edata,end,可在程序内使用这些符号获取文本段,初始化数据段,未初始化数据段结尾处下一字节的地址。代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern char etext,edata,end;
int main()
{
    printf("%p\n",&etext);
    printf("%p\n",&edata);
    printf("%p\n",&end);
}

如何获取虚拟内存的页面大小?

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("page-size:%d\n",sysconf(_SC_PAGESIZE));
}

如何读取任一进程的命令行参数和程序名?

通过读取proc/PID/cmdline可以得到任一进程的命令行参数信息,如果想获取程序本身的命令行参数,可以使用proc/self/cmdline来读取,对于如何获取进程的程序名有如下两种方法:

  • 读取/proc/self/exe的符号链接内容,这个文件会通过符号链接到真正的可执行文件路径,是绝对路径,通过readlink可以读取其中链接的绝对路径名称
#include <stdio.h>
#include <unistd.h>
#include <string.h>
char * get_program_path(char *buf,int count);
char * get_program_name(char *buf,int count);

int main()
{
    //程序测试
    char buf[1024];
    bzero(buf,sizeof(buf));
    //打印路径名称
    printf("%s\n",get_program_path(buf,sizeof(buf)));
    bzero(buf,sizeof(buf));
    //打印程序名称
    printf("%s\n",get_program_name(buf,sizeof(buf)));

}

/*
 * 获取程序的路径名称
 */
char * get_program_path(char *buf,int count)
{
    int i=0;
    int retval = readlink("/proc/self/exe",buf,count-1);
    if((retval < 0 || retval >= count - 1))
    {
        return NULL;
    }
    //添加末尾结束符号
    buf[retval] = ‘\0‘;
    char *end = strrchr(buf,‘/‘);
    if(NULL == end)
        buf[0] = ‘\0‘;
    else
        *end = ‘\0‘;
    return buf;
}

/*
 * 获取这个程序的文件名,其实这有点多余,argv[0]
 * 就代表这个执行的程序文件名
 */
char * get_program_name(char *buf,int count)
{
    int retval = readlink("/proc/self/exe",buf,count-1);
    if((retval < 0 || retval >= count - 1))
    {
        return NULL;
    }
    buf[retval] = ‘\0‘;
    //获取指向最后一次出现‘/‘字符的指针
    return strrchr(buf,‘/‘);
}
  • 通过GNU C语言提供的两个全局变量来实现
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>

extern char * program_invocation_name;
extern char * program_invocation_short_name;
int main()
{
        printf("%s\n",program_invocation_name);
        printf("%s\n",program_invocation_short_name);
}

volatile关键字的作用?

将变量声明为volatile是告诉优化器不要对其进行优化,从而避免了代码重组。例如下面这段程序:

int a = 10;

int main()
{
    a = a + 1;
    while(a == 2);
}

对上面的代码使用gcc -O -S进行优化编译,查看其汇编代码。关键代码如下:

    movl    a(%rip), %eax
    addl    $1, %eax        //a = a + 1
    movl    %eax, a(%rip)   //写回内存
    cmpl    $2, %eax        //while(a == 2)

编译器对齐进行优化,发现a = a + 1while(a == 2)中间没有对a进行修改,因此根据代码的上下文分析后进行优化,直接拿%eax进行比较。但是编译器的优化仅仅只能根据当前的代码上下文来优化,如果在多线程场景下另外一个函数中对a进行了修改,但是这里却使用的是a的旧值,这就会导致代码逻辑上出现了问题,很难debug。我们来看看加了volatile关键字后情况变成什么样了。下面是加了volatile后的汇编代码:

    movl    a(%rip), %eax
    addl    $1, %eax
    movl    %eax, a(%rip)
    movl    a(%rip), %eax   //在比较之前重新读取了a的值
    cmpl    $2, %eax

volatile关键字远远在比我这里描述的更加复杂,这里有篇文章建议大家阅读一下,深刻了解下这个关键字的作用。C/C++ Volatile关键词深度剖析

内存分配

brk 和 sbrk的作用是很什么?

brk和sbrk是linux提供给我们的两个用于分配内存的系统调用,内存的分配其实就是将堆区的内存空间进行隐射和物理内存页进行关联。我们的程序会大量的调用这两个系统调用,这导致一个问题就是大量的系统

调用开销的产生,为此malloc和free封装了这两个函数,通过brk和sbrk预先分配一段比较大的内存空间,然后一点点的分配出去,通过维护内部的一个数据结构记录分配出去的内存块信息,方便后面的回收和合并

这样就避免了大量系统调用的调用开销。下面是这两个函数的函数原型:

       #include <unistd.h>
       int brk(void *addr);
       void *sbrk(intptr_t increment);

brk可以调整program break的位置,program break是堆的顶端,也就是堆最大可以增长到的地址,而sbrk则是设置program break为原有地址加上increment后的位置。sbrk(0)返回当前的

program break位置。

有哪些malloc的调试工具和库?

glibc提供了一些malloc调试库分别如下:

  • mtrace和muntrace函数分别在程序中打开和关闭对内存分配调用进行跟踪的功能
[[email protected] test]# cat mtrace.c
#include <mcheck.h>
#include <stdlib.h>
#include <stdio.h>

int
main(int argc, char *argv[])
{
    int j;

    mtrace();

    for (j = 0; j < 2; j++)
        malloc(100);            /* Never freed--a memory leak */

    calloc(16, 16);             /* Never freed--a memory leak */
    exit(EXIT_SUCCESS);
}

进行编译,生成mtrace调试信息,因为调试信息叫复杂,glibc提供了mtrace命令用户分析调试信息
[[email protected] test]# gcc -g mtrace.c
[[email protected] test]# export MALLOC_TRACE="/tmp/t" //设置这个环境变量,mtrace会读取这个环境变量,把调试信息输出到这个环境变量所指向的文件
[[email protected] test]# ./a.out
[[email protected] test]# cat /tmp/t
= Start
@ ./a.out:[0x400637] + 0x132a460 0x64
@ ./a.out:[0x400637] + 0x132a4d0 0x64
@ ./a.out:[0x400650] + 0x132a540 0x100
[[email protected] test]# mtrace ./a.out $MALLOC_TRACE

Memory not freed:
-----------------
           Address     Size     Caller              //在12行进行了二次内存分配,大小是0x64,在16行进行了一次内存分配,分配的大小是0x100
0x000000000132a460     0x64  at /root/test/mtrace.c:12 (discriminator 2)
0x000000000132a4d0     0x64  at /root/test/mtrace.c:12 (discriminator 2)
0x000000000132a540    0x100  at /root/test/mtrace.c:16

可以看出mtrace起到了内存分配的跟踪功能,会把所有的内存分配和释放操作就记录下来。

  • mcheck和mproe函数允许对已分配的内存块进行一致性检查。例如对已分配内存之外进行了写操作。
[[email protected] test]# cat mcheck.c
#include <stdlib.h>
#include <stdio.h>
#include <mcheck.h>

int main(int argc, char *argv[])
{
    char *p;
    if (mcheck(NULL) != 0) {                    //需要在第一次调用malloc前调用。
        fprintf(stderr, "mcheck() failed\n");
        exit(EXIT_FAILURE);
    }

    p = malloc(1000);
    free(p);
    free(p);                                    //doubel free
    exit(EXIT_SUCCESS);
}
[[email protected] test]# ./a.out
block freed twice
Aborted

上面只是简单的演示了其基本用法,更详细的用法参见man 文档。

  • MALLOC_CHECK_环境变量 提供了类似于mcheck的功能和mprobe的功能,但是MALLOC_CHECK_这种方式无需进行修改和重新编译,通过设置不同的值来控制对内存分配错误的响应方式下面是一个使用MALLOC_CHECK_环境变量的实现方式mcheck的功能的例子:
#include <stdlib.h>
#include <stdio.h>
#include <mcheck.h>

int main(int argc, char *argv[])
{
    char *p;
    p = malloc(1000);
    ++p;
    free(p);    //非法释放
    free(p);    //double free
    exit(EXIT_SUCCESS);
}

编译上面得到代码之前先导出下MALLOC_CHECK_环境变量
[[email protected] test]# export MALLOC_CHECK_=1 //其值应该是一个单数字,具体的含义可以参考man  3 mallopt
[[email protected] test]# gcc mcheck.c -lmcheck  //编译的时候链接mcheck即可
[[email protected] test]# ./a.out
memory clobbered before allocated block
Aborted

上面的三种方式都是通过函数库的形式给程序添加了内存分配的检测,和追踪功能,我们也可以使用一些第三方的工具来完成这些功能,比较流行的有Valgrind,Insure++等。

如何控制和监测malloc函数包?

linux提供了mallopt用来修改各选参数,以控制malloc所采用的算法,例如:何时进行sbrk进行堆收缩。规定从堆中分配内存块的上限,超出上限的内存块则使用mmap系统调用,此外还提供了mallinfo函数,这个函数会返回一个结构包含了malloc分配内存的各种统计数据。下面是mallinfo的接口声明和基本使用。

#include <malloc.h>
struct mallinfo mallinfo(void);
struct mallinfo {
   int arena;     /* Non-mmapped space allocated (bytes) */
   int ordblks;   /* Number of free chunks */
   int smblks;    /* Number of free fastbin blocks */
   int hblks;     /* Number of mmapped regions */
   int hblkhd;    /* Space allocated in mmapped regions (bytes) */
   int usmblks;   /* Maximum total allocated space (bytes) */
   int fsmblks;   /* Space in freed fastbin blocks (bytes) */
   int uordblks;  /* Total allocated space (bytes) */
   int fordblks;  /* Total free space (bytes) */
   int keepcost;  /* Top-most, releasable space (bytes) */
};

下面是一段代码显示了当前进程的malloc分配内存信息

#include <stdlib.h>
#include <stdio.h>
#include <mcheck.h>
#include <malloc.h>

static void display_mallinfo(void)
{
     struct mallinfo mi;
     mi = mallinfo();
     printf("Total non-mmapped bytes (arena):       %d\n", mi.arena);
     printf("# of free chunks (ordblks):            %d\n", mi.ordblks);
     printf("# of free fastbin blocks (smblks):     %d\n", mi.smblks);
     printf("# of mapped regions (hblks):           %d\n", mi.hblks);
     printf("Bytes in mapped regions (hblkhd):      %d\n", mi.hblkhd);
     printf("Max. total allocated space (usmblks):  %d\n", mi.usmblks);
     printf("Free bytes held in fastbins (fsmblks): %d\n", mi.fsmblks);
     printf("Total allocated space (uordblks):      %d\n", mi.uordblks);
     printf("Total free space (fordblks):           %d\n", mi.fordblks);
     printf("Topmost releasable block (keepcost):   %d\n", mi.keepcost);
}

int main(int argc, char *argv[])
{
    char *p;
    p = malloc(1000);
    display_mallinfo();
    free(p);
    printf("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    display_mallinfo();
    exit(EXIT_SUCCESS);
}

下面是运行后的结果:

[[email protected] test]# ./a.out
Total non-mmapped bytes (arena):       135168
# of free chunks (ordblks):            1
# of free fastbin blocks (smblks):     0
# of mapped regions (hblks):           0
Bytes in mapped regions (hblkhd):      0
Max. total allocated space (usmblks):  0
Free bytes held in fastbins (fsmblks): 0
Total allocated space (uordblks):      1024             //这是分配的内存,我的代码中分配的是1000,因为malloc会字节对齐,因此变成了1024
Total free space (fordblks):           134144
Topmost releasable block (keepcost):   134144
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Total non-mmapped bytes (arena):       135168
# of free chunks (ordblks):            1
# of free fastbin blocks (smblks):     0
# of mapped regions (hblks):           0
Bytes in mapped regions (hblkhd):      0
Max. total allocated space (usmblks):  0
Free bytes held in fastbins (fsmblks): 0
Total allocated space (uordblks):      0
Total free space (fordblks):           135168
Topmost releasable block (keepcost):   135168

关于mallopt的使用这里就略过了,因为这东西较复杂,笔者自己也没认真看过。如果你希望了解,我给你推荐的第一手资料绝对是man 3 mallopt

为什么要内存对齐,如何内存对齐?

关于为什么要内存对齐,我推荐给大家一篇文章Data alignment: Straighten up and fly right,通常我们在讨论内存的时候常常会使用byte来作为内存的最小分配单位,于是乎大家就认为内存是一个字节一个字节的进行读取的……,但其实这是一个误区,byte做内存的基本单位这是从程序员的角度来看待内存的,如果是CPU的话,它不会也这样看待,毕竟一次只读一个字节似乎有点太慢,的确,对于CPU来说,内存是一个个内存块来读取,内存块的大小通常是2的整数次幂。不同的硬件架构不同,一般是4或8个字节,如果字节不对齐会有什么后果呢?最直接的后果就是会导致你的程序变慢。具体分析如下:

对于单字节对齐的系统来说(这也正是程序员看到的内存状态)从地址0开始读取4个字节和从地址1开始读取4个字节没有任何区别,所以也不存在字节对齐的概念,对不对齐其实都一样。对于4字节对齐的系统来说,CPU一次要读取4个字节的内容,从地址0开始读取4个字节0~3,只需要读取一次就ok了。如果从1开始读取的话,需要读二次,第一次读0~3,第二次读4~7,然后截取这两个内存块的1~4这个区域,就是读取到的四个字节的内容了。因为CPU只会一个个内存块的边界开始读取一个内存块,地址1并不是内存块的边界,因此CPU会从0开始读取。就是这样的一个简单操作导致了CPU多进行了一次读操作,可想而知内存对齐该有多重要。关于内存对齐的更多分析请看我给大家推荐的文章。linux提供了posix_memalign和memalign两个函数用于分配字节对齐的内存地址,其函数原型如下:

       #include <stdlib.h>
       int posix_memalign(void **memptr, size_t alignment, size_t size);
       #include <malloc.h>
       void *memalign(size_t alignment, size_t size);

如何在栈上分配内存?

我们知道malloc是在堆上进行内存分配的,但是你有听过在栈上也可以分配内存的嘛,的确是可以的alloca就可以在栈上进行内存的分配,因为当前函数的栈帧是位于堆栈的顶部。帧的上方是存在可扩展空间,只需要改堆栈指针值即可,其函数原型如下:

       #include <alloca.h>
       void *alloca(size_t size);

通过alloca分配的内存不需要进行释放,因为函数运行结束后自动释放对应的栈帧,修改器堆栈指针为前一个栈帧的末尾地址。alloca是不是很神奇,笔者很想知道其实现原理。尽管上文中已经说到了,其实就是利用栈上的扩展空间,扩展了栈的空间,使用栈上的扩展空间来进行内存的分配。下面是其实现代码的汇编表示.

#include <stdio.h>
#include <alloca.h>
#include <stdlib.h>

int main()
{
    void *y = NULL;
    y = alloca(4);

}

        pushq   %rbp        //保存上一个栈帧的基址寄存器
        movq    %rsp, %rbp  //设置当前栈帧的基址寄存器
        subq    $16, %rsp   //开闭16个字节的空间,因为是向下增长,所以是subq
        movq    $0, -8(%rbp) //void *y = NULL
        movl    $16, %eax    //下面是一连串的地址大小计算,现在可以不用管这些细节
        subq    $1, %rax
        addq    $19, %rax
        movl    $16, %ecx
        movl    $0, %edx
        divq    %rcx
        imulq   $16, %rax, %rax
        subq    %rax, %rsp      //修改rsp的地址,也就是栈顶地址
        movq    %rsp, %rax
        addq    $15, %rax
        shrq    $4, %rax
        salq    $4, %rax
        movq    %rax, -8(%rbp)  //将分配的地址赋值给y 也就是y = alloca(4)
        leave
        ret
打印下y本身的地址和分配的地址如下:
y的地址:0x7ffd366b7fd8
分配的地址:0x7ffd366b7fb0

根据y的地址结合汇编代码可知,栈顶的地址是0x7ffd366b7fd8 - 8 =0x7ffd366b7fd0分配的地址是0x7ffd366b7fb0, 两者相差0x20。也就是说虽然分配的是4个字节,但是栈顶却减少了0x20个字节,那么现在的栈顶就是0x7ffd366b7fb0,之前的栈顶是0x7ffd366b7fd0,这中间的区域就是分配的空间,至于为什么是0x20这一应该是和malloc的初衷相同,考虑到字节对齐吧。

时间: 2024-10-12 09:49:12

进程概述和内存分配的相关文章

内存管理概述、内存分配与释放、地址映射机制(mm_struct, vm_area_struct)、malloc/free 的实现

http://blog.csdn.net/pi9nc/article/details/23334659 注:本分类下文章大多整理自<深入分析linux内核源代码>一书,另有参考其他一些资料如<linux内核完全剖析>.<linux c 编程一站式学习>等,只是为了更好地理清系统编程和网络编程中的一些概念性问题,并没有深入地阅读分析源码,我也是草草翻过这本书,请有兴趣的朋友自己参考相关资料.此书出版较早,分析的版本为2.4.16,故出现的一些概念可能跟最新版本内核不同.

delphi.memory.分配及释放---New/Dispose, GetMem/FreeMem及其它函数的区别与相同,内存分配函数

来自:http://www.cnblogs.com/qiusl/p/4028437.html?utm_source=tuicool&utm_medium=referral -------------------------------------------------------------------------------------------------------- 我估摸着内存分配+释放是个基础函数,有些人可能没注意此类函数或细究,但我觉得还是弄明白的好. 介绍下面内存函数前,先说

UNIX高级环境编程(15)进程和内存分配 &lt; 故宫角楼 &gt;

故宫角楼是很多摄影爱好者常去的地方,夕阳余辉下的故宫角楼平静而安详. ? 首先,了解一下进程的基本概念,进程在内存中布局和内容. 此外,还需要知道运行时是如何为动态数据结构(如链表和二叉树)分配额外内存的. 一 进程 1 进程和程序 进程:是一个可执行程序的实例. 程序:包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程.包含如下信息: 二进制格式标识:如最常见的ELF格式. 机器语言指令:对程序算法进行编码. 程序入口地址:标识程序开始执行时的起始指令位置. 数据:程序文件包含的变量

内存分配原理 -进程分配内存的两种方式,分别有系统调用完成brk() 和mmap()(不设计共享内存)

如何查看进程发生缺页中断的次数? 用ps -o majflt,minflt -C program命令查看. majflt代表major fault,中文名叫大错误,minflt代表minor fault,中文名叫小错误.           这两个数值表示一个进程自启动以来所发生的缺页中断的次数. 发成缺页中断后,执行了那些操作? 当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作: 1.检查要访问的虚拟地址是否合法 2.查找/分配一个物理页 3.填充物理页内容(读取磁盘,或者直接置0

C/C++程序内存分配(和Linux进程分配有一些区别)

C/C++程序内存分配 一.一个由C/C++编译到程序占用的内存分为以下几个部分: 1.栈区(stack)--由编译器自动分配释放,在不需要的时候自动清除.用于存放函数的参数.局部变量等.操作方式类似数据结构中的栈(后进先出). 2.堆区(heap)--一般由程序员分配释放,若程序员分配后不释放,程序结束后可能由OS回收.不同于数据结构中的堆,分配方式有些类似链表. 3.全局区(静态区)--全局变量和静态变量存储在这里.程序结束后由系统释放.在以前到C语言中,全局变量又细分为初始化的(DATA段

Netty源码—五、内存分配概述

Netty中的内存管理应该是借鉴了FreeBSD内存管理的思想--jemalloc.Netty内存分配过程中总体遵循以下规则: 优先从缓存中分配 如果缓存中没有的话,从内存池看看有没有剩余可用的 如果已申请的没有的话,再真正申请内存 分段管理,每个内存大小范围使用不同的分配策略 我们先总体上看下Netty内存分配的策略,然后再结合对应的数据结构来看看每种策略的具体实现. 总体分配策略 netty根据需要分配内存的大小使用不同的分配策略,主要分为以下几种情况(pageSize默认是8K, chun

SQLite剖析之动态内存分配

SQLite通过动态内存分配来获取各种对象(例如数据库连接和SQL预处理语句)所需内存.建立数据库文件的内存Cache.以及保存查询结果.我们做了很多努力来让SQLite的动态内存分配子系统可靠.可预测.健壮并且高效.本文概述SQLite的动态内存分配,软件开发人员在使用SQLite时可以据此获得最佳性能. 1.特性    SQLite内核和它的内存分配子系统提供以下特性:    (1)对内存分配失败的健壮处理.如果一个内存分配请求失败(即malloc()或realloc()返回NULL),SQ

程序的内存分配之堆和栈的区别

堆栈概述 ??在计算机领域,堆栈是一个不容忽视的概念,堆栈是两种数据结构.堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除.在单片机应用中,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场.要点:堆,队列优先,后进先出(例如:乘车排队,先来的排在前面先上车,后来的就要排的后面后上车).栈,先进后出(First-In/Last-Out)(例如:超市排队结账,大一点的超市收银台都是一段狭长的过道,本来下一个是你了,突然这个收银台说

c#程序内存分配

c#程序内存分配 进程可使用内存数就是操作系统给进程分配的最大地址,一般的32位操作系统提供给用户地址最大都是3g(操作系统自己保留1g),windows由于商业目的,对于个人用户只提供了2g地址,要使用3g扩展需要对系统配置作修改.还有就是intel的32位芯片实际上是支持36位寻址能力的(64g),但是也是出于商业目的对于大部分个人芯片都禁止了这个功能,这个禁止可能是物理上的限制个人用户几乎无法修改. 而且在操作系统中物理可用内存和进程实际占用内存是有区别的,因为有虚拟地址和交换区这种概念,