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


Ch. 57-3 :SOCK_DGRAM


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

Advertisements
Tagged with: , , ,
Posted in linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archives
%d bloggers like this: