192 lines
8.2 KiB
ReStructuredText
192 lines
8.2 KiB
ReStructuredText
shell与测例的加载
|
||
===================================
|
||
|
||
本节导读
|
||
-----------------------------------
|
||
|
||
本节将会展示新的bin_loader加载测例到进程的方式,并且展示我们的shell测例是如何运行的。
|
||
|
||
新的bin_loader
|
||
------------------------------------------------------------------------
|
||
|
||
exec会调用bin_loader,将对应文件名的测例加载到指定的进程p之中。请结合注释理解 bin_loader 的变化:
|
||
|
||
.. code-block:: c
|
||
:linenos:
|
||
|
||
int bin_loader(uint64 start, uint64 end, struct proc *p)
|
||
{
|
||
void *page;
|
||
// 注意现在我们不要求对其了,代码的核心逻辑还是把 [start, end)
|
||
// 映射到虚拟内存的 [BASE_ADDRESS, BASE_ADDRESS + length)
|
||
uint64 pa_start = PGROUNDDOWN(start);
|
||
uint64 pa_end = PGROUNDUP(end);
|
||
uint64 length = pa_end - pa_start;
|
||
uint64 va_start = BASE_ADDRESS;
|
||
uint64 va_end = BASE_ADDRESS + length;
|
||
// 不再一次 map 很多页面,而是逐页 map,为什么?
|
||
for (uint64 va = va_start, pa = pa_start; pa < pa_end;
|
||
va += PGSIZE, pa += PGSIZE) {
|
||
// 这里我们不会直接映射,而是新分配一个页面,然后使用 memmove 进行拷贝
|
||
// 这样就不会有对其的问题了,但为何这么做其实有更深层的原因。
|
||
page = kalloc();
|
||
memmove(page, (const void *)pa, PGSIZE);
|
||
// 这个 if 就是为了防止 start end 不对其导致拷贝了多余的内核数据
|
||
// 我们需要手动把它们清空
|
||
if (pa < start) {
|
||
memset(page, 0, start - va);
|
||
} else if (pa + PAGE_SIZE > end) {
|
||
memset(page + (end - pa), 0, PAGE_SIZE - (end - pa));
|
||
}
|
||
mappages(p->pagetable, va, PGSIZE, (uint64)page, PTE_U | PTE_R | PTE_W | PTE_X);
|
||
}
|
||
// 同 lab4 map user stack
|
||
p->ustack = va_end + PAGE_SIZE;
|
||
for (uint64 va = p->ustack; va < p->ustack + USTACK_SIZE;
|
||
va += PGSIZE) {
|
||
page = kalloc();
|
||
memset(page, 0, PGSIZE);
|
||
mappages(p->pagetable, va, PGSIZE, (uint64)page, PTE_U | PTE_R | PTE_W);
|
||
}
|
||
// 设置 trapframe
|
||
p->trapframe->sp = p->ustack + USTACK_SIZE;
|
||
p->trapframe->epc = va_start;
|
||
p->max_page = PGROUNDUP(p->ustack + USTACK_SIZE - 1) / PAGE_SIZE;
|
||
p->state = RUNNABLE;
|
||
return 0;
|
||
}
|
||
|
||
其中,对于用户栈、trapframe、trampoline 的映射没有变化,但是对 .bin 数据的映射似乎面目全非了,竟然由一个循环完成。其实,这个循环的逻辑十分简单,就是对于 .bin 的每一页,都申请一个新页并进行内容拷贝,最后建立这一页的映射。之所以这么麻烦完全是由于我们的物理内存管理过于简陋,一次只能分配一个页,如果能够分配连续的物理页,那么这个循环可以被一个 mappages 替代。
|
||
|
||
那么另一个更重要的问题是,为什么要拷贝呢?想想 lab4 我们是怎么干的,直接把虚存和物理内存映射就好了,根本没有拷贝。那么,拷贝是为了什么呢?其实,按照 lab4 的做法,程序运行之后就会修改仅有一份的程序"原像",你会发现,lab4 的程序都是一次性的,如果第二次执行,会发现 .data 和 .bss 段数据都被上一次执行改掉了,不是初始化的状态。但是 lab4 的时候,每个程序最多执行一次,所以这么做是可以的。但在 lab5 所有程序都可能被无数次的执行,我们就必须对“程序原像”做保护,在“原像”的拷贝上运行程序了。
|
||
|
||
测例的执行
|
||
------------------------------------------------------------------------
|
||
|
||
从本章开始,大家可以发现我们的 run_all_app 函数被 load_init_app 取代了:
|
||
|
||
.. code-block:: c
|
||
:linenos:
|
||
|
||
// os/loader.c
|
||
|
||
// load all apps and init the corresponding `proc` structure.
|
||
int load_init_app()
|
||
{
|
||
int id = get_id_by_name(INIT_PROC);
|
||
if (id < 0)
|
||
panic("Cannpt find INIT_PROC %s", INIT_PROC);
|
||
struct proc *p = allocproc();
|
||
if (p == NULL) {
|
||
panic("allocproc\n");
|
||
}
|
||
debugf("load init proc %s", INIT_PROC);
|
||
loader(id, p);
|
||
return 0;
|
||
}
|
||
|
||
这个 load_init_app load 的 INIT_PROC 一般来说就是我们在本章第一节展示的那个 usershell,不过可以通过在 Makefile 中传入 INIT_PROC 参数而改变,大部分情况下,不推荐修改,这是由于 usershell 具有不错的灵活性。
|
||
|
||
|
||
usershell
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
``user/src/usershell.c`` 就是 usershell 的代码了,有兴趣的同学可以研究下这个 shell:
|
||
|
||
.. code-block:: c
|
||
|
||
const unsigned char LF = 0x0a;
|
||
const unsigned char CR = 0x0d;
|
||
const unsigned char DL = 0x7f;
|
||
const unsigned char BS = 0x08;
|
||
|
||
// 手搓了一个极简的 stack,用来维护用户输入,保存一行的输入
|
||
char line[100] = {};
|
||
int top = 0;
|
||
void push(char c){ line[top++] = c; }
|
||
void pop() { --top; }
|
||
int is_empty() { return top == 0;}
|
||
void clear() { top = 0; }
|
||
|
||
int main()
|
||
{
|
||
printf("C user shell\n");
|
||
printf(">> ");
|
||
fflush(stdout);
|
||
while (1) {
|
||
char c = getchar();
|
||
switch (c) {
|
||
// 回车,执行当前 stack 中字符串对应的程序
|
||
case LF:
|
||
case CR:
|
||
printf("\n");
|
||
if (!is_empty()) {
|
||
push('\0');
|
||
int pid = fork();
|
||
if (pid == 0) {
|
||
// child process
|
||
if (exec(line, NULL) < 0) {
|
||
printf("no such program: %s\n",
|
||
line);
|
||
exit(0);
|
||
}
|
||
panic("unreachable!");
|
||
} else {
|
||
int xstate = 0;
|
||
int exit_pid = 0;
|
||
exit_pid = waitpid(pid, &xstate);
|
||
assert(pid == exit_pid);
|
||
printf("Shell: Process %d exited with code %d\n",
|
||
pid, xstate);
|
||
}
|
||
clear();
|
||
}
|
||
printf(">> ");
|
||
fflush(stdout);
|
||
break;
|
||
// 退格建,pop一个char
|
||
case BS:
|
||
case DL:
|
||
if (!is_empty()) {
|
||
putchar(BS);
|
||
printf(" ");
|
||
putchar(BS);
|
||
fflush(stdout);
|
||
pop();
|
||
}
|
||
break;
|
||
// 普通输入,回显并 push 一个 char
|
||
default:
|
||
putchar(c);
|
||
fflush(stdout);
|
||
push(c);
|
||
break;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
|
||
可以看到这个测例实际上就是实现了一个简单的字符串处理的函数,并且针对解析得到的不同的指令调用不同的系统调用。要注意这需要shell支持read的系统调用。当读入用户的输入时,它会死循环的等待用户输入一个代表程序名称的字符串(通过sys_read),当用户按下空格之后,shell 会使用 fork 和 exec 创建并执行这个程序,然后通过 sys_wait 来等待程序执行结束,并输出 exit_code。有了 shell 之后,我们可以只执行自己希望的程序,也可以执行某一个程序很多次来观察输出,这对于使用体验是极大的提升!可以说,第五章的所有努力都是为了支持 shell。
|
||
|
||
我们简单看一下sys_read的实现,它与 sys_write 有点相似:
|
||
|
||
.. code-block:: c
|
||
|
||
uint64 sys_read(int fd, uint64 va, uint64 len)
|
||
{
|
||
if (fd != STDIN)
|
||
return -1;
|
||
struct proc *p = curr_proc();
|
||
char str[MAX_STR_LEN];
|
||
len = MIN(len, MAX_STR_LEN);
|
||
for (int i = 0; i < len; ++i) {
|
||
// consgetc() 会阻塞式的等待读取一个 char
|
||
int c = consgetc();
|
||
str[i] = c;
|
||
}
|
||
copyout(p->pagetable, va, str, len);
|
||
return len;
|
||
}
|
||
|
||
目前我们只支持标准输入stdin的输入(对应fd = STDIN)。 |