1. 共享内存实战从创建到读写记得我第一次在Linux下用共享内存做进程通信时踩过一个典型的坑两个进程同时写共享内存导致数据错乱。当时调试到凌晨三点才明白原来共享内存本身并不提供同步机制。下面我们就从最基础的实验代码出发逐步拆解共享内存的使用要点。1.1 创建共享内存的关键步骤先看这段创建共享内存的代码#include sys/shm.h #define BUFSZ 4096 int main() { key_t key ftok(keyfile, 0); // 生成唯一key int shm_id shmget(key, BUFSZ, IPC_CREAT | 0666); system(ipcs -m temp); // 查看共享内存状态 return 0; }这里有几个容易忽略的细节ftok的第二个参数是项目ID0-255不同进程要用相同的参数才能得到相同的keyshmget的第三个参数中的权限标志如0666必须设置否则其他进程可能无法访问实际项目中建议用IPC_EXCL标志检测是否已存在相同key的共享内存我曾经遇到过一个线上问题因为忘记设置权限标志导致生产环境的监控进程无法读取数据。用ipcs -m命令查看时发现共享内存的权限栏显示为0这就是典型权限设置遗漏的表现。1.2 读写共享内存的完整流程读写操作比创建要复杂得多先看写进程的代码// write.c void *s shmat(shm_id, 0, 0); char *t Hello shared memory!; memcpy(s, t, strlen(t)1); shmdt(s);读进程的代码类似但要注意错误处理// read.c if (argc ! 2) { printf(USAGE: atshm identifier\n); exit(1); } void *s shmat(atoi(argv[1]), 0, 0); printf(%s\n, (char*)s); shmdt(s);实测中我发现三个常见问题忘记检查shmat返回值当内存不足时会返回(void*)-1没有正确处理字符串结束符导致读取出乱码在多进程环境下读写操作没有同步保护特别提醒共享内存在进程退出后不会自动释放必须用ipcrm命令或shmctl函数显式删除否则会一直占用系统资源。我建议在代码中加入信号处理逻辑在程序退出时自动清理void cleanup(int sig) { shmctl(shm_id, IPC_RMID, NULL); exit(0); } signal(SIGINT, cleanup);2. POSIX信号量深度解析当我们需要协调多个进程对共享内存的访问时信号量就是救星。但信号量的使用远比看起来复杂特别是在处理进程意外终止的情况时。2.1 信号量的基本使用先看这个最简单的信号量创建代码#include semaphore.h sem_t *sem sem_open(/semfile, O_CREAT, 0666, 1);这里有几个关键点信号量名称要以斜杠开头且不能包含其他斜杠初始值设为1表示二进制信号量互斥锁权限0666允许所有用户访问在我的一个多进程日志系统中使用信号量实现了写锁sem_wait(sem); // 获取锁 fprintf(logfile, %s\n, logmsg); sem_post(sem); // 释放锁2.2 信号量的高级特性POSIX信号量相比System V信号量有个巨大优势支持进程间共享。通过sem_open创建的命名信号量会自动持久化即使创建进程退出其他进程仍能访问。但这也带来一个隐患如果进程崩溃时持有信号量会导致死锁。解决方案是使用SEM_UNDO标志struct sembuf op {0, -1, SEM_UNDO}; semop(sem_id, op, 1);这个标志会让内核在进程退出时自动释放信号量。我在一个数据库连接池项目中就因为这个特性避免了严重的生产事故。另一个实用技巧是定时等待struct timespec ts; clock_gettime(CLOCK_REALTIME, ts); ts.tv_sec 2; // 等待2秒 if (sem_timedwait(sem, ts) -1) { if (errno ETIMEDOUT) { printf(等待信号量超时\n); } }3. 死锁的四大条件与破解之道去年我们团队遇到一个棘手的死锁问题日志服务在高峰时段会完全卡死。经过分析发现是因为违反了锁的获取顺序。下面结合这个案例详解死锁的应对策略。3.1 死锁的必备条件死锁的发生必须同时满足四个条件就像化学反应的必要不充分条件互斥访问资源一次只能被一个进程占用占有并等待进程持有资源的同时等待其他资源不可剥夺已分配的资源不能被强制收回循环等待存在进程资源的环形等待链我们的日志服务问题就出在第四条线程A先获取网络锁再获取文件锁而线程B的顺序正好相反。在高并发时形成了A等BB等A的死锁。3.2 死锁预防实战技巧固定锁顺序是最有效的预防措施。我们制定了这样的规则所有线程必须先获取网络锁然后才能获取文件锁最后获取数据库锁用代码表示就是// 正确的锁顺序 void process_request() { pthread_mutex_lock(net_mutex); pthread_mutex_lock(file_mutex); // 处理逻辑 pthread_mutex_unlock(file_mutex); pthread_mutex_unlock(net_mutex); }另一个实用技巧是尝试锁if (pthread_mutex_trylock(mutex) 0) { // 获取锁成功 } else { // 处理获取失败的情况 }在嵌入式系统中我们还常用优先级继承协议。当一个高优先级任务等待低优先级任务持有的锁时临时提升低优先级任务的优先级pthread_mutexattr_t attr; pthread_mutexattr_setprotocol(attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(mutex, attr);4. 经典案例生产者-消费者模型让我们用共享内存和信号量实现一个完整的生产者-消费者系统这是我当年面试某大厂时的实战题。4.1 模型设计与实现首先定义共享内存结构struct shared_buffer { sem_t empty; // 空槽位信号量 sem_t full; // 满槽位信号量 sem_t mutex; // 互斥锁 int data[10]; // 环形缓冲区 int in, out; // 读写指针 };生产者核心逻辑sem_wait(shm-empty); // 等待空位 sem_wait(shm-mutex); // 获取互斥锁 shm-data[shm-in] item; shm-in (shm-in 1) % 10; sem_post(shm-mutex); sem_post(shm-full); // 增加满位计数消费者对称实现sem_wait(shm-full); sem_wait(shm-mutex); int item shm-data[shm-out]; shm-out (shm-out 1) % 10; sem_post(shm-mutex); sem_post(shm-empty);4.2 死锁预防在这个模型中的应用这里有个精妙的锁顺序设计先获取资源信号量empty/full再获取互斥锁。如果顺序反过来sem_wait(mutex); sem_wait(empty); // 可能阻塞当缓冲区满时生产者会持有mutex等待empty而消费者因拿不到mutex无法消费这就形成了死锁。另一个优化点是使用条件变量替代信号量pthread_cond_t cond PTHREAD_COND_INITIALIZER; // 生产者 pthread_mutex_lock(mutex); while (count BUFFER_SIZE) { pthread_cond_wait(cond, mutex); } // 插入数据 pthread_cond_signal(cond); pthread_mutex_unlock(mutex);在Linux内核的kfifo实现中就采用了类似的环形缓冲区加锁机制。通过基准测试发现这种设计比纯信号量版本吞吐量高出15%-20%。