《ORANGE-s-一个操作系统实现》内存管理(8)

fork

认识fork

生成一个子进程的系统调用被成为fork(),操作系统接到一个fork请求后,会将调用者复制一份,这时就会有两个一模一样的进程同时进行。其中子进程是从父进程得到数据、堆栈以及代码而来的。
我们最先要解决的问题是,谁作为最开始的父进程?参考Linux以及Minix可以知道是init进程,所以我们先写出Init进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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");

int pid = fork();
if (pid != 0) { /* parent process */
printf("parent is running, child pid:%d\n", pid);
spin("parent");
}
else { /* child process */
printf("child is running, pid:%d\n", getpid());
spin("child");
}
}

可以看到其中调用了即将实现的fork(),而且判定了返回值。如果返回0则表明自己是子进程,否则返回的是子进程的pid并表明自己是父进程。
然后我们要增加MM进程,它将负责从用户进程接受消息。
下图是一个进程涉及的所有数据结构以及相互联系:
mark

fork前要做好的工作

  • 在proc_table[]中预留出一些空白项,供新进程使用
  • 在proc_table[]中的每一个进程表项中的idt_sel项都设定好
  • 将进程所需的GDT表项都初始化好

    fork()库函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    PUBLIC int fork()
    {
    MESSAGE msg;
    msg.type = FORK;

    send_recv(BOTH, TASK_MM, &msg);
    assert(msg.type == SYSCALL_RET);
    assert(msg.RETVAL == 0);

    return msg.PID;
    }

MM

和文件管理进程一样,MM要有一个主消息循环,如下:

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
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);
}
}
}

当MM接到FORK消息后,调用do_fork()来处理。在内存的分配上,我们采用固定内存的方式,每个内存块大小为1MB。
fork()系统调用的结果余下:
mark

exit和wait

生成子进程最重要的是fork(),而进程的消亡则是用到系统调用exit()。
而系统调用wait()是父进程得到返回值的方法,用该系统调用挂起,等子进程退出时,wait()调用方结束,并且父进程因此得带返回值。

1
2
3
4
5
6
7
8
9
10
11
int pid = fork();
if (pid != 0) { /* parent process */
printf("parent is running, child pid:%d\n", pid);
int s;
int child = wait(&s);
printf("child (%d) exited with status: %d.\n", child, s);
}
else { /* child process */
printf("child is running, pid:%d\n", getpid());
exit(123);
}

和fork()类似,上述两个系统调用同样是发送消息给MM,它们发送的消息分别是EXIT和WAIT。并由MMM中对应的消息函数进行处理。
do_exit/do_wait和msg_send/msg_receive这两对函数是类似的例如,假设进程P有子进程。而A调用exit(),那么MM会:

  1. 告诉FS:A退出,请做出相应处理
  2. 释放A占用的内存
  3. 判断P是否正在WAITING
  4. 遍历proc_table[]
    如果P调用wait(),那么MM将会:
  5. 遍历proc_table[]。
  6. 如果P的子进程没有一个在HANGING,则设P的WITING位
  7. 如果P没有子进程,则向P发送消息,消息携带一个表示出错的返回值。
    完成了这以后,子进程的产生和消亡都有了,运行一下,结果如图:
    mark

    exec

    认识exec

    exec的语义很简单,它将当前的进程映像替换成另一个。也就是说我们可以从硬盘上读取另一个可执行的文件,用它替换掉刚刚被fork出来的子进程,于是被替换的子进程就成为了新进程。
    下面的exec()的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int pid = fork();
    if (pid != 0) { /* parent process */
    printf("parent is running, child pid:%d\n", pid);
    int s;
    int child = wait(&s);
    printf("child (%d) exited with status: %d.\n", child, s);
    }
    else { /* child process */
    execl("/echo", "echo", "hello", "world", 0);
    }

为自己的操作系统编写应用程序

例如Linux中的echo,它和操作系统的接口是系统调用。本质上,一个应用程序只能调用两种东西:属于自己的函数,以及中断。写一个echo最笨的方法就是将send_recv()、printf()、write()等所有用到的系统调用的代码都复制到源文件中,然后编译下。而更好的做法是制作一个类似C运行时库的东西,我们把之前已经写好的应用程序可以使用的库函数单独链接成一个文件,每次写应用程序时的时候直接链接起来就好了。
到目前位置,可以被用来链接成库的文件及其包含的主要函数有这些:

  • 真正的系统调用:sendrec和printx:lib/syscall.asm
  • 字符串操作:memcpy、memset、strcpy、strlen:lib/string.asm
  • FS的接口:lib/open.c lib/read.c lib/write.c lib/close.c lib/unlink.c
  • MM的接口:lib/fork.c lib/exit.c lib/wait.c
  • SYS的接口:lib/getpid.c
  • 其他:lib/misc.c lib/vsprintf.c lib/printf.c
    把这些函数单独链接成一个库,起名为orangescrt.a表明这是我们的C运行时库.
    现在先写一个最简单的echo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include "stdio.h"

    int main(int argc, char * argv[])
    {
    int i;
    for (i = 1; i < argc; i++)
    printf("%s%s", i == 1 ? "" : " ", argv[i]);
    printf("\n");

    return 0;
    }

编译链接:

1
2
3
gcc -I ../include/ -c -fno-builtin -Wall -o echo.o echo.c
nasm -I ../include/ -f elf -o start.o start.asm
ld -Ttext 0x1000 -o echo echo.o start.o ../lib/orangescrt.a

安装应用程序

安装程序到我们的文件系统中,需要做以下工作:

  • 编程应用程序,并编译链接
  • 将链接好的应用程序打成一个tar包:inst.tar
  • 将inst.tar用工具dd写入磁盘的某段特定扇区
  • 启动系统,这时mkfs()会在文件系统中建立一个新文件cmd.tar,它的inode中的i_start_sect成员会被设为X
  • 在某个进程中将cmd.tar解包,将其中包含的文件存入文件系统。
    运行结果如下:
    mark
    mark

    简单的shell

    shell可以很复杂,我们这里是实现读取命令并执行:
    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
    void shabby_shell(const char * tty_name)
    {
    int fd_stdin = open(tty_name, O_RDWR);
    assert(fd_stdin == 0);
    int fd_stdout = open(tty_name, O_RDWR);
    assert(fd_stdout == 1);

    char rdbuf[128];

    while (1) {
    write(1, "$ ", 2);
    int r = read(0, rdbuf, 70);
    rdbuf[r] = 0;

    int argc = 0;
    char * argv[PROC_ORIGIN_STACK];
    char * p = rdbuf;
    char * s;
    int word = 0;
    char ch;
    do {
    ch = *p;
    if (*p != ' ' && *p != 0 && !word) {
    s = p;
    word = 1;
    }
    if ((*p == ' ' || *p == 0) && word) {
    word = 0;
    argv[argc++] = s;
    *p = 0;
    }
    p++;
    } while(ch);
    argv[argc] = 0;

    int fd = open(argv[0], O_RDWR);
    if (fd == -1) {
    if (rdbuf[0]) {
    write(1, "{", 1);
    write(1, rdbuf, r);
    write(1, "}\n", 2);
    }
    }
    else {
    close(fd);
    int pid = fork();
    if (pid != 0) { /* parent */
    int s;
    wait(&s);
    }
    else { /* child */
    execv(argv[0], argv);
    }
    }
    }

    close(1);
    close(0);
    }

    /*****************************************************************************
    * Init
    *****************************************************************************/
    /**
    * The hen.
    *
    *****************************************************************************/
    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);

    shabby_shell(tty_list[i]);
    assert(0);
    }
    }

    while (1) {
    int s;
    int child = wait(&s);
    printf("child (%d) exited with status: %d.\n", child, s);
    }

    assert(0);
    }

运行结果如下:

mark
PS:不知道为何在虚拟机下Ubuntu+Bochs就无法在Bochs里使用CTRL+Fn键!!!!

您的支持将鼓励我继续创作!