操作系統實驗2進程控制和系統調用

NO IMAGE

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 系列函數但是失敗:因為只創建了一個子進程,以及,函數在執行期間會被相互覆蓋。(同時發生的緣故)

相關文章

Win10安裝TensorFlow步驟及問題

計算機網絡大作業:基於WIFI信號強度的主被動定位

計算機網絡實驗七:交換帶寬與端口密度

2017春季學期編譯原理期末實驗報告