php:與進程的並發。角與 shmop 的進程間通信

PHPz
發布: 2024-08-20 06:34:37
原創
436 人瀏覽過

php 不是一種開發人員通常會考慮記憶體等問題的語言。我們只是圍繞變數和函數進行處理,然後讓內部為我們找出所有「記憶體的東西」。讓我們改變這一點。

在本系列的第一部分中,我們建立了一個 PHP 腳本,該腳本能夠透過分叉子進程來同時執行多個任務。它運作得很好,但有一個明顯的、未解決的問題:這些子進程無法將資料傳回父進程。

在本期中,我們將透過使用 shmop(php 的「共享記憶體操作」)來解決該問題。

php: concurrency with processes. pt. interprocess communication with shmop
shmop!

內容

  • 關於共享記憶體的非常簡短且極其可選的解釋
  • shmop 的基本天橋
  • 實際實施
  • 使用 shmop_open 開啟記憶體區塊
  • 使用 shmop_write 來...寫入
  • 使用 shmop_read 讀取記憶體
  • 使用 shmop_delete 進行清理
  • 處理錯誤

對共享記憶體的非常簡短且極其可選的解釋

當一個新進程啟動時,作業系統會為其分配一塊記憶體以供使用。進程無法讀取或寫入不屬於自己的內存,因為這將是一場安全噩夢。完全合理。

然而,在處理本系列第一部分中使用 pcntl_fork 創建的進程時,這給我們帶來了一個問題,因為這意味著子進程沒有簡單的方法可以相互通信或與父進程通信。子進程在創建時將獲得其父進程記憶體的副本,因此在 fork 之前分配的所有變數都可以訪問,但對這些變數的任何更改都將僅限於子進程。不同的記憶等等。如果我們希望子進程能夠寫入父進程可以讀取的變量,我們就會遇到問題。

對此有許多解決方案,所有這些解決方案都歸入「進程間通訊」或 iPC 的一般類別下。我們將用於 php 腳本的是「共享記憶體」。

顧名思義,共享記憶體是任意數量的進程可以存取的記憶體區塊。共享記憶體區塊由(希望)唯一的鍵來識別。任何知道密鑰是什麼的進程都可以存取該記憶體區塊。這使得子進程可以向其父進程報告;子進程將資料寫入共享記憶體區塊,退出後,父進程將讀取共享資料。這是一個優雅的解決方案。

當然,在執行此操作時,我們需要避免一些問題:我們需要確保標識共享記憶體區塊的金鑰是唯一的,並且我們需要強制共享記憶體通訊只能進行一次避免多個進程同時嘗試寫入同一個區塊並造成混亂的方法。我們將在實作中涵蓋所有這些。

shmop 的基本天橋

php 有豐富且強大的 api 來處理共享記憶體。手冊中指出“Shmop 是一組易於使用的函數,允許 PHP 讀取、寫入、創建和刪除 Unix 共享內存段”,並且......這沒有錯。

我們來看看使用共享記憶體的核心步驟:

建立唯一的key:所有共享記憶體都由一個key來識別。任何知道共享記憶體區塊的鍵是什麼的進程都可以存取它。傳統上,該金鑰是透過從檔案系統產生資料(即從現有檔案的索引節點建立的值)來建立的,因為檔案系統是所有進程共有的。我們將為此使用 ftok。

使用鍵分配記憶體區塊:進程可以使用共享記憶體區塊的鍵透過 shmop_open「開啟」它。如果共享記憶體區塊不存在,則創建它。 open函數的傳回值是可用於讀寫的指標。如果你以前使用過 fopen 和 fwrite,這個過程應該很熟悉。

將資料寫入記憶體區塊:寫入共享記憶體的介面與fwrite非常相似。使用指針,並將要寫入記憶體的字串作為參數傳遞。執行此操作的函數稱為 shmop_write。

從記憶體區塊讀取資料:從共享記憶體讀取資料是透過 shmop_read 完成的,同樣使用 shmop_open 中的指標。傳回值是一個字串。

使用鍵刪除記憶體區塊:在不再需要共享記憶體後刪除它很重要。這是透過 shmop_delete 完成的。

實際執行

讓我們從一個例子開始。下面的程式碼可以工作,如果你足夠不好奇或者是 tl;dr 類型,你可以複製貼上修改它,但對於其他人,我們將回顧所有 shmop 步驟並解釋它們的作用以及它們如何工作。

// the file used by ftok. can be any file.
$shmop_file = "/usr/bin/php8.3";

for($i = 0; $i < 4; $i++) {
    // create the fork
    $pid = pcntl_fork();

    // an error has ocurred
    if($pid === -1) {
        echo "error".PHP_EOL;
    }

    // child process
    else if(!$pid) {

        // create a random 'word' for this child to write to shared memory 
        $random_word = join(array_map(fn($n) => range('a', 'z')[rand(0, 25)], range(1,5)));

        // write to shmop
        $shm_key = ftok($shmop_file, $i);
        $shm_id = shmop_open($shm_key, 'n', 0755, 1024);
        shmop_write($shm_id, $random_word, 0);

        print "child $i wrote '$random_word' to shmop".PHP_EOL;

        // terminate the child process
        exit(0);
    }
}

// wait for all child processes to finish
while(($pid = pcntl_waitpid(0, $status)) != -1) {
    echo "pid $pid finished".PHP_EOL;
}

// read all our shared memories
for($i = 0; $i < 4; $i++) {

    // recreate the shm key
    $shm_key = ftok($shmop_file, $i);

    // read from the shared memory
    $shm_id = shmop_open($shm_key, 'a', 0755, 1024);
    $shmop_contents = shmop_read($shm_id, 0, 1024);

    print "reading '$shmop_contents' from child $i".PHP_EOL;

    // delete the shared memory so the shm key can be reused in future runs of this script
    shmop_delete($shm_id);
}
登入後複製

使用 ftok 建立共享記憶體金鑰

正如我們上面介紹的,所有共享記憶體區塊都由唯一的整數鍵標識,在我們開始分配記憶體的任務之前,我們已經建立了該鍵。

老實說,我們可以使用任何我們想要的整數,只要它是唯一的,但是普遍接受的規範方法是使用 ftok 使用文件系統中的現有文件作為整數來創建一個整數參考點。

這樣做的理由非常簡單。進程彼此之間一無所知,這使得它們很難共享共同商定的值。不過,系統上所有行程確實所共有的少數幾個共同點之一就是檔案系統。因此,ftok。

除了現有檔案的路徑之外,ftok 還接受一個project_id 參數。根據文檔,這是一個“單一字串”,使用其他程式語言的人會稱之為“字元”。項目id的目的是防止創建共享記憶體時發生衝突。如果兩個不同供應商的兩個項目都決定使用 /etc/passwd 作為 ftok 的參數,那麼混亂就會隨之而來。

讓我們來看一個相當簡單的例子:

$shm_key = ftok('/usr/bin/php8.3', 'j');
print "shm_key = $shm_key";
登入後複製

在這裡,我們將完整路徑傳遞給我們知道系統上存在的文件,並提供一個字元的project_id「j」。如果我們運行這個,列印語句將輸出如下內容:

shm_key = 855706266
登入後複製

這是一個很好的整數,可以用來創建我們的共享記憶體!

如果您在系統上執行此程式碼,即使您使用了相同的參數,您幾乎肯定會得到不同的回傳值。這是因為,在底層,ftok 使用檔案的 inode,而這在不同的系統中是不同的。

如果由於某種原因,我們傳遞給 ftok 一個存在的文件,我們會收到警告。

PHP Warning:  ftok(): ftok() failed - No such file or directory in <our script> on line <the line>
shm_key = -1
登入後複製

請注意,這只是一個警告,ftok 會向前衝並給我們一個 -1 的值,這將導致以後出現問題。小心點。

現在,讓我們重新檢視第 20 行對 ftok 的呼叫:

$shm_key = ftok($shmop_file, $i);
登入後複製

這裡我們向 ftok 傳遞了我們在 $shm_key 中設定的檔案的路徑,在本例中為 /usr/bin/php8.3,我們知道系統上存在一個檔案。

對於我們的project_id,我們使用$i,我們正在循環的陣列的索引。我們這樣做是為了讓每個子進程都有自己的共享記憶體塊來儲存其結果。請記住,如果多個進程嘗試寫入共享內存,就會發生不好的事情。在這裡使用索引可以幫助我們避免這種情況。

使用 shmop_open 開啟記憶體區塊

如果您曾經使用 php 的 fopen 和 fwrite 等工具進行過文件訪問,那麼使用 shmop 將會非常熟悉。

讓我們從使用 shmop_open 開啟共享記憶體區塊開始:

$shm_id = shmop_open($shm_key, 'n', 0755, 1024);
登入後複製

這個函數有四個參數:

  • key:我們使用 ftok 建立的唯一金鑰。
  • 模式:我們想要的存取類型。當使用 fopen 開啟檔案時,我們使用 r 表示「讀取」或 w 表示寫入等模式。 shmop_open 的模式與此相似,但也有差別。我們將回顧以下所有內容。
  • permissions:共享記憶體區塊的讀取/寫入/執行權限,以八進位表示。處理共享記憶體的介面與檔案存取非常相似,其中包括權限。如果您對八進位表示法沒有信心,可以使用檔案權限計算器。我們在此範例中使用 0755,但您可能需要收緊它。
  • size:記憶體區塊的大小(以位元組為單位)。在範例中,我們分配了 1 MB,這顯然是多餘的。但是,請注意,如果我們覆蓋共享記憶體區塊,該值將被截斷。如果我們嘗試從記憶體區塊中讀取比其大小更多的字節,則會發生致命錯誤。如果我們不確定要寫入記憶體的資料的確切大小,最好高估我們需要的記憶體區塊有多大。

這裡的一個重要注意事項是,如果該鍵上的記憶體區塊尚不存在,則呼叫 shmop_open 將建立一個新的記憶體區塊。這與 fopen 的行為類似,但對於 shmop_open,此行為取決於我們傳遞的「模式」參數。

如範例所示,shmop_open 傳回一個可用於存取的指標:讀取或寫入,取決於用於開啟記憶體區塊的模式。

php: concurrency with processes. pt. interprocess communication with shmop
007 are bad permissions for a spy

a little bit more about that 'mode' argument

the mode argument that we pass to shmop_open determines how we can access our shared memory block. there are four options, all covered in the official documentation, but for the sake of simplicity, we'll only look at the two we need for our purposes.

  • n : the 'n' stands for 'new' and is used when we want to create a new shared memory block. there does exist a mode c for 'create', but we are choosing to use n here because this mode will fail if we try to open a memory block that already exists. that's a safety feature! in fact, the docs state that using n to create a new shared memory block is 'useful' for 'security purposes'. the pointer returned from shmop_open using mode n is writeable.
  • a : this is for 'access'; ie. reading. do not confuse this with fopen's a mode, which is for 'append'. memory blocks opened with mode a are read-only.

if we look at the example, we can see that when we open the shared memory block in the child process to write our data, we use the n mode. this creates the new memory block in a safe way and returns a pointer that we can write to.

using shmop_write to... write.

once our child process has created a new shared memory block and received a pointer to it, it can write whatever it wants there using shmop_write.

in our example, doing this looks like:

shmop_write($shm_id, $random_word, 0);
登入後複製

the shmop_write function takes three arguments:

  • the pointer: the pointer returned from shmop_open. note that shmop_open must be called with a mode that allows writing (n in our example), otherwise attempts to use shmop_write will fail.
  • the value to write: the string to write to the shared memory block.
  • the offset: the number of bytes in memory to offset the start point of the write by. using the offset can allow us to append to a value already in the shared memory block, but doing this means keeping track of bytes written and can become unmanageable pretty quickly. in our example, we use the offset 0; we start writing at the beginning of the memory block.

shmop_write returns, as an integer, the number of bytes written.

a short note about shmop_close

if you've done file access using fopen, you're probably (hopefully!) in the habit of calling fclose when you're done writing.

we do not do that with shmop.

there is a shmop_close function, but it has been deprecated since php 8.0 and does nothing (other than throw a deprecation warning, that is). the standard practice with shmop is to just leave the pointer 'open' after we're done writing. we'll delete it later.

reading from shared memory

once all the child processes have written their data to their respective shared memory blocks an exited, all that remains is for the parent process to read that data. the strategy for this is:

  • recreate the key of the shared memory block
  • use the key to open the shared memory in 'access only' mode
  • read the data into a variable

let's look again at the example we have for reading shared memory.

// read all our shared memories
for($i = 0; $i < 4; $i++) {

    // recreate the shm key
    $shm_key = ftok($shmop_file, $i);

    // read from the shared memory
    $shm_id = shmop_open($shm_key, 'a', 0755, 1024);
    $shmop_contents = shmop_read($shm_id, 0, 1024);

    print "reading '$shmop_contents' from child $i".PHP_EOL;

    // delete the shared memory so the shm key can be reused in future runs of this script
    shmop_delete($shm_id);
}
登入後複製

recreating the shmop key

when we made the key to create our shared memory blocks, we used ftok with two arguments: the path an existing file in the filesystem, and a 'project id'. for the project id, we used the index of the array we looped over to fork multiple children.

we can use the exact same strategy to recreate the keys for reading. as long as we input the same two arguments into ftok, we get the same value back.

opening the shared memory

we open the shared memory block for reading almost exactly the same way as we did above for writing. the only difference is the mode.

for reading, we use the a mode. this stands for 'access', and gives us a read-only pointer to our shared memory block.

reading from the shared memory block

once we have a pointer to our shared memory block, we can read from it using shmop_read.

shmop_read takes three arguments:

  • the pointer we got from shmop_open.
  • the offset, in bytes. since we are reading the entirety of the memory block, starting at the beginning, this is 0 in our example (and will probably be for most real-life uses, as well)
  • the number of bytes to read. in most cases, the smart thing here is to just read the entire size of the block, in our example 1024 bytes.

the return type is a string. if there are errors reading, we get a boolean false.

deleting shared memory blocks

once we are done reading our shared memory, we can delete it.

this is an important step. unlike variables in our script, the memory we assigned with shmop will persist after our program has exited, hogging resources. we do not want to litter our system with blocks of unused, reserved memory, piling up higher and higher with each successive run of our script!

freeing up shared memory blocks is done with shmop_delete. this function takes one argument: the pointer we created with shmop_open, and returns a boolean true on success.

note that shmop_delete destroys the memory block and frees up the space for other applications to use. we should only call it when we're completely done with using the memory.

handling errors

the example we've been going over doesn't really do any error handling. this is a decision borne out of a desire for brevity, not delusional optimism. in real applications we should certainly do some error testing!

we used a path to a file as an argument for ftok; we should test that it exists. shmop_write will throw a value error if our memory block is opened read-only or we overwrite its size. that should be handled. if there's a problem reading data, shmop_read will return false. test for that.

php: concurrency with processes. pt. interprocess communication with shmop
i am asking you to do some error handling

fixing 'already exists' errors with shmop_open

if we open a shared memory block and then the script terminates before we call shmop_delete, the memory block still exists. if we then try to open that memory block again with shmop_open using the n mode, we will get the error:

PHP Warning:  shmop_open(): Unable to attach or create shared memory segment "File exists" in /path/to/my/script on line <line number>
登入後複製

if our script is well-designed, this shouldn't happen. but, while developing and testing we may create these orphaned memory blocks. let's go over how to delete them.

the first step is to get the key of the memory block as a hex number. we do this by calling ftok as normal, and then converting the returned integer from base ten to base-16 like so:

$shm_key = ftok($shmop_file, $i);
$shm_key_hex = "0x".base_convert($shm_key, 10, 16);
登入後複製

we do this because linux comes with a number of 'interprocess communication' tools that we can use to manage shared memory blocks, and they all use hexadecimal numbers for their keys.

the first command line tool we'll use is ipcs. we're going to use this to confirm that the shared memory block we want to delete does, in fact, exist.

the ipcs command, when run without arguments, will output all interprocess communication channels, including all shared memory blocks. we'll narrow down that output by using grep with the hexadecimal key we created above. for instance, if our shared memory block's key in hexadecimal is 0x33010024, we could do this:

ipcs | grep "0x33010024"
登入後複製

if we get a line of output, the memory block exists. if nothing is returned, it does not.

once we've confirm that a shared memory block exists, we can remove it with ipcrm

ipcrm --shmem-key 0x33010024
登入後複製

knowing how to inspect and clean up (without resorting to a restart) shared memory allows us to develop and experiment without turning our ram into a ghost town of abandoned blocks.

wrapping up

achieving concurrency in php using fork and shared memory does take some effort and knowledge (and the official manual is scant help). but it does work and, if you've made it through this article and the first installment on pcntl_fork, you should have a good base from which to start.

? this post originally appeared in the grant horwood technical blog

以上是php:與進程的並發。角與 shmop 的進程間通信的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!