下面是正式介绍和解释。
在C或操作系统上的fork会复制(dup)文件描述符,使得父子进程对同一文件使用不同文件描述符。但Perl的fork只会复制文件句柄而不会复制文件描述符,父子进程的不同文件句柄会共享同一个文件描述符,并使用引用计数的方式来统计有多少个文件句柄在使用这个文件描述符。
之所以复制文件句柄是因为文件句柄在Perl中是一种变量类型,在不同作用域内是互相独立的。而文件描述符对Perl来说相对更底层一些,属于操作系统的数据资源,对Perl来说是属于可以共享的数据。
也就是说,如果只fork了一次,那么父子进程的两个文件句柄都共享同一个文件描述符,都指向这个文件描述符,这个文件描述符上的引用计数为2。当父进程close关闭了该文件描述符上的一个文件句柄,子进程需要也关闭一次才是真的关闭这个文件描述符。
不仅如此,由于文件描述符是共享的,导致加在文件描述符上的锁(比如flock锁)在父子进程上看上去也是共享的。尽管只在父子某一个进程上加一把锁,但这两个进程都将持有这把锁。如果想要释放这个文件描述符上的锁,直接unlock(flock $fh, LOCK_UN)或关闭文件描述符即可。
但是注意,close()关闭的只是文件描述符上的一个文件句柄引用,在文件描述符真的被关闭之前(即所有文件句柄都被关掉),锁会一直存在于描述符上。所以,很多时候使用close去释放时的操作(之所以使用close而非unlock类操作,是因为unlock存在race condition,多个进程可能会在释放锁的同时抢到那个文件的锁),可能需要在多个进程中都执行,而使用unlock类的操作只需在父子中的任何一进程中即可释放锁。
例如,分析下面的代码中父进程三处加独占锁位置(1)、(2)、(3)对子进程中加共享锁的影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use Fcntl qw(:flock); open my $fh, ">", "a.log"; # (1) flock $fh, LOCK_EX; # 这里开始fork子进程 my $pid = fork; # (3) flock $fh, LOCK_EX; unless($pid){ # 子进程 # flock $fh, LOCK_SH; } # 父进程 # (2) flock $fh, LOCK_EX;首先分析父进程在(3)处加锁对子进程的影响。(3)是在fork后且进入子进程代码段之前运行的,也就是说父子进程都执行了一次flock加独占锁,显然只有一个进程能够加锁。但无论是谁加锁了,这个描述符上的锁对另一个进程都是共享的,也就是两个进程都持有EX锁,这似乎违背了我们对独占锁的独占性常识,但并没有,因为实际上文件描述符上只有一个锁,只不过这个锁被两个进程中的文件句柄持有了。因为子进程也持有EX锁,自己可以直接申请SH锁实现自己的锁切换,如果父进程这时还没有关闭文件句柄或解锁,它也将持有SH锁。
再看父进程中加在(1)或(2)处的独占锁,他们其实是等价的,因为在有了子进程后,无论在哪里加锁,锁(文件描述符)都是共享的,引用计数都会是2。这时子进程要获取共享锁是完全无需阻塞的,因为它自己就持有了独占锁。
也就是说,上面无论是在(1)、(2)还是(3)处加锁,在子进程中都能随意无阻塞换锁,因为子进程在换锁前已经持有了这个文件描述符上的锁。
那么上面的示例中,如何让子进程申请互斥锁的时候被阻塞?只需在子进程中打开这个文件的新文件句柄即可,它会创建一个新的文件描述符,在两个文件描述符上申请锁时会检查锁的互斥性。但是必须记住,要让子进程能成功申请到互斥锁,必须在父进程中unlock或者在父子进程中都close(),往往我们会忘记在子进程中也关闭文件句柄而导致文件描述符继续存在,其上的锁也继续保留,从而导致子进程在该文件描述符上持有的锁阻塞了自己去申请其它描述符的锁。
例如,下面在子进程中打开了新的$fh1,且父子进程都使用close()来保证文件描述符的关闭、锁的释放。当然,也可以直接在父或子进程中使用一次flock $fh, LOCK_UN来直接释放锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 use Fcntl qw(:flock); open my $fh, ">", "a.log"; # (1) flock $fh, LOCK_EX; # 这里开始fork子进程 my $pid = fork; # (3) flock $fh, LOCK_EX; unless($pid){ # 子进程 open $fh1, ">", "a.log"; close $fh; # close(1) # flock $f
