<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[chainhelen blog]]></title><description><![CDATA[listening]]></description><link>http://blog.hacking.pub/</link><generator>Ghost 0.11</generator><lastBuildDate>Tue, 27 Aug 2024 04:09:26 GMT</lastBuildDate><atom:link href="http://blog.hacking.pub/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[国内环境运行openapi库]]></title><description><![CDATA[<p>代码如下，直接运行会报错，</p>

<pre><code>from openai import OpenAI

client = OpenAI(api_key = "") // 这里填入自己的API_KEY，在openapi官网帐号上有

# Prompt for the conversation

response = client.chat.completions.create(  
  model="gpt-3.5-turbo",
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Who won the world series in 2020?"},
    {"role": "assistant", "content": "The Los Angeles</code></pre>]]></description><link>http://blog.hacking.pub/2024/05/02/langchainguo-nei-huan-jing/</link><guid isPermaLink="false">4bfda649-9695-4a6e-ba17-d1e5264ac5d7</guid><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Thu, 02 May 2024 07:10:41 GMT</pubDate><content:encoded><![CDATA[<p>代码如下，直接运行会报错，</p>

<pre><code>from openai import OpenAI

client = OpenAI(api_key = "") // 这里填入自己的API_KEY，在openapi官网帐号上有

# Prompt for the conversation

response = client.chat.completions.create(  
  model="gpt-3.5-turbo",
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Who won the world series in 2020?"},
    {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
    {"role": "user", "content": "Where was it played?"}
  ]
)

print(completion.choices[0].message.content)  
</code></pre>

<p>本人通常使用Clash软件翻墙，sock5代理，openai库最好直接使用http代理，所以需要将sock5代理转换成http代理  </p>

<pre><code>sudo apt-get install privoxy // 安装 privoxy

sudo vim /etc/privoxy/config // 变更配置  
在里面添加：
# 其中forward-socks5 是当前clash监听的sock5代理
# listen-address 是转化后http代理地址 

forward-socks5   /               127.0.0.1:7890 .  
listen-address localhost:8118  
</code></pre>

<p>本地终端直接设置http代理</p>

<pre><code>export https_proxy=http://localhost:8118  
export http_proxy=http://localhost:8118  
</code></pre>

<p>继续运行上述代码</p>

<pre><code>openai.RateLimitError: Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}  
</code></pre>

<p>说要已经调用成功，但是没有充值</p>]]></content:encoded></item><item><title><![CDATA[xv6系列 util实验]]></title><description><![CDATA[<h4 id="sleep">Sleep</h4>

<p>没啥难度，主要调用系统调用 sleep</p>

<pre><code>#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int  
main(int argc, char *argv[])  
{
    char *err1 = "not right\0";
    char *err2 = "sleep wrong\0";

    if (argc != 2) {
        write(2, err1, strlen(err1));
    }
    int n = atoi(argv[1]);
    int ret = sleep(n);
    if</code></pre>]]></description><link>http://blog.hacking.pub/2022/02/09/untitled-8/</link><guid isPermaLink="false">8c5537d3-079a-4b5f-805a-6417585704c1</guid><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Wed, 09 Feb 2022 14:37:26 GMT</pubDate><content:encoded><![CDATA[<h4 id="sleep">Sleep</h4>

<p>没啥难度，主要调用系统调用 sleep</p>

<pre><code>#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int  
main(int argc, char *argv[])  
{
    char *err1 = "not right\0";
    char *err2 = "sleep wrong\0";

    if (argc != 2) {
        write(2, err1, strlen(err1));
    }
    int n = atoi(argv[1]);
    int ret = sleep(n);
    if (ret &lt; 0) {
        write(2, err2, strlen(err2));
    }
    exit(0);
}
</code></pre>

<h4 id="pingpong">pingpong</h4>

<p>熟悉一下fork以及pipe的用法，xv6 book介绍比较详细  </p>

<pre><code>#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int  
main(int argc, char *argv[])  
{
    char *err1 = "len(argc) not right\n0";
//    char *err2 = "pipe faile\0";
    char *err3 = "fork faile\0";

    char buf[8];

    if (argc != 1) {
        write(2, err1, strlen(err1));
        exit(1);
    }
    int p[2];
    pipe(p);

    int pid = fork();
    if (pid &lt; 0) {
        write(2, err3, strlen(err3));
        exit(1);
    }
    if (pid == 0) {
        // child receive one byte
        if (read(p[0], buf, 1) == 1) {
            printf("%d: received pong\n", getpid());
        }
        // child send one byte
        write(p[1], "c", 1);
        exit(0);
    }

    // parent send one byte
    write(p[1], "p", 1);
    // parent receive one byte
    if (read(p[0], buf, 1) == 1) {
        printf("%d: received ping\n", getpid());
    }

    exit(0);
}
</code></pre>

<h4 id="primes">primes</h4>

<p>看网上到处都是递归版本，实际只要找到循环节是啥即可  </p>

<p>有两个注意的细节点
1. close要放在前面，只读不写的需要先把写close了，否则另一个子进程可能会卡死 <br>
2. wait需要加一下，否则输出容易串场  </p>

<pre><code>#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int  
main(int argc, char *argv[])  
{
    char *err1 = "fork failed\0";

    int prev[2];
    pipe(prev);

    int pid = fork();

    if (pid &lt; 0) {
        write(2, err1,  strlen(err1));
        exit(1);
    }

    if (pid == 0) {
        while(1) {
            int base = 0;
            close(prev[1]);  // 有两个点需要注意，这个close要放在前面，只读不写的需要先把写close了;wait需要加一下，否则容易串场
            if(0 == read(prev[0], &amp;base, sizeof(base))) {
                // printf("read == 0\n");
                close(prev[0]);
                exit(0);
            }
            // printf("read &lt;- %d\n", base);
            printf("prime %d\n", base);

            int next[2];
            pipe(next);
            int pid = fork();
            if (pid &lt; 0) {
                write(2, err1, strlen(err1));
                exit(1);
            }

            // parent
            if (pid &gt; 0) {
                int cur = 0;
                for(;0 != read(prev[0], &amp;cur, sizeof(cur));) {
                    if (0 != cur % base) {
                        write(next[1], &amp;cur, sizeof(cur));
                    }
                }
                close(prev[0]);
                close(next[0]);
                close(next[1]);
                wait(0);
                exit(0);
            }

            // child
            close(prev[0]);
            prev[0] = next[0];
            prev[1] = next[1];
        }
        exit(0);
    }

    // parent
    close(prev[0]);
    for (int i = 2;i &lt;= 32;i++) {
        // printf("write -&gt; %d\n", i);
        write(prev[1], &amp;i, sizeof(i));
    }
    close(prev[1]);
    wait(0);

    exit(0);
}
</code></pre>

<h4 id="find">find</h4>

<ol>
<li>pfilename是稍微改了ls.c的fmtfilename  </li>
<li>c语言中的字符串最后末尾不上 0 </li>
</ol>

<pre><code>#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

char*  
pfilename(char *path)  
{
  char *p;

  // Find first character after last slash.
  for(p=path+strlen(path); p &gt;= path &amp;&amp; *p != '/'; p--)
    ;
  p++;

  return p;
}



int find(char *path, char *filename) {  
    int fd;
    char buf[512], *p;
    struct stat st;
    struct dirent de;

    if ((fd = open(path, 0)) &lt; 0) {
        fprintf(2, "find: cannot open %s\n", path);
        return -1;
    }
    if(fstat(fd, &amp;st) &lt; 0) {
        fprintf(2, "find: cannot stat %s\n", path);
        close(fd);
        return -1;
    }

    switch(st.type) {
        case T_FILE:
            if(0 == strcmp(pfilename(path), filename)) {
                fprintf(1, "%s\n", path);
            }
            break;
        case T_DIR:
            strcpy(buf, path);
            p = buf + strlen(buf);
            *p++='/';
            while(read(fd, &amp;de, sizeof(de)) == sizeof(de)) {
                if(de.inum == 0) {
                    continue;
                }
                if(0 == strcmp(".", de.name) || 0 == strcmp("..", de.name)) {
                    continue;
                }
                memmove(p, de.name, DIRSIZ);
                p[DIRSIZ] = 0;
                if(stat(buf, &amp;st) &lt; 0) {
                    printf("find: cannot stat %s\n", buf);
                    continue;
                }

                find(buf, filename);
            }
            break;
    }
    close(fd);
    return 0;
}

int  
main(int argc, char *argv[])  
{
    if (argc &lt; 3) {
        fprintf(2, "not enough arguments\n");
        exit(1);
    }
    find(argv[1], argv[2]);
    exit(0);
}
</code></pre>]]></content:encoded></item><item><title><![CDATA[xv6 系列一（搭建环境）]]></title><description><![CDATA[<h4 id="">准备</h4>

<p>需要三个部分
1. RISC-V 工具链 <br>
2. 虚拟机 <br>
3. xv6源码</p>

<h4 id="1">1. 工具链</h4>

<h6 id="riscv">RISC-V工具链</h6>

<p>RISC-V 建议采用手动编译的方式，<code>brew install riscv-tools</code>相对于来说比较卡  </p>

<pre><code>git clone --recursive https://github.com/riscv/riscv-gnu-toolchain  
</code></pre>

<p>代码下载时间比较久</p>

<p>问题： <br>
a. 如果下载sub module下载失败了，需要进入 <code>riscv-gnu-toolchain</code>目录下，重新执行 <code>git submodule update --init --recursive</code> 下载 <br>
b. 遇到错误<code>/bin/sh: flock: command not found</code>，同上</p>]]></description><link>http://blog.hacking.pub/2022/02/04/xv6-xi-lie-da-jian-huan-jing/</link><guid isPermaLink="false">9cff0837-450c-4313-a517-2652a28c1434</guid><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Fri, 04 Feb 2022 12:54:59 GMT</pubDate><content:encoded><![CDATA[<h4 id="">准备</h4>

<p>需要三个部分
1. RISC-V 工具链 <br>
2. 虚拟机 <br>
3. xv6源码</p>

<h4 id="1">1. 工具链</h4>

<h6 id="riscv">RISC-V工具链</h6>

<p>RISC-V 建议采用手动编译的方式，<code>brew install riscv-tools</code>相对于来说比较卡  </p>

<pre><code>git clone --recursive https://github.com/riscv/riscv-gnu-toolchain  
</code></pre>

<p>代码下载时间比较久</p>

<p>问题： <br>
a. 如果下载sub module下载失败了，需要进入 <code>riscv-gnu-toolchain</code>目录下，重新执行 <code>git submodule update --init --recursive</code> 下载 <br>
b. 遇到错误<code>/bin/sh: flock: command not found</code>，同上 <br>
c. 遇到错误<code>configure: error: GNU awk not found</code>，手动安装<code>brew install gawk</code> 以及 <code>brew install gsed</code>    </p>

<h6 id="">工具链编译</h6>

<pre><code>./configure --prefix=/usr/local/opt/riscv-gnu-toolchain    #配置产物路径
make                                                       #编译  
</code></pre>

<h6 id="">添加环境变量</h6>

<p>bash 放在 ~/.bash_profile，如果zsh放在 ~/.zshrc  </p>

<pre><code>export PATH=$PATH:/usr/local/opt/riscv-gnu-toolchain/bin  
source ~/.zshrc  
</code></pre>

<p>键入 <code>riscv64-unknown-elf-gcc -v</code> 如果能显示版本说明安装成功</p>

<h4 id="2qemu">2. QEMU</h4>

<pre><code>brew install qemu  
</code></pre>

<h4 id="3xv6">3. xv6</h4>

<p><a href="https://pdos.csail.mit.edu/6.828/2021/xv6.html">mit xv6</a> 这里提示下载源码 </p>

<pre><code>git clone git://github.com/mit-pdos/xv6-riscv.git  
</code></pre>

<p>然后 进入 目录<code>xv6-riscv</code>，键入<code>make &amp;&amp; make qemu</code></p>]]></content:encoded></item><item><title><![CDATA[golang 异步抢占例子]]></title><description><![CDATA[<p>golang 异步抢占例子，继上篇文章《golang 非协作式抢占》添加一些手操事例</p>

<p>一定要go1.14以后的版本，本文版本</p>

<pre><code>go version go1.15.1 linux/amd64  
</code></pre>

<p>源码<code>cat main.go</code></p>

<pre><code>package main

import (  
    "fmt"
    "golang.org/x/sys/unix"
    "runtime"
    "time"
)

func main() {  
    runtime.GOMAXPROCS(1)
    for {
        time.Sleep(time.Second * 5)
        go func() {
            fmt.Printf("tid %d\n", unix.</code></pre>]]></description><link>http://blog.hacking.pub/2020/09/04/golang-yi-bu-qiang-zhan-li-zi/</link><guid isPermaLink="false">21d900c5-533d-4a5c-b194-0d28897ad8e0</guid><category><![CDATA[golang]]></category><category><![CDATA[go语言]]></category><category><![CDATA[go源码]]></category><category><![CDATA[godbg]]></category><category><![CDATA[异步]]></category><category><![CDATA[非协作式抢占]]></category><category><![CDATA[异步抢占]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Fri, 04 Sep 2020 11:17:39 GMT</pubDate><media:content url="http://img.hacking.pub/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/aysnc_preemtion.png" medium="image"/><content:encoded><![CDATA[<img src="http://img.hacking.pub/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/aysnc_preemtion.png" alt="golang 异步抢占例子"><p>golang 异步抢占例子，继上篇文章《golang 非协作式抢占》添加一些手操事例</p>

<p>一定要go1.14以后的版本，本文版本</p>

<pre><code>go version go1.15.1 linux/amd64  
</code></pre>

<p>源码<code>cat main.go</code></p>

<pre><code>package main

import (  
    "fmt"
    "golang.org/x/sys/unix"
    "runtime"
    "time"
)

func main() {  
    runtime.GOMAXPROCS(1)
    for {
        time.Sleep(time.Second * 5)
        go func() {
            fmt.Printf("tid %d\n", unix.Gettid())
            for {
            }
        }()
    }
    time.Sleep(time.Hour)
}
</code></pre>

<p>编译</p>

<pre><code>go build -o main main.go  
</code></pre>

<h4 id="">运行</h4>

<p>两个shell终端，一个用于执行，一个查看<code>strace</code></p>

<h5 id="">关闭异步抢占</h5>

<p>第一个终端执行  <code>GODEBUG=asyncpreemptoff=1 ./main</code>  可以看到输出了一个<code>tid 25674</code></p>

<p>第二个终端执行 <code>strace -p 25674</code>，可以看到显示  </p>

<p><img src="http://img.hacking.pub/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/1.png" alt="golang 异步抢占例子"></p>

<h5 id="">开启异步抢占</h5>

<p>第一个终端执行 <code>./main</code>，可以看到输出了一个<code>tid  3141</code></p>

<p>第二终端执行<code>strace -p 3141</code> 可以看到右侧显示大量的SIGURG信号</p>

<p><img src="http://img.hacking.pub/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/2.png" alt="golang 异步抢占例子"></p>

<p>注意到右侧的si_pid 与 tid不同，也就是虽然 GMP中虽然 P被设置了1，但是 M这个时候数量是多于 P的。
发送的信号之后主要做的是保存 ctx，接着 M与P depatch掉。（</p>

<h6 id="gc">GC</h6>

<p>通常发生在饥饿态。当然如果搞复杂一点，也可以弄一个 <code>GC</code> 想要 stop the word，但是同步抢占无法 depatch其中一个P。我们知道 stop the word，所有的M需要与 P depath，然后进入<code>idle</code>列表。
这种无法<code>depathc</code> 会拉长整个 GC时间，甚至是挂起整个进程。上篇《golang 非协作抢占》有链接可以看一下奇葩的事例。所以异步抢占，是有利于 GC的，但是整体来说，基于信号的方式，性能非常差。</p>

<h5 id="">调试器</h5>

<p>对于调试器来说，会拦截到的本该进程接受到的<code>SIGURG</code>，所以需要对其忽略，这个是golang异步占用的原提议提到的（看我上篇文章）</p>

<p>golang的dlv <a href="https://github.com/go-delve/delve/issues/1754">https://github.com/go-delve/delve/issues/1754</a> 会收到影响</p>

<p>比方说我写的go调试器toy，里面需要将<a href="https://github.com/chainhelen/godbg/pull/6">https://github.com/chainhelen/godbg/pull/6</a> 信号忽略</p>

<h4 id="tightloop">tight loop</h4>

<p>另外需要注意的是可能有人说，上面这个例子<code>for</code>是个死循环，正常不会写这种代码。</p>

<p>但是实际生产环境，由于编译器不添加 <code>gcflag='-N -l'</code>，SSA阶段会做大量的指令和内联优化，</p>

<p>是有可能把函数调用优化成上述的代码，当然这是另一种go用法的失误（或者说go设计者并不希望你这么用，详细可以看上篇《golang 非协作抢占》里面有几个链接都是比较好的事例）。</p>

<h4 id="bug">其他BUG</h4>

<ol>
<li><p>正常运行的时候，<code>GODEBUG=asyncpreemptoff=1</code>开关关闭，就是说不允许异步抢占（进程自己不发送<code>SIGURG emit</code>），那么就不会发生协程异步抢占了吗？哈哈，有人就是手贱，手动发送。<a href="https://github.com/golang/go/issues/38531">https://github.com/golang/go/issues/38531</a></p></li>
<li><p>还有一些跑满CPU的bug，<a href="https://github.com/golang/go/issues/37741">https://github.com/golang/go/issues/37741</a>  </p></li>
<li>还有有人提议把runtime 自己产生的信号，过滤掉，不要发送到用户态的代码 <a href="https://github.com/golang/go/issues/37942">https://github.com/golang/go/issues/37942</a></li>
</ol>

<h4 id="">代码位置</h4>

<p><strong>下面是分析源码，不喜欢看的可以跳过。</strong>
先找一下发送信号的位置。先看最最基本的，某个G占用超过10ms的情况，由 monitor 协程发起的。</p>

<h6 id="">发送方</h6>

<pre><code>// src/runtime/proc.go
func sysmon() {  
    ...
        // retake P's blocked in syscalls
        // and preempt long running G's
        if retake(now) != 0 {
            idle = 0
        } else {
            idle++
        }
    ...
}
</code></pre>

<p>上面的这个<code>retake</code>对所有的G进行重整</p>

<pre><code>// src/runtime/proc.go

func retake(now int64) uint32 {  
    for i := 0; i &lt; len(allp); i++ {
    ...
        if s == _Prunning || s == _Psyscall {
            // Preempt G if it's running for too long.
            t := int64(_p_.schedtick)

            // 这地方就是协程自己调度了，没有被抢占
            // 所以G对应的schedtick和schedwhen跟监控的不一致，需要重新更新一些monitor的数值

            if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
            } else if pd.schedwhen+forcePreemptNS &lt;= now {

                // 如果超过了10ms就需要进行抢占了

                preemptone(_p_)
                // In case of syscall, preemptone() doesn't
                // work, because there is no M wired to P.
                sysretake = true
            }
        }
    ...
    }
}
</code></pre>

<p>上面遍历所有的P，monotor发现上次我就监控过你，这次发现你了，然后还时间间隔超了10ms，那对不起了，我要执行 <code>preemptone(_p_)</code>了。</p>

<pre><code>func preemptone(_p_ *p) bool {  
    ...

    // 注意这个两个属性其实是重复的，就是说作用是一样的，都是为了标记当前是在抢占中
    // 可以看一下preempt的注解
    gp.preempt = true
    gp.stackguard0 = stackPreempt

    // Request an async preemption of this P.
    if preemptMSupported &amp;&amp; debug.asyncpreemptoff == 0 {
        _p_.preempt = true
        preemptM(mp)
    }

    ...
}
</code></pre>

<p>在之前的版本协作式抢占中，这个标记设置好了，就只能干等着，直到 morestask (函数调用)或者gopark（IO操作，例如channel之类）才行。但是这里面明显就能看到有一个 preemptM信号通知线程。</p>

<h6 id="">接收方</h6>

<pre><code>./src/runtime/signal_unix.go

func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {  
    if sig == sigPreempt &amp;&amp; debug.asyncpreemptoff == 0 {
        // Might be a preemption signal.
        doSigPreempt(gp, c)
        // Even if this was definitely a preemption signal, it
        // may have been coalesced with another signal, so we
        // still let it through to the application.
    }
}
</code></pre>

<p>上面是发送信号的地方，<code>sigPreempt</code>是一个常量，该文件能看到 <code>const sigPreempt = _SIGURG</code></p>

<pre><code>./src/runtime/signal_unix.go

// doSigPreempt handles a preemption signal on gp.
func doSigPreempt(gp *g, ctxt *sigctxt) {  
    // Check if this G wants to be preempted and is safe to
    // preempt.
    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)
        }
    }

    // Acknowledge the preemption.
    atomic.Xadd(&amp;gp.m.preemptGen, 1)
    atomic.Store(&amp;gp.m.signalPending, 0)
}
</code></pre>

<p>这里面可以看到需要判断<code>异步安全点</code>的（就是上篇那个那个，自己看），然后返回一个可以植入的<code>pc</code>寄存器位置。</p>

<pre><code>./src/runtime/preempt.go

func isAsyncSafePoint(gp *g, pc, sp, lr uintptr) (bool, uintptr) {  
    ....

    // 上面别的地方check还挺简单的，就不贴了。
    // 但是这里面我一开始没看懂第二个条件。
    // 因为我看到asyncPreemptStack的赋值 var asyncPreemptStack = ^uintptr(0)
    // 是-1的补码，就是说如果是无符号，那么就是最大的整数
    // 所以怎么可能两个数相减超过最大（二进制全1）
    // 后来发现func init()函数里面对asyncPreemptStack重新赋值了。。。。

    // 实际第二个条件它的含义是检查的是当前G是否还够栈插入异步抢占栈空间。
    // 而且第一个条件是不能省掉的，仅用第二个条件因为可能相减（无符号）存在溢出
    // （在不stack split的情况下）
    //  
    //          ——ho      |
    //          |         |
    // bp——---- |         |
    //   |      |         |
    //   |      |         |
    //   |      |         |
    //sp ——--   |         |
    //          |       \ | /
    //          |        \|/
    //          |      stack递减方向
    //          —— lo
    //  
    if sp &lt; gp.stack.lo || sp-gp.stack.lo &lt; asyncPreemptStack {
        return false, 0
    }

    // 底下还有一些判断，大多数跟编译器生成的信息有关，主要跟pcdata、funcdata有关
}
</code></pre>

<pre><code>./src/runtime/preempt_amd64.s 

TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0

    MOVQ AX, 0(SP)
    MOVQ CX, 8(SP)
    MOVQ DX, 16(SP)
    MOVQ BX, 24(SP)
    ...
    CALL ·asyncPreempt2(SB)
    ...
    MOVQ 16(SP), DX
    MOVQ 8(SP), CX
    MOVQ 0(SP), AX
</code></pre>

<p>这部分是汇编实现的，我个人电脑是amd64/linux，所以看的是这个文件，这里面其实就是保存了所有的寄存器，然后偷偷在中间调用了asyncPreempt2（这个是golang实现的），这样让其进行了调度。再偷偷恢复了自己的寄存器。</p>

<p>后面代码就有点小复杂，而且并不是异步抢占的核心逻辑。看代码主要要看三个函数</p>

<pre><code>func preemptPark(gp *g)  
func gopreempt_m(gp *g)  
func schedule()  
</code></pre>

<p>补一张我画的天师神图，希望一路驱魔斩妖保平安</p>

<p><img src="http://img.hacking.pub/golang%20%E5%BC%82%E6%AD%A5%E6%8A%A2%E5%8D%A0%E4%BE%8B%E5%AD%90/aysnc_preemtion.png" alt="golang 异步抢占例子"></p>

<h4 id="">注意</h4>

<p>golang的异步信号是针对（target）线程(thread)级别的，通常由<code>sysmon</code>发起抢占信号。就是说对应GMP模型中的M，而最终发生切换动作的其实是 G。</p>

<blockquote>
  <p>参考：</p>
  
  <p>&lt;<a href="https://cloud.tencent.com/developer/article/1450290">因goroutine运行时间过长而发生的抢占调度</a></p>
  
  <p><a href="https://www.jianshu.com/p/604d277cbc6f">https://www.jianshu.com/p/604d277cbc6f</a></p>
  
  <p><a href="https://golang.org/doc/asm">https://golang.org/doc/asm</a></p>
  
  <p><a href="https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-06-func-again.html">https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-06-func-again.html</a></p>
  
  <p><a href="https://medium.com/a-journey-with-go/go-asynchronous-preemption-b5194227371c">https://medium.com/a-journey-with-go/go-asynchronous-preemption-b5194227371c</a></p>
  
  <p><a href="https://www.bookstack.cn/read/qcrao-Go-Questions/goroutine 调度器-sysmon 后台监控线程做了什么.md">https://www.bookstack.cn/read/qcrao-Go-Questions/goroutine%20%E8%B0%83%E5%BA%A6%E5%99%A8-sysmon%20%E5%90%8E%E5%8F%B0%E7%9B%91%E6%8E%A7%E7%BA%BF%E7%A8%8B%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88.md</a></p>
</blockquote>]]></content:encoded></item><item><title><![CDATA[关于沟通]]></title><description><![CDATA[<p>近一年，严格来说可能是11个月，我时常反思关于沟通的问题。一直到近几个月的沉默。我理解都是沟通是原罪。以下言论都是我抹去了大多性格上的短板因素，刚好也是去年这个时候开始想这个因素变量（令自己恐怖的是这个件事我竟然想了一年，老是觉得是上周的事情），这么做是为了减少计算量，我需要计算自己这点上的影响数值。</p>

<ol>
<li><p>首先要把自己的位置放正，其次要把态度放正。位置是要明白自己需要产生多大的影响，而且能产生多大的影响，这是一种base，目的与能力的一种权衡；其次态度可以使自己更加客观看自己，不容易受情绪影响，下的结论会更贴近真实客观。</p></li>
<li><p>需要明白沟通是需要载体，我们将 model 简化，单对单场景，载体是人。沟通方式，无非是语言、文字、可视化资料。但是往往令人忽略的是沟通载体的差异性，也就是所谓的共识。排除掉所有性格（情绪）的外界因素，剩下影响共识的是经历和学识。或者说“共识”是 “经历和学识”的一种高度抽象，然而“经历和学识”还是太过于抽象，于是在我的认知范畴内将其转化为可计算的。难点在于如何作为简化计算公式和输入（我不能随便对外把公式列出来，所以本文可能就是篇水文。但是我所有文章都是写给自己的，是希望给自己带来总结和启发）</p></li></ol>]]></description><link>http://blog.hacking.pub/2020/09/02/guan-yu-gou-tong/</link><guid isPermaLink="false">c0115474-279e-4110-8253-1961db22b78f</guid><category><![CDATA[沟通]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Wed, 02 Sep 2020 13:42:51 GMT</pubDate><content:encoded><![CDATA[<p>近一年，严格来说可能是11个月，我时常反思关于沟通的问题。一直到近几个月的沉默。我理解都是沟通是原罪。以下言论都是我抹去了大多性格上的短板因素，刚好也是去年这个时候开始想这个因素变量（令自己恐怖的是这个件事我竟然想了一年，老是觉得是上周的事情），这么做是为了减少计算量，我需要计算自己这点上的影响数值。</p>

<ol>
<li><p>首先要把自己的位置放正，其次要把态度放正。位置是要明白自己需要产生多大的影响，而且能产生多大的影响，这是一种base，目的与能力的一种权衡；其次态度可以使自己更加客观看自己，不容易受情绪影响，下的结论会更贴近真实客观。</p></li>
<li><p>需要明白沟通是需要载体，我们将 model 简化，单对单场景，载体是人。沟通方式，无非是语言、文字、可视化资料。但是往往令人忽略的是沟通载体的差异性，也就是所谓的共识。排除掉所有性格（情绪）的外界因素，剩下影响共识的是经历和学识。或者说“共识”是 “经历和学识”的一种高度抽象，然而“经历和学识”还是太过于抽象，于是在我的认知范畴内将其转化为可计算的。难点在于如何作为简化计算公式和输入（我不能随便对外把公式列出来，所以本文可能就是篇水文。但是我所有文章都是写给自己的，是希望给自己带来总结和启发）。</p></li>
</ol>

<p>沟通也是一种输入输出，其中输入成本最低，输出最难；要让对方在对于同样字眼或者画面与自身的认知概率分布尽量一致，共识才会高。其次，即便理解大致相同，但是某个小点上的差异也可能造成天差地别的结果。如何提高输出的有效率，首先需要理解输入，因为你的输出是别人的输入。输入可能是零散的，输出必须是体系的。首先需要去感受别人的输入（就是说“经历”和“学识”），他们长时间接受哪些“输入”，那么采用那种“输入”方式去输出给他。需要注意的是这种“输入”是否成体系，也就是值不值得去理解一些人的“输入”方式。要知道这些方式最终给自己带来的价值是什么。那么最大化输出价值才是最终追求的。</p>]]></content:encoded></item><item><title><![CDATA[golang  神坑range]]></title><description><![CDATA[<p>总结一下碰到的一些range问题，尤其是边loop边delete的骚操作，我一直以为是人家开源库的bug，直到我看到golang官方也这么用。</p>

<p>首先是比较经典的问题，我们团队之前确实有人写这样代码出事了。</p>

<h4 id="v">v是数据复制</h4>

<ol>
<li>代码本意是为了获取每个slice每个元素的指针，构造成新的slice</li>
</ol>

<pre><code>type P struct {  
    Name string
    Age  int
}

func main() {  
    o := []P{
        P{"chain1", 20},
        P{"chain2", 21},
        P{"chain3", 22},
    }
    oPointer := make([]*P, 0, 3)
    for _, v := range o {
        oPointer = append(oPointer, &amp;v)
    }
    fmt.Println(oPointer)
    for _, v := range oPointer</code></pre>]]></description><link>http://blog.hacking.pub/2020/09/02/golang-shen-keng-rangeuntitled-8/</link><guid isPermaLink="false">4251b858-e793-4769-954f-495159e629a2</guid><category><![CDATA[golang]]></category><category><![CDATA[go]]></category><category><![CDATA[go语言]]></category><category><![CDATA[go源码]]></category><category><![CDATA[range]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Wed, 02 Sep 2020 12:34:41 GMT</pubDate><content:encoded><![CDATA[<p>总结一下碰到的一些range问题，尤其是边loop边delete的骚操作，我一直以为是人家开源库的bug，直到我看到golang官方也这么用。</p>

<p>首先是比较经典的问题，我们团队之前确实有人写这样代码出事了。</p>

<h4 id="v">v是数据复制</h4>

<ol>
<li>代码本意是为了获取每个slice每个元素的指针，构造成新的slice</li>
</ol>

<pre><code>type P struct {  
    Name string
    Age  int
}

func main() {  
    o := []P{
        P{"chain1", 20},
        P{"chain2", 21},
        P{"chain3", 22},
    }
    oPointer := make([]*P, 0, 3)
    for _, v := range o {
        oPointer = append(oPointer, &amp;v)
    }
    fmt.Println(oPointer)
    for _, v := range oPointer {
        fmt.Println(v)
    }
}
</code></pre>

<p>我们知道最后<code>oPointer</code>打印出来都是3个同样的 &amp;{"chain3", 22}，而这个&amp;v产生的指针也并不是指向原来o中 index = 2的数据，因为range 的时候，v把 o里面的数据copy了一份，由于o[i]是结构体，所以直接copy了结构体数据</p>

<ol>
<li><p>那么第二个问题，类似，想把每一个元素的<code>age</code>都修改成 18（希望每一个看到本文的，都永远18岁）</p></li>
</ol>

<pre><code>    type P struct {
    Name string
    Age  int
}

func main() {  
    o := []P{
        P{"chain1", 20},
        P{"chain2", 21},
        P{"chain3", 22},
    }

    for _, v := range o {
        v.Age = 18
    }

    fmt.Println(o)
}
</code></pre>

<p>实际上一个都没改，逃逸分析o会被分配到堆上面，而那个v在栈上，每一次都从o[i]对应的堆上面把对象活生生copy过来，然后修改栈上面自己的年龄，对于原slice没有任何改动。</p>

<h4 id="vcopy">v copy性能</h4>

<p>这个copy 使得 对于len比较大并且元素结构体过大的slice，数据结构 range 有性能问题，可以看一下这个case <a href="https://stackoverflow.com/questions/45786687/runtime-duffcopy-is-called-a-lot">stackoverflow#45786687</a></p>

<p>主要看下<code>runtime.duffcopy</code>这个函数，在异步抢占，也特地强调了这个函数。</p>

<h4 id="">小结</h4>

<p>通过上面</p>

<ol>
<li>注意到<code>range</code>对于slice来说，最好只用于元素是指针  </li>
<li>range千万不可对slice元素进行取地址  </li>
<li>它就是数据的一个副本，如果元素不是指针，只读不要写</li>
</ol>

<h4 id="">神技</h4>

<h5 id="">删除</h5>

<p>通天神技，在dlv项目里面看到的，当时我一度以为是bug</p>

<pre><code>func (t *Target) ClearInternalBreakpoints() error {  
    bpmap := t.Breakpoints()
    threads := t.ThreadList()
    for addr, bp := range bpmap.M {                 
        ...
        delete(bpmap.M, addr)            // 就是这个！！！
    }
    return nil
}
</code></pre>

<p>但是你会发现，官网文档介绍<code>for</code>的<a href="https://golang.org/doc/effective_go.html#for">例子</a>就是这么干的！（It is safe）</p>

<pre><code>for key := range m {  
    if key.expired() {
        delete(m, key)
    }
}
</code></pre>

<p>其中介绍这样的</p>

<pre><code>The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If map entries that have not yet been reached are removed during iteration, the corresponding iteration values will not be produced. If map entries are created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0.  
</code></pre>

<h6 id="">新增</h6>

<p>恩恩，删除算个啥？<code>for</code> map 来个新增</p>

<pre><code>    data := map[string]string{"1": "A", "2": "B", "3": "C"}
    for k, v := range data {
        data[v] = k
        fmt.Printf("res %v\n", data)
    }
</code></pre>

<p>你会发现每次运行的结果都可能不一样，有时候少，有时候多，说明这样遍历打印不是safe。</p>

<p>其实动一下脑子，go的map是hash桶，也就是说 unordered，其中的hash随机种子会影响这个位置（遍历顺序）。</p>

<p>如果你看过hmap源码，就知道了</p>

<pre><code>func makemap(t *maptype, hint int, h *hmap) *hmap {  
        ...
    h.hash0 = fastrand()    // 这个！！！！
        ...
    return h
}
</code></pre>]]></content:encoded></item><item><title><![CDATA[golang 非协作式抢占]]></title><description><![CDATA[<p>最近因为dlv里面有个bug，关于go1.14版本之后非协作式抢占扩大了调试状态下的一些协程问题。</p>

<p>于是就找了原始的提议看了看，恩，果然大部分都看不懂！！！</p>

<p>利用google/baidu式的翻译，手敲了一遍，并且存疑的地方都加了一点东西，算是给自己一点做个笔录吧，后面有理解更深的地方会更新一下自己的理解。</p>

<p>翻译笔录英文版本如<a href="https://link.zhihu.com/?target=https%3A//github.com/golang/proposal/blob/5b63da9579c3b19294be614dcad33e20a9a4ad22/design/24543-non-cooperative-preemption.md">链接</a>  </p>

<h4 id="">摘要</h4>

<p>go 当前在编译器插入的函数序章中插入协作抢占点。在大多数情况下，这足以让golang开发人员忽略抢占细节，专注于编码清晰的并行代码，但是这种方式有着非常尖锐的问题，以至于我们一次又一次的看到它降低了开发人员的体验。当它出现错误时候，就会呈现出惊人的错误现象，导致未知的系统层面的延迟问题，甚至有时候会完全冻结（就是卡住不动的意思）。因为这个是存在于golang语言语义之外的语言实现问题，这些错误是非常令人吃惊的，并且非常难以调试。</p>

<p>@dr2chase已经投入了大量的精力在循环中建立协作抢占点的原型，这是解决这个问题的一种方法。然而，即使是复杂的方法也会导致在紧密循环（我理解就是死循环或者短时间无法跳出的循环）中无法接受的减速（这种情况，减速通常是不可接受的）。</p>

<p>我建议go的实现方式切换到非协作抢占式，这将允许goroutine在本质上可以在任何时刻被抢占，而不需要显示的抢占检查。这种方式可以解决延迟抢占的问题，并且可以实现运行时零开销。</p>

<p>非协作抢占式是一个具有一整套实现技术的概念。本文描述并推动了向非协作抢占式的转换，并且讨论了go中所有的实现非协作式抢占的共同关注点。具体的实现方式详见文本附属的次级提案。</p>

<h2 id=""> </h2>

<h4 id="">背景</h4>

<p>在go1.10之前（</p>]]></description><link>http://blog.hacking.pub/2020/09/01/golang-fei-xie-zuo-shi-qiang-zhan/</link><guid isPermaLink="false">08b089bb-c727-4f87-a246-3edc933e3143</guid><category><![CDATA[go]]></category><category><![CDATA[go语言]]></category><category><![CDATA[go源码]]></category><category><![CDATA[非协作式抢占]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Tue, 01 Sep 2020 15:14:58 GMT</pubDate><content:encoded><![CDATA[<p>最近因为dlv里面有个bug，关于go1.14版本之后非协作式抢占扩大了调试状态下的一些协程问题。</p>

<p>于是就找了原始的提议看了看，恩，果然大部分都看不懂！！！</p>

<p>利用google/baidu式的翻译，手敲了一遍，并且存疑的地方都加了一点东西，算是给自己一点做个笔录吧，后面有理解更深的地方会更新一下自己的理解。</p>

<p>翻译笔录英文版本如<a href="https://link.zhihu.com/?target=https%3A//github.com/golang/proposal/blob/5b63da9579c3b19294be614dcad33e20a9a4ad22/design/24543-non-cooperative-preemption.md">链接</a>  </p>

<h4 id="">摘要</h4>

<p>go 当前在编译器插入的函数序章中插入协作抢占点。在大多数情况下，这足以让golang开发人员忽略抢占细节，专注于编码清晰的并行代码，但是这种方式有着非常尖锐的问题，以至于我们一次又一次的看到它降低了开发人员的体验。当它出现错误时候，就会呈现出惊人的错误现象，导致未知的系统层面的延迟问题，甚至有时候会完全冻结（就是卡住不动的意思）。因为这个是存在于golang语言语义之外的语言实现问题，这些错误是非常令人吃惊的，并且非常难以调试。</p>

<p>@dr2chase已经投入了大量的精力在循环中建立协作抢占点的原型，这是解决这个问题的一种方法。然而，即使是复杂的方法也会导致在紧密循环（我理解就是死循环或者短时间无法跳出的循环）中无法接受的减速（这种情况，减速通常是不可接受的）。</p>

<p>我建议go的实现方式切换到非协作抢占式，这将允许goroutine在本质上可以在任何时刻被抢占，而不需要显示的抢占检查。这种方式可以解决延迟抢占的问题，并且可以实现运行时零开销。</p>

<p>非协作抢占式是一个具有一整套实现技术的概念。本文描述并推动了向非协作抢占式的转换，并且讨论了go中所有的实现非协作式抢占的共同关注点。具体的实现方式详见文本附属的次级提案。</p>

<h2 id=""> </h2>

<h4 id="">背景</h4>

<p>在go1.10之前（包括go1.10），go只在函数调用使用了安全点的协作抢占（即使这样，如果函数太小或者内联也不会有）。这意味着go只能在特定点进行，当前并发执行的goroutines切换。这样做的主要优点是编译器可以在这些安全点确保有用的不变量（这个不变量是指？？）。特别是，编译器确保所有安全点的所有本地垃圾收集根都是已知的，这对于精确的垃圾回收是至关重要的。它还可以确保没有寄存器在安全点上活动，这意味着go在运行时切换goroutines而不必保存和恢复一个大的寄存器集（其实这个意思就是，golang的协程切换就是一些普通的函数调用，大量的寄存器的保存恢复不需要手动去控制，它只依赖普通函数调用时候的寄存器变化，其中参数和返回值都是通过栈传递的。实际上 golang在协程切换的时候，只有 1. 与栈相关的<code>SP</code>和<code>BP</code>寄存器 2. <code>PC</code>寄存器 3. 用于保存函数闭包的上下文信息，dx寄存器 ）。</p>

<p>然而，在稀少安全点的地方，会导致大量的问题：</p>

<ol>
<li>在生产代码中最常见的是，会延迟STW操作，例如启动和结束GC循环。这会增加STW延迟，并且在较大的核心计数器会显著影响吞吐量（例如，如果大量线程在运行时等待一个“落伍者”长时间被停住）（这个例子没有完全看懂，这里面的“落伍者”是啥意思？？）  </li>
<li>这会延迟调度，阻止竞争goroutine及时执行。  </li>
<li>这会延迟堆栈扫描，在运行时等待抢占点消耗CPU，并最终延迟GC终止，从而导致有效的STW，其中系统耗尽堆，无法分配goroutine。(没有完全懂？？)  </li>
<li>在一些极端的例子中，会导致程序停止，例如当在 atomic load 上自旋的goroutine耗尽了负责设置该原子性的gorotinue时。这通常说明代码是错误的或者有缺陷的，但令人惊讶的是，这显然浪费了开发人员大量的调试时间。<a href="https://golang.org/issue/543">#543</a>, <a href="https://golang.org/issue/12553">#12553</a>, <a href="https://golang.org/issue/13546">#13546</a>, <a href="https://golang.org/issue/14561">#14561</a>, <a href="https://golang.org/issue/15442">#15442</a>, <a href="https://golang.org/issue/17174">#17174</a>, <a href="https://golang.org/issue/20793">#20793</a>, <a href="https://golang.org/issue/21053">#21053</a>（我理解这块讲的是，生产代码会有一些内联的小函数，例如 <a href="https://github.com/golang/go/issues/12553">#12553</a>中for{test()}，虽然有函数调用，但是被内联了，所以gc在任何时间点里stop它）</li>
</ol>

<p>这些问题阻碍了开发人员的生产力和生产效率，并使Go的用户接触到他们本来不必关心的实现细节。</p>

<h4 id="">协作循环抢占</h4>

<p>@dr2chase 花费大量的精力去尝试使用协作循环抢占来解决问题。这个是运行时采用协作抢占的标准做法，其中编译器在flow graph的 back-edges插入抢占检查和安全点。这个极大程度上提高了抢占的质量，因为代码永远不会在没有 back-edges的情况下执行任意非常长的时间。（这个我理解，是在for循环的生成代码中也加入了协作抢占，其中对应代码在<a href="https://github.com/golang/go/commit/7f1ff65c3947b916cc4d0827fd8c1307d7efd7bf">#7efd7bf</a>）。</p>

<p>我们最新的循环抢占方法，我们称之为基于故障的抢占（fault-based preemption）,在x86 和 UNIX 平台（<a href="https://go-review.googlesource.com/c/go/+/43050/">CL 43050</a>）上的循环添加了一条指令，没有分支，也没有寄存器压力。尽管如此，geomean对一套大型基准测试的减速率为7.8%，少数异常值明显更差。即使与go1.9相比，由于一些改进，go1.9的减速只有1%，但是大多数基准测试都会出现一些减速，并且仍然存在显著的异常值。</p>

<p>基于故障的抢占（fault-based preemption）还有几个实现缺点。它不能针对特点的线程或者goroutine，所以对于堆栈扫描、参差不齐的屏障（这个是指写屏蔽吗？？）或常规的调度程序抢占来说，它是不匹配的。它也是“粘滞”的（就是置后的意思），因为在我们恢复所有循环之前，无法恢复任何循环，因此如果安全点发生在不安全的状态下（例如当运行时锁被持有时），安全点就不能简单地恢复。在非 x86 和 非 UNIX平台上，他需要更多的指令（和更多的开销）。最后，他会干扰调试器，调试器会假设错误的内存引用是停止程序的一个很好理性（就是说程序可能会停止程序）。由于一个<a href="https://bugs.llvm.org/show_bug.cgi?id=22868">内核错误</a>，目前还不清楚它是否可以在OSX上的若干调试器下运行。</p>

<h4 id="">非协作抢占</h4>

<p>非协作抢占发生在并行执行上下文之间切换，而无需显式的抢占检查或来自这些上下文的辅助。所有现代的桌面和服务器系统都使用这种方法在线程之间进行切换。如果没有这一点，一个行为不良（poorly-behaved）的应用程序可能会楔入整个系统，就像一个行为不良的goroutine当前如何楔入Go应用程序一样。这也是一个方便的抽象，隐藏了操作系统对有限数量的CPU进行时间复用（time-multiplexing）的事实，它让我们可以像有无限多个CPU一样进行编程。</p>

<p>操作系统调度器使用硬件中断支持将正在运行的线程切换到操作系统调度器中，这样可以保存该线程的状态，例如它的CPU寄存器，以便以后可以继续运行。在Go中，我们将使用操作系统支持来做同样的事情。在类UNIX的操作系统上，这可以使用信号来完成。</p>

<p>然而，由于GC的存在，Go 有着操作系统没有的要求：Go必须无论goroutine停在哪个位置，都能堆栈上找到活动指针（live pointers）。go中非协作抢占的大多数复杂性都源于此要求。</p>

<h4 id="">提议</h4>

<p>我提议go通过发送 POSIX 信号（或者等效的OS机制）来停止正在运行的goroutine并捕获其CPU状态，从而实现非协作的goroutine抢占。如果goroutine在必须是GC原子（GC atomic）处中断，如“处理不安全点”（<a href="https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md#handling-unsafe-points">Handing unsafe-points</a>）部分中所述的情况，那么运行时可以先简单地恢复goroutine并稍后再试。</p>

<p>实施Go的非协作抢占的关键困难点是在抢占的goroutine堆栈中查找活动指针（live pointer）。有许多方法可以做到这一点，这些在子提议（sub-proposals）中有详细介绍：</p>

<ul>
<li>“安全点无处不在”(<a href="https://github.com/golang/proposal/blob/master/design/24543/safe-points-everywhere.md">safe-points everywhere proposal</a>) 的提议描述了一种实现，其中编译器几乎为每条指令都记录堆栈并注册映射。这允许运行时在任何地方暂停gorutine并找到GC根。</li>
<li>“保守的内帧（inner-frame）扫描 ”（<a href="https://github.com/golang/proposal/blob/master/design/24543/conservative-inner-frame.md">conservative inner-frame scanning proposal</a> ）提议了一种使用保守的GC技术在抢占的goroutine的最内层堆栈帧中查找指针的实现。无需任何额外的安全点元数据即可完成此操作。</li>
</ul>

<h4 id="">处理非安全点</h4>

<p>go中任何非协作式抢占的方式都必须处理与GC相关的原子性的代码序列（这个话怎么理解？？？），相对于GC安全点相比，我们称这些为“不安全点”。一些已知的情况是：</p>

<ol>
<li>涉及unsafe.Pointer的表达式可以临时将其指向对象的唯一指针表示为uintptr。因此，当从unsafe.Pointer派生的uintptr处于活动状态时，必须将其认为是非安全点。同样的，我们必须将reflect.Value.Pointer，reflect.Value.UnsafeAddr和reflect.Value.InterfaceData识别为unsafe.Pointer到unintptr的转换。或者，如果编译器可以可靠地检测到此类uintptrs，则可以将其标记为指针，但是这样会有存在中间值可能无法标示合法指针的危险。  </li>
<li>在写屏障中，在启动写屏障的检查和直接写入之间的位置一定不是安全点。例如，假设goroutine正在将指向B的指针写入对象A。如果发生了检查，则GC启动并扫描A，然后goroutine将B写入A并将所有对B的引用从其堆栈中删除，GC可能会失败标记B。（这个意思就是有A.B，当扫描A的时候，A还没有B的引用，然后刚好其他协程把A.B这个引用生成了，而此时扫描结束了）  </li>
<li>在某些地方，编译器会生成可能超出分配末尾的临时指针，例如在切片和数组上的范围循环中。当这些指针处于活跃状态时，要么必须避免这些情况，要么就必须禁止安全点。</li>
</ol>

<p>所有这些情况都必须已经避免了重大的重新排序，以避免在调用时候分裂（我理解这个是指栈分裂）。在内部，这是通过“mem”伪值实现的，该伪值必须顺序地遍历所有操作内存的SSA值。Mem也通过不允许重新排序进行遍历，甚至它们并不接触内存。例如，unsafe.Pointer和uintptr之间的转换是通过特殊的“Convert”操作完成的，该操作仅使用内存来限制重新排序。（所以排序到底是个啥？）</p>

<p>这些问题有几种可能的解决方案，其中一些可以组合使用：</p>

<ol>
<li>我们可以标记不应包含抢占点的基本块。对于unsafe.Pointer的转换，我们将选择退出包含该转换的基本块。对于遵循unsafe.Pointer规则的代码，这应该足够了，但是它可能会破坏不正确的代码，如今一旦发生，就会变得难以调试（怎么翻译？？）。对于写屏障，这也是足够的。对于循环，这过于广泛，将需要拆分一些基本块。  </li>
<li>对于<code>unsafe.Pointer</code>转换，我们可以简单地选择退出所有从unsafe.Pointer转换为uintptr的函数。这将很容易实现，甚至可以使损坏的不安全代码像现在一样工作，但是可能会产生广泛的影响，尤其在存在内联的情况下。  </li>
<li>1和2的简单组合是选择退出从unsafe.Pointer到uintptr转换到函数调用（今天是安全点）的所有基本块。  </li>
<li>对于范围循环，编译器可以对他们进行不同的编译，以使她从不构造越界指针（请参见下文）。  </li>
<li>一个更精确，更通用的方法（由于@cherrymui）将是创建新的SSA操作来“taint”和“untaint”内存。“taint”操作将需要一个内存并返回一个新的“taint”的内存。该“taint”将流向本身具有“tainted”值的任何值。“untaint"操作需要一个值和一个内存，并返回一个“untaint”的值和未污染的mem。在活动性分析期间，无论“taint”是否存活，安全点都是被禁用的。这可能最精确的解决方案，并且即使不正确地使用不安全的工作也可能正常运行，但是需要复杂的实现。</li>
</ol>

<p>更广泛地说，值得考虑让编译器检查不安全，使用指针的代码并主动拒绝不遵循“允许模式”的代码。这可以作为一个简单的类型系统来实现，该系统可以将类似与指针的uintptr与数字unintr区分开。但这超出了本提议的范围。</p>

<h4 id="">范围循环</h4>

<p>对于Go1.10，范围循环被编译如下：</p>

<pre><code>for i, x := range s { b }  
  ⇓
for i, _n, _p := 0, len(s), &amp;s[0]; i &lt; _n; i, _p = i+1, _p + unsafe.Sizeof(s[0]) { b }  
  ⇓
i, _n, _p := 0, len(s), &amp;s[0]  
goto cond  
body:  
{ b }
i, _p = i+1, _p + unsafe.Sizeof(s[0])  
cond:  
if i &lt; _n { goto body } else { goto end }  
end:  
</code></pre>

<p>降低（lowering，我理解是递归下降）的问题是<code>_p</code>可能会在循环终止之前临时指向分配结束。当前这是安全的，因为 <code>_p</code>的值处于活动状态时永远不会有安全点（握草，啥个意思？？？）。</p>

<p>这种“降低做法”（lowering，我理解是递归下降）要求编译器将增量和条件块标记为不安全点。但是如果body过小，可能会导致安全点很少。它还需要为增量创建一个单独的块，当前通常将其附加到主体的末尾。分隔这些块会抑制重新排序的机会。（所以跟排序到底关系？？？）</p>

<p>为了准备非协作抢占，Go1.11开始按照如下方式编译范围循环，以避免产生过头（past-the-end）的指针：</p>

<pre><code>i, _n, _p := 0, len(s), &amp;s[0]  
if i &gt;= _n { goto end } else { goto body }  
top:  
_p += unsafe.Sizeof(s[0])  
body:  
{ b }
i++  
if i &gt;= _n { goto end } else { goto top }  
end:  
</code></pre>

<p>这允许在循环中的任何地方使用安全点。与原始循环编译相比，它生成的代码略多，但是执行条件分支指针(n + 1)的数量相同，并且SSA基本块（basic blocks）相同。</p>

<p>这种“降低做法”（lowering，我理解是递归下降）确实使“边界检查消除”（bounds-check elimination）变得复杂。在Go1.10中，“边界检查消除”（bounds-check elimination） 知道 body 中的 <code>i &lt; _n</code>，因为这个body块受cond块控制支配。但是在新的“下降过程”（我理解是递归下降），推到这个事实是需要检测 body 的两条 路径上的 <code>i &lt;_n</code>，因此在 body中为真。</p>

<h4 id="">运行时安全点</h4>

<p>除了生成的代码外，通常不会将运行时编写为可任意抢占，而且很多地方都不能被抢占。因此，我们可能会在运行时默认禁用安全点。但是调用时除外（当前发生的位置）。</p>

<p>尽管对于大多数运行时而言，这机会没有什么缺点，甚至运行时的某些部分可能会从非协作抢占中收益，例如像 memmove这样的内存功能。非协作抢占对抢占不减慢的普通case及其友好（原文大概是这个意思，直接翻译完全不会），因为我们只需要标记它们的寄存器映射（对于memmove之类的函数，由于所有指针已经受参数保护了，因此通常为空）。</p>

<p>随着时间的流逝，我们可能对于运行时有着更多的选择。</p>

<h4 id="">不安全的标准库代码</h4>

<p>windows的系统调用库包含大量的不遵循<code>unsafe.Pointer</code>规则的<code>unsafe.Pointer</code>转换。它广泛地对于安全点的行为、存活性以及何时发生堆栈移动做出了不稳定的假设。这个部分可能需要进行彻底的审核，或者像运行时一样需要选择性退出。</p>

<p>也许更麻烦的是某些windows syscall程序包类型具有uintptr字段，这些字段实际上是指针，因此迫使调用者执行不安全的指针转换。例如，参考 <a href="https://golang.org/issue/21376">#21376</a>。(这个例子非常棒，个人建议可以直接看@<strong>zhangyoufu</strong> 指出来的问题)</p>

<h4 id="">确保非安全点的进度</h4>

<p>我们建议仅在goroutine在不安全的点被中断时放弃并稍后重试。这样做的危险之一是，在紧缩循环（tight loops）中。然而，大量情况下，此方法还有更复杂的替代方法。</p>

<p>对于运行时或者没有安全点（例如汇编）的函数的中断，信号处理程序可以展开堆栈，会插入一个蹦床（trampoline），该蹦床处于下一次返回具有安全点元数据的函数位置（好绕！！！）。接着，运行时可以让goroutine继续运行，而蹦床（trampoline）将尽快将其暂停。</p>

<p>对于写屏障和<code>unsafe.Pointer</code>指针序列（这个“序列”我理解就是汇编指令的意思），编译器可以在序列末尾插入一个廉价的显示抢占检查。例如，运行时可以修改一些在序列末尾检查的寄存器，并让线程继续执行。在写屏障序列中，这甚至可能是写屏障标志被加载到的寄存器，并且编译器可以在序列末尾插入一个简单的寄存器测试和条件分支。为了一进一步缩小序列，运行时可以将stop函数的地址放入该寄存器中，因此stop序列只是一个寄存器调用和一个跳转。</p>

<p>此检查的替代方法包括正向和反向模拟。正向模拟非常棘手，因为编译器仅生成运行时知道如何模拟的操作，这个过程要异常小心。如果编译器始终可以生成可重新启动的序列（简单地往回移动 PC 到写屏障 flag 检查处），那么逆向模拟就很容易完成；但是如果序列中有大量写操作或者更复杂的写入（例如DUFFCOPY），那么反向模拟很快就会变得复杂。</p>

<h4 id="">其他注意事项</h4>

<p>所有提议的非协作抢占方法都包括通过向其发出OS信号来停止正在运行的goroutine。本节讨论了这种情况的一般后果。</p>

<p><strong>windows支持</strong>。与基于故障（fault-based）的循环抢占不同，在windows中很容易支持信号抢占，因为它提供了SuspendThread和GetThreadContext，这使得获取线程的寄存器集变得很简单。</p>

<p><strong>选择一个信号</strong>。我们必须选择一个不太可能干扰现有信号使用或者调试器的信号。没有完美的选择，但是有一些启发式方法。</p>

<ol>
<li>默认情况下，它应当是能被调试器传递的信号。在linux上，是SIGALRM，SIGURG，SIGCHILD，SIGIO，SIGVTALRM，SIGPROF和SIGWINCH，已经一些glibc内部信号。  </li>
<li>libc不应在混合GO/C二进制文件内部使用它，因为libc可能会认为它自己是唯一可以处理这些信号的工具（only thing 应该翻译成啥，比较好，“工具”好像也不太好）。例如 SIGCANCEL或SIGSETID。  </li>
<li>应该选择一个可以虚假（spuriously）发生而且不会产生后果的信号。例如，SIGALRM是一个很错误的选择，因为这个信号处理程序无法判断它是否是由真实进程警报（alarm）引起的（可以说这意味着信号已经损坏了，但是我离题了）。SIGUSR1和SIGUSR2也很糟糕，因为应用程序经常以有意义的方式使用它们。  </li>
<li>我们需要处理没有实时信号的平台（例如macOS），所以这些都是要被淘汰的选项。</li>
</ol>

<p>我们使用 SIGURG 因为它满足所有这些条件，极大概率上不会因为其“真实”（real）含义而被其他应用程序使用（同样还有一个原因是因为外带数据基本上未被使用，并且因为SIGURG不会报告哪个套接字具有条件，这些使其变得毫无用处），即便这样，该应用程序也必须准备接受虚假的SIGURG。SIGIO也不是一个不好，但可能更可以用于真实（real）。</p>

<p><strong>调度程序抢占</strong>。这种机制非常适合临时抢占，在抢占之后，相同的goroutine将继续执行，因为我们不需要保存完整的寄存器状态，并且可以依赖现有的信号返回路径来恢复完整的寄存器状态。这适用于所有与GC相关的抢占，但不适用于调度程序执行的永久抢占。但是，我们仍然可以在此机制上构建。例如，由于大多时候goroutine具有自我抢占功能，我们只需要在不常见的情况下保存完整的信号状态，因此<code>g</code>可以包含一个指向完整保存状态的指针，该指针仅在抢占之后使用。恢复完整的信号状态可以通过编写跟架构相关（architecture-dependent）的代码来恢复完整的寄存器集（增强runtime.gogo功能），或者通过自信令，在所需要的上下文中进行交换，然后操作系统还原完整的寄存器集。</p>

<p><strong>定位与恢复</strong>。与基于故障的循环抢占相比，信号抢占可以针对特定线程，并且可以立即恢复。以线程为目标与以goroutine为目标的合作式抢占有所不同。但是，在许多情况下，实际上会更好，因为针对goroutine进行抢占是不明智的，因此需要重试循环，这可能会大大增加STW时间。利用此优势进行堆栈扫描讲需要对我们跟踪GC跟的方式进行一些重组，但是结果应消除我们当前使用的阻塞重试循环。</p>

<p><strong>非指针指针</strong>。这有可能暴露<code>unsafe.Pointer</code>对临时存储非指针的不正确使用。此类使用明显违反了<code>unsafe.Pointer</code>规则，但可能会发生（特别是在使用cgo的代码中）。</p>

<h4 id="">备选方案</h4>

<h6 id="">单步执行</h6>

<p>相比较努力让程序可以停在在任意指令，编译器可以在只在后端（back-edges）就为安全点发出元数据，而运行时可以使用硬件单补支持线程推到一个安全点（或者编译器提供分支以达到安全点的位置，例如当前循环抢占方法）。这行得通（有点令人惊讶），但是这彻底迷惑了调试器，因为调试器和操作系统都假设调试器拥有单步调试，而不是进程本身。这也就要求编译器为这些安全点提供寄存器刷新存根，这增加了代码大小（并因此增加了指令高速缓存压力）以及堆栈大小，这与协作循环抢占非常类似。但是与协作循环抢占不同，这种方法不会影响主线代码的大小或者性能。</p>

<h6 id="">跳转重写</h6>

<p>通过在中断点之后重写下一条安全点跳转指令以跳转到抢占路径并且像往常一样执行恢复执行，可以解决单补执行的问题。为了简化次过程，编译器可以留出足够的空间（通过填充NOP)，因此仅需要修改跳转目标。</p>

<p>这种方式具有可修改代码的缺点。这是一种隐患，他会破坏 .text 页面共享，并且在IOS上是完全不被允许的。他也不能以单个goroutine为目标（因为另一个goroutine可能正在执行相同的代码），并且可能与其他内核上的并发执行发生奇怪的交互。</p>

<h5 id="">离线执行</h5>

<p>同样，但是不需要修改现有.text文本的另一种选择是离线执行。在这种方法中，信号处理程序将指令流从中断点转移到下一个安全点跳转到临时缓冲区中，对其进行修补以最终跳到运行时，然后按此重新定位的顺序恢复执行。</p>

<p>这可以通过单补执行和跳转重新解决大多数问题，但是实现起来非常复杂，并且每一个平台都需要大量的实现工作。IOS上也不允许使用。</p>

<p>这种方法有先例。例如，当linux uprobes注入INT 3 时，它将重写的指令重新定位到“执行离线”区域中，从而避免了从INT3指令恢复时常见的问题。考虑到x86指令编码的复杂性，简化了实现，但是依然非常复杂。</p>]]></content:encoded></item><item><title><![CDATA[丢弃包袱]]></title><description><![CDATA[<p>可能一时也没有想好起什么题目，脑袋里的思绪大概是往14年那飘，当时关于纯粹体系构想在心里初步形成。某天晚上手写了很长的一篇文章，一直作了保留。那时候状态就是疯狂否定自己以前的思维模式，无论是学习还是生活都按照既有思路（根理论）出发，得出一个基本行为模式。如果不说的这么抽象(或者玄乎)，可能跟平时常说的"人设"这个词最像。所有的行为都需要严格按照"既定的人设"进行，不可违反，否则就类似计算机中了病毒（或者严重bug），无法正常生存下去。</p>

<p>甚至当时有种想法现在看来很傻，心里按照既定公式来计算每个人的"纯粹度"，以此在"纯粹"这个维度来将人分强弱等级。公式有许多的输入参数，而且公式是需要不停地更正的，因为变量因素这个东西实在是很难控制，只能通过局部最优解去揣测全局最优。这些我自己总结的思考方式对我的影响实在深远，不利的一面也异常明显。我会花大量精力去列举出大量繁琐的公式，长时间进行调参以规整自己的行为模式。</p>

<p>可是最近几个月，一连串的事情让我措手不及，我甚至连调整参数的空余都没有，就已经被击败了。我开始反思，想参考别人的思维方式。可是结论有点悲催，我需要重新整理整个体系，甚至是推翻它，这是一种信念崩塌的感觉。心智这件事可能比自我想象描述中的还要重要得多，甚至想把它并列上升到"智商"、"情商"</p>]]></description><link>http://blog.hacking.pub/2020/07/11/diu-qi-bao-fu/</link><guid isPermaLink="false">c62c9b0d-d946-47c7-9e5c-70381b1657ef</guid><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Sat, 11 Jul 2020 09:55:29 GMT</pubDate><media:content url="http://img.hacking.pub/%E4%B8%A2%E5%BC%83%E5%8C%85%E8%A2%B1/339348501.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://img.hacking.pub/%E4%B8%A2%E5%BC%83%E5%8C%85%E8%A2%B1/339348501.jpg" alt="丢弃包袱"><p>可能一时也没有想好起什么题目，脑袋里的思绪大概是往14年那飘，当时关于纯粹体系构想在心里初步形成。某天晚上手写了很长的一篇文章，一直作了保留。那时候状态就是疯狂否定自己以前的思维模式，无论是学习还是生活都按照既有思路（根理论）出发，得出一个基本行为模式。如果不说的这么抽象(或者玄乎)，可能跟平时常说的"人设"这个词最像。所有的行为都需要严格按照"既定的人设"进行，不可违反，否则就类似计算机中了病毒（或者严重bug），无法正常生存下去。</p>

<p>甚至当时有种想法现在看来很傻，心里按照既定公式来计算每个人的"纯粹度"，以此在"纯粹"这个维度来将人分强弱等级。公式有许多的输入参数，而且公式是需要不停地更正的，因为变量因素这个东西实在是很难控制，只能通过局部最优解去揣测全局最优。这些我自己总结的思考方式对我的影响实在深远，不利的一面也异常明显。我会花大量精力去列举出大量繁琐的公式，长时间进行调参以规整自己的行为模式。</p>

<p>可是最近几个月，一连串的事情让我措手不及，我甚至连调整参数的空余都没有，就已经被击败了。我开始反思，想参考别人的思维方式。可是结论有点悲催，我需要重新整理整个体系，甚至是推翻它，这是一种信念崩塌的感觉。心智这件事可能比自我想象描述中的还要重要得多，甚至想把它并列上升到"智商"、"情商"一起。因为即便你伪装得再好，对于自己是无法做到完美欺骗的，如果做到了完美欺骗，那也就不存在欺骗了。有意思的是，人与人之间这点上的差别非常明显，平时相处的过程中最可能能影响对于某个人的印象（我瞎猜的）。</p>

<p>我想从今日起完全抛弃掉"纯粹理论"，这个东西已经成为自我发展的包袱了。顺带也可能需要扔掉思想包袱，反正戒啥都是一个字，忍，你不去做第一次，就永远不会有第二次。尝试不要去按照先前的思维方式去规划自己行为。</p>]]></content:encoded></item><item><title><![CDATA[tcp 拥塞控制]]></title><description><![CDATA[<p><img src="http://img.hacking.pub/tcp%20%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6/tcp.svg" alt="tcp.svg"></p>]]></description><link>http://blog.hacking.pub/2020/06/28/tcp-yong-sai-kong-zhi/</link><guid isPermaLink="false">a9f16c94-445c-4fb0-9899-4cadf1a1282b</guid><category><![CDATA[tcp]]></category><category><![CDATA[拥塞控制]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Sun, 28 Jun 2020 06:57:58 GMT</pubDate><content:encoded><![CDATA[<p><img src="http://img.hacking.pub/tcp%20%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6/tcp.svg" alt="tcp.svg"></p>]]></content:encoded></item><item><title><![CDATA[不战而屈人之兵]]></title><description><![CDATA[<h4 id="">背景</h4>

<p>最近有很多事情变化太快，对于局势的不解，反而让我有更多的时间反思了.  </p>

<h4 id="">前提</h4>

<p>首先个人不是神，不可能想全所有的细节进行推演. <br>
局势上宏观任何的变动对于微观上都可能是致命的，甚至无法预料. <br>
而恰恰宏观势的观察异常困难，尤其是面临各种信息差的时候.    </p>

<h4 id="">核心竞争力</h4>

<p>个人反思一下，对于任何一家企业招人，它愿意招你的根本条件是什么？（这里排除极个别异常的情况） <br>
是你能给这家公司带来利益，那么利益是经由你怎么产生的，这个就是你在这家公司的价值 <br>
对于个人（普通员工）直接带来利益的手段，就是解决问题 <br>
如何花费最小的代价解决不同层面的问题决定了你在这家公司的职级  </p>

<p>这里扔去网上那些对于<code>核心竞争力</code>这个词的定义，思考一下同样情况下竞争力 <br>
即基本技能水平达到与你同样门槛的其他人，你是如何表达出你的优势，从而让上层人员优先选择你. <br>
<code>见胜不过众人之所知,非善之善者也</code>.  </p>

<h4 id="">形</h4>

<p>始终要明确一点，你的影响力一定是由<code>人</code>进行传播的. <br>
换句话说，就是你需要明白时刻与你影响力的载体进行打交道 <br>
如何花费最小代价（尤其是一些行为后遗症）达到传播的效果，快准狠 <br>
另外对于形的分类而言，需要考虑的是 你的上级，你的上上级，你的同级，你的下级，</p>]]></description><link>http://blog.hacking.pub/2020/04/12/bu-zhan-er-qu-ren-zhi-bing/</link><guid isPermaLink="false">5f9035d4-60fe-4dcb-b0ab-27f9a467ed18</guid><category><![CDATA[思考]]></category><category><![CDATA[孙子兵法]]></category><category><![CDATA[不战而屈人之兵]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Sun, 12 Apr 2020 06:56:51 GMT</pubDate><content:encoded><![CDATA[<h4 id="">背景</h4>

<p>最近有很多事情变化太快，对于局势的不解，反而让我有更多的时间反思了.  </p>

<h4 id="">前提</h4>

<p>首先个人不是神，不可能想全所有的细节进行推演. <br>
局势上宏观任何的变动对于微观上都可能是致命的，甚至无法预料. <br>
而恰恰宏观势的观察异常困难，尤其是面临各种信息差的时候.    </p>

<h4 id="">核心竞争力</h4>

<p>个人反思一下，对于任何一家企业招人，它愿意招你的根本条件是什么？（这里排除极个别异常的情况） <br>
是你能给这家公司带来利益，那么利益是经由你怎么产生的，这个就是你在这家公司的价值 <br>
对于个人（普通员工）直接带来利益的手段，就是解决问题 <br>
如何花费最小的代价解决不同层面的问题决定了你在这家公司的职级  </p>

<p>这里扔去网上那些对于<code>核心竞争力</code>这个词的定义，思考一下同样情况下竞争力 <br>
即基本技能水平达到与你同样门槛的其他人，你是如何表达出你的优势，从而让上层人员优先选择你. <br>
<code>见胜不过众人之所知,非善之善者也</code>.  </p>

<h4 id="">形</h4>

<p>始终要明确一点，你的影响力一定是由<code>人</code>进行传播的. <br>
换句话说，就是你需要明白时刻与你影响力的载体进行打交道 <br>
如何花费最小代价（尤其是一些行为后遗症）达到传播的效果，快准狠 <br>
另外对于形的分类而言，需要考虑的是 你的上级，你的上上级，你的同级，你的下级，你的打交道其他团队人员，对于不同的类型，需要的手段也是不尽相同的   </p>

<blockquote>
  <p>虽然我这里面没有讲到<code>势</code>，但是<code>形</code>与<code>势</code>是相辅相成的，也就是说你的影响力同样会反作用于你的传播<code>形</code>，时刻要注意这两者之间的平衡  </p>
</blockquote>

<h4 id="">圆回来</h4>

<p>我想了很久，对于"不战而屈人之兵"还有"知彼知己"（我认为时刻保持了解对手比了解自己更重要一些） <br>
都是跟<code>形</code>密切相关，如果把每一次的争论看成利益之争，如果保证利益的最大化才是最重要的(并且可持续性)  </p>

<pre><code>上兵伐谋  
其次伐交  
其次伐兵  
其下攻城
</code></pre>

<p><br>
接触大部分人在考虑问题时（尤其是情绪化比较严重的人），非常容易掉落在<code>其次伐兵</code>的境界，虽然能赢到胜利，但往往也是损耗了太多自己的<code>信</code>与<code>仁</code>，有点像<code>杀敌三千，自损八百</code> </p>

<pre><code>孙子曰: 将者， 智 信 仁 勇 严
</code></pre>

<p>而<code>伐交</code>的境界，容易让人感到你很厉害，都是处理过于狠辣(或者雷厉风行？)？,或者是中性的态度（这种标签不利于影响力的传播）？  </p>

<p>所以其实每一次的行动，我们都应该好好思考一下，能不能做到<code>上兵伐谋</code>？<code>不战而屈人之兵</code>，甚至反过来褒奖你   </p>

<h4 id="">总结</h4>

<p>我觉得我今后的每一次沟通，都应该认真去思考一下<code>不战而屈人之兵</code>，时刻提醒自己能不能做到<code>上兵伐谋</code>       </p>]]></content:encoded></item><item><title><![CDATA[调试器与GO]]></title><description><![CDATA[<h2 id="">调试器</h2>

<p>如果把调试器看成一个产品的时候，首先他应该具体的功能有哪些？ <br>
1. 断点 <br>
2. 代码与当前断点映射（例如查看源码） <br>
3. 断点过后继续运行（继续执行，单步，单出） <br>
4. 打印变量  </p>

<p>附属的功能： <br>
1. 打印调用栈 <br>
2. 打印堆栈 <br>
3. 函数调用 <br>
4. 条件断点 <br>
5. 反汇编 <br>
...  </p>

<h2 id="">调试状态</h2>

<p><code>linux</code>会有trace进程的状态，对被<code>trace</code>的<code>进程</code>可以进行控制  </p>

<h2 id="">基本功能</h2>

<h4 id="">断点</h4>

<p>断点通常分为两种，硬件断点和软件断点</p>

<ol>
<li><p>先说硬件断点，这个我们（普通的后台开发）平时不太常用，对于x86系统来说， 是依赖寄存器DR0~DR7和2个MSR实现的 <br>
说直白一点，就是可以把内存地址放到寄存器内，当cpu进行 读、写、</p></li></ol>]]></description><link>http://blog.hacking.pub/2020/04/09/tiaoshiqiyugo/</link><guid isPermaLink="false">66d42df2-d93d-4235-a76d-10f1c3137715</guid><category><![CDATA[go]]></category><category><![CDATA[调试器]]></category><category><![CDATA[delve]]></category><category><![CDATA[源码剖析]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Thu, 09 Apr 2020 11:13:40 GMT</pubDate><content:encoded><![CDATA[<h2 id="">调试器</h2>

<p>如果把调试器看成一个产品的时候，首先他应该具体的功能有哪些？ <br>
1. 断点 <br>
2. 代码与当前断点映射（例如查看源码） <br>
3. 断点过后继续运行（继续执行，单步，单出） <br>
4. 打印变量  </p>

<p>附属的功能： <br>
1. 打印调用栈 <br>
2. 打印堆栈 <br>
3. 函数调用 <br>
4. 条件断点 <br>
5. 反汇编 <br>
...  </p>

<h2 id="">调试状态</h2>

<p><code>linux</code>会有trace进程的状态，对被<code>trace</code>的<code>进程</code>可以进行控制  </p>

<h2 id="">基本功能</h2>

<h4 id="">断点</h4>

<p>断点通常分为两种，硬件断点和软件断点</p>

<ol>
<li><p>先说硬件断点，这个我们（普通的后台开发）平时不太常用，对于x86系统来说， 是依赖寄存器DR0~DR7和2个MSR实现的 <br>
说直白一点，就是可以把内存地址放到寄存器内，当cpu进行 读、写、修改该地方内存时，就会触发trap   </p></li>
<li><p>而对于软件断点，依赖CPU的指令 <br>
对于x86架构CPU来说，<code>int 3</code>指令就是暂停的系统中断指令，<code>0XCC</code> （实际上还有一种软断点编码<a href="https://github.com/go-delve/delve/commit/e69d536e819bbbd3539d5748d61942330863cf11#diff-fb7b7a020e32bf8bf477c052ac2d2857e7e587478be6039aebc7135c658417b2R579">[]byte{0xcd, 0x03}</a>） <br>
当CPU指令执行 <code>0xCC</code>，<code>task</code>（CPU的工作基本单位）就会陷入暂停状态(trap或者中断)，等待新的命令      </p></li>
</ol>

<p><img src="http://img.hacking.pub/debugger/go-delve-debugger.gif" alt="go-delve-debugger" title="">   </p>

<p><img src="http://img.hacking.pub/debugger/debugger3.svg" alt="debugger3.svg" title="">  </p>

<h4 id="">代码和当前断点映射</h4>

<p>这个需要编译器和链接器支持，编译参数 <code>go build -gcflags="-N -l" -o main main.go</code> <br>
保证禁用编译器优化和内联优化   ctx
<code>objdump --dwarf main &gt; log.txt</code>  可以看到 行号 状态机    </p>

<pre><code> The Directory Table is empty. 

 The File Name Table (offset 0x279ad):
  Entry Dir Time    Size    Name   
  1 0   0   0   /home/chainhelen/main.go

 Line Number Statements:
  [0x000279ca]  Extended opcode 2: set Address to 0x48cf30
  [0x000279d5]  Set File Name to entry 1 in the File Name Table
  [0x000279d7]  Advance Line by 1 to 2 
  [0x000279d9]  Special opcode 9: advance Address by 0 to 0x48cf30 and Line by 5 to 7 
  [0x000279da]  Set prologue_end to true
  [0x000279db]  Special opcode 154: advance Address by 15 to 0x48cf3f and Line by 0 to 7 
  [0x000279dc]  Special opcode 145: advance Address by 14 to 0x48cf4d and Line by 1 to 8 
  [0x000279dd]  Set is_stmt to 0
  [0x000279de]  Special opcode 34: advance Address by 3 to 0x48cf50 and Line by 0 to 8 
  [0x000279df]  Set is_stmt to 1
  [0x000279e0]  Advance PC by 66 to 0x48cf92
  [0x000279e2]  Special opcode 244: advance Address by 24 to 0x48cfaa and Line by 0 to 8 
  [0x000279e3]  Special opcode 55: advance Address by 5 to 0x48cfaf and Line by 1 to 9 
  [0x000279e4]  Special opcode 102: advance Address by 10 to 0x48cfb9 and Line by -2 to 7
  [0x000279e5]  Extended opcode 1: End of Sequence
</code></pre>

<pre><code>objdump --dwarf ./main | grep -C2 "DW_TAG_compile_unit\|DW_AT_producer"  
</code></pre>

<p><a href="https://wiki.osdev.org/DWARF">dwarf 知识简介</a> <br>
<a href="https://www.zhihu.com/question/387920953/answer/1219734250">golang行号状态机的小bug</a> <br>
<a href="https://golang.org/pkg/debug/dwarf/">PrologueEnd 断点</a></p>

<h4 id="">断点过后继续运行（继续执行，单步，单出）</h4>

<p>主要有两个系统调用可以让进程从<code>trap</code>恢复出来，  <code>PTRACE_CONT</code>、<code>PTRACE_SINGLESTEP</code> <br>
而对于 <code>PTRACE_CONT</code> 是让进程恢复正常的trace状态并且一直运行下去 <br>
<code>PTRACE_SINGLESTEP</code> 是执行一条汇编语句  </p>

<p>两个问题 <br>
1. 如果断点断住了，那么breakpoint的位置是 <code>int 3</code>，<code>PTRACE_CONT</code>是不能正常运行的，举个例子  </p>

<pre><code>(dlv) disassemble
TEXT main.main(SB) /home/chainhelen/main.go  
    ...
    main.go:7   0x4aa3b8    488d6c2460      lea rbp, ptr [rsp+0x60]
=&gt;    main.go:8   0x4aa3bd*   0f57c0          xorps xmm0, xmm0
    main.go:8   0x4aa3c0    0f11442438      movups xmmword ptr [rsp+0x38], xmm0
    main.go:8   0x4aa3c5    488d442438      lea rax, ptr [rsp+0x38]
    ...
    main.go:7   0x4aa429    e8b2e9faff      call $runtime.morestack_noctxt
    main.go:1   0x4aa42e    e96dffffff      jmp $main.main
</code></pre>

<p>实际上的状态是<code>0XCC</code>  </p>

<pre><code>(dlv) disassemble
TEXT main.main(SB) /home/chainhelen/main.go  
    ...
    main.go:7   0x4aa3b8    488d6c2460      lea rbp, ptr [rsp+0x60]
=&gt;    main.go:8   0x4aa3bd*   cc57c0          xorps xmm0, xmm0
    main.go:8   0x4aa3c0    0f11442438      movups xmmword ptr [rsp+0x38], xmm0
    main.go:8   0x4aa3c5    488d442438      lea rax, ptr [rsp+0x38]
    ...
    main.go:7   0x4aa429    e8b2e9faff      call $runtime.morestack_noctxt
    main.go:1   0x4aa42e    e96dffffff      jmp $main.main
</code></pre>

<p><br>
如果你从这个地方直接运行  <code>PTRACE_CONT</code>，那么通常结果是出错（或者CPU跳过错误指令继续前向） </p>

<p>需要的是让指令继续前行（PC寄存器，IP寄存器，RIP寄存器，或者EIP寄存器） <br>
2. 正常运行了，如何实现source code的单步调试 <br>
而是源码级别的单步调试，那么需要对每一行的源码都下断点，还有注意返回帧(函数栈或者叫调用栈)  </p>

<h4 id="">打印变量</h4>

<p>对于打印变量，对于全局变量（包括global或者package导出的）主要是符号表 <br>
是否符号表就够了？不够！  </p>

<pre><code>main.c  
nm main

main.go  
nm main  
</code></pre>

<p>局部变量怎么办，跟package无关，也就是跟导出符号无关，也就是不在符号表内 <br>
并且对于编译器来说，所有的非static局部变量，最终都在栈上面（还有逃逸的部分在堆上），利用压栈弹栈方式 <br>
在编译链接过程中，局部变量都已经转化成栈底（或者栈顶）偏移量   </p>

<p>这个时候就需要dwarf记录各个变量的信息，通常非优化编译，都是可以保留的  </p>

<pre><code>// main.go
package main

import (  
    "fmt"
)

func main() {  
    hello := "world"
    fmt.Println(hello)
}

// go build
go build -gcflags="-l -N" -o main main.go 

// objdump
objdump --dwarf main &gt; log.txt  


274873  &lt;2&gt;&lt;76ed1&gt;: Abbrev Number: 10 (DW_TAG_variable)  
274874     &lt;76ed2&gt;   DW_AT_name        : hello  
274875     &lt;76ed8&gt;   DW_AT_decl_line   : 8  
274876     &lt;76ed9&gt;   DW_AT_type        : &lt;0x37482&gt;  
274877     &lt;76edd&gt;   DW_AT_location    : 3 byte block: 91 b8 7f    (DW_OP_fbreg: -72)  
</code></pre>

<p>这种做法是对的吗？？？哪种场景的变量不正确 <a href="https://zhuanlan.zhihu.com/p/280929169">Stack unwinding</a></p>

<h2 id="">附属功能</h2>

<h4 id="">打印调用栈</h4>

<p>主要指函数调用，通常我们有函数帧（栈），栈中包含了返回地址，通过返回地址来保证读取到函数栈   </p>

<p><img src="http://img.hacking.pub/go%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%20defer/test_defer3.svg" alt="test_defer3.svg" title="">  </p>

<p>(图片好像有点小错误，但是我忘记了哪个寄存器搞错了)</p>

<h4 id="">函数调用</h4>

<p>需要语言本身提供支持（原理上来讲不支持也可以注入的方式，但是异常复杂），因为涉及到内存（堆栈）和寄存器的变动   </p>

<pre><code>"debugCall"
"runtime.debugCall"
"runtime.debugCallV1"
</code></pre>

<p><a href="https://golang.org/src/runtime/debugcall.go">debugcall</a></p>

<h4 id="">条件断点</h4>

<p>条件断点是一件很慢的事情，普通的10000次循环，只check一个变量，延迟会达到10s <br>
<a href="https://github.com/go-delve/delve/issues/1549">go-delve/delve 1549</a>  </p>

<h4 id="">反汇编</h4>

<p>指令翻译  <a href="https://github.com/go-delve/delve/blob/v1.5.0/pkg/proc/x86_disasm.go#L51-L70">符号对应关系提前从二进制文件中解析出来</a></p>

<h2 id="go">go特殊的部分</h2>

<h4 id="">协程</h4>

<ol>
<li>cgo的协程是要做切换栈的，我们对于切换的协程是如何做到    </li>
</ol>

<p><img src="http://img.hacking.pub/debugger/debugger.svg" alt="debugger.svg" title="">    </p>

<p>asm_amd64.s 文件 <br>
<a href="https://github.com/go-delve/delve/issues/935">go-delve/delve 935 cgo better</a>   </p>

<p>2.切换用户无关，那么trace权限如何控制？ <br>
lockOSThread <a href="https://go-review.googlesource.com/c/go/+/155138/2/src/syscall/exec_linux.go#28">trace协程与线程绑定</a>   </p>

<p>3.系统线程和协程绑定关系保存在当前协程的tls中（见下文） <br>
所有线程，Linux 下在/proc中可以获取到task <br>
所有协程，依赖底下几个内存变量，<a href="https://github.com/golang/go/blob/370682ae98d7edc3ce9772d6d1d746df93ba9e6d/src/runtime/proc.go#L497-L506">官方runtime的代码</a>，<a href="https://github.com/go-delve/delve/blob/v1.5.0/pkg/proc/goroutine_cache.go#L16-L23">dlv 解析代码</a>，<a href="https://github.com/golang/go/blob/9393b5bae5944acebed3ab6f995926b7de3ce429/src/runtime/heapdump.go#L405-L422">官方runtime使用代码</a>  </p>

<pre><code>runtime.allglen  
runtime.allgs  
runtime.allg(旧版本)  
</code></pre>

<h4 id="">单步</h4>

<h6 id="">序章</h6>

<p>关于协程调度的地方，序章需要跳过做一些识别  </p>

<pre><code>// main.go 

package main

import (  
    "fmt"
)

func A() {  
    fmt.Println("hello world")  // set breakpoint
}

func main() {  
    A()
}

TEXT main.A(SB) /home/chainhelen/main.go  
    main.go:7   0x4aa3a0    64488b0c25f8ffffff  mov rcx, qword ptr fs:[0xfffffff8]
    main.go:7   0x4aa3a9    483b6110        cmp rsp, qword ptr [rcx+0x10]
    main.go:7   0x4aa3ad    767a            jbe 0x4aa429
=&gt;    main.go:7   0x4aa3af*   4883ec68        sub rsp, 0x68
    main.go:7   0x4aa3b3    48896c2460      mov qword ptr [rsp+0x60], rbp
    main.go:7   0x4aa3b8    488d6c2460      lea rbp, ptr [rsp+0x60]
    main.go:8   0x4aa3bd    0f57c0          xorps xmm0, xmm0
    main.go:8   0x4aa3c0    0f11442438      movups xmmword ptr [rsp+0x38], xmm0
    main.go:8   0x4aa3c5    488d442438      lea rax, ptr [rsp+0x38]
</code></pre>

<p>可能这个地方不够明显，断点在<code>0x4aa3a0</code> 或者 <code>0x4aa3af</code> 差别不是很大  </p>

<h6 id="">栈分裂</h6>

<p>再来看一下栈分裂（或者翻译成分裂栈？）情况(写屏障类似)  </p>

<pre><code>package main

import (  
    "fmt"
)

func A() {  
    s := make([]string, 0, 0)
    s = append(s, "hello", "world") // set breakpoint
    fmt.Println(s)
}

func main() {  
    A()
}
</code></pre>

<p><br>
(流程上操作为主)  </p>

<p><a href="https://go-review.googlesource.com/c/go/+/228417">go1.15引入的一个分裂栈问题</a>，<a href="https://go-review.googlesource.com/c/go/+/248878/7/src/runtime/lockrank_off.go#27">解决</a></p>

<h6 id="interface">interface</h6>

<p>对于interface的实现，也需要跳过  </p>

<pre><code>package main

import (  
    "fmt"
)

type Iface interface {  
    Blah() string
}

type A struct {  
    a int
}

func (A) Blah() string {  
    return "blah"
}

func main() {  
    var iface Iface = A{a: 1}
    s := iface.Blah()    // line 21
    fmt.Printf("%s\n", s)
}
</code></pre>

<p>(流程上操作为主) </p>

<p>第二种interface的实现，组合方式</p>

<pre><code>package main

import "fmt"

type A struct {  
    a int
}

type B struct {  
    *A
}

type Iface interface {  
    PtrReceiver() string
    NonPtrReceiver() string
}

func (*A) PtrReceiver() string {  
    return "blah"
}

func (A) NonPtrReceiver() string {  
    return "blah"
}

func main() {  
    var iface Iface = &amp;B{&amp;A{1}}
    s := iface.PtrReceiver()
    s = iface.NonPtrReceiver()
    fmt.Printf("%s\n", s)
}
</code></pre>

<p>(流程上操作为主)    </p>

<h6 id="tls">tls</h6>

<p>并不是指<code>tls</code>双向验证，而是<code>thread local storage</code>. <br>
(linux语境) M 代表的是 <code>task</code>，每一个<code>task</code>都需要一块内存来存放数据，例如 <code>G</code>结构  </p>

<p>而且 <code>go</code> 汇编中是有一个名为 <code>TLS</code>寄存器的  </p>

<p><img src="http://img.hacking.pub/debugger/debugger2.svg" alt="debugger2.svg" title="">   </p>

<p>amd64 linux fs -8 （fs地址偏移量直接-8） <br>
i386 linux gs -4   （gs是ldt，需要找到对应gdt地址-4）</p>

<p><a href="https://www.zhihu.com/question/284288720/answer/1033460480">golang tls</a> <br>
<a href="https://github.com/go-delve/delve/pull/1884/files#diff-1cc68140838ee78508038d162157bc5b5f3d68e5b492a864dc7a4f9596c008c6R1031-R1033">dlv中对于这部分的注释</a></p>

<h6 id="pie">pie</h6>

<p>地址无关，尤其是动态库，.text段被加载到虚拟内存任意位置都是可以利用当前指令的相对位置（动态重定位）</p>

<pre><code>// main.go
package main

import (  
    "fmt"
)

func main() {  
    fmt.Println("hello world\n")
}


go build -buildmode=pie -gcflags="-N -l" -o main.pie ~/main.go  
objdump -s -S main.pie &gt; main.pie.log  

go build -gcflags="-N -l" -o main.nopie ~/main.go  
objdump -s -S main.nopie &gt; main.nopie.log  
</code></pre>

<p>分别在64bit和32bit上面执行，拿到的文件做一下对比  </p>

<pre><code>export CGO_ENABLED="0"  
go build -buildmode=pie -gcflags="-N -l" -o main.pie ~/main.go 

linux x86 32位 编译不过  
linux x86 64位 能编译过  
</code></pre>

<pre><code>32位  
    world.go:17     0x86ef64    e80729f7ff      call $__x86.get_pc_thunk.dx
    world.go:17     0x86ef69    8d9277b20200        lea edx, ptr [edx+0x2b277]
    world.go:17     0x86ef6f    89542430        mov dword ptr [esp+0x30], edx
    world.go:17     0x86ef73    894c2434        mov dword ptr [esp+0x34], ecx
    world.go:17     0x86ef77    8400            test byte ptr [eax], al
    world.go:17     0x86ef79    eb00            jmp 0x86ef7b
    world.go:17     0x86ef7b    89442444        mov dword ptr [esp+0x44], eax
    world.go:17     0x86ef7f    c744244801000000    mov dword ptr [esp+0x48], 0x1
    world.go:17     0x86ef87    c744244c01000000    mov dword ptr [esp+0x4c], 0x1
    world.go:17     0x86ef8f    890424          mov dword ptr [esp], eax
    world.go:17     0x86ef92    c744240401000000    mov dword ptr [esp+0x4], 0x1
    world.go:17     0x86ef9a    c744240801000000    mov dword ptr [esp+0x8], 0x1
    world.go:17     0x86efa2    e8d9a2ffff      call $fmt.Println
</code></pre>

<p>
一些<a href="https://zhuanlan.zhihu.com/p/91420787">get_pc_thunk介绍</a>，dlv没有直接解析反汇编指令跳过，对于<code>get_pc_thunk.dx</code> 需要跳过  <a href="https://github.com/go-delve/delve/pull/1884/files#diff-1cc68140838ee78508038d162157bc5b5f3d68e5b492a864dc7a4f9596c008c6R990-R1008">get_pc_thunk</a></p>

<blockquote>
  <p>参考 <br>
  <a href="https://zhuanlan.zhihu.com/p/69504370?from_voters_page=true">一个古老又广泛的寻址技术：段寄存器</a> <br>
  <a href="http://man7.org/linux/man-pages/man2/ptrace.2.html">ptrace.2</a>   </p>
</blockquote>]]></content:encoded></item><item><title><![CDATA[《腾讯产品法》读后感？]]></title><description><![CDATA[<p>粗读一遍，细读一遍总结 <br>
文章最后几节看了，但是我并没有画到导图中，我觉得可能目前对我用处不大   </p>

<p>相对而言，本篇看起来更像是自我总结。 <br>
我更喜欢《幕后产品 打造突破式产品思维》这种由己及外的方式。</p>

<p><img src="http://img.hacking.pub/%E3%80%8A%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F%3F/%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95.svg" alt="" title="">  </p>

<p><a href="http://img.hacking.pub/%E3%80%8A%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F%3F/%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95.xmind">xmind下载</a></p>]]></description><link>http://blog.hacking.pub/2020/03/31/teng-xun-chan-pin-fa-du-hou-gan/</link><guid isPermaLink="false">5372bb43-ff69-41cf-b1e8-025781c16a63</guid><category><![CDATA[产品]]></category><category><![CDATA[腾讯产品法]]></category><category><![CDATA[产品经历]]></category><category><![CDATA[自我总结]]></category><category><![CDATA[方法论]]></category><category><![CDATA[思维导图]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Tue, 31 Mar 2020 07:01:00 GMT</pubDate><content:encoded><![CDATA[<p>粗读一遍，细读一遍总结 <br>
文章最后几节看了，但是我并没有画到导图中，我觉得可能目前对我用处不大   </p>

<p>相对而言，本篇看起来更像是自我总结。 <br>
我更喜欢《幕后产品 打造突破式产品思维》这种由己及外的方式。</p>

<p><img src="http://img.hacking.pub/%E3%80%8A%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F%3F/%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95.svg" alt="" title="">  </p>

<p><a href="http://img.hacking.pub/%E3%80%8A%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95%E3%80%8B%E8%AF%BB%E5%90%8E%E6%84%9F%3F/%E8%85%BE%E8%AE%AF%E4%BA%A7%E5%93%81%E6%B3%95.xmind">xmind下载</a></p>]]></content:encoded></item><item><title><![CDATA[2020年]]></title><description><![CDATA[<h4 id="">背景</h4>

<p>以前很少按年跨度来做总结（除了工作上），对于我而言总觉得年的跨度太长了 <br>
下面三张脑图的一些点是这两年陆陆续续补上去的 <br>
（svg矢量图可以右键在另一个tab里面打开，看起来比较清晰）  </p>

<h4 id="">架构</h4>

<p>这张图的第一版本是2年前我维护<code>nodejs api</code> 网关写的（表达的认知可能还是比较肤浅）以及后续在后端团队里面的补充 <br>
最核心的还是对于网关的认知，项目本身它承载着比较重要而且前沿的思想理论 <br>
（但是目前感觉这个项目没有充分完成它的使命，辜负了它的创始人）  </p>

<p><img src="http://img.hacking.pub/2020%E5%B9%B4/%E6%9E%B6%E6%9E%84%E8%AE%A4%E7%9F%A5.svg" alt="网关" title="">  </p>

<h4 id="">分布式事务</h4>

<p>18~19年在维护旧系统里面，遇到最恶心的问题之一：微服务之间数据不一致 <br>
尝试了一些解决方案，最后还是比较倾向柔性事务这个概念 <br>
（还有一些图，因为涉及了比较敏感数据，我感觉不应该放在blog上面）  </p>

<p><img src="http://img.hacking.pub/2020%E5%B9%B4/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1.svg" alt="分布式事务" title="">   </p>

<h4 id="">技能</h4>

<p>这个图主要是目前工作中遇到的问题，打算串起来  </p>

<p><img src="http://img.hacking.pub/2020%E5%B9%B4/skill.svg" alt="skill.svg"></p>]]></description><link>http://blog.hacking.pub/2019/12/29/2019nian/</link><guid isPermaLink="false">37fb7a3d-372b-43e9-87e2-15b92693921c</guid><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Sun, 29 Dec 2019 13:50:02 GMT</pubDate><content:encoded><![CDATA[<h4 id="">背景</h4>

<p>以前很少按年跨度来做总结（除了工作上），对于我而言总觉得年的跨度太长了 <br>
下面三张脑图的一些点是这两年陆陆续续补上去的 <br>
（svg矢量图可以右键在另一个tab里面打开，看起来比较清晰）  </p>

<h4 id="">架构</h4>

<p>这张图的第一版本是2年前我维护<code>nodejs api</code> 网关写的（表达的认知可能还是比较肤浅）以及后续在后端团队里面的补充 <br>
最核心的还是对于网关的认知，项目本身它承载着比较重要而且前沿的思想理论 <br>
（但是目前感觉这个项目没有充分完成它的使命，辜负了它的创始人）  </p>

<p><img src="http://img.hacking.pub/2020%E5%B9%B4/%E6%9E%B6%E6%9E%84%E8%AE%A4%E7%9F%A5.svg" alt="网关" title="">  </p>

<h4 id="">分布式事务</h4>

<p>18~19年在维护旧系统里面，遇到最恶心的问题之一：微服务之间数据不一致 <br>
尝试了一些解决方案，最后还是比较倾向柔性事务这个概念 <br>
（还有一些图，因为涉及了比较敏感数据，我感觉不应该放在blog上面）  </p>

<p><img src="http://img.hacking.pub/2020%E5%B9%B4/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1.svg" alt="分布式事务" title="">   </p>

<h4 id="">技能</h4>

<p>这个图主要是目前工作中遇到的问题，打算串起来  </p>

<p><img src="http://img.hacking.pub/2020%E5%B9%B4/skill.svg" alt="skill.svg"></p>]]></content:encoded></item><item><title><![CDATA[tcp连接迁移]]></title><description><![CDATA[<h4 id="">意义</h4>

<p>搜了一下好像并没有文章讲这件事，网上的<code>tcp迁移</code>貌似讲的都是网关，跟<code>upstream</code>直连偷偷换连接（偷改四元组，类似负载均衡）;；不过我说的可能跟其他人看到标题时候想的也不一样，实际主要说的是热更新tcp长链不断。  </p>

<h4 id="mosn">mosn</h4>

<p>主要也是看了<a href="https://github.com/sofastack/sofa-mosn">mosn</a>（蚂蚁版本envoy）源码，这块挺有意思的。 <br>
实际上我也没有完全看明白它的实现，里面代码封装太多层（抽象，也不好调试）。比方说，<code>conntion</code>迁移的时候是和<code>reader buffer</code>的数据一起过去了，如何避免中间时间差的<code>read buffer</code>；我只是翻懂了大致的思路，然后自己写出来一个（写法上有一些差异）。  </p>

<h4 id="">热更新</h4>

<p>关于热更新：类似nginx或者go http服务的热更新，一般都比较熟知大致流程； <br>
新进程继承listen port（子进程继承），并且接受新的http请求； <br>
旧进程上面已存在的http请求正常运行至结束，旧进程在所有http请求关闭后自动退出； <br>
所以新旧进程短时间可能会同时存在运行。</p>

<p>可以拿nginx和python来做个试验。   </p>

<p><img src="http://img.hacking.pub/tcp%E8%BF%9E%E6%8E%A5%E8%BF%81%E7%A7%BB/nginx_python.png" alt="nginx_python" title="">  </p>

<p>第一步：可以看到Nginx监听了80端口，没有任何连接过来</p>]]></description><link>http://blog.hacking.pub/2019/12/14/tcplian-jie-qian-yi/</link><guid isPermaLink="false">2690bf27-0b7a-49d5-81fe-8dd9e5e65901</guid><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Sat, 14 Dec 2019 06:02:56 GMT</pubDate><content:encoded><![CDATA[<h4 id="">意义</h4>

<p>搜了一下好像并没有文章讲这件事，网上的<code>tcp迁移</code>貌似讲的都是网关，跟<code>upstream</code>直连偷偷换连接（偷改四元组，类似负载均衡）;；不过我说的可能跟其他人看到标题时候想的也不一样，实际主要说的是热更新tcp长链不断。  </p>

<h4 id="mosn">mosn</h4>

<p>主要也是看了<a href="https://github.com/sofastack/sofa-mosn">mosn</a>（蚂蚁版本envoy）源码，这块挺有意思的。 <br>
实际上我也没有完全看明白它的实现，里面代码封装太多层（抽象，也不好调试）。比方说，<code>conntion</code>迁移的时候是和<code>reader buffer</code>的数据一起过去了，如何避免中间时间差的<code>read buffer</code>；我只是翻懂了大致的思路，然后自己写出来一个（写法上有一些差异）。  </p>

<h4 id="">热更新</h4>

<p>关于热更新：类似nginx或者go http服务的热更新，一般都比较熟知大致流程； <br>
新进程继承listen port（子进程继承），并且接受新的http请求； <br>
旧进程上面已存在的http请求正常运行至结束，旧进程在所有http请求关闭后自动退出； <br>
所以新旧进程短时间可能会同时存在运行。</p>

<p>可以拿nginx和python来做个试验。   </p>

<p><img src="http://img.hacking.pub/tcp%E8%BF%9E%E6%8E%A5%E8%BF%81%E7%A7%BB/nginx_python.png" alt="nginx_python" title="">  </p>

<p>第一步：可以看到Nginx监听了80端口，没有任何连接过来 <br>
第二步：利用python的requests库发http请求，注意调用session()是为了保持tcp暂时不断 <br>
第三步：可以看见tcp已经保持不断，随后<code>nginx -s reload</code>平滑重启nginx，发现旧进程上面的长链tcp服务器端主动断了   </p>

<h4 id="tcp">tcp长链不断</h4>

<p>新旧进程间如何保证tcp长链不断？ <br>
主要利用Linux可以发送 control message 格式，来传递fd（包括listener/tcp conntion 都是可以的） <br>
需要注意一些细节，例如<code>read buffer</code>传过去的时机；又或者因为现在的<code>tcp</code>是双工的，所以对于<code>write</code>的<code>conntion</code>是不需要重复传输<code>conntion</code>的，<code>read</code>的<code>conntion</code>已经发送过一遍了。</p>

<h4 id="reply">reply</h4>

<p>我写了一个极小demo，留作记录 <br>
我的程序会指定监听端口，当客户端连接上来的时候，无论发送过来，只有看到'#'，就会把字符串截取加上指定的前缀吐出去   </p>

<p>举个栗子  </p>

<pre><code>// 程序编译、运行  
go build -o main  
./main -l :8081 -p hello 
// 然后就不用看上面服务端的日志，主要看下面客户端返回

// 客户端使用nc连接并发送数据  
nc 127.0.0.1 8081  
// 发送
123#456#789  
// 收到
hello reply : 123#  
hello reply : 456#  
</code></pre>

<p><br>
因为<code>789</code>后面没有'#'号，所以程序并没有把 <code>789</code> 返回  </p>

<pre><code>// 这个时候启动新的进程
./main -p world

//并且把`789`后续的`#`补上发出去，你会发现nc收到了 
world reply: 789#

// 最终旧进程平滑关闭
</code></pre>

<p><br>
全流程下来 nc 与服务端的连接没有断链（可以任意中途netstat -anp查看连接），tcp保持长链无感    </p>

<p><img src="http://img.hacking.pub/tcp%E8%BF%9E%E6%8E%A5%E8%BF%81%E7%A7%BB/reply.png" alt="reply.png" title="">    </p>

<h5 id="">附件</h5>

<p><a href="http://img.hacking.pub/cm2.source.zip">代码下载地址</a>   </p>

<p>附一份代码的流程图，防止忘了    </p>

<p><img src="http://img.hacking.pub/tcp%E8%BF%9E%E6%8E%A5%E8%BF%81%E7%A7%BB/cm2.png" alt="cm2.png" title="">  </p>]]></content:encoded></item><item><title><![CDATA[落盘的b+树（二）]]></title><description><![CDATA[<h4 id="">事务</h4>

<p>之前的<a href="https://github.com/chainhelen/bptree/tree/master">b+树</a>，并没有满足一致性的需求（或者说原子性）, 因为涉及到写操作的时候，牵扯多了多个节点变更，如果出现其中一个节点写出现问题，整体上就没有办法恢复了.   </p>

<p>所以加了一个<a href="https://github.com/chainhelen/bptree/tree/transcation">分支</a>，实现了出现部分写入问题，可以进行回滚. </p>

<h4 id="">解决</h4>

<p>需要解决这种问题，目前必须采用<code>double write</code>，就是提前讲数据落在别的地方，等出现问题的时候能够从别处恢复 <br>
整体上记住一件事，出现<code>double write</code>的场景都是因为写是<code>inplace update</code>而不是<code>append only</code>  </p>

<h4 id="redoundo">redo/undo</h4>

<p>例如<code>mysql</code>中<code>innodb</code>的<code>undo</code>和<code>redo</code> <br>
都知道<code>undo</code>记录原始数据的值，本质上是逻辑日志(面向tuple记录) <br>
<code>undo</code>是保证原子性(另一个作用是为了mysql的快照读这种黑科技，mvcc)</p>]]></description><link>http://blog.hacking.pub/2019/11/07/untitled-6/</link><guid isPermaLink="false">c8ec8179-723c-4946-a41e-b526802d8ef2</guid><category><![CDATA[b+]]></category><category><![CDATA[bplus]]></category><category><![CDATA[b树]]></category><category><![CDATA[事务]]></category><category><![CDATA[transcation]]></category><dc:creator><![CDATA[chainhelen]]></dc:creator><pubDate>Thu, 07 Nov 2019 09:00:00 GMT</pubDate><content:encoded><![CDATA[<h4 id="">事务</h4>

<p>之前的<a href="https://github.com/chainhelen/bptree/tree/master">b+树</a>，并没有满足一致性的需求（或者说原子性）, 因为涉及到写操作的时候，牵扯多了多个节点变更，如果出现其中一个节点写出现问题，整体上就没有办法恢复了.   </p>

<p>所以加了一个<a href="https://github.com/chainhelen/bptree/tree/transcation">分支</a>，实现了出现部分写入问题，可以进行回滚. </p>

<h4 id="">解决</h4>

<p>需要解决这种问题，目前必须采用<code>double write</code>，就是提前讲数据落在别的地方，等出现问题的时候能够从别处恢复 <br>
整体上记住一件事，出现<code>double write</code>的场景都是因为写是<code>inplace update</code>而不是<code>append only</code>  </p>

<h4 id="redoundo">redo/undo</h4>

<p>例如<code>mysql</code>中<code>innodb</code>的<code>undo</code>和<code>redo</code> <br>
都知道<code>undo</code>记录原始数据的值，本质上是逻辑日志(面向tuple记录) <br>
<code>undo</code>是保证原子性(另一个作用是为了mysql的快照读这种黑科技，mvcc)  </p>

<p>而<code>redo</code>记录事务中数据变化后的数据，本质上是物理逻辑日志日志(面向page页，physical-to-a-page, logical-within-a-page)，更像是"残缺"的快照 <br>
说<code>残缺</code>是因为如果<code>快照</code>下全部页的占用空间成本太高了，通常不会全部记录 <br>
说<code>快照</code>是因为记录的数据本身，而不是逻辑数据 <br>
<code>redo</code>是为了保证持久性(实际上，我认为如果不考虑<code>性能问题</code>，<code>redo</code>是没有必要存在的，<code>mysql</code>为了性能，把部分成功的<code>commit</code>事务脏页留在了内存，当出现闪断等崩溃时候，内存全丢，期望从<code>redo</code>里面恢复)</p>

<p>由于<code>undo/redo</code>是顺序写(page cahce起到的作用，系统／物理内存层面，不涉及用户态内存，跟用户态的buffer作用很像，但是概论上有差别)，都属于<code>wal</code>, 速度很快   </p>

<p>ps: undo也是被当成数据(对undo页的修改，也需要先写redo日志)，有相应的redo日志，通过redo日志可以恢复undo</p>

<h4 id="doublewrite">double write</h4>

<p>这里面说的<code>double write</code>跟上面有点差别，但是概论上是一样 <br>
<code>double write</code>这里面原因是<code>mysql</code>的数据页是以 16k为单位的，而文件系统通常是以4k为单位的（磁盘io的通常是以512Byte为单位的，通常文件系统能保证4k单位写入是原子的，例如ext3/ext4，但是ext2就不行的，我猜文件系统实现也是以<code>double write</code>方式类似，<code>redo</code>的日志设计成512Bytes），数据页和文件系统的写入单位不一致，极端情况下就会可能出现数据页部分写入的问题（可以理解成数据页的原子写入），所以<code>mysql</code>需要<code>double write</code>将这部分数据先保存另一个地方，保证能够恢复     </p>

<h4 id="boltdb">boltdb</h4>

<p>对于boltdb的实现，是典型的<code>append only</code>，所以没有<code>redo</code>和<code>undo</code>，将所有db文件直接mmap. 维护两个版本<code>meta</code>，每次涉及到的修改<code>node</code>(对应磁盘<code>page</code>)都不破坏原来的节点数据，直接新建一个新的. <br>
落盘顺序是 <code>data</code>（不影响当前版本的数据）/<code>meta</code>. 如果<code>data</code>落盘失败，不影响当前已完成事务的数据；如果<code>mata</code>失败，由于<code>meta</code>会做<code>checksum</code>检查<code>meta</code>数据是否是正确的，当前<code>meta</code>也是不认为是完成事务的.   </p>

<p>注意一点，就是<code>boltdb</code>的<code>b+</code>树跟正常的有点不一样，page结构体没有<code>next</code>信息（没有维护指向兄弟的信息），没有<code>parent</code>节点信息. <br>
这个是因为如果包含了，那么每次<code>insert</code>一个新的<code>key</code>，为了包括数据一致性，整颗树都得dump到内存中. <br>
不维护这些信息，动态从<code>page</code>到<code>node</code>的时候重新构建，这样每次产生的脏页只是查询链路的节点（如果不考虑分裂）.  </p>

<h4 id="">参考</h4>

<blockquote>
  <p><a href="https://youjiali1995.github.io/storage/boltdb/">https://youjiali1995.github.io/storage/boltdb/</a> <br>
  <a href="https://blog.csdn.net/mydriverc2/article/details/50629599">https://blog.csdn.net/mydriverc2/article/details/50629599</a> <br>
  <a href="https://www.cnblogs.com/cchust/p/3961260.html">https://www.cnblogs.com/cchust/p/3961260.html</a>  double write 和 redo 区别 <br>
  <a href="https://www.zhihu.com/question/280594447/answer/416845508">https://www.zhihu.com/question/280594447/answer/416845508</a> 阿里云的polardb 为什么可以取消double write <br>
  <a href="https://zhuanlan.zhihu.com/p/86622268">https://zhuanlan.zhihu.com/p/86622268</a> 这里面redo是物理日志写的有问题 <br>
  <a href="https://www.cnblogs.com/geaozhang/p/8555660.html">https://www.cnblogs.com/geaozhang/p/8555660.html</a> <br>
  <a href="http://mysql.taobao.org/monthly/2017/07/01/">http://mysql.taobao.org/monthly/2017/07/01/</a> MySQL · 引擎特性 · InnoDB崩溃恢复</p>
</blockquote>]]></content:encoded></item></channel></rss>