當(dāng)前位置:首頁 > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]只是簡(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)程和線程,


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)程和線程

中探討的進(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 進(jìn)程必知必會(huì)

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)等待下一次喚醒。

Linux 進(jìn)程必知必會(huì)

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)程必知必會(huì)

Linux 進(jìn)程間通信

Linux 進(jìn)程間的通信機(jī)制通常被稱為 Internel-Process communication,IPC下面我們來說一說 Linux 進(jìn)程間通信的機(jī)制,大致來說,Linux 進(jìn)程間的通信機(jī)制可以分為 6 種

Linux 進(jìn)程必知必會(huì)

下面我們分別對(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)

Linux 進(jìn)程必知必會(huì)

進(jìn)程可以選擇忽略發(fā)送過來的信號(hào),但是有兩個(gè)是不能忽略的:SIGSTOPSIGKILL 信號(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)程。

Linux 進(jìn)程必知必會(huì)

  • 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)行通信

Linux 進(jìn)程必知必會(huì)

在兩個(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ù)

Linux 進(jìn)程必知必會(huì)

管道實(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)程可見(很像線程間的通信)。

Linux 進(jìn)程必知必會(huì)

在使用共享內(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 行為

Linux 進(jìn)程必知必會(huì)

寫入的第一個(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)成圖

Linux 進(jìn)程必知必會(huì)

exec 系統(tǒng)調(diào)用是一些函數(shù)的集合,這些函數(shù)是

  • execl
  • execle
  • execlp
  • execv
  • execve
  • execvp

下面來看一下 exec 的工作原理

  1. 當(dāng)前進(jìn)程映像被替換為新的進(jìn)程映像
  2. 新的進(jìn)程映像是你做為 exec 傳遞的參數(shù)
  3. 結(jié)束當(dāng)前正在運(yùn)行的進(jìn)程
  4. 新的進(jìn)程映像有 PID,相同的環(huán)境和一些文件描述符(因?yàn)槲刺鎿Q進(jìn)程,只是替換了進(jìn)程映像)
  5. 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 進(jìn)程必知必會(huì)

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)行。

Linux 進(jìn)程必知必會(huì)

一種解決方案是,一些用戶級(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è)線程。

Linux 進(jìn)程必知必會(huì)

所有能夠阻塞的調(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ù)用起來

Linux 進(jìn)程必知必會(huì)

在這種模型中,編程人員可以自由控制用戶線程和內(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í)。

Linux 進(jìn)程必知必會(huì)

大致流程如下:

調(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)造模型

Linux 進(jìn)程必知必會(huì)

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_setatomic_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_tryLockmutex_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)過程如下

Linux 進(jìn)程必知必會(huì)


特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:

Linux 進(jìn)程必知必會(huì)

Linux 進(jìn)程必知必會(huì)

長(zhǎng)按訂閱更多精彩▼

Linux 進(jìn)程必知必會(huì)

如有收獲,點(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)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉