旅する情報系大学院生

旅と留学とプログラミング

shellを作っている話(かきかけ)

この記事は、IS17er Advent Calendar 2016 - Adventarの三日目の記事として書かれました。
まだ自分の満足する完成には程遠いので、永遠にかきかけです。

とりあえず、ソースコード。
GitHub - yamaguchi1024/shell

概要

大学の課題として、@cookies146と一緒に作っています。役割分担は、私が機能の実装などでcookiesが構成を考えたりライブラリを作ったりです。
サポートしている機能は下記の通り。
・コマンドの実行
・組み込みコマンドの実行
・キーボードからのシグナルハンドル
・リダイレクト
・パイプ
・上矢印を押すと履歴を表示する
・qでシェルを終了できる
・2秒以上時間のかかるファイルを実行した時に、実行してからの経過時間とCPU使用率、仮想メモリ使用量、実際のメモリ使用量、最大メモリ使用量が表示される。

いつかはジョブ管理も実装したいなーと思っています。また、CPUとメモリ使用量が表示される機能は使いたいと言ってくれる方がいるので、シェルとは別に切り出して別のコマンドか何かにできたらいいなと思っています。
目標は、自分の作ったCPU上で自分の作ったOSが動きその上で自分の作ったシェルが動きその上で自分の作ったママ言語が動くことです。

特に苦労した(している)上矢印の履歴とCPU・メモリ使用量の機能について語ります。

上矢印

getlineやgetcでstdinからの入力を読み込んだ場合、改行が入力されるまでバッファにたまってしまうので上矢印だけで履歴を表示するためには別の方法を考える必要がありました。
そこでalpha改さんに教えてもらったのがkbhitという関数(below)です。

int kbhit(void)
{
    struct termios oldt, newt;
    int ch; 
    int oldf;

    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    newt.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &newt);
    oldf = fcntl(STDIN_FILENO, F_GETFL, 0); 
    fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);

    ch = getchar();

    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
    fcntl(STDIN_FILENO, F_SETFL, oldf);

    if (ch != EOF) {
        ungetc(ch, stdin);
        return 1;
    }                                                                                                                                                                     

    return 0;
}

キーボードからの入力(というよりはキーボードが押されると)trueが帰ってくるので、これで何かしらの入力があったときに検知することはできます。
上矢印の制御文字は\x1B\x5B\x41なので、getcharして\x1Bなら表示はしないがそれ以外なら普通の入力なのでungetcしてからgetlineするということをしています。なぜわざわざungetcしているかというと、そうしないと最初の一文字だけ見えなくなってしまうからです。

        while(1){
            if(kbhit()){
                c=getchar();
                if(c=='\x1B'){
                    c=getchar();
                    c=getchar();
                    oprand="\x1B\x5b";
                    oprand+=(char)c;
                    break;}
                else{
                    ungetc(c,stdin);
                    printf("%c",c);
                    std::getline(std::cin,oprand);
                    break;
                }
            }       
        }           

また、履歴を保存するファイルが必要なので、入力が上矢印以外だったら.shrcに書き込んで、シェルを閉じるときにunlinkしています。

上矢印が押された時の処理は、基本的には.shrcを開いて下から上矢印を押された分だけ表示するだけですが、上矢印を押すのをやめて\nを押したか、あるいは上矢印を押し続けているかという状態をmain関数に渡すと厄介だったので、\nが押されるまでuparrow関数の中にいます。(長いので貼るのは割愛)


CPU・メモリ使用量の表示どこからこの情報を取ってくるかで悩みました。
結局/proc/[id]/statやstatusから取ってきているのですが、forkした子プロセスのidを元にファイルを探しているので、例えばa.outのなかで別のフプロセスb.outが呼び出された時はどうしようもありません。しかし、topコマンドも/procから読んでるし他にいい方法はなさそうです。

CPU使用率は/proc/[id]/statのutime,stime(14,15列目)を足した値/ /proc/statのCPUの稼働時間を表している一行目の各列を足した値という式で求めていて、メモリ使用量は/proc/[id]/statusのVmPeak,VmSize,VmRSSの行を出力しています。

最も苦労しているのが、親プロセスの中で繰り返し何秒化ごとに処理を実行する中で、子プロセスが死んだことをどのように監視するかです。
普通親プロセスが子プロセスを監視するときにはwaitとかwaitpidを使いますが、waitpidのオプションWNOHANGで子プロセスが終了していない時にもWIFEXITEDが0になるという問題がありダメで、シグナルハンドラを使ってなんとか動くようになりました(エラーは出るけど)

時間やばいのでとりあえず更新します。