2011年8月21日日曜日

シェルスクリプトで排他制御(flock)

シェルスクリプトの一部を排他制御する必要があり、調べたところ、CentOS 5 以降なら、util-linux に含まれる flock コマンドを使えることがわかりました。オンラインマニュアルに典型的な使い方が書いてあり、これは、なかなか素晴らしいと思いました。最初に考えた人は、すごいなと思います。オンラインマニュアルからの抜粋です。
FLOCK(1)                              H. Peter Anvin                              FLOCK(1)

NAME
       flock - Manage locks from shell scripts

SYNOPSIS
       flock [-sxon] [-w timeout] lockfile [-c] command...

       flock [-sxon] [-w timeout] lockdir [-c] command...

       flock [-sxun] [-w timeout] fd

DESCRIPTION
       This utility manages flock(2) locks from within shell scripts or the command line.

       The first and second forms wraps the lock around the executing a command, in a man・
       er similar to su(1) or newgrp(1).  It locks a specified file or  directory,  which
       is created (assuming appropriate permissions), if it does not already exist.

       The  third form is convenient inside shell scripts, and is usually used the follow・
       ng manner:

       (
         flock -s 200
         # ... commands executed under lock ...
       ) 200>/var/lock/mylockfile
...
オンラインマニュアルの例では、サブシェルを起動していますが、次のようにすれば、シェルスクリプト全体をシリアライズすることも可能です。
#!/bin/bash

exec 9>>/var/lock/mylockfile

flock -n 9
if [ $? -ne 0 ] ; then
    # ロックできなければ、失敗(exit 1)させるかどうかは、処理の内容次第です。
    # ロック解放を待ち合わせるのなら、-n オプションを付けなければ良い。
fi

#
# 排他の必要な処理を記述。
#

flock -u 9  # スクリプトに対応するプロセスが終了すれば、
            # カーネルが自動的にロック解放しますが、明示的に記述する。
exit 0
exec 9>>/var/lock/mylockfile という書き方をすると、/var/lock/mylockfile を追記モードで open して、その fd を 9 番に割り当てます。シェル内部では、dup2(2) が使われます。

CentOS 4 以下では、flock コマンドはまだ提供されておらず、lockfile という似たコマンドがあるようです。しかし、lockfile のほうは、原始的なテクニックが使われており、flock の方が信頼性は高いと思われます。

2011-09-04追加
flock コマンドは、flock(2) システムコールに依存しており、NFS では動作しないので注意が必要なようです。オンラインマニュアル (man 2 flock) から抜粋です。
2011-09-08 実験してみたところでは動作しました。追記内容を参照してください。
NOTES
       flock(2)  does  not  lock files over NFS.  Use fcntl(2) instead: that does
       work over NFS, given a sufficiently recent version of Linux and  a  server
       which supports locking.
flock はファイルシステム毎に実装 (struct file operations の flock メンバ) することができ、確かに NFS では、次のように定義されていました。
以下、2.6.18-194.el5 (CentOS 5.5) から抜粋です。
     54 const struct file_operations nfs_file_operations = {
     55         .llseek         = nfs_file_llseek,
     56         .read           = do_sync_read,
     57         .write          = do_sync_write,
     58         .aio_read               = nfs_file_read,
     59         .aio_write              = nfs_file_write,
     60         .mmap           = nfs_file_mmap,
     61         .open           = nfs_file_open,
     62         .flush          = nfs_file_flush,
     63         .release        = nfs_file_release,
     64         .fsync          = nfs_fsync,
     65         .lock           = nfs_lock,
     66         .flock          = nfs_flock,
     67         .sendfile       = nfs_file_sendfile,
     68         .check_flags    = nfs_check_flags,
     69 };
...
    639 /*
    640  * Lock a (portion of) a file
    641  */
    642 static int nfs_flock(struct file *filp, int cmd, struct file_lock *fl)
    643 {
    644         dprintk("NFS: nfs_flock(f=%s/%ld, t=%x, fl=%x)\n",
    645                         filp->f_dentry->d_inode->i_sb->s_id,
    646                         filp->f_dentry->d_inode->i_ino,
    647                         fl->fl_type, fl->fl_flags);
    648 
    649         /*
    650          * No BSD flocks over NFS allowed.
    651          * Note: we could try to fake a POSIX lock request here by
    652          * using ((u32) filp | 0x80000000) or some such as the pid.
    653          * Not sure whether that would be unique, though, or whether
    654          * that would break in other places.
    655          */
    656         if (!(fl->fl_flags & FL_FLOCK))
    657                 return -ENOLCK;
    658 
"fs/nfs/file.c"
flock(2) システムコールのコードは次のようになっています。
   1590 /**
   1591  *      sys_flock: - flock() system call.
   1592  *      @fd: the file descriptor to lock.
   1593  *      @cmd: the type of lock to apply.
   1594  *
   1595  *      Apply a %FL_FLOCK style lock to an open file descriptor.
   1596  *      The @cmd can be one of
   1597  *
   1598  *      %LOCK_SH -- a shared lock.
   1599  *
   1600  *      %LOCK_EX -- an exclusive lock.
   1601  *
   1602  *      %LOCK_UN -- remove an existing lock.
   1603  *
   1604  *      %LOCK_MAND -- a `mandatory' flock.  This exists to emulate Windows Share Modes.
   1605  *
   1606  *      %LOCK_MAND can be combined with %LOCK_READ or %LOCK_WRITE to allow other
   1607  *      processes read and write access respectively.
   1608  */
   1609 asmlinkage long sys_flock(unsigned int fd, unsigned int cmd)
   1610 {
   1611         struct file *filp;
   1612         struct file_lock *lock;
   1613         int can_sleep, unlock;
   1614         int error;
   1615 
   1616         error = -EBADF;
   1617         filp = fget(fd);
   1618         if (!filp)
   1619                 goto out;
   1620 
   1621         can_sleep = !(cmd & LOCK_NB);
   1622         cmd &= ~LOCK_NB;
   1623         unlock = (cmd == LOCK_UN);
   1624 
   1625         if (!unlock && !(cmd & LOCK_MAND) && !(filp->f_mode & 3))
   1626                 goto out_putf;
   1627 
   1628         error = flock_make_lock(filp, &lock, cmd);
   1629         if (error)
   1630                 goto out_putf;
   1631         if (can_sleep)
   1632                 lock->fl_flags |= FL_SLEEP;   1633 
   1634         error = security_file_lock(filp, cmd);
   1635         if (error)
   1636                 goto out_free;
   1637 
   1638         if (filp->f_op && filp->f_op->flock)
   1639                 error = filp->f_op->flock(filp,
   1640                                           (can_sleep) ? F_SETLKW : F_SETLK,
   1641                                           lock);
   1642         else
   1643                 error = flock_lock_file_wait(filp, lock);
...
"fs/locks.c"
NFS の場合は、f_op->flock が定義されているので、1639行目が実行され、最終的に ENOLOCK エラーが返却されることになるようです。2011-09-08 読み誤っていましたので、追記内容を参照してください。
ext3 等の場合は、f_op->flock が未定義なので、1643行目が実行されます。
なお、lock->fl_flags に FL_FLOCK を設定しているのは、1628行目です。
    278 /* Fill in a file_lock structure with an appropriate FLOCK lock. */
    279 static int flock_make_lock(struct file *filp, struct file_lock **lock,
    280                 unsigned int cmd)
    281 {
    282         struct file_lock *fl;
    283         int type = flock_translate_cmd(cmd);
    284         if (type < 0)
    285                 return type;
    286        
    287         fl = locks_alloc_lock();
    288         if (fl == NULL)
    289                 return -ENOMEM;
    290 
    291         fl->fl_file = filp;
    292         fl->fl_pid = current->tgid;
    293         fl->fl_flags = FL_FLOCK;
    294         fl->fl_type = type;
    295         fl->fl_end = OFFSET_MAX;
    296        
    297         *lock = fl;
    298         return 0;
    299 }
"fs/locks.c"

2011-09-08追記
fs/nfs/file.c の 656 行目を条件を読み間違えていました。ここは、FL_FLOCK が設定されていないのに、ここに来た場合に ENOLCK を返すというだけでした。
それで、続きを読んでみると、POSIX のロックを使ってシミュレートするよ とあります。
    639 /*
    640  * Lock a (portion of) a file
    641  */
    642 static int nfs_flock(struct file *filp, int cmd, struct file_lock *fl)
    643 {
    644         dprintk("NFS: nfs_flock(f=%s/%ld, t=%x, fl=%x)\n",
    645                         filp->f_dentry->d_inode->i_sb->s_id,
    646                         filp->f_dentry->d_inode->i_ino,
    647                         fl->fl_type, fl->fl_flags);
    648 
    649         /*
    650          * No BSD flocks over NFS allowed.
    651          * Note: we could try to fake a POSIX lock request here by
    652          * using ((u32) filp | 0x80000000) or some such as the pid.
    653          * Not sure whether that would be unique, though, or whether
    654          * that would break in other places.
    655          */
    656         if (!(fl->fl_flags & FL_FLOCK))
    657                 return -ENOLCK;
    658 
    659         /* We're simulating flock() locks using posix locks on the server */
    660         fl->fl_owner = (fl_owner_t)filp;
    661         fl->fl_start = 0;
    662         fl->fl_end = OFFSET_MAX;
    663 
    664         if (fl->fl_type == F_UNLCK)
    665                 return do_unlk(filp, cmd, fl);
    666         return do_setlk(filp, cmd, fl);
    667 }
"fs/nfs/file.c"
それじゃあ、flock コマンドが NFS を越えて正しく動作するのかというと、POSIX ロックと BSD ロック (flock) の違い (POSIX ロックは fork を越えられない) により正しく動作しないのでは? っと思い、テストプログラムで確かめてみました。CentOS 5 で確認しています。
#!/bin/bash

exec 9>>./flock.test.lock

flock -n 9
if [ $? -ne 0 ] ; then
        echo "fail" 1>&2
        exit 1
fi

sleep 30

flock -u 9
exit 0
# df -TP .
Filesystem    Type 1024-blocks      Used Available Capacity Mounted on
xx.xx.xx.xx:/ nfs   5921856   5455872    165176      98% /mnt_nfs
# /tmp/flock.sh & /tmp/flock.sh 
[1] 6452
fail
2つ同時起動で、2つ目が失敗してますから、ちゃんとロック出来ているようです。
どうなっているのか理解出来ていませんが、NFS の開発者の方は、flock も視野に実装してくれたということでしょうかね。

2011-09-09追記
CentOS 6 を NFS サーバにして、flock を試すとうまく行かず、次のようなエラーになってしまいました。クライアントは CentOS 5 です。
# ./flock.sh 
flock: 9: No locks available
fail
サーバ側を見てみると、次のエラーが出ていました。
Sep  8 23:39:14 centos6 rpc.statd[6061]: No canonical hostname found for xx.xx.xx.xx
Sep  8 23:39:14 centos6 rpc.statd[6061]: STAT_FAIL to centos6 for SM_MON of xx.xx.xx.xx
Sep  8 23:39:14 centos6 kernel: lockd: cannot monitor xx.xx.xx.xx
調べてみると、次の情報が見つかりました。

http://dfwarden.blogspot.com/2011/02/psa-nfs-locking-in-rhel6-needs-reverse.html

確かに、サーバ側 (CentOS 6) の /etc/hosts に、クライアントの IP アドレスを登録したら、flock 出来るようになりました。信頼性・セキュリティの強化が図られているということでしょうかね。

なお、lockd: cannot monitor のメッセージは、クライアント側で nfslock サービスが上がっていない場合にも出るメッセージなようです。メモ。

3 件のコメント:

  1. 例示のシェルスクリプトですが「flock -n 9」では flock(1) が終了するとロックは開放されます。その後の処理が排他されませんよ。

    返信削除
    返信
    1. fumiyas さん、コメントありがとうございました。
      [root@my41 ~]# uname -a
      Linux my41 2.6.18-308.el5 #1 SMP Tue Feb 21 20:06:06 EST 2012 x86_64 x86_64 x86_64 GNU/Linux
      [root@my41 ~]# cat /etc/redhat-release
      CentOS release 5.8 (Final)
      [root@my41 ~]# exec 9>>/var/lock/mylockfile
      [root@my41 ~]# ls -li /var/lock/mylockfile
      2288458 -rw-r--r-- 1 root root 0 Sep 9 08:34 /var/lock/mylockfile
      [root@my41 ~]# grep 2288458 /proc/locks
      [root@my41 ~]# flock -n 9
      [root@my41 ~]# grep 2288458 /proc/locks
      1: FLOCK ADVISORY WRITE 6586 08:05:2288458 0 EOF
      このようにロック取得できると思いますが、如何でしょうか。

      削除
  2. あれ、本当だ。マニュアル flock(2) に書いてありました。
    私の認識が間違っていました。すみません。

    返信削除

人気ブログランキングへ にほんブログ村 IT技術ブログへ