Linuxシステムコールの勉強(その7)

Linuxシステムコール

Linuxシステムコール

前回はこちら


今回は低水準ファイル入出力のまとめとして、効率的なファイル入出力とファイルディスクリプタの複製について説明します。



まずは効率的なファイル入出力について。
[:title=前回]サンプルとして載せたソースを効率的に書き換えてみます。
まずソースの中でファイルを読み取る処理がありましたが、この部分で第三引数は一度に読み取るデータのバイト数だと説明しました。

len = read(fd, &c, 1);

つまり[:title=前回]載せたソースでは一度に1バイトずつ読み取り、それを表示していたのですが、まずはこれの実行速度を測定してみます。
速度を測定するために、ソースに時刻を表示するための関数を追記してテストしてみます。

/**********************************************
 * dataread.c
 * 引数で指定されたファイルを表示する
 **********************************************/

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>


void progresstime()
{
    fprintf(stderr,"%d\n",clock());
}


int main(int argc, char *argv[])
{
    int     fd;
    char    c ;
    ssize_t len;
    
    // 引数が足りなかったらエラーを表示して終了 //
    if ( argc < 2 )
    {
        fprintf(stderr,"usage: %s filename\n",basename(argv[0]) );
        return 1;
    }

    // 開始時刻を表示 //
    progresstime();

    // ファイルを読み取り専用で開く際にエラー //
    if ( ((fd = open(argv[1],O_RDONLY)) == -1 )
    {
        perror("open()");
        return 2;
    }

    do {
        // ファイルを1文字ずつ読み込む //
        len = read(fd, &c, 1);
        if ( len < 0 )
        {
            perror("read()");
        }
    } while ( len > 0 );

    // ファイルを閉じる //
    close(fd);

    // 終了時刻を表示 //
    progresstime();

    if ( len == 0 ) return 0;
    else            return 3;
}


ライブラリ関数clock()はそのプロセスが起動してからの経過時間(単位はmilisecond)を返します。つまり、ファイル開く直前にprogresstime()を実行しておき、ファイルを閉じた直後に再度progresstime()を実行する事でその間に行われた処理に要した時間を知る事が出来るのです。


さっそく試してみます。

[itotto@ ~/]$ ls -lh samplefile.dat
-rw-r--r--  1 itotto  users    49M Nov  1 00:29 samplefile.dat
↑ファイルは49MByteのテキストファイル

[itotto@ ~/]$ gcc -o dataread dataread.c && ./dataread samplefile.dat > /dev/null
0
7530

[itotto@ ~/]$ 


この結果を見ると、49MByteのテキストデータを1バイトずつ読み込んで処理すると約7.5秒かかることがわかります。


さて、一度に読み取るデータをもう少し大きくしてみます。読み取るサイズとしてはC言語の標準IOライブラリであるstdio.hに定義されているBUFSIZという定数を利用します。


/**********************************************
 * dataread.c
 * 引数で指定されたファイルを表示する
 **********************************************/

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>


void progresstime()
{
    fprintf(stderr,"%d\n",clock());
}

int main(int argc, char *argv[])
{
    int     fd;
    char    c[BUFSIZ] ; /***** ←修正 *****/
    ssize_t len;
    
    // 引数が足りなかったらエラーを表示して終了 //
    if ( argc < 2 )
    {
        fprintf(stderr,"usage: %s filename\n",basename(argv[0]) );
        return 1;
    }

    // 開始時刻を表示 //
    progresstime();

    // ファイルを読み取り専用で開く際にエラー //
    if ( ((fd = open(argv[1],O_RDONLY)) == -1 )
    {
        perror("open()");
        return 2;
    }

    do {
        // ファイルを1文字ずつ読み込む //
        len = read(fd, &c, BUFSIZ); /***** ←修正 *****/
        if ( len < 0 )
        {
            perror("read()");
        }
    } while ( len > 0 );

    // ファイルを閉じる //
    close(fd);

    // 終了時刻を表示 //
    progresstime();

    if ( len == 0 ) return 0;
    else            return 3;
}


データを読み取るcという文字データをBUFSIZの配列として定義を変更します。またread()で読み取るデータサイズ(第三引数)にもBUFSIZを指定しました。ではさっそく実行してみます。

[itotto@ ~/]$ gcc -o dataread dataread.c && ./dataread samplefile.dat > /dev/null
0
8

[itotto@ ~/]$ 

処理は0.008秒で完了しました。約1000分の1の時間で処理が完了している事からも、データを1バイトずつ処理するよりはある程度まとまった量を処理した方が効率が良い事が分かります。
ただし、これが大きければ大きいほどいいのかというとそういうわけではなく、大き過ぎるとメモリを大量に使ってしまい他の処理に悪い影響を及ぼしてしまう事もあります。
そういった意味でも、stdio.hに定義されているBUFSIZは一度に扱う単位としては最適なサイズで定義されていますのでこれを活用するのが良さそうです。


(ファイル操作のまとめ)

    1. ファイルを開いたら必ず閉じる
    2. ファイル操作は細かく繰り返すのではなく、ある程度まとめて行うと効果的

さて。上のテストでも使いましたがUnixのシェルやWindowsDOSには、出力先を変えるリダイレクトという機能があります。例えばこんな感じです。

[itotto@ ~/]$ echo OK
OK

[itotto@ ~/]$ echo OK > test.dat

[itotto@ ~/]$ cat test.dat
OK

[itotto@ ~/]$ echo OK2 | tee test.dat
OK2

[itotto@ ~/]$ cat test.dat
OK2

[itotto@ ~/]$ 


通常シェル上で何か処理をした場合には、すべて標準出力に対して表示がされます。この出力先をファイルや標準エラー出力などに変えるのがリダイレクトです。
この出力先を変えるという処理がどのように実装されているのかというと、ファイルディスクリプタの複製機能を使っているのです。まずはdup()とdup2()の定義は以下のとおりです。

/*** ファイルディスクリプタの複製 ***/
// #include <unistd.h>
// 
// 第一引数 複製元のファイルディスクリプタ
//
// 返却値
//  成功時 :  0
//  失敗時 : -1
int dup(int oldfd);


/*** ファイルディスクリプタの複製(その2) ***/
// #include <unistd.h>
// 
// 第一引数 複製元のファイルディスクリプタ
// 第二引数 複製元のファイルディスクリプタ
//
// 返却値
//  成功時 :  0
//  失敗時 : -1
int dup2(int oldfd, int newfd);
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    ssize_t sz;
    char    buff[BUFSIZ];
    int     fd;

    // 引数としてファイル名が渡された場合はここを実行 //
    if ( argc > 1 )
    {
        if( (fd = open(argv[1], O_RDONLY)) == -1 )
        {
            perror("open()");
            return 1;
        }

        // 標準入力のファイルディスクリプタを解放 //
        close(0);

        // 実行時引数で指定されたファイルのディスクリプタを複写する
        // → 直前に0を解放しているので、複写先として0が選定される
        if ( dup(fd) == -1 )
        {
            perror("dup()");
            return 2;
        }

        // ファイルの方のディスクリプタは0に複写されたのでもう不要 //
        close(fd);
    }

    while( (sz = read(0, buff, BUFSIZ)) > 0 )
    {
        if ( write(1, buff, sz)== -1 )
        {
            perror("write()");
            return 3;
        }
    }

    if ( sz < 0 )
    {
        perror("read()");
        return 4;
    }
    else
    {
        return 0;
    }
}


簡単に流れを説明。
引数でファイルが指定された場合はそれを開きます。そして標準入力のファイルディスクリプタを解放した直後に開いたファイルのディスクリプタを複写します。ディスクリプタの取得要求に対して、kernelは現在確保出来る最小値を確保しますので、この場合は0が確保されます。これにより引数で指定されたファイルのディスクリプタと標準入力のディスクリプタが0になるので、以後これらを区別して扱う必要がなくなります。

[itotto@ ~/]$ cat test.dat
itotto
hogehoge

[itotto@ ~/]$ gcc -o redirect redirect.c && ./redirect test.dat
itotto
hogehoge

[itotto@ ~/]$ 


(ファイルディスクリプタの複製)

    1. ディスクリプタの要求があった場合にkernelは割り当てられる最小の値を返却する
    2. 標準入出力(とエラー入出力)のディスクリプタも解放する事は可能(特別ではない)


全てファイルとして扱えるというUNIX系OSの面白さが何となく理解出来たような気がします。


次回は端末制御についてまとめます。


次へ進む