《操作系统真象还原》第15章(下)
15.5加载用户进程
“
exec会把一个可执行文件的的绝对路径作为参数,把当前正在运行的用户进程的进程体(代码段、数据段、堆、栈)用该可执行文件的进程体替换,从而实现了新进程的执行。
有了exec,用户便可以完成任意外部命令(用户进程)的运行。
执行外部命令时bash才会fork出子进程并调用exec从磁盘上加载外部命令对应的程序,然后执行该程序,从而实现了外部命令的执行。
”
15.6实现系统调用wait和exit
“
exit的作用很直白,就是使进程“主动”退出。
任何时候进程都会调用exit,即使程序员未写入调用exit的代码,在C运行库的最后也会发起exit的调用。由此可见,结束程序运行始终是通过调用exit系统调用实现的,因为这是唯一让系统重新拿回处理器控制权的机会。
wait的作用是阻塞父进程自己,直到任意一个子进程结束运行。
wait的作用主要有两个,一是使父进程阻塞,二是获得子进程的返回值。
这本质上属于进程同步的问题,也就是协调父子进程某些代码的执行次序,我们希望子进程执行完某些代码后再让父进程执行。对于父子进程的同步,可以用wait系统调用来解决,这其实就是wait的初心。
回收pcb内存空间的工作是在系统调用wait对应的内核实现中。
当父进程提前退出时,它所有的子进程还在运行,没有一个执行了exit,因为它们的生命周期尚未结束,还在运行中,个个都拥有“全尸”(进程体),这些进程就被称为孤儿进程。这时候所有的子进程会被init进程收养,init进程会成为这些子进程的新父亲,当子进程退出时会由init负责为其“收尸”的。
僵尸进程就是针对子进程的返回值能否成功提交给父进程而提出的,父进程不调用wait,就无法获知子进程的返回值,从而内核就无法回收子进程pcb所占的空间,因此就会在队列中占据一个进程表项。僵尸进程是没有进程体的,因为其进程体已经在调用exit时被内核回收了。现在只剩下一个pcb还在进程队列中。
”
15.7管道
“
进程间通信的方式有很多种,有消息队列、共享内存、socket网络通信等,还有一种就是管道。
管道其实就是内核空间中的环形内存缓冲区。
因此通常的用法是进程在创建管道之后,马上调用fork,克隆出一个子进程,子进程完全继承了父进程的一切,也就是说和父进程一模一样,因此继承了管道的描述符,这为父子进程通信提供了保证。
一般情况下,父子进程中都是一个读数据,一个写数据,并不会存在一方又读又写的情况,因此在父子进程中会分别关掉不使用的管道描述符。
”
步骤:
5.加载用户进程
6.wait和exit系统调用
7.(匿名)管道
5.加载用户进程
①userprog/exec.c:

1 #include "exec.h"
2 #include "thread.h"
3 #include "stdio-kernel.h"
4 #include "fs.h"
5 #include "string.h"
6 #include "global.h"
7 #include "memory.h"
8
9 extern void int_exit(void);
10 typedef uint32_t Elf32_Word,Elf32_Addr,Elf32_Off;
11 typedef uint16_t Elf32_Half;
12 #define TASK_NAME_LEN 16
13 /* 32位elf头 */
14 struct Elf32_Ehdr{
15 unsigned char e_ident[16];
16 Elf32_Half e_type;
17 Elf32_Half e_machine;
18 Elf32_Word e_version;
19 Elf32_Addr e_entry;
20 Elf32_Off e_phoff;
21 Elf32_Off e_shoff;
22 Elf32_Word e_flags;
23 Elf32_Half e_ehsize;
24 Elf32_Half e_phentsize;
25 Elf32_Half e_phnum;
26 Elf32_Half e_shentsize;
27 Elf32_Half e_shnum;
28 Elf32_Half e_shstrndx;
29 };
30
31 /* 程序头表Program header,即段描述头 */
32 struct Elf32_Phdr{
33 Elf32_Word p_type;
34 Elf32_Off p_offset;
35 Elf32_Addr p_vaddr;
36 Elf32_Addr p_paddr;
37 Elf32_Word p_filesz;
38 Elf32_Word p_memsz;
39 Elf32_Word p_flags;
40 Elf32_Word p_align;
41 };
42
43 /* 段类型 */
44 enum segment_type{
45 PT_NULL, // 可忽略
46 PT_LOAD, // 可加载程序段
47 PT_DYNAMIC, // 动态加载信息
48 PT_INTERP, // 动态加载器名称
49 PT_NOTE, // 一些辅助信息
50 PT_SHLIB, // 保留
51 PT_PHDR // 程序头表
52 };
53
54 /* 将文件描述符fd指向的文件中,偏移为offset,大小filesz的段加载到虚拟内存vaddr */
55 static bool segment_load(int32_t fd,uint32_t offset,uint32_t filesz,uint32_t vaddr){
56 uint32_t vaddr_first_page=vaddr & 0xfffff000; // vaddr所在的页框
57 uint32_t size_in_first_page=PG_SIZE-(vaddr & 0x00000fff); // 加载到内存后,文件第一个页框所占用的字节大小
58 uint32_t occupy_pages=0;
59 /* 若第一个页框容不下该段 */
60 if (filesz>size_in_first_page){
61 uint32_t left_size=filesz-size_in_first_page;
62 occupy_pages=DIV_ROUND_UP(left_size,PG_SIZE)+1;
63 }else {
64 occupy_pages=1;
65 }
66
67 /* 为进程分配内存 */
68 uint32_t page_idx=0;
69 uint32_t vaddr_page=vaddr_first_page;
70 while (page_idx<occupy_pages){
71 uint32_t* pde=pde_ptr(vaddr_page);
72 uint32_t* pte=pte_ptr(vaddr_page);
73
74 /* 如果pde或者pte不存在就分配内存,
75 * pde的判断要在pte之前,否则pde不存在会导致
76 * 判断pte时缺页异常 */
77 if (!(*pde & 0x00000001) || !(*pte & 0x00000001)){
78 if (get_a_page(PF_USER,vaddr_page)==NULL){
79 return false;
80 }
81 } // 若原进程页表已经分配了,就利用现有的物理页,直接覆盖进程体
82 vaddr_page+=PG_SIZE;
83 ++page_idx;
84 }
85 sys_lseek(fd,offset,SEEK_SET);
86 sys_read(fd,(void*)vaddr,filesz);
87 return true;
88 }
89
90 /* 从文件系统上加载用户程序pathname,成功则返回程序的起始地址,失败则返回-1 */
91 static int32_t load(const char* pathname){
92 int32_t ret=-1;
93 struct Elf32_Ehdr elf_header;
94 struct Elf32_Phdr prog_header;
95 memset(&elf_header,0,sizeof(struct Elf32_Ehdr));
96
97 int32_t fd=sys_open(pathname,O_RDONLY); // 只读
98 if (fd==-1){
99 return -1;
100 }
101
102 if (sys_read(fd,&elf_header,sizeof(struct Elf32_Ehdr))!=sizeof(struct Elf32_Ehdr)){
103 ret=-1;
104 goto done;
105 }
106
107 /* 校验elf头 */
108 if (memcmp(elf_header.e_ident,"\177ELF\1\1\1",7) \
109 || elf_header.e_type!=2 \
110 || elf_header.e_machine!=3 \
111 || elf_header.e_version!=1 \
112 || elf_header.e_phnum>1024 \
113 || elf_header.e_phentsize!=sizeof(struct Elf32_Phdr)){
114 ret=-1;
115 goto done;
116 }
117
118 Elf32_Off prog_header_offset=elf_header.e_phoff;
119 Elf32_Half prog_header_size=elf_header.e_phentsize;
120
121 /* 遍历所有程序头 */
122 uint32_t prog_idx=0;
123 while (prog_idx<elf_header.e_phnum){
124 memset(&prog_header,0,prog_header_size);
125
126 /* 将文件的指针定位到程序头 */
127 sys_lseek(fd,prog_header_offset,SEEK_SET);
128
129 /* 只获取程序头 */
130 if (sys_read(fd,&prog_header,prog_header_size)!=prog_header_size){
131 ret=-1;
132 goto done;
133 }
134
135 /* 如果是可加载段就调用segment_load加载到内存 */
136 if (prog_header.p_type==PT_LOAD){
137 if (!segment_load(fd,prog_header.p_offset,prog_header.p_filesz,prog_header.p_vaddr)){
138 ret=-1;
139 goto done;
140 }
141 }
142
143 /* 更新下一个程序头的偏移 */
144 prog_header_offset+=elf_header.e_phentsize;
145 ++prog_idx;
146 }
147 ret=elf_header.e_entry;
148 done:
149 sys_close(fd);
150 return ret;
151 }
152
153 /* 用path指向的程序替换当前进程 */
154 int32_t sys_execv(const char* path,const char* argv[]){
155 uint32_t argc=0;
156 while (argv[argc]){
157 ++argc;
158 }
159 int32_t entry_point=load(path);
160 if (entry_point==-1){ // 加载失败
161 return -1;
162 }
163
164 struct task_struct* cur=running_thread();
165 /* 修改进程名 */
166 memcpy(cur->name,path,TASK_NAME_LEN);
167 cur->name[TASK_NAME_LEN-1]=0;
168
169 struct intr_stack* intr_0_stack=(struct intr_stack*)((uint32_t)cur+PG_SIZE-sizeof(struct intr_stack));
170 /* 将参数传递给用户进程 */
171 intr_0_stack->ebx=(int32_t)argv;
172 intr_0_stack->ecx=argc;
173 intr_0_stack->eip=(void*)entry_point;
174 /* 使用户进程的栈地址为最高用户空间地址 */
175 intr_0_stack->esp=(void*)0xc0000000;
176
177 /* exec不同于fork,为使新进程更快被执行,直接从中断返回 */
178 asm volatile("movl %0,%%esp;jmp intr_exit"::"g"(intr_0_stack):"memory");
179 return 0;
180 }
感觉还是有点怪怪的,居然不把结构体放在.h文件中,虽然也无所谓吧。
②userprog/exec.h:

1 #include "stdio.h"
2 int main(void){
3 printf("prog_no_arg from disk.\n");
4 while (1); // 因为没有exit(),用while (1); 卡住
5 return 0;
6 }
③shell/shell.c,修改my_shell():

1 /* 简单的shell */
2 void my_shell(void){
3 cwd_cache[0]='/';
4 while (1){
5 print_prompt();
6 memset(final_path,0,MAX_PATH_LEN);
7 memset(cmd_line,0,MAX_PATH_LEN);
8 readline(cmd_line,MAX_PATH_LEN);
9 if (cmd_line[0]==0){ // 只键入回车
10 continue;
11 }
12 argc=-1;
13 argc=cmd_parse(cmd_line,argv,' ');
14 if (argc==-1){
15 printf("num of arguments exceed %d\n",MAX_ARG_NR);
16 continue;
17 }
18 if (!strcmp("ls",argv[0])){
19 buildin_ls(argc,argv);
20 }else if (!strcmp("cd",argv[0])){
21 if (buildin_cd(argc,argv)!=NULL){
22 memset(cwd_cache,0,MAX_PATH_LEN);
23 strcpy(cwd_cache,final_path);
24 }
25 }else if (!strcmp("pwd",argv[0])){
26 buildin_pwd(argc,argv);
27 }else if (!strcmp("ps",argv[0])){
28 buildin_ps(argc,argv);
29 }else if (!strcmp("clear",argv[0])){
30 buildin_clear(argc,argv);
31 }else if (!strcmp("mkdir",argv[0])){
32 buildin_mkdir(argc,argv);
33 }else if (!strcmp("rmdir",argv[0])){
34 buildin_rmdir(argc,argv);
35 }else if (!strcmp("rm",argv[0])){
36 buildin_rm(argc,argv);
37 }else { // 如果是外部命令,需要从磁盘上加载
38 int32_t pid=fork();
39 if (pid){ // 父进程
40 /* 必须加while (1);,一般情况下父进程比子进程先执行,
41 * 则会进行下一轮循环将final_path清空,这样子进程将无法从final_path中获得参数 */
42 while (1);
43 }else { // 子进程
44 make_clear_abs_path(argv[0],final_path);
45 argv[0]=final_path;
46 /* 先判断下文件是否存在 */
47 struct stat file_stat;
48 memset(&file_stat,0,sizeof(struct stat));
49 if (stat(argv[0],&file_stat)==-1){
50 printf("my_shell: cannot access %s: No such file or directory\n",argv[0]);
51 }else {
52 execv(argv[0],argv);
53 }
54 while (1);
55 }
56 }
57 int32_t arg_idx=0;
58 while (arg_idx<MAX_ARG_NR){
59 argv[arg_idx]=NULL;
60 ++arg_idx;
61 }
62 }
63 PANIC("my_shell: should not be here");
64 }
④创建新目录command,command/prog_no_arg.c:
1 #include "stdio.h"
2 int main(void){
3 printf("prog_no_arg from disk.\n");
4 while (1); // 因为没有exit(),用while (1); 卡住
5 return 0;
6 }
就只是写个程序试试,还不算真正的(外部)命令 。
⑤command/compile.sh:

1 BIN="prog_no_arg"
2 CFLAGS="-Wall -m32 -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -Wsystem-headers"
3 LIB="../lib/"
4 OBJS="../build/string.o ../build/syscall.o ../build/stdio.o ../build/assert.o"
5 DD_IN=$BIN
6 DD_OUT="/home/zbb/bochs/hd60M.img"
7
8 gcc $CFLAGS -I $LIB -o $BIN".o" $BIN".c"
9 ld -m elf_i386 -e main $BIN".o" $OBJS -o $BIN
10 SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
11
12 if [ -f $BIN ];then
13 dd if=./$DD_IN of=$DD_OUT bs=512 \
14 count=$SEC_CNT seek=300 conv=notrunc
15 fi
细心的你已经看到,在OBJS内有个新的assert.o。是的,这就是之前那个小写的panic和assert所在的文件,因为我发现如果用debug.o(即PANIC和ASSERT所在地)文件的话,是会报错显示很多函数符号找不到,所以不得不写assert.c和assert.h。要问我怎么知道这assert.c的内容的——好吧,我瞄了一下作者的源码,链接放在文末了 :)
⑥lib/user/assert.c:
1 #include "assert.h"
2 #include "stdio.h"
3 void user_spin(char* filename, int line, const char* func, const char* condition) {
4 printf("\n\n\n\nfilename %s\nline %d\nfunction %s\ncondition %s\n", filename, line, func, condition);
5 while(1);
6 }
⑦lib/user/assert.h:

1 #ifndef __LIB_USER_ASSERT_H
2 #define __LIB_USER_ASSERT_H
3
4 #define NULL ((void*)0)
5
6 void user_spin(char* filename, int line, const char* func, const char* condition);
7 #define panic(...) user_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)
8
9 #ifdef NDEBUG
10 #define assert(CONDITION) ((void)0)
11 #else
12 #define assert(CONDITION) \
13 if (!(CONDITION)) { \
14 panic(#CONDITION); \
15 }
16
17 #endif/*NDEBUG*/
18
19 #endif/*__LIB_USER_ASSERT_H*/
然后,string.c中的各函数因为用到了ASSERT(),所以要换成assert()。
再在command目录下执行“sh compile.sh”,就会出现:
好吧,这个prog_no_arg好大。。。所以下面main()的第27行就和书中的有所不同。
⑧kernel/main.c:

1 #include "print.h"
2 #include "init.h"
3 #include "thread.h"
4 #include "interrupt.h"
5 #include "console.h"
6 #include "ioqueue.h"
7 #include "process.h"
8 #include "syscall.h"
9 #include "syscall-init.h"
10 #include "stdio.h"
11 #include "memory.h"
12 #include "fs.h"
13 #include "string.h"
14 #include "file.h"
15 #include "shell.h"
16 #include "debug.h"
17 #include "ide.h"
18 #include "stdio-kernel.h"
19
20 void init(void);
21
22 int main(void){
23 put_str("Welcome,\nI am kernel!\n");
24 init_all();
25 intr_enable();
26
27 uint32_t file_size=10284;;
28 uint32_t sec_cnt=DIV_ROUND_UP(file_size,512);
29 struct disk* sda=&channels[0].devices[0];
30 void* prog_buf=sys_malloc(file_size);
31 ide_read(sda,300,prog_buf,sec_cnt);
32 int32_t fd=sys_open("/prog_no_arg",O_CREAT|O_RDWR);
33 if (fd!=-1){
34 if (sys_write(fd,prog_buf,file_size)==-1){
35 printk("file write error!\n");
36 while (1);
37 }
38 }
39
40 cls_screen();
41 console_put_str("[zbb@LAPTOP //]$ ");
42 while(1);
43 return 0;
44 }
45
46 /* init进程 */
47 void init(void){
48 uint32_t ret_pid=fork();
49 if (ret_pid){ // 父进程
50 printf("parent...\n");
51 while (1);
52 }else { // 子进程
53 printf("son...\n");
54 my_shell();
55 }
56 PANIC("init: should not be here.");
57 }
记得修改一下makefile,就有了结果:
果然没有这么顺利,其实我的内心在看到这一刻的时候有点小崩,因为我一直以来参考的那位博主到这一步因为缺页错误也寄了,然后我也倒在了这一步。。。、
然后我知道,内存管理和文件系统的代码是海量的,根本不可能一步步跟着走,看看到底是哪一步,甚至哪个二进制错了。我也不可能再去从源码替换每一个文件找错,哎。
难道故事的终点就到这里了吗?
诚然,如果你是后来者,你一定会看到我结束此篇,所以我没有放弃——我打印出了每一步执行的参数、变量,就像我曾经在刷题时一样(好吧,这让我回想起了以前调试线段树的时光,那时的我还会花一个上午盯着屏幕看每个值的变化)——直到在memory.c的get_a_page()中输出vaddr与cur->userprog_vaddr.vaddr_start,我发现它们俩的值是相同的,说实话,可能是因为我对内存的理解不够深刻的原因,一开始并没有觉得它们俩可能相同,然后我苦苦思索,到底是为什么它们俩会相同。我想不明白,直到有个想法冒出来:“为什么它们俩不能相同呢”,我突然觉得这两个值可以相同啊,只不过一个是prog_no_arg的某个可执行段的虚拟地址的所在页框首地址,一个是当前用户进程的起始地址。也许这也只是一个美丽的误会,让我以为它们两个真的可以相同。于是我又去看了此时的源码中的kernel/memory.c,那个ASSERT(bit_idx>0)偷偷变成了ASSERT(bit_idx>=0),果然是的!我认定就是这里有问题,而没有一个人指出来,包括本书作者和我看过的所有博主,太离谱了。然后,你懂了,它真的就行了 > <
话不多说,来看看一晚上折磨的结果:
看,成功输出了“prog_no_arg from disk”,吼吼。
接下来我们使用户进程支持参数
①command/prog_arg.c:

1 #include "stdio.h"
2 #include "syscall.h"
3 #include "string.h"
4 int main(int argc,char** argv){
5 int arg_idx=0;
6 while (arg_idx<argc){
7 printf("argv[%d] is %s.\n",arg_idx,argv[arg_idx]);
8 ++arg_idx;
9 }
10 int pid=fork();
11 if (pid){
12 int delay=900000;
13 while (delay--);
14 printf("\n I'm father prog, my pid: %d, I will show process list.\n",getpid());
15 ps();
16 } else{
17 char abs_path[512]={0};
18 printf("\n I'm child prog, my pid: %d, I will exec %s right now.\n",getpid(),argv[1]);
19 if (argv[1][0]!='/'){
20 getcwd(abs_path,512);
21 strcat(abs_path,"/");
22 strcat(abs_path,argv[1]);
23 execv(abs_path,argv);
24 } else{
25 execv(argv[1],argv);
26 }
27 }
28 while(1);
29 return 0;
30 }
②command/compile2.sh:

1 BIN="prog_arg"
2 CFLAGS="-Wall -m32 -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -Wsystem-headers"
3 LIB="../lib/ ../lib/kernel/ ../lib/user/ ../kernel/ ../device/ ../thread/ ../userprog ../fs/ ../shell/"
4 OBJS="../build/string.o ../build/syscall.o ../build/stdio.o ../build/assert.o start.o"
5 DD_IN=$BIN
6 DD_OUT="/home/zbb/bochs/hd60M.img"
7
8 nasm -f elf ./start.S -o ./start.o
9 ar rcs simple_crt.a $OBJS start.o
10 gcc $CFLAGS -I $LIB -o $BIN".o" $BIN".c"
11 ld -m elf_i386 $BIN".o" simple_crt.a -o $BIN
12 SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
13
14 if [ -f $BIN ];then
15 dd if=./$DD_IN of=$DD_OUT bs=512 \
16 count=$SEC_CNT seek=300 conv=notrunc
17 fi
此时,注意要把ld那一行的 ”-e main“删除,这样才会以start.S中的_start为默认入口地址。
③command/start.S:

1 [bits 32]
2 extern main
3 section .text
4 global _start
5 _start:
6 ; 下面这两个要和execv中load之后的寄存器一致
7 push ebx ; 压入argv
8 push ecx ; 压入argc
9 call main
再在command目录下执行“sh compile2.sh”,就会出现:
④kernel/main.c:

1 #include "print.h"
2 #include "init.h"
3 #include "thread.h"
4 #include "interrupt.h"
5 #include "console.h"
6 #include "ioqueue.h"
7 #include "process.h"
8 #include "syscall.h"
9 #include "syscall-init.h"
10 #include "stdio.h"
11 #include "memory.h"
12 #include "fs.h"
13 #include "string.h"
14 #include "file.h"
15 #include "shell.h"
16 #include "debug.h"
17 #include "ide.h"
18 #include "stdio-kernel.h"
19
20 void init(void);
21
22 int main(void){
23 put_str("Welcome,\nI am kernel!\n");
24 init_all();
25 intr_enable();
26
27 uint32_t file_size=10448;
28 uint32_t sec_cnt=DIV_ROUND_UP(file_size,512);
29 struct disk* sda=&channels[0].devices[0];
30 void* prog_buf=sys_malloc(file_size);
31 ide_read(sda,300,prog_buf,sec_cnt);
32 int32_t fd=sys_open("/prog_arg",O_CREAT|O_RDWR);
33 if (fd!=-1){
34 if (sys_write(fd,prog_buf,file_size)==-1){
35 printk("file write error!\n");
36 while (1);
37 }
38 }
39
40 cls_screen();
41 console_put_str("[zbb@LAPTOP //]$ ");
42 while(1);
43 return 0;
44 }
45
46 /* init进程 */
47 void init(void){
48 uint32_t ret_pid=fork();
49 if (ret_pid){ // 父进程
50 printf("parent...\n");
51 while (1);
52 }else { // 子进程
53 printf("son...\n");
54 my_shell();
55 }
56 PANIC("init: should not be here.");
57 }
相较前一个kernel/main.c,修改了file_size和sys_open()。
哎,GG了。对不起各位,这个我改不出来了,已经一个下午了,真的失去耐心了。我尝试过在start.S中添加一句“jmp $”,然后也没能死循环,而在exec.c的最后一句内嵌汇编上面输出是可行。推测一下是给栈赋值的时候出现了问题。。。就这样吧,试试看后面还可以继续写不。
6.wait和exit系统调用
①thread/thread.h,在pcb中增加一个exit_status成员,并添加函数声明:

int8_t exit_status; // 进程结束时自身调用exit()传入的参数
void sys_ps(void);
void thread_exit(struct task_struct* thread_over,bool need_schedule);
struct task_struct* pid2thread(int32_t pid);
void release_pid(pid_t pid);
②kernel/memory.c,新增一个释放物理页框的函数:

1 /* 根据物理页框地址pg_phy_addr在相应的内存池的位图清0,不改动页表 */
2 void free_a_phy_page(uint32_t pg_phy_addr){
3 struct pool* mem_pool;
4 uint32_t bit_idx=0;
5 if (pg_phy_addr>=user_pool.phy_addr_start){
6 mem_pool=&user_pool;
7 bit_idx=(pg_phy_addr-user_pool.phy_addr_start)/PG_SIZE; // 获取位图索引
8 }else {
9 mem_pool=&kernel_pool;
10 bit_idx=(pg_phy_addr-kernel_pool.phy_addr_start)/PG_SIZE;
11 }
12 bitmap_set(&mem_pool->pool_bitmap,bit_idx,0); // 物理内存池中的相应位置0
13 }
③kernel/memory.h:
void free_a_phy_page(uint32_t pg_phy_addr);
④thread/thread.c:
修改thread_init():

1 /* 初始化线程环境 */
2 void thread_init(void){
3 put_str("thread_init start\n");
4
5 list_init(&thread_ready_list);
6 list_init(&thread_all_list);
7 pid_pool_init();
8
9 /* 先创建第一个用户进程: init */
10 process_execute(init,"init"); // 首先初始化init进程,其pid为1
11
12 /* 将当前main函数创建为线程 */
13 make_main_thread();
14
15 /* 创建idle线程 */
16 idle_thread=thread_start("idle",10,idle,NULL);
17
18 put_str("thread_init done\n");
19 }
增加几个基础函数:

1 /* 回收thread_over的pcb和页表,并将其从调度队列中移出 */
2 void thread_exit(struct task_struct* thread_over,bool need_schedule){
3 /* 要保证schedule在关中断的情况下调用 */
4 intr_disable();
5 thread_over->status=TASK_DIED;
6
7 /* 如果thread_over不是当前线程,就可能还在队列中,将其从中移出 */
8 if (elem_find(&thread_ready_list,&thread_over->general_tag)){
9 list_remove(&thread_over->general_tag);
10 }
11 if (thread_over->pgdir){ // 如果是进程,回收进程的页表
12 mfree_page(PF_KERNEL,thread_over->pgdir,1);
13 }
14
15 /* 从all_thread_list中去掉此任务 */
16 list_remove(&thread_over->all_list_tag);
17
18 /* 回收pcb所在的页,主线程的pcb不再堆中,跨过 */
19 if (thread_over!=main_thread){
20 mfree_page(PF_KERNEL,thread_over,1);
21 }
22
23 /* 归还pid */
24 release_pid(thread_over->pid);
25
26 /* 如果需要下一轮调度则主动调用schedule */
27 if (need_schedule){
28 schedule();
29 PANIC("thread_exit: should not be here.\n");
30 }
31 }
32
33 /* 比对任务的pid */
34 static bool pid_check(struct list_elem* pelem,int32_t pid){
35 struct task_struct* pthread=elem2entry(struct task_struct,all_list_tag,pelem);
36 if (pthread->pid==pid){
37 return true;
38 }
39 return false;
40 }
41
42 struct task_struct* pid2thread(int32_t pid){
43 struct list_elem* pelem=list_traversal(&thread_all_list,pid_check,pid);
44 if (pelem==NULL){
45 return NULL;
46 }
47 struct task_struct* thread=elem2entry(struct task_struct,all_list_tag,pelem);
48 return thread;
49 }
⑤userprog/wait_exit.c:

1 #include "wait_exit.h"
2 #include "global.h"
3 #include "debug.h"
4 #include "thread.h"
5 #include "list.h"
6 #include "stdio-kernel.h"
7 #include "memory.h"
8 #include "bitmap.h"
9 #include "fs.h"
10
11 /* 释放用户进程资源:
12 * 1.页表中对应的物理页
13 * 2.虚拟内存池占物理页框
14 * 3.关闭打开的文件 */
15 static void release_prog_resource(struct task_struct* release_thread){
16 uint32_t* pgdir_vaddr=release_thread->pgdir;
17 uint16_t user_pde_nr=768,pde_idx=0;
18 uint32_t pde=0;
19 uint32_t* v_pde_ptr=NULL; // v表示var,和函数pde_ptr区分
20
21 uint16_t user_pte_nr=1024,pte_idx=0;
22 uint32_t pte=0;
23 uint32_t* v_pte_ptr=NULL; // 和pte_ptr区分
24
25 uint32_t* first_pte_vaddr_in_pde=NULL; // 用于记录pde中第0个pte的地址
26 uint32_t pg_phy_addr=0;
27
28 /* 回收页表中用户空间的页框 */
29 while (pde_idx<user_pde_nr){
30 v_pde_ptr=pgdir_vaddr+pde_idx;
31 pde=*v_pde_ptr;
32 if (pde & 0x00000001){ // 如果页目录项p位为1,表示该目录项下可能有页表
33 first_pte_vaddr_in_pde=pte_ptr(pde_idx * 0x400000); // 一个页表为4M
34 pte_idx=0;
35 while (pte_idx<user_pte_nr){
36 v_pte_ptr=first_pte_vaddr_in_pde+pte_idx;
37 pte=*v_pte_ptr;
38 if (pte & 0x00000001){
39 /* 将pte中记录的物理页框直接在相应的内存池的位图中清0 */
40 pg_phy_addr=pte & 0xfffff000;
41 free_a_phy_page(pg_phy_addr);
42 }
43 ++pte_idx;
44 }
45 /* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
46 pg_phy_addr=pde & 0xfffff000;
47 free_a_phy_page(pg_phy_addr);
48 }
49 ++pde_idx;
50 }
51
52 /* 回收用户虚拟地址池所占的物理内存 */
53 uint32_t bitmap_pg_cnt=(release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len)/PG_SIZE;
54 uint8_t* user_vaddr_pool_bitmap=release_thread->userprog_vaddr.vaddr_bitmap.bits;
55 mfree_page(PF_KERNEL,user_vaddr_pool_bitmap,bitmap_pg_cnt);
56
57 /* 关闭进程打开的文件 */
58 uint8_t fd_idx=3;
59 while (fd_idx<MAX_FILES_OPEN_PER_PROC){
60 if (release_thread->fd_table[fd_idx]!=-1){
61 sys_close(fd_idx);
62 }
63 ++fd_idx;
64 }
65 }
66
67 /* list_traversal的回调函数,
68 * 查找pelem的parent_pid是否是ppid,成功则返回true,失败则返回false */
69 static bool find_child(struct list_elem* pelem,int32_t ppid){
70 /* elem2entry中间的参数all_list_tag取决于pelem对应的变量名 */
71 struct task_struct* pthread=elem2entry(struct task_struct,all_list_tag,pelem);
72 if (pthread->parent_pid==ppid){
73 return true;
74 }
75 return false;
76 }
77
78 /* list_traversal的回调函数,
79 * 查找状态为TASK_HANGING的任务 */
80 static bool find_hanging_child(struct list_elem* pelem,int32_t ppid){
81 struct task_struct* pthread=elem2entry(struct task_struct,all_list_tag,pelem);
82 if (pthread->parent_pid==ppid && pthread->status==TASK_HANGING){
83 return true;
84 }
85 return false;
86 }
87
88 /* list_traversal的回调函数,
89 * 将一个子进程过继给init */
90 static bool init_adopt_a_child(struct list_elem* pelem,int32_t pid){
91 struct task_struct* pthread=elem2entry(struct task_struct,all_list_tag,pelem);
92 if (pthread->parent_pid==pid){
93 pthread->parent_pid=1; // init进程的pid=1
94 }
95 return false;
96 }
97
98 /* 等待子进程调用exit,将子进程的退出状态保存到status指向的变量。
99 * 成功则返回子进程pid,失败则返回-1 */
100 pid_t sys_wait(int32_t* status){
101 struct task_struct* parent_thread=running_thread();
102
103 while (1){
104 /* 优先处理已经是挂起状态的任务 */
105 struct list_elem* child_elem=list_traversal(&thread_all_list,find_hanging_child,parent_thread->pid);
106 /* 若有挂起的子进程 */
107 if (child_elem!=NULL){
108 struct task_struct* child_thread=elem2entry(struct task_struct,all_list_tag,child_elem);
109 *status=child_thread->exit_status;
110
111 /* thread_exit之后,pcb被回收,因此提前获取pid */
112 uint16_t child_pid=child_thread->pid;
113
114 /* 2.从就绪队列和全部队列中删除进程表项 */
115 thread_exit(child_thread,false);
116 /* 进程表项是进程或线程的最后保留的资源,至此该进程彻底消失 */
117
118 return child_pid;
119 }
120
121 /* 判断是否有子进程 */
122 child_elem=list_traversal(&thread_all_list,find_child,parent_thread->pid);
123 if (child_elem==NULL){ // 若没有子进程则出错返回
124 return -1;
125 }else {
126 /* 若子进程还未运行完,即还未调用exit(),则挂起 */
127 thread_block(TASK_WAITING); // 等待子进程执行exit()唤醒自己
128 }
129 }
130 }
131
132 /* 子进程结束自己时调用 */
133 void sys_exit(int32_t status){
134 struct task_struct* child_thread=running_thread();
135 child_thread->exit_status=status;
136 if (child_thread->parent_pid==-1){
137 PANIC("sys_exit: child_thread->parent_pid is -1\n");
138 }
139
140 /* 将进程child_thread的所有子进程都过继给init */
141 list_traversal(&thread_all_list,init_adopt_a_child,child_thread->pid);
142
143 /* 回收进程child_thread的资源 */
144 release_prog_resource(child_thread);
145
146 /* 如果父进程正在等待子进程退出,将父进程唤醒 */
147 struct task_struct* parent_thread=pid2thread(child_thread->parent_pid);
148 if (parent_thread->status==TASK_WAITING){
149 thread_unblock(parent_thread);
150 }
151
152 /* 将自己挂起,等待父进程获取其status,并回收其pcb */
153 thread_block(TASK_HANGING);
154 }
⑥userprog/wait_exit.h:

1 #ifndef __USERPROG_WAIT_EXIT_H
2 #define __USERPROG_WAIT_EXIT_H
3 #include "thread.h"
4 pid_t sys_wait(int32_t* status);
5 void sys_exit(int32_t status);
6 #endif
⑦lib/user/syscall.h+lib/user/syscall.c+userprog/syscall-init.c:
很平常的增加exit和wait系统调用的步骤,就不赘述了:)
⑧shell/shell.c,修改my_shell():

1 /* 简单的shell */
2 void my_shell(void){
3 cwd_cache[0]='/';
4 while (1){
5 print_prompt();
6 memset(final_path,0,MAX_PATH_LEN);
7 memset(cmd_line,0,MAX_PATH_LEN);
8 readline(cmd_line,MAX_PATH_LEN);
9 if (cmd_line[0]==0){ // 只键入回车
10 continue;
11 }
12 argc=-1;
13 argc=cmd_parse(cmd_line,argv,' ');
14 if (argc==-1){
15 printf("num of arguments exceed %d\n",MAX_ARG_NR);
16 continue;
17 }
18 if (!strcmp("ls",argv[0])){
19 buildin_ls(argc,argv);
20 }else if (!strcmp("cd",argv[0])){
21 if (buildin_cd(argc,argv)!=NULL){
22 memset(cwd_cache,0,MAX_PATH_LEN);
23 strcpy(cwd_cache,final_path);
24 }
25 }else if (!strcmp("pwd",argv[0])){
26 buildin_pwd(argc,argv);
27 }else if (!strcmp("ps",argv[0])){
28 buildin_ps(argc,argv);
29 }else if (!strcmp("clear",argv[0])){
30 buildin_clear(argc,argv);
31 }else if (!strcmp("mkdir",argv[0])){
32 buildin_mkdir(argc,argv);
33 }else if (!strcmp("rmdir",argv[0])){
34 buildin_rmdir(argc,argv);
35 }else if (!strcmp("rm",argv[0])){
36 buildin_rm(argc,argv);
37 }else { // 如果是外部命令,需要从磁盘上加载
38 int32_t pid=fork();
39 if (pid){ // 父进程
40 int32_t status;
41 int32_t child_pid=wait(&status); // 子进程若没有执行exit(),则my_shell会被阻塞
42 if (child_pid==-1){
43 PANIC("my_shell: no child.\n");
44 }
45 printf("child_pid %d, it's status: %d\n",child_pid,status);
46 }else { // 子进程
47 make_clear_abs_path(argv[0],final_path);
48 argv[0]=final_path;
49 /* 先判断下文件是否存在 */
50 struct stat file_stat;
51 memset(&file_stat,0,sizeof(struct stat));
52 if (stat(argv[0],&file_stat)==-1){
53 printf("my_shell: cannot access %s: No such file or directory\n",argv[0]);
54 exit(-1);
55 }else {
56 execv(argv[0],argv);
57 }
58 }
59 }
60 int32_t arg_idx=0;
61 while (arg_idx<MAX_ARG_NR){
62 argv[arg_idx]=NULL;
63 ++arg_idx;
64 }
65 }
66 PANIC("my_shell: should not be here");
67 }
考虑到下面的cat需要连接到多个库文件,所以先修改makefile并make all后,再对command内进行修改。
⑨command/start.S:

1 [bits 32]
2 extern main
3 extern exit
4 section .text
5 global _start
6 _start:
7 ; 下面这两个要和execv中load之后的寄存器一致
8 push ebx ; 压入argv
9 push ecx ; 压入argc
10 call main
11
12 ; 将main的返回值通过栈传给exit,gcc用eax存储返回值(ABI规定)
13 push eax
14 call exit ; exit不会返回
⑩command/cat.c:

1 #include "syscall.h"
2 #include "stdio.h"
3 #include "string.h"
4 int main(int argc,char** argv){
5 if (argc>2 || argc==1){
6 printf("cat: only support 1 argument.\neg: cat filename\n");
7 exit(-2);
8 }
9 int buf_size=1024;
10 char abs_path[512]={0};
11 void* buf=malloc(buf_size);
12 if (buf==NULL){
13 printf("cat: malloc memory failed.\n");
14 return -1;
15 }
16 if (argv[1][0]!='/'){
17 getcwd(abs_path,512);
18 strcat(abs_path,"/");
19 strcat(abs_path,argv[1]);
20 }else {
21 strcpy(abs_path,argv[1]);
22 }
23 int fd=open(abs_path,O_RDONLY);
24 if (fd==-1){
25 printf("cat: open: open%s failed.\n",argv[1]);
26 return -1;
27 }
28 int read_bytes=0;
29 while (1){
30 read_bytes=read(fd,buf,buf_size);
31 if (read_bytes==-1){
32 break;
33 }
34 write(1,buf,read_bytes);
35 }
36 free(buf);
37 close(fd);
38 return 66;
39 }
⑪command/compile3:

1 BIN="cat"
2 CFLAGS="-Wall -m32 -fno-stack-protector -c -fno-builtin -W -Wstrict-prototypes \
3 -Wmissing-prototypes -Wsystem-headers"
4 #LIB= "../lib/ ../lib/kernel/ ../lib/user/ ../kernel/ ../device/ ../thread/ \
5 # ../userprog/ ../fs/ ../shell/"
6 OBJS="../build/string.o ../build/syscall.o \
7 ../build/stdio.o ../build/assert.o start.o"
8 DD_IN=$BIN
9 DD_OUT="/home/zbb/bochs/hd60M.img"
10
11 nasm -f elf ./start.S -o ./start.o
12 ar rcs simple_crt.a $OBJS start.o
13
14 gcc $CFLAGS -I "../lib/" -I "../lib/kernel/" -I "../lib/user/" -I "../kernel/" -I "../device/" -I "../thread/" -I "../userprog/" -I "../fs/" -I "../shell/" -o $BIN".o" $BIN".c"
15 ld -m elf_i386 $BIN".o" simple_crt.a -o $BIN
16 SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
17
18 if [ -f $BIN ];then
19 dd if=./$DD_IN of=$DD_OUT bs=512 \
20 count=$SEC_CNT seek=300 conv=notrunc
21 fi
此处因为编译器的原因,需要在CFLAGS中添加“-fno-stack-protector”,否则会报错。
然后获得:
因为prog_no_arg和prog_arg用不到了所以我就删掉了。
⑫最后,就是修改kernel/main.c:

1 #include "print.h"
2 #include "init.h"
3 #include "thread.h"
4 #include "interrupt.h"
5 #include "console.h"
6 #include "ioqueue.h"
7 #include "process.h"
8 #include "syscall.h"
9 #include "syscall-init.h"
10 #include "stdio.h"
11 #include "memory.h"
12 #include "fs.h"
13 #include "string.h"
14 #include "file.h"
15 #include "shell.h"
16 #include "debug.h"
17 #include "ide.h"
18 #include "stdio-kernel.h"
19
20 void init(void);
21
22 int main(void){
23 put_str("Welcome,\nI am kernel!\n");
24 init_all();
25 intr_enable();
26
27 uint32_t fd0=open("/file1",O_CREAT);
28 close(fd0);
29 fd0=open("/file1",O_RDWR);
30 printf("fd: %d\n",fd0);
31 write(fd0,"Hello world\n",12);
32 close(fd0);
33
34 uint32_t file_size=10440;
35 uint32_t sec_cnt=DIV_ROUND_UP(file_size,512);
36 struct disk* sda=&channels[0].devices[0];
37 void* prog_buf=sys_malloc(file_size);
38 ide_read(sda,300,prog_buf,sec_cnt);
39 int32_t fd=sys_open("/cat",O_CREAT|O_RDWR);
40 if (fd!=-1){
41 if (sys_write(fd,prog_buf,file_size)==-1){
42 printk("file write error!\n");
43 while (1);
44 }
45 }
46
47 // cls_screen();
48 console_put_str("[zbb@LAPTOP //]$ ");
49 thread_exit(running_thread(),true);
50 while(1);
51 return 0;
52 }
53
54 /* init进程 */
55 void init(void){
56 uint32_t ret_pid=fork();
57 if (ret_pid){ // 父进程
58 int status;
59 int child_pid;
60 /* init在此处不停地回收僵尸进程 */
61 while (1){
62 child_pid=wait(&status);
63 printf("I'm init, My pid is 1, I receive a child. It's pid is %d, status is %d\n",child_pid,status);
64 }
65 }else { // 子进程
66 my_shell();
67 }
68 PANIC("init: should not be here.");
69 }
和书中不同的是,因为此前file1文件已经被删除了,所以在main()中创建并写入“Hello world\n”。
就酱,因为上一步的失利,并不觉得能够成功。。。
结果——
——欸,竟然成功了,啊???我反而更困惑了。
7.(匿名)管道
①device/ioqueue.c,增加一个求数据长度的函数ioq_length():

1 /* 返回环形缓冲区中的数据长度 */
2 uint32_t ioq_length(struct ioqueue* ioq){
3 uint32_t len=0;
4 if (ioq->head>=ioq->tail){
5 len=ioq->head-ioq->tail;
6 }else {
7 len=bufsize-(ioq->tail-ioq->head);
8 }
9 return len;
10 }
②device/ioqueue.h:
uint32_t ioq_length(struct ioqueue* ioq);
③shell/pipe.c:

1 #include "pipe.h"
2 #include "memory.h"
3 #include "fs.h"
4 #include "file.h"
5 #include "ioqueue.h"
6 #include "thread.h"
7
8 /* 判断文件描述符local_fd是否是管道 */
9 bool is_pipe(uint32_t local_fd){
10 uint32_t global_fd=fd_local2global(local_fd);
11 return file_table[global_fd].fd_flag==PIPE_FLAG;
12 }
13
14 /* 创建管道,成功返回0,失败返回-1 */
15 int32_t sys_pipe(int32_t pipefd[2]){
16 int32_t global_fd=get_free_slot_in_global();
17
18 /* 申请一页内核内存作环形缓冲区 */
19 file_table[global_fd].fd_inode=get_kernel_pages(1);
20
21 /* 初始化环形缓冲区 */
22 ioqueue_init((struct ioqueue*)file_table[global_fd].fd_inode);
23 if (file_table[global_fd].fd_inode==NULL){
24 return -1;
25 }
26
27 /* 将fd_flag复用为管道标志 */
28 file_table[global_fd].fd_flag=PIPE_FLAG;
29
30 /* 将fd_pos复用为管道打开数 */
31 file_table[global_fd].fd_pos=2;
32 pipefd[0]=pcb_fd_install(global_fd);
33 pipefd[1]=pcb_fd_install(global_fd);
34 return 0;
35 }
36
37 /* 从管道中读数据 */
38 uint32_t pipe_read(int32_t fd,void* buf,uint32_t count){
39 char* buffer=buf;
40 uint32_t bytes_read=0;
41 uint32_t global_fd=fd_local2global(fd);
42
43 /* 获取管道的环形缓冲区 */
44 struct ioqueue* ioq=(struct ioqueue*)file_table[global_fd].fd_inode;
45
46 /* 选择较小的数据读取量,避免阻塞 */
47 uint32_t ioq_len=ioq_length(ioq);
48 uint32_t size=ioq_len>count?count:ioq_len;
49 while (bytes_read<size){
50 *buffer=ioq_getchar(ioq);
51 ++bytes_read;
52 ++buffer;
53 }
54 return bytes_read;
55 }
56
57 /* 往管道中写数据 */
58 uint32_t pipe_write(int32_t fd,const void* buf,uint32_t count){
59 uint32_t bytes_write=0;
60 uint32_t global_fd=fd_local2global(fd);
61 struct ioqueue* ioq=(struct ioqueue*)file_table[global_fd].fd_inode;
62
63 /* 选择较小的数据写入量,避免阻塞 */
64 uint32_t ioq_left=bufsize-ioq_length(ioq);
65 uint32_t size=ioq_left>count?count:ioq_left;
66
67 const char* buffer=buf;
68 while (bytes_write<size){
69 ioq_putchar(ioq,*buffer);
70 ++bytes_write;
71 ++buffer;
72 }
73 return bytes_write;
74 }
对了,记得在fs/fs.c中将fd_local2global()改为非static,否则会error找不到这个函数。
④shell/pipe.h:

1 #ifndef __SHELL_PIPE_H
2 #define __SHELL_PIPE_H
3 #include "stdint.h"
4 #include "global.h"
5
6 #define PIPE_FLAG 0XFFFF
7 bool is_pipe(uint32_t local_fd);
8 int32_t sys_pipe(int32_t pipefd[2]);
9 uint32_t pipe_read(int32_t fd,void* buf,uint32_t count);
10 uint32_t pipe_write(int32_t fd,const void* buf,uint32_t count);
11 #endif
⑤fs/fs.c,改写sys_close、sys_write和sys_read并添加头文件:

1 #include "pipe.h"
2
3 /* 关闭文件描述符fd指向的文件,成功返回0,否则-1 */
4 int32_t sys_close(int32_t fd){
5 int32_t ret=-1;
6 if (fd>2){
7 uint32_t global_fd=fd_local2global(fd);
8 if (is_pipe(fd)){
9 /* 如果此管道上的描述符都被关闭,释放管道的环形缓冲区 */
10 if (--file_table[global_fd].fd_pos==0){
11 mfree_page(PF_KERNEL,file_table[global_fd].fd_inode,1);
12 file_table[global_fd].fd_inode=NULL;
13 }
14 ret=0;
15 }else {
16 ret=file_close(&file_table[global_fd]);
17 }
18 running_thread()->fd_table[fd]=-1; // 使该文件描述符可用
19 }
20 return ret;
21 }
22
23 /* 将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败则返回-1 */
24 int32_t sys_write(int32_t fd,const void* buf,uint32_t count){
25 if (fd<0){
26 printk("sys_write: fd error\n");
27 return -1;
28 }
29 if (fd==stdout_no){
30 /* 标准输出有可能被重定向为管道缓冲区,因此要判断 */
31 if (is_pipe(fd)){
32 return pipe_write(fd,buf,count);
33 }else {
34 char tmp_buf[1024]={0};
35 memcpy(tmp_buf,buf,count);
36 console_put_str(tmp_buf);
37 return count;
38 }
39 }else if (is_pipe(fd)){ // 若是管道
40 return pipe_write(fd,buf,count);
41 }else {
42 uint32_t _fd=fd_local2global(fd);
43 struct file* wr_file=&file_table[_fd];
44 if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR){
45 uint32_t bytes_written=file_write(wr_file,buf,count);
46 return bytes_written;
47 }else {
48 console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
49 return -1;
50 }
51 }
52 }
53
54 /* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
55 int32_t sys_read(int32_t fd,void* buf,uint32_t count){
56 ASSERT(buf!=NULL);
57 int32_t ret=-1;
58 uint32_t global_fd=0;
59 if (fd<0 || fd==stdout_no || fd==stderr_no){
60 printk("sys_read: fd error\n");
61 }else if (fd==stdin_no){
62 /* 标准输入有可能被重定向为管道缓冲区,因此要判断 */
63 if (is_pipe(fd)){
64 ret=pipe_read(fd,buf,count);
65 }else {
66 char* buffer=buf;
67 uint32_t bytes_read=0;
68 while (bytes_read<count){
69 *buffer=ioq_getchar(&kbd_buf);
70 ++bytes_read;
71 ++buffer;
72 }
73 ret=(bytes_read==0?-1:(int32_t)bytes_read);
74 }
75 }else if (is_pipe(fd)){ // 若是管道就调用管道的方法
76 ret=pipe_read(fd,buf,count);
77 }else {
78 global_fd=fd_local2global(fd);
79 ret=file_read(&file_table[global_fd],buf,count);
80 }
81 return ret;
82 }
⑥userprog/fork.c,增加管道打开数:

1 /* 更新inode打开数 */
2 static void update_inode_open_cnts(struct task_struct* thread){
3 int32_t local_fd=3,global_fd=0;
4 while (local_fd<MAX_FILES_OPEN_PER_PROC){
5 global_fd=thread->fd_table[local_fd];
6 ASSERT(global_fd<MAX_FILE_OPEN);
7 if (global_fd!=-1){
8 if (is_pipe(local_fd)){
9 ++file_table[global_fd].fd_pos;
10 }else {
11 ++file_table[global_fd].fd_inode->i_open_cnts;
12 }
13 }
14 ++local_fd;
15 }
16 }
⑦userprog/wait_exit.c,修改release_prog_resource,并添加头文件:

1 #include "file.h"
2 #include "pipe.h"
3
4 /* 释放用户进程资源:
5 * 1.页表中对应的物理页
6 * 2.虚拟内存池占物理页框
7 * 3.关闭打开的文件 */
8 static void release_prog_resource(struct task_struct* release_thread){
9 uint32_t* pgdir_vaddr=release_thread->pgdir;
10 uint16_t user_pde_nr=768,pde_idx=0;
11 uint32_t pde=0;
12 uint32_t* v_pde_ptr=NULL; // v表示var,和函数pde_ptr区分
13
14 uint16_t user_pte_nr=1024,pte_idx=0;
15 uint32_t pte=0;
16 uint32_t* v_pte_ptr=NULL; // 和pte_ptr区分
17
18 uint32_t* first_pte_vaddr_in_pde=NULL; // 用于记录pde中第0个pte的地址
19 uint32_t pg_phy_addr=0;
20
21 /* 回收页表中用户空间的页框 */
22 while (pde_idx<user_pde_nr){
23 v_pde_ptr=pgdir_vaddr+pde_idx;
24 pde=*v_pde_ptr;
25 if (pde & 0x00000001){ // 如果页目录项p位为1,表示该目录项下可能有页表
26 first_pte_vaddr_in_pde=pte_ptr(pde_idx * 0x400000); // 一个页表为4M
27 pte_idx=0;
28 while (pte_idx<user_pte_nr){
29 v_pte_ptr=first_pte_vaddr_in_pde+pte_idx;
30 pte=*v_pte_ptr;
31 if (pte & 0x00000001){
32 /* 将pte中记录的物理页框直接在相应的内存池的位图中清0 */
33 pg_phy_addr=pte & 0xfffff000;
34 free_a_phy_page(pg_phy_addr);
35 }
36 ++pte_idx;
37 }
38 /* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
39 pg_phy_addr=pde & 0xfffff000;
40 free_a_phy_page(pg_phy_addr);
41 }
42 ++pde_idx;
43 }
44
45 /* 回收用户虚拟地址池所占的物理内存 */
46 uint32_t bitmap_pg_cnt=(release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len)/PG_SIZE;
47 uint8_t* user_vaddr_pool_bitmap=release_thread->userprog_vaddr.vaddr_bitmap.bits;
48 mfree_page(PF_KERNEL,user_vaddr_pool_bitmap,bitmap_pg_cnt);
49
50 /* 关闭进程打开的文件 */
51 uint8_t local_fd=3;
52 while (local_fd<MAX_FILES_OPEN_PER_PROC){
53 if (release_thread->fd_table[local_fd]!=-1){
54 if (is_pipe(local_fd)){
55 uint32_t global_fd=fd_local2global(local_fd);
56 if (--file_table[global_fd].fd_pos==0){
57 mfree_page(PF_KERNEL,file_table[global_fd].fd_inode,1);
58 file_table[global_fd].fd_inode=NULL;
59 }
60 }else {
61 sys_close(local_fd);
62 }
63 }
64 ++local_fd;
65 }
66 }
⑧lib/user/syscall.h+lib/user/syscall.c+userprog/syscall-init.c,不必贴出。
⑨command/prog_pipe.c,作为我们管道间进程通信的测试用例:

1 #include "stdio.h"
2 #include "syscall.h"
3 #include "string.h"
4 int main(int argc,char** argv){
5 int32_t fd[2]={-1};
6 pipe(fd);
7 int32_t pid=fork();
8 if (pid){ // 父进程
9 close(fd[0]); // 关闭输入
10 write(fd[1],"Hi, my son, I love u!",22);
11 printf("\nI'm father, my pid is %d\n",getpid());
12 return 8;
13 }else {
14 close(fd[1]); // 关闭输出
15 char buf[32]={0};
16 read(fd[0],buf,22);
17 printf("\nI'm child, my pid is %d\n",getpid());
18 printf("I'm child, my father said to me: \"%d\"\n",buf);
19 return 9;
20 }
21 }
⑩command/compile4.sh,和compile3.sh几乎一样:

1 BIN="prog_pipe"
2 CFLAGS="-Wall -m32 -fno-stack-protector -c -fno-builtin -W -Wstrict-prototypes \
3 -Wmissing-prototypes -Wsystem-headers"
4 #LIB= "../lib/ ../lib/kernel/ ../lib/user/ ../kernel/ ../device/ ../thread/ \
5 # ../userprog/ ../fs/ ../shell/"
6 OBJS="../build/string.o ../build/syscall.o \
7 ../build/stdio.o ../build/assert.o start.o"
8 DD_IN=$BIN
9 DD_OUT="/home/zbb/bochs/hd60M.img"
10
11 nasm -f elf ./start.S -o ./start.o
12 ar rcs simple_crt.a $OBJS start.o
13
14 gcc $CFLAGS -I "../lib/" -I "../lib/kernel/" -I "../lib/user/" -I "../kernel/" -I "../device/" -I "../thread/" -I "../userprog/" -I "../fs/" -I "../shell/" -o $BIN".o" $BIN".c"
15 ld -m elf_i386 $BIN".o" simple_crt.a -o $BIN
16 SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
17
18 if [ -f $BIN ];then
19 dd if=./$DD_IN of=$DD_OUT bs=512 \
20 count=$SEC_CNT seek=300 conv=notrunc
21 fi
结果如下:
幸好还是成功地输出了。
到了最后一节了,在shell中支持管道。
①shell/pipe.c,增加重定向函数:

1 /* 将文件描述符old_local_fd重定向为new_local_fd */
2 void sys_fd_redirect(uint32_t old_local_fd,uint32_t new_local_fd){
3 struct task_struct* cur=running_thread();
4 /* 针对恢复标准描述符 */
5 if (new_local_fd<3){
6 cur->fd_table[old_local_fd]=new_local_fd;
7 }else {
8 uint32_t new_global_fd=cur->fd_table[new_local_fd];
9 cur->fd_table[old_local_fd]=new_global_fd;
10 }
11 }
②shell/pipe.h,添加头文件:
void sys_fd_redirect(uint32_t old_local_fd,uint32_t new_local_fd);
③shell/shell.c,增加cmd_execute()并修改my_shell:

1 #include "shell.h"
2 #include "stdint.h"
3 #include "fs.h"
4 #include "file.h"
5 #include "syscall.h"
6 #include "stdio.h"
7 #include "global.h"
8 #include "string.h"
9 #include "debug.h"
10 #include "buildin_cmd.h"
11
12 #define MAX_ARG_NR 16 // 加上命令名外,最多支持15个参数
13
14 /* 存储输入的命令 */
15 static char cmd_line[MAX_PATH_LEN]={0};
16 char final_path[MAX_PATH_LEN]={0}; // 清除路径缓冲
17
18 /* 用来记录当前目录,是当前目录的缓存,每次执行cd命令时会更新此内容 */
19 char cwd_cache[MAX_PATH_LEN]={0};
20
21 /* 输出提示符 */
22 void print_prompt(void){
23 printf("[zbb@LAPTOP %s]$ ",cwd_cache);
24 }
25
26 /* 从键盘缓冲区中最多读入count个字节到buf */
27 static void readline(char* buf,int32_t count){
28 ASSERT(buf!=NULL && count>0);
29 char* pos=buf;
30
31 while (read(stdin_no,pos,1)!=-1 && (pos-buf)<count){ // 直至回车符停止
32 switch (*pos){
33 /* 找到回车或换行符后认为键入命令结束 */
34 case '\n':
35 case '\r':
36 *pos=0; // 添加cmd_line的终止字符0
37 putchar('\n');
38 return;
39
40 case '\b':
41 if (cmd_line[0]!='\b'){ // 到行首
42 --pos;
43 putchar('\b');
44 }
45 break;
46
47 /* Ctrl+l 清屏 */
48 case 'l'-'a':
49 /* 1.先将当前字符'l'-'a'置为0 */
50 *pos=0;
51 /* 2.再将屏幕清空 */
52 clear();
53 /* 3.打印提示符 */
54 print_prompt();
55 /* 4.将之前键入的内容再次打印 */
56 printf("%s",buf);
57 break;
58
59 /* Ctrl+u 清掉输入 */
60 case 'u'-'a':
61 while (buf!=pos){
62 putchar('\b');
63 *(pos--)=0;
64 }
65 break;
66
67 /* 非控制则输出字符 */
68 default:
69 putchar(*pos);
70 ++pos;
71 }
72 }
73 printf("readline: can't find enter_key in the cmd_line, max num of char is 128\n");
74 }
75
76 /* 分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组 */
77 static int32_t cmd_parse(char* cmd_str,char** argv,char token){
78 ASSERT(cmd_str!=NULL);
79 int32_t arg_idx=0;
80 while (arg_idx<MAX_ARG_NR){
81 argv[arg_idx]=NULL;
82 ++arg_idx;
83 }
84 char* next=cmd_str;
85 int32_t argc=0;
86 /* 外层循环处理整个命令行 */
87 while (*next){
88 /* 去除命令字或参数间空格 */
89 while (*next==token){
90 ++next;
91 }
92 /* 处理最后一个参数后接空格的情况 */
93 if (*next==0){
94 break;
95 }
96 argv[argc]=next;
97
98 /* 内层循环处理命令行中的每个命令字及参数 */
99 while (*next && *next!=token){ // 在字符串结束前找单词分隔符
100 ++next;
101 }
102
103 /* 如果未结束(是token字符),使token变成0 */
104 if (*next){
105 *next++=0;
106 }
107
108 /* argv数组越界则返回-1 */
109 if (argc>MAX_ARG_NR){
110 return -1;
111 }
112 ++argc;
113 }
114 return argc;
115 }
116
117 /* 执行命令 */
118 static void cmd_execute(uint32_t argc,char** argv){
119 if (!strcmp("ls",argv[0])){
120 buildin_ls(argc,argv);
121 }else if (!strcmp("cd",argv[0])){
122 if (buildin_cd(argc,argv)!=NULL){
123 memset(cwd_cache,0,MAX_PATH_LEN);
124 strcpy(cwd_cache,final_path);
125 }
126 }else if (!strcmp("pwd",argv[0])){
127 buildin_pwd(argc,argv);
128 }else if (!strcmp("ps",argv[0])){
129 buildin_ps(argc,argv);
130 }else if (!strcmp("clear",argv[0])){
131 buildin_clear(argc,argv);
132 }else if (!strcmp("mkdir",argv[0])){
133 buildin_mkdir(argc,argv);
134 }else if (!strcmp("rmdir",argv[0])){
135 buildin_rmdir(argc,argv);
136 }else if (!strcmp("rm",argv[0])){
137 buildin_rm(argc,argv);
138 }else if (!strcmp("help",argv[0])){
139 buildin_help(argc,argv);
140 }else { // 如果是外部命令,需要从磁盘上加载
141 int32_t pid=fork();
142 if (pid){ // 父进程
143 int32_t status;
144 int32_t child_pid=wait(&status); // 子进程若没有执行exit(),则my_shell会被阻塞
145 if (child_pid==-1){
146 PANIC("my_shell: no child.\n");
147 }
148 printf("child_pid %d, it's status: %d\n",child_pid,status);
149 }else { // 子进程
150 make_clear_abs_path(argv[0],final_path);
151 argv[0]=final_path;
152
153 /* 先判断下文件是否存在 */
154 struct stat file_stat;
155 memset(&file_stat,0,sizeof(struct stat));
156 if (stat(argv[0],&file_stat)==-1){
157 printf("my_shell: cannot access %s: No such file or directory\n",argv[0]);
158 exit(-1);
159 }else {
160 execv(argv[0],argv);
161 }
162 }
163 }
164 }
165
166 char* argv[MAX_ARG_NR]={NULL}; // argv必须为全局变量
167 int32_t argc=-1;
168
169 /* 简单的shell */
170 void my_shell(void){
171 cwd_cache[0]='/';
172 while (1){
173 print_prompt();
174 memset(final_path,0,MAX_PATH_LEN);
175 memset(cmd_line,0,MAX_PATH_LEN);
176 readline(cmd_line,MAX_PATH_LEN);
177 if (cmd_line[0]==0){ // 只键入回车
178 continue;
179 }
180
181 /* 针对管道的处理 */
182 char* pipe_symbol=strchr(cmd_line,'|');
183 if (pipe_symbol){
184 /* 支持多重管道操作,如cmd1|cmd2|...|cmdn,
185 * cmd1的标准输出和cmdn的标准输入需要单独处理 */
186
187 /* 1.生成管道 */
188 int32_t fd[2]={-1};
189 pipe(fd);
190 /* 将标准输出重定向到fd[1],使后续的输出信息重定向到内核环形缓冲区 */
191 fd_redirect(1,fd[1]);
192
193 /* 2.第一个命令 */
194 char* each_cmd=cmd_line;
195 pipe_symbol=strchr(each_cmd,'|');
196 *pipe_symbol=0;
197
198 /* 执行第一个命令,命令的输出会写道环形缓冲区 */
199 argc=-1;
200 argc=cmd_parse(each_cmd,argv,' ');
201 cmd_execute(argc,argv);
202
203 /* 跨过'|',处理下一个命令 */
204 each_cmd=pipe_symbol+1;
205
206 /* 将标准输入重定向到fd[0],使之指向内核环形缓冲区 */
207 fd_redirect(0,fd[0]);
208
209 /* 3.中间的命令,命令的输入和输出都指向内核环形缓冲区 */
210 while ((pipe_symbol=strchr(each_cmd,'|'))){
211 *pipe_symbol=0;
212 argc=-1;
213 argc=cmd_parse(each_cmd,argv,' ');
214 cmd_execute(argc,argv);
215 each_cmd=pipe_symbol+1;
216 }
217
218 /* 4.处理管道中最后一个命令 */
219 /* 将标准输出恢复到屏幕 */
220 fd_redirect(1,1);
221
222 /* 执行最后一个命令 */
223 argc=-1;
224 argc=cmd_parse(each_cmd,argv,' ');
225 cmd_execute(argc,argv);
226
227 /* 5.将标准输入恢复到键盘 */
228 fd_redirect(0,0);
229
230 /* 6.关闭管道 */
231 close(fd[0]);
232 close(fd[1]);
233 }else { // 一般无管道操作的命令
234 argc=-1;
235 argc=cmd_parse(cmd_line,argv,' ');
236 if (argc==-1){
237 printf("num of arguments exceed %d\n",MAX_ARG_NR);
238 continue;
239 }
240 cmd_execute(argc,argv);
241 }
242 }
243 PANIC("my_shell: should not be here");
244 }
④fs/fs.c,为了支持help命令:

1 /* 显示系统支持的内部命令 */
2 void sys_help(void){
3 printk("\
4 buildin commands:\n\
5 ls: show directory or file information\n\
6 cd: change current work directory\n\
7 mkdir: create a directory\n\
8 rmdir: remove a empty directory\n\
9 rm: remove a regular file\n\
10 pwd: show current work directory\n\
11 ps: show process information\n\
12 clear: clear screen\n\
13 shortcut key:\n\
14 ctrl+l: clear screen\n\
15 ctrl+u: clear input\n\n");
16 }
⑤fs/fs.h:
void sys_help(void);
⑥lib/user/syscall.c+lib/user/syscall.h+userprog/syscall-init.c,同样不再啰嗦。
⑦command/cat.c:

1 #include "syscall.h"
2 #include "stdio.h"
3 #include "string.h"
4 int main(int argc,char** argv){
5 if (argc>2){
6 printf("cat: argument error\n");
7 exit(-2);
8 }
9
10 if (argc==1){
11 char buf[512]={0};
12 read(0,buf,512);
13 printf("%s",buf);
14 exit(0);
15 }
16
17 int buf_size=1024;
18 char abs_path[512]={0};
19 void* buf=malloc(buf_size);
20 if (buf==NULL){
21 printf("cat: malloc memory failed.\n");
22 return -1;
23 }
24 if (argv[1][0]!='/'){
25 getcwd(abs_path,512);
26 strcat(abs_path,"/");
27 strcat(abs_path,argv[1]);
28 }else {
29 strcpy(abs_path,argv[1]);
30 }
31 int fd=open(abs_path,O_RDONLY);
32 if (fd==-1){
33 printf("cat: open: open%s failed.\n",argv[1]);
34 return -1;
35 }
36 int read_bytes=0;
37 while (1){
38 read_bytes=read(fd,buf,buf_size);
39 if (read_bytes==-1){
40 break;
41 }
42 write(1,buf,read_bytes);
43 }
44 free(buf);
45 close(fd);
46 return 66;
47 }
记得用compile3.sh进行编译链接。
⑧kernel/main.c,修改两句话:

1 #include "print.h"
2 #include "init.h"
3 #include "thread.h"
4 #include "interrupt.h"
5 #include "console.h"
6 #include "ioqueue.h"
7 #include "process.h"
8 #include "syscall.h"
9 #include "syscall-init.h"
10 #include "stdio.h"
11 #include "memory.h"
12 #include "fs.h"
13 #include "string.h"
14 #include "file.h"
15 #include "shell.h"
16 #include "debug.h"
17 #include "ide.h"
18 #include "stdio-kernel.h"
19
20 void init(void);
21
22 int main(void){
23 put_str("Welcome,\nI am kernel!\n");
24 init_all();
25 intr_enable();
26
27 uint32_t file_size=10488;
28 uint32_t sec_cnt=DIV_ROUND_UP(file_size,512);
29 struct disk* sda=&channels[0].devices[0];
30 void* prog_buf=sys_malloc(file_size);
31 ide_read(sda,300,prog_buf,sec_cnt);
32 int32_t fd=sys_open("/cat",O_CREAT|O_RDWR);
33 if (fd!=-1){
34 if (sys_write(fd,prog_buf,file_size)==-1){
35 printk("file write error!\n");
36 while (1);
37 }
38 }
39
40 // cls_screen();
41 console_put_str("[zbb@LAPTOP //]$ ");
42 thread_exit(running_thread(),true);
43 while(1);
44 return 0;
45 }
46
47 /* init进程 */
48 void init(void){
49 uint32_t ret_pid=fork();
50 if (ret_pid){ // 父进程
51 int status;
52 int child_pid;
53 /* init在此处不停地回收僵尸进程 */
54 while (1){
55 child_pid=wait(&status);
56 printf("I'm init, My pid is 1, I receive a child. It's pid is %d, status is %d\n",child_pid,status);
57 }
58 }else { // 子进程
59 my_shell();
60 }
61 PANIC("init: should not be here.");
62 }
一定要在hd80M.img中先把此前的cat文件rm掉,再bin/bochs -f bochsrc.disk哈。
最后执行:
很明显,'|'命令是可行的,但是其实有个小问题:如果要在屏幕中打印比较多的字符,程序就会悬停(如两幅图的最后一个命令),我猜应该是管道的缓冲区满了。
本章实现了三个重要的系统调用——exec、wait和exit,让我们来看看吧:
我觉得有必要先说明一下fork、exec、wait和exit的使用:
在本项目中,我们在init程序中fork()出子进程。此时,对于父进程,它的任务就是wait()等待子进程的exit(),因为init作为父进程,所以此时的回收的子进程都被视作僵尸进程。对于子进程,其任务就是my_shell()创建shell,然后再exec()执行一系列操作。
而一般的,是在父进程中fork()出子进程。此时,对于父进程,wait()等待子进程exit(),如果父进程没有wait(),则子进程会沦为“僵尸进程”,内核无法回收它的pcb;如果父进程先于子进程exit(),则子进程沦为“孤儿进程”,由init进程收养之。对于子进程,则会直接exec()去执行相应的程序,最后exit()。我们站在子进程的视角看,fork()与exec()通常是先后调用的,至于为什么不将二者合二为一,这就又是历史包袱的问题了。
对于exec——sys_exec():
1.根据文件路径获取程序的入口地址entry_point——load(path):
每个可执行文件的都有一个ELF头elf_header,elf_header中的e_entry属性即为程序的入口地址。同时,将每个可加载段segment_load()加载到内存。
2.获得当前进程的PCB后,将中断栈的eip=entry_point,esp=0xc0000000(用户空间最高处)。因为exec()后,程序会直接去执行相应的程序,所以要"jmp intr_exit"直接从中断返回,获得上下文,开始执行相应程序。
对于wait——sys_wait()——等待子进程调用exit(),一旦有一个子进程调用exit(),就返回子进程pid:
1.获得当前进程的PCB。
2.对于当前进程的子进程:
若已经有挂起状态(TASK_HANGING),则回收该子进程——thread_exit(),回收它的pgdir、PCB、从list中移除——并返回pid。
若没有挂起的进程,则挂起当前进程(其实就是thread_block()它),等有子进程exit()了再唤醒。
对于exit——sys_exit()——进程结束自己时调用:
1.若子进程还有子进程,则将孙子进程过继给init——init_adopt_a_child()——将parent_pid属性改为1即可(因为init的pid=1)。
2.释放子进程的资源——release_prog_resource()——释放资源如下:
(1)页表中对应的物理页,这与上方的回收pgdir可不同,pgdir是进程的页目录,这里是页表和物理页框,页寻址有三级结构呢。
(2)虚拟内存池占用的物理页框。
(3)关闭打开的文件。
3.如果有父进程正在wait()等待子进程exit(),则thread_unblock()唤醒父进程。因为父进程不会立刻执行,因此将自己挂起,等待父进程回收自己。
注意到,wait和exit都会释放进程资源,不过我觉得它们有两点不同:①wait是父进程释放子进程的资源,exit是进程自己释放自己的资源 ②wait释放的资源是进程的核心——PCB;exit释放的是除PCB外的其它进程资源。
参考博客:
- OS_lab/code/c15 at master · fuujiro/OS_lab (github.com)
- 《操作系统真象还原》第十五章 ---- 实现系统交互 操作系统最终章 四十五天的不易与坚持终完结撒花(中)_Love 6的博客-CSDN博客
- 操作系统真象还原实验记录之实验三十二:加载用户进程-CSDN博客
- 操作系统真象还原实验记录之实验三十三:实现系统调用wait和exit_mxy990811的博客-CSDN博客
- 操作系统真象还原实验记录之实验三十四:实现管道_判断管道是否实现_mxy990811的博客-CSDN博客
结束语:
好吧,关于最后的话语我还是放在最后一篇博客(至今没有)吧,现在是周一下午,本来说是要在上一周结束掉的,但是这一章是在bug太多了,感觉dubug的时间要远多于学习和coding的时间,而且周上末还和室友出去嗨皮了一下,遂至今日完成,over!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库