diff --git a/1.14/signal_based_preemption.md b/1.14/signal_based_preemption.md index e184c05..a573f70 100644 --- a/1.14/signal_based_preemption.md +++ b/1.14/signal_based_preemption.md @@ -124,7 +124,7 @@ sigaction 的三个参数: ### sigaltstack -修改信号执行时所用的函数栈。 +修改信号执行时所用的函数栈。 ## 简单的信号处理函数 @@ -380,7 +380,79 @@ asyncPreempt 是汇编实现的,分为三个部分: └─────────────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────┘ ``` +#### 你所想的上下文切换, 不一定等于你想要的上下文切换 + +这边需要注意的是, 抢占式调度中, 是`asyncPremmpt`这个操作保存了所有寄存器的值, 而`go`内部的诸如`systemstack`、`mcall`以及包括`runtime.Gosched()`的使用等, 是不会进行所有寄存器值的保留的, 所以在一些面对一些不常见的使用场景的时候需要注意, 避免寄存器发生非预期的篡改 + +例: +```go +// test.go +package main + +import ( + "fmt" + "os" + "os/signal" + "runtime" + "syscall" + "time" +) + +//go:noescape +func setget(v int64) int64 + +//go:noescape +func set(v int64) + +func gosched() { + runtime.Gosched() +} +func main() { + _ = gosched // avoid warning + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGURG) + go func() { + // for debug + for { + <-c + fmt.Println("SIGURG") + } + }() + go func() { + for { + if v := setget(1); v != 1 { + fmt.Println("not equal:", v) + os.Exit(1) + } else { + fmt.Println("equal") + } + } + }() + go func() { + for { + set(2) + } + }() + time.Sleep(time.Hour) +} +``` +```assembly +//test.s +#include "textflag.h" + +TEXT ·setget(SB),NOSPLIT,$0-16 + MOVQ v+0(FP), R13 + //CALL ·gosched(SB) // runtime.Gosched() 没有进行所有寄存器现场的保留 + MOVQ R13, ret+8(FP) + RET +TEXT ·set(SB),NOSPLIT,$0-8 + MOVQ v+0(FP), R13 + RET + +``` +以上这个简短的程序实现的是内部正常调度(非抢占式)下, 调度前后寄存器异常的问题。如果想要在应用内很好控制自身协程调度的, 那么就要小心这类问题。 ## asyncPreempt2 +接下去进行的就是常规内部上下文切换`mcall` ```go func asyncPreempt2() { diff --git a/assembly.md b/assembly.md index a246c16..474251c 100644 --- a/assembly.md +++ b/assembly.md @@ -135,7 +135,7 @@ Go 的汇编还引入了 4 个伪寄存器,援引官方文档的描述: >- `FP`: Frame pointer: arguments and locals. >- `PC`: Program counter: jumps and branches. >- `SB`: Static base pointer: global symbols. ->- `SP`: Stack pointer: top of stack. +>- `SP`: Stack pointer: the highest address within the local stack frame. 官方的描述稍微有一些问题,我们对这些说明进行一点扩充: @@ -152,7 +152,7 @@ Go 的汇编还引入了 4 个伪寄存器,援引官方文档的描述: 4. 在 go tool objdump/go tool compile -S 输出的代码中,是没有伪 SP 和 FP 寄存器的,我们上面说的区分伪 SP 和硬件 SP 寄存器的方法,对于上述两个命令的输出结果是没法使用的。在编译和反汇编的结果中,只有真实的 SP 寄存器。 5. FP 和 Go 的官方源代码里的 framepointer 不是一回事,源代码里的 framepointer 指的是 caller BP 寄存器的值,在这里和 caller 的伪 SP 是值是相等的。 -以上说明看不懂也没关系,在熟悉了函数的栈结构之后再反复回来查看应该就可以明白了。个人意见,这些是 Go 官方挖的坑。。 +以上说明看不懂也没关系,在熟悉了函数的栈结构之后再反复回来查看应该就可以明白了。 ## 变量声明 @@ -258,7 +258,7 @@ TEXT ·get(SB), NOSPLIT, $0-8 // => 只有函数头,没有实现 TEXT pkgname·add(SB), NOSPLIT, $0-8 MOVQ a+0(FP), AX - MOVQ a+8(FP), BX + MOVQ b+8(FP), BX ADDQ AX, BX MOVQ BX, ret+16(FP) RET @@ -999,11 +999,5 @@ go compile -S: 参考资料[4]需要特别注意,在该 slide 中给出的 callee stack frame 中把 caller 的 return address 也包含进去了,个人认为不是很合适。 + - - diff --git a/atomic.md b/atomic.md index 59b12b4..ed3e9c4 100644 --- a/atomic.md +++ b/atomic.md @@ -359,4 +359,6 @@ TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, $0-12 MOVL val+8(FP), AX XCHGL AX, 0(BX) // 交换指令 RET -``` \ No newline at end of file +``` + + diff --git a/bootstrap.md b/bootstrap.md index f058f98..20337a6 100644 --- a/bootstrap.md +++ b/bootstrap.md @@ -518,4 +518,6 @@ func main() { \ No newline at end of file +--> + + diff --git a/channel.md b/channel.md index dc4af9c..a31d9e3 100644 --- a/channel.md +++ b/channel.md @@ -305,7 +305,7 @@ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { } // qcount 是 buffer 中已塞进的元素数量 - // dataqsize 是 buffer 的总大小 + // dataqsiz 是 buffer 的总大小 // 说明还有余量 if c.qcount < c.dataqsiz { // Space is available in the channel buffer. Enqueue the element to send. @@ -705,3 +705,6 @@ func closechan(c *hchan) { Q: 如果有多个channel同时唤醒同一个goroutine,这个并发控制是怎么做的? Q: 为什么向 channel 发数据的时候,会直接把数据从一个 goroutine 的栈拷贝到另一个 goroutine 的栈? + + + diff --git a/compilers.md b/compilers.md new file mode 100644 index 0000000..f9ab2f0 --- /dev/null +++ b/compilers.md @@ -0,0 +1,1247 @@ +# 编译原理 +> 本文主要介绍编译的几个主要过程及周边工具的使用, 对于工具内部具体实现的算法不做分析, 感兴趣的可自行搜索 + +## 词法分析 + +```mermaid +sequenceDiagram + Source code->>Token stream: Lexical analysis + Note left of Source code: 1 + (2 - 3) * 4 / 5 + Note left of Source code: SELECT * FROM TABLE1 LIMIT 1; + Note left of Source code: {"key1": 1, "key2": "val", "key3": {}} + #------ + Note right of Token stream: 1
+
(
2
-
3
)
*
4
/
5 + Note right of Token stream: SELECT
*
FROM
TABLE1
LIMIT
1
; + Note right of Token stream: {
"key1"
:
1
,
"key2"
:
"val"
,
"key3"
:
{
,
}
,
}
+``` + +第一步将源代码处理成为`token stream`, 这边的源代码可以是一段简单的`go`代码, `DML`, `DSL`, 甚至是`JSON`格式的文件或者其他文本内容等等, `Lexical analysis`的目的就是按照某个定义规则将文本处理成为一连串的`token stream` +> 标记 / Token: 指处理好后的一个字串, 是构成源代码的最小单位, 比如我们可以归类 golang 中的关键字, 例如 var、const、import 等等, 或者一个字符串变量 "str" 或者操作符 :=、>=、== 等等,只要是符合我们定义的语法规则处理后出现的字串, 都可以称为一个 token + +如上图, 左边框内的三条源代码案例, 经过词法分析后, 可能会(具体看自己对`token`的定义处理规则)输出右边的三块`token stream`(每一行代表一个`token`) + +### lex / flex +lex / flex 是常用的词法分析器,支持正则表示某类 token + +flex 文件完整格式: +```c +%{ +Declarations +%} +Definitions +%% +Rules +%% +User subroutines +``` + +例: +test.l +```c + +/* Declarations */ +%{ +void yyerror(const char *msg); +%} + + +/* Definitions */ +WHITESPACE ([ \t\r\a]+) +OPERATOR ([+*-/%=,;!<>(){}]) +INTEGER ([0-9]+) + + +/* Rules */ +%% + +{WHITESPACE} { /* void */ } + +{OPERATOR} { printf("%s\n", yytext); } + +{INTEGER} { printf("%d\n", atoi(yytext)); } + +\n { /* void */ } + +. { printf("analysis error: unknow [%s]\n", yytext); exit(1); } + +%% + +/* User subroutines */ +int main(int argc, char* argv[]) { + FILE *fp = NULL; + if (argc == 2) { + fp = fopen(argv[1], "r"); + if (fp) { + yyin = fp; + } + } + yylex(); + if (fp) { + fclose(fp); + } + return 0; +} + +int yywrap(void) { + return 1; +} + +void yyerror(const char *msg) { + fprintf(stderr, "Error :\n\t%s\n", msg); + exit(-1); +} +``` + + +以上小段词法分析代码定义了三种`token`:`WHITESPACE`, `OPERATOR`, `INTEGER`, 分别用正则定义了他们的规则, 而后在 `Rules` 规则阶段分别对这三种 `token` 进行了各自的处理 +```shell +# 编译 +flex -o test.c test.l +gcc -std=c89 -o flextest test.c +./test.c test.txt +``` +而后用根据我们定义的规则生成的词法分析器`flextest`来处理一个简单的案例 + +```shell +cat test.txt +1 + (2 - 3) * 4 / 5 sss + +./flextest ./test.txt +1 ++ +( +2 +- +3 +) +* +4 +/ +5 +analysis error: unknow [s] +``` +根据输出的`token stream`可以看到, 能通过`token`规则处理的字串会完成输出一个成功处理的`token`, 规则之外的则处理失败 + +经过以上的小案例, 那么如果让我们自己来做一个`golang`的词法分析 `token` 的定义, 难度就不会特别大了 + +这边可以来简单看下`golang`编译器源码内的`token`定义 + +```go +// src/go/token/token.go +var tokens = [...]string{ + ILLEGAL: "ILLEGAL", + + EOF: "EOF", + COMMENT: "COMMENT", + + IDENT: "IDENT", + INT: "INT", + FLOAT: "FLOAT", + IMAG: "IMAG", + CHAR: "CHAR", + STRING: "STRING", + + ADD: "+", + SUB: "-", + MUL: "*", + QUO: "/", + REM: "%", + + AND: "&", + OR: "|", + XOR: "^", + SHL: "<<", + SHR: ">>", + AND_NOT: "&^", + + ADD_ASSIGN: "+=", + SUB_ASSIGN: "-=", + MUL_ASSIGN: "*=", + QUO_ASSIGN: "/=", + REM_ASSIGN: "%=", + + AND_ASSIGN: "&=", + OR_ASSIGN: "|=", + XOR_ASSIGN: "^=", + SHL_ASSIGN: "<<=", + SHR_ASSIGN: ">>=", + AND_NOT_ASSIGN: "&^=", + + LAND: "&&", + LOR: "||", + ARROW: "<-", + INC: "++", + DEC: "--", + + EQL: "==", + LSS: "<", + GTR: ">", + ASSIGN: "=", + NOT: "!", + + NEQ: "!=", + LEQ: "<=", + GEQ: ">=", + DEFINE: ":=", + ELLIPSIS: "...", + + LPAREN: "(", + LBRACK: "[", + LBRACE: "{", + COMMA: ",", + PERIOD: ".", + + RPAREN: ")", + RBRACK: "]", + RBRACE: "}", + SEMICOLON: ";", + COLON: ":", + + BREAK: "break", + CASE: "case", + CHAN: "chan", + CONST: "const", + CONTINUE: "continue", + + DEFAULT: "default", + DEFER: "defer", + ELSE: "else", + FALLTHROUGH: "fallthrough", + FOR: "for", + + FUNC: "func", + GO: "go", + GOTO: "goto", + IF: "if", + IMPORT: "import", + + INTERFACE: "interface", + MAP: "map", + PACKAGE: "package", + RANGE: "range", + RETURN: "return", + + SELECT: "select", + STRUCT: "struct", + SWITCH: "switch", + TYPE: "type", + VAR: "var", +} +``` +附上一段 `go` 内置实现的词法分析 +```go +package main + +import ( + "fmt" + "go/scanner" + "go/token" +) + +func main() { + // src is the input that we want to tokenize. + src := ` + package main + + func main() { + var num1, num2 int + num1 += num2 + _ = num1 + num1 += "str" + return + } + ` + + // Initialize the scanner. + var s scanner.Scanner + fset := token.NewFileSet() // positions are relative to fset + file := fset.AddFile("", fset.Base(), len(src)) // register input "file" + s.Init(file, []byte(src), nil /* no error handler */, scanner.ScanComments) + + // Repeated calls to Scan yield the token sequence found in the input. + fmt.Printf("%s\t%s\t%s\n", "pos", "token", "literal") + for { + pos, tok, lit := s.Scan() + if tok == token.EOF { + break + } + fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit) + } +} + +``` +```shell +#output +pos token literal +2:2 package "package" +2:10 IDENT "main" +2:14 ; "\n" +4:2 func "func" +4:7 IDENT "main" +4:11 ( "" +4:12 ) "" +4:14 { "" +5:3 var "var" +5:7 IDENT "num1" +5:11 , "" +5:13 IDENT "num2" +5:18 IDENT "int" +5:21 ; "\n" +6:3 IDENT "num1" +6:8 += "" +6:11 IDENT "num2" +6:15 ; "\n" +7:3 IDENT "_" +7:5 = "" +7:7 IDENT "num1" +7:11 ; "\n" +8:3 IDENT "num1" +8:8 += "" +8:11 STRING "\"str\"" +8:16 ; "\n" +9:3 return "return" +9:9 ; "\n" +10:2 } "" +10:3 ; "\n" +``` +注意, 这边示例代码有意使用了错误的语法, 目的是为了让大家知道`token`提取的词法分析过程中, 是没有必要同步进行语义的分析的, 输出的结果也可以和之前的`token`列表自行对照一下 +## 语法分析 +根据第一步[词法分析](#词法分析)我们目前已经获取到了自源代码处理好之后的一个`token stream`, 在语法分析阶段主要负责的就是把这一串「看似毫无规则」的标记流进行语法结构上的处理 + +例如 +1. 判断某个赋值操作是否可以执行, 赋值号两边的变量及数据类型是否匹配 +2. 运算规则是否符合语法规则 +3. 语句优先级 +4. …… + +在这个阶段可以直接翻译成目标代码, 或者生成诸如语法树之类的数据结构以便后续语义分析,优化等阶段利用。 + +> 上下文无关文法: 文法中所有的产生式左边只有一个非终结符 +> https://www.zhihu.com/question/21833944 + +### token stream 处理过程第一步 +对于`token stream`首先我们得处理的是初步生成语法树而后交由下面的步骤进行处理, 然而这里并不是随意生成一颗语法树的, 它得以某种规则进行初步的约束, 可以试想, 如果生成的语法树有多种可能, 每次生成的结果都不一致, 那么对于这类的语法树进行后续的分析则没有任何意义 + +本案例中我们要实现的是一个简单的`SQL`解析器, 那么接下来看看如何通过`token stream`初步生成我们所需要的语法树 + +回头看下上下文无关语法的简单介绍, 所有的`产生式`右边都是由左侧唯一的`非终结符`产生的, 就例如我们常见的一条 `SQL` +`SELECT * FROM TABLE1;` +用上下文表达式可简单表达为 +```shell +QUERY = [SELECT TOKEN] KEY [FROM TOKEN] TABLE; +KEY = [* TOKEN] | TOKEN | TOKEN , +TABLE = TOKEN +``` +> 这个案例中 `KEY` 定义为三种形式, 对应的 `SQL` 分别为 `SELECT * FROM TABLE;` `SELECT ID FROM TABLE;` `SELECT ID, COLUMN1, COLUMN2 FROPM TABLE;` 用竖线 | 来表示推到的多种可能 + +可以清晰地看到, 最开始看似杂乱无序的一条语句最终就可以通过分治的思想化解为一个个小问题, 以此类推最终对应到我们制定的 `TOKEN` 规则, 如果`TOKEN STREAM`最终匹配不上我们所制定的所有规则, 那么则解析失败 + + +有了此类分析规则的简单概念后, 接下来我们就利用几个简单的工具实现这个解析器的功能 +### bison +[bison](https://www.gnu.org/software/bison/) GNU bison是一个自由软件,用于自动生成语法分析器程序,实际上可用于所有常见的操作系统. 可以结合上文`flex`分析后生成的`token stream`继续进行语法部分的分析 + +> 继上文`flex`工具编辑一个简单的`SQL`解析器, 本案例非完全支持完整语法, 仅支持`SELECT A, B, C FROM TABLE;` `SELECT A, B, C, *;` 形式用作展示 + +test.l +```c +/* Declarations */ +%{ +#define YYSTYPE char * +#include "y.tab.h" +void yyerror(const char *msg); +#define _DUPTEXT { yylval = strdup(yytext); } +%} + + +/* Definitions */ +IDENTIFIER [_a-zA-Z][_a-zA-Z0-9]* +OPERATOR [,;*] +WHITESPACE ([ \t\r\a]+) +/* Rules */ +%% +\n { /* void */ } +SELECT { _DUPTEXT; return T_SELECT; } +FROM { _DUPTEXT; return T_FROM; } +{WHITESPACE} { /* ignore every whitespace */ } +{OPERATOR} { _DUPTEXT; return yytext[0]; } +{IDENTIFIER} { _DUPTEXT; return T_IDENTIFIER; } +. { return 0; } + +%% + +int yywrap(void) { + return 1; +} + +void yyerror(const char *s) { + extern int yylineno; + extern char *yytext; + int len = strlen(yytext); + int i = 0; + char buf[512] = {0}; + for (; i < len; i++) + { + sprintf(buf, "%s%d:%c ", buf, yytext[i], yytext[i]); + } + fprintf(stderr, "ERROR: %s at symbol '%s' on line %d\n", s, buf, yylineno); +} +``` +test.y +```c +%{ +#include +#include +#include +#include "tree.h" +int columns = 0; +%} +/* 定义产生式所产生的数据类型 */ +%union{ + struct ast *_ast; + char** strs; + char* str; +} +/* 定义各个产生式返回的数据类型 */ +%type <_ast> Q +%type K +%type '*' ';' +%token T_SELECT T_FROM T_IDENTIFIER + +%start S +/* + SELECT A, B, C FROM TABLE; + SELECT A, B, C, *; +*/ +%% + +S : { /* void */ } + | Q { ast_print($1); ast_free($1); exit(0); } + ; +/* SQL 产生式定义 */ +Q : T_SELECT K T_FROM T_IDENTIFIER ';' { + struct ast *_ast = new_ast(); + _ast->command = T_SELECT; + ast_add_table(_ast, $4); + ast_add_fields(_ast, $2); + free($4); + int i = 0; + for (; i < _ast->filed_size; i++) { + free($2[i]); + } + free($2); + $$ = _ast; /* 将生产的 struct ast* 作为返回值返返回上一级产生式, 每个产生式的返回值有数据类型的限制, 具体看用户定义 */ + } + + | T_SELECT K ';' { + struct ast *_ast = new_ast(); + _ast->command = T_SELECT; + ast_add_fields(_ast, $2); + int i = 0; + for (; i < _ast->filed_size; i++) { + free($2[i]); + } + free($2); + $$ = _ast; + } + ; + +/* columns 产生式定义 */ +K : '*' { + columns++; + char** filed = malloc(sizeof(char*)); + filed[0] = strdup($1); + $$ = filed; + } + | K ',' T_IDENTIFIER { + columns++; + int len = columns; + char** result = malloc(sizeof(char*) * len); + int i = 0; + for (; i < len-1; i++) { + result[i] = strdup($1[i]); + free($1[i]); + } + free($1); + result[i] = strdup($3); + $$ = result; + } + | T_IDENTIFIER { + columns++; + char** filed = malloc(sizeof(char*)); + filed[0] = strdup($1); + $$ = filed; + } + ; + +%% + +int main() { + return yyparse(); +} +``` +tree.h +```c +struct ast +{ + int command; + char **fileds; + int filed_size; + char *table; +}; +void ast_add_table(struct ast *, char *); + +void ast_add_fields(struct ast *, char **); + +void ast_print(struct ast *); + +void ast_free(struct ast *); + +struct ast *new_ast(); + +extern int columns; +``` +tree.h +```c +#include "tree.h" +#include +#include +#include +#include "y.tab.h" +struct ast *new_ast() +{ + struct ast *_ast = malloc(sizeof(struct ast)); + _ast->table = NULL; + _ast->filed_size = 0; + _ast->fileds = NULL; + return _ast; +} + +void ast_print(struct ast *_ast) +{ + if (!_ast) + return; + printf("command: %d\n", _ast->command); + int i = 0; + for (; i < _ast->filed_size; i++) + { + printf("column%i:%s\n", i, _ast->fileds[i]); + } + printf("tablename: %s\n", _ast->table); + return; +} + +void ast_add_table(struct ast *_ast, char *table) +{ + if (!_ast || _ast->table) + return; + _ast->table = strdup(table); + return; +} + +void ast_add_fields(struct ast *_ast, char **fileds) +{ + if (!_ast || _ast->fileds) + return; + int len = columns; + _ast->filed_size = len; + char **_fileds = malloc(sizeof(char *) * len); + int i = 0; + for (; i < len; i++) + { + _fileds[i] = strdup(fileds[i]); + } + _ast->fileds = _fileds; +} + +void ast_free(struct ast *_ast) +{ + if (_ast->table) + free(_ast->table); + int i = 0; + for (; i < _ast->filed_size; i++) + free(_ast->fileds[i]); + free(_ast->fileds); + free(_ast); +} +``` +output +```shell +flex test.l && bison -vdty test.y && gcc -std=c89 -o test y.tab.c lex.yy.c tree.c +./test +SELECT A FROM B; +command: 258 +column0:A +tablename: B +``` +成功解析这条小`SQL`的各个部分, 当然这边的过程生成的是一个非常简单的数据结构, 仅作对应信息的统计, 通过设计更成熟的语法树而后结合`token stream`及以上的概念就可以初步生成一棵后续步骤所需要的语法树 + +再回过头来看看第一步中那段`go`代码生成的语法树(依旧是用错误代码来生成,提示此处的语法树生成仅仅是初步的阶段, 后续还要进行诸多步骤的处理) +```go +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "log" +) + +func main() { + src := ` + package main + + func main() { + var num1, num2 int + num1 += num2 + _ = num1 + num1 += "str" + return + } + ` + + // Initialize the parser. + fset := token.NewFileSet() // positions are relative to fset + f, err := parser.ParseFile(fset, "", src, 0) + if err != nil { + log.Fatalln(err) + } + ast.Print(fset, f) +} +``` +#### AST_output: +```shell + 0 *ast.File { + 1 . Package: 2:2 + 2 . Name: *ast.Ident { + 3 . . NamePos: 2:10 + 4 . . Name: "main" + 5 . } + 6 . Decls: []ast.Decl (len = 1) { + 7 . . 0: *ast.FuncDecl { + 8 . . . Name: *ast.Ident { + 9 . . . . NamePos: 4:7 + 10 . . . . Name: "main" + 11 . . . . Obj: *ast.Object { + 12 . . . . . Kind: func + 13 . . . . . Name: "main" + 14 . . . . . Decl: *(obj @ 7) + 15 . . . . } + 16 . . . } + 17 . . . Type: *ast.FuncType { + 18 . . . . Func: 4:2 + 19 . . . . Params: *ast.FieldList { + 20 . . . . . Opening: 4:11 + 21 . . . . . Closing: 4:12 + 22 . . . . } + 23 . . . } + 24 . . . Body: *ast.BlockStmt { + 25 . . . . Lbrace: 4:14 + 26 . . . . List: []ast.Stmt (len = 5) { + 27 . . . . . 0: *ast.DeclStmt { + 28 . . . . . . Decl: *ast.GenDecl { + 29 . . . . . . . TokPos: 5:3 + 30 . . . . . . . Tok: var + 31 . . . . . . . Lparen: - + 32 . . . . . . . Specs: []ast.Spec (len = 1) { + 33 . . . . . . . . 0: *ast.ValueSpec { + 34 . . . . . . . . . Names: []*ast.Ident (len = 2) { + 35 . . . . . . . . . . 0: *ast.Ident { + 36 . . . . . . . . . . . NamePos: 5:7 + 37 . . . . . . . . . . . Name: "num1" + 38 . . . . . . . . . . . Obj: *ast.Object { + 39 . . . . . . . . . . . . Kind: var + 40 . . . . . . . . . . . . Name: "num1" + 41 . . . . . . . . . . . . Decl: *(obj @ 33) + 42 . . . . . . . . . . . . Data: 0 + 43 . . . . . . . . . . . } + 44 . . . . . . . . . . } + 45 . . . . . . . . . . 1: *ast.Ident { + 46 . . . . . . . . . . . NamePos: 5:13 + 47 . . . . . . . . . . . Name: "num2" + 48 . . . . . . . . . . . Obj: *ast.Object { + 49 . . . . . . . . . . . . Kind: var + 50 . . . . . . . . . . . . Name: "num2" + 51 . . . . . . . . . . . . Decl: *(obj @ 33) + 52 . . . . . . . . . . . . Data: 0 + 53 . . . . . . . . . . . } + 54 . . . . . . . . . . } + 55 . . . . . . . . . } + 56 . . . . . . . . . Type: *ast.Ident { + 57 . . . . . . . . . . NamePos: 5:18 + 58 . . . . . . . . . . Name: "int" + 59 . . . . . . . . . } + 60 . . . . . . . . } + 61 . . . . . . . } + 62 . . . . . . . Rparen: - + 63 . . . . . . } + 64 . . . . . } + 65 . . . . . 1: *ast.AssignStmt { + 66 . . . . . . Lhs: []ast.Expr (len = 1) { + 67 . . . . . . . 0: *ast.Ident { + 68 . . . . . . . . NamePos: 6:3 + 69 . . . . . . . . Name: "num1" + 70 . . . . . . . . Obj: *(obj @ 38) + 71 . . . . . . . } + 72 . . . . . . } + 73 . . . . . . TokPos: 6:8 + 74 . . . . . . Tok: += + 75 . . . . . . Rhs: []ast.Expr (len = 1) { + 76 . . . . . . . 0: *ast.Ident { + 77 . . . . . . . . NamePos: 6:11 + 78 . . . . . . . . Name: "num2" + 79 . . . . . . . . Obj: *(obj @ 48) + 80 . . . . . . . } + 81 . . . . . . } + 82 . . . . . } + 83 . . . . . 2: *ast.AssignStmt { + 84 . . . . . . Lhs: []ast.Expr (len = 1) { + 85 . . . . . . . 0: *ast.Ident { + 86 . . . . . . . . NamePos: 7:3 + 87 . . . . . . . . Name: "_" + 88 . . . . . . . } + 89 . . . . . . } + 90 . . . . . . TokPos: 7:5 + 91 . . . . . . Tok: = + 92 . . . . . . Rhs: []ast.Expr (len = 1) { + 93 . . . . . . . 0: *ast.Ident { + 94 . . . . . . . . NamePos: 7:7 + 95 . . . . . . . . Name: "num1" + 96 . . . . . . . . Obj: *(obj @ 38) + 97 . . . . . . . } + 98 . . . . . . } + 99 . . . . . } + 100 . . . . . 3: *ast.AssignStmt { + 101 . . . . . . Lhs: []ast.Expr (len = 1) { + 102 . . . . . . . 0: *ast.Ident { + 103 . . . . . . . . NamePos: 8:3 + 104 . . . . . . . . Name: "num1" + 105 . . . . . . . . Obj: *(obj @ 38) + 106 . . . . . . . } + 107 . . . . . . } + 108 . . . . . . TokPos: 8:8 + 109 . . . . . . Tok: += + 110 . . . . . . Rhs: []ast.Expr (len = 1) { + 111 . . . . . . . 0: *ast.BasicLit { + 112 . . . . . . . . ValuePos: 8:11 + 113 . . . . . . . . Kind: STRING + 114 . . . . . . . . Value: "\"str\"" + 115 . . . . . . . } + 116 . . . . . . } + 117 . . . . . } + 118 . . . . . 4: *ast.ReturnStmt { + 119 . . . . . . Return: 9:3 + 120 . . . . . } + 121 . . . . } + 122 . . . . Rbrace: 10:2 + 123 . . . } + 124 . . } + 125 . } + 126 . Scope: *ast.Scope { + 127 . . Objects: map[string]*ast.Object (len = 1) { + 128 . . . "main": *(obj @ 11) + 129 . . } + 130 . } + 131 . Unresolved: []*ast.Ident (len = 1) { + 132 . . 0: *(obj @ 56) + 133 . } + 134 } +``` +### goyacc +`goyacc` 是一个 `golang` 版的 `yacc` 工具(作用和上文介绍的`flex & bison`)类似, 不同的是没有对应的`lex`工具, 这部分逻辑需要自己实现 +接下来用`goyacc`将上述的`SQL`小解析器实现一遍 + +parser.y +```c +%{ +package sql + +var columns int; + +func setResult(l yyLexer, v *ast) { + l.(*lex).result = v +} + +%} +/* 定义产生式所产生的数据类型 */ +%union{ + _ast * ast + strs []string + str string +} +/* 定义各个产生式返回的数据类型 */ +%type <_ast> Q +%type K +%type '*' ';' +%token T_SELECT T_FROM T_IDENTIFIER + +%start S +/* + SELECT A, B, C FROM TABLE; + SELECT A, B, C, *; +*/ +%% + +S : { /* void */ } + | Q { setResult(yylex, $1) } + ; +/* SQL 产生式定义 */ +Q : T_SELECT K T_FROM T_IDENTIFIER ';' { + _ast := new_ast(); + _ast.command = T_SELECT; + ast_add_table(_ast, $4); + ast_add_fields(_ast, $2); + $$ = _ast; /* 将生产的 struct ast* 作为返回值返返回上一级产生式, 每个产生式的返回值有数据类型的限制, 具体看用户定义 */ + } + + | T_SELECT K ';' { + _ast := new_ast(); + _ast.command = T_SELECT; + ast_add_fields(_ast, $2); + $$ = _ast; + } + ; + +/* columns 产生式定义 */ +K : '*' { + columns++; + $$ = []string{$1}; + } + | K ',' T_IDENTIFIER { + columns++; + $$ = append($1, $3); + } + | T_IDENTIFIER { + columns++; + $$ = []string{$1}; + } + ; + +%% +``` +tree.go +```go +package sql + +import ( + "errors" + "fmt" + "os" + "strings" +) + +//go:generate go run golang.org/x/tools/cmd/goyacc -l -o parser.go parser.y + +type ast struct { + command int + fileds []string + filed_size int + table string +} +type lex struct { + input []byte + pos int + result *ast + err error +} + +func Parse(sql string) (*ast, error) { + l := &lex{ + input: []byte(sql), + } + _ = yyParse(l) // yyParse 入参需要实现 goyacc lex 接口 + return l.result, l.err +} + +func (l *lex) Lex(lval *yySymType) int { // token 解析入口 + str := l.nextString() + switch strings.ToLower(str) { + case "": + return 0 + case "*", ";", ",": + lval.str = str // 对应 flex 中 yylval = strdup(yytext); 赋值操作, 需要和返回的类型相对于, goyacc 会根据返回的类型去对应的字段获取 + return int(byte(str[0])) // 对应 flex 中 return 返回 token 类型 + case "select": + lval.str = str + return T_SELECT + case "from": + lval.str = str + return T_FROM + default: + lval.str = str + return T_IDENTIFIER + } +} +func (l *lex) nextString() string { + /* trim left */ + for { + if l.pos >= len(l.input) { + return "" + } + switch l.input[l.pos] { + case ' ', '\r', '\n', '\t', '\a': + l.pos++ + default: + goto next + } + } +next: + /* get next string */ + var index int = l.pos + for ; index < len(l.input); index++ { + switch l.input[index] { + case ' ', '\r', '\n', '\t', '\a': + goto next_2 + case '*', ';', ',': + if index == l.pos { + index++ + } + goto next_2 + default: + } + } +next_2: + result := l.input[l.pos:index] + l.pos = index + return string(result) +} + +// Error satisfies yyLexer. +func (l *lex) Error(s string) { + l.err = errors.New(s) +} + +func new_ast() *ast { + return &ast{} +} + +func ast_print(_ast *ast) { + if _ast == nil { + return + } + fmt.Printf("command:%s\n", func() string { + switch _ast.command { + case T_SELECT: + return "select" + default: + return "unknow" + } + }()) + for i := range _ast.fileds { + fmt.Printf("column%d:%s\n", i, _ast.fileds[i]) + } + fmt.Printf("tablename:%s\n", _ast.table) +} + +func ast_add_table(_ast *ast, table string) { + if _ast == nil || _ast.table != "" { + return + } + _ast.table = table +} + +func ast_add_fields(_ast *ast, fileds []string) { + if _ast == nil { + return + } + _ast.fileds = fileds + _ast.filed_size = len(fileds) +} + +func ast_free(_ast *ast) { + /* void */ + return +} + +func exit(i int) { + os.Exit(i) +} + +``` +lex_test.go +```go +package sql + +import ( + "fmt" + "log" + "testing" +) + +func TestLex(t *testing.T) { + sqls := []string{ + `SELECT * FROM TABLE1;`, + `SelecT A,B,C FROM TB;`, + `SELECT A,B,C;`, + } + for _, sql := range sqls { + _ast, err := Parse(sql) + if err != nil { + log.Fatalln(err) + } + ast_print(_ast) + fmt.Println() + } +} + +``` +output: +```shell +go run golang.org/x/tools/cmd/goyacc -l -o parser.go parser.y +go test -v . +=== RUN TestLex +command:select +column0:* +tablename:TABLE1 + +command:select +column0:A +column1:B +column2:C +tablename:TB + +command:select +column0:A +column1:B +column2:C +tablename: + +--- PASS: TestLex (0.00s) +PASS +ok json1/sql 0.006s +``` + +## 类型检查 +在进行类型检查这一步的时候, 整个文件可以粗略分为`变量声明及作用域`以及`表达式`两块内容, 在检查 AST 的过程中遇到一些定义的变量会在当前作用域中查找是否有对应的声明, 如果没找到则顺着作用域向上去父级作用域寻找 + +### 声明及作用域 +作用域可大致分为几类 +1. 全局 +2. 对应函数体内 +3. 块级表达式内 {...} // 单独一对花括号包裹范围内 +```go +// 作用域结构体定义 +type Scope struct { + parent *Scope + children []*Scope + elems map[string]Object // lazily allocated + pos, end token.Pos // scope extent; may be invalid + comment string // for debugging only + isFunc bool // set if this is a function scope (internal use only) +} +func (s *Scope) LookupParent(name string, pos token.Pos) (*Scope, Object) { + for ; s != nil; s = s.parent { + if obj := s.elems[name]; obj != nil && (!pos.IsValid() || obj.scopePos() <= pos) { + return s, obj + } + } + return nil, nil +} +``` +回顾下 `AST file` 的数据结构 +```go +type File struct { + Doc *CommentGroup // associated documentation; or nil + Package token.Pos // position of "package" keyword + Name *Ident // package name + Decls []Decl // top-level declarations; or nil + Scope *Scope // package scope (this file only) + Imports []*ImportSpec // imports in this file + Unresolved []*Ident // unresolved identifiers in this file + Comments []*CommentGroup // list of all comments in the source file +} +``` +再结合之前输出的 [`AST`](#ast_output), 我们可以看到在生成的过程中, 大部分的 `identifiers token` 是能够确认对应类型的, 例如函数声明之类的, 那么对应函数名的 `token` 就可以被成功解析为对应类型的语法树中的一个节点 + +但是依旧存在一些在`AST`初步生成阶段无法被成功解析的, 那么会被存放在`Unresolved`字段中, 就比如上面输出的`int`, 那么这时候就通过向上从父级中依次查找, 如果最终能够找到对应定义, 那么检查成功, 否则则抛出未定义异常 + +例: +```go +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "log" +) + +func main() { + src := ` + + package main + + func main() { + var num1, num2 int + num1 += num2 + _ = num1 + testval++ + return + } + ` + + // Initialize the parser. + fset := token.NewFileSet() // positions are relative to fset + f, err := parser.ParseFile(fset, "", src, parser.AllErrors|parser.ParseComments) + if err != nil { + log.Fatalln(err) + } + pkg, err := new(types.Config).Check("test.go", fset, []*ast.File{f}, nil) + if err != nil { + log.Fatal(err) + } + + _ = pkg +} + +``` +output: +```shell +2021/09/20 15:19:01 9:3: undeclared name: testval +``` +### 表达式检查 +截取之前生成的`AST`中的一小段 +`num1 += num2` +```shell +65 . . . . . 1: *ast.AssignStmt { +66 . . . . . . Lhs: []ast.Expr (len = 1) { +67 . . . . . . . 0: *ast.Ident { +68 . . . . . . . . NamePos: 6:3 +69 . . . . . . . . Name: "num1" +70 . . . . . . . . Obj: *(obj @ 38) +71 . . . . . . . } +72 . . . . . . } +73 . . . . . . TokPos: 6:8 +74 . . . . . . Tok: += +75 . . . . . . Rhs: []ast.Expr (len = 1) { +76 . . . . . . . 0: *ast.Ident { +77 . . . . . . . . NamePos: 6:11 +78 . . . . . . . . Name: "num2" +79 . . . . . . . . Obj: *(obj @ 48) +80 . . . . . . . } +81 . . . . . . } +82 . . . . . } +``` +先看下这个简单的赋值表达式生成的树形结构 +```mermaid +graph TB + +A((op: +=)) +B((exprL: num1)) +C((exprR: num2)) +A-->B +A-->C +``` +对于当前这部分表达式检查, 需要进行的步骤为 +1. 确认当前操作符(+=) +2. 左子树表达式递归, 并确认表达式最终类型 +3. 右子树表达式递归, 并确认表达式最终类型 +4. 左右 expr 类型校验, 如符合当前操作符规则, 成功, 反之失败 + +```go +// The binary expression e may be nil. It's passed in for better error messages only. +func (check *Checker) binary(x *operand, e *ast.BinaryExpr, lhs, rhs ast.Expr, op token.Token) { + var y operand + + check.expr(x, lhs) // 左子树表达式递归 + check.expr(&y, rhs) // 右子树表达式递归 + /* 先判断下特殊的操作类型 */ + if x.mode == invalid { + return + } + if y.mode == invalid { + x.mode = invalid + x.expr = y.expr + return + } + + if isShift(op) { + check.shift(x, &y, e, op) + return + } + + check.convertUntyped(x, y.typ) + if x.mode == invalid { + return + } + check.convertUntyped(&y, x.typ) + if y.mode == invalid { + x.mode = invalid + return + } + + if isComparison(op) { + check.comparison(x, &y, op) + return + } + /* 默认要求 x y 类型一致 */ + if !check.identical(x.typ, y.typ) { // 类型校验 + // only report an error if we have valid types + // (otherwise we had an error reported elsewhere already) + if x.typ != Typ[Invalid] && y.typ != Typ[Invalid] { + check.invalidOp(x.pos(), "mismatched types %s and %s", x.typ, y.typ) + } + x.mode = invalid + return + } + + if !check.op(binaryOpPredicates, x, op) { + x.mode = invalid + return + } + + if op == token.QUO || op == token.REM { + // check for zero divisor + if (x.mode == constant_ || isInteger(x.typ)) && y.mode == constant_ && constant.Sign(y.val) == 0 { + check.invalidOp(y.pos(), "division by zero") + x.mode = invalid + return + } + + // check for divisor underflow in complex division (see issue 20227) + if x.mode == constant_ && y.mode == constant_ && isComplex(x.typ) { + re, im := constant.Real(y.val), constant.Imag(y.val) + re2, im2 := constant.BinaryOp(re, token.MUL, re), constant.BinaryOp(im, token.MUL, im) + if constant.Sign(re2) == 0 && constant.Sign(im2) == 0 { + check.invalidOp(y.pos(), "division by zero") + x.mode = invalid + return + } + } + } + + if x.mode == constant_ && y.mode == constant_ { + xval := x.val + yval := y.val + typ := x.typ.Underlying().(*Basic) + // force integer division of integer operands + if op == token.QUO && isInteger(typ) { + op = token.QUO_ASSIGN + } + x.val = constant.BinaryOp(xval, op, yval) + // Typed constants must be representable in + // their type after each constant operation. + if isTyped(typ) { + if e != nil { + x.expr = e // for better error message + } + check.representable(x, typ) + } + return + } + + x.mode = value + // x.typ is unchanged +} +``` +> 这边以 `go/types` 标准库的类型检查作为案例, 编译器整体流程大同小异 + +以上, 通过`TOKEN`声明以及对应作用域的维护及查找, 再结合各操作符下表达式的递归分析过程, 对于一棵语法树的类型检查就可以进行了 + +## 中间代码生成 +## 机器码生成 +## 参考资料 +[flex & bison](https://pandolia.net/tinyc/index.html) + +[goyacc 入门案例](https://github.com/sougou/parser_tutorial) \ No newline at end of file diff --git a/context.md b/context.md index 4885a38..e767fd9 100644 --- a/context.md +++ b/context.md @@ -566,3 +566,5 @@ func main() { # 总结 ctx 的结构显然是根据代码的执行模型来设计的,虽然设计得比较巧妙,但因为将取消和上下文携带功能混合在一起,在一些情况下还是会给我们埋些比较隐蔽的坑。使用时需要多多注意。 + + diff --git a/defer.md b/defer.md index ae0d8bf..ada7c2b 100644 --- a/defer.md +++ b/defer.md @@ -76,7 +76,7 @@ func newdefer(siz int32) *_defer { ```go type _defer struct { siz int32 // 函数的参数总大小 - started bool // TODO defer 是否已开始执行? + started bool // defer 是否已开始执行 sp uintptr // 存储调用 defer 函数的函数的 sp 寄存器值 pc uintptr // 存储 call deferproc 的下一条汇编指令的指令地址 fn *funcval // 描述函数的变长结构体,包括函数地址及参数 @@ -160,3 +160,6 @@ A: deferproc 和 deferreturn 是成对出现的,对于编译器的实现来说 https://ieevee.com/tech/2017/11/23/go-panic.html + + + diff --git a/futex.md b/futex.md index 812d091..9d51161 100644 --- a/futex.md +++ b/futex.md @@ -235,3 +235,6 @@ http://blog.sina.com.cn/s/blog_e59371cc0102v29b.html https://www.jianshu.com/p/570a61f08e27 https://eli.thegreenplace.net/2018/basics-of-futexes/ + + + diff --git a/gc.md b/gc.md index ff0ec21..b7973ed 100644 --- a/gc.md +++ b/gc.md @@ -742,3 +742,5 @@ gc时间,stw时间和响应延迟之间是什么关系 宏观来看gc划分为多少个阶段 + + diff --git a/gc_write_barrier.md b/gc_write_barrier.md new file mode 100644 index 0000000..d606aff --- /dev/null +++ b/gc_write_barrier.md @@ -0,0 +1,37 @@ +# GC write barrier 详解 + +在垃圾回收领域所讲的 barrier 包括 read barrier 与 write barrier,无论是哪一种,都与并发编程领域的 memory barrier 不是一回事。 + +在 GC 中的 barrier 其本质是 : snippet of code insert before pointer modify。 + +所以在阅读相关材料时,请注意不要将两者混淆。 + +在当前 Go 语言的实现中,GC 只有 write barrier,没有 read barrier。 + +在应用进入 GC 标记阶段前的 stw 阶段,会将全局变量 runtime.writeBarrier.enabled 修改为 true,当应用从 STW 中恢复,重新开始执行,垃圾回收的标记阶段便与应用逻辑开始并发执行,这时所有的堆上指针修改操作在修改之前会额外调用 runtime.gcWriteBarrier: + +![](./images/garbage_collection/barrier_asm.png) + +我们随便找找这些反汇编结果在代码中对应的行: + +![](./images/garbage_collection/barrier_code.png) + +Go 语言当前使用了混合 barrier 来实现 gc 标记期间的被修改对象动态跟踪,早期只使用了 Dijistra 插入 barrier,但 Dijistra barrier 需要在标记完成之后进行栈重扫,因此在 1.8 时修改为混合 barrier。 + +Dijistra 插入屏障伪代码如下: + +![](http://xargin.com/content/images/2021/12/image-42.png) + +堆上指针修改时,新指向的对象要标灰。 + +但是因为 Go 的栈上对象不加 barrier,所以会存在对象丢失的问题: + +![](http://xargin.com/content/images/2021/12/djb.gif) + +还有一种非常有名的 barrier,Yuasa 删除屏障,与 Dijistra 屏障相反,它是在堆指针指向的对象发生变化时,将之前指向的对象标灰: + +![](http://xargin.com/content/images/2021/12/image-43.png) + +和 Dijistra 类似,也存在对象漏标问题: + +![](http://xargin.com/content/images/2021/12/yb.gif) diff --git a/generics.md b/generics.md index caf2ba5..4a36142 100644 --- a/generics.md +++ b/generics.md @@ -40,3 +40,5 @@ cat source.go | genny gen "Something=string" 没有官方的泛型支持,社区怎么搞都是邪道。2021 年 1 月,官方的方案已经基本上成型,并释出了 [draft design](https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md)。 + + diff --git a/goroutine.md b/goroutine.md index 0de6ace..38b020b 100644 --- a/goroutine.md +++ b/goroutine.md @@ -7,4 +7,6 @@ > Written with [StackEdit](https://stackedit.io/). \ No newline at end of file +--> + + diff --git a/images/garbage_collection/barrier_asm.png b/images/garbage_collection/barrier_asm.png new file mode 100644 index 0000000..5012031 Binary files /dev/null and b/images/garbage_collection/barrier_asm.png differ diff --git a/images/garbage_collection/barrier_code.png b/images/garbage_collection/barrier_code.png new file mode 100644 index 0000000..5f971b0 Binary files /dev/null and b/images/garbage_collection/barrier_code.png differ diff --git a/interface.md b/interface.md index 703507d..a365ff4 100644 --- a/interface.md +++ b/interface.md @@ -543,3 +543,5 @@ go.itab.*os.File,io.Writer SRODATA dupok size=32 // 下面就是正常流程了 ``` + + diff --git a/io.md b/io.md index 2bbac62..c15b884 100644 --- a/io.md +++ b/io.md @@ -193,4 +193,534 @@ vmtouch /disk/data/tmp/test - https://tech.meituan.com/2017/05/19/about-desk-io.html - https://github.com/ncw/directio # DMA -# Zero-Copy \ No newline at end of file +# Zero-Copy + +## splice +在介绍 splice 及其在 golang 中的应用之前, 先从一段简单的网络代理代码开始入手 +#### read & write +```go +var ( + p sync.Pool +) + +func init() { + p.New = func() interface{} { + return make([]byte, DEFAULTSIZE) + } +} +// src 客户端 tcp 链接 +// dst mock server tcp 链接 +func normal(src, dst net.Conn) { + var bts []byte = p.Get().([]byte) + var bts2 []byte = p.Get().([]byte) + defer p.Put(bts) + defer p.Put(bts2) + // mock server to client + go func() { + for { + num, err := dst.Read(bts2[:]) + if err != nil { + break + } + var num_write int + for num > 0 { + num_write, err = src.Write(bts2[num_write:num]) + if err != nil { + return + } + num -= num_write + } + } + }() + // client to mock server + for { + num, err := src.Read(bts[:]) + if err != nil { + break + } + var num_write int + for num > 0 { + num_write, err = dst.Write(bts[num_write:num]) + if err != nil { + return + } + num -= num_write + } + } +} +``` +以上片段实现了一个简单的功能: 将客户端请求的 tcp 数据通过`read`系统调用读出放入本地用户空间 缓存, 而后再调用`write`发送给目标服务器,反之亦然 + +整个过程如下图所示(暂不考虑 IO 模型调度以及 DMA 等细节部分) + +```shell +[ user space ] + + -------------------------------------------- + | application | + -------------------------------------------- + ····················|·································|·················· + | read() | write() +[ kernel space ] | | + ----------------- ----------------- + | socket buffer | | socket buffer | + ----------------- ----------------- + | copy | + ····················|·································|·················· +[ hardware sapce ] | | + ------------------------------------------------------ + | network interface | + ------------------------------------------------------ + +``` +对于透传或者部分透传(例如七层头部解析后透明代理请求体)之类的需求场景来说, 这种流程的成本无疑是很高的, 可以总结下涉及的几个浪费点 + +- 数据需要从内核态拷贝到用户态 +- 应用层在 read 及 write 的过程中对这部分 byte 的操作开销(申请、释放、对象池维护等) + +#### splice 介绍 +```c +/* + splice() moves data between two file descriptors without copying + between kernel address space and user address space. It + transfers up to len bytes of data from the file descriptor fd_in + to the file descriptor fd_out, where one of the file descriptors + must refer to a pipe. +*/ +ssize_t splice(int fd_in, off64_t *off_in, int fd_out, + off64_t *off_out, size_t len, unsigned int flags); +``` +一句话概括就是, `splice` 不需要从内核空间复制这部分数据到用户空间就可以支持将数据从两个文件描述符之间进行转移, 不过两个描述符至少得有一个是 `pipe`, 以下列举如何利用`splice`完成 `socket->socket` 的数据代理 + +example: +```go +func example(src, dst net.Conn) { + // 由于 src 及 dst 都是 socket, 没法直接使用 splice, 因此先创建临时 pipe + const flags = syscall.O_CLOEXEC | syscall.O_NONBLOCK + var fds [2]int // readfd, writefd + if err := syscall.Pipe2(fds[:], flags); err != nil { + panic(err) + } + // 使用完后关闭 pipe + defer syscall.Close(fds[0]) + defer syscall.Close(fds[1]) + // 获取 src fd + srcfile, err := src.(*net.TCPConn).File() + if err != nil { + panic(err) + } + srcfd := int(srcfile.Fd()) + syscall.SetNonblock(srcfd, true) + ... + // 从 srcfd 读出, 写入 fds[1] (pipe write fd) + num, err := syscall.Splice(srcfd, nil, fds[1], nil, DEFAULTSIZE/* size to splice */, SPLICE_F_NONBLOCK) + ... +} +``` +此时的调用过程变为: + +```shell +[ user space ] + + ----------------------------------------------------------------------------------------------- + | application | + ----------------------------------------------------------------------------------------------- + ········································|····················|·····················|·································· + | splice() | pipe() | splice() +[ kernel space ] | | | + ----------------- ”copy“ ----------------------- ”copy“ ----------------- + | socket buffer |· · · · · · · · · >| pipe writefd & readfd |· · · · · · · · >| socket buffer | + ----------------- ----------------------- ----------------- + | copy | + ····················|··············································································| +[ hardware sapce ] | | + ----------------------------------------------------------------------------------------------- + | network interface | + ----------------------------------------------------------------------------------------------- +``` +此时产生的系统调用为 +- 首先 `pipe()` 调用, 创建临时管道 +- 调用 `splice()` 将 `srcfd` 数据 ”拷贝“ 到 `pipe` +- 调用 `splice()` 将 `pipe` 中的数据 ”拷贝“ 到 `dstfd` + +可以注意到图中以及总结部分的”拷贝“给加了引号, 具体了解过`pipe`底层实现的小伙伴应该理解, 在这边简单表述下, `splice` 是基于 `pipe buffer` 实现的, 本质上在数据传输的时候并没有进行数据的拷贝, 而是仅仅将数据的内存地址等信息塞进了`pipe_buffer`中对应的字段 + +至此, 完成了 kernel-user space 的拷贝优化, 不过可能细心的人会发现, 这种方式虽然减少了数据的拷贝, 但是同样额外增加了系统调用(create pipe & close pipe), 接下来就关于这部分的取舍与具体场景进行具体讨论 + +#### splice 还是 read & write? +如何取舍使用哪种方式? + +两种方法各有各的好处, 往往选择层面的考虑在于应用层的具体策略, 如是否进行透传(/部分), 饥饿问题, 对象池策略等等 + +下面提供几种场景下的测试以供参考 +benchmark 代码: +```go +/* + * 测试环境: go 1.14.3 centos7 + */ +package main + +import ( + "bytes" + "io" + "net" + "net/http" + "sync" + "sync/atomic" + "testing" + "time" +) + +var ( + p sync.Pool +) + +func init() { + p.New = func() interface{} { + return make([]byte, DEFAULTSIZE) + } +} + +const ( + // mock http 请求体大小 + REQUESTBYTESIZE = 0 + // 应用层对象池 byte 大小 + DEFAULTSIZE = 1 << 10 + + SPLICE_F_MOVE = 0x1 + SPLICE_F_NONBLOCK = 0x2 + SPLICE_F_MORE = 0x4 + SPLICE_F_GIFT = 0x8 +) +// io.Copy 该场景下内部调用 splice syscall, 感兴趣的自行查看源码 +func gosplice(src, dst net.Conn) { + defer src.Close() + defer dst.Close() + go func() { + io.Copy(src, dst) + }() + io.Copy(dst, src) +} + +func normal(src, dst net.Conn) { + defer src.Close() + defer dst.Close() + var bts []byte = p.Get().([]byte) + var bts2 []byte = p.Get().([]byte) + defer p.Put(bts) + defer p.Put(bts2) + go func() { + for { + num, err := dst.Read(bts2[:]) + if err != nil { + break + } + var num_write int + for num > 0 { + num_write, err = src.Write(bts2[num_write:num]) + num -= num_write + if err != nil { + return + } + } + } + }() + // local to mock serve + for { + num, err := src.Read(bts[:]) + if err != nil { + break + } + var num_write int + for num > 0 { + num_write, err = dst.Write(bts[num_write:num]) + num -= num_write + if err != nil { + return + } + } + } +} + +// Server http server +var Server *http.Server + +type s struct{} + +func (ss s) ServeHTTP(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(200) + return +} +func TestMain(m *testing.M) { + // mock tcp server + var ss s + go func() { + Server = &http.Server{ + Addr: "0.0.0.0:9610", + Handler: ss, + WriteTimeout: 60 * time.Second, + ReadTimeout: 60 * time.Second, + } + err := Server.ListenAndServe() + if err != nil { + panic(err) + } + }() + go func() { // normal 9611 + l, err := net.ListenTCP("tcp4", &net.TCPAddr{ + IP: net.ParseIP("0.0.0.0"), + Port: 9611, + }) + if err != nil { + panic(err) + } + for { + n, err := l.Accept() + if err != nil { + continue + } + remote, err := net.DialTCP("tcp4", &net.TCPAddr{ + IP: net.ParseIP("0.0.0.0"), Port: 0, + }, &net.TCPAddr{ + IP: net.ParseIP("0.0.0.0"), Port: 9610, + }) + if err != nil { + continue + } + go normal(n, remote) + } + }() + go func() { // gosplice 9612 + l, err := net.ListenTCP("tcp4", &net.TCPAddr{ + IP: net.ParseIP("0.0.0.0"), + Port: 9612, + }) + if err != nil { + panic(err) + } + for { + n, err := l.Accept() + if err != nil { + continue + } + remote, err := net.DialTCP("tcp4", &net.TCPAddr{ + IP: net.ParseIP("0.0.0.0"), Port: 0, + }, &net.TCPAddr{ + IP: net.ParseIP("0.0.0.0"), Port: 9610, + }) + if err != nil { + continue + } + go gosplice(n, remote) + } + }() + m.Run() +} +func BenchmarkNormalReadWrite(b *testing.B) { + // normal 9611 + c := http.Client{ + Timeout: time.Minute, + } + var total, success uint32 + b.ResetTimer() + for i := 0; i < b.N; i++ { + atomic.AddUint32(&total, 1) + req, err := http.NewRequest("POST", "http://0.0.0.0:9611", bytes.NewReader(make([]byte, REQUESTBYTESIZE))) + if err != nil { + b.Fatalf("%s", err.Error()) + } + res, err := c.Do(req) + if err == nil && res.StatusCode == 200 { + atomic.AddUint32(&success, 1) + } + c.CloseIdleConnections() + } + b.Logf("test:%s,total: %d,rate: %.2f%%\n", b.Name(), total, float64(success*100/total)) +} + +func BenchmarkGoSplice(b *testing.B) { + // normal 9612 + c := http.Client{ + Timeout: time.Minute, + } + var total, success uint32 + b.ResetTimer() + for i := 0; i < b.N; i++ { + atomic.AddUint32(&total, 1) + req, err := http.NewRequest("POST", "http://0.0.0.0:9612", bytes.NewReader(make([]byte, REQUESTBYTESIZE))) + if err != nil { + b.Fatalf("%s", err.Error()) + } + res, err := c.Do(req) + if err == nil && res.StatusCode == 200 { + atomic.AddUint32(&success, 1) + } + c.CloseIdleConnections() + } + b.Logf("test:%s, total: %d, success rate: %.2f%%\n", b.Name(), total, float64(success*100/total)) +} +``` +- 场景一: 单次请求数据量较少, 应用维护单个 buffer 较小 +```go +REQUESTBYTESIZE = 0 // http request body +DEFAULTSIZE = 1 << 10 // buffer size 1kb +``` +```shell +RRunning tool: /usr/local/bin/go test -benchmem -run=^$ -bench ^(BenchmarkNormalReadWrite|BenchmarkGoSplice)$ barrier/t + +goos: linux +goarch: amd64 +pkg: barrier/t +BenchmarkNormalReadWrite-4 6348 179699 ns/op 4847 B/op 62 allocs/op +--- BENCH: BenchmarkNormalReadWrite-4 + test_test.go:173: test:BenchmarkNormalReadWrite,total: 1,rate: 100.00% + test_test.go:173: test:BenchmarkNormalReadWrite,total: 100,rate: 100.00% + test_test.go:173: test:BenchmarkNormalReadWrite,total: 6348,rate: 100.00% +BenchmarkGoSplice-4 6652 179622 ns/op 4852 B/op 62 allocs/op +--- BENCH: BenchmarkGoSplice-4 + test_test.go:194: test:BenchmarkGoSplice, total: 1, success rate: 100.00% + test_test.go:194: test:BenchmarkGoSplice, total: 100, success rate: 100.00% + test_test.go:194: test:BenchmarkGoSplice, total: 6652, success rate: 100.00% +PASS +ok barrier/t 2.391s +``` +两种方式无明显性能差异 +- 场景二: 单次请求数据量多, 应用维护单个 buffer 较小 +```go +REQUESTBYTESIZE = 1 << 20 // 1 MB +DEFAULTSIZE = 1 << 10 // buffer size 1kb +``` +```shell +Running tool: /usr/local/bin/go test -benchmem -run=^$ -bench ^(BenchmarkNormalReadWrite|BenchmarkGoSplice)$ barrier/t + +goos: linux +goarch: amd64 +pkg: barrier/t +BenchmarkNormalReadWrite-4 465 2329209 ns/op 1073956 B/op 163 allocs/op +--- BENCH: BenchmarkNormalReadWrite-4 + test_test.go:173: test:BenchmarkNormalReadWrite,total: 1,rate: 100.00% + test_test.go:173: test:BenchmarkNormalReadWrite,total: 100,rate: 100.00% + test_test.go:173: test:BenchmarkNormalReadWrite,total: 376,rate: 100.00% + test_test.go:173: test:BenchmarkNormalReadWrite,total: 465,rate: 100.00% +BenchmarkGoSplice-4 963 1555386 ns/op 1070506 B/op 157 allocs/op +--- BENCH: BenchmarkGoSplice-4 + test_test.go:194: test:BenchmarkGoSplice, total: 1, success rate: 100.00% + test_test.go:194: test:BenchmarkGoSplice, total: 100, success rate: 100.00% + test_test.go:194: test:BenchmarkGoSplice, total: 963, success rate: 100.00% +PASS +ok barrier/t 4.056s +``` +当链接需要处理的数据量较多而应用层每次处理的 buffer 相比起来较小, 以至于需要 read & write 的次数更多的时候, 差异就会比较明显 + +#### go 中的 splice +在上面的介绍过程中简单说了下 `io.Copy` 在 `socket` 之间操作的时候, 当机器架构支持的时候会采取 `splice`, 接下来就此进行详细分析来介绍下 `runtime` 在 `splice` 上的一些决策以及当前`runtime`在 `splice` 上的一些不足 +```go +/* + * src/net/spice_linux.go + */ +// splice transfers data from r to c using the splice system call to minimize +// copies from and to userspace. c must be a TCP connection. Currently, splice +// is only enabled if r is a TCP or a stream-oriented Unix connection. +// +// If splice returns handled == false, it has performed no work. +func splice(c *netFD, r io.Reader) (written int64, err error, handled bool) { + /* + * 因为前面介绍过 splice 是通过 pipe buffer 实现的 + * 在调用的时候 kernel无需进行数据拷贝, 仅操作数据原信息(基础字段的指针等) + * 所以这边默认 splice 的 len 开得比较大, 读到 EOF 为止 + */ + var remain int64 = 1 << 62 // by default, copy until EOF + lr, ok := r.(*io.LimitedReader) + if ok { + remain, r = lr.N, lr.R + if remain <= 0 { + return 0, nil, true + } + } + + var s *netFD + if tc, ok := r.(*TCPConn); ok { + s = tc.fd + } else if uc, ok := r.(*UnixConn); ok { + if uc.fd.net != "unix" { + return 0, nil, false + } + s = uc.fd + } else { + return 0, nil, false + } + + written, handled, sc, err := poll.Splice(&c.pfd, &s.pfd, remain) + if lr != nil { + lr.N -= written + } + return written, wrapSyscallError(sc, err), handled +} +``` +```go +/* + * go 1.14.3 + * src/internal/poll/splice_linux.go + */ +// Splice transfers at most remain bytes of data from src to dst, using the +// splice system call to minimize copies of data from and to userspace. +// +// Splice creates a temporary pipe, to serve as a buffer for the data transfer. +// src and dst must both be stream-oriented sockets. +// +// If err != nil, sc is the system call which caused the error. +func Splice(dst, src *FD, remain int64) (written int64, handled bool, sc string, err error) { + // dst 以及 src 均为 socket.fd, 因此若想使用 splice 则需要借助 pipe + // 创建临时 pipe + prfd, pwfd, sc, err := newTempPipe() + if err != nil { + return 0, false, sc, err + } + defer destroyTempPipe(prfd, pwfd) + var inPipe, n int + for err == nil && remain > 0 { + max := maxSpliceSize + if int64(max) > remain { + max = int(remain) + } + // spliceDrain 调用 splice syscall + inPipe, err = spliceDrain(pwfd, src, max) + // The operation is considered handled if splice returns no + // error, or an error other than EINVAL. An EINVAL means the + // kernel does not support splice for the socket type of src. + // The failed syscall does not consume any data so it is safe + // to fall back to a generic copy. + // + // spliceDrain should never return EAGAIN, so if err != nil, + // Splice cannot continue. + // + // If inPipe == 0 && err == nil, src is at EOF, and the + // transfer is complete. + handled = handled || (err != syscall.EINVAL) + if err != nil || (inPipe == 0 && err == nil) { + break + } + // splicePump 调用 splice syscall + n, err = splicePump(dst, prfd, inPipe) + if n > 0 { + written += int64(n) + remain -= int64(n) + } + } + if err != nil { + return written, handled, "splice", err + } + return written, true, "", nil +} +``` + +相信上面简短的分析大家也可以看到, 每次在进行 `splice` 的时候都会利用临时 `pipe`, 频繁的创建、销毁, 用户态-内核态的切换会带来非常多不必要的开销, 当前社区内也有关于 `splice temp-pipe` 生命周期的[讨论](https://go-review.googlesource.com/c/go/+/271537/)。 + +再者, 因为当前关联到 `socket` 的 `splice` 实现在 `runtime` 层面和内置 `io 模型(epoll 等)`高度耦合, 基本无法解耦单独应用, 而如果想自己来实现 `splice(syscall.Splice)` 的话则不得不顺带在用户层面实现自己的`io 模型`再来使用, 会比较繁琐(上面测试用例使用内置 `splice api` 也是因为这个原因) + +## 参考资料 + +- https://go-review.googlesource.com/c/go/+/271537/ +- https://zhuanlan.zhihu.com/p/308054212 + + diff --git a/lockfree.md b/lockfree.md index f9c19d1..261e9cb 100644 --- a/lockfree.md +++ b/lockfree.md @@ -1,4 +1,6 @@ # lock free programming in Go # 参考资料 -https://docs.google.com/presentation/d/1wuNNW-g6v8qizIc_IxAGZTj-49TODKF0TYddTA1VDUo/mobilepresent?slide=id.p \ No newline at end of file +https://docs.google.com/presentation/d/1wuNNW-g6v8qizIc_IxAGZTj-49TODKF0TYddTA1VDUo/mobilepresent?slide=id.p + + diff --git a/map.md b/map.md index 123cfd8..351146d 100644 --- a/map.md +++ b/map.md @@ -1267,3 +1267,5 @@ func (h *hmap) incrnoverflow() { } } ``` + + diff --git a/memory.md b/memory.md index d71141a..b937fcf 100644 --- a/memory.md +++ b/memory.md @@ -1396,3 +1396,6 @@ func (p *notInHeap) add(bytes uintptr) *notInHeap { ### 堆外内存用法 嗯,堆外内存只是 runtime 自己玩的东西,用户态是使用不了的,属于 runtime 专用的 directive。 + + + diff --git a/memory_barrier.md b/memory_barrier.md index 94383c0..cc1a7ce 100644 --- a/memory_barrier.md +++ b/memory_barrier.md @@ -126,7 +126,7 @@ mesi 协议解决了多核环境下,内存多层级带来的问题。使得 ca ## CPU 导致乱序 -使用 litmus 进行形式化验证: +使用 litmus 进行验证: ``` cat sb.litmus @@ -181,6 +181,56 @@ Time SB 0.11 在两个核心上运行汇编指令,意料之外的情况 100w 次中出现了 96 次。虽然很少,但确实是客观存在的情况。 +有文档提到,x86 体系的内存序本身比较严格,除了 store-load 以外不存在其它类型的重排,也可以用下列脚本验证: + +``` +X86 RW +{ x=0; y=0; } + P0 | P1 ; + MOV EAX,[y] | MOV EAX,[x] ; + MOV [x],$1 | MOV [y],$1 ; +locations [x;y;] +exists (0:EAX=1 /\ 1:EAX=1) +``` + +``` +%%%%%%%%%%%%%%%%%%%%%%%%% +% Results for sb.litmus % +%%%%%%%%%%%%%%%%%%%%%%%%% +X86 OOO + +{x=0; y=0;} + + P0 | P1 ; + MOV EAX,[y] | MOV EAX,[x] ; + MOV [x],$1 | MOV [y],$1 ; + +locations [x; y;] +exists (0:EAX=1 /\ 1:EAX=1) +Generated assembler + ##START _litmus_P0 + movl -4(%rsi,%rcx,4), %eax + movl $1, -4(%rbx,%rcx,4) + ##START _litmus_P1 + movl -4(%rbx,%rcx,4), %eax + movl $1, -4(%rsi,%rcx,4) + +Test OOO Allowed +Histogram (2 states) +500000:>0:EAX=1; 1:EAX=0; x=1; y=1; +500000:>0:EAX=0; 1:EAX=1; x=1; y=1; +No + +Witnesses +Positive: 0, Negative: 1000000 +Condition exists (0:EAX=1 /\ 1:EAX=1) is NOT validated +Hash=7cdd62e8647b817c1615cf8eb9d2117b +Observation OOO Never 0 1000000 +Time OOO 0.14 +``` + +无论运行多少次,Positive 应该都是 0。 + ## barrier 从功能上来讲,barrier 有四种: @@ -519,3 +569,5 @@ https://stackoverflow.com/questions/29880015/lock-prefix-vs-mesi-protocol https://github.com/torvalds/linux/blob/master/Documentation/memory-barriers.txt http://www.overbyte.com.au/misc/Lesson3/CacheFun.html + + diff --git a/netpoll.md b/netpoll.md index 942adad..7ab59f9 100644 --- a/netpoll.md +++ b/netpoll.md @@ -1355,3 +1355,5 @@ func poll_runtime_pollUnblock(pd *pollDesc) { } } ``` + + diff --git a/panic.md b/panic.md index 7765ec1..e34b3ad 100644 --- a/panic.md +++ b/panic.md @@ -295,4 +295,6 @@ func main() { defer panic(2) panic(1) } -``` \ No newline at end of file +``` + + diff --git a/pprof.md b/pprof.md new file mode 100644 index 0000000..f825a1a --- /dev/null +++ b/pprof.md @@ -0,0 +1,778 @@ +# pprof +> 本章节没有介绍具体 pprof 以及周边工具的使用, 而是进行了 runtime pprof 实现原理的分析, 旨在提供给读者一个使用方面的参考 +在进行深入本章节之前, 让我们来看三个问题, 相信下面这几个问题也是大部分人在使用 pprof 的时候对它最大的困惑, 那么可以带着这三个问题来进行接下去的分析 +- 开启 pprof 会对 runtime 产生多大的压力? +- 能否选择性在合适阶段对生产环境的应用进行 pprof 的开启 / 关闭操作? +- pprof 的原理是什么? + +go 内置的 `pprof API` 在 `runtime/pprof` 包内, 它提供给了用户与 `runtime` 交互的能力, 让我们能够在应用运行的过程中分析当前应用的各项指标来辅助进行性能优化以及问题排查, 当然也可以直接加载 `_ "net/http/pprof"` 包使用内置的 `http 接口` 来进行使用, `net` 模块内的 pprof 即为 go 替我们封装好的一系列调用 `runtime/pprof` 的方法, 当然也可以自己直接使用 +```go +// src/runtime/pprof/pprof.go +// 可观察类目 +profiles.m = map[string]*Profile{ + "goroutine": goroutineProfile, + "threadcreate": threadcreateProfile, + "heap": heapProfile, + "allocs": allocsProfile, + "block": blockProfile, + "mutex": mutexProfile, + } +``` + +## allocs +```go + +var allocsProfile = &Profile{ + name: "allocs", + count: countHeap, // identical to heap profile + write: writeAlloc, +} +``` +- writeAlloc (主要涉及以下几个 api) + - ReadMemStats(m *MemStats) + - MemProfile(p []MemProfileRecord, inuseZero bool) + +```go +// ReadMemStats populates m with memory allocator statistics. +// +// The returned memory allocator statistics are up to date as of the +// call to ReadMemStats. This is in contrast with a heap profile, +// which is a snapshot as of the most recently completed garbage +// collection cycle. +func ReadMemStats(m *MemStats) { + // STW 操作 + stopTheWorld("read mem stats") + // systemstack 切换 + systemstack(func() { + // 将 memstats 通过 copy 操作复制给 m + readmemstats_m(m) + }) + + startTheWorld() +} +``` + +```go +// MemProfile returns a profile of memory allocated and freed per allocation +// site. +// +// MemProfile returns n, the number of records in the current memory profile. +// If len(p) >= n, MemProfile copies the profile into p and returns n, true. +// If len(p) < n, MemProfile does not change p and returns n, false. +// +// If inuseZero is true, the profile includes allocation records +// where r.AllocBytes > 0 but r.AllocBytes == r.FreeBytes. +// These are sites where memory was allocated, but it has all +// been released back to the runtime. +// +// The returned profile may be up to two garbage collection cycles old. +// This is to avoid skewing the profile toward allocations; because +// allocations happen in real time but frees are delayed until the garbage +// collector performs sweeping, the profile only accounts for allocations +// that have had a chance to be freed by the garbage collector. +// +// Most clients should use the runtime/pprof package or +// the testing package's -test.memprofile flag instead +// of calling MemProfile directly. +func MemProfile(p []MemProfileRecord, inuseZero bool) (n int, ok bool) { + lock(&proflock) + // If we're between mProf_NextCycle and mProf_Flush, take care + // of flushing to the active profile so we only have to look + // at the active profile below. + mProf_FlushLocked() + clear := true + /* + * 记住这个 mbuckets -- memory profile buckets + * allocs 的采样都是记录在这个全局变量内, 下面会进行详细分析 + * ------------------------------------------------- + * (gdb) info variables mbuckets + * All variables matching regular expression "mbuckets": + + * File runtime: + * runtime.bucket *runtime.mbuckets; + * (gdb) + */ + for b := mbuckets; b != nil; b = b.allnext { + mp := b.mp() + if inuseZero || mp.active.alloc_bytes != mp.active.free_bytes { + n++ + } + if mp.active.allocs != 0 || mp.active.frees != 0 { + clear = false + } + } + if clear { + // Absolutely no data, suggesting that a garbage collection + // has not yet happened. In order to allow profiling when + // garbage collection is disabled from the beginning of execution, + // accumulate all of the cycles, and recount buckets. + n = 0 + for b := mbuckets; b != nil; b = b.allnext { + mp := b.mp() + for c := range mp.future { + mp.active.add(&mp.future[c]) + mp.future[c] = memRecordCycle{} + } + if inuseZero || mp.active.alloc_bytes != mp.active.free_bytes { + n++ + } + } + } + if n <= len(p) { + ok = true + idx := 0 + for b := mbuckets; b != nil; b = b.allnext { + mp := b.mp() + if inuseZero || mp.active.alloc_bytes != mp.active.free_bytes { + // mbuckets 数据拷贝 + record(&p[idx], b) + idx++ + } + } + } + unlock(&proflock) + return +} +``` + +总结一下 `pprof/allocs` 所涉及的操作 +- 短暂的 `STW` 以及 `systemstack` 切换来获取 `runtime` 相关信息 +- 拷贝全局对象 `mbuckets` 值返回给用户 + +### mbuckets +上文提到, `pprof/allocs` 的核心在于对 `mbuckets` 的操作, 下面用一张图来简单描述下 `mbuckets` 的相关操作 +```go +var mbuckets *bucket // memory profile buckets +type bucket struct { + next *bucket + allnext *bucket + typ bucketType // memBucket or blockBucket (includes mutexProfile) + hash uintptr + size uintptr + nstk uintptr +} +``` + + +```shell + --------------- + | user access | + --------------- + | + ------------------ | +| mbuckets list | copy | +| (global) | ------------------------------------- + ------------------ + | + | + | create_or_get && insert_or_update bucket into mbuckets + | + | + -------------------------------------- +| func stkbucket & typ == memProfile | + -------------------------------------- + | + ---------------- + | mProf_Malloc | // 堆栈等信息记录 + ---------------- + | + ---------------- + | profilealloc | // next_sample 计算 + ---------------- + | + | /* + | * if rate := MemProfileRate; rate > 0 { + | * if rate != 1 && size < c.next_sample { + | * c.next_sample -= size + | 采样 * } else { + | 记录 * mp := acquirem() + | * profilealloc(mp, x, size) + | * releasem(mp) + | * } + | * } + | */ + | + ------------ 不采样 + | mallocgc |-----------... + ------------ +``` + +由上图我们可以清晰的看见, `runtime` 在内存分配的时候会根据一定策略进行采样, 记录到 `mbuckets` 中让用户得以进行分析, 而采样算法有个重要的依赖 `MemProfileRate` + +```go +// MemProfileRate controls the fraction of memory allocations +// that are recorded and reported in the memory profile. +// The profiler aims to sample an average of +// one allocation per MemProfileRate bytes allocated. +// +// To include every allocated block in the profile, set MemProfileRate to 1. +// To turn off profiling entirely, set MemProfileRate to 0. +// +// The tools that process the memory profiles assume that the +// profile rate is constant across the lifetime of the program +// and equal to the current value. Programs that change the +// memory profiling rate should do so just once, as early as +// possible in the execution of the program (for example, +// at the beginning of main). +var MemProfileRate int = 512 * 1024 +``` +默认大小是 512 KB, 可以由用户自行配置. + +值的注意的是, 由于开启了 pprof 会产生一些采样的额外压力及开销, go 团队已经在较新的编译器中有选择地进行了这个变量的配置以[改变](https://go-review.googlesource.com/c/go/+/299671/8/src/runtime/mprof.go)默认开启的现状 + +具体方式为代码未进行相关引用则编译器将初始值配置为 0, 否则则为默认(512 KB) + +(本文讨论的基于 1.14.3 版本, 如有差异请进行版本确认) + +#### pprof/allocs 总结 +- 开启后会对 runtime 产生额外压力, 采样时会在 `runtime malloc` 时记录额外信息以供后续分析 +- 可以人为选择是否开启, 以及采样频率, 通过设置 `runtime.MemProfileRate` 参数, 不同 go 版本存在差异(是否默认开启), 与用户代码内是否引用(linker)相关模块/变量有关, 默认大小为 512 KB + +`allocs` 部分还包含了 `heap` 情况的近似计算, 放在下一节分析 +## heap +>allocs: A sampling of all past memory allocations + +>heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample. + +对比下 `allocs` 和 `heap` 官方说明上的区别, 一个是分析所有内存分配的情况, 一个是当前 `heap` 上的分配情况. `heap` 还能使用额外参数运行一次 `GC` 后再进行分析 + +看起来两者差别很大。。。不过实质上在代码层面两者除了一次 `GC` 可以人为调用以及生成的文件类型不同之外 (debug == 0 的时候) 之外没啥区别. + +### heap 采样(伪) +```go +// p 为上文提到过的 MemProfileRecord 采样记录 +for _, r := range p { + hideRuntime := true + for tries := 0; tries < 2; tries++ { + stk := r.Stack() + // For heap profiles, all stack + // addresses are return PCs, which is + // what appendLocsForStack expects. + if hideRuntime { + for i, addr := range stk { + if f := runtime.FuncForPC(addr); f != nil && strings.HasPrefix(f.Name(), "runtime.") { + continue + } + // Found non-runtime. Show any runtime uses above it. + stk = stk[i:] + break + } + } + locs = b.appendLocsForStack(locs[:0], stk) + if len(locs) > 0 { + break + } + hideRuntime = false // try again, and show all frames next time. + } + // rate 即为 runtime.MemProfileRate + values[0], values[1] = scaleHeapSample(r.AllocObjects, r.AllocBytes, rate) + values[2], values[3] = scaleHeapSample(r.InUseObjects(), r.InUseBytes(), rate) + var blockSize int64 + if r.AllocObjects > 0 { + blockSize = r.AllocBytes / r.AllocObjects + } + b.pbSample(values, locs, func() { + if blockSize != 0 { + b.pbLabel(tagSample_Label, "bytes", "", blockSize) + } + }) + } +``` +```go +// scaleHeapSample adjusts the data from a heap Sample to +// account for its probability of appearing in the collected +// data. heap profiles are a sampling of the memory allocations +// requests in a program. We estimate the unsampled value by dividing +// each collected sample by its probability of appearing in the +// profile. heap profiles rely on a poisson process to determine +// which samples to collect, based on the desired average collection +// rate R. The probability of a sample of size S to appear in that +// profile is 1-exp(-S/R). +func scaleHeapSample(count, size, rate int64) (int64, int64) { + if count == 0 || size == 0 { + return 0, 0 + } + + if rate <= 1 { + // if rate==1 all samples were collected so no adjustment is needed. + // if rate<1 treat as unknown and skip scaling. + return count, size + } + + avgSize := float64(size) / float64(count) + scale := 1 / (1 - math.Exp(-avgSize/float64(rate))) + + return int64(float64(count) * scale), int64(float64(size) * scale) +} +``` + +为什么要在标题里加个伪? 看上面代码片段也可以注意到, 实质上在 `pprof` 分析的时候并没有扫描所有堆上内存进行分析 (想想也不现实) , 而是通过之前采样出的数据, 进行计算 (现有对象数量, 大小, 采样率等) 来估算出 `heap` 上的情况, 当然给我们参考一般来说是足够了 + +## goroutine +- debug >= 2 的情况, 直接进行堆栈输出, 详情可以查看 [stack](runtime_stack.md) 章节 + +```go +// fetch == runtime.GoroutineProfile +func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runtime.StackRecord) (int, bool)) error { + // Find out how many records there are (fetch(nil)), + // allocate that many records, and get the data. + // There's a race—more records might be added between + // the two calls—so allocate a few extra records for safety + // and also try again if we're very unlucky. + // The loop should only execute one iteration in the common case. + var p []runtime.StackRecord + n, ok := fetch(nil) + for { + // Allocate room for a slightly bigger profile, + // in case a few more entries have been added + // since the call to ThreadProfile. + p = make([]runtime.StackRecord, n+10) + n, ok = fetch(p) + if ok { + p = p[0:n] + break + } + // Profile grew; try again. + } + + return printCountProfile(w, debug, name, runtimeProfile(p)) +} +``` + +```go +// GoroutineProfile returns n, the number of records in the active goroutine stack profile. +// If len(p) >= n, GoroutineProfile copies the profile into p and returns n, true. +// If len(p) < n, GoroutineProfile does not change p and returns n, false. +// +// Most clients should use the runtime/pprof package instead +// of calling GoroutineProfile directly. +func GoroutineProfile(p []StackRecord) (n int, ok bool) { + gp := getg() + + isOK := func(gp1 *g) bool { + // Checking isSystemGoroutine here makes GoroutineProfile + // consistent with both NumGoroutine and Stack. + return gp1 != gp && readgstatus(gp1) != _Gdead && !isSystemGoroutine(gp1, false) + } + // 熟悉的味道, STW 又来了 + stopTheWorld("profile") + // 统计有多少 goroutine + n = 1 + for _, gp1 := range allgs { + if isOK(gp1) { + n++ + } + } + // 当传入的 p 非空的时候, 开始获取各个 goroutine 信息, 整体姿势和 stack api 几乎一模一样 + if n <= len(p) { + ok = true + r := p + + // Save current goroutine. + sp := getcallersp() + pc := getcallerpc() + systemstack(func() { + saveg(pc, sp, gp, &r[0]) + }) + r = r[1:] + + // Save other goroutines. + for _, gp1 := range allgs { + if isOK(gp1) { + if len(r) == 0 { + // Should be impossible, but better to return a + // truncated profile than to crash the entire process. + break + } + saveg(^uintptr(0), ^uintptr(0), gp1, &r[0]) + r = r[1:] + } + } + } + + startTheWorld() + + return n, ok +} +``` +总结下 `pprof/goroutine` +- STW 操作, 如果需要观察详情的需要注意这个 API 带来的风险 +- 整体流程基本就是 stackdump 所有协程信息的流程, 差别不大没什么好讲的, 不熟悉的可以去看下 stack 对应章节 + +## pprof/threadcreate +可能会有人想问, 我们通常只关注 `goroutine` 就够了, 为什么还需要对线程的一些情况进行追踪? 例如无法被抢占的阻塞性[系统调用](syscall.md), `cgo` 相关的线程等等, 都可以利用它来进行一个简单的分析, 当然大多数情况考虑的线程问题(诸如泄露等), 一般都是上层的使用问题所导致的(线程泄露等) +```go +// 还是用之前用过的无法被抢占的阻塞性系统调用来进行一个简单的实验 +package main + +import ( + "fmt" + "net/http" + _ "net/http/pprof" + "os" + "syscall" + "unsafe" +) + +const ( + SYS_futex = 202 + _FUTEX_PRIVATE_FLAG = 128 + _FUTEX_WAIT = 0 + _FUTEX_WAKE = 1 + _FUTEX_WAIT_PRIVATE = _FUTEX_WAIT | _FUTEX_PRIVATE_FLAG + _FUTEX_WAKE_PRIVATE = _FUTEX_WAKE | _FUTEX_PRIVATE_FLAG +) + +func main() { + fmt.Println(os.Getpid()) + go func() { + b := make([]byte, 1<<20) + _ = b + }() + for i := 1; i < 13; i++ { + go func() { + var futexVar int = 0 + for { + // Syscall && RawSyscall, 具体差别分析可自行查看 syscall 章节 + fmt.Println(syscall.Syscall6( + SYS_futex, // trap AX 202 + uintptr(unsafe.Pointer(&futexVar)), // a1 DI 1 + uintptr(_FUTEX_WAIT), // a2 SI 0 + 0, // a3 DX + 0, //uintptr(unsafe.Pointer(&ts)), // a4 R10 + 0, // a5 R8 + 0)) + } + }() + } + http.ListenAndServe("0.0.0.0:8899", nil) +} +``` +```shell +# GET /debug/pprof/threadcreate?debug=1 +threadcreate profile: total 18 +17 @ +# 0x0 + +1 @ 0x43b818 0x43bfa3 0x43c272 0x43857d 0x467fb1 +# 0x43b817 runtime.allocm+0x157 /usr/local/go/src/runtime/proc.go:1414 +# 0x43bfa2 runtime.newm+0x42 /usr/local/go/src/runtime/proc.go:1736 +# 0x43c271 runtime.startTemplateThread+0xb1 /usr/local/go/src/runtime/proc.go:1805 +# 0x43857c runtime.main+0x18c /usr/local/go/src/runtime/proc.go:186 +``` +```shell +# 再结合诸如 pstack 的工具 +ps -efT | grep 22298 # pid = 22298 +root 22298 22298 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22299 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22300 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22301 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22302 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22303 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22304 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22305 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22306 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22307 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22308 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22309 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22310 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22311 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22312 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22316 13767 0 16:59 pts/4 00:00:00 ./mstest +root 22298 22317 13767 0 16:59 pts/4 00:00:00 ./mstest + +pstack 22299 +Thread 1 (process 22299): +#0 runtime.futex () at /usr/local/go/src/runtime/sys_linux_amd64.s:568 +#1 0x00000000004326f4 in runtime.futexsleep (addr=0xb2fd78 , val=0, ns=60000000000) at /usr/local/go/src/runtime/os_linux.go:51 +#2 0x000000000040cb3e in runtime.notetsleep_internal (n=0xb2fd78 , ns=60000000000, ~r2=) at /usr/local/go/src/runtime/lock_futex.go:193 +#3 0x000000000040cc11 in runtime.notetsleep (n=0xb2fd78 , ns=60000000000, ~r2=) at /usr/local/go/src/runtime/lock_futex.go:216 +#4 0x00000000004433b2 in runtime.sysmon () at /usr/local/go/src/runtime/proc.go:4558 +#5 0x000000000043af33 in runtime.mstart1 () at /usr/local/go/src/runtime/proc.go:1112 +#6 0x000000000043ae4e in runtime.mstart () at /usr/local/go/src/runtime/proc.go:1077 +#7 0x0000000000401893 in runtime/cgo(.text) () +#8 0x00007fb1e2d53700 in ?? () +#9 0x0000000000000000 in ?? () +``` +其他的线程如果感兴趣也可以仔细查看 + +`pprof/threadcreate` 具体实现和 `pprof/goroutine` 类似, 无非前者遍历的对象是全局 `allm`, 而后者为 `allgs`, 区别在于 `pprof/threadcreate => ThreadCreateProfile` 时不会进行进行 `STW` + +## pprof/mutex +mutex 默认是关闭采样的, 通过 `runtime.SetMutexProfileFraction(int)` 来进行 `rate` 的配置进行开启或关闭 + +和上文分析过的 `mbuckets` 类似, 这边用以记录采样数据的是 `xbuckets`, `bucket` 记录了锁持有的堆栈, 次数(采样)等信息以供用户查看 +```go +//go:linkname mutexevent sync.event +func mutexevent(cycles int64, skip int) { + if cycles < 0 { + cycles = 0 + } + rate := int64(atomic.Load64(&mutexprofilerate)) + // TODO(pjw): measure impact of always calling fastrand vs using something + // like malloc.go:nextSample() + // 同样根据 rate 来进行采样, 这边用以记录 rate 的是 mutexprofilerate 变量 + if rate > 0 && int64(fastrand())%rate == 0 { + saveblockevent(cycles, skip+1, mutexProfile) + } +} +``` +```shell + --------------- + | user access | + --------------- + | + ------------------ | +| xbuckets list | copy | +| (global) | ------------------------------------- + ------------------ + | + | + | create_or_get && insert_or_update bucket into xbuckets + | + | + -------------------------------------- +| func stkbucket & typ == mutexProfile | + -------------------------------------- + | + ------------------ + | saveblockevent | // 堆栈等信息记录 + ------------------ + | + | + | /* + | * //go:linkname mutexevent sync.event + | * func mutexevent(cycles int64, skip int) { + | * if cycles < 0 { + | * cycles = 0 + | * } + | 采样 * rate := int64(atomic.Load64(&mutexprofilerate)) + | 记录 * // TODO(pjw): measure impact of always calling fastrand vs using something + | * // like malloc.go:nextSample() + | * if rate > 0 && int64(fastrand())%rate == 0 { + | * saveblockevent(cycles, skip+1, mutexProfile) + | * } + | * + | */ + | + ------------ 不采样 + | mutexevent | ----------.... + ------------ + | + | + ------------ + | semrelease1 | + ------------ + | + | + ------------------------ + | runtime_Semrelease | + ------------------------ + | + | + ------------ + | unlockSlow | + ------------ + | + | + ------------ + | Unlock | + ------------ +``` + +## pprof/block +同上, 主要来分析下 `bbuckets` +```shell + --------------- + | user access | + --------------- + | + ------------------ | +| bbuckets list | copy | +| (global) | ------------------------------------- + ------------------ + | + | + | create_or_get && insert_or_update bucket into bbuckets + | + | + -------------------------------------- +| func stkbucket & typ == blockProfile | + -------------------------------------- + | + ------------------ + | saveblockevent | // 堆栈等信息记录 + ------------------ + | + | + | /* + | * func blocksampled(cycles int64) bool { + | * rate := int64(atomic.Load64(&blockprofilerate)) + | * if rate <= 0 || (rate > cycles && int64(fastrand())%rate > cycles) { + | * return false + | 采样 * } + | 记录 * return true + | * } + | */ + | + ------------ 不采样 + | blockevent | ----------.... + ------------ + |---------------------------------------------------------------------------- + | | | + ------------ ----------------------------------------------- ------------ + | semrelease1 | | chansend / chanrecv && mysg.releasetime > 0 | | selectgo | + ------------ ----------------------------------------------- ------------ +``` +相比较 `mutex` 的采样, `block` 的埋点会额外存在于 `chan` 中, 每次 `block` 记录的是前后两个 `cpu 周期` 的差值 (cycles) +需要注意的是 `cputicks` 可能在不同系统上存在一些[问题](https://github.com/golang/go/issues/8976). 暂不放在这边讨论 + +## pprof/profile +上面分析的都属于 `runtime` 在运行的过程中自动采用保存数据后用户进行观察的, `profile` 则是用户选择指定周期内的 `CPU Profiling` + +#总结 +- `pprof` 的确会给 `runtime` 带来额外的压力, 压力的多少取决于用户使用的各个 `*_rate` 配置, 在获取 `pprof` 信息的时候需要按照实际情况酌情使用各个接口, 每个接口产生的额外压力是不一样的. +- 不同版本在是否默认开启上有不同策略, 需要自行根据各自的环境进行确认 +- `pprof` 获取到的数据仅能作为参考, 和设置的采样频率有关, 在计算例如 `heap` 情况时会进行相关的近似预估, 非实质上对 `heap` 进行扫描 + +```shell + ------------------------- +| pprof.StartCPUProfile | + ------------------------- + | + | + | + ------------------------- +| sleep(time.Duration) | + ------------------------- + | + | + | + ------------------------- +| pprof.StopCPUProfile | + ------------------------- +``` +`pprof.StartCPUProfile` 与 `pprof.StopCPUProfile` 核心为 `runtime.SetCPUProfileRate(hz int)` 控制 `cpu profile` 频率, 但是这边的频率设置和前面几个有差异, 不仅仅是设计 rate 的设置, 还涉及全局对象 `cpuprof` log buffer 的分配 +```go +var cpuprof cpuProfile +type cpuProfile struct { + lock mutex + on bool // profiling is on + log *profBuf // profile events written here + + // extra holds extra stacks accumulated in addNonGo + // corresponding to profiling signals arriving on + // non-Go-created threads. Those stacks are written + // to log the next time a normal Go thread gets the + // signal handler. + // Assuming the stacks are 2 words each (we don't get + // a full traceback from those threads), plus one word + // size for framing, 100 Hz profiling would generate + // 300 words per second. + // Hopefully a normal Go thread will get the profiling + // signal at least once every few seconds. + extra [1000]uintptr + numExtra int + lostExtra uint64 // count of frames lost because extra is full + lostAtomic uint64 // count of frames lost because of being in atomic64 on mips/arm; updated racily +} +``` +`log buffer` 的大小每次分配是固定的, 无法进行调节 + +### cpuprof.add +将 `stack trace` 信息写入 `cpuprof` 的 `log buffer` +```go +// add adds the stack trace to the profile. +// It is called from signal handlers and other limited environments +// and cannot allocate memory or acquire locks that might be +// held at the time of the signal, nor can it use substantial amounts +// of stack. +//go:nowritebarrierrec +func (p *cpuProfile) add(gp *g, stk []uintptr) { + // Simple cas-lock to coordinate with setcpuprofilerate. + for !atomic.Cas(&prof.signalLock, 0, 1) { + osyield() + } + + if prof.hz != 0 { // implies cpuprof.log != nil + if p.numExtra > 0 || p.lostExtra > 0 || p.lostAtomic > 0 { + p.addExtra() + } + hdr := [1]uint64{1} + // Note: write "knows" that the argument is &gp.labels, + // because otherwise its write barrier behavior may not + // be correct. See the long comment there before + // changing the argument here. + cpuprof.log.write(&gp.labels, nanotime(), hdr[:], stk) + } + + atomic.Store(&prof.signalLock, 0) +} +``` + +来看下调用 `cpuprof.add` 的流程 +```shell + ------------------------ +| cpu profile start | + ------------------------ + | + | + | start timer (setitimer syscall / ITIMER_PROF) + | 每个一段时间(rate)在向当前 P 所在线程发送一个 SIGPROF 信号量 -- + | | + | | + ------------------------ loop | +| sighandler |---------------------------------------------- + ------------------------ | + | | + | /* | + | * if sig == _SIGPROF { | + | * sigprof(c.sigpc(), c.sigsp(), c.siglr(), gp, _g_.m) + | * return | + | */ } | + | | + ---------------------------- | stop + | sigprof(stack strace) | | + ---------------------------- | + | | + | | + | | + ---------------------- | + | cpuprof.add | | + ---------------------- ---------------------- + | | cpu profile stop | + | ---------------------- + | + ---------------------- + | cpuprof.log buffer | + ---------------------- + | --------------------- --------------- + ----------------------------------------| cpuprof.read |----------------| user access | + --------------------- --------------- +``` +由于 `GMP` 的模型设计, 在绝大多数情况下通过这种 `timer` + `sig` + `current thread` 以及当前支持的抢占式调度, 这种记录方式是能够很好进行整个 `runtime cpu profile` 采样分析的, 但也不能排除一些极端情况是无法被覆盖的, 毕竟也只是基于当前 M 而已. + +# 总结 +#### 可用性: +runtime 自带的 pprof 已经在数据采集的准确性, 覆盖率, 压力等各方面替我们做好了一个比较均衡及全面的考虑 + +在绝大多数场景下使用起来需要考虑的性能点无非就是几个 rate 的设置 + +不同版本的默认开启是有差别的, 几个参数默认值可自行确认, 有时候你觉得没有开启 pprof 但是实际上已经开启了 + +当选择的参数合适的时候, pprof 远远没有想象中那般“重” +#### 局限性: +得到的数据只是采样(根据 rate 决定) 或预估值 + +无法 cover 所有场景, 对于一些特殊的或者极端的情况, 需要各自进行优化来选择合适的手段完善 +#### 安全性: +生产环境可用 pprof, 注意接口不能直接暴露, 毕竟存在诸如 STW 等操作, 存在潜在风险点 + +#开源项目 pprof 参考 +[nsq](https://github.com/nsqio/nsq/blob/v1.2.0/nsqd/http.go#L78-L88) +[etcd](https://github.com/etcd-io/etcd/blob/release-3.4/pkg/debugutil/pprof.go#L23) 采用的是[配置式](https://github.com/etcd-io/etcd/blob/release-3.4/etcd.conf.yml.sample#L76)选择是否开启 + +# 参考资料 +https://go-review.googlesource.com/c/go/+/299671 + + diff --git a/readme.md b/readme.md index cbd4555..d0c7ab2 100644 --- a/readme.md +++ b/readme.md @@ -29,6 +29,8 @@ 23. [x] [stack dump](runtime_stack.md) 24. [x] [Atomic](atomic.md) 25. [ ] [Generics](generics.md) +26. [x] [IO](io.md) +26. [x] [pprof](pprof.md) # Authors diff --git a/scheduler.md b/scheduler.md index ec9ed3e..7d4557b 100644 --- a/scheduler.md +++ b/scheduler.md @@ -1,4 +1,4 @@ -> 注: 在抢占式调度的 go 版本下如果需要对 runtime 进行调试,诸如使用 gdb, lldb, [delve](https://github.com/go-delve/delve) 等工具时,需要注意 GODEBUG=asyncpreemptoff=1 环境变量,该变量会导致 runtime 是否进行抢占式调度,由于 https://github.com/golang/go/issues/36494 ,导致部分系统下该变量会被一些(如 delve)工具配置开启,从而导致超出预期的调试情况,需要读者自行关注 +> 注: 在抢占式调度的 go 版本下如果需要对 runtime 进行调试,诸如使用 gdb, lldb, [delve](https://github.com/go-delve/delve) 等工具时,需要注意 GODEBUG=asyncpreemptoff=1 环境变量,该变量会决定 runtime 是否开启抢占式调度,由于 https://github.com/golang/go/issues/36494 ,导致部分系统下该变量会被一些(如 delve)工具配置开启,从而导致超出预期的调试情况,需要读者自行关注 # 调度 ## 基本数据结构 @@ -752,6 +752,8 @@ runtime.gcenable --> main.init main.init --> main.main ``` +**主线程也是需要和 p 绑定来运行的**,绑定过程在 procresize -> acquirep 中。 + ### sysmon 线程 sysmon 是在 `runtime.main` 中启动的,不过需要注意的是 sysmon 并不是在 m0 上执行的。因为: @@ -2346,3 +2348,4 @@ gcMarkDone --> forEachP 当然,这里 entersyscall 和 entersyscallblock 比较特殊,虽然这俩函数的实现中有设置抢占标记,但实际上这两段逻辑是不会被走到的。因为 syscall 执行时是在 m 的 g0 栈上,如果在执行时被抢占,那么会直接 throw,而无法恢复。 + diff --git a/select.md b/select.md index 24d32d0..ed93053 100644 --- a/select.md +++ b/select.md @@ -826,3 +826,5 @@ sclose: Q: 如果select多个channel,有一个channel触发了,其他channel的waitlist需要不要主动去除?还是一直在那等着? A: waitlist 的出列是由 `func (q *waitq) dequeue() *sudog` 函数控制的,每个 sudog 携带了一个 `selectDone` 标志位,通过 `cas` 操作在每次 `dequeue` 的时候「惰性」去除队列中无效的元素 + + diff --git a/semaphore.md b/semaphore.md index 4fbeddd..51d4640 100644 --- a/semaphore.md +++ b/semaphore.md @@ -236,7 +236,7 @@ func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags) { // 高成本的情况: // 增加 waiter count 的值 - // 再尝试调用一次 cansemacquire,成本了就直接返回 + // 再尝试调用一次 cansemacquire,成功了就直接返回 // 没成功就把自己作为一个 waiter 入队 // sleep // (之后 waiter 的 descriptor 被 signaler 用 dequeue 踢出) @@ -735,4 +735,6 @@ func notifyListCheck(sz uintptr) { func sync_nanotime() int64 { return nanotime() } -``` \ No newline at end of file +``` + + diff --git a/signal.md b/signal.md deleted file mode 100644 index 09c2668..0000000 --- a/signal.md +++ /dev/null @@ -1,3 +0,0 @@ -# Signal - -Go 1.12 的抢占使用 signal 来实现,所以我们来分析一下 Go runtime 中是怎么处理这些 signal 的。 diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..f6a4c5e --- /dev/null +++ b/site/README.md @@ -0,0 +1,3 @@ +# golang + +source of go.xargin.com diff --git a/site/archetypes/default.md b/site/archetypes/default.md new file mode 100644 index 0000000..00e77bd --- /dev/null +++ b/site/archetypes/default.md @@ -0,0 +1,6 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + diff --git a/site/config.toml b/site/config.toml new file mode 100644 index 0000000..80f33af --- /dev/null +++ b/site/config.toml @@ -0,0 +1,6 @@ +baseURL = "http://go.xargin.com/" +languageCode = "en-us" +title = "Go 语言笔记" +theme = "book" +googleAnalytics = "G-KLR638LKEQ" + diff --git a/site/content/_index.md b/site/content/_index.md new file mode 100644 index 0000000..e5fb159 --- /dev/null +++ b/site/content/_index.md @@ -0,0 +1,13 @@ +--- +title: Go 语言笔记 +type: docs +--- + +# Go 语言笔记 + +## 为什么会有这本书 + +之前关于 Go 的输出主要散落在 blog 和 github 的 golang-notes 以及公众号中,内容比较分散。不方便阅读,到了这个时间点,本人已经使用 Go 已有超过 6 个年头,可以将之前的输出集合起来,进行系统化的输出了。 + +![nobody knows more about Go than me](/images/index/banner.jpg) + diff --git a/site/content/docs/_index.md b/site/content/docs/_index.md new file mode 100644 index 0000000..e69de29 diff --git a/site/content/docs/api_programming/_index.md b/site/content/docs/api_programming/_index.md new file mode 100644 index 0000000..7718841 --- /dev/null +++ b/site/content/docs/api_programming/_index.md @@ -0,0 +1,6 @@ +--- +title: API 开发 +weight: 5 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/api_programming/fasthttp.md b/site/content/docs/api_programming/fasthttp.md new file mode 100644 index 0000000..a886942 --- /dev/null +++ b/site/content/docs/api_programming/fasthttp.md @@ -0,0 +1,2 @@ +# FastHTTP + diff --git a/site/content/docs/api_programming/httprouter.md b/site/content/docs/api_programming/httprouter.md new file mode 100644 index 0000000..bc5ea15 --- /dev/null +++ b/site/content/docs/api_programming/httprouter.md @@ -0,0 +1 @@ +# http router 的实现 diff --git a/site/content/docs/api_programming/mysql.md b/site/content/docs/api_programming/mysql.md new file mode 100644 index 0000000..c64321e --- /dev/null +++ b/site/content/docs/api_programming/mysql.md @@ -0,0 +1 @@ +# mysql diff --git a/site/content/docs/api_programming/orm.md b/site/content/docs/api_programming/orm.md new file mode 100644 index 0000000..c69e1ca --- /dev/null +++ b/site/content/docs/api_programming/orm.md @@ -0,0 +1 @@ +# orm && sql builder diff --git a/site/content/docs/api_programming/validator.md b/site/content/docs/api_programming/validator.md new file mode 100644 index 0000000..76a423c --- /dev/null +++ b/site/content/docs/api_programming/validator.md @@ -0,0 +1 @@ +# validator diff --git a/site/content/docs/api_programming/viper.md b/site/content/docs/api_programming/viper.md new file mode 100644 index 0000000..63d973f --- /dev/null +++ b/site/content/docs/api_programming/viper.md @@ -0,0 +1 @@ +# viper diff --git a/site/content/docs/assembly/_index.md b/site/content/docs/assembly/_index.md new file mode 100644 index 0000000..18a2318 --- /dev/null +++ b/site/content/docs/assembly/_index.md @@ -0,0 +1,6 @@ +--- +title: 汇编基础 +weight: 1 +bookCollapseSection: true +--- + diff --git a/site/content/docs/assembly/assembly.md b/site/content/docs/assembly/assembly.md new file mode 100644 index 0000000..22fef9c --- /dev/null +++ b/site/content/docs/assembly/assembly.md @@ -0,0 +1,1006 @@ +--- +title: Plan9 汇编解析 +weight: 10 +--- + +# plan9 assembly 完全解析 + +众所周知,Go 使用了 Unix 老古董(误 们发明的 plan9 汇编。就算你对 x86 汇编有所了解,在 plan9 里还是有些许区别。说不定你在看代码的时候,偶然发现代码里的 SP 看起来是 SP,但它实际上不是 SP 的时候就抓狂了哈哈哈。 + +本文将对 plan9 汇编进行全面的介绍,同时解答你在接触 plan9 汇编时可能遇到的大部分问题。 + +本文所使用的平台是 linux amd64,因为不同的平台指令集和寄存器都不一样,所以没有办法共同探讨。这也是由汇编本身的性质决定的。 + +## 基本指令 + +### 栈调整 + +intel 或 AT&T 汇编提供了 push 和 pop 指令族,~~plan9 中没有 push 和 pop~~,plan9 中虽然有 push 和 pop 指令,但一般生成的代码中是没有的,我们看到的栈的调整大多是通过对硬件 SP 寄存器进行运算来实现的,例如: + +```go +SUBQ $0x18, SP // 对 SP 做减法,为函数分配函数栈帧 +... // 省略无用代码 +ADDQ $0x18, SP // 对 SP 做加法,清除函数栈帧 +``` + +通用的指令和 X64 平台差不多,下面分节详述。 + +### 数据搬运 + +常数在 plan9 汇编用 $num 表示,可以为负数,默认情况下为十进制。可以用 $0x123 的形式来表示十六进制数。 + +```go +MOVB $1, DI // 1 byte +MOVW $0x10, BX // 2 bytes +MOVD $1, DX // 4 bytes +MOVQ $-10, AX // 8 bytes +``` + +可以看到,搬运的长度是由 MOV 的后缀决定的,这一点与 intel 汇编稍有不同,看看类似的 X64 汇编: + +```asm +mov rax, 0x1 // 8 bytes +mov eax, 0x100 // 4 bytes +mov ax, 0x22 // 2 bytes +mov ah, 0x33 // 1 byte +mov al, 0x44 // 1 byte +``` + +plan9 的汇编的操作数的方向是和 intel 汇编相反的,与 AT&T 类似。 + +```go +MOVQ $0x10, AX ===== mov rax, 0x10 + | |------------| | + |------------------------| +``` + +不过凡事总有例外,如果想了解这种意外,可以参见参考资料中的 [1]。 + +### 常见计算指令 + +```go +ADDQ AX, BX // BX += AX +SUBQ AX, BX // BX -= AX +IMULQ AX, BX // BX *= AX +``` + +类似数据搬运指令,同样可以通过修改指令的后缀来对应不同长度的操作数。例如 ADDQ/ADDW/ADDL/ADDB。 + +### 条件跳转/无条件跳转 + +```go +// 无条件跳转 +JMP addr // 跳转到地址,地址可为代码中的地址,不过实际上手写不会出现这种东西 +JMP label // 跳转到标签,可以跳转到同一函数内的标签位置 +JMP 2(PC) // 以当前指令为基础,向前/后跳转 x 行 +JMP -2(PC) // 同上 + +// 有条件跳转 +JZ target // 如果 zero flag 被 set 过,则跳转 + +``` + + +### 指令集 + +可以参考源代码的 [arch](https://github.com/golang/arch/blob/master/x86/x86.csv) 部分。 + +额外提一句,Go 1.10 添加了大量的 SIMD 指令支持,所以在该版本以上的话,不像之前写那样痛苦了,也就是不用人肉填 byte 了。 + +## 寄存器 + +### 通用寄存器 + +amd64 的通用寄存器: + +```gdb +(lldb) reg read +General Purpose Registers: + rax = 0x0000000000000005 + rbx = 0x000000c420088000 + rcx = 0x0000000000000000 + rdx = 0x0000000000000000 + rdi = 0x000000c420088008 + rsi = 0x0000000000000000 + rbp = 0x000000c420047f78 + rsp = 0x000000c420047ed8 + r8 = 0x0000000000000004 + r9 = 0x0000000000000000 + r10 = 0x000000c420020001 + r11 = 0x0000000000000202 + r12 = 0x0000000000000000 + r13 = 0x00000000000000f1 + r14 = 0x0000000000000011 + r15 = 0x0000000000000001 + rip = 0x000000000108ef85 int`main.main + 213 at int.go:19 + rflags = 0x0000000000000212 + cs = 0x000000000000002b + fs = 0x0000000000000000 + gs = 0x0000000000000000 +``` + +在 plan9 汇编里都是可以使用的,应用代码层面会用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 这 14 个寄存器,虽然 rbp 和 rsp 也可以用,不过 bp 和 sp 会被用来管理栈顶和栈底,最好不要拿来进行运算。 + +plan9 中使用寄存器不需要带 r 或 e 的前缀,例如 rax,只要写 AX 即可: + +```go +MOVQ $101, AX = mov rax, 101 +``` + +下面是通用通用寄存器的名字在 X64 和 plan9 中的对应关系: + +| X64 | rax | rbx| rcx | rdx | rdi | rsi | rbp | rsp | r8 | r9 | r10 | r11 | r12 | r13 | r14 | rip| +|--|--|--|--| --| --|--| --|--|--|--|--|--|--|--|--|--| +| Plan9 | AX | BX | CX | DX | DI | SI | BP | SP | R8 | R9 | R10 | R11 | R12 | R13 | R14 | PC | + +### 伪寄存器 + +Go 的汇编还引入了 4 个伪寄存器,援引官方文档的描述: + +>- `FP`: Frame pointer: arguments and locals. +>- `PC`: Program counter: jumps and branches. +>- `SB`: Static base pointer: global symbols. +>- `SP`: Stack pointer: the highest address within the local stack frame. + +官方的描述稍微有一些问题,我们对这些说明进行一点扩充: + +- FP: 使用形如 `symbol+offset(FP)` 的方式,引用函数的输入参数。例如 `arg0+0(FP)`,`arg1+8(FP)`,使用 FP 不加 symbol 时,无法通过编译,在汇编层面来讲,symbol 并没有什么用,加 symbol 主要是为了提升代码可读性。另外,官方文档虽然将伪寄存器 FP 称之为 frame pointer,实际上它根本不是 frame pointer,按照传统的 x86 的习惯来讲,frame pointer 是指向整个 stack frame 底部的 BP 寄存器。假如当前的 callee 函数是 add,在 add 的代码中引用 FP,该 FP 指向的位置不在 callee 的 stack frame 之内,而是在 caller 的 stack frame 上。具体可参见之后的 **栈结构** 一章。 +- PC: 实际上就是在体系结构的知识中常见的 pc 寄存器,在 x86 平台下对应 ip 寄存器,amd64 上则是 rip。除了个别跳转之外,手写 plan9 代码与 PC 寄存器打交道的情况较少。 +- SB: 全局静态基指针,一般用来声明函数或全局变量,在之后的函数知识和示例部分会看到具体用法。 +- SP: plan9 的这个 SP 寄存器指向当前栈帧的局部变量的开始位置,使用形如 `symbol+offset(SP)` 的方式,引用函数的局部变量。offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间。假如局部变量都是 8 字节,那么第一个局部变量就可以用 `localvar0-8(SP)` 来表示。这也是一个词不表意的寄存器。与硬件寄存器 SP 是两个不同的东西,在栈帧 size 为 0 的情况下,伪寄存器 SP 和硬件寄存器 SP 指向同一位置。手写汇编代码时,如果是 `symbol+offset(SP)` 形式,则表示伪寄存器 SP。如果是 `offset(SP)` 则表示硬件寄存器 SP。务必注意。对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的 SP 都是硬件寄存器 SP,无论是否带 symbol。 + +我们这里对容易混淆的几点简单进行说明: + +1. 伪 SP 和硬件 SP 不是一回事,在手写代码时,伪 SP 和硬件 SP 的区分方法是看该 SP 前是否有 symbol。如果有 symbol,那么即为伪寄存器,如果没有,那么说明是硬件 SP 寄存器。 +2. SP 和 FP 的相对位置是会变的,所以不应该尝试用伪 SP 寄存器去找那些用 FP + offset 来引用的值,例如函数的入参和返回值。 +3. 官方文档中说的伪 SP 指向 stack 的 top,是有问题的。其指向的局部变量位置实际上是整个栈的栈底(除 caller BP 之外),所以说 bottom 更合适一些。 +4. 在 go tool objdump/go tool compile -S 输出的代码中,是没有伪 SP 和 FP 寄存器的,我们上面说的区分伪 SP 和硬件 SP 寄存器的方法,对于上述两个命令的输出结果是没法使用的。在编译和反汇编的结果中,只有真实的 SP 寄存器。 +5. FP 和 Go 的官方源代码里的 framepointer 不是一回事,源代码里的 framepointer 指的是 caller BP 寄存器的值,在这里和 caller 的伪 SP 是值是相等的。 + +以上说明看不懂也没关系,在熟悉了函数的栈结构之后再反复回来查看应该就可以明白了。 + +## 变量声明 + +在汇编里所谓的变量,一般是存储在 .rodata 或者 .data 段中的只读值。对应到应用层的话,就是已初始化过的全局的 const、var、static 变量/常量。 + +使用 DATA 结合 GLOBL 来定义一个变量。DATA 的用法为: + +```go +DATA symbol+offset(SB)/width, value +``` + +大多数参数都是字面意思,不过这个 offset 需要稍微注意。其含义是该值相对于符号 symbol 的偏移,而不是相对于全局某个地址的偏移。 + +使用 GLOBL 指令将变量声明为 global,额外接收两个参数,一个是 flag,另一个是变量的总大小。 + +```go +GLOBL divtab(SB), RODATA, $64 +``` + +GLOBL 必须跟在 DATA 指令之后,下面是一个定义了多个 readonly 的全局变量的完整例子: + +```go +DATA age+0x00(SB)/4, $18 // forever 18 +GLOBL age(SB), RODATA, $4 + +DATA pi+0(SB)/8, $3.1415926 +GLOBL pi(SB), RODATA, $8 + +DATA birthYear+0(SB)/4, $1988 +GLOBL birthYear(SB), RODATA, $4 +``` + +正如之前所说,所有符号在声明时,其 offset 一般都是 0。 + +有时也可能会想在全局变量中定义数组,或字符串,这时候就需要用上非 0 的 offset 了,例如: + +```go +DATA bio<>+0(SB)/8, $"oh yes i" +DATA bio<>+8(SB)/8, $"am here " +GLOBL bio<>(SB), RODATA, $16 +``` + +大部分都比较好理解,不过这里我们又引入了新的标记 `<>`,这个跟在符号名之后,表示该全局变量只在当前文件中生效,类似于 C 语言中的 static。如果在另外文件中引用该变量的话,会报 `relocation target not found` 的错误。 + +本小节中提到的 flag,还可以有其它的取值: +>- `NOPROF` = 1 + (For `TEXT` items.) Don't profile the marked function. This flag is deprecated. +>- `DUPOK` = 2 + It is legal to have multiple instances of this symbol in a single binary. The linker will choose one of the duplicates to use. +>- `NOSPLIT` = 4 + (For `TEXT` items.) Don't insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space at the top of the stack segment. Used to protect routines such as the stack splitting code itself. +>- `RODATA` = 8 + (For `DATA` and `GLOBL` items.) Put this data in a read-only section. +>- `NOPTR` = 16 + (For `DATA` and `GLOBL` items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector. +>- `WRAPPER` = 32 + (For `TEXT` items.) This is a wrapper function and should not count as disabling `recover`. +>- `NEEDCTXT` = 64 + (For `TEXT` items.) This function is a closure so it uses its incoming context register. + +当使用这些 flag 的字面量时,需要在汇编文件中 `#include "textflag.h"`。 + +## .s 和 .go 文件的全局变量互通 + +在 `.s` 文件中是可以直接使用 `.go` 中定义的全局变量的,看看下面这个简单的例子: + +refer.go: + +```go +package main + +var a = 999 +func get() int + +func main() { + println(get()) +} + +``` + +refer.s: + +```go +#include "textflag.h" + +TEXT ·get(SB), NOSPLIT, $0-8 + MOVQ ·a(SB), AX + MOVQ AX, ret+0(FP) + RET +``` + +·a(SB),表示该符号需要链接器来帮我们进行重定向(relocation),如果找不到该符号,会输出 `relocation target not found` 的错误。 + +例子比较简单,大家可以自行尝试。 + +## 函数声明 + +我们来看看一个典型的 plan9 的汇编函数的定义: + +```go +// func add(a, b int) int +// => 该声明定义在同一个 package 下的任意 .go 文件中 +// => 只有函数头,没有实现 +TEXT pkgname·add(SB), NOSPLIT, $0-8 + MOVQ a+0(FP), AX + MOVQ a+8(FP), BX + ADDQ AX, BX + MOVQ BX, ret+16(FP) + RET +``` + +为什么要叫 TEXT ?如果对程序数据在文件中和内存中的分段稍有了解的同学应该知道,我们的代码在二进制文件中,是存储在 .text 段中的,这里也就是一种约定俗成的起名方式。实际上在 plan9 中 TEXT 是一个指令,用来定义一个函数。除了 TEXT 之外还有前面变量声明说到的 DATA/GLOBL。 + +定义中的 pkgname 部分是可以省略的,非想写也可以写上。不过写上 pkgname 的话,在重命名 package 之后还需要改代码,所以推荐最好还是不要写。 + +中点 `·` 比较特殊,是一个 unicode 的中点,该点在 mac 下的输入方法是 `option+shift+9`。在程序被链接之后,所有的中点`·` 都会被替换为句号`.`,比如你的方法是 `runtime·main`,在编译之后的程序里的符号则是 `runtime.main`。嗯,看起来很变态。简单总结一下: + +```go + + 参数及返回值大小 + | + TEXT pkgname·add(SB),NOSPLIT,$32-32 + | | | + 包名 函数名 栈帧大小(局部变量+可能需要的额外调用函数的参数空间的总大小,但不包括调用其它函数时的 ret address 的大小) + +``` + +## 栈结构 + +下面是一个典型的函数的栈结构图: + +``` + + ----------------- + current func arg0 + ----------------- <----------- FP(pseudo FP) + caller ret addr + +---------------+ + | caller BP(*) | + ----------------- <----------- SP(pseudo SP,实际上是当前栈帧的 BP 位置) + | Local Var0 | + ----------------- + | Local Var1 | + ----------------- + | Local Var2 | + ----------------- - + | ........ | + ----------------- + | Local VarN | + ----------------- + | | + | | + | temporarily | + | unused space | + | | + | | + ----------------- + | call retn | + ----------------- + | call ret(n-1)| + ----------------- + | .......... | + ----------------- + | call ret1 | + ----------------- + | call argn | + ----------------- + | ..... | + ----------------- + | call arg3 | + ----------------- + | call arg2 | + |---------------| + | call arg1 | + ----------------- <------------ hardware SP 位置 + return addr + +---------------+ + + +``` + +从原理上来讲,如果当前函数调用了其它函数,那么 return addr 也是在 caller 的栈上的,不过往栈上插 return addr 的过程是由 CALL 指令完成的,在 RET 时,SP 又会恢复到图上位置。我们在计算 SP 和参数相对位置时,可以认为硬件 SP 指向的就是图上的位置。 + +图上的 caller BP,指的是 caller 的 BP 寄存器值,有些人把 caller BP 叫作 caller 的 frame pointer,实际上这个习惯是从 x86 架构沿袭来的。Go 的 asm 文档中把伪寄存器 FP 也称为 frame pointer,但是这两个 frame pointer 根本不是一回事。 + +此外需要注意的是,caller BP 是在编译期由编译器插入的,用户手写代码时,计算 frame size 时是不包括这个 caller BP 部分的。是否插入 caller BP 的主要判断依据是: + +1. 函数的栈帧大小大于 0 +2. 下述函数返回 true + +```go +func Framepointer_enabled(goos, goarch string) bool { + return framepointer_enabled != 0 && goarch == "amd64" && goos != "nacl" +} +``` + +如果编译器在最终的汇编结果中没有插入 caller BP(源代码中所称的 frame pointer)的情况下,伪 SP 和伪 FP 之间只有 8 个字节的 caller 的 return address,而插入了 BP 的话,就会多出额外的 8 字节。也就说伪 SP 和伪 FP 的相对位置是不固定的,有可能是间隔 8 个字节,也有可能间隔 16 个字节。并且判断依据会根据平台和 Go 的版本有所不同。 + +图上可以看到,FP 伪寄存器指向函数的传入参数的开始位置,因为栈是朝低地址方向增长,为了通过寄存器引用参数时方便,所以参数的摆放方向和栈的增长方向是相反的,即: + +```shell + FP +high ----------------------> low +argN, ... arg3, arg2, arg1, arg0 +``` + +假设所有参数均为 8 字节,这样我们就可以用 symname+0(FP) 访问第一个 参数,symname+8(FP) 访问第二个参数,以此类推。用伪 SP 来引用局部变量,原理上来讲差不多,不过因为伪 SP 指向的是局部变量的底部,所以 symname-8(SP) 表示的是第一个局部变量,symname-16(SP)表示第二个,以此类推。当然,这里假设局部变量都占用 8 个字节。 + +图的最上部的 caller return address 和 current func arg0 都是由 caller 来分配空间的。不算在当前的栈帧内。 + +因为官方文档本身较模糊,我们来一个函数调用的全景图,来看一下这些真假 SP/FP/BP 到底是个什么关系: + +``` + + caller + +------------------+ + | | + +----------------------> -------------------- + | | | + | | caller parent BP | + | BP(pseudo SP) -------------------- + | | | + | | Local Var0 | + | -------------------- + | | | + | | ....... | + | -------------------- + | | | + | | Local VarN | + -------------------- + caller stack frame | | + | callee arg2 | + | |------------------| + | | | + | | callee arg1 | + | |------------------| + | | | + | | callee arg0 | + | ----------------------------------------------+ FP(virtual register) + | | | | + | | return addr | parent return address | + +----------------------> +------------------+--------------------------- <-------------------------------+ + | caller BP | | + | (caller frame pointer) | | + BP(pseudo SP) ---------------------------- | + | | | + | Local Var0 | | + ---------------------------- | + | | + | Local Var1 | + ---------------------------- callee stack frame + | | + | ..... | + ---------------------------- | + | | | + | Local VarN | | + SP(Real Register) ---------------------------- | + | | | + | | | + | | | + | | | + | | | + +--------------------------+ <-------------------------------+ + + callee +``` + +## argsize 和 framesize 计算规则 + +### argsize + +在函数声明中: + +```go + TEXT pkgname·add(SB),NOSPLIT,$16-32 +``` + +前面已经说过 $16-32 表示 $framesize-argsize。Go 在函数调用时,参数和返回值都需要由 caller 在其栈帧上备好空间。callee 在声明时仍然需要知道这个 argsize。argsize 的计算方法是,参数大小求和+返回值大小求和,例如入参是 3 个 int64 类型,返回值是 1 个 int64 类型,那么这里的 argsize = sizeof(int64) * 4。 + +不过真实世界永远没有我们假设的这么美好,函数参数往往混合了多种类型,还需要考虑内存对齐问题。 + +如果不确定自己的函数签名需要多大的 argsize,可以通过简单实现一个相同签名的空函数,然后 go tool objdump 来逆向查找应该分配多少空间。 + +### framesize + +函数的 framesize 就稍微复杂一些了,手写代码的 framesize 不需要考虑由编译器插入的 caller BP,要考虑: + +1. 局部变量,及其每个变量的 size。 +2. 在函数中是否有对其它函数调用时,如果有的话,调用时需要将 callee 的参数、返回值考虑在内。虽然 return address(rip)的值也是存储在 caller 的 stack frame 上的,但是这个过程是由 CALL 指令和 RET 指令完成 PC 寄存器的保存和恢复的,在手写汇编时,同样也是不需要考虑这个 PC 寄存器在栈上所需占用的 8 个字节的。 +3. 原则上来说,调用函数时只要不把局部变量覆盖掉就可以了。稍微多分配几个字节的 framesize 也不会死。 +4. 在确保逻辑没有问题的前提下,你愿意覆盖局部变量也没有问题。只要保证进入和退出汇编函数时的 caller 和 callee 能正确拿到返回值就可以。 + +## 地址运算 + +地址运算也是用 lea 指令,英文原意为 `Load Effective Address`,amd64 平台地址都是 8 个字节,所以直接就用 LEAQ 就好: + +```go +LEAQ (BX)(AX*8), CX +// 上面代码中的 8 代表 scale +// scale 只能是 0、2、4、8 +// 如果写成其它值: +// LEAQ (BX)(AX*3), CX +// ./a.s:6: bad scale: 3 + +// 用 LEAQ 的话,即使是两个寄存器值直接相加,也必须提供 scale +// 下面这样是不行的 +// LEAQ (BX)(AX), CX +// asm: asmidx: bad address 0/2064/2067 +// 正确的写法是 +LEAQ (BX)(AX*1), CX + + +// 在寄存器运算的基础上,可以加上额外的 offset +LEAQ 16(BX)(AX*1), CX + +// 三个寄存器做运算,还是别想了 +// LEAQ DX(BX)(AX*8), CX +// ./a.s:13: expected end of operand, found ( +``` + +使用 LEAQ 的好处也比较明显,可以节省指令数。如果用基本算术指令来实现 LEAQ 的功能,需要两~三条以上的计算指令才能实现 LEAQ 的完整功能。 + +## 示例 + +### add/sub/mul + +math.go: + +```go +package main + +import "fmt" + +func add(a, b int) int // 汇编函数声明 + +func sub(a, b int) int // 汇编函数声明 + +func mul(a, b int) int // 汇编函数声明 + +func main() { + fmt.Println(add(10, 11)) + fmt.Println(sub(99, 15)) + fmt.Println(mul(11, 12)) +} +``` + +math.s: + +```go +#include "textflag.h" // 因为我们声明函数用到了 NOSPLIT 这样的 flag,所以需要将 textflag.h 包含进来 + +// func add(a, b int) int +TEXT ·add(SB), NOSPLIT, $0-24 + MOVQ a+0(FP), AX // 参数 a + MOVQ b+8(FP), BX // 参数 b + ADDQ BX, AX // AX += BX + MOVQ AX, ret+16(FP) // 返回 + RET + +// func sub(a, b int) int +TEXT ·sub(SB), NOSPLIT, $0-24 + MOVQ a+0(FP), AX + MOVQ b+8(FP), BX + SUBQ BX, AX // AX -= BX + MOVQ AX, ret+16(FP) + RET + +// func mul(a, b int) int +TEXT ·mul(SB), NOSPLIT, $0-24 + MOVQ a+0(FP), AX + MOVQ b+8(FP), BX + IMULQ BX, AX // AX *= BX + MOVQ AX, ret+16(FP) + RET + // 最后一行的空行是必须的,否则可能报 unexpected EOF +``` + +把这两个文件放在任意目录下,执行 `go build` 并运行就可以看到效果了。 + +### 伪寄存器 SP 、伪寄存器 FP 和硬件寄存器 SP + +来写一段简单的代码证明伪 SP、伪 FP 和硬件 SP 的位置关系。 +spspfp.s: + +```go +#include "textflag.h" + +// func output(int) (int, int, int) +TEXT ·output(SB), $8-48 + MOVQ 24(SP), DX // 不带 symbol,这里的 SP 是硬件寄存器 SP + MOVQ DX, ret3+24(FP) // 第三个返回值 + MOVQ perhapsArg1+16(SP), BX // 当前函数栈大小 > 0,所以 FP 在 SP 的上方 16 字节处 + MOVQ BX, ret2+16(FP) // 第二个返回值 + MOVQ arg1+0(FP), AX + MOVQ AX, ret1+8(FP) // 第一个返回值 + RET + +``` + +spspfp.go: + +```go +package main + +import ( + "fmt" +) + +func output(int) (int, int, int) // 汇编函数声明 + +func main() { + a, b, c := output(987654321) + fmt.Println(a, b, c) +} +``` + +执行上面的代码,可以得到输出: + +```shell +987654321 987654321 987654321 +``` + +和代码结合思考,可以知道我们当前的栈结构是这样的: + +```shell +------ +ret2 (8 bytes) +------ +ret1 (8 bytes) +------ +ret0 (8 bytes) +------ +arg0 (8 bytes) +------ FP +ret addr (8 bytes) +------ +caller BP (8 bytes) +------ pseudo SP +frame content (8 bytes) +------ hardware SP +``` + +本小节例子的 framesize 是大于 0 的,读者可以尝试修改 framesize 为 0,然后调整代码中引用伪 SP 和硬件 SP 时的 offset,来研究 framesize 为 0 时,伪 FP,伪 SP 和硬件 SP 三者之间的相对位置。 + +本小节的例子是为了告诉大家,伪 SP 和伪 FP 的相对位置是会变化的,手写时不应该用伪 SP 和 >0 的 offset 来引用数据,否则结果可能会出乎你的预料。 + +### 汇编调用非汇编函数 + +output.s: + +```go +#include "textflag.h" + +// func output(a,b int) int +TEXT ·output(SB), NOSPLIT, $24-24 + MOVQ a+0(FP), DX // arg a + MOVQ DX, 0(SP) // arg x + MOVQ b+8(FP), CX // arg b + MOVQ CX, 8(SP) // arg y + CALL ·add(SB) // 在调用 add 之前,已经把参数都通过物理寄存器 SP 搬到了函数的栈顶 + MOVQ 16(SP), AX // add 函数会把返回值放在这个位置 + MOVQ AX, ret+16(FP) // return result + RET + +``` + +output.go: + +```go +package main + +import "fmt" + +func add(x, y int) int { + return x + y +} + +func output(a, b int) int + +func main() { + s := output(10, 13) + fmt.Println(s) +} + +``` + +### 汇编中的循环 + +通过 DECQ 和 JZ 结合,可以实现高级语言里的循环逻辑: + +sum.s: + +```go +#include "textflag.h" + +// func sum(sl []int64) int64 +TEXT ·sum(SB), NOSPLIT, $0-32 + MOVQ $0, SI + MOVQ sl+0(FP), BX // &sl[0], addr of the first elem + MOVQ sl+8(FP), CX // len(sl) + INCQ CX // CX++, 因为要循环 len 次 + +start: + DECQ CX // CX-- + JZ done + ADDQ (BX), SI // SI += *BX + ADDQ $8, BX // 指针移动 + JMP start + +done: + // 返回地址是 24 是怎么得来的呢? + // 可以通过 go tool compile -S math.go 得知 + // 在调用 sum 函数时,会传入三个值,分别为: + // slice 的首地址、slice 的 len, slice 的 cap + // 不过我们这里的求和只需要 len,但 cap 依然会占用参数的空间 + // 就是 16(FP) + MOVQ SI, ret+24(FP) + RET +``` + +sum.go: + +```go +package main + +func sum([]int64) int64 + +func main() { + println(sum([]int64{1, 2, 3, 4, 5})) +} +``` + +## 扩展话题 + +### 标准库中的一些数据结构 + +#### 数值类型 + +标准库中的数值类型很多: + +1. int/int8/int16/int32/int64 +2. uint/uint8/uint16/uint32/uint64 +3. float32/float64 +4. byte/rune +5. uintptr + +这些类型在汇编中就是一段存储着数据的连续内存,只是内存长度不一样,操作的时候看好数据长度就行。 + +#### slice + +前面的例子已经说过了,slice 在传递给函数的时候,实际上会展开成三个参数: + +1. 首元素地址 +2. slice 的 len +3. slice 的 cap + +在汇编中处理时,只要知道这个原则那就很好办了,按顺序还是按索引操作随你开心。 + +#### string + +```go +package main + +//go:noinline +func stringParam(s string) {} + +func main() { + var x = "abcc" + stringParam(x) +} +``` + +用 `go tool compile -S` 输出其汇编: + +```go +0x001d 00029 (stringParam.go:11) LEAQ go.string."abcc"(SB), AX // 获取 RODATA 段中的字符串地址 +0x0024 00036 (stringParam.go:11) MOVQ AX, (SP) // 将获取到的地址放在栈顶,作为第一个参数 +0x0028 00040 (stringParam.go:11) MOVQ $4, 8(SP) // 字符串长度作为第二个参数 +0x0031 00049 (stringParam.go:11) PCDATA $0, $0 // gc 相关 +0x0031 00049 (stringParam.go:11) CALL "".stringParam(SB) // 调用 stringParam 函数 +``` + +在汇编层面 string 就是地址 + 字符串长度。 + +#### struct + +struct 在汇编层面实际上就是一段连续内存,在作为参数传给函数时,会将其展开在 caller 的栈上传给对应的 callee: + +struct.go + +```go +package main + +type address struct { + lng int + lat int +} + +type person struct { + age int + height int + addr address +} + +func readStruct(p person) (int, int, int, int) + +func main() { + var p = person{ + age: 99, + height: 88, + addr: address{ + lng: 77, + lat: 66, + }, + } + a, b, c, d := readStruct(p) + println(a, b, c, d) +} +``` + +struct.s + +```go +#include "textflag.h" + +TEXT ·readStruct(SB), NOSPLIT, $0-64 + MOVQ arg0+0(FP), AX + MOVQ AX, ret0+32(FP) + MOVQ arg1+8(FP), AX + MOVQ AX, ret1+40(FP) + MOVQ arg2+16(FP), AX + MOVQ AX, ret2+48(FP) + MOVQ arg3+24(FP), AX + MOVQ AX, ret3+56(FP) + RET +``` + +上述的程序会输出 99, 88, 77, 66,这表明即使是内嵌结构体,在内存分布上依然是连续的。 + +#### map + +通过对下述文件进行汇编(go tool compile -S),我们可以得到一个 map 在对某个 key 赋值时所需要做的操作: + +m.go: + +```go +package main + +func main() { + var m = map[int]int{} + m[43] = 1 + var n = map[string]int{} + n["abc"] = 1 + println(m, n) +} +``` + +看一看第七行的输出: + +```go +0x0085 00133 (m.go:7) LEAQ type.map[int]int(SB), AX +0x008c 00140 (m.go:7) MOVQ AX, (SP) +0x0090 00144 (m.go:7) LEAQ ""..autotmp_2+232(SP), AX +0x0098 00152 (m.go:7) MOVQ AX, 8(SP) +0x009d 00157 (m.go:7) MOVQ $43, 16(SP) +0x00a6 00166 (m.go:7) PCDATA $0, $1 +0x00a6 00166 (m.go:7) CALL runtime.mapassign_fast64(SB) +0x00ab 00171 (m.go:7) MOVQ 24(SP), AX +0x00b0 00176 (m.go:7) MOVQ $1, (AX) +``` + +前面我们已经分析过调用函数的过程,这里前几行都是在准备 runtime.mapassign_fast64(SB) 的参数。去 runtime 里看看这个函数的签名: + +```go +func mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer { +``` + +不用看函数的实现我们也大概能推测出函数输入参数和输出参数的关系了,把入参和汇编指令对应的话: + +```go +t *maptype +=> +LEAQ type.map[int]int(SB), AX +MOVQ AX, (SP) + +h *hmap +=> +LEAQ ""..autotmp_2+232(SP), AX +MOVQ AX, 8(SP) + +key uint64 +=> +MOVQ $43, 16(SP) +``` + +返回参数就是 key 对应的可以写值的内存地址,拿到该地址后我们把想要写的值写进去就可以了: + +```go +MOVQ 24(SP), AX +MOVQ $1, (AX) +``` + +整个过程还挺复杂的,我们手抄一遍倒也可以实现。不过还要考虑,不同类型的 map,实际上需要执行的 runtime 中的 assign 函数是不同的,感兴趣的同学可以汇编本节的示例自行尝试。 + +整体来讲,用汇编来操作 map 并不是一个明智的选择。 + +#### channel + +channel 在 runtime 也是比较复杂的数据结构,如果在汇编层面操作,实际上也是调用 runtime 中 chan.go 中的函数,和 map 比较类似,这里就不展开说了。 + +### 获取 goroutine id + +Go 的 goroutine 是一个叫 g 的结构体,内部有自己的唯一 id,不过 runtime 没有把这个 id 暴露出来,但不知道为什么有很多人就是想把这个 id 得到。于是就有了各种或其 goroutine id 的库。 + +在 struct 一小节我们已经提到,结构体本身就是一段连续的内存,我们知道起始地址和字段的偏移量的话,很容易就可以把这段数据搬运出来: + +go_tls.h: + +```go +#ifdef GOARCH_arm +#define LR R14 +#endif + +#ifdef GOARCH_amd64 +#define get_tls(r) MOVQ TLS, r +#define g(r) 0(r)(TLS*1) +#endif + +#ifdef GOARCH_amd64p32 +#define get_tls(r) MOVL TLS, r +#define g(r) 0(r)(TLS*1) +#endif + +#ifdef GOARCH_386 +#define get_tls(r) MOVL TLS, r +#define g(r) 0(r)(TLS*1) +#endif +``` + +goid.go: + +```go +package goroutineid +import "runtime" +var offsetDict = map[string]int64{ + // ... 省略一些行 + "go1.7": 192, + "go1.7.1": 192, + "go1.7.2": 192, + "go1.7.3": 192, + "go1.7.4": 192, + "go1.7.5": 192, + "go1.7.6": 192, + // ... 省略一些行 +} + +var offset = offsetDict[runtime.Version()] + +// GetGoID returns the goroutine id +func GetGoID() int64 { + return getGoID(offset) +} + +func getGoID(off int64) int64 +``` + +goid.s: + +```go +#include "textflag.h" +#include "go_tls.h" + +// func getGoID() int64 +TEXT ·getGoID(SB), NOSPLIT, $0-16 + get_tls(CX) + MOVQ g(CX), AX + MOVQ offset(FP), BX + LEAQ 0(AX)(BX*1), DX + MOVQ (DX), AX + MOVQ AX, ret+8(FP) + RET +``` + +这样就实现了一个简单的获取 struct g 中的 goid 字段的小 library,作为玩具放在这里: +>https://github.com/cch123/goroutineid + +### SIMD + +[SIMD](https://cch123.gitbooks.io/duplicate/content/part3/performance/simd-instruction-class.html) 是 Single Instruction, Multiple Data 的缩写,在 Intel 平台上的 SIMD 指令集先后为 SSE,AVX,AVX2,AVX512,这些指令集引入了标准以外的指令,和宽度更大的寄存器,例如: + +- 128 位的 XMM0~XMM31 寄存器。 +- 256 位的 YMM0~YMM31 寄存器。 +- 512 位的 ZMM0~ZMM31 寄存器。 + +这些寄存器的关系,类似 RAX,EAX,AX 之间的关系。指令方面可以同时对多组数据进行移动或者计算,例如: + +- movups : 把4个不对准的单精度值传送到xmm寄存器或者内存 +- movaps : 把4个对准的单精度值传送到xmm寄存器或者内存 + +上述指令,当我们将数组作为函数的入参时有很大概率会看到,例如: + +arr_par.go: + +```go +package main + +import "fmt" + +func pr(input [3]int) { + fmt.Println(input) +} + +func main() { + pr([3]int{1, 2, 3}) +} +``` + +go compile -S: + +```go +0x001d 00029 (arr_par.go:10) MOVQ "".statictmp_0(SB), AX +0x0024 00036 (arr_par.go:10) MOVQ AX, (SP) +0x0028 00040 (arr_par.go:10) MOVUPS "".statictmp_0+8(SB), X0 +0x002f 00047 (arr_par.go:10) MOVUPS X0, 8(SP) +0x0034 00052 (arr_par.go:10) CALL "".pr(SB) +``` + +可见,编译器在某些情况下已经考虑到了性能问题,帮助我们使用 SIMD 指令集来对数据搬运进行了优化。 + +因为 SIMD 这个话题本身比较广,这里就不展开细说了。 + +## 特别感谢 + +研究过程基本碰到不太明白的都去骚扰卓巨巨了,就是这位 https://mzh.io/ 大大。特别感谢他,给了不少线索和提示。 + +## 参考资料 + +1. https://quasilyte.github.io/blog/post/go-asm-complementary-reference/#external-resources +2. http://davidwong.fr/goasm +3. https://www.doxsey.net/blog/go-and-assembly +4. https://github.com/golang/go/files/447163/GoFunctionsInAssembly.pdf +5. https://golang.org/doc/asm + +参考资料[4]需要特别注意,在该 slide 中给出的 callee stack frame 中把 caller 的 return address 也包含进去了,个人认为不是很合适。 + + diff --git a/site/content/docs/assembly/exercises.md b/site/content/docs/assembly/exercises.md new file mode 100644 index 0000000..3c16b05 --- /dev/null +++ b/site/content/docs/assembly/exercises.md @@ -0,0 +1,7 @@ +--- +title: 习题 +weight: 2 +draft: true +--- + +# 习题 diff --git a/site/content/docs/bootstrap/_index.md b/site/content/docs/bootstrap/_index.md new file mode 100644 index 0000000..3832726 --- /dev/null +++ b/site/content/docs/bootstrap/_index.md @@ -0,0 +1,7 @@ +--- +title: Go 进程的启动流程 +weight: 2 +bookCollapseSection: true +draft: true +--- + diff --git a/site/content/docs/bootstrap/boot.md b/site/content/docs/bootstrap/boot.md new file mode 100644 index 0000000..a8fe587 --- /dev/null +++ b/site/content/docs/bootstrap/boot.md @@ -0,0 +1,7 @@ +--- +title: 启动流程 +weight: 2 +--- +# 启动流程 + + diff --git a/site/content/docs/bootstrap/elf.md b/site/content/docs/bootstrap/elf.md new file mode 100644 index 0000000..15a78a7 --- /dev/null +++ b/site/content/docs/bootstrap/elf.md @@ -0,0 +1 @@ +# elf 文件简介 diff --git a/site/content/docs/bootstrap/exercises.md b/site/content/docs/bootstrap/exercises.md new file mode 100644 index 0000000..a49ba48 --- /dev/null +++ b/site/content/docs/bootstrap/exercises.md @@ -0,0 +1,2 @@ +--- +--- \ No newline at end of file diff --git a/site/content/docs/compiler_and_linker/_index.md b/site/content/docs/compiler_and_linker/_index.md new file mode 100644 index 0000000..ce63fe2 --- /dev/null +++ b/site/content/docs/compiler_and_linker/_index.md @@ -0,0 +1,6 @@ +--- +title: 编译器与链接器 +weight: 3 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/compiler_and_linker/calling_convention.md b/site/content/docs/compiler_and_linker/calling_convention.md new file mode 100644 index 0000000..e0609f0 --- /dev/null +++ b/site/content/docs/compiler_and_linker/calling_convention.md @@ -0,0 +1,25 @@ +--- +title: 调用规约 +weight: 3 +draft: true +--- + +# 调用规约 + +早期版本的 Go 在向函数传递参数时,是使用栈的。 + +从 1.17 开始,调用函数时会尽量使用寄存器传参。 + +"".add STEXT size=121 args=0x10 locals=0x18 funcid=0x0 + 0x0000 00000 (add.go:8) TEXT "".add(SB), ABIInternal, $24-16 + 0x0000 00000 (add.go:8) CMPQ SP, 16(R14) + 0x0004 00004 (add.go:8) JLS 94 + 0x0006 00006 (add.go:8) SUBQ $24, SP + 0x005d 00093 (add.go:11) RET + 0x005e 00094 (add.go:11) NOP + 0x005e 00094 (add.go:8) MOVQ AX, 8(SP) + 0x0063 00099 (add.go:8) MOVQ BX, 16(SP) + 0x0068 00104 (add.go:8) CALL runtime.morestack_noctxt(SB) + 0x006d 00109 (add.go:8) MOVQ 8(SP), AX + 0x0072 00114 (add.go:8) MOVQ 16(SP), BX + 0x0077 00119 (add.go:8) JMP 0 diff --git a/site/content/docs/compiler_and_linker/compiler.md b/site/content/docs/compiler_and_linker/compiler.md new file mode 100644 index 0000000..6592c8e --- /dev/null +++ b/site/content/docs/compiler_and_linker/compiler.md @@ -0,0 +1,6 @@ +--- +title: 编译器 +weight: 3 +draft: true +--- +# 编译器 diff --git a/site/content/docs/compiler_and_linker/linker.md b/site/content/docs/compiler_and_linker/linker.md new file mode 100644 index 0000000..ea2d801 --- /dev/null +++ b/site/content/docs/compiler_and_linker/linker.md @@ -0,0 +1,6 @@ +--- +title: 链接器 +weight: 3 +draft: true +--- +# linker diff --git a/site/content/docs/data_structure/_index.md b/site/content/docs/data_structure/_index.md new file mode 100644 index 0000000..08b7aa5 --- /dev/null +++ b/site/content/docs/data_structure/_index.md @@ -0,0 +1,5 @@ +--- +title: 内置数据结构 +weight: 10 +bookCollapseSection: true +--- diff --git a/site/content/docs/data_structure/channel.md b/site/content/docs/data_structure/channel.md new file mode 100644 index 0000000..da9c943 --- /dev/null +++ b/site/content/docs/data_structure/channel.md @@ -0,0 +1,10 @@ +--- +title: channel +weight: 10 +bookCollapseSection: false +--- +# channel 实现 + +{{}} + +{{}} diff --git a/site/content/docs/data_structure/context.md b/site/content/docs/data_structure/context.md new file mode 100644 index 0000000..ed1e832 --- /dev/null +++ b/site/content/docs/data_structure/context.md @@ -0,0 +1,7 @@ +--- +title: 内置数据结构 +weight: 10 +bookCollapseSection: true +draft: true +--- +# context 的实现 diff --git a/site/content/docs/data_structure/interface.md b/site/content/docs/data_structure/interface.md new file mode 100644 index 0000000..be9c333 --- /dev/null +++ b/site/content/docs/data_structure/interface.md @@ -0,0 +1,7 @@ +--- +title: 内置数据结构 +weight: 10 +bookCollapseSection: true +draft: true +--- +# interface 实现 diff --git a/site/content/docs/data_structure/map.md b/site/content/docs/data_structure/map.md new file mode 100644 index 0000000..61f68ea --- /dev/null +++ b/site/content/docs/data_structure/map.md @@ -0,0 +1,27 @@ +--- +title: map +weight: 10 +bookCollapseSection: false +--- + +# map 实现 + +![map struct](/images/runtime/data_struct/map.png) + +## map 元素定位过程 + +![top hash](/images/runtime/data_struct/map_tophash.png) + +## 特权函数 + +![func translate](/images/runtime/data_struct/map_function_translate.png) + +## 函数翻译 + +![func translate](/images/runtime/data_struct/map_func_translate2.png) + +## map 遍历过程 + +{{}} + +{{}} diff --git a/site/content/docs/data_structure/semaphore.md b/site/content/docs/data_structure/semaphore.md new file mode 100644 index 0000000..73af24b --- /dev/null +++ b/site/content/docs/data_structure/semaphore.md @@ -0,0 +1,9 @@ +--- +title: 内置数据结构 +weight: 10 +bookCollapseSection: true +draft: true +--- + +# 信号量的实现 + diff --git a/site/content/docs/data_structure/string.md b/site/content/docs/data_structure/string.md new file mode 100644 index 0000000..49eee42 --- /dev/null +++ b/site/content/docs/data_structure/string.md @@ -0,0 +1,7 @@ +--- +title: 内置数据结构 +weight: 10 +bookCollapseSection: true +draft: true +--- +# string 不可变字符串 diff --git a/site/content/docs/data_structure/timer.md b/site/content/docs/data_structure/timer.md new file mode 100644 index 0000000..907fa4e --- /dev/null +++ b/site/content/docs/data_structure/timer.md @@ -0,0 +1,6 @@ +--- +title: 内置数据结构 +weight: 10 +bookCollapseSection: true +draft: true +--- \ No newline at end of file diff --git a/site/content/docs/ddd/_index.md b/site/content/docs/ddd/_index.md new file mode 100644 index 0000000..e28e493 --- /dev/null +++ b/site/content/docs/ddd/_index.md @@ -0,0 +1,6 @@ +--- +title: 领域驱动设计 +weight: 2 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/debug/_index.md b/site/content/docs/debug/_index.md new file mode 100644 index 0000000..a3feaba --- /dev/null +++ b/site/content/docs/debug/_index.md @@ -0,0 +1,6 @@ +--- +title: 调试工具 +weight: 5 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/debug/dlv_tutorial.md b/site/content/docs/debug/dlv_tutorial.md new file mode 100644 index 0000000..ae66d9d --- /dev/null +++ b/site/content/docs/debug/dlv_tutorial.md @@ -0,0 +1 @@ +# dlv tutorial diff --git a/site/content/docs/debug/ssa_debug.md b/site/content/docs/debug/ssa_debug.md new file mode 100644 index 0000000..3e5bba3 --- /dev/null +++ b/site/content/docs/debug/ssa_debug.md @@ -0,0 +1 @@ +# debug ssa func diff --git a/site/content/docs/design_patterns/_index.md b/site/content/docs/design_patterns/_index.md new file mode 100644 index 0000000..b45eae5 --- /dev/null +++ b/site/content/docs/design_patterns/_index.md @@ -0,0 +1,6 @@ +--- +title: 设计模式 +weight: 6 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/design_patterns/decorator.md b/site/content/docs/design_patterns/decorator.md new file mode 100644 index 0000000..7fad81a --- /dev/null +++ b/site/content/docs/design_patterns/decorator.md @@ -0,0 +1 @@ +# 装饰器模式 diff --git a/site/content/docs/optimization/_index.md b/site/content/docs/optimization/_index.md new file mode 100644 index 0000000..ff520c9 --- /dev/null +++ b/site/content/docs/optimization/_index.md @@ -0,0 +1,6 @@ +--- +title: 性能优化 +weight: 8 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/optimization/benchmark.md b/site/content/docs/optimization/benchmark.md new file mode 100644 index 0000000..58abd58 --- /dev/null +++ b/site/content/docs/optimization/benchmark.md @@ -0,0 +1 @@ +# benchmark 入门 diff --git a/site/content/docs/optimization/continuous_profiling.md b/site/content/docs/optimization/continuous_profiling.md new file mode 100644 index 0000000..ef9e56f --- /dev/null +++ b/site/content/docs/optimization/continuous_profiling.md @@ -0,0 +1 @@ +# continuous profiling diff --git a/site/content/docs/optimization/optimizations.md b/site/content/docs/optimization/optimizations.md new file mode 100644 index 0000000..c6ae860 --- /dev/null +++ b/site/content/docs/optimization/optimizations.md @@ -0,0 +1 @@ +# 性能优化 diff --git a/site/content/docs/optimization/pprof_tutorial.md b/site/content/docs/optimization/pprof_tutorial.md new file mode 100644 index 0000000..43a898b --- /dev/null +++ b/site/content/docs/optimization/pprof_tutorial.md @@ -0,0 +1 @@ +# pprof 指南 diff --git a/site/content/docs/quality/_index.md b/site/content/docs/quality/_index.md new file mode 100644 index 0000000..d6f49c9 --- /dev/null +++ b/site/content/docs/quality/_index.md @@ -0,0 +1,5 @@ +--- +title: 工程质量管控 +weight: 20 +bookCollapseSection: true +--- diff --git a/site/content/docs/quality/auto_review.md b/site/content/docs/quality/auto_review.md new file mode 100644 index 0000000..83193f6 --- /dev/null +++ b/site/content/docs/quality/auto_review.md @@ -0,0 +1,9 @@ +--- +title: 自动化 Code Review +weight: 1 +draft: true +--- + +# 自动化 Code Review + +## reviewdog diff --git a/site/content/docs/quality/custom_linter.md b/site/content/docs/quality/custom_linter.md new file mode 100644 index 0000000..7bd48f9 --- /dev/null +++ b/site/content/docs/quality/custom_linter.md @@ -0,0 +1,5 @@ +--- +title: 定制 linter +weight: 1 +draft: true +--- \ No newline at end of file diff --git a/site/content/docs/quality/performance.md b/site/content/docs/quality/performance.md new file mode 100644 index 0000000..7d7e022 --- /dev/null +++ b/site/content/docs/quality/performance.md @@ -0,0 +1,360 @@ +## 为什么要做优化 + +这是一个速度决定一切的年代,只要我们的生活还在继续数字化,线下的流程与系统就在持续向线上转移,在这个转移过程中,我们会碰到持续的性能问题。 + +互联网公司本质是将用户共通的行为流程进行了集中化管理,通过中心化的信息交换达到效率提升的目的,同时用规模效应降低了数据交换的成本。 + +用人话来讲,公司希望的是用尽量少的机器成本来赚取尽量多的利润。利润的提升与业务逻辑本身相关,与技术关系不大。而降低成本则是与业务无关,纯粹的技术话题。这里面最重要的主题就是“性能优化”。 + +如果业务的后端服务规模足够大,那么一个程序员通过优化帮公司节省的成本,或许就可以负担他十年的工资了。 + +## 优化的前置知识 + +从资源视角出发来对一台服务器进行审视的话,CPU、内存、磁盘与网络是后端服务最需要关注的四种资源类型。 + +对于计算密集型的程序来说,优化的主要精力会放在 CPU 上,要知道 CPU 基本的流水线概念,知道怎么样在使用少的 CPU 资源的情况下,达到相同的计算目标。 + +对于 IO 密集型的程序(后端服务一般都是 IO 密集型)来说,优化可以是降低程序的服务延迟,也可以是提升系统整体的吞吐量。 + +IO 密集型应用主要与磁盘、内存、网络打交道。因此我们需要知道一些基本的与磁盘、内存、网络相关的基本数据与常见概念: + +- 要了解内存的多级存储结构:L1,L2,L3,主存。还要知道这些不同层级的存储操作时的大致延迟:[latency numbers every programmer should know](https://colin-scott.github.io/personal_website/research/interactive_latency.html)。 +- 要知道基本的文件系统读写 syscall,批量 syscall,数据同步 syscall。 +- 要熟悉项目中使用的网络协议,至少要对 TCP, HTTP 有所了解。 + +## 优化越靠近应用层效果越好 + +> Performance tuning is most effective when done closest to where the work is performed. For workloads driven by applications, this means within the application itself. + +我们在应用层的逻辑优化能够帮助应用提升几十倍的性能,而最底层的优化可能也就只能提升几个百分点了。 + +这个很好理解,我们可以看到一个 GTA Online 的新闻:[rockstar thanks gta online player who fixed poor load times](https://www.pcgamer.com/rockstar-thanks-gta-online-player-who-fixed-poor-load-times-official-update-coming/)。 + +简单来说,GTA online 的游戏启动过程让玩家等待时间过于漫长,经过各种工具分析,发现一个 10M 的文件加载就需要几十秒,用户 diy 进行优化之后,将加载时间减少 70%,并分享出来:[how I cut GTA Online loading times by 70%](https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/)。 + +这就是一个非常典型的案例,GTA 在商业上取得了巨大的成功,但不妨碍它局部的代码是一坨屎。我们只要把这里的重复逻辑干掉,就可以完成三倍的优化效果。同样的案例,如果我们去优化磁盘的读写速度,则可能收效甚微。 + +## 优化是与业务场景相关的 + +不同的业务场景优化的侧重也是不同的。 + +对于大多数无状态业务模块来说,内存一般不是瓶颈,所以业务 API 的优化主要聚焦于延迟和吞吐。对于网关类的应用,因为有海量的连接,除了延迟和吞吐,内存占用可能就会成为一个关注的重点。对于存储类应用,内存是个逃不掉的瓶颈点。 + +在关注一些性能优化文章时,我们也应特别留意作者的业务场景。场景的侧重可能会让某些人去选择使用更为 hack 的手段进行优化,而 hack 往往也就意味着 bug。如果你选择了少有人走过的路,那你未来要面临的也是少有人会碰到的 bug。解决起来令人头疼。 + +## 优化的工作流程 + +对于一个典型的 API 应用来说,优化工作基本遵从下面的工作流: + +1. 建立评估指标,例如固定 QPS 压力下的延迟或内存占用,或模块在满足 SLA 前提下的极限 QPS +2. 通过自研、开源压测工具进行压测,直到模块无法满足预设性能要求:如大量超时,QPS 不达预期,OOM +3. 通过内置 profile 工具寻找性能瓶颈 +4. 本地 benchmark 证明优化效果 +5. 集成 patch 到业务模块,回到 2 + +## 可以使用的工具 + +### pprof + +#### memory profiler + +Go 内置的内存 profiler 可以让我们对线上系统进行内存使用采样,有四个相应的指标: + +- inuse\_objects:当我们认为内存中的驻留对象过多时,就会关注该指标 +- inuse\_space:当我们认为应用程序占据的 RSS 过大时,会关注该指标 +- alloc\_objects:当应用曾经发生过历史上的大量内存分配行为导致 CPU 或内存使用大幅上升时,可能关注该指标 +- alloc\_space:当应用历史上发生过内存使用大量上升时,会关注该指标 + +网关类应用因为海量连接的关系,会导致进程消耗大量内存,所以我们经常看到相关的优化文章,主要就是降低应用的 inuse\_space。 + +而两个对象数指标主要是为 GC 优化提供依据,当我们进行 GC 调优时,会同时关注应用分配的对象数、正在使用的对象数,以及 GC 的 CPU 占用的指标。 + +GC 的 CPU 占用情况可以由内置的 CPU profiler 得到。 + +#### cpu profiler + +> The builtin Go CPU profiler uses the setitimer(2) system call to ask the operating system to be sent a SIGPROF signal 100 times a second. Each signal stops the Go process and gets delivered to a random thread’s sigtrampgo() function. This function then proceeds to call sigprof() or sigprofNonGo() to record the thread’s current stack. + +Go 语言内置的 CPU profiler 使用 setitimer 系统调用,操作系统会每秒 100 次向程序发送 SIGPROF 信号。在 Go 进程中会选择随机的信号执行 sigtrampgo 函数。该函数使用 sigprof 或 sigprofNonGo 来记录线程当前的栈。 + +> Since Go uses non-blocking I/O, Goroutines that wait on I/O are parked and not running on any threads. Therefore they end up being largely invisible to Go’s builtin CPU profiler. + +Go 语言内置的 cpu profiler 是在性能领域比较常见的 On-CPU profiler,对于瓶颈主要在 CPU 消耗的应用,我们使用内置的 profiler 也就足够了。 + +如果我们碰到的问题是应用的 CPU 使用不高,但接口的延迟却很大,那么就需要用上 Off-CPU profiler,遗憾的是官方的 profiler 并未提供该功能,我们需要借助社区的 fgprof。 + +### fgprof + +> fgprof is implemented as a background goroutine that wakes up 99 times per second and calls runtime.GoroutineProfile. This returns a list of all goroutines regardless of their current On/Off CPU scheduling status and their call stacks. + +fgprof 是启动了一个后台的 goroutine,每秒启动 99 次,调用 runtime.GoroutineProfile 来采集所有 gorooutine 的栈。 + +虽然看起来很美好: + +``` +func GoroutineProfile(p []StackRecord) (n int, ok bool) { + ..... +stopTheWorld("profile") + +for _, gp1 := range allgs { +...... +} + +if n <= len(p) { +// Save current goroutine. +........ +systemstack(func() { +saveg(pc, sp, gp, &r[0]) +}) + +// Save other goroutines. +for _, gp1 := range allgs { +if isOK(gp1) { +....... +saveg(^uintptr(0), ^uintptr(0), gp1, &r[0]) + ....... +} +} +} + +startTheWorld() + +return n, ok +} +``` + +但调用 GoroutineProfile 函数的开销并不低,如果线上系统的 goroutine 上万,每次采集 profile 都遍历上万个 goroutine 的成本实在是太高了。所以 fgprof 只适合在测试环境中使用。 + +### trace + +一般情况下我们是不需要使用 trace 来定位性能问题的,通过压测 + profile 就可以解决大部分问题,除非我们的问题与 runtime 本身的问题相关。 + +比如 STW 时间比预想中长,超过百毫秒,向官方反馈问题时,才需要出具相关的 trace 文件。比如类似 [long stw](https://github.com/golang/go/issues/19378) 这样的 issue。 + +采集 trace 对系统的性能影响还是比较大的,即使我们只是开启 gctrace,把 gctrace 日志重定向到文件,对系统延迟也会有一定影响,因为 gctrace 的 print 是在 stw 期间来做的:[gc trace 阻塞调度](http://xiaorui.cc/archives/6232)。 + +### perf + +如果应用没有开启 pprof,在线上应急时,我们也可以临时使用 perf: + +![perf demo](https://cch123.github.io/perf_opt/perf.png) + +## 微观性能优化 + +在编写 library 时,我们会关注关键的函数性能,这时可以脱离系统去探讨性能优化,Go 语言的 test 子命令集成了相关的功能,只要我们按照约定来写 Benchmark 前缀的测试函数,就可以实现函数级的基准测试。我们以常见的二维数组遍历为例: + +``` +package main + +import "testing" + +var x = make([][]int, 100) + +func init() { + for i := 0; i < 100; i++ { + x[i] = make([]int, 100) + } +} + +func traverseVertical() { + for i := 0; i < 100; i++ { + for j := 0; j < 100; j++ { + x[j][i] = 1 + } + } +} + +func traverseHorizontal() { + for i := 0; i < 100; i++ { + for j := 0; j < 100; j++ { + x[i][j] = 1 + } + } +} + +func BenchmarkHorizontal(b *testing.B) { + for i := 0; i < b.N; i++ { + traverseHorizontal() + } +} + +func BenchmarkVertical(b *testing.B) { + for i := 0; i < b.N; i++ { + traverseVertical() + } +} + +``` + +执行 `go test -bench=.` + +``` +BenchmarkHorizontal-12 102368 10916 ns/op +BenchmarkVertical-12 66612 18197 ns/op +``` + +可见横向遍历数组要快得多,这提醒我们在写代码时要考虑 CPU 的 cache 设计及局部性原理,以使程序能够在相同的逻辑下获得更好的性能。 + +除了 CPU 优化,我们还经常会碰到要优化内存分配的场景。只要带上 -benchmem 的 flag 就可以实现了。 + +举个例子,形如下面这样的代码: + +``` +logStr := "userid :" + userID + "; orderid:" + orderID +``` + +你觉得代码写的很难看,想要优化一下可读性,就改成了下列代码: + +``` +logStr := fmt.Sprintf("userid: %v; orderid: %v", userID, orderID) +``` + +这样的修改方式在某公司的系统中曾经导致了 p2 事故,上线后接口的超时俱增至 SLA 承诺以上。 + +我们简单验证就可以发现: + +``` +BenchmarkPrint-12 7168467 157 ns/op 64 B/op 3 allocs/op +BenchmarkPlus-12 43278558 26.7 ns/op 0 B/op 0 allocs/op +``` + +使用 + 进行字符串拼接,不会在堆上产生额外对象。而使用 fmt 系列函数,则会造成局部对象逃逸到堆上,这里是高频路径上有大量逃逸,所以导致线上服务的 GC 压力加重,大量接口超时。 + +出于谨慎考虑,修改高并发接口时,拿不准的尽量都应进行简单的线下 benchmark 测试。 + +当然,我们不能指望靠写一大堆 benchmark 帮我们发现系统的瓶颈。 + +实际工作中还是要使用前文提到的优化工作流来进行系统性能优化。也就是尽量从接口整体而非函数局部考虑去发现与解决瓶颈。 + +## 宏观性能优化 + +接口类的服务,我们可以使用两种方式对其进行压测: + +- 固定 QPS 压测:在每次系统有大的特性发布时,都应进行固定 QPS 压测,与历史版本进行对比,需要关注的指标包括,相同 QPS 下的系统的 CPU 使用情况,内存占用情况(监控中的 RSS 值),goroutine 数,GC 触发频率和相关指标(是否有较长的 stw,mark 阶段是否时间较长等),平均延迟,p99 延迟。 +- 极限 QPS 压测:极限 QPS 压测一般只是为了 benchmark show,没有太大意义。系统满负荷时,基本 p99 已经超出正常用户的忍受范围了。 + +压测过程中需要采集不同 QPS 下的 CPU profile,内存 profile,记录 goroutine 数。与历史情况进行 AB 对比。 + +Go 的 pprof 还提供了 --base 的 flag,能够很直观地帮我们发现不同版本之间的指标差异:[用 pprof 比较内存使用差异](https://colobu.com/2019/08/20/use-pprof-to-compare-go-memory-usage/)。 + +总之记住一点,接口的性能一定是通过压测来进行优化的,而不是通过硬啃代码找瓶颈点。关键路径的简单修改往往可以带来巨大收益。如果只是啃代码,很有可能将 1% 优化到 0%,优化了 100% 的局部性能,对接口整体影响微乎其微。 + +## 寻找性能瓶颈 + +在压测时,我们通过以下步骤来逐渐提升接口的整体性能: + +1. 使用固定 QPS 压测,以阶梯形式逐渐增加压测 QPS,如 1000 -> 每分钟增加 1000 QPS +2. 压测过程中观察系统的延迟是否异常 +3. 观察系统的 CPU 使用情况 +4. 如果 CPU 使用率在达到一定值之后不再上升,反而引起了延迟的剧烈波动,这时大概率是发生了阻塞,进入 pprof 的 web 页面,点击 goroutine,查看 top 的 goroutine 数,这时应该有大量的 goroutine 阻塞在某处,比如 Semacquire +5. 如果 CPU 上升较快,未达到预期吞吐就已经过了高水位,则可以重点考察 CPU 使用是否合理,在 CPU 高水位进行 profile 采样,重点关注火焰图中较宽的“平顶山” + +## 一些优化案例 + +### gc mark 占用过多 CPU + +在 Go 语言中 gc mark 占用的 CPU 主要和运行时的对象数相关,也就是我们需要看 inuse\_objects。 + +定时任务,或访问流量不规律的应用,需要关注 alloc\_objects。 + +优化主要是下面几方面: + +#### 减少变量逃逸 + +尽量在栈上分配对象,关于逃逸的规则,可以查看 Go 编译器代码中的逃逸测试部分: + +![Pasted-Graphic](http://xargin.com/content/images/2021/03/Pasted-Graphic.png) + +查看某个 package 内的逃逸情况,可以使用 build + 全路径的方式,如: + +`go build -gcflags="-m -m" github.com/cch123/elasticsql` + +需要注意的是,逃逸分析的结果是会**随着版本变化**的,所以去背诵网上逃逸相关的文章结论是没有什么意义的。 + +#### 使用 sync.Pool 复用堆上对象 + +sync.Pool 用出花儿的就是 fasthttp 了,可以看看我之前写的这一篇:[fasthttp 为什么快](http://xargin.com/why-fasthttp-is-fast-and-the-cost-of-it/)。 + +最简单的复用就是复用各种 struct,slice,在复用时 put 时,需要 size 是否已经扩容过头,小心因为 sync.Pool 中存了大量的巨型对象导致进程占用了大量内存。 + +#### 修改 GOGC + +当前有 memory ballast 和动态 GOGC 两种方案: +1. [memory ballast](https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/) +2. [GOGCTuner](https://github.com/cch123/gogctuner) + +后者可以根据 gc cycle 动态调整 GOGC,使应用占用的内存水位始终保持在 70%,既不 OOM,又能合理利用内存空间来降低 GC 触发频率。 + +### 调度占用过多 CPU + +goroutine 频繁创建与销毁会给调度造成较大的负担,如果我们发现 CPU 火焰图中 schedule,findrunnable 占用了大量 CPU,那么可以考虑使用开源的 workerpool 来进行改进,比较典型的 [fasthttp worker pool](https://github.com/valyala/fasthttp/blob/master/workerpool.go#L19)。 + +如果客户端与服务端之间使用的是短连接,那么我们可以使用长连接。 + +### 进程占用大量内存 + +当前大多数的业务后端服务是不太需要关注进程消耗的内存的。 + +我们经常看到做 Go 内存占用优化的是在网关(包括 mesh)、存储系统这两个场景。 + +对于网关类系统来说,Go 的内存占用主要是因为 Go 独特的抽象模型造成的,这个很好理解: + +![Pasted-Graphic-1](http://xargin.com/content/images/2021/03/Pasted-Graphic-1.png) + +海量的连接加上海量的 goroutine,使网关和 mesh 成为 Go OOM 的重灾区。所以网关侧的优化一般就是优化: + +- goroutine 占用的栈内存 +- read buffer 和 write buffer 占用的内存 + +很多项目都有相关的分享,这里就不再赘述了。 + +对于存储类系统来说,内存占用方面的努力也是在优化 buffer,比如 dgraph 使用 cgo + jemalloc 来优化他们的产品[内存占用](https://dgraph.io/blog/post/manual-memory-management-golang-jemalloc/)。 + +堆外内存不会在 Go 的 GC 系统里进行管辖,所以也不会影响到 Go 的 GC Heap Goal,所以也不会像 Go 这样内存占用难以控制。 + +### 锁冲突严重,导致吞吐量瓶颈 + +我在 [几个 Go 系统可能遇到的锁问题](http://xargin.com/lock-contention-in-go/) 中分享过实际的线上 case。 + +进行锁优化的思路无非就一个“拆”和一个“缩”字: + +- 拆:将锁粒度进行拆分,比如全局锁,我能不能把锁粒度拆分为连接粒度的锁;如果是连接粒度的锁,那我能不能拆分为请求粒度的锁;在 logger fd 或 net fd 上加的锁不太好拆,那么我们增加一些客户端,比如从 1-> 100,降低锁的冲突是不是就可以了。 +- 缩:缩小锁的临界区,比如业务允许的前提下,可以把 syscall 移到锁外面;比如我们只是想要锁 map,但是却不小心锁了连接读写的逻辑,或许简单地用 sync.Map 来代替 map Lock,defer Unlock 就能简单地缩小临界区了。 + +### timer 相关函数占用大量 CPU + +同样是在某些网关应用中较常见,优化方法手段: + +- 使用时间轮/粗粒度的时间管理,精确到 ms 级一般就足够了 +- 升级到 Go 1.14+,享受官方的升级红利 + +## 模拟真实工作负载 + +在前面的论述中,我们对问题进行了简化。真实世界中的后端系统往往不只一个接口,压测工具、平台往往只支持单接口压测。 + +公司的业务希望知道的是某个后端系统最终能支持多少业务量,例如系统整体能承载多少发单量而不会在重点环节出现崩溃。 + +虽然大家都在讲微服务,但单一服务往往也不只有单一功能,如果一个系统有 10 个接口(已经算是很小的服务了),那么这个服务的真实负载是很难靠人肉去模拟的。 + +这也就是为什么互联网公司普遍都需要做全链路压测。像样点的公司都会定期进行全链路压测演练,以便知晓随着系统快速迭代变化,系统整体是否出现了严重的性能衰退。 + +通过真实的工作负载,我们才能发现真实的线上性能问题。讲全链路压测的文章也很多,本文就不再赘述了。 + +## 当前性能问题定位工具的局限性 + +本文中几乎所有优化手段都是通过 Benchmark 和压测来进行的,但真实世界的软件会有下列场景: + +- 做 ToB 生意,我们的应用是部署在客户侧(比如一些数据库产品),客户说我们的应用会 OOM,但是我们很难拿到 OOM 的现场,不知道到底是哪些对象分配导致了 OOM +- 做大型平台,平台上有各种不同类型的用户编写代码,升级用户代码后,线上出现各种 CPU 毛刺和 OOM 问题 + +这些问题在压测中是发现不了的,需要有更为灵活的工具和更为强大的平台,关于这些问题,我将在 4 月 10 日的武汉 Gopher Meetup 上进行分享,欢迎关注。 + +参考资料: + +[cache contention](https://web.eecs.umich.edu/~zmao/Papers/xu10mar.pdf) + +[every-programmer-should-know](https://github.com/mtdvio/every-programmer-should-know) + +[go-perfbook](https://github.com/dgryski/go-perfbook) + +[Systems Performance](https://www.amazon.com/Systems-Performance-Brendan-Gregg/dp/0136820158/ref=sr_1_1?dchild=1&keywords=systems+performance&qid=1617092159&sr=8-1) diff --git a/site/content/docs/quality/refactoring.md b/site/content/docs/quality/refactoring.md new file mode 100644 index 0000000..d556488 --- /dev/null +++ b/site/content/docs/quality/refactoring.md @@ -0,0 +1,5 @@ +--- +title: 整洁代码 +weight: 1 +draft: true +--- diff --git a/site/content/docs/quality/static_analysis.md b/site/content/docs/quality/static_analysis.md new file mode 100644 index 0000000..21c2184 --- /dev/null +++ b/site/content/docs/quality/static_analysis.md @@ -0,0 +1,277 @@ +--- +title: 静态分析 +weight: 1 +--- + +# 静态分析 + +静态分析是通过扫描并解析用户代码,寻找代码中的潜在 bug 的一种手段。 + +静态分析一般会集成在项目上线的 CI 流程中,如果分析过程找到了 bug,会直接阻断上线,避免有问题的代码被部署到线上系统。从而在部署早期发现并修正潜在的问题。 + +## 社区常见 linter + +时至今日,社区已经有了丰富的 linter 资源供我们使用,本文会挑出一些常见 linter 进行说明。 + +### go lint + +go lint 是官方出的 linter,是 Go 语言最早期的 linter 了,其可以检查: + +* 导出函数是否有注释 +* 变量、函数、包命名不符合 Go 规范,有下划线 +* receiver 命名是否不符合规范 + +但这几年社区的 linter 蓬勃发展,所以这个项目也被官方 deprecated 掉了。其主要功能被另外一个 linter:revive[^1] 完全继承了。 + +### go vet + +go vet 也是官方提供的静态分析工具,其内置了锁拷贝检查、循环变量捕获问题、printf 参数不匹配等工具。 + +比如新手老手都很容易犯的 loop capture 错误: + +```go +package main + +func main() { + var a = map[int]int {1 : 1, 2: 3} + var b = map[int]*int{} + for k, r := range a { + go func() { + b[k] = &r + }() + } +} +``` + +go vet 会直接把你骂醒: + +```shell +~/test git:master ❯❯❯ go vet ./clo.go +# command-line-arguments +./clo.go:8:6: loop variable k captured by func literal +./clo.go:8:12: loop variable r captured by func literal +``` + +执行 go tool vet help 可以看到 go vet 已经内置的一些 linter。 + +```shell +~ ❯❯❯ go tool vet help +vet is a tool for static analysis of Go programs. + +vet examines Go source code and reports suspicious constructs, +such as Printf calls whose arguments do not align with the format +string. It uses heuristics that do not guarantee all reports are +genuine problems, but it can find errors not caught by the compilers. + +Registered analyzers: + + asmdecl report mismatches between assembly files and Go declarations + assign check for useless assignments + atomic check for common mistakes using the sync/atomic package + bools check for common mistakes involving boolean operators + buildtag check that +build tags are well-formed and correctly located + cgocall detect some violations of the cgo pointer passing rules + composites check for unkeyed composite literals + copylocks check for locks erroneously passed by value + errorsas report passing non-pointer or non-error values to errors.As + httpresponse check for mistakes using HTTP responses + loopclosure check references to loop variables from within nested functions + lostcancel check cancel func returned by context.WithCancel is called + nilfunc check for useless comparisons between functions and nil + printf check consistency of Printf format strings and arguments + shift check for shifts that equal or exceed the width of the integer + stdmethods check signature of methods of well-known interfaces + structtag check that struct field tags conform to reflect.StructTag.Get + tests check for common mistaken usages of tests and examples + unmarshal report passing non-pointer or non-interface values to unmarshal + unreachable check for unreachable code + unsafeptr check for invalid conversions of uintptr to unsafe.Pointer + unusedresult check for unused results of calls to some functions +``` + +默认情况下这些 linter 都是会跑的,当前很多 IDE 在代码修改时会自动执行 go vet,所以我们在写代码的时候一般就能发现这些错了。 + +但 `go vet` 还是应该集成到线上流程中,因为有些程序员的下限实在太低。 + +### errcheck + +Go 语言中的大多数函数返回字段中都是有 error 的: + +```go +func sayhello(wr http.ResponseWriter, r *http.Request) { + io.WriteString(wr, "hello") +} + +func main() { + http.HandleFunc("/", sayhello) + http.ListenAndServe(":1314", nil) // 这里返回的 err 没有处理 +} +``` + +这个例子中,我们没有处理 `http.ListenAndServe` 函数返回的 error 信息,这会导致我们的程序在启动时发生静默失败。 + +程序员往往会基于过往经验,对当前的场景产生过度自信,从而忽略掉一些常见函数的返回错误,这样的编程习惯经常为我们带来意外的线上事故。例如,规矩的写法是下面这样的: + +```go +data, err := getDataFromRPC() +if err != nil { + return nil, err +} + +// do business logic +age := data.age +``` + +而自信的程序员可能会写成这样: + +```go +data, _ := getDataFromRPC() + +// do business logic +age := data.age +``` + +如果底层 RPC 逻辑出错,上层的 data 是个空指针也是很正常的,如果底层函数返回的 err 非空时,我们不应该对其它字段做任何的假设。这里 data 完全有可能是个空指针,造成用户程序 panic。 + +errcheck 会强制我们在代码中检查并处理 err。 + +### gocyclo + +gocyclo 主要用来检查函数的圈复杂度。圈复杂度可以参考下面的定义: + +> 圈复杂度(Cyclomatic complexity)是一种代码复杂度的衡量标准,在 1976 年由 Thomas J. McCabe, Sr. 提出。在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。 + +看定义较为复杂但计算还是比较简单的,我们可以认为: + +* 一个 if,那么函数的圈复杂度要 + 1 +* 一个 switch 的 case,函数的圈复杂度要 + 1 +* 一个 for 循环,圈复杂度 + 1 +* 一个 && 或 ||,圈复杂度 + 1 + +在大多数语言中,若函数的圈复杂度超过了 10,那么我们就认为该函数较为复杂,需要做拆解或重构。部分场景可以使用表驱动的方式进行重构。 + +由于在 Go 语言中,我们使用 `if err != nil` 来处理错误,所以在一个函数中出现多个 `if err != nil` 是比较正常的,因此 Go 中函数复杂度的阈值可以稍微调高一些,15 是较为合适的值。 + +下面是在个人项目 elasticsql 中执行 gocyclo 的结果,输出 top 10 复杂的函数: + +```shell +~/g/s/g/c/elasticsql git:master ❯❯❯ gocyclo -top 10 ./ +23 elasticsql handleSelectWhere select_handler.go:289:1 +16 elasticsql handleSelectWhereComparisonExpr select_handler.go:220:1 +16 elasticsql handleSelect select_handler.go:11:1 +9 elasticsql handleGroupByFuncExprDateHisto select_agg_handler.go:82:1 +9 elasticsql handleGroupByFuncExprDateRange select_agg_handler.go:154:1 +8 elasticsql buildComparisonExprRightStr select_handler.go:188:1 +7 elasticsql TestSupported select_test.go:80:1 +7 elasticsql Convert main.go:28:1 +7 elasticsql handleGroupByFuncExpr select_agg_handler.go:215:1 +6 elasticsql handleSelectWhereOrExpr select_handler.go:157:1 +``` + +### bodyclose + +使用 bodyclose[^2] 可以帮我们检查在使用 HTTP 标准库时忘记关闭 http body 导致连接一直被占用的问题。 + +```go +resp, err := http.Get("http://example.com/") // Wrong case +if err != nil { + // handle error +} +body, err := ioutil.ReadAll(resp.Body) +``` + +像上面这样的例子是不对的,使用标准库很容易犯这样的错。bodyclose 可以直接检查出这个问题: + +```shell +# command-line-arguments +./httpclient.go:10:23: response body must be closed +``` + +所以必须要把 Body 关闭: + +```go +resp, err := http.Get("http://example.com/") +if err != nil { + // handle error +} +defer resp.Body.Close() // OK +body, err := ioutil.ReadAll(resp.Body) +``` + +HTTP 标准库的 API 设计的不太好,这个问题更好的避免方法是公司内部将 HTTP client 封装为 SDK,防止用户写出这样不 Close HTTP body 的代码。 + +### sqlrows + +与 HTTP 库设计类似,我们在面向数据库编程时,也会碰到 sql.Rows 忘记关闭的问题,导致连接大量被占用。sqlrows[^3] 这个 linter 能帮我们避免这个问题,先来看看错误的写法: + +```go +rows, err := db.QueryContext(ctx, "SELECT * FROM users") +if err != nil { + return nil, err +} + +for rows.Next() { + err = rows.Scan(...) + if err != nil { + return nil, err // NG: this return will not release a connection. + } +} +``` + +正确的写法需要在使用完后关闭 sql.Rows: + +```go +rows, err := db.QueryContext(ctx, "SELECT * FROM users") +if err != nil { + return nil, err +} +defer rows.Close() +``` + +与 HTTP 同理,公司内也应该将 DB 查询封装为合理的 SDK,不要让业务使用标准库中的 API,避免上述错误发生。 + +### funlen + +funlen[^4] 和 gocyclo 类似,但是这两个 linter 对代码复杂度的视角不太相同,gocyclo 更多关注函数中的逻辑分支,而 funlen 则重点关注函数的长度。默认函数超过 60 行和 40 条语句时,该 linter 即会报警。 + +## linter 集成工具 + +一个一个去社区里找 linter 来拼搭效率太低,当前社区里已经有了较好的集成工具,早期是 gometalinter,后来性能更好,功能更全的 golangci-lint 逐渐取而代之。目前 golangci-lint 是 Go 社区的绝对主流 linter。 + +### golangci-lint + +golangci-lint[^5] 能够通过配置来 enable 很多 linter,基本主流的都包含在内了。 + +在本节开头讲到的所有 linter 都可以在 golangci-lint 中进行配置, + +使用也较为简单,只要在项目目录执行 golangci-lint run . 即可。 + +```shell +~/g/s/g/c/elasticsql git:master ❯❯❯ golangci-lint run . +main.go:36:9: S1034: assigning the result of this type assertion to a variable (switch stmt := stmt.(type)) could eliminate type assertions in switch cases (gosimple) + switch stmt.(type) { + ^ +main.go:38:34: S1034(related information): could eliminate this type assertion (gosimple) + dsl, table, err = handleSelect(stmt.(*sqlparser.Select)) + ^ +main.go:40:23: S1034(related information): could eliminate this type assertion (gosimple) + return handleUpdate(stmt.(*sqlparser.Update)) + ^ +main.go:42:23: S1034(related information): could eliminate this type assertion (gosimple) + return handleInsert(stmt.(*sqlparser.Insert)) + ^ +select_handler.go:192:9: S1034: assigning the result of this type assertion to a variable (switch expr := expr.(type)) could eliminate type assertions in switch cases (gosimple) + switch expr.(type) { +``` + +## 参考资料 + +[^1]:https://revive.run/ + +[^2]:https://github.com/timakin/bodyclose + +[^3]:https://github.com/gostaticanalysis/sqlrows + +[^4]:https://github.com/ultraware/funlen + +[^5]:https://github.com/golangci/golangci-lint diff --git a/site/content/docs/runtime/_index.md b/site/content/docs/runtime/_index.md new file mode 100644 index 0000000..308963f --- /dev/null +++ b/site/content/docs/runtime/_index.md @@ -0,0 +1,5 @@ +--- +title: 运行时 +weight: 2 +bookCollapseSection: true +--- diff --git a/site/content/docs/runtime/memory_management/_index.md b/site/content/docs/runtime/memory_management/_index.md new file mode 100644 index 0000000..0b705e3 --- /dev/null +++ b/site/content/docs/runtime/memory_management/_index.md @@ -0,0 +1,4 @@ +--- +title: 内存管理 +weight: 10 +--- diff --git a/site/content/docs/runtime/memory_management/escape_analysis.md b/site/content/docs/runtime/memory_management/escape_analysis.md new file mode 100644 index 0000000..009a738 --- /dev/null +++ b/site/content/docs/runtime/memory_management/escape_analysis.md @@ -0,0 +1,7 @@ +--- +title: 逃逸分析 +weight: 10 +draft: true +--- + +# 逃逸分析 diff --git a/site/content/docs/runtime/memory_management/finalizer.md b/site/content/docs/runtime/memory_management/finalizer.md new file mode 100644 index 0000000..42b6c5e --- /dev/null +++ b/site/content/docs/runtime/memory_management/finalizer.md @@ -0,0 +1,7 @@ +--- +title: finalizer +weight: 10 +draft: true +--- + +# finalizer diff --git a/site/content/docs/runtime/memory_management/garbage_collection.md b/site/content/docs/runtime/memory_management/garbage_collection.md new file mode 100644 index 0000000..dbf5dfb --- /dev/null +++ b/site/content/docs/runtime/memory_management/garbage_collection.md @@ -0,0 +1,171 @@ +--- +title: 垃圾回收 +weight: 10 +--- + +# 垃圾回收(WIP) + +基于 Go 1.17。 + +![GC 的多个阶段](/images/runtime/memory/gcphases.jpg) + +## 三色抽象 + +在 Go 的代码中并无直接提示对象颜色的代码,对象的颜色主要由: + +* 对象对应的 gcmarkbit 位是否为 1 +* 对象的子对象是否已入队完成,若已完成,对象本身应该已经在队列外了 + +这两个状态来决定,三种颜色分别为: + +* 黑色:对象的 gcmarkbit 为 1,且对象已从队列中弹出 +* 灰色:对象的 gcmarkbit 为 1,其子对象未被处理完成,对象本身还在队列中 +* 白色:对象的 gcmarkbit 为 0,还未被标记流程所处理 + +## GC 触发 + +当前 GC 有三个触发点: + +* runtime.GC +* forcegchelper +* heap trigger + +## 并发标记流程 + + +{{}} + +{{}} + +### 关键组件及启动流程 + +worker 的三种模式 + +* 全职模式:gcMarkWorkerDedicatedMode +* 比例模式:gcMarkWorkerFractionalMode +* 兼职模式:gcMarkWorkerIdleMode + + +### gc roots + +垃圾回收的标记流程是将存活对象对应的 bit 位置为 1,堆上存活对象在内存中会形成森林结构,标记开始之前需要先将所有的根确定下来。 + +根对象包括四个来源: + +* bss 段 +* data 段 +* goroutine 栈 +* finalizer 关联的 special 类对象 + +### gcDrain + +gcDrain 是标记的核心流程 + +#### markroot + +根标记的流程很简单,就是根据 gcMarkrootPrepare 中计算出的索引值,遍历使用的根,执行 scanblock。 + +这些全局变量、goroutine 栈变量被扫描后,会被推到 gcw 队列中,成为灰色对象。 + +#### 标记过程中的队列 gcw && wbBuf && work.full + +{{}} + +{{}} + +#### 排空本地 gcw 和全局 work.full + +#### 标记终止流程 + +## mutator 与 marker 并发执行时的问题 + +### 对象丢失问题 + +GC 标记过程与 mutator 是并发执行的,所以在标记过程中,堆上对象的引用关系也会被动态修改,这时候可能有下面这种情况: + +{{}} + +{{}} + +丢失的对象会被认为是垃圾而被回收掉,这样在 mutator 后续访问该对象时便会发生内存错误。为了解决这个问题,mutator 在 GC 标记阶段需要打开 write barrier。所谓的 write barrier,就是在堆上指针发生修改前,插入一小段代码: + +![write barrier demo](/images/runtime/memory/write_barrier_demo.jpg) + +每次修改堆上指针都会判断 runtime.writeBarrier.enabled 是否为 true,如果为 true,那么在修改指针前需要调用 runtime.gcWriteBarrier。 + +Go 语言使用的 gc write barrier 是插入和删除的混合屏障,我们先来看看插入和删除屏障是什么。 + +#### dijistra 插入屏障 + +{{}} + +{{}} + +#### yuasa 删除屏障 + +{{}} + +{{}} + +#### Go 语言使用的混合屏障 + +runtime.gcWriteBarrier 是汇编函数,可以看到会将指针在修改前指向的值,和修改后指向的值都 push 到 wbBuf 中。 + +如果 wbBuf 满,那么就会将其 push 到 gcw 中,gcw 满了会 push 到全局的 work.full 中。 + +```go +TEXT runtime·gcWriteBarrier(SB),NOSPLIT,$112 + ...... + MOVQ (p_wbBuf+wbBuf_next)(R13), R12 + // Increment wbBuf.next position. + LEAQ 16(R12), R12 + MOVQ R12, (p_wbBuf+wbBuf_next)(R13) + CMPQ R12, (p_wbBuf+wbBuf_end)(R13) + // Record the write. + MOVQ AX, -16(R12) // Record value + MOVQ (DI), R13 + MOVQ R13, -8(R12) // Record *slot + // Is the buffer full? (flags set in CMPQ above) + JEQ flush +ret: + MOVQ 96(SP), R12 + MOVQ 104(SP), R13 + // Do the write. + MOVQ AX, (DI) + RET + +flush: + ...... + CALL runtime·wbBufFlush(SB) + ...... + JMP ret + ...... +``` + +## 清扫流程 sweep + +标记完成后,在 gcMarkTermination 中调用 gcSweep 会唤醒后台清扫 goroutine。该 goroutine 循环遍历所有 mspan,sweepone -> sweep 的主要操作为: + +* mspan.allocBits = mspan.gcMarkBits +* mspan.gcMarkBits clear + +清扫完成后会有三种情况: +* 该 mspan 全空了,那么调用 freeSpan 释放该 mspan 使其回归 arena,等待 scavenge 最终将这些 page 归还给操作系统 +* 尽管清扫了,但该 mspan 还是满的,那么将该 mspan 从 full 的 Unswept 链表移动到 full 的 Swept 部分 +* 清扫后 mspan 中出现了空槽,那么将该 mspan 从 full/partial 的 Unswept 链表移动到 partial 的 Swept 部分 + +### 协助清扫 + +TODO + +## 归还内存流程 scavenge + +bgscavenge -> pageAlloc.scavenge -> pageAlloc.scavengeOne -> pageAlloc.scavengeRangeLocked -> sysUnused -> madvise + + +### GOGC 及 GC 调步算法 + + +## debug.FreeOsMemory + +TODO \ No newline at end of file diff --git a/site/content/docs/runtime/memory_management/memory_alloctor.md b/site/content/docs/runtime/memory_management/memory_alloctor.md new file mode 100644 index 0000000..f149226 --- /dev/null +++ b/site/content/docs/runtime/memory_management/memory_alloctor.md @@ -0,0 +1,32 @@ +--- +title: 内存分配 +weight: 10 +--- + +# 内存分配 + +## bump/sequential allocator + +{{}} + + +{{}} + +## freelist allocator + +{{}} + +{{}} + +## dangling pointer + +{{}} + +{{}} + + +## tiny alloc + +{{}} + +{{}} \ No newline at end of file diff --git a/site/content/docs/runtime/netpoll/_index.md b/site/content/docs/runtime/netpoll/_index.md new file mode 100644 index 0000000..515c93b --- /dev/null +++ b/site/content/docs/runtime/netpoll/_index.md @@ -0,0 +1,5 @@ +--- +title: netpoll +weight: 10 +draft: true +--- diff --git a/site/content/docs/runtime/netpoll/basics.md b/site/content/docs/runtime/netpoll/basics.md new file mode 100644 index 0000000..8ec08a2 --- /dev/null +++ b/site/content/docs/runtime/netpoll/basics.md @@ -0,0 +1,127 @@ +--- +title: 网络编程基础 +weight: 1 +draft: true +--- + +# 网络编程基础 + +## 阻塞与非阻塞 + +在 linux 中,一切外部资源都被抽象为文件。网络连接也不例外,当我们在执行网络读、写操作时,可以使用 [fcntl](https://man7.org/linux/man-pages/man2/fcntl.2.html) 这个 syscall 来调整网络 fd 的阻塞模式: + +```c +int flag = fcntl(fd, F_GETFL, 0); +fcntl(fd, F_SETFL, flag|O_NONBLOCK); +``` + +fd 默认都是阻塞模式的,使用 fcntl 调整之后即为非阻塞模式。非阻塞与阻塞的区别可以用这张图来理解: + + + +## C 语言 epoll 示例 + +```c +#include +#include +#include +#include +#include +#include +#include + +#define BUF_SIZE 1024 +#define EPOLL_SIZE 50 + +void error_handling( char * msg ); + + +int main( int argc, char * argv[] ) +{ + int sock_fd, conn_fd; + struct sockaddr_in serv_addr, client_addr; + socklen_t addr_size; + int str_len, i; + char buf[BUF_SIZE]; + struct epoll_event * ep_events; + struct epoll_event event; + + int epfd, event_cnt; + + if ( argc != 2 ) + { + printf( "Usage : %s \n", argv[0] ); + exit( 1 ); + } + + sock_fd = socket( PF_INET, SOCK_STREAM, 0 ); + memset( &serv_addr, 0, sizeof(serv_addr) ); + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = htonl( INADDR_ANY ); + serv_addr.sin_port = htons( atoi( argv[1] ) ); + + if ( bind( sock_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr) ) == -1 ) + { + error_handling( "bind error" ); + } + + if ( listen( sock_fd, 5 ) == -1 ) + { + error_handling( "listen error" ); + } + + epfd = epoll_create( EPOLL_SIZE ); + ep_events = malloc( sizeof(struct epoll_event) * EPOLL_SIZE ); + + event.events = EPOLLIN; + event.data.fd = sock_fd; + epoll_ctl( epfd, EPOLL_CTL_ADD, sock_fd, &event ); + + while ( 1 ) + { + event_cnt = epoll_wait( epfd, ep_events, EPOLL_SIZE, -1 ); + if ( event_cnt == -1 ) + { + puts( "epoll_wait error" ); + break; + } + + for ( i = 0; i < event_cnt; i++ ) + { + if ( ep_events[i].data.fd == sock_fd ) + { + addr_size = sizeof(client_addr); + conn_fd = accept( sock_fd, (struct sockaddr *) &client_addr, &addr_size ); + event.events = EPOLLIN; + event.data.fd = conn_fd; + epoll_ctl( epfd, EPOLL_CTL_ADD, conn_fd, &event ); + printf( "connected client : %d\n", conn_fd ); + } else { + str_len = read( ep_events[i].data.fd, buf, BUF_SIZE ); + if ( str_len == 0 ) /* close request EOF? */ + { + epoll_ctl( epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL ); + close( ep_events[i].data.fd ); /* close(conn_fd); */ + printf( "closed client: %d\n", ep_events[i].data.fd ); + } else { + write( ep_events[i].data.fd, buf, str_len ); /* echo result! */ + } + } + } + } + close( sock_fd ); + close( epfd ); + return(0); +} + + +void error_handling( char * msg ) +{ + fputs( msg, stderr ); + fputc( '\n', stderr ); +} +``` + +## 参考资料 + +https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md diff --git a/site/content/docs/runtime/netpoll/netpoll.md b/site/content/docs/runtime/netpoll/netpoll.md new file mode 100644 index 0000000..6996b89 --- /dev/null +++ b/site/content/docs/runtime/netpoll/netpoll.md @@ -0,0 +1,20 @@ +--- +title: Go 语言的网络抽象 +weight: 2 +--- + +# netpoller + +从网络编程基础篇我们知道,网络编程中涉及到的主要系统调用是下面这些: + +* socket +* bind +* listen +* accept +* epoll_create +* epoll_wait +* epoll_ctl +* read +* write + +因此在学习 Go 对网络层的抽象时,我们也是重点关注 Go 的 netpoller 中,这些 syscall 被封装进了哪个具体的流程里。 diff --git a/site/content/docs/runtime/scheduler/_index.md b/site/content/docs/runtime/scheduler/_index.md new file mode 100644 index 0000000..178e6d8 --- /dev/null +++ b/site/content/docs/runtime/scheduler/_index.md @@ -0,0 +1,5 @@ +--- +title: 调度器 +weight: 10 +--- + diff --git a/site/content/docs/runtime/scheduler/gmp.md b/site/content/docs/runtime/scheduler/gmp.md new file mode 100644 index 0000000..b3329eb --- /dev/null +++ b/site/content/docs/runtime/scheduler/gmp.md @@ -0,0 +1,7 @@ +--- +title: G、M、P 抽象 +weight: 10 +draft: true +--- + +# G、M、P 抽象 diff --git a/site/content/docs/runtime/scheduler/handling_blocking.md b/site/content/docs/runtime/scheduler/handling_blocking.md new file mode 100644 index 0000000..62174d5 --- /dev/null +++ b/site/content/docs/runtime/scheduler/handling_blocking.md @@ -0,0 +1,28 @@ +--- +title: 处理阻塞 +weight: 10 +draft: true +--- + +# 处理阻塞 + +在 Go 语言中,我们可能写出很多会阻塞执行的代码。 + +## channel 发送阻塞 + +![block on channel send](/images/runtime/block_on_channel_send.jpg) + + +## channel 接收阻塞 + +## net.Conn 读阻塞 + +## net.Conn 写阻塞 + +## time.Sleep 阻塞 + +## select 阻塞 + +## lock 阻塞 + + diff --git a/site/content/docs/runtime/scheduler/preemption.md b/site/content/docs/runtime/scheduler/preemption.md new file mode 100644 index 0000000..399a016 --- /dev/null +++ b/site/content/docs/runtime/scheduler/preemption.md @@ -0,0 +1,622 @@ +--- +title: 抢占 +weight: 2 +--- + +# 抢占 + +从 Go 1.14 开始,通过使用信号,Go 语言实现了调度和 GC 过程中的真“抢占“。 + +抢占流程由抢占的发起方向被抢占线程发送 SIGURG 信号。 + +当被抢占线程收到信号后,进入 SIGURG 的处理流程,将 asyncPreempt 的调用强制插入到用户当前执行的代码位置。 + +本节会对该过程进行详尽分析。 + +## 抢占发起的时机 + +抢占会在下列时机发生: + +* STW 期间 +* 在 P 上执行 safe point 函数期间 +* sysmon 后台监控期间 +* gc pacer 分配新的 dedicated worker 期间 +* panic 崩溃期间 + +{{< rawhtml >}} + +{{< /rawhtml >}} + +除了栈扫描,所有触发抢占最终都会去执行 preemptone 函数。栈扫描流程比较特殊: + +{{< rawhtml >}} + +{{< /rawhtml >}} + +从这些流程里,我们挑出三个来一探究竟。 + +### STW 抢占 + +![GC 的多个阶段](/images/runtime/memory/gcphases.jpg) + +上图是现在 Go 语言的 GC 流程图,在两个 STW 阶段都需要将正在执行的线程上的 running 状态的 goroutine 停下来。 + +```go +func stopTheWorldWithSema() { + ..... + preemptall() + ..... + // 等待剩余的 P 主动停下 + if wait { + for { + // wait for 100us, then try to re-preempt in case of any races + // 等待 100us,然后重新尝试抢占 + if notetsleep(&sched.stopnote, 100*1000) { + noteclear(&sched.stopnote) + break + } + preemptall() + } + } + +``` + + +### GC 栈扫描 + +goroutine 的栈是 GC 扫描期间的根,所有 markroot 中需要将用户的 goroutine 停下来,主要是 running 状态: + +```go +func markroot(gcw *gcWork, i uint32) { + // Note: if you add a case here, please also update heapdump.go:dumproots. + switch { + ...... + default: + // the rest is scanning goroutine stacks + var gp *g + ...... + + // scanstack must be done on the system stack in case + // we're trying to scan our own stack. + systemstack(func() { + stopped := suspendG(gp) + scanstack(gp, gcw) + resumeG(stopped) + }) + } +} +``` + +suspendG 中会调用 preemptM -> signalM 对正在执行的 goroutine 所在的线程发送抢占信号。 + +### sysmon 后台监控 + +```go +func sysmon() { + idle := 0 // how many cycles in succession we had not wokeup somebody + for { + ...... + // retake P's blocked in syscalls + // and preempt long running G's + if retake(now) != 0 { + idle = 0 + } else { + idle++ + } + } +} +``` + +执行 syscall 太久的,需要将 P 从 M 上剥离;运行用户代码太久的,需要抢占停止该 goroutine 执行。这里我们只看抢占 goroutine 的部分: + +```go +const forcePreemptNS = 10 * 1000 * 1000 // 10ms + +func retake(now int64) uint32 { + ...... + for i := 0; i < len(allp); i++ { + _p_ := allp[i] + s := _p_.status + if s == _Prunning || s == _Psyscall { + // Preempt G if it's running for too long. + t := int64(_p_.schedtick) + if int64(pd.schedtick) != t { + pd.schedtick = uint32(t) + pd.schedwhen = now + } else if pd.schedwhen+forcePreemptNS <= now { + preemptone(_p_) + } + } + ...... + } + ...... +} +``` + +## 协作式抢占原理 + +cooperative preemption 关键在于 cooperative,抢占的时机在各个版本实现差异不大,我们重点来看看这个协作过程。 + +### 函数头、函数尾插入的栈扩容检查 + +在 Go 语言中发生函数调用时,如果函数的 framesize > 0,说明在调用该函数时可能会发生 goroutine 的栈扩张,这时会在函数头、函数尾分别插入一段汇编码: + +```go +package main + +func main() { + add(1, 2) +} + +//go:noinline +func add(x, y int) (int, bool) { + var z = x + y + println(z) + return x + y, true +} +``` + +add 函数在使用 go tool compile -S 后会生成下面的结果: + +```go +// 这里的汇编代码使用 go1.14 生成 +// 在 go1.17 之后,函数的调用规约发生变化 +// 1.17 与以往版本头部的汇编代码也会有所不同,但逻辑保持一致 +"".add STEXT size=103 args=0x20 locals=0x18 + 0x0000 00000 (add.go:8) TEXT "".add(SB), ABIInternal, $24-32 + 0x0000 00000 (add.go:8) MOVQ (TLS), CX + 0x0009 00009 (add.go:8) CMPQ SP, 16(CX) + 0x000d 00013 (add.go:8) JLS 96 + ...... func body + 0x005f 00095 (add.go:11) RET + 0x0060 00096 (add.go:11) NOP + 0x0060 00096 (add.go:8) CALL runtime.morestack_noctxt(SB) + 0x0065 00101 (add.go:8) JMP 0 +``` + +TLS 中存储的是 G 的指针,偏移 16 字节即是 G 的结构体中的 stackguard0。由于 goroutine 的栈也是从高地址向低地址增长,因此这里检查当前 SP < stackguard0 的话,说明需要对栈进行扩容了。 + +### morestack 中的调度逻辑 + +```go +// morestack_noctxt 是个简单的汇编方法 +// 直接跳转到 morestack +TEXT runtime·morestack_noctxt(SB),NOSPLIT|NOFRAME,$0-0 + MOV ZERO, CTXT + JMP runtime·morestack(SB) + +TEXT runtime·morestack(SB),NOSPLIT,$0-0 + ...... + // 前面会切换将执行现场保存到 goroutine 的 gobuf 中 + // 并将执行栈切换到 g0 + // Call newstack on m->g0's stack. + MOVQ m_g0(BX), BX + MOVQ BX, g(CX) + MOVQ (g_sched+gobuf_sp)(BX), SP + CALL runtime·newstack(SB) + ...... + RET +``` + +morestack 会将 goroutine 的现场保存在当前 goroutine 的 gobuf 中,并将执行栈切换到 g0,然后在 g0 上执行 runtime.newstack。 + +在未实现信号抢占之前,用户的 g 到底啥时候能停下来,负责 GC 栈扫描的 goroutine 也不知道,所以 scanstack 也就只能设置一下 preemptscan 的标志位,最终栈扫描要 [newstack](https://github.com/golang/go/blob/2bc8d90fa21e9547aeb0f0ae775107dc8e05dc0a/src/runtime/stack.go#L917) 来配合,下面的 newstack 是 Go 1.13 版本的实现: + +```go +func newstack() { + thisg := getg() + gp := thisg.m.curg + preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt + if preempt { + if thisg.m.locks != 0 || thisg.m.mallocing != 0 || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning { + gp.stackguard0 = gp.stack.lo + _StackGuard + gogo(&gp.sched) // never return + } + } + + if preempt { + // 要和 scang 过程配合 + // 老版本的 newstack 和 gc scan 过程是有较重的耦合的 + casgstatus(gp, _Grunning, _Gwaiting) + if gp.preemptscan { + for !castogscanstatus(gp, _Gwaiting, _Gscanwaiting) { + } + if !gp.gcscandone { + gcw := &gp.m.p.ptr().gcw + // 注意这里,偶合了 GC 的 scanstack 逻辑代码 + scanstack(gp, gcw) + gp.gcscandone = true + } + gp.preemptscan = false + gp.preempt = false + casfrom_Gscanstatus(gp, _Gscanwaiting, _Gwaiting) + casgstatus(gp, _Gwaiting, _Grunning) + gp.stackguard0 = gp.stack.lo + _StackGuard + gogo(&gp.sched) // never return + } + + casgstatus(gp, _Gwaiting, _Grunning) + gopreempt_m(gp) // never return + } + ...... +} + +``` + +抢占成功后,当前的 goroutine 会被放在全局队列中: + +```go +func gopreempt_m(gp *g) { + goschedImpl(gp) +} + +func goschedImpl(gp *g) { + status := readgstatus(gp) + ...... + + casgstatus(gp, _Grunning, _Grunnable) + dropg() + lock(&sched.lock) + globrunqput(gp) // 将当前 goroutine 放进全局队列 + unlock(&sched.lock) + + schedule() // 当前线程重新进入调度循环 +} +``` + +### 信号式抢占实现后的 newstack + +在实现了信号式抢占之后,对于用户的 goroutine 何时中止有了一些预期,所以 newstack 就不需要耦合 scanstack 的逻辑了,新版的 [newstack](https://github.com/golang/go/blob/c3b47cb598e1ecdbbec110325d9d1979553351fc/src/runtime/stack.go#L948) 实现如下: + +```go +func newstack() { + thisg := getg() + gp := thisg.m.curg + preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt + + if preempt { + if !canPreemptM(thisg.m) { + // 让 goroutine 继续执行 + // 下次再抢占它 + gp.stackguard0 = gp.stack.lo + _StackGuard + gogo(&gp.sched) // never return + } + } + + if preempt { + // 当 GC 需要发起 goroutine 的栈扫描时 + // 会设置这个 preemptStop 为 true + // 这时候需要 goroutine 自己去 gopark + if gp.preemptStop { + preemptPark(gp) // never returns + } + + // 除了 GC 栈扫描以外的其它抢占场景走这个分支 + // 看起来就像 goroutine 自己调用了 runtime.Gosched 一样 + gopreempt_m(gp) // never return + } + ...... 后面就是正常的栈扩展逻辑了 +} +``` + +newstack 中会使用 [canPreemptM](https://github.com/golang/go/blob/287c5e8066396e953254d7980a80ec082edf11bd/src/runtime/preempt.go#L287)判断哪些场景适合抢占,哪些不适合。如果当前 goroutine 正在执行(即 status == running),并且满足下列任意其一: + +* 持有锁(主要是写锁,读锁其实判断不出来); +* 正在进行内存分配 +* preemptoff 非空 + +便不应该进行抢占,会在下一次进入到 newstack 时再进行判断。 + +## 非协作式抢占 + +非协作式抢占,就是通过信号处理来实现的。所以我们只要关注 SIGURG 的处理流程即可。 + +### 信号处理初始化 + +当 m0(即程序启动时的第一个线程)初始化时,会进行信号处理的初始化工作: + +```go +// mstartm0 implements part of mstart1 that only runs on the m0. +func mstartm0() { + initsig(false) +} + +// Initialize signals. +func initsig(preinit bool) { + for i := uint32(0); i < _NSIG; i++ { + setsig(i, funcPC(sighandler)) + } +} + +var sigtable = [...]sigTabT{ + ...... + /* 23 */ {_SigNotify + _SigIgn, "SIGURG: urgent condition on socket"}, + ...... +} + +``` + +最后都是执行 sigaction: + +```go +TEXT runtime·rt_sigaction(SB),NOSPLIT,$0-36 + MOVQ sig+0(FP), DI + MOVQ new+8(FP), SI + MOVQ old+16(FP), DX + MOVQ size+24(FP), R10 + MOVL $SYS_rt_sigaction, AX + SYSCALL + MOVL AX, ret+32(FP) + RET +``` + +与一般的 syscall 区别不大。 + +信号处理初始化的流程比较简单,就是给所有已知的需要处理的信号绑上 sighandler。 + +### 发送信号 + +```go +func preemptone(_p_ *p) bool { + mp := _p_.m.ptr() + gp := mp.curg + gp.preempt = true + gp.stackguard0 = stackPreempt + + // 向该线程发送 SIGURG 信号 + if preemptMSupported && debug.asyncpreemptoff == 0 { + _p_.preempt = true + preemptM(mp) + } + + return true +} +``` + +preemptM 的流程较为线性: + +```go +func preemptM(mp *m) { + if atomic.Cas(&mp.signalPending, 0, 1) { + signalM(mp, sigPreempt) + } +} + +func signalM(mp *m, sig int) { + tgkill(getpid(), int(mp.procid), sig) +} +``` + +最后使用 tgkill 这个 syscall 将信号发送给指定 id 的线程: + +```go +TEXT ·tgkill(SB),NOSPLIT,$0 + MOVQ tgid+0(FP), DI + MOVQ tid+8(FP), SI + MOVQ sig+16(FP), DX + MOVL $SYS_tgkill, AX + SYSCALL + RET +``` + +### 接收信号后的处理 + +当线程 m 接收到信号后,会从用户栈 g 切换到 gsignal 执行信号处理逻辑,即 sighandler 流程: + +```go +func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) { + _g_ := getg() + c := &sigctxt{info, ctxt} + + ...... + if sig == sigPreempt && debug.asyncpreemptoff == 0 { + doSigPreempt(gp, c) + } + ...... +} +``` + +如果收到的是抢占信号,那么执行 doSigPreempt 逻辑: + +```go +func doSigPreempt(gp *g, ctxt *sigctxt) { + // 检查当前 G 被抢占是否安全 + if wantAsyncPreempt(gp) { + if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok { + // Adjust the PC and inject a call to asyncPreempt. + ctxt.pushCall(funcPC(asyncPreempt), newpc) + } + } + ...... +} +``` + +isAsyncSafePoint 中会把一些不应该抢占的场景过滤掉,具体包括: + +* 当前代码在汇编编写的函数中执行 +* 代码在 runtime,runtime/internal 或者 reflect 包中执行 + +doSigPreempt 代码中的 pushCall 是关键步骤: + +```go +func (c *sigctxt) pushCall(targetPC, resumePC uintptr) { + // Make it look like we called target at resumePC. + sp := uintptr(c.rsp()) + sp -= sys.PtrSize + *(*uintptr)(unsafe.Pointer(sp)) = resumePC + c.set_rsp(uint64(sp)) + c.set_rip(uint64(targetPC)) +} +``` + +pushCall 相当于将用户将要执行的下一条代码的地址直接 push 到栈上,并 jmp 到指定的 target 地址去执行代码: + +{{< columns >}} + +before + +```shell +----- PC = 0x123 +local var 1 +----- +local var 2 +----- <---- SP +``` + +<---> + +after + +```shell +----- PC = targetPC +local var 1 +----- +local var 2 +----- +prev PC = 0x123 +----- <---- SP +``` + +{{}} + +{{< columns>}} +{{< /columns >}} + +这里的 target 就是 asyncPreempt。 + +### asyncPreempt 执行流程分析 + +asyncPreempt 分为上半部分和下半部分,中间被 asyncPreempt2 隔开。上半部分负责将 goroutine 当前执行现场的所有寄存器都保存到当前的运行栈上。 + +下半部分负责在 asyncPreempt2 返回后将这些现场恢复出来。 + +```go +TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 + PUSHQ BP + MOVQ SP, BP + ...... 保存现场 1 + MOVQ AX, 0(SP) + MOVQ CX, 8(SP) + MOVQ DX, 16(SP) + MOVQ BX, 24(SP) + MOVQ SI, 32(SP) + ...... 保存现场 2 + MOVQ R15, 104(SP) + MOVUPS X0, 112(SP) + MOVUPS X1, 128(SP) + ...... + MOVUPS X15, 352(SP) + + CALL ·asyncPreempt2(SB) + + MOVUPS 352(SP), X15 + ...... 恢复现场 2 + MOVUPS 112(SP), X0 + MOVQ 104(SP), R15 + ...... 恢复现场 1 + MOVQ 8(SP), CX + MOVQ 0(SP), AX + ...... + RET + +``` + +asyncPreempt2 中有两个分支: + +```go +func asyncPreempt2() { + gp := getg() + gp.asyncSafePoint = true + if gp.preemptStop { // 这个 preemptStop 是在 GC 的栈扫描中才会设置为 true + mcall(preemptPark) + } else { // 除了栈扫描,其它抢占全部走这条分支 + mcall(gopreempt_m) + } + gp.asyncSafePoint = false +} +``` + +GC 栈扫描走 if 分支,除栈扫描以外所有情况均走 else 分支。 + +**栈扫描抢占流程** + +suspendG -> preemptM -> signalM 发信号。 + +sighandler -> asyncPreempt -> 保存执行现场 -> asyncPreempt2 -> **preemptPark** + +preemptPark 和 gopark 类似,挂起当前正在执行的 goroutine,该 goroutine 之前绑定的线程就可以继续执行调度循环了。 + +scanstack 执行完之后: + +resumeG -> ready -> runqput 会让之前被停下来的 goroutine 进当前 P 的队列或全局队列。 + +**其它流程** + +preemptone -> preemptM - signalM 发信号。 + +sighandler -> asyncPreempt -> 保存执行现场 -> asyncPreempt2 -> **gopreempt_m** + +gopreempt_m 会直接将被抢占的 goroutine 放进全局队列。 + +无论是栈扫描流程还是其它流程,当 goroutine 程序被调度到时,都是从汇编中的 `CALL ·asyncPreempt2(SB)` 的下一条指令开始执行的,即 asyncPreempt 汇编函数的下半部分。 + +这部分会将之前 goroutine 的现场完全恢复,就和抢占从来没有发生过一样。 + +## 动画演示 + +下面的动画是可以点的哦~ + +**sighandler 收到抢占信号,保存 PC,并将 PC 指向 asyncPreempt 的过程动画:** + +{{}} + +{{}} + + +**GC 栈扫描时的抢占和恢复过程:** + +{{}} + +{{}} + + +**除了栈扫描之外的抢占和恢复过程:** + +{{}} + +{{}} diff --git a/site/content/docs/runtime/scheduler/sched_loop.md b/site/content/docs/runtime/scheduler/sched_loop.md new file mode 100644 index 0000000..2ea1f2c --- /dev/null +++ b/site/content/docs/runtime/scheduler/sched_loop.md @@ -0,0 +1,34 @@ +--- +title: 调度流程(WIP) +weight: 1 +--- + +## 组件大图 + +![](/images/runtime/schedule/sche_big.png) + +## Go 的调度流程 + +我们可以认为 goroutine 的创建与调度循环是一个生产-消费流程。整个 go 程序的运行就是在不断地执行 goroutine 的生产与消费流程。 + +创建 goroutine 即是在创建任务,这些生产出来的 goroutine 可能会有三个去处,分别是: + +* p.runnext +* p.localrunq +* schedt.global runq + +按照执行权来讲,优先级是逐渐降低的。 + +调度循环会不断地从上面讲的三个目标中消费 goroutine,并执行。 + +## goroutine 生产 + +{{}} + +{{}} + +## goroutine 消费 + +{{}} + +{{}} diff --git a/site/content/docs/std_library/_index.md b/site/content/docs/std_library/_index.md new file mode 100644 index 0000000..e7e0be6 --- /dev/null +++ b/site/content/docs/std_library/_index.md @@ -0,0 +1,6 @@ +--- +title: 标准库 +weight: 5 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/sync/_index.md b/site/content/docs/sync/_index.md new file mode 100644 index 0000000..3e93950 --- /dev/null +++ b/site/content/docs/sync/_index.md @@ -0,0 +1,5 @@ +--- +title: 同步编程 +weight: 5 +bookCollapseSection: true +--- diff --git a/site/content/docs/sync/lock_free.md b/site/content/docs/sync/lock_free.md new file mode 100644 index 0000000..290f271 --- /dev/null +++ b/site/content/docs/sync/lock_free.md @@ -0,0 +1,8 @@ +--- +title: 同步编程 +weight: 5 +bookCollapseSection: true +draft: true +--- + +# lock free programming diff --git a/site/content/docs/sync/memory_barrier.md b/site/content/docs/sync/memory_barrier.md new file mode 100644 index 0000000..1da714b --- /dev/null +++ b/site/content/docs/sync/memory_barrier.md @@ -0,0 +1,7 @@ +--- +title: 同步编程 +weight: 5 +bookCollapseSection: true +draft: true +--- +# Memory Barrier diff --git a/site/content/docs/sync/patterns.md b/site/content/docs/sync/patterns.md new file mode 100644 index 0000000..e565ddc --- /dev/null +++ b/site/content/docs/sync/patterns.md @@ -0,0 +1,8 @@ +--- +title: 同步编程 +weight: 5 +bookCollapseSection: true +draft: true +--- + +# 并发编程模式 diff --git a/site/content/docs/sync/theory.md b/site/content/docs/sync/theory.md new file mode 100644 index 0000000..bdc60be --- /dev/null +++ b/site/content/docs/sync/theory.md @@ -0,0 +1,8 @@ +--- +title: 同步编程 +weight: 5 +bookCollapseSection: true +draft: true +--- + +# 并发编程理论 diff --git a/site/content/docs/sync/tools.md b/site/content/docs/sync/tools.md new file mode 100644 index 0000000..2662af1 --- /dev/null +++ b/site/content/docs/sync/tools.md @@ -0,0 +1,8 @@ +--- +title: 同步编程 +weight: 5 +bookCollapseSection: true +draft: true +--- + +# 并发工具 diff --git a/site/content/docs/sync/tools/_index.md b/site/content/docs/sync/tools/_index.md new file mode 100644 index 0000000..715279b --- /dev/null +++ b/site/content/docs/sync/tools/_index.md @@ -0,0 +1,6 @@ +--- +title: 同步工具 +weight: 5 +bookCollapseSection: true +--- + diff --git a/site/content/docs/sync/tools/syncPool.md b/site/content/docs/sync/tools/syncPool.md new file mode 100644 index 0000000..29e31da --- /dev/null +++ b/site/content/docs/sync/tools/syncPool.md @@ -0,0 +1,7 @@ +--- +title: sync.Pool[WIP] +weight: 5 +bookCollapseSection: true +--- + +![map struct](/images/sync/syncPool.png) diff --git a/site/content/docs/syntax_sugar/_index.md b/site/content/docs/syntax_sugar/_index.md new file mode 100644 index 0000000..11ae5e4 --- /dev/null +++ b/site/content/docs/syntax_sugar/_index.md @@ -0,0 +1,6 @@ +--- +title: 语法糖 +weight: 5 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/syntax_sugar/defer.md b/site/content/docs/syntax_sugar/defer.md new file mode 100644 index 0000000..45a4cc1 --- /dev/null +++ b/site/content/docs/syntax_sugar/defer.md @@ -0,0 +1 @@ +# defer 的实现 diff --git a/site/content/docs/system_programming/_index.md b/site/content/docs/system_programming/_index.md new file mode 100644 index 0000000..feb46d7 --- /dev/null +++ b/site/content/docs/system_programming/_index.md @@ -0,0 +1,6 @@ +--- +title: 系统编程 +weight: 5 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/system_programming/syscall.md b/site/content/docs/system_programming/syscall.md new file mode 100644 index 0000000..7bf6a30 --- /dev/null +++ b/site/content/docs/system_programming/syscall.md @@ -0,0 +1 @@ +# syscall 理论 diff --git a/site/content/docs/system_programming/vdso.md b/site/content/docs/system_programming/vdso.md new file mode 100644 index 0000000..63b9d7f --- /dev/null +++ b/site/content/docs/system_programming/vdso.md @@ -0,0 +1 @@ +# vdso syscall diff --git a/site/content/docs/third_party/_index.md b/site/content/docs/third_party/_index.md new file mode 100644 index 0000000..21f7d69 --- /dev/null +++ b/site/content/docs/third_party/_index.md @@ -0,0 +1,6 @@ +--- +title: 第三方库 +weight: 5 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/third_party/parser.md b/site/content/docs/third_party/parser.md new file mode 100644 index 0000000..a3c71f6 --- /dev/null +++ b/site/content/docs/third_party/parser.md @@ -0,0 +1,3 @@ +# parser + +## 使用 antlr 实现 parser diff --git a/site/content/docs/time/_index.md b/site/content/docs/time/_index.md new file mode 100644 index 0000000..3402c64 --- /dev/null +++ b/site/content/docs/time/_index.md @@ -0,0 +1,6 @@ +--- +title: 时间处理 +weight: 5 +bookCollapseSection: true +draft: true +--- diff --git a/site/content/docs/time/monotonic.md b/site/content/docs/time/monotonic.md new file mode 100644 index 0000000..7bf57e7 --- /dev/null +++ b/site/content/docs/time/monotonic.md @@ -0,0 +1 @@ +# monotonic diff --git a/site/layouts/shortcodes/rawhtml.html b/site/layouts/shortcodes/rawhtml.html new file mode 100644 index 0000000..b90bea2 --- /dev/null +++ b/site/layouts/shortcodes/rawhtml.html @@ -0,0 +1,2 @@ + +{{.Inner}} diff --git a/site/resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.content b/site/resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.content new file mode 100644 index 0000000..b200717 --- /dev/null +++ b/site/resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.content @@ -0,0 +1 @@ +@charset "UTF-8";:root{--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-link:#0055bb;--color-visited-link:#8440f1;--body-background:white;--body-font-color:black;--icon-filter:none;--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.flex{display:flex}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}.clearfix::after{content:"";display:table;clear:both}html{font-size:16px;scroll-behavior:smooth;touch-action:manipulation}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background);letter-spacing:.33px;font-weight:400;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box}body *{box-sizing:inherit}h1,h2,h3,h4,h5{font-weight:400}a{text-decoration:none;color:var(--color-link)}img{vertical-align:baseline}:focus{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}aside nav ul{padding:0;margin:0;list-style:none}aside nav ul li{margin:1em 0;position:relative}aside nav ul a{display:block}aside nav ul a:hover{opacity:.5}aside nav ul ul{padding-inline-start:1rem}ul.pagination{display:flex;justify-content:center;list-style-type:none}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}.book-brand{margin-top:0}.book-brand img{height:1.5em;width:auto;vertical-align:middle;margin-inline-end:.5rem}.book-menu{flex:0 0 16rem;font-size:.875rem}.book-menu .book-menu-content{width:16rem;padding:1rem;background:var(--body-background);position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;cursor:pointer;word-wrap:break-word}.book-menu a.active{color:var(--color-link)}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label+ul{display:block}.book-menu input.toggle+label::after{content:"▸"}.book-menu input.toggle:checked+label::after{content:"▾"}body[dir=rtl] .book-menu input.toggle+label::after{content:"◂"}body[dir=rtl] .book-menu input.toggle:checked+label::after{content:"▾"}.book-section-flat{margin-bottom:2rem}.book-section-flat:not(:first-child){margin-top:2rem}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:3rem}.book-header{display:none;margin-bottom:1rem}.book-header label{line-height:0}.book-header img.book-icon{height:1.5em;width:1.5em}.book-search{position:relative;margin:1rem 0;border-bottom:1px solid transparent}.book-search input{width:100%;padding:.5rem;border:0;border-radius:.25rem;background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:.75rem}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:.875rem}.book-footer img{height:1em;width:1em;margin-inline-end:.5rem}.book-comments{margin-top:1rem}.book-languages{position:relative;overflow:visible;padding:1rem;margin:-1rem}.book-languages ul{margin:0;padding:0;list-style:none}.book-languages ul li{white-space:nowrap;cursor:pointer}.book-languages:hover .book-languages-list,.book-languages:focus .book-languages-list,.book-languages:focus-within .book-languages-list{display:block}.book-languages .book-languages-list{display:none;position:absolute;bottom:100%;left:0;padding:.5rem 0;background:var(--body-background);box-shadow:0 0 .25rem rgba(0,0,0,.1)}.book-languages .book-languages-list li img{opacity:.25}.book-languages .book-languages-list li.active img,.book-languages .book-languages-list li:hover img{opacity:initial}.book-languages .book-languages-list a{color:inherit;padding:.5rem 1rem}.book-home{padding:1rem}.book-menu-content,.book-toc-content,.book-page,.book-header aside,.markdown{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){#menu-control,#toc-control{display:inline}.book-menu{visibility:hidden;margin-inline-start:-16rem;font-size:16px;z-index:1}.book-toc{display:none}.book-header{display:block}#menu-control:focus~main label[for=menu-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:absolute;top:0;bottom:0;left:0;right:0}#toc-control:focus~main label[for=toc-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@font-face{font-family:roboto;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-regular.woff2)format("woff2"),url(fonts/roboto-v27-latin-regular.woff)format("woff")}@font-face{font-family:roboto;font-style:normal;font-weight:700;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-700.woff2)format("woff2"),url(fonts/roboto-v27-latin-700.woff)format("woff")}@font-face{font-family:roboto mono;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-mono-v13-latin-regular.woff2)format("woff2"),url(fonts/roboto-mono-v13-latin-regular.woff)format("woff")}body{font-family:roboto,sans-serif}code{font-family:roboto mono,monospace}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:400;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;vertical-align:middle;text-decoration:none}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus{opacity:initial}.markdown h4,.markdown h5,.markdown h6{font-weight:bolder}.markdown h5{font-size:.875em}.markdown h6{font-size:.75em}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a:hover{text-decoration:underline}.markdown a:visited{color:var(--color-visited-link)}.markdown img{max-width:100%}.markdown code{padding:0 .25rem;background:var(--gray-200);border-radius:.25rem;font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border-radius:.25rem;overflow-x:auto}.markdown pre code{padding:0;background:0 0}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:.25rem}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200)}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;border:1px solid var(--gray-200);border-radius:.25rem}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer}.markdown details[open] summary{margin-bottom:0}.markdown figure{margin:1rem 0}.markdown figure figcaption p{margin-top:0}.markdown-inner>:first-child{margin-top:0}.markdown-inner>:last-child{margin-bottom:0}.markdown .book-expand{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden}.markdown .book-expand .book-expand-head{background:var(--gray-100);padding:.5rem 1rem;cursor:pointer}.markdown .book-expand .book-expand-content{display:none;padding:1rem}.markdown .book-expand input[type=checkbox]:checked+.book-expand-content{display:block}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden;display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-tabs input[type=radio]:focus+label{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}.markdown .book-columns{margin-left:-1rem;margin-right:-1rem}.markdown .book-columns>div{margin:1rem 0;min-width:10rem;padding:0 1rem}.markdown a.book-btn{display:inline-block;font-size:.875rem;color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:.25rem;cursor:pointer}.markdown a.book-btn:hover{text-decoration:none}.markdown .book-hint.info{border-color:#6bf;background-color:rgba(102,187,255,.1)}.markdown .book-hint.warning{border-color:#fd6;background-color:rgba(255,221,102,.1)}.markdown .book-hint.danger{border-color:#f66;background-color:rgba(255,102,102,.1)} \ No newline at end of file diff --git a/site/resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.json b/site/resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.json new file mode 100644 index 0000000..112aa6d --- /dev/null +++ b/site/resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.json @@ -0,0 +1 @@ +{"Target":"book.min.958cea7827621d6fbcb3acf091344c3e44e3d2a9428f9c3c38bb9eb37bf8c45d.css","MediaType":"text/css","Data":{"Integrity":"sha256-lYzqeCdiHW+8s6zwkTRMPkTj0qlCj5w8OLues3v4xF0="}} \ No newline at end of file diff --git a/site/static/images/index/banner.jpg b/site/static/images/index/banner.jpg new file mode 100644 index 0000000..8e14478 Binary files /dev/null and b/site/static/images/index/banner.jpg differ diff --git a/site/static/images/runtime/block_on_channel_send.jpg b/site/static/images/runtime/block_on_channel_send.jpg new file mode 100644 index 0000000..2462fa1 Binary files /dev/null and b/site/static/images/runtime/block_on_channel_send.jpg differ diff --git a/site/static/images/runtime/data_struct/map.png b/site/static/images/runtime/data_struct/map.png new file mode 100644 index 0000000..b2d2bfe Binary files /dev/null and b/site/static/images/runtime/data_struct/map.png differ diff --git a/site/static/images/runtime/data_struct/map_func_translate2.png b/site/static/images/runtime/data_struct/map_func_translate2.png new file mode 100644 index 0000000..761edec Binary files /dev/null and b/site/static/images/runtime/data_struct/map_func_translate2.png differ diff --git a/site/static/images/runtime/data_struct/map_function_translate.png b/site/static/images/runtime/data_struct/map_function_translate.png new file mode 100644 index 0000000..8ec419f Binary files /dev/null and b/site/static/images/runtime/data_struct/map_function_translate.png differ diff --git a/site/static/images/runtime/data_struct/map_tophash.png b/site/static/images/runtime/data_struct/map_tophash.png new file mode 100644 index 0000000..39e85b8 Binary files /dev/null and b/site/static/images/runtime/data_struct/map_tophash.png differ diff --git a/site/static/images/runtime/memory/gcphases.jpg b/site/static/images/runtime/memory/gcphases.jpg new file mode 100644 index 0000000..ecfd519 Binary files /dev/null and b/site/static/images/runtime/memory/gcphases.jpg differ diff --git a/site/static/images/runtime/memory/write_barrier_demo.jpg b/site/static/images/runtime/memory/write_barrier_demo.jpg new file mode 100644 index 0000000..9245d11 Binary files /dev/null and b/site/static/images/runtime/memory/write_barrier_demo.jpg differ diff --git a/site/static/images/runtime/schedule/sche_big.png b/site/static/images/runtime/schedule/sche_big.png new file mode 100644 index 0000000..0183c4f Binary files /dev/null and b/site/static/images/runtime/schedule/sche_big.png differ diff --git a/site/static/images/sync/syncPool.png b/site/static/images/sync/syncPool.png new file mode 100644 index 0000000..63c87a8 Binary files /dev/null and b/site/static/images/sync/syncPool.png differ diff --git a/site/themes/book/.github/workflows/main.yml b/site/themes/book/.github/workflows/main.yml new file mode 100644 index 0000000..67f73e1 --- /dev/null +++ b/site/themes/book/.github/workflows/main.yml @@ -0,0 +1,24 @@ +name: Build with Hugo + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + hugo-version: + - 'latest' + - '0.68.0' + steps: + - uses: actions/checkout@v2 + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v2 + with: + hugo-version: ${{ matrix.hugo-version }} + extended: true + + - name: Run Hugo + working-directory: exampleSite + run: hugo --themesDir ../.. diff --git a/site/themes/book/.gitignore b/site/themes/book/.gitignore new file mode 100644 index 0000000..e52eb52 --- /dev/null +++ b/site/themes/book/.gitignore @@ -0,0 +1,3 @@ +public/ +exampleSite/public/ +.DS_Store diff --git a/site/themes/book/LICENSE b/site/themes/book/LICENSE new file mode 100644 index 0000000..e7a669a --- /dev/null +++ b/site/themes/book/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 Alex Shpak + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/site/themes/book/README.md b/site/themes/book/README.md new file mode 100644 index 0000000..c879150 --- /dev/null +++ b/site/themes/book/README.md @@ -0,0 +1,327 @@ +# Hugo Book Theme + +[![Hugo](https://img.shields.io/badge/hugo-0.68-blue.svg)](https://gohugo.io) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +![Build with Hugo](https://github.com/alex-shpak/hugo-book/workflows/Build%20with%20Hugo/badge.svg) + +### [Hugo](https://gohugo.io) documentation theme as simple as plain book + +![Screenshot](https://github.com/alex-shpak/hugo-book/blob/master/images/screenshot.png) + +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) +- [Menu](#menu) +- [Blog](#blog) +- [Configuration](#configuration) +- [Shortcodes](#shortcodes) +- [Versioning](#versioning) +- [Contributing](#contributing) + +## Features + +- Clean simple design +- Light and Mobile-Friendly +- Multi-language support +- Customisable +- Zero initial configuration +- Handy shortcodes +- Comments support +- Simple blog and taxonomy +- Primary features work without JavaScript +- Dark Mode + +## Requirements + +- Hugo 0.68 or higher +- Hugo extended version, read more [here](https://gohugo.io/news/0.48-relnotes/) + +## Installation + +Navigate to your hugo project root and run: + +``` +git submodule add https://github.com/alex-shpak/hugo-book themes/book +``` + +Then run hugo (or set `theme = "book"`/`theme: book` in configuration file) + +``` +hugo server --minify --theme book +``` + +### Creating site from scratch + +Below is an example on how to create a new site from scratch: + +```sh +hugo new site mydocs; cd mydocs +git init +git submodule add https://github.com/alex-shpak/hugo-book themes/book +cp -R themes/book/exampleSite/content . +``` + +```sh +hugo server --minify --theme book +``` + +## Menu + +### File tree menu (default) + +By default, the theme will render pages from the `content/docs` section as a menu in a tree structure. +You can set `title` and `weight` in the front matter of pages to adjust the order and titles in the menu. + +### Leaf bundle menu + +You can also use leaf bundle and the content of its `index.md` file as menu. +Given you have the following file structure: + +``` +├── content +│ ├── docs +│ │ ├── page-one.md +│ │ └── page-two.md +│ └── posts +│ ├── post-one.md +│ └── post-two.md +``` + +Create a file `content/menu/index.md` with the content: + +```md ++++ +headless = true ++++ + +- [Book Example]({{< relref "/docs/" >}}) + - [Page One]({{< relref "/docs/page-one" >}}) + - [Page Two]({{< relref "/docs/page-two" >}}) +- [Blog]({{< relref "/posts" >}}) +``` + +And Enable it by setting `BookMenuBundle: /menu` in Site configuration. + +- [Example menu](https://github.com/alex-shpak/hugo-book/blob/master/exampleSite/content/menu/index.md) +- [Example config file](https://github.com/alex-shpak/hugo-book/blob/master/exampleSite/config.yaml) +- [Leaf bundles](https://gohugo.io/content-management/page-bundles/) + +## Blog + +A simple blog is supported in the section `posts`. +A blog is not the primary usecase of this theme, so it has only minimal features. + +## Configuration + +### Site Configuration + +There are a few configuration options that you can add to your `config.toml` file. +You can also see the `yaml` example [here](https://github.com/alex-shpak/hugo-book/blob/master/exampleSite/config.yaml). + +```toml +# (Optional) Set Google Analytics if you use it to track your website. +# Always put it on the top of the configuration file, otherwise it won't work +googleAnalytics = "UA-XXXXXXXXX-X" + +# (Optional) If you provide a Disqus shortname, comments will be enabled on +# all pages. +disqusShortname = "my-site" + +# (Optional) Set this to true if you use capital letters in file names +disablePathToLower = true + +# (Optional) Set this to true to enable 'Last Modified by' date and git author +# information on 'doc' type pages. +enableGitInfo = true + +# (Optional) Theme is intended for documentation use, therefore it doesn't render taxonomy. +# You can remove related files with config below +disableKinds = ['taxonomy', 'taxonomyTerm'] + +[params] + # (Optional, default light) Sets color theme: light, dark or auto. + # Theme 'auto' switches between dark and light modes based on browser/os preferences + BookTheme = 'light' + + # (Optional, default true) Controls table of contents visibility on right side of pages. + # Start and end levels can be controlled with markup.tableOfContents setting. + # You can also specify this parameter per page in front matter. + BookToC = true + + # (Optional, default none) Set the path to a logo for the book. If the logo is + # /static/logo.png then the path would be 'logo.png' + BookLogo = 'logo.png' + + # (Optional, default none) Set leaf bundle to render as side menu + # When not specified file structure and weights will be used + BookMenuBundle = '/menu' + + # (Optional, default docs) Specify section of content to render as menu + # You can also set value to "*" to render all sections to menu + BookSection = 'docs' + + # Set source repository location. + # Used for 'Last Modified' and 'Edit this page' links. + BookRepo = 'https://github.com/alex-shpak/hugo-book' + + # Specifies commit portion of the link to the page's last modified commit hash for 'doc' page + # type. + # Required if 'BookRepo' param is set. + # Value used to construct a URL consisting of BookRepo/BookCommitPath/ + # Github uses 'commit', Bitbucket uses 'commits' + BookCommitPath = 'commit' + + # Enable 'Edit this page' links for 'doc' page type. + # Disabled by default. Uncomment to enable. Requires 'BookRepo' param. + # Path must point to the site directory. + BookEditPath = 'edit/master/exampleSite' + + # (Optional, default January 2, 2006) Configure the date format used on the pages + # - In git information + # - In blog posts + BookDateFormat = 'Jan 2, 2006' + + # (Optional, default true) Enables search function with flexsearch, + # Index is built on fly, therefore it might slowdown your website. + # Configuration for indexing can be adjusted in i18n folder per language. + BookSearch = true + + # (Optional, default true) Enables comments template on pages + # By default partials/docs/comments.html includes Disqus template + # See https://gohugo.io/content-management/comments/#configure-disqus + # Can be overwritten by same param in page frontmatter + BookComments = true + + # /!\ This is an experimental feature, might be removed or changed at any time + # (Optional, experimental, default false) Enables portable links and link checks in markdown pages. + # Portable links meant to work with text editors and let you write markdown without {{< relref >}} shortcode + # Theme will print warning if page referenced in markdown does not exists. + BookPortableLinks = true + + # /!\ This is an experimental feature, might be removed or changed at any time + # (Optional, experimental, default false) Enables service worker that caches visited pages and resources for offline use. + BookServiceWorker = true +``` + +### Multi-Language Support + +Theme supports Hugo's [multilingual mode](https://gohugo.io/content-management/multilingual/), just follow configuration guide there. You can also tweak search indexing configuration per language in `i18n` folder. + +### Page Configuration + +You can specify additional params in the front matter of individual pages: + +```toml +# Set type to 'docs' if you want to render page outside of configured section or if you render section other than 'docs' +type = 'docs' + +# Set page weight to re-arrange items in file-tree menu (if BookMenuBundle not set) +weight = 10 + +# (Optional) Set to 'true' to mark page as flat section in file-tree menu (if BookMenuBundle not set) +bookFlatSection = false + +# (Optional) Set to hide nested sections or pages at that level. Works only with file-tree menu mode +bookCollapseSection = true + +# (Optional) Set true to hide page or section from side menu (if BookMenuBundle not set) +bookHidden = false + +# (Optional) Set 'false' to hide ToC from page +bookToC = true + +# (Optional) If you have enabled BookComments for the site, you can disable it for specific pages. +bookComments = true + +# (Optional) Set to 'false' to exclude page from search index. +bookSearchExclude = true +``` + +### Partials + +There are few empty partials you can override in `layouts/partials/` + +| Partial | Placement | +| -------------------------------------------------- | ------------------------------------------- | +| `layouts/partials/docs/inject/head.html` | Before closing `` tag | +| `layouts/partials/docs/inject/body.html` | Before closing `` tag | +| `layouts/partials/docs/inject/footer.html` | After page footer content | +| `layouts/partials/docs/inject/menu-before.html` | At the beginning of `