shmop本身不提供锁机制,必须在应用层引入外部锁(如flock);其mode参数仅控制文件权限,不影响并发访问顺序;共享内存读写需配对加锁,且须处理数据边界问题。

shmop 本身不提供锁机制,必须额外加锁
PHP 的 shmop 扩展只负责内存段的创建、读写和销毁,所有函数(如 shmop_write、shmop_read)都是无锁的。多个进程同时调用 shmop_write 写同一块偏移位置,或一边写一边读未对齐的数据,必然导致脏数据或截断——这不是 bug,是设计如此。
你不能依赖 shmop 自带同步能力,必须在应用层引入外部锁机制。常见误区是以为 shmop_open 的 mode 参数(如 0644)能控制并发,它只是系统级权限位,不影响进程间访问顺序。
flock 文件锁是最简单可靠的跨进程同步方案
用临时文件做锁载体,配合 flock() 是最轻量、兼容性最好、且无需额外扩展的方式。关键点在于:锁文件路径需全局唯一且与共享内存键可推导,避免多组共享内存互相干扰。
- 生成锁文件路径时,建议用
ftok(__FILE__, 'x')得到的整数键转为十六进制,拼成如/tmp/shm.lock.12345,和shmop_open($key, ...)使用同一个$key - 必须在
shmop_write前获取LOCK_EX,在shmop_read前获取LOCK_SH(若允许并发读);读写操作完成后立即flock($fp, LOCK_UN),再调用shmop_close - 不要在
flock持有期间做耗时操作(如网络请求、大数组序列化),否则锁持有时间过长,成为性能瓶颈 -
flock在 PHP-FPM 下有效,但注意:如果进程被 SIGKILL 强杀,锁可能不会自动释放(Linux 内核会回收,一般不用手动处理)
为什么不用 Mutex 或 APCu 实现锁
Mutex 扩展在 PHP 7.2 中已废弃,且仅支持 CLI 模式,在 FPM/Apache 下不可靠;APCu 的 apcu_entry() 或 apcu_store(..., $ttl=0) 虽可模拟锁,但它本质是用户缓存,不具备原子性保证——两个进程同时 apcu_fetch 返回 false 后都执行 apcu_store,就会发生竞态覆盖。
立即学习“PHP免费学习笔记(深入)”;
更现实的问题是:APCu 锁无法跨不同用户 UID 的进程(比如 www-data 和 cron 用户),而 flock 基于文件系统,只要路径可访问、权限一致,就生效。
如果你坚持用内存锁,唯一可行的是结合 sem_get() + sem_acquire()(System V 信号量),但它需要 sysvsem 扩展启用,配置复杂,且在容器或某些云环境(如 AWS Lambda)中默认不可用。
一个最小可行的读写封装示例
// 示例:安全写入共享内存
function shm_safe_write($key, $data) {
$shm_id = shmop_open($key, "c", 0644, strlen($data) + 1);
if (!$shm_id) return false;
$lock_file = "/tmp/shm.lock." . dechex($key);
$fp = fopen($lock_file, "c");
if (!$fp || !flock($fp, LOCK_EX)) {
shmop_close($shm_id);
return false;
}
$result = shmop_write($shm_id, $data, 0) !== false;
flock($fp, LOCK_UN);
fclose($fp);
shmop_close($shm_id);
return $result;
}
// 示例:安全读取(带长度头更健壮,此处省略)
function shm_safe_read($key) {
$shm_id = shmop_open($key, "a", 0644, 0);
if (!$shm_id) return false;
$lock_file = "/tmp/shm.lock." . dechex($key);
$fp = fopen($lock_file, "c");
if (!$fp || !flock($fp, LOCK_SH)) {
shmop_close($shm_id);
return false;
}
$size = shmop_size($shm_id);
$data = shmop_read($shm_id, 0, $size);
flock($fp, LOCK_UN);
fclose($fp);
shmop_close($shm_id);
return rtrim($data, "\0");
}
真正容易被忽略的是:共享内存内容没有边界标记,shmop_read 返回的字节流末尾可能含残留垃圾(尤其是多次写入长度不等时)。生产环境务必约定协议,比如前 4 字节存实际长度,或用 \0 截断,否则 shm_safe_read 可能读出上一次的旧尾巴。



















