Open
105

『ふつうのLinuxプログラミング』をやっていく会

README

副読本

リポジトリ

https://github.com/mohira/normal-linux-programming

開催実績

  1. 2021/03/11(木) 30分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-9e29a6e1c01730
  2. 2021/03/12(金) 60分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-0a18b2c455b2ae
  3. 2021/03/13(土) 30分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-eeab3a2a2b7e82
  4. 2021/03/14(日) 60分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-c445f294ad51d2
  5. 2021/03/15(月) 60分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-60521552a6cdd0
  6. 2021/03/16(火) 60分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-3b2132e6d70063
  7. 2021/03/17(水) 90分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-6b87351f272871
  8. 2021/03/18(水) 3分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-19894092d6c386
  9. 2021/03/19(金) 40分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-78b8226a337df2
  10. 2021/03/20(土) 70分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-a8bbb08251799a
  11. 2021/03/21(日) 20分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-3a97cf92141dfb
  12. 2021/03/28(日) 210分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-d0855fb9b6f998
  13. 2021/03/29(月) 150分 https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-71dd205320f9d5

2021/03/11(木) 30分

  • C言語実行環境の構築
  • 1.4まで

MEMO

  • 第1部はソースコード少ないみたい。ちょっとタフかもしれないとのこと
  • C言語全然わからんが、CLion(https://www.jetbrains.com/ja-jp/clion/) を使うと割と楽に書ける気がする。Thanks
    JetBrains!!
  • CMakeLists.txt ぜんぜんわからん
  • 1つのプロジェクト(?)につき、main()関数は1つまでっぽい。複数作るとBuild通らない。けど、ターミナルでやればおk。

次回

  • 1.5 コマンドライン引数 から

2021/03/12(金) 60分

  • 1.4〜2.2 (第2章おわり)

MEMO

  • 曖昧だったいろんな知識のおさらいと補強って感じ
  • 説明文の自分の言葉で言い換えていくといい感じ
  • ちょっとしっかりやりすぎている説もあるが、まあそれはそれでよし
  • セグフォ
  • man にはセクションあったのね
  • LinuxのVMをたまたま残しておいたのでラッキーだった
    • VirtualBox + Vagrant 用のファイルを作っておくのもいいね。Dockerもいいけど!

次回

  • 第3章 Linuxを描き出す3つの概念 から

Segmentation Fault

参考: C言語で16文字でセグフォらせる - Qiita

とりあえず起こしてみた。

$ gcc -dumpversion 
11.0.3
*a;main(){*a=0;}
$ gcc intentional_segmentation_fault.c
intentional_segmentation_fault.c:1:2: warning: type specifier missing, defaults to 'int'
      [-Wimplicit-int]
*a;main(){*a=0;}
 ^
intentional_segmentation_fault.c:1:4: warning: type specifier missing, defaults to 'int'
      [-Wimplicit-int]
*a;main(){*a=0;}
   ^
2 warnings generated.
$ ./a.out
zsh: segmentation fault  ./a.out

gccのオプション

数値が大きいほうが最適化がつよいらしい。へ〜

  • -o または -o1
  • -o2
  • -o3

ファイル名展開機能: Glob(グロブ)

グロブ(英: glob)とは主にUnix系環境において、ワイルドカードでファイル名のセットを指定するパターンのことである。
例えば、UNIXのコマンド「mv *.xlsx 営業実績/」はカレントディレクトリから営業実績/ディレクトリへと.xlsxで終わる全てのファイルを移動する。
ここで、は「任意の文字列」を表すワイルドカードであり、.xlsxはグロブである。
*以外に一般的なワイルドカードは疑問符 (?) であり、これは任意の1文字を表す。

manコマンドは8種類あるらしい == セクション とかいう概念

  • そんなにあるんかいな

セクション

  1. ユーザコマンド
  2. システムコール
  3. ライブラリ関数
  4. デバイスファイルなど
  5. ファイルフォーマット
  6. ゲーム
  7. 規格など
  8. システム管理用コマンド

セクションの説明は man の結果のほうがわかりやすいかも?

$ man man
...
       1   Executable programs or shell commands
       2   System calls (functions provided by the kernel) ← この表現のがよくない?
       3   Library calls (functions within program libraries)
       4   Special files (usually found in /dev)
       5   File formats and conventions eg /etc/passwd
       6   Games
       7   Miscellaneous  (including  macro  packages  and  conventions),  e.g. man(7),
           groff(7)
       8   System administration commands (usually only for root)
       9   Kernel routines [Non standard]
...

セクションの違い

# オプションなし
$ man strlen | head
STRLEN(3)                      Linux Programmer's Manual                      STRLEN(3)

strlen は 3以外ない?

$ man 1 strlen | head
No manual entry for strlen in section 1

$ man 2 strlen | head
No manual entry for strlen in section 2

$ man 3 strlen | head -n1
STRLEN(3)                      Linux Programmer's Manual                      STRLEN(3)

$ man 4 strlen | head
No manual entry for strlen in section 4

$ man 5 strlen | head
No manual entry for strlen in section 5

$ man 6 strlen | head
No manual entry for strlen in section 6

$ man 7 strlen | head
No manual entry for strlen in section 7

$ man 8 strlen | head
No manual entry for strlen in section 8

9以上のやつ

$ man 9 strlen | head
No manual entry for strlen in section 9

$ man 10 strlen | head -n1
No manual entry for 10

$ man 11 strlen | head -n1
No manual entry for 11
STRLEN(3)                      Linux Programmer's Manual                      STRLEN(3)

# でかい数字でも No manual
$ man 1234567890 strlen | head -n1
No manual entry for 1234567890
STRLEN(3)                      Linux Programmer's Manual                      STRLEN(3)

セクションが2つあるやつ printf

セクション1 == ユーザコマンド

$ man 1 printf | head
PRINTF(1)                            User Commands                            PRINTF(1)

NAME
       printf - format and print data

SYNOPSIS
       printf FORMAT [ARGUMENT]...
       printf OPTION

DESCRIPTION

セクション3 == ライブラリ関数

$ man 3 printf | head
PRINTF(3)                      Linux Programmer's Manual                      PRINTF(3)

NAME
       printf,  fprintf,  dprintf,  sprintf,  snprintf,  vprintf,  vfprintf,  vdprintf,
       vsprintf, vsnprintf - formatted output conversion

SYNOPSIS
       #include <stdio.h>

       int printf(const char *format, ...);

セクション指定なし == PRINTF(1)

$ man printf | head
PRINTF(1)                            User Commands                            PRINTF(1)

NAME
       printf - format and print data

SYNOPSIS
       printf FORMAT [ARGUMENT]...
       printf OPTION

DESCRIPTION

OSの厳密な(?)定義はないという話。わりとテキトーな概念!

実のところ、OSという単語の意味するところは厳密には決まっていません。
OSの種類や実装の方法、時代によって変遷するものです。

やはり! とはいえ、なにか良い言い方も知りたいところではある。

OSの主要な機能はなにか? とか、OSがないと(OSがないコンピュータが想像できないけど)どうなるか? を考えていけば輪郭がはっきりしそう。

極端な例を挙げれば、かつてウェブブラウザがOSの一部であるかそうでないかアメリカ司法省と喧嘩をした木々用もいました。
つまり、OSというのは裁判の結果によって範囲が変わってしまうような、わりとテキトーな概念なのです。

MicrosoftのIEの話っぽい。

IEはWindows 95の一部かどうかによってあーだこーだな感じだったらしい。

ディストリビューション ≒ 広義のLinux と 狭義のLinux

本来はLinuxとは「Linuxカーネル」のみを指す狭義の言葉です。このLinuxカーネルとはOSの中核となる部分で、コンピュータのハードウェア制御を行うソフトウェアのみを指すため、実際にユーザが使うツールやアプリケーションは含まれません。
三宅 英明,大角 祐介. 新しいLinuxの教科書 (Japanese Edition) (p.4). Kindle 版.

ツールやアプリケーションってのは、この本で言うところの「ソフトウェアパッケージ(部品のこと)」って感じかな。
具体的にはシェル(bash, zsh, ... )とかprocs(ps, pstree, top)とか、X Window Systemとか。

それだけでは不便ですから、Linuxカーネルに加えて基本的なコマンド群やアプリケーションなどを含めて、ユーザがそのまま利用できるようにパッケージングしたものが提供されています。
これを広義に「Linux」と呼びます。この広義のLinuxが、すなわちLinuxディストリビューションと考えてよいでしょう。
三宅 英明,大角 祐介. 新しいLinuxの教科書 (Japanese Edition) (p.4). Kindle 版.

それはそうだよねって感じ。例えばゲーム機でも、すぐに遊べるような状態で販売されている。(もし、プレステのむき出しの本体部分(?)のみで、各種スロットとかケーブルとかコントローラーとかがバラ売りだとめんどくさい)

重要: 「Linuxカーネル」と「Linux OS」と呼び分ける。この本では。

次のように記述するってさ。

  • 「カーネルとしてのLinux」 は 「Linux カーネル」
  • 「OSとしてのLinux」は「Linux OS」

Linuxカーネルのプログラム本体発見!

vagrant@ubuntu-bionic:/$ ls boot/
System.map-4.15.0-135-generic  grub                           vmlinuz-4.15.0-135-generic
config-4.15.0-135-generic      initrd.img-4.15.0-135-generic
vagrant@ubuntu-bionic:/$ ls 
bin   etc         initrd.img.old  lost+found  opt   run   srv  usr      vmlinuz
boot  home        lib             media       proc  sbin  sys  vagrant  vmlinuz.old
dev   initrd.img  lib64           mnt         root  snap  tmp  var

デバイス(Device) は 比較的単位の大きい物理的な部品のこと

そういえば、デバイスって言葉自体は結構意味が広いよな。

CPUやメモリ、HDDとか、DVD-ROMドライブとか、そういうのが含まれる。

で、カーネルはこういった Device を統括しているすごいやつ。

ところで、Deviceごとに使い方は違うけど、それはどうしているわけ? → デバイスドライバ(Device Driver)

それはさておき、「ドライバ」っていうと、「ネジを回すやつ」が先に浮かんじゃわない?

HDDが3つ(仮に、A社製のHDD_A、B社製のHDD_B、C社製のHDD_C)があったら、それぞれのHDDの操作方法はきっと違うよね。作っている人が違ったら、中身の作りだったり使い方も違うわな。
(例えば、エアコンのリモコンとかも会社によって違うよね)

となると、カーネル的には、ABC全対応のプログラム必要なわけで。それは大変だよねと。

そこで、カーネルのうち、デバイス(ここではHDDのことね)を操作する部分のコードだけ独立させて、交換可能にしておく。そんでもって、実際にAを使うときはA用のプログラム、Bを使うときにはB用のプログラムってやれば便利。

で、この「○○用のプログラム」のことを「デバイスドライバ」っていうわけね。
まあ、単にレイヤーを設けている話といえばそれまでという感じもある。

システムコール(System Call) の System は カーネルのこと ← じゃあ、「カーネルコール」でよくね?

システムコールの「システム」は要するにカーネルのことで、システム(system)を呼び出す(call)から「システムコール」と呼ばれます。

「システム」って言葉は広い意味をもつ言葉。だからつかみにくいところがあると思う。そういうときに、こういう説明は嬉しいよね。ありがたや。

主語を見失わないようにしないとね。プログラムくん が カーネルさん に依頼する

  • o: システム(=カーネル)呼び出す
  • x: システム(=カーネル)呼び出す

で、じゃあ誰がカーネルさんを呼び出すんじゃ!? って話だけど、これは「なんかのハードウェアを操作したいプログラムくん」ってことね。

要は、プログラムくんが、カーネルさんに依頼する。

逆に言えば、ハードウェアをいじらないなら(そういう目的はあるのか!?)別に、カーネルさんに依頼する必要はないということじゃね? システム(カーネル)コールはいらんのじゃい。

唐突に、「Linux世界って要するに何だ」に対する答えを考える

カーネルを働かせるのだ!

あと、とにかく、この3つを理解する。==本書の狙い。

  1. ファイルシステム
  2. プロセス
  3. ストリーム

だから、カーネルさんを働かせて(=システムコールを通じて)、プロセスだのストリームだのの、イメージをかためていく。

本書ではいろいろと概念的な説明も行いますが、それはすべて理解を助けるための補助線でしかないのです。システムコールこそがLinuxカーネルの核心です

カーネルだけにね! ってかんじもあるなw

それはさておき、システムコールを理解することに集中するとよいということだな。

システムコール と ライブラリ関数 を区別する

ライブラリ関数 := システムコール以外に使える関数(暗に、システムコールを関数とみなしている感じだね)

  • 具体例
    • システムコール: read write fork など
    • ライブラリ関数: printf() exit() strlen() strcopy() など

ライブラリ関数を使うためには リンク(link)が必要

  • 「関数をリンクする」という言い回しには慣れないよね。importとかrequireが馴染む感じ
  • NAZO: 「リンカ」ってやつとは、無関係?

システムコールとライブラリ関数の区別はとりあえず気にしなくてよさげ!

実はシステムコールとライブラリ関数の区別は曖昧で、

でた〜、この曖昧パターン!

昔はシステムコールだった手続きも今はライブラリ関数として実装されていることがありますし、その逆もあります。

あ、たしかになるほど? 例えば、どれが該当する?

そういった意味では、システムコールとライブラリ関数の違いを本気で意識しなければならないのは、カーネルや基幹ライブラリを書くプログラマくらいと言えるかもしれません。

あ、はい。

mam の セクションで違うやつもあるじゃん?

  • こういうときは違いがでるよね
  • 1 は User Commands
  • 3 は Linux Programmer's Manual
$ man 1 printf 
PRINTF(1)                            User Commands                            PRINTF(1)

NAME
       printf - format and print data

SYNOPSIS
       printf FORMAT [ARGUMENT]...
       printf OPTION

DESCRIPTION
       Print ARGUMENT(s) according to FORMAT, or execute according to OPTION:
...
$ man 3 printf | head -n 50
PRINTF(3)                      Linux Programmer's Manual                      PRINTF(3)

NAME
       printf,  fprintf,  dprintf,  sprintf,  snprintf,  vprintf,  vfprintf,  vdprintf,
       vsprintf, vsnprintf - formatted output conversion

SYNOPSIS
       #include <stdio.h>

...

DESCRIPTION
       The functions in the printf() family produce output according  to  a  format  as
       described  below.   The functions printf() and vprintf() write output to stdout,
       the standard output stream; fprintf() and vfprintf() write output to  the  given
       output  stream;  sprintf(),  snprintf(), vsprintf() and vsnprintf() write to the
       character string str.


通称、libc それは、標準Cライブラリ(standart C library)

vagrant@ubuntu-bionic:/$ ls -l lib/x86_64-linux-gnu/libc.so.6 
lrwxrwxrwx 1 root root 12 Dec  7 16:38 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.27.so

System calls (functions provided by the kernel) って説明わかりやすいと思う

システムコールを、「カーネルによって提供されている関数」って言っている。関数やん!

$ man man
...
       1   Executable programs or shell commands
       2   System calls (functions provided by the kernel)
       3   Library calls (functions within program libraries)
       4   Special files (usually found in /dev)
       5   File formats and conventions, e.g. /etc/passwd
       6   Games
       7   Miscellaneous (including  macro  packages  and  conventions),  e.g.  man(7),
           groff(7)
       8   System administration commands (usually only for root)
       9   Kernel routines [Non standard]
...

2021/03/13(土) 30分

  • 第3章はじめから〜3.1まで

MEMO

  • 改めて「ファイル」について考えた。分類と、ファイルが持つ性質
  • なーんか、ファイルシステムにめっちゃ興味がでてきている
  • GoSysとあわせてやっていきたいね

次回

  • 3.2 プロセス から

おさらい: Linuxの世界をおさえるための3つ重要概念

  1. ファイルシステム
  2. ストリーム
  3. プロセス

第3章ではこれらを説明していく感じ。

よくよく考えると、「ファイル」って色々あるねという話

「広義のファイル」

ls で表示されるやつ。だから、ディレクトリも含まれていることになるね。

vagrant@ubuntu-bionic:~$ ls /
bin   etc         initrd.img.old  lost+found  opt   run   srv  usr      vmlinuz
boot  home        lib             media       proc  sbin  sys  vagrant  vmlinuz.old
dev   initrd.img  lib64           mnt         root  snap  tmp  var

普通のファイル(Regular file)

  • NAZO: "regular file" とか "normal file" って言い方は標準的なのか?
  • いわゆるファイルって感じ。ディレクトリとかリンクじゃないやつ。

ディレクトリ(Directory)

  • まあ、ディレクトリですね。はい。

UNIXでは伝統的にディレクトリのデータも単なるバイト列として読むことができたのですが、Linuxではそれを禁止しています。

へ〜。なんでだろうね?

シンボリックリンク(Symbolic Link)

  • 他のファイルの名前を格納したファイルのこと。まあ、リンクですね。はい。
  • MEMO: "soft link" ということもあるらしい。確かに、"hard link" もあるからそうですね。

ここまでの3つは普通に知っているというか、おなじみの連中。

広義のファイルだけど、あんまり知らない連中その1: デバイスファイル

デバイスファイル

  • ハードウェア(デバイス)をファイルとして表現したもの ← デバイスをファイルで扱うのってすごくね?
  • 2種類あるらしい。

1. キャラクタデバイスファイル

  • ex: プリンタとかモデムとか

2. ブロックデバイスファイル

# このへんのやつらがデバイスファイル
vagrant@ubuntu-bionic:~$ ls -l /dev/sd*
brw-rw---- 1 root disk 8,  0 Mar 13 14:28 /dev/sda
brw-rw---- 1 root disk 8,  1 Mar 13 14:28 /dev/sda1
brw-rw---- 1 root disk 8, 16 Mar 13 14:28 /dev/sdb

広義のファイルだけど、あんまり知らない連中その2: 名前付きパイプ(named pipe)

プロセス間通信に使うファイル。FIFO。
使用頻度低いから、本では扱わないって。残念。

広義のファイルだけど、あんまり知らない連中その3: UNIX ドメインソケット

これもプロセス間通信で使うやつ。
『Goならわかるシステムプログラミング』の6章でもでてきたね。最速らしい。

現在では、TCPソケットで代用できるので、本書では扱わない。

あれま。でも、ソケットがよくわかってないので、感想はあまりない。GoSysにも期待。

意外と考えたことのないファイルの性質は3つ

そりゃそうだ、という感じ。だけど、改めて整理するのも悪くない感じ。
ファイルの性質を意識すると、ファイルシステムを考えるのに役立ちそう。というか必須な感じがあるね。

  1. 何らかのデータを保持する
  2. 付帯情報がついている
  3. 名前(パス)で指定できる

ってか、ファイルが何かを指し示していたり、情報をもっているわけだけど、その実体はどこにあるの? って話。普段意識してないし、意識しなくていいことがすごいとおもうんだけど、実体はどこよ? を考えよう。

まあ、結局は、HDDとかの記憶装置内(瞬間的にはメモリとかにもあるのかな?)だと思う。で、そうなると、HDDとかってのは、ファイルとして扱えるわけじゃなくて、(多分、ビット列か? ) そのために、ファイルシステムが必要的な感じだと思う。歴史的にはファイルシステムが後な気がする(本当?)。

ここまで書いてみて思ったけど、記憶装置のこと全然知らないなww

mount コマンドで、どのようなファイルシステムが使われているかを調べる

# 数をみる
vagrant@ubuntu-bionic:~$ mount | wc -l
34

# 多いので、`-t ext4` で絞り込む
vagrant@ubuntu-bionic:~$ mount -t ext4
/dev/sda1 on / type ext4 (rw,relatime,data=ordered)
vagrant@ubuntu-bionic:~$ mount -t tmpfs
tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=403888k,mode=755)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=403884k,mode=700,uid=1000,gid=1000)

mount のマニュアル

  • ってか、8だね! セクション8 == システム管理系のコマンド(System Administration)
$ man mount
MOUNT(8)                                                                        System Administration                                                                        MOUNT(8)

NAME
       mount - mount a filesystem

SYNOPSIS
       mount [-l|-h|-V]

       mount -a [-fFnrsvw] [-t fstype] [-O optlist]

       mount [-fnrsvw] [-o options] device|dir

       mount [-fnrsvw] [-t fstype] [-o options] device dir

DESCRIPTION
       All  files  accessible in a Unix system are arranged in one big tree, the file hierarchy, rooted at /.  These files can be spread out over several devices.  The mount command
       serves to attach the filesystem found on some device to the big file tree.  Conversely, the umount(8) command will detach it again.  The filesystem is  used  to  control  how
       data is stored on the device or provided in a virtual way by network or another services.

...

# ↓↓ このへんか ↓↓

   Listing the mounts
       The listing mode is maintained for backward compatibility only.

       For more robust and customizable output use findmnt(8), especially in your scripts.  Note that control characters in the mountpoint name are replaced with '?'.

       The following command lists all mounted filesystems (of type type):

              mount [-l] [-t type]

       The option -l adds labels to this listing.  See below.

全部調べてみた → 詳しくは [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 の7章「ファイルシステム」をみてね

  • ファイルシステム感ある。
  • 疑似ファイルシステム(Pseudo Filesystem)もあるってさ。気になる。
vagrant@ubuntu-bionic:~$ mount -l | sort
/dev/sda1 on / type ext4 (rw,relatime,data=ordered) [cloudimg-rootfs]
/vagrant on /vagrant type vboxsf (rw,nodev,relatime)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime)
configfs on /sys/kernel/config type configfs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,pagesize=2M)
lxcfs on /var/lib/lxcfs type fuse.lxcfs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)
mqueue on /dev/mqueue type mqueue (rw,relatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
pstore on /sys/fs/pstore type pstore (rw,nosuid,nodev,noexec,relatime)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=34,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=12025)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=403888k,mode=755)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=403884k,mode=700,uid=1000,gid=1000)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
udev on /dev type devtmpfs (rw,nosuid,relatime,size=2006652k,nr_inodes=501663,mode=755)
vagrant on /vagrant type vboxsf (rw,nodev,relatime)

2021/03/14(日) 60分

  • 3.2~3.3(第3章おわり)

MEMO

  • バイト列の通り道、それがストリーム
  • バイト列がやりとりできれば、なんでもストリーム
  • 別のコンピュータであっても、ストリーム
  • とにかくストリーム
  • プロセスは囚われの身
  • 必要なものは3つだけ。ファイルシステム、プロセス、ストリーム。これでLinuxOSの仕組みが説明できちゃう。まじかいな!?

次回

  • 4.1 ユーザとグループ から

「3.2 プロセス」は、はいそうですねという感じ。

ストリーム(Stream)の定義はオリジナルなので注意!

本書では ストリーム == バイトストリーム == バイト列が流れる通り道

より厳密な定義は5章でやるってさ。とにかく、バイト列の流れ!

他の本だと「ストリーム」は別の概念らしいよ

本書で言うストリームは他書では単に「ファイル」や「open file」と呼ばれていますから覚えておいてください

ほうほう

さらに、LinuxやUNIXに関する他の書籍では、ストリームという語を次の2つの意味で使うことがあります。

・FILE型の値
・STREAMSカーネルモジュール

へ〜。

で、そのストリームはどこでどう使われるわけ?

例1: プロセスがファイルの内容にアクセスしたいとき

プロセス <------> ファイル

じゃなくて、あいだにいくつかある。

まず、カーネルに依頼する(==システムコールを使う)ことで、ファイルにつながるストリームを作ってもらう。で、そのストリームを操作してファイルの中身を取りだす(==readする)。

こんな感じ。

プロセス ---> カーネル
   |            ↓
    ------- [ストリーム] ----> ファイル

語彙 ← こういうのホント嬉しいね

  • 「ストリームからバイト列を取り出すこと」:=「読む(read)」
    • ex: 「ストリームからバイト列を読む」
    • ex: 「ファイルの内容を読む」
  • 「ストリームからバイト列を流し込むこと」:=「書く(write)」
    • ex: ストリームにバイト列を書く
    • ex: ファイルに書く

「プロセスが直接ファイルの中身を読んでるわけじゃない」って理解でおk?

  • カーネルにストリーム作成を依頼する
  • ストリームから(読みたいファイル内容の)バイト列を読む

っていうステップがあるわけだな。

バイト列ならなんでもストリームで扱える話

例2: デバイスファイル

  • SSDやHDDは「バイトの塊」のようなハードウェアなので、ファイルと同じ用にストリームで扱える
  • キーボードも、「押されたキーを表すバイト列」を送りつけてくるストリームって考えるといける

とにかく、バイト列の出入りがあれば、なんでもストリーム

例3: パイプ

例1と例2には

  • プロセス ---[ストリーム]--- ファイル
  • プロセス ---[ストリーム]--- ハードウェア(デバイスファイル)

だったけど、別に プロセスとプロセス を繋げったっていいわけで! そらそうだ。

で、ストリームの両端がプロセスになっているやーつを パイプ(pipe) っていう。

パイプの仕組みをちょっと覗く

# たとえばこんなやつね
$ man echo | less
  1. まず各コマンドを独立したプロセスとして同時に実行する
  2. そのプロセス間をストリーム(今回はパイプ)でつなぐ

別々のコンピュータでのやりとりだってバイト列ならストリーム

例4: ネットワーク通信

そらそうだわ。

え、ストリームとかいう抽象つえ〜〜〜。

ちゃんと、バイト列が送れたらストリーム!
間にネットワークがあるけど、間に何があろうがバイト列のやりとり! ストリーム!

プロセス間通信(IPC: InterProcess Communication): プロセス同士がストリームを通じて意思の疎通をはかること

具体例は、パイプやネットワーク通信。
(ネットワーク通信は、必ずしもプロセス同士じゃないけどね)

「意思の疎通」って言葉を使っているけど、Wikipediaとかを見るに「プロセス間でのデータのやり取り」って感じだなあ。

ストリームを利用しないプロセス間通信もあるらしい。例えば、POSIX IPC。

Wikipediaによれば、プロセス間通信の方法はめっちゃあるやん

https://ja.wikipedia.org/wiki/プロセス間通信#主なIPC技法
技法 提供しているオペレーティングシステムや環境
ファイル 多くのOS
シグナル 多くのOS。WindowsではCのランタイムライブラリでのみ実装しており、IPCとしての利用は推奨していない[要出典]。
メッセージキュー 多くのOS
ソケット 多くのOS
UNIXドメインソケット POSIX準拠システム
パイプ POSIX準拠システム、Windows
名前付きパイプ POSIX準拠システム、Windows
セマフォ POSIX準拠システム、Windows
共有メモリ POSIX準拠システム、Windows
メモリマップトファイル POSIX準拠システム、Windows
メッセージパッシング
(shared nothing)
MPI パラダイム、Java RMI、CORBA、MSMQ(英語版), MailSlot(英語版)、QNX、その他
Binder Android

「ソケット」もプロセス間通信の方法

ソケット本の説明もGreatだ。このソケット本は、ソケットに注力している。というか、プロセス間通信はソケットしかでてこない。

プロセスは囚われの身。電話しかできない。

アプリケーションが動作するプロセスは、自力でほかのプロセスとのやりとりなどを行うことはできません。また、直接ハードウェアを制御できず、カーネルに対してハードウェア制御の依頼を出すことしかできません。ユーザが書いたアプリケーションが、プロセスの外部と何らかのやりとりをするには、カーネルの助けが必要です。カーネルに通信を仲介してもらうわけです。

くどいかもしれないけど、**「プロセスは、別のプロセスと直接やりとりできない」**わけよ。要は、他のプロセスが使っていうメモリを書き換えるとかはできないわけ(逆に、できちゃったらやばいけどねw)。プロセスは囚われの身。

しかもハードウェアもいじれない(ようにしている)から、頼むぜカーネルさん! つまりは、システムコール! もしもし〜?

OSによっては、アプリケーションがカーネルに対する依頼を行う仕組みを「システムコール」と呼んでいます。通信を行うためのソケットも、システムコールの1つです。OS内で稼働するプロセスは互いに分離されているため、直接やりとりすることはできません。同じコンピュータ内に存在しているプロセス同士が何らかのやりとりをするには、図2.3のように、カーネルにデータの送受信を仲介してもらう必要があります。
小川 晃通. ポートとソケットがわかればインターネットがわかるTCP/IP・ネットワーク技術を学びたいあなたのために (Japanese Edition) (Kindle の位置No.821-830). Kindle 版.

「ストリーム」って言葉は出てこない(『ふつうLinux』の定義だからそれはそう)けど、非常にわかりやすいね。

たった3つあればいい

 プロセス <---[ストリーム]---> プロセス
    |
    |
    |
 [ストリーム]
    |
    |
    |
ファイルシステム  

まじ!?

2021/03/15(月) 60分

  • 4.1 ユーザとグループ

MEMO

  • マルチユーザシステムの意義を改めて考える回
  • 主体はプロセス! ユーザーじゃないよ!
  • クレデンシャル!
  • クレデンシャルのプロセスを直接見たい〜〜〜

次回

  • 4.2 シェルと端末 から

なぜ、Linuxがマルチユーザシステムなのか?

おそらく、一番わかりやすい理由が「UNIXがそうだったから」でしょう

なるほど。説得力あるわ。

昔は1台のコンピュータが非常に高価でしたから、コンピュータを1人で独占するなどということは考えられませんでした。
高価なコンピュータをみんなで分けあって使うのが当然だったのです。

これな〜。想像でしかないけど、まあ、わかる話ではある。体験してみたいよね。

家族で1台のPCを共有する感じが一番近いかな? 最近はあまりなさそうな気がするけど。

じゃあ、1人でコンピュータを使うときに、マルチユーザシステムは意味あんの? って話

結論、ある。

研究用アカウント と 個人用アカウント を分けていたことはあるな。昔。

研究の内容が趣味の内容で埋もれたり、発表とかするときに余計なものが映ったりするのが防げるから。

複数ユーザが使う前提でシステムを設計することで、システムをより安全にできる

お、どういうことだろ?

例えばシステムには libc.so.6 のように重要なファイルをもあれば、ちょっとしたメモのようにどうなってもいいファイルもあります。
この両者が同じように扱われていいはずはありません。
(中略)
普段使うときは libc.so.6 のような重要ファイルは消せないようになっているのが便利ではないでしょうか。

あ〜、なるほど、そうじゃん。「うっかり消しちゃった☆」が防げるのか。
複数のユーザがいることは、複数の権限があることってことやな。
もちろん、複数ユーザであれば、複数の権限とは限らんけど、自然と複数の権限があるイメージだよね。

システムにとって重要なファイルは普段使うユーザとは別のユーザ所有にしておき、所有者以外には変更できないようにするのです。
こうしておけば、重要なファイルをうっかり消してしまうこともありません。

わかる。

ってか、コンピュータでなくてもそうじゃんね。

ユーザとか権限って概念は、コンピュータ以前からあるでしょ。

グループ

ユーザとパーミッションだけでなく、グループも加えるとより柔軟。

これも、コンピュータに関係なく自然にある概念だと思う。

パーミッション

いつものやつね。

クレデンシャル(Credential)

ユーザAとしてアクセスするってどういうこと?

ユーザAが所有するファイルがあり、ファイルパーミッションは rw-r--r--(644)。
ユーザAはそのファイルにアクセスすれば読み書きできる。

ここで、クイズ。ユーザAとしてアクセスするとは具体的にどういうことか?

Linuxにおいて、活動の主体はユーザではなく、プロセスである!

なるほど〜〜〜。

ユーザという割と目に見える概念(いまだって、「私」がコンピュータを操作している)が主体に思えるけど、そうじゃない。

「俺が操作しているぞ!」って思っていたとしても、Linuxの仕組み上は、「プロセスが操作している」。

ある意味依頼しているだけという感じもあるな。

さて、クイズの正解はなにかというと、これ↓↓

「ユーザーAとしてアクセスする」==「ユーザAの属性をもったプロセスがアクセスする」

ユーザAの属性 == クレデンシャル == 代理人としての証

Bobという人物がいるとする。
で、Bobにしかできない操作があったときに検証するのは、本当にBob(同一人物)か? じゃなくて、Bobがやっているのと同じであると認められている何かを持っている人か? って感じ。証明書だな。あるいは、委任状っぽくもある。

現実世界(?)でも、委任状をもっていれば、本人でなくても、本人として振る舞えるよね。あれですよ。あれ。

え、いつ証明書(クレデンシャル)を渡したの?

ログインしたときに渡しているんだな〜これが。

もっと具体的に言うと、ログインの過程で、証明書を持つプロセスがシステム上に作成される。
んで、この「証明書を持つプロセス」が、別のコマンド(==プロセス)を起動するときに、証明書のコピーも自動で渡している。
これにより、うまいこといくんですなあ。

例えば、特定のファイル操作をするときに、この証明書をチェックしているわけよ。「あ、Aさんの証明書ですね、読み書きどうぞ〜」ってね。

繰り返しになるけど、主体はプロセスだからね。ユーザじゃなくて。
だから、上の文でも、「プロセスが別のコマンド(==プロセス)を起動する」って書いてあるね。
(ユーザがなにかのコマンドを起動しているわけじゃない〜)

NAZO: 証明書をもつプロセス(クレデンシャルのプロセス?)ってどれ?

PIDが1だし、/sbin/initって名前もそれっぽい感じがあるけど、でもこれは起動のやつかな?

Linux起動の仕組みを理解しよう[init/inittab編]:Windowsユーザーに教えるLinuxの常識(10)(1/2 ページ) - @IT

vagrant@ubuntu-bionic:~$ ps aux | head
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.2  77984  8960 ?        Ss   02:19   0:01 /sbin/init

プロセスの親子関係をみればわかる??

vagrant@ubuntu-bionic:~$ pstree -p
systemd(1)─┬─VBoxService(1078)─┬─{VBoxService}(1079)
           │                   ├─{VBoxService}(1080)
           │                   ├─{VBoxService}(1081)
           │                   ├─{VBoxService}(1082)
           │                   ├─{VBoxService}(1083)
           │                   ├─{VBoxService}(1084)
           │                   └─{VBoxService}(1085)
           ├─accounts-daemon(962)─┬─{accounts-daemon}(1034)
           │                      └─{accounts-daemon}(1057)
           ├─agetty(985)
           ├─agetty(1031)
           ├─atd(814)
           ├─cron(957)
           ├─dbus-daemon(869)
           ├─irqbalance(862)───{irqbalance}(894)
           ├─lvmetad(461)
           ├─lxcfs(821)─┬─{lxcfs}(897)
           │            └─{lxcfs}(899)
           ├─networkd-dispat(950)───{networkd-dispat}(1176)
           ├─polkitd(1086)─┬─{polkitd}(1107)
           │               └─{polkitd}(1109)
           ├─rsyslogd(849)─┬─{rsyslogd}(873)
           │               ├─{rsyslogd}(874)
           │               └─{rsyslogd}(876)
           ├─sshd(966)─┬─sshd(2315)───sshd(2394)───bash(2395)───pstree(2509)
           │           └─sshd(2409)───sshd(2477)───bash(2478)───sleep(2505)
           ├─systemd(2317)───(sd-pam)(2318)
           ├─systemd-journal(440)
           ├─systemd-logind(961)
           ├─systemd-network(663)
           ├─systemd-resolve(678)
           ├─systemd-udevd(473)
           └─unattended-upgr(1041)───{unattended-upgr}(1174)

ユーザデータベースとグループのデータ分析

そうですね。

vagrant@ubuntu-bionic:~$ head /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
vagrant@ubuntu-bionic:~$ head /etc/group
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:syslog,ubuntu
tty:x:5:
disk:x:6:
lp:x:7:
mail:x:8:
news:x:9:

2021/03/16(火) 60分

  • 4.2 シェルと端末 から

MEMO

  • 端末の歴史を追う会
  • 2ページしか進まなかったのではめっちゃ最高。それだけ調べたり考えたりだからね。
  • 端末はそもそもハードウェアだったし、今使っている端末は、「端末エミュレータ」
  • 端末の話から、テレタイプの話になり、紙テープとか、コンピュータの歴史みたいな話になってちょっとした沼という感じだったw
  • このWebページすごい → 「IT関連の歴史」目次<木暮仁
    • 現代のコンピュータだけじゃなくて、計算尺とかそのへんの歴史からある

次回

  • 4.2 シェルと端末 の 仮想コンソール から

「端末」がイメージしにくい話

Linuxのユーザーインターフェースを分解する → 「シェル(Shell)」と「端末(Terminal)」

この2つの分離が最初は分かりづらいよね。

端末は「コンピュータのハードウェアのうち、人が直接に接する部分を指しています
つまり、現代の典型的な環境なら、手元のパソコンのことです。

端末 ==パソコン ってことになっちゃうから、イメージしにくいよね。

昔は1台のコンピュータをたくさんのユーザが一緒に使っていました。
つまり、大元となるコンピュータにはディスクもキーボードも付いておらず、それに「端末」がたくさんつながっている、という構成だったのです。

ここだよここ。まず、コンピュータなんだよね、パーソナルコンピュータじゃなくて。

初めてコンピュータがすでにパソコンだったし、キーボードもディスプレイもセット、っていうかそれも含めて「パソコン」だと思ってたわけで。

そういうことだわ。頭ではわかるけど、イメージわかないねぇ。

端末の歴史を追いかける

重要な参考文献

1910年代くらい: UNIX発明以前のテレタイプ

  • モールス信号でやりとりしてた時代から考えていくぞ!

歴史の話

  • テレタイプが登場して、モールス一辺倒だったアメリカの電信機が、ほぼすべて置き換えられてしまった!
    • NAZO: なんでテレタイプが人気だったんだろう? そんなにかんたんに置き換わるものなのか?
  • このへんは、1910年代くらいのお話
    • MEMO: 第一次世界大戦は1914年〜1918年だよ

1960年代: コンピュータ端末として活用されるテレタイプ

  • 1963年: ASCIIコード制定、ASR-33(Teletype Model 33)発売
  • 1967年: ASCIIに小文字が追加。Model 37

文字コードの標準化って大事なんだね。

テレタイプを用いることにより、タイプライタから直接入力するのではなく、あらかじめ紙テープに穿孔しておき読み込むことにより、作業効率を高めることができる。

そうなの? しらんけど。

1920年代〜1960年代

1931年、AT&Tは電信サービスTWX(TeletypeWriter eXchange)を開始した。電話と同様に、手動交換機により、どのテレタイプとも送受信できる。
このサービス方式は、その後テレックス(Telex:Teletype Exchange Service)といわれるようになり、1950年代には、通信社のニュース配信、気象通報、商取引など広く活用された。

1930年代は、米軍が通信を重視するようになった時代であり、テレタイプ社製Model 15は、第二次大戦中を通して軍用に約20万台が作られたという。

石油タンカーなど船舶との交信には、かなり後(1970年代)までテレックスが使われていた。当時、テレックス料金は文字数課金でかなり高く、文字を減らすことがコストダウンにつながった。それで、電文を略号に圧縮して送信、受け取った紙テープをコンピュータにかけて平文にするような工夫をしていた。

この辺の歴史も気になるところだが、我慢。

1970年代: UNIX黎明期のテレタイプ

構成

歴史の話

  • UNIX初版が 1971年
  • ASCII対応のテレタイプはUNIXの開発に用いられた!
    • 「テレタイプ・モデル33・35・37」ってやつらしい
      • ASR-33 - Wikipedia
      • もともとは、通信機器として開発されたが、ASCIIに対応していたので、コンピュータの入出力端末としても流用された
    • UNIXの開発に利用されたのは、ベル研究所の「PDP-11」
      • 「PDP-11」は「テレタイプ・モデル33」が2台接続されていた(画像みてね)
      • PDP-11 - Wikipedia
  • 1980年代でテレタイプは終焉
    • これは、ディスプレイを装備したオンライン端末やパソコンが出現したのが要因らしい

テレタイプモデル33(ASR-33)

1970-80年代?: ダム端末(初期のキャラクタ端末)

構成

  • 出力: 文字だけ表示可能なディスプレイ
    • デジタルの電卓みたいな感じをイメージすればいいらしい
    • 色もつけられない
  • 入力: キーボード ← もうタイプライターじゃないのね
  • その他: それに付随するハードウェア
  • 「ザ・端末」って感じ。動画をみるとわかる
Dec VT100

歴史の話

  • Dumb は 「バカ」みたいな意味らしい。なぜなら、ダム端末自体に計算能力がなかったから

1990年代?: ビットマップディスプレイを装備したキャラクタ端末

  • ビットマップディスプレイがあるのでつよい
  • X端末(X terminal)がその1例
  • まだまだ「ハードウェア」
  • というか、いわゆるパソコンと区別つかないわ。

現代: 端末エミュレータ

  • もともとハードウェアであった端末をソフトウェアにしてしまったやつ! だから、エミュレータ。
    • ってか、初期の端末はハードウェアだったってことは、ちょっとハッてするよね
  • CUIだった時代からGUIに移り変わっていったわけだけど、そこでまたCUIってのはおもろい

2021/03/17(水) 90分

  • 4.2 シェルと端末 から 5.3 標準入力、標準出力、標準エラー出力 まで

MEMO

  • 端末めっちゃわかってきた
  • タイプライターとかテレタイプ端末を動画で勉強するとかなりイメージつかめる。ありがたい。
  • デバイスとプロセスの紐付きの解像度がさらに高まる
  • ストリームと仲良くなってきた感がでてきた
  • ストリームだとか、プロセスといった存在がちょっと見えるようになってきた
  • はやくストリーム作ったりあーだこーだしたい気持ちがあふれる
  • GoSysProと合流できそう

次回

  • 5.4 ストリームの読み書き から

仮想コンソール ≒ 端末

実はLinuxでは物理的な端末がそのまま使われるわけではなく、仮想コンソール(Virtual Console)というものが間に挟まっています。

わかる。まー、ターミナルエミュレータと区別がわかりづらいやつね。

仮想コンソールとはソフトウェア的な端末ということですね。
コンソール(console)とは端末と同じような意味です

同じってことで!

コンソールと端末は同じとみなしてよさげ

What's the difference between a console, a terminal, and a shell? - Scott Hanselman's Blog

In the software world a Terminal and a Console are, for all intents, synonymous.

シェル、コンソール、端末、コマンドラインの違いを比較表で解説

一応、距離感のニュアンスはあるらしい(?)

コンソールとは(その機器に物理的に付属している)機器を操作するための装置、あるいはプログラム。一般的にローカルにある。

端末とは機器を操作する装置あるいはプログラムのこと。機器から物理的に離れたところにあるイメージ

「a」と表示するときに端末はどうなっているか?

キャラクタ端末(Dec VT100とかの時代 ≒ 1970〜1980年代とか)

端末の話まとめ → https://zenn.dev/mohira/scraps/c1886a998ccd4f#comment-414faee323ea14

「a」を表示したければ、「aという文字を表示しろ」とだけ指定すれば十分です。

素直なやり方だね。それはそうという感じ。

GUIを備えた端末(現代)

「a」を表示するときは、あらかじめメモリ上に「a」を表現する画像を作っておき、「このような画像を表示せよ」と命令します。

へ〜!

文字と数値を紐付けておいて、その数値だけを伝える。

で、そういった対応規則で、もっとも普及しているのが ASCII(American Standard Code for Information Interchange)。

ASCIIコードのLF(LineFeed)

  • カーソルを1行下に移動するようにキャラクタ端末に命令するときに使う
  • 数値10に対応する
  • C言語では \n で表現する
  • 大昔のテレタイプなら、紙を1行分上にずらすことになる
    • 紙が上にずれるから、文字が下に移動するね

CR と LF の違いは、テレタイプの動画みると、めっちゃわかりやすい!

ふつう(?)、「改行」といえば、次の行(LF)かつ行頭(CR)に移動するけど、本来って元々は別々。

テレタイプモデル19のわかりやすい動画

(1) Teletype Model 19 (and Model 15) Demonstration - YouTube

https://www.youtube.com/watch?v=jxkygWI-Wfs

紙テープを使ったほうがタイプライターより便利な感じがわかる説

このへんから → https://youtu.be/jxkygWI-Wfs?t=462

紙テープはやり直しができる!!! ミスったときのやり直し方法 → https://youtu.be/jxkygWI-Wfs?t=555

ってか、紙テープではなく手打ちで入力した場合のテレタイプって「やり直し」できないじゃん!!!

紙テープから再現するところ

ほー、おもしれ〜 → https://youtu.be/jxkygWI-Wfs?t=623

ASCIIコードのBEL

  • キャラクタ端末に送信するとベルが鳴る
  • 数値の7に対応する
  • 実際のテレタイプでベルが鳴る動画 → https://youtu.be/jxkygWI-Wfs?t=253

ベル音を鳴らすコード

ベル音っていうか、「ブゥッ」みたいな音だけどねw

#include <stdlib.h>
#include "stdio.h"

int main() {
    printf("\007");

    exit(0);
}

ベル機能はタイプライターの影響か?

これを繰り返し、印字部分がある程度右側に近づくと改行を促す意味で「チーン」とベルが鳴り、利用者に知らせる仕組みになっている。打鍵したい単語が右側部分に収まりそうにないと判断した場合は、ローラー部分に付いている改行レバーを掴んで印字位置を左側まで戻してやる。これを繰り返す事で、用紙を文字で埋めていく。
https://ja.wikipedia.org/wiki/タイプライター

この動画が最強にわかりやすい → (1) Speed Typing Test (Halda Star Typewriter) - YouTube

そもそも、テレタイプは、タイプライターの一種でよさそう

テレタイプ端末(テレタイプたんまつ、英語: teletype)は印刷電信機、テレプリンタ(英語: Teleprinter)、TTYともいい、今日ではほとんど使われなくなった電動機械式タイプライターで、簡単な有線・無線通信を通じて2地点間の印字電文による電信(電気通信)に用いられてきた。

ベルの音を利用するパターンもあったみたいだ

ニュース配信サービスや個別のテレタイプ端末では、重要なメッセージを受信した際にベルを鳴らす機能があった。例えば、UPI通信社のサービスでは、ベルを4回鳴らす "Urgent" メッセージ、5回鳴らす "Bulletin"、10回鳴らす FLASH などがあった。
https://ja.wikipedia.org/wiki/テレタイプ端末

タイプライターといえば、やはり、ルロイ・アンダーソン

端末がファイルになっている嬉しさを体感する実験

UNIXではいろいろなものをファイルとして表現する特徴がある

ところで、この発想やばいよね。文字通り、全てをファイルで扱うのだから。

んで、ファイルとして表現されていれば、ストリームでやりたい放題できるってことよ。

仮想コンソールを確認して、ストリームがつながっていることを実験する

  • macOSだけどいける
# ターミナルその1
$ tty
/dev/ttys000
$ echo "hello from ttys000" > /dev/ttys001
# ターミナルその2
$ tty
/dev/ttys001
$ hello from ttys000

ttys0ってだれ? すでに仮想コンソールがいくつかある!?

  • ttys0 は root がやっているし、起動したら生成される感じか?
# macOS
$ ls -l /dev | grep ttys0
crw-rw-rw-  1 root    wheel            4,  48  3 17 21:31 ttys0
crw--w----  1 mohira  tty             16,   0  3 17 21:38 ttys000
crw--w----  1 mohira  tty             16,   1  3 17 21:44 ttys001

こっちはpts(擬似端末)

  • 「キャラクタ」デバイスファイルの意味がわかった感あるね。そういえば。
# ターミナルその1
vagrant@ubuntu-bionic:~$ tty
/dev/pts/0

# ターミナルその2
vagrant@ubuntu-bionic:~$ tty
/dev/pts/1
# ターミナルその1
vagrant@ubuntu-bionic:~$ ls -l /dev/pts
total 0
crw--w---- 1 vagrant tty  136, 0 Mar 17 12:46 0
crw--w---- 1 vagrant tty  136, 1 Mar 17 12:46 1
c--------- 1 root    root   5, 2 Mar 17 12:37 ptmx

# ターミナルその2
vagrant@ubuntu-bionic:~$ tty
/dev/pts/0
vagrant@ubuntu-bionic:~$ echo hello > /dev/pts/1
vagrant@ubuntu-bionic:~$ echo "FROM pts1" > /dev/pts/1
# 別のターミナル
vagrant@ubuntu-bionic:~$ tty
/dev/pts/1
vagrant@ubuntu-bionic:~$ hello
vagrant@ubuntu-bionic:~$ FROM pts1

各種デバイス、端末、カーネル、ストリーム、プロセスの関係性

ここまで見えてくると面白いよね。現時点では、瞬間的にはイメージできない。けど、ゆっくり書けば理解できる感じ。

ってか、改めて考えると、いろんなキーボードを使って端末からあらゆる操作ができるのってすごいな。すごい。

シェルも単なるプロセス(コマンド)

  • ログイン時に起動される点だけ特殊
  • ユーザーからの命令を解釈して実行するプログラム
    • ex: echo を どっかに出力する命令に解釈してくれる感じね

5章 ストリームに関わるシステムコール

システムコールが先、ライブラリ関数が後

ストリームに関してはシステムコールとライブラリ関数の両方から話す必要がりますが、
本章ではまず下層に相当するシステムコールの話から始めます。
書籍によっては先にライブラリから話すこともありますが、本書では実用よりは内部構造を重視して、下層を先に話す子にしました。

こっちのほうがありがたい!

たった4つのシステムコールで、Linuxの入出力はほぼ語り尽くせる

まじかよ! こういうの勇気でるわ。

100個必要ですだと、心がやられるからね。

  1. read: ストリームからバイト列を読み込む
  2. write: ストリームにバイト列を書き込む
  3. open: ストリームを作る ← いまんとこ、こいつが最強だと思う
  4. close: 用済みのストリームを始末する

ファイルディスクリプタを使ってストリームに番号を振る

すでに説明してきたように、プロセスがファイルを読み書きしたり他のプロセスとやりとりをするときにはストリームを使います。

このへんでやったやつ

プロセスはとにかくストリームを扱うわけだけど、どのストリームを対象にするかわかってないといけないわけ。 で、その情報がファイルディスクリプタ。

ファイルディスクリプタは、プログラム上は整数データ。

ファイルディスクリプタは識別子なので、リンクなのは自然だね!

$ docker run --rm -it --name Bob ubuntu:latest /bin/bash

root@c8e5b1e5ffdf:/# ls -l /proc/1/fd
total 0
lrwx------ 1 root root 64 Jan  9 01:43 0 -> /dev/pts/0
lrwx------ 1 root root 64 Jan  9 01:43 1 -> /dev/pts/0
lrwx------ 1 root root 64 Jan  9 01:43 2 -> /dev/pts/0
lrwx------ 1 root root 64 Jan  9 01:43 255 -> /dev/pts/0

ストリームはカーネルの持ち物! プロセスからは直接触れません! ざんねん!

  • ストリームはカーネルが所持している
    • 実際には、ストリームを管理するデータ構造を持っているらしい
  • プロセスからダイレクトにストリームは見れない
  • プロセスはストリームではなくファイルディスクリプタを指定することしかできない
    • なんかしらんけど、「ストリームをプロセスに直接見せるわけにはいかない」らしい
      • 気持ちはちょっとわかる

ファイルディスクリプタを知る方法が難しい問題

プロセスはファイルディスクリプタを指定してストリームを操る。これはおk。

では、そのファイルディスクリプタはどうやって知ればよいわけ? っていう話

(え、どうやってやるんだろ?)

っていうか、ストリームってどうやってつくるんだろう? openシステムコールをやるとわかるのかな?

プロセス開始時から使えるストリーム3人衆

普通にシェルからプロセスが起動されたら、この3つのストリームは用意されているし、ファイルディスクリプタも固定されている!

  • ファイルディスクリプタ0番: 標準入力(Standard Input)
    • マクロ → STDIN_FILENO
  • ファイルディスクリプタ1番: 標準出力(Standard Output)
    • マクロ → STDOUT_FILENO
  • ファイルディスクリプタ2番: 標準エラー出力(Standard Error Output)
    • マクロ → STDERR_FILENO
    • 人間向けメッセージの出力先
      • エラーメッセージを標準出力に流すと見落としちゃうから、追加でストリームを用意したわけ

ストリームやプロセスを意識して、標準入力と標準出力を眺める

  • この辺の話はいつもつかっているけど、あらためて、ストリームやプロセスを意識してみると面白い
  • というか、ストリームを意識できるようになってきた、っていうほうが正しそう

catで、キーボードからの入力をディスプレイに出力する

$ cat
hello ← キーボード入力
hello ← 出力されている方
goodbye ← キーボード入力
goodbye ← 出力されている方

図5.3

リダイレクトで、ファイルとcatプロセスをつないでディスプレイに表示する

$ cat < hello.c 

図5.4

ここですごいのは、catコマンドは自分がファイルから読み込んでいることをしらないということです。

catプロセスは、あくまでストリームから流れきたバイト列をもらっているだけど、そのデータの出どころがファイルなのか別のプロセスからは知らない。というか知るのが不可能なのでは。
ストリームをうまいことつなぐのは、シェルがやってくれるわけだからね。

パイプのコンビネーション

$ grep print < hello.c | head

図5.5

grepheadをつなぐストリーム(=プロセス間をつなぐストリーム)をパイプと呼ぶのでしたね。そういえば。

2021/03/18(水) 3分

  • 5.4 ストリームの読み書き から

MEMO

  • GoSysProで白熱したので今日はちょっと読むだけ。
  • read(2)write(2) はそのまんまGoSysProでやった感じなので、おkおk
    • Goの io.Reader io.Writer はシステムコールの相似形だなあ。ほんと。
  • GoSysPro6章でソケットの話に入っているので相性いい感じある。

次回

  • 5.4 ストリームの読み書き から改めてやる

2021/03/19(金) 40分

  • 5.4 ストリームの読み書き

MEMO

  • 「ファイルを開く」って、改めて考えると意味分かんない問題
    • 言い回しというか、コロケーションを考えたいよね
      • ファイルを開く
      • ストリームを作る
      • ソケットを接続する、ソケットを開く? ソケットをつなげる
    • 「ストリーム」を、水道管みたいなイメージで捉えているのが問題なのかな? うーん、まあいいか。
  • ついに、ストリームの定義が公開!
  • man コマンドありがてえ

次回

  • 5.5 ファイルを開く から

read(2)

ストリームからバイト列を読み込むにはシステムコールread()を使います。

実際に、manでみると、read from a file descriptor ってなっているね。

「ストリーム」は『ふつうのLinux』の特有の使い回しだからね。

$ man 2 read
READ(2)                        Linux Programmer's Manual                        READ(2)

NAME
       read - read from a file descriptor

SYNOPSIS
       #include <unistd.h>

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

DESCRIPTION
       read()  attempts to read up to count bytes from file descriptor fd into the buf-
       fer starting at buf.

...

int じゃなくて、わざわざ ssize_t 型を返しているのはなぜ?

#include <unistd.h>

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

ssize_tは符号付き整数を表現するデータ型。それはおk。でも、それならintでよくね?

なぜわざわざ独自の方を定義するのでしょうか。それは、OSや使っているマシン(CPU)、カーネルのバージョンなどの違いに関係なく同じソースコードを使えるようにするためです。
これは単なる綺麗ごとではなく、現実に、カーネルのバージョンが違うだけで実際の型が変わってしまうこともあります。
横着してint型やlong型で済ませたりせず、プロトタイプで使われている型を使ってください。

確かに、Goでも int とした場合には、CPUアーキテクチャによって変わったりするなあ。そういえば。

返り値

read()は読み込みが問題なく完了したときは読み込んだバイト数を返します。

おk。

ファイル終端に達したときは0を、エラーが起きたときは-1を返します。
read()ではbufsizeバイトより少ないバイト数しか読まないケースも頻繁に発生するので、必ず戻り値をチェックしてください。

いや〜、なかなか大変そうだ。いつもつかっている高級言語は楽ちんなんだなあ。

'\0' を終端とするAPIorしないAPIの話

C言語の文字列(charの配列)には任意のバイト列が格納できますが、特に人間が読める文字列を格納する場合は'\0'で終端するのが慣習です。

へ〜。へ〜。

APIにも'\0'で終端されていることを前提とするものと、そうでないものがあります。

あー、これはハマりそうだわ。

read()は、'\0'終端を前提としない文字列を扱うグループに入ります。
したがって、読み込んだ文字列にも'\0'がついているとは限りません。
一方、printf()などは'\0'終端を前提とするAPIなので、read()で読み込んだ文字列をそのままprintf()にわたすのは間違いです。
両者を混ぜて使ってしまうと、単に結果が変になるだけでなく、セキュリティホールにもなり得ます。

write(2)

bufsizeバイト文をbufからファイルディスクリプタfd番のストリーム書き込みます。

ところで、「ストリームに書き込む」っていうのにちょっと慣れてきた感じある。

あと、io.Writerまんまだよね。

$ man 2 write
WRITE(2)                       Linux Programmer's Manual                       WRITE(2)

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

DESCRIPTION
       write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.
...

ストリームの定義

本書のストリームとは、
ファイルディスクリプタで表現され、
read()またはwrite()で操作できるもののことです。

まってました!

open()すれば、こんにちはストリーム

例えば、ファイルをopen()するとread()またはwrite()を実行できるものが作られますから、そこにはストリームがあります。
本書の後半で述べるように、パイプやネットワークソケットも、やはりストリームの仲間です。

TCPソケット通信のシステムコールをみたときにはすでに気づいていたぜ!

ところで、「ファイルをopenする」って言い回しは不思議に思えてきた件

日本語で「開く」といった場合は、ダブルクリックで「ファイルを開く」とか実行する的な感じがあるよね。

あれ? openってなんだ? ってか、なんでopen?

例えば、ソケットなら「ソケットに接続する(connect)」とか「つなげる」とかって表現のほうがいいような?

もうそういうもんって感じで慣れたのもあるけど、改めて考えると、わかりにくいような?

open(2) をちょっとだけ見よう

The open() system call opens the file specified by pathname.

これはいつもの open だな。

A call to open() creates a new open file description, an entry in the systemwide table of open files.
The open file description records the file offset and the file status flags (see below).
A file descriptor is a reference to an open file description

The open file descriptionか。

# man 2 open
OPEN(2)                        Linux Programmer's Manual                        OPEN(2)

NAME
       open, openat, creat - open and possibly create a file

SYNOPSIS
    ...
DESCRIPTION
       The  open()  system call opens the file specified by pathname.  If the specified
       file does not exist, it may optionally (if O_CREAT is  specified  in  flags)  be
       created by open().

    ...
       A call to open() creates a new open file description, an entry  in  the  system-
       wide table of open files.  The open file description records the file offset and
       the file status flags (see below).  A file descriptor is a reference to an  open
       file  description;  this reference is unaffected if pathname is subsequently re-
       moved or modified to refer to a different file.  For  further  details  on  open
       file descriptions, see NOTES.

次の5.5がopen(2)の話なので、またそのときに考えるか。

2021/03/20(土) 70分

  • 5.5 ファイルを開く から

MEMO

  • 実験できて楽しい
    • straceでみたら、わかった気になれるというか、ある程度の証明になる
    • システムコールがちょっとわかれば、GoやPythonで遊べる
    • 特に、Pythonは楽でいい(慣れているから)
      • 低水準のAPIあるじゃんね!
      • 「Pythonでわかるシステムコール」みたいな話もやれないことはない気がする
      • 高水準のありがたみとかもわかって最高
  • 未解決1: open() のフラグ O_* は何の略?
  • 未解決2: ストリームをカーネルが勝手に閉じてくれるのはいつ? それはどうやって確認する?

次回

  • 5.6 catコマンドを作る から

NAZO: open() の flag における O_ は 何の省略だろう?

The argument flags must include one of the following access modes:
O_RDONLY, O_WRONLY, or O_RDWR.
These request opening the file read-only, write-only, or read/write, respectively.

O以外の部分はこれだと思う。

  • O_RDONLY: ReaD ONLY
  • O_WRONLY: WRite ONLY
  • O_RDWR: ReaD WRite
  • O_CREAT: CREATe
  • O_EXCL: ??? ← なぞ!
  • O_TRUNC: TRUNCate
  • O_APPEND: APPEND

flag名 は Python(CPython)でも同じなのは、そりゃそうか

フラグとファイルモードの値についての詳細は C ランタイムのドキュメントを参照してください;
(O_RDONLY や O_WRONLY のような) フラグ定数は os モジュールでも定義されています。

open() じゃなくて os.open() を使えば、システムコールに近い世界で遊べる!

注釈
この関数は低水準の I/O 向けのものです。
通常の利用では、組み込み関数 open() を使用してください。
open() は read() や write() (そしてさらに多くの) メソッドを持つ ファイルオブジェクト を返します。
ファイル記述子をファイルオブジェクトでラップするには fdopen() を使用してください。

普通は open()関数を使うよねという話。というか、それしか使ってこなかったよね。

os.open()だと、返ってくるのがファイルディスクリプタだから、面倒そうだww

そう考えると、file objectってめっちゃ便利だな。

os.open()版のファイル操作

$ echo "hello world" > sample.txt
import os


def main():
    assert open('sample.txt').read() == 'hello world'

    fd = os.open('sample.txt', flags=os.O_RDONLY)

    print(fd)  # 3

    # 5バイト分読み込むことを繰り返す
    print(os.read(fd, 5))  # b'hello'
    print(os.read(fd, 5))  # b' worl'
    print(os.read(fd, 5))  # b'd' ← 終端にくる
    print(os.read(fd, 5))  # b'' ← 空文字バイト

    os.close(3)

    # OSError: [Errno 9] Bad file descriptor
    # ストリームが閉じているから読み込めない
    os.read(fd, 5)


if __name__ == '__main__':
    main()

close(2)

ファイルディスクリプタを「閉じる」。

つまり、ファイルディスクリプタfdに紐付いているストリームの後始末をする。

なお、「ストリームはclose()で後始末しないといけません」と先ほど言いましたが、
現実にはプロセスが終了する時点でそのプロセスが使っていたストリームはすべてカーネルが破棄してくれます。

じゃあ、実験してみようじゃないの。

ですが、プロセスが同時に使えるストリームの数には制限がありますし、
ストリームの向こうにプロセスがいる場合はclose()してやるまで相手も終了できないことがあります。

たしかに、通信の場合はこういうのあるか。

このような理由から、ストリームを使い終わったらちゃんとclose()するプログラムのほうが「よい」プログラムであるとは言えます。

いわゆるお作法的な話だけど、それでは足りないので、これも実験というか、体験しよう。

実験1: 明示的に close() しなくても、本当にストリームを閉じてくれるのか?

なお、「ストリームはclose()で後始末しないといけません」と先ほど言いましたが、
現実にはプロセスが終了する時点でそのプロセスが使っていたストリームはすべてカーネルが破棄してくれます。

Goで実験するぞ! 作戦はこう。

  • strace でシステムコールみたときに close(fd) となっている仮説
    • Yes → おk
    • No → では、いつ閉じているのか? また、それを見る方法はなにか?
# sample.txtを用意しておく
$ echo hello > sample.txt

file.Close() した場合

package main

import "os"

func main() {
	file, _ := os.Open("sample.txt")
	print(file)
	file.Close()  // closeしている!
}
$ go build file_open_and_close.go
$ strace ./file_open_and_close
...
openat(AT_FDCWD, "sample.txt", O_RDONLY|O_CLOEXEC) = 3
epoll_create1(EPOLL_CLOEXEC)            = 4
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2975252136, u64=139907239952040}}) = 0
fcntl(3, F_GETFL)                       = 0x8000 (flags O_RDONLY|O_LARGEFILE)
fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
write(2, "0xc00000e028", 12)            = 12
epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc00003060c) = 0
close(3)                                = 0  <-- fd3番 == sample.txtのストリームが closeされているね!
exit_group(0)                           = ?
+++ exited with 0 +++

file.Close() しない場合 ← closeされてねえええ あれ?

  • もしかして、straceの後(そんなのあるのか?)にclose()されている?
  • でも、たしかに、どこかのタイミングで、ファイルディスクリプタを閉じないと、永久に残っちゃうからね
  • 下のコードを繰り返し実行しても毎回fd3番だから、どこかでfd3番は閉じられているはず
  • 少なくとも、straceでは見れなかった
  • じゃあ、それはいつよ?
    • プロセスが終了するタイミングで、カーネルがファイルディスクリプタを閉じるらしいんだけど、その様子を目撃する方法ってありませんか? プロセスが閉じるタイミングのシステムコールとか見れないのかな?
package main

import "os"

func main() {
	file, _ := os.Open("sample.txt")
    // closeしていない
	print(file)
}
$ go build file_open_only.go
$ strace ./file_open_only
...
openat(AT_FDCWD, "sample.txt", O_RDONLY|O_CLOEXEC) = 3
epoll_create1(EPOLL_CLOEXEC)            = 4
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=901267112, u64=140704029884072}}) = 0
fcntl(3, F_GETFL)                       = 0x8000 (flags O_RDONLY|O_LARGEFILE)
fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
write(2, "0xc00000e028", 12)            = 12
exit_group(0)                           = ?       <-- fd3番はcloseされてなくね?
+++ exited with 0 +++

実験2: ファイルディスクリプタを閉じないと困る例

>>> import resource
>>> resource.setrlimit(resource.RLIMIT_NOFILE, (3, 3))  # ファイルディスクリプタの上限を3に設定する
>>> open('sample.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 24] Too many open files: 'sample.txt'

2021/03/21(日) 20分

  • 5.6 catコマンドを作る を写経しただけ

MEMO

  • JetBrainsIDEの設定 → 妥協策
  • cat.cを写経するだけ。解説は読んでない。

次回

  • 5.6 catコマンドを作る から再開

とりあえずcat.cだけ写経

眠かったので、写経するだけ。

CLionがあるので写経はあんまり困らない。

実装の内容も、素直なので難しくはない。

2021/03/28(日) 210分

  • 5.6 catコマンドを作る
  • 5.7 その他のシステムコール
  • 5.8 練習問題

MEMO

  • C, Go, Python で同時に実装するの楽しい。頭の混乱も楽しい。
  • どのレイヤーで実装するか悩むやつ。抽象化がよくわかる体験
  • Cのライブラリ関数の基礎がないから歯がゆいw
  • NAZO: 言語ごとのシステムコールの量の違いは何からきている?

次回

  • 6章 ストリームにかかわるライブラリ関数

cat を3言語で実装 → straceでみてみた → 違い と 違いの原因はなに?

  • open() write() read() あたりは同じ感じ。予想通り
  • ほかがいろいろ違う感じがある
  • Cはstraceの出力が少ない! なんで???
    • システムコールの利用回数が少ないってことだけど、なんで?
      • メインとなる部分以外はどんなシステムコールが必要なわけ?
    • あと、見やすくて助かる
$ wc -l strace_cat_*
      58 strace_cat_c.txt
     181 strace_cat_go.txt
     539 strace_cat_py.txt

cat.c

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

static void do_cat(char *path);

static void die(const char *s);

int main(int argc, char *argv[]) {
    int i;

    if (argc < 2) {
        fprintf(stderr, "%s: file name not given\n", argv[0]);
        exit(1);
    }

    for (i = 0; i < argc; ++i) {
        do_cat(argv[i]);
    }
    exit(0);


}

#define BUFFER_SIZE 2048

static void do_cat(char *path) {
    int fd;
    unsigned char buf[BUFFER_SIZE];
    int n;

    fd = open(path, O_RDONLY);
    if (fd < 0) {
        die(path);
    }

    for (;;) {
        n = read(fd, buf, sizeof buf);
        if (n < 0) {
            die(path);
        }

        if (n == 0) {
            break;
        }

        if (write(STDOUT_FILENO, buf, n) < 0) {
            die(path);
        }

    }
    if (close(fd) < 0) {
        die(path);
    }

}

static void die(const char *s) {
    perror(s);
    exit(1);
}
# gcc cat.c; strace -o strace_cat_c.txt ./a.out  ../README.md
execve("./a.out", ["./a.out", "../README.md"], 0x7ffc9c3138d8 /* 11 vars */) = 0
brk(NULL)                               = 0x56392f6d7000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffcc6473e60) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=12058, ...}) = 0
mmap(NULL, 12058, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4d7b1fb000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4d7b1f9000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d7b007000
mprotect(0x7f4d7b02c000, 1847296, PROT_NONE) = 0
mmap(0x7f4d7b02c000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f4d7b02c000
mmap(0x7f4d7b1a4000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7f4d7b1a4000
mmap(0x7f4d7b1ef000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f4d7b1ef000
mmap(0x7f4d7b1f5000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f4d7b1f5000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f4d7b1fa540) = 0
mprotect(0x7f4d7b1ef000, 12288, PROT_READ) = 0
mprotect(0x56392e26c000, 4096, PROT_READ) = 0
mprotect(0x7f4d7b22b000, 4096, PROT_READ) = 0
munmap(0x7f4d7b1fb000, 12058)           = 0
openat(AT_FDCWD, "./a.out", O_RDONLY)   = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@\21\0\0\0\0\0\0"..., 2048) = 2048
write(1, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@\21\0\0\0\0\0\0"..., 2048) = 2048
read(3, "\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\0\0\0\0\0\0"..., 2048) = 2048
write(1, "\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\0\0\0\0\0\0"..., 2048) = 2048
read(3, "\363\17\36\372H\203\354\10H\213\5\331/\0\0H\205\300t\2\377\320H\203\304\10\303\0\0\0\0\0"..., 2048) = 2048
write(1, "\363\17\36\372H\203\354\10H\213\5\331/\0\0H\205\300t\2\377\320H\203\304\10\303\0\0\0\0\0"..., 2048) = 2048
read(3, "\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\0\0\0\0\0\0"..., 2048) = 2048
write(1, "\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\0\0\0\0\0\0"..., 2048) = 2048
read(3, "\1\0\2\0%s: file name not given\n\0\0\0\0"..., 2048) = 2048
write(1, "\1\0\2\0%s: file name not given\n\0\0\0\0"..., 2048) = 2048
read(3, "\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\0\0\0\0\0\0"..., 2048) = 2048
write(1, "\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\0\0\0\0\0\0"..., 2048) = 2048
read(3, "\0\0\0\0\0\0\0\0\10@\0\0\0\0\0\0GCC: (Ubuntu 9.3"..., 2048) = 2048
write(1, "\0\0\0\0\0\0\0\0\10@\0\0\0\0\0\0GCC: (Ubuntu 9.3"..., 2048) = 2048
read(3, "_init_array_end\0_DYNAMIC\0__init_"..., 2048) = 2048
write(1, "_init_array_end\0_DYNAMIC\0__init_"..., 2048) = 2048
read(3, "\0\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\333\0\0\0\1\0\0\0"..., 2048) = 728
write(1, "\0\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\333\0\0\0\1\0\0\0"..., 728) = 728
read(3, "", 2048)                       = 0
close(3)                                = 0
openat(AT_FDCWD, "../README.md", O_RDONLY) = 3
read(3, "# \343\200\216\343\201\265\343\201\244\343\201\206\343\201\256Linux\343\203\227\343\203\255\343\202\260\343"..., 2048) = 507
write(1, "# \343\200\216\343\201\265\343\201\244\343\201\206\343\201\256Linux\343\203\227\343\203\255\343\202\260\343"..., 507) = 507
read(3, "", 2048)                       = 0
close(3)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

cat.go

package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

const BufferSize = 2048

func main() {
	argc := len(os.Args)

	if argc < 2 {
		_, _ = fmt.Fprintf(os.Stderr, "%s: file name not given\n", os.Args[0])
		os.Exit(1)
	}

	for _, path := range os.Args[1:] {
		doCat(path)
	}

}


func doCat(path string) {
	buf := make([]byte, BufferSize)

	file, err := os.Open(path)
	defer file.Close()

	if err != nil {
		log.Fatal(err)
	}

	for {
		// Goなら読み込んだバイト数じゃなくてerrで判定するよな
		_, err := file.Read(buf)

		if err == io.EOF {
			break
		}

		if _, err := io.WriteString(os.Stdout, string(buf)); err != nil {
			log.Fatal(err)
		}

		if err != nil {
			log.Fatal(err)
		}

	}
}
# go build cat.go; strace -o strace_cat_go.txt ./cat ../README.md

cat.py

import os
import sys


def main():
    argv = sys.argv
    argc = len(argv)

    if argc < 2:
        print(f'{argv[0]}: file name not given', file=sys.stderr)
        exit(1)

    for file_path in argv[1:]:
        do_cat(file_path)


def do_cat(file_path: str) -> None:
    buffer_size = 64

    fd = os.open(file_path, os.O_RDONLY)

    while True:
        byte = os.read(fd, buffer_size)

        if len(byte) == 0:
            break

        os.write(sys.stdout.fileno(), byte)


if __name__ == '__main__':
    main()
# strace -o strace_cat_py.txt python3 cat.py ../README.md

ファイルオフセット

同じファイルディスクリプタに対して何度もread()システムコールを呼ぶと、
必ず前回の続きが返ってきます。

うすうすわかっていた、というか、それを利用した実装をしていたわけですな。

ファイルオフセットはストリームの属性で、lseek(2)で操作できる

$ man 2 lseek
LSEEK(2)                                               Linux Programmer's Manual                                              LSEEK(2)

NAME
       lseek - reposition read/write file offset

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       off_t lseek(int fd, off_t offset, int whence);

DESCRIPTION
       lseek()  repositions the file offset of the open file description associated with the file descriptor fd to the argument offset
       according to the directive whence as follows:


lseek()l は long の l

昔はseek()というシステムコールがあって、その第2引数offsetの型はshortでした。
それをlongにしたのがlseek()というわけです。

型の方の、shortとlongか!

現在は引数の型が抽象化されてoff_t型になりましtが、名前は変わっていません。

dup(2) はどこで使うんだろうね。12章だってさ。

dup() と dup2()は、プロセスにかかわるシステムコールと一緒に使うことが多いので、第12章で改めて解説します。

$ man 2 dup
DUP(2)                                                 Linux Programmer's Manual                                                DUP(2)

NAME
       dup, dup2, dup3 - duplicate a file descriptor

SYNOPSIS
       #include <unistd.h>

       int dup(int oldfd);
       int dup2(int oldfd, int newfd);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <fcntl.h>              /* Obtain O_* constant definitions */
       #include <unistd.h>

       int dup3(int oldfd, int newfd, int flags);

DESCRIPTION
       The dup() system call creates a copy of the file descriptor oldfd, using the lowest-numbered unused file descriptor for the new
       descriptor.

ioctl(2) は 何回か見たことあるぞ!

UNIXのopen(), read(), close()という統一インターフェイスは非常に綺麗ですが、
その綺麗なモデルからはみ出した汚い部分は、すべてioctl()に寄せ集められているわけです。

The ioctl() system call manipulates the underlying device parameters of special files.

ぜーんぶやりそうな雰囲気がある説明だ。

それはそうと、何をやっているかは全然わからん。

$ man 2 ioctl
IOCTL(2)                                               Linux Programmer's Manual                                              IOCTL(2)

NAME
       ioctl - control device

SYNOPSIS
       #include <sys/ioctl.h>

       int ioctl(int fd, unsigned long request, ...);

DESCRIPTION
       The  ioctl()  system call manipulates the underlying device parameters of special files.  In particular, many operating charac-
       teristics of character special files (e.g., terminals) may be controlled with ioctl() requests.  The argument  fd  must  be  an
       open file descriptor.

       The  second  argument is a device-dependent request code.  The third argument is an untyped pointer to memory.  It's tradition-
       ally char *argp (from the days before void * was valid C), and will be so named for this discussion.

       An ioctl() request has encoded in it whether the argument is an in parameter or out parameter, and the  size  of  the  argument
       argp in bytes.  Macros and defines used in specifying an ioctl() request are located in the file <sys/ioctl.h>.


5.8 練習問題(1) catコマンドの改造

ファイルディスクリプタ(整数型)を直接扱う感じの実装にすると、拡張しづらくなるのがすごくよくわかる体験だった。

このへんは、Goのio.Readerがめっちゃ素敵だと思う。

2021/03/29(月) 150分

  • 6章 ストリームにかかわるライブラリ関数 から 6.10まで

MEMO

  • バッファリングの効果を実験できてよかった!
  • バッファリングの仕組みの意義や、あえてバッファリングしないときはどんなときか? という話はとっても大事だと思う
  • macOS版のDockerだとopen()じゃなくて、openat() システムコールなのは罠やん! GoSysやっててよかったぜ
  • わりかしスムーズな感じでやれている説がある
    • C言語だけど、Goの経験が生きているな。だいたいわかる。
  • puts とか getss って Stream の S か!
  • バッファオーバーフロー(Buffer overflow)
  • C言語の古めのライブラリ関数の罠よ!
  • システムコール解説中心のところは適宜読み流して、実装からやったほうが楽しいし、結局行ったり来たりできたほうがわかる

次回

  • 6.11 練習問題から

システムコールを直接扱うと面倒なこと

問題1: バイト数単位でしか読み書きができない!

行単位とか、文字数単位とかが都合良いよね! あるいは、ファイルの中身全部読み込むとか

これはホントそのとおり! 記述量も増えるし、メンドクセ(っ'-')╮ =͟͟͞(責任) ブォン

問題2: システムコール呼び出しは遅い

実は、システムコール呼び出しは関数呼び出しよりずっと遅いのです。

どうにか実験できないかな? というか、どう実験するとこれがハッキリわかるかな?

解決策: 標準入出力ライブラリ stdio (Standard I/O library)

システムコール呼び出し遅いよねとか、バイト単位での読み書きだるいよね問題を解消してくれるやーつ.

バッファリングによる、システムコール遅いよね問題の解決と実験

バッファリング(Buffering)の図解

「カーネルレベルのストリーム」と「プログラム」の間に、1つレイヤーを追加する発想。 たぶん、キャッシュメモリとかと同じ感じだと思う。

https://www.youtube.com/watch?v=QZUiU3jg09Q


実験1: バッファリングしないと遅いよねの実験

実験計画

  1. システムコールを直接呼び出す(==バッファリング非対応)コード
  2. stdio活用(==バッファリング対応)コード

両者において、どれだけシステムコールがよばれているかをstraceで確認する。

準備: 4096バイトのダミーデータをつくる

$ head -c 4096 /dev/urandom > dummy4096.dat
$ ls -l data.txt
-rw-r--r-- 1 root root 4096 Mar 29 09:29 dummy4096.dat

計測1: システムコールを直接呼び出す(==バッファリング非対応)コード

  • code → https://github.com/mohira/normal-linux-programming/blob/main/chap05/cat.c
  • 注意点
    • macOSでのDockerなので、システムコールが open(2) ではなく openat(2) になっていることに注意!
    • /dev/null にリダイレクトして、標準出力(catしたことによる出力)を捨てている == straceだけが見やすいね
  • 注目ポイント!
    • 4096バイトのファイルに対して、用意しているバッファーのサイズが2048バイトなので、read(), write()
      が2セット行われているところに注目!
$ gcc -o cat chap05/cat.c;
$ strace -e trace=open,openat,write,read,close ./cat dummy4096.dat > /dev/null
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
openat(AT_FDCWD, "data.txt", O_RDONLY)  = 3
read(3, "gp\373&\177\273\350'\302\220\357K\224\217P\200\343\210t\272\371\253\314\201{\3347\352\273\217\245'"..., 2048) = 2048
write(1, "gp\373&\177\273\350'\302\220\357K\224\217P\200\343\210t\272\371\253\314\201{\3347\352\273\217\245'"..., 2048) = 2048
read(3, "\35\26\221\212\337\360\25\203nO\5l\233(\20jC_\275r4Y Fs\336|\233ja\350\323"..., 2048) = 2048
write(1, "\35\26\221\212\337\360\25\203nO\5l\233(\20jC_\275r4Y Fs\336|\233ja\350\323"..., 2048) = 2048
read(3, "", 2048)                       = 0
close(3)                                = 0
+++ exited with 0 +++

計測2: stdio活用(==バッファリング対応)コード

$ gcc -o cat2_stdio chap06/cat2_stdio.c;
$ strace -e trace=open,openat,write,read,close ./cat2_stdio dummy4096.dat > /dev/null
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
openat(AT_FDCWD, "data.txt", O_RDONLY)  = 3
read(3, "gp\373&\177\273\350'\302\220\357K\224\217P\200\343\210t\272\371\253\314\201{\3347\352\273\217\245'"..., 4096) = 4096
read(3, "", 4096)                       = 0
close(3)                                = 0
write(1, "gp\373&\177\273\350'\302\220\357K\224\217P\200\343\210t\272\371\253\314\201{\3347\352\273\217\245'"..., 4096) = 4096
+++ exited with 0 +++

追加計測: バッファーのサイズを極端に小さくして、バッファリングの意義を体感する

$ gcc -o cat_128 chap06/cat_128.c
$ strace -e trace=open,openat,write,read,close ./cat_128 dummy4096.dat > /dev/null
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
openat(AT_FDCWD, "dummy4096.dat", O_RDONLY) = 3
read(3, "\31\250\17\301\n|pI\373\214E\231\364\235 \221G\245\213\304\317a\372\n|`\21X\30F\vM"..., 128) = 128
write(1, "\31\250\17\301\n|pI\373\214E\231\364\235 \221G\245\213\304\317a\372\n|`\21X\30F\vM"..., 128) = 128
read(3, "\222\2157\35\207 \237\\\230\242(eo\tI\4\321\355\352\f\320z\337jL\375\201\354\257\242<\201"..., 128) = 128
write(1, "\222\2157\35\207 \237\\\230\242(eo\tI\4\321\355\352\f\320z\337jL\375\201\354\257\242<\201"..., 128) = 128
read(3, "\334+\274?s\"\372kG\361\t\354\207l\230\266A@\202w\n\247wf\307\37`6\244\207\250\265"..., 128) = 128
write(1, "\334+\274?s\"\372kG\361\t\354\207l\230\266A@\202w\n\247wf\307\37`6\244\207\250\265"..., 128) = 128
read(3, "y\267\326+\306\24\305\227M6M\371\333t%\t/Q,x\271+_\265\341\230R\237\23K\177\225"..., 128) = 128
write(1, "y\267\326+\306\24\305\227M6M\371\333t%\t/Q,x\271+_\265\341\230R\237\23K\177\225"..., 128) = 128
read(3, "#\313\356\2109\240\31\345\324\n\nw\271\325t\275\260\337\323\335*\10\17\f\336\363$\351\307\316\343\241"..., 128) = 128
write(1, "#\313\356\2109\240\31\345\324\n\nw\271\325t\275\260\337\323\335*\10\17\f\336\363$\351\307\316\343\241"..., 128) = 128

...

read(3, "\204\23c\220#\351.\275\177\202g\32\307\206\345\314\22\237\352\206v\317\350\30\20\26\7\2434\333/S"..., 128) = 128
write(1, "\204\23c\220#\351.\275\177\202g\32\307\206\345\314\22\237\352\206v\317\350\30\20\26\7\2434\333/S"..., 128) = 128
read(3, "", 128)                        = 0
close(3)                                = 0
+++ exited with 0 +++

疑問: ところでバッファのサイズいくらなの? どこで決まるの?

  • 素直に考えれば、4096バイトだと思う。検証しよう
  • 場所は stdio.h の マクロ BUFSIZ

1024だと!?

これはつじつまが合わない。なんかおかしい。

// stdio.h
#define	BUFSIZ	1024		/* size of buffer used by setbuf */
#include <stdio.h>

int main() {
    printf("%d", BUFSIZ); // 1024
}

環境の違いだった!

  • Ubuntu(Docker) -> 8192
  • macOS -> 1024
# Docker環境
$ cat /usr/include/stdio.h | grep BUFSIZ
#define BUFSIZ 8192
   Else make it use buffer BUF, of size BUFSIZ.  */
# Docker
root@1f5b60e2445c:/src# gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# mac
$ gcc --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 11.0.3 (clang-1103.0.32.62)
Target: x86_64-apple-darwin19.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

バッファリングモードでの書き込みにおける2つの例外

  1. ストリームの向こうに端末がある場合
  2. アンバッファーモード
  3. 標準エラー出力

1. ストリームの向こうに端末がある場合は、バッファが満タンになるの待ってらんねぇぞ!

ストリームの向こうに端末がある場合は、バッファが一杯になるまで待たず、
改行('\n')が書き込まれた時点でwrite()を実行します。

向こうに端末があるということは、人間がその出力を読んでいる可能性が高いということです。
バッファが一杯になるのはそれこそ何時間も先かもしれないので、
ある程度まとまった単位(つまり行)だけたまったら即座に出力したほうが、
プログラムの応答が早くなり、人間にとって使いやすくなります。

完全に納得。こういう背景というか理由が大事だよなぁ。

2. アンバッファードモードは即時write()なモード

バッファリングしないよ〜ってんだから、そうですね。はい。

3. 標準エラー出力はすぐ知れるほうが嬉しいよね

これも重要な話だなぁ。速度じゃなくて、すぐに出力されることが大事!

FILE型の意義: ファイルディスクリプタ(整数)そのままは扱いづらいんじゃ!

ほんとこれよ。型ありがとう。

fdだけで、ファイル読み書きっていうか、ストリームの操作を実装するとよく分かる。

stdio対応版のcat

  • 6.3から先に写経。システムコールの説明を読むだけはつまらん
  • コードがかなりスッキリするのがわかる
#include <stdio.h>
#include <stdlib.h>

// stdio版catコマンド
// 標準力には対応していないよ
int main(int argc, char *argv[]){
    for (int i = 1; i < argc; ++i) {
        FILE *f;
        f = fopen(argv[i], "r");
        if (!f) {
            perror(argv[i]);
            exit(1);
        }
        int c;
        while ((c = fgetc(f)) != EOF) {
            // 標準出力がパイプだったりすると、そのパイプの先にいるプロセスが終了したあとに書き込みでエラーがおこるらしい
            // だから、エラーチェックしている
            if (putchar(c) < 0) {
                exit(1);
            }
        }
        fclose(f);
    }

    exit(0);
}

putchar(3) は、出力先が固定のAPI

  • putc(c, stdout) ってこと
  • getchar(3) も同じパターンで、 getc(stdin)
$ man 3 putchar
...
     The putchar() function is identical to putc() with an output stream of stdout.
...

stdio のAPIの説明では「file descriptor」はでてこないね

  • manを見る感じ、file descriptor という単語が出てこない!
  • すべて stream に置き換わっている感じ。API感あるわ〜。

fopen(3)open(2)システムコールに対応するAPI

  • いわゆる ファイルオープン だよね
$ man 3 fopen
FOPEN(3)                 BSD Library Functions Manual                 FOPEN(3)

NAME
     fopen, fdopen, freopen, fmemopen -- stream open functions

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
    ...

DESCRIPTION
     The fopen() function opens the file whose name is the string pointed to by path and associates a stream with it.

fclose(3)close(2)システムコールに対応するAPI

  • はい、そうですね。という感じ
$ man 3 fclose
FCLOSE(3)                BSD Library Functions Manual                FCLOSE(3)

NAME
     fclose, fcloseall -- close a stream

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include <stdio.h>

     int
     fclose(FILE *stream);

     void
     fcloseall(void);

DESCRIPTION
     The fclose() function dissociates the named stream from its underlying file or set of functions.  If the stream was
     being used for output, any buffered data is written first, using fflush(3).

     The fcloseall() function calls fclose() on all open streams.

fgets(3) は問題があるぞ!

fgets()には問題があります。
最大の問題点は、ちゃんと1行読んで止まったのか、バッファいっぱいまで書き込んで止まったのかを区別できないことです。

FGETS(3)                 BSD Library Functions Manual                 FGETS(3)

NAME
     fgets, gets -- get a line from a stream

#include <stdio.h>

int main(){
    char buf[4];
    fgets(buf, sizeof buf, stdin);

    printf("bufsize: %lu\n%s", sizeof buf, buf);
}
$ echo "hello\nworld" > hello.txt
$ gcc sample_fgets.c; ./a.out < hello.txt
bufsize: 4
hel

バッファオーバーフロー(Buffer overflow): gets() を使ってはならぬ! セキュリティ的にやべーぞ!

バッファオーバーフローとは、バッファをはみ出して使ってしまうことです。
「はみだして使う」というのは例えば、「char buf[1024]」と定義されたbufがあるときに、領域外のbuf[1025]やbuf[9999]に値を代入してしまうことです。

言っている意味はわかるんだけど、実際にどういう状態になっているんだろうね? 謎だ。

ということで、とりあえずバッファオーバーフローを起こすコードを持ってきた。Buffer Overflow Software Attack | OWASP Foundation

#include <stdio.h>

// https://owasp.org/www-community/attacks/Buffer_overflow_attack
int main(int argc, char **argv) {
    char buf[8]; // buffer for eight characters
    gets(buf); // read from stdio (sensitive function!)
    printf("%s\n", buf); // print out data stored in buf
    return 0; // 0 as return value
}
$ gcc sample_buffer_overflow.c

$ ./a.out
warning: this program uses gets(), which is unsafe.
1234567
1234567

$ ./a.out
warning: this program uses gets(), which is unsafe.
12345678
12345678
zsh: abort      ./a.out

別の説明

プログラムで何らかのデータを処理する場合、処理すべきデータや処理した後のデータを保持するためのメモリ領域が必要になります。これが「バッファ」です。

ユーザーの入力したテキストを一文字ずつ処理したり、圧縮された画像データを伸長したり、といった処理を行うコードでは、処理対象となるデータを保持するためのメモリ領域を、関数のローカル変数として用意したり、malloc()関数を使ってヒープメモリから割り当てたりします。

重要なことは、Cのコード上、これらのメモリ領域は固定長でしか記述できないということです。
処理の途中で倍のサイズが必要になったからといって、勝手にサイズを倍にしてくれるような便利な仕組みはありません。
必要であれば、倍のサイズのメモリ領域を確保し、データを移し替えて処理を続けるような仕組みを自分で実装する、それがC言語です。

この固定長であるメモリ領域にデータをコピーする際、用意された領域の外まで書き込みを行ってしまうことを「バッファオーバーフロー」といいます。

心に刻め

プログラミングの初心者は「システムに用意されているAPIはよいものだ、間違いなどない」と思い込んでいることがよくあります。
しかしgets()の例1つとっても、その認識は間違っていることがわかるでしょう。
特に、古くからあるlibcの関数は何かと問題が多いので、十分に注意してください。

はい。

あとは読み飛ばした

このへんは、サラリと読み流す感じにした。
単純に疲れたってのと、実験がセットじゃないのと、コード例がなかったからと、文量が少なかったから。

  • 6.5 固定長の入出力
  • 6.6 ファイルオフセット
  • 6.7 ファイルディスクリプタとFILE型
  • 6.8 バッファリングの操作
  • 6.9 EOF とエラー

Pythonのバッファサイズってこれか?

root@70247a5c964a:/src/chap06# python3 -c "import io; print(io.DEFAULT_BUFFER_SIZE);"
8192
ログインするとコメントできます