FreeBSD6 + jail + httpd22 + fcgid/fastcgi + Ruby on Rails

久しぶりにものすごくはまったのでメモ。
 
目的:
FreeBSD jail 上の httpd + suexec 環境に ruby on rails を入れたが、
CGI で動かすと遅いので fcgi 対応にしたい。
 
手順:
– jail host の /etc/rc.conf に以下の記述を追加

sysvipc_enable=”YES”
jail_sysvipc_allow=”YES” # jail 環境で httpd を動かしている場合のみ追加

fastcgi を追加(ruby_fcgi 用)

$ wget http://www.fastcgi.com/dist/fcgi-2.4.1-SNAP-0311112127.tar.gz # http://www.fastcgi.com/dist/ の fastcgi の最新版
$ tar xvzf fcgi-2.4.1-SNAP-0311112127.tar.gz
$ cd fcgi-2.4.1-SNAP-0311112127
$ ./configure –prefix=/usr/local/fastcgi # /usr/local/fastcgi 以下に展開(*1)
$ sudo make install

ruby_fcgi を追加(gem install fcgi では each_cgi がないと怒られる場合があるため、ソースからビルド)

$ wget http://rubyforge.org/frs/download.php/11368/ruby-fcgi-0.8.7.tar.gz
$ tar xvzf ruby-fcgi-0.8.7.tar.gz
$ cd ruby-fcgi-0.8.7
$ ruby install.rb config — –with-fcgi-dir=/usr/local/fastcgi # (*1) で指定したフォルダ。(*1)で –prefix を指定しなかった場合、ここで config を実行しなくてよい。
$ sudo ruby install.rb

– mod_fcgid または mod_fastcgi をビルド/インストール
mod_fastcgi の場合:

$ wget http://www.fastcgi.com/dist/mod_fastcgi-2.4.6.tar.gz
$ tar xvzf mod_fastcgi-2.4.6.tar.gz
$ cd mod_fastcgi-2.4.6
$ cp Makefile.AP2 Makefile
$ make
$ sudo make install
$ # httpd2 が /usr/local/apache2 になければ Makefile を編集して top_dir を変更するか、次のようにする
$ sudo make top_dir=/opt/httpd/2.0.40
$ sudo make install top_dir=/opt/httpd/2.0.40

mod_fcgid の場合:

$ wget http://downloads.sourceforge.net/mod-fcgid/mod_fcgid.2.2.tgz?modtime=1185976592&big_mirror=0
$ tar xvzf mod_fcgid.2.2.tgz
$ cd mod_fcgid.2.2
$ make
$ sudo make install
$ # httpd2 が /usr/local/apache2 にない場合はmod_fastcgi の場合と同様の手法で top_dir 変数を変更する。

– mod_fcgid または mod_fastcgi を httpd にロード/設定
httpd.conf:

# mod_fastcgi を使う場合コメントアウト:
#LoadModule fastcgi_module modules/mod_fastcgi.so
<IfModule fastcgi_module>
   AddHandler fastcgi-script .fcgi
# ソケットの場所(省略時は httpd の log/fastcgi/ )
# FastCgiIpcDir /var/run/fastcgi
   FastCgiConfig -autoUpdate -idle-timeout 20 -killInterval 3600 -maxClassProcesses 2
   # suexec を使う場合必要
   FastCgiWrapper /usr/local/apache2/bin/suexec
</IfModule>
# mod_fcgid を使う場合コメントアウト:
#LoadModule fcgid_module modules/mod_fcgid.so
<IfModule fcgid_module>
   AddHandler fcgid-script .fcgi
   SocketPath /tmp/fcgidsock
   SharememPath /tmp/fcgidshm
   IPCCommTimeout 40
   IPCConnectTimeout 10
   # suexec を使う場合も使わない場合も FCGIWrapper は設定しなくてよい
</IfModule>
 
# suexec 例(SuexecUserGroup が肝):
<VirtualHost 192.168.10.123>
    ServerAdmin webmaster@example.com
    DocumentRoot /usr/local/apache2/htdocs/
    ServerName example.com
    SuexecUserGroup john web
</VirtualHost>

 
– rails の設定を変更
{rails_root}/public/.htaccess

– AddHandler fastcgi-script .fcgi
– RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
+ RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
– DefaultInitEnv RAILS_ENV development
+ SetEnv RAILS_ENV development

DefaultInitEnv とか SetEnv で変数がわたらない環境の場合は
{rails_root}/config/environment.rb の次の項目をコメントアウトして編集する

# Uncomment below to force Rails into production mode when
# you don’t control web/app server and can’t set it the proper way
#ENV[‘RAILS_ENV’] ||= ‘production’

 
– エラーが出る場合のヒント
http://ontherails.jp/2007/10/13/25 などを参考に。
ログファイルを見る:

httpd の error_log, suexec_log
rails の log/fastcgi.crash.log, log/development.log, log/production.log

など。
特に fastcgi/fcgid をかませた時のみ

Application error
Rails application failed to start properly

とページ表示される場合は rails の fastcgi のログを見るのが有効そうです。
 
参考:
configuration – The mod_fcgid Home Page
Apache2 + fcgid + Ruby on Railsメモ – sakuramateo
FreeBSD + Apache2 + mod_fcgid + Ruby On Rails(ふわふわな毎日)
_ fcgid(valda’s diary)
FastCGI 化(ふぇみにん日記)
Rails Apache + FastCGI 環境構築のはまりパターン
lighttpd + fcgi で each_cgi なるメソッドは知らないといわれる(Don’tStopMusic)

mdconfig の使い方色々(tmpfs, .iso のマウント, GEOM など)

md (/sbin/mdconfig) を使うと任意のファイルやメモリ領域をファイルシステム化することができます。
これを使うと、 Linux の tmpfs のようにメモリ領域にファイルを置いたり、CD イメージデータなどを仮想ファイルシステムとしてマウントすることができます。
 


■Linux の tmpfs (/dev/shm) のように使う
書式:

mdconfig -a -t swap -s <サイズ> [ -u <デバイス番号>] # swap 込みで確保(推奨)
mdconfig -a -t malloc -o reverse -s <サイズ> [ -u <デバイス番号>] # malloc (9)によるメモリ確保。大容量には向かない

 例:

$ su
# mdconfig -a -t swap -s 10m # あいているデバイス番号でメモリ 10MB を割り当て(0 〜 )
md0 # /dev/md0 が作成された
# newfs /dev/md0 # /dev/md0 に結びついた領域にファイルシステム作成
# mount /dev/md0 /mnt # アクセスできるようにマウント
# df -h | grep md0 # マウントできたか確認。
/dev/md0 9.4M 4.0K 8.7M 0% /mnt
# dd if=/dev/zero of=/mnt/foo.txt bs=1m count=5 # 適当にデータ書き込み
5+0 records in
5+0 records out
5242880 bytes transferred in 0.063932 secs (82007207 bytes/sec)
# dd if=/dev/zero of=/tmp/foo2.txt bs=1m count=5 # 比較用。/tmp より /mnt に書くほうが早くなっているはず
5+0 records in
5+0 records out
5242880 bytes transferred in 0.159325 secs (32906800 bytes/sec)
# ls -l /mnt
total 5138
drwxrwxr-x 2 root operator 512 2 4 17:15 .snap
-rw-r–r– 1 root wheel 5242880 2 4 17:18 foo.txt

 開放手順:

# umount /mnt # マウントポイントから切り離す
# mdconfig -d -u 0 # /dev/md0 をデタッチ


■任意のファイルをファイルシステムとして扱う
 (CD イメージ,ハードディスクイメージなどをマウントする)
 書式:

mdconfig -a -t vnode -f <ファイル名> [ -u <デバイス番号> ] # 指定したファイルをファイルシステムとして扱う

 例:

$ su
# ls /root/cd.iso # マウント対象の CD-ROM イメージ
cd.iso
# mdconfig -a -t vnode -f /root/cd.iso -u 1 # cd.iso を /dev/md1 に結びつける(今回は -u 1 で md1 を明示的に確保)
# mount -t cd9660 /dev/md1 /mnt # CD-ROM なので cd9660 をファイルシステムに指定してマウント。
# ls /mnt # CD-ROM の中身へのアクセス成功。
boot.cat f2.txt f6.txt initrd.gz splash.rle
boot.txt f3.txt f7.txt isolinux.bin
f1.txt f4.txt f8.txt isolinux.cfg
f10.txt f5.txt f9.txt linux

 開放手順: (前述の例と同じ)

# umount /mnt
# mdconfig -d -u 1 # /dev/md1 をデタッチ


■ zip 圧縮してサイズを節約する(geom_uzip: GEOM の利用例)
GEOM_UZIP を使うと、vnode 用イメージファイルをZIP圧縮してサイズを節約することができます。読み込み専用でしかマウントできないという欠点はありますが、CDイメージの保存やデータのバックアップなどの用途には有用です。
 例:

$ dd if=/dev/acd0 of=cdimg.iso bs=2352 # ファイルシステムのイメージデータ(なんでもよいがここでは CDドライブから吸出し(cd9660形式)) (1)
$ mkuzip -o cdimg.uzip cdimg.iso # 元のファイルシステムを geom_uzip で使える形式(*.uzip) に圧縮(2)
$ rm cdimg.iso # cdimg.iso はもう不要 (3)
$ su
# kldload geom_uzip # geom_uzip がカーネルに組み込み済みでなければここでロードする(4)
# mdconfig -a -t vnode -f cdimg.uzip # cdimg.uzip を vnode 形式で仮想ディスク化
md0
# ls /dev/md0* # geom_uzip が認識されていれば,geom_uzip 用イメージを mdconfig すると /dev/md*.uzip が同時に作成される
/dev/md0 /dev/md0.uzip /dev/mdctl
# mount -t cd9660 -r /dev/md0.uzip /mnt # /dev/md*.uzip を元のファイルシステム(この例ではcd9660)で read-only (-r)でマウントする。

 開放手順: (前述の例と同じ)

# umount /mnt
# mdconfig -d -u 1 # /dev/md1 をデタッチ

 
(1) 〜 (3) をまとめて次のように記述することもできます。

dd if=/dev/acd0 bs=2352 | mkuzip -o cdimg.uzip /dev/stdin

恒常的に geom_uzip を使う場合は、(4)のかわりに geom_uzip をカーネルバイナリに組み込むか /boot/loader.conf に次の一文を追加すると起動時に自動でロードされます。

geom_uzip_load=”YES”

 
(4)以降については *.uzip が代用される以外は前述の vnode の例と同じです。


■ geli を使った仮想ファイルシステムの暗号化:
mdconfig を geli(GEOM_ELI) や gdbm(GEOM_DBM) と組み合わせると、TrueCrypt や pgpdisk のように暗号化仮想ファイルシステムを構築することができます。今回は geli の例を紹介します。
 
geli を使うと既存のストレージがファイルシステムごと暗号化され、専用のラッパデバイス(/dev/*.eli) からしかアクセスできなくなります。/dev/*.eli をアタッチするには認証(パスフレーズ+鍵ファイル)が必要なため、必要な時のみアタッチ、マウントすることでデータの機密性を保つことができます。/dev/*.eli を使うユーザからは暗号化がされていない時と同様に利用できるため、大きな変更なしで簡単に利用できます。
 
 /boot/loader.conf:

geom_eli_load=”NO” # Disk encryption driver (see geli(8))

例:

# dd if=/dev/random of=/root/eli.key bs=64 count=1 # geli での認証用鍵ファイル(64byte)
# dd if=/dev/zero of=img.eli count=10 bs=1m # 仮想ファイルシステム用イメージファイル作成(ここでは 10MB=10*1m)。
# su
# mdconfig -at vnode -f img.eli
md0
# geli init -s 4096 -K /root/eli.key /dev/md0 # /dev/md0 を geli 用のファイルシステムとして初期化(暗号化)
Enter new passphrase:
Reenter new passphrase:
# geli attach -k /root/eli.key /dev/md0 # /dev/md0 に透過的にアクセスするためのデバイス取得(/dev/md0.eli が作成される)
Enter passphrase:
# newfs /dev/md0.eli # ファイルシステムは UFS 形式として初期化(暗号化された領域内)
/dev/md0.eli: 10.0MB (20472 sectors) block size 16384, fragment size 4096
        using 4 cylinder groups of 2.50MB, 160 blks, 192 inodes.
super-block backups (for fsck -b #) at:
 160, 5280, 10400, 15520
# mount /dev/md0.eli /mnt # あとは普通に(UFSとして)マウント。

 解放手順:

# umount /mnt # /dev/md0 のマウント先をアンマウント
# geli detach md0.eli # /dev/md0.eli を解除
# mdconfig -du 0 # /dev/md0 を解除

 
最初に作成した鍵ファイル(例では/root/eli.key) はフラッシュメモリなど暗号化デバイスとは別の場所に保存するのが望ましいです。
また、

geli init

の引数に -K <keyfile> を含めない場合はキーレス認証(パスフレーズのみ)での暗号化になり、 -P を追加するとパスフレーズレス認証(鍵ファイルのみ)での暗号化になります。
注意点として、geli init をするとデバイスに結びついたファイルシステムが初期化されることにも気をつけて下さい。詳細については geli(4) を参照のこと。
 


参考:
– mdconfig(8)
mdだって凄いんです。(404 Blog Not Found)
Linux で仮想 CD ドライブをマウントする(Hermit Complex 楽天支店)

# mount -t iso9660 -o loop /home/tagosuck/tagosucd.iso /mnt/tagosucd0

tips – ltrace で共有ライブラリの関数呼び出しをトレースする

http://0xcc.net/blog/archives/000103.html
より。

ltrace [ {オプション} ] {実行するプログラム} [ {引数..} ]

のようにすると、任意のプログラムを動的にブレークポイントを挿入して実行し、共有ライブラリとシステムコールの呼び出しをトレースすることができます。
簡単にいうと GDB より高級なデバッガのような感じです。
 
Linux の場合は標準でインストールされることが多そうですが、yum や apt-get を使う場合は

yum install ltrace

または

apt-get install ltrace

でインストールできます。
また FreeBSD の場合も ports に含まれているため、

portinstall sysutils/ltrace

とすることでインストールできます。
 
利用例:
ls / を実行しライブラリ関数をトレースし、log.txt にトレース結果を出力する(-o)。

$ ltrace -o log.txt ls / # プログラムの実行とトレース
bin dev home lib media mnt proc sbin tmp var
boot etc initrd lost+found misc opt root sys usr
$ cat log.txt # トレース結果参照
_start(0xbff1abd4, 0xbff1abd7, 0, 0xbff1abd9, 0xbff1abf0 <unfinished …>
__libc_start_main(0x804ea41, 2, 0xbfe34df4, 0x8056f58, 0x8056fac <unfinished …
setlocale(6, “”) = “en_US”
bindtextdomain(“coreutils”, “/usr/share/locale”) = “/usr/share/locale”
textdomain(“coreutils”) = “coreutils”
__cxa_atexit(0x8053701, 0, 0, 0x805b570, 0xbfe34d68) = 0
is_selinux_enabled(0x8053701, 0xd1bbe4, 0x32206e75, 0x34312036, 0x3a30333a) = 0
isatty(1) = 1
# 以下略

利用例2:
ライブラリのトレースだけでなくシステムコールもトレース(-S)し、
関数呼び出しの関係を階層構造で出力する(-n {number})。

$ ltrace -o log2.txt -S -n 2 ls /
# 略

$ cat log2.txt
SYS_uname(0xbfe4106c) = 0

SYS_brk(NULL) = 0x99ca000
SYS_access(0xbffdc1, 4, 0xc03fd4, 0xbffdc1, 4) = -2
SYS_open(“/etc/ld.so.cache”, 0, 00) = 3
# 中略
SYS_munmap(0xb7f92000, 4096) = 0
_start(0xbff20bd4, 0xbff20bd7, 0, 0xbff20bd9, 0xbff20bf0 <unfinished …>
    __libc_start_main(0x804ea41, 2, 0xbfe412c4, 0x8056f58, 0x8056fac <unfinished
 …>
        setlocale(6, “” <unfinished …>
            SYS_open(“/usr/lib/locale/locale-archive”, 32768, 01540) = 3
            SYS_fstat64(3, 0xd301a0, 0xd2eff4, 0xd301a0, 0xd0fc60) = 0
            SYS_mmap2(0, 0x200000, 1, 2, 3) = 0xb7d82000
            SYS_mmap2(0, 204800, 1, 2, 3) = 0xb7d50000
            SYS_mmap2(0, 4096, 1, 2, 3) = 0xb7d4f000
            SYS_close(3) = 0
        <... setlocale resumed> ) = “en_US”
# 以下略

利用例3: プログラムの経過時間を一緒に出力する(パフォーマンスチューニング用途など)

$ ltrace -tt -o log.txt ls / # マイクロ秒単位
19:56:53.267749 SYS_uname(0xbff417dc) = 0
19:56:53.267865 SYS_brk(NULL) = 0x9e12000
19:56:53.267917 SYS_access(0xbffdc1, 4, 0xc03fd4, 0xbffdc1, 4) = -2
# 以下略

-t にすると 19:56:53 のように秒単位、-tt にすると例のようにマイクロ秒単位、 -ttt にすると マイクロ秒単位で 1201517930.289331 のような UNIX タイムスタンプ形式になります。

tips – 実行時に共有ライブラリの優先順位を変える(シンボル重複時の動作)

Linux では複数の共有ライブラリ(*.so)に同じ関数が定義されている場合、重複する関数については先に読み込まれた共有ライブラリが利用され、その他のライブラリで定義される重複シンボルについては無視されます。
 
共有ライブラリのロード順序は ld でのリンク順になります。
また、既存の実行可能ファイルのロード順序を調べるには ldd を使います。

$ gcc src.c -o foo -lc -lm # libc.so, libm.so の順にロード
$ ldd foo # foo では libc.so が優先されていることを確認
        libc.so.6 => /lib/tls/libc.so.6 (0x00c07000)
        libm.so.6 => /lib/tls/libm.so.6 (0x00d35000)
        /lib/ld-linux.so.2 (0x00bed000)
$ gcc src.c -o bar -lm -lc # 同じソースに対してリンク順序を入れ替えて bar 作成。
$ ldd bar # bar では libm.so が優先されていることを確認
        libm.so.6 => /lib/tls/libm.so.6 (0x00d35000)
        libc.so.6 => /lib/tls/libc.so.6 (0x00c07000)
        /lib/ld-linux.so.2 (0x00bed000)

 
今回の本題、どうやって共有ライブラリを優先するかを実行時に決めるか?ですが、
LD_PRELOAD を使うことでコンパイル時のロード順を入れ替えることができます。
 

>

$ # LD_PRELOAD に /lib/tls/libc.so.6 (libc.so へのパス)を指定
$ LD_PRELOAD=/lib/tls/libc.so.6
$ ldd bar # 優先順位が変わっていることを確認
        /lib/tls/libc.so.6 (0x00c07000)
libm.so.6 => /lib/tls/libm.so.6 (0x00d35000)
        /lib/ld-linux.so.2 (0x00bed000)

ちなみに、LD_PRELOAD を設定すると、(setuid/setgid されたプログラムなど)一部を除くほとんどのプログラムで任意のライブラリを事前ロードできます。
これを利用すると任意のプログラムのライブラリ定義関数を置き換えることが可能です(参考:hook_tcp.so)。
 
参考:
共有ライブラリの作成(HP テクニカルドキュメント)
 
hook_tcp.so(チームチドリ)

What is hook_tcp.so:
LD_PRELOADを利用して、プログラム中で実行される connect()の接続先をsyslogへ記録するプログラムです。

とのことで、libc.so の CONNECT(2) を hook して動作します。
 
4.17. LIDS を使う時は、LD_PRELOAD 環境変数に注意した方がいいですか?(LIDS FAQ)

Google を使った MD5 逆変換

http://labs.cybozu.co.jp/blog/nishio/2007/12/googlemd5.html
より。
 

http://md5-db.com/hashes/

から始まる URL をハッシュと文字列の対照表にして Google に読ませてMD5検索から実体を得ようという試みらしいです。

http://md5-db.com/hashes/foobar

とか。
このサイトのMD5値は 2008-01-28 現在数桁分しか Google でヒットしませんでしたが、一般的な英単語、短い英数字文字列などの MD5 値は他のサイトで結構ヒットするようで。。
 
d35cfefa43f4ad9ef9d5b962d487a12c を Google で検索

カテゴリ別記事一覧ページを生成する

chalow で出力したページ用にカテゴリ別記事一覧ページがほしくなったので、
chalow の ChangeLogReader.pm を使い ChangeLog から自動生成するスクリプトを即興で書いてみました。
 
ソース
 
結果サンプル(装飾用 CSS)
 
色々だいぶ適当さがあるのはご愛嬌です。

任意のデータを表現するURL

RFC 2397 に規定される data:// から始まる URL を利用すると、任意のデータを URL として表現できます。

書式:
“data://” [MIME] [“;base64”] “,” data

たとえば

<a href=”http://www.ecoop.net/”>hello!</a>

という HTML テキストは data スキームを使うと以下のようになります。

data://text/html,%3Ca+href%3D%22http%3A%2F%2Fwww.ecoop.net%2F%22%3Ehello%21%3C%2Fa%3

また、base64 形式でも指定できます。

data://text/html;base64,PGEgaHJlZj0iaHR0cDovL3d3dy5lY29vcC5uZXQvIj5oZWxsbyE8L2E+

 
IMG タグのソースURLとして利用する例(RFC2397より引用)

   <IMG
   SRC=”data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw
   AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz
   ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp
   a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl
   ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis
   F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH
   hhx4dbgYKAAA7″
   ALT=”Larry”>

テキストファイルだけでなく画像データや音声データなど、任意のデータファイルを表現することができます。ただし A タグのHREF属性で利用する場合など利用環境によって URIの文字列長制限がある場合もあるため、比較的小さいデータを表現するときに利用するのがいいでしょう。
 
参考:
RFC 2397 The “data” URL scheme

デッドロックのテスト用コード

test_lock.php:

<?php
error_reporting(E_ALL);
$f = array();
$f[0] = null;
$f[] = fopen(“lock_1”, “r+”);
$f[] = fopen(“lock_2”, “r+”);
 
if(@intval($_SERVER[‘argv’][1]) >= 2){
$task = array(2, 1);
}else{
$task = array(1 ,2);
}
 
foreach($task as $n){
echo “Locking $n … “;
 
echo flock($f[$n], LOCK_EX, $t=true)? “OK”: “Fail”;
echo “\n”;
echo “waiting some seconds…”;
sleep (5);
echo “OK\n”;
}
echo “Finished!\n”;

実行:

$ php test_lock.php &
$ php test_lock.php 2 &

 
ひとつのみ起動した場合 Finished! と表示されますが、上のように並列で実行した場合は正しく動作しなくなります。
 
手元の環境では FreeBSD 6.2 と Windows XP SP2 の場合はデッドロックして固まり、Linux 2.4.21-50.ELsmp の場合は 2番目のロックに fail し、デッドロックにはなりませんでした。
 
参考:
– flock のモード切替え時の挙動は環境によって異なる[2008-01-08-1]

flock のモード切替え時の挙動は環境によって異なる

flock() でロック獲得中にロック状態を切り替える(LOCK_SH から LOCK_EX にする、など)と、環境や状況によってロックが保持されない場合があります。
 
検証用コード:
A… ロック切り替えプロセス(keeper.php )

<?php
$f=fopen(“t.txt”,”r+”);
for($i = 0; $i < 10; $i++){
  // ロック切り替えまたは取得(LOCK_EX -> LOCK_SH)
  echo “shared($i)\n”;
  flock($f, LOCK_SH);
  sleep(3);
  // ロック切り替え(LOCK_SH -> LOCK_EX)
  echo “exclusive($i)\n”;
  flock($f, LOCK_EX);sleep(3);
}
echo “Done! the treasure was protected!\n”; // ここまで theif.php にロックがとられなければ期待通り。
?>

B…ロック奪取プロセス(thief.php)

<?php
$f=fopen(“t.txt”, “r+”);
flock($f, LOCK_EX); // ロック待ち
// ロック奪取成功
echo “lock obtained!\n”;
echo “im doing something, hehehe.”;
for($i=0;$i<10;$i++){
  sleep(1);
  echo “.”;
}
echo “\nfinished! bye ;P\n”;’
fclose($f); // ロック開放
?>

実行:

$ touch t.txt
$ nice php locker.php &
$ php thief.php

結果:
Windows XP SP2 + Cygwin + PHP 5.1.2 (FS: NTFS) … ロック保持されない(thief won)
Linux 2.4.21-50.ELsmp + PHP 4.4.6 (FS: ext3) … ロック保持される(keeper won)
FreeBSD 6.2 + PHP 5.2.5 (FS: ufs) … ロック保持される(keeper won)
 
Windows XP (FS: NTFS) はロック自体はできますが、LOCK_EX <-> LOCK_SH の切り替えでいったんロックが解除されるという挙動になりました。
 
なお、PHP マニュアルの flock() の項にもあるとおりファイルシステムが FAT の場合など同じ Windows でも環境によっては結果が変わるため、この結果を鵜呑みにせず必ずご自身の環境でテストしてください。


追記(2008-01-09):
ちなみに、PHP 5.2.5 でソースをたどってみたところ PHP の flock() は ext/standard/flock_compat.c で flock (2) ではなく fcntl (2) を使って実装されていました。Linux や BSD など fcntl でロック変換がアトミックにできる場合は問題なく動作しそうです。
なお Windows で、かつ fcntl がない(Cygwin など UNIX 擬似環境ビルドではない)場合は、 UNIX と互換性があるように LockFileEx() で実装されています。