多进程服务器中如何让进程感知共享内存映射的日志文件变更?
多进程mmap日志切换后父进程无法感知的问题解决
问题场景
在多进程服务器中使用mmap写入日志,初始状态下所有进程均可写入同一日志文件,但当某一子进程将共享内存映射切换至新日志文件后,父进程仍会写入旧日志文件,无法感知映射的变更。
问题代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <string.h> #include <errno.h> // 自旋锁简化实现 typedef int log_atomic_t; void log_spinlock(log_atomic_t *lock, int val, int max) { while (__sync_lock_test_and_set(lock, val) != 0) { usleep(1); } } void log_unlock(log_atomic_t *lock) { __sync_lock_release(lock); } typedef struct shared_mgs_s { log_atomic_t lock; int offset; } shared_msg_t; char *write_addr; shared_msg_t *sm; log_atomic_t *log_mutex_ptr; #define FILE_SIZE 1 * 1024 * 1024 int map_log_file(char *file_name) { int fd = -1; if ((fd = open(file_name, O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)) < 0) { printf("create %s failed %d\n",file_name, errno); return -1; } if (ftruncate(fd, FILE_SIZE) < 0) { printf("ftruncate %d failed", errno); return -1; } write_addr = (char*)mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); sm->offset = 0; return 0; } void write_log(char *msg) { memcpy(write_addr + sm->offset, msg, strlen(msg)); sm->offset += strlen(msg); } int create_shared_info() { sm = mmap(NULL, 64, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (!sm) return -1; sm->offset = 0; sm->lock = 0; log_mutex_ptr = &(sm->lock); return 0; } int main() { if(create_shared_info() < 0) { printf("create shared info failed\n"); return -1; } if(map_log_file("./old.log") != 0) { printf("map old log failed\n"); return -1; } if(0 == fork()) { log_spinlock(log_mutex_ptr, 1, 2048); munmap(write_addr, FILE_SIZE); if(map_log_file("./new.log") != 0) { printf("create new log failed\n"); return -1; } write_log("from child\n"); log_unlock(log_mutex_ptr); exit(0); } else { usleep(10); // 确保子进程先获取锁切换日志 log_spinlock(log_mutex_ptr, 1, 2048); write_log("from parent\n"); log_unlock(log_mutex_ptr); } return 0; }
问题根源
全局变量write_addr是进程私有内存空间中的指针,子进程修改自身的write_addr指向新的映射区域后,父进程的write_addr仍指向旧的日志文件映射,两者互不干扰,因此父进程无法感知映射变更。
解决方案
核心思路是将当前日志文件的唯一标识存入共享内存,所有进程在写日志前验证自身映射是否与共享内存中的最新标识一致,不一致则重新映射。具体步骤如下:
- 修改共享内存结构体:增加字段存储当前日志文件的inode和设备号(作为唯一标识,避免文件名重命名导致的误判)。
- 更新映射函数:在映射新日志文件时,将新文件的inode和设备号写入共享内存。
- 增加映射验证逻辑:每个进程在获取锁后、写日志前,检查自身当前映射的文件标识是否与共享内存中的一致,不一致则重新映射。
修改后的代码示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <string.h> #include <errno.h> #include <sys/stat.h> // 自旋锁简化实现 typedef int log_atomic_t; void log_spinlock(log_atomic_t *lock, int val, int max) { while (__sync_lock_test_and_set(lock, val) != 0) { usleep(1); } } void log_unlock(log_atomic_t *lock) { __sync_lock_release(lock); } // 修改共享结构体:增加日志文件的inode和设备号作为唯一标识 typedef struct shared_mgs_s { log_atomic_t lock; int offset; ino_t log_inode; // 当前日志文件inode dev_t log_dev; // 当前日志文件所在设备号 char log_name[256]; // 当前日志文件名 } shared_msg_t; char *write_addr = NULL; shared_msg_t *sm; log_atomic_t *log_mutex_ptr; #define FILE_SIZE 1 * 1024 * 1024 // 获取文件的inode和设备号 int get_file_id(const char *file_name, ino_t *inode, dev_t *dev) { struct stat st; int fd = open(file_name, O_RDONLY); if (fd < 0) { return -1; } if (fstat(fd, &st) < 0) { close(fd); return -1; } *inode = st.st_ino; *dev = st.st_dev; close(fd); return 0; } // 检查当前映射是否对应最新日志文件 int is_mapped_to_current_log() { if (write_addr == NULL) { return 0; } ino_t current_inode; dev_t current_dev; if (get_file_id(sm->log_name, ¤t_inode, ¤t_dev) < 0) { return 0; } // 对比共享内存中的标识 return (current_inode == sm->log_inode && current_dev == sm->log_dev) ? 1 : 0; } int map_log_file(char *file_name) { int fd = -1; if ((fd = open(file_name, O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)) < 0) { printf("create %s failed %d\n",file_name, errno); return -1; } if (ftruncate(fd, FILE_SIZE) < 0) { printf("ftruncate failed %d\n", errno); close(fd); return -1; } // 解除旧映射(如果存在) if (write_addr != NULL) { munmap(write_addr, FILE_SIZE); write_addr = NULL; } write_addr = (char*)mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (write_addr == MAP_FAILED) { printf("mmap failed %d\n", errno); close(fd); return -1; } close(fd); // 获取新文件的inode、设备号并写入共享内存 struct stat st; if (stat(file_name, &st) < 0) { printf("stat %s failed %d\n", file_name, errno); return -1; } sm->log_inode = st.st_ino; sm->log_dev = st.st_dev; strncpy(sm->log_name, file_name, sizeof(sm->log_name)-1); sm->log_name[sizeof(sm->log_name)-1] = '\0'; sm->offset = 0; return 0; } void write_log(char *msg) { memcpy(write_addr + sm->offset, msg, strlen(msg)); sm->offset += strlen(msg); } int create_shared_info() { sm = mmap(NULL, sizeof(shared_msg_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (!sm) return -1; sm->offset = 0; sm->lock = 0; sm->log_inode = 0; sm->log_dev = 0; memset(sm->log_name, 0, sizeof(sm->log_name)); log_mutex_ptr = &(sm->lock); return 0; } int main() { if(create_shared_info() < 0) { printf("create shared info failed\n"); return -1; } if(map_log_file("./old.log") != 0) { printf("map old log failed\n"); return -1; } if(0 == fork()) { log_spinlock(log_mutex_ptr, 1, 2048); if(map_log_file("./new.log") != 0) { printf("create new log failed\n"); log_unlock(log_mutex_ptr); return -1; } write_log("from child\n"); log_unlock(log_mutex_ptr); exit(0); } else { usleep(10); // 确保子进程先获取锁切换日志 log_spinlock(log_mutex_ptr, 1, 2048); // 检查当前映射是否为最新日志,否则重新映射 if (!is_mapped_to_current_log()) { if (map_log_file(sm->log_name) != 0) { printf("remap log failed\n"); log_unlock(log_mutex_ptr); return -1; } } write_log("from parent\n"); log_unlock(log_mutex_ptr); } return 0; }
关键说明
- 用
inode+设备号作为日志文件的唯一标识,避免文件名被重命名或删除导致的误判,比单纯存储文件名更可靠。 - 所有进程在获取锁后必须先验证映射的有效性,确保写入的是最新的日志文件。
- 子进程切换日志文件时,通过共享内存同步最新文件的标识和文件名,让其他进程能够感知到变更并重新映射。
内容的提问来源于stack exchange,提问作者Klen
相关产品推荐
相关产品推荐

