顯示具有 linux programming 標籤的文章。 顯示所有文章
顯示具有 linux programming 標籤的文章。 顯示所有文章

2015年2月6日 星期五

跳掉檔案開頭數 bytes 的作法

假設有個大檔案 (e.g. 500MB) 在開頭存有 meta data, 後面存有內容, 要怎麼跳掉開頭的 meta data, 只留下內容呢?

命令列的作法

這裡看到兩個有效率的作法:

使用 subshell: 先用一個 dd 跳掉開頭 1131 bytes (block size=1131, read 1 block), 再用第二個 dd 用正常的 block size 讀寫檔, 這樣效率就和直接用 dd 複製檔案差不多了。

$ ( dd bs=1131 count=1 of=dev_null && dd bs=4K of=out.mp3 ) < 100827_MR029_LobbyControl.mp3

使用 pipe 配合 a group of commands: 概念差不多, 只是改用 pipe。

$ dd if=filtered.dump bs=512k | { dd bs=1131 count=1 of=/dev/null; dd bs=512k of=trimmed.dump; }

Python 不建立新檔的作法

產生新檔案的時間成本有點高, 可能會花到數秒。如果是 Python 程式內要用的話, 可以直接用 file object 然後想辦法跳掉開頭資料。

偷懶的作法是直接用 file object, 然後先呼叫 file.read(N) 跳掉開頭 N bytes。缺點是若 file object 會傳入 third-party lib 使用, 其它程式碼可能會用 seek() 回頭讀開頭的內容。

保險的作法是自訂一個 file object wrapper。這個 wrapper 行為和內建 file object 一模一樣, 只是讀不到開頭數個 bytes。這樣效率和一般的 file object 幾乎沒差。 程式碼見這裡, 另外這裡是測試碼。關鍵是覆寫和移動檔案位置有關的 seektell。然後用 Python 提供的 __getattr__(self, attr) 實作剩下沒改到的 method。

那麼, 要怎麼知道該覆寫那些 method 呢? 用 ipython 建一個 file object, 然後利用補字功能, 再一個個看 method 說明, 很快就知道該覆寫那些了。

以下是示意的操作過程:

$ ipython

In [1]: f = open('/etc/fstab')

In [2]: f.<TAB>
f.close       f.encoding    f.fileno      f.isatty      f.name        f.next        f.readinto    f.readlines   f.softspace   f.truncate    f.writelines
f.closed      f.errors      f.flush       f.mode        f.newlines    f.read        f.readline    f.seek        f.tell        f.write       f.xreadlines

In [2]: f.close?
Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method close of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
close() -> None or (perhaps) an integer.  Close the file.

Sets data attribute .closed to True.  A closed file cannot be used for
further I/O operations.  close() may be called more than once without
error.  Some kinds of file objects (for example, opened by popen())
may return an exit status upon closing.

In [3]: f.encoding?

...

In [21]: f.seek?

Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method seek of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
seek(offset[, whence]) -> None.  Move to new file position.

Argument offset is a byte count.  Optional argument whence defaults to
0 (offset from start of file, offset should be >= 0); other values are 1
(move relative to current position, positive or negative), and 2 (move
relative to end of file, usually negative, although many platforms allow
seeking beyond the end of a file).  If the file is opened in text mode,
only offsets returned by tell() are legal.  Use of other offsets causes
undefined behavior.
Note that not all file objects are seekable.

...

2015年1月29日 星期四

取得執行檔所在的目錄名稱

有時自己寫的程式, 會在執行檔所在的位置放其它資源檔或設定檔。如果偷懶直接用相對路徑讀檔, 可能因為先前有用 chdir 切換 process 所在的位置, 而讀不到檔案。保險見起, 可以改用執行檔所在的位置作為起始路徑來讀檔。

這裡有取得執行檔所在的目錄的程式碼, 重點是利用 /proc/self/exe 找出產生目前 process 的執行檔的路徑。平時需要寫 shell script 也可用同樣方法取得路徑

2015/02/06 更新

原本寫用 /proc/PID/exe, 經 Kito Cheng 提醒, 改為 /proc/self/exe 更方便。

2014年4月25日 星期五

過濾 iOS log 的方法

平常開發 Linux 程式需要即時觀察 log, 又有太多 log 要過濾的時候, 會用

$ tail -f LOG_FILE | grep PATTERN  # 只留符合 PATTERN 的 log

或是

$ tail -f LOG_FILE | grep -v PATTERN  # 去掉符合 PATTERN 的 log

command line 的 pipe 可以無限接, 方便做很多延伸運用。

開發 Android 的時候可以透過命令列的 adb logcat, 或是 Eclipse 也有提供介面過濾 log。

開發 iOS 時比較困擾, 通俗是用 NSLog 寫到 console, 但是沒有找到用 XCode 過濾的方法。

轉念一想, 既然 iOS 裝置接上 XCode 後可以看到過去的 console log, 表示 log 存在檔案裡。直接取 log 的原始檔案, 比照在 Linux 的習慣直接對檔案操作應該比較簡單。找了一陣子找到檔案放在 $HOME/Library/Application Support/iPhone Simulator/7.1/Library/Logs/system.log, 接著套上在 Linux 的習慣, 就方便多了。

2014年3月29日 星期六

Ctrl+C 運作的原理以及 session, terminal 和 process group

簡短版說明

前陣子遇到一個神祕的問題:

執行程式 P 的時候, 可以用 ^C 中止它。但透過 shell script S 執行 P (S 內只有該程式名稱和參數而已), 就無法用 ^C 中止它。^\ 也是一樣情況, 不過 ^Z 可以正常 suspend。

檢查其它的環境因素發現:

  • kill -INT PID 可以中止 P, 表示程式沒有攔截 SIGINT。
  • stty -a -F TTY_DEVICE 顯示 intr = ^C 且 isig, 表示輸入 ^C 後會被轉譯成 SIGINT。按 ^C 後螢幕上有顯示 ^C。

綜合以上的資訊, 無法理解還有什麼原因會讓 ^C 轉送 SIGINT 失效。

卡了一陣子後, 回頭翻 TLPI ch34 Process Groups, Sessions, and Job Control 以及 ch62 Teriminals 才找到答案。

答案是: P 有用 setpgid() 產生新的 process group, 於是 P 就不是 terminal foreground process group, 而 ^C 只會送給 terminal foreground process group。

詳細說明

在沒有透過 shell script 執行 P 的情況, P 會自成一個新的 process group, 同時也是 terminal foreground process group (這是 shell fork process 後設定的)。之後即使程式有呼叫 setpgid(), 也不會改變 pgid, 因為 P 本來就是自己 process group leader。這時, P 仍然是 terminal foreground process group, 收得到傳給 terminal 的 ^C。

另一方面, 透過 shell script S 執行的情況, S 自成一個新的 process group 也會是 terminal foreground process group (同樣的, 是 shell 設定的)。S 執行 P, P 又用 setpgid() 自己成立一個新的 process group, 這個新的 process group 不是 terminal foreground process group, 也就收不到 ^C 了。

若希望 P 能收到 ^C, 呼叫 setpgid() 後, 記得要呼叫 tcsetpgrp()。

像是這樣:

setpgid(0, 0);
tcsetpgrp(0, getpgid(0));

Terminal 相關知識

TLPI ch62 Terminals 介紹 terminal 相關參數的意思。可以用指令 tty 得知目前 shell 連接的 terminal, 像這樣:

$ tty
/dev/pts/10

之後就可以在別的 terminal 用 stty -F TTY 觀察或修改該 terminal 的設定, 方便了解程式是否有修改 terminal 參數:

$ stty -F /dev/pts/10 -a
speed 38400 baud; rows 47; columns 183; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V;
flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke

stty 顯示的含意可看 man sttyTLPI ch62。透過 stty 可以確定 intr 和 isig 都有正常運作, 問題不在這裡。

Session, Process Group 相關知識

為了方便管理 process, Linux 有 session 和 process group 兩層結構。一個 session 可以沒有 terminal, 或是有唯一一個 terminal。一個 terminal 也只能屬於一個 session。

一個 session 可有多個 process group, 有些 system call 或指令可操作整個 process group, 所以切成多個 process group 易於管理 process。

比方說:

  • kill(-PID, SIG) 會送 signal SIG 給所有 pgid 為 PID 的 process, 方便一次暫停或殺掉整群 process (shell 用 pipe 串起多個指令時, 這些指令就是同一個 pgid)
  • terminal 收到 ^C、^Z、^\ 這類會轉成 signal 的控制字元時, 會送給整個 terminal foreground process group (有設 isig 時才會轉成 signal, 預設有設 isig)。

若想查看 pid, ppid, pgid, sid, tpgid (terminal foreground process group), 可用 ps -o pid,ppid,pgid,sid,tpgid。細節見 ps 或 man proc, ps 也只是讀 /proc/PID/stat 和 /proc/PID/ 下其它檔案。

完整的介紹見 TLPI ch34 Process Groups, Sessions, and Job Control。

2014年1月20日 星期一

尋找 memory error 的強力工具: Address Sanitizer (ASan)

官網有很詳細的介紹, ASan 可以偵測出以下的問題:

  • Use after free (dangling pointer dereference)
  • Heap buffer overflow
  • Stack buffer overflow
  • Global buffer overflow
  • Use after return

都是出錯時很難察覺, 之後會造成奇異的行為, 或是讓程式掛在莫明奇妙的地方。

clang 3.1 和 gcc 4.8 開始內建 ASan 的功能。號稱執行速度平均慢兩倍, 比起 Valgrind 的 20 倍, 實在相當誘人。這裡有說明 ASan 怎麼做的。這樣在 Linux 上也有不錯的 memory error detector 了。不知什麼時候能追上 OS X 的腳步

除了 Chromium 有用之外, Firefox 也有使用

2013年7月20日 星期六

Linux 寫 code 確認某個 process 仍活著

shell script 直接用 ps PID | grep ... 檢查, 無須多說。寫 C/C++ 程式的話, 基於同樣的想法, 可以用 stat("/proc/PID/stat") 檢查檔案是否存在, 但這作法有一些不確定性, 有可能目標 process 掛了, 之後有新的 process 用了同樣的 pid, 結果誤判目標還活著。

若自己有權限送 signal 給目標 process 的話 (如 child 或 parent process), 可以用 kill 來檢查:

bool IsAlive(pid_t pid)
{
    return !(kill(pid, SIGCONT) == -1 && errno == ESRCH);
}

送個無關緊要的 signal 給目標 process (若它真的有機會進入 STOP 狀態的話, 得挑別的 signal), 再檢查錯誤值判斷目標是否還活著。覺得用 kill 檢查的小技巧還滿有趣的, 備忘一下。

2014-03-28 更新

實際用了以後發現一個不方便的副作用。使用 gdb 連上 process 後, gdb 會在 process 收到 signal 時中斷。若採用送 signal 定期偵測 process 是否活著的話, gdb 會三不五時地中斷, 不方便除錯。

還是用 stat("/proc/PID/stat") 比較方便一些。或是觀察的 process 是 child process 的話, 可用 waitpid + WNOHANG 判斷。

2013年6月23日 星期日

計算 Linux 記憶體使用量

雖然直覺上會使用 top 或 free 觀看結果, 但是 top 或 free 顯示的資訊包含 kernel 使用的 cache 量, 不是「所有 process 使用的量」。因此, 我習慣看 htop 顯示的值

Viller Hsiao 提醒, 才知道原來 htop 顯示的記憶體用量和 free 第二行的數值一樣, 都是讀 /proc/meminfo, 然後得出實際使用量 = MemTotal - MemFree - Buffers - Cached。若需要寫 script 記錄系統的 memory peak, 可用 free 的輸出, 畢竟 htop 沒有 batch mode 可用。

以下是一個 free 顯示的例子:

$ free
             total       used       free     shared    buffers     cached
Mem:      11418888    3247916    8170972          0    1274988     693072
-/+ buffers/cache:    1279856   10139032
Swap:            0          0          0

我之前一直誤會第二行的意思, 原來它的意思是「考慮第一行顯示的 buffers 和 cached 後, 重新計算 used 和 free 的值」。

Viller Hsiao 翻了一下原始碼證實這件事。我自己也試著練習翻閱原始碼, 增加翻原始碼查資料的能力。

以下是查詢過程的一些記錄。

free

找 free 原始碼的過程比較麻煩一點:

$ apt-file search /usr/bin/free | grep "free$"
procps: /usr/bin/free
$ aptitude show procps
...
Description: /proc file system utilities
 This package provides command line and full screen utilities for browsing procfs, a "pseudo" file system dynamically generated by the kernel to provide information about the status of
 entries in its process table (such as whether the process is running, stopped, or a "zombie").

 It contains free, kill, pkill, pgrep, pmap, ps, pwdx, skill, slabtop, snice, sysctl, tload, top, uptime, vmstat, w, and watch.
Homepage: http://procps.sf.net/
$ apt-get source procps

但原始碼滿好讀的:

free.c:

 63         meminfo();
...
 93             unsigned KLONG buffers_plus_cached = kb_main_buffers + kb_main_cached;
 94             printf(
 95                 "-/+ buffers/cache: %10Lu %10Lu\\n",
 96                 S(kb_main_used - buffers_plus_cached),
 97                 S(kb_main_free + buffers_plus_cached)
 98             );

利用上面的關鍵變數和已知關鍵資訊存在 /proc/meminfo 裡 (man proc 然後搜 meminfo), 進一步找到 proc/sysinfo.c, 從 memoinfo() 的實作證實 free 是讀 /proc/meminfo 的特定欄位, 計算出實際使用量。

htop

htop 本身套件名就是 htop, 但程式碼比較複雜一些。

先假設 htop 也是讀 /proc/meminfo, 以 meminfo 作為關鍵字下手, 可以找到關鍵的程式碼:

ProcessList.h

 20 #define PROCDIR "/proc"
...
 28 #define PROCMEMINFOFILE PROCDIR "/meminfo"

ProcessList.c

750 void ProcessList_scan(ProcessList* this) {
751    unsigned long long int usertime, nicetime, systemtime, systemalltime, idlealltime, idletime, totaltime, virtalltime;
752    unsigned long long int swapFree = 0;
753
754    FILE* file = fopen(PROCMEMINFOFILE, "r");
755    assert(file != NULL);
756    int cpus = this->cpuCount;
757    {
758       char buffer[128];
759       while (fgets(buffer, 128, file)) {
760
761          switch (buffer[0]) {
762          case 'M':
763             if (String_startsWith(buffer, "MemTotal:"))
764                sscanf(buffer, "MemTotal: %llu kB", &this->totalMem);
765             else if (String_startsWith(buffer, "MemFree:"))
766                sscanf(buffer, "MemFree: %llu kB", &this->freeMem);
767             else if (String_startsWith(buffer, "MemShared:"))
768                sscanf(buffer, "MemShared: %llu kB", &this->sharedMem);
769             break;
770          case 'B':
771             if (String_startsWith(buffer, "Buffers:"))
772                sscanf(buffer, "Buffers: %llu kB", &this->buffersMem);
773             break;
774          case 'C':
775             if (String_startsWith(buffer, "Cached:"))
776                sscanf(buffer, "Cached: %llu kB", &this->cachedMem);
777             break;
778          case 'S':
779             if (String_startsWith(buffer, "SwapTotal:"))
780                sscanf(buffer, "SwapTotal: %llu kB", &this->totalSwap);
781             if (String_startsWith(buffer, "SwapFree:"))
782                sscanf(buffer, "SwapFree: %llu kB", &swapFree);
783             break;
784          }
785       }
786    }
787
788    this->usedMem = this->totalMem - this->freeMem;
789    this->usedSwap = this->totalSwap - swapFree;
790    fclose(file);

另一方面, 以下是唯一用到 memory 欄位的程式碼:

MemoryMeter.c

 28 static void MemoryMeter_setValues(Meter* this, char* buffer, int size) {
 29    long int usedMem = this->pl->usedMem;
 30    long int buffersMem = this->pl->buffersMem;
 31    long int cachedMem = this->pl->cachedMem;
 32    usedMem -= buffersMem + cachedMem;
 33    this->total = this->pl->totalMem;
 34    this->values[0] = usedMem;
 35    this->values[1] = buffersMem;
 36    this->values[2] = cachedMem;
 37    snprintf(buffer, size, "%ld/%ldMB", (long int) usedMem / 1024, (long int) this->total / 1024);
 38 }

Meter.h

 21 typedef struct Meter_ Meter;
...
 53 struct Meter_ {
...
 62    ProcessList* pl;
...
 65 };

證實兩者計算方式相同。

結語

有疑問的時候, 還是翻原始碼最穩, 多做幾次之後會更順手。

另外藉由這次的經驗, 發覺寫純 C 程式時, 作者自己會有一套封裝資料結構和控制的方法。不像 C++ 已內建物件觀念, 不同的 C 程式會有不同的結構和控制的手法, 讀不同專案時, 需要理解一下該專案使用的方式。也許多讀幾次後, 會發覺大家的手法也差不多吧。

除錯小技巧: 在程式中直接中斷及偵測是否被 gdb 監控中

在程式中直接中斷

《Binary Hack》 #92 所言, 在 gdb 監控 process 的情況, 使用下列程式碼會進入中斷:

raise(SIGTRAP);

對 x86 環境, 下列作法也許會更好, 少了一層 function call

__asm__ __volatile__("int3");

Scott 有個 debugbreak 有處理 x86 以外的情況, 有需要時再來參考看看。

偵測是否被 gdb 監控中

比起在程式中直接中斷, 我比較需要知道目前程式是否被 gdb 監控 (attach) 中。以下是兩個實用的情境:

  • 原本已有 log 錯誤訊息, 在 log 極為嚴重錯誤訊息 (如 severity = FATAL) 時, 檢查看看是否有 gdb 在監控, 有的話不如順便進入中斷, 提前察看錯誤點。
  • 寫 multi-thread 時, 有時會有特定 thread 負責特定的事, 像是 UI 或 IO。有時會監控這個 thread 是否正常運作。或是有網路連線時, 有時會想監控另一端連線是否仍然正常運作。其中一種監控 thread 或網路連線的方式是週期性察看有無更新資料, 超過一定時間沒更新, 就中止程序或關掉連線重連。在使用 gdb 除錯時, 有可能在中斷點待太久而觸發前述安全防護機制, 反而無法長時間除錯。這時自動關閉防護措施會比較方便。

《Binary Hack》 #92 的作法利用設 gdb 會覆寫 SIGTRAP 的 signal handler 做檢查, 這個作法可以跨比較多平台, 但可用時機太窄。我偏好 Chromium 的作法, 直接讀 /proc/PID/status, 由 "TracerPid" 的值得知目前是否有 process 控控自己。程式執行中的任何時間都可用這方法得知正確的結果, 不過只適用於 Linux 和 Android。以下是參考 Chromium 實作稍作修改的版本:

2013年5月30日 星期四

linux thread 與 signal

雖然是基礎知識, 一陣子沒用大概會忘記, 備忘一下重點:

  • 在 multi-thread 情況下, signal() 的行為是未定義, 必須用 sigaction() 註冊 signal
  • 用 kill 送 signal 給 process 後, process 內任何一個 thread 都有可能執行 signal handler。若不希望特定的 thread 收到 signal, 可用 pthread_sigmask() 改變特定 thread 的行為, 或是用 pthread_kill() 送出 signal 給指定的 thread
  • SIGSEGV 會送給發生 segmentation fault 的 thread。所以有 core dump 的時候, gdb 可以正確找出那個 thread 造成 segfault

參考資料:

2013年2月11日 星期一

async 與 non-blocking IO

以前一直沒有分清楚 asynchronous 和 non-blocking 的差異, 寫久了以後才明白兩者是不同層次的東西。

  • asynchronous programming: 程式不是一條線走到底, 可能目前做到一個段落停下來, 「過一會兒」再繼續執行
  • non-blocking IO: 呼叫讀寫函式時, 不會等到完成才返回

通常要讀寫外部資料時 (一般檔案、網路、pipe 等) , 無法確定返回的時間, 這樣會阻擋程式的運作, 所以希望這個讀取 (或寫入) 不要等完成後才返回 (即「非同步」)。

若不希望此次呼叫有不確定時間的擔擱, 有兩種可能的作法:

  • 開新 thread 或用既有的 thread 進行讀寫。總之, 停是停別人家的 thread, 和本 thread 無關
  • 設定 file descriptor 為 O_NONBLOCK, 告訴 OS 這個 fd 為 non-blocking IO, 呼叫後要能立即返回

兩者作法的優缺點分析已超出本「隨手記」的範圍 (書本作者愛用的大絕), 需要特別注意的是, 設了 O_NONBLOCK 不表示呼叫 read/write 後就結束了。

man 2 read (或 man 2 write) 的 ERRORS 提到:

  • EAGAIN The file descriptor fd refers to a file other than a socket and has been marked nonblocking (O_NONBLOCK), and the read would block.
  • EAGAIN or EWOULDBLOCK The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the read would block. POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibilities.

意思是說:

  • 招對實體檔案無效 (為了檔案讀寫效能, 一次做完可減少硬碟更動讀寫頭的時間)
  • 對 socket 仍有可能失效, 記得用 select / epoll 之類的 system call 請系統在有機會完成 non-blocking 操作時通知你。也就是說, 系統只保證此次呼叫不會 block 你, 不保證此次呼叫一定會成功。由開發者持續嘗試呼叫, 總有完成的時候。

2012年11月28日 星期三

在 x86-64 上對 system call 使用 conditional break

在了解目標模組的動態行為時, 最近覺得從模組的 public interface 設中斷點是滿不錯的作法。廣意來說, system call 則是最一般化的 public interface, 從 system call 回頭夾擊目標也滿有效的。比方像這篇strace 找到切入點。

若想獲得更多資訊, 可以用 gdb 在 system call 上設中斷點, 一步步從退回來找到呼叫者、傳遞的資料等更多訊息。但在尋找 write、send 這類廣為使用的 system call 時, 被呼叫的次數過於頻繁, 就有點難找了。

查了一下, 看到這篇提到從 register 設定 conditional break 的方法:

(gdb) b write if 1==$rdi  # stop only when write(1, ...)

Scott 補充了以下的資訊:

要在 syscall argument 上設條件,且要 portable 以目前的工具不容易。用 gdb 的話需能背出『x86-64 上參數依序擺在 rdi, rsi, rdx, rcx, r8d, r9d』。 x86-64 上特別好記,因 syscall 與 一般 function call 被設計成將參數擺在同位置。ARM 上還有 64bit 參數開頭一定擺在偶數暫存器如 r0-r1, r2-r3 而不擺在 r1-r2 等規則。

目前 gdb 沒有將 syscall parameter 存在如 $syscall_arg0 的 convenience variable 中,否則寫: catch syscall write if $syscall_arg0 == 1 即可。

用 "system call calling convention" 當關鍵字查到《What are the calling conventions for UNIX & Linux system calls on x86-64》, 文中有許多相關資訊, 就先備忘吧。

2012年11月24日 星期六

善用 prctl(PR_SET_NAME, name) 協助 debug multi-thread

參考文章: Name your threads

man 2 prtcl 說 prctl(PR_SET_NAME, char*) 會改變 process name, 但是它其實改的是 thread name。使用 gdb 的 info thread 會顯示 thread name, 這對於除錯 multi-thread 很有幫助。需要注意的是, 小心別改到 main thread 的名稱, 避免其它相關 kill 指令或自己寫的 script 失效。

2012-11-25 更新

Scott 提醒, 使用 pthread_setname_np(pthread_t, const char*) 更頗當, 且可用來設別的 thread 的名字。注意參數是 pthread_t 不是 tid, 還有不知為何, Ubuntu 12.04 下沒有它的 man page, 但 /usr/include/pthread.h 有它的宣告, 且編譯連結試用後, 也沒有問題。

2012年6月16日 星期六

寫 linux daemon 的注意事項

一直以來都很納悶為什麼寫 daemon 時需要 double fork, double fork 是必要的嗎? 最後才發覺我問錯問題了, 而問錯問題永遠不會得到對的答案。我該問的問題是, 成為一個 daemon, 必須該做那些事?

答案是:

  1. chdir("/"): 避免有任何目錄因為還有 process 在路徑內, 使得目錄已被移除, 卻無法釋放空間。
  2. 避免成為 zombie: 若 daemon 的 parent process 是 init 的話, daemon 結束時 init 會自動 wait 它, 避免成為 zombie。反之, 則有這點顧慮。
  3. 脫離 terminal: 因為關閉 terminal 的時候, kernel 會送 SIGHUP 給 session leader [*1]。以 shell 的實作來說, shell 是 session leader, 它在收到 SIGHUP 時, 會送 SIGHUP 給它所產生的所有 process group, 而 SIGHUP 預設行為是 terminate。但這不是 daemon 希望的行為, 相較於因此改變 SIGHUP 的行為, 不如脫離 terminal 更省事, 還可視需要保留 SIGHUP 做別的用途 [*2]
  4. 同上, 脫離 terminal 可避免不小心讀寫到 terminal 而收到 SIGTTIN 和 SIGTTOU。它們是 background process 讀寫 terminal 時觸發的, 藉由這樣告知 process 它目前是 background process 並嘗試要讀寫 terminal。雖然也可以改變這兩個 signal 的處理行為 (預設為 stopped), 但是不如完全不會發生來得省事。

有了以上需求, 就有不同的作法。POSIX 沒有定義 daemon() 函式, 不過 BSD 體系包含 Linux 裡有 daemon() 函式, 用來成為 daemon。eglibc 的實作方式如下:

  1. fork 一次, 讓 parent process 掛掉, 以滿足 (2)
  2. 呼叫 setsid() 成為新的 session leader
  3. chdir("/"), 以滿足 (1)
  4. 重導 0, 1, 2 到 /dev/null 以滿足 (3、4)

但 daemon() 沒有做第二次 fork(), 所以它仍是 session leader, 有機會開啟新的 terminal。若要避免這種可能, 呼叫完 daemon() 後再呼叫一次 fork(), 並讓原本的 parent process 離開, 這樣 daemon process 就會處在一個沒有 terminal 也沒有 session leader 的情況, 會更安全。

從再呼叫一次 fork() 來看, 會更能理解為什麼 daemon() 要開新的 session, 這樣在呼叫完 daemon() 後有比較大的主導權, 看是自己想開新的 terminal, 或是再做一次 fork() 確保不存在 session leader, 都沒有問題。

PS

  • *1 看了一些文件, 感覺上 session 是針對 terminal 而有的概念。而 process group 則和使用 signal 有關。
  • *2 有些 daemon 收到 SIGHUP 會重讀設定檔。由於 daemon 不可能觸發原本 SIGHUP 的使用情境, 可以安心拿它來做自己的用途。

2012年6月13日 星期三

在 Linux 上找執行時間的瓶頸

記錄一下自己加速程式的心得。重點是: 有個可靠且易於重覆執行的方式, 來計算執行時間。

logging

我最常用的方法是手動 logging 計算函式的執行時間。通常我有兩種作法

  • 在函式的開頭取得系統時間, 離開時再最得系統時間, 兩者相減得到執行的時間。 [*1]
  • 使用一個會自動附上 timestamp 的 logging function, 會記錄這次到上次 log 的時間差, 或事後用外部 script 算出各筆 log 之間的時間差。這樣可以在懷疑的函式不同地方做 log, 了解各區塊的執行時間。

PS *1 實作細節

  • 像 Python 有 decorator 的語法, 可以寫個 log_time(), 內容是「取得時間、執行目標函式、取得時間並記錄相減結果、傳回目標函式的傳回值」, 再用 decorator 的語法加到有興趣的函式上。
  • C++ 的話, 可以寫個 class LogTime, 在 constructor 以及 destructor 計時, 達到同樣效果。然後在目標函式的第一行宣告 LogTime log。

優點

  • 可以明確抓出函式花的時間, 對於「加快程式」來說, 是最確實的反應。不論是真的 CPU bound 或 lock 或 I/O 導致緩慢, 都會反應在 log 裡。
  • 可自行控制 log 函式的細膩度, 比方說先抓出 sort() 最花時間, 再來到 sort 裡放 log, 觀察在 sort 的那部份最花時間。

缺點

  • 得自己手動在懷疑的部份加上 log 函式, 不方便大範圍無腦偵查。

使用 profiler

linux - Alternatives to gprof 列了不少工具, 大概掃過了這篇, 也試了一下, 這部份的經驗很淺, 下面可能會有些錯誤, 像是對工具的錯誤認知, 或少用了什麼功能而覺得不好用。

感覺上這類工具只能反應出真的 CPU bound, 若原因是 lock 或 I/O 拖慢效能, 需要用其它管道查明源頭。

我是在 VirtualBox 裡的 Ubuntu 11.04 試用。選用的前提是對實際執行的負擔要低, 愈接近原本情況愈好。

perf

perf 很容易使用, 基本指令:

  • perf record CMD # 執行並記錄 CMD
  • perf record -p PID # 記錄 process PID
  • perf report # 看報告
  • sudo perf top # 顯示即時全系統狀況

但不知為啥占最多時間的總是奇怪的東西, 我寫個 for-loop 一直做加法, 結果部份時間花在 snd_ac97_codec (聲音?)。移除 PulseAudio 後, 變成花在 binfmt_misc。對於其它不是像 busy loop 的程式, 有可能最花時間的部份變成 snd_ac97_codec 或 binfmt_misc。讓我不知是否能相信它的結果。

2013-04-17 更新

在實體機器 (非VM) 上執行 perf 後, 就沒有一堆怪東西了。試用的結果相當滿意, perf 明顯的好處有兩點:

大概會讀一些組語的話, 用 perf 的效果更好, 可以在 perf report 後再觀看各函式最花時間的位置, perf 會嘗試對回組語附近的原始碼。注意 perf 觀察到的是 sample 結果, sample 量愈多, 可以採信的程度愈深。比方說 sample 量「不夠多」的話, 可以參考那個函式最花時間, 但可能不適合參考函式內最花時間的程式是那幾行。

oprofile

oprofile 也不難用, 但在 VM 裡會有些問題, 且無法發揮完整功能。解決的方法是在啟動 oprofile daemon 前就執行 sudo modprobe oprofile timer=1。若有先執行過 oprofile, 即使照著上面說的執行 --deinit, 結果在執行 modprobe 時會卡住。用 strace 看的結果, 像在 busy-waiting oprofile 結束。重開機後, 在執行 oprofile 前先執行 modprobe 就 ok。

小結相關指令如下:

  • sudo modprobe oprofile timer=1 # 可配合 dmesg | grep oprofile 看目前是否有用 timer interrupt
  • sudo opcontrol --start --verbose --no-vmlinux # 見後面的說明
  • sudo opcontrol --reset # 將記錄歸零
  • sudo opcontrol --dump # 將記錄寫到硬碟
  • opreport # 顯示整體摘要
  • opreport -l PROGRAM # 顯示 PROGRAM 的取樣情況
  • opreport -l PROGRAM -g # 加上原始碼位置和行號
  • sudo opcontrol --shutdown # 結束 daemon

啟動 daemon 後, 之後就是一直

  • ./PROG
  • sudo opcontrol --dump
  • opreport -l ./PROG
  • sudo opcontrol --reset

但是在沒有 vmlinux 的情況, 會看到有大部份時間花在 no-vmlinux (kernel 裡)。所以除非自己寫的部份真的相當忙, 幫助也是有限。我在 Ubuntu 上找了一陣子, 有文章提到裝別人弄好的 vmlinux, 不過沒有成功, 就先忽略這件事了。

對照 perfoprofile 反應的數據, 兩者各有一塊不明的部份占去最多時間, 其它項目的相對大小很接近, 多少算是提示。但我會懷疑數據反應出的實際影響。同一個函式, perf 可能說 20%, 但 oprofile 說 15%, 結果我也無法明白它們實際的執行時間有多長。

GooglePerformanceTools

gperftools 看起來也滿不錯的 --- 直到我抓下來看到 INSTALL 裡對於 x86-64 的注意事項後。於是我決定之後再來試用它, 至少先玩過 tcmalloc 後, 再回來看這個吧。

2012年5月26日 星期六

atexit() 和 exit() 的注意事項

有時候程式不正常結束時, 希望透過 atexit() 最做低限度的善後程續。但試用的結果是, atexit() 本身滿好用的, 正常結束後會符合預期做事。但是呼叫 exit() 隱藏的問題, 卻不那麼直覺。

目前遇過兩個狀況:

  • 在 signal handler 內使用 exit(): 想在收到異常通知時透過 exit() 善後。結果可能會讓程式卡住掛不掉。這也許和註冊的 callback 做什麼事有關。不過 exit() 不是 async-signal-safe functions (見 man 7 signal), 文件也提到在 signal 內用非 async-signal-safe functions 的行為是未定義的, 所以這種狀況出包也沒什麼好探究的了。
  • multi-thread 的處理: 註冊的 callback 裡有和 thread 相關的善後機制, 然後在錯的 thread 裡或錯的時機呼叫 exit() 造成 dead-lock。當狀況異常想呼叫 exit() 時, 其實也沒太多心力將 thread 之間的行為弄得更好。

另外關於 _exit(), man page 提到它不會呼叫 callback, 但有一些 implementation dependent 的行為, 感覺上不怎麼踏實。可以確定的是, _exit() 會關掉 file descriptors 而可能造成未知的延遲 (文件又提到可用 tcflush 避免這點, 但看不懂到底是有效還是 implementation dependent)。所以若要確保程式會立即結束, 用 abort() 或 kill(getpid(), SIGKILL) 應該比較穩吧, 只是就無法提供 exit status 了。

2012年5月2日 星期三

SIGCHLD 和 zombie

關於 zombie

  • zombie: ps 列的 status 為 Z, 表示 process 已結束, 資源也釋放了, 等待 parent process 呼叫 wait/waitpid 回收它的 exit status, 裡面包含 exit code
  • zombie 是無法被 kill 的, 僵屍無敵啊!!
  • zombie 會占據極小的系統資源, 如可 forked 的 process 數
  • 不註冊 SIGCHLD = 註冊 signal handler 為 SIG_IGR => OS 會在 child process 掛掉時直接回收, 不會變 zombie

關於 SIGCHLD 收發時機

  • 即使 child process 收到 SIGKILL, 它也不會當下就掛掉而讓 parent process 在當下收到 SIGCHLD。process 得等取得 CPU 時才會「處理」 SIGKILL 而掛掉。總之, 系統不保證 SIGCHLD 收到的時間
  • child process 正式掛掉後, 當 parent process 取得 CPU 時, 系統會盡快讓它收到 SIGCHLD。換句話說, 若 parent process 被停住的期間「收到」 SIGCHILD, resume 後會立即收到 SIGCHILD
  • SIGCHLD 如同其它 signal, 不會被 queued, 只有保證在有註冊 handler 的情況下, 至少會呼叫一次 handler (細節說明請查: signal mask/block )
  • 註冊 handler 前收到 SIGCHLD 的話, 不同系統作法不同, 可能會補送也可能不會補送

關於 handler

  • signal handler 會被繼承 (見 man fork 開頭), 所以若 A 有註冊 SIGCHLD 則 A 的 child process 和後續 grandchild process 都有 SIGCHLD handler。寫 handler 時要注意是否能沿用下去, 或是明確在每次 fork 後重設一次 child process 的 signal handler
  • 標準寫法如下, status 裡會記錄許多有用資訊, 見 man waitpid:
int status;
while ((pid=waitpid(-1, &status, WNOHANG)) > 0) {
    // Do something
}

2012年3月14日 星期三

執行檔和檔案路徑注意事項

daemon 執行的時候, 通常會在程式裡 chdir 到 / 再開始做事, 藉此避免之後 daemon 所在的目錄無法被砍掉 (即使從檔案系統上下指令砍掉該目錄, 實際上它仍存在, 要等 daemon 結束才會真的釋放)。

程式執行中若需要寫入一些暫存檔, 要考慮到權限問題, 可以寫到 /tmp 最省事, 若需要永久保留, 考慮寫到 $HOME/.PROG/ 下, 不要直接寫到執行檔目錄下。平時開發寫到執行檔目錄下沒有問題, 但當需要將程式打包成套件, 裝到系統目錄時 (如 /usr/bin、/usr/local/bin), 就會有權限問題而無法寫入。

2012年2月14日 星期二

Linux process priorities and scheduling 心得

摘要一下讀了 TLPI 後的心得。

scheduling policy

POSIX 規定了幾種 scheduling policy, 它們的優先權如下:

SCHED_FIFO = SCHED_RR > SCHED_OTHER ~ SCHED_BATCH > SCHED_IDLE

舉例來說: SCHED_FIFO (99) > SCHED_FIFO (1) > SCHED_OTHER -20 (0) > SCHED_OTHER 19 (0) > SCHED_IDLE (0)

括號裡的數字是 process 的 static priority; -20 和 19 是 nice value, 見後文說明。

各 policy 的效果為:

  • 所有 policy 都是 preemptive, 也就是高優先權 process 想執行的時候, 會搶走執行中低優先權 process 的 CPU
  • SCHED_FIFOSCHED_RR 的 static priority 範圍必須落在 1 ~ 99, 剩下三個 (SCHED_OTHER、...) 只能設 priority = 0。達到的效果是 SCHED_FIFOSCHED_RR 永遠會比後幾種 policy 先執行
  • SCHED_FIFOSCHED_RR 是 real-time scheduling, 不過是 linux kernel 盡可能做到即時, 真的要做 real-time system (如汽車), 得用改過的 linux kernel
  • SCHED_OTHERSCHED_BATCH 的 static priority 都為 0, 所以會另外參考 dynamic priority 來決定順序, 這個值主要取決於 nice value (用 nice/renice 設), 只是參考值, 較低的 nice value 不會永遠表示較高的 dynamic priority
  • 沒做設定的話, 預設 policy 是 SCHED_OTHER
  • SCHED_BATCH 用在不需互動的程式, 會減少 wake-up 的頻率
  • nice value 對 SCHED_IDLE 無效, 這個 policy 會保證有最低優先權
  • 以上所有效果都會繼承到 sub-process

所以, 若用 SHCED_OTHER 配上 nice value, 可達到優先效果, 也不會有 process 餓死, 都搶不到 CPU。若用了 SCHED_FIFOSCHED_RR, 要小心搶光 CPU 資源的情況。用 SCHED_OTHER 配上負值 nice value 也要小心。

保險機制

做為保險, 可用 setrlimit(RLIMIT_CPU) 限制執行時間, 超過 soft limit 會收到 SIGXCPU, 預設會掛掉該 process。若沒掛的話, 之後每秒鐘會收到一次 SIGXCPU, 直到超過 hard limit 收到 SIGKILL, 保證掛掉該 process。在這之間就有操作空間來調整自己的優先權。

或用 setrlimit(RLIMIT_RTTIME), 來限制在 real-time scheduling policy 下最長執行的時間, 遇到 blocking system call 後會歸零, 可避免失控超時。超時後的行為和 setrlimit(RLIMIT_CPU) 相同。 setrlimit 也和設 scheduling 一樣, 會繼承到 sub-process。

Affinity

Linux 另有特別的 system call 可限制 process 只能跑在那些 CPU, 在 man sched_setaffinity 的 CONFORMING TO 該節有註明這是 linux-specific 的功能。

透過 set affinity, 可以滿足一些特殊需求:

  • 一台 8 core 的 server, 跑 8 個 process 限制它們各自用同一個 core 來服務大量 client, 讓 context switch 的次數降到最低
  • 限制某些類型的 process 只能用部份 CPU, 確保隨時有餘力服務其它 process。比方說留一個 CPU 不跑 real-time process, 至少失控時還能登入使用 shell 處理
  • 若 multi-thread 的程式沒寫好容易掛, 限制它們只跑在一個 core, 也許比較不會當 (這是我看 stackoverflow 裡某位路人提到他的用法 ...)

sched_setaffinity() 可以設在 process 也可設在 thread 上, 用的時候注意一下 man page 針對 pid 的說明。這個值也會繼承到 sub-process

另外, 經 wens 提醒, 還有 pthread_setaffinity_np() 可用來設 thread 的 affinity。查了一下 man page, 它是基於 sched_setaffinity() 的實作。待比較熟 multi-thread、pthread 的事情後, 大概會比較清楚為什麼要多包一個 pthread_setaffinity_np() 吧。

參考資料

  • TLPI ch35
  • man sched_setscheduler
  • man sched_setaffinity
  • man pthread_setaffinity_np

2012年1月29日 星期日

讀懂函式庫的 man page

初學 Linux 時, 覺得 man page 真不是寫給一般人看的, 到像是寫給已經懂的人備忘用的。即使現在已比較習慣讀 man page 了, 還是這麼覺得。

以前只知道 man function 看要引入什麼 header, 了解函式的參數、傳回值。最近才明白裡面的其它資訊。

背景知識可以搜尋 "how to read man page" 或看 Wikipedia 介紹得知。這裡只針對使用函式庫講基礎知識。另外這裡有顯示彩色 man page 的設定

man 3 sqrt 為例:

SQRT(3)                                          Linux Programmer's Manual                                         SQRT(3)

NAME
       sqrt, sqrtf, sqrtl - square root function

SYNOPSIS
       #include <math.h>

       double sqrt(double x);
       float sqrtf(float x);
       long double sqrtl(long double x);

       Link with -lm.

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sqrtf(), sqrtl():
           _BSD_SOURCE || _SVID_SOURCE || _XOPEN_SOURCE >= 600 || _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L;
           or cc -std=c99
  • #include 表示需要的 header
  • 下面有函式的 signature
  • Link with -lm 表示使用 gcc 編譯時要加 -lm, ld 才會找到 libm.so。詳細的運作過程見《ld, ld.so 和 ldconfig 的行為》
  • Feature Test Macro 是 UNIX 跨平台用的一套規範, 見 man feature_test_macros 了解 feature test macro 的作用, 以及 glibc 認得的類型。feature test macro 要定義在檔案的一開頭才行, 或用 gcc -D 定義。

關於 feature test macro, 這裡指出要定義 _BSD_SOURCE、...、_XOPEN_SOURCE 等, 才能使用 sqrtf() 和 sqrtl() (math.h 才會引入它們的宣告}}}。反之, sqrt() 有跨所有平台, 可直接使用。

這裡這裡有相關說明, 解釋 -std=c99-std=gnu99 的效果, 會定義不同的 feature test macro。若沒有考慮可攜性的話, 用 c99 編不過時改用 gnu99 可編過, 不用擔心會有什麼副作用。

所以, 要使用特定函式時, 要先看 man FUNCTION 了解需要定義的 feature test macro, 讓 compiler 可以編過。再來從 man FUNCTION 得知 static linking 需要的參數。

要檢查目前程式定義了那些 feature test macro, 可以用:

gcc -E -dD MYPROG.c

除查看 feature test macro 外, 用來查引入的常數也很方便, 比方說忘了 stdin 的 file number 的常數, 引入 unistd.h 後, 用 gcc 展開看一看, 就會找到#define STDIN_FILENO 0

2012年1月27日 星期五

signal 小知識

TLPI 長知識。

signal 設定和 fork

fork 後會繼承 signal 全部設定, 而 exec 後會保留 ignore 的設定, 但因為 address space 不同, 會將有設過的 signal handler 改回 default handler(見 man 7 signal)。

先送 SIGTERM 再送 SIGKILL 砍程式

若程式有照標準寫, 可能會有 SIGTERM 的 handler, 在收到這 signal 時做些清理動作 (砍暫存檔、釋放資源等), 再自我了結。而 SIGKILL 就直接掛了。所以, 先給人家一個機會掛得優雅一些, 若對方拒絕的話, 再狠一點直接掛了它。

附帶一提, ctrl + c 是送出 SIGINT。

執行 signal handler 的時機

process 得先拿到 CPU 才能執行 signal handler, 換句話說, 若 process P 處於 stop 的狀態 (D、S、T), 或系統太忙撥不出時間給它, 即使已送出 SIGTERM 給 P 了, P 也無法處理。而送 signal 的一方只能由 signal system call 的返回值判斷 P 仍活著, 有成功送出 signal, 無法得知 P 何時能處理它。

特別的是, SIGKILL 一送出對方就掛了, 不用取得 CPU 再掛掉。沒看到有特別的文獻指出 SIGKILL 這點特別的行為。man 7 signal 只有指出「The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored」。就實用角度來看, 讓 SIGKILL、SIGSTOP 立即生效, 確實比較合理。

SIGHUP 的行為

terminal 斷線 (hangup) 時, terminal 的主控 process 會收到 SIGHUP。而它的預設行為是終結程式, 所以關掉 terminal 時, 裡面的程式會直接結束。

bash 和 ksh 在結束的時候會送 SIGHUP 給背景程式, 若背景程式也沒寫 SIGHUP handler, 那它們也會一起結束。這是關掉 terminal, 全部程式會一起結束的原因。

要避免這個行為, 可用 nohup 執行程式, 它會做 I/O 重導、執行 signal (SIGHUP, SIG_IGN), 然後執行目標程式。或是用 bash 的 disown -h, 再放到背景跑, 或用 gdb attach process, 再輸入 handle SIGHUP nopass

用 SIGQUIT 產生 core dump

在 terminal 按 ctrl + \ 時會送出 SIGQUIT 給前景程式, 這個 signal 的作用是要就該程式自我了結並產生 core dump。當然, 一些設定要先設好, 才會有 core dump

這有許多好處:

  • 程式進無窮迴圈時按 ctrl + \, 再用 gdb 列 backtrace 看卡在那。雖說直接 用 gdb attach 也 ok。若高中練 ACM 知道這個方法, 可省下一些除錯時間 (遠目)。
  • 方便測試 core dump 設定, 不用另寫個小程式故意寫入不合法的位置。

在 Fedora 下裝 id-utils

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