栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

跟我一起学Linux系统编程007-C语言变量分配、堆内存分配,函数可重入

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

跟我一起学Linux系统编程007-C语言变量分配、堆内存分配,函数可重入

1 C语言内存分配方式

在Linux系统上,程序被载入内存,时成为进程之时,内核为用户进程地址空间建立了代码段、数据段和堆栈段,各种C语言代码中需要占用空间的变量、常量也获得了内存空间。从进程内存布局上来讲C语言全局变量、局部变量、动态内存和常量对应的区域,分别对应以下四个区域。

(1) 静态存储区域分配。程序编译的时候就已经分配好,进程启动后获得实际的内存空间。这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

(2) 在栈上分配。进程运行,函数压栈后,函数内部声明定义的局部变量,获得栈内存空间。这些栈空间,随着函数结束时弹栈释放。栈操作内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)从堆上分配,也称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但雷区很多,使用不当,后果严重。

(4)其实还有一个常量,const定义的,或#define定义的,因为是放在代码段,运行时用来初始化一些全局变量的值。这部分存在代码段,运行时不占内存空间,所以没有实际的内存分配。

C语言的动态内存分配基本函数是malloc(),在Linux上的基本实现是通过内核的brk和mmap这两个系统调用。brk()是一个非常简单的系统调用,只是简单地改变mm_struct结构的成员变量brk的值。 mmap系统调用实现了更有用的动态内存分配功能,可以将一个磁盘文件的全部或部分内容映射到用户空间中,进程读写文件的操作变成了读写内存的操作。

2 C语言堆内存分配函数

C语言跟内存申请相关的函数主要有 alloc、calloc、malloc、free、realloc、sbrk等。brk和mmap是系统调用函数,与C语言无关。

  • 其中alloc是向栈申请内存,因此无需释放。说实话,alloc是个怪胎,工程中尽量不要使用。
  • malloc分配堆区内存之后,需显示的初始化,比如调用函数memset来干这个活。calloc与malloc仅不同的一个地方是:会将得到的堆空间初始化为0。
  • 而realloc则对malloc申请的内存进行大小的调整。当然释放还是要靠free来做。
  • sbrk内部调用brk,用来增加brk调用的program break大小,就是增大申请到的堆数据段小。

类堆缓冲机制

malloc/calloc/free基本上都是C函数库实现的,跟OS无关。C函数库内部通过一定的结构来保存当前有多少可用内存。申请内存时,并非你想象的,用多少得到多少。哪怕你只malloc一个字节,实测64位Ubuntu20上,malloc也会调用brk把堆指针 program break在堆地址空间起点上移128K字节。如果程序 malloc的大小累计超出了128K的空间,那么malloc才会调用brk系统调用来增加和分配可用空间。free后释放的内存并不立即返回给os,而是记录malloc的内部数据结构中,以便再次分配。

有人这样比方:malloc用brk做批发,一次性向OS申请大的内存(一次至少128K字节),而malloc等函数返回值,则类似于零售,进程一点一点的要堆内存,malloc就一点一点的给出,不超过128K字节,就不再调用brk。这套机制虽不是缓冲,却类似于缓冲,所以称之为类缓冲。

当然,如果伸手向堆要的空间太大,malloc内部会调用mmap来进行。(这个值要多大,才会调用mmap,我目前尚未有时间挖掘malloc源码,请有经验的高人多多指点。)

使用这套类缓冲机制的原因主要有三点:

减轻内核资源负载: 系统调用申请内存代价昂贵,涉及到用户态和核心态的转换。

碎片重用机制:其实,大部分的情况上,应用层堆申请和释放的是小片内存,而且反复申请再释放。如果每次都完全按实malloc分配、物理free删除的话,进程会非常低效。

3 函数可重入性

可重入是系统编程中一个非常重要的事项。

假定你的函数用了一个全局变量,在A线程下工作得很好。可是一旦B线程调用这个函数的时候,结果有可能乱了套。A线程与B线程有可能同时对该全局变量进行读写,读写动作交织在一起,最后的结果无法预料。

函数所谓的可重入性,是在多线程的语境下的概念:一个函数如果同时被多条线程调用,他返回的结果都是严格一致的,那么该函数被称为“可重入”函数(reentrance funciton),否则被称为“不可重入”函数。

可重入函数是多线程并发、驱动硬件设备、协调资源等编程实际中必须要考虑的问题。否则代码就会有隐患,更糟糕的是这些隐患往往只能在特定场景下才能复现。

函数不可重入的主要原因有三:

  1. 因为函数内部使用了共享资源,比如全局变量、环境变量,线程很容易出错。
  2. 因为函数内部调用了其他不可重入函数。部分库调用因为是不可重入的,调用了也是不可重入,好比malloc()、free()。也有些内核调用也是不可重入的,比如getpwuid()。
  3. 因为函数执行结果与某硬件设备相关,比如函数体内调用了标准I/O函数,或者函数内部进行了浮点运算。许多的处理器/编译器中,浮点通常都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现)。

工程应用上,我们在PC上的Linux、windows等大系统中开发上层应用,大多数情况下,还真不太需要考虑可重入情况。但在实时小系统的设计中,可重入性、线程安全,是几乎每个函数都要注意的事情。

把一个不可重入函数变成可重入的惟一方法是用可重入规则来重写他。其实很简单,只要遵照了几条很容易理解的规则,那么写出来的函数就是可重入的。

(1)函数内只使用局部变量,不要使用全局变量、静态变量。因为多个线程极可能覆盖这些变量值。

(2)在和硬件发生交互的时候,注意临界区保护,就是进入函数要关中断,离开函数开中断。

(3)不能调用任何不可重入的函数。

(4)谨慎使用堆栈。

4 可重入性与线程安全的关系

可重入与 线程安全两个概念都关系到函数处理资源的方式。可是,他们还是有重大区别的:可重入概念会影响函数的外部接口,而线程安全只关心函数的功能实现。

大多数状况下,修改函数接口,使得全部的数据都经过函数的调用者提供,不可重入函数就改变成了可重入的函数。

只须修改函数的实现部分,通常情况下是加入同步机制以保护共享的资源,使之不会被几个线程同时访问,要将非线程安全的函数改成线程安全的了。比如一个读写Nand Flash的函数,函数开头加锁,函数返回前解锁。

从操做系统背景与CPU调度策略方面来讲

可重入是在单线程操做系统背景下,重入的函数或者子程序,按照后进先出的线性序依次执行完毕。

多线程执行的函数或子程序,各个线程的执行时机是由操做系统调度,不可预期的,可是该函数的每一个执行线程都会不时的得到CPU的时间片,不断向前推动执行进度。

可重入函数未必是线程安全的;线程安全函数未必是可重入的。

例如,一个函数打开某个文件并读入数据(open/write/read操作)。这个函数是可重入的,由于它的多个实例同时执行不会形成冲突;但它不是线程安全的,由于在它读入文件时可能有别的线程正在修改该文件,为了线程安全必须对文件加“同步锁”。

另外一个例子,函数在它的函数体内部访问共享资源使用了加锁、解锁操做,因此它是线程安全的,可是却不可重入。由于若该函数一个实例运行到已经执行加锁但未执行解锁时被停下来,系统又启动该函数的另一个实例,则新的实例在加锁处将转入等待。若是该函数是一个中断处理服务,在中断处理时又发生新的中断将致使资源死锁。fprintf函数就是线程安全但不可重入。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/335587.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号