Linux进程内存管理
数据的内部存储
问题: 对于多个字节存储的数据而言,数据存储顺序的问题就显现出来了!
大小端问题:(big endian / little endian)
-
通信系统中的大小端:在串口等串行通信中,一次只能发送1个字节。这时候我要发送一个int类型的数就遇到一个问题。int类型有4个字节,我是按照:byte0 byte1 byte2 byte3这样的顺序发送,还是按照byte3 byte2 byte1 byte0这样的顺序发送。规则就是发送方和接收方必须按照同样的字节顺序来通信,否则就会出现错误。这就叫通信系统中的大小端模式。这是大小端这个词和计算机挂钩的最早问题。
-
计算机存储系统的大小端: 在计算机内存/硬盘/Nnad中。因为存储系统是32位的,但是数据仍然是按照字节为单位的。于是乎一个32位的二进制在内存中存储时有2种分布方式:高字节对应高地址(小端模式)、高字节对应低地址(大端模式)。
-
说明: 现实的情况就是:有些CPU公司用大端(譬如C51单片机);有些CPU用小端(譬如ARM)。(大部分是用小端模式,大端模式的不算多)。于是乎我们写代码时,当不知道当前环境是用大端模式还是小端模式时就需要用代码来检测当前系统的大小端。
-
检测方法:
方法1:使用共用体
//共用体中很重要的一点:a和b都是从u1的低地址开始的
union myunion
{
int a;
char b;
};
//如果是小端模式返回1,如果是大端模式返回0
int is_little_endian(void)
{
union myunion u1;
u1.a=1;
return u1.b;
}
方法2:指针方式来测试机器的大小端:
//指针方法:如果是小端模式返回1,如果是大端模式返回0
int is_little_endian2(void)
{
int a=1;
char b=*((char *)(&a));
return b;
}
或者:
int main(void){
int a=0x12345678;
char *p;
p=(char*)(&a);
if(*p==0x78)
printf("The little endian\n");
else
printf("The big endian\n");
return 0;
}
- 测试数据存储方式的函数:(由大小端测试拓展):有助于编写移植性良好的代码
#include<stdio.h>
/*数据存储形式测试函数,大端法返回0,小端法返回1*/
int test_endian(void){
int a=0x12345678;
char *p;
p=(char *)(&a);
if(*p == 0x78)
return 1;
return 0;
}
int main(void){
if(test_endian()==1){
/*小端法*/
/*相应的操作*/
}
else{
/*大端法*/
/*相应的操作*/
}
return 0;
}
C程序的存储布局(内存四区)
代码段:
- 代码段是进程中最重要的一个段。
- 正文段通常是只读的,防止程序无意识修改造成错误。
- 一个程序在多数情况下是不需要更改自身代码的,只有一种情况例外,就是一些长时间运行的升级程序。例如:对于一个24小时运行的服务器程序,要在不停止程序的情况下完成部分代码的更换,该如何实现呢?这时候直接改写代码段可能就是唯一-的办法了, 至少在曾经的一段历史时期里是这样的。但是现在我们有了一种更安全,更可靠的方法来解决这个问题。这个解决方案就是采用共享库的形式。
- 正文段通常是可共享的。
数据段和缓冲段:
- 初始化数据段:初始化数据段(.data):通常也称为数据段,它包含程序中明确给定初值的全局变量和静态变量。例如,int max= 100; (全局变量)或者static int count=10; 初始化数据段在编译的时候确定该段的大小,在程序运行过程中该段的大小不能发生改变。
- 非初始化数据段: 非初始化数据段(.bss): 存储在这个段中的数据通常是没有明确给定初值的全局变量和静态变量。例如:int max; (全局变量)或者static int count;。
- data段和bss段的特点: bss段中的数据会初始化为0或者NULL(指针变量),如果全局变量和静态变量本身有给定值,而这个值是0或者NULL时,编译器会将其内容写到bss段,而不是data段。bss段中的内容并不作为程序文件的一部分,而被保存到外存中,程序文件只是标记变量的大小和属性等信息,而data是属于程序文件的。测试代码如下:
说明:编译下列程序后看文件大小变化!!!
#include<stdio.h>
int a[30];
//int a[30]={10,10,10,..,10};//写30个10
//int a[30]={0,0,0,...,0};//写30个0
int main(){
printf("hello world\n");
return 0;
}
栈:
- 特点: 所有的自动变量以及函数调用时所需要保存的信息(返回地址、函数调用前各寄存器的值等)都存储于栈上,每次调用函数时栈会随着函数的调用而生长,随着函数调用结束而消亡;
- 自动变量只有3种存储方式: 分别是存储在数据段或者bss段(静态局部变量),存储在寄存器里(寄存器变量):存储在栈中(一般自动变量)。由于绝大多数自动变量存储在栈中,所以自动变量的作用域往往只在函数内,其生命期也往往只持续到函数调用的结束;
- 一个典型的错误: 将一个指向局部变量的指针作为函数的返回值返回!!!
堆:
- 特点: 堆用于存储用户申请的内存空间,系统通常在堆中进行动态内存分配;
- 程序内存布局: x86体系结构下这些段的一些典型安排方式如下图,根据不同的处理器体系结构,程序内存布局会有一定差异。比如说对于小端法的处理器,其栈由高地址向低地址缩减,堆由低地址向高地址增长。但是对于大端法的处理器,情况正好和小端法的机器相反。
常量的存储
动态内存管理
- 特点: C语言中只能通过malloc和其派生函数动态申请内存。malloc 作为一一个库函数,其Linux版本封装了sbrk0系统调用,而该系统调用负责向操作系统申请内存。malloc函数分配的内存分配在堆中,其内存空间在未释放之前均可以被引用,保证其生命期。
- 说明: 堆中的内存在释放之前都可以被引用。堆中的内存虽然在释放之前是一直可以引用的,但是指向该内存区域的指针却是有生命期区别的。比较常见的一个错误如下代码:该程序在函数内动态分配一块内存。该函数f1的参数使用的是传值的形式,因此该参数的值并不能被改变。因此使用二级指针在被调函数分配内存,主调函数使用!
动态内存分配深入研究
探究动态内存分配的实质,系统使用mem_control_block结构管理所有已分配的内存块,其结构代码如下:
/*
内存控制块的结构,管理所有的分配内存块
is_available:该块是否可用。1代表可用,0代表不可用
size:该块的大小
*/
struct mem_control_block{
int is_available;
int size;
};
- 因此,可以简单实现malloc函数。malloc 函数首先将用户需要分配的字节数加上一个内存控制块的大小,这时得出的一个字节数应当是实际需要分配的字节数。之后顺序遍历堆中的所有内存块,如果该块可用,并且该块大于实际需要的字节数,则将该内存块的首地址返回,并且将该块设置为可用。否则继续尝试下一个内存块。如果所有的内存块都不满足条件,则调用系统调用sbrk函数,通过操作系统分配一块内存。malloc函数将这块内存拓展在堆内,这时相当于堆增长了,如图所示。
- 函数用到两个重要的全局变量managed_memory_start 和last_valid_address, 分别表示第一个内存块的首地址和最后一个内存块最后一块内存的末地址。下面是malloc函数的原理性代码实现,该代码实现了malloc函数的基本流程。
//(1)设置内存块控制结构
/*内存控制块结构*/
struct mem_control_block{
int is_available;//本块内存是否可用
int size;//本块内存的大小
}
//(2)设置内存块地址标记
void *managed_memory_start;//第一个内存块的首地址,堆底
void *last_valid_address;//最后一个内存块最后一块内存的末地址,堆顶
//(3)malloc函数实现
/*
malloc 函数负责动态分配一块内存
返回值是分配好的内存块的首地址
参数是需要分配的内存块的大小
*/
void *malloc(size_t numbytes){
void *current_location;//当前所在的内存块
struct mem_control_block *current_location_mcb;//当前块的内存控制块结构
void *memory_location;
numbytes=numbytes+sizeof(struct mem_control_block);//得到所需内存块大小
memory_location=NULL;
current_location=managed_memory_start;//从队列头开始遍历
//遍历内存块队列,找到一个最合适的内存块
while(current_location != last_valid_address){
//取得当前内存块的控制结构
current_location_mcb=(struct mem_control_block *)current_location;
if(current_location_mcb->is_available){//当前块可用
if(current_location_mcb->size >= numbytes){//大小合适
current_location_mcb->is_available=0;//设置标志
memory_location=current_location;
break;
}
}
current_location=current_location+current_location_mcb->size;//取得下一个内存块
}
if(!memory_location){// 没有找到合适的内存块
if(-1 == sbrk(numbytes))//向操作系统申请新的内存
return NULL;//申请失败,说明系统中没有可用的内存了
//已有的内存队列添加新的内存
memory_location=last_valid_address;
last_valid_address=last_valid_address+numbytes;
current_location_mcb=memory_location;
current_location_mcb->is_available=0;
current_location_mcb->size=numbytes;
}
//运行至此memory_location已经得到了需要的内存块
//越过内存控制块结构,返回给用户可用内存块的首地址
memory_location=memory_location+sizeof(struct mem_control_lock);
return memory_location;
}
//(4)free函数实现
void free(void *firstbyte){
struct mem_control_block *mcb;
mcb=firstbyte-sizeof(struct mem_control_block);//取得该块内存控制块的首地址
mcb->is_available=1;//关键的一步,将该块设置为可用!!!
return;
}