The Linux Programming Interfaceという良書
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
を簡略化した、ファイルの内容を別のファイルにコピーするプログラムを題材に
open
read
write
close
lseek
について説明しています。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日目の記事とします。
Comments