Unix/Linux的内存管理

来源:互联网 发布:csgo控制台优化 编辑:程序博客网 时间:2024/05/18 17:39
1.内存分配和回收的相关函数
    STL容器->自动分配和自动回收;
    |
    C++语言->new/delete;/*不是函数,没有括号,是操作符;*/
    |
    C语言->malloc()/free();
    |
    Unix系统函数->sbrk()/brk();/*都可以自动分配和自动回收;*/
    |
    Unix系统函数->mmap()分配/munmap()回收;
    |        (//用户层)
--------------------------------------------------
    Unix系统调用(内核层)->kmalloc(),vmalloc();

2.进程的内存空间
    2.1进程和程序的概念
        程序是保存在硬盘上的"可执行文件";
        进程是运行在内存中的程序实例;
        也就是说,程序是静态的文件,进程是内存中真正运行的部分;
    2.2一个进程的内存空间如何组成 /* 重点中的重点 */

       +--------------------------------+ (3Gb)
       |6.栈stack                       |
       +--------------------------------+
       |5.堆heap                        |
       +--------------------------------+   ^
       |4.BSS段(没有初值或者初值为0)    |   |
       +--------------------------------+   |
       |3.数据段(有初值的全局变量)      |   |
       +--------------------------------+   |
       |2.只读数据                      |   |
       |1.代码区                        |
       +--------------------------------+ (0Gb)

---------------------------------------------------------------
6->栈区:    保存局部变量(包括函数形参,块变量,函数返回值等);
            不同函数调用的空间遵循先进先出的原则;
            /* 内存的分配和回收都自动进行; */
5->堆区:    动态分配内存的区域,也叫自由区,受系统保护;
            new/delete/malloc()/free()都是针对堆区;
            堆区的内存完全由程序员管理,包括分配和回收;
            如果程序员没有回收将造成内存泄漏,直到进程结束;
------------------------
4->BSS段:   没有初值或初值为0的普通全局变量;
            /* BSS段会在main()执行之前自动清0; */
------------------------
3->全局区:  保存static变量和初值非0的全局变量;
            /* 主函数执行前分配,进程结束时回收; */
2->只读常量区: 存储字符串的字面值及const全局变量;
               字符串字面值就是双引号""括起来的字符串;
               /* 在某些资料中把该区并入代码区; */
1->代码区:  存放代码(包括函数)的区域;/* 只读的区域; */
--------------------------------------------------------------
注:
    对于普通全局变量,若初值为0则放BSS段,若有初值则放全局区;
    对于普通局部变量(不管有没有初值)放在栈区;
    static修饰的变量(不管是全局还是局部的变量)都放在全局区;
    不建议static修饰局部变量,类似于钉子户,该消失的时候未能消失;
    const修饰的全局变量放在只读区;
    const修饰的局部变量放在栈区;

3.Unix/Linux的虚拟内存空间
    在Unix/Linux系统中,内存地址其实是一个虚拟的内存地址,不是真实的物理内存地址;
    每个进程在启动时,就先天赋予了0~4GB的虚拟内存地址(与实际物理内存大小无关); 虚拟内存地址本质上就是一个整数,这个整数通过"内存映射"对应一个物理内存的地址; 但先天不对应任何的物理内存; 虚拟内存地址自身存储不了任何数据,只有内存映射之后才能存储数据,否则引发段错误(core dumped);
    malloc()做内存分配时,先做内存的映射,然后返回虚拟内存地址; 程序员所接触到的内存地址都是虚拟内存的地址;
    在进程的这4G虚拟内存被赋予时,并不是都做了内存映射,或许只映射了几K的物理内存,让程序正常运行就够了;
    其中第0~3G是给用户使用的,叫用户空间;第3~4G是给系统内核使用的,叫内核空间;用户空间的程序不能直接访问内核空间,但可以通过内核提供的系统函数进入内核空间;
    其中第0-3G是每个进程都会有的,第3-4G是所有进程共享的,每个进程被分配有8K的内核空间,叫内核栈;
    内存的管理是以"字节"作为基本单位;内存的映射以"内存页"作为基本单位;在目前主流的操作系统中就是4096字节(4k,0x1000);
    内存映射可以映射物理内存,也可以映射硬盘上的文件;
    代码区和只读常量区在一起;
    BSS段和全局区在一起;
    堆区和栈区都是独立的;

Linux系统中,几乎一切都可以用文件表示(包括进程process);
    /proc/进程PID/ 目录下会记录进程相关的数据,但这个数据不是保存在文件中的,是内存中的数据,所以文件大小(占用磁盘的大小)为0;
    /proc和/sys都是内存文件系统;
    cat /proc/进程PID/maps 可以查看内存使用的信息;
    ls -l /proc/10566 | more //可以分页查看

每个进程都会使用0~3G的用户空间,从小到大的次序是:代码区,只读常量区,全局区,BSS段,堆区,栈区;
堆区和栈区离得非常远,堆区在靠近0的位置从小向大延展,栈区在靠近3G的位置从大向小延展;
/* * 进程内存空间演示 */#include <stdio.h>#include <stdlib.h>int i1 = 10;        //i1在全局区static int i2 = 20;    //static变量i2在全局区int i3;                //没有初值的全局变量i3在BSS段const int i4 = 40;    //const全局变量i4在只读区void func(int i5) {    //i5在栈区    int i6;            //局部变量在栈区    static int i7 = 70;    //static变量i7在全局区    const int i8 = 80;    //局部变量在栈区    int *pi = malloc(4);//栈区pi指向手动分配的内存堆区    char *s1 = "abc";    //s1保存字符串字面值指向只读常量区    char s2[] = "abc";    //s2是局部变量在栈区,"abc"是字符串字面值在只读区,数组保存字符串的一份拷贝,数据保存到栈区;//    printf("@栈区i5: %p\n", &i5);    printf("@栈区i6: %p\n", &i6);    printf("@全局区i7: %p\n", &i7);    printf("@栈区i8: %p\n", &i8);    printf("@栈区pi: %p, pi指向堆区: %p\n", &pi, pi);    printf("@栈区s1: %p\n", &s1);    printf("@栈区s1: %p, s1指向只读区@: %p\n", &s1, s1);    printf("@只读区\"abc\": %p\n", "abc");    printf("@栈区s2: %p, s2指向栈区: %p\n", &s2, s2);}int main() {    printf("pid = %d\n", getpid());    //取进程的ID;    printf("@全局i1: %p\n", &i1);    printf("@全局i2: %p\n", &i2);    printf("@BSS段i3: %p\n", &i3);    printf("@只读常量i4: %p\n", &i4);    printf("@代码func: %p\n", &func);    func(1);    printf("please use cat /proc/%d/maps \n", getpid());    printf("sleeping 100s ...\n");    sleep(100);        //睡眠100s后退出;    return 0;}



pid = 7851
@全局i1: 0x8049a2c
@全局i2: 0x8049a30
@BSS段i3: 0x8049a40
@只读常量i4: 0x8048714
@代码func: 0x8048484
@栈区i5: 0xbf970950
@栈区i6: 0xbf97093c
@全局区i7: 0x8049a34
@栈区i8: 0xbf970938
@栈区pi: 0xbf970934, pi指向堆区: 0x920b008
@栈区s1: 0xbf970930
@栈区s1: 0xbf970930, s1指向只读区@: 0x8048718
@只读区"abc": 0x8048718
@栈区s2: 0xbf97092c, s2指向栈区: 0xbf97092c

-----------#cat /proc/5841/maps--------------
00227000-003b7000 r-xp 00000000 08:18 1573894    /lib/libc-2.12.so
003b7000-003b9000 r--p 00190000 08:18 1573894    /lib/libc-2.12.so
003b9000-003ba000 rw-p 00192000 08:18 1573894    /lib/libc-2.12.so
003ba000-003bd000 rw-p 00000000 00:00 0
00b4f000-00b50000 r-xp 00000000 00:00 0          [vdso]
00e7c000-00e9a000 r-xp 00000000 08:18 1573887    /lib/ld-2.12.so
00e9a000-00e9b000 r--p 0001d000 08:18 1573887    /lib/ld-2.12.so
00e9b000-00e9c000 rw-p 0001e000 08:18 1573887    /lib/ld-2.12.so
08048000-08049000 r-xp 00000000 08:18 1048674    /home/uc/day03/a.out
08049000-0804a000 rw-p 00000000 08:18 1048674    /home/uc/day03/a.out
0920b000-0922c000 rw-p 00000000 00:00 0          [heap]
b7729000-b772a000 rw-p 00000000 00:00 0
b7743000-b7745000 rw-p 00000000 00:00 0
bf95e000-bf973000 rw-p 00000000 00:00 0          [stack]
---------------------------------------------

size命令可以查看程序中内存区域的情况:
#size a.out
   text       data        bss        dec        hex    filename
   1796        276         12       2084        824    a.out

    获取进程ID使用函数getpid();
    查看内存页的大小使用函数getpagesize();
    每个进程都有自己的0~4G的虚拟内存空间,但它们映射的实际物理内存完全不同,进程之间的内存空间互不干扰;
    不同进程的相同编号的虚拟地址空间对应不同的物理空间;

引发段错误的常见原因
    使用了没有映射的虚拟内存地址;
    执行了没有权限的操作,比如修改只读区;

字符串的处理和数据结构的基本应用
---------string_func.c----------------
/* * 字符串操作注意事项演示 * 2015-07-12 */#include <stdio.h>#include <stdlib.h>#include <string.h>/* 1字符串的声明和赋值 */void str_pointer_strcpy() {    /* =是在修改地址(将地址值赋值), strcpy()是修改地址中的内容(值); */    /* 指向只读区的用=, 否则用strcpy比较好 */    char *s1 = "abc";    /* s1在栈区,保存了"abc"(只读区)的首地址 */    char s2[] = "abc";    /* s2是局部变量(数组), s2保存了"abc"的拷贝 */    char *s3 = s2;        /* s3是普通局部变量在栈区,保存了数组s2的地址 */    s1 = "123";        //对! 改地址,此时s1指向了"123";    //strcpy(s1, "123");//段错误;修改只读区,此时s1是指向只读区的指针    //s2 = "123"; //数组是常指针,不能再次改变指向,只可以改变其中的内容    strcpy(s2, "123");    //对! 改变(s2数组的)内容    strcpy(s3, "123");    //对! 改值;因s3现在是s2数组的首地址,结果会将"123"存入数组    //s3 = "123"; //对! 改地址(栈区->只读常量区),但不安全;    char *pi = malloc(sizeof(int));    /* pi 指向分配的堆内存 */    //pi = "123";//将pi指向"123"只读区,用改地址的方式会造成堆内存泄漏(堆地址丢失);    strcpy(pi, "123");    //比较安全的方式;pi仍指向堆内存(4个字节有效),内存存放内容为"123"字符串;}/* 2字符串的长度用strlen();sizeof取的是大小不是长度 */void str_len_sizeof() {    int i;    char s4[] = { };    /* 数组的大小是0 */    printf("s4 addr = %p, strlen = %d, sizeof = %d\n", s4, strlen(s4), sizeof(s4));    strcpy(s4, "1234567");    /* 数组的可以当指针, strcpy关心的是指针 */    /* s4[0-7] 是否合法/安全? 不安全 */    /* strlen的参数是个指针, 而sizeof则会判断类型 */    printf("s4's addr: %p, s4[0]'s addr: %p, s4[1]'s addr: %p\n", s4, &s4[0], &s4[1]);    /* s4[0]就是s4 */    printf("s4's addr: %p, s4[0]: %p, s4[1]: %p\n", s4, s4[0], s4[1]);    /* s4[]存放了"1234567"的数据拷贝 */    /* s4是一个在栈区的局部变量 */    printf("strlen(s4) = %d, sizeof(s4) = %d\n", strlen(s4), sizeof(s4));    // strlen = 7, sizeof = 0    /* 如果把strcpy一行注释掉,运行结果是strlen和曾经有初值的一次一样,sizeof为0; */    for (i = 0; i <= strlen(s4) - 1; i++) {        printf("strlen(s4) = %d, s4[%d] = %x\n", strlen(s4), i, s4[i]);        /* s4[0-7] 是否合法/安全? 不安全 */        /* 指针可以当数组,数组可以当指针 */        /* 越界的会是乱码 */    }    printf("\n");}/* 3字符串的拼接,比如文件和所在目录拼接 */void str_sprintf() {    char *file = "hello.c";    char *path = "/home/soft01";    char buf[100] = { };    sprintf(buf, "%s/%s", path, file);    /* 字符串拼接常用的函数 */    /* 等价于     * strcpy(buf, path);     * strcat(buf, "/");     * strcat(buf, file);     */    printf("%s\n", buf);}/* 4利用指针的操作进行字符串功能,比如拆分 */void str_sscanf() {    char *s5 = "zhangfei 25";    char name[20] = { };    int age;    sscanf(s5, "%s %d", name, &age);    /* 将s5按格式拆分成name和age */    /*     * int i;     * for(i = 0; i < strlen(s5); i++){     *     if (*(s5+i) == ' ') {     *         memcpy(name, s5, i);     *         strcpy(age, s5+i+1);     *         break;     *     }     * }     */    printf("name = %s, age = %d\n", name, age);}/* 5字符串和基本类型之间的转换(int,double); */void str_int_convert() {    char *s6 = "123";    int num = 456;    //sscanf()/sprintf()    char buf2[20] = { };    sprintf(buf2, "%d", num);    /* 将num输出的456(字符串)放入字符数组buf2 */    printf("buf2 is string: %s\n", buf2);    int num2;    sscanf(s6, "%d", &num2);    /* 将s6的"123"作为输入, 作为数字类型放入num中 */    printf("num2 = integer: %d\n", num2);    /* 其实printf("%d\n", num);输出到屏幕时已经转换为字符串 */    /* scanf("%d", &num);要求从键盘输入时,输入的都是字符串 */    /* 从键盘输入的都作为字符串 */}/* C语言关于字符串操作的基本要求是特重点 */int main(){    str_pointer_strcpy();    str_len_sizeof();    str_sprintf();    str_sscanf();    str_int_convert();    return 0;}



0 0