Pythonで学ぶ「詳解 UNIXプログラミング」(その12) 第12章 高度な入出力

12.1 ブロックしない大量のwrite

import os, sys, fcntl
buf  = sys.stdin.read()
print >>sys.stderr, "read {0} bytes".format(len(buf))

wrote = 0
fno = sys.stdout.fileno()
val = fcntl.fcntl(fno, fcntl.F_GETFL, 0)
fcntl.fcntl(fno, fcntl.F_SETFL, val | os.O_NONBLOCK)	# set nonblocking

while True:
    try:
        n = os.write(fno, buf)
    except OSError, e:
        print >>sys.stderr, "errno: {0}".format(e.errno)
        continue
    else:
        print >>sys.stderr, "wrote {0} bytes".format(n)
    buf = buf[n:]
    if not buf:
        break
        
fcntl.fcntl(fno, fcntl.F_SETFL, val )	# clear nonblocking

12.2 ファイル領域のロックを設定・解除する関数

import fcntl
def read_lock(fd, offset, whence, length):
    return fcntl.lockf(fd, fcntl.LOCK_SH|fcntl.LOCK_NB, length, offset, whence)

def readw_lock(fd, offset, whence, length):
    return fcntl.lockf(fd, fcntl.LOCK_SH, length, offset, whence)

def write_lock(fd, offset, whence, length):
    return fcntl.lockf(fd, fcntl.LOCK_EX|fcntl.LOCK_NB, length, offset, whence)

def writew_lock(fd, offset, whence, length):
    return fcntl.lockf(fd, fcntl.LOCK_EX, length, offset, whence)

def un_lock(fd, offset, whence, length):
    return fcntl.lockf(fd, fcntl.LOCK_UN, length, offset, whence)

領域を指定したファイルロックはfcntl.lockf()で実装されている。fcntl.lockf()ではF_RDLCKfcntl.LOCK_SHF_WDLCKfcntl.LOCK_EXとなるので注意。また、fcntl()へのコマンドは、fcntl.LOCK_NBが指定されていればF_SETLK、指定されていなければF_SETLKWとなる。

12.3 ロックの条件をテストする関数

import fcntl, struct

def lock_test(fd, type, offset, whence, length):
    lockdata = struct.pack('hhqqI', type, whence, offset, length, 0)
    ret = fcntl.fcntl(fd, fcntl.F_GETLK, lockdata)
    
    type, whence, offset, length, pid = struct.unpack('hhqqI', ret)
    if type == fcntl.F_UNLCK:
        return None		# false, region is not locked by another proc
    else:
        return pid		# true, return pid of lock owner

def is_readlock(fd, offset, whence, lenght):
    return lock_test(fd, fcntl.F_RDLCK, offset, whence, length)

def is_writelock(fd, offset, whence, lenght):
    return lock_test(fd, fcntl.F_WRLCK, offset, whence, length)

PythonではF_GETLKのインターフェースは用意されていないため、fcntl.fcntl()を用いて呼び出している。flock構造体の定義は環境によって異なるので注意が必要だ。Linuxであれば、今時LFSが入っていないということもないだろうが…

12.4 プログラム例 - デッドロック

# create a file and write two bytes to it
f = open("templock", "w", 0)
f.write("ab")
fd = f.fileno()

TELL_WAIT()
pid = os.fork()
if pid == 0: #child
    lockabyte("child", fd, 0)
    TELL_PARENT(os.getppid())
    WAIT_PARENT()
    lockabyte("child", fd, 1)

else:        # parent
    lockabyte("parent", fd, 1)
    TELL_CHILD(pid)
    WAIT_CHILD()
    lockabyte("parent", fd, 0)

12.5 複数の同じデーモンが実行することを防ぐデーモン起動時のコード

import os, errno, sys, fcntl

PIDFILE = "daemon.pid"

pidfile = open(PIDFILE, "w", 0)
fd = pidfile.fileno()

try:
    # try and set a write lock on the entire file
    write_lock(fd, 0, os.SEEK_SET, 0)
except IOError, e:
    if e.errno in (errno.EACCES, errno.EAGAIN):
        sys.exit(0)		# gracefully exit, daemon is already running

# truncate to zero length, now that we have the lock
pidfile.truncate(0)
# and write our process ID
pidfile.write("{0}\n".format(os.getpid()))

# set close-on-exit flag for descriptor
val = fcntl.fcntl(pidfile, fcntl.F_GETFD, 0)
fcntl.fcntl(pidfile, fcntl.F_GETFD, val | fcntl.FD_CLOEXEC)

# leave file open until we terminate: lock will be held

# do whatever

12.6 ファイルの終わりに対応してロックを解除する際の問題を示すプログラム

import os

out = open("temp.lock", "w+", 0)
fd = out.fileno()
for i in range(1000000):
    # lock from current EOF to EOF
    write_lock(fd, 0, os.SEEK_END, 0)
    os.write(fd, "1")
    un_lock(fd, 0, os.SEEK_END, 0)
    os.write(fd, "1")

かなり長時間このスクリプトを動かしても、エラーは発生しなかった。最近のカーネルではロックの最大数等はどう管理されているんだろう?

12.7 必須ロックを使えるかどうかを調べる

import os, fcntl, stat

out = open("temp.lock", "w+", 0)
fd = out.fileno()
out.write("abcdef")

# turn on set-group-id and turn off group-execute
st = os.fstat(fd)
os.fchmod(fd, (st.st_mode & ~stat.S_IXGRP) | stat.S_ISGID)

TELL_WAIT()

pid = os.fork()
if pid:    # parent
    # write lock entire file
    write_lock(fd, 0, os.SEEK_SET, 0)
    TELL_CHILD(pid)
    os.waitpid(pid, 0)
else:
    WAIT_PARENT()	# wait for parent to set lock
    val = fcntl.fcntl(fd, fcntl.F_GETFL, 0)
    fcntl.fcntl(fd, fcntl.F_SETFL, val | os.O_NONBLOCK)
    
    try:
        read_lock(fd, 0, os.SEEK_SET, 0)
    except IOError, e:
        print "read_lock of already-locked region returns {0}".format(e.errno)
    else:
        sys.exit("child: read lock succeeded")
    
    # now try to read the mandatory locked file
    os.lseek(fd, 0, os.SEEK_SET)
    try:
        buf = os.read(fd, 2)
    except IOError, e:
        sys.exit("read failed (mandatory locking works")
    else:
        print "read OK (no mandatory locking), buf =", buf

普通に実行すると

read_lock of already-locked region returns 11
read OK (no mandatory locking), buf = ab

と必須ロック不可となる。

#  mount -o bind,mand testdir /testdir

とディレクトリを必須ロック可にして再マウントし、もう一度やってみたが、同じ結果だった。ディレクトリを再マウントするだけではmandが有効にならないのだろうか?わざわざ試すのも面倒なので放置。

12.7〜12.11

LinuxではシステムVのストリームをサポートしておらず、Pythonにも対応する機能はないのでパス。

12.12 writen関数

import os
def writen(fd, buf):
    while buf:
        try:
            n = os.write(fd, buf)
        except IOError:
            return len(buf)
        buf = buf[n:]

12.13 readn関数

import os

def readn(fd, n):
    ret = []
    while n > 0:
        try:
            s = os.read(fd, n)
        except IOError, e:
            break
        
        ret.append(s)
        
        nread = len(s)
        if nread == 0:
            break
        n -= nread
        
    return "".join(ret)

12.14 メモリマップ入出力を用いたファイルのコピー

import sys, os, mmap, stat

if len(sys.argv) != 3:
    sys.exit("usage: mmapcpy <fromfile> <tofile>")

fdin = os.open(sys.argv[1], os.O_RDONLY)
fdout = os.open(sys.argv[2], 
    os.O_RDWR | os.O_CREAT | os.O_TRUNC, 
    stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP)

st = os.fstat(fdin)		# need size of input file

# set size of output size
os.lseek(fdout, st.st_size-1, os.SEEK_SET)
os.write(fdout, " ")

src = mmap.mmap(fdin, st.st_size, prot=mmap.PROT_READ)
dst = mmap.mmap(fdout, st.st_size, prot=mmap.PROT_READ|mmap.PROT_WRITE)

dst[:] = src[:]