PHP 读写共享内存实践

令人悲伤的起因

PHP 语言封装了大量易用的高级 API,开发人员可以基于这些 API 快速构建应用,所以一般很少会去关心内存分配、信号量操作等底层细节。

然而,在维护的一个 PHP 老系统的扩展出了点小问题,在扩展层面的各种尝试无果之后,我不得不另辟蹊径,用 PHP 搞起了共享内存和信号量。

所以,有了这篇文章,记录下 PHP 读写共享内存的一些关键步骤和注意事项,以后也许还用得上。

一些基本概念

相信大家面试的时候,应该会经常被问到「进程通信的方式有哪些」这个问题。背题背得好的都会答到共享内存、信号量这两个,但在实际工作中,使用高级编程语言或者是面向业务编程时,估计很少会接触到。

共享内存

共享内存指在多处理器的计算机系统中,可以被不同中央处理器访问的大容量内存

—— 维基百科

共享内存的存在,使两个不相关的进程可以访问同一块内存,进行高效的进程通信。在 Linux 系统下,可以通过 ipcs -m 查看系统中存在的共享内存。

1
2
3
4
[root@nas ~]# ipcs -m

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status

共享内存被普遍认为是进程间通信方式中,最高效的一种(没有之一)。有博主对比过 memcache、文件、共享内存,读写 10 万次 1K 数据的耗时,详见下表。

读(单位:s) 写(单位:s)
memcache 7.8 8.11
file 2.6 3.2
shm 0.1 0.07

虽然共享内存是进程间通信效率最高的方式,然而由于未提供同步机制,在多进程同时读写同一个内存块时,会发生意料之外的状况。

举个例子,如果你希望使用共享内存记录网站浏览次数,由于 php-fpm 是多进程的工作模式,在高并发场景下,多个进程同时取到了同样的浏览次数值,加一之后再先后写入共享内存,就会导致浏览次数记录不准确。

信号量

信号量(英语:semaphore)又称为信号标,是一个同步对象,用于保持在 0 至指定最大值之间的一个计数值。

—— 维基百科

可以把信号量理解为一个系统层面的计数器,创建计数器时设定了一个最大值。获取信号量时,计数器值减一,释放信号量时,计数器加一。当计数器值为零时,不可再获取。

还是上一节提到的「网站浏览次数」的例子,先创建一个最大值为 1 的信号量,进程需要先获取这个信号量,才可以对读写共享内存,则可以保证记录的正确性。

PHP 共享内存和信号量

共享内存

PHP 中对共享内存段的操作有两组函数:System V IPCShared Memory。其中,System V IPC 系列函数能够更方便的操作数据,无需像 Shared Memory 那样必须自己掌握读写时的偏移量、长度等,也不用序列化/反序列化来回转换。

但是 System V IPC 系列不支持Windows,所以如果要在 Windows 环境下使用,只能选 Shared Memory。在某些场景下,如果还有其它语言编写的程序需要读写同一段共享内存,为了保证格式统一,则只能使用 Shared Memory。

PHP 默认不支持这两组函数,需要在编译 PHP 时分别加上 –enable-sysvshm–enable-shmop

信号量

在编译 PHP 时,加上 –enable-sysvsem 可以使用信号量操作的函数。

PHP 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建 System V IPC key
$semKey = ftok(php_ini_loaded_file(), 'x');
// 获取用于访问信号量的 ID
$semId = sem_get($semKey);

// 创建或者打开共享内存(4 字节大小)
$shmId = shmop_open('0x00000001', 'c', 0666, 4);
if ($shmId) {
return;
}

// 获取锁
sem_acquire($semId);

// 读取共享内存
$rawData = shmop_read($shmId, 0, 4);
$arrayData = unpack('L*', $rawData);
var_dump($arrayData);

// 写入内容
$val = array_values($arrayData)[0] + 1;
$packedStr = pack('L*', $val);
shmop_write($shmId, $packedStr, 0);

// 释放锁
sem_release($semId);