寫在前面

  • 進程基礎
    • 進程概念
    • 進程描述符
    • 進程創建
    • 上下文切換
    • 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創建一個進程,大致經歷的過程如下:

    1. 初始化進程描述符
    2. 申請相應的內存區域
    3. 設置進程狀態、加入調度隊列等等
    4. ...

為了完整的描述一個進程,操作系統設計了非常複雜的數據結構、也申請了大量的內存空間。但是得益於寫時複製技術,這些初始化操作,並沒有明顯的降低進程的創建速度。

寫時複製技術:當新進程(子進程)被創建時,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等寄存器,從而改變程序的執行流。

軟體上下文切換

    1. 內存地址的切換,切換頁全局目錄,安裝新的地址空間。
    2. 內核態堆棧的切換。

進程切換髮生在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手冊中查到,截圖如下:

比較常見的信號,解釋如下:

  • SIGCHLD: 一個進程通過 fork 函數創建,當它結束時,會向父進程發送SIGCHLD信號。
  • SIGHUP: 掛起信號,當檢測到控制終端,或者控制進程死亡時。比如用戶退出shell終端時,該shell啟動的所有進程,都會收到這個信號,默認是終止進程。
  • SIGINT:當用戶按下Ctrl+C組合鍵時,終端會向該進程發送此信號,默認是終止進程。
  • SIGKILL: 常用的kill -9指令會發送該信號,無條件終止進程,本信號無法被忽略。
  • SIGSEGV: 進程訪問了非法的內存地址,默認行為是終止進程併產生core堆棧。
  • SIGTERM: 程序結束信號,該信號可以被阻塞和忽略,通常標識程序正常退出。
  • SIGSTOP: 停止進程的執行,該信號不能被忽略,默認動作為暫停進程。
  • SIGPIPE: 當往一個寫端關閉的管道或socket連接中連續寫入數據時會引發SIGPIPE信號,引發SIGPIPE信號的寫操作將設置errno為EPIPE。在TCP通信中,當通信的雙方中的一方close一個連接時,若另一方接著發數據,根據TCP協議的規定,會收到一個RST響應報文,若再往這個伺服器發送數據時,系統會發出一個SIGPIPE信號給進程,告訴進程這個連接已經斷開了,不能再寫入數據。

其實在項目開發中,常常會和信號處理打交道。比如在處理程序優雅退出時,一般需要捕獲SIGINT、SIGPIPE、SIGTERM等信號,以合理的釋放資源、處理剩餘鏈接等,防止程序意外crash,導致的一些問題。

後台進程與守護進程

在接觸Linux系統時,常常會遇到後台進程與守護進程,這裡簡單的介紹一下這兩種進程。

  • 後台進程:通常情況下,進程是放置在前台執行,並佔據當前shell,在進程結束前,用戶無法再通過shell做其他操作。對於那些沒有交互的進程,可以將其放在後台啟動,也就是啟動時加一個 &,那麼在該進程運行期間,我們仍是可以通過shell操作其他命令。不過當shell退出時,該後台進程也會退出
  • 守護進程:如果一個進程總是以後台的方式啟動,並且不能受shell退出的影響而退出,那麼可以將其改造為守護進程。後續進程是系統長期運行的後台進程,比如mysqld、nginx等常見的服務進程。

那麼這兩者有啥區別呢?

  • 守護進程已經完全脫離終端,而後台進程並未完全脫離終端,即後台進程仍是可以輸出到終端的。
  • 在終端關閉時,後台進程會收到信號退出,但是守護進程則不會。

舉個例子,通過./spider &在後台執行抓取任務,但沒過多久,終端自動斷開,導致spider進程中斷退出。

在進一步了解守護進程之前,還需要了解一些會話和進程組的概念。

  • 進程組:由一系列相互關聯的進程組成,由PGID來標識,一般是進程組創建進程的PID。進程組的存在是為了方便對多個相關進程執行統一的操作,比如發送信號量給統一進程組的所有進程。
  • 會話:由若干個進程組組成,每一個進程組從屬於一個會話,一個會話對應著一個控制終端,該終端為會話所有進程組的進程所共用,其中只有前台進程組才可以與終端交互。

那如何實現一個守護進程呢?

  1. 在後台運行:fork出子進程A,當前進程退出,保留子進程A。
  2. 脫離控制終端:目的是擺脫終端的影響,通過setsid()重新為子進程A設置新的會話。
  3. 禁止子進程A重新打開終端:因為設置新會話之後的進程A,是進程組的組長,所以它是有能力重新申請打開一個控制終端。通過再次fork子進程B,並退出進程A,B不再是進程組組長,也無法打開新的終端。
  4. 關閉已打開的文件描述符、改變工作目錄等等。
  5. 處理SIGCHILD信號:由於守護進程一般是長期運行的進程,當產生子進程時,需要處理子進程退出時發送的SIGCHILD信號,不然子進程就會變成殭屍進程,從而佔據系統資源。

總結來說,守護進程是一種長期運行於後台的進程,它脫離了控制終端,不受用戶終端退出的影響。可以通過nohup操作,將一個進程變成守護進程執行。比如nohup ./spider &,這樣即使終端斷開後,spider進程仍會繼續執行。

淺談nginx多進程模型

nginx是一款高性能的Web伺服器,由於它優秀的性能、成熟的社區、完善的文檔,受到廣大開發者的喜愛和支持。它的高性能與其架構是分不開的,nginx的框架如下圖所示:

nginx架構圖-來源於網上

Nginx是經典的多進程模型,它啟動以後以守護進程的方式在後台運行,後台進程包含一個master進程,和多個worker進程。其中master進程相當於控制進程,有以下作用:

  • 接收外界信號執行指令,包括配置載入、向worker髮指令、優雅退出等等。
  • 維護worker進程的狀態,當worker進程退出後,自動啟動新的worker。

其中 master 進程支持的信號處理如下:

  • TERM、INT:快速退出
  • QUIT:優雅退出
  • HUP: 變更配置,用新配置啟動worker,優雅關閉老的worker等。
  • USR1: 重新打開日誌文件
  • USR2: 升級二進位文件(nginx升級)
  • WINCH: worker進程的優雅退出

單個worker進程也支持信號處理,包括:

  • TERM、INT: 快速退出
  • QUIT: 優雅退出
  • USR1: 重新打開日誌文件
  • WINCH: 終端調試等

worker進程基於非同步非阻塞的模式處理每個請求,這種非阻塞的模式,大大提高了worker進程處理請求的速度。為了儘可能的提高性能,nginx對每個worker進程設置了CPU的親和性,盡量把worker進程綁定在指定的CPU上執行,以減少上下文切換帶來的開銷。由於這種綁核的模式,一般推薦worker進程的數目,為CPU的核數。

nginx使用了master<->worker這種多進程的模型,有哪些好處呢?

  • worker進程間很少共享資源,在處理各自請求時,幾乎不用加鎖,省掉了鎖帶來的開銷。
  • worker進程間異常不會相互影響,一個進程掛掉之後,其他進程還在工作,可以提高服務的穩定性。
  • 儘可能的利用多核特性,最大化利用系統資源。

更多內容來源於:如何理解和應用Linux進程模型?

常用工具介紹

Linux內置了許多工具,用於排查系統問題和查看資源使用情況,這裡簡單介紹和進程有關的幾個工具。

ps: 查看進程的基本屬性

lsof: 查看進程打開的文件情況

有兩個場景:

  • 場景一:機器上一個文件大小不停的增長,導致磁碟空間一次又一次的爆滿,如果這時候你想把寫文件的罪魁禍首進程找到,那應該怎麼做呢?
  • 場景二:發現磁碟已經快滿了,通過rm -f 刪除一些大文件,但磁碟空間並沒有明顯減少,這個時候應該怎麼做呢?

對於這些場景,我們可以藉助lsof命令,

  • 對於場景一來說:可以查看該文件被哪個進程打開,找到罪魁禍首進程,然後對其處理。
  • 對於場景二來說:如果這個文件被其他進程打開,通過rm -f是無法真正刪掉一個文件的,還需要殺掉打開該文件的進程,以關閉文件描述符,那麼文件才能真正被清理。

lsof的常見用法如下:

  • 查看特定用戶打開的文件列表:lsof -u xxx
  • 查看特定埠打開的文件列表:lsof -i 8080
  • 查看特定埠範圍打開的文件列表:lsof -i :1-1024
  • 基於TCP或者UDP查看打開的文件列表:lsof -i udp
  • 查看特定進程打開的文件列表:lsof -p $pid
  • 查看打開特定文件的進程列表:lsof -t $file_name
  • 查看打開特定目錄的進程列表:lsof +D $file_path
  • 等等

netstat: 查看網路連接情況

strace: 查看系統調用情況

推薦閱讀:

相关文章