PHP共享內存詳解

前言

在PHP中有這麼一族函數,他們是對UNIX的V IPC函數族的包裝。php

它們不多被人們用到,可是它們卻很強大。巧妙的運用它們,可讓你事倍功半。瀏覽器

它們包括:緩存

  1. 信號量(Semaphores)
  2. 共享內存(Shared Memory)
  3. 進程間通訊(Inter-Process Messaging, IPC)

 

基於這些,咱們徹底有可能將PHP包裝成一基於消息驅動的系統。session

可是,首先,咱們須要介紹幾個重要的基礎:併發

1. ftok函數

  1. int ftok ( string pathname, string proj )
  2. //ftok將一個路徑名pathname和一個項目名(必須爲一個字符), 轉化成一個整形的用來使用系統V IPC的key

2. tickspost

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的消息通訊。fetch

  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的結果 :

 

可見,現代磁盤優化比我們老觀念中的性能要好些了~  

但利用內存依然是個不錯的玩法~ 

相關文章
相關標籤/搜索