Nachos Lab05 系统调用

理解Nachos系统调用

Exercise 1 源代码阅读

阅读与系统调用相关的源代码,理解系统调用的实现原理。

code/userprog/syscall.h

code/userprog/exception.cc

code/test/start.s

code/userprog/syscall.h:这里定义了Nachos中的系统调用号,以及相应的系统调用接口。Nachos目前共有11种系统调用。

code/userprog/exception.cc:这里定义了异常处理函数。当异常发生的时候,ExceptionHandler函数会被调用。系统调用也是异常的一种,即SyscallException。然后根据从寄存器中读到的系统调用号,来进行相应的处理。

code/test/start.s:用于辅助Nachos中用户程序的执行。该文件共有两个作用,其一是定义程序执行时跳转到main函数,以及程序执行结束时调用Exit系统调用。另一个作用是,实现了用户程序的系统调用接口,该接口会将参数放在寄存器中,并跳转到异常处理函数执行相应的处理。

文件系统相关的系统调用

Exercise 2 系统调用实现

类比Halt的实现,完成与文件系统相关的系统调用:Create, Open,Close,Write,Read。Syscall.h文件中有这些系统调用基本说明。

前置工作

首先新增两个文件syscalldep.h和syscalldep.cc,然后在code/Makefile.common中注册这两个文件,以及对应的目标文件。这两个文件的作用是,定义一些系统调用真实实现,让其在code/userprog/exception.cc中被调用实现真正的处理。

由于上一次实习已经实现了Nachos中的文件系统,因此修改code/userprog/Makefile,将文件系统完成后的宏换上。这里不要忘了把启用TLB加上,不然可能会出现一些问题。注:此时的用户程序是实现了交换分区之后的版本。

另外,如果使用Nachos内实现的文件系统,则不定长文件名也会存在问题。不定长文件名会存在的问题在Nachos Lab04 文件系统Exercise 2 扩展文件属性中讨论过。为了方便测试,将不定长文件名改为定长文件名。然后根据需要修改文件名的最大长度限制FileNameMaxLen

在Machine中实现一个PC前进的方法,用于增加PC。虽然该方法早先在Nachos Lab02 虚拟内存中实现过,但是当时的实现方式存在一些问题,因此在此处更正。

1
2
3
4
5
void Machine::PCAdvance() {
registers[PrevPCReg] = registers[PCReg];
registers[PCReg] = registers[NextPCReg];
registers[NextPCReg] = registers[NextPCReg] + sizeof(int);
}

Create

实现

首先从寄存器中读取字符串的起始地址。由于文件名是不定长的,因此读取文件名分成了两步。先循环一遍,获取字符串长度;再进行一次循环从内存中读取字符。最后调用文件系统的Create方法,由于文件支持动态长度,因此初始大小设为0。由于文件名改成了有最大长度限制,因此这里name字符数组直接初始化为定长大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* code/userprog/syscalldep.cc
*/
void SysCreate() {
int base = machine->ReadRegister(4);
int count = 0;
int value;

char name[FileNameMaxLen + 1];
do {
while (!machine->ReadMem(base + count, 1, &value));
name[count] = (char)value;
count++;
} while (value != '\0' && count <= FileNameMaxLen + 1);

if (!fileSystem->Create(name, 0)) {
printf("Create %s Failed\n", name);
}

// 将PC + 4,让系统调用处理完之后,用户程序执行后一条指令
machine->PCAdvance();
}

然后在ExceptionHandler注册该系统调用的处理。同时修改PC增加的规则,除SC_Halt之外,都需要将PC增加,来让用户程序能够继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* code/userprog/exception.cc
*/
void
ExceptionHandler(ExceptionType which)
{
int type = machine->ReadRegister(2);
if (which == SyscallException) {
...
} else if (type == SC_Create) {
DEBUG('a', "File Create!\n");

SysCreate();
}

// 将PC + 4,让系统调用处理完之后,用户程序执行后一条指令
// 执行SC_Halt时,Nachos关机,因此不需要再切换PC
if (type != SC_Halt) {
int nextPc = machine->ReadRegister(NextPCReg);
machine->WriteRegister(PCReg, nextPc);
}
}
...
}
测试

修改code/test/halt.c文件,让其调用Create系统调用。

1
2
3
4
5
6
7
8
#include "syscall.h"

int
main()
{
Create("aaa");
Halt();
}

需要注意的是,由于修改了文件系统为Nachos真实实现的文件系统,使用StartProcess打开的是Nachos内部的可执行程序,因此需要先将可执行程序拷贝到Nachos内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -cp ../test/halt test
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -l
0 VirtualMemory
0 test
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!

Ticks: total 129060, idle 127010, system 2050, user 0
Disk I/O: reads 22, writes 9
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

然后执行test程序,可以看到文件”aaa”被成功创建。

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
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
Machine halting!

Ticks: total 25584559, idle 25394330, system 190200, user 29
Disk I/O: reads 1497, writes 1188
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -l
0 VirtualMemory
0 test
0 aaa
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!

Ticks: total 129060, idle 127010, system 2050, user 0
Disk I/O: reads 22, writes 9
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

Open

实现

Open调用的实现由于有返回值,因此需要将Nachos中的文件描述符,即OpenFile指针写入到r2寄存器中。同样,在ExceptionHandler中加入SC_Open调用的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void SysOpen() {
int base = machine->ReadRegister(4);
int value;
int count = 0;

char name[FileNameMaxLen + 1];
do {
while (!machine->ReadMem(base + count, 1, &value));
name[count] = (char)value;
count++;
} while (value != '\0' && count <= FileNameMaxLen + 1);

OpenFile *openFile = fileSystem->Open(name);
if (openFile == NULL) {
printf("Open %s Failed\n", name);
}
machine->WriteRegister(2, (OpenFileId)openFile);
printf("File %s => OpenFileId %d\n", name, (OpenFileId)openFile);
}
测试

再次修改code/test/halt.c的代码,使用Exit来将获取到的文件描述符输出。

1
2
3
4
5
6
int
main()
{
OpenFileId fd = Open("aaa");
Exit(fd);
}

结果如下:

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
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x open_test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
File aaa => OpenFileId 136395216
Exit Code 136395216
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!

Ticks: total 21985080, idle 21786997, system 198050, user 33
Disk I/O: reads 1579, writes 1249
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

Close

实现

关闭文件,则从寄存器中解析出文件描述符指针的值,然后调用detele来关闭文件。由于文件指针的值是一个整数,因此直接从寄存器中将其取出即可,而不用去内存中寻找。

1
2
3
4
5
6
7
8
void SysClose() {
int base = machine->ReadRegister(4);

OpenFile *openFile = (OpenFile *)base;

delete openFile;
printf("File id %d closed!\n", (int)openFile);
}
测试

先打开文件,再调用Close关闭文件。

1
2
3
4
5
6
7
int
main()
{
OpenFileId fd = Open("aaa");
Close(fd);
Halt();
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x close_test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
File aaa => OpenFileId 165435856
File id 165435856 closed!
Machine halting!

Ticks: total 22065080, idle 21866038, system 199000, user 42
Disk I/O: reads 1586, writes 1255
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

Write

实现

分别从r4,r5,r6三个寄存器中读取输入参数。由于输入的buffer是字符串,因此从内存中逐字节将其读到变量in中。再调用文件的Write方法将其写入进文件。同时in的大小需要稍大一些,用于存储末尾的\0字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void SysWrite() {
int buf_base = machine->ReadRegister(4);
int size = machine->ReadRegister(5);
int openFileId = machine->ReadRegister(6);
int value;

char *in = new char[size + 1];

for (int i = 0; i < size; i++) {
while (!machine->ReadMem(buf_base + i, 1, &value));
in[i] = (char)value;
}
in[size] = '\0';

printf("Get instream string => %s\n", in);
OpenFile *openFile = (OpenFile *)openFileId;
int r = openFile->Write(in, size);
printf("Writing in fileId %d with %d bytes\n", (OpenFileId)openFile, r);

delete[] in;
}
测试

再次修改测试函数,调用Write系统调用。

1
2
3
4
5
6
7
8
int
main()
{
OpenFileId fd = Open("aaa");
Write("HelloWorld!", 11, fd);
Close(fd);
Halt();
}

结果如下:

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
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x write_test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
File aaa => OpenFileId 142665376
Get instream string => HelloWorld!
Writing in fileId 142665376 with 11 bytes
File id 142665376 closed!
Machine halting!

Ticks: total 24161070, idle 23941760, system 219250, user 60
Disk I/O: reads 1749, writes 1386
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

Read

实现

与写类似,从文件中读取数据,让后逐字节将其写入到内存当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void SysRead() {
int buf_base = machine->ReadRegister(4);
int size = machine->ReadRegister(5);
int openFileId = machine->ReadRegister(6);

char *out = new char[size];
OpenFile *openFile = (OpenFile *)openFileId;
int r = openFile->Read(out, size);
printf("Read %d bytes from FileId %d\n", r, openFileId);

for (int i = 0; i < r; i++) {
while (!machine->WriteMem(buf_base + i, 1, (int)out[i]));
}
machine->WriteRegister(2, r);

delete[] out;
}
测试

由于Nachos内部没有实现标准输出相关的调用。因此将读取的结果写入到另一个文件中来查看读取的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
main()
{
char buf[100];
OpenFileId fd = Open("aaa");
Read(buf, 11, fd);
Close(fd);
buf[11] = '\0';

Create("readRes");
fd = Open("readRes");
Write(buf, 11, fd);
Close(fd);

Halt();
}

结果如下,可以看到新文件中写入了从原文件中读到的字符串”HelloWorld!”。

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
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x read_test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
File aaa => OpenFileId 135694800
Read 11 bytes from FileId 135694800
File id 135694800 closed!
File readRes => OpenFileId 135693984
Get instream string => HelloWorld!
Writing in fileId 135693984 with 11 bytes
File id 135693984 closed!
Machine halting!

Ticks: total 27793070, idle 27539230, system 253730, user 110
Disk I/O: reads 2027, writes 1607
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

Exercise 3 编写用户程序

编写并运行用户程序,调用练习2中所写系统调用,测试其正确性。

这一部分与Exercise 2放在一起,见Exercise 2的测试部分。

执行用户程序相关的系统调用

Exercise 4 系统调用实现

实现如下系统调用:Exec,Fork,Yield,Join,Exit。Syscall.h文件中有这些系统调用基本说明。

前置条件

此处的系统调用涉及多线程,因此也就有了子线程和父线程的概念。在code/threads/thread.h中定义一个宏MaxNumSubThread来标记一个线程能拥有的最大子线程数。修改Thread类,新增一个线程指针数组,用于记录其所拥有的子线程;一个父线程指针,用于标识父线程;以及一个状态码整数,用于记录最后一个退出的子线程的状态码。以及三个方法分别用于创建子线程,删除子线程,以及确认子线程是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* code/threads/thread.h
*/
#define MaxNumSubThread 10

class Thread {
...
public:
Thread *subThreads[MaxNumSubThread];
Thread *parThread;
int lastSubExitStatus;

Thread * AddSubThread();
void DelSubThread(Thread * st);
bool CheckThread(Thread * st);
};

然后在构造函数对新增的信息进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
/*
* code/threads/thread.cc
*/
Thread::Thread(char* threadName)
{
...
for (int i = 0; i < MaxNumSubThread; i++) {
subThreads[i] = NULL;
}
parThread = NULL;
lastSubExitStatus = 0;
}

创建,销毁和确认子线程的方法如下:

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
Thread * Thread::AddSubThread() {
for (int i = 0; i < MaxNumSubThread; i++) {
if (subThreads[i] == NULL) {
printf("Thread %s add a subthread\n", name);
Thread *st = new Thread("SubThread");
subThreads[i] = st;
st->parThread = this;
return st;
}
}
return NULL;
}

void Thread::DelSubThread(Thread *st) {
for (int i = 0; i < MaxNumSubThread; i++) {
if (subThreads[i] == st) {
subThreads[i] = NULL;
break;
}
}
}

bool Thread::CheckSubThread(Thread *st) {
for (int i = 0; i < MaxNumSubThread; i++) {
if (subThreads[i] == st) {
return TRUE;
}
}
return FALSE;
}

状态码数组的相关方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Thread::SetSubExitStatus(Thread *st, int status) {
for (int i = 0; i < MaxNumSubThread; i++) {
if (subThreads[i] == st) {
subExitStatus[i] = status;
break;
}
}
}

int Thread::GetSubExitStatus(Thread *st) {
for (int i = 0; i < MaxNumSubThread; i++) {
if (subThreads[i] == st) {
return subExitStatus[i];
}
}
}

除了父子进程之外,还需要考虑用户地址空间的拷贝。对AddrSpace类,新增一个空构造函数,以及一个复制方法Duplicate

1
2
3
4
5
6
7
8
9
10
11
/*
* code/userprog/addrspace.h
*/
class AddrSpace {
public:
...
AddrSpace() {};
...
AddrSpace *Duplicate();
...
};

该复制方法Duplicate会拷贝当前用户空间的所有页,然后返回一个新的地址空间。在设计上,出于简便,直接采取了拷贝所有地址空间页的方式,而非Linux上延迟拷贝的方式。

根据lazy-loading的设计,SetUpNewOneEntry方法会向交换分区申请一页,然后将旧地址空间的对应页的信息全部复制到新页中。旧地址空间页需要分为仍在交换分区,以及被换入内存两类考虑。

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
void SetUpNewOneEntry(TranslationEntry &oldOne, TranslationEntry &newOne) {
newOne.virtualPage = oldOne.virtualPage;
newOne.valid = FALSE;
newOne.use = FALSE;
newOne.dirty = FALSE;
newOne.readOnly = FALSE;

int sp = machine->swapBitMap->Find(); ASSERT(sp != -1);
newOne.swapPage = sp;
printf("Sub Thread %s, swap memory page %d is allocated!\n", currentThread->getName(), sp);

if (oldOne.swapPage != -1) {
// 说明该页还在交换分区
char *buf = new char[PageSize];

machine->swapFile->ReadAt(buf, PageSize, oldOne.swapPage * PageSize);
machine->swapFile->WriteAt(buf, PageSize, sp * PageSize);

delete[] buf;
} else {
int ppn = oldOne.physicalPage;
int value;
char ch;

// 逐字节从内存中拷贝
for (int i = 0; i < PageSize; i++) {
while (!machine->ReadMem(ppn + i, 1, &value));
ch = (char)value;
machine->swapFile->WriteAt(&ch, 1, sp * PageSize + i);
}
}
}

AddrSpace * AddrSpace::Duplicate() {
AddrSpace * newAddrSpace = new AddrSpace();

newAddrSpace->numPages = numPages;
// newAddrSpace->pageTable = pageTable;
newAddrSpace->pageTable = new TranslationEntry[numPages];


printf("Duplicating User Space!\n");

for (int i = 0; i < numPages; i++) {
SetUpNewOneEntry(pageTable[i], newAddrSpace->pageTable[i]);
}

return newAddrSpace;
}

Exec

实现

首先创建一个类似于code/userprog/progtest.cc中的StartProcess的函数ForkAndExec,该函数被子线程调用Fork执行。

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
void ForkAndExec(char *filename) {
// 从文件系统打开可执行文件
OpenFile *executable = fileSystem->Open(filename);
AddrSpace *space2;

if (executable == NULL) {
printf("Unable to open file %s\n", filename);
delete[] filename;
return;
}

printf("Start to executing file %s\n", filename);
delete[] filename;

// 创建空间存储用户程序
space2 = new AddrSpace(executable);
currentThread->space = space2;

delete executable; // close file

space2->InitRegisters(); // set the initial register values
// 告诉设备的用户程序的页表和页数目
space2->RestoreState(); // load page table register


machine->Run(); // jump to the user progam
ASSERT(FALSE);
}

然后实现Exec系统调用。在该调用中,首先创建一个子线程exec_t,让子线程调用Fork来执行新的用户程序。同时,将子线程指针的值(SpaceId)作为返回值写入到寄存器中。

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
void SysExec() {
int base = machine->ReadRegister(4);
int value;
int count = 0;

char *name = new char[FileNameMaxLen + 1];
do {
while (!machine->ReadMem(base + count, 1, &value));
name[count] = (char)value;
count++;
} while (value != '\0' && count <= FileNameMaxLen + 1);


Thread *exec_t = currentThread->AddSubThread();
if (exec_t == NULL) {
printf("Create a subthread failed!\n");
}

machine->WriteRegister(2, (SpaceId)exec_t);
printf("Space Id => %d\n", (SpaceId)exec_t);

if (exec_t != NULL) {
exec_t->Fork(ForkAndExec, (void *)name);
}

}
测试

被Exec执行的程序do_exec如下,创建一个文件sc_exec_f。

1
2
3
4
5
6
int
main()
{
Create("sc_exec_f");
Exit(0);
}

执行Exec系统调用的程序如下:

1
2
3
4
5
6
int
main()
{
int ad = Exec("do_exec");
Exit(ad);
}

其测试等到Join和Exit实现之后再进行。

Exit

实现

其实现是在Nachos Lab02 虚拟内存中的基础上做了一些的修改。

Exit退出线程是还会判断,是否有父线程。如果有,则在父线程中设置自己的Exit状态码,然后删除自己。

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
void SysExit() {
int status = machine->ReadRegister(4);
DEBUG('t', "", status);
printf("Exit Code %d\n", status);

// 如果父线程存在,则从父线程的子线程列表中删除自己
if (currentThread->parThread != NULL) {
currentThread->parThread->lastSubExitStatus = status;
currentThread->parThread->DelSubThread(currentThread);
}

if (currentThread->space != NULL) {
for (unsigned int i = 0; i < machine->pageTableSize; i++) {
// 回收物理空间,并置页表项的valid值为FALSE

int phyPage = machine->pageTable[i].physicalPage;
DEBUG('m', "Physical memory page %d is cleared!\n", phyPage);
// printf(" %s, physical memory page %d is cleared!\n", currentThread->getName(), phyPage);
machine->memBitMap->Clear(phyPage);
}
// 回收space空间,执行进程Finish函数
delete currentThread->space;
currentThread->space = NULL;
}

currentThread->Finish();
}
测试

其执行效果,部分已在其他测试中出现。新增的部分,则等到Join的实现再测试。

Join

实现

通过调用CheckSubThread反复确认,子线程是否存在于子线程数组中。如果子线程运行结束则会调用Exit系统调用,将其在子线程数组中的位置设为NULL,从而导致确认的结果为FALSE;反之,则当前线程让出CPU,直至确认的结果为FALSE

1
2
3
4
5
6
7
8
9
10
void SysJoin() {
int base = machine->ReadRegister(4);
SpaceId id = (SpaceId)base;

while (currentThread->CheckSubThread((Thread *)id)) {
currentThread->Yield();
}

machine->WriteRegister(2, currentThread->lastSubExitStatus);
}
测试

首先创建一个名为exec_test的测试程序,该测试程序会调用Exec系统调用去执行程序”do_exec”,同时使用Join调用等待该子程序的执行结束。最后将子程序的状态码,用Exit返回。

1
2
3
4
5
6
7
int
main()
{
SpaceId ad = Exec("do_exec");
int status = Join(ad);
Exit(status);
}

然后创建测试程序do_exec,该程序会创建一个名为”st_c”的文件,并返回状态码1。

1
2
3
4
5
6
int
main()
{
Create("st_c");
Exit(1);
}

测试结果如下。注:此处的测试使用的是Unix文件系统。之所以不使用Nachos内的文件系统是因为文件同步机制实现的有点问题,导致子线程的调用fileSystem->Open时,执行不下去。

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
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x exec_test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
Thread main add a subthread
Space Id => 159160096
Start to executing file do_exec
Thread SubThread, swap memory page 0 is allocated!
Thread SubThread, swap memory page 1 is allocated!
Thread SubThread, swap memory page 2 is allocated!
Thread SubThread, swap memory page 10 is allocated!
Thread SubThread, swap memory page 11 is allocated!
Thread SubThread, swap memory page 12 is allocated!
Thread SubThread, swap memory page 13 is allocated!
Thread SubThread, swap memory page 14 is allocated!
Thread SubThread, swap memory page 15 is allocated!
Thread SubThread, swap memory page 16 is allocated!
Thread SubThread, swap memory page 17 is allocated!
Exit Code 272
Exit Code 272
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!

Ticks: total 105, idle 8, system 30, user 67
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

然后在本地可以看到st_c文件创建成功。

Fork

实现

由于Thread::Fork方法仅能传递一个参数,因此创建一个数据类型UserPC类,来辅助传参。UserPC仅记录两个信息,uPC(PC值),space(地址空间)。

1
2
3
4
5
class UserPC {
public:
int uPC;
AddrSpace * space;
};

然后实现Fork系统调用。Fork系统调用和Exec系统调用的主要区别在于,Fork类似于Pyhton中的多线程,而Exec则类似于Python中的多进程的概念。Fork用于创建一个新线程来处理当前程序的某个部分(函数);而Exec会创建一个新线程来执行一个新的程序。

因此Fork的实现步骤如下:

  1. 从寄存器中获取任务的PC值,存到base中
  2. 调用AddSubThread方法,创建当前线程的子线程exec_t
  3. 借助刚创建的UserPC类来记录,任务PC值(存在base中)和父线程地址空间的拷贝(调用Duplicate方法复制)
  4. 子线程exec_t调用SaveUserState方法,来为子线程设置寄存器状态。因为子线程相当于父线程的拷贝,并且子线程是从父线程中的某个点继续运行,所以子线程需要复制父线程的寄存器状态
  5. 子线程调用Thread::Fork方法来执行任务ForkFunc,并将UserPC对象传递进去。
  6. ForkFunc中,首先设置子线程的地址空间(来自于父线程地址空间的拷贝)
  7. ForkFunc中,然后修改当前PC寄存器的值,和NextPC寄存器的值。目的是为了让子线程从此处开始执行。
  8. ForkFunc中,最后调用Machine::Run来执行子线程。
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
void SysFork() {
int base = machine->ReadRegister(4);

printf("Forking...\n");
Thread *exec_t = currentThread->AddSubThread();
printf("1 => Create sub thread id %d\n", (SpaceId)exec_t);
UserPC *userPc = new UserPC;
userPc->uPC = base;
userPc->space = currentThread->space->Duplicate();
printf("2 => Duplicate old space %d to new space %d\n", (int)currentThread->space, (int)userPc->space);

printf("3 => Copy user's states\n");
exec_t->SaveUserState();

exec_t->Fork(ForkFunc, (int)userPc);
}

void ForkFunc(int which) {

UserPC *userPc = (UserPC *)which;

currentThread->space = userPc->space;

int uPC = userPc->uPC;
printf("4 => Setup PCReg to %d\n", uPC);
machine->WriteRegister(PCReg, uPC);
machine->WriteRegister(NextPCReg, uPC + 4);

printf("5 => All setting down, start to run\n");
machine->Run();
}
测试

编写测试函数如下,使用Fork来多线程执行调用函数Func。Func会创建文件”fork_t”。

1
2
3
4
5
6
7
8
9
void Func() {
Create("fork_t");
}

int
main()
{
Fork(Func);
}

测试结果如下:

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
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x fork_test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
Forking...
Thread main add a subthread
1 => Create sub thread id 148031264
Duplicating User Space!
Sub Thread main, swap memory page 0 is allocated!
Sub Thread main, swap memory page 1 is allocated!
Sub Thread main, swap memory page 2 is allocated!
Sub Thread main, swap memory page 10 is allocated!
Sub Thread main, swap memory page 11 is allocated!
Sub Thread main, swap memory page 12 is allocated!
Sub Thread main, swap memory page 13 is allocated!
Sub Thread main, swap memory page 14 is allocated!
Sub Thread main, swap memory page 15 is allocated!
Sub Thread main, swap memory page 16 is allocated!
Sub Thread main, swap memory page 17 is allocated!
2 => Duplicate old space 148031000 to new space 148031600
3 => Copy user's states
4 => Setup PCReg to 208
5 => All setting down, start to run
Exit Code 0
Exit Code 0
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!

Ticks: total 106, idle 17, system 30, user 59
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

从截图中可以看到文件fork_t被成功创建。

Yield

实现

此处有一点需要注意,需要先将PC增加,再调用Yield。否则就会导致进程被唤醒时,仍处于执行Yield调用的指令位置。这会导致程序反复执行Yield。因此在ExceptionHandler将其放在PC增加之后处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* code/userprog/exception.cc
*/
void
ExceptionHandler(ExceptionType which)
{
...
else if (type == SC_Fork) {
DEBUG('a', "Program Fork!\n");

SysFork();
}

// 将PC + 4,让系统调用处理完之后,用户程序执行后一条指令
machine->PCAdvance();

if (type == SC_Yield) {
DEBUG('a', "Program Yield!\n");

SysYield();
}
}
...

然后Yield调用实现比较简单,就是调用Thread::Yield方法。

1
2
3
4
5
void SysYield() {
printf("Thread %s yield...\n", currentThread->getName());

currentThread->Yield();
}
测试

首先创建测试程序do_yield。该程序会创建文件”yield_t”,然后调用Yield。被唤醒之后会打开文件yield_t。

1
2
3
4
5
6
7
8
9
int
main()
{ OpenFileId fd;
Create("yield_t");
Yield();
fd = Open("yield_t");
Close(fd);
Exit(fd);
}

然后创建测试程序yield_test。该程序会执行do_yield程序。

1
2
3
4
5
6
7
int
main()
{ SpaceId id;
id = Exec("do_yield");
Join(id);
Exit(0);
}

测试结果如下。可以看到线程main执行Join调用,等待子线程SubThread的执行。子线程执行任务直到调用Yield调用,切换回主线程main,main继续在Join中等待,再次切换到子线程继续执行。

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
root@02487b68b87e:/nachos/nachos-3.4/code/userprog# ./nachos -x yield_test
Thread main, swap memory page 0 is allocated!
Thread main, swap memory page 1 is allocated!
Thread main, swap memory page 2 is allocated!
Thread main, swap memory page 3 is allocated!
Thread main, swap memory page 4 is allocated!
Thread main, swap memory page 5 is allocated!
Thread main, swap memory page 6 is allocated!
Thread main, swap memory page 7 is allocated!
Thread main, swap memory page 8 is allocated!
Thread main, swap memory page 9 is allocated!
Thread main, swap memory page 10 is allocated!
Thread main add a subthread
Space Id => 165242656
Thread main do waiting
Start to executing file do_yield
Thread SubThread, swap memory page 0 is allocated!
Thread SubThread, swap memory page 1 is allocated!
Thread SubThread, swap memory page 2 is allocated!
Thread SubThread, swap memory page 10 is allocated!
Thread SubThread, swap memory page 11 is allocated!
Thread SubThread, swap memory page 12 is allocated!
Thread SubThread, swap memory page 13 is allocated!
Thread SubThread, swap memory page 14 is allocated!
Thread SubThread, swap memory page 15 is allocated!
Thread SubThread, swap memory page 16 is allocated!
Thread SubThread, swap memory page 17 is allocated!
Thread main do waiting
Thread SubThread yield...
Thread main do waiting
File yield_t => OpenFileId 165267856
File id 165267856 closed!
Exit Code 165267856
File id 165267856 closed!
Exit Code 165267856
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!

Ticks: total 193, idle 11, system 80, user 102
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Cleaning up...

可以看到子线程中的文件”yield_t”被成功创建。

Exercise 5 编写用户程序

编写并运行用户程序,调用练习4中所写系统调用,测试其正确性。

这一部分与Exercise 4放在一起,见Exercise 4的测试部分。

遇到的困难

参考资料

[1] 百度文库. Nachos系统调用实习报告[EB/OL]. https://wenku.baidu.com/view/cde42078aef8941ea66e0566.html

Author

Chaos Chen

Posted on

2021-01-02

Updated on

2023-06-30

Licensed under

Commentaires