久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

Linux操作系統全面知識點有哪些

272次閱讀
沒有評論

共計 21761 個字符,預計需要花費 55 分鐘才能閱讀完成。

本篇內容主要講解“Linux 操作系統全面知識點有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓丸趣 TV 小編來帶大家學習“Linux 操作系統全面知識點有哪些”吧!

Linux 簡介

UNIX   是一個交互式系統,用于同時處理多進程和多用戶同時在線。為什么要說 UNIX,那是因為 Linux 是由 UNIX 發展而來的,UNIX   是由程序員設計,它的主要服務對象也是程序員。Linux 繼承了 UNIX   的設計目標。從智能手機到汽車,超級計算機和家用電器,從家用臺式機到企業服務器,Linux 操作系統無處不在。

大多數程序員都喜歡讓系統盡量簡單,優雅并具有一致性。舉個例子,從最底層的角度來講,一個文件應該只是一個字節集合。為了實現順序存取、隨機存取、按鍵存取、遠程存取只能是妨礙你的工作。相同的,如果命令

ls A*

意味著只列出以 A 為開頭的所有文件,那么命令

rm A*

應該會移除所有以 A 為開頭的文件而不是只刪除文件名是 A* 的文件。這個特性也是最小吃驚原則(principle of least surprise)

最小吃驚原則一半常用于用戶界面和軟件設計。它的原型是:該功能或者特征應該符合用戶的預期,不應該使用戶感到驚訝和震驚。

一些有經驗的程序員通常希望系統具有較強的功能性和靈活性。設計 Linux 的一個基本目標是每個應用程序只做一件事情并把他做好。所以編譯器只負責編譯的工作,編譯器不會產生列表,因為有其他應用比編譯器做的更好。

很多人都不喜歡冗余,為什么在 cp 就能描述清楚你想干什么時候還使用 copy?這完全是在浪費寶貴的 hacking time。為了從文件中提取所有包含字符串 ard 的行,Linux 程序員應該輸入

grep ard f

Linux 接口

Linux 系統是一種金字塔模型的系統,如下所示

應用程序發起系統調用把參數放在寄存器中(有時候放在棧中),并發出 trap   系統陷入指令切換用戶態至內核態。因為不能直接在 C 中編寫 trap 指令,因此 C   提供了一個庫,庫中的函數對應著系統調用。有些函數是使用匯編編寫的,但是能夠從 C   中調用。每個函數首先把參數放在合適的位置然后執行系統調用指令。因此如果你想要執行 read 系統調用的話,C 程序會調用 read   函數庫來執行。這里順便提一下,是由 POSIX 指定的庫接口而不是系統調用接口。也就是說,POSIX   會告訴一個標準系統應該提供哪些庫過程,它們的參數是什么,它們必須做什么以及它們必須返回什么結果。

除了操作系統和系統調用庫外,Linux 操作系統還要提供一些標準程序,比如文本編輯器、編譯器、文件操作工具等。直接和用戶打交道的是上面這些應用程序。因此我們可以說 Linux 具有三種不同的接口:系統調用接口、庫函數接口和應用程序接口

Linux 中的 GUI(Graphical User Interface) 和 UNIX 中的非常相似,這種 GUI 創建一個桌面環境,包括窗口、目標和文件夾、工具欄和文件拖拽功能。一個完整的 GUI 還包括窗口管理器以及各種應用程序。

Linux 上的 GUI 由 X 窗口支持,主要組成部分是 X 服務器、控制鍵盤、鼠標、顯示器等。當在 Linux 上使用圖形界面時,用戶可以通過鼠標點擊運行程序或者打開文件,通過拖拽將文件進行復制等。

Linux 組成部分

事實上,Linux 操作系統可以由下面這幾部分構成

引導程序(Bootloader):引導程序是管理計算機啟動過程的軟件,對于大多數用戶而言,只是彈出一個屏幕,但其實內部操作系統做了很多事情

內核(Kernel):內核是操作系統的核心,負責管理 CPU、內存和外圍設備等。

初始化系統(Init System):這是一個引導用戶空間并負責控制守護程序的子系統。一旦從引導加載程序移交了初始引導,它就是用于管理引導過程的初始化系統。

后臺進程(Daemon):后臺進程顧名思義就是在后臺運行的程序,比如打印、聲音、調度等,它們可以在引導過程中啟動,也可以在登錄桌面后啟動

圖形服務器(Graphical server):這是在監視器上顯示圖形的子系統。通常將其稱為 X 服務器或 X。

桌面環境(Desktop environment):這是用戶與之實際交互的部分,有很多桌面環境可供選擇,每個桌面環境都包含內置應用程序,比如文件管理器、Web 瀏覽器、游戲等

應用程序(Applications):桌面環境不提供完整的應用程序,就像 Windows 和 macOS 一樣,Linux 提供了成千上萬個可以輕松找到并安裝的高質量軟件。

Shell

盡管 Linux 應用程序提供了 GUI,但是大部分程序員仍偏好于使用命令行(command-line interface),稱為 shell。用戶通常在 GUI 中啟動一個 shell 窗口然后就在 shell 窗口下進行工作。

shell 命令行使用速度快、功能更強大、而且易于擴展、并且不會帶來肢體重復性勞損(RSI)。

下面會介紹一些最簡單的 bash shell。當 shell 啟動時,它首先進行初始化,在屏幕上輸出一個 提示符(prompt),通常是一個百分號或者美元符號,等待用戶輸入

等用戶輸入一個命令后,shell   提取其中的第一個詞,這里的詞指的是被空格或制表符分隔開的一連串字符。假定這個詞是將要運行程序的程序名,那么就會搜索這個程序,如果找到了這個程序就會運行它。然后  shell 會將自己掛起直到程序運行完畢,之后再嘗試讀入下一條指令。shell   也是一個普通的用戶程序。它的主要功能就是讀取用戶的輸入和顯示計算的輸出。shell 命令中可以包含參數,它們作為字符串傳遞給所調用的程序。比如

cp src dest

會調用 cp 應用程序并包含兩個參數 src 和 dest。這個程序會解釋第一個參數是一個已經存在的文件名,然后創建一個該文件的副本,名稱為 dest。

并不是所有的參數都是文件名,比如下面

head -20 file

第一個參數 -20,會告訴 head 應用程序打印文件的前 20 行,而不是默認的 10 行。控制命令操作或者指定可選值的參數稱為標志(flag),按照慣例標志應該使用 – 來表示。這個符號是必要的,比如

head 20 file

是一個完全合法的命令,它會告訴 head 程序輸出文件名為 20 的文件的前 10 行,然后輸出文件名為 file 文件的前 10 行。Linux 操作系統可以接受一個或多個參數。

為了更容易的指定多個文件名,shell 支持 魔法字符(magic character),也被稱為通配符(wild cards)。比如,* 可以匹配一個或者多個可能的字符串

ls *.c

告訴 ls 列舉出所有文件名以 .c 結束的文件。如果同時存在多個文件,則會在后面進行并列。

另一個通配符是問號,負責匹配任意一個字符。一組在中括號中的字符可以表示其中任意一個,因此

ls [abc]*

會列舉出所有以 a、b 或者 c 開頭的文件。

shell 應用程序不一定通過終端進行輸入和輸出。shell 啟動時,就會獲取 標準輸入、標準輸出、標準錯誤文件進行訪問的能力。

標準輸出是從鍵盤輸入的,標準輸出或者標準錯誤是輸出到顯示器的。許多 Linux 程序默認是從標準輸入進行輸入并從標準輸出進行輸出。比如

sort

會調用 sort 程序,會從終端讀取數據(直到用戶輸入 ctrl-d 結束),根據字母順序進行排序,然后將結果輸出到屏幕上。

通常還可以重定向標準輸入和標準輸出,重定向標準輸入使用 后面跟文件名。標準輸出可以通過一個大于號 進行重定向。允許一個命令中重定向標準輸入和輸出。例如命令

sort  in  out

會使 sort 從文件 in 中得到輸入,并把結果輸出到 out 文件中。由于標準錯誤沒有重定向,所以錯誤信息會直接打印到屏幕上。從標準輸入讀入,對其進行處理并將其寫入到標準輸出的程序稱為 過濾器。

考慮下面由三個分開的命令組成的指令

sort  in  temp;head -30  temp;rm temp

首先會調用 sort 應用程序,從標準輸入 in 中進行讀取,并通過標準輸出到 temp。當程序運行完畢后,shell 會運行 head,告訴它打印前 30 行,并在標準輸出 (默認為終端) 上打印。最后,temp 臨時文件被刪除。輕輕的,你走了,你揮一揮衣袖,不帶走一片云彩。

命令行中的第一個程序通常會產生輸出,在上面的例子中,產生的輸出都不 temp 文件接收。然而,Linux 還提供了一個簡單的命令來做這件事,例如下面

sort  in | head -30

上面 | 稱為豎線符號,它的意思是從 sort 應用程序產生的排序輸出會直接作為輸入顯示,無需創建、使用和移除臨時文件。由管道符號連接的命令集合稱為管道(pipeline)。例如如下

grep cxuan *.c | sort | head -30 | tail -5  f00

對任意以 .t 結尾的文件中包含 cxuan 的行被寫到標準輸出中,然后進行排序。這些內容中的前 30 行被 head 出來并傳給 tail,它又將最后 5 行傳遞給 foo。這個例子提供了一個管道將多個命令連接起來。

可以把一系列 shell 命令放在一個文件中,然后將此文件作為輸入來運行。shell 會按照順序對他們進行處理,就像在鍵盤上鍵入命令一樣。包含 shell 命令的文件被稱為 shell 腳本(shell scripts)。

推薦一個 shell 命令的學習網站:https://www.shellscript.sh/

shell 腳本其實也是一段程序,shell 腳本中可以對變量進行賦值,也包含循環控制語句比如 if、for、while 等,shell 的設計目標是讓其看起來和 C 相似(There is no doubt that C is father)。由于 shell 也是一個用戶程序,所以用戶可以選擇不同的 shell。

Linux 應用程序

Linux 的命令行也就是 shell,它由大量標準應用程序組成。這些應用程序主要有下面六種

文件和目錄操作命令

過濾器

文本程序

系統管理

程序開發工具,例如編輯器和編譯器

其他

除了這些標準應用程序外,還有其他應用程序比如 Web 瀏覽器、多媒體播放器、圖片瀏覽器、辦公軟件和游戲程序等。

我們在上面的例子中已經見過了幾個 Linux 的應用程序,比如 sort、cp、ls、head,下面我們再來認識一下其他 Linux 的應用程序。

我們先從幾個例子開始講起,比如

cp a b

是將 a 復制一個副本為 b,而

mv a b

是將 a 移動到 b,但是刪除原文件。

上面這兩個命令有一些區別,cp 是將文件進行復制,復制完成后會有兩個文件 a 和 b;而 mv 相當于是文件的移動,移動完成后就不再有 a 文件。cat 命令可以把多個文件內容進行連接。使用 rm 可以刪除文件;使用 chmod 可以允許所有者改變訪問權限;文件目錄的的創建和刪除可以使用 mkdir 和 rmdir 命令;使用 ls 可以查看目錄文件,ls 可以顯示很多屬性,比如大小、用戶、創建日期等;sort 決定文件的顯示順序

Linux 應用程序還包括過濾器 grep,grep 從標準輸入或者一個或多個輸入文件中提取特定模式的行;sort 將輸入進行排序并輸出到標準輸出;head 提取輸入的前幾行;tail 提取輸入的后面幾行;除此之外的過濾器還有 cut 和 paste,允許對文本行的剪切和復制;od 將輸入轉換為 ASCII;tr 實現字符大小寫轉換;pr 為格式化打印輸出等。

程序編譯工具使用 gcc;

make 命令用于自動編譯,這是一個很強大的命令,它用于維護一個大的程序,往往這類程序的源碼由許多文件構成。典型的,有一些是 header files 頭文件,源文件通常使用 include 指令包含這些文件,make 的作用就是跟蹤哪些文件屬于頭文件,然后安排自動編譯的過程。

下面列出了 POSIX 的標準應用程序

程序應用 ls 列出目錄 cp 復制文件 head 顯示文件的前幾行 make 編譯文件生成二進制文件 cd 切換目錄 mkdir 創建目錄 chmod 修改文件訪問權限 ps 列出文件進程 pr 格式化打印 rm 刪除一個文件 rmdir 刪除文件目錄 tail 提取文件最后幾行 tr 字符集轉換 grep 分組 cat 將多個文件連續標準輸出 od 以八進制顯示文件 cut 從文件中剪切 paste 從文件中粘貼

Linux 內核結構

在上面我們看到了 Linux 的整體結構,下面我們從整體的角度來看一下 Linux 的內核結構

內核直接坐落在硬件上,內核的主要作用就是 I/O 交互、內存管理和控制 CPU 訪問。上圖中還包括了 中斷 和 調度器,中斷是與設備交互的主要方式。中斷出現時調度器就會發揮作用。這里的低級代碼停止正在運行的進程,將其狀態保存在內核進程結構中,并啟動驅動程序。進程調度也會發生在內核完成一些操作并且啟動用戶進程的時候。圖中的調度器是 dispatcher。

注意這里的調度器是 dispatcher 而不是 scheduler,這兩者是有區別的

scheduler 和 dispatcher 都是和進程調度相關的概念,不同的是 scheduler 會從幾個進程中隨意選取一個進程;而 dispatcher 會給 scheduler 選擇的進程分配 CPU。

然后,我們把內核系統分為三部分。

I/O 部分負責與設備進行交互以及執行網絡和存儲 I/O 操作的所有內核部分。

從圖中可以看出 I/O 層次的關系,最高層是一個虛擬文件系統,也就是說不管文件是來自內存還是磁盤中,都是經過虛擬文件系統中的。從底層看,所有的驅動都是字符驅動或者塊設備驅動。二者的主要區別就是是否允許隨機訪問。網絡驅動設備并不是一種獨立的驅動設備,它實際上是一種字符設備,不過網絡設備的處理方式和字符設備不同。

上面的設備驅動程序中,每個設備類型的內核代碼都不同。字符設備有兩種使用方式,有一鍵式的比如 vi 或者 emacs,需要每一個鍵盤輸入。其他的比如 shell,是需要輸入一行按回車鍵將字符串發送給程序進行編輯。

網絡軟件通常是模塊化的,由不同的設備和協議來支持。大多數 Linux 系統在內核中包含一個完整的硬件路由器的功能,但是這個不能和外部路由器相比,路由器上面是協議棧,包括 TCP/IP 協議,協議棧上面是 socket 接口,socket 負責與外部進行通信,充當了門的作用。

磁盤驅動上面是 I/O 調度器,它負責排序和分配磁盤讀寫操作,以盡可能減少磁頭的無用移動。

I/O 右邊的是內存部件,程序被裝載進內存,由 CPU 執行,這里會涉及到虛擬內存的部件,頁面的換入和換出是如何進行的,壞頁面的替換和經常使用的頁面會進行緩存。

進程模塊負責進程的創建和終止、進程的調度、Linux 把進程和線程看作是可運行的實體,并使用統一的調度策略來進行調度。

在內核最頂層的是系統調用接口,所有的系統調用都是經過這里,系統調用會觸發一個 trap,將系統從用戶態轉換為內核態,然后將控制權移交給上面的內核部件。

Linux 進程和線程

下面我們就深入理解一下 Linux 內核來理解 Linux 的基本概念之進程和線程。系統調用是操作系統本身的接口,它對于創建進程和線程,內存分配,共享文件和 I/O 來說都很重要。

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

基本概念

每個進程都會運行一段獨立的程序,并且在初始化的時候擁有一個獨立的控制線程。換句話說,每個進程都會有一個自己的程序計數器,這個程序計數器用來記錄下一個需要被執行的指令。Linux 允許進程在運行時創建額外的線程。

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

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

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

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

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

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

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

pid = fork(); //  調用  fork  函數創建進程  if(pid   0){ error() // pid   0, 創建失敗  } else if(pid   0){ parent_handle() //  父進程代碼  } else { child_handle() //  子進程代碼  }

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

Linux 進程間通信

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

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

信號 signal

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

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

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

操作系統會中斷目標程序的進程來向其發送信號、在任何非原子指令中,執行都可以中斷,如果進程已經注冊了新號處理程序,那么就執行進程,如果沒有注冊,將采用默認處理的方式。

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

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

SIGABRT 和 SIGIOT

SIGABRT 和 SIGIOT 信號發送給進程,告訴其進行終止,這個 信號通常在調用 C 標準庫的 abort()函數時由進程本身啟動

SIGALRM、SIGVTALRM、SIGPROF

當設置的時鐘功能超時時會將  SIGALRM、SIGVTALRM、SIGPROF 發送給進程。當實際時間或時鐘時間超時時,發送 SIGALRM。當進程使用的 CPU   時間超時時,將發送 SIGVTALRM。當進程和系統代表進程使用的 CPU 時間超時時,將發送 SIGPROF。

SIGBUS

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

SIGCHLD

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

SIGCONT

SIGCONT 信號指示操作系統繼續執行先前由 SIGSTOP 或 SIGTSTP 信號暫停的進程。該信號的一個重要用途是在 Unix shell 中的作業控制中。

SIGFPE

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

SIGUP

當 SIGUP 信號控制的終端關閉時,會發送給進程。許多守護程序將重新加載其配置文件并重新打開其日志文件,而不是在收到此信號時退出。

SIGILL

SIGILL 信號在嘗試執行非法、格式錯誤、未知或者特權指令時發出

SIGINT

當用戶希望中斷進程時,操作系統會向進程發送 SIGINT 信號。用戶輸入 ctrl – c 就是希望中斷進程。

SIGKILL

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

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

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

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

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

SIGPIPE

SIGPIPE 嘗試寫入進程管道時發現管道未連接無法寫入時發送到進程

SIGPOLL

當在明確監視的文件描述符上發生事件時,將發送 SIGPOLL 信號。

SIGRTMIN 至 SIGRTMAX

SIGRTMIN 至 SIGRTMAX 是實時信號

SIGQUIT

當用戶請求退出進程并執行核心轉儲時,SIGQUIT 信號將由其控制終端發送給進程。

SIGSEGV

當 SIGSEGV 信號做出無效的虛擬內存引用或分段錯誤時,即在執行分段違規時,將其發送到進程。

SIGSTOP

SIGSTOP 指示操作系統終止以便以后進行恢復時

SIGSYS

當 SIGSYS 信號將錯誤參數傳遞給系統調用時,該信號將發送到進程。

SYSTERM

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

SIGTSIP

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

SIGTTIN 和 SIGTTOU

當 SIGTTIN 和 SIGTTOU 信號分別在后臺嘗試從 tty 讀取或寫入時,信號將發送到該進程。

SIGTRAP

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

SIGURG

當套接字具有可讀取的緊急或帶外數據時,將 SIGURG 信號發送到進程。

SIGUSR1 和 SIGUSR2

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

SIGXCPU

當 SIGXCPU 信號耗盡 CPU 的時間超過某個用戶可設置的預定值時,將其發送到進程

SIGXFSZ

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

SIGWINCH

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

管道 pipe

Linux 系統中的進程可以通過建立管道 pipe 進行通信。

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

sort  f | head

它會創建兩個進程,一個是 sort,一個是  head,sort,會在這兩個應用程序之間建立一個管道使得 sort 進程的標準輸出作為 head 程序的標準輸入。sort   進程產生的輸出就不用寫到文件中了,如果管道滿了系統會停止 sort 以等待 head 讀出數據

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

共享內存 shared memory

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

在使用共享內存前,需要經過一系列的調用流程,流程如下

創建共享內存段或者使用已創建的共享內存段(shmget())

將進程附加到已經創建的內存段中(shmat())

從已連接的共享內存段分離進程(shmdt())

對共享內存段執行控制操作(shmctl())

先入先出隊列 FIFO

先入先出隊列 FIFO 通常被稱為 命名管道(Named Pipes),命名管道的工作方式與常規管道非常相似,但是確實有一些明顯的區別。未命名的管道沒有備份文件:操作系統負責維護內存中的緩沖區,用來將字節從寫入器傳輸到讀取器。一旦寫入或者輸出終止的話,緩沖區將被回收,傳輸的數據會丟失。相比之下,命名管道具有支持文件和獨特  API,命名管道在文件系統中作為設備的專用文件存在。當所有的進程通信完成后,命名管道將保留在文件系統中以備后用。命名管道具有嚴格的 FIFO   行為

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

消息隊列 Message Queue

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

套接字 Socket

還有一種管理兩個進程間通信的是使用 socket,socket 提供端到端的雙相通信。一個套接字可以與一個或多個進程關聯。就像管道有命令管道和未命名管道一樣,套接字也有兩種模式,套接字一般用于兩個進程之間的網絡通信,網絡套接字需要來自諸如 TCP(傳輸控制協議)或較低級別 UDP(用戶數據報協議)等基礎協議的支持。

套接字有以下幾種分類

順序包套接字(Sequential Packet Socket):此類套接字為最大長度固定的數據報提供可靠的連接。此連接是雙向的并且是順序的。

數據報套接字(Datagram Socket):數據包套接字支持雙向數據流。數據包套接字接受消息的順序與發送者可能不同。

流式套接字(Stream Socket):流套接字的工作方式類似于電話對話,提供雙向可靠的數據流。

原始套接字(Raw Socket):可以使用原始套接字訪問基礎通信協議。

Linux 中進程管理系統調用

現在關注一下 Linux 系統中與進程管理相關的系統調用。在了解之前你需要先知道一下什么是系統調用。

操作系統為我們屏蔽了硬件和軟件的差異,它的最主要功能就是為用戶提供一種抽象,隱藏內部實現,讓用戶只關心在 GUI 圖形界面下如何使用即可。操作系統可以分為兩種模式

內核態:操作系統內核使用的模式

用戶態:用戶應用程序所使用的模式

我們常說的上下文切換 指的就是內核態模式和用戶態模式的頻繁切換。而系統調用指的就是引起內核態和用戶態切換的一種方式,系統調用通常在后臺靜默運行,表示計算機程序向其操作系統內核請求服務。

系統調用指令有很多,下面是一些與進程管理相關的最主要的系統調用

fork

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

exec

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

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

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

代碼段(codesegment/textsegment)

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

此空間大小在代碼運行前就已經確定

內存空間一般屬于只讀,某些架構的代碼也允許可寫

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

數據段(datasegment)

可讀可寫

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

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

bss 段(bsssegment):

可讀可寫

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

bss 段中的數據一般默認為 0

Data 段

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

棧(stack):

可讀可寫

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

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

堆(heap):

可讀可寫

存儲的是程序運行期間動態分配的 malloc/realloc 的空間

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

下面是這些區域的構成圖

exec 系統調用是一些函數的集合,這些函數是

execl

execle

execlp

execv

execve

execvp

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

當前進程映像被替換為新的進程映像

新的進程映像是你做為 exec 傳遞的燦睡

結束當前正在運行的進程

新的進程映像有 PID,相同的環境和一些文件描述符(因為未替換進程,只是替換了進程映像)

CPU 狀態和虛擬內存受到影響,當前進程映像的虛擬內存映射被新進程映像的虛擬內存代替。

waitpid

等待子進程結束或終止

exit

在許多計算機操作系統上,計算機進程的終止是通過執行 exit 系統調用命令執行的。0 表示進程能夠正常結束,其他值表示進程以非正常的行為結束。

其他一些常見的系統調用如下

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

Linux 進程和線程的實現 Linux 進程

在 Linux 內核結構中,進程會被表示為 任務,通過結構體 structure   來創建。不像其他的操作系統會區分進程、輕量級進程和線程,Linux   統一使用任務結構來代表執行上下文。因此,對于每個單線程進程來說,單線程進程將用一個任務結構表示,對于多線程進程來說,將為每一個用戶級線程分配一個任務結構。Linux   內核是多線程的,并且內核級線程不與任何用戶級線程相關聯。

對于每個進程來說,在內存中都會有一個 task_struct 進程描述符與之對應。進程描述符包含了內核管理進程所有有用的信息,包括 調度參數、打開文件描述符等等。進程描述符從進程創建開始就一直存在于內核堆棧中。

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

我們上面提到了進程描述符,這是一個非常重要的概念,我們上面還提到了進程描述符是位于內存中的,這里我們省略了一句話,那就是進程描述符是存在用戶的任務結構中,當進程位于內存并開始運行時,進程描述符才會被調入內存。

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

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

調度參數(scheduling parameters):進程優先級、最近消耗 CPU 的時間、最近睡眠時間一起決定了下一個需要運行的進程

內存映像(memory image):我們上面說到,進程映像是執行程序時所需要的可執行文件,它由數據和代碼組成。

信號(signals):顯示哪些信號被捕獲、哪些信號被執行

寄存器:當發生內核陷入 (trap) 時,寄存器的內容會被保存下來。

系統調用狀態(system call state):當前系統調用的信息,包括參數和結果

文件描述符表(file descriptor table):有關文件描述符的系統被調用時,文件描述符作為索引在文件描述符表中定位相關文件的 i-node 數據結構

統計數據(accounting):記錄用戶、進程占用系統 CPU 時間表的指針,一些操作系統還保存進程最多占用的 CPU 時間、進程擁有的最大堆棧空間、進程可以消耗的頁面數等。

內核堆棧(kernel stack):進程的內核部分可以使用的固定堆棧

其他:當前進程狀態、事件等待時間、距離警報的超時時間、PID、父進程的 PID 以及用戶標識符等

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

當執行 fork 系統調用時,調用進程會陷入內核并創建一些和任務相關的數據結構,比如內核堆棧(kernel stack) 和 thread_info 結構。

關于 thread_info 結構可以參考

https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch_2avr32_2include_2asm_2thread__info_8h_source.html

這個結構中包含進程描述符,進程描述符位于固定的位置,使得 Linux 系統只需要很小的開銷就可以定位到一個運行中進程的數據結構。

進程描述符的主要內容是根據父進程的描述符來填充。Linux 操作系統會尋找一個可用的 PID,并且此 PID 沒有被任何進程使用,更新進程標示符使其指向一個新的數據結構即可。為了減少 hash table 的碰撞,進程描述符會形成鏈表。它還將 task_struct 的字段設置為指向任務數組上相應的上一個 / 下一個進程。

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

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

在子進程開始運行后,操作系統會調用 exec 系統調用,內核會進行查找驗證可執行文件,把參數和環境變量復制到內核,釋放舊的地址空間。

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

下面是一個示例,用戶輸出 ls,shell 會調用 fork 函數復制一個新進程,shell 進程會調用 exec 函數用可執行文件 ls 的內容覆蓋它的內存。

Linux 線程

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

用戶級線程

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

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

內核級線程

內核級線程通常使用幾個進程表在內核中實現,每個任務都會對應一個進程表。在這種情況下,內核會在每個進程的時間片內調度每個線程。

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

從用戶空間 – 內核空間 – 用戶空間的開銷比較大,但是線程初始化的時間損耗可以忽略不計。這種實現的好處是由時鐘決定線程切換時間,因此不太可能將時間片與任務中的其他線程占用時間綁定到一起。同樣,I/O 阻塞也不是問題。

混合實現

結合用戶空間和內核空間的優點,設計人員采用了一種內核級線程的方式,然后將用戶級線程與某些或者全部內核線程多路復用起來

在這種模型中,編程人員可以自由控制用戶線程和內核線程的數量,具有很大的靈活度。采用這種方法,內核只識別內核級線程,并對其進行調度。其中一些內核級線程會被多個用戶級線程多路復用。

Linux 調度

下面我們來關注一下 Linux 系統的調度算法,首先需要認識到,Linux 系統的線程是內核線程,所以 Linux 系統是基于線程的,而不是基于進程的。

為了進行調度,Linux 系統將線程分為三類

實時先入先出

實時輪詢

分時

實時先入先出線程具有最高優先級,它不會被其他線程所搶占,除非那是一個剛剛準備好的,擁有更高優先級的線程進入。實時輪轉線程與實時先入先出線程基本相同,只是每個實時輪轉線程都有一個時間量,時間到了之后就可以被搶占。如果多個實時線程準備完畢,那么每個線程運行它時間量所規定的時間,然后插入到實時輪轉線程末尾。

注意這個實時只是相對的,無法做到絕對的實時,因為線程的運行時間無法確定。它們相對分時系統來說,更加具有實時性

Linux 系統會給每個線程分配一個 nice   值,這個值代表了優先級的概念。nice 值默認值是 0,但是可以通過系統調用 nice 值來修改。修改值的范圍從 -20 –  +19。nice 值決定了線程的靜態優先級。一般系統管理員的 nice 值會比一般線程的優先級高,它的范圍是 -20 – -1。

下面我們更詳細的討論一下 Linux 系統的兩個調度算法,它們的內部與調度隊列(runqueue) 的設計很相似。運行隊列有一個數據結構用來監視系統中所有可運行的任務并選擇下一個可以運行的任務。每個運行隊列和系統中的每個 CPU 有關。

Linux O(1) 調度器是歷史上很流行的一個調度器。這個名字的由來是因為它能夠在常數時間內執行任務調度。在 O(1) 調度器里,調度隊列被組織成兩個數組,一個是任務正在活動的數組,一個是任務過期失效的數組。如下圖所示,每個數組都包含了 140 個鏈表頭,每個鏈表頭具有不同的優先級。

大致流程如下:

調度器從正在活動數組中選擇一個優先級最高的任務。如果這個任務的時間片過期失效了,就把它移動到過期失效數組中。如果這個任務阻塞了,比如說正在等待  I/O 事件,那么在它的時間片過期失效之前,一旦 I/O   操作完成,那么這個任務將會繼續運行,它將被放回到之前正在活動的數組中,因為這個任務之前已經消耗一部分 CPU   時間片,所以它將運行剩下的時間片。當這個任務運行完它的時間片后,它就會被放到過期失效數組中。一旦正在活動的任務數組中沒有其他任務后,調度器將會交換指針,使得正在活動的數組變為過期失效數組,過期失效數組變為正在活動的數組。使用這種方式可以保證每個優先級的任務都能夠得到執行,不會導致線程饑餓。

在這種調度方式中,不同優先級的任務所得到 CPU 分配的時間片也是不同的,高優先級進程往往能得到較長的時間片,低優先級的任務得到較少的時間片。

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

Linux 系統不知道一個任務究竟是 I/O 密集型的還是 CPU 密集型的,它只是依賴于交互式的方式,Linux 系統會區分是靜態優先級 還是 動態優先級。動態優先級是采用一種獎勵機制來實現的。獎勵機制有兩種方式:獎勵交互式線程、懲罰占用 CPU 的線程。在 Linux O(1) 調度器中,最高的優先級獎勵是 -5,注意這個優先級越低越容易被線程調度器接受,所以最高懲罰的優先級是 +5。具體體現就是操作系統維護一個名為 sleep_avg 的變量,任務喚醒會增加 sleep_avg 變量的值,當任務被搶占或者時間量過期會減少這個變量的值,反映在獎勵機制上。

O(1) 調度算法是 2.6 內核版本的調度器,最初引入這個調度算法的是不穩定的 2.5 版本。早期的調度算法在多處理器環境中說明了通過訪問正在活動數組就可以做出調度的決定。使調度可以在固定的時間 O(1) 完成。

O(1) 調度器使用了一種 啟發式 的方式,這是什么意思?

在計算機科學中,啟發式是一種當傳統方式解決問題很慢時用來快速解決問題的方式,或者找到一個在傳統方法無法找到任何精確解的情況下找到近似解。

O(1) 使用啟發式的這種方式,會使任務的優先級變得復雜并且不完善,從而導致在處理交互任務時性能很糟糕。

為了改進這個缺點,O(1) 調度器的開發者又提出了一個新的方案,即 公平調度器(Completely Fair Scheduler, CFS)。CFS 的主要思想是使用一顆紅黑樹作為調度隊列。

數據結構太重要了。

CFS 會根據任務在 CPU 上的運行時間長短而將其有序地排列在樹中,時間精確到納秒級。下面是 CFS 的構造模型

CFS 的調度過程如下:

CFS   算法總是優先調度哪些使用 CPU 時間最少的任務。最小的任務一般都是在最左邊的位置。當有一個新的任務需要運行時,CFS   會把這個任務和最左邊的數值進行對比,如果此任務具有最小時間值,那么它將進行運行,否則它會進行比較,找到合適的位置進行插入。然后 CPU   運行紅黑樹上當前比較的最左邊的任務。

在紅黑樹中選擇一個節點來運行的時間可以是常數時間,但是插入一個任務的時間是 O(loog(N)),其中 N 是系統中的任務數。考慮到當前系統的負載水平,這是可以接受的。

調度器只需要考慮可運行的任務即可。這些任務被放在適當的調度隊列中。不可運行的任務和正在等待的各種 I/O 操作或內核事件的任務被放入一個等待隊列中。等待隊列頭包含一個指向任務鏈表的指針和一個自旋鎖。自旋鎖對于并發處理場景下用處很大。

Linux 系統中的同步

下面來聊一下 Linux 中的同步機制。早期的 Linux 內核只有一個 大內核鎖(Big Kernel Lock,BKL)。它阻止了不同處理器并發處理的能力。因此,需要引入一些粒度更細的鎖機制。

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

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

Linux 啟動

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

當計算機電源通電后,BIOS 會進行開機自檢(Power-On-Self-Test, POST),對硬件進行檢測和初始化。因為操作系統的啟動會使用到磁盤、屏幕、鍵盤、鼠標等設備。下一步,磁盤中的第一個分區,也被稱為 MBR(Master Boot Record) 主引導記錄,被讀入到一個固定的內存區域并執行。這個分區中有一個非常小的,只有 512 字節的程序。程序從磁盤中調入 boot 獨立程序,boot 程序將自身復制到高位地址的內存從而為操作系統釋放低位地址的內存。

復制完成后,boot 程序讀取啟動設備的根目錄。boot 程序要理解文件系統和目錄格式。然后 boot 程序被調入內核,把控制權移交給內核。直到這里,boot 完成了它的工作。系統內核開始運行。

內核啟動代碼是使用匯編語言完成的,主要包括創建內核堆棧、識別 CPU 類型、計算內存、禁用中斷、啟動內存管理單元等,然后調用 C 語言的 main 函數執行操作系統部分。

這部分也會做很多事情,首先會分配一個消息緩沖區來存放調試出現的問題,調試信息會寫入緩沖區。如果調試出現錯誤,這些信息可以通過診斷程序調出來。

然后操作系統會進行自動配置,檢測設備,加載配置文件,被檢測設備如果做出響應,就會被添加到已鏈接的設備表中,如果沒有相應,就歸為未連接直接忽略。

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

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

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

getty 程序會在終端上輸入

login:

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

整個系統啟動過程如下

Linux 內存管理

Linux 內存管理模型非常直接明了,因為 Linux 的這種機制使其具有可移植性并且能夠在內存管理單元相差不大的機器下實現 Linux,下面我們就來認識一下 Linux 內存管理是如何實現的。

基本概念

每個 Linux 進程都會有地址空間,這些地址空間由三個段區域組成:text 段、data 段、stack 段。下面是進程地址空間的示例。

數據段(data segment) 包含了程序的變量、字符串、數組和其他數據的存儲。數據段分為兩部分,已經初始化的數據和尚未初始化的數據。其中尚未初始化的數據就是我們說的 BSS。數據段部分的初始化需要編譯就期確定的常量以及程序啟動就需要一個初始值的變量。所有 BSS 部分中的變量在加載后被初始化為 0。

和 代碼段(Text segment) 不一樣,data segment 數據段可以改變。程序總是修改它的變量。而且,許多程序需要在執行時動態分配空間。Linux 允許數據段隨著內存的分配和回收從而增大或者減小。為了分配內存,程序可以增加數據段的大小。在 C 語言中有一套標準庫 malloc 經常用于分配內存。進程地址空間描述符包含動態分配的內存區域稱為 堆(heap)。

第三部分段是 棧段(stack segment)。在大部分機器上,棧段會在虛擬內存地址頂部地址位置處,并向低位置處(向地址空間為 0 處)拓展。舉個例子來說,在 32 位 x86 架構的機器上,棧開始于 0xC0000000,這是用戶模式下進程允許可見的 3GB 虛擬地址限制。如果棧一直增大到超過棧段后,就會發生硬件故障并把頁面下降一個頁面。

當程序啟動時,棧區域并不是空的,相反,它會包含所有的 shell 環境變量以及為了調用它而向 shell 輸入的命令行。舉個例子,當你輸入

cp cxuan lx

時,cp 程序會運行并在棧中帶著字符串 cp cxuan lx,這樣就能夠找出源文件和目標文件的名稱。

當兩個用戶運行在相同程序中,例如編輯器(editor),那么就會在內存中保持編輯器程序代碼的兩個副本,但是這種方式并不高效。Linux 系統支持共享文本段作為替代。下面圖中我們會看到 A 和 B 兩個進程,它們有著相同的文本區域。

數據段和棧段只有在 fork 之后才會共享,共享也是共享未修改過的頁面。如果任何一個都需要變大但是沒有相鄰空間容納的話,也不會有問題,因為相鄰的虛擬頁面不必映射到相鄰的物理頁面上。

除了動態分配更多的內存,Linux 中的進程可以通過內存映射文件來訪問文件數據。這個特性可以使我們把一個文件映射到進程空間的一部分而該文件就可以像位于內存中的字節數組一樣被讀寫。把一個文件映射進來使得隨機讀寫比使用 read 和 write 之類的 I/O 系統調用要容易得多。共享庫的訪問就是使用了這種機制。如下所示

我們可以看到兩個相同文件會被映射到相同的物理地址上,但是它們屬于不同的地址空間。

映射文件的優點是,兩個或多個進程可以同時映射到同一文件中,任意一個進程對文件的寫操作對其他文件可見。通過使用映射臨時文件的方式,可以為多線程共享內存提供高帶寬,臨時文件在進程退出后消失。但是實際上,并沒有兩個相同的地址空間,因為每個進程維護的打開文件和信號不同。

Linux 內存管理系統調用

下面我們探討一下關于內存管理的系統調用方式。事實上,POSIX 并沒有給內存管理指定任何的系統調用。然而,Linux 卻有自己的內存系統調用,主要系統調用如下

系統調用描述 s = brk(addr)改變數據段大小 a = mmap(addr,len,prot,flags,fd,offset)進行映射 s = unmap(addr,len)取消映射

如果遇到錯誤,那么 s 的返回值是 -1,a 和 addr 是內存地址,len 表示的是長度,prot 表示的是控制保護位,flags 是其他標志位,fd 是文件描述符,offset 是文件偏移量。

brk 通過給出超過數據段之外的第一個字節地址來指定數據段的大小。如果新的值要比原來的大,那么數據區會變得越來越大,反之會越來越小。

mmap 和 unmap   系統調用會控制映射文件。mmp 的第一個參數 addr 決定了文件映射的地址。它必須是頁面大小的倍數。如果參數是 0,系統會分配地址并返回  a。第二個參數是長度,它告訴了需要映射多少字節。它也是頁面大小的倍數。prot 決定了映射文件的保護位,保護位可以標記為 可讀、可寫、可執行或者這些的結合。第四個參數  flags 能夠控制文件是私有的還是可讀的以及 addr 是必須的還是只是進行提示。第五個參數 fd   是要映射的文件描述符。只有打開的文件是可以被映射的,因此如果想要進行文件映射,必須打開文件;最后一個參數 offset   會指示文件從什么時候開始,并不一定每次都要從零開始。

Linux 內存管理實現

內存管理系統是操作系統最重要的部分之一。從計算機早期開始,我們實際使用的內存都要比系統中實際存在的內存多。內存分配策略克服了這一限制,并且其中最有名的就是 虛擬內存(virtual memory)。通過在多個競爭的進程之間共享虛擬內存,虛擬內存得以讓系統有更多的內存。虛擬內存子系統主要包括下面這些概念。

大地址空間

操作系統使系統使用起來好像比實際的物理內存要大很多,那是因為虛擬內存要比物理內存大很多倍。

保護

系統中的每個進程都會有自己的虛擬地址空間。這些虛擬地址空間彼此完全分開,因此運行一個應用程序的進程不會影響另一個。并且,硬件虛擬內存機制允許內存保護關鍵內存區域。

內存映射

內存映射用來向進程地址空間映射圖像和數據文件。在內存映射中,文件的內容直接映射到進程的虛擬空間中。

公平的物理內存分配

內存管理子系統允許系統中的每個正在運行的進程公平分配系統的物理內存。

共享虛擬內存

盡管虛擬內存讓進程有自己的內存空間,但是有的時候你是需要共享內存的。例如幾個進程同時在 shell 中運行,這會涉及到 IPC 的進程間通信問題,這個時候你需要的是共享內存來進行信息傳遞而不是通過拷貝每個進程的副本獨立運行。

到此,相信大家對“Linux 操作系統全面知識點有哪些”有了更深的了解,不妨來實際操作一番吧!這里是丸趣 TV 網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-08-25發表,共計21761字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 紫阳县| 治多县| 玉环县| 嵊泗县| 定安县| 海淀区| 江山市| 绩溪县| 柳林县| 克什克腾旗| 青龙| 三穗县| 宾阳县| 太湖县| 大关县| 岳池县| 杨浦区| 互助| 静安区| 安陆市| 天镇县| 宁国市| 昭通市| 房产| 白玉县| 天祝| 定兴县| 许昌县| 九寨沟县| 鸡西市| 平和县| 监利县| 日喀则市| 车致| 萨嘎县| 西乡县| 广汉市| 尉犁县| 汶川县| 无锡市| 准格尔旗|