一、進程的建立與操做linux
任務描述:編程
相關知識:數組
由 fork 建立的新進程被稱爲子進程(child process)。該函數被調用一次,但返回兩次。兩次返回的區別是子進程的返回值是 0,而父進程的返回值則是新子進程的進程 ID。將子進程 ID 返回給父進程的理由是:由於一個進程的子進程能夠多於一個,因此沒有一個函數使一個進程能夠得到其全部子進程的進程 ID。fork 使子進程獲得返回值0 的理由是:一個進程只會有一個父進程,因此子進程老是能夠調用 getppid 以得到其父進程的進程 ID (進程 ID 0 老是由交換進程使用,因此一個子進程的進程 ID 不可能爲 0 )。子進程和父進程共享不少資源,除了打開文件以外,不少父進程的其餘性質也由子進程繼承數據結構
main.c:函數
#include<stdio.h> #include<unistd.h> #include<sys/types.h> int a=1; int main(void) { int b=2; pid_t pid; pid=fork(); if(pid==0){ printf("PID is %d,",getpid()); printf("PPID is %d\n",getppid()); a++;b++; printf("a=%d,b=%d\n",a,b);
exit(0); } else{ sleep(1); printf("after fork\n"); printf("PID is %d,",getpid()); printf("PPID is %d\n",getppid()); printf("a=%d,b=%d\n",a,b); } sleep(5); return 0; }
二、vfork函數的使用spa
任務描述:命令行
相關知識:線程
fork()函數經過系統調用建立一個與原來進程幾乎徹底相同的進程。
vfork 用於建立新進程,而該新進程的目的是exec一個新程序(執行一個可執行的文件)。新進程有本身的地址空間,所以 vfork 函數並不將父進程的地址空間徹底複製到子進程中。
在使用 vfork()時,必須在子進程中調用 exit()函數調用,不然會出現:_new_exitfn:Assertion `l != ((void *)0)' failed
wait()會暫時中止目前進程的執行,直到有信號來到或子進程結束。若是在調用 wait()時子進程已經結束,則 wait()會當即返回子進程結束狀態值。子進程的結束狀態值會由參數 status 返回,而子進程的進程識別碼也會一快返回。若是不在乎結束狀態值,則參數 status 能夠設成 NULL。設計
main.c:指針
#include<stdio.h> #include<unistd.h> #include<sys/types.h> int main(void) { int b=0,i; pid_t pid; printf("fork choose 0;vfork choose 1\n"); scanf("%d",&i); switch(i){ case 0:pid=fork();break; case 1:pid=vfork();break; default:printf("error\n"); } if(pid==0){ b++; exit(0); } wait(pid); printf("data is %d\n",b); return 0; }
三、子進程與父進程執行的順序
任務描述:
main.c:
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<stdlib.h> int main(void) { int i; pid_t pid; pid=fork(); if(pid==0){ for(i=1;i<6;i++){ sleep(1); printf("process ID %d,i=%d\n",getpid(),i); } exit(0); } else if(pid > 0 ){ sleep(3); printf("PPID is %d,i=%d\n",getpid(),i); }else{ perror("fork error\n"); exit(-1); } printf("hello"); wait(pid); // system("ps -o pid,ppid,state,tty,command"); return 0; }
四、vfork
任務描述:
main.c:
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<stdlib.h> int main(void) { int i; pid_t pid; pid=vfork(); if(pid==0){ for(i=1;i<10;i++){ sleep(1); printf("process ID %d,i=%d\n",getpid(),i); } exit(0); } else{ sleep(3); printf("PPID is %d,i=%d\n",getpid(),i); wait(pid); // system("ps -o pid,ppid,state,tty,command"); } return 0; }
運行程序能夠看出使用vfork是先執行子進程再執行父進程的.
五、clone函數
任務描述:
相關知識:
clone 函數功能強大,帶了衆多參數,所以由他建立的進程要比前面 2 種方法要複雜。clone 可讓你有選擇性的繼承父進程的資源,你能夠選擇想 vfork 同樣和父進程共享一個虛存空間,從而使創造的是線程,你也能夠不和父進程共享,你甚至能夠選擇創造出來的進程和父進程再也不是父子關係,而是兄弟關係。函數的結構int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);這裏 fn 是函數指針,咱們知道進程的 4 要素,這個就是指向程序的指針,就是所謂的「劇本", child_stack 明顯是爲子進程分配系統堆棧空間(在 linux 下系統堆棧空間是 2頁面,就是 8K 的內存,其中在這塊內存中,低地址上放入了值,這個值就是進程控制塊 task_struct 的值),flags 就是標誌用來描述你須要從父進程繼承那些資源, arg 就是傳給子進程的參數)。下面是 flags 能夠取的值:
CLONE_PARENT | 建立的子進程的父進程是調用者的父進程,新進程與建立它的進程成了「兄弟」而不是「父子」 |
CLONE_FS | 子進程與父進程共享相同的文件系統,包括 root、當前目錄、umask |
CLONE_FILES | 子進程與父進程共享相同的文件描述符(file descriptor)表 |
CLONE_NEWNS | 在新的 namespace 啓動子進程,namespace 描述了進程的文件hierarchy |
CLONE_SIGHAND | 子進程與父進程共享相同的信號處理(signal handler)表 |
CLONE_PTRACE | 若父進程被 trace,子進程也被 trace |
CLONE_VFORK | 父進程被掛起,直至子進程釋放虛擬內存資源 |
CLONE_VM | 子進程與父進程運行於相同的內存空間 |
CLONE_PID | 子進程在建立時 PID 與父進程一致 |
CLONE_THREAD | Linux 2.4 中增長以支持 POSIX 線程標準,子進程與父進程共享相同的線程羣 |
main.c:
#define _GNU_SOURCE #define childstack 1000 #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sched.h> int variable=9; void *stack; int fn() { variable=42; free(stack); exit(1); } int main(void) { void *stack; stack=malloc(childstack); printf("The variable was %d\n",variable); clone(&fn,stack + childstack,CLONE_VM|CLONE_FILES,NULL); sleep(3); printf("in child process\n"); printf("The variable is now %d\n",variable); return 0; }
六、WIFEXIED宏
任務描述:
相關知識:
進程一旦調用了 wait,就當即阻塞本身,由 wait 自動分析是否當前進程的某個子進程已經退出,若是讓它找到了這樣一個已經變成殭屍的子進程,wait 就會收集這個子進程的信息,並把它完全銷燬後返回;若是沒有找到這樣一個子進程,wait 就會一直阻塞在這裏,直到有一個出現爲止。
參數 status 用來保存被收集進程退出時的一些狀態,它是一個指向 int 類型的指針。但若是咱們對這個子進程是如何死掉的滿不在乎,只想把這個殭屍進程消滅掉,(事實上絕大多數狀況下,咱們都會這樣想),咱們就能夠設定這個參數爲 NULL,就象下面這樣:
pid = wait(NULL);
若是成功,wait 會返回被收集的子進程的進程 ID,若是調用進程沒有子進程,調用就會失敗,此時 wait 返回-1,同時 errno 被置爲 ECHILD。
參數 status:
若是參數 status 的值不是 NULL,wait 就會把子進程退出時的狀態取出並存入其中,這是一個整數值(int),指出了子進程是正常退出仍是被非正常結束的(一個進程也能夠被其餘進程用信號結束,咱們將在之後的文章中介紹),以及正常結束時的返回值,或被哪個信號結束的等信息。因爲這些信息被存放在一個整數的不一樣二進制位中,因此用常規的方法讀取會很是麻煩,人們就設計了一套專門的宏(macro)來完
成這項工做:
①WIFEXITED(status) 這個宏用來指出子進程是否爲正常退出的,若是是,它會返回一個非零值(請注意,雖然名字同樣,這裏的參數 status 並不一樣於 wait 惟一的參數---指向整數的指針 status,而是那個指針所指向的整數,切記不要搞混了)
②WEXITSTATUS(status) 當 WIFEXITED 返回非零值時,咱們能夠用這個宏來提取子進程的返回值,若是子進程調用 exit(5),WEXITSTATUS(status) 就會返回 5;若是子進程調用 exit(7),WEXITSTATUS(status)就會返回 7。請注意,若是進程不是正常退出的,也就是說, WIFEXITED 返回 0,這個值就毫無心義。
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { pid_t pid; pid=fork(); int status; if(pid==0){ printf("This is child process with pid of %d\n",getpid()); exit(3); } else{ pid=wait(&status); sleep(3); if(WIFEXITED(status)!=0) printf("the child process %d exit normally\n",pid); if(WEXITSTATUS(status)==3) printf("the return code is %d\n",WEXITSTATUS(status)); } return 0; }
七、waitpid函數
任務描述:
相關知識:
pid_t waitpid(pid_t pid,int *status,int options)
pid:
從參數的名字 pid 和類型 pid_t 中就能夠看出,這裏須要的是一個進程 ID。但當pid 取不一樣的值時,在這裏有不一樣的意義。pid>0 時,只等待進程 ID 等於 pid 的子進程,無論其它已經有多少子進程運行結束退出了,只要指定的子進程尚未結束,waitpid 就會一直等下去。
pid=-1 時,等待任何一個子進程退出,沒有任何限制,此時 waitpid 和 wait 的做用如出一轍。
pid=0 時,等待同一個進程組中的任何子進程,若是子進程已經加入了別的進程組,waitpid 不會對它作任何理睬。
pid<-1 時,等待一個指定進程組中的任何子進程,這個進程組的 ID 等於 pid 的絕對值。
options:
options 提供了一些額外的選項來控制 waitpid,目前在 Linux 中只支持WNOHANG 和 WUNTRACED 兩個選項,這是兩個常數,能夠用"|"運算符把它們鏈接起來使用,好比:ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);若是咱們不想使用它們,也能夠把 options 設爲 0,如:ret=waitpid(-1,NULL,0);若是使用了 WNOHANG 參數調用 waitpid,即便沒有子進程退出,它也會當即返回,不會像 wait 那樣永遠等下去。
返回值和錯誤
當正常返回的時候,waitpid 返回收集到的子進程的進程 ID;
若是設置了選項 WNOHANG,而調用中 waitpid 發現沒有已退出的子進程可收集,則返回 0;
若是調用中出錯,則返回-1,這時 errno 會被設置成相應的值以指示錯誤所在;
當 pid 所指示的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid 就會出錯返回,這時 errno 被設置爲 ECHILD;
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/wait.h> int main(void) { int pr; pid_t pid; pid=fork(); if(pid==0){ printf("This is child, pid=%d. Sleeping...\n",getpid()); sleep(10); exit(1); } else{ do{ pr=waitpid(pid,NULL,WNOHANG);//沒有子進程退出,它也會當即返回,不會像 wait 那樣永遠等下去 if(pr==0){ printf("No child exited\n"); sleep(1); } }while(pr==0); if(pid==pr) printf("successfully get child %d\n",pid); } return 0; }
八、_exit函數
任務描述:
相關知識:
①exit 系統調用帶有一個整數類型的參數 status,咱們能夠利用這個參數傳遞進程結束時的狀態,好比說,該進程是正常結束的,仍是出現某種意外而結束的,通常來講,0 表示沒有意外的正常結束;其餘的數值表示出現了錯誤,進程非正常結束。咱們在實際編程時,能夠用 wait 系統調用接收子進程的返回值,從而針對不一樣的狀況進行不一樣的處理。
②exit 和_exit 做爲系統調用而言,_exit 和 exit 是一對孿生兄弟。這種區別主要體如今它們在函數庫中的定義。_exit 在 Linux 函數庫中的原型是:
#include<unistd.h>
void _exit(int status);
exit 比較一下,exit()函數定義在 stdlib.h 中,而_exit()定義在 unistd.h 中,從名字上看,stdlib.h 彷佛比 unistd.h 高級一點,那麼,它們之間到底有什麼區別呢?
_exit()函數的做用最爲簡單:直接使進程中止運行,清除其使用的內存空間,並銷燬其在內核中的各類數據結構;exit()函數則在這些基礎上做了一些包裝,在執行退出以前加了若干道工序,也是由於這個緣由,有些人認爲 exit 已經不能算是純粹的系統調用. exit()函數與_exit()函數最大的區別就在於 exit()函數在調用 exit 系統調用以前要檢查文件的打開狀況,把文件緩衝區中的內容寫回文件,就是「清理 I/O 緩衝」.在Linux 的標準函數庫中,有一套稱做「高級 I/O」的函數,咱們熟知的 printf()、fopen()、fread()、fwrite()都在此列,它們也被稱做「緩衝I/O(buffered I/O)」,其特徵是對應每個打開的文件,在內存中都有一片緩衝區,每次讀文件時,會多讀出若干條記錄,這樣下次讀文件時就能夠直接從內存的緩衝區中讀取,每次寫文件的時候,也僅僅是寫入內存中的緩衝區,等知足了必定的條件(達到必定數量,或遇到特定字符,如換行符\n 和文件結束符 EOF),再將緩衝區中的內容一次性寫入文件,這樣就大大增長了文件讀寫的速度,但也爲咱們編程帶來了一點點麻煩。若是有一些數據,咱們認爲已經寫入了文件,實際上由於沒有知足特定的條件,它們還只是保存在緩衝區內,這時咱們用_exit()函數直接將進程關閉,緩衝區中的數據就會丟失,反之,若是想保證數據的完整性,就必定要使用 exit()函數。
main1.c:
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> int main(void) { printf("hello\n"); printf("fuck"); exit(0); return 0; }
main2.c:
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main(void) { printf("hello\n"); printf("fuck"); _exit(0); return 0; }
九、exec函數族的使用
任務描述:
相關知識:
①exec 函數族說明
fork()函數用於建立一個子進程,該子進程幾乎複製了父進程的所有內容,可是,這個新建立的進程如何執行呢?exec 函數族就提供了一個在進程中啓動另外一個程序執行的方法。它能夠根據指定的文件名或目錄名找到可執行文件,並用它來取代原調用進程的數據段、代碼段和堆棧段,在執行完以後,原調用進程的內容除了進程號外,其餘所有被新的進程替換了。另外,這裏的可執行文件既能夠是二進制文件,也能夠是Linux 下任何可執行的腳本文件。
在 Linux 中使用 exec 函數族主要有兩種狀況:
● 當進程認爲本身不能再爲系統和用戶作出任何貢獻時,就能夠調用 exec 函數族中的任意一個函數讓本身重生。
● 若是一個進程想執行另外一個程序,那麼它就能夠調用 fork()函數新建一個進程,而後調用 exec 函數族中的任意一個函數,這樣看起來就像經過執行應用程序而產生了一個新進程(這種狀況很是廣泛)。
②exec 函數族語法
實際上,在 Linux 中並無 exec()函數,而是有 6 個以 exec 開頭的函數,它們之間的語法有細微差異,本書在後面會詳細講解。
exec 函數族成員函數語法:
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[])
這 6 個函數在函數名和使用語法的規則上都有細微的區別,下面就從可執行文件查找方式、參數傳遞方式及環境變量這幾個方面進行比較。
● 查找方式。讀者能夠注意到,表 2 中的前 4 個函數的查找方式都是完整的文件目錄路徑,而最後兩個函數(也就是以 p 結尾的兩個函數)能夠只給出文件名,系統就會自動按照環境變量「$PATH」所指定的路徑進行查找。
● 參數傳遞方式。exec 函數族的參數傳遞有兩種方式:一種是逐個列舉的方式,而另外一種則是將全部參數總體構造指針數組傳遞。在這裏是以函數名的第 5 位字母來區分的,字母爲「l」(list)的表示逐個列舉參數的方式,其語法爲 const char *arg;字母爲「v」(vertor)的表示將全部參數總體構造指針數組傳遞,其語法爲 char*const argv[]。讀者能夠觀察 execl()、execle()、execlp()的語法與 execv()、execve()、execvp()的區別,它們的具體用法在後面的實例講解中會具體說明。這裏的參數實際上就是用戶在使用這個可執行文件時所需的所有命令選項字符串(包括該可執行程序命令自己)。要注意的是,這些參數必須以 NULL 結束。
● 環境變量。exec 函數族能夠默認系統的環境變量,也能夠傳入指定的環境變量。這裏以「e」(environment)結尾的兩個函數 execle()和 execve()就能夠在 envp[]中指定當前進程所使用的環境變量。
事實上,這 6 個函數中真正的系統調用只有 execve(),其餘 5 個都是庫函數,它們最終都會調用 execve()這個系統調用。在使用 exec 函數族時,必定要加上錯誤判斷語句。exec 很容易執行失敗,其中最多見的緣由有:
● 找不到文件或路徑,此時 errno 被設置爲 ENOENT。
● 數組 argv 和 envp 忘記用 NULL 結束,此時 errno 被設置爲 EFAUL。
● 沒有對應可執行文件的運行權限,此時 errno 被設置爲 EACCES。
main.c:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> int main(int argc, char *argv[]) { char *env[] = {"/bin/ls","ls", "-a", NULL}; pid_t pid; pid=fork(); if( fork() == 0 ){ printf("1------------execl------------\n" ); if( execl( "/bin/ls", "ls","-a", NULL ) == -1 ){ perror( "execl error " ); exit(1); } } wait(pid); if( fork() == 0 ){ printf("2------------execlp------------\n"); if( execlp( "ls", "ls", "-a", NULL ) < 0 ){ perror( "execlp error " ); exit(1); } } wait(pid); if( fork() == 0 ){ printf("3------------execle------------\n"); if( execle("/bin/ls", "ls", "-a", NULL, NULL) == -1 ){ perror("execle error "); exit(1); } } wait(pid); return 0; }
十、exec函數族的使用
任務描述:
相關知識:
每一個程序都有一個環境變量表,和命令行參數表同樣,環境變量表也是一個指針數組。
extern char ** environ
echoall.c:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int argc, char *argv[]) { int i; char **ptr; extern char **environ; for(i=0;i<argc;i++) printf("argv[%d]: %s\n", i, argv[i]); for(ptr=environ; *ptr!=0; ptr++) printf("%s\n",*ptr); exit(0); }
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<errno.h> #include<string.h> int main(int argc,char**argv,char**env) { execle("./echoall","./echoall", "myarg1", "MY ARG2",NULL,env); fprintf(stderr,"%s\n",strerror(errno)); return 0; }
十一、exec函數族使用
任務描述:
hello.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main(int argc,char *argv[]) { int i; char **ptr; extern char **environ; for(ptr = environ; *ptr !=0; ptr ++) printf("%s\n",*ptr); exit(0); }
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { char *ps_envp[]={"AA=11","BB=22",NULL}; pid_t pid; pid=fork(); if(pid<0){ printf("error!\n"); return -1; } if(pid==0){ printf("Entering main ...\n"); printf("hello pid=%d\n",getpid()); if(execle("./hello","hello",NULL,ps_envp)==-1){ printf("error\n"); exit(0); } }
ptr=wait(NULL); return 0; }
十二、僵死進程
任務描述:
相關知識:
殭屍進程:一個進程使用fork建立子進程,若是子進程退出,而父進程並無調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲僵死進程。
產生緣由:任何一個子進程(init除外)在exit()以後,並不是立刻就消失掉,而是留下一個稱爲殭屍進程(Zombie)的數據結構,等待父進程處理。這是每一個 子進程在結束時都要通過的階段。父進程還沒有對已終止子進程調用 wait 函數善後,尚留存進程 ID、終止狀態等信息,只有等到父進程處理或父進程結束被 init 領養才能釋放資源。若是子進程在exit()以後,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態是「Z」。若是父進程能及時處理,可能用ps命令就來不及看到子進程的殭屍狀態,但這並不等於子進程不通過殭屍狀態。 若是父進程在子進程結束以前退出,則子進程將由init接管。init將會以父進程的身份對殭屍狀態的子進程進行處理。
waitpid 返回終止子進程的進程 ID。並將該子進程的終止狀態存放在有 status 指向的存儲單元中。
#include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t pid; pid=fork(); if(pid==0){ printf("create a zoombie\n"); exit(0); } printf("pid is %d\n",getpid()); // system("ps a"); system("ps -o pid,ppid,state,tty,command"); return 0; }
1三、 僵死進程的避免
任務描述:
#include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t pid; pid=fork(); if(pid==0) { printf("create a son\n"); pid=fork(); if(pid>0) exit(0); printf("create a grandson\n"); sleep(2); printf("parent pid= %d\n",getppid()); exit(0); } return 0; }