成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Linux 進程必知必會

系統(tǒng) Linux
只是簡單的描述了一下 Linux 基本概念,通過幾個例子來說明 Linux 基本應(yīng)用程序,然后以 Linux 基本內(nèi)核構(gòu)造來結(jié)尾。那么本篇文章我們就深入理解一下 Linux 內(nèi)核來理解 Linux 的基本概念之進程和線程。系統(tǒng)調(diào)用是操作系統(tǒng)本身的接口,它對于創(chuàng)建進程和線程,內(nèi)存分配,共享文件和 I/O 來說都很重要。

[[333103]]

只是簡單的描述了一下 Linux 基本概念,通過幾個例子來說明 Linux 基本應(yīng)用程序,然后以 Linux 基本內(nèi)核構(gòu)造來結(jié)尾。那么本篇文章我們就深入理解一下 Linux 內(nèi)核來理解 Linux 的基本概念之進程和線程。系統(tǒng)調(diào)用是操作系統(tǒng)本身的接口,它對于創(chuàng)建進程和線程,內(nèi)存分配,共享文件和 I/O 來說都很重要。

我們將從各個版本的共性出發(fā)來進行探討。

基本概念

Linux 一個非常重要的概念就是進程,Linux 進程和我們在

寫給大忙人看的進程和線程

中探討的進程模型非常相似。每個進程都會運行一段獨立的程序,并且在初始化的時候擁有一個獨立的控制線程。換句話說,每個進程都會有一個自己的程序計數(shù)器,這個程序計數(shù)器用來記錄下一個需要被執(zhí)行的指令。Linux 允許進程在運行時創(chuàng)建額外的線程。

 

Linux 是一個多道程序設(shè)計系統(tǒng),因此系統(tǒng)中存在彼此相互獨立的進程同時運行。此外,每個用戶都會同時有幾個活動的進程。因為如果是一個大型系統(tǒng),可能有數(shù)百上千的進程在同時運行。

在某些用戶空間中,即使用戶退出登錄,仍然會有一些后臺進程在運行,這些進程被稱為 守護進程(daemon)。

Linux 中有一種特殊的守護進程被稱為 計劃守護進程(Cron daemon) ,計劃守護進程可以每分鐘醒來一次檢查是否有工作要做,做完會繼續(xù)回到睡眠狀態(tài)等待下一次喚醒。

[[333105]]

 

“ Cron 是一個守護程序,可以做任何你想做的事情,比如說你可以定期進行系統(tǒng)維護、定期進行系統(tǒng)備份等。在其他操作系統(tǒng)上也有類似的程序,比如 Mac OS X 上 Cron 守護程序被稱為 launchd 的守護進程。在 Windows 上可以被稱為 計劃任務(wù)(Task Scheduler)。

在 Linux 系統(tǒng)中,進程通過非常簡單的方式來創(chuàng)建,fork 系統(tǒng)調(diào)用會創(chuàng)建一個源進程的拷貝(副本)。調(diào)用 fork 函數(shù)的進程被稱為 父進程(parent process),使用 fork 函數(shù)創(chuàng)建出來的進程被稱為 子進程(child process)。父進程和子進程都有自己的內(nèi)存映像。如果在子進程創(chuàng)建出來后,父進程修改了一些變量等,那么子進程是看不到這些變化的,也就是 fork 后,父進程和子進程相互獨立。

雖然父進程和子進程保持相互獨立,但是它們卻能夠共享相同的文件,如果在 fork 之前,父進程已經(jīng)打開了某個文件,那么 fork 后,父進程和子進程仍然共享這個打開的文件。對共享文件的修改會對父進程和子進程同時可見。

那么該如何區(qū)分父進程和子進程呢?子進程只是父進程的拷貝,所以它們幾乎所有的情況都一樣,包括內(nèi)存映像、變量、寄存器等。區(qū)分的關(guān)鍵在于 fork 函數(shù)調(diào)用后的返回值,如果 fork 后返回一個非零值,這個非零值即是子進程的 進程標(biāo)識符(Process Identiier, PID),而會給子進程返回一個零值,可以用下面代碼來進行表示

  1. pid = fork();    // 調(diào)用 fork 函數(shù)創(chuàng)建進程 
  2. if(pid < 0){ 
  3.   error()     // pid < 0,創(chuàng)建失敗 
  4. else if(pid > 0){ 
  5.   parent_handle() // 父進程代碼 
  6. else { 
  7.   child_handle()  // 子進程代碼 

父進程在 fork 后會得到子進程的 PID,這個 PID 即能代表這個子進程的唯一標(biāo)識符也就是 PID。如果子進程想要知道自己的 PID,可以調(diào)用 getpid 方法。當(dāng)子進程結(jié)束運行時,父進程會得到子進程的 PID,因為一個進程會 fork 很多子進程,子進程也會 fork 子進程,所以 PID 是非常重要的。我們把第一次調(diào)用 fork 后的進程稱為 原始進程,一個原始進程可以生成一顆繼承樹

 

Linux 進程間通信

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

 

下面我們分別對其進行概述

信號 signal

信號是 UNIX 系統(tǒng)最先開始使用的進程間通信機制,因為 Linux 是繼承于 UNIX 的,所以 Linux 也支持信號機制,通過向一個或多個進程發(fā)送異步事件信號來實現(xiàn),信號可以從鍵盤或者訪問不存在的位置等地方產(chǎn)生;信號通過 shell 將任務(wù)發(fā)送給子進程。

你可以在 Linux 系統(tǒng)上輸入 kill -l 來列出系統(tǒng)使用的信號,下面是我提供的一些信號

 

進程可以選擇忽略發(fā)送過來的信號,但是有兩個是不能忽略的:SIGSTOP 和 SIGKILL 信號。SIGSTOP 信號會通知當(dāng)前正在運行的進程執(zhí)行關(guān)閉操作,SIGKILL 信號會通知當(dāng)前進程應(yīng)該被殺死。除此之外,進程可以選擇它想要處理的信號,進程也可以選擇阻止信號,如果不阻止,可以選擇自行處理,也可以選擇進行內(nèi)核處理。如果選擇交給內(nèi)核進行處理,那么就執(zhí)行默認(rèn)處理。

操作系統(tǒng)會中斷目標(biāo)程序的進程來向其發(fā)送信號、在任何非原子指令中,執(zhí)行都可以中斷,如果進程已經(jīng)注冊了新號處理程序,那么就執(zhí)行進程,如果沒有注冊,將采用默認(rèn)處理的方式。

例如:當(dāng)進程收到 SIGFPE 浮點異常的信號后,默認(rèn)操作是對其進行 dump(轉(zhuǎn)儲)和退出。信號沒有優(yōu)先級的說法。如果同時為某個進程產(chǎn)生了兩個信號,則可以將它們呈現(xiàn)給進程或者以任意的順序進行處理。

下面我們就來看一下這些信號是干什么用的

  • SIGABRT 和 SIGIOT

SIGABRT 和 SIGIOT 信號發(fā)送給進程,告訴其進行終止,這個 信號通常在調(diào)用 C標(biāo)準(zhǔn)庫的abort()函數(shù)時由進程本身啟動

  • SIGALRM 、 SIGVTALRM、SIGPROF

當(dāng)設(shè)置的時鐘功能超時時會將 SIGALRM 、 SIGVTALRM、SIGPROF 發(fā)送給進程。當(dāng)實際時間或時鐘時間超時時,發(fā)送 SIGALRM。當(dāng)進程使用的 CPU 時間超時時,將發(fā)送 SIGVTALRM。當(dāng)進程和系統(tǒng)代表進程使用的CPU 時間超時時,將發(fā)送 SIGPROF。

  • SIGBUS

SIGBUS 將造成總線中斷錯誤時發(fā)送給進程

  • SIGCHLD

當(dāng)子進程終止、被中斷或者被中斷恢復(fù),將 SIGCHLD 發(fā)送給進程。此信號的一種常見用法是指示操作系統(tǒng)在子進程終止后清除其使用的資源。

  • SIGCONT

SIGCONT 信號指示操作系統(tǒng)繼續(xù)執(zhí)行先前由 SIGSTOP 或 SIGTSTP 信號暫停的進程。該信號的一個重要用途是在 Unix shell 中的作業(yè)控制中。

  • SIGFPE

SIGFPE 信號在執(zhí)行錯誤的算術(shù)運算(例如除以零)時將被發(fā)送到進程。

[[333106]]

 

  • SIGUP

當(dāng) SIGUP 信號控制的終端關(guān)閉時,會發(fā)送給進程。許多守護程序?qū)⒅匦录虞d其配置文件并重新打開其日志文件,而不是在收到此信號時退出。

  • SIGILL

SIGILL 信號在嘗試執(zhí)行非法、格式錯誤、未知或者特權(quán)指令時發(fā)出

  • SIGINT

當(dāng)用戶希望中斷進程時,操作系統(tǒng)會向進程發(fā)送 SIGINT 信號。用戶輸入 ctrl - c 就是希望中斷進程。

  • SIGKILL

SIGKILL 信號發(fā)送到進程以使其馬上進行終止。與 SIGTERM 和 SIGINT 相比,這個信號無法捕獲和忽略執(zhí)行,并且進程在接收到此信號后無法執(zhí)行任何清理操作,下面是一些例外情況

僵尸進程無法殺死,因為僵尸進程已經(jīng)死了,它在等待父進程對其進行捕獲

處于阻塞狀態(tài)的進程只有再次喚醒后才會被 kill 掉

init 進程是 Linux 的初始化進程,這個進程會忽略任何信號。

SIGKILL 通常是作為最后殺死進程的信號、它通常作用于 SIGTERM 沒有響應(yīng)時發(fā)送給進程。

  • SIGPIPE

SIGPIPE 嘗試寫入進程管道時發(fā)現(xiàn)管道未連接無法寫入時發(fā)送到進程

  • SIGPOLL

當(dāng)在明確監(jiān)視的文件描述符上發(fā)生事件時,將發(fā)送 SIGPOLL 信號。

  • SIGRTMIN 至 SIGRTMAX

SIGRTMIN 至 SIGRTMAX 是實時信號

  • SIGQUIT

當(dāng)用戶請求退出進程并執(zhí)行核心轉(zhuǎn)儲時,SIGQUIT 信號將由其控制終端發(fā)送給進程。

  • SIGSEGV

當(dāng) SIGSEGV 信號做出無效的虛擬內(nèi)存引用或分段錯誤時,即在執(zhí)行分段違規(guī)時,將其發(fā)送到進程。

  • SIGSTOP

SIGSTOP 指示操作系統(tǒng)終止以便以后進行恢復(fù)時

  • SIGSYS

當(dāng) SIGSYS 信號將錯誤參數(shù)傳遞給系統(tǒng)調(diào)用時,該信號將發(fā)送到進程。

  • SYSTERM

我們上面簡單提到過了 SYSTERM 這個名詞,這個信號發(fā)送給進程以請求終止。與 SIGKILL 信號不同,該信號可以被過程捕獲或忽略。這允許進程執(zhí)行良好的終止,從而釋放資源并在適當(dāng)時保存狀態(tài)。SIGINT 與SIGTERM 幾乎相同。

  • SIGTSIP

SIGTSTP 信號由其控制終端發(fā)送到進程,以請求終端停止。

  • SIGTTIN 和 SIGTTOU

當(dāng) SIGTTIN 和SIGTTOU 信號分別在后臺嘗試從 tty 讀取或?qū)懭霑r,信號將發(fā)送到該進程。

  • SIGTRAP

在發(fā)生異常或者 trap 時,將 SIGTRAP 信號發(fā)送到進程

  • SIGURG

當(dāng)套接字具有可讀取的緊急或帶外數(shù)據(jù)時,將 SIGURG 信號發(fā)送到進程。

  • SIGUSR1 和 SIGUSR2

SIGUSR1 和 SIGUSR2 信號被發(fā)送到進程以指示用戶定義的條件。

  • SIGXCPU

當(dāng) SIGXCPU 信號耗盡 CPU 的時間超過某個用戶可設(shè)置的預(yù)定值時,將其發(fā)送到進程

  • SIGXFSZ

當(dāng) SIGXFSZ 信號增長超過最大允許大小的文件時,該信號將發(fā)送到該進程。

  • SIGWINCH

SIGWINCH 信號在其控制終端更改其大小(窗口更改)時發(fā)送給進程。

管道 pipe

Linux 系統(tǒng)中的進程可以通過建立管道 pipe 進行通信

[[333107]]

 

在兩個進程之間,可以建立一個通道,一個進程向這個通道里寫入字節(jié)流,另一個進程從這個管道中讀取字節(jié)流。管道是同步的,當(dāng)進程嘗試從空管道讀取數(shù)據(jù)時,該進程會被阻塞,直到有可用數(shù)據(jù)為止。shell 中的管線 pipelines 就是用管道實現(xiàn)的,當(dāng) shell 發(fā)現(xiàn)輸出

  1. sort <f | head 

它會創(chuàng)建兩個進程,一個是 sort,一個是 head,sort,會在這兩個應(yīng)用程序之間建立一個管道使得 sort 進程的標(biāo)準(zhǔn)輸出作為 head 程序的標(biāo)準(zhǔn)輸入。sort 進程產(chǎn)生的輸出就不用寫到文件中了,如果管道滿了系統(tǒng)會停止 sort 以等待 head 讀出數(shù)據(jù)

 

管道實際上就是 |,兩個應(yīng)用程序不知道有管道的存在,一切都是由 shell 管理和控制的。

共享內(nèi)存 shared memory

兩個進程之間還可以通過共享內(nèi)存進行進程間通信,其中兩個或者多個進程可以訪問公共內(nèi)存空間。兩個進程的共享工作是通過共享內(nèi)存完成的,一個進程所作的修改可以對另一個進程可見(很像線程間的通信)。

 

在使用共享內(nèi)存前,需要經(jīng)過一系列的調(diào)用流程,流程如下

  • 創(chuàng)建共享內(nèi)存段或者使用已創(chuàng)建的共享內(nèi)存段(shmget())
  • 將進程附加到已經(jīng)創(chuàng)建的內(nèi)存段中(shmat())
  • 從已連接的共享內(nèi)存段分離進程(shmdt())
  • 對共享內(nèi)存段執(zhí)行控制操作(shmctl())

先入先出隊列 FIFO

先入先出隊列 FIFO 通常被稱為 命名管道(Named Pipes),命名管道的工作方式與常規(guī)管道非常相似,但是確實有一些明顯的區(qū)別。未命名的管道沒有備份文件:操作系統(tǒng)負(fù)責(zé)維護內(nèi)存中的緩沖區(qū),用來將字節(jié)從寫入器傳輸?shù)阶x取器。一旦寫入或者輸出終止的話,緩沖區(qū)將被回收,傳輸?shù)臄?shù)據(jù)會丟失。相比之下,命名管道具有支持文件和獨特 API ,命名管道在文件系統(tǒng)中作為設(shè)備的專用文件存在。當(dāng)所有的進程通信完成后,命名管道將保留在文件系統(tǒng)中以備后用。命名管道具有嚴(yán)格的 FIFO 行為

 

寫入的第一個字節(jié)是讀取的第一個字節(jié),寫入的第二個字節(jié)是讀取的第二個字節(jié),依此類推。

消息隊列 Message Queue

一聽到消息隊列這個名詞你可能不知道是什么意思,消息隊列是用來描述內(nèi)核尋址空間內(nèi)的內(nèi)部鏈接列表。可以按幾種不同的方式將消息按順序發(fā)送到隊列并從隊列中檢索消息。每個消息隊列由 IPC 標(biāo)識符唯一標(biāo)識。消息隊列有兩種模式,一種是嚴(yán)格模式, 嚴(yán)格模式就像是 FIFO 先入先出隊列似的,消息順序發(fā)送,順序讀取。還有一種模式是 非嚴(yán)格模式,消息的順序性不是非常重要。

套接字 Socket

還有一種管理兩個進程間通信的是使用 socket,socket 提供端到端的雙相通信。一個套接字可以與一個或多個進程關(guān)聯(liá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 中進程管理系統(tǒng)調(diào)用

現(xiàn)在關(guān)注一下 Linux 系統(tǒng)中與進程管理相關(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)用指令有很多,下面是一些與進程管理相關(guān)的最主要的系統(tǒng)調(diào)用

fork

fork 調(diào)用用于創(chuàng)建一個與父進程相同的子進程,創(chuàng)建完進程后的子進程擁有和父進程一樣的程序計數(shù)器、相同的 CPU 寄存器、相同的打開文件。

exec

exec 系統(tǒng)調(diào)用用于執(zhí)行駐留在活動進程中的文件,調(diào)用 exec 后,新的可執(zhí)行文件會替換先前的可執(zhí)行文件并獲得執(zhí)行。也就是說,調(diào)用 exec 后,會將舊文件或程序替換為新文件或執(zhí)行,然后執(zhí)行文件或程序。新的執(zhí)行程序被加載到相同的執(zhí)行空間中,因此進程的 PID不會修改,因為我們沒有創(chuàng)建新進程,只是替換舊進程。但是進程的數(shù)據(jù)、代碼、堆棧都已經(jīng)被修改。如果當(dāng)前要被替換的進程包含多個線程,那么所有的線程將被終止,新的進程映像被加載執(zhí)行。

這里需要解釋一下進程映像(Process image) 的概念

什么是進程映像呢?進程映像是執(zhí)行程序時所需要的可執(zhí)行文件,通常會包括下面這些東西

  • 代碼段(codesegment/textsegment)

又稱文本段,用來存放指令,運行代碼的一塊內(nèi)存空間

此空間大小在代碼運行前就已經(jīng)確定

內(nèi)存空間一般屬于只讀,某些架構(gòu)的代碼也允許可寫

在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。

  • 數(shù)據(jù)段(datasegment)

可讀可寫

存儲初始化的全局變量和初始化的 static 變量

數(shù)據(jù)段中數(shù)據(jù)的生存期是隨程序持續(xù)性(隨進程持續(xù)性) 隨進程持續(xù)性:進程創(chuàng)建就存在,進程死亡就消失

  • bss 段(bsssegment):

可讀可寫

存儲未初始化的全局變量和未初始化的 static 變量

bss 段中的數(shù)據(jù)一般默認(rèn)為 0

  • Data 段

是可讀寫的,因為變量的值可以在運行時更改。此段的大小也固定。

  • 棧(stack):

可讀可寫

存儲的是函數(shù)或代碼中的局部變量(非 static 變量)

棧的生存期隨代碼塊持續(xù)性,代碼塊運行就給你分配空間,代碼塊結(jié)束,就自動回收空間

  • 堆(heap):

可讀可寫

存儲的是程序運行期間動態(tài)分配的 malloc/realloc 的空間

堆的生存期隨進程持續(xù)性,從 malloc/realloc 到 free 一直存在

下面是這些區(qū)域的構(gòu)成圖

 

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

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

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

  1. 當(dāng)前進程映像被替換為新的進程映像
  2. 新的進程映像是你做為 exec 傳遞的燦睡
  3. 結(jié)束當(dāng)前正在運行的進程
  4. 新的進程映像有 PID,相同的環(huán)境和一些文件描述符(因為未替換進程,只是替換了進程映像)
  5. CPU 狀態(tài)和虛擬內(nèi)存受到影響,當(dāng)前進程映像的虛擬內(nèi)存映射被新進程映像的虛擬內(nèi)存代替。

waitpid

等待子進程結(jié)束或終止

exit

在許多計算機操作系統(tǒng)上,計算機進程的終止是通過執(zhí)行 exit 系統(tǒng)調(diào)用命令執(zhí)行的。0 表示進程能夠正常結(jié)束,其他值表示進程以非正常的行為結(jié)束。

其他一些常見的系統(tǒng)調(diào)用如下

 

系統(tǒng)調(diào)用指令 描述
pause 掛起信號
nice 改變分時進程的優(yōu)先級
ptrace 進程跟蹤
kill 向進程發(fā)送信號
pipe 創(chuàng)建管道
mkfifo 創(chuàng)建 fifo 的特殊文件(命名管道)
sigaction 設(shè)置對指定信號的處理方法
msgctl 消息控制操作
semctl 信號量控制

Linux 進程和線程的實現(xiàn)

Linux 進程

Linux 進程就像一座冰山,你看到的只是冰山一角。

在 Linux 內(nèi)核結(jié)構(gòu)中,進程會被表示為 任務(wù),通過結(jié)構(gòu)體 structure 來創(chuàng)建。不像其他的操作系統(tǒng)會區(qū)分進程、輕量級進程和線程,Linux 統(tǒng)一使用任務(wù)結(jié)構(gòu)來代表執(zhí)行上下文。因此,對于每個單線程進程來說,單線程進程將用一個任務(wù)結(jié)構(gòu)表示,對于多線程進程來說,將為每一個用戶級線程分配一個任務(wù)結(jié)構(gòu)。Linux 內(nèi)核是多線程的,并且內(nèi)核級線程不與任何用戶級線程相關(guān)聯(lián)。

對于每個進程來說,在內(nèi)存中都會有一個 task_struct 進程描述符與之對應(yīng)。進程描述符包含了內(nèi)核管理進程所有有用的信息,包括 調(diào)度參數(shù)、打開文件描述符等等。進程描述符從進程創(chuàng)建開始就一直存在于內(nèi)核堆棧中。

Linux 和 Unix 一樣,都是通過 PID 來區(qū)分不同的進程,內(nèi)核會將所有進程的任務(wù)結(jié)構(gòu)組成為一個雙向鏈表。PID 能夠直接被映射稱為進程的任務(wù)結(jié)構(gòu)所在的地址,從而不需要遍歷雙向鏈表直接訪問。

我們上面提到了進程描述符,這是一個非常重要的概念,我們上面還提到了進程描述符是位于內(nèi)存中的,這里我們省略了一句話,那就是進程描述符是存在用戶的任務(wù)結(jié)構(gòu)中,當(dāng)進程位于內(nèi)存并開始運行時,進程描述符才會被調(diào)入內(nèi)存。

“ 進程位于內(nèi)存被稱為 PIM(Process In Memory) ,這是馮諾伊曼體系架構(gòu)的一種體現(xiàn),加載到內(nèi)存中并執(zhí)行的程序稱為進程。簡單來說,一個進程就是正在執(zhí)行的程序。

進程描述符可以歸為下面這幾類

  • 調(diào)度參數(shù)(scheduling parameters):進程優(yōu)先級、最近消耗 CPU 的時間、最近睡眠時間一起決定了下一個需要運行的進程
  • 內(nèi)存映像(memory image):我們上面說到,進程映像是執(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):記錄用戶、進程占用系統(tǒng) CPU 時間表的指針,一些操作系統(tǒng)還保存進程最多占用的 CPU 時間、進程擁有的最大堆棧空間、進程可以消耗的頁面數(shù)等。
  • 內(nèi)核堆棧(kernel stack):進程的內(nèi)核部分可以使用的固定堆棧
  • 其他:當(dāng)前進程狀態(tài)、事件等待時間、距離警報的超時時間、PID、父進程的 PID 以及用戶標(biāo)識符等

有了上面這些信息,現(xiàn)在就很容易描述在 Linux 中是如何創(chuàng)建這些進程的了,創(chuàng)建新流程實際上非常簡單。為子進程開辟一塊新的用戶空間的進程描述符,然后從父進程復(fù)制大量的內(nèi)容。為這個子進程分配一個 PID,設(shè)置其內(nèi)存映射,賦予它訪問父進程文件的權(quán)限,注冊并啟動。

當(dāng)執(zhí)行 fork 系統(tǒng)調(diào)用時,調(diào)用進程會陷入內(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)中包含進程描述符,進程描述符位于固定的位置,使得 Linux 系統(tǒng)只需要很小的開銷就可以定位到一個運行中進程的數(shù)據(jù)結(jié)構(gòu)。

進程描述符的主要內(nèi)容是根據(jù)父進程的描述符來填充。Linux 操作系統(tǒng)會尋找一個可用的 PID,并且此 PID 沒有被任何進程使用,更新進程標(biāo)示符使其指向一個新的數(shù)據(jù)結(jié)構(gòu)即可。為了減少 hash table 的碰撞,進程描述符會形成鏈表。它還將 task_struct 的字段設(shè)置為指向任務(wù)數(shù)組上相應(yīng)的上一個/下一個進程。

“ task_struct :Linux 進程描述符,內(nèi)部涉及到眾多 C++ 源碼,我們會在后面進行講解。

從原則上來說,為子進程開辟內(nèi)存區(qū)域并為子進程分配數(shù)據(jù)段、堆棧段,并且對父進程的內(nèi)容進行復(fù)制,但是實際上 fork 完成后,子進程和父進程沒有共享內(nèi)存,所以需要復(fù)制技術(shù)來實現(xiàn)同步,但是復(fù)制開銷比較大,因此 Linux 操作系統(tǒng)使用了一種 欺騙 方式。即為子進程分配頁表,然后新分配的頁表指向父進程的頁面,同時這些頁面是只讀的。當(dāng)進程向這些頁面進行寫入的時候,會開啟保護錯誤。內(nèi)核發(fā)現(xiàn)寫入操作后,會為進程分配一個副本,使得寫入時把數(shù)據(jù)復(fù)制到這個副本上,這個副本是共享的,這種方式稱為 寫入時復(fù)制(copy on write),這種方式避免了在同一塊內(nèi)存區(qū)域維護兩個副本的必要,節(jié)省內(nèi)存空間。

在子進程開始運行后,操作系統(tǒng)會調(diào)用 exec 系統(tǒng)調(diào)用,內(nèi)核會進行查找驗證可執(zhí)行文件,把參數(shù)和環(huán)境變量復(fù)制到內(nèi)核,釋放舊的地址空間。

現(xiàn)在新的地址空間需要被創(chuàng)建和填充。如果系統(tǒng)支持映射文件,就像 Unix 系統(tǒng)一樣,那么新的頁表就會創(chuàng)建,表明內(nèi)存中沒有任何頁,除非所使用的頁面是堆棧頁,其地址空間由磁盤上的可執(zhí)行文件支持。新進程開始運行時,立刻會收到一個缺頁異常(page fault),這會使具有代碼的頁面加載進入內(nèi)存。最后,參數(shù)和環(huán)境變量被復(fù)制到新的堆棧中,重置信號,寄存器全部清零。新的命令開始運行。

下面是一個示例,用戶輸出 ls,shell 會調(diào)用 fork 函數(shù)復(fù)制一個新進程,shell 進程會調(diào)用 exec 函數(shù)用可執(zhí)行文件 ls 的內(nèi)容覆蓋它的內(nèi)存。

 

Linux 線程

現(xiàn)在我們來討論一下 Linux 中的線程,線程是輕量級的進程,想必這句話你已經(jīng)聽過很多次了,輕量級體現(xiàn)在所有的進程切換都需要清除所有的表、進程間的共享信息也比較麻煩,一般來說通過管道或者共享內(nèi)存,如果是 fork 函數(shù)后的父子進程則使用共享文件,然而線程切換不需要像進程一樣具有昂貴的開銷,而且線程通信起來也更方便。線程分為兩種:用戶級線程和內(nèi)核級線程

用戶級線程

用戶級線程避免使用內(nèi)核,通常,每個線程會顯示調(diào)用開關(guān),發(fā)送信號或者執(zhí)行某種切換操作來放棄 CPU,同樣,計時器可以強制進行開關(guān),用戶線程的切換速度通常比內(nèi)核線程快很多。在用戶級別實現(xiàn)線程會有一個問題,即單個線程可能會壟斷 CPU 時間片,導(dǎo)致其他線程無法執(zhí)行從而 餓死。如果執(zhí)行一個 I/O 操作,那么 I/O 會阻塞,其他線程也無法運行。

 

一種解決方案是,一些用戶級的線程包解決了這個問題。可以使用時鐘周期的監(jiān)視器來控制第一時間時間片獨占。然后,一些庫通過特殊的包裝來解決系統(tǒng)調(diào)用的 I/O 阻塞問題,或者可以為非阻塞 I/O 編寫任務(wù)。

內(nèi)核級線程

內(nèi)核級線程通常使用幾個進程表在內(nèi)核中實現(xiàn),每個任務(wù)都會對應(yīng)一個進程表。在這種情況下,內(nèi)核會在每個進程的時間片內(nèi)調(diào)度每個線程。

 

所有能夠阻塞的調(diào)用都會通過系統(tǒng)調(diào)用的方式來實現(xiàn),當(dāng)一個線程阻塞時,內(nèi)核可以進行選擇,是運行在同一個進程中的另一個線程(如果有就緒線程的話)還是運行一個另一個進程中的線程。

從用戶空間 -> 內(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)核級線程,并對其進行調(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)是基于線程的,而不是基于進程的。

為了進行調(diào)度,Linux 系統(tǒng)將線程分為三類

  • 實時先入先出
  • 實時輪詢
  • 分時

實時先入先出線程具有最高優(yōu)先級,它不會被其他線程所搶占,除非那是一個剛剛準(zhǔn)備好的,擁有更高優(yōu)先級的線程進入。實時輪轉(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。

下面我們更詳細的討論一下 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)先級進程往往能得到較長的時間片,低優(yōu)先級的任務(wù)得到較少的時間片。

這種方式為了保證能夠更好的提供服務(wù),通常會為 交互式進程 賦予較高的優(yōu)先級,交互式進程就是用戶進程。

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)維護一個名為 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ù)時性能很糟糕。

為了改進這個缺點,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ù)值進行對比,如果此任務(wù)具有最小時間值,那么它將進行運行,否則它會進行比較,找到合適的位置進行插入。然后 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ā)處理的能力。因此,需要引入一些粒度更細的鎖機制。

Linux 提供了若干不同類型的同步變量,這些變量既能夠在內(nèi)核中使用,也能夠在用戶應(yīng)用程序中使用。在地層中,Linux 通過使用 atomic_set 和 atomic_read 這樣的操作為硬件支持的原子指令提供封裝。硬件提供內(nèi)存重排序,這是 Linux 屏障的機制。

具有高級別的同步像是自旋鎖的描述是這樣的,當(dāng)兩個進程同時對資源進行訪問,在一個進程獲得資源后,另一個進程不想被阻塞,所以它就會自旋,等待一會兒再對資源進行訪問。Linux 也提供互斥量或信號量這樣的機制,也支持像是 mutex_tryLock 和 mutex_tryWait 這樣的非阻塞調(diào)用。也支持中斷處理事務(wù),也可以通過動態(tài)禁用和啟用相應(yīng)的中斷來實現(xiàn)。

Linux 啟動

下面來聊一聊 Linux 是如何啟動的。

當(dāng)計算機電源通電后,BIOS會進行開機自檢(Power-On-Self-Test, POST),對硬件進行檢測和初始化。因為操作系統(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)會進行自動配置,檢測設(shè)備,加載配置文件,被檢測設(shè)備如果做出響應(yīng),就會被添加到已鏈接的設(shè)備表中,如果沒有相應(yīng),就歸為未連接直接忽略。

配置完所有硬件后,接下來要做的就是仔細手工處理進程0,設(shè)置其堆棧,然后運行它,執(zhí)行初始化、配置時鐘、掛載文件系統(tǒng)。創(chuàng)建 init 進程(進程 1 ) 和 守護進程(進程 2)。

init 進程會檢測它的標(biāo)志以確定它是否為單用戶還是多用戶服務(wù)。在前一種情況中,它會調(diào)用 fork 函數(shù)創(chuàng)建一個 shell 進程,并且等待這個進程結(jié)束。后一種情況調(diào)用 fork 函數(shù)創(chuàng)建一個運行系統(tǒng)初始化的 shell 腳本(即 /etc/rc)的進程,這個進程可以進行文件系統(tǒng)一致性檢測、掛載文件系統(tǒng)、開啟守護進程等。

然后 /etc/rc 這個進程會從 /etc/ttys 中讀取數(shù)據(jù),/etc/ttys 列出了所有的終端和屬性。對于每一個啟用的終端,這個進程調(diào)用 fork 函數(shù)創(chuàng)建一個自身的副本,進行內(nèi)部處理并運行一個名為 getty 的程序。

getty 程序會在終端上輸入

login:

等待用戶輸入用戶名,在輸入用戶名后,getty 程序結(jié)束,登陸程序 /bin/login 開始運行。login 程序需要輸入密碼,并與保存在 /etc/passwd 中的密碼進行對比,如果輸入正確,login 程序以用戶 shell 程序替換自身,等待第一個命令。如果不正確,login 程序要求輸入另一個用戶名。

整個系統(tǒng)啟動過程如下

本文轉(zhuǎn)載自微信公眾號「程序員cxuan」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系程序員cxuan公眾號。

 

責(zé)任編輯:武曉燕 來源: 程序員cxuan
相關(guān)推薦

2024-11-15 11:11:48

2019-01-30 14:14:16

LinuxUNIX操作系統(tǒng)

2023-12-26 12:10:13

2024-01-03 07:56:50

2022-05-18 09:01:19

JSONJavaScript

2022-08-19 10:31:32

Kafka大數(shù)據(jù)

2022-09-28 08:40:04

殺死一個終端進程

2015-10-20 09:46:33

HTTP網(wǎng)絡(luò)協(xié)議

2018-10-26 14:10:21

2023-04-20 14:31:20

Python開發(fā)教程

2024-06-13 09:10:22

2015-08-17 16:05:35

javascript對象編程

2024-01-09 13:58:22

PandasPython數(shù)據(jù)分析

2023-05-08 15:25:19

Python編程語言編碼技巧

2022-03-21 09:52:44

LinuxSystemd日志

2023-09-12 11:25:15

2022-08-26 14:46:31

機器學(xué)習(xí)算法線性回歸

2024-07-26 08:32:44

panic?Go語言

2019-11-06 10:56:59

Python數(shù)據(jù)分析TGI

2020-08-23 18:18:27

Python列表數(shù)據(jù)結(jié)構(gòu)
點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 国产精品免费视频一区 | 91原创视频在线观看 | 日韩欧美国产精品一区二区 | 国内精品免费久久久久软件老师 | 久在线 | 国产视频一区二区三区四区五区 | 成人小视频在线观看 | 三级欧美| 精品国产乱码久久久久久蜜柚 | 一级毛片在线看 | 国产福利视频 | 久草视频观看 | 欧美中文字幕 | 一区二区在线看 | 91久久精品视频 | 亚洲免费三区 | 欧美国产精品一区二区三区 | 精品自拍视频在线观看 | 成人国产精品一级毛片视频毛片 | 黄色播放| 日日天天| 免费a网站| 岛国av一区二区 | 男人的天堂在线视频 | 夜夜操av| 黄色a三级| 老司机67194精品线观看 | 色噜噜狠狠色综合中国 | 国产日韩一区二区三区 | 亚洲 欧美 综合 | 精品视频网| 欧美亚洲日本 | 在线国产一区 | 精品国产一区二区三区四区在线 | 日韩国产在线 | 中文字幕一区二区三区四区五区 | 国产精品福利在线 | 久久精品免费 | 一区二区三区视频 | 91一区二区三区在线观看 | 97精品国产一区二区三区 |