親子関係のないプロセス間でファイルディスクリプタを共有する

UNIXLinux でプロセス間でファイルディスクリプタを共有するには、ファイルディスクリプタを作成後に fork して親子間で共有するという方法が良く取られますが、UNIXドメインソケットを使って、親子関係のないプロセス間でファイルディスクリプタを受け渡すことができます。

C だといろいろ面倒なのですが、Ruby だと UNIXSocket#send_io#recv_io を使って簡単に実現できます。

sender.rb

require 'socket'

File.unlink 'socket' rescue nil
sock = UNIXServer.new 'socket'
s = sock.accept
f = File.open ARGV.first
s.send_io f

receiver.rb

require 'socket'

sock = UNIXSocket.new 'socket'
f = sock.recv_io
puts f.read

sender.rb は UNIXドメインソケットをオープンして接続を待ち、引数で指定されたファイルをオープンしてそのファイルディスクリプタをクライアントに渡して終了します。

receiver.rb は sender.rb から受け取ったファイルディスクリプタからデータを読み込んで出力します。

ある端末で:

% seq 10 > data
% ruby sender.rb data

別の端末で:

% ruby receiver.rb
1
2
3
4
5
6
7
8
9
10

sender.rb が開いた data ファイルの内容を receiver.rb が読み込むことができました。

sender.rb でファイルディスクリプタを渡す前に少し読み込んでみます。

sender.rb

require 'socket'

File.unlink 'socket' rescue nil
sock = UNIXServer.new 'socket'
s = sock.accept
f = File.open ARGV.first
puts f.sysread(6)
s.send_io f
% ruby sender.rb data
1
2
3
% ruby receiver.rb
4
5
6
7
8
9
10

渡してるのはファイルディスクリプタなので、ちゃんとファイル内の位置も引き継がれてます。

同じシステム内で通常のファイルを渡してもあまり面白くありません。 実用的な用途は、接続済みのソケットとか、相手にアクセス権のないファイルを開いて渡すとかでしょうか。

接続済みのTCPソケットを渡してみます。

sender.rb

require 'socket'

File.unlink 'socket' rescue nil
sock = UNIXServer.new 'socket'
s = sock.accept
f = TCPSocket.new ARGV[0], ARGV[1]
s.send_io f

receiver.rb

require 'socket'

sock = UNIXSocket.new 'socket'
f = sock.recv_io
puts f.gets
% ruby ./sender.rb localhost 25
% ruby ./receiver.rb
220 localhost ESMTP Postfix (Ubuntu)

MTA の Postfix は複数のプロセスでシステムを構成していますが、SMTP サーバーとの接続を他のプロセスで再利用するために、実際にこの仕組みを使用しています(Ruby ではなく C ですけど)。