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。

在 Fedora 下裝 id-utils

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