您的位置:首页 > 运维架构 > Linux

Unix/Linux的内存管理

2015-08-24 19:38 686 查看
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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: