1. 程式人生 > 其它 >作業系統總結(二)——程序

作業系統總結(二)——程序

2.程序和執行緒、協程

2.1 程序、執行緒和協程的區別與聯絡

程序 執行緒 協程
定義 資源分配和擁有的基本單位,執行一個可執行程式會建立一個或多個程序,程序就是執行起來的可執行程式 程式執行的基本單位,是輕量級的程序。每個程序中都有唯一的主執行緒,且只能有一個,主執行緒和程序是相互依存的關係,主執行緒結束程序也會結束 使用者態的輕量級執行緒,執行緒內部排程的基本單位
切換情況 程序CPU環境(棧、暫存器、頁表和檔案控制代碼等)的儲存以及新排程的程序CPU環境的設定 儲存和設定程式計數器、少量暫存器和棧的內容 先將暫存器上下文和棧儲存,等切換回來的時候再進行恢復
切換者
作業系統 作業系統 使用者
切換過程 使用者態->核心態->使用者態 使用者態->核心態->使用者態 使用者態
呼叫棧 核心棧 核心棧 使用者棧
擁有資源 CPU資源、記憶體資源、檔案資源和控制代碼等 程式計數器、暫存器、棧 擁有自己的暫存器上下文和棧
併發性 不同程序之間切換實現併發,各自佔有CPU實現並行 一個程序內部的多個執行緒併發執行 同一時間只能執行一個協程,而其他協程處於休眠狀態,適合對任務進行分時處理
系統開銷 切換虛擬地址空間,切換核心棧和硬體上下文,CPU快取記憶體失效、頁表切換,開銷很大 切換時只需儲存和設定少量暫存器內容,因此開銷很小
直接操作棧則基本沒有核心切換的開銷,可以不加鎖的訪問全域性變數,所以上下文的切換非常快
通訊方式 程序間通訊需要藉助作業系統 執行緒間可以直接讀寫程序資料段(如全域性變數)來進行通訊 共享記憶體、訊息佇列

程序與執行緒的區別和聯絡:

1)執行緒是程式排程的基本單位,程序是資源擁有的基本單位。

2)當只有一個執行緒時,可以認為執行緒就等於程序;當程序擁有多個執行緒時,執行緒共享本程序中相同的虛擬記憶體和全域性變數,並且在切換時不需要修改,但是多個程序之間的資源是互相獨立的。

3)一個程序崩潰後,在保護模式下不會對其它程序產生影響。而執行緒只是一個程序中的不同執行路徑。執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,一個執行緒死掉就等於整個程序死掉,所以多程序的程式要比多執行緒的程式健壯。

4)執行緒不能脫離程序單獨存在,只能依賴於程序執行。

5)相比於程序,執行緒能減少併發執行的開銷。因為程序建立、切換、終止消耗的資源比執行緒多,同一程序內的執行緒具有相同的地址空間,且共享資源,所以切換和傳遞資料效率都比較高。

2.2 程序

1.程序的概念、狀態及切換

程序資源分配和擁有的基本單位,執行一個可執行程式會建立一個或多個程序,程序就是執行起來的可執行程式 。

程序具有的特徵:

  • 動態性:程序是程式的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的;

  • 併發性:任何程序都可以同其他程序一起併發執行;

  • 獨立性:程序是系統進行資源分配和排程的一個獨立單位;

  • 結構性:程序由程式、資料和程序控制塊三部分組成。

一個程序具有以下幾種基本狀態:

  • 新的:程序正在建立。

  • 執行:指令正在執行。

  • 等待:程序等待發生某個事件(如 I/O 完成或收到訊號)。

  • 就緒:程序等待分配處理器。

  • 終止:程序已經完成執行

如圖所示:

只有就緒態和執行態可以相互轉換,其它的都是單向轉換。就緒狀態的程序通過排程演算法從而獲得CPU 時間,轉為執行狀態;而執行狀態的程序,在分配給它的 CPU 時間片用完之後就會轉為就緒狀態,等待下一次排程。

2.程序的控制

(1)建立程序

作業系統允許一個程序建立另一個程序,程序在執行過程中可能建立多個新的程序。建立程序稱為父程序,而新的程序稱為子程序,每個新程序可以再建立其他程序。

一般來說,當一個程序建立子程序時,該子程序需要一定的資源(CPU 時間、記憶體、檔案、I/O 裝置等)來完成任務。子程序可以從作業系統那裡直接獲得資源,也可以只從父程序那裡獲得資源子集。父程序可能要在子程序之間分配資源或共享資源(如記憶體或檔案)。限制子程序只能使用父程序的資源,可以防止建立過多程序,導致系統超載

當程序建立新程序時,可有兩種執行可能:

1)父程序與子程序併發執行。

2)父程序等待,直到某個或全部子程序執行完。

新程序的地址空間也有兩種可能:

1)子程序是父程序的複製品(它具有與父程序同樣的程式和資料)。

2)子程序載入另一個新程式。

(2)終止程序

程序的終止主要有3種方式:正常結束、異常結束以及外界訊號(kill)干擾。

當程序完成執行最後語句並且通過系統呼叫 exit() 請求作業系統刪除自身時,程序終止。這時,程序可以返回狀態值(通常為整數)到父程序(通過系統呼叫 wait())。

父程序終止子程序的原因有很多,如:

1)子程序使用了超過它所分配的資源。(為判定是否發生這種情況,父程序應有一個機制,以檢查子程序的狀態)。

2)分配給子程序的任務,不再需要。

3)父程序正在退出,而且作業系統不允許無父程序的子程序繼續執行。

有些系統不允許子程序在父程序已終止的情況下存在。對於這類系統,如果一個程序終止(正常或不正常),那麼它的所有子程序也應終止。這種現象,稱為級聯終止,通常由作業系統來啟動

(3)阻塞和喚醒程序

當程序需要等待某一事件完成時,會呼叫阻塞語句讓自己阻塞等待,此時需要由另一個程序喚醒。

(4)常用C庫函式

pid_t fork();
//函式功能:建立程序
//返回值:錯誤返回-1;父程序中返回pid > 0;子程序中pid = 0
​
pid_t getpid();
//函式功能:獲取本程式執行時程序的編號
​
pid_t getppid();
//函式功能:獲取父程序編號
​
void exit(int status);
//函式功能:結束程序
//status是退出狀態,為0表示正常退出

(5)程序圖繪製

1)使用fork建立一個新程序和對應的程序圖

/*使用fork建立一個新程序*/
int main()
{
  pid_t pid;
    int x = 1;
    
    pid = fork();
    
    if(pid == 0)
    {
        /*子程序*/
        printf("child : x=%d\n", ++x);
        exit(0);
    }
    
    /*父程序*/
    printf("parent : x=%d\n", --x);
    exit(0);
}

2)巢狀fork及其程序圖

/*巢狀fork*/
int main()
{
    fork();
    fork();
    printf("hello\n");
    exit(0);
}

3.守護程序、殭屍程序和孤兒程序

(1)守護程序

指在後臺執行的,沒有控制終端與之相連的程序。它獨立於控制終端,週期性地執行某種任務。Linux的大多數伺服器就是用守護程序的方式實現的,如web伺服器程序http等。

建立守護程序:

1)讓程式在後臺執行。方法是呼叫 fork() 產生一個子程序,然後使父程序退出。

2)呼叫 setsid() 建立一個新對話期。控制終端、登入會話和程序組通常是從父程序繼承下來的,守護程序要擺脫它們,不受它們的影響,方法是呼叫 setsid() 使程序成為一個會話組長。setsid() 呼叫成功後,程序成為新的會話組長和程序組長,並與原來的登入會話、程序組和控制終端脫離。

3)禁止程序重新開啟控制終端。經過以上步驟,程序已經成為一個無終端的會話組長,但是它可以重新申請開啟一個終端。為了避免這種情況發生,可以通過使程序不再是會話組長來實現。再一次通過 fork() 建立新的子程序,使呼叫fork的程序退出。

4)關閉不再需要的檔案描述符。子程序從父程序繼承開啟的檔案描述符。如不關閉,將會浪費系統資源,造成程序所在的檔案系統無法卸下以及引起無法預料的錯誤。首先獲得最高檔案描述符值,然後用一個迴圈程式,關閉0到最高檔案描述符值的所有檔案描述符。

5)將當前目錄更改為根目錄。

6)子程序從父程序繼承的檔案建立遮蔽字可能會拒絕某些許可權。為防止這一點,使用unmask(0)將遮蔽字清零。

7)處理SIGCHLD訊號。對於伺服器程序,在請求到來時往往生成子程序處理請求。如果子程序等待父程序捕獲狀態,則子程序將成為殭屍程序(zombie),從而佔用系統資源。如果父程序等待子程序結束,將增加父程序的負擔,影響伺服器程序的併發效能。在Linux下可以簡單地將SIGCHLD訊號的操作設為SIG_IGN。這樣,子程序結束時不會產生殭屍程序。

(2)殭屍程序

一個子程序在呼叫return或exit(0)結束自己的生命的時候,其實它並沒有真正的被銷燬,而是留下一個殭屍程序,在Linux中有<defunct>標誌,如圖所示:

如果按Ctrl+C終止父程序後,父程序退出,殭屍程序隨之消失。如果要銷燬殭屍程序,應該向建立子程序的父程序傳遞子程序的exit引數值或者return語句的返回值

殭屍程序的危害:

殭屍程序是子程序結束時,父程序又沒有回收子程序佔用的資源,殭屍程序在消失之前會繼續佔用系統資源。

如果父程序先退出,子程序被系統接管,子程序退出後系統會回收其佔用的相關資源,不會成為殭屍程序。 由於子程序的結束和父程序的執行是一個非同步過程,即父程序永遠無法預測子程序到底什麼時候結束,子程序退出時會保留一定的資源,只有父程序通過wait / waitpid來取時才釋放。 但這樣就導致了問題,如果程序不呼叫wait / waitpid的話,那麼保留的那段資訊就不會釋放,其程序號就會一直被佔用,但是系統所能使用的程序號是有限的,如果大量的產生殭屍程序,將因為沒有可用的程序號而導致系統不能產生新的程序,此即為殭屍程序的危害,應當避免

殭屍程序的解決辦法:

1)子程序退出之前,會向父程序傳送一個訊號,父程序呼叫wait / waitpid函式等待這個訊號,等到並把它徹底銷燬後返回,這樣就不會產生殭屍程序

wait函式宣告如下:

pid_t wait(int* statloc);
//引數status用來儲存被收集程序退出時的一些狀態,如果只想銷燬殭屍程序,可以直接設為NULL
//返回值:成功時返回終止的子程序ID,失敗時返回-1

如果statloc不是NULL,wait就會把子程序退出時的狀態取出並存入其中,該引數指標所指向的單元中還包含其他資訊,因此需要通過下列巨集進行分離:

//WIFEXITED 子程序正常終止時返回“true”
//WEXITSTATUS 返回子程序的返回值
​
//也就是說,呼叫wait函式後應該編寫如下程式碼:
if(WIFEXITED(status))  //是正常終止的嗎?
{
  puts("Normal termination!");
  printf("Child pass num:%d", WEXITSTATUS(status))  //返回值
}

呼叫wait函式,如果沒有已終止的子程序,那麼程式將阻塞直到有子程序終止。 而waitpid函式可以防止阻塞,函式宣告如下:

pid_t wait(pid_t pid, int* statloc, int options);
//pid:目標子程序的id,若傳遞-1,則可以等待任意子程序終止,
//options:傳遞常量WNOHANG,即使沒有終止的子程序也不會阻塞程式,而是返回0並退出函式
//返回值:成功時返回終止的子程序ID,失敗時返回-1

這個方法說得容易,在併發的服務程式中這是不可能的,因為父程序要做其它的事,例如等待客戶端的新連線,不可能去等待子程序的退出訊號。

2)另一種方法就是告訴父程序子程序終止了,具體做法很簡單,在主程式中啟用以下程式碼:

signal(SIGCHLD,SIG_IGN); // 忽略子程序退出的訊號,避免產生殭屍程序

如果父程序很忙,可以用signal註冊訊號處理函式,這樣子程序結束後, 父程序會收到該訊號,這樣父程序可以暫時放下工作,處理子程序結束相關事宜,如呼叫wait回收

如果父程序不關心子程序什麼時候結束,那麼可以用signal通知核心,自己對子程序的結束不感興趣,那麼子程序結束後,核心會回收, 並不再給父程序傳送訊號。

(3)孤兒程序

如果一個父程序終止了而子程序沒有終止,則此時子程序變為孤兒程序。核心會安排init程序稱為孤兒程序的養父,並由init程序完成對它們的回收工作。init程序的PID為1,是在系統啟動時由核心建立的,它不會終止,是所有程序的祖先。

4.程序間通訊

程序的資料空間是獨立的,私有的,不能相互訪問,但是在某些情況下程序之間需要通訊來實現某功能或交換資料,包括:

1)資料傳輸:一個程序需要將它的資料傳送給另一個程序。

2)共享資料:多個程序想要操作共享資料,一個程序對共享資料的修改,別的程序應該立刻看到。

3)通知事件:一個程序需要向另一個或一組程序傳送訊息,通知它(它們)發生了某種事件(如通知程序退出)。

4)程序控制:一個程序希望控制另一個程序的執行。

程序通訊的方式大概分為六種。

  • 管道(pipe) 通過作業系統提供建立管道,讓程序通訊。

  • 訊息佇列(message) 程序可以向佇列中新增訊息,其它的程序則可以讀取佇列中的訊息。

  • 訊號(signal) 訊號是由使用者、系統或程序傳送給目標程序的資訊,以通知目標有某種事件發生。

  • 共享記憶體(shared memory) 多個程序可以訪問同一塊記憶體空間。

  • 訊號量(semaphore) 也叫訊號燈,用於程序之間對共享資源進行加鎖。

  • 套接字(socket):可用於不同計算機之間的程序間通訊。

(1)管道

所謂管道,就是核心裡面的一段快取,從管道的一端寫入的資料,實際上是快取在核心中,另一端讀取,也是從核心中讀取。

②無名管道:

一種半雙工的通訊方式,只能在具有親緣關係的程序間使用(父子程序)

假設使用fork建立一個管道,會複製父程序的檔案描述符,兩個程序都有fds[0]和fds[1](讀端和寫端)。子程序通過 fds[1] 把資料寫入管道,父程序從 fds[0] 再把資料讀出來,如圖所示:

相關函式宣告如下:

#include<unistd.h>
​
int pipe(int fd[2]);
/*
fd[0]:通過管道接收資料時使用的檔案描述符,即管道出口
fd[1]:通過管道傳輸資料時使用的檔案描述符,即管道入口
返回值:成功時返回0,失敗時返回-1
*/

優點:

  • 簡單方便

缺點:

  • 侷限於單向通訊

  • 只能建立在它的程序以及其有親緣關係的程序之間

  • 緩衝區有限

②有名管道

一種半雙工的通訊方式,它允許不相關程序間的通訊。有名管道是FIFO檔案,存在於檔案系統中,可以通過檔案路徑名來指出。

相關函式宣告如下:

#include<sys/types.h>
#include<sys/stat.h>
​
int mkfifo(const char *pathname, mode_t mode);
/*
pathname:即將建立的FIFO檔案路徑,如果檔案存在需要先刪除。
mode:用來規定FIFO的讀寫許可權
返回值:成功時返回0,失敗時返回-1
*/

優點:

  • 可以實現任意關係的程序間的通訊

缺點:

  • 長期存於系統中,使用不當容易出錯,

  • 緩衝區有限

(2)訊號

訊號一種比較複雜的通訊方式,是由使用者、系統或程序傳送給目標程序的資訊,以通知目標有某種事件發生。

Linux訊號可由如下條件產生:

  • 對於前臺程序,使用者可以通過輸入特殊的終端字元來給它傳送訊號,例如Ctrl+C是中斷訊號

  • 系統異常,比如浮點異常和非法記憶體訪問

  • 系統狀態變化,比如alarm定時器到期將引起SIGALRM訊號

  • 執行kill命令或呼叫kill函式

伺服器程式必須處理(或至少忽略)一些常見的訊號,以免異常終止。Linux的可用訊號都定義在bits/signum.h標頭檔案中。如果程式在執行處於阻塞狀態的系統呼叫時接收到訊號,並且設定了訊號處理函式,則預設情況下系統呼叫將被中斷。對於預設行為是暫停程序的訊號,如果沒有為它們設定訊號處理函式,則它們可以中斷某些系統呼叫。

相關函式:

#include<signal.h>
​
_sighandler_t signal(int sig, _sighandler_t _handler);
/*
sig:指出要捕獲的訊號型別
_handler:是_sighandler_t 型別的函式指標,用於指定訊號sig的處理函式
*/
  
int sigaction( int sig, const struct sigaction* act, struct sigaction* oact );
/*
sig:指出要捕獲的訊號型別
act:指定新的訊號處理方式
oact:輸出訊號先前的處理方式
*/

Linux相關訊號

1)SIGHUP

當掛起程序的控制終端時,SIGHUP訊號將被觸發。對於沒有控制終端的網路後臺程式而言,它們通常利用SIGHUP訊號來強制伺服器重新讀取配置檔案。

2)SIGPIPE

預設情況下,往一個讀端關閉的管道或socket連線中寫資料將引發SIGPIPE訊號。需要在程式碼中捕獲並處理該訊號,或者至少忽略它,因為程式接收到SIGPIPE訊號預設行為是結束程序。

3)SIGURG

在Linux環境下,核心通知應用程式帶外資料(重要資料,不與普通資料使用相同的通道)到達主要有兩種方法,一種是I/O複用技術,select等系統呼叫在接收到帶外資料時將返回並嚮應用程式報告。另一種方法就是使用SIGURG訊號。

(3)訊號量

訊號量本質上是一個計數器,用於協調多個程序對共享資料物件的讀/寫。它不以傳送資料為目的,主要是用來保護共享資源(共享記憶體、訊息佇列、socket連線池、資料庫連線池等),保證共享資源在一個時刻只有一個程序獨享。

訊號量是一個特殊的變數,只允許程序對它進行等待訊號和傳送訊號操作。最簡單的訊號量是取值0和1的二元訊號量,這是訊號量最常見的形式。

相關函式:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
​
//建立訊號量
int semget(key_t key, int nsems, int semflg);
/*
key:是訊號量在系統中的編號
nsems:建立訊號量的個數。
semflg:呼叫函式的操作型別,也可用於設定訊號量集的訪問許可權
返回值:建立成功返回訊號量識別符號,失敗返回-1。
*/
​
//控制訊號量
int semctl(int semid, int semnum, int cmd, union semun arg);
/*
semid:semget函式返回的訊號量集識別符號。
semnum:訊號量集陣列上的下標,表示某一個訊號量,一般填0。
cmd:對訊號量操作的命令種類,
   IPC_RMID:銷燬訊號量,不需要第四個引數;
   SETVAL:初始化訊號量的值,(訊號量成功建立後,需要設定初始值),這個值由第四個引數決定。
arg:union semun型別。如下:
  union semun
  {
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
  };
*/
​
//改變訊號量的值
int semop(int semid, struct sembuf *sops, unsigned nsops);
/*
semid:semget函式返回的訊號量集識別符號
sops:指向struct sembuf結構的指標,如下:
  struct sembuf
  {
    short sem_num;   / / 訊號量集的個數,單個訊號量設定為0。
    short sem_op;    // 訊號量在本次操作中需要改變的資料:-1-等待操作;1-傳送操作。
    short sem_flg;   // 把此標誌設定為SEM_UNDO,作業系統將跟蹤這個訊號量。
  };
nsops:操作訊號量的個數,即sops結構變數的個數,設定它的為1表示只對一個訊號量的操作
*/

優點:

  • 可以同步程序

缺點:

  • 訊號量有限

(4)訊息佇列

訊息佇列是有訊息的連結串列,存放在核心中並由訊息佇列識別符號標識。

相關函式

#include<sys/msg.h>
​
//建立訊息佇列
int msgget(key_t key, int msgflg);
/*
key:是訊息佇列在系統中的編號
msgflg:呼叫函式的操作型別,也可用於設定訊息佇列集的訪問許可權
返回值:成功時返回一個正整數值,它是訊息佇列的識別符號,失敗時返回-1
*/
​
//將一個新的訊息寫入佇列
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);
//從訊息佇列中讀取訊息
int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
/*
msqid:由msgget呼叫返回的訊息佇列識別符號
msg_ptr:指向一個準備傳送的訊息,訊息必須被定義為如下型別:
  struct msgbuf
  {
    long mtype;     // 訊息型別 
    char mtext[512];   // 訊息資料 
  };
msg_sz:訊息的大小
msgtype:訊息型別
返回值:成功執行時,msgsnd()返回0,msgrcv()返回拷貝到mtext陣列的實際位元組數。失敗兩者都返回-1
*/
​
​
//控制訊息佇列中的某些屬性
int msgctl(int msqid, int cmd, struct msqid_ds* buf);
/*
cmd:指定要執行的命令
msqid_ds:佇列結構體
返回值:成功返回0,失敗返回-1
*/

優點:

  • 訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。

  • 可以實現任意程序間的通訊,並通過系統呼叫函式來實現訊息傳送和接收之間的同步,無需考慮同步問題,方便

缺點:

  • 資訊的複製需要額外消耗 CPU 的時間,不適宜於資訊量大或操作頻繁的場合

(5)共享記憶體

共享記憶體就是對映一段能被其他程序所訪問的記憶體,這段共享記憶體由一個程序建立,但多個程序都可以訪問。共享記憶體是在多個程序之間共享和傳遞資料最高效的方式,它是針對其他程序間通訊方式執行效率低而專門設計的。如果某個程序修改了共享記憶體中的資料,其它的程序讀到的資料也將會改變。 共享記憶體並未提供鎖機制,也就是說,在某一個程序對共享記憶體的進行讀寫的時候,不會阻止其它的程序對它的讀寫。如果要對共享記憶體的讀/寫加鎖,可以使用訊號量

相關函式:

#include <sys/types.h>
#include <sys/shm.h>
​
//建立共享記憶體
int shmget(key_t key, size_t size, int shmflg);
/*
key:是共享記憶體段在系統中的編號
size:建立共享記憶體大小。
shmflg:呼叫函式的操作型別,也可用於設定共享記憶體的訪問許可權
返回值:建立成功返回訊號量識別符號,失敗返回-1。
*/
​
//控制訊號量
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
shmid:shmget函式返回的共享記憶體識別符號。
cmd:對共享記憶體操作的命令種類,
  IPC_STAT:得到共享記憶體的狀態,把共享記憶體的shmid_ds結構複製到buf中
  IPC_SET:改變共享記憶體的狀態,把buf所指的shmid_ds結構中的uid、gid、mode複製到共享記憶體的shmid_ds結構內
  IPC_RMID:刪除這片共享記憶體
buf:共享記憶體管理結構體
*/
​
//把共享記憶體區物件對映到呼叫程序的地址空間
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
shmid:共享記憶體識別符號
shmaddr:指定共享記憶體出現在程序記憶體地址的什麼位置,通常直接指定為NULL讓核心自己決定一個合適的地址位置
shmflg:呼叫函式的操作型別,也可用於設定共享記憶體的訪問許可權
返回值:呼叫成功時返回一個指向共享記憶體第一個位元組的指標,如果呼叫失敗返回-1.
*/
  
//斷開共享記憶體連線
int shmdt(const void *shmaddr);
/*
shmid:shmat連線的共享記憶體的起始地址
返回值:成功返回0,失敗返回-1。
*/

優點:

  • 無須複製,快捷,資訊量大,不存在讀取檔案、訊息傳遞等過程,只需要到相應對映到的記憶體地址直接讀寫資料即可。

缺點:

  • 通訊是通過將共享空間緩衝區直接附加到程序的虛擬地址空間中來實現的,因此程序間的讀寫操作存在同步問題。

  • 利用記憶體緩衝區直接交換資訊,記憶體的實體存在於計算機中,只能同一個計算機系統中的諸多程序共享,不方便網路通訊。

補充:ipcs命令用於報告共享記憶體、訊號量和訊息佇列資訊。

  • ipcs -a:列出共享記憶體、訊號量和訊息佇列資訊。

  • ipcs -l:列出系統限額。

  • ipcs -u:列出當前使用情況。

(6)套接字

適用於不同機器間程序通訊,在本地也可作為兩個程序通訊的方式。

優點:

  • 傳輸資料為位元組級,傳輸資料可自定義,資料量小效率高

  • 傳輸資料時間短,效能高

  • 適合於客戶端和伺服器端之間資訊實時互動

  • 可以加密,資料安全性強

缺點:

  • 需對傳輸的資料進行解析,轉化成應用級的資料。

5.程序排程演算法

排程的概念:

當 CPU 有一堆任務要處理時,由於其資源優先,這些事情就沒有辦法同時處理。這就需要確定某種規則來決定處理這些任務的順序,這就是 “排程” 研究的問題。

所謂的程序排程,就是從程序的就緒佇列(阻塞)中按照一定的演算法選擇一個程序並將 CPU 分配給它執行,以實現程序的併發執行,這是作業系統中最基本的一種排程,在一般的作業系統中都必須配置程序排程。程序排程的頻率很高,一般幾十毫秒一次。

程序排程演算法分如下:

(1)非搶佔式的排程演算法

1)先來先服務 first-come first-serverd(FCFS)

所謂非搶佔式的意思就是,當程序正在執行時,它就會一直執行,直到該程序完成或發生某個事件而被阻塞時,才會把 CPU 讓給其它程序。

1)先來先服務 first-come first-serverd(FCFS)

該演算法按照程序到達的先後順序進行排程,先到的程序就先被排程,或者說,當前等待時間越久的越先得到服務。

該演算法公平、實現簡單,但是對短程序不利,排在長程序後面的短程序需要等待很長時間,短程序的響應時間太長了,使用者互動體驗會變差。

2) 最短作業優先 shortest job first(SJF)

該演算法每次排程時選擇當前已到達的、且執行時間最短的程序。

該演算法和FCFS相反,對長程序不利長,長程序處於一直等待短程序執行完畢的狀態。因為如果一直有短程序到來,那麼長程序永遠得不到排程。

(2)搶佔式的排程演算法

1)最短剩餘時間優先 shortest remaining time next(SRTN)

該演算法是最短作業優先的搶佔式版本,按剩餘執行時間的順序進行排程。 當一個新的作業到達時,其整個執行時間與當前程序的剩餘時間作比較。如果新的程序需要的時間更少,則掛起當前程序,執行新的程序。否則新的程序等待。

該演算法比比 SJF 稍微公平一點,如果當前程序即將完成,那麼即使不斷進來短程序,還是有很大概率優先順序比短程序高,不會長時間得不到排程

2)輪轉排程 Round-Robin(RR)

該演算法也稱時間片排程演算法,排程程式每次把 CPU 分給就緒佇列的第一個程序,並規定固定的使用時間,成為時間片,通常為 10ms ~ 200ms。就緒佇列中的每個程序輪流地執行一個時間片,當時間片耗盡時就強迫當前執行程序讓出 CPU 資源,轉而排到就緒佇列尾部,等待下一輪排程。

需要注意的是,時間片的長度是一個很關鍵的因素:

  • 如果時間片設定得太短,就會導致頻繁的程序上下文切換,降低了 CPU 效率

  • 如果時間片設定得太長,那麼隨著就緒佇列中程序數目的增加,輪轉一次消耗的總時間加長,即每個程序的響應速度變慢。當時間片大到足以讓程序完成其所有任務,RR 便退化為 FCFS

(3)最高優先順序排程演算法 Highest Priority First(HPF)

該演算法就是從就緒佇列中選擇最高優先順序的程序執行。程序優先順序分為靜態優先順序和動態優先順序。

  • 靜態優先順序:建立程序時,就預先規定優先順序,並且整個執行過程中該程序的優先順序都不會發生變化。一般來說,核心程序的優先順序都是高於使用者程序的。

  • 動態優先順序:根據程序的動態變化調整優先順序,比如隨著程序的執行時間增加,適當地降低其優先順序。隨著就緒佇列中程序的等待時間增加,其優先順序也會適當地被提高。

另外需要注意的是,HPF 並非是固定的搶佔式或非搶佔式策略,系統可預先規定使用哪種策略:

  • 非搶佔式:當就緒佇列中出現優先順序高的程序,則執行完當前程序後,再選擇該優先順序高的程序。

  • 搶佔式:當就緒佇列中出現優先順序高的程序,則立即強制剝奪當前執行程序的 CPU 資源,分配給優先順序更高的程序執行。

(4)多級反饋佇列演算法 Multilevel Feedback Queue (MFQ)

該演算法是RR和HPF演算法的結合。“多級”表示有多個佇列,每個佇列的優先順序從高到低,同時優先順序越高的時間片越短。“反饋”表示如果有新的程序加入優先順序高的佇列,立刻停止當前正在執行的程序,轉去執行優先順序高的佇列。如圖所示:

在上圖中,新的程序會按照先來先服務的原則在第一級佇列末尾等候,如果第一級佇列的時間片內該程序還沒有執行或沒有處理完,則移至第二級佇列的末尾,依次類推。對於短程序,往往在前幾級佇列就處理完了,對於長程序,可以在多個佇列中完成,所以該演算法對於短程序和長程序都有較好的執行效率

參考:

  1. 《深入理解計算機系統》

  2. 《圖解系統》- 小林coding
  3. 《TCP-IP網路程式設計》 韓-尹聖雨

  4. 《Linux高效能伺服器程式設計》

  5. https://github.com/huihut/interview

  6. https://blog.csdn.net/qq_32642107/article/details/102919692
  7. https://blog.csdn.net/qq_45593575/article/details/120779693