文章目录
- fork 的原理与实现
- 简介
- 什么是 fork
- fork 的实现
- 思路
- 代码
- get_a_page_without_opvaddrbitmap
- copy_pcb_vaddrbitmap_stack0
- copy_body_stack3
- build_child_stack
- update_inode_open_cnts
- copy_process
- sys_fork
- 添加 fork 系统调用与实现 init 进程
- 添加 fork 系统调用
- 实现 init 进程
- shell 的前置准备工作
- 实现一个简单的 shell
- readline / print_prompt 函数
- 添加 Ctrl+u 和 Ctrl+l 快捷键
- cmd_str 分割输入的指令,相对于高级语言中字符串的 split 函数。
- 解析路径
- 实现 ls、c、mkdir、ps、rm 等指令
- shell 主程序
- 现在 Shell 的问题
fork 的原理与实现
简介
pid_t fork( void);
pid_t 是一个宏定义,其实质是int 被定义在 #includesys/types.h>
中。
返回值: 若成功调用一次则返回两个值,其返回值的意义如下
-
0
:子进程 -
子进程ID,即 >0
:父进程 -
-1
:错误
fork 的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。(相关函数请看 fork 的实现 -> 思路
)
什么是 fork
#include <unistd.h>
#include <stdio.h>
int main() {
printf("I will fork in 5 seconds.\n");
sleep(5);
int pid = fork();
if(pid == -1) {
printf("fork error!\n");
return 1;
}
if(pid) {
printf("[fork->pid=%d] I am father, my pid is %d\n.", pid, getpid());
sleep(5);
return 0;
} else {
printf("[fork->pid=%d] I am child, my pid is %d\n.", pid, getpid());
sleep(5);
return 0;
}
}
需要打开两个终端,一个用于运行程序,一个用于查看进程信息。
终端一:运行测试程序
终端二:查看进程信息
一开始并无相关进程信息,接下来执行测试程序,首先执行父进程,得到 pid=8917
,休眠 5s 后执行 fork() 函数,之后会发现输出了两条语句,也就是说 fork 返回一次返回了两个不同的返回值,为什么呢?
看进程信息,fork 后多了一个“子进程”,其 pid=8932
,也就是说 fork 其实是将父进程克隆了一份,而进程拥有独立的地址空间,因此两个进程执行的是独立且相同的代码,所以它们并不共享同一内存空间,也就是执行的是两套代码(代码是相同的,只是有两套而已)。
看输出的结果,是先执行的父进程,其 fork 返回父进程的 pid,其次执行的才是子进程,其子进程 fork 返回的是 0。
注意,这里并不是父子进程各调用一次,父子进程合计才调用了一次 fork,也就是说执行一次 fork 会返回两个值,只是返回的地方不一样,一次是在父进程,一次是在父进程。
fork 的实现
思路
前面说明了 fork 本质是将父进程克隆了一份,称为子进程。首先我们需要明确要复制的资源,复制完成后让处理器的 cs:eip 指向新进程的指令部分。
实现 fork 分两步:
- 先复制进程资源。
- 让处理器的 cs:eip 指向子进程的指令部分(也就是程序代码部分)。
明确要复制的资源有:
- 进程的 PCB,即 task_struct。相关函数:
copy_pcb_vaddrbitmap_stack0
- 程序体,即代码段、数据段等,这是进程的实体。相关函数:
copy_body_stack3
- 用户栈,编译器会把局部变量存入栈,而调用函数也需要压栈执行。相关函数:
copy_body_stack3
- 内核栈,进入内核态时,一方面要用它来保护上下文环境,而另一方面和用户栈一样。相关函数:
copy_pcb_vaddrbitmap_stack0
- 虚拟地址池,每个进程都拥有独立的内存空间,其虚拟地址是虚拟地址池来管理的。相关函数:
copy_pcb_vaddrbitmap_stack0
- 页表,让进程拥有独立的内存空间。相关函数:
create_page_dir
克隆后的进程要如何执行:将新进程加入到就绪队列就可以了,当然要提前把相关的栈准备好。相关函数:build_child_stack
注意:复制完 PCB 后,要记得文件描述符所对应打开的文件的次数要增加。相关函数:
update_inode_open_cnts
代码
get_a_page_without_opvaddrbitmap
kernel/memory.c
// 安装 1页 大小的 vaddr,专门针对 fork 时虚拟地址位图无需操作的情况
void* get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr) {
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
lock_acquire(&mem_pool -> lock);
void* page_phyaddr = palloc(mem_pool);
if(page_phyaddr == NULL) {
lock_release(&mem_pool -> lock);
return NULL;
}
page_table_add((void*) vaddr, page_phyaddr);
lock_release(&mem_pool -> lock);
return (void*) vaddr;
}
copy_pcb_vaddrbitmap_stack0
userprog/fork.c
// 将父进程的 PCB、虚拟地址位图拷贝给子进程
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct* child_thread, struct task_struct* parent_thread) {
// 复制 PCB 所在的整个页,里面包含了进程 PCB 信息以及特权级0的栈
memcpy(child_thread, parent_thread, PG_SIZE);
child_thread -> pid = fork_pid();
child_thread -> elapsed_ticks = 0;
child_thread -> status = TASK_READY;
child_thread -> ticks = child_thread -> priority; // 重置时间片,将其填满
child_thread -> parent_pid = parent_thread -> pid;
child_thread -> general_tag.prev = child_thread -> general_tag.next = NULL;
child_thread -> all_list_tag.prev = child_thread -> all_list_tag.next = NULL;
block_desc_init(child_thread -> u_block_desc);
// 复制父进程的虚拟内存池位图
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
void* vaddr_btmp = get_kernel_pages(bitmap_pg_cnt);
if(vaddr_btmp == NULL) return -1;
// 将父进程的虚拟内存池位图复制一份给子进程, child_thread 其实也可以换成 parent_thread
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;
}
copy_body_stack3
// 复制子进程的进程体(代码和数据)以及用户栈
static void copy_body_stack3(struct task_struct* child_thread, struct task_struct* parent_thread, void* buf_page) {
uint8_t* vaddr_btmp = parent_thread -> userprog_vaddr.vaddr_bitmap.bits;
uint32_t btmp_byte_len = parent_thread -> userprog_vaddr.vaddr_bitmap.btmp_bytes_len;
uint32_t vaddr_start = parent_thread -> userprog_vaddr.vaddr_start;
uint32_t idx_byte = 0;
uint32_t idx_bit = 0;
uint32_t prog_vaddr = 0;
// 在父进程的用户空间中查找已有数据的页
while(idx_byte < btmp_byte_len) {
if(vaddr_btmp[idx_byte]) { // 逐个字节判断
idx_bit = 0;
while(idx_bit < 8) { // 逐个位判断
if((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte]) {
// 计算虚拟地址
prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start;
// 将父进程所在用户空间中的数据复制到内核缓冲区 buf_page
// 目的是下面切换到子进程的页表后,还能访问到父进程的数据
memcpy(buf_page, (void*) prog_vaddr, PG_SIZE);
// 将页表切换到子进程,目的是避免下面申请内存的函数将 pte 及 pde 安装到父进程的页表中
page_dir_activate(child_thread);
// 申请虚拟地址 prog_vaddr
get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);
// 从内核缓冲区中将父进程数据复制到子进程的用户空间中
memcpy((void*) prog_vaddr, buf_page, PG_SIZE);
// 恢复父进程页表
page_dir_activate(parent_thread);
}
idx_bit++;
}
}
idx_byte++;
}
}
将父进程用户空间中的数据复制到子进程的用户空间。但各用户进程的低3G空间是独立的,因此用户进程不能互相访问彼此的空间,但高1G是内核空间,内核空间是所有用户共享的,因此要把数据从一个进程拷贝到另一个进程,必须要借助内核空间作为数据中转,即先父进程用户空间的数据先复制到位于内核空间的 buf_page 中,最后再将 buf_page 复制到子进程的用户空间中。
这里采用一页一页对拷的形式,即父进程找到一页,子进程就申请一页空间,然后对拷。但不同进程之所以有单独的虚拟地址空间,是因为它们各自有单独的页目录,我们分配内存时,会在页表中产生新的 PTE,若申请的内存跨越 4MB 的页表大小,则还需要在页目录表中创建 PDE,既然是为子进程分配内存,那么就要保证这些 PTE 和 PDE 是创建在子进程的页目录表中的。所以在将 buf_page 的数据拷贝到子进程之前,一定要将页表替换为子进程的页表。
build_child_stack
// 为子进程构建 thread_stack 和修改返回值
static int32_t build_child_stack(struct task_struct* child_thread) {
// -----------------------
// 使子进程 pid 返回 0
// -----------------------
// 获取子进程0级栈栈顶
struct intr_stack* intr_0_stack = (struct intr_stack*)((uint32_t) child_thread + PG_SIZE - sizeof(struct intr_stack));
// 修改子进程的返回值为 0
intr_0_stack -> eax = 0;
// 为 switch_to 构建 struct thread_stack,构建在 intr_stack 之下的空间
uint32_t* ret_addr_in_thread_stack = (uint32_t*)intr_0_stack - 1;
/*** 这三行不是必要的,只是为了梳理thread_stack中的关系 ***/
uint32_t* esi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 2;
uint32_t* edi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 3;
uint32_t* ebx_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 4;
/**********************************************************/
/* ebp在thread_stack中的地址便是当时的esp(0级栈的栈顶),
即esp为"(uint32_t*)intr_0_stack - 5" */
uint32_t* ebp_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 5;
// 更新内存中的数据
// switch_to 的返回地址更新为 intr_exit,直接从中断返回
*ret_addr_in_thread_stack = (uint32_t) intr_exit;
/* 下面这两行赋值只是为了使构建的thread_stack更加清晰,其实也不需要,
* 因为在进入intr_exit后一系列的pop会把寄存器中的数据覆盖 */
*ebp_ptr_in_thread_stack = *ebx_ptr_in_thread_stack =\
*edi_ptr_in_thread_stack = *esi_ptr_in_thread_stack = 0;
/*********************************************************/
// 把构建的 thread_stack 的栈顶作为 switch_to 恢复数据时的栈顶
child_thread -> self_kstack = ebp_ptr_in_thread_stack;
return 0;
}
update_inode_open_cnts
// 更新 inode 打开数,其实就是更新文件被打开的次数
static void update_inode_open_cnts(struct task_struct* thread) {
int32_t local_fd = 3, global_fd = 0;
while(local_fd < MAX_FILES_OPEN_PER_PROC) {
global_fd = thread -> fd_table[local_fd];
ASSERT(global_fd < MAX_FILE_OPEN);
if(global_fd != -1)
file_table[global_fd].fd_inode->i_open_cnt++;
local_fd++;
}
}
copy_process
// 拷贝父进程本身所占资源给子进程(对前面函数的封装罢了)
static int32_t copy_process(struct task_struct* child_thread, struct task_struct* parent_thread) {
// 内核缓冲区,作为父进程用户空间的数据复制到子进程用户空间的中转
void* buf_page = get_kernel_pages(1);
if(buf_page == NULL) return -1;
// 复制父进程的 PCB、虚拟地址位图、内核栈到子进程
if(copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) return -1;
// 为子进程创建页表,此页表仅包含内核空间
child_thread -> pgdir = create_page_dir();
if(child_thread -> pgdir == NULL) return -1;
// 复制父进程的进程体及用户栈给子进程
copy_body_stack3(child_thread, parent_thread, buf_page);
// 构建子进程的 thread_stack 和修改返回值 pid
build_child_stack(child_thread);
// 更新文件(inode)被打开的次数
update_inode_open_cnts(child_thread);
mfree_page(PF_KERNEL, buf_page, 1);
return 0;
}
sys_fork
// fork 子进程,内核线程不可直接调用
pid_t sys_fork(void) {
struct task_struct* parent_thread = running_thread();
struct task_struct* child_thread = get_kernel_pages(1);
if(child_thread == NULL) return -1;
ASSERT(INTR_OFF == intr_get_status() && parent_thread -> pgdir != NULL);
if(copy_process(child_thread, parent_thread) == -1) return -1;
// 添加到就绪队列和所有线程队列,子进程由调度器安排运行
ASSERT(!elem_find(&thread_ready_list, &child_thread->general_tag));
list_append(&thread_ready_list, &child_thread -> general_tag);
ASSERT(!elem_find(&thread_all_list, &child_thread->all_list_tag));
list_append(&thread_all_list, &child_thread -> all_list_tag);
return child_thread -> pid; // 父进程返回子进程的 pid
}
添加 fork 系统调用与实现 init 进程
添加 fork 系统调用
这个没啥好说,老套路走一遍就行。
实现 init 进程
在 Linux 中,init 是用户级进程,它是第一个启动的程序,因此它的 pid=1
,后续的所有进程都是它的孩子,故 init 是所有进程父进程,所以它还负责所有子进程的资源回收。
既然 init 是父进程,也就是说它要主动调用 fork 才能派生出子子孙孙,所以在实现它之前要先实现 fork 系统调用。
kernel/main.c
void init(void);
int main(void) {
put_str("I am kernel.\n");
init_all();
while(1);
return 0;
}
// init 进程
void init(void) {
uint32_t ret_pid = fork();
if(ret_pid)
printf("i am father, my pid is %d, child pid is %d\n", getpid(), ret_pid);
else
printf("i am child, my pid is %d, ret pid is %d\n", getpid(), ret_pid);
while(1);
}
thread/thread.c
extern void init(void);
...
// 初始化线程环境
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
// 线创建第一个用户进程 init
process_execute(init, "init"); // 由于是第一个创建的进程,因此该进程 pid = 1
// 将当前 main 函数创建为线程
make_main_thread();
// 创建 idle 线程
idle_thread = thread_start("idle", 10, idle, NULL);
put_str("thead_init done\n");
}
shell 的前置准备工作
三个新的j基础系统调用:
- read 系统调用,获取键盘输入。
- putchar 系统调用,输出字符。
- clear 系统调用,清空屏幕。
其中 clear 的内核实现是 cls_screen,采用纯汇编实现,代码位于:kernel/print.S
这里就贴出 read 的内核实现函数:
fs/fs.c
// 从文件描述符 fd 指向的文件中读取 count 个字节到 buf,成功返回读取的字节数,到文末返回-1
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {
ASSERT(buf != NULL);
int32_t ret = -1;
if(fd < 0 || fd == stdout_no || fd == stderr_no) { // 标准输出流
printk("sys_read: fd error.\n");
} else if(fd == stdin_no) { // 标准输入流
char* buffer = buf;
uint32_t bytes_read = 0;
while(bytes_read < count) { // 不断的从输入缓冲区中读取数据
*buffer = ioq_getchar(&kdb_buf); // 从键盘缓冲区 kdb_buf 中读取1个字节
bytes_read++;
buffer++;
}
ret = (bytes_read == 0 ? -1 : (int32_t) bytes_read);
} else { // 文件读取
uint32_t _fd = fd_local2global(fd);
ret = file_read(&file_table[_fd], buf, count);
}
return ret;
}
若干个系统操作,添加完后总共有:
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR,
SYS_GETCWD,
SYS_OPEN,
SYS_CLOSE,
SYS_LSEEK,
SYS_UNLINK,
SYS_MKDIR,
SYS_OPENDIR,
SYS_CLOSEDIR,
SYS_CHDIR,
SYS_RMDIR,
SYS_READDIR,
SYS_REWINDDIR,
SYS_STAT,
SYS_PS // ps 指令
};
ps 指令的实现:
- 思路:遍历所有进程队列
thread_all_list
,输出 PCB,即 task_struct 中的相关信息。 - 代码位于:thread/thread.c
- 相关函数:pad_print 填充函数(控制输出的格式),elem2thread_info 核心函数,sys_ps 对 ele2thread_info 函数的封装。
实现一个简单的 shell
操作系统如果想和用户交互,那么就必须知道用户的输入,知道了输入那么就要做出相应的输出,此乃交互。各种操作系统的交互方式不过只是提供了一个“外壳”供用户操作,Window 下有 GUI 图形化界面和命令行窗口,而 Linux 则是通过命令行的形式和用户交互,只是 Linux 的叫法更加直接,就直接称为“Shell”(外壳的英文)。
readline / print_prompt 函数
shell/shell.c
// 输出提示符
void print_prompt(void) {
printf("[xiaoling@localhost %s]$ ", cwd_cache);
}
// 从键盘缓冲区中最多读入 count 个字节到 buf
static void readline(char* buf, int32_t count) {
assert(buf != NULL && count > 0);
char* pos = buf;
while(read(stdin_no, pos, 1) != -1 && (pos - buf) < count) {
switch(*pos) {
// 清空屏幕,保留当前输入
case 'l' - 'a':
*pos = 0; // 重置
clear(); // 清空屏幕
print_prompt(); // 重新打印提示符
printf("%s", buf); // 重新将之前输入的命令再次打印
break;
// 清空当前输入
case 'u' - 'a':
while(buf != pos) {
putchar('\b');
*(pos--) = 0;
}
break;
case '\n':
case '\r':
*pos = 0;
putchar('\n');
return;
case '\b':
if(buf[0] != '\b') {
--pos;
putchar('\b');
}
break;
default:
putchar(*pos);
pos++;
}
}
printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}
添加 Ctrl+u 和 Ctrl+l 快捷键
Ctrl+u
:清空屏幕,但保留正在输入的指令。
Ctrl+l
:清空当前输入。
简单说一下,两者的实现都是 u-a
和 l-a
各自所得差形成的属于 ASC 码表 中的不可见字符,因此不会产生可见字符,利用这个特点作为快捷键。
cmd_str 分割输入的指令,相对于高级语言中字符串的 split 函数。
例如:ls /opt -a
解析完后变成:[ls, /opt, -a]
shell/shell.c
// 将 cmd_str 字符串以 token 字符分割
static int32_t cmd_parse(char* cmd_str, char** argv, char token) {
assert(cmd_str != NULL);
int32_t arg_idx = 0;
while(arg_idx < MAX_ARG_NR) {
argv[arg_idx] = NULL;
arg_idx++;
}
char* next = cmd_str;
int32_t argc = 0;
while(*next) {
// 跳过空格
while(*next == token) next++;
/* 处理最后一个参数后接空格的清空,如:ls dir */
if(*next == 0) break;
argv[argc] = next;
// 获取整个有效参数
while(*next && *next != token) next++;
// 若未结束
if(*next) *next++ = 0; // 则以字符串结束符0来表示一个单词的结束
// 避免越界
if(argc > MAX_ARG_NR) return -1;
argc++; // 下一个参数
}
return argc;
}
解析路径
我们经常在终端中使用相对路径 .
和 ..
,我们需要将这些路径转换成绝对路径 /**/**
,这样便于我们操作。
shell/buildin_cmd.c
// 将路径 old_abs_path 中的 .. 和 . 转换成实际路径存入 new_abs_path
static void wash_path(char* old_abs_path, char* new_abs_path) {
assert(old_abs_path[0] == '/');
char name[MAX_FILE_NAME_LEN] = {0};
char* sub_path = old_abs_path;
sub_path = path_parse(sub_path, name);
if(name[0] == 0) { // 只键入了 /
new_abs_path[0] = '/';
new_abs_path[1] = 0;
return;
}
new_abs_path[0] = 0;
strcat(new_abs_path, "/");
while(name[0]) {
if(!strcmp("..", name)) { // 若解析出来的目录是 ..
char* slash_ptr = strrchr(new_abs_path, '/');
if(slash_ptr != new_abs_path) { // 若 .. 后还未到达顶层,例如 /a/b .. 后为 /a
*slash_ptr = 0;
} else { // 若 .. 后到达了顶层,例如 /a .. 后为 /
*(slash_ptr + 1) = 0;
}
} else if(strcmp(".", name)) { // 若解析出来的目录不是 .
if(strcmp(new_abs_path, "/")) { // 判断顶层是否为 /
strcat(new_abs_path, "/"); // 不是,则追加,这个判断是为了避免开头变成 // 的情况
}
// 追加目录
strcat(new_abs_path, name);
} // 若解析出来的目录是 . 则无需任何操作
// 继续遍历下一层路径
memset(name, 0, MAX_FILE_NAME_LEN);
if(sub_path)
sub_path = path_parse(sub_path, name);
}
}
// 将 path 处理成不含 .. 和 . 的绝对路径,保存到 final_path 中,path 是用户键入的
void make_clear_abs_path(char* path, char* final_path) {
char abs_path[MAX_PATH_LEN] = {0};
// 线判断输入的是否为绝对路径
if(path[0] != '/') {
memset(abs_path, 0, MAX_PATH_LEN);
// 获取当前层的绝对路径
if(getcwd(abs_path, MAX_PATH_LEN) != NULL) {
if(!((abs_path[0] == '/') && (abs_path[1] == 0))) {
strcat(abs_path, "/");
}
}
}
// 将键入的路径 path 拼接到当前层的绝对路径后面
strcat(abs_path, path);
// 将 abs_path 中的 . or .. 转为不含 . or .. 的绝对路径 final_path
wash_path(abs_path, final_path);
}
实现 ls、c、mkdir、ps、rm 等指令
命令分为两类:外部命令、内部命令。
外部命令:存储在文件系统上的外部程序,执行该命令实际上是从文件系统上加载该程序到内存中运行,也就是说外部命令会以进程的方式执行。例如:ls,存储路径为 /bin/ls
。
内部命令(内建命令):是系统本身提供的功能,并不以单独的程序文件存在,只是一些单独功能的函数,执行内部命令实际上就是调用这些函数。例如:cd、fg、jobs 等命令都是由 bash 提供的,因此它们称为 BASH_BUILTINS。
**内部命令的编写规则: **
- 命名方式:前缀
buildin_
+ 命令名 - 形参均是 argc 和 argv,argv 是数组,其 argc 是数组长度,即命令参数的个数。
- 函数实现是调用同功能的系统调用实现的,如 buildin_cd 是调用系统调用 chdir 完成的。
- 在进行系统调用前,调用函数 make_clear_abs_path 把相对路径转为绝对路径。
这里就只贴出 ls 和 ps 的构建函数 shell/buildin_cmd.c
// ls 命令的内建函数
void buildin_ls(uint32_t argc, char** argv) {
char* pathname = NULL;
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
bool long_info = false;
uint32_t arg_path_nr = 0;
uint32_t arg_idx = 1; // 跨过argv[0],因为argv[0]=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 { // 得到路径参数值
if(arg_path_nr == 0) {
pathname = argv[arg_idx];
arg_path_nr = 1;
} else {
printf("ls: only support one path\n");
return;
}
}
arg_idx++;
}
if(pathname == NULL) { // 若没有给明确的路径,则默认当前路径为路径参数
if(getcwd(final_path, MAX_PATH_LEN) != NULL) {
pathname = final_path;
} else {
printf("ls: getcwd for default path failed\n");
return;
}
} else {
make_clear_abs_path(pathname, final_path);
pathname = final_path;
}
// 得到目标文件的属性
if(stat(pathname, &file_stat) == -1) {
printf("ls: cannot access %s: No such file or directory\n", pathname);
return;
}
// 判断文件类型
if(file_stat.st_filetype == FT_DIRECTORY) { // 是目录
struct dir* dir = opendir(pathname);
struct dir_entry* dir_e = NULL;
char sub_pathname[MAX_PATH_LEN] = {0};
uint32_t pathname_len = strlen(pathname);
uint32_t last_char_idx = pathname_len - 1;
memcpy(sub_pathname, pathname, pathname_len);
// 保证路径为 /a/b/c/ 而不是 /a/b/c 这是为了便于后面 stat 读取
if(sub_pathname[last_char_idx] != '/') {
sub_pathname[pathname_len] = '/';
pathname_len++;
}
rewinddir(dir);
if(long_info) {
char ftype;
printf("total: %d\n", file_stat.st_size);
while((dir_e = readdir(dir))) {
ftype = 'd';
if(dir_e -> f_type == FT_REGULAR) ftype = '-';
sub_pathname[pathname_len] = 0;
strcat(sub_pathname, dir_e -> filename); // 拼接文件名到路径后面
memset(&file_stat, 0, sizeof(struct stat));
if(stat(sub_pathname, &file_stat) == -1) {
printf("ls: cannot access %s: No such file or directory\n", dir_e->filename);
return;
}
printf("%c %d %d %s\n", ftype, dir_e->i_no, file_stat.st_size, dir_e->filename);
}
} else {
while((dir_e = readdir(dir))) {
printf("%s ", dir_e -> filename);
}
printf("\n");
}
closedir(dir);
} else {
if(long_info)
printf("- %d %d %s\n", file_stat.st_ino, file_stat.st_size, pathname);
else
printf("%s\n", pathname);
}
}
// mkdir 命令内建函数
int32_t buildin_mkdir(uint32_t argc, char** argv) {
int32_t ret = -1;
if(argc != 2) {
printf("mkdir: only support 1 argument!\n");
} else {
make_clear_abs_path(argv[1], final_path);
if(!strcmp("/", final_path)) return ret; // 不能创建根目录
if(mkdir(final_path) == 0)
ret = 0;
else
printf("mkdir: create directory %s failed.\n", argv[1]);
}
return ret;
}
// ps 命令内建函数
void buildin_ps(uint32_t argc, char** argv UNUSED) {
if(argc != 1) {
printf("ps: no argument support!\n");
return;
}
ps(); // 系统调用,内核实现是 sys_ps,位于 thread/thread.c
}
shell 主程序
// 存储输入的命令
static char cmd_line[MAX_PATH_LEN] = {0};
char final_path[MAX_PATH_LEN] = {0}; // 用于洗路径时的缓冲
// 记录当前操作的所在目录,每次 cd 都会更新这个路径
char cwd_cache[MAX_PATH_LEN] = {0};
char* argv[MAX_ARG_NR]; // argv 必须为全局变量,为了以后 exec 的程序可以访问到参数
int32_t argc = -1; // 参数个数
...
// 简单的 shell
void my_shell(void) {
cwd_cache[0] = '/';
cwd_cache[1] = 0;
while(1) {
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if(cmd_line[0] == 0) continue; // 若只输入了一个回车符
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if(argc == -1) {
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
if(!strcmp("ls", argv[0])) buildin_ls(argc, argv);
else if(!strcmp("cd", argv[0])) {
if(buildin_cd(argc, argv) != NULL) {
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
}
else if(!strcmp("pwd", argv[0])) buildin_pwd(argc, argv);
else if(!strcmp("ps", argv[0])) buildin_ps(argc, argv);
else if(!strcmp("clear", argv[0])) buildin_clear(argc, argv);
else if(!strcmp("mkdir", argv[0])) buildin_mkdir(argc, argv);
else if(!strcmp("rmdir", argv[0])) buildin_rmdir(argc, argv);
else if(!strcmp("rm", argv[0])) buildin_rm(argc, argv);
else printf("external command.\n");
}
panic("my_shell: should not be here");
}
现在 Shell 的问题
看!我们通过 if-else 的形式来判断用户输入命令,若每个命令都要这样判断,工作量大不说,我们的外部命令也无法执行,因为我们的外部命令的命令名是未知的,这是无法预判的。
或许这可以通过 exec 解决?
但…我无法继续走下去了,因为…