進程控制fork與vfork
1. 進程標識符
在前面進程描述一章節裏已經介紹過進程的兩個基本標識符pid和ppid,現在將詳細介紹進程的其他標識符。
每個進程都有非負的整形表示唯一的進程ID。一個進程終止後,其進程ID就可以再次使用了。如下是一個典型進程的ID及其類型和功能。
進程名:swapper (交換進程),進程ID:0,類型:系統進程,作用:它是內核的一部分,不執行磁盤上的程序,是調度進程。
進程名:init(init進程),進程ID:1,類型:用戶進程 ,作用:永遠不會終止,啟動系統,讀取系統初始化的文件。
進程名:pagedaemon(頁精靈進程),進程ID:2 ,類型:系統進程,作用:虛存系統的請頁操作。
除了進程ID,每個進程還有一些其他的標識符。下列函數返回這些標識符:
1 #include <sys/types.h> 2 #include <unistd.h> 3 pid_t getpid(void); //返回值:調用進程的進程ID 4 pid_t getppid(void); //返回值:調用進程的父進程ID 5 uid_t getuid(void); //返回值:調用進程的實際用戶ID 6 uid_t geteuid(void); //返回值:調用進程的有效用戶ID 7 gid_t getgid(void); //返回值:調用進程的實際組ID 8 gid_t getegid(void); //返回值:調用進程的有效組ID
以上6個函數,如果執行成功,則返回對應的ID值;失敗,則返回-1。除了進程ID和父進程ID這兩個值不能夠更改以外,其他的4個ID值在適當的條件下可以被更改。下面的示例程序用於獲取當前進程的6個ID值並打印出來。我們通過代碼來實際看看:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <errno.h> 4 #include <stdlib.h> 5 int main() 6 { 7 uid_t uid; 8 uid_t euid; 9 pid_t pid; 10 pid_t ppid; 11 pid = fork(); 12 if(pid < 0) 13 { 14 printf("%d\n",errno); 15 exit(2); 16 } 17 else if(pid == 0){ //child 18 uid = getuid(); 19 euid = geteuid(); 20 printf("child -> pid: %d, ppid : %d ,uid : %d, euid : %d\n",getpid(),getppid(),uid, euid); 21 exit(0); 22 } 23 else{ 24 uid = getuid(); 25 euid = geteuid(); 26 printf("father -> pid: %d, ppid : %d ,uid : %d, euid : %d\n",getpid(),getppid(),uid, euid); 27 sleep(2); 28 } 29 return 0; 30 }
程序運行效果如下:
2. 實際用戶和有效用戶
(1)實際用戶ID和實際用戶組ID:標識我是誰。也就是登錄用戶的uid和gid,比如我的Linux以admin登錄,在Linux運行的所有的命令的實際用戶ID都是admin的uid,實際用戶組ID都是admin的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 #include <stdio.h> 2 #include <errno.h> 3 #include <string.h> 4 #define _PATH_ "./log" 5 int main() 6 { 7 FILE *fp = fopen(_PATH_, "w"); 8 if( NULL == fp ){ 9 printf("open file is error, error code is : %d\n",_PATH_, errno); 10 return 1; 11 } 12 char *str = "this is a test\n"; 13 int i=0; 14 while( i<100 ){ 15 fwrite(str, 1, strlen(str),fp); 16 i++; 17 } 18 fclose(fp); 19 return 0; 20 }
3. 進程創建
3.1 fork()函數創建進程
前面進程描述一節裏簡單介紹過進程的創建,分別通過fork()和execve()函數創建子進程,這裏再進一步深入探討有關fork()創建子進程的問題。
一個現有進程可以調用fork創建一個新進程。返回值: 子進程中返回0,父進程中返回子進程ID,出錯返回-1。如下:
子進程是父進程的副本。例如:子進程獲得父進程數據空間、堆和棧的副本(主要是數據結構的副本)。 父子進程不共享這些存儲空間部分。父子進程共享正文段。由於fork之後經常歸屬exec,所以現在很多實現並不執行一個父進程數據段、棧和堆的完全復制。作為替代,使用了寫時復制(Copy-On-Write)技術。這些區域由父子進程共享,而且內核將他們的訪問權限改變為只讀的。如果父子進程中的任意個試圖修改這些區域,則內核只為修改區域的那塊內存制作一個副本。
下面的程序演示了fork函數,從中可以看出子進程對變量所作的改變並不去影響父進程中該變量的值。
1 #include <unistd.h> 2 #include <stdio.h> 3 int glob = 6; /* externalvariable in initialized data */ 4 char buf[] = "a write to stdout\n"; 5 int main(void) 6 { 7 int var; /* automatic variable on the stack */ 8 pid_t pid; 9 var = 88; 10 if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1) 11 perror("write error"); 12 printf("before fork\n"); /* we don‘t flush stdout */ 13 if ((pid = fork()) < 0) 14 { 15 perror("fork error"); 16 } 17 else if (pid == 0) { /* child */ 18 glob++; /* modifyvariables */ 19 var++; 20 } 21 else { 22 sleep(2); /* parent*/ 23 } 24 printf("pid = %d, glob = %d, var = %d\n", getpid(), glob,var); 25 exit(0); 26 }
執行及輸出結果:一般來說fork之後父進程和子進程的執行順序是不確定的,這取決於內核的調度算法。在上面的程序中,父進程是自己休眠2秒鐘,以使子進程先執行。
程序中fork與I/O函數之間的關系: write是不帶緩沖 的(http://blog.sina.com.cn/s/blog_6fb9dec201017tk3.html),因為在fork之前調用write,所以其數據只寫到標準輸出一次。標準I/O是緩沖的,如果標準輸出到終端設備,則它是行緩沖,否則它是全緩沖。當以交互方式運行該程序時,只得到printf輸出的行一次, 因為標準輸出到終端緩沖區由換行符沖洗。但將標準輸出重定向到一個文件時,由於緩沖區是全緩沖,遇到換行符不輸出,當調用fork時,其printf的數據仍然在緩沖區中,該數據將被復制到子進程中,該緩沖區也被復制到子進程中。於是父子進程的都有了帶改行內容的標準I/O緩沖區,所以每個進程終止時,會沖洗其緩沖區中的數據,得到第一個printf輸出兩次。
3.2 fork的文件共享特性
fork的一個特性是父進程的所有打開文件描述符都被復制到子進程中。父子進程的每個相同的打開描述符共享一個文件表項。假設一個進程有三個不同的打開文件,在從fork返回時,我們有如下所示結構:
在fork之後處理的文件描述符有兩種常見的情況:
1. 父進程等待子進程完成。在這種情況下,父進程不需對其描述符做任何處理。當子進程終止後,子進程對文件偏移量的修改已執行的更新。
2. 父子進程各各執行不同的程序段。這種情況下,在fork之後,父子進程各自關閉他們不需要使用的文件描述符,這樣就不會幹擾對方使用文件描述符。 這種方法在聯絡服務進程中經常使用。
父子進程之間的區別:
1. fork的返回值;
2. 進程ID不同;
3. 具有不同的父進程ID;
4. 子進程的tms_utime、 tms_stime、 tms_cutime及tms_ustime均被設置為0;
5. 父進程設置的文件鎖不會被子進程繼承;
6. 子進程的未處理鬧鐘被清除;
7. 子進程的未處理信號集被設置為空集。
fork有下面兩種方法:
1. 一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子進程來處理請求。
2. 一個進程要執行一個不同的程序。例如子進程從fork返回後,調用exec函數。
fork調用失敗的原因:
1. 系統中有太多的進程;
2. 實際用戶的進程數超過了限制。
3.3 vfork函數
vfork用於創建一個新進程,且該新進程的目的是exec一個新程序。 vfork與fork都創建一個新進程,但它不將新進程的地址空間復制到子進程中,因為子進程會立即調用exec,於是不會存訪問該地址空間。相反, 在子進程調用exec或exit之前,它在父進程的空間中運行,也就是說會更改父進程的數據段、棧和堆。 vfork和fork另一區別在於: vfork保證子進程先運行,在它調用exec或(exit)之後父進程才可能被調度運用。
下面是vfork的使用程序:
1 #include <stdio.h> 2 #include <string.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 int g_val = 0; 6 void fun() 7 { 8 printf("child exit\n"); 9 } 10 int main() 11 { 12 int _val = 0; 13 pid_t id = vfork(); 14 if( id < 0 ){ 15 exit(1); 16 } 17 else if( id == 0 ){ //child 18 atexit(fun); 19 printf("this is child process.\n"); 20 // ++g_val; //驗證第?點不同 21 // ++_val; 22 sleep(3); 23 exit(0); 24 } 25 else{ 26 printf("this is father process\n"); 27 //printf(“father exit, g_val = %d, _val = %d\n",g_val, _val); 28 } 29 return 0; 30 }
可見子進程直接改變了父進程的變量值,因為子進程在父進程的地址空間中運行。
進程控制fork與vfork