Linux下的进程概论与编程二(进程控制)
一、進程標識符
1、每個進程都有非負的整形表示唯一的進程ID。
幾個典型進程的ID及其功能:
2、除了進程ID,每個進程還有一些其他的標識符。
下列函數返回這些標識符:
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); //返回值:調用進程的進程ID pid_t getppid(void); //返回值:調用進程的父進程ID uid_t getuid(void); //返回值:調用進程的實際用戶ID uid_t geteuid(void); //返回值:調用進程的有效用戶ID gid_t getgid(void); //返回值:調用進程的實際組ID gid_t getegid(void); //返回值:調用進程的有效組ID #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h>int main() {int uid;int euid;pid_t pid;pid_t ppid;pid = fork();if (pid < 0){printf("fork faile!!\n");return 0;}else if (pid == 0){printf("pid: %d, ppid: %d, gid: %d, euid: %d,egid: %d\n",getpid(), getppid(), getuid(), geteuid(), getegid());}else{printf("pid: %d, ppid: %d, gid: %d, euid: %d,egid: %d\n",getpid(), getppid(), getuid(), geteuid(), getegid());}return 0; }二、實際用戶和有效用戶
1、實際用戶ID和實際用戶組ID:
標識我是誰。也就是登錄用戶的uid和gid,比如我的Linux以 wzb登錄,在Linux運行的所有的命令的實際用戶ID都是wzb的uid,實際用戶組ID都是wzb的gid(可以用id命令查看)。
2、有效用戶ID和有效用戶組ID:
進程用來決定我們對資源的訪問權限。一般情況下,有效用戶ID等于實際用戶ID,有效用戶組ID等于實際用戶組ID。當設置-用戶-ID
(SUID)位設置,則有效用戶ID等于文件的所有者的uid,而不是實際用戶ID;同樣,如果設置了設置-用戶組-ID(SGID)位,則有效用戶組ID等于文件所有者的gid,而不是實際用戶組ID。
實際用戶ID/實際組ID標識進程究竟是誰(即是進程在系統的唯一標識),有效用戶ID/有效組ID/附加組ID決定了進程的訪問權限。
suid (chmod u+s file)只能應用在可執行文件上,允許任意用戶在執行文件時以文件擁有者的身份執行。
sgid (chmod g+s file)只能應用在可執行文件上,使任意用戶在執行可執行文件時,將以擁有組成員的身份執行。
說明:suid 和 sgid 表示在bin在運行時,會具有擁有著的權限,換句話說,只要運行該可執行程序,那么運行者也是有權限對擁有者的所有相關文件(可執行程序會讀寫)進行操作。
?
三、進程創建
1、fork函數
#include <unistd.h> pid_t fork(void);1>一個現有進程可以調用fork創建一個新進程。
返回值:子進程中返回0,父進程中返回子進程ID,出錯返回-1。子進程是父進程的副本。例如:子進程獲得父進程數據空間、堆和棧的副本(主要是數據結構的副本)。父子進程不共享這些存儲空間部分。父子進程共享正文段。
由于fork之后經常歸屬exec,所以現在很多實現并不執行一個父進程數據段、棧和堆的完全復制。作為替代,使用了寫時拷貝(Copy-On-Write)技術。這些區域由父子進程共享,而且內核將他們的訪問權限改變為只讀的。如果父子進程中的任一個試圖修改這些區域,則內核只為修改區域的那塊內存制作一個副本。
2>一般來說fork之后父進程和子進程的執行順序是不確定的,這取決于內核的調度算法。
fork的一個特性是父進程的所有打開文件描述符都被復制到子進程中。父子進程的每個相同的打開描述符共享一個文件表項。假設一個進程有三個不同的打開文件,在從fork返回時,我們有如下所示結構:
3>在fork之后處理的文件描述符有兩種常見的情況:
1. 父進程等待子進程完成。在這種情況下,父進程無需對其描述符做任何處理。當子進程終止后,子進程對文件偏移量的修改已執行的更新。
2. 父子進程各自執行不同的程序段。這種情況下,在fork之后,父子進程各自關閉他們不需要使用的文件描述符,這樣就不會干擾對方使用文件描述符。這種方法在網絡服務進程中經常使用。
4>父子進程之間的區別:
1. fork的返回值
2. 進程ID不同
3. 具有不同的父進程ID
4. 子進程的tms_utime、tms_stime、tms_cutime及tms_ustime均被設置為0
5. 父進程設置的文件鎖不會被子進程繼承
6. 子進程的未處理鬧鐘被清除
7. 子進程的未處理信號集被設置為空集
5>fork有下面兩種用法:
1. 一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子進程來處理請求。
2. 一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數。
6>fork調用失敗的原因:
1. 系統中有太多的進程
2. 實際用戶的進程數超過了限制
2、vfork函數
vfork用于創建一個新進程,而該新進程的目的是exec一個新程序。vfork與fork都創建一個子進程,但它不將父進程的地址空間復制到子進程中,因為子進程會立即調用exec,于是不會存訪問該地址空間。相反,在子進程調用exec或exit之前,它在父進程的空間中運行,也就是說會更改父進程的數據段、棧和堆。vfork和fork另一區別在于:vfork保證子進程先運行,在它調用exec或(exit)之后父進程才可能被調度運行。
?
四、進程等待
1>為什么要進行進程等待?
用來回收子進程狀態(如僵尸狀態),回收子進程的信息和資源。
進程的退出碼:main函數的返回值或exit的參數,進程的退出碼用來判斷進程運行是否正確。
2>wait和waitpid函數作用
一個進程在終止時會關閉所有文件描述符,釋放在用戶空間分配的內存,但它的PCB還保留著,內核在其中保存了一些信息:
如果是正常終止則保存著退出狀態,如果是異常終止則保存著導致該進程終止的信號是哪個。這個進程的父進程可以調用wait或waitpid獲取這些信息,然后徹底清除掉這個進程。
當一個進程正常或異常終止時,內核就向其父進程發送一個SIGCHLD信號。因為子進程終止是一個異步事件,所以發生這種信號也是內核向父進程發的異步通知。父進程可以選擇忽略該信號,或者提供一個該信號發生時即被調用執行的函數。對于這種信號的系統默認動作是忽略它。
3>調用wait或waitpid的進程可能會發生什么情況:
1.如果其所有子進程都還在運行,則阻塞
2.如果一個子進程已終止,正等待父進程獲取其終止狀態,則取得該子進程的終止狀 態立即返回。
3.如果它沒有任何子進程,則立即出錯返回。
4>wait函數
用來等待任何一個子進程退出,由父進程調用。
返回值:成功返回被等待子進程的pid,失敗返回-1
status:輸出型參數,拿回子進程的退出信息,不關心則可以設置成為NULL
wait:阻塞式調用,等待的子進程不退出時,父進程一直不退出
如果進程由于接收到SIGCHLD而調用wait,則可期望wait會立即返回。但如果在任意時刻調用wait,則進程可能阻塞。
在一個子進程 終止前,wait使其調用者阻塞,而waitpid有一個選項,可使調用者不阻塞。
如果status不是一個空指針,則終止進程的終止狀態就存放在它所指的單元內。如果不關心終止狀態,則可將該參數設為空指針(waitpid同樣適用)。
5>waitpid函數
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int* status,int options)
返回值:
1. 當正常返回的時候waitpid返回收集到的子進程的進程ID;
2. 如果設置了選項WNOHANG(非阻塞式調用),而調用中waitpid發現沒有已退出的子進程可收集,則返回0;
3. 如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;
4. 當pid所指示的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid就會出錯返回,這時errno被設置為ECHILD.
?
參數:
1. pid:
Pid=-1,等待任一個子進程。與wait等效。
Pid>0.等待其進程ID與pid相等的子進程。
Pid==0等待其組ID等于調用進程組ID的任一個?子進程。
Pid<-1等待其組ID等于pid絕對值的任一子進程。
2. status:
WIFEXITED(status) : 若為正常終止子進程返回的狀態,則為真。(查看進程是否是正常退出)
WEXITSTATUS(status) : 若WIFEXITED非零,提取子進程退出碼。(查看進程的退出碼)
3. options:
WNOHANG :若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。
若正常結束,則返回該子進程的ID。
?
WIFEXITED(status) : 若為正常終止子進程返回的狀態,則為真。
WEXITSTATUS(status) :如果進程不是正常退出的,也就是說, WIFEXITED返回0,這個值就毫無意義。
6>進程的阻塞式等待代碼:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main() {pid_t pid;pid = fork();if(pid < 0){printf("%s fork error\n",__FUNCTION__);return 1;}else if( pid == 0 )//child{ printf("child is run, pid is : %d\n",getpid());sleep(5);exit(257);} else//father {int status = 0;pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5Sprintf("this is test for wait\n");if( WIFEXITED(status) && ret == pid ){printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else{printf("wait child failed, return.\n");return 1;} } return 0; }進程的非阻塞等待方式代碼:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main() {pid_t pid;pid = fork();if(pid < 0){printf("%s fork error\n",__FUNCTION__);return 1;}else if( pid == 0 ){ //childprintf("child is run, pid is : %d\n",getpid());sleep(5);exit(1);}else{int status = 0;pid_t ret = 0;do{ret = waitpid(-1, &status, WNOHANG);//?非阻塞式等待if( ret == 0 ){printf("child is running\n");}sleep(1);}while(ret == 0);if( WIFEXITED(status) && ret == pid ){printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else{printf("wait child failed, return.\n");return 1;} } return 0; }五、進程的程序替換
1、用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。
2、有六種以exec開頭的函數,統稱exec函數:
這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回,如果調用出錯則 返回-1, 所以exec函數只有出錯的返回值而沒有成功的返回值。
記憶規律:
不帶字母p(表示path)的exec函數 第一個參數必須是程序的相對路徑或絕對路徑,例如”/bin/ls”或”./a.out”,而不能 是”ls”或”a.out”。
對于帶字母p的函數: 如果參數中包含/,則
將其視為路徑名。 否則視為不帶路徑的程序名,在PATH環境變量的目錄列表中搜索這個程序。
帶有字母l(表示list)的exec函數要求將新程序的每個命令行參數都當作一個參數傳給它,命令行參數的個數是可變的,因此函數原型中有…,…中的最后一個可變參數應該是NULL, 起sentinel的作用。
帶有字母v(表示vector)的函數,則應該先構造一個指向各參數的指針數組,然后將該數組的首地址當作參數傳給它,數組中的最后一個指針也應該是NULL,就像main函數的argv參數或者環境變量表一樣。
對于以e(表示environment)結尾的exec函數,可以把一份新的環境變量表傳給它,其他exec函數仍使用當前的環境變量表執行新程序。
3、一個完整的例子
實例:模擬一個 Shell 外殼程序,并且讓它支持輸出重定向。
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<sys/fcntl.h> #include<sys/wait.h>int main() {while (1){printf("[test@192.168.110.142 test]$ ");fflush(stdout);char buf[1024];ssize_t s = read(0, buf, sizeof(buf)-1);if (s > 0){buf[s-1]= 0;}char *_myshell[32];_myshell[0] = buf;char* start = buf;int i = 1;while (*start){if (*start == ' '){*start = 0;start++;_myshell[i++] = start;}start++;}_myshell[i] = NULL;if (strcmp(_myshell[0], "exit") == 0){break;}if (strcmp(_myshell[i-2], ">") == 0){_myshell[i - 2] = NULL;pid_t id = fork();if (id < 0){perror("fork error!");}else if (id == 0)//child{close(1);open(_myshell[i-1], O_WRONLY|O_CREAT, 0666);execvp(_myshell[0], _myshell);}else{wait(0);}}else{pid_t id = vfork();if (id < 0){perror("vfork");}else if (0 == id){execvp(_myshell[0], _myshell);}else{wait(0);}}}return 0; }六、進程終止
1、進程終止的5種方式
正常退出
從main函數返回–語言級別的返回操作
調用exit–C庫函數
調用_exit–系統調用
異常退出
調用abort 產生SIGABOUT信號
由信號終止 ctrl+c /SIGINT
2、exit函數
對于三個終止函數(exit、_exit和_Exit),實現這一點的方法是,將其退出狀態作為參數傳送給函數。在異常終止情況下,內核產生一個指示其異常終止原因的終止狀態。在任意一種情況下,該終止狀態的父進程都能使用wait或waitpid函數取得其終止狀態。
在調用_exit時,內核將進程的退出狀態轉換成終止狀態。
exit和_exit的區別:
1)_exit是一個系統調用,exit是一個c庫函數
2)exit會執行刷新I/O緩存
3)exit會執行調用終止處理程序
?
總結
以上是生活随笔為你收集整理的Linux下的进程概论与编程二(进程控制)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 搜索二叉树
- 下一篇: C++11新特性学习