@SovietPower
2022-05-08T05:33:35.000000Z
字数 17272
阅读 1888
OS
https://www.zybuluo.com/SovietPower/note/1885177
参考:
https://zhuanlan.zhihu.com/p/104497182
https://blog.csdn.net/weixin_44765402/article/details/111089137
要修改的文件在src/userprog目录下:process.c, process.h, syscall.c, syscall.h,以及之前的thread.c, thread.h。
src/userprog文件结构:
需先创建具有文件系统分区的磁盘:
在userprog下make,然后进入build文件夹,执行:
pintos-mkdisk filesys.dsk --filesys-size=2pintos -f -qpintos -p ../../examples/echo.c -a echo -- -qpintos -q run 'echo x'# 后三条可合并:pintos -p ../../examples/echo -a echo.c -- -f -q run 'echo x’
build出现filesys.dsk,且最后一条语句执行成功,说明磁盘文件创建成功。
只需修改process.c。
使用下面这种格式进行测试:
pintos -v -k -T 60 --filesys-size=2 -p tests/userprog/args-none -a args-none -- -q -f run args-nonepintos -v -k -T 60 --filesys-size=2 -p tests/userprog/args-single -a args-single -- -q -f run 'args-single onearg'pintos -v -k -T 60 --qemu --filesys-size=2 -p tests/userprog/open-empty -a open-empty -- -q -f run open-empty# 含义:pintos: perl 脚本; -v -k -T 60 :启动参数–filesys-size=2 :定义文件系统大小-p:载入文件; -a:测试名; -q –f run:系统运行命令
最后结果:

问题
使用命令pintos -q run 'echo x',会得到echo x被作为命令一共处理的错误:
Executing 'echo x':Execution of 'echo x' complete. # 应为'echo'
命令echo和参数x被处理做了一个命令echo x,所以不能正确处理参数。
在process.c中,process_execute()创建处理命令的线程。
这里参数file_name是完整的echo x,创建的线程名称应为echo而不是echo x。
所以使用strtok_r()将命令和参数分离,在线程名称处传入thread_name。
strtok_r (char *s, const char *delimiters, char **save_ptr)将s从第一个分隔符字符串delimiters出现的位置分开,前一部分作为返回值,后一部分存入save_ptr。
可连续调用以彻底分割字符串(在第二次及以后s可为NULL,但第一次不可)。
...strlcpy (fn_copy, file_name, PGSIZE);char *thread_name, *tmp;thread_name = strtok_r(file_name, " ", &tmp);/* Create a new thread to execute FILE_NAME. */tid = thread_create (thread_name/* file_name */, PRI_DEFAULT, start_process, fn_copy); // 创建执行该程序的新线程if (tid == TID_ERROR)palloc_free_page (fn_copy);return tid;}
thread_create()中传入的要执行的函数为start_process(),start_process()中主要通过success = load (file_name, &if_.eip, &if_.esp),加载要执行的命令及参数到栈中(为该线程分配空间)。
根据要求,load()需将要执行命令/函数(主要是参数)分配一个新栈,然后令ESP指向该栈。
但load()仅在setup_stack()新建了一个栈,并令esp指向新栈的地址,并未在栈中放入命令的参数。
bool load (const char *file_name, void (**eip) (void), void **esp){.../* Set up stack. */if (!setup_stack (esp))goto done;...}
因此,除在setup_stack()新建一个栈外,还需在其中将参数传进去。
所以修改setup_stack()的定义:
static bool setup_stack (void **esp)改为:static bool setup_stack (void **esp, char *file_name)
load()中,if (!setup_stack (esp))改为if (!setup_stack (esp, file_name))。
此外,load()根据file_name打开可执行文件(命令)。
/* Open executable file. */file = filesys_open (file_name);
此处的file_name也应为不包含参数的命令。所以同样要分开:
/* Open executable file. */char *copy = malloc(strlen(file_name)+1);strlcpy(copy, file_name, strlen(file_name)+1);char *command, *tmp;command = strtok_r(file_name, " ", &tmp);file = filesys_open (command);free(copy);if (file == NULL){printf ("load: %s: open failed\n", file_name);goto done;}
注意要拷贝file_name,因为strtok_r会对串本身进行改变,但之后要用到该串。之后可以释放。
(process_execute()中也拷贝了fn_copy)
使用strtok_r()将file_name的参数分离开,依次放入栈。
就类似C的argc, **argv。命令本身是第一个参数。
注意参数是字符串,类似**argv,应将这些字符串先放入用户栈中(局部变量),再将这些字符串的地址作为实际参数**argv的各位置的值依次(逆序,从*argv[argc-1]到*argv[0])压入栈中。
同时要注意用户栈是向下增长的(栈底在高地址处)。
所以过程为:
1. 将各参数放入栈中(顺序任意),并记录每个参数的地址addr[i]。这里不需要对齐,因为就是一长串的用户数据。
2. 将*esp按4字节对齐。之后真正参数的压栈都需按4字节对齐。
3. 将每个参数的地址addr[i],即*argv[i],压入栈中。
4. 将argv的地址,即**argv,压入栈中。
5. 将argc的数值,即参数个数,压入栈中。
6. 将函数返回值,压入栈中。因为这里类似main(),返回值用不到,为0就可以了(但是必须要分配这个空间)。
修改后的setup_stack():
{...// 参数数统计,用于分配*argv的空间int i, argc=1, len=strlen(file_name);for (i=1; i<len; ++i)if (file_name[i]==' ' && file_name[i-1]!=' ')++argc;char *argument, *tmp;unsigned int int_size = sizeof(int);// 保存各参数的地址*argv[i]int *argv = calloc(argc, int_size);// 将各字符串压入栈(顺序任意,为了方便就依次)argument = strtok_r(file_name, " ", &tmp);for(i=0; argument!=NULL; argument=strtok_r(NULL, " ", &tmp)){*esp -= strlen(strlen(argument)+1); // 栈向下增长memcpy(*esp, argument, strlen(argument)+1);argv[i++] = *((int *)esp);}// 对齐*esp -= (unsigned)*esp%4;// 将参数*argv[i]压入栈,每次都对齐(每次一个(unsigned) int即正好对齐)for (i=argc-1; ~i; --i){*esp -= int_size;memcpy(*esp, &argv[i], int_size);}// 将参数**argv压入栈(对齐)。**argv即*argv[0]的起始地址,即此时的*esp值。*esp -= int_size;*(int *)*esp = (int)*esp + int_size; // 注意要转指针类型确保正确赋值!或是用memcpy。// memcpy(*esp, (int)*esp+4, int_size);// 将参数argc压入栈*esp -= int_size;*(int *)*esp = argc;// 将函数返回值0压入栈*esp -= int_size;*(int *)*esp = 0;return success;}
if_.esp原来指向esp的内核和用户区边界为避免内存访问越界(访问到内核区),需将setup_stack()函数中的*esp = PHYS_BASE;修改成*esp = PHYS_BASE - 12;?
目标是实现syscall-nr.h中的以下系统调用函数(每个调用也对应了一个编号):
enum{/* Projects 2 and later. */SYS_HALT, /* Halt the operating system. */SYS_EXIT, /* Terminate this process. */SYS_EXEC, /* Start another process. */SYS_WAIT, /* Wait for a child process to die. */SYS_CREATE, /* Create a file. */SYS_REMOVE, /* Delete a file. */SYS_OPEN, /* Open a file. */SYS_FILESIZE, /* Obtain a file's size. */SYS_READ, /* Read from a file. */SYS_WRITE, /* Write to a file. */SYS_SEEK, /* Change position in a file. */SYS_TELL, /* Report current position in a file. */SYS_CLOSE, /* Close a file. */...};
通过以下测试集:
createexecwaithaltopenclosereadwrite
随便查看一个测试create-normal.c,可见它是调用了syscall.c中的
bool create (const char *file, unsigned initial_size){return syscall2 (SYS_CREATE, file, initial_size);}
该函数在成功时返回1。
该文件中定义了不同的syscall0, syscall1, syscall2, syscall3,分别是有0、1、2、3个参数的系统调用。
会分别使用pushl %[arg];将每个参数的地址依次压入栈,然后将当前系统调用的编号的地址压入栈,然后执行0x30号中断(int $0x30,AT&T语法的中断指令),然后将栈指针esp加(是参数数+1,即压入栈的元素数),即(在执行完0x30号中断后?)将esp恢复到压栈前的位置。
如syscall2:
/* Invokes syscall NUMBER, passing arguments ARG0 and ARG1, andreturns the return value as an `int'. */#define syscall2(NUMBER, ARG0, ARG1) \({ \int retval; \asm volatile \("pushl %[arg1]; pushl %[arg0]; " \"pushl %[number]; int $0x30; addl $12, %%esp" \: "=a" (retval) \: [number] "i" (NUMBER), \[arg0] "r" (ARG0), \[arg1] "r" (ARG1) \: "memory"); \retval; \})
userprog/syscall.c中,syscall_init()进行了初始化:
void syscall_init (void){intr_register_int (0x30, 3, INTR_ON, syscall_handler, "syscall");}
它为0x30号中断定义了syscall_handler(),即0x30号中断的中断处理程序,所以修改这个函数。
参数struct intr_frame *f中保存了中断时的栈指针esp,此时esp指向中断号NUMBER的地址。
所以*esp即中断号的地址,检查*esp是一个合法地址后,**esp即为中断号:
int *p = f->esp;// 检查是否是合法地址is_valid_addr(p);int syscall_number = *p;
然后需要定义这些函数:
void syscall_halt(void);void syscall_exit(struct intr_frame *f);int syscall_exec(struct intr_frame *f);int syscall_wait(struct intr_frame *f);int syscall_create(struct intr_frame *f);int syscall_remove(struct intr_frame *f);int syscall_open(struct intr_frame *f);int syscall_filesize(struct intr_frame *f);int syscall_read(struct intr_frame *f);int syscall_write(struct intr_frame *f);void syscall_seek(struct intr_frame *f);int syscall_tell(struct intr_frame *f);void syscall_close(struct intr_frame *f);
然后调用这些函数:
switch (syscall_number){case SYS_HALT: syscall_halt(); break;case SYS_EXIT: syscall_exit(f); break;case SYS_EXEC: f->eax = syscall_exec(f); break;case SYS_WAIT: f->eax = syscall_wait(f); break;case SYS_CREATE: f->eax = syscall_create(f); break;case SYS_REMOVE: f->eax = syscall_remove(f); break;case SYS_OPEN: f->eax = syscall_open(f); break;case SYS_FILESIZE: f->eax = syscall_filesize(f); break;case SYS_READ: f->eax = syscall_read(f); break;case SYS_WRITE: f->eax = syscall_write(f); break;case SYS_SEEK: syscall_seek(f); break;case SYS_TELL: f->eax = syscall_tell(f); break;case SYS_CLOSE: syscall_close(f); break;default: printf("No such syscall %d.\n", syscall_number); break;}
注意有返回值的函数,要将返回值赋值给f->eax。
然后就是实现上述函数。
在thread.h中:
在thread中添加一些值:
struct thread{.../* userprog */int exit_status; // 退出状态int fd_count; // 打开文件数struct list open_files; // 打开文件列表struct file *self; // 指向自己的可执行文件的指针struct thread *parent; // 父进程struct list children_list; // 子进程列表bool load_successful; // 子进程是否加载成功struct semaphore load_sema; // 用于加载的信号量,会阻塞当前进程直到子进程加载完毕struct child_process *waiting_child; // 等待加载完成的子进程...};
定义子进程:
struct child_process{int tid; // 进程号int exit_status; // 退出状态struct list_elem child_elem; // 子进程列表中的list元素bool finished; // 是否待回收struct semaphore wait_sema; // 用于回收的信号量,会阻塞当前进程直到子进程回收完毕};
定一个全局锁sysfile_lock,和退出状态的常量:
struct lock filesys_lock; // 用于文件操作#define EXIT_STATUS 998244353
thread_init()中,对filesys_lock初始化:
lock_init (&filesys_lock);
thread_create()中,初始化child_process:
/* 初始化该子进程(是被thread_current()创建出来的) */struct child_process *c = malloc(sizeof(*c));c->tid = tid;c->finished = false;c->exit_status = t->exit_status;sema_init(&c->wait_sema, 0);list_push_back(&running_thread()->children_list, &c->child_elem);// 注意这里不能用thread_current(),thread_current()会判断当前进程状态为RUNNING,其实不是?
init_thread()中,对新定义的字段初始化:
// userprogt->parent = running_thread();// 注意这里不能用thread_current(),thread_current()会判断当前进程状态为RUNNING,其实不是?t->self = NULL;t->exit_status = EXIT_STATUS;t->fd_count = 2;list_init(&t->open_files);t->load_successful = false;sema_init(&t->load_sema, 0);t->waiting_child = NULL;list_init(&t->children_list);
在之后要实现的syscall_open,也需对fd_count和open_files进行更新。
syscall.h中定义文件结构process_file:
#include "kernel/list.h"struct process_file{int fd;struct file *ptr; // 文件指针struct list_elem elem; // 打开文件队列元素};
process.c的load()中,在最后done前为file赋值:
...thread_current()->self = file;done:...
process_exit()中,释放文件和子进程文件:
// 释放当前进程(需检查退出状态)if (cur->exit_status == EXIT_STATUS)exit_process(-1);printf("%s exits (%d)\n", cur->name, cur->exit_status);if (lock_held_by_current_thread(&filesys_lock))lock_release(&filesys_lock);// 释放当前进程的文件和其打开文件列表中的文件lock_acquire(&filesys_lock);clean_all_files(&cur->open_files);file_close(cur->self);// 释放子进程// 这里每次pop,而不是遍历队列删除,因为free后就没了while(!list_empty(&cur->children_list)){struct list_elem *first = list_pop_front(&cur->children_list);free(list_entry(first, struct child_process, child_elem));}lock_release(&filesys_lock);
这里需要定义释放进程的函数exit_process():
// 会将当前进程的退出状态设为status;将当前进程的父进程的子进程队列中,将该进程的child_process结构中的exit_status设为status,将finished设为true。最后调用thread_exit()终止进程。void exit_process(int status){struct child_process *cp;struct thread *cur = thread_current();struct list *children = &cur->parent->children_list;enum intr_level old_level = intr_disable();for (struct list_elem *e=list_begin(children); e!=list_end(children); e=list_next(e)){cp = list_entry(e, struct child_process, child_elem);if (cp->tid == cur->tid){cp->finished = true;cp->exit_status = status;break;}}cur->exit_status = status;intr_set_level(old_level);thread_exit();}
还要定义释放链表所有文件的函数clean_all_files():
void clean_all_files(struct list *files){struct process_file *pf;while(!list_empty(files)){pf = list_entry(list_pop_front(files), struct process_file, elem);file_close(pf->ptr);free(pf);}}
顺便定义释放指定文件的函数clean_single_file():
void clean_single_file(struct process_file *pf){if (pf != NULL){list_remove(&pf->elem);file_close(pf->ptr);free(pf);}}
以及查询链表中指定fd的链表元素的函数search_by_fd():
struct process_file *search_by_fd(struct list *files, int fd){struct process_file *pf;for (struct list_elem *e=list_begin(files); e!=list_end(files); e=list_next(e)){pf = list_entry(e, struct process_file, elem);if (pf->fd == fd)return pf;}return NULL;}
函数基本通过调用filesys.h和file.h中的函数实现。
在syscall.c中,实现判断地址是否合法的is_valid_addr()。
如果地址为非法,或不是用户地址,直接结束进程。
void *is_valid_addr(const void *vaddr){void *page_ptr = NULL;if (!is_user_vaddr(vaddr) || !(page_ptr=pagedir_get_page(thread_current()->pagedir, vaddr))){exit_process(-1);return 0;}return page_ptr;}
定义pop_stack(),取出syscall时栈中指定的参数。
void pop_stack(int *esp, int *save, int offset){int *tmp = esp;*save = *((int *)is_valid_addr(tmp + offset));}
syscall_halt():
// 关闭机器void syscall_halt(void){shutdown_power_off(); // 在"devices/shutdown.h"中}
syscall_exit():
// 结束进程,并将进程的exit_status设为指定参数void syscall_exit(struct intr_frame *f){int status;pop_stack(f->esp, &status, 1);exit_process(status);}
syscall_exec():
// 加载指定文件,文件名在参数中int syscall_exec(struct intr_frame *f){char *file_name = NULL;pop_stack(f->esp, &file_name, 1);if (!is_valid_addr(file_name))return -1;return exec_process(file_name);}
需要定义exec_process():
// 加载指定文件名的进程// 先获取文件系统锁,然后分离文件名,尝试是否能打开该文件。如果可以,则加载该进程,返回新tid。int exec_process(char *file_name){lock_acquire(&filesys_lock);char *copy = malloc(strlen(file_name)+1);strlcpy(copy, file_name, strlen(file_name)+1);char *name, *tmp;name= strtok_r(copy, " ", &tmp);struct file *f = filesys_open(name);int tid;if (f == NULL)tid = -1;elsefile_close(f), tid = process_execute(file_name);lock_release(&filesys_lock);return tid;}
syscall_wait():
// 等待指定子进程结束,然后返回其退出状态int syscall_wait(struct intr_frame *f){int child_tid;pop_stack(f->esp, &child_tid, 1);return process_wait(child_tid);}
syscall_create():
// 创建指定名称、指定初始大小的文件int syscall_create(struct intr_frame *f){off_t initial_size;char *name;pop_stack(f->esp, &name, 4);pop_stack(f->esp, &initial_size, 5);if (!is_valid_addr(name))return -1;lock_acquire(&filesys_lock);int res = filesys_create(name, initial_size);lock_release(&filesys_lock);return res;}
syscall_remove():
// 删除拥有指定名称的文件int syscall_remove(struct intr_frame *f){char *name;pop_stack(f->esp, &name, 1);if (!is_valid_addr(name))return -1;lock_acquire(&filesys_lock);int res = (filesys_remove(name) != 0);lock_release(&filesys_lock);return res;}
syscall_open():
注意要实现process_file的初始化
// 打开拥有指定名称的文件int syscall_open(struct intr_frame *f){char *name;pop_stack(f->esp, &name, 1);if (!is_valid_addr(name))return -1;lock_acquire(&filesys_lock);struct file *file = filesys_open(name);lock_release(&filesys_lock);if (file == NULL)return -1;// 初始化process_file,更新当前进程的fd_count和open_filesstruct process_file *p = malloc(sizeof(*p));p->ptr = file;p->fd = thread_current()->fd_count++;list_push_back(&thread_current()->open_files, &p->elem);return p->fd;}
syscall_filesize():
// 获取指定文件的大小int syscall_filesize(struct intr_frame *f){int fd;pop_stack(f->esp, &fd, 1);lock_acquire(&filesys_lock);int res = file_length(search_by_fd(&thread_current()->open_files, fd)->ptr);lock_release(&filesys_lock);return res;}
syscall_read():
注意input_getc()在"devices/input.h"中
// 从描述符为fd的文件中读入int syscall_read(struct intr_frame *f){int size, fd;char *buffer;pop_stack(f->esp, &fd, 5);pop_stack(f->esp, &buffer, 6);pop_stack(f->esp, &size, 7);if (!is_valid_addr(buffer))return -1;if (fd == 0) // 从stdin输入{uint8_t *buffer = buffer;for (int i=0; i<size; ++i)buffer[i] = input_getc();return size;}struct process_file *p = search_by_fd(&thread_current()->open_files, fd);if (p == NULL)return -1;lock_acquire(&filesys_lock);int res = file_read(p->ptr, buffer, size);lock_release(&filesys_lock);return res;}
syscall_write():
注意putbuf在"kernel/stdio.h"中。
// 向描述符为fd的文件中输出int syscall_write(struct intr_frame *f){int size, fd;char *buffer;pop_stack(f->esp, &fd, 5);pop_stack(f->esp, &buffer, 6);pop_stack(f->esp, &size, 7);if (!is_valid_addr(buffer))return -1;if (fd == 1) // 向stdout输出{putbuf(buffer, size);return size;}struct process_file *p = search_by_fd(&thread_current()->open_files, fd);if (p == NULL)return -1;lock_acquire(&filesys_lock);int res = file_write(p->ptr, buffer, size);lock_release(&filesys_lock);return res;}
syscall_seek():
// 更改文件指针的定位void syscall_seek(struct intr_frame *f){int fd, pos;pop_stack(f->esp, &fd, 4);pop_stack(f->esp, &pos, 5);lock_acquire(&filesys_lock);file_seek(search_by_fd(&thread_current()->open_files, fd)->ptr, pos);lock_release(&filesys_lock);}
syscall_tell():
// 获取当前文件指针的位置int syscall_tell(struct intr_frame *f){int fd;pop_stack(f->esp, &fd, 1);lock_acquire(&filesys_lock);int res = file_tell(search_by_fd(&thread_current()->open_files, fd)->ptr);lock_release(&filesys_lock);return res;}
syscall_close():
// 释放描述符为fd的文件void syscall_close(struct intr_frame *f){int fd;pop_stack(f->esp, &fd, 1);lock_acquire(&filesys_lock);clean_single_file(search_by_fd(&thread_current()->open_files, fd));lock_release(&filesys_lock);}
pintos系统不保证多线程安全,为了防止多个线程同时对文件系统进行操作,需要在所有相关的地方对文件系统加锁。
除了上述定义的文件要注意加锁外,已有的函数中:
process.c中的load()要加锁,以及禁止文件写入。
{...// 获取文件系统锁lock_acquire(&filesys_lock);/* Allocate and activate page directory. */t->pagedir = pagedir_create ();if (t->pagedir == NULL)goto done;process_activate ();...file = filesys_open (command);free(copy);if (file == NULL){printf ("load: %s: open failed\n", file_name);goto done;}// 禁止当前文件被写入file_deny_write(file);...done:/* We arrive here whether the load is successful or not. */file_close (file);// 释放文件系统锁lock_release(&filesys_lock);return success;}
之前定义了load_sema,父进程在子进程加载完成前,需阻塞;当子进程加载完成后,解除阻塞并让父进程得知子进程的加载状态load_successful。
process.c中的process_execute()里,thread_create()创建了进程,若创建成功,此时应使用sema_down(&load_sema)阻塞父进程并加载子进程,在子进程加载完毕即start_process()的最后,使用sema_up(&load_sema)唤醒父进程。
process_execute():
.../* Create a new thread to execute FILE_NAME. */tid = thread_create (thread_name/* file_name */, PRI_DEFAULT, start_process, fn_copy); // 创建执行该程序的新线程if (tid == TID_ERROR)palloc_free_page (fn_copy);else{struct thread *cur = thread_current();sema_down(&cur->load_sema);if (!cur->load_successful)return -1;}return tid;}
start_process():
.../* If load failed, quit. */palloc_free_page (file_name);// 子进程加载完成后,让父进程得知其加载状态struct thread *cur = thread_current();cur->parent->load_successful = success;if (!success)thread_exit ();// 唤醒父进程sema_up(&cur->parent->load_sema);asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory");NOT_REACHED ();}
由于pintos设计要求,父进程每次只能回收一个进程,因此回收时也需用信号量限制。
在父进程调用process_wait()等待一个指定子进程结束时,(如果该子进程未完成)需阻塞,直到子进程退出。
所以在process_wait()中:
intprocess_wait (tid_t child_tid){enum intr_level old_level = intr_disable();struct list_elem *e = find_child_process(child_tid);struct child_process *c = list_entry(e, struct child_process, child_elem);intr_set_level(old_level);if (!e || !c)return -1;thread_current()->waiting_child = c;// 子进程未完成,阻塞父进程if (!c->finished)sema_down(&c->wait_sema);// 子进程结束list_remove(e);return c->exit_status;}
这里要实现find_child_process():
// 查找指定的child_processstruct list_elem *find_child_process(int child_tid){struct child_process *cp;struct thread *cur = thread_current();for (struct list_elem *e=list_begin(&cur->children_list); e!=list_end(&cur->children_list); e=list_next(e)){cp = list_entry(e, struct child_process, child_elem);if (cp->tid == child_tid)return e;}return NULL;}
thread_exit()中:
void thread_exit (void){ASSERT (!intr_context ());// 若父进程在等待当前进程结束,唤醒父进程struct thread *cur = thread_current();enum intr_level old_level = intr_disable();if (cur->parent->waiting_child != NULL && cur->parent->waiting_child->tid == cur->tid)sema_up(&cur->parent->waiting_child->wait_sema);intr_set_level(old_level);#ifdef USERPROGprocess_exit ();#endif...}

注意malloc()是"threads/malloc.h"里的!不是stdlib.h,会出错。
头文件要引入正确!
应该参考文档的。