内存管理:静态分配和动态分配的完美结合
0赞
先看看我们面临的需求:
产品A:有一个RS485口,可以挂0~256个节点,但不能带电增加和减少节点。软件上,上电初始化时,要为每个节点分配一个缓冲区。
产品B:一个通信装置,相同的CPU平台上,因为所配的接口数量不一样,衍生出多个型号。软件上,也要要为每个接口分配一个缓冲区。
需求C:在OS下编程,有许多常驻内存的线程,这些线程在整个运行周期内是不会退出运行的,同时运行着许多用户线程,这些线程是动态创建和销毁的。
我们知道,内存分配分为动态分配和静态分配两种,他们各有特点。
静态分配是在rw段分配,由编译器执行,快速,高效,无开销,无碎片,无泄漏,如果内存不足,编译时就能知道。缺点是不灵活,不能释放,不能满足动态需求。
动态分配则是在运行时,从堆中分配,执行速度慢,内存利用率低,可能产生碎片和内存泄漏,优点嘛,恰好就是静态分配的缺点。
可以说,动态分配和静态分配的优缺点是一一对应的。我们从内存分配的角度,分析一下实现上述三种需求的不同方法。
产品A:
方案1,静态分配,按最大可能,分配256块缓冲区,优点自不必说,缺点是内存开销太大。
方案2,动态分配,在上电初始化时,检测连接的节点数,按实际连接的节点数动态分配缓冲区。缺点是要付出动态分配的开销,如果节点数接近256的话,需要的内存可能比静态分配还要多。
理想方案:按照实际连接的节点数,“静态”地分配缓冲区。不行吗?djyos可以这样,且看后面分析。
产品B:
其实和A很类似,但从项目经理以及产品档案管理和维护的角度,A和B是不同的。
方案1和方案2和A一样。
方案3:没个型号按照其实际接口数量,静态分配内存。缺点是每个型号的软件不一样,源代码至少有一个宏不一样,用来控制接口数量,二进制的代码页不一样。每一次的版本升级、bug修正,都需要每个型号单独重新编译。
理想方案:同A,但所有型号保持从源代码和二进制代码一致。
需求C:我们知道,每个线程,都需要分配运行栈,如果使用静态分配方案,就不能动态创建和删除线程,这在OS中是不允许的,如果用动态分配方案,那么常驻内存的线程,用动态分配就太浪费了。因此有一些OS,会提供两套创建线程的函数,分别对应静态线程和动态线程。即使如此,亦有问题,比如一个模块,在产品x中其线程是静态的,在产品y中是动态的,则不能直接移植。两者能兼容吗?djyos是可以的。
在djyos中,提出了准静态分配的概念,在必要的初始化完成前,先执行静态堆初始化,此后调用malloc函数,结果执行的是从堆中静态分配,好像堆的底部成了RW段一样,这时候分配器的内存,可以释放,但要求严格按照“malloc-free”成对出现的方式,否则虽然不会出错,但free变成空操作。初始化完成后,将执行动态堆初始化,此后调用malloc动态分配,就和和其他OS的动态分配一样,此时malloc和free可以按照任意顺序调用。
过程是:
上电启动;
调用静态堆初始化;
提供appinit接口,应用程序可以在这里调用需要静态初始化的模块,比如扫描RS485节点数。此时,调用malloc分配内存,实际执行的是静态分配。
调用动态堆初始化;----此后调用malloc函数,将执行真正的动态分配。
启动多事件调度;
调用main函数;----在main中调用的初始化,或者创建线程,如果调用malloc,执行的当然就是动态分配了。
诸位看官,看出玄机来了吧,对于产品A和B,只要在动态堆初始化执行完之前,调用节点扫描程序,就会按照实际配置的节点数,“静态”地分配缓冲区。对于需求C也一样,进一步地,如果一个模块,在产品x中其线程是静态的,在产品y中是动态的,代码是完全一致的,二进制级别一致。完全取决于你是在appinit接口中调用,还是main函数中调用。
也可以这样理解:
初始化时 可以有动态扩展静态分配内存区的机会
应该是一旦进行了动态内存分配,就不能再“准静态分配了”
