幾個概念
- CPU、虛擬CPU
- 進程
- 內存、虛擬地址空間
- 物理的CPU被OS虛擬成了多個虛擬的CPU,這些虛擬CPU分別運行各自的程序,這些正在運行的程序被稱為進程。
- 物理內存被OS虛擬成了多個虛擬地址空間,每個進程都有獨立的、自己的地址空間,程序的指令和數據都在地址空間中
- 磁盤被OS虛擬化為文件系統,文件是被多個程序共享的,它并不是多個虛擬的磁盤,不過也不是無條件共享,涉及到例如互斥共享等多個問題,以后再談。
1 Virtualizing the CPU
我們在Linux系統上運行C語言程序,體會一下虛擬化的意義。
Windows對多用戶的支持不是很好,相關的系統API可能也沒有,推薦適用Linux或Unix系統。
// cpu.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <assert.h>int main(int argc,int *argv[]){if(argc != 2){fprintf(stderr,"usage:cpu <string>\n");exit(1);}char *str = argv[1];for(int i = 0;i < 4;i++){sleep(1);printf("%s\n",str);}return 0;
}
這個程序很容易,需要運行的時候輸入一個參數,比如一個字符,值得解釋的是sleep(1)
,也就是讓程序暫停1秒,這非常重要,這意味著物理的CPU在這1s時間可以不用執行該進程,轉而執行其他進程。
注意,虛擬后的CPU,最終仍然要在真實物理CPU來執行,要想讓每個進程都得到執行,那就應該以合理的方式讓他們切換執行。
我們先運行一個進程試試看,輸入命令./cpu A1
:
打印了4個A1,并且是每隔1s打印一個,這與我們的預期相符。
接下來,我們同時運行多個進程試試看,輸入命令./cpu A1 & ./cpu B2 & ./cpu C3 &
按照直觀的理解,不應該是
A1
A1
A1
A1
B2
B2
B2
B2
C3
C3
C3
C3
不應該是這樣嗎?但是看起來這3個進程并不是順序執行的,而是并發執行的,也就是它們趁著其他進程在sleep
的時候,搶占了CPU去執行自己了(注意,我們假設計算機只有1個CPU,而且是單核的)。
這樣一來,就出現了圖中的亂序了。
我們也能充分的感受到,不要讓物理CPU閑著的重要理念,同時我們也能想象到,多個進程同時執行,就會涉及到更多的問題,如果是之前的順序執行,我們只需要進程1執行,其他等待–>進程1執行完成,進程2執行,其他等待–>進程2執行完成,進程3執行–>進程3執行完成。
也就是說,我們只需要等著一個程序執行完,再執行其他程序,這樣很簡單,但是效率非常低,比如,如果正在執行的程序不使用CPU,去“sleep”了,或者去找I/O設備“玩”了,CPU就只能呆著,其他程序也不能進來執行,CPU利用率很低。
為了避免這種問題,現代OS都采用了類似多道批處理的技術,正在執行的程序不執行時,其他程序會進入CPU執行,而不會允許CPU空閑,要榨干CPU!
就如上面的程序,當一個進程sleep的時候,其他進程就會進入CPU執行,但是,具體如何執行,取決于OS的調度程序,取決于OS設計的策略,所以目前我們還不能得知它具體是如何運作的(也許你可以查看Linux內核,不過如果你有此能力,就不會看見這篇文章了)。
1.1 補充:實例中的C語言知識
以下請自學
1.1.1 main函數參數,argc和argv
1.1.2 fprintf()
1.1.3 sleep()
2 Virtualizing Memory
我們先上代碼
// mem.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main(int argc,int *argv[]){int *p = malloc(sizeof(int));
// assert(p != NULL);printf("(%d) memory address of p: %08x\n",getpid(),(unsigned)p);*p = 0;for(int i = 0;i < 4;i++){sleep(1);*p = *p + 1;printf("(%d) p: %d\n",getpid(),*p);}return 0;
}
運行程序./mem
運行多個進程:./mem & ./mem & ./mem &
這里,我們依然能夠看到的是虛擬化CPU,不過,虛擬化內存在哪里呢?目前還看不出來,因為Linux默認是啟動地址空間隨機化的,這樣會讓系統更安全,不易受到攻擊,不過為了展現虛擬化內存,我們應該關掉它。
輸入命令sysctl -w kernel.randomize_va_space=0
,再輸入./mem & ./mem & ./mem &
我們可以看到,三個進程居然地址完全一樣!按理說,1個地址只能對應1個進程,所以,你就能體會到虛擬地址空間的含義了,這并不是真實的物理地址,它會通過某種機制,映射到真實物理地址去。
3 Sharing Disk Information
還記得我們剛才的兩個程序嗎?他們同時啟動了多個進程,并且,這幾個進程是同一個程序,也就是說,同一個存儲在磁盤的文件,被多次讀取到了內存,這也就意味著,磁盤信息是可以被同時多次讀取的,我們也可以說,這幾個進程共享了一個磁盤文件。
思考:為什么內存和CPU要虛擬化為多個,而磁盤卻是共享的?
- 進程是運行中的程序,它是“活的;
- 程序是靜止在磁盤中的指令和數據,它是“死的”。
對于正在運行的進程來說,我們需要為其獨立地分配一整套生態系統,保證它正常執行,并且每個程序運行時候的結果可能不同,所以,就虛擬地提供了CPU和地址空間,讓它們是相互獨立的;而對于靜止的指令和數據來說,完全沒有必要虛擬成多份,那反而是浪費空間,當然這是針對讀取而言,寫入還需要視情況,不過整體來說,讀取信息是及其場景的,將磁盤設為共享也是合理的。
另外要談的是,磁盤文件必須通過軟件和硬件協作的方式,使其持久地保存,而不是很快就消失了,或者被其他數據覆蓋掉了。
4 Concurrency
虛擬化對應的是進程,而并發對應的不僅僅是OS的進程,在OS之上的應用程序,也存在并發的問題,他就是多線程編程;虛擬化讓一個CPU能并發地執行多個進程,而一個進程,也能并發地執行多個線程。
你一定知道多線程編程,是的,就是那個,我們現在重新審視一下它。
// threads.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>volatile int counter = 0;
int loops;void *worker(void *arg) {int i;for (i = 0; i < loops; i++) {counter++;}return NULL;
}int main(int argc, char *argv[]) {if (argc != 2) {fprintf(stderr, "usage: threads <value>\n");exit(1);}loops = atoi(argv[1]);pthread_t p1, p2;printf("Initial value : %d\n", counter);pthread_create(&p1, NULL, worker, NULL);pthread_create(&p2, NULL, worker, NULL);pthread_join(p1, NULL);pthread_join(p2, NULL);printf("Final value : %d\n", counter);return 0;
}
我們進行編譯gcc threads.c -o threads -lpthread
,注意,<pthread.h>
不是Linux默認的庫,編譯鏈接需要加上參數-lpthread
,也就是需要鏈接額外的Import Library:libpthread.a
。
我們進行測試:
對于輸入的參數N,輸出結果應該是2N(先知道事實,看不懂多線程程序沒有關系),但是最后兩個,當參數足夠大,比如5億的時候,結果就詭異了。
這是由于計數器的值的更新不是原子操作,他需要:
- 內存–>寄存器
- 寄存器遞增
- 寄存器–>內存
3個步驟,但是,這幾個步驟可能被其他操作打斷,這就造成了結果的詭異。關于原子操作以后再說。
5 小結
我們談了幾件事兒
- 物理CPU – 虛擬化CPU – 多進程并發
- 物理內存 – 虛擬地址空間 – 進程獨立地址空間
- 磁盤(持久性) – 文件系統 – 共享磁盤信息
- OS之上的并發:單個進程中的多線程
版權聲明
本文是讀書筆記,來自于書籍《Operating System:Three Easy Pieces》