進程作為程序真正發(fā)揮作用時的“形態(tài)”,我們有必要對它的一些相關(guān)操作非常熟悉,這一節(jié)主要描述進程相關(guān)的概念和操作,將介紹包括程序、進程、作業(yè)等基本概念以及進程狀態(tài)查詢、進程通信等相關(guān)的操作。
程序是指令的集合,而進程則是程序執(zhí)行的基本單元。為了讓程序完成它的工作,必須讓程序運行起來成為進程,進而利用處理器資源、內(nèi)存資源,進行各種 I/O
操作,從而完成某項特定工作。
從這個意思上說,程序是靜態(tài)的,而進程則是動態(tài)的。
進程有區(qū)別于程序的地方還有:進程除了包含程序文件中的指令數(shù)據(jù)以外,還需要在內(nèi)核中有一個數(shù)據(jù)結(jié)構(gòu)用以存放特定進程的相關(guān)屬性,以便內(nèi)核更好地管理和調(diào)度進程,從而完成多進程協(xié)作的任務(wù)。因此,從這個意義上可以說“高于”程序,超出了程序指令本身。
如果進行過多進程程序的開發(fā),又會發(fā)現(xiàn),一個程序可能創(chuàng)建多個進程,通過多個進程的交互完成任務(wù)。在 Linux 下,多進程的創(chuàng)建通常是通過 fork
系統(tǒng)調(diào)用來實現(xiàn)。從這個意義上來說程序則”包含”了進程。
另外一個需要明確的是,程序可以由多種不同程序語言描述,包括 C 語言程序、匯編語言程序和最后編譯產(chǎn)生的機器指令等。
下面簡單討論 Linux 下面如何通過 Shell 進行進程的相關(guān)操作。
通常在命令行鍵入某個程序文件名以后,一個進程就被創(chuàng)建了。例如,
$ sleep 100 &
[1] 9298
用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]
程序被執(zhí)行后,就被加載到內(nèi)存中,成為了一個進程。上面顯示了該進程的內(nèi)存映像(虛擬內(nèi)存),包括程序指令、數(shù)據(jù),以及一些用于存放程序命令行參數(shù)、環(huán)境變量的??臻g,用于動態(tài)內(nèi)存申請的堆空間都被分配好。
關(guān)于程序在命令行執(zhí)行過程的細節(jié),請參考《Linux 命令行下程序執(zhí)行的一剎那》。
實際上,創(chuàng)建一個進程,也就是說讓程序運行,還有其他的辦法,比如,通過一些配置讓系統(tǒng)啟動時自動啟動程序(具體參考 man init
),或者是通過配置 crond
(或者 at
)讓它定時啟動程序。除此之外,還有一個方式,那就是編寫 Shell 腳本,把程序?qū)懭胍粋€腳本文件,當(dāng)執(zhí)行腳本文件時,文件中的程序?qū)⒈粓?zhí)行而成為進程。這些方式的細節(jié)就不介紹,下面了解如何查看進程的屬性。
需要補充一點的是:在命令行下執(zhí)行程序,可以通過 ulimit
內(nèi)置命令來設(shè)置進程可以利用的資源,比如進程可以打開的最大文件描述符個數(shù),最大的??臻g,虛擬內(nèi)存空間等。具體用法見 help ulimit
。
可以通過 ps
命令查看進程相關(guān)屬性和狀態(tài),這些信息包括進程所屬用戶,進程對應(yīng)的程序,進程對 cpu
和內(nèi)存的使用情況等信息。熟悉如何查看它們有助于進行相關(guān)的統(tǒng)計分析等操作。
查看系統(tǒng)當(dāng)前所有進程的屬性:
$ ps -ef
查看命令中包含某字符的程序?qū)?yīng)的進程,進程 ID
是 1 。 TTY
為?表示和終端沒有關(guān)聯(lián):
$ ps -C init
PID TTY TIME CMD
1 ? 00:00:01 init
選擇某個特定用戶啟動的進程:
$ ps -U falcon
按照指定格式輸出指定內(nèi)容,下面輸出命令名和 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
獲取使用虛擬內(nèi)存最大的 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
系統(tǒng)所有進程之間都有“親緣”關(guān)系,可以通過 pstree
查看這種關(guān)系:
$ pstree
上面會打印系統(tǒng)進程調(diào)用樹,可以非常清楚地看到當(dāng)前系統(tǒng)中所有活動進程之間的調(diào)用關(guān)系。
$ top
該命令最大特點是可以動態(tài)地查看進程信息,當(dāng)然,它還提供了一些其他的參數(shù),比如 -S
可以按照累計執(zhí)行時間的大小排序查看,也可以通過 -u
查看指定用戶啟動的進程等。
補充: top
命令支持交互式,比如它支持 u
命令顯示用戶的所有進程,支持通過 k
命令殺掉某個進程;如果使用 -n 1
選項可以啟用批處理模式,具體用法為:
$ top -n 1 -b
下面來討論一個有趣的問題:如何讓一個程序在同一時間只有一個在運行。
這意味著當(dāng)一個程序正在被執(zhí)行時,它將不能再被啟動。那該怎么做呢?
假如一份相同的程序被復(fù)制成了很多份,并且具有不同的文件名被放在不同的位置,這個將比較糟糕,所以考慮最簡單的情況,那就是這份程序在整個系統(tǒng)上是唯一的,而且名字也是唯一的。這樣的話,有哪些辦法來回答上面的問題呢?
總的機理是:在程序開頭檢查自己有沒有執(zhí)行,如果執(zhí)行了則停止否則繼續(xù)執(zhí)行后續(xù)代碼。
策略則是多樣的,由于前面的假設(shè)已經(jīng)保證程序文件名和代碼的唯一性,所以通過 ps
命令找出當(dāng)前所有進程對應(yīng)的程序名,逐個與自己的程序名比較,如果已經(jīng)有,那么說明自己已經(jīng)運行了。
ps -e -o "%c" | tr -d " " | grep -q ^init$ #查看當(dāng)前程序是否執(zhí)行
[ $? -eq 0 ] && exit #如果在,那么退出, $?表示上一條指令是否執(zhí)行成功
每次運行時先在指定位置檢查是否存在一個保存自己進程 ID
的文件,如果不存在,那么繼續(xù)執(zhí)行,如果存在,那么查看該進程 ID
是否正在運行,如果在,那么退出,否則往該文件重新寫入新的進程 ID
,并繼續(xù)。
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
#... 代碼主體
#設(shè)置信號0的動作,當(dāng)程序退出時觸發(fā)該信號從而刪除掉臨時文件
trap "rm $pidfile" 0
更多實現(xiàn)策略自己盡情發(fā)揮吧!
在保證每個進程都能夠順利執(zhí)行外,為了讓某些任務(wù)優(yōu)先完成,那么系統(tǒng)在進行進程調(diào)度時就會采用一定的調(diào)度辦法,比如常見的有按照優(yōu)先級的時間片輪轉(zhuǎn)的調(diào)度算法。這種情況下,可以通過 renice
調(diào)整正在運行的程序的優(yōu)先級,例如:`
$ 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 #需要權(quán)限才行
[sudo] password for falcon:
5089: old priority 0, new priority 1
$ ps -e -o "%p %c %n" | grep xfs #再看看,優(yōu)先級已經(jīng)被調(diào)整過來了
5089 xfs 1
既然可以通過命令行執(zhí)行程序,創(chuàng)建進程,那么也有辦法結(jié)束它??梢酝ㄟ^ kill
命令給用戶自己啟動的進程發(fā)送某個信號讓進程終止,當(dāng)然“萬能”的 root
幾乎可以 kill
所有進程(除了 init
之外)。例如,
$ sleep 50 & #啟動一個進程
[1] 11347
$ kill 11347
kill
命令默認(rèn)會發(fā)送終止信號( SIGTERM
)給程序,讓程序退出,但是 kill
還可以發(fā)送其他信號,這些信號的定義可以通過 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
命令發(fā)送 SIGSTOP
信號給某個程序,讓它暫停,然后發(fā)送 SIGCONT
信號讓它繼續(xù)運行。
$ sleep 50 &
[1] 11441
$ jobs
[1]+ Running sleep 50 &
$ kill -s SIGSTOP 11441 #這個等同于我們對一個前臺進程執(zhí)行CTRL+Z操作
$ jobs
[1]+ Stopped sleep 50
$ kill -s SIGCONT 11441 #這個等同于之前我們使用bg %1操作讓一個后臺進程運行起來
$ jobs
[1]+ Running sleep 50 &
$ kill %1 #在當(dāng)前會話(session)下,也可以通過作業(yè)號控制進程
$ jobs
[1]+ Terminated sleep 50
可見 kill
命令提供了非常好的功能,不過它只能根據(jù)進程的 ID
或者作業(yè)來控制進程,而 pkill
和 killall
提供了更多選擇,它們擴展了通過程序名甚至是進程的用戶名來控制進程的方法。更多用法請參考它們的手冊。
當(dāng)程序退出后,如何判斷這個程序是正常退出還是異常退出呢?還記得 Linux 下,那個經(jīng)典 hello world
程序嗎?在代碼的最后總是有條 return 0
語句。這個 return 0
實際上是讓程序員來檢查進程是否正常退出的。如果進程返回了一個其他的數(shù)值,那么可以肯定地說這個進程異常退出了,因為它都沒有執(zhí)行到 return 0
這條語句就退出了。
那怎么檢查進程退出的狀態(tài),即那個返回的數(shù)值呢?
在 Shell
中,可以檢查這個特殊的變量 $?
,它存放了上一條命令執(zhí)行后的退出狀態(tài)。
$ 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 成為了一個潛規(guī)則,雖然沒有標(biāo)準(zhǔn)明確規(guī)定,不過當(dāng)程序正常返回時,總是可以從 $?
中檢測到 0,但是異常時,總是檢測到一個非 0 值。這就告訴我們在程序的最后最好是跟上一個 exit 0
以便任何人都可以通過檢測 $?
確定程序是否正常結(jié)束。如果有一天,有人偶爾用到你的程序,試圖檢查它的退出狀態(tài),而你卻在程序的末尾莫名地返回了一個 -1
或者 1,那么他將會很苦惱,會懷疑他自己編寫的程序到底哪個地方出了問題,檢查半天卻不知所措,因為他太信任你了,竟然從頭至尾都沒有懷疑你的編程習(xí)慣可能會與眾不同!
為便于設(shè)計和實現(xiàn),通常一個大型的任務(wù)都被劃分成較小的模塊。不同模塊之間啟動后成為進程,它們之間如何通信以便交互數(shù)據(jù),協(xié)同工作呢?在《UNIX 環(huán)境高級編程》一書中提到很多方法,諸如管道(無名管道和有名管道)、信號(signal
)、報文(Message
)隊列(消息隊列)、共享內(nèi)存(mmap/munmap
)、信號量(semaphore
,主要是同步用,進程之間,進程的不同線程之間)、套接口(Socket
,支持不同機器之間的進程通信)等,而在 Shell 中,通常直接用到的就有管道和信號等。下面主要介紹管道和信號機制在 Shell 編程時的一些用法。
在 Linux 下,可以通過 |
連接兩個程序,這樣就可以用它來連接后一個程序的輸入和前一個程序的輸出,因此被形象地叫做個管道。在 C 語言中,創(chuàng)建無名管道非常簡單方便,用 pipe
函數(shù),傳入一個具有兩個元素的 int
型的數(shù)組就可以。這個數(shù)組實際上保存的是兩個文件描述符,父進程往第一個文件描述符里頭寫入東西后,子進程可以從第一個文件描述符中讀出來。
如果用多了命令行,這個管子 |
應(yīng)該會經(jīng)常用。比如上面有個演示把 ps
命令的輸出作為 grep
命令的輸入:
$ ps -ef | grep init
也許會覺得這個“管子”好有魔法,竟然真地能夠鏈接兩個程序的輸入和輸出,它們到底是怎么實現(xiàn)的呢?實際上當(dāng)輸入這樣一組命令時,當(dāng)前 Shell 會進行適當(dāng)?shù)慕馕觯亚懊嬉粋€進程的輸出關(guān)聯(lián)到管道的輸出文件描述符,把后面一個進程的輸入關(guān)聯(lián)到管道的輸入文件描述符,這個關(guān)聯(lián)過程通過輸入輸出重定向函數(shù) dup
(或者 fcntl
)來實現(xiàn)。
有名管道實際上是一個文件(無名管道也像一個文件,雖然關(guān)系到兩個文件描述符,不過只能一邊讀另外一邊寫),不過這個文件比較特別,操作時要滿足先進先出,而且,如果試圖讀一個沒有內(nèi)容的有名管道,那么就會被阻塞,同樣地,如果試圖往一個有名管道里寫東西,而當(dāng)前沒有程序試圖讀它,也會被阻塞。下面看看效果。
$ mkfifo fifo_test #通過mkfifo命令創(chuàng)建一個有名管道
$ echo "fewfefe" > fifo_test
#試圖往fifo_test文件中寫入內(nèi)容,但是被阻塞,要另開一個終端繼續(xù)下面的操作
$ cat fifo_test #另開一個終端,記得,另開一個。試圖讀出fifo_test的內(nèi)容
fewfefe
這里的 echo
和 cat
是兩個不同的程序,在這種情況下,通過 echo
和 cat
啟動的兩個進程之間并沒有父子關(guān)系。不過它們依然可以通過有名管道通信。
這樣一種通信方式非常適合某些特定情況:例如有這樣一個架構(gòu),這個架構(gòu)由兩個應(yīng)用程序構(gòu)成,其中一個通過循環(huán)不斷讀取 fifo_test
中的內(nèi)容,以便判斷,它下一步要做什么。如果這個管道沒有內(nèi)容,那么它就會被阻塞在那里,而不會因死循環(huán)而耗費資源,另外一個則作為一個控制程序不斷地往 fifo_test
中寫入一些控制信息,以便告訴之前的那個程序該做什么。下面寫一個非常簡單的例子。可以設(shè)計一些控制碼,然后控制程序不斷地往 fifo_test
里頭寫入,然后應(yīng)用程序根據(jù)這些控制碼完成不同的動作。當(dāng)然,也可以往 fifo_test
傳入除控制碼外的其他數(shù)據(jù)。
應(yīng)用程序的代碼
$ 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 #修改這兩個程序的可執(zhí)行權(quán)限,以便用戶可以執(zhí)行它們
$ ./app.sh #在一個終端啟動這個應(yīng)用程序,在通過./control.sh發(fā)送控制碼以后查看輸出
The CONTROL number is ONE, do something ... #發(fā)送1以后
The CONTROL number is ZERO, do something ... #發(fā)送0以后
The CONTROL number not recognized, do something else... #發(fā)送一個未知的控制碼以后
$ ./control.sh 1 #在另外一個終端,發(fā)送控制信息,控制應(yīng)用程序的工作
$ ./control.sh 0
$ ./control.sh 4343
這樣一種應(yīng)用架構(gòu)非常適合本地的多程序任務(wù)設(shè)計,如果結(jié)合 web cgi
,那么也將適合遠程控制的要求。引入 web cgi
的唯一改變是,要把控制程序 ./control.sh
放到 web
的 cgi
目錄下,并對它作一些修改,以使它符合 CGI
的規(guī)范,這些規(guī)范包括文檔輸出格式的表示(在文件開頭需要輸出 content-tpye: text/html
以及一個空白行)和輸入?yún)?shù)的獲取 (web
輸入?yún)?shù)都存放在 QUERY_STRING
環(huán)境變量里頭)。因此一個非常簡單的 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
管道,并且有寫權(quán)限,以便通過瀏覽器控制 app.sh
:
http://ipaddress\_or\_dns/cgi-bin/control.sh?0
問號 ?
后面的內(nèi)容即 QUERY_STRING
,類似之前的 $1
。
這樣一種應(yīng)用對于遠程控制,特別是嵌入式系統(tǒng)的遠程控制很有實際意義。在去年的暑期課程上,我們就通過這樣一種方式來實現(xiàn)馬達的遠程控制。首先,實現(xiàn)了一個簡單的應(yīng)用程序以便控制馬達的轉(zhuǎn)動,包括轉(zhuǎn)速,方向等的控制。為了實現(xiàn)遠程控制,我們設(shè)計了一些控制碼,以便控制馬達轉(zhuǎn)動相關(guān)的不同屬性。
在 C 語言中,如果要使用有名管道,和 Shell 類似,只不過在讀寫數(shù)據(jù)時用 read
,write
調(diào)用,在創(chuàng)建 fifo
時用 mkfifo
函數(shù)調(diào)用。
信號是軟件中斷,Linux 用戶可以通過 kill
命令給某個進程發(fā)送一個特定的信號,也可以通過鍵盤發(fā)送一些信號,比如 CTRL+C
可能觸發(fā) SGIINT
信號,而 CTRL+\
可能觸發(fā) SGIQUIT
信號等,除此之外,內(nèi)核在某些情況下也會給進程發(fā)送信號,比如在訪問內(nèi)存越界時產(chǎn)生 SGISEGV
信號,當(dāng)然,進程本身也可以通過 kill
,raise
等函數(shù)給自己發(fā)送信號。對于 Linux 下支持的信號類型,大家可以通過 man 7 signal
或者 kill -l
查看到相關(guān)列表和說明。
對于有些信號,進程會有默認(rèn)的響應(yīng)動作,而有些信號,進程可能直接會忽略,當(dāng)然,用戶還可以對某些信號設(shè)定專門的處理函數(shù)。在 Shell 中,可以通過 trap
命令(Shell 內(nèi)置命令)來設(shè)定響應(yīng)某個信號的動作(某個命令或者定義的某個函數(shù)),而在 C 語言中可以通過 signal
調(diào)用注冊某個信號的處理函數(shù)。這里僅僅演示 trap
命令的用法。
$ function signal_handler { echo "hello, world."; } #定義signal_handler函數(shù)
$ trap signal_handler SIGINT #執(zhí)行該命令設(shè)定:收到SIGINT信號時打印hello, world
$ hello, world #按下CTRL+C,可以看到屏幕上輸出了hello, world字符串
類似地,如果設(shè)定信號 0 的響應(yīng)動作,那么就可以用 trap
來模擬 C 語言程序中的 atexit
程序終止函數(shù)的登記,即通過 trap signal_handler SIGQUIT
設(shè)定的 signal_handler
函數(shù)將在程序退出時執(zhí)行。信號 0 是一個特別的信號,在 POSIX.1
中把信號編號 0 定義為空信號,這常被用來確定一個特定進程是否仍舊存在。當(dāng)一個程序退出時會觸發(fā)該信號。
$ cat sigexit.sh
#!/bin/bash
function signal_handler {
echo "hello, world"
}
trap signal_handler 0
$ chmod +x sigexit.sh
$ ./sigexit.sh #實際Shell編程會用該方式在程序退出時來做一些清理臨時文件的收尾工作
hello, world
當(dāng)我們?yōu)橥瓿梢恍?fù)雜的任務(wù)而將多個命令通過 |,\>,<, ;, (,)
等組合在一起時,通常這個命令序列會啟動多個進程,它們間通過管道等進行通信。而有時在執(zhí)行一個任務(wù)的同時,還有其他的任務(wù)需要處理,那么就經(jīng)常會在命令序列的最后加上一個&,或者在執(zhí)行命令后,按下 CTRL+Z
讓前一個命令暫停。以便做其他的任務(wù)。等做完其他一些任務(wù)以后,再通過 fg
命令把后臺任務(wù)切換到前臺。這樣一種控制過程通常被成為作業(yè)控制,而那些命令序列則被成為作業(yè),這個作業(yè)可能涉及一個或者多個程序,一個或者多個進程。下面演示一下幾個常用的作業(yè)控制操作。
$ sleep 50 &
[1] 11137
使用 Shell 內(nèi)置命令 fg
把作業(yè) 1 調(diào)到前臺運行,然后按下 CTRL+Z
讓該進程暫停
$ fg %1
sleep 50
^Z
[1]+ Stopped sleep 50
$ jobs #查看當(dāng)前作業(yè)情況,有一個作業(yè)停止
[1]+ Stopped sleep 50
$ sleep 100 & #讓另外一個作業(yè)在后臺運行
[2] 11138
$ jobs #查看當(dāng)前作業(yè)情況,一個正在運行,一個停止
[1]+ Stopped sleep 50
[2]- Running sleep 100 &
$ bg %1
[2]+ sleep 50 &
不過,要在命令行下使用作業(yè)控制,需要當(dāng)前 Shell,內(nèi)核終端驅(qū)動等對作業(yè)控制支持才行。
更多建議: