UNIX 環境編程 之 fork 函數詳解

一 概述

一個進程,包括代碼、數據和分配給進程的資源。fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,也就是兩個進程可以做完全相同的事,但如果初始參數或者傳入的變量不同,兩個進程也可以做不同的事。

二 fork 函數

fork調用的一個奇妙之處就是它僅僅被調用一次,卻能夠返回兩次,它可能有三種不同的返回值:
1)在父進程中,fork返回新創建子進程的進程ID;
2)在子進程中,fork返回0;
3)如果出現錯誤,fork返回一個負值;
其實就相當於鏈表,進程形成了鏈表,父進程的fpid(p 意味point)指向子進程的進程id, 因爲子進程沒有子進程,所以其fpid爲0.

fork有兩種用法:
(1) 一個父進程希望複製自己,使父、子進程同時執行不同的代碼段。這在網絡服務進程中是常見的——父進程等待委託者的服務請求。當這種請求到達時,父進程調用 fork,使子進程處理此請求。父進程則繼續等待下一個服務請求。
(2) 一個進程要執行一個不同的程序。這對shell是常見的情況。在這種情況下,子進程在從fork返回後立即調用exec 。

一個進程調用fork()函數後,系統先給新的進程分配資源,例如存儲數據和代碼的空間。然後把原來的進程的所有值都複製到新的新進程中,只有少數值與原來的進程的值不同。相當於克隆了一個自己。注意,這是子進程所擁有的拷貝。父、子進程並不共享這些存儲空間部分。如果正文段是隻讀的,則父、子進程共享正文段
(備註:實際上,fork並不會立刻複製數據段與堆棧。而是在它們發生修改時纔會複製,而且通過MMU的使用,每次複製只會複製4K。通過寫時拷貝的方式,fork極大的提升了效率,而且安全性更高。)

三 fork 使用

unix 環境編程中給的例子,很具代表。把fork出的子進程,和父進程中的資源區別聯繫很好的展示了出來。下面給出unix用的例子:

/*
 * froktest.c
 *
 *  Created on: 2019-5-9
 *      Author: root
 */



/*
 * popen.c Written by W. Richard Stevens
 */


#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int  glob = 6;
char buf[] = "a write to stdout\n";

int main(void){

	int var  = 88;
	pid_t pid;
	if(write(STDOUT_FILENO,buf,strlen(buf)) != strlen(buf)){
		printf("write error");
	}
	printf("before fork\n");

	if((pid = fork()) <0){
		printf("fork error");
	}else if(pid == 0){
		glob ++;
		var ++;
	}else{
		sleep(2);
	}

	printf("pid:%d>>>glob:%d>>>var:%d\n",getpid(),glob,var);

	return 1;

}

運行結果如下
運行結果
可以看出子進程11944會將父進程11943的數據拷貝一份,而這時子進程就有一塊自己的緩存,而子進程在自己緩存的操作,並不影響父進程。
./fork 運行 和 ./ fork >temp.out 運行
前者當以交互方式運行該程序時,只得到 printf輸出的行一次,其原因是標準輸出緩存由新行符刷新。但是後者當將標準輸出重新定向到一個文件時,卻得到 printf輸出行兩次。原因是前者交互的時候,printf 輸出到標準輸出,是行緩存,輸出完緩存就清空了。而後者重定向到文件,printf 輸出就變成全緩存了,fork子進程的時候,printf要輸出的這行緩存也被拷貝進子進程的緩存中,因此當後面子進程運行printf 時就會再打印printf一次了。

fork 後的子進程繼承哪些父進程性質?

  • 對任一打開文件描述符的,fd引用數+1,父子進程共享同一文件的位移量。 這條性質很重要。
  • 連接的共享存儲段
  • 信號屏蔽和排列。
  • 資源限制,所在的權限,組等

而不會繼承

  • 進程id,父進程id
  • 父進程設置的鎖,子進程不繼承。

socket fd fork引用問題

進程 systest

int main(void){
	while(1){
		sleep(10);
	}
	return 1;
}

代碼很簡單,就是進程啓動後一直再運行,編譯後生產 systest

進程 fdtest_fork

int main(void){
	int  sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd <= 0) {
		printf("socket error:%d:%s\n",errno,strerror(errno));
		return 0;
	}
	struct sockaddr_in in_addr;
	in_addr.sin_family = AF_INET;
	in_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
	in_addr.sin_port = htons(8888);
	memset(in_addr.sin_zero, 0, sizeof(in_addr.sin_zero));
	socklen_t addr_len = sizeof(struct sockaddr_in);
	int ret = bind(sockfd, (struct sockaddr*) &in_addr, addr_len);
	if (ret != 0) {
		printf("socket error:%d:%s\n",errno,strerror(errno));
		vg_sock_close(sockfd);
		return 0;
	}
	ret = listen(sockfd, 1024);
	if (ret != 0) {
		printf("socket error:%d:%s\n",errno,strerror(errno));
		vg_sock_close(sockfd);
		return 0;
	}
	printf("===============sockfd:%d\n",sockfd);
	system("./systest");

	while (1) {
		sleep(10);
	}

	return 0;
}

大概就是在本地ip 0.0.0.0 綁定了一個tcp 端口 8888,返回一個sockfd
後直接調用了system 運行了 systest 進程 將代碼編譯生產 fdtest_fork 可執行文件

運行後結果如下
fdtest_fork
可發現fdtest_fork 進程 15038 創建了systest 進程 15039
然後利用kill 15038 將 fdtest_fork 進程殺死結果如下
kill fdtest_fork
在這裏插入圖片描述
發現進程fdtest_fork 進程不在了,但是當再次運行fdtest_fork 時你會發現,socket fd一直創建不成功。結果如下
fdtest_fork
原因就是 當fdtest_fork 第一次創建了socket fd後,而 system 函數 內部調用了fork,子進程調用了exec,exec會將sockfd引用計數+1(這個時候sockfd 引用計數等於2) 而殺死了fdtest_fork 進程 後,systest進程還保持着sockfd的引用 使得socket fd資源一直無法釋放。一直佔用着 tcp 8888。所以再運行fdtest_fork 時會一直顯示地址被佔用。

這個坑之前項目碰到過,定位了好一會,才發現問題所在。這裏隨便把解決方法列出來。

  • 方法1 :重寫system , 在fork 出的子進程中,先關閉父進程的socket 描述符資源
  • 方法2:對打開文件的處理與每個描述符的exec 關閉標誌位(FD_CLOEXEC)有關,進程中每個打開描述符都有一個exec關閉標誌。若此標誌設置,則在執行exec時關閉該描述符,否則該描述符仍打開。除非特地用fcntl設置了該標誌,否則系統的默認操作是在exec後仍保持這種描述符打開。
val = fcntl(fd,F_GETFD,0);
val|=FD_CLOEXEC;
fcntl(fd,F_SETFD,val |FD_CLOEXEC)

四 vfork 函數

vfork用於創建一個新進程,而該新進程的目的是 exec一個新程序,在子進程調用 exec或exit 之前,它在父進程的空間中運行。
上面的例子中,若是將fork 改成 vfork 。vfork中的子進程對變量glob和var做增1操作,結果就會改變了父進程中的變量值。

vfork和fork之間的另一個區別是:vfork保證子進程先運行,在它調用 exec 或exit之後父進程纔可能被調度運行。(如果在調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。)這裏不做深入,有興趣可驗證下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章