本文內容:
1.進程的結構
2.程序轉化為進程的過程
3.進程的創建
4.進程的結束
背景知識:
1.進程是計算機中處於運行的程序的實體
2.進程是線程的容器
3.程序本身只是指令,數據以及組織形式的描述,進程才是程序真正的運行實例
4.多個進程可以與同一個程序關聯,而每個進程則是以同步或者異步的方式獨立運行
一.Linux的進程結構
Linux進程結構由三部分組成:代碼段,數據段,堆棧段
代碼段:存放程序代碼,如果多個進程運行同一個程序則他們使用同一個代碼段
數據段:存放程序的全局變量,常量,靜態變量
堆棧段:函數的參數,函數內部定義的局部變量,進程控制塊PCB(處於進程核心堆棧的底部)
ps:
1.PCB是進程存在的唯一標識,系統通過PCB的存在而感知進程的存在
2.系統通過PCB對進程進行調度和管理,PCB包括創建進程,執行程序,退出進程以及改變進程優先級等
3.進程與PID進程標識符是一對一關系,而與程序文件之間是多對一關系!
Linux程序的生成分為四個階段:預編譯,編譯,匯編,鏈接
ps:編譯器G++經過預編譯,編譯,匯編三個步驟將源程序文件轉化為目標文件,如果程序有多個目標文件或者程序使用了庫函數,則編譯器還需要將所有的目標wen就鏈接起來,最后形成可執行程序
程序轉換為進程的步驟:
1)內核將程序代碼和數據讀入內存,為程序分配內存空間
2)內核為進程分配進程標識符PID和其他資源
3)內核為進程保存PID以及相應的狀態信息,把進程放到運行隊列中等待執行,程序轉化為進程后就可以被操作系統的調度程序調度執行了
三.進程的創建
背景知識:
1.進程創建有兩種方式:由操作系統創建,由父進程創建
2.系統啟動時,操作系統會創建一些進程,他們承擔着管理和分配系統資源的任務,這些進程通常被叫做系統進程
3.系統允許一個進程創建子進程,從而形成進程樹結構
4.整個Linux系統的所有進程也是一個樹形結構、
5.除了0號進程是由系統創建的,其他進程都是由他們的父進程創建的
pid_t fork(void)
1.對於父進程,fork函數返回子進程的PID
2.對於子進程,fork函數返回0
3.如果創建出錯,則fork函數返回-1
函數分析:fork函數創建一個新進程,並從內核中為進程分配一個新的可用的進程標識符PID,然后將父進程空間中的內核復制到子進程,包括父進程的數據段和堆棧段,和父進程共享代碼段,這個時候子進程和父進程一模一樣!
問題:為什么對於不同的進程(父進程,子進程),fork函數的返回值會不一樣呢?
因為在復制時復制了進程的堆棧段,所以兩個進程都停留在fork函數中,等待返回,因此fork函數會返回兩次,為了方便區別父進程和子進程,所以返回值不一樣
#include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<errno.h> #include<semaphore.h> using namespace std; int main() { pid_t pid; pid=fork(); if(pid<0) { cout<<"fork error"<<endl; exit(-1);//abnormal exit } else if(pid==0) { cout<<"son process,son:"<<getpid()<<",parent:"<<getppid()<<endl; } else { cout<<"parent process,parent:"<<getpid()<<"son:"<<pid<<endl; sleep(2); } return 0; }
分析:getpid為獲得當前進程的pid,getppid為獲得當前進程的父進程的pid,上述代碼驗證了fork的不同返回值
下面我們驗證一下父進程和子進程只共享了代碼段,而沒有共享數據段和堆棧段
#include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<errno.h> #include<semaphore.h> using namespace std; int data_x=1; int main() { pid_t pid; int stack_x=1; int *heap=(int*)malloc(sizeof(int)); *heap=3; pid=fork(); if(pid<0) { cout<<"fork error"<<endl; exit(-1); }else if(pid==0) { data_x++; stack_x++; (*heap)++; cout<<"son,data_x="<<data_x<<",stack_x="<<stack_x<<",heap="<<*heap<<endl; exit(0); }else { sleep(2); cout<<"parent,data_x="<<data_x<<",stack_x="<<stack_x<<",heap="<<*heap<<endl; } return 0; }
分析:我們發現數據段,棧中,堆中的數據,兩個進程的這些數據都是不一樣的,證明父進程和子進程沒有共享數據段和堆棧段!,對子進程中數據段和堆棧段中內容的修改,並不會影響父進程中的數據,父子進程共享代碼段的目的是節省存儲空間
父進程的資源大部分被子進程復制,只有小部分是不同的,比如pid,該進程的父進程號等這些東西
關於“寫時復制”概念的說明:
現在的Linux內核在實現fork函數時往往在創建子進程時並不立即復制父進程的數據段和堆棧段,而是當子進程修改這些數據內容時復制操作才會發生,內核才會給子進程分配進程空間,將父進程的內容復制過來,然后繼續后面的操作,這樣的實現對一些為了復制自身完成一些工作的進程來說更為合理!,效率也更高
四.進程的結束:
Linux中分為進程正常退出和進程異常退出
1)正常退出的方式:main函數中return 0,調用exit函數,調用_exit函數
2)異常退出的方式:調用abort函數,進程收到某個信號而該信號會使進程終止
當然,不管哪一種方式,系統最終都會執行一段相同的代碼:用來關閉進程打開的文件描述符,釋放其鎖占用的內存資源
需要區別的是,return之后控制器交給了調用函數,而exit是個函數,執行完后系統的控制權交給了系統
現在我們再來看一下_exit函數和exit函數:
_exit函數更為接近底層,exit函數是_exit函數的一個封裝,那么exit函數比 _exit函數多做了什么事情呢?
exit函數會進行【讀完/寫完緩存IO】的操作,而_exit函數則不會,在不恰當的時候使用_exit函數無法保證數據的完整性!
換句話說就是,exit函數在徹底結束進程之前會檢查文件的打開情況,把文件緩沖區的內容寫回文件!
那調用_exit函數為什么會出現數據不完整的情況呢?我們深究一下Linux底層
在Linux標准函數庫中,有一種被稱為【緩沖IO】的操作,其特征就是對應每一個打開的文件,在內存中都有一片緩沖區,每次讀文件時會連續的讀出若干條數據,這樣在下次讀數時就可以直接從內存的緩沖區中讀取,提高了速度,同樣的,每次寫文件的時候也僅僅是寫入內存緩沖區,等滿足一定的條件后(積累到一定數量的字符),再將緩沖區中的內容一次性寫入文件,這種技術大大增加了文件的讀寫速度,但是也給編程增添了一點小坑,比如有一些數據,理論上應該寫入了文件,但實際上因為沒有滿足特定的條件,它還知識保存是內存的緩沖區中,如果采用_exit函數直接結束進程,緩沖區的數據就會丟失,因此想要保證數據的完整性,就一定要使用exit函數,而不是_exit函數