意識低い系会社員

意識低い系会社員の日常

【スポンサーリンク】

ふつうのLinuxプログラミング 第2版の内容メモ(5章)

【スポンサーリンク】

 

こんにちは。

 

最近、ふつうのLinuxプログラミング 第2版を読んでいるので知識の定着のために学んだ内容を要約したメモを書きます。

このエントリは完全な個人のメモです。

 

お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。

 

1章~4章の内容はこちら。

amistad06-a.hatenablog.com

 

第5章 ストリームにかかわるシステムコール

この章で説明すること

Linuxのストリームに関する主なシステムコールは以下の4つ。

read

write

open

close

この4つのシステムコールだけでLinux(UNIX)のストリームはほぼOK。

 

ファイルディスクリプタ

プログラムからストリームを扱うためにはファイルディスクリプタが必要。

ファイルディスクリプタの中身はただの整数(int)で、カーネルが持つストリームに対応する番号が割り振られる。

 

標準入力、標準出力、標準エラー出力

シェルからプロセスが起動した場合、どのプロセスにも以下の3つのストリームとそれに対応するファイルディスクリプタが用意されている。

・標準入力

・標準出力

・標準エラー出力

それぞれに対応するマクロは以下。

標準入力:STDIN_FILENO

標準出力:STDOUT_FILENO

標準エラー出力:STDERR_FILENO

パイプやリダイレクトなどで標準入力や標準出力を使うことで様々なコマンドを組み合わせて複雑な操作を行うことができる。

標準エラーメッセージは、エラーメッセージの出力先を標準出力と変えることにより、パイプやリダイレクトをしているときにユーザ(人間)がエラーに気づきやすいようになっている。

 

 

read(2)

ストリームからバイト列を読み込む。

バイト列を読み込むためのバッファは使う人が自分で用意しなければいけない。

 

write(2)

ストリームにバイト列を書き込む。

 

open(2)

ファイルに接続されたストリームを作り、そのストリームに対応するファイルディスクリプタを返す。

 

close(2)

ストリームを閉じる。

使い終わったストリームは必ず閉じること。

 

catコマンドを作る

この4つのシステムコールを使用してlinuxコマンドのcat(オプション無し)を作ってみる。

catはconCATenate(連結する)が由来。

ソースコードは以下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

static void do_cat(const char *path);
static void die(const char *s);

int main(int argc, char *arg[]){
    int i;

    if(argc < 2){
        fprintf(stderr, "%s: file name not given\n", arg[0]);
        exit(1);
    }

    for(i = 1; i< argc; i++){
        do_cat(arg[i]);
    }
    exit(0);
}

#define BUFFER_SIZE 2048

static void do_cat(const char *path){
    int fd;
    unsigned char buf[BUFFER_SIZE];
    int n;

    fd = open(path, O_RDONLY);
    if(fd < 0) die(path);
    for(;;){
        n = read(fd, buf, sizeof buf);
        if(n < 0) die(path);
        if(n == 0) break;
        if(write(STDOUT_FILENO, buf, n) < 0) die(path);
    }
    if(close(fd) < 0) die(path);
}

static void die(const char *s){
    perror(s);
    exit(1);
}

詳細な処理の説明は割愛。 

簡単な流れは以下。

  1. 引数無しでコマンドを打たれた場合、エラーメッセージを出力して終了
  2. 引数で渡されたファイルに対して以下の処理を繰り返す
  3. ストリーム読み書きのためのバッファを確保
  4. ファイルに繋がるストリームを開く(open)
  5. ストリームからバイト列を読み込む(read)
  6. 標準出力に読み込んだバイト列を書き込む(write)
  7. ストリームを閉じる(close)

 

errno変数について

一般的に、システムコールが失敗したときはerrnoに失敗要因に対応図けられた定数が記憶される。

ログ出力やデバッグに必要なので覚えておくとよい。

errnoの値はman参照。

errnoに対応するエラーを知りたいときは以下のファイルを参照。

/usr/include/asm-generic/errno-base.h

または

/usr/include/asm-generic/errno.h

 

perror(3)

errnoに対応するエラーメッセージを標準エラー出力に出す。

errnoとセットで覚えておくとよい。

 

strerror(3)

errnoに対応したエラーメッセージを返す。

エラーメッセージの先頭ポインタが返されるが、次のstrerror()呼び出しのタイミングで参照先が書き換わるので戻り値はすぐに使用する。

 

ファイルオフセット

ストリームを開いて、read()を繰り返したらread()は必ず前回の続きが返ってくる。

Linux内部ではどこまでストリームを読んだのかが記憶されていて、その位置のことをファイルオフセットという。

 

lseek(2)

ファイルディスクリプタ内部のファイルオフセットを指定した場所に移す。

 

dup(2), dup2(2)

ファイルディスクリプタを複製する。

dupはduplicateが由来。

 

ioctl(2)

ストリームに繋がるデバイスに特化した操作をすべて含めたシステムコール。

read, writeではできないものの寄せ集め。

例えば以下のような操作。

・光学ドライブのトレイの開け閉め、ディスクの再生

・プリンタの駆動や一時停止

など。

プリンタのプロパティ(両面印刷、サイズ、カラーの指定など)とか印刷の開始とか。

ドライバを作成する人が自由に定義可能な命令。

ユーザーランドとカーネルランドのやり取り。

 

fcntl(2)

ioctlの中からファイルディスクリプタの操作を分離したもの。

ファイルディスクリプタの複製や、状態フラグの書き換えなどができる。

 

演習問題

1. この章で作ったcatコマンドを改良して、コマンドライン引数でファイル名が渡されなかったら標準入力を読むように変更する。

サポートサイトの解説を見る前に書いたソースコードを載せます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFFER_SIZE 2048

static void do_cat(const char *path);
static void die(const char *s);

int main(int argc, char *arg[]){
    int i;
    int n;
    unsigned char buf[BUFFER_SIZE];

    if(argc < 2){
        for(;;){
            if(read(STDIN_FILENO, buf, sizeof buf)) exit(1);
            if(n == 0) exit(0);
            if(write(STDOUT_FILENO, buf, sizeof buf) < 0 ) exit(1);
         }
    }

    for(i = 1; i< argc; i++){
        do_cat(arg[i]);
    }
    exit(0);
}

static void do_cat(const char *path){
    int fd;
    unsigned char buf[BUFFER_SIZE];
    int n;

    fd = open(path, O_RDONLY);
    if(fd < 0) die(path);
    for(;;){
        n = read(fd, buf, sizeof buf);
        if(n < 0) die(path);
        if(n == 0) break;
        if(write(STDOUT_FILENO, buf, n) < 0) die(path);
    }
    if(close(fd) < 0) die(path);
}

static void die(const char *s){
    perror(s);
    exit(1);
}

 

細かい説明は割愛。

正しい実装方法と解説が見たい方は書籍のサポートサイトを参照してください。

 

会社の先輩によるレビューコメント

・同じコードが見受けられるので、なんとかまとめてください。

・-Wall オプションを付けてコンパイルしたら警告が出るので直してください。(致命的な欠陥です。)

 

2. バッファの中の'\n'を数えてファイルの行数を出力するコマンドを作る(wc -lと同じ機能)

こちらも解説を見る前に書いたソースコードを載せます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFFER_SIZE 2048

static int count_lines(const char *path);
static void die(const char *s);

int main(int argc, char *arg[]){
    if(argc < 2){
        fprintf(stderr, "%s: file name not given\n", arg[0]);
        exit(1);
    }

    if(argc > 2) printf("%s\n", "count lines only first file");

    printf("%d\n", count_lines(arg[1]));
    exit(0);
}

static int count_lines(const char *path){
    int fd;
    unsigned char buf[BUFFER_SIZE];
    int n;
    int lines = 0;

    fd = open(path, O_RDONLY);
    if(fd < 0) die(path);
    for(;;){
        n = read(fd, buf, sizeof buf);
        if(n < 0) die(path);
        if(n == 0) break;
        for(int i = 0; i < n; i++){
            if(buf[i] == '\n') lines++;
        }
    }
    if(close(fd) < 0) die(path);
    return lines;
}

static void die(const char *s){
    perror(s);
    exit(1);
}

 

細かい説明は割愛。

正しい実装方法と解説が見たい方は書籍のサポートサイトを参照してください。

 

会社の先輩によるレビューコメント

strchr()とかを使ってみてもいいのでは。

・sizeofは複数個所で使用しているのでBUFFER_SIZEにした方が少しだけ処理が早くなるのでは。

・(強いて言えば)変数宣言をピラミッド型とかにした方が見た目が美しい。

・同じ変数に対してif分が2回以上連続で続くときはelse ifで書けないか検討してください(今回のケースでは該当しないが、その方が早いケースがある)。

 

第5章から実際のプログラミングを交えながらの内容になるので読むペースはかなり落ちます。

このエントリも長くなってしまったので、5章だけで一旦終わろうと思います。

 

第6章の内容はこちら。

amistad06-a.hatenablog.com

 

おわり。

 

【スポンサーリンク】