2017-3-29 實驗目的 1.熟悉fork,execve,exit等系統呼叫的使用 2.通過編寫程式理解Linux程式生命週期 實驗內容 [基本要求] 編寫Linux環境下C程式,使用fork,exec,exit系統呼叫 [具體要求] ·程式呼叫fork建立子程式 ·父子程式至少有一個呼叫exec族系統呼叫執行其他程式 ·呼叫exit終止程式 ·程式碼有註釋,提交實驗報告 [進一步要求] ·用wait系統呼叫代替示例程式碼中的sleep ·在呼叫exec時使用自己編寫的C程式或者shell程式 ·將實驗1與實驗2結合 ·使用exec其他幾個函式或者在實驗報告中給出它們的定義與區別 ·實驗報告中寫出fork與vfork的區別,exit與_exit的區別,fork與clone這兩個系統呼叫的異同 ·談談你對fork返回兩次的理解 ·另外瞭解五個常用系統呼叫,在實驗報告中寫明用法和對其功能的理解
3.實驗報告 (1)用wait()呼叫代替sleep() 實際上wait()是要求父程式等到子程式結束之後再執行,而sleep()是讓這個父程式等待一段時間。
(2)呼叫exec時使用自己編寫的C或者shell程式 以系統呼叫函式execve()為例,使用execve_str[] 存入對應的引數,呼叫的程式是實驗一中寫的sh檔案。 execve("/Mytest/test.sh",execve_str,NULL)
(3)exec函式族的各個函式的定義與區別
在 Linux 中使用exec函式族主要有兩種情況:
當程式認為自己不能再為系統和使用者做出任何貢獻時,就可以呼叫 exec 函式族中的任意一個函式讓自己重生。
如果一個程式想去執行另一個程式,那麼它就可以呼叫fork()函式新建一個程式,然後呼叫exec函式族中的任意一個函式,指定新的程式來覆蓋子程式的程式碼段、資料段、堆和棧。從而讓子程式去執行一個新的程式,而不是執行父程式的副本。這樣看起來就像通過執行應用程式而產生了一個新程式。 標頭檔案: #include <unistd.h> 六個定義如下: int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); 上面 5 個函式屬於庫函式,這些函式都最終呼叫了下面的 execve 函式,這6個函式中,只有execve 函式屬於Linux的系統呼叫 int execve(const char *path, char *const argv[], char *const envp[]); 這些函式在呼叫成功時不會返回,只有在呼叫出錯時才返回 -1 實際上六個函式的功能是差不多的, 只是因為C語言沒有函式的過載,所以接受不同的引數就要用不同的名字區分它們。
六個函式的命名是有規律的: exec[l or v][p][e] exec函式裡的引數可以分成3個部分,執行檔案部分,命令引數部分,環境變數部分. 環境變數部分是1個陣列,最後必須以NULL結尾 命名規則: e後續:引數必須帶環境變數部分,環境變零部分引數會成為執行exec函式期間的環境變數, 比較少用 l 後續:命令引數部分必須以"," 相隔, 最後1個命令引數必須是NULL v 後續:命令引數部分必須是1個以NULL結尾的字串指標陣列的頭部指標.例如char * pstr就是1個字串的指標, char * pstr[] 就是陣列了, 分別指向各個字串. p後續:執行檔案部分可以不帶路徑, exec函式會在$PATH中找
(4)fork與vfork的區別 呼叫fork()之後,子程式和父程式都會繼續執行fork呼叫之後的指令。子程式是父程式的副本。它將獲得父程式的資料空間,堆和棧的副本,這些都是副本,父子程式並不共享這部分的記憶體。 而用vfork建立的子程式與父程式共享地址空間,也就是說子程式完全執行在父程式的地址空間上,如果這時子程式修改了某個變數,這將影響到父程式。vfork的好處是在子程式被建立後往往僅僅是為了呼叫exec執行另一個程式,因為它就不會對父程式的地址空間有任何引用,所以對地址空間的複製是多餘的 ,因此通過vfork共享記憶體可以減少不必要的開銷。 要注意的是用vfork()建立的子程式必須顯示呼叫exit()來結束,否則子程式將不能結束,而fork()則不存在這個情況。
(5)exit與_exit的區別 exit和_exit都是Linux下的退出函式,exit作用是:直接使程式停止執行,清除其使用的記憶體空間,並銷燬其在核心中的各種資料結構;而exit是_exit函式的進一步封裝,執行了其他的清理工作,然後才呼叫_exit函式,在與輸入輸出或fork等函式一起使用時候會表現出一些差異 exit函式能保證資料的完整性,在退出之前會做些清理工作,然後再呼叫_exit再退出的;而_exit是直接退出程式,但最終兩者都會關閉程式開啟的檔案描述符,釋放記憶體。
(6)fork與clone這兩個系統呼叫的異同 系統呼叫fork()是無引數的,而clone()則帶有引數。fork()是全部複製, clone()是則可以將父程式資源有選擇地複製給子程式,而沒有複製的資料結構則通過指標的複製讓子程式共享,具體要複製哪些資源給子程式,由引數列表中的clone_flags來決定。另外,clone()返回的是子程式的pid
(5)對fork返回兩次的理解 fork是建立子程式,該程式是對父程式的完全複製,二者最大的區別只在於PID,如果要對子程式進行利用就需要分辨父子關係,fork兩次返回值就幫我們做到了這一點,返回0 就是對應的子程式,而返回PID的就是對應的父程式。
(6)瞭解五個常用系統呼叫 程式控制:getpid() Linux函式庫的原型: #include<sys/types.h> /* 提供型別pid_t的定義 / #include<unistd.h> / 提供函式的定義 */ pid_t getpid(void); 返回值:目前程式的程式識別碼 getpid的作用很簡單,就是返回當前程式的程式ID,許多程式利用取到的此值來建立臨時檔案, 以避免臨時檔案相同帶來的問題。 檔案讀寫:read() write() read(由已開啟的檔案讀取資料) #include<unistd.h> ssize_t read(int fd,void * buf ,size_t count); 函式說明:read()會把引數fd所指的檔案傳送count個位元組到buf指標所指的記憶體中。若引數count為0,則read()不會有作用並返回0。返回值為實際讀取到的位元組數,如果返回0,表示已到達檔案尾或是無可讀取的資料,此外檔案讀寫位置會隨讀取到的位元組移動。 P.S. 如果順利,read()會返回實際讀到的位元組數,最好能將返回值與引數count 作比較,若返回的位元組數比要求讀取的位元組數少,則有可能讀到了檔案尾、從管道(pipe)或終端機讀取,或者是read()被訊號中斷了讀取動作。當有錯誤發生時則返回-1,錯誤程式碼存入errno中,而檔案讀寫位置則無法預期. write(將資料寫入已開啟的檔案內)
#include<unistd.h> ssize_t write (int fd,const void * buf,size_t count);
函式說明 write()會把引數buf所指的記憶體寫入count個位元組到引數fd所指的檔案內。當然,檔案讀寫位置也會隨之移動。 返回值 如果順利write()會返回實際寫入的位元組數。當有錯誤發生時則返回-1,錯誤程式碼存入errno中。
檔案系統操作:access()
用於檢查呼叫程式是否可以對指定的檔案執行某種操作。
access函式用法:
#include <unistd.h> int access(const char *pathname, int mode);
Linux access函式引數:
pathname: 需要測試的檔案路徑名。
mode: 需要測試的操作模式,可能值是一個或多個R_OK(可讀?), W_OK(可寫?), X_OK(可執行?) 或 F_OK(檔案存在?)組合體。
函式返回說明:成功執行時,返回0。失敗返回-1
記憶體管理:brk()
功能就是分配/回收記憶體,一般用於回收記憶體
#include <unistd.h> int brk(void* position)
引數position就是新位置,無論原來的位置在哪裡。返回:成功返回0,失敗返回 -1。
brk系統呼叫,可以讓程式的堆指標增長一定的大小,邏輯上消耗掉一塊本程式的虛擬地址區間,malloc向OS獲取的記憶體大小比較小時,將直接通過brk呼叫獲取虛擬地址,結果是將本程式的brk指標推高
5.錯誤 1)想呼叫execve的時候傳進當前子程式的pid結果失敗 解決方案:這其實是對於execve的傳參,以及shell傳參不夠清楚的問題 查詢對execve( )定義如下: int execve( char *pathname, char *argv[ ], char *envp[ ]) 第一個引數是命令所在路徑,第二個引數是命令集合,第三個是傳遞給執行檔案的環境變數集。一般對於envp預設引數是NULL,那麼如果要將實驗一與實驗二結合,就需要呼叫自己寫的.sh程式碼,將程式的PID傳進檔案中。 困難1:一開始我是想直接將定義的pid傳入,結果發現pid_t是一個int型別的定義,然後這樣定義的pid只有1、0、-1三種情況,並不是真正的子程式的pid。 困難2:如果用getpid()得到子程式的pid,那麼返回的值是一個int類,就需要將其轉換成一個字串。【在這裡發現直接呼叫C程式裡面的itoa()失敗,但是標頭檔案沒有問題,可能是編譯環境問題?最終我用sprintf()自己寫了一個itoa() 】 困難3:直接在execve裡面傳入轉換後的字串是會報錯的,傳入指標也是。主要是因為在定義的時候已經規定了必須傳入一個指標陣列(第二個引數),所以傳入其他的情況會失敗。 困難4:.sh報錯(在第二點談),傳入的引數沒有顯示,導致sh這個程式無法執行。 查詢對shell傳遞引數如下: 指令碼內獲取引數的格式為:$n n代表一個數字,1 為執行指令碼的第一個引數,2 為執行指令碼的第二個引數... 以$ ./test.sh 1 2 這個語句為例,$0表示執行檔名(./test.sh),$1 = 1 $2=2 由此,我將sh 內容做了處理:首先開頭改為#!/bin/bash,然後直接令pid=$1; 最後當我從C程式裡面呼叫execve() 的時候如果同時將子程式PID 傳入,該sh檔案將會根據該PID做出相應的處理。
2)呼叫自己的.sh的時候報錯/無法退出:全部都說 :不是有效識別符號read: 之類的內容,估計是windows環境下sublime沒有轉換格式的問題,當我換成notepad++開啟sh檔案並將環境調成UNIX就沒有問題了。
3)想同時呼叫六個exec 系列函式但是失敗:因為只建立了一個子程式,以及,函式在執行期間會被相互覆蓋。(同時發生的緣故)