寫在前面
- 進程基礎
- 進程概念
- 進程描述符
- 進程創建
- 上下文切換
- init進程
- 進程應用
- 進程間通信
- 信號處理
- 後台進程與守護進程
- 淺談nginx多進程模型
- 常用工具介紹
- ps: 查看進程屬性
- lsof: 查看打開的文件情況
- netstat: 查看網路連接情況
- strace: 查看系統調用情況
進程基礎
基礎概念
進程是操作系統的基本概念之一,它是操作系統分配資源的基本單位,也是程序執行過程的實體。程序是代碼和數據的集合,本身是一個靜態的概念,而進程是程序的一次執行的實體,是一個動態的概念。
那在Linux操作系統中,是如何描述一個進程的呢?
進程描述符
為了管理進程,內核需要對每個進程的屬性和所需要做的事情,進行清楚的描述,這個就是進程描述符的作用,Linux中的進程描述符由task_struct
標識。
task_struct
的數據結構是相當複雜的,不僅包含了很進程屬性的欄位,而且也包括了指向其他數據結構的指針。大致結構如下:
- state: 描述進程狀態
- thread_info: 進程的基本信息
- mm:
mm_struct
指向內存區描述符的指針
- tty:
tty_struct
終端相關的描述符
- fs:
fs_struct
當前目錄
- files:
files_struct
指向文件描述符的指針
- signal:
signal_struct
所接收的信號描述
- 很多等等。。
總結一下,進程描述符完整的保存了一個進程的屬性和生命周期內的數據、狀態和行為,由一個複雜的數據結構task_struct
來表示。
進程創建
Linux創建一個進程,大致經歷的過程如下:
- 初始化進程描述符
- 申請相應的內存區域
- 設置進程狀態、加入調度隊列等等
- ...
為了完整的描述一個進程,操作系統設計了非常複雜的數據結構、也申請了大量的內存空間。但是得益於寫時複製技術,這些初始化操作,並沒有明顯的降低進程的創建速度。
寫時複製技術:當新進程(子進程)被創建時,Linux內核並不會立馬將父進程的內容複製給子進程,而僅僅當進程空間的內容發生變化時,才執行複製操作。寫時複製技術允許父子進程讀取相同的物理頁,只要兩者有一個試圖更改頁內容,內核就會把這個頁的內容拷貝到新的物理頁,並把這塊頁分給正在寫的進程。
Linux中有三種系統調用可以創建進程 clone()、fork()、vfork()
- clone(): 最基礎的創建進程的系統調用,可以指明子進程的基礎屬性(由各種FLAG標識)、堆棧等等。
- fork(): 通過clone()實現,它的堆棧指向的是父進程的堆棧,因此父子進程共享同一個用戶態堆棧。fork的子進程需要完全copy父進程的內存空間,但是得益於寫時複製技術,這個過程其實挺快。
- vfork(): 也是基於clone()來實現的,是歷史上對fork()的優化,因為fork()需要copy父進程的內存空間,並且fork()後常常執行execve()將另一個程序載入進來,在寫時複製技術之前,這種不必要的copy是代價是比較高昂的。因此vfork()實現時,會指明flag告訴clone()共享父進程的虛擬內存空間,以加快進程的創建過程。
上下文切換
概念:進程創建好之後,內核必須有能力掛起正在CPU運行的進程,並切換其他進程到CPU上執行。這種過程被稱作為進程切換、任務切換或者上下文切換。
這個過程包括硬體上下文切換和軟體上下文切換。
硬體上下文切換:主要通過彙編指令far jmp操作,將一個進程的描述符指針,替換為另一個進程描述符指針,並改變 eip、cs、esp等寄存器,從而改變程序的執行流。
軟體上下文切換:
- 內存地址的切換,切換頁全局目錄,安裝新的地址空間。
- 內核態堆棧的切換。
進程切換髮生在schedule()
函數中,內核提供了一個 need_resched 的標誌,來表明是否需要重新執行一次調度。當某個進程被搶佔或者更高優先順序的進程進入可執行狀態時,內核都會設置這個標誌。那什麼時候,內核會檢查這個標誌,來重新調度程序呢?那就是從內核態切換成用戶態,或者從中斷返回時。
執行系統調用時,會經歷用戶態與內核態的切換以及中斷返回。也就是說,每一次執行系統調用,比如fork、read、write等,都可能觸發內核調度新進程。
init進程
Linux進程是以樹形的結構組織的,每一個進程都有唯一的進程標識,簡稱PID。PID為1的常常是init進程,它相對於普通進程來說,有三個特殊之處:
- 它沒有默認的信號處理,因此如果發信號給init進程的話,會被它忽略掉,除非顯示的註冊過該信號。如果熟悉docker的同學,會觀察到docker化的進程,如果按ctrl-c是沒啥反應的,因為docker化的進程它們有獨立的pid命名空間,第一個新創出的進程,pid為1,是不會理會kill signal信號的。
- 如果一個進程退出時,它還有子進程存在,被稱為孤兒進程,那麼這些孤兒進程會重新成為init進程的子進程,轉由init進程來管理這些子進程,包括回收退出狀態、從進程表中移除等。
- 如果init進程跪了,那麼所有用戶進程都會被退出。
與孤兒進程類似的是殭屍進程,清理殭屍進程的方法,是殺掉不斷產生殭屍進程的父進程,然後這些殭屍進程會稱為孤兒進程,由init進程接管、回收。
進程應用
進程間通信
談到通信我們都知道,通信的雙方必須存在一種可以承載信息的介質,對於計算機之間的通信來說,這種介質可以是雙絞線、光纖、電磁波。那對於進程間的通信呢?這種介質有哪些呢?在Linux中,滿足這種條件的介質,可以是:
- 操作系統提供的內存介質,比如共享內存、管道、信號量等。
- 文件系統提供的文件介質,比如UNIX域套接字、文件等
- 網路設備提供的網卡介質,比如socket套接字。
- 等等。
對於操作系統提供的介質來說,常用的有
- 信號量機制
- 匿名管道(僅限父子進程)與有名管道
- SysV和POSIX
- 等等
優缺點介紹:
- 信號量:不能傳遞複雜消息,只能用來同步
- 匿名管道:容量有限速度較慢,只有父子進程能通訊
- 有名管道:任何進程間都能通訊,但速度較慢。
- 消息隊列:容量受到系統限制,有隊列的特性,先進先出。
- 共享內存:速度快,可以控制容量大小,但需要進行同步操作。
它們的用法相對較為簡單,在需要使用時查閱相關文檔即可,共享內存是比較常用的做法。
信號處理
信號最早是在Unix系統被引入,它主要用於進程間的通信,同時進程可以主動註冊信號處理函數,來檢測或者應對系統發生的事件。比如當進程訪問非法地址空間時,進程會收到操作系統發送SIGSEGV信號,默認情況下的處理方式是:該進程會退出並且把堆棧dump出來,簡稱出core。
總的來說信號的主要目的:
- 讓進程知道已經發生的特定事件。
- 強迫進程處理這個特定事件。
目前Linux支持的信號,已經默認的處理函數,可以在man手冊中查到,截圖如下: