一、實現一個什么都不能做的嵌入式操作系統
1.首先確定CPU,在這里為了簡單,就選用嵌入式的CPU,比如ARM系列,之所以用RISC(簡單指令集)類型的CPU,其方便之處是沒有實模式與保護模式之分,采用線性的統一尋址,也就是不需要進行段頁式內存管理,還有就是芯片內部集成了一些常用外設控制器,比如以太網卡,串口等等,不需要像在PC機的主板上那么多外設芯片 2.確定要實現的模塊和功能,為了簡單,只實現多任務調度(但有限制,比如最多不超過10),實現中斷處理(不支持中斷優先級),不進行動態SHELL交互,不實現動態模塊加載,不實現fork之類的動態進程派生和加載(也就是說要想在你的操作系統上加入用戶程序,只能靜態編譯進內核中;不支持文件系統,不支持網絡,不支持PCI,USB,磁盤等外設(除了支持串口,呵呵,串口最簡單嘛),不支持虛擬內存管理(也就是說多任務中的每個進程都可以訪問到任何地址,這樣做的話,一個程序死了,那么這個操作系統也就玩完了) 3.確定要使用的編譯器,這里采用GCC,文件采用ELF格式,當然,最終的文件就是BIN格式,GCC和LINUX有著緊密的聯系,自己的操作系統,需要C庫支持和系統調用支持,所以需要自己去裁剪C庫,自己去實現系統調用 4.實現步驟:首先是CPU選型,交叉編譯環境的建立,然后就是寫BOOTLOADER,寫操作系統. 二、如何實現BOOTLOADER
1.之所以要實現一個專用的BOOTLOADER,一是為了更好的移植和自身的升級,二是為了方便操作系統的調試,當然,你完全可以將這部分所要實現的與操作系統相關的功能集成到操作系統中去 2.確定一個簡單的BOOTLOADER所要完成的功能:我們這里只需要完成兩個主要功能,一是將操作系統加載到內存中去運行,二是將自己和操作系統內核固化到ROM存儲區(這里的ROM可以是很多設備,比如嵌入式芯片中的FLASH,PC機上的軟盤,U盤,硬盤等) 3.BOOTLOADER的編寫: 第一步:要進行相關硬件的初使化,比如在at91rm9200這塊嵌入式板子上(以后都使用這一款芯片,主要是我對這款芯片比較熟悉,嘿嘿),大概要做接下來的幾方面的工作,
其一:將CPU模式切換進系統模式,關閉系統中斷,關閉看門狗,根據具體情況進行內存區域映射,初始化內存控制區,包括所使用的內存條的相關參數,刷新頻率等, 其二:設定系統運行頻率,包括使用外部晶振,設置 CPU頻率,設置總線頻率,設置外部設備所采用的頻率等。 其三:設置系統中斷相關,包括定時器中斷,是否使用FIQ中斷,外部中斷等,還有就是中斷優先級設置,這里只實現兩個優先級,只有時鐘中斷高一級,其它都一樣,而中斷向量初始化時都將這些中斷向量指向0x18處,并關閉這里的所有中斷,如果板子還接有諸如FLASH設備的話,還需要設置諸如FLASH相關操制寄存器, 其四:需要關閉CACHE,到此為止,芯片相關內容就完成初始化了
第二步:中斷向量表 ARM的中斷與PC機芯片的中斷向量表有一點差異,嵌入式設備為了簡單,當發生中斷時,由CPU直接跳入由0x0開始的一部分區域(ARM芯片自身決定了它中斷時就會跳入0x0開始的一片區域內,具體跳到哪個地址是由中斷的模式決定的,一般用到的就是復位中斷,FIQ,IRQ中斷,SWI中斷,指令異常中斷,數據異常中斷,預取指令異常中斷),而當CPU進入相應的由0x0開始的向量表中時,這就需要用戶自己編程接管中斷處理程序了,這就是需要用戶自己編寫中斷向量表,中斷向量表里存放的就是一些跳轉指令,比如當CPU發生一個IRQ中斷時,就會自動跳入到0x18處,這里就是用戶自己編寫的一個跳轉指令,假如用戶在此編寫了一條跳轉到0x20010000處的指令,那么這個地址就是一個總的IRQ中斷處理入口,一個CPU可能有多個IRQ中斷,在這個總的入口處如何區分不同的中斷呢?就由用戶編程來決定了,具體實現請參見以后相關部分,中斷向量表的一般用一個vector.S文件,當然,如何命名那是你自己的喜愛,但有一點需要聲明,那就是在鏈接時一定要將它定位在0x0處.
三、如何實現一個最簡單的操作系統
這里為了簡單,就不考慮可移植性開求,不從BOOT部分來接收參數,也不對硬件進行檢測,也不需要進行DATA段,代碼段的重定位。我只是讀了LINUX內核相關部分,并未自己去實現一個操作系統,所以我以下所說的只是概念性的東西: 1.接管系統的中斷處理,由于BOOT部分的代碼決定了那個中斷向量表,從而決定了系統中斷之后進入的內存位置,但BOOT并不知道操作系統的中斷處理函數位置所在啊,怎么辦呢? 如果你的板子可以重映射地址,也就是可以將內存條所在的位置重映射成0x0開始,那么在鏈接內核的時候,就將操作系統自己的中斷向量表定位在0x0處并且在BOOTLOADER引導結束時就完成映射操作,并讓CPU跳轉到0x0處執行;如果沒有重映射功能,我就不曉得怎么辦了,不過我想到一個折衷的辦法,就是在BOOTLOADER啟動完成時(也就是將CPU控制權交給操作系統內核時),重新改寫FLASH的0x0區域,就是將操作系統的內核的中斷向量表寫入FLASH區的0x0處,比如,當一個IRQ發生時,CPU決定了會跳入0x18(假設這里FLASH占用地址總線0x0至0x0fffffff,內存占用0x20000000至0x2fffffff),而BOOTLOADER在最后將0x18處的代碼修改成了0x20000000加上0x18的地址處的代碼,而這個地址就是內核的中斷向量表中的相關跳轉指令,就相當于跳轉進了內核所關聯的IRQ處理函數的地址上去執行中斷處理函數了,而這樣的不好之處在于:當系統重新上電之后,BOOT的中斷向量表已經被修改,除非BOOT本身不使用中斷,呵,在這樣簡單的系統中,BOOT是不需要中斷功能的 2.這里為了簡單,所以沒有使用分頁內存管理,就不需要建立頁表等操作,直接進行操作系統的堆棧設置,同BOOT一樣的設置過程一樣,接著就進行BSS段清零操作,這里的BSS段是指操作系統自身的BSS段,與BOOT的BSS段是同一個含義只是用在了不同的地方了,接著就跳入了MAIN函數 3.為了最大可能的簡單,采用靜態建立任務結構數組,比如只建立十個任務,那么首先要為這十個任務結構分配段內存,可以在堆上分配(這個分配的內存直到操作系統結束才會被釋放,當然也可以指定一片操作系統的其它地方都用不到的內存區域,不過這樣寫的話就有點外行的味道了,而符務結構數組的指針卻是全局變量,存放在BSS段或者DATA段),由于在上一步中已經分配了一個系統堆棧,那么我們這十個任務就分享這總體的堆棧區域這里的重點就是如果定義每個任務結構數組里面的結構,可以參照LINUX的相關部分設計 4.中斷處理:在第一步中已經確定了CPU進行相關的幾類型的中斷跳轉地址,而相同類型的中斷卻只有一個入口地址,這里的中斷處理就會完成以幾個動作: 其一:入棧操作,包括所有寄存器入棧,至于這個棧,就是在第二步中所設置的IRQ棧, 其二:屏掉所有中斷,呵,這里為了簡單起見,所以在處理中斷時不允許再次發生中斷 其三:讀取中斷相關的寄存器,判別是發生了什么中斷,以至于跳進相關的中斷處理函數中去執行(在這里只包括兩種中斷,一是時鐘中斷,另一個是SWI中斷,也就是所謂的系統調用時需要用到的) 其四:等待中斷處理完成,然后就開啟中斷并出棧,恢復現場,將CPU控制權交給被中斷的代碼處 注意: 其一:在MIAN中必須首先確定整個系統有哪些需要處理的中斷,也就是有哪些中斷處理函數,然后才編寫這里的中斷處理函數 其二:本操作系統不處理虛擬內存,其至連CPU異常都不處理(一切都為了簡單),一旦發生異常,系統就死機 5.對TIMER的實現,首先確定時間片,為了讓系統更穩定,而且我們不需要實時功能,盡可能讓時間片設置長一點,比如我們讓一個任務運行20個時鐘滴答數,然后應根據系統頻率來確定每個系統滴答所占用的毫秒,這里使用5毫秒讓系統定時器中斷一次,那么就需要寫時鐘寄存器,具體參閱芯片資料,計算下來,一個任務最大可能連續運行100毫秒,注意:我們的操作系統不支持內核搶占,同時只支持兩級中斷優先級,就是只有時鐘中斷的優先級高一點,其它的優先級都低一級,但是在中斷處理一節中卻屏掉了這個功能。因為一進入中斷處理,就禁止中斷,所以不管其它中斷優先級有多高都沒有用的,這樣做優點是簡單了,但不好之處顯而易見,特別在相關中斷處理函數如果進入了死循環,那么整個系統就死了,而且時間片也變得不準確了,反正都不用實時,也不需要實時鐘支持嘛至于中斷優先級設置請參閱芯片資料
6.進程調度的實現,也就是do_timer函數(時鐘中斷處理函數),有一個全局變量指針,指向的就是當前任務結構數組(或者鏈表),當時鐘中斷時,就進入此函數中,首先判斷任務結構體中的時間片是否用完,如未用完,就減一,然后退出中斷,讓CPU繼續運行當前的任結構,若用完了時間片,就重置時間片,并重新尋找任何結構數組中的下一個等待運行的任務,若找到了,就切換至新的任務,至于如何切換,請見下一頁描述,如果未找到就切換到IDLE任務(類似于LINUX,呵呵,所有的處理就是模仿LINUX,由于本人水平太差,所就不能自創一招),注意:為了簡單,所以沒有實現任務優先級,也未實現任務休眠等,也就是說只要靜態地決定了有十個任務,這十個任務就按先后順序一個一個執行,而且每個任務都不允許結束,就是說在每個進程中的最后一句代碼都必須用死循環,不然的話系統就跑飛了),還有一點,進程不支持信號,沒有休眠與喚醒操作,這個CPU就是不停地在運行,呵呵,反正CPU又不是人,所以不需要人權的哈!!!這種調度是不是簡單得不能再簡單了?????!!!! 7.串口不使用中斷,這就是最大可能的降低難度,串口使用論詢的方式來實現讀寫(當然是阻塞的方式了哦,而且只有寫,不允許讀,因為讀的時候需要涉及到采用中斷方式,因為輪詢方式有個不好的地方,那就是正在讀的時候,這里有可能當前進程的時間片用完了,系統切換到另一個進程,這里你在PC機的串口輸入的數據就丟棄了,唉,又是為了簡單嘛) 8,最后一步就是MIAN函數的最后一部分,將本進程當作IDLE進程(相當于修改任務結構數組中的數據),開啟中斷,將當前進程加入一段死循環,以免它退出去。 9.編譯你的BOOTLOADER,KERNEL,并燒寫至FLASH,反復調試 10.至此將你的at91rm9200(或者是其它相類似的芯片)的串口接上PC機,打開超級終端,打開板子電源,說不定你的操作系統就打印出了hello,world了!!!一個最簡單的操作系統就出來了!!
四、任務結構數組(或鏈表)的實現
我們的任務結構就采用鏈表形式吧,但其長度是限定了的,頭指針是一個全局指針變量(指針變量是一個無符號整型指針,其指針本身所在的地址是在BSS段,但其指向的內容是分配在堆上的一片內存),分配內核內存的函數就用kmalloc吧,kmalloc函數需要自己編寫呵,為了簡單,這個函數只接受一個參數,就是所需分配大小,這個函數做得很簡單,首先有一個全局針指,它在初始化時指向了整個堆的起始位置,并且固定大小,就是所謂的內核堆棧,在內核堆棧之后就是用戶堆棧,由于總共有十個任務,當然不包括內核本身的任務,所以整個堆棧就平均分成十一部分,注意:在所有任務初始化完成之后,還有一個步驟就是將內核這個任務移到用戶態,相當于要將自己的任務結構的堆棧指針修改一下就行了),判斷大小是否超出了內核堆的可分配范圍,還有一點,需要維護內核堆和其它任務的堆,需要進行分塊,并且有一個全局的內存使用標識,就用數組吧,簡單,0表示相應的內存部分未占用,1就表示占用,對應的kfree就相當于把標志置0),對于內存的維護,比較復雜,為了簡單,就定為4K,并且不能進行大于四K的內存申請,因為大于4K之后,由于沒有虛擬地址的概念,就不能實現堆上的連續分配地址,當然在棧上分配是可以大于4K的,棧是由編譯器和CPU所決定了的。 任務結構包括: 1.所剩的時間片 2.本任務所指向的代碼段內存地址,這里也就是函數入口地址 3.本任務所指向的數據段地址,這里的數據段被包含進了整個內核中,所以并沒有用,作為保留 4.本任務的函數體是否存在,也就是否會被調度 5.本任務所使用的棧指針 6.本任務所使用的堆指針 7.本任務的標識,用0代表是IDLE,1代表是其它進程 8.所有寄存器的值 9.當前PC值,初始化時被置成了函數入口地址 首先講解一下任務數組結構的初始化: 將先定義一個全局指針,然后將此指針強制轉換為一個任務結構指針,并通過kmalloc函在內核所占用的堆(前而講過內核的堆的起始就是整個堆的起始)上去分配十個任務結構所占的內存,這里是絕不會超過4K的并且為這十個任務結構賦值,將第一個任務置為IDLE,時間片為20,代碼段內存地址為main函數的的地址,數據段地址忽略,函數體存在,可以被調度,棧指針指向的位置根據以下來計算: 假定每個給每個任務可使用的堆棧設定為64K,而整個堆的起始位置是0x20030000,那么第一個堆指針所指向的就是0x20030000,棧就是0x20030000+64K的位置,第二個以后就以此類推 注意:在初始化任務結構之前,不允許系統使用堆,但可以使用棧,那么內核任務棧部分就分成了兩個,在未進行調度之前,棧就是上一頁中第二步中所設的棧,那么上一頁設置堆棧的時候就得注意必須將堆棧空間設成十個64K再加上在本步驟使用以前的最大可能所需的棧空間
再講解一下任務切換時所要做的事情: 進入整個中斷處理入口時,會將所有寄存器推入IRQ棧之中,并把值拷貝到當前任務結構相應的字段當中,并取出被中斷的進程的當前PC值存入當前任務結構中的相應字段中,接下就判別中斷類型,以進入相應的中斷處理函數,這里就會進入do_timer函數中,以下就是進入此函數之后的流程: 內核中還有一個全局指針,就是當前任務指針,它本身也是在系統BSS段中,它的定義如上一步中的那個全局指針一樣,當由系統時鐘中斷之后,就取出這個全局指針,上一步初始化完成之后,還會把這個指針指向第一個任務結構所在位置,也就是0x20030000處,那么就取出這個任務結構中的時間片字段,判斷其是否為0,
若為0,就進行以下的操作: 保存用戶態下的棧指針至當前任務結構,保存堆指針,并將搜索一下可以被調度的任務結構,并將此任務結構賦給當前任務指針,置需要進行任務切換標識,此標識同樣是一個全局變量,但它是被賦了初值,會放在整個系統的DATA段中,返回do_timer函數。
若不為0,就進行以下操作: 將時間片減一,返回do_timer函數接下來判斷任務切換標識,若為0,則進行以下操作: 不需要進行任務切換,所有寄存器出棧(這里的棧指的是IRQ棧),重新開啟中斷,切換到用戶模式,加載當前任務結構中的當前PC值字段,以退出中斷處理程序若此標識為1,則執行以下操作: 就需要進行任務切換,讓所有寄存器出棧(這里的棧指的是IRQ棧),將當前任務結構中的所有寄 存器的值恢復到相應寄存器中,將用戶態下的棧指針恢復至當前任務結構棧指針,將堆指針恢復至 當前任務結構堆指針,并把需要進行任務切換標識恢復為0,重新開啟中斷,切換到用戶模式,任務切換是通過加載PC值來實現的,也就是通過加載當前任務結構中的當前PC值字段,以退出中斷處理程序
五、系統調用的實現
本系統是完全可以不實現系統調用的,因為沒有實現內核態和用戶態的保護,完全可以不實現自己的C庫,所有的函數都像kmalloc之類的實現一樣,在內核中直接寫函數原型,但為了以后擴展,還是說一下系統調用,這里以malloc系統調用來實現 首先說明還有一個堆指針(前面在kmalloc時有一個堆指針,不過那個堆指針是為內核任務,中斷處理所提供),這里這個堆指針是用于用戶態的,它在系統初始化完成之前會賦上初值,其初值就是第一個任務結構所使用的堆的起始位置,也就是在內核所使用的堆加上64K的位置 函數庫中的malloc函數實現步驟如下: 1.首先檢測申請大小是否超出了4K,若超出4K,就返回錯誤 2.進行系統調用(這里用_syscall1,并只傳遞一個參數(所需分配大小) 系統調用函數_syscall1的實現: 1.將寄存器壓入堆棧(這里的棧指向就是當前任務的棧) 2.將系統調用號1放至R0,參數放入R1 3.發出SWI指令以產生SWI中斷(就是所說的軟中斷,陷阱) 此時系統發生中斷,會進入SWI中斷處理入口,下面說一下SWI入口函數的實現 1.取出R0的值,判斷其值,進入相應的分支處理代碼段 2.在此進入_malloc處理代碼段,取出R1的值,然后再得到前面所說的當前堆指針,并申請對應數據塊大小,置用于內存占用標識的相應字段,將當前堆指針放入R0,移動當前堆指針,改變當前任務結構的堆指針,切換到用戶態,返回SWI中斷系統調用_syscall1的返回處理: 為了簡單,在從內核態返回用戶態時,不再進行任務的重新調度,所以上面的步驟就相對簡單 1.當從SWI中斷返回后,系統就運行在了用戶態,此時取出R0的值,并賦值給需要申請內存的指針 2.在用戶態彈出寄存器,返回到上一層函數 malloc函數的返回,此時malloc函數直接返回指針就行了,整個malloc的流程就結束了,其它的系統調用同這個過程類似 到此為止,這個操作系統初步實現了,但好像什么事情都不能做,如果讓它支持串口中斷的話,或許可以做那么一點點事情,比如像單片機那樣的功能,整個系統的難點就是中斷處理和任務切換,在本例中,由于ARM不支持像0x86那樣的CPU級的保護模式,所以進行任務切換的時候,就得自己通過加載PC值的方法來實現,呵,因為我想不到更好的辦法,但這個辦法有一個不好解決的地方,就是寄存器入棧和出棧的保護,在進入中斷時,必須保護寄存器,但如果需要進行重新調度,就得從中斷上下文切換到進程上下文中,如何從中斷上下文切換到進程上下文呢?
我在這里所采用的方法很笨拙: 1.首先讓寄存器入棧 2.讓寄存器保存至當前任務結構數組,被中斷掉的進程的PC值保存至任務結構 3.處理timer中斷 4.如果進行任務切換,尋找下一個可調度的進程,然后把當前任務結構指下剛搜索到的任務結構,讓寄存器出棧,恢復當前任務結構里的值到寄存器,恢復堆棧指針,切換到用戶態,通過加載當前任務結構的PC值來恢復被掛起的進程這里在中斷上下文中使用了任務結構,這在LINUX上好像是不這樣用的,中斷上下文和進程上下文是兩個不同的概念,中斷上下文中不能訪問進程上下文里的任務結構,我實在想不出有什么辦法來實現進程調度了,所以請看到我這則文章的人提出好一點的方法. |