before lab6

This commit is contained in:
ridethepig 2022-11-21 23:07:57 +08:00
parent 890505eecb
commit a9e2c29c26
63 changed files with 1708 additions and 7054 deletions

View File

@ -6,7 +6,8 @@
"${workspaceFolder}/inc" "${workspaceFolder}/inc"
], ],
"defines": [], "defines": [],
"cStandard": "gnu99" "cStandard": "gnu99",
"compilerArgs": ["-m32"]
} }
], ],
"version": 4 "version": 4

View File

@ -1,6 +1,6 @@
{ {
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.tabSize": 4, "editor.tabSize": 8,
"editor.insertSpaces": false, "editor.insertSpaces": false,
"files.associations": { "files.associations": {
"*.h": "c", "*.h": "c",

View File

@ -90,7 +90,7 @@ $(IMAGE): $(OBJDIR)/boot/boot.bin $(OBJDIR)/boot/loader.bin $(OBJDIR)/kern/kerne
@sudo mount -o loop $@ /mnt @sudo mount -o loop $@ /mnt
@sudo cp $(OBJDIR)/boot/loader.bin /mnt -v @sudo cp $(OBJDIR)/boot/loader.bin /mnt -v
@sudo cp $(OBJDIR)/kern/kernel.bin /mnt -v @sudo cp $(OBJDIR)/kern/kernel.bin /mnt -v
@sudo cp $(USER_BINS) /mnt -v @sudo cp $(USER_BINS) /mnt
@sudo umount /mnt @sudo umount /mnt
all: $(IMAGE) all: $(IMAGE)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -46,9 +46,7 @@ struct Secthdr {
u32 sh_addralign; u32 sh_addralign;
u32 sh_entsize; u32 sh_entsize;
}; };
typedef struct Elf Elf32_Ehdr;
typedef struct Proghdr Elf32_Phdr;
typedef struct Secthdr Elf32_Shdr;
// Values for Proghdr::p_type // Values for Proghdr::p_type
#define PT_LOAD 1 #define PT_LOAD 1

139
inc/errno.h Normal file
View File

@ -0,0 +1,139 @@
#ifndef MINIOS_ERRNO_H
#define MINIOS_ERRNO_H
#define EPERM 1
#define ENOENT 2
#define ESRCH 3
#define EINTR 4
#define EIO 5
#define ENXIO 6
#define E2BIG 7
#define ENOEXEC 8
#define EBADF 9
#define ECHILD 10
#define EAGAIN 11
#define ENOMEM 12
#define EACCES 13
#define EFAULT 14
#define ENOTBLK 15
#define EBUSY 16
#define EEXIST 17
#define EXDEV 18
#define ENODEV 19
#define ENOTDIR 20
#define EISDIR 21
#define EINVAL 22
#define ENFILE 23
#define EMFILE 24
#define ENOTTY 25
#define ETXTBSY 26
#define EFBIG 27
#define ENOSPC 28
#define ESPIPE 29
#define EROFS 30
#define EMLINK 31
#define EPIPE 32
#define EDOM 33
#define ERANGE 34
#define EDEADLK 35
#define ENAMETOOLONG 36
#define ENOLCK 37
#define ENOSYS 38
#define ENOTEMPTY 39
#define ELOOP 40
#define EWOULDBLOCK EAGAIN
#define ENOMSG 42
#define EIDRM 43
#define ECHRNG 44
#define EL2NSYNC 45
#define EL3HLT 46
#define EL3RST 47
#define ELNRNG 48
#define EUNATCH 49
#define ENOCSI 50
#define EL2HLT 51
#define EBADE 52
#define EBADR 53
#define EXFULL 54
#define ENOANO 55
#define EBADRQC 56
#define EBADSLT 57
#define EDEADLOCK EDEADLK
#define EBFONT 59
#define ENOSTR 60
#define ENODATA 61
#define ETIME 62
#define ENOSR 63
#define ENONET 64
#define ENOPKG 65
#define EREMOTE 66
#define ENOLINK 67
#define EADV 68
#define ESRMNT 69
#define ECOMM 70
#define EPROTO 71
#define EMULTIHOP 72
#define EDOTDOT 73
#define EBADMSG 74
#define EOVERFLOW 75
#define ENOTUNIQ 76
#define EBADFD 77
#define EREMCHG 78
#define ELIBACC 79
#define ELIBBAD 80
#define ELIBSCN 81
#define ELIBMAX 82
#define ELIBEXEC 83
#define EILSEQ 84
#define ERESTART 85
#define ESTRPIPE 86
#define EUSERS 87
#define ENOTSOCK 88
#define EDESTADDRREQ 89
#define EMSGSIZE 90
#define EPROTOTYPE 91
#define ENOPROTOOPT 92
#define EPROTONOSUPPORT 93
#define ESOCKTNOSUPPORT 94
#define EOPNOTSUPP 95
#define ENOTSUP EOPNOTSUPP
#define EPFNOSUPPORT 96
#define EAFNOSUPPORT 97
#define EADDRINUSE 98
#define EADDRNOTAVAIL 99
#define ENETDOWN 100
#define ENETUNREACH 101
#define ENETRESET 102
#define ECONNABORTED 103
#define ECONNRESET 104
#define ENOBUFS 105
#define EISCONN 106
#define ENOTCONN 107
#define ESHUTDOWN 108
#define ETOOMANYREFS 109
#define ETIMEDOUT 110
#define ECONNREFUSED 111
#define EHOSTDOWN 112
#define EHOSTUNREACH 113
#define EALREADY 114
#define EINPROGRESS 115
#define ESTALE 116
#define EUCLEAN 117
#define ENOTNAM 118
#define ENAVAIL 119
#define EISNAM 120
#define EREMOTEIO 121
#define EDQUOT 122
#define ENOMEDIUM 123
#define EMEDIUMTYPE 124
#define ECANCELED 125
#define ENOKEY 126
#define EKEYEXPIRED 127
#define EKEYREVOKED 128
#define EKEYREJECTED 129
#define EOWNERDEAD 130
#define ENOTRECOVERABLE 131
#define ERFKILL 132
#define EHWPOISON 133
#endif

8
inc/kern/exec.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef MINIOS_KERN_EXEC_H
#define MINIOS_KERN_EXEC_H
#include <kern/process.h>
ssize_t kern_exec(PROCESS_0 *p_proc, const char *pathname);
#endif /* MINIOS_KERN_EXEC_H */

8
inc/kern/exit.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef MINIOS_KERN_EXIT_H
#define MINIOS_KERN_EXIT_H
#include <kern/process.h>
ssize_t kern_exit(PROCESS_0 *p_proc, int exit_code);
#endif /* MINIOS_KERN_EXIT_H */

9
inc/kern/fork.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef MINIOS_KERN_FORK_H
#define MINIOS_KERN_FORK_H
#include <type.h>
#include <kern/process.h>
ssize_t kern_fork(PROCESS_0 *p_father);
#endif /* MINIOS_KERN_FORK_H */

View File

@ -6,6 +6,6 @@
ssize_t kern_read(int fd, void *buf, size_t count); ssize_t kern_read(int fd, void *buf, size_t count);
ssize_t kern_write(int fd, const void *buf, size_t count); ssize_t kern_write(int fd, const void *buf, size_t count);
void read_file(const char *filename, void *dst); ssize_t read_file(const char *filename, void *dst);
#endif /* MINIOS_KERN_FS_H */ #endif /* MINIOS_KERN_FS_H */

View File

@ -72,9 +72,9 @@
#define RIGHT (0x28 + FLAG_EXT) /* Right */ #define RIGHT (0x28 + FLAG_EXT) /* Right */
/* ACPI keys */ /* ACPI keys */
#define POWER (0x29 + FLAG_EXT) /* Power */ // #define POWER (0x29 + FLAG_EXT) /* Power */
#define SLEEP (0x2A + FLAG_EXT) /* Sleep */ // #define SLEEP (0x2A + FLAG_EXT) /* Sleep */
#define WAKE (0x2B + FLAG_EXT) /* Wake Up */ // #define WAKE (0x2B + FLAG_EXT) /* Wake Up */
/* Num Pad */ /* Num Pad */
#define PAD_SLASH (0x2C + FLAG_EXT) /* / */ #define PAD_SLASH (0x2C + FLAG_EXT) /* / */

View File

@ -1,6 +1,12 @@
#ifndef MINIOS_KERN_KMALLOC_H #ifndef MINIOS_KERN_KMALLOC_H
#define MINIOS_KERN_KMALLOC_H #define MINIOS_KERN_KMALLOC_H
#include <type.h>
void phy_free_4k(phyaddr_t v);
phyaddr_t phy_malloc_4k(void); phyaddr_t phy_malloc_4k(void);
void kfree(void *v);
void * kmalloc(size_t n);
#endif /* MINIOS_KERN_KMALLOC_H */ #endif /* MINIOS_KERN_KMALLOC_H */

View File

@ -2,7 +2,11 @@
#define MINIOS_KERN_PMAP_H #define MINIOS_KERN_PMAP_H
#include <type.h> #include <type.h>
#include <kern/process.h>
void map_kern(phyaddr_t cr3); void map_kern(u32 cr3, struct page_node **page_list);
void map_elf(PROCESS_0 *p_proc, void *elf_addr);
void map_stack(PROCESS_0 *p_proc);
void recycle_pages(struct page_node *page_list);
#endif #endif /* MINIOS_KERN_PMAP_H */

View File

@ -38,16 +38,49 @@ struct kern_context {
u32 esp; u32 esp;
}; };
enum proc_statu {IDLE, READY, SLEEP, ZOMBIE, INITING};
typedef struct s_proc PROCESS_0;
/*
*
* cr3laddr被置为-1
* 线
*/
struct page_node {
struct page_node *nxt;
phyaddr_t paddr;
uintptr_t laddr;
};
/*
*
*/
struct son_node {
struct son_node *pre;
struct son_node *nxt;
PROCESS_0 *p_son;
};
struct tree_node {
PROCESS_0 *p_fa;
struct son_node *sons;
};
/* pcb */ /* pcb */
typedef struct s_proc { struct s_proc {
struct user_context user_regs; struct user_context user_regs;
struct kern_context kern_regs; struct kern_context kern_regs;
u32 lock;
enum proc_statu statu;
u32 pid; u32 pid;
phyaddr_t cr3; phyaddr_t cr3;
struct page_node *page_list;
int exit_code;
int priority; int priority;
int ticks; int ticks;
int target_tick; struct tree_node fork_tree;
}PROCESS_0; };
#define KERN_STACKSIZE (8 * KB) #define KERN_STACKSIZE (8 * KB)
@ -60,7 +93,7 @@ typedef union u_proc {
// kern/main.c // kern/main.c
extern PROCESS *p_proc_ready; extern PROCESS *p_proc_ready;
/* pcb表 */ /* pcb表 */
#define PCB_SIZE 2 #define PCB_SIZE 20
// kern/main.c // kern/main.c
extern PROCESS proc_table[]; extern PROCESS proc_table[];
@ -71,9 +104,6 @@ void switch_kern_context(
); );
// 处理函数 // 处理函数
void schedule(void); u32 kern_get_pid(PROCESS_0 *p_proc);
u32 kern_get_pid(PROCESS *p_proc);
void kern_delay_tick(PROCESS*, u32);
extern bool has_runnable;
#endif /* MINIOS_KERN_PROCESS_H */ #endif /* MINIOS_KERN_PROCESS_H */

6
inc/kern/sche.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef MINIOS_KERN_SCHE_H
#define MINIOS_KERN_SCHE_H
void schedule(void);
#endif

View File

@ -18,10 +18,18 @@ extern ssize_t (*syscall_table[])(void);
ssize_t do_get_ticks(void); ssize_t do_get_ticks(void);
// kern/process.c // kern/process.c
ssize_t do_get_pid(void); ssize_t do_get_pid(void);
// kern/exec.c
ssize_t do_exec(const char *pathname);
// kern/exit.c
ssize_t do_exit(int status);
// kern/fork.c
ssize_t do_fork(void);
// kern/wait.c
ssize_t do_wait(int *wstatus);
// kern/fs.c // kern/fs.c
ssize_t do_read(int fd, void *buf, size_t count); ssize_t do_read(int fd, void *buf, size_t count);
ssize_t do_write(int fd, const void *buf, size_t count); ssize_t do_write(int fd, const void *buf, size_t count);
ssize_t do_delay_ticks(u32 ticks); // 不告诉你这个实现在哪
ssize_t do_mmap(int pid, void *src, void* dst, size_t length); ssize_t do_fork_ack(void);
#endif /* MINIOS_KERN_SYSCALL_H */ #endif /* MINIOS_KERN_SYSCALL_H */

View File

@ -77,10 +77,20 @@ disable_int()
static inline void static inline void
enable_int() enable_int()
{ {
if (init_kernel)
asm volatile("sti"); asm volatile("sti");
} }
#define DISABLE_INT() \
{ \
u32 IF_BIT = read_eflags() & FL_IF; \
if (IF_BIT != 0) \
disable_int();
#define ENABLE_INT() \
if (IF_BIT != 0) \
enable_int(); \
}
/* 系统调用实际处理函数(C接口) */ /* 系统调用实际处理函数(C接口) */
void syscall_handler(void); void syscall_handler(void);

8
inc/kern/wait.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef MINIOS_KERN_WAIT_H
#define MINIOS_KERN_WAIT_H
#include <type.h>
ssize_t kern_wait(int *wstatus);
#endif /* MINIOS_KERN_WAIT_H */

View File

@ -69,6 +69,7 @@
// Address in page table or page directory entry // Address in page table or page directory entry
#define PTE_ADDR(pte) ((phyaddr_t) (pte) & ~0xFFF) #define PTE_ADDR(pte) ((phyaddr_t) (pte) & ~0xFFF)
#define PTE_FLAG(pte) ((size_t) (pte) & 0xFFF)
// Control Register flags // Control Register flags
#define CR0_PE 0x00000001 // Protection Enable #define CR0_PE 0x00000001 // Protection Enable

View File

@ -6,7 +6,10 @@
#define _NR_get_pid 1 #define _NR_get_pid 1
#define _NR_read 2 #define _NR_read 2
#define _NR_write 3 #define _NR_write 3
#define _NR_delay_ticks 4 #define _NR_exec 4
#define _NR_mmap 5 #define _NR_fork 5
#define _NR_wait 6
#define _NR_exit 7
#define _NR_fork_ack 8
#endif /* MINIOS_SYSCALL_H */ #endif /* MINIOS_SYSCALL_H */

View File

@ -9,9 +9,10 @@
// lib/user/stdio.c // lib/user/stdio.c
int printf(const char *fmt, ...); int printf(const char *fmt, ...);
int vprintf(const char *fmt, va_list); int vprintf(const char *fmt, va_list);
u8 getch(); u8 getch(void);
u8 getchar(void);
void fflush(); void fflush(void);
#endif /* MINIOS_USER_STDIO_H */ #endif /* MINIOS_USER_STDIO_H */

View File

@ -11,11 +11,14 @@ ssize_t syscall3(size_t NR_syscall, size_t p1, size_t p2, size_t p3);
ssize_t syscall4(size_t NR_syscall, size_t p1, size_t p2, size_t p3, size_t p4); ssize_t syscall4(size_t NR_syscall, size_t p1, size_t p2, size_t p3, size_t p4);
ssize_t syscall5(size_t NR_syscall, size_t p1, size_t p2, size_t p3, size_t p4, size_t p5); ssize_t syscall5(size_t NR_syscall, size_t p1, size_t p2, size_t p3, size_t p4, size_t p5);
ssize_t get_ticks(); ssize_t get_ticks(void);
ssize_t get_pid(); ssize_t get_pid(void);
ssize_t read(int fd, void *buf, size_t count); ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);
ssize_t delay_ticks(u32 ticks); ssize_t exec(const char *pathname);
ssize_t lml_mmap(int pid, void* src, void* dst, size_t length); ssize_t fork(void);
ssize_t exit(int status);
ssize_t wait(int *wstatus);
ssize_t fork_ack(void);
#endif #endif

14
inc/user/wait.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef MINIOS_USER_WAIT_H
#define MINIOS_USER_WAIT_H
#define WEXITSTATUS(s) (((s) & 0xff00) >> 8)
#define WTERMSIG(s) ((s) & 0x7f)
#define WSTOPSIG(s) WEXITSTATUS(s)
#define WCOREDUMP(s) ((s) & 0x80)
#define WIFEXITED(s) (!WTERMSIG(s))
#define WIFSTOPPED(s) ((short)((((s)&0xffff)*0x10001)>>8) > 0x7f00)
#define WIFSIGNALED(s) (((s)&0xffff)-1U < 0xffu)
#define WIFCONTINUED(s) ((s) == 0xffff)
#endif /* MINIOS_USER_WAIT_H */

6164
kern.asm

File diff suppressed because it is too large Load Diff

View File

@ -9,23 +9,34 @@ KERN_ENTRY_ADDR := 0xC0200000
KERN_SRCFILES:= kern/astart.asm \ KERN_SRCFILES:= kern/astart.asm \
kern/atrap.asm \ kern/atrap.asm \
kern/aswitch.asm \ kern/aswitch.asm \
kern/exec.c \
kern/exit.c \
kern/fork.c \
kern/fork_ack.c \
kern/fs.c \ kern/fs.c \
kern/keyboard.c \
kern/kmalloc.c \
kern/main.c \ kern/main.c \
kern/pmap.c \ kern/pmap.c \
kern/process.c \ kern/process.c \
kern/sche.c \
kern/start.c \ kern/start.c \
kern/syscall.c \ kern/syscall.c \
kern/time.c \ kern/time.c \
kern/trap.c \ kern/trap.c \
kern/wait.c \
lib/kern/terminal.c \ lib/kern/terminal.c \
lib/kern/keyboard.c \
lib/kern/kmalloc.c \
lib/printfmt.c \ lib/printfmt.c \
lib/string.c lib/string.c
KERN_OBJFILES := $(patsubst %.c, $(OBJDIR)/%.o, $(KERN_SRCFILES)) KERN_OBJFILES := $(patsubst %.c, $(OBJDIR)/%.o, $(KERN_SRCFILES))
KERN_OBJFILES := $(patsubst %.asm, $(OBJDIR)/%.o, $(KERN_OBJFILES)) KERN_OBJFILES := $(patsubst %.asm, $(OBJDIR)/%.o, $(KERN_OBJFILES))
$(OBJDIR)/kern/fork_ack.o: lib/kern/fork_ack.o
@echo + cp $<
@mkdir -p $(@D)
@cp $< $(@D)
$(OBJDIR)/kern/%.o: kern/%.c $(OBJDIR)/.vars.CFLAGS $(OBJDIR)/kern/%.o: kern/%.c $(OBJDIR)/.vars.CFLAGS
@echo + cc $< @echo + cc $<
@mkdir -p $(@D) @mkdir -p $(@D)

View File

@ -230,6 +230,35 @@ general_protection:
push 13 ; vector_no = D push 13 ; vector_no = D
jmp exception jmp exception
page_fault: page_fault:
push eax
mov eax, [esp + 4]
mov [StackTop - 1024], eax
pop eax
pushad ; `.
push ds ; |
push es ; | 保存原寄存器值
push fs ; |
push gs ; /
mov dx, ss
mov ds, dx
mov es, dx
mov eax, esp
test dword [eax + CSREG - P_STACKBASE], 3
jz .1 ; 根据段寄存器的状态信息判断是否发生内核重入
mov esp, StackTop ; 如果没有发生内核重入就意味着要到用户栈了先将esp移入临时内核栈
push eax ; 传入进程表首地址信息
call to_kern_stack ; 这个函数之后esp变为进程内核栈
pop eax ; 恢复进程表首地址信息这个是restart第一句话要用的
.1:
mov ecx, [eax + EFLAGSREG - P_STACKBASE]
push ecx
mov ecx, [eax + CSREG - P_STACKBASE]
push ecx
mov ecx, [eax + EIPREG - P_STACKBASE]
push ecx
mov ebx, [StackTop - 1024]
push ebx
push 14 ; vector_no = E push 14 ; vector_no = E
jmp exception jmp exception
copr_error: copr_error:

150
kern/exec.c Normal file
View File

@ -0,0 +1,150 @@
#include <assert.h>
#include <errno.h>
#include <elf.h>
#include <string.h>
#include <x86.h>
#include <kern/fs.h>
#include <kern/exec.h>
#include <kern/kmalloc.h>
#include <kern/pmap.h>
#include <kern/protect.h>
#include <kern/sche.h>
#include <kern/syscall.h>
#include <kern/trap.h>
static inline void
init_segment_regs(PROCESS_0 *p_proc)
{
p_proc->user_regs.cs = (SELECTOR_FLAT_C & SA_RPL_MASK & SA_TI_MASK)
| SA_TIL | RPL_USER;
p_proc->user_regs.ds = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK)
| SA_TIL | RPL_USER;
p_proc->user_regs.es = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK)
| SA_TIL | RPL_USER;
p_proc->user_regs.fs = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK)
| SA_TIL | RPL_USER;
p_proc->user_regs.ss = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK)
| SA_TIL | RPL_USER;
p_proc->user_regs.gs = (SELECTOR_VIDEO & SA_RPL_MASK & SA_TI_MASK)
| RPL_USER;
}
static inline void
init_pagetbl(PROCESS_0 *p_proc)
{
phyaddr_t new_cr3 = phy_malloc_4k();
memset((void *)K_PHY2LIN(new_cr3), 0, PGSIZE);
struct page_node *new_page_list = kmalloc(sizeof(struct page_node));
new_page_list->nxt = NULL;
new_page_list->paddr = new_cr3;
new_page_list->laddr = -1;
map_kern(new_cr3, &new_page_list);
// 这里需要特别注意的是,替换用户页表这种危险行为
// 无论如何都是要关中断的,不允许中间有任何调度
// 否则很有可能换到一半,啪,一个中断进来调度了
// 调度回来时要加载cr3然后惊喜地发现page fault了
struct page_node *old_page_list;
DISABLE_INT();
old_page_list = p_proc->page_list;
p_proc->cr3 = new_cr3;
p_proc->page_list = new_page_list;
lcr3(p_proc->cr3);
ENABLE_INT();
// 最后记得回收进程页面资源
recycle_pages(old_page_list);
}
ssize_t
kern_exec(PROCESS_0 *p_proc, const char *pathname)
{
ssize_t ret = 0;
// 路径名的地址必须在内核中的地址
// 因为exec会回收用户程序的页表
// 这会导致程序中的地址发生缺页触发page fault
assert((uintptr_t)pathname >= K_PHY2LIN(0));
// 进来先给自己上个锁
while (xchg(&p_proc->lock, 1) == 1)
schedule();
if ((ret = read_file(pathname, (void *)K_PHY2LIN(48 * MB))) < 0)
goto free;
// 这里有一个特判如果不是elf文件会返回ENOEXEC
if (*(uintptr_t *)K_PHY2LIN(48 * MB) != ELF_MAGIC) {
ret = -ENOEXEC;
goto free;
}
assert(p_proc->statu == READY);
// 初始化用户寄存器
memset(&p_proc->user_regs, 0, sizeof(p_proc->user_regs));
init_segment_regs(p_proc);
p_proc->user_regs.eflags = 0x1202; /* IF=1, IOPL=1 */
// 初始化页表
init_pagetbl(p_proc);
// 将elf加载到文件指定的地址
map_elf(p_proc, (void *)K_PHY2LIN(48 * MB));
// 初始化用户栈空间
map_stack(p_proc);
free:
// 最后记得释放锁
xchg(&p_proc->lock, 0);
return ret;
}
/*
* src路径名转换为段目录项名到dst
* -ENOENT
*/
static inline ssize_t
translate_pathname(char *dst, const char *src)
{
assert(strlen(dst) == 11);
char *st = (char *)src;
char *ed = st + strlen(st);
char *dot = ed;
for (char *c = st ; *c ; c++) {
if (*c == '.')
dot = c;
}
if (dot - st > 8)
return -ENOENT;
memcpy(dst, st, dot - st);
if (ed - dot - 1 > 3)
return -ENOENT;
memcpy(dst + 8, dot + 1, ed == dot ? 0 : ed - dot - 1);
for (char *c = dst ; *c ; c++) {
if ('a' <= *c && *c <= 'z')
*c += 'A' - 'a';
}
return 0;
}
ssize_t
do_exec(const char *pathname)
{
ssize_t ret = 0;
char *name_cpy;
// 原来路径名转短目录项名的工作可以交给do_exec来做
name_cpy = kmalloc(12);
memset(name_cpy, ' ', 11);
name_cpy[11] = '\0';
// 当然如果转文件名失败了会返回-ENOENT
if ((ret = translate_pathname(name_cpy, pathname)) < 0)
goto free;
ret = kern_exec(&p_proc_ready->pcb, name_cpy);
free:
if (name_cpy)
kfree(name_cpy);
return ret;
}

117
kern/exit.c Normal file
View File

@ -0,0 +1,117 @@
#include <assert.h>
#include <x86.h>
#include <kern/exit.h>
#include <kern/kmalloc.h>
#include <kern/pmap.h>
#include <kern/sche.h>
#include <kern/syscall.h>
#include <kern/trap.h>
static void
awake_father_and_become_zombie(PROCESS_0 *p_proc)
{
PROCESS_0 *p_fa;
// 由于我们已经协商了上锁的顺序,上锁顺序是从父亲到儿子
// 但是这里我们必须锁了自己才能知道父进程是谁
// 所以这里我们换了一种做法,不再一个劲的等锁
// 如果父进程上锁失败,就直接将自己的锁释放拱手让人
// 这样做有个好处,要么两个锁同时被上掉,要么两个锁同时被释放
// 这也是一个非常有趣的实现方法
// 而真实情况是将大锁拆小锁不可能一个pcb就一个大锁保着这样又浪费效率又难写
while (1) {
if (xchg(&p_proc->lock, 1) == 1)
goto loop;
p_fa = p_proc->fork_tree.p_fa;
if (xchg(&p_fa->lock, 1) == 1)
goto free;
break;
free:
xchg(&p_proc->lock, 0);
loop:
schedule();
}
// 这两句assert防止其他奇奇怪怪的状态出现
assert(p_proc->statu == READY);
assert(p_fa->statu == READY || p_fa->statu == SLEEP);
p_proc->statu = ZOMBIE;
p_fa->statu = READY;
xchg(&p_fa->lock, 0);
xchg(&p_proc->lock, 0);
}
static void
transfer_orphans(PROCESS_0 *p_proc)
{
PROCESS_0 *p_init = &proc_table[0].pcb;
// 上锁顺序为:初始进程->当前进程->子进程
while (xchg(&p_init->lock, 1) == 1)
schedule();
while (xchg(&p_proc->lock, 1) == 1)
schedule();
for (struct son_node *p = p_proc->fork_tree.sons ; p ;) {
PROCESS_0 *p_son = p->p_son;
struct son_node *p_nxt = p->nxt;
// 上子进程的锁,因为需要修改子进程的父进程信息(移到初始进程下)
while (xchg(&p_son->lock, 1) == 1)
schedule();
// 将子进程的进程树信息做修改
// 将节点node移到初始进程的链表头处
p_son->fork_tree.p_fa = p_init;
// 确保这个是双向链表头
assert(p->pre == NULL);
// 接下来就是一坨又臭又长的链表操作部分
if (p->nxt != NULL)
p->nxt->pre = p->pre;
p->nxt = p_init->fork_tree.sons;
if (p->nxt != NULL)
p->nxt->pre = p;
p_init->fork_tree.sons = p;
// 最后释放子进程的锁
xchg(&p_son->lock, 0);
p = p_nxt;
}
// 在移交完后当前进程的子进程信息会被清空
p_proc->fork_tree.sons = NULL;
// 初始进程可能在休眠,而且子进程可能是僵尸进程,需要将初始进程唤醒
// 初始进程会一直调用wait系统调用回收僵尸子进程
assert(p_init->statu == READY || p_init->statu == SLEEP);
p_init->statu = READY;
free:
xchg(&p_proc->lock, 0);
xchg(&p_init->lock, 0);
}
ssize_t
kern_exit(PROCESS_0 *p_proc, int exit_code)
{
// 托孤,将所有子进程转移到初始进程下
transfer_orphans(p_proc);
// 上锁修改exit code
while (xchg(&p_proc->lock, 1) == 1)
schedule();
p_proc->exit_code = exit_code;
xchg(&p_proc->lock, 0);
// 下面两个操作会修改进程的状态,
// 这是非常危险的,最好用开关中断保护上
DISABLE_INT();
// 这个函数干了两件事,唤醒父进程,将自己状态置为僵尸进程
// 关中断就相当于两件事同时干了
awake_father_and_become_zombie(p_proc);
// 在触发了调度之后这个进程在被回收之前永远无法被调度到
schedule();
ENABLE_INT();
panic("exit failed!");
}
ssize_t
do_exit(int status)
{
// 为什么这个参数这么奇怪?你可能需要读读手册
return kern_exit(&p_proc_ready->pcb, (status & 0xFF) << 8);
}

57
kern/fork.c Normal file
View File

@ -0,0 +1,57 @@
#include <assert.h>
#include <kern/fork.h>
#include <kern/syscall.h>
ssize_t
kern_fork(PROCESS_0 *p_fa)
{
// 这可能是你第一次实现一个比较完整的功能,你可能会比较畏惧
// 但是放心,别怕,先别想自己要实现一个这么大的东西而毫无思路
// 这样你在焦虑的同时也在浪费时间,就跟你在实验五中被页表折磨一样
// 人在触碰到未知的时候总是害怕的,这是天性,所以请你先冷静下来
// fork系统调用会一步步引导你写出来不会让你本科造火箭的
panic("Unimplement! CALM DOWN!");
// 推荐是边写边想,而不是想一车然后写,这样非常容易计划赶不上变化
// fork的第一步你需要找一个空闲IDLE的进程作为你要fork的子进程
panic("Unimplement! find a idle process");
// 再之后你需要做的是好好阅读一下pcb的数据结构搞明白结构体中每个成员的语义
// 别光扫一遍,要搞明白这个成员到底在哪里被用到了,具体是怎么用的
// 可能exec和exit系统调用的代码能够帮助你对pcb的理解不先理解好pcb你fork是无从下手的
panic("Unimplement! read pcb");
// 在阅读完pcb之后终于可以开始fork工作了
// 本质上相当于将父进程的pcb内容复制到子进程pcb中
// 但是你需要想清楚,哪些应该复制到子进程,哪些不应该复制,哪些应该子进程自己初始化
// 其中有三个难点
// 1. 子进程"fork"的返回值怎么处理?(需要你对系统调用整个过程都掌握比较清楚,如果非常清晰这个问题不会很大)
// 2. 子进程内存如何复制别傻乎乎地复制父进程的cr3本质上相当于与父进程共享同一块内存
// 而共享内存肯定不符合fork的语义这样一个进程写内存某块地方会影响到另一个进程这个东西需要你自己思考如何复制父进程的内存
// 3. 在fork结束后肯定会调度到子进程那么你怎么保证子进程能够正常进入用户态
// (你肯定会觉得这个问题问的莫名其妙的,只能说你如果遇到那一块问题了就会体会到这个问题的重要性,
// 这需要你对调度整个过程都掌握比较清楚)
panic("Unimplement! copy pcb?");
// 别忘了维护进程树,将这对父子进程关系添加进去
panic("Unimplement! maintain process tree");
// 最后你需要将子进程的状态置为READY说明fork已经好了子进程准备就绪了
panic("Unimplement! change status to READY");
// 在你写完fork代码时先别急着运行跑先要对自己来个灵魂拷问
// 1. 上锁上了吗?所有临界情况都考虑到了吗?(永远要相信有各种奇奇怪怪的并发问题)
// 2. 所有错误情况都判断到了吗错误情况怎么处理RTFM->`man 2 fork`
// 3. 你写的代码真的符合fork语义吗
panic("Unimplement! soul torture");
return 0;
}
ssize_t
do_fork(void)
{
return kern_fork(&p_proc_ready->pcb);
}

View File

@ -1,4 +1,5 @@
#include <assert.h> #include <assert.h>
#include <errno.h>
#include <fat32.h> #include <fat32.h>
#include <mmu.h> #include <mmu.h>
#include <string.h> #include <string.h>
@ -137,7 +138,7 @@ read_data_sec(void *dst, u32 current_clus)
* filename要求是短目录项名11 * filename要求是短目录项名11
* dst推荐是3GB + 48MB * dst推荐是3GB + 48MB
*/ */
void ssize_t
read_file(const char *filename, void *dst) read_file(const char *filename, void *dst)
{ {
assert(strlen(filename) == 11); assert(strlen(filename) == 11);
@ -171,10 +172,11 @@ read_file(const char *filename, void *dst)
} }
if (file_clus == 0) if (file_clus == 0)
panic("file can't found! filename: %s", filename); return -ENOENT;
// 读入文件 // 读入文件
while (file_clus < 0x0FFFFFF8) { while (file_clus < 0x0FFFFFF8) {
dst = read_data_sec(dst, file_clus); dst = read_data_sec(dst, file_clus);
file_clus = get_next_clus(file_clus); file_clus = get_next_clus(file_clus);
} }
return 0;
} }

View File

@ -1,22 +0,0 @@
#include <assert.h>
#include <mmu.h>
#include <kern/kmalloc.h>
#include <kern/trap.h>
static phyaddr_t malloc_4k_p = 64 * MB;
/*
* 4kb
* 64MB~128MB
*/
phyaddr_t
phy_malloc_4k(void)
{
assert(malloc_4k_p < 128 * MB);
phyaddr_t addr = malloc_4k_p;
malloc_4k_p += PGSIZE;
return addr;
}

View File

@ -1,18 +1,11 @@
#include <assert.h> #include <assert.h>
#include <mmu.h>
#include <string.h>
#include <type.h>
#include <x86.h> #include <x86.h>
#include <elf.h>
#include <kern/fs.h>
#include <kern/kmalloc.h>
#include <kern/stdio.h> #include <kern/stdio.h>
#include <kern/pmap.h>
#include <kern/process.h>
#include <kern/protect.h>
#include <kern/trap.h>
#include <kern/syscall.h> #include <kern/syscall.h>
#include <kern/protect.h>
#include <kern/process.h>
#include <kern/trap.h>
// 标志着内核是否处理完成 // 标志着内核是否处理完成
bool init_kernel; bool init_kernel;
@ -22,35 +15,6 @@ PROCESS *p_proc_ready;
// pcb表 // pcb表
PROCESS proc_table[PCB_SIZE]; PROCESS proc_table[PCB_SIZE];
static inline void
init_segment_regs(PROCESS *p_proc)
{
p_proc->pcb.user_regs.cs = (SELECTOR_FLAT_C & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_USER;
p_proc->pcb.user_regs.ds = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_USER;
p_proc->pcb.user_regs.es = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_USER;
p_proc->pcb.user_regs.fs = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_USER;
p_proc->pcb.user_regs.ss = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_USER;
p_proc->pcb.user_regs.gs = (SELECTOR_VIDEO & SA_RPL_MASK & SA_TI_MASK) | RPL_USER;
}
static inline void
lml_map(phyaddr_t cr3, uintptr_t vaddr, phyaddr_t paddr, u32 flags)
{
uintptr_t *pde_ptr = (uintptr_t *)K_PHY2LIN(cr3);
// get pdx item, pde_ptr + PDX(vaddr)
pde_ptr += PDX(vaddr);
if (!(*pde_ptr & PTE_P))
{
// pdx ot exists, allocate a page and assign to pde
*pde_ptr = phy_malloc_4k() | PTE_P | flags;
}
uintptr_t *ptx_ptr = (uintptr_t *)K_PHY2LIN(*pde_ptr & (~0xfff));
// get ptx item
ptx_ptr += PTX(vaddr);
// assume paddr is allocated, just set it to paddr[31:12] | FLAGS
*ptx_ptr = (paddr & (~0xfff)) | PTE_P | flags;
}
/* /*
* main函数 * main函数
* *
@ -59,158 +23,33 @@ void kernel_main(void)
{ {
kprintf("---start kernel main---\n"); kprintf("---start kernel main---\n");
PROCESS *p_proc = proc_table;
for (int i = 0; i < PCB_SIZE; i++, p_proc++)
{
// 初始化进程段寄存器
init_segment_regs(p_proc);
// 为进程分配cr3物理内存
p_proc->pcb.cr3 = phy_malloc_4k();
// 将3GB~3GB+128MB的线性地址映射到0~128MB的物理地址
map_kern(p_proc->pcb.cr3);
// 在map_kern之后就内核程序对应的页表已经被映射了
// 就可以直接lcr3于此同时执行流不会触发page fault
// 如果不先map_kern执行流会发现执行的代码的线性地址不存在爆出Page Fault
// 当然选不选择看个人的想法,评价是都行,各有各的优缺点
// lcr3(p_proc->pcb.cr3);
static char filename[PCB_SIZE][12] = {
// "TESTPID BIN",
// "TESTKEY BIN",
// "DELAY BIN",
// "DELAY BIN",
// "DELAY BIN",
"SNAKE BIN",
"HACK BIN",
};
// 从磁盘中将文件读出需要注意的是要满足短目录项的文件名长度11
// 前八个为文件名后三个为后缀名跟BootLoader做法一致
// 推荐将文件加载到3GB + 48MB处应用程序保证不会有16MB那么大
read_file(filename[i], (void *)K_PHY2LIN(48 * MB));
// 现在你就需要将从磁盘中读出的ELF文件解析到用户进程的地址空间中
// panic("unimplement! load elf file");
// ---------------------------- load elf --------------------------------------
void *elf_start = (void *)K_PHY2LIN(48 * MB);
assert(*(u32 *)elf_start == ELF_MAGIC);
Elf32_Ehdr *ehdr = (Elf32_Ehdr *)elf_start;
for (int i = 0; i < ehdr->e_phnum; ++i)
{
Elf32_Phdr *phdr = elf_start + i * ehdr->e_phentsize + ehdr->e_phoff;
if (phdr->p_type == PT_LOAD)
{
u32 vpage_fst = phdr->p_va & (~0xfff); // clear low 12 bit, start of the first pg
u32 vpage_lst = (phdr->p_va + phdr->p_memsz - 1) & (~0xfff); // start of the last pg
// in fact, we allocate 4k aligned vpage to every segment;
// its ok because elf32 segment align is 0x1000
u32 page_num = ((vpage_lst - vpage_fst) >> 12) + 1;
phyaddr_t newpage = 0; // useless initialization
for (int j = 0; j < page_num; ++j)
{
newpage = phy_malloc_4k();
lml_map(p_proc->pcb.cr3, vpage_fst + (j << 12), newpage, PTE_P | PTE_W | PTE_U);
}
phyaddr_t page_start = newpage - (page_num - 1) * 4 * KB;
memcpy((void *)K_PHY2LIN(page_start), (void *)(elf_start + phdr->p_offset), phdr->p_filesz);
memset((void *)K_PHY2LIN(page_start) + phdr->p_filesz, 0, phdr->p_memsz - phdr->p_filesz);
}
}
// 上一个实验中,我们开栈是用内核中的一个数组临时充当栈
// 但是现在就不行了用户是无法访问内核的地址空间3GB ~ 3GB + 128MB
// 需要你自行处理给用户分配用户栈。
// ---------------------------- alloc user stack ------------------------------
phyaddr_t ustack_low = phy_malloc_4k();
for (int i = 0; i < 6; ++i)
phy_malloc_4k();
phyaddr_t ustack_high = phy_malloc_4k(); // alloc 8 pages, 32KB stack for each program
for (int i = 0; i < 8; ++i)
{
lml_map(p_proc->pcb.cr3, (3 * GB - (8 - i) * PGSIZE), ustack_low + i * PGSIZE, PTE_P | PTE_W | PTE_U);
}
p_proc->pcb.user_regs.esp = 3 * GB; // high to low stack
p_proc->pcb.user_regs.eip = ehdr->e_entry; // refer to restart:
// 初始化用户寄存器
p_proc->pcb.user_regs.eflags = 0x1202; /* IF=1, IOPL=1 */
// panic("unimplement! init user stack and esp");
// 接下来初始化内核寄存器,
// 为什么需要初始化内核寄存器原因是加入了系统调用后
// 非常有可能出现系统调用执行过程中插入其余中断的情况,
// 如果跟之前一样所有进程共享一个内核栈会发生不可想象的结果,
// 为了避免这种情况,就需要给每个进程分配一个进程栈。
// 当触发时钟中断发生调度的时候不再是简单的切换p_proc_ready
// 而是需要将内核栈进行切换,而且需要切换执行流到另一个进程的内核栈。
// 所以需要一个地方存放当前进程的寄存器上下文,这是一个富有技巧性的活,深入研究会觉得很妙,
// 如果需要深入了解去查看kern/process.c中的schedule函数了解切换细节。
p_proc->pcb.kern_regs.esp = (u32)(p_proc + 1) - 8;
// 保证切换内核栈后执行流进入的是restart函数。
*(u32 *)(p_proc->pcb.kern_regs.esp + 0) = (u32)restart;
// 这里是因为restart要用`pop esp`确认esp该往哪里跳。
*(u32 *)(p_proc->pcb.kern_regs.esp + 4) = (u32)&p_proc->pcb;
// 初始化其余量
p_proc->pcb.pid = i;
static int priority_table[PCB_SIZE] = {1, 1};
// priority 预计给每个进程分配的时间片
// ticks 进程剩余的进程片
p_proc->pcb.priority = p_proc->pcb.ticks = priority_table[i];
p_proc->pcb.target_tick = 0;
}
p_proc_ready = proc_table; p_proc_ready = proc_table;
// 切换进程页表和tss
lcr3(p_proc_ready->pcb.cr3); PROCESS_0 *p_proc = &p_proc_ready->pcb;
tss.esp0 = (u32)(&p_proc_ready->pcb.user_regs + 1); p_proc->statu = READY;
// kern regs
p_proc->kern_regs.esp = (u32)(p_proc_ready + 1) - 8;
// 保证切换内核栈后执行流进入的是restart函数。
*(u32 *)(p_proc->kern_regs.esp + 0) = (u32)restart;
// 这里是因为restart要用`pop esp`确认esp该往哪里跳。
*(u32 *)(p_proc->kern_regs.esp + 4) = (u32)p_proc;
// 初始化进程树
p_proc->fork_tree.p_fa = NULL;
p_proc->fork_tree.sons = NULL;
// 初始化ticks
p_proc->priority = p_proc->ticks = 1;
// 在实现了exec系统调用后main函数的负担就少了
// 只需要调用一个函数接口就能实现进程的加载
if (do_exec("initproc.bin") < 0)
panic("init process failed");
// 切换tss
tss.esp0 = (u32)(&p_proc->user_regs + 1);
lcr3(p_proc->cr3);
init_kernel = true; init_kernel = true;
// 开个无用的kern_context存当前执行流的寄存器上下文之后就没用了直接放在临时变量中 // 开个无用的kern_context存当前执行流的寄存器上下文之后就没用了直接放在临时变量中
struct kern_context idle; struct kern_context idle;
switch_kern_context(&idle, &p_proc_ready->pcb.kern_regs); switch_kern_context(&idle, &p_proc->kern_regs);
assert(0); assert(0);
} }
static inline void
sys_mmap(phyaddr_t cr3_src, phyaddr_t cr3_dst, uintptr_t va_src, uintptr_t va_dst)
{
uintptr_t *pde_ptr_src = (uintptr_t *)K_PHY2LIN(cr3_src);
uintptr_t *pde_ptr_dst = (uintptr_t *)K_PHY2LIN(cr3_dst);
// its quite okay, kernel space is mapped to all processes
pde_ptr_src += PDX(va_src);
pde_ptr_dst += PDX(va_dst);
assert((*pde_ptr_dst & PTE_P) && (*pde_ptr_src & PTE_P));
// it is not a map procedure, so pde should be present
uintptr_t *pte_ptr_src = (uintptr_t *)K_PHY2LIN(*pde_ptr_src & (~0xfff));
uintptr_t *pte_ptr_dst = (uintptr_t *)K_PHY2LIN(*pde_ptr_dst & (~0xfff));
pte_ptr_src += PTX(va_src);
pte_ptr_dst += PTX(va_dst);
assert((*pte_ptr_dst & PTE_P) && (*pte_ptr_src & PTE_P));
// make should page table entry also exists
*pte_ptr_dst = *pte_ptr_src;
// simple assign src to dst, no need to recycle, though should be
}
#define PG_MASK 0x0fff // 12bit mask
ssize_t do_mmap(int pid, void *src, void *dst, size_t length)
{
if ((length & PG_MASK) || ((u32)src & PG_MASK) || ((u32)dst & PG_MASK))
{
// make sure src,dst,len are 4K aligned
assert(0);
return -1;
}
if (pid > PCB_SIZE)
{
assert(0);
return -1;
}
disable_int();
for (int off = 0; off < length; off += PGSIZE)
{
sys_mmap(proc_table[pid].pcb.cr3, p_proc_ready->pcb.cr3, (uintptr_t)src + off, (uintptr_t)dst + off);
}
enable_int();
return 0;
}

View File

@ -1,46 +1,154 @@
#include <assert.h> #include <assert.h>
#include <elf.h>
#include <string.h>
#include <mmu.h> #include <mmu.h>
#include <kern/kmalloc.h> #include <kern/kmalloc.h>
#include <kern/pmap.h> #include <kern/pmap.h>
/*
* page_list页面信息
*
*/
static phyaddr_t
alloc_phy_page(struct page_node **page_list)
{
phyaddr_t paddr = phy_malloc_4k();
struct page_node *new_node = kmalloc(sizeof(struct page_node));
new_node->nxt = *page_list;
new_node->paddr = paddr;
new_node->laddr = -1;
*page_list = new_node;
return paddr;
}
/*
* MINIOS中比较通用的页表映射函数
* laddr处的虚拟页面映射到物理地址为paddrpaddr为-1
* pte_flag置位到页表项PTE_P | PTE_W | PTE_U
* page_list这个链表中
*/
static void
lin_mapping_phy(u32 cr3,
struct page_node **page_list,
uintptr_t laddr,
phyaddr_t paddr,
u32 pte_flag)
{
assert(PGOFF(laddr) == 0);
uintptr_t *pde_ptr = (uintptr_t *)K_PHY2LIN(cr3);
if ((pde_ptr[PDX(laddr)] & PTE_P) == 0) {
phyaddr_t pte_phy = alloc_phy_page(page_list);
memset((void *)K_PHY2LIN(pte_phy), 0, PGSIZE);
pde_ptr[PDX(laddr)] = pte_phy | PTE_P | PTE_W | PTE_U;
}
phyaddr_t pte_phy = PTE_ADDR(pde_ptr[PDX(laddr)]);
uintptr_t *pte_ptr = (uintptr_t *)K_PHY2LIN(pte_phy);
phyaddr_t page_phy;
if (paddr == (phyaddr_t)-1) {
if ((pte_ptr[PTX(laddr)] & PTE_P) != 0)
return;
page_phy = alloc_phy_page(page_list);
(*page_list)->laddr = laddr;
} else {
if ((pte_ptr[PTX(laddr)] & PTE_P) != 0)
warn("this page was mapped before, laddr: %x", laddr);
assert(PGOFF(paddr) == 0);
page_phy = paddr;
}
pte_ptr[PTX(laddr)] = page_phy | pte_flag;
}
/* /*
* *
* 3GB ~ 3GB + 128MB的线性地址映射到0 ~ 128MB的物理地址 * 3GB ~ 3GB + 128MB的线性地址映射到0 ~ 128MB的物理地址
*/ */
void void
map_kern(phyaddr_t cr3) map_kern(u32 cr3, struct page_node **page_list)
{ {
// 初始化pde页目录表 for (phyaddr_t paddr = 0 ; paddr < 128 * MB ; paddr += PGSIZE) {
// 由于cr3是物理地址需要进行一步转化转化到线性地址才能访存 lin_mapping_phy(cr3,
uintptr_t *pde_ptr = (uintptr_t *)K_PHY2LIN(cr3); page_list,
// 基地址在3GB处 K_PHY2LIN(paddr),
pde_ptr += PDX(3 * GB); paddr,
// 一个页目录项映射4MB的内存计算需要初始化的页目录项数目 PTE_P | PTE_W | PTE_U);
int pde_num = (128 * MB) / (4 * MB); }
// 被映射的物理地址 }
phyaddr_t phy_addr = 0;
while (pde_num--) {
// 对于每一个页目录项申请一页用于页表
phyaddr_t pte_phy = phy_malloc_4k();
// 保证申请出来的物理地址是4k对齐的
assert(PGOFF(pte_phy) == 0);
// 初始化页目录项,权限是存在(P)|写(W)|用户能够访问(U)
*pde_ptr++ = pte_phy | PTE_P | PTE_W | PTE_U;
// 接下来对新申请的页表初始化页表项 /*
// 由于申请的pte_phy是物理地址需要进行一步转化转化到线性地址才能访存 * elf文件信息将数据搬迁到指定位置
uintptr_t *pte_ptr = (uintptr_t *)K_PHY2LIN(pte_phy); * eip的置位
// 初始化页表的所有页表项 *
int pte_num = NPTENTRIES; */
while (pte_num--) { void
// 初始化页表项,权限是存在(P)|写(W),用户不能访问 map_elf(PROCESS_0 *p_proc, void *elf_addr)
// 直接线性映射物理页 {
*pte_ptr++ = phy_addr | PTE_P | PTE_W; assert(p_proc->lock != 0);
// 换下一个物理页
phy_addr += PGSIZE; struct Elf *eh = (struct Elf *)elf_addr;
struct Proghdr *ph = (struct Proghdr *)(elf_addr + eh->e_phoff);
for (int i = 0 ; i < eh->e_phnum ; i++, ph++) {
if (ph->p_type != PT_LOAD)
continue;
uintptr_t st = ROUNDDOWN(ph->p_va, PGSIZE);
uintptr_t en = ROUNDUP(st + ph->p_memsz, PGSIZE);
for (uintptr_t laddr = st ; laddr < en ; laddr += PGSIZE) {
u32 pte_flag = PTE_P | PTE_U;
if ((ph->p_flags & ELF_PROG_FLAG_WRITE) != 0)
pte_flag |= PTE_W;
lin_mapping_phy(p_proc->cr3,
&p_proc->page_list,
laddr,
(phyaddr_t)-1,
pte_flag);
}
memcpy( (void *)ph->p_va,
(const void *)eh + ph->p_offset,
ph->p_filesz);
memset( (void *)ph->p_va + ph->p_filesz,
0,
ph->p_memsz - ph->p_filesz);
}
p_proc->user_regs.eip = eh->e_entry;
}
/*
* 访
* (0xbffff000~0xc0000000)
* esp寄存器放置好
*/
void
map_stack(PROCESS_0 *p_proc)
{
assert(p_proc->lock != 0);
lin_mapping_phy(p_proc->cr3,
&p_proc->page_list,
K_PHY2LIN(-PGSIZE),
(phyaddr_t)-1,
PTE_P | PTE_W | PTE_U);
p_proc->user_regs.esp = K_PHY2LIN(0);
}
/*
* page_list回收所有的页面
*/
void
recycle_pages(struct page_node *page_list)
{
for (struct page_node *prevp, *p = page_list ; p ;) {
phy_free_4k(p->paddr);
prevp = p, p = p->nxt;
kfree(prevp);
} }
} }
// 经常assert是个好习惯
assert(phy_addr == 128 * MB);
}

View File

@ -1,14 +1,8 @@
#include <elf.h>
#include <mmu.h>
#include <string.h>
#include <x86.h> #include <x86.h>
#include <kern/trap.h>
#include <kern/pmap.h>
#include <kern/process.h> #include <kern/process.h>
#include <kern/protect.h> #include <kern/sche.h>
#include <kern/syscall.h> #include <kern/syscall.h>
#include <kern/time.h>
/* /*
* *
@ -34,100 +28,20 @@ to_kern_stack(u32 ret_esp)
); );
} }
/*
*
*/
bool has_runnable = true;
void
schedule(void)
{
// 获取当前的eflags的中断位
u32 IF_BIT = read_eflags() & FL_IF;
// 如果中断没关,一定!一定!一定!要关中断保证当前执行流操作的原子性
if (IF_BIT != 0)
disable_int();
// kprintf("sched\n");
PROCESS *p_cur_proc = p_proc_ready;
PROCESS *p_next_proc = p_proc_ready;
has_runnable = false;
_again:
do {
p_next_proc = p_next_proc + 1;
if (p_next_proc >= proc_table + PCB_SIZE) {
p_next_proc = proc_table;
}
if (p_next_proc->pcb.target_tick == 0 || p_next_proc->pcb.target_tick <= kern_get_ticks()) {
p_next_proc->pcb.target_tick = 0;
has_runnable = true;
break;
}
} while(p_next_proc != p_proc_ready);
if (!has_runnable) {
enable_int(); // remember to enable int, or it'll die here
asm volatile("hlt"); // hlt will wake up when int comes
IF_BIT = read_eflags() & FL_IF;
if (IF_BIT != 0) disable_int(); // retry with int disabled
goto _again;
}
p_proc_ready = p_next_proc;
// 切换进程页表和tss
lcr3(p_proc_ready->pcb.cr3);
tss.esp0 = (u32)(&p_proc_ready->pcb.user_regs + 1);
// 调度函数最精髓的部分,非常的有意思。
// 对于目前的os来说eax,ebx,ecx,edx,esi,edi,esp,ebp,eflags这九个寄存器就可以表达当前执行流的上下文了
// 所以这个函数干了个什么事呢它先将当前的esp保存到kern_regs中保存当前的栈信息
// 之后将esp移到当前kern_context栈顶然后按顺序依次将eax,ecx,edx,ebx,ebp,esi,edi,eflags压栈
// 这样就把当前进程寄存器上下文都保存下来了再之后esp移到目标kern_context栈底
// 再依次恢复eflags,edi,esi,ebp,ebx,edx,ecx,eax
// 最后一步恢复esp这个esp本身被push时保留的是进程栈中的esp这样就完成了执行流的切换。
// 从当前进程的角度上这就很有意思,相当于执行流进入了一个函数之后突然就被送往其他次元了,而且当前次元的时间静止了。
// 然后再之后突然又回来了,还是之前的那些寄存器信息,感觉这个函数啥都没干。
// 由于涉及到一大堆跟执行流切换相关的操作,所以一定!一定!一定!要关中断。
switch_kern_context(
&p_cur_proc->pcb.kern_regs,
&p_next_proc->pcb.kern_regs);
// 在传送回之后需要做的第一件事就是忘掉所有的全局变量的缓存
// 防止编译器优化导致某些变量被优化掉比如说某个全局变量a
// 你拿eax去获取a的值在发生调度之后a的值发生变化下面用到了a
// 然后编译器想都不想就直接用eax接着用了一个bug就这么来了
// 这玩意学术名词叫内存屏障,这还只是单核,如果多核就更加恐怖了,
// 你会看到内存屏障遍地插这也是为什么os难的一部分原因跟缓存斗跟硬件斗跟编译器斗跟并发斗其乐无穷。
// 要敬佩那些写系统软件的人,斗都要斗麻了的。
asm volatile ("":::"memory");
// 这里可能你会比较疑惑为什么还要开中断,
// 这个时候你需要要以单线程的思路去想,
// 上面的switch_kern_context在执行后执行流就交给了其他进程了
// 但是其他进程总是会通过调度调度回来,对于当前这个进程来说,
// 它相当于突然被送到其他次元了包还掉在地上关中断而且那个次元的时间就静止了The World
// 当在其他次元兜兜转转回来了之后,还得把包捡回来(开中断)。
// 不用去关心其他次元是怎么干的,但是对于这个次元来说还是得捡包的。
// 如果每个次元都遵守这个规则,就可以保证执行不会出问题。
if (IF_BIT != 0)
enable_int();
}
u32 u32
kern_get_pid(PROCESS *p_proc) kern_get_pid(PROCESS_0 *p_proc)
{ {
return p_proc->pcb.pid; u32 ret = 0;
while (xchg(&p_proc->lock, 1) == 1)
schedule();
ret = p_proc->pid;
free:
xchg(&p_proc->lock, 0);
return ret;
} }
ssize_t ssize_t
do_get_pid(void) do_get_pid(void)
{ {
return (ssize_t)kern_get_pid(p_proc_ready); return (ssize_t)kern_get_pid(&p_proc_ready->pcb);
}
void kern_delay_ticks(PROCESS *p_proc, u32 target_tick) {
p_proc->pcb.target_tick = target_tick;
}
ssize_t
do_delay_ticks(u32 ticks) {
kern_delay_ticks(p_proc_ready, kern_get_ticks() + ticks);
schedule();
return 0;
} }

48
kern/sche.c Normal file
View File

@ -0,0 +1,48 @@
#include <x86.h>
#include <kern/process.h>
#include <kern/protect.h>
#include <kern/trap.h>
/*
*
*/
void
schedule(void)
{
// 如果中断没关,一定!一定!一定!要关中断保证当前执行流操作的原子性
DISABLE_INT();
PROCESS *p_cur_proc = p_proc_ready;
PROCESS *p_next_proc = p_proc_ready;
asm volatile ("":::"memory");
static bool in_sche;
if (in_sche)
goto free;
in_sche = true;
do {
if (++p_next_proc >= proc_table + PCB_SIZE)
p_next_proc = proc_table;
if (p_cur_proc == p_next_proc &&
p_next_proc->pcb.statu != READY) {
asm volatile("sti\n\thlt\n\tcli":::"memory");
}
} while (p_next_proc->pcb.statu != READY);
p_proc_ready = p_next_proc;
// 切换进程页表和tss
lcr3(p_proc_ready->pcb.cr3);
tss.esp0 = (u32)(&p_proc_ready->pcb.user_regs + 1);
in_sche = false;
switch_kern_context(
&p_cur_proc->pcb.kern_regs,
&p_next_proc->pcb.kern_regs);
asm volatile ("":::"memory");
free:
ENABLE_INT();
}

View File

@ -9,16 +9,22 @@ static ssize_t sys_get_ticks(void);
static ssize_t sys_get_pid(void); static ssize_t sys_get_pid(void);
static ssize_t sys_read(void); static ssize_t sys_read(void);
static ssize_t sys_write(void); static ssize_t sys_write(void);
static ssize_t sys_delay_ticks(void); static ssize_t sys_exec(void);
static ssize_t sys_mmap(void); static ssize_t sys_fork(void);
static ssize_t sys_wait(void);
static ssize_t sys_exit(void);
static ssize_t sys_fork_ack(void);
ssize_t (*syscall_table[])(void) = { ssize_t (*syscall_table[])(void) = {
[_NR_get_ticks] sys_get_ticks, [_NR_get_ticks] sys_get_ticks,
[_NR_get_pid] sys_get_pid, [_NR_get_pid] sys_get_pid,
[_NR_read] sys_read, [_NR_read] sys_read,
[_NR_write] sys_write, [_NR_write] sys_write,
[_NR_delay_ticks] sys_delay_ticks, [_NR_exec] sys_exec,
[_NR_mmap] sys_mmap, [_NR_fork] sys_fork,
[_NR_wait] sys_wait,
[_NR_exit] sys_exit,
[_NR_fork_ack] sys_fork_ack,
}; };
/* /*
@ -89,17 +95,53 @@ sys_write(void)
return do_write(get_arg(0), (const void *)get_arg(1), get_arg(2)); return do_write(get_arg(0), (const void *)get_arg(1), get_arg(2));
} }
/*
* ssize_t exec(const char *pathname)
* pathname对应的ELF文件
*/
static ssize_t static ssize_t
sys_delay_ticks(void) { sys_exec(void)
return do_delay_ticks(get_arg(0)); {
return do_exec((const void *)get_arg(0));
} }
/* /*
* ssize_t do_mmap(int pid, void *src, void* dst, size_t length); * ssize_t fork(void)
* pid号进程的线性地址[src, src + length)线[dst, dst + length) *
*/ */
static ssize_t static ssize_t
sys_mmap(void) { sys_fork(void)
return do_mmap(get_arg(0), (void*)get_arg(1), (void*)get_arg(2), get_arg(3)); {
return do_fork();
}
/*
* ssize_t wait(int *wstatus)
* exitwstatus接收子进程的返回码
*/
static ssize_t
sys_wait(void)
{
return do_wait((int *)get_arg(0));
}
/*
* ssize_t exit(int status)
* 退status作为返回码传递给父进程
*/
static ssize_t
sys_exit(void)
{
return do_exit(get_arg(0));
}
/*
* ssize_t fork_ack(void)
* forkbomb.c所用fork的次数
*
*/
static ssize_t
sys_fork_ack(void)
{
return do_fork_ack();
} }

View File

@ -1,11 +1,11 @@
#include <assert.h> #include <assert.h>
#include <string.h>
#include <x86.h> #include <x86.h>
#include <kern/keyboard.h> #include <kern/keyboard.h>
#include <kern/keymap.h> #include <kern/keymap.h>
#include <kern/process.h> #include <kern/process.h>
#include <kern/protect.h> #include <kern/protect.h>
#include <kern/sche.h>
#include <kern/stdio.h> #include <kern/stdio.h>
#include <kern/syscall.h> #include <kern/syscall.h>
#include <kern/time.h> #include <kern/time.h>
@ -150,11 +150,6 @@ void
clock_interrupt_handler(int irq) clock_interrupt_handler(int irq)
{ {
timecounter_inc(); timecounter_inc();
if (!has_runnable) return;
// maybe do nothing would be better, try to avoid recursive call to schedule
// because we must be in a schedule when runnable is false
// we just need a clock int to wake up it
// 当一个函数的时间片全用完时 // 当一个函数的时间片全用完时
if (--p_proc_ready->pcb.ticks == 0) { if (--p_proc_ready->pcb.ticks == 0) {
p_proc_ready->pcb.ticks = p_proc_ready->pcb.priority; p_proc_ready->pcb.ticks = p_proc_ready->pcb.priority;
@ -168,12 +163,14 @@ clock_interrupt_handler(int irq)
void void
kb_interrupt_handler(int irq) kb_interrupt_handler(int irq)
{ {
u8 ch = inb(0x60); u32 ch = (u32)inb(0x60);
if ((ch & 0x80) != 0) if ((ch & 0x80) != 0)
return; return;
ch = keymap[ch]; ch = keymap[ch];
if ('a' <= ch && ch <= 'z') if (0x20 <= ch && ch <= 0x7e)
add_keyboard_buf(ch); add_keyboard_buf(ch);
if (ch == ENTER)
add_keyboard_buf('\n');
} }
/* /*

39
kern/wait.c Normal file
View File

@ -0,0 +1,39 @@
#include <assert.h>
#include <mmu.h>
#include <kern/syscall.h>
#include <kern/wait.h>
ssize_t
kern_wait(int *wstatus)
{
// 相比于fork来说wait的实现简单很多
// 语义实现比较清晰没有fork那么多难点要处理所以这里并不会给大家太多引导
// 需要大家自己思考wait怎么实现。
// 在实现之前你必须得读一遍文档`man 2 wait`
// 了解到wait大概要做什么
panic("Unimplement! Read The F**king Manual");
// 当然读文档是一方面,最重要的还是代码实现
// wait系统调用与exit系统调用关系密切所以在实现wait之前需要先读一遍exit为好
// 可能读完exit的代码你可能知道wait该具体做什么了
panic("Unimplement! Read The F**king Source Code");
// 接下来就是你自己的实现了,我们在设计的时候这段代码不会有太大问题
// 在实现完后你任然要对自己来个灵魂拷问
// 1. 上锁上了吗?所有临界情况都考虑到了吗?(永远要相信有各种奇奇怪怪的并发问题)
// 2. 所有错误情况都判断到了吗错误情况怎么处理RTFM->`man 2 wait`
// 3. 是否所有的资源都正确回收了?
// 4. 你写的代码真的符合wait语义吗
panic("Unimplement! soul torture");
return 0;
}
ssize_t
do_wait(int *wstatus)
{
assert((uintptr_t)wstatus < KERNBASE);
assert((uintptr_t)wstatus + sizeof(wstatus) < KERNBASE);
return kern_wait(wstatus);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

BIN
lab6-finish.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
lib/kern/fork_ack.o Normal file

Binary file not shown.

View File

@ -3,6 +3,7 @@
#include <kern/keymap.h> #include <kern/keymap.h>
#include <kern/trap.h> #include <kern/trap.h>
#include <kern/sche.h>
#define KB_INBUF_SIZE 1024 #define KB_INBUF_SIZE 1024
@ -26,7 +27,8 @@ void
add_keyboard_buf(u8 ch) add_keyboard_buf(u8 ch)
{ {
// 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力 // 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力
while (xchg(&kb_input.lock, 1) == 1); while (xchg(&kb_input.lock, 1) == 1)
schedule();
if (kb_input.count == KB_INBUF_SIZE) if (kb_input.count == KB_INBUF_SIZE)
goto free; goto free;
@ -50,7 +52,8 @@ read_keyboard_buf(void)
u8 res; u8 res;
// 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力 // 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力
while (xchg(&kb_input.lock, 1) == 1); while (xchg(&kb_input.lock, 1) == 1)
schedule();
if (kb_input.count == 0) { if (kb_input.count == 0) {
res = -1; res = -1;

124
lib/kern/kmalloc.c Normal file
View File

@ -0,0 +1,124 @@
#include <assert.h>
#include <mmu.h>
#include <x86.h>
#include <kern/stdio.h>
#include <kern/kmalloc.h>
#include <kern/sche.h>
#include <kern/trap.h>
static u32 phy_malloc_4k_lock;
static phyaddr_t phy_malloc_4k_p = 96 * MB;
/*
* free_4k的代码
*
*
*/
void
phy_free_4k(phyaddr_t paddr)
{
assert(paddr % PGSIZE == 0);
}
/*
* 4kb
* 96MB~128MB
*/
phyaddr_t
phy_malloc_4k(void)
{
while(xchg(&phy_malloc_4k_lock, 1) == 1)
schedule();
assert(phy_malloc_4k_p < 128 * MB);
phyaddr_t paddr = phy_malloc_4k_p;
phy_malloc_4k_p += PGSIZE;
free:
xchg(&phy_malloc_4k_lock, 0);
return paddr;
}
struct kmalloc_header {
struct kmalloc_header *ptr;
size_t size;
};
static struct kmalloc_header *freePtr;
static u32 malloc_lock;
/*
* kmalloc申请的内存
*/
void
kfree(void *v)
{
struct kmalloc_header *bp, *p;
bp = (struct kmalloc_header*)v - 1;
while(xchg(&malloc_lock, 1) == 1)
schedule();
for (p = freePtr; !(bp > p && bp < p->ptr); p = p->ptr) {
if (p >= p->ptr && (bp > p || bp < p->ptr)) {
break;
}
}
if (bp + bp->size == p->ptr) {
bp->size += p->ptr->size;
bp->ptr = p->ptr->ptr;
} else {
bp->ptr = p->ptr;
}
if (p + p->size == bp) {
p->size += bp->size;
p->ptr = bp->ptr;
} else {
p->ptr = bp;
}
freePtr = p;
free:
xchg(&malloc_lock, 0);
}
/*
* n字节
* 64MB~96MB
*/
void *
kmalloc(size_t n)
{
struct kmalloc_header *p, *prevP;
size_t nUnits;
void *ret;
while(xchg(&malloc_lock, 1) == 1)
schedule();
nUnits = (n + sizeof(struct kmalloc_header) - 1)
/ sizeof(struct kmalloc_header) + 1;
if ((prevP = freePtr) == 0) {
freePtr = prevP = (void *)K_PHY2LIN(64 * MB);
freePtr->ptr = freePtr;
freePtr->size = (32 * MB) / sizeof(struct kmalloc_header);
}
for (p = prevP->ptr ; ; prevP = p, p = p->ptr) {
if (p->size >= nUnits) {
if (p->size == nUnits) {
prevP->ptr = p->ptr;
} else {
p->size -= nUnits;
p += p->size;
p->size = nUnits;
}
freePtr = prevP;
ret = (void*)(p + 1);
goto free;
}
if (p == freePtr) {
panic("malloc failed!");
}
}
free:
xchg(&malloc_lock, 0);
return ret;
}

View File

@ -1,4 +1,5 @@
#include <assert.h> #include <assert.h>
#include <mmu.h>
#include <string.h> #include <string.h>
#include <stdarg.h> #include <stdarg.h>
#include <type.h> #include <type.h>
@ -17,8 +18,6 @@ write_to_terminal(u16 disp_pos, u16 content)
} }
struct kprintfbuf { struct kprintfbuf {
// 锁
u32 lock;
// 通用 // 通用
u16 color; u16 color;
i16 cursor_row; i16 cursor_row;
@ -357,13 +356,13 @@ kprintfputch(int ch, struct kprintfbuf *b)
int int
vkprintf(const char *fmt, va_list ap) vkprintf(const char *fmt, va_list ap)
{ {
while (xchg(&TTY.lock, 1) == 1); int rc;
DISABLE_INT();
TTY.cnt = 0; TTY.cnt = 0;
vprintfmt((void*)kprintfputch, &TTY, fmt, ap); vprintfmt((void*)kprintfputch, &TTY, fmt, ap);
rc = TTY.cnt;
int rc = TTY.cnt; ENABLE_INT();
xchg(&TTY.lock, 0);
return rc; return rc;
} }

View File

@ -1,8 +1,10 @@
[section .text] [section .text]
extern main extern main
extern exit
global _start global _start
_start: ; 第一条指令 _start: ; 第一条指令
call main call main
jmp $ push eax
call exit

View File

@ -21,8 +21,8 @@ printfputch(int ch, struct printfbuf *b)
{ {
b->cnt++; b->cnt++;
*b->buf_p++ = (char)ch; *b->buf_p++ = (char)ch;
if (b->buf_p == b->buf + PRINTFBUF_SIZE) { if (ch == '\n' || b->buf_p == b->buf + PRINTFBUF_SIZE) {
write(STDOUT, b->buf, sizeof(b->buf)); write(STDOUT, b->buf, b->buf_p - b->buf);
b->buf_p = b->buf; b->buf_p = b->buf;
} }
} }
@ -61,11 +61,16 @@ printf(const char *fmt, ...)
* printf的缓冲区全写出去 * printf的缓冲区全写出去
*/ */
void void
fflush() fflush(void)
{ {
struct printfbuf *b = &printfb; struct printfbuf *b = &printfb;
while (xchg(&b->lock, 1) == 1);
write(STDOUT, b->buf, b->buf_p - b->buf); write(STDOUT, b->buf, b->buf_p - b->buf);
b->buf_p = b->buf; b->buf_p = b->buf;
xchg(&b->lock, 0);
} }
@ -82,7 +87,7 @@ static struct getchbuf getchb = {
}; };
u8 u8
getch() getch(void)
{ {
struct getchbuf *b = &getchb; struct getchbuf *b = &getchb;
@ -100,3 +105,11 @@ getch()
return rc; return rc;
} }
u8
getchar(void)
{
u8 rc = -1;
while ((rc = getch()) == (u8)-1);
return rc;
}

View File

@ -1,21 +1,5 @@
#include <user/syscall.h> #include <user/syscall.h>
// 第一个冒号区间:汇编命令
// 第二个冒号区间:输出操作符扩展
// "=a" 这条命令结束后将eax寄存器的值赋给ret变量
// 第三个冒号区间:输入操作符扩展
// "a" 将变量赋给eax寄存器
// "b" 将变量赋给ebx寄存器
// "c" 将变量赋给ecx寄存器
// "d" 将变量赋给edx寄存器
// "S" 将变量赋给esi寄存器
// "D" 将变量赋给edi寄存器
// 第四个冒号区间:指令约束,当汇编指令会修改相关操作时需要修改
// 最主要是防被gcc编译器优化
// "a/b/c/d/S/D" 提醒编译器这些寄存器的值会在汇编指令执行中被修改
// "cc" 提醒编译器eflags寄存器的值在汇编指令执行中会被修改
// "memory" 提醒编译器内存中的数据在汇编指令执行中会被修改
ssize_t ssize_t
syscall0(size_t NR_syscall) syscall0(size_t NR_syscall)
{ {
@ -38,7 +22,8 @@ syscall1(size_t NR_syscall, size_t p1)
: "cc", "memory"); : "cc", "memory");
return ret; return ret;
} }
ssize_t syscall2(size_t NR_syscall, size_t p1, size_t p2) ssize_t
syscall2(size_t NR_syscall, size_t p1, size_t p2)
{ {
ssize_t ret; ssize_t ret;
asm volatile("int $0x80" asm volatile("int $0x80"
@ -49,7 +34,8 @@ ssize_t syscall2(size_t NR_syscall, size_t p1, size_t p2)
: "cc", "memory"); : "cc", "memory");
return ret; return ret;
} }
ssize_t syscall3(size_t NR_syscall, size_t p1, size_t p2, size_t p3) ssize_t
syscall3(size_t NR_syscall, size_t p1, size_t p2, size_t p3)
{ {
ssize_t ret; ssize_t ret;
asm volatile("int $0x80" asm volatile("int $0x80"
@ -61,7 +47,8 @@ ssize_t syscall3(size_t NR_syscall, size_t p1, size_t p2, size_t p3)
: "cc", "memory"); : "cc", "memory");
return ret; return ret;
} }
ssize_t syscall4(size_t NR_syscall, size_t p1, size_t p2, ssize_t
syscall4(size_t NR_syscall, size_t p1, size_t p2,
size_t p3, size_t p4) size_t p3, size_t p4)
{ {
ssize_t ret; ssize_t ret;
@ -75,7 +62,8 @@ ssize_t syscall4(size_t NR_syscall, size_t p1, size_t p2,
: "cc", "memory"); : "cc", "memory");
return ret; return ret;
} }
ssize_t syscall5(size_t NR_syscall, size_t p1, size_t p2, ssize_t
syscall5(size_t NR_syscall, size_t p1, size_t p2,
size_t p3, size_t p4, size_t p5) size_t p3, size_t p4, size_t p5)
{ {
ssize_t ret; ssize_t ret;
@ -92,13 +80,13 @@ ssize_t syscall5(size_t NR_syscall, size_t p1, size_t p2,
} }
ssize_t ssize_t
get_ticks() get_ticks(void)
{ {
return syscall0(_NR_get_ticks); return syscall0(_NR_get_ticks);
} }
ssize_t ssize_t
get_pid() get_pid(void)
{ {
return syscall0(_NR_get_pid); return syscall0(_NR_get_pid);
} }
@ -116,12 +104,31 @@ write(int fd, const void *buf, size_t count)
} }
ssize_t ssize_t
delay_ticks(u32 ticks) exec(const char *pathname)
{ {
return syscall1(_NR_delay_ticks, ticks); return syscall1(_NR_exec, (size_t)pathname);
} }
ssize_t lml_mmap(int pid, void* src, void* dst, size_t length) ssize_t
fork(void)
{ {
return syscall4(_NR_mmap, pid, (size_t)src, (size_t)dst, length); return syscall0(_NR_fork);
}
ssize_t
exit(int status)
{
return syscall1(_NR_exit, status);
}
ssize_t
wait(int *wstatus)
{
return syscall1(_NR_wait, (size_t)wstatus);
}
ssize_t
fork_ack(void)
{
return syscall0(_NR_fork_ack);
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -10,26 +10,24 @@ USER_LIBSRCS:= lib/printfmt.c \
USER_LIBOBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(USER_LIBSRCS)) USER_LIBOBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(USER_LIBSRCS))
USER_LIBOBJS := $(patsubst %.asm, $(OBJDIR)/%.o, $(USER_LIBOBJS)) USER_LIBOBJS := $(patsubst %.asm, $(OBJDIR)/%.o, $(USER_LIBOBJS))
USER_SRCS := user/testpid.c \ USER_SRCS := user/testfork.c \
user/testkey.c \ user/testwait.c \
user/delay.c \ user/shell.c \
user/hack.c \ user/initproc.c \
user/snake.bin \ user/forkbomb.c \
user/firework.bin \
USER_OBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(USER_SRCS)) USER_OBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(USER_SRCS))
USER_BINS := $(patsubst %.bin, $(OBJDIR)/%.bin, $(USER_SRCS)) USER_BINS := $(patsubst %.bin, $(OBJDIR)/%.bin, $(USER_SRCS))
USER_BINS := $(patsubst %.c, $(OBJDIR)/%.bin, $(USER_SRCS)) USER_BINS := $(patsubst %.c, $(OBJDIR)/%.bin, $(USER_BINS))
$(OBJDIR)/user/%.o: user/%.c $(OBJDIR)/.vars.CFLAGS $(OBJDIR)/user/%.o: user/%.c $(OBJDIR)/.vars.CFLAGS
@echo + cc $< @echo + cc $<
@mkdir -p $(@D) @mkdir -p $(@D)
@$(CC) $(CFLAGS) -c -o $@ $< @$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/user/%.bin: user/%.bin $(OBJDIR)/user/firework.bin: lib/user/firework.bin
@echo + cp $<
@echo 'd2dcd918806dfd71bb55e52a4081e95e' > $(OBJDIR)/.snakemd5
@md5sum $< | awk '{print $$1}' | cmp -s $(OBJDIR)/.snakemd5 || (echo 'you are a cheater!!!'; exit 1;)
@mkdir -p $(@D) @mkdir -p $(@D)
@cp $< $(@D) @cp $< $(@D)

View File

@ -1,22 +0,0 @@
#include <assert.h>
#include <user/stdio.h>
#include <user/syscall.h>
int main()
{
int pid = get_pid();
int target_ticks = (pid + 1) * 1000;
while(1) {
printf("\x1b[%dmI'm %d!\x1b[0m", 90 + pid + 1, pid);
fflush();
int before_ticks = get_ticks();
delay_ticks(target_ticks);
int after_ticks = get_ticks();
int real_ticks = after_ticks - before_ticks;
int delta_ticks = real_ticks - target_ticks;
assert(-5 <= delta_ticks && delta_ticks <= 5);
}
}

47
user/forkbomb.c Normal file
View File

@ -0,0 +1,47 @@
#include <user/stdio.h>
#include <user/syscall.h>
static size_t random_seed;
static size_t rand() { return (((random_seed = random_seed * 214013L + 2531011L) >> 16) & 0x7fff);}
void bomb_salve(void)
{
fork_ack();
random_seed = get_ticks() & 0x7fff;
for (int i = 0 ; i < get_pid() ; i++)
rand();
for (int i = 0 ; i < (rand() & 0xf) ; i++) {
int pid = fork();
if (pid == 0)
bomb_salve();
for (int i = 0 ; i < rand() ; i++)
; // do nothing
}
if (rand() % 2 == 1) {
while (wait(NULL) >= 0);
}
exit(0);
}
void bomb_master(void)
{
random_seed = get_ticks() & 0x7fff;
while (1) {
int pid = fork();
if (pid == 0)
bomb_salve();
for (int i = 0 ; i < rand() ; i++)
; // do nothing
while (wait(NULL) >= 0);
}
}
int main()
{
bomb_master();
}

View File

@ -1,38 +0,0 @@
#include <assert.h>
#include <user/stdio.h>
#include <user/syscall.h>
#define PID_SNAKE 0
#define PAGE_SIZE 4096
#define DATA_SEG_SIZE (4 * PAGE_SIZE)
typedef struct Food {
char hint[16];
int x;
int y;
int score;
} Food;
char __attribute__((aligned(0x1000))) snake_seg_data[DATA_SEG_SIZE];
// this attribute is necessary to make the pointer 4K aligned
int main()
{
Food* food = NULL;
lml_mmap(PID_SNAKE, (void*)0x0804d000, (void*)snake_seg_data, DATA_SEG_SIZE);
// data segment:
// Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
// LOAD 0x004000 0x0804d000 0x0804d000 0x0163c 0x12500 RW 0x1000
// 'This is a food' starts @ file offset 0x542c, while .data starts @ 0x4000
// so, offset is 0x142c
food = (Food*)(snake_seg_data + 0x142c);
// printf("%s\n", food->hint);
while (1) {
// printf("%d %d", food->x, food->y);
if (food->x <= 0) food->x = 1;
if (food->y <= 0) food->y = 1;
if (food->x >= 70) food->x = 69;
if (food->y >= 25) food->y = 24;
// according to some trial, XMAX(right veritcal line border)=69, YMAX(low edge of screen)=24
}
return 0;
}

22
user/initproc.c Normal file
View File

@ -0,0 +1,22 @@
#include <assert.h>
#include <user/stdio.h>
#include <user/syscall.h>
int main()
{
ssize_t pid = fork();
if (pid == 0) {
int ret = exec("shell.bin");
panic("exec failed! errno: %d", ret);
}
if (pid < 0)
panic("init process fork failed errno: %d", pid);
int wstatus;
while (1) {
pid = wait(&wstatus);
}
}

52
user/shell.c Normal file
View File

@ -0,0 +1,52 @@
#include <assert.h>
#include <string.h>
#include <user/stdio.h>
#include <user/wait.h>
#include <user/syscall.h>
int main() {
int childStatus = 0;
while (1) {
printf("\x1b[32mMINIOS\x1b[0m$ ");
fflush();
int cntInput = 0;
char input[512] = {0};
while (1) {
char c = getchar();
printf("%c",c);
fflush();
if (c != '\n') {
input[cntInput++] = c;
} else {
input[cntInput++] = '\0';
break;
}
}
if (strcmp(input, "?") == 0) {
printf("%d\n",WEXITSTATUS(childStatus));
continue;
}
int ForkRetVal = fork();
if (ForkRetVal < 0) {
printf("fork failed!\n");
continue;
}
if (ForkRetVal == 0) {
exec(input);
printf("exec failed!\n");
exit(1);
}
int wait_pid;
wait_pid = wait(&childStatus);
if ((wait_pid = wait(&childStatus)) >= 0) {
// do nothing
}
}
return 0;
}

22
user/testfork.c Normal file
View File

@ -0,0 +1,22 @@
#include <user/stdio.h>
#include <user/syscall.h>
int main()
{
int pid = fork();
if (pid) {
while (1) {
printf("I'm fa, son pid = %d", pid);
fflush();
for (int i = 0 ; i < (int)1e8 ; i++)
;//do nothing
}
} else {
while (1) {
printf("I'm son");
fflush();
for (int i = 0 ; i < (int)1e8 ; i++)
;//do nothing
}
}
}

View File

@ -1,12 +0,0 @@
#include <user/stdio.h>
int main()
{
while(1) {
u8 ch = getch();
if (ch == 255)
continue;
printf("%c", ch);
fflush();
}
}

View File

@ -1,13 +0,0 @@
#include <user/stdio.h>
#include <user/syscall.h>
int main()
{
while (1) {
printf("pid: %d!", get_pid());
fflush();
for (int i = 0 ; i < (int)1e8 ; i++)
;//do nothing
}
return 0;
}

115
user/testwait.c Normal file
View File

@ -0,0 +1,115 @@
#include <assert.h>
#include <errno.h>
#include <user/stdio.h>
#include <user/syscall.h>
#include <user/wait.h>
void test_fork_wait1(void)
{
ssize_t pid = fork();
assert(pid >= 0);
if (pid == 0)
exit(114);
int wstatus;
assert(pid == wait(&wstatus));
assert(WEXITSTATUS(wstatus) == 114);
printf("\x1b[92mtest_fork_wait1 passed!\x1b[0m\n");
}
void test_fork_wait2(void)
{
ssize_t pid = fork();
assert(pid >= 0);
if (pid == 0)
exit(514);
assert(pid == wait(NULL));
printf("\x1b[92mtest_fork_wait2 passed!\x1b[0m\n");
}
void test_empty_wait(void)
{
assert(wait(NULL) == -ECHILD);
printf("\x1b[92mtest_empty_wait passed!\x1b[0m\n");
}
void test_fork_limit(void)
{
int xor_sum = 0;
for (int i = 1 ; i <= 17 ; i++) {
ssize_t pid = fork();
assert(pid >= 0);
if (pid == 0)
exit(0);
xor_sum ^= pid;
}
assert(fork() == -EAGAIN);
int wait_cnt = 0, wait_pid;
while ((wait_pid = wait(NULL)) >= 0)
wait_cnt++, xor_sum ^= wait_pid;
assert(wait_cnt == 17);
assert(xor_sum == 0);
printf("\x1b[92mtest_fork_limit passed!\x1b[0m\n");
}
void test_wait_is_sleeping(void)
{
ssize_t rt_pid = get_pid();
for (int i = 1 ; i <= 17 ; i++) {
ssize_t pid = fork();
assert(pid >= 0);
if (pid > 0) {
int wstatus;
assert(pid == wait(&wstatus));
assert(WEXITSTATUS(wstatus) == 42);
if (get_pid() != rt_pid)
exit(42);
break;
}
}
if (get_pid() != rt_pid) {
ssize_t la_ticks = get_ticks();
for (int i = 1 ; i <= (int)5e6 ; i++) {
ssize_t now_ticks = get_ticks();
assert(now_ticks - la_ticks <= 2);
la_ticks = now_ticks;
}
exit(42);
}
printf("\x1b[92mtest_wait_is_sleeping passed!\x1b[0m\n");
}
int main()
{
test_fork_wait1();
test_fork_wait2();
test_empty_wait();
test_fork_limit();
test_wait_is_sleeping();
printf("\x1b[92mall tests passed!\x1b[0m\n");
}

View File

@ -1,359 +0,0 @@
<div align='center'>
<font size='6'>实验五 系统调用、页表</font>
</div>
<div align='center'>
<font size='4'>谷建华</font>
</div>
<div align='center'>
<font size='4'>2022-10-28 v0.3</font>
</div>
#### 实验目的
1. 学习用户态执行系统调用到内核态处理的整个过程.
2. 学习页表,理解如何从线性地址经过页表映射到物理地址实现进程间的内存资源隔离.
3. 学习页表项相关位的功能,如何维护页表数据结构.
#### 实验预习内容
1. 系统调用规范.
2. 页表数据结构.
3. 页表项的组成.
#### 实验内容
1. 补全`main.c`中的代码,为每个用户进程加载在磁盘中的ELF程序文件到用户地址空间.
(1) `main.c`中用户态的程序加载方式由原先加载kernel中的函数变成加载用户ELF文件的方式,你需要添加相应代码实现用户态程序加载功能.
(2) 我们将文件系统读取函数`read_file`准备好,给定短目录项名和加载的地址该函数会将指定文件加载到地址中.
(3) 我们将内核段地址映射函数`map_kern`准备好,它会将`3GB + 0MB ~ 3GB + 128MB`的线性地址(内存分布模型见实验参考最后一节)映射到`0MB ~ 128MB`的物理地址,供大家学习页表的初始化过程.
(4) `phy_malloc_4k`是一个非常简单的内存分配函数,它会分配物理地址`64MB ~ 128MB`区间的的内存,每次分配4KB,一个页面大小(暂时可以不用考虑内存回收的情况,只需要光分配就行了).
(5) 你需要做的是将用户ELF文件从文件系统中读取,并解析ELF文件将程序段正确加载到每个进程中的地址空间,使得能够运行用户态程序.我们准备了两个用户测例文件`testpid.bin`和`testkey.bin`,第一个测试`get_pid`系统调用,第二个测试键盘输入,**需要验证两个程序能够并发地执行**.
(6) 如果你在实现中遇到了`Page Fault`,那么基本上意味着你加载错了,这个时候请根据报错的`eip`信息pc程序计数器然后对ELF文件进行反汇编将触发错误的指令找到,必要的时候需要通过`gdb`一条汇编一条汇编地执行直到找到问题.跟页表打交道非常容易出现这种问题,请耐心调试.
2. 添加`delay_ticks`系统调用.
(1) `delay_ticks`的系统调用的C语言语义如下
```C
// 进程休眠ticks个时钟中断
// 系统调用默认返回0
ssize_t delay_ticks(u32 ticks);
```
(2) `delay_ticks`的系统调用号自行定义,你需要写在内核`kern/syscall.c`和用户`lib/user/syscall.c`的两部分的接口.
(3) 在`user/delay.c`存放着测试该系统调用的代码,而`delay_ticks`函数被注释掉了(我们并没有给出函数接口),你需要在实现对应接口后取消注释测试用户程序`delay.bin`.
(4) 你需要开三个用户进程同时执行`delay.bin`,这三个用户进程的pid号分别为`0,1,2`,`delay.bin`会获取进程pid号来给出休眠的时长.预计效果图如下:
![](./lab5-finish.png)
(5) 你需要注意一个边界情况:当三个进程都休眠了,你怎么解决?谁接替执行流?或是干脆休眠核让执行流干脆不执行直到下个中断?
3. (自我提高内容,自己想做就做,不用写进报告,**禁止内卷**)添加`mmap`系统调用对贪吃蛇程序`snake.bin`进行数据注入.
(1) 你还记得实验四的贪吃蛇吗?还是这个程序,不过这次这个程序抽风了,它的食物出现在了地图外,那岂不是永远无法游玩了?不过好在天无绝人之路,善良的设计者设计食物结构体结构如下:
```c
struct Food {
char hint[16];
int x;
int y;
int score;
};
struct Food food = {
.hint = "This is a food",
//...
};
```
food的值会放在elf的data段中,相信根据elf文件的信息和上面的初始化内容你能够找到food在内存中的地址.
(2) 编写`mmap`系统调用,其C语言语义如下非官方语义,也可以自行设计其他语义解释):
```c
// 将pid号进程的线性地址[src, src + length)中的页面共享到该进程的线性地址[dst, dst + length)
// src,length和dst需要4096对齐
// 由于是自行实现的,没有过多的要求.
ssize_t mmap(int pid, void *src, size_t length, void *dst);
```
自行写一个hack程序,在`main.c`中创建两个用户进程`snake.bin`和自己的hack程序,hack程序通过调用`mmap`系统调用跟`snake.bin`的部分内存共享,然后hack程序修改`food`的数据让食物回到地图内(每帧会重新绘制地图).这样就可以重新游玩贪吃蛇.
#### 实验总结
1. 系统调用的入口参数是如何传入内核的?返回值又是如何返回给进程的?
2. 时钟中断和系统调用对IDT的初始化有什么不同
3. 该实验中初始化内核页表和用户页表有啥区别?
#### 实验参考
##### 系统调用
在上一个实验中,我们可以调用`kprintf`函数实现输出的功能,我们仅仅实现了一个“虚假”的用户态,虽然段寄存器的权限在用户态,但是我们依然能够调用kernel中的函数,但是这么做有一个弊端,用户态下能够轻易调用内核的函数,这是我们所不希望的,所以我们通过页表将进程资源隔离,通过页表给用户进程戴上一个VR眼镜,这样用户进程在执行的时候就看不到内核的函数.
但是`printf`是需要向终端输出字符的,但是终端显示被内核所掌控,这个时候用户就需要使用系统调用向内核请求服务.简单理解就是用户做不了的需要向内核申请服务.
系统调用类似函数调用,不过它是跨越权限的,系统调用也有它的一套规范,这套规范可以阅读`man syscall`手册查看,下面是调用规范的部分截取.
```
Architecture calling conventions
Every architecture has its own way of invoking and passing arguments to
the kernel. The details for various architectures are listed in the
two tables below.
The first table lists the instruction used to transition to kernel mode
(which might not be the fastest or best way to transition to the ker
nel, so you might have to refer to vdso(7)), the register used to indi
cate the system call number, the register(s) used to return the system
call result, and the register used to signal an error.
Arch/ABI Instruction System Ret Ret Error Notes
call # val val2
───────────────────────────────────────────────────────────────────
alpha callsys v0 v0 a4 a3 1, 6
arc trap0 r8 r0 - -
arm/OABI swi NR - a1 - - 2
arm/EABI swi 0x0 r7 r0 r1 -
arm64 svc #0 x8 x0 x1 -
blackfin excpt 0x0 P0 R0 - -
i386 int $0x80 eax eax edx -
```
###### 系统调用汇编命令
看最后一行,我们可以获悉linux对i386架构是调用`int $0x80`汇编指令进行系统调用,调用这条指令后就可以直接进内核态进行系统调用处理.
在之前的学习中,我们知道`int`的命令实际上是触发中断,后面跟着的数字是中断号,所以在i386架构中,系统调用本质上跟中断类似,不过不是由硬件触发是由用户自己调用触发.如果愿意,我们可以修改系统调用的中断号,只要不发生冲突就行.
```c
// 在start.c中系统调用初始化
// INT_VECTOR_SYSCALL值可以任意,这次的实验用的是linux的0x80
init_gate(idt + INT_VECTOR_SYSCALL, DA_386IGate,
int_syscall, PRIVILEGE_USER);
```
###### 系统调用号
但是要知道内核可不只有一种功能,如何获悉用户想要哪种功能就看下一列`System call #`,可以知道内核是根据`eax`寄存器的值获悉用户需要哪种功能,这个值就叫系统调用号.
系统调用号可以有很多种规范,相同的系统调用号在不同的规范下内核会实现不同的功能,我们的实验用是自创教学用的规范,专业标准的是叫POSIX可移植操作系统接口规范,它的一部分是对于系统调用号对应的功能进行定义,这样只要遵循的相同的系统调用规范就能让一个相同的用户程序在不同的操作系统上运行.在`man syscalls`手册中有写到linux的系统调用号在64位机器上系统调用号在`/usr/include/asm/unistd_64.h`文件中,数了数有四百多个,量可以说是相当大了.
###### 系统调用其余参数
系统调用可不光有一个系统调用号,它还可以附带一些参数,我们继续在`man syscall`中阅读可以看到这么一段:
```
The second table shows the registers used to pass the system call argu
ments.
Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
──────────────────────────────────────────────────────────────
alpha a0 a1 a2 a3 a4 a5 -
arc r0 r1 r2 r3 r4 r5 -
arm/OABI a1 a2 a3 a4 v1 v2 v3
arm/EABI r0 r1 r2 r3 r4 r5 r6
arm64 x0 x1 x2 x3 x4 x5 -
blackfin R0 R1 R2 R3 R4 R5 -
i386 ebx ecx edx esi edi ebp -
```
看最后一行,可以知道系统调用是由`ebx`, `ecx`, `edx`, `esi`, `edi`, `ebp`这六个寄存器传递参数.不像正常的函数调用传参靠栈,而是靠寄存器传递参数,而且最多只能传递6个,如果要传递复杂的结构可以考虑传入结构体指针的方式解决.
虽然上面的规范如此,但是没有定死,我们完全可以将`ebx`作为系统调用号,`eax`作为参数,不过这么做有点吃饱了撑着不去兼容规范.
###### 系统调用返回值以及错误码
再看第一张表的`Retval`,可以获悉到系统调用也是可以有返回值的,返回值一般放在`eax`寄存器,当`eax`寄存器放不下的时候会将第二部分放`edx`,但是这种情况比较罕见,所以暂且不用去理会.
系统调用的返回值与普通函数的返回值中不太一样的一点是它会返回错误号,错误号是一类特殊的返回值,当用户请求的系统调用正确时会返回0或系统调用规定的值,但有错误(常见的比如文件路径不存在,用户越权写系统文件)时系统调用会返回错误号,错误号值的范围从-1,-2,-3...以此类推,大概有一百来个.通过错误号,用户可以知道自己调用系统调用触发了什么问题,可以阅读`man errno`手册可以获得错误号对应的语义.在终端中输入错误命令或在平常C语言代码中调用`perror`函数输出的都是根据上一个系统调用的错误码信息输出相应的字符串.
```
$ ls
cstat cstat.c
$ stat 114514
stat: cannot stat '114514': No such file or directory
$ cat cstat.c
// 你如果要问我怎么知道要引入这些头文件的,直接man翻手册啊
// 正经人谁背的住那么多头文件,不如直接翻手册
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
struct stat statbuf;
stat("1919810", &statbuf);
perror("");
}
$ ./cstat
No such file or directory
```
就如上面的例子,两个都是访问的不存在的文件,获取到的错误码信息为ENOENT,具体值在POSIX中值被规定成了2,相当于系统调用返回-2,它的语义就是`No such file or directory`,所以两个命令都输出了这句话.
由于错误码有限多个,所以同一个错误码在不同系统调用中有不同的语义,你如果会去翻系统调用的手册的时候会经常发现ERRORS这一栏,这里定义了一系列可能出现的问题以及会返回的对应错误码.
###### 如何获取程序用到的系统调用
当你在终端中输入一行命令后发现它报错了,你肯定会很疑惑它为什么会这么报错,这个时候一种可行的做法就是使用`strace`命令,它会截获用户程序对应的系统调用.
```shell
$ strace stat ./114514
# strace 输出的东西太多,就截取跟114514相关的
execve("/usr/bin/stat", ["stat", "./114514"], 0x7ffc75313828 /* 66 vars */) = 0
lstat("./114514", 0x7ffca6294210) = -1 ENOENT (No such file or directory)
write(2, "cannot stat './114514'", 22) = 22
```
根据strace的结果可以知道哪些系统调用发生了异常,上面可以知道`lstat`这个系统调用发生了异常,返回了ENOENT,用户程序可以通过其系统调用了解其实际干了什么,这也是一种猜测用户行为debug的一种方式.
###### MINIOS系统调用处理逻辑
对于我们实验的代码,系统调用分成三步处理:
+ `sys_*` 第一层处理函数,获取系统调用中需要的寄存器参数丢给下一层处理
+ `do_*` 第二层处理函数,会将参数做一些转换和边界判断丢给下一层处理.
+ `kern_*` 第三层处理函数,也是实际处理函数,因为学生开发总是稀里糊涂的乱用处理函数,规定在内核中如果只能调用`kern_*`处理语义相同的功能.
这三层在代码中可以体现,整个中断的处理流程跟时钟中断非常类似,还要简单.
```nasm
; atrap.asm中的系统调用处理函数
; 相比于硬件中断这段代码可太简单了
int_syscall:
call save
sti
call syscall_handler
cli
ret
```
进内核态后先保存用户程序上下文(寄存器),然后调用`syscall_handler`函数进行处理,处理完毕后返回用户态,就这么简单.
`syscall_handler`仅仅是根据系统调用号判断该用什么处理函数.
```c
static ssize_t sys_get_ticks(void);
static ssize_t sys_get_pid(void);
static ssize_t sys_read(void);
static ssize_t sys_write(void);
// 这是种有趣的写法,举个例子
// 这种写法钦定了syscall_table的第_NR_get_ticks项与sys_get_ticks函数绑定
ssize_t (*syscall_table[])(void) = {
[_NR_get_ticks] sys_get_ticks,
[_NR_get_pid] sys_get_pid,
[_NR_read] sys_read,
[_NR_write] sys_write,
};
/*
* 系统调用处理函数
*/
void
syscall_handler()
{
int syscall_id = p_proc_ready->pcb.user_reg.eax;
ssize_t ret = (*syscall_table[syscall_id])();
// 将用户态的寄存器值修改,在返回用户态时返回值就自然被写到了eax上
p_proc_ready->pcb.regs.eax = ret;
}
```
##### 页表
在实验三,我们实际上已经引入了页表用于扩展地址空间,将由实模式的段偏移寻址模式变为页表寻址增加寻址能力,但是页表映射是恒等映射,让你们觉得好像内存哪里都能访问.
###### 线性地址和物理地址
我们平时写的程序访问地址都是访问线性地址,线性地址是不用关心物理内存的约束,也就是意味着我们内存128MB的虚拟机,你可以尝试访问1GB处的线性地址,如果换成物理地址,你只能访问128MB以内的地址.
我们访问线性地址,实际上也是在访问内存中的某处的物理地址,通过页表这个数据结构将线性地址转化为物理地址,就仿佛带了个VR眼镜,你觉得你能够看到某处线性地址,但是它实际上被VR眼镜转化到某一物理地址去了,同理你带不同的VR眼镜,相同的线性地址被解析成了不同的物理地址,每个进程能够访问到的物理内存地址范围互不相同,除非通过特殊手段,一个进程是不能访问到另一个进程的地址空间,这样就实现了进程资源之间的地址空间隔离.
###### VR眼镜cr3
在i386架构采用三级页表模式页目录、页表和物理页这个下文会慢慢解释,cr3中存放了页目录的地址.而这个cr3寄存器,它的意义就如上文提到的VR眼镜,每一个进程会维护一个cr3的值,代表进程的地址空间,每当进程发生调度的时候,新的进程会使用`lcr3`函数将cr3寄存器值更新,相当于将cr3的值mov过去,具体见执行代码,相当于换VR眼镜的过程.
###### 线性地址转换到物理地址
好了,到了最麻烦的一部分,这一小节逻辑可以说是非常绕,根据经验,页表这一部分相当容易出现理论觉得懂了看代码觉得这不简单但是实操发现自己还是啥都不懂的情况,为了能够让大家顺利做实验,我们会写的尽量详细,写代码的时候建议多读几遍这一小节.
线性地址一共有32位,硬件工程师将这32位划分成了三个部分
```
A linear address 'la' has a three-part structure as follows:
+--------10------+-------10-------+---------12----------+
| Page Directory | Page Table | Offset within Page |
| Index | Index | |
+----------------+----------------+---------------------+
\--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/
\---------- PGNUM(la) ----------/
```
硬件工程师规定一个页面的大小为4kb,4096bytes.将一个$ 2^{32} $的区间划分成$ 2^{20} $个页面,通过32位的高10位和中10位确定线性地址对应是哪一个**物理页面**,然后根据低12位确认在页面中的偏移从而确定线性地址对应的**实际物理地址**,对线性地址内存的修改最终会映射到对物理内存的修改.
接下来要研究怎么根据高10位和中10位确认线性地址对应的是哪个物理页面.上文中的cr3寄存器就在这派上了用场,这个寄存器里面存着的值语义是一个**物理页面**的首地址,相当于对物理内存按照4kb为一块进行划分,其中任意一块的**首地址**(物理地址,低12位为0)可以成为一个合法的cr3值,cr3寄存器的值的意义如下,其中高20位就能表达物理页面的首地址,低12位全部保留为0.
```
+-----------20-----------+-----12-----+
| Physical Address of | all 0 |
| Page | |
+------------------------+------------+
```
一个页面4kb大,对于32位机器来说一个指针的大小为4字节,一个页面一共能够存放$ 2^{10} $个指向下一个物理页面的“指针”,高十位和中十位两个10位的数据,分别是页目录表和页表的下标,所以以高10位作为偏移量能够在cr3对应的物理页面找到指向假设为物理页面1首地址的“指针”,然后再以中10位作为偏移量在物理页面1找到指向假设为物理页面2首地址的“指针”,物理页面2就是线性地址对应的物理页面.cr3对应的物理页面官方命名为**页目录表**,物理页面1官方命名为**页表**.如果你对字典树Trie树这个数据结构比较熟悉的话,你就会发现这就是一个字符集大小为$ 2^{10} $的字典树.
指向下一物理页面的“指针”在页目录表中被命名为**页目录项**在页表中被命名为**页表项**,线性地址的高10位又称**页目录索引**,中10位又称**页表索引**.不过觉得页目录项和页表项区分开来非常别扭,它们的结构和功能都完全一致,所以我觉得可以统称为**页表项**.
由于物理页面的首地址的**低12位全是0**,所以硬件工程师将低12位重新利用起来.所以一个页表项的组成如下:
```
+-----------20-----------+-----12-----+
| Physical Address of | Flag bit |
| Page | |
+------------------------+------------+
```
页表项的高20位就能表示下一个物理页面的物理地址首地址,低12位是页表项的标志位,我们写实验的时候只需要关心这几位:
+ `#define PTE_P 0x001` 存在位第0位,标志着这一页表项指向的物理页面是否存在,如果没有被置位就视为不存在.
+ `#define PTE_W 0x002` 读写位第1位,标志着这一页是否可写,如果被置位意味着是读写的,如果没有被置位意味着是只读的.
+ `#define PTE_U 0x004` 用户位第2位,标志着这一页的权限段,如果被置位意味着是用户和内核都是可以访问的,如果没有被置位意味着只有内核有权限能够访问.
最后梳理一下,线性地址转化为物理地址的过程:
1. 根据cr3寄存器找到页目录表.
2. 根据线性地址的高10位找到对应的页目录项.
3. 根据页目录项的标志位判断是否有该页,做权限的检测等,如果非法就直接触发页面异常`Page Fault`.
4. 如果合法,根据页目录项找到页表.
5. 根据线性地址的中10位找到对应的页表项.
6. 根据页表项的标志位判断是否有该页,做权限的检测等,如果非法就直接触发页面异常`Page Fault`.
7. 如果合法,根据页表项找到物理页.
8. 根据线性地址的低12位找到对应的偏移,至此一次访存完成.
可以看到其实页表本质是经过多次`找页表-找页表项`的循环,对于32位指令集架构是重复了2次,对于64位指令集架构就没32位那么漂亮了,它转换一个线性地址可能要重复3次、4次.
###### tlb缓存科普
根据上面的流程,实际上为了完成线性地址一次访存,需要访存物理内存三次,这会极大增大运行时的开销,所以硬件工程师将页表信息缓存下来,这在大家的计组课上讲过叫快表tlb,下次访问相同物理页时会使用tlb的信息加速访存.
tlb加速了访存,但是于此同时增大的维护难度,会出现你修改了内存中的页表项后尝试访问页表项对应的线性地址发现修改的内容没有起作用的情况.这个时候就需要将页表进行刷新保证下次访存不会出问题.
刷新命令其实很简单:
```nasm
mov eax, cr3
mov cr3, eax
```
相当于对cr3做一次重载硬件就会重新刷新tlb信息.
###### MINIOS内存模型
之前我们实验的内存模型是恒等映射的.
![](./before_memory.png)
在这个实验由于用户地址空间隔离的需要,需要将内核程序迁移到`3GB ~ 3GB + 128MB`处运行,所以在很多方面如果需要访问物理内存地址需要访问**物理地址 + 3GB**的线性地址,低3GB用于给用户程序用.
![](./now_memory.png)
针对这次实验,内核的内存模型如下:
![](memory_in_kernel.png)
+ 3GB \~ ?: 内核程序使用会随着开发动态变化一般不会很大顶天3MB.
+ 3GB + 48MB \~ 3GB + 64MB: `read_file`函数将文件从磁盘读出后会将文件数据加载在此处.
+ 3GB + 64MB \~ 3GB + 128MB这段内存是空闲的可以用于分配给用户进程`phy_malloc_4k`会使用此段内存用于分配页面,相当于分配`64MB ~ 128MB`的物理内存.

270
实验六-v0.1.md Normal file
View File

@ -0,0 +1,270 @@
<div align='center'>
<font size='6'>实验六 fork和进程树</font>
</div>
<div align='center'>
<font size='4'>谷建华</font>
</div>
<div align='center'>
<font size='4'>2022-11-14 v0.1</font>
</div>
#### 实验目的
1. 学习fork进程复制的原理和实现细节.
2. 学习进程树以理解进程间父子关系.
3. 学习简单的并发控制,内存资源管理.
#### 实验预习内容
1. fork、wait、exit系统调用基本语义和相互依赖关系.
2. 进程树结构.
3. 并发控制,理解互斥锁,关中断对内核执行流的影响.
#### 实验内容
1. 编写fork系统调用.
(1) 编写fork系统调用,使调用者能够复制进程.
(2) 在fork基本语义的实现的同时,需要考虑一种边界情况,当内核pcb资源满了这次实验内核pcb资源只有20个无法再分配时如何处理这个需要你自行阅读手册`man 2 fork`这个2什么意思可以看手册的手册`man man`,它会告诉你答案)了解怎么处理这个细节.
(3) exec系统调用已经由我们实现好了,修改`main.c`中的`do_exec`函数调用参数变为`"testfork.bin"`,测试fork是否如预期复制了进程,复制出的两个进程并发着运行.
2. 编写wait系统调用.
(1) 编写wait系统调用,使调用者在调用时会遍历它的所有子进程,如果有僵尸子进程则将其回收,将子进程状态返回,否则该进程变成睡眠状态直到有子进程exit将其唤醒.
(2) 你在写wait基本语义的同时,需要考虑一种边界情况,当调用者进程没有子进程时如何处理?这个需要你自行阅读手册`man 2 wait`了解怎么处理这个细节.
(3) exit系统调用已经由我们实现好了,修改`main.c`中的`do_exec`函数调用参数变为`"initproc.bin"`,这是OS的初始进程,它会fork出一个新进程并exec成`"shell.bin"`执行shell程序后永远不停地执行wait系统,shell大家做课内实验的时候已经实现了,现在你需要以内核的视角实现以支持shell的系统调用.
(4) `testwait.bin`是一个用于检测你实现的fork、wait的功能正确性的用户程序,你可以通过shell调用它,如果不爆panic则你实现的功能全通过.
3. fork炸弹测试
(1) 如果光实现功能,那还算简单,但是实际上操作系统很大的难点不在于实现**出**功能,而是实现**对**功能,能够经受系统中运行的各种复杂环境的考验,并发控制和资源管理对一个操作系统尤为重要.
(2) `forkbomb.bin`是一个用于测试fork、wait和exit的程序,里面会不断随机调用fork、wait和exit系统调用达成fork炸弹的目的,你需要成功运行fork系统调用20000两万内核pcb的数量**必须**为20个,内核中会有一段专门的程序检测fork的次数.测试通过的效果图如下:
![](lab6-finish.png)
(3) (自我提高内容,自己想做就做,不用写进报告,**禁止内卷**)上面的两万次还算小打小闹,检验内核程序基本的并发控制和资源管理,而这次需要你成功运行fork系统调用2000000两百万次,预计耗时若干小时,这基本上能将你实现的系统调用的并发问题都暴露出来,虽然关中断很有效,但是希望你在做这个实验的时候能尽量少关,尽可能将你的并发问题暴露出来,如果你通过了这个测试就能看到我们准备的小彩蛋.
#### 实验总结
1. 进程树算是相当经典的描述进程关系的框架,但是如果不允许用进程树这个框架,意味着没有父子进程关系,你会怎么去设计进程的创建与销毁?(开放性答案,不要觉得自己做法低级,放心大胆往上写,任何言之有理的答案都行)
2. 在这次实验的时候你遇到了什么离谱的并发bug可以写没有,如果没有,那么恭喜你,你是一个严谨的人,你考虑问题非常周全)
#### 实验指导
##### fork-状态机模型
经过前几个实验的实践,我们从一个内核的视角看向用户程序可以逐渐隐约感觉到用户程序是一个状态机.具体什么意思,可以看回pcb中存储的用户寄存器.
```c
struct user_context {
u32 gs;
u32 fs;
u32 es;
u32 ds;
u32 edi;
u32 esi;
u32 ebp;
u32 kernel_esp;
u32 ebx;
u32 edx;
u32 ecx;
u32 eax;
u32 retaddr;
u32 eip;
u32 cs;
u32 eflags;
u32 esp;
u32 ss;
};
```
`cs,ds,es,fs,gs,ss,eax,ebx,ecx,edx,esi,edi,ebp,esi,eip,eflags`这些用户寄存器就可以描述用户当前的运行状态,再结合用户程序的页表pcb中的cr3变量,也就是用户程序能够访问的内存.这些对于一个用户程序来说就是它能够**直接**控制的所有信息.其余信息需要通过系统调用向内核申请服务获悉.
用户程序在执行的时候pc执行的汇编命令可以先简单抽象为三种形式
1. 修改寄存器
2. 修改内存
3. 系统调用等其余指令
也就是说如果不考虑第三种形式(下面都暂时不考虑),仅考虑用户程序由前两种形式的汇编指令组成,给用户进程指定的用户寄存器信息和内存,这个用户进程的执行过程可以**唯一确定**.
举个简单的例子,假设一个人要涂一面墙(内存),人手里有把刷子(寄存器),人可以进行下面两个操作之一(汇编指令):
1. 将刷子的颜色调成指定颜色
2. 往墙上某个区域用刷子涂上颜色,墙上原有的部分的颜色会被覆盖
假设给人固定好初始的刷子颜色、墙面颜色(颜色可能五花八门)和操作序列,那么每一次操作后墙面颜色和刷子颜色是可以**唯一确定**的.可以通过数学归纳法证明经过若干次操作后墙面颜色和刷子颜色我们也是可以**唯一确定**的.
所以对于一个用户进程,可以将它的寄存器信息和内存信息抽象成用户程序的<u>状态</u>,而汇编指令就是一个映射方法,它将用户程序从一个<u>状态</u>映射到另一个<u>状态</u>.只要给定初始寄存器信息和内存信息,并确定执行流(汇编指令序列),那么执行流中任意一环的程序<u>状态</u>我们都是可以**唯一确定**的,这就是**状态机模型**.
接下来回到第三种形式的汇编指令`系统调用等其余指令`,第三种形式指令本质上也是对用户寄存器和内存修改的指令,系统调用根据pcb中的信息反馈给用户,由于pcb对用户是不可见的,所以在用户视角下这个指令是不可预测的,意味着在用户视角下这条指令会发生什么是不可唯一确定的.而当我们将视角从用户移到内核,甚至硬件(某些指令是跟硬件交互的),当我们知道的信息越多,用户指令执行后的状态总是可以**唯一确定**的.
##### fork-状态机复制
在讲完状态机模型后fork就比较好理解了,我们根据状态机模型可知,如果给定确定的寄存器信息和内存信息,用户程序的执行状态是可以唯一确定的(假设没有系统调用这种对用户程序来说**不可唯一确定**的指令).
fork本质上就是对**状态机的复制**,当用户进程fork的时候,内核会新创建一个pcb,将当前进程的所有状态机信息原封不动地复制一份,也就是将当前进程的用户寄存器和内存原封不动复制一份给新的进程.
在复制完状态机信息后产生了两个进程,这两个进程的内存信息和寄存器信息**完全一样**,所以之后的执行流,用户程序状态机也会**完全一致**(直到第一次遇到系统调用这种指令),这样就通过一个进程fork出两个进程.
##### fork-进程树
fork能够一模一样复制一份进程的状态机,但是实际上这终究是两个独立的进程,调用fork系统调用的进程被称为父进程,在fork中新创建的进程称为子进程.这样除了初始进程pid号为0,每个进程都有它的父进程,从数据结构的视角下进程之间的父子关系能够构成一个以0号进程为根的进程树.
这样进程每次执行fork后,该进程在进程树上会新增一个儿子节点,这个儿子节点指向的进程是fork新被创建的进程.
##### fork-父子进程返回值
这属于是大家在做上课就已经需要搞明白的东西,为了能够区分fork完后进程是父进程还是子进程,从它的返回值判断,如果返回值为0,则执行流获悉它是子进程,否则返回值为子进程的pid号,执行流获悉它是父进程.
##### exit
上面提到的进程树不只是用于描述进程之间的关系,与之关系密切的是exit和wait系统调用,相信大家在课内实验写简易shell的时候用到了fork,exec,wait这三个系统调用,然后还有一个系统调用exit被隐式调用了,被隐藏在了main函数中的返回值中.
##### exit-用户程序执行的终点
我们先随便编写一个`hello_world`程序,然后通过`strace`命令查看执行所用到的系统调用.
```
$ ls
hello_world hello_world.c
$ cat ./hello_world.c
#include <stdio.h>
int main()
{
puts("hello world!");
return 114;
}
$ strace ./hello_world
... # 太长了就不展示上面的了
write(1, "hello world!\n", 13hello world!
) = 13
exit_group(114) = ?
+++ exited with 114 +++
```
可以看到用户程序在执行完exitexit_group系统调用与线程相关,但这次实验没有线程的概念,所以可以简单理解为exit后用户程序就停止运行了,而main中的返回值114也写入到exit系统调用的参数中了.
##### exit-程序的第一条指令和最后一条指令
在我们初学编程的时候肯定会有一个潜意识就是程序的第一条指令在main函数,这对初学者来说肯定是这样的,也很符合我们的直觉,觉得第一条指令在main函数.
不过这只是编译器给我们的错觉,实际上在进入main函数之前和结束main函数跳出之后程序实际上还是做了很多事的,在进入main函数之前会初始化很多东西,在结束了main函数跳出之后程序需要将很多在缓冲区的数据及时推送到内核,最后调用exit系统调用结束程序的执行.
在MINIOS中,用户程序的开始并不在main函数,由于`ld`工具默认`_start`是elf文件运行的程序入口,所以在`lib/user/astart.asm`存放着真正的用户程序入口,就是简单的调用了main,等main函数退出后eax寄存器存放着的是main函数的返回值,将eax作为参数调用exit函数结束程序的运行.虽然这个`_start`函数非常简单,但是还是能够作为一个例子证明main函数并不是程序运行的入口.
```nasm
[section .text]
extern main
extern exit
global _start
_start: ; 第一条指令
call main
push eax
call exit
```
##### exit-具体语义
通过实例可以发现exit系统调用实际上是一个进程的终止,通过翻阅linux手册`man 2 _exit`这个2什么意思可以看手册的手册`man man`,它会告诉你答案).里面有三段话能够帮助我们理解exit系统调用的具体语义.
```
The function _exit() terminates the calling process "immediately". Any
open file descriptors belonging to the process are closed. Any chil
dren of the process are inherited by init(1) (or by the nearest "sub
reaper" process as defined through the use of the prctl(2)
PR_SET_CHILD_SUBREAPER operation). The process's parent is sent a
SIGCHLD signal.
The value status & 0xFF is returned to the parent process as the
process's exit status, and can be collected using one of the wait(2)
family of calls.
These functions do not return.
```
从这段话中可以知道实现exit系统调用需要注意的事情
+ exit没有返回值,执行完后进程结束它的生命周期.
+ 如果有子进程还未被回收,这些子进程全部会被转移到初始进程下成为初始进程的子进程.(否则这个进程退出后这些子进程的父进程就不见了,这个肯定是不允许的,而初始进程肯定是活着的,交给它刚刚好)
+ 父进程会收到一个SIGCHLD信号信号这个实验不谈,可以简单理解为唤醒父进程).
+ exit中的参数会取其低8位这是很多博客不会讲到的,只有查手册才能搞明白具体的语义)作为返回参数传递给父进程.
+ 父进程需要调用wait这类的系统调用获取该进程的exit参数.
通过这些信息可以提炼出exit这个系统调用需要做的事:
+ 变成僵尸进程,状态置为ZOMBIE进程的生命周期已经结束了,它已经是个将死之"人"了,但是没有被父进程回收.所以就成了僵尸,不能运行但是就占着一部分资源).
+ 将所有子进程的父进程变成初始进程(学术名词叫托孤,还是挺有诗意的).
+ 处理exit参数.
+ 将部分资源回收(某些资源回收需要父进程完成).
+ 唤醒父进程.
##### wait
理解wait最重要的是需要先理解exit,wait这个系统调用的语义就如它的英文语义一样,就一个字:“等”.这个系统调用实际上就是在等待任意子进程exit,如果有子进程状态是ZOMBIE就回收子进程资源返回子进程状态.如果当前没有子进程可以回收,就进入SLEEP状态等待子进程exit唤醒该进程.
如果你顺着上面的指导和exit的语义思考可以得出对wait系统调用的实现的一个模糊的映像,可以知道它大概需要做什么,但是可能一些实现细节不是很清楚.不过在这次不能一步步全部引导你什么地方要做什么,这次需要你自己去查手册`man 2 wait`去仔细阅读理解wait这个系统调用的具体语义设计出一个符合语义的系统调用程序,要学会查手册,不是全部读一遍跟背诵默写一样,而是挑你需要的看,大概需要看50~60行英文.
##### 并发
指导的最后一个部分交给并发部分,并发这玩意在上课的前几节讲概念的时候你们会感觉还好,并发就是一段时间内同时做几件事.但是真正遇到并发问题的时候你们就不会这么想了,并发能够带来非常多的问题,可以说是很多是让你摸不着头脑的问题.
因为MINIOS内核态允许中断,所以很多时候在执行内核程序时会发生调度,这会导致多个内核进程在一个时间段同时执行的并发场景,这会导致一些非常诡异的并发问题.
讲一个实验设计中的例子,还记得实验五中`phy_malloc_4k`这个函数吗?在设计初期为了防止多个内核进程同时访问这个函数上了一个锁保证分配不会因为并发问题打架:
```c
phyaddr_t
phy_malloc_4k(void)
{
assert(phy_malloc_4k_p < 128 * MB);
while(xchg(&phy_malloc_4k_lock, 1) == 1)
schedule();
phyaddr_t addr = phy_malloc_4k_p;
phy_malloc_4k_p += PGSIZE;
free:
xchg(&phy_malloc_4k_lock, 0);
return addr;
}
```
但是在测试的时候发生了一件诡异的事,这个函数分配了一个128MB~128MB+4KB的物理页面出去然后内核在用的时候触发了page fault.在设计的时候挠了半天头,想不明白怎么能分配这么一个离谱的页面.
在仔细想了想之后搞明白了出问题的原因,第一个进程在争取到锁之后在`phy_malloc_4k_p += PGSIZE;`这句执行之前触发了调度,然后第二个进程在调用这个函数的时候成功躲过了assert语句的判断.调度回第一个进程把`phy_malloc_4k_p`更新,相当于第二个进程在执行assert语句的时候拿的是变量的旧值,没有拿新值判断,这个时候`phy_malloc_4k_p`的值可能是128MB,所以导致能够分配128MB~128MB+4KB这么一个离谱的物理页面.
所以正确的做法是将这个assert函数移到锁的临界区内,保证在临界区内值的判断是正确的.
```c
phyaddr_t
phy_malloc_4k(void)
{
while(xchg(&phy_malloc_4k_lock, 1) == 1)
schedule();
assert(phy_malloc_4k_p < 128 * MB);
phyaddr_t addr = phy_malloc_4k_p;
phy_malloc_4k_p += PGSIZE;
free:
xchg(&phy_malloc_4k_lock, 0);
return addr;
}
```
##### 并发-一把大锁保平安
在并发面前,很多事情非常难以预料,我们人脑计算能力是非常渺小的,无法考虑那么全面.能够做的只能一把大锁保平安,简单来说,如果一个执行流需要访问/修改某一公共资源时,就需要给公共资源加上一个锁使用`xchg`原子指令进行上互斥锁进入临界区,防止公共资源同时被其他执行流访问/修改的情况.
##### 并发-多把大锁保平安?
在实际实践的时候,执行流可能需要访问多个公共资源,比如exit就需要访问三个pcb的锁,fork和wait需要访问两个pcb的锁,这就意味我们可能要上多把锁.
如果上一把互斥锁那还好处理,执行流要么进入临界区要么没进去,如果上多把锁就又涉及到上锁这个行为本身的并发问题,考虑一个情况:
两个执行流,第一个要先给a上锁再给b上锁,第二个要先给b上锁再给a上锁,然后一个可能的情况就是第一个执行流给a上完锁之后马上发生了调度,然后调度到第二个执行流,第二个执行流给b上完锁了发现a被上锁了于是就在等,然后调度回第一个执行流,第一个执行流发现b被上了锁,于是也在等.于是这两个执行流等到天荒地老也没等到对面释放锁.
对于这个问题的解决有一种办法,就是协商好上锁顺序,如果两个执行流都按照ab或ba的上锁顺序就能保证至少一个执行流能够运作,当一个执行流执行完释放完锁后另一个执行流再上锁.
##### 并发-实验六的上锁顺序
对于此次实验来说,需要上多把锁的场景有fork,exit和wait,这些场景都需要对进程pcb上锁,所以这里需要协商规范一下上锁顺序.
上锁规范是以进程树的0号进程为根,上锁顺序从根到叶子,就是说对于一个进程如果它,它祖宗,它儿子都需要被上锁,那么上锁顺序必须为祖宗->该节点->儿子.这种上锁方式能够保证上锁的节点在进程树中是深度递增的,无论什么情况都能保证至少一个执行流能够正常运行,不过这里暂且不证明.