TLPI

ご存知でしょうか。TLPIという良書を。

原著でも1500ページ超、翻訳版は1600ページを超える鈍器ですが、その内容はページ数にも納得が行くほど素晴らしく、この重さでも机の上に置いておきたくなるものです。

そしてなんとO’ReillyではPDF/EPUBが購入できます。唯一のデメリットであった携帯性が改善されている訳です。もう買うしか無い。

また、TLPIのサンプルも公式で公開されています。

基本はmanなんですが、サンプルのコードや演習課題も提示されており、これ一冊全部やったら勉強にかなり良いだろうなと思っています。

File I/O: The Universal I/O Model

この記事では、サンプルとして配布されているChapter 4 File I/O: Universal I/O Modelに触れようと思います。

1章ではUNIXの歴史、2章では基礎概念、3章ではシステムプログラミングの基礎的な概念について触れ、そして4章でついに本格的なLinuxのシステムプログラミングに入るんですが、最初にFile I/Oをやっているのもかなり良いと思います。Linuxでは”Everything is a file“なので。

この章では主に、cpを簡略化した、ファイルの内容を別のファイルにコピーするプログラムを題材に

について説明しています。lseekについてはcpとは別にread,writeと組み合わせた別の例も提示されています。

実際のコードをベースに説明しているので非常にわかりやすくて良いです。

Exercises

さて、最初にも触れましたが、この本には演習課題があります。

File I/Oの章における演習としては

The tee command reads its standard input until end-of-file, writing a copy of the input to standard output and to the file named in its command-line argument. (We show an example of the use of this command when we discuss FIFOs in Section 44.7.) Implement tee using I/O system calls. By default, tee overwrites any existing file with the given name. Implement the –a command-line option (tee –a file), which causes tee to append text to the end of a file if it already exists. (Refer to Appendix B for a description of the getopt() function, which can be used to parse command-line options.)

teeの実装

Write a program like cp that, when used to copy a regular file that contains holes (sequences of null bytes), also creates corresponding holes in the target file.

holeに対応したcpの実装

が挙げられています。

やっていきましょう。普段ならRustでやるところですが、今回はやっぱりCで実装してみます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <err.h>

#define BUFFER_SIZE 4096

void usageError(const char *progName) {
    fprintf(stderr, "Usage: %s [-a] file\n", progName);
    exit(EXIT_FAILURE);
}

int main(int argc, char **argv) {
    int opt;
    int append = 0;
    char buffer[BUFFER_SIZE];
    ssize_t bytesRead;

    while ((opt = getopt(argc, argv, "a")) != -1) {
        switch (opt) {
            case 'a':
                append = 1;
                break;
            default:
                usageError(argv[0]);
        }
    }

    if (optind >= argc) {
        usageError(argv[0]);
    }

    const char *filename = argv[optind];
    int flags = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC);
    mode_t filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; // 0o666

    int fd = open(filename, flags, filePerms);
    if (fd == -1) {
        err(EXIT_FAILURE, "Error opening file %s", filename);
    }

    while ((bytesRead = read(STDIN_FILENO, buffer, BUFFER_SIZE)) > 0) {
        if (write(STDOUT_FILENO, buffer, bytesRead) != bytesRead) {
            close(fd);
            err(EXIT_FAILURE, "Error writing to stdout");
        }

        if (write(fd, buffer, bytesRead) != bytesRead) {
            close(fd);
            err(EXIT_FAILURE, "Error writing to file %s", filename);
        }
    }

    if (bytesRead == -1) {
        err(EXIT_FAILURE, "Error reading from stdin");
    }

    close(fd);
    return EXIT_SUCCESS;
}

できました。

$ gcc tee.c -o tee
$ echo foobar | tee test
foobar
$ cat test
foobar
$ echo foobar | tee -a test
foobar
$ cat test
foobar
foobar

いい感じです。

では次にhole(連続したNULL bytes)に対応したcpを作ります

SEEK_HOLEでHoleを探して出力先のファイルも同じ位置にseek、またはtruncateすることでHoleのコピーを行っています。

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <err.h>

#define BUFFER_SIZE 4096

int sourceFd = -1;
int targetFd = -1;

__attribute__((destructor))
void cleanup() {
    if (sourceFd != -1) {
        close(sourceFd);
    }
    if (targetFd != -1)  {
        close(targetFd);
    }
}

int main(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, "Usage: %s source-file target-file\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    char buffer[BUFFER_SIZE];
    char *source = argv[1];
    sourceFd = open(source, O_RDONLY);
    if (sourceFd == -1) {
        err(EXIT_FAILURE, "Error opening file %s for read", source);
    }

    struct stat sourceStat;
    if (fstat(sourceFd, &sourceStat) == -1) {
        err(EXIT_FAILURE, "Error get stat of %s", source);
    }

    char *target = argv[2];
    targetFd = open(target, O_WRONLY | O_TRUNC | O_CREAT, sourceStat.st_mode);
    if (targetFd == -1) {
        close(sourceFd);
        err(EXIT_FAILURE, "Error open or creating file %s for write", target);
    }

    off_t cur = 0;
    off_t next = 0;
    int sourceSize = sourceStat.st_size;
    while (cur != sourceSize) {
        if ((cur = lseek(sourceFd, cur, SEEK_DATA)) == -1) {
            if (errno == ENXIO) {
                if (ftruncate(targetFd, sourceSize) == -1) {
                    err(EXIT_FAILURE, "Error truncate %s", target);
                }
                break;
            }
            err(EXIT_FAILURE, "Error seek %s to next data", source);
        }

        if (lseek(targetFd, cur, SEEK_SET) == -1) {
            err(EXIT_FAILURE, "Error set seek %s", target);
        }

        if ((next = lseek(sourceFd, cur, SEEK_HOLE)) == -1) {
            err(EXIT_FAILURE, "Error seek %s to next hole", source);
        }
        
        if (lseek(sourceFd, cur, SEEK_SET) == -1) {
            err(EXIT_FAILURE, "Error set seek %s", source);
        }

        ssize_t bytesRead = 0;
        while ((bytesRead = read(sourceFd, buffer, BUFFER_SIZE)) > 0) {
            if (write(targetFd, buffer, bytesRead) != bytesRead) {
                err(EXIT_FAILURE, "Error write to %s", source);
            }
        }

        cur = next;
    }

    return EXIT_SUCCESS;
}

よさそう

$ gcc cp.c -o cp
$ dd if=/dev/zero of=file_with_hole bs=4096 count=1 2>/dev/null
$ stat --format="%s" file_with_hole
4096
$ stat --format="%s" target
4096
$ echo "foobar" >> file_with_hole
$ stat --format="%s" file_with_hole
4103
$ ./cp file_with_hole target
$ stat --format="%s" target
4103

こんな感じで、本やmanを参照しながらコードを書く練習にもなるのでかなりおすすめです。ぜひどうぞ。金はもらってません。

終わりに

この記事はn01e0 Advent Calendar 2024の24日目の記事とします。