UNIXプログラミング入門~ファイル編(2/2)~

c/c++でunix系osプログラミングを行うときに使うunistd.hやその周辺のライブラリのお話その2です。
このメモでは、ファイルの入出力に関して取り上げます。

前回は、ファイルのopencloseだけを取り扱ったので、まったく実用的ではありませんでしたが、今回は、opencloseに加えてreadwriteを取り扱うので多少は実用ファイル操作ができるようになります。

データの読み取り-read-

データの読み取りには、read関数を利用します。

read関数のプロトタイプはunistd.hで以下のように宣言されてます。

int read(int fd, void* buf, int count);

第一引数fdで指定されたファイルから最大で第三引数countバイトのデータを読み込んで、第二引数bufに格納します。

戻り値は、実際に読み込んだバイト数です。

必ずしもcountバイト読み込むわけではありません。
特にネットワークソケットやFIFO等のような、読み書きに待ちが生じるようなファイルディスクリプタを指定した場合には、ファイルの終端に達しなくともcountよりも小さな値が返ることがあります。

ファイルの終端に達したときは0が返ります。

利用例

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

#define BUF_SIZE 128
int main(int argc, char const *argv[])
{
    char buf[BUF_SIZE];
    int rsize = 0;
    int fd;
    
    for(size_t i = 1; i < argc; i++)
    {
        if ((fd = open(argv[i], O_RDONLY)) < 0)
        {
            fprintf(stderr, "ファイルを開けませんでした\n");
            return -1;
        }

        while (rsize = read(fd, buf, BUF_SIZE - 1))
        {
            if (rsize < 0)
            {
                fprintf(stderr, "読み込み中に問題が起きました\n");
                close(fd);
                return -1;
            }
            buf[rsize + 1] = 0;
            printf("%s", buf);
        }

        close(fd);
    }
    return 0;
}

上記の例だとcatコマンドと似たようなことを行っています。

データの書き込み-write-

データの書き込みにはwrite関数を利用します。

write関数のプロトタイプはunistd.hで以下のように宣言されてます。

int write(int fd, void* buf, int count);

第一引数fdで指定されたファイルに第二引数bufに含まれるデータを最大で第三引数countバイト書き込みます。

戻り値は、実際に書き込んだバイト数です。

必ずしもcountバイト書き込むわけではありません。
read同様、特にネットワークソケットやFIFO等のような、読み書きに待ちが生じるようなファイルディスクリプタを指定した場合には、バッファーの終端に達しなくともcountよりも小さな値が返ることがあります。

利用例

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

#define BUF_SIZE 128
int main(int argc, char const *argv[])
{
    char buf[BUF_SIZE];
    int rsize = 0;
    int wsize = 0;
    int rfd, wfd;

    for (size_t i = 2; i < argc; i+=2)
    {
        if ((rfd = open(argv[i-1], O_RDONLY)) < 0)
        {
            fprintf(stderr, "ファイルを開けませんでした\n");
            return -1;
        }

        if ((wfd = open(argv[i], O_WRONLY | O_CREAT)) < 0)
        {
            fprintf(stderr, "ファイルを開けませんでした\n");
            return -1;
        }
        while (rsize = read(rfd, buf, BUF_SIZE))
        {
            if (rsize < 0)
            {
                fprintf(stderr, "読み込み中に問題が起きました\n");
                close(rfd);
                close(wfd);
                return -1;
            }
            for (int j = 0; j < rsize; ++j)
            {
                wsize = write(wfd, buf + j, rsize - j);
                if (wsize < 0)
                {
                    fprintf(stderr, "書き込み中に問題が発生しました\n");
                    close(rfd);
                    close(wfd);
                }
                j += wsize;
            }
        }
        close(rfd);
        close(wfd);
    }
    return 0;
}

上記の例ではファイルのコピーを行っています。

標準入出力のファイルディスクリプタ

unistd.hでは標準入出力に対応するファイルディスクリプタは以下のように定義されています。

#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2

これらのファイルディスクリプタは、open関数で開かなくとも利用できます。

エラーの種類を確認する

unistd.hで定義されている関数の多くは、エラーが起きたときに-1を返しますが、それだけではどんな理由でエラーが起きたかわかりません。

エラーの種類はグローバル変数errnoに記録されているのでエラーが発生したらこれを確認しましょう。(要#include <errno.h>)

strerror_r関数を利用するとエラー番号からそれに対応する文字列を取り出すことができます。(要#include <string.h>)

例えば、明らかに無効なファイルディスクリプタからデータを読み込む例を考えてみます。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h> // グローバル変数 errno
#include <string.h> // strerror_r関数

#define BUF_SIZE 128
int main(int argc, char const *argv[])
{
    char errmsg[BUF_SIZE];
    char buf[BUF_SIZE];
    int rsize = 0;
    int fd = 0x1234;// 無効なファイルディスクリプタ

    while (rsize = read(fd, buf, BUF_SIZE))
    {
        if (rsize < 0)
        {
            strerror_r(errno, errmsg, sizeof(errmsg));
            printf("ERROR : %s\n", errmsg);
            return -1;
        }
    }

    close(fd);
    return 0;
}

出力

ERROR : Bad file descriptor

このようにエラーの種類を取り出すことができます。

まとめ

  • データの読み込みにはread関数、書き込みにはwrite関数を使う
  • どちらも、必ずしも第三引数で指定したサイズ分のデータを読み書きするわけではないので実際の読み書きしたサイズは戻り値で確認すること