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均使用的这种抽象。
这种抽象将抽象层和具体实现层分开,下降耦合度的同时提供了很是高的灵活性。