初识linux之进程
目錄
一、馮諾依曼體系結構
1.馮諾依曼體系結構的五大構成
(1)存儲器
(2)輸入設備和輸出設備
(3)運算器和控制器
2.CPU執行
(1)CPU數據來源
(2)內存、CPU和磁盤的數據交換
3.遠端數據傳輸過程
二、操作系統
1.操作系統的概念
2.理解硬件管理
(1)管理的本質
(2)數據獲取
(3)管理的方法
3.對軟件的管理
?4.系統調用和庫函數的關系
?三、進程
1.進程的概念
(1)內存中的進程
(2)PCB(進程控制塊)
(3)進程管理
2.查看進程
(1)windows下的進程查看
(2)linux下的進程查看
(3)結束進程
3.進程的常見調用
(1)通過系統調用獲取進程標示符(進程id和父進程id)
(2)通過系統調用創建進程
4.進程的狀態
(1)程序與內存與CPU的關聯
(2)狀態種類
(3)運行狀態
(4)阻塞狀態
(5)掛起狀態
5.linux中的進程狀態查看
(1)運行(running)狀態
(2)休眠(sleeping)狀態
(3)停止(stopped)狀態
(4)深度睡眠(disk sleep)狀態
(5)追蹤(tracing stop)狀態
?(6)死亡(dead)狀態
(7)僵尸(zombie)狀態
(8)linux下的進程狀態后的“+”含義
6.僵尸進程與孤兒進程
(1)僵尸進程
(2)孤兒進程
7.進程優先級
(1)進程優先級的作用
(2)linux下的進程優先級特點
(3)linux下的進程優先級修改
8.進程相關概念
(1)競爭性
(2)獨立性
(3)并發
(4)并行
9.進程切換
四、環境變量
1.PATH環境變量
(1)將程序路徑添加進該環境變量
2.添加環境變量
(1)添加本地變量
3.取消環境變量和本地變量
?4.查看環境變量內容
(1)查看環境變量
(2)查看本地變量和環境變量
5.windows下的環境變量
6.int main()主函數中的參數
(1)int argc和char* argv[]
(2)char* env[]
五、進程地址空間
1.C/C++地址空間
2.虛擬地址空間
(1)進程認為自身獨占系統資源
(2)系統如何使進程認為自己獨占所有資源
3.虛擬地址
(1)進程地址空間的區域劃分
(2)虛擬地址
4.頁表
5.虛擬地址空間存在原因
(1)保護其他進程和用戶信息
(2)保證進程的獨立性
(3)便于編譯器以統一視角編譯代碼
5.線性地址與邏輯地址
(1)線性地址
(2)邏輯地址
一、馮諾依曼體系結構
在了解linux下的進程之前,我們要先對計算機的構成有一個基本的認識。計算機的組成是由一位名叫馮·諾依曼的數學家提出的。由此,計算機的構成也被叫做“馮諾依曼體系結構”
馮諾依曼體系結構的構成如上圖所示。主要由“輸入設備”、“輸出設備”、“存儲器”、“運算器”和“控制器”五個組件構成。因為本段落并不是主講計算機構成,而僅僅是為linux下的進程做鋪墊,因此不會過于細致深入的講解。如果想要更加深入的理解計算機,可以去看《深入理解計算機系統》
1.馮諾依曼體系結構的五大構成
(1)存儲器
存儲器,簡單來講就是存儲數據的地方。而在計算機中,存儲器主要指的是“內存”,計算機中的程序一般都是要加載到內存中去運行。這個“內存”要與我們日常說的磁盤空間的容量相區分。
磁盤空間的容量是外存,但是因為日常中大部分人對計算機的內存和外存區分不清楚,因此統一都是叫“內存”。磁盤空間的外存的特點是有“永久性存儲能力”,即存儲的數據不因計算機的關閉而丟失。而內存有一個特點,就是“掉電易失”。指的是如果計算機斷電了,內存中的數據就會丟失。
(2)輸入設備和輸出設備
輸入設備和輸入設備,簡單來講就是計算機的外設。鼠標、鍵盤、磁盤和網卡等都屬于計算機的輸入或輸出設備。注意,一個外設既可以是輸入設備,也可能是輸出設備。例如磁盤,我們可以從外部輸入數據到磁盤上,也可以從磁盤上讀取數據
(3)運算器和控制器
運算器是用于處理計算機中對數據的數學計算和邏輯運算等的組件。控制器則是完成指令相關操作的組件。這兩個組件沒必要多說,我們只需要知道“運算器 + 控制器 + 部分其他組件 = CPU”,即中央處理器即可
2.CPU執行
在日常編寫代碼的過程中,不知道大家有沒有想過,CPU究竟是如何識別代碼中所要傳遞的信息的。要知道,CPU雖然計算和運行速度非常快,但是其本身只能被動的接受傳輸過來的數據和命令,并按照傳過來的命令執行對應的操作。就如同我們在高中生活中只能被動接受老師的指令進行學習一般。
但是CPU要執行對應的命令,就必須先明白我們傳過去的數據的意思。為此,CPU自身存在一個指令集,該指令集中就是CPU所能執行的命令。同時我們知道計算機底層是由0,1信號組成的。而我們寫代碼編譯的本質其實就是一個翻譯的過程,即將我們的代碼翻譯為“二進制可執行程序”。計算機將該程序傳給CPU,CPU再根據指令集進行對應的操作
(1)CPU數據來源
同時,CPU在讀取和寫入數據時,為了提高計算機的運行效率,只和“內存”打交道。但是,內存中在一開始是沒有數據的。內存中的數據是來源于磁盤,也就是外存的。我們的計算機中的各種程序、文件等數據都是存在磁盤之中,當內存需要時再從磁盤加載到內存中。
但是,我們說了在CPU中讀寫數據是為了提高效率。如果每次需要數據都從磁盤先加載內存,再從內存加載到CPU,那么效率其實并不會有什么提升。為此,磁盤中的部分數據其實是會提前加載到內存中的。比如我們的操作系統。其實我們的操作系統也是一個軟件,其數據在關機狀態下存于磁盤中,當我們開機時就會將操作系統的信息加載到內存中。所以其實我們計算機開機的過程就是在加載數據到內存。因此,一個程序要運行必須加載到內存
(2)內存、CPU和磁盤的數據交換
由此,內存也可以看做一個容量很大的緩存,用于與CPU和磁盤之間數據交換的適配。CPU和磁盤都會預先將數據加載到內存,在需要時從內存中刷新數據到磁盤或CPU中。我們經常說的IO過程,其實就是數據從外設中加載到內存和從內存加載到外設中的過程
而內存中需要刷新數據,在有些數據無用時需要刪除數據等等。這些操作內存自身是無法完成的,而是交由操作系統完成。
3.遠端數據傳輸過程
在日常生活中,我們肯定都有過通過QQ或微信給別人發送信息的經歷。但是大家有沒有想過這個從自己的設備發送信息到他人的設備的過程是怎么樣的呢?這個問題當我們了解了馮諾依曼體系結構后就能有一個初步的認知
假設你和你朋友的QQ已經處于登錄狀態,已經加載到了內存當中。現在你和你的朋友之間發了一條“你好”的信息。此時這條信息會先從你的輸入設備,也就是鍵盤或觸摸屏傳輸數據到內存中的QQ的程序中,然后從內存里加載到輸出設備,也就是網卡中。然后這條信息通過網絡傳輸到你朋友的輸入設備網卡中,再從輸入設備中加載到你朋友的內存的QQ程序中,最后從內存中加載到你朋友的輸出設備顯示器上。上述過程不考慮網絡傳輸過程和內存中的數據處理等問題。由此,遠端的數據傳輸可以看成如下圖所示:
二、操作系統
因為操作系統太過龐大,在這里想要完全講完是不可能的,因此此處主要是對操作系統的一個初步理解
1.操作系統的概念
操作系統,簡單來講就是一個進行軟件硬件資源管理的軟件。那么操作系統為了要對軟硬件進行管理呢?原因是為了通過合理的管理軟硬件資源(方法),為用戶提供良好的(穩定、高效、安全)執行環境(目的)。
而操作系統的管理主要涉及四個方面:“進程管理”、“文件系統”、“內存管理”和“驅動管理”。
2.理解硬件管理
(1)管理的本質
在理解操作的管理之前。我們要先對管理由一個初步的認知。
為了更好的理解,我們在這里舉一個例子。我們在學校里都有一個共同的管理者,也就是校長會對我們進行管理。在這里,校長是管理者,我們則是被管理者。但是,大多數學校里面的人,其實和校長并沒有交集,甚至可能沒有見過面。但是校長依然能夠將整個學校里的人管理的井井有條。這就是說明:“管理者不需要和被管理者直接交互,依然能夠管理好被管理者”。
然后,在學校里我們也有輔導員和班長,有人可能認為輔導員和班長也是管理者。但是他們在本質上其實并不是管理者,僅僅只是管理者下的執行者,執行管理者下達的管理方法。因為他們并沒有對管理上重大事宜決策的權力。由此,管理者還需要具備一個因素,就是“擁有對重大事宜的決策權”。
同時我們要知道,管理不是亂管理,每項決策的通過執行都需要有對應的依據。以學校管理為例,學校要對學生進行管理,就需要有學生對應的身份信息,學習成績等各類信息。由此,學校的管理就是“對數據進行管理”。而管理的本質也就是“對數據進行管理”。
(2)數據獲取
既然管理者要對被管理者進行管理的本質是對數據進行管理,但是管理者和被管理者又不直接接觸,那么被管理者的數據管理者要怎么拿到呢?由此,在管理者和被管理者之間就還有一個執行者:
當然,這里是進行了簡化的,實際情況會更加復雜,但是大框架就是這樣。
現在有了執行者這層關系,管理者想要獲得數據,就是下達指令給執行者,執行者與被管理直接接觸拿到對應的數據,然后執行者再將獲取的數據交給管理者,管理者再通過得到的數據進行決策:
將這層關系推到操作系統上,就可以得出操作系統對硬件進行管理時,會通過一層執行者,也就是驅動程序進行管理:
有人可能不太理解什么是驅動。驅動簡單來講,就是使我們的各類硬件可以正常使用的程序。我們的鍵盤、鼠標等硬件并不是插入接口后就能直接使用的。有些人可能就遇見過硬件已經插入接口了,但是無法正常使用,并且計算機中顯示“未查找到驅動程序”或“驅動程序損壞”等提示。這就是因為硬件的使用需要操作系統通過驅動程序來進行管理,驅動程序損壞就會導致對應的硬盤無法被正常管理使用。
(3)管理的方法
我們依然以學校為例子。一個學校少則上千人,多則上萬人。校長作為管理者,如果單憑一個人管理數據,無疑是不太現實的。但是學生雖然很多,但是每個學生的數據都是有共同點,無非就是包括姓名、地址、學號、身份證號等各類個人信息。由此,通過這些個人信息我們就可以抽象出一個struct結構體:
我們讓學生將自己數據輸入到該結構體中,并利用指針的形式形成一個鏈表。由此,對學生數據的管理就演變成了對鏈表的管理。當我們需要對應學生數據時,直接操作該鏈表即可。通過這種方式,就大大簡化了操作的復雜度。
而這個通過學生的信息特點進行描述的過程就是一個對管理對象的信息進行建模的過程。同時,這個構建結構體的過程在C++中就可以看做是“面向對象”的過程。
通過上述例子,其進行管理的過程其實就是在先對信息進行描述,再根據描述的內容進行建模。因此,管理的方法其實就可以看做是“先描述,再組織”
3.對軟件的管理
首先我們要知道,軟件不僅可以管理硬件,也可以管理軟件。就好比學校里的校長作為一個人,不僅可以管理學校內的各種硬件設備,也可以管理人一樣。
操作系統作為一個軟件,既可以管理計算機內的硬件,也可以管理計算機內的軟件。但是,操作系統 內部其實也是有一定的保護措施的。就好比現實中的銀行,你要去銀行辦理業務,業務員坐在服務窗口后面,你與業務員之間隔著一塊玻璃。這塊玻璃其實就是為了在有危險分子破壞銀行時用于保護業務員的安全的。
操作系統也是同理。但是就像業務員需要為用戶辦理業務,因此開了一個小窗口進行服務:
操作系統也是需要為上層用戶提供服務的。但是為了避免上層用戶對操作系統底層數據的破壞,也是提供了一個“操作系統接口”來為用戶提供服務。要注意的是,這個接口其實就是系統調用接口,因為操作系統是用C語言寫的,這個系統調用接口其實也是“C式接口”
當然,雖然操作系統提供了各式各樣的系統調用接口,但是對于大部分用戶來說,直接調用系統接口進行操作還是過于復雜和困難,由此又出現了“用戶操作接口”。這個接口由shell外殼、C/C++的lib和部分指令組成。用戶使用計算機就是通過各種指令等操作去調用“用戶調用接口”,再用用戶調用接口去調用“系統調用接口”:
?4.系統調用和庫函數的關系
(1)從開發角度來看,操作系統對外會表現為一個整體,但是會暴露自己的部分接口,供上層開發使用,這部分由操作系統提供的接口,叫做系統調用
(2)系統調用在使用上,功能比較基礎,對用戶的要求也相對較高。所以,有心的開發者可以對部分系統調用進行適度封裝,從而形成“庫”。有了庫,就很有利于更上層用戶或者開發者進行二次開發
?三、進程
1.進程的概念
進程,指的就是一個運行起來(即加載到了內存中)的程序,就叫做進程。也可以簡單理解為在內存中的程序就是一個進程。
而我們所說的程序,其本質就是文件,這些文件存放在磁盤上
(1)內存中的進程
在之前我們已經了解到,磁盤中的程序會加載到內存中,但其過程并不是我們所想象的那么簡單。因為加載到內存中的程序,一般來講并不會只有一個,都是多個進程同時存在于內存中:
如果有疑惑,可以打開電腦上的任務管理器,這里面就有進程查看窗口:
并不是說如果你打開電腦什么軟件都不開,就不會有進程。只要你的電腦處于開啟狀態,就會有系統進程在內存當中,少則十幾個數十個,多則上百個。
對于如此多的進程,操作系統則需要對這些進程進行管理,包括分配空間的大小,分配的空間的位置等各種各樣的信息都需要操作系統進行管理。而我們之前說過,管理的方法是“先描述,再組織”。
(2)PCB(進程控制塊)
為了方便對這些進程進行管理,操作系統也根據這些進程的一些共同信息進行整理歸類,形成了“PCB”,也就是“進程控制塊”。進程控制塊我們現在可以簡單的看為就是一個結構體,這個結構體里面包含了進程的各類屬性和該進程對應的代碼、屬性地址(這些屬性在磁盤中的可執行程序中是沒有的,會在加載到內存時自動形成):
由此,每一個進程在內存都擁有一個“struct task_struct*”。至于為什么是結構體而不是類,是因為操作系統是由C語言編寫的,C語言中并沒有類的概念
(3)進程管理
當我們理解的“進程控制塊”后,就可以更好的理解進程在系統中的交互了。
當一個程序從磁盤加載到內存中時(注意加載的僅僅只是程序的執行代碼等內容),會對應生成了一份“進程控制塊”,該控制塊會指向內存中對應的進程。當存在多進程時,這些控制塊以鏈表的形式相互連接。如果CPU想調用某個進程,內存就會通過控制塊鏈表找到對應的控制塊,再由控制塊找到對應進程的執行代碼發送給CPU:
由此,所謂的對進程進行管理,就變成了對進程對應的“PCB”進行相關管理,進而變成了對鏈表的增刪查
在進程控制塊的內核當中的struct task_strcut是內核結構體,是用于描述進程的結構體。當我們形成進程時,該內核結構體會為我們創建一個內核對象:”task_struct 對象”。然后再將該結構與我們的代碼和數據相關聯起來,進而完成“先描述,再組織”的工作
由此,我們可以得到進程其實就是“進程 = 內核數據結構(task_struct) + 進程對應的磁盤代碼”
2.查看進程
(1)windows下的進程查看
在windows下要查看進程是很簡單的,直接打開任務管理器就可以看見:
(2)linux下的進程查看
為了便于查看,我們現在寫以下一個會循環打印“i'm a process”這句話的程序:
?我們先生成一個“myprocess”程序,此時該程序還沒有運行,僅僅只是一個在磁盤上的二進制文件:
?我們執行該程序,此時它就變成了一個進程:
但是我們現在雖然知道它是一個進程,但卻無法查看該進程。要查看該進程主要用以下方法。
1.執行“ps ajx | grep 程序名”命令
該命令中的“ps ajx”是顯示系統中的所有進程。整條命令是用于篩選帶有對應“程序名”字樣的進程。我們執行后就可以找到對應的進程:
如果不理解這里面顯示的信息,可以輸入“ps ajx | head -1 && ps ajx | grep ‘程序名’”。該命令會輸出對應的數據的標題:
(3)結束進程
要在linux下結束一個進程非常簡單,我們直接執行“kill -9 PID號”即可:
3.進程的常見調用
(1)通過系統調用獲取進程標示符(進程id和父進程id)
1.進程id和父進程id
在linux下,每個程序都有自己的進程id和父進程id。
在了解進程id和父進程id之前,我們要先了解兩個函數:getpid()和getppid()。第一個函數是用來獲取進程id的, 而第二個函數則是用來獲取父進程id的:
這里的返回值是“pid_t”,我們暫時將其理解為返回一個整數即可。
為了方便演示,我們用以下程序演示:
該程序的運行結果如下所示:
可以看到,此時就將該進程的pid和ppid打印出來了。但是,如果我們多執行幾次:
我們就會發現,該程序的進程id會不斷改變。這很容易理解,因為進程結束后重新加載到內存中都會有一個新的進程id。就好比你高考考上了一個大學,你入學后會給你一個學號,但是你不滿意該學校,復讀了一年又考上了同一個大學,此時又會給你一個學號,但是這兩個學號是不一樣的。
進程id改變我們能理解,但奇怪的是父進程id卻沒有改變。既然如此,我們現在就輸入“ps ajx | head -1 && ps ajx | grep 29513”命令:
可以看到,在進程名字這一塊,29513顯示的是“bash”。這其實就是一個“總進程”。在操作系統啟動時,操作系統就已經為我們指派了一個進程,我們所有的進程都是在該進程下執行的“子進程”。而該機制的目的就是為了避免我們的進程有問題時,對系統使用產生影響。
而這里的這個“bash”進程,其實就是我們的shell。我們執行“kill -9 進程id”命令來結束該進程:
可以看到,當我將該“bash”進程結束掉后,我的linux賬戶就直接退出了。當然,在某些云服務器上可能不會退出,但也會導致命令無法使用
注意:命令行上啟動的進程,一般它的父進程沒有特殊情況的話,都是bash
(2)通過系統調用創建進程
1.fork()函數調用
要通過系統調用創建進程,直接調用fork()函數即可,該函數的作用就是創建一個進程:
雖然我們知道了fork()函數,也知道了該函數的作用,但我們不知道該函數執行后會出現什么現象。因此,我們用以下程序來進行測試:
在執行該程序前,我們要先知道,在fork()函數調用前,該進程只有一個父進程。而在fork()函數調用后,才會新形成一個進程,此時就是“父進程 + 子進程”兩個進程。
現在我們來執行該程序:
可以看到,我的程序里面僅僅只是打印一句話,但是這里卻打印了兩句話,并且第一句話中的子進程id就是第二句話的父進程id。
這就是因為fork()創建了一個進程。第一個進程的父進程很明顯是“bash”,而第二個進程由于是在該程序中創建的,因此它的父進程就是進程10755。這也就說明了fork()確實能創建進程
2.fork()函數的返回值
從上面的程序中我們確實可以證明fork()函數可以創建進程。但是這種使用方式卻沒有什么意義,因為將fork()函數之后的代碼執行兩次并沒有什么用。
因此,現在我們來看看該函數的返回值。
首先是函數的返回值類型:
可以看到,這個函數的返回值是“pid_t”,前面也說過,這個返回值看成返回一個整數即可,沒什么好奇怪的。
接下來我們看看該函數對返回值的解釋:
在這里我們只用看成功時的返回值,失敗的現象我們就先不考慮。可以看到,在這里的返回值解釋中說到,“如果成功,就把子進程的pid返回給父進程,并將0返回給子進程”。那么我們修改一下程序如下圖來測試一下:
?然后我們執行該程序:
可以看到,執行結果確實是將子進程的pid返回給了父進程,并將0返回給了子進程。一眼看上去沒有什么問題,和手冊里面的返回值解釋的一模一樣。
但是,我們在學習C或C++時說過,“一個變量在同一時間只能有一個返回值”。而在這里,在該程序沒有對返回值進行任何修改操作的情況下,竟然讓變量在同一時間擁有了兩個返回值。這就是問題所在。
3.fork()的使用場景
對于上述情況,我們現在是無法理解的,因為我們對進程的理解還不夠深入。但我們理解了進程控制之后才能對上述情況有個更清晰的認識。
因此既然我們無法理解現象,但我們可以用它。現在我們寫如下一個程序:
如果id == 0,我們就循環打印子進程。如果id > 0,就循環打印父進程。注意,我這里寫的是兩個死循環
然后來執行該程序:
可以看到這里程序在正常運行,這種同一程序中多個進程同時運行的現象就叫做“多進程”。
但是我們要注意到,該程序不僅打印了子進程,也打印了父進程。我們的程序寫的是一個if判斷,并且里面的內容是死循環打印。按道理來講同一時間只會有一個if else判斷成立。但是,在這里這兩個判斷條件同時成立并且都在死循環打印
這種現象其實就說明了兩個點:
(1)fork()執行后,會有父進程和子進程兩個進程在執行后續的代碼
(2)fork()之后的代碼,會被父進程和子進程共享
意思就是,在這里是兩個不同的進程在執行相同的代碼,根據fork()返回值的不同執行了各自不同的部分。而這種通過返回值的不同,使父子進程執行后續共享代碼的不同部分的編程方式就叫做“并發式編程”
4.進程的狀態
在理解進程狀態前,我們要先理解CPU是如何調度進程和進程時如何存在于內存中的
(1)程序與內存與CPU的關聯
首先我們要知道,我們的各類硬件都是通過驅動程序來支持運行的,每個驅動程序在磁盤中都會一個對應的“PCB進程結構體”,這個結構體中包含了對應的硬件的所有屬性信息。
而磁盤中的程序要運行也是同樣的。磁盤中的程序啟動后會進入內存中變成進程,此時該進程中包含了運行程序的代碼等內容。而當一個程序變成一個進程后,內存中就會對應生成該進程的“PCB進程結構體”,該結構體中包含了對應進程的各類屬性和對應進程的代碼地址。
當這些進程要運行時,就必需要由CPU進行調度執行。但是我們的CPU只有一個,哪怕你是多核,你的CPU的數量依然無法與進程的數量相比。并且一個CPU一般來講只能同時運行一個進程。那么有人就會奇怪了,既然一個CPU同時只能運行一個進程,那么為什么我們的計算機上能有數十上百個進程在同時運行呢?
這是因為CPU雖然同時只能運行一個進程,但是它的運行速度非常快,運行一個進程的速度是以納秒甚至微秒為單位的。因此,CPU在運行進程時可以看成是在不間斷的循環跑所有進程,一個進程跑完后立刻下一個進程。就好比我們在公司面試時一次面試只能進去一個人,面試官就是CPU,而我們就是那一個個進程。同時因為其速度非常快,在感官上就會給我們所有進程在同時運行的錯覺,其實這些進程都是一個一個的運行的。
既然進程需要循環跑,那么就必定需要按照某個順序來跑,不然CPU無法知道自己應該接著運行哪個程序。由此,又有了“運行隊列”的概念。從名字就可以知道,這是一個隊列。在這個隊列中包含了指向進程結構體的指針和一些其他需要的屬性。運行隊列按一定順序將這些結構體鏈接在一起,CPU運行進程時就按照該運行隊列中的順序運行。
要注意,在運行隊列中的是“PCB結構體”,而不是進程本身。就好比我們找工作投簡歷時,發給公司的是我們的簡歷,公司在對簡歷排序篩選時也是對我們的簡歷進行排序,而不是讓我們本人過去對我們進行排序。
?現在有了對上述的內存中的進程與CPU的關聯的知識后,我們就能很容易的理解各類進程狀態了
(2)狀態種類
進程有很多種狀態,如運行、新建、就緒、掛起、阻塞、等待、停止、掛機、死亡等各類狀態。在這里不會將所有狀態都講解一遍,而是選擇其中比較常見的幾種狀態講解
(3)運行狀態
運行狀態,指的就是“處于運行隊列中的進程的狀態”。換言之,只要一個進程在CPU的運行隊列中,那么該進程就處于運行狀態,都是“R”狀態。無論它是否真的在CPU中運行。就好比我們面試時,只要在面試房間外排隊的人都將自己調整為了面試狀態準備面試,無論這些人是否是在面試房間內接受面試
(4)阻塞狀態
我們要知道,CPU在運行進程時,這些進程不僅需要CPU中的資源,也可能需要硬件資源。就比如我們在QQ上和別人聊天,此時QQ處于運行狀態,需要占用CPU資源,同時我們發送信息也需要網卡,顯示器等硬件資源。
當然,有人可能會覺得既然進程需要硬件資源直接去用就行了。但是硬件資源的調用速度非常慢,當然這個慢是與CPU的速度相比。因此就可能出現CPU在調用某個進程時,該進程還需要用到硬件資源而使CPU不得不停下等待的狀況。為了應對這個問題,在我們的硬件的“PCB結構體”中就還有一個“等待隊列”。該隊列和CPU的“運行隊列”差不多,是用于等待硬件進行調用完成進程資源需求的:
假如現在在CPU中有一個正在運行的進程想要向磁盤寫入數據,那么CPU就會將該進程從運行隊列中剝離出來,將其放到磁盤的等待隊列中,然后繼續執行其他進程。如果磁盤此時正在為其他進程服務,該進程就會在等待隊列中等待。就好比我們去銀行辦理業務,業務窗口的工作人員告訴我們說我們需要先去另一個窗口填一個表才能幫我們辦理對應業務,此時我們就直接去另一個窗口填表,如果沒人就直接填,有人就排隊等待。但是我們原來的那個業務窗口不可能等我們填完表幫我們辦理好后再幫其他人辦理業務,而是在我們去另一個窗口時接著幫其他人辦理業務。這種進程在硬件的等待隊列中等待硬件資源就緒,就叫做進程處于“阻塞狀態”
當該進程的資源使用完成后,操作系統就會將該進程從磁盤中調出來,將其狀態改為“運行狀態”后重新加入CPU的運行隊列運行。
從這里我們也可以看出來,其實進程的不同狀態指的就是“進程在不同的隊列中”
注意,進程在不同隊列中指的是進程的“PCB結構體”在不同隊列中,而不是進程本身
(5)掛起狀態
我們說過阻塞狀態就是一個進程因為需要使用硬件資源而進入對應硬件的等待隊列時的狀態。
但是如果此時一個硬件的等待隊列中存在多個等待進程,那么就會導致這些進程雖然無法進入CPU的運行隊列運行,但也無法使用硬盤資源,只能等待。
此時對應進程的“PCB結構體”雖然在硬盤的等待隊列中無法運行,但是其本身的數據依然在內存中占據空間。因此對于這種占著磁盤空間卻什么都沒干的進程,內存為了騰出容量供其他進程使用,就會暫時將該進程的代碼、執行進度等數據從內存中挪到磁盤的一塊專門存放這種未運行進程數據的空間內
而這種在內存中的一個進程的數據被從內存中暫時轉移到磁盤內的行為,就叫做“掛起”。而此時的進程就是處于“掛起狀態”
當對應進程的進程結構體進入硬件使用完資源被重新放入運行隊列時,該進程的資源就會被從磁盤內重新轉移到內存中。這種對進程數據的轉移就叫做“內存數據的換入換出”
當然,與掛起相關的還有“阻塞掛起狀態”,“準備掛起狀態”等。有興趣的可以自行了解,基本除了“運行狀態”外,其他的狀態都可以與“掛起狀態”相結合
5.linux中的進程狀態查看
如果大家有興趣,可以去linux中的內核源代碼中搜索“task_state_array”來查看liunx下的進程狀態的表示。這里就不去查找了,linux下的進程狀態表示就如下圖:
(1)運行(running)狀態
在linux下,進程的運行狀態用“R”表示。
為了方便看到進程狀態,我們現在寫以下一個死循環程序:
?注意,因為這里程序是在死循環運行,除非我們停下程序,不然在該窗口下是無法執行命令的。因此我們要先復制窗口,然后在另一個窗口下執行命令:
右擊你的賬戶選項卡,選擇里面的復制會話即可
我們將該程序運行起來后,輸入“ps axj | grep 程序名”命令:
此時我們可以看到,在該myprocess程序下的狀態欄下,就有一個“R+”的符號,這就表示該程序處于“運行狀態”。至于“+”號,我們暫時不用管。下面的那個進程時grep命令的進程,也不用管
(2)休眠(sleeping)狀態
睡眠狀態在linux下的符號是“S”。
我們現在準備以下一個死循環打印程序:
現在我們來運行該程序:
此時該程序在不停的打印數據。然后我們在另一個窗口輸入“ps ajx | grep 程序名”命令:
?可以看到,現在該程序的運行狀態就變成了“S+”,即“睡眠狀態”。那大家就會覺得很奇怪,明明該程序一直在運行,為什么這里顯示的不是“運行狀態”而是“睡眠狀態”
這其實是因為該程序需要循環打印一條消息,要打印該消息就需要使用顯示器的資源。但是顯示器作為一個硬件,其運行速度比CPU的速度慢很多。可以說在該程序中有99%的時間都在訪問顯示器,只有1%的時間是CPU運行該進程。當我們查看該進程時,極大可能查看到的都是該進程在訪問顯示器時的狀態,而非在CPU中運行的狀態。因此當我們查看一個需要訪問硬件的進程時,基本都是睡眠狀態。“睡眠狀態”也可以看做就是“阻塞狀態”
(3)停止(stopped)狀態
停止狀態一般被視作掛起狀態或阻塞狀態的一種,但是linux將該狀態單獨拿出來了
我們繼續運行以下程序:
然后我們在另一個窗口中輸入“ps axj?| grep 程序名”命令:
此時它依然是“休眠狀態”。然后我們輸入“kill -l”命令,該命令可以查看kill命令所能帶的選項:
此時我們可以看到,在該命令的選項中有一個“19”號命令,可以用于暫停。然后我們輸入“kill -19 進程pid”命令。如果不知道哪個是進程pid,可以輸入“ps axj | head -1 && ps axj | grep 進程名”進行查看:
當我們輸入暫停命令后,可以看到,此時我們的程序下方就出現了“Stopped”提示,表示該進程已被暫停。然后我們輸入“ps axj | grep 進程名”命令:
此時我們就可以看到,該進程的進程狀態時“T”,即暫停狀態?
當然,既然能暫停,就可以繼續,如果我們想進程繼續運行,就可以輸入“kill -18 進程pid”來運行:
此時該進程又重新變成了“S”,即睡眠狀態
(4)深度睡眠(disk sleep)狀態
深度睡眠狀態是一種特殊的狀態,在linux中用“D”標識,與“睡眠狀態”相對應。簡單來講,“睡眠狀態”的進程我們是可以將它終止的,但是“深度睡眠狀態”的進程無法被終止。這種狀態是為了防止如磁盤這類進程被操作系統終止導致數據丟失用的。
要更好理解“深度睡眠狀態”,在這里我們舉一個場景:假設某一天,我們有一個進程A,該進程需要向磁盤寫入數據。于是進程A就帶著自己的數據地址來到磁盤,讓磁盤幫它處理。但是此時我們的內存中存在著許多進程,使得整個內存已經快滿了面臨內存不足的情況。操作系統作為管理這些的存在,為了騰出內存空間供其他進程使用,于是將很多進程都“掛起”了。
但是掛起很多進程后還是無法解決內存不足的問題,于是操作系統就開始刪除那些占著內存不干事的進程。而進程A剛好就在等磁盤寫入數據,無所事事。于是操作系統來到進程A身邊,將進程A結束了。過了一會兒磁盤寫入數據結束了,但是不知道什么原因寫入失敗,磁盤就想將寫入失敗的事實返回給進程A,但是進程A此時已經被結束了,磁盤無法返回數據。但是磁盤不能因為無法返回數據就停下不解決其他進程的問題,于是磁盤就將這些失敗數據扔掉了。
等到用戶需要這些數據時,就發現磁盤中沒有對應的數據,并且了解到是因為操作系統為了解決內存不足的問題將進程A結束了,導致無法及時發現數據未寫入。于是用戶就將進程A設置為了“深度睡眠狀態”,該狀態下的進程無法被操作系統刪除,同時也無法被用戶刪除
“深度睡眠狀態”我們在平時是很少見到的,基本只有在“高IO”或“高并發”狀態下才會看見。當然,不要覺得這個進程狀態很好用,如果一個公司的服務器中的進程出現了大量的“D”狀態,就說明該服務器正處于“崩潰”的邊緣,也就是我們平時聽到的一些公司在高訪問下的“服務器崩潰”或“宕機”。這種情況解決起來是非常困難的,因為無論是操作系統還是用戶都不會干涉這種狀態的進程。要想解決只能慢慢減輕服務器的負擔使服務器緩過來或者等程序自己醒來甚至斷電重啟。
如果大家想看看該狀態,可以輸入“dd”命令。該命令會形成一個幾十上百g的臨時文件。大家最好不要輕易嘗試該命令,因為如果你的機器空間不夠,執行該命令后你的系統很有可能會直接崩潰掛掉
(5)追蹤(tracing stop)狀態
追蹤狀態在linux下是用“t”表示,意思是該進程正在被追蹤
我們寫如下一個程序,該程序會打印4次“追蹤測試”:
然后我們對該程序進行調試,并在第8行處打上斷點:
現在我們運行該程序,該程序就會在第8行停下:
此時我們再在另一個窗口查看該進程的狀態:
可以看到,該進程的進程狀態顯示的是“t”,就是我們的追蹤狀態,此時該進程的運行正在被追蹤。而該狀態的存在就是我們能夠調試程序的原因。該狀態下程序停止,等待用戶查看此時產出的數據后后續操作
?(6)死亡(dead)狀態
該狀態在linux中用“X”表示,意思是一個進程的死亡。理解起來很簡單,就是一個進程的結束。
但是我們很難去驗證該狀態的存在,因為在linux下只要一個進程死亡,系統就會立刻或者延遲回收該進程的空間,雖然有可能延遲,但是是相對于系統速度而言的,對我們來講就是一瞬間的事
(7)僵尸(zombie)狀態
僵尸狀態是linux下一個非常特殊的狀態,用“Z”表示,用于表示一個進程已經退出了,但是它占用的資源還未釋放
舉個例子,你是一個小區的住戶,你每天都會出門鍛煉,你出門的時候都會和你的鄰居打個招呼問個好。但是有一天,你去給你這個鄰居打招呼時無論怎么喊都沒人理你,于是你推開門進去,發現你的鄰居已經不動了。看到這個情況你趕緊打電話給110和120,120的人來后查看了以下,確認這個人已經去世了。此時警察來了,封鎖了整個現場并對你的鄰居進行調查看看是什么情況。等到警察和醫院把你的鄰居的死因等等信息都獲取后,才會通知他的家屬讓他們將人帶去埋了。
在這個例子中,我們的鄰居就好比是進程,110和120就好比是該進程的父進程或操作系統。鄰居去世就是該進程結束了,但是進程雖然結束了,它的數據還會在內存中保留下來供父進程或操作系統獲取。等它們信息獲取完后,才會由父進程或操作系統將該進程所占用的資源釋放掉。而這個進程已經結束但資源未回收的狀態,就叫做“僵尸狀態”
要查看僵尸狀態也很簡單,我們寫一個子進程已經結束但父進程未結束,無法回收空間的程序即可:
該程序會創建一個進程,并根據返回值執行不同的操作,子進程會在5s后退出,但是父進程會死循環執行
等5s子進程結束后我們再查看該子進程的狀態:
此時可以看到,該子進程的狀態就是“Z+”,處于僵尸狀態。同時,該進程的名字后面還有“defunct”提示符,表示“該進程已失效”
僵尸狀態是在一般情況下是每個進程在結束時都會短暫存在的狀態,這里的短暫是以系統角度來說的,正常情況下我們是看不到的。
(8)linux下的進程狀態后的“+”含義
我們以以下一個死循環程序為例:
該程序運行后是休眠狀態“S+”,沒什么好說的:
然后現在輸入“kill -19 進程pid”將該程序暫停:
此時該程序進入了暫停狀態。然后我們再輸入“kill -18 進程pid”命令讓該程序繼續運行后并查看它的進程狀態:
這時我們就會發現,該進程雖然繼續運行了,但是它的進程狀態不是顯示的“S+”,,而是“S”。然后我們在該進程運行的窗口下按下“ctrl c”和其他各類指令:
此時我們會發現,無論我們按下多少次“ctrl c”都無法結束該進程,并且在這個窗口下我們還能執行各種命令,但是這個打印進程就是不會停止
原因就是因為此時該進程已經變成了一個后臺進程,一直都在后臺運行。“ctrl c”命令是結束前臺進程的命令,無法結束后臺進程。因此在進程符號中有“+”的就是前臺進程,沒有“+”的就是后臺進程
如果我們想結束該后臺進程,就要使用“kill -9 進程pid”命令來結束
6.僵尸進程與孤兒進程
(1)僵尸進程
僵尸進程,簡單來說就是處于僵尸狀態的進程的資源一直未被回收。雖然已經退出的進程的信息都被保存在“PCB結構體”中,但結構體的維護也是需要資源的
如果一個進程的已經退出了,但是它的父進程或操作系統一直都沒有將它所占用的資源回收,就會導致應該釋放的資源無法釋放。如果一個父進程創建了很多子進程卻不回收它的資源,就會使得內存中的可用空間越來越小,進而造成內存泄漏問題。
所以僵尸進程是我們必須要避免和解決的問題。
(2)孤兒進程
僵尸進程是父進程還未結束時,子進程先結束導致子進程的資源無法釋放。而孤兒進程則是子進程未結束,父進程先結束,導致子進程沒有人管,就可能出現當子進程結束時,其資源無法釋放。為此,linux下在這種子進程未結束而父進程先結束的情況下,會讓操作系統領養子進程,待子進程結束后的資源由操作系統來釋放
我們寫以下一個程序來測試:
現在運行該程序,該程序會循環打印子進程和父進程的pid及其ppid。然后我們再在該程序運行時查看它的狀態:
此時兩個進程都是“睡眠狀態”。現在我們輸入“kill -9 進程pid”命令,將父進程結束,并查看進程的狀態:
可以看到,此時父進程就結束了,但是子進程還在。有人可能就會奇怪,子進程結束時父進程未結束都會導致該子進程成為“僵尸進程”,但這里的父進程結束后就直接結束了,沒有成為“僵尸進程”。原因是所有的進程都是在父進程“bash”下運行的,上面的那個父進程也不例外。但是“bash”父進程和普通的父進程不一樣,它比較負責任,當它的子進程結束時,會自動釋放子進程的資源。
我們可以注意到,該子進程的狀態從“S+”變成了“S”。就這說明當子進程的父進程先結束時,未結束的子進程會變成“后臺進程”。此時的子進程無法用“ctrl c”的方法結束,只能用“kill -9 進程pid”命令來結束
如果眼尖的人就會發現,剩下的子進程的ppid變成了“1”:
我們輸入“ps ajx | head -1 && ps ajx | grep systemd”查看一下操作系統:
可以看到,操作系統的pid就是1,這就說明“當一個父進程先結束,但其子進程未結束時,該子進程會被操作系統領養”。對于該子進程后續的資源釋放也就都是有操作系統來進行
7.進程優先級
進程優先級這一概念比較簡單,并且用戶能對進程優先級的操作和干涉也是比較少的。因此進程優先級不必過多了解
(1)進程優先級的作用
優先級的概念就簡單,就是確定誰先誰后的問題。進程優先級也是同樣的。因為在計算機中,資源總歸是少數, 而要使用資源的進程才是多數。如果不確定進程優先級,將哪個進程先執行哪個進程后執行劃分出來,就可能出現混亂,導致資源被隨意占用
(2)linux下的進程優先級特點
在一般情況下,系統的進程優先級都是用一個數字來確定的。但是在linux系統中的進程的優先級是用兩個數字來設置。
至于為什么用數字來設置,就好比我們去學校食堂吃飯,當你點餐后每個窗口都會給你一個寫著你的號碼的小票,這個號碼就是廚師做你的飯的次序,也就是廚師做飯的優先級
(3)linux下的進程優先級修改
linux下的進程優先級是可以修改的。在實際修改之前,我們先寫以下一個死循環程序:
我們將這個程序運行起來后,再在另一個窗口輸入“ps -la”查看進程的優先級:
在這里面的“PRI”表示priority,即“優先級”。“NI”表示“nice”,是用于修改的優先級的。在默認情況下,linux下的普通進程的優先級“PRI”都是80,“NI”的0。
并且在linux下,進程的優先級 = 老的優先級 + NI值
現在我們輸入“top”命令修改優先級(如果使用top修改的權限不足,可以嘗試用“sudo”提權再修改):
輸入“top”命令后會出現以上界面(該界面未截全,下面還有很多進程)。在這個界面按下鍵盤上的“r”:
?就會彈出這個輸入行。在這里輸入你要修改的進程的pid(注意,要用你的字母鍵盤上面的那一行數字輸入,右邊的數字鍵盤可能無法輸入):
有以上輸入行后,就可以輸入你要設置的優先級了。在這里我們將優先級設置為-100:
設置好后按下“q”退出該界面。然后在輸入“ps -la”命令查看優先級:
可以看到,雖然我們設置的優先級是“-100”,但是該進程的優先級“PRI”僅僅變成了“60”,而“NI”則變成了“-20”
然后我們再將該進程的優先級設置成100后再來查看該進程優先級:
此時“PRI”變成了“99”,而“NI”則變成了“19”。這就說明,在linux下,用戶所能設置的優先級僅僅是40個維度,即“60~99”。這同時也說明了用戶能對進程中的優先級干涉是很少的
同時要記住,在linux下,雖然說進程的優先級 = 老的優先級 + NI值,但是這個“老的優先級”其實并不是我們設置完后的優先級值,而是其默認的80。
比如此時我們的程序優先級是99,我們再將其設置為1:
此時的優先級是“80 + 1 = 81”,而非“99 + 1 = 100”?
8.進程相關概念
(1)競爭性
一個系統的進程是非常多的,動則數十上百個。但是在我們的計算機中,CPU都是很少的,一般只有h3一個。所以進程之間為了使用資源,是具有競爭屬性的。為了更高效的完成任務,更合理的競爭相關資源,便有了優先級
(2)獨立性
在多進程運行下,每個進程獨享各種資源,多進程運行期間互不干擾。
比如我們的手機上我們可以同時打開如QQ、微信、b站等各類軟件。但是一個軟件的退出或崩潰不會影響到其他軟件。這就是因為各個進程之間具有獨立性
(3)并發
多個進程在一個CPU下采用“進程切換”的方式,在一段時間內,讓多個進程都得以推進,稱之為并發
在我們的計算機中,一個CPU在同一時間只能有一個進程運行。但是CPU并不是在運行完一個進程后再運行下一個進程。而是設置了一個“時間片”來限制。假如這個時間片是10毫秒,那么就說明每個進程都只能在CPU中運行10毫秒。時間一到,無論該進程有沒有運行完,都必須切換成下一個進程。當輪到同一個進程時就從其上次運行的地方開始再運行10毫秒,如此循環往復
(4)并行
雖然一般的計算機只有一個CPU,但有些計算機是會有2個甚至更多的CPU的。而一個CPU中同一時間只能有一個進程運行,當存在多個CPU時,就會出現多個進程同時運行的情況,這就叫做“并行”
要將“多CPU”與“多核”相區分。“多核”中的“核”指的是CPU中的內核處理器,而非CPU本身。也就是說,“多核”指的是一個CPU中有多個“內核處理器”,而非多個CPU
9.進程切換
在并發中我們說了, 進程在CPU中運行時,并不是一個進程運行結束后才運行下一個進程。而是設置一個“時間片”,每個進程都會跑時間片所規定的時間。如果時間片是10毫秒,那么每個進程都跑10毫秒,無論該進程有沒有結束。
該機制的作用就是為了防止某些進程長期占用CPU導致CPU無法執行其他進程。舉個例子,我們有時會寫一個死循環程序。當執行該程序時,如果CPU要跑完該進程才切換為其他進程的話,我們就無法進行其他任何操作,因此此時CPU已被該死循環進程占用,且CPU同一時間只能運行一個進程,無法切換為其他進程。但實際上并不會出現上述情況,原因就是有時間片的存在,導致該死循環進程執行一定時間后就被切換成其他進程了
現在我們知道了CPU會進行進程切換。但是CPU是怎么知道上一個進程執行到什么地方了呢?其實,在CPU中是存在一套寄存器的,注意是一套,而不是一個。在這套寄存器里面有著保存各類數據的寄存器。當我們的進程要切換為下一個進程時,CPU中的寄存器會將該進程執行的指令的下一條執行的地址保存下來,與此同時被保存下來的還有該進程運行所產生和需要的各類臨時數據,這些數據被存放在“PCB結構體”中,當然這一說法并不準確,“PCB結構體”中并沒有空間來保存這一數據,但現在我們暫時可以理解就保存在結構體中。
寄存器中的數據,每當一個進程加載進CPU時都會被覆蓋,因此需要有這種恢復機制。就好比我們定義一個變量,這個變量只能有一個值,當有其他值給這個變量時,原來的值就會被覆蓋
因此,當進程進行切換時,將執行到的指令地址和各類臨時數據保存下來的操作,叫做“上下文保護”。當進程在恢復運行時,通過PCB結構體將上一次執行的指令和數據恢復到寄存器中并繼續運行的操作叫做“上下文恢復”
有人可能會說,一個寄存器的空間也就4個字節或8個字節,雖說CPU中不止一個寄存器,但是我們寫程序時會定義很多個變量,有很多的值。甚至我們電腦上幾十個G的軟件都有,寄存器怎么保存的下呢?其實寄存器雖然空間小,但是其速度非常快。并且當一個程序運行時,它在某個時間段只會執行固定數量的指令或某一行代碼。而寄存器的內部只會保存當前進程在當前時間所執行的指令和需要的數據,在這些指令之前和之后的那些不需要使用的指令,寄存器都不會保存。因此,寄存器中的數據是一直變化的
四、環境變量
環境變量其實就是指“操作系統為了滿足不同的應用場景而預先在系統內設置的一大批的全局變量”。同時我們要有一個認識:“環境變量其實就是字符串”。
在了解環境變量之前,我們要先理解一個問題,就是在linux下我們自己寫的程序和程序中的指令有什么不同?我們輸入分別輸入“file /usr/bin/ls”和“file myprocess”查看系統命令ls和我們自己寫的myprocess程序:
可以看到,系統命令其實也是一個程序,我們使用系統命令時其實就是在運行一個程序。當我們運行一個程序我們自己寫的程序時,我們必須要讓系統知道該程序所在路徑,因此“./”的作用其實就是提供“相對路徑”,告訴系統該程序就在當前路徑內
但是系統命令作為一個程序,卻不需要帶上“./”。這不是因為系統不需要去找它的路徑,而是因為系統幫我們到默認路徑上去找了
因此,如果你想讓你的程序可以不帶“./”運行,就可以執行“sudo cp 程序名 /usr/bin/”命令,該命令會把你的程序放到默認路徑上
可以看到,在上圖中,我們用程序名去執行會報錯
但是當我們執行cp命令后(如果是普通用戶就需要加sudo,root用戶則不需要):
此時我們就無需帶“./”就可以執行對應程序。這也就說明了linux下的系統命令其實就是存在于“/usr/bin/”路徑下的程序
當然,并不建議大家將自己寫的程序添加到該默認路徑下,因為你自己寫的程序不夠安全,也沒有經過檢測,很可能會污染命令池
如果你想刪除在默認路徑下的程序,執行“sudo rm -f /usr/bin/程序名”即可
1.PATH環境變量
環境變量應用于系統的不同場景,我們以下面的一個例子來理解環境變量中的“PATH”
現在我們知道了為什么系統命令無需帶“./”。但是系統是如何找到對應的默認路徑的呢?這其實就是因為系統中定義了一個叫做"PATH"的環境變量
我們輸入“echo $PATH”進行查看:
可以看到,在該環境變量中存在很多路徑,我們的系統其實就是從這些路徑里面去找對應的命令,如果沒有找到就會報錯。我們的ls等命令無需帶“./”也是這些命令的所在路徑在該環境變量中
(1)將程序路徑添加進該環境變量
我們也可以將自己的程序的路徑添加到該環境變量中。執行“export PATH=$PATH:程序路徑”即可將自己的程序添加進PATH環境變量:
此時我們再執行自己的程序時,無需帶“./”就可以執行了:
注意,最好不要執行“export PATH=程序路徑”,該命令會將環境變量中的路徑覆蓋,而非添加路徑。執行后我們linux系統下的很多指令就會無法使用
當然,就算執行了也關系,因為該環境變量是一個“內存級”的環境變量,我們重登linux賬戶該環境變量就會恢復成原來的內容
當然,系統中還有許多環境變量,這里的PATH環境變量只是用來舉個例子,讓大家認識一下環境變量。其他的還有如“USER”環境變量用于識別用戶身份、“HOME”環境變量用于表示用戶家目錄、“PWD”環境變量記錄用戶當前所在路徑等等
2.添加環境變量
(1)添加本地變量
本地變量,就是指只在本地shell存在的變量。簡單來講,就是我們定義的本地變量只能在自己的進程中使用,無法被進程繼承。而環境變量,則是在該linux機器下的所有進程中都可以生效,會被進程繼承
如果我們想添加一個本地變量,可以在命令行上輸入“變量名="內容”(雙引號可帶可不帶,但建議帶上,避免你的變量中有空格等字符導致系統識別錯誤):
可以看到,通過這種方式,我們就可以直接定義一個本地變量。但是這僅僅是一個本地變量,如果我們用“env”這種搜索環境變量的命令是搜索不到的:
該指令沒有搜索到對應的環境變量
然后再來寫一個程序測試一下:
其中的myenv()函數會識別環境變量,如果該環境變量存在,就返回它的內容,否則就返回NULL
現在運行該程序:
此時輸出找不到該環境變量,這也說明了我們此時定義的僅僅是本地變量而非環境變量?
1.本地變量轉環境變量
如果我們想將本地變量改成環境變量,直接輸入“export 本地變量名”即可:
此時我們就可以搜到該本地變量了
3.取消環境變量和本地變量
如果我們不想要某個環境變量或本地變量,想取消掉,直接輸入“unset 變量名”即可:
?4.查看環境變量內容
(1)查看環境變量
如果想查看當前系統中的環境變量,直接輸入“env”命令即可:
這里并沒有截全,實際上在下面還有很多環境變量
如果你想查看單個環境變量的內容,就可以輸入“echo $環境變量”來查看指定的環境變量內容:
該環境變量中說明了我們命令行中上下翻動可記錄的最多的命令個數。如果你想看你歷史上使用過的命令,輸入“history”命令即可查看:
(2)查看本地變量和環境變量
如果我們還想看系統中的本地變量和環境變量,就可以輸入“set”命令。假設我們現在有一個“MYENV”本地變量,用“env”命令是搜索不到的。此時我們用“set”來搜索:
5.windows下的環境變量
其實不止linux有環境變量,windows也是有環境變量的
我們右擊電腦上的“此電腦”圖標,選擇屬性,點進去就可以看到有“高級系統設置”選項:
點進去后我們就可以看到“環境變量”選項:
點進“環境變量”選項就可以看見我們的電腦上的環境變量:
注意,如果沒有相關知識和技能,千萬不要嘗試修改或刪除這里的內容
6.int main()主函數中的參數
(1)int argc和char* argv[]
很多人可能不知道,我們每次寫代碼是的主函數int main()中其實是有參數的。其中一共有三個參數。我們先來介紹其中的前兩個參數
之前也說過,linux下的命令其實就是程序。但是,這些命令大家在使用過程中可以知道,是可以帶其他選項的。如“ls -l”、“ls -a”等等。既然這些程序都可以帶選項,那么按道理來講,我們自己寫的程序也是可以帶選項的。
因此,我們先寫下面一個程序來看看參數“char*argv[]”中有什么:
我們執行該程序后可以看到打印了如下內容:
?argv[0]上的內容就是我們剛才執行的命令。此時我們在帶上幾個選項來運行該程序:
可以看到,此時argv[]中就是我們輸入的命令行中的參數。那此時“int argc”和“char* argv[]”的作用就很明顯了。argc是用來表明命令中參數的數量的,而argv則是用來存儲參數的。在此時我們的輸入在命令行的內容如“./myprocess -a -b -c”被看做是字符串,當進入主函數后,這個字符串被分割為“./myprocess”、“-a”、“-b”、“-c”一個個小字符串
既然argv中保存了選項,那我們再修改下程序:
此時我們執行該程序并帶上對應的選項:
可以看到,該程序成功的幫我們把對應的內容打印出來了。這也就進一步的說明了,其實linux中的命令就是用C語言寫的程序,并且我們自己寫的程序也是可以帶上選項執行對應功能的,只是我們以前沒有場景使用罷了
(2)char* env[]
對于這個參數,我們從名字上來看就很眼熟,因為查看環境變量的命令就是“env”。那么我們可以推測該參數是不是和環境變量有關。
我們寫以下一個程序來測試一下:
這里因為env[]沒有標識其內容個數的變量,因此直接用的是"env[i]"當判斷條件。但是這是因為env[]的最后一個字符默認指向“NULL”才能這樣用
?我們現在來運行一下程序:
其結果我們也很眼熟,我們再執行“env”查看環境變量:
它們的內容一模一樣。這也就說明了,主函數中的“env[]”是用來存儲環境變量的。
因此,每個進程都會收到一份環境表,環境表是一個字符指針數組,每個指針指向一個以'‘\0’結尾的環境字符串。同時存儲這些環境變量的是char*數組。這就是為什么我們說“環境變量其實就是字符串”
五、進程地址空間
1.C/C++地址空間
在了解進程地址空間之前,我們先來回顧下C/C++的地址空間。大家應該都知道,C和C++的地址空間被分為代碼區、堆區、棧區等多個區域:
在堆區和棧區之間是有一個公共空間的,供雙方使用
當時我們可能以為這些地址空間就是內存上的內存。但是實際上它們并不是內存。在了解它們就是是什么之前,我們先來寫一個以下內容的程序:
該程序會創建一個子進程,并根據I變量“id”的返回值執行不同的操作。同時這里面定義了一個全局變量“global_value”,該變量在3s后會被子進程修改為300。并且父子進程選項中會打印其自己的pid、ppid和全局變量“global_value”的值及其地址
現在我們來執行該程序:
根據打印內容我們可以發現,在3s之前,父子進程打印的全局變量的值和地址都是相同的。這很正常。但是當3s后子進程將全局變量修改后,我們發現子進程打印的值變成了300,但是父進程打印的值依然是100。并且更其奇怪的是,此時父子進程打印的全局變量的地址竟然是一樣的
我們之前說過,同一個地址上是不能同時存在不同的值的。但此時在同一個地址上卻出現了不同的值。這就說明,此時我們打印出來的地址并不是內存上真正的空間地址,而是“虛擬地址”。我們以前學習C/C++語言時打印的所有地址其實都不是內存上的物理地址,而是虛擬地址。“虛擬地址”也被叫做“線性地址”或“邏輯地址”
而我們以前所說的C/C++地址空間其實就是“虛擬地址空間”。虛擬地址空間的存在也使我們更好的支持了并發
2.虛擬地址空間
(1)進程認為自身獨占系統資源
現在我們知道了在內存中存在虛擬地址空間。在理解虛擬地址空間之前,我們要先知道一個概念,“進程會認為它獨占系統資源”。當然,在實際上進程并沒有獨占系統資源。
舉個例子,假如現在有一個有錢人,身價上百億。而他自己比較喜歡花天酒地,在外面有三個私生子。這三個私生子之間互不認識。有一天這個有錢人分別對他的三個兒子說,“你好好學習好好工作,等我去世以后就把遺產全部給你”。此時它的三個都非常高興,因為此時他們都認為自己會繼承父親的所有遺產。于是這三個兒子需要用錢時都會向付錢要錢。雖然他們知道自己將來能繼承全部遺產,但現在還沒有繼承,所以不會無限度的要錢。而父親為了不讓他們亂花錢,就設置了一個限度,超過這個限度就不會給他們錢。
在這個例子里面,三個兒子就好比是“進程”,有錢人就好比是“內存”。三個兒子都認為自己會繼承全部遺產就好比是“進程認為自己獨占內存所有資源”。兒子向父親要錢就是“進程向內存申請空間”。父親設置的給錢的額度就是“內存給進程設置的申請空間的最大值”。而父親給三個兒子畫的繼承遺產的大餅就是“進程地址空間”
因此簡單來講,進程地址空間就是“操作系統給進程畫的大餅”
(2)系統如何使進程認為自己獨占所有資源
既然操作系統要讓進程誤認為自己獨占所有系統資源,那肯定要有一個方法。我們之前講過操作系統的管理方式是“先描述,再組織”,并且也了解了“PCB進程結構體”。而操作系統誤導進程的方法也是相似的。在操作系統中,會構建一個struct結構體,該結構體中就包含了操作系統對進程畫的餅的所有內容。
同時在系統中有幾十上百個進程,操作系統為了避免遺忘或弄錯給進程畫的餅,就會在每個進程中都放入一個結構體,用該結構體來記憶操作系統給進程畫的餅
因此進程地址空間的本質就是“是內核的一種數據結構,叫做mm_struct”
3.虛擬地址
在之前我們說進程地址空間的本質是“內核的一種叫做mm_struct的數據結構體”。既然是數據結構體體,那肯定就有結構體成員
在以前的學習中,想必大家都知道,地址空間描述的基本空間大小是字節。即一個地址代表一個字節。
我們今天以32位系統來舉例,假設我們現在有一個32位的系統,那么在這個系統下, 就會有2^32次方個地址。而一個地址代表一個字節,就說明在32位的系統下有4GB的空間。而這2^32次方個地址就是用unsigned int類型來表示的。因為該類型所占空間大小為4字節,共32個bit位,就能夠保證2^32個地址都有唯一性
同時要注意,在系統中地址是從低地址向高地址使用的
(1)進程地址空間的區域劃分
1.區域劃分
我們說過在內存中劃分有堆區和棧區。但是我們并不了解堆區和棧區是如何劃分的。在這里,就舉一個例子來理解堆區與棧區的劃分
假設今天有小李和小王兩個人是同桌,他們之間經常玩鬧,有一天小李把小王惹生氣了,于是小王和小李絕交并拿起了筆,在桌子上畫了一條線,說這就是他們之間的“三八線”,各自用各自的區域不準越界。小李此時這種在雙方之間劃分使用區域的方式就叫做“區域劃分”
2.區域調整
當小李和小王過了一段時間后,小李安耐不住想和小王玩,但又苦于這條三八線,于是找小王提建議說能不能在中間劃分一個公共區域,大家就在這個公共區域玩。小王此時也沒有那么生氣了,也想和小李玩,就答應了小李的請求。于是小王拿起筆,在雙方直接劃了一個公共區域,之前是小李小王各50cm,現在就改成小李小王各45cm,中間的10cm作為他們兩個之間的公共玩耍區域。此時這種劃分公共區域的方法就叫做“區域調整”
3.區域擴大
小李和小王和好一段時間后,小李逐漸得意忘形,有一天又把小王惹生氣了, 并且比第一次還嚴重。于是小王此時怒不可遏,又拿出尺子來劃分“三八線”。這次小王就沒有再用評分的原則,而是直接縮減小李的空間,改成小李30cm,自己70cm。這種擴大自己空間的方式就叫做“空間擴大”
在系統中,要實現對空間的劃分,同樣會形成一個結構體。我們假設這個結構體叫做“struct Destop”,那么該結構體內就是保存了各個空間的起始地址和結束地址:
由此,我們的地址空間也是用同樣的方法進行了劃分。在32位系統的進程地址空間中有4GB空間,這些空間就大致如下劃分:
?當然,這里并沒有全部寫完, 只是寫了部分區域劃分
(2)虛擬地址
現在我們知道了在系統中有一個叫“struct mm_struct”的內核結構體。那么進程在運行時,就會需要依靠這個結構體來生成一份進程地址空間。假設我們現在有一個進程,該進程的PCB結構體“task_struct”中有一個“mm”指針,該指針指向一塊malloc出來的“struct mm_struct”類型的空間:
而這塊空間中,當然要有對應的區域劃分,我們假設區域是如下方式劃分的:
在這里面的如0x1111 1111到0x1222 2222這種區域劃分出來的地址就叫做“區域起始地址”和“區域結束地址”,而這些地址全部都是“虛擬地址”。并且這些虛擬地址的數量一定要是2^32個
也就是說,我們的進程中都有一塊虛擬空間,這些虛擬空間中全部都是虛擬地址,而這些虛擬地址就是給我們的代碼、數據、堆區、棧區等各個空間使用的。要注意的是,如代碼區、數據區這些區域的大小是固定不變的。但是棧區和堆區的大小是會改變的。而結合上面所說的,堆區和棧區等的空間變化其本質上就是修改結構體中對應區域的起始地址和結束地址
而我們之前說的操作系統會進程畫餅,讓操作系統誤以為自己獨占所有資源。這個餅其實就是我們上面的mm,即我們的虛擬地址空間
4.頁表
雖然進程中用的是虛擬進程空間和虛擬地址,但是進程最終還是需要存在內存中,使用物理地址的。要使用物理地址,就需要用虛擬地址找到物理地址,而虛擬地址找到物理地址的媒介,其實就是頁表
假設現在我們有一個磁盤,在這個磁盤中有一個test.exe程序,該程序加載到內存中要占用1k字節的空間。這個進程的在進入內存時,會對應生成一個“PCB結構體”,該結構體中的mm指針指向一塊malloc出來的進程地址空間。假設此時這個進程想要定義一個char ch = 1,需要一個字節的空間。此時該空間會先有一個虛擬地址,然后再通過頁表,將虛擬地址映射到物理地址上,此時才完成了定義
當然,實際的頁表并不是一個虛擬地址對應一個物理地址。因為我們假設一個地址占四個字節,找一次地址就要2個地址,也就是8個字節。而32位系統下一共有2^32個地址,再乘以8,就需要32GB空間,這樣僅僅一個頁表就比內存都大了。使用在系統中的頁表實際是非常復雜的,采用了樹狀結構來減少空間使用。此處只是為了方便認識頁表才這樣畫?
注意,頁表是每個進程都有的,而非在系統只有一個頁表:
5.虛擬地址空間存在原因
有人可能認為,直接讓進程訪問物理地址而不是從虛擬地址通過頁表映射到物理地址上會更加方便。誠然,這樣確實更方便,但是也會存在一定問題
(1)保護其他進程和用戶信息
首先虛擬地址空間就是為了保護其他進程和用戶信息。假設我們現在沒有虛擬地址空間,進程可以直接訪問物理內存。如果該進程中存在越界訪問的問題,并且這個進程的旁邊是另一個進程的數據,此時就會導致其他進程的數據有遭到修改的風險
同時,如果你的電腦上有一個惡意程序,該程序運行起來直接就訪問了你的物理內存,而你的物理內存上存在許多信息,包括你的各類用戶信息。那么此時該進程就可以隨意訪問你的信息,并將其返回到程序中供他人非法獲取
有人就很奇怪了, 雖然進程是通過虛擬地址找到物理地址的,但最終還是要到物理地址上去,那虛擬地址如何保護我們的數據呢?其實這一保護功能并不是由虛擬地址完成,而是由“頁表”完成。不要簡單的認為頁表只能提供映射,其實它還存在許多其他功能,就包括對進程行為的識別
舉個例子,我們小時候都得到過壓歲錢,這些壓歲錢在我們手里,我們可以直接用,想買什么買什么。但是這個時候我們的父母通常會過來,告訴我們說把壓歲錢交給他們保管。我們上交之后,每次想買點零食時,父母都會從我們的壓歲錢里面拿點出來給我們。有一天,我們想買本漫畫書,找父母要錢,此時他們就以妨礙學習為由,不給我們錢。
在這里面,我們就是進程,父母就是頁表。父母把壓歲錢收管,根據我們的需求來決定是否給我們錢就是頁表在對進程的行為進行識別,判斷是否合法
(2)保證進程的獨立性
我們之前寫過這樣一個程序:
該程序重復打印子進程和父進程的pid、ppid和一個全局變量"global_value"的值。并且子進程會在3s后修改global_value的值:
雖然全局變量“global_value”的值被改變了,但是在父子進程打印的全局變量的地址并沒有改變的情況下,父子進程打印的gobal_value的值卻不同。導致這個結果的原因就是“進程具有獨立性”
在內存中,每個進程都有其獨立的內核數據結構,父子進程也不例外:
?我們說了父子進程因為其代碼都是相同的,因此是“共享代碼”的。也就是說,這兩個進程在全局變量"gobal_value"未被修改時,父子進程通過虛擬地址找到的都是同一塊空間上的同一個值:
但是在過了3s后,此時子進程會修改“gobal_value”的值,如果修改原空間上的值,勢必會導致父進程的值也會被修改。就無法保證父子進程的獨立性
因此某一個進程要對共享代碼中的數據進行修改時,會先將原內存中的內容拷貝一份,然后在物理內存中重新找一塊空間將內容拷貝進去。再修改頁表將對應虛擬地址映射到物理地址,然后再修改這塊空間上的值
這種任何一方嘗試寫入或修改數據,?操作系統先進行數據拷貝,更改頁表映射然后再讓程序進行修改行為,叫做“寫時拷貝”。父子進程就是通過“寫時拷貝”的方式來實現不同進程的數據分離。這是由操作系統來幫我們做的
對于那種毫不相干的進程,他們之間的內核數據結構和進程對應的代碼和數據都是獨立的,這也就保證了數據之間的獨立性
因此,地址空間的存在,可以更方便的進行進程和進程的數據代碼的解耦,保證進程之間的獨立性
(3)便于編譯器以統一視角編譯代碼
我們一直說每個程序加載到內存中會有一個PCB結構體,里面保存了關于該進程的各類屬性和代碼數據地址。但是,不僅僅是程序加載到內存中會有一塊虛擬地址空間保存虛擬地址,程序在加載到內存之前也是有地址的。這個地址也是虛擬地址,不過我們通常叫做邏輯地址
很簡單的一個道理,我們自己寫的程序,在我們編譯運行時我們說了,函數調用時要通過函數的地址去找到對應的函數聲明,而這里的函數地址并不是加載到內存中才有的,而是在我們代碼進行編譯的時候就有了。換句話說,我們的程序在進入內存變成進程之前它的內部就已經有一套虛擬地址了。這套虛擬地址和進程結構體中的虛擬進程空間采用同樣的方式,都是有2^32個地址
舉個例子。假設我們現在有一個my.exe程序,這個程序的代碼如下圖所示:
當我們寫好這個程序,將該程序進行編譯時,該程序的內部就會自動為每個函數、每行代碼乃至每個變量都生成一個虛擬地址,并且里面調用了函數或者使用了其他變量的代碼的地址就是其對應的定義處的地址:
當我們的程序運行起來加載到內存里面時,該進程本身就又有了一套地址。注意這里該進程其實有兩套地址,一套是進程內部進行跳轉的虛擬地址,另一套是標識該進程在物理內存中存在的物理地址
此時我們要意識到,當程序加載進內存時,會形成一個PCB結構體,這個結構體中有一個mm指針指向開辟的進程地址空間,該地址空間中會用多個start、end值來標識各個區域的空間劃分。我們以代碼段為例,上述程序中的代碼都需要保存在代碼區中,而代碼區的大小就是主函數的代碼大小,我們圖中的主函數是從0x1111 1111開始的,我們假設該代碼的大小是10kb,那么代碼區的結束位置就是“0x1111 1111?+ 10kb”。
有了這些準備后我們再來進程與CPU的尋址問題。這里要記住,加載到CPU中的是進程的PCB結構體,而非進程本身。假設該程序此時運行到主函數中的func()函數調用處,此時系統就會將func()函數的虛擬地址0x1122 2222加載到CPU中,CPU再通過該地址找到該進程的進程地址空間的對應位置,然后通過進程地址空間與頁表的映射找到物理內存中的代碼存儲位置,然后去調用func()函數。func()中的a變量也是同理。要調用a變量,就要將a變量的虛擬地址加載到CPU中,CPU通過該地址找到該進程中的進程地址空間中的a變量的位置,通過這個地址與頁表映射找到物理內存中a的物理地址進行調用
在這整個過程中,CPU都知道對應代碼的虛擬地址,并沒有見到過它的物理地址。而程序中自行形成的虛擬地址的格式與地址位置和進程中的進程結構體指向的進程地址空間是差不多的。也就是說,程序中的虛擬地址與進程地址空間的虛擬地址在一般情況下是一樣的。這樣也就便于CPU直接將對應虛擬地址放到進程地址空間去進行頁表映射?
因此,進程地址空間存在的另一個重要原因就是“讓進程以同一的視角,來看待進程對應的代碼和數據等各個區域,方便使用。同時也讓編譯器以統一的視角來編譯代碼。”這樣,代碼編譯完后,就可以直接使用了
5.線性地址與邏輯地址
(1)線性地址
我們之前說,虛擬地址又叫做線性地址。因為虛擬地址是從0一直到2^32,是連續不斷的。因此在一些教材里面,虛擬地址又被叫做“線性地址”
(2)邏輯地址
邏輯地址其實就是在程序內部的用于代碼跳轉的地址。只不過在linux中我們說的邏輯地址和虛擬地址是一個東西。但是虛擬地址的名字聽起來更好理解,所以在文中用的都是虛擬地址,但是在實際中常用的名字是邏輯地址
總結
以上是生活随笔為你收集整理的初识linux之进程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQL检索语句
- 下一篇: 有一个字符串,如11.2美元34人民币;