php 不是一種開發人員通常會考慮記憶體等問題的語言。我們只是圍繞變數和函數進行處理,然後讓內部為我們找出所有「記憶體的東西」。讓我們改變這一點。
在本系列的第一部分中,我們建立了一個 PHP 腳本,該腳本能夠透過分叉子進程來同時執行多個任務。它運作得很好,但有一個明顯的、未解決的問題:這些子進程無法將資料傳回父進程。
在本期中,我們將透過使用 shmop(php 的「共享記憶體操作」)來解決該問題。
當一個新進程啟動時,作業系統會為其分配一塊記憶體以供使用。進程無法讀取或寫入不屬於自己的內存,因為這將是一場安全噩夢。完全合理。
然而,在處理本系列第一部分中使用 pcntl_fork 創建的進程時,這給我們帶來了一個問題,因為這意味著子進程沒有簡單的方法可以相互通信或與父進程通信。子進程在創建時將獲得其父進程記憶體的副本,因此在 fork 之前分配的所有變數都可以訪問,但對這些變數的任何更改都將僅限於子進程。不同的記憶等等。如果我們希望子進程能夠寫入父進程可以讀取的變量,我們就會遇到問題。
對此有許多解決方案,所有這些解決方案都歸入「進程間通訊」或 iPC 的一般類別下。我們將用於 php 腳本的是「共享記憶體」。
顧名思義,共享記憶體是任意數量的進程可以存取的記憶體區塊。共享記憶體區塊由(希望)唯一的鍵來識別。任何知道密鑰是什麼的進程都可以存取該記憶體區塊。這使得子進程可以向其父進程報告;子進程將資料寫入共享記憶體區塊,退出後,父進程將讀取共享資料。這是一個優雅的解決方案。
當然,在執行此操作時,我們需要避免一些問題:我們需要確保標識共享記憶體區塊的金鑰是唯一的,並且我們需要強制共享記憶體通訊只能進行一次避免多個進程同時嘗試寫入同一個區塊並造成混亂的方法。我們將在實作中涵蓋所有這些。
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 還接受一個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,我們正在循環的陣列的索引。我們這樣做是為了讓每個子進程都有自己的共享記憶體塊來儲存其結果。請記住,如果多個進程嘗試寫入共享內存,就會發生不好的事情。在這裡使用索引可以幫助我們避免這種情況。
如果您曾經使用 php 的 fopen 和 fwrite 等工具進行過文件訪問,那麼使用 shmop 將會非常熟悉。
讓我們從使用 shmop_open 開啟共享記憶體區塊開始:
$shm_id = shmop_open($shm_key, 'n', 0755, 1024);
這個函數有四個參數:
這裡的一個重要注意事項是,如果該鍵上的記憶體區塊尚不存在,則呼叫 shmop_open 將建立一個新的記憶體區塊。這與 fopen 的行為類似,但對於 shmop_open,此行為取決於我們傳遞的「模式」參數。
如範例所示,shmop_open 傳回一個可用於存取的指標:讀取或寫入,取決於用於開啟記憶體區塊的模式。
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.
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.
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:
shmop_write returns, as an integer, the number of bytes written.
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.
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:
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); }
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.
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.
once we have a pointer to our shared memory block, we can read from it using shmop_read.
shmop_read takes three arguments:
the return type is a string. if there are errors reading, we get a boolean false.
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.
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.
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.
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中文網其他相關文章!