配合视频学习体验更佳!
a小节:https://www.bilibili.com/video/BV1gk4y1w7u1/?vd_source=701807c4f8684b13e922d0a8b116af31
b小节:https://www.bilibili.com/video/BV1Mm4y1N7fS/?vd_source=701807c4f8684b13e922d0a8b116af31
c小节:https://www.bilibili.com/video/BV1Th4y1A7xw/?vd_source=701807c4f8684b13e922d0a8b116af31
d小节:https://www.bilibili.com/video/BV1cm4y1N7XG/?vd_source=701807c4f8684b13e922d0a8b116af31
e小节:https://www.bilibili.com/video/BV1qh4y1Y7gE/?vd_source=701807c4f8684b13e922d0a8b116af31
f小节:https://www.bilibili.com/video/BV1Su4y1675s/?vd_source=701807c4f8684b13e922d0a8b116af31
g小节:https://www.bilibili.com/video/BV1RP41187cD/?vd_source=701807c4f8684b13e922d0a8b116af31
h小节:https://www.bilibili.com/video/BV1mH4y1D7Ze/?vd_source=701807c4f8684b13e922d0a8b116af31
i小节:https://www.bilibili.com/video/BV1rP411t7bd/?vd_source=701807c4f8684b13e922d0a8b116af31
j小节:https://www.bilibili.com/video/BV1Jk4y1F7ob/?vd_source=701807c4f8684b13e922d0a8b116af31
k小节:https://www.bilibili.com/video/BV1pm4y1N7CH/?vd_source=701807c4f8684b13e922d0a8b116af31
代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system
小节a:
fork
这一小节,我们要实现fork
fork是用于复制进程的,也就是根据父进程复制出一个子进程。但是由于他们本质是两个进程,所以还是有很多不相同的地方,比如独立的资源,单独的pid之类的。
有这样一段代码
#include <unistd.h>
#include <stdio.h>
int main() {
int pid = fork();
if (pid == -1)
return 1;
printf("who am I ? my pid is %d\n", getpid());
sleep (5) ;
return 0;
}
当执行完fork()后,fork之后的代码会由于属于两个进程(调用fork的主进程与被复制出来的子进程)而被执行两次(自然是主进程与子进程各执行一次)。
由于fork复制进程,而且复制步骤是在fork自己的代码结束前就完成(假设fork代码1000行,第800行就完成了复制),所以fork代码最后一行的return 就会被执行两次。对于父进程来说,fork会返回子进程pid。对于子进程来说,fork会返回0。我们就可以根据fork返回的不同值来区别父子进程,以让父子进程执行不同的代码。比如:
if (pid) {
printf("I am father, my pid is d\n",getpid());
sleep(5);
return 0;
}
else {
printf("I am child, my pid is d\n",getpid());
sleep(5);
return 0;
}
现在开始实现fork,先实现一些基础设施函数
fork_pid
就是封装了allocate_pid
,因为allocate_pid
之前实现的时候有关键字static,所以作者为了不去修改这个,就采取了进一步封装
修改(myos/thread/thread.c)
/* fork进程时为其分配pid,因为allocate_pid已经是静态的,别的文件无法调用.
不想改变函数定义了,故定义fork_pid函数来封装一下。*/
pid_t fork_pid(void)
{
return allocate_pid();
}
函数声明,修改(myos/thread/thread.h)
pid_t fork_pid(void);
get_a_page_without_opvaddrbitmap
用于为指定的虚拟地址创建物理页映射,与get_a_page
相比,少了操作进程pcb中的虚拟内存池位图
修改(myos/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;
}
函数声明,修改(myos/kernel/memory.h)
void *get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr);
copy_pcb_vaddrbitmap_stack0
用于根据传入的父子进程pcb指针,先复制整个父进程pcb内容到子进程pcb中,然后再针对设置子进程pcb内容,包含:pid, elapsed_ticks, status, ticks, parent_pid, general_tag, all_list_tag, u_block_desc, userprog_vaddr(让子进程拥有自己的用户虚拟地址空间内存池,但是其位图是拷贝父进程的)。这个过程中,内核栈中的内容被完全拷贝了。
(myos/userprog/fork.c)
#include "fork.h"
#include "stdint.h"
#include "global.h"
#include "thread.h"
#include "string.h"
#include "debug.h"
#include "process.h"
/* 将父进程的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;
}
copy_body_stack3
用于根据传入的父子进程pcb指针,复制进程的用户空间堆与栈中的数据。核心原理:遍历父进程的userprog_vaddr当中的虚拟地址空间位图,来判断父进程的用户虚拟地址空间中是否有数据。如果有,就拷贝到内核空间的中转区中,然后调用page_dir_activate
,切换到子进程页表,调用get_a_page_without_opvaddrbitmap
为子进程特定虚拟地址申请一个物理页(其中并不涉及子进程userprog_vaddr中的位图修改),然后从内核中转区中把数据拷贝到子进程相同的虚拟地址内。
修改(myos/userprog/fork.c)
extern void intr_exit(void);
/* 复制子进程的进程体(代码和数据)及用户栈 */
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++;
}
}
build_child_stack
用于修改子进程的返回值和设定其内核栈。子进程返回0原理:我们之前构建系统调用机制时,系统调用的返回值会放入内核栈中的中断栈(intr_stack
)eax的位置,这样中断退出(intr_exit
)就会push eax时将返回值放入eax中。所以我们将子进程的内核栈中断栈eax的值改成0。
我们的子进程上机运行是通过让自己就绪之后,等待某个时钟中断调用switch_to函数上机
mov eax, [esp + 24]
mov esp, [eax]
pop ebp
pop ebx
pop edi
pop esi
ret
switch_to会从子进程的pcb中找到内核栈的栈顶放入esp中,然后执行switch_to的那4条pop和ret指令,我们现在经过拷贝后的子进程内核栈布局如图:
所以,我们直接去用子进程这样的内核栈布局肯定不行,要人为去修改成
也就是在intr_stack前面增加switch_to栈(也就是书p694提到的thread_stack),让pcb最顶端的esp指向switch_to栈栈顶,并且switch_to栈中返回地址要填上intr_exit
函数地址。这样执行ret之后,就能去执行intr_exit
,并利用intr_stack执行中断返回,由于intr_stack中拷贝了父进程进入中断时的用户栈信息,cs: ip 信息,所以中断退出后,子进程将会继续执行父进程之后的代码。
修改(myos/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;
}
update_inode_open_cnts
由于fork出来的子进程几乎和父进程一样,所以父进程打开的文件,子进程也要打开。所以,父进程的全局打开文件结构中记录文件打开的次数都需要 + 1。原理:遍历进程pcb(父,子均可)中的文件描述符,找到对应的全局打开文件结构索引就行了
修改(myos/user/fork.c)
#include <file.h>
/* 更新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++;
}
}
copy_process
就是fork时用于复制父进程资源的函数,就是前面函数的封装。原理:调用copy_pcb_vaddrbitmap_stack0
复制父进程的pcb、虚拟地址位图、内核栈到子进程;然后调用create_page_dir
为子进程创建页表,这个页表已经包含了内核地址空间的映射;然后调用copy_body_stack3
复制进程的用户空间堆与栈中的数据;然后调用build_child_stack
用于修改子进程的返回值和设定其内核栈;最后调用update_inode_open_cnts
更新inode的打开数。
修改(myos/user/fork.c)
/* 拷贝父进程本身所占资源给子进程 */
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;
}
sys_fork
用于复制出一个进程,并将其加入就绪队列
(myos/userprog/fork.c)
#include "interrupt.h"
/* 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
}
函数声明(myos/userprog/fork.h)
#ifndef __USERPROG_FORK_H
#define __USERPROG_FORK_H
#include "stdint.h"
pid_t sys_fork(void);
#endif
然后我们添加fork
系统调用
添加系统调用号,修改(myos/lib/user/syscall.h)
#include "thread.h"
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK
};
用户态系统调用入口,修改(myos/lib/user/syscall.c)
#include "thread.h"
/* 派生子进程,返回子进程pid */
pid_t fork(void)
{
return _syscall0(SYS_FORK);
}
函数声明,修改(myos/lib/user/syscall.h)
pid_t fork(void);
系统调用表中添加实际系统调用函数,修改(myos/userprog/syscall-init.c)
#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的用户进程,所以必须要放在主线程创建之创建。后续所有的进程都是它的孩子,它还负责所有子进程的资源回收
修改(myos/thread/thread.c/thread_init),让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");
}
测试代码与init
进程实现
(myos/kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
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)
;
}
编译运行会报页错误,经过排查,修改(myos/thread/thread.c/thread_create)
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
// pthread->self_kstack -= sizeof(struct intr_stack); //-=结果是sizeof(struct intr_stack)的4倍
// self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));
/* 再留出线程栈空间,可见thread.h中定义 */
// pthread->self_kstack -= sizeof(struct thread_stack);
pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));
为
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
pthread->self_kstack -= sizeof(struct intr_stack); //-=结果是sizeof(struct intr_stack)的4倍
// self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
// pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));
/* 再留出线程栈空间,可见thread.h中定义 */
pthread->self_kstack -= sizeof(struct thread_stack);
// pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));
典型的程序依靠错误运行,暂不知道为何错误
小节b:
获取键盘输入
sys_read
用于从指定文件描述符中获取conunt字节数据,如果文件描述符是stdin_no,那么直接循环调用ioq_getchar
从键盘获取内容,否则调用file_read
从文件中读取内容
sys_put_char
用于向屏幕输出一个字符
修改(myos/fs/fs.c/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;
}
/* 向屏幕输出一个字符 */
void sys_putchar(char char_asci)
{
console_put_char(char_asci);
}
函数声明,修改(myos/fs/fs.h)
void sys_putchar(char char_asci);
cls_screen
用于清空屏幕,核心原理:向代表80列×25行,共2000个字符位置的内存写入空格符,然后设定光标位置为左上角(即位置0)
修改(myos/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
函数声明,修改(myos/lib/kernel/print.h)
void cls_screen(void);
将sys_read
、sys_putchar
、cls_screen
做成系统调用
添加系统调用号,修改(myos/lib/user/syscall.h)
enum SYSCALL_NR
{
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR
};
准备好read
、put_char
与clear
的用户态入口,修改(myos/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);
}
/* 输出一个字符 */
void putchar(char char_asci)
{
_syscall1(SYS_PUTCHAR, char_asci);
}
/* 清空屏幕 */
void clear(void)
{
_syscall0(SYS_CLEAR);
}
然后声明函数,修改(myos/lib/user/syscall.h)
int32_t read(int32_t fd, void* buf, uint32_t count);
void putchar(char char_asci);
void clear(void);
将系统调用实际执行程序,添加至系统调用表中,修改(myos/lib/user/syscall.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] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
put_str("syscall_init done\n");
}
shell是用户与操作系统之间交互的接口,我们天天使用的Linux终端就是一个shell。它的功能就是获取用户的键盘输入,然后从中解析命令,然后根据命令去执行对应的动作。
print_prompt
用于输出命令提示符,也就是我们在终端输入命令时,前面那串字符
(myos/shell/shell.c)
#include "shell.h"
#include "stdio.h"
char cwd_cache[64] = {0};
/* 输出提示符 */
void print_prompt(void)
{
printf("[rabbit@localhost %s]$ ", cwd_cache);
}
readline
循环调用read
从键盘输入缓冲读取字符,每次读取一个,最多读入count个字节到buf。根据每次读入的值不同,处理方式也不同:/n,/r表示按下enter键,用户输入命令结束,缓冲区输入个0表示命令字符串结尾。/b表示按下退格键,就删除一个字符。普通字符就直接读入buf。每种字符都调用了putchar
进行打印,是因为我们的键盘中断处理函数已经删除打印功能。
修改(myos/shell/shell.c)
#include "file.h"
#include "debug.h"
#include "syscall.h"
/* 从键盘缓冲区中最多读入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");
}
my_shell
就是shell进程,不断循环:调用print_prompt
输出命令提示符,然后调用readline
获取用户输入
修改(myos/shell/shell.c)
#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");
}
函数声明,(myos/shell/shell.h)
#ifndef __KERNEL_SHELL_H
#define __KERNEL_SHELL_H
void print_prompt(void);
void my_shell(void);
#endif
我们让init
来开启shell
,修改(myos/kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "debug.h"
#include "shell.h"
#include "console.h"
void init(void);
int main(void) {
put_str("I am kernel\n");
init_all();
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while(1);
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
while (1)
;
}
else
{ // 子进程
my_shell();
}
PANIC("init: should not be here");
}
删除(myos/device/keyboard.c/intr_keyboard_handler)中的打印语句
put_char(cur_char); // 临时的
makefile记得新增包含静态库
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog/ -I fs/ -I shell/
这里的问题:
1、main函数中有打印命令提示符的语句,而init_all
中调用thread_init
调用process_execute
创建了init进程,init运行时会fork出只调用shell的进程,这个进程会调用print_prompt
打印命令提示符,这就和main当中打印是冲突了的。要想实现书上的效果,那么fork出运行shell的进程调用print_prompt
必须在main
调用cls_screen
之前。这依赖于特定的任务执行顺序,不过一般不会出错。
小节c:
添加快捷键
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
读出这两种情况的处理逻辑即可
修改(myos/shell/shell.c)
/* 从键盘缓冲区中最多读入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");
}
小节d:
解析键入的字符
cmd_parse
分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组。这个函数就是个字符串处理函数,从诸如 'ls dir ’ 这样的命令中拆单词,拆成 ‘ls’ 与 ‘dir’
修改(myos/shell/shell.c)
#define MAX_ARG_NR 16 // 加上命令名外,最多支持15个参数
/* 分析字符串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;
}
my_shell
增加测试代码, 输出每个分离出来的单词
修改(myos/shell/shell.c)
char *argv[MAX_ARG_NR]; // argv必须为全局变量,为了以后exec的程序可访问参数
char final_path[MAX_PATH_LEN] = {0}; // 用于洗路径时的缓冲
int32_t argc = -1;
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");
}
小节e:
实现输入命令,然后调用对应的函数
先实现一个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中的信息
修改(myos/thread/thread.c)
#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);
}
添加函数声明,修改(myos/thread/thread.h)
void sys_ps(void);
然后将上一章和本章实现的sys开头的函数,全部封装成系统调用
首先添加系统调用号,修改(myos/lib/user/syscall.h)
#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
};
然后实现它们的用户态入口,修改(myos/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);
}
添加系统调用用户态入口函数声明,修改修改(myos/lib/user/syscall.h)
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);
最后在系统调用表中添加真正的系统调用执行函数,修改(myos/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] = 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");
}
操作系统为了方便用户使用,一般都会提供相对路径功能。比如我们当前工作路径是/home/kanshan/Desktop,我们想要运行一个编译好的程序输入./test,实际上是被操作系统解析成了/home/kanshan/Desktop/test,也就是当前工作路径 + 相对路径 = 绝对路径。
wash_path
将路径old_abs_path(这是调用者提供的绝对路径)中的…和.转换为实际路径后存入new_abs_path。例如,给定路径/a/b/..
应被转换成/a
。给定路径/a/b/.
应被转换成/a/b
。核心原理:调用path_parse
解析路径,如果是..
,则退回上一层路径。如果是.
,则什么都不做。带入一个例子,比如/a/../home/.
就可以明白次函数如何工作
make_clear_abs_path
将路径(包含相对路径与绝对路径两种)处理成不含…和.的绝对路径,存储在final_path中。核心原理:判断输入路径是相对路径还是绝对路径,如果是相对路径,调用getcwd获得当前工作目录的绝对路径,将用户输入的路径追加到工作目录路径之后形成绝对目录路径,将其作为参数传给wash_path
进行路径转换。
#include "buildin_cmd.h"
#include "debug.h"
#include "dir.h"
#include "string.h"
#include "fs.h"
#include "syscall.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中只有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);
}
问题1:代码中:new_abs_path[0] = 0;意义何在?
确保了后续在new_abs_path
上的任何字符串连接(例如通过strcat
函数)都会从头开始。
函数声明,(myos/shell/buildin_cmd.h)
#ifndef __SHELL_BUILDIN_CMD_H
#define __SHELL_BUILDIN_CMD_H
void make_clear_abs_path(char *path, char *final_path);
#endif
支持代码,修改(myos/fs/fs.c)
static char *path_parse(char *pathname, char *name_store)
为
char *path_parse(char *pathname, char *name_store)
my_shell
增加测试代码,修改(myos/shell/shell.c)
#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");
}
小节f:
实现一系列内建命令
shell命令分为外部命令与内部命令。执行外部命令,实际上就是执行了一个进程。而内部命令,就是执行操作系统自带的函数。我们现在来实现一系列内部命令所需要的内建函数。
每个内建函数都会传入两个参数:
uint32_t argc
: 这个参数表示传入到该函数的参数个数。在命令ls -l
中,ls
是命令,而-l
是ls
的参数。在这个例子中,argc
就是2,因为有两个参数:ls
和-l
。char** argv
: 这是一个指向字符串数组的指针,代表传入的参数值。argv
的每一个元素都是一个字符串,表示命令行上的一个参数。
buildin_pwd
就是调用了getcwd
修改(myos/shell/buildin_cmd.c)
#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");
}
}
}
支持代码,修改(myos/shell/shell.h)
#include "fs.h"
extern char final_path[MAX_PATH_LEN];
buildin_cd
就是调用了make_clear_abs_path
解析argv[1]
成绝对路径,然后调用chdir
来切换目录
修改(myos/shell/buildin_cmd.c)
/* 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
选项,则只输出文件名。
- 如果使用了
修改(myos/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
{ // 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);
}
}
}
buildin_ps
就是调用ps
修改(myos/shell/buildin_cmd.c)
/* ps命令内建函数 */
void buildin_ps(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("ps: no argument support!\n");
return;
}
ps();
}
buildin_clear
就是调用clear
修改(myos/shell/buildin_cmd.c)
/* clear命令内建函数 */
void buildin_clear(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("clear: no argument support!\n");
return;
}
clear();
}
buildin_mkdir
就是调用make_clear_abs_path
解析argv[1]
成绝对路径,然后调用mkdir
修改(myos/shell/buildin_cmd.c)
/* 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_rmdir
就是调用make_clear_abs_path
解析argv[1]
成绝对路径,然后调用rmdir
修改(myos/shell/buildin_cmd.c)
/* 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_rm
就是调用make_clear_abs_path
解析argv[1]
成绝对路径,然后调用unlink
修改(myos/shell/buildin_cmd.c)
/* 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;
}
函数声明,修改(myos/shell/buildin_cmd.h)
#include "global.h"
void buildin_pwd(uint32_t argc, char **argv UNUSED);
char *buildin_cd(uint32_t argc, char **argv);
void buildin_ls(uint32_t argc, char **argv);
void buildin_ps(uint32_t argc, char **argv UNUSED);
void buildin_clear(uint32_t argc, char **argv UNUSED);
int32_t buildin_mkdir(uint32_t argc, char **argv);
int32_t buildin_rmdir(uint32_t argc, char **argv);
int32_t buildin_rm(uint32_t argc, char **argv);
my_shell
就是增加了通过判断arg[0](这个是要调用的命令名)是什么,然后对应调用内建函数
修改(myos/shell/shell.c)
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");
}
小节g:
加载用户进程
segment_load
将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存。核心原理:我们编译程序后,编译器已经指定好了可加载段的虚拟地址,我们直接按照这个虚拟地址,把段加载到内存中对应的虚拟地址就可以了。由于这个函数是fork
之后从磁盘编译好的程序加载可加载段时使用,所以我们使用的是调用fork的进程的页表,所以我们要判断目的内存虚拟地址是否在页表中有效,如果无效,则为指定虚拟地址申请物理内存。申请内存完毕,我们调用sys_read
从磁盘中加载可加载段到指定内存虚拟地址中即可。
(myos/userprog/exec.c)
#include "exec.h"
#include "stdint.h"
#include "global.h"
#include "memory.h"
#include "fs.h"
typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;
/* 32位elf头 */
struct Elf32_Ehdr
{
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
};
/* 程序头表Program header.就是段描述头 */
struct Elf32_Phdr
{
Elf32_Word p_type; // 见下面的enum segment_type
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
};
/* 段类型 */
enum segment_type
{
PT_NULL, // 忽略
PT_LOAD, // 可加载程序段
PT_DYNAMIC, // 动态加载信息
PT_INTERP, // 动态加载器名称
PT_NOTE, // 一些辅助信息
PT_SHLIB, // 保留
PT_PHDR // 程序头表
};
/* 将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存 */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz, uint32_t vaddr)
{
uint32_t vaddr_first_page = vaddr & 0xfffff000; // vaddr地址所在的页框
uint32_t size_in_first_page = PG_SIZE - (vaddr & 0x00000fff); // 加载到内存后,文件在第一个页框中占用的字节大小
uint32_t occupy_pages = 0;
/* 若一个页框容不下该段 */
if (filesz > size_in_first_page)
{
uint32_t left_size = filesz - size_in_first_page;
occupy_pages = DIV_ROUND_UP(left_size, PG_SIZE) + 1; // 1是指vaddr_first_page
}
else
{
occupy_pages = 1;
}
/* 为进程分配内存 */
uint32_t page_idx = 0;
uint32_t vaddr_page = vaddr_first_page;
while (page_idx < occupy_pages)
{
uint32_t *pde = pde_ptr(vaddr_page);
uint32_t *pte = pte_ptr(vaddr_page);
/* 如果pde不存在,或者pte不存在就分配内存.
* pde的判断要在pte之前,否则pde若不存在会导致
* 判断pte时缺页异常 */
if (!(*pde & 0x00000001) || !(*pte & 0x00000001))
{
if (get_a_page(PF_USER, vaddr_page) == NULL)
{
return false;
}
} // 如果原进程的页表已经分配了,利用现有的物理页,直接覆盖进程体
vaddr_page += PG_SIZE;
page_idx++;
}
sys_lseek(fd, offset, SEEK_SET);
sys_read(fd, (void *)vaddr, filesz);
return true;
}
load
根据传入的路径,加载磁盘中的程序的可加载段,最后返回程序入口地址。原理:编译好的程序在磁盘中,起始就是ELF header,我们去把这个读出来,从中得到program header的偏移、数量、每个大小。然后我们根据这些信息去循环读出program header,根据每个program header信息去调用segment_load
将可加载段加载到内存中。
修改(myos/userprog/exec.c)
#include "string.h"
/* 从文件系统上加载用户程序pathname,成功则返回程序的起始地址,否则返回-1 */
static int32_t load(const char *pathname)
{
int32_t ret = -1;
struct Elf32_Ehdr elf_header;
struct Elf32_Phdr prog_header;
memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));
int32_t fd = sys_open(pathname, O_RDONLY);
if (fd == -1)
{
return -1;
}
if (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) != sizeof(struct Elf32_Ehdr))
{
ret = -1;
goto done;
}
/* 校验elf头 */
if (memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) || elf_header.e_type != 2 || elf_header.e_machine != 3 || elf_header.e_version != 1 || elf_header.e_phnum > 1024 || elf_header.e_phentsize != sizeof(struct Elf32_Phdr))
{
ret = -1;
goto done;
}
Elf32_Off prog_header_offset = elf_header.e_phoff;
Elf32_Half prog_header_size = elf_header.e_phentsize;
/* 遍历所有程序头 */
uint32_t prog_idx = 0;
while (prog_idx < elf_header.e_phnum)
{
memset(&prog_header, 0, prog_header_size);
/* 将文件的指针定位到程序头 */
sys_lseek(fd, prog_header_offset, SEEK_SET);
/* 只获取程序头 */
if (sys_read(fd, &prog_header, prog_header_size) != prog_header_size)
{
ret = -1;
goto done;
}
/* 如果是可加载段就调用segment_load加载到内存 */
if (PT_LOAD == prog_header.p_type)
{
if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz, prog_header.p_vaddr))
{
ret = -1;
goto done;
}
}
/* 更新下一个程序头的偏移 */
prog_header_offset += elf_header.e_phentsize;
prog_idx++;
}
ret = elf_header.e_entry;
done:
sys_close(fd);
return ret;
}
在C和C++中,使用\xHH
格式的十六进制转义序列时,需要特别小心,因为这个序列会继续解析所有有效的十六进制数字,直到遇到一个非十六进制数字或序列的长度达到其最大值。字符串 "\x7fELF"
会被解析为一个字符 \x7fE
,然后是 LF
,而不是我们预期的 \x7f
和 ELF
。
sys_execv
用path指向的程序替换当前进程,注意,这个函数是fork
之后调用的。原理:先调用load
加载程序可执行段到内存中,并得到了程序入口地址。然后修改pcb中的数据即可,包括:程序名字、内核栈中中断栈中用于传参的寄存器(该函数运行在内核态下,通过intr_exit
返回到用户态执行新的进程,所以中断栈中的数据会被intr_exit
的push操作送入寄存器,以此达到传参目的)、中断栈中eip用于跳转程序入口、中断栈esp用于设定新进程的栈顶位置(fork
中拷贝了父进程页表、重新申请了物理地址空间用于拷贝用户栈数据,所以并不用担心新进程用户栈用到的虚拟地址没有映射物理地址)。最后通过内联汇编设定esp为中断栈的位置,然后跳转执行intr_exit
,就可以执行新的进程了。
修改(myos/userprog/exec.c)
#include "thread.h"
/* 用path指向的程序替换当前进程 */
int32_t sys_execv(const char *path, const char *argv[])
{
uint32_t argc = 0;
while (argv[argc])
{
argc++;
}
int32_t entry_point = load(path);
if (entry_point == -1)
{ // 若加载失败则返回-1
return -1;
}
struct task_struct *cur = running_thread();
/* 修改进程名 */
memcpy(cur->name, path, TASK_NAME_LEN);
cur->name[TASK_NAME_LEN - 1] = 0;
struct intr_stack *intr_0_stack = (struct intr_stack *)((uint32_t)cur + PG_SIZE - sizeof(struct intr_stack));
/* 参数传递给用户进程 */
intr_0_stack->ebx = (int32_t)argv;
intr_0_stack->ecx = argc;
intr_0_stack->eip = (void *)entry_point;
/* 使新用户进程的栈地址为最高用户空间地址 */
intr_0_stack->esp = (void *)0xc0000000;
/* exec不同于fork,为使新进程更快被执行,直接从中断返回 */
asm volatile("movl %0, %%esp; jmp intr_exit" : : "g"(intr_0_stack) : "memory");
return 0;
}
支持代码,修改(myos/thread/thread.h)
#define TASK_NAME_LEN 16
将sys_execv
做成系统调用
添加系统调用号,修改(myos/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,
SYS_EXECV
};
用户态系统调用入口,修改(myos/lib/user/syscall.c)
int execv(const char *pathname, char **argv)
{
return _syscall2(SYS_EXECV, pathname, argv);
}
声明用户态系统调用入口,修改(myos/lib/user/syscall.h)
int execv(const char* pathname, char** argv);
系统调用表修改,修改(myos/userprog/syscall-init.c)
#include "exec.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;
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;
syscall_table[SYS_EXECV] = sys_execv;
put_str("syscall_init done\n");
}
修改my_shell
增加对于外部命令去磁盘加载编译好的二进制进程序并执行的代码,核心就是先fork
创建子进程,然后子进程调用make_clear_abs_path
解析传入的路径,然后调用execv
去执行
修改(myos/shell/shell.c/my_shell)
#include "syscall.h"
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
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
/* 下面这个while必须要加上,否则父进程一般情况下会比子进程先执行,
因此会进行下一轮循环将findl_path清空,这样子进程将无法从final_path中获得参数*/
while (1)
;
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
}
else
{
execv(argv[0], argv);
}
while (1)
;
}
}
int32_t arg_idx = 0;
while (arg_idx < MAX_ARG_NR)
{
argv[arg_idx] = NULL;
arg_idx++;
}
}
PANIC("my_shell: should not be here");
}
编译好一个用户程序prog_no_arg
,我们需要自行写入hd60M.img,然后用操作系统从hd60M.img中取到编译好的用户程序到内存,再写入有文件系统的hd80M.img,最后再执行prog_no_arg
(myos/command/prog_no_arg.c)
#include "stdio.h"
int main(void)
{
printf("prog_no_arg from disk\n");
while (1)
;
return 0;
}
由于这个程序复用了printf,而printf调用了vsprintf,而vsprintf调用了strcpy,而strcpy调用了ASSERT宏,而ASSERT使用了PANIC,而PANIC使用了panic_spin,而panic_spin使用了,intr_disable()。也就是说,我们调用用户态printf的程序,如果中间strcpy的ASSERT出错,将会直接在用户态调用intr_disable,这是绝对不能允许的,运行会报特权级保护错误!正确的做法是,先通过系统调用切换至内核态,然后再调用intr_disable。
所以,我们先实现用户态使用的assert
(myos/lib/user/assert.c)
#include "assert.h"
#include "stdio.h"
void user_spin(char *filename, int line, const char *func, const char *condition)
{
printf("\n\n\n\nfilename %s\nline %d\nfunction %s\ncondition %s\n", filename, line, func, condition);
while (1)
;
}
(myos/lib/user/assert.h)
#ifndef __LIB_USER_ASSERT_H
#define __LIB_USER_ASSERT_H
#include "global.h"
void user_spin(char *filename, int line, const char *func, const char *condition);
#define panic(...) user_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)
#ifdef NDEBUG
#define assert(CONDITION) ((void)0)
#else
#define assert(CONDITION) \
if (!(CONDITION)) \
{ \
panic(#CONDITION); \
}
#endif /*NDEBUG*/
#endif /*__LIB_USER_ASSERT_H*/
如此一来,我们的assert
判断出错,将会通过printf
内的write
系统调用正常进入内核态
我们去把内核中用户态程序用到的ASSERT与PANIC都改掉
修改(myos/lib/string.c)中所有的ASSERT
为assert
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
修改(myos/shell/buildin_cmd.c)中所有的ASSERT
为assert
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
修改(myos/shell/shell.c)中所有的ASSERT
为assert
,修改所有的PANIC
为panic
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
修改(myos/kernel/main.c)中所有的PANIC
为panic
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
给出操作prog_no_arg.c的脚本,该脚本主要功能:编译prog_no_arg.c,然后将其与使用到的.o文件进行链接(这里我们复用了给操作系统用的.o文件,按道理来说,我们需要单独实现用户程序的.o文件,但是我们偷个懒吧),最后写入磁盘hd60M.img偏移300扇区的位置
注意:相较于作者脚本,已经修改好了用到的编译器为gcc-4.4,CFLAGS,ld后面的参数,**一定要自行修改DD_OUT
为自己环境中的hd60M.img路径!!!**运行脚本需要在操作系统make all之后(这样用到的.o文件才会出现在build目录下),运行脚本需要在command目录下,脚本运行前需要添加可执行权限,命令:chmod +x compile.sh
,执行脚本命令:./compile.sh
(myos/command/compile.sh)
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="prog_no_arg"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIB="../lib/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
$CC $CFLAGS -I $LIB -o $BIN".o" $BIN".c"
ld -e main $BIN".o" $OBJS -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
########## 以上核心就是下面这三条命令 ##########
#gcc -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
# -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
# ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_no_arg of=/home/work/my_workspace/bochs/hd60M.img \
# bs=512 count=10 seek=300 conv=notrunc
测试代码,(myos/kernel/main.c),注意:file_size
这个变量请自行修改成自己的prog_no_arg大小,在command目录下ls -l即可查看prog_no_arg大小。我们前后会make all两次,第一次是为了让prog_no_arg有.o文件可以用,第二次是修改main.c以从hd60M.img中加载prog_no_arg到hd80M.img中
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 20684; //这个变量请自行修改成自己的prog_no_arg大小
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/prog_no_arg", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while (1)
;
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
while (1)
;
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
运行出错,经过排查,修改(myos/fs/fs.c/sys_getcwd)为
if (child_inode_nr == 0)
{
buf[0] = '/';
buf[1] = 0;
return buf;
}
为
if (child_inode_nr == 0)
{
buf[0] = '/';
buf[1] = 0;
sys_free(io_buf);
return buf;
}
修改(myos/kernel/memory.c/get_a_page)
...
if (cur->pgdir != NULL && pf == PF_USER)
{
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
}
...
if (page_phyaddr == NULL)
return NULL;
...
为
...
if (cur->pgdir != NULL && pf == PF_USER)
{
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx >= 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
}
...
if (page_phyaddr == NULL)
{
lock_release(&mem_pool->lock);
return NULL;
}
...
小节h:
使用户进程支持传参
_start
这个函数将会被execv
执行,由它去代为执行真正的用户程序,因为这个函数会在链接用户程序前,成为真正的程序入口,这样可以做到给用户程序传参的目的。因为我们在sys_exec
中有设定要开启的程序内核栈中的中断栈ebx,ecx的代码,intr_exit
会将ebx中放入参数字符串指针数组的地址,ecx中放入参数个数,然后_start
又将这两个寄存器入栈,而标准main
函数的声明都是int mian(int argc, char** argv)
,所以main
就可以自然在栈中找到自己要的参数。而且由于_start
入栈传参符合main
的函数声明,也就是从左到右依次入栈,所以这个main
的运行无需而外设定,就可以正常运行,就像任何普通有参数的c语言函数一样。
(myos/command/start.S)
[bits 32]
extern main
section .text
global _start
_start:
;下面这两个要和execv中load之后指定的寄存器一致
push ebx ;压入argv
push ecx ;压入argc
call main
prog_arg.c
中的main
开启了个子进程,去执行argv[1]
中指明的程序。
(myos/commadn/prog_arg.c)
#include "stdio.h"
#include "syscall.h"
#include "string.h"
int main(int argc, char **argv)
{
int arg_idx = 0;
while (arg_idx < argc)
{
printf("argv[%d] is %s\n", arg_idx, argv[arg_idx]);
arg_idx++;
}
int pid = fork();
if (pid)
{
int delay = 900000;
while (delay--)
;
printf("\n I`m father prog, my pid:%d, I will show process list\n", getpid());
ps();
}
else
{
char abs_path[512] = {0};
printf("\n I`m child prog, my pid:%d, I will exec %s right now\n", getpid(), argv[1]);
if (argv[1][0] != '/')
{
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
execv(abs_path, argv);
}
else
{
execv(argv[1], argv);
}
}
while (1)
;
return 0;
}
处理prog_arg.c脚本(myos/command/compile.sh)
相比于上一小节脚本,修改了要编译的程序BIN,包含头文件LIB,链接.o文件OBJS,编译start.S的命令,创建静态库命令,链接命令。
ar rcs simple_crt.a $OBJS start.o
的解释:
-
ar
: 这是一个用来创建、修改和提取静态库的程序。静态库通常用于将多个目标文件(object files)打包成一个文件,这样在链接时就可以一次性链接多个目标文件。 -
rcs
:r
: 替换或添加指定的目标文件到库中。如果库中已经有了同名的目标文件,那么这个文件会被新文件替换。c
: 如果库文件不存在,那么创建一个新的库文件。s
: 创建目标文件的索引。这可以加速链接时的速度。
-
simple_crt.a
: 这是你想要创建或修改的静态库的名字。 -
$OBJS start.o
: 这是一个目标文件列表,将被添加或替换到静态库中。
所以,整个命令的意思是:将 $OBJS
和 start.o
中列出的所有目标文件添加或替换到 simple_crt.a
静态库中,并为这些目标文件创建一个索引。如果 simple_crt.a
还不存在,那么会创建一个新的静态库文件。
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="prog_arg"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib -I ../lib/user -I ../fs -I ../thread -I ../lib/kernel -I ../kernel"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
########## 以上核心就是下面这三条命令 ##########
#gcc -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
# -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
# ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_no_arg of=/home/work/my_workspace/bochs/hd60M.img \
# bs=512 count=10 seek=300 conv=notrunc
测试代码(myos/kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 20840;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/prog_arg", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while (1)
;
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
while (1)
;
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
prog_arg.c
会与start.S
中的代码编译后共同链接成一个新的程序,然后如同上一个小节一样,先写入裸盘hd60M.img,然后main.c
中从裸盘加载新的应用程序prog_arg
进入内存,然后写入有文件系统的hd80M.img。当我们启动操作系统,在shell中输入./prog_arg /prog_no_arg
后,argv[0] = prog_arg, argv[1] = prog_no_arg
。首先exec
启动prog_arg
(因为argv[0]
才是要运行的程序,而后续的argv[1+n]
是我们传递给这个程序的参数)。我们在sys_exec(exec的真正实现)
中已经将程序要用的到参数字符串指针数组地址传递给了ebx
,且_start
才是prog_arg
的真正入口,而_start
中有一句push_ebx
的代码,也就是说参数字符串指针数组地址已经传递给了prog_arg
程序,自然prog_arg
程序能够通过argv[1]
去启动prog_no_arg
小节i:
进程终止与资源回收
首先介绍几个重要的概念:
exit
系统调用:此调用用于终止进程。当一个进程调用 exit
时,它会释放除进程控制块(pcb)以外的所有资源。pcb需要被特别处理,因为它包含了进程的重要信息,如退出状态。特别注意:exit
系统调用属于程序运行库内容,无论进程是否主动调用,都会执行。就像我们那个_start
函数一样。
wait
系统调用:这是一个与进程同步和资源回收相关的调用。具体来说,它有以下功能:
- 阻塞父进程,直到一个子进程退出,并接收子进程的返回值。
- 回收子进程使用过的pcb资源,从而确保没有资源浪费。
当一个父进程创建一个子进程来执行某项任务时,父进程可能需要知道子进程的退出状态。子进程完成其任务后,会将其退出状态保存在pcb中并调用exit
退出。此时,子进程的pcb不会被立即回收,因为它包含了子进程的退出状态。只有当父进程通过wait
系统调用来查询子进程的状态时,子进程的pcb才会被回收。
孤儿进程:如果一个父进程在其子进程结束之前退出,那么这些子进程将被称为孤儿进程,也就是说没有父进程来回收他们的pcb资源。为了防止资源浪费,这些孤儿进程会被init
进程“领养”,即成为init
进程的子进程,由init
来回收他们的pcb。
僵尸进程:当一个子进程终止,但其父进程没有调用wait
来回收其资源时,此时这个子进程也无法过继给init,于是这个子进程就变成了僵尸进程。它们仍然占用pcb,但不执行任何操作。僵尸进程的存在可能会导致资源浪费。
pcb增加表示退出状态的成员,比如正常退出还是其他啥的。修改(myos/thread/thread.h)
/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct
{
uint32_t *self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
pid_t pid;
enum task_status status;
uint8_t priority; // 线程优先级
char name[16]; // 用于存储自己的线程的名字
uint8_t ticks; // 线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; // 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; // general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; // all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t *pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 已打开文件数组
uint32_t cwd_inode_nr; // 进程所在的工作目录的inode编号
int16_t parent_pid; // 父进程pid
struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
int8_t exit_status; // 进程结束时自己调用exit传入的参数
uint32_t stack_magic; // 如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
free_a_phy_page
用于回收物理地址,实质就是回收了物理地址池位图对应的位。如此,这个物理地址下次就会被再次分配。
修改(myos/kernel/memory.c)
/* 根据物理页框地址pg_phy_addr在相应的内存池的位图清0,不改动页表*/
void free_a_phy_page(uint32_t pg_phy_addr)
{
struct pool *mem_pool;
uint32_t bit_idx = 0;
if (pg_phy_addr >= user_pool.phy_addr_start)
{
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
}
else
{
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);
}
添加函数声明,修改(myos/kernel/memory.h)
void free_a_phy_page(uint32_t pg_phy_addr);
由于我们的进程在退出后要释放自己的pid,然而原有的pid管理只有分配,而无回收。所以我们要实现用pid位图来管理pid的分配与回收,修改(myos/thread/thread.c)(以下新allocate_pid
函数需要替代原有的allocate_pid
函数)
allocate_pid
用于根据pid位图中空余位的偏移 + 起始pid来分配pid
pid_pool_init
用于初始化pid位图,并在thread_init
内调用
release_pid
来释放pid,实质就是将释放pid对应的pid位图中的位置0
thread_init
增加pid池初始化代码
/* pid的位图,最大支持1024个pid */
uint8_t pid_bitmap_bits[128] = {0};
/* pid池 */
struct pid_pool
{
struct bitmap pid_bitmap; // pid位图
uint32_t pid_start; // 起始pid
struct lock pid_lock; // 分配pid锁
} pid_pool;
/* 分配pid */
static pid_t allocate_pid(void)
{
lock_acquire(&pid_pool.pid_lock);
int32_t bit_idx = bitmap_scan(&pid_pool.pid_bitmap, 1);
bitmap_set(&pid_pool.pid_bitmap, bit_idx, 1);
lock_release(&pid_pool.pid_lock);
return (bit_idx + pid_pool.pid_start);
}
/* 初始化pid池 */
static void pid_pool_init(void)
{
pid_pool.pid_start = 1;
pid_pool.pid_bitmap.bits = pid_bitmap_bits;
pid_pool.pid_bitmap.btmp_bytes_len = 128;
bitmap_init(&pid_pool.pid_bitmap);
lock_init(&pid_pool.pid_lock);
}
/* 释放pid */
void release_pid(pid_t pid)
{
lock_acquire(&pid_pool.pid_lock);
int32_t bit_idx = pid - pid_pool.pid_start;
bitmap_set(&pid_pool.pid_bitmap, bit_idx, 0);
lock_release(&pid_pool.pid_lock);
}
void thread_init(void)
{
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
pid_pool_init();
/* 先创建第一个用户进程: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");
}
thread_exit
用于回收指定任务的pcb和页表,并将其从就绪队列中删除
pid_check
会被list_traversal
调用,用于对比传入的all_list_tag
指针对应任务的pid是不是要找的传入pid
pid2thread
根据传入pid找pcb,原理是使用list_traversal
调用pid_check
,当pid_check
找到了会返回true,于是list_traversal
会返回pcb指针
修改(thread/thread.c)
/* 回收thread_over的pcb和页表,并将其从调度队列中去除 */
void thread_exit(struct task_struct *thread_over, bool need_schedule)
{
/* 要保证schedule在关中断情况下调用 */
intr_disable();
thread_over->status = TASK_DIED;
/* 如果thread_over不是当前线程,就有可能还在就绪队列中,将其从中删除 */
if (elem_find(&thread_ready_list, &thread_over->general_tag))
{
list_remove(&thread_over->general_tag);
}
if (thread_over->pgdir)
{ // 如是进程,回收进程的页表
mfree_page(PF_KERNEL, thread_over->pgdir, 1);
}
/* 从all_thread_list中去掉此任务 */
list_remove(&thread_over->all_list_tag);
/* 回收pcb所在的页,主线程的pcb不在堆中,跨过 */
if (thread_over != main_thread)
{
mfree_page(PF_KERNEL, thread_over, 1);
}
/* 归还pid */
release_pid(thread_over->pid);
/* 如果需要下一轮调度则主动调用schedule */
if (need_schedule)
{
schedule();
PANIC("thread_exit: should not be here\n");
}
}
/* 比对任务的pid */
static bool pid_check(struct list_elem *pelem, int32_t pid)
{
struct task_struct *pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->pid == pid)
{
return true;
}
return false;
}
/* 根据pid找pcb,若找到则返回该pcb,否则返回NULL */
struct task_struct *pid2thread(int32_t pid)
{
struct list_elem *pelem = list_traversal(&thread_all_list, pid_check, pid);
if (pelem == NULL)
{
return NULL;
}
struct task_struct *thread = elem2entry(struct task_struct, all_list_tag, pelem);
return thread;
}
函数声明,修改(myos/thread/thread.h)
void thread_exit(struct task_struct* thread_over, bool need_schedule);
struct task_struct* pid2thread(int32_t pid);
void release_pid(pid_t pid);
release_prog_resource
用于根据传入的pcb指针,释放任务的资源,包括1、页表中对应的物理页面(这里用的方法是遍历页表);2、虚拟内存池占用的物理页框;3、关闭打开的文件
(myos/userprog/wait_exit.c)
#include "wait_exit.h"
#include "stdint.h"
#include "global.h"
#include "thread.h"
#include "fs.h"
/* 释放用户进程资源:
* 1 页表中对应的物理页
* 2 虚拟内存池占物理页框
* 3 关闭打开的文件 */
static void release_prog_resource(struct task_struct *release_thread)
{
uint32_t *pgdir_vaddr = release_thread->pgdir;
uint16_t user_pde_nr = 768, pde_idx = 0;
uint32_t pde = 0;
uint32_t *v_pde_ptr = NULL; // v表示var,和函数pde_ptr区分
uint16_t user_pte_nr = 1024, pte_idx = 0;
uint32_t pte = 0;
uint32_t *v_pte_ptr = NULL; // 加个v表示var,和函数pte_ptr区分
uint32_t *first_pte_vaddr_in_pde = NULL; // 用来记录pde中第0个pte的地址
uint32_t pg_phy_addr = 0;
/* 回收页表中用户空间的页框 */
while (pde_idx < user_pde_nr)
{
v_pde_ptr = pgdir_vaddr + pde_idx;
pde = *v_pde_ptr;
if (pde & 0x00000001)
{ // 如果页目录项p位为1,表示该页目录项下可能有页表项
first_pte_vaddr_in_pde = pte_ptr(pde_idx * 0x400000); // 一个页表表示的内存容量是4M,即0x400000
pte_idx = 0;
while (pte_idx < user_pte_nr)
{
v_pte_ptr = first_pte_vaddr_in_pde + pte_idx;
pte = *v_pte_ptr;
if (pte & 0x00000001)
{
/* 将pte中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pte & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pte_idx++;
}
/* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pde & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pde_idx++;
}
/* 回收用户虚拟地址池所占的物理内存*/
uint32_t bitmap_pg_cnt = (release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len) / PG_SIZE;
uint8_t *user_vaddr_pool_bitmap = release_thread->userprog_vaddr.vaddr_bitmap.bits;
mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt);
/* 关闭进程打开的文件 */
uint8_t fd_idx = 3;
while (fd_idx < MAX_FILES_OPEN_PER_PROC)
{
if (release_thread->fd_table[fd_idx] != -1)
{
sys_close(fd_idx);
}
fd_idx++;
}
}
fild_child
会被list_traversal
调用,用于对比传入的all_list_tag
指针对应任务的parient_id是不是要找的传入ppid
修改(myos/userprog/wait_exit.c)
/* list_traversal的回调函数,
* 查找pelem的parent_pid是否是ppid,成功返回true,失败则返回false */
static bool find_child(struct list_elem *pelem, int32_t ppid)
{
/* elem2entry中间的参数all_list_tag取决于pelem对应的变量名 */
struct task_struct *pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid)
{ // 若该任务的parent_pid为ppid,返回
return true; // list_traversal只有在回调函数返回true时才会停止继续遍历,所以在此返回true
}
return false; // 让list_traversal继续传递下一个元素
}
find_hanging_child
会被list_traversal
调用,用于对比传入的all_list_tag
指针对应任务的ppid是不是传入的ppid,且状态要是不是TASK_HANGING(进程没有完全退出就是这个状态)。此函数用于父进程来找到自己退出的子进程以回收它的剩余资源
修改(myos/userprog/wait_exit.c)
/* list_traversal的回调函数,
* 查找状态为TASK_HANGING的任务 */
static bool find_hanging_child(struct list_elem* pelem, int32_t ppid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid && pthread->status == TASK_HANGING) {
return true;
}
return false;
}
init_adopt_a_child
将传入的all_list_tag
指针对应任务parent_pid
改为1,也就是将一个子进程过继给init
修改(myos/userprog/wait_exit.c)
/* list_traversal的回调函数,
* 将一个子进程过继给init */
static bool init_adopt_a_child(struct list_elem *pelem, int32_t pid)
{
struct task_struct *pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == pid)
{ // 若该进程的parent_pid为pid,返回
pthread->parent_pid = 1;
}
return false; // 让list_traversal继续传递下一个元素
}
sys_wait
等待子进程调用exit,将子进程的退出状态保存到status指向的变量,并回收子进程的pcb与页表,最后返回子进程pid。如果子进程都在运行,那么就阻塞自己。这个函数有两种用法,一种是init
while(1)不断调用,来不断回收子进程的资源;一种是父进程fork之后调用,然后等待子进程退出后继续运行,然后回收子进程剩余资源。
修改(myos/userprog/wait_exit.c)
/* 等待子进程调用exit,将子进程的退出状态保存到status指向的变量.
* 成功则返回子进程的pid,失败则返回-1 */
pid_t sys_wait(int32_t *status)
{
struct task_struct *parent_thread = running_thread();
while (1)
{
/* 优先处理已经是挂起状态的任务 */
struct list_elem *child_elem = list_traversal(&thread_all_list, find_hanging_child, parent_thread->pid);
/* 若有挂起的子进程 */
if (child_elem != NULL)
{
struct task_struct *child_thread = elem2entry(struct task_struct, all_list_tag, child_elem);
*status = child_thread->exit_status;
/* thread_exit之后,pcb会被回收,因此提前获取pid */
uint16_t child_pid = child_thread->pid;
/* 2 从就绪队列和全部队列中删除进程表项*/
thread_exit(child_thread, false); // 传入false,使thread_exit调用后回到此处
/* 进程表项是进程或线程的最后保留的资源, 至此该进程彻底消失了 */
return child_pid;
}
/* 判断是否有子进程 */
child_elem = list_traversal(&thread_all_list, find_child, parent_thread->pid);
if (child_elem == NULL)
{ // 若没有子进程则出错返回
return -1;
}
else
{
/* 若子进程还未运行完,即还未调用exit,则将自己挂起,直到子进程在执行exit时将自己唤醒 */
thread_block(TASK_WAITING);
}
}
}
sys_exit
子进程用来结束自己,退出时的事项:1、在自己的pcb中留下退出状态;2、将自己的子进程全部过继给init;3、回收自己除pcb与页表外的资源;4、可能有父进程在等待自己调用exit
,所以还要唤醒等待的父进程;5、阻塞自己,也就是换下cpu。这个函数会被运行库调用,进程即使不主动调用,也会执行
#include "debug.h"
/* 子进程用来结束自己时调用 */
void sys_exit(int32_t status)
{
struct task_struct *child_thread = running_thread();
child_thread->exit_status = status;
if (child_thread->parent_pid == -1)
{
PANIC("sys_exit: child_thread->parent_pid is -1\n");
}
/* 将进程child_thread的所有子进程都过继给init */
list_traversal(&thread_all_list, init_adopt_a_child, child_thread->pid);
/* 回收进程child_thread的资源 */
release_prog_resource(child_thread);
/* 如果父进程正在等待子进程退出,将父进程唤醒 */
struct task_struct *parent_thread = pid2thread(child_thread->parent_pid);
if (parent_thread->status == TASK_WAITING)
{
thread_unblock(parent_thread);
}
/* 将自己挂起,等待父进程获取其status,并回收其pcb */
thread_block(TASK_HANGING);
}
将sys_wait
与sys_exit
封装成系统调用
添加系统调用号,修改(myos/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,
SYS_EXECV,
SYS_EXIT,
SYS_WAIT
};
创建用户态系统调用入口,修改(myos/lib/user/syscall.c)
/* 以状态status退出 */
void exit(int32_t status)
{
_syscall1(SYS_EXIT, status);
}
/* 等待子进程,子进程状态存储到status */
pid_t wait(int32_t *status)
{
return _syscall1(SYS_WAIT, status);
}
声明,用户态系统调用函数入口,修改(myos/lib/user/syscall.h)
void exit(int32_t status);
pid_t wait(int32_t* status);
系统调用表中,添加实际系统调用处理函数,修改(myos/userprog/syscall-init.c)
#include "wait_exit.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;
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;
syscall_table[SYS_EXECV] = sys_execv;
syscall_table[SYS_EXIT] = sys_exit;
syscall_table[SYS_WAIT] = sys_wait;
put_str("syscall_init done\n");
}
将exit
函数集成到运行库中。这样,即使程序中没有明确调用exit
,它也会在程序结束时自动被调用,与_start
相同。需要特别注意的是,子进程的退出机制与普通的函数返回机制不同。当子进程终止时,它并不是“返回”给其父进程;相反,它只是简单地结束了自己的执行。父进程和子进程在内存地址空间和执行上下文中是完全独立的,子进程不可能按照常规的函数调用方式“返回”一个值给父进程(做到这点需要其他的进程间通信机制支持)。取而代之的是,子进程提供一个退出状态,来描述其终止的方式或原因。因此,这里的push eax
并不是我们在普通函数调用中看到的那种返回值——比如一个指针或某种计算结果。实际上,它代表了子进程的结束状态,就像我们在每个main
函数中写的return 0
一样。
修改(myos/command/start.S)
[bits 32]
extern main
extern exit
section .text
global _start
_start:
;下面这两个要和execv中load之后指定的寄存器一致
push ebx ;压入argv
push ecx ;压入argc
call main
;将main的返回值通过栈传给exit,gcc用eax存储返回值,这是ABI规定的
push eax
call exit
;exit不会返回
cat
用于读取文件内容,不是以系统调用的方式存在,而是以用户进程的方式存在。核心原理就是调用read
系统调用,然后调用write
系统调用来打印
(myos/command/cat.c)
#include "syscall.h"
#include "stdio.h"
#include "string.h"
int main(int argc, char **argv)
{
if (argc > 2 || argc == 1)
{
printf("cat: only support 1 argument.\neg: cat filename\n");
exit(-2);
}
int buf_size = 1024;
char abs_path[512] = {0};
void *buf = malloc(buf_size);
if (buf == NULL)
{
printf("cat: malloc memory failed\n");
return -1;
}
if (argv[1][0] != '/')
{
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
}
else
{
strcpy(abs_path, argv[1]);
}
int fd = open(abs_path, O_RDONLY);
if (fd == -1)
{
printf("cat: open: open %s failed\n", argv[1]);
return -1;
}
int read_bytes = 0;
while (1)
{
read_bytes = read(fd, buf, buf_size);
if (read_bytes == -1)
{
break;
}
write(1, buf, read_bytes);
}
free(buf);
close(fd);
return 66;
}
处理cat.c的脚本
(myos/command/compile.sh)
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
../kernel/ -I ../device/ -I ../thread/ -I \
../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
现在我们有了能够退出程序的机制,终于不用再来用while(1)让程序卡住而不乱跳啦!
修改(myos/shell/shell.c/my_shell)
else
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
/* 下面这个while必须要加上,否则父进程一般情况下会比子进程先执行,
因此会进行下一轮循环将findl_path清空,这样子进程将无法从final_path中获得参数*/
while (1)
;
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
}
else
{
execv(argv[0], argv);
}
while (1)
;
}
}
为
else
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
int32_t status;
int32_t child_pid = wait(&status); // 此时子进程若没有执行exit,my_shell会被阻塞,不再响应键入的命令
if (child_pid == -1)
{ // 按理说程序正确的话不会执行到这句,fork出的进程便是shell子进程
panic("my_shell: no child\n");
}
printf("child_pid %d, it's status: %d\n", child_pid, status);
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
}
else
{
execv(argv[0], argv);
}
}
}
修改(myos/kernel/main.c),main函数要完成cat程序从hd60M.img到hd80M.img的加载,并且要退出;init进程不断调用wait
来回收过继的僵尸进程资源
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 21196;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("cat.c", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true);
return 0;
}
/* 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");
}
支持代码,删除(myos/userprog/fork.c/copy_pcb_vaddrbitmap_stack0)
ASSERT(strlen(child_thread->name) < 11); // pcb.name的长度是16,为避免下面strcat越界
strcat(child_thread->name, "_fork");
小节j:
本节要支持管道,管道是用于父子进程通信的机制
管道本质上是位于内核空间的环形缓冲区。遵循Linux的设计哲学——一切皆文件,我们将管道也视为一个文件。这样,我们就可以通过文件描述符进行对管道的读写操作。在进行父子进程之间的通信时,父进程首先创建一个管道,从而得到两个文件描述符,一个用于读,另一个用于写。随后,父进程使用fork
创建子进程。子进程继承了父进程打开的文件,因此也可以通过这些文件描述符与管道进行通信,从而实现与父进程的交互。
- 匿名管道:仅对创建它的进程及其子进程可见,其他进程无法访问。
- 有名管道:可以被系统中的所有进程访问。
我们的管道机制将会复用现有文件系统
ioq_length
返回环形缓冲区内的数据长度
修改(myos/device/ioqueue.c)
/* 返回环形缓冲区中的数据长度 */
uint32_t ioq_length(struct ioqueue *ioq)
{
uint32_t len = 0;
if (ioq->head >= ioq->tail)
{
len = ioq->head - ioq->tail;
}
else
{
len = bufsize - (ioq->tail - ioq->head);
}
return len;
}
函数声明,修改(myos/device/ioqueue.h)
uint32_t ioq_length(struct ioqueue *ioq);
is_pipe
判断文件描述符对应的文件是不是管道
(myos/shell/pipe.c)
#include "pipe.h"
#include "stdint.h"
#include "global.h"
#include "file.h"
#include "fs.h"
/* 判断文件描述符local_fd是否是管道 */
bool is_pipe(uint32_t local_fd)
{
uint32_t global_fd = fd_local2global(local_fd);
return file_table[global_fd].fd_flag == PIPE_FLAG;
}
函数声明,(myos/shell/pipe.h)
#ifndef __SHELL_PIPE_H
#define __SHELL_PIPE_H
#include "global.h"
#define PIPE_FLAG 0xFFFF
bool is_pipe(uint32_t local_fd);
#endif
支持代码,修改(myos/fs/fs.c)
static uint32_t fd_local2global(uint32_t local_fd)
为
uint32_t fd_local2global(uint32_t local_fd)
并添加函数声明,修改(myos/fs/fs.h)
uint32_t fd_local2global(uint32_t local_fd);
sys_pipe
用于创建管道,核心就是创建了个全局打开文件结构,然后申请一页内核页,并让之前的文件结构内的fd_inode
成员指向这个内核页(之前的文件系统中,该成员指向一个struct inode),之后再将这个内核页起始位置创建struct ioqueue
并初始化。然后在进程中安装两个文件描述符,指向这个文件结构。最后记录下这两个文件描述符。
修改(myos/shell/pipe.c)
#include "ioqueue.h"
/* 创建管道,成功返回0,失败返回-1 */
int32_t sys_pipe(int32_t pipefd[2])
{
int32_t global_fd = get_free_slot_in_global();
/* 申请一页内核内存做环形缓冲区 */
file_table[global_fd].fd_inode = get_kernel_pages(1);
/* 初始化环形缓冲区 */
ioqueue_init((struct ioqueue *)file_table[global_fd].fd_inode);
if (file_table[global_fd].fd_inode == NULL)
{
return -1;
}
/* 将fd_flag复用为管道标志 */
file_table[global_fd].fd_flag = PIPE_FLAG;
/* 将fd_pos复用为管道打开数 */
file_table[global_fd].fd_pos = 2;
pipefd[0] = pcb_fd_install(global_fd);
pipefd[1] = pcb_fd_install(global_fd);
return 0;
}
pipe_read
传入管道的文件描述符、一个缓冲地址、读取字节数。通过管道的文件描述符找到环形缓冲区struct ioqueue
,然后调用ioq_getchar
从中读取数据即可。
修改(myos/shell/pipe.c)
/* 从管道中读数据 */
uint32_t pipe_read(int32_t fd, void *buf, uint32_t count)
{
char *buffer = buf;
uint32_t bytes_read = 0;
uint32_t global_fd = fd_local2global(fd);
/* 获取管道的环形缓冲区 */
struct ioqueue *ioq = (struct ioqueue *)file_table[global_fd].fd_inode;
/* 选择较小的数据读取量,避免阻塞 */
uint32_t ioq_len = ioq_length(ioq);
uint32_t size = ioq_len > count ? count : ioq_len;
while (bytes_read < size)
{
*buffer = ioq_getchar(ioq);
bytes_read++;
buffer++;
}
return bytes_read;
}
pipe_write
传入管道的文件描述符、一个缓冲地址、写入字节数。通过管道的文件描述符找到环形缓冲区struct ioqueue
,然后调用ioq_putchar
向其写入数据即可。
修改(myos/shell/pipe.c)
/* 往管道中写数据 */
uint32_t pipe_write(int32_t fd, const void *buf, uint32_t count)
{
uint32_t bytes_write = 0;
uint32_t global_fd = fd_local2global(fd);
struct ioqueue *ioq = (struct ioqueue *)file_table[global_fd].fd_inode;
/* 选择较小的数据写入量,避免阻塞 */
uint32_t ioq_left = bufsize - ioq_length(ioq);
uint32_t size = ioq_left > count ? count : ioq_left;
const char *buffer = buf;
while (bytes_write < size)
{
ioq_putchar(ioq, *buffer);
bytes_write++;
buffer++;
}
return bytes_write;
}
函数声明,修改(myos/shell/pipe.h)
int32_t sys_pipe(int32_t pipefd[2]);
uint32_t pipe_read(int32_t fd, void *buf, uint32_t count);
uint32_t pipe_write(int32_t fd, const void *buf, uint32_t count);
将sys_pipe
做成系统调用
增加系统调用号,修改(myos/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,
SYS_EXECV,
SYS_EXIT,
SYS_WAIT,
SYS_PIPE
};
增加用户态系统调用入口,修改(myos/lib/user/syscall.c)
/* 生成管道,pipefd[0]负责读入管道,pipefd[1]负责写入管道 */
int32_t pipe(int32_t pipefd[2])
{
return _syscall1(SYS_PIPE, pipefd);
}
声明用户态系统调用函数,修改(myos/lib/user/syscall.h)
int32_t pipe(int32_t pipefd[2]);
系统调用表中,增加实际系统调用处理函数,修改(myos/userprog/syscall-init.c)
#include "pipe.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;
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;
syscall_table[SYS_EXECV] = sys_execv;
syscall_table[SYS_EXIT] = sys_exit;
syscall_table[SYS_WAIT] = sys_wait;
syscall_table[SYS_PIPE] = sys_pipe;
put_str("syscall_init done\n");
}
sys_close
增加对管道文件的关闭代码,先调用is_pipe
判断文件描述符对应的文件结构是管道文件,然后文件结构中的fd_pos
-1(该成员记录管道文件的打开次数,在之前文件系统中,该成员记录文件当前操作的位置),如果此时fd_pos
为0,那么直接释放环形缓冲区对应的那页内存即可。
修改(myos/fs/fs.c/sys_close)
#include "pipe.h"
/* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */
int32_t sys_close(int32_t fd)
{
int32_t ret = -1; // 返回值默认为-1,即失败
if (fd > 2)
{
uint32_t global_fd = fd_local2global(fd);
if (is_pipe(fd))
{
/* 如果此管道上的描述符都被关闭,释放管道的环形缓冲区 */
if (--file_table[global_fd].fd_pos == 0)
{
mfree_page(PF_KERNEL, file_table[global_fd].fd_inode, 1);
file_table[global_fd].fd_inode = NULL;
}
ret = 0;
}
else
{
ret = file_close(&file_table[global_fd]);
}
running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
}
return ret;
}
sys_write
增加对管道文件的写入代码,在fd == stdout_no
增加调用is_pipe
判断文件描述符对应的文件是不是管道文件(标准输出有可能会被重定向为管道文件),如果是,则调用pipe_write
。然后增加fd即使不是标准输出判断是不是管道文件,如果是,调用pipe_write
写入。
修改(myos/fs/fs.c/sys_write)
/* 将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败返回-1 */
int32_t sys_write(int32_t fd, const void *buf, uint32_t count)
{
if (fd < 0)
{
printk("sys_write: fd error\n");
return -1;
}
if (fd == stdout_no)
{
/* 标准输出有可能被重定向为管道缓冲区, 因此要判断 */
if (is_pipe(fd))
{
return pipe_write(fd, buf, count);
}
else
{
char tmp_buf[1024] = {0};
memcpy(tmp_buf, buf, count);
console_put_str(tmp_buf);
return count;
}
}
else if (is_pipe(fd))
{ /* 若是管道就调用管道的方法 */
return pipe_write(fd, buf, count);
}
else
{
uint32_t _fd = fd_local2global(fd);
struct file *wr_file = &file_table[_fd];
if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR)
{
uint32_t bytes_written = file_write(wr_file, buf, count);
return bytes_written;
}
else
{
console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
return -1;
}
}
}
sys_read
增加对管道文件的读入代码,在fd == stdoin_no
增加调用is_pipe
判断文件描述符对应的文件是不是管道文件(标准输入有可能会被重定向为管道文件),如果是,则调用pipe_read
。然后增加fd即使不是标准输入判断是不是管道文件,如果是,调用pipe_read
读出。
修改(myos/fs/fs.c/sys_read)
/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void *buf, uint32_t count)
{
ASSERT(buf != NULL);
int32_t ret = -1;
uint32_t global_fd = 0;
if (fd < 0 || fd == stdout_no || fd == stderr_no)
{
printk("sys_read: fd error\n");
}
else if (fd == stdin_no)
{
/* 标准输入有可能被重定向为管道缓冲区, 因此要判断 */
if (is_pipe(fd))
{
ret = pipe_read(fd, buf, count);
}
else
{
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 if (is_pipe(fd))
{ /* 若是管道就调用管道的方法 */
ret = pipe_read(fd, buf, count);
}
else
{
global_fd = fd_local2global(fd);
ret = file_read(&file_table[global_fd], buf, count);
}
return ret;
}
update_inode_open
增加对于管道文件的处理代码,如果是,那么fd_pos
+ 1
修改(myos/userprog/fork.c/update_inode_open)
#include "pipe.h"
/* 更新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)
{
if (is_pipe(local_fd))
{
file_table[global_fd].fd_pos++;
}
else
{
file_table[global_fd].fd_inode->i_open_cnts++;
}
}
local_fd++;
}
}
release_prog_resource
增加程序退出时对于打开的管道文件资源的处理代码,原理与sys_close
增加的代码一样
修改(myos/userprog/wait_exit.c/release_prog_resource)
#include "pipe.h"
#include "file.h"
static void release_prog_resource(struct task_struct *release_thread)
{
uint32_t *pgdir_vaddr = release_thread->pgdir;
uint16_t user_pde_nr = 768, pde_idx = 0;
uint32_t pde = 0;
uint32_t *v_pde_ptr = NULL; // v表示var,和函数pde_ptr区分
uint16_t user_pte_nr = 1024, pte_idx = 0;
uint32_t pte = 0;
uint32_t *v_pte_ptr = NULL; // 加个v表示var,和函数pte_ptr区分
uint32_t *first_pte_vaddr_in_pde = NULL; // 用来记录pde中第0个pte的地址
uint32_t pg_phy_addr = 0;
/* 回收页表中用户空间的页框 */
while (pde_idx < user_pde_nr)
{
v_pde_ptr = pgdir_vaddr + pde_idx;
pde = *v_pde_ptr;
if (pde & 0x00000001)
{ // 如果页目录项p位为1,表示该页目录项下可能有页表项
first_pte_vaddr_in_pde = pte_ptr(pde_idx * 0x400000); // 一个页表表示的内存容量是4M,即0x400000
pte_idx = 0;
while (pte_idx < user_pte_nr)
{
v_pte_ptr = first_pte_vaddr_in_pde + pte_idx;
pte = *v_pte_ptr;
if (pte & 0x00000001)
{
/* 将pte中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pte & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pte_idx++;
}
/* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pde & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pde_idx++;
}
/* 回收用户虚拟地址池所占的物理内存*/
uint32_t bitmap_pg_cnt = (release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len) / PG_SIZE;
uint8_t *user_vaddr_pool_bitmap = release_thread->userprog_vaddr.vaddr_bitmap.bits;
mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt);
/* 关闭进程打开的文件 */
uint8_t local_fd = 3;
while (local_fd < MAX_FILES_OPEN_PER_PROC)
{
if (release_thread->fd_table[local_fd] != -1)
{
if (is_pipe(local_fd))
{
uint32_t global_fd = fd_local2global(local_fd);
if (--file_table[global_fd].fd_pos == 0)
{
mfree_page(PF_KERNEL, file_table[global_fd].fd_inode, 1);
file_table[global_fd].fd_inode = NULL;
}
}
else
{
sys_close(local_fd);
}
}
local_fd++;
}
}
测试管道的用户进程(myos/command/prog_pipe.c)
#include "stdio.h"
#include "syscall.h"
#include "string.h"
#include "stdint.h"
int main(int argc, char **argv)
{
int32_t fd[2] = {-1};
pipe(fd);
int32_t pid = fork();
if (pid)
{ // 父进程
close(fd[0]); // 关闭输入
write(fd[1], "Hi, my son, I love you!", 24);
printf("\nI`m father, my pid is %d\n", getpid());
return 8;
}
else
{
close(fd[1]); // 关闭输出
char buf[32] = {0};
read(fd[0], buf, 24);
printf("\nI`m child, my pid is %d\n", getpid());
printf("I`m child, my father said to me: \"%s\"\n", buf);
return 9;
}
}
处理prog_pipe的脚本
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="prog_pipe"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
../kernel/ -I ../device/ -I ../thread/ -I \
../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
测试代码,用于将prog_pipe从hd60M.img加载到hd80M.img中
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 21432;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/prog_pipe", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true);
return 0;
}
/* 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");
}
小节k:
在shell中支持管道
一般来说,键盘充当程序的输入源,而屏幕则是程序的输出目标,这被称为标准输入和输出。然而,程序也可以从文件接收输入或将其输出发送到文件中,这种方式被称为非标准输入和输出。当我们想从标准输入输出切换到文件输入输出时,我们使用输入输出重定向。通过这种方式,我们可以将一个命令的输出用作另一个命令的输入,这正是管道的功能。在Linux中,这种操作通常是通过命令行的管道符“|”完成的。例如,在命令ls | grep kanshan
中,ls
命令列出当前目录下的所有文件并原本会将其输出到屏幕,但由于存在管道符|
,它的输出会利用管道重定向为grep
命令的输入。
sys_fd_redirect
其功能是将一个已有的文件描述符old_local_fd
重定向为另一个文件描述符new_local_fd
。实际用法如:fd_redirect(1,fd[1]);
(fd[1]是管道文件对应的文件描述符,其全局文件结构索引一定大于2)用于标准输入重定位到管道文件(结合sys_write
理解);fd_redirect(1,1);
用于恢复标准输出(结合sys_write
理解)
修改(myos/shell/pipe.c)
/* 将文件描述符old_local_fd重定向为new_local_fd */
void sys_fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd)
{
struct task_struct *cur = running_thread();
/* 针对恢复标准描述符 */
if (new_local_fd < 3)
{
cur->fd_table[old_local_fd] = new_local_fd;
}
else
{
uint32_t new_global_fd = cur->fd_table[new_local_fd];
cur->fd_table[old_local_fd] = new_global_fd;
}
}
函数声明,修改(myos/shell/pipe.h)
void sys_fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd);
sys_help
打印系统支持的内部命令
修改(myos/fs/fs.c)
/* 显示系统支持的内部命令 */
void sys_help(void)
{
printk("\
buildin commands:\n\
ls: show directory or file information\n\
cd: change current work directory\n\
mkdir: create a directory\n\
rmdir: remove a empty directory\n\
rm: remove a regular file\n\
pwd: show current work directory\n\
ps: show process information\n\
clear: clear screen\n\
shortcut key:\n\
ctrl+l: clear screen\n\
ctrl+u: clear input\n\n");
}
添加声明,修改(myos/fs/fs.h)
void sys_help(void);
将sys_fd_redirect
与sys_help
做成系统调用
添加系统调用号,修改(myos/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,
SYS_EXECV,
SYS_EXIT,
SYS_WAIT,
SYS_PIPE,
SYS_FD_REDIRECT,
SYS_HELP
};
实现用户态系统调用入口,修改(myos/lib/user/syscall.c)
/* 将文件描述符old_local_fd重定向到new_local_fd */
void fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd)
{
_syscall2(SYS_FD_REDIRECT, old_local_fd, new_local_fd);
}
/* 显示系统支持的命令 */
void help(void)
{
_syscall0(SYS_HELP);
}
声明用户态系统调用函数,修改(myos/lib/user/syscall.h)
void fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd);
void help(void);
系统调用表添加实际系统调用处理函数,修改(myos/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] = 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;
syscall_table[SYS_EXECV] = sys_execv;
syscall_table[SYS_EXIT] = sys_exit;
syscall_table[SYS_WAIT] = sys_wait;
syscall_table[SYS_PIPE] = sys_pipe;
syscall_table[SYS_FD_REDIRECT] = sys_fd_redirect;
syscall_table[SYS_HELP] = sys_help;
put_str("syscall_init done\n");
}
将help
封装成内建命令,修改(myos/shell/buildin_cmd.c)
/* 显示内建命令列表 */
void buildin_help(uint32_t argc UNUSED, char **argv UNUSED)
{
help();
}
声明,修改(myos/shell/buildin_cmd.h)
void buildin_help(uint32_t argc UNUSED, char **argv UNUSED);
cmd_execute
去取代原有shell中执行内部与外部命令功能
修改(myos/shell/shell.c)
/* 执行命令 */
static void cmd_execute(uint32_t argc, char **argv)
{
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 if (!strcmp("help", argv[0]))
{
buildin_help(argc, argv);
}
else
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
int32_t status;
int32_t child_pid = wait(&status); // 此时子进程若没有执行exit,my_shell会被阻塞,不再响应键入的命令
if (child_pid == -1)
{ // 按理说程序正确的话不会执行到这句,fork出的进程便是shell子进程
panic("my_shell: no child\n");
}
printf("child_pid %d, it's status: %d\n", child_pid, status);
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
exit(-1);
}
else
{
execv(argv[0], argv);
}
}
}
}
新的my_shell
,主要功能是从用户获取命令行输入,解析并执行命令,尤其支持管道|
命令的功能。
主要新增部分:检查用户输入中是否包含管道符号|
- 如果有管道命令:
- 创建一个管道。
- 重定向标准输出到管道的写端。
- 解析并执行第一个命令。
- 重定向标准输入到管道的读端。
- 对于每一个中间的命令(除了最后一个):
- 解析并执行命令。
- 恢复标准输出到屏幕。
- 执行管道中的最后一个命令。
- 恢复标准输入为键盘。
- 关闭管道。
- 如果没有管道命令:
- 解析用户输入的命令。
- 如果参数数量超过了设定的最大值,则提示错误。
- 否则执行命令。
修改(myos/shell/shell.c)
#include "pipe.h"
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;
}
/* 针对管道的处理 */
char *pipe_symbol = strchr(cmd_line, '|');
if (pipe_symbol)
{
/* 支持多重管道操作,如cmd1|cmd2|..|cmdn,
* cmd1的标准输出和cmdn的标准输入需要单独处理 */
/*1 生成管道*/
int32_t fd[2] = {-1}; // fd[0]用于输入,fd[1]用于输出
pipe(fd);
/* 将标准输出重定向到fd[1],使后面的输出信息重定向到内核环形缓冲区 */
fd_redirect(1, fd[1]);
/*2 第一个命令 */
char *each_cmd = cmd_line;
pipe_symbol = strchr(each_cmd, '|');
*pipe_symbol = 0;
/* 执行第一个命令,命令的输出会写入环形缓冲区 */
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);
/* 跨过'|',处理下一个命令 */
each_cmd = pipe_symbol + 1;
/* 将标准输入重定向到fd[0],使之指向内核环形缓冲区*/
fd_redirect(0, fd[0]);
/*3 中间的命令,命令的输入和输出都是指向环形缓冲区 */
while ((pipe_symbol = strchr(each_cmd, '|')))
{
*pipe_symbol = 0;
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);
each_cmd = pipe_symbol + 1;
}
/*4 处理管道中最后一个命令 */
/* 将标准输出恢复屏幕 */
fd_redirect(1, 1);
/* 执行最后一个命令 */
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);
/*5 将标准输入恢复为键盘 */
fd_redirect(0, 0);
/*6 关闭管道 */
close(fd[0]);
close(fd[1]);
}
else
{ // 一般无管道操作的命令
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
cmd_execute(argc, argv);
}
}
panic("my_shell: should not be here");
}
cat
新增无参数时从键盘获得输入(记得删除原有hd80M.img中的cat)
(myos/command/cat.c)
#include "syscall.h"
#include "stdio.h"
#include "string.h"
#include "fs.h"
int main(int argc, char **argv)
{
if (argc > 2)
{
printf("cat: argument error\n");
exit(-2);
}
if (argc == 1)
{
char buf[512] = {0};
read(0, buf, 512);
printf("%s", buf);
exit(0);
}
int buf_size = 1024;
char abs_path[512] = {0};
void *buf = malloc(buf_size);
if (buf == NULL)
{
printf("cat: malloc memory failed\n");
return -1;
}
if (argv[1][0] != '/')
{
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
}
else
{
strcpy(abs_path, argv[1]);
}
int fd = open(abs_path, O_RDONLY);
if (fd == -1)
{
printf("cat: open: open %s failed\n", argv[1]);
return -1;
}
int read_bytes = 0;
while (1)
{
read_bytes = read(fd, buf, buf_size);
if (read_bytes == -1)
{
break;
}
write(1, buf, read_bytes);
}
free(buf);
close(fd);
return 66;
}
处理cat的脚本
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
../kernel/ -I ../device/ -I ../thread/ -I \
../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
测试代码,(myos/kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 21816;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/cat", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true);
return 0;
}
/* 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");
}
运行出错,排查后需要修改(myos/device/ioqueue.h)
#define bufsize 64 //定义缓冲区大小.
为
#define bufsize 2048 //定义缓冲区大小.