Lab4: System Call - exec, fork and exit

1. 更改架構

為了方便 required 4 的要求先更改整體架構。

1-1. system call

仿效 glibc 使用巨集產生 privilege function 的 wrapper 提供給 user。以 printf 為例,在 user 可透過 uart_write system call 間接呼叫 _uart_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <include/syscall.h>

int putc(unsigned char c)
{
// Blocking write
// Write a character to ring buffer and return the character
// written as an unsigned char cast to an int.
if (c == '\n') {
while (1 != uart_write(&(char[1]){'\r'}, 1)) {
}
}
while (1 != uart_write(&(char[1]){c}, 1)) {
}
return (int) c;
}

int64_t sys_uart_write(void *buf, size_t size)
{
return (int64_t) _uart_write(buf, size);
}

但在 kernel 呼叫 printk 則可直接使用 _uart_write ,避開 system call 帶來的 overhead。

1
2
3
4
5
6
7
8
9
10
11
12
#include <include/syscall.h>

static inline int putc_k(unsigned char c)
{
if (c == '\n') {
while (1 != _uart_write(&(char[1]){'\r'}, 1)) {
}
}
while (1 != _uart_write(&(char[1]){c}, 1)) {
}
return (int) c;
}

巨集參考

1-2. object file 分離

仿造 sungyuanyao 的作法,將 source file 分成三部份放

  • kernel: 需要存取硬體相關操作例如 uart
  • user: 定義 user task
  • lib: 提供給 user 的函式庫例如printf

2. Syscall - exec()

2-1. system call definition

Glibc 是用 marco 去定義 system call wrapper,如果要定義 int64_t reset(uint64_t) ,會使用 SYSCALL_ARG1

1
2
3
4
5
6
7
8
9
#define SYSCALL_ARG1(name, ret_t, type0)                \
ret_t name(type0 x0) \
{ \
return INTERNAL_SYSCALL(name, 1, INPUT_ARGS_1); \
}

SYSCALL_ARG1(reset, int64_t, uint64_t)
| |
return value type arg0 type

Expands to:

1
2
3
int64_t reset(uint64_t x0) {
return INTERNAL_SYSCALL(reset, 1, INPUT_ARGS_1);
}

但定義 int64_t exec(((void *)())) 會出錯,原因在於exec 是帶有 function pointer 型態參數的函式:

1
2
3
4
5
SYSCALL_ARG1(reset, int64_t, ((void *)()))

// inline expansion
// error: expected declaration specifiers or ‘...’ before ‘(’ token
int64_t exec(((void *)())) { return INTERNAL_SYSCALL(exec, 1, INPUT_ARGS_1); }

遇到 exec 這種特例只得直接寫死:

1
int64_t exec(void (*x0)()){ return INTERNAL_SYSCALL(exec, 1, INPUT_ARGS_1); }

又或者使用 pointer to void 作為參數型態,傳遞 function pointer 進去?

1
2
3
4
5
int64_t exec(void *x0){
void (*f)() = x0;
}

ret = exec(&func);

先查看規格書的用法。截自 Cppreference - Pointer declaration,並沒有提到 function pointer 隱性轉換至 pointer to void。

Pointer declaration
Pointer is a type of an object that refers to a function or an object of another type, possibly adding qualifiers.

Pointers to void
Pointer to object of any type can be implicitly converted to pointer to void (optionally const or volatile-qualified), and vice versa:

  • If a pointer to object is converted to a pointer to void and back, its value compares equal to the original pointer.
  • No other guarantees are offered

但是透過以下程式碼實驗,是可以正常編譯執行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int i = 2;

void s(){
i++;
}

void a(void *f){
void (*r)() = f;
(*r)();
}

int main(){
a(&s);
return i;
}

但是開啟 -Wpedantic 跑出一堆警告

1
2
3
-Wpedantic
-pedantic
Issue all the warnings demanded by strict ISO C and ISO C++; reject all programs that use forbidden extensions, and some other programs that do not follow ISO C and ISO C++.
1
2
3
4
5
6
7
8
9
10
11
12
~$ gcc test.c -o test -Wall -std=c99 -Wpedantic
test.c: In function ‘a’:
test.c:8:23: warning: ISO C forbids initialization between function pointer and ‘void *’ [-Wpedantic]
8 | void (*r)() = f;
| ^
test.c: In function ‘main’:
test.c:13:11: warning: ISO C forbids passing argument 1 of ‘a’ between function pointer and ‘void *’ [-Wpedantic]
13 | a(&s);
| ^~
test.c:7:14: note: expected ‘void *’ but argument is of type ‘void (*)()’
7 | void a(void *f){
| ~~~~~~^

再次查閱規格書 p521, ISO/IEC 9899:1999 (E),發現 function pointer be cast to pointer to void 是屬於 extension

J.5 Common extensions

The following extensions are widely used in many systems, but are not portable to all
implementations.

J.5.7 Function pointer casts

A pointer to an object or to void may be cast to a pointer to a function, allowing data to
be invoked as a function (6.5.4).

A pointer to a function may be cast to a pointer to an object or to void, allowing a
function to be inspected or modified (for example, by a debugger) (6.5.4).

在有隱慮的情況下,還是放棄 pointer to void 的想法。

2-2. implementation

exec 目的在替換 TrapFrame 內的 elr_el1, sp,使其回到 user space 時執行給定的 function 與使用新的 stack pointer:

1
2
3
4
5
6
7
8
9
10
11
12
int64_t sys_exec(struct TrapFrame *tf)
{
const task_t *task = get_current();
void *ustack = get_ustacktop_by_id(task->tid);
/*
* User task will resume from 'func' not where to call
* exec and use new stack pointer.
*/
tf->elr_el1 = tf->x[0];
tf->sp = (uint64_t) ustack;
return 0;
}

3. Syscall - fork()

Kernel should allocate new task struct and kernel stack for child. Also, parent’s user context should be copied to child’s. But you should modify the trapframe to make the return value different.

3-1. stack pointer of exception handler

Exception handler 所使用的 stack pointer 是基於是最近使用的 EL1 stack pointer,那在 kernel/user task 發生 exception 時是基於誰的 stack pointer?列舉所有可能:

  1. kernel task 發生 exception 時,exception handler 所使用的 stack pointer 是屬於該 task 的 kernel stack。
  2. user task 是從 kernel task 執行 exec 切換到 EL0,因此在 user 發生 exception 切換至 EL1 時是使用屬於該 task 的 kernel stack。
  3. switch_to 在 EL1 載入 kernel stack,再跳轉回 kernel/user task

綜合上述,不論 exception 發生在 user 或 kernel,所使用的 stack pointer 就是當前 task 的 kernel stack。

好像講了一段廢話

3-2. implementation

實作借用了 sungyuanyao 的想法,不然想半天沒想法

在 exception handler,也就是 fork syscall 內

  1. 先用 privilege_task_create 取得 child process 的 task_struct
  2. 初始化 task_struct ,讓 child process 如同從 EL1 跳回 EL0 完成 syscall,因此我們需要
    1. task_context.lr 指向 ret_to_user,完成 kernel_exit 的最後一步
    2. kernel_exit 最後是還原 TrapFrame 到各個 register,task_context.sp 應該指向 TrapFrame
    3. TrapFrame 也需要初始化,就從 parent process 複製過來,唯一不同的是
      1. TrapFrame 內的 x0 = 0 (return value)
      2. TrapFrame 內的sp (sp_el1) 要指向 child process 的 user stack
  3. child process 的 user stack 從 parent process 複製過來
  4. 將 child process 加入 runqueue,並設為 TASK_RUNNABLE

比起文字,還是看圖快些。

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
         user                      user

stack top stack top
+------------+ +------------+
| | | |
| | copy | |
| | -------> | |
| | | |
+-->+------------+ +-->+------------+
| | | | | |
| | unused | | | unused |
| | | | | |
| +------------+ | +------------+
| |
| |
| |
| |
| |
| kernel | kernel
| |
| stack top | stack top
| +-------------+ | +-------------+ --+
| | | | | | |
| | something | +---+-- sp | |
| | else | | x0=0 | sizeof(TrapFrame)
| | | +--> | | |
| | | | | | |
| +-------------+ | +-------------+ --+
| | | ----+ | |
+---+-- sp | copy | |
| x0=6 | | |
| | | unused |
+-------------+ | |
| | | |
| unused | | |
| | | |
+-------------+ +-------------+

parent process child process

pid=5 pid=6

4. Syscall - exit()

將目前 task state 標記為 TASK_ZOMBIE ,並放入 zombie_list 等待 zombie reaper 來回收。

1
2
3
4
5
6
7
8
void do_exit()
{
task_t *cur = (task_t *) get_current();
cur->state = TASK_ZOMBIE;
list_add_tail(&cur->node, &zombie_list);
schedule();
__builtin_unreachable();
}

5. Zombie Reaper

zombie_list 為一 doubly linked list,專門放 TASK_ZOMBIE 的 task。zombie reaper 就是將 task state 重新標記為 TASK_UNUSED,這樣 privilege_task_create 就能再次取得該 task。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void zombie_reaper()
{
while (1) {
if (list_empty(&zombie_list)) {
schedule();
enable_irq();
} else {
/* free zombie task's resource */
task_t *zt, *tmp;
list_for_each_entry_safe(zt, tmp, &zombie_list, node)
{
list_del_init(&zt->node);
zt->state = TASK_UNUSED;
printk(
"[PID %d] Zombie reaper frees process [PID %d] resources\n",
do_get_taskid(), zt->tid);
}
}
}
}

6. test case

6-1. For required 1 and required 2

6-2. For required 3 and required 4

因為 timer 頻率較低的關係,執行到 task id=3 剛好觸發 rescheduling 所以切換到 task id=4