加入星計劃,您可以享受以下權益:

  • 創(chuàng)作內容快速變現
  • 行業(yè)影響力擴散
  • 作品版權保護
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • 7.2  Linux進程控制編程
  • 相關推薦
  • 電子產業(yè)圖譜
申請入駐 產業(yè)圖譜

進程控制開發(fā)之:Linux進程控制編程

2013/09/13
1
閱讀需 38 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

?

7.2??Linux進程控制編程

1.fork()

Linux中創(chuàng)建一個新進程的惟一方法是使用fork()函數。fork()函數是Linux中一個非常重要的函數,和讀者以往遇到的函數有一些區(qū)別,因為它看起來執(zhí)行一次卻返回兩個值。難道一個函數真的能返回兩個值嗎?希望讀者能認真地學習這一部分的內容。

(1)fork()函數說明。

fork()函數用于從已存在的進程中創(chuàng)建一個新進程。新進程稱為子進程,而原進程稱為父進程。使用fork()函數得到的子進程是父進程的一個復制品,它從父進程處繼承了整個進程的地址空間,包括進程上下文、代碼段、進程堆棧、內存信息、打開的文件描述符、信號控制設定、進程優(yōu)先級、進程組號、當前工作目錄、根目錄、資源限制和控制終端等,而子進程所獨有的只有它的進程號、資源使用和計時器等。

因為子進程幾乎是父進程的完全復制,所以父子兩個進程會運行同一個程序。因此需要用一種方式來區(qū)分它們,并使它們照此運行,否則,這兩個進程不可能做不同的事。

實際上是在父進程中執(zhí)行fork()函數時,父進程會復制出一個子進程,而且父子進程的代碼從fork()函數的返回開始分別在兩個地址空間中同時運行。從而兩個進程分別獲得其所屬fork()的返回值,其中在父進程中的返回值是子進程的進程號,而在子進程中返回0。因此,可以通過返回值來判定該進程是父進程還是子進程。

同時可以看出,使用fork()函數的代價是很大的,它復制了父進程中的代碼段、數據段和堆棧段里的大部分內容,使得fork()函數的系統開銷比較大,而且執(zhí)行速度也不是很快。

(2)fork()函數語法。

表7.2列出了fork()函數的語法要點。

表7.2 fork()函數語法要點

所需頭文件

#include?<sys/types.h>?//?提供類型pid_t的定義
#include?<unistd.h>

函數原型

pid_t?fork(void)

函數返回值

0:子進程

子進程ID(大于0的整數):父進程

-1:出錯

(3)fork()函數使用實例。

/*?fork.c?*/

#include?<sys/types.h>

#include?<unistd.h>

#include?<stdio.h>

#include?<stdlib.h>

int?main(void)

{

?????pid_t?result;

????

?????/*調用fork()函數*/

?????result?=?fork();

????

?????/*通過result的值來判斷fork()函數的返回情況,首先進行出錯處理*/

?????if(result?==??-1)

?????{

?????printf("Fork?errorn");

?????}

?????else?if?(result?==?0)?/*返回值為0代表子進程*/

?????{

?????????printf("The?returned?value?is?%dn

????????????????????In?child?process!!nMy?PID?is?%dn",result,getpid());

?????}

?????else?/*返回值大于0代表父進程*/

?????{

?????????printf("The?returned?value?is?%dn

????????????????????In?father?process!!nMy?PID?is?%dn",result,getpid());

?????}

?????return?result;

}

將可執(zhí)行程序下載到目標板上,運行結果如下所示:

$?arm-linux-gcc?fork.c?–o?fork?(或者修改Makefile)

$?./fork

The?returned?value?is?76???????/*?在父進程中打印的信息?*/

In?father?process!!

My?PID?is?75

The?returned?value?is?:0???????/*?在子進程中打印的信息?*/

In?child?process!!

My?PID?is?76

從該實例中可以看出,使用fork()函數新建了一個子進程,其中的父進程返回子進程的PID,而子進程的返回值為0。

?

(4)函數使用注意點。

fork()函數使用一次就創(chuàng)建一個進程,所以若把fork()函數放在了if?else判斷語句中則要小心,不能多次使用fork()函數。

小知識

由于fork()完整地復制了父進程的整個地址空間,因此執(zhí)行速度是比較慢的。為了加快fork()的執(zhí)行速度,有些UNIX系統設計者創(chuàng)建了vfork()。vfork()也能創(chuàng)建新進程,但它不產生父進程的副本。它是通過允許父子進程可訪問相同物理內存從而偽裝了對進程地址空間的真實拷貝,當子進程需要改變內存中數據時才復制父進程。這就是著名的“寫操作時復制”(copy-on-write)技術。

現在很多嵌入式Linux系統的fork()函數調用都采用vfork()函數的實現方式,實際上uClinux所有的多進程管理都通過vfork()來實現。

2.exec函數族

(1)exec函數族說明。

fork()函數是用于創(chuàng)建一個子進程,該子進程幾乎復制了父進程的全部內容,但是,這個新創(chuàng)建的進程如何執(zhí)行呢?這個exec函數族就提供了一個在進程中啟動另一個程序執(zhí)行的方法。它可以根據指定的文件名或目錄名找到可執(zhí)行文件,并用它來取代原調用進程的數據段、代碼段和堆棧段,在執(zhí)行完之后,原調用進程的內容除了進程號外,其他全部被新的進程替換了。另外,這里的可執(zhí)行文件既可以是二進制文件,也可以是Linux下任何可執(zhí)行的腳本文件。

在Linux中使用exec函數族主要有兩種情況。

n 當進程認為自己不能再為系統和用戶做出任何貢獻時,就可以調用exec函數族中的任意一個函數讓自己重生。

n 如果一個進程想執(zhí)行另一個程序,那么它就可以調用fork()函數新建一個進程,然后調用exec函數族中的任意一個函數,這樣看起來就像通過執(zhí)行應用程序而產生了一個新進程(這種情況非常普遍)。

(2)exec函數族語法。

實際上,在Linux中并沒有exec()函數,而是有6個以exec開頭的函數,它們之間語法有細微差別,本書在下面會詳細講解。

下表7.3列舉了exec函數族的6個成員函數的語法。

表7.3 exec函數族成員函數語法

所需頭文件

#include?<unistd.h>

函數原型

int?execl(const?char?*path,?const?char?*arg,?...)

int?execv(const?char?*path,?char?*const?argv[])

int?execle(const?char?*path,?const?char?*arg,?...,?char?*const?envp[])

int?execve(const?char?*path,?char?*const?argv[],?char?*const?envp[])

int?execlp(const?char?*file,?const?char?*arg,?...)

int?execvp(const?char?*file,?char?*const?argv[])

函數返回值

-1:出錯

這6個函數在函數名和使用語法的規(guī)則上都有細微的區(qū)別,下面就可執(zhí)行文件查找方式、參數表傳遞方式及環(huán)境變量這幾個方面進行比較。

n 查找方式。

讀者可以注意到,表7.3中的前4個函數的查找方式都是完整的文件目錄路徑,而最后2個函數(也就是以p結尾的兩個函數)可以只給出文件名,系統就會自動按照環(huán)境變量“$PATH”所指定的路徑進行查找。

n 參數傳遞方式。

exec函數族的參數傳遞有兩種方式:一種是逐個列舉的方式,而另一種則是將所有參數整體構造指針數組傳遞。

在這里是以函數名的第5位字母來區(qū)分的,字母為“l(fā)”(list)的表示逐個列舉參數的方式,其語法為char?*arg;字母為“v”(vertor)的表示將所有參數整體構造指針數組傳遞,其語法為*const?argv[]。讀者可以觀察execl()、execle()、execlp()的語法與execv()、execve()、execvp()的區(qū)別。它們具體的用法在后面的實例講解中會具體說明。

這里的參數實際上就是用戶在使用這個可執(zhí)行文件時所需的全部命令選項字符串(包括該可執(zhí)行程序命令本身)。要注意的是,這些參數必須以NULL表示結束,如果使用逐個列舉方式,那么要把它強制轉化成一個字符指針,否則exec將會把它解釋為一個整型參數,如果一個整型數的長度char?*的長度不同,那么exec函數就會報錯。

n 環(huán)境變量。

exec函數族可以默認系統的環(huán)境變量,也可以傳入指定的環(huán)境變量。這里以“e”(environment)結尾的兩個函數execle()和execve()就可以在envp[]中指定當前進程所使用的環(huán)境變量。

表7.4是對這4個函數中函數名和對應語法的小結,主要指出了函數名中每一位所表明的含義,希望讀者結合此表加以記憶。

表7.4 exec函數名對應含義

前4位

統一為:exec

第5位

l:參數傳遞為逐個列舉方式

execl、execle、execlp

v:參數傳遞為構造指針數組方式

execv、execve、execvp

第6位

e:可傳遞新進程環(huán)境變量

execle、execve

p:可執(zhí)行文件查找方式為文件名

execlp、execvp

(3)exec使用實例。

下面的第一個示例說明了如何使用文件名的方式來查找可執(zhí)行文件,同時使用參數列表的方式。這里用的函數是execlp()。

/*execlp.c*/

#include?<unistd.h>

#include?<stdio.h>

#include?<stdlib.h>

int?main()

{

?????if?(fork()?==?0)

?????{

?????????/*調用execlp()函數,這里相當于調用了"ps?-ef"命令*/

?????????if?((ret?=?execlp("ps",?"ps",?"-ef",?NULL))?<?0)

?????????{

?????????????printf("Execlp?errorn");

?????????}

?????}

}

在該程序中,首先使用fork()函數創(chuàng)建一個子進程,然后在子進程里使用execlp()函數。讀者可以看到,這里的參數列表列出了在shell中使用的命令名和選項。并且當使用文件名進行查找時,系統會在默認的環(huán)境變量PATH中尋找該可執(zhí)行文件。讀者可將編譯后的結果下載到目標板上,運行結果如下所示:

$?./execlp

??PID?TTY?????Uid????????Size?State?Command

????1?????????root???????1832?????S????init

????2?????????root??????????0?????S????[keventd]

????3?????????root??????????0?????S????[ksoftirqd_CPU0]

????4?????????root??????????0?????S????[kswapd]

????5?????????root??????????0?????S????[bdflush]

????6?????????root??????????0?????S????[kupdated]

????7?????????root??????????0?????S????[mtdblockd]

????8?????????root??????????0?????S????[khubd]

???35?????????root???????2104?????S????/bin/bash?/usr/etc/rc.local

???36?????????root???????2324?????S????/bin/bash

???41?????????root???????1364?????S????/sbin/inetd

???53?????????root??????14260?????S????/Qtopia/qtopia-free-1.7.0/bin/qpe?-qws

???54?????????root??????11672?????S????quicklauncher

???65?????????root??????????0?????S????[usb-storage-0]

???66?????????root??????????0?????S????[scsi_eh_0]

???83?????????root???????2020?????R????ps?-ef

$?env

……

PATH=/Qtopia/qtopia-free-1.7.0/bin:/usr/bin:/bin:/usr/sbin:/sbin

?????……

?

此程序的運行結果與在shell中直接鍵入命令“ps?-ef”是一樣的,當然,在不同系統的不同時刻都可能會有不同的結果。

接下來的示例使用完整的文件目錄來查找對應的可執(zhí)行文件。注意目錄必須以“/”開頭,否則將其視為文件名。

/*execl.c*/

#include?<unistd.h>

#include?<stdio.h>

#include?<stdlib.h>

int?main()

{

????if?(fork()?==?0)

????{

?????????/*調用execl()函數,注意這里要給出ps程序所在的完整路徑*/

?????????if?(execl("/bin/ps","ps","-ef",NULL)?<?0)

?????????{

?????????printf("Execl?errorn");

?????????}

????}

}

同樣下載到目標板上運行,運行結果同上例。

下面的示例利用函數execle(),將環(huán)境變量添加到新建的子進程中,這里的“env”是查看當前進程環(huán)境變量的命令,如下所示:

/*?execle.c?*/

#include?<unistd.h>

#include?<stdio.h>

#include?<stdlib.h>

int?main()

{

????/*命令參數列表,必須以NULL結尾*/

????char?*envp[]={"PATH=/tmp","USER=david",?NULL};

????

????if?(fork()?==?0)

????{

?????????/*調用execle()函數,注意這里也要指出env的完整路徑*/

?????????if?(execle("/usr/bin/env",?"env",?NULL,?envp)?<?0)

?????????{

?????????????printf("Execle?errorn");

?????????}

????}

}

下載到目標板后的運行結果如下所示:

$./execle

PATH=/tmp

USER=sunq

最后一個示例使用execve()函數,通過構造指針數組的方式來傳遞參數,注意參數列表一定要以NULL作為結尾標識符。其代碼和運行結果如下所示:

#include?<unistd.h>

#include?<stdio.h>

#include?<stdlib.h>

int?main()

{

????/*命令參數列表,必須以NULL結尾*/

????char?*arg[]?=?{"env",?NULL};

????char?*envp[]?=?{"PATH=/tmp",?"USER=david",?NULL};

????

????if?(fork()?==?0)

????{

?????????if?(execve("/usr/bin/env",?arg,?envp)?<?0)

?????????{

?????????printf("Execve?errorn");

????????}

????}

}

下載到目標板后的運行結果如下所示:

$?./execve

PATH=/tmp

USER=david

(4)exec函數族使用注意點。

在使用exec函數族時,一定要加上錯誤判斷語句。exec很容易執(zhí)行失敗,其中最常見的原因有:

n 找不到文件或路徑,此時errno被設置為ENOENT;?

n 數組argv和envp忘記用NULL結束,此時errno被設置為EFAULT;?

n 沒有對應可執(zhí)行文件的運行權限,此時errno被設置為EACCES。

小知識

事實上,這6個函數中真正的系統調用只有execve(),其他5個都是庫函數,它們最終都會調用execve()這個系統調用。

?

3.exit()和_exit()

(1)exit()和_exit()函數說明。

exit()和_exit()函數都是用來終止進程的。當程序執(zhí)行到exit()或_exit()時,進程會無條件地停止剩下的所有操作,清除包括PCB在內的各種數據結構,并終止本進程的運行。但是,這兩個函數還是有區(qū)別的,這兩個函數的調用過程如圖7.4所示。

圖7.4??exit和_exit函數流程圖

從圖中可以看出,_exit()函數的作用是:直接使進程停止運行,清除其使用的內存空間,并清除其在內核中的各種數據結構;exit()函數則在這些基礎上做了一些包裝,在執(zhí)行退出之前加了若干道工序。exit()函數與_exit()函數最大的區(qū)別就在于exit()函數在調用exit系統之前要檢查文件的打開情況,把文件緩沖區(qū)中的內容寫回文件,就是圖中的“清理I/O緩沖”一項。

由于在Linux的標準函數庫中,有一種被稱作“緩沖I/O(buffered?I/O)”操作,其特征就是對應每一個打開的文件,在內存中都有一片緩沖區(qū)。每次讀文件時,會連續(xù)讀出若干條記錄,這樣在下次讀文件時就可以直接從內存的緩沖區(qū)中讀??;同樣,每次寫文件的時候,也僅僅是寫入內存中的緩沖區(qū),等滿足了一定的條件(如達到一定數量或遇到特定字符等),再將緩沖區(qū)中的內容一次性寫入文件。

這種技術大大增加了文件讀寫的速度,但也為編程帶來了一些麻煩。比如有些數據,認為已經被寫入文件中,實際上因為沒有滿足特定的條件,它們還只是被保存在緩沖區(qū)內,這時用_exit()函數直接將進程關閉,緩沖區(qū)中的數據就會丟失。因此,若想保證數據的完整性,就一定要使用exit()函數。

(2)exit()和_exit()函數語法。

表7.5列出了exit()和_exit()函數的語法規(guī)范。

表7.5 exit()和_exit()函數族語法

所需頭文件

exit:#include?<stdlib.h>

_exit:#include?<unistd.h>

函數原型

exit:void?exit(int?status)

_exit:void?_exit(int?status)

函數傳入值

status是一個整型的參數,可以利用這個參數傳遞進程結束時的狀態(tài)。一般來說,0表示正常結束;其他的數值表示出現了錯誤,進程非正常結束。
在實際編程時,可以用wait()系統調用接收子進程的返回值,從而針對不同的情況進行不同的處理

(3)exit()和_exit()使用實例。

這兩個示例比較了exit()和_exit()兩個函數的區(qū)別。由于printf()函數使用的是緩沖I/O方式,該函數在遇到“n”換行符時自動從緩沖區(qū)中將記錄讀出。示例中就是利用這個性質來進行比較的。以下是示例1的代碼:

/*?exit.c?*/

#include?<stdio.h>

#include?<stdlib.h>

int?main()

{

??????printf("Using?exit...n");

??????printf("This?is?the?content?in?buffer");

??????exit(0);

}

$./exit

Using?exit...

This?is?the?content?in?buffer?$

讀者從輸出的結果中可以看到,調用exit()函數時,緩沖區(qū)中的記錄也能正常輸出。

以下是示例2的代碼:

/*?_exit.c?*/

#include?<stdio.h>

#include?<unistd.h>

int?main()

{

??????printf("Using?_exit...n");

??????printf("This?is?the?content?in?buffer");?/*?加上回車符之后結果又如何?*/

??????_exit(0);

}

$?./_exit

Using?_exit...

$

讀者從最后的結果中可以看到,調用_exit()函數無法輸出緩沖區(qū)中的記錄。

小知識

在一個進程調用了exit()之后,該進程并不會立刻完全消失,而是留下一個稱為僵尸進程(Zombie)的數據結構。僵尸進程是一種非常特殊的進程,它已經放棄了幾乎所有的內存空間,沒有任何可執(zhí)行代碼,也不能被調度,僅僅在進程列表中保留一個位置,記載該進程的退出狀態(tài)等信息供其他進程收集,除此之外,僵尸進程不再占有任何內存空間。

4.wait()和waitpid()

(1)wait()和waitpid()函數說明。

wait()函數是用于使父進程(也就是調用wait()的進程)阻塞,直到一個子進程結束或者該進程接到了一個指定的信號為止。如果該父進程沒有子進程或者他的子進程已經結束,則wait()就會立即返回。

waitpid()的作用和wait()一樣,但它并不一定要等待第一個終止的子進程,它還有若干選項,如可提供一個非阻塞版本的wait()功能,也能支持作業(yè)控制。實際上wait()函數只是waitpid()函數的一個特例,在Linux內部實現wait()函數時直接調用的就是waitpid()函數。

(2)wait()和waitpid()函數格式說明。

表7.6列出了wait()函數的語法規(guī)范。

表7.6 wait()函數族語法

所需頭文件

#include?<sys/types.h>
#include?<sys/wait.h>

函數原型

pid_t?wait(int?*status)

函數傳入值

這里的status是一個整型指針,是該子進程退出時的狀態(tài)
·??status若不為空,則通過它可以獲得子進程的結束狀態(tài)
另外,子進程的結束狀態(tài)可由Linux中一些特定的宏來測定

函數返回值

成功:已結束運行的子進程的進程號
失?。?1

表7.7列出了waitpid()函數的語法規(guī)范。

表7.7 waitpid()函數語法

所需頭文件

#include?<sys/types.h>
#include?<sys/wait.h>

函數原型

pid_t?waitpid(pid_t?pid,?int?*status,?int?options)

續(xù)表

函數傳入值

Pid

pid?>?0:只等待進程ID等于pid的子進程,不管已經有其他子進程運行結束退出了,只要指定的子進程還沒有結束,waitpid()就會一直等下去

pid?=?-1:等待任何一個子進程退出,此時和wait()作用一樣

pid?=?0:等待其組ID等于調用進程的組ID的任一子進程

pid?<?-1:等待其組ID等于pid的絕對值的任一子進程

status

同wait()

options

WNOHANG:若由pid指定的子進程不立即可用,則waitpid()不阻塞,此時返回值為0

WUNTRACED:若實現某支持作業(yè)控制,則由pid指定的任一子進程狀態(tài)已暫停,且其狀態(tài)自暫停以來還未報告過,則返回其狀態(tài)

0:同wait(),阻塞父進程,等待子進程退出

函數返回值

正常:已經結束運行的子進程的進程號

使用選項WNOHANG且沒有子進程退出:0

調用出錯:-1

?

3)waitpid()使用實例。

由于wait()函數的使用較為簡單,在此僅以waitpid()為例進行講解。本例中首先使用fork()創(chuàng)建一個子進程,然后讓其子進程暫停5s(使用了sleep()函數)。接下來對原有的父進程使用waitpid()函數,并使用參數WNOHANG使該父進程不會阻塞。若有子進程退出,則waitpid()返回子進程號;若沒有子進程退出,則waitpid()返回0,并且父進程每隔一秒循環(huán)判斷一次。該程序的流程圖如圖7.5所示。

圖7.5??waitpid示例程序流

該程序源代碼如下所示:

/*?waitpid.c?*/

#include?<sys/types.h>

#include?<sys/wait.h>

#include?<unistd.h>

#include?<stdio.h>?????????????????????????

#include?<stdlib.h>?

int?main()

{

????pid_t?pc,?pr;

????

????pc?=?fork();

????if?(pc?<?0)

????{

????????printf("Error?forkn");

????}

????else?if?(pc?==?0)?/*子進程*/

????{?

????????/*子進程暫停5s*/

????????sleep(5);????????

????????/*子進程正常退出*/

????????exit(0);

????}

????else?/*父進程*/

????{

????????/*循環(huán)測試子進程是否退出*/

???????do

????????{

?????????????/*調用waitpid,且父進程不阻塞*/

?????????????pr?=?waitpid(pc,?NULL,?WNOHANG);

?????????????/*若子進程還未退出,則父進程暫停1s*/

?????????????if?(pr?==?0)

?????????????{

??????????????????printf("The?child?process?has?not?exitedn");

?????????????sleep(1);

?????????????}

????????}?while?(pr?==?0);

????????

????????/*若發(fā)現子進程退出,打印出相應情況*/

????????if?(pr?==?pc)

????????{

????????????printf("Get?child?exit?code:?%dn",pr);

????????}

????????else

????????{

????????????printf("Some?error?occured.n");

????????}

????}

}

將該程序交叉編譯,下載到目標板后的運行結果如下所示:

$./waitpid

The?child?process?has?not?exited

The?child?process?has?not?exited

The?child?process?has?not?exited

The?child?process?has?not?exited

The?child?process?has?not?exited

Get?child?exit?code:?75

可見,該程序在經過5次循環(huán)之后,捕獲到了子進程的退出信號,具體的子進程號在不同的系統上會有所區(qū)別。

讀者還可以嘗試把“pr?=?waitpid(pc,?NULL,?WNOHANG);”這句改為“pr?=?waitpid(pc,?NULL,?0);”或者“pr?=?wait(NULL);”,運行的結果為:

$./waitpid

Get?child?exit?code:?76

可見,在上述兩種情況下,父進程在調用waitpid()或wait()之后就將自己阻塞,直到有子進程退出為止。

相關推薦

電子產業(yè)圖譜

華清遠見(www.farsight.com.cn)是國內領先嵌入師培訓機構,2004年注冊于中國北京海淀高科技園區(qū),除北京總部外,上海、深圳、成都、南京、武漢、西安、廣州均有直營分公司。華清遠見除提供嵌入式相關的長期就業(yè)培訓、短期高端培訓、師資培訓及企業(yè)員工內訓等業(yè)務外,其下屬研發(fā)中心還負責嵌入式、Android及物聯網方向的教學實驗平臺的研發(fā)及培訓教材的出版,截止目前為止已公開出版70余本嵌入式/移動開發(fā)/物聯網相關圖書。企業(yè)理念:專業(yè)始于專注 卓識源于遠見。企業(yè)價值觀:做良心教育、做專業(yè)教育,更要做受人尊敬的職業(yè)教育。