Redis コア開発者 @pnoordhuis のツイートで Unix ドメインソケットに abstract socket address なるソケットアドレスがあることを知る。
ということで Unix ドメインソケットのソケットアドレスの種類を調べてみた。
ソケットアドレスの種類
Unix ドメインソケットでは大きく分けて次の3種類のアドレスで通信できる。
- ファイルシステムパス名(pathname)
- 無名(unnamed)
- 抽象名前空間(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 | |
$ |
# 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() |
# 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() |
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 |
# 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() |
# 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() |
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
- unix(7) : unix, AF_UNIX, AF_LOCAL – Sockets for local interprocess communication
- Michael Kerrisk : The Linux Programming Interface
CH.57 : SOCKETS : UNIX DOMAIN - LWN : discussion on abstract namespace sockets
http://lwn.net/Articles/423944/