PHP 读写共享内存实践
令人悲伤的起因
PHP 语言封装了大量易用的高级 API,开发人员可以基于这些 API 快速构建应用,所以一般很少会去关心内存分配、信号量操作等底层细节。
然而,在维护的一个 PHP 老系统的扩展出了点小问题,在扩展层面的各种尝试无果之后,我不得不另辟蹊径,用 PHP 搞起了共享内存和信号量。
所以,有了这篇文章,记录下 PHP 读写共享内存的一些关键步骤和注意事项,以后也许还用得上。
一些基本概念
相信大家面试的时候,应该会经常被问到「进程通信的方式有哪些」这个问题。背题背得好的都会答到共享内存、信号量这两个,但在实际工作中,使用高级编程语言或者是面向业务编程时,估计很少会接触到。
共享内存
共享内存指在多处理器的计算机系统中,可以被不同中央处理器访问的大容量内存
—— 维基百科
共享内存的存在,使两个不相关的进程可以访问同一块内存,进行高效的进程通信。在 Linux 系统下,可以通过 ipcs -m
查看系统中存在的共享内存。
1 | [root@nas ~]# ipcs -m |
共享内存被普遍认为是进程间通信方式中,最高效的一种(没有之一)。有博主对比过 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 IPC 和 Shared 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 | // 创建 System V IPC key |