操作系统真象还原 学习笔记17--系统交互

selph
selph
发布于 2021-03-03 / 798 阅读
0
0

操作系统真象还原 学习笔记17--系统交互

本篇对应书籍第十五章15.1--15.4的内容

本篇介绍了fork的原理和实现,使用fork实现了init进程和简单的shell,同时实现了部分系统调用

fork 的原理与实现

fork是什么?

fork相当于同一个程序多次加载,在内存中产生多个同名程序

fork的作用是克隆进程,子进程是在fork函数返回之后开始执行,执行的是fork之后的代码

也就是fork返回之后,主进程拥有了子进程,子进程拥有了父进程

fork的实现

fork利用老进程克隆出一个新进程并使新进程执行,新进程之所以能够执行,本质上是它具备程序体,这其中包括代码和数据等资源。因此fork就是把某个进程的全部资源复制了一份,然后让处理器的cs:eip寄存器指向新进程的指令部分。 故:实现fork也要分两步,先复制进程资源,然后再跳过去执行。

进程的资源:

  1. 进程的pcb, 即task_struct, 这是让任务有 “存在感” 的身份证。
  2. 程序体, 即代码段数据段等,这是进程的实体。
  3. 用户栈,不用说了,编译器会把局部变量在栈中创建,并且函数调用也离不了栈。
  4. 内核栈,进入内核态时,一方面要用它来保存上下文环境,另一方面的作用同用户栈一样。
  5. 虚拟地址池,每个进程拥有独立的内存空间, 其 虚拟地址是用虚拟地址池来管理的。
  6. 页表,让进程拥有独立的内存空间。

只要将新进程加入到就绪队列中就可以执行了。

需要先做一些基础设施,PCB结构中添加parent_pid成员:

thread/thread.h

PCB结构中添加:

    uint32_t cwd_inode_nr;          // 进程所在工作目录的inode编号
    int16_t parent_pid;             // 父进程 pid
    uint32_t stack_magic;           // 栈的边界标记, 用于检测栈的溢出

thread/thread.c

/* fork进程时为其分配pid,因为allocate_pid已经是静态的,别的文件无法调用.
不想改变函数定义了,故定义fork_pid函数来封装一下。*/
pid_t fork_pid(void) {
   return allocate_pid();
}

init_thread函数中添加:

    pthread->cwd_inode_nr = 0;
    pthread->parent_pid = -1;        // -1表示没有父进程
    pthread->stack_magic = 0x19870916; // 自定义魔数

-1表示没有父进程,

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 上

extern void intr_exit(void);

/* 将父进程的pcb、虚拟地址位图拷贝给子进程 */
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct* child_thread, struct task_struct* parent_thread) {
/* a 复制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);
/* b 复制父进程的虚拟地址池的位图 */
   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->userprog_vaddr.vaddr_bitmap.bits还是指向父进程虚拟地址的位图地址
    * 下面将child_thread->userprog_vaddr.vaddr_bitmap.bits指向自己的位图vaddr_btmp */
   memcpy(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_bytes_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_bytes_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;
	 /* 下面的操作是将父进程用户空间中的数据通过内核空间做中转,最终复制到子进程的用户空间 */

	       /* a 将父进程在用户空间中的数据复制到内核缓冲区buf_page,
	       目的是下面切换到子进程的页表后,还能访问到父进程的数据*/
	       memcpy(buf_page, (void*)prog_vaddr, PG_SIZE);

	       /* b 将页表切换到子进程,目的是避免下面申请内存的函数将pte及pde安装在父进程的页表中 */
	       page_dir_activate(child_thread);
	       /* c 申请虚拟地址prog_vaddr */
	       get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);

	       /* d 从内核缓冲区中将父进程数据复制到子进程的用户空间 */
	       memcpy((void*)prog_vaddr, buf_page, PG_SIZE);

	       /* e 恢复父进程页表 */
	       page_dir_activate(parent_thread);
	    }
	    idx_bit++;
	 }
      }
      idx_byte++;
   }
}

copy_pcb_vaddrbitmap_stack0 函数的功能是将父进程的PCB,虚拟地址位图和0级栈拷贝给子进程

  • 将父进程的PCB那一内存页直接复制给子进程的PCB,然后再进行修改即可
  • 计算父进程虚拟地址位图大小,然后在子进程申请这个大小的空间并把父进程的虚拟地址位图复制过来

copy_body_stack3 函数的功能是复制子进程的进程体(代码和数据)及用户栈

  • 只要把用户空间中的有效部分拷贝出来就行,不同进程之间拷贝需要经过共用的内核空间进行中转
  • 这里的策略是,遍历父进程虚拟内存池,每找到1位有效空间就复制到内核空间缓冲区buf_page中,缓冲区只有1页大小,复制过去之后,切换页表为子进程的页表,然后申请虚拟地址(不申请位图空间),拷贝到子进程,然后再把页表切换为父进程

userprog/fork.c 下

/* 为子进程构建thread_stack和修改返回值 */
static int32_t build_child_stack(struct task_struct* child_thread) {
/* a 使子进程pid返回值为0 */
   /* 获取子进程0级栈栈顶 */
   struct intr_stack* intr_0_stack = (struct intr_stack*)((uint32_t)child_thread + PG_SIZE - sizeof(struct intr_stack));
   /* 修改子进程的返回值为0 */
   intr_0_stack->eax = 0;

/* b 为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_cnts++;
      }
      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;
   }

   /* a 复制父进程的pcb、虚拟地址位图、内核栈到子进程 */
   if (copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) {
      return -1;
   }

   /* b 为子进程创建页表,此页表仅包括内核空间 */
   child_thread->pgdir = create_page_dir();
   if(child_thread->pgdir == NULL) {
      return -1;
   }

   /* c 复制父进程进程体及用户栈给子进程 */
   copy_body_stack3(child_thread, parent_thread, buf_page);

   /* d 构建子进程thread_stack和修改返回值pid */
   build_child_stack(child_thread);

   /* e 更新文件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);    // 为子进程创建pcb(task_struct结构)
   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
}

build_child_stack 函数:为子进程构建thread_stack和修改返回值

  • fork父进程可以通过中断返回回到之前的代码
  • 子进程则需要自行构造中断栈手动从中断返回才能继续执行fork之后的代码

update_inode_open_cnts 函数:更新inode打开数

  • 将该进程打开的文件inode信息中的inode打开数+1

copy_process 函数:拷贝父进程本身所占资源给子进程

  • 是前面函数的封装

sys_fork 函数:fork子进程,内核线程不可直接调用

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

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

init 要主动调用 fork 才能派生出子子孙孙,所以需要先完成 fork系统调用:

  1. 在 syscall.h 中的 enum SYSCALL_NR 结构中添加 SYS_FORK。
  2. 在 syscall.c 中添加 fork(),原型是pid_t fork(void),实现是 “return _syscallO(SYS _FORK);"
  3. 在 syscall-init.c 中的函数 syscall—init 中,添加代码 "syscall_table[SYS—FORK]= sys_fork;"

接下来看init的实现,init定义在main中:

kernel/main.c

/* init进程 */
void init(void) {
   uint32_t ret_pid = fork();
   if(ret_pid) {  // 父进程
      int status;
      int child_pid;
       /* init在此处不停的回收僵尸进程 */
       while(1) {
	  child_pid = wait(&status);
	  printf("I`m init, My pid is 1, I recieve a child, It`s pid is %d, status is %d\n", child_pid, status);
       }
   } else {	  // 子进程
      my_shell();
   }
   panic("init: should not be here");
}

为了让init进程获得pid为1,需要最早让init进程得到加载执行,需要在thread.c这里修改

thread/thread.c

// 初始化线程环境
void thread_init(void) {
...

    /* 先创建第一个用户进程:init */
    process_execute(init, "init");         // 放在第一个初始化,这是第一个进程,init进程的pid为1
   
    // 将当前 main 函数创建为线程
    make_main_thread();
    idle_thread = thread_start("idle", 10, idle, NULL);

    put_str("thread_init done\n");
}

运行 Bochs

编译,运行:

image-20210301123029025

符合预期

添加 read 系统调用,获取键盘输入

原来的 sys_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(&kbd_buf);
	    bytes_read++;
	    buffer++;
	 }
	 ret = (bytes_read == 0 ? -1 : (int32_t)bytes_read);
   } else {
      uint32_t _fd = fd_local2global(fd);
      ret = file_read(&file_table[global_fd], buf, count);   
   }
   return ret;
}

这里从键盘的环形输入缓冲区一次获取一个字符,直到获取完count个字符为止

接下来添加系统调用

lib/user/syscall.c

/* 从文件描述符fd中读取count个字节到buf */
int32_t read(int32_t fd, void* buf, uint32_t count) {
   return _syscall3(SYS_READ, fd, buf, count);
}

在syscall-init里添加sys_read,在syscall.h里添加SYS_READ

添加 putchar、clear系统调用

实现clear内核实现:

lib/kernel/print.S

global cls_screen
cls_screen:
   pushad
   ;;;;;;;;;;;;;;;
	; 由于用户程序的cpl为3,显存段的dpl为0,故用于显存段的选择子gs在低于自己特权的环境中为0,
	; 导致用户程序再次进入中断后,gs为0,故直接在put_str中每次都为gs赋值. 
   mov ax, SELECTOR_VIDEO	       ; 不能直接把立即数送入gs,须由ax中转
   mov gs, ax

   mov ebx, 0
   mov ecx, 80*25
 .cls:
   mov word [gs:ebx], 0x0720		  ;0x0720是黑底白字的空格键
   add ebx, 2
   loop .cls 
   mov ebx, 0

 .set_cursor:				  ;直接把set_cursor搬过来用,省事
;;;;;;; 1 先设置高8位 ;;;;;;;;
   mov dx, 0x03d4			  ;索引寄存器
   mov al, 0x0e				  ;用于提供光标位置的高8位
   out dx, al
   mov dx, 0x03d5			  ;通过读写数据端口0x3d5来获得或设置光标位置 
   mov al, bh
   out dx, al

;;;;;;; 2 再设置低8位 ;;;;;;;;;
   mov dx, 0x03d4
   mov al, 0x0f
   out dx, al
   mov dx, 0x03d5 
   mov al, bl
   out dx, al
   popad
   ret

添加系统调用:

lib/user/syscall.c

/* 输出一个字符 */
void putchar(char char_asci) {
   _syscall1(SYS_PUTCHAR, char_asci);
}

/* 清空屏幕 */
void clear(void) {
   _syscall0(SYS_CLEAR);
}

记得在syscall.h和syscall-init.c里添加对应的宏和初始化

到此基础部分实现差不多了,下一节实现一个简单的shell

实现一个简单的shell

shell 雏形

shell/shell.c

#define cmd_len 128        // 最大支持128字符输入
#define MAX_ARG_NR 16	   // 加上命令名外,最多支持15个参数

/* 存储输入的命令 */
static char cmd_line[cmd_len] = {0};

/* 用来记录当前目录,是当前目录的缓存,每次执行cd命令时会更新此内容 */
char cwd_cache[64] = {0};

/* 输出提示符 */
void print_prompt(void) {
   printf("[selph@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 '\n':
	 case '\r':
	    *pos = 0;	   // 添加cmd_line的终止字符0
	    putchar('\n');
	    return;

	 case '\b':
	    if (cmd_line[0] != '\b') {		// 阻止删除非本次输入的信息
	       --pos;	   // 退回到缓冲区cmd_line中上一个字符
	       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");
}


/* 简单的shell */
void my_shell(void) {
   cwd_cache[0] = '/';
   while (1) {
      print_prompt(); 
      memset(cmd_line, 0, cmd_len);
      readline(cmd_line, cmd_len);
      if (cmd_line[0] == 0) {	 // 若只键入了一个回车
	 continue;
      }
   }
   panic("my_shell: should not be here");
}

这里这个my_shell函数实现的shell仅仅是输入字符,然后显示字符,按下回车后,重新接受输入,然后显示字符,还没实际功能,但看起来已经挺唬人的了

kernel/main.c

int main(void) {
   put_str("I am kernel\n");
   init_all();

   cls_screen();
   console_put_str("[selph@localhost /]$ ");

   while(1);
   return 0;
}

/* init进程 */
void init(void) {
   uint32_t ret_pid = fork();
   if(ret_pid) {  // 父进程
      while(1);       
   } else {	  // 子进程
	  my_shell();
   }
   while(1);
}

运行 Bochs

编译,运行:

image-20210301181509844

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

本小节介绍了实现快捷键功能的原理

Ctrl+u是清楚输入,清除本行的输入

Ctrl+l是清空屏幕,相当于clear命令,但不会清除本次输入

在kerboard.c里我们写过伏笔:

        // 只处理ascii码不为0的键
        if (cur_char) {
            /*****************  快捷键ctrl+l和ctrl+u的处理 *********************
            * 下面是把ctrl+l和ctrl+u这两种组合键产生的字符置为:
            * cur_char的asc码-字符a的asc码, 此差值比较小,
            * 属于asc码表中不可见的字符部分.故不会产生可见字符.
            * 我们在shell中将ascii值为l-a和u-a的分别处理为清屏和删除输入的快捷键*/
	        if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {
	            cur_char -= 'a';
	        }
            /****************************************************************/
      
            /* 若kbd_buf中未满并且待加入的cur_char不为0,
            * 则将其加入到缓冲区kbd_buf中 */
            if(!ioq_full(&kbd_buf)) {
                //put_char(cur_char);
                ioq_putchar(&kbd_buf, cur_char);
            }

当输入组合键Ctrl+u和+l的时候,就用u和l的ascii减掉a的ascii,成为控制字符,这样一来,这个组合键就可以变成一个ASCII码来进行处理了

shell/shell.c

/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char* buf, int32_t count) {
...

	 case '\b':
	    if (cmd_line[0] != '\b') {		// 阻止删除非本次输入的信息
	       --pos;	   // 退回到缓冲区cmd_line中上一个字符
	       putchar('\b');
	    }
	    break;
        
    	 /* ctrl+l 清屏 */
	 case 'l' - 'a': 
	    /* 1 先将当前的字符'l'-'a'置为0 */
	    *pos = 0;
	    /* 2 再将屏幕清空 */
	    clear();
	    /* 3 打印提示符 */
	    print_prompt();
	    /* 4 将之前键入的内容再次打印 */
	    printf("%s", buf);
	    break;

	 /* ctrl+u 清掉输入 */
	 case 'u' - 'a':
	    while (buf != pos) {
	       putchar('\b');
	       *(pos--) = 0;
	    }
	    break;

	 /* 非控制键则输出字符 */
	 default:
	    putchar(*pos);
	    pos++;
      }
   }
   printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

按键处理新增组合键的内容

解析键入字符

给shell增加命令解析的功能:

shell/shell.c

/* 分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组 */
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 dir2 " */
      if (*next == 0) {
	 break; 
      }
      argv[argc] = next;

     /* 内层循环处理命令行中的每个命令字及参数 */
      while (*next && *next != token) {	  // 在字符串结束前找单词分隔符
	 next++;
      }

      /* 如果未结束(是token字符),使tocken变成0 */
      if (*next) {
	 *next++ = 0;	// 将token字符替换为字符串结束符0,做为一个单词的结束,并将字符指针next指向下一个字符
      }
   
      /* 避免argv数组访问越界,参数过多则返回0 */
      if (argc > MAX_ARG_NR) {
	 return -1;
      }
      argc++;
   }
   return argc;
}

char* argv[MAX_ARG_NR] = {NULL};
int32_t argc = -1;
/* 简单的shell */
void my_shell(void) {
    cwd_cache[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;
	    }

        int32_t arg_idx = 0;
        while(arg_idx < argc){
            printf("%s ",argv[arg_idx]);
            arg_idx++;
        }
        printf("\n");

    }
    panic("my_shell: should not be here");
}

cmd_parse 函数:分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组

my_shell 函数:将解析到的单词依次打印出来

解析命令就是把输入的字符接收进来,然后按空格拆分成单词,存入数组中

这里为了测试,就是把接收进来的单词再打印出来

运行 Bochs 功能验证

编译,运行:

image-20210303113536392

符合预期

添加系统调用

lib/user/syscall.h

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
};

lib/user/syscall.c

/* 获取当前工作目录 */
char* getcwd(char* buf, uint32_t size) {
   return (char*)_syscall2(SYS_GETCWD, buf, size);
}

/* 以flag方式打开文件pathname */
int32_t open(char* pathname, uint8_t flag) {
   return _syscall2(SYS_OPEN, pathname, flag);
}

/* 关闭文件fd */
int32_t close(int32_t fd) {
   return _syscall1(SYS_CLOSE, fd);
}

/* 设置文件偏移量 */
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence) {
   return _syscall3(SYS_LSEEK, fd, offset, whence);
}

/* 删除文件pathname */
int32_t unlink(const char* pathname) {
   return _syscall1(SYS_UNLINK, pathname);
}

/* 创建目录pathname */
int32_t mkdir(const char* pathname) {
   return _syscall1(SYS_MKDIR, pathname);
}

/* 打开目录name */
struct dir* opendir(const char* name) {
   return (struct dir*)_syscall1(SYS_OPENDIR, name);
}

/* 关闭目录dir */
int32_t closedir(struct dir* dir) {
   return _syscall1(SYS_CLOSEDIR, dir);
}

/* 删除目录pathname */
int32_t rmdir(const char* pathname) {
   return _syscall1(SYS_RMDIR, pathname);
}

/* 读取目录dir */
struct dir_entry* readdir(struct dir* dir) {
   return (struct dir_entry*)_syscall1(SYS_READDIR, dir);
}

/* 回归目录指针 */
void rewinddir(struct dir* dir) {
   _syscall1(SYS_REWINDDIR, dir);
}

/* 获取path属性到buf中 */
int32_t stat(const char* path, struct stat* buf) {
   return _syscall2(SYS_STAT, path, buf);
}

/* 改变工作目录为path */
int32_t chdir(const char* path) {
   return _syscall1(SYS_CHDIR, path);
}

/* 显示任务列表 */
void ps(void) {
   _syscall0(SYS_PS);
}

userprog/syscall-init.c

// 初始化系统调用
void syscall_init(void) {
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE]  = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE]   = sys_free;
    syscall_table[SYS_FORK]	  = sys_fork;
    syscall_table[SYS_READ]   = sys_read;
    syscall_table[SYS_PUTCHAR]= console_put_char;
    syscall_table[SYS_CLEAR]  = cls_screen;
    syscall_table[SYS_GETCWD] = sys_getcwd;
   syscall_table[SYS_OPEN]	  = sys_open;
   syscall_table[SYS_CLOSE]	  = sys_close;
   syscall_table[SYS_LSEEK]	  = sys_lseek;
   syscall_table[SYS_UNLINK]  = sys_unlink;
   syscall_table[SYS_MKDIR]	  = sys_mkdir;
   syscall_table[SYS_OPENDIR] = sys_opendir;
   syscall_table[SYS_CLOSEDIR]= sys_closedir;
   syscall_table[SYS_CHDIR]	  = sys_chdir;
   syscall_table[SYS_RMDIR]	  = sys_rmdir;
   syscall_table[SYS_READDIR] = sys_readdir;
   syscall_table[SYS_REWINDDIR] = sys_rewinddir;
   syscall_table[SYS_STAT]	    = sys_stat;
   syscall_table[SYS_PS]	    = sys_ps;
    put_str("syscall_init done\n");
}

这一堆系统调用都是之前实现过的,除了ps:

thread/thread.c

/* 以填充空格的方式输出buf */
static void pad_print(char* buf, int32_t buf_len, void* ptr, char format) {
   memset(buf, 0, buf_len);
   uint8_t out_pad_0idx = 0;
   switch(format) {
      case 's':
	 out_pad_0idx = sprintf(buf, "%s", ptr);
	 break;
      case 'd':
	 out_pad_0idx = sprintf(buf, "%d", *((int16_t*)ptr));
      case 'x':
	 out_pad_0idx = sprintf(buf, "%x", *((uint32_t*)ptr));
   }
   while(out_pad_0idx < buf_len) { // 以空格填充
      buf[out_pad_0idx] = ' ';
      out_pad_0idx++;
   }
   sys_write(stdout_no, buf, buf_len - 1);
}

/* 用于在list_traversal函数中的回调函数,用于针对线程队列的处理 */
static bool elem2thread_info(struct list_elem* pelem, int arg UNUSED) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   char out_pad[16] = {0};

   pad_print(out_pad, 16, &pthread->pid, 'd');

   if (pthread->parent_pid == -1) {
      pad_print(out_pad, 16, "NULL", 's');
   } else { 
      pad_print(out_pad, 16, &pthread->parent_pid, 'd');
   }

   switch (pthread->status) {
      case 0:
	 pad_print(out_pad, 16, "RUNNING", 's');
	 break;
      case 1:
	 pad_print(out_pad, 16, "READY", 's');
	 break;
      case 2:
	 pad_print(out_pad, 16, "BLOCKED", 's');
	 break;
      case 3:
	 pad_print(out_pad, 16, "WAITING", 's');
	 break;
      case 4:
	 pad_print(out_pad, 16, "HANGING", 's');
	 break;
      case 5:
	 pad_print(out_pad, 16, "DIED", 's');
   }
   pad_print(out_pad, 16, &pthread->elapsed_ticks, 'x');

   memset(out_pad, 0, 16);
   ASSERT(strlen(pthread->name) < 17);
   memcpy(out_pad, pthread->name, strlen(pthread->name));
   strcat(out_pad, "\n");
   sys_write(stdout_no, out_pad, strlen(out_pad));
   return false;	// 此处返回false是为了迎合主调函数list_traversal,只有回调函数返回false时才会继续调用此函数
}

 /* 打印任务列表 */
void sys_ps(void) {
   char* ps_title = "PID            PPID           STAT           TICKS          COMMAND\n";
   sys_write(stdout_no, ps_title, strlen(ps_title));
   list_traversal(&thread_all_list, elem2thread_info, 0);
}

功能就是将线程列表中各个线程的信息打印出来

路径解析转换

当前路径+相对路径=绝对路径

路径解析也主要是把路径中的 “ ..“和“ .“替换成实际的目录,将用户键入的路径,无论是绝对路径,还是相对路径, 一 律转换成不含 “ .” 和 “ .. "的绝对路径,然后再把转换后的路径作为命令的参数,最后再对某些有默认参数的命令做针对性处理就好了。

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后返回 
      new_abs_path[0] = '/';
      new_abs_path[1] = 0;
      return;
   }
   new_abs_path[0] = 0;	   // 避免传给new_abs_path的缓冲区不干净
   strcat(new_abs_path, "/");
   while (name[0]) {
      /* 如果是上一级目录“..” */
      if (!strcmp("..", name)) {
	 char* slash_ptr =  strrchr(new_abs_path, '/');
       /*如果未到new_abs_path中的顶层目录,就将最右边的'/'替换为0,
	 这样便去除了new_abs_path中最后一层路径,相当于到了上一级目录 */
	 if (slash_ptr != new_abs_path) {	// 如new_abs_path为“/a/b”,".."之后则变为“/a”
	    *slash_ptr = 0;
	 } else {	      // 如new_abs_path为"/a",".."之后则变为"/"
      /* 若new_abs_path中只有1个'/',即表示已经到了顶层目录,
	 就将下一个字符置为结束符0. */
	    *(slash_ptr + 1) = 0;
	 }
      } else if (strcmp(".", name)) {	  // 如果路径不是‘.’,就将name拼接到new_abs_path
	 if (strcmp(new_abs_path, "/")) {	  // 如果new_abs_path不是"/",就拼接一个"/",此处的判断是为了避免路径开头变成这样"//"
	    strcat(new_abs_path, "/");
	 }
	 strcat(new_abs_path, name);
      }  // 若name为当前目录".",无须处理new_abs_path

      /* 继续遍历下一层路径 */
      memset(name, 0, MAX_FILE_NAME_LEN);
      if (sub_path) {
	 sub_path = path_parse(sub_path, name);
      }
   }
}

/* 将path处理成不含..和.的绝对路径,存储在final_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))) {	     // 若abs_path表示的当前目录不是根目录/
	    strcat(abs_path, "/");
	 }
      }
   }
   strcat(abs_path, path);
   wash_path(abs_path, final_path);
}

shell/shell.c

char* argv[MAX_ARG_NR] = {NULL};
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;
	      }

         char buf[MAX_PATH_LEN] = {0};
         int32_t arg_idx = 0;
         while(arg_idx < argc){
            make_clear_abs_path(argv[arg_idx],buf);
            printf("%s -> %s\n",argv[arg_idx],buf);
            arg_idx++;
         }
         printf("\n");

   }
   panic("my_shell: should not be here");
}

运行 Bochs 功能验证

编译,运行:

image-20210303123742838

实现 ls、cd、mkdir、ps、rm 等命令

命令分为外部命令和内部命令,外部命令是我们的程序执行的,内部命令是系统内核提供的功能

内部命令的编写规则:

  1. 内部命令都以前缀"buildin_" + “命令名”的形式命名, 如cd命令的函数是buildin_cd。
  2. 形参均是 argc 和 argv, argc 是参数数组 argv中参数的个数 。
  3. 函数实现是调用同功能的系统调用实现的,如函数buildin_cd是调用系统调用chdir完成的。
  4. 在进行系统调用前调用函数 make_clear_ abs_path 把路径转换为绝对路径。

功能都是系统调用实现的,通过解析键入字符和路径,获取输入的命令,根据命令判断来去调用系统调用从而实现了简单的控制功能

buildin_cmd.c

pwd
/* pwd命令的内建函数 */
void buildin_pwd(uint32_t argc, char** argv UNUSED) {
   if (argc != 1) {
      printf("pwd: no argument support!\n");
      return;
   } else {
      if (NULL != getcwd(final_path, MAX_PATH_LEN)) {
	 printf("%s\n", final_path); 
      } else {
	 printf("pwd: get current work directory failed.\n");
      }
   }
}
cd
/* cd命令的内建函数 */
char* buildin_cd(uint32_t argc, char** argv) {
   if (argc > 2) {
      printf("cd: only support 1 argument!\n");
      return NULL;
   }

   /* 若是只键入cd而无参数,直接返回到根目录. */
   if (argc == 1) {
      final_path[0] = '/';
      final_path[1] = 0;
   } else {
      make_clear_abs_path(argv[1], final_path);
   }

   if (chdir(final_path) == -1) {
      printf("cd: no such directory %s\n", final_path);
      return NULL;
   }
   return final_path;
}
ls
/* 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]是字符串“ls”
   while (arg_idx < argc) {
      if (argv[arg_idx][0] == '-') {	  // 如果是选项,单词的首字符是-
	 if (!strcmp("-l", argv[arg_idx])) {         // 如果是参数-l
	    long_info = true;
	 } else if (!strcmp("-h", argv[arg_idx])) {   // 参数-h
	    printf("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 {	     // ls的路径参数
	 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) {	 // 若只输入了ls 或 ls -l,没有输入操作路径,默认以当前路径的绝对路径为参数.
      if (NULL != getcwd(final_path, MAX_PATH_LEN)) {
	 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);
      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);
      } else {
	 printf("%s\n", pathname);  
      }
   }
}
ps
/* ps命令内建函数 */
void buildin_ps(uint32_t argc, char** argv UNUSED) {
   if (argc != 1) {
      printf("ps: no argument support!\n");
      return;
   }
   ps();
}
clear
/* clear命令内建函数 */
void buildin_clear(uint32_t argc, char** argv UNUSED) {
   if (argc != 1) {
      printf("clear: no argument support!\n");
      return;
   }
   clear();
}
mkdir
/* 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)) {
	 if (mkdir(final_path) == 0) {
	    ret = 0;
	 } else {
	    printf("mkdir: create directory %s failed.\n", argv[1]);
	 }
      }
   }
   return ret;
}
rmdir
/* rmdir命令内建函数 */
int32_t buildin_rmdir(uint32_t argc, char** argv) {
   int32_t ret = -1;
   if (argc != 2) {
      printf("rmdir: only support 1 argument!\n");
   } else {
      make_clear_abs_path(argv[1], final_path);
   /* 若删除的不是根目录 */
      if (strcmp("/", final_path)) {
	 if (rmdir(final_path) == 0) {
	    ret = 0;
	 } else {
	    printf("rmdir: remove %s failed.\n", argv[1]);
	 }
      }
   }
   return ret;
}
rm
/* rm命令内建函数 */
int32_t buildin_rm(uint32_t argc, char** argv) {
   int32_t ret = -1;
   if (argc != 2) {
      printf("rm: only support 1 argument!\n");
   } else {
      make_clear_abs_path(argv[1], final_path);
   /* 若删除的不是根目录 */
      if (strcmp("/", final_path)) {
	 if (unlink(final_path) == 0) {
	    ret = 0;
	 } else {
	    printf("rm: delete %s failed.\n", argv[1]);
	 }
	    
      }
   }
   return ret;
}
help
/* 显示内建命令列表 */
void buildin_help(uint32_t argc UNUSED, char** argv UNUSED) {
   help();
}

shell/shell.c

char* argv[MAX_ARG_NR] = {NULL};
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");
}

运行 Bochs 功能验证

编译,运行:

image-20210303132949009

参考资料

  • 《操作系统真象还原》Chapter 15

评论