如何從零開始寫一個簡單的操作系統?
看了這個:從零開始寫一個簡單的操作系統 求指教。
大二的時候,老師(中山大學萬海)對我們說:「如果有誰能自己寫一個內核出來,那麼,他平時可以不來聽課,也不用做平時作業,做出來還能加分,怎麼樣,有沒有人有興趣?」 和老師一番討價還價之後,我成為全年級幾百號人里唯一一個自己寫內核/整個學期都不去教室聽課/任何作業都不做的那個人(代表著我的身邊將沒有可以提供參考的人,任何資料都只能自己找)。 一開始買了《30天自製操作系統》,上面寫著需要軟盤還有其它的模擬器,我的初衷是寫一個可以燒在真機上一按開機鍵就能跑起來的那種,所以看了幾頁後就丟開了。後來又找了國人寫的一本,也不是特別符合,也丟開了。 這時我看到了那本教材(俗稱綠寶書),約莫800頁。之後的兩個星期里,我每天泡圖書館,以每小時10頁的速度讀完了它,在上面亂塗亂畫了許多標記。800頁的英文書,我從中學到了大量的基本概念(線程進程,內存演算法,定址方式等等)。
接著我尋思直接從網路上而不是從書上尋找資料,TA師兄給我提供了一個OS Development,我照著上邊的例子,寫了數以千記的彙編代碼,習得了彙編技能。
此時,我具備基本的概念知識,對程序的語言也已經理解,知道了虛擬機的調試方法,差的,就只有對內核整體是如何協作不太明白。於是我去找來老師用於教學的PintOS,找來MIT那個項目的代碼,還有國內一個高校自製的OS(是幾個研究生一起寫的),仔細研究了一遍,最後開始寫代碼。 在那個學期里,我放棄了LOL,一心看代碼,寫內核,寫各種模塊,將過程記錄在博客上,花了三個月的時間,最終寫出一個具備terminal的內核(文件系統沒寫好,時間不夠),可以跑命令,運行函數,管理內存和進程,處理中斷。如果你想知道具體整個編寫的過程是怎樣的,可以看看我當時的記錄,如下(很長):原文:(http://www.ilovecl.com/2015/09/15/os_redleaf/ )
今後,我就要開始折騰操作系統,有了一點小小幹勁。
我的計劃是,先看過一份用於教育目的的系統源碼,再去翻找相應的資料(我手頭已有綠寶書),在翻資料的同時開始寫代碼,然後做好移植真機的工作,DONE!
我也明白,理性很豐滿,現實很骨感,這過程不會如同我計劃中這般簡單和輕鬆。但是,見難而退可不是我的風格(那樣我會被紅葉二小姐調戲的),不管如何,我都會,怎麼說呢,儘力吧。出於課程需求,斯坦福那些人親自寫了一個名為「pintos」的系統。pintos的結構比較簡單,分為進程管理、文件系統、用戶程序、虛擬內存等幾個部分,也正是因為這個原因,我選擇pintos作為我的參考藍本,現在在讀它的源碼。
在接下來的幾個月時間裡,不出意外的話,我會不斷的在博客上更新我的進度。
(三)交叉編譯環境倘若我們要在ubuntu上編譯另外一個完整的OS,交叉編譯環境是必不可少的玩意,維基百科有云:
交叉編譯器(英語:Cross compiler)是指一個在某個系統平台下可以產生另一個系統平台的可執行文件的編譯器。
(想起以前,我為了給路由器編譯OPENWRT,下載大量源碼,愣是編譯了幾天幾夜。那時候的我,真是「可愛」。)為了配置好交叉編譯環境,我廢了好大力氣,最後勉強找到了組織。
編譯環境大致分為2部分,binutils和gcc。我先裝好gcc-4.9.1,之後下載gcc-4.9.1和binutils-2.25的源代碼,似乎gcc版本與binutils版本要對應來著…開始編譯之前,需要準備全局變數(在命令行中敲入以下命令):
export PREFIX=」$HOME/opt/cross」
export TARGET=i686-elf export PATH=」$PREFIX/bin:$PATH」 編譯Binutils cd $HOME/binutils-2.25 mkdir build-binutils cd build-binutils#注意是在源碼目錄下面新建一個文件夾,然後cd到該文件夾里,然後才配置configure,不這麼做的話,嘿嘿..
../binutils-x.y.z/configure –target=$TARGET –prefix=」$PREFIX」 –with-sysroot –disable-nls –disable-werror make make install –disable-nls 告訴binutils,不要添加本地語言支持–with-sysroot 告訴binutils,在交叉編譯器中允許sysroot
編譯GCC
cd $HOME/gcc-4.9.1 mkdir build-gcc cd build-gcc#注意是在源碼目錄下面新建一個文件夾,然後cd到該文件夾里,然後才配置configure,不這麼做的話,嘿嘿..
../gcc-x.y.z/configure –target=$TARGET –prefix=」$PREFIX」 –disable-nls –enable-languages=c,c++ –without-headers make all-gcc make all-target-libgcc make install-gcc make install-target-libgcc –disable-nls 告訴GCC,不要添加本地語言支持。–without-headers 告訴GCC,不要依賴任何本地庫,我們必須在自己的OS中實現庫。
–enable-languages 告訴GCC,不要支持除了C、C++之外的語言。
提醒
不同機器配置不同,編譯速度也不同。
編譯這兩個軟體,我花了近3個鐘,機器配置之低自不必說,說了都是淚。
如果任何人的任何編譯過程出了任何問題,請仔細地、認真地、用心地再看看上面的命令,在你沒有弄懂它的原理之前,請不要擅自做任何「改進」(血淋淋、赤裸裸的教訓呀)。
(五)OS模糊框架翻完了手頭的綠寶書,我才曉得,人都是被逼出來的。
操作系統的概念都差不多已經知道,接下來,該由「理論態」切換到「實踐態」了喔(書還是不能看太多,會中毒的–)。
對了,從別人推薦的地方弄來了一個框架(曾在android平台寫了幾萬代碼,我深深體會到框架的作用),輕鬆開工吧。
先說明一下這個框架:Meaty Skeleton,開源示例,內核和用戶分離,方便擴展,嗯,沒了。
最近煩雜事情很多,心情,不算愉快也不算低落吧,近來又夢見紅葉,不知道又要發生什麼,不管。
(六)內核第一步任務:GDT完成天色已晚,又下著雨,我也忘記帶傘了,嗯,等會兒再回去好了,這個商城的環境還是蠻好的。
今天實現了GDT。
(也不算是實現吧,因為我打算使用純分頁的流氓招數,放棄純分段或分段分頁混合,所以就不太用心於實現GDT,只是瀏覽INTEL的官網,借用了幾個FLAG定義之類的東西,匆匆就寫完了GDT)
下面是記憶:
使用內嵌式彙編
分4個段,兩個高級的內核分段,兩個低級id用戶分段 預留了一個TSS,雖然也不打算用硬體實現任務切換(聽前輩們說,硬體實現非常的麻煩) 把 設置GDT表的函數(init_gdt)放在kernel/arch/i386/global_descriptor_table.c中,而段 segment_descriptor的定義(seg_desc)則放在kernel/include/kernel /global_descriptor_table.h 引用了英特爾的一份公開資料 一些全局或者說全世界通用的參數放在kernel/include/kernel/global_parameter.h,有些人更絕,把所有函數的原型放在一個地方,哪怕內核級函數和用戶級函數混在一起 翻了太多資料,頭都暈了 按進度來看,有點緊,也無妨。(七)內核第二步任務:IDT完成
佛說人者,非人者,名人者。
已經寫好IDT的載入,加上之前的GDT載入,就已經完成兩個與機器硬體相關的模塊(準確的說,應該是給CPU的特定單元載入內容)。不過我並沒傳說高手那麼厲害,高手們一天一個模塊,可我近幾天連IDT對應的IRC和HANDLE都還沒弄。在bochs上調試時,分別 鍵入info gdt 和info idt 1,能看到GDT和IDT的內容。
今日要點:
ATT彙編和尋常的INTEL有些許區別,不過區別不是很大
GDT和IDT都是固定的表,必須實現,實現方法各異 之前留下的TSS並非用於切換任務,而是用於保存從「用戶態」回到「內核態」時必須使用的跳轉地址 未完待續 後記,IDT裡面的OFFSET並沒有得到正確的值,因為IRQ還沒設置好,相應的HANDLE還沒有弄好 2015年4月16日01:14:25補充:設 置了IDT表中的頭32個項,也就是ISR(interrupt service routines),它專門處理諸如「除以0」/「Page Fault」/「Double Fault」等exception,它對exception的處理方式也很簡單,或者說根本沒有處理,僅僅是列印exception的類型而已。
我 隨便寫了一句int a = 1/0,調試的時候,bochs提示」write_virtual_checks(): no write access to seg」。可能是內核還沒具有從用戶態跳轉到內核態的能力吧,畢竟IDT的頭32個項都擁有ring0的級別,明天再看看。
補上3種中斷類型:
Exception: These are generated internally by the CPU and used to alert the running kernel of an event or situation which requires its attention. On x86 CPUs, these include exception conditions such as Double Fault, Page Fault, General Protection Fault, etc.
Interrupt Request (IRQ) or Hardware Interrupt: This type of interrupt is generated externally by the chipset, and it is signalled by latching onto the #INTR pin or equivalent signal of the CPU in question. There are two types of IRQs in common use today.IRQ Lines, or Pin-based IRQs: These are typically statically routed on the chipset. Wires or lines run from the devices on the chipset to an IRQ controller which serializes the interrupt requests sent by devices, sending them to the CPU one by one to prevent races. In many cases, an IRQ Controller will send multiple IRQs to the CPU at once, based on the priority of the device. An example of a very well known IRQ Controller is the Intel 8259 controller chain, which is present on all IBM-PC compatible chipsets, chaining two controllers together, each providing 8 input pins for a total of 16 usable IRQ signalling pins on the legacy IBM-PC. Message Based Interrupts: These are signalled by writing a value to a memory location reserved for information about the interrupting device, the interrupt itself, and the vectoring information. The device is assigned a location to which it wites either by firmware or by the kernel software. Then, an IRQ is generated by the device using an arbitration protocol specific to the device』s bus. An example of a bus which provides message based interrupt functionality is the PCI Bus.Software Interrupt: This is an interrupt signalled by software running on a CPU to indicate that it needs the kernel』s attention. These types of interrupts are generally used forSystem Calls. On x86 CPUs, the instruction which is used to initiate a software interrupt is the 「INT」 instruction. Since the x86 CPU can use any of the 256 available interrupt vectors for software interrupts, kernels generally choose one. For example, many contemporary unixes use vector 0x80 on the x86 based platforms. 今天載入到IDT中的,正是第一種類型(Exception),只不過換了個名字叫ISR而已。未完待續。
2015年4月18日12:06:45補充:
之前的」write_virtual_checks(): no write access to seg」錯誤並不是許可權的問題,而是段寄存器DS的值錯誤,它的值應該是0x10,可我給它賦值0x08。0x08是段寄存器CS的值,0x10才是段寄存器DS的值。
另外,這att彙編裡面,把C語言函數的地址賦給寄存器,必須在函數名前面加上$。
至此,ISR徹底完成,只是,似乎IRQ又出了點問題….
未完待續。
(十)內核第三步任務:分頁完成稍微做下記錄…
得到內存大小
首先,利用grab得到物理內存的實際大小。物理內存管理
然後,用一個數組map來監督物理內存,數組的每一項都對應著一個4K的物理內存。在這裡我遇到了一個問題:數組的大小如何設置?因為還沒有內存分配功能,所以不可能allocate一塊或new一塊內存來存放數組。找來找去也沒找到合適的方案,就自己弄一個粗魯一點兒的:設置數組大小為1024 1024。這樣一來,數組的每一項對應4K,有1024 1024項,恰好可以對應4G大小的物理內存。但這樣又有一個缺陷,倘若物理內存沒有4G而是128M,那麼該數組就有大部分元素被廢棄了。現在先,額,不管這個,之後再解決。至於這物理內存它的實際分配,我是這麼覺得的:把前64M的物理內存當作內核專屬(把內核的所有內容全都載入到此處),剩餘的物理內存才是空閑內存,用於allocate。
為了方便分配物理內存,我採取最最最簡單的方法:把所有空閑的物理頁放到一條鏈里,需要的時候直接拿出來就可以了。
虛擬內存管理
之後,就是把page_directory地址放入CR3並開啟硬體分頁功能了。page_directory,page_table等作用於虛擬地址。對於這4G的虛擬地址空間,排在前面大小為MEM_UPPER的一大塊虛擬內存都是內核空間,剩下的排在後面的都是用戶空間。也就是說,在有512M的物理的情況下,虛擬內存的前512M是內核態,後面的3584M是用戶態。
分頁錯誤
內存分配的過程中,可能出現「頁面不存在」、「頁面只讀」及「許可權不足」3種錯誤。處理分頁錯誤,CPU會自動調用14號ISRS,我們要做的,是把我們寫的處理函數地址放到14號ISRS的函數欄即可。每次分頁錯誤,CUP調用14號ISRS,繼而跳入我們設計好的處理函數(-_-陷阱?)。
不過我現在也是暫時先不寫分頁錯誤的處理函數,如果內存真的任性真的出錯了,我也不會管它的,傲嬌就傲嬌吧。
到這裡,分頁就算是初步完成了。
致命的傷痛
很遺憾,物理內存設置好了,虛擬內存設置好了,也正常工作了,但是我一旦開啟硬體的分頁功能,就有」physical address not available」的錯誤,直接重啟了,到底是怎麼回事…再看看吧…未完待續。
2015年5月1日12:54:14補充:
bochs的」physical address not available」提示是這麼個回事,把一個內容不對的分頁目錄載入進硬體(也就是把分頁目錄地址置入CR3)。在初始化分頁目錄時,我直接用了4M大頁的方式初始化,但弄錯了byte和KB的數量級,所以就出了一點小小的問題。
遺留:page fault函數,待日後再寫。
寫內存分配去吧!
未完待續。
(十一)內核第四步任務:內存分配完成內存分配?這可是個麻煩的活,不過,如果你足夠聰明的話,就沒什麼問題了。 ——前人
上 一次,我準備好了分頁的相關內容,比如說,載入分頁目錄/開啟硬體支持/劃分物理內存/劃分虛擬內存等等。這一次,不會慫,就是干(為寫內存分配模塊而奮 斗,高扛自由的鮮紅旗幟,勇敢地向前沖….)。分頁準備好之後,下一步是如何地分配內存,比如,如何分配一頁空白的可用的物理內存?如何分配一塊空白 的虛擬內存?如何連續地分配等等等等。 第一節:申請和釋放空白物理內存 申請物理內存,在分頁的機制下,就是申請一頁或連續幾頁空白的物理內存,釋放則反過來。在 分頁的時候,我已經將所有的空白物理頁都放進了一個鏈表之中,現在要申請一個空白物理頁,從鏈表中拿出來即可,太簡單了。釋放空白物理頁,將物理頁重新放 進鏈表裡即可,也是非常的簡單,有點簡單過頭了。當然啦,簡單有省時省力的優點,同時,也有「無法同時分配許多頁/分配大內存時(比如數十M)很吃力」的 缺點。這,按我的習慣,先留著,以後再說,現在能簡單就簡單。
寫好allocate_page和free_page兩個函數之後,分配空白頁倒是正常,但是內核出現」double fault」的錯誤,也就是8號ISR被CPU調用了,具體為甚,現在還不清楚,待我瞧瞧再說。
未完待續。
查資料如下:
Normally, when the processor detects an exception while trying to invoke the handler for a prior exception, the two exceptions can be handled serially. If, however, the processor cannot handle them serially, it signals the double-fault exception instead. To determine when two faults are to be signalled as a double fault, the 80386 divides the exceptions into three classes: benign exceptions, contributory exceptions, and page faults. Table 9-3 shows this classification.
Table 9-4 shows which combinations of exceptions cause a double fault and which do not.
The processor always pushes an error code onto the stack of the double-fault handler; however, the error code is always zero. The faulting instruction may not be restarted. If any other exception occurs while attempting to invoke the double-fault handler, the processor shuts down.
————————————————————————–
Table 9-3. Double-Fault Detection Classes
Class ID Description1 Debug exceptions
2 NMI 3 Breakpoint Benign 4 Overflow Exceptions 5 Bounds check 6 Invalid opcode 7 Coprocessor not available 16 Coprocessor error0 Divide error
9 Coprocessor Segment Overrun Contributory 10 Invalid TSS Exceptions 11 Segment not present 12 Stack exception 13 General protectionPage Faults 14 Page fault
————————————————————————–Table 9-4. Double-Fault Definition
SECOND EXCEPTIONBenign Contributory Page
Exception Exception Fault Benign OK OK OK ExceptionFIRST Contributory OK DOUBLE OK
EXCEPTION ExceptionPage
Fault OK DOUBLE DOUBLE ————————————————————————–大概意思是:同時出現了2個中斷,CPU不知道該處理哪個先,就是這樣,就是如此的簡單。之前沒有這個錯誤,但分配和釋放幾個物理頁之後就有這個問題,我估摸著兩個都是Page fault,再看看吧。
剛剛調試了一下,我發現不是分配和釋放幾個物理頁的問題,而是cli()和sti()的成對出現,去掉它們就沒這個問題;更奇怪的是,就算只有sti() 允許中斷出現,也會double fault,莫非我這前面關了中斷或者是前面遇到了不可解決的中斷遺留到現在?難道,是irq的重定位有問題?到底是為什麼呢?先算入歷史遺留問題吧,還 有重要的模塊要完成。 (事情有點麻煩了呢?並不是內存分配這裡出了問題,而是sti()惹的禍,不管這哪個位置,只要調用sti()開啟中斷,就會double fault,看來必須解決這個問題才行,我不可能一直不開中斷吧…-_-) 睡了一覺,起來查資料,看到了關鍵的一句:make sure you didn』t forget the CPU-pushed error code (for exceptions 8,10 and 14 at least)到了,我翻出代碼一看,哎呀嘛,我只注意到了8號軟中斷,沒注意到10號和14號軟中斷(14號處理page fault),刪去兩行代碼後,順利開啟中斷! 未完待續。 第二節:分配內存(malloc/free) 既然已經可以正常地分配和釋放物理內存頁,那麼在這一小節之中,很自然地,我的任務就是分配內存了。所謂「天將降大任於斯人也,必先讓他實現一個內存分配的演算法」,不外乎就是說,要實現void malloc(int size)和int free(void p, int num_page)兩個大眾情人函數。
它 的大概思路就是這樣的:先初始化一個桶,把可用的內存塊都塞進去,要分配內存時,直接從桶裡面找,找到了當然萬事大吉大家都開心,如果找不到,就調用上面 那個申請空白的物理內存頁的函數,弄一個4K物理內存頁過來,將這個內存頁分割成小塊,丟到桶裡面,然後繼續找,就是這樣….
2015年5月5日23:19:08補充:遇到一個bug:每次申請的時候,可以正常申請,但是一旦使用了申請的內存,內核就報」page fault」的錯誤。想來想去,看來看去,最終發現,我在初始化分頁機制的時候出了點小小的問題。
秘技解決:
初 始化虛擬內存時,我將大小和物理內存一樣大(比如129920K)的虛擬內存設為內核級別並可用,剩下3個多G的虛擬內存是用戶級別但不可用,我使用4M 大頁載入分頁表,所以我實際上載入了129920/4096 = 31個大小為4M可用的內核級別虛擬內存頁,也就是說,在虛擬內存這個空間里,僅僅有31 4096 = 126976K的可用空間,其它的虛擬內存均是不可用的非法的;而在初始化物理內存時,我將前64M留給內核,後面的物理內存用於malloc和 free,比如有129920K,我把它劃分為129920 / 4 = 32480個4K大小的物理內存頁,也就是說,在物理內存這個空間里,僅僅有32480 4 = 129920K的可用空間,其它的物理內存均不在管理範圍之內;這樣一來,就出大問題了。
假設我們要申請一個物理頁,由於使用鏈的方式管理物理頁,申請到的就是排在後面的物理內存,比如申請到了129916K到129920K這一個物理內存頁,現在,我們要使用它,會發生什麼呢?page fault!!!!!!!
為 什麼?很明顯,在虛擬內存的空間里,最大的有效內存是126976K,CPU的分頁表裡只能找到前126976K,現在讓CPU去找129916K,它根 本就找不到!它以為這個虛擬地址並沒有對應這物理地址,是個錯誤!(附上page fault的引發條件:A page fault exception is caused when a process is seeking to access an area of virtual memory that is not mapped to any physical memory, when a write is attempted on a read-only page, when accessing a PTE or PDE with the reserved bit or when permissions are inadequate.)
於是我稍作改正,就正常了,可以正常使用申請到的內存-_-。未完待續。
(十二)內核第五步任務:系統時鐘中斷、鍵盤中斷我現在的狀態不是很好,剛弄好系統時鐘中斷,每10ms發出一個中斷請求;但鍵盤中斷並沒有弄好,沒有識別鍵盤的按鍵SCANCODE,所以暫時只能識別第一次按鍵,系統收不到第二次按鍵中斷,明個兒我再來看看,已經很晚了--!!
未完待續。2015年5月9日 15:51:00補充:
查了一番資料,調試了一番,現在,鍵盤中斷正常工作了,鍵盤可以正常工作,每輸入一個字元,就在屏幕上顯示出來。
嗯哼,可以進入到進程模塊了。
(十三)內核第六步任務:進程創建在自習室里,我突然想到一個問題:一個進程,如何去創建它?(雖然之前翻完了大寶書,但畢竟一個多月都過去了,忘了具體的實現-_-)
翻 翻書,找到一個和我的設想相差不多的方案:用一個特定的結構體代表一個進程,結構體中包含進程的相關信息,比如說進程的pid、上下文、已打開的文件、優 先級、已經佔用的CPU時間、已經等待的時間、虛擬內存空間、錯誤碼等等,創建進程的時候,只需要跳轉到進程的虛擬內存空間即可。至於如何跳轉,那就是內 核態的事情了,一般的進程都處在用戶態,也就不必關心太多。
如此,我們便是可以創建並運行一個進程了(不考慮文件系統),既然可以創建進程,可以切換進程,那麼進程調度就很容易了,不過就是個複雜的進程切換過程而已,下一節便是寫進程的調度罷。
(十四)內核第七步任務:進程切換與進程調度黃粱一夢。
看到這句古語,頓時感慨萬千,沒想到僅僅數周時間,我的人生竟發生了這麼大的轉折(不是一夜暴富),彷彿一夜醒來,到另外一個平行世界裡去。甚至,在睡夢中我都會驚醒。逝 者已逝,再多的話語也沒用。只是,我不甘願就這麼結束而已。她也曾經說過:「此身不得自由,又何談放縱」,現在我竟是極度贊同了。曾經想過在割腕的那一瞬 間,她的腦海里究竟有什麼,有沒有浮光掠影,有沒有回放這一生的片段?如此年輕的生命,選擇自我了斷,需要多少黑暗沉澱,多少的落寞與失望…似乎一下 子也看開了。
(以上只是個人情感的流露,忍不住必須得寫些什麼,請忽略)
簡單記錄一下吧,沒什麼心情。
進程切換時,只需要切換進程上下文,把context刷新一遍即可。
至於進程調度,這個就簡單許多了(其實也挺複雜),在時鐘中斷到來的時候,調整各個進程的優先順序,並切換到相應的進程,就是這麼簡單。
嗯,就這樣吧,現在只想戴上耳機聽聽音樂….
終於可以來回答這道題了……
一年多前,也就是大一下學期末的時候,我看到這個問題下 @fleuria 叔的答案,然後看了 F 叔給的這個鏈接 基於 Bochs 的操作系統內核實現 ,當然是什麼都看不懂,除了驚詫之外也了解了一件事情:一個人寫一個簡單的操作系統內核是一件非常帥氣並且可行的事情。
於是我開始寫了,那時候我的水平大概是:只會做 C 語言的習題,編譯的話只知道按 F9,彙編知道常見的指令,另外會一點點的 Win 32 編程,能流暢使用 Windows。
一開始我找了《30 天自製操作系統》來看,每天看書,然後把從書里把代碼打出來,一次一次地編譯運行。因為要同時寫彙編和 C,所以從那時候起就開始用 vim。
在啃完了差不多半本書後,開始覺得沒意思了……因為覺得作者為了讓內容更簡單而省略了太多細節。也看了於淵的《Orange『s 一個操作系統的誕生》,依然沒看下去:彙編用得太多了。期間也曾斗膽發郵件給 F叔,然後他推薦了 Bran"s Kernel Development Tutorial 這個教程,於是我就從這教程重新開始了: 「30天自製操作系統」 Stop 「OS67 」 Start
那時候大概是大二上學期,於是在 github 上又開了一個 repo,一開始在 Windows 下開發,後來又切換到了 Linux 下,因為 Bran"s 用的 bootloader 是 Grub,不符合我的初衷,所以就自己寫了一個,之後便跟一路教程寫,跨過了保護模式這道坎,完成了基本的設備驅動。
在完成 Bran"s 後,我又部分參考了 寫一個操作系統內核有多難?大概的內容、步驟是什麼? - To淺墨的回答 中推薦的:hurley25/hurlex-doc · GitHub 文檔,完成了一些簡單的調試函數和庫函數,printk 和內存分配。
事實證明,儘早寫好調試函數諸如 panic, assert 和 printk 是非常重要的。 大量地使用有助於你儘快地發現 bug (當然前提是這些函數本身不能有 bug)。看完了 hurlex-doc 該看的部分後,很長一段時間了都不知道該幹嘛好,模仿 hurlex-doc 里的內核線程切換也一直出錯。這一情況一直持續到我開始讀 Xv6, a simple Unix-like teaching operating system 。
如果你去看知乎關於「自製內核」的問題,你會發現 xv6 被反覆地提及並推薦,事實上它非常值得被推薦:這是我讀完大部分代碼之後真切體會到的。
之前的 Bran『s 和 hurlex-doc 的篇幅都比較小,我是在電腦和 kindle 上看完的,xv6 相對來說代碼量比較大,有 9000+ 行和一份文檔,之後我又找到了這個:ranxian/xv6-chinese · GitHub xv6 文檔的中文譯版,所以我就去花了十二塊錢學校列印店列印了一份中文文檔和一份代碼。這又是一個正確的決定,讓我不必對著電腦就能看代碼。
在之後的時間裡,我先讀了 xv6 中文件系統相關的部分,然後改寫它的代碼為我的內核添加了一個 類似 Minix 的文件系統。 然後幾乎又照抄了其中了進程調度的部分(做了部分簡化),又在原來的代碼基礎上為添加操作系統的介面,接著寫用戶程序,過程幾乎是「一路順風」。看 xv6 的那段時間也經常是處於醍醐灌頂的狀態。
最後我終於在差不多一個月前完成了這個簡陋的操作系統內核:
LastAvenger/OS67 · GitHub (沒錯其實我是來騙 star 的)歷時一年,一路點亮了不少技能樹(雖然都點得不好),這樣算是「從零開始寫一個簡單的操作系統」么? 跟進一步說,有誰不是從零開始的呢? 所以想做的話,現在就開始做好了。
這是被「翻爛」了的 xv6 源代碼和中文文檔(其實是放書包里被磨爛了)「故事」講完了,接下來說一點經驗之談吧……* 知乎上總是有人在討論「做一個玩具編譯器和做一個玩具內核何者更有趣」之類的問題,然後總有各種大V 跳出來說內核有多 dirty 而編譯器多 clean,事實上除了 CPU 上的幾個表因為歷史原因長得噁心一點,內核並沒有什麼特別 dirty 的地方,另外,想做點什麼打發時間,不過是兩個代碼量稍多的入門項目,有什麼好糾結的?
* 寫內核的過程中,你會接觸到一些這輩子大概只會用到一次的知識,A20 線已經成為歷史,日常的編程裡面也不需要你懂得 GDT IDT 的結構。但是單憑內核主要部分部分(文件系統,進程,內存)給你帶來的知識而言,這點冗餘是值得的。 * 儘早實現調試函數並大量使用,善於利用 bochs 的內置調試器,能省下你不少時間。* 有時候覺得書里的做法非常奇怪,你覺得你有更好的做法,一般是你想錯了。(當然只是一般)* 上面說看 xv6 一路順風是假的,20% 時間在抄代碼,80% 的時間用來調試。* 對我這種能力一般的人來說,「寫內核」只是好聽的說法,正確的說法是「抄內核」。當然,就算是抄一個,也算是受益匪淺了。* 抄 xv6 的好處在於,即使你的代碼出錯了,你可以堅信,正確的答案肯定在 xv6 的代碼里,或許只是你還沒理解透而已,只要不斷地看和理解,你就離正確的道路越來越近。最後,感謝 @fleuria 在微博和郵件里的多次幫助, @To淺墨 的 hurlex-doc 文檔,鮮染同學翻譯的 xv6 中文文檔, @郭家華 完美地解答了我一開始的疑問,讓我在內核中得以使用 C 語言。在 #archlinuxcn 頻道里也得到了很多人的幫助。也可以參考我的畢業論文: 基於 Bochs 的操作系統內核實現
彙編不重要,但是要有一定計算機組成的基礎,並對一個現代 kernel 的結構有大體的認識,至少大致上理解虛擬內存和文件系統有哪些東西。不要看 《the orange"s》和《三十天編操作系統》,面太小,代碼質量不高,就別拿 DOS 當操作系統了。個人比較推薦《萊昂氏 UNIX 源碼分析》(已絕版,可淘寶列印)、《linux 0.11 內核詳解/剖析》 ,寫代碼之前至少先都啃一遍。教程的話推薦 《bran"s kernel development tutorial》和 osdev 上的一些資料。順著它們搭開發環境,出一個簡單的 bootlaoder,可以編譯 C 代碼即可。然後拿大把大把的時間慢慢給 bootloader 加東西就好了,能用 C 就不要用彙編。開發中的很多細節要到開發時才留意的到,這時可以自己思考,也可以去抄 linux 0.11, xv6, unixv6 這幾套優秀的源碼。
現在想來,開發一個 kernel 的主要內容在於「實現」而不是「設計」,更重要的是用時間去理解這些優秀的設計為什麼合理,自己別出心裁的想法一般不用多想,一定是錯的。
不要在 x86 的一些歷史遺留問題上花太多時間,比如 bootloader 的保護模式會在開頭擋住一大批人,可是這並不重要,只要知道有這個介面可以引導你的二進位代碼即可。知道 GDT 可以區分用戶態/內核態,IDT 可以給中斷綁回調就行了。在調試上會花費大量時間,可以慢慢琢磨怎樣提高調試的效率。然後需要的就只是耐性了,說實話也挺無聊。
我來寫一個如何在15天內完成一個嵌入式實時操作系統,並移植到stm32單片機的攻略吧。第一次看到這個問題是在大概兩個月之前,從那時候開始決定自己也寫一個操作系統,於是開始看os的基本概念,進程切換,任務調度,內存管理,任務通信,文件系統等等。前言:大約在兩個周不到前動手,平均每天7個小時,從完全不知道怎麼下手(真的快哭了),到現在完成了一個----基於優先順序時間片調度,具有信號量,消息隊列,內存管理的嵌入式系統並命名為--LarryOS,並且移植到stm32(Cortex-M3架構)上,還在stm32上實現了一個malloc和free函數,受益匪淺。現在還正在完善。我是用自己命名的,當然,大家寫好了也可以用自己命名,圖個開心么 ,想想是不是很激動啊。 對於這個問題下的回答的看法:大神真的好多,幾乎都是x86型的,難度真的要比我的大,不得不承認。不過,我覺得對於新手,寫那樣一個系統真的是太艱辛了,比如我這種入ee坑的在校大三學生,課真的太多了,周內最少三節課,而且和cs沒有一毛錢關係,課餘時間太少,如果花費一個學期或者三個月的時間來寫,可能有些得不償失。因為許多想寫os的朋友,和我一樣,一是覺得寫os很酷,二是想通過寫來理解os,畢竟看書看了就忘,也沒有衡量自己學的好壞的標準。因此,選擇寫嵌入式系統,是一個非常好的選擇 。知識儲備:這個問題想必是很多同學顧慮的,到底寫一個嵌入式系統需要什麼知識儲備?我從自己的經歷出發,並加以簡化,大家可以參考一下。自身版:1c語言要求:我自己在大學裡幾乎沒學過c語言,上機幾乎10道題會2道,期末全靠背。後來開始自學了c,看了c語言程序設計現代方法,c和指針 c專家編程 第一本書的課後題,做了3分之2吧,就這樣,沒了2彙編要求:會基本的x86指令,僅僅是基本,看了王爽的彙編語言,程序寫的很少,僅僅上機課寫過,不過能很快寫出來。3微機原理或者計算機組成原理:老師上課太坑了實在,我自己看了csapp的前四章大概,豁然開朗,推薦這個。4操作系統:看了三個禮拜os的基本概念,僅僅是了解概念,沒辦法深入,也沒地方。。。。5數據結構:看過浙大的數據結構公開課的3分之2,會寫基本的鏈表,隊列,最基本的樹和樹的基本的遍歷辦法,圖完全不會 6單片機基礎:大約兩年的單片機基礎7新手看到這裡,可能已經慌亂了。。。。不要怕,我說的很多東西,寫嵌入式系統用不到啊 ,繼續,發一個精簡版精簡版1:c語言 能把我推薦的c書的第一本或者c primer之類的書看個一半,或者自己本身知道指針的概念,知道結構體指針,函數指針,二級指針的用法,僅僅是概念和寫法就行了,不用深入,用不了多久。2彙編:知道有幾條常用指令,用法和功能是什麼就可以了3組成原理:知道中斷是什麼,入棧和出,寄存器,知道彙編對應的計算機大概動作就可以了,用不了一個周4操作系統:找個公開課或者書,看看大概的os概念,一個周就夠了,我現在很多概念還是不知道,後面可以慢慢學5數據結構:會寫鏈表,隊列就行了,我們不寫文件系統,不用樹6單片機基礎:用過單片機,不用自己寫驅動代碼,僅僅可以在別人的驅動下,編寫一些簡單的邏輯代碼就行,完全沒學過的人,兩個禮拜就可以用stm32來編程了,如果之前學過51,一個禮拜不到即可,因為我們只是藉助一下單片機,避免使用虛擬機,方便操作系統的調試。因為我們可以用單片機,輸出某些信息在串口或者液晶屏,讓我們直接看到代碼的錯誤點,方便我們調試。因為很多大一的新生想寫,推出一個極限版~~極限版1.學過C,知道有指針這個東西,其他的邊做邊學。2.不懂彙編,邊做邊查,邊查邊寫。3.知道什麼是寄存器,知道中斷的概念4.知道OS是由什麼組成的5.數據結構完全不會,遇到鏈表,隊列時再查,或者直接抄也行6.不學如何使用單片機,遇到再查發一個很裝逼的封面助興正文:一、開發環境
對我來言,這倒是很重要的一點。第一次萌生想寫OS的想法時,在網上搜索了不少資源,大多數都是在敘述框架,如何構建一個操作系統。然而對於當時的我來說,根本不知道用什麼平台來寫,如何調試自己的程序,看了一些朋友的發帖,推薦用XX模擬器,在XX平台開發,完全看不懂,界面好像也很老舊,而且大多是英文,當時有點敬而遠之。自己手上只有個codeblocks軟體,想來想去也不知道怎麼用這個開發OS。直到有一天,突然頓悟,OS不就是一堆程序,我還打算讓他運行在單片機上,那麼用單片機常用的開發工具不就行了!!!---------Keil uVision5
學過單片機的朋友,相信非常熟悉這個軟體,使用起來也非常簡單,網上隨便一查,就可以下載和安裝了,這裡就不詳細展開說了。如果不知道如何使用的,先熟悉一下這個環境,再開始寫,相信半個小時即可上手。
二、參考資料
既然是運行在真機上,必然要對它有所了解,我們這裡採用的是STM32(Cortex-M3架構),市面上非常火熱的一款,資料豐富,大家有什麼問題谷歌一下,有很多前人的經驗讓你借鑒。如果不知道如何谷歌的朋友,點開這個鏈接去操作,免費,大約15分鐘就能翻牆了如何優雅的訪問谷歌、谷歌學術等網站 | 歐拉的博客。
1.Cortex-M3權威指南(中文版),這本書會詳細的講解,中斷處理,異常,ARM彙編等知識,我們會在任務切換的時候用到。(PDF即可)
2.嵌入式實時操作系統ucos(邵貝貝審校),ucos中有很多我們可以借鑒的地方。
3.谷歌,有一些基礎知識遺忘的時候,谷歌可以讓你很快補充上來三、從寫一個最簡單的os做起
我們這裡假設我們寫一個支持32個任務並發執行的簡易OS
1.任務就緒表
我們假設這裡任務有兩種狀態,就緒態和非就緒態。
我們定義一個32位的變數OSRdyTbl(就緒表),它的最高位(第31位)為最低優先順序,最低位(第0位)為最高優先順序。OSSetPrioRdy()函數的功能是,你傳遞一個數(優先順序),把這個優先順序對應的任務設置為就緒態。同理,見圖:
OSDelPrioRdy()函數將某任務從就緒表移除
OSGetHighRdy()函數選出最高優先順序的任務
這裡我們就完成了,設置某任務為就緒態,從就緒態刪除某任務,獲得最高優先順序任務的任務。
2.任務控制塊
想必這個概念大家很清楚了,每個任務都對應一個任務控制塊,典型的用處是,任務切換時,通過控制塊來獲知每個任務的堆棧(因為控制塊有指針指向該任務的堆棧)
此外,再定義幾個變數。
注釋寫的很清楚,不解釋了!
3.主堆棧
在此STM32中,提供兩個堆棧指針,一個是主堆棧(MSP),一個是任務堆棧(PSP),可以通過查Cortex-M3權威指南(中文版)得到。所以我們既要建立一個主堆棧,又要為每個任務建立自己的堆棧(一個任務一個),這裡我們先不管任務堆棧,只看主堆棧。
OS_EXCEPT_STK_SIZE是個宏,大家可以自己設定,我這裡設的是1024,一定要盡量大一些。為什麼?因為裸機下,進入中斷服務程序時,系統會把許多寄存器入棧,而且支持中斷嵌套,也就是剛入棧完又入棧,所以有可能會堆棧溢出,非常危險。
CPU_ExceptStkBase大家先別管,它指向的是數組最後一個元素。
4.建立一個任務
我們通過Task_Create()函數來建立一個任務,第一個參數用來傳遞該任務的任務控制塊,第二個參數用來傳遞函數指針,第三個傳遞該任務的堆棧。tcb-&>StkPtr=p_stk這句將該任務控制塊中應當指向棧頂的指針,指向了該任務的新棧頂(前面定義了TCB,自己可以翻一翻),在寫該函數時,一定要看Cortex-M3權威指南,不然你怎麼知道有這麼多寄存器,而不是僅僅從R1到R7?看到這裡,還想看懂的,應該都是真的想寫OS的朋友,這裡有不懂的去看Cortex-M3權威指南,你會豁然開朗的。這裡,Task_End是一個幾乎永遠不會執行的函數,何時會執行,先不管了,看它的內容。
5.歡迎來到主函數
①第一行:在該主函數中,第一行我們讓主堆棧指針指向了主堆棧,那麼,這個主堆棧是哪裡來的呢?很簡單,我們自己定義的,哈哈。
很熟悉吧,前面發過這個圖,大小你來指定,注意要大!!!!!
②第二、三行:建立一個任務,第一個參數傳遞了該任務的控制塊,第二個參數是該任務的任務函數,第三個是堆棧(數組最後一個)
是不是很好奇,任務1和任務2是什麼?一起了來看
大家自己隨意定義,開心就好。
Task_Switch()函數又是什麼呀?
這裡Task_Switch()是我們用來測試的程序,當任務1運行時,完成i++後,將最高優先順序設置為任務2,並用OSCtxSw()切換到任務2,OSCtxSw是用彙編寫的,是一個隱藏BOOS,我們先不管。
③第四行:程序剛運行時,是沒有最高優先順序的,所以我們用p_TCBHighRdy=TCB_Task1;來隨意指定一個任務為最高優先順序
④:最後一行:OSStartHighRdy()該函數也是彙編,和OSCtxSw()並稱為2大BOSS,我們會在後面解密。OSStartHighRdy和OSCtxSw很相似,不過OSStartHighRdy()用於當所有任務都沒有運行過時,用於初始化(當然具有任務切換的作用)並成功運行第一個任務,而OSCtxSw()是在OSStartHighRdy()之後,使用OSCtxSw()時,最起碼有一個任務已經運行了(或正在運行)。
6.完工?
到這裡,從宏觀說已經基本完工了,這就是一個簡易的OS的基本狀況。看到這裡,大家可以休息一下了,消化一下,馬上有BOSS要出現了,解決那兩個BOSS後,就真正做成了一個簡易的OS。
7.兩大boss之OSStartHighRdy()函數
7.1任務視圖
終於開始了任務切換環節,這是我畫的一副任務切換圖,自我感覺非常好,不過大家應該看起來很困難,字太丑了~~~~
通過分析上面這張圖,來確定如何寫OSStartHighRdy()函數:
①中:此時有一個任務1,但是任務1我們僅僅是建立了,並沒有讓它運行。這裡我們認為任務1是由三部分組成:任務代碼,任務堆棧,該任務的任務控制塊。
②中:我們想讓任務1運行起來,任務1是什麼?就是一堆代碼,如何運行起來?-----讓PC(程序計數器)指向任務代碼即可(依靠出棧POP)。同時,我們還要讓SP指向任務的堆棧,這裡的SP當然是PSP(任務堆棧)
7.2寫OSStartHighRdy()函數
下面開始寫OSStartHighRdy(),它的功能就是上圖的①和②
2,3,4,5行中的那些數字,是外設對應的地址,我們往相應的地址寫值,即可完成某些目的。12,14,15行是我們之前定義過的幾個變數,忘了的回頭翻一翻。17行是將OSStartHighRdy()函數extern了一下,因為我們在主函數要用。
上圖就是OSStartHighRdy的內容,我們一起來看。28,29,30行設置了PendSv異常的優先順序,問題來了,什麼是PendSv???
Cortex-M3權威指南中其實講了,很詳細,這裡為了縮減篇幅,不詳細說,大家只用知道PendSV 異常會自動延遲任務切換的請求,直到其它的中斷處理程序都完成了處理後才放行。而我們只用觸發PendSV異常,並把任務切換的那些步驟,寫在PendSv中斷處理任務中。
7.2.1PendSv處理程序
經過39和40行,我們往控制器里寫值,觸發了PendSv異常,現在程序會進入異常處理程序,就是下圖:
在此函數中,如果PSP為0,則進入OS_CPU_PendSVHandler_nosave()函數,其實在OSStartHighRdy我們將PSP設置為了0,所以必然會進入OS_CPU_PendSVHandler_nosave()函數。
7.2.2OS_CPU_PendSVHandler_nosave()函數
在該函數中,我們讓p_TCB_Cur指向了最高優先順序的任務,因為有出棧,所以重新更新了SP。
7.2.3恭喜
到了此處,一個任務已經可以成功運行起來了!!!!!!
BUT only one task!我們需要讓它多任務切換
7.3寫任務切換OSCtxSw()函數
與OSStartHighRdy非常相似,也是往中斷控制器里寫值,進入PendSv異常。
7.3.1任務視圖
依然是這張喜聞樂見的圖!!!!!!!
③:任務1要切換到任務2,因為待會要進入任務2,會破壞任務1,所以我們要保存任務1的現場,把寄存器入棧
④:因為任務1有入棧動作,棧頂肯定變了,我們修改任務1控制塊的值,讓它指向新的棧頂
⑤:什麼都沒有,只是想說明。我們建立了一個任務2,僅僅是建立了。
⑥:很簡單,要想讓任務2運行,則讓PC和SP分別指向任務2的任務代碼和任務堆棧即可。
⑦:什麼都沒有,只是想說明,任務2活的很開心,可以運行了
7.3.2由OSCtxSw()進入PendSv異常
與OSStartHighRdy不同的是,這時的PSP肯定不為0了,所以不會跳轉,會順序執行55行以後的程序,也就是任務視圖裡的③,繼續執行④。執行完60行的程序後,繼續順序執行,執行下面程序:
和OSStartHighRdy函數的後續步驟幾乎一樣,找到優先順序最高的任務,讓指針指向它即可!!!!
1.大功告成!!!!!!!!
到這裡,我們已經徹底完成了一個簡易OS的設計!!!!
8.1如何查看成果?
8.1.1增添程序
我們在主函數中加入一個函數
大家可以把它理解為一個庫,調用之後,我們就可以在串口(屏幕)顯示某些字元了。
同理,在任務1和任務2中加入printf函數
8.1.2編譯並下載程序
8.1.3利用串口調試助手觀察
網上搜串口調試助手,會有很多工具,隨意下一個就OK!
可以看到,按照我們的預期,任務1和任務2輪流輸出字元在屏幕上!
8.2如何調試程序?
8.2.1調試器
必然是神器--J-link或者ST-link,一個大概50,嵌入式開發神器,記得以前剛開始玩單片機的時候,調試全靠列印消息在屏幕,覺得好用的不得了,經常有人給我說,你用調試器調試啊,我都鄙夷的回一句,需要麼?(哈哈,那時確實不需要) 等開始寫OS時才知道,這東西真是救命稻草,沒有它,怎麼看寄存器的值和異常返回的值呢?
8.2.2界面
點擊DEBUG後,你可以看到寄存器的值。想想我們之前要入棧,出棧,如果哪一步錯了,自己估計把串口吃了,也看不出來吧,哈哈!!!!!!
單步調試什麼的,不說了,用的很多。
9.寫到此處的感想
答主因為寫這個OS,在凳子上久坐了兩周,這兩天腰疼,只好躺著寫這個回答了!哈哈!
也正好因為腰疼,感覺時間比較多。不過和想像的真的不一樣,本來覺得可以一氣呵成,結果短短的篇幅,就寫的自己的思維都大亂了,而且也挺費時間的,前後用了有6個小時了。想寫OS的朋友,參考上面的步驟,加上自己琢磨,應該也能寫一個出來。如果哪裡有不清楚的,不要心急,如果真的是一兩天就寫成了,還有什麼鍛煉的意義,有點失去初衷了。不懂的就去查權威指南和OS的書籍,相信你會收穫的非常多!
為什麼要寫這個回答?這是我寫了6個小時多以後來補充的問題,因為我自己也納悶了,放著自己的事不做,跑來寫這麼一堆幹什麼........剛才路上走著想到答案了----------想留個紀念。還有不到兩個月就要寒假了,我打算考研,也就是說這是大學裡做的最後一個項目了(畢設除外),真的挺傷感。從大一一路折騰過來,現在要突然一年不能折騰,簡直淚流滿面!!!!!!!!!!!!!!!!!!希望我這個簡單的開頭,能讓想寫OS的新人得到一絲啟發。寫操作系統能學到什麼?這也是我想寫這個回答的原因,對我的改變挺大前幾天有個好朋友(大一開始和我一起學單片機的朋友,後來他一直在做單片機項目,卻沒有補過任何基礎知識),打電話問我XX模塊怎麼用,其實是很簡單的一個問題,直接百度都可以,但是他還是想要來問下我,我說很簡單,就XX做就可以,有問題你再百度,但是他好像不想聽到這種回答,想讓我說到他一聽就知道怎麼做了,才敢開始做,不然就不敢啟動項目。那時我才突然意識到,以前的我就是這樣啊,做什麼項目做什麼東西,老是想要集成的模塊,資料豐富的模塊,如果沒有什麼源驅動代碼,我就不敢做了,生怕買回來,用不了之類的,甚至有源碼也不敢買,因為源碼和我的機型不一樣,連修改源碼都怕。開始寫OS,我還蠻恐懼的,因為不是科班,沒學過,不懂。涉及的東西也很廣,從編程語言到數據結構,組成原理,操作系統,怕拿不下來,也找不到好的資源,不知道怎麼寫起。通過這次項目,我想應該學到了一下東西:1.遇到不懂的沒有那種很強的抵觸感了,開始學會查晶元手冊,查原理書,開始配合谷歌查BUG,現在即使是拿來我沒有接觸過的模塊,無論最後做的出來與否,我肯定先去了解它是什麼,概念是什麼,再去找廠家提供的資料,提供的源碼,去一行一行的看,自己修改或者重寫,這個比下面說到的什麼具體的知識,我想都重要的多。我現在還記得第一次和第二次,第三次打開Cortex-M3權威指南(中文版)時的情景,看了幾頁就嚇得我關了,簡直是天書啊,其實耐心看,真的能看懂。2.把自己數據結構里學的東西,真正帶到了項目,雖然也寫過隊列等這些數據結構的題,但是在以前的嵌入式開發里,幾乎用不到,有時候覺得好沒用啊這些。這次通過實現OS的消息隊列,看linux的文件系統,知道了這些在實際的巨大用處。3.對OS的基本概念和運轉有了認識4.對C語言的理解深刻了許多5.每個人的體驗都不一樣,沒什麼補充的了O(∩_∩)O哈哈~四、簡單幾步將簡易OS改造為--優雅的簡易OS
1.為什麼不優雅?
在該函數中,任務1執行完,立馬就會切換到任務2,然而在實際中,我們希望是這樣的:
任務1每100毫秒執行一次
2.系統節拍
幾乎每個實時操作系統都需要一個周期性的時鐘源,稱為時鐘節拍或者系統節拍,用來跟蹤任務延時和任務等待延時
我們在main函數中輸入這樣一句
這裡我們配置了定時器中斷,每5毫秒一次中斷。中斷後會進入中斷處理函數,下面來寫中斷處理函數:
大家只用管if裡面的函數即可:我們在某個函數中將TCB_Task[i].Dly置為x,中斷處理函數會每5毫秒,將非0的TCB_Task[i].Dly減一。如果TCB_Task[i].Dly非0,對應的任務是不會運行的(因為被我們刪除就緒態了,這裡看不到),當TCB_Task[i].Dly減為0,我們才將該任務置為就緒態
3.編寫OSTimeDly()函數
也就是1中圖片所示的函數,它可以讓某任務指定每X毫秒運行一次。
第65行,可以關閉中斷,同理,第68行,開啟中斷。為什麼要關閉中斷?因為中斷會影響我們執行下面的代碼,先關閉中斷,執行完後再打開。66行將該任務從就緒態變為非就緒態,將要延遲的時間賦值為TCB_Task[OSPrioCur].Dly,然後調度(也就是切換任務)
4.調度函數OSSched()
非常簡單,剛才我們將某個任務變為了非就緒態,緊接著就找就緒態任務中優先順序最高的任務,然後切換
5.是否完成了呢?----空閑任務
乍一看,好像完成了,實則不然,雖然我們任務1每X毫秒運行一次,任務2每Y毫秒運行一次,但終歸裡面有空閑時間:任務1和任務2都不在運行,所以我們需要創建一個空閑任務,當CPU沒有東西可以運行時,運行空閑任務。以下就是空閑任務:
6.來到主函數:
其他的倒是沒問題,72行有個陌生的函數:OS_TASK_Init();
其實就是之前的OSStartHighRdy()函數的升級版,非常簡單
先創建一個空閑函數,再獲取優先順序最高的任務,然後執行最高優先順序的任務。
7.一個優雅的簡易OS誕生了
不好看?想加個界面?沒問題-----其實已經有了,大家觀察58行,就是讓液晶屏顯示一句話,我們把背景修改為紅色,醒目一點:
很開心能有這麼多朋友喜歡,非常感謝 。我開了一個簡單的頭,相信真的喜歡os的朋友,只要認真去做,一定也能實現一個更好的作品。後面的暫時不打算寫,如果有了新的思路,一定會再寫出來。授人以魚不如授人以漁,看到這裡已經有動手的能力了,想寫的朋友不要害怕,儘管去做!!!加油五、加入信號量六、加入消息隊列七、內存管理八、實現一個free和malloc玩玩?操作系統這玩意…並不是都像windows那樣圖形界面一堆工具,甚至不像linux發行版那樣帶一堆命令行工具。以linux為例,圖形界面就不說了命令行?那是bash,是個獨立軟體包,人家在bsd在unix在darwin上都跑得妥妥的。一個純粹的操作系統,其實只是定義了驅動介面(用別人的驅動),定義了最簡單的進程調度管理,定義了內存分配。這就已經是操作系統了。所以寫一個新的操作系統真的真的不是特別困難。困難的是你的os出來之後除了你自己大概是不會有人給他寫驅動寫程序的,除非用戶多;啥都沒有的os不會有人用。於是惡性循環…
我大約是在08-09年的時候寫過一個迷你的操作系統。大家可以在這裡看到我的源代碼 https://code.google.com/p/minios2/。當時這個項目完全是自己的興趣愛好。後來代碼較多了覺得需要花費過多的精力不合適就放棄了。
整個項目是從0開始的。因為主要在windows上開發,所以主力編譯器是msvc6.0。雖然說很不可思議。但是當你明白了編譯鏈接的原理以及PE文件的格式之後,這其實並不難。當然現在如果用高版本的msvc寫的話會更容易。
另一個難點是需要尋找以及閱讀大量的資料。包括比如386保護模式,bios調用,8259中斷控制器,pci匯流排控制器,8253時鐘控制器,ATA硬碟控制器,各種乙太網卡控制器等。當時這些資料在網上非常分散,收集很不容易。現在貌似好找多了。
另外你需要對數據結構,計算機體系結構以及操作系統原理有一定的了解。這個基本上本科和研究生課程里的知識就足夠了。當然你也要有足夠的編程經驗,因為有些錯誤可能會很難調試。
以下是我當時寫的一個簡單的文檔。
minios目前已經完成的功能:
bootsector
進入保護模式
內存分配模塊
簡單的線程調度模塊
信號量
時鐘
延時調用DPC和定時調用TPC
統一的設備驅動模型
標準輸入輸出設備驅動
內存及字元串相關的標準C庫函數
附件codes.zip的目錄結構如下:
codes
|-relocate 連接程序的源代碼,將bootsector和minios連接成一個可啟動的磁碟鏡像
|-bootsector bootsector的源代碼
|-minios minios的源代碼
|-bin 所有的目標都在此目錄中。其中minios.vhd就是可啟動的磁碟鏡像
如何啟動minios:
你必須安裝Microsoft的Virtual PC 2007
你可以在微軟的官方網站下載他的安裝程序,程序大小約30M
http://download.microsoft.com/download/8/5/6/856bfc39-fa48-4315-a2b3-e6697a54ca88/32%20BIT/setup.exe
安裝完成後就可以雙擊codes/bin/vm.vmc運行minios了
如何編譯minios:
編譯minios共需要三種編譯器。
codes/bootsector/bootsector.asm必須用nasm進行編譯,將編譯的結果命名為bootsector並且拷貝到codes/bin
codes/minios/platform/platform.asm必須用masm32進行編譯,編譯的結果在codes/minios/platform/platform.obj
其餘的代碼都用vc6編譯即可,vc6的工程在codes/minios/minios.dsw
如果你手邊沒有nasm和masm32,不要緊,因為這兩個文件一般不需要改動,直接用我編譯好的目標文件就可以了
雙擊minios.dsw打開vc6,點擊菜單Project-&>Project Setting-&>Debug,修改Executable for
debug session一欄
將Virtual PC.exe的完整路徑填入。如果你安裝在默認的路徑下,就不需要修改它。
然後直接Ctrl-F5運行就可以編譯並且運行了。
vc工程是在dll的工程的基礎上配置的
1、將所有相關的文件加到工程中來。
2、由於對於debug版本的代碼生成,vc會加入不少調試代碼,不好控制,所以刪除Win32 Debug的配置
3、由於默認的Release配置中,會加入Intrinsic
Functions的優化,他會用vc libc中的函數代替你寫的標準C語言庫函數。因此必須自定義優化方案。project setting-&>C++-&>Optimizations選customize並且勾上除了Assume
No Aliasing, Generate Intrinsic Functions, Favor Small Code, Full Optimization外的優化選項。
4、在project
setting-&>C++-&>Preprocessor-&>Additional include directories中加入include這個目錄,並且勾上Ignore
standard include paths
5、project
setting-&>Link中,output file name改成../bin/minios.dll。勾上Ignore all default libraries和Generate mapfile,
object/libraty modules中的內容清空
6、project
setting-&>Link-&>Debug中mapfile name改成../bin/minios.map,project setting-&>Link-&>Output中Entry-point
symbol改成main
7、project
setting-&>post-build step中添加一行"../bin/relocate.exe" ../bin/minios.dll ../bin/bootsector
../bin/minios.vhd
8、project
setting-&>Debug中Executable for debug session改成C:Program
FilesMicrosoft Virtual PCVirtual PC.exe,working directory改成../bin,Program
arguments改成-singlepc -pc vm -launch
如果我沒有忘記什麼的話,應該就這些了。這樣你的vc就可以編譯minios的原代碼了。編譯的結果在../bin/minios.dll
為什麼使用dll的工程呢?
因為windows的dll中有一個relocation的段,他列出了該dll文件如果要重定位的話所有需要修改的地址偏移。假設dll默認的載入位置是0x10000000,而在minios中我希望把它定位在0x400000則只需要把重定位表的每一項所指向的地址減去(0x10000000- 0x400000)就可以了。這也是relocate.exe這個程序的主要工作。
至於具體pe文件的結構以及重定向表的結構,網上有很多,我手邊暫時沒有資料,可以參看relocate.exe的原代碼
minios的引導過程和內存布局
首先,pc機的bios程序會將bootsector載入到0x7C00, 此時段寄存器的值我也不大清楚,但是不要緊,自己重新設一遍吧。把ds, es, ss都設成cs一樣的值,把sp放在0x8000的位置上,這樣我們就有了512位元組的堆棧了。
然後,bootsector將minios讀出放在從0x400000開始的內存空間上。隨後bootsector簡單的設置了GDT後直接進入保護模式並且將控制權交給minios的entrypoint。從0x100000到0x3fffff是內核堆,內核所需要的動態內存都可以從此堆上使用keMalloc和keFree分配。內存最低的4K位元組被用來存放中斷向量指針,就像純DOS那樣。從0x4000開始到0x8000存放了PCI匯流排配置數據塊。從0x10000到0x1ffff的64K位元組用來作為IDE的DMA內存塊,其他的低端內存暫時還沒有分配,可能會作為文件系統的緩存。
main函數先重新配置了8259中斷控制器,8253時鐘控制器,設置了IDT
GDT,初始化時鐘,內核堆和任務子系統後,建立了第一個任務main,入口是keEntryMain
keEntryMain首先打開中斷並且載入console和keboard的驅動,然後建立DPC任務,kdebug任務以及一個測試任務
這本書 應該可以滿足你
過來人網站的清華操作系統公開課:操作系統 - 陳渝現在在學堂在線有這門課。
補充一個
Pearson - Operating Systems Design and Implementation, 3/EMINIX完整實現一段一段給你講,包括代碼都給了。是的,就是那個MINIX。有中文版,但是是第二版。有很多相關的書。《OrangeS:一個操作系統的實現》《30天自製操作系統》可以選擇一本跟著做
嵌入式系統的RTOS,比如ucos,大概是入門最快的。
最簡單的os了。mit 6.828
我來騙一下star哈哈szhou42/osdev虛擬內存?硬碟驅動,EXT2文件系統,虛擬文件系統?簡陋的GUI界面?多進程(爛尾樓 )?網路數據收發(只是簡單收發raw packets,各種網路協議還在寫)?這是我學習過程中用到的主要網站和資料
BrokenThorn Entertainment
Global Descriptor Table
Unofficial Mirror for JamesM"s Kernel Development Tutorial Series
Expanded Main Page
Build software better, together 樓上大神全是幾千字長文看起來有點怕,我簡明扼要地說一些寫OS的重要心得吧1 教程上沒看懂的代碼千萬不要照抄,最好在自己理解的基礎上重新寫一遍。我在看JamesM"s Kernel教程的paging部分時就吃了這個虧,全盤抄了他的代碼,結果發現他的代碼總有神奇的bugs讓你的os崩潰。結果我把寫了一個半月的代碼全部推翻,在完全理解後自己重新寫出來的paging代碼,不能說完全沒bugs吧,至少現在已經四五個月了都沒有再因為paging部分的代碼而使os崩潰。2 搭建方便使用的調試器,我個人用的是qemu+gdb配合,源碼級調試3 在os最基礎的設施(中斷,異常,VGA driver)都實現後,馬上寫個printf和hexdump函數,因為有一些極端情況,gdb下斷點+單步跟蹤+觀察變數 這種辦法會失效 :(。4 快點寫個malloc函數!越快越好!!文件系統,進程管理,GUI這些都需要用到大量的數據結構,而最方便的方法就是用malloc來申請和釋放這些結構。最後,說一下os開發的流程,這只是我個人的路線。第一步,很多人會想寫bootloader,但是我建議先跳過這一步,直接用grub或者qemu的自帶bootloader,先跳過這些繁瑣的細節,專註於OS內核的開發。第二步,建立好各種gdt,idt,中斷,異常等機制,這樣系統出什麼錯的時候馬上就能發現。第三步,printf函數,這意味著你得先寫VGA driver,但兩者都不難第四步,實現虛擬內存和分頁機制,在此基礎上實現kmalloc函數。第五步,實現多進程/線程第六步,寫個PCI驅動!PCI是用來訪問各種硬體的,例如硬碟,網卡,都得通過PCI來控制,實際上我就是因為想寫硬碟驅動,才寫的PCI驅動。第七步,寫硬碟驅動,實現EXT2文件系統,實現VFS文件系統。第八步,GUI,設計一種數據結構存儲和顯示各種窗口。初步可以用VGA試驗一下編寫圖形操作系統的樂趣,但是要想有高解析度 真彩色還是得寫VESA驅動才能得到。 很多人覺得圖形操作系統很酷炫,但這反而是寫OS裡面最簡單,最容易調試的一步(當然了要做VESA驅動還是很麻煩,因為在保護模式下沒法用中斷調用)。第九步,實現網卡驅動,實現TCP/IP協議棧!第十步,發揮你的想像!Network File System maybe?試著做過。這是一個極有挑戰的事情。它需要你有非常廣的知識:
最基本數字電路知識,這樣你才能知道如何控制晶元。你需要熟悉一款CPU,這很難。新出的CPU如ARM資料少。傳統的CPU如x86設置起來又極為複雜。你需要非常熟悉C語言、比較熟悉彙編語言,還要熟悉彙編inline,需要比較熟悉編譯器,熟悉freestanding的環境。你需對操作系統是如何工作的非常清楚,因為你幾乎不可能設計出一個標新立異的操作系統來。你需要有極強的診斷調試能力,你通常無法知道你的程序為什麼不能正常運行,這幾乎會逼瘋你。如果是x86建議按以下步驟來走:1、寫一個bootloader能輸出HelloWorld.2、你的Bootload能載入另一段程序主程序,主程序能顯示HelloWorld。3、主程序能進入保護模式,能接收定時中斷、並在屏幕上顯示時間。能做到第3點你應該就知道下面應該幹什麼了。但我覺得工作5年,能做到第3點的人少於1%。強答一個給自己的os做個廣告。。
https://github.com/zhengruohuang/toddler這個os的設計目標是。。highly portable, highly flexible, highly usable
(high performance,real-time神碼的暫時不在考慮範圍之內,下面你會看到,很多設計並不high performance)另外,雖然說是portable,但是除了ia32以外,只把一小部分移植到了raspberry pi 2上。。(如果有人想donate各種奇特arch的machine的話歡迎聯繫我。。尤其需要mips和sparc)大概說一下各種feature
首先這個os支持smp,嗯,貌似比不少人的都厲害了然後,這個是microkernel。。但是並沒有做成那種誇張的many-server的(儘管如此但是其實也有不少server)然後當然,kernel本身也被做成了一個process,而kernel本身並沒有像其他os一樣被map到了所有process的1g或2g的位置。每個process的最高4mb的位置,被map到了被稱為hal的地方。。對,就是hardward abstraction layer那個意思的hal。這樣每個process幾乎可以利用全部的address space,以及,尤其對於kernel,便不再存在所謂high memory的概念了,對kernel而言,便是簡單的1 to 1 map,這樣便極大的簡化了physical mem mgmt的複雜程度,以及,整個設計看起來非常的乾淨另外對於hal,便是實現highly portable的核心。所有arch相關的代碼全部在hal中,而hal提供了一組函數以及定義供kernel使用,當然代價是犧牲了很大的性能以及相當多extra的context switch。hal所導出的函數都是經過精心設計的,盡量避免了over abstraction。例如說,kernel並不假設某個arch支持mmu或某種特定的mmu設計,因此hal所導出的與mem map相關的函數,也非像linux一樣假設了抽象的多級頁表結構然後,是計劃之中的。。environmental systems。對於os本身而言,kernel和其他essential 的各種server並不應當是unix-like,或者刻意去支持、模仿某一種os或標準,而那些工作,應當交由env systems來實現。例如說,雖然某一個server中,實現了一種樹形結構用於管理多種資源,但並不應是完全像vfs一樣,而完全兼容linux的vfs則應當在linux env system中實現,也就是將vfs的結構map到底層os的結構中。當然這樣做的目的是,一個修改過的linux kernel作為一個env system,從而運行多種linux程序,而實現highly usable。當然,env system還可以油很多,比如freebsd env system,reactos env system等等
當然,這麼做雖然蠻有趣,但並不一定是最好的辦法。但是作為一個人在業餘無聊時間開發的os,為了讓它能運行很多很多程序,從而可能有人來用它,最終變為highly usable,也是不得以而為之的了最後還有件事。。這個os還有自己的build system
ps 看到有的答案說「有時候覺得書里的做法非常奇怪,你覺得你有更好的做法,一般是你想錯了。(當然只是一般)」 真的蠻不喜歡這種說法的雖然我很推崇祖先的智慧很偉大。。而且「當然只是一般你想錯了」但是,os這個東東,完全取決於你的設計,你覺得有更好的做法,是在正常不過的事情了,祖先的做法,很多是有歷史局限性的(例如fork和exec,如果你不知道的話,這麼做最開始完全是為了照顧一種奇葩的機器,但後人把這種奇怪的機制吹了天花亂墜也是莫名其妙),但假如你無法跳出那種歷史局限的話,一味的否定自己更好地想法也是正常不過了有本書不錯,叫做操作系統設計:Xinu方法 (豆瓣)
手把手寫操作系統。作者是普度大學大名鼎鼎的Comer。譯者是交大的鄒恆明。麻雀雖小,五臟齊全:進程調度,信號量,消息傳遞,內存池,io中斷,時鐘,文件系統,shell(叫做tty),還有驅動(雖然只是路由器),甚至還寫了個小型的UDP協議棧。不過有一點,作者用的是mips,雖然有改寫成x86版本(xinu在vbox上,使用debian8,並在上面進行xinu編程 - u011274209的專欄 - 博客頻道 - CSDN.NET)。而且裡面用的是內核級線程,但作者稱為進程。