初識fork()函數:在Linux中,fork()函數是一個非常重要的系統調用,它用于從一個已存在的進程中創建一個新的進程。新創建的進程被稱為子進程,而原進程則被稱為父進程。
#include <unistd.h> pid_t fork(void); 返回值:子進程中返回0,父進程返回子進程的ID,錯誤時返回-1
當一個進程調用fork()函數時,控制權轉移到內核中的fork代碼。內核執行以下操作:
可以看到,這里的例子創建了一個進程,PID為3109,這就是子進程。
當一個進程調用fork()函數后,會產生兩個具有相同二進制代碼的進程,且它們都運行到相同的點。但每個進程可以開始它們自己的執行路徑,如下面的程序所示。
int main(void) { pid_t pid; printf("Before: pid is %dn", getpid()); if ((pid=fork()) == -1) perror("fork()"),exit(1); printf("After: pid is %d, fork return %dn", getpid(), pid); sleep(1); return 0; }
運行結果:
[root@localhost Linux]# ./a.out Before: pid is 43676 After: pid is 43676, fork return 43677 After: pid is 43677, fork return 0
這里可以看到三行輸出,一行是Before,兩行是After。進程43676先打印Before消息,然后它打印了After。另一個After消息是由進程43677打印的。注意,進程43677沒有打印Before,為什么呢?如下圖所示:
因此,在fork之前,父進程獨立執行;在fork之后,父子兩個執行流分別執行。注意,fork之后,誰先執行完全由調度器決定。
fork()函數的返回值:子進程返回0,父進程返回子進程的PID。
寫時拷貝(copy-on-write, COW)是計算機系統中廣泛應用的一種優化技術,尤其是在操作系統、虛擬化和內存管理領域。其主要目的是節省內存資源和提高效率。
工作原理:寫時拷貝的基本思想是,當多個進程共享相同的資源(例如內存或文件)時,如果一個進程對這些資源進行修改,系統并不會立即為該進程創建資源的副本,而是推遲到該進程真正進行修改時,才為它分配一個新的副本。具體步驟如下:
- 共享資源:多個進程最初可以共享同一塊內存區域或文件(即資源是只讀的)。
- 標記只讀:系統會將這些共享的資源標記為只讀。
- 修改時拷貝:當一個進程嘗試修改共享資源時,操作系統會為該進程創建資源的副本,并將其設為可寫。其他進程仍然使用原始資源,而修改的進程則使用新的副本。
- 繼續共享:如果其他進程繼續只讀訪問原始資源,不會進行拷貝,節省內存和計算資源。
具體的理解可以看下面這一張圖片:
優點:
- 節省內存:由于多個進程或線程可以共享同一資源副本,減少了內存的消耗。
- 提高性能:避免不必要的拷貝操作,只有在修改資源時才進行拷貝,從而提高了效率。
- 提高數據一致性:寫時拷貝確保在修改數據時不會影響其他進程或線程讀取到的數據,避免了數據沖突。
缺點:
- 延遲開銷:在第一次修改資源時,系統需要創建資源的副本,這可能帶來一定的性能開銷。
- 資源消耗:如果多個進程頻繁進行寫操作,系統會進行多次資源拷貝,可能增加資源消耗。
fork的常規用法以及調用失敗的原因:一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子進程來處理請求。一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數。
原因:
- 系統中有太多的進程
- 實際用戶的進程數超過了限制
- 進程終止
進程終止的本質是釋放系統資源,就是釋放進程申請的相關內核數據結構和對應的數據和代碼。
進程終止對應的三種情況:
- 代碼運行完畢,結果正確
- 代碼運行完畢,結果不正確
- 代碼異常終止
進程常見的退出方法:
正常終止(可以通過 echo $? 查看進程退出碼):
- 從main返回
- 調用exit
_exit
異常退出:
- ctrl + c,信號終止
退出碼(退出狀態)可以告訴我們最后一次執行的命令的狀態。在命令結束以后,我們可以知道命令是成功完成的還是以錯誤結束的。其基本思想是,程序返回退出代碼 0 時表示執行成功,沒有問題。
代碼 1 或 0 以外的任何代碼都被視為不成功。
下面是Linux shell常見的退出碼:
_exit函數:
#include <unistd.h> void _exit(int status); 參數:status 定義了進程的終止狀態,父進程通過wait來獲取該值
說明:雖然status是int,但是僅有低8位可以被父進程所用。所以_exit(-1)時,在終端執行$?發現返回值是255。
exit函數:
#include <unistd.h> void exit(int status);
exit最后也會調用_exit, 但在調用_exit之前,還做了其他工作:
- 執行用戶通過 atexit或on_exit定義的清理函數。
- 關閉所有打開的流,所有的緩存數據均被寫入
- 調用_exit
示例:
int main() { printf("hello"); exit(0); } <p>int main() { printf("hello"); _exit(0); }
上面的結果分別為:
運行結果: [root@localhost linux]# ./a.out hello [root@localhost linux]#</p><p>運行結果: [root@localhost linux]# ./a.out [root@localhost linux]#
return退出:return是一種更常見的退出進程方法。執行return n等同于執行exit(n),因為調用main的運行時函數會將main的返回值作為exit的參數。
進程等待:進程等待是指在操作系統中,當一個進程無法繼續執行時,它進入一種阻塞狀態,等待某些條件或事件的發生才能恢復執行。等待通常發生在進程需要等待資源(如CPU、內存、I/O設備等)或與其他進程之間的同步和通信。
進程等待的必要性:
- 資源共享與避免沖突:多個進程共享資源時,等待機制確保不會發生沖突,避免競爭條件。
- 進程同步與通信:確保進程按照正確順序執行,例如生產者和消費者模型。
- CPU資源管理:避免無謂的CPU占用,讓等待的進程釋放CPU,提高系統效率。
- 防止死鎖:通過合理設計等待策略,避免多個進程互相等待,進入死鎖狀態。
- 提升并發性:使系統能夠并發執行多個進程,最大化資源利用。
- 提高系統穩定性:管理進程優先級,保證重要任務及時執行,確保系統穩定運行。
進程等待的方法:
wait方法:
#include <sys></p><h1>include <sys></h1><p>pid_t wait(int<em> status); 返回值:成功返回被等待進程的PID,失敗返回-1。 參數:輸出型參數,獲取子進程退出狀態,不關心則可以設置成為NULL
waitpid方法:
pid_t waitpid(pid_t pid, int </em>status, int options); 返回值:當正常返回的時候waitpid返回收集到的子進程的進程ID;如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在; 參數:pid:Pid=-1,等待任意一個子進程。與wait等效。Pid>0.等待其進程ID與pid相等的子進程。 status: 輸出型參數 WIFEXITED(status): 若為正常終止子進程返回的狀態,則為真。(查看進程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子進程退出碼。(查看進程的退出碼) options:默認為0,表示阻塞等待。WNOHANG: 若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該子進程的ID。
如果子進程已經退出,調用wait/waitpid時,wait/waitpid會立即返回,并且釋放資源,獲得子進程退出信息。如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞。如果不存在該子進程,則立即出錯返回。
獲取子進程的status:wait和waitpid都有status參數,該參數是一個輸出型參數,由操作系統填充。如果傳遞NULL,表示不關心子進程的退出狀態信息。否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16位):
進程的阻塞等待方式:
int main() { pid_t pid; pid = fork(); if(pid < 0) { perror("fork"); exit(1); } else if(pid == 0) { printf("子進程運行中,PID=%dn", getpid()); sleep(5); exit(0); } else { printf("父進程等待子進程...n"); wait(NULL); printf("子進程已終止,父進程繼續...n"); } return 0; }
進程的非阻塞等待方式:
#include <stdio.h></p><h1>include <stdlib.h></h1><h1>include <sys></h1><h1>include <unistd.h></h1><h1>include <vector></h1><p>typedef void (*handler_t)(); // 函數指針類型 std::vector<handler_t> handlers; // 函數指針數組</p><p>void fun_one() { printf("這是一個臨時任務1n"); }</p><p>void fun_two() { printf("這是一個臨時任務2n"); }</p><p>void Load() { handlers.push_back(fun_one); handlers.push_back(fun_two); }</p><p>void handler() { if (handlers.empty()) Load(); for (auto iter : handlers) iter(); }</p><p>int main() { pid_t pid; pid = fork(); if (pid < 0) { perror("fork"); exit(1); } else if (pid == 0) { printf("子進程運行中,PID=%dn", getpid()); sleep(5); exit(0); } else { printf("父進程非阻塞等待子進程...n"); int status; while (waitpid(pid, &status, WNOHANG) == 0) { printf("父進程繼續執行...n"); sleep(1); handler(); } if (WIFEXITED(status)) { printf("子進程正常終止,退出碼=%dn", WEXITSTATUS(status)); } else { printf("子進程異常終止n"); } } return 0; }