【Linux】進程的結構,創建,結束,以及程序轉化為的進程的過程


本文內容:

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號進程是由系統創建的,其他進程都是由他們的父進程創建的


關於進程的創建函數fork:

pid_t fork(void)

1.對於父進程,fork函數返回子進程的PID

2.對於子進程,fork函數返回0

3.如果創建出錯,則fork函數返回-1

函數分析:fork函數創建一個新進程,並從內核中為進程分配一個新的可用的進程標識符PID,然后將父進程空間中的內核復制到子進程,包括父進程的數據段和堆棧段,和父進程共享代碼段,這個時候子進程和父進程一模一樣!

問題:為什么對於不同的進程(父進程,子進程),fork函數的返回值會不一樣呢?

因為在復制時復制了進程的堆棧段,所以兩個進程都停留在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;
}

QQ截圖20190716141340

分析: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;
}

QQ截圖20190716143450

分析:我們發現數據段,棧中,堆中的數據,兩個進程的這些數據都是不一樣的,證明父進程和子進程沒有共享數據段和堆棧段!,對子進程中數據段和堆棧段中內容的修改,並不會影響父進程中的數據,父子進程共享代碼段的目的是節省存儲空間

父進程的資源大部分被子進程復制,只有小部分是不同的,比如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函數




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM