前言
在C程序开发中我们会用到很多标准库的函数,如内存分配的那几个函数(malloc/calloc/realloc/free),标准输入输出的函数(printf/getchar/sprintf…)等。这些标准函数给我们提供了跨平台的能力,我们知道任何平台上都会有这些库,这些函数,以及它们的标准行为(实际上不同平台的实现可能会有一些差异,但不在这篇博文的讨论范围内),因此各种通用的模块,大型同性交友网站上那些库,一般都会肆无忌惮地直接使用它们。
但是使用的很顺手的同时,一个合格的程序员也应该注意到它们的风险。因为它们不见得是可重入的。
可重入
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。—— 百度百科
这些标准库函数可不可重入取决于其实现。在Windows/Linux环境下我们用它们好像从来也没有出现过什么问题,这主要是因为它们的实现中帮助我们做了线程安全方面的事(这句未经严格求证),因此即使在多线程环境下也不会出问题。
但是对于我们这些苦逼的嵌入式程序员来说,很有可能你用的这些标准库函数实际上就是不可重入的。这会出现什么问题?也许你运行个十天半个月的,啥事情都没发生,然后突然某一天系统宕机了,结果你还复现不出来;也有可能正好你的业务逻辑完美避开了线程安全的雷区,于是这辈子相安无事(太理想化了)。
嵌入式的标准库会不可重入主要是因为本身这些实现就不是为多任务环境写的,它并意识不到自己是在一个OS上,自然也不会考虑什么线程安全不安全的问题。
在CodeWarrior IDE上,直接去看malloc的实现(在IDE的文件夹中找到alloc.c)会发现其用了全局的指针来实现内存管理,而printf的实现也用了一个全局的函数指针变量,所以如果你用了OS的话直接使用这些函数时就可能会出问题。
互斥锁保护临界区
每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
多个进程中涉及到同一个临界资源的临界区称为相关临界区。.——百度百科
那这种情况怎么办呢?
一个很常用的技术就是通过在访问临界区的时候上锁,在这里,由于我们不会直接去改标准函数的实现,所以整个标准函数库就被我们当做临界区,同时只能有一个线程访问其中的函数。
这里还得意识到,由于malloc、calloc、realloc和free函数都用到同样的临界资源,因此它们是相关的,所以同时只能有四个函数中的一个被访问,因此它们要上的是同一个锁。
那这样就可以写出线程安全的堆实现(就不贴出所有的相关模块了,没有的实现的函数就当伪代码看吧,应该可读性还是不错的,应该可以直接看懂)。
/*
*******************************************************************************************
*
* Heap Of The Abstract OS Layer
* 抽象操作系统层的堆
*
* File : MyOSHeap.h
* By : Lin Shijun(http://blog.csdn.net/lin_strong)
* Date: 2019/09/08
* version: V1.0
* NOTE(s):a. This file is to adapt default heap interfaces(malloc/calloc/realloc/free) to
* thread safe interfaces, which use MyOSMutex as lock.
* b. force include this file to redirect all the memory interfaces
* c. the malloc shouldn't be a macro in stdlib.h
*********************************************************************************************
*/
#ifndef _MYOSHEAP_H
#define _MYOSHEAP_H
#include <stdlib.h>
#undef malloc
#define malloc MyOSHeap_malloc
#undef calloc
#define calloc MyOSHeap_calloc
#undef realloc
#define realloc MyOSHeap_realloc
#undef free
#define free MyOSHeap_free
// see the corresponding interface in stdlib.h but thread-safe
void * MyOSHeap_malloc(size_t _Size);
void * MyOSHeap_realloc(void * _Memory, size_t _NewSize);
void * MyOSHeap_calloc(size_t _Count, size_t _Size);
void MyOSHeap_free(void * _Memory);
// for free the memory used by this module, not thread-safe.
void MyOSHeap_Destroy(void);
#endif
/*
*******************************************************************************************
*
* Heap Of The Abstract OS Layer
* 抽象操作系统层的堆
*
* File : MyOSHeap.c
* By : Lin Shijun(http://blog.csdn.net/lin_strong)
* Date: 2019/09/08
* version: V1.0
* NOTE(s):
*********************************************************************************************
*/
/*
******************************************************************************************
* INCLUDES
******************************************************************************************
*/
#include "MyOSHeap.h"
#include "MyOSMutex.h"
#include "MyOS.h"
// resume malloc define
#undef malloc
#undef calloc
#undef realloc
#undef free
/*
******************************************************************************************
* LOCAL FUNCTION
******************************************************************************************
*/
static BOOL _lock(void);
static void _unlock(void);
/*
******************************************************************************************
* INTERFACE IMPLEMENTATIONS
******************************************************************************************
*/
void * MyOSHeap_malloc(size_t _Size){
void * ret = NULL;
BOOL locked;
locked = _lock();
ret = malloc(_Size);
if(locked)
_unlock();
return ret;
}
void * MyOSHeap_realloc(void * _Memory, size_t _NewSize){
void * ret = NULL;
BOOL locked;
locked = _lock();
ret = realloc(_Memory, _NewSize);
if(locked)
_unlock();
return ret;
}
void * MyOSHeap_calloc(size_t _Count, size_t _Size){
void * ret = NULL;
BOOL locked;
locked = _lock();
ret = calloc(_Count, _Size);
if(locked)
_unlock();
return ret;
}
void MyOSHeap_free(void * _Memory){
BOOL locked;
locked = _lock();
free(_Memory);
if(locked)
_unlock();
}
static MyOSMutex _mutex = NULL;
void MyOSHeap_Destroy(void){
if(_mutex != NULL)
MyOSMutex_Destroy(_mutex);
_mutex = NULL;
}
/*
******************************************************************************************
* LOCAL FUNCTION IMPLEMENTATION
******************************************************************************************
*/
// Lazy Man Model
static MyOSMutex _lock_getInstance(void){
MyOS_SR sr;
if(_mutex == NULL){
sr = MyOS_disableInterrupts();
if(_mutex == NULL){
_mutex = MyOSMutex_Create();
}
MyOS_enableInterrupts(sr);
}
return _mutex;
}
static BOOL _lock(void){
return MYOSMUTEXERR_NO == MyOSMutex_Pend(_lock_getInstance(), 0);
}
static void _unlock(void){
MyOSMutex_Post(_mutex);
}
(那些宏定义什么的在下一节解释)
这里这个实现是基于互斥锁的方式保护不可重入的函数实现的线程安全。printf等同理。
当然,有些大牛可能会说malloc的实现有内存碎片问题什么的,我要用更好的内存分配器。
嗯,赞同,确实是这样的,但我们这里讨论的是怎么把不可重入函数改成线程安全的,那是另一回事。我下一步也打算研究研究tcmalloc,或者基于uCOS的内存管理自己搞个内存分配器。
预处理器/宏 替代技术
跳过之前的讨论,现在我们准备了一个线程安全版本的标准库函数,要替换掉原来对标准库函数的调用,那我们该怎么办呢?
勤快的人可能已经开始打开一个个文件把所有的malloc调用都改为MyOSHeap_malloc了。但这样一来很麻烦,二来一要改调用就直接改源码其实很不利于模块的通用性。这么长的名字看着肯定也没有原来一个简单的malloc舒服。
所以最好的方案应该就是不动原代码,源码中该用malloc还用malloc,但是你得让它实际调用到线程安全的那个版本。
这里介绍的这个技术简单来说就是利用了宏的文本替换功能,预处理器会在预处理阶段把所有宏定义过的文本进行替换,这样,如果在一段代码前它看到了
#define malloc abcdefg
那后面比如出现了这样的代码
p = (char *)malloc(1024);
那实际上在预处理后,也就是在进入编译器之前,它就会变成。
p = (char *)abcdefg(1024);
因此只要在所有代码前都加上那个#define,那所有的malloc就都被对应替换掉了。
这也是.h文件中这段
#include <stdlib.h>
#undef malloc
#define malloc MyOSHeap_malloc
#undef calloc
#define calloc MyOSHeap_calloc
#undef realloc
#define realloc MyOSHeap_realloc
#undef free
#define free MyOSHeap_free
所做的事情,实现了一个重定向。
这样,只要在所有代码前都#include这个头文件,就完成了所有替代。
当然,怎么可能傻到打开所有文件在每一个文件前面都加一行#include。。
编译器选项中都会有一个强制include文件的选项,我们只要在编译器选项中选择强制include这个头文件,就相当于在每个文件前加了这一行。
具体来说,CodeWarrior中在Edit-Standard Settings-compiler for HC12-Options-Input-Additional include file里。
而VS2012中,打开工程属性页 - 配置属性 - C/C++ - 高级 - 强制包含文件
然后就在不动源码的情况下很愉快的把所有的标准库函数都替代掉了O(∩_∩)O~
其他技术
除了预处理器替代,其实还有些其他技术。
比如链接时替代,基本就是如果原先的函数是打包到静态库lib文件,然后把库导入到工程中的话,你可以在工程中直接创建一模一样名字的函数,这样,在静态链接时,链接器会优先链接工程中的那个函数,这样就把原来的那个函数替代掉了。
链接时替代好像也可以通过编译器选项来实现,但是我没实践过,就不说了。
另外还有些替代技术,展开来说又得好大的篇幅,就跳过吧。
结语
结语写点啥呢。
其实这些个替代技术是从《Test-Driven Development for Embedded C》上学来的,推荐大家去看看这本书,可以学到很多让代码写的更优雅的技术。
熟练运用好这些替代技术还可以玩些做很多更有意思的事情。比如,可以对内存分配进行监控,检查内存泄露点。CppUTest和Unity的内存泄露检测就是这么搞的。
现在我正在写嵌入式TDD实战系列,手把手教嵌入式TDD的整个过程,敬请期待。
2019/09/19 今天稍微看了下源码,发现CW的printf好像可以通过宏定义LIBDEF_REENTRANT_PRINTF改成可重入版本。但我也没试验过,在这里记录下。