用 pgid 終止一組管道程序
之前寫 rtklib 的腳本時, 發現把管道丟到背景後,只會拿到管道最後一個程序的 pid, 要用 pgid 才能一次終止所有程序。 於是研究了一下怎麼拿到 pgid,和使用的方式。
正確關掉丟到背景的程序
以前以為在 sh 裡把程序丟到背景後,
就只能用 kill %1
的方式終止。
有一段時間還不知道為什麼腳本執行起來有問題。
最近才發現是 %
的表示法只能在互動式 shell 裡用,
在腳本裡是會被忽略的。
(那時候還開了 dash 測都沒問題,
原來 dash 也有分互動模式和腳本模式, 互動模式才可以用 %
。)
又 google 了很久,才學到可以用 $!
取得丟到背景的程序 pid。
會用到這功能,去年有寫過自動重啟某程序的腳本,
需要判斷該程序是不是還活著,
就踩了很多 $(jobs)
因為是在子程序中執行,
看不到母程序的子程序的搞笑事件。
( jobs > jobs-list
倒是可以運作,因為不是子程序。)
另外就是 rtklib 的 cli 版本程式, 都會很貼心的忽略 SIGHUP 訊號, 以讓程序在 shell 退出後還能繼續執行; 逼我要手動用 kill 終止。
rtkrcv -o rtkrcv.conf -p 5564 &
pid_rtkrcv=$!
trap on_exit EXIT TERM INT HUP
on_exit() {
kill $pid_rtkrcv
trap - EXIT
}
rtklib str2str 的串流問題
之前寫 rtklib 的腳本,因為不想用 http 明文傳登入資訊, 所以接 igs 的星曆都是用 curl 走 https, 再用管道傳給 str2str 串流到 tcp 給 rtkrcv 用。
curl --user-agent 'NTRIP RTKLIB (curl)' \
https://username:password@products.igs-ip.net/CLK93 \
| str2str -out tcpsvr://:5567 &
pid_ntrip=$!
on_exit() {
kill $pid_ntrip
trap - EXIT
}
trap on_exit EXIT INT TERM HUP
那時就有注意到,在終止後雖然 str2str 被殺掉了, 但 curl 仍執行了一段時間, 才因為寫入到已經關閉的程序,發生錯誤自己退出。
後來是解 另一個 bug : str2str 把 serial (com port) 串流到 tcp 時, 如果有多個客戶端同時連接,收到的內容會是錯的。 因為用到二個 str2str,只關掉最後一個程序前面的還是會繼續執行, 所以才想找一次關掉整個管道的方法。
程序群組
以前就有看過程序群組的概念 , 也就是 shell 會把一個管道中的所有程序分到同一個新群組, 這樣就可以一次終止整個群組來結束整個管道。 群組 id 也就是群組長程序 id,也就是群組中第一支程序的 id。 posix c 的 kill 函數,如果傳入的 pid 是負數, 就代表終止該程序群組中的所有程序。
當時沒有想太多,只覺得 shell 會處理好。
現在回頭看,在 kill %1
時,如果 %1
是一組管道,
那 shell 實際上做的就是終止整個程序群組。
而 shell 指令 kill 也和 c 的 kill 函數一樣,
可以接受負數來一次終止群組中的所有程序。
只是如果直接寫 kill -5566
,會被當成選項,
然後因為沒有 5566 這個項選項就報錯了。
可以用二個減號告訴 kill 之後的都不是選項:
kill -- -5566
,就能正確解析。
或在參數前面加個空白: kill ' -5566'
因為第一個字元不是減號,
kill 就不會把該參數當成選項,但仍能解析出負數。
要終止群組需要的是群組 id,也就是群組長的 id,然後傳給 kill。
但問題是在腳本中, $!
是管道中最後一個程序的 id,
而沒辦法知道一組管道的第一支程序的 id。
有二個解決方式,一是把整個管道包在一支子程序裡,
二是用 ps 查出該程序所屬的程序群組。
把管道包進單一子程序
這招是 google 時在 stackoverflow 上第一個找出的方法。
把原本 foo | bar &
寫成 (foo | bar) &
,
這樣就只有一支程序,所以最後一支程序也就是第一支程序,
也就是群組長了,在括號中的管道也會同一個群組中。
就能用 kill -- -$!
直接殺掉。
但缺點是會多包一層,多出一個子程序。
~ $ sleep 6m | sleep 8m &
[1] 12661
~ $ (sleep 6m | sleep 8m) &
[2] 12664
~ $ pstree $$
bash(9149)-+-bash(12664)-+-sleep(12665)
| `-sleep(12667)
|-pstree(12669)
|-sleep(12660)
`-sleep(12661)
但後來發現不管用,要開啟 job control 功能才有此效果, 在 dash 的非互動模式預設是關閉的, 也查不到是否為 posix 標準。
用 ps 列出該程序所屬的群組
雖然 $!
只有管道中最後一個程序的 id,
但用 ps -j $!
就能看到該程序的群組,顯示為 pgid。
查閱 ps 的手冊後,可以知道如果只要列出 pgid,
可以用以下選項: ps --no-headers --format pgid:1 $!
。
其中 format 是只列出特定欄位,而 :1
是指定欄位的寬度,
預設是似乎是 5,會出現一個前置空白,
如果搭配 kill 需要負數,要前置減號時會變成 - 5566
,
會無法解析,所以要消掉前置的空白。
pgid() {
ps --no-headers --format pgid:1 $1
}
str2str -in serial://serial0#ubx \
| str2str -out file://f9p.ubx::T \
-out tcpsvr://:5567 &
pgid_ublox=`pgid $!`
on_exit() {
kill -- -$pgid_ublox
trap - EXIT
}
trap on_exit EXIT INT TERM HUP
子 shell 的問題
寫腳本時碰到一個問題,偶爾 curl 會斷線退出, 所以我需要讓 curl 退出後重啟。 我很直覺寫了個 while 迴圈來處理:
while sleep 5
do curl --user-agent 'NTRIP RTKLIB (curl)' \
https://username:password@products.igs-ip.net/CLK93
done \
| str2str -out tcpsvr://:5567 &
pgid_ntrip=`pgid $!`
但後來發現這種寫法會讓 sleep curl str2str 三支程序 pgid 都和 shell 一樣, 而不會生出一個新程序群組。 所以在終止整個程序群組時也會終止 shell 自己。
然後我在 shell 又 trap 了終止信號, 造成在終止時發信號給自己, 又觸發一次事件處理,又發一次信號; 最後疊太多層 stack overflow。
on_exit() {
echo exiting
kill -- -$pgid_ublox
trap - EXIT
}
trap on_exit EXIT INT TERM HUP
找到問題後也想不到什麼好的辦法, 畢竟就是不會開新群組,只有原本的群組。 只能先把重複觸發事件處理的問題解決, 在收到信號就直接重置處理函數就不會重複觸發了。
on_exit() {
trap - EXIT INT TERM HUP
echo exiting
kill -- -$pgid_ublox
}
trap on_exit EXIT INT TERM HUP
至於在管道中使用 shell 內建語法造成無法新建群組的問題, 除非打開 job control, 不然也只能用 ps 和 awk 去抓了。
ps -o pgid:1,pid -au $USER \
| awk "(\$1==$pgid && \$2!=$pgid) {print \$2}" \
| xargs kill