PHP多進程初探 --- 進程間通訊二三事

[原文地址:https://blog.ti-node.com/blog...]php

每每開啓多進程的目的是爲了一塊兒幹活加速效率,前面說了不一樣進程之間的內存空間都是相互隔離的,也就說進程A是沒法讀或寫進程B中的任何數據內容的,反之亦然。可是,有些時候,多個進程之間必需要有相互通知的機制,用職場上的話來講就叫「及時溝通」。你們都在一塊兒作同一件事情的不一樣部分,彼此之間「及時溝通」是很重要的。node

因而進程間通訊就誕生了,英文縮寫IPC,全稱InterProcess Communication。
常見的進程間通訊方式有:管道(分無名和有名兩種)、消息隊列、信號量、共享內存和socket,最後一種方式今天不提,放到後面的php socket編程中去說,重點說前四種方式。linux

管道是*NIX上常見的一個東西,你們平時使用linux的時候也都在用,簡單理解就是|,好比ps -aux|grep php這就是管道,大概意思相似於ps進程和grep進程兩個進程之間用|完成了通訊。管道是一種半雙工(如今也有系統已經支持全雙工的管道)的工做方式,也就是說數據只能沿着管道的一個方向進行傳遞,不能夠在同一個管道上反向傳數據。管道分爲兩種,一種叫作未命名的管道,另外一種叫作命名管道,未命名管道只能在擁有公共祖先的兩個進程之間使用,簡單理解就是隻能用於父進程和和其子進程之間的通訊,可是命名管道則能夠用於任何兩個毫無關連的進程之間的通訊(一下子將要演示的將是這種命名管道)。編程

須要特殊指出的是消息隊列、信號量和共享內存這三種IPC同屬於XSI IPC(XSI能夠認爲是POSIX標準的超集,簡單粗暴理解爲C++之於C)。這三種IPC在*NIX中通常都有兩個「名字」來爲其命名,一個叫作標誌符,一個叫作鍵(key)。標誌符是一個非負整數,每當一個IPC結構被建立而後又被銷燬後,標誌符便會+1一直加到整數的最大整數數值,然後又從0開始從新計算。既然是爲了多進程通訊使用,那麼多進程在使用XSI IPC的時候就須要使用一個名字來找到相應的IPC,而後才能對其進行讀寫(術語叫作多個進程在同一個IPC結構上匯聚),因此POSIX建議是不管什麼時候建立一個IPC結構,都應指定一個鍵(key)與之關聯。一句話總結就是:標誌符是XSI IPC的內部名稱,鍵(key)是XSI IPC的外部名稱。服務器

使多個進程在XSI IPC上匯聚的方法大概有以下三種:網絡

  • 使用指定鍵IPC_PRIVATE來建立一個IPC結構,而後將返回的標誌符保存到一個文件中,而後進程之間經過讀取這個文件中的標誌符進行通訊。使用公共的頭文件。這麼作的缺點是多了IO操做。
  • 將共同認同的鍵寫入到公共頭文件中。這麼作的缺點這個鍵可能已經與一個IPCi結構關聯,這樣在使用這個鍵建立結構的時候就可能會出錯,而後必須刪除已有的IPC結構再從新建立。
  • 認同一個文件路徑名和項目ID,而後使用ftok將這兩個參數轉換成一個鍵。這將是咱們使用的方式。

XSI IPC結構有一個與之對應的權限結構,叫作ipc_perm,這個結構中定義了IPC結構的建立者、擁有者等。數據結構

多進程通訊之一:命名管道。 在php中,建立一個管道的函數叫作posix_mkfifo(),管道建立完成後其實就是一個文件,而後就能夠用任何與讀寫文件相關的函數對其進行操做了,代碼大概演示一下:socket

<?php
// 管道文件絕對路徑
$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';
// 若是這個文件存在,那麼使用posix_mkfifo()的時候是返回false,不然,成功返回true
if( !file_exists( $pipe_file ) ){
  if( !posix_mkfifo( $pipe_file, 0666 ) ){
    exit( 'create pipe error.'.PHP_EOL );
  }
}
// fork出一個子進程
$pid = pcntl_fork();
if( $pid < 0 ){
  exit( 'fork error'.PHP_EOL );
} else if( 0 == $pid ) {
  // 在子進程中
  // 打開命名管道,並寫入一段文本
  $file = fopen( $pipe_file, "w" );
  fwrite( $file, "helo world." );
  exit;
} else if( $pid > 0 ) {
  // 在父進程中
  // 打開命名管道,而後讀取文本
  $file = fopen( $pipe_file, "r" );
  // 注意此處fread會被阻塞
  $content = fread( $file, 1024 );
  echo $content.PHP_EOL;
  // 注意此處再次阻塞,等待回收子進程,避免殭屍進程
  pcntl_wait( $status );
}

運行結果以下:
函數

多進程通訊之二:消息隊列。這個怕是不少人都聽過,不過印象每每停留在kafka、rabbitmq之類的用於服務器解耦網絡消息隊列軟件上。消息隊列是消息的連接表(一種常見的數據結構),可是這種消息隊列存儲於系統內核中(不是用戶態),通常咱們外部程序使用一個key來對消息隊列進行讀寫操做。在PHP中,是經過msg_*系列函數完成消息隊列操做的。ui

<?php
// 使用ftok建立一個鍵名,注意這個函數的第二個參數「須要一個字符的字符串」
$key = ftok( __DIR__, 'a' );
// 而後使用msg_get_queue建立一個消息隊列
$queue = msg_get_queue( $key, 0666 );
// 使用msg_stat_queue函數能夠查看這個消息隊列的信息,而使用msg_set_queue函數則能夠修改這些信息
//var_dump( msg_stat_queue( $queue ) );  
// fork進程
$pid = pcntl_fork();
if( $pid < 0 ){
  exit( 'fork error'.PHP_EOL );
} else if( $pid > 0 ) {
  // 在父進程中
  // 使用msg_receive()函數獲取消息
  msg_receive( $queue, 0, $msgtype, 1024, $message );
  echo $message.PHP_EOL;
  // 用完了記得清理刪除消息隊列
  msg_remove_queue( $queue );
  pcnlt_wait( $status );
} else if( 0 == $pid ) {
  // 在子進程中
  // 向消息隊列中寫入消息
  // 使用msg_send()向消息隊列中寫入消息,具體能夠參考文檔內容
  msg_send( $queue, 1, "helloword" );
  exit;
}

運行結果以下:

可是,值得你們繼續深刻研究的是msg_send()和msg_receive()兩個函數,這兩個的每個參數都是很是值得深刻研究和嘗試的。篇幅問題,這裏就再也不詳細描述。

多進程通訊之三:信號量與共享內存。共享內存是最快是進程間通訊方式,由於n個進程之間並不須要數據複製,而是直接操控同一份數據。實際上信號量和共享內存是分不開的,要用也是搭配着用。*NIX的一些書籍中甚至不建議新手輕易使用這種進程間通訊的方式,由於這是一種極易產生死鎖的解決方案。共享內存顧名思義,就是一坨內存中的區域,可讓多個進程進行讀寫。這裏最大的問題就在於數據同步的問題,好比一個在更改數據的時候,另外一個進程不能夠讀,否則就會產生問題。因此爲了解決這個問題才引入了信號量,信號量是一個計數器,是配合共享內存使用的,通常狀況下流程以下:

  • 當前進程獲取將使用的共享內存的信號量
  • 若是信號量大於0,那麼就表示這塊兒共享資源可使用,而後進程將信號量減1
  • 若是信號量爲0,則進程進入休眠狀態一直到信號量大於0,進程喚醒開始從1

一個進程再也不使用當前共享資源狀況下,就會將信號量減1。這個地方,信號量的檢測而且減1是原子性的,也就說兩個操做必須一塊兒成功,這是由系統內核來實現的。

在php中,信號量和共享內存前後一共也就這幾個函數:

其中,sem_是信號量相關函數,shm_是共享內存相關函數。

<?php
// sem key
$sem_key = ftok( __FILE__, 'b' );
$sem_id = sem_get( $sem_key );
// shm key
$shm_key = ftok( __FILE__, 'm' );
$shm_id = shm_attach( $shm_key, 1024, 0666 );
const SHM_VAR = 1;
$child_pid = [];
// fork 2 child process
for( $i = 1; $i <= 2; $i++ ){
  $pid = pcntl_fork();
  if( $pid < 0 ){
    exit();
  } else if( 0 == $pid ) {
    // 獲取鎖
    sem_acquire( $sem_id );
    if( shm_has_var( $shm_id, SHM_VAR ) ){
      $counter = shm_get_var( $shm_id, SHM_VAR );
      $counter += 1;
      shm_put_var( $shm_id, SHM_VAR, $counter );
    } else {
      $counter = 1;
      shm_put_var( $shm_id, SHM_VAR, $counter );
    }
    // 釋放鎖,必定要記得釋放,否則就一直會被阻鎖死
    sem_release( $sem_id );
    exit;
  } else if( $pid > 0 ) {
    $child_pid[] = $pid;
  }
}
while( !empty( $child_pid ) ){
  foreach( $child_pid as $pid_key => $pid_item ){
    pcntl_waitpid( $pid_item, $status, WNOHANG );
    unset( $child_pid[ $pid_key ] );
  }
}
// 休眠2秒鐘,2個子進程都執行完畢了
sleep( 2 );
echo '最終結果'.shm_get_var( $shm_id, SHM_VAR ).PHP_EOL;
// 記得刪除共享內存數據,刪除共享內存是有順序的,先remove後detach,順序反過來php可能會報錯
shm_remove( $shm_id );
shm_detach( $shm_id );

運行結果以下:

確切說,若是不用sem的話,上述的運行結果在必定機率下就會產生1而不是2。可是隻要加入sem,那就必定保證100%是2,絕對不會出現其餘數值。

[原文地址:https://blog.ti-node.com/blog...]

相關文章
相關標籤/搜索