Github : https://github.com/He11oLiu/JOShtml
本文將介紹在本人JOS
中實現的簡單圖形界面應用程序接口,應用程序啓動器,以及一些利用了圖形界面的示例應用程序。java
本文主要涉及如下部分:git
RW/RW
調色板framebuffer
共享區域8bit
顏色深度BMP
格式圖片讀取與繪製
BMP
頭老是出現問題?不合理的數據?CGA
模擬器在圖形庫
中,已經將圖形模式打開,將顯存映射到內存中的一段空間。並進行了簡單的測試。github
實際上,直接對顯存寫是很不負責任的行爲。很早以前在寫java
的界面的時候,就接觸了雙緩衝技術,其實與顯示有關的思想都是差很少的,咱們應該提供一個framebuffer
。當完成一個frame
後,再將這個frame
update到顯存中。算法
uint8_t *framebuffer; void init_framebuffer(){ if((framebuffer = (uint8_t *) kmalloc((size_t)(graph.scrnx*graph.scrny)))== NULL) panic("Not enough memory for framebuffer!"); } void update_screen(){ memcpy(graph.vram,framebuffer,graph.scrnx*graph.scrny); }
通過實現kmalloc
與kfree
,已經能夠分配這個緩衝區,並直接向緩衝區寫入,最後再進行update
shell
#define PIXEL(x, y) *(framebuffer + x + (y * graph.scrnx)) int draw_xx() { xxx; update_screen(); }
從一個單一的應用程序角度來看,應分配一個單獨的畫布,而後選擇在一個位置顯示。canvas
typedef struct canvas { uint16_t width; uint16_t height; uint8_t *data; } canvas_t;
設計的模式是,與文件系統服務器相似,提供一個圖形系統服務器,用於接收從其餘的程序發來的請求。請求包括顯示的位置,以及canvas
。該服務器將canvas
寫入frambuffer並update。其餘程序與圖形服務器經過IPC
進行通信。瀏覽器
剩餘的事情就能夠交給用戶空間了。包括對canvas
的處理,更新顯示,添加各類元件。以前寫的字庫也能夠不用寫在內核了...緩存
首先實現繪製canvas
。服務器
int draw_canvas(uint16_t x, uint16_t y, canvas_t *canvas) { int i, j; int width = (x + canvas->width) > graph.scrnx ? graph.scrnx : (x + canvas->width); int height = (y + canvas->height) > graph.scrny ? graph.scrny : (y + canvas->height); cprintf("width %d height %d\n",width,height); for (j = y; j < height; j++) for (i = x; i < width; i++) PIXEL(i, j) = *(canvas->data + (i - x) + (j - y) * canvas->width); update_screen(); return 0; }
而後在lib
中新建canvas
的相關方法:
int canvas_init(uint16_t width, uint16_t height, canvas_t *canvas); int canvas_draw_bg(uint8_t color, canvas_t *canvas); int canvas_draw_ascii(uint16_t x, uint16_t y, char *str, uint8_t color, canvas_t *canvas); int canvas_draw_cn(uint16_t x, uint16_t y, char *str, uint8_t color, canvas_t *canvas); int canvas_draw_rect(uint16_t x, uint16_t y, uint16_t l, uint16_t w, uint8_t color, canvas_t *canvas);
其中只須要將原來的PIXAL
宏換爲
#define CANVAS_PIXEL(canvas, x, y) *(canvas->data + x + (y * canvas->width))
測試canvas
canvas_t canvas_test; canvas_init(300, 200, &canvas_test); uint8_t testcanvas[60000]; canvas_test.data = (uint8_t *)testcanvas; canvas_draw_bg(0x22,&canvas_test); canvas_draw_ascii((uint16_t)2, (uint16_t)2, test_ascii, (uint8_t)0xff, &canvas_test); canvas_draw_cn((uint16_t)2, (uint16_t)50, test_cn, (uint8_t)0xff, &canvas_test); draw_canvas(500, 500, &canvas_test);
第一種設計與以前描述的一致:
提供一個圖像服務器,接收請求,從用戶進程傳來須要畫的畫布和顯示位置,並在位置上進行繪畫。這種方式遇到的問題是畫布過大,一頁可能裝不下。須要mmap
(還沒寫)
第二種設計是一個launcher
和application
兩個單獨的單頁面切換制度。
這樣就是launcher
提供應用啓動界面,application
提供應用界面。
從新回顧了一下內存分配,內核與用戶態數據共享的方法後,決定先就第二個思路實現一個簡單的用戶內核都可見可讀寫的Framebuffer
。
RW/RW
的Framebuffer
首先分析一個以前作過的pages
,是如何作到用戶態能夠讀,內核態能夠寫的。
在mem_init
的時候在在內核空間中分配指定的空間給pages
pages = boot_alloc(sizeof(struct PageInfo) * npages); memset(pages, 0, sizeof(struct PageInfo) * npages);
利用boot_map_region
將其映射到內核頁表中的UPAGES
的位置。
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U | PTE_P);
這樣內核中依然能夠經過pages
訪問頁表,而用戶程序在entry
的時候經過給pages
變量賦予存儲位置
.globl pages .set pages, UPAGES
也能夠經過pages
變量進行訪問。
framebuffer
再思考若是須要這麼一個framebuffer
,咱們須要放到哪裏。仿造上面的UVPD
,UPAGES
,等,決定就放在接近ULIM
的位置。一個PTSIZE
也遠超咱們須要的空間,爲之後擴展也留下了餘量。
/* * ULIM, MMIOBASE --> +------------------------------+ 0xef800000 * | Cur. Page Table (User R-) | R-/R- PTSIZE * UVPT ----> +------------------------------+ 0xef400000 * | RO PAGES | R-/R- PTSIZE * FRAMEBUF ----> +------------------------------+ 0xef000000 * | FRAME BUFFER | RW/RW PTSIZE * UPAGES ----> +------------------------------+ 0xeec00000 * | RO ENVS | R-/R- PTSIZE * UTOP,UENVS ------> +------------------------------+ 0xee800000 */ // User read-only virtual page table (see 'uvpt' below) #define UVPT (ULIM - PTSIZE) // Read-only copies of the Page structures #define UPAGES (UVPT - PTSIZE) // Read-write framebuffer #define FRAMEBUF (UPAGES - PTSIZE) // Read-only copies of the global env structures #define UENVS (FRAMEBUF - PTSIZE) // #define UENVS (UPAGES - PTSIZE)
因爲圖像初始化在內存初始化以後,須要留一個接口來進行映射。(boot_map
是隱式函數)
void map_framebuffer(void *kva) { boot_map_region(kern_pgdir, FRAMEBUF, PTSIZE, PADDR(kva), PTE_W | PTE_U | PTE_P); }
在分配好內核中的Framebuffer
就能夠開始映射了
void init_framebuffer() { if ((framebuffer = (uint8_t *)kmalloc((size_t)(graph.scrnx * graph.scrny))) == NULL) panic("Not enough memory for framebuffer!"); map_framebuffer(framebuffer); }
在libmain
的時候初始化便可
framebuffer = (uint8_t *)FRAMEBUF;
用戶程序在寫完frambuffer
後,如何才能刷新屏幕?這又須要一個新的內核調用
static int sys_updatescreen() { update_screen(); return 0; }
配套的一些代碼就不解釋了。
上一個部分已經將一個用戶與內核都可讀寫的緩衝區域,並提供了一個系統調用,用於將顯示緩存內容拷貝至MMIO
顯存。從理論上來講,用戶空間的程序如今已經能夠直接在這塊Framebuffer
上繪製任何圖形。
可是對於一個友好的用戶界面,至少要支持一種格式的圖片顯示。這裏選擇一種最簡單的,沒有壓縮過的位圖顯示實現。推薦各位想本身寫圖形界面的小夥伴也從這裏入手。
關於BMP
的讀取能夠參考這篇文章256-Color VGA Programming in C Bitmaps & Palette Manipulation。要注意詳細讀其中的每個細節,直接掃一眼看代碼寫的話會遇到不少問題,下面會提到我遇到的問題與解決方案。
There are many file formats for storing bitmaps, such as RLE, JPEG, TIFF, TGA, PCX, BMP, PNG, PCD and GIF. The bitmaps studied in this section will be 256-color bitmaps, where eight bits represents one pixel.
One of the easiest 256-color bitmap file format is Windows' BMP. This file format can be stored uncompressed, so reading BMP files is fairly simple.
Windows' BMP
是沒有壓縮過的,因此讀這種BMP
會很是方便。這裏也準備就支持這種格式的圖片。
There are a few different sub-types of the BMP file format. The one studied here is Windows' RGB-encoded BMP format. For 256-color bitmaps, it has a 54-byte header (Table III) followed by a 1024-byte palette table. After that is the actual bitmap, which starts at the lower-left hand corner.
BMP的文件格式以下:
Data | Description |
---|---|
WORD Type; |
File type. Set to "BM". |
DWORD Size; |
Size in BYTES of the file. |
DWORD Reserved; |
Reserved. Set to zero. |
DWORD Offset; |
Offset to the data. |
DWORD headerSize; |
Size of rest of header. Set to 40. |
DWORD Width; |
Width of bitmap in pixels. |
DWORD Height; |
Height of bitmap in pixels. |
WORD Planes; |
Number of Planes. Set to 1. |
WORD BitsPerPixel; |
Number of bits per pixel. |
DWORD Compression; |
Compression. Usually set to 0. |
DWORD SizeImage; |
Size in bytes of the bitmap. |
DWORD XPixelsPerMeter; |
Horizontal pixels per meter. |
DWORD YPixelsPerMeter; |
Vertical pixels per meter. |
DWORD ColorsUsed; |
Number of colors used. |
DWORD ColorsImportant; |
Number of "important" colors. |
下面就我遇到的四個嚴重的問題,來實現BMP
格式的圖片讀取。
這裏要注意GCC
默認4字節對齊!!!!!!
可是Bitmap
的文件頭是14Bytes
,若是不加特殊標記,其會變成16Bytes
,致使文件偏移錯誤
typedef struct bitmap_fileheader { uint16_t bfType; uint32_t bfSize; uint16_t bfReserved1; uint16_t bfReserved2; uint32_t bfOffBits; }__attribute__((packed)) bitmap_fileheader; typedef struct bitmap_infoheader { uint32_t biSize; uint32_t biWidth; uint32_t biHeight; uint16_t biPlanes; uint16_t biBitCount; uint32_t biCompression; uint32_t biSizeImage; uint32_t biXPelsPerMeter; uint32_t biYPelsPerMeter; uint32_t biClrUsed; uint32_t biClrImportant; } bitmap_infoheader;
這裏添加的__attribute__((packed))
關鍵字用於告訴編譯器,最小單位進行對齊,而不使用默認的四單位進行對齊。
最開始設置VBE
的時候,我覺得所謂8位色
就是真8位色
,以前徒手擼FPGA
的顯卡的時候也是這麼設計的,直接讀取後分位後丟給一個D/A
輸出給VGA
變成各自的顏色信號。可是實際系統沒有這麼簡單,其實現了一個8位
到32位
的對應關係,提供了256
位色的調色板。這樣能支持更自由的調色方案,顯示更加定製化的顏色。因此以前我沒有初始化調色板,利用了系統默認的調色板,因此顯示纔出現問題。
可是理解BMP
又出現了誤差,覺得大致上是遵循RGB3bit3bit2bit
的配色方案,先寫了一個初始化調色板的函數:
void init_palette() { int i; outb(0x03c8, 0); for (i = 0; i < 256; i++) { outb(0x03c9, (i & 0xe0) >> 2); //| 0xA); outb(0x03c9, (i & 0x1c) << 1); //| 0xA); outb(0x03c9, (i & 0x03) << 3); //| 0xA); } }
其選擇了最接近想表達的顏色的32位顏色並給端口輸出。可是顏色仍是不大對勁,調色板應該不是這麼簡單的對應關係。
從新讀以前文章的介紹,發現每個圖片文件都有本身的調色板,這種調色板還不太同樣,以後使用PS
繪製系統圖標的時候深有感觸,後面再說。
如今面臨的主要問題是,咱們須要從用戶空間讀取文件後,才能取出調色板的具體內容,可是經過端口與VGA
調色板的通信在個人設計裏面是不可以經過用戶空間實現的。那麼又要進入內核。那麼這個調色板的信息如何傳給內核?動態分配的話不能經過棧來傳,內核沒有用戶的頁表,也就沒法經過地址進行訪問。
爲了可以從用戶空間讀取調色板配置文件,並在內核中修改調色板,在原來設計framebuffer
的地址上又從新設計了一塊專門用於保存調色板的區域,與以前的framebuffer
同樣,都是RW/RW
的。
計算一下佔用的空間:256 * sizeof(uint8_t) + sizeof(uint8_t)*SCRNSIZE
仍是比PTSIZE
小,不要緊,繼續用以前分配的memorylayout
,只須要定義一個結構體方便咱們來算偏移便可。
因此對於一個BMP
圖片瀏覽器,顯示圖片的整個流程是這樣的:
到這裏還有誤解,認爲BMP
的調色板可能大體一致 "而後發現幾個文件的調色基本一致,因而單獨設計了一個用於保存調色板信息的文件,用如下工具導出"。當時的記錄是這樣,naive!可是這個程序對於其後導出PS
調色板有幫助,因此也放在這裏。
void read_bmp_palette(char *file) { FILE *fp; long index; int x; /* open the file */ if ((fp = fopen(file, "rb")) == NULL) { printf("Error opening file %s.\n", file); exit(1); } uint8_t buf[1000]; bitmap_fileheader head; bitmap_infoheader info; uint16_t width, height; bitmap_image image; bitmap_infoheader *infoheader = &info; fread(&head, sizeof(bitmap_fileheader),1,fp); fread( &info, sizeof(bitmap_infoheader),1,fp); struct palette palette[256]; FILE *fout = fopen("palette.plt", "wb"); for (int i = 0; i < 256; i++) { fread(&palette[i], sizeof(struct palette), 1, fp); palette[i].rgb_red >>= 2; palette[i].rgb_green >>= 2; palette[i].rgb_blue >>= 2; fwrite(&palette[i], sizeof(struct palette), 1, fout); } fclose(fout); fclose(fp); }
好了,到這裏運氣好的話,應該能夠正常顏色繪製出來一個位圖了。(那啥讀取位圖內容顯示在屏幕上的代碼實在太簡單了,就不單獨說了)
以前之因此說運氣好,是由於恰好這個圖片信息中的高爲正的,那麼按照基本邏輯,能夠畫出來一個倒的圖片。仍是太naive,很差好看文檔中的頭文件具體參數描述,想固然的給了圖片高爲一個無符號數。
在BMP
的文件頭中,高爲一個有符號數。正表示下面的位圖像素信息是倒着來的,負表示下面的位圖像素信息是正着的……這個設計,好吧...
在Q2
中提到,想用一個調色板文件預配置後就無論其餘圖片的調色板的思路太單純了...當使用一些比較fancy
的素材進來的時候,發現其顏色根本徹底不同,失真的可怕。
爲了更加理解調色板這個設定,咱們須要一個photoshop
。設置圖片爲圖像->模式->索引模式
後,就能夠生成咱們須要的位圖了。注意這裏的設置頁面:
能夠發現系統有本身的調色板,可能用於繪製全部的圖標使用的。(固然可能也已是歷史的產物了)後面我將用相同的思路實現圖標的繪製。還有一些局部的選項,這樣就會利用一個顏色更加單一,可是轉化出來的圖片更接近32位色的圖片的調色板來生產了。
打開圖像->模式->顏色表
能夠看到當前圖片使用的調色板:
能夠看到它徹底不按照套路出牌,並無以前說的R3G3B2
的影子。
因此對於一個頁面,如何選擇調色板?個人方案是把這個頁面全部的素材丟到一個ps
文件中,並生成針對這個頁面還原度最高的調色板方案。在繪製這個頁面的時候先載入這個頁面的調色板,再進行繪製。
PS
能夠導出調色板,按照官方的文檔,也是一個簡單的二進制的堆疊,與上面的思路相似寫一個調色板轉系統plt
文件的導出便可。
好吧,個人選擇是不讀,能夠在網上找找32位
色妥協到8位
色的算法,然而實在效果很是糟糕,單獨生成調色板算法就複雜了,不如交給PS
。畢竟這不是操做系統的重點。
本部分將解釋我設計的圖形化界面數據結構,框架以及接口。
其實這部分設計的比較亂,也就只能支持單頁面切換的需求了。做爲一個技術試探是足夠了,可是擴展性不好,想繼續在這上面作文章可能須要推倒重來。
先看效果圖:
界面由標題和內容組成,界面是應用程序請求屏幕資源的基本單位。界面的數據結構以下:
struct interface { uint8_t titletype; char title[MAX_TITLE]; uint8_t title_textcolor; uint8_t title_color; uint8_t content_type; uint8_t content_color; // about the size and buff of interface uint16_t scrnx; uint16_t scrny; uint8_t *framebuffer; };
其包含了這個界面的基本信息,以及當前屏幕的各項參數,各類函數將直接向framebuffer
上操做。
void draw_interface(struct interface *interface); void draw_title(struct interface *interface); // if color == back means transparent int draw_cn(uint16_t x, uint16_t y, char *str, uint8_t color, uint8_t back, uint8_t fontmag, struct interface *interface); int draw_ascii(uint16_t x, uint16_t y, char *str, uint8_t color, uint8_t back, uint8_t fontmag, struct interface *interface); void draw_fontpixel(uint16_t x, uint16_t y, uint8_t color, uint8_t fontmag, struct interface *interface); void interface_init(uint16_t scrnx, uint16_t scrny, uint8_t *framebuffer, struct interface *interface); void add_title(char *title, uint8_t title_textcolor, uint8_t title_color, struct interface *interface); int init_palette(char *plt_filename, struct frame_info *frame); void draw_content(struct interface *interface); int draw_screen(uint16_t x, uint16_t y, struct screen *screen, uint8_t color, uint8_t back, uint8_t fontmag);
提供了以上基本操做,實現都很簡單,沒有作錯誤處理。
值得一提的是字體的設置。因爲用的點陣字庫,放大後會馬賽克。這裏使用的方法爲打包具體繪製像素方法至draw_fontpixel
,其提供了多個像素抽象爲一個字體像素進行統一繪製的方法。
本部分終於到了圖形界面的程序應用了。具體應用如何使用上面設計的接口呢?
首先看一個最簡單的例子:
#include <inc/lib.h> #define BACKGROUND 0x00 struct interface interface; void input_handler(); void display_info(); void umain(int argc, char **argv) { int r; // 初始化本界面使用的調色板 if ((r = init_palette("/bin/sysinfo.plt", frame)) < 0) printf("Open palette fail %e\n", r); // 初始化界面信息 interface_init(graph.scrnx, graph.scrny, graph.framebuffer, &interface); interface.titletype = TITLE_TYPE_TXT; strcpy(interface.title, "System information"); interface.title_color = 0x5a; interface.title_textcolor = 0xff; interface.content_type = APP_NEEDBG; interface.content_color = BACKGROUND; // 繪製界面 draw_interface(&interface); // 繪製Bitmap if ((r = draw_bitmap("/bin/sysinfo.bmp", 100, 160, &interface)) < 0) printf("Open clock back fail %e\n", r); // 顯示信息 display_info(); // 繪製結束,刷新屏幕 sys_updatescreen(); // 處理按鍵中斷 input_handler(); } void input_handler() { unsigned char ch; ch = getchar(); while (1) { switch (ch) { case KEY_ESC: exit(); } ch = getchar(); } } void display_info() { ... struct sysinfo info; // 經過系統調用獲取一些系統信息 sys_getinfo(&info); draw_ascii(display_x, display_y, "Sys : He11o_Liu's JOS version 0.1", 0xff, 0x00, fontmeg, &interface); display_y += font_height; draw_ascii(display_x, display_y, "Github : https://github.com/He11oLiu/JOS", 0xff, 0x00, fontmeg, &interface); display_y += font_height; draw_ascii(display_x, display_y, "Blog : http://blog.csdn.net/he11o_liu", 0xff, 0x00, fontmeg, &interface); ... }
一個簡單的具備圖像界面的程序由如下步驟:
啓動器算比較複雜的一個部分,專門設計了一個單獨的數據結構和繪製方法:
struct launcher_content { int app_num; int app_sel; uint8_t background; char icon[MAX_APP][MAX_PATH]; char app_bin[MAX_APP][MAX_PATH]; }; void draw_launcher(struct interface *interface, struct launcher_content *launcher);
用icon
來保存對應的app
的圖標文件路徑,用app_bin
來保存對應的程序的路徑。當選擇了對應的程序的時候spawn
這個程序,並等待其運行結束後回收進程並重繪啓動器:
void launch_app() { char *app_bin = launcher.app_bin[launcher.app_sel]; int r; char *argv[2]; argv[0] = app_bin; argv[1] = 0; printf("[launcher] Launching %s\n",app_bin); if ((r = spawn(app_bin, (const char **)argv)) < 0) { printf("App %s not found!\n",app_bin); return; } wait(r); printf("[launcher] %s normally exit\n",app_bin); init_palette("/bin/palette.plt", frame); refresh_interface(); }
因爲沒有寫系統時鐘,只提供了對於RTC的系統調用。這裏實現Fork了一個進程用於監控RTC的更新,並在適當時候更新屏幕,主進程用於監聽鍵盤,並在退出的時候摧毀子進程。
這個程序的代碼已經放在上面了,主要是設計了一個新的syscall
,用於從內核中返回一些基本系統信息。
終端程序與普通程序的設計思路徹底不一樣,本部分將根據個人思考來一步步闡述如何實現終端APP
。
做爲一個終端程序,
終端模擬器應該支持一種printf
能顯示到屏幕的功能。
printf
是向文件描述符1
進行輸出。
查看以前寫的console
代碼,openconsole
的操做就是分配一個文件描述符,設置文件操做爲(鍵盤輸入,串口輸出)的策略。
因此咱們這個終端模擬器應該提供一種新的device
,這種device
提供了(鍵盤輸入,屏幕輸出)的功能。
struct Dev devscreen = { .dev_id = 's', .dev_name = "screen", .dev_read = devscreen_read, .dev_write = devscreen_write, .dev_close = devscreen_close, .dev_stat = devscreen_stat};
直接在屏幕上顯示出來並非一個很好的選擇,參考CGA
的顯示,設計了一個屏幕字符緩衝區。
struct screen { uint8_t screen_col; uint8_t screen_row; uint16_t screen_pos; char screen_buf[SCREEN_SIZE]; };
提供新的bprintf
方法,方便screen device
調用。
做爲終端模擬器,其須要集成fork
出來的各類進程的輸出。
printf
1
號,則應該指向上面定義的screen
(這條思路最後沒走通)part1
這個部分的實現仍是比較順利的。上面已經定義了新的device
。
device
的read
策略,仍是從鍵盤讀,無須進行修改。device
的寫策略,則須要寫入到屏幕了。這裏新寫了一個bprintf
的函數與其配套方法。(bprintf
a.k.a printf to buf
)bprintf
的基本實現與以前在CGA
模式的輸出相似,因此才叫仿CGA
模式。主要是bputchar
的實現:
void bputchar(char c) { switch (c) { case '\b': /* backspace */ if (screen.screen_pos > 0) { screen.screen_pos--; // delete the character screen.screen_buf[screen.screen_pos] = ' '; } break; case '\n': /* new line */ screen.screen_pos += SCREEN_COL; /* fallthru */ case '\r': /* return to the first character of cur line */ screen.screen_pos -= (screen.screen_pos % SCREEN_COL); break; case '\t': bputchar(' '); bputchar(' '); bputchar(' '); bputchar(' '); break; default: screen.screen_buf[screen.screen_pos++] = c; /* write the character */ break; } // When current pos reach the bottom of the creen // case '\n' : screen.screen_pos -= SCREEN_COL will work // case other: screen.screen_pos must equal to SCREEN_SIZE if (screen.screen_pos >= SCREEN_SIZE) { int i; // Move all the screen upward (a line) memmove(screen.screen_buf, screen.screen_buf + SCREEN_COL, (SCREEN_SIZE - SCREEN_COL) * sizeof(uint8_t)); // Clear the bottom line for (i = SCREEN_SIZE - SCREEN_COL; i < SCREEN_SIZE; i++) screen.screen_buf[i] = ' '; screen.screen_pos -= SCREEN_COL; } screen.screen_col = SCREEN_COL; screen.screen_row = SCREEN_ROW; draw_screen(100, 80, &screen, 0x00, 0xff, 1); }
bputchar
實現了對特殊描述符,換行,翻頁的狀況的處理,並將打印的內容放入屏幕字符緩衝區。
最後要實現的就是把屏幕緩衝區的內容放倒屏幕上。這個實現起來就比較簡單了,遍歷字符串,而後一個個字從字庫中獲取顯示信息顯示出來便可。
part2
part2
纔是設計終端中須要動腦子的地方。正如思路中所說,我一開始的想法是:
父進程中的文件描述符1
號,則應該指向上面定義的screen
然而沒有考慮這個問題:
interface
與screen
參數均屬於與之平等的另外一個用戶程序!在調用bprintf
的時候,沒有初始化screen
,也不知道interface
在哪裏。
之因此CGA
模式可使用這個思路是由於CGA
的文字緩衝區是在內核中,能夠看爲這項服務是內核提供的,是一個上下級的關係,而不是平行的
若是必需要走這條路,有如下解決方法:
screen
與interface
,能夠作到直接新建一個輸出頁的效果。Pipe
!!!!!!!!老思路中的第一條解決方法走通了後又思考了一下子,實在不想走第二第三條路。
換個思路一想,原來這個事能夠這麼簡單。申請一個pipe
,讀取端給 (輸出到屏幕的 )服務進程做爲輸入來源,輸出端給用戶程序做爲輸出。程序輸出的內容會經過pipe
發送給服務進程,最終服務進程顯示到屏幕上便可。
整個程序的流程以下:
1
(默認輸出)pipe
fork
子進程
pipe
,保留寫的pipe
,並將寫的pipe
給默認輸出1
,後面的程序輸出都會寫進pipe
中。子進程開始運行shell
。pipe
,保留讀的pipe
,並將讀的pipe
給默認輸入0
,後面程序的輸入都會讀pipe
中的內容。父進程進入循環,服務全部的輸入輸出到屏幕的功能。來看核心代碼:
void umain(int argc, char **argv) { ... close(1); // 打開屏幕CGA輸出到文件描述符1 if ((r = openscreen(&interface)) < 0) panic("fd = %e\n", r); cprintf("fd = %d\n", r); // 申請一個pipe if ((r = pipe(p)) < 0) { cprintf("pipe: %e", r); exit(); } readfd = p[0]; writefd = p[1]; r = fork(); if (r == 0) { close(readfd); close(1); // 寫入端給子進程做爲其輸出默認文件描述符1 dup(writefd, 1); // 運行shell (修改過,沒有文件描述符操做版本) msh(argc, argv); printf("msh exit\n"); exit(); } else { close(writefd); close(0); // 讀入端做爲其默認讀取文件描述符0 if (readfd != 0) dup(readfd, 0); close(readfd); } e = &envs[ENVX(r)]; while(1) { // 獲取全部的pipe中的數據並顯示在模擬CGA屏幕上 r = getchar(); printf("%c", r); // 當shell退出的時候退出 if(e->env_status == ENV_FREE) break; } }
這個部分將記錄這幾天寫圖形界面的收穫。
以前看知乎上的大佬們的論調:圖形界面和操做系統有啥關係?沒啥好寫的,簡單!其實寫寫簡單的圖形界面一個是轉換一下思路,有簡單可見的產出激勵,另外一個是進一步理解體會操做系統的設計,並實際修改一些JOS
中的設計,並實現一些相似以前照着任務指南寫出來的功能。In another words, get your hands dirty.
按照JOS的思路,仍是但願保持一個相對小的內核,提供最基本的服務,剩下的交給用戶空間玩耍。可是到實際的問題上,包括了
功能的劃分,用戶須要的服務可否在用戶態實現大部分,內核實現小部分(如上次的用戶空間的頁錯誤處理與此次的Framebuffer
與屏幕更新)。這樣的設計更加flexable
,而且保證了呆在內核中的時間很是短暫(畢竟還用着big kernel lock
…)
內核與用戶空間的信息交換。棧或者固定地址的交互。
棧比較靈活,能夠在每次的系統調用的時候直接壓進去,交換完了後再取回來。可是隻能傳值,傳的內容比較少。
固定地址則使得系統變得不那麼靈活,不利於擴展與移植。可是能夠高效的大量數據交換。在寫這塊的時候實現了kmalloc
,並理解了以前mem_init
時作的各類映射的意義。
理想的用戶與用戶的關係是平齊的(不提供服務的用戶),在寫用戶程序的時候不知道其父進程是誰,也不會要求子進程知道本身的存在。可是跨進程之間的服務需求仍然存在,如對於一個進程輸出的獲取,或圖形界面中的界面重疊。這就須要一個服務提供者的存在,來抽象用戶之間的需求。好比以前設計的文件系統服務器,好比這回原本準備實現的圖形界面服務器。
這部分感觸最深的是unix
中的文件描述符的設計,簡直太妙。將全部的內容所有抽象成文件,就能夠靈活的在不一樣的需求以前切換。最簡單的讀寫文件,讀寫串口,讀寫屏幕,pipe均使用的這種抽象。
這種抽象將抽象層和具體實現層分開,下降耦合度的同時提供了很是高的靈活性。