深度解析DDS在ROS2核心技术中的关键作用
2026/7/10 23:09:26
消息队列在内核层面已经实现了完整的并发保护机制,用户空间的操作是原子的, 不会出现数据竞争:
内核锁机制:
ipc_lock/ipc_unlock) 保护消息队列结构msgsnd,msgrcv,msgctl) 都在持有锁的情况下执行原子操作保证:
msgsnd(): 消息的分配、拷贝、入队操作是原子的msgrcv(): 消息的查找、出队、删除操作是原子的msgctl(): 队列的删除、状态更新操作是原子的等待队列机制:
虽然内核保证了操作的原子性, 但在应用逻辑层面仍可能存在竞争问题:
问题: 多个进程同时等待接收同一条消息时, 只有一个进程能收到.
场景:
// 进程 A 和进程 B 同时执行msgrcv(msqid,&msg,size,1,0);// 都等待接收 mtype=1 的消息结果:
解决方案:
问题: 多个进程可能同时尝试删除同一个消息队列.
场景:
// 进程 A 和进程 B 同时执行msgctl(msqid,IPC_RMID,NULL);// 都尝试删除队列结果:
EIDRM错误 (队列已被删除)EIDRM错误最佳实践:
EIDRM后正常退出问题: 多个进程同时发送消息时, 消息的最终顺序可能不确定.
场景:
// 进程 Amsgsnd(msqid,&msg1,size,0);// 发送消息 1// 进程 B (几乎同时)msgsnd(msqid,&msg2,size,0);// 发送消息 2结果:
解决方案:
问题: 多个进程使用不同的msgtyp接收消息时, 可能产生竞争.
场景:
// 进程 A: 接收 mtype=1 的消息msgrcv(msqid,&msg,size,1,0);// 进程 B: 接收任意类型的消息msgrcv(msqid,&msg,size,0,0);结果:
最佳实践:
共享内存允许多个进程同时访问同一块物理内存, 这带来了严重的竞争条件(Race Condition)问题:
数据竞争(Data Race)
counter++, 可能丢失一次更新读写竞争
写写竞争
非原子操作
array[i] = array[i] + 1不是原子操作由于共享内存没有内核层面的保护,必须使用用户空间的同步机制来避免竞争.
System V 信号量是最常用的共享内存同步机制, 适合跨进程同步.
特点:
示例代码:
#include<stdio.h>#include<sys/shm.h>#include<sys/sem.h>#include<sys/ipc.h>#include<string.h>#include<unistd.h>#defineSHM_SIZE1024// 信号量操作结构unionsemun{intval;structsemid_ds*buf;unsignedshort*array;};// P 操作(等待)voidsem_wait(intsemid,intsemnum){structsembufop;op.sem_num=semnum;op.sem_op=-1;// 减 1op.sem_flg=SEM_UNDO;// 进程退出时自动恢复semop(semid,&op,1);}// V 操作(释放)voidsem_signal(intsemid,intsemnum){structsembufop;op.sem_num=semnum;op.sem_op=1;// 加 1op.sem_flg=SEM_UNDO;semop(semid,&op,1);}intmain(){key_tkey=ftok(".",'s');// 创建信号量集(包含 1 个信号量, 初始值为 1, 用作互斥锁)intsemid=semget(key,1,IPC_CREAT|0666);if(semid==-1){perror("semget");return1;}// 设置信号量初始值为 1unionsemun arg;arg.val=1;if(semctl(semid,0,SETVAL,arg)==-1){perror("semctl");return1;}// 创建共享内存intshmid=shmget(key,SHM_SIZE,IPC_CREAT|0666);if(shmid==-1){perror("shmget");return1;}// 附加共享内存char*shmaddr=(char*)shmat(shmid,NULL,0);if(shmaddr==(void*)-1){perror("shmat");return1;}// 使用信号量保护共享内存访问for(inti=0;i<1000;i++){sem_wait(semid,0);// 获取锁// 临界区: 安全地访问共享内存int*counter=(int*)shmaddr;(*counter)++;printf("PID %d: counter = %d\n",getpid(),*counter);sem_signal(semid,0);// 释放锁}// 清理shmdt(shmaddr);semctl(semid,0,IPC_RMID);shmctl(shmid,IPC_RMID,NULL);return0;}POSIX 命名信号量也可以用于进程间同步, 使用更简单.
示例代码:
#include<stdio.h>#include<sys/shm.h>#include<semaphore.h>#include<fcntl.h>#include<sys/stat.h>#include<unistd.h>#defineSHM_SIZE1024#defineSEM_NAME"/my_semaphore"intmain(){key_tkey=ftok(".",'s');// 创建或打开命名信号量sem_t*sem=sem_open(SEM_NAME,O_CREAT,0666,1);if(sem==SEM_FAILED){perror("sem_open");return1;}// 创建共享内存intshmid=shmget(key,SHM_SIZE,IPC_CREAT|0666);char*shmaddr=(char*)shmat(shmid,NULL,0);// 使用信号量保护for(inti=0;i<1000;i++){sem_wait(sem);// P 操作// 临界区int*counter=(int*)shmaddr;(*counter)++;sem_post(sem);// V 操作}// 清理shmdt(shmaddr);sem_close(sem);sem_unlink(SEM_NAME);return0;}可以将pthread_mutex_t放在共享内存中, 但需要特殊初始化.
注意事项:
PTHREAD_PROCESS_SHARED属性pthread_mutexattr_setpshared()设置共享属性示例代码:
#include<stdio.h>#include<sys/shm.h>#include<pthread.h>#include<sys/ipc.h>#defineSHM_SIZE1024typedefstruct{pthread_mutex_tmutex;intcounter;chardata[1024];}shared_data_t;intmain(){key_tkey=ftok(".",'s');// 创建共享内存intshmid=shmget(key,sizeof(shared_data_t),IPC_CREAT|0666);shared_data_t*shm=(shared_data_t*)shmat(shmid,NULL,0);// 初始化互斥锁属性pthread_mutexattr_tattr;pthread_mutexattr_init(&attr);pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_SHARED);// 初始化共享内存中的互斥锁pthread_mutex_init(&shm->mutex,&attr);// 使用互斥锁保护for(inti=0;i<1000;i++){pthread_mutex_lock(&shm->mutex);// 临界区shm->counter++;pthread_mutex_unlock(&shm->mutex);}// 清理pthread_mutex_destroy(&shm->mutex);shmdt(shm);return0;}对于简单的计数器操作, 可以使用原子操作, 无需加锁.
Linux 原子操作 API:
__sync_fetch_and_add()(GCC 内置)__atomic_fetch_add()(C11 标准)atomic_t(内核接口, 用户空间不直接使用)示例代码:
#include<stdio.h>#include<sys/shm.h>#include<sys/ipc.h>#defineSHM_SIZE1024intmain(){key_tkey=ftok(".",'s');intshmid=shmget(key,SHM_SIZE,IPC_CREAT|0666);int*counter=(int*)shmat(shmid,NULL,0);// 使用原子操作, 无需加锁for(inti=0;i<1000;i++){// GCC 内置原子操作__sync_fetch_and_add(counter,1);// 或者使用 C11 标准原子操作// __atomic_fetch_add(counter, 1, __ATOMIC_SEQ_CST);}shmdt(counter);return0;}注意: 原子操作只适用于简单的读-修改-写操作, 对于复杂的临界区仍然需要锁.
自旋锁适合短时间的临界区, 但需要放在共享内存中.
示例代码:
#include<stdio.h>#include<sys/shm.h>#include<stdatomic.h>#defineSHM_SIZE1024typedefstruct{atomic_flag lock;// 自旋锁intcounter;}shared_data_t;voidspin_lock(atomic_flag*lock){while(atomic_flag_test_and_set(lock)){// 自旋等待}}voidspin_unlock(atomic_flag*lock){atomic_flag_clear(lock);}intmain(){key_tkey=ftok(".",'s');intshmid=shmget(key,sizeof(shared_data_t),IPC_CREAT|0666);shared_data_t*shm=(shared_data_t*)shmat(shmid,NULL,0);// 初始化自旋锁atomic_flag_clear(&shm->lock);// 使用自旋锁for(inti=0;i<1000;i++){spin_lock(&shm->lock);// 临界区shm->counter++;spin_unlock(&shm->lock);}shmdt(shm);return0;}| 特性 | 消息队列 | 共享内存 |
|---|---|---|
| 内核保护 | ✅ 内核保证操作原子性 | ❌ 需要用户空间同步 |
| 数据竞争 | ✅ 不存在 (内核保护) | ❌ 存在 (需要加锁) |
| 消息消费 | ✅ 自动删除 (一对一) | ❌ 需要手动管理 |
| 同步需求 | ✅ 不需要额外同步 | ❌ 必须使用信号量/互斥锁 |
| 竞争类型 | 应用逻辑竞争 (预期行为) | 数据竞争 (需要避免) |
| 加锁机制 | 不需要 | System V/POSIX 信号量、互斥锁、原子操作、自旋锁 |
| 使用复杂度 | 低 (内核自动处理) | 高 (需要手动同步) |
| 性能影响 | 系统调用开销 | 同步机制开销 + 内存访问 |
消息队列:
共享内存:
选择建议:
man 2 msgget,man 2 msgsnd,man 2 msgrcv,man 2 msgctlman 2 shmget,man 2 shmat,man 2 shmdt,man 2 shmctlman 2 semget,man 2 semop,man 2 semctlman 7 mq_overviewman 7 shm_overview