pid_t fork( void);
pid_t 是一个宏定义,其实质是int 被定义在 #includesys/types.h>
中。
返回值: 若成功调用一次则返回两个值,其返回值的意义如下
0
:子进程
子进程ID,即 >0
:父进程
-1
:错误
fork 的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。(相关函数请看 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;}
}
需要打开两个终端,一个用于运行程序,一个用于查看进程信息。
终端一:运行测试程序
终端二:查看进程信息
一开始并无相关进程信息,接下来执行测试程序,首先执行父进程,得到 pid=8917
,休眠 5s 后执行 fork() 函数,之后会发现输出了两条语句,也就是说 fork 返回一次返回了两个不同的返回值,为什么呢?
看进程信息,fork 后多了一个“子进程”,其 pid=8932
,也就是说 fork 其实是将父进程克隆了一份,而进程拥有独立的地址空间,因此两个进程执行的是独立且相同的代码,所以它们并不共享同一内存空间,也就是执行的是两套代码(代码是相同的,只是有两套而已)。
看输出的结果,是先执行的父进程,其 fork 返回父进程的 pid,其次执行的才是子进程,其子进程 fork 返回的是 0。
注意,这里并不是父子进程各调用一次,父子进程合计才调用了一次 fork,也就是说执行一次 fork 会返回两个值,只是返回的地方不一样,一次是在父进程,一次是在父进程。
前面说明了 fork 本质是将父进程克隆了一份,称为子进程。首先我们需要明确要复制的资源,复制完成后让处理器的 cs:eip 指向新进程的指令部分。
实现 fork 分两步:
明确要复制的资源有:
copy_pcb_vaddrbitmap_stack0
copy_body_stack3
copy_body_stack3
copy_pcb_vaddrbitmap_stack0
copy_pcb_vaddrbitmap_stack0
create_page_dir
克隆后的进程要如何执行:将新进程加入到就绪队列就可以了,当然要提前把相关的栈准备好。相关函数:build_child_stack
注意:复制完 PCB 后,要记得文件描述符所对应打开的文件的次数要增加。相关函数:
update_inode_open_cnts
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;
}
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;
}
// 复制子进程的进程体(代码和数据)以及用户栈
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 的数据拷贝到子进程之前,一定要将页表替换为子进程的页表。
// 为子进程构建 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;
}
// 更新 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++;}
}
// 拷贝父进程本身所占资源给子进程(对前面函数的封装罢了)
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;
}
// 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
}
这个没啥好说,老套路走一遍就行。
在 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");
}
三个新的j基础系统调用:
其中 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 中的相关信息。操作系统如果想和用户交互,那么就必须知道用户的输入,知道了输入那么就要做出相应的输出,此乃交互。各种操作系统的交互方式不过只是提供了一个“外壳”供用户操作,Window 下有 GUI 图形化界面和命令行窗口,而 Linux 则是通过命令行的形式和用户交互,只是 Linux 的叫法更加直接,就直接称为“Shell”(外壳的英文)。
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
:清空当前输入。
简单说一下,两者的实现都是 u-a
和 l-a
各自所得差形成的属于 ASC 码表 中的不可见字符,因此不会产生可见字符,利用这个特点作为快捷键。
例如: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,存储路径为 /bin/ls
。
内部命令(内建命令):是系统本身提供的功能,并不以单独的程序文件存在,只是一些单独功能的函数,执行内部命令实际上就是调用这些函数。例如:cd、fg、jobs 等命令都是由 bash 提供的,因此它们称为 BASH_BUILTINS。
**内部命令的编写规则: **
buildin_
+ 命令名这里就只贴出 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
}
// 存储输入的命令
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");
}
看!我们通过 if-else 的形式来判断用户输入命令,若每个命令都要这样判断,工作量大不说,我们的外部命令也无法执行,因为我们的外部命令的命令名是未知的,这是无法预判的。
或许这可以通过 exec 解决?
但…我无法继续走下去了,因为…