コマンドの実行
本日の内容
OSの基礎知識
Shell自作
今日作るプログラムはrcsh.cとします.
shellって何?という人は端末のページを読んでください. rcsh.cの基本的なプログラムの流れは以下のようになります.
無限ループ
コマンドの入力を受け付ける
子プロセスを発行する
子プロセスはコマンドをexecする.
親プロセスはコマンドの終了を待機する.
プロセスというOSの単語が出てきたので解説していきます.
OSの基礎知識
Shell自作
ここから,いよいよCプログラムを書いていきます.
まずは特定のコマンドを子プロセスをforkしてexecさせるプログラムを書きましょう.
ここではsleep 5を実行してみましょう.
C言語の標準ライブラリではシステムコールのラッパー関数が含まれているため通常の関数呼び出しによってシステムコールを発行することができます.
使う関数
pid_t fork()
プロセスをフォークして子プロセスを生成する.
返り値はPIDである.PIDは親プロセス側では子プロセスのPIDが,子プロセス側では0が返る.
(補足)pid_t型はsys/types.hに含まれる
int execv(char *path, char* argv[)]
execシステムコールを発行し,新しいプロセスイメージに置き換わる.
pathには実行するバイナリを,argvには引数を指定する(NULLポインタで終了する必要がある).
execには様々なフロントエンドがある.他の関数(execl, execvpなど)についてはman 3 execなどを参照.
execはプロセスイメージを置き換えるため返り値は本来返らないはずである.エラーが発生した場合は-1が返り,errnoに内容が設定される.
pid_t wait(int *status)
子プロセスのいずれかが終了するまで待機する.
SIGCHILDを受け取ったら子プロセスのリソースを解放し,statusに状態情報を格納する.状態情報はマクロによって内容を知ることができる.マクロの情報はman 2 waitを参照
返り値は終了した子プロセスのPIDとなる.エラーの場合-1を返す.
code:rcsh.c
#include <unistd.h> // fork, exec関数が含まれる int main(void) {
char* cmd[] = {"/bin/sleep", "5", NULL};
int pid = fork(); // 本来はintではなくsys/types.hのpid_tを用いる
if(pid == 0) {
puts("I am child proc.");
exit(0);
}
int status;
wait(&status);
printf("Child process exited with status %d\n", WEXITSTATUS(status));
return 0;
}
今度はコマンドを入力から受け取れるようにしましょう.
入力した文字列を空白などで区切って「文字列の配列」として作り直す必要があります.
標準入力からコマンドを読みパースして返す関数read_cmdの実装例を解説します.
code:read_cmd.c
char **read_cmd(int *argc) {
const int BUFSIZE = 1024;
const char TOK_DELIM[] = " \t\r\n\a";
int arg_cnt = 0, max_argc = 10;
char *input = malloc(sizeof(char) * BUFSIZE);
char **argv = malloc(max_argc * sizeof(char *));
// 標準入力からコマンドを読む
printf("rcsh> ");
if (fgets(input, BUFSIZE, stdin) == NULL) {
return NULL;
}
// パース処理
char *word = strtok(input, TOK_DELIM);
while ((word = strtok(NULL, TOK_DELIM))) {
}
*argc = arg_cnt;
return argv;
}
見慣れない関数strtokが登場しました.この関数は文字列を文字列の配列に切り分けたいときに使う関数で,1回呼び出すごとに1単語ずつ切り分けていきます.1回目に呼び出したときと2回目以降に呼び出したときで呼び出し方が少し変わるので注意が必要です.
1回目に呼び出すときは切り分けたい文字列inputと区切り文字の配列TOK_DELIMを引数として与えます.
2回目以降にはinputを与える必要がありません.
code:strtok_example.c
char input[] = "foo bar bal";
char TOK_DELIM = " \n"; // {' ', '\n'}という宣言と同じ意味であることに注意
char *word = strtok(input, TOK_DELIM);
// ここで,wordはinputの'f'のアドレスを指すポインタであるが,strtokがfooの次の文字' 'を'\0'に書き換えているためwordは文字列"foo"として解釈することができる
puts(word); // => foo
word = strtok(NULL, TOK_DELIM);
// 現在のinputは "foo\0bar\0bal"となっており,wordは"bar"の'b'を指す
puts(word); // => bar
word = strtok(NULL, TOK_DELIM);
puts(word); // => bal
// 返す単語が無くなるとNULLを返す
if(strtok(NULL, TOK_DELIM) == NULL) {
puts("parse done");
}
基本的にread_cmd関数はこのstrtokを用いて文字ポインタ配列argvに各単語の開始アドレスを格納しているだけです.mallocでメモリを確保しているので解放しなければなりませんが文字列と文字ポインタ配列を作っているのでそれぞれ解放する必要があります.関数を書いておくといいかもしれません.
code:read_cmd.c
void free_argv(char **argv) {
free(*argv); // argv0を解放して標準入力からコピーした文字列データを消去 free(argv); // 文字列データ内の各単語開始位置を指していたポインタ配列を消去
}
read_cmd関数を使うと最初char* cmd[] = {"sleep" "5"};と宣言していた部分をそのまま置き換えられます.
code:rcsh.c
#include <unistd.h> // fork, exec関数が含まれる int main(void) {
int argc;
char* cmd[] = read_cmd(&argc);
int pid = fork(); // 本来はintではなくsys/types.hのpid_tを用いる
if(pid == 0) {
puts("I am child proc.");
exit(0);
}
int status;
wait(&status);
printf("Child process exited with status %d\n", WEXITSTATUS(status));
return 0;
}