文章目錄
- 基本概念
- 描述行程-PCB
- task_struct-PCB的一種
- task_struct內容分類
- 查看行程
- 通過系統目錄查看
- 通過ps命令查看
- 通過系統呼叫獲取行程的PID和PPID
- 通過系統呼叫創建行程- fork初始
- fork函式創建子行程
- 使用if進行分流
- Linux行程狀態
- 運行狀態-R
- 淺度睡眠狀態-S
- 深度睡眠狀態-D
- 暫停狀態-T
- 僵尸狀態-Z
- 死亡狀態-X
- 僵尸行程
- 僵尸行程的危害
- 孤兒行程
- 行程優先級
- 基本概念
- 查看系統行程
- PRI與NI
- 查看行程優先級資訊
- 通過top命令更改行程的nice值
- 通過renice命令更改行程的nice值
- 四個重要概念
- 環境變數
- 基本概念
- 常見環境變數
- 查看環境變數的方法
- 測驗PATH
- 測驗HOME
- 測驗SHELL
- 和環境變數相關的命令
- 環境變數的組織方式
- 通過代碼獲取環境變數
- 通過系統呼叫獲取環境變數
- 程式地址空間
- 行程地址空間
- Linux2.6內核行程調度佇列
- 一個CPU擁有一個runqueue
- 優先級
- 活動佇列
- 過期佇列
- active指標和expired指標
基本概念
課本概念: 程式的一個執行實體,正在執行的程式等,
內核觀點: 擔當分配系統資源(CPU時間,記憶體)的物體,
只要寫過代碼的都知道,當你的代碼進行編譯鏈接后便會生成一個可執行程式,這個可執行程式本質上是一個檔案,是放在磁盤上的,當我們雙擊這個可執行程式將其運行起來時,本質上是將這個程式加載到記憶體當中了,因為只有加載到記憶體后,CPU才能對其進行逐行的陳述句執行,而一旦將這個程式加載到記憶體后,我們就不應該將這個程式再叫做程式了,嚴格意義上將應該將其稱之為行程,

描述行程-PCB
系統當中可以同時存在大量行程,使用命令ps aux便可以顯示系統當中存在的行程,

而當你開機的時候啟動的第一個程式就是我們的作業系統(即作業系統是第一個加載到記憶體的),我們都知道作業系統是做管理作業的,而其中就包括了行程管理,而系統內是存在大量行程的,那么作業系統是如何對行程進行管理的呢?
這時我們就應該想到管理的六字真言:先描述,再組織,作業系統管理行程也是一樣的,作業系統作為管理者是不需要直接和被管理者(行程)直接進行溝通的,當一個行程出現時,作業系統就立馬對其進行描述,之后對該行程的管理實際上就是對其描述資訊的管理,
行程資訊被放在一個叫做行程控制塊的資料結構中,可以理解為行程屬性的集合,課本上稱之為PCB(process control block),
作業系統將每一個行程都進行描述,形成了一個個的行程控制塊(PCB),并將這些PCB以雙鏈表的形式組織起來,

這樣一來,作業系統只要拿到這個雙鏈表的頭指標,便可以訪問到所有的PCB,此后,作業系統對各個行程的管理就變成了對這條雙鏈表的一系列操作,
例如創建一個行程實際上就是先將該行程的代碼和資料加載到記憶體,緊接著作業系統對該行程進行描述形成對應的PCB,并將這個PCB插入到該雙鏈表當中,而退出一個行程實際上就是先將該行程的PCB從該雙鏈表當中洗掉,然后作業系統再將記憶體當中屬于該行程的代碼和資料進行釋放或是置為無效,
總的來說,作業系統對行程的管理實際上就變成了對該雙鏈表的增、刪、查、改等操作,
task_struct-PCB的一種
行程控制塊(PCB)是描述行程的,在C++當中我們稱之為面向物件,而在C語言當中我們稱之為結構體,既然Linux作業系統是用C語言進行撰寫的,那么Linux當中的行程控制塊必定是用結構體來實作的,
- PCB實際上是對行程控制塊的統稱,在Linux中描述行程的結構體叫做task_struct,
- task_struct是Linux內核的一種資料結構,它會被裝載到RAM(記憶體)里并且包含行程的資訊,
task_struct內容分類
task_struct就是Linux當中的行程控制塊,task_struct當中主要包含以下資訊:
- 標示符: 描述本行程的唯一標示符,用來區別其他行程,
- 狀態: 任務狀態,退出代碼,退出信號等,
- 優先級: 相對于其他行程的優先級,
- 程式計數器(pc): 程式中即將被執行的下一條指令的地址,
- 記憶體指標: 包括程式代碼和行程相關資料的指標,還有和其他行程共享的記憶體塊的指標,
- 背景關系資料: 行程執行時處理器的暫存器中的資料,
- I/O狀態資訊: 包括顯示的I/O請求,分配給行程的I/O設備和被行程使用的檔案串列,
- 記賬資訊: 可能包括處理器時間總和,使用的時鐘總和,時間限制,記賬號等,
- 其他資訊,
查看行程
通過系統目錄查看
在根目錄下有一個名為proc的系統檔案夾,

檔案夾當中包含大量行程資訊,其中有些子目錄的目錄名為數字,

這些數字其實是某一行程的PID,對應檔案夾當中記錄著對應行程的各種資訊,我們若想查看PID為1的行程的行程資訊,則查看名字為1的檔案夾即可,

通過ps命令查看
單獨使用ps命令,會顯示所有行程資訊,
[cl@VM-0-15-centos dir2]$ ps aux

ps命令與grep命令搭配使用,即可只顯示某一行程的資訊,
[cl@VM-0-15-centos dir2]$ ps aux | head -1 && ps aux | grep proc | grep -v grep

通過系統呼叫獲取行程的PID和PPID
通過使用系統呼叫函式,getpid和getppid即可分別獲取行程的PID和PPID,
我們可以通過一段代碼來進行測驗,

當運行該代碼生成的可執行程式后,便可回圈列印該行程的PID和PPID,

我們可以通過ps命令查看該行程的資訊,即可發現通過ps命令得到的行程的PID和PPID與使用系統呼叫函式getpid和getppid所獲取的值相同,

通過系統呼叫創建行程- fork初始
fork函式創建子行程
fork是一個系統呼叫級別的函式,其功能就是創建一個子行程,
例如,運行以下代碼:

若是代碼當中沒有fork函式,我們都知道代碼的運行結果就是回圈列印該行程的PID和PPID,而加入了fork函式后,代碼運行結果如下:

運行結果是回圈列印兩行資料,第一行資料是該行程的PID和PPID,第二行資料是代碼中fork函式創建的子行程的PID和PPID,我們可以發現fork函式創建的行程的PPID就是proc行程的PID,也就是說proc行程與fork函式創建的行程之間是父子關系,
每出現一個行程,作業系統就會為其創建PCB,fork函式創建的行程也不例外,

我們知道加載到記憶體當中的代碼和資料是屬于父行程的,那么fork函式創建的子行程的代碼和資料又從何而來呢?
我們看看以下代碼的運行結果:

運行結果:

實際上,使用fork函式創建子行程,在fork函式被呼叫之前的代碼被父行程執行,而fork函式之后的代碼,則默認情況下父子行程都可以執行,需要注意的是,父子行程雖然代碼共享,但是父子行程的資料各自開辟空間(采用寫時拷貝),
小貼士: 使用fork函式創建子行程后就有了兩個行程,這兩個行程被作業系統調度的順序是不確定的,這取決于作業系統調度演算法的具體實作,
使用if進行分流
上面說到,fork函式創建出來的子行程與其父行程共同使用一份代碼,但我們如果真的讓父子行程做相同的事情,那么創建子行程就沒有什么意義了,
實際上,在fork之后我們通常使用if陳述句進行分流,即讓父行程和子行程做不同的事,
fork函式的回傳值:
1、如果子行程創建成功,在父行程中回傳子行程的PID,而在子行程中回傳0,
2、如果子行程創建失敗,則在父行程中回傳 -1,
既然父行程和子行程獲取到fork函式的回傳值不同,那么我們就可以據此來讓父子行程執行不同的代碼,從而做不同的事,
例如,以下代碼:

fork創建出子行程后,子行程會進入到 if 陳述句的回圈列印當中,而父行程會進入到 else if 陳述句的回圈列印當中,

Linux行程狀態
一個行程從創建而產生至撤銷而消亡的整個生命期間,有時占有處理器執行,有時雖可運行但分不到處理器,有時雖有空閑處理器但因等待某個時間的發生而無法執行,這一切都說明行程和程式不相同,行程是活動的且有狀態變化的,于是就有了行程狀態這一概念,

這里我們具體談一下Linux作業系統中的行程狀態,Linux作業系統的源代碼當中對于行程狀態有如下定義:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
小貼士: 行程的當前狀態是保存到自己的行程控制塊(PCB)當中的,在Linux作業系統當中也就是保存在task_struct當中的,
在Linux作業系統當中我們可以通過 ps aux 或 ps axj 命令查看行程的狀態,
[cl@VM-0-15-centos ~]$ ps aux

[cl@VM-0-15-centos ~]$ ps axj

運行狀態-R
一個行程處于運行狀態(running),并不意味著行程一定處于運行當中,運行狀態表明一個行程要么在運行中,要么在運行佇列里,也就是說,可以同時存在多個R狀態的行程,
小貼士: 所有處于運行狀態,即可被調度的行程,都被放到運行佇列當中,當作業系統需要切換行程運行時,就直接在運行佇列中選取行程運行,
淺度睡眠狀態-S
一個行程處于淺度睡眠狀態(sleeping),意味著該行程正在等待某件事情的完成,處于淺度睡眠狀態的行程隨時可以被喚醒,也可以被殺掉(這里的睡眠有時候也可叫做可中斷睡眠(interruptible sleep)),
例如執行以下代碼:

代碼當中呼叫sleep函式進行休眠100秒,在這期間我們若是查看該行程的狀態,則會看到該行程處于淺度睡眠狀態,
[cl@VM-0-15-centos stat]$ ps aux | head -1 && ps aux | grep proc | grep -v grep

而處于淺度睡眠狀態的行程是可以被殺掉的,我們可以使用kill命令將該行程殺掉,

深度睡眠狀態-D
一個行程處于深度睡眠狀態(disk sleep),表示該行程不會被殺掉,即便是作業系統也不行,只有該行程自動喚醒才可以恢復,該狀態有時候也叫不可中斷睡眠狀態(uninterruptible sleep),處于這個狀態的行程通常會等待IO的結束,
例如,某一行程要求對磁盤進行寫入操作,那么在磁盤進行寫入期間,該行程就處于深度睡眠狀態,是不會被殺掉的,因為該行程需要等待磁盤的回復(是否寫入成功)以做出相應的應答,(磁盤休眠狀態)
暫停狀態-T
在Linux當中,我們可以通過發送SIGSTOP信號使行程進入暫停狀態(stopped),發送SIGCONT信號可以讓處于暫停狀態的行程繼續運行,
例如,我們對一個行程發送SIGSTOP信號,該行程就進入到了暫停狀態,

我們再對該行程發送SIGCONT信號,該行程就繼續運行了,

小貼士: 使用kill命令可以列出當前系統所支持的信號集,
[cl@VM-0-15-centos stat]$ kill -l

僵尸狀態-Z
當一個行程將要退出的時候,在系統層面,該行程曾經申請的資源并不是立即被釋放,而是要暫時存盤一段時間,以供作業系統或是其父行程進行讀取,如果退出資訊一直未被讀取,則相關資料是不會被釋放掉的,一個行程若是正在等待其退出資訊被讀取,那么我們稱該行程處于僵尸狀態(zombie),
首先,僵尸狀態的存在是必要的,因為行程被創建的目的就是完成某項任務,那么當任務完成的時候,呼叫方是應該知道任務的完成情況的,所以必須存在僵尸狀態,使得呼叫方得知任務的完成情況,以便進行相應的后續操作,
例如,我們寫代碼時都在主函式最后回傳0,

實際上這個0就是回傳給作業系統的,告訴作業系統代碼順利執行結束,在Linux作業系統當中,我們可以通過使用echo $?命令獲取最近一次行程退出時的退出碼,
[cl@VM-0-15-centos exitcode]$ echo $?

小貼士: 行程退出的資訊(例如退出碼),是暫時被保存在其行程控制塊當中的,在Linux作業系統中也就是保存在該行程的task_struct當中,
死亡狀態-X
死亡狀態只是一個回傳狀態,當一個行程的退出資訊被讀取后,該行程所申請的資源就會立即被釋放,該行程也就不存在了,所以你不會在任務串列當中看到死亡狀態(dead),
僵尸行程
前面說到,一個行程若是正在等待其退出資訊被讀取,那么我們稱該行程處于僵尸狀態,而處于僵尸狀態的行程,我們就稱之為僵尸行程,
例如,對于以下代碼,fork函式創建的子行程在列印5次資訊后會退出,而父行程會一直列印資訊,也就是說,子行程退出了,父行程還在運行,但父行程沒有讀取子行程的退出資訊,那么此時子行程就進入了僵尸狀態,
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(count){
printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child quit...\n");
exit(1);
}
else if(id > 0){ //father
while(1){
printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else{ //fork error
}
return 0;
}
運行該代碼后,我們可以通過以下監控腳本,每隔一秒對該行程的資訊進行檢測,
[cl@VM-0-15-centos zombie]$ while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
檢測后即可發現,當子行程退出后,子行程的狀態就變成了僵尸狀態,

僵尸行程的危害
- 僵尸行程的退出狀態必須一直維持下去,因為它要告訴其父行程相應的退出資訊,可是父行程一直不讀取,那么子行程也就一直處于僵尸狀態,
- 僵尸行程的退出資訊被保存在task_struct(PCB)中,僵尸狀態一直不退出,那么PCB就一直需要進行維護,
- 若是一個父行程創建了很多子行程,但都不進行回收,那么就會造成資源浪費,因為資料結構物件本身就要占用記憶體,
- 僵尸行程申請的資源無法進行回收,那么僵尸行程越多,實際可用的資源就越少,也就是說,僵尸行程會導致記憶體泄漏,
孤兒行程
在Linux當中的行程關系大多數是父子關系,若子行程先退出而父行程沒有對子行程的退出資訊進行讀取,那么我們稱該行程為僵尸行程,但若是父行程先退出,那么將來子行程進入僵尸狀態時就沒有父行程對其進行處理,此時該子行程就稱之為孤兒行程,
若是一直不處理孤兒行程的退出資訊,那么孤兒行程就會一直占用資源,此時就會造成記憶體泄漏,因此,當出現孤兒行程的時候,孤兒行程會被1號init行程領養,此后當孤兒行程進入僵尸狀態時就由int行程進行處理回收,
例如,對于以下代碼,fork函式創建的子行程會一直列印資訊,而父行程在列印5次資訊后會退出,此時該子行程就變成了孤兒行程,
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(1){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
sleep(1);
}
}
else if(id > 0){ //father
int count = 5;
while(count){
printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("father quit...\n");
exit(0);
}
else{ //fork error
}
return 0;
}
觀察代碼運行結果,在父行程未退出時,子行程的PPID就是父行程的PID,而當父行程退出后,子行程的PPID就變成了1,即子行程被1號行程領養了,

行程優先級
基本概念
什么是優先級?
優先級實際上就是獲取某種資源的先后順序,而行程優先級實際上就是行程獲取CPU資源分配的先后順序,就是指行程的優先權(priority),優先權高的行程有優先執行的權力,
優先級存在的原因?
優先級存在的主要原因就是資源是有限的,而存在行程優先級的主要原因就是CPU資源是有限的,一個CPU一次只能跑一個行程,而行程是可以有多個的,所以需要存在行程優先級,來確定行程獲取CPU資源的先后順序,
查看系統行程
在Linux或者Unix作業系統中,用ps -l命令會類似輸出以下幾個內容:
[cl@VM-0-15-centos pri]$ ps -l

列出的資訊當中有幾個重要的資訊,如下:
- UID:代表執行者的身份,
- PID:代表這個行程的代號,
- PPID:代表這個行程是由哪個行程發展衍生而來的,亦即父行程的代號,
- PRI:代表這個行程可被執行的優先級,其值越小越早被執行,
- NI:代表這個行程的nice值,
PRI與NI
- PRI代表行程的優先級(priority),通俗點說就是行程被CPU執行的先后順序,該值越小行程的優先級別越高,
- NI代表的是nice值,其表示行程可被執行的優先級的修正數值,
- PRI值越小越快被執行,當加入nice值后,將會使得PRI變為:PRI(new) = PRI(old) + NI,
- 若NI值為負值,那么該行程的PRI將變小,即其優先級會變高,
- 調整行程優先級,在Linux下,就是調整行程的nice值,
- NI的取值范圍是-20至19,一共40個級別,
注意: 在Linux作業系統當中,PRI(old)默認為80,即PRI = 80 + NI,
查看行程優先級資訊
當我們創建一個行程后,我們可以使用ps -al命令查看該行程優先級的資訊,
[cl@VM-0-15-centos pri]$ ps -al

注意: 在Linux作業系統中,初始行程一般優先級PRI默認為80,NI默認為0,
通過top命令更改行程的nice值
top命令就相當于Windows作業系統中的任務管理器,它能夠動態實時的顯示系統當中行程的資源占用情況,

使用top命令后按“r”鍵,會要求你輸入待調整nice值的行程的PID,

輸入行程PID并回車后,會要求你輸入調整后的nice值,

輸入nice值后按“q”即可退出,如果我們這里輸入的nice值為10,那么此時我們再用ps命令查看行程的優先級資訊,即可發現行程的NI變成了10,PRI變成了90(80+NI),

注意: 若是想將NI值調為負值,也就是將行程的優先級調高,需要使用sudo命令提升權限,
通過renice命令更改行程的nice值
使用renice命令,后面跟上更改后的nice值和行程的PID即可,

之后我們再用ps命令查看行程的優先級資訊,也可以發現行程的NI變成了10,PRI變成了90(80+NI),

注意: 若是想使用renice命令將NI值調為負值,也需要使用sudo命令提升權限,
四個重要概念
競爭性: 系統行程數目眾多,而CPU資源只有少量,甚至1個,所以行程之間是具有競爭屬性的,為了高效完成任務,更合理競爭相關資源,便有了優先級,
獨立性: 多行程運行,需要獨享各種資源,多行程運行期間互不干擾,
并行: 多個行程在多個CPU下分別同時進行運行,這稱之為并行,
并發: 多個行程在一個CPU下采用行程切換的方式,在一段時間之內,讓多個行程都得以推進,稱之為并發,
環境變數
基本概念
環境變數(environment variables)一般是指在作業系統中用來指定作業系統運行環境的一些引數,
例如,我們撰寫的C/C++代碼,在各個目標檔案進行鏈接的時候,從來不知道我們所鏈接的動靜態庫在哪里,但是照樣可以鏈接成功,生成可執行程式,原因就是有相關環境變數幫助編譯器進行查找,
環境變數通常具有某些特殊用途,并且在系統當中通常具有全域特性,
常見環境變數
- PATH: 指定命令的搜索路徑,
- HOME: 指定用戶的主作業目錄(即用戶登錄到Linux系統中的默認所處目錄),
- SHELL: 當前Shell,它的值通常是/bin/bash,
查看環境變數的方法
我們可以通過echo命令來查看環境變數,方式如下:
echo?$NAME?//NAME為待查看的環境變數名稱
例如,查看環境變數PATH,
[cl@VM-0-15-centos ENV]$ echo $PATH

測驗PATH
大家有沒有想過這樣一個問題:為什么執行ls命令的時候不用帶./就可以執行,而我們自己生成的可執行程式必須要在前面帶上./才可以執行?

容易理解的是,要執行一個可執行程式必須要先找到它在哪里,既然不帶./就可以執行ls命令,說明系統能夠通過ls名稱找到ls的位置,而系統是無法找到我們自己的可執行程式的,所以我們必須帶上./,以此告訴系統該可執行程式位于當前目錄下,
而系統就是通過環境變數PATH來找到ls命令的,查看環境變數PATH我們可以看到如下內容:

可以看到環境變數PATH當中有多條路徑,這些路徑由冒號隔開,當你使用ls命令時,系統就會查看環境變數PATH,然后默認從左到右依次在各個路徑當中進行查找,
而ls命令實際就位于PATH當中的某一個路徑下,所以就算ls命令不帶路徑執行,系統也是能夠找到的,

那可不可以讓我們自己的可執行程式也不用帶路徑就可以執行呢?
當然可以,下面給出兩種方式:
方式一:將可執行程式拷貝到環境變數PATH的某一路徑下,
既然在未指定路徑的情況下系統會根據環境變數PATH當中的路徑進行查找,那我們就可以將我們的可執行程式拷貝到PATH的某一路徑下,此后我們的可執行程式不帶路徑系統也可以找到了,
[cl@VM-0-15-centos ENV]$ sudo cp proc /usr/bin

方式二:將可執行程式所在的目錄匯入到環境變數PATH當中,
將可執行程式所在的目錄匯入到環境變數PATH當中,這樣一來,沒有指定路徑時系統就會來到該目錄下進行查找了,
[cl@VM-0-15-centos ENV]$ export PATH=$PATH:/home/cl/dirforproc/ENV

將可執行程式所在的目錄匯入到環境變數PATH當中后,位于該目錄下的可執行程式也就可以在不帶路徑的情況下執行了,

測驗HOME
任何一個用戶在運行系統登錄時都有自己的主作業目錄(家目錄),環境變數HOME當中即保存的該用戶的主作業目錄,
普通用戶示例:

超級用戶示例:

測驗SHELL
我們在Linux作業系統當中所敲的各種命令,實際上需要由命令列解釋器進行解釋,而在Linux當中有許多種命令列解釋器(例如bash、sh),我們可以通過查看環境變數SHELL來知道自己當前所用的命令列解釋器的種類,

而該命令列解釋器實際上是系統當中的一條命令,當這個命令運行起來變成行程后就可以為我們進行命令列解釋,

和環境變數相關的命令
1、echo:顯示某個環境變數的值,

2、export:設定一個新的環境變數,

3、env:顯示所有的環境變數,

部分環境變數說明:
| 環境變數名稱 | 表示內容 |
|---|---|
| PATH | 命令的搜索路徑 |
| HOME | 用戶的主作業目錄 |
| SHELL | 當前Shell |
| HOSTNAME | 主機名 |
| TERM | 終端型別 |
| HISTSIZE | 記錄歷史命令的條數 |
| SSH_TTY | 當前終端檔案 |
| USER | 當前用戶 |
| 郵箱 | |
| PWD | 當前所處路徑 |
| LANG | 編碼格式 |
| LOGNAME | 登錄用戶名 |
4、set:顯示本地定義的shell變數和環境變數,
5、unset:清除環境變數,

環境變數的組織方式
在系統當中,環境變數的組織方式如下:

每個程式都會收到一張環境變數表,環境表是一個字符指標陣列,每個指標指向一個以’\0’結尾的環境字串,最后一個字符指標為空,
通過代碼獲取環境變數
你知道main函式其實是有引數的嗎?
main函式其實有三個引數,只是我們平時基本不用它們,所以一般情況下都沒有寫出來,
我們可以在Windows下的編譯器進行驗證,當我們除錯代碼的時候,若是一直使用逐步除錯,那么最侄訓來到呼叫main函式的地方,

在這里我們可以看到,呼叫main函式時給main函式傳遞了三個引數,
我們先來說說main函式的前兩個引數,
在Linux作業系統下,撰寫以下代碼,生成可執行程式并運行,

運行結果如下:

現在我們來說說main函式的前兩個引數,main函式的第二個引數是一個字符指標陣列,陣列當中的第一個字符指標存盤的是可執行程式的位置,其余字符指標存盤的是所給的若干選項,最后一個字符指標為空,而main函式的第一個引數代表的就是字符指標陣列當中的有效元素個數,

下面我們可以嘗試撰寫一個簡單的代碼,該代碼運行起來后會根據你所給選項給出不同的提示陳述句,
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[], char* envp[])
{
if(argc > 1)
{
if(strcmp(argv[1], "-a") == 0)
{
printf("you used -a option...\n");
}
else if(strcmp(argv[1], "-b") == 0)
{
printf("you used -b option...\n");
}
else
{
printf("you used unrecognizable option...\n");
}
}
else
{
printf("you did not use any option...\n");
}
return 0;
}
代碼運行結果如下:

現在我們來說說main函式的第三個引數,
main函式的第三個引數接收的實際上就是環境變數表,我們可以通過main函式的第三個引數來獲取系統的環境變數,
例如,撰寫以下代碼,生成可執行程式并運行,

運行結果就是各個環境變數的值:

除了使用main函式的第三個引數來獲取環境變數以外,我們還可以通過第三方變數environ來獲取,

運行該代碼生成的可執行程式,我們同樣可以獲得環境變數的值:

注意: libc中定義的全域變數environ指向環境變數表,environ沒有包含在任何頭檔案中,所以在使用時要用extern進行宣告,
通過系統呼叫獲取環境變數
除了通過main函式的第三個引數和第三方變數environ來獲取環境變數外,我們還可以通過系統呼叫getenv函式來獲取環境變數,
getenv函式可以根據所給環境變數名,在環境變數表當中進行搜索,并回傳一個指向相應值的字串指標,
例如,使用getenv函式獲取環境變數PATH的值,

運行結果:

程式地址空間
下面這張空間布局圖相信大家都見過:

在Linux作業系統中,我們可以通過以下代碼對該布局圖進行驗證:

運行結果如下,與布局圖所示是吻合的:

下面我們來看一段奇怪的代碼:

代碼當中用fork函式創建了一個子行程,其中讓子行程相將全域變數g_val該從100改為200后列印,而父行程先休眠3秒鐘,然后再列印全域變數的值,
按道理來說子行程列印的全域變數的值為200,而父行程是在子行程將全域變數改后再列印的全域變數,那么也應該是200,但是代碼運行結果如下:

可以看到父行程列印的全域變數g_val的值仍為之前的100,更奇怪的是在父子行程中列印的全域變數g_val的地址是一樣的,也就是說父子行程在同一個地址處讀出的值不同,
如果說我們是在同一個物理地址處獲取的值,那必定是相同的,而現在在同一個地址處獲取到的值卻不同,這只能說明我們列印出來的地址絕對不是物理地址!!!
實際上,我們在語言層面上列印出來的地址都不是物理地址,而是虛擬地址,物理地址用戶一概是看不到的,是由作業系統統一進行管理的,
所以就算父子行程當中列印出來的全域變數的地址(虛擬地址)相同,但是兩個行程當中全域變數的值卻是不同的,

注意: 虛擬地址和物理地址之間的轉化由作業系統完成,
行程地址空間
我們之前將那張布局圖稱為程式地址空間實際上是不準確的,那張布局圖實際上應該叫做行程地址空間,行程地址空間本質上是記憶體中的一種內核資料結構,在Linux當中行程地址空間具體由結構體mm_struct實作,
行程地址空間就類似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被劃分為各個區域,例如代碼區、堆區、堆疊區等,而在結構體mm_struct當中,便記錄了各個邊界刻度,例如代碼區的開始刻度與結束刻度,如下圖所示:

在結構體mm_struct當中,各個邊界刻度之間的每一個刻度都代表一個虛擬地址,這些虛擬地址通過頁表映射與物理記憶體建立聯系,由于虛擬地址是由0x00000000到0xffffffff線性增長的,所以虛擬地址又叫做線性地址,
擴展知識:
1、堆向上增長以及堆疊向下增長實際就是改變mm_struct當中堆和堆疊的邊界刻度,
2、我們生成的可執行程式實際上也被分為了各個區域,例如初始化區、未初始化區等,當該可執行程式運行起來時,作業系統則將對應的資料加載到對應記憶體當中即可,大大提高了作業系統的作業效率,而進行可執行程式的“磁區”操作的實際上就算編譯器,所以說代碼的優化級別實際上是編譯器說了算,
每個行程被創建時,其對應的行程控制塊(task_struct)和行程地址空間(mm_struct)也會隨之被創建,而作業系統可以通過行程的task_struct找到其mm_struct,因為task_struct當中有一個結構體指標存盤的是mm_struct的地址,
例如,父行程有自己的task_struct和mm_struct,該父行程創建的子行程也有屬于其自己的task_struct和mm_struct,父子行程的行程地址空間當中的各個虛擬地址分別通過頁表映射到物理記憶體的某個位置,如下圖:

而當子行程剛剛被創建時,子行程和父行程的資料和代碼是共享的,即父子行程的代碼和資料通過頁表映射到物理記憶體的同一塊空間,只有當父行程或子行程需要修改資料時,才將父行程的資料在記憶體當中拷貝一份,然后再進行修改,
例如,子行程需要將全域變數g_val改為200,那么此時就在記憶體的某處存盤g_val的新值,并且改變子行程當中g_val的虛擬地址通過頁表映射后得到的物理地址即可,

這種在需要進行資料修改時再進行拷貝的技術,稱為寫時拷貝技術,
1、為什么資料要進行寫時拷貝?
行程具有獨立性,多行程運行,需要獨享各種資源,多行程運行期間互不干擾,不能讓子行程的修改影響到父行程,
2、為什么不在創建子行程的時候就進行資料的拷貝?
子行程不一定會使用父行程的所有資料,并且在子行程不對資料進行寫入的情況下,沒有必要對資料進行拷貝,我們應該按需分配,在需要修改資料的時候再分配(延時分配),這樣可以高效的使用記憶體空間,
3、代碼會不會進行寫時拷貝?
90%的情況下是不會的,但這并不代表代碼不能進行寫時拷貝,例如在進行行程替換的時候,則需要進行代碼的寫時拷貝,
為什么要有行程地址空間?
1、有了行程地址空間后,就不會有任何系統級別的越界問題存在了,例如行程1不會錯誤的訪問到行程2的物理地址空間,因為你對某一地址空間進行操作之前需要先通過頁表映射到物理記憶體,而頁表只會映射屬于你的物理記憶體,總的來說,虛擬地址和頁表的配合使用,本質功能就是包含記憶體,
2、有了行程地址空間后,每個行程都認為看得到都是相同的空間范圍,包括行程地址空間的構成和內部區域的劃分順序等都是相同的,這樣一來我們在撰寫程式的時候就只需關注虛擬地址,而無需關注資料在物理記憶體當中實際的存盤位置,
3、有了行程地址空間后,每個行程都認為自己在獨占記憶體,這樣能更好的完成行程的獨立性以及合理使用記憶體空間(當實際需要使用記憶體空間的時候再在記憶體進行開辟),并能將行程調度與記憶體管理進行解耦或分離,
對于創建行程的現階段理解:
一個行程的創建實際上伴隨著其行程控制塊(task_struct)、行程地址空間(mm_struct)以及頁表的創建,
Linux2.6內核行程調度佇列

一個CPU擁有一個runqueue
如果有多個CPU就要考慮行程個數的父子均衡問題,
優先級
queue下標說明:
- 普通優先級:100~139,
- 實時優先級:0~99,
我們行程的都是普通的優先級,前面說到nice值的取值范圍是-20~19,共40個級別,依次對應queue當中普通優先級的下標100~139,
注意: 實時優先級對應實時行程,實時行程是指先將一個行程執行完畢再執行下一個行程,現在基本不存在這種機器了,所以對于queue當中下標為0~99的元素我們不關心,
活動佇列
時間片還沒有結束的所有行程都按照優先級放在活動佇列當中,其中nr_active代表總共有多少個運行狀態的行程,而queue[140]陣列當中的一個元素就是一個行程佇列,相同優先級的行程按照FIFO規則行程排隊調度,
調度程序如下:
- 從0下標開始遍歷queue[140],
- 找到第一個非空佇列,該佇列必定為優先級最高的佇列,
- 拿到選中佇列的第一個行程,開始運行,調度完成,
- 接著拿到選中佇列的第二個行程進行調度,直到選中行程佇列當中的所有行程都被調度,
- 繼續向后遍歷queue[140],尋找下一個非空佇列,
bitmap[5]:queue陣列當中一共有140個元素,即140個優先級,一共140個行程佇列,為了提高查找非空佇列的效率,就可以用5 × \times × 32個位元位表示佇列是否為空,這樣一來便可以大大提高查找效率,
總結: 在系統當中查找一個最合適調度的行程的時間復雜度是一個常數,不會隨著行程增多而導致時間成本增加,我們稱之為行程調度的O(1)演算法,
過期佇列
- 過期佇列和活動佇列的結構相同,
- 過期佇列上放置的行程都是時間片耗盡的行程,
- 當活動佇列上的行程被處理完畢之后,對過期佇列的行程進行時間片重新計算,
active指標和expired指標
- active指標永遠指向活動佇列,
- expired指標永遠指向過期佇列,
由于活動佇列上時間片未到期的行程會越來越少,而過期佇列上的行程數量會越來越多(新創建的行程都會被放到過期佇列上),那么總會出現活動佇列上的全部行程的時間片都到期的情況,這時將active指標和expired指標的內容交換,就相當于讓過期佇列變成活動佇列,活動佇列變成過期佇列,就相當于又具有了一批新的活動行程,如此回圈進行即可,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/302513.html
標籤:其他
