PHP共享内存详解

简介: 前言 在PHP中有这么一族函数,他们是对UNIX的V IPC函数族的包装。 它们很少被人们用到,但是它们却很强大。巧妙的运用它们,可以让你事倍功半。 它们包括: 信号量(Semaphores) 共享内存(Shared Memory) 进程间通信(Inter-Process Messaging, IPC)   基于这些,我们完全有可能将PHP包装成一基于消息驱动的系统。

前言

在PHP中有这么一族函数,他们是对UNIX的V IPC函数族的包装。

它们很少被人们用到,但是它们却很强大。巧妙的运用它们,可以让你事倍功半。

它们包括:

  1. 信号量(Semaphores)
  2. 共享内存(Shared Memory)
  3. 进程间通信(Inter-Process Messaging, IPC)

 

基于这些,我们完全有可能将PHP包装成一基于消息驱动的系统。

但是,首先,我们需要介绍几个重要的基础:

1. ftok

  1. int ftok ( string pathname, string proj )
  2. //ftok将一个路径名pathname和一个项目名(必须为一个字符), 转化成一个整形的用来使用系统V IPC的key

2. ticks

Ticks是从PHP 4.0.3开始才加入到PHP中的,它是一个在declare代码段中解释器每执行N条低级语句就会发生的事件。N的值是在declare中的directive部分用ticks=N来指定的。

  1. function getStatus($arg){
  2.  print_r connection_status();
  3.  
  4.  debug_print_backtrace();
  5.  
  6. }
  7. reigster_tick_function("getStatus", true);
  8.  
  9. declare(ticks=1){
  10.  
  11.  for($i =1; $i<999; $i++){
  12.  
  13.  echo "hello";
  14.  
  15.  }
  16.  
  17. }
  18.  
  19. unregister_tick_function("getStatus");

这个就基本相当于:

  1. function getStatus($arg){
  2.  print_r connection_status();
  3.  
  4.  debug_print_backtrace();
  5.  
  6. }
  7.  
  8. reigster_tick_function("getStatus", true);
  9.  
  10. declare(ticks=1){
  11.  
  12.  for($i =1; $i<999; $i++){
  13.  
  14.  echo "hello"; getStatus(true);
  15.  
  16.  }
  17.  
  18. }
  19.  
  20. unregister_tick_function("getStatus");

消息,我现在用一个例子来说明,如何结合Ticks来实现PHP的消息通信。

  1. $mesg_key = ftok(__FILE__, 'm');
  2. $mesg_id = msg_get_queue($mesg_key, 0666);
  3.  
  4. function fetchMessage($mesg_id){
  5.  
  6.  if(!is_resource($mesg_id)){
  7.  
  8.  print_r("Mesg Queue is not Ready");
  9.  
  10.  }
  11.  
  12.  if(msg_receive($mesg_id, 0, $mesg_type, 1024, $mesg, false, MSG_IPC_NOWAIT)){
  13.  
  14.  print_r("Process got a new incoming MSG: $mesg ");
  15.  
  16.  }
  17.  
  18. }
  19.  
  20. register_tick_function("fetchMessage", $mesg_id);
  21.  
  22. declare(ticks=2){
  23.  
  24.  $i = 0;
  25.  
  26.  while(++$i < 100){
  27.  
  28.  if($i%5 == 0){
  29.  
  30.  msg_send($mesg_id, 1, "Hi: Now Index is :". $i);
  31.  }
  32.  }
  33. }
  34.  
  35. //msg_remove_queue($mesg_id);

在这个例子中,首先将我们的PHP执行Process加入到一个由ftok生成的Key所获得的消息队列中。

然后,通过Ticks,没隔俩个语句,就去查询一次消息队列。

然后模拟了消息发送。

在浏览器访问这个脚本,结果如下:

  1. Process got a new incoming MSG: s:19:"Hi: Now Index is :5";
  2. Process got a new incoming MSG: s:20:"Hi: Now Index is :10";
  3. Process got a new incoming MSG: s:20:"Hi: Now Index is :15";
  4. Process got a new incoming MSG: s:20:"Hi: Now Index is :20";
  5. Process got a new incoming MSG: s:20:"Hi: Now Index is :25";
  6. Process got a new incoming MSG: s:20:"Hi: Now Index is :30";
  7. Process got a new incoming MSG: s:20:"Hi: Now Index is :35";
  8. Process got a new incoming MSG: s:20:"Hi: Now Index is :40";
  9. Process got a new incoming MSG: s:20:"Hi: Now Index is :45";
  10. Process got a new incoming MSG: s:20:"Hi: Now Index is :50";
  11. Process got a new incoming MSG: s:20:"Hi: Now Index is :55";
  12. Process got a new incoming MSG: s:20:"Hi: Now Index is :60";
  13. Process got a new incoming MSG: s:20:"Hi: Now Index is :65";
  14. Process got a new incoming MSG: s:20:"Hi: Now Index is :70";
  15. Process got a new incoming MSG: s:20:"Hi: Now Index is :75";
  16. Process got a new incoming MSG: s:20:"Hi: Now Index is :80";
  17. Process got a new incoming MSG: s:20:"Hi: Now Index is :85";
  18. Process got a new incoming MSG: s:20:"Hi: Now Index is :90";
  19. Process got a new incoming MSG: s:20:"Hi: Now Index is :95";

看到这里是不是,大家已经对怎么模拟PHP为事件驱动已经有了一个概念了? 别急,我们继续完善。

2. 信号量

信号量的概念,大家应该都很熟悉。通过信号量,可以实现进程通信,竞争等。 再次就不赘述了,只是简单的列出PHP中提供的信号量函数集。

  1. sem_acquire -- Acquire a semaphore
  2. sem_get -- Get a semaphore id
  3. sem_release -- Release a semaphore
  4. sem_remove -- Remove a semaphore

具体信息,可以翻阅PHP手册。

3. 内存共享

PHP sysvshm提供了一个内存共享方案:sysvshm,它是和sysvsem,sysvmsg一个系列的,但在此处,我并没有使用它,我使用的shmop系列函数,结合TIcks

  1. function memoryUsage(){
  2.  printf("%s: %s<br/>", date("H:i:s", $now), memory_get_usage());
  3.  
  4.  //var_dump(debug_backtrace());
  5.  
  6.  //var_dump(__FUNCTION__);
  7.  
  8.  //debug_print_backtrace();
  9.  
  10. }
  11.  
  12. register_tick_function("memoryUsage");
  13.  
  14. declare(ticks=1){
  15.  
  16. $shm_key = ftok(__FILE__, 's');
  17.  
  18. $shm_id = shmop_open($shm_key, 'c', 0644, 100);
  19.  
  20. }
  21.  
  22. printf("Size of Shared Memory is: %s<br/>", shmop_size($shm_id));
  23.  
  24. $shm_text = shmop_read($shm_id, 0, 100);
  25.  
  26. eval($shm_text);
  27.  
  28. if(!empty($share_array)){
  29.  
  30.  var_dump($share_array);
  31.  
  32.  $share_array['id'] += 1;
  33.  
  34. }else{
  35.  
  36.  $share_array = array('id' => 1);
  37.  
  38. }
  39.  
  40. $out_put_str = "$share_array = " . var_export($share_array, true) .";";
  41.  
  42. $out_put_str = str_pad($out_put_str, 100, " ", STR_PAD_RIGHT);
  43.  
  44. shmop_write($shm_id, $out_put_str, 0);
  45.  
  46. ?>

运行这个例子,不断刷新,我们可以看到index在递增。

单单使用这个shmop就能完成一下,PHP脚本之间共享数据的功能:以及,比如缓存,计数等等。

 

使用场景

监控汇总

目前正在用的一个场景,针对某一台机器上的错误进行汇总并报警,我们把一分钟之内的相同报警合并成一条,用共享内存来暂存,非常实用且高效。

PHP SESSION

如果你是单机的服务,且又启用了session,那么可以把session换成共享内存的来存储,会比文件要快上不少,这里还要强调是单机,这是最大的软肋,但就功能上来讲没有memcache方便。

什么是共享内存

共享内存是一种在同一台机器的不同进程(应用程序)之间交换数据的方式。一个进程可创建一个可供其他进程访问的内存段,并赋予它相应的权限。每个内存段拥有一个惟一的ID,我们通常称之为shmid,这个ID指向一个物理内存区域,其他进程可通过此ID来操作这块内存, 包扩读取、写入以及删除。

共享内存的使用是一种在进程之间交换数据的快速方法,主要因为在创建内存段之后传递数据,不会涉及内核。这种方法常常称为进程间通信 (IPC)。其他 IPC 方法包括管道、消息队列、RPC 和套接字。

PHP 中几种常见的共享内存使用方式

APC 可以缓存 PHP 的 opcode 提高应用的性能,可以在同个 PHP-FPM 进程池的进程间共享数据,常用功能如下:

  • apc_store
  • apc_fetch
  • apc_add
  • apc_delete
  • apcinc apcdec
  • apc_cas
  • apcclearcache
  • apcsmainfo

Shmop Unix 系统共享内存使用接口常用功能:

  • shmop_open
  • shmop_close
  • shmop_read
  • shmop_write
  • shmop_delete
ipcs -m 查看本机共享内存的状态和统计。
ipcrm -m shmid 或 ipcrm -M shmkey 清除共享内存中的数据。

SystemV Shm常用功能:

  • ftok
  • shm_attach
  • shm_detach
  • shmputvar
  • shmgetvar
  • shmremovevar

使用共享内存需要考虑操作的原子性和锁、并行和互斥。

sem 信号量相关函数: 
* semget * semremove * semacquire * semrelease

PHP 提供的 IPC 机制。

  • --enable-shmop 共享内存,只能按字节操作
  • --enable-sysvsem 信号量
  • --enable-sysvshm 共享内存,和 shmop 的差别是提供的操作函数不同,支持 key、value操作
  • --enable-sysvmsg 消息队列

本文主讲

如何使用 PHP shmop 创建和操作共享内存段,使用它们存储可供其他应用程序使用的数据。

1. 创建内存段

共享内存函数类似于文件操作函数,但无需处理一个流,您将处理一个共享内存访问 ID。第一个示例就是 shmopopen 函数,它允许您打开一个现有的内存段或创建一个新内存段。此函数非常类似于经典的 fopen 函数,后者打开用于文件操作的流,返回一个资源供其他希望读取或写入该打开的流的函数使用。让我们看看 shmopopen的用法:

<?php  
$key = ftok(__FILE__, 'h');
$mode = 'c';
$permissions = 0644;
$size = 1024;
$shmid = shmop_open($key, $mode, $permissions, $size);
?>

第一个参数($key):

系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个key值。通常情况下,该key值通过ftok函数得到, * *key是一个我们逻辑上表示共享内存段的标识。不同进程只要选择同一个Key值就可以共享同一段存储段。

第二个参数($mode):

访问模式,它类似于fopen的访问模式,有以下几种

  • 模式 “a”,它允许您访问只读内存段
  • 模式 “w”,它允许您访问可读写的内存段
  • 模式 “c”,它创建一个新内存段,或者如果该内存段已存在,尝试打开它进行读写 *模式 “n”,它创建一个新内存段,如果该内存段已存在,则会失败,返回 false,并伴随有warning: unable to attach or create shared memory segment

第三个参数($permissions):

内存段的权限。您必须在这里提供一个八进制值,它类似于UNIX操作系统文件和目录的操作权限。

第四个参数($size):

内存段大小,以字节为单位。在写入一个内存段之前,您必须在它之上分配适当的字节数。

返回结果:

此函数返回一个 ID 编号,其他函数可使用该 ID 编号操作该共享内存段。这个 ID 是共享内存访问 ID,与系统 ID 不同,它以参数的形式传递。请注意不要混淆这两者。如果失败,shmop_open 将返回 FALSE。

shmop_open成功后,使用ipcs -m, 可以查看到刚刚创建的内存段,注意 申请的内存段有严格的权限,比如用root用户申请的,普通用户就无权访问

2. 向内存段写入数据

使用 shmop_write 函数向共享内存块写入数据。此函数的使用很简单,它仅接受 3 个参数,如下所示。

<?php  
//这里shmid可以延用上一段代码返回的shmid
$shmid = shmop_open(ftok(__FILE__,'h'), 'c', 0644, 1024); 
shmop_write($shmid, "Hello World!", 0);

?>

这个函数类似于 fwrite 函数, 在这里有三个参数。 * 第一个参数(shmidshmopopenIDshmid):是shmopopen返回的ID,它识别您操作的共享内存块。∗第二个参数(data):是您希望存储的数据。 * 第三个参数($offset):是您希望开始写入的位置。默认情况下,我们始终使用 0 来表示开始写入的位置。

返回结果:此函数在失败时会返回 FALSE,在成功时会返回写入的字节数。

3. 从内存段读取数据

从共享内存段读取数据很简单。您只需要一个打开的内存段和 shmop_read 函数,它接受三个参数,如下所示:

<?php  
$shmid = shmop_open(ftok(\__FILE_\_,'h'), 'c', 0644, 1024);
shmop_write($shmid, "Hello World\!", 0);  
var_dump(shmop_read($shmid, 0, 11));  
?>
  • 第一个参数($shmid):是 shmop_open 返回的 ID,它识别您操作的共享内存块。
  • 第二个参数($start):是您希望从内存段读取的位置,这个参数可以始终为0, 表示数据的开头
  • 第三个参数(countshmopsize(count):是您希望读取的字节数。一般情况下我们用shmopsize(shmid),以便完整的读取它。

4. 删除内存段

shmop_delete 该函数只接收一个参数,如下所示:

<?php  
$shmid = shmop_open(ftok(\__FILE_\_,'h'), 'c', 0644, 1024);
shmop_delete($shmid);  
?>

其实这个函数不会实际删除该内存段。它将该内存段标记为删除状态,因为共享内存段在有其他进程正在使用它时无法被删除。shmop_delete 函数将该内存段标记为删除,阻止任何其他进程打开它。要删除它,我们需要关闭该内存段。

5. 关闭内存段

打开一个共享内存段会 “附加” 到它。附加该内存段之后,我们可在其中进行读取和写入,但完成操作后,我们必须从它解除。

<?php  
$shmid = shmop_open(ftok(\__FILE_\_,'h'), 'c', 0644, 1024);
shmop_write($shmid, "Hello World\!", 0);  
shmop_delete($shmid); shmop_close($shmid);  
?>

共享内存的原子操作 - 信号控制

针对共享内存的写操作本身不是原子性的,那么当我们大量并发进行读写的时候,怎么保证原子性呢,这里要引入信号量进行控制。

PHP 也提供了内置扩展 sysvsem ,其实我们在看sysvsem 提供的一系列sem*的方法的时候,就会想到,这和上面提到的shmop*有什么区别呢,我们来看官房文档中的这一个解释:PHP already had a shared memory extension (sysvshm) written by Christian Cartus cartus@atrior.de, unfortunately this extension was designed with PHP only in mind and offers high level features which are extremely bothersome for basic SHM we had in mind.

也就是说:sysvshm 扩展提供的方法在存储之前对用户的数据进行serialize处理,这里就导致这个存储的数据是无法与其它语言共享的,这一系列方法是php only的方法。

引入信号控制之后的示例:

<?php  
$key = ftok(_FILE_, 'h') $mode = "c";
$permissions = 0755;
$size = 1024; // 内存段的大小,单位是字节
$semid = sem_get($key); # 请求信号控制权
if (sem_acquire($semid)) {  
    $shmid = shmop_open($key, 'c', 0644, 1024); # 读取并写入数据
    shmop_write($shmid, '13800138000', 0); # 关闭内存块
    shmop_close($shmid); # 释放信号 sem_release($semid);
}

共享内存的操作是非常快的,在本地想要模拟实现写入冲突是非常困难的,但是本地想模拟实现写入冲突实际上是非常难的(考虑到计算机的执行速度)。在本地测试中,使用 for 循环操作时如果不使用shmop_close 关闭资源会出现无法打开共享内存的错误警告。这应该是因为正在共享内存被上一次操作占用中还没有释放导致。

共享内存,memcache,文件的读写速度对比。

以下是同时读写1k的数据读写100000次的时间对比:

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

<?php
//$key = ftok(_FILE_, 'h'); //$key = ftok('/tmp/', 'h'); $key = 0xff4; $permissions = 0755; $size = 1024; // 内存段的大小,单位是字节 $semid = sem_get($key); # 请求信号控制权 if (sem_acquire($semid)) { $content = file_get_contents(__FILE__); $shmid = shmop_open($key, 'c', 0644, strlen($content)); # 读取并写入数据 shmop_write($shmid, $content, 0); # 关闭内存块 shmop_close($shmid); # 释放信号 sem_release($semid); }

 

<?php
/**
 * @param $fn
 */
function profile(callable $fn)
{
    $t = microtime(1);
    $i = 0;
    while ($i < 999) {
        $fn();
        ++$i;
    }
    var_dump('takes ', microtime(1) - $t);
}



$fnIO = function (){
    file_get_contents(__DIR__."/chmop_create.php");
};


$fn = function (){
    //$key = ftok('chmop', 'h');
//$key = ftok('/tmp/', 'h');
    $key = 0xff4;
    $permissions = 0755;
    $size = 1024; // 内存段的大小,单位是字节
    $semid = sem_get($key); # 请求信号控制权
    if (sem_acquire($semid)) {
        $shmid = shmop_open($key, 'a', 0644, 1024); # 读取并写入数据
        $out = shmop_read($shmid, 0, shmop_size($shmid)); # 关闭内存块
//        var_dump($out);
//        echo $out;
        shmop_close($shmid); # 释放信号 sem_release($semid);
    }
};
profile($fn);
profile($fnIO);

 这是Mac Pro 2014上的结果:

 

这是ali ECS的结果 :

 

可见,现代磁盘优化比咱们老观念中的性能要好些了~  

但利用内存依然是个不错的玩法~ 

谋胆并重
目录
相关文章
|
4月前
|
Java PHP
从引用计数到循环垃圾回收——解锁PHP高效内存管理的秘密
【8月更文挑战第2天】深入理解PHP中的垃圾回收机制
96 3
|
4月前
|
中间件 Shell PHP
|
6月前
|
设计模式 PHP 容器
在PHP中避免循环引用导致的内存泄漏问题
在PHP中避免循环引用导致的内存泄漏问题
|
7月前
|
缓存 PHP 数据库
【PHP开发专栏】PHP代码优化与内存管理
【4月更文挑战第30天】本文探讨了PHP的代码优化和内存管理,旨在提升Web应用性能。第一部分介绍了代码优化,包括减少代码重复、选择高效数据结构、减少函数调用、使用缓存、优化数据库查询、图像处理和正则表达式优化。第二部分讲解内存管理,建议减少全局变量、正确使用内存分配函数、利用对象引用计数、避免内存泄露及优化内存分配。第三部分通过在线论坛的缓存应用和图像处理的内存池技术展示了实践案例。
62 2
|
7月前
|
存储 缓存 算法
php遇到内存溢出
php遇到内存溢出
62 3
|
PHP
PHP获取当前脚本内存占用情况
PHP获取当前脚本内存占用情况
688 0
|
前端开发 API PHP
php内存溢出:Allowed memory size of 1342bytes exhausted (tried to allocate 8192 bytes)本地配置和宝塔配置解决方案
php内存溢出:Allowed memory size of 1342bytes exhausted (tried to allocate 8192 bytes)本地配置和宝塔配置解决方案
159 0
|
Java PHP
关于php递归函数内存溢出的问题
关于php递归函数内存溢出的问题
121 1
关于php递归函数内存溢出的问题
|
Java PHP
如何避免PHP中的内存泄漏问题?底层原理是什么?
如何避免PHP中的内存泄漏问题?底层原理是什么?