您好,登錄后才能下訂單哦!
本篇文章為大家展示了怎么解析Linux進(jìn)程,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
只是簡單的描述了一下 Linux 基本概念,通過幾個例子來說明 Linux 基本應(yīng)用程序,然后以 Linux 基本內(nèi)核構(gòu)造來結(jié)尾。那么我們就深入理解一下 Linux 內(nèi)核來理解 Linux 的基本概念之進(jìn)程和線程。系統(tǒng)調(diào)用是操作系統(tǒng)本身的接口,它對于創(chuàng)建進(jìn)程和線程,內(nèi)存分配,共享文件和 I/O 來說都很重要。
我們將從各個版本的共性出發(fā)來進(jìn)行探討。
基本概念
Linux 一個非常重要的概念就是進(jìn)程,Linux 進(jìn)程和我們在
寫給大忙人看的進(jìn)程和線程
中探討的進(jìn)程模型非常相似。每個進(jìn)程都會運行一段獨立的程序,并且在初始化的時候擁有一個獨立的控制線程。換句話說,每個進(jìn)程都會有一個自己的程序計數(shù)器,這個程序計數(shù)器用來記錄下一個需要被執(zhí)行的指令。Linux 允許進(jìn)程在運行時創(chuàng)建額外的線程。
Linux 是一個多道程序設(shè)計系統(tǒng),因此系統(tǒng)中存在彼此相互獨立的進(jìn)程同時運行。此外,每個用戶都會同時有幾個活動的進(jìn)程。因為如果是一個大型系統(tǒng),可能有數(shù)百上千的進(jìn)程在同時運行。
在某些用戶空間中,即使用戶退出登錄,仍然會有一些后臺進(jìn)程在運行,這些進(jìn)程被稱為 守護(hù)進(jìn)程(daemon)。
Linux 中有一種特殊的守護(hù)進(jìn)程被稱為 計劃守護(hù)進(jìn)程(Cron daemon) ,計劃守護(hù)進(jìn)程可以每分鐘醒來一次檢查是否有工作要做,做完會繼續(xù)回到睡眠狀態(tài)等待下一次喚醒。
“ Cron 是一個守護(hù)程序,可以做任何你想做的事情,比如說你可以定期進(jìn)行系統(tǒng)維護(hù)、定期進(jìn)行系統(tǒng)備份等。在其他操作系統(tǒng)上也有類似的程序,比如 Mac OS X 上 Cron 守護(hù)程序被稱為 launchd 的守護(hù)進(jìn)程。在 Windows 上可以被稱為 計劃任務(wù)(Task Scheduler)。
在 Linux 系統(tǒng)中,進(jìn)程通過非常簡單的方式來創(chuàng)建,fork 系統(tǒng)調(diào)用會創(chuàng)建一個源進(jìn)程的拷貝(副本)。調(diào)用 fork 函數(shù)的進(jìn)程被稱為 父進(jìn)程(parent process),使用 fork 函數(shù)創(chuàng)建出來的進(jìn)程被稱為 子進(jìn)程(child process)。父進(jìn)程和子進(jìn)程都有自己的內(nèi)存映像。如果在子進(jìn)程創(chuàng)建出來后,父進(jìn)程修改了一些變量等,那么子進(jìn)程是看不到這些變化的,也就是 fork 后,父進(jìn)程和子進(jìn)程相互獨立。
雖然父進(jìn)程和子進(jìn)程保持相互獨立,但是它們卻能夠共享相同的文件,如果在 fork 之前,父進(jìn)程已經(jīng)打開了某個文件,那么 fork 后,父進(jìn)程和子進(jìn)程仍然共享這個打開的文件。對共享文件的修改會對父進(jìn)程和子進(jìn)程同時可見。
那么該如何區(qū)分父進(jìn)程和子進(jìn)程呢?子進(jìn)程只是父進(jìn)程的拷貝,所以它們幾乎所有的情況都一樣,包括內(nèi)存映像、變量、寄存器等。區(qū)分的關(guān)鍵在于 fork 函數(shù)調(diào)用后的返回值,如果 fork 后返回一個非零值,這個非零值即是子進(jìn)程的 進(jìn)程標(biāo)識符(Process Identiier, PID),而會給子進(jìn)程返回一個零值,可以用下面代碼來進(jìn)行表示
pid = fork(); // 調(diào)用 fork 函數(shù)創(chuàng)建進(jìn)程 if(pid < 0){ error() // pid < 0,創(chuàng)建失敗 } else if(pid > 0){ parent_handle() // 父進(jìn)程代碼 } else { child_handle() // 子進(jìn)程代碼 }
父進(jìn)程在 fork 后會得到子進(jìn)程的 PID,這個 PID 即能代表這個子進(jìn)程的唯一標(biāo)識符也就是 PID。如果子進(jìn)程想要知道自己的 PID,可以調(diào)用 getpid 方法。當(dāng)子進(jìn)程結(jié)束運行時,父進(jìn)程會得到子進(jìn)程的 PID,因為一個進(jìn)程會 fork 很多子進(jìn)程,子進(jìn)程也會 fork 子進(jìn)程,所以 PID 是非常重要的。我們把第一次調(diào)用 fork 后的進(jìn)程稱為 原始進(jìn)程,一個原始進(jìn)程可以生成一顆繼承樹
Linux 進(jìn)程間通信
Linux 進(jìn)程間的通信機制通常被稱為 Internel-Process communication,IPC下面我們來說一說 Linux 進(jìn)程間通信的機制,大致來說,Linux 進(jìn)程間的通信機制可以分為 6 種
下面我們分別對其進(jìn)行概述
信號 signal
信號是 UNIX 系統(tǒng)最先開始使用的進(jìn)程間通信機制,因為 Linux 是繼承于 UNIX 的,所以 Linux 也支持信號機制,通過向一個或多個進(jìn)程發(fā)送異步事件信號來實現(xiàn),信號可以從鍵盤或者訪問不存在的位置等地方產(chǎn)生;信號通過 shell 將任務(wù)發(fā)送給子進(jìn)程。
你可以在 Linux 系統(tǒng)上輸入 kill -l 來列出系統(tǒng)使用的信號,下面是我提供的一些信號
進(jìn)程可以選擇忽略發(fā)送過來的信號,但是有兩個是不能忽略的:SIGSTOP 和 SIGKILL 信號。SIGSTOP 信號會通知當(dāng)前正在運行的進(jìn)程執(zhí)行關(guān)閉操作,SIGKILL 信號會通知當(dāng)前進(jìn)程應(yīng)該被殺死。除此之外,進(jìn)程可以選擇它想要處理的信號,進(jìn)程也可以選擇阻止信號,如果不阻止,可以選擇自行處理,也可以選擇進(jìn)行內(nèi)核處理。如果選擇交給內(nèi)核進(jìn)行處理,那么就執(zhí)行默認(rèn)處理。
操作系統(tǒng)會中斷目標(biāo)程序的進(jìn)程來向其發(fā)送信號、在任何非原子指令中,執(zhí)行都可以中斷,如果進(jìn)程已經(jīng)注冊了新號處理程序,那么就執(zhí)行進(jìn)程,如果沒有注冊,將采用默認(rèn)處理的方式。
例如:當(dāng)進(jìn)程收到 SIGFPE 浮點異常的信號后,默認(rèn)操作是對其進(jìn)行 dump(轉(zhuǎn)儲)和退出。信號沒有優(yōu)先級的說法。如果同時為某個進(jìn)程產(chǎn)生了兩個信號,則可以將它們呈現(xiàn)給進(jìn)程或者以任意的順序進(jìn)行處理。
下面我們就來看一下這些信號是干什么用的
SIGABRT 和 SIGIOT
SIGABRT 和 SIGIOT 信號發(fā)送給進(jìn)程,告訴其進(jìn)行終止,這個 信號通常在調(diào)用 C標(biāo)準(zhǔn)庫的abort()函數(shù)時由進(jìn)程本身啟動
SIGALRM 、 SIGVTALRM、SIGPROF
當(dāng)設(shè)置的時鐘功能超時時會將 SIGALRM 、 SIGVTALRM、SIGPROF 發(fā)送給進(jìn)程。當(dāng)實際時間或時鐘時間超時時,發(fā)送 SIGALRM。當(dāng)進(jìn)程使用的 CPU 時間超時時,將發(fā)送 SIGVTALRM。當(dāng)進(jìn)程和系統(tǒng)代表進(jìn)程使用的CPU 時間超時時,將發(fā)送 SIGPROF。
SIGBUS
SIGBUS 將造成總線中斷錯誤時發(fā)送給進(jìn)程
SIGCHLD
當(dāng)子進(jìn)程終止、被中斷或者被中斷恢復(fù),將 SIGCHLD 發(fā)送給進(jìn)程。此信號的一種常見用法是指示操作系統(tǒng)在子進(jìn)程終止后清除其使用的資源。
SIGCONT
SIGCONT 信號指示操作系統(tǒng)繼續(xù)執(zhí)行先前由 SIGSTOP 或 SIGTSTP 信號暫停的進(jìn)程。該信號的一個重要用途是在 Unix shell 中的作業(yè)控制中。
SIGFPE
SIGFPE 信號在執(zhí)行錯誤的算術(shù)運算(例如除以零)時將被發(fā)送到進(jìn)程。
SIGUP
當(dāng) SIGUP 信號控制的終端關(guān)閉時,會發(fā)送給進(jìn)程。許多守護(hù)程序?qū)⒅匦录虞d其配置文件并重新打開其日志文件,而不是在收到此信號時退出。
SIGILL
SIGILL 信號在嘗試執(zhí)行非法、格式錯誤、未知或者特權(quán)指令時發(fā)出
SIGINT
當(dāng)用戶希望中斷進(jìn)程時,操作系統(tǒng)會向進(jìn)程發(fā)送 SIGINT 信號。用戶輸入 ctrl - c 就是希望中斷進(jìn)程。
SIGKILL
SIGKILL 信號發(fā)送到進(jìn)程以使其馬上進(jìn)行終止。與 SIGTERM 和 SIGINT 相比,這個信號無法捕獲和忽略執(zhí)行,并且進(jìn)程在接收到此信號后無法執(zhí)行任何清理操作,下面是一些例外情況
僵尸進(jìn)程無法殺死,因為僵尸進(jìn)程已經(jīng)死了,它在等待父進(jìn)程對其進(jìn)行捕獲
處于阻塞狀態(tài)的進(jìn)程只有再次喚醒后才會被 kill 掉
init 進(jìn)程是 Linux 的初始化進(jìn)程,這個進(jìn)程會忽略任何信號。
SIGKILL 通常是作為最后殺死進(jìn)程的信號、它通常作用于 SIGTERM 沒有響應(yīng)時發(fā)送給進(jìn)程。
SIGPIPE
SIGPIPE 嘗試寫入進(jìn)程管道時發(fā)現(xiàn)管道未連接無法寫入時發(fā)送到進(jìn)程
SIGPOLL
當(dāng)在明確監(jiān)視的文件描述符上發(fā)生事件時,將發(fā)送 SIGPOLL 信號。
SIGRTMIN 至 SIGRTMAX
SIGRTMIN 至 SIGRTMAX 是實時信號
SIGQUIT
當(dāng)用戶請求退出進(jìn)程并執(zhí)行核心轉(zhuǎn)儲時,SIGQUIT 信號將由其控制終端發(fā)送給進(jìn)程。
SIGSEGV
當(dāng) SIGSEGV 信號做出無效的虛擬內(nèi)存引用或分段錯誤時,即在執(zhí)行分段違規(guī)時,將其發(fā)送到進(jìn)程。
SIGSTOP
SIGSTOP 指示操作系統(tǒng)終止以便以后進(jìn)行恢復(fù)時
SIGSYS
當(dāng) SIGSYS 信號將錯誤參數(shù)傳遞給系統(tǒng)調(diào)用時,該信號將發(fā)送到進(jìn)程。
SYSTERM
我們上面簡單提到過了 SYSTERM 這個名詞,這個信號發(fā)送給進(jìn)程以請求終止。與 SIGKILL 信號不同,該信號可以被過程捕獲或忽略。這允許進(jìn)程執(zhí)行良好的終止,從而釋放資源并在適當(dāng)時保存狀態(tài)。SIGINT 與SIGTERM 幾乎相同。
SIGTSIP
SIGTSTP 信號由其控制終端發(fā)送到進(jìn)程,以請求終端停止。
SIGTTIN 和 SIGTTOU
當(dāng) SIGTTIN 和SIGTTOU 信號分別在后臺嘗試從 tty 讀取或?qū)懭霑r,信號將發(fā)送到該進(jìn)程。
SIGTRAP
在發(fā)生異?;蛘?trap 時,將 SIGTRAP 信號發(fā)送到進(jìn)程
SIGURG
當(dāng)套接字具有可讀取的緊急或帶外數(shù)據(jù)時,將 SIGURG 信號發(fā)送到進(jìn)程。
SIGUSR1 和 SIGUSR2
SIGUSR1 和 SIGUSR2 信號被發(fā)送到進(jìn)程以指示用戶定義的條件。
SIGXCPU
當(dāng) SIGXCPU 信號耗盡 CPU 的時間超過某個用戶可設(shè)置的預(yù)定值時,將其發(fā)送到進(jìn)程
SIGXFSZ
當(dāng) SIGXFSZ 信號增長超過最大允許大小的文件時,該信號將發(fā)送到該進(jìn)程。
SIGWINCH
SIGWINCH 信號在其控制終端更改其大小(窗口更改)時發(fā)送給進(jìn)程。
管道 pipe
Linux 系統(tǒng)中的進(jìn)程可以通過建立管道 pipe 進(jìn)行通信
在兩個進(jìn)程之間,可以建立一個通道,一個進(jìn)程向這個通道里寫入字節(jié)流,另一個進(jìn)程從這個管道中讀取字節(jié)流。管道是同步的,當(dāng)進(jìn)程嘗試從空管道讀取數(shù)據(jù)時,該進(jìn)程會被阻塞,直到有可用數(shù)據(jù)為止。shell 中的管線 pipelines 就是用管道實現(xiàn)的,當(dāng) shell 發(fā)現(xiàn)輸出
sort <f | head
它會創(chuàng)建兩個進(jìn)程,一個是 sort,一個是 head,sort,會在這兩個應(yīng)用程序之間建立一個管道使得 sort 進(jìn)程的標(biāo)準(zhǔn)輸出作為 head 程序的標(biāo)準(zhǔn)輸入。sort 進(jìn)程產(chǎn)生的輸出就不用寫到文件中了,如果管道滿了系統(tǒng)會停止 sort 以等待 head 讀出數(shù)據(jù)
管道實際上就是 |,兩個應(yīng)用程序不知道有管道的存在,一切都是由 shell 管理和控制的。
共享內(nèi)存 shared memory
兩個進(jìn)程之間還可以通過共享內(nèi)存進(jìn)行進(jìn)程間通信,其中兩個或者多個進(jìn)程可以訪問公共內(nèi)存空間。兩個進(jìn)程的共享工作是通過共享內(nèi)存完成的,一個進(jìn)程所作的修改可以對另一個進(jìn)程可見(很像線程間的通信)。
在使用共享內(nèi)存前,需要經(jīng)過一系列的調(diào)用流程,流程如下
創(chuàng)建共享內(nèi)存段或者使用已創(chuàng)建的共享內(nèi)存段(shmget())
將進(jìn)程附加到已經(jīng)創(chuàng)建的內(nèi)存段中(shmat())
從已連接的共享內(nèi)存段分離進(jìn)程(shmdt())
對共享內(nèi)存段執(zhí)行控制操作(shmctl())
先入先出隊列 FIFO
先入先出隊列 FIFO 通常被稱為 命名管道(Named Pipes),命名管道的工作方式與常規(guī)管道非常相似,但是確實有一些明顯的區(qū)別。未命名的管道沒有備份文件:操作系統(tǒng)負(fù)責(zé)維護(hù)內(nèi)存中的緩沖區(qū),用來將字節(jié)從寫入器傳輸?shù)阶x取器。一旦寫入或者輸出終止的話,緩沖區(qū)將被回收,傳輸?shù)臄?shù)據(jù)會丟失。相比之下,命名管道具有支持文件和獨特 API ,命名管道在文件系統(tǒng)中作為設(shè)備的專用文件存在。當(dāng)所有的進(jìn)程通信完成后,命名管道將保留在文件系統(tǒng)中以備后用。命名管道具有嚴(yán)格的 FIFO 行為
寫入的第一個字節(jié)是讀取的第一個字節(jié),寫入的第二個字節(jié)是讀取的第二個字節(jié),依此類推。
消息隊列 Message Queue
一聽到消息隊列這個名詞你可能不知道是什么意思,消息隊列是用來描述內(nèi)核尋址空間內(nèi)的內(nèi)部鏈接列表??梢园磶追N不同的方式將消息按順序發(fā)送到隊列并從隊列中檢索消息。每個消息隊列由 IPC 標(biāo)識符唯一標(biāo)識。消息隊列有兩種模式,一種是嚴(yán)格模式, 嚴(yán)格模式就像是 FIFO 先入先出隊列似的,消息順序發(fā)送,順序讀取。還有一種模式是 非嚴(yán)格模式,消息的順序性不是非常重要。
套接字 Socket
還有一種管理兩個進(jìn)程間通信的是使用 socket,socket 提供端到端的雙相通信。一個套接字可以與一個或多個進(jìn)程關(guān)聯(lián)。就像管道有命令管道和未命名管道一樣,套接字也有兩種模式,套接字一般用于兩個進(jìn)程之間的網(wǎng)絡(luò)通信,網(wǎng)絡(luò)套接字需要來自諸如TCP(傳輸控制協(xié)議)或較低級別UDP(用戶數(shù)據(jù)報協(xié)議)等基礎(chǔ)協(xié)議的支持。
套接字有以下幾種分類
順序包套接字(Sequential Packet Socket):此類套接字為最大長度固定的數(shù)據(jù)報提供可靠的連接。此連接是雙向的并且是順序的。
數(shù)據(jù)報套接字(Datagram Socket):數(shù)據(jù)包套接字支持雙向數(shù)據(jù)流。數(shù)據(jù)包套接字接受消息的順序與發(fā)送者可能不同。
流式套接字(Stream Socket):流套接字的工作方式類似于電話對話,提供雙向可靠的數(shù)據(jù)流。
原始套接字(Raw Socket):可以使用原始套接字訪問基礎(chǔ)通信協(xié)議。
Linux 中進(jìn)程管理系統(tǒng)調(diào)用
現(xiàn)在關(guān)注一下 Linux 系統(tǒng)中與進(jìn)程管理相關(guān)的系統(tǒng)調(diào)用。在了解之前你需要先知道一下什么是系統(tǒng)調(diào)用。
操作系統(tǒng)為我們屏蔽了硬件和軟件的差異,它的最主要功能就是為用戶提供一種抽象,隱藏內(nèi)部實現(xiàn),讓用戶只關(guān)心在 GUI 圖形界面下如何使用即可。操作系統(tǒng)可以分為兩種模式
內(nèi)核態(tài):操作系統(tǒng)內(nèi)核使用的模式
用戶態(tài):用戶應(yīng)用程序所使用的模式
我們常說的上下文切換 指的就是內(nèi)核態(tài)模式和用戶態(tài)模式的頻繁切換。而系統(tǒng)調(diào)用指的就是引起內(nèi)核態(tài)和用戶態(tài)切換的一種方式,系統(tǒng)調(diào)用通常在后臺靜默運行,表示計算機程序向其操作系統(tǒng)內(nèi)核請求服務(wù)。
系統(tǒng)調(diào)用指令有很多,下面是一些與進(jìn)程管理相關(guān)的最主要的系統(tǒng)調(diào)用
fork
fork 調(diào)用用于創(chuàng)建一個與父進(jìn)程相同的子進(jìn)程,創(chuàng)建完進(jìn)程后的子進(jìn)程擁有和父進(jìn)程一樣的程序計數(shù)器、相同的 CPU 寄存器、相同的打開文件。
exec
exec 系統(tǒng)調(diào)用用于執(zhí)行駐留在活動進(jìn)程中的文件,調(diào)用 exec 后,新的可執(zhí)行文件會替換先前的可執(zhí)行文件并獲得執(zhí)行。也就是說,調(diào)用 exec 后,會將舊文件或程序替換為新文件或執(zhí)行,然后執(zhí)行文件或程序。新的執(zhí)行程序被加載到相同的執(zhí)行空間中,因此進(jìn)程的 PID不會修改,因為我們沒有創(chuàng)建新進(jìn)程,只是替換舊進(jìn)程。但是進(jìn)程的數(shù)據(jù)、代碼、堆棧都已經(jīng)被修改。如果當(dāng)前要被替換的進(jìn)程包含多個線程,那么所有的線程將被終止,新的進(jìn)程映像被加載執(zhí)行。
這里需要解釋一下進(jìn)程映像(Process image) 的概念
什么是進(jìn)程映像呢?進(jìn)程映像是執(zhí)行程序時所需要的可執(zhí)行文件,通常會包括下面這些東西
代碼段(codesegment/textsegment)
又稱文本段,用來存放指令,運行代碼的一塊內(nèi)存空間
此空間大小在代碼運行前就已經(jīng)確定
內(nèi)存空間一般屬于只讀,某些架構(gòu)的代碼也允許可寫
在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。
數(shù)據(jù)段(datasegment)
可讀可寫
存儲初始化的全局變量和初始化的 static 變量
數(shù)據(jù)段中數(shù)據(jù)的生存期是隨程序持續(xù)性(隨進(jìn)程持續(xù)性) 隨進(jìn)程持續(xù)性:進(jìn)程創(chuàng)建就存在,進(jìn)程死亡就消失
bss 段(bsssegment):
可讀可寫
存儲未初始化的全局變量和未初始化的 static 變量
bss 段中的數(shù)據(jù)一般默認(rèn)為 0
Data 段
是可讀寫的,因為變量的值可以在運行時更改。此段的大小也固定。
棧(stack):
可讀可寫
存儲的是函數(shù)或代碼中的局部變量(非 static 變量)
棧的生存期隨代碼塊持續(xù)性,代碼塊運行就給你分配空間,代碼塊結(jié)束,就自動回收空間
堆(heap):
可讀可寫
存儲的是程序運行期間動態(tài)分配的 malloc/realloc 的空間
堆的生存期隨進(jìn)程持續(xù)性,從 malloc/realloc 到 free 一直存在
下面是這些區(qū)域的構(gòu)成圖
exec 系統(tǒng)調(diào)用是一些函數(shù)的集合,這些函數(shù)是
execl
execle
execlp
execv
execve
execvp
下面來看一下 exec 的工作原理
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
當(dāng)前進(jìn)程映像被替換為新的進(jìn)程映像
新的進(jìn)程映像是你做為 exec 傳遞的燦睡
結(jié)束當(dāng)前正在運行的進(jìn)程
新的進(jìn)程映像有 PID,相同的環(huán)境和一些文件描述符(因為未替換進(jìn)程,只是替換了進(jìn)程映像)
CPU 狀態(tài)和虛擬內(nèi)存受到影響,當(dāng)前進(jìn)程映像的虛擬內(nèi)存映射被新進(jìn)程映像的虛擬內(nèi)存代替。
waitpid
等待子進(jìn)程結(jié)束或終止
exit
在許多計算機操作系統(tǒng)上,計算機進(jìn)程的終止是通過執(zhí)行 exit 系統(tǒng)調(diào)用命令執(zhí)行的。0 表示進(jìn)程能夠正常結(jié)束,其他值表示進(jìn)程以非正常的行為結(jié)束。
其他一些常見的系統(tǒng)調(diào)用如下
系統(tǒng)調(diào)用指令 | 描述 |
---|---|
pause | 掛起信號 |
nice | 改變分時進(jìn)程的優(yōu)先級 |
ptrace | 進(jìn)程跟蹤 |
kill | 向進(jìn)程發(fā)送信號 |
pipe | 創(chuàng)建管道 |
mkfifo | 創(chuàng)建 fifo 的特殊文件(命名管道) |
sigaction | 設(shè)置對指定信號的處理方法 |
msgctl | 消息控制操作 |
semctl | 信號量控制 |
Linux 進(jìn)程和線程的實現(xiàn)
Linux 進(jìn)程
Linux 進(jìn)程就像一座冰山,你看到的只是冰山一角。
在 Linux 內(nèi)核結(jié)構(gòu)中,進(jìn)程會被表示為 任務(wù),通過結(jié)構(gòu)體 structure 來創(chuàng)建。不像其他的操作系統(tǒng)會區(qū)分進(jìn)程、輕量級進(jìn)程和線程,Linux 統(tǒng)一使用任務(wù)結(jié)構(gòu)來代表執(zhí)行上下文。因此,對于每個單線程進(jìn)程來說,單線程進(jìn)程將用一個任務(wù)結(jié)構(gòu)表示,對于多線程進(jìn)程來說,將為每一個用戶級線程分配一個任務(wù)結(jié)構(gòu)。Linux 內(nèi)核是多線程的,并且內(nèi)核級線程不與任何用戶級線程相關(guān)聯(lián)。
對于每個進(jìn)程來說,在內(nèi)存中都會有一個 task_struct 進(jìn)程描述符與之對應(yīng)。進(jìn)程描述符包含了內(nèi)核管理進(jìn)程所有有用的信息,包括 調(diào)度參數(shù)、打開文件描述符等等。進(jìn)程描述符從進(jìn)程創(chuàng)建開始就一直存在于內(nèi)核堆棧中。
Linux 和 Unix 一樣,都是通過 PID 來區(qū)分不同的進(jìn)程,內(nèi)核會將所有進(jìn)程的任務(wù)結(jié)構(gòu)組成為一個雙向鏈表。PID 能夠直接被映射稱為進(jìn)程的任務(wù)結(jié)構(gòu)所在的地址,從而不需要遍歷雙向鏈表直接訪問。
我們上面提到了進(jìn)程描述符,這是一個非常重要的概念,我們上面還提到了進(jìn)程描述符是位于內(nèi)存中的,這里我們省略了一句話,那就是進(jìn)程描述符是存在用戶的任務(wù)結(jié)構(gòu)中,當(dāng)進(jìn)程位于內(nèi)存并開始運行時,進(jìn)程描述符才會被調(diào)入內(nèi)存。
“ 進(jìn)程位于內(nèi)存被稱為 PIM(Process In Memory) ,這是馮諾伊曼體系架構(gòu)的一種體現(xiàn),加載到內(nèi)存中并執(zhí)行的程序稱為進(jìn)程。簡單來說,一個進(jìn)程就是正在執(zhí)行的程序。
進(jìn)程描述符可以歸為下面這幾類
調(diào)度參數(shù)(scheduling parameters):進(jìn)程優(yōu)先級、最近消耗 CPU 的時間、最近睡眠時間一起決定了下一個需要運行的進(jìn)程
內(nèi)存映像(memory image):我們上面說到,進(jìn)程映像是執(zhí)行程序時所需要的可執(zhí)行文件,它由數(shù)據(jù)和代碼組成。
信號(signals):顯示哪些信號被捕獲、哪些信號被執(zhí)行
寄存器:當(dāng)發(fā)生內(nèi)核陷入 (trap) 時,寄存器的內(nèi)容會被保存下來。
系統(tǒng)調(diào)用狀態(tài)(system call state):當(dāng)前系統(tǒng)調(diào)用的信息,包括參數(shù)和結(jié)果
文件描述符表(file descriptor table):有關(guān)文件描述符的系統(tǒng)被調(diào)用時,文件描述符作為索引在文件描述符表中定位相關(guān)文件的 i-node 數(shù)據(jù)結(jié)構(gòu)
統(tǒng)計數(shù)據(jù)(accounting):記錄用戶、進(jìn)程占用系統(tǒng) CPU 時間表的指針,一些操作系統(tǒng)還保存進(jìn)程最多占用的 CPU 時間、進(jìn)程擁有的最大堆棧空間、進(jìn)程可以消耗的頁面數(shù)等。
內(nèi)核堆棧(kernel stack):進(jìn)程的內(nèi)核部分可以使用的固定堆棧
其他:當(dāng)前進(jìn)程狀態(tài)、事件等待時間、距離警報的超時時間、PID、父進(jìn)程的 PID 以及用戶標(biāo)識符等
有了上面這些信息,現(xiàn)在就很容易描述在 Linux 中是如何創(chuàng)建這些進(jìn)程的了,創(chuàng)建新流程實際上非常簡單。為子進(jìn)程開辟一塊新的用戶空間的進(jìn)程描述符,然后從父進(jìn)程復(fù)制大量的內(nèi)容。為這個子進(jìn)程分配一個 PID,設(shè)置其內(nèi)存映射,賦予它訪問父進(jìn)程文件的權(quán)限,注冊并啟動。
當(dāng)執(zhí)行 fork 系統(tǒng)調(diào)用時,調(diào)用進(jìn)程會陷入內(nèi)核并創(chuàng)建一些和任務(wù)相關(guān)的數(shù)據(jù)結(jié)構(gòu),比如內(nèi)核堆棧(kernel stack) 和 thread_info 結(jié)構(gòu)。
“ 關(guān)于 thread_info 結(jié)構(gòu)可以參考https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch_2avr32_2include_2asm_2thread__info_8h_source.html
這個結(jié)構(gòu)中包含進(jìn)程描述符,進(jìn)程描述符位于固定的位置,使得 Linux 系統(tǒng)只需要很小的開銷就可以定位到一個運行中進(jìn)程的數(shù)據(jù)結(jié)構(gòu)。
進(jìn)程描述符的主要內(nèi)容是根據(jù)父進(jìn)程的描述符來填充。Linux 操作系統(tǒng)會尋找一個可用的 PID,并且此 PID 沒有被任何進(jìn)程使用,更新進(jìn)程標(biāo)示符使其指向一個新的數(shù)據(jù)結(jié)構(gòu)即可。為了減少 hash table 的碰撞,進(jìn)程描述符會形成鏈表。它還將 task_struct 的字段設(shè)置為指向任務(wù)數(shù)組上相應(yīng)的上一個/下一個進(jìn)程。
“ task_struct :Linux 進(jìn)程描述符,內(nèi)部涉及到眾多 C++ 源碼,我們會在后面進(jìn)行講解。
從原則上來說,為子進(jìn)程開辟內(nèi)存區(qū)域并為子進(jìn)程分配數(shù)據(jù)段、堆棧段,并且對父進(jìn)程的內(nèi)容進(jìn)行復(fù)制,但是實際上 fork 完成后,子進(jìn)程和父進(jìn)程沒有共享內(nèi)存,所以需要復(fù)制技術(shù)來實現(xiàn)同步,但是復(fù)制開銷比較大,因此 Linux 操作系統(tǒng)使用了一種 欺騙 方式。即為子進(jìn)程分配頁表,然后新分配的頁表指向父進(jìn)程的頁面,同時這些頁面是只讀的。當(dāng)進(jìn)程向這些頁面進(jìn)行寫入的時候,會開啟保護(hù)錯誤。內(nèi)核發(fā)現(xiàn)寫入操作后,會為進(jìn)程分配一個副本,使得寫入時把數(shù)據(jù)復(fù)制到這個副本上,這個副本是共享的,這種方式稱為 寫入時復(fù)制(copy on write),這種方式避免了在同一塊內(nèi)存區(qū)域維護(hù)兩個副本的必要,節(jié)省內(nèi)存空間。
在子進(jìn)程開始運行后,操作系統(tǒng)會調(diào)用 exec 系統(tǒng)調(diào)用,內(nèi)核會進(jìn)行查找驗證可執(zhí)行文件,把參數(shù)和環(huán)境變量復(fù)制到內(nèi)核,釋放舊的地址空間。
現(xiàn)在新的地址空間需要被創(chuàng)建和填充。如果系統(tǒng)支持映射文件,就像 Unix 系統(tǒng)一樣,那么新的頁表就會創(chuàng)建,表明內(nèi)存中沒有任何頁,除非所使用的頁面是堆棧頁,其地址空間由磁盤上的可執(zhí)行文件支持。新進(jìn)程開始運行時,立刻會收到一個缺頁異常(page fault),這會使具有代碼的頁面加載進(jìn)入內(nèi)存。最后,參數(shù)和環(huán)境變量被復(fù)制到新的堆棧中,重置信號,寄存器全部清零。新的命令開始運行。
下面是一個示例,用戶輸出 ls,shell 會調(diào)用 fork 函數(shù)復(fù)制一個新進(jìn)程,shell 進(jìn)程會調(diào)用 exec 函數(shù)用可執(zhí)行文件 ls 的內(nèi)容覆蓋它的內(nèi)存。
Linux 線程
現(xiàn)在我們來討論一下 Linux 中的線程,線程是輕量級的進(jìn)程,想必這句話你已經(jīng)聽過很多次了,輕量級體現(xiàn)在所有的進(jìn)程切換都需要清除所有的表、進(jìn)程間的共享信息也比較麻煩,一般來說通過管道或者共享內(nèi)存,如果是 fork 函數(shù)后的父子進(jìn)程則使用共享文件,然而線程切換不需要像進(jìn)程一樣具有昂貴的開銷,而且線程通信起來也更方便。線程分為兩種:用戶級線程和內(nèi)核級線程
用戶級線程
用戶級線程避免使用內(nèi)核,通常,每個線程會顯示調(diào)用開關(guān),發(fā)送信號或者執(zhí)行某種切換操作來放棄 CPU,同樣,計時器可以強制進(jìn)行開關(guān),用戶線程的切換速度通常比內(nèi)核線程快很多。在用戶級別實現(xiàn)線程會有一個問題,即單個線程可能會壟斷 CPU 時間片,導(dǎo)致其他線程無法執(zhí)行從而 餓死。如果執(zhí)行一個 I/O 操作,那么 I/O 會阻塞,其他線程也無法運行。
一種解決方案是,一些用戶級的線程包解決了這個問題??梢允褂脮r鐘周期的監(jiān)視器來控制第一時間時間片獨占。然后,一些庫通過特殊的包裝來解決系統(tǒng)調(diào)用的 I/O 阻塞問題,或者可以為非阻塞 I/O 編寫任務(wù)。
內(nèi)核級線程
內(nèi)核級線程通常使用幾個進(jìn)程表在內(nèi)核中實現(xiàn),每個任務(wù)都會對應(yīng)一個進(jìn)程表。在這種情況下,內(nèi)核會在每個進(jìn)程的時間片內(nèi)調(diào)度每個線程。
所有能夠阻塞的調(diào)用都會通過系統(tǒng)調(diào)用的方式來實現(xiàn),當(dāng)一個線程阻塞時,內(nèi)核可以進(jìn)行選擇,是運行在同一個進(jìn)程中的另一個線程(如果有就緒線程的話)還是運行一個另一個進(jìn)程中的線程。
從用戶空間 -> 內(nèi)核空間 -> 用戶空間的開銷比較大,但是線程初始化的時間損耗可以忽略不計。這種實現(xiàn)的好處是由時鐘決定線程切換時間,因此不太可能將時間片與任務(wù)中的其他線程占用時間綁定到一起。同樣,I/O 阻塞也不是問題。
混合實現(xiàn)
結(jié)合用戶空間和內(nèi)核空間的優(yōu)點,設(shè)計人員采用了一種內(nèi)核級線程的方式,然后將用戶級線程與某些或者全部內(nèi)核線程多路復(fù)用起來
在這種模型中,編程人員可以自由控制用戶線程和內(nèi)核線程的數(shù)量,具有很大的靈活度。采用這種方法,內(nèi)核只識別內(nèi)核級線程,并對其進(jìn)行調(diào)度。其中一些內(nèi)核級線程會被多個用戶級線程多路復(fù)用。
Linux 調(diào)度
下面我們來關(guān)注一下 Linux 系統(tǒng)的調(diào)度算法,首先需要認(rèn)識到,Linux 系統(tǒng)的線程是內(nèi)核線程,所以 Linux 系統(tǒng)是基于線程的,而不是基于進(jìn)程的。
為了進(jìn)行調(diào)度,Linux 系統(tǒng)將線程分為三類
實時先入先出
實時輪詢
分時
實時先入先出線程具有最高優(yōu)先級,它不會被其他線程所搶占,除非那是一個剛剛準(zhǔn)備好的,擁有更高優(yōu)先級的線程進(jìn)入。實時輪轉(zhuǎn)線程與實時先入先出線程基本相同,只是每個實時輪轉(zhuǎn)線程都有一個時間量,時間到了之后就可以被搶占。如果多個實時線程準(zhǔn)備完畢,那么每個線程運行它時間量所規(guī)定的時間,然后插入到實時輪轉(zhuǎn)線程末尾。
“ 注意這個實時只是相對的,無法做到絕對的實時,因為線程的運行時間無法確定。它們相對分時系統(tǒng)來說,更加具有實時性
Linux 系統(tǒng)會給每個線程分配一個 nice 值,這個值代表了優(yōu)先級的概念。nice 值默認(rèn)值是 0 ,但是可以通過系統(tǒng)調(diào)用 nice 值來修改。修改值的范圍從 -20 - +19。nice 值決定了線程的靜態(tài)優(yōu)先級。一般系統(tǒng)管理員的 nice 值會比一般線程的優(yōu)先級高,它的范圍是 -20 - -1。
下面我們更詳細(xì)的討論一下 Linux 系統(tǒng)的兩個調(diào)度算法,它們的內(nèi)部與調(diào)度隊列(runqueue) 的設(shè)計很相似。運行隊列有一個數(shù)據(jù)結(jié)構(gòu)用來監(jiān)視系統(tǒng)中所有可運行的任務(wù)并選擇下一個可以運行的任務(wù)。每個運行隊列和系統(tǒng)中的每個 CPU 有關(guān)。
Linux O(1) 調(diào)度器是歷史上很流行的一個調(diào)度器。這個名字的由來是因為它能夠在常數(shù)時間內(nèi)執(zhí)行任務(wù)調(diào)度。在 O(1) 調(diào)度器里,調(diào)度隊列被組織成兩個數(shù)組,一個是任務(wù)正在活動的數(shù)組,一個是任務(wù)過期失效的數(shù)組。如下圖所示,每個數(shù)組都包含了 140 個鏈表頭,每個鏈表頭具有不同的優(yōu)先級。
大致流程如下:
調(diào)度器從正在活動數(shù)組中選擇一個優(yōu)先級最高的任務(wù)。如果這個任務(wù)的時間片過期失效了,就把它移動到過期失效數(shù)組中。如果這個任務(wù)阻塞了,比如說正在等待 I/O 事件,那么在它的時間片過期失效之前,一旦 I/O 操作完成,那么這個任務(wù)將會繼續(xù)運行,它將被放回到之前正在活動的數(shù)組中,因為這個任務(wù)之前已經(jīng)消耗一部分 CPU 時間片,所以它將運行剩下的時間片。當(dāng)這個任務(wù)運行完它的時間片后,它就會被放到過期失效數(shù)組中。一旦正在活動的任務(wù)數(shù)組中沒有其他任務(wù)后,調(diào)度器將會交換指針,使得正在活動的數(shù)組變?yōu)檫^期失效數(shù)組,過期失效數(shù)組變?yōu)檎诨顒拥臄?shù)組。使用這種方式可以保證每個優(yōu)先級的任務(wù)都能夠得到執(zhí)行,不會導(dǎo)致線程饑餓。
在這種調(diào)度方式中,不同優(yōu)先級的任務(wù)所得到 CPU 分配的時間片也是不同的,高優(yōu)先級進(jìn)程往往能得到較長的時間片,低優(yōu)先級的任務(wù)得到較少的時間片。
這種方式為了保證能夠更好的提供服務(wù),通常會為 交互式進(jìn)程 賦予較高的優(yōu)先級,交互式進(jìn)程就是用戶進(jìn)程。
Linux 系統(tǒng)不知道一個任務(wù)究竟是 I/O 密集型的還是 CPU 密集型的,它只是依賴于交互式的方式,Linux 系統(tǒng)會區(qū)分是靜態(tài)優(yōu)先級 還是 動態(tài)優(yōu)先級。動態(tài)優(yōu)先級是采用一種獎勵機制來實現(xiàn)的。獎勵機制有兩種方式:獎勵交互式線程、懲罰占用 CPU 的線程。在 Linux O(1) 調(diào)度器中,最高的優(yōu)先級獎勵是 -5,注意這個優(yōu)先級越低越容易被線程調(diào)度器接受,所以最高懲罰的優(yōu)先級是 +5。具體體現(xiàn)就是操作系統(tǒng)維護(hù)一個名為 sleep_avg 的變量,任務(wù)喚醒會增加 sleep_avg 變量的值,當(dāng)任務(wù)被搶占或者時間量過期會減少這個變量的值,反映在獎勵機制上。
“ O(1) 調(diào)度算法是 2.6 內(nèi)核版本的調(diào)度器,最初引入這個調(diào)度算法的是不穩(wěn)定的 2.5 版本。早期的調(diào)度算法在多處理器環(huán)境中說明了通過訪問正在活動數(shù)組就可以做出調(diào)度的決定。使調(diào)度可以在固定的時間 O(1) 完成。
O(1) 調(diào)度器使用了一種 啟發(fā)式 的方式,這是什么意思?
“ 在計算機科學(xué)中,啟發(fā)式是一種當(dāng)傳統(tǒng)方式解決問題很慢時用來快速解決問題的方式,或者找到一個在傳統(tǒng)方法無法找到任何精確解的情況下找到近似解。
O(1) 使用啟發(fā)式的這種方式,會使任務(wù)的優(yōu)先級變得復(fù)雜并且不完善,從而導(dǎo)致在處理交互任務(wù)時性能很糟糕。
為了改進(jìn)這個缺點,O(1) 調(diào)度器的開發(fā)者又提出了一個新的方案,即 公平調(diào)度器(Completely Fair Scheduler, CFS)。CFS 的主要思想是使用一顆紅黑樹作為調(diào)度隊列。
“ 數(shù)據(jù)結(jié)構(gòu)太重要了。
CFS 會根據(jù)任務(wù)在 CPU 上的運行時間長短而將其有序地排列在樹中,時間精確到納秒級。下面是 CFS 的構(gòu)造模型
CFS 的調(diào)度過程如下:
CFS 算法總是優(yōu)先調(diào)度哪些使用 CPU 時間最少的任務(wù)。最小的任務(wù)一般都是在最左邊的位置。當(dāng)有一個新的任務(wù)需要運行時,CFS 會把這個任務(wù)和最左邊的數(shù)值進(jìn)行對比,如果此任務(wù)具有最小時間值,那么它將進(jìn)行運行,否則它會進(jìn)行比較,找到合適的位置進(jìn)行插入。然后 CPU 運行紅黑樹上當(dāng)前比較的最左邊的任務(wù)。
在紅黑樹中選擇一個節(jié)點來運行的時間可以是常數(shù)時間,但是插入一個任務(wù)的時間是 O(loog(N)),其中 N 是系統(tǒng)中的任務(wù)數(shù)。考慮到當(dāng)前系統(tǒng)的負(fù)載水平,這是可以接受的。
調(diào)度器只需要考慮可運行的任務(wù)即可。這些任務(wù)被放在適當(dāng)?shù)恼{(diào)度隊列中。不可運行的任務(wù)和正在等待的各種 I/O 操作或內(nèi)核事件的任務(wù)被放入一個等待隊列中。等待隊列頭包含一個指向任務(wù)鏈表的指針和一個自旋鎖。自旋鎖對于并發(fā)處理場景下用處很大。
Linux 系統(tǒng)中的同步
下面來聊一下 Linux 中的同步機制。早期的 Linux 內(nèi)核只有一個 大內(nèi)核鎖(Big Kernel Lock,BKL) 。它阻止了不同處理器并發(fā)處理的能力。因此,需要引入一些粒度更細(xì)的鎖機制。
Linux 提供了若干不同類型的同步變量,這些變量既能夠在內(nèi)核中使用,也能夠在用戶應(yīng)用程序中使用。在地層中,Linux 通過使用 atomic_set 和 atomic_read 這樣的操作為硬件支持的原子指令提供封裝。硬件提供內(nèi)存重排序,這是 Linux 屏障的機制。
具有高級別的同步像是自旋鎖的描述是這樣的,當(dāng)兩個進(jìn)程同時對資源進(jìn)行訪問,在一個進(jìn)程獲得資源后,另一個進(jìn)程不想被阻塞,所以它就會自旋,等待一會兒再對資源進(jìn)行訪問。Linux 也提供互斥量或信號量這樣的機制,也支持像是 mutex_tryLock 和 mutex_tryWait 這樣的非阻塞調(diào)用。也支持中斷處理事務(wù),也可以通過動態(tài)禁用和啟用相應(yīng)的中斷來實現(xiàn)。
Linux 啟動
下面來聊一聊 Linux 是如何啟動的。
當(dāng)計算機電源通電后,BIOS會進(jìn)行開機自檢(Power-On-Self-Test, POST),對硬件進(jìn)行檢測和初始化。因為操作系統(tǒng)的啟動會使用到磁盤、屏幕、鍵盤、鼠標(biāo)等設(shè)備。下一步,磁盤中的第一個分區(qū),也被稱為 MBR(Master Boot Record) 主引導(dǎo)記錄,被讀入到一個固定的內(nèi)存區(qū)域并執(zhí)行。這個分區(qū)中有一個非常小的,只有 512 字節(jié)的程序。程序從磁盤中調(diào)入 boot 獨立程序,boot 程序?qū)⒆陨韽?fù)制到高位地址的內(nèi)存從而為操作系統(tǒng)釋放低位地址的內(nèi)存。
復(fù)制完成后,boot 程序讀取啟動設(shè)備的根目錄。boot 程序要理解文件系統(tǒng)和目錄格式。然后 boot 程序被調(diào)入內(nèi)核,把控制權(quán)移交給內(nèi)核。直到這里,boot 完成了它的工作。系統(tǒng)內(nèi)核開始運行。
內(nèi)核啟動代碼是使用匯編語言完成的,主要包括創(chuàng)建內(nèi)核堆棧、識別 CPU 類型、計算內(nèi)存、禁用中斷、啟動內(nèi)存管理單元等,然后調(diào)用 C 語言的 main 函數(shù)執(zhí)行操作系統(tǒng)部分。
這部分也會做很多事情,首先會分配一個消息緩沖區(qū)來存放調(diào)試出現(xiàn)的問題,調(diào)試信息會寫入緩沖區(qū)。如果調(diào)試出現(xiàn)錯誤,這些信息可以通過診斷程序調(diào)出來。
然后操作系統(tǒng)會進(jìn)行自動配置,檢測設(shè)備,加載配置文件,被檢測設(shè)備如果做出響應(yīng),就會被添加到已鏈接的設(shè)備表中,如果沒有相應(yīng),就歸為未連接直接忽略。
配置完所有硬件后,接下來要做的就是仔細(xì)手工處理進(jìn)程0,設(shè)置其堆棧,然后運行它,執(zhí)行初始化、配置時鐘、掛載文件系統(tǒng)。創(chuàng)建 init 進(jìn)程(進(jìn)程 1 ) 和 守護(hù)進(jìn)程(進(jìn)程 2)。
init 進(jìn)程會檢測它的標(biāo)志以確定它是否為單用戶還是多用戶服務(wù)。在前一種情況中,它會調(diào)用 fork 函數(shù)創(chuàng)建一個 shell 進(jìn)程,并且等待這個進(jìn)程結(jié)束。后一種情況調(diào)用 fork 函數(shù)創(chuàng)建一個運行系統(tǒng)初始化的 shell 腳本(即 /etc/rc)的進(jìn)程,這個進(jìn)程可以進(jìn)行文件系統(tǒng)一致性檢測、掛載文件系統(tǒng)、開啟守護(hù)進(jìn)程等。
然后 /etc/rc 這個進(jìn)程會從 /etc/ttys 中讀取數(shù)據(jù),/etc/ttys 列出了所有的終端和屬性。對于每一個啟用的終端,這個進(jìn)程調(diào)用 fork 函數(shù)創(chuàng)建一個自身的副本,進(jìn)行內(nèi)部處理并運行一個名為 getty 的程序。
getty 程序會在終端上輸入
login:
等待用戶輸入用戶名,在輸入用戶名后,getty 程序結(jié)束,登陸程序 /bin/login 開始運行。login 程序需要輸入密碼,并與保存在 /etc/passwd 中的密碼進(jìn)行對比,如果輸入正確,login 程序以用戶 shell 程序替換自身,等待第一個命令。如果不正確,login 程序要求輸入另一個用戶名。
整個系統(tǒng)啟動過程如下
上述內(nèi)容就是怎么解析Linux進(jìn)程,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。