《操作系统-真象还原》15. 系统交互
创始人
2024-05-08 12:58:55
0

文章目录

    • fork 的原理与实现
      • 简介
      • 什么是 fork
      • fork 的实现
        • 思路
        • 代码
          • get_a_page_without_opvaddrbitmap
          • copy_pcb_vaddrbitmap_stack0
          • copy_body_stack3
          • build_child_stack
          • update_inode_open_cnts
          • copy_process
          • sys_fork
      • 添加 fork 系统调用与实现 init 进程
        • 添加 fork 系统调用
        • 实现 init 进程
    • shell 的前置准备工作
    • 实现一个简单的 shell
      • readline / print_prompt 函数
      • 添加 Ctrl+u 和 Ctrl+l 快捷键
      • cmd_str 分割输入的指令,相对于高级语言中字符串的 split 函数。
      • 解析路径
      • 实现 ls、c、mkdir、ps、rm 等指令
      • shell 主程序
    • 现在 Shell 的问题

fork 的原理与实现

简介

pid_t fork( void);

pid_t 是一个宏定义,其实质是int 被定义在 #includesys/types.h> 中。

返回值: 若成功调用一次则返回两个值,其返回值的意义如下

  • 0:子进程

  • 子进程ID,即 >0:父进程

  • -1:错误

fork 的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。(相关函数请看 fork 的实现 -> 思路

什么是 fork

#include 
#include int main() {printf("I will fork in 5 seconds.\n");sleep(5);int pid = fork();if(pid == -1) {printf("fork error!\n");return 1;}if(pid) {printf("[fork->pid=%d] I am father, my pid is %d\n.", pid, getpid());sleep(5);return 0;} else {printf("[fork->pid=%d] I am child, my pid is %d\n.", pid, getpid());sleep(5);return 0;}
}

需要打开两个终端,一个用于运行程序,一个用于查看进程信息。
终端一:运行测试程序

image-20230107152613896

终端二:查看进程信息

image-20230107152648429

一开始并无相关进程信息,接下来执行测试程序,首先执行父进程,得到 pid=8917,休眠 5s 后执行 fork() 函数,之后会发现输出了两条语句,也就是说 fork 返回一次返回了两个不同的返回值,为什么呢?

看进程信息,fork 后多了一个“子进程”,其 pid=8932,也就是说 fork 其实是将父进程克隆了一份,而进程拥有独立的地址空间,因此两个进程执行的是独立且相同的代码,所以它们并不共享同一内存空间,也就是执行的是两套代码(代码是相同的,只是有两套而已)。

看输出的结果,是先执行的父进程,其 fork 返回父进程的 pid,其次执行的才是子进程,其子进程 fork 返回的是 0。

注意,这里并不是父子进程各调用一次,父子进程合计才调用了一次 fork,也就是说执行一次 fork 会返回两个值,只是返回的地方不一样,一次是在父进程,一次是在父进程

fork 的实现

思路

前面说明了 fork 本质是将父进程克隆了一份,称为子进程。首先我们需要明确要复制的资源,复制完成后让处理器的 cs:eip 指向新进程的指令部分。
实现 fork 分两步:

  1. 先复制进程资源。
  2. 让处理器的 cs:eip 指向子进程的指令部分(也就是程序代码部分)。

明确要复制的资源有:

  1. 进程的 PCB,即 task_struct。相关函数:copy_pcb_vaddrbitmap_stack0
  2. 程序体,即代码段、数据段等,这是进程的实体。相关函数:copy_body_stack3
  3. 用户栈,编译器会把局部变量存入栈,而调用函数也需要压栈执行。相关函数:copy_body_stack3
  4. 内核栈,进入内核态时,一方面要用它来保护上下文环境,而另一方面和用户栈一样。相关函数:copy_pcb_vaddrbitmap_stack0
  5. 虚拟地址池,每个进程都拥有独立的内存空间,其虚拟地址是虚拟地址池来管理的。相关函数:copy_pcb_vaddrbitmap_stack0
  6. 页表,让进程拥有独立的内存空间。相关函数:create_page_dir

克隆后的进程要如何执行:将新进程加入到就绪队列就可以了,当然要提前把相关的栈准备好。相关函数:build_child_stack

注意:复制完 PCB 后,要记得文件描述符所对应打开的文件的次数要增加。相关函数:update_inode_open_cnts

代码

get_a_page_without_opvaddrbitmap

kernel/memory.c

// 安装 1页 大小的 vaddr,专门针对 fork 时虚拟地址位图无需操作的情况
void* get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr) {struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;lock_acquire(&mem_pool -> lock);void* page_phyaddr = palloc(mem_pool);if(page_phyaddr == NULL) {lock_release(&mem_pool -> lock);return NULL;}page_table_add((void*) vaddr, page_phyaddr);lock_release(&mem_pool -> lock);return (void*) vaddr;
}
copy_pcb_vaddrbitmap_stack0

userprog/fork.c

// 将父进程的 PCB、虚拟地址位图拷贝给子进程
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct* child_thread, struct task_struct* parent_thread) {// 复制 PCB 所在的整个页,里面包含了进程 PCB 信息以及特权级0的栈memcpy(child_thread, parent_thread, PG_SIZE);child_thread -> pid = fork_pid();child_thread -> elapsed_ticks = 0;child_thread -> status = TASK_READY;child_thread -> ticks = child_thread -> priority; // 重置时间片,将其填满child_thread -> parent_pid = parent_thread -> pid;child_thread -> general_tag.prev = child_thread -> general_tag.next = NULL;child_thread -> all_list_tag.prev = child_thread -> all_list_tag.next = NULL;block_desc_init(child_thread -> u_block_desc);// 复制父进程的虚拟内存池位图uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);void* vaddr_btmp = get_kernel_pages(bitmap_pg_cnt);if(vaddr_btmp == NULL) return -1;// 将父进程的虚拟内存池位图复制一份给子进程, child_thread 其实也可以换成 parent_threadmemcpy(vaddr_btmp, child_thread -> userprog_vaddr.vaddr_bitmap.bits, bitmap_pg_cnt * PG_SIZE);child_thread -> userprog_vaddr.vaddr_bitmap.bits = vaddr_btmp;ASSERT(strlen(child_thread -> name) < 11); // pcb.name 长度为 16,为避免下面 strcat 越界strcat(child_thread -> name, "_fork");return 0;
}
copy_body_stack3
// 复制子进程的进程体(代码和数据)以及用户栈
static void copy_body_stack3(struct task_struct* child_thread, struct task_struct* parent_thread, void* buf_page) {uint8_t* vaddr_btmp = parent_thread -> userprog_vaddr.vaddr_bitmap.bits;uint32_t btmp_byte_len = parent_thread -> userprog_vaddr.vaddr_bitmap.btmp_bytes_len;uint32_t vaddr_start = parent_thread -> userprog_vaddr.vaddr_start;uint32_t idx_byte = 0;uint32_t idx_bit = 0;uint32_t prog_vaddr = 0;// 在父进程的用户空间中查找已有数据的页while(idx_byte < btmp_byte_len) {if(vaddr_btmp[idx_byte]) { // 逐个字节判断idx_bit = 0;while(idx_bit < 8) { // 逐个位判断if((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte]) {// 计算虚拟地址prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start;// 将父进程所在用户空间中的数据复制到内核缓冲区 buf_page// 目的是下面切换到子进程的页表后,还能访问到父进程的数据memcpy(buf_page, (void*) prog_vaddr, PG_SIZE);// 将页表切换到子进程,目的是避免下面申请内存的函数将 pte 及 pde 安装到父进程的页表中page_dir_activate(child_thread);// 申请虚拟地址 prog_vaddrget_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);// 从内核缓冲区中将父进程数据复制到子进程的用户空间中memcpy((void*) prog_vaddr, buf_page, PG_SIZE);// 恢复父进程页表page_dir_activate(parent_thread);}idx_bit++;}}idx_byte++;}
}

将父进程用户空间中的数据复制到子进程的用户空间。但各用户进程的低3G空间是独立的,因此用户进程不能互相访问彼此的空间,但高1G是内核空间,内核空间是所有用户共享的,因此要把数据从一个进程拷贝到另一个进程,必须要借助内核空间作为数据中转,即先父进程用户空间的数据先复制到位于内核空间的 buf_page 中,最后再将 buf_page 复制到子进程的用户空间中。

这里采用一页一页对拷的形式,即父进程找到一页,子进程就申请一页空间,然后对拷。但不同进程之所以有单独的虚拟地址空间,是因为它们各自有单独的页目录,我们分配内存时,会在页表中产生新的 PTE,若申请的内存跨越 4MB 的页表大小,则还需要在页目录表中创建 PDE,既然是为子进程分配内存,那么就要保证这些 PTE 和 PDE 是创建在子进程的页目录表中的。所以在将 buf_page 的数据拷贝到子进程之前,一定要将页表替换为子进程的页表。

build_child_stack
// 为子进程构建 thread_stack 和修改返回值
static int32_t build_child_stack(struct task_struct* child_thread) {// -----------------------// 使子进程 pid 返回 0// -----------------------// 获取子进程0级栈栈顶struct intr_stack* intr_0_stack = (struct intr_stack*)((uint32_t) child_thread + PG_SIZE - sizeof(struct intr_stack));// 修改子进程的返回值为 0intr_0_stack -> eax = 0;// 为 switch_to 构建 struct thread_stack,构建在 intr_stack 之下的空间uint32_t* ret_addr_in_thread_stack  = (uint32_t*)intr_0_stack - 1;/***   这三行不是必要的,只是为了梳理thread_stack中的关系 ***/uint32_t* esi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 2; uint32_t* edi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 3; uint32_t* ebx_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 4; /**********************************************************//* ebp在thread_stack中的地址便是当时的esp(0级栈的栈顶),即esp为"(uint32_t*)intr_0_stack - 5" */uint32_t* ebp_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 5; // 更新内存中的数据// switch_to 的返回地址更新为 intr_exit,直接从中断返回*ret_addr_in_thread_stack = (uint32_t) intr_exit;/* 下面这两行赋值只是为了使构建的thread_stack更加清晰,其实也不需要,* 因为在进入intr_exit后一系列的pop会把寄存器中的数据覆盖 */*ebp_ptr_in_thread_stack = *ebx_ptr_in_thread_stack =\*edi_ptr_in_thread_stack = *esi_ptr_in_thread_stack = 0;/*********************************************************/// 把构建的 thread_stack 的栈顶作为 switch_to 恢复数据时的栈顶child_thread -> self_kstack = ebp_ptr_in_thread_stack;return 0;
}
update_inode_open_cnts
// 更新 inode 打开数,其实就是更新文件被打开的次数
static void update_inode_open_cnts(struct task_struct* thread) {int32_t local_fd = 3, global_fd = 0;while(local_fd < MAX_FILES_OPEN_PER_PROC) {global_fd = thread -> fd_table[local_fd];ASSERT(global_fd < MAX_FILE_OPEN);if(global_fd != -1)file_table[global_fd].fd_inode->i_open_cnt++;local_fd++;}
}
copy_process
// 拷贝父进程本身所占资源给子进程(对前面函数的封装罢了)
static int32_t copy_process(struct task_struct* child_thread, struct task_struct* parent_thread) {// 内核缓冲区,作为父进程用户空间的数据复制到子进程用户空间的中转void* buf_page = get_kernel_pages(1);if(buf_page == NULL) return -1;// 复制父进程的 PCB、虚拟地址位图、内核栈到子进程if(copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) return -1;// 为子进程创建页表,此页表仅包含内核空间child_thread -> pgdir = create_page_dir();if(child_thread -> pgdir == NULL) return -1;// 复制父进程的进程体及用户栈给子进程copy_body_stack3(child_thread, parent_thread, buf_page);// 构建子进程的 thread_stack 和修改返回值 pidbuild_child_stack(child_thread);// 更新文件(inode)被打开的次数update_inode_open_cnts(child_thread);mfree_page(PF_KERNEL, buf_page, 1);return 0;
}
sys_fork
// fork 子进程,内核线程不可直接调用
pid_t sys_fork(void) {struct task_struct* parent_thread = running_thread();struct task_struct* child_thread = get_kernel_pages(1);if(child_thread == NULL) return -1;ASSERT(INTR_OFF == intr_get_status() && parent_thread -> pgdir != NULL);if(copy_process(child_thread, parent_thread) == -1) return -1;// 添加到就绪队列和所有线程队列,子进程由调度器安排运行ASSERT(!elem_find(&thread_ready_list, &child_thread->general_tag));list_append(&thread_ready_list, &child_thread -> general_tag);ASSERT(!elem_find(&thread_all_list, &child_thread->all_list_tag));list_append(&thread_all_list, &child_thread -> all_list_tag);return child_thread -> pid; // 父进程返回子进程的 pid
}

添加 fork 系统调用与实现 init 进程

添加 fork 系统调用

这个没啥好说,老套路走一遍就行。

实现 init 进程

在 Linux 中,init 是用户级进程,它是第一个启动的程序,因此它的 pid=1,后续的所有进程都是它的孩子,故 init 是所有进程父进程,所以它还负责所有子进程的资源回收。

既然 init 是父进程,也就是说它要主动调用 fork 才能派生出子子孙孙,所以在实现它之前要先实现 fork 系统调用。

kernel/main.c

void init(void);int main(void) {put_str("I am kernel.\n");init_all();while(1);return 0;
}// init 进程
void init(void) {uint32_t ret_pid = fork();if(ret_pid)printf("i am father, my pid is %d, child pid is %d\n", getpid(), ret_pid);elseprintf("i am child, my pid is %d, ret pid is %d\n", getpid(), ret_pid);while(1);
}

thread/thread.c

extern void init(void);
...
// 初始化线程环境
void thread_init(void) {put_str("thread_init start\n");list_init(&thread_ready_list);list_init(&thread_all_list);lock_init(&pid_lock);// 线创建第一个用户进程 initprocess_execute(init, "init"); // 由于是第一个创建的进程,因此该进程 pid = 1// 将当前 main 函数创建为线程make_main_thread();// 创建 idle 线程idle_thread = thread_start("idle", 10, idle, NULL);put_str("thead_init done\n");
}

shell 的前置准备工作

三个新的j基础系统调用:

  • read 系统调用,获取键盘输入。
  • putchar 系统调用,输出字符。
  • clear 系统调用,清空屏幕。

其中 clear 的内核实现是 cls_screen,采用纯汇编实现,代码位于:kernel/print.S

这里就贴出 read 的内核实现函数:

fs/fs.c

// 从文件描述符 fd 指向的文件中读取 count 个字节到 buf,成功返回读取的字节数,到文末返回-1
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {ASSERT(buf != NULL);int32_t ret = -1;if(fd < 0 || fd == stdout_no || fd == stderr_no) { // 标准输出流printk("sys_read: fd error.\n");} else if(fd == stdin_no) { // 标准输入流char* buffer = buf;uint32_t bytes_read = 0;while(bytes_read < count) { // 不断的从输入缓冲区中读取数据*buffer = ioq_getchar(&kdb_buf); // 从键盘缓冲区 kdb_buf 中读取1个字节bytes_read++;buffer++;}ret = (bytes_read == 0 ? -1 : (int32_t) bytes_read);} else { // 文件读取uint32_t _fd = fd_local2global(fd);ret = file_read(&file_table[_fd], buf, count);}return ret;
}

若干个系统操作,添加完后总共有:

enum SYSCALL_NR {SYS_GETPID,SYS_WRITE,SYS_MALLOC,SYS_FREE,SYS_FORK,SYS_READ,SYS_PUTCHAR,SYS_CLEAR,SYS_GETCWD,SYS_OPEN,SYS_CLOSE,SYS_LSEEK,SYS_UNLINK,SYS_MKDIR,SYS_OPENDIR,SYS_CLOSEDIR,SYS_CHDIR,SYS_RMDIR,SYS_READDIR,SYS_REWINDDIR,SYS_STAT,SYS_PS // ps 指令
};

ps 指令的实现:

  • 思路:遍历所有进程队列 thread_all_list,输出 PCB,即 task_struct 中的相关信息。
  • 代码位于:thread/thread.c
  • 相关函数:pad_print 填充函数(控制输出的格式),elem2thread_info 核心函数,sys_ps 对 ele2thread_info 函数的封装。

实现一个简单的 shell

操作系统如果想和用户交互,那么就必须知道用户的输入,知道了输入那么就要做出相应的输出,此乃交互。各种操作系统的交互方式不过只是提供了一个“外壳”供用户操作,Window 下有 GUI 图形化界面和命令行窗口,而 Linux 则是通过命令行的形式和用户交互,只是 Linux 的叫法更加直接,就直接称为“Shell”(外壳的英文)。

readline / print_prompt 函数

shell/shell.c

// 输出提示符
void print_prompt(void) {printf("[xiaoling@localhost %s]$ ", cwd_cache);
}// 从键盘缓冲区中最多读入 count 个字节到 buf
static void readline(char* buf, int32_t count) {assert(buf != NULL && count > 0);char* pos = buf;while(read(stdin_no, pos, 1) != -1 && (pos - buf) < count) {switch(*pos) {// 清空屏幕,保留当前输入case 'l' - 'a':*pos = 0; // 重置clear(); // 清空屏幕print_prompt(); // 重新打印提示符printf("%s", buf); // 重新将之前输入的命令再次打印break;// 清空当前输入case 'u' - 'a':while(buf != pos) {putchar('\b');*(pos--) = 0;}break;case '\n':case '\r':*pos = 0;putchar('\n');return;case '\b':if(buf[0] != '\b') {--pos;putchar('\b');}break;default:putchar(*pos);pos++;}}printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

添加 Ctrl+u 和 Ctrl+l 快捷键

Ctrl+u:清空屏幕,但保留正在输入的指令。

Ctrl+l:清空当前输入。

简单说一下,两者的实现都是 u-al-a 各自所得差形成的属于 ASC 码表 中的不可见字符,因此不会产生可见字符,利用这个特点作为快捷键。

cmd_str 分割输入的指令,相对于高级语言中字符串的 split 函数。

例如:ls /opt -a 解析完后变成:[ls, /opt, -a]

shell/shell.c

// 将 cmd_str 字符串以 token 字符分割
static int32_t cmd_parse(char* cmd_str, char** argv, char token) {assert(cmd_str != NULL);int32_t arg_idx = 0;while(arg_idx < MAX_ARG_NR) {argv[arg_idx] = NULL;arg_idx++;}char* next = cmd_str;int32_t argc = 0;while(*next) {// 跳过空格while(*next == token) next++;/* 处理最后一个参数后接空格的清空,如:ls dir  */if(*next == 0) break;argv[argc] = next;// 获取整个有效参数while(*next && *next != token) next++;// 若未结束if(*next) *next++ = 0; // 则以字符串结束符0来表示一个单词的结束// 避免越界if(argc > MAX_ARG_NR) return -1;argc++; // 下一个参数}return argc;
}

解析路径

我们经常在终端中使用相对路径 ...,我们需要将这些路径转换成绝对路径 /**/**,这样便于我们操作。

shell/buildin_cmd.c

// 将路径 old_abs_path 中的 .. 和 . 转换成实际路径存入 new_abs_path
static void wash_path(char* old_abs_path, char* new_abs_path) {assert(old_abs_path[0] == '/');char name[MAX_FILE_NAME_LEN] = {0};char* sub_path = old_abs_path;sub_path = path_parse(sub_path, name);if(name[0] == 0) { // 只键入了 /new_abs_path[0] = '/';new_abs_path[1] = 0;return;}new_abs_path[0] = 0;strcat(new_abs_path, "/");while(name[0]) {if(!strcmp("..", name)) { // 若解析出来的目录是 ..char* slash_ptr = strrchr(new_abs_path, '/');if(slash_ptr != new_abs_path) { // 若 .. 后还未到达顶层,例如 /a/b .. 后为 /a*slash_ptr = 0;} else { // 若 .. 后到达了顶层,例如 /a .. 后为 /*(slash_ptr + 1) = 0;}} else if(strcmp(".", name)) { // 若解析出来的目录不是 .if(strcmp(new_abs_path, "/")) { // 判断顶层是否为 /strcat(new_abs_path, "/"); // 不是,则追加,这个判断是为了避免开头变成 // 的情况}// 追加目录strcat(new_abs_path, name);} // 若解析出来的目录是 . 则无需任何操作// 继续遍历下一层路径memset(name, 0, MAX_FILE_NAME_LEN);if(sub_path)sub_path = path_parse(sub_path, name);}
}// 将 path 处理成不含 .. 和 . 的绝对路径,保存到 final_path 中,path 是用户键入的
void make_clear_abs_path(char* path, char* final_path) {char abs_path[MAX_PATH_LEN] = {0};// 线判断输入的是否为绝对路径if(path[0] != '/') {memset(abs_path, 0, MAX_PATH_LEN);// 获取当前层的绝对路径if(getcwd(abs_path, MAX_PATH_LEN) != NULL) {if(!((abs_path[0] == '/') && (abs_path[1] == 0))) {strcat(abs_path, "/");}}}// 将键入的路径 path 拼接到当前层的绝对路径后面strcat(abs_path, path);// 将 abs_path 中的 . or .. 转为不含 . or .. 的绝对路径 final_pathwash_path(abs_path, final_path);
}

实现 ls、c、mkdir、ps、rm 等指令

命令分为两类:外部命令、内部命令。

外部命令:存储在文件系统上的外部程序,执行该命令实际上是从文件系统上加载该程序到内存中运行,也就是说外部命令会以进程的方式执行。例如:ls,存储路径为 /bin/ls

内部命令(内建命令):是系统本身提供的功能,并不以单独的程序文件存在,只是一些单独功能的函数,执行内部命令实际上就是调用这些函数。例如:cd、fg、jobs 等命令都是由 bash 提供的,因此它们称为 BASH_BUILTINS。

**内部命令的编写规则: **

  1. 命名方式:前缀 buildin_ + 命令名
  2. 形参均是 argc 和 argv,argv 是数组,其 argc 是数组长度,即命令参数的个数。
  3. 函数实现是调用同功能的系统调用实现的,如 buildin_cd 是调用系统调用 chdir 完成的。
  4. 在进行系统调用前,调用函数 make_clear_abs_path 把相对路径转为绝对路径。

这里就只贴出 ls 和 ps 的构建函数 shell/buildin_cmd.c

// ls 命令的内建函数
void buildin_ls(uint32_t argc, char** argv) {char* pathname = NULL;struct stat file_stat;memset(&file_stat, 0, sizeof(struct stat));bool long_info = false;uint32_t arg_path_nr = 0;uint32_t arg_idx = 1; // 跨过argv[0],因为argv[0]=lswhile(arg_idx < argc) {if(argv[arg_idx][0] == '-') { // 若是参数,则前缀为 -if(!strcmp("-l", argv[arg_idx])) { // 参数 -llong_info = true;} else if(!strcmp("-h", argv[arg_idx])) { // 参数 -hprintf("usage: -l list all infomation about the file.\n-h for help\nlist all files in the current dirctory if no option\n");return;} else { // 只支持 -h -l 两个参数printf("ls: invalid option %s\nTry `ls -h' for more information.\n", argv[arg_idx]);return;}} else { // 得到路径参数值if(arg_path_nr == 0) {pathname = argv[arg_idx];arg_path_nr = 1;} else {printf("ls: only support one path\n");return;}}arg_idx++;}if(pathname == NULL) { // 若没有给明确的路径,则默认当前路径为路径参数if(getcwd(final_path, MAX_PATH_LEN) != NULL) {pathname = final_path;} else {printf("ls: getcwd for default path failed\n");return;}} else {make_clear_abs_path(pathname, final_path);pathname = final_path;}// 得到目标文件的属性if(stat(pathname, &file_stat) == -1) {printf("ls: cannot access %s: No such file or directory\n", pathname);return;}// 判断文件类型if(file_stat.st_filetype == FT_DIRECTORY) { // 是目录struct dir* dir = opendir(pathname);struct dir_entry* dir_e = NULL;char sub_pathname[MAX_PATH_LEN] = {0};uint32_t pathname_len = strlen(pathname);uint32_t last_char_idx = pathname_len - 1;memcpy(sub_pathname, pathname, pathname_len);// 保证路径为 /a/b/c/ 而不是 /a/b/c 这是为了便于后面 stat 读取if(sub_pathname[last_char_idx] != '/') {sub_pathname[pathname_len] = '/';pathname_len++;}rewinddir(dir);if(long_info) {char ftype;printf("total: %d\n", file_stat.st_size);while((dir_e = readdir(dir))) {ftype = 'd';if(dir_e -> f_type == FT_REGULAR) ftype = '-';sub_pathname[pathname_len] = 0;strcat(sub_pathname, dir_e -> filename); // 拼接文件名到路径后面memset(&file_stat, 0, sizeof(struct stat));if(stat(sub_pathname, &file_stat) == -1) {printf("ls: cannot access %s: No such file or directory\n", dir_e->filename);return;}printf("%c  %d  %d  %s\n", ftype, dir_e->i_no, file_stat.st_size, dir_e->filename);}} else {while((dir_e = readdir(dir))) {printf("%s ", dir_e -> filename);}printf("\n");}closedir(dir);} else {if(long_info) printf("-  %d  %d  %s\n", file_stat.st_ino, file_stat.st_size, pathname);elseprintf("%s\n", pathname);}
}
// mkdir 命令内建函数
int32_t buildin_mkdir(uint32_t argc, char** argv) {int32_t ret = -1;if(argc != 2) {printf("mkdir: only support 1 argument!\n");} else {make_clear_abs_path(argv[1], final_path);if(!strcmp("/", final_path)) return ret; // 不能创建根目录if(mkdir(final_path) == 0) ret = 0;elseprintf("mkdir: create directory %s failed.\n", argv[1]);}return ret;
}
// ps 命令内建函数
void buildin_ps(uint32_t argc, char** argv UNUSED) {if(argc != 1) {printf("ps: no argument support!\n");return;}ps(); // 系统调用,内核实现是 sys_ps,位于 thread/thread.c
}

shell 主程序

// 存储输入的命令
static char cmd_line[MAX_PATH_LEN] = {0};
char final_path[MAX_PATH_LEN] = {0}; // 用于洗路径时的缓冲// 记录当前操作的所在目录,每次 cd 都会更新这个路径
char cwd_cache[MAX_PATH_LEN] = {0};char* argv[MAX_ARG_NR]; // argv 必须为全局变量,为了以后 exec 的程序可以访问到参数
int32_t argc = -1; // 参数个数...// 简单的 shell
void my_shell(void) {cwd_cache[0] = '/';cwd_cache[1] = 0;while(1) {print_prompt();memset(final_path, 0, MAX_PATH_LEN);memset(cmd_line, 0, MAX_PATH_LEN);readline(cmd_line, MAX_PATH_LEN);if(cmd_line[0] == 0) continue; // 若只输入了一个回车符argc = -1;argc = cmd_parse(cmd_line, argv, ' ');if(argc == -1) {printf("num of arguments exceed %d\n", MAX_ARG_NR);continue;}if(!strcmp("ls", argv[0])) buildin_ls(argc, argv);else if(!strcmp("cd", argv[0])) {if(buildin_cd(argc, argv) != NULL) {memset(cwd_cache, 0, MAX_PATH_LEN);strcpy(cwd_cache, final_path);}}else if(!strcmp("pwd", argv[0])) buildin_pwd(argc, argv);else if(!strcmp("ps", argv[0])) buildin_ps(argc, argv);else if(!strcmp("clear", argv[0])) buildin_clear(argc, argv);else if(!strcmp("mkdir", argv[0])) buildin_mkdir(argc, argv);else if(!strcmp("rmdir", argv[0])) buildin_rmdir(argc, argv);else if(!strcmp("rm", argv[0])) buildin_rm(argc, argv);else printf("external command.\n");}panic("my_shell: should not be here");
}

现在 Shell 的问题

看!我们通过 if-else 的形式来判断用户输入命令,若每个命令都要这样判断,工作量大不说,我们的外部命令也无法执行,因为我们的外部命令的命令名是未知的,这是无法预判的。

或许这可以通过 exec 解决?

但…我无法继续走下去了,因为…

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...