$ sleep 100 &[1] 9298
用
pidof可以查看指定程序名稱的進程ID:
$ pidof sleep9298
$ cat /proc/9298/maps08048000-0804b000 r-xp 00000000 08:01 977399 /bin/sleep0804b000-0804c000 rw-p 000300978:01009 /bin/sleep0804c000-0806d000 rw-p 0804c000 00:00 0 [heap]b7c8b000-b7cca000 r--p 00000000 08:01 443354...b--p 000000000 08:01 443354...bdbed 00:00 0 [stack]ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
程式被執行後,就被載入到記憶體中,成為了一個行程。上面顯示了該進程的記憶體映像(虛擬記憶體),包括程式指令、數據,以及一些用於存放程式命令列參數、環境變數的堆疊空間,用於動態記憶體申請的堆空間都被分配好。
關於程式在命令列執行過程的細節,請參考《Linux 命令列下程式執行的一剎那》。
實際上,創建一個進程,也就是說讓程式運行,還有其他的辦法,例如,透過一些配置讓系統啟動時自動啟動程序(具體參考
man init),或者是透過配置
crond(或者
at)讓它定時啟動程式。除此之外,還有一個方式,那就是編寫Shell 腳本,把程式寫入一個腳本文件,當執行腳本文件時,文件中的程式將被執行而成為進程。這些方式的細節就不介紹,以下了解如何查看進程的屬性。
需要補充一點的是:在命令列下執行程序,可以透過
ulimit內建指令來設定行程可以利用的資源,例如行程可以開啟的最大檔案描述符個數,最大的堆疊空間,虛擬記憶體空間等。具體用法見
help ulimit。
可以透過
ps命令查看進程相關屬性和狀態,這些資訊包括進程所屬用戶,進程對應的程序,進程對
cpu和記憶體的使用情況等資訊。熟悉如何查看它們有助於進行相關的統計分析等操作。
查看系統目前所有進程的屬性:
$ ps -ef
查看命令中包含某字符的程式對應的進程,進程
ID是1 。
TTY為?表示和終端沒有關聯:
$ ps -C init PID TTY TIME CMD 1 ? 00:00:01 init
選擇某個特定使用者啟動的進程:
$ ps -U falcon
依照指定格式輸出指定內容,下方輸出命令名稱和
cpu使用率:
$ ps -e -o %C %c
列印
cpu使用率最高的前4 個程序:
$ ps -e -o %C %c | sort -u -k1 -r | head -5 7.5 firefox-bin 1.1 Xorg 0.8 scim-panel-gtk 0.2 scim-bridge
取得使用虛擬記憶體最大的5 個進程:
$ ps -e -o %z %c | sort -n -k1 -r | head -5349588 firefox-bin 96612 xfce4-terminal 88840 xfdesktop 76332 gedit 58920 scim-panel-gtk
系統所有進程之間都有「親緣」關係,可以透過
pstree看看這種關係:
$ pstree
上面會列印系統進程呼叫樹,可以非常清楚地看到目前系統中所有活動進程之間的呼叫關係。
$ top
該命令最大特點是可以動態地查看進程信息,當然,它還提供了一些其他的參數,例如
-S可以依照累計執行時間的大小排序來查看,也可以透過
-u查看指定使用者啟動的進程等。
補充:
top命令支援互動式,例如它支持
u命令顯示用戶的所有進程,支援透過
k命令殺掉某個進程;如果使用
-n 1選項可以啟用批次模式,具體用法為:
$ top -n 1 -b
下面來討論一個有趣的問題:如何讓一個程式在同一時間只有一個在運作。
這意味著當一個程式正在執行時,它將不能再啟動。那該怎麼做呢?
假如一份相同的程序被複製成了很多份,並且具有不同的文件名被放在不同的位置,這個將比較糟糕,所以考慮最簡單的情況,那就是這份程序在整個系統上是唯一的,而且名字也是唯一的。這樣的話,有哪些方法可以回答上面的問題呢?
總的機理是:在程式開頭檢查自己有沒有執行,如果執行了則停止否則繼續執行後續程式碼。
策略則是多樣的,由於前面的假設已經保證程式檔案名稱和程式碼的唯一性,所以透過
ps指令找出目前所有行程對應的程式名,逐一與自己的程式名稱比較,如果已經有,那麼表示自己已經執行了。
ps -e -o %c | tr -d | grep -q ^init$ #查看當前程式是否執行[ $? -eq 0 ] && exit #如果在,那麼退出, $?表示上一條指令是否執行成功
每次運行時先在指定位置檢查是否存在一個保存自己進程
ID的文件,如果不存在,那麼繼續執行,如果存在,那麼查看該進程
ID是否正在運行,如果在,那麼退出,否則往該檔案重新寫入新的進程
ID,並繼續。
pidfile=/tmp/$0.pidif [ -f $pidfile ]; then OLDPID=$(cat $pidfile) ps -e -o %p | tr -d | grep -q ^$OLDPID$ [ $? -eq 0 ] && exitfiecho $$ > $pidfile#... 代碼主體#設定訊號0的動作,當程式退出時觸發該訊號從而刪除掉臨時檔案trap rm $pidfile 0
更多實現策略自己盡情發揮吧!
在確保每個行程都能夠順利執行外,為了讓某些任務優先完成,那麼系統在進行進程調度時就會採用一定的調度辦法,例如常見的有按照優先權的時間片輪轉的調度演算法。這種情況下,可以透過
renice調整正在運行的程式的優先級,例如:`
$ ps -e -o %p %c %n | grep xfs 5089 xfs 0
$ renice 1 -p 5089renice: 5089: setpriority: Operation not permitted$ sudo renice 1 -p 5089 #需要權限才行[sudo] password for falcon:5089: old priority 0, new priority 1$ %c %n | grep xfs #再看看,優先權已經被調整過來了5089 xfs 1
既然可以透過命令列執行程序,創建進程,那麼也有辦法結束它。可以透過
kill命令給用戶自己啟動的進程發送某個訊號讓進程終止,當然「萬能」的
root幾乎可以
kill所有進程(除了
init之外)。例如,
$ sleep 50 & #啟動一個程序[1] 11347$ kill 11347
kill命令預設會發送終止訊號(
SIGTERM)給程序,讓程序退出,但是
kill還可以發送其他訊號,這些訊號的定義可以透過
man 7 signal查看到,也可以透過
kill -l列出來。
$ man 7 signal$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGGUV 12) SIGS ) SIGTERM 16) SIGSTKFLT17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) 非SIGPWR 31) SIGSYS 34) SIGRTMIN35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+439) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 43SRT+5 40) SIGRTMIN+6 41) SIGRTMIN+7 433SRT) ) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+1247) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-1451) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGBMAX S -9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-659) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-263) SIGRTMAX-1 64) SIGRTMAX
例如,用
kill命令發送
SIGSTOP信號給某個程序,讓它暫停,然後發送
SIGCONT訊號讓它繼續運作。
$ sleep 50 &[1] 11441$ jobs[1]+ Running sleep 50 &$ kill -s SIGSTOP 11441 #這個等同於我們對一個前台進程執行CTRL+Z操作$ jobs[1]+ Stopped sleep 50$ kill - s SIGCONT 11441 #這個等同之前我們使用bg %1操作讓一個後台程序運作起來$ jobs[1]+ Running sleep 50 &$ kill %1 #在目前會話(session)下,也可以透過作業號控制進程$ jobs[1]+ Terminated sleep 50
可見
kill指令提供了非常好的功能,不過它只能根據進程的
ID或作業來控制進程,而
pkill和
killall提供了更多選擇,它們擴展了透過程式名稱甚至是進程的使用者名稱來控制進程的方法。更多用法請參考它們的手冊。
當程序退出後,如何判斷這個程序是正常退出還是異常退出?還記得Linux 下,那個經典
hello world程序嗎?在代碼的最後總是有條
return 0語句。這個
return 0實際上是讓程式設計師來檢查進程是否正常退出的。如果進程回傳了一個其他的數值,那麼可以肯定地說這個進程異常退出了,因為它都沒有執行到
return 0這句語句就退出了。
那要怎麼檢查進程退出的狀態,也就是那個回傳的數值呢?
在
Shell中,可以檢查這個特殊的變數
$?,它存放了上一條命令執行後的退出狀態。
$ test1bash: test1: command not found$ echo $?127$ cat ./test.c | grep hello$ echo $?1$ cat ./test.c | grep hi printf(hi, myself!n);$ echo $?0
似乎返回0 成為了一個潛規則,雖然沒有標準明確規定,不過當程序正常返回時,總是可以從
$?中偵測到0,但是異常時,總是偵測到一個非0 值。這就告訴我們在程序的最後最好是跟上一個
exit 0以便任何人都可以通過檢測
$?確定程序是否正常結束。如果有一天,有人偶爾用到你的程序,試圖檢查它的退出狀態,而你卻在程序的末尾莫名地返回了一個
-1或者1,那麼他將會很苦惱,會懷疑他自己編寫的程式到底哪個地方出了問題,檢查半天卻不知所措,因為他太信任你了,竟然從頭到尾都沒有懷疑你的程式設計習慣可能會與眾不同!
為便於設計和實現,通常一個大型的任務都被分割成較小的模組。不同模組之間啟動後成為進程,它們之間如何通信以便交互數據,協同工作呢?在《UNIX 環境高級程式設計》一書中提到許多方法,諸如管道(無名管道和有名管道)、訊號(
signal)、報文(
Message)隊列(訊息隊列)、共享記憶體(
mmap/munmap)、信號量(
semaphore,主要是同步用,行程之間,行程的不同執行緒之間)、套接口(
Socket,支援不同機器之間的進程通訊)等,而在Shell 中,通常直接用到的就有管道和訊號等。以下主要介紹管道和訊號機制在Shell 程式設計時的一些用法。
在Linux 下,可以透過
|連接兩個程序,這樣就可以用它來連接後一個程式的輸入和前一個程式的輸出,因此被形象化地叫做個管道。在C 語言中,建立無名管道非常簡單方便,使用
pipe函數,傳入一個具有兩個元素的
int型的數組就可以。這個陣列實際上保存的是兩個檔案描述符,父進程往第一個檔案描述符裡頭寫入東西後,子進程可以從第一個檔案描述符讀出來。
如果用多了命令列,這個管子
|應該會常用。例如上面有個示範把
ps命令的輸出作為
grep命令的輸入:
$ ps -ef | grep init
也許會覺得這個「管子」好有魔法,竟然真地能夠連結兩個程式的輸入和輸出,它們到底是怎麼實現的呢?實際上當輸入這樣一組指令時,目前Shell 會進行適當的解析,把前面一個行程的輸出關聯到管道的輸出檔描述符,把後面一個行程的輸入關聯到管道的輸入檔描述符,這個關聯過程透過輸入輸出重定向函數
dup(或者
fcntl)來實現。
有名管道實際上是一個文件(無名管道也像一個文件,雖然關係到兩個文件描述符,不過只能一邊讀另外一邊寫),不過這個文件比較特別,操作時要滿足先進先出,而且,如果試圖讀一個沒有內容的有名管道,那麼就會被阻塞,同樣地,如果試圖往一個有名管道裡寫東西,而當前沒有程式試圖讀它,也會被阻塞。下面看看效果。
$ mkfifo fifo_test #透過mkfifo指令建立一個有名管道$ echo fewfefe > fifo_test#試圖往fifo_test檔案中寫入內容,但是被阻塞,要另開一個終端繼續下面的操作$ cat fifo_test #另開一個終端,記得,另開一個。試圖讀出fifo_test的內容fewfefe
這裡的
echo和
cat是兩個不同的程序,在這種情況下,透過
echo和
cat啟動的兩個進程之間並沒有父子關係。不過它們依然可以透過有名管道進行通訊。
這樣一種通訊方式非常適合某些特定情況:例如有這樣一個架構,這個架構由兩個應用程式構成,其中一個透過循環不斷讀取
fifo_test中的內容,以便判斷,它下一步要做什麼。如果這個管道沒有內容,那麼它就會被阻塞在那裡,而不會因死循環而耗費資源,另外一個則作為一個控製程序不斷地往
fifo_test中寫入一些控制訊息,以便告訴之前的那個程式該做什麼。下面寫一個非常簡單的例子。可以設計一些控制碼,然後控製程式不斷地往
fifo_test裡頭寫入,然後應用程式根據這些控制碼完成不同的動作。當然,也可以往
fifo_test傳入除控制碼外的其他資料。
應用程式的程式碼
$ cat app.sh #!/bin/bash FIFO=fifo_test while :; do CI=`cat $FIFO` #CI --> Control Info case $CI in 0) echo The CONTROL number is ZERO, do something ... ;; 1) echo The CONTROL number is ONE, do something ... ;; *) echo The CONTROL number not recognized, do something else... ;; esac done
控製程式的程式碼
$ cat control.sh #!/bin/bash FIFO=fifo_test CI=$1 [ -z $CI ] && echo the control info should not be empty && exit echo $CI > $FIFO
一個程序透過管道控制另外一個程序的工作
$ chmod +x app.sh control.sh #修改這兩個程序的可執行權限,以便用戶可以執行它們$ ./app.sh #在一個終端啟動這個應用程序,在通過./control.sh發送控制碼以後查看輸出The CONTROL number is ONE, do something ... #發送1以後The CONTROL number is ZERO, do something ... #發送0以後The CONTROL number not recognized, do something else... #發送一個未知的控制碼以後$ ./control.sh 1 #在另外一個終端,發送控制訊息,控制應用程式的工作$ ./control.sh 0 $ ./control. sh 4343
這樣一種應用架構非常適合本地的多程式任務設計,如果結合
web cgi,那麼也將適合遠端控制的要求。引入
web cgi的唯一改變是,要把控製程序
./control.sh放到
web的
cgi目錄下,並對它作一些修改,以使它符合
CGI的規範,這些規範包括文檔輸出格式的表示(在文件開頭需要輸出
content-tpye: text/html以及一個空白行)和輸入參數的獲取
(web輸入參數都存放在
QUERY_STRING環境變數裡頭)。因此一個非常簡單的
CGI控製程式可以寫成這樣:
#!/bin/bashFIFO=./fifo_testCI=$QUERY_STRING[ -z $CI ] && echo the control info should not be empty && exitecho -e content-type: text/htmlnnecho $CI > $FIFO
在實際使用時,請確保
control.sh能夠訪問到
fifo_test管道,並且有寫入權限,以便透過瀏覽器控制
app.sh:
http://ipaddress_or_dns/cgi-bin/control.sh?0
問號
?後面的內容即
QUERY_STRING,類似之前的
$1。
這樣一種應用對於遠端控制,特別是嵌入式系統的遠端控制很有實際意義。在去年的暑期課程上,我們就透過這樣一種方式來實現馬達的遠端控制。首先,實現了一個簡單的應用程式以便控制馬達的轉動,包括轉速,方向等的控制。為了實現遠端控制,我們設計了一些控制碼,以便控制馬達轉動相關的不同屬性。
在C 語言中,如果要使用有名管道,和Shell 類似,只不過在讀寫資料時用
read,
write調用,在創建
fifo時用
mkfifo函數呼叫。
訊號是軟體中斷,Linux 使用者可以透過
kill命令會向某個進程發送特定的訊號,也可以透過鍵盤發送一些訊號,例如
CTRL+C可能觸發
SGIINT信號,而
CTRL+可能觸發
SGIQUIT訊號等,除此之外,核心在某些情況下也會向進程發送訊號,例如在存取記憶體越界時產生
SGISEGV訊號,當然,進程本身也可以透過
kill,
raise等函數給自己發送訊號。對於Linux 下支援的訊號類型,大家可以透過
man 7 signal或者
kill -l查看到相關清單和說明。
對於有些訊號,行程會有預設的反應動作,而有些訊號,行程可能直接會忽略,當然,使用者也可以對某些訊號設定專門的處理函數。在Shell 中,可以透過
trap指令(Shell 內建指令)來設定回應某個訊號的動作(某個指令或定義的某個函數),而在C 語言中可以透過
signal呼叫註冊某個訊號的處理函數。這裡僅僅是演示
trap命令的用法。
$ function signal_handler { echo hello, world.; } #定義signal_handler函數$ trap signal_handler SIGINT #執行此指令設定:收到SIGINT訊號時列印hello, world$ hello, world #按下CTRL+C,可以看到螢幕上輸出了hello, world字串
類似地,如果設定訊號0 的反應動作,那麼就可以用
trap來模擬C 語言程式中的
atexit程序終止函數的登記,即透過
trap signal_handler SIGQUIT設定的
signal_handler函數將在程式退出時執行。訊號0 是一個特別的訊號,在
POSIX.1中把訊號編號0 定義為空訊號,這常被用來確定一個特定進程是否仍舊存在。當一個程式退出時會觸發該訊號。
$ cat sigexit.sh#!/bin/bashfunction signal_handler { echo hello, world}trap signal_handler 0$ chmod +x sigexit.sh$ ./sigexit.sh #實際Shell編程會用該方式清理程式退出時來做一些來做一些事情臨時文件的收尾工作hello, world
當我們為完成一些複雜的任務而將多個命令通過
|,>,<, ;, (,)等組合在一起時,通常這個命令序列會啟動多個進程,它們間透過管道等進行通訊。而有時在執行一個任務的同時,還有其他的任務需要處理,那麼就常常會在指令序列的最後加上一個&,或是在執行指令後,按下
CTRL+Z讓前一個指令暫停。以便做其他的任務。等做完其他一些任務以後,再透過
fg指令把後台任務切換到前台。這樣一種控制過程通常被成為作業控制,而那些命令序列則被變成作業,這個作業可能涉及一個或多個程序,一個或多個行程。下面示範幾個常用的作業控制操作。
$ sleep 50 &[1] 11137
使用Shell 內建指令
fg把作業1 調到前台運行,然後按下
CTRL+Z讓該進程暫停
$ fg %1sleep 50^Z[1]+ Stopped sleep 50
$ jobs #查看當前作業情況,有一個作業停止[1]+ Stopped sleep 50$ sleep 100 & #讓另外一個作業在後台運行[2] 11138$ jobs #查看當前作業情況,一個正在運行,一個停止[ 1]+ Stopped sleep 50[2]- Running sleep 100 &
$ bg %1[2]+ sleep 50 &
不過,要在命令列下使用作業控制,需要目前Shell,核心終端驅動等對作業控制支援才行。
《UNIX 環境進階程式設計》