2011年12月30日 星期五

C/C++ 檢查和打開 debug 功能的小技巧

打開 debug flag 有助於了解模組的行為。工程師開發時應該都有幫自己除錯留後路, 所以找到他們開發的方式, 可省下自己瞎試的時間。

以前寫 Python 或 Java 時, 都有近乎標準的 logging module 可用, 所以要找到開啟 logging 機制的方式滿簡單的, 看是用那家的 logging module, 針對它的設定檔設 log file, 或在程式開始時塞段程式啟動 logging 即可。不過 C/C++ 好像沒有那麼一致。

留意程式碼會看到輸出訊息的巨集, 像是 P("What the F**k!!");, 然後在某些 debug 相關 header 檔裡會看到類似這樣的東西:

#ifdef DEBUG
#define P(x) ...
#else
#define P(x) ;
#endif

其中 "..." 的部份是該模組用來輸出除錯訊息的機制, 可能是寫到檔案, 或輸出到 stdout/stderr 等。這樣在沒有 define DEBUG 的情況下, 完全不會產生除錯訊息的程式碼, 減少執行負擔。

要確定程式真的有編進除錯訊息的話, 隨便找個程式內有的除錯訊息, 然後用 strings 檢查:

$ strings PROGRAM | grep "What the F**k!!"

看有沒有結果。有的話大概就 ok (找個少見的字串最穩), 不然就是沒編進去, 檢查看看 #define DEBUG 1 有沒有設好, 或是有沒有在 makefile 的 CFLAGS 設好 -DDEBUG

再來, 也可以用 strace 檢查執行期間到底有沒有執行到:

$ strace -e write PROGRAM | grep "What the F**k!!"

不過 write 寫入的字串不見得會一次剛好符合一整串, 這和檔案 flush 機制有關。找子字串, 或用眼睛注意一下比較穩一些。懶得找方法打開 log 檔的話, 用 strace -e write 將就看也還可以, 不過會拖滿執行速度, 且 strace 有時會影響 process 的狀態, 常期用的話還是打開 log 檔最穩。

2011年12月26日 星期一

打開 core dump 和使用 cgdb 檢查程式掛點原因

前置動作

首先, 用 gcc/g++ 編程式時記得加 -g 以加入除錯資訊。

接著參考《產生 core dump 的方法》設成可以產生 core dump。

檢查 backtrace

$ gdb PROGRAM

然後在 gdb 內執行

core core.PROGRAM.PID.TIMESTAMP

接著就能用 bt N 看最底層 N 個 call stack 為何, 也就是所謂的命案現場啦。然後可用 up, do, l 等指令切換 call stack 和列出週圍程式。

在和 gdb 相處一段時間後, 覺得這樣在 stack 之間移動很方便, 但看週圍的程式太辛苦了, 還是會開另一個視窗用 VIM 看完整一些的程式。

看到 jserv 建議使用 cgdb, 試了發現, 人生...啊不, 是視窗變彩色的了!! 不只有彩色的程式碼, 還外加類 VI 的瀏覽方式, 相當順手。

目前有用到的功能如下:

  • 按 i、ESC 在程式碼視窗和 gdb 命令列之間切換。
  • 程式碼視窗裡可用 VI 的部份指令移動行數; 回到命令列後和 gdb 完全相容, 可按上下鍵選用之前的指令。
  • 在程式碼視窗按空白鍵加減 break point。

官網文件有詳細的說明, 之後來掃一遍, 看有什麼好東西可用。

2011年12月25日 星期日

善用 strace、debugger 從執行期間找出問題根源

最近被迫在短時間內學會 strace、gdb 這些之前一直用不到的重兵器, 都還不熟練就是了。剛好使用 hgsubversion 時有些困擾, 雖說它和 svn 整合得很好, 無縫接好 pull / push, 但它不會顯示 mercurial 對應到的 svn 版本, 平時看其它和 svn 整合的工具 (如 issue tracking) 會很困擾, 用得都是 svn 版號。

剛剛想到可以學 Strace -- The Sysadmin's Microscope 的做法, 用 strace 找出關聯的程式, 再來看怎麼修正它。

我推測 hg 一定有記錄 svn 相關版本的方式, 不然無法和 svn server 同步資料。於是挑個會讀到 svn 資料的指令來試:

strace -o trace.log -s 512 -e read,open hg svn info

用顯示的 svn 版號 123 來翻 trace.log, 發現這兩行:

open("/path/to/project/.hg/svn/lastpulled", O_RDONLY) = 3
read(3, "123\n", 4096)                 = 5

於是找到 meta data 存在 .hg/svn/ 下。 到該目錄下找到 .hg/svn/rev_map 這個檔案, 裡面存 hg 和 svn 的版號對應表。至少這樣就有足夠的材料寫個 script 來轉換 hg、svn 的版號。

不過若能直接加到 hg 裡, 應該會更方便也較可攜。要做這點相對容易, 可以到 hgsubversion 原始碼目錄下用 ack 找相關程式。

先用 ack rev_map 找到存 meta data 的物件 revmap, 再用 ack revmap 找到 wrappers.py 是換掉 hg 指令的程式。

再來用 pdb 觀察 revmap 如何被使用。先在 wrappers.py 裡設中斷點, 然後執行 pdb /usr/local/bin/hg parents --svn 找出 meta data 如何被讀出來。於是明白可在函式 parents 的部份塞入幾行顯示 svn 版本:

--- a/hgsubversion/wrappers.py        2011-12-25 00:34:39.170606104 +0800
+++ b/hgsubversion/wrappers.py        2011-12-25 00:33:04.161800527 +0800
@@ -57,6 +57,9 @@
         raise hgutil.Abort('No parent svn revision!')
     displayer = cmdutil.show_changeset(ui, repo, opts, buffered=False)
     displayer.show(ha)
+    # patch svn revision
+    print '\033[1;32msvn revision: %d\033[0m' % hashes[ha.node()][0]
+    print
     return 0

這樣打 hg parents --svn 就會多輸出一行 svn 的版號。雖說顯示在 hg log 會更方便, 不過 wrappers.py 裡沒有 log, 之後有再找時間看看怎麼加。

附帶一提, 剛用 pdb 時還不太習慣, 忘了 python 是執行期載入程式, 不能像 gdb 那樣在執行前就指定檔名指定行數設中斷點。而是要在 script 裡直接塞

import pdb; pdb.set_trace()

2011-12-25 更新

如留言裡的討論, 上述的修正沒有實質幫助, 就當作練 debugger 吧。最後覺得另外寫個 shell script 最省事效果也最好, 寫好的東西放在這裡

2011年12月24日 星期六

加速 linking time

頻繁修改一點程式又要執行看結果時, 主要的時間都花在 linking。這時才體會到省下 linking time 也是很重要的事。

這篇提到兩個減少 linking time 的做法

  • 使用 ramdisk
  • gold

我本來就有用 SSD, 改將編譯結果全放到 ramdisk 後, 提昇的效果不怎麼明顯。到是用 gold 後減少了一半的 linking 時間。

不過 gold 也不是那麼完美, 這篇提到一些問題。我自己用的時候發覺滿常遇到 ld 可以編, 但 gold 不行。最後的解套方式是寫個小 script 切換 /usr/bin/ld 連到的程式。不常編的東西就暫時換回 ld 連結個一次就好。

另外 jserv 提到 gcc 有命令列參數可直接指定用那個 linker (ref.)。wens 則查到 Debian/Ubuntu changelog 裡有寫 下可用 -B/usr/lib/compat-ld-B/usr/lib/gold-ld for ld.gold

順便 google 一下看怎麼查 changelog, 似乎是看 /usr/share/doc/binutils/changelog.Debian.gz 或用 aptitude changelog PKG, 不過這兩個作法都只有最近的 changelog, 沒有完整記錄, 像查 binutils 時就沒有找到上面提的參數說明。

2011年12月22日 星期四

iPad: waiting for sync to start 的解法

沒想到我也會有寫這種 3C 裝置文章的一天。

今天用 Windows7 的 iTunes 連 iPad 要安裝新的 app, 結果一直出現 "waiting for sync to start", 等很久後就說失敗。聽同事說可能是在不同台電腦用 iTunes sync, 有時會錯亂, 他以前也遇過。google 到不少人有類似經驗, 不過沒什麼好解法, 大概的做法是

  • 將 iPad 的連接線拔掉重插看看
  • 按住上方電源鈕和 home button 關機, 再重開看看
  • 將 iPad 回復到原廠設定看看
  • 移掉 iTunes, 再重裝看看

很不幸的, 最後是做完最後兩步才 ok。重裝後有些東西可能會不見, 軟體是認 Apple ID, 影響不大。聽說有些 in-app purchase 的東西 (如遊戲道具?), 不會重發的。

btw, 雖說 Apple 的產品包裝得相當貼近消費者, 對我這種習慣用 console 的人來說, 有錯誤卻看不到任何詳細資訊, 實在是很悶的事。只能亂 google 和無腦亂試。

2011年12月21日 星期三

列出用到的 shared library

本文整理大家的建議和自己實際操作的心得。新手上路, 有錯還請指正。

static library 就是一包 object file, 沒什麼需要提的, static library 沒有記錄其它資訊。所以, 編 shared library 或 executable 時要自行處理好 static library 的相依性, 在前篇有提到一點資訊。

shared library 有兩種, 一種在 linking 時要指定好 shared library 用到的 undefined symbol 放在那些 shared library 裡, 待執行時再載入到記憶體使用; 另一種用 dlopen() 和 dlsym() 載入 (這兩個函式存在 libdl 內)。

前者比較單純, 可用 ldd 透過靜態分析了解用到那些 shared library, 且各自實際指到的檔案。ldd 本身是一個 shell script, 用到 ld.so 事先定義的一些機制 (LD_TRACE_LOADED_OBJECTS) 來讀取資料, man ld.so 裡有相關的說明。其中 LD_LIBRARY_PATH 和 LD_PRELOAD 相當實用, 無法在連結時解決問題時, 至少還有這招可在載入時處理。

若想分析透過 dlopen() 載入的動態函式庫, 有幾個做法

  • 在程式執行中觀察 /proc/PID/maps, 這個檔案記錄 process 用到的各區段記憶體為何, 可從對應到的檔案看出有載入的 shared library。必要時可配合 gdb 在想觀察的部份停住, 再從外部看 /proc/PID/maps這裡man proc 有相關說明。
  • strace 執行程式, 觀察開啟的檔案: strace -f -e open PROGRAM 2>&1 | grep "\.so"

就我自己小試的心得, 看 /proc/PID/maps 最穩, 且方便看各別 process、thread 載入的函式庫, 也不會拖慢觀察目標的執行程式。不過 strace 不需配合 gdb 停在該停的地方, 就「快篩」的角度來看, 也滿有用的, 加上 -f 後方便追蹤 multi-process、multi-thread, 不過執行速度好像有慢一些, 不太確定。之後再比較看看兩者適合的使用時機。

參考資料

2011-12-22 更新

  • 依 wens 和 Scott 的留言更新上面 strace 的例子。
  • strace -e 的參數看來頗有用的, 之後有需求時再來研究。

2011年12月20日 星期二

用 file 查看文字檔編碼或執行檔為 32bit 或 64bit

實用的例子:

  • file -i TEXT_FILE: 會顯示 charset
  • file BINARY_FILE: 會顯示 32-bit 或 64-bit

當然還有其它更多資訊, 目前這兩個對我來說很實用, 希望這樣寫過一次多少會記久一點 ...。

2011年12月19日 星期一

解決 undefined symbol / reference

C++ 新手上路, 有錯還請幫忙指正。

基本觀念

相較於 script language 或 Java 來說, C/C++ 有完整的「編譯 -> 連結 -> 執行」三個階段, 各階段都可能發生 undefined symbol。在解決惱人的 undefined symbol 前, 得先明白整個編譯流程:

  1. 編譯 .c / .cpp 為 .o (object file) 時, 需要提供 header 檔 (用到 gcc 參數 -I)。事實上, 在編譯單一檔案時, gcc/g++ 根本不在意真正的 symbol 是否存在, 反正有宣告它就信了, 所以有引對 header 即可。這也是可分散編譯的原因 (如 distcc ), 程式之間在編譯成 .o 檔時, 並沒有相依性。
  2. 用 linker (ld 或 gold) 將 *.o 連結成 dynamic library 或執行檔時, 需要提供要連結的 library (用到 gcc 參數 -L 指定目錄位置, 用 -l 指定要連什麼函式庫)。不同於前一步, 此時 symbol 一定要在。
  3. 執行的時候, 會再動態開啟 shared library 讀出 symbol。換句話說, 前一個步驟只是檢查是否有。檢查通過也連結成 executable 或 shared library 後, 若執行時對應的檔案不見了, 仍會在執行期間找不到 symbol。若位置沒設好, 可能需要用 LIB_LIBRARY_PATH 指定動態函式的位置, 但不建議這麼做, 最好在執行 linker 時就指定好位置。原因見《Why LD_LIBRARY_PATH is bad》

明白這點後, 就看 undefined symbol 發生在那個階段, 若是編 object file 時發生, 就是沒和編譯器說 header 檔在那, 記得用 -I 告訴它。若在 linking 時發生, 就要同時設好 -L 和 -l。不過難就難在要去那找 undefined symbol 的出處。

解決問題的流程

首先是判斷 symbol 是不是自己用到的原始碼裡, 可配合 id-utils 找看看 (我是用 gj, 比較方便一點)。或是看有沒有 man page, 有 man page 的話, 裡面會記錄用到的 header 和該怎麼下連結參數。若在專案裡找不到, 再用 Google 搜看看 symbol, 運氣好可能會找到套件名稱, 運氣不好.....目前還不知怎麼處理較好, 目前是四處亂翻看看。如果是網路上找來的程式碼, 別人已附好正確的 include 了, 這時用 apt-file search HEADER_PATH 就能找到套件名稱 ( 記得先跑 apt-file update 更新資料庫 ), 比方說: apt-file search openssl/rsa.h 會得到 libssl-dev: /usr/include/openssl/rsa.h。

在 Ubuntu 上, 通常需要裝 X-dev 以取得 header 檔。若是已經裝好套件了, 可用 dpkg --searchlocate 或是 dpkg -L PKG_NAME 找出 header 位置。

若編譯過但 linking 時出錯, 要做進一步分析, 先看是那一個程式用到 undefined symbol。不管是自己的程式出錯, 或是用到的函式庫出錯, 都可從對應的原始碼找到編譯時用的 header X.h。

  • 先看有沒有 man page, 有的話, 裡面會寫該下什麼參數連結。像 man sqrt 會看到說要 "Link with -lm" (記得裝 manpages-dev)
  • 若 X.h 是自己的, 就在附近找看看原始碼在那, 有沒有編譯到。
  • 若 X.h 放在系統目錄裡, 可用 apt-file search X.h 找出 library 的可能出處 ( 記得先跑 apt-file update 更新資料庫 )。接著可用下列方式之一找出函式庫的可能位置:
    • dpkg --search SUBSTRING_OF_LIBRARY_NAME
    • dpkg -L PKG_NAME | grep lib
    • locate SUBSTRING_OF_LIBRARY_NAME # 記得先跑 updatedb

若知道函式庫的確切名稱, 且有 pkg-config 的資訊的話, 可用 pkg-config --libs LIBRARY_NAME 直接找出 gcc/g++ linking 時該下的參數 (附帶一提, 用 --cflags 找出編譯時用到的參數, 像是 -I 接的)。不然, 用其它方式找到函式庫位置後, 要依 -L-l 的規則寫下參數。記得 -l 後接的名稱不用加 "lib", 像 libm.so 是用 -lm。

實際寫較具規模的專案時, 可能不會用手刻 makefile, 要視自己用的整合工具, 將找到的資訊加入整合工具中。

其它相關資訊

  • 可配合 nm LIBRARY 查看 symbol, man nm 有各狀態說明, U 表示 undefined。若該函式應該要出自該函式庫, 卻標為 U, 表示該函式庫一開始就沒編好, 要重編該函式庫。反之, 若該函式定義在外部函式庫, 則是連結時出錯。
  • nm 只適用 static library 或未 strip 前的 shared library。strip 後的 shared lib 得用 readelf -Ws 來看, 這個情境下沒 nm 簡單易讀。(2014-10-27 更新: 也可用 nm -D)
  • 函式庫有 U 通常是正常的, 編執行檔或 dynamic library 時才要指定連結的位置。換句話說, 若執行檔 X 用到 static library A, 而 A 用到 library B。則編 X 時, 要加上 -lA 和 -lB 的參數。編 X 的部份要知道它用到的函式庫有那些相依性, 而不是 A 自己會搞定自己的相依性, 這點不太直覺 (ref.)。
  • static library 只是一堆 object file 的集合體。之所以會用 ar 和 ranlib 編 static library, 目的是減少連結的檔案以方便管理。在用 readelf -Ws 讀 static library 時, 會列出各個 object file 的內容。讀 dynamic library 時就沒這樣列了 (ref.)。
  • 在 Linux 下 linking 時要注意函式庫的順序, 摘錄 gcc manpage 關於 -l 的說明:
    It makes a difference where in the command you write this option; the linker searches and processes libraries and object files in the order they are specified. Thus, foo.o -lz bar.o searches library z after file foo.o but before bar.o. If bar.o refers to functions in z, those functions may not be loaded.
  • 當 libm.so 和 libm.a 同時存在時, -lm 會連到 libm.so, 官方說明見 man ld--library=namespec 該段 (ref.)。感謝 cmtsij 的說明。
  • 可用 ldd 找出 dynamic library 實際連到的檔案。

參考資料

2011年12月16日 星期五

用 Eclipse CDT 讀 C/C++ 原始碼

上篇, 記一下 Eclipse CDT 的用法, 全部加起來耗去我不少時間啊。

1-1. 匯入舊的非 Eclipse CDT 專案:

File -> New -> Makefile Project with Existing Code

之所以選用沿用舊的檔案位置, 而非開新專案再匯入程式碼, 是因為這樣比較方便和 VCS 共存。可先用 VCS ( git/hg/svn/... ) 取出專案, 再用 Eclipse CDT 來讀碼。VCS plugin 再怎麼成熟, 都不會比直接用 command line 或專屬的 GUI 來得完整。

1-2. 選 Toolchain for Indexer Settings

GNU Autotools Toolchain

少勾這個, 之後匯入程式碼後會有 Type 'std::string' could not be resolved 這類鳥錯誤。因為少加了預設 header。實際要選那個 toolchain 要看開發環境而定, 沒有仔細研究, 至少在 Ubuntu 上這樣做 ok。

2. 將 Indexer 範圍改為全部

Window -> Preference -> C/C++ -> Indexer

Index unused headersIndex source and header files opened in editor 勾起來。剩下加減看看, 我有調大 Cache limits, 不知影響多大。

雖然 Eclipse CDT 會猜測 include 那些檔案而只 index 那些檔案的 symbol, 但 C/C++ 的 include 似乎是很混亂的世界, 有 macro, ifdef 等東西混在裡面, 很難光用靜態分析搞定。對我這一直活在安全的 Java 和 Python 世界的人來說, 真是晴天霹靂, 花了不少時間才明白這事。

所以結論是, 都 index 就是了, 多花些時間總比找不到來得好。還有, C++ 的世界是很危險的, 能活在 Java 或 Python 的世界的話不要過來。

3. 建 index

在左側的 Project Explorerproject 名稱上按右鍵, 選 Index -> Rebuild

然後等個一陣子, 之後就可開始看程式。按 F3 看定義、按 F4 看繼承關係、按 Ctrl+Alt+H 看在那些地方被呼叫, 都很方便。

注意有些檔案仍會出現 symbol not found, 自己想辦法找到它的位置 (例如用 gj), 在 Eclipse CDT 內打開後, 就會自動建該檔的 index。再回來看原本的程式, 就會找到該 symbol 了。

4. (非必要) 若有用到專案外的標頭檔, 需要另外加入

Project -> Properties -> C/C++ Genral -> Paths and Symbols

點右側中間的 C++ 再點 Add, 加入需要的標頭檔。之後再照上個步驟重編 index。

參考資料:

2011-12-19 Update

Ubuntu 11.04 的系統背景色是黑色, 而 Eclipse 部份提示字會用系統背景色, 於是變成黑底黑字的慘狀。 How to change tooltip background color in Unity? 有提到解法: 修改 /usr/share/themes/Ambiance/gtk-2.0/gtkrc tooltip。然後再換到別的 theme 再換回來, 就會更新了。至於換 theme 的方式, 則是 System -> Preferences -> Appearance

2011年12月12日 星期一

閱讀 C/C++ 原始碼的好幫手

最近有需求讀 C/C++ 的東西, 試了 ctags, cscope 覺得不理想。問了一下收到許多回應 (G+plurk ), 真是太感謝大家了, 減少入門摸索的時間。

試用的感想如下:

grep

  • 優點: 好上手
  • 缺點: 陽春
  • 安裝: 內建於 Linux

gtags

  • 優點: 可找 caller 和 callee
  • 缺點: 因為索引檔是由 ctags 來的, 會漏東西; 執行方式也有些不便
  • 安裝: 程式很久沒人更新了, 要做一些修正才裝得起來
    • 參照官網指示
    • make 時看少了什麼 header, 手動補一下 header
    • 然後 make 還是會失敗, 將 gas.py 的 "import as" 改為 "import asm", 下面用到的模組名也要跟著改, as.py 也要改為 asm.py。python 2.6 後 as 是 keyword
    • 編好後將幾個用到的 python scripts 第一行由 python2.4 改為 python

ack

  • 優點: 比 grep 容易使用, 省得配合一些 command 過濾檔案, 見官網的《Top 10 reasons to use ack instead of grep.》。而且還有彩色的輸出!!
  • 缺點: 因為沒建 index 的關係, 速度較 id-utils 慢, 我的測試情境要 3s, 而 id-utils 只要 0.006s
  • 安裝: curl http://betterthangrep.com/ack-standalone > ~/bin/ack && chmod 0755 !#:3

id-utils

  • 優點: 速度快, 和測 ack 同樣的情況, 建索引 5.3s, 之後搜尋瞬殺
  • 缺點: 介面沒有 ack 直覺易用, 我寫了個小程式 gj 以 id-utils 為底, 提供彩色輸出和進一步過濾檔名的功能。
  • 安裝: Ubuntu 超容易, aptitude install id-utils

Eclipse CDT

  • 優點: 方便開新視窗看 caller、callee
  • 缺點: 不方便搭 vim 使用 (對 vim 重度使用者才有差); 建 index 有點久, 我的測試情境要數分鐘到十分鐘吧
  • 安裝: 結果這個是我試最久的, 因為不知怎麼建 index。參考官網 FAQ, 建索引前要先設 include dir path。我一直找不到 context menu, 結果它就是左側的那個專案清單。另外 Eclipse CDT 也會漏一些東西, C++ 特別嚴重。

結論

  • 用 Eclipes CDT 方便平時快速跳到定義
  • 輔以 id-utils + gj 確保不會漏東西。之後用一用再視需求來更新 gj 功能。

2011-12-16 Update

Eclipse CDT 的問題有一部份是我設錯, 詳細設法見用 Eclipse CDT 讀 C/C++ 原始碼

2012-02-02 Update

2011年12月9日 星期五

Web basic access authentication

之前都沒注意到, 原來用 apache2 做使用者身份認證 (如使用 htaccess) 時, 跳出來詢問使用者名稱和密碼的對話框並不是網頁, 而是 client 軟體提供的輸入框。

《Basic access authentication》對此有詳細說明, 或用 Firefox 的 HttpFox 觀察 http header request 和 response 也不錯。幾個重點:

  • basic authentication 的傳輸沒有加密, 最好在 SSL 下使用
  • server 發覺沒通過身份認證時會傳 401 並在 header 裡附上 WWW-Authenticate: Basic ... 的訊息
  • client 送出 Authorization: Basic ... 的訊息, 其中 "..." 是 base64(USERNAME + ":" + PASSWORD)
  • 除關掉瀏覽器外, 沒有明確的方式「登出」這種登入方式

2011年12月7日 星期三

VirtualBox 設 shared folder

《How To Share Files In VirtualBox With Vista Guest And Ubuntu Host》設定 host OS。

若 client 是 Linux, 見 《HOWTO: Use Shared Folders》, 就一行指令啦:

sudo mount -t vboxsf SHARE_FOLDER_NAME /mnt/share

SHARE_FOLDER_NAME 是在 host OS 設定時打的名稱。

2011年12月3日 星期六

CPython 的 garbage collection

來消化之前有讀沒時間寫的東西。

長期使用 python 後, 會很納悶為啥記憶體一直漲, 明明已沒用到了, 卻沒有減少。這篇提到 CPython 的 gc 機制採用 reference counting, 另有備案的 mark and sweep, 用來解決 circular reference。btw, 強烈推薦 Back to basic: Series on dynamic memory management, 淺顯地介紹各種 gc 運作的方式。

Scott 的說法, reference counting 算是苦工的半自動化管理記憶體, 因為實作細節交給實作者處理, 用 Python API 寫 CPython 的 extension 時, 開發者得自己管理物件的 reference count, 是件很辛苦的事。好處是在 reference count 為 0 的時候, 會立即回收記憶體。但缺點是, 若 a、b 兩物件互相參照到對方, 卻沒被任何人用到的話, a 和 b 都無法被回收, 因為 reference count 永遠會是 1。

為解決這個問題, 只好多提供 gc.collect 喚起 mark and sweep 的演算法清掉 a、b。但 mark and sweep 也有自己的問題, 主要有兩點:

  • 不像 reference counting 會在沒用到的第一時間點立即回收, 而是 mark and sweep 執行後才回收。
  • 執行 mark and sweep 的時候為確保沒有算錯 reference graph, 必須暫停目前所有執行中的 python process/thread, 結束後才可繼續執行。可想而知, 記憶體用量大後, 這個暫停時間也會太久, 不適合需要不斷有回應的程式 (如 GUI、Web)。

所以又有了 generational garbage collection, 關鍵的想法是: 觀察到大部份物件都很早死 (英年早逝啊~), 所以只要回收年輕的物件即可回收大部份的記憶體。於是將使用到的物件分不同「年代」, 預設只回收最年輕的一代, 沒回收到的物件就放到下一代。下回要再回收的時候, 就不動這個舊一代的物件, 減少要建立的 reference graph。

回想平時寫程式的結構, 可以想見, 像頻繁使用的 local variable 仍會被這個方法回收, 偶而用到的 global variable 只有第一次會算到, 之後被當作舊一代的物件後, 就不會再檢查。這樣只要有個方式確保不會漏查舊一代指向新一代的物件, 就可確保不會誤刪仍用到的物件。至於明明可回收卻沒回收到的物件, 相較於省下的時間來說, 算是可接受的取捨。generational garbage collection 值得一看, 有提到一些巧思, 取得計算時間和空間的平衡。

可想而知, CPython 用的是 generational gc, 而 JVM 也是。看來好東西大家都會一起用。

Btw, 即使了解了這些仍無法解釋為啥 CPython 有一堆沒有歸還一堆沒在用的記憶體, 實際的情況比想像中還複雜, CPython 有一些「絕對不會歸還」的記憶體, 像是 small integer pool, 和重覆回收使用的 PyIntObject, 或是有 circular reference 且這之中有物件實作 __del__。

2011-12-04 Update

看到 Thinker 針對本篇的補充, 貼過來備忘: CPython 的 GC 二、三事

2011年11月25日 星期五

Effective C++ item 30: 了解 inline

之前寫的《C++ inline 用法背後的原因》有一點小錯, 原來也有可在連結時期做 inline 的建置環境, 如 .NET CLI。另外提到 template 的實作通常要也要寫在 header 裡, 理由同 inline, 這樣 compiler 才知道怎麼具現化它。

此外, 這則條款提到 inline 的特性只是建議, 不是強制行為。並強調 inline 的缺點有

  • 若函式有點大且頻繁地使用, inline 後 object code 會變很大。可以想見會浪費硬碟空間和記憶體, 也可能更容易造成記憶體換頁而減低 instruction cache hit rate。
  • 在更改 inline 後的函式時, 使用到 inline 函式的 object code 必須重編。若沒用 inline 且是使用 dynamic linking 的話, 可以完全不動客戶端的程式。
  • 大部份 debugger 不支援設中斷點

第一點的負擔比較難評估, 第二、三點到是滿顯見的問題。所以結論是, 除簡單的 getter 或 max / min 這類小函式, 盡量別用 inline, 直到發覺某個小函式占據可觀比例的執行時間後, 再考慮使用 inilne。

2011年11月24日 星期四

ARM Instruction Set 初步心得

之前看了一點點 ARM 的東西, 做個記錄。我完全不熟這個領域, 下面的心得可能會有許多錯誤。

《Tonc: Whirlwind Tour of ARM Assembly》這篇超級詳細地從頭教怎麼寫 ARM assembly code。作者原本的用意是教人寫 GBA, 而 GBA 底層是跑 ARM, 所以會需要寫 ARM assembly code 最佳化。

看完這篇後再來看大方向的觀念《ARM架構》, 就很有感覺了。對一個大學時代只寫過一點 x86 assembly, 只在課本上看 RISC 的人來說, 讓我比較印象深刻的是

  • ARM 的指令相當精簡, 很容易懂。概念上來看, 硬體成本應該會較低較省電。
  • 為減少指令過於精簡不方便使用或效率差的負擔, 強化了一些功能。又一個好例子可用來說明系統設計是一連串的取捨
  • ARM 的 shift 沒負擔, 透過 Barrel shifter 可同時在 OP2 做 shift / rotate 等操作。
  • ARM 的所有指令都有另留幾個 bit 以支援 conditional code, 藉此減少需要猜測 branch 的負擔。猜錯 branch 得另外重取要用到的指令和資料, 時間成本似乎滿大的。這裡借用《Tonc: Whirlwind Tour of ARM Assembly》實作 max() 的範例程式表示什麼是 conditional code:
@ // r2= max(r0, r1):
@ r2= r0>=r1 ? r0 : r1;

@ Traditional code
    cmp     r0, r1
    blt .Lbmax      @ r1>r0: jump to r1=higher code
    mov     r2, r0  @ r0 is higher
    b   .Lrest      @ skip r1=higher code
.Lbmax:
    mov     r2, r1  @ r1 is higher
.Lrest:
    ...             @ rest of code
    
@ With conditionals; much cleaner
    cmp     r0, r1
    movge   r2, r0  @ r0 is higher
    movlt   r2, r1  @ r1 is higher
    ...             @ rest of code
  • 由於 ARM Instruction Set 希望全部指令大小一樣是 32 bits, 又拿了一些 bits 給 Barrel shifter 和 conditional code 用, 所以能放常數的 bits 又更少了。也不像 x86 那樣指令大小不同, 可以看 OP code 決定多一個 cycle 從下個 cycle 取出常數。所以, 只能讀一個 byte 大小的數字, 再配合 Barrel shifter 改變數值。所以, 可以讀入 0x1C000000, 0x001C0000, 但無法讀入 0x010C0000。有這種需求時, 得拆多個指令組起來, 或是放到附近的 constant pool。
  • 部份裝置主要讀取指令的匯流排寬度是 16 bits (如 GBA), 所以 32 bits 指令得花兩個 cycle 才能讀完。於是有 Thumb 的設計, 指令精簡為 16 bits, 不過也就沒有 conditional code 等加速的空間。但 Thumb 也有機會讓全部的程式變更小, 對嵌入式系統來說, 更小的執行檔表示可省下更多記憶體存執行檔, 藉此減少硬體成本。開發者可混用 ARM 和 Thumb, 達到時間和空間的平衡點。

2011年11月23日 星期三

超輕量級的 python web framework: Bottle

寫完上篇冗長的 Django 心得後, 改來介紹極端相反的 web framework: Bottle

有一則關於 Python 的笑話是這麼說的:

Python is the only language with more Web frameworks than keywords.

在初評估 web framework 時, 對照一大串 web framework 看到這則笑話, 實在是令我哭笑不得。不過在有不同需求後, 到覺得這樣也是有好處啦。

當初要做 ego-post 的時候, 想找個很方便部署的框架, 比較方便推廣, 最終目的是讓一般人也能用 (後來發覺寫成 Chrome App 更適合)。我的主要功能用 javascript 和 html5 (用到 localStorage) 實作, web 後端只是用來存長期資料而已。一開始是試 Flask, 但裝 Flask 需要另外裝 Werkzeug 和 Jinja 2, 所以最後改用 Bottle

Bottle 有多小呢? 全部就一個 bottle.py 而已, 所以懶得要求使用者另外裝 Bottle 的話, 將 bottle.py 加到目前專案就結束了。而且它不到 3000 行, 有興趣了解最基本的 web 應用需要那些功能, 看 bottle.py 應該可以學到不少東西。像 http response code 418 I'm a teapot 這種蠢東西, 就是從 bottle.py 裡學到的。

2011年11月21日 星期一

Effective C++ item 23: 以 non-member、non-friend 取代 member function

看到這條款有種水到渠成的快感, 來寫下心得。

在我大學寫 Java 四年的時光裡, 我寫了一堆 over design 假 OOP 的東西。於是, 在我出社會工作的三年裡, 我改用 Python 盡可能的都寫成 function, 覺得很卡時 (像是一堆參數重覆傳來傳去), 才會包成 class。想體會看看兩種極端的寫法, 藉此看看能否從中悟出什麼道理。

這七年的時光合起來, 我沒悟出什麼大道理, 到是覺得後來這種寫法偶而是有點不便, 不過大部份時候還挺順的, 不會包得太笨重。直到看到 Effective C++ item 23, 才有種恍然大悟的感覺, 明白背後的原因。

作者提出一個觀點: 愈少程式能動到資料, 封裝的效果愈好。所以, 若這個操作可以寫成 non-member、non-friend function, 可保證它不能存取到 private data。再者, 也比較有機會讓不同 class 來重用這個 function。像是 std 裡面的 sort(), 就比放到 vector 裡成為 member function 來得好。既可提高 vector 的封裝效果, 也方便重用 sort (有提供 index 和 comparable 即可)。這篇兼顧「減少相依性」和「提高重用性」切入說明這則條款的精髓, 值得一讀。

初看這個觀點有些反直覺, 還是會覺得放到 class 裡比較像「封裝」。要找函式時也比較方便, 看 class 的 header 就可知道全部操作。但 item 23 接著強調: 若要方便找函式, 放在 namespace 下也有類似的效果, 如同 Java 寫成 utility class 的 static member function 一樣。

回想 Java 的 Arrays.sort() 和 Collections.sort() 也是同樣的設計, 只是 Java 沒 namespace, 這類函式改放到某個 class 下。當然, Python 的 sorted() 也是如此, 不過是放在 __builtin__ 這個 package 裡。其它類型的還有 max()、min() 等函式。Python 和 Java 都有提供方便的 max()、min() 來操作 container, 不知 C++ 有沒有類似的東西, 只看到 algorithm 有比較兩個數字的實作。

2011-11-22 Update

Aethanyc 提醒, STL 裡有 max_element()

2011年11月19日 星期六

C++ 建構式和解構式使用 exception 的注意事項

item 08: Prevent exceptions from leaving destructor

一樣是要避免未定義行為, 清資源清到一半卻因 exception 而沒做完, 是不道德的, 有機會造成更多 memory leak。

《[17.9] How can I handle a destructor that fails?》指出更嚴重的問題, 當 exception A 發生時, C++ 不會執行剩下的程式, 而會跳到 catch A 的部份。但跳到 catch 前, 要先清掉這之中的 call stack, 自然會觸發各 function call 的 local variable 的 destructor。若在這時又丟出 exception B, 沒辦法決定該執行 catch A, 還是跳去執行 catch B。忠孝兩難全, 只好要求 process 自盡結束它短暫的一生。

所以說有 destructor 也不是都是好事, 像 Python / Java 沒法和你保證什麼時候會呼叫 "destructor" ( Python 的 __del__、Java 的 finalizer ), 自然不用擔心有這種 exception 中的 exception 兩難。只會另外建議你不要用 "destructor"

書中另外建議, 若想兼顧「自動回收資源」 (環保做功德) 和「避免在 destructor 內吞掉 exception」, 可另外提供方法讓使用者可呼叫函式清資源, 再自行處理可能丟出的 exception (像關 db connection)。若使用者忘了呼叫這類方法, destructor 再做最低限度的保護, 幫忙呼叫該函式清資源。不過有 exception 時, 使用者也沒得抱怨 destructor 吞掉 exception 沒處理。

那 constructor 內可不可以丟出 exception?

照理說要能丟比較何理, 不然怎麼處理 invalid argument? 查一下 FAQ, 果然有令人愉快的答案:

結論是不會有 memory leak, 也不會呼叫 destructor, 可以安心地丟 exception 出來。

C++ 建構式和解構式使用 virtual 的注意事項

原本打算快速掃過一遍《Effective C++ 3/e》, 有個大略印象, 之後再視需求仔細看相關守則, 但看到一半發現忘了一堆前面看過的東西。還是來寫寫筆記好了。

  • item 07: declare destructors virtual in polymorphic base classes。這樣 delete base pointer 時, 才會呼叫到真正的 destructor, 然後正確地依序往上呼叫各類別的 destructor。
  • item 09: never call virtual functions during construction or destruction。理由是 C++ 在 constructor / destructor 裡沒有多型的效果, 因為 C++ 認為在建構或解構時呼叫子類別的函式有危險, 有可能用到未定義的資料, 乾脆定成在 constructor / destructor 裡沒有多型的效果
  • 續上則, 補充說明原因。建構式的呼叫順序是由上往下, 而解構式是由下往上。所以若在建構式呼叫子類別的函式, 該函式可能用到子類別特有的欄位, 這時自然還沒初始化, 導致未定義行為。反之, 若在解構式呼叫子類別的函式, 子類別的解構式已清掉它的欄位, 這時再使用到也會導致未定義行為。
  • 續上則, 值得注意的是, 在 Java 裡沒有禁止這麼做。Java 類別欄位的 (class field) 的所有型別都有預設的初始值 (像是 0、false、null 等)。Java 任何時候呼叫任何函式, 都會有多型效果。在建構式或解構式時這麼做, 會呼叫到子類別的函式, 至於這類的行為是否符合需求, 就自行判斷啦。下面附上 Java 的測試碼, 順便測一下欄位和方法的行為, 注意兩者行為不同, 方法有多型效果。
class A {
 String m = "A.m";
 
 void p() { System.out.println("A.p(): " + m); }
 
 A() { System.out.println("A()"); p(); }
}

class B extends A {
 String m = "B.m";
 
 B() { System.out.println("B()"); p(); }
 
 void p() { System.out.println("B.p(): " + m); }
 
}

public class Test {
 public static void main(String[] args) {
  A a = new B();
  System.out.println("--- After new ---");
  a.p();
  System.out.println(a.m);
  System.out.println(((B)a).m);
 }

}
/*
執行結果:
A()
B.p(): null
B()
B.p(): B.m
--- After new ---
B.p(): B.m
A.m
B.m
*/

2011年11月5日 星期六

C++ inline 用法背後的原因

看過編出來的組語碼後, 深刻感受到 inline 可以省掉大量操作記憶體的時間, 特別是函式內容簡短又常被呼叫的話, 也許在 stack 上操作記憶體的時間比實際運算的還久。

初學 inline 語法讓我很困惑, 它是如此地令人困惑以致於 FAQ 裡有數則說明:

令人困惑的點在於, 無論是 non-member function 或 member function, 若要使用 inline, 函式的定義都要寫在 header 裡。否則 compiler 在編譯時會抱怨「undefined reference to ...」 -- 一個不明確的錯誤訊息。

直到看了 compiler 和 linker 運作的基礎知識後, 才明白是怎麼回事。原因是 compiler 一次處理一個 .cpp 檔以及它引入的 header (由前處理器引進來的, 所以其實還是只有處理一份原始碼檔案)。所以在使用 inline 且函式定義寫在另一個 cpp 檔時, compiler 無法進行 inline 最佳化。也許是為了方便實作 compiler / linker (或為了符合既有的工具?), 而如此設定 inline 的語法吧。

附帶一提, 看了《淺談 GCC 編譯技術 Break Compilation Boundaries with GCC》才知道 compiler optimization 有分不同層級, 像是 Link-time optimization。若要移除沒用到的程式, 就得在 link-time 才能確定某些程式真的沒有被用到。

2011年10月30日 星期日

小心 C++ 的 non-local static 物件的初始順序

Java 有保證在第一次使用 class 內任何東西時, 會執行 class initialization 的程式。而 C++ 沒有保證不同編譯單元的 non-local static 物件的初始順序。

  • 編譯單元是指同一 object file 的原始碼和其引入的檔案
  • static 物件指不在 heap 也不在 stack 上的東西, 包含全域變數、namespace 上的變數、class 內 static member 等

這會導致一個可怕的事實: 使用另一個 object file 的 namespace 下的物件時 (這是滿常見的需求), 會導致未定義的行為。《Effective C++ 3/e》item 4 的建議解法是用函式存 local static 物件, 然後用函式取得該物件 (類似 singleton 的概念)。

用程式來說, 就是:

// Bad! Lead to an undefined behavior if clients access tfs.
FileSystem tfs;

// Good!
FileSystem& tfs() {
    static FileSystem fs;
    return fs;
}

附帶一提, 不同語言的規範差真多, 之前看 ARM 的組語, 才發覺之前只知道 x86 的東西, 而對 CPU 有些誤解。現在確信只學單一工具會有風險, 思維會受限於該工具, 誤以為那就是世界。人生有許多事也是如此, 想到莊子所說的話:

井蛙不可以語於海者,拘於虛也;﹔夏蟲不可以語於冰者,篤於時也;曲士不可以語於道者,束於教也。

體悟漸多後, 漸漸也不知要寫什麼心得才好。總之就是多看看, 時時提醒自己所知很窄, 別錯將一種方法當作只有這種方法。

x86 呼叫函式的設計慣例 (call convention)

在看了《BUFFER OVERFLOW 5 - C/C++ Function Operation》後, 才明白 stack frame 的細節, 果然教人攻擊的文件講得最清楚啊。後來又看了《x86 calling conventions》, 才明白 compiler 編譯高階語言為組語時, 只是定好一套規範, 確保能和 OS 的 linker、loader 搭起來, 到沒有規定一定得怎麼做。編譯的其中一項設計, 是規範 caller 和 callee (被呼叫的函式) 之間怎麼互動, 也就是怎麼傳參數過去, 怎麼取回運算結果。《x86 calling conventions》有列多種做法。

換句話說, 這和設計及使用框架一樣, 為了方便上層開發者, 針對某項工作制定了一整個框架, 只要照著框架的規範使用框架提供的工具, 寫起來就會很快。不過也因此犠牲掉一些彈性和效率, 是不可免的 trade-off。巨觀來看, 設計工具將高階語言編成可執行檔, 也是設計一套框架。如同使用其它框架一樣, 框架幫開發者省去細節, 專心於開發應用程式上; 而了解框架能進一步讓使用者用得更順手, 還有從框架的設計中學到深入的技巧。

呼叫函式時, caller 大致上需要做以下的事, 順序是我大概列的, 只是一種 call convention, 沒有寫得很精確:

  1. 準備參數給 callee
  2. 備份 caller 用到的一些暫存器
  3. 備份 caller 下一個要執行的指令位置
  4. 跳到 callee 要執行的指令位置
  5. ( callee 備份 caller 的 frame's base pointer, 改變 frame's base pointer (ebp)、 stack pointer (esp) )
  6. ( callee 自己準備空間做為區域變數 )
  7. ( callee 結束後, callee 要清掉自己的區域變數 )
  8. 待 callee 結束後, 取得 callee 運算結果
  9. 待 callee 結束後, 清掉傳給 callee 的參數

對照用 gcc -S 編譯 C 程式成組語來看原組語碼, 有一些小心得:

  • 了解呼叫函式要做這麼多事, 也難怪小動作要盡量 inline, 不要真的呼叫函式; 還有避免使用遞迴, 改用 loop + stack。
  • 明白函式內用 static 宣告的變數, 會放到 .data 或 .bss (視有無初始值), 而不在 stack 上。所以函式結束後, 能夠保留原本狀態。
  • 因為呼叫函式是一層層地將函式參數、caller return address、區域變數等資料丟到 stack 上, 當函式結束時, 不用清掉剛才用的區域變數, 只要將 frame pointer 和 stack pointer 指回 caller 用的位置, 就能跳回 caller 之前執行的指令接著跑 (有時仍需還原一些用到的 register)。雖然因此不能取得區域變數, 但省下清記憶體的時間, 算不錯的 trade-off。反過來說, 也沒人規定「一定不能取回結束函式內的區域變數」, 這是 C/C++ 的語法規範, 而相關工具也只是照語法實作而已。
  • 由於 stack pointer (esp) 會變動, 使用 frame pointer (ebp) 比較方便取得參數和區域變數, 分別在 ebp 的上下方。ebp 附近也存了準備還原回 caller 狀態的暫存器, 如 caller 的 ebp。
  • gcc 在進入函式時, 會先計算之後呼叫函式時, 需要多大的空間傳參數, 多讓 stack 往下長一些空間, 這樣之後要呼叫函式時, 可以直接將參數放到預留空間上, 這樣函式結束時, 不需要清掉參數。下面舉例說明。
$ cat f.c
#include <stdio.h>

int add2(int a, int b) {
    return a + b;
}

int add3(int a, int b, int c) {
    return a + b + c;
}

int main(void) {
    add2(3, 5);
    add3(3, 5, 8);
    return 0;
}
$ gcc -S f.c
$ cat f.s
        .file   "f.c"
        .text
.globl add2
        .type   add2, @function
add2:
        pushl   %ebp
        movl    %esp, %ebp
        movl    12(%ebp), %eax
        movl    8(%ebp), %edx
        leal    (%edx,%eax), %eax
        popl    %ebp
        ret
        .size   add2, .-add2
.globl add3
        .type   add3, @function
add3:
        pushl   %ebp
        movl    %esp, %ebp
        movl    12(%ebp), %eax
        movl    8(%ebp), %edx
        leal    (%edx,%eax), %eax
        addl    16(%ebp), %eax
        popl    %ebp
        ret
        .size   add3, .-add3
.globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $12, %esp  # 在 stack 上預留 4 * 3 = 12 bytes 放參數
        movl    $5, 4(%esp)
        movl    $3, (%esp)
        call    add2
        movl    $8, 8(%esp)  # 呼叫完 add2 後不需清 stack 上的參數
        movl    $5, 4(%esp)
        movl    $3, (%esp)
        call    add3
        movl    $0, %eax
        leave
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
        .section        .note.GNU-stack,"",@progbits

原本的作法是在呼叫函式前用 push 放參數, 呼叫完函式後, caller 用 pop 清掉參數。上面的例子, 則省掉清參數的動作, 也是空間換時間的好例子。

之所以如此強調「清函式參數」和「清區域變數」的行為, 是因為記憶體相關操作相對於 CPU 運算很慢, 要記憶體也慢、清記憶體也慢 (可以實驗看看跑 tree-based 的演算法, 然後比較有無 free tree 的情況, 效能差多少), 了解這些小動作省去清記憶體的動作, 覺得很讚。仔細一想, 這和預先要一塊 buffer, 然後自己管記憶體以減少頻繁地要小塊記憶體的概念相似, 只是在呼叫函式的時候, stack 就是那塊 buffer。

最後附上兩個自己試的小例子, 說明 stack frame 結束後, 原記憶體上的資料仍會留著, 可以將它讀出來, 不過不是什麼正確的行為就是了。

例一:

#include <stdio.h>

void a(void) {
    int i;
    printf("%d ", i);
    i = 10;
    printf("%d\n", i);
}

void b(void) {
    int i;
    printf("%d ", i);
    i = 20;
    printf("%d\n", i);
}

int main(void) {
    a();
    b();
    a();
    return 0;
}

執行結果:

-1080771896 10
10 20
20 10

例二:

#include <stdio.h>

struct Point {
    long x, y;
};

struct Point get_p(void) {
    struct Point a;
    return a;
}

void set_x(void) {
    struct Point a;
    a.x = 1;
}

void set_y(void) {
    struct Point a;
    a.y = 2;
}

int main(void) {
    struct Point t;
    t.x = t.y = 100;
    set_x();
    set_y();
    t = get_p();
    printf("%ld %ld\n", t.x, t.y);
    return 0;
}

執行結果:

1 2

2011年10月28日 星期五

交接心得

將之前隨手的筆記稍微整理一下備忘。

主要參考文章: 《How to hand over a project systematically? - Stack Overflow》。Stack Overflow 真是居家必備的好參考資料。其它參考文章滿發散的, 沒有詳記出處, 看些設置專案的 best practice 也有用, 預防勝於治療。

消化後, 我自己覺得重要的事, 依順序如下:

  1. 每個專案要有個 readme 說明如何開始。確保對方知道如何跑 tests 還有跑範例指令得出範例結果, 確認專案沒有問題。建立專案環境包含知道專案相依性, 若有用適當的軟體, 會少很多痛苦, 像 python 的 virtualenv + pip 是不錯的選擇, 可以盡可能地找出最乾淨的相依組合。
  2. 為什麼我們需要這東西, 它的目的為何
  3. System Context 和 Architecture Overview: 方便對整體有個概念, 兩張圖的用途不同, 參考《Architecture Overview》。不過我的交接對像本來就知道部份內容, 雖然有畫這些圖, 卻不知效果如何。
  4. data flow diagram: 個人偏好有個實例表示資料如何在各元件之間轉換, 能自己想通整個資料走向, 對理解整個架構和除錯很有幫助。我自己在理解專案時, 常會自己畫資料流, 藉此弄清楚架構, 找出我不明白的環節。

從上面列的東西, 可看出來重心放在小巧實例, 方便快速上手, 有東西跑, 比較會有感覺。再來是架構 (概念)和設計背後的原因, 這些很難從程式碼看出來的東西。程式註解也該寫這麼做的原因, 而不是它在做什麼。後者看程式就可以懂了, 寫文件的時間也要花在刀口上。此外, 文件本身也會有維護成本, 盡量寫真的必要的部份。

產生 core dump 檔案

重新寫過記錄在《產生 core dump 的方法》

C/C++ const

個人認為 coding 的核心就是管理「物件/函式」之間的 state, 像 namespace (或 Java 的 package)、class、少用全域變數、static 與否等, 都是協助管理 state 的語法。另外, const 也是很有用的語法, 在 C/C++ 這裡用法更靈活。

參考《Const-Correctness in C++》, 筆記如下:

  • const 和 pointer 的關係: 有分目標變數是否為常數, 和指標本身是否為常數兩種, 共有四種組合。詳見之前寫的: 《C 的型別宣告》
  • const_cast: 將常數轉回非常數, 但為造成未定義的行為, 使用情境很窄, 見文末的例子。
  • 基於 "..." 的實際運作情況, 寫成 char const *s = "..." 較好。若使用 char *s = "...", 之後改變 s[3] 會造成未定義結果。該篇說明的例子是, 改變 s[3] 後會改到別的常數字串, 因為是用同一個 string pool (見該篇例子); 而我用 gcc 在 linux 下試的結果是 segmentation fault。
  • const member function 很有意思 (即宣告成 void foo() const), 用它可做到類似 Haskell 的 stateless 的效果, 語意上是不會改變 member fields, 所以 const member function 只能呼叫其它 const member function, 和 Haskell 預設 function 的行為很像, 有助於維護程式。個人相當喜歡這個效果, 可惜 Java 沒有, 得自己用 interface 做到類似效果, 不怎麼直覺。
  • 承上, const member function 的表現行為, 相當於讓 this 的型別變成 Klass const * const this, 其中 Klass 是某個類別名稱。所以在取用 this->xxx = ... 時自然會造成編譯錯誤。附帶一提, 一般 member function 的 this 的型別則是 Klass * const this, 也就是 this 都是常數指標, 不能讓 this 指到別的物件, 即 this = ... 會造成編譯錯誤; const member function 則多了「指到的對像為常數」的限制, 即 this->xxx = ... 會造成編譯錯誤。
  • const member function 有個顯著的缺點, 它無法做 cache。既然知道它不會改 member field, 那應該有很大的機會針對傳入參數做 cache, 省下運算時間。但衝突的是, const member function 就是不能改 member field, 自然也不方便做 cache (用全域變數太髒了)。這時得引入關鍵字 mutable。const member function 可以改變宣告為 mutable 的 member field。

2011-10-30 Update

看完《Effective C++ 3/e》 item 3 談論如何使用 const, 多學到了:

  • const std::vector<int>::iterator iter = vec.begin(); 相當於 int * const
  • std::vector<int>::const_iterator iter = vec.begin(); 才是 int const *
  • 即使用了 const member function, 仍要避免傳出指標, 以免指標指向的內容被改變, 造成 const member function 看似沒有改變內容的假像
  • 若需要同時提供同樣操作, 但有傳回 const 和非 const 的兩種版本, 為了避免重覆程式碼, 可實作傳回 const 的版本, 然後在非 const 的版本用轉型的方式呼叫 const 的版本:
const char& operator[](std::size_t position) const {
    ...
}
char& operator[](std:size_t position) {
    return const_cast<char&>(
        static_cast<const T&>(*this)[position]
    );
}

2011年10月14日 星期五

關於 C/C++ 負整數的 shift 以及 arithmetic right shift 的小技巧

在看 《Tonc: Whirlwind Tour of ARM Assembly》 的時候學到一個小技巧: 若 n 是有號 32bit 整數, 則 n>>31 相當於 n >= 0 ? 0 : -1。

看完 arithmetic shift 的定義後, 才發現我沒弄懂 arithmetic right shift, 原來是一直補進 signed bit (most significant bit), 當它是負數時, 等於一直補 1 進來。所以, -1 不管右移幾次, 結果仍是 -1, 不是 0。

將這訊息發到 G+plurk 後, 看到許多人提到 C、C++ 沒定義 signed shift 的行為, 所以用的時候要小心。jserv 提到 compiler 可以加參數改變位元運算的結果。

查看 gcc 的說明, 得知 gcc 有實作 arithmetic right shift, 但看不懂 left shift 的部份。Scott 提到這篇有解釋, 然後我明白了一件事: 關於 C99、C++0x draft 的定義, 我都看不太懂啊 ...。孩子的教育不能等, 要好好學英文。

btw, Louis 提到一個有趣的應用, ((n>>31)^n) - (n>>31) 等同於 abs(n)。拆解這個式子可得知, n 是正數時:

  • n>>31 是 0
  • n^0 仍是 n
  • n - 0 仍是 n

n 是負數時:

  • n>>31 是 -1
  • -1 以 2 補數表示為 0xFFFFFFFF
  • n^(-1) 等同於 1 補數運算
  • 最後再減 -1, 得到 2 補數運算的值

順便試著寫了以下的 C code, 再編成 assembly code, 想說看看會差多少:

int abs1(int n) {
    return n >= 0 ? n : -n;
}

int abs2(int n) {
    if (n >= 0)
        return n;
    return -n;
}

int abs3(int n) {
    return ((n>>31)^n) - (n>>31);
}

結果 abs1 和 abs3 差不多, 以下是 abs1 用 gcc 4.4.3 -O3 編後的結果:

abs1:
 pushl %ebp
 movl %esp, %ebp
 movl 8(%ebp), %eax
 popl %ebp
 movl %eax, %edx
 sarl $31, %edx
 xorl %edx, %eax
 subl %edx, %eax
 ret

gcc 偷吃步用 arithmetic right shift 避免使用 branch。不知這是針對特例的最佳化, 還是用某種神祕的機制算出來的。附帶一提, 我用不同版本的 gcc 編出來的結果不同, 用 4.5.2 最佳化編譯後, abs1 和 abs3 的結果完全一樣, 只差在用的 register 不同。

另外, 若用 ARM assembly 的話, abs() 可以寫成:

@@ 參數由 r0 傳入, 結果寫到 r0
cmp     r0, #0
sublt     r0, 0, r0

而用 arithmetic right shift 的版本則是:

eor     r1, r0, r0, asr #31
sub     r0, r1, r0, asr #31

都是兩個算數指令, 速度應該一樣吧。我沒裝 ARM 開發環境, 應該沒寫錯吧。藉機練習寫看看, 體會指令附加 branch 以及 barrel shifter 的便利之處。指令詳細說明, 見這篇的 "23.3.2. Data instructions"。"r0, asr #31" 那部份是 Op2, 可在同一 cycle 內取得 shift 後的值。

2011年10月4日 星期二

Django 的 runserver 如何實作 autoreload

做完上篇的勘查後, 好奇之下順便看一下 runserver 怎麼實作 autoreload。

看 core/management/commands/runserver.py 會看到 autoreload.main(inner_run)。然後看 utils/autoreload.py, 稍微看原始碼, 配合 pdb觀察行為, 還滿簡單的。

inner_run 是真正處理 web request 的函式, autoreload 的行為如下:

  1. 在執行 inner_run 前, 先 fork 出 child process
  2. 讓 child process 開另一個 thread
  3. parent process 等待 fork 出來的 child process 結束
  4. child process 的一個 thread 每秒掃一次所有載入的 module, 看看原始碼是否有更新。有的話, 結束這個 process, 傳回 exit code 3
  5. child process 的另一個 thread 執行 inner_run
  6. parent process 發覺 child process 的 exit code 為 3 時, 重 fork 一次, 然後回到步驟 2

要注意的是, 要用 parent process 重 fork, 而不是 child process 自己再 fork 新 process。這樣才能確保由原本乾淨的狀態重讀全部 modules (parent process fork 完就沒做事了, 確保狀態沒有一絲改變)。

偵測程式更動的程式是 code_changed(), 關鍵部份是如何找出所有相關程式:

filter(lambda v: v, map(lambda m: getattr(m, "__file__", None), sys.modules.values()))

然後將 .pyc, .pyo 改為 .py, 接著記錄所有 py 檔的上次更新時間, 留待下次呼叫此函式時比對。從這段程式可看出, 包含 site-package 或 virtualenv 裡的程式也在偵測範圍內, 還挺方便的。

小心 Django 的 ADMIN_MEDIA_PREFIX

踏到這個雷兩次了, 記錄一下。

在使用 runserver 開發程式時, runserver 會特別處理 ADMIN_MEDIA_PREFIX, 先處理 prefix 為 ADMIN_MEDIA_PREFIX 的路徑, 再將其它情況交回給原本 urls.py 裡定義的 handler。

這導致當自己的靜態檔案放在 media/ 下, 又忘了改 ADMIN_MEDIA_PREFIX 時 ( settings.py 預設為 media/), 會讀不到自己 media/ 下的檔案, 而出現 "Page not found: ..." 的錯誤訊息。

從以下兩個現象, 推測中間有人先處理掉 request:

  • browser 有送出 request 但得到 404
  • runserver 的 console 沒輸出訊息表示有收到 request 並回覆 404

所以推測有人從中做梗!!

解法很簡單, 就是將 ADMIN_MEDIA_PREFIX 隨便設一個其它名稱, 像是 "admin_media"。之前有人發 issue 請 Django 改掉 django-admin.py 的值, 不過下場是 wontfix。

相關的程式從 core/management/commands/runserver.py 找到 core/servers/basehttp.py, 然後看 AdminMediaHandler.call 的部份。

這段先看是否符合 ADMIN_MEDIA_PREFIX , 不是的話就由原本使用者寫的方式處理:

        # Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore
        # all requests if ADMIN_MEDIA_PREFIX isn't a relative URL.
        if self.media_url.startswith('http://') or self.media_url.startswith('https://') \
            or not environ['PATH_INFO'].startswith(self.media_url):
            return self.application(environ, start_response)

接下來是針對 ADMIN_MEDIA_PREFIX 的特殊處理, 也只是看檔案、讀檔案而已:

        # Find the admin file and serve it up, if it exists and is readable.
        try:
            file_path = self.file_path(environ['PATH_INFO'])
        except ValueError: # Resulting file path was not valid.
            status = '404 NOT FOUND'
            headers = {'Content-type': 'text/plain'}
            output = ['Page not found: %s' % environ['PATH_INFO']]
            start_response(status, headers.items())
            return output
        if not os.path.exists(file_path):
            status = '404 NOT FOUND'
            headers = {'Content-type': 'text/plain'}
            output = ['Page not found: %s' % environ['PATH_INFO']]
        else:
            try:
                fp = open(file_path, 'rb')
            except IOError:
                status = '401 UNAUTHORIZED'
                headers = {'Content-type': 'text/plain'}
                output = ['Permission denied: %s' % environ['PATH_INFO']]
        ...

2011年9月30日 星期五

用 Chrome console 從鎖右鍵的網站中取得文字

遇到鎖右鍵的網站時, 複製文字變得有點麻煩, 要檢視原始碼再去掉 tag。剛從 Shakalaca 那看到《Become a Javascript Console Power-User》, 學到不錯的方式, 比較省事地取出文字:

  1. 按 F12 開 Chrome console
  2. 點 Elements 選放大鏡
  3. 選取要的段落 (html node)
  4. 在 console 裡打 copy($0.innerText)

這樣就複製好了, 只剩貼到文字編輯器裡去掉不要的部份即可。

2011年9月28日 星期三

C 的型別宣告

直到看了 Dan Saks 的 const T vs T const, 才(比較)明白 C 的型別宣告, 知道 int const *a 和 int * const a 的差別。原本雖然知道 int (*f[10])(int )、int (*f)(int) 和 int *f(int) 的差別, 但沒有很清楚細部原因, 看了這篇後才懂得更徹底。

C 的宣告分幾個部份, 以 static unsigned long int *x[N] 來說, *x[N] 是 declarator, 剩下的是 declaration specifier。declarator 的部份用運算子優先順序來解讀, 語意清楚許多。即 () 最優先, 其次為 [], 再來為 *。所以:

  • int (*f)(float) 表示 f 是函式指標, 指向一個函式其傳回值為 int, 參數為 float
  • int *f(float) 表示 f 是函式, 傳回 int*, 參數為 float
  • int (*f[10])(float) 表示 f 是一個陣列, 每個元素是一函式指標

令人混淆的部份在於, declaration specifier 的順序其實沒有影響。所以, static unsigned int n 和 int unsigned static n 是一樣的。再加上 const 和 volatile 不同於其它 declaration specifier, 它們可以同時寫在 declarator, 於是產生不易理解的結果。

我還在參透 volatile 的效果, 這裡先以 const 為例:

  • int const n 和 const int n 都表示 n 是指向唯讀的 int
  • int const *n 表示 n 是指標, 指標指向的型別為唯讀的 int
  • int * const n 表示 n 是唯讀的指標, 指向 int
  • int const * const n 表示 n 是唯讀的指標, 且 n 指向唯讀的 int

簡單的判別方式是以 * 為分界點, 由右往左看, 包含 * 的右邊是 declarator, 表示 declarator 的型別, 所以這裡有 const 表示是唯讀的指標; * 的左邊是 declaration specifier, 這裡有 const 表示指到的型別為唯讀。附帶一提, 用英文來解讀的話似乎比較清楚, 不知是自己受程式語言的影響, 還是英文天性比較有邏輯一點, 總覺得用英文思考比較有利於理解數學、程式之類的事。

比方說:

  • int *p: p is a pointer to a int
  • int *p[N]: p is an array of pointer to int (用用運算子的優先順序看)

善用 const 產生 immutable 的效果可以減少 bug、易於維護, 學好這些觀念滿實用的。作者建議將 * 貼近 declarator 並且將 const 寫在最右側, 避免誤會。 * 貼近 declarator 是因為 compiler 本來就是這麼解讀的, 將 * 放在 int 旁會不易分辨 declaration specifier 和 declarator。而將 const 寫在最右側則可避免誤會, 像文中的例子:

typedef void *VP;
const VP vectorTable[]

原以為表示 vectorTable 的內容都為 const, 但 compiler 會解讀成

VP const vectorTable[];
// 等同於
void * const vectorTable[];

變成 vectorTable 是一個陣列, 每一個元素是唯讀的指標, 指標指向的型別為 void (即沒指定特定型別)。若希望宣告 vectorTable 的指標不為唯讀, 是指到的型別唯讀, 有兩種作法:

// 1.
void const * vectorTable[];
// 2.
typedef void const *VCP;
VCP vectorTable[];

寫完這篇後讓我想到 int **p 的情況, * 和 * 之間可以插三個 const, 這才體會到作者強調的觀念:

Although C and C++ read mostly from top-to-bottom and left-to-right, pointer declarations read, in a sense, backward.

用這種方式解讀 int const * const * const p 的確比較直覺, p is a read-only pointer to a read-only pointer to a read-only int。

ps. const-correctness 提到更完整的 const 語意, 包含函式的部份。

2011年9月27日 星期二

用 LD_PRELOAD 替換動態連結的函式庫

換掉動態連結的函式庫有不少用途, 比方說像 Scott 提的「用來驗證是否執行到特定函式」, 或如 jserv 提的「在沒有原始碼的情況下修補執行檔的行為」。最近又看到這個東西, 記一下筆記以免忘了。

來看程式碼和執行效果。

mylib.c

#include <stddef.h>
#include <stdio.h>

int putchar(int c) {
    printf("call putchar()\n");
    return c;
}

void *memset(void *s, int c, size_t n) {
    printf("call memset()\n");
    return s;
}

main.c

#include <string.h>
#include <stdio.h>

int main(void) {
    char s[10];
    memset(s, 0, 0);
    putchar('X');
    putchar('\n');
    return 0;
}

執行結果

$ gcc -Wall -fpic -shared -o libmylib.so mylib.c
$ gcc -o main main.c
# 沒用 LD_PRELOAD 的結果
$ ./main
X
# 用 LD_PRELOAD 後的結果, 記得加 "./", 不然會找不到 libmylib.so
$ LD_PRELOAD=./libmylib.so ./main
call putchar()
call putchar()
$ strings main | grep memset

可以看出 putchar 有被換成自己編的版本, 但 memset 沒有。用 strings 查看 binary 檔 main 會發覺並沒有呼叫 memset 的跡象。猜測是 compiler 發覺 memset 實際上沒任何作用, 所以沒呼叫。

修改 main.c, 將 memset(s, 0, 0) 換成 memset(s, 0, 1) 再來看看:

$ gcc -o main main.c
$ strings main | grep memset
memset
$ LD_PRELOAD=./libmylib.so ./main
call memset()
call putchar()
call putchar()

結果顯示有換到 memset, 表示之前是完全沒呼叫 memset, 才會以為沒換到。printf 也是如此, 只輸出字串時, 不會呼叫 printf, 而是用 puts。

參考資料: 《Modifying a Dynamic Library Without Changing the Source Code | Linux Journal》

2011年9月26日 星期一

安裝 PythonWebkit

偶然看到 PythonWebkit 這個專案, 作者花了兩週左右的時間寫好 WebKit 的 python 介面, 於是操作 DOM 不再是 javascript 的專利, 透過 PythonWebkit 就能用 python 操作 DOM。作者自稱除一些小功能外, 這個專案已到可用的階段:

The current status is that the SVG 2D Canvas element is not available, and CSS Styles have to be modified through the style.setProperty function (rather than being accessible as python properties). Other than these gotchas, the Python Webkit bindings are in a useable state, providing full W3C compliant access to the full set of DOM functionality (except SVG Canvas) as provided by Webkit, and ordinarily only accessible via javascript.

不過相關文件超少, 花了些時間才裝成功, 也增加了一些編原始碼的功力。

背景介紹

WebKit歷史很有意思, 有興趣的人自行參照連結。在 Linux 上目前有兩種 port, 一個是用 GTK+ 實作的WebKitGTK+, 另一個是 QT 的實作版本。

PyWebKitGtk 是另外一個基於 WebKitGTK+ 的專案, 透過 WebKit 提供的 gobject binding 提供 python 介面, 但沒有提供直接操作 DOM 的功能。雖然有些補救方案, PythonWebkit 的作者覺得直接改出 python 介面比較省事, 效率也較好, 該頁有寫詳細的設計考量。所以, PythonWebkit 是以 WebKitGTK+ 為底, 修改部份內容直接提供 python 的 DOM API。

剛開始作者用 PyWebKitGtk 的 python module "webkit", 藉此省掉寫 python module 的步驟, 然後加上 patch 讓 webkit (python module) 能取出 DOM。也就是說, PythonWebkit 依賴 PyWebKitGtk; PyWebKitGtk 依賴 WebKitGTK+; WebKitGTK+WebKit 在 Linux 下的實作版本之一。

後來作者自己重寫一個 python module "pywebkitgtk", 就不用再裝 PyWebKitGtk 了。網路上找 PythonWebkit 的文件, 有部份會提到 PyWebKitGtk, 但現在已用不到它了。弄清楚這堆名稱差不多的專案, 花了我一些時間 ..., 搞清楚後也滿有成就感的。

上面是簡化的說法, 實際上 PythonWebkit 不是直接依賴 WebKitGTK+, 也可以改用基於 DirectFB 的 PyWebkitDFB。由於這還在實驗階段, 我就沒試了。

安裝方式

主要參考 PythonWebkit 的說明, 另外需要參照 WebKitGTK+ 的安裝說明, 了解它用到的套件。在 Ubuntu 下的安裝步驟如下:

1. 升級到 11.04, 11.04 的 glib 版本才夠新 (>= 2.27.4)

不然會出現

configure: error: You need the GLib dev tools in your path

2. 安裝編譯 WebKitGTK+ 需要的套件

注意: 最後三個指令不能用 aptitude, 只有用 apt-get 才能確保移除後仍保有需要的設定

$ apt-get install python-dev python-ply
$ apt-get build-essential
$ apt-get build-dep libwebkit-1.0-2
$ apt-get install libwebkit-1.0-2
$ apt-get remove libwebkit-1.0-2

在 Ubuntu 下自己編原始碼時, 似乎滿常用這招的, 透過原有的套件管理系統裝好相依的套件、取得需要的 header 等資源, 藉此減少手動操作部份。

3. 取得 PythonWebkit 原始碼, 這步會花很長的時間, 好像會到一小時吧

$ git clone git://git.savannah.gnu.org/pythonwebkit.git

4. 切換到 branch python_codegen

$ git checkout python_codegen

注意: 千萬不要加 "-b", 我不熟 git 參數, 誤加了 -b, 變成開一個新的 local branch 叫 python_codegen, 而不是取出遠端的 branch python_codegen。git 指令請參考《Git 版本控制系統(2) 開 branch 分支和操作遠端 repo.》

有取對 branch 的話, grep 整個目錄應該會找到 pywebkitgtk 相關檔案; 反之則否。pywebkitgtk 是 PythonWebkit 產生的 python module, 作用和 PyWebKitGtk 的 webkit 差不多, 不過多了取得 DOM 的方法 (web view 的 GetDomDocument())。

5. 開始編譯

$ mkdir build
$ cd build
$ ../autogen.sh
$ make

我的電腦是三年前買的, 在 VM 裡用雙 CPU 編的話 (配合 make -j 2), 大概四十多分鐘可編完。

執行 autogen.sh 時, 若發現缺了東西, 可用 aptitude search 加關鍵字, 或 apt-file search xxx.h 查看看放在那個套件, 通常是要裝一些 X-dev 的 package。印象中我好像有少了 pygtk。有問題時, 記得用 dpkg -l 和 WebKitGTK+ 的 Dependencies 對一下, 看有沒有少裝什麼。然後再重跑 autogen.sh。

6. 檢查結果

編好的東西放在 .libs/ 下, 不知是不是某種慣例。上網查 make install 的討論, 似乎沒有明確的方法可以找到會裝那些東西進系統。幸運的話, 可以用 checkinstall 裝成 deb 檔, 就能用 dpkg -L 查裝了什麼, 也能用 dpkg -r 移除。不過我用 checkinstall 的結果, 似乎比直接 make install 多裝了不少東西, 最後直接用 make install。

回到正題, 先切到 .libs 下看編完的東西是否可用。.libs/ 下應該要看到 libwebkit 的 so 檔, 還有 pywebkitgtk.so。接著在 .libs/ 目錄下執行 ipython, 依序打入:

import gtk
import pywebkitgtk
url = "http://www.gnu.org/software/pythonwebkit"
wv = pywebkitgtk.WebView(1024,768, url=url)
doc = wv.GetDomDocument()
ns = doc.getElementsByTagName('h2')
ns.length  # 9
ns.item(0).innerHTML  # 'Why is this important - why is it a "big deal"?'

注意, 上述程式要在 X Window 下執行。import gtk 後, 在產生 WebView 物件時會跳出視窗。若沒有 import gtk 的話, 不會有任何錯誤訊息, 但取得的 doc 裡面卻沒任何東西, ns.length 會是 0。

7. 安裝到系統

$ sudo make install

然後檢查 /usr/local/lib 是否有在 ldconfig 路徑中:

$ grep -R "/usr/local/lib" /etc/ld.so.conf.d/

若沒有的話, 在 /etc/ld.so.conf.d/ 下新增一個檔案:

$ sudo echo "/usr/local/lib" > /etc/ld.so.conf.d/webkit.conf

最後是更新 ldconfig 的 cache:

$ sudo ldconfig

不確定原本 /etc/ld.so.conf.d/ 下就有 /usr/local/lib 的情況是否要執行 ldconfig, 好像要執行才會 ok, 但看文件說, 一般 make install 時應該會自動執行這個動作。

之後就能在任何地方直接 import pywebkitgtk 啦。

用法

附上一個小例子: 取出 TechCrunch 文章有多少人按 Facebook 的 like:

import os

import gtk 
import pywebkitgtk

wv = pywebkitgtk.WebView(10, 10, url='http://techcrunch.com/2011/09/25/clinging-to-friction-some-thoughts-on-facebooks-f8/')
os.sleep(10)  # Wait Facebook plugin to be loaded.
dom = wv.GetDomDocument()
iframes = dom.getElementsByClassName('fb_ltr')
for i in range(iframes.length):
    e = iframes.item(i)
    ns = e.contentDocument.getElementsByClassName('connect_widget_button_count_count')
    if ns.length > 0:
        print ns.item(0).firstChild.nodeValue  # Count of "FB like"

TechCrunch 的推文數大多是用 AJAX 載入到 iframe 裡, 用普通的方法無法抓到內容。上面的作法相當粗糙, 就將就用啦, 這不是本篇的重點。

產生 wv 後會看到 console 出現一堆訊息, 大概是載入頁面後, 以及執行完後續 AJAX 時產生的。之後再來看看這些訊息是否有害。

PythonWebkit 沒有多提相關 API, 因為作者為了方便大家查文件, 讓 PythonWebkit 和原本的 DOM API 100% 相容 (目前只有 CSS 部份有一點不同), 所以, 直接學 DOM 就可以了。配合 ipython 直接操作, 還滿容易試的。

Trouble shooting

1. configure: error: You need the GLib dev tools in your path

參考 這篇, 要用新版 glib, Ubuntu 要到 11.04 才符合。我鐵齒的從 10.04 一路試, 直到 11.04 才真的 ok。不過 libsoup 用 2.24 也 OK, 不需要像 PythonWebkit 說要用最新版才行(2.29.90), 不確定之後會有什麼問題。

2. 編譯時出現 "error: webkitmarshal.h: No such file or directory"

參照這篇, 執行

$ touch ./Source/WebKit/gtk/webkitmarshal.list

然後重跑 make。

3. 無法 import pywebkitgtk / 沒有編出 pywebkitgtk.so

記得在取得 repository 後要先執行 "git checkout pyhton_codegen" 才執行 ../autogen.sh, 千萬不要寫成 "git checkout -b pyhton_codegen"

4. import pywebkitgtk 時說找不到 libwebkitgtk

In [1]: import pywebkitgtk
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
/usr/local/lib/<ipython-input-1-8dab5af5d34b> in <module>()
----> 1 import pywebkitgtk

ImportError: libwebkitgtk-python-1.0.so.0: cannot open shared object file: No such file or directory

先用 ldd 檢查路徑是否確實有問題:

$ ldd /usr/local/lib/python2.7/dist-packages/pywebkitgtk.so | grep libwebkitgtk
	libwebkitgtk-python-1.0.so.0 => not found

出現 "not found" 表示 pywebkitgtk.so 找不到 libwebkitgtk-python-1.0.so.0。接著檢查 /etc/ld.so.conf.d/* 有沒有含 /usr/local/lib (當然, 要先確定你的 libwebkit-python-1.0.so 有裝到那)

將 /usr/local/lib 加到 ldconfig 的路徑後, 執行 sudo ldconfig

另外看到有人提到在編譯前這麼做:

$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig

不過我試了沒成功, 之後有空再來看 pkgconfig 是什麼。

其它參考資料和雜項心得

  • Program Library HOWTO: 動態函式庫的相關知識, 有提到 LD_LIBRARY_PATH 和 LD_DEBUG, 必要時可協助除錯。
  • 作者個人網站, 有他的 e-mail。原本想說再試不出來就寄信問看看。話說找這個也花了點時間, PythonWebkit 整篇都沒提到他是誰啊 ..., 在這封信才看到多一點情報。
  • 有問題時, 可以看看 commit log, 了解主要的修改為何。從這次的經驗, 發覺除系統安裝的知識外, 操作 VCS 的知識和讀 commit (changeset) 的能力也很重要。

HTML DOM 筆記

有了 pythonwebkit 後, 可以用 python 直接存取 DOM。於是得自己學一下 DOM 的操作方式, 不能再依賴 jQuery 啦。

花個半小時邊看邊操作 《HTML DOM Tutorial》, 很快就有個基本概念了。記一下自己不熟的部份:

  • DOM 的 node 類型有: document, element, text, attribute, comment
  • element node 沒有包含文字, 要從它的 text node 拿:
    • document.getElementsByTagName('p')[0].nodeValue # null
    • document.getElementsByTagName('p')[0].firstChild.nodeValue # the expected texts
  • 操作 iframe 的內容
    • 取得 iframe 的 DOM: d = document.getElementsByTagName('iframe')[0].contentDocument
    • 接著就能用一樣的 DOM API 操作 d
    • 我只有在 Chrome 下試, 畢竟 pythonwebkit 的功能應該和 Chrome 差不多才對。要跨瀏覽器操作 DOM 的話, 得多試幾種變數, 取出 iframe 的 DOM

2011年9月23日 星期五

vim 停住一會兒才啟動的解法

有時用 putty 連到 server, 打 vi 後會停一兩秒才進去。有時在 screen 下甚至會停住不動。參考這篇的說法, 用 vi -X 不要連線到 X Server 就 ok 了。

會有這樣的問題, 是因為我用到 gnome 版的 vim (vim.gnome):

$ which vi
/usr/bin/vi
$ ll /usr/bin/vi
lrwxrwxrwx 1 root root 20 2010-05-12 12:17 /usr/bin/vi -> /etc/alternatives/vi*
$ ll /etc/alternatives/vi
lrwxrwxrwx 1 root root 18 2010-08-06 13:58 /etc/alternatives/vi -> /usr/bin/vim.gnome*
$ ll /usr/bin/vim.gnome
-rwxr-xr-x 1 root root 2144568 2009-01-08 11:03 /usr/bin/vim.gnome*

若用 vim.tiny 或 vim.basic, 不加 -X 也不會停一會兒。

相關除錯指令:

  • vi -V 會看到「讀取 viminfo 檔案 "/home/fcamel/.viminfo" 訊息開啟 X Window 失敗」 (Reading viminfo file "/home/fcamel/.viminfo" info oldfilesOpening the X display failed)
  • 在 X Window 下無此問題

安裝完 Ubuntu 後的初步設定

2017-04-04 更新

記一下個人裝 Ubuntu 後, 會做的基本設定。

  1. sudo apt-get install aptitude
  2. sudo aptitude update
  3. sudo aptitude install build-essential apt-file checkinstall manpages-dev htop rsync screen cdargs openssh-server mosh vim git subversion exuberant-ctags cscope python-dev python-setuptools id-utils python-pip
  4. sudo apt-file update
  5. git clone git://github.com/fcamel/configs.git
  6. cd configs && ./install.sh # 裝我個人用的設定檔, 如 vim, bash, screen

若需要裝 java, 另外需做:

  1. sudo aptitude install python-software-properties
  2. sudo add-apt-repository "deb http://archive.canonical.com/ natty partner"
  3. sudo aptitude update
  4. sudo aptitude install sun-java6-jdk

若是用 Parallel 的話,裝完 Parallel 附的 Ubuntu 後要作的事:

  • 修改網路:Actions -> Configure -> Hardware -> Network -> 選 Ethernet,這樣會自己有獨立的 LAN IP
  • 新增硬碟空間:在關閉 OS 的情況下,Actions -> COnfigure -> Hardware -> Hard Disk 1 -> Edit,然後調整大小。作完後重開 VM。再用 gparted 動態調大 partition 大小。需要先刪掉 swap, 再刪 extended partition,才有空間調 root partition。

日後想到什麼再補。

Ubuntu 用 UUID 掛載硬碟的原因

之前看 Ubuntu 的 fstab 改用 UUID 覺得很納悶, 為啥要在 /dev/disk/by-uuid/ 下建 soft link 再指到 /dev/sdXN (X 是 abcd, N 是 1-8), 然後在 fstab 裡用 UUID 而不用 /dev/sdXN。

今天用 VirtualBox 移除硬碟後才發覺它的好處: 加裝或移除硬碟後, 雖然 /dev/sdXN 的 X 會變 (如移掉第二顆硬碟, /dev/sdc3 會變 /dev/sdb3), UUID 卻不會變。所以若 /etc/fstab 用 UUID 來掛載 partition, 可以直接開機, 而省掉進單人模式改 /etc/fstab 的步驟。

2011年9月12日 星期一

tab vs. space

討論 coding style 時, 這個常會戰到爆。記一下在多人一起開發一陣子軟體後的心得。

我後來覺得 space 比較方便, 因為可確保任何地方看到的畫面一致, 且不需另外設定。支援 tab 的人認為只需要一次設定就可以了, 這樣可保留彈性。困難的地方在於有太多地方可能會看到程式碼: IDE、editor、VCS log、issue tracking、e-mail、其它可能的 web-based 工具 (如 code review)。而有些地方無法提供客制化設定 tab 寬度 (如 e-mail)。

以我自己用 Java 來說, 平時可能用 Eclipse, 特定需求時用 vim, 偶而要用 hgtk 看別人的 commit (一些程式碼的 diff), 或用 hg blame 看修改記錄 (輸出到 console 或用 hgtk 看), 偶而寄 code diff 給別人討論。在這些過程中, 會發覺 tab 實在是不太方便。

至於 space 的缺點: 一但定了就很難改, 實務上不是問題, 各個語言都有標準規範 (大概都是四個空白), 照著公定規範做就是了。若不幸地原本已有一大堆其它長度, 讀個一陣也會習慣, 應該會比在不同地方, 縮排亂掉來得好。

MySQL vs. PostgreSQL

去年的隨手心得, 記在這備忘。主要是參考《MySQL vs PostgreSQL - WikiVS》 2010-09 的版本, 後來情況應該也有些變化。另外, 很多東西還是要自己測過才準, 後來和一些很熟 DB 的前輩聊, 發覺實際情況還是和看到的有些出入。需求不同, 又有太多細節可以調整了。

後來覺得選用 MySQL 的好處是:

  • 資源最豐富, 有書有文件, 也有一堆顧問公司 (如 Percona)
  • 有許多大公司實際執行到海量的實例, 不用擔心會流量會卡住 (雖說大部份時候應該要先擔心服務不夠好, 流量太小)
  • 有些 syntax sugar 或特制功能很好用, 像是 insert ... on duplicate update , 或 insert ignore
  • 有趣的是, 該文認為 MySQL 比 PostgreSQL 流行的原因之一, 是因為 PostgreSQL 沒法限制各個 database 的大小, 對於商用服務來說, 這點很重要 (商用服務會在意限制資源、管理權限等, open source 專案先以個人需求為主, 一開始大概都不會重視這些吧)

備忘

fcamel 2010-09-01 19:54:43
MySQL vs. PostgreSQL, 有這種對照文真不錯, 書上有看過的相關觀念可以重新消化一次, 之前沒看過的....在這也有看沒感覺
MySQL vs PostgreSQL - WikiVS
MySQL vs PostgreSQLFrom WikiVS, the open comparison websiteJump to: navigation, search MySQL Postgr
發表回應
1

fcamel 2010-09-01 19:55:43
PostgreSQL 只有一個 engine, 一般來說在大量同時寫入下會比 MySQL 搭各種 engine 快
2

fcamel 2010-09-01 19:56:36
PostgreSQL 比較穩, RDBMS 功能較齊全, 這點在 stackoverflow 上可找出一卡車支持者。也可找出一卡車 MySQL 使用者說他們遇到 DB 炸了多少次
3

fcamel 2010-09-01 19:57:24
MySQL + MyISAM 有無敵快的 count, 因為它沒 MVCC, 可以視情況直接 count index 而不用取 data rows。又因為沒 MVCC, 可以安心地 cache count
4

fcamel 2010-09-01 19:58:58
8.x 的 PostgreSQL 沒有 Insert Ignore / Replace
5

fcamel 2010-09-01 20:00:31
MySQL + MyISAM 在 read-only 或低頻率寫入的情況下是超級快, 因為它沒 constraint, 沒 MVCC。無賴戰法: 我就什麼功能都沒有, 但是只讀不寫的話, 我超快的哦!!
6

fcamel 2010-09-01 20:00:49
剛好我們現在正好只需要這種無賴戰法 XD
7

fcamel 2010-09-01 20:06:12
mysql 內建 replication 機制, 效率比 PostgreSQL 好 (?)
8

fcamel 2010-09-01 20:07:41
Ubuntu 8.04 的 mysql 版本有點舊, 只到 5.0.51a, 之後有幾版大幅提昇速度, 5.1 又有多 sharding 的樣子。
9

fcamel 2010-09-01 20:10:56
PostgrelSQL 有 Bitmap Indexes, 可以合併兩個以上的 indexes, 之後來看看這是啥。一直很好奇怎麼做才能同時用兩個 indexes
10

fcamel 2010-09-01 20:13:45
MySQL 有 covering index, 而 PostgrelSQL 沒有, 令我滿意外的, 原以為 index 那區 MySQL 會兵敗如山倒
11

fcamel 2010-09-01 20:15:14
PostgreSQL 支援 expression indexes, 意思是也可以查 "%keyword%" 嗎? 太神奇了! 之後來查查這是啥
12

fcamel 2010-09-01 20:19:32
這個有意思: "In PostgreSQL there is no built-in mechanism for limiting database size. This is the main reason why the most of the web hosting companies are using MySQL. Also, PostgreSQL doesn't have scheduled procedures. "
13

fcamel 2010-09-01 20:20:32
結果 PostgreSQL 無法流行的主因是沒做這種看似不重要的功能嗎? XD
14

fcamel 2010-09-01 20:23:40
天啊...., 難怪有人提到他比較喜歡 PostgreSQL 的 license, MySQL client library 是 GPL 非 LGPL!! 若沒和 Sun (現在是 Oracle?) 買授權, 就得 open source, 太可怕了...
15

fcamel 2010-09-01 20:30:10
看完兩者的討論後還真想用 PostgreSQL 啊, 考量現況還是繼續用 MySQL 比較務實 Orz
16

fcamel 2010-09-01 20:41:29
stackoverflow 有人提到用 PostgreSQL 加 column 似乎不用鎖住 table, 他們之前用 mysql + replication 的結果, 就不敢加 column, 只好另開 table 用 join 取資料
17

榮尼王 2010-09-01 20:53:54
mysql 加上 master-master 架構的話,alter table 就隨便了啊
18

fcamel 2010-09-01 21:04:56
soga, 還沒看深, 果然各種問題都有一定的配套解法, 謝啦。
 回應

讓 mysql 使用 utf-8 的相關設定

這對於新手來說應該很有幫助吧

在 [mysqld] 下的相關設定:

character-set-server=utf8
collation-server=utf8_general_ci
init-connect='SET NAMES utf8'

MySQL 的設定檔位置不太好找, 細節參考《fcamel 技術隨手記: 更改 mysql conf 的注意事項》

warm up server 的一般作法

一開始用 MySQL 的 MyISAM 時, 發覺有語法可以事先載入整個 index, 就以為大部份 server 應該有提供這功能。不過後來發覺 InnoDB 沒有, 所以要自己下 SELECT 掃一次 table, 讓 MySQL 自動觸發 cache 機制。後來用 Solr, 查了一陣子, 似乎也沒有這種機制。大概設計 server 的人覺得另外設計語法會增加維護成本, 不如讓大家自己查詢全部要用的資料, 讓 server 自己管好 cache 比較實際吧。

mysql flush key cache

MyISAM 的 index 會暫存到 key cache 裡, 若想預先載入整個 table 的 indexes 到 key cache 裡, 可以用 LOAD INDEX INTO CACHE。但若要 flush key cache 的話, 並沒有像 flush query cache 那樣有 RESET QUERY CACHE 的語法。替代作法是, 改變 key cache 的大小或 block size, MySQL 就會重設 key cache。

2011年9月6日 星期二

使用 ReviewBoard API 刪掉多個 request

ReviewBoard 沒有提供批次刪掉的介面, 測試期間產生一堆 request, 懶得用手一個個刪, 想說用 API 來刪。

使用 ReviewBoardweb api, 需要在連線中的 http header 加上 cookie 資訊用作身份認證。用 python 的 httplib2 做這事也不會太麻煩, 不過若能用 command line 解決, 會更簡單一些。後來我用 curl 做這事, 順便熟悉一下它的功能, 常看文件用 curl 示範 API 用法。

參考《Use CURL to Login to Websites with a Script》的教學, 學會 curl 登入網頁的用法。依我的需求消化後的步驟如下:

  1. FireBug 之類的工具找出 login form 需要的欄位。
  2. 連到 web server 產生 coolkie: curl --cookie-jar cjar http://myhost/account/login/
  3. 送出 username/password 登入, 並更新 cookie (存到 cjar 裡): curl --cookie cjar --cookie-jar cjar http://myhost/account/login/ --data 'username=fcamel' --data 'password=xxxx'
  4. 使用 FireBug 觀察 ReviewBoard 刪除 request 時連的網址 (或是看 web api 文件)
  5. 執行指令砍掉編號 1 ~ 10 的 request: for i in {1..10}; do curl --cookie cjar --cookie-jar cjar http://myhost/api/json/reviewrequests/$i/close/discarded/; done

備註: 砍 request 要用 http://myhost/api/json/reviewrequests/$i/delete/, 但要有 admin 權限

在同一個 apache 裡跑兩個 port

使用 apache2 + mod_wsgi 的作法架 ReviewBoard 的話, relative path 有 bug, 無法將 ReviewBoard 架在 http://.../reviews/ 這樣的位置。網路上搜搜看到也有人提到同樣的問題。自己試著硬解了一陣子, 沒有很乾淨又一致正確的結果。後來想說, 乾脆在同一台機器的 apache 裡跑聽兩個 port 好了, 這樣就能在用專屬非 80 的 port 來架 ReviewBoard

查了一下, 架法還滿簡單的: 見 VirtualHost Examples, 另外 CommonMisconfigurations - Httpd Wiki 也滿有幫助的。使用多個 port 時, 記得要加 listen。Ubuntu 的話是加在 /etc/apache2/ports.conf 裡, 它會被 /etc/apache2/apache2.conf include 進去。

2011年8月29日 星期一

Synchronization and the Java Memory Model 筆記

昨天看 Effective Java 提到 synchronization 同時提供 exclusive execution 和 communication, 但多數人會忽略後面這點。原本不懂這是什麼意思, 看了《Synchronization and the Java Memory Model》才知道是怎麼回事, Java Memory Model 真的和直觀的想法很不一樣。記錄一下筆記。

final class SetCheck {
  private int  a = 0;
  private long b = 0;

  void set() {
    a =  1;
    b = -1;
  }

  boolean check() {
    return ((b ==  0) ||
            (b == -1 && a == 1)); 
  }
}

這段程式有可能 return false, 因為 set() 裡的 a、b 設值可能會順序相反, set 和 check 可能同時被交錯執行。Java 只有保證單一 thread 自己執行的時候, 看起來像按順序由上而下、由左而右執行 (文中用 as-if-serial 表示, 這詞還滿妙的)。

在 multi-thread 時, 情況不同, 各個 thread 有自己的 memory, Java 沒有保證什麼時候 thread 之間才會看到更新後的情況, 這點滿可怕的。但可以用 synchronization lock 或宣告 volatile 強迫 thread 之間同步資料。我想這就是書上說 communication 的意思。

Java 有保證除 long 和 double 外的欄位, 更新值的時候是 atomic 的 (也就是包含 reference), 而 volatile 宣告的欄位也都是 atomic, 包含 volatile long 和 double。這裡要注意幾點:

  • i++ 包含 i + 1 和設值兩個操作, 沒有 atomic。所以設 volatile 也會有危險, 有需要的話還是要用 synchronized, 或照 Effective Java 的建議, 看能不能用 java.util.concurrent.atomic 的類別滿足需求。
  • long、double 外的欄位有 atomic, 但沒保證更新後其它 thread 會讀到新值。若要其它thread 不會讀到 stale value, 要宣告 volatile, 或在讀取前加上 synchronized。
  • array 有 volatile 不表示裡面的元素有 volatile (這還算直覺)。

後面說明各種特殊情況, 還有強調因為 CPU 管理 cache 方式, 有些時候其實 JVM 沒有保證 thread 之間的資料有更新, 但執行起來卻像有這一回事, 讓這種 bug 更難重製。

Visibility 那段值得細讀, 說明什麼情況下可確保 thread 之間有取到或寫入最新的值。像 start Thread 後會確保該 thread 讀到最新的值, 所以, 盡量在 start() 前建立物件, 還有別在 constructor 內 start thread, 避免 thread 執行後取得不完整的狀態。

2011-09-06 更新: 備忘簡短易懂的入門文件: Java Concurrency / Multithreading - Tutorial

2011年8月27日 星期六

Effective Java 讀書筆記: Item 66 - 用 synchronized 確保 concurrency

這節提供一個不直覺的錯誤

package trial;

import java.util.concurrent.TimeUnit;

public class Trial {
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                    System.out.println(i);
                }
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

作者說在沒用 synchronization 機制的情況下, Java 沒有保證 thread 什麼時候看到彼此之間的修改。上面的例子, 在作者的機器上不會停。不過我自己試的結果是一秒後結束。作者說原因是 JVM 有可能最佳化 while 那行, 變成只檢查一次 stopRequested, 結果就不會停了。這個 VM 最佳化技巧叫做 hoisting (文中的例子則是改用 stack 上的變數取代 heap 上的)。

解法是宣告 stopRequested 時加上 volatile。不過作者後來接著強調 volatile 很難寫對, 叔叔有練過, 小朋友別亂用 volatile, 乖乖用 java.util.concurrent.atomic 下的類別, 像是 AtomicLong。這個 package 下的東西用了 machine-level 的方法減少 lock overhead, 比用 synchronized 有效率。

當然, 保險的話, 讀寫時都用 synchronized 最安全, 不過就會損失一些效率了。

shell script 處理含空白字元的檔名

以前我習慣寫

for f in $(ls mydir)
do
    ...
done

但遇到檔名有含空白時就炸了, 像這樣

$ ls mydir/ | cat
a b
c
$ for f in $(ls mydir); do echo $f; done
a
b
c

解法是設 IFS 這個變數

$ IFS=$'\n'
$ for f in $(ls mydir); do echo $f; done
a b
c
unset IFS

也可用這個方式在 shell script 裡做到等同於「字串陣列」

files=$(cat <<EOF
1st line
2nd line
3rd line
EOF
)
IFS=$'\n'
for f in $files
do
    echo $f
done
unset IFS

最後附上 man bash 裡的說明:

The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is ``<space><tab><new‐ line>''.

virtualbox 使用 USB 裝置

2012-12-16 更新

  • 現在 (4.x 版) 似乎無需做任何設定, 只要有裝 Oracle VM VirtualBox Extension Pack, 在 VirtualBox 視窗右下角按 USB 的圖示, 再點目標裝置, 即可加入或移除該裝置
  • 同一時間只有 host 或 guest 可擁有該裝置, 所以從 guest OS 移除, 相當於接回 host OS
  • 目前 VirtualBox 只支援 USB 2.0 的插槽, 若偵測不到時, 注意一下是否為這個問題
  • 有時拔拔插插, VirtualBox 會進入奇怪的狀態, 接上去 guest OS 無法連接且跳出 device is busy 的錯誤訊息。試看看拔除該裝置, 重開 guest OS
  • (續上則) 若重開 guest OS 無效, 並且 host OS 已移除該裝置, VirtualBox 的 USB 清單卻仍顯示 "captured", 試看看拔除該裝置, 重開 host OS

原文

網路上搜一下, 比較多是 Ubuntu 當 host 的解法, 我的情況是 Win7 當 host, Ubuntu 當 guest。

這兩篇說明很詳細

現在的版本圖形介面很好用了, 不用像第二篇說的那樣用指令操作。這裡記下我的操作步驟:

  1. 關掉 guest OS
  2. 在 VirtualBox 選單, 選擇 guest OS -> Settings -> USB -> Enable USB 2.0
  3. 會出現訊息框, 說明要安裝 Oracle VM VirtualBox Extension Pack。下載後安裝它
  4. host OS 插入 USB 隨身碟
  5. 在 VirtualBox 選單, 選擇 guest OS -> Settings -> USB, 點右邊有綠色 "+" 的 USB 頭的圖示, 選擇該 USB 隨身碟, 加入它的 filter
  6. 從 host OS 移除 USB 隨身碟
  7. 開啟 guest OS
  8. 插入 USB 隨身碟, 於是 guest OS 會自動偵測隨身碟, 掛到 "/media/USB DISK"

相關查看的指令:

  • 用 sudo fdisk -l 查看所有硬碟裝置, 我的情況是, 原本只有 /dev/sda, 隨身碟自動掛上去後, 多了 /dev/sdb。另外 ls -l /dev/disk/by-id 可看到 USB 隨身碟的名稱, 用 soft link 連到 /dev/sdb。
  • lsusb 列出 USB bus 資訊, 不清楚還能查什麼。裝完 Extension Pack 後多了 Linux Foundtation 2.0 root hub, 原本只有 1.1
  • 之後可自己用 sudo umount ... 和 sudo mount /dev/sdb1 SOME_DIR 來掛載和移除 USB 隨身碟

備註

  • 加好 USB filter 後, 插入該 USB 隨身碟都是掛到 guest OS, 若要將它掛到 host OS, 大概要關掉 guest OS, 等有需求時再看看怎麼處理較好
  • 加入 USB filter 時, 我一開始是先點藍色的 "o" 圖示, 加入一個空的 filter。原本猜說是全部放行的意思, 不過試了以後, 在插入 USB 隨身碟時, Win7 就當了

2011年8月21日 星期日

用 Sphinx 寫文件

以下是最近試用 Sphinx 的心得, 有錯還請指正。

我原本很納悶, 專案文件是用 Wiki 寫好, 還是用 Sphinx 寫來得好。為啥要特別做個工具來產生文件呢? 抱著實驗的性質試了 Sphinx, 試下去才明白, 難怪不少專案用 Sphinx 來寫。

Sphinx 寫有幾個好處:

  • 文件原始碼可和程式碼放在同一個 repository, 也可藉此順便做版本管理。不過我覺得以版本管理來說, 還是沒 Wiki 方便。
  • 可透過語法載入程式碼內的註解, 這點 Wiki 就沒輒了, 我覺得這是用 Sphinx 的最大優勢。
  • 一堆好用的 plugin, 像是畫圖、寫數學式子、畫 graph等。將 graphviz 原始碼直接寫在文件裡, 還挺方便的, 不過這點 Wiki 也辦得到。
  • 嵌入 ipython 語法, 顯示 ipython 的結果, 包含自動填入執行結果等功能。這個相當好用, 適合寫執行的範例 (寫實例演練的必備工具), Wiki 就沒有啦。
  • 產生的結果是純 html, 並有提供文章搜尋功能 (實作方式很妙, 建好 index 存成 js 程式碼, 透過 js 執行搜尋)。架網站提供文件時, 不需另裝任合套件 (PHP / CGI / Web framework / etc)。
  • 功能彈性, 若是 Python guy 的話, 缺什麼語法可自己寫 extension 補一下。Sphinx 也有提供巨集功能自定語法。

相較之下, Wiki 最大的好處是編完存檔就看到結果, 省掉 make html 的步驟, 方便大家隨時一起共筆, 提供 lock 避免多人同時編輯相衝, 方便查閱版次之間的差異。Sphinx 得透過 VCS 做, 也不方便提供 html 的版次差異。所以若是多人同時共筆, 偏重於功能說明, Wiki 還是較方便。

這裡列幾個熱門專案選擇的做法:

看完後發覺 ........, 一時好像也看不出個什麼頭緒, 之後再慢慢觀察吧。

Sphinx 入門不難, 以下是幾個相關網站:

  • sampledoc: 一小份入門文件, 說明必要的部份, 快速上手。
  • 線上試語法
  • Read the Docs: Python Software Foundation 提供的網站, 設好後, 會自動 fetch codes, 並重編 Sphinx 文件, 還有支援顯示各個版本文件。有了這個站後, 就不用擔心用 Sphinx 還得自己另外找網站放文件。
  • 官網: 東西多, 自然也較難找, 不過文件滿完整的

Sphinx 寫文件後, 我才明白文件包含兩種不同的內容: tutorial 和 library reference。像 Java Doc、Epydoc 這類工具用來抽註解產生 library reference, 避免在兩個地方寫文件。Java Doc 我沒實際試用, Epydoc 雖說產生的結果很炫, 卻不方便用作者自己的觀點組織整個專案的套件, 描述它們的互動, 或是提供常見的使用情境等。

使用者第一時間需要的是一個可立即執行的 tutorial, library reference 則是備查。Sphinx 好用的地方就在於, 它可以自由組織 tutorial, 並提供語法引入 library reference (用 autodocEpydoc extension )。所以用 Sphinx 可同時滿足這兩種需求。

除 tutorial 和 library reference, 開發者可能會需要 system context 和 architecture overview 了解整個架構。這部份就得自己另外用工具畫, Power Point 之類的軟體滿適合的, 重點是容易上手。最近看了《How to make Awesome Diagrams for your slides》 覺得相當有幫助, 順便列在這備忘。

2011年8月18日 星期四

Sphinx 的 autodoc 載入問題

為了使用 autodoc, 我在 conf.py 裡這樣寫:

sys.path.append('/usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/ext')
extensions = ['graphviz', 'autodoc']

結果跑出:

autodoc documenter <class 'autodoc.ModuleDocumenter'> must be a subclass of Documenter

正確的寫法是:

extensions = ['sphinx.ext.graphviz', 'sphinx.ext.autodoc']

開 pdb 追下去, 發現是 sphinx.application 和 sphinx.ext.autodoc 之間 circular import 造成的問題。

在 sphinx.application 裡:

def add_autodocumenter(self, cls):
    from sphinx.ext import autodoc
    autodoc.add_documenter(cls)
    self.add_directive('auto' + cls.objtype, autodoc.AutoDirective)

會載入 sphinx.ext.autodoc, 但我原本的寫法, 讓 sphinx 透過 __import__ 載入 autodoc, 結果產生兩個不同的物件, 但都是 autodoc, 結果剩下的一些程式操作, 就發生了悲劇。

在 autodoc 裡的這段程式就丟出 exception:

if not issubclass(cls, Documenter):
    raise ExtensionError('autodoc documenter %r must be a subclass '

cls (即 ModuleDocumenter) 真的不是 Documenter 的 subclass, 因為這個 ModuleDocumenter 是其中一個 autodoc 的, 而 Documenter 卻是另一個 autodoc 的。

若硬註解掉這兩行, 雖然 autodoc 的部份指令能用, 卻也會部份失效, 像 :members: 就會完全沒效果, 原因也是出在有兩個 autodoc 上, 結果找 members 的那個 autodoc 的 AutoDirective._registry 是空的, 造成它找不到能處理 members 的 Documenter。

備註: autodoc.AutoDirective 負責處理 directive auto*。AutoDirective 依指令名稱從 _registry 中找出適用的 class 來處理, 比方說 automodule 就會找 ModuleDocumenter 來處理。正常的程序下, autodoc.setup() 會填好 AutoDirective._registry, 但在上面說的錯誤設定下, 會變成 sphinx.ext.autodoc.AutoDirective._registry 有設好, 而 autodoc.AutoDirective._registry 是空的。

下面是用 pdb 看設錯情況發生的狀況, 滿有趣的:

> /usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/ext/autodoc.py(1177)run()
-> documenter.generate(more_content=self.content)
  /usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/ext/autodoc.py(715)generate()
-> self.document_members(all_members)
  /usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/ext/autodoc.py(981)document_members()
-> ModuleLevelDocumenter.document_members(self, all_members)
  /usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/ext/autodoc.py(609)document_members()
-> for (mname, member, isattr) in self.filter_members(members, want_all):
(Pdb) l
1172            # process the options with the selected documenter's option_spec
1173            self.genopt = Options(assemble_option_dict(
1174                self.options.items(), doc_class.option_spec))
1175            # generate the output
1176            documenter = doc_class(self, self.arguments[0])
1177 ->         documenter.generate(more_content=self.content)
1178            if not self.result:
1179                return self.warnings
1180
1181            # record all filenames as dependencies -- this will at least
1182            # partially make automatic invalidation possible
(Pdb) documenter.__module__
'autodoc'
(Pdb) id(AutoDirective)
21622640
(Pdb) down
> /usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/ext/autodoc.py(715)generate()
-> self.document_members(all_members)
(Pdb) id(AutoDirective)
20956032
(Pdb) import sys
(Pdb) id(sys.modules['autodoc'].AutoDirective)
20956032
(Pdb) id(sys.modules['sphinx.ext.autodoc'].AutoDirective)
21622640

表示一開始在 sphinx.ext.autodoc, 但 documenter 是從 autodoc 取來的, 結果就無聲無息的掛了。

2011年8月14日 星期日

《CPython 源碼剖析》讀書心得 - ch2 - int

PyIntObject

展開巨集後的定義

typedef struct {
   Py_ssize_t ob_refcnt;
   struct _typeobject *ob_type;
   long ob_ival;
} PyIntObject;

基本操作

這篇記了加法的實作: 加完後檢查溢位,若溢位的話, 改用大數 PyLongObject。

PyInt_AS_LONG vs. PyInt_ASLong

在運算過程中, 視情況會用不同方式取出 ob_ival:

  • 前者是巨集, 沒檢查 type, 速度快
  • 後者是函式, 有檢查 type, 速度慢

其它 type 也會看到類似的操作。

immutable

不可變的物件有許多好處, 包括可以任意被所有程式共享, 不用擔心造成依賴關係, 會不小心互炸。代價是各項操作後, 時常需要產生新的物件, 可能會拖慢速度。若會用到大量物件的話, 通常會搭配 object pool 減少生成物件的次數, 藉此簡省計算時間, 或是進一步在生成前先檢查是否已存在同樣物件, 有的話直接共享同一物件, 不生成重覆的物件。

在後面的例子會看到, CPython 有用 object pool 處理 int, 但也付出無限擴大記憶體的代價; 另外, CPython 只針對小部份範圍的數字共用物件, 以取得效率平衡。

memory management

intobject.c 開頭寫下為了減少 malloc 次數, 而採取的作法, 以及它造成的問題:

/* Integers are quite normal objects, to make object handling uniform.
(Using odd pointers to represent integers would save much space
but require extra checks for this special case throughout the code.)
Since a typical Python program spends much of its time allocating
and deallocating integers, these operations should be very fast.
Therefore we use a dedicated allocation scheme with a much lower
overhead (in space and time) than straight malloc(): a simple
dedicated free list, filled when necessary with memory from malloc().

block_list is a singly-linked list of all PyIntBlocks ever allocated,
linked via their next members. PyIntBlocks are never returned to the system before shutdown (PyInt_Fini).

free_list is a singly-linked list of available PyIntObjects, linked via abuse of their ob_type members.
*/

CPython 配了一塊記憶體自己管 int object 的生成和回收, 這塊記憶體只會愈長愈大, 永遠不會變小, 換句話說, 在 64 bit OS 上, 同時用到五千萬的整數後 (e.g., range(50000000)), 即使之後不會再同時用到五千萬個整數, 還是會占去 8*3*50000000 = 約 1G 不會回收的空間。

PyIntBlocks 和 free_list

#define BLOCK_SIZE  1000    /* 1K less typical malloc overhead */
#define BHEAD_SIZE  8   /* Enough for a 64-bit pointer */
#define N_INTOBJECTS    ((BLOCK_SIZE - BHEAD_SIZE) /
sizeof(PyIntObject))

struct _intblock {
   struct _intblock *next;
   PyIntObject objects[N_INTOBJECTS];
};

typedef struct _intblock PyIntBlock;

static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;  // the head of next-to-new int object

static PyIntObject *
fill_free_list(void)
{
   PyIntObject *p, *q;
   /* Python's object allocator isn't appropriate for large blocks. */
   p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
   if (p == NULL)
       return (PyIntObject *) PyErr_NoMemory();
   ((PyIntBlock *)p)->next = block_list;
   block_list = (PyIntBlock *)p;
   /* Link the int objects together, from rear to front, then return
      the address of the last int object in the block. */
   p = &((PyIntBlock *)p)->objects[0];
   q = p + N_INTOBJECTS;
   while (--q > p)
       q->ob_type = (struct _typeobject *)(q-1);
   q->ob_type = NULL;
   return p + N_INTOBJECTS - 1;
}

這裡用了不少 trick, 若沒先看書, 大概要讀一陣子吧。首先讓 PyIntBlock 不要占超過 1k, 大概是這樣 kernel 比較好找空間, 效率比較高。

fill_free_list 用 ob_type 當作 singly-linked list 的 next, 反正等產生 PyIntObject 時, 就會將它指到正確的值, 開頭的註解有先消毒了, 說會 "abuse of their ob_type members", 好奇 grep 了一下 2.5.2 的程式, 幸好 "abuse" 只出現 29 次 ...。

PyObject *
PyInt_FromLong(long ival)
{
   register PyIntObject *v;
   /*--- Begin of #1 ---*/
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
   if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
       v = small_ints[ival + NSMALLNEGINTS];
       Py_INCREF(v);
       return (PyObject *) v;
   }
#endif
   /*--- End of #1 ---*/
   if (free_list == NULL) {
       if ((free_list = fill_free_list()) == NULL)
           return NULL;
   }
   /* Inline PyObject_New */
   v = free_list;
   free_list = (PyIntObject *)v->ob_type;
   PyObject_INIT(v, &PyInt_Type);
   v->ob_ival = ival;
   return (PyObject *) v;
}

先略過 #1 的部份, 後面再解釋。free_list 指到 linked list 的頭, 準備產生 int object, 在 deallocation 時, 會將 free_list 改指到用不到的 PyIntObject, 所以這塊 int object 的記憶體不會有 memory leak, 只是當 free_list 這串超長, 一堆空間用不到時, 它也不會釋放空間就是了。

small_ints

CPython 初始化時, 會先配置一塊空間存 [-5, 256] 的整數, 改變巨集重編 CPython 可改變這個範圍。

之後用到這範圍整數時, 不會重新產生新的 PyIntObject。上面 #1 那塊程式碼就是從 small_ints 取值, small_ints 也是用 PyIntBlocks 裡的空間。

透過下面的操作說明 small_ints 的效果 (id 會傳回 memroy address):

In [1]: id(-5)
Out[1]: 7747448

In [2]: id(-5)
Out[2]: 7747448  # 沒變, 同一個物件

In [3]: id(-6)
Out[3]: 8558280

In [4]: id(-6)
Out[4]: 8557608  # 變了, 表示這個 -6 是新的物件

In [5]: id(256)
Out[5]: 7753136

In [6]: id(256)
Out[6]: 7753136  # 沒變

In [7]: id(257)
Out[7]: 8557512

In [8]: id(257)
Out[8]: 8557440  # 變了

我原本以為 CPython 像多數語言有共用字串那樣, 有共用所有整數。沒想到只有共用小範圍的整數。仔細想想也有道理, 若每次取數字都要 hash, 效率也會很差吧, 這之間的平衡真難拿捏。

了解 PyIntOjbect 的實作方式後, 才明白為何 CPython 會吃掉這麼多記憶體, 還有為何像 psyco 能在不改 code 的情況下讓 CPython 大幅加速科學計算, 一部份的原因是省下一些生成 PyIntObject 的時間

2011年8月11日 星期四

Sphinx customized theme

官網說明有完整說明, 這裡記個人速記版。

找出 default theme 位置並複制一份出來, 供之後修改:

# 找出 default theme 的位置
$ locate default/static
/usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/themes/default/static
/usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/themes/default/static/default.css_t
/usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/themes/default/static/sidebar.js

# 切到自己建的 Sphinx 目錄
$ cd docs

# 複制 default theme 出來
$ cp -r /usr/lib/python2.5/site-packages/Sphinx-1.0.7-py2.5.egg/sphinx/themes/default MY_THEME  

編輯 conf.py 加入:

html_theme = 'MY_THEME'
...
html_theme_path = ['.']

這樣之後改 MY_THEME 下的東西即可。

若只需要改 css 的話, 不需複製整個 theme 出來, 可以改 conf.py 裡的 html_static_path, 改用自己的 default.css 覆蓋 default theme 用的 css。照上面的方式找到 default theme 用的 css, 複制出來放到 _static/default.css, 之後再修改這個檔案即可。

2011年8月10日 星期三

小撇步: 直接看指令的原始碼

~/.bashrc 裡這麼寫:

function see() {
    vi `which $1`
}

complete -c command see

就能打:

$ see apt-file

看 apt-file 的內容, 並支援 completion, 可按 TAB 補完指令名稱。

2011年8月1日 星期一

在 Ubuntu 上使用 CUnit + curses 執行 unit test

安裝

  1. sudo aptitude install libncurses5-dev
  2. wget -O CUnit-2.1-2-src.tar.bz2 http://downloads.sourceforge.net/project/cunit/CUnit/2.1-2/CUnit-2.1-2-src.tar.bz2?r=&ts=1312211219&use_mirror=nchc
  3. tar jxvf CUnit-2.1-2-src.tar.bz2
  4. cd CUnit-2.1-2/
  5. ./configure --enable-curses
  6. make
  7. sudo make install

這樣會裝到 /usr/local/include/CUnit 和 /usr/local/lib/libcunit.*。

編譯

  1. 貼上這裡的範例程式, 存檔為 example.c
  2. 註解掉最下方的 CU_basic_run_tests();, 改為 CU_curses_run_tests;, 並在上面加入#include "CUnit/CUCurses.h"
  3. gcc example.c -lcunit -lncurses -o example

執行

  1. ./example

失敗的話, 檢查一下 /usr/local/lib 是否有在 /etc/ld.so.conf 裡, 加進去後執行 /sbin/ldconfig –v 更新設定。細節參考: 《error while loading shared libraries的解決方法》

若不想用 curses 的話, 就用 CU_basic_run_tests。

若確定用不到 curses, 安裝時可以不裝 libncurses5-dev、configure 時可以少掉 --enable-curses。這樣的話, 編譯時也不用加 -lncurses。

不過用 curses 跑還挺炫的說, 會有綠色的 progress bar!! 之後再來看看是否實用。

找出占用 port 的程式

netstat -nap | grep 8000

會找出使用 port 8000 的程式

2011年7月31日 星期日

semantic versioning

WanCW前篇留言 提到 《Semantic Versioning》, 是 github 的創辦人 Tom Preston-Werner 寫的。當初好像就是看到這篇吧。

文中先說明為了減少 dependency hell 的影響, 若大家定版號時共同遵守同樣的規範, 就能比較放心地說明自己的 package 需要那些版本的 packages:

If the dependency specifications are too tight, you are in danger of version lock (the inability to upgrade a package without having to release new versions of every dependent package). If dependencies are specified too loosely, you will inevitably be bitten by version promiscuity (assuming compatibility with more future versions than is reasonable). Dependency hell is where you are when version lock and/or version promiscuity prevent you from easily and safely moving your project forward.

接著提出 semantic version 的定義:

Consider a version format of X.Y.Z (Major.Minor.Patch). Bug fixes not affecting the API increment the patch version, backwards compatible API additions/changes increment the minor version, and backwards incompatible API changes increment the major version.

然後用嚴僅的方式陳列 semantic version 的定義, 言簡意賅。這裡用我的方式描述一下, 完整的定義以原文為準:

  • 版號 X.Y.Z 代表 Major.Minor.Patch
  • X = 0 時, 隨便你搞
  • X > 0 後, 在程式裡或文件註明那些是 public API
  • 沒動到 public API (如修 bug、重構):Z++
  • 動到 public API 相關程式, 但介面不變 (如加新功能):Y++
  • public API 介面有變:X++

FAQ 裡有說明如何處理特殊情況, 像是「若一有 incompatible API changes 就升 major version, 那不是一下就跳到 42.x.x」? 作者回答那表示你沒有認真看待 public API, 要盡可能地減少 incompatible changes。「若不小心破壞規則, 該如何修正」? 作者回答修正回來後, 再發一個新版號, 並在文件中註明沒遵守規則的版號。另外還有一個 issuegithub 上, 供大家反應不完備的部份, 真是不錯的規範。

2011年7月30日 星期六

Python 2 to 3 的問題

看到有朋友在問 python 2 和 python 3 有何差距, 來寫一下自己的理解。先聲明, 我沒有實際轉過。

之前曾在別的地方讀到版本編號的含意, 忘了出處, 找了一下發現 RubyGems 的說明和我先前看到的內容一樣, 對版本 a.b.c 來說:

  • a 往上 +1, 表示有大幅改版, 會有不向下相容的部份, 官方 release note (或 changelog) 裡會描述那些地方沒有向下相容 (backwards incompatible changes), 如 Fabric 的 Changelog 有提到那些函式行為和之前不同。
  • b 往上 +1, 表示有曾加新 feature 或 bug fix, 但介面沒有改變, 可以安心昇級
  • c 只是修改實作方式, 對使用者來說, 行為完全沒變。

當然還有許多其它的表示方式, 就.....先忽略它們好了。說明這點是想強調 2 到 3 的難處, 在於有些不相容的改變。

《Case study: porting chardet to Python 3 - Dive Into Python 3》提供一個轉換的案例, 其中 Fixing What 2to3 Can’t 說明要手動修改的部份。

除了修改自己程式的問題外, 但是若別人的程式沒有升到 3, 改完也沒用。比方說大家都愛用的 NumPy, 官方說目前只支援 2.4 ~ 2.6, 像 SciPymatplotlib (強大的繪圖函式庫) 會用到 NumPy, 在 NumPy 升上去以前, 它們也無法升級。所以在重量級 packages 支援 python 3 以前, 一般使用者仍不方便升到 python 3。

2011-07-30 Updated

看到 Python 3 Wall of Shame 有統計各 package 被下載的次數以及是否支援 python 3, 才發覺原來 NumPy 已支援 Python 3 了

2011-07-31 Updated

WanCWEric Chen 提醒, 更新如下:
* 版號的定義是寫在 Semantic versioning
* matplotlib 已有官方的 Python3 branch

2011-08-02 Updated

Eric Chen 提醒, SciPy 也有支援 3.x

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...