操作系统真相还原:Shell进程,fork实现

第15章-系统交互

这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件

15.1 fork的原理与实现

15.1.1 什么是fork

fork函数的原型是pid_t fork(void),父进程返回子进程的PID,子进程返回0。

15.1.2 fork的实现

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

进程资源:

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

​ 在真正编写 fork 代码之前,咱们还要增加一些基础设施,首先在 thread.htask_struct 中增加了成员“int16_t parent_pid“它位于 cwd_inode_nr 之后,表示父进程的 pid,也就是自己的父进程是谁。

​ 然后在thread.c 中的init_thread函数中增加一句“ pthread->parent = -1 ”,使任务的父进程默认为-1 ,-1表示没有父进程。

​ 然后添加fork_pid就是封装了allocate_pid,因为allocate_pid之前实现的时候有static,所以作者为了不去修改这个,就采取了进一步封装.

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

​ 然后再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;
}

get_a_page_without_opvaddrbitmap:用于为指定的虚拟地址创建物理页映射,与get_a_page相比,少了操作进程pcb中的虚拟内存池位图。它接受 2 个参数,内存池标识 pf、虚拟地址 vaddr,功能是为 vaddr 分配一物理页,但无需从虚拟地址内存池中设置位图。

​ 然后再fork.c中去实现fork的核心部分,代码如下:

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:接受2个参数,子进程 child_thread、父进程 parent_thread,功能是将父进程的 pcb、虚拟地址位图拷贝给子进程。

​ 用于根据传入的父子进程pcb指针,先复制整个父进程pcb内容到子进程pcb中,然后再针对设置子进程pcb内容,包含:pid, elapsed_ticks, status, ticks, parent_pid, general_tag, all_list_tag, u_block_desc, userprog_vaddr(让子进程拥有自己的用户虚拟地址空间内存池,但是其位图是拷贝父进程的)。这个过程中,内核栈中的内容被完全拷贝了。

copy_body_stack3:它接受 3 个参数,子进程 child_thread、父进程 pthrent_thread、页缓冲区 buf_page, buf_page 必须是内核页,我们要用它作为所有进程的数据共享缓冲区。函数功能是复制子进程的进程体(代码和数据)及用户栈。

​ 用于根据传入的父子进程pcb指针,复制进程的用户空间堆与栈中的数据。核心原理:遍历父进程的userprog_vaddr当中的虚拟地址空间位图,来判断父进程的用户虚拟地址空间中是否有数据。如果有,就拷贝到内核空间的中转区中,然后调用page_dir_activate,切换到子进程页表,调用get_a_page_without_opvaddrbitmap为子进程特定虚拟地址申请一个物理页(其中并不涉及子进程userprog_vaddr中的位图修改),然后从内核中转区中把数据拷贝到子进程相同的虚拟地址内。

​ 接下来实现fork的第二部分

/* 为子进程构建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;	   //0xc011afa0 
   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:接受 1 个参数,子进程 child_thread . 功能是为子进程构建 thread_stack 和修改
返回值。

​ 大伙儿知道,父进程在执行 fork 系统调用时会进入内核态,中断入口程序会保存父进程的上下文,这其中包括进程在用户态下的 CS:EIP 的值,因此父进程从 fork 系统调用返回后,可以继续 fork 之后的代码执行。在这之前我们己经通过函数 copy_pcb_vaddrbitmap_stack0 将父进程的内核技复制到了子进程的内核栈中,那里保存了返回地址,也就是 fork 之后的地址,为了让子进程也能继续 fork 之后的代码运行,咱们必须让它同父进程一样,从中断退出,也就是要经过 intr_exit 。 子进程是由调试器 schedule 调度执行的,它要用到switch_to 函数,而 switch_to 函数要从栈thread_stack中恢复上下文,因此我们要想办法构建出合适的 thread_stack

其实就是在中断栈之后再构建一个Switch_to栈,同时让PCB的esp指向swith_to栈栈顶,并且switch_to栈中返回地址要填上intr_exit函数地址。这样执行ret之后,就能去执行intr_exit,并利用intr_stack执行中断返回,由于intr_stack中拷贝了父进程进入中断时的用户栈信息,cs: ip 信息,所以中断退出后,子进程将会继续执行父进程之后的代码。

在这里插入图片描述

​ 因为系统调用的返回值会放入内核栈中的中断栈(intr_stackeax的位置,这样中断退出(intr_exit)就会push eax时将返回值放入eax中。所以我们将子进程的内核栈中断栈eax的值改成0。这就是fork子进程自己的返回值了。

update_inode_open_cnts:由于fork出来的子进程几乎和父进程一样,所以父进程打开的文件,子进程也要打开。所以,父进程的全局打开文件结构中记录文件除前 3 个标准文件描述符之外的所有文件描述符打开的次数都需要 + 1。原理:遍历进程pcb(父,子均可)中的文件描述符,找到对应的全局打开文件结构索引就行了。

copy_process:此函数接受 2 个参数,子进程 child_thread 和父进程 parent_thread,功能是拷贝父进程本身所占资源给子进程。

​ 此函数是前面所介绍的函数的封装,函数开头申请了 l 页的内核空间作为内核缓冲区,即 buf_page。 然后调用函数 copy_pcb_vaddrbitmap_stackO 把父进程子的 pcb、虚拟地址位图及内核占复制给子进程,接着调用 create_page_dir 函数为子进程创建页表,该函数定义在 process.c 中,很久以前介绍过。然后调用函数 copy_body_stack3 复制父进程进程体及用户战给子进程,接着调用函数 build_child_stack 为子进程构建
thread_stack,随后调用 update_inode_open_cnts 更新 inode 的打开数,最后释放 buf_page

sys_forkfork 的内核实现部分, fork 本身是无参数的,因此 sys_fork 也无参数。 函数先调用 get_kernel_pages(1)获得 1 页内核空间作为子进程的 pcb。接下来调用 copy_process 复制父进程的信息到子进程,然后将其加入到就绪队列和全部队列,最后返回子进程的 pid

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

​ 首先添加fork系统调用步骤如下:

  1. syscall.h中的enum SYSCALL_NR结构中添加SYS_FORK
  2. syscall.c中添加fork()
  3. syscall-init.c中的函数syscall_init中添加代码。
#include "thread.h"

enum SYSCALL_NR {
   SYS_GETPID,
   SYS_WRITE,
   SYS_MALLOC,
   SYS_FREE,
   SYS_FORK
};

#include "thread.h"

/* 派生子进程,返回子进程pid */
pid_t fork(void)
{
    return _syscall0(SYS_FORK);
}

#include "fork.h"

/* 初始化系统调用 */
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;
	put_str("syscall_init done\n");
}

init进程:我们学习Linux做法,让init作为pid为1的用户进程,所以必须要放在主线程创建之前创建。后续所有的进程都是它的孩子,它还负责所有子进程的资源回收。

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);
    /* 先创建第一个用户进程:init */
    process_execute(init, "init");         // 放在第一个初始化,这是第一个进程,init进程的pid为1
    /* 将当前main函数创建为线程 */
    make_main_thread();
    /* 创建idle线程 */
    idle_thread = thread_start("idle", 10, idle, NULL);
    put_str("thread_init done\n");
}

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

​ Linux中从键盘获取输入是利用 read 系统调用,咱们在很久之前实现了 sys_read ,也许有同学会说,现在只要按照那三个步骤添加 read 系统调用就行了。其实……旧版本的 sys_read 只能从文件中获取数据,还不能从标准输入设备键盘中读取数据,因此当务之急,先要改进 sys_read,让其支持键盘。

#include "keyboard.h"
#include "ioqueue.h"

/* 从文件描述符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[_fd], buf, count);
    }
    return ret;
}



sys_read用于从指定文件描述符中获取conunt字节数据,如果文件描述符是stdin_no,那么直接循环调用ioq_getchar从键盘获取内容,否则调用file_read从文件中读取内容

15.3添加putchar、clear系统调用

/* 向屏幕输出一个字符 */
void sys_putchar(char char_asci)
{
    console_put_char(char_asci);
}

sys_put_char用于向屏幕输出一个字符。

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

cls_screen用于清空屏幕,核心原理:向代表80列×25行,共2000个字符位置的内存写入空格符,然后设定光标位置为左上角(即位置0)

然后把上诉的程序添加到系统调用中:

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

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

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

/* 初始化系统调用 */
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] = sys_putchar;
    syscall_table[SYS_CLEAR] = cls_screen;
    put_str("syscall_init done\n");
}

15.4 实现一个简单的shell

本节主要是实现一个简单的shell,能处理键入的命令,实现内部命令及外部命令,支持简单的快捷键等。

15.4.1 shell雏形

shell 的功能大致是获取用户的键入,然后分析输入的字符串,判断是内部命令,还是外部命令,然后执行不同的策略。

#include "shell.h"
#include "stdio.h"

#define cmd_len 128 //最大支持键入128个字符的命令行输入

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

/* 输出提示符 */
void print_prompt(void)
{
    printf("[rabbit@localhost %s]$ ", cwd_cache);
}

数组 cmd_len用来存储键入的命令。数组cwd_cache用来存储当前目录名,主要是在命令提示符中,它由以后实现的 cd 命令来维护。

print_prompt用于输出命令提示符也就是咱们登录 shell 后,命令行中显示的主机名等 ,也就是我们在终端输入命令时,前面那串字符。

/* 从键盘缓冲区中最多读入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 (buf[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");
}

readline循环调用read从键盘输入缓冲读取字符,每次读取一个,最多读入count个字节到buf。根据每次读入的值不同,处理方式也不同:/n/r表示按下enter键,用户输入命令结束,缓冲区输入个0表示命令字符串结尾。/b表示按下退格键,就删除一个字符。普通字符就直接读入buf。每种字符都调用了putchar进行打印,是因为我们的键盘中断处理函数已经删除打印功能。

#include "string.h"

#define cmd_len 128 // 最大支持键入128个字符的命令行输入
static char cmd_line[cmd_len] = {0};

/* 简单的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");
}

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

readline中新增加对于组合键的处理,ctrl + l 清除除了当前行外的其他行。ctrl + u清除本行的输入,效果类似于连续按下多个退格。我们在键盘中断处理程序中已经预先写好了按下ctrl + l 与 ctrl + u 的处理。

	        if ((ctrl_status && cur_char == 'l') || (ctrl_status && cur_char == 'u')) {
	            cur_char -= 'a';
	        }
            if (!ioq_full(&kbd_buf)) {
                ioq_putchar(&kbd_buf, cur_char);
            }


也就是说,我们按下ctrl + l 与 ctrl + u时,放入键盘输入缓冲区的字符是ascii 码为 ‘l’ - ‘a’ 与 ‘u’ - ‘a’,这两个ascii码都属于不可见的控制字符。所以我们只需要增加readline读出这两种情况的处理逻辑即可。

/* 从键盘缓冲区中最多读入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;

        /* 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");
}

15.4.3 解析键入的字符
/*分析字符串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]; //argv必须为全局变量,为了以后exec的程序可以访问参数
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数组。这个函数就是个字符串处理函数,从诸如 'ls dir ’ 这样的命令中拆单词,拆成 ‘ls’‘dir’

15.4.4 添加系统调用

到现在咱们的 shell 还名不符实,它不能帮用户做任何事情。为了让 shell 能够尽快与用户互动,咱们还需要做些基础工作,等基础设施搭建完成了,咱们的 shell 就具有“灵性”了。 到现在咱们的 shell 还名不符实,它不能帮用户做任何事情。为了让 shell 能够尽快与用户互动,咱们还需要做些基础工作,等基础设施搭建完成了,咱们的shell 就具有“灵性”了。

首先实现一下系统调用ps:

pad_print用于对齐输出,也就是有一个buf区长度10字节,然后我们无论要输出什么,都向这个buf中写入,然后空余部分全部填充空格,最后将整个buf输出。比如输出“hello”,经过处理就变成了"hello "

elem2thread_info调用pad_print来对齐输出每个pcb的pid, ppid, status, elapsed_ticks, name

sys_ps调用list_traversal遍历所有任务队列,在其中回调elem2thread_info来输出进程或线程pcb中的信息。

#include "stdio.h"
#include "fs.h"
#include "file.h"

/* 以填充空格的方式输出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);
}

现在把之前的所有系统调用进行封装:

#include "fs.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
};

然后实现它们的用户态入口

/* 获取当前工作目录 */
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);
}

添加系统调用用户态入口函数声明

char *getcwd(char *buf, uint32_t size);
int32_t open(char *pathname, uint8_t flag);
int32_t close(int32_t fd);
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence);
int32_t unlink(const char *pathname);
int32_t mkdir(const char *pathname);
struct dir *opendir(const char *name);
int32_t closedir(struct dir *dir);
int32_t rmdir(const char *pathname);
struct dir_entry *readdir(struct dir *dir);
void rewinddir(struct dir *dir);
int32_t stat(const char *path, struct stat *buf);
int32_t chdir(const char *path);
void ps(void);

最后在系统调用表中添加真正的系统调用执行函数

/* 初始化系统调用 */
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] = sys_putchar;
    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");
}
15.4.5 路径解析转换

​ 相对路径就是以当前工作路径为基础,命令或参数都是相对于当前工作路径得出的,“当前工作路径”+“相对路径”=“绝对路径” 。

​ 路径输入发生在用户态,而系统调用通过中断的方式发生在内核态,咱们这里反复强调的一句话是操作系统虽是中断驱动的,但我们又希望它不停地运行,故不希望执行中断处理程序的时间过长,因此我们要为内核代码减荷,让它们尽量快点从内核态返回,以处理更多的中断。于是很自然地想到,我们不应该把路径转换的工作交给内核态下的文件系统函数,最好由用户态的程序完成,提交给内核态下文件系统函数的路径参数应该是由用户态程序转换后的绝对路径。

​ 本节将用户键入的路径,无论是绝对路径,还是相对路径,一律转换成不含“.”和“..”的绝对路径,然后再把转换后的路径作为命令的参数,最后再对某些有默认参数的命令做针对性处理就好了。

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-05-21 13:21:07
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-05-24 13:54:19
 * @FilePath: /OS/chapter15/15.4/shell/buildin_cmd.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include "buildin_cmd.h"
#include "stdint.h"
#include "stdio.h"
#include "debug.h"
#include "global.h"
#include "fs.h"
#include "string.h"
#include "syscall.h"
#include "assert.h"
#include "dir.h"
#include "shell.h"

/*将路径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中只有一个'/',即表示已经到了顶层目录,就将下一个字符置为结束符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);
}

wash_path:接受两个参数,转换齐纳的旧绝对路径old_abs_path和转换后的新绝对路径new_abs_path,功能是将路径old_abs_path 中的“..”和“.”转换为实际路径后存入 new_abs_path

wash_path 的原理是调用函数 path_parse 从左到右解析 old_abs_path 路径中的每一层,若解析出来的目录名不是“..”就将其连接到 new_abs_path,若是“..”,就将 new_abs_path 的最后一层目录去掉。强调一下, new_abs_path 才是转换后的绝对路径的结果,在路径解析中遇到“..”时就是去修改 new_abs_path

make_clear_abs_path将路径(包含相对路径与绝对路径两种)处理成不含…和.的绝对路径,存储在final_path中。核心原理:判断输入路径是相对路径还是绝对路径,如果是相对路径,调用getcwd获得当前工作目录的绝对路径,将用户输入的路径追加到工作目录路径之后形成绝对目录路径,将其作为参数传给wash_path进行路径转换。

my_shell增加测试代码:

#include "buildin_cmd.h"

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++;
        }
    }
    PANIC("my_shell: should not be here");
}
15.4.6 实现ls、cd、mkdir、ps、rm等命令

命令分为两大类,一种是外部命令,另一种是内部命令。

外部命令是指该命令是个存储在文件系统上的外部程序,执行该命令实际上是从文件系注上加载该程序到内存后运行的过程,也就是说外部命令会以进程的方式执行。

内部命令也称为内建命令,是系统本身提供的功能,它们并不以单独的程序文件存在,只是一些单独
的功能函数,系统执行这些命令实际上是在调用这些函数。

现在我们要用内部函数的方式来实现上诉的一些命令,每个内建函数都会传入两个参数:

  1. uint32_t argc: 这个参数表示传入到该函数的参数个数。在命令 ls -l 中,ls 是命令,而 -lls的参数。在这个例子中,argc 就是2,因为有两个参数:ls-l
  2. char** argv: 这是一个指向字符串数组的指针,代表传入的参数值。argv 的每一个元素都是一个字符串,表示命令行上的一个参数。

buildin_pwd就是调用了getcwd

#include "shell.h"
#include "stdio.h"


/* 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");
        }
    }
}

buildin_cd就是调用了make_clear_abs_path解析argv[1]成绝对路径,然后调用chdir来切换目录

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

buildin_ls:用于列出文件或目录

函数核心原理:

  1. 命令行参数解析:

    使用while循环遍历所有的命令行参数argv,并进行以下处理:

    • 如果参数以 - 开头,那么它被视为一个选项。目前支持两个选项:-l-h。其中 -l 选项使信息以长格式输出,而 -h 选项则打印帮助信息
    • 如果参数不是一个选项,则被视为一个路径参数。函数只支持一个路径参数。
  2. 设置默认路径:
    如果用户未提供路径参数,函数将使用当前工作目录作为默认路径。

  3. 获取文件或目录状态:
    使用 stat 函数检查指定路径文件或目录的状态。如果路径不存在,函数将打印错误信息并返回。

  4. 目录处理:

    如果指定的路径是一个目录:

    • 打开这个目录。
    • 如果使用了 -l 选项,则以长格式输出目录中的每个目录项。这包括文件类型(目录或普通文件)、i节点号、文件大小和文件名。
    • 如果没有使用 -l 选项,则只输出文件名。
    • 最后,关闭目录。
  5. 文件处理:

    如果指定的路径是一个文件:

    • 如果使用了 -l 选项,则以长格式输出文件的信息。
    • 如果没有使用 -l 选项,则只输出文件名。
    /*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],它是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 infomation.\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 firectory\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);
            }
        }
    }
    

buildin_ps就是调用ps

/*ps命令内建函数*/
void buildin_ps(uint32_t argc, char** argv UNUSED){
    if(argc != 1){
        printf("ps: no argument support!\n");
        return;
    }
    ps();
}

buildin_clear就是调用clear

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

buildin_mkdir就是调用make_clear_abs_path解析argv[1]成绝对路径,然后调用mkdir

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

buildin_rmdir就是调用make_clear_abs_path解析argv[1]成绝对路径,然后调用rmdir

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

my_shell就是增加了通过判断arg[0](这个是要调用的命令名)是什么,然后对应调用内建函数

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值