Nachos是什么
Nachos (Not Another Completely Heuristic Operating System),是一個教學用操作系統,提供了操作系統框架:
- 線程
- 中斷
- 虛擬內存(位圖管理所有物理頁,虛擬地址與物理地址之間的轉換等)
- 同步與互斥機制(鎖、條件變量、信號量),讀者寫者問題,生產者消費者問題,BARRIER問題等
- 線程調度(基於優先級可搶占式調度,時間片輪轉算法,FIFO調度)
- 文件系統
- 系統調用
- 機器指令、匯編指令、寄存器
……
Nachos模擬了一個MIPS模擬器,運行用戶程序。


目錄結構
.
├── COPYRIGHT
├── gnu-decstation-ultrix // 交叉編譯工具鏈
├── nachos-3.4.zip // 未經任何修改的源碼和交叉編譯工具,實驗就是修改源碼完善各個模塊的功能
├── README
└── nachos-3.4 // 實驗過程中完善的代碼
├── test // 該目錄下編寫用戶自己的程序,需要修改Makfile添加自己的文件
├── bin // 用戶自己的程序需要利用coff2noff轉換,才能在nachos下跑起來
├── filesys // 文件系統管理
│ ├── directory.cc // 目錄文件,由目錄項組成,目錄項里記錄了文件頭所在扇區號
│ ├── directory.h
│ ├── filehdr.cc
│ ├── filehdr.h // 文件頭數據結構,通過索引記錄了文件內容實際存儲的所有扇區號
│ ├── filesys.cc // 文件系統數據結構,創建/刪除/讀/寫/修改/重命名/打開/關別等接口
│ ├── filesys.h
│ ├── fstest.cc
│ ├── Makefile
│ ├── openfile.cc // 管理所有打開的文件句柄
│ ├── openfile.h
│ ├── synchdisk.cc // 同步磁盤類,加鎖保證互斥和文件系統的一致性
│ ├── synchdisk.h
│ └── test
├── machine // 機器硬件模擬
│ ├── console.cc // 終端
│ ├── console.h
│ ├── disk.cc // 磁盤
│ ├── disk.h
│ ├── interrupt.cc // 中斷處理器,利用FIFO維護一個中斷隊列
│ ├── interrupt.h
│ ├── timer.cc // 模擬硬件時鍾,用於時鍾中斷
│ ├── timer.h
│ ├── translate.cc // 用戶程序空間虛擬地址和物理之間的轉換類
│ └── translate.h
├── network // 網絡系統管理
│ ├── Makefile
│ ├── nettest.cc
│ ├── post.cc
│ ├── post.h
│ └── README
├── threads // 內核線程管理
│ ├── list.cc // 工具模塊 定義了鏈表結構及其操作
│ ├── list.h
│ ├── main.cc // main入口,可以傳入argv參數
│ ├── Makefile
│ ├── scheduler.cc // 調度器,維護一個就緒的線程隊列,時間片輪轉/FIFO/優先級搶占
│ ├── scheduler.h
│ ├── stdarg.h
│ ├── switch.c // 線程啟動和調度模塊
│ ├── switch.h
│ ├── switch-old.s
│ ├── switch.s // 線程切換
│ ├── synch.cc // 同步與互斥,鎖/信號量/條件變量
│ ├── synch.dis
│ ├── synch.h
│ ├── synchlist.cc // 類似於一個消息隊列
│ ├── synchlist.h
│ ├── system.cc // 主控模塊
│ ├── system.h
│ ├── thread.cc // 線程數據結構
│ ├── thread.h
│ ├── threadtest.cc
│ ├── utility.cc
│ └── utility.h
├── userprog // 用戶進程管理
│ ├── addrspace.cc // 為noff文件的代碼段/數據段分配空間,虛擬地址空間
│ ├── addrspace.h
│ ├── bitmap.cc // 位圖,用於管理扇區的分配和物理地址的分配
│ ├── bitmap.h
│ ├── exception.cc // 異常處理
│ ├── Makefile
│ ├── progtest.cc // 測試nachos是否可執行用戶程序
│ └── syscall.h // 系統調用
└── vm // 虛擬內存管理
└── Makefile // 多線程編譯: make -j4
└── Makefile.common // 各個模塊公共的Makefile內容存放到這里面
└── Makefile.dep // 依賴
環境
選擇Linux或Unix系統,安裝32位GCC開發環境,安裝32的ubuntu。
源碼獲取
https://github.com/icoty/nachos-3.4-Lab
內容一:總體概述
本次Lab針對的內容是實現線程機制最基本的數據結構——進程控制塊(PCB)。當一個進程創建時必然會生成一個相應的進程控制塊,記錄一些該線程特征,如進程ID、進程狀態、進程優先級,進程開始運行時間,在cpu上已經運行了多少時間,程序計數器,SP指針,根目錄和當前目錄指針,文件描述符表,用戶ID,組ID,指向代碼段、數據段和棧段的指針等(當然,Nachos簡化了進程控制塊的內容)。實驗的主要內容是修改和擴充PCB,主要難點在於發現修改PCB影響到的文件並進行修改。PCB是系統感知進程存在的唯一標志,且進程與PCB一一對應。可將PCB內部信息划分為:進程描述信息,進程控制信息,進程占有的資源和使用情況,進程的cpu現場。擴展字段如下:

內容二:任務完成情況
任務完成列表(Y/N)
| Exercise1 | Exercise2 | Exercise3 | Exercise4 | |
|---|---|---|---|---|
| 第一部分 | Y | Y | Y | Y |
具體Exercise的完成情況
Exercise1 調研
調研Linux或Windows中進程控制塊(PCB)的基本實現方式,理解與Nachos的異同。
linux-4.19.23調研:Linux中的每一個進程由一個task_struct數據結構來描述。task_struct也就是PCB的數據結構。task_struct容納了一個進程的所有信息,linux內核代碼中的task_struct在linux-4.19.23/include/linux/sched.h內。
Linux內核進程狀態:如下可分為運行態,可中斷和不可中斷態,暫停態,終止態,僵死狀態,掛起狀態等。
Linux內核進程調度:sched_info數據結構,包括被調度次數,等待時間,最后一次調度時間。
vi linux-4.19.23/include/linux/sched.h:
……
/* Used in tsk->state: */
#define TASK_RUNNING 0x0000 // 運行態
#define TASK_INTERRUPTIBLE 0x0001 // 可中斷
#define TASK_UNINTERRUPTIBLE 0x0002 // 不可中斷
#define __TASK_STOPPED 0x0004
#define __TASK_TRACED 0x0008
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x0010
#define EXIT_ZOMBIE 0x0020 // 僵死態
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED 0x0040
#define TASK_DEAD 0x0080
#define TASK_WAKEKILL 0x0100
#define TASK_WAKING 0x0200
#define TASK_NOLOAD 0x0400
#define TASK_NEW 0x0800
#define TASK_STATE_MAX 0x1000
……
……
struct sched_info {
#ifdef CONFIG_SCHED_INFO
/* Cumulative counters: */
/* # of times we have run on this CPU: */
unsigned long pcount;
/* Time spent waiting on a runqueue: */
unsigned long long run_delay;
/* Timestamps: */
/* When did we last run on a CPU? */
unsigned long long last_arrival;
/* When were we last queued to run? */
unsigned long long last_queued;
#endif /* CONFIG_SCHED_INFO */
};
……
時鍾與鎖:內核需要記錄進程在其生存期內使用CPU的時間以便於統計、計費等有關操作。進程耗費CPU的時間由兩部分組成:一是在用戶態下耗費的時間,一是在系統態下耗費的時間。這類信息還包括進程剩余的時間片和定時器信息等,以控制相應事件的觸發。
文件系統信息:進程可以打開或關閉文件,文件屬於系統資源,Linux內核要對進程使用文件的情況進行記錄。
虛擬內存信息:除了內核線程,每個進程都擁有自己的地址空間,Linux內核中用mm_struct結構來描述。
物理頁管理信息:當物理內存不足時,Linux內存管理子系統需要把內存中部分頁面交換到外存,並將產生PageFault的地址所在的頁面調入內存,交換以頁為單位。這部分結構記錄了交換所用到的信息。
多處理器信息:與多處理器相關的幾個域,每個處理器都維護了自己的一個進程調度隊列,Linux內核中沒有線程的概念,統一視為進程。
處理器上下文信息:當進程因等待某種資源而被掛起或停止運行時,處理機的狀態必須保存在進程的task_struct,目的就是保存進程的當前上下文。當進程被調度重新運行時再從進程的task_struct中把上下文信息讀入CPU(實際是恢復這些寄存器和堆棧的值),然后開始執行。
與Nachos的異同:Nachos相對於Linux系統的線程部分來講,要簡單許多。它的PCB僅有幾個必須的變量,並且定義了一些最基本的對線程操作的函數。Nachos線程的總數目沒有限制,線程的調度比較簡單,而且沒有實現線程的父子關系。很多地方需要完善。
Exercise2 源代碼閱讀
code/threads/main.cc:main.cc是整個nachos操作系統啟動的入口,通過它可以直接調用操作系統的方法。通過程序中的main函數,配以不同的參數,可以調用Nachos操作系統不同部分的各個方法。
code/threads/threadtest.cc:nachos內核線程測試部分,Fork兩個線程,交替調用Yield()主動放棄CPU,執行循環體,會發現線程0和線程1剛好是交替執行。
int main(int argc, char **argv)
{
int argCount; // the number of arguments
DEBUG('t', "Entering main");
(void) Initialize(argc, argv);
#ifdef THREADS
for (argc--, argv++; argc > 0; argc -= argCount, argv += argCount) {
argCount = 1;
switch (argv[0][1]) {
case 'q':
testnum = atoi(argv[1]);
argCount++;
break;
case 'T':
if(argv[0][2] == 'S')
testnum = 3;
break;
default:
testnum = 1;
break;
}
}
ThreadTest();
#endif
……
}
threadtest.cc
// 線程主動讓出cpu,在FIFO調度策略下能夠看到多個線程按順序運行
void SimpleThread(int which)
{
for (int num = 0; num < 5; num++) {
int ticks = stats->systemTicks - scheduler->getLastSwitchTick();
// 針對nachos內核線程的時間片輪轉算法,判斷時間片是否用完,如果用完主動讓出cpu
if(ticks >= TimerSlice){
currentThread->Yield();
}
// 多個線程同時執行該接口的話,會交替執行,交替讓出cpu
// currentThread->Yield();
}
}
root@yangyu-ubuntu-32:/mnt/nachos-3.4/code/threads#
root@yangyu-ubuntu-32:/mnt/nachos-3.4/code/threads# ./nachos -q 1
userId=0,threadId=0,prio=5,loop:0,lastSwitchTick=0,systemTicks=20,usedTicks=20,TimerSlice=30
userId=0,threadId=1,prio=5,loop:0,lastSwitchTick=20,systemTicks=30,usedTicks=10,TimerSlice=30
userId=0,threadId=0,prio=5,loop:1,lastSwitchTick=30,systemTicks=40,usedTicks=10,TimerSlice=30
userId=0,threadId=1,prio=5,loop:1,lastSwitchTick=40,systemTicks=50,usedTicks=10,TimerSlice=30
userId=0,threadId=0,prio=5,loop:2,lastSwitchTick=50,systemTicks=60,usedTicks=10,TimerSlice=30
userId=0,threadId=1,prio=5,loop:2,lastSwitchTick=60,systemTicks=70,usedTicks=10,TimerSlice=30
userId=0,threadId=0,prio=5,loop:3,lastSwitchTick=70,systemTicks=80,usedTicks=10,TimerSlice=30
userId=0,threadId=1,prio=5,loop:3,lastSwitchTick=80,systemTicks=90,usedTicks=10,TimerSlice=30
userId=0,threadId=0,prio=5,loop:4,lastSwitchTick=90,systemTicks=100,usedTicks=10,TimerSlice=30
userId=0,threadId=1,prio=5,loop:4,lastSwitchTick=100,systemTicks=110,usedTicks=10,TimerSlice=30
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!
Ticks: total 130, idle 0, system 130, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0
Cleaning up...
code/threads/thread.h:這部分定義了管理Thread的數據結構,即Nachos中線程的上下文環境。主要包括當前線程棧頂指針,所有寄存器的狀態,棧底,線程狀態,線程名。當前棧指針和機器狀態的定義必須必須放作為線程成員變量的前兩個,因為Nachos執行線程切換時,會按照這個順序找到線程的起始位置,然后操作線程上下文內存和寄存器。在Thread類中還聲明了一些基本的方法,如Fork()、Yield()、Sleep()等等,由於這些方法的作用根據名字已經顯而易見了,在此不再贅述。
code/threads/thread.cc: Thread.cc中主要是管理Thread的一些事務。主要接口如下:
- Fork(VoidFunctionPtr func,int arg):func是新線程運行的函數,arg是func函數的入參,Fork的實現包括分為幾步:分配一個堆棧,初始化堆棧,將線程放入就緒隊列。
- Finish():不是直接收回線程的數據結構和堆棧,因為當前仍在這個堆棧上運行這個線程。先將threadToBeDestroyed的值設為當前線程,在Scheduler的Run()內切換到新的線程時在銷毀threadToBeDestroyed。Yield()、Sleep()。這里實現的方法大多是都是原子操作,在方法的一開始保存中斷層次關閉中斷,並在最后恢復原狀態。
- Yield():當前線程放入就緒隊列,從scheduler就緒隊列中的找到下一個線程上cpu,以達到放棄CPU的效果。
Exercise3 擴展線程的數據結構
Exercise4 增加全局線程管理機制
這里我把Exercise3和Exercise4放在一起完成。
-
在Thread類中添加私有成員userId和threadId,添加公有接口getUserId()和getThreadId(),userId直接沿用Linux個getuid()接口。
-
system.h內部添加全局變量maxThreadsCount=128,全局數組threads[maxThreadsCount],每創建一個線程判斷並分配threadId。
-
-TS模仿Linux的PS命令打印所有線程信息,仔細閱讀list.cc代碼和scheduler.cc的代碼,就會發現可以直接用scheduler.cc::Print()接口,不用我們重新造輪子。
-
在system.cc中的void Initialize(int argc, char argv)函數體對全局數組初始化。如下我用root用戶執行分配的userId為0,切換到其他用戶userId會發生變化,線程id分別為0和1。當線程數超過128個線程時,ASSERT斷言報錯。
threadtest.cc:
void ThreadTest()
{
switch (testnum) {
case 1:
ThreadTest1();
break;
case 2:
ThreadCountLimitTest();
break;
case 3:
ThreadPriorityTest();
break;
case 4:
ThreadProducerConsumerTest();
break;
case 5:
ThreadProducerConsumerTest1();
break;
case 6:
barrierThreadTest();
break;
case 7:
readWriteThreadTest();
break;
default:
printf("No test specified.\n");
break;
}
}
// 線程最多128個,超過128個終止運行
void ThreadCountLimitTest()
{
for (int i = 0; i <= maxThreadsCount; ++i) {
Thread* t = new Thread("fork thread");
printf("thread name = %s, userId = %d, threadId = %d\n", t->getName(), t->getUserId(), t->getThreadId());
}
}
root@yangyu-ubuntu-32:/mnt/nachos-3.4/code/threads# ./nachos -TS
thread name = fork thread, userId = 0, threadId = 1
thread name = fork thread, userId = 0, threadId = 2
thread name = fork thread, userId = 0, threadId = 3
……
thread name = fork thread, userId = 0, threadId = 122
thread name = fork thread, userId = 0, threadId = 123
thread name = fork thread, userId = 0, threadId = 124
thread name = fork thread, userId = 0, threadId = 125
thread name = fork thread, userId = 0, threadId = 126
thread name = fork thread, userId = 0, threadId = 127
allocatedThreadID fail, maxThreadsCount:[128]
Assertion failed: line 73, file "../threads/thread.cc"
Aborted (core dumped)
root@yangyu-ubuntu-32:/mnt/nachos-3.4/code/threads#
內容三:遇到的困難以及解決方法
困難1
開始make編譯出錯,通過定位到具體行,復制出來手動執行,發現是gcc交叉編譯工具鏈路徑不對。
困難2
剛開始修改代碼驗證效果,重定義錯誤,外部文件全局變量使用方式不對導致。
內容四:收獲及感想
動手實踐很重要,不管你是做什么事、什么項目、什么作業,一定要落實到代碼和跑到程序上面來。絕知此事要躬行,學習來不得半點虛假。
內容五:對課程的意見和建議
暫無。
內容六:參考文獻
暫無。
