虽然文件系统这部分还有许多可以添加的,比如添加个查找文件或者给文件改个名字等功能,但这些已经是手到擒来的事情,等到之后有空再说。现在进入到了文件管理部分了,先模仿linux的fork系统调用,来实现自己的Fork,这也是实现自己的SHELL的第一步。
一个进程需要一个PCB结构,一个GDT中的描述符来描述此进程的LDT,以及代码,数据和堆栈所占用的内存。所需要的内存我们只能在fork的时候才能分配,在这里先把静态的部分先搞定。记得在Init_PCB函数中我们只初始化了编译前就确定的进程的PCB,在这里要把整个系统最多支持的PCB先初始化一遍。
首先先在PCB结构定义中追加两个字段,以方便管理:
include/proc.h
- int available; /* 此PCB是否可用 */
- int parent_pid; /* 此PCB所属进程的父进程的pid */
再修改Init_PCB函数:
kernel/proc.c
- /*----------------------------------------------------------------------Init_PCB
- 初始化与PCB的相关东东
- */
- void Init_PCB()
- {
- /* 当前进程个数赋值 */
- d_Cur_Proc_Num = 9;
- PCB *p_Cur_PCB = PCB_Table; /* 指向第一个PCB */
- Proc_Unique *p_Cur_Proc_Unique = Proc_Unique_Table; /* 指向第一个Proc_Unique */
- u32 Proc_Stack_Top = (u32)All_Proc_Stack_Space; /* 定位各个进程的栈顶 */
- u32 Selector_First_LDT_Index = SELECTOR_FIRST_LDT; /* 第一个进程的LDT在GDT的选择子 */
- /* 填充各个进程的PCB */
- int rpl;
- int dpl;
- int eflags;
- int i;
- for(i = 0;i < MAX_PROC;i++)
- {
- /*
- 如果是系统进程,RPL为1,LDT中的DPL为1,标志寄存器为0x1202
- 如果是用户进程,RPL为3,LDT中的DPL为3,标志寄存器为0x3202
- 以上除去GS,GS始终指向GDT中的视频段,RPL为3
- ss,ds,es,fs指向当前进程的LDT的FLAT_RW段
- gs指向GDT中的视频段,RPL为1
- cs指向当前进程的LDT的FLAT_C段
- */
- if(Is_System_Proc[i] == 1)
- {
- rpl = SA_RPL1;
- dpl = DA_DPL1;
- eflags = 0x1202;
- }
- else
- {
- rpl = SA_RPL3;
- dpl = DA_DPL3;
- eflags = 0x3202;
- }
- if(i < d_Cur_Proc_Num)
- {
- p_Cur_PCB->available = 0; /* 此PCB不可用 */
- /* 填充各段寄存器 */
- p_Cur_PCB->stack_frame.fs = SELECTOR_LDT_FLAT_RW + rpl + SA_TIL;
- p_Cur_PCB->stack_frame.gs = SELECTOR_VIDEO + SA_RPL3;
- p_Cur_PCB->stack_frame.es = SELECTOR_LDT_FLAT_RW + rpl + SA_TIL;
- p_Cur_PCB->stack_frame.ds = SELECTOR_LDT_FLAT_RW + rpl + SA_TIL;
- p_Cur_PCB->stack_frame.ss = SELECTOR_LDT_FLAT_RW + rpl + SA_TIL;
- p_Cur_PCB->stack_frame.cs = SELECTOR_LDT_FLAT_C + rpl + SA_TIL;
- Str_Cpy(p_Cur_PCB->proc_name,p_Cur_Proc_Unique->proc_name); /* 进程名拷贝 */
- /* esp指进程栈的栈顶 */
- p_Cur_PCB->stack_frame.esp = Proc_Stack_Top + p_Cur_Proc_Unique->proc_stack_size;
- /* 为下一次赋值做准备 */
- Proc_Stack_Top += p_Cur_Proc_Unique->proc_stack_size;
- /* eip指向进程体 */
- p_Cur_PCB->stack_frame.eip = (u32)p_Cur_Proc_Unique->proc_exec_addr;
- /* eflags赋值 */
- p_Cur_PCB->stack_frame.eflags = eflags;
- /*
- 复制GDT的FLAT_RW和FLAT_C段描述符到当前进程的LDT中
- 并把LDT的这两个描述符的DPL设为1
- */
- Memory_Copy(&p_Cur_PCB->LDT[0],&GDT[1],sizeof(Descriptor));
- p_Cur_PCB->LDT[0].attr1 |= dpl;
- Memory_Copy(&p_Cur_PCB->LDT[1],&GDT[2],sizeof(Descriptor));
- p_Cur_PCB->LDT[1].attr1 |= dpl;
- /* 指向下一个进程的Proc_Unique */
- p_Cur_Proc_Unique++;
- }
- else
- {
- p_Cur_PCB->available = 1; /* 此PCB可用 */
- PCB_Table[i].ticks = PCB_Table[i].priority = 0; /* 两者为0不能调度 */
- }
- p_Cur_PCB->proc_id = i; /* 进程号赋值 */
- /* 当前进程的LDT在GDT的选择子赋值 */
- p_Cur_PCB->LDT_Selector = Selector_First_LDT_Index;
- /* 填充此进程的LDT在GDT中的描述符 */
- Fill_Desc(Selector_First_LDT_Index / 8,(u32)p_Cur_PCB->LDT,sizeof(Descriptor) * 2 - 1,DA_LDT);
- /* IPC有关的字段赋值 */
- p_Cur_PCB->ipc_status = NO_BLOCK;
- p_Cur_PCB->send_to = NO_PROC;
- p_Cur_PCB->receive_from = NO_PROC;
- p_Cur_PCB->has_int_msg = 0;
- p_Cur_PCB->p_message = 0;
- p_Cur_PCB->sending_queue_first = 0;
- p_Cur_PCB->sending_queue_next = 0;
- /* FD的指针数组初始化为0 */
- int j;
- for(j = 0;j < MAX_FILE_PER_PROC;j++)
- {
- p_Cur_PCB->fd_ptr_table[j] = 0;
- }
- p_Cur_PCB->parent_pid = -1; /* 没有父进程 */
- /* 为下一次赋值做准备 */
- Selector_First_LDT_Index += 8;
- /* 指向下一个进程的PCB */
- p_Cur_PCB++;
- }
- /* 所有进程的ticks和priority的初始化 */
- PCB_Table[0].ticks = PCB_Table[0].priority = 30;
- PCB_Table[1].ticks = PCB_Table[1].priority = 25;
- PCB_Table[2].ticks = PCB_Table[2].priority = 20;
- PCB_Table[3].ticks = PCB_Table[3].priority = 300;
- PCB_Table[4].ticks = PCB_Table[4].priority = 300;
- PCB_Table[5].ticks = PCB_Table[5].priority = 300;
- PCB_Table[6].ticks = PCB_Table[6].priority = 300;
- PCB_Table[7].ticks = PCB_Table[7].priority = 300;
- PCB_Table[8].ticks = PCB_Table[8].priority = 300;
- /* 设定各用户进程所绑定的TTY */
- PCB_Table[0].bind_tty = 0;
- PCB_Table[1].bind_tty = 0;
- PCB_Table[2].bind_tty = 2;
- PCB_Table[3].bind_tty = 0;
- PCB_Table[4].bind_tty = 0;
- PCB_Table[5].bind_tty = 0;
- PCB_Table[6].bind_tty = 0;
- PCB_Table[7].bind_tty = 0;
- PCB_Table[8].bind_tty = 0;
- /* 初值赋0 */
- d_Flag_Reenter = 0;
- /* 给p_Next_PCB赋值,指向一个进程的PCB */
- p_Next_PCB = PCB_Table + 0;
- }
以后就根据available来确定空闲的PCB,来分配个各子进程。
OK,下一步就是动态的添加一个子进程的工作了。先添加一个文件kernel/mm.c,来存放管理内存的代码。
首先是用户接口函数:
kernel/mm.c
- /*--------------------------------------------------------------------------Fork
- Fork函数的用户接口
- 调用失败返回-1,不生成子进程
- 成功对父进程返回子进程号,对子进程返回0
- */
- int Fork()
- {
- Message fork_msg;
- fork_msg.msg_type = MM_FORK;
- Send_Receive_Shell(BOTH,PROC_MM_PID,&fork_msg);
- return fork_msg.r1;
- }
我们又专门建立了一个进程专门处理内存管理的消息,执行体如下:
kernel/mm.c
- /*-----------------------------------------------------------------------Proc_MM
- 内存管理进程执行体
- */
- void Proc_MM()
- {
- Init_MM();
- Message mm_msg;
- while(1)
- {
- Send_Receive_Shell(RECEIVE,ANY,&mm_msg);
- switch(mm_msg.msg_type)
- {
- case MM_FORK:
- mm_msg.r1 = Do_Fork(&mm_msg);
- break;
- default:
- Panic("UNKNOWN MSG TYPE!/n");
- break;
- }
- Send_Receive_Shell(SEND,mm_msg.src_proc_pid,&mm_msg);
- }
- }
相关宏定义略过,添加一个进程的工作也不用赘述。
我们看到此进程在执行前先执行了一个初始化函数Init_MM,来看看:
kernel/mm.c
- /*-----------------------------------------------------------------------Init_MM
- 初始化MM
- */
- static void Init_MM()
- {
- memory_size = *(u32*)0x500; /* 取得内存大小 */
- free_memory_size = memory_size - CHILD_START_ADDR; /* 剩余内存大小 */
- block_memory_size = 1024 * 1024; /* 每个子进程的大小 */
- Printf("Memory Size:%dMB/n",memory_size / (1024 * 1024));
- }
先从内存的0x500处得到了内存大小,我们知道在loader执行时打印出来了内存信息,我们就在loader进入内核前把内存大小存入了内存0x500处中,再这里就取了出来,干嘛要得到内存大小咧,肯定是为了管理内存咯。。先修改loader:
boot/loader.asm
- ;把内存字节数送入内存500h处,供内核读取
- mov eax,[d_Memory_Size]
- mov dword [500h],eax
- ;********************************************************************
- jmp Selector_Flat_C:Kernel_Entry_Point_Phy_Addr ;****正式进入内核****
- ;********************************************************************
下一条语句是取得可用内存的剩余大小,我们要从内存的CHILD_START_ADDR开始存放子进程的内存,也就是从10MB开始,再下一条是确定一个子进程占用的内存大小,我们确定为1M,虽然有些浪费,但为了简便,也先只能如此了。最后打印内存大小。
用到的宏如下:
include/proc.h
- #define CHILD_START_ADDR 10 * 1024 * 1024 /* 第一个子进程的起始地址 */
下一步就是关键的Do_Fork函数了:
kernel/mm.c
- /*-----------------------------------------------------------------------Do_Fork
- Fork函数功能函数
- */
- static int Do_Fork(Message *fork_msg)
- {
- int parent_pid = fork_msg->src_proc_pid; /* 父进程的进程号 */
- /* 找一个可用的PCB,child_pid为进程号 */
- int child_pid;
- for(child_pid = 0;child_pid < MAX_PROC;child_pid++)
- {
- if(PCB_Table[child_pid].available == 1)
- {
- break;
- }
- }
- /* 检测内存是否足够 */
- if(free_memory_size < block_memory_size)
- {
- return -1; /* 不够返回-1 */
- }
- else
- {
- free_memory_size -= block_memory_size; /* 空闲内存减去一个块大小 */
- }
- Disable_Int(); /* 关中断,保持原子性 */
- u16 ldt_sel = PCB_Table[child_pid].LDT_Selector; /* 把正确的LDT选择子拿出 */
- /* 把父进程的PCB复制到子进程 */
- Memory_Copy(&PCB_Table[child_pid],&PCB_Table[parent_pid],sizeof(PCB));
- PCB_Table[child_pid].LDT_Selector = ldt_sel; /* LDT选择子赋回 */
- PCB_Table[child_pid].parent_pid = parent_pid; /* 设置父进程号 */
- Is_System_Proc[child_pid] = Is_System_Proc[parent_pid]; /* 设置为父进程的值 */
- Sprintf(PCB_Table[child_pid].proc_name,"child:%d",child_pid); /* 设置子进程名 */
- u32 copy_src;
- u32 copy_dest;
- Descriptor *s,*dd,*dt;
- u32 seg_limit;
- /* 如果是从非子进程fork子进程,则复制0-1M的内存 */
- if(parent_pid < KERNEL_PROC_NUM)
- {
- copy_src = 0;
- }
- /* 否则从作为父进程的子进程的LDT的数据段起始处开始复制 */
- else
- {
- s = &(PCB_Table[parent_pid].LDT[SELECTOR_LDT_FLAT_RW]);
- copy_src = (s->base_high << 24) | (s->base_mid << 16) | s->base_low;
- }
- /* 定位fork出的新的子进程占据的内存首地址 */
- copy_dest = CHILD_START_ADDR + (child_pid - KERNEL_PROC_NUM) * block_memory_size;
- /* 设置新的子进程的LDT的2个描述符的首地址和段界限 */
- dd = &PCB_Table[child_pid].LDT[SELECTOR_LDT_FLAT_RW >> 8];
- dd->base_low = copy_dest & 0xffff;
- dd->base_mid = (copy_dest >> 16) & 0xff;
- dd->base_high = (copy_dest >> 24) & 0xff;
- seg_limit = (dd->limit_high_attr2 & DA_LIMIT_4K) ?
- (block_memory_size / 4096 - 1) : block_memory_size - 1;
- dd->limit_low = seg_limit & 0xffff;
- dd->limit_high_attr2 &= 0xf0;
- dd->limit_high_attr2 |= ((seg_limit >> 16) & 0xf);
- dt = &PCB_Table[child_pid].LDT[SELECTOR_LDT_FLAT_C >> 8];
- dt->base_low = copy_dest & 0xffff;
- dt->base_mid = (copy_dest >> 16) & 0xff;
- dt->base_high = (copy_dest >> 24) & 0xff;
- seg_limit = (dt->limit_high_attr2 & DA_LIMIT_4K) ?
- (block_memory_size / 4096 - 1): block_memory_size - 1;
- dt->limit_low = seg_limit & 0xffff;
- dt->limit_high_attr2 &= 0xf0;
- dt->limit_high_attr2 |= ((seg_limit >> 16) & 0xf);
- /* 复制1M内存到新的子进程分配好的地址处 */
- Memory_Copy((void*)copy_dest,(void*)copy_src,block_memory_size);
- Enable_Int(); /* 开中断,此时新的子进程有机会被调度 */
- d_Cur_Proc_Num++; /* 总进程数自增1 */
- /* 此时如果子进程被调度,就RECEIVE消息阻塞住,发个消息给它,返回0表示它是子进程 */
- Message child_msg;
- child_msg.r1 = 0;
- Send_Receive_Shell(SEND,child_pid,&child_msg);
- Printf("FREE SIZE:%dMB/n",free_memory_size / (1024 * 1024));
- return child_pid; /* 返回给父进程的子进程号 */
- }
用到的宏如下:
include/proc.h
- #define KERNEL_PROC_NUM 9 /* 在内核中的非子进程的数量 */
其中用到了一个新的函数Sprintf:
lib/lib_kernel_in_c.c
- /*-----------------------------------------------------------------------Sprintf
- 把格式字符串的内容写到缓冲区中
- */
- int Sprintf(char *dest,const char *fmt,...)
- {
- char buf[128]; /* 解析好的串存放之地 */
- /* 指向格式字符串的后一个参数 */
- char *args_begin = (char*)((char*)(&fmt) + 4);
- /* 解析格式字符串 */
- int len = V_Printf(buf,fmt,args_begin);
- Memory_Copy(dest,buf,len); /* 把解析好的串写到缓冲区中 */
- return len; /* 返回串的长度 */
- }
这样似乎就大功告成了,起始不然,记得在第7章的时候原书出现了一个va2la的函数,当然也没在意。在抓狂了1天多之后终于发现了这个问题。举个例子,在新fork出的子进程中如果要发个消息给某个进程,所定义的Message是分配在栈上的,那么内存地址肯定在10M开外了,在该子进程访问这个Message时肯定没问题,线性地址 = 虚拟地址 + LDT中数据段的首地址能正确找到它。但要把消息复制给别的进程时,此时控制权是掌控在内核态的,而内核态使用的是0-4G的扁平地址,所以就不能正确的找到要发送的Message了。在使用了消息实现的各功能函数如Milli_Delay时就会发生莫名其妙的错误。调用Printf时情况也一样。要修正这个错误很简单,首先添加一个转换函数:
lib/lib_kernel_in_c.c
- /*----------------------------------------------------Virtual_Addr_2_Linear_Addr
- 虚拟地址得到线性地址
- */
- void *Virtual_Addr_2_Linear_Addr(u32 proc_id,void *v_addr)
- {
- /* 取得指定进程号的LDT的数据段描述符 */
- Descriptor *d = &(PCB_Table[proc_id].LDT[SELECTOR_LDT_FLAT_RW >> 3]);
- /* 取出基地址 */
- u32 base = (d->base_high << 24) | (d->base_mid << 16) | (d->base_low);
- /* 加上虚拟地址得到线性地址 */
- u32 la = base + (u32)v_addr;
- return (void*)la;
- }
先修改System_Call_Write函数:
kernel/system_call.c
- /*-------------------------------------------------------------System_Call_Write
- RINT 0,系统调用1号功能函数
- */
- void System_Call_Write(int unused1,int unused2,const char *buf,PCB *pcb)
- {
- /* 把调用Printf函数的进程绑定的控制台的指针取出来 */
- Console *con = &Console_Table[pcb->bind_tty];
- /* 取出解析好的缓冲区的线性地址 */
- char * linear_addr = Virtual_Addr_2_Linear_Addr(PCB_2_PID(pcb),buf);
- /* 如果是Panic调用的或者是Assert调用的并且是系统进程调用的 */
- if((*linear_addr == MAGIC_CHAR_PANIC) || (*linear_addr == MAGIC_CHAR_ASSERT && Is_System_Proc[PCB_2_PID(pcb)] == 1))
- {
- Disable_Int(); /* 为了能hlt住系统,必须关掉中断 */
- char *v = (char*)VIDEO_START_ADDR; /* 指向显存首地址 */
- char *m = (char*)linear_addr + 1; /* 略掉标记字符 */
- /* 不超过显存则继续 */
- while((u32)v < VIDEO_START_ADDR + VIDEO_MEM_SIZE)
- {
- /* 如果串没结束,则打印之 */
- if(*m != '/0')
- {
- *v++ = *m++;
- *v++ = Make_Color(GREEN,BLACK);
- }
- else
- {
- /* 结束了的话则把剩下的空间填为空,这样则使每隔10行打印一次 */
- while((((u32)v - VIDEO_START_ADDR) % (ROW_BYTE_NUM * 10)) != 0)
- {
- *v++ = ' ';
- *v++ = Make_Color(WHITE,BLACK);
- }
- /* m重新指串的第2个字符 */
- m = (char*)linear_addr + 1;
- }
- }
- __asm__ __volatile__("hlt"); /* 叫停系统 */
- }
- /* 如果在用户进程调用Assert,则略过标志字符 */
- if(*linear_addr == MAGIC_CHAR_ASSERT)
- {
- linear_addr++;
- }
- /* 取出解析好的串的每一个字符,交给Out_Char打印 */
- while(*linear_addr != '/0')
- {
- Out_Char(con,*linear_addr++);
- }
- }
很简单,在kernel/ipc.c中的Msg_Send和Msg_Receive函数中复制Message的地方也转换一下,这里也不赘述。
OK,来看看Fork能不能正常工作,在进程A中先fork一下,再在子进程中Fork以下,生成一个子子进程。在父进程,子进程,子子进程中分别打印P,C,G:
kernel/proc.c
- /*------------------------------------------------------------------------Proc_A
- 进程A的执行体
- */
- void Proc_A()
- {
- int pid = Fork();
- if(pid > 0)
- {
- Printf("parent is running!/n");
- while(1)
- {
- Printf("P ");
- Milli_Delay(200);
- }
- }
- else if(fd == 0)
- {
- Printf("child is running!/n");
- int pid1 = Fork();
- if(pid1 > 0)
- {
- while(1)
- {
- Printf("C ");
- Milli_Delay(200);
- }
- }
- else if(pid1 == 0)
- {
- Printf("grand child is running!/n");
- while(1)
- {
- Printf("G ");
- Milli_Delay(200);
- }
- }
- else
- {
- /* fork fail */
- while(1);
- }
- }
- else
- {
- /* fork fail */
- while(1);
- }
- }
跟上学期做的操作系统实验几乎是一样的吧。。
make,bochs,运行,结果如下:
中间的输出受到了FS进程的输入些许干扰,不过还是可以看出来的。
这里还遗留了一个问题,就是读写文件等与文件系统相关的操作时还没转化地址,还有文件共享问题。这个留到下篇。。