[{"content":"  命令类型 命令 功能   启动/停止 run 启动程序    r     run 命令行参数 以传入参数的方式启动程序    run \u0026gt; 输出文件 将输出重定向到输出文件    continue 继续运行，直到下一个断点    c     kill 停止程序    quit 退出gdb   源代码 list 查看源代码    l     list 行号 显示指定行号代码    list 函数名 显示指定函数的代码    list - 往前显示代码    list 开始, 结束 显示指定区间的代码    list 文件名:行号 显示指定文件名的指定行代码    set listsize 数字 设置显示的代码行数    show listsize 查看一次显示的代码行数    directory 目录名 添加目录到源代码搜索路径中    dir 目录名     show directories 查看源代码搜索目录    directory     dir 清空添加到源代码搜索目录中的目录   断点管理 break 断点命令    b     break 函数名 为函数设置断点    break 代码行号 在某一代码行上设置断点    break 类名:函数名 在某个类的函数上设置断点    break 文件名:函数名 在文件名指定的函数上设置断点    break 文件名:行号 在文件名指定的代码行上设置断点    break *地址 在指定地址设置断点    break +偏移量 在当前代码行加上偏移量的位置设置断点    break -偏移量 在当前代码行减去偏移量的位置设置断点    break 行号 if条件 设置条件断点    tbreak 设置临时断点    watch 表达式 添加观察点    clear 删除所有断点    clear 函数 删除该函数的断点    clear 行号 删除行号对应的断点    delete 删除所有断点，包括观察点和捕获点    d     delete 断点编号 删除指定编号断点    delete 断点范围 删除指定范围断点    disable 断点范围 禁用指定范围的断点    enable 断点范围 启用指定范围断点    enable 断点编号 once 启用指定断点一次   执行 continue 数量 继续执行，忽略指定数量的命中次数    finish 跳出当前函数    step 逐语句执行    s     step 步数 逐语句执行步数    next 逐过程执行    n     next 数量 逐过程执行指定行数的代码    where 显示当前执行的具体函数和代码行   调用栈 backtrace 显示调用栈信息    bt     bt 栈帧数 显示指定数量的栈帧（从小到大）    bt -栈帧数 显示指定数量的栈帧（从大到小）    backtrace full 显示所有栈帧的局部变量    frame 显示当前帧    frame 帧编号 切换帧到指定编号的帧    f 帧编号     up 切换帧，将当前帧增大1    down 切换帧，将当前帧减少1    up 帧数量 切换帧，将当前帧增大指定数量切换帧，将当前帧减少指定数量    down 帧数量    查看信息 info frame 查看当前帧的信息    info args  查看当前帧的参数    info locals 查看当前帧的局部变量    info breakpoints 查看所有断点信息    info break      i b     info break 断点编号 查看指定断点编号的断点信息    info watchpoints 查看所有观察点信息    info registers 查看所有整型寄存器信息    info threads 查看所有线程信息   查看变量 x 地址 查看指定地址的内存    x /nfu 地址 以格式化的方式查看指定地址的内存    print 变量名 查看变量    p 变量名     p 文件名::变量名 查看指定文件的变量    ptype 变量 查看变量类型    ptype 数据类型 查看类型详细信息   gdb模式 set logging on  设置日志开关    set logging off      show logging     set logging file 日志文件 设置日志文件名，默认名称为gdb.txt    set print array on  数组显示是否友好开关，默认是关闭的    set print array off     show print array     set print array-indexes on 显示数组索引开关，默认是关闭的    set print array-indexes off      show print array-indexes     set print pretty on  格式化结构体，默认是关闭的    set print pretty off      show print pretty     set print union on  联合体开关，默认是关闭的    set print union off      show print union   ","permalink":"https://lambertxiao.github.io/posts/gdb_usage/doc/","summary":"好记性不如烂笔头","title":"使用GDB调试程序"},{"content":"NUMA 今天在学习DPDK的过程中接触到了NUMA，特此记录\n什么是NUMA NUMA全称为 Non-uniform memory access, 即非一致性内存访问，它是一种计算机内存设计方案，主要用于多处理器的计算机中。但在理解NUMA之前， 我们需要先了解SMP（Symmetric multiprocessing），即对称性多处理；在SMP架构中，CPU和内存的关系如下图所示：\n所有的CPU核心处理器都通过系统总线与内存进行数据交互（中间有Cache层会对数据进行访问加速），这架构很简单对吧，一目了然，但是这个架构存在一个问题， 就是当CPU的核心处理器越来越多时，由于总线在同一时刻只能有一个设备在访问，因此CPU的核心处理器之间会相互争夺总线的使用权，核心越多， 争夺得越激烈，运行效率就越低。\n在这种情况下，NUMA诞生了，NUMA的架构其实也很简单，如下\nNUMA 尝试通过为每个处理器提供单独的内存来解决此问题，从而避免多个处理器尝试寻址同一内存时对性能造成的影响。 NUMA定义了一个叫Node的概念，每一个Node会包含一组CPU的处理器，内存控制器（Memory Controler）和一组内存。CPU的处理器通过内存控制器访问内存。 Node与Node之间是物理上相互连接着的。 Node内的处理器访问Node里的内存，称为local access，Node内的处理器访问其他Node的内存，称为remote access。\n怎么使用NUMA 经过前面的介绍，我们知道，local access是在一个Node内部发生的，争夺内存控制器的使用权的处理器数量是有限的，因此它必然是很高效的； 我们在编程过程中也要尽量让程序在一个Node内运行，而不要在各个Node间相互调度。在Linux上，提供了一个 numactl 的工具可以帮助我们对处理器的numa策略进行配置。\n显示node的配置 numactl -H available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 8 9 20 21 22 23 24 25 26 27 28 29 node 0 size: 63822 MB node 0 free: 20142 MB node 1 cpus: 10 11 12 13 14 15 16 17 18 19 30 31 32 33 34 35 36 37 38 39 node 1 size: 64507 MB node 1 free: 19918 MB node distances: node 0 1 0: 10 21 1: 21 10 可以看到，我的机器上被划分了两个Node，并且每个node分配的内存在60GB左右，其中 Node0包含处理器（0 1 2 3 4 5 6 7 8 9 20 21 22 23 24 25 26 27 28 29）， Node1包含（10 11 12 13 14 15 16 17 18 19 30 31 32 33 34 35 36 37 38 39）。 Node Distances是NUMA架构中的节点距离，指的是从一个节点访问另一个节点上的内存所需要付出的代价或延迟。\n","permalink":"https://lambertxiao.github.io/posts/numa/doc/","summary":"NUMA 今天在学习DPDK的过程中接触到了NUMA，特此记录\n什么是NUMA NUMA全称为 Non-uniform memory access, 即非一致性内存访问，它是一种计算机内存设计方案，主要用于多处理器的计算机中。但在理解NUMA之前， 我们需要先了解SMP（Symmetric multiprocessing），即对称性多处理；在SMP架构中，CPU和内存的关系如下图所示：\n所有的CPU核心处理器都通过系统总线与内存进行数据交互（中间有Cache层会对数据进行访问加速），这架构很简单对吧，一目了然，但是这个架构存在一个问题， 就是当CPU的核心处理器越来越多时，由于总线在同一时刻只能有一个设备在访问，因此CPU的核心处理器之间会相互争夺总线的使用权，核心越多， 争夺得越激烈，运行效率就越低。\n在这种情况下，NUMA诞生了，NUMA的架构其实也很简单，如下\nNUMA 尝试通过为每个处理器提供单独的内存来解决此问题，从而避免多个处理器尝试寻址同一内存时对性能造成的影响。 NUMA定义了一个叫Node的概念，每一个Node会包含一组CPU的处理器，内存控制器（Memory Controler）和一组内存。CPU的处理器通过内存控制器访问内存。 Node与Node之间是物理上相互连接着的。 Node内的处理器访问Node里的内存，称为local access，Node内的处理器访问其他Node的内存，称为remote access。\n怎么使用NUMA 经过前面的介绍，我们知道，local access是在一个Node内部发生的，争夺内存控制器的使用权的处理器数量是有限的，因此它必然是很高效的； 我们在编程过程中也要尽量让程序在一个Node内运行，而不要在各个Node间相互调度。在Linux上，提供了一个 numactl 的工具可以帮助我们对处理器的numa策略进行配置。\n显示node的配置 numactl -H available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 8 9 20 21 22 23 24 25 26 27 28 29 node 0 size: 63822 MB node 0 free: 20142 MB node 1 cpus: 10 11 12 13 14 15 16 17 18 19 30 31 32 33 34 35 36 37 38 39 node 1 size: 64507 MB node 1 free: 19918 MB node distances: node 0 1 0: 10 21 1: 21 10 可以看到，我的机器上被划分了两个Node，并且每个node分配的内存在60GB左右，其中 Node0包含处理器（0 1 2 3 4 5 6 7 8 9 20 21 22 23 24 25 26 27 28 29）， Node1包含（10 11 12 13 14 15 16 17 18 19 30 31 32 33 34 35 36 37 38 39）。 Node Distances是NUMA架构中的节点距离，指的是从一个节点访问另一个节点上的内存所需要付出的代价或延迟。","title":"什么是NUMA"},{"content":"UIO 又是学DPDK的时候接触到的知识，DPDK里有特别多的用户态的驱动，实现用户态驱动需要会用到的技术之一就是UIO\n什么是UIO UIO全称为UserSpace I/O，是Linux内核提供的一个用户态驱动框架；它由两部分组成，一个非常小巧的内核模块和一个用户态驱动；\n为什么需要用UIO 对于许多类型的设备，创建Linux内核驱动程序是多余的。真正需要的是能处理硬件的中断和访问设备的内存空间。控制设备的逻辑不一定必须在内核内，因为设备不需要利用内核提供的任何其他资源。一种常见的设备类型是工业I/O卡。为了解决这种情况，UIO诞生了。对于典型的工业I/O卡，只需要一个非常小的内核模块。驱动程序的主要部分将在用户空间中运行。这简化了开发并降低了内核模块中出现严重错误的风险。\n什么硬件适合用UIO UIO不是一个通用的驱动程序接口。已经被其他内核子系统处理得很好的设备（如网络、串行或USB）不适合UIO驱动程序。非常适合UIO驱动程序的硬件可以满足以下所有要求：\n 设备具有可映射的内存。通过写入该存储器，可以完全控制该设备。 设备通常会产生中断。 该设备不适合标准内核子系统之一。  使用UIO的好处  只需编写和维护一个小的内核模块。 在用户空间中开发驱动程序的主要部分，使用您习惯的所有工具和库。 驱动程序中的错误不会使内核崩溃。 你的驱动程序的更新可以在不重新编译内核的情况下进行。  UIO的困局 UIO的设计初衷是为了提供一种简单、轻量级的用户空间I/O框架，使得用户空间程序能够直接访问设备I/O内存和中断，而不需要内核空间的参与。因此，\n 不支持 DMA（不受 IOMMU 的保护），也就意味着没有DMA为它做虚拟地址到物理地址的转化，只能直接访问物理地址，危险性极高 由于不支持DMA，所以UIO只能在用户空间操作硬件设备的寄存器空间，而无法支持通过DMA把内核空间的数据传送到用户空间。对于需要通过DMA进行大量数据传输的I/O设备，如网卡、显卡等，UIO就无法满足需求，需要使用其他机制，如VFIO（Virtual Function I/O）等。 UIO需要root权限。  ","permalink":"https://lambertxiao.github.io/posts/uio/doc/","summary":"UIO 又是学DPDK的时候接触到的知识，DPDK里有特别多的用户态的驱动，实现用户态驱动需要会用到的技术之一就是UIO\n什么是UIO UIO全称为UserSpace I/O，是Linux内核提供的一个用户态驱动框架；它由两部分组成，一个非常小巧的内核模块和一个用户态驱动；\n为什么需要用UIO 对于许多类型的设备，创建Linux内核驱动程序是多余的。真正需要的是能处理硬件的中断和访问设备的内存空间。控制设备的逻辑不一定必须在内核内，因为设备不需要利用内核提供的任何其他资源。一种常见的设备类型是工业I/O卡。为了解决这种情况，UIO诞生了。对于典型的工业I/O卡，只需要一个非常小的内核模块。驱动程序的主要部分将在用户空间中运行。这简化了开发并降低了内核模块中出现严重错误的风险。\n什么硬件适合用UIO UIO不是一个通用的驱动程序接口。已经被其他内核子系统处理得很好的设备（如网络、串行或USB）不适合UIO驱动程序。非常适合UIO驱动程序的硬件可以满足以下所有要求：\n 设备具有可映射的内存。通过写入该存储器，可以完全控制该设备。 设备通常会产生中断。 该设备不适合标准内核子系统之一。  使用UIO的好处  只需编写和维护一个小的内核模块。 在用户空间中开发驱动程序的主要部分，使用您习惯的所有工具和库。 驱动程序中的错误不会使内核崩溃。 你的驱动程序的更新可以在不重新编译内核的情况下进行。  UIO的困局 UIO的设计初衷是为了提供一种简单、轻量级的用户空间I/O框架，使得用户空间程序能够直接访问设备I/O内存和中断，而不需要内核空间的参与。因此，\n 不支持 DMA（不受 IOMMU 的保护），也就意味着没有DMA为它做虚拟地址到物理地址的转化，只能直接访问物理地址，危险性极高 由于不支持DMA，所以UIO只能在用户空间操作硬件设备的寄存器空间，而无法支持通过DMA把内核空间的数据传送到用户空间。对于需要通过DMA进行大量数据传输的I/O设备，如网卡、显卡等，UIO就无法满足需求，需要使用其他机制，如VFIO（Virtual Function I/O）等。 UIO需要root权限。  ","title":"什么是UIO"},{"content":"VFIO 什么是VFIO  VFIO可以简单理解为一个增强版的UIO，上一个文章里提到了UIO的几个不足，不支持DMA，仅支持有限的中断，需要用root访问等，而VFIO就是为了弥补这些不足诞生的。\n VFIO也是linux提供的一个用户态驱动框架，使用VFIO能开发用户态驱动。VFIO 允许用户将物理设备直接分配给虚拟机，从而让虚拟机获得接近物理机的性能。（VFIO 可以绕过虚拟化管理程序，直接将虚拟机的 I/O 请求发送到物理设备）\n为什么要用VFIO 一些应用程序，特别是在高性能计算领域，由于需要很低的延时开销，需要从用户空间直接访问设备。在没有VFIO的时候，有两个选择，\n 可以选择直接开发内核驱动（复杂又繁琐，稳定性差） 使用UIO框架，但该框架没有IOMMU保护的概念，有限的中断支持，并且需要root权限才能访问PCI配置空间等内容。  VFIO驱动程序框架旨在解决这些问题，取代KVM PCI特定的设备分配代码，并提供比UIO更安全，功能更丰富的用户空间驱动程序环境。\nVFIO被用在哪里  KVM QEMU VMware ESXi Hyper-V  怎么使用 VFIO 使用用户态驱动来实现设备直通。用户态驱动运行在虚拟机的用户空间中，可以直接访问物理设备。\nVFIO 的工作流程如下：\n 用户在虚拟机中安装 VFIO 驱动。 用户将物理设备分配给虚拟机。 VFIO 驱动在虚拟机的用户空间中启动。 VFIO 驱动访问物理设备。 VFIO 提供了一系列 API 来管理设备直通。这些 API 允许用户：   列出可用的物理设备 将物理设备分配给虚拟机 从虚拟机中释放物理设备 配置 VFIO 驱动  VFIO的组成 Device（设备） 设备是指要操作的硬件设备，这些设备可以是网卡、显卡、存储控制器等。在VFIO中，设备是通过IOMMU（Input/Output Memory Management Unit）进行管理的。IOMMU是一个硬件单元，它可以把设备的IO地址映射成虚拟地址，为设备提供页表映射，使得设备可以直接通过DMA（Direct Memory Access）方式访问内存。 设备在VFIO中是被隔离和暴露给虚拟机或用户空间程序的关键资源。通过VFIO，设备可以被分配给特定的虚拟机或用户空间程序，以实现设备直通。\nGroup（组） Group是IOMMU能进行DMA隔离的最小硬件单元。一个group可以包含一个或多个device，具体取决于物理平台上硬件的IOMMU拓扑结构，Group是硬件上的划分。这意味着，如果一个设备在硬件拓扑上是独立的，那么它本身就构成一个IOMMU group。而如果多个设备在硬件上是互联的，需要相互访问数据，那么这些设备需要被放到同一个IOMMU group中。在VFIO中，group是设备直通的最小单位。也就是说，当设备直通给一个虚拟机时，group内的所有设备都必须同时直通给该虚拟机。\nContainer（容器） Container是由多个group组成的集合，Container是逻辑上的划分，为了让内部的group能共享某些资源。 虽然group是VFIO的最小隔离单元，但在某些情况下，将多个group组合到一个container中可以提高系统的性能。例如，当多个group需要共享一个页表时，将它们组合到一个container中是有益的。此外，将多个group放入一个container中也方便用户进行管理和控制。\nVFIO在DKDP中的使用 在上面的例子中，PCI设备1和PCI设备2是被分配给客户DPDK应用的两个设备。在主机中，这两个设备都使用内核VFIO驱动程序分配给客户机。在Guest系统中，当我们将设备分配给DPDK应用程序时，我们可以使用VFIO、VFIO no-iommu mode、UIO三种模式。然而，只有当我们使用通用VFIO驱动程序（需要vIOMMU）分配设备时，我们才能获得安全分配的设备。通过“UIO”或“VFIO no-iommu模式”分配设备是不安全的。\n","permalink":"https://lambertxiao.github.io/posts/vfio/doc/","summary":"VFIO 什么是VFIO  VFIO可以简单理解为一个增强版的UIO，上一个文章里提到了UIO的几个不足，不支持DMA，仅支持有限的中断，需要用root访问等，而VFIO就是为了弥补这些不足诞生的。\n VFIO也是linux提供的一个用户态驱动框架，使用VFIO能开发用户态驱动。VFIO 允许用户将物理设备直接分配给虚拟机，从而让虚拟机获得接近物理机的性能。（VFIO 可以绕过虚拟化管理程序，直接将虚拟机的 I/O 请求发送到物理设备）\n为什么要用VFIO 一些应用程序，特别是在高性能计算领域，由于需要很低的延时开销，需要从用户空间直接访问设备。在没有VFIO的时候，有两个选择，\n 可以选择直接开发内核驱动（复杂又繁琐，稳定性差） 使用UIO框架，但该框架没有IOMMU保护的概念，有限的中断支持，并且需要root权限才能访问PCI配置空间等内容。  VFIO驱动程序框架旨在解决这些问题，取代KVM PCI特定的设备分配代码，并提供比UIO更安全，功能更丰富的用户空间驱动程序环境。\nVFIO被用在哪里  KVM QEMU VMware ESXi Hyper-V  怎么使用 VFIO 使用用户态驱动来实现设备直通。用户态驱动运行在虚拟机的用户空间中，可以直接访问物理设备。\nVFIO 的工作流程如下：\n 用户在虚拟机中安装 VFIO 驱动。 用户将物理设备分配给虚拟机。 VFIO 驱动在虚拟机的用户空间中启动。 VFIO 驱动访问物理设备。 VFIO 提供了一系列 API 来管理设备直通。这些 API 允许用户：   列出可用的物理设备 将物理设备分配给虚拟机 从虚拟机中释放物理设备 配置 VFIO 驱动  VFIO的组成 Device（设备） 设备是指要操作的硬件设备，这些设备可以是网卡、显卡、存储控制器等。在VFIO中，设备是通过IOMMU（Input/Output Memory Management Unit）进行管理的。IOMMU是一个硬件单元，它可以把设备的IO地址映射成虚拟地址，为设备提供页表映射，使得设备可以直接通过DMA（Direct Memory Access）方式访问内存。 设备在VFIO中是被隔离和暴露给虚拟机或用户空间程序的关键资源。通过VFIO，设备可以被分配给特定的虚拟机或用户空间程序，以实现设备直通。\nGroup（组） Group是IOMMU能进行DMA隔离的最小硬件单元。一个group可以包含一个或多个device，具体取决于物理平台上硬件的IOMMU拓扑结构，Group是硬件上的划分。这意味着，如果一个设备在硬件拓扑上是独立的，那么它本身就构成一个IOMMU group。而如果多个设备在硬件上是互联的，需要相互访问数据，那么这些设备需要被放到同一个IOMMU group中。在VFIO中，group是设备直通的最小单位。也就是说，当设备直通给一个虚拟机时，group内的所有设备都必须同时直通给该虚拟机。\nContainer（容器） Container是由多个group组成的集合，Container是逻辑上的划分，为了让内部的group能共享某些资源。 虽然group是VFIO的最小隔离单元，但在某些情况下，将多个group组合到一个container中可以提高系统的性能。例如，当多个group需要共享一个页表时，将它们组合到一个container中是有益的。此外，将多个group放入一个container中也方便用户进行管理和控制。","title":"什么是VFIO"},{"content":"Virtio 什么是Virtio Virtio是一种半虚拟化 I/O 设备虚拟化技术，用于在虚拟机管理程序（hypervisor）中实现虚拟设备。 它提供了一种通用的、高性能的设备模型和应用编程接口 (API)，可以减少虚拟化开销并提高虚拟机的性能。\n 半虚拟化是一种虚拟化技术，它介于完全虚拟化和裸机运行之间。与完全虚拟化不同，半虚拟化需要对操作系统内核进行修改，使其意识到自己正在运行在虚拟化环境中。\n 为什么使用Virtio Virtio的出现是为了解决传统虚拟化设备模拟的性能问题。传统虚拟化设备模拟通常需要在虚拟机管理程序和虚拟机之间进行多层数据复制，这会导致性能下降。Virtio通过半虚拟化技术将设备驱动程序的一部分功能移到虚拟机中，从而减少了数据复制的次数，提高了性能。\n传统虚拟化   完全虚拟化 完全虚拟化是一种完全模拟物理硬件的虚拟化技术。在这种方式下，虚拟机管理程序（Hypervisor）会创建一个虚拟的硬件环境，包括虚拟 CPU、虚拟内存、虚拟磁盘、虚拟网络等。虚拟机操作系统认为自己正在运行在真实的物理硬件上。完全虚拟化的实现方式主要有两种：\n 软件二进制翻译：虚拟机管理程序会将虚拟机操作系统的二进制指令翻译成真实 CPU 可以执行的指令。这种方式可以支持任何操作系统，但性能会受到一定的影響。 动态二进制翻译：虚拟机管理程序会在虚拟机操作系统运行时动态地翻译指令。这种方式可以提高性能，但需要对操作系统内核进行一些修改。    硬件辅助虚拟化\n硬件辅助虚拟化是一种利用 CPU 和其他硬件提供的虚拟化功能的虚拟化技术。在这种方式下，虚拟机管理程序可以利用 CPU 的虚拟化扩展指令（如 Intel VT 或 AMD SVM）来提高虚拟化的性能和效率。硬件辅助虚拟化的实现方式主要有两种：\n 全虚拟化：虚拟机管理程序仍然会模拟一部分硬件，但会利用 CPU 的虚拟化扩展指令来提高性能。 半虚拟化：虚拟机操作系统需要进行一些修改，以便直接使用 CPU 的虚拟化扩展指令。这种方式可以获得更高的性能，但对操作系统的兼容性要求更高。    Virtio的主要优点包括：\n 提高性能：Virtio通过减少数据复制的次数来提高虚拟机的性能。 提高可靠性：Virtio使用共享内存机制进行通信，这可以提高虚拟机的可靠性。 提高可移植性：Virtio提供了一种通用的设备模型，可以移植到不同的虚拟机管理程序和虚拟机操作系统中。  Virtio应用在哪里 Virtio主要用于虚拟机环境中，包括服务器虚拟化、桌面虚拟化和云计算等。\nVirtio工作原理 Virtio采用前端-后端架构\n 前端驱动程序(FE), FE驱动程序只需要提供配置接口、传递消息、生成请求和启动后端virtio驱动程序的服务。因此，FE驱动程序易于实现，并且消除了模拟设备的性能开销 后端驱动程序(BE), BE驱动程序在主机操作系统的用户区或内核区运行，使用来自FE驱动程序的请求并将其发送到主机本地设备驱动程序。一旦主机本地设备驱动程序完成请求，BE驱动程序通知FE驱动程序请求完成 前端驱动程序和后端驱动程序通过虚拟队列（Virtqueue）进行通信。虚拟队列是一种共享内存机制，用于在虚拟机管理程序和虚拟机之间传递数据。 virtio支持PCI/PCIe总线和MMIO总线  参考文章：https://projectacrn.github.io/latest/developer-guides/hld/hld-virtio-devices.html#virtio-apis\n","permalink":"https://lambertxiao.github.io/posts/virtio/doc/","summary":"Virtio 什么是Virtio Virtio是一种半虚拟化 I/O 设备虚拟化技术，用于在虚拟机管理程序（hypervisor）中实现虚拟设备。 它提供了一种通用的、高性能的设备模型和应用编程接口 (API)，可以减少虚拟化开销并提高虚拟机的性能。\n 半虚拟化是一种虚拟化技术，它介于完全虚拟化和裸机运行之间。与完全虚拟化不同，半虚拟化需要对操作系统内核进行修改，使其意识到自己正在运行在虚拟化环境中。\n 为什么使用Virtio Virtio的出现是为了解决传统虚拟化设备模拟的性能问题。传统虚拟化设备模拟通常需要在虚拟机管理程序和虚拟机之间进行多层数据复制，这会导致性能下降。Virtio通过半虚拟化技术将设备驱动程序的一部分功能移到虚拟机中，从而减少了数据复制的次数，提高了性能。\n传统虚拟化   完全虚拟化 完全虚拟化是一种完全模拟物理硬件的虚拟化技术。在这种方式下，虚拟机管理程序（Hypervisor）会创建一个虚拟的硬件环境，包括虚拟 CPU、虚拟内存、虚拟磁盘、虚拟网络等。虚拟机操作系统认为自己正在运行在真实的物理硬件上。完全虚拟化的实现方式主要有两种：\n 软件二进制翻译：虚拟机管理程序会将虚拟机操作系统的二进制指令翻译成真实 CPU 可以执行的指令。这种方式可以支持任何操作系统，但性能会受到一定的影響。 动态二进制翻译：虚拟机管理程序会在虚拟机操作系统运行时动态地翻译指令。这种方式可以提高性能，但需要对操作系统内核进行一些修改。    硬件辅助虚拟化\n硬件辅助虚拟化是一种利用 CPU 和其他硬件提供的虚拟化功能的虚拟化技术。在这种方式下，虚拟机管理程序可以利用 CPU 的虚拟化扩展指令（如 Intel VT 或 AMD SVM）来提高虚拟化的性能和效率。硬件辅助虚拟化的实现方式主要有两种：\n 全虚拟化：虚拟机管理程序仍然会模拟一部分硬件，但会利用 CPU 的虚拟化扩展指令来提高性能。 半虚拟化：虚拟机操作系统需要进行一些修改，以便直接使用 CPU 的虚拟化扩展指令。这种方式可以获得更高的性能，但对操作系统的兼容性要求更高。    Virtio的主要优点包括：\n 提高性能：Virtio通过减少数据复制的次数来提高虚拟机的性能。 提高可靠性：Virtio使用共享内存机制进行通信，这可以提高虚拟机的可靠性。 提高可移植性：Virtio提供了一种通用的设备模型，可以移植到不同的虚拟机管理程序和虚拟机操作系统中。  Virtio应用在哪里 Virtio主要用于虚拟机环境中，包括服务器虚拟化、桌面虚拟化和云计算等。\nVirtio工作原理 Virtio采用前端-后端架构\n 前端驱动程序(FE), FE驱动程序只需要提供配置接口、传递消息、生成请求和启动后端virtio驱动程序的服务。因此，FE驱动程序易于实现，并且消除了模拟设备的性能开销 后端驱动程序(BE), BE驱动程序在主机操作系统的用户区或内核区运行，使用来自FE驱动程序的请求并将其发送到主机本地设备驱动程序。一旦主机本地设备驱动程序完成请求，BE驱动程序通知FE驱动程序请求完成 前端驱动程序和后端驱动程序通过虚拟队列（Virtqueue）进行通信。虚拟队列是一种共享内存机制，用于在虚拟机管理程序和虚拟机之间传递数据。 virtio支持PCI/PCIe总线和MMIO总线  参考文章：https://projectacrn.github.io/latest/developer-guides/hld/hld-virtio-devices.html#virtio-apis","title":"什么是Virtio"},{"content":"今天来探讨一下 linux 环境下进程的内存布局，这次探讨包括以下内容\n 一个 C 程序启动后，它的内存使用情况 一些工具的使用  objdump readelf strace    程序的内存布局长啥样呢 一般的，当你在网上查阅文章、书籍、ChatGPT 的时候，都会告诉你内存的布局基本就长下图这样\n首先，我们将程序的内存理解成一段逻辑上连续的空间，这里为什么强调是逻辑上连续呢，因为实际上这里的内存指的是虚拟内存，而虚拟内存到物理内存之间有一层映射，一块连续的虚拟内存可能在物理内存上是映射到不连续的内存上的。\n其次，如上图，当程序运行起来时，它所占用的内存会被按功能拆分，分为了多个区，从低地址到高地址来看，分别是代码区，数据区（包含 BSS 和 Data），内存映射区，堆区和栈区。\n但纸上得来终觉浅，接下来我们通过一个 C 程序来实际看一下它的内存布局和上图里的布局是不是一样的。\n#include \u0026lt;stdio.h\u0026gt;#include \u0026lt;stdlib.h\u0026gt; int main () { char * addr; printf(\u0026#34;curr process id: %d\\n\u0026#34;, getpid()); printf(\u0026#34;Before malloc in the main thread\\n\u0026#34;); getchar(); addr = (char *) malloc(1000); printf(\u0026#34;After malloc and before free in main thread\\n\u0026#34;); getchar(); free(addr); printf(\u0026#34;After free in main thread\\n\u0026#34;); getchar(); return 0; } 这是一个很简单的 C 程序，在整个 main 函数里，我们打印了当前的进程 id，并多次使用了getchar() 让进程能在我们希望的地方停下来，方便我们来观察它的内存。\n将该代码保存到main.c文件中，我们编译并运行它，gcc main.c -o main \u0026amp;\u0026amp; ./main, 我们会得到一个如下的输出\n[root@lambertx memory_layout]# gcc main.c -o main \u0026amp;\u0026amp; ./main curr process id: 4070 Before malloc in the main thread 此时会发现程序符合我们预期的 hang 住了，接下来我们需要通过某种方式看到 4070 这个进程的内存布局；我们都知道，在 linux 系统中，有一句很有名的就是“一切皆文件”，其实进程的内存布局也不意外，它存在一个虚拟的文件中。下面我们需要另起一个 terminal，并通过 cat 命令查看一下这个文件的内容\ncat /proc/4070/maps 一般的，我们会看到类似如下的输出\n[root@lambertx]# cat /proc/4070/maps 00400000-00401000 r-xp 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 00600000-00601000 r--p 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 00601000-00602000 rw-p 00001000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 7f3220378000-7f322051a000 r-xp 00000000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f322051a000-7f322071a000 ---p 001a2000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f322071a000-7f322071e000 r--p 001a2000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f322071e000-7f3220720000 rw-p 001a6000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f3220720000-7f3220724000 rw-p 00000000 00:00 0 7f3220724000-7f3220744000 r-xp 00000000 fd:01 50354459 /usr/lib64/ld-2.18.so 7f3220935000-7f3220938000 rw-p 00000000 00:00 0 7f3220941000-7f3220944000 rw-p 00000000 00:00 0 7f3220944000-7f3220945000 r--p 00020000 fd:01 50354459 /usr/lib64/ld-2.18.so 7f3220945000-7f3220946000 rw-p 00021000 fd:01 50354459 /usr/lib64/ld-2.18.so 7f3220946000-7f3220947000 rw-p 00000000 00:00 0 7ffecf289000-7ffecf2aa000 rw-p 00000000 00:00 0 [stack] 7ffecf38c000-7ffecf38f000 r--p 00000000 00:00 0 [vvar] 7ffecf38f000-7ffecf391000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] 首先，让我们来理解一下这个文件的格式，/proc/$PID/maps的每一行描述了进程中一段连续的虚拟内存区域\n 第一列 address 是一个地址范围，描述了该区域在进程地址空间中的起始和结束地址 第二列 pem 是一个访问权限设置，其中 s 表示私有或共享页面。如果一个进程试图访问不允许的内存，就会发生分段错误（segmentation fault） 第三列 offset 是一个偏移值，与 mmap 有关，如果该区域是通过使用 mmap 映射到文件的，那么这个偏移量就是文件中映射开始的偏移量。 第四列 dev 是一个设备描述符，如果该区域是从文件映射的，那么这是文件所在位置的主设备和次设备号（十六进制），主设备号指向设备驱动程序，次设备号由设备驱动程序解释，或者对于特定设备驱动程序来说是特定设备，例如多个软盘驱动器。 第五列 inode 是一个文件编号，如果该区域是从文件映射的，那么这是文件编号。 第六列 pathname 是一个文件路径，如果该区域是从文件映射的，那么这是文件的名称。有特殊区域的名称如[heap]，[stack]和[vdso]，[vdso]代表虚拟动态共享对象，其被系统调用用来切换到内核模式。  仔细观察上面的输出可以发现，有部分行似乎在 pathname 列上没有任何值，这些区域被称为匿名区域。匿名区域是通过 mmap 创建的，但不附加到任何文件，它们用于各种不同的用途，比如共享内存、不在堆上的缓冲区，以及 pthread 库把匿名映射区域用作新线程的堆栈。\n如果你多次运行这个程序时，观察/proc/$PID/maps可以发现，部分行的 address 区域每次都会有不同的地址。这意味着对于某些内存区域，地址不是静态分配的。这实际上是由于一种安全特性，通过随机化某些区域的地址空间，使攻击者更难以获取他们感兴趣的特定内存块。但是，有些区域始终是固定的，因为你需要它们是固定的，这样你才能让内核知道如何加载程序。\n可以查看一下这一行输出，每次执行都是一样的，\nffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] ffffffffff600000-ffffffffff601000这段内存总是与 vsyscall 绑定。\n实际上，还有一种被称为 PIE(位置无关的可执行文件）的可执行文件。PIE 会使程序数据和可执行内存也随机化。有兴趣的可以自行 google。\n为什么内存不是从地址 0x00 开始 让我们回到 cat /proc/4070/maps 这个命令的输出，先关注一下前三行\n00400000-00401000 r-xp 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 00600000-00601000 r--p 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 00601000-00602000 rw-p 00001000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 你有没有发现一件奇怪的事，第一行的内存地址是从00400000-00401000开始的，那再往前的内存0-00400000呢？\n#include \u0026lt;stdio.h\u0026gt; int main () { void * addr = (void *) 0x0; printf(\u0026#34;0x%x\\n\u0026#34;, ((char *) addr)[0]); // prints 0x0  printf(\u0026#34;0x%x\\n\u0026#34;, ((char *) addr)[1]); // prints 0x1  printf(\u0026#34;0x%x\\n\u0026#34;, ((char *) addr)[2]); // prints 0x2 } 我们用一个简单的代码去访问一下进程的 0x0, 0x1, 0x0，毫无意外的，程序会Segmentation fault (core dumped)，\n为什么会有这约 4MiB 的间隙？为什么不是从 0 地址开始分配内存呢？\n参考这里的讨论，https://stackoverflow.com/questions/14314021/why-linux-gnu-linker-chose-address-0x400000\n其实这个问题很简单，这个间隙的存在主要是由 malloc 和链接器实现者的任意选择造成的。他们在实现的时候，对于 64 位 ELF 可执行文件，非 PIE（位置无关可执行文件）的入口点应该位于 0x400000；而对于 32 位 ELF 可执行文件，入口点则位于 0x08048000。如果你生成了一个位置无关的可执行文件，起始地址则会变为 0x0。\n仅此而已，没有什么特殊的理由了，某一天当你成为了编译器的实现者，你可以将内存的起始位置放到 0 地址 :)\n代码段 我们来看 maps 文件的第一行\n00400000-00401000 r-xp 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 这一行所描述的区域就是代码区；该区存储了程序的二进制代码。这是我们编译后的程序的主要部分。执行程序时，CPU 从这个区域读取指令（即 EIP 寄存器总是指向这个区域的某一个命令）。\nELF 格式及程序的元信息 那么问题来了，既然我们编写的代码经过编译链接后都存在了代码段里，那么 CPU 怎么知道要从这个区域的哪里开始读取并执行指令呢？\n这就要提到一个叫 ELF 的玩意了；ELF (Executable and Linkable Format) 是一种用于表示可执行文件、目标代码、共享库和核心转储的标准文件格式，常见于 Unix 和 Unix-like 的操作系统中。ELF 格式会规定一些文件头，这些文件头里会存放着程序的元信息，诸如程序入口点，魔数，版本，机器等信息。\n这里我们凭借一个叫 readelf 的工具，来读取一下我们的./main文件，看看这个文件里的都记录了哪些元信息。\n readelf 是一个用于读取和显示 ELF (Executable and Linkable Format) 文件的工具。\n [root@lambertx memory_layout]# readelf --file-header ./main ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x4005a0 Start of program headers: 64 (bytes into file) Start of section headers: 6712 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 31 Section header string table index: 30 通过输出我们可以观察到 main 这个文件的元信息，注意到有一行 Entry point address: 0x4005a0，这就是程序的入口位置，CPU 也是从这个位置开始执行我们的代码的。\nreadelf 工具的原理很简单，就是从可执行文件的代码段起始地址 0x400000 读取了两个结构体，感兴趣的可以看下面的代码模拟实现\n#include \u0026lt;stdio.h\u0026gt;#include \u0026lt;stdint.h\u0026gt; // from: http://rpm5.org/docs/api/readelf_8h-source.html  typedef uint64_t Elf64_Addr; typedef uint64_t Elf64_Off; typedef uint64_t Elf64_Xword; typedef uint32_t Elf64_Word; typedef uint16_t Elf64_Half; typedef uint8_t Elf64_Char; #define EI_NIDENT 16  // this struct is exactly 64 bytes // this means it goes from 0x400000 - 0x400040 typedef struct { Elf64_Char e_ident[EI_NIDENT]; // 16 B  Elf64_Half e_type; // 2 B  Elf64_Half e_machine; // 2 B  Elf64_Word e_version; // 4 B  Elf64_Addr e_entry; // 8 B  Elf64_Off e_phoff; // 8 B  Elf64_Off e_shoff; // 8 B  Elf64_Word e_flags; // 4 B  Elf64_Half e_ehsize; // 2 B  Elf64_Half e_phentsize; // 2 B  Elf64_Half e_phnum; // 2 B  Elf64_Half e_shentsize; // 2 B  Elf64_Half e_shnum; // 2 B  Elf64_Half e_shstrndx; // 2 B } Elf64_Ehdr; // this struct is exactly 56 bytes // this means it goes from 0x400040 - 0x400078 typedef struct { Elf64_Word p_type; // 4 B  Elf64_Word p_flags; // 4 B  Elf64_Off p_offset; // 8 B  Elf64_Addr p_vaddr; // 8 B  Elf64_Addr p_paddr; // 8 B  Elf64_Xword p_filesz; // 8 B  Elf64_Xword p_memsz; // 8 B  Elf64_Xword p_align; // 8 B } Elf64_Phdr; int main(int argc, char *argv[]){ // from examination of objdump and /proc/ID/maps, we can see that this is the first thing loaded into memory  // earliest in the virtual memory address space, for a 64 bit ELF executable  // %lx is required for 64 bit hex, while %x is just for 32 bit hex  Elf64_Ehdr * ehdr_addr = (Elf64_Ehdr *) 0x400000; printf(\u0026#34;Magic: 0x\u0026#34;); for (unsigned int i = 0; i \u0026lt; EI_NIDENT; ++i) { printf(\u0026#34;%x\u0026#34;, ehdr_addr-\u0026gt;e_ident[i]); } printf(\u0026#34;\\n\u0026#34;); printf(\u0026#34;Type: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_type); printf(\u0026#34;Machine: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_machine); printf(\u0026#34;Version: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_version); printf(\u0026#34;Entry: %p\\n\u0026#34;, (void *) ehdr_addr-\u0026gt;e_entry); printf(\u0026#34;Phdr Offset: 0x%lx\\n\u0026#34;, ehdr_addr-\u0026gt;e_phoff); printf(\u0026#34;Section Offset: 0x%lx\\n\u0026#34;, ehdr_addr-\u0026gt;e_shoff); printf(\u0026#34;Flags: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_flags); printf(\u0026#34;ELF Header Size: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_ehsize); printf(\u0026#34;Phdr Header Size: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_phentsize); printf(\u0026#34;Phdr Entry Count: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_phnum); printf(\u0026#34;Section Header Size: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_shentsize); printf(\u0026#34;Section Header Count: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_shnum); printf(\u0026#34;Section Header Table Index: 0x%x\\n\u0026#34;, ehdr_addr-\u0026gt;e_shstrndx); Elf64_Phdr * phdr_addr = (Elf64_Phdr *) 0x400040; printf(\u0026#34;Type: %u\\n\u0026#34;, phdr_addr-\u0026gt;p_type); // 6 - PT_PHDR - segment type  printf(\u0026#34;Flags: %u\\n\u0026#34;, phdr_addr-\u0026gt;p_flags); // 5 - PF_R + PF_X - r-x permissions equal to chmod binary 101  printf(\u0026#34;Offset: 0x%lx\\n\u0026#34;, phdr_addr-\u0026gt;p_offset); // 0x40 - byte offset from the beginning of the file at which the first segment is located  printf(\u0026#34;Program Virtual Address: %p\\n\u0026#34;, (void *) phdr_addr-\u0026gt;p_vaddr); // 0x400040 - virtual address at which the first segment is located in memory  printf(\u0026#34;Program Physical Address: %p\\n\u0026#34;, (void *) phdr_addr-\u0026gt;p_paddr); // 0x400040 - physical address at which the first segment is located in memory (irrelevant on Linux)  printf(\u0026#34;Loaded file size: 0x%lx\\n\u0026#34;, phdr_addr-\u0026gt;p_filesz); // 504 - bytes loaded from the file for the PHDR  printf(\u0026#34;Loaded mem size: 0x%lx\\n\u0026#34;, phdr_addr-\u0026gt;p_memsz); // 504 - bytes loaded into memory for the PHDR  printf(\u0026#34;Alignment: %lu\\n\u0026#34;, phdr_addr-\u0026gt;p_align); // 8 - alignment using modular arithmetic (mod p_vaddr palign) === (mod p_offset p_align)  return 0; } 总结一下，从 0x400000 开始，它包含了所有 ELF 可执行文件头，这些头告诉操作系统如何使用这个程序，以及一些元数据信息（魔数，程序的入口点等）。具体请参阅：http://www.ouah.org/RevEng/x430.htm\nobjdump 好，现在我们通过 maps 文件知道了地址 00400000-00401000 存放的是我们编译后的代码，那么有没有什么工具可以让我们看一下这段代码里做了啥事吗？\n我们可以用 objdump 工具\n objdump 是一款常用的二进制文件分析工具，它可以从可执行文件或目标文件中提取出各种信息，如汇编代码、符号表、重定位表、段信息等。objdump 可以反汇编二进制文件，显示二进制指令，符号表，调试信息等，为程序员和系统开发者提供了深入分析和调试的能力。\n 执行命令\nobjdump --disassemble-all --start-address=0x400000 --stop-address=0x401000 main 会得到类似的输出（以下输出直截取了部分）\n[root@lambertx memory_layout]# objdump --disassemble-all --start-address=0x400000 --stop-address=0x401000 main main: file format elf64-x86-64 Disassembly of section .interp: 0000000000400238 \u0026lt;.interp\u0026gt;: 400238:\t2f (bad) 400239:\t6c insb (%dx),%es:(%rdi) 40023a:\t69 62 36 34 2f 6c 64 imul $0x646c2f34,0x36(%rdx),%esp 400241:\t2d 6c 69 6e 75 sub $0x756e696c,%eax 400246:\t78 2d js 400275 \u0026lt;_init-0x273\u0026gt; 400248:\t78 38 js 400282 \u0026lt;_init-0x266\u0026gt; 40024a:\t36 2d 36 34 2e 73 ss sub $0x732e3436,%eax 400250:\t6f outsl %ds:(%rsi),(%dx) 400251:\t2e 32 00 ... 事实上，我们会看到一段反汇编出来的代码，注意到代码的位置是从地址 0000000000400238 开始，而并非从 0x400000开始的，为啥呢？还记得前面讲过的 ELF 吗，其实是0x400000-0x400238之间存的是 ELF 的元数据，而非程序的代码。\n数据段 回到 maps 文件的第二行和第三行，其中第二行描述了 Data 段的信息，第三行描述了 BSS 段的信息。\n00600000-00601000 r--p 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 00601000-00602000 rw-p 00001000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main Data 段存储的是初始化的全局变量和静态变量。在程序开始执行之前，操作系统会为这些变量分配内存，并将它们的值设置为程序中的初始值。例如，static char * foo = \u0026ldquo;bar\u0026rdquo;; 就会在这个段中分配内存，字符串 \u0026ldquo;bar\u0026rdquo; 会被存储在这个内存区域。\nBSS 段存储的是未初始化的全局变量和静态变量。在程序开始执行之前，操作系统也会为这些变量分配内存，但它们会被填充为 0。例如，static char * username; 就会在这个段中分配内存，但默认值会是 0。\n我们用同样用 objdump 来看看，这两行的内容，执行命令\nobjdump --disassemble-all --start-address=0x600000 --stop-address=0x602000 main 会得到类似的输出（同样省略了部分输出）\n[root@lambertx memory_layout]# objdump --disassemble-all --start-address=0x600000 --stop-address=0x601000 main main: file format elf64-x86-64 Disassembly of section .init_array: 0000000000600e10 \u0026lt;__frame_dummy_init_array_entry\u0026gt;: 600e10:\t60 (bad) 600e11:\t06 (bad) 600e12:\t40 00 00 add %al,(%rax) 600e15:\t00 00 add %al,(%rax) ... Disassembly of section .fini_array: 0000000000600e18 \u0026lt;__do_global_dtors_aux_fini_array_entry\u0026gt;: 600e18:\t40 06 rex (bad) 600e1a:\t40 00 00 add %al,(%rax) 600e1d:\t00 00 add %al,(%rax) ... Disassembly of section .jcr: 0000000000600e20 \u0026lt;__JCR_END__\u0026gt;: ... Disassembly of section .dynamic: ...(此处忽略大段输出) Disassembly of section .got: 0000000000600ff8 \u0026lt;.got\u0026gt;: ... Disassembly of section .got.plt: ...(此处忽略大段输出) Disassembly of section .data: 0000000000601050 \u0026lt;__data_start\u0026gt;: 601050:\t00 00 add %al,(%rax) ... Disassembly of section .bss: 0000000000601054 \u0026lt;__bss_start\u0026gt;: 601054:\t00 00 add %al,(%rax) 可以发现，实际上 data 段实际是从0x601050开始的，然后 bss 段是从0x601054开始的，并且 data 段前面还包含了许多我们暂时还不认识的段。所有的段依次是是\n .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss  从名称上看，除了.data 和.bss 外的段，应该是跟初始化、析构、动态链接相关，有机会再开一篇文章独立讲讲。\n 我们发现这三个段目前通过 address 范围算出来的大小都是 4KB，为什么呢？其实是由于我们现在的程序还太过于简单（没有静态变量也没有全局变量），而 4KB 是 linux 上默认的内存 page 的大小，因此这三个段此时的大小都是 4KB。\n 堆区 仍然回到 maps 文件的输出\n00400000-00401000 r-xp 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 00600000-00601000 r--p 00000000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main 00601000-00602000 rw-p 00001000 fd:01 51061371 /root/workspace/cpp-test/memory_layout/main // 内存去哪了？ 7f3220378000-7f322051a000 r-xp 00000000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f322051a000-7f322071a000 ---p 001a2000 fd:01 50354460 /usr/lib64/libc-2.18.so ... (省略) 重点看一下第三行的结束地址第四行和开始地址，好家伙，直接从地址 00602000 到了 7f3220378000，中间大概 127TB 的空间，来了一个超级大跳跃，那么中间的这段内存去哪了呢？\n你可以已经猜到了，这么庞大的一段内存其实就是我们平时提到的堆内存，用来存放我们程序里动态分配的内存，由于是动态分配的，那么需要的内存大小就是不可预期的，所以这一段虚拟的范围特别的大；堆内存从低地址往高地址的方向增长。\n还记得我们的进程还停留在 getchar() 上吗，此时的进程还没有向堆区申请过内存，接下来我们在终端上输入一个字符然后按回车，让程序继续往下走并执行 addr = (char *) malloc(1000)\n此时查看 maps 文件会发现，多了一行堆区的描述\n011f0000-01211000 rw-p 00000000 00:00 0 [heap] 此时堆的大小是 132KB ，那么这个时候一定就有小伙伴有疑问了，明明我们分配的只有 100B，为啥实际是 132KB？\n这里先补充一个知识点\n glibc 中的 malloc 通过内部调用 brk 或 mmap 调用来从操作系统获取内存，brk 系统调用通常用于增加堆的大小，而 mmap 将用于加载共享库、为线程创建新区域等其他用途。当请求的内存量大于 MMAP_THRESHOLD（通常默认值是 128KB）时，它实际上会切换到使用 mmap 而不是 brk。\n ok，由于我们 malloc 的大小是 100B，因此实际是通过 brk 的方式申请的内存，而 brk 去申请时是带有一个填充大小（padded size）的，简而言之，就是 brk 每次需要跟系统申请内存的时候都会多申请一点，从而减少系统调用的次数和上下文切换的次数。这些多申请出的内存会在后续的 malloc 调用中被使用。\n空口无凭，我们通过 strace 命令来验证一下当 malloc(1000)时，是不是真的走的是 brk，通过下面的程序\ncheck_brk.c\n#include \u0026lt;stdlib.h\u0026gt; int main () { char * addr = (char *) malloc(1000); free(addr); return 0; } 以及 check_mmap.c\n#include \u0026lt;stdlib.h\u0026gt; int main () { char * addr = (char *) malloc(1024 * 128); free(addr); return 0; } 编译并生成可执行文件后，分别用 strace 命令运行，可以看到 strace 输出的结果确实是符合预期\n简单总结下 brk 和 mmap\n brk 适用于小块内存的分配，所有通过 brk 分配出来的内存在堆上都是连续的。并且回收的时候需要从堆顶开始回收，因此不灵活。 mmap 适用于较大内存的分配，分配出来的内存会落在内存映射区，也不要求内存是连续分配的且可以独立回收，因此它更灵活。  内存映射区 在堆下面，就是内存映射区，除了上一节里提到的 mmap 方式申请的内存会落在这个区域外，共享库的内存段和匿名缓冲区也在这里\n7f3220378000-7f322051a000 r-xp 00000000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f322051a000-7f322071a000 ---p 001a2000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f322071a000-7f322071e000 r--p 001a2000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f322071e000-7f3220720000 rw-p 001a6000 fd:01 50354460 /usr/lib64/libc-2.18.so 7f3220720000-7f3220724000 rw-p 00000000 00:00 0 7f3220724000-7f3220744000 r-xp 00000000 fd:01 50354459 /usr/lib64/ld-2.18.so 7f3220935000-7f3220938000 rw-p 00000000 00:00 0 7f3220941000-7f3220944000 rw-p 00000000 00:00 0 7f3220944000-7f3220945000 r--p 00020000 fd:01 50354459 /usr/lib64/ld-2.18.so 7f3220945000-7f3220946000 rw-p 00021000 fd:01 50354459 /usr/lib64/ld-2.18.so 7f3220946000-7f3220947000 rw-p 00000000 00:00 0 当我们的程序需要依赖一些动态链接库，这些库就会在程序启动时被加载到这个内存映射区。\n作为一个 C 程序员，你一定也用过 ldd 命令吧，事实上，ldd 命令就是通过读取 maps 文件，从而拿到一个可执行文件所依赖的外部库的信息的。\n举个例子\n[root@lambertx memory_layout]# ldd main linux-vdso.so.1 (0x00007ffdde795000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007f8bf795a000) /lib64/ld-linux-x86-64.so.2 (0x00007f8bf7d06000) [root@lambertx memory_layout]# [root@lambertx memory_layout]# ldd main linux-vdso.so.1 (0x00007ffff25e3000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007fcb49bd3000) /lib64/ld-linux-x86-64.so.2 (0x00007fcb49f7f000) [root@lambertx memory_layout]# [root@lambertx memory_layout]# ldd main linux-vdso.so.1 (0x00007ffcf3fd1000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007f44ea0f7000) /lib64/ld-linux-x86-64.so.2 (0x00007f44ea4a3000) 上面执行了三次 ldd 命令，ldd 会将 main 这个可执行文件的依赖库输出，同时我们注意到每次输出结果里，依赖库的被加载到的内存位置都是不一样的，这其实是 linux 的一个安全机制。同样的，当你多次运行同一个程序，查看/proc/$PID/maps 也会看到不同的地址。\n栈区 7ffecf289000-7ffecf2aa000 rw-p 00000000 00:00 0 [stack] 这段空间就是平常我们说的栈区里，当我们程序运行的时候，局部变量，函数参数，函数返回值，函数返回地址等数据就会存放在这块区域；数据写入的时候叫做入栈，数据不要了则叫出栈（通过改变 ESP 寄存器的指向）；栈的空间分配方向由高地址往低地址。\n同时也可以看出，这里栈空间的大小是 132KB，在程序的运行错误里有一个很经典的错误叫 stackoverflow，就是指的这个栈区写满了。\n最后的区域 7ffecf38c000-7ffecf38f000 r--p 00000000 00:00 0 [vvar] 7ffecf38f000-7ffecf391000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] 关于 vvar，vdso 和 vsyscall，这三个玩意可以归于为了提升系统调用性能而提出来的奇巧淫技了；vsyscall 出现得最早，比如读取时间 gettimeofday ，内核会把时间数据和 gettimeofday 的实现映射到这块区域，用户空间可以直接调用，不需要从用户空间切换到内核空间。 但是 vsyscall 区域太小了，而且映射区域固定，有安全问题。 后来又造出了 vdso，之所以 vsyscall 保留是为了兼容已有程序。 vdso 相当于加载一个 linux-vd.so 库文件一样，也就是把一些函数实现映射到这个区域，而 vvar 也就是存放数据的地方了，那么用户可以通过调用 vdso 里的函数，使用 vvar 里的数据，来获得自己想要的信息。\n总结 经过上面的分析，我们可以得到更新过后的内存布局示意图了\n本文所有的分析都是基于 centos7 系统，以及默认的 gcc 编译配置，因此如果你经过相同的实验并发现结果和本文的结果不同，也是合理的。\n参考文档  https://github.com/torvalds/linux/blob/v6.0/Documentation/x86/x86_64/mm.rst https://gist.github.com/CMCDragonkai/10ab53654b2aa6ce55c11cfc5b2432a4#understanding-the-memory-layout-of-linux-executables https://cboard.cprogramming.com/linux-programming/101090-what-differences-between-brk-mmap.html  ","permalink":"https://lambertxiao.github.io/posts/%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%86%85%E5%AD%98%E5%B8%83%E5%B1%80/doc/","summary":"今天来探讨一下 linux 环境下进程的内存布局","title":"进程的内存是怎么布局的"},{"content":"工作中用到了，由于需要签入指定的域名，折腾了一番，写篇文章记录一下\n生成证书的配置文件 创建openssl.conf，填入下面内容\n[req] distinguished_name = req_distinguished_name req_extensions = v3_req [req_distinguished_name] countryName = country stateOrProvinceName = province localityName = city organizationName = company name commonName = domain name or ip [v3_req] subjectAltName = @alt_names [alt_names] DNS.1=test.com DNS.2=www.test.com 生成私钥文件 openssl genrsa -out test.key 2048 生成证书的request文件 openssl req -new -key test.key -out test.csr -config openssl.conf -subj '/C=CN/ST=BeiJing/L=BeiJing/O=test.com/OU=test/CN=test/emailAddress=test@qq.com' 查看生成的request文件 openssl req -in test.csr -text -noout 生成证书文件 openssl x509 -req -days 3650 -sha1 -in test.csr -signkey test.key -out test.crt -CAcreateserial -extensions v3_req -extfile ./openssl.conf 查看生成的证书 openssl x509 -in test.crt -text -noout ","permalink":"https://lambertxiao.github.io/posts/openssl%E7%94%9F%E6%88%90%E8%87%AA%E7%AD%BE%E5%90%8D%E8%AF%81%E4%B9%A6/doc/","summary":"工作中用到了，由于需要签入指定的域名，折腾了一番，写篇文章记录一下\n生成证书的配置文件 创建openssl.conf，填入下面内容\n[req] distinguished_name = req_distinguished_name req_extensions = v3_req [req_distinguished_name] countryName = country stateOrProvinceName = province localityName = city organizationName = company name commonName = domain name or ip [v3_req] subjectAltName = @alt_names [alt_names] DNS.1=test.com DNS.2=www.test.com 生成私钥文件 openssl genrsa -out test.key 2048 生成证书的request文件 openssl req -new -key test.key -out test.csr -config openssl.conf -subj '/C=CN/ST=BeiJing/L=BeiJing/O=test.com/OU=test/CN=test/emailAddress=test@qq.com' 查看生成的request文件 openssl req -in test.csr -text -noout 生成证书文件 openssl x509 -req -days 3650 -sha1 -in test.","title":"openssl生成自签名证书"},{"content":"前言 最近研究c++协程库libco的时候发现，它内部大量运用了dlsym技术，对如connect, sendto, recv, read, write等系统调用做了hook, 从而使得libco能在单线程的情况下调度协程。出与对dlsym技术的学习总结，出了这篇博文\ndlsym概述 dlsym是动态链接库中的一个关键函数，可以通过符号名（Symbol）获取函数指针。定义如下：\nvoid *dlsym(void *handle, const char *symbol); 其中，handle表示动态链接库的句柄，symbol表示需要查找的符号名。dlsym返回符号名对应的函数指针，如果未找到符号，则返回NULL。\ndlsym使用方法 dlsym + dlopen 先准备一个libfoo.so\n// foo.h extern \u0026#34;C\u0026#34; void foo(); // foo.cpp #include \u0026lt;stdio.h\u0026gt;#include \u0026#34;foo.h\u0026#34; void foo() { printf(\u0026#34;you are foo\u0026#34;); } 编译动态链接库\ng++ -fPIC -shared -o libfoo.so foo.cpp -ldl // main.cpp #include \u0026lt;dlfcn.h\u0026gt;#include \u0026lt;stdio.h\u0026gt;#include \u0026#34;foo.h\u0026#34; typedef void* (*foo_func)(); int main() { void* handle = dlopen(\u0026#34;./libfoo\u0026#34;, RTLD_LAZY); foo_func foo = (foo_func)dlsym(handle, \u0026#34;foo\u0026#34;); char* err = dlerror(); if (err) { printf(\u0026#34;%s\\n\u0026#34;, err); return 0; } foo(); } 执行命令，可以看到我们通过dlsym这种方式调用到了libfoo.so中的foo函数\ng++ -std=c++11 -o main main.cpp LD_PRELOAD=./libfoo.so ./main dlsym + RTLD_DEFAULT 一般而言，symbol的查找是有先后顺序的，并且可能多个动态链接库里有同一个symbol名称； 比如echo这个symbol, 可能在liba.so里有定义，在libc.so里也有定义。对于这种情况，我们可以使用RTLD_DEFAULT。 RTLD_DEFAULT是个特殊的宏，表示在查找symbol的时候，返回第一个查找到的symbol的地址，找到了就不需往别的动态链接库里查找了。\n#include \u0026lt;stdio.h\u0026gt;#include \u0026lt;dlfcn.h\u0026gt; typedef void (*printf_func_ptr)(const char *, ...); int main() { printf_func_ptr printf_ptr = (printf_func_ptr)dlsym(RTLD_DEFAULT, \u0026#34;printf\u0026#34;); printf_ptr(\u0026#34;Hello, world!\\n\u0026#34;); return 0; } 执行后可以发现，我们通过dlsym调用到了系统库里的printf函数\ndlsym + RTLD_NEXT + LD_PRELOAD 与RTLD_DEFAULT不同的点在于，RTLD_NEXT表示查找symbol的时候，跳过所在的动态链接库，继续去别的lib中找到symbol的位置，找到了就返回。\n这有什么用呢？通过LD_PRELOAD环境变量，可以用来对某些系统调用做hook。看下面例子\n// hook.cpp  #include \u0026lt;stdio.h\u0026gt;#include \u0026lt;stdint.h\u0026gt;#include \u0026lt;dlfcn.h\u0026gt; #define ENABLE_HOOK_FUNC(name) \\ if (!g_sys_##name) { \\ g_sys_##name = (sys_##name##_t)dlsym(RTLD_NEXT, #name); \\ }  typedef void* (*sys_malloc_t)(size_t size); static sys_malloc_t g_sys_malloc = NULL; extern \u0026#34;C\u0026#34; { void* malloc(size_t size) { ENABLE_HOOK_FUNC(malloc); printf(\u0026#34;invoke malloc hook function\\n\u0026#34;); void *p = g_sys_malloc(size); return p; } 编译动态链接库\ng++ -fPIC -shared -o libhook.so hook.cpp -ldl  注意点，由于hook.cpp是c++文件，因此必须在需要做hook的函数外面写上extern \u0026quot;C\u0026quot;, 否则实际生成的so的方法名不一定是malloc\n // main.cpp  #include \u0026lt;memory\u0026gt; int main() { printf(\u0026#34;before malloc\\n\u0026#34;); char* buf = (char*)malloc(sizeof(char)); if (!buf) { printf(\u0026#34;malloc error\\n\u0026#34;); } printf(\u0026#34;after malloc\\n\u0026#34;); return 0; } 通过LD_PRELOAD即可实现对malloc函数的hook\ng++ -std=c++11 -o main main.cpp LD_PRELOAD=./libhook.so ./main 执行后可以发现，我们通过LD_PRELOAD前置了一个动态链接库，成功地实现了对系统库里的malloc函数进行hook\ndlsym + RTLD_NEXT 在实际使用libco的过程中，并没有发现需要使用LD_PRELOAD，那有没有办法不需要用LD_PRELOAD也可以hook掉系统函数呢，研究发现，可以用如下方式来实现\n先hook掉malloc函数\n// hook.cpp #include \u0026lt;stdio.h\u0026gt;#include \u0026lt;stdint.h\u0026gt;#include \u0026lt;dlfcn.h\u0026gt;#include \u0026lt;memory.h\u0026gt; #define ENABLE_HOOK_FUNC(name) \\ if (!g_sys_##name) { \\ g_sys_##name = (sys_##name##_t)dlsym(RTLD_NEXT, #name); \\ }  typedef void* (*sys_malloc_t)(size_t size); static sys_malloc_t g_sys_malloc = NULL; extern \u0026#34;C\u0026#34; { void* malloc(size_t size) { ENABLE_HOOK_FUNC(malloc); printf(\u0026#34;invoke malloc hook function\\n\u0026#34;); char* p = (char*)g_sys_malloc(size); memset(p, \u0026#39;0\u0026#39;, sizeof(char)*size); return p; } } 将hook.cpp编译成动态库\ng++ -fPIC -shared -o libhook.so hook.cpp -ldl // main.cpp #include \u0026lt;memory\u0026gt; int main() { printf(\u0026#34;before malloc\\n\u0026#34;); char* buf = (char*)malloc(sizeof(char)*24); if (!buf) { printf(\u0026#34;malloc error\\n\u0026#34;); } printf(\u0026#34;after malloc\\n\u0026#34;); printf(\u0026#34;%s\\n\u0026#34;, buf); return 0; } 在编译main.cpp的时候，带上该动态库\ng++ -std=c++11 -o main main.cpp -L /root/workspace/dlsym-demo/ -lhook 直接执行./main，可以发现hook生效了，malloc出来的内存的内容都被设置'0'\nbefore malloc invoke malloc hook function after malloc 000000000000000000000000� ","permalink":"https://lambertxiao.github.io/posts/linux-dlsym/doc/","summary":"dlsym(dynamic link symbol)","title":"Linux-dlsym"},{"content":"简介 pkg-config 是一种用于获取已安装软件包编译选项的工具。它的主要作用是为那些使用 Autoconf 或 Automake 等工具进行软件包管理的开发者提供便利。\npkg-config 支持查询第三方软件包的头文件目录（包含文件夹）、库文件路径和编译选项等信息，以便在编译时链接这些库文件，从而使代码能够顺利地编译并运行。\n安装 pkg-config 是一个开源软件包，可以在 Linux 或 macOS 系统上通过包管理器直接安装：\nsudo apt-get install pkg-config or sudo yum install pkg-config or brew install pkg-config 使用 pkg-config 主要通过一个.pc的文本文件提供编译选项、头文件和库文件等信息。\n当安装一个软件包时，通常也会同时安装一个对应的 .pc 文件。例如，安装了 libcurl-dev 包，就会同时安装一个 curl.pc 文件，这个文件中包含了 libcurl 的头文件路径、库文件路径以及选项等信息。\n 需要注意的是，并不是什么软件包都会自带一个.pc文件的，本质上.pc文件只是个文本文件，我们可以自行在机器上为对应的库添加对应的.pc文件\n 下面以liburing.pc文件作为示例\nprefix=/usr exec_prefix=${prefix} libdir=/usr/lib includedir=/usr/include Name: liburing Version: 2.4 Description: io_uring library URL: https://git.kernel.dk/cgit/liburing/ Libs: -L${libdir} -luring Cflags: -I${includedir} 我们可以使用 pkg-config 工具来查询已安装软件包的信息，其中最常用的命令是\n pkg-config --libs 查询头文件目录 pkg-config --cflags 查询库文件路径和链接选项  举些栗子 下面是一些常用的 pkg-config 命令：\n查询已安装所有软件包的详细信息：\npkg-config --list-all 查询 libcurl 库的头文件目录：\npkg-config --cflags libcurl 查询 libcurl 库的库文件路径和链接选项：\npkg-config --libs libcurl 注意点 在某些情况下，你会发现pkg-config找不到你所指定的库，可能是由于你安装库的路径不是在pkg-config默认的搜索路径上，可通过PKG_CONFIG_PATH环境变量设置搜索路径\n常见搜索路径有/usr/lib/pkgconfig 和 /usr/share/pkgconfig 和 /usr/local/lib/pkgconfig\nexport PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/local/lib/pkgconfig  如果同一软件包在不同的 .pc 文件中定义了相同的编译选项，那么最后查询结果会以搜索顺序最靠前的 .pc 文件中定义的选项为准\n ","permalink":"https://lambertxiao.github.io/posts/%E5%B7%A5%E5%85%B7%E4%BB%8B%E7%BB%8D-pkg-config/doc/","summary":"pkg-config 是一种用于获取已安装软件包编译选项的工具","title":"怎么使用pkg-config"},{"content":"简介 mmap(memory map) 是一种将磁盘上的文件映射到内存中的方法，它可以帮助程序更高效地访问磁盘文件。\n原理  参见：https://www.man7.org/linux/man-pages/man2/mmap.2.html\n #include \u0026lt;sys/mman.h\u0026gt; void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length); mmap()函数会创建一个映射，将fd对应的文件内容映射到addr位置。如果 addr 为 NULL，则内核选择与页面对齐的地址创建映射。如果 addr 不为 NULL，内核会选择一个就近的位置进行映射 (但总是高于或等于 /proc/sys/vm/mmap_min_addr 指定的值), 并尝试在该位置创建映射。如果已存在另一个映射，则内核选择一个新地址。映射的地址作为调用的结果返回。在 mmap() 调用返回后，可以立即关闭文件描述符 fd，而不会破坏映射。\nprot参数用户描述了映射所需的内存保护 (且必须不与其他文件的打开模式冲突)。\n PROT_EXEC Pages may be executed. PROT_READ Pages may be read. PROT_WRITE Pages may be written. PROT_NONE Pages may not be accessed.  flags参数常见的有这2种：\n MAP_SHARED 创建一个共享的映射，在该映射上的更新能被其他进程感知到 MAP_PRIVATE 创建一个私有的，copy-on-write的映射，在该映射上的改动对于其他进程不可见  示例代码 #include \u0026lt;stdio.h\u0026gt;#include \u0026lt;fcntl.h\u0026gt;#include \u0026lt;unistd.h\u0026gt;#include \u0026lt;sys/mman.h\u0026gt; #define PAGE_SIZE 4096  int main() { // 打开磁盘文件  int fd = open(\u0026#34;test.txt\u0026#34;, O_RDWR); if (fd \u0026lt; 0) { perror(\u0026#34;open\u0026#34;); return -1; } // 映射磁盘文件到内存中  char* addr = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { perror(\u0026#34;mmap\u0026#34;); close(fd); return -1; } printf(\u0026#34;磁盘文件内容：%s\\n\u0026#34;, addr); *(addr + 5) = \u0026#39;a\u0026#39;; int ret = munmap(addr, PAGE_SIZE); if (ret == -1) { perror(\u0026#34;munmap\u0026#34;); } close(fd); return 0; } 首先我们准备一个test.txt，在open完文件后，将文件内容通过mmap映射到内存中，然后在内存中进行修改，使用mumap退出映射，查看文件的内容。\n","permalink":"https://lambertxiao.github.io/posts/%E9%9B%B6%E6%8B%B7%E8%B4%9D-mmap/doc/","summary":"读写文件新姿势","title":"如何使用mmap读写文件"},{"content":"gtest 简单指北  源码地址: https://github.com/lambertxiao/gtest-lesson\n 准备工作 下载googletest库，并编译安装\n googletest源码地址：https://github.com/google/googletest googletest文档地址：https://google.github.io/googletest/\n 怎么运行当前示例 mkdir build cd build cmake ../ \u0026amp;\u0026amp; make ./test_main 示例讲解 test1.cpp 演示了如何做最基本的单元测试\n#include \u0026lt;gtest/gtest.h\u0026gt; int add(int x, int y) { return x + y; } TEST(AdditionTest, testAdd) { EXPECT_EQ(add(1, 2), 3); } test2.cpp 演示了如何写一个测试用例集\n#include \u0026lt;gtest/gtest.h\u0026gt; class Math { public: int add(int x, int y) { return x + y; } int sub(int x, int y) { return x - y; } }; class MathTestSuite : public testing::Test { protected: Math math; }; TEST_F(MathTestSuite, testAdd) { EXPECT_EQ(math.add(1, 2), 3); } TEST_F(MathTestSuite, testSub) { EXPECT_EQ(math.sub(3, 1), 2); } test3.cpp 演示了如何做mock测试，定义了一个简单的常见,FileWriter需要依赖DiskUtil做文件写入，而通过mock掉DiskUtil的写入返回值，从而决定FileWriter应该执行什么逻辑\n#include \u0026lt;gtest/gtest.h\u0026gt;#include \u0026lt;gmock/gmock.h\u0026gt; using ::testing::Return; using ::testing::StrictMock; class IDiskUitl { public: virtual int write(int offset, char* data, int len) = 0; }; class FileWriter { public: IDiskUitl* diskutil_; uint64_t offset_; FileWriter(IDiskUitl *diskutil, uint64_t offset) : diskutil_(diskutil), offset_(offset) {} int writeFile(char* data, int len) { int ret = diskutil_-\u0026gt;write(offset_, data, len); if (ret == 0) { offset_ += len; } return ret; } }; class MockDiskUtil : public IDiskUitl { public: MOCK_METHOD(int, write, (int, char*, int), (override)); }; class FileWriterTestSuite : public testing::Test { protected: // 每个case执行前都会调用  void SetUp() override {} StrictMock\u0026lt;MockDiskUtil\u0026gt; diskutil_; }; TEST_F(FileWriterTestSuite, testWriteSuccess) { uint64_t woff = 1024; FileWriter w(\u0026amp;diskutil_, woff); std::string data = \u0026#34;hello\u0026#34;; EXPECT_CALL(diskutil_, write(woff, (char*)data.c_str(), data.length())).WillOnce(Return(0)); int ret = w.writeFile((char*)data.c_str(), data.length()); EXPECT_EQ(ret, 0); EXPECT_EQ(w.offset_, woff + data.length()); } TEST_F(FileWriterTestSuite, testWriteFailed) { uint64_t woff = 1024; FileWriter w(\u0026amp;diskutil_, woff); std::string data = \u0026#34;hello\u0026#34;; EXPECT_CALL(diskutil_, write(woff, (char*)data.c_str(), data.length())).WillOnce(Return(1)); int ret = w.writeFile((char*)data.c_str(), data.length()); EXPECT_EQ(ret, 1); EXPECT_EQ(w.offset_, woff); } ","permalink":"https://lambertxiao.github.io/posts/gtest-lesson/doc/","summary":"少年，你懂测试驱动开发吗？","title":"gtest-使用指北"},{"content":"cmake 简单指北  源码地址: https://github.com/lambertxiao/cmake-lesson\n section1 介绍一些cmake的基本命令以及如何使用cmake编译单个cpp文件, 主要包含命令\n cmake_minimum_required 指定cmake的版本 project 指定项目名称 set 设置变量 add_executable 生成可执行文件  section2 介绍如何使用cmake添加一个library，主要包含命令\n add_subdirectory 添加一个目录，cmake会去解析这个目录底下的CMakeLists.txt文件 target_link_libraries 链接依赖库到可执行文件 target_include_directories 将需要include的目录告诉可执行文件  section3 介绍如何在cmake里使用编译参数和条件判断，主要包括命令\n option 设置编译参数 if 条件判断 endif 条件判断 configure_file 设置配置文件  section4 介绍如何链接外部依赖库以及多文件编译方式，主要包括命令\n aux_source_directory 收集指定目录底下的源码文件路径到某个变量 link_directories 指定需要链接的库的所在目录  ","permalink":"https://lambertxiao.github.io/posts/cmake-lesson/doc/","summary":"入坑C/C++前的一座大山","title":"cmake-简单指北"},{"content":"什么是liburing liburing是一个用于异步IO库，它提供了简洁易用的API来处理文件I/O、网络I/O以及事件驱动I/O等各种I/O操作。liburing库基于Linux内核中的io_uring特性实现，将I/O请求从应用层转移到内核层以提高应用程序的I/O性能。\n 由于liburing在内核版本5.1才引入，所以需要运行环境的linux内核版本大于等于5.1\n 为什么使用liburing liburing库对于一些高并发、高吞吐量的程序，特别是网络服务器、云存储等高性能系统的设计和实现有很大的帮助作用。\n如何使用 建议参考源码里的步骤自行编译安装，源码：https://github.com/axboe/liburing\n常用函数介绍：\n io_uring_queue_init：用于初始化io_uring并且返回其句柄。 io_uring_queue_exit：用于关闭并释放io_uring的句柄。 io_uring_get_sqe：用于获取一个可用的sqe，即I/O请求对应的队列元素数据结构。 io_uring_prep_readv：用于准备一个异步读请求。 io_uring_prep_writev：用于准备一个异步写请求。 io_uring_sqe_set_data：用于将用户私有数据关联到一个sqe（请求）中。 io_uring_submit：用于提交一个或一批异步IO请求到io_uring。 io_uring_peek_cqe：用于查看完成队列（cq）中的未处理项数量。 io_uring_wait_cqe：阻塞等待一个处理完成的io。 io_uring_cqe_get_data：用于获取特定的完成队列项（cqe），其中包含先前提交的IO请求的结果以及相关的私有数据。 io_uring_cqe_seen：用于标记一个完成队列项（cqe）已被处理过。  下面使用liburing封装一个DiskUtil实现对文件的基本读写\n编写disk_util.h头文件\n#include \u0026lt;functional\u0026gt;#include \u0026lt;string\u0026gt;#include \u0026lt;liburing.h\u0026gt;#include \u0026lt;thread\u0026gt; enum IO_OP { OP_READ = 0, OP_WRITE = 1 }; struct IORequest { IO_OP opcode; // 0: read, 1: write  char* buffer; off_t offset; size_t length; std::function\u0026lt;void(int)\u0026gt; callback; }; class DiskUtil { public: DiskUtil(const std::string\u0026amp; file_path, int block_size); ~DiskUtil(); void submit_request(IORequest* req); void start(); void stop(); private: void io_worker_thread(); int open_file(); void close_file(); void process_io_request(IORequest* req); int submit_io_request(IORequest* req); void complete_io_request(); private: int fd_ = -1; io_uring io_ring_; const std::string file_path_; const int block_size_; std::thread io_worker_; bool is_running_ = false; }; 编写disk_tool.cpp\n#include \u0026lt;vector\u0026gt;#include \u0026lt;string\u0026gt;#include \u0026lt;fcntl.h\u0026gt;#include \u0026lt;unistd.h\u0026gt;#include \u0026lt;cstring\u0026gt;#include \u0026lt;stdexcept\u0026gt;#include \u0026lt;iostream\u0026gt;#include \u0026lt;liburing.h\u0026gt;#include \u0026#34;disk_util.h\u0026#34; DiskUtil::DiskUtil(const std::string\u0026amp; file_path, int block_size) : file_path_(file_path), block_size_(block_size) { fd_ = open_file(); if (fd_ \u0026lt; 0) { throw std::runtime_error(\u0026#34;Failed to open file\u0026#34;); } int ret = io_uring_queue_init(128, \u0026amp;io_ring_, 0); if (ret \u0026lt; 0) { close(fd_); throw std::runtime_error(\u0026#34;Failed to setup io ring, ret:\u0026#34; + std::to_string(ret)); } is_running_ = true; io_worker_ = std::thread(std::bind(\u0026amp;DiskUtil::io_worker_thread, this)); } DiskUtil::~DiskUtil() { stop(); io_uring_queue_exit(\u0026amp;io_ring_); close_file(); } void DiskUtil::submit_request(IORequest* req) { submit_io_request(req); } void DiskUtil::start() { is_running_ = true; } void DiskUtil::stop() { if (is_running_) { is_running_ = false; if (io_worker_.joinable()) { io_worker_.join(); } } } int DiskUtil::open_file() { int flags = O_RDWR | O_DIRECT | O_CREAT; int mode = S_IRUSR | S_IWUSR; return open(file_path_.c_str(), flags); } void DiskUtil::close_file() { if (fd_ \u0026gt;= 0) { close(fd_); } } int DiskUtil::submit_io_request(IORequest* req) { struct io_uring_sqe* sqe = io_uring_get_sqe(\u0026amp;io_ring_); if (!sqe) { return -1; } struct iovec iov; iov.iov_base = req-\u0026gt;buffer; iov.iov_len = req-\u0026gt;length; if (req-\u0026gt;opcode == OP_READ) { io_uring_prep_readv(sqe, fd_, \u0026amp;iov, 1, req-\u0026gt;offset); } else if (req-\u0026gt;opcode == OP_WRITE) { io_uring_prep_writev(sqe, fd_, \u0026amp;iov, 1, req-\u0026gt;offset); } io_uring_sqe_set_data(sqe, (void*)req); io_uring_submit(\u0026amp;io_ring_); return 0; } void DiskUtil::io_worker_thread() { while (is_running_) { struct io_uring_cqe* cqe = nullptr; int ret = io_uring_wait_cqe(\u0026amp;io_ring_, \u0026amp;cqe); if (ret \u0026lt; 0) { std::cout \u0026lt;\u0026lt; \u0026#34;io_uring_wait_cqe error:\u0026#34; \u0026lt;\u0026lt; ret \u0026lt;\u0026lt; std::endl; return; } std::cout \u0026lt;\u0026lt; \u0026#34;ret:\u0026#34; \u0026lt;\u0026lt; ret \u0026lt;\u0026lt; std::endl; if (cqe-\u0026gt;res \u0026lt; 0) { std::cerr \u0026lt;\u0026lt; \u0026#34;IO error: \u0026#34; \u0026lt;\u0026lt; std::strerror(-cqe-\u0026gt;res) \u0026lt;\u0026lt; \u0026#34; ret_code:\u0026#34; \u0026lt;\u0026lt; cqe-\u0026gt;res \u0026lt;\u0026lt; std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(500)); cqe = nullptr; continue; } else { IORequest* req = (IORequest*)io_uring_cqe_get_data(cqe); if (req-\u0026gt;callback) { req-\u0026gt;callback(0); } } io_uring_cqe_seen(\u0026amp;io_ring_, cqe); } } 下面是调用的例子\n#include \u0026lt;cstring\u0026gt;#include \u0026lt;cstdlib\u0026gt;#include \u0026lt;iostream\u0026gt;#include \u0026#34;disk_util.h\u0026#34; #define BLOCK_SIZE 4096  void test_read(DiskUtil\u0026amp; disk, off_t offset, size_t length) { char* buffer = nullptr; int ret = posix_memalign((void**)\u0026amp;buffer, 4096, length); if (ret \u0026lt; 0) { std::cout \u0026lt;\u0026lt; \u0026#34;malloc buffer error\u0026#34;; return; } IORequest* req = new IORequest; req-\u0026gt;opcode = OP_READ; req-\u0026gt;buffer = buffer; req-\u0026gt;offset = offset; req-\u0026gt;length = length; req-\u0026gt;callback = [req](int ret) { if (ret == 0) { std::cout \u0026lt;\u0026lt; \u0026#34;Read complete, data:\u0026#34; \u0026lt;\u0026lt; req-\u0026gt;buffer \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; \u0026#34;Read failed: \u0026#34; \u0026lt;\u0026lt; ret \u0026lt;\u0026lt; std::endl; } free(req-\u0026gt;buffer); delete req; }; disk.submit_request(req); } void test_write(DiskUtil\u0026amp; disk, off_t offset, size_t length, const char* data) { char* buffer = nullptr; int ret = posix_memalign((void**)\u0026amp;buffer, 4096, length); if (ret \u0026lt; 0) { std::cout \u0026lt;\u0026lt; \u0026#34;malloc buffer error\u0026#34;; return; } std::memcpy(buffer, data, length); IORequest* req = new IORequest(); req-\u0026gt;opcode = OP_WRITE; req-\u0026gt;buffer = buffer; req-\u0026gt;offset = offset; req-\u0026gt;length = length; req-\u0026gt;callback = [req](int ret) { if (ret == 0) { std::cout \u0026lt;\u0026lt; \u0026#34;Write complete\u0026#34; \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; \u0026#34;Write failed: \u0026#34; \u0026lt;\u0026lt; ret \u0026lt;\u0026lt; std::endl; } free(req-\u0026gt;buffer); delete req; }; disk.submit_request(req); } int main() { DiskUtil disk(\u0026#34;test.disk\u0026#34;, BLOCK_SIZE); disk.start(); size_t length = 4096; const char* data = \u0026#34;Hello world\u0026#34;; test_write(disk, 0, length, data); std::this_thread::sleep_for(std::chrono::seconds(1)); test_read(disk, 0, length); // std::system(\u0026#34;rm test.disk\u0026#34;);  disk.stop(); return 0; } 注意点：\n 与libaio不同的是，liburing并不强制上层传递的buffer是4K对齐的，但是为了获取最佳性能，建议还是使用4K对齐的buffer。 实测在内核5.4版本上使用io_uring_prep_read和io_uring_prep_write方法会在调用io_uring_wait_cqe判断cqe-\u0026gt;res的大小时报invalid argument，原因未知  完整代码参见github链接：https://github.com/lambertxiao/storage/tree/master/liburing\n为什么liburing性能高  零拷贝：在传统的系统调用中，数据需要在用户空间和内核空间进行多次内存拷贝，而io_uring可以通过内存映射来实现零拷贝，将数据从内核的缓存区直接传输到用户空间，从而减少了内存拷贝带来的性能开销。 批处理I/O操作：io_uring使用批处理机制来减少系统调用的次数，可以将多个I/O操作打包成一个请求提交给内核进行处理，从而提高了系统吞吐量。 高效内存管理：io_uring在内核中使用了自身的内存池机制，能够更有效地管理内部的内存，避免了频繁的分配和释放内存所带来的性能损失。 锁控制：io_uring使用了内核层面的锁机制，避免了多线程I/O操作时的竞争情况，提高了系统的并发能力。 io_uring的底层实现使用了ring buffer方式，可以根据需要进行高效的扩展，能够更好地应对高负载工作环境  ","permalink":"https://lambertxiao.github.io/posts/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8liburing%E8%AF%BB%E5%86%99%E7%A3%81%E7%9B%98/doc/","summary":"什么是liburing liburing是一个用于异步IO库，它提供了简洁易用的API来处理文件I/O、网络I/O以及事件驱动I/O等各种I/O操作。liburing库基于Linux内核中的io_uring特性实现，将I/O请求从应用层转移到内核层以提高应用程序的I/O性能。\n 由于liburing在内核版本5.1才引入，所以需要运行环境的linux内核版本大于等于5.1\n 为什么使用liburing liburing库对于一些高并发、高吞吐量的程序，特别是网络服务器、云存储等高性能系统的设计和实现有很大的帮助作用。\n如何使用 建议参考源码里的步骤自行编译安装，源码：https://github.com/axboe/liburing\n常用函数介绍：\n io_uring_queue_init：用于初始化io_uring并且返回其句柄。 io_uring_queue_exit：用于关闭并释放io_uring的句柄。 io_uring_get_sqe：用于获取一个可用的sqe，即I/O请求对应的队列元素数据结构。 io_uring_prep_readv：用于准备一个异步读请求。 io_uring_prep_writev：用于准备一个异步写请求。 io_uring_sqe_set_data：用于将用户私有数据关联到一个sqe（请求）中。 io_uring_submit：用于提交一个或一批异步IO请求到io_uring。 io_uring_peek_cqe：用于查看完成队列（cq）中的未处理项数量。 io_uring_wait_cqe：阻塞等待一个处理完成的io。 io_uring_cqe_get_data：用于获取特定的完成队列项（cqe），其中包含先前提交的IO请求的结果以及相关的私有数据。 io_uring_cqe_seen：用于标记一个完成队列项（cqe）已被处理过。  下面使用liburing封装一个DiskUtil实现对文件的基本读写\n编写disk_util.h头文件\n#include \u0026lt;functional\u0026gt;#include \u0026lt;string\u0026gt;#include \u0026lt;liburing.h\u0026gt;#include \u0026lt;thread\u0026gt; enum IO_OP { OP_READ = 0, OP_WRITE = 1 }; struct IORequest { IO_OP opcode; // 0: read, 1: write  char* buffer; off_t offset; size_t length; std::function\u0026lt;void(int)\u0026gt; callback; }; class DiskUtil { public: DiskUtil(const std::string\u0026amp; file_path, int block_size); ~DiskUtil(); void submit_request(IORequest* req); void start(); void stop(); private: void io_worker_thread(); int open_file(); void close_file(); void process_io_request(IORequest* req); int submit_io_request(IORequest* req); void complete_io_request(); private: int fd_ = -1; io_uring io_ring_; const std::string file_path_; const int block_size_; std::thread io_worker_; bool is_running_ = false; }; 编写disk_tool.","title":"如何使用liburing读写磁盘"},{"content":"前言 有些时候，我们的开发环境需要用到多个版本的gcc, g++，在centos上有方便的工具帮助我们来处理这件事\n操作步骤  安装centos-release-scl  sudo yum install centos-release-scl scl 的含义是 SoftwareCollections，软件集合之意.\n安装devtoolset  安装gcc8使用如下命令：\nyum install devtoolset-8-gcc* 安装gcc7使用如下命令：\nyum install devtoolset-7-gcc* 安装的内容会在/opt/rh目录下\n激活对应的devtoolset  scl enable devtoolset-8 bash 它实际上会调用/opt/rh/devtoolset-8/enable 脚本, 完成工具切换\n查看版本  g++ -v gcc -v ","permalink":"https://lambertxiao.github.io/posts/%E5%9C%A8centos%E4%B8%8A%E5%AE%89%E8%A3%85%E5%A4%9A%E7%89%88%E6%9C%ACgcc/doc/","summary":"前言 有些时候，我们的开发环境需要用到多个版本的gcc, g++，在centos上有方便的工具帮助我们来处理这件事\n操作步骤  安装centos-release-scl  sudo yum install centos-release-scl scl 的含义是 SoftwareCollections，软件集合之意.\n安装devtoolset  安装gcc8使用如下命令：\nyum install devtoolset-8-gcc* 安装gcc7使用如下命令：\nyum install devtoolset-7-gcc* 安装的内容会在/opt/rh目录下\n激活对应的devtoolset  scl enable devtoolset-8 bash 它实际上会调用/opt/rh/devtoolset-8/enable 脚本, 完成工具切换\n查看版本  g++ -v gcc -v ","title":"在centos上安装多版本gcc"},{"content":"什么是libaio libaio是Linux异步I/O文件操作库，它能够提供更高效的文件异步I/O读写方式。\n为什么使用libaio 异步I/O操作是指将数据传输请求发送给操作系统后，操作系统会立即返回并继续执行其他任务，而不必等待数据传输完成，这种操作方式可以充分利用CPU和I/O设备的资源，提高系统的I/O性能。\n相比于传统的同步I/O操作方式，异步I/O操作需要通过系统调用和事件通知机制实现，在编程实现上具有较高的难度。而libaio封装了这些细节，使得开发人员可以更方便地使用异步I/O操作，从而提高应用程序的I/O性能。因此，如果需要高效的文件I/O操作，并期望充分利用系统资源，可以考虑使用libaio。\n如何使用 安装libaio\napt install libaio-dev 接口介绍\n io_setup：用于初始化异步IO环境并返回其句柄。 io_destroy：用于清除异步IO环境并关闭相应的文件描述符。 io_getevents：用于等待指定数量的IO事件（如读、写或错误）并将它们存储在指定的缓冲区中。 io_prep_pread：用于为读取一个文件块准备异步IO操作。 io_prep_pwrite：用于为写入一个文件块准备异步IO操作。 io_submit：用于提交一或多个异步IO请求，并将其排入异步IO环境中，等待事件处理。  下面使用libaio封装一个DiskUtil实现对文件的基本读写\n编写disk_util.h头文件\n#include \u0026lt;functional\u0026gt;#include \u0026lt;string\u0026gt;#include \u0026lt;libaio.h\u0026gt;#include \u0026lt;thread\u0026gt; enum IO_OP { OP_READ = 0, OP_WRITE = 1 }; struct IORequest { IO_OP opcode; // 0: read, 1: write  char* buffer; off_t offset; size_t length; std::function\u0026lt;void(int)\u0026gt; callback; }; class DiskUtil { public: DiskUtil(const std::string\u0026amp; file_path, int block_size); ~DiskUtil(); void submit_request(IORequest* req); void start(); void stop(); private: void io_worker_thread(); int open_file(); void close_file(); void process_io_request(IORequest* req); int submit_io_request(IORequest* req); void complete_io_request(io_context_t ctx, io_event* events, int num_events); private: int fd_ = -1; const std::string file_path_; const int block_size_; // 对应一个会话的上下文  io_context_t io_ctx_ = 0; std::thread io_worker_; bool is_running_ = false; }; 编写disk_tool.cpp\n#include \u0026lt;unistd.h\u0026gt;#include \u0026lt;sys/types.h\u0026gt;#include \u0026lt;sys/stat.h\u0026gt;#include \u0026lt;fcntl.h\u0026gt;#include \u0026lt;libaio.h\u0026gt;#include \u0026lt;iostream\u0026gt;#include \u0026lt;thread\u0026gt;#include \u0026#34;disk_util.h\u0026#34; DiskUtil::DiskUtil(const std::string\u0026amp; file_path, int block_size) : file_path_(file_path), block_size_(block_size) { fd_ = open_file(); if (fd_ \u0026lt; 0) { throw std::runtime_error(\u0026#34;Failed to open file\u0026#34;); } io_ctx_ = 0; int rc = io_setup(128, \u0026amp;io_ctx_); if (rc \u0026lt; 0) { close_file(); throw std::runtime_error(\u0026#34;Failed to setup io context\u0026#34;); } is_running_ = true; io_worker_ = std::thread(std::bind(\u0026amp;DiskUtil::io_worker_thread, this)); } DiskUtil::~DiskUtil() { stop(); close_file(); io_destroy(io_ctx_); } void DiskUtil::submit_request(IORequest* req) { submit_io_request(req); } void DiskUtil::start() { is_running_ = true; } void DiskUtil::stop() { if (is_running_) { is_running_ = false; if (io_ctx_) { io_destroy(io_ctx_); io_ctx_ = 0; } if (io_worker_.joinable()) { io_worker_.join(); } } } void DiskUtil::io_worker_thread() { const int MAX_EVENTS = 128; struct io_event events[MAX_EVENTS]; while (is_running_) { int num_events = 0; while (num_events \u0026lt;= 0) { num_events = io_getevents(io_ctx_, 1, MAX_EVENTS, events, NULL); if (num_events \u0026lt; 0) { std::cerr \u0026lt;\u0026lt; \u0026#34;io_getevents returned error: \u0026#34; \u0026lt;\u0026lt; num_events \u0026lt;\u0026lt; std::endl; break; } } if (num_events \u0026gt; 0) { complete_io_request(io_ctx_, events, num_events); } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } int DiskUtil::open_file() { int flags = O_RDWR | O_DIRECT | O_SYNC | O_CREAT; int mode = S_IRUSR | S_IWUSR; return open(file_path_.c_str(), flags, mode); } void DiskUtil::close_file() { if (fd_ \u0026gt;= 0) { close(fd_); fd_ = -1; } } void DiskUtil::process_io_request(IORequest* req) { std::cout \u0026lt;\u0026lt; \u0026#34;process op:\u0026#34; \u0026lt;\u0026lt; req-\u0026gt;opcode \u0026lt;\u0026lt; std::endl; if (req-\u0026gt;callback) { req-\u0026gt;callback(0); } } int DiskUtil::submit_io_request(IORequest* req) { struct iocb* cb = new iocb; if (req-\u0026gt;opcode == OP_WRITE) { io_prep_pwrite(cb, fd_, req-\u0026gt;buffer, req-\u0026gt;length, req-\u0026gt;offset); } else { io_prep_pread(cb, fd_, req-\u0026gt;buffer, req-\u0026gt;length, req-\u0026gt;offset); } // 将当前的请求的指针绑定到callback上  cb-\u0026gt;data = (void*)req; int num_events = 1; int rc = io_submit(io_ctx_, num_events, \u0026amp;cb); if (rc != num_events) { return -1; } return 0; } void DiskUtil::complete_io_request(io_context_t ctx, io_event* events, int num_events) { for (int i = 0; i \u0026lt; num_events; i++) { struct iocb *io_cb = reinterpret_cast\u0026lt;struct iocb *\u0026gt;(events[i].obj); IORequest *req = reinterpret_cast\u0026lt;IORequest *\u0026gt;(io_cb-\u0026gt;data); process_io_request(req); delete io_cb; } } 可以看到，使用libaio读写文件的代码逻辑其实很简单，但是需要特别注意的点，在打开文件时，必须带上 O_DIRECT的flag!!!\n下面是调用的例子\n#include \u0026lt;cstring\u0026gt;#include \u0026lt;cstdlib\u0026gt;#include \u0026lt;iostream\u0026gt;#include \u0026#34;disk_util.h\u0026#34; #define BLOCK_SIZE 4096  void test_read(DiskUtil\u0026amp; disk, off_t offset, size_t length) { char* buffer = nullptr; int ret = posix_memalign((void**)\u0026amp;buffer, 4096, length); if (ret \u0026lt; 0) { std::cout \u0026lt;\u0026lt; \u0026#34;malloc buffer error\u0026#34;; return; } IORequest* req = new IORequest; req-\u0026gt;opcode = OP_READ; req-\u0026gt;buffer = buffer; req-\u0026gt;offset = offset; req-\u0026gt;length = length; req-\u0026gt;callback = [req](int ret) { if (ret == 0) { std::cout \u0026lt;\u0026lt; \u0026#34;Read complete, data:\u0026#34; \u0026lt;\u0026lt; req-\u0026gt;buffer \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; \u0026#34;Read failed: \u0026#34; \u0026lt;\u0026lt; ret \u0026lt;\u0026lt; std::endl; } free(req-\u0026gt;buffer); delete req; }; disk.submit_request(req); } void test_write(DiskUtil\u0026amp; disk, off_t offset, size_t length, const char* data) { char* buffer = nullptr; int ret = posix_memalign((void**)\u0026amp;buffer, 4096, length); if (ret \u0026lt; 0) { std::cout \u0026lt;\u0026lt; \u0026#34;malloc buffer error\u0026#34;; return; } std::memcpy(buffer, data, length); IORequest* req = new IORequest(); req-\u0026gt;opcode = OP_WRITE; req-\u0026gt;buffer = buffer; req-\u0026gt;offset = offset; req-\u0026gt;length = length; req-\u0026gt;callback = [req](int ret) { if (ret == 0) { std::cout \u0026lt;\u0026lt; \u0026#34;Write complete\u0026#34; \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; \u0026#34;Write failed: \u0026#34; \u0026lt;\u0026lt; ret \u0026lt;\u0026lt; std::endl; } free(req-\u0026gt;buffer); delete req; }; disk.submit_request(req); } int main() { DiskUtil disk(\u0026#34;test.disk\u0026#34;, BLOCK_SIZE); disk.start(); size_t length = 4096; const char* data = \u0026#34;Hello world\u0026#34;; test_write(disk, 0, length, data); std::this_thread::sleep_for(std::chrono::seconds(1)); test_read(disk, 0, length); // std::system(\u0026#34;rm test.disk\u0026#34;);  disk.stop(); return 0; } 注意点：\n 传递给libaio的读写buffer必须是4K对齐的，为此需要用posix_memalign函数来malloc内存。 上述例子为了实现简单，直接开启了一个线程负责libaio的读写请求，其实libaio也支持与epoll结合使用，可以将读写文件的fd交由epoll来管理，从而实现整个服务只使用单线程(单线程简单易懂，又不需要考虑锁的逻辑，实际在项目中运用时推荐使用)  完整代码参见github链接：https://github.com/lambertxiao/storage/tree/master/libaio\n为什么libaio性能高   异步I/O操作：libaio使用异步I/O操作方式，当发起一个I/O操作后，操作系统会立即返回并继续执行其他任务，而不必等待数据传输完成，这种操作方式可以充分利用CPU和I/O设备的资源，提高系统的I/O性能。 】2. 零拷贝：libaio能够在内核空间和用户空间之间实现零拷贝数据传输，即在操作系统内核中完成数据传输操作，并直接将数据传递到应用程序的用户空间，避免了数据拷贝操作，减少了CPU的负载，提高了数据传输速度和性能。\n  4K对齐：libaio要求数据块对齐到4K的边界，这样可以避免额外的I/O操作，充分利用操作系统的缓存机制，从而提高文件读写性能。\n  多线程I/O ：libaio支持多线程I/O操作，可以并发地进行多个I/O操作，从而更加充分地利用CPU和I/O设备的资源，提高系统I/O性能。\n  ","permalink":"https://lambertxiao.github.io/posts/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8libaio%E8%AF%BB%E5%86%99%E7%A3%81%E7%9B%98/doc/","summary":"什么是libaio libaio是Linux异步I/O文件操作库，它能够提供更高效的文件异步I/O读写方式。\n为什么使用libaio 异步I/O操作是指将数据传输请求发送给操作系统后，操作系统会立即返回并继续执行其他任务，而不必等待数据传输完成，这种操作方式可以充分利用CPU和I/O设备的资源，提高系统的I/O性能。\n相比于传统的同步I/O操作方式，异步I/O操作需要通过系统调用和事件通知机制实现，在编程实现上具有较高的难度。而libaio封装了这些细节，使得开发人员可以更方便地使用异步I/O操作，从而提高应用程序的I/O性能。因此，如果需要高效的文件I/O操作，并期望充分利用系统资源，可以考虑使用libaio。\n如何使用 安装libaio\napt install libaio-dev 接口介绍\n io_setup：用于初始化异步IO环境并返回其句柄。 io_destroy：用于清除异步IO环境并关闭相应的文件描述符。 io_getevents：用于等待指定数量的IO事件（如读、写或错误）并将它们存储在指定的缓冲区中。 io_prep_pread：用于为读取一个文件块准备异步IO操作。 io_prep_pwrite：用于为写入一个文件块准备异步IO操作。 io_submit：用于提交一或多个异步IO请求，并将其排入异步IO环境中，等待事件处理。  下面使用libaio封装一个DiskUtil实现对文件的基本读写\n编写disk_util.h头文件\n#include \u0026lt;functional\u0026gt;#include \u0026lt;string\u0026gt;#include \u0026lt;libaio.h\u0026gt;#include \u0026lt;thread\u0026gt; enum IO_OP { OP_READ = 0, OP_WRITE = 1 }; struct IORequest { IO_OP opcode; // 0: read, 1: write  char* buffer; off_t offset; size_t length; std::function\u0026lt;void(int)\u0026gt; callback; }; class DiskUtil { public: DiskUtil(const std::string\u0026amp; file_path, int block_size); ~DiskUtil(); void submit_request(IORequest* req); void start(); void stop(); private: void io_worker_thread(); int open_file(); void close_file(); void process_io_request(IORequest* req); int submit_io_request(IORequest* req); void complete_io_request(io_context_t ctx, io_event* events, int num_events); private: int fd_ = -1; const std::string file_path_; const int block_size_; // 对应一个会话的上下文  io_context_t io_ctx_ = 0; std::thread io_worker_; bool is_running_ = false; }; 编写disk_tool.","title":"如何使用libaio读写磁盘"},{"content":"简介 Valgrind是一个非常有用的开源工具集，主要用于Linux和其他类Unix操作系统上的代码调试、跟踪内存泄漏、性能分析以及各种内存错误等问题的诊断和解决。\nValgrind包含了多个工具，这些工具可以检测出内存泄漏、数组越界访问、使用未初始化的内存、传递未初始化的变量等等问题。同时，它还可以提供程序的时间性能的分析，以及调试信息的输出。\n 作为C, C++开发者，用了valgrind，妈妈再也不用担心你写的代码有内存泄漏了\n 怎么用 valgrind的用法非常简单，仅需要在你的执行命令前加上valgrind\n$ valgrind ./your-program [your-program params]  如果你的程序是使用gcc编译的，在编译的时候加上-g选项，valgrind就可以给你更多的与错误相关的信息\n 看懂valgrind的错误提示 valgrind能诊断出来如下几种错误：\n Invalid read Invalid write Conditional jumps Syscall param points to unadressable bytes Invalid free Mismatched free Fishy values  Invalid write 举个例子\nint main(void) { char *str = malloc(sizeof(char) * 10); int i = 0; while (i \u0026lt; 15) { str[i] = \u0026#39;\\0\u0026#39;; i = i + 1; } free(str); return (0); } 上面例子里, str指向了一块大小为10字节的内存，但是却在while循环里，设置了超过10字节的内容\n$ gcc main.c -g $ valgrind ./a.out ==18332== Memcheck, a memory error detector ==18332== Copyright (C) 2002-2017, and GNU GPL\\'d, by Julian Seward et al. ==18332== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info ==18332== Command: ./a.out ==18332== ==18332== Invalid write of size 1 ==18332== at 0x400553: main (test.c:7) ==18332== Address 0x521004a is 0 bytes after a block of size 10 alloc\\'d ==18332== at 0x4C2EB6B: malloc (vg_replace_malloc.c:299) ==18332== by 0x400538: main (test.c:3) ==18332== ==18332== ==18332== HEAP SUMMARY: ==18332== in use at exit: 0 bytes in 0 blocks ==18332== total heap usage: 1 allocs, 1 frees, 10 bytes allocated ==18332== ==18332== All heap blocks were free'd -- no leaks are possible ==18332== ==18332== For counts of detected and suppressed errors, rerun with: -v ==18332== ERROR SUMMARY: 5 errors from 1 contexts (suppressed: 0 from 0) “Invalid write” 意味着我们的程序将数据写到了不该写入的地方\nInvalid read int main(void) { int i; int *ptr = NULL; i = *ptr; return (0); } 上面的代码将未初始化的指针ptr解引用，使用valgrind一起运行这段代码，会得到如下的错误\n==26212== Invalid read of size 4 ==26212== at 0x400497: main (test.c:8) ==26212== Address 0x0 is not stack\\'d, malloc\\'d or (recently) free\\'d Conditional jumps int main(void) { int i; if (i == 0) { my_printf(\u0026#34;Hello\\n\u0026#34;); } return (0); } 使用valgrind诊断后，它会告诉你使用了会初始化的值来作为条件跳转\n==28042== Conditional jump or move depends on uninitialised value(s) ==28042== at 0x4004E3: main (test.c:5) Syscall param points to unadressable bytes int main(void) { int fd = open(\u0026#34;test\u0026#34;, O_RDONLY); char *buff = malloc(sizeof(char) * 3); free(buff); read(fd, buff, 2); } 如上所示，buff所指向的内存在被free后，又被用来作为读取，此时valgrind就要敲你了\n==32002== Syscall param read(buf) points to unaddressable byte(s) ==32002== at 0x4F3B410: __read_nocancel (in /usr/lib64/libc-2.25.so) ==32002== by 0x400605: main (test.c:11) ==32002== Address 0x5210040 is 0 bytes inside a block of size 3 free\\'d ==32002== at 0x4C2FD18: free (vg_replace_malloc.c:530) ==32002== by 0x4005EF: main (test.c:10) ==32002== Block was alloc\\'d at ==32002== at 0x4C2EB6B: malloc (vg_replace_malloc.c:299) ==32002== by 0x4005DF: main (test.c:8) Invalid free int main(void) { char *buff = malloc(sizeof(char) * 54); free(buff); free(buff); return (0); } 这也是个很经典的内存释放问题，一个内存被多次释放，valgrind又要敲你了\n==755== Invalid free() / delete / delete[] / realloc() ==755== at 0x4C2FD18: free (vg_replace_malloc.c:530) ==755== by 0x400554: main (test.c:10) ==755== Address 0x5210040 is 0 bytes inside a block of size 54 free\\'d ==755== at 0x4C2FD18: free (vg_replace_malloc.c:530) ==755== by 0x400548: main (test.c:9) ==755== Block was alloc\\'d at ==755== at 0x4C2EB6B: malloc (vg_replace_malloc.c:299) ==755== by 0x400538: main (test.c:7) Mismatched free int main() { char* p1 = (char*)malloc(sizeof(char)*4); delete p1; char* p2 = new char; free(p2); return 0; } 用new分配的内存，用了free来释放（理应用delete); 用malloc分配的内存，用了delete来释放（理应用free）\n==3281== Mismatched free() / delete / delete [] ==3281== at 0x4C2A51D: operator delete(void*) (vg_replace_malloc.c:586) ==3281== by 0x400771: main (in /root/workspace/cpp/main) ==3281== Address 0x5a02040 is 0 bytes inside a block of size 4 alloc'd ==3281== at 0x4C28F73: malloc (vg_replace_malloc.c:309) ==3281== by 0x400761: main (in /root/workspace/cpp/main) ==3281== ==3281== Mismatched free() / delete / delete [] ==3281== at 0x4C2A06D: free (vg_replace_malloc.c:540) ==3281== by 0x40078B: main (in /root/workspace/cpp/main) ==3281== Address 0x5a02090 is 0 bytes inside a block of size 1 alloc'd ==3281== at 0x4C29593: operator new(unsigned long) (vg_replace_malloc.c:344) ==3281== by 0x40077B: main (in /root/workspace/cpp/main) Fishy values int main() { int size = -10; char* p1 = (char*)malloc(sizeof(char)*size); return 0; } 上面的程序中，不小心将需要malloc的内存大小设置为负数，valgrind可以帮我检测出来\n==3378== Argument 'size' of function malloc has a fishy (possibly negative) value: -10 ==3378== at 0x4C28F73: malloc (vg_replace_malloc.c:309) ==3378== by 0x400698: main (in /root/workspace/cpp/main) ","permalink":"https://lambertxiao.github.io/posts/valgrind/%E6%80%8E%E4%B9%88%E4%BD%BF%E7%94%A8valgrind%E8%AF%8A%E6%96%AD%E5%86%85%E5%AD%98%E9%97%AE%E9%A2%98/","summary":"让valgrind为你的程序保驾护航","title":"怎么使用valgrind诊断内存问题"},{"content":"它是什么 内存模型是用来描述多线程程序中，不同线程之间共享内存的方式和规范。\n为什么需要它 在多核、多线程的计算机系统中，同一块内存（即临界区）可能被多个线程同时访问，如果不加以规范，在许多情况下会导致程序出错。内存模型的出现就是为了解决多线程下程序的正确性问题。\n它规定了什么东西 对于一般的编程语言而言，在讨论内存模型时，本质上是在讨论如下三个方面的问题：\n 原子性 内存屏障 内存重排序  原子性 很好理解，即一个操作要么全部完成，要么不完成。基本有多线程能力的编程语言都会提供如atomic add, atomic set 等操作\n内存屏障 内存屏障可以通过CPU指令级别的方式来保证内存操作的原子性和顺序性，确保线程的数据修改在内存中是有序的，避免多个线程同时修改同一块内存导致数据混乱的情况发生。它可以划分为两种类型：读屏障和写屏障。\n  读屏障用于确保在执行某个操作之前，内存中的数据已经被加载到寄存器中，保证读取到的是最新的数据。\n  写屏障用于确保在执行某个操作之后，寄存器中的数据已经被刷新回内存中，保证修改后的数据对其他线程可见。\n  内存屏障原理 内存屏障的原理是通过CPU添加特殊的指令来实现的。这些指令在执行时会强制CPU暂停一段时间，以确保其前面和后面的内存访问发生的顺序与代码编写时的顺序一致。这样可以防止多线程程序出现数据异常的情况。\n具体来说，内存屏障的原理包括2个方面：\n  内存同步指令：内存同步指令包括内存屏障和锁定。内存屏障通过添加特殊的机器指令到CPU中来实现内存同步，其作用是让编译器和处理器在读写内存时，避免可能出现的指令重排、乱序执行等问题。\n  内存一致性协议：内存一致性协议保证了多个CPU之间共享的内存数据的一致性，当一个CPU修改了共享内存中的某个数据时，其他CPU能迅速地看到并更新本地缓存中的数据。\n  总之，内存屏障的原理是通过CPU添加特殊的指令，来保证线程对共享内存的读写顺序和同步。这样可以避免多线程程序出现数据竞争和错误，保证程序的正确性和性能。\n内存重排序 内存重排序是指编译器和CPU为了优化程序而对指令进行的重新排序。在某些情况下，这种重排序可能会破坏程序的正确性。所以一般的编程语言的内存模型都会规定了一些内存重排序的规则，以避免程序出错。\n展开讲讲内存一致性协议 内存一致性协议是指多处理器间共享内存数据的一致性保证机制，保证了多个处理器之间共享的内存数据的一致性。内存一致性协议通常包含如下几个基本原则：\n 共享内存的数据一致性：处理器之间共享内存的数据需要保证一致性。 处理器之间的通信：每个处理器必须能够检查其他处理器的缓存中是否已经存在内存数据。 数据的变化：处理器修改缓存中的数据时必须通知其他处理器缓存中该数据的状态已经过期，需要重新从主存中读取。 内存的访问速度：为了保证访问速度，内存一致性协议需要尽可能减少处理器之间的通信频率。  在多处理器系统中，各处理器访问共享内存时，会首先在自己的缓存中查找该内存位置的数据。如果该数据已被缓存，则处理器就从缓存中获取该数据。如果缓存中没有找到该数据，则需要从内存中读取数据。当处理器修改缓存中的共享数据时，内存一致性协议的机制就会保证其他处理器能够感知到数据的变化，并进行相应的更新。\n总之，内存一致性协议是多处理器系统中的一种内存保证机制，保证多个处理器之间共享的内存数据的正确性和一致性。它可以有效避免由于多个处理器对同一块共享内存进行操作而产生的一系列问题。\n","permalink":"https://lambertxiao.github.io/posts/%E8%81%8A%E8%81%8A%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/doc/","summary":"原子性, 内存屏障, 内存重排序","title":"聊聊内存模型"},{"content":"文件系统之ext2 为什么学习ext2 linux早期的文件系统，虽说功能不是特别强大，但是麻雀虽小却五脏齐全，适合用于学习。\n怎么得到一个ext2的文件系统 这里首先要知道文件系统并不一定需要安装在硬盘上，我们可以通过创建一个块设备，并将块设备格式化为ext2文件系统。\n首先我们通过dd创建一个16M的空白块设备\ndd if=/dev/zero of=mdisk bs=1M count=16 将该块设备进行ext2格式化\nmkfs.ext2 mdisk 此时mdisk这个文件已经被格式化为ext2格式了，我们通过下面的命令确认一下\ndumpe2fs mdisk  dumpe2fs 是linux自带的一个工具，可以查看文件系统的描述信息\n 一般的，我们会得到以下的信息\ndumpe2fs 1.44.5 (15-Dec-2018) Filesystem volume name: \u0026lt;none\u0026gt; Last mounted on: \u0026lt;not available\u0026gt; Filesystem UUID: 6f4abbce-ba88-491b-98c6-82275d98afa6 Filesystem magic number: 0xEF53 Filesystem revision #: 1 (dynamic) Filesystem features: ext_attr resize_inode dir_index filetype sparse_super large_file Filesystem flags: signed_directory_hash Default mount options: user_xattr acl Filesystem state: clean Errors behavior: Continue Filesystem OS type: Linux Inode count: 4096 Block count: 16384 Reserved block count: 819 Free blocks: 15723 Free inodes: 4085 First block: 1 Block size: 1024 Fragment size: 1024 Reserved GDT blocks: 63 Blocks per group: 8192 Fragments per group: 8192 Inodes per group: 2048 Inode blocks per group: 256 Filesystem created: Wed Nov 23 01:06:01 2022 Last mount time: n/a Last write time: Wed Nov 23 01:06:01 2022 Mount count: 0 Maximum mount count: -1 Last checked: Wed Nov 23 01:06:01 2022 Check interval: 0 (\u0026lt;none\u0026gt;) Reserved blocks uid: 0 (user root) Reserved blocks gid: 0 (group root) First inode: 11 Inode size:\t128 Default directory hash: half_md4 Directory Hash Seed: f18550d1-7a1e-4c49-b8cc-742790eca888 Group 0: (Blocks 1-8192) Primary superblock at 1, Group descriptors at 2-2 Reserved GDT blocks at 3-65 Block bitmap at 66 (+65) Inode bitmap at 67 (+66) Inode table at 68-323 (+67) 7855 free blocks, 2037 free inodes, 2 directories Free blocks: 338-8192 Free inodes: 12-2048 Group 1: (Blocks 8193-16383) Backup superblock at 8193, Group descriptors at 8194-8194 Reserved GDT blocks at 8195-8257 Block bitmap at 8258 (+65) Inode bitmap at 8259 (+66) Inode table at 8260-8515 (+67) 7868 free blocks, 2048 free inodes, 0 directories Free blocks: 8516-16383 Free inodes: 2049-4096 东西太多看不懂？没关系接下来一一解释。\next2的布局 由图可以看出，ext2会将整个块设备划分为多个块组(Block Group)，每个块组里又划分成6个区域，分别是超级块、块组描述符、数据块位图，inode位图，inode表以及数据块。其中各部分的作用如下\n  超级块\n超级块是文件系统的起始位置，它是整个文件系统的入口。文件系统的挂载（初始化）就是从读取这里的数据开始 的。在超级块中记录了整个文件系统的描述信息，如格式化时指定的文件系统逻辑块大小信息、逻辑块的数量、inode的 数量，根节点的id和文件系统的特性。上面dumpe2fs中输出的描述信息，基本就是超级块的内容。超级块对应对应ext2源码里的 ext2_super_block结构体。\n  块组描述符\n块组描述符的描述信息包括对应块组中数据块位图的位置、inode位图的位置和inode表的位置等信息。另外，还包 括数据块和inode的剩余情况等信息。对应源码里的 ext2_group_desc。\n  块位图\n块位图标识了块组中哪个数据块被使用了，哪个数据块没有被使用。在块位图区中将1字节(Byte）划分为8份，也 就是用1位(bit）对一个逻辑块进行标记。如果该位是0，则表示该位对应的逻辑块未被使用；如果该位是1，则表示该位 对应的逻辑块已经被使用。\n  inode位图\n和块位图类似，标识哪个inode被使用了，哪个inode没有被使用\n  inode表\n记录inode的信息，对应结构体ext2_inode_info\n  数据块\n存放具体的数据以及目录项ext2_dir_entry_2\n  举个例子 回到一开始我们创建的mdisk，通过dumpe2fs我们知道，这个mdisk的块大小为1024个字节，并且被划分了2个块组。\n... Block size: 1024 ... Group 0: (Blocks 1-8192) Primary superblock at 1, Group descriptors at 2-2 Reserved GDT blocks at 3-65 Block bitmap at 66 (+65) Inode bitmap at 67 (+66) Inode table at 68-323 (+67) 7855 free blocks, 2037 free inodes, 2 directories Free blocks: 338-8192 Free inodes: 12-2048 Group 1: (Blocks 8193-16383) Backup superblock at 8193, Group descriptors at 8194-8194 Reserved GDT blocks at 8195-8257 Block bitmap at 8258 (+65) Inode bitmap at 8259 (+66) Inode table at 8260-8515 (+67) 7868 free blocks, 2048 free inodes, 0 directories Free blocks: 8516-16383 Free inodes: 2049-4096 可以简单的看出，Group0的块范围从1~8192(为什么不是从0开始呢？因为第一个block被拿去作为文件系统的启动块了), 其中超级块在Group0的1号block里，块组描述符在2号描述符里，Reserved GDT blocks 意味着ext2还保留了部分块不对外使用。块位图在第66号块里，inode位图在67号块里，inode表在68～323号块里，剩下的338～8192块作为数据块。\n 注意到Group1里的第一个块是Backup superblock, 这是ext2为里防止超级块数据损毁而在其他的块组里做的备份，值得学习\n 接下来，我们借助hexdump工具查看mdisk的内容。\n查看启动块的内容  对于hexdump命令，-s代表偏移多少字节，-n 代表查看多少字节\n # 由于启动块就在第0个block上，所以我们偏移0个字节，往后查看1024个字节的内容 hexdump -s 0 -n 1024 mdisk 返回如下（最左边一列是十六进制的地址，地址后面是十六进制表示的数据，每一行是16个字节）\n0000000 0000 0000 0000 0000 0000 0000 0000 0000 * 0000400 可以看出，启动块此时并没有写入任何数据，1024个字节都是0\n查看超级块的内容 hexdump -s 1024 -n 1024 mdisk 返回\nroot@10-23-47-166:~/workspace/ext2# hexdump -s 1024 -n 1024 mdisk 0000400 1000 0000 4000 0000 0333 0000 3d6b 0000 0000410 0ff5 0000 0001 0000 0000 0000 0000 0000 0000420 2000 0000 2000 0000 0800 0000 0000 0000 0000430 0179 637d 0000 ffff ef53 0001 0001 0000 0000440 0179 637d 0000 0000 0000 0000 0001 0000 0000450 0000 0000 000b 0000 0080 0000 0038 0000 0000460 0002 0000 0003 0000 4a6f cebb 88ba 1b49 0000470 c698 2782 985d a6af 0000 0000 0000 0000 0000480 0000 0000 0000 0000 0000 0000 0000 0000 * 00004c0 0000 0000 0000 0000 0000 0000 0000 003f 00004d0 0000 0000 0000 0000 0000 0000 0000 0000 00004e0 0000 0000 0000 0000 0000 0000 85f1 d150 00004f0 1e7a 494c ccb8 2774 ec90 88a8 0001 0000 0000500 000c 0000 0000 0000 0179 637d 0000 0000 0000510 0000 0000 0000 0000 0000 0000 0000 0000 * 0000560 0001 0000 0000 0000 0000 0000 0000 0000 0000570 0000 0000 0000 0000 0000 0000 0000 0000 * 0000800 我们通过对照源码的ext2_super_block结构体可以看出对应的含义\nstruct ext2_super_block { __le32\ts_inodes_count;\t/* Inodes count */ __le32\ts_blocks_count;\t/* Blocks count */ __le32\ts_r_blocks_count;\t/* Reserved blocks count */ __le32\ts_free_blocks_count;\t/* Free blocks count */ __le32\ts_free_inodes_count;\t/* Free inodes count */ __le32\ts_first_data_block;\t/* First Data Block */ __le32\ts_log_block_size;\t/* Block size */ __le32\ts_log_frag_size;\t/* Fragment size */ __le32\ts_blocks_per_group;\t/* # Blocks per group */ __le32\ts_frags_per_group;\t/* # Fragments per group */ __le32\ts_inodes_per_group;\t/* # Inodes per group */ __le32\ts_mtime;\t/* Mount time */ __le32\ts_wtime;\t/* Write time */ __le16\ts_mnt_count;\t/* Mount count */ __le16\ts_max_mnt_count;\t/* Maximal mount count */ __le16\ts_magic;\t/* Magic signature */ __le16\ts_state;\t/* File system state */ __le16\ts_errors;\t/* Behaviour when detecting errors */ __le16\ts_minor_rev_level; /* minor revision level */ __le32\ts_lastcheck;\t/* time of last check */ __le32\ts_checkinterval;\t/* max. time between checks */ __le32\ts_creator_os;\t/* OS */ __le32\ts_rev_level;\t/* Revision level */ __le16\ts_def_resuid;\t/* Default uid for reserved blocks */ __le16\ts_def_resgid;\t/* Default gid for reserved blocks */ /* * These fields are for EXT2_DYNAMIC_REV superblocks only. * * Note: the difference between the compatible feature set and * the incompatible feature set is that if there is a bit set * in the incompatible feature set that the kernel doesn\u0026#39;t * know about, it should refuse to mount the filesystem. * * e2fsck\u0026#39;s requirements are more strict; if it doesn\u0026#39;t know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesn\u0026#39;t understand... */ __le32\ts_first_ino; /* First non-reserved inode */ __le16 s_inode_size; /* size of inode structure */ __le16\ts_block_group_nr; /* block group # of this superblock */ __le32\ts_feature_compat; /* compatible feature set */ __le32\ts_feature_incompat; /* incompatible feature set */ __le32\ts_feature_ro_compat; /* readonly-compatible feature set */ __u8\ts_uuid[16];\t/* 128-bit uuid for volume */ char\ts_volume_name[16]; /* volume name */ char\ts_last_mounted[64]; /* directory where last mounted */ __le32\ts_algorithm_usage_bitmap; /* For compression */ /* * Performance hints. Directory preallocation should only * happen if the EXT2_COMPAT_PREALLOC flag is on. */ __u8\ts_prealloc_blocks;\t/* Nr of blocks to try to preallocate*/ __u8\ts_prealloc_dir_blocks;\t/* Nr to preallocate for dirs */ __u16\ts_padding1; /* * Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set. */ __u8\ts_journal_uuid[16];\t/* uuid of journal superblock */ __u32\ts_journal_inum;\t/* inode number of journal file */ __u32\ts_journal_dev;\t/* device number of journal file */ __u32\ts_last_orphan;\t/* start of list of inodes to delete */ __u32\ts_hash_seed[4];\t/* HTREE hash seed */ __u8\ts_def_hash_version;\t/* Default hash version to use */ __u8\ts_reserved_char_pad; __u16\ts_reserved_word_pad; __le32\ts_default_mount_opts; __le32\ts_first_meta_bg; /* First metablock block group */ __u32\ts_reserved[190];\t/* Padding to the end of the block */ }; 举例说明，对于第一行\n0000400 1000 0000 4000 0000 0333 0000 3d6b 0000 1000 0000 表示inode数量4096（注意右边才是高地址），4000 0000 表示block的数量，即16384。可以看到hexdump里打印的内容和dumpe2fs里的描述信息是一致的。\n查看block bitmap  block bitmap在第66号block上，因此偏移66*1024个字节\n hexdump -s 67584 -n 1024 mdisk 返回\n0010800 ffff ffff ffff ffff ffff ffff ffff ffff * 0010820 ffff ffff ffff ffff ffff 0001 0000 0000 0010830 0000 0000 0000 0000 0000 0000 0000 0000 * 0010c00  一行16个字节，即16*8个位\n 可以看出总共有 (16 * 2 + 10) * 8 + 1 = 337 个位被标记成1，即有337个块被使用了\n查看inode bitmap  block bitmap在第67号block上，因此偏移67*1024个字节\n hexdump -s 68608 -n 1024 mdisk 返回\n0010c00 07ff 0000 0000 0000 0000 0000 0000 0000 0010c10 0000 0000 0000 0000 0000 0000 0000 0000 * 0010d00 ffff ffff ffff ffff ffff ffff ffff ffff * 0011000 十六进制的07ff即二进制的 11111111111, 总用有11个1，代表有11个inode被使用了, 这个适合你可能会有疑问，明明我们什么文件都还没创建，怎么就有11个inode被使用了呢？其实这里是ext2保留了前11个inode作为内部使用。特别是2号inode，是作为整个文件系统的根目录。\n查看inode表 在dumpe2fs的输出中，我们知道了每个inode的大小位128个字节，并且inode表在第68～323号块中。\n查看1号inode的内容\nhexdump -s 69632 -n 128 mdisk 返回\n0011000 0000 0000 0000 0000 0179 637d 0179 637d 0011010 0179 637d 0000 0000 0000 0000 0000 0000 0011020 0000 0000 0000 0000 0000 0000 0000 0000 * 0011080 同上面看超级块的方法类似，我们对照inode结构体ext2_inode来看\n/* * Structure of an inode on the disk */ struct ext2_inode { __le16\ti_mode;\t/* File mode */ __le16\ti_uid;\t/* Low 16 bits of Owner Uid */ __le32\ti_size;\t/* Size in bytes */ __le32\ti_atime;\t/* Access time */ __le32\ti_ctime;\t/* Creation time */ __le32\ti_mtime;\t/* Modification time */ __le32\ti_dtime;\t/* Deletion Time */ __le16\ti_gid;\t/* Low 16 bits of Group Id */ __le16\ti_links_count;\t/* Links count */ __le32\ti_blocks;\t/* Blocks count */ __le32\ti_flags;\t/* File flags */ union { struct { __le32 l_i_reserved1; } linux1; struct { __le32 h_i_translator; } hurd1; struct { __le32 m_i_reserved1; } masix1; } osd1;\t/* OS dependent 1 */ __le32\ti_block[EXT2_N_BLOCKS];/* Pointers to blocks */ __le32\ti_generation;\t/* File version (for NFS) */ __le32\ti_file_acl;\t/* File ACL */ __le32\ti_dir_acl;\t/* Directory ACL */ __le32\ti_faddr;\t/* Fragment address */ union { struct { __u8\tl_i_frag;\t/* Fragment number */ __u8\tl_i_fsize;\t/* Fragment size */ __u16\ti_pad1; __le16\tl_i_uid_high;\t/* these 2 fields */ __le16\tl_i_gid_high;\t/* were reserved2[0] */ __u32\tl_i_reserved2; } linux2; struct { __u8\th_i_frag;\t/* Fragment number */ __u8\th_i_fsize;\t/* Fragment size */ __le16\th_i_mode_high; __le16\th_i_uid_high; __le16\th_i_gid_high; __le32\th_i_author; } hurd2; struct { __u8\tm_i_frag;\t/* Fragment number */ __u8\tm_i_fsize;\t/* Fragment size */ __u16\tm_pad1; __u32\tm_i_reserved2[2]; } masix2; } osd2;\t/* OS dependent 2 */ }; 可以看到 0179 637d 对应 i_atime, 即时间戳 1669136761\n查看数据块 前面我们都是分析ext2文件系统格式化之后自带的数据内容，还没有真正自己往ext2中写入过数据。那么怎么自己往ext2里写入数据呢，我们需要执行以下几个步骤，将mdisk这个块挂载起来\n losetup 命令用于设置循环设备。循环设备可把文件虚拟成区块设备，籍以模拟整个文件系统，让用户得以将其视为硬盘驱动器，光驱或软驱等设备，并挂入当作目录来使用。\n losetup /dev/loop0 mdisk mount /dev/loop0 /mnt/test 此时mdisk即被挂载到/mnt/test目录里，进入目录后我们发现跟普通的目录看不出区别。\ncd 进入 /mnt/test, 创建一个带内容的文件a.txt\nroot@ubuntu:/mnt/test# echo abcd \u0026gt; a.txt root@ubuntu:/mnt/test# ls -ali total 18 2 drwxr-xr-x 3 root root 1024 Nov 23 02:10 . 786437 drwxr-xr-x 3 root root 4096 Nov 21 01:42 .. 12 -rw-r--r-- 1 root root 5 Nov 23 02:10 a.txt 11 drwx------ 2 root root 12288 Nov 23 01:06 lost+found 通过上面的ls命令我们知道了a.txt是inode号为12，那么接下来我们就去mdisk的inode表里找到这个inode。\n每个inode的大小位128个字节，并且inode表在第68～323号块中，因此12号inode所在的偏移为68 * 1024 + 11 * 128 = 71168, 所以hexdump命令如下：\nhexdump -s 71040 -n 128 mdisk 返回\n0011580 81a4 0000 0005 0000 1282 637d 10b3 637d 0011590 10b3 637d 0000 0000 0000 0001 0002 0000 00115a0 0000 0000 0001 0000 0201 0000 0000 0000 00115b0 0000 0000 0000 0000 0000 0000 0000 0000 * 00115e0 0000 0000 baff 4d19 0000 0000 0000 0000 00115f0 0000 0000 0000 0000 0000 0000 0000 0000 0011600 81a4 0000 即是i_mode，转化为八进制即100644, 即 -rw-r--r--; 0005 0000 即是i_size，转化为十进制即5;\ni_block是存着数据块的地址，在偏移40个字节处的位置，即 0201, 十进制即513, 所以算出实际的偏移地址为513 * 1024 = 525312, 而inode的size为5，因此hexdump命令如下：\nhexdump -s 525312 -n 5 -C mdisk 返回\n00080400 61 62 63 64 0a |abcd.| 00080405 可以看出，确实存放着我们之前echo的abcd\n查看inode的子目录 前面提到，ext2_inode里的i_blocks存放的是数据块的地址；对于文件，数据块即是存放文件内容的地方；而对于目录，数据块存放的则是目录项 ext2_dir_entry_2。\n/* * The new version of the directory entry. Since EXT2 structures are * stored in intel byte order, and the name_len field could never be * bigger than 255 chars, it\u0026#39;s safe to reclaim the extra byte for the * file_type field. */ struct ext2_dir_entry_2 { __le32\tinode;\t/* Inode number */ __le16\trec_len;\t/* Directory entry length */ __u8\tname_len;\t/* Name length */ __u8\tfile_type; char\tname[];\t/* File name, up to EXT2_NAME_LEN */ }; 我们之前在根目录底下创建过a.txt文件，现在我们就去根目录对应的inode表(2号inode)里去查看它是如何存储的。\n首先查看2号inode表\nhexdump -s 69760 -n 128 mdisk 返回\n0011080 41ed 0000 0400 0000 2d34 637e 2d32 637e 0011090 2d32 637e 0000 0000 0000 0003 0002 0000 00110a0 0000 0000 0001 0000 0144 0000 0000 0000 00110b0 0000 0000 0000 0000 0000 0000 0000 0000 再通过十六进制 0144 算出偏移十进制 324 * 1024 = 331776\n# hexdump -s 331776 -n 128 mdisk -C 00051000 02 00 00 00 0c 00 01 02 2e 00 00 00 02 00 00 00 |................| 00051010 0c 00 02 02 2e 2e 00 00 0b 00 00 00 14 00 0a 02 |................| 00051020 6c 6f 73 74 2b 66 6f 75 6e 64 00 00 0c 00 00 00 |lost+found......| 00051030 d4 03 05 01 61 2e 74 78 74 00 00 00 00 00 00 00 |....a.txt.......| 00051040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00051080 可以看到确实是存储着inode对应目录里的子项\next2的文件数据如何管理 在Ext2文件系统中，文件数据的位置信息存储在ext2_inode的1block成员变量中。该变量是一个32位整型数组，共有15个成员，前12个成员中的内容为文件数据的物理地址，后3个成员存储的内容指向磁盘数据块。数据块中存储的数据并不是文件的数据，而是地址数据。由于在这种情況下数据块存储的并非是文件数据，而是inode与文件数据中间的数据，因此被称为间接块 (Indirect Block，简称IB）\n举例，对于小文件来说，通过直接引用就可以完成数据的存储和查找。比如，在格式化时文件逻辑块大小是4KB, 48KB（4Kx12） 以内的文件都可以通过直接引用完成。但是，如果文件大于48KB，直接引用则无法容纳所有的数据，48KB以外的数据先要通过一级间接引用进行存储。以此类推，当超过本级存储空间的最大值时，要启用下一级进行文件数据的管理。\n","permalink":"https://lambertxiao.github.io/posts/%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F-ext2/doc/","summary":"麻雀虽小，但五脏俱全","title":"文件系统之ext2"},{"content":"查找当前gcc的版本 http://ftp.gnu.org/gnu/gcc/\n下载对应的版本，并解压 cd ~/gcc 使用gcc自带的脚本安装依赖 ./contrib/download_prerequisites 生成makefile mkdir build \u0026amp;\u0026amp; cd build/ ../configure -enable-checking=release -enable-languages=c,c++ -disable-multilib 编译 make -j 8 如果中途出现g++: error: gengtype-lex.c: No such file or directory, 需要安装apt install flex\n安装到机器上 make install 设置环境变量 export CC=/usr/local/bin/gcc export CXX=/usr/local/bin/g++ 查看默认编译选项 echo \u0026quot;\u0026quot; | gcc -v -x c++ -E -\n解决GLIBCXX之类的错误 这是因为glibc++的版本太老，先查找\nwhereis libstdc++.so.6 // /usr/lib/x86_64-linux-gnu/libstdc++.so.6 ls -al /usr/lib/x86_64-linux-gnu/libstdc++.so.6 // /usr/lib/x86_64-linux-gnu/libstdc++.so.6 -\u0026gt; libstdc++.so.6.0.24 在gcc的build目录下查找新编译出来的libstdc++.so\nfind . -name \u0026quot;*libstdc++*\u0026quot; 将找到的libstdc++.so.6.0.30放到/usr/lib/x86_64-linux-gnu/下，并通过软链替换掉原本的指向\n","permalink":"https://lambertxiao.github.io/posts/c++/%E5%AE%89%E8%A3%85%E6%96%B0%E7%89%88gcc/doc/","summary":"查找当前gcc的版本 http://ftp.gnu.org/gnu/gcc/\n下载对应的版本，并解压 cd ~/gcc 使用gcc自带的脚本安装依赖 ./contrib/download_prerequisites 生成makefile mkdir build \u0026amp;\u0026amp; cd build/ ../configure -enable-checking=release -enable-languages=c,c++ -disable-multilib 编译 make -j 8 如果中途出现g++: error: gengtype-lex.c: No such file or directory, 需要安装apt install flex\n安装到机器上 make install 设置环境变量 export CC=/usr/local/bin/gcc export CXX=/usr/local/bin/g++ 查看默认编译选项 echo \u0026quot;\u0026quot; | gcc -v -x c++ -E -\n解决GLIBCXX之类的错误 这是因为glibc++的版本太老，先查找\nwhereis libstdc++.so.6 // /usr/lib/x86_64-linux-gnu/libstdc++.so.6 ls -al /usr/lib/x86_64-linux-gnu/libstdc++.so.6 // /usr/lib/x86_64-linux-gnu/libstdc++.so.6 -\u0026gt; libstdc++.so.6.0.24 在gcc的build目录下查找新编译出来的libstdc++.so\nfind . -name \u0026quot;*libstdc++*\u0026quot; 将找到的libstdc++.so.6.0.30放到/usr/lib/x86_64-linux-gnu/下，并通过软链替换掉原本的指向","title":"安装新版gcc"},{"content":"git clone https://github.com/aws/aws-sdk-cpp cd aws-sdk-cpp mkdir build \u0026amp;\u0026amp; cd build cmake ../ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_ONLY=\u0026quot;s3\u0026quot; cmake --build aws-cpp-sdk-core cmake --build aws-cpp-sdk-s3 cmake --install aws-cpp-sdk-core --prefix ~/workspace/aws cmake --install aws-cpp-sdk-s3 --prefix ~/workspace/aws BUILD_ONLY 选项可以指定编译的模块，不同模块用;隔开\n","permalink":"https://lambertxiao.github.io/posts/c++/%E7%BC%96%E8%AF%91aws-sdk-cpp/doc/","summary":"git clone https://github.com/aws/aws-sdk-cpp cd aws-sdk-cpp mkdir build \u0026amp;\u0026amp; cd build cmake ../ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_ONLY=\u0026quot;s3\u0026quot; cmake --build aws-cpp-sdk-core cmake --build aws-cpp-sdk-s3 cmake --install aws-cpp-sdk-core --prefix ~/workspace/aws cmake --install aws-cpp-sdk-s3 --prefix ~/workspace/aws BUILD_ONLY 选项可以指定编译的模块，不同模块用;隔开","title":"编译aws-sdk-cpp"},{"content":"普通指针存在的问题 如何解决 官方库里给的方法 ","permalink":"https://lambertxiao.github.io/posts/c++/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/doc/","summary":"普通指针存在的问题 如何解决 官方库里给的方法 ","title":"c++之什么是智能指针"},{"content":"","permalink":"https://lambertxiao.github.io/posts/c++/eventfd/doc/","summary":"","title":"什么是eventfd"},{"content":"野指针 看代码\nvoid *p; // 此时p为野指针  “野指针”可能指向任意内存段，因此它可能会损坏正常的数据，也有可能引发其他未知错误\n 正确做法\nvoid *p = NULL 悬空指针 看代码\nvoid *p = malloc(size); free(p); // p为悬空指针了  free(p) 之后，p指针仍然指向之前分配的内存，有可能会引发不可预知的错误\n 正确做法\nvoid *p = malloc(size); free(p); p = NULL ","permalink":"https://lambertxiao.github.io/posts/c-%E9%87%8E%E6%8C%87%E9%92%88%E5%92%8C%E6%82%AC%E7%A9%BA%E6%8C%87%E9%92%88/doc/","summary":"野指针 看代码\nvoid *p; // 此时p为野指针  “野指针”可能指向任意内存段，因此它可能会损坏正常的数据，也有可能引发其他未知错误\n 正确做法\nvoid *p = NULL 悬空指针 看代码\nvoid *p = malloc(size); free(p); // p为悬空指针了  free(p) 之后，p指针仍然指向之前分配的内存，有可能会引发不可预知的错误\n 正确做法\nvoid *p = malloc(size); free(p); p = NULL ","title":"c-野指针和悬空指针"},{"content":"一句话终结\nfd是数组的索引，数组是进程结构体task_struct里的数组*files，*files记录着进程打开的文件, fd作为索引指向某个具体的文件files_struct\nstruct task_struct { // ...  /* Open file information: */ struct files_struct *files; // ... } struct files_struct { // 读相关字段  atomic_t count; bool resize_in_progress; wait_queue_head_t resize_wait; // 打开的文件管理结构  struct fdtable __rcu *fdt; struct fdtable fdtab; // 写相关字段  unsigned int next_fd; unsigned long close_on_exec_init[1]; unsigned long open_fds_init[1]; unsigned long full_fds_bits_init[1]; struct file * fd_array[NR_OPEN_DEFAULT]; }; ","permalink":"https://lambertxiao.github.io/posts/%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F-fd%E7%A9%B6%E7%AB%9F%E6%98%AF%E5%95%A5/doc/","summary":"一句话终结","title":"文件系统-fd究竟是啥"},{"content":"抛砖引玉 想象一个场景，AB两个客户端处在不同的内网里，如何能让A和B相互能访问到彼此？\n我们知道，当A运行在内网里，假设A的内网地址为（192.160.10.1）, 访问公网上一个Server时，站在Server的角度来看A的请求IP，可以发现并不是A的内网地址，为什么呢？因为从A到S的过程中，其实经过了一层NAT，NAT记录一层映射关系，\nA的内网IP和端口 - 出口的IP和端口\n问题 谁来执行NAT 路由器？\n打洞成功率 ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C-nat%E6%89%93%E6%B4%9E/doc/","summary":"洞次打次，洞次打次","title":"网络-NAT打洞"},{"content":"协议介绍 ssdp协议是实现UpNp的协议之一，它使用的传输层协议是UDP，应用层协议则是类HTTP（这种协议组合又称为HTTPU)。\n工作原理 设备查询(ssdp:discover) 当一个控制端加入网络的时候，它会想要知道网络里当前其余设备的信息，此时它会通过向多播地址发起 ssdp:discover 请求来查询设备信息\n 多播地址一般固定为 239.255.255.250:1900，这个组播地址不属于任何服务器或个人，它有点类似一个微信群号，任何成员（组播源）往微信群（组播IP）发送消息（组播数据），这个群里的成员（组播接收者）都会接收到此消息。\n 流程示意 sequenceDiagram participant 控制端 participant 多播地址 控制端 - 多播地址: 发送M-SEARCH请求(ssdp:discover) 多播地址 - 控制端: 响应设备信息   控制端发送的是UDP的多播包，只发一次，但会有很多地址都收到这个包\n 协议格式 请求：\nM-SEARCH * HTTP/1.1 S: uuid:ijklmnop-7dec-11d0-a765-00a0c91e6bf6 Host: 239.255.255.250:1900 Man: \u0026#34;ssdp:discover\u0026#34; ST: ge:fridge MX: 3 响应：\nHTTP/1.1 200 OK Cache-Control: max-age= seconds until advertisement expires S: uuid:ijklmnop-7dec-11d0-a765-00a0c91e6bf6 Location: URL for UPnP description for root device Cache-Control: no-cache=\u0026#34;Ext\u0026#34;,max-age=5000ST:ge:fridge 设备在线消息(ssdp:alive) 当有新设备加入网络时，它应当向一个特定的多播地址使用NOTIFY方法发送ssdp:alive消息，以宣布自己的在线\n流程示意 sequenceDiagram participant 设备 participant 多播地址 设备 - 多播地址: 发送NOTIFY请求(ssdp:alive)   NOTIFY请求不会有响应\n 协议格式 NOTIFY * HTTP/1.1HOST: 239.255.255.250:1900CACHE-CONTROL: max-age = seconds until advertisement expiresLOCATION: URL for UPnP description for root deviceNT: search targetNTS: ssdp:aliveUSN: advertisement UUID 设备离线通知(ssdp:byebye) 当一个设备准备从网络中下线时，它应当向一个特定的多播地址使用NOTIFY方法发送ssdp:byebye消息，以说明自己准备要离线了。\n 如果设备超时未发送ssdp:alive消息也会被视为下线\n 流程示意 sequenceDiagram participant 设备 participant 多播地址 设备 - 多播地址: 发送NOTIFY请求(ssdp:byebye)  协议格式 NOTIFY * HTTP/1.1 HOST: 239.255.255.250:1900NT: search target NTS: ssdp:byebye USN: advertisement UUID 代码实现节选  代码实现节选自goupnp, https://github.com/huin/goupnp\n func (srv *Server) ListenAndServe() error { var err error var addr *net.UDPAddr if addr, err = net.ResolveUDPAddr(\u0026#34;udp\u0026#34;, srv.Addr); err != nil { log.Fatal(err) } var conn net.PacketConn if srv.Multicast { if conn, err = net.ListenMulticastUDP(\u0026#34;udp\u0026#34;, srv.Interface, addr); err != nil { return err } } else { if conn, err = net.ListenUDP(\u0026#34;udp\u0026#34;, addr); err != nil { return err } } return srv.Serve(conn) } // Serve messages received on the given packet listener to the srv.Handler. func (srv *Server) Serve(l net.PacketConn) error { maxMessageBytes := DefaultMaxMessageBytes if srv.MaxMessageBytes != 0 { maxMessageBytes = srv.MaxMessageBytes } for { buf := make([]byte, maxMessageBytes) n, peerAddr, err := l.ReadFrom(buf) if err != nil { return err } buf = buf[:n] go func(buf []byte, peerAddr net.Addr) { // At least one router\u0026#39;s UPnP implementation has added a trailing space \t// after \u0026#34;HTTP/1.1\u0026#34; - trim it. \tbuf = trailingWhitespaceRx.ReplaceAllLiteral(buf, crlf) req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(buf))) if err != nil { log.Printf(\u0026#34;httpu: Failed to parse request: %v\u0026#34;, err) return } req.RemoteAddr = peerAddr.String() srv.Handler.ServeMessage(req) // No need to call req.Body.Close - underlying reader is bytes.Buffer. \t}(buf, peerAddr) } } 上面代码实现了一个UDP的Server，用于作为多播地址端接受设备的消息\n// ServeMessage implements httpu.Handler, and uses SSDP NOTIFY requests to // maintain the registry of devices and services. func (reg *Registry) ServeMessage(r *http.Request) { if r.Method != methodNotify { return } nts := r.Header.Get(\u0026#34;nts\u0026#34;) var err error switch nts { case ntsAlive: err = reg.handleNTSAlive(r) case ntsUpdate: err = reg.handleNTSUpdate(r) case ntsByebye: err = reg.handleNTSByebye(r) default: err = fmt.Errorf(\u0026#34;unknown NTS value: %q\u0026#34;, nts) } if err != nil { log.Printf(\u0026#34;goupnp/ssdp: failed to handle %s message from %s: %v\u0026#34;, nts, r.RemoteAddr, err) } } 处理具体ssdp:alive, ssdp:byebye等逻辑\n// Registry maintains knowledge of discovered devices and services. // // NOTE: the interface for this is experimental and may change, or go away // entirely. type Registry struct { lock sync.Mutex byUSN map[string]*Entry listenersLock sync.RWMutex listeners map[chan\u0026lt;- Update]struct{} } registry维护了所有发现的device\nfunc (reg *Registry) handleNTSAlive(r *http.Request) error { entry, err := newEntryFromRequest(r) if err != nil { return err } reg.lock.Lock() reg.byUSN[entry.USN] = entry reg.lock.Unlock() reg.sendUpdate(Update{ USN: entry.USN, EventType: EventAlive, Entry: entry, }) return nil } func newEntryFromRequest(r *http.Request) (*Entry, error) { now := time.Now() expiryDuration, err := parseCacheControlMaxAge(r.Header.Get(\u0026#34;CACHE-CONTROL\u0026#34;)) if err != nil { return nil, fmt.Errorf(\u0026#34;ssdp: error parsing CACHE-CONTROL max age: %v\u0026#34;, err) } loc, err := url.Parse(r.Header.Get(\u0026#34;LOCATION\u0026#34;)) if err != nil { return nil, fmt.Errorf(\u0026#34;ssdp: error parsing entry Location URL: %v\u0026#34;, err) } bootID, err := parseUpnpIntHeader(r.Header, \u0026#34;BOOTID.UPNP.ORG\u0026#34;, -1) if err != nil { return nil, err } configID, err := parseUpnpIntHeader(r.Header, \u0026#34;CONFIGID.UPNP.ORG\u0026#34;, -1) if err != nil { return nil, err } searchPort, err := parseUpnpIntHeader(r.Header, \u0026#34;SEARCHPORT.UPNP.ORG\u0026#34;, ssdpSearchPort) if err != nil { return nil, err } if searchPort \u0026lt; 1 || searchPort \u0026gt; 65535 { return nil, fmt.Errorf(\u0026#34;ssdp: search port %d is out of range\u0026#34;, searchPort) } return \u0026amp;Entry{ RemoteAddr: r.RemoteAddr, USN: r.Header.Get(\u0026#34;USN\u0026#34;), NT: r.Header.Get(\u0026#34;NT\u0026#34;), Server: r.Header.Get(\u0026#34;SERVER\u0026#34;), Host: r.Header.Get(\u0026#34;HOST\u0026#34;), Location: *loc, BootID: bootID, ConfigID: configID, SearchPort: uint16(searchPort), LastUpdate: now, CacheExpiry: now.Add(expiryDuration), }, nil } ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C-ssdp%E5%8D%8F%E8%AE%AE/doc/","summary":"基于udp+http协议，在upnp中被使用到","title":"网络-ssdp协议"},{"content":"简介 dd 命令用于读取、转换并输出数据，听着很抽象吧，实际用起来很简单，看以下例子\n基本使用方法 dd if=\u0026lt;inputDevice\u0026gt; of=\u0026lt;outputDevice\u0026gt; dd的原理是从if指定的文件或设备中，读取数据，再输出到of指定的文件或设备中\n生成一个10M的空内容文件 dd if=/dev/zero of=test.dat bs=1024k count=10 命令执行后，会在当前文件夹得到一个文件名为test.dat且文件大小为10M的文件，bs=1024k 指定一次往输出端输出1M数据，count=10 指定共往输出端输出10次\n生成随机内容文件 dd if=/dev/random of=test.dat bs=1024k count=10 从文件中读取1M内容 dd if=test.dat of=/dev/null bs=1024k count=1 批量生成文件 seq 10 | xargs -i dd if=/dev/zero of={}.dat bs=1024k count=1 以上命令会生成10个1M大小的文件\ntotal 10248 drwxrwxr-x 2 ubuntu ubuntu 4096 Jun 16 11:13 . drwxr-xr-x 12 ubuntu ubuntu 4096 Jun 16 11:13 .. -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 10.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 1.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 2.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 3.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 4.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 5.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 6.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 7.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 8.dat -rw-rw-r-- 1 ubuntu ubuntu 1048576 Jun 16 11:13 9.dat ","permalink":"https://lambertxiao.github.io/posts/linux-dd%E5%91%BD%E4%BB%A4/doc/","summary":"生成文件的好用工具","title":"Linux-dd命令"},{"content":"简介 strace是linux上的一个可以查看执行命令对应的系统调用的工具。\n使用场景  跟踪命令底下执行的系统调用，无需借助内核及程序日志 定位命令执行失败的原因  使用方式 基本用法 举个例子，以 echo \u0026quot;a\u0026quot; \u0026gt; a.txt 命令为例，可以直接在命令前面加上strace后执行，\nstrace echo \u0026quot;a\u0026quot; \u0026gt; a.txt\n... fstat(1, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0 write(1, \u0026#34;a\\n\u0026#34;, 2) = 2 close(1) = 0 ... 以上输出精简了部分内容，可以看到echo \u0026quot;a\u0026quot; \u0026gt; a.txt对应了几个系统调用，先fstat判断文件是否存在，再通过write将内容写入文件，最后关闭文件。\n统计每个系统调用 strace -c dd if=/dev/zero of=big.dat bs=1024k count=10, 使用dd生成一个10M的文件\n加上-c 参数会统计每一系统调用的所执行的时间,次数和出错的次数等。\n10+0 records in 10+0 records out 10485760 bytes (10 MB, 10 MiB) copied, 0.0104206 s, 1.0 GB/s % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 86.41 0.006694 514 13 write 9.80 0.000759 58 13 read 2.79 0.000216 12 18 12 openat 0.81 0.000063 7 9 close 0.19 0.000015 3 4 fstat 0.00 0.000000 0 1 lseek 0.00 0.000000 0 9 mmap 0.00 0.000000 0 4 mprotect 0.00 0.000000 0 1 munmap 0.00 0.000000 0 3 brk 0.00 0.000000 0 3 rt_sigaction 0.00 0.000000 0 6 pread64 0.00 0.000000 0 1 1 access 0.00 0.000000 0 2 dup2 0.00 0.000000 0 1 execve 0.00 0.000000 0 2 1 arch_prctl ------ ----------- ----------- --------- --------- ---------------- 100.00 0.007747 90 14 total 通过上面的统计信息可以发现，该命令执行期间主要在执行read和write的系统调用，并且write操作占了大部分的时间\n指定跟踪的系统调用类型   -e trace=process\n跟踪涉及过程管理的所有系统调用。这对于观察进程的派生，等待和执行步骤很有用。\n  -e trace=network\n跟踪所有与网络相关的系统调用。\n  -e trace=signal\n跟踪所有与信号相关的系统调用。\n  -e trace=ipc\n跟踪所有与IPC相关的系统调用。\n  ","permalink":"https://lambertxiao.github.io/posts/linux-strace%E5%91%BD%E4%BB%A4/doc/","summary":"查看系统调用的神器","title":"Linux-strace"},{"content":"Golang-Coredump设置  当服务运行过程中异常crash时，我们通常需要借助操作系统生成的core文件来定位问题，core文件中会包含程序crash时的堆栈信息，而对于golang程序，设置coredump配置可分为下面几个步骤:\n  设置ulimit\necho -e \u0026quot;\\n* soft core unlimited\\n\u0026quot; \u0026gt;\u0026gt; /etc/security/limits.conf\n  设置core文件输出目录\nmkdir -p /data/corefiles \u0026amp;\u0026amp; chmod 777 /data/corefiles\n  设置core_pattern\necho -e \u0026quot;\\nkernel.core_pattern=/data/corefiles/core-%e-%s-%u-%g-%p-%t\u0026quot; \u0026gt;\u0026gt; /etc/sysctl.conf\n  使配置生效\nsysctl -p /etc/sysctl.conf\n  在启动go应用前增加环境变量 GOTRACEBACK=crash\nexport GOTRACEBACK=crash # 或 GOTRACEBACK=crash ./your-app   查看coredump内容 使用go语言的dlv dlv可用于调试go程序，也可用于core文件的诊断\n安装\ngo install github.com/go-delve/delve/cmd/dlv@master 使用dlv打开core文件\ndlv core \u0026lt;executable\u0026gt; \u0026lt;core\u0026gt; [flags] 进入dlv的终端后执行 help, 查看帮助\nWARN[0000] CGO_CFLAGS already set, Cgo code could be optimized. layer=dlv Type \u0026#39;help\u0026#39; for list of commands. (dlv) help The following commands are available: Running the program: call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!) continue (alias: c) --------- Run until breakpoint or program termination. next (alias: n) ------------- Step over to next source line. rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve. restart (alias: r) ---------- Restart process. rev ------------------------- Reverses the execution of the target program for the command specified. rewind (alias: rw) ---------- Run backwards until breakpoint or start of recorded history. step (alias: s) ------------- Single step through program. step-instruction (alias: si) Single step a single cpu instruction. stepout (alias: so) --------- Step out of the current function. Manipulating breakpoints: break (alias: b) ------- Sets a breakpoint. breakpoints (alias: bp) Print out info for active breakpoints. clear ------------------ Deletes breakpoint. clearall --------------- Deletes multiple breakpoints. condition (alias: cond) Set breakpoint condition. on --------------------- Executes a command when a breakpoint is hit. toggle ----------------- Toggles on or off a breakpoint. trace (alias: t) ------- Set tracepoint. watch ------------------ Set watchpoint. Viewing program variables and memory: args ----------------- Print function arguments. display -------------- Print value of an expression every time the program stops. examinemem (alias: x) Examine raw memory at the given address. locals --------------- Print local variables. print (alias: p) ----- Evaluate an expression. regs ----------------- Print contents of CPU registers. set ------------------ Changes the value of a variable. vars ----------------- Print package variables. whatis --------------- Prints type of an expression. Listing and switching between threads and goroutines: goroutine (alias: gr) -- Shows or changes current goroutine goroutines (alias: grs) List program goroutines. thread (alias: tr) ----- Switch to the specified thread. threads ---------------- Print out info for every traced thread. Viewing the call stack and selecting frames: deferred --------- Executes command in the context of a deferred call. down ------------- Move the current frame down. frame ------------ Set the current frame, or execute command on a different frame. stack (alias: bt) Print stack trace. up --------------- Move the current frame up. Other commands: check (alias: checkpoint) ----------- Creates a checkpoint at the current position. checkpoints ------------------------- Print out info for existing checkpoints. clear-checkpoint (alias: clearcheck) Deletes checkpoint. config ------------------------------ Changes configuration parameters. disassemble (alias: disass) --------- Disassembler. dump -------------------------------- Creates a core dump from the current process state edit (alias: ed) -------------------- Open where you are in $DELVE_EDITOR or $EDITOR exit (alias: quit | q) -------------- Exit the debugger. funcs ------------------------------- Print list of functions. help (alias: h) --------------------- Prints the help message. libraries --------------------------- List loaded dynamic libraries list (alias: ls | l) ---------------- Show source code. source ------------------------------ Executes a file containing a list of delve commands sources ----------------------------- Print list of source files. transcript -------------------------- Appends command output to a file. types ------------------------------- Print list of types Type help followed by a command for full documentation. (dlv) 通过stack命令可知发生crash时的堆栈信息\n(dlv) stack 0 0x0000000000472f21 in runtime.raise at /usr/local/go/src/runtime/sys_linux_amd64.s:164 1 0x000000000044f67d in runtime.dieFromSignal at /usr/local/go/src/runtime/signal_unix.go:768 2 0x000000000044fcd1 in runtime.sigfwdgo at /usr/local/go/src/runtime/signal_unix.go:982 3 0x000000000044e4f4 in runtime.sigtrampgo at /usr/local/go/src/runtime/signal_unix.go:416 4 0x00000000004732a3 in runtime.sigtramp at /usr/local/go/src/runtime/sys_linux_amd64.s:399 5 0x00000000004733a0 in runtime.sigreturn at /usr/local/go/src/runtime/sys_linux_amd64.s:493 6 0x000000000043800c in runtime.crash at /usr/local/go/src/runtime/signal_unix.go:860 7 0x000000000043800c in runtime.fatalpanic at /usr/local/go/src/runtime/panic.go:1217 8 0x0000000000437945 in runtime.gopanic at /usr/local/go/src/runtime/panic.go:1065 9 0x000000000080eca5 in git.ucloudadmin.com/epoch/us3fs/internal.(*US3fsX).ReadFile at /Users/lambert.xiao/workspace/us3fs/internal/fuse_high.go:286 10 0x00000000007f0004 in git.ucloudadmin.com/epoch/go-fuse/fuseutil.(*fileSystemServer).handleOp at /Users/lambert.xiao/gopath/pkg/mod/git.ucloudadmin.com/epoch/go-fuse@v0.0.3/fuseutil/file_system.go:183 11 0x00000000004716e1 in runtime.goexit at /usr/local/go/src/runtime/asm_amd64.s:1371 (dlv) 使用gdb gdb虽然也可以用于core文件的查看，但go官方并不推荐使用gdb，详情参考：https://go.dev/doc/gdb\n","permalink":"https://lambertxiao.github.io/posts/golang/golang-coredump%E8%AE%BE%E7%BD%AE/doc/","summary":"定位crash问题少不了","title":"Golang-Coredump设置"},{"content":"开个坑，记录实现一个p2p网络里需要了解知识\n UPnP ssdp 简单服务发现协议 soap 简单对象访问协议 httpu httpmu httpmu NAT UDP打洞 pmp  ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C-p2p/doc/","summary":"坚持更新","title":"实现一个p2p网络都需要会什么"},{"content":"切分策略 范围切分 通过某个字段的大小范围来决定数据放在哪张库\n优点：简单，可扩展 缺点：写流量会集中在某个表\n中间表映射 优点：灵活 缺点：引入了新的单点，增加了流程的复杂度\nhash切分 选取一个sharding_key, 利用取mod等操作，将key映射到某个节点\n优点：数据分片比较均匀，不容易出现热点和并发访问的瓶颈 缺点：后续扩容需要迁移数据、存在跨节点查询的问题\n分表字段选择 选择原理：减少跨库，跨表查询\n资源准备和代码改造 核心流程：\n 数据库资源准备 分库分表规则配置 代码改造  写入：单写老库、双写、单写新库 读取：读老、读新、部分读老部分读新 灰度：有灰度读写切换的能力    数据迁移 增量数据同步  同步双写 异步双写 写老库，监听binglog异步同步到新库 中间件同步工具  全量数据迁移 保证新库有全部数据\n 自研迁移任务 中间件同步工具   控制好同步速率\n 一致性校验 确保新库数据正确，达到切换标准\n核心流程：\n 读取老库数据 读取新库数据 比较新老数据 不一致进行补偿  新库存在，老库不存在；新库删除数据 新库不存在，老库存在；新库插入数据 新库存在、老库存在，比较所有字段    灰度切换 定义好灰度的规则，放量后观察一段时间\n","permalink":"https://lambertxiao.github.io/posts/%E6%95%B0%E6%8D%AE%E5%BA%93-%E5%88%86%E5%BA%93%E7%AD%96%E7%95%A5/doc/","summary":"能不接触数据库尽量不接触","title":"数据库分库、迁移"},{"content":"libp2p能干嘛   网络中的节点发现\n可以用来发现p2p网络中的其他节点以及维护其他节点的状态\n  数据传输\np2p网络中节点间数据的传输，并支持多种传输层协议，如TCP、UDP、QUIC等。在传输时还有传输通道加密的功能\n  libp2p是如何支持多种传输协议的 libp2p定义了一个统一的格式，举例如下\n  /ip4/127.0.0.1/tcp/8080\n  /ip4/127.0.0.1/udp/8090\n  /ip4/192.168.0.1/tcp/8080/p2p/QmcEPrat8ShnCph8WjkREzt5CPXF2RwhYxYBALDcLC1iV6\n含义为，对方使用ip4网络，地址为192.168.0.1，tcp协议，监听在8080端口，是一个p2p节点，节点id为QmcEPrat8ShnCph8WjkREzt5CPXF2RwhYxYBALDcLC1iV6\n  libp2p如何创建一个节点 func makeRandomHost(t *testing.T, port int) (host.Host, error) { priv, _, err := crypto.GenerateKeyPair(crypto.RSA, 2048) require.NoError(t, err) return New([]Option{ ListenAddrStrings(fmt.Sprintf(\u0026#34;/ip4/127.0.0.1/tcp/%d\u0026#34;, port)), Identity(priv), DefaultTransports, DefaultMuxers, DefaultSecurity, NATPortMap(), }...) } 可以看出，构建node时，需要传递一些配置:\n 包含node的网络地址、协议、私钥 管理数据传输的transport 管理连接多路复用的muxer 管理连接传输安全的security 管理网络穿透的natManager  // DefaultTransports are the default libp2p transports. // // Use this option when you want to *extend* the set of transports used by // libp2p instead of replacing them. var DefaultTransports = ChainOptions( Transport(tcp.NewTCPTransport), Transport(quic.NewTransport), Transport(ws.New), ) 默认支持3种transport，tcp、quic、websocket\nvar DefaultMuxers = Muxer(\u0026#34;/yamux/1.0.0\u0026#34;, yamux.DefaultTransport) 默认的连接多路复用器是yamux\n创建一个node后，libp2p做了什么 // NewNode constructs a new libp2p Host from the Config. // // This function consumes the config. Do not reuse it (really!). func (cfg *Config) NewNode() (host.Host, error) { swrm, err := cfg.makeSwarm() if err != nil { return nil, err } h, err := bhost.NewHost(swrm, \u0026amp;bhost.HostOpts{ ConnManager: cfg.ConnManager, AddrsFactory: cfg.AddrsFactory, NATManager: cfg.NATManager, EnablePing: !cfg.DisablePing, UserAgent: cfg.UserAgent, MultiaddrResolver: cfg.MultiaddrResolver, EnableHolePunching: cfg.EnableHolePunching, HolePunchingOptions: cfg.HolePunchingOptions, EnableRelayService: cfg.EnableRelayService, RelayServiceOpts: cfg.RelayServiceOpts, }) if err != nil { swrm.Close() return nil, err } if cfg.Relay { // If we\u0026#39;ve enabled the relay, we should filter out relay \t// addresses by default. \t// \t// TODO: We shouldn\u0026#39;t be doing this here. \toldFactory := h.AddrsFactory h.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr { return oldFactory(autorelay.Filter(addrs)) } } if err := cfg.addTransports(h); err != nil { h.Close() return nil, err } // TODO: This method succeeds if listening on one address succeeds. We \t// should probably fail if listening on *any* addr fails. \tif err := h.Network().Listen(cfg.ListenAddrs...); err != nil { h.Close() return nil, err } // Configure routing and autorelay \tvar router routing.PeerRouting if cfg.Routing != nil { router, err = cfg.Routing(h) if err != nil { h.Close() return nil, err } } // Note: h.AddrsFactory may be changed by relayFinder, but non-relay version is \t// used by AutoNAT below. \tvar ar *autorelay.AutoRelay addrF := h.AddrsFactory if cfg.EnableAutoRelay { if !cfg.Relay { h.Close() return nil, fmt.Errorf(\u0026#34;cannot enable autorelay; relay is not enabled\u0026#34;) } ar, err = autorelay.NewAutoRelay(h, cfg.AutoRelayOpts...) if err != nil { return nil, err } } autonatOpts := []autonat.Option{ autonat.UsingAddresses(func() []ma.Multiaddr { return addrF(h.AllAddrs()) }), } if cfg.AutoNATConfig.ThrottleInterval != 0 { autonatOpts = append(autonatOpts, autonat.WithThrottling(cfg.AutoNATConfig.ThrottleGlobalLimit, cfg.AutoNATConfig.ThrottleInterval), autonat.WithPeerThrottling(cfg.AutoNATConfig.ThrottlePeerLimit)) } if cfg.AutoNATConfig.EnableService { autonatPrivKey, _, err := crypto.GenerateEd25519Key(rand.Reader) if err != nil { return nil, err } ps, err := pstoremem.NewPeerstore() if err != nil { return nil, err } // Pull out the pieces of the config that we _actually_ care about. \t// Specifically, don\u0026#39;t setup things like autorelay, listeners, \t// identify, etc. \tautoNatCfg := Config{ Transports: cfg.Transports, Muxers: cfg.Muxers, SecurityTransports: cfg.SecurityTransports, Insecure: cfg.Insecure, PSK: cfg.PSK, ConnectionGater: cfg.ConnectionGater, Reporter: cfg.Reporter, PeerKey: autonatPrivKey, Peerstore: ps, } dialer, err := autoNatCfg.makeSwarm() if err != nil { h.Close() return nil, err } dialerHost := blankhost.NewBlankHost(dialer) if err := autoNatCfg.addTransports(dialerHost); err != nil { dialerHost.Close() h.Close() return nil, err } // NOTE: We\u0026#39;re dropping the blank host here but that\u0026#39;s fine. It \t// doesn\u0026#39;t really _do_ anything and doesn\u0026#39;t even need to be \t// closed (as long as we close the underlying network). \tautonatOpts = append(autonatOpts, autonat.EnableService(dialerHost.Network())) } if cfg.AutoNATConfig.ForceReachability != nil { autonatOpts = append(autonatOpts, autonat.WithReachability(*cfg.AutoNATConfig.ForceReachability)) } autonat, err := autonat.New(h, autonatOpts...) if err != nil { h.Close() return nil, fmt.Errorf(\u0026#34;cannot enable autorelay; autonat failed to start: %v\u0026#34;, err) } h.SetAutoNat(autonat) // start the host background tasks \th.Start() var ho host.Host ho = h if router != nil { ho = routed.Wrap(h, router) } if ar != nil { return autorelay.NewAutoRelayHost(ho, ar), nil } return ho, nil }  创建了一个swarm对象，swarm是一个连接复用器，使用同一个channel来管理与其他节点的消息的收发 使用swarm和config对象创建了一个host，host实现了host.Host接口 调用host的network的listen方法，开启服务监听 拿到host对应的peerRouting  主要接口定义 Network // Network is the interface used to connect to the outside world. // It dials and listens for connections. it uses a Swarm to pool // connections (see swarm pkg, and peerstream.Swarm). Connections // are encrypted with a TLS-like protocol. type Network interface { Dialer io.Closer // SetStreamHandler sets the handler for new streams opened by the \t// remote side. This operation is threadsafe. \tSetStreamHandler(StreamHandler) // NewStream returns a new stream to given peer p. \t// If there is no connection to p, attempts to create one. \tNewStream(context.Context, peer.ID) (Stream, error) // Listen tells the network to start listening on given multiaddrs. \tListen(...ma.Multiaddr) error // ListenAddresses returns a list of addresses at which this network listens. \tListenAddresses() []ma.Multiaddr // InterfaceListenAddresses returns a list of addresses at which this network \t// listens. It expands \u0026#34;any interface\u0026#34; addresses (/ip4/0.0.0.0, /ip6/::) to \t// use the known local interfaces. \tInterfaceListenAddresses() ([]ma.Multiaddr, error) // ResourceManager returns the ResourceManager associated with this network \tResourceManager() ResourceManager } 代表一个抽象的网络层，可以开启网络监听，并使用预设好的handler处理其他node的请求\nHost type Host interface { // ID returns the (local) peer.ID associated with this Host  ID() peer.ID // Peerstore returns the Host\u0026#39;s repository of Peer Addresses and Keys.  Peerstore() peerstore.Peerstore // Returns the listen addresses of the Host  Addrs() []ma.Multiaddr // Networks returns the Network interface of the Host  Network() network.Network // Mux returns the Mux multiplexing incoming streams to protocol handlers  Mux() protocol.Switch // Connect ensures there is a connection between this host and the peer with  // given peer.ID. Connect will absorb the addresses in pi into its internal  // peerstore. If there is not an active connection, Connect will issue a  // h.Network.Dial, and block until a connection is open, or an error is  // returned. // TODO: Relay + NAT.  Connect(ctx context.Context, pi peer.AddrInfo) error // SetStreamHandler sets the protocol handler on the Host\u0026#39;s Mux.  // This is equivalent to:  // host.Mux().SetHandler(proto, handler)  // (Threadsafe)  SetStreamHandler(pid protocol.ID, handler network.StreamHandler) // SetStreamHandlerMatch sets the protocol handler on the Host\u0026#39;s Mux  // using a matching function for protocol selection.  SetStreamHandlerMatch(protocol.ID, func(string) bool, network.StreamHandler) // RemoveStreamHandler removes a handler on the mux that was set by  // SetStreamHandler  RemoveStreamHandler(pid protocol.ID) // NewStream opens a new stream to given peer p, and writes a p2p/protocol  // header with given ProtocolID. If there is no connection to p, attempts  // to create one. If ProtocolID is \u0026#34;\u0026#34;, writes no header.  // (Threadsafe)  NewStream(ctx context.Context, p peer.ID, pids ...protocol.ID) (network.Stream, error) // Close shuts down the host, its Network, and services.  Close() error // ConnManager returns this hosts connection manager  ConnManager() connmgr.ConnManager // EventBus returns the hosts eventbus  EventBus() event.Bus } PeerRouting // PeerRouting is a way to find address information about certain peers. // This can be implemented by a simple lookup table, a tracking server, // or even a DHT. type PeerRouting interface { // FindPeer searches for a peer with given ID, returns a peer.AddrInfo \t// with relevant addresses. \tFindPeer(context.Context, peer.ID) (peer.AddrInfo, error) } 可以根据peerId找到peer\nAutoNAT // AutoNAT is the interface for NAT autodiscovery type AutoNAT interface { // Status returns the current NAT status \tStatus() network.Reachability // PublicAddr returns the public dial address when NAT status is public and an \t// error otherwise \tPublicAddr() (ma.Multiaddr, error) io.Closer } ","permalink":"https://lambertxiao.github.io/posts/libp2p/doc/","summary":"在filecoin中发现这个库，读读源码，学习学习","title":"[Writing]使用libp2p来构建点对点网络"},{"content":"简介 cmake和automake本质上都是用来生成Makefile的工具，为啥Makefile需要工具来生成呢？因为大型的C和C++项目构建步骤特别繁琐，自己维护Makefile特别麻烦，因此懒惰的程序员们开发了各种生成Makefile的工具，cmake和automake就是这样的工具。\nautomake使用  运行autoscan命令 将configure.scan 文件重命名为configure.in，并修改configure.in文件 在project目录下新建Makefile.am文件，并在core和shell目录下也新建makefile.am文件 在project目录下新建NEWS、 README、 ChangeLog 、AUTHORS文件 将/usr/share/automake-1.X/目录下的depcomp和complie文件拷贝到本目录下 运行aclocal命令 运行autoconf命令 运行automake -a命令 运行./confiugre脚本(常见的开源软件基本已经提前生成好了configure文件了)  cmake使用 使用cmake仅需要提供一份CMakeLists.txt, 然后运行cmake命令，即可得到一份Makefile文件。\n功能对比   命令 automake cmake   变量定义 name=... set(name, \"...\")   环境检测   AC_INIT AC_PROG_CC AC_CHECK_LIB([pthread], [pthread_rwlock_init]) AC_PROG_RANLIB AC_OUTPUT     find_library(lib libname pathllist) find_package(packename) find_path(var name pathlist) find_program(var name pathlist)     子目录 SUBDIRS= add_subdirectory(list)   可执行文件   bin_PROGRAMS=binname binname_SOURCES= binname_LDADD= binname_CFLAGS= binname_LDFLAGS=     add_executable(binname ${sources}) target_link_libraries(binname librarylist)     动态库   lib_LIBRARIES=libname.so libname_SOURCES=    add_library(libname shared ${source})    静态库   lib_LIBRARIES=libname.a ibname_a_SOURCES=    add_library(libname static ${source})    头文件   INCLUDES= include_HEADES=或CFLAGS=-I    include_directories(list)    源码搜索   aux_source_directories(. list)    依赖库 LIBS= 或 LDADD=  target_link_libraries(binname librarylist)    标志 CFLAGS= 或 LDFLAGS=  set(CMAKE_C_FLAGS ...)    libtool   AC_PROG_LIBTOOL lib_LTLIBRARIES=name.la name_la_SOURCES=      条件语句 使用Make的条件语句 if() endif if(my), else(my), endif(my), while(condition), endwhile(condition)   执行外部命令  exec_program(commd )   子模块  include()   信息输出  messge(STATUS \"messge\")   项目  project(name)   其他文件 EXTRA_DIST install(FILES files.. ), install(DIRECTORY dirs..)   安装设置 EXTRA_DIST install   ","permalink":"https://lambertxiao.github.io/posts/cmakeautomake/doc/","summary":"在C和C++的开源项目里没少见他俩","title":"CMake和Automake"},{"content":"pprof能做什么 pprof能提供正在运行的go程序的各项维度指标，可以帮助我们很好的了解程序的运行状态，如内存的使用，cpu的消耗，是否发现死锁等\npprof提供的profile    profile 解释     cpu 默认进行 30s 的 CPU Profiling，得到一个分析用的 profile 文件   goroutine 查看当前所有运行的 goroutines 堆栈跟踪   block 查看导致阻塞同步的堆栈跟踪   heap 查看活动对象的内存分配情况   mutex 查看导致互斥锁的竞争持有者的堆栈跟踪   threadcreate 查看创建新OS线程的堆栈跟踪    怎么拿到对应的profile文件 当在服务里引入pprof包之后，可能通过http访问的方式拿到profile文件\nwget - O analysis.pprof http://${ip}:${port}/debug/pprof/${profile} 怎么分析 以heap.pprof举例\n// 查看常驻内存的使用情况 go tool pprof -inuse_space heap.pprof // 查看常驻对象的使用情况 go tool pprof -inuse_objects heap.pprof // 查看内存临时分配情况 go tool pprof -alloc_space heap.pprof // 查看对象临时分配情况 go tool pprof -alloc_objects heap.pprof 通过top命令可以查看占用最多的地方\n(pprof) top 20 Showing nodes accounting for 24.34GB, 99.52% of 24.46GB total Dropped 33 nodes (cum \u0026lt;= 0.12GB) Showing top 20 nodes out of 34 flat flat% sum% cum cum% 10.36GB 42.37% 42.37% 10.36GB 42.37% git.ucloudadmin.com/epoch/us3fs/internal.(*Blob).makeAddBh 7.49GB 30.63% 73.01% 7.49GB 30.63% git.ucloudadmin.com/epoch/us3fs/internal.(*BufferHead).tryClear 2.36GB 9.64% 82.64% 2.79GB 11.41% git.ucloudadmin.com/epoch/us3fs/internal.(*Cacher).getBlob (inline) 1.82GB 7.46% 90.10% 2.53GB 10.36% git.ucloudadmin.com/epoch/us3fs/internal.(*US3fs).makeAddInode 0.65GB 2.65% 92.76% 0.65GB 2.65% sync.NewCond (inline) 0.43GB 1.77% 94.53% 0.43GB 1.77% git.ucloudadmin.com/epoch/us3fs/internal.XMap.init (inline) 0.33GB 1.37% 95.90% 0.33GB 1.37% git.ucloudadmin.com/epoch/us3fs/internal.(*Cacher).getBlobSet (inline) 0.30GB 1.24% 97.14% 0.30GB 1.24% git.ucloudadmin.com/epoch/us3fs/internal.(*US3fs).makeDentry 0.28GB 1.14% 98.28% 0.28GB 1.14% encoding/json.(*decodeState).literalStore 0.25GB 1.03% 99.31% 3.09GB 12.64% git.ucloudadmin.com/epoch/us3fs/internal.(*Dir).makeAddChild 0.05GB 0.21% 99.52% 3.42GB 13.98% git.ucloudadmin.com/epoch/us3fs/internal.(*Dir).readdirFromOsV2 0 0% 99.52% 0.28GB 1.14% encoding/json.(*decodeState).array 0 0% 99.52% 0.28GB 1.14% encoding/json.(*decodeState).object 0 0% 99.52% 0.28GB 1.14% encoding/json.(*decodeState).unmarshal 0 0% 99.52% 0.28GB 1.14% encoding/json.(*decodeState).value 0 0% 99.52% 0.28GB 1.14% encoding/json.Unmarshal 0 0% 99.52% 24.46GB 100% git.ucloudadmin.com/epoch/go-fuse/fuseutil.(*fileSystemServer).handleOp 0 0% 99.52% 0.28GB 1.14% git.ucloudadmin.com/epoch/us3-gosdk.(*UFileRequest).PrefixFileList 0 0% 99.52% 10.36GB 42.37% git.ucloudadmin.com/epoch/us3fs/internal.(*Cacher).mapRead 0 0% 99.52% 20.65GB 84.42% git.ucloudadmin.com/epoch/us3fs/internal.(*Cacher).read  flat：表示此函数分配、并由该函数持有的内存空间 flat%：与程序持有总内存的占比 sum%： cum：表示由这个函数或它调用堆栈下面的函数分配的内存总量。 cum%：与程序持有总内存的占比  以web方式打开 go tool pprof -http=:9999 heap.pprof ","permalink":"https://lambertxiao.github.io/posts/golang/golang-pprof%E4%BD%BF%E7%94%A8/doc/","summary":"pprof能做什么 pprof能提供正在运行的go程序的各项维度指标，可以帮助我们很好的了解程序的运行状态，如内存的使用，cpu的消耗，是否发现死锁等\npprof提供的profile    profile 解释     cpu 默认进行 30s 的 CPU Profiling，得到一个分析用的 profile 文件   goroutine 查看当前所有运行的 goroutines 堆栈跟踪   block 查看导致阻塞同步的堆栈跟踪   heap 查看活动对象的内存分配情况   mutex 查看导致互斥锁的竞争持有者的堆栈跟踪   threadcreate 查看创建新OS线程的堆栈跟踪    怎么拿到对应的profile文件 当在服务里引入pprof包之后，可能通过http访问的方式拿到profile文件\nwget - O analysis.pprof http://${ip}:${port}/debug/pprof/${profile} 怎么分析 以heap.pprof举例\n// 查看常驻内存的使用情况 go tool pprof -inuse_space heap.pprof // 查看常驻对象的使用情况 go tool pprof -inuse_objects heap.pprof // 查看内存临时分配情况 go tool pprof -alloc_space heap.","title":"golang-pprof使用"},{"content":"题目：给定一个递增的，含有重复元素的整型数组，求任意两个元素和为target的元素的组合数，\n例：[1,1,2,4,4,6,7,7], 求任意两个元素和为8的元素组合数\nfunc count(arr []int, target int) { left, right := 0, len(arr) - 1 ans := 0 for left \u0026lt; right { val := arr[left] + arr[right] if val == target { // 由于存在重复的元素，需要计算左右各自有多少重复的  lcnt, rcnt := 1, 1 left++ right-- for left \u0026lt; len(arr) - 1 \u0026amp;\u0026amp; arr[left] == arr[left-1] { lcnt++ left++ } for right \u0026gt;= 0 \u0026amp;\u0026amp; arr[right] == arr[right+1] { rcnt++ right-- } // 左边的个数*右边的个数得到组合数  ans += lcnt * rcnt } else val \u0026gt; target { right-- } else { left++ } } return ans } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C%E5%8F%98%E7%A7%8D/doc/","summary":"面试流利说遇到了，做得磕磕绊绊","title":"算法-两数之和变种"},{"content":"简介 iptables其实不是真正的防火墙，我们可以把它理解成一个客户端代理，用户通过iptables这个代理，将用户的安全设定执行到对应的”安全框架”中，这个”安全框架”才是真正的防火墙，这个框架的名字叫netfilter\nnetfilter才是防火墙真正的安全框架（framework），netfilter位于内核空间。\nNetfilter Netfilter是Linux操作系统核心层内部的一个数据包处理模块，它具有如下功能：\n 网络地址转换(Network Address Translate) 数据包内容修改 数据包过滤的防火墙功能  iptables iptables里有三个概念需要先明确一下\n规则 通俗来讲，规则定义了 “如果某个数据包复合这样的规则，就这么处理它”。规则存储在内核空间的信息包过滤表中，这些规则分别指定了源地址、目的地址、传输协议（如TCP、UDP、ICMP）和服务类型（如HTTP、FTP和SMTP）等。当数据包与规则匹配时，iptables就根据规则所定义的方法来处理这些数据包，如放行（accept）, 拒绝（reject）, 丢弃（drop）\n匹配条件   源地址Source IP\n  目标地址 Destination IP\n  扩展匹配条件\n 源端口Source Port 目标端口Destination Port    处理动作  ACCEPT：允许数据包通过。 DROP：直接丢弃数据包，不给任何回应信息，这时候客户端会感觉自己的请求泥牛入海了，过了超时时间才会有反应。 REJECT：拒绝数据包通过，必要时会给数据发送端一个响应的信息，客户端刚请求就会收到拒绝的信息。 SNAT：源地址转换，解决内网用户用同一个公网地址上网的问题。 MASQUERADE：是SNAT的一种特殊形式，适用于动态的、临时会变的ip上。 DNAT：目标地址转换。 REDIRECT：在本机做端口映射。 LOG：在/var/log/messages文件中记录日志信息，然后将数据包传递给下一条规则，也就是说除了记录以外不对数据包做任何其他操作，仍然让下一条规则去匹配  链 有了规则后，那么数据包是如何去匹配规则的？在这里引出了链\n当一个数据包从网卡到达内核空间时，它需要经历一道道关卡。netfilter定义了五道关卡，分别是\n prerouting input forward postrouting output  从图上可以看出，一个数据包根据内容的不同，不一定会将所有的关卡都走一遍。\n有人会问，每个关卡看着都像一个单节点，为什么这里要称之为链呢？其实，当数据包进入关卡后，需要匹配关卡内定义的规则，而一个关卡是可以定义多条规则的，当我们把这些规则都串到一个链表上的时候，就形成了链。\n表 上面可以看出，链上其实就是一系列的规则，且这些规则有些都很相似，比如，A类规则都是对IP或者端口的过滤，B类规则是修改报文。 因此，iptables定义了表的概念，一个表就是具有相同功能的规则的集合。iptables预定义了4种表，如下：\n   表名 功能 对应内核模块 可以使用的链     filter 负责过滤 iptables_filter input, forward, output   nat 网络地址转换 ptable_nat prerouting, output, postrouting   mangle 拆解报文，做出修改，并重新封装 iptable_mangle prerouting, input, forward, output, postrouting   raw 关闭nat表上启用的连接追踪机制 iptable_raw prerouting, output    表的优先级:\nraw –\u0026gt; mangle –\u0026gt; nat –\u0026gt; filter\n完整流程 ","permalink":"https://lambertxiao.github.io/posts/iptables/doc/","summary":"iptables, netfilter, 5链3表","title":"iptables和netfilter"},{"content":"简介 MongoDB采用自底向上的方式来构造查询计划，每一个查询计划 (query plan）都会被分解为若千个有层次的阶段 (stage）。有意思的是，整个查询计划最终会呈现出一颗多叉树的形状。\n整个计算过程是从下向上投递的，每一个阶段的计算结果都是其上层阶段的输入\nvar collection = db.getCollection(\u0026#34;practise\u0026#34;); var count = 10000; var base = 10; var items = []; for (var i = 0; i \u0026lt;= count; i++) { var item = {}; item.x = Math.round(Math.random() * base); item.y = Math.round(Math.random() * base); item.z = Math.round(Math.random() * base); item.did = \u0026#34;ITEM\u0026#34; + i; items.push(item); if (i % 1000 == 0) { collection.insertMany(items); items = []; } } db.getCollection(\u0026#39;practise\u0026#39;).ensureIndex({x: 1, y: 1, z: 1}) db.getCollection(\u0026#39;practise\u0026#39;).ensureIndex({did: 1}) 全表扫描 db.getCollection(\u0026#39;practise\u0026#39;).find({aaa: 11}).explain() { ... \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;aaa\u0026#34; : { \u0026#34;$eq\u0026#34; : 11.0 } }, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;COLLSCAN\u0026#34;, \u0026#34;filter\u0026#34; : { \u0026#34;aaa\u0026#34; : { \u0026#34;$eq\u0026#34; : 11.0 } }, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34; }, \u0026#34;rejectedPlans\u0026#34; : [] }, ... } 单键索引命中 db.getCollection(\u0026#39;practise\u0026#39;).find({did: \u0026#34;ITEM24\u0026#34;}).explain() { ... \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;did\u0026#34; : { \u0026#34;$eq\u0026#34; : \u0026#34;ITEM24\u0026#34; } }, \u0026#34;queryHash\u0026#34; : \u0026#34;B4F08825\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;9F7BDA42\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;did\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;did_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;did\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;did\u0026#34; : [ \u0026#34;[\\\u0026#34;ITEM24\\\u0026#34;, \\\u0026#34;ITEM24\\\u0026#34;]\u0026#34; ] } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, ... } 覆盖索引 db.getCollection(\u0026#39;practise\u0026#39;).find({did: \u0026#34;ITEM24\u0026#34;}, {did: 1, _id: 0}).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;did\u0026#34; : { \u0026#34;$eq\u0026#34; : \u0026#34;ITEM24\u0026#34; } }, \u0026#34;queryHash\u0026#34; : \u0026#34;BA4C57C6\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;302BC8FB\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;PROJECTION_COVERED\u0026#34;, \u0026#34;transformBy\u0026#34; : { \u0026#34;did\u0026#34; : 1.0, \u0026#34;_id\u0026#34; : 0.0 }, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;did\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;did_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;did\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;did\u0026#34; : [ \u0026#34;[\\\u0026#34;ITEM24\\\u0026#34;, \\\u0026#34;ITEM24\\\u0026#34;]\u0026#34; ] } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 列表查询+skip/limit db.getCollection(\u0026#39;practise\u0026#39;).find({x: {$gt: 3}}).skip(10).limit(5).explain() { ... \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;x\u0026#34; : { \u0026#34;$gt\u0026#34; : 3.0 } }, \u0026#34;queryHash\u0026#34; : \u0026#34;39913629\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;CB9286EB\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;LIMIT\u0026#34;, \u0026#34;limitAmount\u0026#34; : 5, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;SKIP\u0026#34;, \u0026#34;skipAmount\u0026#34; : 10, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;(3.0, inf.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } } } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, ... } 内存排序 使用了{x1: 1}, 因此无法利用索引进行排序，只能在内存里排序。当内存排序超过了memLimit时，查询就会出错\ndb.getCollection(\u0026#39;practise\u0026#39;).find({x: {$gt: 3}}).sort({x1: 1}).explain(\u0026#34;executionStats\u0026#34;) { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;x\u0026#34; : { \u0026#34;$gt\u0026#34; : 3.0 } }, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;SORT\u0026#34;, \u0026#34;sortPattern\u0026#34; : { \u0026#34;x1\u0026#34; : 1 }, \u0026#34;memLimit\u0026#34; : 104857600, \u0026#34;type\u0026#34; : \u0026#34;simple\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;(3.0, inf.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 组合索引无法命中 查询无法满足前缀匹配原则，实际上做了全表扫描\ndb.getCollection(\u0026#39;practise\u0026#39;).find({y: 1, z: 3}).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;$and\u0026#34; : [ { \u0026#34;y\u0026#34; : { \u0026#34;$eq\u0026#34; : 1.0 } }, { \u0026#34;z\u0026#34; : { \u0026#34;$eq\u0026#34; : 3.0 } } ] }, \u0026#34;queryHash\u0026#34; : \u0026#34;2B5DAA81\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;189A1787\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;COLLSCAN\u0026#34;, \u0026#34;filter\u0026#34; : { \u0026#34;$and\u0026#34; : [ { \u0026#34;y\u0026#34; : { \u0026#34;$eq\u0026#34; : 1.0 } }, { \u0026#34;z\u0026#34; : { \u0026#34;$eq\u0026#34; : 3.0 } } ] }, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34; }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 组合索引排序命中 sort({y: -1, z: -1}) y和z都使用了降序，所以可以使用索引排序\ndb.getCollection(\u0026#39;practise\u0026#39;).find({x: 1}).sort({y: -1, z: -1}).limit(5).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;x\u0026#34; : { \u0026#34;$eq\u0026#34; : 1.0 } }, \u0026#34;queryHash\u0026#34; : \u0026#34;D1E516FC\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;36B48F45\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;LIMIT\u0026#34;, \u0026#34;limitAmount\u0026#34; : 5, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;backward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;[1.0, 1.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MaxKey, MinKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MaxKey, MinKey]\u0026#34; ] } } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 组合索引命中，内存排序 sort({y: 1, z: -1}) y和z排序方向不同，所以只能在内存中排序。需要注意的是，这里的内存排序是基于索引的而不是文档的，但在mongodb4.0及以前的版本中，对于这种查询的排序是基于文档的，也就是先执行FETCH再执行SORT。\ndb.getCollection(\u0026#39;practise\u0026#39;).find({x: 1}).sort({y: 1, z: -1}).limit(5).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;x\u0026#34; : { \u0026#34;$eq\u0026#34; : 1.0 } }, \u0026#34;queryHash\u0026#34; : \u0026#34;048FB511\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;3CE59AC2\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;SORT\u0026#34;, \u0026#34;sortPattern\u0026#34; : { \u0026#34;y\u0026#34; : 1, \u0026#34;z\u0026#34; : -1 }, \u0026#34;memLimit\u0026#34; : 104857600, \u0026#34;limitAmount\u0026#34; : 5, \u0026#34;type\u0026#34; : \u0026#34;default\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;[1.0, 1.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 组合索引命中，范围+排序 db.getCollection(\u0026#39;practise\u0026#39;).find({x: {$gt: 3}}).sort({x: 1, y: 1, z: 1}).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;x\u0026#34; : { \u0026#34;$gt\u0026#34; : 3.0 } }, \u0026#34;queryHash\u0026#34; : \u0026#34;D7099141\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;4B559A11\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;(3.0, inf.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 不合适的组合索引，范围+排序 x不是等值匹配，因此{y: 1, z: 1}的排序无法利用组合索引的顺序，此时产生了内存排序\ndb.getCollection(\u0026#39;practise\u0026#39;).find({x: {$gt: 3}}).sort({y: 1, z: 1}).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;x\u0026#34; : { \u0026#34;$gt\u0026#34; : 3.0 } }, \u0026#34;queryHash\u0026#34; : \u0026#34;916DB19C\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;B7596367\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;SORT\u0026#34;, \u0026#34;sortPattern\u0026#34; : { \u0026#34;y\u0026#34; : 1, \u0026#34;z\u0026#34; : 1 }, \u0026#34;memLimit\u0026#34; : 104857600, \u0026#34;type\u0026#34; : \u0026#34;default\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;(3.0, inf.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 合并排序 这里使用$in将目标值锁定在有限的若干个值上，数据库会使用归并排序的方式来保证结果的有序性\ndb.getCollection(\u0026#39;practise\u0026#39;).find({x: {$in: [1, 2, 3, 4]}}).sort({y: 1}).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;x\u0026#34; : { \u0026#34;$in\u0026#34; : [ 1.0, 2.0, 3.0, 4.0 ] } }, \u0026#34;queryHash\u0026#34; : \u0026#34;C28AC753\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;AB92D2F0\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;SORT_MERGE\u0026#34;, \u0026#34;sortPattern\u0026#34; : { \u0026#34;y\u0026#34; : 1.0 }, \u0026#34;inputStages\u0026#34; : [ { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;[1.0, 1.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } }, { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;[2.0, 2.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } }, { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;[3.0, 3.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } }, { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;[4.0, 4.0]\u0026#34; ], \u0026#34;y\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } } ] } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } 跨索引的合并排序 db.practise.ensureIndex({x: 1, z: 1}); db.practise.ensureIndex({y: 1, z: 1}); db.getCollection(\u0026#39;practise\u0026#39;).find({$or: [{x: 1}, {y: 1}]}).sort({z: 1}).explain() { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;namespace\u0026#34; : \u0026#34;test.practise\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;$or\u0026#34; : [ { \u0026#34;x\u0026#34; : { \u0026#34;$eq\u0026#34; : 1.0 } }, { \u0026#34;y\u0026#34; : { \u0026#34;$eq\u0026#34; : 1.0 } } ] }, \u0026#34;queryHash\u0026#34; : \u0026#34;56592F73\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;CE19B698\u0026#34;, \u0026#34;maxIndexedOrSolutionsReached\u0026#34; : false, \u0026#34;maxIndexedAndSolutionsReached\u0026#34; : false, \u0026#34;maxScansToExplodeReached\u0026#34; : false, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;SUBPLAN\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;SORT_MERGE\u0026#34;, \u0026#34;sortPattern\u0026#34; : { \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;inputStages\u0026#34; : [ { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;x\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;x_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;x\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;x\u0026#34; : [ \u0026#34;[1.0, 1.0]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } }, { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;y\u0026#34; : 1.0, \u0026#34;z\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;y_1_z_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;y\u0026#34; : [], \u0026#34;z\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;y\u0026#34; : [ \u0026#34;[1.0, 1.0]\u0026#34; ], \u0026#34;z\u0026#34; : [ \u0026#34;[MinKey, MaxKey]\u0026#34; ] } } ] } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, } ","permalink":"https://lambertxiao.github.io/posts/mongo-%E6%9F%A5%E8%AF%A2%E8%AE%A1%E5%88%92/doc/","summary":"什么是查询计划","title":"MongoDB查询计划"},{"content":"前提  mongodb的索引是通过B+树来实现的\n db.test表里插入记录，同时为字段a建立索引\ndb.test.ensureIndex({a: 1}) 等值检索 db.test.find({a: 3}) 范围查询 db.test.find({a: {$gte: 6}}) 分页查询 db.test.find({a: {$gte: 6}}).skip(2).limit(1) 排序的分页查询 db.test.find({a: {$gte: 6}}).sort({a: -1}).skip(2).limit(1) $ne查询 db.test.find({a: {$ne: 3}}).limit(5) 复合索引查询 db.test.ensureIndex({a: 1, b: 1}) db.test.find({a: 5}).sort({b: -1}).limit(1) ","permalink":"https://lambertxiao.github.io/posts/mongo-%E7%B4%A2%E5%BC%95%E6%A3%80%E7%B4%A2%E5%9B%BE%E8%A7%A3/doc/","summary":"亿点点图","title":"MongoDB索引检索图解"},{"content":"介绍 首先，覆盖索引并不是一种索引，而是指一种查询优化的行为。\n我们知道，在一棵二级索引的B+树上，索引1的值存在于树的叶子节点上。因此，如果我们希望查询的字段被包含在索引中，则直接查找二级索引树就可以获得，而不需要再次通过_id索引查找出原始的文档。\n相比“非覆盖式”的查找，覆盖索引1的这种行为可以減少一次对最终文档数据的检索操作（该操作也被称为回表）。\n大部分情况下，二级素引树常驻在内存中，覆盖索引式的查询可以保证一次检索行为仅仅发生在内存中，即避免了对磁盘的1/0操作，这对于性能的提升有显著的效果。\n简单来说，就是需要查询的字段已经在索引里了，可以优化查询的方式，使mongo不去查源文档，从而减少一次查询\n怎么用 假设doc结构如下：\n{ \u0026#34;_id\u0026#34; : ObjectId(\u0026#34;62764716ecf253c7e26f4af3\u0026#34;), \u0026#34;name\u0026#34; : \u0026#34;1\u0026#34;, \u0026#34;gender\u0026#34; : \u0026#34;male\u0026#34; } 现有索引：\ndb.user.ensureIndex({name: 1}) 按下面方式查询时会触发覆盖索引优化：\ndb.user.find({name: \u0026#34;1\u0026#34;}, {name: 1, _id: 0}) _id: 0 让_id字段不要随查询返回\n使用下面命令确认：\ndb.user.find({name: \u0026#34;1\u0026#34;}, {name: 1, _id: 0}).explain() /* 1 */ { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;plannerVersion\u0026#34; : 1, \u0026#34;namespace\u0026#34; : \u0026#34;test01.user\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;name\u0026#34; : { \u0026#34;$eq\u0026#34; : \u0026#34;1\u0026#34; } }, \u0026#34;queryHash\u0026#34; : \u0026#34;3066FB64\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;A5386A93\u0026#34;, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;PROJECTION_COVERED\u0026#34;, \u0026#34;transformBy\u0026#34; : { \u0026#34;name\u0026#34; : 1.0, \u0026#34;_id\u0026#34; : 0.0 }, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;name\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;name_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;name\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;name\u0026#34; : [ \u0026#34;[\\\u0026#34;1\\\u0026#34;, \\\u0026#34;1\\\u0026#34;]\u0026#34; ] } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, \u0026#34;serverInfo\u0026#34; : { \u0026#34;host\u0026#34; : \u0026#34;dev03-ufile-69-21\u0026#34;, \u0026#34;port\u0026#34; : 28018, \u0026#34;version\u0026#34; : \u0026#34;4.4.2-2-g200cba6\u0026#34;, \u0026#34;gitVersion\u0026#34; : \u0026#34;200cba613b10a2edb9ced9def5c4a2000062330f\u0026#34; }, \u0026#34;ok\u0026#34; : 1.0 } 对比不加_id: 0\n/* 1 */ { \u0026#34;queryPlanner\u0026#34; : { \u0026#34;plannerVersion\u0026#34; : 1, \u0026#34;namespace\u0026#34; : \u0026#34;test01.user\u0026#34;, \u0026#34;indexFilterSet\u0026#34; : false, \u0026#34;parsedQuery\u0026#34; : { \u0026#34;name\u0026#34; : { \u0026#34;$eq\u0026#34; : \u0026#34;1\u0026#34; } }, \u0026#34;queryHash\u0026#34; : \u0026#34;D8E51AF6\u0026#34;, \u0026#34;planCacheKey\u0026#34; : \u0026#34;1A14F94A\u0026#34;, \u0026#34;winningPlan\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;PROJECTION_SIMPLE\u0026#34;, \u0026#34;transformBy\u0026#34; : { \u0026#34;name\u0026#34; : 1.0 }, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;FETCH\u0026#34;, \u0026#34;inputStage\u0026#34; : { \u0026#34;stage\u0026#34; : \u0026#34;IXSCAN\u0026#34;, \u0026#34;keyPattern\u0026#34; : { \u0026#34;name\u0026#34; : 1.0 }, \u0026#34;indexName\u0026#34; : \u0026#34;name_1\u0026#34;, \u0026#34;isMultiKey\u0026#34; : false, \u0026#34;multiKeyPaths\u0026#34; : { \u0026#34;name\u0026#34; : [] }, \u0026#34;isUnique\u0026#34; : false, \u0026#34;isSparse\u0026#34; : false, \u0026#34;isPartial\u0026#34; : false, \u0026#34;indexVersion\u0026#34; : 2, \u0026#34;direction\u0026#34; : \u0026#34;forward\u0026#34;, \u0026#34;indexBounds\u0026#34; : { \u0026#34;name\u0026#34; : [ \u0026#34;[\\\u0026#34;1\\\u0026#34;, \\\u0026#34;1\\\u0026#34;]\u0026#34; ] } } } }, \u0026#34;rejectedPlans\u0026#34; : [] }, \u0026#34;serverInfo\u0026#34; : { \u0026#34;host\u0026#34; : \u0026#34;dev03-ufile-69-21\u0026#34;, \u0026#34;port\u0026#34; : 28018, \u0026#34;version\u0026#34; : \u0026#34;4.4.2-2-g200cba6\u0026#34;, \u0026#34;gitVersion\u0026#34; : \u0026#34;200cba613b10a2edb9ced9def5c4a2000062330f\u0026#34; }, \u0026#34;ok\u0026#34; : 1.0 }  IXSCAN, 索引扫描阶段 PROJECTION，投射阶段，即提取对应的name字段 FETCH，文档获取阶段  ","permalink":"https://lambertxiao.github.io/posts/mongo-%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95/doc/","summary":"什么是覆盖索引","title":"MongoDB覆盖索引"},{"content":"分片模式下数据分发流程  mongos在启动后，其内部会维护一份路由表缓存并通过心跳机制与Contg Server （配置中心） 保持同步 业务请求进入后，由mongos开始接管 mongos检索本地路由表，根据请求中的分片键信息找到相应的chunk，进一步确定所在的分片。 mongos向目标分片发起操作，并返回最终结果  避免广播操作 需要向所有的分片查询结果\n单分片故障会影响整个查询\n保证索引唯一性 分片模式会影响索引的唯一性。由于没有手段保证多个分片上的数据唯一，所以唯一性索引必须与分片键使用相同的字段，或者以分片键作为前级。\n如下面的选择可以避免冲突。\n(1） 唯一性索引为：{a：1}，分片键采用a字段。\n(2） 唯一性索引为：{a：1，b：1}, 分片键采用a字段。\n分片均衡 手动均衡 自动均衡 ","permalink":"https://lambertxiao.github.io/posts/mongo/doc/","summary":"分片模式下数据分发流程  mongos在启动后，其内部会维护一份路由表缓存并通过心跳机制与Contg Server （配置中心） 保持同步 业务请求进入后，由mongos开始接管 mongos检索本地路由表，根据请求中的分片键信息找到相应的chunk，进一步确定所在的分片。 mongos向目标分片发起操作，并返回最终结果  避免广播操作 需要向所有的分片查询结果\n单分片故障会影响整个查询\n保证索引唯一性 分片模式会影响索引的唯一性。由于没有手段保证多个分片上的数据唯一，所以唯一性索引必须与分片键使用相同的字段，或者以分片键作为前级。\n如下面的选择可以避免冲突。\n(1） 唯一性索引为：{a：1}，分片键采用a字段。\n(2） 唯一性索引为：{a：1，b：1}, 分片键采用a字段。\n分片均衡 手动均衡 自动均衡 ","title":"MongoDB"},{"content":"622. 设计循环队列 622. 设计循环队列\n设计你的循环队列实现。 循环队列是一种线性数据结构，其操作表现基于 FIFO（先进先出）原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。\n循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里，一旦一个队列满了，我们就不能插入下一个元素，即使在队列前面仍有空间。但是使用循环队列，我们能使用这些空间去存储新的值。\n你的实现应该支持如下操作：\n MyCircularQueue(k): 构造器，设置队列长度为 k 。 Front: 从队首获取元素。如果队列为空，返回 -1 。 Rear: 获取队尾元素。如果队列为空，返回 -1 。 enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。 deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。 isEmpty(): 检查循环队列是否为空。 isFull(): 检查循环队列是否已满。  type MyCircularQueue struct { data []int size int headIdx int count int } func Constructor(k int) MyCircularQueue { q := MyCircularQueue{ data: make([]int, k), size: k, headIdx: 0, count: 0, } return q } func (q *MyCircularQueue) EnQueue(value int) bool { if q.count == q.size { return false } insertIdx := (q.headIdx + q.count) % q.size q.data[insertIdx] = value q.count++ return true } func (q *MyCircularQueue) DeQueue() bool { if q.count == 0 { return false } q.headIdx = (q.headIdx + 1) % q.size q.count-- return true } func (q *MyCircularQueue) Front() int { if q.count == 0 { return -1 } return q.data[q.headIdx] } func (q *MyCircularQueue) Rear() int { if q.count == 0 { return -1 } return q.data[(q.headIdx + q.count - 1) % q.size] } func (q *MyCircularQueue) IsEmpty() bool { return q.count == 0 } func (q *MyCircularQueue) IsFull() bool { return q.count == q.size } /** * Your MyCircularQueue object will be instantiated and called as such: * obj := Constructor(k); * param_1 := obj.EnQueue(value); * param_2 := obj.DeQueue(); * param_3 := obj.Front(); * param_4 := obj.Rear(); * param_5 := obj.IsEmpty(); * param_6 := obj.IsFull(); */ 705. 设计哈希集合 705. 设计哈希集合\n不使用任何内建的哈希表库设计一个哈希集合（HashSet）。\n实现 MyHashSet 类：\n void add(key) 向哈希集合中插入值 key 。 bool contains(key) 返回哈希集合中是否存在这个值 key 。 void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值，什么也不做。  type MyHashSet struct { data []int } func Constructor() MyHashSet { hs := MyHashSet{ data: make([]int, 40000), } return hs } func (s *MyHashSet) Add(key int) { // 先分成32个桶，桶里用一个int的32位表示32个值int为4个字节，有32位可以用来表示状态  idx, offset := s.getLoc(key) // 将0000 0000 0000 0000 0000 0000 0000 0001其中最后一位1移动到对应位置  s.data[idx] |= 1 \u0026lt;\u0026lt; offset } func (s *MyHashSet) getLoc(key int) (int, int) { bucketIdx := key / 32 offset := key % 32 return bucketIdx, offset } func (s *MyHashSet) Remove(key int) { idx, offset := s.getLoc(key) // 将最后一位1移动到目标位之后，～按位取反，再\u0026amp;上，从而将对应位置上的1清掉  v := ^(1 \u0026lt;\u0026lt; offset) s.data[idx] \u0026amp;= ^(1 \u0026lt;\u0026lt; offset) } func (s *MyHashSet) Contains(key int) bool { idx, offset := s.getLoc(key) v := s.data[idx] return (v \u0026gt;\u0026gt; offset) \u0026amp; 1 == 1 } /** * Your MyHashSet object will be instantiated and called as such: * obj := Constructor(); * obj.Add(key); * obj.Remove(key); * param_3 := obj.Contains(key); */ 706. 设计哈希映射 706. 设计哈希映射\n不使用任何内建的哈希表库设计一个哈希映射（HashMap）。\n实现 MyHashMap 类：\n MyHashMap() 用空映射初始化对象 void put(int key, int value) 向 HashMap 插入一个键值对 (key, value) 。如果 key 已经存在于映射中，则更新其对应的- 值 value 。 int get(int key) 返回特定的 key 所映射的 value ；如果映射中不包含 key 的映射，返回 -1 。 void remove(key) 如果映射中存在 key 的映射，则移除 key 和它所对应的 value 。  type MyHashMap struct { data []list.List } func Constructor() MyHashMap { m := MyHashMap{ data: make([]list.List, 769), } return m } type node struct { key, value int } func (m *MyHashMap) Put(key int, value int) { h := m.hash(key) for e := m.data[h].Front(); e != nil; e = e.Next() { if et := e.Value.(node); et.key == key { e.Value = node{key, value} return } } m.data[h].PushBack(node{key, value}) } func (m *MyHashMap) hash(key int) int { return key % 769 } func (m *MyHashMap) Get(key int) int { h := m.hash(key) for e := m.data[h].Front(); e != nil; e = e.Next() { if et := e.Value.(node); et.key == key { return et.value } } return -1 } func (m *MyHashMap) Remove(key int) { h := m.hash(key) for e := m.data[h].Front(); e != nil; e = e.Next() { if e.Value.(node).key == key { m.data[h].Remove(e) } } } /** * Your MyHashMap object will be instantiated and called as such: * obj := Constructor(); * obj.Put(key,value); * param_2 := obj.Get(key); * obj.Remove(key); */ 641. 设计循环双端队列 641. 设计循环双端队列\n设计实现双端队列。\n实现 MyCircularDeque 类:\n MyCircularDeque(int k) ：构造函数,双端队列最大为 k 。 boolean insertFront()：将一个元素添加到双端队列头部。 如果操作成功返回 true ，否则返回 false 。 boolean insertLast() ：将一个元素添加到双端队列尾部。如果操作成功返回 true ，否则返回 false 。 boolean deleteFront() ：从双端队列头部删除一个元素。 如果操作成功返回 true ，否则返回 false 。 boolean deleteLast() ：从双端队列尾部删除一个元素。如果操作成功返回 true ，否则返回 false 。 int getFront() )：从双端队列头部获得一个元素。如果双端队列为空，返回 -1 。 int getRear() ：获得双端队列的最后一个元素。 如果双端队列为空，返回 -1 。 boolean isEmpty() ：若双端队列为空，则返回 true ，否则返回 false 。 boolean isFull() ：若双端队列满了，则返回 true ，否则返回 false 。  type MyCircularDeque struct { head, tail *Node cap, size int } type Node struct { prev, next *Node val int } func Constructor(k int) MyCircularDeque { head, tail := new(Node), new(Node) head.next = tail tail.prev = head q := MyCircularDeque{ cap: k, head: head, tail: tail, } return q } func (q *MyCircularDeque) InsertFront(value int) bool { if q.IsFull() { return false } q.size++ node := \u0026amp;Node{val: value} head := q.head next := head.next head.next, node.prev = node, head node.next, next.prev = next, node return true } func (q *MyCircularDeque) InsertLast(value int) bool { if q.IsFull() { return false } q.size++ node := \u0026amp;Node{val: value} tail := q.tail prev := tail.prev prev.next, node.prev = node, prev tail.prev, node.next = node, tail return true } func (q *MyCircularDeque) DeleteFront() bool { if q.IsEmpty() { return false } q.size-- head := q.head head.next = head.next.next head.next.prev = head return true } func (q *MyCircularDeque) DeleteLast() bool { if q.IsEmpty() { return false } q.size-- tail := q.tail tail.prev = tail.prev.prev tail.prev.next = tail return true } func (q *MyCircularDeque) GetFront() int { if q.IsEmpty() { return -1 } return q.head.next.val } func (q *MyCircularDeque) GetRear() int { if q.IsEmpty() { return -1 } return q.tail.prev.val } func (q *MyCircularDeque) IsEmpty() bool { return q.head.next == q.tail } func (q *MyCircularDeque) IsFull() bool { return q.size == q.cap } /** * Your MyCircularDeque object will be instantiated and called as such: * obj := Constructor(k); * param_1 := obj.InsertFront(value); * param_2 := obj.InsertLast(value); * param_3 := obj.DeleteFront(); * param_4 := obj.DeleteLast(); * param_5 := obj.GetFront(); * param_6 := obj.GetRear(); * param_7 := obj.IsEmpty(); * param_8 := obj.IsFull(); */ 1206. 设计跳表 1206. 设计跳表\n不使用任何库函数，设计一个 跳表 。\n跳表 是在 O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树，其功能与性能相当，并且跳表的代码长度相较下更短，其设计思想与链表相似。\n跳表中有很多层，每一层是一个短的链表。在第一层的作用下，增加、删除和搜索操作的时间复杂度不超过 O(n)。跳表的每一个操作的平均时间复杂度是 O(log(n))，空间复杂度是 O(n)。\n了解更多 : https://en.wikipedia.org/wiki/Skip_list\n在本题中，你的设计应该要包含这些函数：\n bool search(int target) : 返回target是否存在于跳表中。 void add(int num): 插入一个元素到跳表。 bool erase(int num): 在跳表中删除一个值，如果 num 不存在，直接返回false. 如果存在多个 num ，删除其中任意一个即可。  注意，跳表中可能存在多个相同的值，你的代码需要处理这种情况。\ntype Skiplist struct { head *Node prevNodes []*Node // 存放插入过程中需要用到的临时节点  maxLevel int } type Node struct { val int next, down *Node } func Constructor() Skiplist { head := \u0026amp;Node{val: -1} sl := Skiplist{ head: head, prevNodes: make([]*Node, 64), maxLevel: 16, } return sl } func (sl *Skiplist) Search(target int) bool { curr := sl.head for curr != nil { // 在同一层上找  for curr.next != nil \u0026amp;\u0026amp; curr.next.val \u0026lt; target { curr = curr.next } if curr.next != nil \u0026amp;\u0026amp; curr.next.val == target { return true } curr = curr.down } return false } func (sl *Skiplist) Add(num int) { level := -1 curr := sl.head for curr != nil { for curr.next != nil \u0026amp;\u0026amp; curr.next.val \u0026lt; num { curr = curr.next } level++ sl.prevNodes[level] = curr curr = curr.down } // 从最底层level往上，对于待插入的节点，决定是否插入在某个level上  insertUp := true var downNode *Node for insertUp \u0026amp;\u0026amp; level \u0026gt;= 0 { prevNode := sl.prevNodes[level] level-- prevNode.next = \u0026amp;Node{val: num, next: prevNode.next, down: downNode } downNode = prevNode.next // 随机决定是否在上一层插入node  insertUp = rand.Intn(2) == 0 } if insertUp { newhead := \u0026amp;Node{val: num, next: nil, down: downNode } sl.head = \u0026amp;Node{val: -1, next: newhead, down: sl.head} } } func (sl *Skiplist) Erase(num int) bool { exist := false curr := sl.head for curr != nil { for curr.next != nil \u0026amp;\u0026amp; curr.next.val \u0026lt; num { curr = curr.next } if curr.next != nil \u0026amp;\u0026amp; curr.next.val == num { exist = true curr.next = curr.next.next } curr = curr.down } return exist } /** * Your Skiplist object will be instantiated and called as such: * obj := Constructor(); * param_1 := obj.Search(target); * obj.Add(num); * param_3 := obj.Erase(num); */ ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/doc/","summary":"双向队列，跳表等","title":"算法-设计数据结构"},{"content":"结构  MemTable：内存数据结构，具体实现是 SkipList。 接受用户的读写请求，新的数据会先在这里写入。 Immutable MemTable：当 MemTable 的大小达到设定的阈值后，会被转换成 Immutable MemTable，只接受读操作，不再接受写操作，然后由后台线程 flush 到磁盘上 —— 这个过程称为 minor compaction。 Log：数据写入 MemTable 之前会先写日志，用于防止宕机导致 MemTable 的数据丢失。一个日志文件对应到一个 MemTable。 SSTable：Sorted String Table。分为 level-0 到 level-n 多层，每一层包含多个 SSTable，文件内数据有序。除了 level-0 之外，每一层内部的 SSTable 的 key 范围都不相交。 Manifest：Manifest 文件中记录 SSTable 在不同 level 的信息，包括每一层由哪些 SSTable，每个 SSTable 的文件大小、最大 key、最小 key 等信息。 Current：重启时，LevelDB 会重新生成 Manifest，所以 Manifest 文件可能同时存在多个，Current 记录的是当前使用的 Manifest 文件名。 TableCache：TableCache 用于缓存 SSTable 的文件描述符、索引和 filter。 BlockCache：SSTable 的数据是被组织成一个个 block。BlockCache 用于缓存这些 block（解压后）的数据。  写逻辑  将key-value封装成WriteBatch； 循环检查当前DB的状态，确定策略(DBImpl::MakeRoomForWrite()): 如果当前L0层的文件数目达到了kL0_SlowdownWritesTrigger(8)阈值，则会延迟1s写入，该延迟只发生一次； 如果当前memtable的size未达到阈值write_buffer_size(默认4MB)，则允许写入； 如果memtable的size已经达到阈值，但immutable memtable仍然存在，则等待compaction将其dump完成； 如果L0文件数目达到了kL0_StopWritesTrigger(12)，则等待compaction memtable完成； 上述条件都不满足，则memtable已经写满，并且immutable memtable不存在，则将当前memetable置成immutable memtable，产生新的memtable和log file，主动触发compaction，允许该次写。 设置WriteBatch的SequenceNumber; 先将WriteBatch中的数据写入log(Log::AddRecord()); 然后将WriteBatch的数据写入memetable，即遍历WriteBatch解析出key/value/valuetype，Delete操作只写入删除的key，ValueType是KTypeDeletion，表示key以及被删除，后续compaction会删除此key-value。 更新SequenceNumber（last_sequence + WriteBatch::count()）。  为了提高写入效率，LevelDB会把多个写线程提交的key-value封装成一个WriteBatch一次性写入。WriteBatch的结构如图3中WriteBatch所示。\n用户的数据写入到log日志后，还要写入一个memtable的结构中，LevelDB利用skiplist实现了memtable，在memtable内key是有序的。immutable memtable与memtable结构是一样的，只提供读不允许写入。用户的数据已经封装在WriteBatch中，在插入memtable时，需要遍历WriteBatch,decode每个key-value。在memtable中，key-value的格式如下：\n读逻辑  如果ReadOption指定了snapshot，则将snapshot的Sequence Number作为最大的Sequence Number，否则，将当前最大的Sequence Number(VersionSet::last_sequence_number)作为最大的Sequence Number。 在memtable中查找(Memtable::Get())。 如果在memtable中未找到，并且存在immutable memtable，就在immutable memtable中查找(Memtable::Get())。 如果(3)仍未找到，在sstable中查找(VersionSet::Get())，从L0开始，每个level上依次查找，一旦找到，即返回。 首先找出level上可能包含key的sstable，FileMetaData结构体内包含每个sstable的key范围。 L0的查找只能顺序遍历每个file_0,因为L0层的sstable文件之间可能存在重叠的key。在L0层可能找到多个sstable。 非L0层的查找，对file_[i]基于FileMetaData::largest做二分查找即可定位到level中可能包含key的sstable。非L0上sstable之间key不会重叠，所以最多找到一个sstable。 如果该level上没有找到可能的sstable,跳过，否则，对要进行查找的sstable获得其Iterator，做seek()操作。 seek()成功检查有效性，依据ValueType判断是否是有效的数据： kTypeValue: 返回对应的value数据。 kTypeDeletion: 返回data not exist。  sstable文件由data block、meta block、metaindex block、index blcok和footer组成。默认大小2MB。\n data block: key-value存储部分，按key有序排列,data block内部的格式如图7左侧。LevelDB采用了前缀压缩，每16个- key-value记录一次重启点restart。data block默认大小4KB。 meta block：存储key-value的filter,默认是bloom filter。 metaindex blokc: 指向meta block的索引。 index block:指向data block的索引。 footer: 索引metaindex block 和 index block。  如果数据不在内存中的组件中，那就需要在磁盘的sstable文件中查找了，基于B+树思想的存储引擎，利用索引可以直接定位到具体的哪个磁盘块，而LSM树的存储引擎需要遍历多个sstable文件才能确定数据在哪个磁盘块，读性能自然就不如B+树了。在介绍如何在磁盘上查找含有指定Key的sstable之前先介绍一个重要的数据结构：\nstruct FileMetaData {}\n1、先定位key可能在哪些sstable文件中。这里的定位利用了上面的FileMetaData结构体里的smallest、largest字段，如果在这个范围里，该文件的FileMetaData加入一个vcetor tmp保存，每一层有很多的sstable文件，而且key可能存在多个版本，所以tmp里面可能存在很多sstable文件，如何确定最新的版本在哪个文件里呢？查找的原则应该是先从最新的sstable文件中查找，对于L0层，LevelDB做了优化，这里要说明一下，sstable文件是用uint64_t number命名的，而且越新的数据number越大，所以vector tmp按照FileNumber排序。非L0层的sstable文件之间key不会重叠，所以可以利用二分查找定位sstable。\n2、在第一步定位了key可能存在的sstable之后，第二步需要定位key在sstable文件的哪个data block里面，图9的sstable格式里有一个重要的模块：index block。index block存储了data block的索引，为了加速读性能，leveldb也做了优化，把经常访问的sstable的index block缓存在cache中，关于cache的知识点后续会介绍。利用index block可以快速定位key可能存在哪个data block中。如何确定key到底在不在data block呢，LevelDB利用bloom filter，如果通过bloom filter得出key不在此data block中，那么该key 肯定不在此data block中，则data not found；如果通过bloom filter得出key在此data block中，还不能完全肯定在此data block中，还需要去遍历该data block\n","permalink":"https://lambertxiao.github.io/posts/leveldb/doc/","summary":"level，多层级","title":"LevelDB总览"},{"content":"compact的作用 DB有一个后台线程负责将memtable持久化成sstable，以及均衡整个DB各个level层的sstable。compact分为minor compaction和major compaction。memtable持久化成sstable称为minor compaction，level(n)和level(n+1)之间某些sstable的merge称为major compaction。\ncompact的时机 compact的种类 Major Compaction 指的是 immutable memtable持久化为 sst 文件\n主要流程：\n  将内存中的memsstable格式化成sst文件的格式；\n  选择这个新sst文件放置的level，规则如图 2 所示（来自文献 [2]）；\n  将新sst文件放置到第2步选出的level中。\n  Major Compaction 指的是 sst 文件之间的 compaction\n主要分为：\n  Manual Compaction，是人工触发的Compaction，由外部接口调用产生\n  Size Compaction，是根据每个level的总文件大小来触发\n第1步：计算的score值，可以得出 max score，从而得出了应该哪一个 level 上进行 Compact，\n第2步：假设上面选出的是 level n，那么第 2 步就是选择出需要 Compact 的文件，其包含两步，首先在 level n 中选出需要 Compact 的文件文件（对应第2.1步）；然后根据level n选出的文件的key的begin和end来选出 level n+1 层的 sst 文件（对应第2.2步）：\n​ 第2.1步：确定level n参与Compact的文件列表\n​ 2.1.1: 将begin key更新为level n 上次Compact操作的文件的largest key。然后顺序查找level的sst文件，返回第一个largest key \u0026gt; begin key的sst文件，并加入到level n需要Compact的文件列表中；\n2.1.2: 如果是n==0，把sst文件都检查一遍，如果存在重叠则加入Compact文件列表中。因为level 0中，所有的文件之间都有可能存在重叠（overlap）。\n​ 第2.2步：确定level n+1参与Compact的文件列表；\n​ 2.2.1: 计算出level n参与Compact的文件列表的所有sst文件的总和key范围的begin和end；\n​ 2.2.2: 根据2.2.1计算出来的begin和end，去获取根level n+1有重叠（overlap）的sst文件列表；\n​ 2.2.3: 计算当前的level n 和 n+1参与Compact的两个文件列表的总和，如果小于阈值kExpandedCompactionByteSizeLimit=50M，那么会继续尝试在level n中选择出合适的sst文件，考虑到不影响理解，具体细节暂时省略。\n  Seek Compaction，每个文件的 seek miss 次数都有一个阈值，如果超过了这个阈值，那么认为这个文件需要Compact\n在levelDB中，每一个新的sst文件，都有一个 allowed_seek 的初始阈值，表示最多容忍 seek miss 多少次，每个调用 Get seek miss 的时候，就会执行减1（allowed_seek\u0026ndash;）。其中 allowed_seek 的初始阈值的计算方式为：\nallowed_seeks = (sst文件的file size / 16384); // 16348——16kb if ( allowed_seeks \u0026lt; 100 ) allowed_seeks = 100; LevelDB认为如果一个 sst 文件在 level i 中总是没总到，而是在 level i+1 中找到，那么当这种 seek miss 积累到一定次数之后，就考虑将其从 level i 中合并到 level i+1 中，这样可以避免不必要的 seek miss 消耗 read I/O。当然在引入布隆过滤器后，这种查找消耗的 IO 就会变小很多。\n执行条件 当 allowed_seeks 递减到小于0了，那么将标记为需要Compact的文件。但是由于Size Compaction的优先级高于Seek Compaction，所以在不存在Size Compaction的时候，且触发了Compaction，那么Seek Compaction就能执行。\n核心过程 计算 sst 的 allowed_seek 都是在 sst 刚开始新建的时候完成；而每次 Get（key）操作都会更新 allowed_seek，当allowed_seeks 递减到小于0了，那么将标记为需要 Compact 的文件。\n  compact的具体实现 func openDB(s *session) (*DB, error) { ... if readOnly { db.SetReadOnly() } else { db.closeW.Add(2) go db.tCompaction() go db.mCompaction() // go db.jWriter() \t} ... }  mCompaction对应着将immutable持久化成sstable tCompaction则是对sstable之间的compact  mCompaction func (db *DB) mCompaction() { ... for { select { case x = \u0026lt;-db.mcompCmdC: switch x.(type) { case cAuto: db.memCompaction() x.ack(nil) x = nil default: panic(\u0026#34;leveldb: unknown command\u0026#34;) } case \u0026lt;-db.closeC: return } } }  mCompaction工作在一个独立的协程中，接收mcompCmdC命令，执行memCompaction操作  memCompaction func (db *DB) memCompaction() { // 拿到immutable \tmdb := db.getFrozenMem() ... // 开启一个 \u0026#34;memdb@flush\u0026#34; 事 \t// Generate tables. \tdb.compactionTransactFunc(\u0026#34;memdb@flush\u0026#34;, func(cnt *compactionTransactCounter) (err error) { stats.startTimer() // 在事务中处理memdb \tflushLevel, err = db.s.flushMemdb(rec, mdb.DB, db.memdbMaxLevel) stats.stopTimer() return }, func() error { for _, r := range rec.addedTables { db.logf(\u0026#34;memdb@flush revert @%d\u0026#34;, r.num) if err := db.s.stor.Remove(storage.FileDesc{Type: storage.TypeTable, Num: r.num}); err != nil { return err } } return nil }) ... } compactionTransactFunc func (db *DB) compactionTransactFunc(name string, run func(cnt *compactionTransactCounter) error, revert func() error) { db.compactionTransact(name, \u0026amp;compactionTransactFunc{run, revert}) }  定义了事务的执行方法run以及事务的恢复方法revert  flushMemdb func (s *session) flushMemdb(rec *sessionRecord, mdb *memdb.DB, maxLevel int) (int, error) { // Create sorted table. \titer := mdb.NewIterator(nil) defer iter.Release() t, n, err := s.tops.createFrom(iter) if err != nil { return 0, err } // 生成的sstable需要被放入哪一层有一套判断方式 \tflushLevel := s.pickMemdbLevel(t.imin.ukey(), t.imax.ukey(), maxLevel) rec.addTableFile(flushLevel, t) s.logf(\u0026#34;memdb@flush created L%d@%d N·%d S·%s %q:%q\u0026#34;, flushLevel, t.fd.Num, n, shortenb(int(t.size)), t.imin, t.imax) return flushLevel, nil } pickMemdbLevel func (v *version) pickMemdbLevel(umin, umax []byte, maxLevel int) (level int) { if maxLevel \u0026gt; 0 { if len(v.levels) == 0 { return maxLevel } if !v.levels[0].overlaps(v.s.icmp, umin, umax, true) { var overlaps tFiles for ; level \u0026lt; maxLevel; level++ { if pLevel := level + 1; pLevel \u0026gt;= len(v.levels) { return maxLevel } else if v.levels[pLevel].overlaps(v.s.icmp, umin, umax, false) { break } if gpLevel := level + 2; gpLevel \u0026lt; len(v.levels) { overlaps = v.levels[gpLevel].getOverlaps(overlaps, v.s.icmp, umin, umax, false) if overlaps.size() \u0026gt; int64(v.s.o.GetCompactionGPOverlaps(level)) { break } } } } } return } tCompaction func (db *DB) tCompaction() { var ( x cCmd waitQ []cCmd ) ... for { ... if x != nil { switch cmd := x.(type) { case cAuto: if cmd.ackC != nil { // Check the write pause state before caching it. \tif db.resumeWrite() { x.ack(nil) } else { waitQ = append(waitQ, x) } } case cRange: x.ack(db.tableRangeCompaction(cmd.level, cmd.min, cmd.max)) default: panic(\u0026#34;leveldb: unknown command\u0026#34;) } x = nil } db.tableAutoCompaction() } } tableRangeCompaction func (db *DB) tableRangeCompaction(level int, umin, umax []byte) error { db.logf(\u0026#34;table@compaction range L%d %q:%q\u0026#34;, level, umin, umax) if level \u0026gt;= 0 { if c := db.s.getCompactionRange(level, umin, umax, true); c != nil { db.tableCompaction(c, true) } } else { // 循环直到没有内容需要合并 \t// Retry until nothing to compact. \tfor { compacted := false // Scan for maximum level with overlapped tables. \tv := db.s.version() m := 1 for i := m; i \u0026lt; len(v.levels); i++ { tables := v.levels[i] if tables.overlaps(db.s.icmp, umin, umax, false) { m = i } } v.release() for level := 0; level \u0026lt; m; level++ { if c := db.s.getCompactionRange(level, umin, umax, false); c != nil { db.tableCompaction(c, true) compacted = true } } if !compacted { break } } } return nil } tableCompaction func (db *DB) tableCompaction(c *compaction, noTrivial bool) { defer c.release() rec := \u0026amp;sessionRecord{} rec.addCompPtr(c.sourceLevel, c.imax) if !noTrivial \u0026amp;\u0026amp; c.trivial() { t := c.levels[0][0] db.logf(\u0026#34;table@move L%d@%d -\u0026gt; L%d\u0026#34;, c.sourceLevel, t.fd.Num, c.sourceLevel+1) rec.delTable(c.sourceLevel, t.fd.Num) rec.addTableFile(c.sourceLevel+1, t) db.compactionCommit(\u0026#34;table-move\u0026#34;, rec) return } var stats [2]cStatStaging for i, tables := range c.levels { for _, t := range tables { stats[i].read += t.size // Insert deleted tables into record \trec.delTable(c.sourceLevel+i, t.fd.Num) } } sourceSize := int(stats[0].read + stats[1].read) minSeq := db.minSeq() db.logf(\u0026#34;table@compaction L%d·%d -\u0026gt; L%d·%d S·%s Q·%d\u0026#34;, c.sourceLevel, len(c.levels[0]), c.sourceLevel+1, len(c.levels[1]), shortenb(sourceSize), minSeq) b := \u0026amp;tableCompactionBuilder{ db: db, s: db.s, c: c, rec: rec, stat1: \u0026amp;stats[1], minSeq: minSeq, strict: db.s.o.GetStrict(opt.StrictCompaction), tableSize: db.s.o.GetCompactionTableSize(c.sourceLevel + 1), } db.compactionTransact(\u0026#34;table@build\u0026#34;, b) // Commit. \tstats[1].startTimer() db.compactionCommit(\u0026#34;table\u0026#34;, rec) stats[1].stopTimer() resultSize := int(stats[1].write) db.logf(\u0026#34;table@compaction committed F%s S%s Ke·%d D·%d T·%v\u0026#34;, sint(len(rec.addedTables)-len(rec.deletedTables)), sshortenb(resultSize-sourceSize), b.kerrCnt, b.dropCnt, stats[1].duration) // Save compaction stats \tfor i := range stats { db.compStats.addStat(c.sourceLevel+1, \u0026amp;stats[i]) } switch c.typ { case level0Compaction: atomic.AddUint32(\u0026amp;db.level0Comp, 1) case nonLevel0Compaction: atomic.AddUint32(\u0026amp;db.nonLevel0Comp, 1) case seekCompaction: atomic.AddUint32(\u0026amp;db.seekComp, 1) } } ","permalink":"https://lambertxiao.github.io/posts/leveldb-compaction/doc/","summary":"了解一下leveldb的WAL是怎么做的","title":"leveldb内部实现之compact"},{"content":"Journal是什么 journal是levedb中的WAL的实现，由于leveldb是将数据先写入内存中再同步到磁盘的，为了防止db异常退出导致内存丢数据，leveldb每次在写入key之前，会利用顺序写文件的方式记录journal。 因此一个journal会记录下一次写入操作的数据。\n 以下代码分析基于go版本的leveldb https://github.com/syndtr/goleveldb\n journal的写入时机 Put写入数据时，最终会走到的writeLocked方法里的以下代码块\n// ourBatch is batch that we can modify. func (db *DB) writeLocked(batch, ourBatch *Batch, merge, sync bool) error { ... // Seq number. \tseq := db.seq + 1 // 在这先写日志 \t// Write journal. \tif err := db.writeJournal(batches, seq, sync); err != nil { db.unlockWrite(overflow, merged, err) return err } // Put batches. \tfor _, batch := range batches { if err := batch.putMem(seq, mdb.DB); err != nil { panic(err) } seq += uint64(batch.Len()) } ... return nil } journal的结构 先分析下journal的实现，在DB结构体中，journal的类型为 *journal.Writer\ntype DB struct { ... journal *journal.Writer // journalWriter storage.Writer \t// journalFd storage.FileDesc \t... } // Writer writes journals to an underlying io.Writer. type Writer struct { // w is the underlying writer. \tw io.Writer // seq is the sequence number of the current journal. \tseq int // f is w as a flusher. \tf flusher // buf[i:j] is the bytes that will become the current chunk. \t// The low bound, i, includes the chunk header. \ti, j int // buf[:written] has already been written to w. \t// written is zero unless Flush has been called. \twritten int // first is whether the current chunk is the first chunk of the journal. \tfirst bool // pending is whether a chunk is buffered but not yet written. \tpending bool // err is any accumulated error. \terr error // buf is the buffer. \tbuf [blockSize]byte }   成员变量w实际上会在打开DB时被传入一个文件，见以下代码，可以看出写入的journal文件实际是 ${dbpath}/%06d.log\nfunc (fs *fileStorage) Create(fd FileDesc) (Writer, error) { ... of, err := os.OpenFile(filepath.Join(fs.path, fsGenName(fd)), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return nil, err } fs.open++ return \u0026amp;fileWrap{File: of, fs: fs, fd: fd}, nil } func fsGenName(fd FileDesc) string { switch fd.Type { case TypeManifest: return fmt.Sprintf(\u0026#34;MANIFEST-%06d\u0026#34;, fd.Num) case TypeJournal: return fmt.Sprintf(\u0026#34;%06d.log\u0026#34;, fd.Num) case TypeTable: return fmt.Sprintf(\u0026#34;%06d.ldb\u0026#34;, fd.Num) case TypeTemp: return fmt.Sprintf(\u0026#34;%06d.tmp\u0026#34;, fd.Num) default: panic(\u0026#34;invalid file type\u0026#34;) } } func OpenFile(path string, readOnly bool) (Storage, error) { ... fs := \u0026amp;fileStorage{ path: path, readOnly: readOnly, flock: flock, logw: logw, logSize: logSize, } ... }   每个journal都有一个序列号seq\n  buf存放着journal的数据\n  一个journal可以拆分为多个chunk, i和j标记当前写入的chunk的左右边界\n  first标记当前写入的chunk是否是第一个chunk\n  pending表示当前的chunk是否还没被写入磁盘\n  journal的写入 writeJournal func (db *DB) writeJournal(batches []*Batch, seq uint64, sync bool) error { // 拿到本次负责写入的writer \twr, err := db.journal.Next() if err != nil { return err } // 将数据写入writer，此时数据还在内存缓冲区中 \tif err := writeBatchesWithHeader(wr, batches, seq); err != nil { return err } // 将journal内的数据刷入磁盘 \tif err := db.journal.Flush(); err != nil { return err } if sync { return db.journalWriter.Sync() } return nil }  通过journal.Next拿到一个writer writeBatchesWithHeader将batches写入writer中  journal.Next const ( blockSize = 32 * 1024 headerSize = 7 ) // Next returns a writer for the next journal. The writer returned becomes stale // after the next Close, Flush or Next call, and should no longer be used. func (w *Writer) Next() (io.Writer, error) { w.seq++ if w.err != nil { return nil, w.err } if w.pending { w.fillHeader(true) } w.i = w.j w.j = w.j + headerSize // Check if there is room in the block for the header. \tif w.j \u0026gt; blockSize { // Fill in the rest of the block with zeroes. \tfor k := w.i; k \u0026lt; blockSize; k++ { w.buf[k] = 0 } w.writeBlock() if w.err != nil { return nil, w.err } } w.first = true w.pending = true return singleWriter{w, w.seq}, nil } func (x singleWriter) Write(p []byte) (int, error) { w := x.w if w.seq != x.seq { return 0, errors.New(\u0026#34;leveldb/journal: stale writer\u0026#34;) } if w.err != nil { return 0, w.err } n0 := len(p) for len(p) \u0026gt; 0 { // Write a block, if it is full. \tif w.j == blockSize { w.fillHeader(false) w.writeBlock() if w.err != nil { return 0, w.err } w.first = false } // Copy bytes into the buffer. \tn := copy(w.buf[w.j:], p) w.j += n p = p[n:] } return n0, nil }  当next调用的时候，检查journal里上一个写入的chunk是否处于pending状态，如果是，则fillHeader往buf写入该chunk的header信息 更新当前写入chunk的左右边界i，j 如果因为本次写入导致buf里的数据已经大于32KB了，则需要将buf里的数据写入到文件里 由此可见，journal写入block的时机实际上是在每次写入的过程中判断的 buf[i:i+6] 这块位置用来存放chunk的header，buf[i:j] 用来存放数据，buf[:written]是实际已经写入的数据  fillHeader const ( fullChunkType = 1 // 整个buf里只有一个chunk \tfirstChunkType = 2 // 第一块chunk \tmiddleChunkType = 3 // 位于中间的chunk \tlastChunkType = 4 // 最后一块chunk ) // fillHeader fills in the header for the pending chunk. func (w *Writer) fillHeader(last bool) { if w.i+headerSize \u0026gt; w.j || w.j \u0026gt; blockSize { panic(\u0026#34;leveldb/journal: bad writer state\u0026#34;) } if last { if w.first { w.buf[w.i+6] = fullChunkType } else { w.buf[w.i+6] = lastChunkType } } else { if w.first { w.buf[w.i+6] = firstChunkType } else { w.buf[w.i+6] = middleChunkType } } // 存入crc校验码 \tbinary.LittleEndian.PutUint32(w.buf[w.i+0:w.i+4], util.NewCRC(w.buf[w.i+6:w.j]).Value()) // 存入chunk的大小 \tbinary.LittleEndian.PutUint16(w.buf[w.i+4:w.i+6], uint16(w.j-w.i-headerSize)) }  chunk有header，header里记录里chunk的类型 chunk存放着数据部分的CRC校验码，用于检查数据是否完整 chunk里存放这数据大小 对于buf, 索引0-3存放chunk数据的crc，索引4-5存放chunk数据的大小，索引6存放着chunk的类型  writeBlock // writeBlock writes the buffered block to the underlying writer, and reserves // space for the next chunk\u0026#39;s header. func (w *Writer) writeBlock() { _, w.err = w.w.Write(w.buf[w.written:]) w.i = 0 w.j = headerSize w.written = 0 } writeBatchesWithHeader func writeBatchesWithHeader(wr io.Writer, batches []*Batch, seq uint64) error { if _, err := wr.Write(encodeBatchHeader(nil, seq, batchesLen(batches))); err != nil { return err } for _, batch := range batches { if _, err := wr.Write(batch.data); err != nil { return err } } return nil } const ( batchHeaderLen = 8 + 4 ) func encodeBatchHeader(dst []byte, seq uint64, batchLen int) []byte { dst = ensureBuffer(dst, batchHeaderLen) binary.LittleEndian.PutUint64(dst, seq) binary.LittleEndian.PutUint32(dst[8:], uint32(batchLen)) return dst } func ensureBuffer(b []byte, n int) []byte { if cap(b) \u0026lt; n { return make([]byte, n) } return b[:n] }  开辟了一个长度为12的buff, 前8位写入seq, 后4位写入batch的长度。并将该buff写入磁盘 将每一个batch的数据写入磁盘  journal.Flush // Flush finishes the current journal, writes to the underlying writer, and // flushes it if that writer implements interface{ Flush() error }. func (w *Writer) Flush() error { w.seq++ w.writePending() if w.err != nil { return w.err } if w.f != nil { w.err = w.f.Flush() return w.err } return nil }  当前journal写入完成了, seq自增 检查是否还有pending的chunk，有的话则写入磁盘  writePending // writePending finishes the current journal and writes the buffer to the // underlying writer. func (w *Writer) writePending() { if w.err != nil { return } if w.pending { w.fillHeader(true) w.pending = false } _, w.err = w.w.Write(w.buf[w.written:w.j]) w.written = w.j } journal的读取时机 我们知道，journal是用于恢复内存数据的，因此在服务宕机重启时会恢复jounal\nfunc (db *DB) recoverJournal() error { // 读取journal文件并排序 \t// Get all journals and sort it by file number. \trawFds, err := db.s.stor.List(storage.TypeJournal) if err != nil { return err } sortFds(rawFds) // 准备好需要恢复的journal \t// Journals that will be recovered. \tvar fds []storage.FileDesc for _, fd := range rawFds { if fd.Num \u0026gt;= db.s.stJournalNum || fd.Num == db.s.stPrevJournalNum { fds = append(fds, fd) } } var ( ofd storage.FileDesc // Obsolete file. \trec = \u0026amp;sessionRecord{} ) // Recover journals. \tif len(fds) \u0026gt; 0 { db.logf(\u0026#34;journal@recovery F·%d\u0026#34;, len(fds)) // Mark file number as used. \tdb.s.markFileNum(fds[len(fds)-1].Num) var ( // Options. \tstrict = db.s.o.GetStrict(opt.StrictJournal) checksum = db.s.o.GetStrict(opt.StrictJournalChecksum) writeBuffer = db.s.o.GetWriteBuffer() jr *journal.Reader mdb = memdb.New(db.s.icmp, writeBuffer) buf = \u0026amp;util.Buffer{} batchSeq uint64 batchLen int ) for _, fd := range fds { db.logf(\u0026#34;journal@recovery recovering @%d\u0026#34;, fd.Num) fr, err := db.s.stor.Open(fd) if err != nil { return err } // Create or reset journal reader instance. \tif jr == nil { jr = journal.NewReader(fr, dropper{db.s, fd}, strict, checksum) } else { jr.Reset(fr, dropper{db.s, fd}, strict, checksum) } // Flush memdb and remove obsolete journal file. \tif !ofd.Zero() { if mdb.Len() \u0026gt; 0 { if _, err := db.s.flushMemdb(rec, mdb, 0); err != nil { fr.Close() return err } } rec.setJournalNum(fd.Num) rec.setSeqNum(db.seq) if err := db.s.commit(rec, false); err != nil { fr.Close() return err } rec.resetAddedTables() db.s.stor.Remove(ofd) ofd = storage.FileDesc{} } // 将journal的内容回放到memdb \t// Replay journal to memdb. \tmdb.Reset() for { r, err := jr.Next() if err != nil { if err == io.EOF { break } fr.Close() return errors.SetFd(err, fd) } buf.Reset() if _, err := buf.ReadFrom(r); err != nil { if err == io.ErrUnexpectedEOF { // This is error returned due to corruption, with strict == false. \tcontinue } fr.Close() return errors.SetFd(err, fd) } batchSeq, batchLen, err = decodeBatchToMem(buf.Bytes(), db.seq, mdb) if err != nil { if !strict \u0026amp;\u0026amp; errors.IsCorrupted(err) { db.s.logf(\u0026#34;journal error: %v (skipped)\u0026#34;, err) // We won\u0026#39;t apply sequence number as it might be corrupted. \tcontinue } fr.Close() return errors.SetFd(err, fd) } // Save sequence number. \tdb.seq = batchSeq + uint64(batchLen) // Flush it if large enough. \tif mdb.Size() \u0026gt;= writeBuffer { if _, err := db.s.flushMemdb(rec, mdb, 0); err != nil { fr.Close() return err } mdb.Reset() } } fr.Close() ofd = fd } // Flush the last memdb. \tif mdb.Len() \u0026gt; 0 { if _, err := db.s.flushMemdb(rec, mdb, 0); err != nil { return err } } } // Create a new journal. \tif _, err := db.newMem(0); err != nil { return err } // Commit. \trec.setJournalNum(db.journalFd.Num) rec.setSeqNum(db.seq) if err := db.s.commit(rec, false); err != nil { // Close journal on error. \tif db.journal != nil { db.journal.Close() db.journalWriter.Close() } return err } // Remove the last obsolete journal file. \tif !ofd.Zero() { db.s.stor.Remove(ofd) } return nil } singleReader.Read type singleReader struct { r *Reader seq int err error } func (x *singleReader) Read(p []byte) (int, error) { r := x.r if r.seq != x.seq { return 0, errors.New(\u0026#34;leveldb/journal: stale reader\u0026#34;) } if x.err != nil { return 0, x.err } if r.err != nil { return 0, r.err } for r.i == r.j { if r.last { return 0, io.EOF } x.err = r.nextChunk(false) if x.err != nil { if x.err == errSkip { x.err = io.ErrUnexpectedEOF } return 0, x.err } } n := copy(p, r.buf[r.i:r.j]) r.i += n return n, nil } nextChunk // nextChunk sets r.buf[r.i:r.j] to hold the next chunk\u0026#39;s payload, reading the // next block into the buffer if necessary. func (r *Reader) nextChunk(first bool) error { for { if r.j+headerSize \u0026lt;= r.n { checksum := binary.LittleEndian.Uint32(r.buf[r.j+0 : r.j+4]) length := binary.LittleEndian.Uint16(r.buf[r.j+4 : r.j+6]) chunkType := r.buf[r.j+6] unprocBlock := r.n - r.j if checksum == 0 \u0026amp;\u0026amp; length == 0 \u0026amp;\u0026amp; chunkType == 0 { // Drop entire block. \tr.i = r.n r.j = r.n return r.corrupt(unprocBlock, \u0026#34;zero header\u0026#34;, false) } if chunkType \u0026lt; fullChunkType || chunkType \u0026gt; lastChunkType { // Drop entire block. \tr.i = r.n r.j = r.n return r.corrupt(unprocBlock, fmt.Sprintf(\u0026#34;invalid chunk type %#x\u0026#34;, chunkType), false) } r.i = r.j + headerSize r.j = r.j + headerSize + int(length) if r.j \u0026gt; r.n { // Drop entire block. \tr.i = r.n r.j = r.n return r.corrupt(unprocBlock, \u0026#34;chunk length overflows block\u0026#34;, false) } else if r.checksum \u0026amp;\u0026amp; checksum != util.NewCRC(r.buf[r.i-1:r.j]).Value() { // Drop entire block. \tr.i = r.n r.j = r.n return r.corrupt(unprocBlock, \u0026#34;checksum mismatch\u0026#34;, false) } if first \u0026amp;\u0026amp; chunkType != fullChunkType \u0026amp;\u0026amp; chunkType != firstChunkType { chunkLength := (r.j - r.i) + headerSize r.i = r.j // Report the error, but skip it. \treturn r.corrupt(chunkLength, \u0026#34;orphan chunk\u0026#34;, true) } r.last = chunkType == fullChunkType || chunkType == lastChunkType return nil } // The last block. \tif r.n \u0026lt; blockSize \u0026amp;\u0026amp; r.n \u0026gt; 0 { if !first { return r.corrupt(0, \u0026#34;missing chunk part\u0026#34;, false) } r.err = io.EOF return r.err } // Read block. \tn, err := io.ReadFull(r.r, r.buf[:]) if err != nil \u0026amp;\u0026amp; err != io.EOF \u0026amp;\u0026amp; err != io.ErrUnexpectedEOF { return err } if n == 0 { if !first { return r.corrupt(0, \u0026#34;missing chunk part\u0026#34;, false) } r.err = io.EOF return r.err } r.i, r.j, r.n = 0, 0, n } } ","permalink":"https://lambertxiao.github.io/posts/leveldb-journal/doc/","summary":"了解一下leveldb的WAL是怎么做的","title":"leveldb内部实现之journal"},{"content":"leveldb里的memdb使用跳表来实现的，不同于常见的使用链表来实现的跳表，memdb是使用数组来模拟的\n 以下代码分析基于go版本的leveldb https://github.com/syndtr/goleveldb\n MemDB的结构 const ( nKV = iota nKey nVal nHeight nNext ) const tMaxHeight = 12 // DB is an in-memory key/value database. type DB struct { cmp comparer.BasicComparer rnd *rand.Rand mu sync.RWMutex kvData []byte // Node data: \t// [0] : KV offset \t// [1] : Key length \t// [2] : Value length \t// [3] : Height \t// [3..height] : Next nodes \tnodeData []int prevNode [tMaxHeight]int maxHeight int n int kvSize int } // New creates a new initialized in-memory key/value DB. The capacity // is the initial key/value buffer capacity. The capacity is advisory, // not enforced. // // This DB is append-only, deleting an entry would remove entry node but not // reclaim KV buffer. // // The returned DB instance is safe for concurrent use. func New(cmp comparer.BasicComparer, capacity int) *DB { p := \u0026amp;DB{ cmp: cmp, rnd: rand.New(rand.NewSource(0xdeadbeef)), maxHeight: 1, kvData: make([]byte, 0, capacity), nodeData: make([]int, 4+tMaxHeight), } p.nodeData[nHeight] = tMaxHeight return p } 三个重要的结构分别是kvData，nodeData，prevNode，下面通过Put操作来解释这三者的作用\n如何Put一个键值对 先通过一幅图理解一下Put的流程\n// 假设现在要插入17 // 从最左边的head节点开始，当前层高是4； // head节点在第4层的next节点的key是6，由于 17 大于6，所以在当前节点的右边，就沿着当前层的链表走到下一节点，也就是key是6节点。 // 6节点 在第4层的next节点是NIL，也就是后面没有节点了，那么就需要在当前节点往下层走，走到第3层。 // 6节点 在第3层的next节点的key是25，由于 17 小于25，那么就需要在当前节点往下层走，走到第2层。 // 6节点 在第2层的next节点的key是9，由于 17 大于9，那么就沿着当前层的链表走到下一节点，也就是key是9的节点。 // 9节点 在第2层的nex节点的key是25，由于 17 小于25，那么就需要在当前节点往下层走，走到第1层。 // 9节点 在第1层的next节点的key是12，由于 17 大于12，那么就沿着当前层的链表走到下一节点，也就是key是12的节点。 // 12节点 在第1层的next节点的key是19，由于 17 小于19，本来应该要继续走到下一层，但是由于当前已经是最后一层了，所以直接返回12的next节点，也就是19节点 此时我们再回过头看代码实现，看看kvData，nodeData，prevNode都存着什么东西\nfunc (p *DB) Put(key []byte, value []byte) error { // 访问需加锁 \tp.mu.Lock() defer p.mu.Unlock() // 这里即是找到节点19的过程，这里返回的node是个偏移量，exact在找到一样key的node时会返回true \tif node, exact := p.findGE(key, true); exact { // 偏移量为原先数组的长度 \tkvOffset := len(p.kvData) // 将key和value放入kvData \tp.kvData = append(p.kvData, key...) p.kvData = append(p.kvData, value...) // 更新node的偏移量 \tp.nodeData[node] = kvOffset // 更新node的value长度 \tm := p.nodeData[node+nVal] p.nodeData[node+nVal] = len(value) // 更新kvsize的大小 \tp.kvSize += len(value) - m return nil } // 如果没有找到相同key的node，那么就需要插入一个新node \t// 随机一个高度h，新node要加入第1层到第h层 \th := p.randHeight() if h \u0026gt; p.maxHeight { // 如果随机到的高度比当前的最大高度大，将[maxHeight, h]范围内的prevNode置为0？ \tfor i := p.maxHeight; i \u0026lt; h; i++ { p.prevNode[i] = 0 } // 更新最大高度 \tp.maxHeight = h } kvOffset := len(p.kvData) // 更新kvdata \tp.kvData = append(p.kvData, key...) p.kvData = append(p.kvData, value...) // Node \tnode := len(p.nodeData) // nodeData中依次放入偏移量，key的长度，value的长度和高度 \tp.nodeData = append(p.nodeData, kvOffset, len(key), len(value), h) // prevNode[i]表示 新加入的node在第i层的前驱节点，这些前驱节点会在findGE时被加入到prevNode中 \tfor i, n := range p.prevNode[:h] { // m位置是前驱节点记录后继节点偏移的下标 \tm := n + nNext + i // 所以p.nodeData[m]是原先前驱节点的后继节点，这里就成为了新加入节点的后继节点了，可以想象一下在A-\u0026gt;C中插入B，B的后继为C，A的后继改为B \tp.nodeData = append(p.nodeData, p.nodeData[m]) // 更新前驱节点的后继节点为当前节点 \tp.nodeData[m] = node } p.kvSize += len(key) + len(value) p.n++ return nil } 由上可知\n nodeData里的node结构主要包含偏移量，key长度，value长度，高度, 以及后继节点位置的集合(如果一个节点在第4层，那么它在每一层都有一个后继节点) kvData先存入key的内容，再存入value的内容 prevNode存放的是临时节点，每次查找新节点的插入位置时，保存每一层的前驱节点，然后在插入新节点时，将这些前驱节点和新加入的节点相关联  每次插入节点时随机一个高度\nfunc (p *DB) randHeight() (h int) { const branching = 4 h = 1 for h \u0026lt; tMaxHeight \u0026amp;\u0026amp; p.rnd.Int()%branching == 0 { h++ } return } 如何查找某个Key的插入位置 再看看如何查找一个key的插入位置\n// Must hold RW-lock if prev == true, as it use shared prevNode slice. func (p *DB) findGE(key []byte, prev bool) (int, bool) { node := 0 // 从跳表顶部自顶向下开始找 \th := p.maxHeight - 1 for { // nNext等于4，这里每次从最大高度的偏移量开始找 \tnext := p.nodeData[node+nNext+h] cmp := 1 if next != 0 { o := p.nodeData[next] // 找到一个节点的key和当前的key进行比较 \tcmp = p.cmp.Compare(p.kvData[o:o+p.nodeData[next+nKey]], key) } // 如果当前的node的key小于要插入的key，则继续往后找 \tif cmp \u0026lt; 0 { // Keep searching in this list \tnode = next } else { // 对于插入或删除而进行的搜索，即使遇到相同的也要继续往下一层比较 \tif prev { // 将找到的节点放置到prevNode的高度中 \tp.prevNode[h] = node } else if cmp == 0 { // 如果找到一样的key，返回这个node \treturn next, true } // 如果已经在最底层了 \tif h == 0 { return next, cmp == 0 } // 往下一层找 \th-- } } }  找到一个node，node的key大于等于当前要插入的key 从最高层往下找，一层一层搜索，比较大小的方法通过comparer接口实现  如何查找一个key的前驱节点 func (p *DB) findLT(key []byte) int { node := 0 h := p.maxHeight - 1 for { next := p.nodeData[node+nNext+h] o := p.nodeData[next] if next == 0 || p.cmp.Compare(p.kvData[o:o+p.nodeData[next+nKey]], key) \u0026gt;= 0 { if h == 0 { break } h-- } else { node = next } } return node } 如何查找最后一个节点 func (p *DB) findLast() int { node := 0 h := p.maxHeight - 1 for { next := p.nodeData[node+nNext+h] if next == 0 { if h == 0 { break } h-- } else { node = next } } return node } 如何删除一个节点 // Delete deletes the value for the given key. It returns ErrNotFound if // the DB does not contain the key. // // It is safe to modify the contents of the arguments after Delete returns. func (p *DB) Delete(key []byte) error { p.mu.Lock() defer p.mu.Unlock() node, exact := p.findGE(key, true) if !exact { return ErrNotFound } h := p.nodeData[node+nHeight] for i, n := range p.prevNode[:h] { m := n + nNext + i p.nodeData[m] = p.nodeData[p.nodeData[m]+nNext+i] } p.kvSize -= p.nodeData[node+nKey] + p.nodeData[node+nVal] p.n-- return nil }  删除操作和插入操作相反，需要更新要删除节点后面的节点 更新被删除节点的前驱节点的后继节点位置 可以看出，被删除的节点其实仍然在kvData和nodeData里面，只是不再有前继续节点指向它们  其余功能 // Contains returns true if the given key are in the DB. // // It is safe to modify the contents of the arguments after Contains returns. func (p *DB) Contains(key []byte) bool { p.mu.RLock() _, exact := p.findGE(key, false) p.mu.RUnlock() return exact } // Get gets the value for the given key. It returns error.ErrNotFound if the // DB does not contain the key. // // The caller should not modify the contents of the returned slice, but // it is safe to modify the contents of the argument after Get returns. func (p *DB) Get(key []byte) (value []byte, err error) { p.mu.RLock() if node, exact := p.findGE(key, false); exact { o := p.nodeData[node] + p.nodeData[node+nKey] value = p.kvData[o : o+p.nodeData[node+nVal]] } else { err = ErrNotFound } p.mu.RUnlock() return } // Find finds key/value pair whose key is greater than or equal to the // given key. It returns ErrNotFound if the table doesn\u0026#39;t contain // such pair. // // The caller should not modify the contents of the returned slice, but // it is safe to modify the contents of the argument after Find returns. func (p *DB) Find(key []byte) (rkey, value []byte, err error) { p.mu.RLock() if node, _ := p.findGE(key, false); node != 0 { n := p.nodeData[node] m := n + p.nodeData[node+nKey] rkey = p.kvData[n:m] value = p.kvData[m : m+p.nodeData[node+nVal]] } else { err = ErrNotFound } p.mu.RUnlock() return } // NewIterator returns an iterator of the DB. // The returned iterator is not safe for concurrent use, but it is safe to use // multiple iterators concurrently, with each in a dedicated goroutine. // It is also safe to use an iterator concurrently with modifying its // underlying DB. However, the resultant key/value pairs are not guaranteed // to be a consistent snapshot of the DB at a particular point in time. // // Slice allows slicing the iterator to only contains keys in the given // range. A nil Range.Start is treated as a key before all keys in the // DB. And a nil Range.Limit is treated as a key after all keys in // the DB. // // WARNING: Any slice returned by interator (e.g. slice returned by calling // Iterator.Key() or Iterator.Key() methods), its content should not be modified // unless noted otherwise. // // The iterator must be released after use, by calling Release method. // // Also read Iterator documentation of the leveldb/iterator package. func (p *DB) NewIterator(slice *util.Range) iterator.Iterator { return \u0026amp;dbIter{p: p, slice: slice} } // Capacity returns keys/values buffer capacity. func (p *DB) Capacity() int { p.mu.RLock() defer p.mu.RUnlock() return cap(p.kvData) } // Size returns sum of keys and values length. Note that deleted // key/value will not be accounted for, but it will still consume // the buffer, since the buffer is append only. func (p *DB) Size() int { p.mu.RLock() defer p.mu.RUnlock() return p.kvSize } // Free returns keys/values free buffer before need to grow. func (p *DB) Free() int { p.mu.RLock() defer p.mu.RUnlock() return cap(p.kvData) - len(p.kvData) } // Len returns the number of entries in the DB. func (p *DB) Len() int { p.mu.RLock() defer p.mu.RUnlock() return p.n } // Reset resets the DB to initial empty state. Allows reuse the buffer. func (p *DB) Reset() { p.mu.Lock() p.rnd = rand.New(rand.NewSource(0xdeadbeef)) p.maxHeight = 1 p.n = 0 p.kvSize = 0 p.kvData = p.kvData[:0] p.nodeData = p.nodeData[:nNext+tMaxHeight] p.nodeData[nKV] = 0 p.nodeData[nKey] = 0 p.nodeData[nVal] = 0 p.nodeData[nHeight] = tMaxHeight for n := 0; n \u0026lt; tMaxHeight; n++ { p.nodeData[nNext+n] = 0 p.prevNode[n] = 0 } p.mu.Unlock() } ","permalink":"https://lambertxiao.github.io/posts/leveldb-memdb/doc/","summary":"长成这样，真是看不出这是个跳表","title":"leveldb内部实现之memdb"},{"content":"136. 只出现一次的数字 136. 只出现一次的数字 给定一个非空整数数组，除了某个元素只出现一次以外，其余每个元素均出现两次。找出那个只出现了一次的元素。\nfunc singleNumber(nums []int) int { var ans int32 = 0 for i := 0; i \u0026lt; 32; i++ { var tmp int32 = 0 for _, num := range nums { // 对于每一个数的每一个二进制位分别累加  // 左移将需要计算的位置放到最右边，再按位与上1，目的是清空其他位的值  tmp += (int32(num) \u0026gt;\u0026gt; i) \u0026amp; 1 } // 由于存在一个元素仅出现一次，跟3取余后能得到目标元素的第i位上的值  if tmp % 2 != 0 { ans |= 1 \u0026lt;\u0026lt; i // 注意这里是利用1左移回去占位，并与ans按位加上  } } return int(ans) } 当然异或的方式也可以\nfunc singleNumber(nums []int) int { v := 0 for _, num := range nums { v = v ^ num } return v } 说明：你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗？\n137. 只出现一次的数字 II 137. 只出现一次的数字 II\n给你一个整数数组 nums ，除某个元素仅出现 一次 外，其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。\n假设对于数组 [4, 3, 3, 3, 2, 2, 2]，将所有的数看作二进制即\n0100 0011 0011 0011 0010 0010 0010 对于32位int，如果我们将每一个二进制位都加起来（相当于上方数组的每一列相加），我们最终会得到 0163, 由于其余的数都出现了三次，所以每一位都和3取余可以得到 0100, 即能得到4\nfunc singleNumber(nums []int) int { var ans int32 = 0 for i := 0; i \u0026lt; 32; i++ { var tmp int32 = 0 for _, num := range nums { // 对于每一个数的每一个二进制位分别累加  // 左移将需要计算的位置放到最右边，再按位与上1，目的是清空其他位的值  tmp += (int32(num) \u0026gt;\u0026gt; i) \u0026amp; 1 } // 由于存在一个元素仅出现一次，跟3取余后能得到目标元素的第i位上的值  if tmp % 3 != 0 { ans |= 1 \u0026lt;\u0026lt; i // 注意这里是利用1左移回去占位，并与ans按位加上  } } return int(ans) } 260. 只出现一次的数字 III 260. 只出现一次的数字 III\n给定一个整数数组 nums，其中恰好有两个元素只出现一次，其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。\n进阶：你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现？\n对于数字6和-6 原码 0000 0000 0000 0110 1000 0000 0000 0110 反码 0000 0000 0000 0110 1111 1111 1111 1001 补码 0000 0000 0000 0110 1111 1111 1111 1010 \u0026amp;后 0000 0000 0000 0010 可以看到 6 \u0026amp; -6 后，只保留了最末尾的1 func singleNumber(nums []int) []int { xorVal := 0 for _, num := range nums { xorVal ^= num } // \u0026amp;之后只保留了最末尾的1  val := xorVal \u0026amp; -xorVal n1, n2 := 0, 0 for _, num := range nums { // 这里其实是判断num在val最末尾1上的位是不是也是1  if num \u0026amp; val == 0 { n1 ^= num } else { n2 ^= num } } return []int{n1, n2} } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E6%95%B0%E5%AD%97/doc/","summary":"挺恶心的位运行","title":"算法-只出现一次的数字"},{"content":"举个例子 怎么接收一个http2请求 func main() { mux := http.NewServeMux() mux.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, \u0026#34;Hello h2c\u0026#34;) }) s := \u0026amp;http.Server{ Addr: \u0026#34;127.0.0.1:8080\u0026#34;, Handler: mux, } log.Fatal(s.ListenAndServeTLS(\u0026#34;server.crt\u0026#34;, \u0026#34;server.key\u0026#34;)) } 需要注意的是，http2是必须和tls一起开启，可看源码 /go/src/net/http/server.go\nfunc (c *conn) serve(ctx context.Context) { ... if tlsConn, ok := c.rwc.(*tls.Conn); ok { ... if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) { // 这里的fn就是http2的handler, 通过http2ConfigureServer方法注册 \tif fn := c.server.TLSNextProto[proto]; fn != nil { h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}} c.setState(c.rwc, StateActive, skipHooks) fn(c.server, tlsConn, h) } return } } // HTTP/1.x from here on. \t// 以下走http1.x的逻辑 \t... } 通过openssl工具生成证书\nopenssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt 怎么发起一个http2请求 func main() { client := http.Client{ Transport: \u0026amp;http.Transport{ ForceAttemptHTTP2: true, TLSClientConfig: \u0026amp;tls.Config{InsecureSkipVerify: true}, }, } resp, err := client.Get(\u0026#34;https://127.0.0.1:8080\u0026#34;) if err != nil { panic(err) } rdata, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(resp.Proto, string(rdata)) }  需要自定义一个Transport 打开ForceAttemptHTTP2 由于服务端的整数是自己生成的，所以需要设置跳过校验 最后resp里的proto会是 HTTP/2.0  // ForceAttemptHTTP2的定义  type Transport struct { ... // ForceAttemptHTTP2 controls whether HTTP/2 is enabled when a non-zero \t// Dial, DialTLS, or DialContext func or TLSClientConfig is provided. \t// By default, use of any those fields conservatively disables HTTP/2. \t// To use a custom dialer or TLS config and still attempt HTTP/2 \t// upgrades, set this to true. \tForceAttemptHTTP2 bool } 源码解析 go中http2相关的源码基本都在 /go/src/net/http/h2_bundle.go 文件中\nserver端 http2Server.ServeConn func (s *http2Server) ServeConn(c net.Conn, opts *http2ServeConnOpts) { baseCtx, cancel := http2serverConnBaseContext(c, opts) defer cancel() sc := \u0026amp;http2serverConn{ srv: s, hs: opts.baseConfig(), conn: c, baseCtx: baseCtx, remoteAddrStr: c.RemoteAddr().String(), bw: http2newBufferedWriter(c), handler: opts.handler(), streams: make(map[uint32]*http2stream), readFrameCh: make(chan http2readFrameResult), wantWriteFrameCh: make(chan http2FrameWriteRequest, 8), serveMsgCh: make(chan interface{}, 8), wroteFrameCh: make(chan http2frameWriteResult, 1), // buffered; one send in writeFrameAsync \tbodyReadCh: make(chan http2bodyReadMsg), // buffering doesn\u0026#39;t matter either way \tdoneServing: make(chan struct{}), clientMaxStreams: math.MaxUint32, // Section 6.5.2: \u0026#34;Initially, there is no limit to this value\u0026#34; \tadvMaxStreams: s.maxConcurrentStreams(), initialStreamSendWindowSize: http2initialWindowSize, maxFrameSize: http2initialMaxFrameSize, headerTableSize: http2initialHeaderTableSize, serveG: http2newGoroutineLock(), pushEnabled: true, } s.state.registerConn(sc) defer s.state.unregisterConn(sc) if sc.hs.WriteTimeout != 0 { sc.conn.SetWriteDeadline(time.Time{}) } if s.NewWriteScheduler != nil { sc.writeSched = s.NewWriteScheduler() } else { sc.writeSched = http2NewRandomWriteScheduler() } // These start at the RFC-specified defaults. If there is a higher \t// configured value for inflow, that will be updated when we send a \t// WINDOW_UPDATE shortly after sending SETTINGS. \tsc.flow.add(http2initialWindowSize) sc.inflow.add(http2initialWindowSize) sc.hpackEncoder = hpack.NewEncoder(\u0026amp;sc.headerWriteBuf) fr := http2NewFramer(sc.bw, c) fr.ReadMetaHeaders = hpack.NewDecoder(http2initialHeaderTableSize, nil) fr.MaxHeaderListSize = sc.maxHeaderListSize() fr.SetMaxReadFrameSize(s.maxReadFrameSize()) sc.framer = fr if tc, ok := c.(http2connectionStater); ok { sc.tlsState = new(tls.ConnectionState) *sc.tlsState = tc.ConnectionState() if sc.tlsState.Version \u0026lt; tls.VersionTLS12 { sc.rejectConn(http2ErrCodeInadequateSecurity, \u0026#34;TLS version too low\u0026#34;) return } if sc.tlsState.ServerName == \u0026#34;\u0026#34; { } if !s.PermitProhibitedCipherSuites \u0026amp;\u0026amp; http2isBadCipher(sc.tlsState.CipherSuite) { sc.rejectConn(http2ErrCodeInadequateSecurity, fmt.Sprintf(\u0026#34;Prohibited TLS 1.2 Cipher Suite: %x\u0026#34;, sc.tlsState.CipherSuite)) return } } if hook := http2testHookGetServerConn; hook != nil { hook(sc) } sc.serve() }  使用http2serverConn对象封装了对conn的管理  http2serverConn.serve func (sc *http2serverConn) serve() { sc.serveG.check() defer sc.notePanic() defer sc.conn.Close() defer sc.closeAllStreamsOnConnClose() defer sc.stopShutdownTimer() defer close(sc.doneServing) // unblocks handlers trying to send  if http2VerboseLogs { sc.vlogf(\u0026#34;http2: server connection from %v on %p\u0026#34;, sc.conn.RemoteAddr(), sc.hs) } // server告诉client端当前我的一些设置 \tsc.writeFrame(http2FrameWriteRequest{ write: http2writeSettings{ {http2SettingMaxFrameSize, sc.srv.maxReadFrameSize()}, {http2SettingMaxConcurrentStreams, sc.advMaxStreams}, {http2SettingMaxHeaderListSize, sc.maxHeaderListSize()}, {http2SettingInitialWindowSize, uint32(sc.srv.initialStreamRecvWindowSize())}, }, }) sc.unackedSettings++ // Each connection starts with intialWindowSize inflow tokens. \t// If a higher value is configured, we add more tokens. \tif diff := sc.srv.initialConnRecvWindowSize() - http2initialWindowSize; diff \u0026gt; 0 { sc.sendWindowUpdate(nil, int(diff)) } // 读取客户端发的前言 \tif err := sc.readPreface(); err != nil { sc.condlogf(err, \u0026#34;http2: server: error reading preface from client %v: %v\u0026#34;, sc.conn.RemoteAddr(), err) return } // 这里理应是通过更新conn的状态来出发某些callback \tsc.setConnState(StateActive) sc.setConnState(StateIdle) if sc.srv.IdleTimeout != 0 { sc.idleTimer = time.AfterFunc(sc.srv.IdleTimeout, sc.onIdleTimer) defer sc.idleTimer.Stop() } // 在新的goroutine中处理后面的frame \tgo sc.readFrames() // closed by defer sc.conn.Close above  settingsTimer := time.AfterFunc(http2firstSettingsTimeout, sc.onSettingsTimer) defer settingsTimer.Stop() // 在主的serve goroutine处理各种事件 \tloopNum := 0 for { loopNum++ select { case wr := \u0026lt;-sc.wantWriteFrameCh: if se, ok := wr.write.(http2StreamError); ok { sc.resetStream(se) break } sc.writeFrame(wr) case res := \u0026lt;-sc.wroteFrameCh: // 读取上层需要返回的内容，以frame的形式写回给客户端 \tsc.wroteFrame(res) case res := \u0026lt;-sc.readFrameCh: // 这里即为framer读出的frame \tif !sc.processFrameFromReader(res) { return } res.readMore() if settingsTimer != nil { settingsTimer.Stop() settingsTimer = nil } case m := \u0026lt;-sc.bodyReadCh: sc.noteBodyRead(m.st, m.n) case msg := \u0026lt;-sc.serveMsgCh: switch v := msg.(type) { case func(int): v(loopNum) // for testing \tcase *http2serverMessage: switch v { case http2settingsTimerMsg: sc.logf(\u0026#34;timeout waiting for SETTINGS frames from %v\u0026#34;, sc.conn.RemoteAddr()) return case http2idleTimerMsg: sc.vlogf(\u0026#34;connection is idle\u0026#34;) sc.goAway(http2ErrCodeNo) case http2shutdownTimerMsg: sc.vlogf(\u0026#34;GOAWAY close timer fired; closing conn from %v\u0026#34;, sc.conn.RemoteAddr()) return case http2gracefulShutdownMsg: sc.startGracefulShutdownInternal() default: panic(\u0026#34;unknown timer\u0026#34;) } case *http2startPushRequest: sc.startPush(v) default: panic(fmt.Sprintf(\u0026#34;unexpected type %T\u0026#34;, v)) } } // If the peer is causing us to generate a lot of control frames, \t// but not reading them from us, assume they are trying to make us \t// run out of memory. \tif sc.queuedControlFrames \u0026gt; sc.srv.maxQueuedControlFrames() { sc.vlogf(\u0026#34;http2: too many control frames in send queue, closing connection\u0026#34;) return } // Start the shutdown timer after sending a GOAWAY. When sending GOAWAY \t// with no error code (graceful shutdown), don\u0026#39;t start the timer until \t// all open streams have been completed. \tsentGoAway := sc.inGoAway \u0026amp;\u0026amp; !sc.needToSendGoAway \u0026amp;\u0026amp; !sc.writingFrame gracefulShutdownComplete := sc.goAwayCode == http2ErrCodeNo \u0026amp;\u0026amp; sc.curOpenStreams() == 0 if sentGoAway \u0026amp;\u0026amp; sc.shutdownTimer == nil \u0026amp;\u0026amp; (sc.goAwayCode != http2ErrCodeNo || gracefulShutdownComplete) { sc.shutDownIn(http2goAwayTimeout) } } } http2serverConn.readFrames 通过framer读取frame后，写入readFrameCh管道，等待主的goroutine处理\n// readFrames is the loop that reads incoming frames. // It takes care to only read one frame at a time, blocking until the // consumer is done with the frame. // It\u0026#39;s run on its own goroutine. func (sc *http2serverConn) readFrames() { gate := make(http2gate) gateDone := gate.Done for { f, err := sc.framer.ReadFrame() select { case sc.readFrameCh \u0026lt;- http2readFrameResult{f, err, gateDone}: case \u0026lt;-sc.doneServing: return } select { case \u0026lt;-gate: case \u0026lt;-sc.doneServing: return } if http2terminalReadFrameError(err) { return } } } http2serverConn.processFrameFromReader func (sc *http2serverConn) processFrameFromReader(res http2readFrameResult) bool { sc.serveG.check() err := res.err if err != nil { if err == http2ErrFrameTooLarge { sc.goAway(http2ErrCodeFrameSize) return true // goAway will close the loop \t} clientGone := err == io.EOF || err == io.ErrUnexpectedEOF || http2isClosedConnError(err) if clientGone { return false } } else { f := res.f if http2VerboseLogs { sc.vlogf(\u0026#34;http2: server read frame %v\u0026#34;, http2summarizeFrame(f)) } // 处理具体的frame \terr = sc.processFrame(f) if err == nil { return true } } switch ev := err.(type) { case http2StreamError: sc.resetStream(ev) return true case http2goAwayFlowError: sc.goAway(http2ErrCodeFlowControl) return true case http2ConnectionError: sc.logf(\u0026#34;http2: server connection error from %v: %v\u0026#34;, sc.conn.RemoteAddr(), ev) sc.goAway(http2ErrCode(ev)) return true // goAway will handle shutdown \tdefault: if res.err != nil { sc.vlogf(\u0026#34;http2: server closing client connection; error reading frame from client %s: %v\u0026#34;, sc.conn.RemoteAddr(), err) } else { sc.logf(\u0026#34;http2: server closing client connection: %v\u0026#34;, err) } return false } } http2serverConn.processFrame 根据不同的frame类型处理\nfunc (sc *http2serverConn) processFrame(f http2Frame) error { sc.serveG.check() // First frame received must be SETTINGS. \tif !sc.sawFirstSettings { if _, ok := f.(*http2SettingsFrame); !ok { return http2ConnectionError(http2ErrCodeProtocol) } sc.sawFirstSettings = true } switch f := f.(type) { case *http2SettingsFrame: // 包含setting信息的frame \treturn sc.processSettings(f) case *http2MetaHeadersFrame: // \treturn sc.processHeaders(f) case *http2WindowUpdateFrame: return sc.processWindowUpdate(f) case *http2PingFrame: return sc.processPing(f) case *http2DataFrame: return sc.processData(f) case *http2RSTStreamFrame: return sc.processResetStream(f) case *http2PriorityFrame: return sc.processPriority(f) case *http2GoAwayFrame: return sc.processGoAway(f) case *http2PushPromiseFrame: // A client cannot push. Thus, servers MUST treat the receipt of a PUSH_PROMISE \t// frame as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. \treturn http2ConnectionError(http2ErrCodeProtocol) default: sc.vlogf(\u0026#34;http2: server ignoring frame: %v\u0026#34;, f.Header()) return nil } } http2serverConn.processData 这里只展开processData方法看看如何处理frame的数据\nfunc (sc *http2serverConn) processData(f *http2DataFrame) error { sc.serveG.check() if sc.inGoAway \u0026amp;\u0026amp; sc.goAwayCode != http2ErrCodeNo { return nil } data := f.Data() id := f.Header().StreamID state, st := sc.state(id) if id == 0 || state == http2stateIdle { return http2ConnectionError(http2ErrCodeProtocol) } if st == nil || state != http2stateOpen || st.gotTrailerHeader || st.resetQueued { // 流量控制 \tif sc.inflow.available() \u0026lt; int32(f.Length) { return http2streamError(id, http2ErrCodeFlowControl) } sc.inflow.take(int32(f.Length)) sc.sendWindowUpdate(nil, int(f.Length)) // conn-level  if st != nil \u0026amp;\u0026amp; st.resetQueued { return nil } return http2streamError(id, http2ErrCodeStreamClosed) } if st.body == nil { panic(\u0026#34;internal error: should have a body in this state\u0026#34;) } if st.declBodyBytes != -1 \u0026amp;\u0026amp; st.bodyBytes+int64(len(data)) \u0026gt; st.declBodyBytes { st.body.CloseWithError(fmt.Errorf(\u0026#34;sender tried to send more than declared Content-Length of %d bytes\u0026#34;, st.declBodyBytes)) return http2streamError(id, http2ErrCodeProtocol) } if f.Length \u0026gt; 0 { // 流量控制 \tif st.inflow.available() \u0026lt; int32(f.Length) { return http2streamError(id, http2ErrCodeFlowControl) } st.inflow.take(int32(f.Length)) if len(data) \u0026gt; 0 { // 将数据写入buf中，等待上层读取 \twrote, err := st.body.Write(data) if err != nil { sc.sendWindowUpdate(nil, int(f.Length)-wrote) return http2streamError(id, http2ErrCodeStreamClosed) } if wrote != len(data) { panic(\u0026#34;internal error: bad Writer\u0026#34;) } st.bodyBytes += int64(len(data)) } // Return any padded flow control now, since we won\u0026#39;t \t// refund it later on body reads. \tif pad := int32(f.Length) - int32(len(data)); pad \u0026gt; 0 { sc.sendWindowUpdate32(nil, pad) sc.sendWindowUpdate32(st, pad) } } if f.StreamEnded() { st.endStream() } return nil } http2serverConn.scheduleFrameWrite frame写回给client时是带有一定策略的, 目前有http2priorityWriteScheduler和http2randomWriteScheduler两种scheduler来调度frame的写回\n// scheduleFrameWrite tickles the frame writing scheduler. // // If a frame is already being written, nothing happens. This will be called again // when the frame is done being written. // // If a frame isn\u0026#39;t being written and we need to send one, the best frame // to send is selected by writeSched. // // If a frame isn\u0026#39;t being written and there\u0026#39;s nothing else to send, we // flush the write buffer. func (sc *http2serverConn) scheduleFrameWrite() { sc.serveG.check() if sc.writingFrame || sc.inFrameScheduleLoop { return } sc.inFrameScheduleLoop = true for !sc.writingFrameAsync { if sc.needToSendGoAway { sc.needToSendGoAway = false sc.startFrameWrite(http2FrameWriteRequest{ write: \u0026amp;http2writeGoAway{ maxStreamID: sc.maxClientStreamID, code: sc.goAwayCode, }, }) continue } if sc.needToSendSettingsAck { sc.needToSendSettingsAck = false sc.startFrameWrite(http2FrameWriteRequest{write: http2writeSettingsAck{}}) continue } if !sc.inGoAway || sc.goAwayCode == http2ErrCodeNo { // 这里应该有类似优先级队列的结构维护着需要写回的数据 \tif wr, ok := sc.writeSched.Pop(); ok { if wr.isControl() { sc.queuedControlFrames-- } sc.startFrameWrite(wr) continue } } if sc.needsFrameFlush { sc.startFrameWrite(http2FrameWriteRequest{write: http2flushFrameWriter{}}) sc.needsFrameFlush = false // after startFrameWrite, since it sets this true \tcontinue } break } sc.inFrameScheduleLoop = false } client端 http2ClientConn.roundTrip func (cc *http2ClientConn) RoundTrip(req *Request) (*Response, error) { resp, _, err := cc.roundTrip(req) return resp, err } func (cc *http2ClientConn) roundTrip(req *Request) (res *Response, gotErrAfterReqBodyWrite bool, err error) { // 检查连接头 \tif err := http2checkConnHeaders(req); err != nil { return nil, false, err } if cc.idleTimer != nil { cc.idleTimer.Stop() } trailers, err := http2commaSeparatedTrailers(req) if err != nil { return nil, false, err } hasTrailers := trailers != \u0026#34;\u0026#34; cc.mu.Lock() // 每一个conn里能承载的steam数量被maxConcurrentStreams控制，没有空闲stream时需要等待 \tif err := cc.awaitOpenSlotForRequest(req); err != nil { cc.mu.Unlock() return nil, false, err } body := req.Body // 计算请求的contentLen \tcontentLen := http2actualContentLength(req) hasBody := contentLen != 0 // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? \tvar requestedGzip bool if !cc.t.disableCompression() \u0026amp;\u0026amp; req.Header.Get(\u0026#34;Accept-Encoding\u0026#34;) == \u0026#34;\u0026#34; \u0026amp;\u0026amp; req.Header.Get(\u0026#34;Range\u0026#34;) == \u0026#34;\u0026#34; \u0026amp;\u0026amp; req.Method != \u0026#34;HEAD\u0026#34; { // Request gzip only, not deflate. Deflate is ambiguous and \t// not as universally supported anyway. \t// See: https://zlib.net/zlib_faq.html#faq39 \t// \t// Note that we don\u0026#39;t request this for HEAD requests, \t// due to a bug in nginx: \t// http://trac.nginx.org/nginx/ticket/358 \t// https://golang.org/issue/5522 \t// \t// We don\u0026#39;t request gzip if the request is for a range, since \t// auto-decoding a portion of a gzipped document will just fail \t// anyway. See https://golang.org/issue/8923 \trequestedGzip = true } // we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,} (DATA is \t// sent by writeRequestBody below, along with any Trailers, \t// again in form HEADERS{1}, CONTINUATION{0,})  // 头部压缩 \thdrs, err := cc.encodeHeaders(req, requestedGzip, trailers, contentLen) if err != nil { cc.mu.Unlock() return nil, false, err } // 每个请求有一个独立的stream \tcs := cc.newStream() cs.req = req cs.trace = httptrace.ContextClientTrace(req.Context()) cs.requestedGzip = requestedGzip bodyWriter := cc.t.getBodyWriterState(cs, body) cs.on100 = bodyWriter.on100 defer func() { cc.wmu.Lock() werr := cc.werr cc.wmu.Unlock() if werr != nil { cc.Close() } }() cc.wmu.Lock() endStream := !hasBody \u0026amp;\u0026amp; !hasTrailers // 往stream里写入header, 里面会通过conn上的framer往conn里写入 \twerr := cc.writeHeaders(cs.ID, endStream, int(cc.maxFrameSize), hdrs) cc.wmu.Unlock() http2traceWroteHeaders(cs.trace) cc.mu.Unlock() if werr != nil { if hasBody { req.Body.Close() // per RoundTripper contract \tbodyWriter.cancel() } // 移除这个stream，并通过cond broadcase所有等着的请求 \tcc.forgetStreamID(cs.ID) // Don\u0026#39;t bother sending a RST_STREAM (our write already failed; \t// no need to keep writing) \thttp2traceWroteRequest(cs.trace, werr) return nil, false, werr } var respHeaderTimer \u0026lt;-chan time.Time if hasBody { // 如果没有设置timer，则直接开启一个协程执行异步写入操作  // 否则好像等着一个什么100 continue？？ \tbodyWriter.scheduleBodyWrite() } else { // 没有body写入，则等待response在超时时间内回来 \thttp2traceWroteRequest(cs.trace, nil) if d := cc.responseHeaderTimeout(); d != 0 { timer := time.NewTimer(d) defer timer.Stop() respHeaderTimer = timer.C } } readLoopResCh := cs.resc bodyWritten := false ctx := req.Context() handleReadLoopResponse := func(re http2resAndError) (*Response, bool, error) { res := re.res if re.err != nil || res.StatusCode \u0026gt; 299 { // On error or status code 3xx, 4xx, 5xx, etc abort any \t// ongoing write, assuming that the server doesn\u0026#39;t care \t// about our request body. If the server replied with 1xx or \t// 2xx, however, then assume the server DOES potentially \t// want our body (e.g. full-duplex streaming: \t// golang.org/issue/13444). If it turns out the server \t// doesn\u0026#39;t, they\u0026#39;ll RST_STREAM us soon enough. This is a \t// heuristic to avoid adding knobs to Transport. Hopefully \t// we can keep it. \tbodyWriter.cancel() cs.abortRequestBodyWrite(http2errStopReqBodyWrite) if hasBody \u0026amp;\u0026amp; !bodyWritten { \u0026lt;-bodyWriter.resc } } if re.err != nil { cc.forgetStreamID(cs.ID) return nil, cs.getStartedWrite(), re.err } res.Request = req res.TLS = cc.tlsState return res, false, nil } // 循环处理chan \tfor { select { case re := \u0026lt;-readLoopResCh: // 读取response \treturn handleReadLoopResponse(re) case \u0026lt;-respHeaderTimer: // 如果response超时了 \tif !hasBody || bodyWritten { // 如果没有body，或者body已经写入，则reset stream \tcc.writeStreamReset(cs.ID, http2ErrCodeCancel, nil) } else { // 终止写入 \tbodyWriter.cancel() cs.abortRequestBodyWrite(http2errStopReqBodyWriteAndCancel) \u0026lt;-bodyWriter.resc } cc.forgetStreamID(cs.ID) // 向上报超时错误 \treturn nil, cs.getStartedWrite(), http2errTimeout case \u0026lt;-ctx.Done(): if !hasBody || bodyWritten { cc.writeStreamReset(cs.ID, http2ErrCodeCancel, nil) } else { bodyWriter.cancel() cs.abortRequestBodyWrite(http2errStopReqBodyWriteAndCancel) \u0026lt;-bodyWriter.resc } cc.forgetStreamID(cs.ID) return nil, cs.getStartedWrite(), ctx.Err() case \u0026lt;-req.Cancel: // 如果请求被取消 \tif !hasBody || bodyWritten { cc.writeStreamReset(cs.ID, http2ErrCodeCancel, nil) } else { bodyWriter.cancel() cs.abortRequestBodyWrite(http2errStopReqBodyWriteAndCancel) \u0026lt;-bodyWriter.resc } cc.forgetStreamID(cs.ID) return nil, cs.getStartedWrite(), http2errRequestCanceled case \u0026lt;-cs.peerReset: // 如果对端重置 \t// processResetStream already removed the \t// stream from the streams map; no need for \t// forgetStreamID. \treturn nil, cs.getStartedWrite(), cs.resetErr case err := \u0026lt;-bodyWriter.resc: bodyWritten = true // Prefer the read loop\u0026#39;s response, if available. Issue 16102. \tselect { case re := \u0026lt;-readLoopResCh: return handleReadLoopResponse(re) default: } if err != nil { cc.forgetStreamID(cs.ID) return nil, cs.getStartedWrite(), err } if d := cc.responseHeaderTimeout(); d != 0 { timer := time.NewTimer(d) defer timer.Stop() respHeaderTimer = timer.C } } } } http2Transport.newClientConn func (t *http2Transport) newClientConn(c net.Conn, singleUse bool) (*http2ClientConn, error) { cc := \u0026amp;http2ClientConn{ t: t, tconn: c, readerDone: make(chan struct{}), nextStreamID: 1, maxFrameSize: 16 \u0026lt;\u0026lt; 10, // spec default \tinitialWindowSize: 65535, // spec default \tmaxConcurrentStreams: 1000, // \u0026#34;infinite\u0026#34;, per spec. 1000 seems good enough. \tpeerMaxHeaderListSize: 0xffffffffffffffff, // \u0026#34;infinite\u0026#34;, per spec. Use 2^64-1 instead. \tstreams: make(map[uint32]*http2clientStream), singleUse: singleUse, wantSettingsAck: true, pings: make(map[[8]byte]chan struct{}), } if d := t.idleConnTimeout(); d != 0 { cc.idleTimeout = d cc.idleTimer = time.AfterFunc(d, cc.onIdleTimeout) } if http2VerboseLogs { t.vlogf(\u0026#34;http2: Transport creating client conn %p to %v\u0026#34;, cc, c.RemoteAddr()) } cc.cond = sync.NewCond(\u0026amp;cc.mu) cc.flow.add(int32(http2initialWindowSize)) // TODO: adjust this writer size to account for frame size + \t// MTU + crypto/tls record padding.  // 将net.Conn赋给h2c的write buffer \tcc.bw = bufio.NewWriter(http2stickyErrWriter{c, \u0026amp;cc.werr}) // 将net.Conn赋给h2c的read buffer \tcc.br = bufio.NewReader(c) // 将readBuffer同rightBuffer都交给framer管理 \tcc.fr = http2NewFramer(cc.bw, cc.br) cc.fr.ReadMetaHeaders = hpack.NewDecoder(http2initialHeaderTableSize, nil) cc.fr.MaxHeaderListSize = t.maxHeaderListSize() // TODO: SetMaxDynamicTableSize, SetMaxDynamicTableSizeLimit on \t// henc in response to SETTINGS frames? \tcc.henc = hpack.NewEncoder(\u0026amp;cc.hbuf) if t.AllowHTTP { cc.nextStreamID = 3 } if cs, ok := c.(http2connectionStater); ok { state := cs.ConnectionState() cc.tlsState = \u0026amp;state } initialSettings := []http2Setting{ {ID: http2SettingEnablePush, Val: 0}, {ID: http2SettingInitialWindowSize, Val: http2transportDefaultStreamFlow}, } if max := t.maxHeaderListSize(); max != 0 { initialSettings = append(initialSettings, http2Setting{ID: http2SettingMaxHeaderListSize, Val: max}) } // 发送请求前言 \u0026#34;PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n\u0026#34; \tcc.bw.Write(http2clientPreface) // 写入初始化设置 \tcc.fr.WriteSettings(initialSettings...) cc.fr.WriteWindowUpdate(0, http2transportDefaultConnFlow) cc.inflow.add(http2transportDefaultConnFlow + http2initialWindowSize) cc.bw.Flush() if cc.werr != nil { cc.Close() return nil, cc.werr } go cc.readLoop() return cc, nil }  initialWindowSize流量控制窗口的默认大小为65535 maxConcurrentStreams最大并发的stream 连接创建后，会写入前言，初始化设置(是否打开serverPush，初始化的窗口大小) 开启readLoop循环读取conn里来的数据  http2ClientConn.readLoop // readLoop runs in its own goroutine and reads and dispatches frames. func (cc *http2ClientConn) readLoop() { rl := \u0026amp;http2clientConnReadLoop{cc: cc} defer rl.cleanup() cc.readerErr = rl.run() if ce, ok := cc.readerErr.(http2ConnectionError); ok { cc.wmu.Lock() cc.fr.WriteGoAway(0, http2ErrCode(ce), nil) cc.wmu.Unlock() } }  用http2clientConnReadLoop封装对conn读取的操作  http2clientConnReadLoop.run func (rl *http2clientConnReadLoop) run() error { cc := rl.cc rl.closeWhenIdle = cc.t.disableKeepAlives() || cc.singleUse gotReply := false // ever saw a HEADERS reply \tgotSettings := false readIdleTimeout := cc.t.ReadIdleTimeout var t *time.Timer if readIdleTimeout != 0 { t = time.AfterFunc(readIdleTimeout, cc.healthCheck) defer t.Stop() } for { // 循环从conn中读取frame \tf, err := cc.fr.ReadFrame() if t != nil { t.Reset(readIdleTimeout) } if err != nil { cc.vlogf(\u0026#34;http2: Transport readFrame error on conn %p: (%T) %v\u0026#34;, cc, err, err) } if se, ok := err.(http2StreamError); ok { if cs := cc.streamByID(se.StreamID, false); cs != nil { cs.cc.writeStreamReset(cs.ID, se.Code, err) cs.cc.forgetStreamID(cs.ID) if se.Cause == nil { se.Cause = cc.fr.errDetail } rl.endStreamError(cs, se) } continue } else if err != nil { return err } if http2VerboseLogs { cc.vlogf(\u0026#34;http2: Transport received %s\u0026#34;, http2summarizeFrame(f)) } if !gotSettings { if _, ok := f.(*http2SettingsFrame); !ok { cc.logf(\u0026#34;protocol error: received %T before a SETTINGS frame\u0026#34;, f) return http2ConnectionError(http2ErrCodeProtocol) } gotSettings = true } maybeIdle := false // whether frame might transition us to idle  switch f := f.(type) { case *http2MetaHeadersFrame: err = rl.processHeaders(f) maybeIdle = true gotReply = true case *http2DataFrame: // 数据frame， \terr = rl.processData(f) maybeIdle = true case *http2GoAwayFrame: // 远端已关闭 \terr = rl.processGoAway(f) maybeIdle = true case *http2RSTStreamFrame: // 重置一个stream \terr = rl.processResetStream(f) maybeIdle = true case *http2SettingsFrame: // 可以通过这个frame调整maxFrameSize，maxConcurrentStreams，peerMaxHeaderListSize \terr = rl.processSettings(f) case *http2PushPromiseFrame: // 直接告诉对端不要发这种frame \terr = rl.processPushPromise(f) case *http2WindowUpdateFrame: // 底下通过修改window size来实现流量控制 \terr = rl.processWindowUpdate(f) case *http2PingFrame: // 用来保活的心跳包 \terr = rl.processPing(f) default: cc.logf(\u0026#34;Transport: unhandled response frame type %T\u0026#34;, f) } if err != nil { if http2VerboseLogs { cc.vlogf(\u0026#34;http2: Transport conn %p received error from processing frame %v: %v\u0026#34;, cc, http2summarizeFrame(f), err) } return err } if rl.closeWhenIdle \u0026amp;\u0026amp; gotReply \u0026amp;\u0026amp; maybeIdle { cc.closeIfIdle() } } }   循环从conn中读取frame\n  针对不同种类的frame有不同的处理\n MetaHeadersFrame DataFrame GoAwayFrame RSTStreamFrame SettingsFrame PushPromiseFrame WindowUpdateFrame PingFrame    http2Framer.ReadFrame // 固定的frame header长度 const http2frameHeaderLen = 9 // ReadFrame reads a single frame. The returned Frame is only valid // until the next call to ReadFrame. // // If the frame is larger than previously set with SetMaxReadFrameSize, the // returned error is ErrFrameTooLarge. Other errors may be of type // ConnectionError, StreamError, or anything else from the underlying // reader. func (fr *http2Framer) ReadFrame() (http2Frame, error) { fr.errDetail = nil if fr.lastFrame != nil { fr.lastFrame.invalidate() } // 先读出frame header \tfh, err := http2readFrameHeader(fr.headerBuf[:], fr.r) if err != nil { return nil, err } if fh.Length \u0026gt; fr.maxReadSize { return nil, http2ErrFrameTooLarge } // 读出frame的payload \tpayload := fr.getReadBuf(fh.Length) if _, err := io.ReadFull(fr.r, payload); err != nil { return nil, err } // 根据frame类型拿到对应的frameParser，处理后拿到frame \tf, err := http2typeFrameParser(fh.Type)(fr.frameCache, fh, payload) if err != nil { if ce, ok := err.(http2connError); ok { return nil, fr.connError(ce.Code, ce.Reason) } return nil, err } // 检查frame的顺序以及streamId是否一致 \tif err := fr.checkFrameOrder(f); err != nil { return nil, err } if fr.logReads { fr.debugReadLoggerf(\u0026#34;http2: Framer %p: read %v\u0026#34;, fr, http2summarizeFrame(f)) } if fh.Type == http2FrameHeaders \u0026amp;\u0026amp; fr.ReadMetaHeaders != nil { return fr.readMetaFrame(f.(*http2HeadersFrame)) } return f, nil } 主要数据结构 server-conn type http2serverConn struct { // Immutable: \tsrv *http2Server hs *Server conn net.Conn bw *http2bufferedWriter // writing to conn \thandler Handler baseCtx context.Context framer *http2Framer doneServing chan struct{} // closed when serverConn.serve ends \treadFrameCh chan http2readFrameResult // written by serverConn.readFrames \twantWriteFrameCh chan http2FrameWriteRequest // from handlers -\u0026gt; serve \twroteFrameCh chan http2frameWriteResult // from writeFrameAsync -\u0026gt; serve, tickles more frame writes \tbodyReadCh chan http2bodyReadMsg // from handlers -\u0026gt; serve \tserveMsgCh chan interface{} // misc messages \u0026amp; code to send to / run on the serve loop \tflow http2flow // conn-wide (not stream-specific) outbound flow control \tinflow http2flow // conn-wide inbound flow control \ttlsState *tls.ConnectionState // shared by all handlers, like net/http \tremoteAddrStr string writeSched http2WriteScheduler // Everything following is owned by the serve loop; use serveG.check(): \tserveG http2goroutineLock // used to verify funcs are on serve() \tpushEnabled bool sawFirstSettings bool // got the initial SETTINGS frame after the preface \tneedToSendSettingsAck bool unackedSettings int // how many SETTINGS have we sent without ACKs? \tqueuedControlFrames int // control frames in the writeSched queue \tclientMaxStreams uint32 // SETTINGS_MAX_CONCURRENT_STREAMS from client (our PUSH_PROMISE limit) \tadvMaxStreams uint32 // our SETTINGS_MAX_CONCURRENT_STREAMS advertised the client \tcurClientStreams uint32 // number of open streams initiated by the client \tcurPushedStreams uint32 // number of open streams initiated by server push \tmaxClientStreamID uint32 // max ever seen from client (odd), or 0 if there have been no client requests \tmaxPushPromiseID uint32 // ID of the last push promise (even), or 0 if there have been no pushes \tstreams map[uint32]*http2stream initialStreamSendWindowSize int32 maxFrameSize int32 headerTableSize uint32 peerMaxHeaderListSize uint32 // zero means unknown (default) \tcanonHeader map[string]string // http2-lower-case -\u0026gt; Go-Canonical-Case \twritingFrame bool // started writing a frame (on serve goroutine or separate) \twritingFrameAsync bool // started a frame on its own goroutine but haven\u0026#39;t heard back on wroteFrameCh \tneedsFrameFlush bool // last frame write wasn\u0026#39;t a flush \tinGoAway bool // we\u0026#39;ve started to or sent GOAWAY \tinFrameScheduleLoop bool // whether we\u0026#39;re in the scheduleFrameWrite loop \tneedToSendGoAway bool // we need to schedule a GOAWAY frame write \tgoAwayCode http2ErrCode shutdownTimer *time.Timer // nil until used \tidleTimer *time.Timer // nil if unused  // Owned by the writeFrameAsync goroutine: \theaderWriteBuf bytes.Buffer hpackEncoder *hpack.Encoder // Used by startGracefulShutdown. \tshutdownOnce sync.Once } client-conn // ClientConn is the state of a single HTTP/2 client connection to an // HTTP/2 server. type http2ClientConn struct { t *http2Transport tconn net.Conn // 底层的连接 \ttlsState *tls.ConnectionState // nil only for specialized impls \treused uint32 // whether conn is being reused; atomic \tsingleUse bool // whether being used for a single http.Request  // readLoop goroutine fields: \treaderDone chan struct{} // closed on error \treaderErr error // set before readerDone is closed  idleTimeout time.Duration // 处理超时相关 \tidleTimer *time.Timer mu sync.Mutex // guards following \tcond *sync.Cond // hold mu; broadcast on flow/closed changes \tflow http2flow // conn级别的流量控制窗口 \tinflow http2flow // conn级别的流量控制窗口 \tclosing bool closed bool wantSettingsAck bool // we sent a SETTINGS frame and haven\u0026#39;t heard back \tgoAway *http2GoAwayFrame // if non-nil, the GoAwayFrame we received \tgoAwayDebug string // goAway frame\u0026#39;s debug data, retained as a string \tstreams map[uint32]*http2clientStream // client-initiated \tnextStreamID uint32 pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams \tpings map[[8]byte]chan struct{} // in flight ping data to notification channel \tbw *bufio.Writer br *bufio.Reader fr *http2Framer // 用来写入stream里的frame \tlastActive time.Time lastIdle time.Time // time last idle \t// Settings from peer: (also guarded by mu) \tmaxFrameSize uint32 // 最大能写入的frame大小 \tmaxConcurrentStreams uint32 peerMaxHeaderListSize uint64 initialWindowSize uint32 hbuf bytes.Buffer // HPACK encoder writes into this \thenc *hpack.Encoder freeBuf [][]byte wmu sync.Mutex // held while writing; acquire AFTER mu if holding both \twerr error // first write error that has occurred } server-stream // stream represents a stream. This is the minimal metadata needed by // the serve goroutine. Most of the actual stream state is owned by // the http.Handler\u0026#39;s goroutine in the responseWriter. Because the // responseWriter\u0026#39;s responseWriterState is recycled at the end of a // handler, this struct intentionally has no pointer to the // *responseWriter{,State} itself, as the Handler ending nils out the // responseWriter\u0026#39;s state field. type http2stream struct { // immutable: \tsc *http2serverConn id uint32 body *http2pipe // non-nil if expecting DATA frames \tcw http2closeWaiter // closed wait stream transitions to closed state \tctx context.Context cancelCtx func() // owned by serverConn\u0026#39;s serve loop: \tbodyBytes int64 // body bytes seen so far \tdeclBodyBytes int64 // or -1 if undeclared \tflow http2flow // limits writing from Handler to client \tinflow http2flow // what the client is allowed to POST/etc to us \tstate http2streamState resetQueued bool // RST_STREAM queued for write; set by sc.resetStream \tgotTrailerHeader bool // HEADER frame for trailers was seen \twroteHeaders bool // whether we wrote headers (not status 100) \twriteDeadline *time.Timer // nil if unused  trailer Header // accumulated trailers \treqTrailer Header // handler\u0026#39;s Request.Trailer } client-stream // clientStream is the state for a single HTTP/2 stream. One of these // is created for each Transport.RoundTrip call. type http2clientStream struct { cc *http2ClientConn // 所属的连接 \treq *Request // 发起stream的源请求 \ttrace *httptrace.ClientTrace // 追踪相关 \tID uint32 // 唯一id \tresc chan http2resAndError bufPipe http2pipe // 每次从dataFrame里读到的内容会写到这里 \tstartedWrite bool // started request body write; guarded by cc.mu \trequestedGzip bool // 是否经由gzip压缩过 \ton100 func() // optional code to run if get a 100 continue response  flow http2flow // guarded by cc.mu 用来控制流量窗口的大小 \tinflow http2flow // guarded by cc.mu 用来控制流量窗口的大小 \tbytesRemain int64 // -1 means unknown; owned by transportResponseBody.Read \treadErr error // sticky read error; owned by transportResponseBody.Read \tstopReqBody error // if non-nil, stop writing req body; guarded by cc.mu \tdidReset bool // whether we sent a RST_STREAM to the server; guarded by cc.mu  peerReset chan struct{} // closed on peer reset \tresetErr error // populated before peerReset is closed  done chan struct{} // closed when stream remove from cc.streams map; close calls guarded by cc.mu  // owned by clientConnReadLoop: \tfirstByte bool // got the first response byte \tpastHeaders bool // got first MetaHeadersFrame (actual headers) \tpastTrailers bool // got optional second MetaHeadersFrame (trailers) \tnum1xx uint8 // number of 1xx responses seen  trailer Header // accumulated trailers \tresTrailer *Header // client\u0026#39;s Response.Trailer } frames frame-header\n// A FrameHeader is the 9 byte header of all HTTP/2 frames. // // See http://http2.github.io/http2-spec/#FrameHeader type http2FrameHeader struct { valid bool // caller can access []byte fields in the Frame  // Type is the 1 byte frame type. There are ten standard frame \t// types, but extension frame types may be written by WriteRawFrame \t// and will be returned by ReadFrame (as UnknownFrame). \tType http2FrameType // Flags are the 1 byte of 8 potential bit flags per frame. \t// They are specific to the frame type. \tFlags http2Flags // Length is the length of the frame, not including the 9 byte header. \t// The maximum size is one byte less than 16MB (uint24), but only \t// frames up to 16KB are allowed without peer agreement. \tLength uint32 // StreamID is which stream this frame is for. Certain frames \t// are not stream-specific, in which case this field is 0. \tStreamID uint32 } settings-frame // A SettingsFrame conveys configuration parameters that affect how // endpoints communicate, such as preferences and constraints on peer // behavior. // // See http://http2.github.io/http2-spec/#SETTINGS type http2SettingsFrame struct { http2FrameHeader p []byte } meta-headers // A MetaHeadersFrame is the representation of one HEADERS frame and // zero or more contiguous CONTINUATION frames and the decoding of // their HPACK-encoded contents. // // This type of frame does not appear on the wire and is only returned // by the Framer when Framer.ReadMetaHeaders is set. type http2MetaHeadersFrame struct { *http2HeadersFrame // Fields are the fields contained in the HEADERS and \t// CONTINUATION frames. The underlying slice is owned by the \t// Framer and must not be retained after the next call to \t// ReadFrame. \t// \t// Fields are guaranteed to be in the correct http2 order and \t// not have unknown pseudo header fields or invalid header \t// field names or values. Required pseudo header fields may be \t// missing, however. Use the MetaHeadersFrame.Pseudo accessor \t// method access pseudo headers. \tFields []hpack.HeaderField // Truncated is whether the max header list size limit was hit \t// and Fields is incomplete. The hpack decoder state is still \t// valid, however. \tTruncated bool } window-update // A WindowUpdateFrame is used to implement flow control. // See http://http2.github.io/http2-spec/#rfc.section.6.9 type http2WindowUpdateFrame struct { http2FrameHeader Increment uint32 // never read with high bit set } ping // A PingFrame is a mechanism for measuring a minimal round trip time // from the sender, as well as determining whether an idle connection // is still functional. // See http://http2.github.io/http2-spec/#rfc.section.6.7 type http2PingFrame struct { http2FrameHeader Data [8]byte } data // A DataFrame conveys arbitrary, variable-length sequences of octets // associated with a stream. // See http://http2.github.io/http2-spec/#rfc.section.6.1 type http2DataFrame struct { http2FrameHeader data []byte } rst-stream // A RSTStreamFrame allows for abnormal termination of a stream. // See http://http2.github.io/http2-spec/#rfc.section.6.4 type http2RSTStreamFrame struct { http2FrameHeader ErrCode http2ErrCode } priority // A PriorityFrame specifies the sender-advised priority of a stream. // See http://http2.github.io/http2-spec/#rfc.section.6.3 type http2PriorityFrame struct { http2FrameHeader http2PriorityParam } go-away // A GoAwayFrame informs the remote peer to stop creating streams on this connection. // See http://http2.github.io/http2-spec/#rfc.section.6.8 type http2GoAwayFrame struct { http2FrameHeader LastStreamID uint32 ErrCode http2ErrCode debugData []byte } push-promise // A PushPromiseFrame is used to initiate a server stream. // See http://http2.github.io/http2-spec/#rfc.section.6.6 type http2PushPromiseFrame struct { http2FrameHeader PromiseID uint32 headerFragBuf []byte // not owned } ","permalink":"https://lambertxiao.github.io/posts/golang/golang-http2%E7%9A%84%E5%AE%9E%E7%8E%B0/doc/","summary":"扒开go源码，看看http2在go里的实现","title":"golang-http2实现"},{"content":"func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) func deferproc(siz int32, fn *funcval) ","permalink":"https://lambertxiao.github.io/posts/golang/golang-runtime/doc/","summary":"func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) func deferproc(siz int32, fn *funcval) ","title":"golang-runtime"},{"content":"简介  KV数据库 数据持久化于磁盘上 底层基于LSM Tree理论  基本使用  以 https://github.com/syndtr/goleveldb 举例\n 打开db db, err := leveldb.OpenFile(\u0026#34;path/to/db\u0026#34;, nil) ... defer db.Close() 读写操作 // Remember that the contents of the returned slice should not be modified. data, err := db.Get([]byte(\u0026#34;key\u0026#34;), nil) ... err = db.Put([]byte(\u0026#34;key\u0026#34;), []byte(\u0026#34;value\u0026#34;), nil) ... err = db.Delete([]byte(\u0026#34;key\u0026#34;), nil) 迭代操作 普通迭代\niter := db.NewIterator(nil, nil) for iter.Next() { // Remember that the contents of the returned slice should not be modified, and \t// only valid until the next call to Next. \tkey := iter.Key() value := iter.Value() ... } iter.Release() err = iter.Error() ... seek到某个key之后迭代\niter := db.NewIterator(nil, nil) for ok := iter.Seek(key); ok; ok = iter.Next() { // Use key/value. \t... } iter.Release() err = iter.Error() ... 条件查找迭代\niter := db.NewIterator(\u0026amp;util.Range{Start: []byte(\u0026#34;foo\u0026#34;), Limit: []byte(\u0026#34;xoo\u0026#34;)}, nil) for iter.Next() { // Use key/value. \t... } iter.Release() err = iter.Error() 批量写入 batch := new(leveldb.Batch) batch.Put([]byte(\u0026#34;foo\u0026#34;), []byte(\u0026#34;value\u0026#34;)) batch.Put([]byte(\u0026#34;bar\u0026#34;), []byte(\u0026#34;another value\u0026#34;)) batch.Delete([]byte(\u0026#34;baz\u0026#34;)) err = db.Write(batch, nil) ... 使用布隆过滤器 o := \u0026amp;opt.Options{ Filter: filter.NewBloomFilter(10), } db, err := leveldb.OpenFile(\u0026#34;path/to/db\u0026#34;, o) ... defer db.Close() ... goleveldb源码分析\n主要流程分析 当Put一个Key时发生了什么 func (db *DB) Put(key, value []byte, wo *opt.WriteOptions) error { return db.putRec(keyTypeVal, key, value, wo) } func (db *DB) putRec(kt keyType, key, value []byte, wo *opt.WriteOptions) error { ... merge := !wo.GetNoWriteMerge() \u0026amp;\u0026amp; !db.s.o.GetNoWriteMerge() sync := wo.GetSync() \u0026amp;\u0026amp; !db.s.o.GetNoSync() // Acquire write lock. \tif merge { select { case db.writeMergeC \u0026lt;- writeMerge{sync: sync, keyType: kt, key: key, value: value}: if \u0026lt;-db.writeMergedC { // Write is merged. \treturn \u0026lt;-db.writeAckC } // Write is not merged, the write lock is handed to us. Continue. \tcase db.writeLockC \u0026lt;- struct{}{}: // Write lock acquired. \tcase err := \u0026lt;-db.compPerErrC: // Compaction error. \treturn err case \u0026lt;-db.closeC: // Closed \treturn ErrClosed } } else { select { case db.writeLockC \u0026lt;- struct{}{}: // Write lock acquired. \tcase err := \u0026lt;-db.compPerErrC: // Compaction error. \treturn err case \u0026lt;-db.closeC: // Closed \treturn ErrClosed } } batch := db.batchPool.Get().(*Batch) batch.Reset() batch.appendRec(kt, key, value) return db.writeLocked(batch, batch, merge, sync) }   写之前需要拿到两个bool值，merge和sync，这两个值来源于传入的 writeOption.NoWriteMerge 和db.session.cacheOption.NoSync\n  如果需要merge，通过 writeMergeC 发起一个合并写请求，并阻塞在 writeMergedC 等待合并完成， 同时如果收到合并成功的请求，需要往 writeAckC 取出一个ack(以上chan都是在openDB时初始化好的无缓冲chan)\nfunc openDB(s *session) (*DB, error) { ... db := \u0026amp;DB{ // Write  batchPool: sync.Pool{New: newBatch}, writeMergeC: make(chan writeMerge), writeMergedC: make(chan bool), writeLockC: make(chan struct{}, 1), writeAckC: make(chan error), // Compaction  tcompCmdC: make(chan cCmd), tcompPauseC: make(chan chan\u0026lt;- struct{}), mcompCmdC: make(chan cCmd), compErrC: make(chan error), compPerErrC: make(chan error), compErrSetC: make(chan error), // Close  closeC: make(chan struct{}), } ... }   如果不需要merge，则将key和value添加到batch中等待批量操作\n  先来看merge操作，找到writeMergeC的接收端\n// ourBatch is batch that we can modify. func (db *DB) writeLocked(batch, ourBatch *Batch, merge, sync bool) error { var ( overflow bool merged int batches = []*Batch{batch} ) if merge { // Merge limit. \tvar mergeLimit int if batch.internalLen \u0026gt; 128\u0026lt;\u0026lt;10 { mergeLimit = (1 \u0026lt;\u0026lt; 20) - batch.internalLen } else { mergeLimit = 128 \u0026lt;\u0026lt; 10 } mergeCap := mdbFree - batch.internalLen if mergeLimit \u0026gt; mergeCap { mergeLimit = mergeCap } ... merge: // 相当于等mergeLimit个请求到了之后才会退出merge循环 \tfor mergeLimit \u0026gt; 0 { select { case incoming := \u0026lt;-db.writeMergeC: if incoming.batch != nil { // Merge batch. \tif incoming.batch.internalLen \u0026gt; mergeLimit { overflow = true break merge } batches = append(batches, incoming.batch) mergeLimit -= incoming.batch.internalLen } else { // Merge put. \tinternalLen := len(incoming.key) + len(incoming.value) + 8 if internalLen \u0026gt; mergeLimit { overflow = true break merge } if ourBatch == nil { ourBatch = db.batchPool.Get().(*Batch) ourBatch.Reset() batches = append(batches, ourBatch) } // We can use same batch since concurrent write doesn\u0026#39;t \t// guarantee write order. \tourBatch.appendRec(incoming.keyType, incoming.key, incoming.value) mergeLimit -= internalLen } sync = sync || incoming.sync merged++ db.writeMergedC \u0026lt;- true default: break merge } } } // Release ourBatch if any. \tif ourBatch != nil { defer db.batchPool.Put(ourBatch) } // Seq number. \tseq := db.seq + 1 // Write journal. \tif err := db.writeJournal(batches, seq, sync); err != nil { db.unlockWrite(overflow, merged, err) return err } // Put batches. \tfor _, batch := range batches { if err := batch.putMem(seq, mdb.DB); err != nil { panic(err) } seq += uint64(batch.Len()) } // Incr seq number. \tdb.addSeq(uint64(batchesLen(batches))) // Rotate memdb if it\u0026#39;s reach the threshold. \tif batch.internalLen \u0026gt;= mdbFree { db.rotateMem(0, false) } db.unlockWrite(overflow, merged, nil) return nil }  每次的mergeLimit通过计算得到 当收到一个merge请求时，如果该请求所属的batch已经满了，则跳出merge循环，否则则一只等到mergeLimit减少到0 更新前会提前写日志writeJournal, writeJournal会同时写入一批batch，这一批batch用同一个序列号  主要结构 Journal Memdb Comparer storage // Storage is the storage. A storage instance must be safe for concurrent use. type Storage interface { // Lock locks the storage. Any subsequent attempt to call Lock will fail \t// until the last lock released. \t// Caller should call Unlock method after use. \tLock() (Locker, error) // Log logs a string. This is used for logging. \t// An implementation may write to a file, stdout or simply do nothing. \tLog(str string) // SetMeta store \u0026#39;file descriptor\u0026#39; that can later be acquired using GetMeta \t// method. The \u0026#39;file descriptor\u0026#39; should point to a valid file. \t// SetMeta should be implemented in such way that changes should happen \t// atomically. \tSetMeta(fd FileDesc) error // GetMeta returns \u0026#39;file descriptor\u0026#39; stored in meta. The \u0026#39;file descriptor\u0026#39; \t// can be updated using SetMeta method. \t// Returns os.ErrNotExist if meta doesn\u0026#39;t store any \u0026#39;file descriptor\u0026#39;, or \t// \u0026#39;file descriptor\u0026#39; point to nonexistent file. \tGetMeta() (FileDesc, error) // List returns file descriptors that match the given file types. \t// The file types may be OR\u0026#39;ed together. \tList(ft FileType) ([]FileDesc, error) // Open opens file with the given \u0026#39;file descriptor\u0026#39; read-only. \t// Returns os.ErrNotExist error if the file does not exist. \t// Returns ErrClosed if the underlying storage is closed. \tOpen(fd FileDesc) (Reader, error) // Create creates file with the given \u0026#39;file descriptor\u0026#39;, truncate if already \t// exist and opens write-only. \t// Returns ErrClosed if the underlying storage is closed. \tCreate(fd FileDesc) (Writer, error) // Remove removes file with the given \u0026#39;file descriptor\u0026#39;. \t// Returns ErrClosed if the underlying storage is closed. \tRemove(fd FileDesc) error // Rename renames file from oldfd to newfd. \t// Returns ErrClosed if the underlying storage is closed. \tRename(oldfd, newfd FileDesc) error // Close closes the storage. \t// It is valid to call Close multiple times. Other methods should not be \t// called after the storage has been closed. \tClose() error }  storage层用来对db进行创建、删除、打开等操作 storage层具备获取db元信息的能力 总而言之，storage层用来管理db  session // session represent a persistent database session. type session struct { // Need 64-bit alignment. \tstNextFileNum int64 // current unused file number \tstJournalNum int64 // current journal file number; need external synchronization \tstPrevJournalNum int64 // prev journal file number; no longer used; for compatibility with older version of leveldb \tstTempFileNum int64 stSeqNum uint64 // last mem compacted seq; need external synchronization  stor *iStorage storLock storage.Locker o *cachedOptions icmp *iComparer tops *tOps manifest *journal.Writer manifestWriter storage.Writer manifestFd storage.FileDesc stCompPtrs []internalKey // compaction pointers; need external synchronization \tstVersion *version // current version \tntVersionId int64 // next version id to assign \trefCh chan *vTask relCh chan *vTask deltaCh chan *vDelta abandon chan int64 closeC chan struct{} closeW sync.WaitGroup vmu sync.Mutex // Testing fields \tfileRefCh chan chan map[int64]int // channel used to pass current reference stat }  session代表同db的一次会话 什么是alignment 什么是jounarl 序列号？ iComparer FileDesc internalKey vTask vDelta tOps  // Creates new initialized session instance. func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) { if stor == nil { return nil, os.ErrInvalid } storLock, err := stor.Lock() if err != nil { return } s = \u0026amp;session{ stor: newIStorage(stor), storLock: storLock, refCh: make(chan *vTask), relCh: make(chan *vTask), deltaCh: make(chan *vDelta), abandon: make(chan int64), fileRefCh: make(chan chan map[int64]int), closeC: make(chan struct{}), } s.setOptions(o) s.tops = newTableOps(s) s.closeW.Add(1) go s.refLoop() s.setVersion(nil, newVersion(s)) s.log(\u0026#34;log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed\u0026#34;) return }  每次创建一个会话，会锁住stroage 什么时候解锁？这意味着leveldb不支持多个session并发咯？  tOps  tOps包含了所有对table的操作 cache是files cache bcache是block cache, bpool是为bcache服务的  // Table operations. type tOps struct { s *session noSync bool evictRemoved bool cache *cache.Cache bcache *cache.Cache bpool *util.BufferPool } FileDesc FileDesc起到一个文件描述符的作用\ntype FileDesc struct { Type FileType Num int64 } // FileType represent a file type. type FileType int // File types. const ( TypeManifest FileType = 1 \u0026lt;\u0026lt; iota TypeJournal TypeTable TypeTemp TypeAll = TypeManifest | TypeJournal | TypeTable | TypeTemp )  Type总共有4种类型 Num是文件号，session里的stNextFileNum维护了当前会话里下一个被分配出去文件号  Cacher  Cacher从接口上看显然是对Node的操作 Node是干什么的  // Cacher provides interface to implements a caching functionality. // An implementation must be safe for concurrent use. type Cacher interface { // Capacity returns cache capacity. \tCapacity() int // SetCapacity sets cache capacity. \tSetCapacity(capacity int) // Promote promotes the \u0026#39;cache node\u0026#39;. \tPromote(n *Node) // Ban evicts the \u0026#39;cache node\u0026#39; and prevent subsequent \u0026#39;promote\u0026#39;. \tBan(n *Node) // Evict evicts the \u0026#39;cache node\u0026#39;. \tEvict(n *Node) // EvictNS evicts \u0026#39;cache node\u0026#39; with the given namespace. \tEvictNS(ns uint64) // EvictAll evicts all \u0026#39;cache node\u0026#39;. \tEvictAll() // Close closes the \u0026#39;cache tree\u0026#39; \tClose() error } // Node is a \u0026#39;cache node\u0026#39;. type Node struct { r *Cache hash uint32 ns, key uint64 mu sync.Mutex size int value Value ref int32 onDel []func() CacheData unsafe.Pointer } Cache  cache用来存放Cacher所持有的nodes数量 带有读写锁  // Cache is a \u0026#39;cache map\u0026#39;. type Cache struct { mu sync.RWMutex mHead unsafe.Pointer // *mNode \tnodes int32 size int32 cacher Cacher closed bool } BufferPool  BufferPool显然是一个缓冲池，结合pool的类型为[]sync.Pool，大概率是用来池化对象的。 6个uint32的属性是干嘛的？  // BufferPool is a \u0026#39;buffer pool\u0026#39;. type BufferPool struct { pool [6]sync.Pool baseline [5]int get uint32 put uint32 less uint32 equal uint32 greater uint32 miss uint32 } vTask vTask即version task，用来引用或发布以一个版本任务\n// vTask defines a version task for either reference or release. type vTask struct { vid int64 files []tFiles created time.Time } vDelta vDelta即version delta, 表示下一个版本和当前指定版本之间的变化信息\n// vDelta indicates the change information between the next version // and the currently specified version type vDelta struct { vid int64 added []int64 deleted []int64 } session.refLoop  fileRef 表文件引用计数器  func (s *session) refLoop() { var ( fileRef = make(map[int64]int) // Table file reference counter \tref = make(map[int64]*vTask) // Current referencing version store \tdeltas = make(map[int64]*vDelta) referenced = make(map[int64]struct{}) released = make(map[int64]*vDelta) // Released version that waiting for processing \tabandoned = make(map[int64]struct{}) // Abandoned version id \tnext, last int64 ) // addFileRef adds file reference counter with specified file number and \t// reference value \taddFileRef := func(fnum int64, ref int) int { ref += fileRef[fnum] if ref \u0026gt; 0 { fileRef[fnum] = ref } else if ref == 0 { delete(fileRef, fnum) } else { panic(fmt.Sprintf(\u0026#34;negative ref: %v\u0026#34;, fnum)) } return ref } // skipAbandoned skips useless abandoned version id. \tskipAbandoned := func() bool { if _, exist := abandoned[next]; exist { delete(abandoned, next) return true } return false } // applyDelta applies version change to current file reference. \tapplyDelta := func(d *vDelta) { for _, t := range d.added { addFileRef(t, 1) } for _, t := range d.deleted { if addFileRef(t, -1) == 0 { s.tops.remove(storage.FileDesc{Type: storage.TypeTable, Num: t}) } } } timer := time.NewTimer(0) \u0026lt;-timer.C // discard the initial tick \tdefer timer.Stop() // processTasks processes version tasks in strict order. \t// \t// If we want to use delta to reduce the cost of file references and dereferences, \t// we must strictly follow the id of the version, otherwise some files that are \t// being referenced will be deleted. \t// \t// In addition, some db operations (such as iterators) may cause a version to be \t// referenced for a long time. In order to prevent such operations from blocking \t// the entire processing queue, we will properly convert some of the version tasks \t// into full file references and releases. \tprocessTasks := func() { timer.Reset(maxCachedTime) // Make sure we don\u0026#39;t cache too many version tasks. \tfor { // Skip any abandoned version number to prevent blocking processing. \tif skipAbandoned() { next += 1 continue } // Don\u0026#39;t bother the version that has been released. \tif _, exist := released[next]; exist { break } // Ensure the specified version has been referenced. \tif _, exist := ref[next]; !exist { break } if last-next \u0026lt; maxCachedNumber \u0026amp;\u0026amp; time.Since(ref[next].created) \u0026lt; maxCachedTime { break } // Convert version task into full file references and releases mode. \t// Reference version(i+1) first and wait version(i) to release. \t// FileRef(i+1) = FileRef(i) + Delta(i) \tfor _, tt := range ref[next].files { for _, t := range tt { addFileRef(t.fd.Num, 1) } } // Note, if some compactions take a long time, even more than 5 minutes, \t// we may miss the corresponding delta information here. \t// Fortunately it will not affect the correctness of the file reference, \t// and we can apply the delta once we receive it. \tif d := deltas[next]; d != nil { applyDelta(d) } referenced[next] = struct{}{} delete(ref, next) delete(deltas, next) next += 1 } // Use delta information to process all released versions. \tfor { if skipAbandoned() { next += 1 continue } if d, exist := released[next]; exist { if d != nil { applyDelta(d) } delete(released, next) next += 1 continue } return } } for { processTasks() select { case t := \u0026lt;-s.refCh: if _, exist := ref[t.vid]; exist { panic(\u0026#34;duplicate reference request\u0026#34;) } ref[t.vid] = t if t.vid \u0026gt; last { last = t.vid } case d := \u0026lt;-s.deltaCh: if _, exist := ref[d.vid]; !exist { if _, exist2 := referenced[d.vid]; !exist2 { panic(\u0026#34;invalid release request\u0026#34;) } // The reference opt is already expired, apply \t// delta here. \tapplyDelta(d) continue } deltas[d.vid] = d case t := \u0026lt;-s.relCh: if _, exist := referenced[t.vid]; exist { for _, tt := range t.files { for _, t := range tt { if addFileRef(t.fd.Num, -1) == 0 { s.tops.remove(t.fd) } } } delete(referenced, t.vid) continue } if _, exist := ref[t.vid]; !exist { panic(\u0026#34;invalid release request\u0026#34;) } released[t.vid] = deltas[t.vid] delete(deltas, t.vid) delete(ref, t.vid) case id := \u0026lt;-s.abandon: if id \u0026gt;= next { abandoned[id] = struct{}{} } case \u0026lt;-timer.C: case r := \u0026lt;-s.fileRefCh: ref := make(map[int64]int) for f, c := range fileRef { ref[f] = c } r \u0026lt;- ref case \u0026lt;-s.closeC: s.closeW.Done() return } } } ","permalink":"https://lambertxiao.github.io/posts/leveldb-bloomfilter/doc/","summary":"level，多层级","title":"LevelDB是干嘛的"},{"content":"例子 func main() { lister, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;0.0.0.0:9009\u0026#34;) if err != nil { return } for { conn, err := lister.Accept() //等待建立连接  if err != nil { continue } //开启协程处理  go func() { defer conn.Close() for { buf := make([]byte, 128) n, err := conn.Read(buf) if err != nil{ return } } }() } } Listen func Listen(network, address string) (Listener, error) { var lc ListenConfig return lc.Listen(context.Background(), network, address) } func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) { addrs, err := DefaultResolver.resolveAddrList(ctx, \u0026#34;listen\u0026#34;, network, address, nil) if err != nil { return nil, \u0026amp;OpError{Op: \u0026#34;listen\u0026#34;, Net: network, Source: nil, Addr: nil, Err: err} } sl := \u0026amp;sysListener{ ListenConfig: *lc, network: network, address: address, } var l Listener la := addrs.first(isIPv4) switch la := la.(type) { case *TCPAddr: l, err = sl.listenTCP(ctx, la) case *UnixAddr: l, err = sl.listenUnix(ctx, la) default: return nil, \u0026amp;OpError{Op: \u0026#34;listen\u0026#34;, Net: sl.network, Source: nil, Addr: la, Err: \u0026amp;AddrError{Err: \u0026#34;unexpected address type\u0026#34;, Addr: address}} } if err != nil { return nil, \u0026amp;OpError{Op: \u0026#34;listen\u0026#34;, Net: sl.network, Source: nil, Addr: la, Err: err} // l is non-nil interface containing nil pointer \t} return l, nil } func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) { fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, \u0026#34;listen\u0026#34;, sl.ListenConfig.Control) if err != nil { return nil, err } return \u0026amp;TCPListener{fd: fd, lc: sl.ListenConfig}, nil } func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) { if (runtime.GOOS == \u0026#34;aix\u0026#34; || runtime.GOOS == \u0026#34;windows\u0026#34; || runtime.GOOS == \u0026#34;openbsd\u0026#34;) \u0026amp;\u0026amp; mode == \u0026#34;dial\u0026#34; \u0026amp;\u0026amp; raddr.isWildcard() { raddr = raddr.toLocal(net) } family, ipv6only := favoriteAddrFamily(net, laddr, raddr, mode) return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr, ctrlFn) } // socket returns a network file descriptor that is ready for // asynchronous I/O using the network poller. func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) { s, err := sysSocket(family, sotype, proto) if err != nil { return nil, err } if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil { poll.CloseFunc(s) return nil, err } if fd, err = newFD(s, family, sotype, net); err != nil { poll.CloseFunc(s) return nil, err } // This function makes a network file descriptor for the \t// following applications: \t// \t// - An endpoint holder that opens a passive stream \t// connection, known as a stream listener \t// \t// - An endpoint holder that opens a destination-unspecific \t// datagram connection, known as a datagram listener \t// \t// - An endpoint holder that opens an active stream or a \t// destination-specific datagram connection, known as a \t// dialer \t// \t// - An endpoint holder that opens the other connection, such \t// as talking to the protocol stack inside the kernel \t// \t// For stream and datagram listeners, they will only require \t// named sockets, so we can assume that it\u0026#39;s just a request \t// from stream or datagram listeners when laddr is not nil but \t// raddr is nil. Otherwise we assume it\u0026#39;s just for dialers or \t// the other connection holders.  if laddr != nil \u0026amp;\u0026amp; raddr == nil { switch sotype { case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET: if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil { fd.Close() return nil, err } return fd, nil case syscall.SOCK_DGRAM: if err := fd.listenDatagram(laddr, ctrlFn); err != nil { fd.Close() return nil, err } return fd, nil } } if err := fd.dial(ctx, laddr, raddr, ctrlFn); err != nil { fd.Close() return nil, err } return fd, nil } func sysSocket(family, sotype, proto int) (int, error) { // See ../syscall/exec_unix.go for description of ForkLock. \tsyscall.ForkLock.RLock() s, err := socketFunc(family, sotype, proto) if err == nil { syscall.CloseOnExec(s) } syscall.ForkLock.RUnlock() if err != nil { return -1, os.NewSyscallError(\u0026#34;socket\u0026#34;, err) } if err = syscall.SetNonblock(s, true); err != nil { poll.CloseFunc(s) return -1, os.NewSyscallError(\u0026#34;setnonblock\u0026#34;, err) } return s, nil } func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error { var err error if err = setDefaultListenerSockopts(fd.pfd.Sysfd); err != nil { return err } var lsa syscall.Sockaddr if lsa, err = laddr.sockaddr(fd.family); err != nil { return err } if ctrlFn != nil { c, err := newRawConn(fd) if err != nil { return err } if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil { return err } } if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil { return os.NewSyscallError(\u0026#34;bind\u0026#34;, err) } if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil { return os.NewSyscallError(\u0026#34;listen\u0026#34;, err) } if err = fd.init(); err != nil { return err } lsa, _ = syscall.Getsockname(fd.pfd.Sysfd) fd.setAddr(fd.addrFunc()(lsa), nil) return nil } func (fd *netFD) init() error { return fd.pfd.Init(fd.net, true) } // Init initializes the FD. The Sysfd field should already be set. // This can be called multiple times on a single FD. // The net argument is a network name from the net package (e.g., \u0026#34;tcp\u0026#34;), // or \u0026#34;file\u0026#34;. // Set pollable to true if fd should be managed by runtime netpoll. func (fd *FD) Init(net string, pollable bool) error { // We don\u0026#39;t actually care about the various network types. \tif net == \u0026#34;file\u0026#34; { fd.isFile = true } if !pollable { fd.isBlocking = 1 return nil } err := fd.pd.init(fd) if err != nil { // If we could not initialize the runtime poller, \t// assume we are using blocking mode. \tfd.isBlocking = 1 } return err } func (pd *pollDesc) init(fd *FD) error { serverInit.Do(runtime_pollServerInit) ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd)) if errno != 0 { if ctx != 0 { runtime_pollUnblock(ctx) runtime_pollClose(ctx) } return errnoErr(syscall.Errno(errno)) } pd.runtimeCtx = ctx return nil } func runtime_pollServerInit() func runtime_pollOpen(fd uintptr) (uintptr, int) func runtime_pollClose(ctx uintptr) func runtime_pollWait(ctx uintptr, mode int) int func runtime_pollWaitCanceled(ctx uintptr, mode int) int func runtime_pollReset(ctx uintptr, mode int) int func runtime_pollSetDeadline(ctx uintptr, d int64, mode int) func runtime_pollUnblock(ctx uintptr) func runtime_isPollServerDescriptor(fd uintptr) bool //go:linkname poll_runtime_pollOpen internal/poll.runtime_pollOpen func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) { pd := pollcache.alloc() lock(\u0026amp;pd.lock) if pd.wg != 0 \u0026amp;\u0026amp; pd.wg != pdReady { throw(\u0026#34;runtime: blocked write on free polldesc\u0026#34;) } if pd.rg != 0 \u0026amp;\u0026amp; pd.rg != pdReady { throw(\u0026#34;runtime: blocked read on free polldesc\u0026#34;) } pd.fd = fd pd.closing = false pd.everr = false pd.rseq++ pd.rg = 0 pd.rd = 0 pd.wseq++ pd.wg = 0 pd.wd = 0 pd.self = pd unlock(\u0026amp;pd.lock) var errno int32 errno = netpollopen(fd, pd) return pd, int(errno) } func netpollopen(fd uintptr, pd *pollDesc) int32 { var ev epollevent ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET *(**pollDesc)(unsafe.Pointer(\u0026amp;ev.data)) = pd return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), \u0026amp;ev) } Accpet // Accept implements the Accept method in the Listener interface; it // waits for the next call and returns a generic Conn. func (l *TCPListener) Accept() (Conn, error) { if !l.ok() { return nil, syscall.EINVAL } c, err := l.accept() if err != nil { return nil, \u0026amp;OpError{Op: \u0026#34;accept\u0026#34;, Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err} } return c, nil } func (ln *TCPListener) accept() (*TCPConn, error) { fd, err := ln.fd.accept() if err != nil { return nil, err } tc := newTCPConn(fd) if ln.lc.KeepAlive \u0026gt;= 0 { setKeepAlive(fd, true) ka := ln.lc.KeepAlive if ln.lc.KeepAlive == 0 { ka = defaultTCPKeepAlive } setKeepAlivePeriod(fd, ka) } return tc, nil } func (fd *netFD) accept() (netfd *netFD, err error) { d, rsa, errcall, err := fd.pfd.Accept() if err != nil { if errcall != \u0026#34;\u0026#34; { err = wrapSyscallError(errcall, err) } return nil, err } if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil { poll.CloseFunc(d) return nil, err } if err = netfd.init(); err != nil { netfd.Close() return nil, err } lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd) netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa)) return netfd, nil } // Accept wraps the accept network call. func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) { if err := fd.readLock(); err != nil { return -1, nil, \u0026#34;\u0026#34;, err } defer fd.readUnlock() if err := fd.pd.prepareRead(fd.isFile); err != nil { return -1, nil, \u0026#34;\u0026#34;, err } for { s, rsa, errcall, err := accept(fd.Sysfd) if err == nil { return s, rsa, \u0026#34;\u0026#34;, err } switch err { case syscall.EINTR: continue case syscall.EAGAIN: if fd.pd.pollable() { if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } case syscall.ECONNABORTED: // This means that a socket on the listen \t// queue was closed before we Accept()ed it; \t// it\u0026#39;s a silly error, so try again. \tcontinue } return -1, nil, errcall, err } } func (pd *pollDesc) pollable() bool { return pd.runtimeCtx != 0 } func (pd *pollDesc) waitRead(isFile bool) error { return pd.wait(\u0026#39;r\u0026#39;, isFile) } func (pd *pollDesc) wait(mode int, isFile bool) error { if pd.runtimeCtx == 0 { return errors.New(\u0026#34;waiting for unsupported file type\u0026#34;) } res := runtime_pollWait(pd.runtimeCtx, mode) return convertErr(res, isFile) } func poll_runtime_pollWait(pd *pollDesc, mode int) int { errcode := netpollcheckerr(pd, int32(mode)) if errcode != pollNoError { return errcode } // As for now only Solaris, illumos, and AIX use level-triggered IO. \tif GOOS == \u0026#34;solaris\u0026#34; || GOOS == \u0026#34;illumos\u0026#34; || GOOS == \u0026#34;aix\u0026#34; { netpollarm(pd, mode) } for !netpollblock(pd, int32(mode), false) { errcode = netpollcheckerr(pd, int32(mode)) if errcode != pollNoError { return errcode } // Can happen if timeout has fired and unblocked us, \t// but before we had a chance to run, timeout has been reset. \t// Pretend it has not happened and retry. \t} return pollNoError } // returns true if IO is ready, or false if timedout or closed // waitio - wait only for completed IO, ignore errors func netpollblock(pd *pollDesc, mode int32, waitio bool) bool { gpp := \u0026amp;pd.rg if mode == \u0026#39;w\u0026#39; { gpp = \u0026amp;pd.wg } // set the gpp semaphore to pdWait \tfor { old := *gpp if old == pdReady { *gpp = 0 return true } if old != 0 { throw(\u0026#34;runtime: double wait\u0026#34;) } if atomic.Casuintptr(gpp, 0, pdWait) { break } } // need to recheck error states after setting gpp to pdWait \t// this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl \t// do the opposite: store to closing/rd/wd, membarrier, load of rg/wg \tif waitio || netpollcheckerr(pd, mode) == 0 { gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5) } // be careful to not lose concurrent pdReady notification \told := atomic.Xchguintptr(gpp, 0) if old \u0026gt; pdWait { throw(\u0026#34;runtime: corrupted polldesc\u0026#34;) } return old == pdReady } // Puts the current goroutine into a waiting state and calls unlockf on the // system stack. // // If unlockf returns false, the goroutine is resumed. // // unlockf must not access this G\u0026#39;s stack, as it may be moved between // the call to gopark and the call to unlockf. // // Note that because unlockf is called after putting the G into a waiting // state, the G may have already been readied by the time unlockf is called // unless there is external synchronization preventing the G from being // readied. If unlockf returns false, it must guarantee that the G cannot be // externally readied. // // Reason explains why the goroutine has been parked. It is displayed in stack // traces and heap dumps. Reasons should be unique and descriptive. Do not // re-use reasons, add new ones. func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) { if reason != waitReasonSleep { checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy \t} mp := acquirem() gp := mp.curg status := readgstatus(gp) if status != _Grunning \u0026amp;\u0026amp; status != _Gscanrunning { throw(\u0026#34;gopark: bad g status\u0026#34;) } mp.waitlock = lock mp.waitunlockf = unlockf gp.waitreason = reason mp.waittraceev = traceEv mp.waittraceskip = traceskip releasem(mp) // can\u0026#39;t do anything that might move the G between Ms here. \tmcall(park_m) } 唤醒 func sysmon() { lock(\u0026amp;sched.lock) sched.nmsys++ checkdead() unlock(\u0026amp;sched.lock) // For syscall_runtime_doAllThreadsSyscall, sysmon is \t// sufficiently up to participate in fixups. \tatomic.Store(\u0026amp;sched.sysmonStarting, 0) lasttrace := int64(0) idle := 0 // how many cycles in succession we had not wokeup somebody \tdelay := uint32(0) for { if idle == 0 { // start with 20us sleep... \tdelay = 20 } else if idle \u0026gt; 50 { // start doubling the sleep after 1ms... \tdelay *= 2 } if delay \u0026gt; 10*1000 { // up to 10ms \tdelay = 10 * 1000 } usleep(delay) mDoFixup() // sysmon should not enter deep sleep if schedtrace is enabled so that \t// it can print that information at the right time. \t// \t// It should also not enter deep sleep if there are any active P\u0026#39;s so \t// that it can retake P\u0026#39;s from syscalls, preempt long running G\u0026#39;s, and \t// poll the network if all P\u0026#39;s are busy for long stretches. \t// \t// It should wakeup from deep sleep if any P\u0026#39;s become active either due \t// to exiting a syscall or waking up due to a timer expiring so that it \t// can resume performing those duties. If it wakes from a syscall it \t// resets idle and delay as a bet that since it had retaken a P from a \t// syscall before, it may need to do it again shortly after the \t// application starts work again. It does not reset idle when waking \t// from a timer to avoid adding system load to applications that spend \t// most of their time sleeping. \tnow := nanotime() if debug.schedtrace \u0026lt;= 0 \u0026amp;\u0026amp; (sched.gcwaiting != 0 || atomic.Load(\u0026amp;sched.npidle) == uint32(gomaxprocs)) { lock(\u0026amp;sched.lock) if atomic.Load(\u0026amp;sched.gcwaiting) != 0 || atomic.Load(\u0026amp;sched.npidle) == uint32(gomaxprocs) { syscallWake := false next, _ := timeSleepUntil() if next \u0026gt; now { atomic.Store(\u0026amp;sched.sysmonwait, 1) unlock(\u0026amp;sched.lock) // Make wake-up period small enough \t// for the sampling to be correct. \tsleep := forcegcperiod / 2 if next-now \u0026lt; sleep { sleep = next - now } shouldRelax := sleep \u0026gt;= osRelaxMinNS if shouldRelax { osRelax(true) } syscallWake = notetsleep(\u0026amp;sched.sysmonnote, sleep) mDoFixup() if shouldRelax { osRelax(false) } lock(\u0026amp;sched.lock) atomic.Store(\u0026amp;sched.sysmonwait, 0) noteclear(\u0026amp;sched.sysmonnote) } if syscallWake { idle = 0 delay = 20 } } unlock(\u0026amp;sched.lock) } lock(\u0026amp;sched.sysmonlock) // Update now in case we blocked on sysmonnote or spent a long time \t// blocked on schedlock or sysmonlock above. \tnow = nanotime() // trigger libc interceptors if needed \tif *cgo_yield != nil { asmcgocall(*cgo_yield, nil) } // poll network if not polled for more than 10ms \tlastpoll := int64(atomic.Load64(\u0026amp;sched.lastpoll)) if netpollinited() \u0026amp;\u0026amp; lastpoll != 0 \u0026amp;\u0026amp; lastpoll+10*1000*1000 \u0026lt; now { atomic.Cas64(\u0026amp;sched.lastpoll, uint64(lastpoll), uint64(now)) list := netpoll(0) // non-blocking - returns list of goroutines \tif !list.empty() { // Need to decrement number of idle locked M\u0026#39;s \t// (pretending that one more is running) before injectglist. \t// Otherwise it can lead to the following situation: \t// injectglist grabs all P\u0026#39;s but before it starts M\u0026#39;s to run the P\u0026#39;s, \t// another M returns from syscall, finishes running its G, \t// observes that there is no work to do and no other running M\u0026#39;s \t// and reports deadlock. \tincidlelocked(-1) injectglist(\u0026amp;list) incidlelocked(1) } } mDoFixup() if GOOS == \u0026#34;netbsd\u0026#34; { // netpoll is responsible for waiting for timer \t// expiration, so we typically don\u0026#39;t have to worry \t// about starting an M to service timers. (Note that \t// sleep for timeSleepUntil above simply ensures sysmon \t// starts running again when that timer expiration may \t// cause Go code to run again). \t// \t// However, netbsd has a kernel bug that sometimes \t// misses netpollBreak wake-ups, which can lead to \t// unbounded delays servicing timers. If we detect this \t// overrun, then startm to get something to handle the \t// timer. \t// \t// See issue 42515 and \t// https://gnats.netbsd.org/cgi-bin/query-pr-single.pl?number=50094. \tif next, _ := timeSleepUntil(); next \u0026lt; now { startm(nil, false) } } if atomic.Load(\u0026amp;scavenge.sysmonWake) != 0 { // Kick the scavenger awake if someone requested it. \twakeScavenger() } // retake P\u0026#39;s blocked in syscalls \t// and preempt long running G\u0026#39;s \tif retake(now) != 0 { idle = 0 } else { idle++ } // check if we need to force a GC \tif t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() \u0026amp;\u0026amp; atomic.Load(\u0026amp;forcegc.idle) != 0 { lock(\u0026amp;forcegc.lock) forcegc.idle = 0 var list gList list.push(forcegc.g) injectglist(\u0026amp;list) unlock(\u0026amp;forcegc.lock) } if debug.schedtrace \u0026gt; 0 \u0026amp;\u0026amp; lasttrace+int64(debug.schedtrace)*1000000 \u0026lt;= now { lasttrace = now schedtrace(debug.scheddetail \u0026gt; 0) } unlock(\u0026amp;sched.sysmonlock) } } Read // Read implements io.Reader. func (fd *FD) Read(p []byte) (int, error) { if err := fd.readLock(); err != nil { return 0, err } defer fd.readUnlock() if len(p) == 0 { // If the caller wanted a zero byte read, return immediately \t// without trying (but after acquiring the readLock). \t// Otherwise syscall.Read returns 0, nil which looks like \t// io.EOF. \t// TODO(bradfitz): make it wait for readability? (Issue 15735) \treturn 0, nil } if err := fd.pd.prepareRead(fd.isFile); err != nil { return 0, err } if fd.IsStream \u0026amp;\u0026amp; len(p) \u0026gt; maxRW { p = p[:maxRW] } for { n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p) if err != nil { n = 0 if err == syscall.EAGAIN \u0026amp;\u0026amp; fd.pd.pollable() { if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } } err = fd.eofError(n, err) return n, err } } Write // Write implements io.Writer. func (fd *FD) Write(p []byte) (int, error) { if err := fd.writeLock(); err != nil { return 0, err } defer fd.writeUnlock() if err := fd.pd.prepareWrite(fd.isFile); err != nil { return 0, err } var nn int for { max := len(p) if fd.IsStream \u0026amp;\u0026amp; max-nn \u0026gt; maxRW { max = nn + maxRW } n, err := ignoringEINTRIO(syscall.Write, fd.Sysfd, p[nn:max]) if n \u0026gt; 0 { nn += n } if nn == len(p) { return nn, err } if err == syscall.EAGAIN \u0026amp;\u0026amp; fd.pd.pollable() { if err = fd.pd.waitWrite(fd.isFile); err == nil { continue } } if err != nil { return nn, err } if n == 0 { return nn, io.ErrUnexpectedEOF } } }  /go/src/runtime/netpoll.go\n 定义了几个抽象方法\n// Initialize the poller. Only called once. func netpollinit() // Arm edge-triggered notifications for fd. The pd argument is to pass // back to netpollready when fd is ready. Return an errno value. func netpollopen(fd uintptr, pd *pollDesc) int32 // Poll the network. If delta \u0026lt; 0, block indefinitely. If delta == 0, // poll without blocking. If delta \u0026gt; 0, block for up to delta nanoseconds. // Return a list of goroutines built by calling netpollready. func netpoll(delta int64) gList // Wake up the network poller, assumed to be blocked in netpoll. func netpollBreak() // Reports whether fd is a file descriptor used by the poller. func netpollIsPollDescriptor(fd uintptr) bool 分平台实现\n netpoll.go netpoll_aix.go netpoll_epoll.go netpoll_fake.go netpoll_kqueue.go netpoll_os_test.go netpoll_solaris.go netpoll_stub.go netpoll_windows.go  看linux怎么做的\n","permalink":"https://lambertxiao.github.io/posts/golang/golang-epoll/doc/","summary":"go怎么用的epoll","title":"Golang-epoll"},{"content":"面试字节跳动四面（严格来说是五面），最终得到的回复是技术没问题，但综合考虑选择了另外的候选人，呵呵哒。稍微复盘了下，一个可能是涨幅要的过多，一个可能是二本的学历对于我进大厂造成了影响，又或者可能是谈职业规划时，没让HR听到想要听到的。 总之，革命尚未成功，同志仍需努力！\n432. 全 O(1) 的数据结构 432. 全 O(1) 的数据结构\n请你设计一个用于存储字符串计数的数据结构，并能够返回计数最小和最大的字符串。\n实现 AllOne 类：\n AllOne() 初始化数据结构的对象。 inc(String key) 字符串 key 的计数增加 1 。如果数据结构中尚不存在 key ，那么插入计数为 1 的 key 。 dec(String key) 字符串 key 的计数减少 1 。如果 key 的计数在减少后为 0 ，那么需要将这个 key 从数据结构中删除。测试用例保证：在减少计数前，key 存在于数据结构中。 getMaxKey() 返回任意一个计数最大的字符串。如果没有元素存在，返回一个空字符串 \u0026quot;\u0026quot; 。 getMinKey() 返回任意一个计数最小的字符串。如果没有元素存在，返回一个空字符串 \u0026quot;\u0026quot; 。  思路：\n hash表存放key到Node的映射 一个Node存放一批相同计数的key 双向链表将所有的Node按照计数从小到大串联起来 当某个key的计数增加或减少时，将key从当前的node中移除，并且在相邻的node中找到自己的容身之地（如果没有对应计数的node，则新建一个node）  type AllOne struct { key2node map[string]*Node dl *DoubleList } type Node struct { prev, next *Node keys map[string]struct{} cnt int } func NewNode(cnt int) *Node { n := \u0026amp;Node{ keys: make(map[string]struct{}), cnt: cnt, } return n } func (n *Node) AddKey(key string) { n.keys[key] = struct{}{} } func (n *Node) RemoveKey(key string) { delete(n.keys, key) } func (n *Node) IsEmpty() bool { return len(n.keys) == 0 } func Constructor() AllOne { ao := AllOne{ key2node: make(map[string]*Node), dl: NewDoubleList(), } return ao } func (ao *AllOne) Inc(key string) { curr, ok := ao.key2node[key] if ok { next := curr.next if next == nil || next.cnt \u0026gt; curr.cnt + 1 { n := NewNode(curr.cnt + 1) n.AddKey(key) ao.key2node[key] = n ao.dl.InsertAfter(n, curr) } else { next.AddKey(key) ao.key2node[key] = next } curr.RemoveKey(key) if curr.IsEmpty() { ao.dl.Remove(curr) } } else { first := ao.dl.First() if first == nil || first.cnt \u0026gt; 1 { n := NewNode(1) n.AddKey(key) ao.key2node[key] = n ao.dl.Push(n) } else { first.AddKey(key) ao.key2node[key] = first } } ao.dl.Print() } func (ao *AllOne) Dec(key string) { curr := ao.key2node[key] if curr.cnt \u0026gt; 1 { prev := curr.prev if prev == nil || prev.cnt \u0026lt; curr.cnt - 1 { n := NewNode(curr.cnt - 1) n.AddKey(key) ao.key2node[key] = n ao.dl.InsertBefore(n, curr) } else { prev.AddKey(key) ao.key2node[key] = prev } } else { delete(ao.key2node, key) } curr.RemoveKey(key) if curr.IsEmpty() { ao.dl.Remove(curr) } } func (ao *AllOne) GetMaxKey() string { last := ao.dl.Last() if last != nil { for k := range last.keys { return k } } return \u0026#34;\u0026#34; } func (ao *AllOne) GetMinKey() string { first := ao.dl.First() if first != nil { for k := range first.keys { return k } } return \u0026#34;\u0026#34; } type DoubleList struct { head, tail *Node } func NewDoubleList() *DoubleList { head, tail := NewNode(-1), NewNode(10000000) head.next, tail.prev = tail, head dl := \u0026amp;DoubleList{ head: head, tail: tail, } return dl } func (dl *DoubleList) Push(node *Node) { prev, next := dl.head, dl.head.next prev.next, node.prev = node, prev next.prev, node.next = node, next } func (dl *DoubleList) Remove(node *Node) { prev, next := node.prev, node.next prev.next, next.prev = next, prev node.prev, node.next = nil, nil } func (dl *DoubleList) InsertBefore(n, before *Node) { prev := before.prev before.prev, n.next = n, before prev.next, n.prev = n, prev } func (dl *DoubleList) InsertAfter(n, after *Node) { next := after.next after.next, n.prev = n, after n.next, next.prev = next, n } func (dl *DoubleList) First() *Node { if dl.IsEmpty() { return nil } return dl.head.next } func (dl *DoubleList) Last() *Node { if dl.IsEmpty() { return nil } return dl.tail.prev } func (dl *DoubleList) Print() { curr := dl.head.next for curr != dl.tail \u0026amp;\u0026amp; curr != nil { curr = curr.next } } func (dl *DoubleList) IsEmpty() bool { return dl.head.next == dl.tail } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%85%A8o1%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/doc/","summary":"写在字节4面后","title":"算法-全O(1)的数据结构"},{"content":"","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E6%80%8E%E4%B9%88%E7%94%A8%E7%9A%84epoll/doc/","summary":"","title":"golang-怎么用的epoll"},{"content":"mtr ","permalink":"https://lambertxiao.github.io/posts/linux-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/doc/","summary":"mtr ","title":"linux-常用命令"},{"content":"一个线程的操作结果对另一个线程是可见的，当且仅当前者的操作先于后者\n","permalink":"https://lambertxiao.github.io/posts/%E4%BB%80%E4%B9%88%E6%98%AFhappen-before/doc/","summary":"字节面试问到了，留个记录","title":"什么是happend-before"},{"content":"","permalink":"https://lambertxiao.github.io/posts/%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6%E7%AE%97%E6%B3%95%E9%83%BD%E6%9C%89%E5%93%AA%E4%BA%9B/doc/","summary":"","title":"拥塞控制算法都有哪些"},{"content":"https://zhuanlan.zhihu.com/p/353692786?utm_medium=social\u0026amp;utm_oi=947783647009439744\n网络IO的变化 BIO ss := new(socket) bind(ss, port) listen(ss) for { s := accept(ss) // 会阻塞住 } 优点：\n 每个连接一个线程去处理，可以同时接收很多连接  缺点：\n BIO中有两处阻塞，第一个是accept等待客户端连接阻塞，第二个是客户端连接后读取客户端数据recv函数阻塞，因此需要在建立连接后开启一个新线程处理 由于每个连接需要一个线程处理，当连接过多时，线程的内存开销比较大，同时CPU的调度消耗也比较大  NIO ss := new(socket) bind(ss, port) listen(ss) for { fd := accept(ss) // 该行不会阻塞住，如果fd不为-1，则表示有新的连接  fd.nonblocking() for { recv(fd) // 此行也不会阻塞，没有数据可读时，会立即返回  } } 优点：\n server端accpet客户端连接和读取客户端数据不阻塞，解决了阻塞问题，避免多线程处理多连接的问题  缺点：\n 假设现在连接的客户端有10w个，此时可能有请求数据的就占极小部分，但这10w个连接每次都需要发起recv请求(一次系统调用)区检查是否有数据到来，这大量浪费了时间和资源  第一版IO多路复用(select/poll) ss := new(socket) bind(ss, port) listen(ss) ss.nonblocking() for { // 内存中记录全部的fd  select(fds) // poll(fds) } 优点：\n 解决了用户需要频繁进行recv系统调用的问题，用户态1次系统调用，交由内核遍历  缺点：\n 每次需要将所有的fds集合传递给内核，由内核遍历，然后将就绪的readyFds返回，内核态无存储能力 内核实际上也仍需要每次遍历全量的fd  第二版IO多路复用(epoll) epoll_create() epoll_ctl() epoll_wait() 优点：\n 调用epoll_create时在内核中开辟了一段空间，分别是存放fd的红黑树，就绪列表，等待列表 epoll_ctl用来增删改红黑树中的fd epoll_wait阻塞在内核态，就绪队列不为空时，返回用户态  为啥需要Epoll 设想一个场景：有100万用户同时与进程A保持着TCP连接，而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包)，也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么，如何才能高效的处理这种场景呢？\nselect和poll是怎么做的： 把进程A添加到这100万个socket的等待队列中，当有一个socket收到数据，进程A会被操作系统唤醒。唤醒之后，进程A并不知道它被哪个socket就绪了，因此它需要去遍历所有的socket列表，找到就绪的socket\nepoll是怎么做的 它在Linux内核中申请了一个简易的文件系统，把原先的一个select或poll调用分成了3部分\nint epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);   首先，epoll会向内核注册了一个文件系统，用于存储被监控socket。\n  epoll在被内核初始化时，同时会开辟出epoll自己的内核高速cache区，用于安置每一个我们想监控的socket，这些socket会以红黑树的形式保存在内核cache里，以支持快速的查找、插入、删除。\n  当调用epoll_create时，就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件，它只服务于epoll。\n  epoll_create时，内核会创建一个eventpoll对象，等待队列（放进程引用），就绪列表 （存放准备好的事件），rbt红黑树（存放epitem, epitem持有这socket的fd）\n  接下来调用epoll_ctl时，做了两件事：\n 将socket的fd封装为epitem后，加入红黑树中（使用红黑树是综合增，删，查各个操作） 给内核的中断程序注册一个回调函数，告诉内核，如果这个句柄的中断到了，就把它放到rdlist里    当一个socket里有数据到了，内核把网卡上的数据copy到内核中后就把socket插入到rdlist里，调用epoll_wait时，就是检查rdlist里是否有内容，有内容就返回，无内容就阻塞。如果被阻塞，则进程会加入到等待队列中，等待被唤醒\n  所以epoll综合来看就是一颗红黑树，一个就绪句柄链表，一个进程等待队列，少量的内核cache\n  Epoll的触发模式   LT水平触发\n  ET边缘触发\n  ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9Cio%E7%9A%84%E6%BC%94%E5%8F%98/doc/","summary":"一颗红黑树，一个就绪句柄链表，一个进程等待队列，少量的内核cache","title":"网络IO的演变"},{"content":"200. 岛屿数量 200. 岛屿数量 给你一个由 \u0026lsquo;1\u0026rsquo;（陆地）和 \u0026lsquo;0\u0026rsquo;（水）组成的的二维网格，请你计算网格中岛屿的数量。\n岛屿总是被水包围，并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。\n此外，你可以假设该网格的四条边均被水包围。\nfunc numIslands(grid [][]byte) int { m, n := len(grid), len(grid[0]) bfs := func(i, j int) { // 从[i, j]位置开始扩散  q := [][]int{{i, j}} for len(q) != 0 { e := q[0] q = q[1:] x, y := e[0], e[1] if x \u0026gt;= 0 \u0026amp;\u0026amp; x \u0026lt; m \u0026amp;\u0026amp; y \u0026gt;= 0 \u0026amp;\u0026amp; y \u0026lt; n \u0026amp;\u0026amp; grid[x][y] == \u0026#39;1\u0026#39; { grid[x][y] = \u0026#39;0\u0026#39; q = append(q, []int{x+1, y}) q = append(q, []int{x-1, y}) q = append(q, []int{x, y+1}) q = append(q, []int{x, y-1}) } } } cnt := 0 for i := 0; i \u0026lt; m; i++ { for j := 0; j \u0026lt; n; j++ { if grid[i][j] == \u0026#39;0\u0026#39; { continue } bfs(i, j) // 为什么找到了一块陆地就可以增加一个岛屿数呢，  // 是因为在bfs方法中会将跟这块陆地相连的其他陆地染色，不会再次重复计算  cnt++ } } return cnt } 301. 删除无效的括号 301. 删除无效的括号 给你一个由若干括号和字母组成的字符串 s ，删除最小数量的无效括号，使得输入的字符串有效。\n返回所有可能的结果。答案可以按 任意顺序 返回。\nfunc removeInvalidParentheses(s string) []string { ans := []string{} if s == \u0026#34;\u0026#34; { return ans } q := []string{} visit := make(map[string]bool) q = append(q, s) // 广度搜索，从长字符串到短字符串搜索，由于求的是最小的更改，所以同一层上如果已经找到了合法字符串  // 则不需要往下执行  isFound := false for len(q) != 0 { ns := q[0] q = q[1:] if isValid(ns) { ans = append(ans, ns) isFound = true } if isFound { continue } for i, c := range ns { if c \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;z\u0026#39; { continue } // 删除第i个元素  nns := ns[:i] + ns[i+1:] if !visit[nns] { q = append(q, nns) visit[nns] = true } } } return ans } func isValid(s string) bool { lb := 0 for _, c := range s { if c == \u0026#39;(\u0026#39; { lb++ } if c == \u0026#39;)\u0026#39; { if lb == 0 { return false } lb-- } } return lb == 0 } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-bfs/doc/","summary":"200. 岛屿数量 200. 岛屿数量 给你一个由 \u0026lsquo;1\u0026rsquo;（陆地）和 \u0026lsquo;0\u0026rsquo;（水）组成的的二维网格，请你计算网格中岛屿的数量。\n岛屿总是被水包围，并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。\n此外，你可以假设该网格的四条边均被水包围。\nfunc numIslands(grid [][]byte) int { m, n := len(grid), len(grid[0]) bfs := func(i, j int) { // 从[i, j]位置开始扩散  q := [][]int{{i, j}} for len(q) != 0 { e := q[0] q = q[1:] x, y := e[0], e[1] if x \u0026gt;= 0 \u0026amp;\u0026amp; x \u0026lt; m \u0026amp;\u0026amp; y \u0026gt;= 0 \u0026amp;\u0026amp; y \u0026lt; n \u0026amp;\u0026amp; grid[x][y] == \u0026#39;1\u0026#39; { grid[x][y] = \u0026#39;0\u0026#39; q = append(q, []int{x+1, y}) q = append(q, []int{x-1, y}) q = append(q, []int{x, y+1}) q = append(q, []int{x, y-1}) } } } cnt := 0 for i := 0; i \u0026lt; m; i++ { for j := 0; j \u0026lt; n; j++ { if grid[i][j] == \u0026#39;0\u0026#39; { continue } bfs(i, j) // 为什么找到了一块陆地就可以增加一个岛屿数呢，  // 是因为在bfs方法中会将跟这块陆地相连的其他陆地染色，不会再次重复计算  cnt++ } } return cnt } 301.","title":"算法-BFS"},{"content":"22. 括号生成 22. 括号生成 数字 n 代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。\nfunc generateParenthesis(n int) []string { ans := []string{} var dfs func(lb, rb int, s string) dfs = func(lb, rb int, s string) { if lb \u0026gt; rb { return } if lb == 0 \u0026amp;\u0026amp; rb == 0 { // 括号用完了，并且是合法的  if isValid(s) { ans = append(ans, s) } return } if lb != 0 { dfs(lb - 1, rb, s + \u0026#34;(\u0026#34;) } if rb != 0 { dfs(lb, rb - 1, s + \u0026#34;)\u0026#34;) } } dfs(n, n, \u0026#34;\u0026#34;) return ans } func isValid(s string) bool { left := 0 for _, c := range s { if c == \u0026#39;(\u0026#39; { left++ } else { if left == 0 { return false } left-- } } return left == 0 } 39. 组合总和 39. 组合总和 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ，找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ，并以列表形式返回。你可以按 任意顺序 返回这些组合。\ncandidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同，则两种组合是不同的。\n对于给定的输入，保证和为 target 的不同组合数少于 150 个。\nfunc combinationSum(candidates []int, target int) [][]int { res := [][]int{} // idx是为了不添加重复的集合  var backtrace func (sum int, track []int, idx int) backtrace = func (sum int, track []int, idx int) { if target == sum { res = append(res, append([]int{}, track...)) return } if sum \u0026gt; target { return } for i := idx; i \u0026lt; len(candidates); i++ { c := candidates[i] backtrace(sum+c, append(track, c), i) } } backtrace(0, []int{}, 0) return res } 40. 组合总和 II 40. 组合总和 II 给定一个候选人编号的集合 candidates 和一个目标数 target ，找出 candidates 中所有可以使数字和为 target 的组合。\ncandidates 中的每个数字在每个组合中只能使用 一次 。\n注意：解集不能包含重复的组合。\nfunc combinationSum2(candidates []int, target int) [][]int { sort.Ints(candidates) ans := [][]int{} var backtrace func (sum int, set []int, idx int) backtrace = func (sum int, set []int, idx int) { if sum == target { ans = append(ans, append([]int{}, set...)) return } if sum \u0026gt; target { return } for i := idx; i \u0026lt; len(candidates); i++ { if i \u0026gt; idx \u0026amp;\u0026amp; candidates[i] == candidates[i-1] { continue } val := candidates[i] backtrace(sum + val, append(set, val), i+1) } } backtrace(0, []int{}, 0) return ans } 46. 全排列 46. 全排列 给定一个不含重复数字的数组 nums ，返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。\nfunc permute(nums []int) [][]int { n := len(nums) ans := [][]int{} used := make([]bool, n) var backtrace func(path []int) backtrace = func(path []int) { if len(path) == n { ans = append(ans, append([]int{}, path...)) return } for idx, v := range nums { if !used[idx] { used[idx] = true backtrace(append(path, v)) used[idx] = false } } } backtrace([]int{}) return ans } 78. 子集 78. 子集 给你一个整数数组 nums ，数组中的元素 互不相同 。返回该数组所有可能的子集（幂集）。\n解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。\nfunc subsets(nums []int) [][]int { size := len(nums) ans := [][]int{} var backtrace func(startIdx int, path []int) backtrace = func(startIdx int, path []int) { ans = append(ans, append([]int{}, path...)) if startIdx \u0026gt;= size { return } for i := startIdx; i \u0026lt; size; i++ { backtrace(i+1, append(path, nums[i])) } } backtrace(0, []int{}) return ans } 79. 单词搜索 79. 单词搜索 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中，返回 true ；否则，返回 false 。\n单词必须按照字母顺序，通过相邻的单元格内的字母构成，其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。\nfunc exist(board [][]byte, word string) bool { workLength := len(word) directions := [][]int{ {-1, 0}, // 往上  {1, 0}, // 往下  {0, -1}, // 往左  {0, 1}, // 往右  } w := len(board) h := len(board[0]) visit := geneVisit(w, h) var check func(i, j, k int) bool check = func(i, j, k int) bool { if board[i][j] != word[k] { return false } // word已经匹配到最后一位  if k == workLength - 1 { return true } visit[i][j] = true defer func() { visit[i][j] = false }() // 朝上下左右继续匹配k+1位  for _, d := range directions { newI := i+d[0] newJ := j+d[1] if newI \u0026gt;= 0 \u0026amp;\u0026amp; newJ \u0026gt;= 0 \u0026amp;\u0026amp; newI \u0026lt; w \u0026amp;\u0026amp; newJ \u0026lt; h{ if visit[newI][newJ] { continue } if check(newI, newJ, k+1) { return true } } } return false } for i, row := range board { for j := range row { if check(i, j, 0) { return true } } } return false } func geneVisit(w, h int) [][]bool { visit := make([][]bool, w) for i := 0; i \u0026lt; w; i++ { visit[i] = make([]bool, h) } return visit } 695. 岛屿的最大面积 695. 岛屿的最大面积 给你一个大小为 m x n 的二进制矩阵 grid 。\n岛屿 是由一些相邻的 1 (代表土地) 构成的组合，这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0（代表水）包围着。\n岛屿的面积是岛上值为 1 的单元格的数目。\n计算并返回 grid 中最大的岛屿面积。如果没有岛屿，则返回面积为 0 。\nfunc maxAreaOfIsland(grid [][]int) int { m, n := len(grid), len(grid[0]) var dfs func(i, j int) int dfs = func(i, j int) int { if i \u0026lt; 0 || j \u0026lt; 0 || i \u0026gt;= m || j \u0026gt;= n || grid[i][j] == 0 { return 0 } // 沉没岛屿, 避免重复计算  grid[i][j] = 0 cnt := 1 cnt += dfs(i-1, j) cnt += dfs(i+1, j) cnt += dfs(i, j-1) cnt += dfs(i, j+1) return cnt } ans := 0 for i := 0; i \u0026lt; m; i++ { for j := 0; j \u0026lt; n; j++ { if grid[i][j] == 1 { ans = max(ans, dfs(i, j)) } } } return ans } 130. 被围绕的区域 130. 被围绕的区域 给你一个 m x n 的矩阵 board ，由若干字符 \u0026lsquo;X\u0026rsquo; 和 \u0026lsquo;O\u0026rsquo; ，找到所有被 \u0026lsquo;X\u0026rsquo; 围绕的区域，并将这些区域里所有的 \u0026lsquo;O\u0026rsquo; 用 \u0026lsquo;X\u0026rsquo; 填充。\n思路：\n 需要从上下左右边界开始找（因为从中间找的话可能找到的O其实是跟边界相连着的，此时这个O是不能替换成X的） 替换与边界相连的O为任意字符# 最后遍历整个board，修正整个board  func solve(board [][]byte) { m, n := len(board), len(board[0]) var dfs func(i, j int) dfs = func(i, j int) { // 判断i，j是否位于边界  if i \u0026lt; 0 || i \u0026gt; m - 1 || j \u0026lt; 0 || j \u0026gt; n - 1 || board[i][j] != \u0026#39;O\u0026#39; { return } // 置为任意字符  board[i][j] = \u0026#39;#\u0026#39; dfs(i+1, j) dfs(i-1, j) dfs(i, j+1) dfs(i, j-1) } // 从左右两边出发，将与边界上的O字符置为#字符  for i := 0; i \u0026lt; m; i++ { dfs(i, 0) dfs(i, n-1) } // 从上下两边出发，将与边界上的O字符置为#字符  for j := 0; j \u0026lt; n; j++ { dfs(0, j) dfs(m-1, j) } for i := 0; i \u0026lt; m; i++ { for j := 0; j \u0026lt; n; j++ { // 没有被置为#的O字符，证明没有同边界相连，可以替换  if board[i][j] == \u0026#39;O\u0026#39; { board[i][j] = \u0026#39;X\u0026#39; } else if board[i][j] == \u0026#39;#\u0026#39; { // 对于#，实际上是同边界相连的O，所以需要还原  board[i][j] = \u0026#39;O\u0026#39; } } } } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-dfs/doc/","summary":"22. 括号生成 22. 括号生成 数字 n 代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。\nfunc generateParenthesis(n int) []string { ans := []string{} var dfs func(lb, rb int, s string) dfs = func(lb, rb int, s string) { if lb \u0026gt; rb { return } if lb == 0 \u0026amp;\u0026amp; rb == 0 { // 括号用完了，并且是合法的  if isValid(s) { ans = append(ans, s) } return } if lb != 0 { dfs(lb - 1, rb, s + \u0026#34;(\u0026#34;) } if rb !","title":"算法-DFS"},{"content":"","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-hash%E8%A1%A8/doc/","summary":"hash表也算是刷题中的老常客了","title":"算法-hash表"},{"content":"146. LRU 缓存 146. LRU 缓存 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。\n实现 LRUCache 类：\n LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。 void put(int key, int value) 如果关键字 key 已经存在，则变更其数据值 value ；如果不存在，则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ，则应该 逐出 最久未使用的关键字。  函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。\ntype LRUCache struct { capacity, size int nodes map[int]*DLinkedNode head, tail *DLinkedNode } type DLinkedNode struct { key, val int prev, next *DLinkedNode } func Constructor(capacity int) LRUCache { c := LRUCache{ capacity: capacity, nodes: make(map[int]*DLinkedNode), head: \u0026amp;DLinkedNode{}, tail: \u0026amp;DLinkedNode{}, } c.head.next = c.tail c.tail.prev = c.head return c } func (c *LRUCache) Get(key int) int { node, ok := c.nodes[key] if !ok { return -1 } c.moveToHead(node) return node.val } func (c *LRUCache) Put(key int, val int) { old, ok := c.nodes[key] if !ok { node := \u0026amp;DLinkedNode{key: key, val: val} c.nodes[key] = node c.addToHead(node) c.size++ if c.size \u0026gt; c.capacity { tail := c.removeTail() delete(c.nodes, tail.key) c.size-- } } else { old.val = val c.moveToHead(old) } } func (c *LRUCache) removeNode(node *DLinkedNode) { node.prev.next = node.next node.next.prev = node.prev } func (c *LRUCache) removeTail() *DLinkedNode { node := c.tail.prev c.removeNode(node) return node } func (c *LRUCache) moveToHead(node *DLinkedNode) { c.removeNode(node) c.addToHead(node) } func (c *LRUCache) addToHead(node *DLinkedNode) { node.prev = c.head node.next = c.head.next c.head.next.prev = node c.head.next = node } 460. LFU 缓存 460. LFU 缓存 请你为 最不经常使用（LFU）缓存算法设计并实现数据结构。\n实现 LFUCache 类：\n LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象 int get(int key) - 如果键 key 存在于缓存中，则获取键的值，否则返回 -1 。 void put(int key, int value) - 如果键 key 已存在，则变更其值；如果键不存在，请插入键值对。当缓存达到其容量 capacity 时，则应该在插入新项之前，移除最不经常使用的项。在此问题中，当存在平局（即两个或更多个键具有相同使用频率）时，应该去除 最近最久未使用 的键。  为了确定最不常使用的键，可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。\n当一个键首次插入到缓存中时，它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作，使用计数器的值将会递增。\n函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。\ntype LFUCache struct { keyToVal map[int]*Node freqToNodes map[int]*DoubleList capacity int minFreq int } func Constructor(capacity int) LFUCache { c := LFUCache{ capacity: capacity, minFreq: 0, keyToVal: make(map[int]*Node), freqToNodes: make(map[int]*DoubleList), } return c } type Node struct { key int val int prev *Node next *Node freq int } func (c *LFUCache) Get(key int) int { node, ok := c.keyToVal[key] if !ok { return -1 } c.increseFreq(node) return node.val } func (c *LFUCache) Put(key int, value int) { if c.capacity == 0 { return } // 1. 存在则调整节点的频率即可 \tnode, ok := c.keyToVal[key] if ok { node.val = value c.increseFreq(node) return } // 2. 判断capacity, 容量不足，删除最少访问的节点 \tif len(c.keyToVal) == c.capacity { c.deleteMinFreqNodes() } // 3. 添加节点到双向链表和索引表 \tnode = \u0026amp;Node{key: key, val: value, freq: 1} c.keyToVal[key] = node if c.freqToNodes[node.freq] == nil { c.freqToNodes[node.freq] = NewDoubleList() } c.freqToNodes[node.freq].Add(node) c.minFreq = 1 } func (c *LFUCache) increseFreq(node *Node) { originFreq := node.freq node.freq++ // 1. 频率更新后，将节点从原来的频率链表中移除 \tdl := c.freqToNodes[originFreq] // O(1) \tdl.Remove(node) // 2. 判断最低频率是否需要更新 \tif dl.IsEmpty() \u0026amp;\u0026amp; originFreq == c.minFreq { c.minFreq++ } // 3. 将更新后的节点插入对应频率的链表的表头 \tif c.freqToNodes[node.freq] == nil { c.freqToNodes[node.freq] = NewDoubleList() } c.freqToNodes[node.freq].Add(node) } func (c *LFUCache) deleteMinFreqNodes() { // 1. 移除最少访问节点，双向链表按时间排序，新的在头，旧的在尾 \tdl := c.freqToNodes[c.minFreq] lastn := dl.Last() dl.Remove(lastn) // 2. 移除索引 \tdelete(c.keyToVal, lastn.key) } type DoubleList struct { head, tail *Node } func NewDoubleList() *DoubleList { head, tail := new(Node), new(Node) head.next, tail.prev = tail, head dl := \u0026amp;DoubleList{ head: head, tail: tail, } return dl } func (dl *DoubleList) Add(node *Node) { prev, next := dl.head, dl.head.next prev.next, node.prev = node, prev next.prev, node.next = node, next } func (dl *DoubleList) Remove(node *Node) { prev, next := node.prev, node.next prev.next, next.prev = next, prev node.prev, node.next = nil, nil } func (dl *DoubleList) First() *Node { return dl.head.next } func (dl *DoubleList) Last() *Node { return dl.tail.prev } func (dl *DoubleList) IsEmpty() bool { return dl.head.next == dl.tail } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-lru%E5%92%8Clfu/doc/","summary":"经典面试题了属实是","title":"算法-LRU和LFU"},{"content":"4. 寻找两个正序数组的中位数 4. 寻找两个正序数组的中位数\n给定两个大小分别为 m 和 n 的正序（从小到大）数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (m+n)) 。\n换个角度想一想：\n 如果m+n为奇数，中位数也就是top((m+n)/2)； 如果m+n是偶数，中位数也就是top((m+n)/2)和top((m+n)/2+1)的平均值  func findMedianSortedArrays(nums1 []int, nums2 []int) float64 { length := len(nums1) + len(nums2) mid := length / 2 if length % 2 == 1 { return float64(getKthElement(nums1, nums2, mid + 1)) } return float64(getKthElement(nums1, nums2, mid) + getKthElement(nums1, nums2, mid+1)) * 0.5 } func getKthElement(nums1, nums2 []int, k int) int { m, n := len(nums1), len(nums2) from1, from2 := 0, 0 for { if from1 == len(nums1) { // nums1走完了，所以仅需在num2中找当前的第k小数，第k小的数的下标即为k - 1, 但需要从start点偏移而来  return nums2[from2 + k - 1] } if from2 == len(nums2) { return nums1[from1 + k - 1] } // 找两个排序数组的最小数，即为两数组数组头的较小数  if k == 1 { return min(nums1[from1], nums2[from2]) } offset := k / 2 - 1 to1 := min(from1 + offset, m - 1) // 越界则取最后一个  to2 := min(from2 + offset, n - 1) // 比较nums1[k/2-1]和nums2[k/2-1]的大小，来减少范围  if nums1[to1] \u0026lt; nums2[to2] { // k减去剔除的数量，比如原来是求第5小的数，现在剔除了2个较小的数，则目前应该求第3小的数了  k -= (to1 - from1 + 1) // 剔除nums1[start1]  from1 = to1 + 1 } else { k -= (to2 - from2 + 1) from2 = to2 + 1 } } } 思路：\n 在两个有序数组里求第K大的数，我们可以每次比较两个数组里k/2-1位置上的数  nums1: [1, 3, 8, 9] nums2: [2, 5, 6, 7] 比如，我们在上面的两个数组里求第5大的数，那么k/2-1=1，所以比较nums1[1]和nums[2]的大小，这里 3 \u0026lt; 5\n 这里首先nums1中比3小的有k/2-1个，nums2比5小的有k/2-1个，此时3又小于5，那么比3小的那k/2-1个数不可能是第K大的数\n  把不可能的数从数组中划掉，得到\n  nums1: [8, 9] nums2: [2, 5, 6, 7]  此时因为减掉了两个数，我们要求的就不是第5大的数了，而是第3大的数\n  重复以上逻辑，直到k等于1了，或者nums1或nums2中的某个数组空了\n  347. 前 K 个高频元素 347. 前 K 个高频元素 给你一个整数数组 nums 和一个整数 k ，请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。\nfunc topKFrequent(nums []int, k int) []int { cnt := make(map[int]int) for _, num := range nums { cnt[num]++ } keys := []int{} for k := range cnt { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { return cnt[keys[i]] \u0026gt; cnt[keys[j]] }) return keys[:k] } 215. 数组中的第K个最大元素 215. 数组中的第K个最大元素 给定整数数组 nums 和整数 k，请返回数组中第 k 个最大的元素。\n请注意，你需要找的是数组排序后的第 k 个最大的元素，而不是第 k 个不同的元素。\nfunc findKthLargest(nums []int, k int) int { h := hp{} for _, num := range nums { heap.Push(\u0026amp;h, num) if len(h) \u0026gt; k { heap.Pop(\u0026amp;h) } } return h[0] } type hp []int func (h hp) Len() int { return len(h) } func (h hp) Less(i, j int) bool { return h[i] \u0026lt; h[j] } func (h hp) Swap(i, j int) { h[i], h[j] = h[j], h[i]} func (h *hp) Push(x interface{}) { *h = append(*h, x.(int)) } func (h *hp) Pop() interface{} { old := *h n := len(old) x := old[n-1] *h = old[0:n-1] return x } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-topk/doc/","summary":"通常可以使用堆来解决topK的问题","title":"算法-topK"},{"content":"617. 合并二叉树 617. 合并二叉树 给你两棵二叉树： root1 和 root2 。\n想象一下，当你将其中一棵覆盖到另一棵之上时，两棵树上的一些节点将会重叠（而另一些不会）。你需要将这两棵树合并成一棵新二叉树。合并的规则是：如果两个节点重叠，那么将这两个节点的值相加作为合并后节点的新值；否则，不为 null 的节点将直接作为新二叉树的节点。\n返回合并后的二叉树。\n注意: 合并过程必须从两个树的根节点开始。\nfunc mergeTrees(root1 *TreeNode, root2 *TreeNode) *TreeNode { if root1 == nil { return root2 } if root2 == nil { return root1 } root := \u0026amp;TreeNode{} root.Val = root1.Val + root2.Val root.Left = mergeTrees(root1.Left, root2.Left) root.Right = mergeTrees(root1.Right, root2.Right) return root } 543. 二叉树的直径 543. 二叉树的直径 给定一棵二叉树，你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。\nfunc diameterOfBinaryTree(root *TreeNode) int { ans := 0 // fn定义为获取一个节点的深度  var depth func(root *TreeNode) int depth = func(root *TreeNode) int { if root == nil { return 0 } // 直径即为左深度加右深度  ld := depth(root.Left) rd := depth(root.Right) length := ld + rd if length \u0026gt; ans { ans = length } // 自己加上左右两边的长度  return max(ld, rd) + 1 } depth(root) return ans } 538. 把二叉搜索树转换为累加树 538. 把二叉搜索树转换为累加树 给出二叉 搜索 树的根节点，该树的节点值各不相同，请你将其转换为累加树（Greater Sum Tree），使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。\n提醒一下，二叉搜索树满足下列约束条件：\n节点的左子树仅包含键 小于 节点键的节点。 节点的右子树仅包含键 大于 节点键的节点。 左右子树也必须是二叉搜索树。\nfunc convertBST(root *TreeNode) *TreeNode { preVal := 0 var traverse func(root *TreeNode) traverse = func(root *TreeNode) { if root == nil { return } traverse(root.Right) root.Val = root.Val + preVal preVal = root.Val traverse(root.Left) } traverse(root) return root } 437. 路径总和 III 437. 路径总和 III 给定一个二叉树的根节点 root ，和一个整数 targetSum ，求该二叉树里节点值之和等于 targetSum 的 路径 的数目。\n路径 不需要从根节点开始，也不需要在叶子节点结束，但是路径方向必须是向下的（只能从父节点到子节点）。\nfunc pathSum(root *TreeNode, targetSum int) int { cnt := 0 preSum := map[int]int{0: 1} // 从root点出发，能找到和为target的path的数量  var dfs func(root *TreeNode, curr int) dfs = func(root *TreeNode, curr int) { if root == nil { return } curr += root.Val cnt += preSum[curr - targetSum] preSum[curr]++ dfs(root.Left, curr) dfs(root.Right, curr) // 当左边和右边都处理完后，回溯当前的节点产生的和  preSum[curr]-- } dfs(root, 0) return cnt } 235. 二叉搜索树的最近公共祖先 235. 二叉搜索树的最近公共祖先\n给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。\n百度百科中最近公共祖先的定义为：“对于有根树 T 的两个结点 p、q，最近公共祖先表示为一个结点 x，满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）。”\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { if root == nil { return nil } if root == p || root == q { return root } // 在左边找  if p.Val \u0026lt; root.Val \u0026amp;\u0026amp; q.Val \u0026lt; root.Val { return lowestCommonAncestor(root.Left, p, q) } // 在右边找  if p.Val \u0026gt; root.Val \u0026amp;\u0026amp; q.Val \u0026gt; root.Val { return lowestCommonAncestor(root.Right, p, q) } // 一大一小的公共祖先一定是root  return root } 236. 二叉树的最近公共祖先 236. 二叉树的最近公共祖先 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。\nfunc lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { if root == nil { return nil } if root == p || root == q { return root } // 在左边找  left := lowestCommonAncestor(root.Left, p, q) // 在右边找  right := lowestCommonAncestor(root.Right, p, q) // 情况1： p, q不存在  if left == nil \u0026amp;\u0026amp; right == nil { return nil } // 情况2: p，q各自存在与左右子树中  if left != nil \u0026amp;\u0026amp; right != nil { return root } // 情况3: p，qt同在一边  if left == nil { return right } else { return left } } 226. 翻转二叉树 226. 翻转二叉树 给你一棵二叉树的根节点 root ，翻转这棵二叉树，并返回其根节点。\nfunc invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } root.Left, root.Right = root.Right, root.Left invertTree(root.Left) invertTree(root.Right) return root } 124. 二叉树中的最大路径和 124. 二叉树中的最大路径和 路径 被定义为一条从树中任意节点出发，沿父节点-子节点连接，达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点，且不一定经过根节点。\n路径和 是路径中各节点值的总和。\n给你一个二叉树的根节点 root ，返回其 最大路径和 。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func maxPathSum(root *TreeNode) int { maxPath := -1001 var maxGain func(root *TreeNode) int maxGain = func(root *TreeNode) int { if root == nil { return 0 } leftGain := max(maxGain(root.Left), 0) rightGain := max(maxGain(root.Right), 0) // 当前节点+左边路径+右边路径即为一个path  maxPath = max(maxPath, root.Val + leftGain + rightGain) return root.Val + max(leftGain, rightGain) } maxGain(root) return maxPath } func max(x, y int) int { if x \u0026gt; y { return x } return y } 114. 二叉树展开为链表 114. 二叉树展开为链表 给你二叉树的根结点 root ，请你将它展开为一个单链表：\n展开后的单链表应该同样使用 TreeNode ，其中 right 子指针指向链表中下一个结点，而左子指针始终为 null 。 展开后的单链表应该与二叉树 先序遍历 顺序相同。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func flatten(root *TreeNode) { curr := root for curr != nil { if curr.Left != nil { left := curr.Left // 在左子树中寻找最右边的节点，这个节点会是curr右子树的前驱节点  rLeft := left for rLeft.Right != nil { rLeft = rLeft.Right } rLeft.Right = curr.Right curr.Right = left curr.Left = nil } curr = curr.Right } } 105. 从前序与中序遍历序列构造二叉树 105. 从前序与中序遍历序列构造二叉树 给定两个整数数组 preorder 和 inorder ，其中 preorder 是二叉树的先序遍历， inorder 是同一棵树的中序遍历，请构造二叉树并返回其根节点。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func buildTree(preorder []int, inorder []int) *TreeNode { if len(inorder) == 0 { return nil } rootVal := preorder[0] idx := 0 // 左右子树分节点  for i, val := range inorder { if val == rootVal { idx = i break } } root := \u0026amp;TreeNode{Val: rootVal} root.Left = buildTree(preorder[1:idx+1], inorder[:idx]) root.Right = buildTree(preorder[idx+1:], inorder[idx+1:]) return root } 106. 从中序与后序遍历序列构造二叉树 106. 从中序与后序遍历序列构造二叉树 给定两个整数数组 inorder 和 postorder ，其中 inorder 是二叉树的中序遍历， postorder 是同一棵树的后序遍历，请你构造并返回这颗 二叉树 。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func buildTree(inorder []int, postorder []int) *TreeNode { if len(inorder) == 0 { return nil } rootVal := postorder[len(postorder)-1] idx := 0 // 左右子树分节点  for i, val := range inorder { if val == rootVal { idx = i break } } root := \u0026amp;TreeNode{Val: rootVal} root.Left = buildTree(inorder[:idx], postorder[:idx]) root.Right = buildTree(inorder[idx+1:], postorder[idx:len(postorder)-1]) return root } 104. 二叉树的最大深度 104. 二叉树的最大深度 给定一个二叉树，找出其最大深度。\n二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。\n说明: 叶子节点是指没有子节点的节点。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func maxDepth(root *TreeNode) int { if root == nil { return 0 } return max(maxDepth(root.Left), maxDepth(root.Right)) + 1 } func max(x, y int) int { if x \u0026gt; y { return x } return y } 102. 二叉树的层序遍历 102. 二叉树的层序遍历 给你二叉树的根节点 root ，返回其节点值的 层序遍历 。 （即逐层地，从左到右访问所有节点）。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func levelOrder(root *TreeNode) [][]int { ans := [][]int{} if root == nil { return ans } q := []*TreeNode{root} for len(q) != 0 { level := []int{} size := len(q) for i := 0; i \u0026lt; size; i++ { level = append(level, q[i].Val) if q[i].Left != nil { q = append(q, q[i].Left) } if q[i].Right != nil { q = append(q, q[i].Right) } } ans = append(ans, level) q = q[size:] } return ans } 101. 对称二叉树 101. 对称二叉树 给你一个二叉树的根节点 root ， 检查它是否轴对称。\nfunc isSymmetric(root *TreeNode) bool { return check(root, root) } func check(left *TreeNode, right *TreeNode) bool { if left == nil \u0026amp;\u0026amp; right == nil { return true } if left == nil || right == nil { return false } if left.Val != right.Val { return false } return check(left.Left, right.Right) \u0026amp;\u0026amp; check(left.Right, right.Left) } 98. 验证二叉搜索树 98. 验证二叉搜索树 给你一个二叉树的根节点 root ，判断其是否是一个有效的二叉搜索树。\n有效 二叉搜索树定义如下：\n节点的左子树只包含 小于 当前节点的数。 节点的右子树只包含 大于 当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。\nfunc isValidBST(root *TreeNode) bool { return f(root, math.MinInt64, math.MaxInt64) } func f(root *TreeNode, min int64, max int64) bool { if root == nil { return true } if int64(root.Val) \u0026lt;= min || int64(root.Val) \u0026gt;= max { return false } return f(root.Left, min, int64(root.Val)) \u0026amp;\u0026amp; f(root.Right, int64(root.Val), max) } 96. 不同的二叉搜索树 96. 不同的二叉搜索树 给你一个整数 n ，求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种？返回满足题意的二叉搜索树的种数。\nclass Solution { public int numTrees(int n) { // 状态 节点数  // 选择 选择哪个节点作为根节点  // 状态转移方程  // dp(n) 使用n个节点，能组成的二叉搜索树种数  // f(i, n) 使用i节点为根，能组成长度为n的二叉搜索树种数  // dp(n) = sum(f(i, n)), i属于1到n  // f(i, n) = dp(i - 1) * dp(n - i)  // dp(n) = sum(dp(i - 1) * dp(n - i)), i属于1到n  // baseCase  // dp[0] = 0  // dp[1] = 1  // dp[2] = 2  // dp[3] = 5  int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = 1; for (int i = 2; i \u0026lt;= n; i++) { for (int j = 1; j \u0026lt;= i; j++) { dp[i] += dp[j - 1] * dp[i - j]; } } return dp[n]; } } 94. 二叉树的中序遍历 94. 二叉树的中序遍历 给定一个二叉树的根节点 root ，返回它的 中序 遍历。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ // 递归 func inorderTraversal(root *TreeNode) []int { ans := []int{} var r func(root *TreeNode) r = func(root *TreeNode) { if root == nil { return } r(root.Left) ans = append(ans, root.Val) r(root.Right) } r(root) return ans } // 迭代 func inorderTraversal2(root *TreeNode) []int { if root == nil { return []int{} } // 核心思想要用栈模拟  stack := []*TreeNode{} ans := []int{} curr := root // 用来指向当前操作的节点  for curr != nil || len(stack) != 0 { if curr != nil { stack = append(stack, curr) curr = curr.Left } else { n := len(stack) - 1 curr = stack[n] stack = stack[:n] ans = append(ans, curr.Val) curr = curr.Right } } return ans } 110. 平衡二叉树 110. 平衡二叉树 给定一个二叉树，判断它是否是高度平衡的二叉树。\n本题中，一棵高度平衡二叉树定义为：\n一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func isBalanced(root *TreeNode) bool { h := height(root) return h != -1 } func height(root *TreeNode) int { if root == nil { return 0 } leftH := height(root.Left) rightH := height(root.Right) if leftH == -1 || rightH == -1 || abs(leftH - rightH) \u0026gt; 1 { return -1 // -1代表不平衡，不需要再继续了  } return max(leftH, rightH) + 1 } 129. 求根节点到叶节点数字之和 129. 求根节点到叶节点数字之和 给你一个二叉树的根节点 root ，树中每个节点都存放有一个 0 到 9 之间的数字。 每条从根节点到叶节点的路径都代表一个数字：\n例如，从根节点到叶节点的路径 1 -\u0026gt; 2 -\u0026gt; 3 表示数字 123 。 计算从根节点到叶节点生成的 所有数字之和 。\n叶节点 是指没有子节点的节点。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func sumNumbers(root *TreeNode) int { if root == nil { return 0 } var dfs func(root *TreeNode, sum int) int dfs = func(root *TreeNode, sum int) int { if root == nil { return 0 } rootSum := root.Val + sum * 10 if root.Left == nil \u0026amp;\u0026amp; root.Right == nil { return rootSum } leftSum := dfs(root.Left, rootSum) rightSum := dfs(root.Right, rootSum) return leftSum + rightSum } return dfs(root, 0) } 109. 有序链表转换二叉搜索树 109. 有序链表转换二叉搜索树\n思路：\n 找到链表的中点，一分为二 中点为head，并且递归生成左右子树 当 left == right 时，证明已经构建完成（可以这么理解，left和right是左闭右开，当left==right时，证明left已经超过边界了）  /** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ /** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func sortedListToBST(head *ListNode) *TreeNode { return build(head, nil) } func build(left *ListNode, right *ListNode) *TreeNode { if left == right { return nil } mid := findMid(left, right) head := \u0026amp;TreeNode{Val: mid.Val} head.Left = build(left, mid) head.Right = build(mid.Next, right) return head } func findMid(left, right *ListNode) *ListNode { s, f := left, left for f != right \u0026amp;\u0026amp; f.Next != right { s = s.Next f = f.Next.Next } return s } 450. 删除二叉搜索树中的节点 450. 删除二叉搜索树中的节点\n给定一个二叉搜索树的根节点 root 和一个值 key，删除二叉搜索树中的 key 对应的节点，并保证二叉搜索树的性质不变。返回二叉搜索树（有可能被更新）的根节点的引用。\n一般来说，删除节点可分为两个步骤：\n首先找到需要删除的节点； 如果找到了，删除它。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func deleteNode(root *TreeNode, key int) *TreeNode { if root == nil { return nil } if root.Val == key { // 叶子节点，直接删除  if root.Left == nil \u0026amp;\u0026amp; root.Right == nil { // 当前节点被删，返回空  return nil } // 左子树为空，右子树上来继位  if root.Left == nil { return root.Right } // 右子树为空，左子树上来继位  if root.Right == nil { return root.Left } // 左右都不为空，将左子树的头节点接到右子树里最左节点的左节点上  leftRoot := root.Left leftestNode := root.Right // 右子树里最左边的节点  for leftestNode.Left != nil { leftestNode = leftestNode.Left } leftestNode.Left = leftRoot return root.Right } if root.Val \u0026lt; key { root.Right = deleteNode(root.Right, key) } else { root.Left = deleteNode(root.Left, key) } return root } 297. 二叉树的序列化与反序列化 297. 二叉树的序列化与反序列化\n序列化是将一个数据结构或者对象转换为连续的比特位的操作，进而可以将转换后的数据存储在一个文件或者内存中，同时也可以通过网络传输到另一个计算机环境，采取相反方式重构得到原数据。\n请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑，你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ type Codec struct {} func Constructor() Codec { c := Codec{} return c } // Serializes a tree to a single string. func (c *Codec) serialize(root *TreeNode) string { ans := []string{} q := []*TreeNode {root} for len(q) != 0 { node := q[0] q = q[1:] if node != nil { ans = append(ans, strconv.Itoa(node.Val)) q = append(q, node.Left) q = append(q, node.Right) } else { ans = append(ans, \u0026#34;X\u0026#34;) } } return strings.Join(ans, \u0026#34;,\u0026#34;) } // Deserializes your encoded data to tree. func (c *Codec) deserialize(data string) *TreeNode { if data == \u0026#34;X\u0026#34; { return nil } nodes := strings.Split(data, \u0026#34;,\u0026#34;) v, _ := strconv.Atoi(nodes[0]) root := \u0026amp;TreeNode{Val: v} q := []*TreeNode {root} curr := 1 for curr \u0026lt; len(nodes) { node := q[0] q = q[1:] leftVal := nodes[curr] if leftVal != \u0026#34;X\u0026#34; { _leftVal, _ := strconv.Atoi(leftVal) leftNode := \u0026amp;TreeNode{Val: _leftVal} node.Left = leftNode q = append(q, leftNode) } rightVal := nodes[curr+1] if rightVal != \u0026#34;X\u0026#34; { _rightVal, _ := strconv.Atoi(rightVal) rightNode := \u0026amp;TreeNode{Val: _rightVal} node.Right = rightNode q = append(q, rightNode) } curr += 2 } return root } /** * Your Codec object will be instantiated and called as such: * ser := Constructor(); * deser := Constructor(); * data := ser.serialize(root); * ans := deser.deserialize(data); */ 700. 二叉搜索树中的搜索 700. 二叉搜索树中的搜索\n给定二叉搜索树（BST）的根节点 root 和一个整数值 val。\n你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在，则返回 null 。\n/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */ func searchBST(root *TreeNode, val int) *TreeNode { if root == nil || root.Val == val { return root } if val \u0026lt;= root.Val { return searchBST(root.Left, val) } return searchBST(root.Right, val) } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E4%BA%8C%E5%8F%89%E6%A0%91/doc/","summary":"子串和子序列是一块难啃的骨头，但大多数时候可以通过动态规划来解决","title":"算法-二叉树"},{"content":"461. 汉明距离 461. 汉明距离 两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。\n给你两个整数 x 和 y，计算并返回它们之间的汉明距离。\nfunc hammingDistance(x int, y int) int { s := x ^ y // 汉明距离即为s中1的数量  res := 0 for s \u0026gt; 0 { // 判断最低位是不是1  res += s \u0026amp; 1 s \u0026gt;\u0026gt;= 1 } return res } 397. 整数替换 397. 整数替换\n给定一个正整数 n ，你可以做如下操作：\n如果 n 是偶数，则用 n / 2替换 n 。 如果 n 是奇数，则可以用 n + 1或n - 1替换 n 。 返回 n 变为 1 所需的 最小替换次数 。\nfunc integerReplacement(n int) int { /** * 二进制观察处理法 * 实现思路: * 最快的移动就是遇到2的次幂(例如数字16 10000 -\u0026gt; 01000 -\u0026gt; 00100 -\u0026gt; 00010 -\u0026gt; 00001) * 将二进制一直左移 最右为0时可以直接移动(例如数字6 000110 -\u0026gt; 000011) * 最右位为1时需把1变成0, 再移动(例如数字9 001001 -\u0026gt; 001000) * 故最优解就是如何在迭代中减少出现末尾1(就是什么时候+1, 什么时候-1 来实现过程中最少出现01或11结尾) * 得出以下结论: * 若n的二进制为 xxxx10, 则下一次处理 n = n/2 次数+1 * 若n的二进制为 xxxx01, 则下一次处理 n = n/2 次数+2(即需要先-1再除以2, 故这里是加2) n \u0026gt; 1 * 若n的二进制为 xxxx11, 则下一次处理 n = n/2 +1 次数+2(即需要先+1再除以2, 故这里是加2) n \u0026gt; 3 * 特殊情况: 数字3 000011, 000011 -\u0026gt; 000010 -\u0026gt; 000001(两次即可) * 边界条件: 000001 -\u0026gt; 答案为0 */ cnt := 0 for n != 1 { if n % 2 == 0 { // n为偶数  cnt++ n /= 2 } else if n == 3 { // 特殊case  cnt += 2 n = 1 } else if (n \u0026amp; 3) == 3 { // n的二进制格式为xxxxxx11  n++ cnt++ } else { // n的二进制格式为xxxxxx01  n-- cnt++ } } return cnt } ","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E4%BD%8D%E8%BF%90%E7%AE%97/doc/","summary":"461. 汉明距离 461. 汉明距离 两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。\n给你两个整数 x 和 y，计算并返回它们之间的汉明距离。\nfunc hammingDistance(x int, y int) int { s := x ^ y // 汉明距离即为s中1的数量  res := 0 for s \u0026gt; 0 { // 判断最低位是不是1  res += s \u0026amp; 1 s \u0026gt;\u0026gt;= 1 } return res } 397. 整数替换 397. 整数替换\n给定一个正整数 n ，你可以做如下操作：\n如果 n 是偶数，则用 n / 2替换 n 。 如果 n 是奇数，则可以用 n + 1或n - 1替换 n 。 返回 n 变为 1 所需的 最小替换次数 。","title":"算法-位运算"},{"content":"560. 和为 K 的子数组 560. 和为 K 的子数组 给你一个整数数组 nums 和一个整数 k ，请你统计并返回该数组中和为 k 的连续子数组的个数。\nfunc subarraySum(nums []int, k int) int { l := len(nums) presum := make([]int, l+1) presum[0] = 0 for i:= 0; i \u0026lt; l; i++ { presum[i+1] = presum[i] + nums[i] } res := 0 for i := 1; i \u0026lt;= l; i++ { for j := 0; j \u0026lt; i; j++ { // nums[j]到nums[i-1]的和为k,  if presum[i] - presum[j] == k { res++ } } } return res } 523. 连续的子数组和 523. 连续的子数组和\n给你一个整数数组 nums 和一个整数 k ，编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组：\n子数组大小 至少为 2 ，且 子数组元素总和为 k 的倍数。 如果存在，返回 true ；否则，返回 false 。\n如果存在一个整数 n ，令整数 x 符合 x = n * k ，则称 x 是 k 的一个倍数。0 始终视为 k 的一个倍数。\n思路：\n 利用hash表记录余数和下标的关系 当余数没有出现时，加入hash表中 当余数已经出现过，则判断是否长度大于1 两种情况符合要求：前缀和直接为k的倍数，或者前缀和之差为k的倍数。加一个边界条件0: -1可以省的判断前一种情况 i - idx 为什么不是 \u0026gt;= 1, 因为pres的val的下标是从-1开始的  func checkSubarraySum(nums []int, k int) bool { if len(nums) \u0026lt; 2 { return false } pres := map[int]int{0: -1} sum := 0 for i, num := range nums { sum = (sum + num) % k if idx, ok := pres[sum]; ok { if i - idx \u0026gt; 1 { return true } } else { pres[sum] = i } } return false } 528. 按权重随机选择 528. 按权重随机选择 给你一个 下标从 0 开始 的正整数数组 w ，其中 w[i] 代表第 i 个下标的权重。\n请你实现一个函数 pickIndex ，它可以 随机地 从范围 [0, w.length - 1] 内（含 0 和 w.length - 1）选出并返回一个下标。选取下标 i 的 概率 为 w[i] / sum(w) 。\n例如，对于 w = [1, 3]，挑选下标 0 的概率为 1 / (1 + 3) = 0.25 （即，25%），而选取下标 1 的概率为 3 / (1 + 3) = 0.75（即，75%）。\n思路：\n假设对于数组w, w = [2, 5, 1], 则选中下标0的概率为 2 / 8, 选择下标1的概率为 5 / 8，选中下标2的概率为 1 / 8，将数组分为3段看，[1, 2], [3, 7], [7, 8]，那么则在1-8之间随机一个数，并看这个数落在哪个段内\ntype Solution struct { pres []int } func Constructor(w []int) Solution { // 将w转化为前缀和  for i := 1; i \u0026lt; len(w); i++ { w[i] += w[i-1] } return Solution{w} } func (s *Solution) PickIndex() int { weight := s.pres[len(s.pres)-1] x := rand.Intn(weight) + 1 return sort.SearchInts(s.pres, x) } /** * Your Solution object will be instantiated and called as such: * obj := Constructor(w); * param_1 := obj.PickIndex(); */ ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%89%8D%E7%BC%80%E5%92%8C/doc/","summary":"前缀和也是常见的数组题的解法了吧","title":"算法-前缀和"},{"content":"55. 跳跃游戏 55. 跳跃游戏 给定一个非负整数数组 nums ，你最初位于数组的 第一个下标 。\n数组中的每个元素代表你在该位置可以跳跃的最大长度。\n判断你是否能够到达最后一个下标。\nfunc canJump(nums []int) bool { l := len(nums) if l == 0 || l == 1 { return true } // 表示能不能到达第i位下标，true代表可以，false代表不可以  d := make([]bool, l) d[0] = true for i := 1; i \u0026lt; l; i++ { for j := i - 1; j \u0026gt;= 0; j-- { // 能找到一个j就好了  d[i] = d[j] \u0026amp;\u0026amp; (j + nums[j]) \u0026gt;= i if d[i] { break } } if !d[i] { return false } } return d[l - 1] } 62. 不同路径 62. 不同路径 一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。\n机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。\n问总共有多少条不同的路径？\nfunc uniquePaths(m int, n int) int { // dp[i][j] 表示从[0, 0]走到[i, j]总共有多少条路径  dp := make([][]int, m) for i, _ := range dp { dp[i] = make([]int, n) } for i := 0; i \u0026lt; m; i++ { for j := 0; j \u0026lt; n; j++ { // [i,j] 一定是由[i-1, j]或[i][j-1]过来的  if i == 0 || j == 0 { dp[i][j] = 1 continue } dp[i][j] = dp[i-1][j] + dp[i][j-1] } } return dp[m-1][n-1] } 64. 最小路径和 64. 最小路径和 给定一个包含非负整数的 m x n 网格 grid ，请找出一条从左上角到右下角的路径，使得路径上的数字总和为最小。\n说明：每次只能向下或者向右移动一步。\nfunc minPathSum(grid [][]int) int { m, n := len(grid), len(grid[0]) dp := make([][]int, m) for i := range dp { dp[i] = make([]int, n) } dp[0][0] = grid[0][0] for i := 1; i \u0026lt; m; i++ { dp[i][0] = dp[i-1][0] + grid[i][0] } for j := 1; j \u0026lt; n; j++ { dp[0][j] = dp[0][j-1] + grid[0][j] } for i := 1; i \u0026lt; m; i++ { for j := 1; j \u0026lt; n; j++ { dp[i][j] = min(dp[i][j-1], dp[i-1][j]) + grid[i][j] } } return dp[m-1][n-1] } 72. 编辑距离 72. 编辑距离 给你两个单词 word1 和 word2， 请返回将 word1 转换成 word2 所使用的最少操作数 。\n你可以对一个单词进行如下三种操作：\n插入一个字符 删除一个字符 替换一个字符\nfunc minDistance(word1 string, word2 string) int { n, m := len(word1), len(word2) dp := make([][]int, n+1) // dp[i][j]表示w1[0:i]和w2[0:j]匹配需要的操作次数  for i := range dp { dp[i] = make([]int, m+1) } for j := 0; j \u0026lt;= m; j++ { // 注意 \u0026lt;=  dp[0][j] = j } for i := 0; i \u0026lt;= n; i++ { dp[i][0] = i } for i := 1; i \u0026lt;= n; i++ { // 注意 \u0026lt;=  for j := 1; j \u0026lt;= m; j++ { if word1[i-1] == word2[j-1] { dp[i][j] = dp[i-1][j-1] } else { dp[i][j] = min(min(dp[i-1][j], dp[i-1][j-1]), dp[i][j-1]) + 1 } } } return dp[n][m] } 322. 零钱兑换 322. 零钱兑换 给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。\n计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。\n你可以认为每种硬币的数量是无限的。\nfunc coinChange(coins []int, amount int) int { if amount == 0 { return 0 } // dp[i]表示组成i金额的最少硬币数  dp := make([]int, amount + 1) dp[0] = 0 for i := 1; i \u0026lt; len(dp); i++ { dp[i] = math.MaxInt32 } for i := 1; i \u0026lt;= amount; i++ { // 枚举所有的硬币面额  for _, coin := range coins { if i - coin \u0026lt; 0 { continue } // 求组成金额i的最少硬币数 = 组成i-coin + 1  dp[i] = min(dp[i], dp[i-coin] + 1) } } if dp[amount] == math.MaxInt32 { return -1 } return dp[amount] } 338. 比特位计数 338. 比特位计数 给你一个整数 n ，对于 0 \u0026lt;= i \u0026lt;= n 中的每个 i ，计算其二进制表示中 1 的个数 ，返回一个长度为 n + 1 的数组 ans 作为答案。\nfunc countBits(n int) []int { dp := make([]int, n+1) for i := 0; i \u0026lt;= n; i++ { if i % 2 == 0 { dp[i] = dp[i/2] // 偶数的1的个数，可以想象一个4(0100)和8(1000), 只是左移了一下，末尾补了个0，没有增加1的个数  } else { dp[i] = dp[i-1] + 1 // 奇数的1的个数就是比上个偶数多了个1  } } return dp } 279. 完全平方数 279. 完全平方数 给你一个整数 n ，返回 和为 n 的完全平方数的最少数量 。\n完全平方数 是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，1、4、9 和 16 都是完全平方数，而 3 和 11 不是。\nfunc numSquares(n int) int { // dp[i] 表示和为i的完全平方数的最少数量  dp := make([]int, n+1) for i := 1; i \u0026lt;= n; i++ { dp[i] = math.MaxInt32 for j := 1; j * j \u0026lt;= i; j++ { dp[i] = min(dp[i-j*j] + 1, dp[i]) } } return dp[n] } 221. 最大正方形 221. 最大正方形 在一个由 \u0026lsquo;0\u0026rsquo; 和 \u0026lsquo;1\u0026rsquo; 组成的二维矩阵内，找到只包含 \u0026lsquo;1\u0026rsquo; 的最大正方形，并返回其面积。\nfunc maximalSquare(matrix [][]byte) int { m, n := len(matrix), len(matrix[0]) // dp[i + 1][j + 1] 表示 「以第 i 行、第 j 列为右下角的正方形的最大边长」  dp := make([][]int, m + 1) for i := range dp { dp[i] = make([]int, n + 1) } ans := 0 for i := 0; i \u0026lt; m; i++ { for j := 0; j \u0026lt; n; j++ { if matrix[i][j] == \u0026#39;1\u0026#39; { dp[i+1][j+1] = 1+ min(dp[i][j], min(dp[i+1][j], dp[i][j+1])) if dp[i+1][j+1] \u0026gt; ans { ans = dp[i+1][j+1] } } } } return ans * ans } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/doc/","summary":"55. 跳跃游戏 55. 跳跃游戏 给定一个非负整数数组 nums ，你最初位于数组的 第一个下标 。\n数组中的每个元素代表你在该位置可以跳跃的最大长度。\n判断你是否能够到达最后一个下标。\nfunc canJump(nums []int) bool { l := len(nums) if l == 0 || l == 1 { return true } // 表示能不能到达第i位下标，true代表可以，false代表不可以  d := make([]bool, l) d[0] = true for i := 1; i \u0026lt; l; i++ { for j := i - 1; j \u0026gt;= 0; j-- { // 能找到一个j就好了  d[i] = d[j] \u0026amp;\u0026amp; (j + nums[j]) \u0026gt;= i if d[i] { break } } if !","title":"算法-动态规划"},{"content":"5. 最长回文子串 5. 最长回文子串\n给你一个字符串 s，找到 s 中最长的回文子串。\nfunc longestPalindrome(s string) string { n := len(s) dp := make([][]bool, n) for i := range dp { dp[i] = make([]bool, n) } dp[0][0] = true begin, maxLen := 0, 1 for j := 1; j \u0026lt; n; j++ { for i := j; i \u0026gt;= 0; i-- { { if i == j { dp[i][j] = true // basecase  } else if s[i] == s[j] { if j - i \u0026lt;= 2 { dp[i][j] = true } else { dp[i][j] = dp[i+1][j-1] } if dp[i][j] { if j - i + 1 \u0026gt; maxLen { maxLen = j - i + 1 begin = i } } } else if s[i] != s[j] { dp[i][j] = false } } } return s[begin:begin+maxLen] }   明确dp的定义，dp[i][j]的定义为s[i..j]是否为回文串\n  明确i，j的变化方向，确定外层循环是i还是j\n 举个例子，当求dp[1][5]是否是回文串，需要先知道dp[2][4]是否是回文串，所以i是从大到小遍历，而j是从小到大遍历，所以外层循环是j，内层循环是i，\n   每计算出一个回文子串，更新begin和length的值\n  53. 最大子数组和 53. 最大子数组和 给你一个整数数组 nums ，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。\n子数组 是数组中的一个连续部分。\nfunc maxSubArray(nums []int) int { l := len(nums) max := nums[0] for i := 1; i \u0026lt; l; i++ { if nums[i] + nums[i-1] \u0026gt; nums[i] { // 这里利用nums[i]直接记录每次的累加和  nums[i] = nums[i] + nums[i-1] } if nums[i] \u0026gt; max { max = nums[i] } } return max } 300. 最长递增子序列 300. 最长递增子序列\n给你一个整数数组 nums ，找到其中最长严格递增子序列的长度。\n子序列 是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。例如，[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。\nfunc lengthOfLIS(nums []int) int { l := len(nums) // dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。  dp := make([]int, l) for i := 0; i \u0026lt; l; i++ { // 每个位置的长度初始值起码有自身一个，因此长度为1  dp[i] = 1 // 每到一个i位，需要j从头开始走，计算出在i位时的最长子序列  for j := 0; j \u0026lt; i; j++ { // 当i位置比j位置的值大，长度+1  if nums[i] \u0026gt; nums[j] { dp[i] = max(dp[i], dp[j] + 1) } } } res := 0 // 最长的子序列并不一定出现在最后一位，所以要全部位置遍历一边  for _, v := range dp { res = max(res, v) } return res } 1143. 最长公共子序列 1143. 最长公共子序列\n给定两个字符串 text1 和 text2，返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ，返回 0 。\n一个字符串的 子序列 是指这样一个新的字符串：它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。\n例如，\u0026ldquo;ace\u0026rdquo; 是 \u0026ldquo;abcde\u0026rdquo; 的子序列，但 \u0026ldquo;aec\u0026rdquo; 不是 \u0026ldquo;abcde\u0026rdquo; 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。\nfunc longestCommonSubsequence(text1 string, text2 string) int { // dp[i][j] 表示s1[0..i]和s2[0..j]的最长公共子序列的长度  m, n := len(text1), len(text2) dp := make([][]int, m+1) // 0表示空字符，所以这里要+1  for i := range dp { dp[i] = make([]int, n+1) } for i := 0; i \u0026lt;= m; i++ { // \u0026lt;=  // 空字符串与任何字符串的最长公共子序列的长度都为0  dp[i][0] = 0 } for j := 0; j \u0026lt;= n; j++ { dp[0][j] = 0 } for i := 1; i \u0026lt;= m; i++ { for j := 1; j \u0026lt;= n; j++ { if text1[i-1] == text2[j-1] { // 两边新增一个相同的字符，长度在原来的基础上+1  dp[i][j] = dp[i-1][j-1] + 1 } else { // 两边新增了一个不同的字符，首先长度上不可能增加  // 新增加的字符可以拿到两遍去匹配原来的字符，取两者较大的那个值  // 考虑 s1 = abc, s2 = abe，此时分别来了一个字符e和h，得到abce和abeh  dp[i][j] = max(dp[i-1][j], dp[i][j-1]) } } } return dp[m][n] } 718. 最长重复子数组 718. 最长重复子数组\n给两个整数数组 nums1 和 nums2 ，返回 两个数组中 公共的 、长度最长的子数组的长度 。\nfunc findLength(nums1 []int, nums2 []int) int { // dp[i][j]表示nums1[0..i]和nums2[0..j]的最长公共子数组的长度  m, n := len(nums1), len(nums2) dp := make([][]int, m + 1) for i := range dp { dp[i] = make([]int, n + 1) } for i := 0; i \u0026lt;= m; i++ { // 空数组与其他任何数组的公共子数组的长度都为0  dp[i][0] = 0 } for j := 0; j \u0026lt;= n; j++ { dp[0][j] = 0 } ans := 0 for i := 1; i \u0026lt;= m; i++ { for j := 1; j \u0026lt;= n; j++ { if nums1[i-1] == nums2[j-1] { dp[i][j] = dp[i-1][j-1] + 1 if dp[i][j] \u0026gt; ans { ans = dp[i][j] } } } } return ans } 115. 不同的子序列 115. 不同的子序列\n给定一个字符串 s 和一个字符串 t ，计算在 s 的子序列中 t 出现的个数。\n字符串的一个 子序列 是指，通过删除一些（也可以不删除）字符且不干扰剩余字符相对位置所组成的新字符串。（例如，\u0026ldquo;ACE\u0026rdquo; 是 \u0026ldquo;ABCDE\u0026rdquo; 的一个子序列，而 \u0026ldquo;AEC\u0026rdquo; 不是）\n题目数据保证答案符合 32 位带符号整数范围。\nfunc numDistinct(s string, t string) int { n, m := len(s), len(t) // dp[i][j]表示t[0:j-1]在s[0:i-1]中出现的个数  dp := make([][]int, n + 1) for i := range dp { dp[i] = make([]int, m + 1) } for i := 0; i \u0026lt; n; i++ { dp[i][0] = 1 } for i := 1; i \u0026lt;= n; i++ { for j := 1; j \u0026lt;= m; j++ { if s[i-1] == t[j-1] { dp[i][j] = dp[i-1][j-1] + dp[i-1][j] } else { dp[i][j] = dp[i-1][j] } } } return dp[n][m] } 409. 最长回文串 409. 最长回文串\n给定一个包含大写字母和小写字母的字符串 s ，返回 通过这些字母构造成的 最长的回文串 。\n在构造过程中，请注意 区分大小写 。比如 \u0026ldquo;Aa\u0026rdquo; 不能当做一个回文字符串。\nfunc longestPalindrome(s string) int { cnt := map[rune]int{} for _, c := range s { cnt[c]++ } ans := 0 meetOdd := false for _, v := range cnt { // 偶数，所有字符都可以用  if v % 2 == 0 { ans += v } else { // 奇数  ans += (v - 1) meetOdd = true } } if meetOdd { ans++ } return ans } 647. 回文子串 647. 回文子串 给你一个字符串 s ，请你统计并返回这个字符串中 回文子串 的数目。\n回文字符串 是正着读和倒过来读一样的字符串。\n子字符串 是字符串中的由连续字符组成的一个序列。\n具有不同开始位置或结束位置的子串，即使是由相同的字符组成，也会被视作不同的子串。\nfunc countSubstrings(s string) int { size := len(s) dp := make([][]bool, size) for i := range dp { dp[i] = make([]bool, size) } cnt := 0 for j := 0; j \u0026lt; size; j++ { for i := 0; i \u0026lt;= j; i++ { if i == j { // basecase  dp[i][j] = true cnt++ continue } else if j - i == 1 \u0026amp;\u0026amp; s[i] == s[j] { // basecase， 两个字符  dp[i][j] = true cnt++ continue } else if s[i] == s[j] \u0026amp;\u0026amp; dp[i+1][j-1] { dp[i][j] = true cnt++ } } } return cnt } 128. 最长连续序列 128. 最长连续序列 给定一个未排序的整数数组 nums ，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。\n请你设计并实现时间复杂度为 O(n) 的算法解决此问题。\nfunc longestConsecutive(nums []int) int { mem := make(map[int]bool) for _, n := range nums { mem[n] = true } maxCnt := 0 for n := range mem { // 计算从k字符出发能拿到的最长串  cnt := 1 // 妙啊，这里需要防止一个数被重复计算  // 如果上一个数也在mem里，那应该从上一个数开始，去计算整个最长串  if !mem[n-1] { for mem[n+1] { cnt++ n++ } } if cnt \u0026gt; maxCnt { maxCnt = cnt } } return maxCnt } 659. 分割数组为连续子序列 659. 分割数组为连续子序列\n给你一个按升序排序的整数数组 num（可能包含重复数字），请你将它们分割成一个或多个长度至少为 3 的子序列，其中每个子序列都由连续整数组成。\n如果可以完成上述分割，则返回 true ；否则，返回 false 。\nfunc isPossible(nums []int) bool { left := map[int]int{} for _, num := range nums { left[num]++ } // 以num结尾的子序列的长度  endNum := map[int]int{} for _, num := range nums { if left[num] == 0 { continue } // 如果num-1存在，则换成以num结尾  if endNum[num-1] \u0026gt; 0 { left[num]-- endNum[num-1]-- endNum[num]++ } else if left[num+1] \u0026gt; 0 \u0026amp;\u0026amp; left[num+2] \u0026gt; 0 { left[num]-- left[num+1]-- left[num+2]-- endNum[num+2]++ } else { return false } } return true } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%AD%90%E4%B8%B2%E5%AD%90%E5%BA%8F%E5%88%97%E9%97%AE%E9%A2%98/doc/","summary":"子串和子序列是一块难啃的骨头，但大多数时候可以通过动态规划来解决","title":"算法-子串子序列问题"},{"content":"19. 删除链表的倒数第 N 个结点\n给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。\nfunc removeNthFromEnd(head *ListNode, n int) *ListNode { // 快慢指针  dummy := \u0026amp;ListNode{} dummy.Next = head s, f := dummy, dummy // faster向前走n+1步，一会可以让slow停在想要删除的节点的前继节点上  for n \u0026gt;= 0 \u0026amp;\u0026amp; f != nil { f = f.Next n-- } for f != nil { f = f.Next s = s.Next } s.Next = s.Next.Next return dummy.Next } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%BF%AB%E6%85%A2%E6%8C%87%E9%92%88/doc/","summary":"19. 删除链表的倒数第 N 个结点\n给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。\nfunc removeNthFromEnd(head *ListNode, n int) *ListNode { // 快慢指针  dummy := \u0026amp;ListNode{} dummy.Next = head s, f := dummy, dummy // faster向前走n+1步，一会可以让slow停在想要删除的节点的前继节点上  for n \u0026gt;= 0 \u0026amp;\u0026amp; f != nil { f = f.Next n-- } for f != nil { f = f.Next s = s.Next } s.Next = s.Next.Next return dummy.Next } ","title":"算法-快慢指针"},{"content":"207. 课程表 207. 课程表 你这个学期必须选修 numCourses 门课程，记为 0 到 numCourses - 1 。\n在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出，其中 prerequisites[i] = [ai, bi] ，表示如果要学习课程 ai 则 必须 先学习课程 bi 。\n例如，先修课程对 [0, 1] 表示：想要学习课程 0 ，你需要先完成课程 1 。 请你判断是否可能完成所有课程的学习？如果可以，返回 true ；否则，返回 false 。\nfunc canFinish(numCourses int, prerequisites [][]int) bool { // 入度  indeg := make([]int, numCourses) // 邻接表  g := make(map[int][]int) for _, q := range prerequisites { indeg[q[0]]++ g[q[1]] = append(g[q[1]], q[0]) } q := make([]int, 0, numCourses) // 无入度节点入队  for i := range indeg { if indeg[i] == 0 { q = append(q, i) } } resCount := 0 for len(q) \u0026gt; 0 { head := q[0] q = q[1:] resCount++ for _, i := range g[head] { indeg[i]-- if indeg[i] == 0 { q = append(q, i) } } } return resCount == numCourses } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E6%8B%93%E8%A1%A5%E6%8E%92%E5%BA%8F/doc/","summary":"207. 课程表 207. 课程表 你这个学期必须选修 numCourses 门课程，记为 0 到 numCourses - 1 。\n在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出，其中 prerequisites[i] = [ai, bi] ，表示如果要学习课程 ai 则 必须 先学习课程 bi 。\n例如，先修课程对 [0, 1] 表示：想要学习课程 0 ，你需要先完成课程 1 。 请你判断是否可能完成所有课程的学习？如果可以，返回 true ；否则，返回 false 。\nfunc canFinish(numCourses int, prerequisites [][]int) bool { // 入度  indeg := make([]int, numCourses) // 邻接表  g := make(map[int][]int) for _, q := range prerequisites { indeg[q[0]]++ g[q[1]] = append(g[q[1]], q[0]) } q := make([]int, 0, numCourses) // 无入度节点入队  for i := range indeg { if indeg[i] == 0 { q = append(q, i) } } resCount := 0 for len(q) \u0026gt; 0 { head := q[0] q = q[1:] resCount++ for _, i := range g[head] { indeg[i]-- if indeg[i] == 0 { q = append(q, i) } } } return resCount == numCourses } ","title":"算法-拓扑排序"},{"content":"20. 有效的括号\n给定一个只包括 \u0026lsquo;('，')'，'{'，'}'，'['，']\u0026rsquo; 的字符串 s ，判断字符串是否有效。\n有效字符串需满足： 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。\nfunction isValid(s: string): boolean { let stack = [] for (let c of s) { if (c == \u0026#39;(\u0026#39; || c == \u0026#39;{\u0026#39; || c == \u0026#39;[\u0026#39;) { stack.push(c) } if (c == \u0026#39;)\u0026#39; || c == \u0026#39;}\u0026#39; || c == \u0026#39;]\u0026#39;) { let e = stack.pop() if (c != rightof(e)) { return false } } } if (stack.length \u0026gt; 0) { return false } return true }; function rightof(c: string): string { if (c == \u0026#34;{\u0026#34;) { return \u0026#34;}\u0026#34; } if (c == \u0026#34;[\u0026#34;) { return \u0026#34;]\u0026#34; } if (c == \u0026#34;(\u0026#34;) { return \u0026#34;)\u0026#34; } return \u0026#34;\u0026#34; } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E6%8B%AC%E5%8F%B7%E9%97%AE%E9%A2%98/doc/","summary":"20. 有效的括号\n给定一个只包括 \u0026lsquo;('，')'，'{'，'}'，'['，']\u0026rsquo; 的字符串 s ，判断字符串是否有效。\n有效字符串需满足： 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。\nfunction isValid(s: string): boolean { let stack = [] for (let c of s) { if (c == \u0026#39;(\u0026#39; || c == \u0026#39;{\u0026#39; || c == \u0026#39;[\u0026#39;) { stack.push(c) } if (c == \u0026#39;)\u0026#39; || c == \u0026#39;}\u0026#39; || c == \u0026#39;]\u0026#39;) { let e = stack.pop() if (c != rightof(e)) { return false } } } if (stack.length \u0026gt; 0) { return false } return true }; function rightof(c: string): string { if (c == \u0026#34;{\u0026#34;) { return \u0026#34;}\u0026#34; } if (c == \u0026#34;[\u0026#34;) { return \u0026#34;]\u0026#34; } if (c == \u0026#34;(\u0026#34;) { return \u0026#34;)\u0026#34; } return \u0026#34;\u0026#34; } ","title":"算法-括号问题"},{"content":"11. 盛最多水的容器 11. 盛最多水的容器 给定一个长度为 n 的整数数组 height 。有 n 条垂线，第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。\n找出其中的两条线，使得它们与 x 轴共同构成的容器可以容纳最多的水。\n返回容器可以储存的最大水量。\n说明：你不能倾斜容器。\nfunc maxArea(height []int) int { l, r := 0, len(height) - 1 ans := 0 for l \u0026lt; r { area := min(height[l], height[r]) * (r - l) ans = max(ans, area) if height[l] \u0026lt; height[r] { l++ } else { r-- } } return ans } 42. 接雨水 42. 接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。\n// 动态规划，提前计算好左右两边的最大高度 func trap(height []int) int { n := len(height) leftMax, rightMax := make([]int, n), make([]int, n) leftMax[0] = height[0] for i := 1; i \u0026lt; n; i++ { leftMax[i] = max(leftMax[i-1], height[i]) } rightMax[n-1] = height[n-1] for i := n - 2; i \u0026gt;= 0; i-- { rightMax[i] = max(rightMax[i+1], height[i]) } ans := 0 for i, h := range height { ans += min(leftMax[i], rightMax[i]) - h } return ans } func trap2(height []int) int { // 每次计算当前的柱子怎么接多少水  // 每个柱子的接水量 = min(往左最大高度，往右最大的高度) - 当前珠子高度  // res += min(l_max, r_max) - height[i]  l := len(height) left := 0 right := l - 1 leftMax := height[0] rightMax := height[l-1] res := 0 for left \u0026lt;= right { leftMax = max(leftMax, height[left]) rightMax = max(rightMax, height[right]) if leftMax \u0026lt; rightMax { res += leftMax - height[left] left++ } else { res += rightMax - height[right] right-- } } return res } 49. 字母异位词分组 49. 字母异位词分组 给你一个字符串数组，请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。\n字母异位词 是由重新排列源单词的字母得到的一个新单词，所有源单词中的字母通常恰好只用一次。\nfunc groupAnagrams(strs []string) [][]string { statistic := make(map[[26]int][]string) for _, str := range strs { // 计算出每个字符串每个字符出现的次数  cnt := [26]int{} for _, c := range str { asiic := c - \u0026#39;a\u0026#39; cnt[asiic]++ } statistic[cnt] = append(statistic[cnt], str) } res := [][]string{} for _, item := range statistic { res = append(res, item) } return res } 53. 最大子数组和 53. 最大子数组和 给你一个整数数组 nums ，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。\n子数组 是数组中的一个连续部分。\nfunc maxSubArray(nums []int) int { l := len(nums) max := nums[0] for i := 1; i \u0026lt; l; i++ { if nums[i] + nums[i-1] \u0026gt; nums[i] { // 这里利用nums[i]直接记录每次的累加和  nums[i] = nums[i] + nums[i-1] } if nums[i] \u0026gt; max { max = nums[i] } } return max } 56. 合并区间 56. 合并区间 以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间，并返回 一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间 。\nfunc merge(intervals [][]int) [][]int { sort.Slice(intervals, func(i, j int) bool { return intervals[i][0] \u0026lt; intervals[j][0] }) ans := [][]int{intervals[0]} for i := 1; i \u0026lt; len(intervals); i++ { last := ans[len(ans)-1] if last[1] \u0026lt; intervals[i][0] { // 没有交集  ans = append(ans, intervals[i]) } else { last[1] = max(last[1], intervals[i][1]) // 有交集，更新右边界  } } return ans } 57. 插入区间 57. 插入区间 给你一个 无重叠的 ，按照区间起始端点排序的区间列表。\n在列表中插入一个新的区间，你需要确保列表中的区间仍然有序且不重叠（如果有必要的话，可以合并区间）。\nfunc insert(intervals [][]int, newInterval []int) [][]int { ans := [][]int{} left, right := 0, 1 merged := false for _, interval := range intervals { // 没有交集  if interval[right] \u0026lt; newInterval[left] { ans = append(ans, interval) } else if interval[left] \u0026gt; newInterval[right] { // 后面不会在遇到有重叠的区间了，所以合并完成  if !merged { ans = append(ans, newInterval) merged = true } ans = append(ans, interval) } else { // 需要合并  newInterval[left] = min(newInterval[left], interval[left]) newInterval[right] = max(newInterval[right], interval[right]) } } if !merged { ans = append(ans, newInterval) } return ans } func max(x, y int) int { if x \u0026gt; y { return x } return y } func min(x, y int) int { if x \u0026lt; y { return x } return y } 75. 颜色分类 75. 颜色分类 给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ，原地对它们进行排序，使得相同颜色的元素相邻，并按照红色、白色、蓝色顺序排列。\n我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。\n必须在不使用库的sort函数的情况下解决这个问题。\nfunc sortColors(nums []int) { p0, p1 := 0, 0 for i, n := range nums { if n == 1 { nums[i], nums[p1] = nums[p1], nums[i] p1++ } else if n == 0 { nums[i], nums[p0] = nums[p0], nums[i] if p0 \u0026lt; p1 { nums[i], nums[p1] = nums[p1], nums[i] } p0++ p1++ } } } 581. 最短无序连续子数组 581. 最短无序连续子数组 给你一个整数数组 nums ，你需要找出一个 连续子数组 ，如果对这个子数组进行升序排序，那么整个数组都会变为升序排序。\n请你找出符合题意的 最短 子数组，并输出它的长度。\nfunc findUnsortedSubarray(nums []int) int { l, r, size := -1, -1, len(nums) leftMax, rightMin := math.MinInt32, math.MaxInt32 for i, leftNum := range nums { // 从左往右  if leftMax \u0026gt; leftNum { // 如果存在左边的数比当前的数大，则说明现在还不在升序区，更新r的位置  r = i } else { leftMax = leftNum } rightNum := nums[size-i-1] // 从右往左  if rightMin \u0026lt; rightNum { // 如果右边存在比当前更小的数，则说明现在还不在升序区，更新l的位置  l = size - i - 1 } else { rightMin = rightNum } } if l == -1 || r == -1 { return 0 } return r - l + 1 } 448. 找到所有数组中消失的数字 448. 找到所有数组中消失的数字 给你一个含 n 个整数的数组 nums ，其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字，并以数组的形式返回结果。\n负数标记法\nfunc findDisappearedNumbers(nums []int) []int { for _, num := range nums { // 将数字标记为负数  idx := abs(num)-1 nums[idx] = -abs(nums[idx]) } ans := []int{} for i, num := range nums { if num \u0026gt; 0 { ans = append(ans, i + 1) } } return ans } func abs(x int) int { if x \u0026gt; 0 { return x } return -x } 442. 数组中重复的数据 442. 数组中重复的数据 给你一个长度为 n 的整数数组 nums ，其中 nums 的所有整数都在范围 [1, n] 内，且每个整数出现 一次 或 两次 。请你找出所有出现 两次 的整数，并以数组形式返回。\n你必须设计并实现一个时间复杂度为 O(n) 且仅使用常量额外空间的算法解决此问题。\nfunc findDuplicates(nums []int) []int { ans := []int{} for _, num := range nums { idx := abs(num)-1 if nums[idx] \u0026lt; 0 { ans = append(ans, abs(num)) } nums[idx] = -nums[idx] } return ans } 416. 分割等和子集 416. 分割等和子集 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。\nfunc canPartition(nums []int) bool { // dp[i][j] 表示为在nums[0..i-1]中能找到数字之和为j  n := len(nums) if n == 0 || n == 1 { return false } sum, maxNum := 0, 0 for _, num := range nums { if num \u0026gt; maxNum { maxNum = num } sum += num } // 不能等分  if sum % 2 != 0 { return false } target := sum / 2 // 如果有单个值已经超过了一半，直接false  if maxNum \u0026gt; target { return false } dp := make([][]bool, n) for i := range dp { dp[i] = make([]bool, target + 1) } for i := range dp { dp[i][0] = true } dp[0][nums[0]] = true for i := 1; i \u0026lt; n; i++ { v := nums[i] for j := 0; j \u0026lt;= target; j++ { if j \u0026gt;= v { dp[i][j] = dp[i-1][j] || dp[i-1][j-v] } else { dp[i][j] = dp[i-1][j] } } } return dp[n-1][target] } 406. 根据身高重建队列 406. 根据身高重建队列 假设有打乱顺序的一群人站成一个队列，数组 people 表示队列中一些人的属性（不一定按顺序）。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ，前面 正好 有 ki 个身高大于或等于 hi 的人。\n请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ，其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性（queue[0] 是排在队列前面的人）。\nfunc reconstructQueue(people [][]int) [][]int { // 先按身高降序排列  sort.Slice(people, func(i, j int) bool { if people[i][0] == people[j][0] { return people[i][1] \u0026lt; people[j][1] // 身高相同情况下按k升序排列  } return people[i][0] \u0026gt; people[j][0] // 否则按身高降序排列  }) res := make([][]int, len(people)) for i := 0; i \u0026lt; len(people); i++ { // 将people插到对应的位置  idx := people[i][1] // 如果原先位置上有元素需要挪开位置  copy(res[idx+1:], res[idx:]) // 将res[idx:]搬到res[idx+1:]，从而空出来res[i]  res[idx] = people[i] } return res } 283. 移动零 283. 移动零 给定一个数组 nums，编写一个函数将所有 0 移动到数组的末尾，同时保持非零元素的相对顺序。\n请注意 ，必须在不复制数组的情况下原地对数组进行操作。\npublic void moveZeroes(int[] nums) { if (nums == null || nums.length == 0) { return; } // 记录可被替换元素的下标  int index = 0; for (int i = 0; i \u0026lt; nums.length; i++) { if (nums[i] != 0) { nums[index] = nums[i]; index++; } } while (index \u0026lt; nums.length) { nums[index++] = 0; } } 74. 搜索二维矩阵 74. 搜索二维矩阵 编写一个高效的算法来判断 m x n 矩阵中，是否存在一个目标值。该矩阵具有如下特性：\n每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。\nfunc searchMatrix(matrix [][]int, target int) bool { m := len(matrix) n := len(matrix[0]) i, j := 0, n - 1 for i \u0026lt; m \u0026amp;\u0026amp; j \u0026gt;= 0 { val := matrix[i][j] if val == target { return true } if val \u0026gt; target { j-- } else if val \u0026lt; target { i++ } } return false } 240. 搜索二维矩阵 II 240. 搜索二维矩阵 II 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性：\n每行的元素从左到右升序排列。 每列的元素从上到下升序排列。\nfunc searchMatrix(matrix [][]int, target int) bool { m := len(matrix) n := len(matrix[0]) i, j := 0, n - 1 for i \u0026lt; m \u0026amp;\u0026amp; j \u0026gt;= 0 { val := matrix[i][j] if val == target { return true } if val \u0026gt; target { j-- } else if val \u0026lt; target { i++ } } return false } 238. 除自身以外数组的乘积 238. 除自身以外数组的乘积 给你一个整数数组 nums，返回 数组 answer ，其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。\n题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。\n请不要使用除法，且在 O(n) 时间复杂度内完成此题。\nfunc productExceptSelf(nums []int) []int { n := len(nums) // 本题解法为前缀积和后缀积  ans := make([]int, n) // 先从左到右，计算前缀积  ans[0] = 1 for i := 1; i \u0026lt; n; i++ { ans[i] = ans[i-1] * nums[i-1] } // 由于控制了O(1)的时间复杂度，使用一个变量记录从右往左的前缀积  r := 1 for i := n - 1; i \u0026gt;= 0; i-- { ans[i] = ans[i] * r // rs[i] = rs[i+1] * nums[i+1]  r *= nums[i] } return ans } 169. 多数元素 169. 多数元素 给定一个大小为 n 的数组，找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。\n你可以假设数组是非空的，并且给定的数组总是存在多数元素。\n摩尔投票法\nfunc majorityElement(nums []int) int { // 当前的candidate  candidate := nums[0] // 当前candidate获得的票数  count := 1 for i := 1; i \u0026lt; len(nums); i++ { // 如果当前candidate没有票了，更换candidate  if count == 0 { candidate = nums[i] count = 1 continue } // 如果当前元素和candidate相同，则票数+1  if candidate == nums[i] { count++ // 如果当前元素和candidate不相同，则两两抵消，票数-1  } else { count-- } } return candidate } 229. 求众数 II 229. 求众数 II 给定一个大小为 n 的整数数组，找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。\nfunc majorityElement(nums []int) []int { if len(nums) \u0026lt; 2 { return nums } candidate1, candidate2 := nums[0], nums[1] cnt1, cnt2 := 0, 0 for _, num := range nums { if num == candidate1 { cnt1++ continue } if num == candidate2 { cnt2++ continue } // 一号候选人没有票，则当前num成为候选人  if cnt1 == 0 { candidate1 = num cnt1 = 1 continue } // 二号候选人没有票，则当前num成为候选人  if cnt2 == 0 { candidate2 = num cnt2 = 1 continue } // 两人都有票，和当前num相互抵消  cnt1-- cnt2-- } // 计算两个候选人的个数  cnt1, cnt2 = 0, 0 for _, num := range nums { if num == candidate1 { cnt1++ } if num == candidate2 { cnt2++ } } ans := []int{} if cnt1 \u0026gt; len(nums) / 3 { ans = append(ans, candidate1) } if candidate2 != candidate1 \u0026amp;\u0026amp; cnt2 \u0026gt; len(nums) / 3 { ans = append(ans, candidate2) } return ans } 152. 乘积最大子数组 152. 乘积最大子数组 给你一个整数数组 nums ，请你找出数组中乘积最大的非空连续子数组（该子数组中至少包含一个数字），并返回该子数组所对应的乘积。\n测试用例的答案是一个 32-位 整数。\n子数组 是数组的连续子序列。\nfunc maxProduct(nums []int) int { maxSum := math.MinInt16 imax := 1 imin := 1 for _, v := range nums { // 当当前元素为负值时，最大变最小，最小变最大  if v \u0026lt; 0 { temp := imax imax = imin imin = temp } imax = max(imax * v, v) imin = min(imin * v, v) maxSum = max(maxSum, imax) } return maxSum } 139. 单词拆分 139. 单词拆分 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。\n注意：不要求字典中出现的单词全部都使用，并且字典中的单词可以重复使用。\nfunc wordBreak(s string, wordDict []string) bool { mem := make(map[string]bool) for _, word := range wordDict { mem[word] = true } size := len(s) // dp[i]表示s[0..i-1]能否由wordDict组成  dp := make([]bool, size + 1) dp[0] = true // 空字符串  for i := 1; i \u0026lt;= size; i++ { for j := 0; j \u0026lt;= i; j++ { if dp[j] \u0026amp;\u0026amp; mem[s[j:i]] { dp[i] = true } } } return dp[size] } 164. 最大间距 164. 最大间距\n给定一个无序的数组 nums，返回 数组在排序之后，相邻元素之间最大的差值 。如果数组元素个数小于 2，则返回 0 。\n您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。\n思路：\n  基数排序\n  假设有数组[3,6,9,1,11,23,4,52,33]\n  先找到最大值52, 52是个两位数，因此只要进行两轮排序\n  第一轮，对于个位上的数排序\n| 个位上的数 | 元素集合 | | 0 | | 1 | 1, 11 | 2 | 52 | 3 | 3, 23, 33 | 4 | 4 | 5 | | 6 | 6 | 7 | | 8 | | 9 | 9 因此，第一轮之后元素的顺序为：[1, 11, 52, 3, 23, 33, 4, 6, 9]\n  第二轮，对于十位上的数排序\n| 十位上的数 | 元素集合 | | 0 | 1, 3, 4, 6, 9 | 1 | 11 | 2 | 23 | 3 | 33 | 4 | | 5 | 52 | 6 | | 7 | | 8 | | 9 | 因此，第二轮之后元素的顺序为：[1, 3, 4, 6, 9, 11, 23, 33, 52]   最终代码：\nfunc bsort(nums []int) { n := len(nums) tmp := make([]int, n) maxVal := max(nums...) for round := 1; round \u0026lt;= maxVal; round *= 10 { cnt := [10]int{} //  for _, num := range nums { digit := num / round % 10 cnt[digit]++ } for i := 1; i \u0026lt; 10; i++ { cnt[i] += cnt[i-1] } // 从后往前放置所有的数，（从后往前是因为同一个位上可能有多个数）  for i := n - 1; i \u0026gt;= 0; i-- { digit := nums[i] / round % 10 tmp[cnt[digit]-1] = nums[i] cnt[digit]-- } copy(nums, tmp) } } func maximumGap(nums []int) (ans int) { n := len(nums) if n \u0026lt; 2 { return } bsort(nums) for i := 1; i \u0026lt; n; i++ { ans = max(ans, nums[i]-nums[i-1]) } return } func max(a ...int) int { res := a[0] for _, v := range a[1:] { if v \u0026gt; res { res = v } } return res } 907. 子数组的最小值之和 907. 子数组的最小值之和 给定一个整数数组 arr，找到 min(b) 的总和，其中 b 的范围为 arr 的每个（连续）子数组。\n由于答案可能很大，因此 返回答案模 10^9 + 7 。\n思路：对于每一个nums[i], 分别向左右找到它的影响范围\nfunc sumSubarrayMins(arr []int) int { n := len(arr) mod := 1000000007 ans := 0 for i := 0; i \u0026lt; n; i++ { l := i - 1 // 找到以arr[i]最为最小值的左边界  for l \u0026gt;= 0 \u0026amp;\u0026amp; arr[i] \u0026lt; arr[l] { l-- } r := i + 1 for r \u0026lt; n \u0026amp;\u0026amp; arr[i] \u0026lt;= arr[r] { r++ } // 左边的个数*右边的个数能得到子数组的数量，*a[i]表示加上多少个a[i]  ans += (i - l) * (r - i) * arr[i] } return ans % mod } 26. 删除有序数组中的重复项 26. 删除有序数组中的重复项\n给你一个 升序排列 的数组 nums ，请你 原地 删除重复出现的元素，使每个元素 只出现一次 ，返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。\n由于在某些语言中不能改变数组的长度，所以必须将结果放在数组nums的第一部分。更规范地说，如果在删除重复项之后有 k 个元素，那么 nums 的前 k 个元素应该保存最终结果。\n将最终结果插入 nums 的前 k 个位置后返回 k 。 不要使用额外的空间，你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。\n通用解法：\nfunc removeDuplicates(nums []int) int { // 移除重复项的通用函数  replace := func(k int) int { // 下一个要被覆盖的位置  replaceIdx := 0 for _, num := range nums { // replaceIdx \u0026lt; k 意味着直接跳过前k个  if replaceIdx \u0026lt; k || num != nums[replaceIdx-k] { nums[replaceIdx] = num replaceIdx++ } } return replaceIdx } return replace(1) } 80. 删除有序数组中的重复项 II 80. 删除有序数组中的重复项 II\n给你一个有序数组 nums ，请你 原地 删除重复出现的元素，使每个元素 最多出现两次 ，返回删除后数组的新长度。\n不要使用额外的数组空间，你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。\n通用解法：\nfunc removeDuplicates(nums []int) int { // 移除重复项的通用函数  replace := func(k int) int { // 下一个要被覆盖的位置  replaceIdx := 0 for _, num := range nums { // replaceIdx \u0026lt; k 意味着直接跳过前k个  if replaceIdx \u0026lt; k || num != nums[replaceIdx-k] { nums[replaceIdx] = num replaceIdx++ } } return replaceIdx } return replace(2) } 48. 旋转图像 48. 旋转图像\n给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。\n你必须在 原地 旋转图像，这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。\nfunc rotate(matrix [][]int) { n := len(matrix) // 每次元素交换都会涉及到n^2/4个元素，所以循环时i，j不需要完整遍历  for i := 0; i \u0026lt; n/2; i++ { for j := 0; j \u0026lt; (n + 1) / 2; j++ { temp := matrix[i][j] matrix[i][j] = matrix[n-j-1][i] matrix[n-j-1][i] = matrix[n-i-1][n-j-1] matrix[n-i-1][n-j-1] = matrix[j][n-i-1] matrix[j][n-i-1] = temp } } } 561. 数组拆分 561. 数组拆分 I\n给定长度为 2n 的整数数组 nums ，你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), \u0026hellip;, (an, bn) ，使得从 1 到 n 的 min(ai, bi) 总和最大。\n返回该 最大总和 。\nfunc arrayPairSum(nums []int) int { sort.Ints(nums) ans := 0 for i := 0; i \u0026lt; len(nums); i += 2 { ans += nums[i] } return ans } 剑指 Offer 03. 数组中重复的数字 剑指 Offer 03. 数组中重复的数字\n找出数组中重复的数字。\n在一个长度为 n 的数组 nums 里的所有数字都在 0～n-1 的范围内。数组中某些数字是重复的，但不知道有几个数字重复了，也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。\n思路:\n值和下标相关联，将nums[i]和nums[nums[i]]交换，即让值和索引相同\nfunc findRepeatNumber(nums []int) int { i := 0 for i \u0026lt; len(nums) { if nums[i] == i { i++ continue } if nums[i] == nums[nums[i]] { return nums[i] } nums[i], nums[nums[i]] = nums[nums[i]], nums[i] } return 0 } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E6%95%B0%E7%BB%84/doc/","summary":"与数组相关的算法题可以又各种骚操作","title":"算法-数组"},{"content":"3. 无重复字符的最长子串 3. 无重复字符的最长子串\n给定一个字符串 s ，请你找出其中不含有重复字符的 最长子串 的长度\nfunc lengthOfLongestSubstring(s string) int { l, r := 0, 0 window := make(map[byte]bool) ans := 0 for ; r \u0026lt; len(s); r++ { c := s[r] for window[c] { window[s[l]] = false l++ } window[c] = true ans = max(ans, r - l + 1) } return ans }  使用一个hash表window维护当前的子串 如果要加入的字符c已经在window内了，则移动L指针，直到window里不包含字符c 如果要加入的字符c不在window里，则直接加入window 每放入一个字符，则计算一遍当前的最大字串的长度  76. 最小覆盖子串 76. 最小覆盖子串 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串，则返回空字符串 \u0026quot;\u0026quot; 。\n注意：\n对于 t 中重复字符，我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 如果 s 中存在这样的子串，我们保证它是唯一的答案。\nfunc minWindow(s string, t string) string { if len(s) \u0026lt; len(t) { return \u0026#34;\u0026#34; } needs := make([]int, 128) for _, c := range t { needs[c]++ } window := make([]int, 128) l, r := 0, 0 n, match := len(t), 0 minBegin, minLen := 0, 100001 for r \u0026lt; len(s) { c := s[r] window[c]++ if window[c] \u0026lt;= needs[c] { match++ } r++ for match == n { if r - l \u0026lt; minLen { minLen = r - l minBegin = l } // 当前窗口内的值已经满足需求了，尝试能否从左边缩小窗口  deleteChar := s[l] window[deleteChar]-- if window[deleteChar] \u0026lt; needs[deleteChar] { match-- } // 继续缩小  l++ } } if minLen == 100001 { return \u0026#34;\u0026#34; } return s[minBegin:minBegin+minLen] } 438. 找到字符串中所有字母异位词 438. 找到字符串中所有字母异位词 给定两个字符串 s 和 p，找到 s 中所有 p 的 异位词 的子串，返回这些子串的起始索引。不考虑答案输出的顺序。\n异位词 指由相同字母重排列形成的字符串（包括相同的字符串）。\nfunc findAnagrams(s string, p string) []int { res := []int{} if len(s) \u0026lt; len(p) { return res } // 需要匹配的字符  matchChars := make([]int, 26) for _, c := range p { matchChars[c - \u0026#39;a\u0026#39;]++ } // 定义双指针  left, right := 0, 0 // 记录[left, right]范围内的字符统计  currChars := make([]int, 26) for right \u0026lt; len(s) { rightChar := s[right] - \u0026#39;a\u0026#39; // 当前范围内rightChar这个字符+1  currChars[rightChar]++ // 如果加入字符后，当前范围内的字符已经超过了需要匹配的字符数  for currChars[rightChar] \u0026gt; matchChars[rightChar] { // 移动左边，减少范围内的字符  currChars[s[left] - \u0026#39;a\u0026#39;]-- left++ } if right - left + 1 == len(p) { res = append(res, left) } right++ } return res } 239. 滑动窗口最大值 给你一个整数数组 nums，有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。\n返回 滑动窗口中的最大值 。\nfunc maxSlidingWindow(nums []int, k int) []int { q := \u0026amp;dq{data: []int{}, k: k} ans := []int{} for i := 0; i \u0026lt; len(nums); i++ { q.Push(nums[i]) if i \u0026gt;= k - 1 { ans = append(ans, q.Top()) q.Pop(nums[i-k+1]) } } return ans } type dq struct { data []int k int } func (q *dq) Push(i int) { d := q.data for len(d) != 0 \u0026amp;\u0026amp; d[len(d)-1] \u0026lt; i { d = d[:len(d)-1] } d = append(d, i) q.data = d } func (q *dq) Pop(i int) { if i == q.Top() { q.data = q.data[1:] } } func (q *dq) Top() int { return q.data[0] } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/doc/","summary":"LR两指针","title":"算法-滑动窗口"},{"content":"2. 两数相加 2. 两数相加\n给你两个 非空 的链表，表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。\n请你将两个数相加，并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外，这两个数都不会以 0 开头。\n206. 反转链表 206. 反转链表 给你单链表的头节点 head ，请你反转链表，并返回反转后的链表。\nfunc reverseList(head *ListNode) *ListNode { // 双指针，pre和curr一前一后  var pre *ListNode curr := head for curr != nil { tmp := curr.Next curr.Next = pre pre = curr curr = tmp } return pre } func reverseList(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } nhead := reverseList(head.Next) head.Next.Next = head // 先指向回自己建立联系  head.Next = nil // 再把多余的联系断掉  return nhead } 19. 删除链表的倒数第 N 个结点 19. 删除链表的倒数第 N 个结点\n给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。\nfunc removeNthFromEnd(head *ListNode, n int) *ListNode { // 快慢指针  dummy := \u0026amp;ListNode{} dummy.Next = head s, f := dummy, dummy // faster向前走n+1步，一会可以让slow停在想要删除的节点的前继节点上  for n \u0026gt;= 0 \u0026amp;\u0026amp; f != nil { f = f.Next n-- } for f != nil { f = f.Next s = s.Next } s.Next = s.Next.Next return dummy.Next } 23. 合并K个升序链表 23. 合并K个升序链表\n给你一个链表数组，每个链表都已经按升序排列。\n请你将所有链表合并到一个升序链表中，返回合并后的链表。\nfunc mergeKLists(lists []*ListNode) *ListNode { h := hp{} for _, node := range lists { if node != nil { heap.Push(\u0026amp;h, node) } } dummy := \u0026amp;ListNode{} curr := dummy for len(h) != 0 { node := heap.Pop(\u0026amp;h).(*ListNode) curr.Next = node curr = curr.Next if node.Next != nil { heap.Push(\u0026amp;h, node.Next) } } return dummy.Next } type hp []*ListNode func (h hp) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h hp) Less(i, j int) bool { return h[i].Val \u0026lt; h[j].Val } func (h hp) Len() int { return len(h) } func (h *hp) Push(x interface{}) { *h = append(*h, x.(*ListNode))} func (h *hp) Pop() interface{} { old := *h n := len(old) e := old[n-1] *h = old[:n-1] return e } 21. 合并两个有序链表 21. 合并两个有序链表\n将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。\nfunc mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode { if list1 == nil { return list2 } if list2 == nil { return list1 } if list1.Val \u0026lt; list2.Val { list1.Next = mergeTwoLists(list1.Next, list2) return list1 } list2.Next = mergeTwoLists(list2.Next, list1) return list2 } 24. 两两交换链表中的节点 24. 两两交换链表中的节点\n给你一个链表，两两交换其中相邻的节点，并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题（即，只能进行节点交换）。\nfunc swapPairs(head *ListNode) *ListNode { // 递归法：明确swapPairs的含义就是将给定的链表两两反转  if head == nil || head.Next == nil { return head } // 将当前节点和后继节点反转  next := head.Next // 除了第一个第二个节点外的节点继续去做递归反转，并接到head后面  head.Next = swapPairs(next.Next) // 反转head和next  next.Next = head return next } 25. K 个一组翻转链表 25. K 个一组翻转链表\n给你一个链表，每 k 个节点一组进行翻转，请你返回翻转后的链表。\nk 是一个正整数，它的值小于或等于链表的长度。\n如果节点总数不是 k 的整数倍，那么请将最后剩余的节点保持原有顺序。\n进阶：\n你可以设计一个只使用常数额外空间的算法来解决此问题吗？ 你不能只是单纯的改变节点内部的值，而是需要实际进行节点交换。\nfunc reverseKGroup(head *ListNode, k int) *ListNode { start, end := head, head for i := 0; i \u0026lt; k; i++ { if end == nil { return head // 不足k个  } end = end.Next } newHead := reverse(start, end) start.Next = reverseKGroup(end, k) return newHead } func reverse(start *ListNode, end *ListNode) *ListNode { var pre *ListNode curr := start for curr != end { t := curr.Next curr.Next = pre pre = curr curr = t } return pre } 234. 回文链表 234. 回文链表 给你一个单链表的头节点 head ，请你判断该链表是否为回文链表。如果是，返回 true ；否则，返回 false 。\nfunc isPalindrome(head *ListNode) bool { slow, faster := head, head for faster != nil \u0026amp;\u0026amp; faster.Next != nil { slow = slow.Next faster = faster.Next.Next } left := head right := reverse(slow) for left != nil \u0026amp;\u0026amp; right != nil { if left.Val != right.Val { return false } left = left.Next right = right.Next } return true } func reverse(head *ListNode) *ListNode { var pre *ListNode for head != nil { next := head.Next head.Next = pre pre = head head = next } return pre } 160. 相交链表 160. 相交链表 给你两个单链表的头节点 headA 和 headB ，请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点，返回 null 。\nfunc getIntersectionNode(headA, headB *ListNode) *ListNode { if headA == nil || headB == nil { return nil } pa, pb := headA, headB for pa != pb { if pa == nil { pa = headB } else { pa = pa.Next } if pb == nil { pb = headA } else { pb = pb.Next } } return pa } 141. 环形链表 141. 环形链表 给你一个链表的头节点 head ，判断链表中是否有环。\n如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。注意：pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。\n如果链表中存在环 ，则返回 true 。 否则，返回 false 。\n/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func hasCycle(head *ListNode) bool { s, f := head, head for f != nil { if f.Next == nil { return false } f = f.Next.Next s = s.Next if s == f { return true } } return false } 142. 环形链表 II 142. 环形链表 II 给定一个链表的头节点 head ，返回链表开始入环的第一个节点。 如果链表无环，则返回 null。\n如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。如果 pos 是 -1，则在该链表中没有环。注意：pos 不作为参数进行传递，仅仅是为了标识链表的实际情况。\n不允许修改 链表。\n/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func detectCycle(head *ListNode) *ListNode { // 设链表共有a+b个节点  // fast = 2 * slow, fast = slow + nb = \u0026gt; slow = nb  // 到达环的时候走过的距离为 = a + nb, 所以就是求a，就是在当前s的位置再走a步  f, s := head, head for f != nil { if f.Next == nil { return nil } f = f.Next.Next s = s.Next if f == s { break } } if f == nil { return nil } // f走过的距离为2倍的s，并且相遇时f走过了s + nb的距离  // 让head，slow指针各走a步，去汇合  for head != nil { if head == s { return head } head = head.Next s = s.Next } return nil } 148. 排序链表 148. 排序链表 给你链表的头结点 head ，请将其按 升序 排列并返回 排序后的链表 。\nfunc sortList(head *ListNode) *ListNode { return sort(head, nil) } func sort(head *ListNode, tail *ListNode) *ListNode { if head == nil { return nil } if head.Next == tail { head.Next = nil return head } mid := findMid(head, tail) return merge(sort(head, mid), sort(mid, tail)) } // 将排好序的链表合并 func merge(l1 *ListNode, l2 *ListNode) *ListNode { dummy := \u0026amp;ListNode{} curr := dummy for l1 != nil || l2 != nil { if l1 == nil { curr.Next = l2 l2 = l2.Next } else if l2 == nil { curr.Next = l1 l1 = l1.Next } else if l1.Val \u0026lt; l2.Val { curr.Next = l1 l1 = l1.Next } else { curr.Next = l2 l2 = l2.Next } curr = curr.Next } return dummy.Next } // 找链表的中点 func findMid(head *ListNode, tail *ListNode) *ListNode { s, f := head, head for f != tail \u0026amp;\u0026amp; f.Next != tail { f = f.Next.Next s = s.Next } return s } 剑指 Offer 22. 链表中倒数第k个节点 剑指 Offer 22. 链表中倒数第k个节点 输入一个链表，输出该链表中倒数第k个节点。为了符合大多数人的习惯，本题从1开始计数，即链表的尾节点是倒数第1个节点。\n例如，一个链表有 6 个节点，从头节点开始，它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。\n/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func getKthFromEnd(head *ListNode, k int) *ListNode { if head == nil { return nil } p, q := head, head i := 0 for i \u0026lt; k { if q == nil { break } q = q.Next i++ } if i \u0026lt; k { return nil } for q != nil { p = p.Next q = q.Next } return p } 重排链表 143. 重排链表 给定一个单链表 L 的头节点 head ，单链表 L 表示为：\nL0 → L1 → … → Ln - 1 → Ln 请将其重新排列后变为：\nL0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → … 不能只是单纯的改变节点内部的值，而是需要实际的进行节点交换。\n/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func reorderList(head *ListNode) { if head == nil { return } mid := middleNode(head) nhead := reverseList(mid.Next) mid.Next = nil // 断掉  mergeList(head, nhead) return } func mergeList(l1, l2 *ListNode) { for l1 != nil \u0026amp;\u0026amp; l2 != nil { t1 := l1.Next t2 := l2.Next l1.Next = l2 l1 = t1 l2.Next = t1 l2 = t2 } } func middleNode(head *ListNode) *ListNode { f, s := head, head for f != nil \u0026amp;\u0026amp; f.Next != nil { f = f.Next.Next s = s.Next } return s } func reverseList(head *ListNode) *ListNode { if head == nil { return nil } var pre *ListNode curr := head for curr != nil { t := curr.Next curr.Next = pre pre = curr curr = t } return pre } 138. 复制带随机指针的链表 138. 复制带随机指针的链表 给你一个长度为 n 的链表，每个节点包含一个额外增加的随机指针 random ，该指针可以指向链表中的任何节点或空节点。\n构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成，其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点，并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。\n例如，如果原链表中有 X 和 Y 两个节点，其中 X.random \u0026ndash;\u0026gt; Y 。那么在复制链表中对应的两个节点 x 和 y ，同样有 x.random \u0026ndash;\u0026gt; y 。\n返回复制链表的头节点。\n用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示：\nval：一个表示 Node.val 的整数。 random_index：随机指针指向的节点索引（范围从 0 到 n-1）；如果不指向任何节点，则为 null 。 你的代码 只 接受原链表的头节点 head 作为传入参数。\n/** * Definition for a Node. * type Node struct { * Val int * Next *Node * Random *Node * } */ func copyRandomList(head *Node) *Node { curr := head // 很妙的解法，原先A-\u0026gt;B-\u0026gt;C，先复制一份节点为A-\u0026gt;A\u0026#39;-\u0026gt;B-\u0026gt;B\u0026#39;-\u0026gt;C-\u0026gt;C\u0026#39;  for curr != nil { curr.Next = \u0026amp;Node{Val: curr.Val, Next: curr.Next} curr = curr.Next.Next } curr = head // 接上random节点  for curr != nil { // 如果原先节点存在random节点  if curr.Random != nil { // 新的random节点一定在原先的random节点后面  curr.Next.Random = curr.Random.Next } curr = curr.Next.Next } dummy := \u0026amp;Node{} ncurr := dummy // 将链表拆分  curr = head for curr != nil { ncurr.Next = curr.Next ncurr = ncurr.Next curr.Next = curr.Next.Next curr = curr.Next } return dummy.Next } 328. 奇偶链表 328. 奇偶链表 给定单链表的头节点 head ，将所有索引为奇数的节点和索引为偶数的节点分别组合在一起，然后返回重新排序的列表。\n第一个节点的索引被认为是 奇数 ， 第二个节点的索引为 偶数 ，以此类推。\n请注意，偶数组和奇数组内部的相对顺序应该与输入时保持一致。\n你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。\n/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func oddEvenList(head *ListNode) *ListNode { if head == nil { return nil } odd, even := head, head.Next eventHead := even for even != nil \u0026amp;\u0026amp; even.Next != nil { odd.Next = even.Next odd = odd.Next even.Next = odd.Next even = even.Next } odd.Next = eventHead return head } 83. 删除排序链表中的重复元素 83. 删除排序链表中的重复元素 给定一个已排序的链表的头 head ， 删除所有重复的元素，使每个元素只出现一次 。返回 已排序的链表 。\n/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func deleteDuplicates(head *ListNode) *ListNode { if head == nil { return nil } curr := head for curr.Next != nil { if curr.Val == curr.Next.Val { curr.Next = curr.Next.Next } else { curr = curr.Next } } return head } 82. 删除排序链表中的重复元素 II 82. 删除排序链表中的重复元素 II 给定一个已排序的链表的头 head ， 删除原始链表中所有重复数字的节点，只留下不同的数字 。返回 已排序的链表 。\n/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func deleteDuplicates(head *ListNode) *ListNode { dummy := \u0026amp;ListNode{Val: -101} dummy.Next = head curr := dummy for curr.Next != nil \u0026amp;\u0026amp; curr.Next.Next != nil { if curr.Next.Val == curr.Next.Next.Val { x := curr.Next.Val for curr.Next != nil \u0026amp;\u0026amp; curr.Next.Val == x { curr.Next = curr.Next.Next } } else { curr = curr.Next } } return dummy.Next } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E9%93%BE%E8%A1%A8/doc/","summary":"2. 两数相加 2. 两数相加\n给你两个 非空 的链表，表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。\n请你将两个数相加，并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外，这两个数都不会以 0 开头。\n206. 反转链表 206. 反转链表 给你单链表的头节点 head ，请你反转链表，并返回反转后的链表。\nfunc reverseList(head *ListNode) *ListNode { // 双指针，pre和curr一前一后  var pre *ListNode curr := head for curr != nil { tmp := curr.Next curr.Next = pre pre = curr curr = tmp } return pre } func reverseList(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } nhead := reverseList(head.","title":"算法-链表"},{"content":"空闲链表法 空闲链表法会在内部会维护一个类似链表的数据结构。当用户程序申请内存时，空闲链表分配器会依次遍历空闲的内存块，找到足够大的内存，然后申请新的资源并修改链表。\n因为不同的内存块通过指针构成了链表，所以使用这种方式的分配器可以重新利用回收的资源，但是因为分配内存时需要遍历链表，所以它的时间复杂度是 𝑂(𝑛)。\n空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择，最常见的是以下四种：\n  首次适应（First-Fit）— 从链表头开始遍历，选择第一个大小大于申请内存的内存块；\n  循环首次适应（Next-Fit）— 从上次遍历的结束位置开始遍历，选择第一个大小大于申请内存的内存块；\n  最优适应（Best-Fit）— 从链表头遍历整个链表，选择最合适的内存块；\n  隔离适应（Segregated-Fit）— 将内存分割成多个链表，每个链表中的内存块大小相同，申请内存时先找到满足条件的链表，再从链表中选择合适的内存块；\n  GO语言中的优化 1. 多规格内存管理 上面说过普通的空闲链表法的时间复杂度为𝑂(𝑛)，聪明的GO工程师们在此之上又做了不少优化，Go语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑，运行时根据对象的大小将对象分成微对象、小对象和大对象三种：\n   类别 大小     微对象 (0, 16B)   小对象 [16B, 32KB]   大对象 (32KB, +∞)    为什么这么处理呢？是因为程序实际运行的过程中，绝大多数的对象大小都在32KB以下，而申请的内存大小影响 Go 语言运行时分配内存的过程和开销，所以分别处理大对象和小对象有利于提高内存分配器的性能。\n2. 多级内存管理 将内存分成不同的级别分别管理，TCMalloc 和 Go 运行时分配器都会引入线程缓存（Thread Cache）、中心缓存（Central Cache）和页堆（Page Heap）三个组件分级管理内存：\n  线程缓存使得在一个线程内分配内存无需加锁，减少锁竞争带来的性能损耗\n  当线程缓存不够用时，运行时会使用中心缓存作为补充解决小对象的内存分配，在遇到 32KB 以上的对象时，内存分配器会选择页堆直接分配大内存。\n  GO 1.10的虚拟内存布局 Go 语言程序的 1.10 版本在启动时会初始化整片虚拟内存区域，如下所示的三个区域 spans、bitmap 和 arena 分别预留了 512MB、16GB 以及 512GB 的内存空间，这些内存并不是真正存在的物理内存，而是虚拟内存\n  spans 区域存储了指向内存管理单元 runtime.mspan 的指针，每个内存单元会管理几页的内存空间，每页大小为 8KB；\n spans区域存放的只是指针，实际mspan的内容还是在arena上的\n   bitmap 用于标识 arena 区域中的那些地址保存了对象，位图中的每个字节都会表示堆区中的 32 字节是否空闲；\n 所以arena的大小除以32等于bitmap的大小\n   arena 区域是真正的堆区，运行时会将 8KB 看做一页，这些内存页中存储了所有在堆上初始化的对象；\n 对于任意一个地址，可以根据 arena 的基地址计算该地址所在的页数并通过 spans 数组获得管理该片内存的管理单元 runtime.mspan\n   GO 1.11的虚拟内存布局 整个堆内存划分成了一块块小的heap arena(每个64MB)\n 这么做的原因？是因为C和Go混合使用时，无法维护堆区的内存是连续的。使用稀疏的内存布局不仅能移除堆大小的上限，还能解决 C 和 Go 混合使用时的地址空间冲突问题。不过因为基于稀疏内存的内存管理失去了内存的连续性这一假设，这也使内存管理变得更加复杂\n 瞄一眼heap arena的结构体\ntype heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan pageInUse [pagesPerArena / 8]uint8 pageMarks [pagesPerArena / 8]uint8 pageSpecials [pagesPerArena / 8]uint8 checkmarks *checkmarksMap zeroedBase uintptr }  bitmap和spans同1.10版本的类似 pageInUse zeroedBase记录了该结构体管理的内存的基地址  地址空间 因为所有的内存最终都是要从操作系统中申请的，所以 Go 语言的运行时构建了操作系统的内存管理抽象层，该抽象层将运行时管理的地址空间分成以下四种状态\n   状态 解释     None 内存没有被保留或者映射，是地址空间的默认状态   Reserved 运行时持有该地址空间，但是访问该内存会导致错误   Prepared 内存被保留，一般没有对应的物理内存访问该片内存的行为是未定义的可以快速转换到 Ready 状态   Ready 可以被安全访问    可以看出以上有好几个系统调用\n 对应代码：go/src/runtime/mem_linux.go\n    函数 作用     runtime.sysAlloc 会从操作系统中获取一大块可用的内存空间，可能为几百 KB 或者几 MB；   runtime.sysFree 会在程序发生内存不足（Out-of Memory，OOM）时调用并无条件地返回内存；   runtime.sysReserve 会保留操作系统中的一片内存区域，访问这片内存会触发异常；   runtime.sysMap 保证内存区域可以快速转换至就绪状态；   runtime.sysUsed 通知操作系统应用程序需要使用该内存区域，保证内存区域可以安全访问；   runtime.sysUnused 通知操作系统虚拟内存对应的物理内存已经不再需要，可以重用物理内存；   runtime.sysFault 将内存区域转换成保留状态，主要用于运行时的调试；    内存管理组件 所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局，\n 每一个处理器P都会分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配 mcache会持有内存管理单元runtime.mspan 每个类型的mspan都会管理特定大小的对象 当mspan中不存在空闲对象时，它们会从 runtime.mheap 持有的 134 个中心缓存 runtime.mcentral 中获取新的内存单元 中心缓存属于全局的堆结构体 runtime.mheap，它会从操作系统中申请内存。  mspan type mspan struct { startAddr uintptr // 起始地址  npages uintptr // 页数  freeindex uintptr allocBits *gcBits gcmarkBits *gcBits allocCache uint64 state mSpanStateBox spanclass spanClass ... }  startAddr 和 npages — 确定该结构体管理的多个页所在的内存，每个页的大小都是 8KB； freeindex — 扫描页中空闲对象的初始索引； allocBits 和 gcmarkBits — 分别用于标记内存的占用和回收情况； allocCache — allocBits 的补码，可以用于快速查找内存中未被使用的内存 state被GC用到了，有四种状态：mSpanDead、mSpanInUse、mSpanManual 和 mSpanFree spanclass是一个 uint8 类型的整数，它的前 7 位存储着跨度类的 ID，最后一位表示是否包含指针。从跨度类的ID可以知道mspan管理的对象的规格和个数  spanclass的种类可参见源码: /go/src/runtime/sizeclasses.go\n   当向 runtime.mspan 申请内存时，它会使用 allocCache 字段以对象为单位在管理的内存中快速查找待分配的空间\nmcache type mcache struct { ... alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass  ... tiny uintptr tinyoffset uintptr local_tinyallocs uintptr } runtime.mcache 是 Go 语言中的线程缓存，它会与线程上的处理器P一一绑定，主要用来缓存用户程序申请的微小对象。每一个线程缓存都持有 68 * 2 个 runtime.mspan，这些内存管理单元都存储在结构体的 alloc 字段中：\n 682的原因是sizeclass总共有68种，其中mcache持有含指针和不含指针的mspan各68个，总计就是682个\n mcache刚刚被初始化时是不包含 runtime.mspan 的，只有当用户程序申请内存时才会从上一级组件获取新的 runtime.mspan 满足内存分配的需求。\nfunc allocmcache() *mcache { var c *mcache systemstack(func() { lock(\u0026amp;mheap_.lock) c = (*mcache)(mheap_.cachealloc.alloc()) c.flushGen = mheap_.sweepgen unlock(\u0026amp;mheap_.lock) }) for i := range c.alloc { c.alloc[i] = \u0026amp;emptymspan // 分配了个空的emptymspan \t} c.nextSample = nextSample() return c } runtime.mcache.refill 会为mcache获取一个指定跨度类的mspan\nfunc (c *mcache) refill(spc spanClass) { s := c.alloc[spc] s = mheap_.central[spc].mcentral.cacheSpan() // 看这里，mcache向mheap里的mcentral拿到了mspan \tc.alloc[spc] = s } 线程缓存中还包含几个用于分配微对象的字段，下面的这三个字段组成了微对象分配器，专门管理 16 字节以下的对象：\ntype mcache struct { tiny uintptr tinyoffset uintptr local_tinyallocs uintptr }  tiny 会指向堆中的一片内存 tinyOffset 是下一个空闲内存所在的偏移量 local_tinyallocs 会记录内存分配器中分配的对象个数  mcentral runtime.mcentral 是内存分配器的中心缓存，与线程缓存不同，访问中心缓存中的内存管理单元需要使用互斥锁\ntype mcentral struct { spanclass spanClass partial [2]spanSet full [2]spanSet } 每个中心缓存都会管理某个跨度类的内存管理单元，它会同时持有两个 runtime.spanSet，分别存储包含空闲对象和不包含空闲对象的内存管理单元。\n上面说过，mcache间接调用了mcentral.cacheSpan 方法获取新的内存管理单元，具体内部细节：\n 调用 mcentral.partialSwept 从清理过的、包含空闲空间的 runtime.spanSet 结构中查找可以使用的内存管理单元； 调用 mcentral.partialUnswept 从未被清理过的、有空闲对象的 runtime.spanSet 结构中查找可以使用的内存管理单元； 调用 mcentral.fullUnswept 获取未被清理的、不包含空闲空间的 runtime.spanSet 中获取内存管理单元并通过 runtime.mspan.sweep 清理它的内存空间； 调用 runtime.mcentral.grow 从堆中申请新的内存管理单元； 更新内存管理单元的 allocCache 等字段帮助快速分配内存；  mheap runtime.mheap 是内存分配的核心结构体，Go 语言程序会将其作为全局变量存储，而堆上初始化的所有对象都由该结构体统一管理，该结构体中包含两组非常重要的字段，其中一个是全局的中心缓存列表 central，另一个是管理堆区内存区域的 arenas 以及相关字段。\n页堆中包含一个长度为 136 的 runtime.mcentral 数组，其中 68 个为跨度类需要 scan 的中心缓存，另外的 68 个是 noscan 的中心缓存\n对象分配 堆上所有的对象都会通过调用 runtime.newobject 函数分配内存，该函数会调用 runtime.mallocgc 分配指定大小的内存空间，这也是用户程序向堆上申请内存空间的必经函数\n总的分配逻辑 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { mp := acquirem() mp.mallocing = 1 c := gomcache() var x unsafe.Pointer noscan := typ == nil || typ.ptrdata == 0 if size \u0026lt;= maxSmallSize { if noscan \u0026amp;\u0026amp; size \u0026lt; maxTinySize { // 微对象分配 \t} else { // 小对象分配 \t} } else { // 大对象分配 \t} publicationBarrier() mp.mallocing = 0 releasem(mp) return x } 微小对象分配逻辑 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size \u0026lt;= maxSmallSize { if noscan \u0026amp;\u0026amp; size \u0026lt; maxTinySize { off := c.tinyoffset if off+size \u0026lt;= maxTinySize \u0026amp;\u0026amp; c.tiny != 0 { x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.local_tinyallocs++ releasem(mp) return x } span := c.alloc[tinySpanClass] v := nextFreeFast(span) if v == 0 { v, _, _ = c.nextFree(tinySpanClass) } x = unsafe.Pointer(v) (*[2]uint64)(x)[0] = 0 (*[2]uint64)(x)[1] = 0 if size \u0026lt; c.tinyoffset || c.tiny == 0 { c.tiny = uintptr(x) c.tinyoffset = size } size = maxTinySize ... } ... } ... }  线程缓存 runtime.mcache 中的 tiny 字段指向了 maxTinySize 大小的块，如果当前块中还包含大小合适的空闲内存，运行时会通过基地址和偏移量获取并返回这块内存 当内存块中不包含空闲的内存时，会先从线程缓存找到跨度类对应的内存管理单元 runtime.mspan，调用 runtime.nextFreeFast 获取空闲的内存；当不存在空闲内存时，我们会调用 runtime.mcache.nextFree 从中心缓存或者页堆中获取可分配的内存块  小对象分配逻辑 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size \u0026lt;= maxSmallSize { ... } else { var sizeclass uint8 if size \u0026lt;= smallSizeMax-8 { sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] } else { sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv] } size = uintptr(class_to_size[sizeclass]) spc := makeSpanClass(sizeclass, noscan) span := c.alloc[spc] v := nextFreeFast(span) if v == 0 { v, span, _ = c.nextFree(spc) } x = unsafe.Pointer(v) if needzero \u0026amp;\u0026amp; span.needzero != 0 { memclrNoHeapPointers(unsafe.Pointer(v), size) } } } else { ... } ... return x }  确定分配对象的大小以及跨度类 runtime.spanClass； 从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间； 调用 runtime.memclrNoHeapPointers 清空空闲内存中的所有数据；  大对象分配逻辑 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size \u0026lt;= maxSmallSize { ... } else { var s *mspan span = c.allocLarge(size, needzero, noscan) span.freeindex = 1 span.allocCount = 1 x = unsafe.Pointer(span.base()) size = span.elemsize } publicationBarrier() mp.mallocing = 0 releasem(mp) return x }   运行时对于大于 32KB 的大对象会单独处理，我们不会从线程缓存或者中心缓存中获取内存管理单元，而是直接调用 runtime.mcache.allocLarge 分配大片内存\n  runtime.mcache.allocLarge 会计算分配该对象所需要的页数，它按照 8KB 的倍数在堆上申请内存\n  ","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D/doc/","summary":"空闲链表 + 多规格多级别管理","title":"golang-内存管理"},{"content":"栈帧布局 我们按照编程语言的语法定义的函数，会被编译器编译为一堆堆的机器指令，当程序运行时，可执行文件被加载到内存，这些机器指令对应到虚拟地址空间中的代码段\n如图里的两个函数A和B，都会分布在代码段上。函数在执行时需要有足够的内存空间，供它存放局部变量，参数，返回值等数据，这段空间对应到虚拟地址空间里的栈。\n栈从高地址往低地址扩展，栈底称为栈基BP，栈顶又称为栈指针SP，GO语言中函数栈帧布局如图\n函数栈帧里一次存放着 调用者的BP, 局部变量，返回值，参数。\n当在函数A中调用函数B时，操作系统会调用 call 指令，call指令只做了两件事情：\n 将下一条指令的地址入栈，也就是返回地址入栈，被调用函数执行结束后会返回到这里 跳转到被调用函数的入口处执行，即通过将被调用函数的入口地址设置给PC或IP寄存器  调用里call指令之后，函数栈帧变成里下面这样\n可以看出，每个函数的栈帧都是一样的结构\n当函数执行完时，会释放自己的栈帧，同时ret指令会被调用，它的作用也有两个\n 弹出call指令压如的返回地址 跳转到这个返回地址  团灭defer 有了上面的知识储备，现在来看几个例子\n例子1 func main() { a, b := 1, 2 swap(a, b) } func swap(a, b int) { a, b = b, a } 有经验的我们一定马上就知道，这次swap并没有成功，我们在栈帧层面上看看是哪里出的问题\n| ... | | main-局部变量：a = 1 | # main函数BP | main-局部变量：b = 2 | | main-给swap的参数: b = 2 | # 这里给swap的a和b都是值拷贝 | main-给swap参数: a = 1 | | main-swap的返回地址 | | main-BP | | ... | # 这里开始进入swap函数的栈帧  注意上面main函数和swap都没有返回值，所以栈帧上也不需要分配返回值。参数入栈从右到左\n 进入swap函数后，swap将main给它的参数交换了，但实际上并没有改动到main里的局部变量\n| ... | | main-局部变量：a = 1 | # main函数里的局部变量不会改动 | main-局部变量：b = 2 | | main-给swap的参数: b = 1 | # b和a交换了 | main-给swap参数: a = 2 | | main-swap的返回地址 | | main-BP | | ... | # 这里开始进入swap函数的栈帧 例子2 func main() { a, b := 1, 2 swap(\u0026amp;a, \u0026amp;b) } func swap(a, b *int) { *a, *b = *b, *a } | ... | | main-局部变量：a = 1 | # main函数BP | main-局部变量：b = 2 | | main-给swap的参数: addrB | # addrA指向a，addrB指向B | main-给swap参数: addrA | | main-swap的返回地址 | | main-BP | | ... | # 这里开始进入swap函数的栈帧 交换之后\n| ... | | main-局部变量：a = 2 | # addrA和addrB指向的数据交换了 | main-局部变量：b = 1 | | main-给swap的参数: addrB | | main-给swap参数: addrA | | main-swap的返回地址 | | main-BP | | ... | # 这里开始进入swap函数的栈帧 例子3 带匿名返回值的例子\nfunc main() { var a, b int b = incr(a) } func incr(a int) int { var b int defer func() { a++ b++ }() a++ b = a return b } 初始时的栈帧\n| ... | | main-局部变量：a = 0 | # main函数BP | main-局部变量：b = 0 | | main-incr的返回值：0 | | main-给incr的参数: a = 0 | | main-incr的返回地址 | | main-BP | | incr-局部变量: b = 0 | | ... | 执行了 a++ 之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 0 | | main-incr的返回值：0 | | main-给incr的参数: a = 1 | 这里增加了1 | main-incr的返回地址 | | main-BP | | incr-局部变量: b = 0 | | ... | 执行了 b = a 之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 0 | | main-incr的返回值：0 | | main-给incr的参数: a = 1 | | main-incr的返回地址 | | main-BP | | incr-局部变量: b = 1 | 这里变了 | ... | 执行 return b 之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 0 | | main-incr的返回值：1 | 这里变了 | main-给incr的参数: a = 1 | | main-incr的返回地址 | | main-BP | | incr-局部变量: b = 1 | | ... | 在defer里执行 a++ 和 b++ 之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 0 | | main-incr的返回值：1 | | main-给incr的参数: a = 2 | 这里加1 | main-incr的返回地址 | | main-BP | | incr-局部变量: b = 2 | 这里加1 | ... | incr的返回值赋给b之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 1 | | main-incr的返回值：1 | | main-给incr的参数: a = 2 | 这里加1 | main-incr的返回地址 | | main-BP | | incr-局部变量: b = 2 | 这里加1 | ... | 因此最后的结果是a为0，b为1\n例子4 带命名返回值的例子\nfunc main() { var a, b int b = incr(a) } func incr(a int) (b int) { defer func() { a++ b++ }() a++ return a } 初始时的栈帧，incr上没有局部变量b\n| ... | | main-局部变量：a = 0 | # main函数BP | main-局部变量：b = 0 | | main-incr的返回值：0 | | main-给incr的参数: a = 0 | | main-incr的返回地址 | | main-BP | | ... | 执行了 a++ 之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 0 | | main-incr的返回值：0 | | main-给incr的参数: a = 1 | # 这里变了 | main-incr的返回地址 | | main-BP | | ... | 执行了 return a 之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 0 | | main-incr的返回值：1 | # 这里变了 | main-给incr的参数: a = 1 | | main-incr的返回地址 | | main-BP | | ... | 在defer里执行 a++ 和 b++ 之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 0 | | main-incr的返回值：2 | # 这里变了 | main-给incr的参数: a = 2 | # 这里变了 | main-incr的返回地址 | | main-BP | | ... | incr的返回值赋给b之后\n| ... | | main-局部变量：a = 0 | | main-局部变量：b = 2 | # 这里变了 | main-incr的返回值：2 | | main-给incr的参数: a = 2 | | main-incr的返回地址 | | main-BP | | ... | 因此最后的结果是a为0，b为2\n总结 了解栈帧布局之后，对于复杂的defer调用，只要能画出函数栈帧情况，问题基本就迎刃而解了\n","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E5%87%BD%E6%95%B0%E6%A0%88%E5%B8%A7%E5%B8%83%E5%B1%80/doc/","summary":"学会golang函数栈帧布局让你团灭各种defer面试题","title":"Golang-函数栈帧布局"},{"content":"总览 binlog bin log称为归档日志、二进制日志，属于MySQL Server层面的，用于记录数据库表结构和表数据的变更，可以简单理解为存储每条变更的sql语句，比如insert、delete、update（当然，不仅是sql，还有事务id，执行时间等等）。\n产生时机 事务提交的时候，一次性将事务中的sql语句按照一定格式记录到binlog\n有什么用 主要有两个作用：主从复制和恢复数据。目前大部分数据库架构都是一主多从，从服务器通过访问主服务器的binlog，保证数据一致性。binlog记录数据库的变更，可以通过它恢复数据\n什么时候落盘 取决于sync_binlog参数\n 0：事务提交后，由操作系统决定什么时候把缓存刷新到磁盘（性能最好，安全性最差） 1：每提交一次事务，调用一次fsync将缓存写入到磁盘（安全性最好，性能最差） n：当提交n次事务后，调用一次fsync将缓存写入到磁盘  文件记录模式 bin log有三种文件记录模式，分别是row、statement、mixed\n  row（row-based replication，PBR）\n记录每一行数据的修改情况 优点：能够清楚记录每行数据修改细节，能够完全保证主从数据一致性 缺点：批量操作时会产生大量的日志，比如alter table\n  statement\n记录每条修改数据的sql，可认为sql语句复制 优点：日志数据量小，减少磁盘IO，提高存储和恢复速度 缺点：在某些情况下会出现主从不一致，比如sql语句中包含**now()**等函数\n  mixed 上面两种模式的混合，MySQL会根据sql语句选择写入模式，一般使用statement模式保存bin log，对于statement模式无法复制的操作，使用row模式保存bin log。\n  redo-log redolog称为重做日志，属于InnoDB存储引擎层的日志，记录物理页的修改信息，而不是某一行或几行修改成什么样。redo-log本质上是WAL\n什么时候产生 事务开始，就会写入redolog。redolog写入到磁盘并不是随着事务提交才写入，而是在事务执行过程中，就已经写入到磁盘\n有什么用 可用于恢复数据。redolog是在事务开始后就写入到磁盘，且是顺序IO，写入速度较快。如果服务器突然掉电，InnoDB引擎会使用redolog把数据库恢复到掉电前的时刻，保证数据的完整性\n什么时候落盘 InnoDB先把日志写到缓冲区（log buffer），然后再把日志从log buffer刷到os buffer，最后调用文件系统的fsync函数将日志刷新到磁盘。重做日志写入时机由参数innodb_flush_log_at_trx_commit决定\n 0：每秒一次，把log buffer写入os buffer，并调用fsync刷到磁盘 1：每次提交事务时，把log buffer写入os buffer，并调用fsync刷到磁盘 2：每次提交事务时，只是写入到os buffer，然后每秒一次调用fsync将日志刷新到磁盘  一般取值为2，因为即使MySQL宕机，数据也没有丢失。只有整个服务器挂了，才损失1秒的数据\n原理 redolog包含了两部分内容，一是redo log buffer, 二是redo logfile。\n其中redo log buffer本质上是一个固定大小的环形的缓冲区，在redo log日志中设置了两个标志位置，checkpoint和write_pos，分别表示记录擦除的位置和记录写入的位置。\n当write_pos标志到了日志结尾时，会从结尾跳至日志头部进行重新循环写入。所以redo log的逻辑结构并不是线性的，而是可看作一个圆周运动。write_pos与checkpoint中间的空间可用于写入新数据，写入和擦除都是往后推移，循环往复的。当write_pos追上checkpoint时，表示redo log日志已经写满。这时不能继续执行新的数据库更新语句，需要停下来先删除一些记录，执行checkpoint规则腾出可写空间。\nundo-log undo log称为回滚日志，属于InnoDB存储引擎层，是逻辑日志，记录每行数据。当我们变更数据时，就会产生undo log，可以认为insert一条数据，undo log会记录一条对应的delete日志，反之亦然。\n什么时候产生 在事务开始前，将当前版本生成undo log\n有什么用 主要作用：提供回滚和多版本并发控制（MVCC）\n 回滚：当需要rollback时，从undo log的逻辑记录读取相应的内容进行回滚 MVCC：undo log记录中存储的是旧版本数据，当一个事务需要读取数据时，会顺着undo链找到满足其可见性的记录  原理 ","permalink":"https://lambertxiao.github.io/posts/mysql-binlog-redolog-undolog/doc/","summary":"名字相近，其实长得不一样","title":"MySQL的三种日志-binlog、redolog、undolog"},{"content":"继承 extends 看图\n实现 implement 依赖 use a 聚合 has a 组合 contains a ","permalink":"https://lambertxiao.github.io/posts/uml-%E7%B1%BB%E5%9B%BE%E7%BA%BF%E6%9D%A1/doc/","summary":"记得住吗？记不住!","title":"UML-类图线条"},{"content":"二分查找 做二分查找牢记循环不变量\n704. 二分查找 704. 二分查找\n给定一个 n 个元素有序的（升序）整型数组 nums 和一个目标值 target ，写一个函数搜索 nums 中的 target，如果目标值存在返回下标，否则返回 -1。\nfunc search(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { m := l + (r - l) \u0026gt;\u0026gt; 1 num := nums[m] if num == target { return m } else if num \u0026gt; target { r = m // 循环不变量，一开始是左闭右开  } else { l = m + 1 } } return -1 } 33. 搜索旋转排序数组 33. 搜索旋转排序数组\n整数数组 nums 按升序排列，数组中的值 互不相同 。\n在传递给函数之前，nums 在预先未知的某个下标 k（0 \u0026lt;= k \u0026lt; nums.length）上进行了 旋转，使数组变为 [nums[k], nums[k+1], \u0026hellip;, nums[n-1], nums[0], nums[1], \u0026hellip;, nums[k-1]]（下标 从 0 开始 计数）。例如， [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。\n给你 旋转后 的数组 nums 和一个整数 target ，如果 nums 中存在这个目标值 target ，则返回它的下标，否则返回 -1 。\nfunc search(nums []int, target int) int { l, r := 0, len(nums) - 1 for l \u0026lt;= r { mid := l + (r - l) \u0026gt;\u0026gt; 1 midv := nums[mid] if midv == target { return mid } if nums[l] \u0026lt;= midv { if target \u0026gt;= nums[l] \u0026amp;\u0026amp; target \u0026lt; midv { r = mid - 1 } else { l = mid + 1 } } else { if target \u0026gt; midv \u0026amp;\u0026amp; target \u0026lt;= nums[r] { l = mid + 1 } else { r = mid - 1 } } } return -1 } 81. 搜索旋转排序数组 II 81. 搜索旋转排序数组 II\n已知存在一个按非降序排列的整数数组 nums ，数组中的值不必互不相同。\n在传递给函数之前，nums 在预先未知的某个下标 k（0 \u0026lt;= k \u0026lt; nums.length）上进行了 旋转 ，使数组变为 [nums[k], nums[k+1], \u0026hellip;, nums[n-1], nums[0], nums[1], \u0026hellip;, nums[k-1]]（下标 从 0 开始 计数）。例如， [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。\n给你 旋转后 的数组 nums 和一个整数 target ，请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ，则返回 true ，否则返回 false 。\n你必须尽可能减少整个操作步骤。\nfunc search(nums []int, target int) bool { l, r := 0, len(nums) - 1 for l \u0026lt;= r { mid := l + (r - l) \u0026gt;\u0026gt; 1 midv := nums[mid] if midv == target { return true } if nums[l] == midv \u0026amp;\u0026amp; midv == nums[r] { l++ r-- } else { if nums[l] \u0026lt;= midv { if target \u0026gt;= nums[l] \u0026amp;\u0026amp; target \u0026lt; midv { r = mid - 1 } else { l = mid + 1 } } else { if target \u0026gt; midv \u0026amp;\u0026amp; target \u0026lt;= nums[r] { l = mid + 1 } else { r = mid - 1 } } } } return false } 34. 在排序数组中查找元素的第一个和最后一个位置 34. 在排序数组中查找元素的第一个和最后一个位置\n给定一个按照升序排列的整数数组 nums，和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target，返回 [-1, -1]。\n进阶： 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗？\n 变形题：对于有多个相同的数，查询左右边界\n func searchRange(nums []int, target int) []int { return []int{ leftBound(nums, target), rightBound(nums, target), } } func leftBound(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { m := l + (r - l) \u0026gt;\u0026gt; 1 n := nums[m] if n \u0026gt;= target { r = m } else { l = m + 1 } } if l \u0026lt; len(nums) \u0026amp;\u0026amp; nums[l] == target { return l } return -1 } func rightBound(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { m := l + (r - l) \u0026gt;\u0026gt; 1 n := nums[m] if n \u0026lt;= target { l = m + 1 } else { r = m } } if r-1 \u0026gt;= 0 \u0026amp;\u0026amp; nums[r-1] == target { return r - 1 } return -1 } 287. 寻找重复数 287. 寻找重复数\n给定一个包含 n + 1 个整数的数组 nums ，其数字都在 [1, n] 范围内（包括 1 和 n），可知至少存在一个重复的整数。 假设 nums 只有 一个重复的整数 ，返回 这个重复的数 。 你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。\nfunc findDuplicate(nums []int) int { // 抽屉原理：把 10 个苹果放进 9 个抽屉，一定存在某个抽屉放至少 2 个苹果。  n := len(nums) l, r := 0, n - 1 for l \u0026lt; r { // mid此时相当于[l,r]之间的中位数  mid := l + (r - l) / 2 cnt := 0 for _, n := range nums { if n \u0026lt;= mid { cnt++ } } // 如果有一半以上的数小于等于中位数，说明小的那一半里有重复  if cnt \u0026gt; mid { r = mid } else { l = mid + 1 } } return l } 153. 寻找旋转排序数组中的最小值 153. 寻找旋转排序数组中的最小值\n已知一个长度为 n 的数组，预先按照升序排列，经由 1 到 n 次 旋转 后，得到输入数组。例如，原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到： 若旋转 4 次，则可以得到 [4,5,6,7,0,1,2] 若旋转 7 次，则可以得到 [0,1,2,4,5,6,7] 注意，数组 [a[0], a[1], a[2], \u0026hellip;, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], \u0026hellip;, a[n-2]] 。\n给你一个元素值 互不相同 的数组 nums ，它原来是一个升序排列的数组，并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。\n你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。\nfunc findMin(nums []int) int { left, right := 0, len(nums) - 1 for left \u0026lt; right { mid := (left + right) / 2 if nums[mid] \u0026gt; nums[right] { left = mid + 1 } else if nums[mid] \u0026lt; nums[right] { right = mid } } return nums[left] } 154. 寻找旋转排序数组中的最小值 II 154. 寻找旋转排序数组中的最小值 II\n已知一个长度为 n 的数组，预先按照升序排列，经由 1 到 n 次 旋转 后，得到输入数组。例如，原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到： 若旋转 4 次，则可以得到 [4,5,6,7,0,1,4] 若旋转 7 次，则可以得到 [0,1,4,4,5,6,7] 注意，数组 [a[0], a[1], a[2], \u0026hellip;, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], \u0026hellip;, a[n-2]] 。\n给你一个可能存在 重复 元素值的数组 nums ，它原来是一个升序排列的数组，并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。\n你必须尽可能减少整个过程的操作步骤。\nfunc findMin(nums []int) int { l, r := 0, len(nums) - 1 for l \u0026lt; r { m := l + (r - l) \u0026gt;\u0026gt; 1 if nums[m] \u0026gt; nums[r] { l = m + 1 } else if nums[m] \u0026lt; nums[r] { r = m } else { r-- } } return nums[l] } 69. x 的平方根 给你一个非负整数 x ，计算并返回 x 的 算术平方根 。 由于返回类型是整数，结果只保留 整数部分 ，小数部分将被 舍去 。 注意：不允许使用任何内置指数函数和算符，例如 pow(x, 0.5) 或者 x ** 0.5 。\nfunc mySqrt(x int) int { if x == 1 { return 1 } l, r := 0, x / 2 + 1 // 左闭右开  for l \u0026lt; r { mid := l + (r - l) / 2 v := mid * mid if v == x { return mid } else if v \u0026gt; x { r = mid } else { l = mid + 1 } } if r * r \u0026gt; x { return r - 1 } return r } 162. 寻找峰值 162. 寻找峰值\n峰值元素是指其值严格大于左右相邻值的元素。 给你一个整数数组 nums，找到峰值元素并返回其索引。数组可能包含多个峰值，在这种情况下，返回 任何一个峰值 所在位置即可。 你可以假设 nums[-1] = nums[n] = -∞ 。 你必须实现时间复杂度为 O(log n) 的算法来解决此问题。\nfunc findPeakElement(nums []int) int { l, r := 0, len(nums) - 1 for l \u0026lt; r { mid := l + (r - l) / 2 if nums[mid] \u0026gt; nums[mid+1] { r = mid } else { l = mid + 1 } } return l } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E4%BA%8C%E5%88%86%E6%B3%95/doc/","summary":"最难不过二分，边界问题最蛋疼","title":"算法-二分法"},{"content":"739. 每日温度 739. 每日温度\n给定一个整数数组 temperatures ，表示每天的温度，返回一个数组 answer ，其中 answer[i] 是指在第 i 天之后，才会有更高的温度。如果气温在这之后都不会升高，请在该位置用 0 来代替。\nfunction dailyTemperatures(temperatures: number[]): number[] { let res: number[] = [] let stack: number[] = [] for (let i = temperatures.length - 1; i \u0026gt;= 0; i--) { let len = stack.length // 当栈不为空，将此时栈里比当前元素小的干掉  while (len != 0 \u0026amp;\u0026amp; temperatures[stack[len-1]] \u0026lt;= temperatures[i]) { stack.pop() } // 出循环后栈顶即为比当前元素大的元素  res[i] = len != 0 ? stack[len-1] - i : 0 stack.push(i) } return res } 1438. 绝对差不超过限制的最长连续子数组 1438. 绝对差不超过限制的最长连续子数组 给你一个整数数组 nums ，和一个表示限制的整数 limit，请你返回最长连续子数组的长度，该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。\n如果不存在满足条件的子数组，则返回 0 。\n滑动窗口 + 单调递减栈 + 单调递增栈\nfunc longestSubarray(nums []int, limit int) int { // 单调递减栈和单调递增栈  minq, maxq := []int{}, []int{} l, r, ans := 0, 0, 0 for r \u0026lt; len(nums) { num := nums[r] for len(minq) != 0 \u0026amp;\u0026amp; minq[len(minq)-1] \u0026lt; num { minq = minq[:len(minq)-1] } minq = append(minq, num) for len(maxq) != 0 \u0026amp;\u0026amp; maxq[len(maxq)-1] \u0026gt; num { maxq = maxq[:len(maxq)-1] } maxq = append(maxq, num) // 此时maxq里可以拿到最小值，minq里可以拿到最大值  for len(maxq) \u0026gt; 0 \u0026amp;\u0026amp; len(minq) \u0026gt; 0 \u0026amp;\u0026amp; minq[0] - maxq[0] \u0026gt; limit { if nums[l] == maxq[0] { maxq = maxq[1:] } if nums[l] == minq[0] { minq = minq[1:] } // 在不满足绝对差小于limit的情况下，需要移动窗口左边界  l++ } ans = max(ans, r - l + 1) r++ } return ans } func max(x, y int) int { if x \u0026gt; y { return x } return y } 862. 和至少为 K 的最短子数组 862. 和至少为 K 的最短子数组\n给你一个整数数组 nums 和一个整数 k ，找出 nums 中和至少为 k 的 最短非空子数组 ，并返回该子数组的长度。如果不存在这样的 子数组 ，返回 -1 。\n子数组 是数组中 连续 的一部分。\nfunc shortestSubarray(nums []int, k int) int { size := len(nums) pres := make([]int, size + 1) for i, num := range nums { pres[i+1] = pres[i] + num } ans := size + 1 // 单调递增队列, q里存放索引  maxq := new(queue) for i := 0; i \u0026lt; len(pres); i++ { for !maxq.isEmpty() \u0026amp;\u0026amp; pres[maxq.last()] \u0026gt; pres[i] { maxq.popRight() } for !maxq.isEmpty() \u0026amp;\u0026amp; pres[i] - pres[maxq.first()] \u0026gt;= k { ans = min(ans, i - maxq.first()) maxq.popLeft() } maxq.pushRight(i) } if ans \u0026lt; size + 1 { return ans } return -1 } func min(x, y int) int { if x \u0026gt; y { return y } return x } type queue []int func (q *queue) popRight() { t := *q t = t[:len(t)-1] *q = t } func (q *queue) pushRight(x int) { t := *q t = append(t, x) *q = t } func (q *queue) popLeft() { t := *q t = t[1:] *q = t } func (q *queue) last() int { t := *q return t[len(t)-1] } func (q *queue) first() int { t := *q return t[0] } func (q queue) isEmpty() bool { return len(q) == 0 } 316. 去除重复字母 316. 去除重复字母\n给你一个字符串 s ，请你去除字符串中重复的字母，使得每个字母只出现一次。需保证 返回结果的字典序最小（要求不能打乱其他字符的相对位置）。\nfunc removeDuplicateLetters(s string) string { // 统计字符  cnt := [26]int{} for _, c := range s { cnt[c-\u0026#39;a\u0026#39;]++ } // 统计单调栈中存的字符  stackCnt := [26]int{} // 单调递增栈  stk := new(stack) for _, c := range s { cc := byte(c - \u0026#39;a\u0026#39;) cnt[cc]-- // 使用掉一个字符就减1  if stackCnt[cc] != 0 { continue } for !stk.isEmpty() \u0026amp;\u0026amp; (stk.top() - \u0026#39;a\u0026#39;) \u0026gt; cc { last := stk.top() - \u0026#39;a\u0026#39; // 移除一个字符的前提是这个字符是有重复的  if cnt[last] \u0026lt;= 0 { break } stackCnt[last] = 0 stk.pop() } stk.push(byte(c)) stackCnt[cc] = 1 } return string(*stk) } type stack []byte func (s *stack) pop() { t := *s t = t[:len(t)-1] *s = t } func (s *stack) push(x byte) { t := *s t = append(t, x) *s = t } func (s *stack) top() byte { t := *s return t[len(t)-1] } func (s *stack) isEmpty() bool { t := *s return len(t) == 0 } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%8D%95%E8%B0%83%E6%A0%88/doc/","summary":"单调栈总是能解决一些看起来很困难的题","title":"算法-单调栈"},{"content":"poolDequeue poolDequeue是一个无锁的固定大小的环形队列，它支持单生产者，多消费者。生产者在头部push或pop元素，消费者在尾部pop元素。\ntype poolDequeue struct { // headTail packs together a 32-bit head index and a 32-bit \t// tail index. Both are indexes into vals modulo len(vals)-1. \t// \t// tail = index of oldest data in queue \t// head = index of next slot to fill \t// \t// Slots in the range [tail, head) are owned by consumers. \t// A consumer continues to own a slot outside this range until \t// it nils the slot, at which point ownership passes to the \t// producer. \t// \t// The head index is stored in the most-significant bits so \t// that we can atomically add to it and the overflow is \t// harmless. \theadTail uint64 // vals is a ring buffer of interface{} values stored in this \t// dequeue. The size of this must be a power of 2. \t// \t// vals[i].typ is nil if the slot is empty and non-nil \t// otherwise. A slot is still in use until *both* the tail \t// index has moved beyond it and typ has been set to nil. This \t// is set to nil atomically by the consumer and read \t// atomically by the producer. \tvals []eface } type eface struct { typ, val unsafe.Pointer }  headTail是个64位无符号数，前32位放置head的下标，后32位放置tail的下标。 head表示下一个该被填充的slot下标，tail表示队列里的最老元素所在下标。 vals是一个环形缓冲区，可以存放interface{}值。  const dequeueBits = 32 // dequeueLimit is the maximum size of a poolDequeue.  // // This must be at most (1\u0026lt;\u0026lt;dequeueBits)/2 because detecting fullness // depends on wrapping around the ring buffer without wrapping around // the index. We divide by 4 so this fits in an int on 32-bit. const dequeueLimit = (1 \u0026lt;\u0026lt; dequeueBits) / 4 // dequeueNil is used in poolDequeue to represent interface{}(nil). // Since we use nil to represent empty slots, we need a sentinel value // to represent nil. type dequeueNil *struct{} func (d *poolDequeue) unpack(ptrs uint64) (head, tail uint32) { const mask = 1\u0026lt;\u0026lt;dequeueBits - 1 head = uint32((ptrs \u0026gt;\u0026gt; dequeueBits) \u0026amp; mask) tail = uint32(ptrs \u0026amp; mask) return } func (d *poolDequeue) pack(head, tail uint32) uint64 { const mask = 1\u0026lt;\u0026lt;dequeueBits - 1 return (uint64(head) \u0026lt;\u0026lt; dequeueBits) | uint64(tail\u0026amp;mask) } unpack和pack是一对相反的操作，用来得到head和tail指针\n// pushHead adds val at the head of the queue. It returns false if the // queue is full. It must only be called by a single producer. func (d *poolDequeue) pushHead(val interface{}) bool { ptrs := atomic.LoadUint64(\u0026amp;d.headTail) head, tail := d.unpack(ptrs) // 检查队列是否已经满了 \tif (tail+uint32(len(d.vals)))\u0026amp;(1\u0026lt;\u0026lt;dequeueBits-1) == head { // Queue is full. \treturn false } // 计算落在哪个slot上 \tslot := \u0026amp;d.vals[head\u0026amp;uint32(len(d.vals)-1)] // Check if the head slot has been released by popTail. \ttyp := atomic.LoadPointer(\u0026amp;slot.typ) if typ != nil { // Another goroutine is still cleaning up the tail, so \t// the queue is actually still full. \treturn false } // The head slot is free, so we own it. \tif val == nil { val = dequeueNil(nil) } *(*interface{})(unsafe.Pointer(slot)) = val // Increment head. This passes ownership of slot to popTail \t// and acts as a store barrier for writing the slot. \tatomic.AddUint64(\u0026amp;d.headTail, 1\u0026lt;\u0026lt;dequeueBits) return true }  通过原子操作得到ptrs，进而得到head和tail指针 当tail + vals长度 = head时，表示队列已满 通过位操作计算出当前要插入的元素落在哪个slot 如果slot的type非空，说明该slot还没有被popTail release，实际上deque还是满的；所以直接return false; 插入的值更新到slot指针指向的值 原子自加head  // popHead removes and returns the element at the head of the queue. // It returns false if the queue is empty. It must only be called by a // single producer. func (d *poolDequeue) popHead() (interface{}, bool) { var slot *eface for { ptrs := atomic.LoadUint64(\u0026amp;d.headTail) head, tail := d.unpack(ptrs) if tail == head { // Queue is empty. \treturn nil, false } // Confirm tail and decrement head. We do this before \t// reading the value to take back ownership of this \t// slot. \thead-- ptrs2 := d.pack(head, tail) if atomic.CompareAndSwapUint64(\u0026amp;d.headTail, ptrs, ptrs2) { // We successfully took back slot. \tslot = \u0026amp;d.vals[head\u0026amp;uint32(len(d.vals)-1)] break } } val := *(*interface{})(unsafe.Pointer(slot)) if val == dequeueNil(nil) { val = nil } // Zero the slot. Unlike popTail, this isn\u0026#39;t racing with \t// pushHead, so we don\u0026#39;t need to be careful here. \t*slot = eface{} return val, true }  原子读取headTail的值，得到head和tail 根据tail == head，检查队列是否为空 head\u0026ndash; 计算出新的ptrs 通过CAS更新headTail的值 如果成功更新则拿到head位置的slot，否则继续for循环 取出slot指针指向的值，并返回  // popTail removes and returns the element at the tail of the queue. // It returns false if the queue is empty. It may be called by any // number of consumers. func (d *poolDequeue) popTail() (interface{}, bool) { var slot *eface for { ptrs := atomic.LoadUint64(\u0026amp;d.headTail) head, tail := d.unpack(ptrs) if tail == head { // Queue is empty. \treturn nil, false } // Confirm head and tail (for our speculative check \t// above) and increment tail. If this succeeds, then \t// we own the slot at tail. \tptrs2 := d.pack(head, tail+1) if atomic.CompareAndSwapUint64(\u0026amp;d.headTail, ptrs, ptrs2) { // Success. \tslot = \u0026amp;d.vals[tail\u0026amp;uint32(len(d.vals)-1)] break } } // We now own slot. \tval := *(*interface{})(unsafe.Pointer(slot)) if val == dequeueNil(nil) { val = nil } // Tell pushHead that we\u0026#39;re done with this slot. Zeroing the \t// slot is also important so we don\u0026#39;t leave behind references \t// that could keep this object live longer than necessary. \t// \t// We write to val first and then publish that we\u0026#39;re done with \t// this slot by atomically writing to typ. \tslot.val = nil atomic.StorePointer(\u0026amp;slot.typ, nil) // At this point pushHead owns the slot.  return val, true }  popTail会被多个consumer一起调用 原子读取headTail的值，得到head和tail 根据tail == head，检查队列是否为空 tail+1并计算出新的ptrs 通过CAS更新headTail的值，更新成功则取到tail对应的slot，否则继续for循环 将slot的val和type都清为nil, 告诉pushHead, slot我们已经使用完了，pushHead可以往里面填充数据了  总结  poolDequeue是一个双端环形队列 poolDequeue是固定大小的 pushHead和popHead只由单个Producer来调用 popTail会由多个consumer来调用 poolDequeue无锁，操作都用的是CAS  ","permalink":"https://lambertxiao.github.io/posts/golang/golang-sync.pool%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0/1-pooldequeue/","summary":"环形队列，无锁，CAS","title":"Golang-sync.Pool结构之poolDequeue"},{"content":"股票问题 对于股票问题，本质上只有两个维度在变，分别是天数和那一天的状态\n121. 买卖股票的最佳时机 121. 买卖股票的最佳时机\n给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。\nfunc maxProfit(prices []int) int { size := len(prices) dp := make([][]int, 2) for i := range dp { dp[i] = make([]int, 2) } dp[0][0] = -prices[0] dp[0][1] = 0 for i := 1; i \u0026lt; size; i++ { dp[i % 2][0] = max(dp[(i-1)%2][0], -prices[i]) dp[i % 2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0]+prices[i]) } return dp[(size-1)%2][1] } 122. 买卖股票的最佳时机 II 122. 买卖股票的最佳时机 II\n给定一个数组 prices ，其中 prices[i] 表示股票第 i 天的价格。 在每一天，你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它，然后在 同一天 出售。返回 你能获得的 最大 利润 。\nfunc maxProfit(prices []int) int { size := len(prices) dp := make([][]int, 2) for i := range dp { dp[i] = make([]int, 2) } dp[0][0] = -prices[0] dp[0][1] = 0 for i := 1; i \u0026lt; size; i++ { dp[i % 2][0] = max(dp[(i-1)%2][0], dp[(i-1)%2][1] - prices[i]) dp[i % 2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0] + prices[i]) } return dp[(size-1)%2][1] } 123. 买卖股票的最佳时机 III 123. 买卖股票的最佳时机 III\n给定一个数组，它的第 i 个元素是一支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。\nfunc maxProfit(prices []int) int { size := len(prices) dp := make([][]int, 5) for i := range dp { dp[i] = make([]int, 5) } // dp[i][j] j有5个状态，0没有操作，1第一次买入，2第一次卖出，3第二次买入，4第二次卖出  dp[0][1] = -prices[0] dp[0][3] = -prices[0] for i := 1; i \u0026lt; size; i++ { dp[i%5][0] = dp[(i-1)%5][0] dp[i%5][1] = max(dp[(i-1)%5][0] - prices[i], dp[(i-1)%5][1]) dp[i%5][2] = max(dp[(i-1)%5][1] + prices[i], dp[(i-1)%5][2]) dp[i%5][3] = max(dp[(i-1)%5][2] - prices[i], dp[(i-1)%5][3]) dp[i%5][4] = max(dp[(i-1)%5][3] + prices[i], dp[(i-1)%5][4]) } return dp[(size-1)%5][4] } 188. 买卖股票的最佳时机 IV 188. 买卖股票的最佳时机 IV\n给定一个整数数组 prices ，它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。\nfunc maxProfit(k int, prices []int) int { if len(prices) == 0 { return 0 } size := len(prices) dp := make([][]int, size) for i := range dp { dp[i] = make([]int, 2 * k + 1) } // dp[i][j] j有5个状态，0没有操作，1第一次买入，2第一次卖出，...2k-1次买入，2k次卖出  // basecase偶数位都为0，奇数为-prices[0]  for j := 1; j \u0026lt; 2 * k + 1; j+=2 { dp[0][j] = -prices[0] } for i := 1; i \u0026lt; size; i++ { for j := 0; j \u0026lt; 2 * k - 1; j+=2 { dp[i][j+1] = max(dp[i-1][j] - prices[i], dp[i-1][j+1]) dp[i][j+2] = max(dp[i-1][j+1] + prices[i], dp[i-1][j+2]) } } return dp[size-1][2*k] } 309. 最佳买卖股票时机含冷冻期 309. 最佳买卖股票时机含冷冻期\n给定一个整数数组prices，其中第 prices[i] 表示第 i 天的股票价格 。​ 设计一个算法计算出最大利润。在满足以下约束条件下，你可以尽可能地完成更多的交易（多次买卖一支股票）: 卖出股票后，你无法在第二天买入股票 (即冷冻期为 1 天)。 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）\nfunc maxProfit(prices []int) int { size := len(prices) if size == 0 { return 0 } dp := make([][]int, size) for i := range dp { dp[i] = make([]int, 4) } // 状态有，0买入状态，1之前卖出，2刚卖出，3处于冷冻期  dp[0][0] = -prices[0] for i := 1; i \u0026lt; size; i++ { dp[i][0] = max(dp[i-1][0], max(dp[i-1][3], dp[i-1][1]) - prices[i]) dp[i][1] = max(dp[i-1][1], dp[i-1][3]) // 冷冻期只有一天，到今天就不是冷冻期了  dp[i][2] = dp[i-1][0] + prices[i] dp[i][3] = dp[i-1][2] } return max(max(dp[size-1][1], dp[size-1][2]), dp[size-1][3]) } 714. 买卖股票的最佳时机含手续费 714. 买卖股票的最佳时机含手续费\n给定一个整数数组 prices，其中 prices[i]表示第 i 天的股票价格 ；整数 fee 代表了交易股票的手续费用。 你可以无限次地完成交易，但是你每笔交易都需要付手续费。如果你已经购买了一个股票，在卖出它之前你就不能再继续购买股票了。 返回获得利润的最大值。 注意：这里的一笔交易指买入持有并卖出股票的整个过程，每笔交易你只需要为支付一次手续费。\nfunc maxProfit(prices []int, fee int) int { size := len(prices) dp := make([][]int, 2) for i := range dp { dp[i] = make([]int, 2) } dp[0][0] = -prices[0] dp[0][1] = 0 for i := 1; i \u0026lt; size; i++ { dp[i % 2][0] = max(dp[(i-1)%2][0], dp[(i-1)%2][1] - prices[i]) dp[i % 2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0] + prices[i] - fee) } return dp[(size-1)%2][1] } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8/doc/","summary":"啥时候A股的最大收益能用算法算出来也就不用上班了","title":"动态规划-买卖股票"},{"content":"198. 打家劫舍 用动态规划团灭打家劫舍题目\n198. 打家劫舍\n你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。\nfunc rob(nums []int) int { length := len(nums) if length == 0 { return 0 } if length == 1 { return nums[0] } d := make([]int, length) d[0] = nums[0] d[1] = max(nums[0], nums[1]) for i := 2; i \u0026lt; length; i++ { d[i] = max(d[i - 1], d[i - 2] + nums[i]) } return d[length-1] } 213. 打家劫舍 II 213. 打家劫舍 II\n你是一个专业的小偷，计划偷窃沿街的房屋，每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈，这意味着第一个房屋和最后一个房屋是紧挨着的。同时，相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警 。 给定一个代表每个房屋存放金额的非负整数数组，计算你 在不触动警报装置的情况下 ，今晚能够偷窃到的最高金额。\nfunc rob(nums []int) int { l := len(nums) if l == 1 { return nums[0] } if l == 2 { return max(nums[0], nums[1]) } d1 := make([]int, l-1) d2 := make([]int, l) // 去掉尾巴  d1[0] = nums[0] d1[1] = max(nums[0], nums[1]) // 去掉头  d2[1] = nums[1] d2[2] = max(nums[1], nums[2]) for i := 2; i \u0026lt; l - 1; i++ { d1[i] = max(d1[i - 1], d1[i - 2] + nums[i]) d2[i+1] = max(d2[i], d2[i - 1] + nums[i+1]) } return max(d1[l-2], d2[l-1]) } 337. 打家劫舍 III 337. 打家劫舍 III\n小偷又发现了一个新的可行窃的地区。这个地区只有一个入口，我们称之为 root 。 除了 root 之外，每栋房子有且只有一个“父“房子与之相连。一番侦察之后，聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ，房屋将自动报警。 给定二叉树的 root 。返回 在不触动警报的情况下 ，小偷能够盗取的最高金额 。\nfunc rob(root *TreeNode) int { if root == nil { return 0 } val := robNode(root) return max(val[0], val[1]) } // 返回一个节点偷与不偷的两种情况的金额 func robNode(root *TreeNode) []int { if root == nil { return []int{0, 0} } left := robNode(root.Left) right := robNode(root.Right) // 偷当前节点  val1 := root.Val + left[1] + right[1] // 不偷当前节点 = 左节点里偷与不偷取最大值 + 右节点里偷与不偷取最大值  val2 := max(left[0], left[1]) + max(right[0], right[1]) return []int{val1, val2} } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E6%89%93%E5%AE%B6%E5%8A%AB%E8%88%8D/doc/","summary":"不会打家劫舍的程序员不是好的小偷","title":"动态规划-打家劫舍"},{"content":"70. 爬楼梯\n假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢？\nfunc climbStairs(n int) int { if n == 0 || n == 1 || n == 2 { return n } d := make([]int, n+1) d[1] = 1 d[2] = 2 i := 3 for i \u0026lt;= n { d[i] = d[i - 1] + d[i - 2] i++ } return d[n] } 746. 使用最小花费爬楼梯\n给你一个整数数组 cost ，其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用，即可选择向上爬一个或者两个台阶。 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。 请你计算并返回达到楼梯顶部的最低花费。\nfunc minCostClimbingStairs(cost []int) int { length := len(cost) d := make([]int, length+1) d[0] = 0 d[1] = 0 for i := 2; i \u0026lt;= length; i++ { d[i] = min(cost[i-1] + d[i -1], cost[i - 2] + d[i-2]) } return d[length] } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E7%88%AC%E6%A5%BC%E6%A2%AF/doc/","summary":"爬个楼梯也事多","title":"动态规划-爬楼梯"},{"content":"李大猪和李奶茶\n","permalink":"https://lambertxiao.github.io/posts/%E8%A7%92%E8%90%BD%E7%94%9F%E7%89%A9-%E5%9D%A8/doc/","summary":"坨坨酱","title":"角落生物-坨坨坨酱"},{"content":"今天小星面试字节，有defer相关的笔试题，对于defer这块，一直处于模糊的阶段，借这次面试失败的动力，彻底搞懂它\n什么是defer golang中的defer实际的源码位于 /usr/local/go/src/runtime/runtime2.go\n// A _defer holds an entry on the list of deferred calls. // If you add a field here, add code to clear it in freedefer and deferProcStack // This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct // and cmd/compile/internal/gc/ssa.go:(*state).call. // Some defers will be allocated on the stack and some on the heap. // All defers are logically part of the stack, so write barriers to // initialize them are not required. All defers must be manually scanned, // and for heap defers, marked. type _defer struct { siz int32 // includes both arguments and results \tstarted bool heap bool // openDefer indicates that this _defer is for a frame with open-coded \t// defers. We have only one defer record for the entire frame (which may \t// currently have 0, 1, or more defers active). \topenDefer bool sp uintptr // sp at time of defer \tpc uintptr // pc at time of defer \tfn *funcval // can be nil for open-coded defers \t_panic *_panic // panic that is running defer \tlink *_defer // If openDefer is true, the fields below record values about the stack \t// frame and associated function that has the open-coded defer(s). sp \t// above will be the sp for the frame, and pc will be address of the \t// deferreturn call in the function. \tfd unsafe.Pointer // funcdata for the function associated with the frame \tvarp uintptr // value of varp for the stack frame \t// framepc is the current pc associated with the stack frame. Together, \t// with sp above (which is the sp associated with the stack frame), \t// framepc/sp can be used as pc/sp pair to continue a stack trace via \t// gentraceback(). \tframepc uintptr } defer在编译器编译后长什么样子 defer的执行顺序 defer的参数捕获列表是什么？ ","permalink":"https://lambertxiao.github.io/posts/golang/golang-defer%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6/doc/","summary":"麻了呀，面试面到了答不出来","title":"Golang-Defer底层实现机制"},{"content":"sync.Pool的作用 一般地，对于一些频繁创建-销毁对象的场景，为了降低GC的压力，会将使用完的对象缓存起来，而不是直接让GC给回收掉。在golang中，提供了sync.Pool这个类，很方便地让我们实现对一个对象的复用\nsync.Pool的用法 pool的用法十分简单，只需要在声明pool对象的时候给入New函数，New函数可以让我们自定义需要生成一个什么对象\ntype Fool struct {} pool := sync.Pool{ New: func() interface{} { return new(Fool) } } foo := pool.Get().(*Fool) pool.Put(foo) sync.Pool的底层结构 让我们打开源码，一窥pool的底层实现原理\n /usr/local/go/src/sync/pool.go\n type Pool struct { noCopy noCopy local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal \tlocalSize uintptr // size of the local array  victim unsafe.Pointer // local from previous cycle \tvictimSize uintptr // size of victims array  // New optionally specifies a function to generate \t// a value when Get would otherwise return nil. \t// It may not be changed concurrently with calls to Get. \tNew func() interface{} } 从注释上我们可以看出，local其实是一个数组，类型为[P]poolLocal, 而数组的长度就是当前P(MPG模型里的P)的数量，所以，对应每一个P，Pool里都有一个poolLocal的本地池，由于P一个时刻只能和一个M绑定，所以访问poolLocal时，可以做到无锁访问。localSize 的值也就明显是P的数量。\nvictim 和 victimSize 咋一看，完全看不出来设计思路，不急，我们先看看两个成员函数的实现。\nGet操作 先从Get方法入手，查看一个对象是如何从池中给到我们的\n// Get selects an arbitrary item from the Pool, removes it from the // Pool, and returns it to the caller. // Get may choose to ignore the pool and treat it as empty. // Callers should not assume any relation between values passed to Put and // the values returned by Get. // // If Get would otherwise return nil and p.New is non-nil, Get returns // the result of calling p.New. func (p *Pool) Get() interface{} { if race.Enabled { race.Disable() } l, pid := p.pin() x := l.private l.private = nil if x == nil { // Try to pop the head of the local shard. We prefer \t// the head over the tail for temporal locality of \t// reuse. \tx, _ = l.shared.popHead() if x == nil { x = p.getSlow(pid) } } runtime_procUnpin() if race.Enabled { race.Enable() if x != nil { race.Acquire(poolRaceAddr(x)) } } if x == nil \u0026amp;\u0026amp; p.New != nil { x = p.New() } return x }  利用p.pin()，将当前的goroutine固定在P上，并且了禁用了抢占，同时得到来poolLocal 检查poolLocal的private是否为空，不为空则会被拿来用 当private为空，会从shared的头部获取一个元素 如果还是获取不到，则会去其他P的对象池里拿元素  func (c *poolChain) popHead() (interface{}, bool) { d := c.head for d != nil { if val, ok := d.popHead(); ok { return val, ok } // There may still be unconsumed elements in the \t// previous dequeue, so try backing up. \td = loadPoolChainElt(\u0026amp;d.prev) } return nil, false }  从head指向的poolDequeue中获取元素  func (p *Pool) getSlow(pid int) interface{} { // See the comment in pin regarding ordering of the loads. \tsize := runtime_LoadAcquintptr(\u0026amp;p.localSize) // load-acquire \tlocals := p.local // load-consume \t// Try to steal one element from other procs. \tfor i := 0; i \u0026lt; int(size); i++ { l := indexLocal(locals, (pid+i+1)%int(size)) if x, _ := l.shared.popTail(); x != nil { return x } } // Try the victim cache. We do this after attempting to steal \t// from all primary caches because we want objects in the \t// victim cache to age out if at all possible. \tsize = atomic.LoadUintptr(\u0026amp;p.victimSize) if uintptr(pid) \u0026gt;= size { return nil } locals = p.victim l := indexLocal(locals, pid) if x := l.private; x != nil { l.private = nil return x } for i := 0; i \u0026lt; int(size); i++ { l := indexLocal(locals, (pid+i)%int(size)) if x, _ := l.shared.popTail(); x != nil { return x } } // Mark the victim cache as empty for future gets don\u0026#39;t bother \t// with it. \tatomic.StoreUintptr(\u0026amp;p.victimSize, 0) return nil }  尝试从其他P的shared池中获取元素  func (c *poolChain) popTail() (interface{}, bool) { d := loadPoolChainElt(\u0026amp;c.tail) if d == nil { return nil, false } for { // It\u0026#39;s important that we load the next pointer \t// *before* popping the tail. In general, d may be \t// transiently empty, but if next is non-nil before \t// the pop and the pop fails, then d is permanently \t// empty, which is the only condition under which it\u0026#39;s \t// safe to drop d from the chain. \td2 := loadPoolChainElt(\u0026amp;d.next) if val, ok := d.popTail(); ok { return val, ok } if d2 == nil { // This is the only dequeue. It\u0026#39;s empty right \t// now, but could be pushed to in the future. \treturn nil, false } // The tail of the chain has been drained, so move on \t// to the next dequeue. Try to drop it from the chain \t// so the next pop doesn\u0026#39;t have to look at the empty \t// dequeue again. \tif atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(\u0026amp;c.tail)), unsafe.Pointer(d), unsafe.Pointer(d2)) { // We won the race. Clear the prev pointer so \t// the garbage collector can collect the empty \t// dequeue and so popHead doesn\u0026#39;t back up \t// further than necessary. \tstorePoolChainElt(\u0026amp;d2.prev, nil) } d = d2 } } 将poolLocal的结构摆开，看看里面有什么\ntype poolLocal struct { poolLocalInternal // Prevents false sharing on widespread platforms with \t// 128 mod (cache line size) = 0 . \tpad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte } // Local per-P Pool appendix. type poolLocalInternal struct { private interface{} // Can be used only by the respective P. \tshared poolChain // Local P can pushHead/popHead; any P can popTail. }  poolLocalInternal上的private是P私有的，在Get的时候会被优先获取 shared是个双向队列，本地的P能从head处插入及获取元素，而其余的P只能拿尾部的内容  下面打开poolChain的结构\n// poolChain is a dynamically-sized version of poolDequeue. // // This is implemented as a doubly-linked list queue of poolDequeues // where each dequeue is double the size of the previous one. Once a // dequeue fills up, this allocates a new one and only ever pushes to // the latest dequeue. Pops happen from the other end of the list and // once a dequeue is exhausted, it gets removed from the list. type poolChain struct { // head is the poolDequeue to push to. This is only accessed \t// by the producer, so doesn\u0026#39;t need to be synchronized. \thead *poolChainElt // tail is the poolDequeue to popTail from. This is accessed \t// by consumers, so reads and writes must be atomic. \ttail *poolChainElt } type poolChainElt struct { poolDequeue // next and prev link to the adjacent poolChainElts in this \t// poolChain. \t// \t// next is written atomically by the producer and read \t// atomically by the consumer. It only transitions from nil to \t// non-nil. \t// \t// prev is written atomically by the consumer and read \t// atomically by the producer. It only transitions from \t// non-nil to nil. \tnext, prev *poolChainElt } 可以看出poolChain本身是个双端队列，持有着队列的head和tail两个指针，而poolChain队列里的每个Item则是个poolDequeue（环形队列），我们知道poolDequeue是固定长度的，但poolChain又是动态长度的，poolChain通过双向链表的形式将poolDequeue串起来使用。\nPut操作 来看sync.Pool的Put操作\n// Put adds x to the pool. func (p *Pool) Put(x interface{}) { if x == nil { return } if race.Enabled { if fastrand()%4 == 0 { // Randomly drop x on floor. \treturn } race.ReleaseMerge(poolRaceAddr(x)) race.Disable() } l, _ := p.pin() if l.private == nil { l.private = x x = nil } if x != nil { l.shared.pushHead(x) } runtime_procUnpin() if race.Enabled { race.Enable() } }  一样地，将当前的P和Goroutine固定住 检查poolLocal的private是否为空，为空则赋值上回收的对象 如果x没被private回收，则投放到shared中  func (c *poolChain) pushHead(val interface{}) { d := c.head if d == nil { // Initialize the chain. \tconst initSize = 8 // Must be a power of 2 \td = new(poolChainElt) d.vals = make([]eface, initSize) c.head = d storePoolChainElt(\u0026amp;c.tail, d) } if d.pushHead(val) { return } // The current dequeue is full. Allocate a new one of twice \t// the size. \tnewSize := len(d.vals) * 2 if newSize \u0026gt;= dequeueLimit { // Can\u0026#39;t make it any bigger. \tnewSize = dequeueLimit } d2 := \u0026amp;poolChainElt{prev: d} d2.vals = make([]eface, newSize) c.head = d2 storePoolChainElt(\u0026amp;d.next, d2) d2.pushHead(val) }  poolChain执行pushHead时，如果poolChain还是空的，则初始化一个size为8的poolDequeue 将回收的元素放入head指向的poolDequeue中 如果head指向的poolDequeue已满了，则创建一个新的poolDequeue，并且缓冲区大小为原来的两倍 将新建的poolDequeue插入头部  sync.Pool中的元素什么时候被回收 GC时\nfunc init() { runtime_registerPoolCleanup(poolCleanup) } 在sync.Pool的init中，注册了GC的Hook\nfunc poolCleanup() { // This function is called with the world stopped, at the beginning of a garbage collection. \t// It must not allocate and probably should not call any runtime functions.  // Because the world is stopped, no pool user can be in a \t// pinned section (in effect, this has all Ps pinned).  // Drop victim caches from all pools. \tfor _, p := range oldPools { p.victim = nil p.victimSize = 0 } // Move primary cache to victim cache. \tfor _, p := range allPools { p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } // The pools with non-empty primary caches now have non-empty \t// victim caches and no pools have primary caches. \toldPools, allPools = allPools, nil } 总结  关键思想是对象的复用，避免重复创建、销毁。减轻 GC 的压力。 sync.Pool 是协程安全的 不要对 Get 得到的对象有任何假设，默认Get到对象是一个空对象，Get之后手动初始化。 好的实践是：Put操作执行前将对象“清空”，并且确保对象被Put进去之后不要有任何的指针引用再次使用 Pool 里对象的生命周期受 GC 影响，不适合于做连接池，因为连接池需要自己管理对象的生命周期。 Pool 不可以指定⼤⼩，⼤⼩只受制于 GC 临界值。 procPin 将 G 和 P 绑定，防止 G 被抢占。在绑定期间，GC 无法清理缓存的对象。 sync.Pool 的设计理念，包括：无锁、操作对象隔离、原子操作代替锁、行为隔离——链表、Victim Cache 降低 GC 开销。  细节备注 // pin pins the current goroutine to P, disables preemption and // returns poolLocal pool for the P and the P\u0026#39;s id. // Caller must call runtime_procUnpin() when done with the pool. func (p *Pool) pin() (*poolLocal, int) { // 将goroutine固定里p上，并拿到里p的id \tpid := runtime_procPin() // In pinSlow we store to local and then to localSize, here we load in opposite order. \t// Since we\u0026#39;ve disabled preemption, GC cannot happen in between. \t// Thus here we must observe local at least as large localSize. \t// We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness). \ts := runtime_LoadAcquintptr(\u0026amp;p.localSize) // load-acquire \tl := p.local // load-consume \tif uintptr(pid) \u0026lt; s { return indexLocal(l, pid), pid } return p.pinSlow() } runtime_procPin方法实际上是对应以下函数\n//go:linkname sync_runtime_procPin sync.runtime_procPin //go:nosplit func sync_runtime_procPin() int { return procPin() } //go:nosplit func procPin() int { _g_ := getg() // 获取了当前的G \tmp := _g_.m mp.locks++ // 这里M的locks自增 \treturn int(mp.p.ptr().id) } ","permalink":"https://lambertxiao.github.io/posts/golang/golang-sync.pool%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0/pool/","summary":"sync.Pool在项目中被频繁用到，那么它底下是怎么实现的呢","title":"Golang-Sync.Pool底层结构"},{"content":"今天面试字节，由于简历里写了限流器相关的项目经验，被要求使用代码实现一个限流器，当时冷不丁来这一下，没做出来，以及博客，纪念我死去的面试\n限流器的作用 限流器常用来限制服务的访问次数，结合自身的业务逻辑，可以实现在可以维度去控制client的访问频次，超过规定的频次后可以直接拒绝访问\n常见限流器的实现方案  令牌桶  常见的限流器库 以 github.com/juju/ratelimit 库为例，看看实现一个限流器需要具备哪些基本API\n// Bucket represents a token bucket that fills at a predetermined rate. // Methods on Bucket may be called concurrently. type Bucket struct { clock Clock // startTime holds the moment when the bucket was \t// first created and ticks began. \tstartTime time.Time // capacity holds the overall capacity of the bucket. \tcapacity int64 // quantum holds how many tokens are added on \t// each tick. \tquantum int64 // fillInterval holds the interval between each tick. \tfillInterval time.Duration // mu guards the fields below it. \tmu sync.Mutex // availableTokens holds the number of available \t// tokens as of the associated latestTick. \t// It will be negative when there are consumers \t// waiting for tokens. \tavailableTokens int64 // latestTick holds the latest tick for which \t// we know the number of tokens in the bucket. \tlatestTick int64 }  桶里记录了当前的时钟，以及桶创建的时间 capacit表示桶里总的令牌数量 quantum表示每经过一个tick会放入的令牌数 fillInterval表示一个tick的时间长度 mu用来支持多协程访问 availableTokens记录当前可用的令牌数 latestTick记录上一个tick的时间  // NewBucket returns a new token bucket that fills at the // rate of one token every fillInterval, up to the given // maximum capacity. Both arguments must be // positive. The bucket is initially full. func NewBucket(fillInterval time.Duration, capacity int64) *Bucket { return NewBucketWithClock(fillInterval, capacity, nil) } // NewBucketWithClock is identical to NewBucket but injects a testable clock // interface. func NewBucketWithClock(fillInterval time.Duration, capacity int64, clock Clock) *Bucket { return NewBucketWithQuantumAndClock(fillInterval, capacity, 1, clock) } // rateMargin specifes the allowed variance of actual // rate from specified rate. 1% seems reasonable. const rateMargin = 0.01 // NewBucketWithRate returns a token bucket that fills the bucket // at the rate of rate tokens per second up to the given // maximum capacity. Because of limited clock resolution, // at high rates, the actual rate may be up to 1% different from the // specified rate. func NewBucketWithRate(rate float64, capacity int64) *Bucket { return NewBucketWithRateAndClock(rate, capacity, nil) } // NewBucketWithRateAndClock is identical to NewBucketWithRate but injects a // testable clock interface. func NewBucketWithRateAndClock(rate float64, capacity int64, clock Clock) *Bucket { // Use the same bucket each time through the loop \t// to save allocations. \ttb := NewBucketWithQuantumAndClock(1, capacity, 1, clock) for quantum := int64(1); quantum \u0026lt; 1\u0026lt;\u0026lt;50; quantum = nextQuantum(quantum) { fillInterval := time.Duration(1e9 * float64(quantum) / rate) if fillInterval \u0026lt;= 0 { continue } tb.fillInterval = fillInterval tb.quantum = quantum if diff := math.Abs(tb.Rate() - rate); diff/rate \u0026lt;= rateMargin { return tb } } panic(\u0026#34;cannot find suitable quantum for \u0026#34; + strconv.FormatFloat(rate, \u0026#39;g\u0026#39;, -1, 64)) } // nextQuantum returns the next quantum to try after q. // We grow the quantum exponentially, but slowly, so we // get a good fit in the lower numbers. func nextQuantum(q int64) int64 { q1 := q * 11 / 10 if q1 == q { q1++ } return q1 } // NewBucketWithQuantum is similar to NewBucket, but allows // the specification of the quantum size - quantum tokens // are added every fillInterval. func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket { return NewBucketWithQuantumAndClock(fillInterval, capacity, quantum, nil) } // NewBucketWithQuantumAndClock is like NewBucketWithQuantum, but // also has a clock argument that allows clients to fake the passing // of time. If clock is nil, the system clock will be used. func NewBucketWithQuantumAndClock(fillInterval time.Duration, capacity, quantum int64, clock Clock) *Bucket { if clock == nil { clock = realClock{} } if fillInterval \u0026lt;= 0 { panic(\u0026#34;token bucket fill interval is not \u0026gt; 0\u0026#34;) } if capacity \u0026lt;= 0 { panic(\u0026#34;token bucket capacity is not \u0026gt; 0\u0026#34;) } if quantum \u0026lt;= 0 { panic(\u0026#34;token bucket quantum is not \u0026gt; 0\u0026#34;) } return \u0026amp;Bucket{ clock: clock, startTime: clock.Now(), latestTick: 0, fillInterval: fillInterval, capacity: capacity, quantum: quantum, availableTokens: capacity, } }  基本思路就是创建桶时，设置桶的总容量，并设置每隔多久时间投入多少令牌  先看take方法, 看令牌的获取逻辑\n// take is the internal version of Take - it takes the current time as // an argument to enable easy testing. func (tb *Bucket) take(now time.Time, count int64, maxWait time.Duration) (time.Duration, bool) { if count \u0026lt;= 0 { return 0, true } tick := tb.currentTick(now) tb.adjustavailableTokens(tick) avail := tb.availableTokens - count if avail \u0026gt;= 0 { tb.availableTokens = avail return 0, true } // Round up the missing tokens to the nearest multiple \t// of quantum - the tokens won\u0026#39;t be available until \t// that tick.  // endTick holds the tick when all the requested tokens will \t// become available. \tendTick := tick + (-avail+tb.quantum-1)/tb.quantum endTime := tb.startTime.Add(time.Duration(endTick) * tb.fillInterval) waitTime := endTime.Sub(now) if waitTime \u0026gt; maxWait { return 0, false } tb.availableTokens = avail return waitTime, true } // currentTick returns the current time tick, measured // from tb.startTime. func (tb *Bucket) currentTick(now time.Time) int64 { return int64(now.Sub(tb.startTime) / tb.fillInterval) } // adjustavailableTokens adjusts the current number of tokens // available in the bucket at the given time, which must // be in the future (positive) with respect to tb.latestTick. func (tb *Bucket) adjustavailableTokens(tick int64) { if tb.availableTokens \u0026gt;= tb.capacity { return } tb.availableTokens += (tick - tb.latestTick) * tb.quantum if tb.availableTokens \u0026gt; tb.capacity { tb.availableTokens = tb.capacity } tb.latestTick = tick return }  当获取的令牌数为0或者负数，直接返回 拿到当前的tick，根据startTime和fillInterval可以简单算出来 adjustavailableTokens根据当前的tick，计算出处于这个tick的时候，桶里应该有多少令牌 比如现在处于tick10, lastestTick是tick5，表示距离上个tick已经过去了5个tick了，5 * quantum 得到应该增加多少令牌数了 availableTokens等于原先的令牌数加上上一步增加的令牌数，并需要保证不超过capacity 更新lastestTick为当前tick 如果availableTokens大于请求的令牌数，则返回成功 否则计算需要等多少个tick才能生成请求的令牌数，并返回需要等待的时间   这里设计得挺巧妙的，仅仅通过数学计算就算出了availableTokens，并不需要设置定时器之类的东西\n // Wait takes count tokens from the bucket, waiting until they are // available. func (tb *Bucket) Wait(count int64) { if d := tb.Take(count); d \u0026gt; 0 { tb.clock.Sleep(d) } }  Wait方法传入请求的令牌数量 tb.Take() 会告诉我们需要等待多久时间才能拿到所有的令牌 sleep直接睡那么长的时间  // WaitMaxDuration is like Wait except that it will // only take tokens from the bucket if it needs to wait // for no greater than maxWait. It reports whether // any tokens have been removed from the bucket // If no tokens have been removed, it returns immediately. func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool { d, ok := tb.TakeMaxDuration(count, maxWait) if d \u0026gt; 0 { tb.clock.Sleep(d) } return ok } const infinityDuration time.Duration = 0x7fffffffffffffff // Take takes count tokens from the bucket without blocking. It returns // the time that the caller should wait until the tokens are actually // available. // // Note that if the request is irrevocable - there is no way to return // tokens to the bucket once this method commits us to taking them. func (tb *Bucket) Take(count int64) time.Duration { tb.mu.Lock() defer tb.mu.Unlock() d, _ := tb.take(tb.clock.Now(), count, infinityDuration) return d } // TakeMaxDuration is like Take, except that // it will only take tokens from the bucket if the wait // time for the tokens is no greater than maxWait. // // If it would take longer than maxWait for the tokens // to become available, it does nothing and reports false, // otherwise it returns the time that the caller should // wait until the tokens are actually available, and reports // true. func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool) { tb.mu.Lock() defer tb.mu.Unlock() return tb.take(tb.clock.Now(), count, maxWait) } // TakeAvailable takes up to count immediately available tokens from the // bucket. It returns the number of tokens removed, or zero if there are // no available tokens. It does not block. func (tb *Bucket) TakeAvailable(count int64) int64 { tb.mu.Lock() defer tb.mu.Unlock() return tb.takeAvailable(tb.clock.Now(), count) } // takeAvailable is the internal version of TakeAvailable - it takes the // current time as an argument to enable easy testing. func (tb *Bucket) takeAvailable(now time.Time, count int64) int64 { if count \u0026lt;= 0 { return 0 } tb.adjustavailableTokens(tb.currentTick(now)) if tb.availableTokens \u0026lt;= 0 { return 0 } if count \u0026gt; tb.availableTokens { count = tb.availableTokens } tb.availableTokens -= count return count } // Available returns the number of available tokens. It will be negative // when there are consumers waiting for tokens. Note that if this // returns greater than zero, it does not guarantee that calls that take // tokens from the buffer will succeed, as the number of available // tokens could have changed in the meantime. This method is intended // primarily for metrics reporting and debugging. func (tb *Bucket) Available() int64 { return tb.available(tb.clock.Now()) } // available is the internal version of available - it takes the current time as // an argument to enable easy testing. func (tb *Bucket) available(now time.Time) int64 { tb.mu.Lock() defer tb.mu.Unlock() tb.adjustavailableTokens(tb.currentTick(now)) return tb.availableTokens } // Capacity returns the capacity that the bucket was created with. func (tb *Bucket) Capacity() int64 { return tb.capacity } // Rate returns the fill rate of the bucket, in tokens per second. func (tb *Bucket) Rate() float64 { return 1e9 * float64(tb.quantum) / float64(tb.fillInterval) } // Clock represents the passage of time in a way that // can be faked out for tests. type Clock interface { // Now returns the current time. \tNow() time.Time // Sleep sleeps for at least the given duration. \tSleep(d time.Duration) } // realClock implements Clock in terms of standard time functions. type realClock struct{} // Now implements Clock.Now by calling time.Now. func (realClock) Now() time.Time { return time.Now() } // Now implements Clock.Sleep by calling time.Sleep. func (realClock) Sleep(d time.Duration) { time.Sleep(d) } 其余的方法很简单\n","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E5%AE%9E%E7%8E%B0%E9%99%90%E6%B5%81%E5%99%A8/doc/","summary":"麻了呀，面试问到限流器相关的知识答不出来","title":"Golang-实现限流器"},{"content":" 以下图片截自 http://thesecretlivesofdata.com/raft/\n 缘起 当我们服务端只有一个节点的时候，任何数据的改动逻辑都是简单的，当客户端给服务器发消息，只要服务器接收并写下了这条消息，那么该消息就算发送成功\n那么当我们服务端有多个节点时，当一条来自客户端的消息到来，应该怎么处理让多个节点达成数据一致呢\n此时，raft就登场了\nraft工作原理 一个节点可以处于以下三种状态：\n Follower跟随者   Candidate候选者   Leader领导者状态  领导者选举 常规流程 最初时，所有节点都以follower的状态开始，每一个节点都有一个随机的选举超时时间（随机的目的是为了让节点之间的选举速度有快有慢，一般在150-300毫秒之间），节点的超时时间一旦走完了，就会成为candidate\n如图，每个节点的时钟转速不一样，先转完的人成为candidate\n如图，C最快跑完了超时时间，它就转化为candidate\n并给自己投一票，同时告诉另外两个节点说：我选举我自己为leader\n此时因为AB并没有给其他人投票，自己超时时间也没到，所以就会把票投给了C，并重置自己的超时时间\nC获得了大多数的选票，所以它成为了Leader\nleader会定期地向follower发送心跳，保证自己的领导地位\nfollower也必须回复leader的心跳包，证明自己是活跃的\n以及循环往复\n假设这个时候，leader C挂了\n此时AB的超时时间因为不会被重置了，那么先走完超时时间的B成为了leader\n注意Term任期增加1\n分裂投票 以上情况是理想情况，实际上大多数选举时，几个节点之间可能存在平票的情况，遇到平票时需要再进一轮选举\nAB节点，速度一样，同时给自己投了票\nC投给了A，D投给了B\n此时AB平票，进入新的一轮\n日志复制 当选出一位leader后，leader负责将系统的所有变更复制到所有的节点，随着心跳包一起发出\n如图，客户端向leader发送了一个set请求\nleader将请求复制到follower上\n当多数节点成功响应了之后，leader才将set请求缺人，并响应客户端\n网络分区时怎么办 当网络产生分区时，如图\n分裂产生了两个leader，对于leader C，由于它能得到三票（来自CDE），超过一半的节点，所以C的改动是生效的。而leader B，它只能得到两票（AB），不满足大多数原则，所以它的改动无法生效\n当分区恢复时，AB发现C的Term比自己的高，AB都将回滚其未提交的条目，并匹配新领导者的日志\n","permalink":"https://lambertxiao.github.io/posts/raft-%E7%AE%80%E5%8D%95%E5%85%A5%E9%97%A8/raft/","summary":"分布式一致性协议的当红辣子鸡","title":"Raft-简单入门"},{"content":"Talk is cheap, show you the code\npackage model import ( \u0026#34;censor-task-manager/pkg/utils\u0026#34; \u0026#34;context\u0026#34; log \u0026#34;github.com/sirupsen/logrus\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/go-redis/redis/v8\u0026#34; ) const SYNC_INTERVAL = 20 * time.Second const LOCK_EXPIRE_DURATION = 30 * time.Second type DistributionNode struct { isMaster bool nodeId string redis *redis.Client ctx context.Context lockKey string // 同步间隔要小于锁的过期时间 \tsyncInterval time.Duration lockExpireDuration time.Duration } func NewDistributionNode(redisHost, redisPassword, lockKey string) *DistributionNode { ctx := context.Background() client := redis.NewClient(\u0026amp;redis.Options{Addr: redisHost, Password: redisPassword}) n := \u0026amp;DistributionNode{ nodeId: utils.Uuid(), redis: client, ctx: ctx, lockKey: lockKey, syncInterval: SYNC_INTERVAL, lockExpireDuration: LOCK_EXPIRE_DURATION, } return n } func (n *DistributionNode) Start() { ticker := time.NewTicker(n.syncInterval) for { locked, err := n.Lock() if err != nil { log.Error(err) time.Sleep(time.Second) continue } if locked { n.isMaster = true } else { master, err := n.getCurrMaster() if err != nil { log.Error(err) return } if master == n.nodeId { n.isMaster = true } } \u0026lt;-ticker.C } } func (n *DistributionNode) Lock() (bool, error) { r, err := n.redis.SetNX(n.ctx, n.lockKey, n.nodeId, n.lockExpireDuration).Result() if err != nil { return false, err } return r, nil } func (n *DistributionNode) Unlock() (bool, error) { lua := ` if redis.call(\u0026#39;GET\u0026#39;, KEYS[1]) == ARGV[1] then return redis.call(\u0026#39;DEL\u0026#39;, KEYS[1]) else return 0 end ` val, err := n.redis.Eval(n.ctx, lua, []string{n.lockKey}, []string{n.nodeId}).Result() if err != nil { return false, err } return val == int64(1), nil } func (n *DistributionNode) IsMaster() bool { return n.isMaster } func (n *DistributionNode) getCurrMaster() (string, error) { return n.redis.Get(n.ctx, n.lockKey).Result() } ","permalink":"https://lambertxiao.github.io/posts/redis-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/redis/","summary":"只要是分布式应用，免不了要使用分布式锁","title":"Redis-实现分布式锁"},{"content":"B+树 为什么需要B+树 比B树具有更好的范围查询能力\n一个m阶的B+树具有如下几个特征：  有k个子树的中间节点包含有k个元素（B树中是k-1个元素），每个元素不保存数据，只用来索引(意味着可以存更多的索引)，所有数据都保存在叶子节点。 所有的叶子结点中包含了全部元素的信息，及指向含这些元素记录的指针，且叶子结点本身依关键字的大小自小而大顺序链接。 所有的中间节点元素都同时存在于子节点，在子节点元素中是最大（或最小）元素。 根节点的最大元素等同于整棵树的最大元素  B+树的优势：\n 单一节点存储更多的元素，使得查询的IO次数更少。 所有查询都要查找到叶子节点，查询性能稳定。 所有叶子节点形成有序链表，便于范围查询。  ","permalink":"https://lambertxiao.github.io/posts/data-struct/b+%E6%A0%91/","summary":"用过mysql的都知道我","title":"B+树"},{"content":"BIOS ","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/bios/","summary":"当年重装系统的时候，没少听过BIOS吧？","title":"BIOS是什么"},{"content":"B树 B树属于多叉树又名平衡多路查找树\n 排序方式：所有节点关键字是按递增次序排列，并遵循左小右大原则 子节点数：非叶子节点的子节点数\u0026gt;1，且\u0026lt;=M ，且M\u0026gt;=2，空树除外（注：M阶代表一个树节点最多有多少个查找路径) 关键字数：枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子 中序遍历，可以获得所有节点  B树示意图： 查询流程 如上图我要从上图中找到E字母，查找流程如下\n  获取根节点的关键字进行比较，当前根节点关键字为M，E \u0026lt; M（26个字母顺序），所以往找到指向左边的子节点（二分法规则，左小右大，左边放小于当前节点值的子节点、右边放大于当前节点值的子节点\n  拿到关键字D和G，D \u0026lt; E \u0026lt; G 所以直接找到D和G中间的节点；\n  拿到E和F，因为E=E 所以直接返回关键字和指针信息（如果树结构里面没有包含所要查找的节点则返回null）；\n  插入流程 定义一个5阶树（平衡5路查找树;），现在我们要把3、8、31、11、23、29、50、28 这些数字构建出一个5阶树出来;\n遵循规则：\n  节点拆分规则：当前是要组成一个5路查找树，那么此时m=5,关键字数必须\u0026lt;=5-1（这里关键字数\u0026gt;4就要进行节点拆分）；\n  排序规则：满足节点本身比左边节点大，比右边节点小的排序规则;\n  先插入 3、8、31、11\n再插入23、29\n再插入50、28\n删除流程  节点合并规则：当前是要组成一个5路查找树，那么此时m=5,关键字数必须大于等于ceil（5/2）（这里关键字数\u0026lt;2就要进行节点合并）； 满足节点本身比左边节点大，比右边节点小的排序规则; 关键字数小于二时先从子节点取，子节点没有符合条件时就向向父节点取，取中间值往父节点放；  特点：\nB树相对于平衡二叉树的不同是，每个节点包含的关键字增多了，特别是在B树应用到数据库中的时候，数据库充分利用了磁盘块的原理（磁盘数据存储是采用块的形式存储的，每个块的大小为4K，每次IO进行数据读取时，同一个磁盘块的数据可以一次性读取出来）把节点大小限制和充分使用在磁盘快大小范围；把树的节点关键字增多后树的层级比原来的二叉树少了，减少数据查找的次数和复杂度;\n增大了关键字（一个节点能承载的数据变多了），从而降低层级\n","permalink":"https://lambertxiao.github.io/posts/data-struct/b%E6%A0%91/","summary":"平衡多路查找树","title":"B树"},{"content":"define的用法 define是C语言中提供的宏定义命令，其主要目的是在编程时提供一定的方便。\n简单的宏定义  define \u0026lt;宏名\u0026gt; \u0026lt;字符串\u0026gt;\n #define PI 3.14 带参数的宏定义  #define \u0026lt;宏名\u0026gt; (\u0026lt;参数表\u0026gt;) \u0026lt;宏体\u0026gt;\n #define INC(a) ((a) = ((a)+1)) 宏替换的时机 在程序中出现的是宏名，在该程序被编译前，先将宏名用被定义的字符串替换，这称为宏替换，替换后才进行编译，宏替换是简单的替换。 宏替换由预处理器完成。预处理器将源程序文件中出现的对宏的引用展开成相应的宏定义，经过预处理器处理的源程序与之前的源程序有所有不同，在这个阶段所进行的工作只是纯粹的替换与展开，没有任何计算功能。\n","permalink":"https://lambertxiao.github.io/posts/c-%E5%95%A5%E6%98%AF%E5%AE%8F%E5%91%80/define%E5%AE%8F/","summary":"宏在C中真的是无所不在了吧","title":"C-啥是宏"},{"content":"extern int x; // 这叫声明并定义x int y = 10; // 这叫声明并定义y extern int x; // 声明而非定义 extern int y = 10; // 会报错 extern它可以应用于一个全局变量，函数或模板声明，说明该符号具有外部链接(external linkage)属性，说白了就是这个符号在这里被声明了，但是会在别处被定义\n指针问题 判断一个指针是否是合法的指针没有高效的方法，这是C/C++指针问题的根源\n","permalink":"https://lambertxiao.github.io/posts/c-%E5%B0%8F%E7%9F%A5%E8%AF%86/doc/","summary":"为了在存储的路上深耕，C/C++知识储备不能少","title":"C-小知识"},{"content":"Dns解析   NS记录\n用来指定域名由哪个 DNS 服务器进行解析\n  A记录\n用来指定主机名对应的 IPv4 地址\n  AAAA记录\n用来指定主机名对应的 IPv6 地址\n  CNAME\n用来定义域名的别名，方便实现将多个域名解析到同一个IP地址\n  PTR记录\n常用于反向地址解析，将 IP 地址解析到对应的名称\n  SOA记录\nSOA记录用于在多台NS记录中哪一台是主DNS服务器\n  ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/dns%E8%A7%A3%E6%9E%90/","summary":"NS记录、A记录都是些啥","title":"DNS记录都有哪些？"},{"content":"Docker ","permalink":"https://lambertxiao.github.io/posts/docker-%E5%8E%9F%E7%90%86/docker/","summary":"Namespace, CGroup, UnionFS的整合怪","title":"Docker原理"},{"content":"Channel的底层结构 channel在runtime里的结构 chan 在go源码的对应runtime包底下的hchan结构\ntype hchan struct { qcount uint // 环形缓冲区里总共有多少元素 \tdataqsiz uint // 环形缓冲区的大小 \tbuf unsafe.Pointer // 指向缓冲区元素的指针 \telemsize uint16 // 缓冲区内元素的大小 \tclosed uint32 // channel是否关闭 \telemtype *_type // 缓冲区里元素的实际类型 \tsendx uint // 发送指针下标 \trecvx uint // 接收指针下标 \trecvq waitq // 接收协程队列 \tsendq waitq // 发送协程队列 \tlock mutex // 处理多协程访问的锁 } type waitq struct { first *sudog last *sudog } type sudog struct { g *g // 哪个协程在等待 \tnext *sudog prev *sudog elem unsafe.Pointer // 等待要发送的数据在哪  // The following fields are never accessed concurrently. \t// For channels, waitlink is only accessed by g. \t// For semaphores, all fields (including the ones above) \t// are only accessed when holding a semaRoot lock.  acquiretime int64 releasetime int64 ticket uint32 // isSelect indicates g is participating in a select, so \t// g.selectDone must be CAS\u0026#39;d to win the wake-up race. \tisSelect bool // success indicates whether communication over channel c \t// succeeded. It is true if the goroutine was awoken because a \t// value was delivered over channel c, and false if awoken \t// because c was closed. \tsuccess bool parent *sudog // semaRoot binary tree \twaitlink *sudog // g.waiting list or semaRoot \twaittail *sudog // semaRoot \tc *hchan // 等待哪个chaneel } 当执行select时，runtime做了什么 当执行以下写法时\nselect { case \u0026lt;-ch: ... default: ... } select实际上会被编译器转化为runtime.selectgo的调用\nfunc selectgo( cas0 *byte, order0 *byte, pc0 *uintptr, nsends int, nrecvs int, block bool, ) (int, bool) 参数依次如下：\n  cas0 指向一个数组，数组里装的是所有select所有case的分支，顺序是send在前，recv在后\n  order0 实际上也是一个数组，数组大小是第一个参数的两倍，实际被用作两个数组，第一个数组用来对所有的channel的轮询进行乱序, 第二个数组用来对所有channel的加锁操作进行排序；轮询需要乱序才能保障公平性，确定加锁顺序是防止死锁\n  pc0 和race检测相关\n  nsends 执行send的分支有多少个，nrecvs 执行接收的分支有多少个\n  block 是否会阻塞，有default的分支的不会被阻塞，没有的会被阻塞\n  返回值如下：\n  第一个int表示哪个case被最终执行来，进入default则是-1\n  第二个参数表示是实际接收到了这个值，还是因为channel关闭而得到了零值\n   channel的常见操作及对应的runtime调用 ch := make(chan byte, 1) func makechan(chanType *byte, size int) (hchan chan any) \u0026lt;-ch func chanrecv1(hchan \u0026lt;-chan any, elem *any) func chanrecv2(hchan \u0026lt;-chan any, elem *any) bool ch \u0026lt;- \u0026#39;x\u0026#39; func chansend1(hchan chan\u0026lt;- any, elem *any) close(ch) func closechan(hchan any) 参考 ","permalink":"https://lambertxiao.github.io/posts/golang/golang-channel%E7%9A%84%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84/channel%E7%9A%84%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84/","summary":"一把锁+一个环形数组+两个等待队列","title":"Golang-Channel的底层结构"},{"content":"Golang小知识 context是干嘛的 context用于停止goroutine，协调多个goroutine的取消，设置超时取消等等。 基于channel和select来实现停止，另外还可以用context在不同的goroutine中传递数据。其中停止goroutine是context的核心\nnew和make的区别？  new是用来创建一个某个类型对象的，并返回指向这个类型的零值的指针。 make是用来创建slice，map，chanel的，并返回引用   指针变量存储的是另一个变量的地址, 引用变量指向另外一个变量。\n 数组与切片  数组定长，不可改变，值传递 切片变长，可改变，地址传递  channel特性  给一个 nil channel 发送数据，造成永远阻塞 从一个 nil channel 接收数据，造成永远阻塞 给一个已经关闭的 channel 发送数据，引起 panic 从一个已经关闭的 channel 接收数据，如果缓冲区中为空，则返回一个零值 无缓冲的channel是同步的，而有缓冲的channel是非同步的  进程、线程、协程之间的区别 进程是资源的分配和调度的一个独立单元，而线程是CPU调度的基本单元\nmap的底层结构 bucket桶，链表，哈希函数，key，装载因子，扩容，用链表法解决哈希冲突； 当向桶中添加了很多 key，造成元素过多，或者溢出桶太多，就会触发扩容。扩容分为等量扩容和 2 倍容量扩容。扩容后，原来一个 bucket 中的 key 一分为二，会被重新分配到两个桶中。扩容过程是渐进的，主要是防止一次扩容需要搬迁的 key 数量过多，引发性能问题\nslice的底层结构 type Slice struct { ptr unsafe.Pointer len int cap int } 当插入时，如果len不大于cap，不会产生新的slice；如果len大于cap，则会创建一个新的slice，并将老的slice元素copy过去\ninterface底层结构 反射机制 ","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E5%B0%8F%E7%9F%A5%E8%AF%86/golang%E5%B0%8F%E7%9F%A5%E8%AF%86/","summary":"面试前总得背一背吧","title":"Golang-小知识"},{"content":"Golang-线程模型 MPG模型  M：Machine代表一个内核线程 P：Processor代表M所需的上下文环境 G：代表了一段需要被并发执行的Go语言代码的封装  为什么是MPG 在Go1.1版本之前，用的其实就是MG模型，Goroutine在一个全局的队列里，每个M想拿到G时，都要先拿到全局的大锁，效率低下\nMPG模型概览 如图所示，\n  全局队列: 存放等待运行的G\n  P的本地队列：同全局队列类似，存放的也是等待运行的G，但存的数量有限，不超过256个。但某个P中的goroutine由创建了其他goroutine时，优先加入所在P的本地队列里，如果队列满里则加入全局队列里\n  P列表：所有的P都在程序启动时创建，并保存在数组中，最多由GOMAXPROCS个G\n  M：就是一个内核线程，线程想要运行任务就得先找P，从P的本地列表获取G，P队列为空时，M也会尝试从全局队列拿一批G放到P的本地列表， 或者从其他的P的本地队列里偷一半放在自己P的本地队列，M运行G，G执行之后，M会从P从获取下一个G，不断重复下去\n   P与M没有绝对关系，一个M阻塞，P就会去创建或切换另一个M\n 调度器的设计策略  复用线程：避免频繁地创建，销毁线程，而是对线程的复用   当本线程无可用G时，尝试从其他线程绑定的P中偷取G，而不是销毁线程\n hand off机制   当本线程因为G进行系统调用阻塞时，线程释放绑定的P，把P转移给其他空闲的线程个执行\n 抢占   一个goroutine最多占用CPU 10ms，防止其他goroutine被饿死\n 调度器关键人物  M0   m0时启动程序后的主线程，m0负责执行初始化操作和启动第一个G，在之后m0就和其他的M一样了\n  G0   G0是每次创建一个M都会第一个创建的goroutine，G0仅用于G，G0本身不指向任何可执行的函数，每个M都会由自己的一个G0\n 调度场景   P1拥用G1，M1获取P后开始运行G1，G1使用go func()创建了G2，为了局部性G2优先加入P1的本地队列\n  G1运行完后，M1上的运行goroutine切换为G0，G0负责调用schedule切换routine，从P1的本地列表获取G2，从G0切换到G2，并开始运行，实现了线程M1的复用\n  假设每个P的本地队列只能存放3个G，G2此时创建了6个，前3个G已经加入P1的本地列表，P1本地队列满了，剩下的G转移到全局队列\n  在创建G时，运行的G会尝试唤醒其他空闲的P和M去组合\n  假如被唤醒的M2绑定了一个P2，但该P本地队列里并没有G，则M会自旋，不断寻找G\n   自旋本质是在运行，线程在运行却没有执行 G，就变成了浪费 CPU. 为什么不销毁现场，来节约 CPU 资源。因为创建和销毁 CPU 也会浪费时间，我们希望当有新 goroutine 创建时，立刻能有 M 运行它\n  M2尝试从全局队列取一批G到P2的本地队列，但每次不取太多，给其他P留点，这是全局队列到P本地队列的负载均衡\n  假设全局队列里没有G了，则M2就要去偷了，去其他由的P上偷一半的G过来\n  什么是KSE  KSE是内核调度实体\n 线程模型其实就是语言所控制的线程与KSE的对应关系，MPG模型中一个M与一个KSE一一对应，一个P可以与多个M相关联，一个M可以管理多个P，一个P上包含一个可运行的G的队列\nProcessor的状态流转 stateDiagram [*] --\u0026gt; gcstop: 创建 gcstop --\u0026gt; idle: 完成可运行G列表的初始化 idle --\u0026gt; running: 与某个M建立关联 running --\u0026gt; idle: 与某个M断开关联 idle --\u0026gt; gcstop: 开始垃圾回收 running --\u0026gt; gcstop: 开始垃圾回收 running --\u0026gt; syscall: 进入系统调用 syscall --\u0026gt; running: 退出系统调用 idle --\u0026gt; dead: 丢弃 running --\u0026gt; dead: 丢弃 syscall --\u0026gt; dead: 丢弃 syscall --\u0026gt; gcstop: 开始垃圾回收 dead --\u0026gt; [*] Goroutine的状态流转 stateDiagram [*] --\u0026gt; idle: 创建 idle --\u0026gt; runnable: 初始化完成 runnable --\u0026gt; running: 开始运行 running --\u0026gt; dead: 运行完成 running --\u0026gt; syscall: 进入系统调用 syscall --\u0026gt; running: 可以直接运行 syscall --\u0026gt; runnable: 不可以直接运行 running --\u0026gt; waiting: 等待事件 waiting --\u0026gt; runnable: 事件到来 dead --\u0026gt; runnable: 重新初始化 dead --\u0026gt; [*]: 程序结束 ","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/golang%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B/","summary":"M(内核线程)、P(处理器)、G(协程)","title":"Golang-线程模型"},{"content":"Golang垃圾回收  所谓垃圾回收，即释放我们不再使用对象的内存\n 三色标记法 要找出存活对象，根据可达性分析，从GC Roots开始进行遍历访问，可达的则为存活对象。 我们把遍历对象图过程中遇到的对象，按“是否访问过”这个条件标记成以下三种颜色：\n 白色：尚未访问过。 黑色：本对象已访问过，而且本对象 引用到 的其他对象 也全部访问过了。 灰色：本对象已访问过，但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后，会转换为黑色  一张图说明：\n假设现在有白、灰、黑三个集合（表示当前对象的颜色），其遍历访问过程为：\n  初始时，所有对象都在 【白色集合】中；\n  将GC Roots 直接引用到的对象 挪到 【灰色集合】中；\n  从灰色集合中获取对象：\n  将本对象 引用到的 其他对象 全部挪到 【灰色集合】中；\n  将本对象 挪到 【黑色集合】里面。\n    重复步骤3，直至【灰色集合】为空时结束。\n  结束后，仍在【白色集合】的对象即为GC Roots 不可达，可以进行回收。\n   当Stop The World 时，如果对象间的引用是不会发生变化的，可以轻松完成标记。而当需要支持并发标记时，即标记期间应用线程还在继续跑，对象间的引用可能发生变化，多标和漏标的情况就有可能发生。\n 问题1: 多标-浮动垃圾 如下图：\n此刻之后，对象E/F/G是“应该”被回收的。然而因为E已经变为灰色了，其仍会被当作存活对象继续遍历下去。最终的结果是：这部分对象仍会被标记为存活，即本轮GC不会回收这部分内存。\n这部分本应该回收 但是 没有回收到的内存，被称之为“浮动垃圾”。浮动垃圾并不会影响应用程序的正确性，只是需要等到下一轮垃圾回收中才被清除。\n问题2: 漏标-读写屏障 假设GC线程已经遍历到E（变为灰色了），此时应用线程先执行了：\nvar G = objE.fieldG; objE.fieldG = null; // 灰色E 断开引用 白色G objD.fieldG = G; // 黑色D 引用 白色G 此时切回GC线程继续跑，因为E已经没有对G的引用了，所以不会将G放到灰色集合；尽管因为D重新引用了G，但因为D已经是黑色了，不会再重新做遍历处理。 最终导致的结果是：G会一直停留在白色集合中，最后被当作垃圾进行清除。这直接影响到了应用程序的正确性，是不可接受的。\n漏标发生条件：\n 灰色对象 断开了 白色对象的引用（直接或间接的引用）；即灰色对象 原来成员变量的引用 发生了变化。 黑色对象 重新引用了 该白色对象；即黑色对象 成员变量增加了 新的引用  从代码的角度看：\nvar G = objE.fieldG; // 1.读 objE.fieldG = null; // 2.写 objD.fieldG = G; // 3.写 读取 对象E的成员变量fieldG的引用值，即对象G； 对象E 往其成员变量fieldG，写入 null值。 对象D 往其成员变量fieldG，写入 对象G ； 我们只要在上面这三步中的任意一步中做一些“手脚”，将对象G记录起来，然后作为灰色对象再进行遍历即可。比如放到一个特定的集合，等初始的GC Roots遍历完（并发标记），该集合的对象 遍历即可（重新标记）\n写屏障 给某个对象的成员变量赋值时，其底层代码大概长这样：\nvoid oop_field_store(oop* field, oop new_value) { pre_write_barrier(field); // 写屏障-写前操作 *field = new_value; post_write_barrier(field, value); // 写屏障-写后操作 }  写屏障 + SATB  当对象E的成员变量的引用发生变化时（objE.fieldG = null;），我们可以利用写屏障，将E原来成员变量的引用对象G记录下来\nvoid pre_write_barrier(oop* field) { if($gc_phase == GC_CONCURRENT_MARK \u0026amp;\u0026amp; !isMarkd(field)) { oop old_value = *field; // 获取旧值 remark_set.add(old_value); // 记录 原来的引用对象 } } 这种做法的思路是：尝试保留开始时的对象图，即原始快照（Snapshot At The Beginning，SATB），当某个时刻 的GC Roots确定后，当时的对象图就已经确定了。 比如 当时 D是引用着G的，那后续的标记也应该是按照这个时刻的对象图走（D引用着G）。如果期间发生变化，则可以记录起来，保证标记依然按照原本的视图来。\n值得一提的是，扫描所有GC Roots 这个操作（即初始标记）通常是需要STW的，否则有可能永远都扫不完，因为并发期间可能增加新的GC Roots。\n SATB破坏了条件一：【灰色对象 断开了 白色对象的引用】，从而保证了不会漏标。\n 写屏障 + 增量更新  当对象D的成员变量的引用发生变化时（objD.fieldG = G;），我们可以利用写屏障，将D新的成员变量引用对象G记录下来\nvoid post_write_barrier(oop* field, oop new_value) { if($gc_phase == GC_CONCURRENT_MARK \u0026amp;\u0026amp; !isMarkd(field)) { remark_set.add(new_value); // 记录新引用的对象 } } 这种做法的思路是：不要求保留原始快照，而是针对新增的引用，将其记录下来等待遍历（重新丢到灰色区），即增量更新\n 增量更新破坏了条件二：【黑色对象 重新引用了 该白色对象】，从而保证了不会漏标。\n 读屏障  读屏障是直接针对第一步：var G = objE.fieldG;，当读取成员变量时，一律记录下来：\nif($gc_phase == GC_CONCURRENT_MARK \u0026amp;\u0026amp; !isMarkd(field)) { oop old_value = *field; remark_set.add(old_value); // 记录读取到的对象 } 这种做法是保守的，但也是安全的。因为条件二中【黑色对象 重新引用了 该白色对象】，重新引用的前提是：得获取到该白色对象，此时已经读屏障就发挥作用了。\n其他的垃圾回收策略 标记-清除法 思想大致如下：\n 先从根对象进行扫描，当我们的根对象指向了某个堆上的对象，我们就认为这个对象是可达的 可达对象指向的对象也是可达的 从根对象开始遍历（广度遍历或深度遍历）  步骤：\n  有触发垃圾回收的事件发生，一般是当申请堆内存的时候，做一个检测机制，或者定时回收\n  STW（Stop The World），挂起整个程序，等待GC\n  从根对象开始扫描，在堆上给每个可达的对象的header做一个活跃标记\n  清除阶段，扫描整个堆，发现是活跃对象的话，则清除掉它的标志位即可。如果发现是非活跃对象即不可达对象，则把对象作为分小块，连接到被称为 空闲链表 的单向链表。在之后进行分配时只要遍历这个空闲链表，就可以找到分块了。\n  清除完成，继续程序\n  优缺点：\n 思想简单，很好实现 缺点是会容易产生要多的碎片，分配的时候速度较低，分配需要遍历空闲链表，最坏的情况是遍历到最后也没有找到合适的 STW时间过长  引用计数法 思路：\n  给每一个对象都分配一个计数器，代表有多少其他的对象引用我\n  当没有对象再引用我的时候，就代表我是垃圾\n  步骤：\n  当对象A刚被创建的时候，肯定有一个对象B指向自己，所以对象A的计数器为1\n  当我们要更新一个指针的时候，原来引用对象的计数器-1，新引用对象的计数器+1\n  当遇到某个计数器为0的对象A时，我们要把所有引用了A的对象的计数器-1，同样的道理，如果引用了A的对象-1后可能也为0，需要递归地更新计数器\n  优缺点：\n 优点：不需要STW，回收过程分布在应用程序的运行中；垃圾会被立即回收 缺点：需要开发人员手动回收，容易内存泄露；无法回收循环引用的对象；递归更新计数器可能会非常深  分代收集 思路：\n把堆分配的对象，给他分代，这种算法基于这样的认知，大部分对象创建后很快就变成垃圾，其余的就是很长时间才会变成垃圾，那么我们没必要对这种长时间才变成垃圾的对象进行GC，浪费时间\n  该算法把堆空间划分为四个部分，分别是生成空间，幸存空间1，幸存空间2，老年代空间。并且我们把前三者合并成为新生代空间。\n  当对象刚创建的时候，分配的空间就是在生成空间。\n  当生成空间满的时候，我们就对新生代空间进行GC，这里是是对整个新生代空间进行GC，采用的GC的算法就是节点复制。我们看图说话\n  我们每次GC的时候，都会对对象的“年龄”加1，当判断对象的年龄到达一定阈值的时候，就把对象移动到老年代空间\n  当我们对新生代对象进行GC的时候，是从新生代的根节点开始扫描，但是注意一有可能我们的老年代的对象也会指向新生代，所以如果我们把这点漏掉了，会多清除一些活跃的对象。为了解决这个问题，我们需要把老年代的对象扫描一遍，但是想想如果这样做的话我们岂不是每次要GC新生代对象的时候，都要把新、老都扫描了？\n  用一个记录集来记录那些老年代的对象指向新生代的情况。这样的话，当我们的GC新生代的时候，从根对象与记录集中就行，那么这个记录怎么做到呢，采用的写入屏障（write barrier）的方法\n  那么我们什么时候GC老年代对象的呢？当我们发现新生代的对象的年龄到了之后，要晋升为老年代对象的时候，会先检查老年代空间是否满了，满的话我们就开始老年代GC，老年代对象GC采用的就是标记清除的方法，注意这里应该是把整个堆都进行了GC\n  ","permalink":"https://lambertxiao.github.io/posts/golang/golang-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/golang%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/","summary":"三色标记法","title":"Golang垃圾回收"},{"content":"Http2简介 ","permalink":"https://lambertxiao.github.io/posts/http2-%E7%AE%80%E4%BB%8B/http2/","summary":"二进制协议、多路复用、流量控制、首部压缩","title":"Http2简介"},{"content":"Https协议 https本质上是在http的基础上加上来安全传输层（ssl或tls），用来保证数据传输的安全性\nhttps交互流程图 sequenceDiagram participant 客户端 participant 服务端 客户端 - 服务端: 请求https://www.google.com 服务端 - 客户端: 响应请求，携带者数据证书（证书包含公钥a） 客户端 -- 客户端: 验证证书的有效性 客户端 - 客户端: 取出公钥a，并生成ramdom-key，作为接下来对称加密的密钥 客户端 - 客户端: 使用公钥a加密ramdom-key得到encrypt-key 客户端 - 服务端: 把encrypt-key发送给服务器 服务端 - 服务端: 使用私钥b解密encrypt-key，获得ramdom-key 服务端 - 客户端: 使用ramdom-key对数据进行对称加密并传输给客户端 客户端 - 服务端: 使用对称加密传递加密后的数据  上图中，涉及对称加密和非对称加密，可以看出https传输主要分成两部分，一是证书的验证，二是加密数据的传输\n安全传输层的交互流程 sequenceDiagram participant 客户端 participant 服务端 客户端 - 服务端: 客户端发送“client hello”消息，包含支持的加密方式，tls版本和随机数a 服务端 - 客户端: 服务端响应“server hello”消息，包含选择的密码组合和数字证书以及随机数b 客户端 -- 客户端: 客户端获取数字证书，从证书中拿出公钥，生成一个随机数c，并用公钥对其加密 客户端 - 服务端: 客户端发送加密后的c给服务器 服务端 - 服务端: 服务器使用私钥解密获得c 客户端 -- 服务端: 客户端和服务端使用约定的算法，并使用随机数a，随机数b，随机数c生成相同的密钥key，用于后面的对称加密 客户端 - 服务端: 客户端发送finished消息 服务端 - 客户端: 服务端发送finished消息 客户端 -- 服务端: 成功建立安全链接，可进行加密通信  ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/http/","summary":"HTTP + TLS = HTTPS","title":"HTTPS是怎么建立的？"},{"content":"Kafka ","permalink":"https://lambertxiao.github.io/posts/kafka-%E7%9F%A5%E8%AF%86%E6%80%BB%E8%A7%88/kafka/","summary":"顺序写、复制状态机、分区、多副本、时间轮","title":"kafka-知识总览"},{"content":"Linux系统上的网络抓包 涉及工具   tcpdump\ntcpdump可监听某个网卡的数据包, 并可指定过滤条件查看满足条件的数据包\n  tshark\n命令行分析抓包文件, 一般用于当抓到的数据包文件过于庞大不易于拉回本地环境分析时, 可在服务器上简单分析并生成分析视图\n  editcap\n可用于按条件拆分抓包文件, 一般用于将一个庞大的数据包拆分成多个小文件, 加快分析速度\n  wireshark\n图形化分析抓包文件\n  基本命令 tcpdump tcpdump -i eth0 port 443 抓取eth0网卡上, 使用443端口通信的网络包\ntcpdump -i eth0 port 443 -w net.pcap 抓包的同时, 写入文件, pcap是抓包文件的常用格式, 可被wireshark识别\ntcpdump -r net.pcap 以文本的格式打开抓包文件, 但由于网络数据包的量特别大,基本上我们不会查看原始抓包文件, 而是利用工具分析抓包文件,生成报告\neditcap editcap net.pcap output.pcap -i 60 将抓包文件按一分钟拆分多个小文件, 小文件会在当前目录下生成, output.pcap 指定小文件的输出名\neditcap net.pcap output.pcap -c 1000 将抓包文件按每1000个包的大小拆分\ntshark tshark -n -q -r net.pcap -z \u0026quot;io,stat,0,tcp.flags.syn == 1 \u0026amp;\u0026amp; tcp.flags.ack == 0,tcp.flags.reset == 1 \u0026amp;\u0026amp; tcp.flags.ack == 0,tcp.analysis.retransmission,tcp.analysis.lost_segment,tcp.analysis.out_of_order\u0026quot; 指定一个抓包文件，输出统计信息, -z 指定输出的列， 输出结果大致如下, 会按列输出指定的过滤条件的包的数量以及大小：\n===================================================================================================================================== | IO Statistics | | | | Interval size: 2226.2 secs (dur) | | Col 1: Frames and bytes | | 2: tcp.flags.syn == 1 \u0026amp;\u0026amp; tcp.flags.ack == 0 | | 3: tcp.flags.reset == 1 \u0026amp;\u0026amp; tcp.flags.ack == 0 | | 4: tcp.analysis.retransmission | | 5: tcp.analysis.lost_segment | | 6: tcp.analysis.out_of_order | |-----------------------------------------------------------------------------------------------------------------------------------| | |1 |2 |3 |4 |5 |6 | | Interval | Frames | Bytes | Frames | Bytes | Frames | Bytes | Frames | Bytes | Frames | Bytes | Frames | Bytes | |-----------------------------------------------------------------------------------------------------------------------------------| | 0.0 \u0026lt;\u0026gt; 83831.2 | 182425 | 58211981 | 3790 | 280362 | 3526 | 209742 | 11786 | 4500028 | 590 | 160967 | 120 | 119764 | ===================================================================================================================================== 常用过滤条件   tcp.flags.syn == 1 \u0026amp;\u0026amp; tcp.flags.ack == 0 客户端握手包\n  tcp.flags.reset == 1 \u0026amp;\u0026amp; tcp.flags.ack == 0 客户端发过来的rst包\n  tcp.analysis.retransmission tcp重传包\n  tcp.analysis.lost_segment 数据包丢失\n  tcp.analysis.out_of_order 数据包乱序\n  ","permalink":"https://lambertxiao.github.io/posts/linux-%E6%8A%93%E5%8C%85%E5%B7%A5%E5%85%B7/linux%E7%B3%BB%E7%BB%9F%E4%B8%8A%E7%9A%84%E6%8A%93%E5%8C%85/","summary":"tcpdump、tshark、editcap、wireshark","title":"Linux系统上的网络抓包"},{"content":"参考链接： https://hzhu212.github.io/posts/2d7c5edb/\n","permalink":"https://lambertxiao.github.io/posts/lsm-tree/doc/","summary":"跳表 + SSTable + LogCompact + 顺序写","title":"LSMTree-一种支持高效读写的存储引擎"},{"content":"Redis 常用数据结构   strings\n  set\n  sorted set\n  hash（哈希表）\n  list（双向链表）\n  缓存一致性问题 什么是雪崩 同一时刻大量的缓存失效\n什么是缓存穿透 缓存和数据库里都没有数据，使用布隆过滤器，在接口处校验非法请求\n什么是缓存击穿 大量的请求同时在一个key上，当这个key失效时，大量请求直接落到数据库；热点数据永不过期\nRedis为什么快   单线程：减少线程间切换的开销，不用去考虑各种锁的问题，不存在加锁释放锁操作\n  完全基于内存的，绝大部份请求都是基于内存的操作\n  Redis的淘汰策略  no-evicition：不淘汰，直接报错 allkeys-random：随机淘汰 allkeys-lru：最近最少使用的淘汰 volatile-random：已设置ttl的key随机淘汰 volatile-lru：已设置ttl的key最近最少使用的淘汰 volatile-ttl：快过期的优先淘汰  Redis的持久化策略   RDB\n定期将内存中的数据保存到一个 dump 的文件中，fork子进程，定期写临时文件，临时文件写完直接替换原来的文件\n  AOF\n把所有的对 Redis 的服务器进行修改的命令都存到一个文件里，命令的集合； 每一个写命令都通过 write 函数追加到 appendonly.aof，同步策略：按秒，按写请求，数据量大，并有性能影响\n  Redis的单点故障问题 主从模式：主节点写，从节点读\n哨兵模式\n","permalink":"https://lambertxiao.github.io/posts/redis-%E7%9F%A5%E8%AF%86%E6%80%BB%E8%A7%88/redis/","summary":"单线程、全内存、AOF、RDB、单点、哨兵、集群","title":"Redis"},{"content":"TCMalloc TCMalloc 是 Google 开发的内存分配器。它具有现代化内存分配器的基本特征：对抗内存碎片、在多核处理器能够 scale。 TCMalloc 还减少了多线程程序的锁争用。\n基本概念   空闲列表(FreeList)\n  线程缓存(ThreadCache)\nTCMalloc 为每个线程分配一个线程本地缓存。线程本地缓存满足小分配。对象根据需要从中央数据结构移动到线程本地缓存中，并使用定期垃圾收集将内存从线程本地缓存迁移回中央数据结构\n  中央缓存(CentralCache)\n  中央页堆(PageHeap)\n  中央页面分配器\n  ThreadCache, CentralCache, PageHeap都有空闲列表，区别在于粒度不同\n小对象分配 小于32K的对象叫小对象，其余叫大对象。\n每个小对象大小映射到大约 170 个可分配的大小类之一。例如，961 到 1024 字节范围内的所有分配都向上舍入到 1024。大小类之间的间隔是这样的：小尺寸分隔 8 个字节，较大尺寸分隔 16 个字节，更大尺寸分隔 32 个字节，依此类推. 最大间距（大小 \u0026gt;= ~2K）为 256 字节。\n线程缓存包含每个大小类的空闲对象的单向链接列表。\n小对象分配流程   我们将其大小映射到相应的大小类\n  在线程缓存中查找当前线程对应的空闲列表\n  如果空闲列表不为空，我们从列表中删除第一个对象并返回它。当遵循这条快速路径时，TCMalloc 根本不获取锁。这有助于显着加快分配速度\n  如果空闲列表为空，我们从这个大小类的中央空闲列表中获取一堆对象，将它们放入线程局部空闲列表中。将新获取的对象之一返回给应用程序。\n  如果中央空闲列表也为空，我们从中央页面分配器分配一系列页面。将页面拆分为一组此大小类的对象。将新对象放置在中央空闲列表上。和以前一样，将这些对象中的一些移动到线程本地空闲列表中。\n    大对象分配流程 大对象大小 (\u0026gt; 32K) 向上舍入为页面大小 (4K) 并由中央页堆处理。中心页堆又是一个空闲列表数组。\n找到满足大小要求的位置。如果该空闲列表为空，则查看下一个空闲列表，依此类推。最后，如有必要，我们会查看最后一个空闲列表。如果失败，我们从系统中获取内存。\n","permalink":"https://lambertxiao.github.io/posts/tcmalloc/tcmalloc/","summary":"Golang的内存分配算法都是跟我学的","title":"TCMalloc"},{"content":"UDP  UDP是一种不可靠的不面向连接的传输层协议\n 报文结构   源端口\n  目标端口\n  长度\n  校验和\n  报文内容\n   UDP仅仅只是在IP数据报上加了端口，用来实现多路复用，多路分解\n ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/udp/","summary":"觉得TCP太慢了，何不试试我？","title":"UDP协议"},{"content":"中断 ","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E4%B8%AD%E6%96%AD/","summary":"放开那个女孩，让我来","title":"中断"},{"content":"模式 实模式   特点\n  寻址特点\n   寻址范围为1MB，地址总线20位    寄存器只使用来16位，采用单一寄存器来寻址的话只能访问到2^16=64KB的空间    访问内存采用 段基址 + 段内偏移地址的方式      程序引用的地址都是真实的物理地址，不灵活也不安全    程序可以随意修改自己的段基址，任意访问改变所有内存    一次只能运行一个程序    地址总线只有20位，最大可用内存只有1M      保护模式   起源\n CPU发展到32位后，推出保护模式，为了区别两种模式，便将之前的模式称为实模式。刚开机时，32位的CPU是先处于16位的实模式，再进入保护模式的 64位系统也一样吗？    地址总线32位\n  通用寄存器、标志寄存器、指令指针寄存器扩展到来32位\n  总结 ","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%A8%A1%E5%BC%8F/","summary":"模式 实模式   特点\n  寻址特点\n   寻址范围为1MB，地址总线20位    寄存器只使用来16位，采用单一寄存器来寻址的话只能访问到2^16=64KB的空间    访问内存采用 段基址 + 段内偏移地址的方式      程序引用的地址都是真实的物理地址，不灵活也不安全    程序可以随意修改自己的段基址，任意访问改变所有内存    一次只能运行一个程序    地址总线只有20位，最大可用内存只有1M      保护模式   起源\n CPU发展到32位后，推出保护模式，为了区别两种模式，便将之前的模式称为实模式。刚开机时，32位的CPU是先处于16位的实模式，再进入保护模式的 64位系统也一样吗？    地址总线32位\n  通用寄存器、标志寄存器、指令指针寄存器扩展到来32位\n  总结 ","title":"你知道实模式和保护模式的区别吗"},{"content":"前缀树 Trie树，即字典树，又称单词查找树或键树，是一种树形结构，是一种哈希树的变种。 典型应用是用于统计和排序大量的字符串（但不仅限于字符串），所以经常被搜索引擎系统用于文本词频统计。它的优点是：最大限度地减少无谓的字符串比较。\nTrie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。\n基本性质  根节点不包含字符，除根节点外每一个节点都只包含一个字符。 从根节点到某一节点，路径上经过的字符连接起来，为该节点对应的字符串。 每个节点的所有子节点包含的字符都不相同  ","permalink":"https://lambertxiao.github.io/posts/data-struct/%E5%89%8D%E7%BC%80%E6%A0%91/","summary":"Leetcode刷题见过我吧","title":"前缀树"},{"content":"ECDH算法 ECDH全称是椭圆曲线迪菲-赫尔曼秘钥交换（Elliptic Curve Diffie–Hellman key Exchange），主要是用来在一个不安全的通道中建立起安全的共有加密资料，一般来说交换的都是私钥，这个密钥一般作为“对称加密”的密钥而被双方在后续数据传输中使用。\n算法流程 我们通过一个经典的场景，Alice和Bob要在一条不安全的线路上交换秘钥，交换的秘钥不能被中间人知晓。 首先，双方约定使用ECDH秘钥交换算法，这个时候双方也知道了ECDH算法里的一个大素数P，这个P可以看做是一个算法中的常量。 P的位数决定了攻击者破解的难度。还有一个整数g用来辅助整个秘钥交换，g不用很大，一般是2或者5，双方知道g和p之后就开始了ECDH交换秘钥的过程了。\n Alice生成一个整数a作为私钥，需要利用p，g，a通过公式 g^a mod p = A 生成A作为公钥传递。 Bob通过链路收到Alice发来的p，g，A，知道了Alice的公钥A。这个时候Bob也生成自己的私钥b，然后通过公式 g^b mod p = B 生成自己公钥B。 Alice收到Bob发来的公钥B以后，同样通过 B^a mod p = K 生成公共秘钥K，这样Alice和Bob就通过不传递私钥a和b完成了对公共秘钥K的协商。  举个栗子 我们通过代入具体的数字来重复一下上面的过程：\n Alice和Bob同意使用质数p和整数g： p = 83, g = 8  Alice选择秘钥 a = 9, 生成公钥 g^a mod p = A 并发送 (8^9) mod 83 = 5 Bob选择秘钥 b = 21, 生成公钥 A^b mod p = K 并发送 (8^21) mod 83 = 18\nAlice计算 B^a mod p = K 18^9 mod 83 = 24 Bob计算 B^a mod p = K 5^21 mod 83 = 24 至此24就是双方协商出来的秘钥。\n存在的问题 ECDH并不验证公钥发送者的身份，所以无法阻止中间人攻击，需要使用CA机构向双方提供可信的数字签名密钥\n","permalink":"https://lambertxiao.github.io/posts/%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95-ecdh/ecdh%E7%AE%97%E6%B3%95/","summary":"ECDH算法 ECDH全称是椭圆曲线迪菲-赫尔曼秘钥交换（Elliptic Curve Diffie–Hellman key Exchange），主要是用来在一个不安全的通道中建立起安全的共有加密资料，一般来说交换的都是私钥，这个密钥一般作为“对称加密”的密钥而被双方在后续数据传输中使用。\n算法流程 我们通过一个经典的场景，Alice和Bob要在一条不安全的线路上交换秘钥，交换的秘钥不能被中间人知晓。 首先，双方约定使用ECDH秘钥交换算法，这个时候双方也知道了ECDH算法里的一个大素数P，这个P可以看做是一个算法中的常量。 P的位数决定了攻击者破解的难度。还有一个整数g用来辅助整个秘钥交换，g不用很大，一般是2或者5，双方知道g和p之后就开始了ECDH交换秘钥的过程了。\n Alice生成一个整数a作为私钥，需要利用p，g，a通过公式 g^a mod p = A 生成A作为公钥传递。 Bob通过链路收到Alice发来的p，g，A，知道了Alice的公钥A。这个时候Bob也生成自己的私钥b，然后通过公式 g^b mod p = B 生成自己公钥B。 Alice收到Bob发来的公钥B以后，同样通过 B^a mod p = K 生成公共秘钥K，这样Alice和Bob就通过不传递私钥a和b完成了对公共秘钥K的协商。  举个栗子 我们通过代入具体的数字来重复一下上面的过程：\n Alice和Bob同意使用质数p和整数g： p = 83, g = 8  Alice选择秘钥 a = 9, 生成公钥 g^a mod p = A 并发送 (8^9) mod 83 = 5 Bob选择秘钥 b = 21, 生成公钥 A^b mod p = K 并发送 (8^21) mod 83 = 18","title":"加密算法之ECDH"},{"content":"KCP 相对于TCP的改进  RTO不翻倍  RTO(Retransmission-TimeOut)即重传超时时间,TCP是基于ARQ协议实现的可靠性，KCP也是基于ARQ协议实现的可靠性，但TCP的超时计算是RTO2，而KCP的超时计算是RTO1.5，也就是说假如连续丢包3次，TCP是RTO8，而KCP则是RTO3.375，意味着可以更快地重新传输数据。通过4字节ts计算RTT(Round-Trip-Time)即往返时延，再通过RTT计算RTO，ts(timestamp)即当前segment发送时的时间戳。\n选择性重传  TCP中实现的是连续ARQ协议，再配合累计确认重传数据，只不过重传时需要将最小序号丢失的以后所有的数据都要重传，而KCP则只重传真正丢失的数据。\n快速重传  与TCP相同，都是通过累计确认实现的，发送端发送了1，2，3，4，5几个包，然后收到远端的ACK：1，3，4，5，当收到ACK = 3时，KCP知道2被跳过1次，收到ACK = 4时，知道2被跳过了2次，此时可以认为2号丢失，不用等超时，直接重传2号包，大大改善了丢包时的传输速度。1字节cmd = 81时，sn相当于TCP中的seq，cmd = 82 时，sn相当于TCP中的ack。cmd相当于WebSocket协议中的openCode，即操作码。\n非延迟ACK  TCP在连续ARQ协议中，不会将一连串的每个数据都响应一次，而是延迟发送ACK，即上文所说的UNA模式，目的是为了充分利用带宽，但是这样会计算出较大的RTT时间，延长了丢包时的判断过程，而KCP的ACK是否延迟发送可以调节。\nUNA + ACK  UNA模式参考特征2和特征4，ACK模式可以参考特征3。4字节una表示cmd = 81时，当前已经收到了小于una的所有数据。\n非退让流控  在传输及时性要求很高的小数据时，可以通过配置忽略上文所说的窗口协议中的拥塞窗口机制，而仅仅依赖于滑动窗口。2字节wnd与TCP协议中的16位窗口大小意义相同，值得一提的是，KCP协议的窗口控制还有其它途径，当cmd = 83时，表示询问远端窗口大小，当cmd = 84时，表示告知远端窗口大小。\n4字节conv表示会话匹配数字，为了在KCP基于UDP实现时，让无连接的协议知道哪个是哪个，相当于WEB系统HTTP协议中的SessionID。\n1字节frg表示拆数据时的编号，4字节len表示整个数据的长度，相当于WebSocket协议中的len。\n","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/kcp/","summary":"TCP退下，让我来！","title":"啥是KCP协议？"},{"content":"留存计算 DELIMITER $$ CREATE DEFINER=`vpRoot`@`%` PROCEDURE `retention`(IN datestr VARCHAR(20)) BEGIN DECLARE s INT DEFAULT 0; DECLARE sdate VARCHAR(20) CHARACTER SET utf8; DECLARE country_zh VARCHAR(20) CHARACTER SET utf8; DECLARE pkg_name VARCHAR(50) CHARACTER SET utf8 ; DECLARE register_num INT DEFAULT 0; DECLARE keep_number_1 INT DEFAULT 0; DECLARE keep_number_2 INT DEFAULT 0; DECLARE keep_number_3 INT DEFAULT 0; DECLARE keep_number_7 INT DEFAULT 0; DECLARE keep_number_15 INT DEFAULT 0; DECLARE keep_number_30 INT DEFAULT 0; DECLARE report CURSOR FOR SELECT t5.pt sdate, t5.countryZh, t5.pkgName, t4.keep_number_1, t4.keep_number_2, t4.keep_number_3, t4.keep_number_7, t4.keep_number_15, t4.keep_number_30, t5.register_num FROM (SELECT t3.sdate, t3.countryZh countryZh, t3.pkgName, CASE WHEN t3.delta = 1 THEN COUNT(userId) END \u0026#39;keep_number_1\u0026#39;, CASE WHEN t3.delta = 2 THEN COUNT(userId) END \u0026#39;keep_number_2\u0026#39;, CASE WHEN t3.delta = 3 THEN COUNT(userId) END \u0026#39;keep_number_3\u0026#39;, CASE WHEN t3.delta = 7 THEN COUNT(userId) END \u0026#39;keep_number_7\u0026#39;, CASE WHEN t3.delta = 15 THEN COUNT(userId) END \u0026#39;keep_number_15\u0026#39;, CASE WHEN t3.delta = 30 THEN COUNT(userId) END \u0026#39;keep_number_30\u0026#39; FROM (SELECT CASE delta WHEN 1 THEN DATE_SUB(datestr, INTERVAL 1 DAY) WHEN 2 THEN DATE_SUB(datestr, INTERVAL 2 DAY) WHEN 3 THEN DATE_SUB(datestr, INTERVAL 3 DAY) WHEN 7 THEN DATE_SUB(datestr, INTERVAL 7 DAY) WHEN 15 THEN DATE_SUB(datestr, INTERVAL 15 DAY) WHEN 30 THEN DATE_SUB(datestr, INTERVAL 30 DAY) END AS sdate, t.countryZh, t.pkgName, t.userId, t.delta FROM (SELECT t1.countryZh, t1.pkgName, t1.userId, DATEDIFF(datestr, t2.pt) AS delta FROM (SELECT countryZh, pkgName, userId FROM hourly_active_user WHERE queryTime = datestr AND userId != \u0026#39;undefined\u0026#39; GROUP BY countryZh , pkgName , userId) t1 LEFT JOIN sys_user t2 ON t1.userId = t2.userId AND t1.countryZh = t2.countryZh AND t1.pkgName = t2.pkgName) t WHERE t.delta IN (1 , 2, 3, 7, 15, 30)) t3 GROUP BY t3.sdate , t3.countryZh , t3.pkgName) t4 RIGHT JOIN (SELECT countryZh, pkgName, pt, COUNT(userId) AS register_num FROM sys_user where pt in ( date_sub(datestr, INTERVAL 1 DAY), date_sub(datestr, INTERVAL 2 DAY), date_sub(datestr, INTERVAL 3 DAY), date_sub(datestr, INTERVAL 7 DAY), date_sub(datestr, INTERVAL 15 DAY), date_sub(datestr, INTERVAL 30 DAY) ) and countryZh is not null GROUP BY countryZh , pkgName , pt) t5 ON t4.countryZh = t5.countryZh AND t4.pkgName = t5.pkgName AND t4.sdate = t5.pt order by t5.pt; DECLARE CONTINUE HANDLER FOR NOT FOUND SET s = 1; select datestr; OPEN report; FETCH report INTO sdate, country_zh, pkg_name, keep_number_1, keep_number_2, keep_number_3, keep_number_7, keep_number_15, keep_number_30, register_num; WHILE s\u0026lt;\u0026gt;1 DO INSERT INTO user_retention_gmt8 (pt, country_zh, pkg_name, register_num) VALUE (sdate, country_zh, pkg_name, register_num) ON DUPLICATE KEY UPDATE `register_num` = register_num; IF keep_number_1 IS NOT NULL THEN INSERT INTO user_retention_gmt8 (pt, country_zh, pkg_name, register_num, keep_number_1) VALUE (sdate, country_zh, pkg_name, register_num, keep_number_1) ON DUPLICATE KEY UPDATE `keep_number_1` = keep_number_1; END IF; IF keep_number_2 IS NOT NULL THEN INSERT INTO user_retention_gmt8(pt, country_zh, pkg_name, register_num, keep_number_2) VALUE (sdate, country_zh, pkg_name, register_num, keep_number_2) ON DUPLICATE KEY UPDATE `keep_number_2` = keep_number_2; END IF; IF keep_number_3 IS NOT NULL THEN INSERT INTO user_retention_gmt8(pt, country_zh, pkg_name, register_num, keep_number_3) VALUE (sdate, country_zh, pkg_name, register_num, keep_number_3) ON DUPLICATE KEY UPDATE `keep_number_3` = keep_number_3; END IF; IF keep_number_7 IS NOT NULL THEN INSERT INTO user_retention_gmt8(pt, country_zh, pkg_name, register_num, keep_number_7) VALUE (sdate, country_zh, pkg_name, register_num, keep_number_7) ON DUPLICATE KEY UPDATE `keep_number_7` = keep_number_7; END IF; IF keep_number_15 IS NOT NULL THEN INSERT INTO user_retention_gmt8(pt, country_zh, pkg_name, register_num, keep_number_15) VALUE (sdate, country_zh, pkg_name, register_num, keep_number_15) ON DUPLICATE KEY UPDATE `keep_number_15` = keep_number_15; END IF; IF keep_number_30 IS NOT NULL THEN INSERT INTO user_retention_gmt8(pt, country_zh, pkg_name, register_num, keep_number_30) VALUE (sdate, country_zh, pkg_name, register_num, keep_number_30) ON DUPLICATE KEY UPDATE `keep_number_30` = keep_number_30; END IF; FETCH report INTO sdate, country_zh, pkg_name, keep_number_1, keep_number_2, keep_number_3, keep_number_7, keep_number_15, keep_number_30, register_num; END WHILE; CLOSE report; END$$ DELIMITER ; ","permalink":"https://lambertxiao.github.io/posts/%E4%B8%9A%E5%8A%A1%E8%AE%A1%E7%AE%97-%E7%95%99%E5%AD%98/%E7%95%99%E5%AD%98%E8%AE%A1%E7%AE%97/","summary":"留存计算 DELIMITER $$ CREATE DEFINER=`vpRoot`@`%` PROCEDURE `retention`(IN datestr VARCHAR(20)) BEGIN DECLARE s INT DEFAULT 0; DECLARE sdate VARCHAR(20) CHARACTER SET utf8; DECLARE country_zh VARCHAR(20) CHARACTER SET utf8; DECLARE pkg_name VARCHAR(50) CHARACTER SET utf8 ; DECLARE register_num INT DEFAULT 0; DECLARE keep_number_1 INT DEFAULT 0; DECLARE keep_number_2 INT DEFAULT 0; DECLARE keep_number_3 INT DEFAULT 0; DECLARE keep_number_7 INT DEFAULT 0; DECLARE keep_number_15 INT DEFAULT 0; DECLARE keep_number_30 INT DEFAULT 0; DECLARE report CURSOR FOR SELECT t5.","title":"如何通过存储过程计算用户留存"},{"content":"数组 数组是可以在内存中连续存储多个元素的结构，在内存中的分配也是连续的，数组中的元素可以通过数组下标进行访问，数组下标从0开始。\n优点：\n 按照索引查询元素速度快 按照索引遍历数组方便  缺点：\n 数组的大小固定后就无法扩容了 数组只能存储一种类型的数据 添加，删除的操作慢，因为要移动其他的元素。  适用场景：\n频繁查询，对存储空间要求不大，很少增加和删除的情况\n二叉排序树  二叉排序树是基于二分法的策略提高数据的查找速度的二叉树的数据结构\n  左子树上所有结点的值均小于或等于它的根结点的值。 右子树上所有结点的值均大于或等于它的根结点的值。 左、右子树也分别为二叉排序树。  队列 队列也是一种线性表，不同的是，队列可以在一端添加元素，在另一端取出元素，也就是：先进先出。 从一端放入元素的操作称为入队，取出元素为出队\n链表 链表是物理存储单元上非连续的、非顺序的存储结构，每个链表元素包含两块内容，一个是存储元素的数据域，另一个是指向下一个结点地址的指针域。根据指针的指向，链表能形成不同的结构，例如单链表，双向链表，循环链表等。\n优点：\n  链表是很常用的一种数据结构，不需要初始化容量，可以任意加减元素；\n  添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可，速度快；\n  缺点：\n 因为含有大量的指针域，占用空间较大 查找元素需要遍历链表来查找，非常耗时  适用场景：\n数据量较小，需要频繁增加，删除操作的场景\n跳表 跳表可以解决有序链表查找插入慢的问题，思路是在有序列表的基础上，增加多级索引，又是一个典型的空间换时间的数据结构\n每一层都是一个有序链表，上一层有指针指向下一层\n栈 栈是一种特殊的线性表(内存连续)，仅能在线性表的一端操作，栈顶允许操作，栈底不允许操作。\n栈的特点是：先进后出，或者说是后进先出，从栈顶放入元素的操作叫入栈，取出元素叫出栈。\n栈常应用于实现递归功能方面的场景\nHash表 散列表，也叫哈希表，是根据关键码和值 (key和value) 直接进行访问的数据结构，通过key和value来映射到集合中的一个位置，这样就可以很快找到集合中的对应元素。\n记录的存储位置=f(key)\n这里的对应关系 f 成为散列函数，又称为哈希 (hash函数)，而散列表就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字，然后就将该数字对数组长度进行取余，取余结果就当作数组的下标，将value存储在以该数字为下标的数组空间里，这种存储空间可以充分利用数组的查找优势来查找元素，所以查找的速度很快。\n哈希表在应用中也是比较常见的，就如Java中有些集合类就是借鉴了哈希原理构造的，例如HashMap，HashTable等，利用hash表的优势，对于集合的查找元素时非常方便的，然而，因为哈希表是基于数组衍生的数据结构，在添加删除元素方面是比较慢的，所以很多时候需要用到一种数组链表来做，也就是拉链法。拉链法是数组结合链表的一种结构，较早前的hashMap底层的存储就是采用这种结构，直到jdk1.8之后才换成了数组加红黑树的结构，其示例图如下\n","permalink":"https://lambertxiao.github.io/posts/data-struct/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/","summary":"数组、栈、链表、队列、hash表\u0026hellip;","title":"常见数据结构"},{"content":"平衡二叉树  平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构\n  非叶子节点最多拥有两个子节点； 非叶子节值大于左边子节点、小于右边子节点； 树的左右两边的层级数相差不会大于1; 没有值相等重复的节点;  为什么要有平衡二叉树 避免二叉树长短腿，查找效率比红黑树高，但是调整的成本页高，在频繁查找时选AVL更好，在增删多时选红黑树更好\n","permalink":"https://lambertxiao.github.io/posts/data-struct/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/","summary":"Leetcode刷题见过我吧","title":"平衡二叉树"},{"content":"","permalink":"https://lambertxiao.github.io/posts/template/","summary":"","title":"我只是个小模板"},{"content":"数据库的索引 为什么要有索引 它是用于提高数据库表数据访问速度的数据库对象\n 索引可以避免全表扫描。多数查询可以仅扫描少量索引页及数据页，而不是遍历所有数据页。 对于非聚集索引，有些查询甚至可以不访问数据页。 聚集索引可以避免数据插入操作集中于表的最后一个数据页。 一些情况下，索引还可用于避免排序操作。  索引是怎么存储的  基于B树  普通的B-Tree的结点中，元素就是一个个的数字。但是上图中，我们把元素部分拆分成了key-data的形式，key就是数据的主键，data就是具体的数据。这样我们在找一条数的时候，就沿着根结点往下找就ok了，效率是比较高的。\n 基于B+树  局部性原理:\n当一个数据被用到时，其附近的数据也通常会马上被使用。操作系统从磁盘读取数据到内存是以磁盘块（block）为基本单位的，位于同一个磁盘块中的数据会被一次性读取出来，而不是需要什么取什么。即使只需要一个字节，磁盘也会从这个位置开始，顺序向后读取一定长度的数据放入内存\nB-Tree和B+Tree该如何选择呢？都有哪些优劣呢？\n  B-Tree因为非叶子结点也保存具体数据，所以在查找某个关键字的时候找到即可返回。而B+Tree所有的数据都在叶子结点，每次查找都得到叶子结点。所以在同样高度的B-Tree和B+Tree中，B-Tree查找某个关键字的效率更高。\n  由于B+Tree所有的数据都在叶子结点，并且结点之间有指针连接，在找大于某个关键字或者小于某个关键字的数据的时候，B+Tree只需要找到该关键字然后沿着链表遍历就可以了，而B-Tree还需要遍历该关键字结点的根结点去搜索。\n  由于B-Tree的每个结点（这里的结点可以理解为一个数据页）都存储主键+实际数据，而B+Tree非叶子结点只存储关键字信息，而每个页的大小有限是有限的，所以同一页能存储的B-Tree的数据会比B+Tree存储的更少。这样同样总量的数据，B-Tree的深度会更大，增大查询时的磁盘I/O次数，进而影响查询效率。\n  innodb引擎数据存储 在InnoDB存储引擎中，也有页的概念，默认每个页的大小为16K，也就是每次读取数据时都是读取4*4k的大小！假设我们现在有一个用户表，我们往里面写数据\n这里需要注意的一点是，在某个页内插入新行时，为了不减少数据的移动，通常是插入到当前行的后面或者是已删除行留下来的空间，所以在某一个页内的数据并不是完全有序的（后面页结构部分有细讲），但是为了为了数据访问顺序性，在每个记录中都有一个指向下一条记录的指针，以此构成了一条单向有序链表，不过在这里为了方便演示我是按顺序排列的！\n由于数据还比较少，一个页就能容下，所以只有一个根结点，主键和数据也都是保存在根结点（左边的数字代表主键，右边名字、性别代表具体的数据）。假设我们写入10条数据之后，Page1满了，再写入新的数据会怎么存放呢？我们继续看下图：\n有个叫“秦寿生”的朋友来了，但是Page1已经放不下数据了，这时候就需要进行页分裂，产生一个新的Page。在innodb中的流程是怎么样的呢？\n1、产生新的Page2，然后将Page1的内容复制到Page2。 2、产生新的Page3，“秦寿生”的数据放入Page3。 3、原来的Page1依然作为根结点，但是变成了一个不存放数据只存放索引的页，并且有两个子结点Page2、Page3。 这里有两个问题需要注意的是 1、为什么要复制Page1为Page2而不是创建一个新的页作为根结点，这样就少了一步复制的开销了？ 如果是重新创建根结点，那根结点存储的物理地址可能经常会变，不利于查找。并且在innodb中根结点是会预读到内存中的，所以结点的物理地址固定会比较好！\n2、原来Page1有10条数据，在插入第11条数据的时候进行裂变，根据前面对B-Tree、B+Tree特性的了解，那这至少是一颗11阶的树，裂变之后每个结点的元素至少为11/2=5个，那是不是应该页裂变之后主键1-5的数据还是在原来的页，主键6-11的数据会放到新的页，根结点存放主键6？ 如果是这样的话新的页空间利用率只有50%，并且会导致更为频繁的页分裂。所以innodb对这一点做了优化，新的数据放入新创建的页，不移动原有页面的任何记录。\n随着数据的不断写入，这棵树也逐渐枝繁叶茂，如下图：\n每次新增数据，都是将一个页写满，然后新创建一个页继续写，这里其实是有个隐含条件的，那就是主键自增！主键自增写入时新插入的数据不会影响到原有页，插入效率高！且页的利用率高！但是如果主键是无序的或者随机的，那每次的插入可能会导致原有页频繁的分裂，影响插入效率！降低页的利用率！这也是为什么在innodb中建议设置主键自增的原因！\n这棵树的非叶子结点上存的都是主键，那如果一个表没有主键会怎么样？在innodb中，如果一个表没有主键，那默认会找建了唯一索引的列，如果也没有，则会生成一个隐形的字段作为主键！\n有数据插入那就有删除，如果这个用户表频繁的插入和删除，那会导致数据页产生碎片，页的空间利用率低，还会导致树变的“虚高”，降低查询效率！这可以通过索引重建来消除碎片提高查询效率！\ninnodb引擎数据查找 数据插入了怎么查找呢？\n1、找到数据所在的页。这个查找过程就跟前面说到的B+Tree的搜索过程是一样的，从根结点开始查找一直到叶子结点。 2、在页内找具体的数据。读取第1步找到的叶子结点数据到内存中，然后通过分块查找的方法找到具体的数据。 这跟我们在新华字典中找某个汉字是一样的，先通过字典的索引定位到该汉字拼音所在的页，然后到指定的页找到具体的汉字。innodb中定位到页后用了哪种策略快速查找某个主键呢？这我们就需要从页结构开始了解。\n左边蓝色区域称为Page Directory，这块区域由多个slot组成，是一个稀疏索引结构，即一个槽中可能属于多个记录，最少属于4条记录，最多属于8条记录。槽内的数据是有序存放的，所以当我们寻找一条数据的时候可以先在槽中通过二分法查找到一个大致的位置。\n右边区域为数据区域，每一个数据页中都包含多条行数据。注意看图中最上面和最下面的两条特殊的行记录Infimum和Supremum，这是两个虚拟的行记录。在没有其他用户数据的时候Infimum的下一条记录的指针指向Supremum，当有用户数据的时候，Infimum的下一条记录的指针指向当前页中最小的用户记录，当前页中最大的用户记录的下一条记录的指针指向Supremum，至此整个页内的所有行记录形成一个单向链表。\n行记录被Page Directory逻辑的分成了多个块，块与块之间是有序的，也就是说“4”这个槽指向的数据块内最大的行记录的主键都要比“8”这个槽指向的数据块内最小的行记录的主键要小。但是块内部的行记录不一定有序。\n每个行记录的都有一个n_owned的区域（图中粉红色区域），n_owned标识这个这个块有多少条数据，伪记录Infimum的n_owned值总是1，记录Supremum的n_owned的取值范围为[1,8]，其他用户记录n_owned的取值范围[4,8]，并且只有每个块中最大的那条记录的n_owned才会有值，其他的用户记录的n_owned为0。\n所以当我们要找主键为6的记录时，先通过二分法在稀疏索引中找到对应的槽，也就是Page Directory中“8”这个槽，“8”这个槽指向的是该数据块中最大的记录，而数据是单向链表结构所以无法逆向查找，所以需要找到上一个槽即“4”这个槽，然后通过“4”这个槽中最大的用户记录的指针沿着链表顺序查找到目标记录。\n索引类型  聚集索引   数据行的物理顺序与列值（一般是主键的那一列）的逻辑顺序相同，一个表中只能拥有一个聚集索引。\n  非聚集索引  该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同，一个表中可以拥有多个非聚集索引。普通索引，唯一索引，全文索引都是非聚集索引\n前面关于数据存储的都是演示的聚集索引的实现，如果上面的用户表需要以“用户名字”建立一个非聚集索引，是怎么实现的呢？我们看下图：\n非聚集索引的存储结构与前面是一样的，不同的是在叶子结点的数据部分存的不再是具体的数据，而数据的聚集索引的key。所以通过非聚集索引查找的过程是先找到该索引key对应的聚集索引的key，然后再拿聚集索引的key到主键索引树上查找对应的数据，这个过程称为回表！\ninnodb和myisam innodb引擎数据在物理上是按主键顺序存放，而MyISAM引擎数据在物理上按插入的顺序存放\n引用： https://zhuanlan.zhihu.com/p/72941280\n","permalink":"https://lambertxiao.github.io/posts/database-%E7%B4%A2%E5%BC%95%E7%9B%B8%E5%85%B3/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E7%B4%A2%E5%BC%95/","summary":"B树和B+树","title":"数据库索引"},{"content":"数据库锁  按锁的粒度划分（即，每次上锁的对象是表，行还是页）：表级锁，行级锁，页级锁 按锁的级别划分：共享锁、排他锁 按加锁方式分：自动锁（存储引擎自行根据需要施加的锁）、显式锁（用户手动请求的锁） 按操作划分：DML锁（对数据进行操作的锁)、DDL锁（对表结构进行变更的锁） 最后按使用方式划分：悲观锁、乐观锁  共享锁 共享锁(S)表示对数据进行读操作。因此多个事务可以同时为一个对象加共享锁。（对于写作来说就是，如果文章处于「已发布」的状态，则所有人都可以同时看。）\nSELECT ... LOCK IN SHARE MODE; 排他锁 排他锁表示对数据进行写操作。如果一个事务对对象加了排他锁，其他事务就不能再给它加任何锁了。（对于写作来说就是，如果文章正在被修改的时候，其他的读者无法看到这篇文章，其他的编辑也无法修改这篇文章。）\nSELECT ... FOR UPDATE; 悲观锁 在关系数据库管理系统里，悲观并发控制（又名“悲观锁”，Pessimistic Concurrency Control，缩写“PCC”）是一种并发控制的方法，即为悲观的思想，认为并发问题总会出现，所以每次一个事务读取某一条记录后，就会把这条记录锁住，这样其它的事务要想更新，必须等以前的事务提交或者回滚解除锁。\n悲观并发控制主要用于数据争用激烈的环境，以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。\n乐观锁 总是假设最好的情况，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据，乐观锁假设认为数据一般情况下不会造成冲突，所以在数据进行提交更新的时候，才会正式对数据的冲突与否进行检测，如果发现冲突了，则让返回用户错误的信息，让用户决定如何去做，一般来说可以使用版本号机制和 CAS 算法实现。\n版本号机制 一般是在数据表中加上一个数据版本号 version 字段，表示数据被修改的次数，当数据被修改时，version 值会加一，当读取数据时，将 version 字段的值一同读出，数据每更新一次，对此 version 值加一。当我们提交更新的时候，判断数据库表对应记录的当前版本信息与第一次取出来的 version 值进行比对，如果数据库表当前版本号与第一次取出来的 version 值相等，则予以更新，否则认为是过期数据。\nCAS 算法 即 compare and swap（比较与交换），是一种有名的无锁算法。\n无锁编程，即不使用锁的情况下实现多线程之间的变量同步，也就是在没有线程被阻塞的情况下实现变量的同步，所以也叫非阻塞同步（Non-blocking Synchronization）。CAS 算法涉及到三个操作数：\n需要读写的内存值 V 进行比较的值 A 拟写入的新值 B\n当且仅当 V 的值等于 A 时，CAS 通过原子方式用新值B来更新V的值，否则不会执行任何操作（比较和替换是一个原子操作）。一般情况下是一个自旋操作，即不断的重试，与 version 事务机制类似，CAS 事务也是一种细粒度的锁。然而，version 为行级锁，粒度过大， 而 CAS 事务为列级锁，粒度更小。根据锁机制的一般原则，粒度越小，并发性能越高。\n但是这样也会有一些缺点，例如：\n  ABA 问题\n比如说一个线程 T1 从内存位置V中取出 A，这时候另一个线程 T2 也从内存中取出 A，并且 T2 进行了一些操作变成了 B，然后 T2 又将 V 位置的数据变成 A，这时候线程 T1 进行 CAS 操作发现内存中仍然是A，然后 T1 操作成功。\n  循环时间长开销大\n自旋 CAS（不成功，就一直循环执行，直到成功）如果长时间不成功，会给 CPU 带来非常大的执行开销。\n  只能保证一个共享变量的原子操作\n当对一个共享变量执行操作时，我们可以使用循环 CAS 的方式来保证原子操作，但是对多个共享变量操作时，循环 CAS 就无法保证操作的原子性。\n  ","permalink":"https://lambertxiao.github.io/posts/database-%E9%94%81%E7%9B%B8%E5%85%B3/%E6%95%B0%E6%8D%AE%E5%BA%93%E9%94%81/","summary":"数据库锁  按锁的粒度划分（即，每次上锁的对象是表，行还是页）：表级锁，行级锁，页级锁 按锁的级别划分：共享锁、排他锁 按加锁方式分：自动锁（存储引擎自行根据需要施加的锁）、显式锁（用户手动请求的锁） 按操作划分：DML锁（对数据进行操作的锁)、DDL锁（对表结构进行变更的锁） 最后按使用方式划分：悲观锁、乐观锁  共享锁 共享锁(S)表示对数据进行读操作。因此多个事务可以同时为一个对象加共享锁。（对于写作来说就是，如果文章处于「已发布」的状态，则所有人都可以同时看。）\nSELECT ... LOCK IN SHARE MODE; 排他锁 排他锁表示对数据进行写操作。如果一个事务对对象加了排他锁，其他事务就不能再给它加任何锁了。（对于写作来说就是，如果文章正在被修改的时候，其他的读者无法看到这篇文章，其他的编辑也无法修改这篇文章。）\nSELECT ... FOR UPDATE; 悲观锁 在关系数据库管理系统里，悲观并发控制（又名“悲观锁”，Pessimistic Concurrency Control，缩写“PCC”）是一种并发控制的方法，即为悲观的思想，认为并发问题总会出现，所以每次一个事务读取某一条记录后，就会把这条记录锁住，这样其它的事务要想更新，必须等以前的事务提交或者回滚解除锁。\n悲观并发控制主要用于数据争用激烈的环境，以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。\n乐观锁 总是假设最好的情况，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据，乐观锁假设认为数据一般情况下不会造成冲突，所以在数据进行提交更新的时候，才会正式对数据的冲突与否进行检测，如果发现冲突了，则让返回用户错误的信息，让用户决定如何去做，一般来说可以使用版本号机制和 CAS 算法实现。\n版本号机制 一般是在数据表中加上一个数据版本号 version 字段，表示数据被修改的次数，当数据被修改时，version 值会加一，当读取数据时，将 version 字段的值一同读出，数据每更新一次，对此 version 值加一。当我们提交更新的时候，判断数据库表对应记录的当前版本信息与第一次取出来的 version 值进行比对，如果数据库表当前版本号与第一次取出来的 version 值相等，则予以更新，否则认为是过期数据。\nCAS 算法 即 compare and swap（比较与交换），是一种有名的无锁算法。\n无锁编程，即不使用锁的情况下实现多线程之间的变量同步，也就是在没有线程被阻塞的情况下实现变量的同步，所以也叫非阻塞同步（Non-blocking Synchronization）。CAS 算法涉及到三个操作数：\n需要读写的内存值 V 进行比较的值 A 拟写入的新值 B\n当且仅当 V 的值等于 A 时，CAS 通过原子方式用新值B来更新V的值，否则不会执行任何操作（比较和替换是一个原子操作）。一般情况下是一个自旋操作，即不断的重试，与 version 事务机制类似，CAS 事务也是一种细粒度的锁。然而，version 为行级锁，粒度过大， 而 CAS 事务为列级锁，粒度更小。根据锁机制的一般原则，粒度越小，并发性能越高。","title":"数据库锁"},{"content":"Quic  QUIC融合了包括TCP，TLS，HTTP/2等协议的特性，但基于UDP传输。\n QUIC 的设计目的   降低了连接建立时延\n  改进了握手控制\n  多路复用\n同一条 QUIC 连接上可以创建多个 stream，处理上层应用的不同请求\n  避免线头阻塞\n  前向纠错\n  连接迁移\n  默认使用TLS 1.3作为全链路安全\n  Quic术语   数据包（Packet）：QUIC 协议中一个完整可处理的单元，可以封装在 UDP 数据报（datagram）中。多个 QUIC 数据包（packets）可以封装在一个 UDP 数据报（datagram）中。\n  帧（Frame）：QUIC 数据包（packet）的有效载荷（payload）。\n  端点（Endpoint）：在 QUIC 连接中生成、接收和处理 QUIC 数据包（packets）\n  客户端（Client）: 创建 QUIC 连接的端点。\n  服务端（Server）: 接收 QUIC 连接的端点。\n    地址（Address）：未经限定使用时，表示网络路径一端的 IP 版本、IP 地址和 UDP 端口号的元组。\n  连接 ID（Connection ID）： 用于标识端点 QUIC 连接的一种标识符。每个端点（endpoint）为其对端（peer）选择一个或多个连接 ID，将其包含在发送到该端点的数据包（packets）中。这个值对 peer 不透明。\n  流（Stream）：QUIC 连接中有序字节的单向（unidirectional）或双向（bidirectional）通道。一个 QUIC 连接可以同时携带多个流。\n  应用程序（Application）：使用 QUIC 发送或者接收数据的实体。\n  Quic的设计 QUIC 同样是一个可靠的协议，它使用 Packet Number 代替了 TCP 的 sequence number，并且每个 Packet Number 都严格递增，也就是说就算 Packet N 丢失了，重传的 Packet N 的 Packet Number 已经不是 N，而是一个比 N 大的值。而\n但是单纯依靠严格递增的 Packet Number 肯定是无法保证数据的顺序性和可靠性。QUIC 又引入了一个 Stream Offset 的概念。\n  QUIC 最基本的传输单元是 Packet\n  PacketNumber\n  StreamOffset\n    Stream 是有序序列的字节\n  Ack Delay 时间\n  通过 window_update 帧告诉对端自己可以接收的字节数，这样发送方就不会发送超过这个数量的数据\n  通过 BlockFrame 告诉对端由于流量控制被阻塞了，无法发送数据\n    基于 stream 和 connecton 级别的流量控制\n  没有队头阻塞的多路复用 在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (stream)。但是 QUIC 的多路复用相比 HTTP2 有一个很大的优势。\nQUIC 一个连接上的多个 stream 之间没有依赖。这样假如 stream2 丢了一个 udp packet，也只会影响 stream2 的处理。不会影响 stream2 之前及之后的 stream 的处理。\n这也就在很大程度上缓解甚至消除了队头阻塞的影响。\n连接过程 QUIC 的连接过程 在 client 端本地没有任何 server 端信息的时候，是无法做到 0RTT 的，下面先来梳理一下 client 首次和 server 通信的流程：\n首次连接\n server 生成一个质数 [公式] 和一个整数 [公式] ，其中 [公式] 是 [公式] 的一个生成元，同时随机生成一个数 [公式] 作为私钥，并计算出公钥 [公式] = [公式]，将 [公式] 三元组打包成 [公式] ，等待 client 连接\n  client 首次发起连接，简单发送 [公式] 给 server\n  server 将已经生成好的 [公式] 返回给 client\n  client 随机生成一个数 [公式]作为自己的私钥，并根据 [公式] 中的 [公式] 和 [公式] 计算出公钥 [公式]\n  client 计算通信使用的密钥 [公式]\n  client 用 [公式] 加密需要发送的业务数据，并带上自己的公钥 [公式] 一起发送给 server\n  server 计算 [公式]，根据笛福赫尔曼密钥交换的原理可以证明两端计算的 [公式]是一样的\n  这里不能使用 [公式] 作为后续通讯的密钥（下面解释），server 需要生成一个新的私钥 [公式] ，并计算新公钥 [公式] ，然后计算新的通讯密钥 [公式]\n  server 用 [公式] 加密需要返回的业务数据，并带上自己的新公钥 [公式] 一起发送给 client\n  client 根据新的 server 公钥计算通讯密钥 [公式] ，并用 [公式] 解密收到的数据包\n  之后双方使用 [公式] 作为密钥进行通讯，直到本次连接结束\n  可以看到，首次连接的时候，在第 3 步时，就已经开始发送实际的业务数据了，而第 1 步和第 2 步正好一去一回花费了 1RTT 时间，所以，首次连接的成本是 1RTT\n非首次连接\nclient 在首次连接后，会把 server 的 [公式] 存下，之后再次发起连接时，因为已经有 [公式] 了，可以直接从上面的第 3 步开始，而这一步已经可以发送业务数据了，所以，非首次连接时，QUIC 可以做到 0RTT\nK1 存在的必要性 为什么要再生成一个[公式] ，不能直接用 [公式] 作为后续通讯的密钥？\nserver 的 [公式] 是静态配置的，是可以长期使用的，其 [公式] 和 [公式] 是提前生成计算好的，为了等待后续 client 连接时计算 [公式] ，[公式] 是不能被销毁的。\n想想上面提到的前向安全性，如果攻击者事先记录下了所有通讯过程中的数据包，而后续 server 的 [公式] 泄漏，那就可以根据公开的 [公式] 算出 [公式] ，这样后续的通讯内容就全都可以解密了。而 [公式] 是由双方动态生成的公私钥对计算得来的，最迟在通讯结束后，双方的临时公私钥对就会销毁，从根本上杜绝了泄漏的可能。\n换句话说，使用 [公式] 作为通讯密钥，未来万一静态配置在 server 的私钥泄漏，那 [公式] 也就泄漏了，所有历史消息都将泄漏；使用 [公式] 作为通讯密钥，双方私钥在短时间内就会被销毁， [公式] 不会泄漏，历史消息的安全性就得到了保障。\n","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/quic/","summary":"我其实是个缝合怪，UDP + TLS + HTTP/2","title":"未来传输协议之星-QUIC"},{"content":"红黑树 为什么要红黑树 避免二叉树长短腿\n基本特性  结点是红色或黑色。 根结点是黑色。 每个叶子结点都是黑色的空结点（NIL结点）。 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点) 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。 红黑树从根到叶子节点的最长路径不会超过最短路径的两倍  红黑树的调整 在插入，删除的过程中，会破坏红黑树的规则，因此需要进行调整，调整有三种操作，分别是变色，左旋和右旋\n","permalink":"https://lambertxiao.github.io/posts/data-struct/%E7%BA%A2%E9%BB%91%E6%A0%91/","summary":"IT界的老大难，面试手撕红黑树","title":"红黑树"},{"content":"线程 线程模型  线程与内核调度实体KSE的对应关系\n 常见线程模型  用户级线程模型(M:1)   此模型下的线程由用户级别的线程库全权管理，效率低下，线程的优先级形同虚设\n  内核级线程模型(1:1)   该模型下的线程是由内核负责管理的，内核的管理成本高，内核级别的线程创建切换同步花的时间更多\n  两级线程模型(M:N)   一个进程可与多个KSE相关联，进程中的线程并不与KSE一一对应，内核级线程 + 应用程序线程，内核和线程库相互合作\n 线程同步 互斥量mutex 必须要使用多个互斥量的时候怎么办？\n试锁定和回退，固定顺序锁\n条件变量 需要和互斥量配套使用，Wait等待通知，Signal单发通知，Broadcast广播通知\n","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%BA%BF%E7%A8%8B/","summary":"线程：我可是CPU调度的基本单位，进程是我爹","title":"线程相关"},{"content":"TCP协议  TCP是一个可靠的，面向连接的传输层协议\n 组成  一台主机上的缓存（包括发送，接收缓存），变量，与另一个进程连接的套接字 另一台主机上的缓存（包括发送，接收缓存），变量，与另一个进程连接的套接字  报文结构  序号  当要发送的数据大小超过MSS时，数据会分包，每个包就需要有序号来标明先后顺序；初始序号时随机选择的，防止碰撞旧连接\n 确认号  确认发出方希望接受到的下一字节的序号\n超时相关 RTT： 往返时间\nSampleRTT\nEstimatedRTT = 0.875 * EstimatedRTT + 0.125 * SampleRTT\nDevRTT\nTimeoutInterval(超时间隔) = EstimatedRTT + 4*DevRTT\n重传 每次超时时，TCP重传具有最小序号的还未被确认的报文段\nACK  累计确认  某一方发出ACK为N的报文，则代表序号小于等于N的数据都收到了； 接收方攒一批有序报文段，一次性确认，一批里每个最多等待500ms\n 冗余ACK  当比期望序号大的失序报文到达时，检测出数据流中的间隔，立即发送冗余ACK，指明我期待的下一个序号\n 快速重传  一旦接收到3个冗余ACK，TCP就执行快速重传，即赶在超时定时器之前，重传报文段\n流量控制 使发送方的发送速率和接收方应用程序的读速率相匹配，防止把TCP连接两端的缓冲区撑爆\n 接收窗口  对于接收方，RecvWindow = RecvBuffer - [LastByteRecv - LastByteRead] 对于发送方，LastByteSent - LastByteAcked \u0026lt;= RecvWindow\n在 TCP 协议中，接收端的窗口的起始点是下一个要接收并且 ACK 的包，即便后来的包都到 了，放在缓存里面，窗口也不能右移，因为 TCP 的 ACK 机制是基于序列号的累计应答，一旦 ACK 了一 个系列号，就说明前面的都到了，所以只要前面的没到，后面的到了也不能 ACK，就会导致后面的到 了，也有可能超时重传，浪费带宽。\n 空闲通知  接收方的缓存区从满到有空闲时，需要在ACK里告诉发送方\n拥塞控制  网络上路由器的传输容量时有限的，缓存区也是有限的，缓存超了就丢掉了，操作系统默认的拥塞控制算法Reno\n 拥塞控制算法需要解决三个问题\n TCP发送方如何限制它向其连接发送流量的速率  通过传递一个叫拥塞窗口CongWin的变量，对于发送方，LastByteSent - LastByteAcked \u0026lt;= {CongWin, RecvWindow}， 拥塞控制的思想就是通过减小其拥塞窗口的大小，降低其发送速率\n二是发送方如何感知其到目的地之间的路径存在拥塞  要么出现超时，要么出现来自接收方的三个冗余ACK\n 发现拥塞之后用什么算法来改变其发送速率\n 加性增  每次收到一个确认后就把CongWin增大一点，其目标是在每个RTT内CongWin增加一个MSS\n 乘性减  每一次发现丢包就把拥塞窗口的大小减半，最后不能低于一个MSS\n 慢启动  TCP连接开始时，CongWin的初始值为一个MSS；每当一个传输的报文段被确认后，CongWin指数型增长；当发生超时时，重新进入慢启动阶段，CongWin又置为一个MSS\n  ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/tcp/","summary":"姜还是老的辣","title":"网络世界的传输基石-TCP"},{"content":"进程  一个程序的动态形式\n 进程间的通信方式   基于通讯\n  基于数据传递\n  管道\n  消息队列\n  socket\n      基于信号\n 操作系统的信号Signal    基于同步\n 信号灯    socket通信 通信域   ipv4域：在基于ipv4协议的网络中的任意两台计算机之上的两个应用程序\n  ipv6域\n  unix域：在同一台计算机上的两个应用程序\n  数据形式   数据报\n  字节流\n  父子进程 父进程可以通过fork的方式创建子进程\n  子进程会获得父进程的数据段，堆和栈的副本，并共享代码段\n  子进程对于它的副本的修改对其父进程和兄弟进程都是不可见的\n  copy on write是用来提高进程创建效率的\n  所有的进程共同组成了一个树状结构，内核启动进程作为根\n  进程标识符（pcb）\n  进程PCB 操作系统存放进程的管理和控制信息的数据结构称为进程控制块\n  程序id\n  特征信息\n区分系统进程，用户进程，内核进程\n  进程状态\n  优先级\n  通信信息\n  现场保护区\n用来保护阻塞的进程\n  资源需求，分配控制信息\n  进程尸体信息\n指明程序路径和名称，进程数据在物理内存还是在交换分页中\n  其他信息\n工作单位，工作区，文件信息\n  进程的状态  可运行状态   将要，立刻或正在cpu上运行，由进程调度器决定\n  可中断的睡眠状态   当进程等待某个事件的时候会进入该状态\n  不可中断的睡眠状态   发送给此种状态的进程的信号，得等进程从此状态转出才会被传递到\n   暂停状态或跟踪状态\n  僵尸状态\n   进程即将结束，绝大多数资源已被回收\n  退出状态  进程的状态转化 stateDiagram state 是否可中断  state 是否正常退出  [*] -- 可运行状态: 创建 可运行状态 -- 是否可中断: 阻塞 是否可中断 -- 可中断的睡眠状态: 可中断 是否可中断 -- 不可中断的睡眠状态: 不可中断 可中断的睡眠状态 -- 可运行状态: 恢复 不可中断的睡眠状态 -- 可运行状态: 恢复 可运行状态 -- 暂停状态或调试状态: 挂起或调试 暂停状态或调试状态 -- 可运行状态: 恢复或退出调试 可运行状态 -- 是否正常退出: 结束 是否正常退出 -- 退出状态: 父进程忽略sigchld信号或被分离 是否正常退出 -- 僵尸状态: 默认情况 僵尸状态 -- [*] 退出状态 -- [*]  系统调用  用户进程调用内核接口的行为被称为系统调用; 系统调用引起进程从用户态到内核态的转化，也就是进程可以访问内核空间了\n 进程调度  同一时刻只有一个进程在cpu上运行\n 关于同步   原子操作: 不能被中断的操作\n  临界区: 只能被串行化的访问或执行的某个资源或代码片段\n  进程的数据: 存在虚拟内存中\n  ","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BF%9B%E7%A8%8B/","summary":"进程：我可是操作系统调度的基本单位，线程是我儿子","title":"进程相关"},{"content":"使用expect可以帮我们完成一些需要交互的终端命令，比如，在用密码ssh登录服务器时，总是需要手动输入密码，可以用以下方法实现自动登录\n#!/usr/bin/expect -f  set username [lindex $argv 0] set host [lindex $argv 1] set port [lindex $argv 2] set password [lindex $argv 3] spawn ssh -p $port \u0026#34;$username@$host\u0026#34; expect \u0026#34;password\u0026#34; send \u0026#34;$password\\r\u0026#34; interact 外部调用\n/usr/bin/expect -f login_with_password.sh $user $publicIp $port $password ","permalink":"https://lambertxiao.github.io/posts/_posts/2020-06-09-expect%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/","summary":"使用expect可以帮我们完成一些需要交互的终端命令，比如，在用密码ssh登录服务器时，总是需要手动输入密码，可以用以下方法实现自动登录\n#!/usr/bin/expect -f  set username [lindex $argv 0] set host [lindex $argv 1] set port [lindex $argv 2] set password [lindex $argv 3] spawn ssh -p $port \u0026#34;$username@$host\u0026#34; expect \u0026#34;password\u0026#34; send \u0026#34;$password\\r\u0026#34; interact 外部调用\n/usr/bin/expect -f login_with_password.sh $user $publicIp $port $password ","title":"expect的简单使用"},{"content":"async function a(ctx, next) { console.log(\u0026#34;invoke a\u0026#34;) await next() console.log(\u0026#34;exit a\u0026#34;) } async function b(ctx, next) { console.log(\u0026#34;invoke b\u0026#34;) await next() console.log(\u0026#34;exit b\u0026#34;) } function compose(middleware) { return (ctx, next) =\u0026gt; { let index = -1 async function dispatch(i) { if (i \u0026lt;= index) { throw new Error(\u0026#39;next() called multiple times\u0026#39;) } index = i let fn = middleware[i] if (i === middleware.length) { fn = next } if (!fn) { return } return await fn(ctx, () =\u0026gt; { return dispatch(i + 1) }) } return dispatch(0) } } let func = compose([a, b]) func(\u0026#39;context\u0026#39;, () =\u0026gt; { console.log(\u0026#34;all done\u0026#34;) }) ","permalink":"https://lambertxiao.github.io/posts/_posts/2020-06-05-koa%E7%9A%84%E6%B4%8B%E8%91%B1%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%9E%8B/","summary":"async function a(ctx, next) { console.log(\u0026#34;invoke a\u0026#34;) await next() console.log(\u0026#34;exit a\u0026#34;) } async function b(ctx, next) { console.log(\u0026#34;invoke b\u0026#34;) await next() console.log(\u0026#34;exit b\u0026#34;) } function compose(middleware) { return (ctx, next) =\u0026gt; { let index = -1 async function dispatch(i) { if (i \u0026lt;= index) { throw new Error(\u0026#39;next() called multiple times\u0026#39;) } index = i let fn = middleware[i] if (i === middleware.length) { fn = next } if (!fn) { return } return await fn(ctx, () =\u0026gt; { return dispatch(i + 1) }) } return dispatch(0) } } let func = compose([a, b]) func(\u0026#39;context\u0026#39;, () =\u0026gt; { console.","title":"koa的洋葱设计模型"},{"content":"踩坑集锦 此篇文章用来记录维护k8s过程中的一系列问题\n 容器端口被占用 Bind for 0.0.0.0:9876 failed: port is already allocated.  找到端口占用的进程  sudo lsof -i -P -n | grep 9000  杀掉进程\n  重启docker\n   kubectl logs 超时 错误类似：\nError from server: Get https://172.31.27.3:10250/containerLogs/prod-sfox/a-group-miner-2hshz/miner-idx-1?follow=true\u0026amp;tailLines=20: dial tcp 172.31.27.3:10250: connect: connection timed out   该操作走内网ip，节点间内网不同\n  10250端口未打开\n   DNS解析失败 具体错误信息一般如下：\npanic: dial tcp: lookup mysql on 10.96.0.10:53: no such host   查看coredns的日志，判断集群内的dns解析是否成功\n  如果集群内成功，是往外网的dns解析出错，检查宿主机的 /etc/resolv.conf  配置\n   Helm报错 UPGRADE FAILED Error: \u0026quot;dev-chain\u0026quot; has no deployed releases Error: UPGRADE FAILED: \u0026quot;dev-chain\u0026quot; has no deployed releases 原因是helm的bug，如果helm有一次deploy的失败记录就没法重新deploy, 需要手动通过 helm delete 命令删除错误的记录\n 容器组里的服务获取不到客户端真实IP 如果你的服务暴露方式是NodePort，当客户端访问IP所属的Node和服务所在的Node不是同一个Node时，因为内部有SNAT和DNAT的存在， 客户端的源IP会丢失，解决方法，在Service的配置中添加 externalTrafficPolicy, 并用 nodeSelector 将pod固定在nodePort对应的同一台机器上\napiVersion: v1 kind: Service metadata: namespace: dev name: signal spec: externalTrafficPolicy: Local type: NodePort ports: - port: 8080 targetPort: 8080 name: http nodePort: 30010 selector: app: signal  docker启动容器报错 docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused \u0026quot;process_linux.go:301: running exec setns process for init caused “exit status 40\u0026quot;”: unknown. 解决方法:\necho 1 \u0026gt; /proc/sys/vm/drop_caches echo vm.min_free_kbytes=1048576 \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p  公用解决方法   查看kubelet日志\nsystemctl status kubelet journalctl -xu kubelet   查看docker的日志\nsystemctl status docker journalctl -xu docker   starting metrics server failed: listen tcp 127.0.0.1:10249: bind: address already in use 重启kube-proxy\n","permalink":"https://lambertxiao.github.io/posts/_posts/2020-06-02-docker%E8%B8%A9%E5%9D%91%E9%9B%86%E9%94%A6/","summary":"踩坑集锦 此篇文章用来记录维护k8s过程中的一系列问题\n 容器端口被占用 Bind for 0.0.0.0:9876 failed: port is already allocated.  找到端口占用的进程  sudo lsof -i -P -n | grep 9000  杀掉进程\n  重启docker\n   kubectl logs 超时 错误类似：\nError from server: Get https://172.31.27.3:10250/containerLogs/prod-sfox/a-group-miner-2hshz/miner-idx-1?follow=true\u0026amp;tailLines=20: dial tcp 172.31.27.3:10250: connect: connection timed out   该操作走内网ip，节点间内网不同\n  10250端口未打开\n   DNS解析失败 具体错误信息一般如下：\npanic: dial tcp: lookup mysql on 10.96.0.10:53: no such host   查看coredns的日志，判断集群内的dns解析是否成功","title":"docker踩坑集锦"},{"content":"使用以下参数，不在local端去解析域名\n--socks5-hostname HOST[:PORT] SOCKS5 proxy, pass host name to proxy --socks5-hostname 等同于 -x socks5h\n示例:\ncurl -x socks5h://127.0.0.1:36000 https://www.google.com.hk ","permalink":"https://lambertxiao.github.io/posts/_posts/2020-05-29-curl%E4%BD%BF%E7%94%A8socks5%E4%BB%A3%E7%90%86%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF/","summary":"使用以下参数，不在local端去解析域名\n--socks5-hostname HOST[:PORT] SOCKS5 proxy, pass host name to proxy --socks5-hostname 等同于 -x socks5h\n示例:\ncurl -x socks5h://127.0.0.1:36000 https://www.google.com.hk ","title":"curl使用socks5代理的正确姿势"},{"content":"先将内容排序, 排序后计数, 再按数量排序\nsort | uniq -c | sort -rnk 1 r表示逆向排序， n表示按数值排序， k表示按第k列进行排序\n","permalink":"https://lambertxiao.github.io/posts/_posts/2020-05-25-shell%E5%88%86%E7%BB%84%E8%AE%A1%E6%95%B0/","summary":"先将内容排序, 排序后计数, 再按数量排序\nsort | uniq -c | sort -rnk 1 r表示逆向排序， n表示按数值排序， k表示按第k列进行排序","title":"Shell分组计数"},{"content":"五种IO模型 同步或异步? 阻塞或非阻塞? 拿小明用水壶烧水举个栗子\n 同步\n 小明在烧水, 水壶比较原始, 水烧开了不会通知, 小明需要自己打开盖子看看水是不是烧开了\n 异步\n 小明又在烧水, 这次的新水壶水烧开了会发出\u0026quot;滴滴滴\u0026quot;响的功能, 小明将水放入壶里后, 就走开了, 等听到水壶通知了再回来\n 阻塞\n 小明的烧水壶很原始, 烧水的时候需要一直摁着开关, 不能干其他的事(即一烧水就要水烧开了才能走)\n 非阻塞\n 小明在等待烧水的过程中, 可以边玩手机边等待\n关注点不同 同步/异步关注的是消息如何通知\n  同步: 自己请求\n  异步: 等待通知\n  阻塞／非阻塞关注的是等待消息通知时的状态\n  阻塞: 其他啥事都不能干\n  非阻塞: 等待通知时可以干别的事\n  IO模型  IO 即数据的读取或写入操作，通常用户进程中的一个完整IO分为两阶段：用户进程空间 \u0026lt;=\u0026gt; 内核空间、内核空间 \u0026lt;=\u0026gt; 设备空间（磁盘、网络等）\n 阻塞IO 进程发起IO系统调用后，进程被阻塞，转到内核空间处理，整个IO处理完毕后返回进程。操作成功则进程获取到数据。\n非阻塞IO 进程发起IO系统调用后，如果内核缓冲区没有数据，需要到IO设备中读取，进程返回一个错误而不会被阻塞；进程发起IO系统调用后，如果内核缓冲区有数据，内核就会把数据返回进程。\nIO多路复用 多个的进程的IO可以注册到一个复用器（select）上，然后用一个进程调用该select， select会监听所有注册进来的IO. 如果select没有监听的IO在内核缓冲区都没有可读数据，select调用进程会被阻塞；而当任一IO在内核缓冲区中有可数据时，select调用就会返回, 而后select调用进程可以自己或通知另外的进程（注册进程）来再次发起读取IO，读取内核中准备好的数据; 多个进程注册IO后，只有另一个select调用进程被阻塞\n信号驱动IO 当进程发起一个IO操作，会向内核注册一个信号处理函数，然后进程返回不阻塞；当内核数据就绪时会发送一个信号给进程，进程便在信号处理函数中调用IO读取数据。\n异步IO 当进程发起一个IO操作，进程返回（不阻塞），但也不能返回结果；内核把整个IO处理完后，会通知进程结果。如果IO操作成功则进程直接获取到数据。\n","permalink":"https://lambertxiao.github.io/posts/%E4%BA%94%E7%A7%8Dio%E6%A8%A1%E5%9E%8B/%E4%BA%94%E7%A7%8Dio%E6%A8%A1%E5%9E%8B/","summary":"同步异步、阻塞非阻塞，傻傻分不清","title":"五种IO模型"},{"content":"Socks5 概念 Socks5是一种网络传输协议, 用于客户端与(代理)服务器通讯使用\n三个阶段 认证阶段 socks5比socks4多了一个认证阶段, 客户端必须将自己使用的socks版本, 加密方式等信息发送给服务端(以节点为单位, 总共258)\n   VER NMETHODS METHODS     1 1 1-255      VER\n使用的socks协议的版本号, 当前最新的版本号是5\n  NMETHODS\nMETHODS 字段占用的字节数\n  METHODS\nMETHODS是客户端支持的认证方式列表，每个方法占1字节。当前的定义是：\n  0x00 不需要认证\n  0x01 GSSAPI\n  0x02 用户名、密码认证\n  0x03 - 0x7F 由IANA分配（保留）\n  0x80 - 0xFE 为私人方法保留\n  0xFF 无可接受的方法\n    服务端从客户端提供的方法中选择一个并通过以下消息通知客户端:\n   VER METHOD     1 1      VER是使用的socks协议的版本号, 当前最新的版本号是5\n  METHOD是服务端选中的方法, 如果返回0xFF表示没有一个认证方法被选中, 客户端需要管理连接\n  连接阶段 认证完成后, 客户端向服务器发送请求, 告诉服务器自己要连接的目标, 客户端请求信息如下:\n   VER CMD RSV ATYP DST.ADDR DST.PORT     1 1 0x00 地址类型 目标地址 目标端口      CMD是sock的命令码\n  0x01表示CONNECT请求\n  0x02表示BIND请求\n  0x03表示UDP转发\n    RSV 0x00, 保留字段\n  ATYP\n  0x01 IPV4地址\n  0x03 域名\n  0x04 IPV6地址\n    DST.ADDR\n  ATYP为0x01, 则为IPv4地址\n  ATYP为0x03，则DST.ADDR部分第一个字节为域名长度，DST.ADDR剩余的内容为域名，没有\\0结尾。\n  ATYP为0x04, 则为IPv6地址\n    服务端响应如下:\n   VER REP RSV ATYP BND.ADDR BND.PORT      1 1 0x00 地址类型 动态 2       REP应答阶段\n  X'00' succeeded\n  X'01' general SOCKS server failure\n  X'02' connection not allowed by ruleset\n  X'03' Network unreachable\n  X'04' Host unreachable\n  X'05' Connection refused\n  X'06' TTL expired\n  X'07' Command not supported\n  X'08' Address type not supported\n  X'09' to X\u0026rsquo;FF' unassigned\n    传输阶段 传输阶段socks5服务器只做单纯的转发功能\n代码实现 远端代理实现 package ss import ( \u0026#34;encoding/binary\u0026#34; \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; ) // LsServer ss服务 type LsServer struct { ListenAddr *net.TCPAddr } // NewLsServer 创建一个server func NewLsServer(address string) (*LsServer, error) { addr, err := net.ResolveTCPAddr(\u0026#34;tcp\u0026#34;, address) if err != nil { return nil, err } return \u0026amp;LsServer{ListenAddr: addr}, nil } func (s *LsServer) handleConn(localConn *net.TCPConn) { defer localConn.Close() buffer := make([]byte, 256) /** 认证阶段 +----+----------+----------+ |VER | NMETHODS | METHODS | +----+----------+----------+ | 1 | 1 | 1 to 255 | +----+----------+----------+ **/ _, err := localConn.Read(buffer) // 第一个字段VER代表Socks的版本，Socks5默认为0x05，其固定长度为1个字节 \tif err != nil \u0026amp;\u0026amp; err != io.EOF { return } if buffer[0] != 0x05 { log.Println(buffer) log.Println(\u0026#34;仅支持socks5\u0026#34;) return } /** +----+--------+ |VER | METHOD | +----+--------+ | 1 | 1 | +----+--------+ **/ // 0x00表示不需要认证 \tlocalConn.Write([]byte{0x05, 0x00}) /** 连接阶段 +----+-----+-------+------+----------+----------+ |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | +----+-----+-------+------+----------+----------+ | 1 | 1 | X\u0026#39;00\u0026#39; | 1 | Variable | 2 | +----+-----+-------+------+----------+----------+ **/ n, err := localConn.Read(buffer) if err != nil \u0026amp;\u0026amp; n \u0026lt; 7 { return } // 判断CMD, 目前仅支持 CONNECT \tif buffer[1] != 0x01 { return } // 解析请求的IP端口 \tvar dstIP []byte switch buffer[3] { case 0x01: dstIP = buffer[4 : 4+net.IPv4len] case 0x03: ipAddr, err := net.ResolveIPAddr(\u0026#34;ip\u0026#34;, string(buffer[5:n-2])) if err != nil { return } dstIP = ipAddr.IP case 0x04: dstIP = buffer[4 : 4+net.IPv6len] default: return } dstPort := buffer[n-2:] dstAddr := \u0026amp;net.TCPAddr{ IP: dstIP, Port: int(binary.BigEndian.Uint16(dstPort)), } // 连接真正的远程服务器 \tserverConn, err := net.DialTCP(\u0026#34;tcp\u0026#34;, nil, dstAddr) if err != nil { log.Println(\u0026#34;连接目标地址错误\u0026#34;, err) return } defer serverConn.Close() /** +----+-----+-------+------+----------+----------+ |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | +----+-----+-------+------+----------+----------+ | 1 | 1 | X\u0026#39;00\u0026#39; | 1 | Variable | 2 | +----+-----+-------+------+----------+----------+ */ // 响应客户端请求 \tlocalConn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) // 传输阶段 \tgo func() { err := transferConnData(localConn, serverConn) if err != nil { // log.Println(\u0026#34;传输数据错误\u0026#34;, err) \tlocalConn.Close() serverConn.Close() } }() transferConnData(serverConn, localConn) } func transferConnData(conn1 *net.TCPConn, conn2 *net.TCPConn) error { buffer := make([]byte, 1024) for { readCount, err := conn1.Read(buffer) if err != nil { // 如果不是正常读完数据 \tif err != io.EOF { return err } return nil } if readCount \u0026gt; 0 { writeCount, err := conn2.Write(buffer[0:readCount]) if err != nil { return err } if readCount != writeCount { return io.ErrShortWrite } } } } // Listen 启动监听 func (s *LsServer) Listen() error { listener, err := net.ListenTCP(\u0026#34;tcp\u0026#34;, s.ListenAddr) if err != nil { return err } defer listener.Close() for { conn, err := listener.AcceptTCP() if err != nil { log.Println(\u0026#34;监听tcp请求出错\u0026#34;, err) continue } go s.handleConn(conn) } } 本地代理实现 package ss import ( \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; ) // LsLocal 本地端 type LsLocal struct { ListenAddr *net.TCPAddr RemoteAddr *net.TCPAddr } // NewLsLocal 创建一个本地端监听浏览器等请求 func NewLsLocal(listenAddress string, remoteAddress string) (*LsLocal, error) { listenAddr, err := net.ResolveTCPAddr(\u0026#34;tcp\u0026#34;, listenAddress) if err != nil { return nil, err } remoteAddr, err := net.ResolveTCPAddr(\u0026#34;tcp\u0026#34;, remoteAddress) if err != nil { return nil, err } return \u0026amp;LsLocal{ListenAddr: listenAddr, RemoteAddr: remoteAddr}, nil } func (local *LsLocal) handleConn(userConn *net.TCPConn) { defer userConn.Close() // 先连接代理服务器 \tlocalConn, err := net.DialTCP(\u0026#34;tcp\u0026#34;, nil, local.RemoteAddr) if err != nil { log.Println(\u0026#34;连接代理服务器错误:　\u0026#34;, err.Error()) return } defer localConn.Close() // 开始传输, local只需负责打通客户端程序和代理服务器 \tgo func() { err := transferConnData(userConn, localConn) if err != nil { // log.Println(\u0026#34;传输数据错误\u0026#34;, err.Error()) \tuserConn.Close() localConn.Close() } }() transferConnData(localConn, userConn) } // Listen 监听来自浏览器等应用的请求 func (local *LsLocal) Listen() error { listener, err := net.ListenTCP(\u0026#34;tcp\u0026#34;, local.ListenAddr) if err != nil { return err } defer listener.Close() for { conn, err := listener.AcceptTCP() if err != nil { log.Println(\u0026#34;监听tcp请求出错\u0026#34;, err) continue } go local.handleConn(conn) } } 启动远端代理 package main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/sfox/proxy/ss\u0026#34; ) func main() { address := \u0026#34;localhost:3000\u0026#34; s, err := ss.NewLsServer(address) if err != nil { log.Println(err) } log.Println(\u0026#34;Server listen on \u0026#34; + address) err = s.Listen() if err != nil { log.Println(err) } } 启动本地代理: package main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/sfox/proxy/ss\u0026#34; ) func main() { local, err := ss.NewLsLocal(\u0026#34;localhost:3001\u0026#34;, \u0026#34;localhost:3000\u0026#34;) if err != nil { log.Println(err) return } err = local.Listen() if err != nil { log.Println(err) return } } 使用curl测试 curl --socks5 localhost:3001 https://www.google.com/ curl可能会提前将请求的域名解析成IP从而造成访问不通, 可以使用以下命令替换:\ncurl -x socks5h://localhost:8080 https://www.google.com ","permalink":"https://lambertxiao.github.io/posts/_posts/2019-12-10-socks5/","summary":"Socks5 概念 Socks5是一种网络传输协议, 用于客户端与(代理)服务器通讯使用\n三个阶段 认证阶段 socks5比socks4多了一个认证阶段, 客户端必须将自己使用的socks版本, 加密方式等信息发送给服务端(以节点为单位, 总共258)\n   VER NMETHODS METHODS     1 1 1-255      VER\n使用的socks协议的版本号, 当前最新的版本号是5\n  NMETHODS\nMETHODS 字段占用的字节数\n  METHODS\nMETHODS是客户端支持的认证方式列表，每个方法占1字节。当前的定义是：\n  0x00 不需要认证\n  0x01 GSSAPI\n  0x02 用户名、密码认证\n  0x03 - 0x7F 由IANA分配（保留）\n  0x80 - 0xFE 为私人方法保留\n  0xFF 无可接受的方法\n    服务端从客户端提供的方法中选择一个并通过以下消息通知客户端:","title":"Socks5"},{"content":"什么是https 首先, 在讲我们为什么需要https之前, 先看看以下两个流程, 描述了A和B之间想要互相通信, 又想让通信的内容安全\n流程1   A将对称秘钥发送给B\n  B使用该秘钥加密数据, 并发送给A\n  A用秘钥解开B发送的内容, AB成功通信\n  缺陷: 当中间人C截获了首次通信时, A发送给B的秘钥, 则AB后续所有的交互信息都能被C解开\n流程2   A将自己的公钥给B\n  B收到A的公钥后, 用A的公钥加密了 B自己生成对称秘钥, 并将加密后的内容回给了B\n  当A收到来自B的回复, A用自己的私钥解开了B的加密内容, 从而得到了B的对称秘钥, 后续通信使用该对称秘钥加密内容\n  缺陷:\n中间人C可在截获了A和B的首次通信后, 用自己的公钥给了B, B收到请求后,他不知道公钥是C发过来的,从而用了C的公钥加密了对称秘钥并回给了C, C拿到内容后,用自己的私钥解开内容,从而拿到了B的对称秘钥\n这个流程的缺陷主要是A和B都无法确定收到的数据是来源与对方的, 而不是来源于中间人的.\n引入证书 内容:\n  证书颁发机构\n  服务端网址\n  机构私钥加密(服务端公钥)\n  机构私钥加密(证书签名)\n  流程3 此时假设A作为服务端\n  A把自己的公钥给证书颁发机构\n  机构也有自己的一对公钥私钥, 机构用私钥加密 A的公钥, 并通过服务端网址等信息生成了一个证书签名,证书签名同样经过 机构的私钥加密.证书制作完成后把证书发给服务端的A\n  这时当A想和B通信时,A不再将自己的公钥给B, 而是将证书给B\n  B收到证书之后, 首先要辨别真伪\n由于证书是机构签发的, 各大浏览器和操作系统都维护了所有权威证书机构的名称和公钥,所以B只要知道是哪个机构颁发的证书,就可以找到对应的公钥, 解开证书得到证书签名\n  B利用CA的公钥解密数字签名，得到服务器信息的摘要，和原本的信息，再对原本的信息hash，和摘要对比来进行校验\n  B此时成功的拿到了来自于A的公钥,后续通信同流程2\n  流程3的关键在与, 引入了第三方权威机构, 从而让B确定自己收到的证书是来自于A的, 而不是中间人C伪造的.\n如果C在一开始劫持了证书为怎么样?\n  C替换为自己的证书\n当B收到后,B会验证证书不通过\n  C将证书再发给B, B回了信息后, 由于C无法没有A的私钥,即使收到了B的回复之后,也无法解开\n  以上流程就是https的主体思想, https在http协议的基础上增加了ssl安全层, 以上所有的一系列认证流程都是在ssl中完成的\n","permalink":"https://lambertxiao.github.io/posts/_posts/2019-12-03-%E4%BB%80%E4%B9%88%E6%98%AFhttps/","summary":"什么是https 首先, 在讲我们为什么需要https之前, 先看看以下两个流程, 描述了A和B之间想要互相通信, 又想让通信的内容安全\n流程1   A将对称秘钥发送给B\n  B使用该秘钥加密数据, 并发送给A\n  A用秘钥解开B发送的内容, AB成功通信\n  缺陷: 当中间人C截获了首次通信时, A发送给B的秘钥, 则AB后续所有的交互信息都能被C解开\n流程2   A将自己的公钥给B\n  B收到A的公钥后, 用A的公钥加密了 B自己生成对称秘钥, 并将加密后的内容回给了B\n  当A收到来自B的回复, A用自己的私钥解开了B的加密内容, 从而得到了B的对称秘钥, 后续通信使用该对称秘钥加密内容\n  缺陷:\n中间人C可在截获了A和B的首次通信后, 用自己的公钥给了B, B收到请求后,他不知道公钥是C发过来的,从而用了C的公钥加密了对称秘钥并回给了C, C拿到内容后,用自己的私钥解开内容,从而拿到了B的对称秘钥\n这个流程的缺陷主要是A和B都无法确定收到的数据是来源与对方的, 而不是来源于中间人的.\n引入证书 内容:\n  证书颁发机构\n  服务端网址\n  机构私钥加密(服务端公钥)\n  机构私钥加密(证书签名)\n  流程3 此时假设A作为服务端\n  A把自己的公钥给证书颁发机构","title":"什么是https"},{"content":"脚本命令使用 生产消息 ./kafka-console-producer.sh --broker-list kafkas:9092 --topic user_register 输入消息:\n{\u0026quot;accountname\u0026quot;:\u0026quot;test@qq.com\u0026quot;,\u0026quot;action\u0026quot;:\u0026quot;register\u0026quot;,\u0026quot;appname\u0026quot;:\u0026quot;CoolLine\u0026quot;,\u0026quot;appversion\u0026quot;:\u0026quot;1.6.xx\u0026quot;,\u0026quot;channelName\u0026quot;:\u0026quot;googleplay\u0026quot;,\u0026quot;cityen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;cityzh\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;continentsen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;continentszh\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;countryen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;countryZh\u0026quot;:\u0026quot;美国\u0026quot;,\u0026quot;createTimestamp\u0026quot;:1566376441,\u0026quot;deviceid\u0026quot;:\u0026quot;edb3d8ce8df595ad\u0026quot;,\u0026quot;isPrivilegedUser\u0026quot;:\u0026quot;true\u0026quot;,\u0026quot;line\u0026quot;:\u0026quot;CoolLine\u0026quot;,\u0026quot;mail\u0026quot;:\u0026quot;test@qq.com\u0026quot;,\u0026quot;pkgname\u0026quot;:\u0026quot;cc.coolline.client\u0026quot;,\u0026quot;platform\u0026quot;:\u0026quot;Android\u0026quot;,\u0026quot;provinceen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;provincezh\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;pt\u0026quot;:\u0026quot;2019-08-21\u0026quot;,\u0026quot;registerip\u0026quot;:\u0026quot;10.244.2.64\u0026quot;,\u0026quot;registertime\u0026quot;:\u0026quot;2019-08-21 16:34:01\u0026quot;,\u0026quot;registertype\u0026quot;:\u0026quot;mail\u0026quot;,\u0026quot;userid\u0026quot;:19348,\u0026quot;userName\u0026quot;:\u0026quot;test@qq.com\u0026quot;,\u0026quot;userpwd\u0026quot;:\u0026quot;86A8F132223D033619389988E663F6C2\u0026quot;,\u0026quot;userstate\u0026quot;:0,\u0026quot;virtualCountryCode\u0026quot;:\u0026quot;AF\u0026quot;} 消费主题 ./kafka-console-consumer.sh --bootstrap-server kafkas:9092 --topic user_register --from-beginning 设置某个主题的消息缓存时间 ./kafka-configs.sh --zookeeper zookeeper:2181 --alter --entity-name ${主题} --entity-type topics --add-config retention.ms=86400000 立即生效 ./kafka-topics.sh --zookeeper zookeeper:2181 --alter --topic ${主题} --config cleanup.policy=delete ","permalink":"https://lambertxiao.github.io/posts/kafak%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%91%BD%E4%BB%A4/doc/","summary":"脚本命令使用 生产消息 ./kafka-console-producer.sh --broker-list kafkas:9092 --topic user_register 输入消息:\n{\u0026quot;accountname\u0026quot;:\u0026quot;test@qq.com\u0026quot;,\u0026quot;action\u0026quot;:\u0026quot;register\u0026quot;,\u0026quot;appname\u0026quot;:\u0026quot;CoolLine\u0026quot;,\u0026quot;appversion\u0026quot;:\u0026quot;1.6.xx\u0026quot;,\u0026quot;channelName\u0026quot;:\u0026quot;googleplay\u0026quot;,\u0026quot;cityen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;cityzh\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;continentsen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;continentszh\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;countryen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;countryZh\u0026quot;:\u0026quot;美国\u0026quot;,\u0026quot;createTimestamp\u0026quot;:1566376441,\u0026quot;deviceid\u0026quot;:\u0026quot;edb3d8ce8df595ad\u0026quot;,\u0026quot;isPrivilegedUser\u0026quot;:\u0026quot;true\u0026quot;,\u0026quot;line\u0026quot;:\u0026quot;CoolLine\u0026quot;,\u0026quot;mail\u0026quot;:\u0026quot;test@qq.com\u0026quot;,\u0026quot;pkgname\u0026quot;:\u0026quot;cc.coolline.client\u0026quot;,\u0026quot;platform\u0026quot;:\u0026quot;Android\u0026quot;,\u0026quot;provinceen\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;provincezh\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;pt\u0026quot;:\u0026quot;2019-08-21\u0026quot;,\u0026quot;registerip\u0026quot;:\u0026quot;10.244.2.64\u0026quot;,\u0026quot;registertime\u0026quot;:\u0026quot;2019-08-21 16:34:01\u0026quot;,\u0026quot;registertype\u0026quot;:\u0026quot;mail\u0026quot;,\u0026quot;userid\u0026quot;:19348,\u0026quot;userName\u0026quot;:\u0026quot;test@qq.com\u0026quot;,\u0026quot;userpwd\u0026quot;:\u0026quot;86A8F132223D033619389988E663F6C2\u0026quot;,\u0026quot;userstate\u0026quot;:0,\u0026quot;virtualCountryCode\u0026quot;:\u0026quot;AF\u0026quot;} 消费主题 ./kafka-console-consumer.sh --bootstrap-server kafkas:9092 --topic user_register --from-beginning 设置某个主题的消息缓存时间 ./kafka-configs.sh --zookeeper zookeeper:2181 --alter --entity-name ${主题} --entity-type topics --add-config retention.ms=86400000 立即生效 ./kafka-topics.sh --zookeeper zookeeper:2181 --alter --topic ${主题} --config cleanup.policy=delete ","title":"Kafka的基本命令"},{"content":"Cloc cloc是linux平台里可以统计代码的工具，并非简单的统计代码的行数，能同时针对各种语言做分类输出\n安装 sudo apt install cloc 使用 cloc $filePath 返回结果：\n$ cloc . 1339 text files. 1302 unique files. 160 files ignored. github.com/AlDanial/cloc v 1.74 T=3.75 s (321.1 files/s, 135799.4 lines/s) -------------------------------------------------------------------------------- Language files blank comment code -------------------------------------------------------------------------------- Go 940 39122 40781 354176 C 37 6806 9795 31924 Markdown 49 1854 0 6552 C/C++ Header 37 1330 3531 3864 Assembly 38 503 884 2402 YAML 70 145 18 1955 Bourne Shell 13 139 323 856 JSON 1 2 0 637 make 9 73 96 296 Protocol Buffers 3 38 27 165 SQL 1 1 0 155 Python 1 14 13 99 Bourne Again Shell 1 8 3 52 TOML 2 5 45 11 Dockerfile 1 1 0 8 -------------------------------------------------------------------------------- SUM: 1203 50041 55516 403152 -------------------------------------------------------------------------------- ","permalink":"https://lambertxiao.github.io/posts/_posts/2019-08-16-linux%E4%B8%8B%E7%BB%9F%E8%AE%A1%E4%BB%A3%E7%A0%81%E7%9A%84%E5%B7%A5%E5%85%B7cloc/","summary":"Cloc cloc是linux平台里可以统计代码的工具，并非简单的统计代码的行数，能同时针对各种语言做分类输出\n安装 sudo apt install cloc 使用 cloc $filePath 返回结果：\n$ cloc . 1339 text files. 1302 unique files. 160 files ignored. github.com/AlDanial/cloc v 1.74 T=3.75 s (321.1 files/s, 135799.4 lines/s) -------------------------------------------------------------------------------- Language files blank comment code -------------------------------------------------------------------------------- Go 940 39122 40781 354176 C 37 6806 9795 31924 Markdown 49 1854 0 6552 C/C++ Header 37 1330 3531 3864 Assembly 38 503 884 2402 YAML 70 145 18 1955 Bourne Shell 13 139 323 856 JSON 1 2 0 637 make 9 73 96 296 Protocol Buffers 3 38 27 165 SQL 1 1 0 155 Python 1 14 13 99 Bourne Again Shell 1 8 3 52 TOML 2 5 45 11 Dockerfile 1 1 0 8 -------------------------------------------------------------------------------- SUM: 1203 50041 55516 403152 -------------------------------------------------------------------------------- ","title":"Linux下统计代码的工具Cloc"},{"content":"Shell里的变量取值 # 假如 $file 没有设定，则使用 my.file.txt 作传回值。(空值及非空值时不作处理) ${file-my.file.txt} # 假如 $file 没有设定或为空值，则使用 my.file.txt 作传回值。 (非空值时不作处理) ${file:-my.file.txt} # 假如 $file 设为空值或非空值，均使用 my.file.txt 作传回值。(没设定时不作处理) ${file+my.file.txt} # 若 $file 为非空值，则使用 my.file.txt 作传回值。 (没设定及空值时不作处理) ${file:+my.file.txt} # 若 $file 没设定，则使用 my.file.txt 作传回值，同时将 $file 赋值为 my.file.txt 。 (空值及非空值时不作处理) ${file=my.file.txt} # 若 $file 没设定或为空值，则使用 my.file.txt 作传回值，同时将 $file 赋值为 my.file.txt 。 (非空值时不作处理) ${file:=my.file.txt} # 若 $file 没设定，则将 my.file.txt 输出至 STDERR。 (空值及非空值时不作处理) ${file?my.file.txt} # 若 $file 没设定或为空值，则将 my.file.txt 输出至 STDERR。 (非空值时不作处理) ${file:?my.file.txt} ","permalink":"https://lambertxiao.github.io/posts/_posts/2019-08-15-shell%E9%87%8C%E7%9A%84%E5%8F%98%E9%87%8F%E5%8F%96%E5%80%BC/","summary":"Shell里的变量取值 # 假如 $file 没有设定，则使用 my.file.txt 作传回值。(空值及非空值时不作处理) ${file-my.file.txt} # 假如 $file 没有设定或为空值，则使用 my.file.txt 作传回值。 (非空值时不作处理) ${file:-my.file.txt} # 假如 $file 设为空值或非空值，均使用 my.file.txt 作传回值。(没设定时不作处理) ${file+my.file.txt} # 若 $file 为非空值，则使用 my.file.txt 作传回值。 (没设定及空值时不作处理) ${file:+my.file.txt} # 若 $file 没设定，则使用 my.file.txt 作传回值，同时将 $file 赋值为 my.file.txt 。 (空值及非空值时不作处理) ${file=my.file.txt} # 若 $file 没设定或为空值，则使用 my.file.txt 作传回值，同时将 $file 赋值为 my.file.txt 。 (非空值时不作处理) ${file:=my.file.txt} # 若 $file 没设定，则将 my.file.txt 输出至 STDERR。 (空值及非空值时不作处理) ${file?my.file.txt} # 若 $file 没设定或为空值，则将 my.file.txt 输出至 STDERR。 (非空值时不作处理) ${file:?","title":"Shell取值表达式"},{"content":"","permalink":"https://lambertxiao.github.io/posts/_posts/docker-%E6%9D%82%E8%AE%B0/","summary":"","title":"Docker 杂记"},{"content":"NodeJS的module 对于 circle.js\n// 这种导出方式，相当于将变量地址导出，会被外界修改 exports.varA = \u0026#39;varA\u0026#39; exports.func1 = () =\u0026gt; { console.log(\u0026#39;func1\u0026#39;) } module.exports.varB = \u0026#39;varB\u0026#39; // 以上写法等价于 module.exports = { varA: \u0026#39;varA\u0026#39;, varB: \u0026#39;varB\u0026#39;, func1 () { console.log(\u0026#39;func\u0026#39;) } } 在 main.js 中可以这么引入\nlet math = require(\u0026#39;./utils/math\u0026#39;) console.log(math.varA) console.log(math.varB) math.func1() math.varA = \u0026#39;A\u0026#39;; math.varB = \u0026#39;B\u0026#39;; math.func1 = () =\u0026gt; { console.log(\u0026#39;another func\u0026#39;) } let math2 = require(\u0026#39;./utils/math\u0026#39;) // varA, varB 和 func1 都发生了改变，证明import进来的模块是单例的 console.log(math2.varA) console.log(math2.varB) math2.func1() ES6的module ES6 模块不是对象，而是通过export命令显式指定输出的代码，再通过import命令输入。\n// ES6模块 import { stat, exists, readFile } from \u0026#39;fs\u0026#39;; 上面代码的实质是从fs模块加载3个方法，其他方法不加载。这种加载称为“编译时加载”或者静态加载，即 ES6 可以在编译时就完成模块加载，效率要比 CommonJS 模块的加载方式高。当然，这也导致了没法引用 ES6 模块本身，因为它不是对象。\n模块功能主要由两个命令构成：export 和 import 。export命令用于规定模块的对外接口，import命令用于输入其他模块提供的功能。\n对于 math.js\nlet func1 = () =\u0026gt; { console.log(\u0026#39;func1\u0026#39;) } let varA = \u0026#39;varA\u0026#39; let defaultFunc = () =\u0026gt; { console.log(\u0026#39;defaultFunc\u0026#39;) } export { func1, varA } export let varB = \u0026#39;varB\u0026#39; export let func2 = () =\u0026gt; { console.log(\u0026#39;func2\u0026#39;) } export default defaultFunc 在 main.js 可以这么引入\nimport { func1, func2, varA, varB } from \u0026#39;./utils/string\u0026#39; import defaultFunc from \u0026#39;./utils/string\u0026#39; func1() func2() defaultFunc() console.log(varA) console.log(varB) ","permalink":"https://lambertxiao.github.io/posts/_posts/2019-08-14-nodejs%E4%B8%8Ees6%E7%9A%84%E6%A8%A1%E5%9D%97%E5%8C%96/","summary":"NodeJS的module 对于 circle.js\n// 这种导出方式，相当于将变量地址导出，会被外界修改 exports.varA = \u0026#39;varA\u0026#39; exports.func1 = () =\u0026gt; { console.log(\u0026#39;func1\u0026#39;) } module.exports.varB = \u0026#39;varB\u0026#39; // 以上写法等价于 module.exports = { varA: \u0026#39;varA\u0026#39;, varB: \u0026#39;varB\u0026#39;, func1 () { console.log(\u0026#39;func\u0026#39;) } } 在 main.js 中可以这么引入\nlet math = require(\u0026#39;./utils/math\u0026#39;) console.log(math.varA) console.log(math.varB) math.func1() math.varA = \u0026#39;A\u0026#39;; math.varB = \u0026#39;B\u0026#39;; math.func1 = () =\u0026gt; { console.log(\u0026#39;another func\u0026#39;) } let math2 = require(\u0026#39;./utils/math\u0026#39;) // varA, varB 和 func1 都发生了改变，证明import进来的模块是单例的 console.log(math2.varA) console.log(math2.varB) math2.func1() ES6的module ES6 模块不是对象，而是通过export命令显式指定输出的代码，再通过import命令输入。","title":"NodeJS与ES6的模块化"},{"content":"异步编程 callback瀑布级回调 Promise Generator Async 和 Await   在函数体前通过关键字async可以将函数变为async函数\n  在async函数中对需要异步执行的函数前需加await关键字\n  await后的函数必须使用Promise对象封装\n  async函数执行后返回的是一个Promise对象\n  NodeJs单线程是怎么保证效率的 ","permalink":"https://lambertxiao.github.io/posts/_posts/2019-08-06-nodejs-%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B/","summary":"异步编程 callback瀑布级回调 Promise Generator Async 和 Await   在函数体前通过关键字async可以将函数变为async函数\n  在async函数中对需要异步执行的函数前需加await关键字\n  await后的函数必须使用Promise对象封装\n  async函数执行后返回的是一个Promise对象\n  NodeJs单线程是怎么保证效率的 ","title":"nodejs-异步编程"},{"content":"什么是以太坊 简单来说，以太坊就是一个基于交易的状态机。什么是状态机呢？可以将其理解成一台机器，这台机器维护着一些状态，在以太坊交易发生时，这些状态会从一个状态转化到另一个状态。以太坊时基于区块链构建的，区块链上保存着状态和交易。当我们与以太坊交互时，其实就是在执行交易、改变系统状态。\n引用一个公式表示就是：\n σ′ =Υ(σ,T)\n Υ是状态转换函数，T是交易，σ是状态，σ′转换后的状态。\n从创世区块开始，无尽的交易不断的刷新着系统当前状态，每产生一个区块就对当前状态做一次快照（patricia trie根）存入区块头中。\n以太坊可以干什么？ 以太坊作为一个开发的区块链平台，它允许任何人在平台中建立和使用通过区块链技术运行的去中心化应用。可将之类比为Internet平台，而我们可以在其上面开发web网站。（但是似乎在以太坊上面只能开发智能合约？）\n什么是智能合约？ 智能合约就是以太坊平台上运行的程序。一旦事件触发合约中的条款，代码自动执行。智能合约的功能是由开发者自行设计的。\n智能合约的工作原理 构建 → 存储 → 执行\n  智能合约由区块链内的多个用户共同参与制定，可用于用户之间的任何交易行为。协议中明确了双方的权利和义务，开发人员将这些权利和义务以电子化的方式进行编程，代码中包含会触发合约自动执行的条件。比方说，你把一套闲置的房子租给A，那么，这份智能租约中就规定了A必须在每月5号之前给你打房租、你必须在收到房租时马上给对方钥匙［2］等条款。\n  一旦编码完成，这份智能合约就被上传到区块链网络上，即全网验证节点都会接收到你和A的租房合约。\n  智能合约会定期检查是否存在 相关事件和触发条件；满足条件的事件将会推送到待验证的队列中。假设A在4号提前打房租给你，这个事件就成了该合约的触发条件（每月5号以前）。\n  区块链上的验证节点先对该 事件进行签名验证 以确保其有效性；等大多数验证节点对该事件达成共识后，智能合约将成功执行，并通知用户。\n  成功执行的合约将移出区块。而未执行的合约则继续等待下一轮处理，直至成功执行。\n  部署到以太坊上的智能合约是要消耗以太币的。就好像把现实中的仲裁人、法官、执行人搬到了区块链上，尽管他们成了一行行的代码，但也是珍贵的计算机资源。智能合约也遵循“Less is more”，逻辑应尽可能地简单。逻辑越复杂，消耗的以太币就越多。\n既然执行要消费以太币，如何支付呢？智能合约是预支付，为了合约顺利执行，一般提前多打一点以太币。如果预支付的以太币不足以支撑整个执行过程，就算进行到半路，合约也会回到初始状态；并且消耗的以太币也不会退回给合约发起人。\n如何触发智能合约？ 智能合约本身规定了触发的条件，只要满足了条件的情况下，合约就会自动触发（比如时间到了自动缴房租）。也可以由外部账户触发。\n以太坊的区块结构 区块链上最重要的结构莫过于区块的结构，以太坊的区块结构和区块链的类似，但是又有着诸多不同。\n以太坊区块是由三大部分组成：区块头，叔块，交易列表。\n  区块头由15个字段组成。\n  叔块其实就是孤块，是由于某个区块上产生了多分支。因以太坊出块速度很快平均十几秒就会打包生成一个块，所以矿工挖矿的竞争性很高，可能同时产出几个都合法的区块，以太坊为了一些安全性起见，允许竞争块也挂在到主链上，同时给与挖出这些孤块的矿工们少许奖励增加工作的公平性。这些孤块最多允许6个高度，这也是6个区块确认主链说法的来源。\n  交易列表，存储的是本区块中所有的交易内容。\n  看一下一个实际的区块信息：\n\u0026quot;blocks\u0026quot; : [ { \u0026quot;blockHeader\u0026quot; : { \u0026quot;bloom\u0026quot; : \u0026quot;0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\u0026quot;, \u0026quot;coinbase\u0026quot; : \u0026quot;0x3535353535353535353535353535353535353535\u0026quot;, \u0026quot;difficulty\u0026quot; : \u0026quot;0x020000\u0026quot;, \u0026quot;extraData\u0026quot; : \u0026quot;\u0026quot;, \u0026quot;gasLimit\u0026quot; : \u0026quot;0x05f5e100\u0026quot;, \u0026quot;gasUsed\u0026quot; : \u0026quot;0x014fa1\u0026quot;, \u0026quot;hash\u0026quot; : \u0026quot;0x39f4659b079e257df8fd7e699528531e97a6b8a442ca0d11200c4a2f7433c483\u0026quot;, \u0026quot;mixHash\u0026quot; : \u0026quot;0x7379f33af4ae2db7e293f808a165135d0b1a99572cc96fb9f7d17ef64a751969\u0026quot;, \u0026quot;nonce\u0026quot; : \u0026quot;0x8e08d7aabeee8773\u0026quot;, \u0026quot;number\u0026quot; : \u0026quot;0x01\u0026quot;, \u0026quot;parentHash\u0026quot; : \u0026quot;0xadbef3bf0b3b7b14f6e7b1a45d240ecc863543a279a86c23f60170e8e7a6bcc3\u0026quot;, \u0026quot;receiptTrie\u0026quot; : \u0026quot;0xb21660268480338c0cd0613358315359b619bd527d5850949c4863cddaec316b\u0026quot;, \u0026quot;stateRoot\u0026quot; : \u0026quot;0xde4ce9b5b2f88ab1680962c64281224b1743bdf94bd6a9e390ea779ff616c1f7\u0026quot;, \u0026quot;timestamp\u0026quot; : \u0026quot;0x03e8\u0026quot;, \u0026quot;transactionsTrie\u0026quot; : \u0026quot;0x56445ba866f3e41851154fb8700dcec8556a178f1833021e030b8a47b494769d\u0026quot;, \u0026quot;uncleHash\u0026quot; : \u0026quot;0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\u0026quot; }, \u0026quot;rlp\u0026quot; : \u0026quot;0xf90308f901f9a0adbef3bf0b3b7b14f6e7b1a45d240ecc863543a279a86c23f60170e8e7a6bcc3a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347943535353535353535353535353535353535353535a0de4ce9b5b2f88ab1680962c64281224b1743bdf94bd6a9e390ea779ff616c1f7a056445ba866f3e41851154fb8700dcec8556a178f1833021e030b8a47b494769da0b21660268480338c0cd0613358315359b619bd527d5850949c4863cddaec316bb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018405f5e10083014fa18203e880a07379f33af4ae2db7e293f808a165135d0b1a99572cc96fb9f7d17ef64a751969888e08d7aabeee8773f90108f90105460183030d4094c305c901078781c232a2a521c2af7980f8385ee980b8a430c8d1da000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000230644e72e131a029b85045b68181585d2833e84879b9709143e1f593f00000001ba021a28cc82b40931239f8653ffa5300e1a506c0ef7fb79a663772cafe6558ab44a075af23441f7f176a2770af41142c77b671391209b15d59144e7a1332179b5e14c0\u0026quot;, \u0026quot;transactions\u0026quot; : [ { \u0026quot;data\u0026quot; : \u0026quot;0x30c8d1da000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000230644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000\u0026quot;, \u0026quot;gasLimit\u0026quot; : \u0026quot;0x030d40\u0026quot;, \u0026quot;gasPrice\u0026quot; : \u0026quot;0x01\u0026quot;, \u0026quot;nonce\u0026quot; : \u0026quot;0x46\u0026quot;, \u0026quot;r\u0026quot; : \u0026quot;0x21a28cc82b40931239f8653ffa5300e1a506c0ef7fb79a663772cafe6558ab44\u0026quot;, \u0026quot;s\u0026quot; : \u0026quot;0x75af23441f7f176a2770af41142c77b671391209b15d59144e7a1332179b5e14\u0026quot;, \u0026quot;to\u0026quot; : \u0026quot;0xc305c901078781c232a2a521c2af7980f8385ee9\u0026quot;, \u0026quot;v\u0026quot; : \u0026quot;0x1b\u0026quot;, \u0026quot;value\u0026quot; : \u0026quot;0x00\u0026quot; } ], \u0026quot;uncleHeaders\u0026quot; : [ ] } ] 区块头的结构 区块头包含15个字段，介绍如下：\ntype Header struct { ParentHash common.Hash //Hp，上一区块全部内容的hash，区块因它而成链！  UncleHash common.Hash //Ho，本区块的ommers（所有叔块）列表的hash  Coinbase common.Address //Hc，成功挖出本区块的矿工地址，用于接收矿工费  Root common.Hash //Hr，本区块所有交易的状态tree的根hash  TxHash common.Hash //Ht，本区块所有交易tree的根hash  ReceiptHash common.Hash //He，本区块所有交易的收据的tree的根hash  Bloom Bloom //Hb，交易收据日志组成的Bloom过滤器  Difficulty *big.Int //Hd，本区块难度级别  Number *big.Int //Hi，区块序号，从创世块0递增  GasLimit uint64 //Hl，每个区块当前的gas limit  GasUsed uint64 //Hg，本区块交易消耗的总gas  Time *big.Int //Hs，本区块创建时的Unix时间戳  Extra []byte //Hx，区块附加数据，\u0026lt;=32字节  MixDigest common.Hash //Hm，256位的hash，与nonce组合证明出块执行了足够的计算  Nonce BlockNonce //Hn，64位的hash，与MixDigest组合证明出块执行了足够的计算 } 有三个特别的字段保存的是patricia trie树的hash值，Root（状态hash）、TxHash（交易列表hash）、ReceiptHash(收据列表hash)。这个Root就是系统状态hash。系统状态就是以太坊整个网络中所有账户的状态，就是world state，它是一个merkle patricia trie结构。这个树（包括所有patricia trie）并不存在于区块，而存在于节点的levelDB中。只有它的根hash存在于区块头Root中，每一个区块头里的Root都是区块被挖出确认时的快照，而world state指现在所有账户的状态。\n什么是 world state？ world state是一颗全局状态树，被存储在每个节点的levelDB里，它被持续地更新。这棵树包含了以太坊网络里每一个账户的key/value映射。所以它表示的是整个以太坊系统所有账户当前的状态。树的一个叶子节点是一个key/value映射，key是KEC(a)即160位的账户地址的哈希，value是账户（nonce、balance、storageRoot、codeHash）的RLP格式序列化字节。\n什么是账户？ 以太坊中有两种账户\n  外部拥有账户（EOA），一般指自然人拥有的账户。\n  合约账户（CA），为智能合约分配的账户。\n  看一下账户的源码定义：\ntype Account struct { Nonce uint64 // 若为EOA是发送的交易序号，如为CA是合约创建的序号。  Balance *big.Int // 这个地址的余额。  Root common.Hash // 账户自身内容RPL编码组成的Merkle Trie的根哈希  CodeHash []byte // 账户绑定的EVM Code，账户一经创建不可修改。 } EOA特征：\n  codeHash为空\n  storageRoot为空\n  通过私钥控制\n  发起交易（转移以太币或触发合约代码）\n  CA特征：\n  不能主动发起交易\n  可以被触发执行合约代码（通过EOA发起的交易或者从其他CA接收的消息调用激活） 怎么判断一个账户是空账户？\n  交易 以太坊是一个基于交易的状态机。任意两个账户之间的交易都会引起world state的改变。\n交易基本定义：【从外部拥有账户】发送的加密签名序列化指令。换句话说交易必须是从EOA发起的才能叫交易，CA之间的通信叫消息也有叫内部交易的。\n交易类型有两种：\n  消息调用（Td）\n  合约创建（Ti）\n  从EOA到EOA的交易仅是转账，EOA到CA可以激活各种操作。\n存储交易数据的结构体：\ntype txdata struct { AccountNonce uint64 //Tn  Price *big.Int //Tp  GasLimit uint64 //Tg  Recipient *common.Address //Tt  Amount *big.Int //Tv  Payload []byte //Td || Ti  V *big.Int R *big.Int S *big.Int // This is only used when marshaling to JSON.  Hash *common.Hash }   Tn必须等于发起交易的账户的nonce（翻阅前面说法可知，账户nonce是该账户发起的第几笔交易的序号，如果是创建合约则代表第几次创建合约的序号）\n  Tp是这笔交易消耗的gas单价\n  Tg是你愿意为这笔交易最多可以支付的上限\n  Tt是接收账户的地址，如果为空说明接受账户是一个CA，否则是EOA\n  Tv是到接收者的额度\n  Td或Ti，如果交易类型是消息调用则Payload写为Td，表示输入数据，例如消息的参数，假设有一个注册域名的合约服务，则Td就是该服务需要的参数如IP等。如果交易类型是创建合约，则Payload写为Ti，表示一段代码，这段代码用于创建合约账户，这段初始化代码只会被执行一次就丢弃掉，第二次执行的是创建完的合约代码体。\n  什么是费用？ 以太坊网络里任何计算都要支付gas（燃料），为什么不直接用eth做费用呢？答案是用两个概念gas和eth区别价值和价格，gas是一种固定衡量的价值，而eth是市场上快速变化的价格，很多EVM（以太坊虚拟机）的操作指令都需要消耗固定的费用就用gas来计价，gas的最小单位是wei，1eth = 1018wei = 109gwei。所以eth和gas之间是有汇率的。\n GasPrice：燃料单价 GasLimit：愿意支付的燃料上限 GasLimit × GasPrice = 愿意支付的最大费用\n 一笔交易中，你设置的最大费用如果没有消耗完，多出的会返回给你。如果最大费用不够计算的花费，那么交易会终止、已改变的状态会回滚、但是钱被消耗不会退回了。这些已消耗的费用都奖励给矿工了。\n费用的三种不同构成：\n  计算操作的固定费用\n  交易（合约创建或消息调用）费用\n  存储（内存、存储账户合约数据）费用\n  存储收费是因为假如你的合约使得状态数据库存储增大，所有节点都会增加存储。以太币是鼓励尽量保持少量存储的。 但是如果有操作是清除一个存储条目，这个操作的费用不但会被免除，而且由于释放空间还会获得退款。\nQ\u0026amp;A 什么是RLP编码？ 什么是Patricia Trie？ ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-10-06-ethereum-1/","summary":"什么是以太坊 简单来说，以太坊就是一个基于交易的状态机。什么是状态机呢？可以将其理解成一台机器，这台机器维护着一些状态，在以太坊交易发生时，这些状态会从一个状态转化到另一个状态。以太坊时基于区块链构建的，区块链上保存着状态和交易。当我们与以太坊交互时，其实就是在执行交易、改变系统状态。\n引用一个公式表示就是：\n σ′ =Υ(σ,T)\n Υ是状态转换函数，T是交易，σ是状态，σ′转换后的状态。\n从创世区块开始，无尽的交易不断的刷新着系统当前状态，每产生一个区块就对当前状态做一次快照（patricia trie根）存入区块头中。\n以太坊可以干什么？ 以太坊作为一个开发的区块链平台，它允许任何人在平台中建立和使用通过区块链技术运行的去中心化应用。可将之类比为Internet平台，而我们可以在其上面开发web网站。（但是似乎在以太坊上面只能开发智能合约？）\n什么是智能合约？ 智能合约就是以太坊平台上运行的程序。一旦事件触发合约中的条款，代码自动执行。智能合约的功能是由开发者自行设计的。\n智能合约的工作原理 构建 → 存储 → 执行\n  智能合约由区块链内的多个用户共同参与制定，可用于用户之间的任何交易行为。协议中明确了双方的权利和义务，开发人员将这些权利和义务以电子化的方式进行编程，代码中包含会触发合约自动执行的条件。比方说，你把一套闲置的房子租给A，那么，这份智能租约中就规定了A必须在每月5号之前给你打房租、你必须在收到房租时马上给对方钥匙［2］等条款。\n  一旦编码完成，这份智能合约就被上传到区块链网络上，即全网验证节点都会接收到你和A的租房合约。\n  智能合约会定期检查是否存在 相关事件和触发条件；满足条件的事件将会推送到待验证的队列中。假设A在4号提前打房租给你，这个事件就成了该合约的触发条件（每月5号以前）。\n  区块链上的验证节点先对该 事件进行签名验证 以确保其有效性；等大多数验证节点对该事件达成共识后，智能合约将成功执行，并通知用户。\n  成功执行的合约将移出区块。而未执行的合约则继续等待下一轮处理，直至成功执行。\n  部署到以太坊上的智能合约是要消耗以太币的。就好像把现实中的仲裁人、法官、执行人搬到了区块链上，尽管他们成了一行行的代码，但也是珍贵的计算机资源。智能合约也遵循“Less is more”，逻辑应尽可能地简单。逻辑越复杂，消耗的以太币就越多。\n既然执行要消费以太币，如何支付呢？智能合约是预支付，为了合约顺利执行，一般提前多打一点以太币。如果预支付的以太币不足以支撑整个执行过程，就算进行到半路，合约也会回到初始状态；并且消耗的以太币也不会退回给合约发起人。\n如何触发智能合约？ 智能合约本身规定了触发的条件，只要满足了条件的情况下，合约就会自动触发（比如时间到了自动缴房租）。也可以由外部账户触发。\n以太坊的区块结构 区块链上最重要的结构莫过于区块的结构，以太坊的区块结构和区块链的类似，但是又有着诸多不同。\n以太坊区块是由三大部分组成：区块头，叔块，交易列表。\n  区块头由15个字段组成。\n  叔块其实就是孤块，是由于某个区块上产生了多分支。因以太坊出块速度很快平均十几秒就会打包生成一个块，所以矿工挖矿的竞争性很高，可能同时产出几个都合法的区块，以太坊为了一些安全性起见，允许竞争块也挂在到主链上，同时给与挖出这些孤块的矿工们少许奖励增加工作的公平性。这些孤块最多允许6个高度，这也是6个区块确认主链说法的来源。\n  交易列表，存储的是本区块中所有的交易内容。\n  看一下一个实际的区块信息：\n\u0026quot;blocks\u0026quot; : [ { \u0026quot;blockHeader\u0026quot; : { \u0026quot;bloom\u0026quot; : \u0026quot;0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\u0026quot;, \u0026quot;coinbase\u0026quot; : \u0026quot;0x3535353535353535353535353535353535353535\u0026quot;, \u0026quot;difficulty\u0026quot; : \u0026quot;0x020000\u0026quot;, \u0026quot;extraData\u0026quot; : \u0026quot;\u0026quot;, \u0026quot;gasLimit\u0026quot; : \u0026quot;0x05f5e100\u0026quot;, \u0026quot;gasUsed\u0026quot; : \u0026quot;0x014fa1\u0026quot;, \u0026quot;hash\u0026quot; : \u0026quot;0x39f4659b079e257df8fd7e699528531e97a6b8a442ca0d11200c4a2f7433c483\u0026quot;, \u0026quot;mixHash\u0026quot; : \u0026quot;0x7379f33af4ae2db7e293f808a165135d0b1a99572cc96fb9f7d17ef64a751969\u0026quot;, \u0026quot;nonce\u0026quot; : \u0026quot;0x8e08d7aabeee8773\u0026quot;, \u0026quot;number\u0026quot; : \u0026quot;0x01\u0026quot;, \u0026quot;parentHash\u0026quot; : \u0026quot;0xadbef3bf0b3b7b14f6e7b1a45d240ecc863543a279a86c23f60170e8e7a6bcc3\u0026quot;, \u0026quot;receiptTrie\u0026quot; : \u0026quot;0xb21660268480338c0cd0613358315359b619bd527d5850949c4863cddaec316b\u0026quot;, \u0026quot;stateRoot\u0026quot; : \u0026quot;0xde4ce9b5b2f88ab1680962c64281224b1743bdf94bd6a9e390ea779ff616c1f7\u0026quot;, \u0026quot;timestamp\u0026quot; : \u0026quot;0x03e8\u0026quot;, \u0026quot;transactionsTrie\u0026quot; : \u0026quot;0x56445ba866f3e41851154fb8700dcec8556a178f1833021e030b8a47b494769d\u0026quot;, \u0026quot;uncleHash\u0026quot; : \u0026quot;0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\u0026quot; }, \u0026quot;rlp\u0026quot; : \u0026quot;0xf90308f901f9a0adbef3bf0b3b7b14f6e7b1a45d240ecc863543a279a86c23f60170e8e7a6bcc3a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347943535353535353535353535353535353535353535a0de4ce9b5b2f88ab1680962c64281224b1743bdf94bd6a9e390ea779ff616c1f7a056445ba866f3e41851154fb8700dcec8556a178f1833021e030b8a47b494769da0b21660268480338c0cd0613358315359b619bd527d5850949c4863cddaec316bb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018405f5e10083014fa18203e880a07379f33af4ae2db7e293f808a165135d0b1a99572cc96fb9f7d17ef64a751969888e08d7aabeee8773f90108f90105460183030d4094c305c901078781c232a2a521c2af7980f8385ee980b8a430c8d1da000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000230644e72e131a029b85045b68181585d2833e84879b9709143e1f593f00000001ba021a28cc82b40931239f8653ffa5300e1a506c0ef7fb79a663772cafe6558ab44a075af23441f7f176a2770af41142c77b671391209b15d59144e7a1332179b5e14c0\u0026quot;, \u0026quot;transactions\u0026quot; : [ { \u0026quot;data\u0026quot; : \u0026quot;0x30c8d1da000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000230644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000\u0026quot;, \u0026quot;gasLimit\u0026quot; : \u0026quot;0x030d40\u0026quot;, \u0026quot;gasPrice\u0026quot; : \u0026quot;0x01\u0026quot;, \u0026quot;nonce\u0026quot; : \u0026quot;0x46\u0026quot;, \u0026quot;r\u0026quot; : \u0026quot;0x21a28cc82b40931239f8653ffa5300e1a506c0ef7fb79a663772cafe6558ab44\u0026quot;, \u0026quot;s\u0026quot; : \u0026quot;0x75af23441f7f176a2770af41142c77b671391209b15d59144e7a1332179b5e14\u0026quot;, \u0026quot;to\u0026quot; : \u0026quot;0xc305c901078781c232a2a521c2af7980f8385ee9\u0026quot;, \u0026quot;v\u0026quot; : \u0026quot;0x1b\u0026quot;, \u0026quot;value\u0026quot; : \u0026quot;0x00\u0026quot; } ], \u0026quot;uncleHeaders\u0026quot; : [ ] } ] 区块头的结构 区块头包含15个字段，介绍如下：","title":"以太坊"},{"content":"区块链的运转流程 当一笔交易经由某个节点或钱包产生时，这笔交易需要被转播给其他的节点来做验证。\n  产生一笔新交易\n一笔新交易产生时，会被广播到区块链中的其他参与节点\n  各节点将多笔交易一起放进区块\n每个节点会将数笔未验证的交易的hash值收集到区块中，每个区块可以包含数百笔或上千笔交易\n  决定由谁来打包区块\n各个节点进行POW，来决定谁可以获得区块的打包权，由最快算出结果的节点打包该区块\n  各节点验证打包的区块\n其他节点会确认这个区块所包含的交易是否有效，确认没被重复花费且具有有效数位签章后，接受该区块，此时该区块才能正式接上区块链\n  交易验证完成\n所有节点一旦接受该区块后，先前没计算完POW工作的区块将会失效，各节点会重新建立一个区块，并开始下一轮POW计算工作\n  Q\u0026amp;A 节点如何验证某个区块的有效性？ 当某个节点收到某个区块后，需要验证的信息有三个：\n  区块编号有效\n对区块的header进行2次hash计算，计算出来的值就是当前区块的hash值\n  区块的上一个区块hash值有效\n对比区块里的上一个区块hash值和当前节点区块链里的最后一个区块的编号（每个节点都有着一条完整的区块链数据），如果相同则验证通过，如果不同，需要顺着已有链往前查找，直到找到这个编号的页（找到了又怎么样呢？这就代表上一个区块后面已经有别的区块链啊？？？这就形成了多分支了）。如果没有找到，则验证不通过。\n  交易清单有效\n这里即是确认每笔交易的付款人是否有足够的余额来支付这笔钱。确认交易输入的UTXO是否存在，且在此之前没有支付给别人，另外，产生交易的节点本身并不验证交易输入的UTXO是否有效，即不验证交易输出用户的余额是否充足，余额的验证是让其他节点来做的\n  当节点收到一笔新交易时，是否会立即开始打包成区块的操作？ 矿工是一直在收集交易信息的，但只有等到上一个区块生成后，拿到上一个区块的hash值时，才会将收集到的交易信息打包新的区块，并进行POW工作\n当一个区块发现它的上一个区块已经连接上了新的区块后，如何处理？ 这就是区块链的多分支问题，当矿工收到两份不一样的区块，它们都基于当前这个矿工节点的链上的最后一个节点，并且内容都合法，都应将其保留，按分支的形式组织起来。区块链规定，任何时刻，都以最长的链作为主链，当某个分支的长度长于另外的分支后，该分支就称为主分支了，其他分支就会被遗忘。\n由于多分支问题产生的双花问题该如何解决？ 中本聪给出的解决方案是，建议收款人不要在公告挂出时立即确认交易完成，而是应该再看一段时间，等待各个挖矿小组再挂出6张确认账簿，并且之前的账簿没有被取消，才确认钱已到账。\n中本聪解释道，之前设定变态的编号规则，正是为了防御这一点。根据前面所述，生成有效账簿页不是那么简单的，要花费大量的人力反复试不同的幸运数 字，而且过程完全是碰运气。如果某账簿页包含你收到钱的确认，并且在后面又延续了6个，那么攻击者想要在落后6页的情况下从另一个分支赶超当前主分支是非 常困难的，除非攻击者拥有非常多的人力，超过其他所有诚实矿工的人力之和。\n而且，如果攻击者有如此多人力，与其花这么大力气搞这种攻击，还不如做良民挖矿来的收益大。这就从动机上杜绝了攻击的形成。\n每个节点打包到区块里的交易都是一样的吗？ 区块的打包是由矿工来做的，矿工可能是随机地选取某几笔交易进行打包，也可能是挑选交易手续费较高的交易进行打包，因此，每个节点产生的区块是不一样的。\n如果各个节点打包的区块交易是完全不同的，那会怎么样？ 比如，节点A打包了一个区块，所含的交易是1和2；节点B也打包了一个区块，所含的交易是3和4；由于两个节点打包的区块的前驱节点都是一致的，如果这两个区块都合法，且都被其他节点验证通过了，那么就会在链上形成分支。\n当一个节点完成POW工作后，广播给其他节点验证时，如果有某个节点此时在进行着其他交易（即不包含相同的交易）的POW工作，会怎么样？ 会停止当前工作，先验证区块的有效性\n矿工是不间断地进行挖矿工作的吗？可以光挖矿而不打包普通交易吗？ 矿工基本上就是在不间断地做POW工作的，矿工的收入主要来源于挖矿和手续费。一般来说，币的数量是有限的，前期矿工应该可以只挖矿，但为了获得更大的收益，都会选择打包有手续费的交易\n产生一笔交易的时候，就会产生一个区块吗？ 并不是，收集多少笔交易之后开始打包成区块，是由挖矿策略或者挖矿程序决定的。\n是否有可能区块生成时，块里没有交易数据？ 可能存在区块里有零笔普通交易，但有一个系统给予的挖矿奖励的交易。对于空交易的区块，由于数量量小，算出满足条件的区块hash值更快。据说早期的区块大都是这种。系统给予奖励，矿工们才有挖矿的热情。\n","permalink":"https://lambertxiao.github.io/posts/_posts/2018-10-05-blockchain-tx/","summary":"区块链的运转流程 当一笔交易经由某个节点或钱包产生时，这笔交易需要被转播给其他的节点来做验证。\n  产生一笔新交易\n一笔新交易产生时，会被广播到区块链中的其他参与节点\n  各节点将多笔交易一起放进区块\n每个节点会将数笔未验证的交易的hash值收集到区块中，每个区块可以包含数百笔或上千笔交易\n  决定由谁来打包区块\n各个节点进行POW，来决定谁可以获得区块的打包权，由最快算出结果的节点打包该区块\n  各节点验证打包的区块\n其他节点会确认这个区块所包含的交易是否有效，确认没被重复花费且具有有效数位签章后，接受该区块，此时该区块才能正式接上区块链\n  交易验证完成\n所有节点一旦接受该区块后，先前没计算完POW工作的区块将会失效，各节点会重新建立一个区块，并开始下一轮POW计算工作\n  Q\u0026amp;A 节点如何验证某个区块的有效性？ 当某个节点收到某个区块后，需要验证的信息有三个：\n  区块编号有效\n对区块的header进行2次hash计算，计算出来的值就是当前区块的hash值\n  区块的上一个区块hash值有效\n对比区块里的上一个区块hash值和当前节点区块链里的最后一个区块的编号（每个节点都有着一条完整的区块链数据），如果相同则验证通过，如果不同，需要顺着已有链往前查找，直到找到这个编号的页（找到了又怎么样呢？这就代表上一个区块后面已经有别的区块链啊？？？这就形成了多分支了）。如果没有找到，则验证不通过。\n  交易清单有效\n这里即是确认每笔交易的付款人是否有足够的余额来支付这笔钱。确认交易输入的UTXO是否存在，且在此之前没有支付给别人，另外，产生交易的节点本身并不验证交易输入的UTXO是否有效，即不验证交易输出用户的余额是否充足，余额的验证是让其他节点来做的\n  当节点收到一笔新交易时，是否会立即开始打包成区块的操作？ 矿工是一直在收集交易信息的，但只有等到上一个区块生成后，拿到上一个区块的hash值时，才会将收集到的交易信息打包新的区块，并进行POW工作\n当一个区块发现它的上一个区块已经连接上了新的区块后，如何处理？ 这就是区块链的多分支问题，当矿工收到两份不一样的区块，它们都基于当前这个矿工节点的链上的最后一个节点，并且内容都合法，都应将其保留，按分支的形式组织起来。区块链规定，任何时刻，都以最长的链作为主链，当某个分支的长度长于另外的分支后，该分支就称为主分支了，其他分支就会被遗忘。\n由于多分支问题产生的双花问题该如何解决？ 中本聪给出的解决方案是，建议收款人不要在公告挂出时立即确认交易完成，而是应该再看一段时间，等待各个挖矿小组再挂出6张确认账簿，并且之前的账簿没有被取消，才确认钱已到账。\n中本聪解释道，之前设定变态的编号规则，正是为了防御这一点。根据前面所述，生成有效账簿页不是那么简单的，要花费大量的人力反复试不同的幸运数 字，而且过程完全是碰运气。如果某账簿页包含你收到钱的确认，并且在后面又延续了6个，那么攻击者想要在落后6页的情况下从另一个分支赶超当前主分支是非 常困难的，除非攻击者拥有非常多的人力，超过其他所有诚实矿工的人力之和。\n而且，如果攻击者有如此多人力，与其花这么大力气搞这种攻击，还不如做良民挖矿来的收益大。这就从动机上杜绝了攻击的形成。\n每个节点打包到区块里的交易都是一样的吗？ 区块的打包是由矿工来做的，矿工可能是随机地选取某几笔交易进行打包，也可能是挑选交易手续费较高的交易进行打包，因此，每个节点产生的区块是不一样的。\n如果各个节点打包的区块交易是完全不同的，那会怎么样？ 比如，节点A打包了一个区块，所含的交易是1和2；节点B也打包了一个区块，所含的交易是3和4；由于两个节点打包的区块的前驱节点都是一致的，如果这两个区块都合法，且都被其他节点验证通过了，那么就会在链上形成分支。\n当一个节点完成POW工作后，广播给其他节点验证时，如果有某个节点此时在进行着其他交易（即不包含相同的交易）的POW工作，会怎么样？ 会停止当前工作，先验证区块的有效性\n矿工是不间断地进行挖矿工作的吗？可以光挖矿而不打包普通交易吗？ 矿工基本上就是在不间断地做POW工作的，矿工的收入主要来源于挖矿和手续费。一般来说，币的数量是有限的，前期矿工应该可以只挖矿，但为了获得更大的收益，都会选择打包有手续费的交易\n产生一笔交易的时候，就会产生一个区块吗？ 并不是，收集多少笔交易之后开始打包成区块，是由挖矿策略或者挖矿程序决定的。\n是否有可能区块生成时，块里没有交易数据？ 可能存在区块里有零笔普通交易，但有一个系统给予的挖矿奖励的交易。对于空交易的区块，由于数量量小，算出满足条件的区块hash值更快。据说早期的区块大都是这种。系统给予奖励，矿工们才有挖矿的热情。","title":"区块链的运转流程"},{"content":"提出问题 什么是Nat？ Nat全称Network Address Translation，即网络地址转换，就是替换IP报文头部的地址信息。Nat通常部署在一个组织的网络出口位置，通过将内部网络IP地址替换为出口网络的IP地址，从而提供公网连通内部网络的能力。\n为什么需要Nat技术？ 解决IPV4地址即将耗尽的问题，对于有网络访问需求而内部又使用私有地址的网络，就要在组织的出口位置部署NAT网关，在报文离开私网进入Internet时，将源IP替换为公网地址，通常是出口设备的接口地址。在接收方收到访问请求后，在接收方看来，此次请求是来自组织的出口设备的，因此接收方会将响应消息发送回出口网关。出口网关再将目的地址替换为私网的源主机地址，发回内网。依据这种模型，数量庞大的内网主机就不需要公有IP地址来。\nNat网络的种类 一对一 一个内部主机占用一个公网IP，一般是用来隐藏内部主机的真实IP\n一对多 典型的应用。即在一个组织网络的出口位置部署Nat网关，所有对公网的访问表现为一台主机。这里面就有产生一个问题：当有多个内部主机去访问同一个服务器时，从返回的信息不足以区分response应该转发到哪个内部主机。因此，这就需要Nat设备根据传输层信息或其他的上层协议去区分不同的会话，这里引出了后面的很多坑，这种一对多的方式也被称为端口转换PAT，NAPT或IP伪装。\n在一对多模型中，按照NAT端口映射方式分类又可做进一步划分，为方便描述，将IP和端口标记为(nAddr)\n  全锥型（谁都可以找我）\n一旦内部主机的IP和端口被Nat网关映射到某个地址A上，之后该主机的所有出口报文的源IP都会是A这个地址。任何一个外部主机 发送到A地址上时，都会被转发到该内部主机。\n  限制锥型（我先找你，你才能找我）\n一旦内部主机的IP和端口被Nat网关映射到某个地址A上，之后该主机的所有出口报文的源IP都会是A这个地址。只有该内部主机向特定的外部主机H发送过数据，那么后续H主机从任意端口发送到地址A的报文都会被转发到该内部主机\n  端口限制锥型（我从某个port找过你，以后你找我也要从这个port）\n基本跟限制锥型是一样的，内部主机发数据到特定外部主机的特定端口上，之后只有这个外部主机从这个特定端口发过来的数据才会被转化到该内部主机\n  对称型 （我从某个port找你，但你要）\n同一内网主机同一端口号，当与同一外部主机通信时，NAT分配的端口号不变；每一次与不同的外网主机通讯，就重新分配另一个端口号\n  Nat的弊端 Nat技术最大的弊端在于 破坏了IP端到端通信的能力，首先，Nat使IP会话的保持时效变短，因为一个会话建立后会在Nat设备上建立一个关联表，在会话静默的这段时间，Nat网关会进行老化操作，会回收资源。一般基于UDP的通信协议很难确定何时通信结束，所以Nat网关主要依赖超时机制回收外部端口。如果应用需要维持连接的时间大于Nat网关的设置，通信就会意外中断。因为网关回收相关转换表资源以后，新的数据到达时就找不到相关的转换信息，必须建立新的连接。\n当这个新数据是由公网侧向内网侧发送时，就会发生无法出发新连接建立，也不能通知到内网侧的主机去重建连接的情况，这时候通信就会中断。\n即使新数据是从内网侧发向公网侧，因为重建的会话表往往使用不同于之前的公网IP和端口地址，公网侧的主机也无法对应到之前的通信上。\n连接保活机制\nNat穿透技术 前面提出来了Nat的弊端，为例解决IP端到端应用在Nat环境下遇到的问题，一般由如下的解决方式，只是每一种方法都不完美，需要内部主机，应用程序或Nat网关上增加额外的处理。\n应用层网关 因为Nat不感知应用协议，所以有必要额外为每个应用协议定制协议分析方法。\n探针技术 STUN和TURN 所谓的探针技术，是通过在所有参与通信的实体上安装探测插件，以检测网络中是否存在Nat网关，并对不同模型实施不同穿透方法的一种技术。\n中间件技术 通过开发通用方法解决Nat穿透，客户端会参与网关公网映射信息的维护，此时Nat网关只要理解客户端的请求并按照要求去分配转换表，不需要自己去分析客户端的应用层数据，其中典型的技术由 UPNP。\nUPNP，即通用即插即用，是一个通用的网络终端与网关的通信协议，具备发布和管理控制的能力，其中，网关映射请求可以为客户动态添加映射表项。此时Nat不再需要理解应用层携带的信息，只转换IP地址和端口信息，而客户端通过控制 消息或信令 发到公网侧的信息中，直接携带公网映射的IP地址和端口，接收端可以按照此信息建立数据连接。Nat网关在接受数据或着连接请求时，按照UPNP建立的表项只转换地址和端口信息，不关心内容，再将数据转发到内网。这种方案需要网关，内部主机和应用程序都支持UPNP技术，且组网允许内部主机和Nat网关之间可以直接交换UPNP信令才能实施。\n中继代理技术 在Nat网关所在位置旁边放置一个应用服务器，这个服务器在内部网络和外部公网分别由自己的网络连接，客户端特定的应用产生网络请求时，将定向发送到应用代理服务器，应用代理服务器根据代理协议解析客户端的请求，再从服务器的公网侧发起一个新的请求，把客户端请求的内容中继到外部网络上，返回的相应反方向中继。\n特定协议的自穿越技术 基于UDP协议的P2P打洞技术详解 什么是UDP打洞？ Nat技术和P2P技术在现有网络上都有着广泛应用，P2P主机位于Nat网关后面的情况屡见不鲜，Nat技术虽然在一定程度上解决了IPv4地址短缺的问题，在构建防火墙，保证网络安全方面都发挥了一定的作用，但却破坏了端到端的网络通信，Nat阻碍主机进行P2P通信的主要原因是Nat不允许外部主机主动访问内网主机，但是P2P技术却要求通信双方都能主动发起访问，所以要在Nat网络环境中进行有效的P2P通信，就必须采用新的解决方案。\n原理 UDP打洞技术是通过集中服务器的协助，在各自的Nat网关上建立相应的表项，使P2P连接的双方发送的报文能够直接穿透对方的Nat网关 ，从而实现P2P客户端的互联。\n什么是集中服务器 集中服务器本质是一台被设置在公网的服务器，建立P2P的双方都可以直接访问到这台服务器，位于Nat网关后面的客户端A和客户端B都可以与一台已知的集中服务器建立连接，并通过这台服务器了解对方的信息并中转各自的信息。\n同时集中服务器的另一个重要的作用是判断某个客户端是否在Nat网关之后。具体的方法是：一个客户端在集中服务器登陆的时候，服务器记录下该客户端的两对二元组信息 {IP地址: UDP端口}，其中一对可以看作是内网IP和端口，另一对可以看作是外网IP和端口。如果该客户端不是位于Nat设备后面，那么两对IP端口应该是一样的。\n打洞session的建立 假设A要向B发起连接请求，具体的打洞过程如下：\n  A最初不知道如何向B发起连接，于是A向集中服务器发送消息，请求集中服务器帮助建立与B的UDP连接\n  B同样不知如何向A发起请求，于是B也向集中服务器发送消息，请求集中服务器帮助建立与A的连接，此时洞的指向为\n  集中服务器将含有B的外网和内网的二元组发送给A，同时，集中服务器将包含有A的外网和内网的地址二元组信息也发送给B。这样一来，A和B都知道对方外网和内网的地址二元组信息了。\n  此时，A经由自己的路由器A，去请求B的外网地址端口（这一个请求的作用是告诉自己的路由器A，之后如果收到了B外网地址端口的请求，则不要拦截）。对于这个请求报文，B的路由器收到后不回转发给B，而是丢弃，因为它认为这个是来路不明的包\n  B请求A的外网地址端口（也会告诉路由器B不要拦截A的请求了），由于路由器A在上一步已经知道不要拦截B的请求了，所以A和B成功建立双向通信\n  打洞过程中要注意的问题   处于不同网络环境下的打洞方法\n  UDP在空闲状态下的超时\n由于UDP转换协议提供的洞不是绝对可靠的，多数Nat设备内部都有一个UDP转换的空闲状态计数器，如果在一段时间内没有UDP数据通信，Nat设备会关掉这个洞，如果P2P应用程序希望洞的存活时间不受Nat网关的限制，最好在穿透Nat以后设定一个穿透的有效期。\n对于有效期的设置目前没有标准值，它与Nat设备内部的配置有关。在这个有效期内，即使没有P2P数据包需要传输，应用程序为了维持洞可以正常工作，也必须向对方发送“打洞”心跳包。两方都必须发；\n另外的一个方法就是，在当前洞超时之前，双方重新打洞，舍弃原来的洞。\n  基于TCP协议的P2P打洞技术详解 建立穿越NAT设备的P2P的TCP连接只比UDP复杂一点点，TCP协议的”“打洞”从协议层来看是与UDP的“打洞”过程非常相似的。尽管如此，基于TCP协议的打洞至今为止还没有被很好的理解，这也造成了的对其提供支持的NAT设备不是很多。在NAT设备支持的前提下，基于TCP的“打洞”技术实际上与基于UDP的“打洞”技术一样快捷、可靠。实际上，只要NAT设备支持的话，基于TCP的P2P技术的健壮性将比基于UDP技术的更强一些，因为TCP协议的状态机给出了一种标准的方法来精确的获取某个TCP session的生命期，而UDP协议则无法做到这一点。\n套接字和TCP端口的重用 实现基于TCP协议的P2P打洞过程中，最主要的问题不是来自于TCP协议，而是来自于应用程序的API接口。这是由于标准的伯克利(Berkeley)套接字的API是围绕着构建客户端/服务器程序而设计的，API允许TCP流套接字通过调用connect()函数来建立向外的连接，或者通过listen()和accept函数接受来自外部的连接，但是，API不提供类似UDP那样的，同一个端口既可以向外连接，又能够接受来自外部的连接。而且更糟的是，TCP的套接字通常仅允许建立1对1的响应，即应用程序在将一个套接字绑定到本地的一个端口以后，任何试图将第二个套接字绑定到该端口的操作都会失败。\n为了让TCP“打洞”能够顺利工作，我们需要使用一个本地的TCP端口来监听来自外部的TCP连接，同时建立多个向外的TCP连接。幸运的是，所有的主流操作系统都能够支持特殊的TCP套接字参数，通常叫做“SO_REUSEADDR”，该参数允许应用程序将多个套接字绑定到本地的一个地址二元组（只要所有要绑定的套接字都设置了SO_REUSEADDR参数即可）。BSD系统引入了SO_REUSEPORT参数，该参数用于区分端口重用还是地址重用，在这样的系统里面，上述所有的参数必须都设置才行。\nTCP打洞过程 假设客户端A希望建立与B的TCP连接。我们像通常一样假定A和B已经与公网上的已知服务器建立了TCP连接。服务器记录下来每个接入的客户端的公网和内网的地址二元组，如同为UDP服务的时候一样。\n从协议层来看，TCP“打洞”与UDP“打洞”是几乎完全相同的过程：\n  客户端A使用其与服务器的连接向服务器发送请求，要求服务器协助其连接客户端B；\n  服务器将B的公网和内网的TCP地址的二元组信息返回给A，同时，服务器将A的公网和内网的地址二元组也发送给B；\n  客户端A和B使用连接服务器的端口异步地发起向对方的公网、内网地址二元组的TCP连接，同时监听各自的本地TCP端口是否有外部的连接联入；\n  A和B开始等待向外的连接是否成功，检查是否有新连接联入。如果向外的连接由于某种网络错误而失败，如：“连接被重置”或者“节点无法访问”，客户端只需要延迟一小段时间（例如延迟一秒钟），然后重新发起连接即可，延迟的时间和重复连接的次数可以由应用程序编写者来确定；\n  TCP连接建立起来以后，客户端之间应该开始鉴权操作，确保目前联入的连接就是所希望的连接。如果鉴权失败，客户端将关闭连接，并且继续等待新的连接联入。客户端通常采用“先入为主”的策略，只接受第一个通过鉴权操作的客户端，然后将进入P2P通信过程不再继续等待是否有新的连接联入。\n  从应用程序的角度来看TCP打洞 从应用程序的角度来看，在进行TCP“打洞”的时候都发生了什么呢？假定A首先向B发出SYN包，该包发往B的公网地址二元组，并且被B的NAT设备丢弃，但是B发往A的公网地址二元组的SYN包则通过A的NAT到达了A，然后，会发生以下的两种结果中的一种，具体是哪一种取决于操作系统对TCP协议的实现：\n  A的TCP实现会发现收到的SYN包就是其发起连接并希望联入的B的SYN包，通俗一点来说就是“说曹操，曹操到”的意思，本来A要去找B，结果B自己找上门来了。A的TCP协议栈因此会把B作为A向B发起连接connect的一部分，并认为连接已经成功。程序A调用的异步connect()函数将成功返回，A的listen()等待从外部联入的函数将没有任何反映。此时，B联入A的操作在A程序的内部被理解为A联入B连接成功，并且A开始使用这个连接与B开始P2P通信。\n由于收到的SYN包中不包含A需要的ACK数据，因此，A的TCP将用SYN-ACK包回应B的公网地址二元组，并且将使用先前A发向B的SYN包一样的序列号。一旦B的TCP收到由A发来的SYN-ACK包，则把自己的ACK包发给A，然后两端建立起TCP连接。简单的说，第一种，就是即使A发往B的SYN包被B的NAT丢弃了，但是由于B发往A的包到达了A。结果是，A认为自己连接成功了，B也认为自己连接成功了，不管是谁成功了，总之连接是已经建立起来了。\n  另外一种结果是，A的TCP实现没有像（1）中所讲的那么“智能”，它没有发现现在联入的B就是自己希望联入的。就好比在机场接人，明明遇到了自己想要接的人却不认识，误认为是其他的人，安排别人给接走了，后来才知道是自己错过了机会，但是无论如何，人已经接到了任务已经完成了。然后，A通过常规的listen()函数和accept()函数得到与B的连接，而由A发起的向B的公网地址二元组的连接会以失败告终。尽管A向B的连接失败，A仍然得到了B发起的向A的连接，等效于A与B之间已经联通，不管中间过程如何，A与B已经连接起来了，结果是A和B的基于TCP协议的P2P连接已经建立起来了。\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-10-04-nat/","summary":"提出问题 什么是Nat？ Nat全称Network Address Translation，即网络地址转换，就是替换IP报文头部的地址信息。Nat通常部署在一个组织的网络出口位置，通过将内部网络IP地址替换为出口网络的IP地址，从而提供公网连通内部网络的能力。\n为什么需要Nat技术？ 解决IPV4地址即将耗尽的问题，对于有网络访问需求而内部又使用私有地址的网络，就要在组织的出口位置部署NAT网关，在报文离开私网进入Internet时，将源IP替换为公网地址，通常是出口设备的接口地址。在接收方收到访问请求后，在接收方看来，此次请求是来自组织的出口设备的，因此接收方会将响应消息发送回出口网关。出口网关再将目的地址替换为私网的源主机地址，发回内网。依据这种模型，数量庞大的内网主机就不需要公有IP地址来。\nNat网络的种类 一对一 一个内部主机占用一个公网IP，一般是用来隐藏内部主机的真实IP\n一对多 典型的应用。即在一个组织网络的出口位置部署Nat网关，所有对公网的访问表现为一台主机。这里面就有产生一个问题：当有多个内部主机去访问同一个服务器时，从返回的信息不足以区分response应该转发到哪个内部主机。因此，这就需要Nat设备根据传输层信息或其他的上层协议去区分不同的会话，这里引出了后面的很多坑，这种一对多的方式也被称为端口转换PAT，NAPT或IP伪装。\n在一对多模型中，按照NAT端口映射方式分类又可做进一步划分，为方便描述，将IP和端口标记为(nAddr)\n  全锥型（谁都可以找我）\n一旦内部主机的IP和端口被Nat网关映射到某个地址A上，之后该主机的所有出口报文的源IP都会是A这个地址。任何一个外部主机 发送到A地址上时，都会被转发到该内部主机。\n  限制锥型（我先找你，你才能找我）\n一旦内部主机的IP和端口被Nat网关映射到某个地址A上，之后该主机的所有出口报文的源IP都会是A这个地址。只有该内部主机向特定的外部主机H发送过数据，那么后续H主机从任意端口发送到地址A的报文都会被转发到该内部主机\n  端口限制锥型（我从某个port找过你，以后你找我也要从这个port）\n基本跟限制锥型是一样的，内部主机发数据到特定外部主机的特定端口上，之后只有这个外部主机从这个特定端口发过来的数据才会被转化到该内部主机\n  对称型 （我从某个port找你，但你要）\n同一内网主机同一端口号，当与同一外部主机通信时，NAT分配的端口号不变；每一次与不同的外网主机通讯，就重新分配另一个端口号\n  Nat的弊端 Nat技术最大的弊端在于 破坏了IP端到端通信的能力，首先，Nat使IP会话的保持时效变短，因为一个会话建立后会在Nat设备上建立一个关联表，在会话静默的这段时间，Nat网关会进行老化操作，会回收资源。一般基于UDP的通信协议很难确定何时通信结束，所以Nat网关主要依赖超时机制回收外部端口。如果应用需要维持连接的时间大于Nat网关的设置，通信就会意外中断。因为网关回收相关转换表资源以后，新的数据到达时就找不到相关的转换信息，必须建立新的连接。\n当这个新数据是由公网侧向内网侧发送时，就会发生无法出发新连接建立，也不能通知到内网侧的主机去重建连接的情况，这时候通信就会中断。\n即使新数据是从内网侧发向公网侧，因为重建的会话表往往使用不同于之前的公网IP和端口地址，公网侧的主机也无法对应到之前的通信上。\n连接保活机制\nNat穿透技术 前面提出来了Nat的弊端，为例解决IP端到端应用在Nat环境下遇到的问题，一般由如下的解决方式，只是每一种方法都不完美，需要内部主机，应用程序或Nat网关上增加额外的处理。\n应用层网关 因为Nat不感知应用协议，所以有必要额外为每个应用协议定制协议分析方法。\n探针技术 STUN和TURN 所谓的探针技术，是通过在所有参与通信的实体上安装探测插件，以检测网络中是否存在Nat网关，并对不同模型实施不同穿透方法的一种技术。\n中间件技术 通过开发通用方法解决Nat穿透，客户端会参与网关公网映射信息的维护，此时Nat网关只要理解客户端的请求并按照要求去分配转换表，不需要自己去分析客户端的应用层数据，其中典型的技术由 UPNP。\nUPNP，即通用即插即用，是一个通用的网络终端与网关的通信协议，具备发布和管理控制的能力，其中，网关映射请求可以为客户动态添加映射表项。此时Nat不再需要理解应用层携带的信息，只转换IP地址和端口信息，而客户端通过控制 消息或信令 发到公网侧的信息中，直接携带公网映射的IP地址和端口，接收端可以按照此信息建立数据连接。Nat网关在接受数据或着连接请求时，按照UPNP建立的表项只转换地址和端口信息，不关心内容，再将数据转发到内网。这种方案需要网关，内部主机和应用程序都支持UPNP技术，且组网允许内部主机和Nat网关之间可以直接交换UPNP信令才能实施。\n中继代理技术 在Nat网关所在位置旁边放置一个应用服务器，这个服务器在内部网络和外部公网分别由自己的网络连接，客户端特定的应用产生网络请求时，将定向发送到应用代理服务器，应用代理服务器根据代理协议解析客户端的请求，再从服务器的公网侧发起一个新的请求，把客户端请求的内容中继到外部网络上，返回的相应反方向中继。\n特定协议的自穿越技术 基于UDP协议的P2P打洞技术详解 什么是UDP打洞？ Nat技术和P2P技术在现有网络上都有着广泛应用，P2P主机位于Nat网关后面的情况屡见不鲜，Nat技术虽然在一定程度上解决了IPv4地址短缺的问题，在构建防火墙，保证网络安全方面都发挥了一定的作用，但却破坏了端到端的网络通信，Nat阻碍主机进行P2P通信的主要原因是Nat不允许外部主机主动访问内网主机，但是P2P技术却要求通信双方都能主动发起访问，所以要在Nat网络环境中进行有效的P2P通信，就必须采用新的解决方案。\n原理 UDP打洞技术是通过集中服务器的协助，在各自的Nat网关上建立相应的表项，使P2P连接的双方发送的报文能够直接穿透对方的Nat网关 ，从而实现P2P客户端的互联。\n什么是集中服务器 集中服务器本质是一台被设置在公网的服务器，建立P2P的双方都可以直接访问到这台服务器，位于Nat网关后面的客户端A和客户端B都可以与一台已知的集中服务器建立连接，并通过这台服务器了解对方的信息并中转各自的信息。\n同时集中服务器的另一个重要的作用是判断某个客户端是否在Nat网关之后。具体的方法是：一个客户端在集中服务器登陆的时候，服务器记录下该客户端的两对二元组信息 {IP地址: UDP端口}，其中一对可以看作是内网IP和端口，另一对可以看作是外网IP和端口。如果该客户端不是位于Nat设备后面，那么两对IP端口应该是一样的。\n打洞session的建立 假设A要向B发起连接请求，具体的打洞过程如下：\n  A最初不知道如何向B发起连接，于是A向集中服务器发送消息，请求集中服务器帮助建立与B的UDP连接","title":"Nat技术详解"},{"content":"UTXO模型 什么是UXTO？ 在区块链里，账本里记录的是一笔又一笔的交易。每笔交易都有若干交易输入，也就是资金来源；也有若干交易输出，也就是资金去向。一般来说，每一笔交易都要花费一笔输入，产生一笔输出，而当其所产生的输出，并被其他交易所花费时，这笔输出就可以被称为 “未花费过的交易输出”，也就是UTXO。\n比特币中交易过程的实现 在比特币的世界里，记录交易记录正是基于UTXO模型。要理解UTXO，最简单的方法就是把一枚比特币从诞生到交易的经历描述一下。\n假设，张三通过挖矿得到了12.5枚比特币。过了几天，他把其中2.5枚比特币交给了李四。再过几天，他和李四各出资2.5比特币凑成5比特币给王五，整个交易过程再UTXO模型里的记录是这样的：\n从图上可以看出，当张三付给李四2.5个比特币时这笔交易时，收款人有两个，分别是李四和张三他自己，张三收到了余额10枚比特币，李四收到了2.5枚比特币，此时，张三原有的挖矿所得的12.5的记录因为以及消费，已经不能算是UTXO的记录了，因此，张三此时的余额就是10枚比特币。\n 可以看出，从消费这一点来看，UTXO类似日常生活中的纸币消费，当你拿10块钱买了3块钱的肥宅快乐水时，你需要付给老板3块，同时老板会找零给你7块。此时你原来的10块钱就不存在了。\n 当李四给了王五2.5枚比特币之后，李四的已经没有比特币了，因此没有李四的交易输出了；同时，王五同时收到了两个人的比特币，收款数额直接计算总数，并合并成一条数额为5的记录。这里似乎是可以不合并分开成两条输出记录的，待确定 ！！！\n 由上面的例子说明，其实并没有什么比特币，只有 UTXO。当我们说张三拥有10枚比特币的时候，我实际上是说，当前区块链账本中，有若干笔交易的UTXO 项收款人写的是张三的地址，而这些UTXO项的数额总和是 10。\n 注意点   Coinbase交易是指矿工挖矿所得比特币的交易，这种交易比较特殊，交易输入并不是来自前面某一个或者某几个交易的UTXO。\n  每一笔交易的交易输入必须等于交易输出\n  计算某个人的账户余额时，只计算 未花费的 交易输出\n  怎么确定交易输入是有效的？ 当节点接收到一笔交易的时候，它需要去 UTXO 数据库里查，看看这笔交易所引用的 UTXO 是否存在，它的收款人（拥有者）是不是当前新交易的付款者。\n怎么保证一笔交易所引用的UTXO没有被重复消费？ 当某一笔比特币交易被创建—签名—广播到区块链网络之中后，每一个节点（比特币交易参与者）会对这笔交易进行验证，看交易的输出是否存在于UTXO。\n如果A拥有1枚比特币被证实确实是“未花费过的交易输出”，他要是将这1枚比特币同事转账给B1、B2两个人，挖矿节点会选择性的记录一笔交易，或许是最先收到的，或许是手续费更高的。\n情况1：\n如果这两笔交易是先后被挖矿节点接收到的，那依据时间戳（时间戳是矿工打包区块时的时间），先被接收到的交易会被验证成功，而后被接收到的交易则会因交易输入已经不存在于UTXO而验证失败。\n情况2：\n如果两个挖矿节点分别 同时 记录了这两笔交易，并且这两笔交易被分别证明是合法的，此时这两个挖矿节点会将各自挖到的新区块广播到全网。这时链就会 分叉。当其中一笔交易（是交易被确认还是新区块被确认？？？）被6个节点确认后，它将获得最终的确认，成为最长链，记录在最长链上的交易最终会被认证是成功的，而记录在另一条链上的交易则不会被认证。\nUTXO模型是怎么计算余额的？ 我们知道，要计算A的余额，在UTXO模型里，其实就是计算有多少笔交易的收款人的地址写的A，且这条交易输出没有被花费，那么这个余额怎么才能快速计算出来呢？\n比特币客户端的实现维护一个UTXO数据库，也称UTXO池，是区块链中所有未支付交易输出的集合。“UTXO池”的名字听上去与交易池相似，但它代表了不同的数据集。UTXO池不同于交易池和孤立交易池的地方在于，它在初始化时不为空，而是包含了数以百万计的未支付交易输出条目，有些条目的历史甚至可以追溯至2009年。UTXO池可能会被安置在本地内存，或者作为一个包含索引的数据库表安置在永久性存储设备中。\n","permalink":"https://lambertxiao.github.io/posts/_posts/2018-10-04-utxo/","summary":"UTXO模型 什么是UXTO？ 在区块链里，账本里记录的是一笔又一笔的交易。每笔交易都有若干交易输入，也就是资金来源；也有若干交易输出，也就是资金去向。一般来说，每一笔交易都要花费一笔输入，产生一笔输出，而当其所产生的输出，并被其他交易所花费时，这笔输出就可以被称为 “未花费过的交易输出”，也就是UTXO。\n比特币中交易过程的实现 在比特币的世界里，记录交易记录正是基于UTXO模型。要理解UTXO，最简单的方法就是把一枚比特币从诞生到交易的经历描述一下。\n假设，张三通过挖矿得到了12.5枚比特币。过了几天，他把其中2.5枚比特币交给了李四。再过几天，他和李四各出资2.5比特币凑成5比特币给王五，整个交易过程再UTXO模型里的记录是这样的：\n从图上可以看出，当张三付给李四2.5个比特币时这笔交易时，收款人有两个，分别是李四和张三他自己，张三收到了余额10枚比特币，李四收到了2.5枚比特币，此时，张三原有的挖矿所得的12.5的记录因为以及消费，已经不能算是UTXO的记录了，因此，张三此时的余额就是10枚比特币。\n 可以看出，从消费这一点来看，UTXO类似日常生活中的纸币消费，当你拿10块钱买了3块钱的肥宅快乐水时，你需要付给老板3块，同时老板会找零给你7块。此时你原来的10块钱就不存在了。\n 当李四给了王五2.5枚比特币之后，李四的已经没有比特币了，因此没有李四的交易输出了；同时，王五同时收到了两个人的比特币，收款数额直接计算总数，并合并成一条数额为5的记录。这里似乎是可以不合并分开成两条输出记录的，待确定 ！！！\n 由上面的例子说明，其实并没有什么比特币，只有 UTXO。当我们说张三拥有10枚比特币的时候，我实际上是说，当前区块链账本中，有若干笔交易的UTXO 项收款人写的是张三的地址，而这些UTXO项的数额总和是 10。\n 注意点   Coinbase交易是指矿工挖矿所得比特币的交易，这种交易比较特殊，交易输入并不是来自前面某一个或者某几个交易的UTXO。\n  每一笔交易的交易输入必须等于交易输出\n  计算某个人的账户余额时，只计算 未花费的 交易输出\n  怎么确定交易输入是有效的？ 当节点接收到一笔交易的时候，它需要去 UTXO 数据库里查，看看这笔交易所引用的 UTXO 是否存在，它的收款人（拥有者）是不是当前新交易的付款者。\n怎么保证一笔交易所引用的UTXO没有被重复消费？ 当某一笔比特币交易被创建—签名—广播到区块链网络之中后，每一个节点（比特币交易参与者）会对这笔交易进行验证，看交易的输出是否存在于UTXO。\n如果A拥有1枚比特币被证实确实是“未花费过的交易输出”，他要是将这1枚比特币同事转账给B1、B2两个人，挖矿节点会选择性的记录一笔交易，或许是最先收到的，或许是手续费更高的。\n情况1：\n如果这两笔交易是先后被挖矿节点接收到的，那依据时间戳（时间戳是矿工打包区块时的时间），先被接收到的交易会被验证成功，而后被接收到的交易则会因交易输入已经不存在于UTXO而验证失败。\n情况2：\n如果两个挖矿节点分别 同时 记录了这两笔交易，并且这两笔交易被分别证明是合法的，此时这两个挖矿节点会将各自挖到的新区块广播到全网。这时链就会 分叉。当其中一笔交易（是交易被确认还是新区块被确认？？？）被6个节点确认后，它将获得最终的确认，成为最长链，记录在最长链上的交易最终会被认证是成功的，而记录在另一条链上的交易则不会被认证。\nUTXO模型是怎么计算余额的？ 我们知道，要计算A的余额，在UTXO模型里，其实就是计算有多少笔交易的收款人的地址写的A，且这条交易输出没有被花费，那么这个余额怎么才能快速计算出来呢？\n比特币客户端的实现维护一个UTXO数据库，也称UTXO池，是区块链中所有未支付交易输出的集合。“UTXO池”的名字听上去与交易池相似，但它代表了不同的数据集。UTXO池不同于交易池和孤立交易池的地方在于，它在初始化时不为空，而是包含了数以百万计的未支付交易输出条目，有些条目的历史甚至可以追溯至2009年。UTXO池可能会被安置在本地内存，或者作为一个包含索引的数据库表安置在永久性存储设备中。","title":"UTXO"},{"content":"简介 Kademlia算法是区块链底层实现点对点通信时所用的算法，它通过对节点之间的数学操作，获得逻辑上的节点距离，并用这个距离构建一个不同层次的路由表。通过对该路由表的查询，更新等操作，使节点与节点之间能够相互发现。\n主要概念 NodeID Kadamlia算法使用160bit（20字节）的哈希值作为节点的唯一标识，当一个节点新加入网络时，会被分配这个NodeID\n距离 节点与节点之间的距离，是将两个节点的NodeID进行XOR操作后得到值，一般将得到的二进制数转化为十进制数后的值作为距离，例如对于NodeID分别为 0011 和 1011 的节点，异或之后得到的值为1000（二进制），即距离为4（十进制）\n公共前缀长度 Common Prefix Length（CPL） 举个列子，假设NodeID为3位，那么对于节点 110，它与周围节点的CPL分别为\n   CPL 所含节点     0 000 001 010 011   1 100 101   2 111   3 110（自身）    可以看出，节点间CPL越大，则节点XOR之后的值越小，即两节点的逻辑距离越小\n二叉前缀树 一个完整的网络空间可以被表示成为一颗二叉树，树的叶子节点代表网络节点 K-Bucket K-Bucket又称为K桶，Bucket里存的是一组CLP的长度一样的节点 路由表 一个由K-Bucket构成的链表\n分裂 某些Kadamlia算法的实现是一开始仅有一个Bucket，当Bucket的容量超过限制时，将最小的cpl的节点和其他节点分裂开的操作\n仍以上面的例子为例，如果一开始仅有一个Bucket，那么 000 ～ 111 的 8 个节点都存在一个Bucket中，假设此时Bucket的容量K为1，即现有的8个节点超过了容量，需要将之分裂\n第一次分裂\nOld Bucket = （000 001 010 011） New Bucket = （100 101 111）\nNew Bucket仍然超过了限制，继续分裂\nOld Bucket = （100 101） New Bucket = （111）\n节点的查找 当某个节点需要寻找另一个节点时，通常先在自己的路由表里查找，如果查找得到，直接获取该节点，否则，朝着与目标节点距离更新的一组节点发起查询请求，以此往复，直到查找到目标节点；这个过程称之为 逐步迭代，递进查找\n在一个对等网络中，某个节点要查询其他节点的信息时，它可依赖的信息只有两个：\n目标节点ID； 当前节点维护的路由表； 其查询的核心思想是：逐步迭代，递近查找。其基本过程如下：\n  发起者首先计算自身(​ L )与目标节点(​ T )的距离，即​ cpl ，查询本地维护的路由表，计算方法是​ Bucket = local.Buckets[cpl] ，这个 Bucket ​中的节点与目标节点有着公共前缀。然后再从该 Bucket ​中选择与目标节点有最长​ cpl 的节点​，接下来本地节点向​发起查询请求(​ QueryPeers )，因为​距离 T ​更近，相当于第一次缩短了与目标节点​ T 的距离；\n  ​ X 收到 L ​发起的对目标节点 T ​的定位消息(Message_FIND_NODE)时，会根据自身维护的路由表信息，返回距离​ T 更近的节点供查询发起者继续查询。当然，如果目标节点就是​自身，那直接返回自身信息即可。需要说明的是：​给​返回的响应并非是距离目标节点最近的那一个节点，而是一批节点（即协议中定义的​值 K ）。这样做有几点好处：1). 避免单个节点不可用导致的查询失败；2). 查询发起者可以根据响应结果进行并发查询，提升查询速度。\n  查询发起者 L 收到响应后，会将被这些作为接下来的查询对象继续进行查询。查询收到响应时，会对响应中的结果进行过滤：如果该节点在之前已经被询问过，便不再加入待查询列表，保证查询的收敛性。 查询的最终结果是得到了一批距离目标节点很近的节点列表，然后从节点列表中选择出最接近目标的​个节点。\n  节点加入 当某个节点新加入网络时，需要为其提供一个Seeder种子节点；通过向Seeder节点发起查找自身节点的请求，使Seeder知道该节点的加入，步骤如下：\n​1. 将 S 加入本地路由表，​成为 N ​的种子节点；\n ​向 S ​发起一次节点查询请求(FIND_NODE)，查询的目的节点其实是​自身；该请求的目的有二：第一告诉 S ​新增了节点 N ​，第二​通过​ S 发现集群中更多的节点。而​发起了指向自身的查询请求也很有意思：其一是因为 N ​此时还不知道系统更多的节点信息；其二是通过这种方式​ N 可以快速地找到更多距离自己更接近的节点。\n  S 收到 N ​的查询目标节点​请求，首先将​节点 N 加入自身的路由表中，然后给​ N 最多返回​ K 个距离 N ​更接近的节点信息； ​\n  N 收到​ S 的响应，将响应中的节点加入自身路由表，然后对这些节点分别发起查询请求，当然，查询的目标还是​自身。\n  节点更新 当某一个节点更新时，需要通知路由表里的节点做两方更新操作\n","permalink":"https://lambertxiao.github.io/posts/_posts/2018-10-03-kadamlia/","summary":"简介 Kademlia算法是区块链底层实现点对点通信时所用的算法，它通过对节点之间的数学操作，获得逻辑上的节点距离，并用这个距离构建一个不同层次的路由表。通过对该路由表的查询，更新等操作，使节点与节点之间能够相互发现。\n主要概念 NodeID Kadamlia算法使用160bit（20字节）的哈希值作为节点的唯一标识，当一个节点新加入网络时，会被分配这个NodeID\n距离 节点与节点之间的距离，是将两个节点的NodeID进行XOR操作后得到值，一般将得到的二进制数转化为十进制数后的值作为距离，例如对于NodeID分别为 0011 和 1011 的节点，异或之后得到的值为1000（二进制），即距离为4（十进制）\n公共前缀长度 Common Prefix Length（CPL） 举个列子，假设NodeID为3位，那么对于节点 110，它与周围节点的CPL分别为\n   CPL 所含节点     0 000 001 010 011   1 100 101   2 111   3 110（自身）    可以看出，节点间CPL越大，则节点XOR之后的值越小，即两节点的逻辑距离越小\n二叉前缀树 一个完整的网络空间可以被表示成为一颗二叉树，树的叶子节点代表网络节点 K-Bucket K-Bucket又称为K桶，Bucket里存的是一组CLP的长度一样的节点 路由表 一个由K-Bucket构成的链表\n分裂 某些Kadamlia算法的实现是一开始仅有一个Bucket，当Bucket的容量超过限制时，将最小的cpl的节点和其他节点分裂开的操作\n仍以上面的例子为例，如果一开始仅有一个Bucket，那么 000 ～ 111 的 8 个节点都存在一个Bucket中，假设此时Bucket的容量K为1，即现有的8个节点超过了容量，需要将之分裂\n第一次分裂\nOld Bucket = （000 001 010 011） New Bucket = （100 101 111）","title":"Kadamlia"},{"content":"什么是Pow算法？ Pow的全称是Proof of Work，即工作量证明，是区块链中用来判断由哪个矿工获得区块打包权的算法。\n区块链的块的结构 在聊Pow之前，首先，必须先认识区块的结构，基本的区块结构如下\n   区块结构     当前块的hash值   前一个区块的哈希值   Merkle根哈希值   时间戳   难度值   随机数Nonce   区块包含的交易列表    Merkle根 交易列表里记录的每一笔交易都有一个唯一的哈希值，将交易的hash值两两组合，最后生成Merkle根\nMerkle保证了区块的交易信息不会被串改\nNonce值 矿工挖矿的过程其实就是对交易数据进行打包后，算出一个符合如下公式的Nonce值的过程\nCryptoJS.SHA256(index + previousHash + timestamp + data + nonce) 该公式的结果是一个hash值，而挖矿的难度就是这个hash值的前面有几个0，难度越大，即要求的0的个数越多，就越难算出来，这个算的过程就是矿工工作的过程，所以这个算法才叫工作量证明算法。\n","permalink":"https://lambertxiao.github.io/posts/_posts/2018-10-03-pow/","summary":"什么是Pow算法？ Pow的全称是Proof of Work，即工作量证明，是区块链中用来判断由哪个矿工获得区块打包权的算法。\n区块链的块的结构 在聊Pow之前，首先，必须先认识区块的结构，基本的区块结构如下\n   区块结构     当前块的hash值   前一个区块的哈希值   Merkle根哈希值   时间戳   难度值   随机数Nonce   区块包含的交易列表    Merkle根 交易列表里记录的每一笔交易都有一个唯一的哈希值，将交易的hash值两两组合，最后生成Merkle根\nMerkle保证了区块的交易信息不会被串改\nNonce值 矿工挖矿的过程其实就是对交易数据进行打包后，算出一个符合如下公式的Nonce值的过程\nCryptoJS.SHA256(index + previousHash + timestamp + data + nonce) 该公式的结果是一个hash值，而挖矿的难度就是这个hash值的前面有几个0，难度越大，即要求的0的个数越多，就越难算出来，这个算的过程就是矿工工作的过程，所以这个算法才叫工作量证明算法。","title":"Pow算法"},{"content":"Grunt 杂记 简介 Grunt是一个基于NodeJS，可用于自动化构建、测试、生成文档的项目管理工具。\nGrunt 能做什么 Grunt可以自动化我们的整个开发流程，简单地说，就是用JavaScript去执行一些程序来完成一些任务。比如说将css、Javascript、图像等资源压缩；将Sass和Less通过预处理器编译成Css；将Coffeescript、Typescript等转化为Javascript；实时监听文件的变化，并执行自动编译任务；\n在Grunt工具箱中，按任务目标我们可以分为：\n 编译文档型：比如编译LESS、Sass、Stylus、Coffeescript等； 文件操作型：比如说合并、压缩JavaScript、CSS、图片等； 质量保障型：比如JSHint、Jasmin、Mocha等； 类库构建型：比如说Backbone.js、ember.js、angular.js等。  这些任务都依赖于给Grunt提供的插件来完成的，但很多工作依旧需要在命令终端手工输入命令来完成这些操作。为此在Grunt中可以使用watch任务来实现一些监听文件改变、自动触发构建等功能。从而减少人工去每次操作任务。\n开始使用 Grunt的使用非常简单，仅需下载相应的命令行工具，针对不同的任务下载不同的插件，即可完成整个自动化过程。\n  将grunt-cli（grunt命令行工具）作为全局模块安装\n $ npm install -g grunt-cli\n   通过npm初始化，为我们生成一份package.json文件\n $ mkdir grunt-app \u0026amp;\u0026amp; cd grunt-app $ npm init\n   为项目安装grunt作为依赖\n $ npm install \u0026ndash;save-dev grunt\n 至此grunt已经安装完毕，但它还没有特殊的功能，要使用grunt的各种功能，需要建一份Gruntfile.js文件，并为之安装相应的插件。\n  新建Gruntfile.js，并写入以下内容\nmodule.exports = function(grunt) { // 之后所有的配置都是配在传递给grunt.initConfig这个方法的对象中 grunt.initConfig({ // 将package.json文件作为一个json对象读入 pkg: grunt.file.readJSON('package.json'), }); // 加载grunt的插件，这里的 grunt-contrib-uglify 是一个例子 grunt.loadNpmTasks('grunt-contrib-uglify'); // 注册一个grunt任务，第一个参数为任务的名字，当任务的名字default时，通过grunt运行项目时，会默认执行该任务；第二个参数是一个数组，是后续所要执行任务名字的集合。 grunt.registerTask('default', ['uglify']); };   安装grunt插件 在安装grunt插件时，需要考虑自己需要哪些功能，下面罗列出开发中需要的大部分功能。并举个简单的例子，详细用法可参考 [Gruntjs][1]。\n  [grunt-contrib-uglify][2] （混淆并压缩js文件）\n 安装   $ npm install grunt-contrib-uglify \u0026ndash;save-dev\n  配置task  grunt.initConfig({ uglify: { my_target: { files: { 'dest/output.min.js': ['src/input1.js', 'src/input2.js'] } } } }); grunt.loadNpmTasks('grunt-contrib-uglify');    2. [grunt-contrib-cssmin][3] （压缩css） * 安装 \u0026gt; $ npm install grunt-contrib-cssmin --save-dev * 配置task ``` grunt.initConfig({ target: { files: { 'output.css': ['foo.css', 'bar.css'] } } }) grunt.loadNpmTasks('grunt-contrib-cssmin'); ``` 3. [grunt-contrib-clean][4] （清除文件和目录，一般用来清除上一次编译生成的文件） * 安装 \u0026gt; $ npm install grunt-contrib-clean --save-dev * 配置task ``` grunt.initConfig({ clean: { build: { src: ['path/to/dir/one', 'path/to/dir/two'] } } }); grunt.loadNpmTasks('grunt-contrib-clean'); ``` 4. [grunt-contrib-watch][5] （监听文件的变化，并自动执行任务） * 安装 \u0026gt; $ npm install grunt-contrib-watch --save-dev * 配置task ``` grunt.initConfig({ watch: { scripts: { files: ['**/*.coffee'], // 监听到文件变化后执行的task tasks: ['coffee'], options: { spawn: false, }, }, css: { files: '**/*.sass', tasks: ['sass'], options: { livereload: true, }, }, }, }); grunt.loadNpmTasks('grunt-contrib-watch'); ``` 5. [grunt-contrib-connect][6] （开启一个服务器，使我们可以通过 `ip:端口号` 的方式访问目录下的资源） * 安装 \u0026gt; $ npm install grunt-contrib-connect --save-dev * 配置task ``` grunt.initConfig({ connect: { server: { options: { port: 9001, base: 'www-root' } } } }); grunt.loadNpmTasks('grunt-contrib-connect'); ``` 6. [grunt-contrib-less][7] （将less编译为css） * 安装 \u0026gt; $ npm install grunt-contrib-less --save-dev * 配置task ``` grunt.initConfig({ development: { files: { 'path/to/result.css': 'path/to/source.less' } } }); grunt.loadNpmTasks('grunt-contrib-less'); ``` 7. [grunt-contrib-coffee][8] （将coffeescript编译成javascript） * 安装 \u0026gt; $ npm install grunt-contrib-coffee --save-dev * 配置task ``` grunt.initConfig({ development: { files: { 'path/to/result.js': 'path/to/source.coffee' } } }); grunt.loadNpmTasks('grunt-contrib-coffee'); ``` [1]: https://github.com/gruntjs \u0026quot;Gruntjs的github链接\u0026quot; [2]: https://github.com/gruntjs/grunt-contrib-uglify \u0026quot;grunt uglify\u0026quot; [3]: https://github.com/gruntjs/grunt-contrib-cssmin \u0026quot;grunt cssmin\u0026quot; [4]: https://github.com/gruntjs/grunt-contrib-clean \u0026quot;grunt clean\u0026quot; [5]: https://github.com/gruntjs/grunt-contrib-watch \u0026quot;grunt watch\u0026quot; [6]: https://github.com/gruntjs/grunt-contrib-connect \u0026quot;grunt connect\u0026quot; [7]: https://github.com/gruntjs/grunt-contrib-less \u0026quot;grunt less\u0026quot; [8]: https://github.com/gruntjs/grunt-contrib-coffee \u0026quot;grunt coffeescript\u0026quot; ","permalink":"https://lambertxiao.github.io/posts/web%E5%89%8D%E7%AB%AF/grunt/","summary":"Grunt是一个基于NodeJS，可用于自动化构建、测试、生成文档的项目管理工具。","title":"Grunt"},{"content":"Gulp 杂记 什么是 gulp? gulp 是一个构建工具，可以通过它自动执行网站开发过程中的公共任务，比如编译 SASS/Less，编译压缩混淆 JavaScript,，合并编译模板和版本控制等。因为 gulp 是基于 Node.js 构建的，所以 gulp 源文件和开发者自己定义的 gulpfile 都被写进 JavaScript 里，前端开发者可以用自己熟悉的语言来编写 gulp 任务。\ngulp 本身并不能完成这么多种任务，不过它可以借助 npm 丰富的插件库。开发者可以在 npm 中搜索 gulpplugin 找到想要的插件。例如本文中将要提到的 gulp-cssmin, gulp-jshint, gulp-concat、gulp-inject 等等。\n为什么选择 gulp？ 其实现有的基于 Node.js 的构建工具有很多，比如 Bower，Yeoman，grunt 等。而且自 2013 年 grunt v0.4.0 发布以后，grunt 已经改变了前端的开发方式。那么为什么我们要选 gulp？\ngulp 最大的特点是所有的任务都是以 Node.js Stream 的形式处理，构建流程可以由 Stream 之间的 pipe 来定义，省去了把中间文件写到磁盘再读取的过程，而且任务都是默认并行，速度比 grunt 快很多，配置也感觉更省心。\n 易于使用：采用代码优于配置策略，gulp 让简单的事情继续简单，复杂的任务变得可管理。 高效：gulp 基于 Node.js 流 Unix 管道连接的方式，不需要往磁盘写中间文件，可以更快地完成构建。 高质量：gulp 每个 task 只完成一个任务，提高 task 的重用度。 易于学习：gulp 核心 API 约 5 个，开发者能在很短的时间内学会，之后就可以通过管道流来组合自己需要的 task。  起步   创建项目，初始化package.json文件，并安装gulp作为开发依赖\n $ mkdir gulp-app \u0026amp;\u0026amp; npm init $ npm install \u0026ndash;save-dev gulp\n   新建gulpfile.js文件，并写入以下内容\nvar gulp = require('gulp') gulp.task('default', () =\u0026gt; { console.log('what the hell') })   命令行执行gulp，会看到输出 \u0026lsquo;what the hell\u0026rsquo;\n  gulp几个重要的api 构建自动化流程 gulp提供了许多可以满足我们工作需求的插件，以下列举部分：\n  gulp-uglify\n 安装   $ npm install \u0026ndash;save-dev gulp-uglify\n  配置task  gulp.task('default', () =\u0026gt; { gulp .src('src/app.js') .pipe(uglify()) .pipe(gulp.dest('dist')) })   gulp-less\n 安装   $ npm install \u0026ndash;save-dev gulp-less\n  配置  var less = require('gulp-less') gulp.task('default', () =\u0026gt; { gulp.src('src/styles/app.less') .pipe(less()) .pipe(gulp.dest('dist/styles')) })   gulp-coffee\n 安装   $ npm install \u0026ndash;save-dev gulp-coffee\n  配置  var coffee = require('gulp-coffee') gulp.task('default', () =\u0026gt; { gulp.src('src/scripts/app.coffee').pipe(coffee()).pipe(gulp.dest('dist/scripts')) })   gulp-clean\n 安装   $ npm install \u0026ndash;save-dev gulp-clean\n  配置  var clean = require('gulp-clean') gulp.task('clean', () =\u0026gt; { gulp.src('dist/').pipe(clean()) })   gulp-autoprefixer\n 安装   $ npm install \u0026ndash;save-dev gulp-autoprefixer\n  配置  var autoPrefixer = require('gulp-autoprefixer') gulp.task('css', () =\u0026gt; { gulp.src('src/styles/app.less').pipe(less()).pipe(autoPrefixer()).pipe(gulp.dest('dist/styles')) })   browser-sync 拥有实时重载（live-reloading）和 CSS 注入的服务器\n 安装   $ npm install \u0026ndash;save-dev browser-sync\n  配置  var gulp = require('gulp') var uglify = require('gulp-uglify') var less = require('gulp-less') var coffee = require('gulp-coffee') var clean = require('gulp-clean') var autoPrefixer = require('gulp-autoprefixer') var browserSync = require('browser-sync'); var reload = browserSync.reload; gulp.task('clean', () =\u0026gt; { gulp.src('dist/').pipe(clean()) }) gulp.task('coffee', () =\u0026gt; { gulp.src('src/scripts/*.coffee') .pipe(coffee()) .pipe(uglify()) .pipe(gulp.dest('dist/scripts')) // .pipe(reload({ stream: true })) }) gulp.task('less', () =\u0026gt; { gulp.src('src/styles/app.less') .pipe(less()) .pipe(autoPrefixer()) .pipe(gulp.dest('dist/styles')) .pipe(reload({ stream: true })) }) // 监视文件改动并重新载入 gulp.task('serve', function() { browserSync({ server: { baseDir: './' } }); gulp.watch('src/styles/app.less', ['less']); gulp.watch('src/scripts/app.coffee', ['coffee']) }); gulp.task('default',['clean', 'coffee', 'less', 'serve']) })   ","permalink":"https://lambertxiao.github.io/posts/web%E5%89%8D%E7%AB%AF/gulp/","summary":"Gulp 是一个构建工具，可以通过它自动执行网站开发过程中的公共任务，比如编译 SASS/Less，编译压缩混淆 JavaScript,，合并编译模板和版本控制等。因为 gulp 是基于 Node.js 构建的，所以 gulp 源文件和开发者自己定义的 gulpfile 都被写进 JavaScript 里，前端开发者可以用自己熟悉的语言来编写 gulp 任务。","title":"Gulp"},{"content":"TCP协议 TCP（Transmission Control Protocol 传输控制协议）是一种面向连接的、可靠的、基于字节流的传输层通信协议，由IETF的RFC 793定义。在简化的计算机网络OSI模型中，它完成第四层传输层所指定的功能，用户数据报协议（UDP）是同一层内另一个重要的传输协议。\n主要特点  面向连接 一对一 可靠交付 全双工 面向字节流  套接字Socket的含义  Socket := IP + Port TCP连接 ：= {Socket1, Socket2} 解释为两个Socket之间的连接  TCP可靠传输的工作原理   停止等待协议\n 超时重传 超时计时器 保留已发送分组的副本 对数据分组和确认分组的编号 重传时间的设置 确认丢失和确认迟到 信道利用率    连续ARQ协议\n  发送方维持的滑动窗口\n  累计确认\n  容易实现，即使确认丢失也不必重传\n  不能向发送方反映出接收方以及正确接收到的所有分组的信息\n      TCP报文段的首部格式   源端口和目的端口\n  序号\n  确认号ack\n  数据偏移\n  保留位\n  紧急URG\n  确认位ACK(TCP连接过程中会用到)\n  推送PSH\n  复位RST\n  同步位SYN(在TCP连接过程中会用到)\n  终止FIN (在TCP释放过程中会用到)\n  窗口\n  检验和\n  紧急指针\n  选项\n  最大报文长度MSS\n  窗口扩大\n  时间戳\n  计算往返时间RTT\n  防止序号绕回\n    选择确认\n    TCP可靠传输的实现   滑动窗口\n  以字节为单位的\n  结构划分\n  已发送并收到确认部分\n  可发送部分\n  不允许发送部分\n    发送窗口\n  描述发送窗口状态的三个指针\n  P1指向第一个已发送但未收到确认的字节的序号\n  P2指向第一个允许发送但尚未发送的字节的序号\n  P3指向第一个不允许发送的字节的序号\n    P3 - P1 通知窗口\n  P2 - P1 已发送但尚未收到确认的字节数\n  P3 - P2 允许发送但尚未发送的字节数\n      超时重传\n  TCP采用了一种自适应算法，该方法还在逐渐改进中\n  算法中声明的相关变量\n  RTT(Round-Trip Time) 往返时间\n  RTT的加权平均往返时间RTTs\n  RTO(RetransmissionTime-Out)超时重传时间\n  RTTd(RTT的偏差的加权平均值)\n    相关公式\n  新的RTTs = (1 - a) * (旧的RTTs) + a * (新的RTT样本)，a推荐取值0.125\n  RTO = RTTs + 4 * RTTd\n  RTTd = (1 - b) * (旧的RTTd) + b * |RTTs - 新的RTT样本|， b的推荐值是0.25\n    如何确定确认报文是对先发送的报文段的确认，还是对后来重传的报文段的确认？\n  Kran算法\n  算法的改进\n    选择确认SACK（Selective ACK）\n  目的： 只传输缺少的数据而不重传已经正确到达接收方的数据\n  前提：建立TCP连接时，需要在首部选项中加上“允许SACK”的选项\n      流量控制\n  接收窗口(rwnd)变化\n  持续计时器 + 零窗口探测报文段（解决非零窗口通知丢失造成死锁）\n  发送方缓存控制\n  缓存达到MSS(最大报文段长度)后发送\n  发送方应用程序指明要求发送报文段\n  发送方的一个计时器期限到了\n      拥塞控制\n只要网络没有出现拥塞，拥塞窗口就再增大一些，以便把更多的分组发送出去。但只要网络出现拥塞，拥塞窗口就减小一些，以减少注入到网络中的分组数\n  原理\n  几种拥塞的控制方法\n  慢开始\n当主机开始发送数据时，如果立即所大量数据字节注入到网络，那么就有可能引起网络拥塞，因为现在并不清楚网络的负荷情况。因此，较好的方法是 先探测一下，即由小到大逐渐增大发送窗口，也就是说，由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时，先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后，把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ，可以使分组注入到网络的速率更加合理。每经过一个传输轮次，拥塞窗口 cwnd 就加倍。一个传输轮次所经历的时间其实就是往返时间RTT。不过“传输轮次”更加强调：把拥塞窗口cwnd所允许发送的报文段都连续发送出去，并收到了对已发送的最后一个字节的确认。另，慢开始的“慢”并不是指cwnd的增长速率慢，而是指在TCP开始发送报文段时先设置cwnd=1，使得发送方在开始时只发送一个报文段（目的是试探一下网络的拥塞情况），然后再逐渐增大cwnd。\n  拥塞避免\n  快重传\n  快恢复\n    如何确定网络发生了拥塞？ 答：只要发送方没有按时收到应当到达的确认报名，就可以猜想网络可能出现问题了\n    运输连接管理   三次握手建立连接\n  建立连接的过程\n  涉及状态\n  CLOSED\n  LISTEN\n  SYN-SENT 同步已发送\n  SYN-RCVD 同步确认\n  ESTABLISHED 连接建立\n    涉及状态位\n  SYN 同步位，置1代表请求连接\n  ACK 确认位，置1代表确认收到\n  seq 序号\n  ack 确认号\n    为什么需要三次握手？最后A还需要再确认一次？\n 为了防止已经失效的连接请求报文段传送到了B      四次挥手连接释放\n  释放连接的过程\n  涉及状态\n  ESTABLISHED\n  FIN-WAIT-1 A请求关闭，但还没有收到B的确认\n  CLOSE-WAIT B收到了关闭请求，并且进行了确认，但是还有数据需要传送给A\n  FIN_WAIT-2 A收到了B的确认，但B的数据还没发送完，A还需要接收数据\n  LAST-ACK B没有数据要发送了，请求关闭连接\n  TIME-WAIT A收到了关闭请求，并确认\n  CLOSED B收到了A的关闭确认，A等待2MSL之后\n    涉及状态位\n FIN 终止控制位，置1代表请求关闭连接 ACK ack seq    时间等待计时器\n  MSL 最长报文段寿命\n  为什么最后A要等2MSL?\n 为了保证最后确认能被B收到，如果B没收到，B会超时重发关闭请求，如果A此时直接CLOSED了，就无法接受该请求      保活计时器\n  作用：防止客户端发生故障时，服务器空等请求\n  每次接收请求时更新\n  2小时无响应，就每75分钟发送一个探测报文段，连发10次确认客户端是否出现故障\n      ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-06-tcp/","summary":"TCP协议 TCP（Transmission Control Protocol 传输控制协议）是一种面向连接的、可靠的、基于字节流的传输层通信协议，由IETF的RFC 793定义。在简化的计算机网络OSI模型中，它完成第四层传输层所指定的功能，用户数据报协议（UDP）是同一层内另一个重要的传输协议。\n主要特点  面向连接 一对一 可靠交付 全双工 面向字节流  套接字Socket的含义  Socket := IP + Port TCP连接 ：= {Socket1, Socket2} 解释为两个Socket之间的连接  TCP可靠传输的工作原理   停止等待协议\n 超时重传 超时计时器 保留已发送分组的副本 对数据分组和确认分组的编号 重传时间的设置 确认丢失和确认迟到 信道利用率    连续ARQ协议\n  发送方维持的滑动窗口\n  累计确认\n  容易实现，即使确认丢失也不必重传\n  不能向发送方反映出接收方以及正确接收到的所有分组的信息\n      TCP报文段的首部格式   源端口和目的端口\n  序号\n  确认号ack","title":"TCP协议"},{"content":"UDP协议 UDP协议全称是用户数据报协议，在网络中它与TCP协议一样用于处理数据包，是一种无连接的协议。在OSI模型中，在第四层——传输层，处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点，也就是说，当报文发送之后，是无法得知其是否安全完整到达的。\n  基本特点\n  无连接的\n  不保证可靠交付\n  面向报文的，在添加了UDP协议的首部后就直接塞给UP层了\n  没有拥塞控制\n  支持一对一，一对多和多对多的交互通信\n  UDP首部开销小\n    UDP首部格式\n  源端口\n  目的端口\n  长度\n  校验和（检测UDP数据包在传输中是否有错，有错就丢弃）\n    ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-06-udp/","summary":"UDP协议 UDP协议全称是用户数据报协议，在网络中它与TCP协议一样用于处理数据包，是一种无连接的协议。在OSI模型中，在第四层——传输层，处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点，也就是说，当报文发送之后，是无法得知其是否安全完整到达的。\n  基本特点\n  无连接的\n  不保证可靠交付\n  面向报文的，在添加了UDP协议的首部后就直接塞给UP层了\n  没有拥塞控制\n  支持一对一，一对多和多对多的交互通信\n  UDP首部开销小\n    UDP首部格式\n  源端口\n  目的端口\n  长度\n  校验和（检测UDP数据包在传输中是否有错，有错就丢弃）\n    ","title":"UDP协议"},{"content":"Webpack 杂记 介绍 首先我们要消除一个常见的误解。webpack 是一个模块打包器(module bundler)（例如，Browserify 或 Brunch）。它不是一个任务执行器(task runner)（例如，Make, Grunt 或者 Gulp ）。任务执行器就是用来自动化处理常见的开发任务，例如项目的检查(lint)、构建(build)、测试(test)。相对于打包器(bundler)，任务执行器则聚焦在偏重上层的问题上面。你可以得益于，使用上层的工具，而将打包部分的问题留给 webpack。\n打包器(bundler)帮助您取得准备用于部署的 JavaScript 和样式表，将它们转换为适合浏览器的可用格式。例如，JavaScript 可以压缩、拆分 chunk 和懒加载，以提高性能。打包是 web 开发中最重要的挑战之一，解决此问题可以消除开发过程中的大部分痛点。\n起步   安装\n $ npm install \u0026ndash;save-dev webpack\n   新建webpack.config.js配置文件，并写入以下内容\nmodule.exports = { entry: './src/scripts/app.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist/scripts') } }   执行编译\n $ ./node_modules/.bin/webpack webpack.config.js\n   使用npm脚本来方便启动webpack，在package.json中加入:\n{ \u0026quot;scripts\u0026quot;: { \u0026quot;build\u0026quot;: \u0026quot;webpack\u0026quot; }, } 现在，可以使用 npm run build 命令，来替代我们之前用到的较长命令。注意，使用 npm 的 scripts，我们可以通过模块名，来引用本地安装的 npm 包，而不是写出完整路径。这是大多数基于 npm 的项目遵循的标准，允许我们直接调用 webpack，而不是去调用 ./node_modules/.bin/webpack。\n  资源管理 在 webpack 出现之前，前端开发人员会使用 grunt 和 gulp 等工具来处理资源，并将它们从 /src 文件夹移动到 /dist 或 /build 目录中。同样方式也被用于 JavaScript 模块，但是，像 webpack 这样的工具，将动态打包(dynamically bundle)所有依赖项（创建所谓的依赖图(dependency graph)）。这是极好的创举，因为现在每个模块都可以_明确表述它自身的依赖，我们将避免打包未使用的模块。\nwebpack 最出色的功能之一就是，除了 JavaScript，还可以通过 loader 引入任何其他类型的文件。也就是说，以上列出的那些 JavaScript 的优点（例如显式依赖），同样可以用来构建网站或 web 应用程序中的所有非 JavaScript 内容。让我们从 CSS 开始起步，或许你可能已经熟悉了这个设置过程。\n 加载css   为了从 JavaScript 模块中 import 一个 CSS 文件，需要安装加载css的插件\n $ npm install \u0026ndash;save-dev style-loader css-loader\n   在 module 配置中 添加 style-loader 和 css-loader\nmodule.exports = { module: { // 通过匹配不同正则表达式的，调用相应的loader rules: [ { // webpack 根据正则表达式，来确定应该查找哪些文件，并将其提供给指定的 loader test: /\\.css$/, use: ['style-loader', 'css-loader'] } ] } }   在app.js中通过import引入css文件\nimport '../styles/app.css';    加载图片   安装读取文件的loader\n $ npm install \u0026ndash;save-dev file-loader\n   在app.js文件中引入图片\n $ import Icon from \u0026lsquo;../images/th.jpeg\u0026rsquo;\n    加载数据   安装csv和xml加载器\n $ npm install \u0026ndash;save-dev csv-loader xml-loader\n   配置\nmodule: { rules: [ { test: /\\.(csv|tsv)$/, use: ['csv-loader'] }, { test: /\\.xml$/, use: ['xml-loader'] } ] }   管理输出  html页面自动引入资源  目前，我们在 index.html 文件中手动引入所有资源，然而随着应用程序增长，并且一旦开始对文件名使用哈希(hash)]并输出多个 bundle，手动地对 index.html 文件进行管理，一切就会变得困难起来。然而，可以通过一些插件，会使这个过程更容易操控。\nconst path = require('path') module.exports = { entry: { app: './src/scripts/app.js', common: './src/scripts/common.js' }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist/scripts') } } 当有多个入口文件时，会编译成多个bundle.js文件，每次改动都需要在页面中手动引入。此时，需要用到 `html-webpack-plugin\n $ npm install \u0026ndash;save-dev html-webpack-plugin\n const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { entry: { app: './src/scripts/app.js', common: './src/scripts/common.js' }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist/scripts') }, plugins: [ new HtmlWebpackPlugin({ title: 'Hi webpack' }) ] }  清理旧的输出文件   $ npm install clean-webpack-plugin \u0026ndash;save-dev\n const CleanWebpackPlugin = require('clean-webpack-plugin'); plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Hi webpack' }) ] 开发实践  使用 source map 定位错误  module.exports = { devtool: 'inline-source-map', }   使用webpack-dev-server，为你提供了一个简单的 web 服务器，并且能够实时重新加载(live reloading)。\n $ npm install \u0026ndash;save-dev webpack-dev-server\n 修改配置文件，告诉开发服务器(dev server)，在哪里查找文件\nmodule.exports = { devServer: { contentBase: './dist' }, } 以上配置告知 webpack-dev-server，在 localhost:8080 下建立服务，将 dist 目录下的文件，作为可访问文件。\n在package.json中添加一个脚本配置\n\u0026quot;scripts\u0026quot;: { \u0026quot;start\u0026quot;: \u0026quot;webpack-dev-server --open\u0026quot; }   代码分离 代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中，然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle，以及控制资源加载优先级，如果使用合理，会极大影响加载时间。\n有三种常用的代码分离方法：\n 入口起点：使用 entry 配置手动地分离代码。 防止重复：使用 CommonsChunkPlugin 去重和分离 chunk。 动态导入：通过模块的内联函数调用来分离代码。  ","permalink":"https://lambertxiao.github.io/posts/web%E5%89%8D%E7%AB%AF/webpack/","summary":"webpack 是一个模块打包器(module bundler)。打包器(bundler)帮助您取得准备用于部署的 JavaScript 和样式表，将它们转换为适合浏览器的可用格式。","title":"Webpack"},{"content":"产生死锁的原因和必要条件 在多道程序系统中，虽可借助于多个进程的并发执行来改善系统的资源利用率，提高系统的吞吐量，但可能发生一种危险——死锁。所谓死锁(Deadlock)，是指多个进程在运行过程中因争夺资源而造成的一种僵局(DeadlyEmbrace)，当进程处于这种僵持状态时，若无外力作用，它们都将无法再向前推进。\n产生死锁的必要条件   互斥条件\n指进程对所分配到的资源进行排它性使用，即在一段时间内某资源只由 一个进程占用。如果此时还有其它进程请求该资源，则请求者只能等待，直至占有该资源的进程用毕释放。\n  请求和保持条件\n指进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又已被其它进程占有，此时请求进程阻塞，但又对自己已获得的其它资源保持不放。\n  不剥夺条件\n指进程已获得的资源，在未使用完之前，不能被剥夺，只能在使用完 时由自己释放。\n  环路等待条件\n指在发生死锁时，必然存在一个进程——资源的环形链，即进程集合{P0，P1，P2，…，Pn}中的 P0正在等待一个 P1占用的资源； P1正在等待 P2占用的资源，……，Pn正在等待已被 P0占用的资源。\n  处理死锁的基本方法   预防死锁\n该方法是通过设置某些限制条件，去破坏产生死锁的四个必要条件中的一个或几个条件，来预防发生死锁。预防死锁是一种较易实现的方法，已被广泛使用。但由于所施加的限制条件往往太严格，因而可能会导致系统资源利用率和系统吞吐量降低。\n  避免死锁\n是在资源的动态分配过程中，用某种方法去防止系统进入不安全状态，从而避免发生死锁\n  检测死锁\n这种方法并不须事先采取任何限制性措施，也不必检查系统是否已经进 入不安全区，而是允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构，及时地检测出死锁的发生，并精确地确定与死锁有关的进程和资源； 然后，采取适当措施，从系统中将已发生的死锁清除掉\n  解除死锁\n这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时，须 将进程从死锁状态中解脱出来。常用的实施方法是撤消或挂起一些进程，以便回收一些资源，再将这些资源分配给已处于阻塞状态的进程，使之转为就绪状态，以继续运行\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E4%BA%A7%E7%94%9F%E6%AD%BB%E9%94%81%E7%9A%84%E5%8E%9F%E5%9B%A0%E5%92%8C%E5%BF%85%E8%A6%81%E6%9D%A1%E4%BB%B6/","summary":"产生死锁的原因和必要条件 在多道程序系统中，虽可借助于多个进程的并发执行来改善系统的资源利用率，提高系统的吞吐量，但可能发生一种危险——死锁。所谓死锁(Deadlock)，是指多个进程在运行过程中因争夺资源而造成的一种僵局(DeadlyEmbrace)，当进程处于这种僵持状态时，若无外力作用，它们都将无法再向前推进。\n产生死锁的必要条件   互斥条件\n指进程对所分配到的资源进行排它性使用，即在一段时间内某资源只由 一个进程占用。如果此时还有其它进程请求该资源，则请求者只能等待，直至占有该资源的进程用毕释放。\n  请求和保持条件\n指进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又已被其它进程占有，此时请求进程阻塞，但又对自己已获得的其它资源保持不放。\n  不剥夺条件\n指进程已获得的资源，在未使用完之前，不能被剥夺，只能在使用完 时由自己释放。\n  环路等待条件\n指在发生死锁时，必然存在一个进程——资源的环形链，即进程集合{P0，P1，P2，…，Pn}中的 P0正在等待一个 P1占用的资源； P1正在等待 P2占用的资源，……，Pn正在等待已被 P0占用的资源。\n  处理死锁的基本方法   预防死锁\n该方法是通过设置某些限制条件，去破坏产生死锁的四个必要条件中的一个或几个条件，来预防发生死锁。预防死锁是一种较易实现的方法，已被广泛使用。但由于所施加的限制条件往往太严格，因而可能会导致系统资源利用率和系统吞吐量降低。\n  避免死锁\n是在资源的动态分配过程中，用某种方法去防止系统进入不安全状态，从而避免发生死锁\n  检测死锁\n这种方法并不须事先采取任何限制性措施，也不必检查系统是否已经进 入不安全区，而是允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构，及时地检测出死锁的发生，并精确地确定与死锁有关的进程和资源； 然后，采取适当措施，从系统中将已发生的死锁清除掉\n  解除死锁\n这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时，须 将进程从死锁状态中解脱出来。常用的实施方法是撤消或挂起一些进程，以便回收一些资源，再将这些资源分配给已处于阻塞状态的进程，使之转为就绪状态，以继续运行\n  ","title":"产生死锁的原因和必要条件"},{"content":"基本分段存储管理方式 如果说推动存储管理方式从固定分区到动态分区分配，进而又发展到分页存储管理方式的主要动力，是提高内存利用率，那么，引入分段存储管理方式的目的，则主要是为了满足用户(程序员)在编程和使用上多方面的要求。\n分段存储管理方式的引入   方便编程\n通常，用户把自己的作业按照逻辑关系划分为若干个段，每个段都是从 0 开始编址，并有自己的名字和长度。因此，希望要访问的逻辑地址是由段名(段号)和段内偏移量(段内地址)决定的。\n  信息共享\n在实现对程序和数据的共享时，是以信息的逻辑单位为基础的。比如，共享某个例程 和函数。分页系统中的“页”只是存放信息的物理单位(块)，并无完整的意义，不便于实现共享；然而段却是信息的逻辑单位。\n  信息保护\n  动态增长\n在实际应用中，往往有些段，特别是数据段，在使用过程中会不断地增长，而事先又无法确切地知道数据段会增长到多大。(是哪种情况?)\n  动态链接\n动态链接是指在作业运行之前，并不把几个目标程序段链接起来。要运行时，先将主程序所对应的目标程序装入内存并启动运行，当运行过程中又需要调用某段时，才将该段(目标程序)调入内存并进行链接。可见，动态链接也要求以段作为管理的单位。\n  分段系统的基本原理   分段\n在分段存储管理方式中，作业的地址空间被划分为若干个段，每个段定义了一组逻辑信息。例如，有主程序段MAIN、子程序段X、数据段D及栈段S等。每个段都有自己的名字。为了实现简单起见，通常可用一个段号来代替段名，每个段都从 0开始编址，并采用一段连续的地址空间。段的长度由相应的逻辑信息组的长度决定，因而各段长度不等。\n分段方式已得到许多编译程序的支持，编译程序 能自动地根据源程序的情况而产生若干个段。编译程序可以为全局变量、用于存储相应参数及返回地址的过程调用栈、每个过程或函数的代码部分、每个过程或函数的局部变量等等，分别建立各自的段。\n  段表\n在前面所介绍的动态分区分配方式中，系统为整个进程分配一个连续的内存空间。而在分段式存储管理系统中，则是为每个分段分配一个连续的分区，而进程中的各个段可以离散地移入内存中不同的分区中。为使程序能正常运行，亦即，能从物理内存中找出每个逻辑段所对应的位置，应像分页系统那样，在系统中为每个进程建立一张段映射表，简称“段表”。每个段在表中占有一个表项，其中记录了该段在内存中的起始地址(又称为“基址”)和段的长度。段表可以存放在一组寄存器中，这样有利于提高地址转换速度，但更常见的是将段表放在内存中\n  地址变换机构\n为了实现从进程的逻辑地址到物理地址的变换功能，在系统中设置了段表寄存器，用于存放段表始址和段表长度TL。在进行地址变换时，系统将逻辑地址中的段号与段表长度TL 进行比较。若S\u0026gt;TL，表示段号太大，是访问越界，于是产生越界中断信号；若未越界，则根据段表的始址和该段的段号，计算出该段对应段表项的位置，从中读出该段在内存的起始地址，然后，再检查段内地址d是否超过该段的段长SL。若超过，即 d\u0026gt;SL，同样发出越界中断信号；若未越界，则将该段的基址d与段内地址相加即可得到要访问的内存物理地址。\n像分页系统一样，当段表放在内存中时，每要访问一个数据，都须访问两次内存，从而极大地降低了计算机的速率。解决的方法也和分页系统类似，再增设一个联想存储器，用于保存最近常用的段表项。\n  分页和分段的主要区别   两者都采用离散分配方式，且都要通过地址映射机构来实现地址变换。\n  分页仅仅是由于系统管理的需要而不是用户的需要。段则是信息的逻辑单位，它含有一组其意义相对完整的信息\n  页的大小固定且由系统决定，由系统把逻辑地址划分为页号和页内地址两部分，是 由机器硬件实现的，因而在系统中只能有一种大小的页面；而段的长度却不固定，决定于用户所编写的程序，通常由编译程序在对源程序进行编译时，根据信息的性质来划分。\n  分页的作业地址空间是一维的，即单一的线性地址空间，程序员只需利用一个记忆 符，即可表示一个地址；而分段的作业地址空间则是二维的，程序员在标识一个地址时，既需给出段名，又需给出段内地址\n  信息共享 分段系统的一个突出优点，是易于实现段的共享，即允许若干个进程共享一个或多个分段，且对段的保护也十分简单易行。在分页系统中，虽然也能实现程序和数据的共享，但远不如分段系统来得方便。\n 举个例子 有一个多用户系统，可同时接纳 40 个用户，他们都执行一个文本编辑程序(Text Editor)。如果文本编辑程序有 160 KB 的代码和另外 40 KB 的数据区，则总共需有 8 MB 的内存空间来支持 40个用户。如果160KB的代码是可重入的(Reentrant)，则无论是在分页系统还是在分段系统中，该代码都能被共享，在内存中只需保留一份文本编辑程序的副本，此时所需的内存空间仅为1760KB(40×40+160)，而不是8000KB。假定每个页面的大小为 4 KB，那么，160KB的代码将占用40个页面，数据区占10个页面。为实现代码的共享，应在每个进程的页表中都建立40个页表项，它们的物理块号都是21#～60#。在每个进程的页表中，还须为自己的数据区建立页表项，它们的物理块号分别是61#～70#、71#～80#、81#～90#，…，等等。\n 在分段系统中，实现共享则容易得多，只需在每个进程的段表中为文本编辑程序设置 一个段表项。\n段页式存储管理方式 分页系统能有效地提高内存利用率，而分段系统则能很好地满足用户需要。如果能对两种存储管理方式“各取所长”，则可以将两者结合成一种新的存储管理方式系统。这种新系统既具有分段系统的便于实现、分段可共享、易于保护、可动态链接等一系列优点，又能像分页系统那样很好地解决内存的外部碎片问题，以及可为各个分段离散地分配内存等问题。把这种结合起来形成的新系统称为“段页式系统”。\n  基本原理\n段页式系统的基本原理，是分段和分页原理的结合，即先将用户程序分成若干个段，再把每个段分成若干个页，并为每一个段赋予一个段名。\n  地址变换过程\n在段页式系统中，为了便于实现地址变换，须配置一个段表寄存器，其中存放段表始址和段表长 TL。进行地址变换时，首先利用段号 S，将它与段表长 TL进行比较。若 S \u0026lt; TL，表示未越界，于是利用段表始址和段号来求出该段所对应的段表项在段表中的位置，从中得到该段的页表始址，并利用逻辑地址中的段内页号P来获得对应页的页表项位置，从中读出该页所在的物理块号b，再利用块号b和页内地址来构成物理地址。\n在段页式系统中，为了获得一条指令或数据，须三次访问内存。第一次访问是访问内存中的段表，从中取得页表始址；第二次访问是访问内存中的页表，从中取出该页所在的物理块号，并将该块号与页内地址一起形成指令或数据的物理地址；第三次访问才是真正从第二次访问所得的地址中，取出指令或数据。显然，这使访问内存的次数增加了近两倍。为了提高执行速度，在地址变换机构中增设一个高速缓冲寄存器。每次访问它时，都须同时利用段号和页号去检索高速缓存，若找到匹配的表项，便可从中得到相应页的物理块号，用来与页内地址一起形成物理地址；若未找到匹配表项，则仍须再三次访问内存。\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E5%88%86%E6%AE%B5%E5%AD%98%E5%82%A8%E7%AE%A1%E7%90%86%E6%96%B9%E5%BC%8F/","summary":"基本分段存储管理方式 如果说推动存储管理方式从固定分区到动态分区分配，进而又发展到分页存储管理方式的主要动力，是提高内存利用率，那么，引入分段存储管理方式的目的，则主要是为了满足用户(程序员)在编程和使用上多方面的要求。\n分段存储管理方式的引入   方便编程\n通常，用户把自己的作业按照逻辑关系划分为若干个段，每个段都是从 0 开始编址，并有自己的名字和长度。因此，希望要访问的逻辑地址是由段名(段号)和段内偏移量(段内地址)决定的。\n  信息共享\n在实现对程序和数据的共享时，是以信息的逻辑单位为基础的。比如，共享某个例程 和函数。分页系统中的“页”只是存放信息的物理单位(块)，并无完整的意义，不便于实现共享；然而段却是信息的逻辑单位。\n  信息保护\n  动态增长\n在实际应用中，往往有些段，特别是数据段，在使用过程中会不断地增长，而事先又无法确切地知道数据段会增长到多大。(是哪种情况?)\n  动态链接\n动态链接是指在作业运行之前，并不把几个目标程序段链接起来。要运行时，先将主程序所对应的目标程序装入内存并启动运行，当运行过程中又需要调用某段时，才将该段(目标程序)调入内存并进行链接。可见，动态链接也要求以段作为管理的单位。\n  分段系统的基本原理   分段\n在分段存储管理方式中，作业的地址空间被划分为若干个段，每个段定义了一组逻辑信息。例如，有主程序段MAIN、子程序段X、数据段D及栈段S等。每个段都有自己的名字。为了实现简单起见，通常可用一个段号来代替段名，每个段都从 0开始编址，并采用一段连续的地址空间。段的长度由相应的逻辑信息组的长度决定，因而各段长度不等。\n分段方式已得到许多编译程序的支持，编译程序 能自动地根据源程序的情况而产生若干个段。编译程序可以为全局变量、用于存储相应参数及返回地址的过程调用栈、每个过程或函数的代码部分、每个过程或函数的局部变量等等，分别建立各自的段。\n  段表\n在前面所介绍的动态分区分配方式中，系统为整个进程分配一个连续的内存空间。而在分段式存储管理系统中，则是为每个分段分配一个连续的分区，而进程中的各个段可以离散地移入内存中不同的分区中。为使程序能正常运行，亦即，能从物理内存中找出每个逻辑段所对应的位置，应像分页系统那样，在系统中为每个进程建立一张段映射表，简称“段表”。每个段在表中占有一个表项，其中记录了该段在内存中的起始地址(又称为“基址”)和段的长度。段表可以存放在一组寄存器中，这样有利于提高地址转换速度，但更常见的是将段表放在内存中\n  地址变换机构\n为了实现从进程的逻辑地址到物理地址的变换功能，在系统中设置了段表寄存器，用于存放段表始址和段表长度TL。在进行地址变换时，系统将逻辑地址中的段号与段表长度TL 进行比较。若S\u0026gt;TL，表示段号太大，是访问越界，于是产生越界中断信号；若未越界，则根据段表的始址和该段的段号，计算出该段对应段表项的位置，从中读出该段在内存的起始地址，然后，再检查段内地址d是否超过该段的段长SL。若超过，即 d\u0026gt;SL，同样发出越界中断信号；若未越界，则将该段的基址d与段内地址相加即可得到要访问的内存物理地址。\n像分页系统一样，当段表放在内存中时，每要访问一个数据，都须访问两次内存，从而极大地降低了计算机的速率。解决的方法也和分页系统类似，再增设一个联想存储器，用于保存最近常用的段表项。\n  分页和分段的主要区别   两者都采用离散分配方式，且都要通过地址映射机构来实现地址变换。\n  分页仅仅是由于系统管理的需要而不是用户的需要。段则是信息的逻辑单位，它含有一组其意义相对完整的信息\n  页的大小固定且由系统决定，由系统把逻辑地址划分为页号和页内地址两部分，是 由机器硬件实现的，因而在系统中只能有一种大小的页面；而段的长度却不固定，决定于用户所编写的程序，通常由编译程序在对源程序进行编译时，根据信息的性质来划分。\n  分页的作业地址空间是一维的，即单一的线性地址空间，程序员只需利用一个记忆 符，即可表示一个地址；而分段的作业地址空间则是二维的，程序员在标识一个地址时，既需给出段名，又需给出段内地址\n  信息共享 分段系统的一个突出优点，是易于实现段的共享，即允许若干个进程共享一个或多个分段，且对段的保护也十分简单易行。在分页系统中，虽然也能实现程序和数据的共享，但远不如分段系统来得方便。\n 举个例子 有一个多用户系统，可同时接纳 40 个用户，他们都执行一个文本编辑程序(Text Editor)。如果文本编辑程序有 160 KB 的代码和另外 40 KB 的数据区，则总共需有 8 MB 的内存空间来支持 40个用户。如果160KB的代码是可重入的(Reentrant)，则无论是在分页系统还是在分段系统中，该代码都能被共享，在内存中只需保留一份文本编辑程序的副本，此时所需的内存空间仅为1760KB(40×40+160)，而不是8000KB。假定每个页面的大小为 4 KB，那么，160KB的代码将占用40个页面，数据区占10个页面。为实现代码的共享，应在每个进程的页表中都建立40个页表项，它们的物理块号都是21#～60#。在每个进程的页表中，还须为自己的数据区建立页表项，它们的物理块号分别是61#～70#、71#～80#、81#～90#，…，等等。","title":"基本分段存储管理方式"},{"content":"基本分页存储管理方式 连续分配方式会形成许多“碎片”，虽然可通过“紧凑”方法将许多碎片拼接成可用的大块空间，但须为之付出很大开销。$\\color{maroon}{如果允许将一个进程直接分散地装入到许多不相邻接的分区中}$，则无须再进行“紧凑”。 基于这一思想而产生了离散分配方式。\n  如果离散分配的基本单位是页，则称为分页存储管理方式；\n  如果离散分配的基本单位是段，则称为分段存储管理方式\n  页面与页表   页面\n  页面和物理块\n分页存储管理是将一个 进程的逻辑地址空间 分成若干个大小相等的片，称为页面或页，并为各页加以编号，从 0 开始，如第 0 页、第 1 页等。\n也把 内存空间 分成与页面相同大小的若干个存储块，称为(物理)块或页框(frame)，也同样为它们加以编号，如0#块、1#块等等。\n在为进程分配内存时，以块为单位将进程中的若干个页分别装入到多个可以不相邻接的物理块中。由于进程的最后一页经常装不满一块而形成了不可利用的碎片，称之为“页内碎片”。\n  页面大小\n在分页系统中的页面其大小应适中。页面若太小，一方面虽然可使内存碎片减小，从而减少了内存碎片的总空间，有利于提高内存利用率，但另一方面也会使每个进程占用较多的页面，从而导致进程的页表过长，占用大量内存；此外，还会降低页面换进换出的效率。然而，如果选择的页面较大，虽然可以减少页表的长度，提高页面换进换出的速度，但却又会使页内碎片增大。因此，页面的大小应选择适中，且页面大小应是 2 的幂，通常为 512 B～8 KB。\n    地址结构\n前一部分为页号P，后一部分为位移量W(或称为页内地址)。图中的地址长度为32位，其中 0～11 位为页内地址，即每页的大小为4KB；12～31 位为页号，地址空间最多允许有 1 M 页。\n对于某特定机器，其地址结构是一定的。若给定一个逻辑地址空间中的地址为 A，页面的大小为 L，则页号 P 和页内地址 d可按下式求得：\nP = INT(A/L) d = MOD(A/L)   页面\n在分页系统中，允许将进程的各个页离散地存储在内存不同的物理块中，但系统应能保证进程的正确运行，即能在内存中找到每个页面所对应的物理块。为此，系统又为每个进程建立了一张页面映像表，简称页表。在进程地址空间内的所有页(0～n)，依次在页表中有一页表项，其中记录了相应页在内存中对应的物理块号。在配置了页表后，进程执行时，通过查找该表，即可找到每页在内存中的物理块号。可见，页表的作用是实现从页号到物理块号的地址映射。\n即使在简单的分页系统中，也常在页表的表项中设置一存取控制字段，用于对该存储块中的内容加以保护。当存取控制字段仅有一位时，可用来规定该存储块中的内容是允许读/写，还是只读；若存取控制字段为二位，则可规定为读/写、只读和只执行等存取方式。如果有一进程试图去写一个只允许读的存储块时，将引起操作系统的一次中断。如果要利用分页系统去实现虚拟存储器，则还须增设一数据项。\n  地址变换机构\n能将用户地址空间中的逻辑地址变换为内存空间中的物理地址。由于页内地址和物理地址是一一对应的(例如，对于页面大小是 1 KB 的页内地址是 0～1023，其相应的物理块内的地址也是0～1023，无须再进行转换)，因此，地址变换机构的任务实际上只是将逻辑地址中的页号，转换为内存中的物理块号。又因为页面映射表的作用就是用于实现从页号到物理块号的变换，因此，地址变换任务是借助于页表来完成的。\n  基本的地址变换机构\n页表的功能可以由一组专门的寄存器来实现。一个页表项用一个寄存器。由于寄存器具有较高的访问速度，因而有利于提高地址变换的速度；但由于寄存器成本较高，且大多数现代计算机的页表又可能很大，使页表项的总数可达几千甚至几十万个，显然这些页表项不可能都用寄存器来实现，因此，页表大多驻留在内存中。在系统中只设置一个页表寄存器PTR(Page-TableRegister)，在其中存放页表在内存的始址和页表的长度。\n  平时，进程未执行时，页表的始址和页表长度存放在本进程的PCB中。当调度程序调度到某进时，才将这两个数据装入页表寄存器中。因此，在单处理机环境下，虽然系统中可以运行多个进程，但只需一个页表寄存器。当进程要访问某个逻辑地址中的数据时，分页地址变换机构会自动地将有效地址(相对地址)分为页号和页内地址两部分，再以页号为索引去检索页表。查找操作由硬件执行。在 执行检索之前，先将页号与页表长度进行比较，如果页号大于或等于页表长度，则表示本次所访问的地址已超越进程的地址空间。于是，这一错误将被系统发现并产生一地址越界中断。若未出现越界错误，则将页表始址与页号和页表项长度的乘积相加，便得到该表项在页表中的位置，于是可从中得到该页的物理块号，将之装入物理地址寄存器中。与此同时，再将有效地址寄存器中的页内地址送入物理地址寄存器的块内地址字段中。这样便完成了从逻辑地址到物理地址的变换。\n  具有快表的地址变换机构\n由于页表是存放在内存中的，这使 CPU 在每存取一个数据时，都要两次访问内存。\n第一次是访问内存中的页表，从中找到指定页的物理块号，再将块号与页内偏移量W拼接，以形成物理地址。 第二次访问内存时，才是从第一次所得地址中获得所需数据(或向此地址中写入数据)。\n因此，采用这种方式将使计算机的处理速度降低近 1/2。可见，以此高昂代价来换取存储器空间利用率的提高，是得不偿失的。为了提高地址变换速度，可在地址变换机构中增设一个具有并行查寻能力的特殊高速缓冲寄存器，又称为“联想寄存器”(AssociativeMemory)，或称为“快表”，在 IBM 系统中又取名为 TLB(Translation Lookaside Buffer)，用以存放当前访问的那些页表项。\n此时的地址变换过程是：在CPU给出有效地址后，由地址变换机构自动地将页号 P送入高速缓冲寄存器，并将此页号与高速缓存中的所有页号进行比较，若其中有与此相匹配的页号，便表示所要访问的页表项在快表中。于是，可直接从快表中读出该页所对应的物理块号，并送到物理地址寄存器中。如在块表中未找到对应的页表项，则还须再访问内存中的页表，找到后，把从页表项中读出的物理块号送地址寄存器；同时，再将此页表项存入快表的一个寄存器单元中，亦即，重新修改快表。但如果联想寄存器已满，则 OS 必须找到一个老的且已被认为不再需要的页表项，将它换出。\n  由于成本的关系，快表不可能做得很大，通常只存放 16～512 个页表项\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E5%9F%BA%E6%9C%AC%E5%88%86%E9%A1%B5%E5%AD%98%E5%82%A8%E7%AE%A1%E7%90%86%E6%96%B9%E5%BC%8F/","summary":"基本分页存储管理方式 连续分配方式会形成许多“碎片”，虽然可通过“紧凑”方法将许多碎片拼接成可用的大块空间，但须为之付出很大开销。$\\color{maroon}{如果允许将一个进程直接分散地装入到许多不相邻接的分区中}$，则无须再进行“紧凑”。 基于这一思想而产生了离散分配方式。\n  如果离散分配的基本单位是页，则称为分页存储管理方式；\n  如果离散分配的基本单位是段，则称为分段存储管理方式\n  页面与页表   页面\n  页面和物理块\n分页存储管理是将一个 进程的逻辑地址空间 分成若干个大小相等的片，称为页面或页，并为各页加以编号，从 0 开始，如第 0 页、第 1 页等。\n也把 内存空间 分成与页面相同大小的若干个存储块，称为(物理)块或页框(frame)，也同样为它们加以编号，如0#块、1#块等等。\n在为进程分配内存时，以块为单位将进程中的若干个页分别装入到多个可以不相邻接的物理块中。由于进程的最后一页经常装不满一块而形成了不可利用的碎片，称之为“页内碎片”。\n  页面大小\n在分页系统中的页面其大小应适中。页面若太小，一方面虽然可使内存碎片减小，从而减少了内存碎片的总空间，有利于提高内存利用率，但另一方面也会使每个进程占用较多的页面，从而导致进程的页表过长，占用大量内存；此外，还会降低页面换进换出的效率。然而，如果选择的页面较大，虽然可以减少页表的长度，提高页面换进换出的速度，但却又会使页内碎片增大。因此，页面的大小应选择适中，且页面大小应是 2 的幂，通常为 512 B～8 KB。\n    地址结构\n前一部分为页号P，后一部分为位移量W(或称为页内地址)。图中的地址长度为32位，其中 0～11 位为页内地址，即每页的大小为4KB；12～31 位为页号，地址空间最多允许有 1 M 页。\n对于某特定机器，其地址结构是一定的。若给定一个逻辑地址空间中的地址为 A，页面的大小为 L，则页号 P 和页内地址 d可按下式求得：\nP = INT(A/L) d = MOD(A/L)   页面\n在分页系统中，允许将进程的各个页离散地存储在内存不同的物理块中，但系统应能保证进程的正确运行，即能在内存中找到每个页面所对应的物理块。为此，系统又为每个进程建立了一张页面映像表，简称页表。在进程地址空间内的所有页(0～n)，依次在页表中有一页表项，其中记录了相应页在内存中对应的物理块号。在配置了页表后，进程执行时，通过查找该表，即可找到每页在内存中的物理块号。可见，页表的作用是实现从页号到物理块号的地址映射。\n即使在简单的分页系统中，也常在页表的表项中设置一存取控制字段，用于对该存储块中的内容加以保护。当存取控制字段仅有一位时，可用来规定该存储块中的内容是允许读/写，还是只读；若存取控制字段为二位，则可规定为读/写、只读和只执行等存取方式。如果有一进程试图去写一个只允许读的存储块时，将引起操作系统的一次中断。如果要利用分页系统去实现虚拟存储器，则还须增设一数据项。\n  地址变换机构","title":"基本分页存储管理方式"},{"content":"对换   对换(Swapping)的引入\n在多道程序环境下，一方面，在内存中的某些进程由于某事件尚未发生而被阻塞运行，但它却占用了大量的内存空间，甚至有时可能出现在内存中所有进程都被阻塞而迫使CPU停止下来等待的情况；另一方面，却又有着许多作业在外存上等待，因无内存而不能入内存运行的情况。显然这对系统资源是一种严重的浪费，且使系统吞吐量下降。为了解决这一问题，在系统中又增设了对换(也称交换)设施。所谓“对换”，是指把内存中暂时不能运行的进程或者暂时不用的程序和数据调出到外存上，以便腾出足够的内存空间，再把已具备运行条件的进程或进程所需要的程序和数据调入内存。对换是提高内存利用率的有效措施。\n如果对换是以整个进程为单位的，便称之为“整体对换”或“进程对换”。而如果对换是以“页”或“段”为单位进行的，则分别称之为“页面对换”或“分段对换”，又统称为“部分对换”。\n  实现进程对换，系统必须能实现三方面的功能\n  对换空间的管理\n在具有对换功能的OS中，通常把外存分为文件区和对换区。前者用于存放文件，后者用于存放从内存换出的进程。\n  进程的换出\n每当一进程由于创建子进程而需要更多的内存空间，但又无足够的内存空间等情况发生时，系统应将某进程换出。其过程是：系统首先选择处于阻塞状态且优先级最低的进程作为换出进程，然后启动磁盘，将该进程的程序和数据传送到磁盘的对换区上。若传送过程未出现错误，便可回收该进程所占用的内存空间，并对该进程的进程控制块做相应的修改。\n  进程的换入\n系统应定时地查看所有进程的状态，从中找出“就绪”状态但已换出的进程，将其中换出时间最久(换出到磁盘上)的进程作为换入进程，将之换入，直至已无可换入的进程或无可换出的进程为止。\n    ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E5%AF%B9%E6%8D%A2/","summary":"对换   对换(Swapping)的引入\n在多道程序环境下，一方面，在内存中的某些进程由于某事件尚未发生而被阻塞运行，但它却占用了大量的内存空间，甚至有时可能出现在内存中所有进程都被阻塞而迫使CPU停止下来等待的情况；另一方面，却又有着许多作业在外存上等待，因无内存而不能入内存运行的情况。显然这对系统资源是一种严重的浪费，且使系统吞吐量下降。为了解决这一问题，在系统中又增设了对换(也称交换)设施。所谓“对换”，是指把内存中暂时不能运行的进程或者暂时不用的程序和数据调出到外存上，以便腾出足够的内存空间，再把已具备运行条件的进程或进程所需要的程序和数据调入内存。对换是提高内存利用率的有效措施。\n如果对换是以整个进程为单位的，便称之为“整体对换”或“进程对换”。而如果对换是以“页”或“段”为单位进行的，则分别称之为“页面对换”或“分段对换”，又统称为“部分对换”。\n  实现进程对换，系统必须能实现三方面的功能\n  对换空间的管理\n在具有对换功能的OS中，通常把外存分为文件区和对换区。前者用于存放文件，后者用于存放从内存换出的进程。\n  进程的换出\n每当一进程由于创建子进程而需要更多的内存空间，但又无足够的内存空间等情况发生时，系统应将某进程换出。其过程是：系统首先选择处于阻塞状态且优先级最低的进程作为换出进程，然后启动磁盘，将该进程的程序和数据传送到磁盘的对换区上。若传送过程未出现错误，便可回收该进程所占用的内存空间，并对该进程的进程控制块做相应的修改。\n  进程的换入\n系统应定时地查看所有进程的状态，从中找出“就绪”状态但已换出的进程，将其中换出时间最久(换出到磁盘上)的进程作为换入进程，将之换入，直至已无可换入的进程或无可换出的进程为止。\n    ","title":"对换"},{"content":"第一章 操作系统引论   操作系统的目标和作用\n OS 作为用户与计算机硬件系统之间的接口 OS 作为计算机系统资源(处理器、存储器、I/O 设备以及信息(数据和程序))的管理者 OS 实现了对计算机资源的抽象    操作系统的基本特性\n  并发性\n并行性是指两个或多个事件在同一时刻发生；而并发性是指两个或多个事件在同一时间间隔内发生。\n  共享性\n指系统中的资源可供内存中多个并发执行的进程(线程)共同使用\n  虚拟技术\n 时分复用技术 空分复用技术    异步性\n由于资源等因素的限制，使进程的执行通常都不是“一气呵成”，而是以“停停走走”的方式运行\n    操作系统的主要功能\n  处理机管理功能\n在传统的多道程序系统中，处理机的分配和运行都是以进程为基本单位，因而对处理机的管理可归结为对进程的管理；在引入了线程的 OS 中，也包含对线程的管理。处理机管理的主要功能是创建和撤消进程(线程)，对诸进程(线程)的运行进行协调，实现进程(线程)之间的信息交换，以及按照一定的算法把处理机分配给进程(线程)。\n  存储器的功能\n存储器管理应具有内存分配、内存保护、地址映射和内存扩充\n  设备管理功能\n设备管理用于管理计算机系统中所有的外围设备，而设备管理的主要任务是：完成用户进程提出的 I/O 请求；为用户进程分配其所需的 I/O 设备；提高 CPU 和 I/O 设备的利用率；提高 I/O 速度；方便用户使用 I/O 设备。为实现上述任务，设备管理应具有缓冲管理、设备分配和设备处理以及虚拟设备等功能。\n  文件管理功能\n文件管理的主要任务是对用户文件和系统文件进行管理，以方便用户使用，并保证文件的安全性。\n    处理机管理功能   进程控制\n进程控制的主要功能是为作业创建进程，撤消已结束的进程\n  进程同步\n 进程互斥方式 指诸进程(线程)在对临界资源进行访问时，应采用互斥方式； 进程同步方式 指在相互合作去完成共同任务的诸进程(线程)间，由同步机构对它们的执行次序加以协调。    进程通信\n进程通信的任务就是用来实现在相互合作的进程之间的信息交换。由源进程利用发送命令直接将消息(Message)挂到目标进程的消息队列上，以后由目标进程利用接收命令从其消息队列中取出消息\n  调度\n 作业调度 作业调度的基本任务是从后备队列中按照一定的算法，选择出若干个作业，为它们分配运行所需的资源(首先是分配内存)。在将它们调入内存后，便分别为它们建立进程，使它们都成为可能获得处理机的就绪进程，并按照一定的算法将它们插入就绪队列。 进程调度 进程调度的任务是从进程的就绪队列中，按照一定的算法选出一个进程，把处理机分配给它，并为它设置运行现场，使进程投入执行。值得提出的是，在多线程OS中，通常是把线程作为独立运行和分配处理机的基本单位，为此，须把就绪线程排成一个队列，每次调度时，是从就绪线程队列中选出一个线程，把处理机分配给它。    存储器的功能   内存分配\n主要任务是为每道程序分配内存空间\n  静态分配\n静态分配方式中，每个作业的内存空间是在作业装入时确定的，的整个运行期间，不允许该作业再申请新的内存空间\n  动态分配\n每个作业所要求的基本内存空间也是在装入时确定的，但允许作业在运行过程中继续申请新的附加内存空间，以适应程序和数据的动态增长，也允许作业在内存中“移动”。\n 内存分配数据结构。该结构用于记录内存空间的使用情况，作为内存分配的依据； 内存分配功能。系统按照一定的内存分配算法为用户程序分配内存空间； 内存回收功能。系统对于用户不再需要的内存，通过用户的释放请求去完成系统的回收功能。      内存保护\n内存保护的主要任务是确保每道用户程序都只在自己的内存空间内运行，彼此互不干扰；为了确保每道程序都只在自己的内存区中运行，必须设置内存保护机制。一种比较简单的内存保护机制是设置两个界限寄存器，分别用于存放正在执行程序的上界和下界。系统须对每条指令所要访问的地址进行检查，如果发生越界，便发出越界中断请求，以停止该程序的执行。\n  地址映射\n一个应用程序(源程序)经编译后，通常会形成若干个目标程序；这些目标程序再经过链接便形成了可装入程序。这些程序的地址都是从“0”开始的，程序中的其它地址都是相对于起始地址计算的。由这些地址所形成的地址范围称为“地址空间”，其中的地址称为“逻辑地址”或“相对地址”。\n  内存扩充\n存储器管理中的内存扩充任务并非是去扩大物理内存的容量，而是借助于虚拟存储技术，从逻辑上去扩充内存容量，使用户所感觉到的内存容量比实际内存容量大得多，以便让更多的用户程序并发运行。这样，既满足了用户的需要，又改善了系统的性能。为此，只需增加少量的硬件。为了能在逻辑上扩充内存，系统必须具有内存扩充机制，用于实现下述各功能：\n  请求调入功能。允许在装入一部分用户程序和数据的情况下，便能启动该程序运行。在程序运行过程中，若发现要继续运行时所需的程序和数据尚未装入内存，可向OS发出请求，由OS从磁盘中将所需部分调入内存，以便继续运行。\n  置换功能。若发现在内存中已无足够的空间来装入需要调入的程序和数据时，系统应能将内存中的一部分暂时不用的程序和数据调至盘上，以腾出内存空间，然后再将所需调入的部分装入内存。\n    设备管理功能   缓冲管理\n如果在 I/O 设备和 CPU之间引入缓冲，则可有效地缓和 CPU 与 I/O 设备速度不匹配的矛盾，提高 CPU 的利用率，进而提高系统吞吐量。\n  设备分配\n是根据用户进程的 I/O 请求、系统的现有资源情况以及按照某种设备的分配策略，为之分配其所需的设备。\n  设备处理\n其基本任务是用于实现 CPU 和设备控制器之间的通信，即由 CPU 向设备控制器发出 I/O 命令，要求它完成指定的 I/O 操作；反之，由 CPU接收从控制器发来的中断请求，并给予迅速的响应和相应的处理\n  文件管理功能   文件存储空间的管理\n为了方便用户的使用，对于一些当前需要使用的系统文件和用户文件，都必须放在可随机存取的磁盘上。在多用户环境下，若由用户自己对文件的存储进行管理，不仅非常困难，而且也必然是十分低效的。因而，需要由文件系统对诸多文件及文件的存储空间实施统一的管理。其主要任务是为每个文件分配必要的外存空间，提高外存的利用率，并能有助于提高文件系统的存、取速度。\n为此，系统应设置相应的数据结构，用于记录文件存储空间的使用情况，以供分配存储空间时参考；系统还应具有对存储空间进行分配和回收的功能。为了提高存储空间的利用率，对存储空间的分配，通常是采用离散分配方式，以减少外存零头，并以盘块为基本分配单位。盘块的大小通常为 1～8 KB。\n  目录管理\n  文件的读/写管理和保护\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-01-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%BC%95%E8%AE%BA/","summary":"第一章 操作系统引论   操作系统的目标和作用\n OS 作为用户与计算机硬件系统之间的接口 OS 作为计算机系统资源(处理器、存储器、I/O 设备以及信息(数据和程序))的管理者 OS 实现了对计算机资源的抽象    操作系统的基本特性\n  并发性\n并行性是指两个或多个事件在同一时刻发生；而并发性是指两个或多个事件在同一时间间隔内发生。\n  共享性\n指系统中的资源可供内存中多个并发执行的进程(线程)共同使用\n  虚拟技术\n 时分复用技术 空分复用技术    异步性\n由于资源等因素的限制，使进程的执行通常都不是“一气呵成”，而是以“停停走走”的方式运行\n    操作系统的主要功能\n  处理机管理功能\n在传统的多道程序系统中，处理机的分配和运行都是以进程为基本单位，因而对处理机的管理可归结为对进程的管理；在引入了线程的 OS 中，也包含对线程的管理。处理机管理的主要功能是创建和撤消进程(线程)，对诸进程(线程)的运行进行协调，实现进程(线程)之间的信息交换，以及按照一定的算法把处理机分配给进程(线程)。\n  存储器的功能\n存储器管理应具有内存分配、内存保护、地址映射和内存扩充\n  设备管理功能\n设备管理用于管理计算机系统中所有的外围设备，而设备管理的主要任务是：完成用户进程提出的 I/O 请求；为用户进程分配其所需的 I/O 设备；提高 CPU 和 I/O 设备的利用率；提高 I/O 速度；方便用户使用 I/O 设备。为实现上述任务，设备管理应具有缓冲管理、设备分配和设备处理以及虚拟设备等功能。\n  文件管理功能","title":"操作系统引论"},{"content":"线程的基本概念 线程的引入 如果说，在操作系统中引入进程的目的，是为了使多个程序能并发执行，以提高资源利用率和系统吞吐量，那么，在操作系统中再引入线程，则是为了减少程序在并发执行时所付出的时空开销，使 OS 具有更好的并发性。\n由于进程是一个资源的拥有者，因而在创建、撤消和切换中，系统必须为之付出较大的时空开销。线程作为调度和分派的基本单位。\n线程与进程的比较 线程具有许多传统进程所具有的特征，所以又称为轻型进程(Light-Weight Process)或进程元，相应地把传统进程称为重型进程(Heavy-WeightProcess)\n 调度  在传统的操作系统中，作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中，则把线程作为调度和分派的基本单位，而进程作为资源拥有的基本单位，使线程基本上不拥有资源，这样线程便能轻装前进，从而可显著地提高系统的并发程度。在同一进程中，线程的切换不会引起进程的切换，但从一个进程中的线程切换到另一个进程中的线程时，将会引起进程的切换。\n  并发性\n在引入线程的操作系统中，不仅进程之间可以并发执行，而且在一个进程中的多个线程之间亦可并发执行，使得操作系统具有更好的并发性，从而能更加有效地提高系统资源的利用率和系统的吞吐量\n  拥有资源\n进程是系统中拥有资源的一个基本单位。一般而言，线程自己不拥有系统资源(也有一点必不可少的资源)，但它可以访问其隶属进程的资源，即一个进程的代码段、数据段及所拥有的系统资源，如已打开的文件、I/O设备等，可以供该进程中的所有线程所共享。\n  系统开销\n在创建或撤消进程时，系统都要为之创建和回收进程控制块，分配或回收资源，如内存空间和I/O设备等，操作系统所付出的开销明显大于线程创建或撤消时的开销。类似地，在进程切换时，涉及到当前进程CPU环境的保存及新被调度运行进程的CPU环境的设置，而线程的切换则仅需保存和设置少量寄存器内容，不涉及存储器管理方面的操作，所以就切换代价而言，进程也是远高于线程的。此外，由于一个进程中的多个线程具有相同的地址空间，在同步和通信的实现方面线程也比进程容易。在一些操作系统中，线程的切换、同步和通信都无须操作系统内核的干预。\n  线程的属性   轻型实体\n线程中的实体基本上不拥有系统资源，只是有一点必不可少的、能保证其独立运行的资源，比如，在每个线程中都应具有一个用于控制线程运行的线程控制块 TCB，用于指示被执行指令序列的程序计数器，保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。\n  独立调度和分派的基本单位\n  可并发执行\n  共享进程资源\n  线程的状态   状态参数\n在 OS 中的每一个线程都可以利用线程标识符和一组状态参数进行描述。状态参数通常有这样几项：\n  寄存器状态，它包括程序计数器 PC（存放下一条指令所在单元） 和堆栈指针中的内容；\n  堆栈，在堆栈中通常保存有局部变量和返回地址；\n  线程运行状态，用于描述线程正处于何种运行状态；\n  优先级，描述线程执行的优先程度；\n  线程专有存储器，用于保存线程自己的局部变量拷贝；\n  信号屏蔽，即对某些信号加以屏蔽。\n    线程运行状态\n  执行状态\n表示线程正获得处理机而运行；\n  就绪状态\n指线程已具备了各种执行条件，一旦获得 CPU 便可执行的状态；\n  阻塞状态\n指线程在执行中因某事件而受阻，处于暂停执行时的状态。\n    线程的创建和终止 在多线程 OS 环境下，应用程序在启动时，通常仅有一个“初始化线程”在执行。它可根据需要再去创建若干个线程。\n  创建新线程\n 调用线程创建函数 提供相应的参数，如指向线程主程序的入口指针、堆栈的大小，以及用于调度的优先级等 返回一个线程标识符    终止线程\n  在线程完成了自己的工作后自愿退出\n  在运行中出现错误或由于某种原因而被其它线程强行终止\n  线程被中止后并不立即释放它所占有的资源，只有当进程中的其它线程执行了 分离函数 后，被终止的线程才与资源分离，此时的资源才能被其它线程利用\n  虽已被终止但尚未释放资源的线程，仍可以被需要它的线程所调用，以使被终止线程重新恢复运行。为此，调用者线程须调用一条被称为“等待线程终止”的连接命令，来与该线程进行连接。如果在一个调用者线程调用“等待线程终止”的连接命令试图与指定线程相连接时，若指定线程尚未被终止，则调用连接命令的线程将会阻塞，直至指定线程被终止后才能实现它与调用者线程的连接并继续执行；若指定线程已被终止，则调用者线程不会被阻塞而是继续执行。\n    线程间的同步和通信   互斥锁(mutex)\n互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开销都较低，因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态，即开锁(unlock)和关锁(lock)状态。相应地，可用两条命令(函数)对互斥锁进行操作。其中的关锁 lock 操作用于将 mutex 关上，开锁操作 unlock 则用于打开 mutex。\n  条件变量\n线程首先对 mutex执行关锁操作，若成功便进入临界区，然后查找用于描述该资源状态的数据结构，以了解资源的情况。只要发现所需资源R正处于忙碌状态，线程便转为等待状态，并对mutex执行开锁操作后 ，等待该资源被释放；若资源处于空闲状态，表明线程可以使用该资源，于是 将该资源设置为忙碌状态，再对 mutex执行开锁操作。下面给出了对上述资源的申请(左半部分)和释放(右半部分)操作的描述。\nLock mutex check data structures； if (resource busy) wait(condition variable)； mark resource as busy； unlock mutex； Lock mutex mark resource as free； unlock mutex； wakeup(condition variable)； 原来占有资源 R 的线程在使用完该资源后，便按照右半部分的描述释放该资源，其中的 wakeup(conditionvariable)表示去唤醒在指定条件变量上等待的一个或多个线程。在大多数情况下，由于所释放的是临界资源，此时所唤醒的只能是在条件变量上等待的某一个线程，其它线程仍继续在该队列上等待。但如果线程所释放的是一个数据文件，该文件允许多个线程同时对它执行读操作。在这种情况下，当一个写线程完成写操作并释放该文件后，如果此时在该条件变量上还有多个读线程在等待，则该线程可以唤醒所有的等待线程。\n  信号量机制   私用信号量\n当某线程需利用信号量来实现同一进程中各线程之间的同步时，可调用创建信号量的命令来创建一私用信号量，其数据结构存放在应用程序的地址空间中。私用信号量属于特定的进程所有，OS并不知道私用信号量的存在，因此，一旦发生私用信号量的占用者异常结束或正常结束，但并未释放该信号量所占有空间的情况时，系统将无法使它恢复为0(空)，也不能将它传送给下一个请求它的线程。\n  公用信号量\n公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。由于它有着一个公开的名字供所有的进程使用，故而把它称为公用信号量。其数据结构是存放在受保护的系统存储区中，由OS为它分配空间并进行管理，故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量，则 OS 会自动将该信号量空间回收，并通知下一进程。\n  线程的实现方式   内核支持线程\n无论是用户进程中的线程，还是系统进程中的线程，他们的创建、撤消和切换等也是依靠内核，在内核空间实现的。此外，在内核空间还为每一个内核支持线程设置了一个线程控制块，内核是根据该控制块而感知某线程的存在，并对其加以控制。\n这种线程实现方式主要有如下四个优点：\n  在多处理器系统中，内核能够同时调度同一进程中多个线程并行执行；\n  如果进程中的一个线程被阻塞了，内核可以调度该进程中的其它线程占有处理器运行，也可以运行其它进程中的线程；\n  内核支持线程具有很小的数据结构和堆栈，线程的切换比较快，切换开销小；\n  内核本身也可以采用多线程技术，可以提高系统的执行速度和效率。\n  内核支持线程的主要缺点是：\n 对于用户的线程切换而言，其模式切换的开销较大，在同一个进程中，从一个线程切换到另一个线程时，需要从用户态转到内核态进行，这是因为用户进程的线程在用户态运行，而线程调度和管理是在内核实现的，系统开销较大    用户级线程\n用户级线程 ULT(User Level Threads)仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能，都无须利用系统调用来实现。对于用户级线程的切换，通常发生在一个应用进程的诸多线程之间，这时，也同样无须内核的支持。由于切换的规则远比进程调度和切换的规则简单，因而使线程的切换速度特别快。\n  线程的实现   内核支持线程的实现\n在仅设置了内核支持线程的OS中，一种可能的线程控制方法是，系统在创建一个新进程时，便为它分配一个任务数据区 PTDA(Per Task Data Area)，其中包括若干个线程控制块TCB 空间。在每一个 TCB 中可保存线程标识符、优先级、线程运行的CPU状态等信息。虽然这些信息与用户级线程 TCB 中的信息相同，但现在却是被保存在内核空间中。\n每当进程要创建一个线程时，便为新线程分配一个TCB，将有关信息填入该 TCB 中，并为之分配必要的资源，如为线程分配数百至数千个字节的栈空间和局部存储区，于是新创建的线程便有条件立即执行。当 PTDA中的所有 TCB 空间已用完，而进程又要创建新的线程时，只要其所创建的线程数目未超过系统的允许值(通常为数十至数百个)，系统可再为之分配新的 TCB 空间；在撤消一个线程时，也应回收该线程的所有资源和TCB。可见，内核支持线程的创建、 撤消均与进程的相类似。在有的系统中为了减少创建和撤消一个线程时的开销，在撤消一个线程时，并不立即回收该线程的资源和 TCB，当以后再要创建一个新线程时，便可直接利用已被撤消但仍保持有资源和 TCB 的线程作为新线程。\n  用户级线程的实现\n用户级线程是在用户空间实现的。所有的用户级线程都具有相同的结构，它们都运行在一个中间系统的上面。当前有两种方式实现的中间系统，即运行时系统和内核控制线程。\n  运行时系统(Runtime System)\n所谓“运行时系统”，实质上是用于管理和控制线程的函数(过程)的集合，其中包括用于创建和撤消线程的函数、线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数，才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间，并作为用户级线程与内核之间的接口。\n在传统的 OS 中，进程在切换时必须先由用户态转为核心态，再由核心来执行切换任务；而用户级线程在切换时则不需转入核心态，而是由运行时系统中的线程切换过程来执行切换任务。该过程将线程的CPU状态保存在该线程的堆栈中，然后按照一定的算法选择一个处于就绪状态的新线程运行，将新线程堆栈中的 CPU 状态装入到CPU相应的寄存器中，一旦将栈指针和程序计数器切换后，便开始了新线程的运行。由于用户级线程的切换无需进入内核，且切换操作简单，因而使用户级线程的切换速度非常快。\n不论在传统的 OS 中，还是在多线程OS中，系统资源都是由内核管理的。在传统的OS 中，进程是利用OS提供的系统调用来请求系统资源的，系统调用通过软中断(如 trap)机制进入OS内核，由内核来完成相应资源的分配。用户级线程是不能利用系统调用的。当线程需要系统资源时，是将该要求传送给运行时系统，由后者通过相应的系统调用来获得系统资源的。\n  内核控制线程\n这种线程又称为轻型进程 LWP(Light Weight Process)。每一个进程都可拥有多个 LWP，同用户级线程一样，每个 LWP 都有自己的数据结构(如TCB)，其中包括线程标识符、优先级、状态，另外还有栈和局部存储区等。它们也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务，这样，当一个用户级线程运行时，只要将它连接到一个LWP 上，此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。\n在一个系统中的用户级线程数量可能很大，为了节省系统开销，不可能设置太多的LWP，而把这些LWP做成一个缓冲池，称为“线程池”。用户进程中的任一用户线程都可以连接到 LWP 池中的任何一个 LWP 上。为使每一用户级线程都能利用LWP与内核通信，可以使多个用户级线程多路复用一个 LWP，但只有当前连接到LWP上的线程才能与内核通信，其余进程或者阻塞，或者等待 LWP。而每一个 LWP 都要连接到一个内核级线程上，这 样，通过 LWP 可把用户级线程与内核线程连接起来，用户级线程可通过 LWP 来访问内核，但内核所看到的总是多个 LWP 而看不到用户级线程。亦即，由 LWP 实现了在内核与用户级线程之间的隔离，从而使用户级线程与内核无关。\n    ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/","summary":"线程的基本概念 线程的引入 如果说，在操作系统中引入进程的目的，是为了使多个程序能并发执行，以提高资源利用率和系统吞吐量，那么，在操作系统中再引入线程，则是为了减少程序在并发执行时所付出的时空开销，使 OS 具有更好的并发性。\n由于进程是一个资源的拥有者，因而在创建、撤消和切换中，系统必须为之付出较大的时空开销。线程作为调度和分派的基本单位。\n线程与进程的比较 线程具有许多传统进程所具有的特征，所以又称为轻型进程(Light-Weight Process)或进程元，相应地把传统进程称为重型进程(Heavy-WeightProcess)\n 调度  在传统的操作系统中，作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中，则把线程作为调度和分派的基本单位，而进程作为资源拥有的基本单位，使线程基本上不拥有资源，这样线程便能轻装前进，从而可显著地提高系统的并发程度。在同一进程中，线程的切换不会引起进程的切换，但从一个进程中的线程切换到另一个进程中的线程时，将会引起进程的切换。\n  并发性\n在引入线程的操作系统中，不仅进程之间可以并发执行，而且在一个进程中的多个线程之间亦可并发执行，使得操作系统具有更好的并发性，从而能更加有效地提高系统资源的利用率和系统的吞吐量\n  拥有资源\n进程是系统中拥有资源的一个基本单位。一般而言，线程自己不拥有系统资源(也有一点必不可少的资源)，但它可以访问其隶属进程的资源，即一个进程的代码段、数据段及所拥有的系统资源，如已打开的文件、I/O设备等，可以供该进程中的所有线程所共享。\n  系统开销\n在创建或撤消进程时，系统都要为之创建和回收进程控制块，分配或回收资源，如内存空间和I/O设备等，操作系统所付出的开销明显大于线程创建或撤消时的开销。类似地，在进程切换时，涉及到当前进程CPU环境的保存及新被调度运行进程的CPU环境的设置，而线程的切换则仅需保存和设置少量寄存器内容，不涉及存储器管理方面的操作，所以就切换代价而言，进程也是远高于线程的。此外，由于一个进程中的多个线程具有相同的地址空间，在同步和通信的实现方面线程也比进程容易。在一些操作系统中，线程的切换、同步和通信都无须操作系统内核的干预。\n  线程的属性   轻型实体\n线程中的实体基本上不拥有系统资源，只是有一点必不可少的、能保证其独立运行的资源，比如，在每个线程中都应具有一个用于控制线程运行的线程控制块 TCB，用于指示被执行指令序列的程序计数器，保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。\n  独立调度和分派的基本单位\n  可并发执行\n  共享进程资源\n  线程的状态   状态参数\n在 OS 中的每一个线程都可以利用线程标识符和一组状态参数进行描述。状态参数通常有这样几项：\n  寄存器状态，它包括程序计数器 PC（存放下一条指令所在单元） 和堆栈指针中的内容；\n  堆栈，在堆栈中通常保存有局部变量和返回地址；\n  线程运行状态，用于描述线程正处于何种运行状态；\n  优先级，描述线程执行的优先程度；\n  线程专有存储器，用于保存线程自己的局部变量拷贝；\n  信号屏蔽，即对某些信号加以屏蔽。","title":"线程的基本概念"},{"content":"经典进程的同步问题 生产者—消费者问题   利用记录型信号量解决\n假定在生产者和消费者之间的公用缓冲池中，具有n个缓冲区，这时可利用互斥信号量mutex实现诸进程对缓冲池的互斥使用。利用信号量emptyCount和fullCount分别表示缓冲池中 空缓冲区 和 满缓冲区 的数量\nmutex, emptyCount, fullCount := 1, n, 0 func proceducer() { wait(emptyCount) wait(mutex) // 生产一个产品，放入一个空的缓冲区中 signal(mutex) signal(fullCount) } func consumer() { wait(fullCount) wait(mutex)； // 消费一个产品，取走一个满缓冲区的内容 signal(mutex)； signal(emptyCount)； }   利用 AND 信号量解决\n对于生产者—消费者问题，也可利用AND信号量来解决，即用 Swait(empty，mutex) 来代替 wait(empty) 和 wait(mutex)；用 Ssignal(mutex，full) 来代替 signal(mutex) 和 signal(full) ；用 Swait(full，mutex) 来代替 wait(full) 和 wait(mutex)，以及用 Ssignal(mutex，empty) 代替 Signal(mutex) 和 Signal(empty)。\nmutex, emptyCount, fullCount := 1, n, 0 func proceducer() { Swait(emptyCount, mutex) // 生产一个产品，放入一个空的缓冲区中 Ssignal(mutex，fullCount) } func consumer() { Swait(fullCount，mutex) // 消费一个产品，取走一个满缓冲区的内容 Ssignal(mutex，emptyCount) }   利用管程解决\n在利用管程方法来解决生产者—消费者问题时，首先便是为它们建立一个管程，并命名为 ProclucerConsumer，或简称为 PC。其中包括两个过程：\n  put(item)过程。生产者利用该过程将自己生产的产品投放到缓冲池中，并用整型变量 count 来表示在缓冲池中已有的产品数目，当 count≥n 时，表示缓冲池已满，生产者须等待。\n  get(item)过程。消费者利用该过程从缓冲池中取出一个产品，当 count≤0 时，表示缓冲池中已无可取用的产品，消费者应等待\n  var notfull，notempty condition type condition struct { queue []processes } func (c *condition) wait() {} func (c *condition) signal() {} type ProclucerConsumer struct { buffer make([]int, count) } func (pc *ProclucerConsumer) put(item int) { if count \u0026gt;= len(pc.buffer) { // 等待非满信号 notfull.wait() } pc.buffer = pc.buffer.append(item) // 在非空队列有进程等待时，唤醒一个等待的进程 if len(notempty.queue) \u0026gt; 0 { notempty.signal() } } func (pc *ProclucerConsumer) get() int { if count \u0026lt;= 0 { // 等待非空信号 notEmpty.wait() } item = pc.buffer[-1] if len(notFull.queue) \u0026gt; 0 { notFull.signal() } return item }   哲学家进餐问题   利用 AND 信号量机制解决\n当哲学家饥饿时，总是先去拿他左边的筷子，即执行 wait(chopstick[i])；成功后，再去拿他右边的筷子，即执行 wait(chopstick[(i+1)mod 5])\nVar chopsiick array of semaphore := (1, 1, 1, 1, 1)； processi repeat think； Sswait(chopstick[(i+1) mod 5]，chopstick[i])； eat； Ssignat(chopstick[(i+1) mod 5]，chopstick[i])； until false；   ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E7%BB%8F%E5%85%B8%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%90%8C%E6%AD%A5%E9%97%AE%E9%A2%98/","summary":"经典进程的同步问题 生产者—消费者问题   利用记录型信号量解决\n假定在生产者和消费者之间的公用缓冲池中，具有n个缓冲区，这时可利用互斥信号量mutex实现诸进程对缓冲池的互斥使用。利用信号量emptyCount和fullCount分别表示缓冲池中 空缓冲区 和 满缓冲区 的数量\nmutex, emptyCount, fullCount := 1, n, 0 func proceducer() { wait(emptyCount) wait(mutex) // 生产一个产品，放入一个空的缓冲区中 signal(mutex) signal(fullCount) } func consumer() { wait(fullCount) wait(mutex)； // 消费一个产品，取走一个满缓冲区的内容 signal(mutex)； signal(emptyCount)； }   利用 AND 信号量解决\n对于生产者—消费者问题，也可利用AND信号量来解决，即用 Swait(empty，mutex) 来代替 wait(empty) 和 wait(mutex)；用 Ssignal(mutex，full) 来代替 signal(mutex) 和 signal(full) ；用 Swait(full，mutex) 来代替 wait(full) 和 wait(mutex)，以及用 Ssignal(mutex，empty) 代替 Signal(mutex) 和 Signal(empty)。\nmutex, emptyCount, fullCount := 1, n, 0 func proceducer() { Swait(emptyCount, mutex) // 生产一个产品，放入一个空的缓冲区中 Ssignal(mutex，fullCount) } func consumer() { Swait(fullCount，mutex) // 消费一个产品，取走一个满缓冲区的内容 Ssignal(mutex，emptyCount) }   利用管程解决","title":"经典进程的同步问题"},{"content":"虚拟存储器的基本概念 虚拟存储器的引入   常规存储器管理方式的特征\n  一次性。在前面所介绍的几种存储管理方式中，都要求将作业全部装入内存后方能运行，即作业在运行前需一次性地全部装入内存，而正是这一特征导致了上述两种情况的发生。此外，还有许多作业在每次运行时，并非其全部程序和数据都要用到。如果一次性地装入其全部程序，也是一种对内存空间的浪费。\n  驻留性。作业装入内存后，便一直驻留在内存中，直至作业运行结束。尽管运行中的进程会因 I/O 而长期等待，或有的程序模块在运行过一次后就不再需要(运行)了，但它们都仍将继续占用宝贵的内存资源。\n    局部性原理\n  程序执行时，除了少部分的转移和过程调用指令外，在大多数情况下仍是顺序执行的。该论点也在后来的许多学者对高级程序设计语言(如 FORTRAN 语言、PASCAL 语言)及 C 语言规律的研究中被证实。\n  过程调用将会使程序的执行轨迹由一部分区域转至另一部分区域，但经研究看出，过程调用的深度在大多数情况下都不超过\n  这就是说，程序将会在一段时间内都局限在这些过程的范围内运行。\n  程序中存在许多循环结构，这些虽然只由少数指令构成，但是它们将多次执行。\n  程序中还包括许多对数据结构的处理，如对数组进行操作，它们往往都局限于很小的范围内。\n  时间局限性。如果程序中的某条指令一旦执行，则不久以后该指令可能再次执行；如果某数据被访问过，则不久以后该数据可能再次被访问。产生时间局限性的典型原因是由于在程序中存在着大量的循环操作。\n  空间局限性。一旦程序访问了某个存储单元，在不久之后，其附近的存储单元也将被访问，即程序在一段时间内所访问的地址，可能集中在一定的范围之内，其典型情况便是程序的顺序执行。\n    虚拟存储器的定义\n基于局部性原理，应用程序在运行之前，没有必要全部装入内存，仅须将那些当前要运行的少数页面或段先装入内存便可运行，其余部分暂留在盘上。程序在运行时，如果它所要访问的页(段)已调入内存，便可继续执行下去；但如果程序所要访问的页(段)尚未调入内存(称为缺页或缺段)，此时程序应利用OS所提供的请求调页(段)功能，将它们调入内存，以使进程能继续执行下去。如果此时内存已满，无法再装入新的页(段)，则还须再利用页(段)的置换功能，将内存中暂时不用的页(段)调至盘上，腾出足够的内存空间后，再将要访问的页(段)调入内存，使程序继续执行下去。这样，便可使一个大的用户程序能在较小的内存空间中运行；也可在内存中同时装入更多的进程使它们并发执行。从用户角度看，该系统所具有的内存容量，将比实际内存容量大得多。但须说明，用户所看到的大容量只是一种感觉，是虚的，故人们把这样的存储器称为虚拟存储器。\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E8%99%9A%E6%8B%9F%E5%AD%98%E5%82%A8%E5%99%A8%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/","summary":"虚拟存储器的基本概念 虚拟存储器的引入   常规存储器管理方式的特征\n  一次性。在前面所介绍的几种存储管理方式中，都要求将作业全部装入内存后方能运行，即作业在运行前需一次性地全部装入内存，而正是这一特征导致了上述两种情况的发生。此外，还有许多作业在每次运行时，并非其全部程序和数据都要用到。如果一次性地装入其全部程序，也是一种对内存空间的浪费。\n  驻留性。作业装入内存后，便一直驻留在内存中，直至作业运行结束。尽管运行中的进程会因 I/O 而长期等待，或有的程序模块在运行过一次后就不再需要(运行)了，但它们都仍将继续占用宝贵的内存资源。\n    局部性原理\n  程序执行时，除了少部分的转移和过程调用指令外，在大多数情况下仍是顺序执行的。该论点也在后来的许多学者对高级程序设计语言(如 FORTRAN 语言、PASCAL 语言)及 C 语言规律的研究中被证实。\n  过程调用将会使程序的执行轨迹由一部分区域转至另一部分区域，但经研究看出，过程调用的深度在大多数情况下都不超过\n  这就是说，程序将会在一段时间内都局限在这些过程的范围内运行。\n  程序中存在许多循环结构，这些虽然只由少数指令构成，但是它们将多次执行。\n  程序中还包括许多对数据结构的处理，如对数组进行操作，它们往往都局限于很小的范围内。\n  时间局限性。如果程序中的某条指令一旦执行，则不久以后该指令可能再次执行；如果某数据被访问过，则不久以后该数据可能再次被访问。产生时间局限性的典型原因是由于在程序中存在着大量的循环操作。\n  空间局限性。一旦程序访问了某个存储单元，在不久之后，其附近的存储单元也将被访问，即程序在一段时间内所访问的地址，可能集中在一定的范围之内，其典型情况便是程序的顺序执行。\n    虚拟存储器的定义\n基于局部性原理，应用程序在运行之前，没有必要全部装入内存，仅须将那些当前要运行的少数页面或段先装入内存便可运行，其余部分暂留在盘上。程序在运行时，如果它所要访问的页(段)已调入内存，便可继续执行下去；但如果程序所要访问的页(段)尚未调入内存(称为缺页或缺段)，此时程序应利用OS所提供的请求调页(段)功能，将它们调入内存，以使进程能继续执行下去。如果此时内存已满，无法再装入新的页(段)，则还须再利用页(段)的置换功能，将内存中暂时不用的页(段)调至盘上，腾出足够的内存空间后，再将要访问的页(段)调入内存，使程序继续执行下去。这样，便可使一个大的用户程序能在较小的内存空间中运行；也可在内存中同时装入更多的进程使它们并发执行。从用户角度看，该系统所具有的内存容量，将比实际内存容量大得多。但须说明，用户所看到的大容量只是一种感觉，是虚的，故人们把这样的存储器称为虚拟存储器。\n  ","title":"虚拟存储器的基本概念"},{"content":"调度算法 先来先服务和短作业(进程)优先调度算法   先来先服务调度算法\n先来先服务(FCFS)调度算法是一种最简单的调度算法，该算法既可用于作业调度，也可用于进程调度。当在作业调度中采用该算法时，每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业，将它们调入内存，为它们分配资源、创建进程，然后放入就绪队列。在进程调度中采用 FCFS算法时，则每次调度是从就绪队列中选择一个最先进入该队列的进程，为之分配处理机，使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。\n  短作业(进程)优先调度算法\n短作业(进程)优先调度算法SJ(P)F，是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业，将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程，将处理机分配给它，使它立即执行并一直执行到完成，或发生某事件而被阻塞放弃处理机时再重新调度。\n  高优先权优先调度算法   非抢占式优先权算法\n在这种方式下，系统一旦把处理机分配给就绪队列中优先权最高的进程后，该进程便一直执行下去，直至完成；或因发生某事件使该进程放弃处理机时，系统方可再将处理机重新分配给另一优先权最高的进程。\n  抢占式优先权调度算法\n在这种方式下，系统同样是把处理机分配给优先权最高的进程，使之执行。但在其执行期间，只要又出现了另一个其优先权更高的进程，进程调度程序就立即停止当前进程(原优先权最高的进程)的执行，重新将处理机分配给新到的优先权最高的进程\n  基于时间片的轮转调度算法   时间片轮转法\n  基本原理\n在早期的时间片轮转法中，系统将所有的就绪进程按先来先服务的原则排成一个队列，每次调度时，把CPU分配给队首进程，并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时，由一个计时器发出时钟中断请求，调度程序便据此信号来停止该进程的执行，并将它送往就绪队列的末尾；然后，再把处理机分配给就绪队列中新的队首进程，同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。\n  时间片大小的确定\n在时间片轮转算法中，时间片的大小对系统性能有很大的影响，如选择很小的时间片将有利于短作业，因为它能较快地完成，但会频繁地发生中断、进程上下文的切换，从而增加系统的开销；反之，如选择太长的时间片，使得每个进程都能在一个时间片内完成，时间片轮转算法便退化为 FCFS 算法，无法满足交互式用户的需求。一个较为可取的大小是，时间片略大于一次典型的交互所需要的时间。这样可使大多数进程在一个时间片内完成。\n    多级反馈队列调度算法\n前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法，仅照顾了短进程而忽略了长进程，而且如果并未指明进程的长度，则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间，而且还可以满足各种类型进程的需要，因而它是目前被公认的一种较好的进程调度算法。\n  应设置多个就绪队列，并为各个队列赋予不同的优先级\n  当一个新进程进入内存后，首先将它放入第一队列的末尾，按 FCFS 原则排队等待调度。当轮到该进程执行时，如它能在该时间片内完成，便可准备撤离系统；如果它在一个时间片结束时尚未完成，调度程序便将该进程转入第二队列的末尾，再同样地按FCFS原则等待调度执行；如果它在第二队列中运行一个时间片后仍未完成，再依次将它放入第三队列，……，如此下去，当一个长作业(进程)从第一队列依次降到第n队列后，在第n队列中便采取按时间片轮转的方式运行。\n  仅当第一队列空闲时，调度程序才调度第二队列中的进程运行\n    ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E8%B0%83%E5%BA%A6%E7%AE%97%E6%B3%95/","summary":"调度算法 先来先服务和短作业(进程)优先调度算法   先来先服务调度算法\n先来先服务(FCFS)调度算法是一种最简单的调度算法，该算法既可用于作业调度，也可用于进程调度。当在作业调度中采用该算法时，每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业，将它们调入内存，为它们分配资源、创建进程，然后放入就绪队列。在进程调度中采用 FCFS算法时，则每次调度是从就绪队列中选择一个最先进入该队列的进程，为之分配处理机，使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。\n  短作业(进程)优先调度算法\n短作业(进程)优先调度算法SJ(P)F，是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业，将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程，将处理机分配给它，使它立即执行并一直执行到完成，或发生某事件而被阻塞放弃处理机时再重新调度。\n  高优先权优先调度算法   非抢占式优先权算法\n在这种方式下，系统一旦把处理机分配给就绪队列中优先权最高的进程后，该进程便一直执行下去，直至完成；或因发生某事件使该进程放弃处理机时，系统方可再将处理机重新分配给另一优先权最高的进程。\n  抢占式优先权调度算法\n在这种方式下，系统同样是把处理机分配给优先权最高的进程，使之执行。但在其执行期间，只要又出现了另一个其优先权更高的进程，进程调度程序就立即停止当前进程(原优先权最高的进程)的执行，重新将处理机分配给新到的优先权最高的进程\n  基于时间片的轮转调度算法   时间片轮转法\n  基本原理\n在早期的时间片轮转法中，系统将所有的就绪进程按先来先服务的原则排成一个队列，每次调度时，把CPU分配给队首进程，并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时，由一个计时器发出时钟中断请求，调度程序便据此信号来停止该进程的执行，并将它送往就绪队列的末尾；然后，再把处理机分配给就绪队列中新的队首进程，同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。\n  时间片大小的确定\n在时间片轮转算法中，时间片的大小对系统性能有很大的影响，如选择很小的时间片将有利于短作业，因为它能较快地完成，但会频繁地发生中断、进程上下文的切换，从而增加系统的开销；反之，如选择太长的时间片，使得每个进程都能在一个时间片内完成，时间片轮转算法便退化为 FCFS 算法，无法满足交互式用户的需求。一个较为可取的大小是，时间片略大于一次典型的交互所需要的时间。这样可使大多数进程在一个时间片内完成。\n    多级反馈队列调度算法\n前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法，仅照顾了短进程而忽略了长进程，而且如果并未指明进程的长度，则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间，而且还可以满足各种类型进程的需要，因而它是目前被公认的一种较好的进程调度算法。\n  应设置多个就绪队列，并为各个队列赋予不同的优先级\n  当一个新进程进入内存后，首先将它放入第一队列的末尾，按 FCFS 原则排队等待调度。当轮到该进程执行时，如它能在该时间片内完成，便可准备撤离系统；如果它在一个时间片结束时尚未完成，调度程序便将该进程转入第二队列的末尾，再同样地按FCFS原则等待调度执行；如果它在第二队列中运行一个时间片后仍未完成，再依次将它放入第三队列，……，如此下去，当一个长作业(进程)从第一队列依次降到第n队列后，在第n队列中便采取按时间片轮转的方式运行。\n  仅当第一队列空闲时，调度程序才调度第二队列中的进程运行\n    ","title":"调度算法"},{"content":"进程同步 进程同步的基本概念   两种形式的制约关系\n  间接相互制约关系\n同处于一个系统中的进程，通常都共享着某种系统资源，如共享CPU、共享 I/O 设备等。所谓间接相互制约即源于这种资源共享，例如，有两个进程 A和 B，如果在A进程提出打印请求时，系统已将惟一的一台打印机分配给了进程 B，则此时进程A只能阻塞；一旦进程B将打印机释放，则A进程才能由阻塞改为就绪状态。\n  直接相互制约关系。\n这种制约主要源于进程间的合作。例如，有一输入进程A通过单缓冲向进程B提供数据。当该缓冲空时，计算进程因不能获得所需数据而阻塞，而当进程A把数据输入缓冲区后，便将进程B唤醒；反之，当缓冲区已满时，进程 A 因不能再向缓冲区投放数据而阻塞，当进程B将缓冲区数据取走后便可唤醒 A。\n    临界资源\n  临界区\n人们把在每个进程中访问临界资源的那段代码称为临界区(critical section)\n  同步机制应遵循的规则\n  空闲让进。当无进程处于临界区时，表明临界资源处于空闲状态，应允许一个请求进入临界区的进程立即进入自己的临界区，以有效地利用临界资源。\n  忙则等待。当已有进程进入临界区时，表明临界资源正在被访问，因而其它试图进入临界区的进程必须等待，以保证对临界资源的互斥访问。\n  有限等待。对要求访问临界资源的进程，应保证在有限时间内能进入自己的临界区，以免陷入“死等”状态。\n  让权等待。当进程不能进入自己的临界区时，应立即释放处理机，以免进程陷入“忙等”状态\n    信号量机制   整型信号量\n把整型信号量定义为一个用于表示资源数目的整型量S，它与一般整型量不同，除初始化外，仅能通过两个标准的原子操作(AtomicOperation)wait(S)和 signal(S)来访问\nfunc wait(s int) { for s \u0026lt;= 0 { } s = s -1 } func signal(s int) { s = s + 1 }   记录型信号量\n在整型信号量机制中的wait操作，只要是信号量S≤0，就会不断地测试。因此，该机制并未遵循“让权等待”的准则，而是使进程处于“忙等”的状态。记录型信号量机制则是一种不存在“忙等”现象的进程同步机制。但在采取了“让权等待”的策略后，又会出现多个进程等待访问同一临界资源的情况。为此，在信号量机制中，除了需要一个用于代表资源数目的整型变量value外，还应增加一个进程链表指针L，用于链接上述的所有等待进程。记录型信号量是由于它采用了记录型的数据结构而得名的。它所包含的上述两个数据项可描述为\ntype semaphore struct { value int // 资源数目 L ListOfProcess // 进程链表指针 } // 意味着进程请求一个单位的该类资源，使系统中可供分配的该类资源数减少一个 func wait(s *semaphore) { s.value = s.value - 1 // 表示该类资源已分配完毕 if s.value \u0026lt; 0 { // 因此进程应调用block原语，进行自我阻塞，放弃处理机，并插入到信号量链表S.L中 block(s.L) } } // 表示执行进程释放一个单位资源，使系统中可供分配的该类资源数增加一个 func signal(s *semaphore) { s.value = s.value + 1 // 若加1后仍是S.value≤0，则表示在该信号量链表中，仍有等待该资源的进程被阻塞，故还应调用wakeup原语，将S.L链表中的第一个等待进程唤醒 if (s.value \u0026lt;= 0) { wakeUp(s.L) } } 如果 S.value 的初值为1，表示只允许一个进程访问临界资源，此时的信号量转化为互斥信号量，用于进程互斥。\n  AND 型信号量\n上述的进程互斥问题，是针对各进程之间只共享一个临界资源而言的。在有些应用场合，是一个进程需要先获得两个或更多的共享资源后方能执行其任务。\nAND 同步机制的基本思想是：将进程在整个运行过程中需要的所有资源，一次性全部地分配给进程，待进程使用完后再一起释放。只要尚有一个资源未能分配给进程，其它所有可能为之分配的资源也不分配给它。\n  信号量集\n在记录型信号量机制中，wait(S)或 signal(S)操作仅能对信号量施以加 1 或减 1 操作，意味着每次只能获得或释放一个单位的临界资源。而当一次需要 N 个某类临界资源时，便要进行N次wait(S)操作，显然这是低效的。此外，在有些情况下，当资源数量低于某一下限值时，便不予以分配。因而，在每次分配之前，都必须测试该资源的数量，看其是否大于其下限值。基于上述两点，可以对 AND 信号量机制加以扩充，形成一般化的“信号量集”机制\nSwait(S，d，d)。此时在信号量集中只有一个信号量S，但允许它每次申请 d 个资源，当现有资源数少于 d 时，不予分配\n  信号量的应用   利用信号量实现进程互斥\n为使多个进程能互斥地访问某临界资源，只须为该资源设置一互斥信号量 mutex，并设 其初始值为 1，然后将各进程访问该资源的临界区 CS 置于 wait(mutex)和 signal(mutex)操作之间即可。这样，每个欲访问该临界资源的进程在进入临界区之前，都要先对mutex执行wait操作，若该资源此刻未被访问，本次 wait 操作必然成功，进程便可进入自己的临界区，这时若再有其他进程也欲进入自己的临界区，此时由于对mutex执行wait操作定会失败，\n  利用信号量实现前趋关系\n还可利用信号量来描述程序或语句之间的前趋关系。设有两个并发执行的进程 P1 和 P2。P1中有语句S1；P2中有语句S2。我们希望在S1执行后再执行 S2。为实现这种前趋关系，我们只须使进程P1和P2共享一个公用信号量 S，并赋予其初值为0，将signal(S)操作放在语句S1后面；而在S2语句前面插入 wait(S)操作，即在进程P1中，用S1；signal(S)；在进程P2中，用wait(S)；S2；由于S被初始化为0，这样，若P2先执行必定阻塞，只有在进程P1执行完 S1；signal(S)；操作后使 S 增为 1 时，P2 进程方能执行语句S2成功。\n  管程机制 虽然信号量机制是一种既方便、又有效的进程同步机制，但每个要访问临界资源的进程都必须自备同步操作wait(S)和signal(S)。这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦，而且还会因同步操作的使用不当而导致系统死锁。这样，在解决上述问题的过程中，便产生了一种新的进程同步工具——管程(Monitors)。\n  管程的定义\n代表共享资源的数据结构，以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序，共同构成了一个操作系统的资源管理模块，我们称之为管程。管程被请求和释放资源的进程所调用。\n  资源抽象化\n系统中的各种硬件资源和软件资源，均可用数据结构抽象地描述其资源特性，即用少量信息和对该资源所执行的操作来表征该资源，而忽略了它们的内部结构和实现细节\n  将对共享数据的操作定义为一组过程\n利用共享数据结构抽象地表示系统中的共享资源，而把对该共享数据结构实施的操作定义为一组过程，如资源的请求和释放过程request和release。进程对共享资源的申请、释放和其它操作，都是通过这组过程对共享数据结构的操作来实现的，这组过程还可以根据资源的情况，或接受或阻塞进程的访问，确保每次仅有一个进程使用共享资源，这样就可以统一管理对共享资源的所有访问，实现进程互斥。\n    管程的组成\n  管程的名称\n  局部于管程内部的共享数据结构说明\n  对该数据结构进行操作的一组过程\n  对局部于管程内部的共享数据设置初始值的语句\n    解释\n管程相当于围墙，它把共享变量和对它进行操作的若干过程围了起来，所有进程要访问临界资源时，都必须经过管程(相当于通过围墙的门)才能进入，而管程每次只准许一个进程进入管程，从而实现了进程互斥\n  管程的特性\n  模块化。管程是一个基本程序单位，可以单独编译。\n  抽象数据类型。管程中不仅有数据，而且有对数据的操作\n  信息掩蔽。管程中的数据结构只能被管程中的过程访问，这些过程也是在管程内部定义的，供管程外的进程调用，而管程中的数据结构以及过程(函数)的具体实现外部不可见。\n    管程和进程不同\n  虽然二者都定义了数据结构，但进程定义的是私有数据结构PCB，管程定义的是公共数据结构，如消息队列等\n  二者都存在对各自数据结构上的操作，但进程是由顺序程序执行有关的操作，而管程主要是进行同步操作和初始化操作\n  设置进程的目的在于实现系统的并发性，而管程的设置则是解决共享资源的互斥使用问题；\n  进程通过调用管程中的过程对共享数据结构实行操作，该过程就如通常的子程序一样被调用，因而管程为被动工作方式，进程则为主动工作方式\n  进程之间能并发执行，而管程则不能与其调用者并发\n  进程具有动态性，由“创建”而诞生，由“撤销”而消亡，而管程则是操作系统中的一个资源管理模块，供进程调用。\n    条件变量\n考虑一种情况：当一个进程调用了管程，在管程中时被阻塞或挂起，直到阻塞或挂起的原因解除，而在此期间，如果该进程不释放管程，则其它进程无法进入管程，被迫长时间地等待。为了解决这个问题，引入了条件变量condition。通常，一个进程被阻塞或挂起的条件(原因)可有多个，因此在管程中设置了多个条件变量，对这些条件变量的访问，只能在管程中进行。\n管程中对每个条件变量都须予以说明，其形式为：Var x，y：condition。对条件变量的操作仅仅是wait和signal，因此条件变量也是一种抽象数据类型，每个条件变量保存了一个链表，用于记录因该条件变量而阻塞的所有进程，同时提供的两个操作即可表示为x.wait和x.signal，其含义为：\n  x.wait：正在调用管程的进程因 x 条件需要被阻塞或挂起，则调用 x.wait 将自己插入到x条件的等待队列上，并释放管程，直到x条件变化。此时其它进程可以使用该管程。\n  x.signal：正在调用管程的进程发现 x 条件发生了变化，则调用 x.signal，重新启动一个因x条件而阻塞或挂起的进程。如果存在多个这样的进程，则选择其中的一个，如果没有，则继续执行原进程，而不产生任何结果。这与信号量机制中的 signal 操作不同，因为后者总是要执行 s:=s+1 操作，因而总会改变信号量的状态。\n    举个例子\n人们到一家叫做计算机的银行取钱，这个银行里面就一个空窗口。最早之前，每个人需要从这个窗口爬进去取钱。这里，银行里面每一个需要取钱的人看作进程，而银行里面的钱可以看做计算机的共享资源，一般是硬件设备或一群共享变量。每个人都向窗口拥挤，场面混乱不堪。后面计算机银行不断改进，发明了一种叫ATM的机器（管程），ATM（管程）封装了钱和对外开放了一些存取钱的操作。这样一来，ATM（管程）在计算机银行的钱和客户之间担任了中介服务的角色。在一个相对封闭的屋子里面，一次只能服务一个人（让进程互斥使用）。ATM屋子里面有人的时候，其他需要依次排队使用。一个人（进程）在ATM使用的时间太长也不行，所以需要一个条件变量（condition）来约束他。条件变量可以让一个线程等待时让另一线程进入管程，这样可以有效防止死锁。\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E8%BF%9B%E7%A8%8B%E5%90%8C%E6%AD%A5/","summary":"进程同步 进程同步的基本概念   两种形式的制约关系\n  间接相互制约关系\n同处于一个系统中的进程，通常都共享着某种系统资源，如共享CPU、共享 I/O 设备等。所谓间接相互制约即源于这种资源共享，例如，有两个进程 A和 B，如果在A进程提出打印请求时，系统已将惟一的一台打印机分配给了进程 B，则此时进程A只能阻塞；一旦进程B将打印机释放，则A进程才能由阻塞改为就绪状态。\n  直接相互制约关系。\n这种制约主要源于进程间的合作。例如，有一输入进程A通过单缓冲向进程B提供数据。当该缓冲空时，计算进程因不能获得所需数据而阻塞，而当进程A把数据输入缓冲区后，便将进程B唤醒；反之，当缓冲区已满时，进程 A 因不能再向缓冲区投放数据而阻塞，当进程B将缓冲区数据取走后便可唤醒 A。\n    临界资源\n  临界区\n人们把在每个进程中访问临界资源的那段代码称为临界区(critical section)\n  同步机制应遵循的规则\n  空闲让进。当无进程处于临界区时，表明临界资源处于空闲状态，应允许一个请求进入临界区的进程立即进入自己的临界区，以有效地利用临界资源。\n  忙则等待。当已有进程进入临界区时，表明临界资源正在被访问，因而其它试图进入临界区的进程必须等待，以保证对临界资源的互斥访问。\n  有限等待。对要求访问临界资源的进程，应保证在有限时间内能进入自己的临界区，以免陷入“死等”状态。\n  让权等待。当进程不能进入自己的临界区时，应立即释放处理机，以免进程陷入“忙等”状态\n    信号量机制   整型信号量\n把整型信号量定义为一个用于表示资源数目的整型量S，它与一般整型量不同，除初始化外，仅能通过两个标准的原子操作(AtomicOperation)wait(S)和 signal(S)来访问\nfunc wait(s int) { for s \u0026lt;= 0 { } s = s -1 } func signal(s int) { s = s + 1 }   记录型信号量","title":"进程同步"},{"content":"进程控制 进程图 子进程可以继承父进程所拥有的资源，例如，继承父进程打开的文件，继承父进程所分配到的缓冲区等。当子进程被撤消时，应将其从父进程那里获得的资源归还给父进程。此外，在撤消父进程时，也必须同时撤消其所有的子进程。为了标识进程之间的家族关系，在PCB中都设置了家族关系表项，以标明自己的父进程及所有的子进程。\n进程的创建 一旦操作系统发现了要求创建新进程的事件后，便调用进程创建原语 Creat( )按下述步骤创建一个新进程。\n  申请空白 PCB\n为新进程申请获得惟一的数字标识符，并从 PCB 集合中索取一个 空白 PCB。\n  为新进程分配资源\n为新进程的程序和数据以及用户栈分配必要的内存空间。\n  初始化进程控制块\n  初始化标识信息\n将系统分配的标识符和父进程标识符填入新 PCB 中\n  初始化处理机状态信息\n使程序计数器指向程序的入口地址，使栈指针指向栈顶；\n  初始化处理机控制信息\n将进程的状态设置为就绪状态或静止就绪状态，对于优先级，通常是将它设置为最低优先级，除非用户以显式方式提出高优先级要求。\n  将新进程插入就绪队列\n如果进程就绪队列能够接纳新进程，便将新进程插入就绪队列\n    进程的终止 如果系统中发生了上述要求终止进程的某事件，OS便调用进程终止原语，按下述过程去终止指定的进程。\n  根据被终止进程的标识符，从PCB集合中检索出该进程的PCB，从中读出该进程的状态。\n  若被终止进程正处于执行状态，应立即终止该进程的执行，并置调度标志为真，用于指示该进程被终止后应重新进行调度。\n  若该进程还有子孙进程，还应将其所有子孙进程予以终止，以防它们成为不可控的进程。\n  将被终止进程所拥有的全部资源，或者归还给其父进程，或者归还给系统。\n  将被终止进程(PCB)从所在队列(或链表)中移出，等待其他程序来搜集信息。\n  进程的阻塞 正在执行的进程，当发现上述某事件时，由于无法继续执行，于是进程便通过调用阻塞原语 block 把自己阻塞。可见，进程的阻塞是进程自身的一种主动行为。进入 block过程后，由于此时该进程还处于执行状态，所以应先立即停止执行，把进控制块中的现行状态由“执行”改为“阻塞”，并将PCB插入阻塞队列。如果系统中设置了因不同事件而阻塞的多个阻塞队列，则应将本进程插入到具有相同事件的阻塞(等待)队列。最后，转调度程序进行重新调度，将处理机分配给另一就绪进程并进行切换，亦即，保留被阻塞进程的处理机状态(在 PCB 中)，再按新进程的 PCB 中的处理机状态设置 CPU 的环境。\n进程唤醒过程 当被阻塞进程所期待的事件出现时，如I/O完成或其所期待的数据已经到达，则由有关进程(比如用完并释放了该I/O设备的进程)调用唤醒原语wakeup()，将等待该事件的进程唤醒。唤醒原语执行的过程是：首先把被阻塞的进程从等待该事件的阻塞队列中移出，将其PCB中的现行状态由阻塞改为就绪，然后再将该 PCB 插入到就绪队列中。\n进程的挂起 当出现了引起进程挂起的事件时，比如，用户进程请求将自己挂起，或父进程请求将自己的某个子进程挂起，系统将利用挂起原语suspend()将指定进程或处于阻塞状态的进程挂起。挂起原语的执行过程是：首先检查被挂起进程的状态，若处于活动就绪状态，便将其改为静止就绪；对于活动阻塞状态的进程，则将之改为静止阻塞。为了方便用户或父进程考查该进程的运行情况而把该进程的 PCB 复制到某指定的内存区域。最后，若被挂起的进程正在执行，则转向调度程序重新调度。\n进程的激活过程 当发生激活进程的事件时，例如，父进程或用户进程请求激活指定进程，若该进程驻留在外存而内存中已有足够的空间时，则可将在外存上处于静止就绪状态的该进程换入内存。这时，系统将利用激活原语active()将指定进程激活。激活原语先将进程从外存调入内存，检查该进程的现行状态，若是静止就绪，便将之改为活动就绪；若为静止阻塞，便将之改为活动阻塞。假如采用的是抢占调度策略，则每当有新进程进入就绪队列时，应检查是否要进行重新调度，即由调度程序将被激活进程与当前进程进行优先级的比较，如果被激活进程的优先级更低，就不必重新调度；否则，立即剥夺当前进程的运行，把处理机分配给刚被激活的进程。\n","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E8%BF%9B%E7%A8%8B%E6%8E%A7%E5%88%B6/","summary":"进程控制 进程图 子进程可以继承父进程所拥有的资源，例如，继承父进程打开的文件，继承父进程所分配到的缓冲区等。当子进程被撤消时，应将其从父进程那里获得的资源归还给父进程。此外，在撤消父进程时，也必须同时撤消其所有的子进程。为了标识进程之间的家族关系，在PCB中都设置了家族关系表项，以标明自己的父进程及所有的子进程。\n进程的创建 一旦操作系统发现了要求创建新进程的事件后，便调用进程创建原语 Creat( )按下述步骤创建一个新进程。\n  申请空白 PCB\n为新进程申请获得惟一的数字标识符，并从 PCB 集合中索取一个 空白 PCB。\n  为新进程分配资源\n为新进程的程序和数据以及用户栈分配必要的内存空间。\n  初始化进程控制块\n  初始化标识信息\n将系统分配的标识符和父进程标识符填入新 PCB 中\n  初始化处理机状态信息\n使程序计数器指向程序的入口地址，使栈指针指向栈顶；\n  初始化处理机控制信息\n将进程的状态设置为就绪状态或静止就绪状态，对于优先级，通常是将它设置为最低优先级，除非用户以显式方式提出高优先级要求。\n  将新进程插入就绪队列\n如果进程就绪队列能够接纳新进程，便将新进程插入就绪队列\n    进程的终止 如果系统中发生了上述要求终止进程的某事件，OS便调用进程终止原语，按下述过程去终止指定的进程。\n  根据被终止进程的标识符，从PCB集合中检索出该进程的PCB，从中读出该进程的状态。\n  若被终止进程正处于执行状态，应立即终止该进程的执行，并置调度标志为真，用于指示该进程被终止后应重新进行调度。\n  若该进程还有子孙进程，还应将其所有子孙进程予以终止，以防它们成为不可控的进程。\n  将被终止进程所拥有的全部资源，或者归还给其父进程，或者归还给系统。\n  将被终止进程(PCB)从所在队列(或链表)中移出，等待其他程序来搜集信息。\n  进程的阻塞 正在执行的进程，当发现上述某事件时，由于无法继续执行，于是进程便通过调用阻塞原语 block 把自己阻塞。可见，进程的阻塞是进程自身的一种主动行为。进入 block过程后，由于此时该进程还处于执行状态，所以应先立即停止执行，把进控制块中的现行状态由“执行”改为“阻塞”，并将PCB插入阻塞队列。如果系统中设置了因不同事件而阻塞的多个阻塞队列，则应将本进程插入到具有相同事件的阻塞(等待)队列。最后，转调度程序进行重新调度，将处理机分配给另一就绪进程并进行切换，亦即，保留被阻塞进程的处理机状态(在 PCB 中)，再按新进程的 PCB 中的处理机状态设置 CPU 的环境。","title":"进程控制"},{"content":"进程的描述 在多道程序环境下，程序的执行属于并发执行，此时它们将失去其封闭性，并具有间断性及不可再现性的特征。这决定了通常的程序是不能参与并发执行的，因为程序执行的结果是不可再现的。这样，程序的运行也就失去了意义。为使程序能并发执行，且为了对并发执行的程序加以描述和控制，人们引入了“进程”的概念。\n  结构特征\n通常的程序是不能并发执行的。为使程序(含数据)能独立运行，应为之配置一进程控制块，即PCB(ProcessControlBlock)；而由 程序段 、相关的 数据段 和 PCB 三部分便构了进程实体。所谓创建进程，实质上是创建进程实体 中的 PCB；而撤消进程，实质上是撤消进程的 PCB，本\n  动态性\n进程的实质是进程实体的一次执行过程，“它由创建而产生，由调度而执行，由撤消而消亡\n  并发性\n这是指多个进程实体同存于内存中，且能在一段时间内同时运行\n  独立性\n指进程实体是一个能独立运行、独立分配资源和独立接受调 度的基本单位\n  异步性\n指进程按各自独立的、不可预知的速度向前推进，或说进程实体按异步方式运行\n  进程的三种基本状态   就绪(Ready)状态\n当进程已分配到除 CPU以外的所有必要资源后，只要再获得CPU，便可立即执行，进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个，通常将它们排成一个队列，称为就绪队列。\n  执行状态\n进程已获得 CPU，其程序正在执行。在单处理机系统中，只有一个进程处于执行状态；在多处理机系统中，则有多个进程处于执行状态。\n  阻塞状态\n正在执行的进程由于发生某事件而暂时无法继续执行时，便放弃处理机而处于暂停状态，亦即进程的执行受到阻塞，把这种暂停状态称为阻塞状态。\n  进程状态转化图\n  挂起状态的引入   引入原因\n  终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时，希望暂时使自己的程序静止下来。亦即，使正在执行的进程暂停执行；若此时用户进程正处于就绪状态而未执行，则该进程暂不接受调度，以便用户研究其执行情况或对程序进行修改。我们把这种静止状态称为挂起状态。\n  父进程请求。有时父进程希望挂起自己的某个子进程，以便考查和修改该子进程，或者协调各子进程间的活动。\n  负荷调节的需要。当实时系统中的工作负荷较重，已可能影响到对实时任务的控制时，可由系统把一些不重要的进程挂起，以保证系统能正常运行。\n  操作系统的需要。操作系统有时希望挂起某些进程，以便检查运行中的资源使用情况或进行记账。\n    状态的切换\n    创建状态和终止状态   创建状态\n为一个新进程创建PCB，并填写必要的管理信息；其次，把该进程转入就绪状态并插入就绪队列之中。当一个新进程被创建时，系统已为其分配了PCB，填写了进程标识等信息，但由于该进程所必需的资源或其它信息，如主存资源尚未分配等，一般而言，此时的进程已拥有了自己的PCB，但进程自身还未进入主存，即创建工作尚未完成，进程还不能被调度运行，其所处的状态就是创建状态。\n  为什么引入创建状态？\n引入创建状态，是为了保证进程的调度必须在创建工作完成后进行，以确保对进程控制块操作的完整性。同时，创建状态的引入，也增加了管理的灵活性，操作系统可以根据系统性能或主存容量的限制，推迟创建状态进程的提交。对于处于创建状态的进程，获得了其所必需的资源，以及对其PCB初始化工作完成后，进程状态便可由创建状态转入就绪状态\n  终止状态\n进程的终止也要通过两个步骤：首先等待操作系统进行善后处理，然后将其 PCB 清零，并将PCB空间返还系统。当一个进程到达了自然结束点，或是出现了无法克服的错误，或是被操作系统所终结，或是被其他有终止权的进程所终结，它将进入终止状态。进入终止态的进程以后不能再执行，但在操作系统中依然保留一个记录，其中保存状态码和一些计时统计数据，供其它进程收集。一旦其它进程完成了对终止状态进程的信息提取之后，操作系统将删除该进程。\n  进程的五种基本状态及转换\n  具有创建、终止和挂起状态的进程状态图\n  进程控制块 进程控制块的作用是使一个在多道程序环境下不能独立运行的程序(含数据)，成为一个能独立运行的基本单位。\n  进程控制块中的信息\n  进程标识符（符用于惟一地标识一个进程）\n  内部标识符。在所有的操作系统中，都为每一个进程赋予了一个惟一的数字标识符，它通常是一个进程的序号。设置内部标识符主要是为了方便系统使用。\n  外部标识符。它由创建者提供，通常是由字母、数字组成，往往是由用户(进程)在访问该进程时使用。为了描述进程的家族关系，还应设置父进程标识及子进程标识。此外，还可设置用户标识，以指示拥有该进程的用户。\n    处理机状态（是进程去记录CPU的状态值，而不是CPU去记录每一个进程的状态值）\n处理机状态信息主要是由处理机的各种寄存器中的内容组成的。处理机在运行时，许多信息都放在寄存器中。当处理机被中断时，所有这些信息都必须保存在 PCB中，以便在该进程重新执行时，能从断点继续执行。这些寄存器包括：\n  通用寄存器\n又称为用户可视寄存器，它们是用户程序可以访问的，用于暂存信息，在大多数处理机中，有 8～32 个通用寄存器，在 RISC 结构的计算机中可超过 100 个；\n  指令计数器\n其中存放了要访问的下一条指令的地址；\n  程序状态字 PSW\n其中含有状态信息，如条件码、执行方式、中断屏蔽标志等；\n  用户栈指针\n指每个用户进程都有一个或若干个与之相关的系统栈，用于存放过程和系统调用参数及调用地址，栈指针指向该栈的栈顶。\n      进程调度信息\n  进程状态\n指明进程的当前状态，作为进程调度和对换时的依据\n  进程优先级\n用于描述进程使用处理机的优先级别的一个整数，优先级高的进程应优先获得处理机\n  进程调度所需的其它信息\n它们与所采用的进程调度算法有关，比如，进程已等待 CPU 的时间总和、进程已执行的时间总和等\n    进程控制信息\n  程序和数据的地址\n指进程的程序和数据所在的内存或外存地(首)址，以便再调度到该进程执行时，能从 PCB 中找到其程序和数据\n  进程同步和通信机制\n指实现进程同步和进程通信时必需的机制，如 消息队列 指针、信号量等，它们可能全部或部分地放在 PCB 中\n      进程控制块的组织方式 在一个系统中，通常可拥有数十个、数百个乃至数千个PCB。为了能对它们加以有效的管理，应该用适当的方式将这些PCB组织起来。目前常用的组织方式有以下两种。\n  链接方式\n这是把具有同一状态的PCB，用其中的链接字链接成一个队列。这样，可以形成就绪队列、若干个阻塞队列和空白队列等。对其中的就绪队列常按进程优先级的高低排列，把优先级高的进程的PCB排在队列前面。此外，也可根据阻塞原因的不同而把处于阻塞状态的进程的PCB排成等待I/O操作完成的队列和等待分配内存的队列等。\n  索引方式\n系统根据所有进程的状态建立几张索引表。例如，就绪索引表、阻塞索引表等，并把各索引表在内存的首地址记录在内存的一些专用单元中。在每个索引表的表目中，记录具有相应状态的某个 PCB 在 PCB 表中的地址。\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E8%BF%9B%E7%A8%8B%E7%9A%84%E6%8F%8F%E8%BF%B0/","summary":"进程的描述 在多道程序环境下，程序的执行属于并发执行，此时它们将失去其封闭性，并具有间断性及不可再现性的特征。这决定了通常的程序是不能参与并发执行的，因为程序执行的结果是不可再现的。这样，程序的运行也就失去了意义。为使程序能并发执行，且为了对并发执行的程序加以描述和控制，人们引入了“进程”的概念。\n  结构特征\n通常的程序是不能并发执行的。为使程序(含数据)能独立运行，应为之配置一进程控制块，即PCB(ProcessControlBlock)；而由 程序段 、相关的 数据段 和 PCB 三部分便构了进程实体。所谓创建进程，实质上是创建进程实体 中的 PCB；而撤消进程，实质上是撤消进程的 PCB，本\n  动态性\n进程的实质是进程实体的一次执行过程，“它由创建而产生，由调度而执行，由撤消而消亡\n  并发性\n这是指多个进程实体同存于内存中，且能在一段时间内同时运行\n  独立性\n指进程实体是一个能独立运行、独立分配资源和独立接受调 度的基本单位\n  异步性\n指进程按各自独立的、不可预知的速度向前推进，或说进程实体按异步方式运行\n  进程的三种基本状态   就绪(Ready)状态\n当进程已分配到除 CPU以外的所有必要资源后，只要再获得CPU，便可立即执行，进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个，通常将它们排成一个队列，称为就绪队列。\n  执行状态\n进程已获得 CPU，其程序正在执行。在单处理机系统中，只有一个进程处于执行状态；在多处理机系统中，则有多个进程处于执行状态。\n  阻塞状态\n正在执行的进程由于发生某事件而暂时无法继续执行时，便放弃处理机而处于暂停状态，亦即进程的执行受到阻塞，把这种暂停状态称为阻塞状态。\n  进程状态转化图\n  挂起状态的引入   引入原因\n  终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时，希望暂时使自己的程序静止下来。亦即，使正在执行的进程暂停执行；若此时用户进程正处于就绪状态而未执行，则该进程暂不接受调度，以便用户研究其执行情况或对程序进行修改。我们把这种静止状态称为挂起状态。\n  父进程请求。有时父进程希望挂起自己的某个子进程，以便考查和修改该子进程，或者协调各子进程间的活动。\n  负荷调节的需要。当实时系统中的工作负荷较重，已可能影响到对实时任务的控制时，可由系统把一些不重要的进程挂起，以保证系统能正常运行。\n  操作系统的需要。操作系统有时希望挂起某些进程，以便检查运行中的资源使用情况或进行记账。","title":"进程的描述操作系统引论"},{"content":"进程通信的类型 共享存储器系统 相互通信的进程共享某些数据结构或共享存储区，进程之间能够通过这些空间进行通信\n  基于共享数据结构的通信方式\n在这种通信方式中，要求诸进程公用某些数据结构，借以实现诸进程间的信息交换。如在生产者—消费者问题中，就是用 有界缓冲区 这种数据结构来实现通信的。\n  基于共享存储区的通信方式\n为了传输大量数据，在存储器中划出了一块 共享存储区，诸进程可通过对共享存储区中数据的读或写来实现通信。这种通信方式属于高级通信。进程在通信前，先向系统申请获得共享存储区中的一个分区，并指定该分区的关键字；若系统已经给其他进程分配了这样的分区，则将该分区的描述符返回给申请者，继之，由申请者把获得的共享存储分区连接到本进程上；此后，便可像读、写普通存储器一样地读、写该公用存储分区。\n  消息传递系统   进程间的数据交换是以格式化的消息(message)为单位的\n  直接通信方式\n这是指发送进程利用OS所提供的发送命令，直接把消息发送给目标进程。此时，要求发送进程和接收进程都以显式方式提供对方的标识符。通常，系统提供下述两条通信命令(原语)：\nSend(Receiver，message)； 发送一个消息给接收进程； Receive(Sender，message)； 接收 Sender 发来的消息；   间接通信方式\n间接通信方式指进程之间的通信需要通过作为共享数据结构的实体。该实体用来暂存发送进程发送给目标进程的消息；接收进程则从该实体中取出对方发送给自己的消息。通常把这种中间实体称为信箱。消息在信箱中可以安全地保存，只允许核准的目标用户随时读取。因此，利用信箱通信方式，既可实现实时通信，又可实现非实时通信\nSend(mailbox，message)； 将一个消息发送到指定信箱； Receive(mailbox，message)； 从指定信箱中接收一个消息；   信箱的分类\n  私用信箱\n信箱的拥有者有权从信箱中读取消息，其他用户则只能将自己构成的消息发送到该信箱中。\n  公用信箱 它由操作系统创建，并提供给系统中的所有核准进程使用。核准进程既可把消息发送到该信箱中，也可从信箱中读取发送给自己的消息\n  共享信箱\n它由某进程创建，在创建时或创建后指明它是可共享的，同时须指出共享进程(用户)的名字。信箱的拥有者和共享者都有权从信箱中取走发送给自己的消息。可以是一对一关系、多对一关系、一对多关系、多对多关系\n        管道通信系统 所谓“管道”，是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件，又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程)，以字符流形式将大量的数据送入管道；而接受管道输出的接收进程(即读进程)，则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的，故又称为管道通信。\n管道机制必须提供以下三方面的协调能力:\n  互斥\n当一个进程正在对 pipe执行读/写操作时，其它(另一)进程必须等待。\n  同步\n指当写(输入)进程把一定数量(如 4 KB)的数据写入pipe，便去睡眠等待，直到读(输出)进程取走数据后，再把它唤醒。当读进程读一空 pipe时，也应睡眠等待，直至写进程将数据写入管道后，才将之唤醒。\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1%E7%9A%84%E7%B1%BB%E5%9E%8B/","summary":"进程通信的类型 共享存储器系统 相互通信的进程共享某些数据结构或共享存储区，进程之间能够通过这些空间进行通信\n  基于共享数据结构的通信方式\n在这种通信方式中，要求诸进程公用某些数据结构，借以实现诸进程间的信息交换。如在生产者—消费者问题中，就是用 有界缓冲区 这种数据结构来实现通信的。\n  基于共享存储区的通信方式\n为了传输大量数据，在存储器中划出了一块 共享存储区，诸进程可通过对共享存储区中数据的读或写来实现通信。这种通信方式属于高级通信。进程在通信前，先向系统申请获得共享存储区中的一个分区，并指定该分区的关键字；若系统已经给其他进程分配了这样的分区，则将该分区的描述符返回给申请者，继之，由申请者把获得的共享存储分区连接到本进程上；此后，便可像读、写普通存储器一样地读、写该公用存储分区。\n  消息传递系统   进程间的数据交换是以格式化的消息(message)为单位的\n  直接通信方式\n这是指发送进程利用OS所提供的发送命令，直接把消息发送给目标进程。此时，要求发送进程和接收进程都以显式方式提供对方的标识符。通常，系统提供下述两条通信命令(原语)：\nSend(Receiver，message)； 发送一个消息给接收进程； Receive(Sender，message)； 接收 Sender 发来的消息；   间接通信方式\n间接通信方式指进程之间的通信需要通过作为共享数据结构的实体。该实体用来暂存发送进程发送给目标进程的消息；接收进程则从该实体中取出对方发送给自己的消息。通常把这种中间实体称为信箱。消息在信箱中可以安全地保存，只允许核准的目标用户随时读取。因此，利用信箱通信方式，既可实现实时通信，又可实现非实时通信\nSend(mailbox，message)； 将一个消息发送到指定信箱； Receive(mailbox，message)； 从指定信箱中接收一个消息；   信箱的分类\n  私用信箱\n信箱的拥有者有权从信箱中读取消息，其他用户则只能将自己构成的消息发送到该信箱中。\n  公用信箱 它由操作系统创建，并提供给系统中的所有核准进程使用。核准进程既可把消息发送到该信箱中，也可从信箱中读取发送给自己的消息\n  共享信箱\n它由某进程创建，在创建时或创建后指明它是可共享的，同时须指出共享进程(用户)的名字。信箱的拥有者和共享者都有权从信箱中取走发送给自己的消息。可以是一对一关系、多对一关系、一对多关系、多对多关系\n        管道通信系统 所谓“管道”，是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件，又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程)，以字符流形式将大量的数据送入管道；而接受管道输出的接收进程(即读进程)，则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的，故又称为管道通信。\n管道机制必须提供以下三方面的协调能力:\n  互斥\n当一个进程正在对 pipe执行读/写操作时，其它(另一)进程必须等待。\n  同步","title":"进程通信的类型"},{"content":"连续分配存储管理方式 固定分区分配 将内存用户空间划分为若干个固定大小的区域，在每个分区中只装入一道作业，这样，把用户空间划分为几个分区，便允许有几道作业并发运行。当有一空闲分区时，便可以再从外存的后备作业队列中选择一个适当大小的作业装入该分区，当该作业结束时，又可再从后备作业队列中找出另一作业调入该分区。\n  划分分区的方法\n  分区大小相等，即使所有的内存分区大小相等。其缺点是缺乏灵活性，即当程序太小时，会造成内存空间的浪费；当程序太大时，一个分区又不足以装入该程序，致使该程序无法运行。尽管如此，这种划分方式仍被用于利用一台计算机去控制多个相同对象的场合，因为这些对象所需的内存空间是大小相等的。例\n  分区大小不等。为了克服分区大小相等而缺乏灵活性的这个缺点，可把内存区划分成含有多个较小的分区、适量的中等分区及少量的大分区。这样，便可根据程序的大小为之分配适当的分区。\n    内存分配\n为了便于内存分配，通常将分区按大小进行排队，并为之建立一张分区使用表，其中各表项包括每个分区的起始地址、大小及状态(是否已分配)，当有一用户程序要装入时，由内存分配程序检索该表，从中找出一个能满足要求的、尚未分配的分区，将之分配给该程序，然后将该表项中的状态置为“已分配”；若未找到大小足够的分区，则拒绝为该用户程序分配内存。\n  动态分区分配   分区分配中的数据结构\n系统中配置着相应的数据结构，用来描述空闲分区和已分配分区的情况，为分配提供依据。常用的数据结构有以下两种形式\n  空闲分区表\n在系统中设置一张空闲分区表，用于记录每个空闲分区的情况。每个空闲分区占一个表目，表目中包括分区序号、分区始址及分区的大小等数据项。\n  空闲分区链\n为了实现对空闲分区的分配和链接，在每个分区的起始部分，设置一些用于控制分区分配的信息，以及用于链接各分区所用的前向指针；在分区尾部则设置一后向指针，通过前、后向链接指针，可将所有的空闲分区链接成一个双向链\n    分区分配算法\n为把一个新作业装入内存，须按照一定的分配算法，从空闲分区表或空闲分区链中选出一分区分配给该作业。\n  首次适应算法(first fit)\n我们以空闲分区链为例来说明采用FF算法时的分配情况。FF算法要求空闲分区链以地址递增的次序链接。在分配内存时，从链首开始顺序查找，直至找到一个大小能满足要求的空闲分区为止；然后再按照作业的大小，从该分区中划出一块内存空间分配给请求者，余下的空闲分区仍留在空闲链中。若从链首直至链尾都不能找到一个能满足要求的分区，则此次内存分配失败，返回。该算法倾向于优先利用内存中低址部分的空闲分区，从而保留了高址部分的大空闲区。这给为以后到达的大作业分配大的内存空间创造了条件。其缺点是低址部分不断被划分，会留下许多难以利用的、很小的空闲分区，而每次查找又都是从低址部分开始，这无疑会增加查找可用空闲分区时的开销。\n  循环首次适应算法(next fit)\n该算法是由首次适应算法演变而成的。在为进程分配内存空间时，不再是每次都从链首开始查找，而是从上次找到的空闲分区的下一个空闲分区开始查找，直至找到一个能满足要求的空闲分区，从中划出一块与请求大小相等的内存空间分配给作业。为实现该算法，应设置一起始查寻指针，用于指示下一次起始查寻的空闲分区，并采用循环查找方式，即如果最后一个(链尾)空闲分区的大小仍不能满足要求，则应返回到第一个空闲分区，比较其大小是否满足要求。找到后，应调整起始查寻指针。该算法能使内存中的空闲分区分布得更均匀，从而减少了查找空闲分区时的开销，但这样会缺乏大的空闲分区。\n  最佳适应算法(best fit)\n所谓“最佳”是指每次为作业分配内存时，总是把能满足要求、又是最小的空闲分区分配给作业，避免“大材小用”。为了加速寻找，该算法要求将所有的空闲分区按其容量以从小到大的顺序形成一空闲分区链。这样，第一次找到的能满足要求的空闲区，必然是最佳的。孤立地看，最佳适应算法似乎是最佳的，然而在宏观上却不一定。因为每次分配后所切割下来的剩余部分总是最小的，这样，在存储器中会留下许多难以利用的小空闲区。\n  最坏适应算法(worst fit)\n最坏适应分配算法要扫描整个空闲分区表或链表，总是挑选一个最大的空闲区分割给作业使用，其优点是可使剩下的空闲区不至于太小，产生碎片的几率最小，对中、小作业有利，同时最坏适应分配算法查找效率很高。该算法要求将所有的空闲分区按其容量以从大到小的顺序形成一空闲分区链，查找时只要看第一个分区能否满足作业要求。但是该算法的缺点也是明显的，它会使存储器中缺乏大的空闲分区。最坏适应算法与前面所述的首次适应算法、循环首次适应算法、最佳适应算法一起，也称为顺序搜索法。\n  快速适应算法(quick fit)\n该算法又称为分类搜索法，是将空闲分区根据其容量大小进行分类，对于每一类具有相同容量的所有空闲分区，单独设立一个空闲分区链表，这样，系统中存在多个空闲分区链表，同时在内存中设立一张管理索引表，该表的每一个表项对应了一种空闲分区类型，并记录了该类型空闲分区链表表头的指针。空闲分区的分类是根据进程常用的空间大小进行划分，如 2 KB、4 KB、8 KB 等，对于其它大小的分区，如 7 KB 这样的空闲区，既可以放在 8 KB 的链表中，也可以放在一个特殊的空闲区链表中。\n该算法的优点是查找效率高，仅需要根据进程的长度，寻找到能容纳它的最小空闲区链表，并取下第一块进行分配即可。另外该算法在进行空闲分区分配时，不会对任何分区产生分割，所以能保留大的分区，满足对大空间的需求，也不会产生内存碎片。\n该算法的缺点是在分区归还主存时算法复杂，系统开销较大。此外，该算法在分配空闲分区时是以进程为单位，一个分区块只属于一个进程，因此在为进程所分配的一个分区块中，或多或少地存在一定的浪费。空闲分区划分越细，浪费则越严重，整体上会造成可观的存储空间浪费，这是典型的以空间换时间的作法。\n    分区分配操作\n  分配内存\n系统应利用某种分配算法，从空闲分区链(表)中找到所需大小的分区。设请求的分区大小为 u.size，表中每个空闲分区的大小可表示为m.size。若m.size-u.size≤size(size 是事先规定的不再切割的剩余分区的大小)，说明多余部分太小，可不再切割，将整个分区分配给请求者；否则(即多余部分超过size)，从该分区中按请求的大小划分出一块内存空间分配出去，余下的部分仍留在空闲分区链(表)中。然后，将分配区的首址返回给调用者。\n  回收内存\n当进程运行完毕释放内存时，系统根据回收区的首址，从空闲区链(表)中找到相应的插入点，此时可能出现以下四种情况之一:\n  回收区与插入点的前一个空闲分区F1相邻接。此时应将回收区与插入点的前一分区合并，不必为回收分区分配新表项，而只需修改其前一分区 F1的大小。\n  回收分区与插入点的后一空闲分区F2相邻接。此时也可将两分区合并，形成新的空闲分区，但用回收区的首址作为新空闲区的首址，大小为两者之和。\n  回收区同时与插入点的前、后两个分区邻接此时将三个分区合并，使用 F1的表项和 F1的首址，取消 F2的表项，大小为三者之和。\n  回收区既不与F1邻接，又不与F2邻接。这时应为回收区单独建立一新表项，填写回收区的首址和大小，并根据其首址插入到空闲链中的适当位置。\n      伙伴系统 固定分区和动态分区方式都有不足之处。固定分区方式限制了活动进程的数目，当进程大小与空闲分区大小不匹配时，内存空间利用率很低。动态分区方式算法复杂，回收空 闲分区时需要进行分区合并等，系统开销较大。伙伴系统方式是对以上两种内存方式的一种折衷方案。\n伙伴系统规定，无论已分配分区或空闲分区，其大小均为2的k次幂，k为整数，l≤k≤m，其中：$ 2^l $ 表示分配的最小分区的大小，$ 2^m $ 表示分配的最大分区的大小，通常 $ 2^m $ 是整个可分配内存的大小。假设系统的可利用空间容量为 $2^m$ 个字，则系统开始运行时，整个内存区是一个大小为 $ 2^m $ 的空闲分区。在系统运行过程中，由于不断的划分，可能会形成若干个不连续的空闲分区，将这些空闲分区根据分区的大小进行分类，对于每一类具有相同大小的所有空闲分区，单独设立一个空闲分区双向链表。这样，不同大小的空闲分区形成了 k(0 ≤ k ≤ m) 个空闲分区链表。当需要为进程分配一个长度为n的存储空间时，首先计算一个 i 值，使 $ 2^(i－1^) \u0026lt; n ≤ 2^i $，然后在空闲分区大小为 $ 2^i $ 的空闲分区链表中查找。若找到，即把该空闲分区分配给进程。否则，表明长度为 $ 2^i $ 的空闲分区已经耗尽，则在分区大小为 $2^(i＋1^)$ 的空闲分区链表中寻找。若存在 $ 2^(i＋1^) $ 的一个空闲分区，则把该空闲分区分为相等的两个分区，这两个分区称为一对伙伴，其中的一个分区用于分配，而把另一个加入分区大小为 $ 2^i $ 的空闲分区链表中。\n若大小为 $ 2^(i＋1^) $ 的空闲分区也不存在，则需要查找大小为 $ 2^(i＋2^) $ 的空闲分区，若找到则对其进行两次分割：第一次，将其分割为大小为 $2^(i＋1^)$的两个分区，一个用于分配，一个加入到大小为$2^(i＋1^)$的空闲分区链表中；第二次，将第一次用于分配的空闲区分割为 $2^i$ 的两个分区，一个用于分配，一个加入到大小为$2^i$的空闲分区链表中。若仍然找不到，则继续查找大小为$2^(i＋3^)$的空闲分区，以此类推。由此可见，在最坏的情况下，可能需要对$2^k$的空闲分区进行k次分割才能得到所需分区。\n与一次分配可能要进行多次分割一样，一次回收也可能要进行多次合并，如回收大小为 $2^i$ 的空闲分区时，若事先已存在$2^i$的空闲分区时，则应将其与伙伴分区合并为大小为$2^(i＋1^)$的空闲分区，若事先已存在$2^(i＋1^)$的空闲分区时，又应继续与其伙伴分区合并为大小为$2^(i＋2^)$的空闲分区，依此类推。\n哈希算法 在上述的分类搜索算法和伙伴系统算法中，都是将空闲分区根据分区大小进行分类，对于每一类具有相同大小的空闲分区，单独设立一个空闲分区链表。\n在为进程分配空间时，需要在一张管理索引表中查找到所需空间大小所对应的表项，从中得到对应的空闲分区链表表头指针，从而通过查找得到一个空闲分区。如果对空闲分区分类较细，则相应的空闲分区链表也较多，因此选择合适的空闲链表的开销也相应增加，且时间性能降低。\n哈希算法就是利用哈希快速查找的优点，以及空闲分区在可利用空间表中的分布规律，建立哈希函数，构造一张以空闲分区大小为关键字的哈希表，该表的每一个表项记录了一个对应的空闲分区链表表头指针。当进行空闲分区分配时，根据所需空闲分区大小，通过哈希函数计算，即得到在哈希表中的位置，从中得到相应的空闲分区链表，实现最佳分配策略。\n可重定位分区分配   动态重定位的引入\n在连续分配方式中，必须把一个系统或用户程序装入一连续的内存空间。如果在系统中只有若干个小的分区，即使它们容量的总和大于要装入的程序，但由于这些分区不相邻接，也无法把该程序装入内存。\n若想把作业装入，可采用的一种方法是：将内存中的所有作业进行移动，使它们全都相邻接，这样，即可把原来分散的多个小分区拼接成一个大分区，这时就可把作业装入该区。这种通过移动内存中作业的位置，以把原来多个分散的小分区拼接成一个大分区的方法，称为“拼接”或“紧凑”，由于经过紧凑后的某些用户程序在内存中的位置发生了变化，此时若不对程序和数据的地址加以修改(变换)，则程序必将无法执行。为此，在每次“紧凑”后，都必须对移动了的程序或数据进行重定位。\n  动态重定位的实现\n在动态运行时装入的方式中，作业装入内存后的所有地址都仍然是相对地址，将相对地址转换为物理地址的工作，被推迟到程序指令要真正执行时进行。为使地址的转换不会影响到指令的执行速度，必须有硬件地址变换机构的支持，即须在系统中增设一个重定位寄存器，用它来存放程序(数据)在内存中的起始地址。程序在执行时，真正访问的内存地址是相对地址与重定位寄存器中的地址相加而形成的。\n地址变换过程是在程序执行期间，随着对每条指令或数据的访问自动进行的，故称为动态重定位。当系统对内存进行了“紧凑”而使若干程序从内存的某处移至另一处时，不需对程序做任何修改，只要用该程序在内存的新起始地址，去置换原来的起始地址即可。\n  动态重定位分区分配算法\n动态重定位分区分配算法与动态分区分配算法基本上相同，差别仅在于：在这种分配算法中，增加了紧凑的功能，通常，在找不到足够大的空闲分区来满足用户需求时进行紧凑。\n  ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-02-%E8%BF%9E%E7%BB%AD%E5%88%86%E9%85%8D%E5%AD%98%E5%82%A8%E7%AE%A1%E7%90%86%E6%96%B9%E5%BC%8F/","summary":"连续分配存储管理方式 固定分区分配 将内存用户空间划分为若干个固定大小的区域，在每个分区中只装入一道作业，这样，把用户空间划分为几个分区，便允许有几道作业并发运行。当有一空闲分区时，便可以再从外存的后备作业队列中选择一个适当大小的作业装入该分区，当该作业结束时，又可再从后备作业队列中找出另一作业调入该分区。\n  划分分区的方法\n  分区大小相等，即使所有的内存分区大小相等。其缺点是缺乏灵活性，即当程序太小时，会造成内存空间的浪费；当程序太大时，一个分区又不足以装入该程序，致使该程序无法运行。尽管如此，这种划分方式仍被用于利用一台计算机去控制多个相同对象的场合，因为这些对象所需的内存空间是大小相等的。例\n  分区大小不等。为了克服分区大小相等而缺乏灵活性的这个缺点，可把内存区划分成含有多个较小的分区、适量的中等分区及少量的大分区。这样，便可根据程序的大小为之分配适当的分区。\n    内存分配\n为了便于内存分配，通常将分区按大小进行排队，并为之建立一张分区使用表，其中各表项包括每个分区的起始地址、大小及状态(是否已分配)，当有一用户程序要装入时，由内存分配程序检索该表，从中找出一个能满足要求的、尚未分配的分区，将之分配给该程序，然后将该表项中的状态置为“已分配”；若未找到大小足够的分区，则拒绝为该用户程序分配内存。\n  动态分区分配   分区分配中的数据结构\n系统中配置着相应的数据结构，用来描述空闲分区和已分配分区的情况，为分配提供依据。常用的数据结构有以下两种形式\n  空闲分区表\n在系统中设置一张空闲分区表，用于记录每个空闲分区的情况。每个空闲分区占一个表目，表目中包括分区序号、分区始址及分区的大小等数据项。\n  空闲分区链\n为了实现对空闲分区的分配和链接，在每个分区的起始部分，设置一些用于控制分区分配的信息，以及用于链接各分区所用的前向指针；在分区尾部则设置一后向指针，通过前、后向链接指针，可将所有的空闲分区链接成一个双向链\n    分区分配算法\n为把一个新作业装入内存，须按照一定的分配算法，从空闲分区表或空闲分区链中选出一分区分配给该作业。\n  首次适应算法(first fit)\n我们以空闲分区链为例来说明采用FF算法时的分配情况。FF算法要求空闲分区链以地址递增的次序链接。在分配内存时，从链首开始顺序查找，直至找到一个大小能满足要求的空闲分区为止；然后再按照作业的大小，从该分区中划出一块内存空间分配给请求者，余下的空闲分区仍留在空闲链中。若从链首直至链尾都不能找到一个能满足要求的分区，则此次内存分配失败，返回。该算法倾向于优先利用内存中低址部分的空闲分区，从而保留了高址部分的大空闲区。这给为以后到达的大作业分配大的内存空间创造了条件。其缺点是低址部分不断被划分，会留下许多难以利用的、很小的空闲分区，而每次查找又都是从低址部分开始，这无疑会增加查找可用空闲分区时的开销。\n  循环首次适应算法(next fit)\n该算法是由首次适应算法演变而成的。在为进程分配内存空间时，不再是每次都从链首开始查找，而是从上次找到的空闲分区的下一个空闲分区开始查找，直至找到一个能满足要求的空闲分区，从中划出一块与请求大小相等的内存空间分配给作业。为实现该算法，应设置一起始查寻指针，用于指示下一次起始查寻的空闲分区，并采用循环查找方式，即如果最后一个(链尾)空闲分区的大小仍不能满足要求，则应返回到第一个空闲分区，比较其大小是否满足要求。找到后，应调整起始查寻指针。该算法能使内存中的空闲分区分布得更均匀，从而减少了查找空闲分区时的开销，但这样会缺乏大的空闲分区。\n  最佳适应算法(best fit)\n所谓“最佳”是指每次为作业分配内存时，总是把能满足要求、又是最小的空闲分区分配给作业，避免“大材小用”。为了加速寻找，该算法要求将所有的空闲分区按其容量以从小到大的顺序形成一空闲分区链。这样，第一次找到的能满足要求的空闲区，必然是最佳的。孤立地看，最佳适应算法似乎是最佳的，然而在宏观上却不一定。因为每次分配后所切割下来的剩余部分总是最小的，这样，在存储器中会留下许多难以利用的小空闲区。\n  最坏适应算法(worst fit)\n最坏适应分配算法要扫描整个空闲分区表或链表，总是挑选一个最大的空闲区分割给作业使用，其优点是可使剩下的空闲区不至于太小，产生碎片的几率最小，对中、小作业有利，同时最坏适应分配算法查找效率很高。该算法要求将所有的空闲分区按其容量以从大到小的顺序形成一空闲分区链，查找时只要看第一个分区能否满足作业要求。但是该算法的缺点也是明显的，它会使存储器中缺乏大的空闲分区。最坏适应算法与前面所述的首次适应算法、循环首次适应算法、最佳适应算法一起，也称为顺序搜索法。\n  快速适应算法(quick fit)\n该算法又称为分类搜索法，是将空闲分区根据其容量大小进行分类，对于每一类具有相同容量的所有空闲分区，单独设立一个空闲分区链表，这样，系统中存在多个空闲分区链表，同时在内存中设立一张管理索引表，该表的每一个表项对应了一种空闲分区类型，并记录了该类型空闲分区链表表头的指针。空闲分区的分类是根据进程常用的空间大小进行划分，如 2 KB、4 KB、8 KB 等，对于其它大小的分区，如 7 KB 这样的空闲区，既可以放在 8 KB 的链表中，也可以放在一个特殊的空闲区链表中。","title":"连续分配存储管理方式"},{"content":"Golang与Java的区别 1. 结构体 -\u0026gt; 类 // 包名即为包含该文件的目录名字 package collection // 声明一个结构体，类似Java中的类 type Stack struct { data []string } // 声明一个Push函数，并通过一个Stack的指针对象实现该方法 // 类似声明了Stack的成员方法 func (s *Stack) Push(x string) { s.data = append(s.data, x) } func (s *Stack) Pop() string { n := len(s.data) - 1 res := s.data[n] s.data[n] = \u0026#34;\u0026#34; // to avoid memory leak  s.data = s.data[:n] return res } func (s *Stack) Size() int { return len(s.data) }   结构体对应Java里的类, 但结构体里只能有变量，不能有方法\n  方法名称前面的 (stack *Stack) 声明了该方法的接收者，对应Java中的 this\n  := 操作符声明并初始化了一个变量，变量的类型取决于操作符右边的表达式\n  package collection_test import ( \u0026#34;fmt\u0026#34; \u0026#34;lambert.com/collection\u0026#34; // 引入包 ) func ExampleStack() { var s collection.Stack // 可以通过包名+结构体的名称使用  s.Push(\u0026#34;world!\u0026#34;) s.Push(\u0026#34;Hello, \u0026#34;) for s.Size() \u0026gt; 0 { fmt.Print(s.Pop()) } fmt.Println() } 主要区别   面向对象\n  Go中没有带有构造函数的类，没有实例方法，也没有类继承；只有动态的方法查找，Go只提供了结构体和接口\n  Go允许将方法置于任何类型上，不需要封装；方法的接收者对应Java中的this, 可以是值或指针\n  Go提供了两种访问级别，对应Java中的public和private。以大写字母开头的声明是public的，其他都是包私有的\n    函数式编程\n 函数是Go中的一等公民，函数可以在方法的参数上传递    指针和引用\n  Go提供了任意类型的指针，不只是对象和数组\n  Go用nil指向无效指针\n  数组在Go中是值，当数组作为函数的参数时，函数接受到的是一个数组的拷贝，并不是一个指针。一般，使用slices作为函数参数，slices是引用\n  maps, slices, channels 传递的是引用\n    内置类型\n  string -\u0026gt; string\n  maps -\u0026gt; HashTable\n    错误处理\n Go不使用异常，而使用erros来表示异常情况    缺少的功能\n  不支持隐式类型转换\n  不支持函数重载，同一作用域中的函数和方法必须具有唯一的名称\n  没有泛型\n    变量声明\n   Go Java     var v int int v = 1;   var v *int Integer v = null;   var v string String v = \u0026ldquo;\u0026quot;;   var v4 [10]int int[] v4 = new int[10];   var v5 []int int[] v5 = null;   var v6 *struct{ a int } class C { int a; } C v6 = null;   var v7 map[string]int HashMap\u0026lt;String, Integer\u0026gt; v7 = null;   var v8 func(a int) int interface F { int f(int a); } F v8 = null;      多个变量同时声明\n  var ( n int x float64 )   函数类型\n在Go中函数是一等公民，可以作为变量\n  for 循环的区别\n 普通循环  for i := 0; i \u0026lt; len(a); i++ { }  循环，对于数组，slice和字符串来说，i为index，v为值；循环map时，i和v为键值对；循环channel是只有值  for i, v := range a { ... }   switch 的区别\nswitch n { case 0: // 当n为0时不执行任何操作  case 1: f() // 仅当n为1时执行 } switch n { case 0, 1: f() // f is called if n == 0 || n == 1.  switch { // case后面可以是表达式  case n \u0026lt; 0: f1() case n == 0: f2() default: f3() }}   自增和自减\nGo不允许 n = i++ 等类似的操作\n  Defer (延迟)\n 一个deferred的函数会在它所包围的函数执行完之前调用  func main() { defer fmt.Println(\u0026#34;World\u0026#34;) fmt.Println(\u0026#34;Hello\u0026#34;) } Hello World  deferred调用会在函数panics的时候提前执行  func main() { defer fmt.Println(\u0026#34;World\u0026#34;) panic(\u0026#34;Stop\u0026#34;) fmt.Println(\u0026#34;Hello\u0026#34;) } World Stop  多个deffered函数调用时遵循后进先出的规则  func main() { fmt.Println(\u0026#34;Hello\u0026#34;) for i := 1; i \u0026lt;= 3; i++ { defer fmt.Println(i) } fmt.Println(\u0026#34;World\u0026#34;) } Hello World 3 2 1  deffered函数可以修改包裹它的函数的返回值  func foo() (result string) { defer func() { result = \u0026#34;Change World\u0026#34; // change value at the very last moment  }() return \u0026#34;Hello World\u0026#34; }  deffered的一般使用场景是来关闭开启的资源  func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } defer src.Close() // 再执行它  dst, err := os.Create(dstName) if err != nil { return } defer dst.Close() // 函数执行完时先执行它  return io.Copy(dst, src) }   Struct\n 一个结构体指针类似Java中的对象  type MyStruct struct { s string n int64 } var x MyStruct // x is initialized to MyStruct{\u0026#34;\u0026#34;, 0}. var px *MyStruct // px is initialized to nil. px = new(MyStruct) // px points to the new struct MyStruct{\u0026#34;\u0026#34;, 0}.  x.s = \u0026#34;Foo\u0026#34; px.s = \u0026#34;Bar\u0026#34;   ","permalink":"https://lambertxiao.github.io/posts/_posts/2018-09-01-java-golang-defference/","summary":"Golang与Java的区别 1. 结构体 -\u0026gt; 类 // 包名即为包含该文件的目录名字 package collection // 声明一个结构体，类似Java中的类 type Stack struct { data []string } // 声明一个Push函数，并通过一个Stack的指针对象实现该方法 // 类似声明了Stack的成员方法 func (s *Stack) Push(x string) { s.data = append(s.data, x) } func (s *Stack) Pop() string { n := len(s.data) - 1 res := s.data[n] s.data[n] = \u0026#34;\u0026#34; // to avoid memory leak  s.data = s.data[:n] return res } func (s *Stack) Size() int { return len(s.data) }   结构体对应Java里的类, 但结构体里只能有变量，不能有方法","title":"Java和Golang的区别"},{"content":"  基于内容寻址\n  点对点\n  IPFS就是提供了一套协议？\n  一种内容可寻址的对等超媒体分发协议\nFilecoin  加密货币，支付系统，是一套激励机制 提供数据存储和获取的方式\n数据监管\n存储的性能，不同的底层系统，性能怎么保证？ 按存储量卖钱？按性能卖钱\nFilecoin和法币的汇率\nlotus是filecoin的实现\nfilecoin怎么保证数据不丢？\n鼓励矿工执行winning post，大家来保证你的数据是不是存储正确的\nWindow PoSt 是矿工在对应的周期内对已经提交的扇区进行证明，证明扇区保存的数据依然存在。 Winning PoSt 是矿工在出块时对已经提交的扇区进行证明，证明扇区保存的数据依然存在。\n存储的是密封的数据\n密封，增加算力成本\nwindow post\n 证明存储的可用性和可靠性 sector：扇区，32GiB 或 64GiB Partition：一个parition 2349或2300个sector Deadline：每半小时一个，一天48个Deadline 一个Deadline证明1或多个Partition  多轮证明\n存储商要押钱\n算力降低，出块概率低\n惩罚高，所以留下来的供应商都比较优质\n检索服务商 目前没有读的能力\n","permalink":"https://lambertxiao.github.io/posts/filecoin/doc/","summary":"基于内容寻址\n  点对点\n  IPFS就是提供了一套协议？\n  一种内容可寻址的对等超媒体分发协议\nFilecoin  加密货币，支付系统，是一套激励机制 提供数据存储和获取的方式\n数据监管\n存储的性能，不同的底层系统，性能怎么保证？ 按存储量卖钱？按性能卖钱\nFilecoin和法币的汇率\nlotus是filecoin的实现\nfilecoin怎么保证数据不丢？\n鼓励矿工执行winning post，大家来保证你的数据是不是存储正确的\nWindow PoSt 是矿工在对应的周期内对已经提交的扇区进行证明，证明扇区保存的数据依然存在。 Winning PoSt 是矿工在出块时对已经提交的扇区进行证明，证明扇区保存的数据依然存在。\n存储的是密封的数据\n密封，增加算力成本\nwindow post\n 证明存储的可用性和可靠性 sector：扇区，32GiB 或 64GiB Partition：一个parition 2349或2300个sector Deadline：每半小时一个，一天48个Deadline 一个Deadline证明1或多个Partition  多轮证明\n存储商要押钱\n算力降低，出块概率低\n惩罚高，所以留下来的供应商都比较优质\n检索服务商 目前没有读的能力","title":""},{"content":"poolChain是poolDequeue的动态大小版本，本质上也是个双向链表，特别之处在于，它每次出队的元素个数是前一次出队的两倍。一旦出队满了m\n// poolChain is a dynamically-sized version of poolDequeue. // // This is implemented as a doubly-linked list queue of poolDequeues // where each dequeue is double the size of the previous one. Once a // dequeue fills up, this allocates a new one and only ever pushes to // the latest dequeue. Pops happen from the other end of the list and // once a dequeue is exhausted, it gets removed from the list. type poolChain struct { // head is the poolDequeue to push to. This is only accessed \t// by the producer, so doesn\u0026#39;t need to be synchronized. \thead *poolChainElt // tail is the poolDequeue to popTail from. This is accessed \t// by consumers, so reads and writes must be atomic. \ttail *poolChainElt } type poolChainElt struct { poolDequeue // next and prev link to the adjacent poolChainElts in this \t// poolChain. \t// \t// next is written atomically by the producer and read \t// atomically by the consumer. It only transitions from nil to \t// non-nil. \t// \t// prev is written atomically by the consumer and read \t// atomically by the producer. It only transitions from \t// non-nil to nil. \tnext, prev *poolChainElt } func storePoolChainElt(pp **poolChainElt, v *poolChainElt) { atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(pp)), unsafe.Pointer(v)) } func loadPoolChainElt(pp **poolChainElt) *poolChainElt { return (*poolChainElt)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(pp)))) } func (c *poolChain) pushHead(val interface{}) { d := c.head if d == nil { // Initialize the chain. \tconst initSize = 8 // Must be a power of 2 \td = new(poolChainElt) d.vals = make([]eface, initSize) c.head = d storePoolChainElt(\u0026amp;c.tail, d) } if d.pushHead(val) { return } // The current dequeue is full. Allocate a new one of twice \t// the size. \tnewSize := len(d.vals) * 2 if newSize \u0026gt;= dequeueLimit { // Can\u0026#39;t make it any bigger. \tnewSize = dequeueLimit } d2 := \u0026amp;poolChainElt{prev: d} d2.vals = make([]eface, newSize) c.head = d2 storePoolChainElt(\u0026amp;d.next, d2) d2.pushHead(val) } func (c *poolChain) popHead() (interface{}, bool) { d := c.head for d != nil { if val, ok := d.popHead(); ok { return val, ok } // There may still be unconsumed elements in the \t// previous dequeue, so try backing up. \td = loadPoolChainElt(\u0026amp;d.prev) } return nil, false } func (c *poolChain) popTail() (interface{}, bool) { d := loadPoolChainElt(\u0026amp;c.tail) if d == nil { return nil, false } for { // It\u0026#39;s important that we load the next pointer \t// *before* popping the tail. In general, d may be \t// transiently empty, but if next is non-nil before \t// the pop and the pop fails, then d is permanently \t// empty, which is the only condition under which it\u0026#39;s \t// safe to drop d from the chain. \td2 := loadPoolChainElt(\u0026amp;d.next) if val, ok := d.popTail(); ok { return val, ok } if d2 == nil { // This is the only dequeue. It\u0026#39;s empty right \t// now, but could be pushed to in the future. \treturn nil, false } // The tail of the chain has been drained, so move on \t// to the next dequeue. Try to drop it from the chain \t// so the next pop doesn\u0026#39;t have to look at the empty \t// dequeue again. \tif atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(\u0026amp;c.tail)), unsafe.Pointer(d), unsafe.Pointer(d2)) { // We won the race. Clear the prev pointer so \t// the garbage collector can collect the empty \t// dequeue and so popHead doesn\u0026#39;t back up \t// further than necessary. \tstorePoolChainElt(\u0026amp;d2.prev, nil) } d = d2 } } ","permalink":"https://lambertxiao.github.io/posts/golang/golang-sync.pool%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0/2-poolchain/","summary":"poolChain是poolDequeue的动态大小版本，本质上也是个双向链表，特别之处在于，它每次出队的元素个数是前一次出队的两倍。一旦出队满了m\n// poolChain is a dynamically-sized version of poolDequeue. // // This is implemented as a doubly-linked list queue of poolDequeues // where each dequeue is double the size of the previous one. Once a // dequeue fills up, this allocates a new one and only ever pushes to // the latest dequeue. Pops happen from the other end of the list and // once a dequeue is exhausted, it gets removed from the list.","title":""},{"content":"块设备初始化  kernal/blk_drv.c\n void blk_dev_init(void) { int i; for (i=0 ; i\u0026lt;NR_REQUEST ; i++) { request[i].dev = -1; request[i].next = NULL; } }  kernel/blk_drv.h\n /* * NR_REQUEST is the number of entries in the request-queue. * NOTE that writes may use only the low 2/3 of these: reads * take precedence. * * 32 seems to be a reasonable number: enough to get some benefit * from the elevator-mechanism, but not so much as to lock a lot of * buffers when they are in the queue. 64 seems to be too many (easily * long pauses in reading when heavy writing/syncing is going on) */ #define NR_REQUEST\t32  /* * Ok, this is an expanded form so that we can use the same * request for paging requests when that is implemented. In * paging, \u0026#39;bh\u0026#39; is NULL, and \u0026#39;waiting\u0026#39; is used to wait for * read/write completion. * 进程要想与块设备进行沟通，必须经过主机内存中的缓冲区， * request就是操作系统管理缓冲区中的缓冲块与块设备上逻辑块之间读写关系的数据结构 */ struct request { int dev;\t/* -1 if no request */ int cmd;\t/* READ or WRITE */ int errors; unsigned long sector; unsigned long nr_sectors; char * buffer; struct task_struct * waiting; struct buffer_head * bh; struct request * next; }; /* * The request-struct contains all necessary data * to load a nr of sectors into memory */ struct request request[NR_REQUEST];  include/linux/fs.h\n struct buffer_head { char * b_data;\t/* pointer to data block (1024 bytes) */ unsigned long b_blocknr;\t/* block number */ unsigned short b_dev;\t/* device (0 = free) */ unsigned char b_uptodate; unsigned char b_dirt;\t/* 0-clean,1-dirty */ unsigned char b_count;\t/* users using this block */ unsigned char b_lock;\t/* 0 - ok, 1 -locked */ struct task_struct * b_wait; struct buffer_head * b_prev; struct buffer_head * b_next; struct buffer_head * b_prev_free; struct buffer_head * b_next_free; };  include/linux/sched.h\n struct task_struct { /* these are hardcoded - don\u0026#39;t touch */ long state;\t/* -1 unrunnable, 0 runnable, \u0026gt;0 stopped */ long counter; long priority; long signal; struct sigaction sigaction[32]; long blocked;\t/* bitmap of masked signals */ /* various fields */ int exit_code; unsigned long start_code,end_code,end_data,brk,start_stack; long pid,father,pgrp,session,leader; unsigned short uid,euid,suid; unsigned short gid,egid,sgid; long alarm; long utime,stime,cutime,cstime,start_time; unsigned short used_math; /* file system info */ int tty;\t/* -1 if no tty, so it must be signed */ unsigned short umask; struct m_inode * pwd; struct m_inode * root; struct m_inode * executable; unsigned long close_on_exec; struct file * filp[NR_OPEN]; /* ldt for this task 0 - zero 1 - cs 2 - ds\u0026amp;ss */ struct desc_struct ldt[3]; /* tss for this task */ struct tss_struct tss; }; ","permalink":"https://lambertxiao.github.io/posts/linux0.11-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/blk_dev_init/","summary":"块设备初始化  kernal/blk_drv.c\n void blk_dev_init(void) { int i; for (i=0 ; i\u0026lt;NR_REQUEST ; i++) { request[i].dev = -1; request[i].next = NULL; } }  kernel/blk_drv.h\n /* * NR_REQUEST is the number of entries in the request-queue. * NOTE that writes may use only the low 2/3 of these: reads * take precedence. * * 32 seems to be a reasonable number: enough to get some benefit * from the elevator-mechanism, but not so much as to lock a lot of * buffers when they are in the queue.","title":""},{"content":"main函数  init/main.c\n // 下面三行分别将指定的线性地址强行转换为给定数据类型的指针，并获取指针所指 // 的内容。由于内核代码段被映射到从物理地址零开始的地方，因此这些线性地址 // 正好也是对应的物理地址。这些指定地址处内存值的含义请参见setup程序读取并保存的参数。 #define EXT_MEM_K (*(unsigned short *)0x90002) // 直接将内存地址转化为结构体指针 #define DRIVE_INFO (*(struct drive_info *)0x90080) #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)  static long memory_end = 0; // 机器具有的物理内存容量（字节数） static long buffer_memory_end = 0; // 高速缓冲区末端地址 static long main_memory_start = 0; // 主内存（将用于分页）开始的位置  int ROOT_DEV = 0; // 根文件系统设备号。 struct drive_info { char dummy[32] } drive_info; // 用于存放硬盘参数表信息  // 内核初始化主程序。初始化结束后将以任务0（idle任务即空闲任务）的身份运行。 void main(void) /* This really IS void, no error here. */ { // 根设备号 -\u0026gt;ROOT_DEV；高速缓存末端地址-\u0026gt;buffer_memory_end;  // 机器内存数-\u0026gt;memory_end；主内存开始地址-\u0026gt;main_memory_start；  // 其中ROOT_DEV已在前面包含进的fs.h文件中声明为extern int  ROOT_DEV = ORIG_ROOT_DEV; drive_info = DRIVE_INFO; // 复制0x90080处的硬盘参数  memory_end = (1\u0026lt;\u0026lt;20) + (EXT_MEM_K\u0026lt;\u0026lt;10); // 内存大小=1Mb + 扩展内存(k)*1024 byte  memory_end \u0026amp;= 0xfffff000; // 忽略不到4kb(1页)的内存数  if (memory_end \u0026gt; 16*1024*1024) // 内存超过16Mb，则按16Mb计  memory_end = 16*1024*1024; // 以下根据内存的大小决定缓冲区的大小  if (memory_end \u0026gt; 12*1024*1024) // 如果内存\u0026gt;12Mb,则设置缓冲区末端=4Mb  buffer_memory_end = 4*1024*1024; else if (memory_end \u0026gt; 6*1024*1024) // 否则若内存\u0026gt;6Mb,则设置缓冲区末端=2Mb  buffer_memory_end = 2*1024*1024; else buffer_memory_end = 1*1024*1024; // 否则设置缓冲区末端=1Mb  main_memory_start = buffer_memory_end; // 如果在Makefile文件中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘。此时主内存将减少。 #ifdef RAMDISK  main_memory_start += rd_init(main_memory_start, RAMDISK*1024); #endif  // 以下是内核进行所有方面的初始化工作  mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c  trap_init(); // 陷阱门(硬件中断向量)初始化，kernel/traps.c  blk_dev_init(); // 块设备初始化,kernel/blk_drv/ll_rw_blk.c  chr_dev_init(); // 字符设备初始化, kernel/chr_drv/tty_io.c  tty_init(); // tty初始化， kernel/chr_drv/tty_io.c  time_init(); // 设置开机启动时间 startup_time  sched_init(); // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c)  // 缓冲管理初始化，建内存链表等。(fs/buffer.c)  buffer_init(buffer_memory_end); hd_init(); // 硬盘初始化，kernel/blk_drv/hd.c  floppy_init(); // 软驱初始化，kernel/blk_drv/floppy.c  sti(); // 所有初始化工作都做完了，开启中断  // 下面过程通过在堆栈中设置的参数，利用中断返回指令启动任务0执行。  move_to_user_mode(); // 移到用户模式下执行  if (!fork()) { /* we count on this going ok */ init(); // 在新建的子进程(任务1)中执行。  } // pause系统调用会把任务0转换成可中断等待状态，再执行调度函数。但是调度函数只要发现系统中  // 没有其他任务可以运行是就会切换到任务0，而不依赖于任务0的状态。  for(;;) pause(); } ","permalink":"https://lambertxiao.github.io/posts/linux0.11-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/main/","summary":"main函数  init/main.c\n // 下面三行分别将指定的线性地址强行转换为给定数据类型的指针，并获取指针所指 // 的内容。由于内核代码段被映射到从物理地址零开始的地方，因此这些线性地址 // 正好也是对应的物理地址。这些指定地址处内存值的含义请参见setup程序读取并保存的参数。 #define EXT_MEM_K (*(unsigned short *)0x90002) // 直接将内存地址转化为结构体指针 #define DRIVE_INFO (*(struct drive_info *)0x90080) #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)  static long memory_end = 0; // 机器具有的物理内存容量（字节数） static long buffer_memory_end = 0; // 高速缓冲区末端地址 static long main_memory_start = 0; // 主内存（将用于分页）开始的位置  int ROOT_DEV = 0; // 根文件系统设备号。 struct drive_info { char dummy[32] } drive_info; // 用于存放硬盘参数表信息  // 内核初始化主程序。初始化结束后将以任务0（idle任务即空闲任务）的身份运行。 void main(void) /* This really IS void, no error here.","title":""},{"content":"设置主存区域的mem_init函数  mm/memory.c\n // 物理内存管理初始化 // 该函数对1MB以上的内存区域以页面为单位进行管理前的初始化设置工作。一个页面长度 // 为4KB bytes.该函数把1MB以上所有物理内存划分成一个个页面，并使用一个页面映射字节 // 数组mem_map[]来管理所有这些页面。对于具有16MB内存容量的机器，该数组共有3840 // 项((16MB-1MB)/4KB)，即可管理3840个物理页面。每当一个物理内存页面被占用时就把 // mem_map[]中对应的字节值增1；若释放一个物理页面，就把对应字节值减1。若字节值为0， // 则表示对应页面空闲；若字节值大于或等于1，则表示对应页面被占用或被不同程序共享占用。 // 在该版本的Linux内核中，最多能管理16MB的物理内存，大于16MB的内存将弃之不用。 // 对于具有16MB内存的PC机系统，在没有设置虚拟盘RAMDISK的情况下start_mem通常是4MB， // end_mem是16MB。因此此时主内存区范围是4MB-16MB,共有3072个物理页面可供分配。而 // 范围0-1MB内存空间用于内核系统（其实内核只使用0-640Kb，剩下的部分被部分高速缓冲和 // 设备内存占用）。 // 参数start_mem是可用做页面分配的主内存区起始地址（已去除RANDISK所占内存空间）。 // end_mem是实际物理内存最大地址。而地址范围start_mem到end_mem是主内存区。  static long HIGH_MEMORY = 0; // 全局变量，存放实际物理内存最高端地址  // linux0.11内核默认支持的最大内存容量是16MB，可以修改这些定义适合更多的内存。 // 内存低端(1MB) #define LOW_MEM 0x100000 // 分页内存15 MB，主内存区最多15M. #define PAGING_MEMORY (15*1024*1024) // 分页后的物理内存页面数（3840） #define PAGING_PAGES (PAGING_MEMORY\u0026gt;\u0026gt;12) // 指定地址映射为页号 #define MAP_NR(addr) (((addr)-LOW_MEM)\u0026gt;\u0026gt;12) // 页面被占用标志. #define USED 100  static unsigned char mem_map [ PAGING_PAGES ] = {0,}; void mem_init(long start_mem, long end_mem) { int i; // 首先将1MB到16MB范围内所有内存页面对应的内存映射字节数组项置为已占用状态，  // 即各项字节全部设置成USED(100)。PAGING_PAGES被定义为(PAGING_MEMORY\u0026gt;\u0026gt;12)，  // 即1MB以上所有物理内存分页后的内存页面数(15MB/4KB = 3840). \tHIGH_MEMORY = end_mem; // 设置内存最高端(16MB) \tfor (i=0 ; i\u0026lt;PAGING_PAGES ; i++) mem_map[i] = USED; // 然后计算主内存区起始内存start_mem处页面对应内存映射字节数组中项号i和主内存区页面数。  // 此时mem_map[]数组的第i项正对应主内存区中第1个页面。最后将主内存区中页面对应的数组项  // 清零(表示空闲)。对于具有16MB物理内存的系统，mem_map[]中对应4MB-16MB主内存区的项被清零。 \ti = MAP_NR(start_mem); // 主内存区其实位置处页面号 \tend_mem -= start_mem; end_mem \u0026gt;\u0026gt;= 12; // 主内存区中的总页面数 \twhile (end_mem--\u0026gt;0) mem_map[i++]=0; // 主内存区页面对应字节值清零 } 总结：将所有的内存页都设置为占用，并经过计算，将主存区域的内存页设置为可用\n","permalink":"https://lambertxiao.github.io/posts/linux0.11-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/mem_init/","summary":"设置主存区域的mem_init函数  mm/memory.c\n // 物理内存管理初始化 // 该函数对1MB以上的内存区域以页面为单位进行管理前的初始化设置工作。一个页面长度 // 为4KB bytes.该函数把1MB以上所有物理内存划分成一个个页面，并使用一个页面映射字节 // 数组mem_map[]来管理所有这些页面。对于具有16MB内存容量的机器，该数组共有3840 // 项((16MB-1MB)/4KB)，即可管理3840个物理页面。每当一个物理内存页面被占用时就把 // mem_map[]中对应的字节值增1；若释放一个物理页面，就把对应字节值减1。若字节值为0， // 则表示对应页面空闲；若字节值大于或等于1，则表示对应页面被占用或被不同程序共享占用。 // 在该版本的Linux内核中，最多能管理16MB的物理内存，大于16MB的内存将弃之不用。 // 对于具有16MB内存的PC机系统，在没有设置虚拟盘RAMDISK的情况下start_mem通常是4MB， // end_mem是16MB。因此此时主内存区范围是4MB-16MB,共有3072个物理页面可供分配。而 // 范围0-1MB内存空间用于内核系统（其实内核只使用0-640Kb，剩下的部分被部分高速缓冲和 // 设备内存占用）。 // 参数start_mem是可用做页面分配的主内存区起始地址（已去除RANDISK所占内存空间）。 // end_mem是实际物理内存最大地址。而地址范围start_mem到end_mem是主内存区。  static long HIGH_MEMORY = 0; // 全局变量，存放实际物理内存最高端地址  // linux0.11内核默认支持的最大内存容量是16MB，可以修改这些定义适合更多的内存。 // 内存低端(1MB) #define LOW_MEM 0x100000 // 分页内存15 MB，主内存区最多15M. #define PAGING_MEMORY (15*1024*1024) // 分页后的物理内存页面数（3840） #define PAGING_PAGES (PAGING_MEMORY\u0026gt;\u0026gt;12) // 指定地址映射为页号 #define MAP_NR(addr) (((addr)-LOW_MEM)\u0026gt;\u0026gt;12) // 页面被占用标志. #define USED 100  static unsigned char mem_map [ PAGING_PAGES ] = {0,}; void mem_init(long start_mem, long end_mem) { int i; // 首先将1MB到16MB范围内所有内存页面对应的内存映射字节数组项置为已占用状态，  // 即各项字节全部设置成USED(100)。PAGING_PAGES被定义为(PAGING_MEMORY\u0026gt;\u0026gt;12)，  // 即1MB以上所有物理内存分页后的内存页面数(15MB/4KB = 3840).","title":""},{"content":"中断程序初始化的trap_init  kernal/traps.c\n // 异常(陷阱)中断程序初始化子程序。设置他们的中断调用门(中断向量)。 // set_trap_gate()与set_system_gate()都使用了中断描述符表IDT中的陷阱门(Trap Gate), // 他们之间的主要区别在于前者设置的特权级为0，后者是3.因此断点陷阱中断int3、溢出中断 // overflow和边界出错中断bounds可以由任何程序产生。 // 这两个函数均是嵌入式汇编宏程序(include/asm/system.h中)  // 以下定义了一些中断处理程序原型，用于在函数trap_init()中设置相应中断门描述符。 // 这些代码在kernal/asm.s或system_call.s中。 void divide_error(void); void debug(void); void nmi(void); void int3(void); void overflow(void); void bounds(void); void invalid_op(void); void device_not_available(void); void double_fault(void); void coprocessor_segment_overrun(void); void invalid_TSS(void); void segment_not_present(void); void stack_segment(void); void general_protection(void); void page_fault(void); void coprocessor_error(void); void reserved(void); void parallel_interrupt(void); void irq13(void); void trap_init(void) { int i; // 设置除操作出错的中断向量值。 \tset_trap_gate(0,\u0026amp;divide_error); set_trap_gate(1,\u0026amp;debug); set_trap_gate(2,\u0026amp;nmi); set_system_gate(3,\u0026amp;int3);\t/* int3-5 can be called from all */ set_system_gate(4,\u0026amp;overflow); set_system_gate(5,\u0026amp;bounds); set_trap_gate(6,\u0026amp;invalid_op); set_trap_gate(7,\u0026amp;device_not_available); set_trap_gate(8,\u0026amp;double_fault); set_trap_gate(9,\u0026amp;coprocessor_segment_overrun); set_trap_gate(10,\u0026amp;invalid_TSS); set_trap_gate(11,\u0026amp;segment_not_present); set_trap_gate(12,\u0026amp;stack_segment); set_trap_gate(13,\u0026amp;general_protection); set_trap_gate(14,\u0026amp;page_fault); set_trap_gate(15,\u0026amp;reserved); set_trap_gate(16,\u0026amp;coprocessor_error); // 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。 \tfor (i=17;i\u0026lt;48;i++) set_trap_gate(i,\u0026amp;reserved); // 设置协处理器中断0x2d(45)陷阱门描述符，并允许其产生中断请求。设置并行口中断描述符。 \tset_trap_gate(45,\u0026amp;irq13); outb_p(inb_p(0x21)\u0026amp;0xfb,0x21); // 允许8259A主芯片的IRQ2中断请求。 \toutb(inb_p(0xA1)\u0026amp;0xdf,0xA1); // 允许8259A从芯片的IRQ3中断请求。 \tset_trap_gate(39,\u0026amp;parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。 }  include/linux/head.h\n typedef struct desc_struct { unsigned long a,b; } desc_table[256]; extern desc_table idt,gdt;  include/asm/system.h\n #define _set_gate(gate_addr,type,dpl,addr) \\ __asm__ (\u0026#34;movw %%dx,%%ax\\n\\t\u0026#34; \\ \u0026#34;movw %0,%%dx\\n\\t\u0026#34; \\ \u0026#34;movl %%eax,%1\\n\\t\u0026#34; \\ \u0026#34;movl %%edx,%2\u0026#34; \\ : \\ : \u0026#34;i\u0026#34; ((short) (0x8000+(dpl\u0026lt;\u0026lt;13)+(type\u0026lt;\u0026lt;8))), \\ \u0026#34;o\u0026#34; (*((char *) (gate_addr))), \\ \u0026#34;o\u0026#34; (*(4+(char *) (gate_addr))), \\ \u0026#34;d\u0026#34; ((char *) (addr)),\u0026#34;a\u0026#34; (0x00080000))  #define set_trap_gate(n,addr) \\ _set_gate(\u0026amp;idt[n],15,0,addr)  #define set_system_gate(n,addr) \\ _set_gate(\u0026amp;idt[n],15,3,addr)  include/asm/io.h\n #define outb(value,port) \\ __asm__ (\u0026#34;outb %%al,%%dx\u0026#34;::\u0026#34;a\u0026#34; (value),\u0026#34;d\u0026#34; (port))  #define inb(port) ({ \\ unsigned char _v; \\ __asm__ volatile (\u0026#34;inb %%dx,%%al\u0026#34;:\u0026#34;=a\u0026#34; (_v):\u0026#34;d\u0026#34; (port)); \\ _v; \\ })  #define outb_p(value,port) \\ __asm__ (\u0026#34;outb %%al,%%dx\\n\u0026#34; \\ \u0026#34;\\tjmp 1f\\n\u0026#34; \\ \u0026#34;1:\\tjmp 1f\\n\u0026#34; \\ \u0026#34;1:\u0026#34;::\u0026#34;a\u0026#34; (value),\u0026#34;d\u0026#34; (port))  #define inb_p(port) ({ \\ unsigned char _v; \\ __asm__ volatile (\u0026#34;inb %%dx,%%al\\n\u0026#34; \\ \u0026#34;\\tjmp 1f\\n\u0026#34; \\ \u0026#34;1:\\tjmp 1f\\n\u0026#34; \\ \u0026#34;1:\u0026#34;:\u0026#34;=a\u0026#34; (_v):\u0026#34;d\u0026#34; (port)); \\ _v; \\ })  ","permalink":"https://lambertxiao.github.io/posts/linux0.11-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/trap_init/","summary":"中断程序初始化的trap_init  kernal/traps.c\n // 异常(陷阱)中断程序初始化子程序。设置他们的中断调用门(中断向量)。 // set_trap_gate()与set_system_gate()都使用了中断描述符表IDT中的陷阱门(Trap Gate), // 他们之间的主要区别在于前者设置的特权级为0，后者是3.因此断点陷阱中断int3、溢出中断 // overflow和边界出错中断bounds可以由任何程序产生。 // 这两个函数均是嵌入式汇编宏程序(include/asm/system.h中)  // 以下定义了一些中断处理程序原型，用于在函数trap_init()中设置相应中断门描述符。 // 这些代码在kernal/asm.s或system_call.s中。 void divide_error(void); void debug(void); void nmi(void); void int3(void); void overflow(void); void bounds(void); void invalid_op(void); void device_not_available(void); void double_fault(void); void coprocessor_segment_overrun(void); void invalid_TSS(void); void segment_not_present(void); void stack_segment(void); void general_protection(void); void page_fault(void); void coprocessor_error(void); void reserved(void); void parallel_interrupt(void); void irq13(void); void trap_init(void) { int i; // 设置除操作出错的中断向量值。 \tset_trap_gate(0,\u0026amp;divide_error); set_trap_gate(1,\u0026amp;debug); set_trap_gate(2,\u0026amp;nmi); set_system_gate(3,\u0026amp;int3);\t/* int3-5 can be called from all */ set_system_gate(4,\u0026amp;overflow); set_system_gate(5,\u0026amp;bounds); set_trap_gate(6,\u0026amp;invalid_op); set_trap_gate(7,\u0026amp;device_not_available); set_trap_gate(8,\u0026amp;double_fault); set_trap_gate(9,\u0026amp;coprocessor_segment_overrun); set_trap_gate(10,\u0026amp;invalid_TSS); set_trap_gate(11,\u0026amp;segment_not_present); set_trap_gate(12,\u0026amp;stack_segment); set_trap_gate(13,\u0026amp;general_protection); set_trap_gate(14,\u0026amp;page_fault); set_trap_gate(15,\u0026amp;reserved); set_trap_gate(16,\u0026amp;coprocessor_error); // 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。 \tfor (i=17;i\u0026lt;48;i++) set_trap_gate(i,\u0026amp;reserved); // 设置协处理器中断0x2d(45)陷阱门描述符，并允许其产生中断请求。设置并行口中断描述符。 \tset_trap_gate(45,\u0026amp;irq13); outb_p(inb_p(0x21)\u0026amp;0xfb,0x21); // 允许8259A主芯片的IRQ2中断请求。 \toutb(inb_p(0xA1)\u0026amp;0xdf,0xA1); // 允许8259A从芯片的IRQ3中断请求。 \tset_trap_gate(39,\u0026amp;parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。 }  include/linux/head.","title":""},{"content":"Ceph\n  Monitor 持有整个集群的状态，包含monitor映射，manager映射，osd映射，mds映射，crush映射，monitor必须做冗余和高可用\n  Manager 负责追踪运行时指标以及集群的状态，对外提供了web端的dashboard和restapi\n  OSD 负责存储数据，处理数据多副本，数据修复，rebalance，并且提供一些监控信息给monitor，同样需要冗余和高可用\n  MDS 负责存储集群的数据，只有ceph文件存储才需要用到，ceph的块存储和对象存储用不到，MDS允许通过posix文件接口来访问\n  ceph使用一个叫逻辑存储池的方法来存储数据，数据以对象的形式存在。ceph会计算对象属于哪个group，group又属于哪个OSD，通过一个叫CRUSH的算法来实现。CRUSH算法使得ceph集群可伸缩，rebalance，可修复\n","permalink":"https://lambertxiao.github.io/posts/%E5%AD%98%E5%82%A8-ceph/doc/","summary":"Ceph\n  Monitor 持有整个集群的状态，包含monitor映射，manager映射，osd映射，mds映射，crush映射，monitor必须做冗余和高可用\n  Manager 负责追踪运行时指标以及集群的状态，对外提供了web端的dashboard和restapi\n  OSD 负责存储数据，处理数据多副本，数据修复，rebalance，并且提供一些监控信息给monitor，同样需要冗余和高可用\n  MDS 负责存储集群的数据，只有ceph文件存储才需要用到，ceph的块存储和对象存储用不到，MDS允许通过posix文件接口来访问\n  ceph使用一个叫逻辑存储池的方法来存储数据，数据以对象的形式存在。ceph会计算对象属于哪个group，group又属于哪个OSD，通过一个叫CRUSH的算法来实现。CRUSH算法使得ceph集群可伸缩，rebalance，可修复","title":""},{"content":"三总线 地址总线： 是专门用来传送地址的，是单向的，地址从CPU传向外部存储器或IO端口。地址总线的位数决定了CPU可寻址的内存空间大小。比如16位就是2的16次方等于64KB\n数据总线： 用来传输数据信息，是双向的，即可以把CPU的数据传送到存储器或IO接口等其他部件，也可以将其他部件的数据传到CPU\n控制总线： 用来传送控制信号和时序信号，双向的\n 控制信号如微处理器送往存储器和IO接口电路的，如读写信号、片选信号、中断响应信号  ","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E4%B8%89%E6%80%BB%E7%BA%BF/","summary":"三总线 地址总线： 是专门用来传送地址的，是单向的，地址从CPU传向外部存储器或IO端口。地址总线的位数决定了CPU可寻址的内存空间大小。比如16位就是2的16次方等于64KB\n数据总线： 用来传输数据信息，是双向的，即可以把CPU的数据传送到存储器或IO接口等其他部件，也可以将其他部件的数据传到CPU\n控制总线： 用来传送控制信号和时序信号，双向的\n 控制信号如微处理器送往存储器和IO接口电路的，如读写信号、片选信号、中断响应信号  ","title":""},{"content":"内存 虚拟内存 内存空间划分   用户空间\n  内核空间\n  内存寻址 内存区域中的每一个单元都是有地址的，这些地址是由指针来标识和定位的，通过指针来寻找内存单元的操作也被称为内存寻址。\n内存管理   分页式\n  分段式\n  物理内存 内存卡\n","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E5%AD%98/","summary":"内存 虚拟内存 内存空间划分   用户空间\n  内核空间\n  内存寻址 内存区域中的每一个单元都是有地址的，这些地址是由指针来标识和定位的，通过指针来寻找内存单元的操作也被称为内存寻址。\n内存管理   分页式\n  分段式\n  物理内存 内存卡","title":""},{"content":"寄存器 ","permalink":"https://lambertxiao.github.io/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%AF%84%E5%AD%98%E5%99%A8/","summary":"寄存器 ","title":""},{"content":"684. 冗余连接 684. 冗余连接 树可以看成是一个连通且 无环 的 无向 图。\n给定往一棵 n 个节点 (节点值 1～n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间，且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ，edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。\n请找出一条可以删去的边，删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案，则返回数组 edges 中最后出现的边。\n思路：\n假设对于所有的边 [[1,2], [2,3], [3,4], [1,4], [1,5]]\n 初始时，定义所有节点的代表节点集合parent  parent[i] = i\n表示i节点的代表节点是自身i\n对于上面的边，五条边，则应该有6个节点，len(parent) = 6\nparent = [0, 1, 2, 3, 4, 5]\n 循环所有的边，判断能否加入这条边到树中\n 对于边x-y，如果x的代表节点不等于y的代表节点，说明没有一条路径能让x直接到y，则此时x-y这条边能加入树中，    func findRedundantConnection(edges [][]int) []int { // 1.集合树：所有节点以代表节点为父节点构成的多叉树  // 2.节点的代表节点：可以理解为节点的父节点，从当前节点出发，可以向上找到的第一个节点  // 3.集合的代表节点：可以理解为根节点，意味着该集合内所有节点向上走，最终都能到达的节点  parent := make([]int, len(edges) + 1) for i := range parent { // 索引i表示i节点，值i表示i节点的代表节点  parent[i] = i } // 给定一个节点，找到这个节点的代表节点  var find func(x int) int find = func(x int) int { // 初始时x节点的代表节点是自己  if parent[x] != x { // 递归往上找到parent  parent[x] = find(parent[x]) } return parent[x] } // 给定一条边 x -\u0026gt; y，判断添加了这条边之后会不会集合树会不会成环  union := func(x, y int) bool { nx, ny := find(x), find(y) // 两个节点的代表节点相同，那么原先一定可以从x走到y了，此时再加入x-y这条边，则一定会成环  if nx == ny { return false } // 更新x的代表节点的代表节点  // 比如对于边 x-y, z-k, 可以将x-y加入到z-k中，则添加y-k的关系  parent[nx] = ny return true } for _, edge := range edges { if !union(edge[0], edge[1]) { return edge } } return nil } ","permalink":"https://lambertxiao.github.io/posts/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95-%E5%B9%B6%E6%9F%A5%E9%9B%86/doc/","summary":"684. 冗余连接 684. 冗余连接 树可以看成是一个连通且 无环 的 无向 图。\n给定往一棵 n 个节点 (节点值 1～n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间，且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ，edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。\n请找出一条可以删去的边，删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案，则返回数组 edges 中最后出现的边。\n思路：\n假设对于所有的边 [[1,2], [2,3], [3,4], [1,4], [1,5]]\n 初始时，定义所有节点的代表节点集合parent  parent[i] = i\n表示i节点的代表节点是自身i\n对于上面的边，五条边，则应该有6个节点，len(parent) = 6\nparent = [0, 1, 2, 3, 4, 5]\n 循环所有的边，判断能否加入这条边到树中\n 对于边x-y，如果x的代表节点不等于y的代表节点，说明没有一条路径能让x直接到y，则此时x-y这条边能加入树中，    func findRedundantConnection(edges [][]int) []int { // 1.","title":""},{"content":" 最近在看libp2p的源码，发现需要先储备一些网络方面的知识才能看得懂，如UPnP\n 什么是UPnP UPnP全名是Universal Plug and Play，即通用即插即用。简单的来说，UPnP 最大的愿景就是希望任何设备只要一接上网络，所有在网络上的设备马上就能知道有新设备加入，这些设备彼此之间能互相沟通，更能直接使用或控制它，一切都不需要设定，完全的Plug and Play。\n组成  设备 控制点  步骤  寻址(Addressing)  开始会给所有设备或者控制点分配一个分配一个IP。 这个过程是这样的，设备或控制点向 DHCP 客户端发送一个 DHCPDISCOVER 消息，DHCP 客户端负责分配向他们分配 IP，如果局域网内没有 DHCP 服务，UPnP 设备将按照 Auto-IP 的协议通过算法呢从 169.254.1.0 to 169.254.254.255 地址范围内获取一个未被使用的 IP 地址。 对于新设备首次与网络建立连接时也会有这个寻址过程。\n发现(Discovery)  这步是 UPnP 真正工作的第一步。 当一个设备被加入到网络中时，UPnP 发现协议允许它向控制点介绍自己的功能，设备会向多次向固定的地址及端口(239.255.255.250:1900)发送消息，控制点会监控给地址及端口。当一个控制点被加入到网络时，UPnP 发现协议允许它搜寻这个网络内它感兴趣的设备。这个过程内彼此交换剪短的信息，如类型、全局唯一标识符、指向详细信息的链接及当前状态(可选)。\n描述(Description)  控制点通过1.发现(Discovery)过程中设备提供的指向设备详细信息的链接，获取设备的详细信息(Device description)及其提供的服务的详细信息(Service description)。\n控制(Control)  控制点通过描述过程对设备的了解，控制点可以发送控制信息控制设备，设备在执行完命令后会给与控制点一个反馈。\n事件(Eventing)  控制点可以监听设备的状态，这样设备的状态或信息发生了变化，只要产生一个事件广播出去，控制点即可进行响应，类似一般的订阅者模式。\n展现(Presentation)  控制点可以从设备获取一个 HTML 页面，用于控制设备或展现设备信息，是对上面3.控制(Control)和4.事件(Eventing)过程的一个补充(即时展现)。\n","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C-upnp/doc/","summary":"最近在看libp2p的源码，发现需要先储备一些网络方面的知识才能看得懂，如UPnP\n 什么是UPnP UPnP全名是Universal Plug and Play，即通用即插即用。简单的来说，UPnP 最大的愿景就是希望任何设备只要一接上网络，所有在网络上的设备马上就能知道有新设备加入，这些设备彼此之间能互相沟通，更能直接使用或控制它，一切都不需要设定，完全的Plug and Play。\n组成  设备 控制点  步骤  寻址(Addressing)  开始会给所有设备或者控制点分配一个分配一个IP。 这个过程是这样的，设备或控制点向 DHCP 客户端发送一个 DHCPDISCOVER 消息，DHCP 客户端负责分配向他们分配 IP，如果局域网内没有 DHCP 服务，UPnP 设备将按照 Auto-IP 的协议通过算法呢从 169.254.1.0 to 169.254.254.255 地址范围内获取一个未被使用的 IP 地址。 对于新设备首次与网络建立连接时也会有这个寻址过程。\n发现(Discovery)  这步是 UPnP 真正工作的第一步。 当一个设备被加入到网络中时，UPnP 发现协议允许它向控制点介绍自己的功能，设备会向多次向固定的地址及端口(239.255.255.250:1900)发送消息，控制点会监控给地址及端口。当一个控制点被加入到网络时，UPnP 发现协议允许它搜寻这个网络内它感兴趣的设备。这个过程内彼此交换剪短的信息，如类型、全局唯一标识符、指向详细信息的链接及当前状态(可选)。\n描述(Description)  控制点通过1.发现(Discovery)过程中设备提供的指向设备详细信息的链接，获取设备的详细信息(Device description)及其提供的服务的详细信息(Service description)。\n控制(Control)  控制点通过描述过程对设备的了解，控制点可以发送控制信息控制设备，设备在执行完命令后会给与控制点一个反馈。\n事件(Eventing)  控制点可以监听设备的状态，这样设备的状态或信息发生了变化，只要产生一个事件广播出去，控制点即可进行响应，类似一般的订阅者模式。\n展现(Presentation)  控制点可以从设备获取一个 HTML 页面，用于控制设备或展现设备信息，是对上面3.控制(Control)和4.事件(Eventing)过程的一个补充(即时展现)。","title":""},{"content":"Http2 ","permalink":"https://lambertxiao.github.io/posts/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/http2/","summary":"Http2 ","title":""},{"content":"","permalink":"https://lambertxiao.github.io/posts/%E8%8B%B1%E8%AF%AD%E5%8D%95%E8%AF%8D%E7%A7%AF%E7%B4%AF/doc/","summary":"","title":""}]