[授權發表]進程和進程的基本操作

by falcon [email protected] of TinyLab.org
2008-02-21

最初發表:泰曉科技 – 聚焦嵌入式 Linux,追本溯源,見微知著!
原文鏈接:進程和進程的基本操作
評論說明:爲更好地聚合大家的討論,請到上面原文的評論區回覆。


【注】這是開源書籍《C語言編程透視》第七章,如果您喜歡該書,請關注我們的新浪微博@泰曉科技

前言

進程作爲程序真正發揮作用時的“形態”,我們有必要對它的一些相關操作非常熟悉,這一節主要描述進程相關的概念和操作,將介紹包括程序、進程、作業等基本概念以及進程狀態查詢、進程通信等相關的操作。

什麼是程序,什麼又是進程

程序是指令的集合,而進程則是程序執行的基本單元。爲了讓程序完成它的工作,必須讓程序運行起來成爲進程,進而利用處理器資源、內存資源,進行各種I/O操作,從而完成某項特定工作。

從這個意思上說,程序是靜態的,而進程則是動態的。

進程有區別於程序的地方還有:進程除了包含程序文件中的指令數據以外,還需要在內核中有一個數據結構用以存放特定進程的相關屬性,以便內核更好地管理和調度進程,從而完成多進程協作的任務。因此,從這個意義上可以說“高於”程序,超出了程序指令本身。

如果進行過多進程程序的開發,又會發現,一個程序可能創建多個進程,通過多個進程的交互完成任務。在Linux下,多進程的創建通常是通過fork系統調用來實現。從這個意義上來說程序則”包含”了進程。

另外一個需要明確的是,程序可以由多種不同程序語言描述,包括C語言程序、彙編語言程序和最後編譯產生的機器指令等。

下面簡單討論Linux下面如何通過Shell進行進程的相關操作。

進程的創建

通常在命令行鍵入某個程序文件名以後,一個進程就被創建了。例如,

範例:讓程序在後臺運行

$ sleep 100 &
[1] 9298

範例:查看進程ID

pidof可以查看指定程序名的進程ID:

$ pidof sleep
9298

範例:查看進程的內存映像

$ cat /proc/9298/maps
08048000-0804b000 r-xp 00000000 08:01 977399     /bin/sleep
0804b000-0804c000 rw-p 00003000 08:01 977399     /bin/sleep
0804c000-0806d000 rw-p 0804c000 00:00 0          [heap]
b7c8b000-b7cca000 r--p 00000000 08:01 443354     
...
bfbd8000-bfbed000 rw-p bfbd8000 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命令查看進程屬性

查看系統當前所有進程的屬性:

$ 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 -5
349588 firefox-bin
 96612 xfce4-terminal
 88840 xfdesktop
 76332 gedit
 58920 scim-panel-gtk

範例:通過pstree查看進程親緣關係

系統所有進程之間都有“親緣”關係,可以通過pstree查看這種關係:

$ pstree

上面會打印系統進程調用樹,可以非常清楚地看到當前系統中所有活動進程之間的調用關係。

範例:用top動態查看進程信息

$ 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".pid" 
if [ -f $pidfile ]; then
   	OLDPID=$(cat $pidfile)
	ps -e -o "%p" | tr -d " " | grep -q "^$OLDPID$"
	[ $? -eq 0 ] && exit
fi

echo $$ > $pidfile

#... 代碼主體

#設置信號0的動作,當程序退出時觸發該信號從而刪除掉臨時文件
trap "rm $pidfile"      0

更多實現策略自己盡情發揮吧!

調整進程的優先級

在保證每個進程都能夠順利執行外,爲了讓某些任務優先完成,那麼系統在進行進程調度時就會採用一定的調度辦法,比如常見的有按照優先級的時間片輪轉的調度算法。這種情況下,可以通過renice調整正在運行的程序的優先級,例如:

範例:獲取進程優先級

$ ps -e -o "%p %c %n" | grep xfs
 5089 xfs               0

範例:調整進程的優先級

$ renice 1 -p 5089
renice: 5089: setpriority: Operation not permitted
$ sudo renice 1 -p 5089   #需要權限纔行
[sudo] password for falcon:
5089: old priority 0, new priority 1
$ ps -e -o "%p %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) SIGSEGV     12) SIGUSR2
13) SIGPIPE     14) SIGALRM     15) SIGTERM     16) SIGSTKFLT
17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU
25) SIGXFSZ     26) SIGVTALRM   27) SIGPROF     28) SIGWINCH
29) SIGIO       30) SIGPWR      31) SIGSYS      34) SIGRTMIN
35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3  38) SIGRTMIN+4
39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7  58) SIGRTMAX-6
59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) 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或者作業來控制進程,而pkillkillall提供了更多選擇,它們擴展了通過程序名甚至是進程的用戶名來控制進程的方法。更多用法請參考它們的手冊。

範例:查看進程退出狀態

當程序退出後,如何判斷這個程序是正常退出還是異常退出呢?還記得Linux下,那個經典"hello,world"程序嗎?在代碼的最後總是有條return 0語句。這個return 0實際上是讓程序員來檢查進程是否正常退出的。如果進程返回了一個其他的數值,那麼可以肯定地說這個進程異常退出了,因爲它都沒有執行到return 0這條語句就退出了。

那怎麼檢查進程退出的狀態,即那個返回的數值呢?

在Shell中,可以檢查這個特殊的變量$?,它存放了上一條命令執行後的退出狀態。

$ test1
bash: 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編程時的一些用法。

範例:無名管道(pipe)

在Linux下,可以通過|連接兩個程序,這樣就可以用它來連接後一個程序的輸入和前一個程序的輸出,因此被形象地叫做個管道。在C語言中,創建無名管道非常簡單方便,用pipe函數,傳入一個具有兩個元素的int型的數組就可以。這個數組實際上保存的是兩個文件描述符,父進程往第一個文件描述符裏頭寫入東西后,子進程可以從第一個文件描述符中讀出來。

如果用多了命令行,這個管子|應該會經常用。比如上面有個演示把ps命令的輸出作爲grep命令的輸入:

$ ps -ef | grep init

也許會覺得這個“管子”好有魔法,竟然真地能夠鏈接兩個程序的輸入和輸出,它們到底是怎麼實現的呢?實際上當輸入這樣一組命令時,當前Shell會進行適當的解析,把前面一個進程的輸出關聯到管道的輸出文件描述符,把後面一個進程的輸入關聯到管道的輸入文件描述符,這個關聯過程通過輸入輸出重定向函數dup(或者fcntl)來實現。

範例:有名管道(named pipe)

有名管道實際上是一個文件(無名管道也像一個文件,雖然關係到兩個文件描述符,不過只能一邊讀另外一邊寫),不過這個文件比較特別,操作時要滿足先進先出,而且,如果試圖讀一個沒有內容的有名管道,那麼就會被阻塞,同樣地,如果試圖往一個有名管道里寫東西,而當前沒有程序試圖讀它,也會被阻塞。下面看看效果。

$ mkfifo fifo_test    #通過mkfifo命令創建一個有名管道
$ echo "fewfefe" > fifo_test
#試圖往fifo_test文件中寫入內容,但是被阻塞,要另開一個終端繼續下面的操作
$ cat fifo_test        #另開一個終端,記得,另開一個。試圖讀出fifo_test的內容
fewfefe

這裏的echocat是兩個不同的程序,在這種情況下,通過echocat啓動的兩個進程之間並沒有父子關係。不過它們依然可以通過有名管道通信。

這樣一種通信方式非常適合某些特定情況:例如有這樣一個架構,這個架構由兩個應用程序構成,其中一個通過循環不斷讀取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/bash

FIFO=./fifo_test
CI=$QUERY_STRING

[ -z "$CI" ] && echo "the control info should not be empty" && exit 

echo -e "content-type: text/html\n\n"
echo $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函數調用。

範例:信號(Signal)

信號是軟件中斷,Linux用戶可以通過kill命令給某個進程發送一個特定的信號,也可以通過鍵盤發送一些信號,比如CTRL+C可能觸發SGIINT信號,而CTRL+\可能觸發SGIQUIT信號等,除此之外,內核在某些情況下也會給進程發送信號,比如在訪問內存越界時產生SGISEGV信號,當然,進程本身也可以通過killraise等函數給自己發送信號。對於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/bash

function 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 %1
sleep 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環境高級編程》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章