![]() Ismael Ripoll 作者簡介: 一九九六年於 Valencia 綜合科技大學取得博士學位。 目前任職於 DISCA, 為作業系統專家。 研究專題有即時系統排程管理和作業系統等等。 一九九四年成為 Linux 的一份子。 嗜好有: 攀登派瑞尼斯山脈 (Pyrenees)、 滑雪、 以及當一名網路遊俠。 與作者聯繫 內容目錄: 酷酷的 Linux 即時系統 (KURT) 即時系統有什麼好的﹖ 可載入的模組 即時系統程式 任務通訊 結論 參考資料 |
RT-Linux II![]() 內容摘要: 這是第二篇探討 RT-Linux 的文章, 我會試著以更實務的觀點來說明 RT-Linux。 不過, 在深入細節之前, 我打算先用一段文字解說一九九八年初, 剛發展成功的即時作業系統的特色, 名為 Linux KURT。 酷酷的 Linux 即時系統 (KURT)一九九八年初, 有一個以 Linux 為基礎之新的即時系統誕生了。 KURT 是軟式的即時作業系統 (soft real-time system), 比如說, 排程程式會試著去滿足任務 (task) 執行時所需求的時間, 即使有任何任務執行完的時間超過所預期的, 也不會造成任何的悲劇。 KURT 的即時性任務可以運用 Linux 所有的公用程式, 這一點恰好跟 RT-Linux 的任務相反。 KURT 作業系統的核心程式有一些局部的修正:
即時系統的任務都是動態載入的模組。 KURT 最具特色的便是排程策略。 排程程式是循環的, 這種型式的排程程式, 是運用一張名為計畫表 (plan) 的表格, 來儲存所有進入排程的任務的動作: 啟動的時刻、 預備執行的任務、 任務的延時等等。 這張表格開機時就會建好, 等到任務要執行時, 排程程式的工作, 就僅剩下依序一項項讀取表中的資料了。 當排程程式讀到表中的尾端時, 排程程式會回到表中的最開頭處, 繼續處理任務的執行工作 —— 也就是這樣, 才把這種排程程式, 命名為循環排程程式 (cyclic scheduler)。 循環排程程式有諸多的優點:
最主要的困難點, 就在於怎樣去製作計畫表了。 還有, 每次只要有任何一項任務的參數做了修正, 就必須重建計畫表; 同時, 儲存計畫表所需的記憶體也大的驚人。 即時系統有什麼好的﹖也許多數人都以為, 即時系統的技術只能應用在 NASA, 或是導彈、 航空器之類的領域裡。 雖然, 在多年以前, 這的確是個不折不扣的事實, 然而, 世事原本就滄桑難料, 近年來由於資訊系統和電子的整合應用, 正逐步大量地引進一般人的日常生活中,情勢已經有了改變。 和我們日常生活息息相關最直接的證據, 就是電傳通訊 (telecommunications) 和多媒體的應用領域, 比如說, 如果我們想要讓電腦, 可以重覆播放一個儲存於硬碟裡的音效檔, 處理音效的程式就得不斷地 (或者更好的, 是固定周期的) 去硬碟裡讀取檔案資料, 解壓縮, 然後把數位資料送出去給音效卡。 假如說, 同一時間, 我們正在操作一套應用軟體, 可能是文書處理程式, 或是正在編譯 Linux 的核心程式; 顯然, CPU 處理器會以周期循環的片段時間, 來處理其他的任務。 如果不放音效, 改在螢幕上重覆播放影像, 微處理器分段處理的結果, 影像便會有間歇性的停頓。 這種系統便是所謂的「軟式即時系統」 (執行期間有中斷的現象並不會造成什麼巨大的災害, 不過, 這樣的系統的確會降低服務的品質)。 RT-Linux 的應用程式, 和一般正常的即時應用程式大異其趣。 對 RT-Linux 而言, 我們可以全然掌控 PC 的運作 (我會說 PC 而不是電腦, 是因為到目前為止, 除了微電腦之外, RT-Linux 還沒有移植到其他種的機器架構上去), 如同過去的 MSDOS 一般。 當一個即時系統的任務交給 CPU 去執行時, 這個執行中的任務是可以去存取 PC 上所有的位址阜的, 例如植入中斷服務常式, 暫時關閉中斷服務常式的功能等等。 換句話說, 我們可以"摧毀"這套系統, 如同視窗系統那麼的不堪一擊。 但是, 話又說回來, 這種方便之門, 對於很喜歡寫些精巧的小程式, 給自己電腦去跑的人而言, 便具有相當大的魅力了。 可載入的模組要想瞭解 RT-Linux, 還有具備能力去使用 RT-Linux, 有關 Linux 動態載入模組的觀念, 是有必要知道的。 Matt Welsh 已經寫了完整的文件, 來解說模組所有的細節。 模組到底是什麼﹖大部份的 UNIX 系統存取硬體時(位址阜、記憶體、中斷等等), 都是經由特別的檔案來處理的, 通常也都是唯一的方法。 系統安裝時, 會事先安裝好每一種週邊設備所需的驅動程式。 不過, 有很多不錯的書籍都有教我們怎麼去寫設備驅動程式, 這常常是既無聊又冗長的工作, 因為我們得寫出為數不少的函數, 把驅動程式和系統連結起來。 模組可以說是"作業系統的片段程式碼", 可以在執行期間隨時加入或是抽離出來。 假設有一支程式的原始碼, 分別儲存在各個不同的檔案裡, 要拿來編譯, 首先, 每個單一的檔案會先被做成 ".o" 之目的檔, 然後, 所有的目的檔再連結成單一的可執行檔。 假設目的檔中, 有一個檔案含有 main 這個函數可以執行, 那麼作業系統便可以將這個檔案載入記憶體中執行, 並且在需要的時候, 把其他的目的檔連結進來一起執行。 當然, 核心本身自然有能力處理這樣的問題。 事實上, 當 Linux 開機之後, 也只有執行檔 vmlinuz 載入記憶體裡, vmlinuz 含有 Linux 核心不可或缺的元件, 待系統轉為執行期間時, 核心便可依需求來載入所需的模組, 或是釋放不需要的模組。 對 Linux 核心而言, 模組是可有可無的特色。 這項特色必得在編譯核心時, 選擇適當的選項才得以竟全功。 就我所知道的, 所有發行系統的核心程式, 都是以模組化定為預設選項的。 我們甚至於可以替系統設計新的模組, 並由核心載入執行, 而不用重新編譯系統, 也不用重新啟動系統。 當一模組載入核心時, 便成了作業系統的一部份, 因此:
如同我們所見的, 動態載入模組已經有一些即時系統程式的特點: 動態載入模組, 會避免由分頁錯誤所造成的時間延遲, 而且動態載入模組, 可以存取所有的硬體資源。 怎麼建模組﹖ 怎麼用﹖模組可以 "C 語言" 來寫。 這裡有個小小模組的例子 (要執行下面所提到的命令,最好是以 su、root 的身份登錄系統): example1.c #define MODULE #include <linux/module.h> #include <linux/cons.h> static int output=1; int init_module(void) { printk("Output= %d\n",output); return 0; } void cleanup_module(void){ printk("Adi鏀, Bye, Chao, Ovuar, \n"); } 以下一行的參數來編譯 example1.c: # gcc -I /usr/src/linux/include/linux -O2 -Wall -D__KERNEL__ -c example1.c選項 -c 是要求 gcc 在產生目的檔之後就停下來, 不要再做連結的動作。 最後的結果是一個目的檔, example1.o。 核心程式缺少標準輸出函數, 因此我們不可以用 printf() 這個函數, 而要以核心程式所提供的 printk() 函數來代替。 printk() 和 printf() 幾乎沒什麼兩樣, 唯一的差別是 printk() 會把輸出的結果, 送到核心程式的環緩衝區 (ring buffer) 裡。 這個緩衝區是系統所有訊息集中的地方, 就像開機時所見到的訊息, 都可以在這個環緩衝區裡找到。 任何時候, 我們都可以用 dmseg 命令查看緩衝區的內容, 或是直接檢驗 /proc/kmsg 這個檔案。 注意到這個模組裡並沒有 main() 函數, 反而有個不帶任何參數的 init_module() 函數。 cleanup_module() 是最後一個釋放模組前, 所必須呼叫的函數。 insmod 是用來載入模組, 然後執行模組的。 # insmod example1.o 現在, 我們已經安裝好 example1 模組了, 而且也執行了 example1 的 init_module() 函數。 要看結果, 請打下列的命令: # dmesg | tail -1 Output= 1 命令 lsmod 會列出當前所有載入核心中的模組: # lsmod Module Pages Used by: example1 1 0 sb 6 1 uart401 2 [sb] 1 sound 16 [sb uart401] 0 (autoclean) 最後呢,我們用 rmmod 來釋放模組: # rmmod example1 # dmesg | tail -2 Output= 1 Adi鏀, Bye, Chao, Orvua, dmesg 顯示了函數 cleanup_module() 已經執行了。 現在, 我們只差還不知道怎麼把參數傳給模組了。 這個方法出奇的簡單, 只要指定數值給整體變數, 再藉由 insmod 把參數傳遞給模組就行了。 例如: # insmod ejemplo1.o output=4 # dmesg | tail -3 Output= 1 Ad甐s, Bye, Chao, Orvua, Output= 4 現在, 模組大大小小的事情, 該知道的都知道了。 回 RT-Linux 了吧! 你的第一個即時系統程式首先, 記住一點, 要想用 RT-Linux, 就得預先準備好 Linux 的核心程式, 以支援即時系統模組。 這一部份的說明, 詳見第一篇文章。 RT-Linux 可以有兩種操作方法:
這一次我想先討論在第一種情況下, 怎樣把 RT-Linux 當作即時系統來使用。 閣下即將要看到的程式範例, 本身並沒有什麼"用途", 僅僅是設定了一個即時系統的任務而已(一個簡單的迴圈程式): example2.c#define MODULE #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> #include <linux/rt_sched.h> RT_TASK task; void fun(int computo) { int loop,x,limit; limit = 10; while(1){ for (loop=0; loop<computo; loop++) for (x=1; x<limit; x++); rt_task_wait(); } } int init_module(void) { RTIME now = rt_get_time(); rt_task_init(&task,fun, 50 , 3000, 1); rt_task_make_periodic(&task, now+(RTIME)(RT_TICKS_PER_SEC*4000)/1000000, (RTIME)(RT_TICKS_PER_SEC * 100)/1000000); return 0; } void cleanup_module(void){ rt_task_delete(&task); } 同樣的,以下列的命令來編譯 example2.c: # gcc -I /usr/src/linux/include/linux -O2 -Wall -D__KERNEL__ -D__RT__ -c example2.c既然是模組,程式的起點自然是函數 init_module()了。這個模組頭一件做的事就是去讀當前的時間然後把時間儲存在一個局部變數裡。rt_get_time()函數會傳回從開機起所經過的時間值 RT_TICKS_PER_SEC (目前來講, RT_TICKS_PER_SEC是1.193.180,解析度是0.838 micro-seconds)。 執行到rt_task_init()時,"task" structure會進行初值設定,但還沒開跑。 這個的任務的主程式是fun(),第二個參數。下一個參數是當新task要開始執行時,要傳遞給新task的資料值。要注意到fun()要的是int型態的參數。下一個參數是task堆疊空間的大小,這是因為每一個task都有他自己的執行緒(thread of execution),因此每個task都需要有自己的堆疊空間。最後一個參數是優先權;就這個程式例來講,只有一件任務在系統上執行,我們可以設定任何所要的數值。 rt_task_make_periodic()會把任務轉換成循環式任務。這需要兩個時間參數,第一個是記錄任務第一次被啟動的絕對時間,而第二個是連續啟動的任務之間,從第一個任務起算的時間間隔。 這個即時系統任務(就是函數fun())是一個無窮的迴圈,只做兩件事情:一是浪費時間的迴圈,二是呼叫 rt_task_wait()。rt_task_wait()是用來暫時中止任務執行的函數,直到下一次的啟動時間(activation time)到了為止。接著任務會從緊接在rt_task_wait()後面的敘述開始執行。讀者應當明瞭,循環式的任務並非每次啟動時都會從頭開始執行程式,而是自行暫停執行,並等待下一次的啟動時間到來。這種設計機制會讓任務僅在第一次呼叫時執行一次初值設定的工作,因為任務不會從頭開始執行。 執行 example2 之前, 須先安裝好 rt_prio_sched 模組, 這是因為 example2 需要 rt_task_make_periodic()、 rt_task_delete() 和 rt_task_init() 這些函數。 函數 rt_get_time() 並沒有包含在這個模組裡面, 這個函數存放在 Linux 的核心程式中, 因此並不需要多做安裝的動作。 # modprobe rt_prio_sched # insmod ./example2.o 已有的 rt_prio_sched, 是系統本身的模組。 這個模組會在 Linux 核心編譯期間建出來, 然後拷貝到 /var/modules/2.0.33/ 這個目錄底下。 我們使用 modprobe 這個命令, 原因是這是個比較簡單的工具, 就載入模組來講(modprobe會在模組的目錄中搜尋欲載入的模組)。 (參見 modprobe(1))。 倘若所有的步驟都很順利, 只要再加 lsmod 命令, 就可以看到兩個模組都正確地載入了。 嗯, 到了這個階段, 閣下已經有了一個即時系統程式在系統上跑了。 你有沒有發現什麼特別的事情發生﹖ 假如處理器本身就跑得比較慢, 你會發現 Linux 跑得比平常還要慢。 你可以試著去增加 fun() 函數中, 內部迴圈的交替次數, 只要改變 rt_task_init() 函數的第三個參數便可以了。 我建議讀者不妨跑一下 ico, 查查看處理器能剩餘的時間少了多少。 因為即時系統程式所耗用的時間, 會造成處理器像是速度變慢了的, Linux 會以為所有的程序, 所需求的執行時間超過原本所需的。 如果計算出來的時間 —— 就是執行所有迴圈所需的時間 —— 大過 100 毫秒 (microseconds), Linux 就會"掛"在那邊了。 因為 Linux 是背景程式, 而這個即時系統任務會耗盡所有的時間。 事實上, Linux 並沒有掛掉, Linux 只是得不到處理器的時間罷了。 任務通訊RT-Linux 只有一種任務通訊的方法: Real-Time FIFO。 這個方法很類似 UNIX 的 PIPEs, 僅以資料流來通訊, 而沒有任何的資料結構。 FIFO 是一塊固定大小的緩衝區, 用來讀寫資料之用。 採用 FIFOs 可以讓即時系統, 任務間的內部通訊趨於穩定; 對於一般 Linux 的任務而言, 也同樣適用。 從正常程序 (process) 的眼光來看, FIFO 是一個特殊字元的檔案。 一般而言, 會是 /dev/rtf0、 /dev/rtf1 等等諸如此類的。 這些檔案並不是 Linux 原有的, 所以必須額外再建。 方法如下: # for i in 0 1 2 3; do mknod /dev/rtf$i c 63 $i; done如果 FIFOs 的需求超過上列所舉的, 其他的 FIFO 就可以依樣畫葫蘆地製作出來, 如 rtf4、 rtf5 等等。 這些特殊檔案的作用, 就好像handler的介面一樣, 但是如果 handler 不存在, 這些特殊檔案就一點價值也沒有了。 事實上, 如果作業系統沒有相對應的 handler, 一定沒有辦法成功地開啟這些特殊檔案。 ![]() 處理 FIFOs 的檔案和正常的檔案沒什麼兩樣 (open、 read/write、 close)。 如果 Linux 正常的程序 (process) 要使用 FIFOs, 首先, 即時系統程式得先建立相對應的 FIFO。 從即時系統任務的眼光來看, FIFOs 是經由特定函數來使用的:
下一個例子來看看這些函數的使用方法。 這是一個 RT-Linux(音效)發行系統中, 做過些微修改的例子: example3.c#define MODULE #include <linux/module.h> #include <linux/rt_sched.h> #include <linux/rtf.h> #include <asm/io.h> RT_TASK task; static int filter(int x){ static int oldx; int ret; if (x & 0x80) { x = 382 - x; } ret = x > oldx; oldx = x; return ret; } void fun(int dummy) { char data; char temp; while (1) { if (rtf_get(0, &data, 1) > 0) { data = filter(data); temp = inb(0x61); temp &= 0xfd; temp |= (data & 1) << 1; outb(temp,0x61); } rt_task_wait(); } } int init_module(void){ rtf_create(0, 4000); /* enable counter 2 */ outb_p(inb_p(0x61)|3, 0x61); /* to ensure that the output of the counter is 1 */ outb_p(0xb0, 0x43); outb_p(3, 0x42); outb_p(00, 0x42); rt_task_init(&task, fun, 0 , 3000, 1); rt_task_make_periodic(&task, (RTIME)rt_get_time()+(RTIME)1000LL, (RTIME)(RT_TICKS_PER_SEC / 8192LL)); return 0; } void cleanup_module(void){ rt_task_delete(&task); rtf_destroy(0); } 如同第二個範例所示, 我們需要 rt_prio_sched 模組的服務, 但是, 這一次為了使用 FIFO, 我們必須同時載入 rt_fifo_new 模組。 頻率 8192Hz 的循環即時系統任務已經建好了。 如果這任務發現有任何的資料, 送到 PC 發聲器的位址阜, 就會從 FIFO 0 裡讀取位元組的資料。 如果我們把一個 ".au" 格式的音效檔案拷貝給 /dev/rtf0, 現在就可以聽聽看音效檔的音效如何了。 因為 PC 的硬體, 僅允許以一位元去調節信號, 所以不必去在意音效的品質低劣。 發行系統的目錄 testing/sound 中有 linux.au 這個檔案, 可以用來測試。 接著是編譯程式, 然後執行: # gcc -I /usr/src/linux/include/linux -O2 -Wall -D__KERNEL__ -D__RT__ -c example3.c# modprobe rt_fifo_new # modprobe rt_prio_sched # insmod example3.o # cat linux.au > /dev/rtf0 注意到 cat 這個工具程式是怎麼用來做覆寫檔案的, 包含了特殊檔案。 命令 cp 有同樣的效果。 要比較即時系統的特色會怎樣影想複製品的品質, 我們只好另外再寫個程式, 用 Linux 正常的程序來處理相同的工作: example4.c#include <unistd.h> #include <asm/io.h> #include <time.h> static int filter(int x){ static int oldx; int ret; if (x & 0x80) x = 382 - x; ret = x > oldx; oldx = x; return ret; } espera(int x){ int v; for (v=0; v<x; v++); } void fun() { char data; char temp; while (1) { if (read(0, &data, 1) > 0) { data = filter(data); temp = inb(0x61); temp &= 0xfd; temp |= (data & 1) << 1; outb(temp,0x61); } espera(3000); } } int main(void){ unsigned char dummy,x; ioperm(0x42, 0x3,1); ioperm(0x61, 0x1,1); dummy= inb(0x61);espera(10); outb(dummy|3, 0x61); outb(0xb0, 0x43);espera(10); outb(3, 0x42);espera(10); outb(00, 0x42); fun(); } 這支程式可以像其他簡單的程式那樣來編譯: # gcc -O2 example4.c -o example4 And to execute it: # cat linux.au | example4 要經由 Linux 的程式來存取硬體的位址阜,我們得先請求作業系統的准許。 這是避免程式直接存取硬體資源, 最基本且必要的保護措施。 例如, 呼叫函數 ioperm() 會告訴 Linux, 我們希望去存取某一定範圍的 I/O 位址阜。 只有當程式是以 root 的身份來執行時, 程式才有可能得到這種權力。 另一個值得注意的是, 8192Hz 的頻率如何調節音效, 使其產生聲音。 雖然有個系統呼叫函數 nanodelay() 可用, 但這個函數僅有固定的百萬分之一秒 (milliseconds) 的解析度; 因此, 我們必須使用迴圈來虛擬等待的時間, 使時鐘會暫停一下下。 這個等待迴圈必須調整到差不多可以在 100MHz 的 Pentium 上執行。 現在, 我建議讀者不妨測試一下 example4 和 ico。 聲音聽起來怎麼樣﹖ 改換成即時系統版本的 Linux 感覺如何﹖ 即時系統是不是有價值可言呢﹖ 結論第二篇文章把焦點放在設計即時系統程式。 文章中所列舉的程式範例, 都非常簡單且缺乏實際用途, 接下來的系列文章, 我會提出更有用的應用範例。 我們將可以用 Linux 來控制電視, 甚至於可以從遠端, 來決定你和你的 Linux 之間通訊的方法 (Linux box)!!! |
本文由 Frank J.S. Chen 所翻譯
主網站由 Miguel A Sepulveda 維護 © Ismael Ripoll 1998 LinuxFocus 1998 |