1. 程式人生 > >PHP共享內存詳解

PHP共享內存詳解

經典 some 交換 time 系統 fopen 函數 ear bsp 這一

使用場景

監控匯總

目前正在用的一個場景,針對某一臺機器上的錯誤進行匯總並報警,我們把一分鐘之內的相同報警合並成一條,用共享內存來暫存,非常實用且高效。

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 * sem

release

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 [email protected], 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的結果 :

技術分享圖片

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

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

PHP共享內存詳解