本文介绍的Perl进程间数据共享内容主体来自于《Pro Perl》的第21章。编程
经过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的一些函数,经常须要导入一些IPC::SysV
模块中的常量,所以不少使用IPC的程序中,都会:ui
use IPC::SysV:
它里面定义了不少常量,完整的可参见perldoc -m IPC::SysV
,下面是一些常见的常量:操作系统
曾经消息队列是进程间通讯的惟一有效方式,它就像管道同样,一端写入一端读取。对于消息队列而言,咱们写入和读取的数据都称之为消息(message)。线程
能够建立两种类型的消息队列:私有的(private)和公有的(public)scala
例如,建立一个私有的消息队列。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对象都是持久化在内存中的)。
信号量也称为信号灯,典故来源于荷兰:火车根据旗标来决定是否通行。就像红灯停、绿灯行的意思,红绿灯就是信号灯,车子就是被阻塞或通行的进程/线程。
在编程语言中,信号量是一个整数值,若是是正数,表示有多少个信号量,也表示可以使用的信号灯数量,也能够是0或负数。信号量要结合PV操做(两个原语)才能真正起做用,P是减一个信号灯操做,V是加一个信号灯操做。
信号量的规则是这样的:
总结起来很简单:若是当前没有信号灯资源(小于或等于0),那么请求信号灯(P原语)的进程就会被阻塞;若是有信号灯资源(大于0),就直接放行。若是一个进程原本就是来增长信号灯资源的(V原语),那么这个进程固然要放行,由于添加了一个信号灯,那么还能够拥有唤醒一个被阻塞进程的能力(若是有被阻塞进程的话)。
若是限制只使用1个信号灯,那么信号量就实现了锁的机制:P是申请锁操做,只有在有值为1的时候才能申请锁,不然被阻塞;V是释放锁,一直被放行。
其实,只要把信号灯理解为一种有限的资源就很容易理解信号量的机制。
固然,具体到信号量的实现上就不必定遵照上面的操做,例如能够一次加N或一次减N,而不是以1做为操做单位。
对于Sys V IPC中信号量来讲:
IPC_NOWAIT
和SEM_UNDO
IPC_NOWAIT
:不阻塞,而是当即返回并设置操做信息为EAGAIN(对于Perl来讲设置$!
)SEM_UNDO
:以该flag执行op时,在进程退出时(不管是正常仍是异常退出)自动归还信号灯。例如已有信号灯10,以undo方式执行减二、加3,最后信号灯数量为11,当退出时反向操做,又变回10。使用sem_undo
能够有效避免进程异常时永久锁住资源不释放的问题多路信号量的模式以下图所示:
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::Msg
和IPC::Semaphore
对应的是IPC::SharedMem
,可是它们都太底层了。对于共享内存来讲,Perl中有更高层次的IPC::Shareable
模块,使得共享内存操做更加方便,它的实现使用了tie
机制,能够简单地附加(attach)一个变量到共享内存段上并轻松地访问它。但可能须要先安装它:
> cpan IPC::Shareable
tie
方法有4个参数:
IPC::Shareable
例如,下面的代码中建立并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::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