操作系統(tǒng)——實驗三

1、通過fork的方式,產(chǎn)生4個進(jìn)程P1,P2,P3,P4,每個進(jìn)程打印輸出自己的名字,例如P1輸出“I am?the process P1”。要求P1最先執(zhí)行,P2、P3互斥執(zhí)行,P4最后執(zhí)行。通過多次測試驗證實現(xiàn)是否正確。

實驗代碼文件:1.c

根據(jù)題意我們可以知道,他們相互之間的前驅(qū)關(guān)系為P1-->P2、P1-->P3、P2-->P4、P3-->P4。由此,我們可以通過添加適當(dāng)?shù)男盘柫縼硗瓿蛇@些關(guān)系操作。

在這里,我們添加了三個信號量:

S1=sem_open("1",O_CREAT,0666,0);

S2=sem_open("2",O_CREAT,0666,0);

S3=sem_open("3",O_CREAT,0666,0);

根據(jù)我們設(shè)置的信號量關(guān)系:

sem_wait(S1); printf("I am the process P2\n"); sem_post(S1); sem_post(S2);

sem_wait(S1); printf("I am the process P3\n"); sem_post(S1); sem_post(S3);

printf("I am the process P1\n"); sem_post(S1); pid_4=fork();

sem_wait(S2); sem_wait(S3); printf("I am the process P4\n"); sem_post(S2); sem_post(S3);

我們可以看出來,P2和P3都在等待P1完成后對S1信號量的V操作。但是由于一次只產(chǎn)生一個資源單位的信號量,所以每次只能P2 P3中的一個進(jìn)程來執(zhí)行,因此他們是互斥的,且前驅(qū)為P1。在此之后,P2 P3執(zhí)行完畢之后會分別對S2 S3進(jìn)行V操作產(chǎn)生各一個資源單位,因此他們是P4的前驅(qū)。

在進(jìn)程產(chǎn)生方面,我們采用fork()調(diào)用的方式來實現(xiàn)。首先在主進(jìn)程中進(jìn)行fork()調(diào)用,產(chǎn)生子進(jìn)程2。通過判斷pid,之后依次產(chǎn)生子進(jìn)程3、子進(jìn)程4:

pid_1=getpid();

pid_2=fork();

if(pid_2==0)//子進(jìn)程2

{ sem_wait(S1); printf("I am the process P2\n"); sem_post(S1); sem_post(S2); }

if(pid_2>0)//主進(jìn)程1

{ pid_3=fork();

if(pid_3==0)//子進(jìn)程3

{ sem_wait(S1); printf("I am the process P3\n"); sem_post(S1); sem_post(S3); }

if(pid_3>0)//主進(jìn)程1

{ printf("I am the process P1\n"); sem_post(S1); pid_4=fork();

if(pid_4==0)//子進(jìn)程4

{ sem_wait(S2); sem_wait(S3); printf("I am the process P4\n"); sem_post(S2); sem_post(S3); } } }

通過編譯執(zhí)行,我們可以得到兩種執(zhí)行結(jié)果,如下圖所示:

1->2->3->4
-1->3->2->4

這是因為題目中間僅要求P2P3互斥,并未指定先后順序。根據(jù)上述的前驅(qū)關(guān)系,我們可以知道,P2與P3誰先得到了P1之行結(jié)束之后對S1進(jìn)行V操作得到的資源誰便先執(zhí)行。

2、火車票余票數(shù)ticketCount初始值為1000,有一個售票線程,一個退票線程,各循環(huán)執(zhí)行多次。添加同步機制,使得結(jié)果始終正確。要求多次測試添加同步機制前后的實驗效果。

未添加同步機制實驗代碼文件:2_1.c

添加同步機制實驗代碼文件:2_2.c

程序的大體結(jié)構(gòu)是典型的生產(chǎn)消費者問題模型。首先我們來看未添加同步機制之前的程序運行結(jié)果:

可以看到,結(jié)果并不是我們預(yù)期得到的,因為票總數(shù)是一定的1000,因此無論如何總票數(shù)只會<=1000。

我們再看添加了同步機制之后的程序:

通過上面的測試結(jié)果可以看出,添加同步機制之后不會發(fā)生類似前面的問題,最終的票數(shù)是期待得到的結(jié)果。上面的實驗證實了增加了同步機制之后的多線程并發(fā)程序有效的解決了臟數(shù)據(jù)的讀取問題。以下為同步機制:

sem_t empty,full; //定義全局同步信號量empty,full

pthread_mutex_t mutex; //定義一個全局互斥量,在不同函數(shù)中

sem_init (&empty, 0, 0); //初始化empty信號量

sem_init (&full, 0, 1000); //初始化full信號量

3、一個生產(chǎn)者一個消費者線程同步。設(shè)置一個線程共享的緩沖區(qū),char buf[10]。一個線程不斷從鍵盤輸入字符到buf,一個線程不斷的把buf的內(nèi)容輸出到顯示器。要求輸出的和輸入的字符和順序完全一致。(在輸出線程中,每次輸出睡眠一秒鐘,然后以不同的速度輸入測試輸出是否正確)。要求多次測試添加同步機制前后的實驗效果。

未添加同步機制實驗代碼文件:3_1.c

添加同步機制實驗代碼文件:3_2.c

未添加同步機制時的實驗結(jié)果:

添加同步機制后的實驗結(jié)果:

通過對比結(jié)果以及分析問題,我們可以知道:

此題是一個經(jīng)典的生產(chǎn)者和消費者問題:輸入線程產(chǎn)生字符,輸出線程消耗字符。

如果不考慮同步機制就讓兩個進(jìn)程同時運行的話便會出現(xiàn)如下問題:

????輸入進(jìn)程產(chǎn)生字符過快,buffer數(shù)組的資源被用盡,繼續(xù)輸入會導(dǎo)致數(shù)組越界或者之前輸入的字符還未打印便被覆蓋。

????輸出進(jìn)程消耗字符過快,繼續(xù)輸出則會訪問到為初始化的數(shù)組元素或者將之前打印過的字符再次打印

根據(jù)以上存在的問題,我們可以通過使用信號量來實現(xiàn)兩個進(jìn)程之間的同步。經(jīng)分析我們需要初始化兩個信號量:full和empty,其中full保證未打印的字符不超過10,empty保證存在需要打印的字符再進(jìn)行打印。

同步機制如下所示:

pthread_mutex_init( &mutex , NULL ); //初始化互斥量

empty=*sem_open("E",O_CREAT,0666,10);

full=*sem_open("F",O_CREAT,0666,0);

因此在添加同步機制之后,在程序運行的一開始,輸入線程未輸入任何字符,輸出線程被阻塞,不會打印任何字符。當(dāng)輸入字符串長度大于10時,輸出線程仍能按序輸出這12個字符。經(jīng)過測試不同的輸入速度,均能滿足題意。

兩個進(jìn)程如下所示:

void *producer( void *arg){ for(int i=0;;i++)

{

????sem_wait(&empty);

????scanf("%c",&buffer[i%10]);

????sem_post(&full); } }

void *consumer( void *arg )

{

for(int i=0;;i++)

{

sem_wait(&full);

printf("輸出:%c\n",buffer[i%10]);

sem_post(&empty); sleep(1);

} }

4、

a)通過實驗測試,驗證共享內(nèi)存的代碼中,receiver能否正確讀出sender發(fā)送的字符串?如果把其中互斥的代碼刪除,觀察實驗結(jié)果有何不同?如果在發(fā)送和接收進(jìn)程中打印輸出共享內(nèi)存地址,他們是否相同,為什么?

原Sender程序:4_Sender.c

原Receiver程序:4_Receiver.c

刪除互斥Sender程序:4_Sender_nosem.c

刪除互斥Receiver程序:4_Receiver_nosem.c

打印共享內(nèi)存地址Sender程序:4_Sender_print.c

打印共享內(nèi)存地址Receiver程序:4_Receiver_print.c

編譯執(zhí)行原Sender與Receiver程序:

我們可以發(fā)現(xiàn)Receiver進(jìn)程已經(jīng)接收到響應(yīng)字符串,功能正常。

我們刪除互斥代碼再次進(jìn)行測試,查看代碼得知這兩個進(jìn)程也是使用信號量實現(xiàn)的同步機制,其核心的函數(shù)為semop()和semctl()。

semop()函數(shù)它的作用是改變信號量的值,原型為:

int?semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);

sem_id是由semget()返回的信號量標(biāo)識符,sembuf結(jié)構(gòu)的定義如下:

struct?sembuf{????

short?sem_num;?// 除非使用一組信號量,否則它為0????

short?sem_op;??// 信號量在一次操作中需要改變的數(shù)據(jù),通常是兩個數(shù),一個是-1,即P(等待)操作,???????????????????

????????????????????????// 一個是+1,即V(發(fā)送信號)操作。????

short?sem_flg;?// 通常為SEM_UNDO,使操作系統(tǒng)跟蹤信號,???????????????????

????????????????????????// 并在進(jìn)程沒有釋放該信號量而終止時,操作系統(tǒng)釋放信號量};

semctl()函數(shù)該函數(shù)用來直接控制信號量信息,它的原型為:

int?semctl(int sem_id, int sem_num, int command, ...);

如果有第四個參數(shù),它通常是一個union semum結(jié)構(gòu),定義如下:

union?semun {????

int?val;????

struct?semid_ds *buf;????

unsigned?short?*arry;};

前兩個參數(shù)與前面一個函數(shù)中的一樣,command通常是下面兩個值中的其中一個SETVAL:用來把信號量初始化為一個已知的值。p 這個值通過union semun中的val成員設(shè)置,其作用是在信號量第一次使用前對它進(jìn)行設(shè)置。IPC_RMID:用于刪除一個已經(jīng)無需繼續(xù)使用的信號量標(biāo)識符。

兩個進(jìn)程通過共享內(nèi)存?zhèn)鬏敂?shù)據(jù),因共享內(nèi)存不可同時讀寫,因此采用二元信號量進(jìn)行進(jìn)程互斥,具體操作如下:

init: 設(shè)置信號量為0,此時只允許寫入,不允許讀取(因為共享內(nèi)存沒有數(shù)據(jù));

Sender: 在sem=0時,寫入數(shù)據(jù)到共享內(nèi)存(阻塞讀);寫入完成后,sem=1,此時可以讀取,不可以寫入;

Receiver: 在sem=1時,讀取數(shù)據(jù);讀取完成后,sem=0,此時只允許寫入。

下面對刪除互斥代碼的程序進(jìn)行編譯運行:

可以看到,如果刪去相關(guān)的互斥代碼Sender進(jìn)程由于scanf的占用并無具體問題,但Rec進(jìn)程并不會等待Sender進(jìn)程,一直在掃描共享內(nèi)存并打印到屏幕上,因此當(dāng)Sender發(fā)送消息時,Rec也會及時更新,并不斷打印出來。

我們在進(jìn)程輸出時將其內(nèi)存打印出,下面為修改程序后編譯執(zhí)行的結(jié)果:

如圖所示,兩個進(jìn)程顯示的內(nèi)存地址并不一致,這與我們內(nèi)存共享的預(yù)期的機制不符。經(jīng)過之前的實驗與查詢資料,這是由于虛擬內(nèi)存機制與內(nèi)存隨機化導(dǎo)致的。

對于進(jìn)程來說,使用的都是虛擬地址。每個進(jìn)程維護(hù)一個單獨的頁表。

頁表是一種數(shù)組結(jié)構(gòu),存放著各虛擬頁的狀態(tài),是否映射,是否緩存。

1)數(shù)組的索引號,表示虛擬頁號

2)數(shù)組的值若為null,表示未映射的頁若非null,第一位表示有效位,為1,表明緩存的頁;為0,表明未緩存的頁。其余位表示緩存到的物理頁號。

我們運行的兩個進(jìn)程在初始化的時候使用了shmat函數(shù),此函數(shù)的作用是將共享內(nèi)存空間掛載到進(jìn)程中,實則就是對進(jìn)程分配字符串的虛擬內(nèi)存映射到共享內(nèi)存的物理內(nèi)存,從而實現(xiàn)內(nèi)存的共享。所以雖然我們打印出來的內(nèi)存地址不一樣,但是它們實際映射的物理內(nèi)存地址是一致的。

ASLR(Address Space Layout Randomization)在2005年被引入到Linux的內(nèi)核 kernel 2.6.12 中,當(dāng)然早在2004年就以patch的形式被引入。隨著內(nèi)存地址的隨機化,使得響應(yīng)的應(yīng)用變得隨機。這意味著同一應(yīng)用多次執(zhí)行所使用內(nèi)存空間完全不同,也意味著簡單的緩沖區(qū)溢出攻擊無法達(dá)到目的。

b)有名管道和無名管道通信系統(tǒng)調(diào)用是否已經(jīng)實現(xiàn)了同步機制?通過實驗驗證,發(fā)送者和接收者如何同步的。比如,在什么情況下,發(fā)送者會阻塞,什么情況下,接收者會阻塞?

無名管道原pipe程序:4_pipe.c

有名管道原rcv程序:4_fifo_rcv.c

有名管道原snd程序:4_fifo_snd.c

這里為了研究同步機制,對原代碼進(jìn)行了一些修改,首先我們來研究無名管道:

/* * Filename: pipe.c */

#include <stdio.h>

#include <unistd.h> //for pipe()

#include <string.h> //for memset()

#include <stdlib.h> //for exit()

int main(){

????int fd[2];

????char buf[20];

????if(-1 == pipe(fd)) { perror("pipe"); exit(EXIT_FAILURE); }

????pid_t pid; pid = fork();

????if(!pid){ write(fd[1], "hello,world", 12);

????printf("發(fā)送已完成,內(nèi)容為:hello,world\n");

? ?memset(buf, '\0', sizeof(buf)); }

????else if(pid>0){ read(fd[0], buf, 12);

????printf("The message is: %s\n", buf); }

????else{ perror("fork"); exit(1); } return 0;}

可以看出來,程序通過pipe函數(shù)創(chuàng)建管道,函數(shù)傳遞一個整形數(shù)組fd,fd的兩個整形數(shù)表示的是兩個文件描述符,其中第一個用于讀取數(shù)據(jù),第二個用于寫數(shù)據(jù)。兩個描述符相當(dāng)遠(yuǎn)管道的兩端,一段負(fù)責(zé)寫數(shù)據(jù),一段負(fù)責(zé)讀數(shù)據(jù)。我們這里將父進(jìn)程設(shè)置為讀進(jìn)程,子進(jìn)程設(shè)置為寫進(jìn)程,結(jié)果如下:


由此我們可以知道,通信功能正常。我們使發(fā)送進(jìn)程sleep 2s,接收進(jìn)程sleep 1s來查看研究進(jìn)程是否正常:

我們繼續(xù)修改代碼,連續(xù)發(fā)送三次消息,查看結(jié)果:

可以看到輸出進(jìn)程是按照輸入進(jìn)程輸入的順序輸出數(shù)據(jù),并且當(dāng)輸入進(jìn)程沒有數(shù)據(jù)輸入,即管道中沒有數(shù)據(jù)的時候,輸出進(jìn)程會阻塞。實現(xiàn)了同步機制。

通過實驗,我們對無名管道的總結(jié)如下:

無名管道由一個在基本文件系統(tǒng)存儲設(shè)備上的INODE,一個與其相連的內(nèi)存INODE,兩個打開文件控制塊(分別對應(yīng)管道的信息發(fā)送端和信息接收端)及其所屬進(jìn)程的描述信息來標(biāo)識,在系統(tǒng)執(zhí)行PIPE(P)命令行之后生成。并在P[0]中返回管道的讀通道打開文件描述等,在P[1]中返回管道的寫通道打開文件描述符。

從結(jié)構(gòu)上看,無名管道沒有文件路徑名,不占用文件目錄項,因此文件目錄結(jié)構(gòu)中的鏈表不適用于這種文件,它只是存在于打開文件結(jié)構(gòu)中的一個臨時文件,隨其所依附的進(jìn)程的生存而生存,當(dāng)進(jìn)程終止時,無名管道也隨之消亡。送入管道的信息一旦被讀進(jìn)程取用就從管道中消失了,讀寫操作之間符合先進(jìn)先出的隊列原則。???

管道文件是進(jìn)程間通信的工具,為了盡量少的占用系統(tǒng)存儲資源,一般系統(tǒng)均將其限制為最大長度為4096(PIPSIZ)字節(jié)的小型文件。當(dāng)欲寫入的消息超過4096字節(jié)時,就產(chǎn)生了讀、寫進(jìn)程之間的同步問題。

首先寫操作查找PIPE文件中當(dāng)前指針的偏移量F-OFFSET,然后從此位置開始盡量寫入信息,當(dāng)長度達(dá)到4096字節(jié)時,系統(tǒng)控制寫進(jìn)程進(jìn)入睡眠狀態(tài),一直等待讀進(jìn)程取走全部信息時,文件長度指針置0,寫進(jìn)程才被喚醒繼續(xù)工作。??

?為防止多個進(jìn)程同時讀寫一個管道文件而產(chǎn)生混亂,在管道文件的INODE標(biāo)志字I-FLAY項中設(shè)置了ILOCK標(biāo)志項,以設(shè)置軟件鎖的方式實現(xiàn)多進(jìn)程間對管道文件的互斥使用。

無名管道存在著如下兩個嚴(yán)重的缺點:

第一,無名管道只能用于連接具有共同祖先的進(jìn)程。???

第二,無名管道是依附進(jìn)程而臨時存在的。

接下來我們研究有名管道,通過閱讀代碼我們可以發(fā)現(xiàn):有名管道可用于更為廣泛的進(jìn)程之間的通信,但其區(qū)別于無名通道的一點則是通信雙方必須同時存在,否則便會阻塞。有名管道的開啟需要創(chuàng)建相關(guān)文件,隨后兩個文件再對文件進(jìn)行操作,但寫入和讀取的數(shù)據(jù)并不會存儲在文件中,而是直接置于內(nèi)存中。由此我們也可以知道其讀寫操作是同時進(jìn)行的。寫進(jìn)程fifo_send分為四個步驟執(zhí)行,首先判斷當(dāng)前目錄下是否已經(jīng)存在my_fifo文件,不存在的話在當(dāng)前目錄下通過mkfifo()函數(shù)創(chuàng)建FIFO類型的文件my_fifo;再通過open()函數(shù)打開my_fifo文件,最后向文件中寫入消息;讀進(jìn)程的過程和寫進(jìn)程的類似,沒有了創(chuàng)建fifo文件的過程。

為了更好的研究有名管道的機制,我們分別研究以下情況:

如圖所示,當(dāng)寫進(jìn)程單獨運行時,盡管管道中不存在數(shù)據(jù),但其仍處于阻塞狀態(tài),隨后讀進(jìn)程進(jìn)入之后,讀寫進(jìn)程之間實現(xiàn)了通信,進(jìn)程得以工作并結(jié)束,這與預(yù)期想法相符合。

接下來我們研究另一種情況,先單獨運行讀進(jìn)程,再運行寫進(jìn)程,發(fā)現(xiàn)讀進(jìn)程單獨運行時被阻塞,但當(dāng)寫進(jìn)程運行后仍然被阻塞,這是因為我們上次實驗的管道文件并未刪除,所以讀進(jìn)程使用的是之前的文件,而寫進(jìn)程在創(chuàng)建文件之前會先檢查是否已有同名文件存在(此文件一般由上次運行該程序時留下,因為該程序在退出時并沒有刪除相關(guān)文件),存在則刪除原文件再創(chuàng)建新文件。所以,由于文件的殘留,讀進(jìn)程先運行之后先訪問了原先的文件,進(jìn)入阻塞狀態(tài),而寫進(jìn)程在運行之后檢測到原文件的存在,將其進(jìn)行了刪除并創(chuàng)建了新文件,如此便相當(dāng)于兩個進(jìn)程并不在一個有名管道兩邊,處于阻塞狀態(tài)。

由此,我們可以看出,有名管道實現(xiàn)了同步機制,但較依賴于管道文件。

有名管道和無名管道基本相同,但也有不同點:無名管道只能由父子進(jìn)程使用;但是通過有名管道,不相關(guān)的進(jìn)程也能交換數(shù)據(jù)。

c)消息通信系統(tǒng)調(diào)用是否已經(jīng)實現(xiàn)了同步機制?通過實驗驗證,發(fā)送者和接收者如何同步的。比如,在什么情況下,發(fā)送者會阻塞,什么情況下,接收者會阻塞?

客戶端代碼:4_Client.c

服務(wù)端代碼:4_Server.c

我們運行這兩個程序查看結(jié)果,功能正常執(zhí)行,在客戶端沒有發(fā)送消息的時候服務(wù)端阻塞,當(dāng)客戶端發(fā)送消息,服務(wù)端及時響應(yīng)。

我們修改Server.c的代碼,使得服務(wù)器的運行速度低于客戶端輸入速度,查看結(jié)果,可以看到,雖然在客戶端發(fā)送消息后,服務(wù)器沒有及時響應(yīng),但是在之后響應(yīng)的時候其順序并未錯亂,響應(yīng)正常,這說明其具備同步機制。

在此機制中,發(fā)送端傳送的消息都會加入一個消息隊列,寫進(jìn)程在此機制中不會被阻塞,其寫入的字符串會一直被添加至隊列的末端,而讀進(jìn)程會從隊列的首端一直讀取消息,消息節(jié)點一旦被讀取便會移除隊列。當(dāng)隊列中不含其需要類型的消息時便會阻塞。在消息隊列提供了一種從一個進(jìn)程向另一個進(jìn)程發(fā)送一個數(shù)據(jù)塊的方法。 ?每個數(shù)據(jù)塊都被認(rèn)為含有一個類型,接收進(jìn)程可以獨立地接收含有不同類型的數(shù)據(jù)結(jié)構(gòu)。我們可以通過發(fā)送消息來避免命名管道的同步和阻塞問題。但是消息隊列與命名管道一樣,每個數(shù)據(jù)塊都有一個最大長度的限制。Linux用宏MSGMAX和MSGMNB來限制一條消息的最大長度和一個隊列的最大長度。消息隊列機制如下圖所示:

5、閱讀Pintos操作系統(tǒng),找到并閱讀進(jìn)程上下文切換的代碼,說明實現(xiàn)的保存和恢復(fù)的上下文內(nèi)容以及進(jìn)程切換的工作流程。

根據(jù)pintos的文件組織方式,與進(jìn)程相關(guān)的定義大都在threads/thread.h部分,于是我們在其中尋找上下文切換方面的代碼,首先我們可以看到thread的結(jié)構(gòu)體:

tid_t?tid:線程的線程標(biāo)識符。每個線程必須具有在內(nèi)核的整個生命周期內(nèi)唯一的tid。默認(rèn)情況下,tid_t是int的typedef,每個新線程接收數(shù)字上的下一個更高的tid,從初始進(jìn)程的1開始。

enum thread_status?status:線程的狀態(tài),一共有以下四種:

????THREAD_RUNNING:線程在給定時間內(nèi)正在運行??梢酝ㄟ^ thread_current()函數(shù)返回正在運行的線程。

????THREAD_READY:該線程已準(zhǔn)備好運行,但它現(xiàn)在沒有運行。可以選擇線程以在下次調(diào)用調(diào)度程序時運行。就緒線程保存在名為ready_list的雙向鏈表中

????THREAD_BLOCKED:線程正在等待某些事務(wù),例如鎖定變?yōu)榭捎?,要調(diào)用的中斷。在通過調(diào)用thread_unblock(函數(shù))轉(zhuǎn)換到THREAD_READY狀態(tài)之前,線程不會再次調(diào)度。

????THREAD_DYING:切換到下一個線程后,調(diào)度程序?qū)N毀該線程。char?name[16]:線程命名的字符串,至少前幾個數(shù)組單元為字符。uint8_t *stack:線程的棧指針。當(dāng)線程運行時,CPU的堆棧指針寄存器跟蹤堆棧的頂部,并且該成員未使用。但是當(dāng)CPU切換到另一個線程時,該成員保存線程的堆棧指針。保存線程的寄存器不需要其他成員,因為必須保存的其他寄存器保存在堆棧中。

int?priority:線程優(yōu)先級,范圍從PRI_MIN(0)到PRI_MAX(63)。較低的數(shù)字對應(yīng)較低的優(yōu)先級,因此優(yōu)先級0是最低優(yōu)先級,優(yōu)先級63是最高優(yōu)先級。struct list_elem?allelem:用于將線程鏈接到所有線程的列表中。每個線程在創(chuàng)建時都會插入到此列表中,并在退出時刪除。應(yīng)該使用thread_foreach()函數(shù)來迭代所有線程。

struct list_elem?elem:用于將線程放入雙向鏈表:ready_list(準(zhǔn)備好運行的線程列表)或sema_down(等待信號量的線程列表)。

uint32_t *pagedir:頁表指針,用于將進(jìn)程結(jié)構(gòu)的虛擬地址映射到物理地址。

unsigned?magic:始終設(shè)置為THREAD_MAGIC,它只是threads / thread.c中定義的任意數(shù)字,用于檢測堆棧溢出。

在這里并沒有明顯的看到關(guān)于上下文切換的函數(shù)定義,暫時考慮其在進(jìn)程函數(shù)定義中有所體現(xiàn),因此接下來我們查看進(jìn)程函數(shù)部分:

通過注釋我們可以知道這一個結(jié)構(gòu)體與上下文進(jìn)程切換相關(guān),于是我們對這個結(jié)構(gòu)體進(jìn)行分析:schedule()是負(fù)責(zé)切換線程的主要函數(shù),其主要被thread_block(),thread_exit(),和thread_yield()這三次函數(shù)調(diào)用。

下面我們仔細(xì)分析一下此函數(shù)的具體實現(xiàn):首先其定義了三個thread結(jié)構(gòu)體的指針,均為局部變量,cur指針指向running_thread ()函數(shù)的返回值。經(jīng)過查看實現(xiàn),它定位了當(dāng)前進(jìn)程。因此cur指針就是當(dāng)前運行進(jìn)程的指針。

next_thread_to_run()此函數(shù)選擇并返回要調(diào)度的下一個線程。應(yīng)該從運行隊列返回一個線程,除非運行隊列為空。如果運行隊列為空,返回idle_thread。值得注意的是如果正在運行的線程可以繼續(xù)運行,它便仍在運行隊列中。于是這個函數(shù)的作用是返回下一個執(zhí)行進(jìn)程的指針。

最后一個prev指針這里定義為了NULL,說明和這里關(guān)系不大。

接下來是三個判斷,分別保證了此時中斷關(guān)閉(程序不能被中斷)、當(dāng)前進(jìn)程不在運行狀態(tài)以及存在下一個進(jìn)程。

經(jīng)過這三個判斷后,便是上下文切換的核心部分:如果當(dāng)前進(jìn)程和下一個進(jìn)程不相等,則調(diào)用switch_threads (cur, next)將當(dāng)前進(jìn)程和下一個進(jìn)程進(jìn)行切換。

switch_threads (cur, next)函數(shù)定義于switch.S中,.S文件是匯編文件:

?分析一下這個匯編代碼: 先4個寄存器壓棧保存寄存器狀態(tài)(保護(hù)作用), 這4個寄存器是switch_threads_frame的成員。然后全局變量thread_stack_ofs記錄線程和棧之間的間隙, 我們都知道線程切換有個保存現(xiàn)場的過程,來看34,35行, 先把當(dāng)前的線程指針放到eax中, 并把線程指針保存在相對基地址偏移量為edx的地址中。38,39: 切換到下一個線程的線程棧指針, 保存在ecx中, 再把這個線程相對基地址偏移量edx地址(上一次保存現(xiàn)場的時候存放的)放到esp當(dāng)中繼續(xù)執(zhí)行。這里ecx, eax起容器的作用, edx指向當(dāng)前現(xiàn)場保存的地址偏移量。簡單來說就是保存當(dāng)前線程狀態(tài), 恢復(fù)新線程之前保存的線程狀態(tài)。然后再把4個寄存器拿出來, 這個是硬件設(shè)計要求的, 必須保護(hù)switch_threads_frame里面的寄存器才可以destroy掉eax, edx, ecx。然后注意到現(xiàn)在eax(函數(shù)返回值是eax)就是被切換的線程棧指針。

我們由此得到一個結(jié)論, schedule先把當(dāng)前線程丟到就緒隊列,然后把線程切換如果下一個線程和當(dāng)前線程不一樣的話。

然后再看shedule最后一行的函數(shù)thread_schedule_tail做了什么, 這里參數(shù)prev是NULL或者在下一個線程的上下文中的當(dāng)前線程指針。根據(jù)函數(shù)的注釋我們可以得知其功能是:通過激活新線程的頁表完成線程切換,如果前一個線程正在死亡,則銷毀它。接下來一步步觀察其具體的執(zhí)行步驟。首先其也會獲取當(dāng)前運行進(jìn)程的指針并保證此時程序不能被中斷。接著其會將當(dāng)其運行進(jìn)程的狀態(tài)改變?yōu)門HREAD_RUNNING以及初始化其時間切片,這可以看做切換進(jìn)程后對新進(jìn)程的一個激活。最后的部分表示如果我們切換的線程正在死亡,銷毀它的struct線程。而我們傳入的prev一定為NULL,所以在切換過程中這一部分并不會執(zhí)行。

下圖為進(jìn)程上下文切換流程圖:

代碼鏈接:BJTU_operating-system-lesson/Lab3 at master · Jerlllly/BJTU_operating-system-lesson · GitHub

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容