UNIXドメインソケットのアドレスの種類

Redis コア開発者 @pnoordhuis のツイートで Unix ドメインソケットに abstract socket address なるソケットアドレスがあることを知る。

ということで Unix ドメインソケットのソケットアドレスの種類を調べてみた。

ソケットアドレスの種類

Unix ドメインソケットでは大きく分けて次の3種類のアドレスで通信できる。

  1. ファイルシステムパス名(pathname)
  2. 無名(unnamed)
  3. 抽象名前空間(abstract)

1. ファイルシステムパス名

一番一般的な手法。sun_path にファイルシステム上のパスを指定する。
ファイルシステム上にファイルを作成しているので、ソケット通信の際にもファイルシステムのパーミッションなどの制約がそのままつきまとう。
サーバプロセスが終了するときには、ソケットファイルを unlink(2) するのがお作法

pathname : a UNIX domain socket can be bound to a null-terminated file system pathname using bind(2). When the address of the socket is returned by getsockname(2), getpeername(2), and accept(2), its length is offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1, and sun_path contains the null-terminated pathname.

echo サーバは次のようになる

$ cat server.py
# http://docs.python.org/library/socket.html
# Echo server program
import socket

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind('/tmp/sock')
s.listen(1)
conn, addr = s.accept()
while True:
    data = conn.recv(1024)
    if not data: break
    conn.sendall(data)
conn.close()
$ cat client.py
# http://docs.python.org/library/socket.html
# Echo client program
import socket

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('/tmp/sock')
s.sendall('Hello, world')
data = s.recv(1024)
s.close()
print 'Received', repr(data)

実行してみる

$ python server.py &
[1] 13081
$ python client.py
Received 'Hello, world'
[1]+  Done                    python server.py

$ netstat -al --protocol=unix
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node   Path
...
unix  2      [ ACC ]     STREAM     LISTENING     26035    /tmp/sock

$ sudo lsof  -U -p 13165 | head
COMMAND     PID       USER   FD   TYPE             DEVICE SIZE/OFF  NODE NAME
...
python    13165     jsmith    0u   CHR              136,2      0t0     5 /dev/pts/2
python    13165     jsmith    1u   CHR              136,2      0t0     5 /dev/pts/2
python    13165     jsmith    2u   CHR              136,2      0t0     5 /dev/pts/2
python    13165     jsmith    3u  unix 0xffff88003d031040      0t0 26035 /tmp/sock

2. 無名ソケット

調べていて初めて知った。
socketpair(2) を使うと、名前のついていない CONNECTED なソケットのペアが生成される。

unnamed : A stream socket that has not been bound to a pathname using bind(2) has no name.  Likewise, the two sockets created by socketpair(2) are unnamed.  When the address of an unnamed socket is returned by getsockname(2), getpeername(2), and accept(2), its length is sizeof(sa_family_t), and sun_path should not be inspected.

fork してファイルディスクリプターを繋ぎ直してあげれば、pipe で実現するのと同じような双方向のプロセス間通信ができる。

# http://pic.dhe.ibm.com/infocenter/aix/v6r1/index.jsp?topic=%2Fcom.ibm.aix.progcomm%2Fdoc%2Fprogcomc%2Fskt_pair_ex.htm
import os
import socket
import sys
import time

DATA1 = "In Xanadu, did Kublai Khan..."
DATA2 = "A stately pleasure dome decree..."

sockets = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
pid = os.fork()

if pid:
        # parent process
        sockets[0].close()
        buf = sockets[1].recv(1024)
        print "C2P-->%s\n"% buf
        sockets[1].sendall(DATA2)
        sockets[1].close()
else:
        # child process
        sockets[1].close()
        sockets[0].sendall(DATA1)
        buf = sockets[0].recv(1024)
        print "P2C-->%s\n"% buf
        sockets[0].close()

実行してみる

$ python socketpair.py
C2P-->In Xanadu, did Kublai Khan...

P2C-->A stately pleasure dome decree...

$ sudo lsof  -U -p 13165 | head
COMMAND     PID       USER   FD   TYPE             DEVICE SIZE/OFF  NODE NAME
...
python    13561     jsmith    0u   CHR              136,2      0t0     5 /dev/pts/2
python    13561     jsmith    1u   CHR              136,2      0t0     5 /dev/pts/2
python    13561     jsmith    2u   CHR              136,2      0t0     5 /dev/pts/2
python    13561     jsmith    4u  unix 0xffff88003d030d00      0t0 27150 socket
python    13562     jsmith    3u  unix 0xffff88003d031380      0t0 27149 socket

$ netstat -al --protocol=unix
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node   Path
...
unix  3      [ ]         STREAM     CONNECTED     27150
unix  3      [ ]         STREAM     CONNECTED     27149

I-NODE は割り振られているが、Path はブランク(無名の所以)。getsockname(2) の戻り値も同じ
socketpair で生成されたプロセスしかこのソケットと通信できない。(unnamed なのでつなぎに行けない)

3. 抽象名前空間
sun_path にファイルシステムのパスではなく、名前(文字列)を渡す。(ただし1バイト目はNULL文字にする。an abstract socket address is distinguished by the fact thatsun_path[0] is a null byte (‘¥0’))
ファイルシステムと紐付いていないので、 chroot 環境下だろうが、そもそもファイルシステムへの write 権限がなかろうが、ソケット通信できてしまう。
Linux でのみ利用可能。

Ubuntu で利用されているイベント稼働型 init デーモンの upstart でも利用されている。

abstract : an abstract socket address is distinguished by the fact that sun_path[0] is a null byte (‘¥0’).  The socket’s address in this namespace is given by the additional bytes in sun_path that are covered by the specified length of the address structure.  (Null bytes in the name have no special significance.)  The name has no connection with file system pathnames.  When the address of an abstract socket is returned by getsockname(2), getpeername(2), and accept(2), the returned addrlen is greater than sizeof(sa_family_t) (i.e., greater than 2), and the name of the socket is contained in the first (addrlen – sizeof(sa_family_t)) bytes of sun_path.  The abstract socket namespace is a nonportable Linux extension.

“The Linux Programming Interface” の Exercise 57-2 に pathname を使ったソケットプログラムを abstract namespace を使って書き直す課題があったので、Python でかいてみた。

$ cat server.py
# vim: set fileencoding=utf8
# Linux Programming Interface
# http://man7.org/tlpi/
# Listing 57-3
import socket
import sys

BUF_SIZE = 100
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
path = '\0' + '抽象名前空間'
s.bind(path)
s.listen(1)
while True:
    conn, addr = s.accept()
    while True:
        data = conn.recv(BUF_SIZE)
        if not data:
            break
        sys.stdout.write(data)
conn.close()

$ cat client.py
# vim: set fileencoding=utf8
# Linux Programming Interface
# http://man7.org/tlpi/
# Listing 57-4
import socket

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
path = '\0' + '抽象名前空間'
s.connect(path)
while True:
    try:
        msg = raw_input('')
    except EOFError:
        break
    s.sendall(msg + '\n')
s.close()

実行してみる

$ sudo lsof -p 13253
python    13253     jsmith    0u   CHR              136,2      0t0     5 /dev/pts/2
python    13253     jsmith    1u   CHR              136,2      0t0     5 /dev/pts/2
python    13253     jsmith    2u   CHR              136,2      0t0     5 /dev/pts/2
python    13253     jsmith    3u  unix 0xffff88003d031380      0t0 26603 socket

$ netstat -l --protocol=unix
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node   Path
...
unix  2      [ ACC ]     STREAM     LISTENING     26603    @抽象名前空間

abstract namespace の場合 Path に”@”マークが接頭する。
pathname の場合とことなり、プロセスを終了する際に、unlink(2) しなくても良い。

ソケットタイプの種類

SOCK_DGRAM と SOCK_STREAM の両方に対応している。(細かく言えば、 SOCK_SEQPACKET なんてというのもある。)データグラム通信はコネクションレスの性質上、信頼性がないため、ネットワーク越しの通信では特定の用途以外では使われない。一方で、Unix Domain Socket の場合、データグラムのデメリットとは無縁。

for UNIX domain sockets, datagram transmission is carried out within the kernel, and is reliable. All messages are delivered in order and unduplicated.
— Michael Kerrisk : The Linux Programming Interface §57.3 Datagram Sockets in the UNIX Domain

SOCK_DGRAM の Unix domain socket は身近なところでは syslog で利用されている。(ソケットのパスは /dev/log)。理解のため、“The Linux Programming Interface” Ch.57.2-3 にある Stream/Datagram Socket のサンプルプログラムを Python に移植してみる。

Ch. 57-2 : SOCK_STREAM

$ python us_xfr_sv.py > b &
[1] 1553
$ cat *py > a
$ python us_xfr_cl.py < a
$ kill %1
[1]+ Terminated python us_xfr_sv.py > b
$ diff -u a b
$
view raw session.txt hosted with ❤ by GitHub
# The Linux Programming Interface Listing 57-4: A simple UNIX domain stream socket client
# Python port of sockets/us_xfr_cl.c
import socket
import sys
def main():
sfd = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sv_sock_path = '/tmp/us_xfr'
try:
sfd.connect(sv_sock_path)
except socket.error:
sys.exit('connect')
while True:
try:
buff = raw_input()
except EOFError:
break
try:
sfd.send(buff + '\n')
except socket.error:
sys.exit('send')
if __name__ == '__main__':
main()
view raw us_xfr_cl.py hosted with ❤ by GitHub
# The Linux Programming Interface Listing 57-3: A simple UNIX domain stream socket server
# Python port of sockets/us_xfr_sv.c
import os
import socket
import sys
BACKLOG = 5
def main():
sfd = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sv_sock_path = '/tmp/us_xfr'
if os.path.exists(sv_sock_path):
os.remove(sv_sock_path)
try:
sfd.bind(sv_sock_path)
except socket.error:
sys.exit('bind')
try:
sfd.listen(BACKLOG)
except socket.error:
sys.exit('listen')
while True:
cfd, addr = sfd.accept()
while True:
data = cfd.recv(1024)
if not data:
break
sys.stdout.write(data)
sys.stdout.flush()
cfd.close()
if __name__ == '__main__':
main()
view raw us_xfr_sv.py hosted with ❤ by GitHub

Ch. 57-3 :SOCK_DGRAM

# The Linux Programming Interface Listing 57-6
$ python ud_ucase_sv.py &
[1] 437
$ python ud_ucase_cl.py hello world
Server received 5 bytes from /tmp/ud_ucase.452
Response 1 : HELLO
Server received 5 bytes from /tmp/ud_ucase.452
Response 2 : WORLD
$ python ud_ucase_cl.py 'long message'
Server received 10 bytes from /tmp/ud_ucase.763
Response 1 : LONG MESSA
$ kill %1
[1]+ Terminated python ud_ucase_sv.py
view raw session.txt hosted with ❤ by GitHub
# The Linux Programming Interface Listing 57-6
# Python port of sockets/ud_ucase_cl.c
import os
import socket
import sys
def main():
pid = os.getpid()
sfd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
svaddr = '/tmp/ud_ucase'
claddr = '/tmp/ud_ucase.%s'%pid
sfd.bind(claddr)
for idx, arg in enumerate(sys.argv[1:]):
msg_len = sfd.sendto(arg, svaddr)
data = sfd.recvfrom(msg_len)
print 'Response %d : %s' % (idx + 1, data[0])
os.remove(claddr)
if __name__ == '__main__':
main()
view raw ud_ucase_cl.py hosted with ❤ by GitHub
# The Linux Programming Interface Listing 57-6
# Python port of sockets/ud_ucase_sv.c
import os
import socket
import sys
BUF_SIZE = 10
def main():
sfd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
svaddr = '/tmp/ud_ucase'
if os.path.exists(svaddr):
os.remove(svaddr)
sfd.bind(svaddr)
while True:
data, claddr = sfd.recvfrom(BUF_SIZE)
print 'Server received %d bytes from %s' % (len(data), claddr)
msg_len = sfd.sendto(data.upper(), claddr)
os.remove(svaddr)
if __name__ == '__main__':
main()
view raw ud_ucase_sv.py hosted with ❤ by GitHub

MEMO

  • 3 つのアドレスのうち pathname しか知らなかったし、使ったこともなかった。
  • lsof は Unix ドメインソケットの情報を /proc/net/unix からかき集めている。
    $ head  /proc/net/unix
    Num       RefCount Protocol Flags    Type St Inode Path
    0000000000000000: 00000002 00000000 00010000 0001 01  7941 /var/run/acpid.socket
    0000000000000000: 00000002 00000000 00010000 0001 01  7733 /var/run/dbus/system_bus_socket
    0000000000000000: 00000002 00000000 00010000 0001 01  6692 @/com/ubuntu/upstart
    0000000000000000: 00000004 00000000 00000000 0002 01  7838 /dev/log
    0000000000000000: 00000002 00000000 00010000 0001 01  9236 /tmp/sock
    0000000000000000: 00000002 00000000 00010000 0005 01  6879 /run/udev/control
    0000000000000000: 00000003 00000000 00000000 0001 03  8972
    0000000000000000: 00000003 00000000 00000000 0001 03  8971
    0000000000000000: 00000002 00000000 00000000 0002 01  8508
    

References

Leave a comment