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