linux系統編程之進程(三):進程複製fork,孤兒進程,殭屍進程
- 本節目標:
- 複製進程映像
- fork系統調用
- 孤兒進程、殭屍進程
- 寫時複製
一,進程複製(或產生)
使用fork函數得到的子進程從父進程的繼承了整個進程的地址空間,包括:進程上下文、進程堆棧、內存信息、打開的文件描述符、信號控制設置、進程優先級、進程組號、當前工作目錄、根目錄、資源限制、控制終端等。
子進程與父進程的區別在於:
- 父進程設置的鎖,子進程不繼承(因為如果是排它鎖,被繼承的話,矛盾了)
- 各自的進程ID和父進程ID不同
- 子進程的未決告警被清除;
- 子進程的未決信號集設置為空集。
二,fork系統調用
包含頭文件 <sys/types.h> 和 <unistd.h>
函數功能:創建一個子進程
函數原型
pid_t fork(void); //一次調用兩次返回值,是在各自的地址空間返回,意味著現在有兩個
基本一樣的進程在執行
參數:無參數。
返回值:
如果成功創建一個子進程,對於父進程來說返回子進程ID
如果成功創建一個子進程,對於子進程來說返回值為0
如果為-1表示創建失敗
流程圖:
父進程調用fork()系統調用,然後陷入內核,進行進程複製,如果成功:
1,則對調用進程即父進程來說返回值為剛產生的子進程pid,因為進程PCB沒有子進程信息,父進程只能通過這樣獲得。
2,對子進程(剛產生的新進程),則返回0,
這時就有兩個進程在接著向下執行
如果失敗,則返回0,調用進程繼續向下執行
注:fork英文意思:分支,fork系統調用複製產生的子進程與父進程(調用進程)基本一樣:代碼段+數據段+堆棧段+PCB,當前的運行環境基本一樣,所以子進程在fork之後開始向下執行,而不會從頭開始執行。
示例程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}\
while (0)\
int main(void)
{
pid_t pid;
printf("before calling fork,calling process pid = %d\n",getpid());
pid = fork();
if(pid == -1)
ERR_EXIT("fork error");
if(pid == 0){
printf("this is child process and child's pid = %d,parent's pid = %d\n",getpid(),getppid());
}
if(pid > 0){
//sleep(1);
printf("this is parent process and pid =%d ,child's pid = %d\n",getpid(),pid);
}
return 0;
}
運行結果:
當沒給父進程沒加sleep時,由於父進程先執行完,子進程成了孤兒進程,系統將其託孤給了1(init)進程,
所以ppid =1。
當加上sleep後,子進程先執行完:
這次可以正確看到想要的結果。
三,孤兒進程、殭屍進程
fork系統調用之後,父子進程將交替執行,執行順序不定。
如果父進程先退出,子進程還沒退出那麼子進程的父進程將變為init進程(託孤給了init進程)。(注:任何一個進程都必須有父進程)
如果子進程先退出,父進程還沒退出,那麼子進程必須等到父進程捕獲到了子進程的退出狀態才真正結束,否則這個時候子進程就成為僵進程(殭屍進程:只保留一些退出信息供父進程查詢)
示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}\
while (0)\
int main(void)
{
pid_t pid;
printf("before calling fork,calling process pid = %d\n",getpid());
pid = fork();
if(pid == -1)
ERR_EXIT("fork error");
if(pid == 0){
printf("this is child process and child's pid = %d,parent's pid = %d\n",getpid(),getppid());
}
if(pid > 0){
sleep(100);
printf("this is parent process and pid =%d ,child's pid = %d\n",getpid(),pid);
}
return 0;
}
以上程序跟前面那個基本一致,就是讓父進程睡眠100秒,好讓子進程先退出
運行結果:
從上可以看到,子進程先退出,但進程列表中還可以查看到子進程,[a.out] <defunct>
,死的意思,即殭屍進程,如果系統中存在過多的殭屍進程,將會使得新的進程不能產生。
四,寫時複製
linux系統為了提高系統性能和資源利用率,在fork出一個新進程時,系統並沒有真正複製一個副本。
如果多個進程要讀取它們自己的那部分資源的副本,那麼複製是不必要的。
每個進程只要保存一個指向這個資源的指針就可以了。
如果一個進程要修改自己的那份資源的“副本”,那麼就會複製那份資源。這就是寫時複製的含義
fork 和vfork: 在fork還沒實現copy on write之前。Unix設計者很關心fork之後立刻執行exec所造成的地址空間浪費,所以引入了vfork系統調用。
vfork有個限制,子進程必須立刻執行_exit或者exec函數。
即使fork實現了copy on write,效率也沒有vfork高,但是我們不推薦使用vfork,因為幾乎每一個vfork的實現,都或多或少存在一定的問題
vfork:
Linux Description
vfork(), just like fork(2), creates a child process of the calling pro-
cess. For details and return value and errors, see fork(2).
vfork() is a special case of clone(2). It is used to create new pro-
cesses without copying the page tables of the parent process. It may
be useful in performance-sensitive applications where a child will be
created which then immediately issues an execve(2).
vfork() differs from fork(2) in that the parent is suspended until the
child terminates (either normally, by calling _exit(2), or abnormally,
after delivery of a fatal signal), or it makes a call to execve(2).
Until that point, the child shares all memory with its parent, includ-
ing the stack. The child must not return from the current function or
call exit(3), but may call _exit(2).
Signal handlers are inherited, but not shared. Signals to the parent
arrive after the child releases the parent’s memory (i.e., after the
child terminates or calls execve(2)).
示例程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}\
while (0)\
int main(void)
{
pid_t pid;
int val = 1;
printf("before calling fork, val = %d\n",val);
//pid = fork();
pid = vfork();
if(pid == -1)
ERR_EXIT("fork error");
if(pid == 0){
printf("chile process,before change val, val = %d\n",val);
val++;
//sleep(1);
printf("this is child process and val = %d\n",val);
_exit(0);
}
if(pid > 0){
sleep(1);
//val++;
printf("this is parent process and val = %d\n",val);
}
return 0;
}
當調用fork時:
運行結果:
可知寫時複製
當使用vfork但子進程沒使用exit退出時:
結果出錯了,
使用vfork且exit退出:
結果正常,父子進程共享
fork之後父子進程共享文件:
fork產生的子進程與父進程相同的文件文件描述符指向相同的文件表,引用計數增加,共享文件文件偏移指針
示例程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}\
while (0)\
int main(void)
{
pid_t pid;
int fd;
fd = open("test.txt",O_WRONLY);
if(fd == -1)
ERR_EXIT("OPEN ERROR");
pid = fork();
if(pid == -1)
ERR_EXIT("fork error");
if(pid == 0){
write(fd,"child",5);
}
if(pid > 0){
//sleep(1);
write(fd,"parent",6);
}
return 0;
}
可知父子進程共享文件偏移指針,父進程寫完後文件偏移到parent後子進程開始接著寫。