2. 共享内存与全局变量:基本原理、使用场景、注意事项与竞态问题
好,咱们进入第二个话题。共享内存和全局变量,这俩东西在嵌入式开发里太常见了。说白了,它们就是让不同任务能「看到」同一块数据的最直接方式。
我刚开始做VxWorks那会儿,觉得这玩意儿简单啊,不就是定义个全局变量嘛。后来吃了不少苦头,才明白这里面的水有多深。今天咱们就把这潭水彻底趟一遍。
2.1 基本原理:它们到底是什么?
先说说全局变量。在VxWorks里,整个系统跑在一个平坦的地址空间里。你想想看,所有任务共享同一个内存视图。所以你在C文件里定义一个全局变量:
int g_shared_counter = 0;
任何任务都能直接读写它。这就是全局变量的本质——所有任务可见。
那共享内存呢?其实在VxWorks里,全局变量本身就是一种共享内存。但咱们通常说的共享内存,是指通过特定API创建和管理的内存块。比如:
char *pSharedMem;
pSharedMem = (char *)malloc(1024); // 动态分配
// 或者用更正式的VxWorks方式
pSharedMem = (char *)memPartAlloc(memSysPartId, 1024);
我个人习惯把全局变量看作「隐式共享内存」,而malloc出来的则是「显式共享内存」。两者底层原理一样,但使用场景和注意事项有区别。
2.2 使用场景:什么时候该用?
我在项目中遇到过不少纠结的同事,总在问「这里该用全局变量还是消息队列?」。我的建议很简单:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 系统配置参数(只读) | 全局变量 | 简单高效,无竞态风险 |
| 传感器数据采集(高频写入) | 共享内存 + 互斥锁 | 避免消息队列的拷贝开销 |
| 任务间传递小数据(低频) | 消息队列 | 自带同步,更安全 |
| 大数据块共享(如视频帧) | 共享内存 | 零拷贝,性能最优 |
说白了,性能敏感、数据量大的场景,优先考虑共享内存。数据量小、需要同步的场景,用消息队列更省心。
2.3 注意事项:别踩这些坑
嗯,这里要注意几个关键点。我当年踩过的坑,今天全抖出来给你们看。
全局变量什么时候初始化?很多新手以为定义时就初始化好了。错!在VxWorks里,全局变量的初始化发生在系统启动阶段,而不是任务创建时。如果你在任务里依赖某个全局变量的初始值,一定要确认它已经被正确初始化了。
我曾经遇到一个bug,一个任务启动时读取全局变量,结果读到的是0。查了两天才发现,那个变量在另一个模块的初始化函数里赋值,而那个模块还没加载完。
共享内存如果被不同CPU架构的任务访问(比如DSP和ARM共享内存),一定要注意对齐问题。我记得有一次,一个结构体在x86上编译是4字节对齐,在ARM上却是8字节对齐。结果数据读出来全是乱的。
解决方案?用__attribute__((packed))或者手动填充字节。
我建议所有全局变量都加个前缀,比如g_或者s_。这样一眼就能看出是共享资源。别问我为什么强调这个——你试试在一个50万行的项目里找一个没前缀的全局变量,那酸爽...
2.4 竞态问题:这才是真正的硬骨头
好,重头戏来了。竞态问题,说白了就是多个任务同时读写同一块数据,导致结果不可预测。
为什么会这样?你想想看,一个简单的g_counter++,在汇编层面可能是三条指令:
LOAD g_counter -> R1
ADD R1, 1 -> R1
STORE R1 -> g_counter
如果任务A执行到LOAD之后被切换出去,任务B进来把g_counter从0改成了1。然后任务A回来继续执行,它不知道g_counter已经变了,还是把1写回去。结果呢?本该是2的计数器,变成了1。
这就是经典的读-改-写竞态。
2.5 解决方案:怎么保护共享数据?
解决竞态问题,VxWorks给了我们几把武器。我按推荐程度排个序:
- 互斥信号量(Mutex):最常用,适合保护一段代码临界区。
- 二进制信号量:轻量级,但要注意优先级反转。
- 任务锁(taskLock):禁止任务调度,简单粗暴,但影响实时性。
- 中断锁(intLock):只在中断服务程序里用,别乱用。
来看个实际例子。假设我们有个共享缓冲区:
SEM_ID g_semMutex;
char g_buffer[1024];
int g_dataLen = 0;
// 初始化
void initSharedMem() {
g_semMutex = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
}
// 写入任务
void writerTask() {
semTake(g_semMutex, WAIT_FOREVER);
// 写入数据到g_buffer
g_dataLen = 512;
semGive(g_semMutex);
}
// 读取任务
void readerTask() {
semTake(g_semMutex, WAIT_FOREVER);
// 从g_buffer读取数据
int len = g_dataLen; // 安全了!
semGive(g_semMutex);
}
记住一个原则——谁拿锁,谁放锁。千万别在任务A里拿锁,在任务B里放锁。我见过有人这么干,结果系统直接死锁,调试了一整夜。
2.6 高级话题:无锁编程
有些场景下,用锁会影响性能。比如一个任务每秒要更新10000次数据,每次拿锁放锁的开销就受不了。这时候可以考虑无锁编程。
VxWorks支持原子操作:
// 原子加1
vxAtomicAdd(&g_counter, 1);
// 原子比较并交换
int oldVal = 0;
vxAtomicCas(&g_counter, &oldVal, 1);
但我要提醒你:无锁编程非常容易出错。我个人的经验是,除非你能证明锁是性能瓶颈,否则老老实实用互斥信号量。别为了炫技给自己挖坑。
2.7 实战建议:我的经验总结
最后,给大家几条我在实际项目中总结出来的建议:
- 能用局部变量就别用全局变量。很多数据其实不需要共享,传参就能解决。
- 共享数据一定要加锁。别觉得「这个变量只有一个人写,其他人只读」就安全。你永远不知道未来谁会改代码。
- 锁的粒度要适中。锁太大,性能差;锁太小,容易死锁。我一般以「一个功能模块」为单位来加锁。
- 调试时加日志。在拿锁和放锁的地方打印任务ID和时间戳,竞态问题一抓一个准。
好了,共享内存和全局变量这块就讲这么多。记住一句话:共享是万恶之源,但有时候又不得不共享。关键是要用对工具,守好规矩。
下一章咱们聊聊消息队列,那玩意儿比共享内存安全多了,但也有自己的脾气。