Closed399

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

ピン留めされたアイテム

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

【感想】

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

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

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

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

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

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

type が 型の定義キーワード

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

Go はクラスじゃなくて、構造体

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)
}

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

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

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

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

content-encording : gzip

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()

2021/01/14(木) 240分

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

MEMO

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

次回開催メモ

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

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

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);

2021/01/21(木) 240分

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

MEMO

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

次回開催メモ

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

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

godoc -http ":6060" -analysis type

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

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

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

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

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()
}

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


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

Empty Interface

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

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

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"
}

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()

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のフォーメーションでいく

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

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 はコピーを実行するためには使用されません.

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

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

straceをしたいが、macはstraceが無かったので、
docker で go環境を用意。

そのときに、-v じゃなくて --mount を知った

docker run -it --rm --mount type=bind,source=`pwd`,target=/app centos:7 bash

2021/01/28(木) 210分

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

MEMO

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

次回開催メモ

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

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}
	}

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

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
}
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)
}

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

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

勘違い

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)
}

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

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

O_TRUNCってTruncateかな?

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

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)
}
	// 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

これはなんで、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()

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))

}

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))
	}

}

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))
}
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


}

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

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)

}

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

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

}

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

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]

}

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)
}

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から!
  • ストリーム楽しみ

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

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))

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)
	}

}

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

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

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

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

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

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

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

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

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`

前の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",
	})

}

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

  • 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]

}

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

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しようとしても、読み取れない?

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

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

Content-Disposition - HTTP | MDN

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")
}

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

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

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)
	}

}

2021/02/08(月) 180分

  • 3.7 ~ Q.3.3

MEMO

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

次回開催メモ

  • Q3.4から

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)
	}
}

2021/02/11(木) 170分

  • Q3.5〜4.2まで

MEMO

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

次回開催メモ

  • 4.3から

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
}

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)
}

並列と並行の違いがよくわからん

  • CPU と I/O の関係? どっちが何を担当している?
  • スレッド と コア?
  • プロセス やら スレッドやら

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

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

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

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

<- 演算子はなんて発音するの?

チャネル

バッファが足りないのに、送信すると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)

}

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

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

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
}

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

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

『プログラミング言語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)
	}
}

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)
}

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()

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)
		}
	}
}

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

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("確認中です")
		}
	}
}

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

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

2021/02/15(月) 120分

  • 4.3

MEMO

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

次回開催メモ

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

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 /]#

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

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

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

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

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

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

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

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

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

シグナル送信

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

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)
}

2021/02/18(木) 180分

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

MEMO

次回開催メモ

  • 5.2.3から

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("なにもないよ")
	}

}

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

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

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

リングプロテクションがクリーンアーキテクチャの図にしか見えないwww

https://ja.wikipedia.org/wiki/リングプロテクション

OpenVMSは4つのモードを使っており、特権の高い方から順に、カーネル、エグゼクティブ、スーパーバイザ、ユーザーと呼んでいる。

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

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

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

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

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

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

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

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

OOM キラー

Out Of Memory

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

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

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

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
}

funcPCがよくわからなかった。
本の内容と違う

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

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

今までtimeコマンドで出てくる user、sysの違いがよくわかってなかったけど、

ユーザモードでの時間と、
特権モードでの時間という違いだとわかってきた

# time cat cat.c > /dev/null

real	0m0.008s
user	0m0.000s
sys	0m0.003s

https://tadtadya.com/user-cpu-time-and-system-cpu-time/

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

おもしろい

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

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

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

2021/02/24(木) 240分

  • 5.2.3 から

MEMO

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

次回開催メモ

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

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用のヘッダーファイルなどから自動生成された定数です。

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
// 略

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

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

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

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

package main

func main() {
	var u uint32

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

	print(u)
}

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

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

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

syscall の ドキュメントは、OSやCPUアーキテクチャによって変わる! URLのクエリパラメータを変えるといける

リンク置いてほしくね?w

https://golang.org/pkg/syscall/?GOOS=linux&GOARCH=arm

https://golang.org/pkg/syscall/?GOOS=linux&GOARCH=arm

DLL

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

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

リンカ

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

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

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")
	}

POSIX(Portable Oper- ating System Interface)

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

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という名称で呼んだほうが発音しやすく憶えやすいと気づき、これを採用すると決め、正式名称という位置づけとなった。

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準拠するように変化している。

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

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

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

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

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

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

__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); // これじゃね?
}

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

2021/03/01(月) 120分

  • 5.5から

MEMO

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

次回開催メモ

  • 5.7から

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

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

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

【疑問】
openとopenatの違いってLinuxだけ?

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

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 +++```

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

pipe2とepollたち、実は働いていない説

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

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

システムコールの種類が多い!(感想

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

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

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

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

fizzbuzz問題でstrace見たらどうなる?

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

2021/03/08(月) 90分

  • 5.7から

MEMO

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

次回開催メモ

  • 6.2から

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

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

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

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

【疑問】OSとカーネルの区別がわからない

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

ランタイムって何

Q5.1 は すでにやっていた!!

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

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

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

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

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

2021/03/15(月) 180分

  • 6.2から

MEMO

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

次回開催メモ

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

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

改行じゃないのつらいw

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

HTTPの使用用途がどんどん広がって複雑化

  • TLS

誤字?

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

照会なのか?

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

意味合い的には、

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

RPCは、POSTメソッドで送るのみ?

  • RPC
  • REST
  • GraphQL

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2021/03/18(木) 210分

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

MEMO

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

次回開催メモ

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

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

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

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

C#の場合は、システムコールの名前(Listen、Bind、Connect)に合わせている
https://qiita.com/T_E_T_R_A/items/bdd310714fea63474a7b

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

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

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

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

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))
}


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

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

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

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

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

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 +++

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

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): 圧縮 から

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

いつ、Keep-Alive終わるの?

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

Connection:Close

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

『Real World HTTP』

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

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

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

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

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

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{,,}  

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

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

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

2021/03/29(月) 120分

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

MEMO

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

次回開催メモ

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

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������>%

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

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

この図解が最強!

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

これもわかりやすい!

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

サーバ側の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_INE