自制操作系统 - 系统交互shell

系统交互shell

Posted by 王富杰 on Thursday, March 7, 2024

一、shell介绍

shell是用户与操作系统进行交互的媒介。目前为止我们已经实现了大部分的系统调用,因此可以来实现一个简单的shell来进行与操作系统的交互。初步的shell将完成一些简单的命令如 ls cd pwd rm cat 等。 实现这些命令还需要先实现几个系统调用。

二、系统调用readdir和clear

readdir是用来读取目录,它的代码如下:

int sys_readdir(fd_t fd, dirent_t *dir, u32 count)
{
    return sys_read(fd, (char *)dir, sizeof(dirent_t));
}

如上所示,读取目录直接调用了文件读这个系统调用。当读取目录时,一次读取一个目录项,这样实现比较简单。假设目录包含以下文件:file1.txt。 一次读取一个目录项结构,通过这个结构的name属性就可以获取目录项的名字。

clear这个功能我们在做显卡驱动的时候就已经实现过 cosolse_clear。 直接把这个函数注册为clear的系统调用即可。

三、shell实现

shell的实现大致逻辑是,设置命令提示符等基础环境和按行读取键盘输入,然后解析键盘输入,进行命令的执行。我们先看shell的主函数,然后通过主函数再具体的实现:

#define MAX_CMD_LEN 256
#define MAX_ARG_NR 16
#define MAX_PATH_LEN 1024
#define BUFLEN 1024
static char cwd[MAX_PATH_LEN];
static char cmd[MAX_CMD_LEN];
static char *argv[MAX_ARG_NR];
static char buf[BUFLEN];

int osh_main()
{
    memset(cmd, 0, sizeof(cmd));   // 初始化命令为0
    memset(cwd, 0, sizeof(cwd));   // 初始化当前目录为0
    builtin_logo();                // 打印系统logo,其实就是在终端打印了字符串。

    while (true)
    {
        print_prompt();             // 打印命令提示符,获取当前目录。
        readline(cmd, sizeof(cmd));  // 读取一行输入,并赋值给cmd
        if (cmd[0] == 0)
        {
            continue;
        }
        int argc = cmd_parse(cmd, argv, ' ');  // 把读取的指令按照空格切分为多个token, 因为有些命令是有参数的
        if (argc < 0 || argc >= MAX_ARG_NR)
        {
            continue;
        }
        execute(argc, argv);  // 执行执行
    }
    return 0;
}

从上我们可以看到,shell的主函数就是循环读取键盘输入,一次读取一行。然后将读取到的命令交给execute函数来处理。接下来我们看execute函数的实现:

static void execute(int argc, char *argv[])
{
    char *line = argv[0];
    ....
    if (!strcmp(line, "pwd"))
    {
        return builtin_pwd();
    }
    if (!strcmp(line, "exit"))
    {
        int code = 0;
        if (argc == 2)
        {
            code = atoi(argv[1]);
        }
        exit(code);
    }
    
    printf("osh: command not found: %s\n", argv[0]);
}

如上为execute,可以看到,execute将会根据命令比对来决定去执行那个函数,这个我们为了节省篇幅删除了大部分判断,例如当命令为 pwd 时,就去执行builtin_pwd函数。那接下来就是命令的具体实现。因为命令还是比较多的,我们这里只列举几个:

oid builtin_pwd()   // 获取当前目录
{
    getcwd(cwd, MAX_PATH_LEN);
    printf("%s\n", cwd);
}

void builtin_clear()   // 清屏,直接调用clear系统调用
{
    clear();
}

void builtin_ls()  // ls命令
{
    fd_t fd = open(cwd, O_RDONLY, 0);
    if (fd == EOF)
        return;
    lseek(fd, 0, SEEK_SET);
    dentry_t entry;
    while (true)
    {
        int len = readdir(fd, &entry, 1);
        if (len == EOF)
            break;
        if (!entry.nr)
            continue;
        if (!strcmp(entry.name, ".") || !strcmp(entry.name, ".."))
        {
            continue;
        }
        printf("%s ", entry.name);
    }
    printf("\n");
    close(fd);
}

如上所示,写好命令的实现之后,shell基本就做好了。接下来就需要再init进程中启动shell就可以了。

四、启动shell

shell实现之后,就需要再用户init进程中启动shell了。如下所示:

static void user_init_thread()
{
    while (true)  // 这个线程一直运行不会退出
    {
        u32 status;
        pid_t pid = fork();   // 2. 创建一个子进程
        if (pid)
        {
            pid_t child = waitpid(pid, &status);     // 等待刚才 fork 出的子进程结束
            printf("wait pid %d status %d %d\n", child, status, time());
        }
        else
        {
            osh_main();   // 子进程执行用户shell
        }
    }
}

父进程会waitpid阻塞自己,一直等到子进程结束。这个流程就是fork 一个子进程 → 子进程运行 shell → 父进程等待子进程结束 →子进程结束后父进程打印退出信息 → 重新 fork 启动新的 shell。这样,用户每次退出 shell,系统都会自动重新启动一个新的 shell,死循环来保证有一个用户界面一直存在。

「真诚赞赏,手留余香」

WangFuJie Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付