『Goならわかるシステムプログラミング』をやっていく会 その1
README
Goならわかるシステムプログラミング – 技術書出版と販売のラムダノート の読書会です。
スクラップの投稿上限を超えてしまうので、続きはこちら ↓↓
参考
-
Goならわかるシステムプログラミング – 技術書出版と販売のラムダノート
- 紙本と電子版(PDF)がセットで、1冊分の値段(税込¥3,520)で買えるのでお得!
- GitHub - LambdaNote/errata-gosyspro-1-4: 『Goならわかるシステムプログラミング』 4刷 正誤情報
-
ASCII.jp:Goならわかるシステムプログラミング
- 書籍のもととなった連載記事
おすすめ技術書としてLTしました(どんな本かイメージしやすいと思います)
2021-03-24 おすすめ技術書LT会 『Goならわかるシステムプログラミング』 - Google スライド
副読本(一緒に読むとべりーぐっど!)
io.Reader
やio.Writer
とか(1〜3章)
-
Goから学ぶI/O
- I/Oの観点で各種パッケージを横断しているのが最高
システムコールとかLinux(5章)
-
ふつうのLinuxプログラミング 第2版 Linuxの仕組みから学べるgccプログラミングの王道
- なかじまさんのやつ → Linux勉强メモ
- 別途勉強を始めたやつ → 『ふつうのLinuxプログラミング』をやっていく会
- [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
- はじめてのOSコードリーディング ――UNIX V6で学ぶカーネルのしくみ Software Design plus
- Yotube - Go言語低レイヤー入門 Hello world が画面に表示されるまで
-
Compiler Explorer
- ブラウザでコンパイル後のアセンブリが見れる! GoやPythonでもいける
- 筑波大学 加藤和彦先生「オペレーティングシステムの世界」 - YouTube
ソケット通信とかHTTPとかWebとか(6〜8章)
-
Webを支える技術 ―― HTTP,URI,HTML,そしてREST
- 2010年の本。Webの歴史から丁寧に扱っている。他の本にはない守備範囲を誇る感じ
-
O'Reilly Japan - Real World HTTP 第2版
- 渋川さんの著作。辞書的に使うといい感じ
-
HTTP | MDN
- 書籍ではないけど、HTTPやWeb技術がめっちゃまとまっているし、信頼性も高い
-
基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版
- 参考文献に挙げられているTCP/IP本
-
基礎からわかるTCP/IP ネットワーク実験プログラミング 第2版
- 姉妹本。TCPやUDPとかめっちゃ詳しくあるし、図解が強いのでいい感じ
-
ポートとソケットがわかればインターネットがわかる――TCP/IP・ネットワーク技術を学びたいあなたのために
- 図解のアプローチがわかりやすい
-
Software Design 2021年5月号
- 第1特集が「ハンズオンTCP/IP」
- 実装に使っている言語は、C言語
- 実務で役立つTCPクライアントの作り方 - Youtube
ファイルシステム(9〜10章)
-
Linux: ハードリンクと inode - Qiita
- inodeやリンク、異なるデバイス間でのmvなどがめっちゃよくまとまっている
-
[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
- ファイルシステムまわりの説明が詳しい
-
Software Design 2017年2月号 | Gihyo Digital Publishing
- 特集2が、ファイルシステムやジャーナリングを扱っている
開催実績
- 2021/01/14(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-6df2f066b2e489
- 2021/01/21(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-482718c035b9ff
- 2021/01/25(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-0935cdbde2738e
- 2021/01/28(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-2a1595f4b8256b
- 2021/02/01(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-d850e165b1853e
- 2021/02/08(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-421a068335e9de
- 2021/02/11(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-4c80d5186fc927
- 2021/02/15(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-05f9fdd3b7d163
- 2021/02/18(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-20c1b1a00452a0
- 2021/02/24(水) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-575cf8b664d608
- 2021/03/01(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-edd7ab9ee376c2
- 2021/03/08(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-b51cec6776f49f
- 2021/03/15(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-1f3dfc14e2c163
- 2021/03/18(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-10629942d8921c
- 2021/03/22(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-28a8a87bcf570f
- 2021/03/29(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-2ff13d5039582f
- 2021/04/05(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-af7a2d27e43716
- 2021/04/08(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-7d511f3ddea513
- 2021/04/12(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-70e9bc82ef92f6
- 2021/04/19(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-0be8157327f936
- 2021/04/26(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-498b4ef316e363
- 2021/04/29(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-2faaa11ddcef04
- 2021/05/03(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-8cd9583befe408
- 2021/05/10(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-823fa3684c7d4d
- 2021/05/13(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-fb8784937790a3
- 2021/05/17(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-5524e6e09ebfcd
- 2021/05/20(木) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-fb947f2e3ae581
- 2021/05/24(月) https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-a8e64bc39b6616
- 2021/05/31(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-ab8564806159d5
- 2021/06/03(木) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-b36ca1c91d54df
- 2021/06/07(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-6317c0bc6531a6
- 2021/06/10(木) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-baa9273826bdfa
- 2021/06/21(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-a9a8060cea89ae
- 2021/06/28(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-1aac1295362fd2
- 2021/07/05(月) https://zenn.dev/link/comments/4d5c141a580e9f
- 2021/07/12(月) https://zenn.dev/mohira/scraps/ea040641f2f122#comment-9d98f3822cf12d
- 2021/07/19(月) https://zenn.dev/link/comments/dd4f0248fafa58
- 2021/07/22(木) https://zenn.dev/link/comments/b28e8e9cee5c85
- 2021/07/26(月) https://zenn.dev/link/comments/2e2e5aa9bc03f3
- 2021/07/29(木) https://zenn.dev/link/comments/b5c661fb258b01
- 2021/08/02(月) https://zenn.dev/link/comments/88b3f862da086a
- 2021/08/12(木) https://zenn.dev/link/comments/cf49944cdbd18a
- 2021/08/26(木) https://zenn.dev/link/comments/4b84ff355d0a14
- 2021/09/06(月) https://zenn.dev/link/comments/7ee9e5309e548b
- 2021/10/11(月) https://zenn.dev/link/comments/4efa6f0dbe5715
- 2021/10/18(月) https://zenn.dev/link/comments/aca481829b6c79
- 2021/11/01(月) https://zenn.dev/link/comments/bd618e7fdee2d7
- 2021/11/08(月) https://zenn.dev/link/comments/42877bd8899b23
- 2021/11/15(月) https://zenn.dev/link/comments/ddaf0682c7dd0b
- 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)
}
CopyBuffer( ) は、WriteTo、ReaferFromの型をsrc、dstが実装している場合には、bufを使わないらしい
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
go のdocker image
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
これはなんで、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()
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から!
- ストリーム楽しみ
CRCってなんだっけ。
→ チェックディジットの一種
これでは!?
PNGファイルをGoで確認するっぽい記事。
式で表現をするやり方のほうが見やすい!
(テスト駆動開発の文脈?)
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じゃなくて、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画像を表示する時にはチャンク名の大文字小文字を区別しない事になっています。
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",
})
}
Portable Network Graphics(ポータブル・ネットワーク・グラフィックス、PNG)
車輪の再発明(のやりかけ)
-
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 レスポンスヘッダーは、コンテンツがブラウザーでインラインで表示されることを求められているか、つまり、ウェブページとして表示するか、ウェブページの一部として表示するか、ダウンロードしてローカルに保存する添付ファイルとするかを示します。
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から
teeコマンドは、T配管から来ている
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("確認中です")
}
}
}
Contextとチャネルで自前で終了通知するのとどう違うのか?
Contextの方が、どのような理由で終了したのかが、 ctx.Err() で取れそう
チャネルやゴルーチンの使い所がわかるとグッと来ると思う!
『プログラミング言語Go』の8章がよさそう
2021/02/15(月) 120分
- 4.3
MEMO
- システムプログラミング感ある回だった
- ファイルシステムは1つだけだと思っていたらそうでもなさそう!
- 疑似ファイルシステムとマウントという戦法
-
lsof
find / -inum {inode}
次回開催メモ
- 4.4から
- go 1.16 に Update したいね。
io
パッケージの影響が大きい → https://golang.org/doc/install?download=go1.16.darwin-amd64.pkg
プロセス間通信の話をしていたときの寄り道
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 というのも、端末からつなげた(ログイン)した瞬間から、生成されるのであって、
ログアウトした瞬間には消えてしまう、はず。という予想。
これじゃん! pipe も pipefs っていう疑似ファイルシステムからきたやつやん!
では、Linuxのほう見ていこう。 大まかなところはこんな感じに。
メモリ上にデータが置かれる
pipefsという擬似ファイルシステムによって実現される
pipefsはユーザーランドからは見ることができない
pipeは擬似ファイルとして扱われるのでファイルに関するデータ構造を持つ(inode、dentry等)
pipe は 疑似ファイル だが、ファイルのようなデータ構造を持っているのか!
Linuxの疑似ファイルシステムはメモリ上!
大雑把な違いとしてはV6の頃のpipeはストレージ領域にデータを持つ実体のあるファイルだったのに対して、Linuxの場合は擬似ファイルシステムによって実現されるメモリ上のみのファイルというところかな。
疑問
パイプと名前付きパイプ(FIFO)の違いがよくわからん
lsofコマンドを初めて使った。
オープンしているファイルを見れるみたい。
シグナル送信
シグナルを送信して遊ぶコード
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
- システムコールの詳しいステップが気になる
- Cをやるしかねぇべ!?
- WebAssenbly入門した
- timeコマンドでいろいろ実験できて最高! → https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-7e3073bc73e85e
次回開催メモ
- 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
OpenVMSは4つのモードを使っており、特権の高い方から順に、カーネル、エグゼクティブ、スーパーバイザ、ユーザーと呼んでいる。
カーネルの意義 と 何を調べているか?
カーネルは、処理の冒頭で、プロセスからの要求が正当なものかどうかをチェックします(たとえばシステムに存在しないような量のメモリを要求していないかどうか、など)。不正な要求であればシステムコールを失敗させます。ユーザプロセスからシステムコールを介さずに直接CPUのモードを変更する方法はありません(あったらカーネルが存在する意味がありません)。
武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 (Japanese Edition) (Kindle の位置No.339-343). Kindle 版.
システムコールが遅い、という話は、
CPUモードの切替に由来しているのか?
あまり、システムコールが遅いというのがピンと来ていない
モードを切り替えるから、システムコールは遅いという話になるのかな?
「システムコールは遅い」 だから バッファを使おうって話があったけど、その理解の糸口になるかも?
武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
OOM キラー
Out Of Memory
CPUの割り込みは、大きくわけてハードウェア割り込み(外部割り込み)とソフトウェア割り込み(内部割り込み)に分類できる。一言で「割り込み」と言った場合、前者を指すことが多いため、後者のことをSWI (SoftWare Interrupt) と呼び区別する場合がある。
計算はユーザーモードでもできるけど、肝心の出力ができない!
なぜなら、出力先は別プロセスであり、別プロセスに情報をおくる == プロセス間通信 は カーネルモードじゃないとできない! つまり、システムコールがないとできない!
5.1.3 システムコールがないとどうなるか? の「できませんラップ」が最高!
声に出して読みたい(実際、読んだ)
WebAssembly入門した!
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
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から
mode
が int
なのは、番号で指定するから!
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
DLL
DLLファイルとは、Windowsのプログラムファイルの種類の一つで、様々なプログラムから利用される汎用性の高い機能を収録した、部品化されたプログラムのこと。標準のファイル拡張子は「.dll」。
リンカ
リンカとは、ソフトウェアの開発ツールの一つで、機械語(マシン語)で記述されたプログラムを連結、編集して実行可能ファイルを作成するソフトウェア。
Windowsの場合は、dll経由でシステムコールを呼び出しているに過ぎないそうです。
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")
}
illumos(いるもす) とかいう OS
POSIX(Portable Oper- ating System Interface)
OS間で共通のシステムコールを決めることで、アプリケーションの移植性を高めるために作られたIEEE規格
POSIX の X とは! 名称の由来
この規格は起源をさかのぼると、もともとは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レジスタに、システムコール番号を格納しているは誰!?
カーネルモードとユーザーモード切り替えの正体!?
- https://github.com/torvalds/linux/blob/master/arch/x86/entry/entry_64.S
- 検索しても、ここにしかないし、これじゃね?
__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); // これじゃね?
}
MSRというのは、モデル固有レジスタ
wrmsrl
は Write Read ModelSpecificRegister Load の略という説に1票
CPU と レジスタの関係
次回開催メモ
Linux環境をつくっておこう。VMで。
自身が立てている環境の構築方法です。
2021/03/01(月) 120分
- 5.5から
MEMO
- システムコール、読んだら案外いける説
- 素直にドキュメントを読むといい感じ! 特に
man
で引数とか説明が有効。
次回開催メモ
- 5.7から
シグナルハンドラの初期化で大量に呼び出される。
シグナルは、SIGINTとかそのあたり。
epoll_create1 というシステムコールがよくわからない。
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、もしかしたら実行された順ではない説?
単純な動作を、複数の言語で実装して、システムコールを眺める、そして比較するってのはめっちゃ勉強になるな。
システムコールの種類が多い!(感想
select vs poll vs epoll の文脈で調べたら、ノンブロッキングIO,非同期IOの話が出てきた
【案】
いろんな言語で、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
DPDK、ユーザ側でコンピュータの根幹をイジれそうなので、かなり怖い
リングプロテクションの考え方は無視している
typo発見! p.95
- x: また数に
- o: またがずに
- o: 跨がずに
Twitterでつぶやいたところ、issue報告をしていただいた!
【疑問】OSとカーネルの区別がわからない
コンピュータ歴史博物館の館長的な存在が必要だw
ランタイムって何
Q5.1 は すでにやっていた!!
第6章
QUIC(クイック と発音するらしい)
実用的なアプリケーションでは、それらの機能を使って、自分のアプリケーションに 必要なプロトコルを実装していくことになります。
独自プロトコルの実装ができるのか
ソケットは、プロセス間通信の一種である。
他と違うのは、アドレスとポート番号がわかれば、ローカルのコンピュータ内だけではなく、外部のコンピュータとも通信が行える
【疑問】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メソッドで送るのみ?
RESTの究極形態: HATEOAS(へいたす)
Many HTTP APIs feature Link headers. They make APIs more self describing and discoverable.
GitHub uses these for pagination in their API, for example:
HATEOASというのは、
HTTPレスポンスに「リンク」情報が入っている。
これでページネーションをする。
Django REST frameworkでもページネーションでHyper Link Pagination あったな。
- RPC
- REST
- GraphQL
これらはHTTPの上で動いているっていうことがわかった。
RPCは、HTTPベース上で使うこともあるが、
元々はインターネット以前からある仕組み、考え方
RPCって本当に関数を呼び出している!
これでイメージつかめたぞ!
SOAP, REST, RPC, GraphQL をいい感じに解説してくれる資料
よさそう
RFCはIETFという組織が中心となって維持管理している、通信の相互接続性を維持するために共通化された仕様書集です。
「Request For Comment」という名前なのに仕様書だというのには少し違和感がありますが、これには歴史的経緯があります。
インターネットのもとになったネットワークはアメリカの国防予算で作られたため、仕様を外部に公開できませんでした。
そのため、「品質アップのためのご意見を広く世界から集める」という名目で仕様を公開することにした名残なのです。
『Real World HTTP 第2版』p.2-3
ソケットがよくわかっていなかったが、こういうグルーピングっぽい
- プロセス間通信
- ソケット
- TCP
- UDP
- UNIXドメインソケット
- ソケット
○クライアントスタブとサーバースタブ
IDLファイルをIDLコンパイラでコンパイルすることで、クライアントスタブとサーバ スタブのソースファイルが生成されます。
クライアントスタブには、アプリケーション本体から呼び出されるサーバで実行される 関数を呼び出す処理が記述されます。
サーバスタブには、クライアントから要求のあったインタフェースの関数を呼び出し、 結果をクライアントに送り返す処理が記述されます。
クライアント・サーバ間の通信は、これらのスタブとRPCランタイムが自動的に行い、 指定された通信プロトコルを利用して通信を行います(図3-2)。
メッセージング型通信、ストリーム型通信と違いがあるらしい
TCPはストリーム型通信?
RPCはメッセージング型通信
分散処理する上では、メッセージング型通信が基本である。
RPCは、HTTPヘッダーなどのメタデータを処理する必要性は特にない
Androidの中の説明はわかりやすい
『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# と Python でのソケットプログラミングの実装をみると、ちょっとわかった気になれた!
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
Connection #0
Keep-Alive対応のサーバー: ずーっと$ 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
Connection #0
Connection #1
Connection #2
Keep-Alive対応してないサーバー: つどつどConnection → $ 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)って何が違うんだ???
send()
じゃなくてwrite()
システムコールが使われていたこと
発端: TLSありのときの通信で、$ 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
callsend(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
MDNのまとめもめっちゃわかりやすい!: HTTP/1.x で使用できる 3種類の通信制御モデルの持つ長所と短所
【疑問】
curlコマンドで叩いた時に、writeとsendtoシステムコールが使い分けられたいるのは、なんで?
echo localhost:8888{,,}
がなんでできるかについて
以下のような文字列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-Encoding
はgzip
でよろしく〜 - 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
これもわかりやすい!
setsockopt
システムコールで設定している説?
Keep-Aliveは、
サーバ側の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 (リソースが一時的に利用できません)
setsockopt(7, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
で、KEEPALIVEをしているのを見て、
「KEEP-ALIVE対応していないサーバだったら、ここの値が変わるのでは?」と思ったけど、特に変わらなかった。
これは、サーバ側がずっと接続を待ち受けているからと思ったりする。
クライアント側で見てみたけど、これも同じだった。
ConnectionのRe-Useしているけど、Socketレベルでは同じように使っているのかもしれない
ソケット通信は、Pythonで見たほうがわかりやすいぞ!
わかりやすいぞ!
- システムコールと
socket
モジュールの命令軍が、システムコールとほぼ同じ名前だから - Goよりなれているから ← でかい!
- (GoやCと比べて)短いコードですむから(エラーハンドリングするかどうかの違いだと思うけど、とりあえず正常系がほしいのでPython有利)
socket --- 低水準ネットワークインターフェース — Python 3.9.2 ドキュメント
偉大なる先人の成果!
あとは、システムコール見ればツモ!
新しい言語を覚えるときに、どこまでやれば入門したと言えば良いのか。
「その言語でウェブサーバが立てられたら良い」
HTTPの独自ヘッダーで、X-のやつ
p.53
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 ソケットを使った マルチキャスト通信 から
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.
httpieでやろうとすると、文字化けをする
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
ããã¯ãç§ããããå°ããã¨ãã«ãæã®èå¹³ãã¸ãã¨ããããããããããããã話ã§ãããããã¯ãç§ãã¡ã®æã®ã¡ããã®ãä¸å±±ãªããã¾ã¨ããã¨ããã«å°ããªãåããã£ã¦ãä¸å±±ãã¾ã¨ãããã¨ã®ãã¾ããããããããã§ãããã®ä¸å±±ãããå°ãã¯ãªããå±±ã®ä¸ã«ããããçãã¤ããã¨ããçããã¾ãããããã¯ãä¸äººã²ã¨ãã¼ã£ã¡ã®å°çã§ããã ã®ä¸ã±ãããã£ã森ã®ä¸ã«ç©´ãã»ã£ã¦ä½ãã§ãã¾ãããããã¦ãå¤ã§ãæ¼ã§ãããããã®æã¸åºã¦ãã¦ãããããã°ãããã¾ããã
HTTP パイプラインは、現代のブラウザーでは既定で有効化されていません。
不具合があるプロキシがまだ一般的であり、これらは開発者が容易には予見あるいは診断できない、奇妙かつ一定しない挙動の原因になります。
パイプラインの正しい実装は複雑です。転送するリソースのサイズ、効果的な RTT および帯域が、パイプラインによる改善に対して直接的な影響力を持ちます。これらがわからなければ、重要なメッセージがそうでないメッセージより遅れる場合があります。重要さの概念は、ページのレイアウト中に高まります!よって、 HTTP パイプラインはほとんどの場合でわずかな改善にしかなりません。
パイプラインは、 HOL の問題に左右されます。
これらの理由により、パイプラインはよりよいアルゴリズムである多重化に置き換えられました。こちらは HTTP/2 で使用されています。
HTTP1.1のパイプライニングはもうなくなってしまったが、
HTTP2の多重化がまさにそれ
2021/04/08(木) 130分
- 7章 UDP ソケットを使った マルチキャスト通信 から 7.3 UDP のマルチキャストの実装例 まで
MEMO
- ついに、マルチキャストデビュー! 時報のたとえはわかりやすい
- QUICとかいう最強プロトコルにテンション上がる
- UDPは実装が短くてサクサク。楽しい
- これは誤植では!? → https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-6892b374c8fe05
次回開催メモ
- 7.4 UDP を使った実世界のサンプル から
- TCP (Transmission Control Protocol)
- UDP (User Datagram Protocol)
UDP は、 TCP と比べて機能が少なくシンプルですが 、その代わりに複数のコン
ピューターに同時にメッセージを送ることが可能なマルチキャストとブロードキャス
トをサポートしています。これは TCP にはない機能です。
なるほど。
そのマルチキャストとブロードキャストというものが、まだよくわかっていない
ユニキャスト、
マルチキャスト、
ブロードキャスト
これらの違いがよくわかっていない。
ユニキャストとマルチキャストはわかる。
1:1か1:多
マルチキャストとブロードキャストの違いはなんだ?
マルチキャストは指定した複数の宛先
ブロードキャストは不特定多数
不特定多数とはなんだ?
ユニキャスト(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 版.
不特定多とは、イーサネット内の不特定。
ARPとかがそうらしい(MACアドレスでの問い合わせ)
TCPはユニキャストオンリー
UDPやDTLSを使って未達パケットを無視することで遅延時間のぶれをなくしたほうがユーザー体験が向上すると考えられます(徐々に会話が遅れて応答がずれていき定期的にリセットが必要になるようなテレビ会議システムでは困りますよね?)。
動画や音声のストリーミングを利用したアプリケーションでは、順序を待ったり再
送を依頼したりすることで、動画や音声の再生が途切れて遅延を引き起こす可能性
があります。 TCP では未達のパケットがあると再送を依頼して通信を遅らせますが、
UDP や DTLS を使って未達パケットを無視することで遅延時間のぶれをなくしたほう
がユーザー体験が向上すると考えられます
これは、すごくわかりやすい。
ちょっと音や映像が飛んだくらいでは、オンライン会議はなんとかなる気がする
UDP の利用例として QUIC があります。 QUIC は、 TCP のレイヤーを軽量化して、さら
に TLS の暗号化と合体させたようなトランスポート層のプロトコルです。前章では TCP
上で動作する HTTP について解説しましたが、 QUIC 上で動作する HTTP には HTTP/3 と
いう名前が付けられることが内定しています( HTTP/3 については本章末のコラムも参
照してください)。
QUICってそういうものなんだ!
UDPが土台になっているし、それがHTTP/3となるわけか
p.134: 誤植なのでは!?
マルチキャストは、リクエスト側の負担を増やすことなく多くのクライアントに同 時にデータを送信できる仕組みです。
これは、「サーバー側」の間違いでは!?!?
「サーバー側が、多数のクライアントに対して、いちいちデータを送る必要がない」というニュアンスだと思う。つまり、楽になるのはメッセージを送る側 == サーバー側。
続く文章から考えても、おかしいな〜と。
その前に、まずはマルチキャストについて簡単に説明します。マルチキャストでは使える宛先IPアドレスがあらかじめ決められていて、ある送信元から同じマルチキャストアドレスに属するコンピューターに対してデータを配信できます。
送信元とマルチキャストアドレスの組み合わせをグループといい、同じグループであれば、受信するコンピューターが100台でも送信側の負担は1台分です。
って、思ったんだけど、
UDPのマルチキャストにおいては、サーバー側がリクエストするから、この記述であっているっぽい!
UDPのマルチキャストでは、サービスを受ける側(クライアント)がソケットをオープンして待ち受け、そこにサービス提供者(サーバー)がデータを送信します。よく考えると、このフローはTCPを利用する場合とは逆の関係です。
実際に、電話で時報を聞くシーンを想像すると、クライアントがListenする感じがわかりやすい!
僕が、117と入力して、受話器に耳を当てる(Listenする!)と、電話の向こう(サーバー)から、時刻が飛んでくる!
マルチキャストの話は、いままでのTCPでのクラサバモデルが破壊されるw
UDPのユニキャストの場合は、今までどおり、サーバ側がListenして、クライアントからリクエストを送る形式だと思われる
UDPは、どちらかというと速さ優先。
動画配信など
バケツに入れた水を少しくらい零しても、まぁいいかーくらい
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ドメインソケット から
Let’s Make an NTP Client in Go
基本的に、最上位に近いNTPサーバは一般には公開されていないため、通常は通信事業者などが公開したNTPサーバーを利用します。
golang.org/x/
■フロー制御とふくそう制御の違いフロー制御とふくそう制御の違いがわかりにくいかもしれません。違いを一言で説明すると、次のようになります。フロー制御は、通信相手の受信キューがあふれないように、送信パケットを制御するふくそう制御は、通信相手との経路上にあるルータやハブのキューがあふれないように、送信パケットを制御するフロー制御もふくそう制御も、キューがあふれないようにするという点は同じですが、どこのキューかが異なります。フロー制御は通信相手だけを考えます。ふくそう制御はネットワークのことを考えます。このように、目的を考えて、それぞれに適したアルゴリズムを考
村山公保. 基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版 (Japanese Edition) (Kindle の位置No.3886-3893). Kindle 版.
HTTP/2では1本のTCP接続の内部に、ストリームという仮想のTCPソケットを作って通信を行います。
1つのTCPセッション内部に、仮想のTCPソケットっていうのはめっちゃわかる感じある!
2021/04/19(月) XXX分
- 8章 UNIXドメインソケット から
MEMO
次回開催メモ
- 8.4 Unix ドメインソケットと TCP のベンチマークから
外部、内部ってのは、「カーネル内部かどうか」ってことか。
Unixドメインソケットが、プロセス間通信の唯一の方法じゃないよね
ソケットのおさらい
一般に、他のアプリケーションとの通信のことをプロセス間通信(IPC、InterProcessCommunication)と呼びます。OSには、シグナル、メッセージキュー、パイプ、共有メモリなど、数多くのプロセス間通信機能が用意されています。ソケットも、そのようなプロセス間通信の一種です。ソケットが他のプロセス間通信と少し違うのは、アドレスとポート番号がわかればローカルのコンピューター内だけではなく外部のコンピューターとも通信が行える点です。
ソケットにはいくつか種類があります。本書で説明するのは次の3つです。
Q. Unix ドメインソケット と パイプの違いってなんだろうね。
Unixドメインソケットで作成されるのは、ソケットファイルという特殊なファイルであり、通常のファイルのような実体はありません。あくまでもプロセス間の高速な通信としてファイルというインタフェースを利用するだけです。
Unix ドメインソケットは、 TCP 型(ストリーム型)と UDP 型(データグラム型)の両方の使い方ができます。
これらはTCPプロトコル、UDPプロトコル、ではない?
$ 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
8.2.2 で作ったUnixドメインサーバに対しての接続方法
curlとncどちらでもいける。
(curlでの送り方は、最後の引数はなんでもいいので、とても気持ち悪い感じ)
% curl --unix-socket "/tmp/unixdomainsocket-sample" hello
Hello Woirld
% nc -U /tmp/unixdomainsocket-sample
GET / HTTP/1.1
HTTP/1.0 200 OK
Hello Woirld
チャネルに見える
名前付きパイプの基本的な使い方として:
書き込むと、どこかで読み出されない限り書き込みが完了しない(ブロックする)
読み出そうとすると、読み出し可能な状態になるまでブロックする
ブロックする性質を利用して、あるプロセスが何かしらの処理が完了するのを別のプロセスから待つことができる。簡単には、何かしらの処理が完了したらパイプに何かを書き込み、それを事前に別のプロセスでreadしておけば待ち受けることができる。
そもそも名前付きパイプって何よ?
Linuxの名前付きパイプというのは、mkfifo
コマンドを使って作るパイプ。
無名パイプ(通常の |
)が、処理が終わったら破棄されるのに対して、名前付きパイプは永続的。
自分で破棄しなけければいけない。
% mkfifo named_pipe
% ll named_pipe
prw-r--r-- 1 jun docker 0 4月 19 23:18 named_pipe|
2021/04/26(月) XXX分
- 8.4 Unix ドメインソケットと TCP のベンチマークから
MEMO
- Unixドメインソケットの速度ににそれほど感動できなかった話(でも速いは早い)
- ファイルシステムのない世界は地獄
- inode領域とデータ領域にわかれている
- ハードリンクの有効活用シーンを知りたい
- inode保険
- やったか!? やってない
- 首の皮一枚
次回開催メモ
- 9.1.1 複雑なファイルシステムと VFS から
ベンチマーク用のコードはこのリポジトリにあるのが便利!
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
Go 言語と C 言語のインタフェースの違い はチェックとしておくといい感じ! ← C言語での解説の本との対応関係で困ったから!
ドメインソケットとパイプの違いって?
パイプは単方向のプロセス間通信
ドメインソケットは、双方向通信
もしも、ファイルシステムがなかったら...
ファイルシステムは紀元前からあった!?
しかしファイルシステムは、太古の時代から、ほとんどの OS で必ずと言っていいほど提供されてきた機能です。
重要されていることがわかるなあ。
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: -
inode領域とデータ領域が分かれている!
だから、「データ容量に余裕があるけど、ファイルがつくれないケース」はありうる(inode枯渇とか)
ディレクトリというのは、実を言うと、配下に含まれる ファイル名とその inode のインデックスの一覧表が格納されている特別なファイルです
p.162 †1 が デッドリンク
いつもの世界
ルートディレクトリ
- FileA
- FileB
- FileC
実際
ルートディレクトリ(inode:2)
- inode:A <-「FileA」
- inode:B <-「FileB」
- inode:C <-「FileC」
ディスクの世界
++---[inode:A]-----+---[inode:B] ---+
+0x142424234353535 + --0x343242353 -+
+-----------------------------------+
FileA
とかFileB
とか)はどこにあるの?
Q. ファイル名(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も消費せず、ディスクも消費しない?
消費ゼロってことはないだろうけど、きっと少ないはず
ハードリンクとシンボリックの違い
ハードリンクは、inodeに対する直接のリンク
シンボリックリンクは、よくあるショートカット
$ 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 4月 26 23:13 0 -> /dev/pts/1
802596 lrwx------ 1 jun docker 64 4月 26 23:13 1 -> /dev/pts/1
802597 lrwx------ 1 jun docker 64 4月 26 23:13 2 -> /dev/pts/1
802598 lr-x------ 1 jun docker 64 4月 26 23:13 3 -> /dev/tty
802599 lr-x------ 1 jun docker 64 4月 26 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 4月 26 23:19 ./
drwxrwxr-x 12 jun docker 4096 4月 26 21:19 ../
-rw-r--r-- 1 jun docker 1303 4月 26 23:19 server_test
-rw-r--r-- 1 jun docker 774 4月 26 21:19 tcpserver.go
-rw-r--r-- 1 jun docker 851 4月 26 21:19 unixdomainsocketstreamserver.go
deleted とは
やったか!? ← やってない
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/dir
にrename(2)
するけど、失敗(ディレクトリだから) - 失敗したら、
/hoge/dir/file
にrename(2)
なので成功する感じ
-
-
- 同じ
rename(2)
でも、言語(Go言語やBash)によって、どういうAPIをつくるかの違いがみれて面白い - この記事が有用すぎる! → Linux: ハードリンクと inode - Qiita
次回開催メモ
- 9.2.5 ファイルの属性の取得 から
また、ファイルシステムには他のファイルシステムをぶら下げる(マウント)こと
も可能です。
最近では、ジャーナリングファイルシステムといって、書き込み中に瞬断が発生し
てもストレージの管理領域と実際の内容に不整合が起きにくくする仕組みも広く利用
されています
ジャーナリングファイルシステムというのは、トランザクション管理みたい。
もともとDBで使われていた技術
vim の swapファイルみたいなものに近いものを感じる
ジャーナリングの流れ
ジャーナリングでは、ファイルシステム内にジャーナル領域という特殊な領域を用意します。ジャーナル領域は、ユーザには認識できないメタデータです。ファイルシステムを更新する際は次のような手順を踏みます。
武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 (Japanese Edition) (Kindle の位置No.3040-3042). Kindle 版.
上記2つの場合のいずれにせよ、ファイルシステムは不整合な状態にはならず、処理前か、あるいは処理後の状態になります。
武内 覚. [試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 (Japanese Edition) (Kindle の位置No.3052-3053). Kindle 版.
これらのさまざまなファイルシステムは、 Linux では VFS ( Virtual File System )と
いう API で、すべて統一的に扱えるようになっています
これら統一的になっているから、ユーザはファイルシステムは一個として見えるのかもしれない
LVMのイメージはこれやん!
いますぐ実践!Linuxシステム管理/Vol.166
スナップショットとは、ファイルシステムのある瞬間の状態を、そのまま抜き出したものです。(あ、ファイルシステムの場合は、ですね。)
もとのファイルシステムは、その後も更新可能ですが、スナップショットは、作成したときの状態をそのまま保持しつづけてくれます。
ですので、ファイルシステムを使用しながら、スナップショットを用いてバックアップをとったり、ヤバい操作を試す直前に、スナップショットを作成しておいて、案の定失敗したときに状態を戻したり、といったことができます。おお、便利そうですね。
LVMのスナップショット機能は、論理ボリュームで実現されています。
スナップショットを作成してから、もとのファイルシステムが更新されたときに、更新される前のデータを保持することで、スナップショット作成時のイメージを保持するようになっています。
(未変更部分は、もとのファイルシステムのデータを参照すれば済みますので、スナップショット側ではデータを保持しません。)
DVDに焼くパターンのバックアップ作戦の弱点
LVMによる自動バックアップ・システムの構築:Linux管理者への道(6)(2/3 ページ) - @IT
安全にバックアップを取るにはどうすればよいでしょうか? 非常に頻繁にデータの更新が行われるようなシステムの場合、システムの稼働中にバックアップを取ろうとすると、バックアップの最中にデータが変更されてしまう可能性があります。その場合、不正確な情報がバックアップされたり、バックアップデータが壊れてしまう可能性もあります。
元データに変更があったらタイムされる!! そりゃそうだけど、えらい!
journald というものが、システムログを記録している
罠〜
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))
}
これで、中身が表示されないのどういう原理?
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()
}
しかし 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}
}
p. 167 そういえば、USBメモリとかのファイルをドラッグアンドドロップすると、コピーされるわな
POSIX系OSであっても、マウントされていて元のデバイスが異なる場合には、renameシステムコールでの移動はできません。下
デバイスやドライブが異なる場合にはファイルを開いてコピーする必要があります。FreeBSDのmvコマンドも、最初にrenameシステムコールを試してみて†3、失敗したら入出力先のファイルを開いてコピーし、そのあとにソースファイルを消しています。
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
mv
コマンドの細かい動きがみえたっ!
いろいろif文してるのね
ファイル、ハードリンク、inode、異なるデバイス間でのmvが完全にまとまっていそうな資料
この実験おもしろそう!
inode に3つハードリンク (A,B,C) を作り、B を同じ、C を異なるファイルシステムに mv して、B, C それぞれを更新し A の変化を観察してみると分かると思います。ご興味があればやってみてください。(ここでは割愛します。)
感想と問い
- スナップショットの領域ってどこにあるんだろうね?
- スナップショットは、ハードリンクとかと関係性があるのかと思っていたけど、なんか違う気がしてきた
- renameシステムコールって、ハードリンクを書き換えるものだと思っていたけど、違うっぽい?
- mvコマンドの中身の挙動が面白い
- journalctlコマンドが、syslogを素直に見るよりわかりやすい感じがしている
2021/05/03(月) ○○分
- 9.2.5 ファイルの属性の取得 から
- 9.2.6 ファイルの存在チェック
- 9.2.7 OS 固有のファイル属性を取得する
MEMO
- なんで、Linuxって直接birthtimeみれないの!?
-
debugfs
でLinuxでもbirthtimeがみれるよ! (カーネル内に情報があるからね)-
debugfs
はデバイスファイルとかをいじるときに使うっぽい(きっと一生使わない説w)
-
- 存在確認せずに直接ファイル操作しようよ習慣
- 自転車置場の議論、わかる。とてもわかる
次回開催メモ
- 9.2.8 ファイルの同一性チェック
fmt.Printf("%o", hoge)
は 8進数な
既存の型に付けるやりかたは二種類ある
type MyType int
は Defined type
type MyType = int
はType alias
DefinedType と TypeAlias(型エイリアス)
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る
}
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パッケージの名前を参照できるようになりました。これは既存のコードには影響しません。
存在確認 → 書き込み は 非推奨な雰囲気
存在チェックそのもののシステムコールは提供されていません。
Python の os. path.exists() も内部で同じシステムコールを呼ぶ os.stat() を使っています し、C 言語でも stat() や、access() を代わりに使います。
access() は現在のプ ロセスの権限でアクセスできるかどうかを診断するシステムコールで、Go 言語は syscall.Access() として POSIX 系 OS で使えます。
ただし、存在チェックそのもの が不要であるというのが現在では主流のようです † 5 。
仮に存在チェックを行ってファイルがあることを確認しても、その後のファイル操 作までの間に他のプロセスやスレッドがファイルを消してしまうことも考えられま す。ファイル操作関数を直接使い、エラーを正しく扱うコードを書くことが推奨され ています。
Node.jsのドキュメントが非常にわかりやすい!
【疑問】
多くのLinuxでBirthTimeが無いのは、なぜ?
どういうときにChagedTime と ModifiedTime が変化するのか?
呼ばれるシステムコールで区別されている!
自転車置き場の議論
自転車置き場については誰もが理解している(もしくは理解していると自分では思っている)ため、自転車置き場の設置については終わりのない議論が生じることになる。関係者の誰もが自分のアイデアを加えることによって自分の存在を誇示したがるのである。
birthtime
はカーネル内に保持しているらしいが、それを直接取り出すAPIを提供していないらしい
Linuxでは
debugfs
使えば、brithtime
とれる!
一応、Linuxでも Root権限で 「To get the file creation date in linux, I use the following method 」あたりの説明
birthtime
(他で言うBirthTime)が取れないの?
謎: Q. なぜLinuxだとなんでなん?
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
debugfs ‐ 通信用語の基礎知識
ユーザープロセスから、デバイスやドライバーなどに関するカーネルのデバッグ情報にアクセスするためのインターフェイスとして提供される、オンメモリーのファイルシステムである。
/dev/nvme0n1p2 デバイスに、対話モードでアクセス。
sudo debugfs /dev/nvme0n1p2
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
2021/05/10(月) 100分
- 9.2.8 ファイルの同一性チェックから
MEMO
- DPDKの話を思い出せてよかった!
- OSのキャッシュ戦略に介入することによる高速化
- PostgreSQLのプランニングに影響する話
- HDDとSSDの影響
次回開催メモ
- 9.5.7 ディレクトリのトラバース
なお、inode 番号やデバイス番号は同一のストレージ/コンピューター内でしかユ ニークではないため、別のコンピューター間では衝突する可能性があります。
Etagでの確認
// 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
}
そのため、ファイルへデータを書き込むと、バッファに蓄えられた時点でアプリケー
ションに処理が返ります。ファイルからデータを読み込むときも、いったんバッファ
に蓄えられますし、すでにバッファに載っており、そのファイルに対する書き込みが
行われていない(バッファが新鮮)ならバッファだけにしかアクセスしません。
バッファが新鮮、という言い回しって、普通なんだろうか?
システムコールを介して読み書きは遅い
↓
そこで、バッファを使おう(裏で勝手に書き込みしたりしてくれる。処理が終わった瞬間、本当にディスクに書いてあるかはわからんということ)
↓
というか、普段は、アプリケーションとバッファ間でやりとりしていると考えても良いよね
↓
バッファの戦略はOS任せだけど、いじることもできるよ ← Dircet I/O
たとえば PostgreSQL では、
seq_page_cost および random_page_cost というシーケンシャルアクセスの場合と
ランダムアクセスの場合のコストを設定できるようになっており、デフォルトでは前者
が 1 、後者が 4 となっています。 SSD では 2 種類のアクセスの速度差が小さいため、両
方のコストに同じ値を設定することで、より高速な処理方法が選択される確率が上がります
これについては、なるほどって思った。
最近はSSD搭載が多いので、変わってきた
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)
}
2021/05/13(木) 110分
- 9.5.7 ディレクトリのトラバース から
MEMO
- ついに2ケタ章突入!
- トラバース と Walk
次回開催メモ
- 10.2 ファイルのロック(syscall.Flock()) から
トラバース(traverse)
ディレクトリのような木構造をすべてたどることを、コンピューター用語ではトラ バースといいます。
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
}
}
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があった場合の処理
}
}
Directory Walk というのは結構、メジャーな言い回しっぽい
監視したいファイルを OS 側に通知しておいて、変更があったら教えてもらう(パッシブな方式)
→ イベントリスナーに登録するような感じ
タイマーなどで定期的にフォルダを走査し、 os.Stat() などを使って変更を探しに行く(アクティブな方式)
→ ポーリングするイメージ
2021/05/17(月) 140分
- 10.2 ファイルのロック(syscall.Flock()) から
MEMO
- 勧告ロック(Advisedly Lock)
- 正しい使用に関してはアプリケーションが責任を持つことから勧告的ロックと呼ばれます。
-
syscall.Flock()
のブロッキング - 共有ロックのノンブロッキングってどういうことがわからんやつ
- コピーオンライトという最適化戦略
次回開催メモ
- 10.4 同期・非同期/ブロッキング・ノンブロッキング から
ただし、syscall.Flock() によるロック状態は、 通常のファイル入出力のためのシステムコールによっては確認されません。
ロック状態になっていたとしても、open(2)
や write(2)
がそれをちゃんとチェックするわけじゃないよってこと?
言い換えると、ロック状態になっていても、open(2)
やwrite(2)
ができてしまうってこと?
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
勧告ロックって要るんか?
ロック関連で、MVCC方式を思い出した
ロックの話はDBの話から探すのもアリ
PostgreSQLは、アプリケーション独自の意味を持つロックを生成する手法を提供します。 これは、その使用に関してシステムによる制限がないこと、つまり、正しい使用に関してはアプリケーションが責任を持つことから勧告的ロックと呼ばれます。 勧告的ロックは、MVCC方式に合わせづらいロック戦略で有用に使用することができます。 例えば、勧告的ロックのよくある利用として、いわゆる「フラットファイル」データ管理システムで典型的な、悲観的なロック戦略を模擬することです。 この用途のためにテーブル内にフラグを格納することもできますが、勧告的ロックの方が高速で、テーブルの膨張を防ぐことができます。 また、セッション終了時にサーバによる自動整理を行うこともできるようになります。
ただし、 syscall.Flock() によるロック状態は、
通常のファイル入出力のためのシステムコールによっては確認されません。そのた
め、ロックをまじめに確認しないプロセスが 1 つでもあると、自由に上書きされてし
まう可能性があります。
●ターミナル1
$ flock /tmp/lock vi file1
●ターミナル2
flockで同じロックファイルを指定すると、ブロックされる
$ flock /tmp/lock cat file1
# ブロックされる
別にファイルじゃなくても良い
$ flock /tmp/lock cat ls
# ブロックされる
参考記事
ロック処理の種類
ANON は ANONYMOUS の略っぽい
匿名メモリ とか Anonymous Memory で検索!
コピーオンライト(Copy-On-Write)
書き換えが発生するまでは、お得。
上京するまでは家賃は一緒ってこと。
コピーオンライト時は、単に読み込みだけで使用されていた場合に、通常どおりメモリ領域にファイルをマッピングします。複数のプロセスが同じファイルをマッピングしていたとすると、カーネル上は1つ分のみメモリ領域が使用され、それ以上のメモリは消費しません。しかし、その領域内でメモリ書き換えが発生するとその領域がまるごとコピーされます。そのため、元のファイルには変更が反映されません。不思議な挙動ですが、書き換えが発生するまでは複数バリエーションの状態を保持する必要がないので、メモリを節約できます。
コピーオンライト: 書き換えが必要になるまでは、原本を直接みたらええやん!
最適化戦略の1つ! って考えから始めるといい感じっぽい。
コンピュータ内部で、ある程度大きなデータを複製する必要が生じたとき、愚直な設計では、直ちに新たな空き領域を探して割り当て、コピーを実行する。
ところが、もし複製したデータに対する書き換えがなければその複製は無駄だったことになる。そこで、複製を要求されても、コピーをした振りをして、とりあえず原本をそのまま参照させるが、ただし、そのままで本当に書き換えてはまずい。
原本またはコピーのどちらかを書き換えようとしたときに、それを検出し、その時点ではじめて新たな空き領域を探して割り当て、コピーを実行する。
これが「書き換え時にコピーする」、すなわちコピーオンライト(Copy-On-Write)の基本的な形態である。基盤となる考え方は、複数の(何らかの)要求者がリソースを要求するときに、少なくとも当初はそれらの要求を区別する必要がないときに同じリソースを与える、というものである。
これは要求者がリソースを「更新」しようとするまで保持され、「更新」が他者に見えないようにリソースの個別のコピーを必要になった時点で作成する。
要求者からはこの一連の動きは見えない。
第一の利点は要求者が全く更新しなければ、個別のコピーを作成する必要が生じないという点である。
もちろん、コピー オンライト機能を使う場合や、確保したメモリの領域にアセンブリ命令が格納されて いて実行を許可する必要がある場合には、mmap 一択です。
むむ?
2021/05/20(木) ○○○○分
- 10.4 同期・非同期/ブロッキング・ノンブロッキング から
MEMO
- 同期・非同期/ブロッキング・ノンブロッキング の概念は混乱しがち! シーケンス図で見ようぞ!
- 正誤表チャンス https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-5931c13169b7f5
- issueにコメントした → https://github.com/LambdaNote/errata-gosyspro-1-4/issues/204
- MVPコメント! https://zenn.dev/mohira/scraps/c0aca378ac9fa7#comment-fa4de600496982
次回開催メモ
- 10.6 FUSE を使った自作のファイルシステムの作成 から
syscall.Flock() によるロックでは、すでにロックされているファイルに対して
ロックをかけようとすると、最初のロックが外れるまでずっと待たされます。そのた
め、定期的に何度もアクセスしてロックが取得できるかトライする、といったことが
できません。これを可能にするのがノンブロッキングモードです( 10.4 節で少し詳し
く説明します)。
今日明かされる話
同期処理と非同期処理 の厳密目な区別
同期処理と非同期処理は、ここでは実データを取りに行くのか、通知をもらうのか で区別されます。
ファイルI/O、ネットワークI/Oなどは、CPU内部処理と比べると劇的に遅いらしい。
その重い処理に引きずられないように、同期処理と非同期処理、そしてブロッキング処理とノンブロッキング処理という分類があるらしい
p.194 正誤表チャンス!? 助詞がおかしい
- 同期処理:OS に I/O タスクを投げて、入出力の準備ができたらアプリケーション が返ってくる
- 同期: OSに仕事を投げて、入出力の準備ができたらアプリケーションに処理が返ってくる
根拠
ASCII.jp:ファイルシステムと、その上のGo言語の関数たち(3) では、「に」になっているから
さらなる提案
- 「実データを」って入れると良さそう
- Before
- 同期処理:OS に I/O タスクを投げて、入出力の準備ができたらアプリケーション が返ってくる
- After
- 同期処理:OS に I/O タスクを投げて、入出力の準備ができたら実データがアプリケーションに返ってくる
AIO(Asynchoronous I/O)
疑問
同期・ノンブロッキング処理は、「APIを呼ぶと即座に完了していないかどうかと、現在準備しているデータが得られる」
クライアントが完了を知る必要があるときには、完了が返ってくるまで何度もAPIを呼ぶ(ポーリング)
ポーリングしない限りは、データももらえないってことだろうか。
epoll
の e
って何?
疑問: 同期/非同期 と ブロッキング/ノンブロッキング は実装でみてほうが理解早そう
(言葉での曖昧さが消せない)
EAGAIN
は Error と TryAGAIN って感じ
EAGAINがよくわからなかったので、errno-bash.h
の中身を見てみた。
参考記事
● /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
「同期処理かつブロッキング」vs「同期処理かつノンブロッキング」
ブロッキングだろうが、ノンブロッキングだろうが、同期処理なのであれば、最終的に実データを、取りに行かないといけない(プロセスが、ね)
で、ノンブロッキングは、合間を縫っていろいろ作業できる。依頼したIOタスク以外のことをやってもいいし、「あの〜、依頼してたデータ取れました〜?」とポーリングすることもできる。
で、どれだけポーリングしてようが、結局、実データを手に入れるわけ(だって、同期処理だから)。
ブロッキングとノンブロッキングの、「待機する」ってのは、水色っぽいバーが分割されているのをみればわかると思う。
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)
}
同期処理のメソッドを、goroutineで囲むことによって簡単に非同期処理にすることができるGo言語すごいって感じた
スクラップ投稿数の上限を超えてしまうので、続きはこちらで!
完!