随着诸如代码重构和单元测试等方法引入实践,调试技能渐渐弱化了,甚至有人主张废除调试器。这是有道理的,原因在于调试的代价往往太大了,特别是调试系统集成之后的BUG,一个BUG花了几天甚至数周时间并非罕见。
而这些难以定位的BUG基本上可以归为两类:内存错误和并发问题。而又以内存错误最为普遍,即使是久经沙场的老手,也有时也难免落入陷阱。前事不忘,后世之师,了解这些常见的错误,在编程时就加以注意,把出错的概率降到最低,可以节省不少时间。
这些列举一些常见的内存错误,供新手参考。
1. 内存泄露。
大家都知道,在堆上分配的内存,如果不再使用了,应该把它释放掉,以便后面其它地方可以重用。在C/C++中,内存管理器不会帮你自动回收不再使用的内存。如果你忘了释放不再使用的内存,这些内存就不能被重用,就造成了所谓的内存泄露。
把内存泄露列为首位,倒并不是因为它有多么严重的后果,而因为它是最为常见的一类错误。一两处内存泄露通常不至于让程序崩溃,也不会出现逻辑上的错误,加上进程退出时,系统会自动释放该进程所有相关的内存,所以内存泄露的后果相对来说还是比较温和的。当然了,量变会产生质变,一旦内存泄露过多以致于耗尽内存,后续内存分配将会失败,程序可能因此而崩溃。
现在的PC机内存够大了,加上进程有独立的内存空间,对于一些小程序来说,内存泄露已经不是太大的威胁。但对于大型软件,特别是长时间运行的软件,或者嵌入式系统来说,内存泄露仍然是致命的因素之一。
不管在什么情况下,采取比较谨慎的态度,杜绝内存泄露的出现,都是可取的。相反,认为内存有的是,对内存泄露放任自流都不是负责的。尽管一些工具可以帮助我们检查内存泄露问题,我认为还是应该在编程时就仔细一点,及早排除这类错误,工具只是用作验证的手段。
2.内存越界访问。
内存越界访问有两种:一种是读越界,即读了不属于自己的数据,如果所读的内存地址是无效的,程度立刻就崩溃了。如果所读内存地址是有效的,在读的时候不会出问题,但由于读到的数据是随机的,它会产生不可预料的后果。另外一种是写越界,又叫缓冲区溢出。所写入的数据对别人来说是随机的,它也会产生不可预料的后果。
内存越界访问造成的后果非常严重,是程序稳定性的致命威胁之一。更麻烦的是,它造成的后果是随机的,表现出来的症状和时机也是随机的,让BUG的现象和本质看似没有什么联系,这给BUG的定位带来极大的困难。
一些工具可以够帮助检查内存越界访问的问题,但也不能太依赖于工具。内存越界访问通常是动态出现的,即依赖于测试数据,在极端的情况下才会出现,除非精心设计测试数据,工具也无能为力。工具本身也有一些限制,甚至在一些大型项目中,工具变得完全不可用。比较保险的方法还是在编程是就小心,特别是对于外部传入的参数要仔细检查。
有些禁止访问的内存,读取也会产生异常。
比如这行代码,会出现Segmentation fault : int i = *(int*)100;
如果是允许访问的内存,那就不会有exception,但是结果未必是程序员预期的。
比如栈区;比如堆区无人申请的内存或者其他模块申请的内存。
int *p = (int *)malloc(sizeof(int));
*p = 123;
printf("%d\n", *p);
free(p); // free之后p已经算是“未申请的内存”
printf("%d\n", *p); // 这样肯定不会有异常,但读出来的值是多少就不一定了。
对于内核,发生Segmentationfault是因为访问了非法内存。
而这个非法内存可以理解为:访问的虚地址不存在,相应的页面表项是空的,也可以理解为虽然地址已经映射到物理内存,但是访问属性不允许,如对一个只读页面进行了写操作。这些都是在页面异常处理函数里面进行处理的,最后如果该页面确实是非法访问,函数通常是输出Segmentationfault,并发出信号终止进程。
其次就是在ELF可执行文件中分成了几个段,装载程序会把这几个段根据各自的段描述项把文件映射到内存指定的位置,通常说来代码段和只读数据是一段,加载到0X8048000(ELF起始地址,前面留128M做别的事情)的地方,并且页面属性为执行。后面是数据段,页面为读写不可执行,高地址初始化为进程的栈,页面属性为读写执行,数据段和栈之间的虚地址空间为堆,可供进程自由申请回收。
这样看来,如果读低地址空间(int i = *(int *)100;小于0X8048000),就会因为页表没有映射而段错误。如果写代码段的地址(long*p = (long*)0x8048ff0; *p = 0;),就会因为页面属性不允许写而出现段错误。
而对于使用malloc分配的堆区内存,在分配的时候就建立好了页面映射,并且至少是可读写的,所以一般不会出问题,一般malloc之间管理一个内存池,即使free了以后,也不是撤销映射,暂时还不归还给内核。因此再printf("%d\n",*p);也不会出问题。
看里面的.text文件就是读写不可执行
下面的例子可以看两种内存分配
第一个打印的是data区,第二个打印的是栈区
3.野指针。
野指针是指那些你已经释放掉的内存指针。当你调用free(p)时,你真正清楚这个动作背后的内容吗?你会说p指向的内存被释放了。没错,p本身有变化吗?答案是p本身没有变化。它指向的内存仍然是有效的,你继续读写p指向的内存,没有人能拦得住你。
释放掉的内存会被内存管理器重新分配,此时,野指针指向的内存已经被赋予新的意义。对野指针指向内存的访问,无论是有意还是无意的,都为此会付出巨大代价,因为它造成的后果,如同越界访问一样是不可预料的。
释放内存后立即把对应指针置为空值,这是避免野指针常用的方法。这个方法简单有效,只是要注意,当然指针是从函数外层传入的时,在函数内把指针置为空值,对外层的指针没有影响。比如,你在析构函数里把this指针置为空值,没有任何效果,这时应该在函数外层把指针置为空值。
4. 访问空指针。
空指针在C/C++中占有特殊的地址,通常用来判断一个指针的有效性。空指针一般定义为0。现代操作系统都会保留从0开始的一块内存,至于这块内存有多大,视不同的操作系统而定。一旦程序试图访问这块内存,系统就会触发一个异常。
操作系统为什么要保留一块内存,而不是仅仅保留一个字节的内存呢?原因是:一般内存管理都是按页进行管理的,无法单纯保留一个字节,至少要保留一个页面。保留一块内存也有额外的好处,可以检查诸如p=NULL; p[1]之类的内存错误。
在一些嵌入式系统(如arm7)中,从0开始的一块内存是用来安装中断向量的,没有MMU的保护,直接访问这块内存好像不会引发异常。不过这块内存是代码段的,不是程序中有效的变量地址,所以用空指针来判断指针的有效性仍然可行。
在访问指针指向的内存时,在确保指针不是空指针。访问空指针指向的内存,通常会导致程序崩溃,或者不可预料的错误。
5. 引用未初始化的变量。
未初始化变量的内容是随机的(像VC一类的编译器会把它们初始化为固定值,如0xcc),使用这些数据会造成不可预料的后果,调试这样的BUG也是非常困难的。
对于态度严谨的程度员来说,防止这类BUG非常容易。在声明变量时就对它进行初始化,是一个编程的好习惯。另外也要重视编译器的警告信息,发现有引用未初始化的变量,立即修改过来。
6. 不清楚指针运算。
对于一些新手来说,指针常常让他们犯糊涂。
比如int *p = …; p+1等于(size_t)p + 1吗
老手自然清楚,新手可能就搞不清了。事实上,p+n 等于 (size_t)p + n * sizeof(*p)
指针是C/C++中最有力的武器,功能非常强大,无论是变量指针还是函数指针,都应该掌握都非常熟练。只要有不确定的地方,马上写个小程序验证一下。对每一个细节都了然于胸,在编程时会省下不少时间。
7.结构的成员顺序变化引发的错误。
在初始化一个结构时,老手可能很少像新手那样老老实实的,一个成员一个成员的为结构初始化,而是采用快捷方式,如:
Structs { int l; char* p; }; intmain(intargc, char* argv[]) { structss1 = {4, "abcd"}; return 0; } |
以上这种方式是非常危险的,原因在于你对结构的内存布局作了假设。如果这个结构是第三方提供的,他很可能调整结构中成员的相对位置。而这样的调整往往不会在文档中说明,你自然很少去关注。如果调整的两个成员具有相同数据类型,编译时不会有任何警告,而程序的逻辑上可能相距十万八千里了。这就是说在不同的编译器不同的CPU上结构的内存对齐方法不一
正确的初始化方法应该是(当然,一个成员一个成员的初始化也行):
structs { int l; char* p; }; intmain(intargc, char* argv[]) { structss1 = {.l=4, .p = "abcd"}; structss2 = {l:4, p:"abcd"}; return 0; } |
内存布局,综述就是除了效率之外,计算机的三部分人独立发展没有协商好造成现在的局面,分成是搞硬件的,软件的和网络的。
8. 结构的大小变化引发的错误。
我们看看下面这个例子:
structbase { int n; }; structs { structbase b; int m; }; |
在OOP中,我们可以认为第二个结构继承了第一结构,这有什么问题吗?当然没有,这是C语言中实现继承的基本手法。
现在假设第一个结构是第三方提供的,第二个结构是你自己的。第三方提供的库是以DLL方式分发的,DLL最大好处在于可以独立替换。但随着软件的进化,问题可能就来了。
当第三方在第一个结构中增加了一个新的成员intk;,编译好后把DLL给你,你直接给了客户了。程序加载时不会有任何问题,在运行逻辑可能完全改变!原因是两个结构的内存布局重叠了。解决这类错误的唯一办法就是全部重新相关的代码。
解决这类错误的唯一办法就是重新编译全部代码。由此看来,DLL并不见得可以动态替换,如果你想了解更多相关内容,建议阅读《COM本质论》。
9.分配/释放不配对。
大家都知道malloc要和free配对使用,new要和delete/delete[]配对使用,重载了类new操作,应该同时重载类的delete/delete[]操作。这些都是书上反复强调过的,除非当时晕了头,一般不会犯这样的低级错误。
而有时候我们却被蒙在鼓里,两个代码看起来都是调用的free函数,实际上却调用了不同的实现。比如在Win32下,调试版与发布版,单线程与多线程是不同的运行时库,不同的运行时库使用的是不同的内存管理器。一不小心链接错了库,那你就麻烦了。程序可能动则崩溃,原因在于在一个内存管理器中分配的内存,在另外一个内存管理器中释放时出现了问题。
10. 返回指向临时变量的指针
大家都知道,栈里面的变量都是临时的。当前函数执行完成时,相关的临时变量和参数都被清除了。不能把指向这些临时变量的指针返回给调用者,这样的指针指向的数据是随机的,会给程序造成不可预料的后果。
下面是个错误的例子:
char* get_str(void) { charstr[] = {"abcd"}; returnstr; } int main(int argc, char* argv[]) { char* p = get_str(); printf("%s\n", p); return 0; } |
下面这个例子没有问题,大家知道为什么吗?
char* get_str(void) { char* str = {"abcd"}; returnstr; } intmain(intargc, char* argv[]) { char* p = get_str(); printf("%s\n", p); return 0; } |
11. 试图修改常量
在函数参数前加上const修饰符(只读),只是给编译器做类型检查用的,编译器禁止修改这样的变量。但这并不是强制的,你完全可以用强制类型转换绕过去,一般也不会出什么错。
而全局常量和字符串,用强制类型转换绕过去,运行时仍然会出错。原因在于它们是是放在.rodata里面的,而.rodata(readonly)内存页面是不能修改的。试图对它们修改,会引发内存错误。
下面这个程序在运行时会出错:
intmain(intargc, char* argv[]) { char* p = "abcd"; *p = '1'; return 0; } |
12. 误解传值与传引用
在C/C++中,参数默认传递方式是传值的,即在参数入栈时被拷贝一份。在函数里修改这些参数,不会影响外面的调用者。如:
#include <stdlib.h> #include <stdio.h> void get_str(char* p) { p = malloc(sizeof("abcd")); strcpy(p, "abcd"); return; } int main(int argc, char* argv[]) { char* p = NULL; get_str(p); printf("p=%p\n", p); return 0; } |
在main函数里,p的值仍然是空值。
13. 重名符号。
无论是函数名还是变量名,如果在不同的作用范围内重名,自然没有问题。但如果两个符号的作用域有交集,如全局变量和局部变量,全局变量与全局变量之间,重名的现象一定要坚决避免。gcc有一些隐式规则来决定处理同名变量的方式,编译时可能没有任何警告和错误,但结果通常并非你所期望的。
下面例子编译时就没有警告:
t.c #include <stdlib.h> #include <stdio.h> Int count = 0; Int get_count(void) { Return count; }
main.c #include <stdio.h> extern int get_count(void); int count; int main(int argc, char* argv[]) { count = 10; printf("get_count=%d\n", get_count()); return 0; } |
如果把main.c中的int count;修改为int count = 0;,gcc就会编辑出错,说multiple definition of `count'。它的隐式规则比较奇妙吧,所以还是不要依赖它为好
14. 栈溢出。
我们在前面关于堆栈的一节讲过,在PC上,普通线程的栈空间也有十几M,通常够用了,定义大一点的临时变量不会有什么问题。
而在一些嵌入式中,线程的栈空间可能只5K大小,甚至小到只有256个字节。在这样的平台中,栈溢出是最常用的错误之一。在编程时应该清楚自己平台的限制,避免栈溢出的可能。 (在2300上面遇到过栈溢出(8K大小)的问题,不行的话想其它办法比如说用堆,或改为传指针)
linux程序栈大小ulimit里面就有了。
ulimit -a里面stack size就是
通过limit -s来重新设置大小
[root@localhostbin]$ ulimit -a
core file size (blocks,-c) 0
data seg size (kbytes, -d) unlimited
schedulingpriority (-e) 0
filesize (blocks, -f) unlimited
pendingsignals (-i) 274432
max locked memory (kbytes, -l) 32
max memory size (kbytes, -m)unlimited
openfiles (-n) 65535
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-timepriority (-r) 0
stacksize (kbytes, -s) 10240
cputime (seconds, -t) unlimited
max userprocesses (-u) 274432
virtual memory (kbytes,-v) unlimited
filelocks (-x) unlimited
[root@localhostbin]$ ulimit -s 5120
[root@localhostbin]$ ulimit -a
core file size (blocks,-c) 0
data seg size (kbytes, -d) unlimited
schedulingpriority (-e) 0
filesize (blocks, -f) unlimited
pendingsignals (-i) 274432
max locked memory (kbytes, -l) 32
max memory size (kbytes, -m)unlimited
openfiles (-n) 65535
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-timepriority (-r) 0
stacksize (kbytes, -s) 5120
cputime (seconds, -t) unlimited
max userprocesses (-u) 274432
virtual memory (kbytes,-v) unlimited
filelocks (-x) unlimited
上面说的是用户空间的栈,内核栈
include/asm-i386/thread_info.h THREAD_SIZE |
#ifdef CONFIG_4KSTACKS |
内核配置的时候,有一个4K内核栈的选项,如果选上了,内核栈为4K;如果没有选上,内核栈是8K。
代码中一个函数,连续有两个长度为4K的buffer,系统默认的8K内核栈全部被这两个buffer占据了,进程PCB信息往哪里存储?不挂掉才怪。Linux内核从地址0x0到0x1000这个位置工4Kb的空间,是用来存放Linux系统的监视程序,4K是人为定的,里面存放系统启动的初始化、校验等。
解决方法:
1. 使用小一些的buffer
2. 使用kmalloc动态分配内存,不过记得在函数结束的时候释放内存
15. 误用sizeof。
尽管C/C++通常是按值传递参数,而数组则是例外,在传递数组参数时,数组退化为指针(即按引用传递),用sizeof是无法取得数组的大小的。
从下面这个例子可以看出:
void test(char str[20]) { printf("%s:size=%d\n", __func__, sizeof(str)); } int main(int argc, char* argv[]) { Char str[20] = {0}; test(str); printf("%s:size=%d\n", __func__, sizeof(str));
return 0; }
[root@localhost mm]# ./t.exe test:size=4 main:size=20 |
16.字节对齐。
字节对齐主要目的是提高内存访问的效率。但在有的平台(如arm7)上,就不光是效率问题了,如果不对齐,得到的数据是错误的。
所幸的是,大多数情况下,编译会保证全局变量和临时变量按正确的方式对齐。内存管理器会保证动态内存按正确的方式对齐。要注意的是,在不同类型的变量之间转换时要小心,如把char*强制转换为int*时,要格外小心。
另外,字节对齐也会造成结构大小的变化,在程序内部用sizeof来取得结构的大小,这就足够了。若数据要在不同的机器间传递时,在通信协议中要规定对齐的方式,避免对齐方式不一致引发的问题。
#pragma pack(n)和__attribute__((aligned(m)))的区别:
前者告诉编译器结构体或类内部的成员变量相对于第一个变量的地址的偏移量的对齐方式,缺省情况下,编译器按照自然边界对齐,当变量所需的自然对齐边界比n大时,按照n对齐,否则按照自然边界对齐;后者告诉编译器一个结构体或者类或者联合或者一个类型的变量(对象)分配地址空间时的地址对齐方式。也就是所,如果将__attribute__((aligned(m)))作用于一个类型,那么该类型的变量在分配地址空间时,其存放的地址一定按照m字节对齐(m必须是2的幂次方)。并且其占用的空间,即大小,也是m的整数倍,以保证在申请连续存储空间的时候,每一个元素的地址也是按照m字节对齐。__attribute__((aligned(m)))也可以作用于一个单独的变量。举例说明:
#include<stdio.h>
#pragma pack(4)
typedef struct{
uint32_t f1;
uint8_t f2;
uint8_t f3;
uint32_t f4;
uint64_t f5;
}__attribute__((aligned(1024))) ts;
int main()
{
printf("Struct size is: %d, aligned on1024\n",sizeof(ts));
printf("Allocate f1 on address:0x%x\n",&(((ts*)0)->f1));
printf("Allocate f2 on address:0x%x\n",&(((ts*)0)->f2));
printf("Allocate f3 on address:0x%x\n",&(((ts*)0)->f3));
printf("Allocate f4 on address:0x%x\n",&(((ts*)0)->f4));
printf("Allocate f5 on address:0x%x\n",&(((ts*)0)->f5));
return 0;
}
输出:
Struct size is: 1024, aligned on 1024
Allocate f1 on address: 0x0
Allocate f2 on address: 0x4
Allocate f3 on address: 0x5
Allocate f4 on address: 0x8
Allocate f5 on address: 0xc
注意
绿色部分表明了__attribute__((aligned(1024)))的作用
红色部分说明#pragma pack(4)只对大小大于4的成员变量的地址偏移起作用
紫色部分说明对于大小大于4的成员变量,其地址偏移按照4字节对齐
17.字节顺序。
字节顺序历来是设计跨平台软件时头疼的问题。字节顺序是关于数据在物理内存中的布局的问题,最常见的字节顺序有两种:大端模式与小端模式。
大端模式是高位字节数据存放在低地址处,低位字节数据存放在高地址处。
小端模式指低位字节数据存放在内存低地址处,高位字节数据存放在内存高地址处;
比如long n = 0x11223344。
模式 第1个字节 第2个字节 第3个字节 第4个字节
大端模式 0x11 0x22 0x330x44
小端模式 0x44 0x33 0x220x11
在普通软件中,字节顺序问题并不引人注目。而在开发与网络通信和数据交换有关的软件时,字节顺序问题就要特殊注意了。
18. 多线程共享变量没有用valotile修饰。
在关于全局内存的一节中,我们讲了valotile的作用,它告诉编译器,不要把变量优化到寄存器中。在开发多线程并发的软件时,如果这些线程共享一些全局变量,这些全局变量最好用valotile修饰。这样可以避免因为编译器优化而引起的错误,这样的错误非常难查。(这种类型的变量每次都刷到内存,而不在cache中缓存)
可能还有其它一些内存相关错误,一时想不全面,这里算是抛砖引玉吧,希望各位高手补充。
一、
//演示数组参数的传递问题
#include "stdafx.h" using namespace std; void test(char str[20]) { cout<<sizeof(str)<<endl;//4,这里传递进来的参数是数组指针 }
int main(int argc, char* argv[]) { char str[20] = {0}; test(str); cout<<sizeof(str)<<endl;//20,这是计算实际数组的大小 system("pause"); return 0; } |
二、
// StringTest.cpp :定义控制台应用程序的入口点。
/*通过这个代码示例,演示:
用字符串初始化字符数组时,实际上字符数组指针,指向了栈内存中,省略了字符串结束符'\0'字符串常量的一个拷贝
而用字符串初始化字符串指针时,字符串指针指向了实际的常量,是不能修改的,否则错误。
*/
#include "stdafx.h" using namespace std; char* get_str1(void) { char str[] = {"abcd"}; //定义字符数组,str指向栈上的一个字符数组, 这个字符数组是由字符串来初始化的 return str; } char* get_str2(void) { char* str = {"abcd"};//定义字符串指针, str指向了实际的常量区域,退出函数以后, 常量内存区域也是有效的 return str; } int main(int argc, char* argv[]) { char* p1 = get_str1(); printf("%s\n", p1);//输出乱码 char* p2 = get_str2(); printf("%s\n", p2);//输出abcd
char* p3 = "abcd"; *p3 = '1';//该语句试图修改常量,所以错误
char p4[]= "abcd"; *p4 = '1'; cout<<p4<<endl;//输出1bcd system("pause"); return 0; } |
linux进程与它的堆栈空间_代码段(指令,只读)、数据段(静态变量,全局变量)、堆栈段(局部变量)、栈的大小ulimit -s
2012-02-0512:39:02| 分类:Linux系统编程 | 标签: |字号大中小 订阅
一)概述
.堆栈是一个用户空间的内存区域,进程使用堆栈作为临时存储。
.堆栈中存放的是函数中的局部变量,在函数的生命周期中可以将变量压入堆栈,编译器需要确保堆栈指针在函数退出前恢复到初始位置,也就是说,内存是自动分配和释放的,C/C++把存储在堆栈中的局部变量当作automatic存储,并使用auto关键字,这是局部变量的默认存储方式,所以现在没有人用auto关键词.
.与动态存储(auto)相对映的静态存储(static),也就是用static定义的局部变量,它不用堆栈来存储,而是使用数据段来存储(也就是说它的生命周期在整个程序运行期间)。
.堆栈的基地址位于用户空间的最高虚拟地址附近,并从那里向下延伸。
.一个进程开始时,堆栈的最大值就不能改变,如果占用的空间超过了堆栈大小,那么就会导致堆栈溢出。
二)进程的内存组织形式
进程被分为三个区域:文本、数据和堆栈。
1)文本区域:
文本区域也叫做代码段,是由程序确定的,
它包括代码(指令)和只读数据,该区域通常被标记为只读,任何对其写入的操作会导致段错误.
2)数据区域(静态内存分配(static)):
数据区域也叫做数据段,
它包括已初始化和未初始化的数据,静态变量存储在这个区域中,它的大小可以用系统调用brk(2)来改变。
详细了解数据区域:
分成初始化为非零的数据区、BSS和堆(Heap)三个区域。
初始化非零数据区域一般存放静态非零数据和全局的非零数据,属于静态内存分配(全局变量,static修饰的变量);
BSS(Block Started by Symbol)区域(都初始化为0了)一般存放未初始化的全局数据(默认值为0)和未初始化的静态数据(默认值为0),属于静态内存分配(全局变量、static修饰的变量);
堆区域一般存放运行时动态分配的内存空间,其大小不固定,可动态扩张或缩减。当调用malloc等函数分配内存时,新分配的内存被动态添加到堆上;当调用free等函数释放内存时,被释放内存从堆中被剔除。
代码段和数据段之间有明确的分隔,
但是数据段和堆栈段之间没有,而且栈是向下增长,堆是向上增长的,因此理论上堆和栈会“增长到一起”,操作系统的内存管理功能需要防止这样的错误发生。
3)堆栈区域(动态内存分配auto,默认,所以不用关键字auto):
堆栈区域也叫堆栈段,
它用于给局部变量动态分配空间,同样函数传递参数和函数返回值也要用到堆栈.
堆栈也可向下增长(向内存低地址)也可以向上增长,这依赖于具体的实现,
通常都是向下增长的,而SP(堆栈指针)也是指向堆栈的最后地址.
4)内存的分配区域:
根据前面所述,堆栈是位于最高虚拟地址附近,而数据段则位于堆栈段之后,最后是代码段。
也就是:
低地址 代码段 或 高地址 堆栈段
数据段 数据段
高地址 堆栈段 低地址 代码段
三)堆栈着色
当两个线程或进程使用相同的堆栈虚拟地址时,它们会争夺同一个cache行,导致竞争和降级行为.
堆栈着色的技术使每一个进程的基址都不相同,通过随机分配堆栈基址,多个进程会使用不同的cache行来避免.
四)堆栈的限制
堆栈空间的最大值是由setrlimit系统调用确定的,也可以通过bash内建的ulimit命令来设定和查看.
例如:
查看当前可使用的最大堆栈(以KB为单位)
ulimit -s
8192 //栈的大小默认是8M
设定为最大的使用堆栈为15KB
ulimit -s 15
此时执行ls将会得到一个段错误.
ls -l /etc/
total 1040
Segmentation fault
通过用strace跟踪ls命令,将发现有如下的系统调用
getrlimit(RLIMIT_STACK, {rlim_cur=15*1024,rlim_max=15*1024}) = 0
说明当前可用的堆栈空间,已经不足以运行strace命令了.
五)常驻内存和锁定内存
常驻内存专指存储在RAM中的内存部分,不包括存储在交换区和未存储的进程的内存.
锁定内存是常驻内存的子集,它指被进程明确地锁定到RAM的虚拟内存中,不能用于交换,并一直常驻于RAM中.
可以使用 realloc函数: 示例代码 int *a=new int[MAX]; a=(int *) realloc (a,2*MAX);//把空间扩大两倍,并且原来的内容复制