Closed399

『Goならわかるシステムプログラミング』をやっていく会 その1

mohiramohira

README

Goならわかるシステムプログラミング – 技術書出版と販売のラムダノート の読書会です。

スクラップの投稿上限を超えてしまうので、続きはこちら ↓↓

https://zenn.dev/mohira/scraps/ea040641f2f122

参考

おすすめ技術書としてLTしました(どんな本かイメージしやすいと思います)

2021-03-24 おすすめ技術書LT会 『Goならわかるシステムプログラミング』 - Google スライド

副読本(一緒に読むとべりーぐっど!)

io.Readerio.Writerとか(1〜3章)

  • Goから学ぶI/O
    • I/Oの観点で各種パッケージを横断しているのが最高

システムコールとかLinux(5章)

ソケット通信とかHTTPとかWebとか(6〜8章)

ファイルシステム(9〜10章)

開催実績

  1. 2021/01/14(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-6df2f066b2e489
  2. 2021/01/21(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-482718c035b9ff
  3. 2021/01/25(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-0935cdbde2738e
  4. 2021/01/28(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-2a1595f4b8256b
  5. 2021/02/01(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-d850e165b1853e
  6. 2021/02/08(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-421a068335e9de
  7. 2021/02/11(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-4c80d5186fc927
  8. 2021/02/15(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-05f9fdd3b7d163
  9. 2021/02/18(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-20c1b1a00452a0
  10. 2021/02/24(水) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-575cf8b664d608
  11. 2021/03/01(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-edd7ab9ee376c2
  12. 2021/03/08(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-b51cec6776f49f
  13. 2021/03/15(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-1f3dfc14e2c163
  14. 2021/03/18(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-10629942d8921c
  15. 2021/03/22(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-28a8a87bcf570f
  16. 2021/03/29(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-2ff13d5039582f
  17. 2021/04/05(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-af7a2d27e43716
  18. 2021/04/08(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-7d511f3ddea513
  19. 2021/04/12(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-70e9bc82ef92f6
  20. 2021/04/19(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-0be8157327f936
  21. 2021/04/26(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-498b4ef316e363
  22. 2021/04/29(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-2faaa11ddcef04
  23. 2021/05/03(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-8cd9583befe408
  24. 2021/05/10(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-823fa3684c7d4d
  25. 2021/05/13(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-fb8784937790a3
  26. 2021/05/17(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-5524e6e09ebfcd
  27. 2021/05/20(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-fb947f2e3ae581
  28. 2021/05/24(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-a8e64bc39b6616
  29. 2021/05/31(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-ab8564806159d5
  30. 2021/06/03(木) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-b36ca1c91d54df
  31. 2021/06/07(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-6317c0bc6531a6
  32. 2021/06/10(木) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-baa9273826bdfa
  33. 2021/06/21(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-a9a8060cea89ae
  34. 2021/06/28(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-1aac1295362fd2
  35. 2021/07/05(月) https://zenn.dev/link/comments/4d5c141a580e9f
  36. 2021/07/12(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-9d98f3822cf12d
  37. 2021/07/19(月) https://zenn.dev/link/comments/dd4f0248fafa58
  38. 2021/07/22(木) https://zenn.dev/link/comments/b28e8e9cee5c85
  39. 2021/07/26(月) https://zenn.dev/link/comments/2e2e5aa9bc03f3
  40. 2021/07/29(木) https://zenn.dev/link/comments/b5c661fb258b01
  41. 2021/08/02(月) https://zenn.dev/link/comments/88b3f862da086a
  42. 2021/08/12(木) https://zenn.dev/link/comments/cf49944cdbd18a
  43. 2021/08/26(木) https://zenn.dev/link/comments/4b84ff355d0a14
  44. 2021/09/06(月) https://zenn.dev/link/comments/7ee9e5309e548b
  45. 2021/10/11(月) https://zenn.dev/link/comments/4efa6f0dbe5715
  46. 2021/10/18(月) https://zenn.dev/link/comments/aca481829b6c79
  47. 2021/11/01(月) https://zenn.dev/link/comments/bd618e7fdee2d7
  48. 2021/11/08(月) https://zenn.dev/link/comments/42877bd8899b23
  49. 2021/11/15(月) https://zenn.dev/link/comments/ddaf0682c7dd0b
  50. 2021/11/22(月) https://zenn.dev/link/comments/3150074d5b9c8e
jnuankjnuank

【感想】

Go言語
シングルバイナリ、クロスコンパイル

C#とかだと.netランタイム
JavaだとJavaランタイム それぞれが必要。

でもGoはランタイムに依存しない

jnuankjnuank

OSの提供する機能を使ったプログラミング が、システムプログラミング。

普段使っているprint() の中身の実装を意識したもの

jnuankjnuank

Go言語の「インタフェース」「チャネル」がよくわからん

jnuankjnuank

type が 型の定義キーワード

type Writer interface {
	Write(p []byte) (n int, err error)
}
jnuankjnuank

goのインタフェースを満たすといったやつ。
Javaなどのインタフェースを実装するといった流れとは、逆な感じ。

このメソッドを実装しているから、Takerだよね。という逆説てきな感じ

type Talker interface {
   Talk() 
}

type Greeter struct {
    name string 
}

func (g Greeter) Talk() {
   fmt.Printf("Hello, my name is %s\n", g.name)
}
jnuankjnuank

&Greeter の頭を外しても、普通に動く

func main(){
	var talker Talker
	talker = &Greeter{"wozozo"}
	talker.Talk()
}
jnuankjnuank

副作用のあるメソッドは、レシーバ引数がポインタとなっている

jnuankjnuank

「Composition over inheritance」は、日本語だと「継承より合成」

jnuankjnuank

Writerが入れ子構造。
gzip.NewWriterがやってくれるのは、gz変換をやってくれる。
これを、更にファイル出力するか、画面に表示するかは、また別。
中継するっていうのは、シェルのパイプみたいなイメージ

	file, err := os.Create("test.txt.gz")
	if err != nil {
		panic(err)
	}
	writer := gzip.NewWriter(file)
	writer.Header.Name = "test.txt"
	io.WriteString(writer, "gzip.Writer example\n")
	writer.Close()
mohiramohira

2021/01/14(木) 240分

  • はじめに
  • 第1章(1.3以降は省略)
  • 2.1〜2.4

MEMO

  • Goのインタフェースに感動する。すげえや! なんというか、とてもキレイ。かっこいい。
    • あとから「インタフェースを満足させる」ことができるという柔軟性。外すのもカンタン。
    • 記述量が少なくて済む。implements とか extends とかみたいなイラネ。
  • Writerの入れ子に一瞬困惑したが、シェルのパイプだと思うと一撃で理解できた
  • システムコールは遅い → バッファリングという仕組み (『ふつうのLinuxプログラミング』 p.102)
    • 標準エラー出力はバッファリングしないのはなぜか?
  • gzcatコマンドは便利

次回開催メモ

  • 先にスレッドを立てて、そこにコメントする形式にしていこう。
jnuankjnuank

あらためてシステムコールの関数と見比べるとほぼ似ている

Go

func main() {
	file, err := os.Create("formated.txt")
	if err != nil {
		panic(err)
	}

	fmt.Fprintf(file, "hello %s\n", "aaaa")
	fmt.Fprintf(file, "数値 %d\n", 12345)
	fmt.Fprintf(file, "小数 %f\n", 98.2733)
	fmt.Fprint(os.Stdout, "hello, stdout")
}

C

        n = read(STDIN_FILENO, buf, sizeof buf);
        if (n == 0) break;
        write(STDOUT_FILENO, buf, n);
mohiramohira

2021/01/21(木) 240分

  • 2.5〜2.9
  • 2章の問題制覇

MEMO

  • Writerはだいぶいい感じに理解
  • ついでに、Goの基礎文法もちょいちょい理解しつつ。
  • 自作のBikkuriWriterを使ってもらった!
  • httpieが gzip理解できるの強すぎた(curlにはできんかった)
  • ソラプログラミングの威力!

次回開催メモ

jnuankjnuank

Goでは、Javaのインタフェースや、 純粋仮想関数

純粋仮想関数 ってなんだろ

jnuankjnuank

godoc -http ":6060" -analysis type

analysis を付けると、implements が見れる

jnuankjnuank

io.Writerとio.Readerを受け取れるような関数にするほうが、柔軟性の高い設計となれる

jnuankjnuank

io.Writerとio.Readerでは、小さなサイズのデータ単位で扱うことができ、大きなメモリ確保する必要がない。

io.WriteFileとio.ReadFileは一括で読み書きするため、バイト列が多くなると、より多くのメモリ確保が必要

jnuankjnuank

encoding/csvの中にあるWrite() を読むと、
カンマ区切りに変換したあとにbufio.Write() が呼ばれ、bufに書き込まれ、
Flush()で実際に書き込む

func main() {
	writer := csv.NewWriter(os.Stdout)
	writer.Write([]string{"abc","efg", "zyx"})
	writer.Write([]string{"1","2", "3"})
	writer.Write([]string{"A","B", "C"})
	writer.Flush()
}
jnuankjnuank

defer を使うと、その関数が終了するときに呼び出される。
正常終了、panicでも呼び出される


	gzipWriter := gzip.NewWriter(os.Stdout)
	defer gzipWriter.Flush()

jnuankjnuank

Empty Interface

interface{} は何でも受け取れる。
すべての構造体は、これを満たす

func (enc *Encoder) Encode(v interface{}) error {
jnuankjnuank

httpieは、gzipもちゃんと認識してくれる

http localhost:80
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 41
Date: Thu, 21 Jan 2021 14:33:09 GMT

{
    "Hello": "World"
}

jnuankjnuank

Writerをどんどん渡すやり方は、処理が逆順になっていく。

シェルだと入力→出力と順になるので、そっちに慣れていると最初はわからないのかも。
特にFlush()のタイミング

echo "{\"Hello\": \"World\"}"  | jq | tee teefile | gzip 
	w.Header().Set("Content-Encoding", "gzip")

	source := map[string]string{
		"Hello": "World",
	}

	gzipWriter := gzip.NewWriter(w)

	multiWriter := io.MultiWriter(gzipWriter, os.Stdout)

	encoder := json.NewEncoder(multiWriter)
	encoder.SetIndent("", "    ")
	encoder.Encode(source)

	gzipWriter.Flush()
mohiramohira

2021/01/25(月) 210分

  • 3~3.2.2 コピーの補助関数 まで

io.CopyBuffer め〜〜〜〜

MEMO

  • 本にでてくるソースコードの断片を、完全なコードにするトレーニングは効果的
    • ただし、自力でうまく再現できないこともある
    • まずは、必ず、公式のExampleを写経しよう!
  • Dockerは便利だ
    • strace でシステムコールを見るときに便利
    • strace -e trace=open,read,write,close ./hoge
    • $ docker run -it --name bob --mount type=bind,source=pwd,target=/go golang:1.15 bash
  • なかじまさんのUNIX知識がめちゃ役立つ → https://zenn.dev/jnuank/scraps/851984f99d0d69
    • あと dd コマンドかっけぇ〜!

次回開催メモ

  • 「疑問を残す」意識で行く → Zenn/GoLand/PDFのフォーメーションでいく
jnuankjnuank

copyBuffer() の内部実装であった構文。
これは型アサーションと言うらしい

	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}

https://go-tour-jp.appspot.com/methods/15

jnuankjnuank

CopyBuffer( ) は、WriteTo、ReaferFromの型をsrc、dstが実装している場合には、bufを使わないらしい

https://golang.org/pkg/io/#example_CopyBuffer

No need to allocate an extra buffer.
CopyBuffer is identical to Copy except that it stages through the provided buffer (if one is required) rather than allocating a temporary one. If buf is nil, one is allocated; otherwise if it has zero length, CopyBuffer panics.

If either src implements WriterTo or dst implements ReaderFrom, buf will not be used to perform the copy.
余分なバッファを割り当てる必要はありません。
CopyBuffer は、一時的なバッファを割り当てるのではなく、(必要な場合には) 提供されたバッファを段階的に使用することを除いては Copy と同じです。buf が nil ならば 1 つが確保され、そうでなければ長さが 0 ならば CopyBuffer はパニックに陥ります。

src が WriterTo を実装しているか,dst が ReaderFrom を実装している場合,buf はコピーを実行するためには使用されません.
jnuankjnuank

io.Copy() は、バッファを32kBで用意をしてくれる。し、使うたびに用意をする。

バッファサイズが大きすぎるとか、毎回割当するのが無駄であるなら、io.CopyBuffer()を使用するといいらしい

mohiramohira

2021/01/28(木) 210分

  • 3.2.3~3.5.2 エンディアン変換まで

MEMO

  • Zennに投げまくりは成功した
  • Reader の理解深まり〜〜〜。書きなれてきつつある感じ
  • エンディアン!
  • 16進数とかビットの話が整理できてよかったね!
  • byte型は、Byteだったね。すまん!

次回開催メモ

  • 次回はエンディアン! ちょっとコードが長めだね!
  • コードを貼っていくのはいい感じっぽいので継続。さらには、コメントで何を発見したかや、なんのコードなのか?を書いていくことを目指そう。
jnuankjnuank

io.CloserはディスクリプタをCloseするものだと思っている。
内部のunexportedなclose()ではこんな漢字で、FDにclose返している

	if e := file.pfd.Close(); e != nil {
		if e == poll.ErrFileClosing {
			e = ErrClosed
		}
		err = &PathError{"close", file.name, e}
	}
jnuankjnuank

ポインタを渡さないと、破壊操作ができない。

mohiramohira
package main

import "fmt"

type Wallet struct {
	Balance int
}

func (w Wallet) Deposit(i int) {
	w.Balance += i

	fmt.Println(w)
}

func main() {
	wallet := Wallet{}

	wallet.Deposit(10)

	fmt.Println(wallet.Balance == 10) // false
}
mohiramohira
package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
)

func main() {
	var b bytes.Buffer
	var rc io.ReadCloser

	//rc = b // コンパイルエラー!

	b2 := ioutil.NopCloser(&b)

	rc = b2

	fmt.Println(rc)
}
jnuankjnuank

NopCloser関数は、引数でio.Readerのポインタを貰うような宣言をしていないように見える???

func NopCloser(r io.Reader) io.ReadCloser {
	return nopCloser{r}
}

mohiramohira

勘違い

bufio.Reader は 構造体なのに、io.Readerというインタフェースと勘違いしちゃった!

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	writer := bufio.NewWriter(os.Stdout)

	readWriter := bufio.NewReadWriter(reader, writer)
	
	fmt.Println(readWriter)
}

Reader といっても、 io.Reader とは限らないよ!

// ここでの *Reader は bufio.go の Readerという構造体! io.Reader という インタフェースじゃないよ!
// NewReadWriter allocates a new ReadWriter that dispatches to r and w.
func NewReadWriter(r *Reader, w *Writer) *ReadWriter {
	return &ReadWriter{r, w}
}

なので、これはだめだよ!

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	reader := os.Stdin
	writer := os.Stdout

	readWriter := bufio.NewReadWriter(reader, writer)

	fmt.Println(readWriter)
}
jnuankjnuank

go言語自体、引数には、構造体(struct)か、インタフェースを指定できる。

どの言語でも普通だとは思うが、io.Reader,io.Writer, strings.Reader構造体などでややこしくなってきている

mohiramohira

ここまでの説明で、ファイルの作成、読み込み、インタフェース間のデータコピーの方法がわかったので、ファイルをコピーする処理も簡単に書けますね。ぜひ挑戦してみてください。

package main

import (
	"io"
	"os"
)

func main() {
	from, err := os.Open("go.mod")

	if err != nil {
		panic(err)
	}
	defer from.Close()

	to, err := os.Create("go.mod.txt")
	if err != nil {
		panic(err)
	}
	defer to.Close()

	io.Copy(to, from)
}
jnuankjnuank
	// Header maps header keys to values. If the response had multiple
	// headers with the same key, they may be concatenated, with comma
	// delimiters.  (RFC 7230, section 3.2.2 requires that multiple headers
	// be semantically equivalent to a comma-delimited sequence.) When
	// Header values are duplicated by other fields in this struct (e.g.,
	// ContentLength, TransferEncoding, Trailer), the field values are
	// authoritative.
	//
	// Keys in the map are canonicalized (see CanonicalHeaderKey).
	Header Header

https://tools.ietf.org/html/rfc7230#section-3.2.2

jnuankjnuank

これはなんで、BodyだけClose() があるのか

conn.Write([]byte("GET / HTTP/1.0\r\nHost: ascii.jp\r\n\r\n")) 
res, err := http.ReadResponse(bufio.NewReader(conn), nil)
// ヘッダーを表示してみる
fmt.Println(res.Header)
// ボディーを表示してみる。最後にはClose()すること 
defer res.Body.Close()

mohiramohira

https://play.golang.org/p/4viylg-l75M

package main

import (
	"bytes"
	"fmt"
)

func main() {
	var buf1 bytes.Buffer
	fmt.Println(buf1)

	buf2 := bytes.NewBuffer([]byte{229,136,157,230,156,159,230,150,135,229,173,151,229,136,151})
	//buf2 := bytes.NewBuffer([]byte{0x10, 0x20, 0x30})

	fmt.Printf("buf2.String()=%v\n", buf2.String())
	fmt.Printf("buf2.Bytes()=%v\n", buf2.Bytes())
	fmt.Printf("buf2.Len()=%v\n", buf2.Len())

	p2 := make([]byte, 16)
	_, _ = buf2.Read(p2)
	fmt.Println(p2)
	fmt.Println(string(p2))

	// bytes.NewBufferString
	buf3 := bytes.NewBufferString("初期文字列")
	fmt.Printf("buf3.String()=%v\n", buf3.String())
	fmt.Printf("buf3=%v\n", buf3)
	p3 := make([]byte, 16)
	buf3.Read(p3)

	fmt.Printf("p3=%v\n", p3)
	fmt.Printf("string(p3)=%v\n", string(p3))

}

mohiramohira
package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	// 1バイトずつ読み込んでみるコード
	sReader := strings.NewReader("hello")

	for {
		b, err := sReader.ReadByte()

		if err == io.EOF {
			break
		}
		fmt.Println(b, string(b))
	}

}

mohiramohira
package main

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

func main() {
	// HTTPレスポンスが格納されたテキストから HTTP/1.1 だけほしいときみたいな
	// $ curl -I example.com > response.txt
	file, err := os.Open("response.txt")
	if err != nil {
		panic(err)
	}

	lReader := io.LimitReader(file, 8)
	p := make([]byte, 8)

	_, _ = lReader.Read(p)

	fmt.Println(string(p))
}
mohiramohira
package main

import (
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	reader := strings.NewReader("Example of io.SectionReader\n")
	sectionReader := io.NewSectionReader(reader, -123, 7)
	w, err := io.Copy(os.Stdout, sectionReader)

	fmt.Println(w, err) // 0 strings.Reader.ReadAt: negative offset


}
mohiramohira

どっちエンディアンなのか? で結果が変わることがあるよ(回文はセーフ)

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

func main() {
	// 16ビットのビッグエンディアンのデータ(10進数で、4097)
	data := []byte{0x10, 0x01}
	var i int16

	// ビッグエンディアンとして読む込み
	binary.Read(bytes.NewReader(data), binary.BigEndian, &i)
	fmt.Printf("data: %d 0x=%x 0b=%b\n", i, i, i)

	// リトルエンディアンとして読み込む
	binary.Read(bytes.NewReader(data), binary.LittleEndian, &i)
	fmt.Printf("data:  %d 0x=%x 0b=%b\n", i, i, i)

}
mohiramohira

リトルエンディアンとビッグエンディアンの違い

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

func main() {
	// エンディアンによる結果の違い(16進数表記でみるとパターンめっちゃわかる)
	data := []byte{0x12, 0x34, 0x56, 0x78}
	var i int32

	// ビッグエンディアンとして読む込み
	binary.Read(bytes.NewReader(data), binary.BigEndian, &i)
	fmt.Printf("ビッグエンディアン: 0x=%x\n", i) // 0x=12345678

	// リトルエンディアンとして読み込む
	binary.Read(bytes.NewReader(data), binary.LittleEndian, &i)
	fmt.Printf("リトルエンディアン: 0x=%x\n", i) // 0x=78563412

}
jnuankjnuank

インタフェースと構造体をちょこちょこ見間違える

mohiramohira

byte

package main

import "fmt"

func main() {
	// byte型アレコレ
	// コンパイルエラー
	// []byte の 1要素は 8bit == 1Byte の領域になっている ← ってか、byte型って言ってるやん!
	// だから、 []byte{0x100} は アウト! 16進数の3桁ってことは、12bit必要で、8bitでは無理なのじゃ!
	// data1 := []byte{0x100} //  constant 256 overflows byte
	// data2 := []byte{0x101} //  constant 257 overflows byte

	// 頭に0がいっぱいつくのはセーフ!
	data3 := []byte{0xff}
	fmt.Println(data3) // [255]
	data4 := []byte{0x0ff}
	fmt.Println(data4) // [255]
	data5 := []byte{0x000000000000000000000000000000000ff}
	fmt.Println(data5) // [255]

}

mohiramohira

byte型 は uint8 のエイリアスだった!

// byte型 は uint8 のエイリアスだった!
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
// type byte = uint8

じゃあ、なんで unit8 じゃなくて byteを使うのか?

byte型は unit8 のシノニム であり、その値が小さな数値量ではなく生データであることを強調しています
『プログラミング言語Go』 p.58

実験

package main

import "fmt"

func main() {
	// 実験
	var b byte
	var i uint8
	b = i // 代入できる!

	fmt.Println(b == i)
}
mohiramohira

2021/02/01(月) 240分

  • 3.5.3 ~ 3.6

MEMO

  • PNGだの、チャンクだの、バイナリ解析だの。
  • PNGのチャンクはビッグエンディアン
  • エンディアン大事 → https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-345d4bd7ddc167
  • わざわざint32のデータ型を指定していた理由! が面白かった
  • バイナリ解析はパターンが決まっている。テキスト解析はパターンが決まってない。そう考えると、バイナリ解析のほうがラクという見方があるのは、なるほどな。
  • binary.Read がタフだったけど、ゆっくり丁寧に読んだらわかった!

次回開催メモ

  • 3.7から!
  • ストリーム楽しみ
jnuankjnuank

式で表現をするやり方のほうが見やすい!
(テスト駆動開発の文脈?)

chunks = append(chunks,
			io.NewSectionReader(file, offset, int64(length)+12))

chunks = append(chunks,
                // この方が、4バイト(長さ)+ 4バイト(種類)+ 長さ + 4バイト (CRC)というのを表現しやすい
		io.NewSectionReader(file, offset, 4+4+int64(length)+4))

mohiramohira
package main

import (
	"encoding/binary"
	"fmt"
	"io"
	"os"
)

func dumpChunk(chunk io.Reader) {
	var length int32 // 32にしているのはナゼなのかを考えてね
	binary.Read(chunk, binary.BigEndian, &length)
	buffer := make([]byte, 4)
	_, _ = chunk.Read(buffer)
	fmt.Printf("chunk '%v' (%d bytes)\n", string(buffer), length)
}

func readChunk(file *os.File) []io.Reader {
	var chunks []io.Reader

	// 最初の8倍とはスキップする(PNGシグニチャ)
	_, _ = file.Seek(8, 0)
	var offset int64 = 8

	for {
		// なぜ、int じゃなくて int32 なの?
		// 32 → 8bit x 4 == 4Bytes
		// あるチャンクの「長さ」の情報がほしいわけです。
		// で、「長さ」情報は、「4バイト」で管理されています。
		// なので、lengthも4バイト(==32bit)である必要がある!
		var length int32

		err := binary.Read(file, binary.BigEndian, &length)
		if err == io.EOF {
			break
		}

		chunks = append(chunks,
			io.NewSectionReader(file, offset, 4+4+int64(length)+4))

		// 次のチャンクの先頭に移動
		// 現在位置は長さを読み終わった箇所なので
		// チャンク名(4バイト) + データ長さ + CRC(4バイト)先に移動
		offset, _ = file.Seek(int64(4+length+4), 1)
	}
	return chunks
}

func main() {
	file, err := os.Open("20210201/Lenna.png")

	if err != nil {
		panic(err)
	}
	defer file.Close()

	chunks := readChunk(file)

	for _, chunk := range chunks {
		dumpChunk(chunk)
	}

}
jnuankjnuank

tEXtじゃなくて、textでもうまく画像が表示されたので、大文字小文字を区別していない可能性があるかも?

chunk 'IHDR' (13 bytes)
chunk 'text' (19 bytes)
ASCII PROGRAMMING++
chunk 'sRGB' (1 bytes)
chunk 'IDAT' (473761 bytes)
chunk 'IEND' (0 bytes)

mohiramohira

チャンク名は4文字なので英語の省略形が使われるのは当然の成り行きですが、大文字小文字が入り乱れてちょっと読みづらい表記になっています。
なぜなのか、それはPNGの仕様で大文字小文字に意味を定義付けているからです。

以上の規則が有りますが、PNG画像を表示する時にはチャンク名の大文字小文字を区別しない事になっています。

http://kimaguresoft.blogspot.com/2019/04/png8.html

jnuankjnuank

1文字目
大文字は必須チャンクを示します。
2文字目
大文字はPNG仕様で定められたチャンクを示します。
APNG拡張は独自の拡張チャンクなので2文字目が小文字になっています。
3文字目
必ず大文字を使う事になっています。将来的には何かしらの意味を付加するかもしれません。
4文字目
大文字はベタコピー禁止を示します。PNGエディタは他のチャンクとの連携を考慮してコピーするか変更するか判断しなくてはなりません。
小文字の場合は何も考えずにコピーしても表示画像に影響が無いチャンクです。

mohiramohira

バイナリ解析の次はテキスト解析です。バイナリ解析の場合は、読み込むバイト数が固定であったり、可変長データの場合も読み込むバイト数や個数などが事前に明示されていることがほとんどです。一方、テキスト解析ではデータ長が決まらず、スキャンしながら区切りを探すしかありません。そのため、探索しながら読み込んでいく必要があります。

こう言われると、テキスト解析のほうが、バイナリ解析よりも難しいという感じもあるな。

mohiramohira

3.6.3 その他の形式の決まったフォーマットの文字列の解析

郵便番号情報をPDFからコピーするのむずすぎた。

var csvSource = `13101,"100","1000003","トウキョウト","チヨダク","ヒトツバシ(1チョウメ)","東京都","千代田区","一ツ橋(1丁目)",1,0,1,0,0,0
13101,"101","1010003","トウキョウト","チヨダク","ヒトツバシ(2チョウメ)","東京都","千代田区","一ツ橋(2丁目)",1,0,1,0,0,0
13101,"100","1000012","トウキョウト","チヨダク","ヒビヤコウエン","東京都","千代田区","日比谷公園",0,0,0,0,0,0
13101,"102","1020093","トウキョウト","チヨダク","ヒラカワチョウ","東京都","千代田区","平河町",0,0,1,0,0,0
13101,"102","1020071","トウキョウト","チヨダク","フジミ","東京都","千代田区","富士見",0,0,1,0,0,0`
jnuankjnuank

前の2.4.7章でやっていたJsonEncoder→gzip→os.Createと入れ子にしていた時は、処理が分かりづらかったけど、
csv.Readerの入れ子構造はすんなり理解ができた。

おそらく、csvReader.Read()自体が、csvをパースした結果、1行ずつ返してくれる「返り値」がわかりやすい為。

func main () {
	reader := strings.NewReader(csvSource)
	csvReader := csv.NewReader(reader)
	for {
		line, err := csvReader.Read()
		if err == io.EOF {
			break
		}
		fmt.Println(line[2], line[6:8])
	}
}

こちらのJsonEncorderの入れ子構造は、 encoder.Encode を実行した時に、どこに書かれるのか? というのが、処理を遡らないと一見分かりづらい。

func main() {
	file, err := os.Create("test.json")
	if err != nil {
		panic(err)
	}

	writer := gzip.NewWriter(file)
	encoder := json.NewEncoder(writer)
	encoder.SetIndent("", "    ")
	encoder.Encode(map[string]string{
		"example": "encoding/json",
		"hello": "world",
	})

}
mohiramohira

車輪の再発明(のやりかけ)

  • io.ReadFull使えば良くない? → やってみよう → binary.Read の再発明をしていることに気づく!w
  • エンディアンの大事さがわかる
package main

import (
	"encoding/binary"
	"fmt"
	"io"
	"os"
)

func main() {
	file, err := os.Open("20210201/Lenna.png")

	if err != nil {
		panic(err)
	}
	defer file.Close()

	// PNGのシグニチャー分の8バイトは読み飛ばす
	_, _ = file.Seek(8, 0)

	// 最初のチャンクの長さ情報を取得しに行く
	b := make([]byte, 4)
	_, _ = io.ReadFull(file, b)

	fmt.Println(b) // [0 0 0 13] ← 末尾4バイト

	// 単なる int へのキャストはできない!
	//fmt.Println(int(b)) // コンパイルエラー!

	// バイト列をどの順序で読むか(エンディアン)によって結果が大きく違う
	fmt.Println(binary.BigEndian.Uint32(b))    //        13 ← [0 0 0 13]
	fmt.Println(binary.LittleEndian.Uint32(b)) // 218103808 ← [13 0 0 0]

}

mohiramohira

「エンディアンによる違い」だけを記事にしたら嬉しいかもと思ってきた。

jnuankjnuank
func main() {
	header := bytes.NewBufferString("Header")
	content := bytes.NewBufferString("Example")
	footer := bytes.NewBufferString("Footer")

	reader := io.MultiReader(header, content, content, content, footer)

	io.Copy(os.Stdout, reader)
}

これの結果は

HeaderExampleFooter

内部のRead()で、offsetが動く為、以降のcontentをReadしようとしても、読み取れない?

jnuankjnuank

io.Pipeは、goルーチンがもうちょっと理解できると、有用性がわかるかもしれない

mohiramohira

通常の HTTP レスポンスにおける Content-Disposition レスポンスヘッダーは、コンテンツがブラウザーでインラインで表示されることを求められているか、つまり、ウェブページとして表示するか、ウェブページの一部として表示するか、ダウンロードしてローカルに保存する添付ファイルとするかを示します。

Content-Disposition - HTTP | MDN

jnuankjnuank

HTTPのcontentでzipと指定しているので、ワンチャン、zip.NewWriterしなくていいんじゃね?
と思ったけど、駄目でした

func handler(w http.ResponseWriter, r *http.Request){
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition","attachment; filename=ascii_sample.zip")

	io.WriteString(w, "http.aaaaaaa sample")
	//zipWriter := zip.NewWriter(w)
	//defer zipWriter.Close()
	//
	//writer, err := zipWriter.Create("newFile.txt")
	//
	//if err != nil {
	//	panic(err)
	//}
	//
	//fmt.Fprint(writer, "writer http zip")
}
mohiramohira

Q3.3:zip ファイルの書き込み
上記の例では、newfile.txt という実際のファイルが、最初に作った出力先の ファイル file へと圧縮されます。では、実際のファイルではなく、文字列 strings. Reader を使って zip ファイルを作成するにはどうすればいいでしょうか。考えてみ てください。

意図がわからーーーーん!

mohiramohira
package main

import (
	"archive/zip"
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"strings"
)

// Q3.3
// zip ファイルの書き込み

func main() {
	file, err := os.Create("q33_1.zip")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	zipWriter := zip.NewWriter(file)
	defer zipWriter.Close()

	w1, err := zipWriter.Create("w1.txt")
	if err != nil {
		log.Fatal(err)
	}

	// せっかくなのでいろんな書き方で
	io.Copy(w1, bytes.NewBufferString("w1 ですよ!"))
	io.Copy(w1, strings.NewReader("hello from strings NewReader"))

	if _, err := w1.Write([]byte("w1 ですよ")); err != nil {
		log.Fatal(err)
	}
	if _, err := fmt.Fprint(w1, "w1 を Fprintで"); err != nil {
		log.Fatal(err)
	}

	// 2つめのテキストファイル
	w2, err := zipWriter.Create("w2.txt")
	if err != nil {
		log.Fatal(err)
	}

	io.Copy(w2, bytes.NewBufferString("w2 なんですよ"))
	io.Copy(w2, strings.NewReader("hello from strings NewReader"))
	if _, err := w2.Write([]byte("w2 なんですよ")); err != nil {
		log.Fatal(err)
	}
	if _, err := fmt.Fprint(w2, "w2 を Fprintで"); err != nil {
		log.Fatal(err)
	}

}
mohiramohira

2021/02/08(月) 180分

  • 3.7 ~ Q.3.3

MEMO

  • アーカイブ と 圧縮 の違い
  • Q3.3の問題文には納得いってない!!!

次回開催メモ

  • Q3.4から
mohiramohira

zipをやったので、tarもやってみた

package main

import (
	"archive/tar"
	"log"
	"os"
	"time"
)

func main() {
	file, err := os.Create("sample.tar")
	if err != nil {
		panic(err)
	}

	tarWriter := tar.NewWriter(file)
	body := []byte("# README")

	header := &tar.Header{
		Name: "README.md",
		Mode: 0600,
		Size: int64(len(body)),
		ModTime: time.Now(),
	}

	if err := tarWriter.WriteHeader(header); err != nil {
		log.Fatal(err)
	}

	if _, err := tarWriter.Write(body); err != nil {
		log.Fatal(err)
	}
}
mohiramohira

2021/02/11(木) 170分

  • Q3.5〜4.2まで

MEMO

  • io.Readerの練習はわりとすぐできた
  • スレッドとかプロセスとかの理解が、やはりかなり怪しい == これがわかれば世界広がる感ある
    • コンピュータの言葉で、説明できるようになりたい。たとえ話じゃなくてね。
  • goroutine は かなり遊べたと思う! いろいろ実験できるの最高
  • 並列と並行わかんね!

次回開催メモ

  • 4.3から
mohiramohira

Q3.5

package main

import (
	"fmt"
	"io"
	"os"
	"strings"
)

// Q3.5:CopyN
// io.Copy() と本章で紹介した構造体のどれかを使って、
// 3.2.2「コピーの補助関 数」で紹介した
// io.CopyN(dest io.Writer, src io.Reader, length int) を 実装してみてください。

func main() {
	r1 := strings.NewReader("hello world")
	_, _ = io.CopyN(os.Stdout, r1, 5)
	fmt.Println("\n======")

	r2 := strings.NewReader("hello world")
	_ = myCopyN(os.Stdout, r2, 5)
	fmt.Println("\n======")

	r3 := strings.NewReader("hello world")
	_ = myCopyN2(os.Stdout, r3, 5)
}

// io.LimitReader 使うほうが楽だ...!!!
func myCopyN2(w io.Writer, r io.Reader, n int64) error {
	lr := io.LimitReader(r, n)

	if _, err := io.Copy(w, lr); err != nil {
		return err
	}

	return nil
}

// io.Copy() を使わないバージョン
func myCopyN(w io.Writer, r io.Reader, n int64) error {
	buf := make([]byte, n)

	if _, err := io.ReadFull(r, buf); err != nil {
		return err
	}

	fmt.Fprint(w, string(buf))

	return nil
}
mohiramohira

Q3.6

package main

import (
	"io"
	"os"
	"strings"
)

var (
	computer    = strings.NewReader("COMPUTER")
	system      = strings.NewReader("SYSTEM")
	programming = strings.NewReader("PROGRAMMING")
)

func main() {
	var stream io.Reader
	// A progrAmming
	// S System
	// C Computer
	// I programmIng
	// I programmIng

	A := io.NewSectionReader(programming, 5, 1)
	S := io.LimitReader(system, 1)
	C := io.LimitReader(computer, 1)
	I1 := io.NewSectionReader(programming, 8, 1)
	I2 := io.NewSectionReader(programming, 8, 1)

	stream = io.MultiReader(A, S, C, I1, I2)

	io.Copy(os.Stdout, stream)
}
mohiramohira
  • CPU と I/O の関係? どっちが何を担当している?
  • スレッド と コア?
  • プロセス やら スレッドやら

このへんがわかると、並列処理と並行処理の話が、コンピュータの言葉で理解できると思う

jnuankjnuank

チャネルは、読み込み・書き込みで準備ができるまでブロックする機能である

io.Pipeに近い機能っぽい。

システムプログラミング的にはこの3つめがポイントっぽい

mohiramohira

チャネル

バッファが足りないのに、送信するとpanic

package main

func main() {
	// バッファがたりねぇ!
	tasks := make(chan string, 1)

	// データを送信(チャネルにデータを送る)
	tasks <- "cmake .."
	// fmt.Println(<-tasks) // バッファの空きを作ればpanicにならない

	// fatal error: all goroutines are asleep - deadlock!
	tasks <- "cmake . --build Debug"

}

空っぽなのに、受信しようとするとpanic

package main

import "fmt"

func main() {
	tasks := make(chan string, 1)

	fmt.Println(<-tasks) // これもpanic
}

goroutineをつかうといいかんじ

package main

import (
	"fmt"
	"time"
)

func main() {
	// バッファなしチャネル
	// バッファなしだけど、goroutineだから、panicにならない
	tasks := make(chan string)

	go func() {
		// データを送信(チャネルにデータを送る)
		fmt.Println("データ送信開始!")

		tasks <- "cmake .."
		fmt.Println("データ送信1個オワタよ!")

		tasks <- "cmake . --build Debug"
		fmt.Println("データ送信完了!!")
	}()

	go func() {
		// データを受信(チャネルからデータを受け取る)
		task1 := <-tasks
		fmt.Println("受信その1 OK",task1)
		task2, ok := <-tasks
		fmt.Println("受信その2 OK",task2, ok)

	}()

	time.Sleep(3 * time.Second)

}
mohiramohira

チャネルがまだオープンであれば

「チャネルがオープン」ってなに?

close()の説明あった!

package main

import "fmt"

func main() {
	// closeするとどうなるの?
	// 『プログラミング言語Go』 p.260
	// そのチャネルに値がこれ以上は送信されないことを示すフラグを設定します
	// その後に送信を試みるとpanicになります。
	// 閉じられたチャネルに対する受信操作は、値がなくなるまで送信された値を生成します。
	// 値がなくなったあとの受信奏者すぐに完了し、チャネルの要素型のゼロ値を生成します。
	ch := make(chan string, 10)

	ch <- "一富士"
	ch <- "弐鷹"

	close(ch)

	v, ok := <-ch
	fmt.Printf("v: %v ok: %v\n", v, ok)

	v, ok = <-ch
	fmt.Printf("v: %v ok: %v\n", v, ok)

	// すべて取り尽くしたので、falseになる
	// close()しておかないと、panic fatal error: all goroutines are asleep - deadlock!
	v, ok = <-ch
	fmt.Printf("v: %v ok: %v\n", v, ok) // v:  ok: false

	v, ok = <-ch
	fmt.Printf("v: %v ok: %v\n", v, ok) // v:  ok: false

	v, ok = <-ch
	fmt.Printf("v: %v ok: %v\n", v, ok) // v:  ok: false

}

たぶんこれ? その1

package main

func main() {
	ch := make(chan string, 10)

	ch <- "一富士"
	ch <- "弐鷹"

	close(ch) // チャネルを閉じたぞ!!!!

	ch <- "三なすび" // panic: send on closed channel
}

jnuankjnuank

close(チャネル) は、 これ以上データが送信されないことを意味する。

なので、 ch <- "データ" はできない。
だけど、受信はできる。

mohiramohira

『プログラミング言語Go』p.265

破綻なくスルスルかけるのすごい。

package main

import (
	"fmt"
	"time"
)

func main() {
	naturals := make(chan int)
	squares := make(chan int)

	// Counter
	go func() {
		for x := 0; ; x++ {
			naturals <- x
			time.Sleep(300 * time.Millisecond)
		}
	}()

	// Squarer
	go func() {
		for {
			x := <-naturals
			squares <- x * x
		}
	}()

	// Printer(メインゴールーチン)
	for {
		fmt.Println(<-squares)
	}
}
mohiramohira

closeする意味を知る例

(p.68) 逆に、終了情 報のシグナルを目的としたチャネルは、複数の goroutine が監視している場合でもす べてに終了を通知できるため、close() を行うほうがよいでしょう。

package main

import (
	"fmt"
	"time"
)

func main() {
	// closeすると、複数のgoroutineに一気に終了を通知できる(嬉しい例)
	done := make(chan bool)

	go func() {
		<-done
		fmt.Println("一富士")
	}()
	go func() {
		<-done
		fmt.Println("にたか")
	}()
	go func() {
		<-done
		fmt.Println("三なすび")
	}()

	go func() {
		fmt.Println("すごく重要な処理")
		time.Sleep(1000 * time.Millisecond)
		fmt.Println("みんな! 終了したぞ!")
		close(done)
	}()

	time.Sleep(3 * time.Second)
}
package main

import (
	"fmt"
	"time"
)

func main() {
	// closeしたほうが通知には便利を知るための、だめな例
	done := make(chan bool)

	go func() {
		<-done
		fmt.Println("一富士")
	}()
	go func() {
		<-done
		fmt.Println("にたか")
	}()
	go func() {
		<-done
		fmt.Println("三なすび")
	}()

	go func() {
		fmt.Println("すごく重要な処理")
		time.Sleep(1000 * time.Millisecond)
		fmt.Println("みんな! 終了したぞ!")

		done<-true // 1つのgoroutineの早いもの勝ちになる
		done<-true // 2つのgoroutineの早いもの勝ちになる
	}()

	time.Sleep(3 * time.Second)
}
mohiramohira

Goのチャネルで個数が未定の動的配列のやつは、PythonでいうGeneratorっぽいと思った

from time import sleep


def gen():
    i = 0

    while True:
        i += 1
        yield i

        if i == 10:
            break


def main():
    for num in gen():
        sleep(0.1)
        print(num)


if __name__ == '__main__':
    main()
mohiramohira

p.70の雰囲気

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	reader := make(chan int)
	exit := make(chan bool)

	go func() {
		// データ読み込み処理
		//time.Sleep(2 * time.Second)
		for i := 1; i <= 20; i++ {
			reader <- i
			time.Sleep(300 * time.Millisecond)
		}
	}()

	go func() {
		// 終了のお知らせ
		time.Sleep(2 * time.Second)
		exit <- true
	}()

	for {
		select {
		case data := <-reader:
			fmt.Println("読み込んだデータは", data)
		case <-exit:
			fmt.Println("本日は閉店しました")
			os.Exit(0)
		}
	}
}
mohiramohira

ポーリングの雰囲気なのか!?

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	reader := make(chan int)

	go func() {
		// データ読み込み処理
		time.Sleep(3 * time.Second)
		reader <- 123
	}()

	for {
		select {
		case data := <-reader:
			fmt.Println("読み込んだデータは", data)
			os.Exit(0)
		default:
			time.Sleep(500 * time.Millisecond)
			fmt.Println("確認中です")
		}
	}
}
mohiramohira

チャネルやゴルーチンの使い所がわかるとグッと来ると思う!

『プログラミング言語Go』の8章がよさそう

mohiramohira

2021/02/15(月) 120分

  • 4.3

MEMO

  • システムプログラミング感ある回だった
  • ファイルシステムは1つだけだと思っていたらそうでもなさそう!
  • 疑似ファイルシステムとマウントという戦法
  • lsof find / -inum {inode}

次回開催メモ

jnuankjnuank

プロセス間通信の話をしていたときの寄り道

A ターミナル

[root@9f2a352ba4ea linux]# cat | cat

Bターミナル

[root@9f2a352ba4ea fd]# ps auxw
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  11816  2916 pts/0    Ss   13:46   0:00 bash
root        20  0.0  0.0  11840  3032 pts/1    Ss   13:48   0:00 /bin/bash
root        44  0.0  0.0   4396   604 pts/0    S+   13:50   0:00 cat
root        45  0.0  0.0   4396   660 pts/0    S+   13:50   0:00 cat
root       209  0.0  0.1  51744  3356 pts/1    R+   14:27   0:00 ps auxw

Bターミナルで、ファイルディスクリプタを確認すると、pipeとなっている。

[root@9f2a352ba4ea /]# ll /proc/{44,45}/fd
/proc/44/fd:
total 0
lrwx------ 1 root root 64 Feb 15 13:51 0 -> /dev/pts/0
l-wx------ 1 root root 64 Feb 15 13:51 1 -> pipe:[277640]
lrwx------ 1 root root 64 Feb 15 13:51 2 -> /dev/pts/0

/proc/45/fd:
total 0
lr-x------ 1 root root 64 Feb 15 13:51 0 -> pipe:[277640]
lrwx------ 1 root root 64 Feb 15 13:51 1 -> /dev/pts/0
lrwx------ 1 root root 64 Feb 15 13:51 2 -> /dev/pts/0

ls -l --color=auto コマンドで、シンボリックリンクのdest側のファイルが存在しない場合は、赤くなる

なので、pipe:[xxx] も、存在しないファイル

find -inum で調べても存在しない

[root@9f2a352ba4ea /]# find ./ -inum 277640

ちなみに、/proc/44/fd/1 のinodeで調べると、見つけられる。

[root@9f2a352ba4ea /]# find / -inum 278674 -ls
278674    0 l-wx------   1 root     root           64 Feb 15 13:51 /proc/44/fd/1 -> pipe:[277640]
[root@9f2a352ba4ea /]#

jnuankjnuank

/procというのは、仮想的なファイルシステムをmountしているのみ。
/devも、それ。

/dev/pts/0 というのも、端末からつなげた(ログイン)した瞬間から、生成されるのであって、
ログアウトした瞬間には消えてしまう、はず。という予想。

mohiramohira

https://kernhack.hatenablog.com/entry/2014/05/31/190317

これじゃん! pipe も pipefs っていう疑似ファイルシステムからきたやつやん!

では、Linuxのほう見ていこう。 大まかなところはこんな感じに。

メモリ上にデータが置かれる
pipefsという擬似ファイルシステムによって実現される
pipefsはユーザーランドからは見ることができない
pipeは擬似ファイルとして扱われるのでファイルに関するデータ構造を持つ(inode、dentry等)

pipe は 疑似ファイル だが、ファイルのようなデータ構造を持っているのか!

Linuxの疑似ファイルシステムはメモリ上!

大雑把な違いとしてはV6の頃のpipeはストレージ領域にデータを持つ実体のあるファイルだったのに対して、Linuxの場合は擬似ファイルシステムによって実現されるメモリ上のみのファイルというところかな。

mohiramohira

シグナル送信

シグナルを送信して遊ぶコード

package main

import (
	"fmt"
	"os"
	"os/signal"
)

// シグナルの送り方
// 1. SIGTERM: `kill {ProcessID}`
// 2. SIGINT: Control+C
// 参考: https://qiita.com/Kernel_OGSun/items/e96cef5487e25517a576#%E6%A8%99%E6%BA%96%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB%E3%81%A8%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B%E6%A8%99%E6%BA%96%E5%8B%95%E4%BD%9C
func main() {
	fmt.Printf("Process ID: %v\n", os.Getpid())

	signals := make(chan os.Signal, 1)
	signal.Notify(signals)

	s := <-signals
	fmt.Printf("\nGot signal: %s \n", s)
}
mohiramohira

2021/02/18(木) 180分

  • 4.4 ~ 5.2.2 macOS におけるシステムコールの実装(syscall.Open)まで

MEMO

次回開催メモ

  • 5.2.3から
mohiramohira

select の case で チャネルへの送信をしたときが謎

package main

// https://golang.org/ref/spec#Select_statements
func main() {
	var ch chan int
	
	select {
	case ch <- 123: // これ該当するときはいつなの? コンパイルエラーではないし...
		println("sent!!")
	default:
		println("なにもないよ")
	}

}
mohiramohira

カーネルモード 特権モード スーパーバイザーモード

いろんな呼び方があるね!

複数の動作モードを持つCPUでは、そのうちの少なくとも1つは完全に無制限のCPU動作を許す。この無制限のモードを通常カーネルモード(あるいはスーパーバイザーモード、特権モード)と呼ぶ。他のモードは通常ユーザーモードと呼ばれるが、別の名で呼ばれることもある(「スレーブモード」など)

mohiramohira

カーネルの意義 と 何を調べているか?

カーネルは、処理の冒頭で、プロセスからの要求が正当なものかどうかをチェックします(たとえばシステムに存在しないような量のメモリを要求していないかどうか、など)。不正な要求であればシステムコールを失敗させます。ユーザプロセスからシステムコールを介さずに直接CPUのモードを変更する方法はありません(あったらカーネルが存在する意味がありません)。

武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 (Japanese Edition) (Kindle の位置No.339-343). Kindle 版.

jnuankjnuank

システムコールが遅い、という話は、
CPUモードの切替に由来しているのか?

あまり、システムコールが遅いというのがピンと来ていない

mohiramohira

モードを切り替えるから、システムコールは遅いという話になるのかな?

「システムコールは遅い」 だから バッファを使おうって話があったけど、その理解の糸口になるかも?

武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

mohiramohira

計算はユーザーモードでもできるけど、肝心の出力ができない!
なぜなら、出力先は別プロセスであり、別プロセスに情報をおくる == プロセス間通信 は カーネルモードじゃないとできない! つまり、システムコールがないとできない!

mohiramohira

5.1.3 システムコールがないとどうなるか? の「できませんラップ」が最高!

声に出して読みたい(実際、読んだ)

mohiramohira

5.2.2 macOS におけるシステムコールの実装(syscall.Open) の実装は結構違うので困惑

// 渋川本
func Open(path string, mode int, perm uint32) (fd int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil { return }
	r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
	use(unsafe.Pointer(_p0))
	fd = int(r0)

	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
// Go 1.16
func Pathconf(path string, name int) (val int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil {
		return
	}
	r0, _, e1 := syscall(funcPC(libc_pathconf_trampoline), uintptr(unsafe.Pointer(_p0)), uintptr(name), 0)
	val = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
jnuankjnuank

CPU動作モードで、
システムコールを発行すると、ユーザモード→特権モードに切り替わることができる、ということはわかった。

この切替は誰がやってくるの?
ユーザモードが勝手に特権モードになれるのか?
suコマンド的なやつ?

mohiramohira

sar コマンドで見張りつつ色々実験すると、どういうプログラムが、どっちのモードが動くかがわかる。そうすると、高速化したいときとかに、検討をつけやすくなると思う。

おもしろい

jnuankjnuank

docker起動コマンド

docker run -it --rm -w /home/linux --mount type=bind,source=$(pwd),target=/home/linux linuxpg bash

ユーザモードと特権モードでのCPUの使い方の違い

sarコマンドで、userとsystemのCPUの違いが見れることがわかった。
topコマンドでも見れる。

で、どんなプログラムが、user、systemのCPUに影響するのかを試したくなった。

yum install -y sysstat

何も出力をしない=システムコールを呼ばないだろうループ文

for i in $(seq 1 10000000 ) ; do : ; done

実際に1秒ごとにsarで計測

sar -P ALL 1

結果

急激に、userCPUが上がった。
ただし、最後の方はseqなのか、forで負荷が掛かったのか、systemCPUが100%になり、プロセスが死んでしまった

15:13:26        CPU     %user     %nice   %system   %iowait    %steal     %idle
15:13:27        all      9.63      0.00      4.81      0.00      0.00     85.56
15:13:27          0     13.98      0.00      4.30      0.00      0.00     81.72
15:13:27          1      5.32      0.00      5.32      0.00      0.00     89.36

15:13:27        CPU     %user     %nice   %system   %iowait    %steal     %idle
15:13:28        all     12.70      0.00     11.64      0.00      0.00     75.66
15:13:28          0     12.90      0.00     12.90      0.00      0.00     74.19
15:13:28          1     11.83      0.00      9.68      0.00      0.00     78.49

15:13:28        CPU     %user     %nice   %system   %iowait    %steal     %idle
15:13:29        all     28.24      0.00     36.47      0.00      0.00     35.29
15:13:29          0     18.39      0.00     21.84      0.00      0.00     59.77
15:13:29          1     38.10      0.00     52.38      0.00      0.00      9.52

15:13:29        CPU     %user     %nice   %system   %iowait    %steal     %idle
15:13:30        all     48.00      0.00     14.86      0.00      0.00     37.14
15:13:30          0     39.33      0.00     13.48      0.00      0.00     47.19
15:13:30          1     55.68      0.00     17.05      0.00      0.00     27.27
jnuankjnuank

userモードでのCPUが上がっているのであれば、ループや演算などで負荷がかかっているように見えるし、

systemでのCPUが上がっているのであれば、入出力などのシステムコール部分で、負荷が上がっているので、入出力を減らせるなら減らしてみるとか、
アプローチができそう

mohiramohira

2021/02/24(木) 240分

  • 5.2.3 から

MEMO

  • ちょっと延長しすぎた! ねるべき!
  • Windowsはちょっとややこしい
  • レジスタとCPUの違い
  • プログラムが実行されるっていどういうこと?
  • 「Goで記述した関数が、実際に実行されるまで」がちょっとわかった
  • 処理の流れは図に起こせばわかる(起こさないと厳しい)
  • カーネルのコードも意外と雰囲気で読める(ことがある)。コメントも重要!

次回開催メモ

  • Linux環境をつくっておこう。VMで。
  • 5.5から
mohiramohira

modeint なのは、番号で指定するから!

func Open(path string, mode int, perm uint32) (fd int, err error) {
    return openat(_AT_FDCWD, path, mode|O_LARGEFILE, perm)
}

OSに対してシステムコール経由で仕事をお願いするときには、どんな処理をしてほしいかを番号で指定します。
「5番の処理を実行してほしい」などとお願いするわけです。
SYS_OPENは、そのための番号として各OS用のヘッダーファイルなどから自動生成された定数です。

mohiramohira

zsysnum_darwin_amd64.go システムコールの定数群

// mksysnum_darwin.pl /usr/include/sys/syscall.h
// Code generated by the command above; DO NOT EDIT.

// +build amd64,darwin

package syscall

const (
	SYS_SYSCALL                        = 0
	SYS_EXIT                           = 1
	SYS_FORK                           = 2
	SYS_READ                           = 3
	SYS_WRITE                          = 4
	SYS_OPEN                           = 5
// 略
jnuankjnuank

OS に対してシステムコール経由で仕事をお願いするときには、どんな処理をして ほしいかを番号で指定します。

OSにシステムコールで経由するのが、数値だから

Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode),uintptr(perm))

mohiramohira

GoLandが検知してくれない && 代入できないじゃなくてOverFlowになるんか!

package main

func main() {
	var u uint32

	u = -1 // constant -1 overflows uint32 代入できないじゃなくてoverflowなのか!

	print(u)
}
jnuankjnuank

syscallは、windows版だと、syscall18まである

  • なぜ、3の倍数?
  • なぜ、Windows版だとsyscall18まであるのか?

https://golang.org/pkg/syscall/?GOOS=windows

mohiramohira

DLL

DLLファイルとは、Windowsのプログラムファイルの種類の一つで、様々なプログラムから利用される汎用性の高い機能を収録した、部品化されたプログラムのこと。標準のファイル拡張子は「.dll」。

https://e-words.jp/w/DLLファイル.html

リンカ

リンカとは、ソフトウェアの開発ツールの一つで、機械語(マシン語)で記述されたプログラムを連結、編集して実行可能ファイルを作成するソフトウェア。

https://e-words.jp/w/リンカ.html

jnuankjnuank

Call from go to c.

windows、solaris、illumos専用っぽい

func cgocall(fn, arg unsafe.Pointer) int32 {
	if !iscgo && GOOS != "solaris" && GOOS != "illumos" && GOOS != "windows" {
		throw("cgocall unavailable")
	}
jnuankjnuank

POSIX(Portable Oper- ating System Interface)

OS間で共通のシステムコールを決めることで、アプリケーションの移植性を高めるために作られたIEEE規格

mohiramohira

POSIX の X とは! 名称の由来

https://ja.wikipedia.org/wiki/POSIX

この規格は起源をさかのぼると、もともとはIEEEの規格番号やISO/IEC標準番号などで呼ばれていたものであるが、それが発展してゆく途中でPOSIXと改名された。最初、この一群の規格は「IEEE 1003」という名でつくられ、ISO/IEC標準での番号は「ISO/IEC 9945」だった。 1988年に「IEEE Std 1003.1-1988」と呼ばれていたころに、並行して「POSIX」という名称でも呼ばれ始めた。POSIXという名前はリチャード・ストールマンがIEEEに提案したものである[5]。

末尾の「X」はUNIX互換OSに「X」の字がつく名前が多いことからつけられた。IEEE側のほうも、番号で呼ぶよりもPOSIXという名称で呼んだほうが発音しやすく憶えやすいと気づき、これを採用すると決め、正式名称という位置づけとなった。

jnuankjnuank

POSIX でない Windows

どうやら、準拠しているものと準拠していないものがあるらしい

Unix系OS以外でも、(すべてではないが)POSIX指向のOSはあり、たとえばWindows NT系はWindows 7/Windows Server 2008 R2世代まではPOSIX 1.0に準拠しているPOSIXサブシステムを搭載しており、POSIXアプリケーションをそのサブシステム上で実行できる[22]。WTO/TBT協定では、非関税障壁として工業製品は国際規格を尊重して仕様を規定することを提唱しているため、米国政府機関のコンピュータシステム導入要件 (FIPS) としてPOSIX準拠であることが規定されていたためである。[23]Windows 2000までPOSIXサブシステムを搭載していたが、Windows XPからはServices for UNIXに同梱のInterixサブシステムに役割を譲り、Windows Server 2003 R2やWindows VistaからはSubsystem for UNIX-based Applications (SUA)となった[22]。Windows 10では、Windows 10 version 1607以降で、WSL(Windows Subsystem for Linux)でPOSIX準拠するように変化している。

jnuankjnuank

そもそも、ユーザーモード領域とカーネル領域とではスタックメモリも別に用意されています。

なんで、スタックメモリがユーザーモードとカーネル領域で違うかは、特に説明がない。
普通に考えると、分けたほうがいいとは確かにわかるが…。

mohiramohira

5.4.2 と 図5.4あたりの解釈 絶対に図に書いたほうがいい!!

登場人物が多すぎるのでな!

  • 未解決
    • AXレジスタに、システムコール番号を格納しているは誰!?

mohiramohira

カーネルモードとユーザーモード切り替えの正体!?

__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
	nr = syscall_enter_from_user_mode(regs, nr); // ← これじゃね?

	instrumentation_begin();
	if (likely(nr < NR_syscalls)) {
		nr = array_index_nospec(nr, NR_syscalls);
		regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
	} else if (likely((nr & __X32_SYSCALL_BIT) &&
			  (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
		nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
					X32_NR_syscalls);
		regs->ax = x32_sys_call_table[nr](regs);
#endif
	}
	instrumentation_end();
	syscall_exit_to_user_mode(regs); // これじゃね?
}
mohiramohira

wrmsrl は Write Read ModelSpecificRegister Load の略という説に1票

mohiramohira

2021/03/01(月) 120分

  • 5.5から

MEMO

  • システムコール、読んだら案外いける説
  • 素直にドキュメントを読むといい感じ! 特に man で引数とか説明が有効。

次回開催メモ

  • 5.7から
jnuankjnuank

シグナルハンドラの初期化で大量に呼び出される。

シグナルは、SIGINTとかそのあたり。

mohiramohira

strace で システムコール を追っているけど、全然踏み込めないwww

mohiramohira

このコードのシステムコールを理解したいぞ!!

package main

import (
	"os"
)

func main() {
	f, _ := os.Create("sample.txt")
	defer f.Close()

	f.Write([]byte("hello world"))
}

openat(AT_FDCWD, "sample.txt", O_RDWR|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
epoll_create1(EPOLL_CLOEXEC)            = 4
pipe2([5, 6], O_NONBLOCK|O_CLOEXEC)     = 0
epoll_ctl(4, EPOLL_CTL_ADD, 5, {EPOLLIN, {u32=5325136, u64=5325136}}) = 0
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=3865341576, u64=140371986509448}}) = -1 EPERM (Operation not permitted)
epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc0000a2da4) = -1 EPERM (Operation not permitted)
write(3, "hello world", 11)             = 11
close(3)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++```
mohiramohira

Pythonと比較するとわかる説!?

#!/bin/usr/python3
f = open("sample.txt", mode="w")

f.write("hello world")

f.close()
openat(AT_FDCWD, "sample.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
ioctl(3, TCGETS, 0x7ffe71d37ab0)        = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
ioctl(3, TCGETS, 0x7ffe71d379f0)        = -1 ENOTTY (Inappropriate ioctl for device)
stat("/go/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
stat("/go/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
stat("/go/src", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
openat(AT_FDCWD, "/go/src", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 4
fstat(4, {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
getdents64(4, /* 8 entries */, 32768)   = 232
getdents64(4, /* 0 entries */, 32768)   = 0
close(4)                                = 0
stat("/usr/lib/python3.7", {st_mode=S_IFDIR|0755, st_size=12288, ...}) = 0
stat("/usr/lib/python3.7/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1801, ...}) = 0
stat("/usr/lib/python3.7/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1801, ...}) = 0
openat(AT_FDCWD, "/usr/lib/python3.7/__pycache__/_bootlocale.cpython-37.pyc", O_RDONLY|O_CLOEXEC) = 4
fstat(4, {st_mode=S_IFREG|0644, st_size=1231, ...}) = 0
lseek(4, 0, SEEK_CUR)                   = 0
fstat(4, {st_mode=S_IFREG|0644, st_size=1231, ...}) = 0
read(4, "B\r\r\n\0\0\0\0\260-\34_\t\7\0\0\343\0\0\0\0\0\0\0\0\0\0\0\0\10\0\0"..., 1232) = 1231
read(4, "", 1)                          = 0
close(4)                                = 0
lseek(3, 0, SEEK_CUR)                   = 0
write(3, "hello world from Python", 23) = 23
close(3)                                = 0
jnuankjnuank

strace、もしかしたら実行された順ではない説?

mohiramohira

単純な動作を、複数の言語で実装して、システムコールを眺める、そして比較するってのはめっちゃ勉強になるな。

jnuankjnuank

【案】
いろんな言語で、open、write、closeをするプログラムをかいて、straceするのは勉強になりそうだ

jnuankjnuank

pollシステムコールは、イベント監視をして、ファイルディスクリプタからファイルディスクリプタへほにゃららするようなやつということを一旦理解。

mohiramohira

Rubyの場合

File.open("sample.txt", mode = "w"){|f|
  f.write("hello from Ruby")
}

システムコールはスッキリしている?

openat(AT_FDCWD, "sample.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 7
ioctl(7, TCGETS, 0x7ffc76ccbbf0)        = -1 ENOTTY (Inappropriate ioctl for device)
write(7, "hello from Ruby", 15)         = 15
close(7)                                = 0
jnuankjnuank

ゴルーチン、チャネルの場合は、pollシステムコールがすごく関わっていそう

mohiramohira

ファイルオープンしまくって、ファイルディスクリプタを枯渇させたら、openシステムコールのエラー見れるんじゃね?

mohiramohira

2021/03/08(月) 90分

  • 5.7から

MEMO

  • 5章突破! いろいろ実験できて面白かったね
  • ランタイムとかあれ? ってなる

次回開催メモ

  • 6.2から
jnuankjnuank

レジスタから返ってくるエラーは、数値でくる。
そのあとのエラー報告に関しては、言語仕様によって変わる

mohiramohira

どのシステムコールも、たいていは正常の場合には 0 より大きい数値、エラーの場 合には-1 を返します。

終了コードとは違うね。
終了コードはたいてい 0 が正常で、それ以外が異常だから。

jnuankjnuank

DPDK、ユーザ側でコンピュータの根幹をイジれそうなので、かなり怖い
リングプロテクションの考え方は無視している

mohiramohira

コンピュータ歴史博物館の館長的な存在が必要だw

jnuankjnuank

実用的なアプリケーションでは、それらの機能を使って、自分のアプリケーションに 必要なプロトコルを実装していくことになります。

独自プロトコルの実装ができるのか

jnuankjnuank

ソケットは、プロセス間通信の一種である。
他と違うのは、アドレスとポート番号がわかれば、ローカルのコンピュータ内だけではなく、外部のコンピュータとも通信が行える

jnuankjnuank

【疑問】UNIXドメインソケットは、コンピュータ内部のみ使える高速なプロセス間通信となっている。
    → だったら、他のプロセス間通信も、全部UNIXドメインソケットでやればいいんじゃないって思ったりする。

NGINX、サーバアプリ間は、プロセスが隔離されている可能性があるから、
単純なパイプはできない可能性。
だからUNIXドメインソケット通信を使っている?

mohiramohira

2021/03/15(月) 180分

  • 6.2から

MEMO

  • RPC, REST, GraphQL
  • RPC -> SOAP
  • キーワードは分散システム! 『Webを支える技術』を読むべし!
  • RPCの歴史とかを調べるには、Google検索を2000年代にするといい感じ。10〜20年くらい前の記事がちょうどよい。

次回開催メモ

  • 6.5 「Go 言語で HTTP サーバーを実装する」 から
mohiramohira

HTTP では改行が区切り文字と決められています
(中略)
昔はヘッダー行の途中で改行を許可していたりして簡単ではありませんでした。

改行じゃないのつらいw

jnuankjnuank

HTTP/1.0から、改行が区切り文字として扱われるようになった

jnuankjnuank

誤字?

「規格上も、 HTTP/2 の規格はバイナリ表現の紹介に限定されています。」

照会なのか?

専門用語で表現の紹介、というのがあったりするのだろうか

意味合い的には、

バイナリ表現しか使えないよ、ということだと思える

jnuankjnuank
  • RPC
  • REST
  • GraphQL

これらはHTTPの上で動いているっていうことがわかった。

jnuankjnuank

RPCは、HTTPベース上で使うこともあるが、
元々はインターネット以前からある仕組み、考え方

mohiramohira

RFCはIETFという組織が中心となって維持管理している、通信の相互接続性を維持するために共通化された仕様書集です。
「Request For Comment」という名前なのに仕様書だというのには少し違和感がありますが、これには歴史的経緯があります。
インターネットのもとになったネットワークはアメリカの国防予算で作られたため、仕様を外部に公開できませんでした。
そのため、「品質アップのためのご意見を広く世界から集める」という名目で仕様を公開することにした名残なのです。
『Real World HTTP 第2版』p.2-3

jnuankjnuank

ソケットがよくわかっていなかったが、こういうグルーピングっぽい

  • プロセス間通信
    • ソケット
      • TCP
      • UDP
      • UNIXドメインソケット
mohiramohira

○クライアントスタブとサーバースタブ
IDLファイルをIDLコンパイラでコンパイルすることで、クライアントスタブとサーバ スタブのソースファイルが生成されます。
クライアントスタブには、アプリケーション本体から呼び出されるサーバで実行される 関数を呼び出す処理が記述されます。
サーバスタブには、クライアントから要求のあったインタフェースの関数を呼び出し、 結果をクライアントに送り返す処理が記述されます。
クライアント・サーバ間の通信は、これらのスタブとRPCランタイムが自動的に行い、 指定された通信プロトコルを利用して通信を行います(図3-2)。

http://www7a.biglobe.ne.jp/~tsuneoka/win32sub2/3.html

jnuankjnuank

メッセージング型通信、ストリーム型通信と違いがあるらしい

TCPはストリーム型通信?
RPCはメッセージング型通信

分散処理する上では、メッセージング型通信が基本である。
RPCは、HTTPヘッダーなどのメタデータを処理する必要性は特にない

mohiramohira

『Webを支える技術』を読むとめっちゃいい!

特に、分散システムとか、Web以前の話とか、RPCとか、の歴史がでかい!

mohiramohira

「手続き(関数)」 と 「リソース」の区別が重要じゃん!!

mohiramohira

「リソース設計」を「URL設計」と勘違いしていたあの頃。

っていうか、「リソース」ってコト自体頭になかったよね。

mohiramohira

6章あたりは、この本がベスト!

  • 『Webを支える技術』
  • 『Real World HTTP』
mohiramohira

本では、RPCはHTTPベース前提っぽい雰囲気に感じちゃうとけど、RPCはWeb以前からある話。

リモートプロシージャコールというのは、別のコンピュータにある機能を、あたかも自分のコンピュー タ内であるかのように呼び出しを行い、必要に応じて返り値を受け取る仕組みです。リモートメソッド 呼び出し(RMI:Remote Method Invocation) と呼ばれることもあります。

RPCの歴史は古く1980年代にまで遡ります。RPCにはさまざまな方式があります。 インターネット の広まりとともにHTTPをベースとするRPCが何種類か登場しました。

mohiramohira

2021/03/18(木) 210分

  • 6.5 「Go 言語で HTTP サーバーを実装する」 から

MEMO

  • システムコールみるの楽しくなってきた
  • 複数のプログラミング言語やシステムコールといった色んな角度から考えるの楽しい
  • TCPとUDPが怪しいというか、わからん。
  • ストリーム型の通信とデータグラム型の通信の違いでアハ体験したい

次回開催メモ

  • 6.6 速度改善(1): HTTP/1.1 の Keep-Alive に対応させる から
jnuankjnuank

Go 言語の場合、サーバーが呼ぶのは Listen() メソッド、クライアントが呼ぶの は Dial() メソッドという具合に、API の命名ルールが決まっています。

なんでDialとListenにしたのだろうか。
システムコールだと、socket、bind、listen、connectとしているらしい。

なんか、ちょっとわかりづらい

mohiramohira

INET は IPv4 で INET6 は IPv6

$ man 2 socket
SOCKET(2)                   BSD System Calls Manual                  SOCKET(2)

NAME
     socket -- create an endpoint for communication

SYNOPSIS
     #include <sys/socket.h>

     int
     socket(int domain, int type, int protocol);

DESCRIPTION
     socket() creates an endpoint for communication and returns a descriptor.

     The domain parameter specifies a communications domain within which communi-
     cation will take place; this selects the protocol family which should be
     used.  These families are defined in the include file <sys/socket.h>.  The
     currently understood formats are

           PF_LOCAL        Host-internal protocols, formerly called PF_UNIX,
           PF_UNIX         Host-internal protocols, deprecated, use PF_LOCAL,
           PF_INET         Internet version 4 protocols,
           PF_ROUTE        Internal Routing protocol,
           PF_KEY          Internal key-management function,
           PF_INET6        Internet version 6 protocols,
           PF_SYSTEM       System domain,
           PF_NDRV         Raw access to network device
jnuankjnuank

データグラム型とストリーム型の違いがいまいちわかっていない。

ストリーム型は、データの終端があるみたい。
データを切り刻んで、送っていく。

データグラム型は、意味のあるデータ単位、エンティティで送っている?

jnuankjnuank

HTTPリクエストを送るクライアントコード

package main

import (
	"bufio"
	"net"
	"net/http"
	"net/http/httputil"
)

func main(){
	// ソケットに対してConnectする(ストリームを接続する)
	conn, err := net.Dial("tcp", "localhost:8888")
	if err != nil {
		panic(err)
	}
	request, err := http.NewRequest("GET", "http://localhost:8888", nil)
	if err != nil {
		panic(err)
	}
	// この時点でリクエストを送っている
	request.Write(conn)
	response, err := http.ReadResponse(bufio.NewReader(conn), request)
	if err != nil {
		panic(err)
	}
	dump, err := httputil.DumpResponse(response, true)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(dump))
}


mohiramohira
  • UDPソケットを使った、HTTP通信
  • データグラム型通信だけど、プロトコルにTCPを使うやつ

これを、GoとかPythonとかで実装したら、きっと意味がわかると思う。頑張ろう。

jnuankjnuank

どこでHTTPリクエスト送っているのか? というのを意識が向きすぎているから、
このコードの意味がちょっとわかっていっていなかった。

Dialでソケットと接続(ストリーム)。
そのファイルディスクリプタに対して、Writeシステムコールを発行している。

と、考えるととらえやすい

mohiramohira

straceでソケット通信のシステムコール

vagrant@ubuntu-bionic:~$ strace -o output.txt nc example.com 80 
GET / HTTP/1.1

HTTP/1.1 400 Bad Request
Content-Type: text/html
Content-Length: 349
Connection: close
Date: Thu, 18 Mar 2021 15:27:07 GMT
Server: ECSF (oxr/831D)

<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        <head>
                <title>400 - Bad Request</title>
        </head>
        <body>
                <h1>400 - Bad Request</h1>
        </body>
</html>
# たぶんこのへんから

socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("93.184.216.34")}, 16) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(48073), sin_addr=inet_addr("10.0.2.15")}, [28->16]) = 0
close(3)                                = 0

socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET6, sin6_port=htons(80), inet_pton(AF_INET6, "2606:2800:220:1:248:1893:25c8:1946", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
close(3)                                = 0

socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK, IPPROTO_TCP) = 3
fcntl(3, F_GETFL)                       = 0x802 (flags O_RDWR|O_NONBLOCK)
fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("93.184.216.34")}, 16) = -1 EINPROGRESS (Operation now in progress)
select(4, NULL, [3], NULL, NULL)        = 1 (out [3])
getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
read(0, "GET / HTTP/1.1\n", 16384)      = 15
write(3, "GET / HTTP/1.1\n", 15)        = 15
read(0, "\n", 16384)                    = 1
write(3, "\n", 1)                       = 1
read(3, "HTTP/1.1 400 Bad Request\r\nConten"..., 16384) = 504
write(1, "HTTP/1.1 400 Bad Request\r\nConten"..., 504) = 504
read(3, "", 16384)                      = 0
shutdown(3, SHUT_RD)                    = 0
read(0, "\n", 16384)                    = 1
write(3, "\n", 1)                       = 1
close(3)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++
jnuankjnuank

今回、Go、C、Python、C#、システムコールといろんな角度から見ることができたのは大きい

mohiramohira

2021/03/22(月) 180分

  • 6.6 速度改善(1): HTTP/1.1 の Keep-Alive に対応させる から

MEMO

  • Keep-Alive感を得るまでが長かった!(本のコードだけでは直接確認できない)
    • 最終的にはシステムコールを見る。そうすると納得せざるを得ない
  • curl -v localhost{,,,,,,} というbashの記法便利ワロタw
  • write(2)send(2)sendto(2) の違いに迫れた!
    • man 見たほうがいいぞ!
  • Webの話は MDN でも検索しような!

次回開催メモ

  • 6.7 速度改善(2): 圧縮 から
mohiramohira

Connectionヘッダーの keep-alive

$ http https://example.com -v
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: example.com
User-Agent: HTTPie/2.4.0
mohiramohira

いつ、Keep-Alive終わるの?

Keep-Aliveによる通信は、クライアント、サーバーのどちらかが次のヘッダーを付与して接続を切るか、タイムアウトするまで接続が維持されます。

Connection:Close

サーバーからKeep-Aliveの終了を明示的に送るのは簡単ではありません。実際にはタイムアウトで接続が切れるのを待つことになります。

『Real World HTTP』

jnuankjnuank

HTTP1.1からkeep-aliveは標準搭載

試しに送ってみると、
Connection:keep-alive になっている。

$ http https://example.com -v | head
GET / HTTP/1.1
User-Agent: HTTPie/1.0.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Host: example.com



HTTP/1.1 200 OK

mohiramohira

ContentLengthがわからないと、、、

response := http.Response{
  StatusCode:    200,
  ProtoMajor:    1,
  ProtoMinor:    1,
  //ContentLength: int64(len(content)),
  Body:          ioutil.NopCloser(strings.NewReader(content)),
}
$ http -v localhost:8888
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8888
User-Agent: HTTPie/2.4.0



HTTP/1.1 200 OK
Connection: close

Hello World
jnuankjnuank

Keep-Aliveに対応している、していない複数リクエストを送ってみるテスト

テスト用コマンド

curl -v localhost:8888{,,} 

6.5.1でのkeep-alive対応していないver

% curl -v localhost:8888{,,} 
*   Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< 
Hello World
* Closing connection 0
* Hostname localhost was found in DNS cache
*   Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#1)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< 
Hello World
* Closing connection 1
* Hostname localhost was found in DNS cache
*   Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#2)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< 
Hello World
* Closing connection 2

6.5.2でのkeep-alive対応版

こちらは、 Re-using existing connection!と出ているので、コネクションを使いまわしている。

% curl -v localhost:8888{,,}                
*   Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 12
< 
Hello World
* Connection #0 to host localhost left intact
* Found bundle for host localhost: 0x55d7510e2af0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 12
< 
Hello World
* Connection #0 to host localhost left intact
* Found bundle for host localhost: 0x55d7510e2af0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 12
< 
Hello World
* Connection #0 to host localhost left intact

mohiramohira

Keep-Alive対応のサーバー: ずーっとConnection #0

$ curl -v localhost:8888{,,}
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 12
<
Hello World
* Connection #0 to host localhost left intact
* Found bundle for host localhost: 0x7facc7904fd0 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 12
<
Hello World
* Connection #0 to host localhost left intact
* Found bundle for host localhost: 0x7facc7904fd0 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 12
<
Hello World
* Connection #0 to host localhost left intact
* Closing connection 0

Keep-Alive対応してないサーバー: つどつどConnection → Connection #0 Connection #1 Connection #2

$ curl -v localhost:8888{,,}
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.64.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
<
Hello World
* Closing connection 0
* Hostname localhost was found in DNS cache
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#1)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.64.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
<
Hello World
* Closing connection 1
* Hostname localhost was found in DNS cache
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#2)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.64.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
<
Hello World
* Closing connection 2
jnuankjnuank

システムコールでも見てみた

no-keep版

 % cat no-keep.txt| grep socket | grep TCP 
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5
% cat no-keep.txt| grep connect | grep -v write
connect(5, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (現在処理中の操作です)
connect(5, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (現在処理中の操作です)
connect(5, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (現在処理中の操作です)

keep-alive版

 % cat keep-alive.txt| grep socket | grep TCP
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5
 % cat keep-alive.txt| grep connect | grep -v write 
connect(5, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (現在処理中の操作です)

まさにこれ!

各strace取得コマンド

keep-aliveなしのサーバ立てた状態で

strace -o no-keep.txt curl -v localhost:8888{,,}     

keep-aliveありのサーバ立てた状態で

strace -o keep-alive curl -v localhost:8888{,,}  
mohiramohira

send(2) と `write(2)って何が違うんだ???

発端: TLSありのときの通信で、send()じゃなくてwrite()システムコールが使われていたこと

$ strace -o example.out curl  https://example.com
% cat example.out| grep "write(5"
write(5, "\26\3\1\2\0\1\0\1\374\3\3\304\24F\16\337\223\216\n\262z\210Y\"\300\334\24b\24\260\204@"..., 517) = 517
write(5, "\24\3\3\0\1\1\26\3\3\2\0\1\0\1\374\3\3\304\24F\16\337\223\216\n\262z\210Y\"\300\334"..., 523) = 523
write(5, "\27\3\3\0E \237\35\224C\206:\324\242\32U*X#\227\33v&\366\324\321\262\301A\322#\277"..., 74) = 74
write(5, "\27\3\3\0]\352\n\376S\245Ez\252\243{\325\315\311\347z\213\331\0254pJ\32\263\275u\207\0"..., 98) = 98
write(5, "\27\3\3\0\23\241n\322\17@\372}\261]\10\2009\341\2563\23\377\25c", 24) = 24

マニュアル見ようぜ!

$ man 2 send
SEND(2)                   Linux Programmer's Manual                   SEND(2)

NAME
       send, sendto, sendmsg - send a message on a socket

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

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
The send() call may be used only when the socket  is  in  a  connected state  (so that the intended recipient is known).  

send(2)write(2) が表現可能!

The only difference between send() and write(2) is the presence of flags.
With a zero flags argument, send() is equivalent to write(2).

send(2)sendto(2) は 等価な書き方がある!

  • send(2) == TCP とか、 sendto(2) == UDP ってわけじゃないやん!

Also, the following
call

send(sockfd, buf, len, flags);

is equivalent to

sendto(sockfd, buf, len, flags, NULL, 0);

実際、TCPなのに、システムコールをみると send()じゃなくて、sendto()が使われている謎がとけた!

% cat keep-alive.txt | grep send  
sendto(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 78, MSG_NOSIGNAL, NULL, 0) = 78
sendto(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 78, MSG_NOSIGNAL, NULL, 0) = 78
sendto(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 78, MSG_NOSIGNAL, NULL, 0) = 78
jnuankjnuank

【疑問】
curlコマンドで叩いた時に、writeとsendtoシステムコールが使い分けられたいるのは、なんで?

jnuankjnuank

echo localhost:8888{,,} がなんでできるかについて

https://qiita.com/ine1127/items/6e5fe80f4a9c64509558#-3

以下のような文字列A{文字列B, 文字列C}ってやると、文字列Aと文字列B、文字列Aと文字列Cみたいな総掛けみたいな出力ができるんだけど、

それを応用して空文字を指定しているだけみたい。

% echo {a,b,c}
a b c
% echo a{a, b, c}
aa ab ac
mohiramohira

2021/03/29(月) 120分

  • 6.7 速度改善(2): 圧縮 から

MEMO

  • straceでゴリ押すと、捗る
  • Pythonで提供されている
  • ソケット通信まわりのシステムコールの理解がカギ!
  • サーバーとクライアントでごっちゃになりやすい
  • ソケット通信の流れを、なんとなくのイメージ捉えてしまうと、混乱する → straceで意味わからんになる
  • TODO: なかじまさんのマシンに、manのデータベースか何かをいれる
  • HTTPヘッダってすごくね?

次回開催メモ

  • 6.8 速度改善(3): チャンク形式のボディー送信
mohiramohira

HTTPヘッダってすごいな〜

  • Client: Accept-Encodinggzip でよろしく〜
  • Sever: Content-Encoding ならいけるぜよ

これでおわるので、すごい。

っていうか、自分でやった感じがないね。

うまくいくと、こうなる

$ go run 671_gzip_client.go
Access: 0
HTTP/1.1 200 OK
Content-Length: 46
Content-Encoding: gzip


Hello World (gzipped)
HTTP/1.1 200 OK
Content-Length: 46
Content-Encoding: gzip


Hello World (gzipped)
HTTP/1.1 200 OK
Content-Length: 46
Content-Encoding: gzip


Hello World (gzipped)

サーバー側でContent-Encodingを外すとこうなる

$ go run 671_gzip_client.go
Access: 0
HTTP/1.1 200 OK
Content-Length: 46

��H����/�IQ�H��,(HM������>
��H����/�IQ�H��,(HM������>HTTP/1.1 200 OK
Content-Length: 46

��H����/�IQ�H��,(HM������>
��H����/�IQ�H��,(HM������>HTTP/1.1 200 OK
Content-Length: 46

��H����/�IQ�H��,(HM������>
��H����/�IQ�H��,(HM������>%
mohiramohira

ソケット通信まわりのシステムコールがカギじゃんけ!

  • open(2)とかread(2)とかのシステムコールは結構意味がわかっていたからstraceして理解が深まった
  • 一方で、ソケット通信まわりは、システムコールがわかってないから、よくわからんよねってなるやーつ

この図解が最強!

『基礎からわかるTCP/IP ネットワーク実験プログラミング第2版』p.103

これもわかりやすい!

pythonでsocket通信を勉強しよう - Qiita

jnuankjnuank

サーバ側のstraceを見てみた。

socket→listen→accept4→で、コネクションが開始できている。

socketで3番ディスクリプタを作り、それを引数にaccept4が7番のディスクリプタを作成。

結果的に、7番のディスクリプタへwrite、readをしている


socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (そのようなファイルやディレクトリはありません)
socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (そのようなファイルやディレクトリはありません)
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
accept4(3, 0xc0000c9c30, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (リソースが一時的に利用できません)
accept4(3, {sa_family=AF_INET, sin_port=htons(34490), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_CLOEXEC|SOCK_NONBLOCK) = 7
getsockname(7, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(7, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(7, SOL_TCP, TCP_KEEPINTVL, [15], 4) = 0
setsockopt(7, SOL_TCP, TCP_KEEPIDLE, [15], 4) = 0
accept4(3, 0xc0000c9c30, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (リソースが一時的に利用できません)
accept4(3, {sa_family=AF_INET, sin_port=htons(34496), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_CLOEXEC|SOCK_NONBLOCK) = 7
getsockname(7, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(7, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(7, SOL_TCP, TCP_KEEPINTVL, [15], 4) = 0
setsockopt(7, SOL_TCP, TCP_KEEPIDLE, [15], 4) = 0
accept4(3, 0xc0000c9c30, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (リソースが一時的に利用できません)
jnuankjnuank

setsockopt(7, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0 で、KEEPALIVEをしているのを見て、
「KEEP-ALIVE対応していないサーバだったら、ここの値が変わるのでは?」と思ったけど、特に変わらなかった。

これは、サーバ側がずっと接続を待ち受けているからと思ったりする。

クライアント側で見てみたけど、これも同じだった。
ConnectionのRe-Useしているけど、Socketレベルでは同じように使っているのかもしれない

mohiramohira

ソケット通信は、Pythonで見たほうがわかりやすいぞ!

わかりやすいぞ!

  1. システムコールとsocketモジュールの命令軍が、システムコールとほぼ同じ名前だから
  2. Goよりなれているから ← でかい!
  3. (GoやCと比べて)短いコードですむから(エラーハンドリングするかどうかの違いだと思うけど、とりあえず正常系がほしいのでPython有利)

socket --- 低水準ネットワークインターフェース — Python 3.9.2 ドキュメント

偉大なる先人の成果!

https://kazuhira-r.hatenablog.com/entry/2020/02/27/002840

あとは、システムコール見ればツモ!

jnuankjnuank

新しい言語を覚えるときに、どこまでやれば入門したと言えば良いのか。

「その言語でウェブサーバが立てられたら良い」

mohiramohira

2021/04/05(月) 150分

  • 6.8 速度改善(3): チャンク形式のボディー送信

MEMO

  • 6章おわり! 3分の1おわり!
  • HTTP/2 も気になるけど、まちがいなく、HTTP/1.1やHTTP/1.0の理解が大事!
  • 『Real World HTTP』での扱いと比較すると面白い!
    • セキュリティとか認証とかの話のウェイトが大きい
  • パイプライニングはストリームに転生した
  • トルネコの大冒険からFFへ

次回開催メモ

  • 7章 UDP ソケットを使った マルチキャスト通信 から
mohiramohira

curlはバッファリングしている!

$ curl -N localhost:8888
$ man curl
 -N, --no-buffer
              Disables the buffering of the output stream. In normal work  situa-
              tions,  curl  will  use a standard buffered output stream that will
              have the effect that it will output the data in chunks, not  neces-
              sarily  exactly when the data arrives.  Using this option will dis-
              able that buffering.

              Note that this is the negated option name documented. You can  thus
              use --buffer to enforce the buffering.


mohiramohira

httpieだとレスポンスボディが文字化け

$ http -v localhost:8888
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8888
User-Agent: HTTPie/2.4.0



HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

これは、私わたしが小さいときに、村の茂平もへいというおじいさんからきいたお話です。むかしは、私たちの村のちかくの、中山なかやまというところに小さなお城があって、中山さまというおとのさまが、おられたそうです。その中山から、少しはなれた山の中に、「ごん狐ぎつね」という狐がいました。ごんは、一人ひとりぼっちの小狐で、しだの一ぱいしげった森の中に穴をほって住んでいました。そして、夜でも昼でも、あたりの村へ出てきて、いたずらばかりしました。

mohiramohira

https://developer.mozilla.org/ja/docs/Web/HTTP/Connection_management_in_HTTP_1.x#http_pipelining

HTTP パイプラインは、現代のブラウザーでは既定で有効化されていません。

不具合があるプロキシがまだ一般的であり、これらは開発者が容易には予見あるいは診断できない、奇妙かつ一定しない挙動の原因になります。
パイプラインの正しい実装は複雑です。転送するリソースのサイズ、効果的な RTT および帯域が、パイプラインによる改善に対して直接的な影響力を持ちます。これらがわからなければ、重要なメッセージがそうでないメッセージより遅れる場合があります。重要さの概念は、ページのレイアウト中に高まります!よって、 HTTP パイプラインはほとんどの場合でわずかな改善にしかなりません。
パイプラインは、 HOL の問題に左右されます。
これらの理由により、パイプラインはよりよいアルゴリズムである多重化に置き換えられました。こちらは HTTP/2 で使用されています。

jnuankjnuank

HTTP1.1のパイプライニングはもうなくなってしまったが、
HTTP2の多重化がまさにそれ

mohiramohira

2021/04/08(木) 130分

  • 7章 UDP ソケットを使った マルチキャスト通信 から 7.3 UDP のマルチキャストの実装例 まで

MEMO

次回開催メモ

  • 7.4 UDP を使った実世界のサンプル から
mohiramohira
  • TCP (Transmission Control Protocol)
  • UDP (User Datagram Protocol)
jnuankjnuank

UDP は、 TCP と比べて機能が少なくシンプルですが 、その代わりに複数のコン
ピューターに同時にメッセージを送ることが可能なマルチキャストとブロードキャス
トをサポートしています。これは TCP にはない機能です。

なるほど。
そのマルチキャストとブロードキャストというものが、まだよくわかっていない

jnuankjnuank

ユニキャスト、
マルチキャスト、
ブロードキャスト

これらの違いがよくわかっていない。

ユニキャストとマルチキャストはわかる。
1:1か1:多

マルチキャストとブロードキャストの違いはなんだ?
マルチキャストは指定した複数の宛先
ブロードキャストは不特定多数

不特定多数とはなんだ?

mohiramohira

ユニキャスト(unicast)は、単一の通信相手に対してデータを転送する方法です。マルチキャスト(multicast)は、決められた複数の相手に対して、同時にデータを転送します。ブロードキャスト(broadcast)は、不特定多数の相手に対して、同時にデータを送信する方法です。TCPはユニキャストに分類され、対してUDPはユニキャスト、マルチキャスト、ブロードキャストのすべてに対応します。

安永 遼真,中山 悠,丸田 一輝. TCP技術入門進化を続ける基本プロトコル (WEB+DB PRESS plus) (Japanese Edition) (Kindle の位置No.867-872). Kindle 版.

マルチキャストは特定のグループ内での通信です。これは回覧板のようなもので、1つの通信メッセージが、回覧されたりコピーされたりしながら伝えられていきます。インターネットでは、RIP2(5.6.4項参照)、OSPF(5.6.5項参照)といった技術でマルチキャストが使われています。

村山公保. 基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版 (Japanese Edition) (Kindle の位置No.2336-2339). Kindle 版.

ブロードキャストは、物理的に区切られた特定のネットワーク内の通信に限られます。特定範囲を超えたブロードキャストは、ほかのネットワークに迷惑をかけるので禁止されています。インターネットでは、ARP(5.5.2項参照)などでブロードキャストが使われています。

村山公保. 基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版 (Japanese Edition) (Kindle の位置No.2342-2345). Kindle 版.

jnuankjnuank

不特定多とは、イーサネット内の不特定。
ARPとかがそうらしい(MACアドレスでの問い合わせ)

mohiramohira

UDPやDTLSを使って未達パケットを無視することで遅延時間のぶれをなくしたほうがユーザー体験が向上すると考えられます(徐々に会話が遅れて応答がずれていき定期的にリセットが必要になるようなテレビ会議システムでは困りますよね?)。

jnuankjnuank

動画や音声のストリーミングを利用したアプリケーションでは、順序を待ったり再
送を依頼したりすることで、動画や音声の再生が途切れて遅延を引き起こす可能性
があります。 TCP では未達のパケットがあると再送を依頼して通信を遅らせますが、
UDP や DTLS を使って未達パケットを無視することで遅延時間のぶれをなくしたほう
がユーザー体験が向上すると考えられます

これは、すごくわかりやすい。
ちょっと音や映像が飛んだくらいでは、オンライン会議はなんとかなる気がする

jnuankjnuank

UDP の利用例として QUIC があります。 QUIC は、 TCP のレイヤーを軽量化して、さら
に TLS の暗号化と合体させたようなトランスポート層のプロトコルです。前章では TCP
上で動作する HTTP について解説しましたが、 QUIC 上で動作する HTTP には HTTP/3 と
いう名前が付けられることが内定しています( HTTP/3 については本章末のコラムも参
照してください)。

QUICってそういうものなんだ!
UDPが土台になっているし、それがHTTP/3となるわけか

mohiramohira

p.134: 誤植なのでは!?

マルチキャストは、リクエスト側の負担を増やすことなく多くのクライアントに同 時にデータを送信できる仕組みです。

これは、「サーバー側」の間違いでは!?!?

「サーバー側が、多数のクライアントに対して、いちいちデータを送る必要がない」というニュアンスだと思う。つまり、楽になるのはメッセージを送る側 == サーバー側。

続く文章から考えても、おかしいな〜と。

その前に、まずはマルチキャストについて簡単に説明します。マルチキャストでは使える宛先IPアドレスがあらかじめ決められていて、ある送信元から同じマルチキャストアドレスに属するコンピューターに対してデータを配信できます。

送信元とマルチキャストアドレスの組み合わせをグループといい、同じグループであれば、受信するコンピューターが100台でも送信側の負担は1台分です。

って、思ったんだけど、

UDPのマルチキャストにおいては、サーバー側がリクエストするから、この記述であっているっぽい!

mohiramohira

UDPのマルチキャストでは、サービスを受ける側(クライアント)がソケットをオープンして待ち受け、そこにサービス提供者(サーバー)がデータを送信します。よく考えると、このフローはTCPを利用する場合とは逆の関係です。

実際に、電話で時報を聞くシーンを想像すると、クライアントがListenする感じがわかりやすい!

僕が、117と入力して、受話器に耳を当てる(Listenする!)と、電話の向こう(サーバー)から、時刻が飛んでくる!

mohiramohira

マルチキャストの話は、いままでのTCPでのクラサバモデルが破壊されるw

jnuankjnuank

UDPのユニキャストの場合は、今までどおり、サーバ側がListenして、クライアントからリクエストを送る形式だと思われる

jnuankjnuank

UDPは、どちらかというと速さ優先。
動画配信など

バケツに入れた水を少しくらい零しても、まぁいいかーくらい

mohiramohira

2021/04/12(月) 120分

  • 7.4 UDP を使った実世界のサンプル から

MEMO

  • TCPとUDPの違いが結構クリアになってきた!
  • なんとなーく、HTTP/1.1 → HTTP/2 → HTTP/3 の関係性も見えてきた
  • TCPがわかると、HTTP/1.1→HTTP/2 → HTTP/3 や QUIC を理解しやすくなる! というか、TCPとUDPがわからんと、理解が厳しいと思う

次回開催メモ

  • 8章 UNIXドメインソケット から
mohiramohira

■フロー制御とふくそう制御の違いフロー制御とふくそう制御の違いがわかりにくいかもしれません。違いを一言で説明すると、次のようになります。フロー制御は、通信相手の受信キューがあふれないように、送信パケットを制御するふくそう制御は、通信相手との経路上にあるルータやハブのキューがあふれないように、送信パケットを制御するフロー制御もふくそう制御も、キューがあふれないようにするという点は同じですが、どこのキューかが異なります。フロー制御は通信相手だけを考えます。ふくそう制御はネットワークのことを考えます。このように、目的を考えて、それぞれに適したアルゴリズムを考

村山公保. 基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版 (Japanese Edition) (Kindle の位置No.3886-3893). Kindle 版.

mohiramohira

HTTP/2では1本のTCP接続の内部に、ストリームという仮想のTCPソケットを作って通信を行います。

1つのTCPセッション内部に、仮想のTCPソケットっていうのはめっちゃわかる感じある!

mohiramohira

2021/04/19(月) XXX分

  • 8章 UNIXドメインソケット から

MEMO

次回開催メモ

  • 8.4 Unix ドメインソケットと TCP のベンチマークから
mohiramohira

外部、内部ってのは、「カーネル内部かどうか」ってことか。

mohiramohira

ソケットのおさらい

一般に、他のアプリケーションとの通信のことをプロセス間通信(IPC、InterProcessCommunication)と呼びます。OSには、シグナル、メッセージキュー、パイプ、共有メモリなど、数多くのプロセス間通信機能が用意されています。ソケットも、そのようなプロセス間通信の一種です。ソケットが他のプロセス間通信と少し違うのは、アドレスとポート番号がわかればローカルのコンピューター内だけではなく外部のコンピューターとも通信が行える点です。
ソケットにはいくつか種類があります。本書で説明するのは次の3つです。

mohiramohira

Unixドメインソケットで作成されるのは、ソケットファイルという特殊なファイルであり、通常のファイルのような実体はありません。あくまでもプロセス間の高速な通信としてファイルというインタフェースを利用するだけです。

jnuankjnuank

Unix ドメインソケットは、 TCP 型(ストリーム型)と UDP 型(データグラム型)の両方の使い方ができます。

これらはTCPプロトコル、UDPプロトコル、ではない?

mohiramohira

$ ls -la  "/var/folders/f0/5f88fx992bq6z8yd53_rc2d80000gn/T/unixdomainsocket-sample"
srwxr-xr-x  1 foo  bar  0  4 19 22:08 /var/folders/f0/5f88fx992bq6z8yd53_rc2d80000gn/T/unixdomainsocket-sample
jnuankjnuank

8.2.2 で作ったUnixドメインサーバに対しての接続方法

curlとncどちらでもいける。
(curlでの送り方は、最後の引数はなんでもいいので、とても気持ち悪い感じ)

https://qiita.com/toritori0318/items/193df8f749a9c4bda883

% curl --unix-socket "/tmp/unixdomainsocket-sample" hello
Hello Woirld

https://qiita.com/hana_shin/items/97e6c03ac5e5ed67ce38#6-unixドメインソケットの使い方

 % nc -U /tmp/unixdomainsocket-sample
GET / HTTP/1.1

HTTP/1.0 200 OK

Hello Woirld

mohiramohira

https://udzura.hatenablog.jp/entry/2019/10/15/231632

チャネルに見える

名前付きパイプの基本的な使い方として:

書き込むと、どこかで読み出されない限り書き込みが完了しない(ブロックする)
読み出そうとすると、読み出し可能な状態になるまでブロックする
ブロックする性質を利用して、あるプロセスが何かしらの処理が完了するのを別のプロセスから待つことができる。簡単には、何かしらの処理が完了したらパイプに何かを書き込み、それを事前に別のプロセスでreadしておけば待ち受けることができる。

jnuankjnuank

そもそも名前付きパイプって何よ?

Linuxの名前付きパイプというのは、mkfifo コマンドを使って作るパイプ。

無名パイプ(通常の | )が、処理が終わったら破棄されるのに対して、名前付きパイプは永続的。
自分で破棄しなけければいけない。

https://qiita.com/akym03/items/aadef9638f78e222de22#名前付きパイプ

% mkfifo named_pipe 
% ll named_pipe 
prw-r--r-- 1 jun docker 0  419 23:18 named_pipe|
mohiramohira

2021/04/26(月) XXX分

  • 8.4 Unix ドメインソケットと TCP のベンチマークから

MEMO

  • Unixドメインソケットの速度ににそれほど感動できなかった話(でも速いは早い)
  • ファイルシステムのない世界は地獄
  • inode領域とデータ領域にわかれている
  • ハードリンクの有効活用シーンを知りたい
  • inode保険
    • やったか!? やってない
    • 首の皮一枚

次回開催メモ

  • 9.1.1 複雑なファイルシステムと VFS から
mohiramohira

macOS環境だと、7〜8倍くらい

$ go test -bench .
goos: darwin
goarch: amd64
pkg: system-programming/chapter08_UnixDomainSocket/benchmark
cpu: Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
BenchmarkTCPServer-8                      	    3234	    333456 ns/op
BenchmarkUnixDomainSocketStreamServer-8   	   16364	     75752 ns/op
PASS
ok  	system-programming/chapter08_UnixDomainSocket/benchmark	4.243s

本は90倍とあるが、まじか!?

$ go test -bench . ⏎
testing: warning: no tests to run 
BenchmarkTCPServer-8 1000 7989037 ns/op
BenchmarkUDSStreamServer-8 20000 91136 ns/op

https://qiita.com/syossan27/items/148e33dd9da4ee3dc89b

mohiramohira

Go 言語と C 言語のインタフェースの違い はチェックとしておくといい感じ! ← C言語での解説の本との対応関係で困ったから!

mohiramohira

ファイルシステムは紀元前からあった!?

しかしファイルシステムは、太古の時代から、ほとんどの OS で必ずと言っていいほど提供されてきた機能です。

重要されていることがわかるなあ。

jnuankjnuank
stat  ./*

  File: ./server_test.go
  Size: 1303      	Blocks: 8          IO Block: 4096   通常ファイル
Device: 10302h/66306d	Inode: 16385946    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/     jun)   Gid: (  998/  docker)
Access: 2021-04-26 21:19:39.833819581 +0900
Modify: 2021-04-26 21:19:33.413477333 +0900
Change: 2021-04-26 21:19:33.413477333 +0900
 Birth: -
  File: ./tcpserver.go
  Size: 774       	Blocks: 8          IO Block: 4096   通常ファイル
Device: 10302h/66306d	Inode: 16385944    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/     jun)   Gid: (  998/  docker)
Access: 2021-04-26 21:19:39.833819581 +0900
Modify: 2021-04-26 21:19:33.393476268 +0900
Change: 2021-04-26 21:19:33.393476268 +0900
 Birth: -
  File: ./unixdomainsocketstreamserver.go
  Size: 851       	Blocks: 8          IO Block: 4096   通常ファイル
Device: 10302h/66306d	Inode: 16385945    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/     jun)   Gid: (  998/  docker)
Access: 2021-04-26 21:19:39.833819581 +0900
Modify: 2021-04-26 21:19:33.421477759 +0900
Change: 2021-04-26 21:19:33.421477759 +0900
 Birth: -


stat  ./ 
  File: ./
  Size: 4096      	Blocks: 8          IO Block: 4096   ディレクトリ
Device: 10302h/66306d	Inode: 18483699    Links: 2
Access: (0755/drwxr-xr-x)  Uid: ( 1000/     jun)   Gid: (  998/  docker)
Access: 2021-04-26 21:19:39.833819581 +0900
Modify: 2021-04-26 21:19:33.425477971 +0900
Change: 2021-04-26 21:19:33.425477971 +0900
 Birth: -

mohiramohira

inode領域とデータ領域が分かれている!

だから、「データ容量に余裕があるけど、ファイルがつくれないケース」はありうる(inode枯渇とか)

mohiramohira

ディレクトリというのは、実を言うと、配下に含まれる ファイル名とその inode のインデックスの一覧表が格納されている特別なファイルです

mohiramohira

いつもの世界

ルートディレクトリ
- FileA
- FileB
- FileC

実際

ルートディレクトリ(inode:2)
  - inode:A <-「FileA」 
  - inode:B <-「FileB」
  - inode:C <-「FileC」

ディスクの世界

++---[inode:A]-----+---[inode:B] ---+
+0x142424234353535 + --0x343242353 -+
+-----------------------------------+

Q. ファイル名(FileAとかFileBとか)はどこにあるの?

FileAのハードリンクをつくった世界

ルートディレクトリ(inode:2)
  - 「FileA」 (inode:A <-「FileA」)
  - Homeディレクトリ
      - 「FileA」のハードリンク
  - devディレクトリ
      - localディレクトリ
          - 「FileA」のハードリンク

inodeでみ世界FileAのハードリンクをつくった世界

ルートディレクトリ(inode:2)
  - (inode:A <-「FileA」)
  - Homeディレクトリ
      - (inode:A <-「ハードリンクその1」)
  - devディレクトリ
      - localディレクトリ
          - (inode:A <-「ハードリンクその2」)      

Q. hardlinkは、inodeも消費せず、ディスクも消費しない?

消費ゼロってことはないだろうけど、きっと少ないはず

jnuankjnuank
$ lsof | grep server_test 
less      18453                              jun    4r      REG              259,2      1303   18483682 /home/jun/Projects/go_study/benchmark/server_test (deleted)
$ ls -li /proc/18453/fd
合計 0
802595 lrwx------ 1 jun docker 64  426 23:13 0 -> /dev/pts/1
802596 lrwx------ 1 jun docker 64  426 23:13 1 -> /dev/pts/1
802597 lrwx------ 1 jun docker 64  426 23:13 2 -> /dev/pts/1
802598 lr-x------ 1 jun docker 64  426 23:13 3 -> /dev/tty
802599 lr-x------ 1 jun docker 64  426 23:13 4 -> '/home/jun/Projects/go_study/benchmark/server_test (deleted)'

復活!!

$ cp /proc/18453/fd/4 server_test

$ ll
合計 20
drwxr-xr-x  2 jun docker 4096  426 23:19 ./
drwxrwxr-x 12 jun docker 4096  426 21:19 ../
-rw-r--r--  1 jun docker 1303  426 23:19 server_test
-rw-r--r--  1 jun docker  774  426 21:19 tcpserver.go
-rw-r--r--  1 jun docker  851  426 21:19 unixdomainsocketstreamserver.go

mohiramohira

2021/04/29(木) 180分

  • 9.1.1 複雑なファイルシステムと VFS から

MEMO

  • ジャーナリング
  • 意外とよくわかってない「スナップショット」
  • mvコマンドの詳細 == rename(2)の動きが細かくわかった!
  • 異なるデバイスでのrename(2)は失敗する
    • rename(2)`してみて、無理だったら、コピーして、もとを削除する
  • 同じデバイス内での移動
    • $ mv file /hoge/dir/file (コピー先をファイル名含めて完全に指定する)
      • 素直にうまくいく
    • $ mv file /hoge/dir
      • /hoge/dirrename(2) するけど、失敗(ディレクトリだから)
      • 失敗したら、/hoge/dir/filerename(2) なので成功する感じ
  • 同じrename(2) でも、言語(Go言語やBash)によって、どういうAPIをつくるかの違いがみれて面白い
  • この記事が有用すぎる! → Linux: ハードリンクと inode - Qiita

次回開催メモ

  • 9.2.5 ファイルの属性の取得 から
jnuankjnuank

また、ファイルシステムには他のファイルシステムをぶら下げる(マウント)こと
も可能です。
最近では、ジャーナリングファイルシステムといって、書き込み中に瞬断が発生し
てもストレージの管理領域と実際の内容に不整合が起きにくくする仕組みも広く利用
されています

ジャーナリングファイルシステムというのは、トランザクション管理みたい。
もともとDBで使われていた技術

vim の swapファイルみたいなものに近いものを感じる

https://www.atmarkit.co.jp/ait/articles/0307/29/news002.html

mohiramohira

ジャーナリングの流れ

ジャーナリングでは、ファイルシステム内にジャーナル領域という特殊な領域を用意します。ジャーナル領域は、ユーザには認識できないメタデータです。ファイルシステムを更新する際は次のような手順を踏みます。

武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 (Japanese Edition) (Kindle の位置No.3040-3042). Kindle 版.

上記2つの場合のいずれにせよ、ファイルシステムは不整合な状態にはならず、処理前か、あるいは処理後の状態になります。

武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 (Japanese Edition) (Kindle の位置No.3052-3053). Kindle 版.

jnuankjnuank

これらのさまざまなファイルシステムは、 Linux では VFS ( Virtual File System )と
いう API で、すべて統一的に扱えるようになっています

これら統一的になっているから、ユーザはファイルシステムは一個として見えるのかもしれない

mohiramohira

いますぐ実践!Linuxシステム管理/Vol.166

スナップショットとは、ファイルシステムのある瞬間の状態を、そのまま抜き出したものです。(あ、ファイルシステムの場合は、ですね。)

もとのファイルシステムは、その後も更新可能ですが、スナップショットは、作成したときの状態をそのまま保持しつづけてくれます。
ですので、ファイルシステムを使用しながら、スナップショットを用いてバックアップをとったり、ヤバい操作を試す直前に、スナップショットを作成しておいて、案の定失敗したときに状態を戻したり、といったことができます。おお、便利そうですね。

LVMのスナップショット機能は、論理ボリュームで実現されています。

スナップショットを作成してから、もとのファイルシステムが更新されたときに、更新される前のデータを保持することで、スナップショット作成時のイメージを保持するようになっています。
(未変更部分は、もとのファイルシステムのデータを参照すれば済みますので、スナップショット側ではデータを保持しません。)

mohiramohira

DVDに焼くパターンのバックアップ作戦の弱点

LVMによる自動バックアップ・システムの構築:Linux管理者への道(6)(2/3 ページ) - @IT

安全にバックアップを取るにはどうすればよいでしょうか? 非常に頻繁にデータの更新が行われるようなシステムの場合、システムの稼働中にバックアップを取ろうとすると、バックアップの最中にデータが変更されてしまう可能性があります。その場合、不正確な情報がバックアップされたり、バックアップデータが壊れてしまう可能性もあります。

mohiramohira

罠〜

GoではなくOSのレイヤーの話になりますが、GoのAPIでファイル出力のためのシステムコールを呼ぶと即座に結果が返ってきます。
これは、OSカーネル内部のバッファメモリへの書き込みが終了した段階で、いったんレスポンスが返ってくるためです。

注意が必要な点として、この段階ではメモリへ書き込まれただけなので、突然の電源断などがあると書き込んだつもりの内容が消えてしまいます

確実にストレージに書き込まれたことを確認するには、File.Sync()メソッドを利用します。

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	f, _ := os.Create("file.txt")
	a := time.Now()

	f.Write([]byte("緑の怪獣"))
	b := time.Now()

	f.Sync()
	c := time.Now()

	f.Close()
	d := time.Now()

	fmt.Printf("Writeにかかった時間: %v\n", b.Sub(a))
	fmt.Printf("Syncにかかった時間: %v\n", c.Sub(b))
	fmt.Printf("Closeにかかった時間: %v\n", d.Sub(c))
}
mohiramohira

これで、中身が表示されないのどういう原理?

package main

import (
	"fmt"
	"io"
	"os"
	"time"
)

func main() {
	filename := "sample.txt"
	_ = os.Remove(filename)

	file, err := os.Create(filename)
	if err != nil {
		panic(err)
	}

	file.Write([]byte("New file content\n"))
	file.Sync()

	fmt.Println("Read file")
	io.Copy(os.Stdout, file)
	file.Close()
}
mohiramohira

しかし Go 言語の os.Remove() は、先にファイルの削除を行い、失敗したらディレクトリの削除を呼び出します

コメントにあった!

System call interface forces us to know whether name is a file or directory.
Try both: it is cheaper on average than doing a Stat plus the right one.

平均的にはお得ってことか!

// Remove removes the named file or (empty) directory.
// If there is an error, it will be of type *PathError.
func Remove(name string) error {
	// System call interface forces us to know
	// whether name is a file or directory.
	// Try both: it is cheaper on average than
	// doing a Stat plus the right one.
	e := ignoringEINTR(func() error {
		return syscall.Unlink(name)
	})
	if e == nil {
		return nil
	}
	e1 := ignoringEINTR(func() error {
		return syscall.Rmdir(name)
	})
	if e1 == nil {
		return nil
	}

	// ...
	return &PathError{Op: "remove", Path: name, Err: e}
}
mohiramohira

p. 167 そういえば、USBメモリとかのファイルをドラッグアンドドロップすると、コピーされるわな

POSIX系OSであっても、マウントされていて元のデバイスが異なる場合には、renameシステムコールでの移動はできません。下

デバイスやドライブが異なる場合にはファイルを開いてコピーする必要があります。FreeBSDのmvコマンドも、最初にrenameシステムコールを試してみて†3、失敗したら入出力先のファイルを開いてコピーし、そのあとにソースファイルを消しています。

jnuankjnuank

POSIX 系 OS であっても、マウントされていて元のデバイスが異なる場合には、
rename システムコールでの移動はできません。

本当にそうなのか、コマンドで調べてみた。

※tmpfsはオンメモリ上の一時ファイルシステム

$ df 
/dev/nvme0n1p2 490691512 33710300 431985732   8% /
tmpfs            1602960       72   1602888   1% /run/user/1000

◆別のデバイスへ移すとき

異なるデバイスへのリンクは、renameシステムコールが使えない。

そのときに、openat→read、コピー先へのopenat→write、コピー元のunlinkを呼んで終わり。

$ strace -o file6mv.out mv file6 /run/user/1000

renameat2(AT_FDCWD, "file6", AT_FDCWD, "/run/user/1000", RENAME_NOREPLACE) = -1 EXDEV (無効なクロスデバイスリンクです)
stat("/run/user/1000", {st_mode=S_IFDIR|0700, st_size=500, ...}) = 0
renameat2(AT_FDCWD, "file6", AT_FDCWD, "/run/user/1000/file6", RENAME_NOREPLACE) = -1 EXDEV (無効なクロスデバイスリンクです)
lstat("file6", {st_mode=S_IFREG|0644, st_size=9, ...}) = 0
newfstatat(AT_FDCWD, "/run/user/1000/file6", 0x7fffbf1368d0, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (そのようなファイルやディレクトリはありません)
unlink("/run/user/1000/file6")          = -1 ENOENT (そのようなファイルやディレクトリはありません)
openat(AT_FDCWD, "file6", O_RDONLY|O_NOFOLLOW) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=9, ...}) = 0
openat(AT_FDCWD, "/run/user/1000/file6", O_WRONLY|O_CREAT|O_EXCL, 0600) = 4
fstat(4, {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
ioctl(4, BTRFS_IOC_CLONE or FICLONE, 3) = -1 EXDEV (無効なクロスデバイスリンクです)
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f83f9e72000
read(3, "hello go\n", 131072)           = 9
write(4, "hello go\n", 9)               = 9
read(3, "", 131072)                     = 0
utimensat(4, NULL, [{tv_sec=1619704004, tv_nsec=154111458} /* 2021-04-29T22:46:44.154111458+0900 */, {tv_sec=1619704001, tv_nsec=754136556} /* 2021-04-29T22:46:41.754136556+0900 */], 0) = 0
flistxattr(3, NULL, 0)                  = 0
flistxattr(3, 0x7fffbf136620, 0)        = 0
fgetxattr(3, "system.posix_acl_access", 0x7fffbf136500, 132) = -1 ENODATA (利用可能なデータがありません)
fstat(3, {st_mode=S_IFREG|0644, st_size=9, ...}) = 0
fsetxattr(4, "system.posix_acl_access", "\2\0\0\0\1\0\6\0\377\377\377\377\4\0\4\0\377\377\377\377 \0\4\0\377\377\377\377", 28, 0) = 0
close(4)                                = 0
close(3)                                = 0
munmap(0x7f83f9e72000, 139264)          = 0
lstat("/", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
newfstatat(AT_FDCWD, "file6", {st_mode=S_IFREG|0644, st_size=9, ...}, AT_SYMLINK_NOFOLLOW) = 0
unlinkat(AT_FDCWD, "file6", 0)          = 0

◆同じデバイス間でmvするとき

単純に、renameシステムコールを呼んでいるだけ!
最初のrename失敗は、Projectsをrename先のファイルとして見るか、すでに存在しているフォルダとしてみるかをトライしている!
mvは実はいろいろいい感じにやってくれている!

$ strace -o file7mv.out mv file7 ~/Projects

ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
renameat2(AT_FDCWD, "file7", AT_FDCWD, "/home/jun/Projects", RENAME_NOREPLACE) = -1 EEXIST (ファイルが存在します)
stat("/home/jun/Projects", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
renameat2(AT_FDCWD, "file7", AT_FDCWD, "/home/jun/Projects/file7", RENAME_NOREPLACE) = 0
mohiramohira

mvコマンドの細かい動きがみえたっ!

いろいろif文してるのね

mohiramohira

ファイル、ハードリンク、inode、異なるデバイス間でのmvが完全にまとまっていそうな資料

https://qiita.com/lnznt/items/6178e1c5f066f22fe9c2#ハードリンクと-inode

この実験おもしろそう!

inode に3つハードリンク (A,B,C) を作り、B を同じ、C を異なるファイルシステムに mv して、B, C それぞれを更新し A の変化を観察してみると分かると思います。ご興味があればやってみてください。(ここでは割愛します。)

jnuankjnuank

感想と問い

  • スナップショットの領域ってどこにあるんだろうね?
    • スナップショットは、ハードリンクとかと関係性があるのかと思っていたけど、なんか違う気がしてきた
  • renameシステムコールって、ハードリンクを書き換えるものだと思っていたけど、違うっぽい?
  • mvコマンドの中身の挙動が面白い
  • journalctlコマンドが、syslogを素直に見るよりわかりやすい感じがしている
mohiramohira

2021/05/03(月) ○○分

  • 9.2.5 ファイルの属性の取得 から
  • 9.2.6 ファイルの存在チェック
  • 9.2.7 OS 固有のファイル属性を取得する

MEMO

  • なんで、Linuxって直接birthtimeみれないの!?
  • debugfs でLinuxでもbirthtimeがみれるよ! (カーネル内に情報があるからね)
    • debugfsはデバイスファイルとかをいじるときに使うっぽい(きっと一生使わない説w)
  • 存在確認せずに直接ファイル操作しようよ習慣
  • 自転車置場の議論、わかる。とてもわかる

次回開催メモ

  • 9.2.8 ファイルの同一性チェック
mohiramohira

DefinedType と TypeAlias(型エイリアス)

https://play.golang.org/p/5EuYtIBqpGn

package main

import "fmt"

// DefinedTypeが違う => 違う型
type MyIntA int
type MyIntB int

// 型エイリアスは同じ型
type AliasOfMyIntA = MyIntA

func main() {
	a := MyIntA(1)
	b := MyIntB(2)
	c := AliasOfMyIntA(3)
	fmt.Println(a, b, c)

	//a = b // CompileError

	a = c // いkる

}

mohiramohira

os.FileMode = fs.FileMode の理由

The types FileInfo, FileMode, and PathError are now aliases for types of the same name in the io/fs package. Function signatures in the os package have been updated to refer to the names in the io/fs package. This should not affect any existing code.

タイプFileInfo、FileMode、およびPathErrorは、io / fsパッケージ内の同じ名前のタイプのエイリアスになりました。 osパッケージの関数シグネチャが更新され、io / fsパッケージの名前を参照できるようになりました。これは既存のコードには影響しません。

Go 1.16 Release Notes - The Go Programming Language

mohiramohira

存在確認 → 書き込み は 非推奨な雰囲気

存在チェックそのもののシステムコールは提供されていません
Python の os. path.exists() も内部で同じシステムコールを呼ぶ os.stat() を使っています し、C 言語でも stat() や、access() を代わりに使います。
access() は現在のプ ロセスの権限でアクセスできるかどうかを診断するシステムコールで、Go 言語は syscall.Access() として POSIX 系 OS で使えます。
ただし、存在チェックそのもの が不要であるというのが現在では主流のようです † 5 。

仮に存在チェックを行ってファイルがあることを確認しても、その後のファイル操 作までの間に他のプロセスやスレッドがファイルを消してしまうことも考えられま す。ファイル操作関数を直接使い、エラーを正しく扱うコードを書くことが推奨され ています。

Node.jsのドキュメントが非常にわかりやすい!

https://nodejs.org/api/fs.html#fs_fs_exists_path_callback

jnuankjnuank

【疑問】

多くのLinuxでBirthTimeが無いのは、なぜ?

mohiramohira

自転車置き場の議論

自転車置き場については誰もが理解している(もしくは理解していると自分では思っている)ため、自転車置き場の設置については終わりのない議論が生じることになる。関係者の誰もが自分のアイデアを加えることによって自分の存在を誇示したがるのである。

https://ja.wikipedia.org/wiki/パーキンソンの凡俗法則

jnuankjnuank

LinuxでのBirthTime取得

debugfsで見れるらしい。

$ sudo debugfs -R 'stat <15074545>' /dev/nvme0n1p2

Inode: 15074545   Type: regular    Mode:  0644   Flags: 0x80000
Generation: 3381138581    Version: 0x00000000:00000001
User:  1000   Group:   998   Project:     0   Size: 27
File ACL: 0
Links: 1   Blockcount: 8
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x609000cb:69432638 -- Mon May  3 22:55:23 2021
 atime: 0x609000cd:e0777cdc -- Mon May  3 22:55:25 2021
 mtime: 0x609000cb:69432638 -- Mon May  3 22:55:23 2021
crtime: 0x609000cb:69432638 -- Mon May  3 22:55:23 2021
Size of extra inode fields: 32
Inode checksum: 0xc598b415
EXTENTS:
(0):60326767
mohiramohira

debugfs ‐ 通信用語の基礎知識

ユーザープロセスから、デバイスやドライバーなどに関するカーネルのデバッグ情報にアクセスするためのインターフェイスとして提供される、オンメモリーのファイルシステムである。

jnuankjnuank

/dev/nvme0n1p2 デバイスに、対話モードでアクセス。

sudo debugfs  /dev/nvme0n1p2
mohiramohira

macOS固有のファイル属性

$ ls -l
total 360816
-rw-r--r--@ 1 mohira  bob  174930415  5  3 23:17 gimp-2.10.22-x86_64-3.dmg
-rw-r--r--@ 1 mohira  bob     137390  5  3 22:44 screenshot.png

$ ls -l@
total 360816
-rw-r--r--@ 1 mohira  bob  174930415  5  3 23:17 gimp-2.10.22-x86_64-3.dmg
	com.apple.macl	       72
	com.apple.metadata:kMDItemWhereFroms	      152
	com.apple.quarantine	       57
-rw-r--r--@ 1 mohira  bob     137390  5  3 22:44 screenshot.png
	com.apple.FinderInfo	       32
	com.apple.lastuseddate#PS	       16
	com.apple.macl	       72
	com.apple.metadata:kMDItemIsScreenCapture	       42
	com.apple.metadata:kMDItemScreenCaptureGlobalRect	       86
	com.apple.metadata:kMDItemScreenCaptureType	       51
mohiramohira

2021/05/10(月) 100分

  • 9.2.8 ファイルの同一性チェックから

MEMO

  • DPDKの話を思い出せてよかった!
  • OSのキャッシュ戦略に介入することによる高速化
  • PostgreSQLのプランニングに影響する話
  • HDDとSSDの影響

次回開催メモ

  • 9.5.7 ディレクトリのトラバース
mohiramohira

なお、inode 番号やデバイス番号は同一のストレージ/コンピューター内でしかユ ニークではないため、別のコンピューター間では衝突する可能性があります。

mohiramohira
// Chtimes changes the access and modification times of the named
// file, similar to the Unix utime() or utimes() functions.
//
// The underlying filesystem may truncate or round the values to a
// less precise time unit.
// If there is an error, it will be of type *PathError.
func Chtimes(name string, atime time.Time, mtime time.Time) error {
	var utimes [2]syscall.Timespec
	utimes[0] = syscall.NsecToTimespec(atime.UnixNano())
	utimes[1] = syscall.NsecToTimespec(mtime.UnixNano())
	if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
		return &PathError{Op: "chtimes", Path: name, Err: e}
	}
	return nil
}

https://man7.org/linux/man-pages/man2/utime.2.html

jnuankjnuank

そのため、ファイルへデータを書き込むと、バッファに蓄えられた時点でアプリケー
ションに処理が返ります。ファイルからデータを読み込むときも、いったんバッファ
に蓄えられますし、すでにバッファに載っており、そのファイルに対する書き込みが
行われていない(バッファが新鮮)ならバッファだけにしかアクセスしません。

バッファが新鮮、という言い回しって、普通なんだろうか?

mohiramohira

システムコールを介して読み書きは遅い

そこで、バッファを使おう(裏で勝手に書き込みしたりしてくれる。処理が終わった瞬間、本当にディスクに書いてあるかはわからんということ)

というか、普段は、アプリケーションとバッファ間でやりとりしていると考えても良いよね

バッファの戦略はOS任せだけど、いじることもできるよ ← Dircet I/O

jnuankjnuank

たとえば PostgreSQL では、
seq_page_cost および random_page_cost というシーケンシャルアクセスの場合と
ランダムアクセスの場合のコストを設定できるようになっており、デフォルトでは前者
が 1 、後者が 4 となっています。 SSD では 2 種類のアクセスの速度差が小さいため、両
方のコストに同じ値を設定することで、より高速な処理方法が選択される確率が上がります

これについては、なるほどって思った。
最近はSSD搭載が多いので、変わってきた

mohiramohira

whichコマンド実装

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

// whichコマンドの実装
func main() {
	if len(os.Args) == 1 {
		fmt.Printf("%s [exec file name]", os.Args[0])
		os.Exit(1)
	}

	for _, path := range filepath.SplitList(os.Getenv("PATH")) {
		execpath := filepath.Join(path, os.Args[1])

		// 存在チェックは os.Stat() でやるやつ(p.169 9.2.6 ファイルの存在チェック)
		_, err := os.Stat(execpath)
		if !os.IsNotExist(err) {
			fmt.Println(execpath)
			return
		}
	}
	os.Exit(1)
}

mohiramohira

2021/05/13(木) 110分

  • 9.5.7 ディレクトリのトラバース から

MEMO

  • ついに2ケタ章突入!
  • トラバース と Walk

次回開催メモ

  • 10.2 ファイルのロック(syscall.Flock()) から
jnuankjnuank

filepath.Walk()の引数で関数を渡せるのだが、
funcの戻り値がerrorだったりすので、ちょっと読みにくかった。

	err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
		if info.IsDir() {
			if info.Name() == "_build" {
				return filepath.SkipDir
			}
			return nil
		}
		ext := strings.ToLower(filepath.Ext(info.Name()))
		if imageSuffix[ext] {
			rel, err := filepath.Rel(root, path)
			if err != nil {
				return nil
			}
			fmt.Printf("%s\n", rel)
		}
		return nil
	})

上と、以下は同じ

	err := filepath.Walk(root, funcName(root))

func funcName(root string) func(path string, info fs.FileInfo, err error) error {
	return func(path string, info fs.FileInfo, err error) error {
		if info.IsDir() {
			if info.Name() == "_build" {
				return filepath.SkipDir
			}
			return nil
		}
		ext := strings.ToLower(filepath.Ext(info.Name()))
		if imageSuffix[ext] {
			rel, err := filepath.Rel(root, path)
			if err != nil {
				return nil
			}
			fmt.Printf("%s\n", rel)
		}
		return nil
	}
}
mohiramohira
package main

import "fmt"

var m = map[string]int{
	"jp": 12000,
	"ch": 140000,
}

func main() {
	fmt.Println(m["jp"])
	fmt.Println(m["us"])

	v, ok := m["us"]
	fmt.Println(v)
	fmt.Println(ok)
	
	if _, ok:= m["us"]; ok {
		// usというKeyがあった場合の処理
	}

}
jnuankjnuank

監視したいファイルを OS 側に通知しておいて、変更があったら教えてもらう(パッシブな方式)
 → イベントリスナーに登録するような感じ
タイマーなどで定期的にフォルダを走査し、 os.Stat() などを使って変更を探しに行く(アクティブな方式)
 → ポーリングするイメージ

mohiramohira

2021/05/17(月) 140分

  • 10.2 ファイルのロック(syscall.Flock()) から

MEMO

  • 勧告ロック(Advisedly Lock)
    • 正しい使用に関してはアプリケーションが責任を持つことから勧告的ロックと呼ばれます。
  • syscall.Flock() のブロッキング
  • 共有ロックのノンブロッキングってどういうことがわからんやつ
  • コピーオンライトという最適化戦略

次回開催メモ

  • 10.4 同期・非同期/ブロッキング・ノンブロッキング から
mohiramohira

ただし、syscall.Flock() によるロック状態は、 通常のファイル入出力のためのシステムコールによっては確認されません。

ロック状態になっていたとしても、open(2)write(2) がそれをちゃんとチェックするわけじゃないよってこと?

言い換えると、ロック状態になっていても、open(2)write(2)ができてしまうってこと?

jnuankjnuank

syscall.flock() は勧告ロックらしい。

勧告ロックは、ファイル入出力するシステムコール側で確認をすることはいので、
ロックを真面目に確認しなかったら意味がない

https://wiki.bit-hive.com/north/pg/勧告ロックと強制ロック#:~:text=通常ファイルのロックは,アドバイザリロック)%E3%81%A8%E8%A8%80%E3%81%84%E3%80%82&text=fctrl%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%B3%E3%83%BC%E3%83%AB%E5%AE%9F%E8%A3%85%E3%81%A7,%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AE%E9%81%95%E3%81%84%E3%81%AF%E3%81%AA%E3%81%84%E3%80%82

mohiramohira

ロックの話はDBの話から探すのもアリ

https://pgsql-jp.github.io/current/html/explicit-locking.html

PostgreSQLは、アプリケーション独自の意味を持つロックを生成する手法を提供します。 これは、その使用に関してシステムによる制限がないこと、つまり、正しい使用に関してはアプリケーションが責任を持つことから勧告的ロックと呼ばれます。 勧告的ロックは、MVCC方式に合わせづらいロック戦略で有用に使用することができます。 例えば、勧告的ロックのよくある利用として、いわゆる「フラットファイル」データ管理システムで典型的な、悲観的なロック戦略を模擬することです。 この用途のためにテーブル内にフラグを格納することもできますが、勧告的ロックの方が高速で、テーブルの膨張を防ぐことができます。 また、セッション終了時にサーバによる自動整理を行うこともできるようになります。

jnuankjnuank

ただし、 syscall.Flock() によるロック状態は、
通常のファイル入出力のためのシステムコールによっては確認されません。そのた
め、ロックをまじめに確認しないプロセスが 1 つでもあると、自由に上書きされてし
まう可能性があります。

●ターミナル1

$ flock  /tmp/lock vi file1 

●ターミナル2
flockで同じロックファイルを指定すると、ブロックされる

$ flock  /tmp/lock cat file1

# ブロックされる

別にファイルじゃなくても良い

$ flock  /tmp/lock cat ls 

# ブロックされる

参考記事
https://qiita.com/hana_shin/items/410f8e021bf58ce9632b

mohiramohira

コピーオンライト(Copy-On-Write)

書き換えが発生するまでは、お得。

上京するまでは家賃は一緒ってこと。

コピーオンライト時は、単に読み込みだけで使用されていた場合に、通常どおりメモリ領域にファイルをマッピングします。複数のプロセスが同じファイルをマッピングしていたとすると、カーネル上は1つ分のみメモリ領域が使用され、それ以上のメモリは消費しません。しかし、その領域内でメモリ書き換えが発生するとその領域がまるごとコピーされます。そのため、元のファイルには変更が反映されません。不思議な挙動ですが、書き換えが発生するまでは複数バリエーションの状態を保持する必要がないので、メモリを節約できます。

mohiramohira

コピーオンライト: 書き換えが必要になるまでは、原本を直接みたらええやん!

最適化戦略の1つ! って考えから始めるといい感じっぽい。

https://ja.wikipedia.org/wiki/コピーオンライト

コンピュータ内部で、ある程度大きなデータを複製する必要が生じたとき、愚直な設計では、直ちに新たな空き領域を探して割り当て、コピーを実行する。
ところが、もし複製したデータに対する書き換えがなければその複製は無駄だったことになる。

そこで、複製を要求されても、コピーをした振りをして、とりあえず原本をそのまま参照させるが、ただし、そのままで本当に書き換えてはまずい。
原本またはコピーのどちらかを書き換えようとしたときに、それを検出し、その時点ではじめて新たな空き領域を探して割り当て、コピーを実行する。
これが「書き換え時にコピーする」、すなわちコピーオンライト(Copy-On-Write)の基本的な形態である。

基盤となる考え方は、複数の(何らかの)要求者がリソースを要求するときに、少なくとも当初はそれらの要求を区別する必要がないときに同じリソースを与える、というものである。
これは要求者がリソースを「更新」しようとするまで保持され、「更新」が他者に見えないようにリソースの個別のコピーを必要になった時点で作成する。
要求者からはこの一連の動きは見えない。
第一の利点は要求者が全く更新しなければ、個別のコピーを作成する必要が生じないという点である。

mohiramohira

もちろん、コピー オンライト機能を使う場合や、確保したメモリの領域にアセンブリ命令が格納されて いて実行を許可する必要がある場合には、mmap 一択です。

むむ?

mohiramohira

2021/05/20(木) ○○○○分

  • 10.4 同期・非同期/ブロッキング・ノンブロッキング から

MEMO

次回開催メモ

  • 10.6 FUSE を使った自作のファイルシステムの作成 から
jnuankjnuank

syscall.Flock() によるロックでは、すでにロックされているファイルに対して
ロックをかけようとすると、最初のロックが外れるまでずっと待たされます。そのた
め、定期的に何度もアクセスしてロックが取得できるかトライする、といったことが
できません。これを可能にするのがノンブロッキングモードです( 10.4 節で少し詳し
く説明します)。

今日明かされる話

mohiramohira

同期処理と非同期処理 の厳密目な区別

同期処理と非同期処理は、ここでは実データを取りに行くのか、通知をもらうのか で区別されます。

jnuankjnuank

ファイルI/O、ネットワークI/Oなどは、CPU内部処理と比べると劇的に遅いらしい。
その重い処理に引きずられないように、同期処理と非同期処理、そしてブロッキング処理とノンブロッキング処理という分類があるらしい

mohiramohira

p.194 正誤表チャンス!? 助詞がおかしい

  • 同期処理:OS に I/O タスクを投げて、入出力の準備ができたらアプリケーション 返ってくる
  • 同期: OSに仕事を投げて、入出力の準備ができたらアプリケーション処理が返ってくる

根拠

ASCII.jp:ファイルシステムと、その上のGo言語の関数たち(3) では、「に」になっているから

さらなる提案

  • 「実データを」って入れると良さそう
  • Before
    • 同期処理:OS に I/O タスクを投げて、入出力の準備ができたらアプリケーション 返ってくる
  • After
    • 同期処理:OS に I/O タスクを投げて、入出力の準備ができたら実データがアプリケーション返ってくる
jnuankjnuank

疑問

同期・ノンブロッキング処理は、「APIを呼ぶと即座に完了していないかどうかと、現在準備しているデータが得られる」
クライアントが完了を知る必要があるときには、完了が返ってくるまで何度もAPIを呼ぶ(ポーリング)

ポーリングしない限りは、データももらえないってことだろうか。

mohiramohira

同期/非同期 と ブロッキング/ノンブロッキング は実装でみてほうが理解早そう
(言葉での曖昧さが消せない)

jnuankjnuank

EAGAINがよくわからなかったので、errno-bash.h の中身を見てみた。

参考記事
https://qiita.com/h2suzuki/items/0cc924cdd9d5c6d47448

● /usr/include/asm-generic/errno-base.h


/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define	EPERM		 1	/* Operation not permitted */
#define	ENOENT		 2	/* No such file or directory */
#define	ESRCH		 3	/* No such process */
#define	EINTR		 4	/* Interrupted system call */
#define	EIO		 5	/* I/O error */
#define	ENXIO		 6	/* No such device or address */
#define	E2BIG		 7	/* Argument list too long */
#define	ENOEXEC		 8	/* Exec format error */
#define	EBADF		 9	/* Bad file number */
#define	ECHILD		10	/* No child processes */
#define	EAGAIN		11	/* Try again */
#define	ENOMEM		12	/* Out of memory */
#define	EACCES		13	/* Permission denied */
#define	EFAULT		14	/* Bad address */
#define	ENOTBLK		15	/* Block device required */
#define	EBUSY		16	/* Device or resource busy */
#define	EEXIST		17	/* File exists */
#define	EXDEV		18	/* Cross-device link */
#define	ENODEV		19	/* No such device */
#define	ENOTDIR		20	/* Not a directory */
#define	EISDIR		21	/* Is a directory */
#define	EINVAL		22	/* Invalid argument */
#define	ENFILE		23	/* File table overflow */
#define	EMFILE		24	/* Too many open files */
#define	ENOTTY		25	/* Not a typewriter */
#define	ETXTBSY		26	/* Text file busy */
#define	EFBIG		27	/* File too large */
#define	ENOSPC		28	/* No space left on device */
#define	ESPIPE		29	/* Illegal seek */
#define	EROFS		30	/* Read-only file system */
#define	EMLINK		31	/* Too many links */
#define	EPIPE		32	/* Broken pipe */
#define	EDOM		33	/* Math argument out of domain of func */
#define	ERANGE		34	/* Math result not representable */

#endif

● /usr/include/asm-generic/errno.h


/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_H
#define _ASM_GENERIC_ERRNO_H

#include <asm-generic/errno-base.h>

#define	EDEADLK		35	/* Resource deadlock would occur */
#define	ENAMETOOLONG	36	/* File name too long */
#define	ENOLCK		37	/* No record locks available */

/*
 * This error code is special: arch syscall entry code will return
 * -ENOSYS if users try to call a syscall that doesn't exist.  To keep
 * failures of syscalls that really do exist distinguishable from
 * failures due to attempts to use a nonexistent syscall, syscall
 * implementations should refrain from returning -ENOSYS.
 */
#define	ENOSYS		38	/* Invalid system call number */

#define	ENOTEMPTY	39	/* Directory not empty */
#define	ELOOP		40	/* Too many symbolic links encountered */
#define	EWOULDBLOCK	EAGAIN	/* Operation would block */
#define	ENOMSG		42	/* No message of desired type */
#define	EIDRM		43	/* Identifier removed */
#define	ECHRNG		44	/* Channel number out of range */
#define	EL2NSYNC	45	/* Level 2 not synchronized */
#define	EL3HLT		46	/* Level 3 halted */
#define	EL3RST		47	/* Level 3 reset */
#define	ELNRNG		48	/* Link number out of range */
#define	EUNATCH		49	/* Protocol driver not attached */
#define	ENOCSI		50	/* No CSI structure available */
#define	EL2HLT		51	/* Level 2 halted */
#define	EBADE		52	/* Invalid exchange */
#define	EBADR		53	/* Invalid request descriptor */
#define	EXFULL		54	/* Exchange full */
#define	ENOANO		55	/* No anode */
#define	EBADRQC		56	/* Invalid request code */
#define	EBADSLT		57	/* Invalid slot */

#define	EDEADLOCK	EDEADLK

#define	EBFONT		59	/* Bad font file format */
#define	ENOSTR		60	/* Device not a stream */
#define	ENODATA		61	/* No data available */
#define	ETIME		62	/* Timer expired */
#define	ENOSR		63	/* Out of streams resources */
#define	ENONET		64	/* Machine is not on the network */
#define	ENOPKG		65	/* Package not installed */
#define	EREMOTE		66	/* Object is remote */
#define	ENOLINK		67	/* Link has been severed */
#define	EADV		68	/* Advertise error */
#define	ESRMNT		69	/* Srmount error */
#define	ECOMM		70	/* Communication error on send */
#define	EPROTO		71	/* Protocol error */
#define	EMULTIHOP	72	/* Multihop attempted */
#define	EDOTDOT		73	/* RFS specific error */
#define	EBADMSG		74	/* Not a data message */
#define	EOVERFLOW	75	/* Value too large for defined data type */
#define	ENOTUNIQ	76	/* Name not unique on network */
#define	EBADFD		77	/* File descriptor in bad state */
#define	EREMCHG		78	/* Remote address changed */
#define	ELIBACC		79	/* Can not access a needed shared library */
#define	ELIBBAD		80	/* Accessing a corrupted shared library */
#define	ELIBSCN		81	/* .lib section in a.out corrupted */
#define	ELIBMAX		82	/* Attempting to link in too many shared libraries */
#define	ELIBEXEC	83	/* Cannot exec a shared library directly */
#define	EILSEQ		84	/* Illegal byte sequence */
#define	ERESTART	85	/* Interrupted system call should be restarted */
#define	ESTRPIPE	86	/* Streams pipe error */
#define	EUSERS		87	/* Too many users */
#define	ENOTSOCK	88	/* Socket operation on non-socket */
#define	EDESTADDRREQ	89	/* Destination address required */
#define	EMSGSIZE	90	/* Message too long */
#define	EPROTOTYPE	91	/* Protocol wrong type for socket */
#define	ENOPROTOOPT	92	/* Protocol not available */
#define	EPROTONOSUPPORT	93	/* Protocol not supported */
#define	ESOCKTNOSUPPORT	94	/* Socket type not supported */
#define	EOPNOTSUPP	95	/* Operation not supported on transport endpoint */
#define	EPFNOSUPPORT	96	/* Protocol family not supported */
#define	EAFNOSUPPORT	97	/* Address family not supported by protocol */
#define	EADDRINUSE	98	/* Address already in use */
#define	EADDRNOTAVAIL	99	/* Cannot assign requested address */
#define	ENETDOWN	100	/* Network is down */
#define	ENETUNREACH	101	/* Network is unreachable */
#define	ENETRESET	102	/* Network dropped connection because of reset */
#define	ECONNABORTED	103	/* Software caused connection abort */
#define	ECONNRESET	104	/* Connection reset by peer */
#define	ENOBUFS		105	/* No buffer space available */
#define	EISCONN		106	/* Transport endpoint is already connected */
#define	ENOTCONN	107	/* Transport endpoint is not connected */
#define	ESHUTDOWN	108	/* Cannot send after transport endpoint shutdown */
#define	ETOOMANYREFS	109	/* Too many references: cannot splice */
#define	ETIMEDOUT	110	/* Connection timed out */
#define	ECONNREFUSED	111	/* Connection refused */
#define	EHOSTDOWN	112	/* Host is down */
#define	EHOSTUNREACH	113	/* No route to host */
#define	EALREADY	114	/* Operation already in progress */
#define	EINPROGRESS	115	/* Operation now in progress */
#define	ESTALE		116	/* Stale file handle */
#define	EUCLEAN		117	/* Structure needs cleaning */
#define	ENOTNAM		118	/* Not a XENIX named type file */
#define	ENAVAIL		119	/* No XENIX semaphores available */
#define	EISNAM		120	/* Is a named type file */
#define	EREMOTEIO	121	/* Remote I/O error */
#define	EDQUOT		122	/* Quota exceeded */

#define	ENOMEDIUM	123	/* No medium found */
#define	EMEDIUMTYPE	124	/* Wrong medium type */
#define	ECANCELED	125	/* Operation Canceled */
#define	ENOKEY		126	/* Required key not available */
#define	EKEYEXPIRED	127	/* Key has expired */
#define	EKEYREVOKED	128	/* Key has been revoked */
#define	EKEYREJECTED	129	/* Key was rejected by service */

/* for robust mutexes */
#define	EOWNERDEAD	130	/* Owner died */
#define	ENOTRECOVERABLE	131	/* State not recoverable */

#define ERFKILL		132	/* Operation not possible due to RF-kill */

#define EHWPOISON	133	/* Memory page has hardware error */

#endif

mohiramohira

「同期処理かつブロッキング」vs「同期処理かつノンブロッキング」

ブロッキングだろうが、ノンブロッキングだろうが、同期処理なのであれば、最終的に実データを、取りに行かないといけない(プロセスが、ね)

で、ノンブロッキングは、合間を縫っていろいろ作業できる。依頼したIOタスク以外のことをやってもいいし、「あの〜、依頼してたデータ取れました〜?」とポーリングすることもできる。

で、どれだけポーリングしてようが、結局、実データを手に入れるわけ(だって、同期処理だから)。

ブロッキングとノンブロッキングの、「待機する」ってのは、水色っぽいバーが分割されているのをみればわかると思う。

https://developer.ibm.com/technologies/linux/articles/l-async/

mohiramohira

p.198 goroutine をたくさん実行し、それぞれに同期・ブロッキング I/O を担当させる と、非同期・ノンブロッキングとなる

package main

import (
	"fmt"
	"log"
	"math/rand"
	"os"
	"path/filepath"
	"strconv"
	"time"
)

func write() {
	// 同期かつブロッキング
	s := strconv.Itoa(rand.Int())
	filename := filepath.Join(os.TempDir(), "file"+s)

	f, err := os.Create(filename)
	if err != nil {
		log.Fatal(err)
	}

	_, _ = f.WriteString("hello from " + s)

	time.Sleep(3 * time.Second)
	fmt.Printf("Finish %s\n", filename)
}

func main() {
	// 同期かつブロッキング
	//write()
	//write()
	//write()
	//write()
	//write()

	// 非同期かつノンブロッキング
	go write()
	go write()
	go write()
	go write()
	go write()
	go write()

	fmt.Println("全然関係ない処理")
	fmt.Println("全然関係ない処理")
	fmt.Println("全然関係ない処理")
	time.Sleep(5 * time.Second)
}
jnuankjnuank

同期処理のメソッドを、goroutineで囲むことによって簡単に非同期処理にすることができるGo言語すごいって感じた

このスクラップは2021/11/29にクローズされました