消失的硬碟空間

話說某一天,一位同事發現某個在 UNIX 上用 C 寫的程式,跑一陣子後似乎會吃掉很多硬碟空間,吃掉的硬碟空間用 du 去算卻跟 df 的結果差異很大,而且把 process 停掉後,空間竟然又自動恢復正常了

最後,用 fstat 去仔細分析,終於找到原因:

已經開啟的檔案,即使開啟中被強制砍掉(unlink),對原 file descriptor 持續寫入的部份仍會繼續佔用硬碟空間,寫得越多,佔用的空間也越多

實務上最常遇到這種狀況的就是 log rotation,尤其是 rotation 後的舊 LOG 是壓縮過的情況。因為經過 gzip 壓縮過後,原始的 LOG 會被刪除,只留下 XXX.gz。這個時候如果沒有人通知原來寫 LOG 的程式要重新開啟一次 LOG (重新寫一個檔案),就會導致程式在不知情狀況下繼續寫 LOG,然後空間就莫名其妙被用掉了!

例如 FreeBSD 下專門作 log rotation 的 newsyslog 設定檔 (newsyslog.conf) 就有個欄位可以設定在 log rotation 後送一個 signal 給 process,而 apache (httpd) 就接受 SIGUSR1 來當作重新開啟 LOG 檔案的訊號(事實上對 apache 而言是 graceful restart)。很多人以為這只是為了讓 LOG 能繼續寫不會漏掉,但其實更重要的是:如果不這麼作,你的硬碟可能很快就爆掉啦…

我們可以寫個簡單的程式來測試一下這種狀況:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <fcntl.h>
int main()
{
        int     fd, i;
        char    cmd[32], buf[1024];
        memset( buf, 0, 1024);
        snprintf(cmd,sizeof(cmd),"df .");
 
        printf("==> open file for write and delete it ...\n");
        fd = open( "test-file.log", O_CREAT|O_WRONLY|O_TRUNC );
        unlink("test-file.log");
        system(cmd);
 
        printf("\n==> write 100MB to file ...\n");
        for( i = 0 ; i < 1000*100 ; i++ )
                write( fd, buf, 1024);
        system(cmd);
 
        printf("\n==> close file ...\n");
        close(fd);
        system(cmd);
}

首先,這個小程式會先開啟一個檔案,然後馬上砍掉它(但先不關閉檔案),接下來執行 “df .” 來查看目前硬碟用量。第二步驟是寫入100MB的垃圾資料到這個已開啟的檔案(file descriptor)中,然後再執行 “df .” 來取得硬碟用量。最後關閉檔案後,再執行一次 “df .”。執行結果如下:

==> open file for write and delete it ...
Filesystem  1K-blocks     Used     Avail Capacity  Mounted on
/dev/ad8s1d 144520482 28011428 104947416    21%    /home

==> write 100MB to file ...
Filesystem  1K-blocks     Used     Avail Capacity  Mounted on
/dev/ad8s1d 144520482 28111508 104847336    21%    /home

==> close file ...
Filesystem  1K-blocks     Used     Avail Capacity  Mounted on
/dev/ad8s1d 144520482 28011428 104947416    21%    /home

我們可以看到程式寫了100MB之後,空間真的被佔掉了,即使我們已經刪除這個檔案,且從目錄的檔案列表中無法直接看到這個檔案了。而當被開啟的檔案關掉後,這些空間也立即被釋放回來了

接下來我們把程式中的 df 改成 fstat,可以更清楚看到狀況

1
        snprintf(cmd,sizeof(cmd),"fstat -f -p %d .", getpid());

這是最後的結果:

==> open file for write and delete it ...
USER     CMD     PID   FD MOUNT     INUM MODE         SZ|DV R/W
cdsheen  a.out 91475   wd /home 12694528 drwxr-xr-x    2048  r
cdsheen  a.out 91475 text /home 12694672 -rwxr-xr-x    7910  r
cdsheen  a.out 91475    3 /home 12694673 ----r-x--x       0  w

==> write 100MB to file ...
USER     CMD     PID   FD MOUNT     INUM MODE         SZ|DV R/W
cdsheen  a.out 91475   wd /home 12694528 drwxr-xr-x    2048  r
cdsheen  a.out 91475 text /home 12694672 -rwxr-xr-x    7910  r
cdsheen  a.out 91475    3 /home 12694673 ----r-x--x  102400000  w

==> close file ...
USER     CMD     PID   FD MOUNT     INUM MODE         SZ|DV R/W
cdsheen  a.out 91475   wd /home 12694528 drwxr-xr-x    2048  r
cdsheen  a.out 91475 text /home 12694672 -rwxr-xr-x    7910  r

Subversion: revert a commit [時光倒轉]

使用 Subversion 偶而就會遇到有人不小心把一堆不該存在的檔案 commit 進來了,這在去年寫的一篇「svndumpflter with wildcards support ?」有提到從歷史記憶中去除個別檔案的方式

但如果你能即時發現最新的一個(或幾個) commit(s) 的任何更動都是不該發生的(通常就是有人做了蠢事),那麼在有其他正常人繼續 commit 之前,其實是有蠻好的挽救機會的 💡

假設你的 repository 名稱是 foobar,最新的 revision number 是 r105,但是你即時發現 r101 ~ r105 都是某人亂搞的結果,那麼在 r106 commit 之前,應該還有機會把時光倒轉到 r100 那時清純未受污染的狀態..

廢話不多說,下面是作法

# cd /home/svn
# svnadmin create foobar.new
# svnadmin dump -r0:100 foobar | svnadmin load --force-uuid foobar.new
# mv foobar foobar.old
# mv foobar.new foobar

做完以上動作後再把該設的權限弄好,hooks 下的東西 copy 回去就可以了

如果有其他人在這段動作完成前 checkout 了 r101~r105 之間的版本,就跟他們說他們看到鬼了,請他們砍掉 source tree 重新 checkout 一份吧 🙄

上面這段作法最重要的地方是 –force-uuid 這個選項,沒有加這個選項會使得回復的 repository 的 UUID 跟原本的不同,進而導致之前已經 checkout 的 working copy 都會認不得新的 repository!

如果還來不及作上述動作,就有其他正常人 commit 東西進來了,其實也不會很麻煩,反正做完回復動作後,統統當作沒發生過,再叫作蠢事的人請一頓飯,拜託其他人把新的修改再改一次就好了

OpenBSD 4.2 – 有官方安裝光碟了

OpenBSD 4.2 just released on Nov 1, 2007!

從這版開始,官方也提供幾個主要硬體的安裝光碟了,
不再像之前一樣需要自己辛苦地製作安裝光碟

http://ftp.giga.net.tw/OS/OpenBSD/4.2/i386/install42.iso
http://ftp.giga.net.tw/OS/OpenBSD/4.2/amd64/install42.iso
http://ftp.giga.net.tw/OS/OpenBSD/4.2/macppc/install42.iso
http://ftp.giga.net.tw/OS/OpenBSD/4.2/sparc/install42.iso
http://ftp.giga.net.tw/OS/OpenBSD/4.2/sparc64/install42.iso

讓大家方便安裝之餘,也請大家有能力的話,多多贊助一下吧

La Fonera 0.7.2 r2 升級+SSH破解

[update on Dec 5, 2007]

Warning: kolofonium may not work for La Fonera 0.7.2 r2!

看了許多網路上的討論,La Fonera 0.7.2 r2 似乎沒辦法用 kolofonium 的方法破解才對,但是我卻成功了,也許是因為我是從已經破解的舊版升級的吧。還是說我的升級成功只是假象?登入畫面的版本號碼是只是因為升級成功了一半… 🙄

沒時間去細究了,改天有空來玩玩 FreeWLAN 的破解好了,看起來更有趣一點

以下是原文
====

去年底所購買的 La Fonera 0.7.1 r2 已經利用 kolofonium 的方法解除 SSH 的封印了,在前一篇文章內已有說明..

由於近期 FON 推出了新版的韌體 0.7.2 r2,號稱 DNS 查詢速度會較快,因此找個時間把韌體升級了

之前那篇文章中,韌體自動升級的功能已經被我關掉了,因此要先 SSH 進去把它改回來

SSH 進去 La Fonera 後,修改 /bin/thinclient 這個檔案,把最後一行的 # 拿掉

#. /tmp/.thinclient.sh $1

接下來把 La Fonera 的插頭拔掉再接回去,等到重新連上線,你就會發覺它已經自動升級完成了

但是這時候,SSH 的功能也被拔掉了… 按照原來的作法把它弄回去就行了

Anyway, 結論就是 kolofonium 的 SSH 破解方法也適用於 La Fonera 0.7.2 r2

不過呢,升級之後,我的 La Fonera 用到一半會自動斷線的問題還是存在…. orz

Access Subversion with Apache Proxy Module

由於網路架構的問題,我們有時會設計讓不同網段必須透過 Proxy 才能存取 Subversion Repository,但是為此架設一個獨立的 Proxy Server (如 squid)實在太麻煩,利用 Apache 內建的 mod_proxy / mod_proxy_http 功能有時候會讓工作簡化很多

ProxyRequests Off
<Proxy *>
    Order deny,allow
    Deny from all
</Proxy>
ProxyVia On

為了安全性考量,如同上面的設定,我們先把所有 Proxy 的存取關掉,然後依據需要開放特定目錄來允許 Proxy

例如我們想把對 /svn 的存取導向到 http://svn.domain/svn,那麼設定內容就會變成:

<VirtualHost *:80>
    ProxyPass /svn http://svn.domain/svn
    ProxyPassReverse /svn http://svn.domain/svn
    <Location /svn>
        <Limit OPTIONS PROPFIND GET REPORT MKACTIVITY PROPPATCH PUT CHECKOUT
 MKCOL MOVE COPY DELETE LOCK UNLOCK MERGE>
          Order Deny,Allow
          Allow from all
          Satisfy Any
        </Limit>
    </Location>
</VirtualHost>

裡面最重要的就是 <Limit OPTIONS PROPFIND… > 這一行(這一整行不能分成兩行,Copy-Paste時請注意),因為 Subversion 會用到除了 HTTP GET / POST 以外的一些特殊 Method,因此必須設定讓 HTTP 的 Proxy Module 允許這些 Methods 通過,才能讓 Subversion 正常運作

另外建議修改 Allow from all 這一行,改成僅允許特定 IP 來連結,也可強化系統的安全性

參考資料: Subversion behind an Apache Reverse Proxy (不過裡面的設定方式跟我上面提供的有一點點小小差異)

如果你還是想用 squid 來解決問題,那您可以參考 Subversion 網站上面的 FAQ

File handle is always global in Perl

Perl 的 file handle 及 directory handle 一定是 global 的,無法宣告成 local

一般使用上沒有大問題,但是當用在遞迴式呼叫時,就會產生錯誤了,下面是一個把目錄下包含子目錄中所有檔案列出來的 perl 程式,採用遞迴式呼叫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/perl
die "Usage: $0 [directory]\n" unless @ARGV == 1;
die "ERROR: $ARGV[0] is not a directory\n" unless -d $ARGV[0];
&show_dir($ARGV[0]);
exit;
sub show_dir
{
        my( $dir ) = @_;
        my( $fname );
        opendir( DIR, $dir );
        while( $fname = readdir(DIR) )
        {
                next if $fname eq '.' || $fname eq '..';
                if( -d "$dir/$fname" ) {
                        &show_dir("$dir/$fname");
                }
                else {
                        print "$dir/$fname\n";
                }
        }
        closedir(DIR);
}

由於 DIR 這個 directory-handle 會被當成 global 變數,因此這段程式會出錯,無法列出所有檔案。

解決方法是把 DIR 這一個 directory-handle 直接用 $dir 來代替,這樣在遞迴的時候就不會因為重複使用而造成錯誤了,下面是改過的程式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/perl
die "Usage: $0 [directory]\n" unless @ARGV == 1;
die "ERROR: $ARGV[0] is not a directory\n" unless -d $ARGV[0];
&show_dir($ARGV[0]);
exit;
sub show_dir
{
        my( $dir ) = @_;
        my( $fname );
        opendir( $dir, $dir );
        while( $fname = readdir($dir) )
        {
                next if $fname eq '.' || $fname eq '..';
                if( -d "$dir/$fname" ) {
                        &show_dir("$dir/$fname");
                }
                else {
                        print "$dir/$fname\n";
                }
        }
        closedir($dir);
}