Perl進程間數據共享

本文介紹的Perl進程間數據共享內容主體來自於《Pro Perl》的第21章。編程

IPC簡介

經過fork建立多個子進程時,進程間的數據共享是個大問題,要麼創建一個進程間通訊的通道,要麼找到一個兩進程都引用的共享變量。本文將介紹Unix IPC的近親System V IPC:message queues(消息隊列)、semaphores(信號量)和shared memory-segments(共享內存段)。它們都是IPC結構,它們被很是普遍地應用於進程間通訊。它們的幫助文檔可參見:數組

$ perldoc IPC::Msg
$ perldoc IPC::Semaphore
$ perldoc IPC::SharedMem

可是,並不是全部操做系統都支持System V IPC,對於那些不遵照POSIX規範的平臺就不支持。固然,也並不是必定要在Unix操做系統上才能使用IPC,只要操做系統支持IPC就能夠,並且就算是Unix系統上也並不是必定支持IPC,可使用ipcs命令來查看是否支持:數據結構

$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

message queues、semaphores和shared memory segments的共同點在於它們的數據都持久存在於內存中,且只要知道資源的ID且有權限訪問,就能夠被任意一個進程(因此隨意多個進程)訪問到,不只如此,還能夠被其它程序訪問到(例如兩個perl程序文件)。因爲數據在內存中持久,且數據資源進行了ID標識,這可使得程序退出前保存狀態,而後重啓時再次獲取到原來的狀態。編程語言

嚴格地說,Perl支持的IPC在於能夠調用一些IPC函數:msgctl、msgget、msgrcv、msgsnd、semctl、semget、semop、shmctl、shmget、shmread、shmwrite。它們幾乎是對C對應函數的封裝,很是底層。儘管這些函數的文檔很是豐富,但這些函數並不容易使用,IPC::家族的模塊提供了面向對象的IPC功能支持。函數

IPC::SysV模塊

要使用IPC的一些函數,經常須要導入一些IPC::SysV模塊中的常量,所以不少使用IPC的程序中,都會:ui

use IPC::SysV:

它裏面定義了不少常量,完整的可參見perldoc -m IPC::SysV,下面是一些常見的常量:操作系統

 

Message Queue(消息隊列)

曾經消息隊列是進程間通訊的惟一有效方式,它就像管道同樣,一端寫入一端讀取。對於消息隊列而言,咱們寫入和讀取的數據都稱之爲消息(message)。線程

能夠建立兩種類型的消息隊列:私有的(private)和公有的(public)scala

  • 私有隊列只對建立它的進程和它的子進程能夠訪問,固然還能夠經過權限控制的方式來改變訪問權限
  • 公有隊列只有有權限,且知道資源ID的進程能夠訪問

例如,建立一個私有的消息隊列。code

use IPC::SysV qw(IPC_PRIVATE IPC_CREAT S_IRWXU);
use IPC::Msg;

my $queue = IPC::Msg->new IPC_PRIVATE S_IRWXU | IPC_CREAT;

IPC_Msg的構造函數new有兩個參數,第一個是要建立的消息隊列的資源ID,在IPC SysV中常稱爲KEY,對於私有隊列來講,KEY須要指定爲IPC_PRIVATE,第二個參數是訪問該隊列的權限,S_IRWXU表示隊列的全部者(U)能夠對該隊列進行讀(R)、寫(W)、執行(X)操做。此處還配合了IPC_CREAT,表示若是隊列不存在就建立新隊列。

權限部分也能夠寫成數值格式的:

my $queue = IPC::Msg->new IPC_PRIVATE 0700 | IPC_CREAT;

因此,這裏建立的私有隊列只有建立者進程和子進程能夠執行讀寫執行的操做。

若是想要建立一個公有隊列,須要爲該公有隊列提供一個資源ID,資源ID是一個數值。下例中給的資源ID是10023,權限是0722,表示建立隊列的進程擁有讀寫執行操做,而資源所在組或其它用戶進程只能寫隊列。

my $q = IPC::Msg->new 10023, 0722 | IPC_CREAT;

若是其它進程想要訪問這個公有隊列,只需經過new方法指定這個公有隊列的KEY便可即表示構建這個已有的隊列,不要指定IPC_CREAT修飾符,不然表示建立動做(儘管IPC結構存在時不會建立,但他表明了建立這個動做,而非訪問動做)。若是要獲取的公有隊列不存在,則返回undef。以下:

my $q = IPC::Msg->new 10023, 0200;

而對於私有隊列,想要知道它的KEY,可使用id()方法:

$KEY = $queue->id

發送和接收消息隊列

有了消息隊列的對象結構以後,就能夠操做這個消息隊列,好比發送消息,接收消息等。相關文檔參見man msgsnd

向隊列發送消息和從隊列中接收消息的方式爲:

$queue->snd($type, $wr_msg, [ $flags ]);
$queue->rcv(\$rd_msg, $length, $type, [ $flags ]);
  • $wr_msg爲想要發送的消息
  • $rd_msg是從消息隊列中讀取消息保存到哪一個標量變量中
  • $type是一個正整數,表示消息隊列的類型,可在rcv方法中指定這個正整數表示選擇接收哪一個數值類型的消息
  • $length表示消息隊列中最多容許接收多少條消息。若是消息長度大於該值,則rcv返回undef,而且$!設置爲E2BIG
  • $flags是可選的,若是設置爲IPC_NOWAIT,則這兩個方法不會阻塞,而是當即返回($!設置爲EAGAIN)

關於rcv type和flag的規則,參考以下解釋。

rcv Type的解釋:
整數值             意義
-----------------------------
 0           rcv老是讀取隊列的第一條消息,無視type

 >0          rcv老是讀取該類型的第一條消息。例如,
             type=2,則只讀取type=2的消息,若是不存在,
             則一直阻塞直到有type=2的消息。可是能夠設
             置IPC_NOWAIT和MSG_EXCEPT常量改變這種模式

 <0          rcv讀取類型不大於type絕對值(從小到大)的第
             一條消息。不嚴謹,但可看示例描述:若是rcv的
             type=-2,則首先讀取type=0的第一條消息,如
             果不存在type=0的消息,則繼續讀取type=1的第
             一條消息,不存在則繼續讀取type=2的第一條消息
flag的解釋:
flag值              意義
------------------------------
MSG_EXCEPT         rcv讀取第一條非type值的消息。例如,rcv
                   的type=1,則讀取第一條type不爲1的消息

MSG_NOERROR        容許消息過長超過$length時截斷超出的部分,
                   而不是在這種狀況下返回E2BIG錯誤  

IPC_NOWAIT         rcv在請求的消息類型不存在時不要阻塞等待,
                   而是當即返回,且設置$!的值爲EAGAIN

將上面的解釋合併起來,很明確的意思是咱們能夠經過設置不一樣的type來實現多級通訊的消息隊列,這一切都交給咱們本身來決定,例如對不一樣子進程或線程發送不一樣的消息。

獲取和設置消息隊列的屬性

可使用set方法來修改消息隊列的權限,它須要一個key-value格式的參數。

例如:

$queue->set(
    uid => $user_id,           # chown
    gid => $group_id,          # chgrp
    mode => $perm,             # 8進制權限位或S_格式的權限
    qbytes => $queue_size,     # 隊列最大容量(capacity)
);

另外,可使用stat方法獲取隊列的屬性對象,經過這個屬性對象,能夠直接修改隊列的對應屬性。只是須要注意的是,當經過stat對象更改屬性時,不會當即應用到消息隊列上生效,只有經過set方法設置後,設置纔會當即生效。

my $stat = $queue->stat;
$stat->mode(0722);
$queue->set($stat);

最後,若是擁有隊列的執行權限,能夠經過remove方法銷燬這個隊列:

$queue->remove;

若是沒法刪除隊列,則remove返回undef,並設置$!。其實刪除隊列挺重要的,由於若是程序退出,隊列可能會繼續保留在內存中(前文已經說過了,IPC對象都是持久化在內存中的)。

信號量(semaphore)

信號量也稱爲信號燈,典故來源於荷蘭:火車根據旗標來決定是否通行。就像紅燈停、綠燈行的意思,紅綠燈就是信號燈,車子就是被阻塞或通行的進程/線程。

在編程語言中,信號量是一個整數值,若是是正數,表示有多少個信號量,也表示可以使用的信號燈數量,也能夠是0或負數。信號量要結合PV操做(兩個原語)才能真正起做用,P是減一個信號燈操做,V是加一個信號燈操做

信號量的規則是這樣的:

  • 若是一個進程請求P操做(減1操做,即請求一個信號燈),若是減去以後爲負數,則該進程被阻塞,若是減去以後爲0或正數,則放行該進程
  • 若是一個進程請求V操做(加1操做,即釋放或增長一個信號燈),進程直接放行
  • 若是請求V操做,若是加1以後仍爲0或負數,則放行該進程的同時還喚醒另外一個被阻塞的進程。若是加1後爲正數,則直接添加一個信號燈資源

總結起來很簡單:若是當前沒有信號燈資源(小於或等於0),那麼請求信號燈(P原語)的進程就會被阻塞;若是有信號燈資源(大於0),就直接放行。若是一個進程原本就是來增長信號燈資源的(V原語),那麼這個進程固然要放行,由於添加了一個信號燈,那麼還能夠擁有喚醒一個被阻塞進程的能力(若是有被阻塞進程的話)。

若是限制只使用1個信號燈,那麼信號量就實現了鎖的機制:P是申請鎖操做,只有在有值爲1的時候才能申請鎖,不然被阻塞;V是釋放鎖,一直被放行

其實,只要把信號燈理解爲一種有限的資源就很容易理解信號量的機制。

固然,具體到信號量的實現上就不必定遵照上面的操做,例如能夠一次加N或一次減N,而不是以1做爲操做單位。

對於Sys V IPC中信號量來講

  1. 每個信號量結構(或信號量對象,經過KEY來標識)能夠有多路信號量,每一路信號量經過信號量序號semnum標識分類。信號量序號從0開始,每一號的信號量上都是互相獨立的,都有本身的信號量值(信號燈的數量,semval)以及控制進程是否阻塞的信號量操做,操做0號信號量不會影響1號信號量
  2. 使用semop函數來操做某個信號量結構,semop函數能夠一次性操做多路信號量,每一路信號量都要求3個值:sem_num, sem_op, flag
    • sem_num:指定要操做哪路信號量
    • sem_op:是一個整數值,用來表示信號量的操做模式,能夠是0、正數、負數
      • 正數N:表示增長N個信號燈資源
      • 0:表示等待信號燈的數量爲0,等待過程當中一直阻塞
      • 負數-N:表示等待信號燈的數量大於等於N,等待過程當中一直阻塞
    • flag:可被信號量識別的flag除了0外只有兩個:IPC_NOWAITSEM_UNDO
      • 0:若是該op不能成功,則一直等待(阻塞)直到能夠成功
      • IPC_NOWAIT:不阻塞,而是當即返回並設置操做信息爲EAGAIN(對於Perl來講設置$!
      • SEM_UNDO:以該flag執行op時,在進程退出時(不管是正常仍是異常退出)自動歸還信號燈。例如已有信號燈10,以undo方式執行減二、加3,最後信號燈數量爲11,當退出時反向操做,又變回10。使用sem_undo能夠有效避免進程異常時永久鎖住資源不釋放的問題
  3. 因爲減法操做"-N"在減的過程當中一直沒有減下去,而是一直阻塞,因此不會出現負數信號燈,而是以等待加法操做的進程數來衡量,這和前面的信號燈機制是不同的
  4. semop的操做是原子的,要麼多路信號量所有操做成功,要麼所有失敗

多路信號量的模式以下圖所示:

SysV IPC經過這樣的信號量規則,可讓進程之間進行協做,例如一個進程能夠經過設置信號量的值來控制另外一個進程是執行仍是阻塞,不只如此,還能夠控制進程間的共享資源。這是一個很是大的話題,這裏給個簡單的信號量控制進程協做的示例來解釋進程間如何訪問共享資源。

1.進程A建立一個信號量,其值爲1,並建立一個共享資源(如一個文件或IPC共享內存段)。因爲這個資源可能涉及到不少初始化,因此如今不當即訪問這個資源
2.進程B啓動,將信號量的值減爲0(即獲取鎖),而後訪問共享資源
3.進程A如今嘗試減小信號量的值(即申請鎖)並訪問共享資源,因爲當前信號量的值爲0,不足以完成減法,因此進程A被阻塞
4.進程B完成了共享資源的訪問,並增長信號量(釋放鎖)的值,這個操做是必定會成功的
5.進程A如今能夠執行減法操做(獲取鎖)了,由於信號量的值已經變成了1,因而可以訪問共享資源
6.進程B嘗試第二次訪問共享資源,但它會被阻塞,由於信號量的值被進程A減爲0
7.進程A完成共享資源的訪問,並增長信號量的值
8.進程B訪問共享資源並減小信號量

儘管共享資源和信號量沒有直接的關聯關係,可是信號量在這裏充當了看門狗,只要想訪問共享資源,都須要從信號量這裏獲取訪問權限。

從上面的操做上能夠發現,減法操做和加法操做順序必須不能錯(先減後加,即PV),並且減法、執行和加法的操做必須在同一個臨界區內執行,便是一個原子操做

建立信號量

建立信號量須要經過IPC::Semaphore模塊,固然,還須要導入IPC::SysV提供使用IPC結構時須要的常量。

一樣,Semaphore做爲一種SysV IPC結構,它也有公有和私有兩種信號量類型,且也使用KEY來標識信號量資源,使用權限來控制訪問、修改信號量。其實建立和獲取消息隊列、信號量和共享內存這3種IPC結構的方式都是一致的,都有公有私有的區別,都是用KEY來標識,都使用權限位來控制訪問能力,

例如,建立私有信號量並獲取它的id標識符:

use IPC::SysV qw(IPC_CREAT IPC_PRIVATE S_IRWXU);
use IPC::Semaphore;

my $size = 4;
my $sem = IPC::Semaphore->new $size, IPC_CREAT | S_IRWXU;

$id = $sem->id;

這裏的$size=4表示初始化該信號量對象時有4路信號量。

建立公有信號量,並設置其KEY爲10023:

my $sem = IPC::Semaphore->new 10023, 4, 0744 | IPC_CREAT;

有了信號量ID,其它進程就能夠獲取到對應的信號量結構,注意不要加上IPC_CREAT修飾符:

my $sem = IPC::Semaphore->new 10023, 0400;

操做信號量

有了信號量結構,就能夠操做這個信號量。有如下幾個常見方法:

getall    返回當前信號量對象中全部路信號量的信
          號量值(即信號燈數量)放進一個列表
          my @semval = $sem->getall;

getval    返回當前信號量對象指定序號的信號量值
          例如返回第4路信號量的信號燈數量
          my $semval = $sem->getval(3);

setall    設置當前信號量對象中全部路信號量的
          信號量值。例如清空上例建立的4路信號量
          $sem->setall( (0) x 4 );

setval    設置指定某路信號量的信號量值
          例如設置第4路信號量的信號燈爲1
          $sem->setval(3, 1);

set       設置信號量對象的UseID、GroupID以及權限
          例如 $sem->set(
              uid => $usr_id,
              gid => $grp_id,
              mode => $perm,
          );

stat      獲取當前信號量對象的stat對象,能夠經過stat
          對象簡單地修改信號量屬性。例如
          $semstat = $sem->stat;
          $semstat->mode(0744);
          $sem->set($semstat);

getpid    返回在此信號量對象上最近一次執行semop操做的進程PID
          PID that did last op

getncnt   返回等待某路信號量的值增長的進程數量
          waiting for increase
          例如 $ncnt = $sem->getncnt;

getzcnt   返回等待某路信號量的值爲0的進程數量
          waiting for zero
          例如 $zcnt = $sem->getzcnt;

op        信號量操做,見下文

對於Perl而言,有了信號量,還須要結合op方法來執行"PV"操做,規則在前面介紹SysV IPC信號量規則的時候已經介紹過了。給個示例:

$sem->op(
    0, -1, 0,
    1, 1, 0,
    3, 0, 0,
);

op能夠一次性操做某信號量對象的多路信號量,每一路信號量由3個元素組成一個小列表,例如上面的0, -1, 0表示操做第一路信號量,其中第一個元素表示sem_num,即第幾路信號量,-1是sem_op,值爲-1表示要等待信號量的值至少爲1以後纔不會阻塞,最後一個元素0表示flag(flag的解釋見前文)。

例如,想要使用信號量來實現鎖機制,鎖機制只需一路信號量且一個信號燈便可:

sub access_resource {
    # 訪問資源,執行減法操做來獲取鎖,若是已經爲0,則本身被阻塞
    $sem->op(0, -1, 0);

    ... 訪問資源 ...

    # 訪問完成,執行加法操做來釋放鎖
    $sem->op(0, 1, 0);
}

最後,信號量和消息隊列相似,都應該在不須要的時候清空它(好比最後一個進程退出且肯定再也不使用它的時候)。

共享內存段

Shared Memory Segments是IPC的第三種結構,和IPC::MsgIPC::Semaphore對應的是IPC::SharedMem,可是它們都太底層了。對於共享內存來講,Perl中有更高層次的IPC::Shareable模塊,使得共享內存操做更加方便,它的實現使用了tie機制,能夠簡單地附加(attach)一個變量到共享內存段上並輕鬆地訪問它。但可能須要先安裝它:

> cpan IPC::Shareable

tie方法有4個參數:

  • (1).一個待附加的變量(變量、數組、hash等,但它們中能夠有更復雜的數據結構,如引用)
  • (2).IPC::Shareable
  • (3).一個IPC結構的key,key能夠是一個數值或字符串,但最多隻能是4個字符,超出的字符將忽略,因此abcd和abcde表明的是同一個key
  • (4).Options,它是可選的hash引用,該hash引用中包含了一個或多個key/value對,稍後解釋

例如,下面的代碼中建立並tie了一個Hash變量(local_hash)到共享內存段上。

use IPC::SysV;
use IPC::Shareable;

our %local_hash;
tie %local_hash, "IPC::Shareable", "mykey", {create => 1};

$local_hash{hashkey} = "hashvalue";

tie一個共享內存段後,在該tied變量之下會有一個tie對象,它能夠經過tie的返回值或tied()函數來獲取。下面兩種獲取tie對象的方式是等價的:

$mytie = tie $sv, 'IPC::Shareable', 'mykey', {...};
$mytie = tied $sv;

下面是關於tie方法第四個參數Options的說明,它是一個hash引用,該hash中可定義的key包括以及它們的默認值爲:

{
    key       => IPC_PRIVATE,
    create    => 0,
    exclusive => 0,
    destroy   => 0,
    mode      => 0666,
    size      => IPC::Shareable::SHM_BUFSIZ(),
}

一個個解釋這6個key,有些布爾值類型的,只需提供Perl中的false值或true值便可,例如數值的0和空字符串均可以表示false。

key
在前面介紹tie方法時解釋了有4個參數,可是其實能夠將第三個參數KEY放進這個hash引用中,並使用key來指定KEY。例如使用3個參數建立一個共享內存變量:
tie %myhash, "IPC::Shareable", {key => "mykey"};
默認值爲IPC_PRIVATE,其它進程沒法訪問該共享內存變量

create
當key不存在時就建立,默認爲false,表示不會嘗試建立key,因此必需要求key是已經存在的,不然將失敗並返回undef

exclusive
若是key存在時,則不建立,而且失敗返回undef。默認值爲false,這時即便key已存在也不會失敗  

mode
八進制的權限位,控制key被建立時的權限。例如0666表示對owner、group、other均可讀、可寫,而0600表示只對owner可讀可寫。默認值爲0666

destroy
設置爲true時,當調用tie的進程退出時將自動銷燬該tie建立的共享內存段。默認值爲false

size
指定共享內存段分配的大小,默認值爲IPC::Shareable::SHM_BUFSIZ(),默認值爲65536字節

關於IPC::Shareable的鎖機制

IPC::Shareable提供了程序級別的鎖機制,它直接拷貝了IPC::ShareLite中的鎖機制,但它們的底層是使用IPC::Semaphore實現的,因此若是想要實現本身的鎖機制,能夠直接使用IPC::Semaphore。能夠直接調用shlock()shunlock()方法來獲取鎖和釋放鎖。但在使用它們以前,須要先獲取到tie對象,前面說過如何獲取tie對象。

例如,下面兩種加鎖方式是等價的:

$mytie = tie $sv, 'IPC::Shareable', 'mykey', {...};
...
$mytie->shlock;


tie $sv, 'IPC::Shareable', 'mykey', {...};
...
(tied $sv)->shlock;

IPC::Shareable提供了獨佔鎖(LOCK_EX)、共享鎖(LOCK_SH)、非阻塞鎖(LOCK_NB,沒法獲取鎖的時候當即返回0)三種鎖,只要將它們做爲shlock方法的參數便可申請對應模式的鎖。多個共享鎖能夠共存,但獨佔鎖和獨佔鎖、共享鎖都互斥。此外,還能夠爲shlock()提供LOCK_UN參數來實現shunlock(),它們是等價的。若是shlock()不提供任何參數,則默認爲LOCK_EX。

在使用這幾個常量以前,須要先導入(all或lock或flock標籤均可以)。例如:

use IPC::Shareable qw(:all);

if ( (tied $sv)->shlock(LOCK_SH | LOCK_NB) ){
    print "The value is $sv\n";
    (tied $sv)->shlock(LOCK_UN);   # (tied $sv)->shunlock;
} else {
    print "Another process has an exclusive lock now\n";
}

上面的示例中結合了共享鎖和非阻塞鎖,其實獨佔鎖和共享鎖均可以結合非阻塞鎖:

shlock(LOCK_EX | LOCK_NB)   # 獲取獨佔鎖失敗時當即返回0,表示資源已鎖定
shlock(LOCK_SH | LOCK_NB)   # 獲取共享鎖失敗時當即返回0,表示資源已被獨佔鎖鎖定

清除共享內存段

tie的第四個參數中,能夠設置destory選項,該選項使得調用tie的進程退出時自動刪除對應的tie對象以及其對應的共享內存段。

除了destory,還有remove、clean_up和clean_up_all能夠用來移除共享內存段。

(tied $sv)->remove;
IPC::Shareable->clean_up;
IPC::Shareable->clean_up_all;

remove方法能夠刪除tie對象對應的共享內存段。無視destory選項的設置,無視是哪一個進程。

clean_up是一個類方法,只刪除調用該方法的進程所建立的共享內存段。非該進程所建立的,clean_up不會刪除。

clean_up_all移除該進程所能看見的全部共享內存段,而不限於該建立者進程。

若是要清除共享內存段、消息隊列、信號量,可使用ipcrm命令。

共享內存段示例

在server.pl文件中:

#!usr/bin/perl -w

use strict;
use IPC::Shareable;

my $key = 'data';
my %options = (
        create => 1,
        exclusive => 1,
        mode => 0644,
        destory => 1,
);

my %colors;
tie %colors, 'IPC::Shareable', $key, { %options } 
    or die "Sever: tied failed";

%colors = (
        red => [
                'fire truck',
                'leaves in the fall',
        ],
        blue => [
                'sky',
                'police cars',
        ],
);

((print "Server: there are 2 colors\n"), sleep 2) while scalar keys %colors == 2;

print "Server: here are all my colors:\n";
foreach my $c (keys %colors){
        print "Server: these are $c: ",
                join(', ', @{$colors{$c}}), "\n";
}

exit;

在client.pl文件中:

#!/usr/bin/perl -w
#
use strict;

use IPC::Shareable;

my $key = 'data';

my %options = (
        create => 0,      # 不建立,直接獲取data
        exclusive => 0, 
        mode => 0644, 
        destory => 0,
);

my %colors;

tie %colors, "IPC::Shareable", $key, { %options } or
        die "Client: tied failed\n";

foreach my $c (keys %colors){
        print "Client: these are $c: ",
                join(', ', @{$colors{$c}}), "\n";
}

delete $colors{'red'};  # 刪除一個key/value
exit;

邏輯很簡單,只爲了證實不一樣進程間能夠獲取同一個共享數據段,且進程退出以後數據還可以繼續保留在共享內存中。

執行它們:

$ perl server.pl &
$ perl client.pl  # 將輸出
$ ipcs
相關文章
相關標籤/搜索