《一个操作系统的实现》总结3——系统任务和用户进程

编程入门 行业动态 更新时间:2024-10-28 19:22:55

《一个操作系统的实现》总结3——系统任务和用户<a href=https://www.elefans.com/category/jswz/34/1771450.html style=进程"/>

《一个操作系统的实现》总结3——系统任务和用户进程

  三、运行中的系统任务和用户进程

 首先,进程之间最重要的是进程通信,先来分析一下通信是如何实现的。

 进程要与其它进程通信均是调用send_recv函数,那我们就来看一下这个函数,节自kernel/proc.c:

/***send_recv调用了sendrec(syscall.asm),sendrec用int	INT_VECTOR_SYS_CALL进行系统调用陷入内核,这时到了sys_call(kernel.asm)中执行,在sys_call中又通过sys_call_table(global.c)初始化的表项进入sys_sendrec(位于本文件)执行,它分别调用msg_send(位于本文件)和msg_receive(位于本文件)来进行进程间消息的传递***/
PUBLIC int send_recv(int function, int src_dest, MESSAGE* msg)
{int ret = 0;if (function == RECEIVE)memset(msg, 0, sizeof(MESSAGE));switch (function) {case BOTH:ret = sendrec(SEND, src_dest, msg);if (ret == 0)ret = sendrec(RECEIVE, src_dest, msg);break;case SEND:case RECEIVE:ret = sendrec(function, src_dest, msg);break;default:assert((function == BOTH) ||(function == SEND) || (function == RECEIVE));break;}return ret;
}

  中间涉及到的处理流程在注释中都已说明,就不再详细地贴出每个函数分析了,有兴趣自己看书吧。

 

 

 接下来,依次分析各个系统任务和用户进程,用户进程很简单,而且不属于操作系统实现,因此重点放在系统任务上。

 先来看看系统任务和用户进程都有哪些,节自kernel/global.c:

PUBLIC	struct task	task_table[NR_TASKS] = {/* entry        stack size        task name *//* -----        ----------        --------- */{task_tty,      STACK_SIZE_TTY,   "TTY"       },{task_sys,      STACK_SIZE_SYS,   "SYS"       },{task_hd,       STACK_SIZE_HD,    "HD"        },{task_fs,       STACK_SIZE_FS,    "FS"        },{task_mm,       STACK_SIZE_MM,    "MM"        }};PUBLIC	struct task	user_proc_table[NR_NATIVE_PROCS] = {/* entry    stack size     proc name *//* -----    ----------     --------- */{Init,   STACK_SIZE_INIT,  "INIT" },{TestA,  STACK_SIZE_TESTA, "TestA"},{TestB,  STACK_SIZE_TESTB, "TestB"},{TestC,  STACK_SIZE_TESTC, "TestC"}};



1、task_tty

 操作系统将tty作为特殊的文件对待,从tty读入和输出到tty由该系统任务进行处理。它负责从键盘缓冲区中拿数据送到请求进程(通过文件系统task_fs中介),或从请求进程拿数据送到显存显示(通过文件系统task_fs)。

节自kernel/tty.c:

/***处理对tty的输入输出***/
PUBLIC void task_tty()
{TTY *	tty;MESSAGE msg;init_keyboard();for (tty = TTY_FIRST; tty < TTY_END; tty++)init_tty(tty);select_console(0);while (1) {for (tty = TTY_FIRST; tty < TTY_END; tty++) {do {tty_dev_read(tty);tty_dev_write(tty);} while (tty->ibuf_cnt);}send_recv(RECEIVE, ANY, &msg);int src = msg.source;assert(src != TASK_TTY);TTY* ptty = &tty_table[msg.DEVICE];switch (msg.type) {case DEV_OPEN:reset_msg(&msg);msg.type = SYSCALL_RET;send_recv(SEND, src, &msg);break;
/***直接给tty发送read和write消息的只有文件系统,因此msg->source只能是文件系统,其它的进程想要读写tty是通过文件系统这个中介来发送消息的,实际需要读和写的进程是msg->PROC_NR指定的进程。注意这点,跟一般的进程间通信是有区别的。***/case DEV_READ:
/***tty_do_read会根据msg设置tty的参数,tty_req_buf、tty_trans_cnt等,然后进入下次循环的开始将会在for循环中处理该msg对应的进程。在该函数完成时会将msg中PROC_NR对应的进程阻塞,进行下一次循环中执行tty_dev_write中,会将缓冲区中的指令逐字节复制到PROC_NR对应的进程的缓冲区中,完成一条指令的输出,即遇到\n时,会给PROC_NR对应的被阻塞的进程发送继续执行的msg。***/tty_do_read(ptty, &msg);break;case DEV_WRITE:
/***将进程缓冲区中的数据写出到显存,然后返回***/tty_do_write(ptty, &msg);break;case HARD_INT:
/***该信号是为了周期性地唤醒tty任务,避免它阻塞在前面的send_recv中。如果一个进程要求从tty输入,而人的输入间隔很大,这就造成一条命令没有一次性输入完,即没有读到\n缓冲区的内容就读完了,这是将会造成tty_dev_write退出,而进入下一次循环,如果没有周期性唤醒,那么在下一次循环中将会卡在send_recv中,因为进程只会发送一次要求从tty读取命令。***/key_pressed = 0;continue;default:dump_msg("TTY::unknown msg", &msg);break;}}
}



2、task_sys

 处理get_ticks和get_pid的请求,其中get_ticks是获取系统自开始运行已经经过的ticks数,get_pid是返回请求进程的pid号。

节自kernel/systask.c:

PUBLIC void task_sys()
{MESSAGE msg;while (1) {send_recv(RECEIVE, ANY, &msg);int src = msg.source;switch (msg.type) {case GET_TICKS:msg.RETVAL = ticks;send_recv(SEND, src, &msg);break;case GET_PID:msg.type = SYSCALL_RET;msg.PID = src;send_recv(SEND, src, &msg);break;default:panic("unknown msg type");break;}}
}



3、task_hd

 除了特殊文件设备tty外,所有的文件都是在硬盘上的普通文件,对文件处理需要对硬盘进行读写。相关的读写都是通过向该系统任务发送消息实现的。

 具体实现涉及硬盘的分区、端口操作等,不作具体解释。有兴趣的参见第9章。

节自kernel/hd.c:

PUBLIC void task_hd()
{MESSAGE msg;init_hd();while (1) {send_recv(RECEIVE, ANY, &msg);int src = msg.source;switch (msg.type) {case DEV_OPEN:
/***初始化硬盘信息,包括分区信息等***/hd_open(msg.DEVICE);break;case DEV_CLOSE:hd_close(msg.DEVICE);break;case DEV_READ:case DEV_WRITE:
/***读写硬盘***/hd_rdwt(&msg);break;case DEV_IOCTL:hd_ioctl(&msg);break;default:dump_msg("HD driver::unknown msg", &msg);spin("FS::main_loop (invalid msg.type)");break;}
/***此处没有HARD_INT消息的处理,因为正常情况下在硬盘没有进行读写时是不会出现硬件中断的。hd_handler中的inform_int(TASK_HD),只有在对硬盘处理的函数中进行interrupt_wait时才会出现。***/send_recv(SEND, src, &msg);}
}



4、task_fs

 对文件的操作,包括打开文件、关闭文件,由于tty也视为特殊的文件,因此也包括控制台输入输出,都是通过给文件系统任务发送消息完成的。它会再针对文件类型的不同将消息转发给task_tty和task_hd,从而实现了对设备的抽象,有利于用户进程使用,用户只知道是文件,并不关系它具体是硬盘上的文件还是其它的什么。

 具体实现涉及作者自己定义的99号文件系统,较为复杂,有兴趣的参见第9章。

节自fs/main.c:

PUBLIC void task_fs()
{printl("Task FS begins.\n");/***初始化文件系统,包括建立proc_table与f_desc_table还有inode_table三个表,给task_hd发送消息打开硬盘,建立文件系统等。***/init_fs();while (1) {send_recv(RECEIVE, ANY, &fs_msg);int msgtype = fs_msg.type;int src = fs_msg.source;pcaller = &proc_table[src];switch (msgtype) {case OPEN:
/***打开一个文件,建立fd为下标的proc_table与f_desc_table还有inode_table之间的关联,返回文件描述符fd***/fs_msg.FD = do_open();break;case CLOSE:fs_msg.RETVAL = do_close();break;case READ:case WRITE:
/***通过文件系统的读写只有两种方式,一种是tty(特殊文件),一种是硬盘(普通文件);前一种是给task_tty发送消息,后一种是给task_hd发送消息。其中写硬盘是同过文件对应的inode的信息来得到扇区位置从而进行读写的。***/fs_msg.CNT = do_rdwt();break;case UNLINK:
/***删除文件时以此来归还系统资源***/fs_msg.RETVAL = do_unlink();break;case RESUME_PROC:
/***RESUME_PROC和下面处理条件中的SUSPEND_PROC是考虑到在文件系统从tty中读取数据时,用户输入间隔对进程来说是很长的,而tty不能在一个进程请求服务的期间只为这一个进程服务,因此在一个进程请求从tty读取数据时,tty会通过向文件系统发送SUSPEND_PROC消息来将该进程挂起,直到用户输入回车表示一条命令结束时,tty才会再次发送RESUME_PROC来让进程恢复运行,这样,tty可同时处理多个进程的请求。当进程处在SUSPEND状态时,文件系统也不会对该等待中的进程发送任何消息,下面发送消息的语句中有一个对SUSPEND状态的过滤,直到tty发送了RESUME_PROC之后,说明用户输入已经完成,文件系统才会给进程发送消息,让其继续运行。***/src = fs_msg.PROC_NR;break;/* case LSEEK: *//* 	fs_msg.OFFSET = do_lseek(); *//* 	break; *//* case FORK: *//* 	fs_msg.RETVAL = fs_fork(); *//* 	break; *//* case EXIT: *//* 	fs_msg.RETVAL = fs_exit(); *//* 	break; *//* case STAT: *//* 	fs_msg.RETVAL = do_stat(); *//* 	break; */default:dump_msg("FS::unknown message:", &fs_msg);assert(0);break;}/* reply */if (fs_msg.type != SUSPEND_PROC) {fs_msg.type = SYSCALL_RET;send_recv(SEND, src, &fs_msg);}}
}



5、task_mm

 实现fork()和exec(),从而使操作系统可以产生子进程和使用系统命令,模拟出一个shell的实现。有了exec,command文件夹中的echo和pwd命令才得以执行,他们都是以command/start.asm中的_start作为入口,并在call这些shell命令的main之前将argc和argv压栈。

节自mm/main.c:

PUBLIC void task_mm()
{init_mm();while (1) {send_recv(RECEIVE, ANY, &mm_msg);int src = mm_msg.source;int reply = 1;int msgtype = mm_msg.type;switch (msgtype) {case FORK:mm_msg.RETVAL = do_fork();break;case EXIT:do_exit(mm_msg.STATUS);reply = 0;break;case EXEC:mm_msg.RETVAL = do_exec();break;case WAIT:do_wait();reply = 0;break;default:dump_msg("MM::unknown msg", &mm_msg);assert(0);break;}if (reply) {mm_msg.type = SYSCALL_RET;send_recv(SEND, src, &mm_msg);}}
}


  该任务主体很简单,下面具体分析一下do_fork、do_exec的实现(do_exit和do_wait也很重要,分别是子进程退出和父进程等待子进程退出并得到返回值后彻底清理子进程资源,涉及进程过继和僵尸进程等特殊情况,具体参见P430):

首先是do_fork,节自mm/forkexit:


PUBLIC int do_fork()
{/* find a free slot in proc_table */struct proc* p = proc_table;int i;for (i = 0; i < NR_TASKS + NR_PROCS; i++,p++)if (p->p_flags == FREE_SLOT)break;int child_pid = i;assert(p == &proc_table[child_pid]);assert(child_pid >= NR_TASKS + NR_NATIVE_PROCS);if (i == NR_TASKS + NR_PROCS) /* no free slot */return -1;assert(i < NR_TASKS + NR_PROCS);/* duplicate the process table */int pid = mm_msg.source;u16 child_ldt_sel = p->ldt_sel;*p = proc_table[pid];p->ldt_sel = child_ldt_sel;p->p_parent = pid;sprintf(p->name, "%s_%d", proc_table[pid].name, child_pid);/* duplicate the process: T, D & S */struct descriptor * ppd;/* Text segment */ppd = &proc_table[pid].ldts[INDEX_LDT_C];/* base of T-seg, in bytes */int caller_T_base  = reassembly(ppd->base_high, 24,ppd->base_mid,  16,ppd->base_low);/* limit of T-seg, in 1 or 4096 bytes,depending on the G bit of descriptor */int caller_T_limit = reassembly(0, 0,(ppd->limit_high_attr2 & 0xF), 16,ppd->limit_low);/* size of T-seg, in bytes */int caller_T_size  = ((caller_T_limit + 1) *((ppd->limit_high_attr2 & (DA_LIMIT_4K >> 8)) ?4096 : 1));/* Data & Stack segments */ppd = &proc_table[pid].ldts[INDEX_LDT_RW];/* base of D&S-seg, in bytes */int caller_D_S_base  = reassembly(ppd->base_high, 24,ppd->base_mid,  16,ppd->base_low);/* limit of D&S-seg, in 1 or 4096 bytes,depending on the G bit of descriptor */int caller_D_S_limit = reassembly((ppd->limit_high_attr2 & 0xF), 16,0, 0,ppd->limit_low);/* size of D&S-seg, in bytes */int caller_D_S_size  = ((caller_T_limit + 1) *((ppd->limit_high_attr2 & (DA_LIMIT_4K >> 8)) ?4096 : 1));/***由于代码段和数据段完全相同,因此后面就直接用了caller_T_base和caller_T_size。***//* we don't separate T, D & S segments, so we have: */assert((caller_T_base  == caller_D_S_base ) &&(caller_T_limit == caller_D_S_limit) &&(caller_T_size  == caller_D_S_size ));/* base of child proc, T, D & S segments share the same space,so we allocate memory just once */
/***给子进程申请新的内存空间***/int child_base = alloc_mem(child_pid, caller_T_size);/* child is a copy of the parent */
/***完整复制父进程在内存中的内容到子进程新申请的空间。***/phys_copy((void*)child_base, (void*)caller_T_base, caller_T_size);/* child's LDT */
/***初始化子进程的LDT,指向新申请的空间***/init_desc(&p->ldts[INDEX_LDT_C],child_base,(PROC_IMAGE_SIZE_DEFAULT - 1) >> LIMIT_4K_SHIFT,DA_LIMIT_4K | DA_32 | DA_C | PRIVILEGE_USER << 5);init_desc(&p->ldts[INDEX_LDT_RW],child_base,(PROC_IMAGE_SIZE_DEFAULT - 1) >> LIMIT_4K_SHIFT,DA_LIMIT_4K | DA_32 | DA_DRW | PRIVILEGE_USER << 5);/* tell FS, see fs_fork() */
/***通知FS,处理共享文件的情况***/MESSAGE msg2fs;msg2fs.type = FORK;msg2fs.PID = child_pid;send_recv(BOTH, TASK_FS, &msg2fs);/***由于在fork中会send_recv(BOTH,...),因此在得到返回值之前,父进程是挂起在RECEIVING状态的,而子进程是完全复制了父进程,因此也处于相同的状态。
所以这里需要结束两者的挂起状态,其中真正给父进程发送是在该函数返回之后的task_mm()里。***//* child PID will be returned to the parent proc */mm_msg.PID = child_pid;/* birth of the child */MESSAGE m;m.type = SYSCALL_RET;m.RETVAL = 0;m.PID = 0;send_recv(SEND, child_pid, &m);return 0;
}


然后是execv,节自lib/exec.c;do_exec,节自mm/exec.c。前者是用户进程调用,后者是task_mm调用:


PUBLIC int execv(const char *path, char * argv[])
{char **p = argv;
/***这个数组前半段存放的是参数中字符指针的值,即字符串开始的地址。后半段是存放的实际的字符串参数。前面的地址跟后面的参数是一一对应的。参见图10.5***/char arg_stack[PROC_ORIGIN_STACK];int stack_len = 0;while(*p++) {assert(stack_len + 2 * sizeof(char*) < PROC_ORIGIN_STACK);stack_len += sizeof(char*);}/***数组的前半段和后半段被0隔开。***/*((int*)(&arg_stack[stack_len])) = 0;stack_len += sizeof(char*);char ** q = (char**)arg_stack;for (p = argv; *p != 0; p++) {
/***这一句是给前半段赋值。***/*q++ = &arg_stack[stack_len];/***后面几句是给后半段赋值。***/assert(stack_len + strlen(*p) + 1 < PROC_ORIGIN_STACK);strcpy(&arg_stack[stack_len], *p);stack_len += strlen(*p);arg_stack[stack_len] = 0;stack_len++;}MESSAGE msg;msg.type	= EXEC;msg.PATHNAME	= (void*)path;msg.NAME_LEN	= strlen(path);msg.BUF		= (void*)arg_stack;msg.BUF_LEN	= stack_len;send_recv(BOTH, TASK_MM, &msg);assert(msg.type == SYSCALL_RET);return msg.RETVAL;
}


PUBLIC int do_exec()
{/* get parameters from the message */int name_len = mm_msg.NAME_LEN;	/* length of filename */
/***src是给MM发信息的进程,即执行exec的进程。***/int src = mm_msg.source;	/* caller proc nr. */assert(name_len < MAX_PATH);char pathname[MAX_PATH];phys_copy((void*)va2la(TASK_MM, pathname),(void*)va2la(src, mm_msg.PATHNAME),name_len);pathname[name_len] = 0;	/* terminate the string *//* get the file size */struct stat s;int ret = stat(pathname, &s);if (ret != 0) {printl("{MM} MM::do_exec()::stat() returns error. %s", pathname);return -1;}/* read the file */
/***之前已经将ELF格式的文件从硬盘上的.tar文件读出并在内存中打开,这里将内存中打开的ELF格式的文件读到MM的缓冲区mmbuf。***/int fd = open(pathname, O_RDWR);if (fd == -1)return -1;assert(s.st_size < MMBUF_SIZE);read(fd, mmbuf, s.st_size);close(fd);/* overwrite the current proc image with the new one */Elf32_Ehdr* elf_hdr = (Elf32_Ehdr*)(mmbuf);int i;
/***将mmbuf中ELF格式的文件解析并写到内存中去,具体的位置由生成该文件时的程序头确定。***/for (i = 0; i < elf_hdr->e_phnum; i++) {Elf32_Phdr* prog_hdr = (Elf32_Phdr*)(mmbuf + elf_hdr->e_phoff +(i * elf_hdr->e_phentsize));if (prog_hdr->p_type == PT_LOAD) {assert(prog_hdr->p_vaddr + prog_hdr->p_memsz <PROC_IMAGE_SIZE_DEFAULT);phys_copy((void*)va2la(src, (void*)prog_hdr->p_vaddr),(void*)va2la(TASK_MM,mmbuf + prog_hdr->p_offset),prog_hdr->p_filesz);}}/***setup the arg stack这一段程序,首先将exec中初始化的堆栈复制到中间人TASK_MM中,然后在TASK_MM中完成堆栈中各个地址值的更新(相对于新载入程序的堆栈orig_stack),然后将更新后的堆栈完整地复制到新载入程序的堆栈位置,即orig_stack指向的位置。***//* setup the arg stack */int orig_stack_len = mm_msg.BUF_LEN;char stackcopy[PROC_ORIGIN_STACK];phys_copy((void*)va2la(TASK_MM, stackcopy),(void*)va2la(src, mm_msg.BUF),orig_stack_len);u8 * orig_stack = (u8*)(PROC_IMAGE_SIZE_DEFAULT - PROC_ORIGIN_STACK);int delta = (int)orig_stack - (int)mm_msg.BUF;/***从生成echo的ELF格式文件的命令可以看到 -Ttext 0x1000,即程序入口在0x1000,该值就是PROC_IMAGE_SIZE_DEFUALT,而PROC_ORIGIN_STACK的值是0x400.堆栈应该被放置在0x600开始的地方,原数组开始的地址是mm_msg.BUF,这里为新的数组stack_copy中的前半段地址值加上delta就完成了堆栈转移后地址值的更新。***/int argc = 0;if (orig_stack_len) {	/* has args */char **q = (char**)stackcopy;for (; *q != 0; q++,argc++)*q += delta;}phys_copy((void*)va2la(src, orig_stack),(void*)va2la(TASK_MM, stackcopy),orig_stack_len);/***将argc与argv写入寄存器。新载入程序的入口是_start,通过它来call main,见代码10.19。因此在进入新载入程序执行前,需要先将参数写入寄存器,以保证_start在call main之前能压入正确的参数。***/proc_table[src].regs.ecx = argc; /* argc */proc_table[src].regs.eax = (u32)orig_stack; /* argv *//* setup eip & esp */
/***将eip设置为新程序入口地址。***/proc_table[src].regs.eip = elf_hdr->e_entry; /* @see _start.asm */
/***栈顶赋值为刚被初始化的新栈,位置在程序入口的前0x400处,因为栈的最大值PROC_ORIGIN_STACK被赋值为了0x400.***/proc_table[src].regs.esp = PROC_IMAGE_SIZE_DEFAULT - PROC_ORIGIN_STACK;strcpy(proc_table[src].name, pathname);return 0;
/***整个初始化完成后,下一步在时钟中断返回时如果选择了该进程运行,就会直接从该程序入口开始执行。***/
}

  这里可能会有一个疑问,从command文件夹中的Makefile可以看出所有的shell指令生成ELF格式的文件时,指定的入口地址都是0x1000,而且前面我们也知道kernel的入口被指定为了0x1000,那这样岂不是在运行shell指令时会覆盖掉kernel?

  来看一下do_fork中的代码:

/***给子进程申请新的内存空间***/int child_base = alloc_mem(child_pid, caller_T_size);/* child is a copy of the parent */
/***完整复制父进程在内存中的内容到子进程新申请的空间。***/phys_copy((void*)child_base, (void*)caller_T_base, caller_T_size);
  再看一下do_exec中的代码:

			phys_copy((void*)va2la(src, (void*)prog_hdr->p_vaddr),(void*)va2la(TASK_MM,mmbuf + prog_hdr->p_offset),prog_hdr->p_filesz);

  注意src是fork出的子进程(参见do_fork代码),它的段基址是申请的新内存空间的最低地址,因此虽然所有的shell指令程序都是ELF格式,而且从Makefile中可以发现入口都是0x1000,但是不会覆盖掉kernel,原因就是段基址不同,这里的p_vaddr都加上了src的段基址。



6、用户进程Init(其它用户进程TestA、TestB、TestC都是空循环进程,不再讨论)

 作者亲切地称之为the hen,确实,是这第一个用户进程生出了后面的进程。该进程会tty1和tty2调用shabby_shell,这是两个粗略实现的shell,里面有一个while(1)的循环,模拟了shell的实现。循环中会为用户的一条命令输入fork一个子进程,并根据用户的输入为这个子进程执行exec。

节自kernel/main.c:

void Init()
{int fd_stdin  = open("/dev_tty0", O_RDWR);assert(fd_stdin  == 0);int fd_stdout = open("/dev_tty0", O_RDWR);assert(fd_stdout == 1);printf("Init() is running ...\n");/* extract `cmd.tar' */untar("/cmd.tar");char * tty_list[] = {"/dev_tty1", "/dev_tty2"};int i;for (i = 0; i < sizeof(tty_list) / sizeof(tty_list[0]); i++) {int pid = fork();if (pid != 0) { /* parent process */printf("[parent is running, child pid:%d]\n", pid);}else {	/* child process */printf("[child is running, pid:%d]\n", getpid());close(fd_stdin);close(fd_stdout);/***在tty1、tty2两个控制台,打开两个shabby_shell***/shabby_shell(tty_list[i]);assert(0);}}/***代码10.15,Init这个循环一是等两个tty的子进程的结束,而是可能有子进程的子进程过继到Init,防止Zombie***/while (1) {int s;int child = wait(&s);printf("child (%d) exited with status: %d.\n", child, s);}assert(0);
}

更多推荐

《一个操作系统的实现》总结3——系统任务和用户进程

本文发布于:2024-02-12 00:14:56,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1684525.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:进程   操作系统   用户   系统

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!