第15章-系统交互
这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件
fork
函数的原型是pid_t fork(void)
,父进程返回子进程的PID
,子进程返回0。
fork
利用老进程克隆出一个新进程并使新进程执行,新进程之所以能够执行,本质上是它具备程序体,这其中包括代码和数据等资源。因此 fork
就是把某个进程的全部资源复制了一份,然后让处理器的 cs:eip
寄存器指向新进程的指令部分。故实现 fork
也要分两步,先复制进程资源,然后再跳过去执行。
进程资源:
pcb
,即task_struct
,这是让任务有“存在感”的身份证。 在真正编写 fork
代码之前,咱们还要增加一些基础设施,首先在 thread.h
的 task_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_stack
)eax
的位置,这样中断退出(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_fork
即 fork
的内核实现部分, fork
本身是无参数的,因此 sys_fork
也无参数。 函数先调用 get_kernel_pages(1
)获得 1 页内核空间作为子进程的 pcb
。接下来调用 copy_process
复制父进程的信息到子进程,然后将其加入到就绪队列和全部队列,最后返回子进程的 pid
。
首先添加fork
系统调用步骤如下:
syscall.h
中的enum SYSCALL_NR
结构中添加SYS_FORK
syscall.c
中添加fork()
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");
}
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
从文件中读取内容
/* 向屏幕输出一个字符 */
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");
}
本节主要是实现一个简单的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");
}
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");
}
/*分析字符串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’
到现在咱们的 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");
}
相对路径就是以当前工作路径为基础,命令或参数都是相对于当前工作路径得出的,“当前工作路径”+“相对路径”=“绝对路径” 。
路径输入发生在用户态,而系统调用通过中断的方式发生在内核态,咱们这里反复强调的一句话是操作系统虽是中断驱动的,但我们又希望它不停地运行,故不希望执行中断处理程序的时间过长,因此我们要为内核代码减荷,让它们尽量快点从内核态返回,以处理更多的中断。于是很自然地想到,我们不应该把路径转换的工作交给内核态下的文件系统函数,最好由用户态的程序完成,提交给内核态下文件系统函数的路径参数应该是由用户态程序转换后的绝对路径。
本节将用户键入的路径,无论是绝对路径,还是相对路径,一律转换成不含“.”和“..”的绝对路径,然后再把转换后的路径作为命令的参数,最后再对某些有默认参数的命令做针对性处理就好了。
/*
* @Author: Adward-DYX [email protected]
* @Date: 2024-05-21 13:21:07
* @LastEditors: Adward-DYX [email protected]
* @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");
}
命令分为两大类,一种是外部命令,另一种是内部命令。
外部命令是指该命令是个存储在文件系统上的外部程序,执行该命令实际上是从文件系注上加载该程序到内存后运行的过程,也就是说外部命令会以进程的方式执行。
内部命令也称为内建命令,是系统本身提供的功能,它们并不以单独的程序文件存在,只是一些单独
的功能函数,系统执行这些命令实际上是在调用这些函数。
现在我们要用内部函数的方式来实现上诉的一些命令,每个内建函数都会传入两个参数:
uint32_t argc
: 这个参数表示传入到该函数的参数个数。在命令 ls -l
中,ls
是命令,而 -l
是ls
的参数。在这个例子中,argc
就是2,因为有两个参数:ls
和 -l
。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
:用于列出文件或目录
函数核心原理:
命令行参数解析:
使用while
循环遍历所有的命令行参数argv
,并进行以下处理:
-
开头,那么它被视为一个选项。目前支持两个选项:-l
和 -h
。其中 -l
选项使信息以长格式输出,而 -h
选项则打印帮助信息设置默认路径:
如果用户未提供路径参数,函数将使用当前工作目录作为默认路径。
获取文件或目录状态:
使用 stat
函数检查指定路径文件或目录的状态。如果路径不存在,函数将打印错误信息并返回。
目录处理:
如果指定的路径是一个目录:
-l
选项,则以长格式输出目录中的每个目录项。这包括文件类型(目录或普通文件)、i节点号、文件大小和文件名。-l
选项,则只输出文件名。文件处理:
如果指定的路径是一个文件:
-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");
}