重新思考内存申请

前言

最近在一个项目中用C语言做一个restful服务的桥接器,具体功能就是把Java端发过来的请求报文解析,再用解析出的内容调用C底层相应的接口,最后把C接口返回的信息再发给Java端。
测试时发现,多数接口的请求能正常接收到返回信息,但某些请求报文特别长的接口,就无法正常获取返回报文。查了代码发现是接收报文变量的长度定义过短导致程序提前退出了。我便把接收报文的变量变为了动态申请内存,申请了1MB的内存,结尾处再释放掉,这样改过之后,程序没有提前退出,却出现了段错误。查了半天,最后发现问题出在一个调试语句上,那里还是用的栈内存申请,在把最大报文长度限制变成1MB之后,加上调用底层C接口中申请的栈地址空间,超过了linux栈空间的限制,导致内存溢出了。

借此机会详细的回顾一下linux下的内存申请相关的内容。

栈内存申请

这是系统分配内存的方式,也是最基本的一种内存申请方式,如要申请十个字节的空间,并全部赋值为尾零:

1
char str[10] = {0};

这样就在栈上申请到了10个字节的空间,这一部分空间是无需程序员手动释放的,在程序执行结束或一个代码块结束后会由操作系统自动收回。其操作方式类似于数据结构的,只要栈剩余的空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出。
在linux系统下,查看系统栈的空间容量可以用ulimit -alimit直接查看:

ulimit -a limit

可以看到,在mac系统下,stacksize的大小是8MB,即8 * 1024 * 1024个字节
所以,这样最多申请的占内存空间为:char str[8192 * 1024],超过这个数字就会报段错误,也就是由栈内存溢出导致的。
测试一下,

1
2
3
4
5
6
#define MAXLEN 8192 * 1023 

int main() {
char str[MAXLEN];
return 0;
}

当把MAXLEN赋值为8192 * 1023时,程序还能正常运行,但改为8192 * 1024时就段错误了,可能包含几KB的栈信息数据。由此证明了栈空间确实是8MB(软限制),可以通过ulimit -s命令临时改变栈空间的大小,突破软限制。

堆内存申请

一般由程序员分配和释放,程序员若不释放,程序结束时可能由OS回收。但它与数据结构的堆是两码事,分配方式类似于数据结构的链表
申请一个10字节的空间书写如下:

1
char *str = (char *)malloc(10);


1
char *str = (char *)calloc(1, 10);

在堆上的内存申请,默认是没有软限制的,只依赖系统硬限制,故在申请大空间时需要用动态申请的方法,不过需要注意的是在用完该内存后要及时释放,否则会导致内存泄露而使内存耗尽。下面就来说一下内存释放的方法。

内存释放

堆上内存需要手动释放,方法如下:

1
2
3
if (str)
free(str);
str = NULL;

一般习惯用宏来实现:

1
#define dataFree(str) if(str){free(str);str=NULL;}

最后把str指针指向NULL的原因是,free操作仅释放了str指向的内存空间,而不包括str指针本身,str仍旧指向那片内存地址,只不过是未申请的了,如果后面的程序又有操作到了str指针,将会产生不可预知的错误,故要将其指向NULL

总结

常常说,细节决定成败。这次犯的错误不是什么大错误,不过却造成了整个桥接器无法使用。在考虑问题的时候一定要周全,不但要保证自己写的模块没有问题,还要考虑到模块与模块之间交互的过程中可能会出现哪些问题。就像这次的内存溢出,自己这里申请1MB是没问题的,但加上其他模块的就超出了系统最大限制了,当然别人的模块也要改,但自己这里一定要足够健壮。