1、通過fork的方式在扰,產(chǎn)生4個進程P1,P2,P3,P4缕减,每個進程打印輸出自己的名字,例如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中的一個進程來執(zhí)行,因此他們是互斥的般又,且前驅(qū)為P1彼绷。在此之后,P2 P3執(zhí)行完畢之后會分別對S2 S3進行V操作產(chǎn)生各一個資源單位茴迁,因此他們是P4的前驅(qū)寄悯。
在進程產(chǎn)生方面,我們采用fork()調(diào)用的方式來實現(xiàn)堕义。首先在主進程中進行fork()調(diào)用猜旬,產(chǎn)生子進程2。通過判斷pid倦卖,之后依次產(chǎn)生子進程3洒擦、子進程4:
pid_1=getpid();
pid_2=fork();
if(pid_2==0)//子進程2
{ sem_wait(S1); printf("I am the process P2\n"); sem_post(S1); sem_post(S2); }
if(pid_2>0)//主進程1
{ pid_3=fork();
if(pid_3==0)//子進程3
{ sem_wait(S1); printf("I am the process P3\n"); sem_post(S1); sem_post(S3); }
if(pid_3>0)//主進程1
{ printf("I am the process P1\n"); sem_post(S1); pid_4=fork();
if(pid_4==0)//子進程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進行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)生字符,輸出線程消耗字符。
如果不考慮同步機制就讓兩個進程同時運行的話便會出現(xiàn)如下問題:
????輸入進程產(chǎn)生字符過快,buffer數(shù)組的資源被用盡拳锚,繼續(xù)輸入會導(dǎo)致數(shù)組越界或者之前輸入的字符還未打印便被覆蓋。
????輸出進程消耗字符過快寻行,繼續(xù)輸出則會訪問到為初始化的數(shù)組元素或者將之前打印過的字符再次打印
根據(jù)以上存在的問題霍掺,我們可以通過使用信號量來實現(xiàn)兩個進程之間的同步。經(jīng)分析我們需要初始化兩個信號量:full和empty拌蜘,其中full保證未打印的字符不超過10抗楔,empty保證存在需要打印的字符再進行打印。
同步機制如下所示:
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)過測試不同的輸入速度勺良,均能滿足題意。
兩個進程如下所示:
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ā)送和接收進程中打印輸出共享內(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īng)接收到響應(yīng)字符串陪蜻,功能正常邦马。
我們刪除互斥代碼再次進行測試,查看代碼得知這兩個進程也是使用信號量實現(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)跟蹤信號,???????????????????
????????????????????????// 并在進程沒有釋放該信號量而終止時医寿,操作系統(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è)置沟突,其作用是在信號量第一次使用前對它進行設(shè)置花颗。IPC_RMID:用于刪除一個已經(jīng)無需繼續(xù)使用的信號量標(biāo)識符。
兩個進程通過共享內(nèi)存?zhèn)鬏敂?shù)據(jù)惠拭,因共享內(nèi)存不可同時讀寫扩劝,因此采用二元信號量進行進程互斥,具體操作如下:
init: 設(shè)置信號量為0职辅,此時只允許寫入棒呛,不允許讀取(因為共享內(nèi)存沒有數(shù)據(jù));
Sender: 在sem=0時,寫入數(shù)據(jù)到共享內(nèi)存(阻塞讀);寫入完成后域携,sem=1,此時可以讀取簇秒,不可以寫入;
Receiver: 在sem=1時秀鞭,讀取數(shù)據(jù)趋观;讀取完成后,sem=0,此時只允許寫入锋边。
下面對刪除互斥代碼的程序進行編譯運行:
可以看到拆内,如果刪去相關(guān)的互斥代碼Sender進程由于scanf的占用并無具體問題,但Rec進程并不會等待Sender進程宠默,一直在掃描共享內(nèi)存并打印到屏幕上麸恍,因此當(dāng)Sender發(fā)送消息時,Rec也會及時更新,并不斷打印出來抹沪。
我們在進程輸出時將其內(nèi)存打印出刻肄,下面為修改程序后編譯執(zhí)行的結(jié)果:
如圖所示,兩個進程顯示的內(nèi)存地址并不一致融欧,這與我們內(nèi)存共享的預(yù)期的機制不符敏弃。經(jīng)過之前的實驗與查詢資料,這是由于虛擬內(nèi)存機制與內(nèi)存隨機化導(dǎo)致的噪馏。
對于進程來說麦到,使用的都是虛擬地址。每個進程維護一個單獨的頁表欠肾。
頁表是一種數(shù)組結(jié)構(gòu)瓶颠,存放著各虛擬頁的狀態(tài),是否映射刺桃,是否緩存粹淋。
1)數(shù)組的索引號,表示虛擬頁號
2)數(shù)組的值若為null瑟慈,表示未映射的頁若非null桃移,第一位表示有效位,為1葛碧,表明緩存的頁借杰;為0,表明未緩存的頁进泼。其余位表示緩存到的物理頁號第步。
我們運行的兩個進程在初始化的時候使用了shmat函數(shù),此函數(shù)的作用是將共享內(nèi)存空間掛載到進程中缘琅,實則就是對進程分配字符串的虛擬內(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ū)溢出攻擊無法達到目的淑仆。
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
這里為了研究同步機制桥温,對原代碼進行了一些修改引矩,首先我們來研究無名管道:
/* * 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)遠管道的兩端履腋,一段負責(zé)寫數(shù)據(jù)珊燎,一段負責(zé)讀數(shù)據(jù)惭嚣。我們這里將父進程設(shè)置為讀進程遵湖,子進程設(shè)置為寫進程,結(jié)果如下:
由此我們可以知道晚吞,通信功能正常延旧。我們使發(fā)送進程sleep 2s,接收進程sleep 1s來查看研究進程是否正常:
我們繼續(xù)修改代碼槽地,連續(xù)發(fā)送三次消息迁沫,查看結(jié)果:
可以看到輸出進程是按照輸入進程輸入的順序輸出數(shù)據(jù),并且當(dāng)輸入進程沒有數(shù)據(jù)輸入捌蚊,即管道中沒有數(shù)據(jù)的時候集畅,輸出進程會阻塞。實現(xiàn)了同步機制缅糟。
通過實驗挺智,我們對無名管道的總結(jié)如下:
無名管道由一個在基本文件系統(tǒng)存儲設(shè)備上的INODE,一個與其相連的內(nèi)存INODE窗宦,兩個打開文件控制塊(分別對應(yīng)管道的信息發(fā)送端和信息接收端)及其所屬進程的描述信息來標(biāo)識赦颇,在系統(tǒng)執(zhí)行PIPE(P)命令行之后生成。并在P[0]中返回管道的讀通道打開文件描述等赴涵,在P[1]中返回管道的寫通道打開文件描述符媒怯。
從結(jié)構(gòu)上看,無名管道沒有文件路徑名髓窜,不占用文件目錄項扇苞,因此文件目錄結(jié)構(gòu)中的鏈表不適用于這種文件,它只是存在于打開文件結(jié)構(gòu)中的一個臨時文件,隨其所依附的進程的生存而生存杨拐,當(dāng)進程終止時祈餐,無名管道也隨之消亡。送入管道的信息一旦被讀進程取用就從管道中消失了哄陶,讀寫操作之間符合先進先出的隊列原則帆阳。???
管道文件是進程間通信的工具,為了盡量少的占用系統(tǒng)存儲資源屋吨,一般系統(tǒng)均將其限制為最大長度為4096(PIPSIZ)字節(jié)的小型文件蜒谤。當(dāng)欲寫入的消息超過4096字節(jié)時,就產(chǎn)生了讀至扰、寫進程之間的同步問題鳍徽。
首先寫操作查找PIPE文件中當(dāng)前指針的偏移量F-OFFSET,然后從此位置開始盡量寫入信息敢课,當(dāng)長度達到4096字節(jié)時阶祭,系統(tǒng)控制寫進程進入睡眠狀態(tài),一直等待讀進程取走全部信息時直秆,文件長度指針置0濒募,寫進程才被喚醒繼續(xù)工作。??
?為防止多個進程同時讀寫一個管道文件而產(chǎn)生混亂圾结,在管道文件的INODE標(biāo)志字I-FLAY項中設(shè)置了ILOCK標(biāo)志項瑰剃,以設(shè)置軟件鎖的方式實現(xiàn)多進程間對管道文件的互斥使用。
無名管道存在著如下兩個嚴重的缺點:
第一筝野,無名管道只能用于連接具有共同祖先的進程晌姚。???
第二,無名管道是依附進程而臨時存在的歇竟。
接下來我們研究有名管道挥唠,通過閱讀代碼我們可以發(fā)現(xiàn):有名管道可用于更為廣泛的進程之間的通信,但其區(qū)別于無名通道的一點則是通信雙方必須同時存在焕议,否則便會阻塞宝磨。有名管道的開啟需要創(chuàng)建相關(guān)文件,隨后兩個文件再對文件進行操作号坡,但寫入和讀取的數(shù)據(jù)并不會存儲在文件中懊烤,而是直接置于內(nèi)存中。由此我們也可以知道其讀寫操作是同時進行的宽堆。寫進程fifo_send分為四個步驟執(zhí)行腌紧,首先判斷當(dāng)前目錄下是否已經(jīng)存在my_fifo文件,不存在的話在當(dāng)前目錄下通過mkfifo()函數(shù)創(chuàng)建FIFO類型的文件my_fifo畜隶;再通過open()函數(shù)打開my_fifo文件壁肋,最后向文件中寫入消息号胚;讀進程的過程和寫進程的類似,沒有了創(chuàng)建fifo文件的過程浸遗。
為了更好的研究有名管道的機制猫胁,我們分別研究以下情況:
如圖所示,當(dāng)寫進程單獨運行時跛锌,盡管管道中不存在數(shù)據(jù)弃秆,但其仍處于阻塞狀態(tài),隨后讀進程進入之后髓帽,讀寫進程之間實現(xiàn)了通信菠赚,進程得以工作并結(jié)束,這與預(yù)期想法相符合郑藏。
接下來我們研究另一種情況衡查,先單獨運行讀進程,再運行寫進程必盖,發(fā)現(xiàn)讀進程單獨運行時被阻塞拌牲,但當(dāng)寫進程運行后仍然被阻塞,這是因為我們上次實驗的管道文件并未刪除歌粥,所以讀進程使用的是之前的文件塌忽,而寫進程在創(chuàng)建文件之前會先檢查是否已有同名文件存在(此文件一般由上次運行該程序時留下,因為該程序在退出時并沒有刪除相關(guān)文件)阁吝,存在則刪除原文件再創(chuàng)建新文件砚婆。所以械拍,由于文件的殘留突勇,讀進程先運行之后先訪問了原先的文件,進入阻塞狀態(tài)坷虑,而寫進程在運行之后檢測到原文件的存在甲馋,將其進行了刪除并創(chuàng)建了新文件,如此便相當(dāng)于兩個進程并不在一個有名管道兩邊迄损,處于阻塞狀態(tài)定躏。
由此,我們可以看出芹敌,有名管道實現(xiàn)了同步機制痊远,但較依賴于管道文件。
有名管道和無名管道基本相同氏捞,但也有不同點:無名管道只能由父子進程使用碧聪;但是通過有名管道,不相關(guā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ā)送端傳送的消息都會加入一個消息隊列,寫進程在此機制中不會被阻塞译蒂,其寫入的字符串會一直被添加至隊列的末端曼月,而讀進程會從隊列的首端一直讀取消息,消息節(jié)點一旦被讀取便會移除隊列柔昼。當(dāng)隊列中不含其需要類型的消息時便會阻塞哑芹。在消息隊列提供了一種從一個進程向另一個進程發(fā)送一個數(shù)據(jù)塊的方法。 ?每個數(shù)據(jù)塊都被認為含有一個類型捕透,接收進程可以獨立地接收含有不同類型的數(shù)據(jù)結(jié)構(gòu)聪姿。我們可以通過發(fā)送消息來避免命名管道的同步和阻塞問題。但是消息隊列與命名管道一樣乙嘀,每個數(shù)據(jù)塊都有一個最大長度的限制末购。Linux用宏MSGMAX和MSGMNB來限制一條消息的最大長度和一個隊列的最大長度。消息隊列機制如下圖所示:
5虎谢、閱讀Pintos操作系統(tǒng)盟榴,找到并閱讀進程上下文切換的代碼,說明實現(xiàn)的保存和恢復(fù)的上下文內(nèi)容以及進程切換的工作流程嘉冒。
根據(jù)pintos的文件組織方式曹货,與進程相關(guān)的定義大都在threads/thread.h部分咆繁,于是我們在其中尋找上下文切換方面的代碼,首先我們可以看到thread的結(jié)構(gòu)體:
tid_t?tid:線程的線程標(biāo)識符顶籽。每個線程必須具有在內(nèi)核的整個生命周期內(nèi)唯一的tid玩般。默認情況下,tid_t是int的typedef礼饱,每個新線程接收數(shù)字上的下一個更高的tid坏为,從初始進程的1開始。
enum thread_status?status:線程的狀態(tài)镊绪,一共有以下四種:
????THREAD_RUNNING:線程在給定時間內(nèi)正在運行匀伏。可以通過 thread_current()函數(shù)返回正在運行的線程蝴韭。
????THREAD_READY:該線程已準備好運行够颠,但它現(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(準備好運行的線程列表)或sema_down(等待信號量的線程列表)岩四。
uint32_t *pagedir:頁表指針,用于將進程結(jié)構(gòu)的虛擬地址映射到物理地址哥攘。
unsigned?magic:始終設(shè)置為THREAD_MAGIC剖煌,它只是threads / thread.c中定義的任意數(shù)字,用于檢測堆棧溢出逝淹。
在這里并沒有明顯的看到關(guān)于上下文切換的函數(shù)定義耕姊,暫時考慮其在進程函數(shù)定義中有所體現(xiàn),因此接下來我們查看進程函數(shù)部分:
通過注釋我們可以知道這一個結(jié)構(gòu)體與上下文進程切換相關(guān)栅葡,于是我們對這個結(jié)構(gòu)體進行分析:schedule()是負責(zé)切換線程的主要函數(shù)茉兰,其主要被thread_block(),thread_exit()欣簇,和thread_yield()這三次函數(shù)調(diào)用邦邦。
下面我們仔細分析一下此函數(shù)的具體實現(xiàn):首先其定義了三個thread結(jié)構(gòu)體的指針,均為局部變量醉蚁,cur指針指向running_thread ()函數(shù)的返回值燃辖。經(jīng)過查看實現(xiàn),它定位了當(dāng)前進程网棍。因此cur指針就是當(dāng)前運行進程的指針黔龟。
next_thread_to_run()此函數(shù)選擇并返回要調(diào)度的下一個線程。應(yīng)該從運行隊列返回一個線程滥玷,除非運行隊列為空氏身。如果運行隊列為空,返回idle_thread惑畴。值得注意的是如果正在運行的線程可以繼續(xù)運行蛋欣,它便仍在運行隊列中。于是這個函數(shù)的作用是返回下一個執(zhí)行進程的指針如贷。
最后一個prev指針這里定義為了NULL陷虎,說明和這里關(guān)系不大。
接下來是三個判斷杠袱,分別保證了此時中斷關(guān)閉(程序不能被中斷)尚猿、當(dāng)前進程不在運行狀態(tài)以及存在下一個進程。
經(jīng)過這三個判斷后楣富,便是上下文切換的核心部分:如果當(dāng)前進程和下一個進程不相等凿掂,則調(diào)用switch_threads (cur, next)將當(dāng)前進程和下一個進程進行切換。
switch_threads (cur, next)函數(shù)定義于switch.S中纹蝴,.S文件是匯編文件:
?分析一下這個匯編代碼: 先4個寄存器壓棧保存寄存器狀態(tài)(保護作用)庄萎, 這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è)計要求的侨舆, 必須保護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)前運行進程的指針并保證此時程序不能被中斷茵乱。接著其會將當(dāng)其運行進程的狀態(tài)改變?yōu)門HREAD_RUNNING以及初始化其時間切片茂洒,這可以看做切換進程后對新進程的一個激活。最后的部分表示如果我們切換的線程正在死亡瓶竭,銷毀它的struct線程督勺。而我們傳入的prev一定為NULL,所以在切換過程中這一部分并不會執(zhí)行在验。
下圖為進程上下文切換流程圖:
代碼鏈接:BJTU_operating-system-lesson/Lab3 at master · Jerlllly/BJTU_operating-system-lesson · GitHub