やかんです。

新年の挨拶投稿の前に自分の勉強メモ投稿をする無礼をお許しください。

東大生やかんのブログ
やかん

挨拶投稿は今書いているところです。

はい。今回は、そろそろ期末試験も近づいてきたので、OS(オペレーティングシステム)の授業で習った内容をちょこまかと復習していこうと思いますという投稿になります。

今日はファイルの基本的なAPI

概念的なところはある程度理解しているつもりなので、実装を通してその理解を確認したいお気持ち。てか、そもそも実装慣れしていないので、普通に実装の練習をしようと思います。

東大生やかんのブログ
やかん

業務でも日常でも使わないAPIたち。

write

まずはwriteから。

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

int main()
{
  int fd = open("output/write_practice1.txt", O_CREAT | O_WRONLY);
  if (fd == -1)
    err(1, "open");

  const char *str = "Hello, World!\n";

  ssize_t w = write(fd, str, strlen(str));
  if (w == -1)
    err(1, "write");

  close(fd);

  return 0;
}

基本的な扱いはこれ。自力で実装しようとすると、「ヘッダーファイルは何使えばいいんだっけ?」とかも迷いが出ますね。習熟あるのみ。

疑問

  1. writeの返り値のssize_tって何。
    • まず、size_tはサイズを表す。で、ssize_tっていうのは、サイズに加えてエラーの有無も格納している。負の数だったらエラーが起きてるよ、ということ。
    • だから、この変数自体を積極的に使おうということはない。せいぜい、エラーが起きたかどうかの検知。
  2. writeで書き込む際、順序としては「プロセスのメモリ空間に書き込む→カーネルにコピー→二次記憶にコピー」だっけ?
    • 大体合ってる。カーネルスペースから二次記憶へのコピーは、write()とは別次元の話。階層が違う。というか、二次記憶へ実際に書き込むこと自体が別階層で行われるから、一旦カーネルスペースにコピーする必要があるんだ。
    • プロセスのメモリ空間っていうのは、主記憶のこと。
  3. writeするときのメモリの先頭は気にしなくていいんだっけ?
    • これ、strがポインタである、ということで理解可能。気にする必要はあります。で、ちゃんと気にできています。
  4. writeの第二引数で指定しているアドレスは、論理?物理?
    • 論理です。プログラムが扱うのは基本的に全て論理。

流れを説明してみる。

  • まず、ファイルディスクリプタでファイルへのアクセスを取得。これ自体はただの整数で、0, 1, 2はそれぞれデフォで使われているから、任意のファイルには3以上の整数が使われるはず。
  • strはポインタ。Cでは確か文字列をポインタとして扱うんだった。
  • お待ちかね、write。ファイルディスクリプタを第一引数で指定して、次に書き込むメモリの先頭のアドレスを指定して、次に最大で書き込むことになるメモリサイズを指定。
  • 最後にファイルディスクリプタの解放。

read(読み込みデータの表示なし)

順番的には本来readが先だけど、まあ都合上writeの次にread。

まず、本当にreadするだけのコード。

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

int main()
{
  int fd = open("write_practice1.c", O_RDONLY);
  if (fd < 0)
    err(1, "open");

  struct stat file_info[1];
  if (fstat(fd, file_info) < 0)
    err(1, "fstat");

  int sz = file_info->st_size;

  char *buffer = (char *)malloc(sz);

  ssize_t r = read(fd, buffer, sz);
  if (r < 0)
    err(1, "read");

  close(fd);
  free(buffer);

  return 0;
}

疑問

  1. mallocのところ、なんでキャストが必要なんだっけ?
    • mallocの返り値はvoid*らしい。これはジェネリックで、プログラムでこの型を使うと、使えるし動くんだけどワーニングめっちゃ出るし型安全じゃない。

流れを説明してみる。

  • ファイルディスクリプタの取得はwriteと同じ。
  • fstatを使って、指定したファイルのファイルサイズなど情報を取得。
  • ファイルサイズに基づいて、mallocでメモリ確保。
  • mallocで確保した領域に、ファイルのデータを読み込む。
  • あとはwriteとほぼ同じ。

read(読み込みデータの表示あり)

上では、本当にただreadだけ実行した。で、この時読み込んだファイルのデータはバイナリデータで、これを任意の表示形式で表示させる部分、取得する部分についてはどう実現するのかっていう問題。

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

int main()
{
  int fd = open("write_practice1.c", O_RDONLY);
  if (fd < 0)
    err(1, "open");

  struct stat file_info[1];
  if (fstat(fd, file_info) < 0)
    err(1, "fstat");

  int sz = file_info->st_size;

  char *buffer = (char *)malloc(sz);

  ssize_t r = read(fd, buffer, sz);
  if (r < 0)
    err(1, "read");

  printf("%s\n", buffer);

  close(fd);
  free(buffer);

  return 0;
}

とりあえず、文字列の場合はこうするだけでいいのか。printfで%sを指定すると、ここに指定された変数(ポインタ)を基点として、終わるまで文字列を出力してくれる。

疑問

  1. つまり、ファイルの内容を扱う、というのは、ファイルの形式に依存するということか?
    • そのようです。

流れを説明してみる。

  • 基本的に前述の通り。
  • readで読み込んだ二次記憶のファイルの内容は、まずカーネルスペースにコピーされる。で、それからプログラム内でmallocで確保した領域にファイルの内容がコピーされる。

read、writeは多分かなりプリミティブなシステムコール(というかこの場合は関数)だから、一旦カーネルスペースを経由させることで、仕事の分離を図っているんだろうなー、と予想。

mmap(本当にファイルをマップするだけ)

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <unistd.h>

int main()
{
  int fd = open("read_practice1.c", O_RDONLY);
  if (fd < 0)
    err(1, "open");

  struct stat file_info[1];
  if (fstat(fd, file_info) < 0)
    err(1, "fstat");

  int sz = file_info->st_size;

  char *mapped = mmap(0, sz, PROT_READ, MAP_PRIVATE, fd, 0);

  close(fd);

  return 0;
}

本当に、読み込むだけなら多分これで済んでいるはず。

流れを説明してみる。

  • fstatまではreadと同じ。
  • mmapを実行することで、プロセスの論理アドレス空間と二次記憶のアドレスが対応づけられる。
    • この時、メモリの確保についてはmmapがやってくれるので考えなくて良い。第一引数にアドレスを指定してもいいが、0を指定することで空いているところを勝手に使ってくれる。

で、mmapで確保した論理アドレスにアクセスがあった時に、「そのアドレスが含まれるページ」に対応した「二次記憶のアドレスが含まれるページ」が物理メモリに読み込まれ、プログラムで操作可能となる。この時、カーネルスペースは経由しない。

mmap(ファイルを実際に読み込む)

これもまたいくつか実装方法がある。まずは、シンプルにprintfを使う。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
  int fd = open("read_practice1.c", O_RDONLY);
  if (fd < 0)
    err(1, "open");

  struct stat file_info[1];
  if (fstat(fd, file_info) < 0)
    err(1, "fstat");

  int sz = file_info->st_size;

  char *mapped = mmap(0, sz, PROT_READ, MAP_PRIVATE, fd, 0);

  printf("%s\n", mapped);

  close(fd);

  return 0;
}

mmapedというポインタは、読み込んだファイル内容(マップされたアドレス)の先頭が格納されているはずだから、printfで%sを指定してあげれば、readの時と同様にファイルの内容を出力できる。

で、mappedが配列のように連なるアドレスの先頭だということを強く意識すれば、以下のように実装可能なこともなんとなくわかった気になれる。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
  int fd = open("read_practice1.c", O_RDONLY);
  if (fd < 0)
    err(1, "open");

  struct stat file_info[1];
  if (fstat(fd, file_info) < 0)
    err(1, "fstat");

  int sz = file_info->st_size;

  char *mapped = mmap(0, sz, PROT_READ, MAP_PRIVATE, fd, 0);

  for (int i = 0; i < sz; i++)
  {
    putchar(mapped[i]);
  }

  close(fd);

  return 0;
}

mapped[i]というのは、文字を表す。mappedがポインタでアドレスを表現するから、これが紛らわしい。だから、putcharのところを

printf("%c", mapped[i]);

のようにしても当然同じこと。まあ、この出力の部分はおそらくreadと同じでファイル形式に依存するはず。一応、バイナリファイルを読み込む場合のコードは以下のようになるらしい。GPTくんが一生懸命書いてくれました。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open("binary_file.bin", O_RDONLY);
    if (fd < 0)
        err(1, "open");

    struct stat file_info[1];
    if (fstat(fd, file_info) < 0)
        err(1, "fstat");

    int sz = file_info->st_size;
    unsigned char *mapped = mmap(0, sz, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED)
        err(1, "mmap");

    for (int i = 0; i < sz; i++) {
        printf("%02x ", mapped[i]); // Display each byte in hexadecimal
        if ((i + 1) % 16 == 0)
            printf("\n"); // New line every 16 bytes for better readability
    }
    printf("\n");

    close(fd);
    return 0;
}

次の勉強に向けて。

なんとなく、OSのゴールは「ページングの理解」だと思っている。目に見えなくてわかりにくいにも関わらず、至る所で登場する概念だからだ。これを理解したい。

復習したいのは以下の内容。

  • マルチプロセスの実装
  • マルチスレッドの実装
  • スレッドのスケジューリングについて復習
  • 並行処理と同期の実装 ← これが重そう!
  • メモリ管理について復習

で、次は並行処理と同期かな。一番重そうだから。

ということで、この記事は終了です。最後までお読みいただき、ありがとうございます。