🐚

自作シェル「sijimi」を作ってみた

2021/11/07に公開

目的

普段利用しているbashなどのシェルがどのようにコマンドを実行しているのかを知りたいと思い、「sijimi」を作成しました。

ソースコードは以下のリンクにあります。
https://github.com/higuruchi/sijimi

sijimiに実装されている機能

  • 任意のコマンドの実行
$ cd ./hoge
$ ls
  • パイプ
$ hoge | fuga
$ hoge | fuga | hogefuga
  • リダイレクト
$ fuga > hoge.txt

使用技術

  • Docker
  • gcc
  • make

コマンドの実行方法

シェルから異なるプログラムを実行するためにforkとexecの2つのシステムコールを使用します。

  • forkシステムコール
#include <unistd.h>
pid_t fork(void);

forkシステムコールは親プロセスを2つの同一なプロセスに増やし、新たなプロセスを生成するためのシステムコールです。親プロセスでは子プロセスのプロセスID、小プロセスでは0が返却されるため、親プロセスと小プロセスを区別することもできます。

  • execシステムコール
#include <unistd.h>
int execvp(const char *file, char *const argv[]);

execシステムコールは自身の内容を書き換えるためのシステムコールで、生成した子プロセスがexecシステムコールを使用することで、子プロセスで任意のプログラムを実行することができます。エラーが発生した時のみ復帰し、−1を返却します。
今回はexecvp関数を用いました。

仮にforkせずにexecを行なってしまった場合、シェルを実行しているプロセスの内容が実行されるコマンドになってしまい、そのまま終了してしまいます。

コマンドを実行するプログラムを以下のようになります

exec_line.c
int execute(ENV *env, line *command_line)
{
    int i, status, ret;
    pid_t pid, wpid;


    if (command_line == NULL) {
        fprintf(stderr, "Input Value Error\n");
        return 1;
    }
    
    pid = fork();

    if (pid == 0) {
        // child process
	// 子プロセスはコマンドを実行
        ret = launch(env, command_line, 0);
    } else if (pid > 0) {
        // parent process
	// 親プロセスは小プロセスが実行終了するまで待つ
        do {
            wpid = waitpid(pid, &status, WUNTRACED);
        } while(!WIFEXITED(status) && !WIFSIGNALED(status));
    }

    return ret;
}

パイプの実装方法

パイプを実装するために、子プロセスの標準出力を親プロセスの標準入力に繋がなければならないためpipeとdupの2つのシステムコールを使用します。

  • pipe関数
#include<unistd.h>
int pipe(int filedes[2]);

pipe関数はパイプを作成します。filedes[0]に読み込みディスクリプタ、filedes[1]に書き込みディスクリプタを作成し、filedes[1]に書き込んだデータをfiledes[0]から読み出すことができます。パイプを作成した後子プロセスを生成することによって、子プロセスと親プロセスでパイプを共有することができるためプロセス間通信をすることができます。

  • dup2関数
#include <unistd.h>
int dup2(int oldfd, int newfd);

ファイルディスクリプタoldfdを複製し、ファイルディスクリプタnewfdを新たなファイルディスクリプタとして利用することができます。そのため、newfdを用いてoldfdに書き込みをすることができます。

仮に以下のようなコマンドを実行した場合、コマンドプロセスの親子関係は以下の図のようになります。

$ hoge | hogefuga

パイプ処理を行う関数は以下のようになります。

int pipe_command(ENV *env, line *command_line, int th_of_command, char **block_array)
{
    pid_t pid;
    int status, pp[2] = {};

    pipe(pp);
    pid = fork();
    if (pid == 0) {
        // child process

        close(pp[0]);
    
        // 標準出力をパイプの書き込み口につなぐ
        dup2(pp[1], 1);
        close(pp[1]);
	
	// コマンド列を実行する関数を呼び出す
        launch(env, command_line->next_line, th_of_command+1);
    }
    if (pid < 0) {
        // fork error
        fprintf(stderr, "fork error\n");
    }
    if (pid > 0) {
        // parent process

        close(pp[1]);
        
        // 標準入力をパイプの読み出し口につなぐ       
        dup2(pp[0], 0);
        close(pp[0]);

        if (exec_builtin_cmd(env, block_array) >= 0) {
            fprintf(stderr, "execution error\n");
            return -1;
        }
        
        if (execvp(block_array[0], block_array) < 0) {
            fprintf(stderr, "execution error\n");
            return -1;
        }
    }
    return 1;
}

リダイレクトの実装方法

リダイレクトを実装するにはdupシステムコールを使用します。
パイプの際は子プロセスの標準出力と親プロセスお標準入力を生成したパイプを用いて繋ぎ直していたのですが、リダイレクト処理の場合は、リダイレクト先の任意のファイルを開き、その際得ることのできるファイルディスクリプタに繋ぎ直すことによってリダイレクトを実現しています。

リダイレクト処理を行う関数は以下のようになります。

int redirect(ENV *env, line *command_line, int th_of_command, char **block_array)
{
    int token_num, fd;
    char *filename;
    FILE *file;

    token_num = num_of_token(&command_line->blk);

    if (is_text(block_array[token_num-1]) == 0) {
        return -1;
    }

    filename = block_array[token_num-1];
    block_array[token_num-1] = NULL;

    if ((file = fopen(filename, "r"))) {
        if (remove(filename) != 0) {
            return -1;
        }
    }

    fd = open(filename, O_CREAT | O_RDWR, 0666);

    // オープンしたファイルのディスクリプたを標準出力に複製する
    dup2(fd, 1);
    close(fd);

    return 1;
}

GitHubで編集を提案

Discussion