Go で ICMP を受信する

Go と raw socket の勉強のために ping コマンドを実装しました。 自分がつまずいた ICMP の受信処理について書いておきます。 勉強用なので golang.org/x/net/icmp などは使っていません。

要約

net.DialIP で生成した *IPConn での ICMP 受信は特定の相手からのみ受信する動作になるようです。 相手を限定せず受信したいなら net.ListenIP*IPConn を生成する必要があるみたいです。

サンプルコード

Do では ICMP エコー要求の送信と ICMP エコー応答の受信を 1 回します。 ICMP を使うために net.ListenIP("ip4:icmp", nil)*IPConn を生成しています。

func Do(ip4Remote *net.IPAddr, timeout time.Duration, identifier, sequenceNumber, dataSize uint16) (rtt time.Duration, rerr error) {
    conn, err := net.ListenIP("ip4:icmp", nil)
    if err != nil {
        return 0, fmt.Errorf("ListenIP error: %w", err)
    }
    defer func() {
        if err := conn.Close(); err != nil {
            rerr = err
        }
    }()

    // 送信する
    // ペイロードはすべて 0 で作成する
    request := NewEchoRequest(identifier, sequenceNumber, make([]byte, dataSize))
    sendData, err := MarshalEcho(request)
    if err != nil {
        return 0, fmt.Errorf("MarshalEcho error: %w", err)
    }
    if err := conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
        return 0, fmt.Errorf("SetWriteDeadline error: %w", err)
    }
    start := time.Now()
    if _, err := conn.WriteToIP(sendData, ip4Remote); err != nil {
        return 0, fmt.Errorf("WriteToIP error: %w", err)
    }

    // 受信する
    timeoutCh := time.After(timeout)
    for {
        select {
        case <-timeoutCh:
            return 0, errors.New("返答受信がタイムアウトしました")
        default:
            recvData := make([]byte, ipv4TotalMaxSize)
            if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
                return 0, fmt.Errorf("SetReadDeadline error: %w", err)
            }
            recvSize, recvFrom, err := conn.ReadFromIP(recvData)
            end := time.Now()
            if err != nil {
                return 0, fmt.Errorf("ReadFromIP error: %w", err)
            }

            recvData = recvData[:recvSize]
            t, err := icmpType(recvData)
            if err != nil {
                continue
            }
            // Linux では localhost などに ping した場合
            // 自分が送った EchoRequest(8) を受信する
            // その後、カーネルのプロトコルモジュールから EchoReply を受信したりする
            if t == 8 {
                continue
            }
            if t == 0 && recvFrom.IP.Equal(ip4Remote.IP) {
                reply, err := UnmarshalEcho(recvData)
                if err != nil {
                    continue
                }
                if err := Pair(request, reply); err != nil {
                    continue
                }
                return end.Sub(start), nil
            }
            if t == 3 {
                return 0, fmt.Errorf("From %v Destination Unreachable", recvFrom.IP.String())
            }
            // 本当は他の Type も処理すべき
        }
    }
}

つまずいたところ

はじめ *IPConn の生成は net.DialIP("ip4:icmp", nil, &net.IPAddr{IP: net.IPv4(8, 8, 8, 8)}) のようにしていました。 この場合 8.8.8.8 からのパケットは受信できますが、他のノードからのパケットが受信できませんでした。 そのため宛先のノードが存在しない場合の Destination Unreachable などが受信できませんでした。 ICMP って TCP のように接続するわけではないので net.DialIPnet.ListenIP で動作が変わるの?、と疑問でした。

net.DialIP の実装も追ってみましたが、よく分からず疑問は解決しませんでした。

net.DialIP の説明

$ go doc net.dialip
package net // import "net"

func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error)
    DialIP acts like Dial for IP networks.

    The network must be an IP network name; see func Dial for details.

    If laddr is nil, a local address is automatically chosen. If the IP field of
    raddr is nil or an unspecified IP address, the local system is assumed.

net.ListenIP の説明

$ go doc net.listenip
package net // import "net"

func ListenIP(network string, laddr *IPAddr) (*IPConn, error)
    ListenIP acts like ListenPacket for IP networks.

    The network must be an IP network name; see func Dial for details.

    If the IP field of laddr is nil or an unspecified IP address, ListenIP
    listens on all available IP addresses of the local system except multicast
    IP addresses.

net.Dial の説明

$ go doc net.dial
package net // import "net"

func Dial(network, address string) (Conn, error)
    Dial connects to the address on the named network.

    Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp",
    "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6"
    (IPv6-only), "unix", "unixgram" and "unixpacket".

    For TCP and UDP networks, the address has the form "host:port". The host
    must be a literal IP address, or a host name that can be resolved to IP
    addresses. The port must be a literal port number or a service name. If the
    host is a literal IPv6 address it must be enclosed in square brackets, as in
    "[2001:db8::1]:80" or "[fe80::1%zone]:80". The zone specifies the scope of
    the literal IPv6 address as defined in RFC 4007. The functions JoinHostPort
    and SplitHostPort manipulate a pair of host and port in this form. When
    using TCP, and the host resolves to multiple IP addresses, Dial will try
    each IP address in order until one succeeds.

    Examples:

        Dial("tcp", "golang.org:http")
        Dial("tcp", "192.0.2.1:http")
        Dial("tcp", "198.51.100.1:80")
        Dial("udp", "[2001:db8::1]:domain")
        Dial("udp", "[fe80::1%lo0]:53")
        Dial("tcp", ":80")

    For IP networks, the network must be "ip", "ip4" or "ip6" followed by a
    colon and a literal protocol number or a protocol name, and the address has
    the form "host". The host must be a literal IP address or a literal IPv6
    address with zone. It depends on each operating system how the operating
    system behaves with a non-well known protocol number such as "0" or "255".

    Examples:

        Dial("ip4:1", "192.0.2.1")
        Dial("ip6:ipv6-icmp", "2001:db8::1")
        Dial("ip6:58", "fe80::1%lo0")

    For TCP, UDP and IP networks, if the host is empty or a literal unspecified
    IP address, as in ":80", "0.0.0.0:80" or "[::]:80" for TCP and UDP, "",
    "0.0.0.0" or "::" for IP, the local system is assumed.

    For Unix networks, the address must be a file system path.

strace の結果

作った ping コマンドを strace して、net.DialIP を使った場合と net.ListenIP を使った場合で比較してみました。 自マシン (192.168.0.10) から存在しないノード 192.168.0.111 に ping したときのログです。 関連しそうな部分だけを抜き出しています。 net.ListenIP のときは Destination host unreachable を受信できています。

net.DialIP の結果

socket(AF_INET, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_ICMP) = 3
setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("192.168.0.111")}, 16) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(1), sin_addr=inet_addr("192.168.0.10")}, [112->16]) = 0
getpeername(3, 0xc00010f8d8, [112])     = -1 ENOTCONN (通信端点が接続されていません)
write(3, "\10\0\267$@\333\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 40) = 40
recvfrom(3, 0xc00010fdb1, 65535, 0, 0xc00010fa98, [112]) = -1 EAGAIN (リソースが一時的に利用できません)

net.ListenIP の結果

socket(AF_INET, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_ICMP) = 3
setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(1), sin_addr=inet_addr("0.0.0.0")}, [112->16]) = 0
getpeername(3, 0xc00018f938, [112])     = -1 ENOTCONN (通信端点が接続されていません)
sendto(3, "\10\0\270\35?\342\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 40, 0, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("192.168.0.111")}, 16) = 40
recvfrom(3, 0xc00018fdb1, 65535, 0, 0xc00018fa98, [112]) = -1 EAGAIN (リソースが一時的に利用できません)
recvfrom(3, "E\300\0X\241\35\0\0@\1Wc\300\250\0\n\300\250\0\n\3\1\374\376\0\0\0\0E\0\0<"..., 65535, 0, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("192.168.0.10")}, [112->16]) = 88

ログを見ると connect(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("192.168.0.111")}, 16) = 0 が怪しそうでした。

コネクションレスでの connect の動作

Web で調べると UDP + connect の解説は見つかるのですが、ICMP + connect の解説は見つけることができませんでした。 ただ UNIXネットワークプログラミング の 6.5 の connect システムコール には、次の説明がありました。

コネクションレスプロトコルに対して connect を用いると、将来その sockfd に対してのデータの 書き込みが行われた際、どのアドレスに送ればよいのかを覚えておくために、指定された servaddr が格納される。 またこのソケットから読み出すことのできるのは、格納されたアドレスに宛てたデータグラムだけになる。

多分、connect 先からの返答しか受信しないということでしょうか...?

C++ で実験してみたところ、 connect を実行してから recv すると確かに 192.168.0.111 以外のノードからパケットを受信できませんでした。connect を実行せずに recv すると 192.168.0.111 以外のノードからのパケットが受信できました。

この結果から net.DialIPnet.ListenIP の違いを理解することが出来ました。

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/ip_icmp.h>

// 以下のページを参考にさせていただきました。
// https://www.geekpage.jp/programming/linux-network/simple-ping.php
int main(int argc, char *argv[])
{
  const int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
  if (sock == -1)
  {
    perror("socket");
    return EXIT_FAILURE;
  }

  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(0);
  addr.sin_addr.s_addr = inet_addr("192.168.0.111");
  if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1)
  {
    perror("connect");
    return EXIT_FAILURE;
  }

  char buf[2048];
  while (true)
  {
      memset(buf, 0, sizeof(buf));
      const int n = recv(sock, buf, sizeof(buf), 0);
      if (n == -1)
      {
        perror("recv");
        close(sock);
        return EXIT_FAILURE;
      }

      const struct iphdr* iphdrptr = (struct iphdr *)buf;
      const struct icmphdr* icmphdrptr = (struct icmphdr *)(buf + (iphdrptr->ihl * 4));
      printf("received %d\n", icmphdrptr->type);
  }

  close(sock);
  return 0;
}

その他

テーブル駆動テストなどを使い、テストを頑張って書いています

$ sudo /usr/local/go/bin/go test ./ping/ -covermode=count -count=1
ok      github.com/hiroygo/goping/ping  2.511s  coverage: 83.1% of statements

参考にさせていただいたサイト

「Linuxで動かしながら学ぶTCP/IPネットワーク入門」用の Vagrantfile

Linuxで動かしながら学ぶTCP/IPネットワーク入門 を読んでいます。 勉強用仮想マシンの Vagrantfile を作ったので貼っておきます。

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu2004"
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get -y install \
      bash \
      coreutils \
      grep \
      iproute2 \
      iputils-ping \
      traceroute \
      tcpdump \
      bind9-dnsutils \
      dnsmasq-base \
      netcat-openbsd \
      python3 \
      curl \
      wget \
      iptables \
      procps \
      isc-dhcp-client
  SHELL
end

docker コンテナで crond を使い aws cli を定期的に実行する

docker-compose で起動させたコンテナ上で定期的に
aws cli を実行させたかったので試してみました。

ファイル構成

$ ls
Dockerfile  crontab  docker-compose.yml

Dockerfile

定期的な処理には busybox crond を使います。
update-ca-certificates しているのは
curlcurl: (60) SSL certificate problem: unable to get local issuer certificate エラーを解決するためです。

docker-compose.yml

aws cli の実行結果を保存するためにホストマシンの ./work
コンテナの /aws にマウントします。
aws cli環境変数もここで設定します。

crontab

1 分毎に aws cli を実行し、マウントしたディレクトリに結果を出力してみます。

コンテナを起動してみる

ちゃんと処理できてます。
後で知ったのですが、mattbanner/aws-cli-cron というイメージも既にあるみたいです。

$ docker-compose up -d --no-deps --build
$ docker-compose logs -f
Attaching to cronaws_cron-aws_1
cron-aws_1  | crond: crond (busybox 1.30.1) started, log level 8
cron-aws_1  | crond: USER root pid   7 cmd aws s3 ls >> /aws/log
cron-aws_1  | crond: USER root pid  13 cmd aws s3 ls >> /aws/log
$ docker-compose down
$ cat work/log 
2021-04-20 10:00:01 fugafuga-bucket-1
2021-04-20 10:00:01 fugafuga-bucket-1

参考にさせていただいたサイト

join コマンドの練習

join コマンドを使ったファイルの結合を練習してみます。
テスト用にファイル breeds と dogs を用意しました。
2 つとも 1 行目はヘッダです。データはソートされていないです。

$ cat breeds 
id breed
4 maltese
1 poodle
3 frenchbulldog
2 pomeranian
5 husky 

$ cat dogs 
id name breed_id
2 koro 5 
1 sofia 4
3 louis 3

結合できなかった行を表示しない場合(内部結合?)

後ほど出てきますが -a オプションを付けないと
結合できなかった行は表示されません。

まずは dogs の breed_id で結合してみます。
join コマンドでは対象データがソートされている必要があるので
sort した結果を join に渡しています。ヘッダ行は邪魔なので
sort 前に sed '1d' で消しています。
join の -1 3 -2 1 は 1 つめのファイルの 3 つめのフィールドと
2 つめのファイルの 1 つめのフィールドを結合するという意味です。

$ join -1 3 -2 1 <(sed '1d' dogs | sort -k3) <(sed '1d' breeds | sort)
3 3 louis frenchbulldog
4 1 sofia maltese
5 2 koro  husky 

join で -o を使用すると表示するフィールドを選択できます。
1.1 は 1 つめのファイルの 1 つめのフィールドという意味です。

$ join -1 3 -2 1 -o 1.1 1.2 1.3 2.2 <(sed '1d' dogs | sort -k3) <(sed '1d' breeds | sort)
3 louis 3 frenchbulldog
1 sofia 4 maltese
2 koro 5 husky

今度は breeds の id で結合してみます。-a オプションが無いので
poodle, pomeranian は表示されません。

$ join -1 1 -2 3 -o 1.1 1.2 2.1 2.2 <(sed '1d' breeds | sort) <(sed '1d' dogs | sort -k3)
3 frenchbulldog 3 louis
4 maltese 1 sofia
5 husky 2 koro

結合できなかった行を表示する場合(外部結合?)

上記の poodle, pomeranian が表示されなかったコマンドに -a 1 を付けて
実行してみます。-a 1 -a 2 という使い方もできるらしいです。
-e で一致するフィールドが無い時に表示する文字列を指定します。

$ join -a 1 -1 1 -2 3 -o 1.1 1.2 2.1 2.2 -e NULL <(sed '1d' breeds | sort) <(sed '1d' dogs | sort -k3)
1 poodle NULL NULL
2 pomeranian NULL NULL
3 frenchbulldog 3 louis
4 maltese 1 sofia
5 husky 2 koro

参考にさせていただいたサイト

サービスのログが journald に収集されるか試す

journald について勉強しています。systemd が起動するサービスの標準出力と標準エラー出力が journald に収集されるか、 テスト用のログを出力するサービスを作り、試してみました。

テスト用のログ出力スクリプト

以下のスクリプト/home/pi/testsh/test.sh に配置しました。
10 秒ごとに標準出力と標準エラー出力に書き込みます。

#!/bin/bash
while true
do
   sleep 10
   echo "hello stdout"
   echo "hello stderr" >&2
done

ユニットファイルを作る

test.sh を実行するサービスのユニットファイルを /etc/systemd/system/test.service に作ります。
参考にさせてもらったサイト: https://qiita.com/DQNEO/items/0b5d0bc5d3cf407cb7ff

$ sudoedit /etc/systemd/system/test.service
$ cat /etc/systemd/system/test.service
[Unit]
Description = test

[Service]
ExecStart = /home/pi/testsh/test.sh
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

test.service がサービスとして認識されたか確認する

OK ですね。

$ sudo systemctl list-unit-files --type=service | grep test
test.service                               disabled    

サービスを起動して、journald のログを確認する

サービス起動前に journalctl | grep hello で journald のログに hello stdouthello stderr が無いことを確認します。

$ journalctl | grep hello

サービスを起動します。

$ sudo systemctl start test.service

再度ログを確認すると、ちゃんと出力されています。

$ journalctl | grep hello
 4月 04 17:24:28 raspberrypi test.sh[13035]: hello stdout
 4月 04 17:24:28 raspberrypi test.sh[13035]: hello stderr
 4月 04 17:24:38 raspberrypi test.sh[13035]: hello stdout
 4月 04 17:24:38 raspberrypi test.sh[13035]: hello stderr

確認が終わったら、サービスを止めておきます。

$ sudo systemctl stop test.service

systemd-cat

systemd-cat を実行しても journald にログを記録できるみたいです。
参考にさせてもらったサイト: https://serverfault.com/questions/573946/how-can-i-send-a-message-to-the-systemd-journal-from-the-command-line

$ journalctl | grep bowwow
$ echo 'bowwow' | systemd-cat
$ journalctl | grep bowwow
 4月 04 17:54:27 raspberrypi cat[13153]: bowwow

「Linuxのしくみ」を読んだメモ

試して理解 Linuxのしくみ 実験と図解で学ぶOSとハードウェアの基礎知識 第3刷
を読み終えました。自分用に整理したメモを残しておきます。
勉強用リポジトリ: https://github.com/hiroygo/linux-in-practice

調査用のコマンドやファイル

/proc/cpuinfo ファイル

cat /proc/cpuinfo で CPU 情報を取得できる。

taskset コマンド

プロセスが動作する論理 CPU を指定できる。

$ taskset -c 0,4 ./fuga

time コマンド

プロセスの処理時間を計測できる。
real が経過時間、user がユーザモードの時間、
sys がカーネルシステムコールを実行していた時間。

$ time ps
    PID TTY          TIME CMD
  41058 pts/0    00:00:00 bash
  41067 pts/0    00:00:00 ps

real    0m0.039s
user    0m0.005s
sys 0m0.006s

strace コマンド

プロセスのシステムコールを確認できる。
strace の出力は 1 行が 1 つのシステムコールに対応する。

free コマンド

メモリ状況を確認できる。開放可能なカーネルメモリ領域のサイズと free の合計が available になる。 開放可能なカーネルメモリ領域とはバッファキャッシュやページキャッシュなど。 Swap 行がスワップ領域の情報。

$ free
              total        used        free      shared  buff/cache   available
Mem:        7820180     1660916     3412396      387088     2746868     5450456
Swap:       2097148           0     2097148

ps コマンド

min_flt と maj_flt でページフォルトの総数が取れる。

$ ps -o pid,comm,min_flt,maj_flt
    PID COMMAND          MINFL  MAJFL
   5617 bash              1524      0
   8884 ps                 178      0

readelf コマンド

ELF フォーマットの実行ファイルの情報を表示する。以下のサンプルでは、メモリマップ開始アドレスは 0000000000002580000000000000a000 になっている。

$ readelf -S /bin/sleep
There are 30 section headers, starting at offset 0x91d8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
...
  [16] .text             PROGBITS         0000000000002580  00002580
       0000000000003692  0000000000000000  AX       0     0     16
...
  [26] .data             PROGBITS         000000000000a000  00009000
       0000000000000080  0000000000000000  WA       0     0     32
...

CPU のカーネルモードとユーザモード(第1章, 第2章)

プロセスから直接デバイスやプロセス管理システムなどにアクセスできないようにするため、 CPU にはカーネルモードとユーザモードがある。カーネルモードの時だけ、 デバイスやプロセス管理システムなどにアクセスできる。プロセスがシステムコールを発行すると、 CPU で割り込みというイベントが発生し、CPU はユーザモードからカーネルモードに遷移する。 システムコール処理が終わると CPU はユーザモードに戻る。 デバイスドライバカーネルモードで動作する。

システムコール(第2章)

システムコールアーキテクチャ依存のアセンブラコードで呼ぶ必要がある。 (特定のレジスタに実行したいシステムコールの番号を設定するなど) C だとインラインアセンブラという機能がある。これだと手間がかかるので、 OS にはシステムコールを呼び出すラッパー関数が用意されている。 CPU がカーネルモードに遷移すると、システムコールの番号から システムコール・テーブルを参照して、対応するカーネルの処理を呼び出す。

プロセスの状態(第4章)

プロセスの状態には実行中、実行待ち(CPU で実行されるの待ち)、スリープ(イベント待ち)、ゾンビがある。

OOM Killer(第5章)

どうやってもメモリが不足するような場合、Out of Memory(OOM) という状態になる。 この場合、OOM Killer が実行され、適当なプロセスが kill されることがある。

仮想記憶(第5章)

プロセスは仮想記憶というメモリを使う。 仮想記憶のアドレスと物理メモリのアドレスの対応はカーネルメモリ領域のページテーブルで ページという単位で管理される。 仮想記憶を使うことで以下を解決する。

  • メモリ断片化
    • 物理メモリでは断片化しているメモリ領域を、あたかも連続しているように見せることが出来る。
  • 他のプロセスやカーネルメモリ領域へのアクセス制御
    • 仮想記憶はプロセスごとに作られ、ページテーブルで管理される。このため他のメモリ領域が見えなくなる。
  • 他のプロセスとのアドレス衝突
    • プロセスを起動するために、ディスクなどからメモリにプロセスをマッピングする時に、他のプロセスとメモリマップ開始アドレスなどが衝突する可能性がある。仮想記憶はプロセスごとに作られるので回避できる。
    • メモリマップ開始アドレスなどは readelf から確認できる。

不正な仮想アドレスにプロセスがアクセスすると、ページフォルトという割り込みが発生し、カーネルページフォルトハンドラが実行され、 カーネルはプロセスに SIGSEGV を通知する。

mmapmalloc(第5章)

mmap ではページ単位でメモリを確保する。malloc でバイト単位で確保できるのは、glibc が中間管理してくれているから。

デマンドページング(第5章)

プロセスが mmap でメモリ確保した段階では、実際に物理メモリが割り当てられているわけではない。 確保した仮想アドレス(ページ)に最初にアクセスした時に、ページフォルトが走り、物理メモリが割り当てされる。

コピーオンライト(第5章)

fork で子プロセスを作成した段階では子プロセス用のメモリ領域は確保されていない。ページテーブルだけコピーされる。 この段階では親プロセスと子プロセスはメモリ領域を共有している。 親プロセスまたは子プロセスのどちらかがメモリに書き込むと、対応するページだけページフォルトが走り、 親プロセス用のページと子プロセス用のページに分かれる。

スワップ(第5章)

ストレージにメモリを退避して、空きメモリを確保する仕組み。 スワップイン、スワップアウトが頻繁に繰り返される状態をスラッシングという。

ページキャッシュ(第6章)

ストレージ上のデータをメモリ上にキャッシュする仕組み。プロセスがデータを変更するとページキャッシュも変更され、後でストレージ上のデータも更新される。 メモリが不足してくると、カーネルはページキャッシュを開放する。 ページキャッシュは複数のプロセスからアクセスされる可能性がある。

ファイルシステム(第7章)

ファイルシステムが無いとユーザは自分で、そのデータについて、ストレージデバイス上でのアドレス、サイズを憶えておかないといけない。 ファイルシステムにはデータ本体(ファイル)とメタデータ(データ本体のアドレス、サイズ、名前、作成日時など)がある。 tmpfs はメモリ上のファイルシステムでアクセスが高速、再起動で消去されるというもの。/tmp などに使われる。

ジャーナリング(第7章)

ファイルシステムの不整合を防ぐ仕組み。ファイルシステムのジャーナル領域にアトミックで実行されるべき複数のファイルシステム操作を保存してから、操作を実行する。

キャラクタデバイスとブロックデバイス(第7章)

Ubuntu でパーティションを作成した時のメモ

1GB のディスク /dev/sdb に 256MB のパーティション
新規作成し、システムにマウントします。
パーティション作成には fdisk を使います。
ファイルシステム作成には mkfs を使います。

/dev/sdb の情報

$ sudo fdisk -l /dev/sdb
Disk /dev/sdb: 1 GiB, 1073741824 bytes, 2097152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xa6fac314

fdisk でパーティションを新規作成する

/dev/sdb1 が新規作成されます。

$ sudo fdisk /dev/sdb

Welcome to fdisk (util-linux 2.31.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): p
Disk /dev/sdb: 1 GiB, 1073741824 bytes, 2097152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xa6fac314

Command (m for help): n
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1): 1
First sector (2048-2097151, default 2048): 2048
Last sector, +sectors or +size{K,M,G,T,P} (2048-2097151, default 2097151): +256M

Created a new partition 1 of type 'Linux' and of size 256 MiB.
Partition #1 contains a ext4 signature.

Do you want to remove the signature? [Y]es/[N]o: Y

The signature will be removed by a write command.

Command (m for help): p
Disk /dev/sdb: 1 GiB, 1073741824 bytes, 2097152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xa6fac314

Device     Boot Start    End Sectors  Size Id Type
/dev/sdb1        2048 526335  524288  256M 83 Linux

Filesystem/RAID signature on partition 1 will be wiped.

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.

mkfs で ext4ファイルシステムを作成する

/dev/sdb1ext4ファイルシステムを作成します。

$ sudo mkfs.ext4 /dev/sdb1
mke2fs 1.44.1 (24-Mar-2018)
Creating filesystem with 262144 1k blocks and 65536 inodes
Filesystem UUID: 6a53a50e-f300-4896-8f70-2c2db61bb01c
Superblock backups stored on blocks: 
        8193, 24577, 40961, 57345, 73729, 204801, 221185

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (8192 blocks): done
Writing superblocks and filesystem accounting information: done 

システムにマウントする

/dev/sdb1/mnt にマウントします。サイズも大丈夫そうです。

$ sudo mount /dev/sdb1 /mnt
$ df -h
Filesystem                    Size  Used Avail Use% Mounted on
udev                          462M     0  462M   0% /dev
...
/dev/sdb1                     240M  2.1M  222M   1% /mnt