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é)果,如下圖所示:


這是因為題目中間僅要求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