用戶態與核心態?哪些操作會導致用戶態切換到核心態?
用戶態與核心態是指操作系統兩種運行級別。操作系統核心的功能與服務(進程)運行在內核態,例如:進程管理、內存管理、設備管理、文件管理等;用戶進程只能訪問用戶代碼和數據,當用戶進程要訪問內核態級別的功能與服務(進程)時,需要通過系統調用來實現。
通常,系統調用、異常 和 外設中斷會導致用戶態到內核態的切換:
系統調用:這是用戶態進程主動要求切換到內核態的一種方式,用戶態進程通過系統調用申請使用操作系統提供的服務程序完成工作。例如Linux系統中常見的fork
、open
、read
、write
、close
等系統調用。
異常:當CPU
在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就轉到了內核態,比如缺頁異常。
外設中斷:當外圍設備完成用戶請求的操作后,會向CPU
發出相應的中斷信號,這時CPU
會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那么這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行后續操作等。
進程與線程的區別?
簡單來說,進程可以理解為代碼在計算機上的一次完整的執行過程,一個進程通常包含多個線程,線程的出現是為了進一步提高程序并發執行的程度。
就資源分配來說: 進程是資源分配的基本單位;線程不擁有資源,但可以共享進程資源。
就CPU調度來說: 線程是CPU
調度的基本單位,同一進程中的線程切換,不會引起進程切換;不同進程中的線程切換,會引起進程切換。
就系統開銷來說: 進程創建和銷毀時,系統都要單獨為它分配和回收資源,開銷遠大于線程的創建和銷毀;進程的上下文切換需要保存更多的信息,線程(同一進程中)的上下文切換只需要保存線程的私有數據:棧、程序計數器(PC
)等,系統開銷更小。
通信方式: 進程擁有各自獨立的地址空間,進程間的通信需要依靠IPC
機制;線程由于共享進程資源,因此可以通過訪問共享數據進行通信,通訊非常方便,但需要解決好共享全局變量的同步與互斥。
進程的基本狀態?
進程有五種基本狀態:創建狀態、就緒狀態、執行狀態、阻塞狀態、終止狀態。狀態的轉換關系如下:
創建狀態:為進程創建PCB
(Process Control Block
,進程控制塊。它是操作系統為了管理進程設置的一個專門的數據結構)并分配除CPU
時間片以外的必要資源;
就緒狀態:當進程已分配到除CPU
以外的所有必要資源后,只要再獲得CPU
,便可立即執行,這種狀態稱為就緒狀態;
執行狀態:進程處于就緒狀態被調度后,進程進入執行狀態;
阻塞狀態:正在執行的進程由于某些事件(如:I/O
請求)而暫時交出CPU
資源,無法繼續運行,進程受到阻塞,進入阻塞狀態;
終止狀態:進程自然結束,或出現錯誤,被系統終釋放資源后進入終止狀態,無法再執行。
進程調度算法有哪些?
先到先服務 (FCFS
) 調度算法 : 從就緒隊列中選擇?個最先進?該隊列的進程為其分配 CPU
資源并使之運行。有利于長作業,但不利于短作業,短作業會因為前面的長作業長時間執行而遲遲得不到調度。同時,I/O
密集型作業會因為多次被阻塞而多次重新排隊。
短作業優先 (SJF
) 調度算法: 從就緒隊列中選出?個估計運?時間最短的進程為其分配 CPU
資源并使之運行。不利于長作業,如果新的短作業不斷到達就緒隊列,長作業會一直不能被調度。
優先級調度調度算法: 為每個進程設置優先級,?先執??優先級的進程,相同優先級的進程按照先來先服務的策略調度。
時間?輪轉調度算法 : 按照先來先服務的策略依次調度進程,每個進程執行固定時間片后被重新放入隊尾。
多級反饋隊列調度算法 :前?介紹的?種進程調度算法都有?定的局限性,?該算法可以兼顧高優先級以及長、短作業。該算法由高到低設置多個優先級不同的隊列,通常優先級越高的隊列設置的時間片越小。該算法按照時間片輪轉算法先調度高優先級隊列里的進程,若高優先級隊列中已沒有需要調度的進程,則依次調度次優先級隊列中的進程。高優先級隊列中的進程如果被調度執行一個時間片的大小后仍沒有完成,則依次放入次優先級隊列中。如果低優先級隊列中的進程被調度執行時高優先級隊列中又有新的進程到達,那么執行完當前時間片后,CPU
會馬上分配給新到達高優先隊列中的進程。
操作系統的僵死進程和孤兒進程的區別?
Linux
系統中,子進程由父進程創建,子進程退出后雖然會釋放部分資源,但進程描述等資源并沒有被釋放,需要父進程調用wait(會阻塞父進程)
或waitpid(可以為非阻塞)
來釋放,可以方便父進程拿到子進程的終止狀態。
僵尸進程:當進程退出之后,他的父進程沒有通過調用wait
或waitpid
回收他的資源,該進程會繼續停留在系統的進程表中,占用內核資源,這樣的進程就是僵尸進程。通過ps
命令顯示的僵尸進程狀態為Z(zombie)
。大量僵尸進程沒被回收會導致資源浪費,更致命的是他們會占用大量進程號,導致系統無法給新進程分配進程號。
孤兒進程:進程結束后,它的一個或多個子進程還在運行,那么這些子進程就是孤兒進程。孤兒進程如果沒有被自己所在的進程組收養,就會作為init(PID = 1)
進程的子進程,他的的資源會由init
進程回收。
僵尸進程在其父進程退出后轉為孤兒進程。
如何解決僵尸進程:
- 通過
kill -9
殺掉其父進程,僵死進程就可以轉為孤兒進程,進而被init
進程回收;- 由于進程退出時會向父進程發送
SIGCHLD
信號,因此,可以在父進程捕獲該信號并通過wait
或waitpid
釋放子進程資源。
什么是死鎖?死鎖產生的必要條件?
死鎖:在許多應用中進程需要以獨占的方式訪問資源,當多個進程并發執行時可能會出現相互等待對方所占用的資源而都無法繼續向下執行的現象,此現象稱為死鎖。
死鎖產生的四個必要條件(發生死鎖時,一定會有以下條件成立):
互斥條件:一個資源只能被一個進程占有,進程應互斥且排他的使用這些資源。
請求與保持條件:進程在請求資源得不到滿足而等待時,不釋放已占有的資源。
不可剝奪條件:進程已經占有的資源,除非進程自己釋放,其他進程不能強行剝奪 。
循環等待條件:若干進程之間形成一種首位尾相接的環形等待資源關系。
處理死鎖的基本策略和常用方法?
預防死鎖(破壞四個必要條件):
并不是所有應用場景都可以破壞互斥條件。案例:
SPOOLing
技術將一臺獨享打印機改造為可供多個用戶共享的打印機。(破壞互斥條件 )當進程在運行前一次申請完他所需要的全部資源,在他的資源未滿足前,不讓他投入運行。(破壞請求和保持條件)
給不同進程設置優先級,當某個高優先級進程需要的資源被其它進程占有的時候,可以由操作系統協助將想要的資源強行剝奪。(破壞不可剝奪條件)
給資源編號,進程必須按照編號從小到大的順序申請自己所需資源。(破壞循環等待條件)
避免死鎖(銀行家算法):
預防死鎖的幾種策略,會嚴重地損害系統性能。在避免死鎖時,要施加較弱的限制,從而獲得較為滿意的系統性能。具有代表性的避免死鎖算法是銀行家算法:
銀行家算法的實質就是要設法保證系統動態分配資源后不會進入不安全狀態,以避免可能產生的死鎖。 即每當進程提出資源請求且系統的資源能夠滿足該請求時,系統將判斷滿足此次資源請求后系統狀態是否安全,如果判斷結果為安全,則給該進程分配資源,否則不分配資源,申請資源的進程將阻塞。
銀行家算法所需數據結構:
Available[j] 向量:系統中可利用的各種資源數目
Max[i, j] 矩陣:每個進程對每種資源的最大需求
Allocation[i, j] 矩陣:每個進程已分配的各類資源的數目
Need[i, j] 矩陣:每個進程還需要的各類資源數
其中三個矩陣間存在下述關系:
Need[i, j] = Max[i, j] - allocation[i, j]
銀行家算法流程:
設Request
是第i
個進程P
的請求向量,如果Request[j] = K
,表示進程P
需要K
個j
類型的資源。當P
發出資源請求后,系統按下述步驟進行檢查:
若
Request[j] <= Need[i, j]
,轉向2
,否則認為出錯(因為它所需的資源數目已超過它所宣布的最大值)。若
Requesti[j] <= Available[j]
,轉向3
,否則須等待(表現為進程P
受阻)。系統嘗試把資源分配給進程
P
,并修改下面數據結構中的數值:
Available[j] = Available[j] – Request[j]
Allocation[i, j] = Allocation[i, j] + Request[j]
Need[i, j] = Need[i, j] –Request[j]
- 試分配后,執行安全性算法,檢查此次分配后系統是否處于安全狀態。若安全,才正式分配;否則,此次試分配作廢,進程
P
等待。
安全性算法步驟:
檢査當前的剩余可用資源是否能滿足某個進程的最大需求,如果可以,就把該進程加入安全序列,并把該進程持有的資源全部回收不斷重復上述過程,看最終是否能讓所有進程都加入安全序列。
注:只要能找出一個安全序列,系統處于安全狀態。當然,安全序列可能有多個。如果分配了資源之后,系統中找不出任何安全序列,系統就進入了不安全狀態。
死鎖檢測和死鎖解除:
如果系統中既不采用預防死鎖也不采用避免死鎖的措施,系統就很有可能發生死鎖。這種情況下,系統應當提供兩種算法。
-
死鎖檢測算法:用于檢測系統狀態,確定系統中是否已經發生了死鎖。
-
所需數據結構:資源分配圖,又叫資源有向圖,如下:
圓圈代表一個進程,方框代表一類資源,方框內的圓圈代表該類資源的一個單位的資源。從進程到資源的有向邊為請求邊,表示該進程申請一個單位的該類資源;從資源到進程的邊為分配邊,表示該類資源已有一個資源分配給了該進程。圖中,進程
P1
已經分得了兩個R1
資源,請求了一個R2
資源;進程P2
分得了一個R1
資源和一個R2
資源,并又請求了一個R1
資源。 -
算法流程:嘗試將滿足運行條件的進程有向邊消去以簡化資源分配圖,如果能簡化說明系統占時沒有出現死鎖。如果此時系統的資源分配圖是不可簡化的,那么此時發生了系統死鎖(死鎖定理)。
資源分配圖簡化實例:
按照死鎖定理中,找出的進程為
P1
,因為它申請的資源可以被滿足,說明(a)
時刻沒有發生死鎖。
-
-
死鎖解除算法:該算法可將系統從死鎖中解脫出來。
- 資源剝奪法:將一些死鎖進程暫時掛起來,并且搶占它的資源,并將這些資源分配給其他的死鎖進程 ,要注意的是應該防止被掛起的進程長時間得不到資源而處于饑餓狀態。
- 撤銷進程法:強制撤銷部分甚至全部死鎖并剝奪這些進程的資源。撤銷的原則可以按照進程優先級和撤銷進程的代價高低進行。
- 進程回退法:讓一或多個進程回退到足以回避死鎖的地步,進程回退時自愿釋放資源而非被剝奪。這個方法要求系統保持進程的歷史信息,并設置還原點。
進程間通信(IPC
)的方式有哪些?
-
管道(
pipe
)管道可用于具有親緣關系的進程間的通信,通常指父子進程之間;同時,管道是一種半雙工的通信方式,數據只能單向流動。所謂管道,實際是內核管理的一串緩存,生命周期隨進程的創建而創建,隨進程的結束而銷毀。
在
Linux
系統(一種UNIX
系統)中,可以在C
代碼中調用pipe
系統調用創建管道并通信:#include <unistd.h> int pipe(int pipedes[2]); // 創建管道
成功返回
0
,失敗返回-1
,pipedes[0]
指向管道的讀端,pipedes[1]
指向管道的寫端。使用時,先創建管道并得到兩個分別指向管道兩端的文件描述符,父進程通過fork
函數創建子進程,然后子進程也有兩個文件描述符分別指向同一管道的兩端;父進程通過close(pipedes[0])
關閉管道讀端,子進程通過close(pipedes[1])
關閉管道寫端;父進程通過write(pipedes[1], ... )
系統調用往管道里寫,子進程通過read(pipedes[0], ...)
系統調用從管道里讀。(這里是父寫子讀,也可以反過來)備注:
頭文件
unistd.h
意為:unix std
,其提供了訪問POSIX
操作系統API
的功能。類似于Windows
系統提供的windows.h
。POSIX
,Portable Operating System Interface of UNIX
,可移植操作系統接口,是IEEE
為了方便在各種UNIX
系統之間移植軟件而定制的標準。示例:
#include <stdio.h> #include <unistd.h> #include <string.h> int main() { int pipedes[2]; if (pipe(pipedes)) { perror("pipe() fail"); return -1; } pid_t pid = fork(); if (pid < 0) { perror("fork() fail"); return -2; } // child process if (pid == 0) { close(pipedes[0]); char str[] = "Hello, parent!"; //這里用沒有發送結束符'\0' int len = write(pipedes[1], str, strlen(str)); printf("write len : %d \n", len); return 0; // parent process } else { close(pipedes[1]); char buf[1024]; int len = read(pipedes[0], buf, sizeof(buf)); //手動添加上結束符 buf[len] = '\0'; printf("read message from child process : %s , length %d \n", buf, len); return 0; } }
-
命名管道(
named pipe
)命名管道克服了管道沒有名字的限制,除具有管道所具有的功能外,它還允許無親緣關系進程間的通信。命名管道提供一個路徑名與之關聯,以文件形式存儲文件系統中,只要進程可以訪問該路徑,就可以通過該命名管道相互通信。
在
Linux
系統中,可以在shell
中通過mkfifo
命令創建命名管道;也可以在C
源代碼中通過調用int mkfifo(const char * pathname, mode_t mode)
系統調用創建,第一個參表示命名管道路徑,第二個表示文件權限,通常為0666
(可讀可寫)。要借助該命名管道通信的進程在源代碼通過open
、write
、read
、close
系統調用配合進行通信。(命名管道也是半雙工的)示例:
-
shell
中運行命令mkfifo mypipe
,會創建一個名為mypipe
的命名管道。
- 創建
namepipe_test_write.c
作為寫端:
``` #include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> // 一次傳輸的字符個數不超過127個字符,最后一位存'\0' #define LEN 128 int main() { int filedes = open("./mypipe", O_WRONLY); char buf[LEN] = {'\0'}; while (1) { printf("please input message: \n"); int i = 0; while (i < LEN - 1) { char ch = getchar(); if (ch == '\n') { break; } buf[i++] = ch; } //手動添加字符串結束符 buf[i] = '\0'; if (strcmp(buf, "quit") == 0) { break; } int len = write(filedes, buf, strlen(buf)); printf("write len : %d \n", len); printf("-----------------------------------\n"); } close(filedes); return 0; } ```
- 創建
namepipe_test_read.c
作為讀端:
``` #include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> int main() { int filedes = open("./mypipe", O_RDONLY); char buf[128] = {'\0'}; while (read(filedes, buf, sizeof(buf)) > 0) { printf("read from pipe : \n%s\n", buf); memset(buf, '\0', sizeof(buf)); printf("-----------------------------------\n"); } close(filedes); return 0; } ```
-
-
信號(
signal
)信號是在軟件層次上對中斷機制的一種模擬,是一種異步通信方式。信號用于通知接收進程有某種事件發生,接收到該信號的進程會相應的采取一些行動。
在
Linux
系統中,信號在signal.h
中定義,信號的名稱都以SIG
開頭,如:SIGINT
:終止信號 (ctrl + c
)SIGQUIT
:退出信號 (ctrl + \
)SIGSTOP
:暫停信號 (ctrl + z
)SIGSCONT
:繼續信號 (ctrl + z
)SIGALRM
:鬧鐘信號,常用作定時器SIGCHLD
:子進程狀態改變,父進程收到信號SIGKILL
:殺死信號(kill -9 pid
)
信號發送:通常,用戶可以通過按鍵、調用
kill
命令、或在C
源代碼中調用int kill(pid_t pid, int sig)
等系統調用來向另一個進程發送信號。信號處理:接收進程可以通過
signal
、sigaction
函數注冊對信號的處理方式。如:忽略(SIGKILL
,SIGSTOP
不能被忽略)、默認處理、自定義處理。 -
消息隊列(
message queue
)消息隊列本質上是位于內核空間的鏈表,鏈表的每個節點都是一條消息,每一條消息都有自己的消息類型,這個消息類型是由發送方和接收方約定。不同于管道,從消息隊列讀取數據時不一定要以先進先出的次序讀取,可以按消息的類型讀取,借此可以實現提前查看緊急消息的功能。消息隊列的生命周期是伴隨著內核的,如果沒有手動釋放消息隊列,他會直到操作系統關閉才釋放。
在
Linux
系統中,利用C
語言,通過調用int msgget(key_t key, int msgflg)
系統調用創建(不存在時)消息隊列并得到消息隊列的唯一標識符。兩個需要通信的進程中,只要傳入msgget
中的key
相同,他們得到的消息隊列標識符就相同。key
通常使用key_t ftok(const char *pathname, int proj_id)
系統調用來獲得。進程獲得消息隊列唯一標識符后可以使用int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
、ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
系統調用分別向消息隊列發送消息和從消息隊列接收消息。示例:
msg.h
:定義消息格式#ifndef _MSG_ #define _MSG_ typedef struct msg { long msgType; //必須是long型且是結構體第一個變量 char message[128]; //類型任意 //...可以有更多數據 } Msg; #endif
msg_queue_test_write.c
:發送端#include <stdio.h> #include <string.h> #include <sys/ipc.h> #include <sys/msg.h> #include <errno.h> #include "msg.h" int main() { key_t key = ftok("./", 2021); if (key == -1) { perror("ftok() fail"); return -1; } //創建(不存在時)消息隊列并得到消息隊列的唯一標識符, 0666表示權限 int msgqid = msgget(key, IPC_CREAT | 0666); if (msgqid == -1) { perror("msgget() fail"); return -2; } //發送類型 1 ~ 5 共 5 種類型的消息 for (int i = 1; i < 6; i++) { Msg msg; msg.msgType = i; strcpy(msg.message, "Hello : "); //四字節int型轉化為字符串長度最多為12:一個符號 + 10個數字 + 一個'\0'結束符 char tmp[12] = {'\0'}; sprintf(tmp, "%d", i); //將i轉化為字符串并拼接至message strcat(msg.message, tmp); //最后一個參數:阻塞方式發送消息,如果消息隊列沒有空間接收新發送的消息則阻塞 int flag = msgsnd(msgqid, &msg, sizeof(msg) - sizeof(long), 0); if (flag == -1) { printf("msgsnd(): send type: %d failed: %s", i, strerror(errno)); } } return 0; }
msg_queue_test_read.c
:接收端#include <stdio.h> #include <string.h> #include <sys/ipc.h> #include <sys/msg.h> #include <errno.h> #include "msg.h" int main() { key_t key = ftok("./", 2021); if (key == -1) { perror("ftok() fail"); return -1; } //創建(不存在時)消息隊列并得到消息隊列的唯一標識符, 0666表示權限 int msgqid = msgget(key, IPC_CREAT | 0666); if (msgqid == -1) { perror("msgget() fail"); return -2; } Msg msg; // 第二個參數:指明消息長度(不包含消息類型) // 倒數第二個參數: 大于0時表示獲取制定類型的消息,如果等于0表示獲取最前面的消息 // 最后一個參數:如果消息隊列為空就阻塞,直到讀取到消息 int len = msgrcv(msgqid, &msg, sizeof(Msg) - sizeof(long), 0, 0); if (len < 0) { perror("msgrcv() fail"); } printf("receive %d length: %s \n", len, msg.message); return 0; }
編譯后在兩個終端中分別開啟讀端和寫端(無論先后),消息隊列開始為空,如果先啟動讀端,讀端會被阻塞,直到寫端寫了數據后,讀端讀取到數據了才會退出。該例中,寫端一次寫入五個消息,讀端可以成功讀取五次,第六次讀取會被阻塞,直到消息隊列又有新的消息到達。如下:(編譯運行環境:
ubuntu 20.14
。在wsl2(ubuntu 20.14)
中編譯可以通過,但運行時提示沒有相應實現)以上代碼并沒有釋放消息隊列(
msgctl(msgqid, IPC_RMID, 0)
函數可以釋放),因此,程序運行結束后,通過命令ipcs -q
仍然可以看到我們創建的消息隊列(系統重啟后消失):最后,我們可以通過命令
ipcrm -q msqid
釋放消息隊列。
-
共享內存(
shared memory
)不同程序擁有各自獨立的邏輯地址空間,不能相互訪問。共享內存通過將一塊物理內存映射到不同進程的邏輯地址空間,使得他們可以訪問同一塊物理內存,從而實現共享內存。訪問共享內存區域和訪問進程獨有的內存區域一樣快,讀取和寫入的過程中不用像管道和消息隊列那在用戶態與內核態之間拷貝信息,因此,共享內存是最快的進程間通信形式。由于多個進程共享同一塊內存區域,為保證正確通信,需要借助同步機制,如:信號量,來進行同步。
在
Linux
系統中,利用C
語言,通過調用int shmget(key_t key, size_t size, int shmflg)
系統調用創建(不存在時)共享內存并得到共享內存的唯一標識符,其中key
和消息隊列中提到的key
相同。進程獲得共享內存唯一標識符后通過調用void *shmat(int shm_id, const void *shm_addr, int shmflg)
系統調用建立(attach
)用戶進程空間到共享內存的映射,得到指向共享內存的指針。根據該指針就可以利用系統讀寫函數向共享內存中寫數據或者從共享內存讀取數據。通信完成后,進程通過調用int shmdt(const void *shmaddr)
函數解除(detach
)映射關系,shmaddr
參數是之前調用shmat
時的返回值(指向共享內存的指針)。示例:
shared_memory_test_write.c
:寫端#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> int main() { //根據路徑和指定的id生成唯一的key key_t key = ftok("/", 2021); if (key == -1) { perror("ftok() fail"); } //創建(不存在時)大小為1024KB的共享內存,0666代表權限 int shmid = shmget(key, 1024, IPC_CREAT | 0666); if (shmid == -1) { perror("shmget() fail"); } //attach,將共享內存映射到當前進程 //第二個參數:共享內存連接到當前進程中的地址,通常為0,表示讓系統來選擇 //第三個參數:shm_flg是一組標志位,通常為0 //返回共享內存地址 void *shm = shmat(shmid, 0, 0); if (shm == (void *)-1) { perror("shmat() fail"); } //將鍵盤輸入的數據寫入共享內存 fgets(shm, 1024, stdin); //detach,把共享內存從當前進程中分離出去 int flag = shmdt(shm); if (flag == -1) { perror("shmdt() fail"); } return 0; }
shared_memory_test_read.c
:讀端#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> int main() { //根據路徑和指定的id生成唯一的key key_t key = ftok("/", 2021); if (key == -1) { perror("ftok() fail"); } //創建(不存在時)大小為1024KB的共享內存,0666代表權限 int shmid = shmget(key, 1024, IPC_CREAT | 0666); if (shmid == -1) { perror("shmget() fail"); } //attach,將共享內存映射到當前進程 //第二個參數:共享內存連接到當前進程中的地址,通常為0,表示讓系統來選擇 //第三個參數:shm_flg是一組標志位,通常為0 //返回共享內存地址 void *shm = shmat(shmid, 0, 0); if (shm == (void *)-1) { perror("shmat() fail"); } //將鍵盤輸入的數據寫入共享內存 fputs(shm, stdout); //detach,把共享內存從當前進程中分離出去 int flag = shmdt(shm); if (flag == -1) { perror("shmdt() fail"); } return 0; }
編譯后先運行寫端,輸入
hello world
回車后程序退出;然后運行讀端,程序讀取并打印出hello world
。如下:(編譯運行環境:ubuntu 20.14
)以上代碼并沒有釋放共享內存(
shmctl(shmid, IPC_RMID, 0)
可以釋放),因此,程序運行結束后,通過命令ipcs -m
仍然可以看到我們創建的共享內存(系統重啟后消失)。最后,我們可以通過命令ipcrm -m shmid
釋放共享內存。 -
信號量(
semaphore
)信號量的原理是一種數據操作鎖的概念,可用于多個進程間的同步。它本身并不具備數據交換的功能,而是通過控制其他的通信資源來實現進程間通信。我們可以將其理解成一個具有原子性的計數器,每當有進程申請使用信號量,通過一個
P操作
來對信號量進行-1
操作,當計數器減到0
的時候就說明沒有資源了,其他進程繼續訪問就會被阻塞,當該進程執行完這段工作釋放臨界資源之后,就會執行V操作
來對信號量進行+1
操作,被阻塞的進程就會被喚醒。在
Linux
系統中,利用C
語言,通過調用int semget(key_t key, int nsems, int semflg)
系統調用創建(不存在時)信號量并得到信號量的唯一標識符,其中key
和前述key
相同。可以通過命令
ipcs -s
查看系統當前存在的信號量,通過命令ipcrm -s semid
可以釋放信號量。 -
套接字(
socket
)更為一般的進程間通信機制,可用于運行在不同主機上的進程之間通信。他通過
IP地址
和端口號
確定一臺主機上一個進程,常用于網絡編程。
備注:以上這些通信方式并不是所有的操作系統都提供了實現,即使操作系統提供了實現,編程語言也不一定提供了訪問的接口。以上消息隊列、共享內存和信號量都是基于
System V
規范的(另一種實現是基于POSIX
規范的)。
常見內存管理方式有哪些?
內存管理機制分為連續分配管理方式和非連續分配管理方式。前者為進程分配一個連續的內存空間,是古老的內存管理方式,常見的有單一連續分配、固定分區分配等。后者是充分利用離散的內存,將進程分散的裝入內存分區中;根據分區大小是否固定可以分為頁式管理(固定分區大小)和段式管理(不固定分區大小);還可以二者混用成段頁式管理。
什么是分頁存儲管理?什么是分段存儲管理?區別是什么?
分頁存儲管理:
分頁存儲管理極大的提高了內存利用率,他將實際的物理內存分為大小相等的塊,通常被稱為頁幀。頁式管理將用戶程序所需內存以離散的頁幀形式分配給他們。每個用戶程序都有自己的邏輯地址空間,邏輯空間也被劃分為與頁幀大小相同的頁面,邏輯頁面和物理頁幀是一一對應的關系。
那么CPU
尋址時,是如何完成邏輯地址到實際物內存地址的轉換呢?首先要知道,邏輯地址被劃分為高位和低位,高位代表當前邏輯地址所在頁面對應的頁號,低位代表的是頁內偏移,以32
位操作系統來說,他的邏輯地址共有32
位,如果頁面(由頁幀大小決定)大小為4KB
(4 * 1024 = 212,注:地址單位為B,字節。)則需要占用低12
位來表示頁內偏移。顯然,CPU
僅僅借助邏輯地址是無法完成尋址的,還需要借助進程頁表才能完成邏輯地址到物理地址的轉換,頁表中記錄的是頁面和頁幀的對應關系。開始尋址時,CPU
根據邏輯地址得到頁號和頁內偏移,查詢頁表可得到頁號對應頁幀在物理內存中的起始地址,頁幀起始地址加上頁內偏移即可得到實際的物理地址。如下圖:
分段存儲管理:
分頁存儲是從計算機的角度進行設計的,目的是為了提高內存的利用率,每一頁面并沒有實際意義。段式存儲管理從程序員和用戶角度出發,把程序分割成具有邏輯意義的段,例如:主程序段、子程序段、數據段等等,每一段的大小不定。段式管理將用戶程序所需內存以離散的內存段的形式分配給她們。借助段式管理容易實現數據的共享與保護。
那么分段存儲管理中,CPU
又是如何完成尋址的呢?分段存儲管理中,邏輯地址同樣被劃分為高位和低位,高位表示段號,低位表示段內偏移。僅僅根據段號和段內偏移尚無法完成尋址,還需要借助進程段表,段表記錄了邏輯段的大小(段長)以及邏輯段在內存中的起始地址(基址)。開始尋址時,CPU
先拿到指明的段號和段內偏移(由于段長不定,段號和段內偏移無法像分頁管理那樣根據邏輯地址和頁面大小直接計算出來頁號和頁內偏移,需要指明邏輯地址中哪部分表示段號,哪部分表示段內偏移,這也是段式管理中邏輯地址是二維的原因),繼續查詢段表可以得到邏輯段的基址(段表中的段長是用來檢查當前段的段內偏移是否超過段長而發生越界),基址加上段內偏移即可得到實際的物理地址。如下圖:
分頁管理和分段管理區別:
- 分頁管理是站在計算機角度進行設計,每一頁并無邏輯意義,目的是減少外部碎片,提高內存的利用率,對用戶不可見;段式管理站在程序員和用戶角度,是一種邏輯上的劃分,是為了滿足用戶的需要,對用戶是可見的,編程時需要指明段名和段內地址(匯編語言中指明了段名和段內地址就指明了邏輯地址的段號和段內偏移)。
- 分頁管理中,頁面大小是固定的;而分段管理中段的大小取決于具體程序代碼段,是變化的。
- 分頁管理中,邏輯地址是一維的;而分段管理中邏輯地址是二維的。
- 在實現對程序和數據的共享與保護時,分段管理中,程序和數據本就按邏輯段存儲在內存段中,容易實現對程序段、數據段的共享控制以及保護控制;而分頁管理中,邏輯上的代碼段或數據段是被分散的存儲在各個離散的內存頁幀當中,很難實現對邏輯程序段或邏輯數據段的共享與保護。
注:進程頁表和進程段表都存放于物理內存中,且一個頁表或段表是占用的連續空間。以上圖中為了方便表達沒有將頁表或段表畫在物理內存中。
段頁式存儲管理了解嗎?
在分頁存儲管理中,內存利用率高,不會產生外部碎片,但不方便實現數據的共享與保護;而分段存儲管理則剛好相反。段頁式存儲管理就是為了結合兩者的優點。簡單來說,段頁式存儲管理將邏輯空間劃分為邏輯段,邏輯段再劃分為邏輯頁面;而物理內存劃分為大小相同的頁幀,邏輯頁面和物理頁幀一一對應,并裝入物理頁幀當中。
在段頁式存儲管理中,邏輯地址被劃分為三段,由高到低依次代表段號、頁號、頁內偏移。CPU
尋址時需要借助段表和頁表,段表記錄了各個邏輯段對應頁表的物理內存起始地址,以及頁表長度;頁表則記錄了各個邏輯頁面對應物理頁幀的起始地址。 尋址開始時,CPU
首先拿到指明的段號并根據頁面(頁幀)大小計算出頁號和頁內偏移,CPU
根據段號和段表可以找到該邏輯段對應頁表的起始物理內存地址,再根據頁號和頁表找到對應頁幀首地址,該首地址加上頁內偏移即可得到實際的物理地址。(段表中的頁表長度用來檢查頁號是否越界)。如下圖:
虛擬存儲器(虛擬內存)了解嗎?
基于局部性原理,在程序裝入時,可以先裝入當前運行需要的部分,然后就可以開始啟動程序,而將其他的部分暫時留在外存。在程序執行時,如果訪問的信息不在內存中,由操作系統將所需要的部分調入內存;如果此時內存已經沒有空間給新調入的部分,那么操作系統按照某種淘汰策略將一部分舊的內容暫時換到外存上存放,然后再將新需要的部分調入內存,接著繼續執行程序。這樣,操作系統就可以執行比實際內存大的多的程序,就好像為用戶提供了一個比實際內存大的多的存儲器。
虛擬存儲器種類:
- 虛擬頁式存儲管理
- 虛擬段式存儲管理
- 虛擬段頁式存儲管理
局部性原理:
- 時間局部性原理:如果程序中的某條指令?旦執?,不久以后該指令可能再次執?;如果某數據被訪問過,不久以后該數據可能再次被訪問。
- 空間局部性原理:?旦程序訪問了某個存儲單元,在不久之后,其附近的存儲單元也將被訪問, 即程序在?段時間內所訪問的地址,可能集中在?定的范圍之內。
頁面淘汰(置換)算法有哪些?
最佳(
Optimal
)淘汰算法:淘汰將來最長時間內不再被訪問的頁面,該算法會保證最低的缺頁率,但它是無法實現的,可作為衡量其他算法優劣的一個標準。先進先出(
FIFO
)淘汰算法:淘汰最先進入的頁面。該算法將那些經常被訪問的頁面也被換出,從而使缺頁率升高。最近最久未使用淘汰算法(
LRU, Least Recently Used
): 淘汰最近最久未使用的頁面。-
時鐘(
CLOCK
)淘汰算法:該算法為每個頁面設置一個訪問位,再將內存中的頁面都通過指針鏈接成一個循環隊列。當某個頁被訪問時,其訪問位置1
。當需要淘汰一個頁面時,只需檢查頁的訪問位。如果是0
,就選擇該頁淘汰;如果是1
,暫不淘汰,將訪問位改為0,繼續檢查下一個頁面。若第一輪查找中所有的頁面都是1
,則這些頁面的訪問位會被依次置為0,在第二輪掃描中一定可以找到訪問位為0
的頁面去淘汰。改進時鐘淘汰算法:
簡單的時鐘置換算法僅考慮到了一個頁面最近是否被訪問過,改進時鐘淘汰算法還要考慮頁面是否被修改過,因此還需要給頁面增加一個修改位,表示當前頁面是否被修改過。在淘汰頁面時,第一輪首先查找第一個(0, 0)的頁(最近沒被訪問,也沒被修改)用于淘汰;沒找到則進行第二輪查找,查找第一個(0,1)的頁(最近沒被訪問,但被修改過)用于淘汰,本輪將所有查找過的頁的訪問位設為0。第二輪依然沒找到就進行第三輪查找,查找第一個(0, 0)的頁用于淘汰,本輪掃描不修改任何標志位。還是沒找到則進行第四輪查找,查找第一個(0,1)的頁淘汰。( 由于第二輪已將所有的頁的訪問位都設為0,因此第三輪、第四輪查找一定會選中一個頁,因此改進時鐘淘汰算法最多會進行四輪掃描。)