笔记09 - xv6 部分系统调用的实现

exec

exec指令用新程序替代当前进程的内存和寄存器,但保留文件描述符、进程id、父进程不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//exec.c
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint argc, sz, sp, ustack[3+MAXARG+1];
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pde_t *pgdir, *oldpgdir;

begin_op();

if((ip = namei(path)) == 0){//寻找name对应的inode
end_op();
return -1;
}
ilock(ip);
pgdir = 0;

// Check ELF header
//读取程序的elf
if(readi(ip, (char*)&elf, 0, sizeof(elf)) < sizeof(elf))// Read data from inode.
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
//创建内核页目录,映射内核空间。实际上是创建二级页表,并在二级页表项上存储物理地址。
//页目录项所存页表的权限是用户可读写,二级页表项所存物理页的权限按照kmap设定
if((pgdir = setupkvm()) == 0)
goto bad;

// Load program into memory.
//根据inode和elf程序头表偏移量, 按段读取程序
sz = 0;
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
//读取段的程序头表项
if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
//根据段的虚拟地址及段占据的内存大小分配物理空间,该过程会将程序的虚拟地址映射到分配到的物理页地址上
//二级页表项所存物理页的权限是用户可读写
if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;
if(ph.vaddr % PGSIZE != 0)
goto bad;
//根据程序头表项信息读取段到内存中
if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
iunlockput(ip);
end_op();
ip = 0;

// Allocate two pages at the next page boundary.
// Make the first inaccessible. Use the second as the user stack.
sz = PGROUNDUP(sz);
//继续为程序分配两页物理空间,二级页表项所存物理页的权限是用户可读写
//取消第一页用户可读写权限,第二页作为用户栈
if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
goto bad;
clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
sp = sz;

// Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[3+argc] = sp;
}
ustack[3+argc] = 0;

ustack[0] = 0xffffffff; // fake return PC
ustack[1] = argc;
ustack[2] = sp - (argc+1)*4; // argv pointer

sp -= (3+argc+1) * 4;
if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
goto bad;

// Save program name for debugging.
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(proc->name, last, sizeof(proc->name));

// Commit to the user image.
oldpgdir = proc->pgdir;
proc->pgdir = pgdir;
proc->sz = sz;
proc->tf->eip = elf.entry; // main
proc->tf->esp = sp;
switchuvm(proc);//设置cpu环境后,加载进程的页目录地址到cr3
freevm(oldpgdir);
return 0;

bad:
if(pgdir)
freevm(pgdir);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}

注意到,exec改变的有进程的页目录、用户空间内容、用户空间size、进程的tf->eip、tf->esp、用户栈、进程名,没有改变的有进程文件描述符、进程pid、父进程、内核栈、进程状态、当前目录等。

exec的整体思路是:1、根据程序路径查找对应的inode;2、根据inode读取程序的elf信息;3、调用setupkvm()创建页目录,并映射内核空间(页目录项所存页表的权限是用户可读写,二级页表项所存物理页的权限按照kmap设定);4、根据inode和elf程序头表偏移量,按段读取程序。每次循环时:4.1、先读取段的程序头表项,程序头表项包含段的虚拟地址及段占据的内存大小,4.2、根据信息调用allocuvm()分配出物理空间,分配过程会将程序的虚拟地址映射到分配到的物理页地址上,二级页表项所存物理页的权限是用户可读写,4.3、根据程序头表项信息(段偏移及段的文件大小)读取段到内存中;5、继续为程序分配两页物理空间,二级页表项所存物理页的权限是用户可读写,取消第一页用户可读写权限,第二页将作为用户栈。6、构建用户栈数据(参数等,见下);7、更新进程的页目录、用户空间size、进程的tf->eip为elf.entry(main)、tf->esp为用户栈指针当前位置,然后调用switchuvm()函数设置cpu环境,加载进程的页目录地址到cr3,释放旧页目录空间;8、返回trapret处,切换为用户态,返回执行新程序代码,不返回执行旧程序代码。

fork

fork用于创建新进程,并且复制当前进程的页目录、tramframe、内存空间大小、文件描述符、当前目录等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//proc.c
int
fork(void)
{
int i, pid;
struct proc *np;

// Allocate process.
//在进程表中寻找slot,成功的话更改进程状态和pid,并初始化进程的内核栈
//注意子进程使用的是新的内核栈
if((np = allocproc()) == 0){
return -1;
}

// Copy process state from p.
//copy一个进程的页目录对应的所有内容,思路是:首先创建新的页目录;
//在进程的用户空间虚拟地址范围内,由虚拟地址0x0开始,取得映射的物理页地址,然后申请新的物理页,将前者物理页的内容copy到后者;
//取出二级页表项的flag,当前虚拟地址映射到新物理页地址,其二级页表项权限设为flag。
if((np->pgdir = copyuvm(proc->pgdir, proc->sz)) == 0){ //see in vm.c
kfree(np->kstack);
np->kstack = 0;
np->state = UNUSED;
return -1;
}
np->sz = proc->sz; //设置进程的用户空间范围
np->parent = proc; //设置进程的父进程
*np->tf = *proc->tf; //设置进程的tramframe,这是结构体的值传递???

// Clear %eax so that fork returns 0 in the child.
np->tf->eax = 0;
//复制fd
for(i = 0; i < NOFILE; i++)
if(proc->ofile[i])
np->ofile[i] = filedup(proc->ofile[i]);
np->cwd = idup(proc->cwd);//复制当前目录

safestrcpy(np->name, proc->name, sizeof(proc->name));

pid = np->pid;

acquire(&ptable.lock);

np->state = RUNNABLE;//父进程必须在子进程的所有内容设置完毕后,才改为runnable,才能被cpu调度

release(&ptable.lock);

return pid;
//对于父进程而言,其系统调用的结果(子进程的pid)将在syscall.c中赋值到tf->eax中,
//然后在trapasm.S中弹出到eax,最终被用户进程获取。
//对于子进程而言,其复制了父进程的用户空间内容(包括代码),并使用新的内核栈,
//内核栈中trapframe的内容复制于父进程(包括下一条指令的eip,这条指令往往是从eax获取系统调用结果),
//并将eax的值改为0,这样子当cpu调度到子进程的时候,内核会将子进程的trapframe弹出,子进程将执行eip指向的指令,即从eax获取系统调用结果,所以获取到的pid是0。
}

注意到,fork新建并复制的内容有进程的内核栈(copy了tramframe)、页目录、用户空间内容,改变的内容有进程的父进程、进程的tf->eax、进程状态,复制的有用户空间size、进程的tramframe、进程的文件描述符、当前目录、进程名等。

fork的过程是:1、在进程表中寻找slot,成功的话更改进程状态和pid,并初始化进程的内核栈。注意子进程使用的是新的内核栈。2、copy当前进程的页目录对应的所有内容,思路是:首先创建新的页目录;在进程的用户空间虚拟地址范围内,由虚拟地址0x0开始,取得映射的物理页地址,然后申请新的物理页,将前者内容copy到后者;取出二级页表项的flag,当前虚拟地址映射到新物理页地址,其二级页表项权限设为flag。3、设置进程的用户空间范围、进程的父进程、进程的tramframe(值传递)。4、清除新进程tf->eax为0,所以子进程的fork返回值为0。5、复制fd、当前目录、进程名。6、设置进程状态为RUNNABLE。7、父进程返回pid。

遗留的问题

1
1、inode以及目录项的设计
显示 Gitment 评论