[自制操做系統] BMP格式文件讀取&圖形界面系統框架/應用接口設計

Github : https://github.com/He11oLiu/JOShtml

本文將介紹在本人JOS中實現的簡單圖形界面應用程序接口,應用程序啓動器,以及一些利用了圖形界面的示例應用程序。java

本文主要涉及如下部分:git

  • 內核/用戶RW/RW調色板framebuffer共享區域
  • 8bit顏色深度BMP格式圖片讀取與繪製
    • BMP頭老是出現問題?不合理的數據?
    • 爲啥讀出來的圖片顏色怪怪的!!
    • 爲啥是倒的,還有的運氣很差出錯了
    • 若是是想繪製多個圖片在一頁上,調色板問題??
    • 若是讀到一個32位色的圖片咋辦?
  • 圖形化界面數據結構,框架以及接口設計
  • 利用圖形化接口實現應用程序:
    • 日曆程序(實時時鐘刷新)
    • 系統信息獲取
    • 終端CGA模擬器

PART1

framebuffer

圖形庫中,已經將圖形模式打開,將顯存映射到內存中的一段空間。並進行了簡單的測試。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);
}

通過實現kmallockfree,已經能夠分配這個緩衝區,並直接向緩衝區寫入,最後再進行updateshell

#define PIXEL(x, y) *(framebuffer + x + (y * graph.scrnx))
int draw_xx()
{
    xxx;
    update_screen();
}

canvas (這種思路已經廢棄)

從一個單一的應用程序角度來看,應分配一個單獨的畫布,而後選擇在一個位置顯示。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(還沒寫)

  • 第二種設計是一個launcherapplication兩個單獨的單頁面切換制度。

    這樣就是launcher提供應用啓動界面,application提供應用界面。

從新回顧了一下內存分配,內核與用戶態數據共享的方法後,決定先就第二個思路實現一個簡單的用戶內核都可見可讀寫的Framebuffer

實現RW/RWFramebuffer

分析如何作才能內核用戶都可讀寫

首先分析一個以前作過的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,咱們須要放到哪裏。仿造上面的UVPDUPAGES,等,決定就放在接近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;
}

配套的一些代碼就不解釋了。

PART2

上一個部分已經將一個用戶與內核都可讀寫的緩衝區域,並提供了一個系統調用,用於將顯示緩存內容拷貝至MMIO顯存。從理論上來講,用戶空間的程序如今已經能夠直接在這塊Framebuffer上繪製任何圖形。

可是對於一個友好的用戶界面,至少要支持一種格式的圖片顯示。這裏選擇一種最簡單的,沒有壓縮過的位圖顯示實現。推薦各位想本身寫圖形界面的小夥伴也從這裏入手。

關於BMP的讀取能夠參考這篇文章256-Color VGA Programming in C Bitmaps & Palette Manipulation。要注意詳細讀其中的每個細節,直接掃一眼看代碼寫的話會遇到不少問題,下面會提到我遇到的問題與解決方案。

Bitmap圖片顯示

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格式的圖片讀取。

Q1:讀頭老是出現問題?不合理的數據?

這裏要注意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))關鍵字用於告訴編譯器,最小單位進行對齊,而不使用默認的四單位進行對齊。

Q2:爲啥讀出來的圖片顏色怪怪的!!

最開始設置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文件頭
  • 用戶讀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);
}

好了,到這裏運氣好的話,應該能夠正常顏色繪製出來一個位圖了。(那啥讀取位圖內容顯示在屏幕上的代碼實在太簡單了,就不單獨說了)

Q3:爲啥是倒的,還有的運氣很差出錯了

以前之因此說運氣好,是由於恰好這個圖片信息中的高爲正的,那麼按照基本邏輯,能夠畫出來一個倒的圖片。仍是太naive,很差好看文檔中的頭文件具體參數描述,想固然的給了圖片高爲一個無符號數。

BMP的文件頭中,高爲一個有符號數。正表示下面的位圖像素信息是倒着來的,負表示下面的位圖像素信息是正着的……這個設計,好吧...

Q4:若是是想繪製多個圖片在一頁上,調色板問題??

Q2中提到,想用一個調色板文件預配置後就無論其餘圖片的調色板的思路太單純了...當使用一些比較fancy的素材進來的時候,發現其顏色根本徹底不同,失真的可怕。

爲了更加理解調色板這個設定,咱們須要一個photoshop。設置圖片爲圖像->模式->索引模式後,就能夠生成咱們須要的位圖了。注意這裏的設置頁面:

能夠發現系統有本身的調色板,可能用於繪製全部的圖標使用的。(固然可能也已是歷史的產物了)後面我將用相同的思路實現圖標的繪製。還有一些局部的選項,這樣就會利用一個顏色更加單一,可是轉化出來的圖片更接近32位色的圖片的調色板來生產了。

打開圖像->模式->顏色表能夠看到當前圖片使用的調色板:

能夠看到它徹底不按照套路出牌,並無以前說的R3G3B2的影子。

因此對於一個頁面,如何選擇調色板?個人方案是把這個頁面全部的素材丟到一個ps文件中,並生成針對這個頁面還原度最高的調色板方案。在繪製這個頁面的時候先載入這個頁面的調色板,再進行繪製。

PS能夠導出調色板,按照官方的文檔,也是一個簡單的二進制的堆疊,與上面的思路相似寫一個調色板轉系統plt文件的導出便可。

Q5:若是讀到一個32位色的圖片咋辦?

好吧,個人選擇是不讀,能夠在網上找找32位色妥協到8位色的算法,然而實在效果很是糟糕,單獨生成調色板算法就複雜了,不如交給PS。畢竟這不是操做系統的重點。

Part3

本部分將解釋我設計的圖形化界面數據結構,框架以及接口。

其實這部分設計的比較亂,也就只能支持單頁面切換的需求了。做爲一個技術試探是足夠了,可是擴展性不好,想繼續在這上面作文章可能須要推倒重來。

先看效果圖:

界面由標題和內容組成,界面是應用程序請求屏幕資源的基本單位。界面的數據結構以下:

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,其提供了多個像素抽象爲一個字體像素進行統一繪製的方法。

Part 4

本部分終於到了圖形界面的程序應用了。具體應用如何使用上面設計的接口呢?

首先看一個最簡單的例子:

#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,用於從內核中返回一些基本系統信息。

Part4 模擬CGA顯示模式終端應用

終端程序與普通程序的設計思路徹底不一樣,本部分將根據個人思考來一步步闡述如何實現終端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

  • 這個新的deviceread策略,仍是從鍵盤讀,無須進行修改。
  • 而這個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

然而沒有考慮這個問題:

interfacescreen參數均屬於與之平等的另外一個用戶程序!在調用bprintf的時候,沒有初始化screen,也不知道interface在哪裏。

之因此CGA模式可使用這個思路是由於CGA的文字緩衝區是在內核中,能夠看爲這項服務是內核提供的,是一個上下級的關係,而不是平行的

若是必需要走這條路,有如下解決方法:

  • 在庫中判斷沒有參數時,直接初始化screeninterface,能夠作到直接新建一個輸出頁的效果。
  • 提高終端到內核中運行(沒法忍受)
  • 使終端成爲如同文件系統的非普通程序,接收輸出請求。這個也很複雜,不夠優雅!!!

新思路 : 利用 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;
    }
}

Part 收穫

這個部分將記錄這幾天寫圖形界面的收穫。

以前看知乎上的大佬們的論調:圖形界面和操做系統有啥關係?沒啥好寫的,簡單!其實寫寫簡單的圖形界面一個是轉換一下思路,有簡單可見的產出激勵,另外一個是進一步理解體會操做系統的設計,並實際修改一些JOS中的設計,並實現一些相似以前照着任務指南寫出來的功能。In another words, get your hands dirty.

用戶與內核的關係

按照JOS的思路,仍是但願保持一個相對小的內核,提供最基本的服務,剩下的交給用戶空間玩耍。可是到實際的問題上,包括了

  • 功能的劃分,用戶須要的服務可否在用戶態實現大部分,內核實現小部分(如上次的用戶空間的頁錯誤處理與此次的Framebuffer與屏幕更新)。這樣的設計更加flexable,而且保證了呆在內核中的時間很是短暫(畢竟還用着big kernel lock…)

  • 內核與用戶空間的信息交換。棧或者固定地址的交互。

    棧比較靈活,能夠在每次的系統調用的時候直接壓進去,交換完了後再取回來。可是隻能傳值,傳的內容比較少。

    固定地址則使得系統變得不那麼靈活,不利於擴展與移植。可是能夠高效的大量數據交換。在寫這塊的時候實現了kmalloc,並理解了以前mem_init時作的各類映射的意義。

用戶與用戶的關係(用戶空間程序)

理想的用戶與用戶的關係是平齊的(不提供服務的用戶),在寫用戶程序的時候不知道其父進程是誰,也不會要求子進程知道本身的存在。可是跨進程之間的服務需求仍然存在,如對於一個進程輸出的獲取,或圖形界面中的界面重疊。這就須要一個服務提供者的存在,來抽象用戶之間的需求。好比以前設計的文件系統服務器,好比這回原本準備實現的圖形界面服務器。

文件描述符 | 句柄 | FD

這部分感觸最深的是unix中的文件描述符的設計,簡直太妙。將全部的內容所有抽象成文件,就能夠靈活的在不一樣的需求以前切換。最簡單的讀寫文件,讀寫串口,讀寫屏幕,pipe均使用的這種抽象。

這種抽象將抽象層和具體實現層分開,下降耦合度的同時提供了很是高的靈活性。

相關文章
相關標籤/搜索