<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jayden&#39;s Blog</title>
  
  
  <link href="https://jaydenchang.top/atom.xml" rel="self"/>
  
  <link href="https://jaydenchang.top/"/>
  <updated>2025-09-05T13:55:31.527Z</updated>
  <id>https://jaydenchang.top/</id>
  
  <author>
    <name>Jayden Chang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>AWS CloudWatch告警推送至外部AlertManager</title>
    <link href="https://jaydenchang.top/post/0x0039.html"/>
    <id>https://jaydenchang.top/post/0x0039.html</id>
    <published>2025-09-04T16:00:00.000Z</published>
    <updated>2025-09-05T13:55:31.527Z</updated>
    
    <content type="html"><![CDATA[<hr><h2 id="aws-端配置">AWS 端配置</h2><p>想要实现 AWS 的 CloudWatch产生的告警推送至业务程序，或者直接邮件、短信方式通知到运维成员，通常使用AWS 的 SNS服务，但是该套服务组合有一个缺点，当告警异常恢复后，CloudWatch并不会发送一条恢复的通知，不符合业务上的需求。</p><p>在查阅无数文档后，终于发现 AWS 有一个服务可以解决这个问题，使用EventBridge 去分发事件：<ahref="https://docs.aws.amazon.com/zh_cn/AmazonCloudWatch/latest/monitoring/cloudwatch-and-eventbridge.html">告警事件和EventBridge - Amazon CloudWatch</a></p><p>CloudWatch 告警会默认推送至 EventBridge，但由于 EventBridge默认都没有配置规则，所以推送到 EventBridge后没有执行其他动作。因此可以在 EventBridge 创建 2个规则，分别对应告警发生和告警恢复。</p><p>进入 EventBridge 页面，在左边导航栏选择 Rules，创建一个新 rule：</p><img src="/post/0x0039/0x0039_1.png" class=""><p>Events 可以选择 All Events，Event pattern 选择 use schema，schemaregistry 默认只有一个 aws.events, schema 选择aws.cloudwatch@CloudWatchAlarmStateChange。</p><p>在下面的 method 里，找到 state 和 previousState：</p><img src="/post/0x0039/0x0039_2.png" class=""><p>分别设置 state.value 和 previousState.value，然后再点击 Generateevent pattern in JSON，然后进入下一步配置通知目标</p><img src="/post/0x0039/0x0039_3.png" class=""><p>这里一般会提前配置好 SNS 的subscription，选择要推送的渠道，下一步然后创建即可。</p><p>我选择的 SNS subscription 会通过 https 回调到内部的 web应用去处理告警信息，下一步要做的是解析 EventBridge 的消息体。</p><h2 id="解析-eventbridge-payload">解析 EventBridge payload</h2><p>payload 格式大概如下所示：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">&#123;        &quot;AlarmName&quot;: &quot;test-low-available-memory&quot;,        &quot;AlarmDescription&quot;: &quot;aws rds memory utilization &gt;&#x3D; 80%&quot;,        &quot;AWSAccountId&quot;: &quot;xxxxxx&quot;,        &quot;AlarmConfigurationUpdatedTimestamp&quot;: &quot;2025-08-12T07:49:42.878+0000&quot;,        &quot;NewStateValue&quot;: &quot;ALARM&quot;,        &quot;NewStateReason&quot;: &quot;Threshold Crossed: 3 out of the last 3 datapoints [7.066517504E8 (12&#x2F;08&#x2F;25 07:45:00), 7.03741952E8 (12&#x2F;08&#x2F;25 07:40:00), 7.072145408E8 (12&#x2F;08&#x2F;25 07:35:00)] were less than or equal to the threshold (8.0E8) (minimum 3 datapoints for OK -&gt; ALARM transition).&quot;,        &quot;StateChangeTime&quot;: &quot;2025-08-12T07:50:34.567+0000&quot;,        &quot;Region&quot;: &quot;EU (Ireland)&quot;,        &quot;AlarmArn&quot;: &quot;arn:aws:cloudwatch:eu-west-1:xxxx:alarm:test-low-available-memory&quot;,        &quot;OldStateValue&quot;: &quot;INSUFFICIENT_DATA&quot;,        &quot;OKActions&quot;: [],        &quot;AlarmActions&quot;: [&quot;arn:aws:sns:eu-west-1:xxx:prometheus_alert&quot;],        &quot;InsufficientDataActions&quot;: [],        &quot;Trigger&quot;: &#123;                &quot;MetricName&quot;: &quot;FreeableMemory&quot;,                &quot;Namespace&quot;: &quot;AWS&#x2F;RDS&quot;,                &quot;StatisticType&quot;: &quot;Statistic&quot;,                &quot;Statistic&quot;: &quot;AVERAGE&quot;,                &quot;Unit&quot;: null,                &quot;Dimensions&quot;: [&#123;                        &quot;value&quot;: &quot;eu2-2&quot;,                        &quot;name&quot;: &quot;DBInstanceIdentifier&quot;                &#125;],                &quot;Period&quot;: 300,                &quot;EvaluationPeriods&quot;: 3,                &quot;DatapointsToAlarm&quot;: 3,                &quot;ComparisonOperator&quot;: &quot;LessThanOrEqualToThreshold&quot;,                &quot;Threshold&quot;: 8.0E8,                &quot;TreatMissingData&quot;: &quot;missing&quot;,                &quot;EvaluateLowSampleCountPercentile&quot;: &quot;&quot;        &#125;&#125;</code></pre><p>可以选择对自己业务有帮助的字段，再拼接为推送至 AlertManager的消息体。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type AwsAlarmArgs struct &#123;Type             string &#96;json:&quot;Type&quot;&#96;MessageId        string &#96;json:&quot;MessageId&quot;&#96;Token            string &#96;json:&quot;Token,omitempty&quot;&#96;TopicArn         string &#96;json:&quot;TopicArn&quot;&#96;Subject          string &#96;json:&quot;Subject,omitempty&quot;&#96;Message          string &#96;json:&quot;Message&quot;&#96;SubscribeURL     string &#96;json:&quot;SubscribeURL,omitempty&quot;&#96;Timestamp        string &#96;json:&quot;Timestamp&quot;&#96;SignatureVersion string &#96;json:&quot;SignatureVersion&quot;&#96;Signature        string &#96;json:&quot;Signature&quot;&#96;SigningCertURL   string &#96;json:&quot;SigningCertURL&quot;&#96;UnsubscribeURL   string &#96;json:&quot;UnsubscribeURL,omitempty&quot;&#96;&#125;type AwsAlarmMsg struct &#123;AlarmName        string          &#96;json:&quot;AlarmName&quot;&#96;AlarmDescription string          &#96;json:&quot;AlarmDescription&quot;&#96;AWSAccountId     string          &#96;json:&quot;AWSAccountId&quot;&#96;NewStateValue    string          &#96;json:&quot;NewStateValue&quot;&#96;NewStateReason   string          &#96;json:&quot;NewStateReason&quot;&#96;StateChangeTime  string          &#96;json:&quot;StateChangeTime&quot;&#96;Region           string          &#96;json:&quot;Region&quot;&#96;OldStateValue    string          &#96;josn:&quot;OldStateValue&quot;&#96;Trigger          AwsAlarmTrigger &#96;json:&quot;Trigger&quot;&#96;&#125;type AwsAlarmTrigger struct &#123;MetricName                       string               &#96;json:&quot;MetricName&quot;&#96;Namespace                        string               &#96;json:&quot;Namespace&quot;&#96;StatisticType                    string               &#96;json:&quot;StatisticType&quot;&#96;Statistic                        string               &#96;json:&quot;Statistic&quot;&#96;Unit                             string               &#96;json:&quot;Unit&quot;&#96;Dimensions                       []AwsAlarmDimensions &#96;json:&quot;Dimensions&quot;&#96;Period                           int                  &#96;json:&quot;Period&quot;&#96;EvaluationPeriods                int                  &#96;json:&quot;EvaluationPeriods&quot;&#96;ComparisonOperator               string               &#96;json:&quot;ComparisonOperator&quot;&#96;Threshold                        float32              &#96;json:&quot;Threshold&quot;&#96;TreatMissingData                 string               &#96;json:&quot;TreatMissingData&quot;&#96;EvaluateLowSampleCountPercentile string               &#96;json:&quot;EvaluateLowSampleCountPercentile&quot;&#96;&#125;type AwsEvent struct &#123;Version    string         &#96;json:&quot;version&quot;&#96;Id         string         &#96;json:&quot;id&quot;&#96;DetailType string         &#96;json:&quot;detail-type&quot;&#96;Source     string         &#96;json:&quot;source&quot;&#96;Account    string         &#96;json:&quot;account&quot;&#96;Time       string         &#96;json:&quot;time&quot;&#96;Region     string         &#96;json:&quot;region&quot;&#96;Resources  []string       &#96;json:&quot;resources&quot;&#96;Detail     AwsEventDetail &#96;json:&quot;detail&quot;&#96;&#125;type AwsEventDetail struct &#123;AlarmName     string                &#96;json:&quot;alarmName&quot;&#96;State         AwsEventState         &#96;json:&quot;state&quot;&#96;PreviousState AwsEventPreviousState &#96;json:&quot;previousState&quot;&#96;Configuration AwsEventConfiguration &#96;json:&quot;configuration&quot;&#96;&#125;type AwsEventState struct &#123;Value      string &#96;json:&quot;value&quot;&#96;Reason     string &#96;json:&quot;reason&quot;&#96;ReasonData string &#96;json:&quot;reasonData&quot;&#96;Timestamp  string &#96;json:&quot;timestamp&quot;&#96;&#125;type AwsEventPreviousState struct &#123;Value      string &#96;json:&quot;value&quot;&#96;Reason     string &#96;json:&quot;reason&quot;&#96;ReasonData string &#96;json:&quot;reasonData&quot;&#96;Timestamp  string &#96;json:&quot;timestamp&quot;&#96;&#125;type AwsEventConfiguration struct &#123;Metrics     []AwsEventMetrics &#96;json:&quot;metrics&quot;&#96;Description string            &#96;json:&quot;description&quot;&#96;&#125;type AwsEventMetrics struct &#123;Id         string             &#96;json:&quot;id&quot;&#96;MetricStat AwsEventMetricStat &#96;json:&quot;metricStat&quot;&#96;ReturnData bool               &#96;json:&quot;returnData&quot;&#96;&#125;type AwsEventMetricStat struct &#123;Metric struct &#123;Namespace  string            &#96;json:&quot;namespace&quot;&#96;Name       string            &#96;json:&quot;name&quot;&#96;Dimensions map[string]string &#96;json:&quot;dimensions&quot;&#96;&#125; &#96;json:&quot;metric&quot;&#96;Period int    &#96;json:&quot;period&quot;&#96;Stat   string &#96;json:&quot;stat&quot;&#96;&#125;type AwsAlarmDimensions struct &#123;Name  string &#96;json:&quot;name&quot;&#96;Value string &#96;json:&quot;value&quot;&#96;&#125;type AwsAlarm struct &#123;Token  stringArgs   AwsAlarmArgsRegion string&#125;</code></pre><p>处理 aws subscription 和 EventBridge 的 payload</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; 接收aws告警并发送给amfunc (p *Pusher) AWSAlert(c *gin.Context) (err error) &#123;token :&#x3D; c.FormValue(&quot;token&quot;)awsArgs :&#x3D; AwsAlarmArgs&#123;&#125;awsEvent :&#x3D; AwsEvent&#123;&#125;body, err :&#x3D; ioutil.ReadAll(c.Request().Body)if err !&#x3D; nil &#123;log.Logger.Errorf(&quot;read aws request body err:%v&quot;, err)return&#125;msgType :&#x3D; c.Request().Header.Get(&quot;x-amz-sns-message-type&quot;) &#x2F;&#x2F; 获取消息类型switch msgType &#123;case &quot;SubscriptionConfirmation&quot;:&#x2F;&#x2F; aws SNS订阅确认消息if err &#x3D; json.Unmarshal(body, &amp;awsArgs); err !&#x3D; nil &#123;log.Logger.Errorf(&quot;json unmarshal err:%v&quot;, err)return&#125;subscribeUrl :&#x3D; awsArgs.SubscribeURLresp, err :&#x3D; http.Get(subscribeUrl)if err !&#x3D; nil &#123;log.Logger.Errorf(&quot;Get url:%s err:%v&quot;, subscribeUrl, err)return err&#125;if resp.StatusCode &#x3D;&#x3D; 200 &#123;log.Logger.Infof(&quot;subscribe aws topic successful, statusCode:%v, status:%s&quot;, resp.StatusCode, resp.Status)&#125; else &#123;log.Logger.Errorf(&quot;subscribe aws topic failed, statusCode:%v, status:%s&quot;, resp.StatusCode, resp.Status)&#125;case &quot;Notification&quot;:&#x2F;&#x2F; aws SNS通知消息if err &#x3D; json.Unmarshal(body, &amp;awsEvent); err !&#x3D; nil &#123;log.Logger.Errorf(&quot;json unmarshal err: body: %v, err: %v&quot;, string(body), err)return&#125;state :&#x3D; &quot;&quot;if awsEvent.Detail.State.Value &#x3D;&#x3D; &quot;ALARM&quot; &#123;state &#x3D; &quot;firing&quot;&#125; else if awsEvent.Detail.State.Value &#x3D;&#x3D; &quot;OK&quot; &#123;state &#x3D; &quot;resolved&quot;&#125; log.Logger.Infof(&quot;get aws alarm: traceId: %v, alarm description: %v&quot;, awsEvent.Id, awsEvent.Detail.Configuration.Description)dimension :&#x3D; &quot;&quot;namespace :&#x3D; &quot;&quot;&#x2F;&#x2F; 正常情况下Metrics有一个元素，此处避免数组越界造成panicif len(awsEvent.Detail.Configuration.Metrics) &gt; 0 &#123;namespace &#x3D; awsEvent.Detail.Configuration.Metrics[0].MetricStat.Metric.Namespacefor _,v :&#x3D;range awsEvent.Detail.Configuration.Metrics[0].MetricStat.Metric.Dimensions &#123;dimension &#x3D; vbreak&#125;&#125;CreateAlert(awsEvent.Detail.State.Timestamp, awsEvent.Detail.AlarmName, awsEvent.Detail.Configuration.Description, namespace, dimension, state, &quot;error&quot;, option.Opt.AmOpt.AwsDs, &quot;aws&quot;)default:&#x2F;&#x2F; 未知类型log.Logger.Errorf(&quot;unknown msgType:%s&quot;, msgType)return&#125;c.JSON(http.StatusOK, map[string]interface&#123;&#125;&#123;&quot;errcode&quot;: 0,&quot;errmsg&quot;:  &quot;ok&quot;,&#125;)return&#125;</code></pre><p>推送至外部 AlertManager</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type AlertManagerArgs struct &#123;StartsAt    string      &#96;json:&quot;startsAt,omitempty&quot;&#96; &#x2F;&#x2F; 报警开始时间EndsAt      string      &#96;json:&quot;endsAt,omitempty&quot;&#96;   &#x2F;&#x2F; 报警结束时间Annotations Annoattions &#96;json:&quot;annotations&quot;&#96;        &#x2F;&#x2F; 告警信息注解Status      string      &#96;json:&quot;status&quot;&#96;             &#x2F;&#x2F; 告警状态Labels      Labels      &#96;json:&quot;labels&quot;&#96;             &#x2F;&#x2F; 告警信息标签&#125;type Annoattions struct &#123;Summary     string &#96;json:&quot;summary&quot;&#96;Description string &#96;json:&quot;description&quot;&#96;&#125;type Labels struct &#123;Severity   string &#96;json:&quot;severity&quot;&#96;Ds         string &#96;json:&quot;ds&quot;&#96;AlertGroup string &#96;json:&quot;alertgroup&quot;&#96;NameSpace  string &#96;json:&quot;namespace&quot;&#96;Instance   string &#96;json:&quot;instance&quot;&#96;AlertName  string &#96;json:&quot;alertname&quot;&#96;&#125;func CreateAlert(startTimeOrigin string, summary, description, namespace, instance, status, severity, ds, alertGroup string) &#123;amArgs :&#x3D; AlertManagerArgs&#123;Status: status,Annotations: Annoattions&#123;Summary:     summary,Description: description,&#125;,Labels: Labels&#123;Severity:   severity,Ds:         ds,AlertGroup: alertGroup,NameSpace:  namespace,Instance:   instance,AlertName:  summary,&#125;,&#125;parseTime, err :&#x3D; time.Parse(&quot;2006-01-02T15:04:05.000-0700&quot;, startTimeOrigin)if err !&#x3D; nil &#123;log.Logger.Errorf(&quot;parse time failed: time: %v, err: %v&quot;, startTimeOrigin, err)return&#125;if status &#x3D;&#x3D; &quot;firing&quot; &#123;amArgs.StartsAt &#x3D; parseTime.Format(&quot;2006-01-02T15:04:05Z&quot;)&#x2F;&#x2F; 延长endsAt,未加endsAt的告警默认2min后就关闭amArgs.EndsAt &#x3D; parseTime.Add(time.Duration(option.Opt.AmOpt.EndTimeout) * time.Hour).Format(&quot;2006-01-02T15:04:05Z&quot;)&#125; else if status &#x3D;&#x3D; &quot;resolved&quot; &#123;amArgs.EndsAt &#x3D; parseTime.Format(&quot;2006-01-02T15:04:05Z&quot;)&#125; else &#123;log.Logger.Errorf(&quot;status invalid: status: %v&quot;, status)return&#125;reqBody :&#x3D; []AlertManagerArgs&#123;amArgs&#125;reqByte, err :&#x3D; json.Marshal(reqBody)if err !&#x3D; nil &#123;log.Logger.Errorf(&quot;json marshal failed: err: %v&quot;, err)return&#125;req, err :&#x3D; http.NewRequest(http.MethodPost, option.Opt.AmOpt.AmHost, bytes.NewBuffer(reqByte))if err !&#x3D; nil &#123;log.Logger.Errorf(&quot;new request failed: err: %v&quot;, err)return&#125;req.SetBasicAuth(option.Opt.AmOpt.Username, option.Opt.AmOpt.Password)req.Header.Set(&quot;Content-Type&quot;, &quot;application&#x2F;json&quot;)_, err &#x3D; http.DefaultClient.Do(req)if err !&#x3D; nil &#123;log.Logger.Errorf(&quot;http do failed: err: %v&quot;, err)return&#125;&#125;</code></pre><p>代码仅供参考，需要根据实际业务再作修改</p><h2 id="参考文档">参考文档</h2><p><ahref="https://docs.aws.amazon.com/zh_cn/AmazonCloudWatch/latest/monitoring/cloudwatch-and-eventbridge.html">告警事件和EventBridge - Amazon CloudWatch</a></p><p><ahref="https://repost.aws/zh-Hans/knowledge-center/sns-sms-messages-china">repost.aws</a></p><p><ahref="https://zhuanlan.zhihu.com/p/141713511">zhuanlan.zhihu.com</a></p><p><ahref="https://www.alibabacloud.com/help/zh/sls/ingest-cloudwatch-alerts-into-log-service">接入CloudWatch告警</a></p><p><ahref="https://docs.aws.amazon.com/zh_cn/sns/latest/dg/http-notification-json.html">HTTP/HTTPS通知 JSON 格式 - Amazon Simple Notification Service</a></p><p><ahref="https://cloud.tencent.com/developer/article/2297333">cloud.tencent.com</a></p>]]></content>
    
    
    <summary type="html">使用自建Web应用实现将AWS CloudWatch的告警推送至外部的AlertManager</summary>
    
    
    
    <category term="DevOps" scheme="https://jaydenchang.top/categories/DevOps/"/>
    
    
    <category term="技术" scheme="https://jaydenchang.top/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Linux" scheme="https://jaydenchang.top/tags/Linux/"/>
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
    <category term="运维" scheme="https://jaydenchang.top/tags/%E8%BF%90%E7%BB%B4/"/>
    
  </entry>
  
  <entry>
    <title>记录一次博客图片迁移</title>
    <link href="https://jaydenchang.top/post/0x0038.html"/>
    <id>https://jaydenchang.top/post/0x0038.html</id>
    <published>2025-07-25T16:00:00.000Z</published>
    <updated>2025-07-27T09:22:10.907Z</updated>
    
    <content type="html"><![CDATA[<hr><p>最近升级了一下个人电脑，从 Windows 换成了MacBook，在迁移资料的同时，顺便给小破站的图片换个图床，直接放在 GitHub的 repo 里。</p><p>hexo 的 <code>_config.yaml</code> 增加以下配置</p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">post_asset_folder: truemarked:  prependRoot: true  postAsset: true</code></pre><p>同时在 hexo 博客的文件夹安装插件：<ahref="https://github.com/hexojs/hexo-renderer-marked">hexo-renderer-marked</a></p><p>给文章起一个相同名的文件夹，例如当前文件是 <code>0x0038.md</code>,那么文件夹就是 0x0038，在 md 里插入图片标签，hexo 会自动渲染</p><pre class="line-numbers language-none"><code class="language-none"><hr>最近升级了一下个人电脑，从 Windows 换成了 MacBook，在迁移资料的同时，顺便给小破站的图片换个图床，直接放在 GitHub 的 repo 里。hexo 的 `_config.yaml` 增加以下配置<hexoPostRenderCodeBlock><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">post_asset_folder: truemarked:  prependRoot: true  postAsset: true</code></pre><p>同时在 hexo 博客的文件夹安装插件：<ahref="https://github.com/hexojs/hexo-renderer-marked">hexo-renderer-marked</a></p><p>给文章起一个相同名的文件夹，例如当前文件是 <code>0x0038.md</code>,那么文件夹就是 0x0038，在 md 里插入图片标签，hexo 会自动渲染</p><pre class="line-numbers language-none"><code class="language-none">![](0x0038&#x2F;xxx.jpg)</code></pre>这次先把图片迁，后面有时间了再慢慢设置其他的。</code></pre><p></hexoPostRenderCodeBlock></p><p>这次先把图片迁，后面有时间了再慢慢设置其他的。</p>]]></content>
    
    
    <summary type="html">重新打理一下博客</summary>
    
    
    
    
    <category term="随笔" scheme="https://jaydenchang.top/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>改造 Kubernetes 自定义调度器</title>
    <link href="https://jaydenchang.top/post/0x0037.html"/>
    <id>https://jaydenchang.top/post/0x0037.html</id>
    <published>2024-05-25T16:00:00.000Z</published>
    <updated>2025-07-25T14:40:02.225Z</updated>
    
    <content type="html"><![CDATA[<hr><h3 id="overview">Overview</h3><p>Kubernetes 默认调度器在调度 Pod 时并不关心特殊资源例如磁盘、GPU等，因此突发奇想来改造调度器，在翻阅官方调度器框架<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>、调度器配置<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup>和参考大佬的文章<sup id="fnref:3"><a href="#fn:3" rel="footnote">3</a></sup>后，自己也来尝试改写一下。</p><h3 id="环境配置">环境配置</h3><p>相关软件版本：</p><ul><li>Kubernetes 版本：v1.19.0</li><li>Docker 版本：v26.1.2</li><li>Prometheus 版本：v2.49</li><li>Node Exporter 版本：v1.7.0</li></ul><p>集群内有 1 个 master 和 3 个 node。</p><h3 id="实验部分">实验部分</h3><h4 id="项目总览">项目总览</h4><p>项目结构如下：</p><pre class="line-numbers language-none"><code class="language-none">.├── Dockerfile├── deployment.yaml├── go.mod├── go.sum├── main.go├── pkg│   ├── cpu│   │   └── cputraffic.go│   ├── disk│   │   └── disktraffic.go│   ├── diskspace│   │   └── diskspacetraffic.go│   ├── memory│   │   └── memorytraffic.go│   ├── network│   │   └── networktraffic.go│   └── prometheus.go├── scheduler├── scheduler.conf└── scheduler.yaml</code></pre><h4 id="插件部分">插件部分</h4><p>下面以构建内存插件为例。</p><p>定义插件名称、变量和结构体</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const MemoryPlugin &#x3D; &quot;MemoryTraffic&quot;var _ &#x3D; framework.ScorePlugin(&amp;MemoryTraffic&#123;&#125;)type MemoryTraffic struct &#123;    prometheus *pkg.PrometheusHandle    handle framework.FrameworkHandle&#125;</code></pre><p>下面来实现 framework.FrameworkHandle 的接口。</p><p>先定义插件初始化入口</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func New(plArgs runtime.Object, h framework.FrameworkHandle) (framework.Plugin, error) &#123;    args :&#x3D; &amp;MemoryTrafficArgs&#123;&#125;    if err :&#x3D; fruntime.DecodeInto(plArgs, args); err !&#x3D; nil &#123;        return nil, err    &#125;    klog.Infof(&quot;[MemoryTraffic] args received. Device: %s; TimeRange: %d, Address: %s&quot;, args.DeviceName, args.TimeRange, args.IP)    return &amp;MemoryTraffic&#123;        handle:     h,        prometheus: pkg.NewProme(args.IP, args.DeviceName, time.Minute*time.Duration(args.TimeRange)),    &#125;, nil&#125;</code></pre><p>实现 Score 接口，Score 进行初步打分</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (n *MemoryTraffic) Score(ctx context.Context, state *framework.CycleState, p *corev1.Pod, nodeName string) (int64, *framework.Status) &#123;    nodeBandwidth, err :&#x3D; n.prometheus.MemoryGetGauge(nodeName)    if err !&#x3D; nil &#123;        return 0, framework.NewStatus(framework.Error, fmt.Sprintf(&quot;error getting node bandwidth measure: %s&quot;, err))    &#125;    bandWidth :&#x3D; int64(nodeBandwidth.Value)    klog.Infof(&quot;[MemoryTraffic] node &#39;%s&#39; bandwidth: %v&quot;, nodeName, bandWidth)    return bandWidth, nil&#125;</code></pre><p>实现 NormalizeScore，对上一步 Score 的打分进行修正</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (n *MemoryTraffic) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *corev1.Pod, scores framework.NodeScoreList) *framework.Status &#123;    var higherScore int64    for _, node :&#x3D; range scores &#123;        if higherScore &lt; node.Score &#123;            higherScore &#x3D; node.Score        &#125;    &#125;    &#x2F;&#x2F; 计算公式为，满分 - (当前内存使用 &#x2F; 总内存 * 100)    &#x2F;&#x2F; 公式的计算结果为，内存使用率越大的节点，分数越低    for i, node :&#x3D; range scores &#123;        scores[i].Score &#x3D; node.Score * 100 &#x2F; higherScore        klog.Infof(&quot;[MemoryTraffic] Nodes final score: %v&quot;, scores[i].Score)    &#125;    klog.Infof(&quot;[MemoryTraffic] Nodes final score: %v&quot;, scores)    return nil&#125;</code></pre><p>配置插件名称和返回 ScoreExtension</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (n *MemoryTraffic) Name() string &#123;    return MemoryPlugin&#125;&#x2F;&#x2F; 如果返回framework.ScoreExtensions 就需要实现framework.ScoreExtensionsfunc (n *MemoryTraffic) ScoreExtensions() framework.ScoreExtensions &#123;    return n&#125;</code></pre><h4 id="prometheus-部分">Prometheus 部分</h4><p>首先来编写查询内存可用率的 PromQL</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const memoryMeasureQueryTemplate &#x3D; &#96; (avg_over_time(node_memory_MemAvailable_bytes[30m]) &#x2F; avg_over_time(node_memory_MemTotal_bytes[30m])) * 100 * on(instance) group_left(nodename) (node_uname_info&#123;nodename&#x3D;&quot;%s&quot;&#125;)&#96;</code></pre><p>然后来声明 PrometheusHandle</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type PrometheusHandle struct &#123;    deviceName string    timeRange  time.Duration    ip         string    client     v1.API&#125;</code></pre><p>另外在插件部分也要声明查询 Prometheus 的参数结构体</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type MemoryTrafficArgs struct &#123;    IP         string &#96;json:&quot;ip&quot;&#96;    DeviceName string &#96;json:&quot;deviceName&quot;&#96;    TimeRange  int    &#96;json:&quot;timeRange&quot;&#96;&#125;</code></pre><p>编写初始化 Prometheus 插件入口</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewProme(ip, deviceName string, timeRace time.Duration) *PrometheusHandle &#123;    client, err :&#x3D; api.NewClient(api.Config&#123;Address: ip&#125;)    if err !&#x3D; nil &#123;        klog.Fatalf(&quot;[Prometheus Plugin] FatalError creating prometheus client: %s&quot;, err.Error())    &#125;    return &amp;PrometheusHandle&#123;        deviceName: deviceName,        ip:         ip,        timeRange:  timeRace,        client:     v1.NewAPI(client),    &#125;&#125;</code></pre><p>编写通用查询接口，可供其他类型资源查询</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (p *PrometheusHandle) query(promQL string) (model.Value, error) &#123;    results, warnings, err :&#x3D; p.client.Query(context.Background(), promQL, time.Now())    if len(warnings) &gt; 0 &#123;        klog.Warningf(&quot;[Prometheus Query Plugin] Warnings: %v\n&quot;, warnings)    &#125;    return results, err&#125;</code></pre><p>获取内存可用率接口</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (p *PrometheusHandle) MemoryGetGauge(node string) (*model.Sample, error) &#123;    value, err :&#x3D; p.query(fmt.Sprintf(memoryMeasureQueryTemplate, node))    fmt.Println(fmt.Sprintf(memoryMeasureQueryTemplate, node))    if err !&#x3D; nil &#123;        return nil, fmt.Errorf(&quot;[MemoryTraffic Plugin] Error querying prometheus: %w&quot;, err)    &#125;    nodeMeasure :&#x3D; value.(model.Vector)    if len(nodeMeasure) !&#x3D; 1 &#123;        return nil, fmt.Errorf(&quot;[MemoryTraffic Plugin] Invalid response, expected 1 value, got %d&quot;, len(nodeMeasure))    &#125;    return nodeMeasure[0], nil&#125;</code></pre><p>然后在程序入口里启用插件并执行</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func main() &#123;    rand.Seed(time.Now().UnixNano())    command :&#x3D; app.NewSchedulerCommand(        app.WithPlugin(network.NetworkPlugin, network.New),        app.WithPlugin(disk.DiskPlugin, disk.New),        app.WithPlugin(diskspace.DiskSpacePlugin, diskspace.New),        app.WithPlugin(cpu.CPUPlugin, cpu.New),        app.WithPlugin(memory.MemoryPlugin, memory.New),    )    &#x2F;&#x2F; 对于外部注册一个plugin    &#x2F;&#x2F; command :&#x3D; app.NewSchedulerCommand(    &#x2F;&#x2F; app.WithPlugin(&quot;example-plugin1&quot;, ExamplePlugin1.New))    if err :&#x3D; command.Execute(); err !&#x3D; nil &#123;        fmt.Fprintf(os.Stderr, &quot;%v\n&quot;, err)        os.Exit(1)    &#125;&#125;</code></pre><h4 id="配置部分">配置部分</h4><p>为方便观察，这里使用二进制方式运行，准备运行时的配置文件</p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">apiVersion: kubescheduler.config.k8s.io&#x2F;v1beta1kind: KubeSchedulerConfigurationclientConnection:  kubeconfig: &#x2F;etc&#x2F;kubernetes&#x2F;scheduler.confprofiles:- schedulerName: custom-scheduler  plugins:    score:      enabled:      - name: &quot;CPUTraffic&quot;        weight: 3      - name: &quot;MemoryTraffic&quot;        weight: 4      - name: &quot;DiskSpaceTraffic&quot;        weight: 3      - name: &quot;NetworkTraffic&quot;        weight: 2      disabled:      - name: &quot;*&quot;  pluginConfig:    - name: &quot;NetworkTraffic&quot;      args:        ip: &quot;http:&#x2F;&#x2F;172.19.32.140:9090&quot;        deviceName: &quot;eth0&quot;        timeRange: 60       - name: &quot;CPUTraffic&quot;      args:        ip: &quot;http:&#x2F;&#x2F;172.19.32.140:9090&quot;        deviceName: &quot;eth0&quot;        timeRange: 0    - name: &quot;MemoryTraffic&quot;      args:        ip: &quot;http:&#x2F;&#x2F;172.19.32.140:9090&quot;        deviceName: &quot;eth0&quot;        timeRange: 0    - name: &quot;DiskSpaceTraffic&quot;      args:        ip: &quot;http:&#x2F;&#x2F;172.19.32.140:9090&quot;        deviceName: &quot;eth0&quot;        timeRange: 0</code></pre><p>kubeconfig 处为 master 节点的scheduler.conf，以实际路径为准，内包含集群的证书哈希，ip 为部署Prometheus 节点的 ip，端口为 Promenade 配置中对外暴露的端口。</p><p>将二进制文件和 scheduler.yaml 放至 master 同一目录下运行：</p><pre class="line-numbers language-none"><code class="language-none">.&#x2F;scheduler --logtostderr&#x3D;true \--address&#x3D;127.0.0.1 \--v&#x3D;6 \--config&#x3D;&#96;pwd&#96;&#x2F;scheduler.yaml \--kubeconfig&#x3D;&quot;&#x2F;etc&#x2F;kubernetes&#x2F;scheduler.conf&quot; \</code></pre><h4 id="验证结果">验证结果</h4><p>准备一个要部署的 Pod，使用指定的调度器名称</p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">apiVersion: apps&#x2F;v1kind: Deploymentmetadata:  name: gin  namespace: default  labels:    app: ginspec:  replicas: 2  selector:    matchLabels:      app: gin  template:    metadata:      labels:        app: gin    spec:      schedulerName: my-custom-scheduler  # 使用自定义调度器      containers:      - name: gin        image: jaydenchang&#x2F;k8s_test:latest        imagePullPolicy: Always        command: [&quot;.&#x2F;app&quot;]        ports:        - containerPort: 9999          protocol: TCP</code></pre><p>最后的可以查看日志，部分日志如下：</p><pre class="line-numbers language-none"><code class="language-none">I0808 17:32:35.138289   27131 memorytraffic.go:83] [MemoryTraffic] node &#39;node1&#39; bandwidth: %!s(int64&#x3D;2680340)I0808 17:32:35.138763   27131 memorytraffic.go:70] [MemoryTraffic] Nodes final score: [&#123;node1 2680340&#125; &#123;node2 0&#125;]I0808 17:32:35.138851   27131 memorytraffic.go:70] [MemoryTraffic] Nodes final score: [&#123;node1 71&#125; &#123;node2 0&#125;]I0808 17:32:35.138911   27131 memorytraffic.go:73] [MemoryTraffic] Nodes final score: [&#123;node1 71&#125; &#123;node2 0&#125;]I0808 17:32:35.139565   27131 default_binder.go:51] Attempting to bind default&#x2F;go-deployment-66878c4885-b4b7k to node1I0808 17:32:35.141114   27131 eventhandlers.go:225] add event for scheduled pod default&#x2F;go-deployment-66878c4885-b4b7kI0808 17:32:35.141714   27131 eventhandlers.go:205] delete event for unscheduled pod default&#x2F;go-deployment-66878c4885-b4b7kI0808 17:32:35.143504   27131 scheduler.go:609] &quot;Successfully bound pod to node&quot; pod&#x3D;&quot;default&#x2F;go-deployment-66878c4885-b4b7k&quot; node&#x3D;&quot;node1&quot; evaluatedNodes&#x3D;2 feasibleNodes&#x3D;2I0808 17:32:35.104540   27131 scheduler.go:609] &quot;Successfully bound pod to node&quot; pod&#x3D;&quot;default&#x2F;go-deployment-66878c4885-b4b7k&quot; node&#x3D;&quot;node1&quot; evaluatedNodes&#x3D;2 feasibleNodes&#x3D;2</code></pre><h3 id="参考链接">参考链接</h3><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><spanstyle="display: inline-block; vertical-align: top; padding-right: 10px;">1.</span><spanstyle="display: inline-block; vertical-align: top;"><a href="https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/">SchedulingFramework | Kubernetes</a></span><a href="#fnref:1" rev="footnote">↩︎</a></li><li id="fn:2"><spanstyle="display: inline-block; vertical-align: top; padding-right: 10px;">2.</span><spanstyle="display: inline-block; vertical-align: top;"><a href="https://kubernetes.io/docs/reference/scheduling/config/">SchedulerConfiguration | Kubernetes</a></span><a href="#fnref:2" rev="footnote">↩︎</a></li><li id="fn:3"><spanstyle="display: inline-block; vertical-align: top; padding-right: 10px;">3.</span><spanstyle="display: inline-block; vertical-align: top;"><a href="https://www.oomkill.com/2022/08/ch22-custom-scheduler.html">基于Prometheus的Kubernetes网络调度器| Cylon's Collection(oomkill.com)</a></span><a href="#fnref:3" rev="footnote"> ↩︎</a></li></ol></div></div></div>]]></content>
    
    
    <summary type="html">重新编译一个新的 k8s 调度器</summary>
    
    
    
    <category term="DevOps" scheme="https://jaydenchang.top/categories/DevOps/"/>
    
    
    <category term="技术" scheme="https://jaydenchang.top/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="k8s" scheme="https://jaydenchang.top/tags/k8s/"/>
    
    <category term="运维" scheme="https://jaydenchang.top/tags/%E8%BF%90%E7%BB%B4/"/>
    
  </entry>
  
  <entry>
    <title>基于Ubuntu20.04在k8s 1.25部署gin+MySQL服务</title>
    <link href="https://jaydenchang.top/post/0x0036.html"/>
    <id>https://jaydenchang.top/post/0x0036.html</id>
    <published>2023-07-03T16:00:00.000Z</published>
    <updated>2025-07-27T05:30:23.622Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言">0. 前言</h4><p>某天突发奇想，既然都学了 docker 了，那干脆，顺便把 kubernetes也学了，于是开始了我长达一个月的环境搭建、踩坑历程。</p><p>最开始，我的想法是，在我的物理机使用 WSL + docker 来部署服务，但是WSL部署的服务好像只是单机版，和实际生产中的情况相差甚远，于是，我去弄了几台服务器，一台阿里云2C2G，一台腾讯云 4C8G，一台腾讯云 2C2G。</p><p>基于本人比较喜欢折腾的特点，我没有选择常见的 CentOS来搭建，而是使用了 Ubuntu (问就是平时用 WSL 用多了，对 Ubuntu 有了感情bushi)。然后就开始了我漫长的异地组网历程。记得前后搭建了半个多月吧，前面七天基本在搭建环境，解决镜像源问题，后面七天在解决两个node之间的通信，后面发现，我租用的服务器，都是弹性服务器，没法换公网和内网的ip，目前跨 VPC 构建 k8s 集群不是一个好方法(<del>毕竟企业不可能这样做，最多也就是学生搞来玩玩</del>)，没办法，只好自己搞虚拟机了，不过还好，又历经一周，虚拟机的搭建成功了，后面如果能搞到更多磁盘和内存的话，可能会尝试双master 和多 node 的集群。</p><h4 id="环境搭建">1. 环境搭建</h4><h5 id="环境说明">1.1 环境说明</h5><p>节点名称、ip：</p><ul><li>master：192.168.22.222</li><li>node1：192.168.22.223</li></ul><p>master 要求 至少 2G RAM，2 核 CPU</p><h5 id="版本信息">1.2 版本信息</h5><ul><li>系统版本：Ubuntu server 20.04.6</li><li>Docker：20.10.21</li><li>Kubernetes：1.25.0</li></ul><h5 id="环境配置">1.3 环境配置</h5><p>设置主机名及解析</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># mastersudo systemctl set-hosename mastersudo cat &gt; &#x2F;etc&#x2F;hosts &lt;&lt; EOF192.168.22.222 master192.168.22.223 node1EOF# node1systemctl set-hosename node1sudo cat &gt; &#x2F;etc&#x2F;hosts &lt;&lt; EOF192.168.22.222 master192.168.22.223 node1EOF</code></pre><p>关闭 swap</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo swapoff -a# 注释&#x2F;etc&#x2F;fstab文件的最后一行sudo sed -i &#39;&#x2F;swap&#x2F;s&#x2F;^&#x2F;#&#x2F;&#39; &#x2F;etc&#x2F;fstab</code></pre><p>开启 IPv4 转发</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo cat &lt;&lt;EOF | sudo tee &#x2F;etc&#x2F;modules-load.d&#x2F;k8s.confoverlaybr_netfilterEOF# 分割线modprobe overlaymodprobe br_netfilter# 分割线sudo cat &lt;&lt;EOF | sudo tee &#x2F;etc&#x2F;sysctl.d&#x2F;k8s.confnet.bridge.bridge-nf-call-iptables  &#x3D; 1net.bridge.bridge-nf-call-ip6tables &#x3D; 1net.ipv4.ip_forward                 &#x3D; 1EOF#分割线sudo sysctl --system</code></pre><h5 id="安装-containerd">1.4 安装 containerd</h5><p>从 k8s 1.25 开始使用 containerd 来作为底层容器支持，根据 <ahref="https://containerd.io/releases/">k8s 和 containerd的匹配要求</a>，这里我们使用 containerd 1.7.0</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># apt 安装无法安装最新的版本，这里使用tar包解压wget https:&#x2F;&#x2F;github.com&#x2F;containerd&#x2F;containerd&#x2F;releases&#x2F;download&#x2F;v1.7.0&#x2F;containerd-1.7.0-linux-amd64.tar.gz tar zxvf containerd-1.7.0-linux-amd64.tar.gz -C &#x2F;usr&#x2F;local# 导出默认配置sudo containerd config default &gt; &#x2F;etc&#x2F;containerd&#x2F;config.toml# 编辑配置文件sudo vim &#x2F;etc&#x2F;containerd&#x2F;config.toml# 进入到 vim 后搜索、替换# 修改sandbox_image行替换为aliyun的pause镜像sandbox_image &#x3D; &quot;registry.aliyuncs.com&#x2F;google_containers&#x2F;pause:3.8&quot;# 配置 systemd cgroup 驱动 [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc]  ...  [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc.options]    SystemdCgroup &#x3D; true    # 配置镜像加速[plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry]      [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors]        [plugins. &quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;docker.io&quot;]          endpoint &#x3D; [&quot;https:&#x2F;&#x2F;registry.aliyuncs.com&quot;]</code></pre><p>添加 containerd 服务</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo cat &gt; &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;containerd.service &lt;&lt; EOF# Copyright The containerd Authors.## Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);# you may not use this file except in compliance with the License.# You may obtain a copy of the License at##     http:&#x2F;&#x2F;www.apache.org&#x2F;licenses&#x2F;LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.[Unit]Description&#x3D;containerd container runtimeDocumentation&#x3D;https:&#x2F;&#x2F;containerd.ioAfter&#x3D;network.target local-fs.target[Service]#uncomment to enable the experimental sbservice (sandboxed) version of containerd&#x2F;cri integration#Environment&#x3D;&quot;ENABLE_CRI_SANDBOXES&#x3D;sandboxed&quot;ExecStartPre&#x3D;-&#x2F;sbin&#x2F;modprobe overlayExecStart&#x3D;&#x2F;usr&#x2F;local&#x2F;bin&#x2F;containerdType&#x3D;notifyDelegate&#x3D;yesKillMode&#x3D;processRestart&#x3D;alwaysRestartSec&#x3D;5# Having non-zero Limit*s causes performance problems due to accounting overhead# in the kernel. We recommend using cgroups to do container-local accounting.LimitNPROC&#x3D;infinityLimitCORE&#x3D;infinityLimitNOFILE&#x3D;infinity# Comment TasksMax if your systemd version does not supports it.# Only systemd 226 and above support this version.TasksMax&#x3D;infinityOOMScoreAdjust&#x3D;-999[Install]WantedBy&#x3D;multi-user.targetEOF</code></pre><p>加载配置，启动 contained 服务</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">systemctl daemon-reloadsystemctl enable --now containerd</code></pre><h5 id="安装-kubernetes">1.5 安装 kubernetes</h5><p>安装必要组件</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo apt-get install -y apt-transport-https ca-certificates curl</code></pre><p>添加阿里云安装源</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo cat &lt;&lt;EOF &gt; &#x2F;etc&#x2F;apt&#x2F;sources.list.d&#x2F;kubernetes.listdeb https:&#x2F;&#x2F;mirrors.aliyun.com&#x2F;kubernetes&#x2F;apt&#x2F; kubernetes-xenial mainEOFgpg --keyserver keyserver.ubuntu.com --recv-keys BA07F4FBgpg --export --armor BA07F4FB | sudo apt-key add -curl https:&#x2F;&#x2F;mirrors.aliyun.com&#x2F;kubernetes&#x2F;apt&#x2F;doc&#x2F;apt-key.gpg | apt-key add -</code></pre><p>安装 k8s</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo apt-get updatesudo apt-get install -y kubelet&#x3D;1.25.0-00 kubeadm&#x3D;1.25.0-00 kubectl&#x3D;1.25.0-00systemctl enable --now kubelet </code></pre><p>标记软件包，避免自动更新</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo apt-mark hold kubelet kubeadm kubectl</code></pre><h5 id="安装-docker">1.6 安装 docker</h5><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">apt install docker.io</code></pre><h5 id="初始化-kubernetes-集群">1.7 初始化 kubernetes 集群</h5><p>使用 kubeadm 初始化</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># 这里的apiserver那行是master的ip, 注意service-cidr和pod-network-cidr和节点ip不要出现在同一网段kubeadm init \--image-repository registry.aliyuncs.com&#x2F;google_containers \--apiserver-advertise-address&#x3D;192.168.22.222 \--service-cidr&#x3D;10.96.0.0&#x2F;12 \--pod-network-cidr&#x3D;22.22.0.0&#x2F;16 \--kubernetes-version v1.25.0</code></pre><p>如果没有报错的话，配置环境变量</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mkdir -p $HOME&#x2F;.kubesudo cp -i &#x2F;etc&#x2F;kubernetes&#x2F;admin.conf $HOME&#x2F;.kube&#x2F;configsudo chown $(id -u):$(id -g) $HOME&#x2F;.kube&#x2F;configexport KUBECONFIG&#x3D;&#x2F;etc&#x2F;kubernetes&#x2F;admin.conf</code></pre><p>node1 端输入类似这样的命令</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># 记得在 node1 开放6443端口kubeadm join 192.168.22.223:6443 --token dc4wxa.qar86v4pb1b2umvm \        --discovery-token-ca-cert-hash sha256:1df0074a2226ed1a56f53b9d33bf263c51d3794b4c4b9d6132f07b68592ac38a # token 是随机生成的</code></pre><p>重新生成 token</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">kubeadm token create --print-join-command</code></pre><h5 id="安装-calico-网络插件">1.8 安装 calico 网络插件</h5><p>流行的有 flannel 和 calico，这里选择 calico</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">wget https:&#x2F;&#x2F;raw.staticdn.net&#x2F;projectcalico&#x2F;calico&#x2F;v3.24.1&#x2F;manifests&#x2F;tigera-operator.yamlsudo kubectl create -f tigera-operator.yamlwget https:&#x2F;&#x2F;raw.staticdn.net&#x2F;projectcalico&#x2F;calico&#x2F;v3.24.1&#x2F;manifests&#x2F;custom-resources.yamlvim custom-resources.yaml# 修改cidr配置apiVersion: operator.tigera.io&#x2F;v1kind: Installationmetadata:  name: defaultspec:  # Configures Calico networking.  calicoNetwork:    # Note: The ipPools section cannot be modified post-install.    ipPools:    - blockSize: 26      cidr: 10.244.0.0&#x2F;16 # 修改为刚刚初始化时的 pod-network-cidr      encapsulation: VXLANCrossSubnet      natOutgoing: Enabled      nodeSelector: all()      </code></pre><p>注意，<code>custom-resources.yaml</code> 的<code>spec.calicoNetwork.ipPools.cidr</code> 一定要和刚刚初始化的<code>pod-network-cidr</code> 一致，不然无法添加 calico 插件</p><p>查看 pod 状态</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ kubectl get pod -ANAMESPACE          NAME                                       READY   STATUS    RESTARTS       AGEcalico-apiserver   calico-apiserver-95575566-mpv54            1&#x2F;1     Running   4 (156m ago)   3d1hcalico-apiserver   calico-apiserver-95575566-n4x5w            1&#x2F;1     Running   4 (156m ago)   3d1hcalico-system      calico-kube-controllers-85666c5b94-lll7q   1&#x2F;1     Running   4 (156m ago)   3d1hcalico-system      calico-node-djqts                          1&#x2F;1     Running   4 (14h ago)    3d1hcalico-system      calico-node-wp4cf                          1&#x2F;1     Running   4 (156m ago)   3d1hcalico-system      calico-typha-76fd59d84d-xn79m              1&#x2F;1     Running   6 (156m ago)   3d1hcalico-system      csi-node-driver-74p7m                      2&#x2F;2     Running   8 (156m ago)   3d1hcalico-system      csi-node-driver-t86b2                      2&#x2F;2     Running   8 (14h ago)    3d1hkube-system        coredns-c676cc86f-tmq7f                    1&#x2F;1     Running   4 (156m ago)   3d1hkube-system        etcd-master                                1&#x2F;1     Running   4 (156m ago)   3d1hkube-system        kube-apiserver-master                      1&#x2F;1     Running   4 (156m ago)   3d1hkube-system        kube-controller-manager-master             1&#x2F;1     Running   4 (156m ago)   3d1hkube-system        kube-proxy-449rk                           1&#x2F;1     Running   4 (14h ago)    3d1hkube-system        kube-proxy-vswhw                           1&#x2F;1     Running   4 (156m ago)   3d1hkube-system        kube-scheduler-master                      1&#x2F;1     Running   4 (156m ago)   3d1htigera-operator    tigera-operator-6675dc47f4-wcfxf           1&#x2F;1     Running   7 (155m ago)   3d1h</code></pre><h4 id="部署-mysql">2. 部署 MySQL</h4><p>先编写一个 <code>mysql-deploy.yaml</code> 配置文件</p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">apiVersion: apps&#x2F;v1                             # apiserver的版本kind: Deployment                                # 副本控制器deployment，管理pod和RSmetadata:  name: mysql                                   # deployment的名称，全局唯一  namespace: default                            # deployment所在的命名空间  labels:    app: mysqlspec:  replicas: 1                                   # Pod副本期待数量  selector:    matchLabels:                                # 定义RS的标签      app: mysql                                # 符合目标的Pod拥有此标签  strategy:                                     # 定义升级的策略    type: RollingUpdate                         # 滚动升级，逐步替换的策略  template:                                     # 根据此模板创建Pod的副本（实例）    metadata:      labels:        app: mysql                              # Pod副本的标签，对应RS的Selector    spec:      nodeName: node1                           # 指定pod运行在的node      containers:                               # Pod里容器的定义部分        - name: mysql                           # 容器的名称          image: mysql:8.0                      # 容器对应的docker镜像          volumeMounts:                         # 容器内挂载点的定义部分            - name: time-zone                   # 容器内挂载点名称              mountPath: &#x2F;etc&#x2F;localtime         # 容器内挂载点路径，可以是文件或目录            - name: mysql-data              mountPath: &#x2F;var&#x2F;lib&#x2F;mysql         # 容器内mysql的数据目录            - name: mysql-logs              mountPath: &#x2F;var&#x2F;log&#x2F;mysql         # 容器内mysql的日志目录          ports:            - containerPort: 3306               # 容器暴露的端口号          env:                                  # 写入到容器内的环境容量            - name: MYSQL_ROOT_PASSWORD         # 定义了一个mysql的root密码的变量              value: &quot;root&quot;      volumes:                                  # 本地需要挂载到容器里的数据卷定义部分        - name: time-zone                       # 数据卷名称，需要与容器内挂载点名称一致          hostPath:            path: &#x2F;etc&#x2F;localtime                # 挂载到容器里的路径，将localtime文件挂载到容器里，可让容器使用本地的时区        - name: mysql-data          hostPath:            path: &#x2F;data&#x2F;mysql&#x2F;data              # 本地存放mysql数据的目录        - name: mysql-logs          hostPath:            path: &#x2F;data&#x2F;mysql&#x2F;logs              # 本地存入mysql日志的目录</code></pre><p>在编写一个对外提供服务的 <code>mysql-svc.yaml</code></p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">apiVersion: v1kind: Servicemetadata:  name: mysql  labels:    name: mysqlspec:  type: NodePort  ports:    - port: 3306      targetPort: 3306      nodePort: 30001  selector:    app: mysql</code></pre><p>创建服务</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">kubectl create -f mysql-deploy.yamlkubectl create -f mysql-svc.yaml</code></pre><p>查看节点是否正常运行</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ kubectl get podNAME                               READY   STATUS    RESTARTS      AGEmysql-566cddf86-v65mw              1&#x2F;1     Running   2 (15h ago)   2d1h</code></pre><p>访问数据库，密码的话，刚有在 yaml 中说明，为<code>root</code>，登陆成功后即可输入数据</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">kubectl exec -it mysql-566cddf86-v65mw -- mysql -u root -p</code></pre><img src="/post/0x0036/mysql.png" class=""><p>开放远程连接权限</p><pre class="line-numbers language-mysql" data-language="mysql"><code class="language-mysql">FLUSH PRIVILEGES;&#x2F;* mysql8.0 只能以这种方式来赋权*&#x2F;alter user &#39;root&#39;@&#39;%&#39; identified with mysql_native_password by &#39;root&#39;;GRANT SELECT, INSERT, UPDATE, DELETE  ON *.* TO &#39;root&#39;@&#39;%&#39;;flush privileges;</code></pre><p>然后 node1 节点要开放 3306 端口和 30001 端口(刚刚设置的对外开放的端口)，可以在宿主机连接集群的数据库</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mysql -u root -h 192.168.22.223 -P 30001 -p</code></pre><img src="/post/0x0036/mysql_login_by_host.png" class=""><h4 id="部署-gin-服务">3. 部署 gin 服务</h4><p><a href="https://gitee.com/jaydenchang/buy-house">BuyHouse:一个简单的gin+MySQL数据查询系统，课程实训项目 (gitee.com)</a></p><p>这里使用了学校实训项目的一个 demo，这里我只用到了 gin 部分。</p><p>制作镜像以前，我们先看看集群里 MySQL 的 ip</p><img src="/post/0x0036/mysql_cluster_ip.png" class=""><p>看到 mysql 集群 ip 为 <code>22.22.166.184</code>，那么 gin里数据库配置 (<code>database/mysql.go</code>) 的 ip 也要改成<code>22.22.166.184</code></p><p>首先是把项目打包成 docker image，在项目根目录 (<code>go.mod</code>所在目录)，编写 Dockerfile</p><pre class="line-numbers language-docker" data-language="docker"><code class="language-docker">FROM golang:1.18-alpine AS builderWORKDIR &#x2F;appCOPY . &#x2F;appRUN go env -w GO111MODULE&#x3D;onRUN go env -w GOPROXY&#x3D;https:&#x2F;&#x2F;goproxy.cn,directRUN CGO_ENABLED&#x3D;0 go build -ldflags &quot;-s -w&quot; -o appFROM alpine AS runnerWORKDIR &#x2F;appCOPY --from&#x3D;builder &#x2F;app&#x2F;app .EXPOSE 9999:9999ENTRYPOINT [&quot;.&#x2F;app&quot;]</code></pre><p>这里使用多级构建(实际也就两层，太懒了，不想搞太多了，十多兆已经是我可以接受的大小了doge)，如果不这样做，构建出的镜像差不多1G，不论是推送到仓库还是拉取，都会很影响效率。</p><p>然后执行</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">docker build -t buy-house .</code></pre><p>docker 就会拉取、打包镜像，用 <code>docker images</code>可以查看多了一个 <code>buy-house</code>的镜像，如果想要推送到个人仓库的话，执行</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">docker tag buy-house jaydenchang&#x2F;buy-house</code></pre><p>然后在 Docker 客户端 (已登陆了个人账号) 推送即可。</p><p>然后编写<code>go-deploy.yaml</code>，这里我使用自己制作的镜像，并上传到了个人仓库</p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">apiVersion: apps&#x2F;v1kind: Deploymentmetadata:  name: go-deployment  labels:    app: gospec:  selector:    matchLabels:      app: go  replicas: 2  minReadySeconds: 5  strategy:    type: RollingUpdate    rollingUpdate:      maxSurge: 1      maxUnavailable: 1  template:    metadata:      labels:        app: go    spec:      containers:      - image: jaydenchang&#x2F;buy-house:latest        name: go        imagePullPolicy: Always        command: [&quot;.&#x2F;app&quot;,&quot;-v&quot;,&quot;v1.3&quot;]        ports:        - containerPort: 9999          protocol: TCP</code></pre><p>编写 <code>go-svc.yaml</code></p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml">apiVersion: v1kind: Servicemetadata:  name: go-service  labels:    app: gospec:  selector:    app: go  ports:    - name: go-port      protocol: TCP      port: 9999      targetPort: 9999      nodePort: 31080  type: NodePort</code></pre><p>生成节点</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">kubectl create -f go-svc.yamlkubectl create -f go-deploy.yaml</code></pre><p>检查一下</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ kubectl get podNAME                               READY   STATUS    RESTARTS      AGEbusybox                            1&#x2F;1     Running   2 (14m ago)   173mgo-deployment-66878c4885-twmzs     1&#x2F;1     Running   0             29sgo-deployment-66878c4885-zxjqb     1&#x2F;1     Running   0             29smysql-566cddf86-v65mw              1&#x2F;1     Running   3 (14m ago)   2d2h</code></pre><p>go 的镜像被分配到了 node1 节点，我们输入<code>node1IP:port</code>，也就是 <code>192.168.22.223:31080</code></p><img src="/post/0x0036/test_port.png" class=""><p>其他接口就不测试了(<del>一个拿不出眼的小项目就不展示太多了</del>)。至此，整个部署过程结束。这次小试牛刀，搭建一个比较简易的双节点集群。未来的学习，可能会尝试更复杂的集群部署(<del>先给自己挖个坑吧</del>)。</p><h4 id="参考链接">参考链接</h4><p><a href="https://q.cnblogs.com/q/136113/">ubuntu 运行 apt-get update时阿里云 k8s 安装源报错_已解决_博问_博客园 (cnblogs.com)</a></p><p><ahref="https://blog.csdn.net/qq_43285879/article/details/120794910">一个k8s集群——跨云服务器部署_k8s跨云部署_qq_43285879的博客-CSDN博客</a></p><p><ahref="https://github.com/kanzihuang/kubespray-extranet">kanzihuang/kubespray-extranet:Create a kubernetes cluster on the public network (github.com)</a></p><p><ahref="https://cn.bing.com/search?q=跨VPC或者跨云供应商搭建K8S集群&amp;aqs=edge..69i57j69i61.301j0j1&amp;FORM=ANCMS9&amp;PC=HCTS">跨VPC或者跨云供应商搭建K8S集群- Search (bing.com)</a></p><p><ahref="https://www.cnblogs.com/tttlv/p/14395865.html">公网环境搭建k8s集群- ttlv - 博客园 (cnblogs.com)</a></p><p><ahref="https://juejin.cn/post/7107954026875977764#heading-15">Kubernetes（k8s）安装以及搭建k8s-Dashboard详解- 掘金 (juejin.cn)</a></p><p><a href="https://zhuanlan.zhihu.com/p/636831803">Kubernetes 1.27快速安装手册 - 知乎 (zhihu.com)</a></p><p><ahref="https://www.aledk.com/2022/10/29/ubuntu-k8s/">基于Ubuntu-22.04kubeadm安装K8s-v1.25.0 | Marshall's blog (aledk.com)</a></p><p><ahref="https://blog.csdn.net/qq_34378595/article/details/123568313">k8s初始化master节点时无calico，coredns一直是pending状态_calicopending_copa~的博客-CSDN博客</a></p><p><ahref="https://blog.csdn.net/weixin_39132936/article/details/103260481">K8s部署自己的web项目_k8s前端_肖仙女hhh的博客-CSDN博客</a></p><p><ahref="https://github.com/kanzihuang/kubespray-extranet/blob/main/docs/solutions.md">公网创建kubernetes 集群的解决方案 · GitHub</a></p><p><ahref="https://zhuanlan.zhihu.com/p/410371256">在Linux公网、云服务器搭建K8s集群- 知乎 (zhihu.com)</a></p><p><ahref="https://blog.csdn.net/weixin_42599091/article/details/107244014">(43条消息)kubernetes集群部署nginx应用服务_kubernetes部署nginx_鱼大虾的博客-CSDN博客</a></p><p><ahref="https://www.cnblogs.com/blayn/p/16037199.html">k8s集群部署mysql完整过程记录- blayn - 博客园 (cnblogs.com)</a></p><p><ahref="https://www.cnblogs.com/guyouyin123/p/15688012.html">部署go项目到k8s集群- Jeff的技术栈 - 博客园 (cnblogs.com)</a></p><p><ahref="https://blog.csdn.net/Scoful/article/details/120729102">如何给go项目打最小docker镜像，足足降低99%_Scoful的博客-CSDN博客</a></p><p><a href="https://jaydenchang.top/post/0x0036">基于Ubuntu20.04在k8s1.25部署gin+MySQL服务 | Jayden's Blog (jaydenchang.top)</a></p>]]></content>
    
    
    <summary type="html">使用Ubuntu20.04双节点，在k8s部署gin+MySQL服务</summary>
    
    
    
    <category term="DevOps" scheme="https://jaydenchang.top/categories/DevOps/"/>
    
    
    <category term="技术" scheme="https://jaydenchang.top/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
    <category term="k8s" scheme="https://jaydenchang.top/tags/k8s/"/>
    
    <category term="运维" scheme="https://jaydenchang.top/tags/%E8%BF%90%E7%BB%B4/"/>
    
  </entry>
  
  <entry>
    <title>自己动手写Docker学习笔记</title>
    <link href="https://jaydenchang.top/post/0x0035.html"/>
    <id>https://jaydenchang.top/post/0x0035.html</id>
    <published>2023-05-20T16:00:00.000Z</published>
    <updated>2025-07-27T08:39:54.033Z</updated>
    
    <content type="html"><![CDATA[<h2 id="零前言">零、前言</h2><p>本文为《自己动手写 Docker》的学习，对于各位学习 docker的同学非常友好，非常建议买一本来学习。</p><p>书中有摘录书中的一些知识点，不过限于篇幅，没有全部摘录<del>(主要也是懒)</del>。项目仓库地址为：<ahref="https://github.com/JaydenChang/simple-docker">JaydenChang/simple-docker(github.com)</a></p><h2 id="一概念篇">一、概念篇</h2><h3 id="基础知识">1. 基础知识</h3><h4 id="kernel">1.1 kernel</h4><p>kernel (内核)指大多数操作系统的核心部分，由操作系统中用于管理存储器、文件、外设和系统资源的部分组成。操作系统内核通常运行进程，并提供进程间通信。</p><h4 id="namespace">1.2 namespace</h4><p>namespace 是 Linux 自带的功能来隔离内核资源的机制。</p><p>Linux 中有 6 种 namespace</p><h5 id="uts-namespace">1.2.1 UTS Namespace</h5><p>UTS，UNIX Time Sharing，用于隔离 nodeName (主机名) 和 domainName(域名)。在该 Namespace 下修改 hostname 不会影相其他的 Namespace。</p><h5 id="ipc-namespace">1.2.2 IPC Namespace</h5><p>IPC，Inter-Process Communication (进程间通讯)，用于隔离 System V IPC和 POSIX message queues (一种消息队列，结构为链表)。</p><p>两种 IPC 本质上差不多，System V IPC 随内核持续，POSIX IPC随进程持续。</p><h5 id="pid-namespace">1.2.3 PID Namespace</h5><p>PID，Process IDs，用于隔绝 PID。同样的进程，在不同 Namespace里是不同的 PID。新建的 PID Namespace 里第一个 PID 是1。</p><h5 id="mount-namespace">1.2.4 Mount Namespace</h5><p>用于隔绝文件系统，挂载了某一目录，在这个 Namespace下就会把这个目录当作根目录，我们看到的文件系统树就会以这个目录为根目录。</p><p>mount 操作本身不会影响到外部，docker 中的 volume也用到了这个特性。</p><h5 id="user-namespace">1.2.5 User Namespace</h5><p>用于 隔离用户组 ID。</p><h5 id="network-namespace">1.2.6 Network Namespace</h5><p>每个 Namespace 都有一套自己的网络设备，可以使用相同的端口号，映射到host 的不同端口。</p><h4 id="linux-cgroups">1.3 Linux Cgroups</h4><p>Cgroups 全称为 Control Groups，是 Linux内核提供的物理资源隔离机制。</p><h5 id="cgroups-的三个组件">1.3.1 Cgroups 的三个组件</h5><ul><li>cgroup：一个 cgroup 包含一组进程，且可以有 subsystem的参数配置，以关联一组 subsystem。</li><li>subsystem：一组资源控制的模块。</li><li>hierarchy：把一组 cgroups 串成一个树状结构，以提供继承的功能。</li></ul><h5 id="这三个组件的关联">1.3.2 这三个组件的关联</h5><p>Linux 有一些限制：</p><ul><li>首先，创建一个 hierarchy。这个 hierarchy 有一个 cgroup根节点，所有的进程都会被加到这个根节点上，所有在这个 hierarchy上创建的节点都是这个根节点的子节点。</li><li>一个 subsystem 只能加到一个 hierarchy 上。</li><li>但是一个 subsystem 可以加到同一个 hierarchy 的多个 cgroups 上。</li><li>一个 hierarchy 可以有多个 subsystem。</li><li>一个进程可以在多个 cgroups 中，但是这些 cgroup 必须在不同的hierarchy 中。</li><li>一个进程 fork 出子进程时，父进程和子进程属于同一个 cgroup。</li></ul><h5 id="cgroup-和-subsystem-和-hierarchy-之间的联系">1.3.3 cgroup 和subsystem 和 hierarchy 之间的联系</h5><ul><li>hierarchy 就是一颗 cgroups 树，由多个 cgroups 构成。每一个 hierarchy建立时会包含 ==<em>所有</em>== 的Linux 进程。这里的 “所有”就是当前系统运行中的所有进程，每个 hierarchy上的全部进程都是一样的，不同的 hierarchy指的其实只是不同的分组方式，这也是为什么一个进程可以存在于多个 hierarchy上；准确来说，一个进程一定会同时存在于所有的 hierarchy上，区别在被放在的 cgroup 可能会有差异。</li><li>Linux 的 subsystem 只有一个的说法，没有一种的说法，也就是在一个hierarchy 上使用了 memory subsystem，那么在其他 hierarchy 就不能使用memory subsystem 了。</li><li>subsystem 是一种资源控制器，有很多个 subsystem，每个 subsystem控制不同的资源。subsystem 和 cgroups 关联。新建一个 cgroups文件夹时，里面会自动生成一堆配置文件，那个就是 subsystem 配置文件。但<code>subsystem 配置文件</code> 不是 <code>subsystem</code>，就像<code>.git</code> 不是 <code>git</code> 一样，就像没安装 git也可以从别人那里获得 <code>.git</code>文件夹，只是不能用罢了。<code>subsystem 配置文件</code>也是如此，新建一个 cgroup 就会生成<code>cgroup 配置文件</code>，但并不代表你关联了一个subsystem。只有当改变了一个<code>cgroup 配置文件</code>，里面要限制某种资源时，就会自动关联到这个被限制的资源所对应的subsystem 上。</li><li>假设我的 Linux 有 12 个 subsystem，也就是说我最多只能建 12 个hierarchy (不加 subsystem 的情况下可以建更多 hierarchy，这样 cgroup就变成纯分组使用)。每一个 hierarchy 上一个 subsystem。如果在某个hierarchy 放多个 subsystem，能建立的 hierarchy就更少了。</li><li>subsystem 和 cgroup 是关联的，不是和 hierarchy关联的，但经常看到有人说把某个 subsystem 和某个 hierarchy关联。实质上一般指的是 hierarchy 中的某一个 cgroup 或多个 cgroup关联。</li></ul><h5 id="cgroup-的-kernel-接口">1.3.4 cgroup 的 kernel 接口</h5><p>kernel 接口，就是在 Linux 上调用 api 来控制 cgroups。</p><ol type="1"><li><p>首先创建一个 hierarchy，而 hierarchy要挂载到一个目录上，这里创建一个目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mkdir hierarchy-test</code></pre></li><li><p>然后挂载：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t cgroup -o none,name&#x3D;hierarchy-test hierarchy-test .&#x2F;hierarchy-test</code></pre></li><li><p>可以在这个目录下看到一大堆文件，这些文件就是 cgroup根节点的配置。</p></li><li><p>然后在这个目录下创建新的空目录，会发现，新的目录里也会有很多cgroup 配置文件，这些目录已成为 cgroup 根节点的子节点 cgroup。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">.├── cgroup.clone_children├── cgroup.procs├── cgroup.sane_behavior├── notify_on_release├── release_agent├── tasks└── temp  # 这是新创建的文件夹    ├── cgroup.clone_children    ├── cgroup.procs    ├── notify_on_release    └── tasks</code></pre></li><li><p>在 cgroup中添加和移动进程：系统的所有进程都会被放到根节点中，可以根据需要移动进程：</p><ul><li><p>只需将进程 ID 写到对应的 cgroup 的 tasks 文件即可。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo sh -c &quot;echo $$ &gt;&gt; tasks&quot;</code></pre><p>该命令就是将当前终端的这个进程加到当前所在的 cgroup 的目录的 tasks文件中。</p></li></ul></li><li><p>通过 subsystem 限制 cgroup 中进程的资源：</p><ul><li>上面的方法有个问题，因为这个 hierarchy 没有关联到任何subsystem，因此不能够控制资源。</li><li>不过其实系统会自动给每个 subsystem 创建一个hierarchy，所以通过控制这个 hierarchy里的配置，可以达到控制进程的目的。</li></ul></li></ol><h5 id="docker-是怎么使用-cgroups-的">1.3.5 docker 是怎么使用 Cgroups的</h5><p>docker 会给每个容器创建一个 cgroup，再限制该 cgroup的资源，从而达到限制容器的资源的作用。</p><p>其实写了这么多，综合上面的前置知识，不难猜测，docker的原理是：隔离主机。</p><h4 id="demo">1.4 Demo</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;fmt&quot;&quot;io&#x2F;ioutil&quot;&quot;os&quot;&quot;os&#x2F;exec&quot;&quot;path&quot;&quot;strconv&quot;&quot;syscall&quot;)const cgroupMemoryHierarchyCount &#x3D; &quot;&#x2F;sys&#x2F;fs&#x2F;cgroup&#x2F;memory&quot;func main() &#123;    &#x2F;&#x2F; 第二次会运行这段代码    &#x2F;&#x2F; 这段代码运行的地方就可以看做是一个简易的容器    &#x2F;&#x2F; 这里只是对进程进行了隔离    &#x2F;&#x2F; 但是可以看到 pid 已经变成了 1，因为我们有 PID Namespace    if os.Args[0] &#x3D;&#x3D; &quot;&#x2F;proc&#x2F;self&#x2F;exe&quot; &#123;        fmt.Printf(&quot;current pid %d\n&quot;, syscall.Getpid())        cmd :&#x3D; exec.Command(&quot;sh&quot;, &quot;-c&quot;, &#96;stress --vm-bytes 200m --vm-keep -m 1&#96;)        cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;&#125;        cmd.Stdin &#x3D; os.Stdin        cmd.Stdout &#x3D; os.Stdout        cmd.Stderr &#x3D; os.Stderr        if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;            fmt.Println(err)            os.Exit(1)        &#125;    &#125;        &#x2F;&#x2F; 第一次运行这段    &#x2F;&#x2F; **command 设置为当前进程，也就是这个 go 程序本身，也就是说 cmd.Start() 会再次运行该程序    cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;)    &#x2F;&#x2F; 在 start 之前，修改 cmd 的各种配置，也就是第二次运行这个程序的时候的配置&#x2F;&#x2F; 创建 namespace    cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr &#123;        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,    &#125;    cmd.Stdin &#x3D; os.Stdin    cmd.Stdout &#x3D; os.Stdout    cmd.Stderr &#x3D; os.Stderr        &#x2F;&#x2F; 因为之后要打印 process 的 id，所以用 start    &#x2F;&#x2F; 如果这里用 run 的话，那么 else 里的代码永远不会执行，因为 stress 永远不会结束    if err :&#x3D; cmd.Start(); err !&#x3D; nil &#123;        fmt.Println(&quot;Error&quot;, err)        os.Exit(1)    &#125; else &#123;        &#x2F;&#x2F; 打印 new process id        fmt.Printf(&quot;%v\n&quot;, cmd.Process.Pid)                &#x2F;&#x2F; 接下来三段对 cgroup 操作        &#x2F;&#x2F; the hierarchy has been already created by linux on the memory subsystem        &#x2F;&#x2F; create a sub cgroup           os.Mkdir(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,        ), 0755)                &#x2F;&#x2F; place container process in this cgroup        ioutil.WriteFile(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;tasks&quot;,        ), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)                &#x2F;&#x2F; restrict the stress process on this cgroup        ioutil.WriteFile(path.Join(        cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;memory.limit_int_bytes&quot;,        ), []byte(&quot;100m&quot;), 0644)                &#x2F;&#x2F; cmd.Start() 不会等待进程结束，所以需要手动等待        &#x2F;&#x2F; 如果不加的话，由于主进程结束了，子进程也会被强行结束        cmd.Process.Wait()    &#125;&#125;</code></pre><h4 id="ufs">1.5 UFS</h4><h5 id="ufs-概念">1.5.1 UFS 概念</h5><p>UFS，Union File System，联合文件系统。docker 在下载一个 image文件时，会看到一次下载很多个文件，这就是UFS。它是一种分层、轻量、高性能的文件系统。UFS 类似git，每次修改文件时，都是一次提交，并有记录，修改都反映在一个新的文件上，而不是修改旧文件。</p><p>UFS 允许多个不同目录挂载到同一个虚拟文件系统下，这就是为什么 image之间可以共享文件，以及继承镜像的原因。</p><h5 id="aufs">1.5.2 AUFS</h5><p>AUFS，Advanced Union File System，是 UFS 的一个改动版本。</p><p>笔者本身使用的是 WSL 做日常开发，WSL 内核不支持AUFS，后面会提到更换内核。</p><h5 id="docker-和-aufs">1.5.3 docker 和 AUFS</h5><p>docker 在早期使用 AUFS，直到现在也可以选择作为一种存储驱动类型。</p><h5 id="image-layer">1.5.4 image layer</h5><p>image 由多层 read-only layer 构成。</p><p>当启动一个 container 时，就会在 image 上再加一层 init layer，initlayer 也是 read-only 的，用于储存容器的环境配置。此外，docker还会创建一个 read-write 的 layer，用于执行所有的写操作。</p><p>当停止容器时，这个 read-write layer 依然保留，只有删除 container时才会被删除。</p><p>那么，怎么删除旧文件呢？</p><p>docker 会在 read-write layer 生成一个<code>.wh.&lt;fileName&gt;</code> 文件来隐藏要删除的文件。</p><h5 id="实现一个-aufs">1.5.5 实现一个 AUFS</h5><p>我们先创建一个如下的文件夹结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt</code></pre><p>然后挂载到 mnt 文件夹上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t aufs -o dirs&#x3D;.&#x2F;container-layer:.&#x2F;image-layer none .&#x2F;mnt</code></pre><p>如果没有手动添加权限的话，默认 dirs 左边第一个文件夹有 write-read权限，其他都是 read-only。</p><p>我们可以发现，imageLayer1 和 writeLayer 的文件出现在 mnt文件夹下：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt    ├── container.txt    └── image.txt</code></pre><p>然后我们修改一下 image.txt的内容，然后再看看整个目录，会发现，<code>container-layer</code>目录下多了一个 <code>image.txt</code>，然后我们看看<code>container-layer</code> 的 <code>image.txt</code>的内容，有添加前后的的文字。</p><p>也就是说，实际上，当修改某一个 layer 的时候，实际上不会改变这个layer，而是将其复制到 container-layer 中，然后再修改这个新的文件。</p><h2 id="二容器篇">二、容器篇</h2><h3 id="linux-的-proc-文件夹">2. Linux 的 /proc 文件夹</h3><h4 id="pid">2.1 PID</h4><p>在 <code>/proc</code>文件夹下可以看到很多文件夹的名字都是个数字，其实就是个 PID。是 Linux为每个进程创建的空间。</p><h4 id="一些重要的目录">2.2 一些重要的目录</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F;proc&#x2F;N # PID 为 N 的进程&#x2F;proc&#x2F;N&#x2F;cmdline # 进程的启动命令&#x2F;proc&#x2F;N&#x2F;cwd # 链接到进程的工作目录&#x2F;proc&#x2F;N&#x2F;environ  # 进程的环境变量列表&#x2F;proc&#x2F;N&#x2F;exe # 链接到进程的执行命令&#x2F;proc&#x2F;N&#x2F;fd # 包含进程相关的所有文件描述符&#x2F;proc&#x2F;N&#x2F;maps # 与进程相关的内存映射信息&#x2F;proc&#x2F;N&#x2F;mem # 进程持有的内存，不可读&#x2F;proc&#x2F;N&#x2F;root # 链接到进程的根目录&#x2F;proc&#x2F;N&#x2F;stat # 进程的状态&#x2F;proc&#x2F;N&#x2F;statm # 进程的内存状态&#x2F;proc&#x2F;N&#x2F;status # 比上面两个更可读&#x2F;proc&#x2F;self # 链接到当前正在运行的进程</code></pre><h3 id="简单实现">3. 简单实现</h3><h4 id="工具">3.1 工具</h4><p>获取帮助编写 command line app 的工具：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go get github.com&#x2F;urfave&#x2F;cli </code></pre><h4 id="实现代码">3.2 实现代码</h4><p>代码结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── command.go├── container│   └── init.go├── dockerCommand│   └── run.go├── go.mod├── go.sum└── main.go</code></pre><h5 id="runcommand">3.2.1 runCommand</h5><p><code>command.go</code> 用于放置各种 command 命令，这里先只写一个runCommand 命令。</p><p>首先用 urfave/cli 创建一个 runCommand 命令：</p><pre class="line-numbers language-golang" data-language="golang"><code class="language-golang">&#x2F;&#x2F; command.govar runCommand &#x3D; cli.Command&#123;    Name:  &quot;run&quot;,    Usage: &quot;Create a container&quot;,    Flags: []cli.Flag&#123;        &#x2F;&#x2F; integrate -i and -t for convenience        &amp;cli.BoolFlag&#123;            Name:  &quot;it&quot;,            Usage: &quot;open an interactive tty(pseudo terminal)&quot;,        &#125;,    &#125;,    Action: func(context *cli.Context) error &#123;        args :&#x3D; context.Args()        if len(args) &#x3D;&#x3D; 0 &#123;            return errors.New(&quot;Run what?&quot;)        &#125;        cmdArray :&#x3D; args.Get(0)        &#x2F;&#x2F; command        &#x2F;&#x2F; check whether type &#96;-it&#96;        tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal                &#x2F;&#x2F; 这个函数在下面定义        dockerCommand.Run(tty, cmdArray)        return nil    &#125;,&#125;</code></pre><h5 id="run">3.2.2 run</h5><p>上面的 Run 函数在 <code>dockerCommand/run.go</code> 下定义。当运行<code>docker run</code> 时，实际上主要是 Action 下的这个函数在工作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; dockerCommand&#x2F;run.go&#x2F;&#x2F; This is the function what &#96;docker run&#96; will callfunc Run(tty bool, cmdArray string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess :&#x3D; container.NewProcess(tty, cmdArray)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil&#123;logrus.Error(err)&#125;initProcess.Wait()os.Exit(-1)&#125;</code></pre><p>但其实这个函数做的也只是去跑一个 initProcess。这个 command process在另一个包里定义。</p><h5 id="newprocess">3.2.3 NewProcess</h5><p>上面提到的 <code>container.NewProcess</code> 在<code>container/init.go</code> 里定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; container&#x2F;init.gofunc NewProcess(tty bool, cmdArray string) *exec.Cmd &#123;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is the below exported function&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;args :&#x3D; []string&#123;&quot;init&quot;, cmdArray&#125;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, args...)&#x2F;&#x2F; new namespaces, thanks to Linuxcmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; this is what presudo terminal means&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;return cmd&#125;</code></pre><p>这个函数的作用是生成一个新的 command process，但这个 command 是<code>/proc/self/exe</code>这个程序本身，也就是，我们最后生成的可执行文件，但这次我们不运行<code>docker run</code>，而是 <code>docker init</code>，这个 init命令在下面定义。</p><h5 id="init">3.2.4 init</h5><p>initCommand 和 runCommand 在同一个文件里定义，也是一个command，但是注意这个 command 不面向用户，只用于协助 runCommand。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; command.go&#x2F;&#x2F; docker init, but cannot be used by uservar initCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;Start initiating...&quot;)cmdArray :&#x3D; context.Args().Get(0)logrus.Infof(&quot;container command: %v&quot;, cmdArray)return container.InitProcess(cmdArray, nil)&#125;,&#125;</code></pre><p>这里使用了 container.InitProcess函数，这个函数是真正用于容器初始化的函数。</p><h5 id="initprocess">3.2.5 InitProcess</h5><p>这里的是 InitProcess，也就是容器初始化的步骤。</p><p>注意 syscall.Exec 这里：</p><ul><li>就是 <code>mount /</code> 并指定 private，不然容器里的 proc会使用外面的 proc，即使在不同 namespace 下。</li><li>所以如果没有加这一段，其实退出容器后还需要在外面再次 mount proc才能使用 ps 等命令</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initiate the containerfunc InitProcess(cmdArray string, args []string) error &#123;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV                &#x2F;&#x2F; mountif err :&#x3D; syscall.Mount(&quot;&quot;, &quot;&#x2F;&quot;, &quot;&quot;, syscall.MS_PRIVATE|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F; fails: %v&quot;, err)return err&#125;        &#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)argv :&#x3D; []string&#123;cmdArray&#125;if err :&#x3D; syscall.Exec(cmdArray, argv, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc fails: %v&quot;, err)&#125;return nil&#125;</code></pre><p>一般来说，我们都是想要这个 cmdArray 作为 PID=1 的进程。but，我们有initProcess 本身的存在，所以 PID = 1 的其实是 initProcess，那如何让cmdArray 作为 PID=1 的存在呢？</p><p>这里有一个 syscall.Exec 神器，Exec 内部会调用 kernel 的 execve函数，这个函数会把当前进程上运行的程序替换为另一个程序，这正是我们想要的，在不改变PID 的情况下，替换程序 (即使 kill PID 为 1 的进程，新创建的进程也会是PID=2)。</p><p>为什么要第一个命令的 PID 为 1？</p><ul><li>因为这样，退出这个进程后，容器就会因为没有前台进程，而自动退出，这也是docker 的特性。</li></ul><h3 id="给-docker-run-增加对容器的资源限制功能">4. 给 docker run增加对容器的资源限制功能</h3><p>这里要用到 subsystem 的知识。</p><h4 id="subsystem.go">4.1 subsystem.go</h4><ul><li>根据 subsystem 的特性，和接口很搭。</li><li>此外再定义一个 ResourceConfig 的类型，用于放置资源控制的配置。</li><li>subsystemInstance 里包括 3 个 subsystem，分别对memory，cpu，cpushare进行限制。因为我们只需要对整个容器进行限制，所以这一套 3 个够了。</li></ul><p>看到这里，有个 cpu，cpushare，cpuset 等等，有点晕，查了下，有关 CPU的 cgroup subsystem，这里列举常见的 3 个：</p><ul><li>cpu：经常看到的 cpushares 在其麾下，share 即相对权重的 cpu调度，用来限制 cgroup 的 cpu 的使用率</li><li>cpuacct：统计 cgroup 的 cpu 使用率</li><li>cpuset：在多核机器上设置 cgroups 可使用的 cpu 核心数和内存</li></ul><p>通常前两者可以合体</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package subsystemstype ResourceConfig struct &#123;MemoryLimit stringCPUShare stringCPUSet string&#125;type Subsystem interface &#123;&#x2F;&#x2F; return the name of which type of subsystemName() string&#x2F;&#x2F; set a resource limit on a cgroupSet(cgroupPath string, res *ResourceConfig) error&#x2F;&#x2F; add a processs with the pid to a groupAddProcess(cgroupPath string, pid int) error&#x2F;&#x2F; remove a cgroupRemoveCgroup(cgroupPath string) error&#125;&#x2F;&#x2F; instance of a subsystemsvar SubsystemsInstance &#x3D; []Subsystem&#123;&amp;CPU&#123;&#125;,&amp;CPUSet&#123;&#125;,&amp;Memory&#123;&#125;,&#125;</code></pre><h4 id="memorysubsystem">4.2 MemorySubsystem</h4><h5 id="name">4.2.1 Name()</h5><p>很简单，返回 “memory” 字符串，表示这个 subsystem 是memorySubsystem。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *MemorySubsystem) Name() string &#123;    return &quot;memory&quot;&#125;</code></pre><h5 id="set">4.2.2 Set()</h5><p>Set() 用于对 cgroup 设置资源限制，因此参数为 cgroup 的 path 和resourceConfig。</p><ol type="1"><li>其中 <code>GetCgroupPath</code> 后面会提及，作用是获取这个 subsystem所挂载的 hierarchy 上的虚拟文件系统下的 从group 路径。</li><li>获取到 cgroupPath 在虚拟文件系统中的位置后，只需要写入"memory.limit_in_bytes" 文件中即可。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; set the memory limit to this cgroup with cgroupPathfunc (ms *Memory) Set(cgroupPath string, res *ResourceConfig) error  &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;memory.limit_in_bytes&quot;), []byte(res.MemoryLimit), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup memory fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="addprocess">4.2.3 AddProcess()</h5><ol type="1"><li>和上面基本一样，只不过是写到 tasks 里。</li><li>pid 变成 byte slice 之前要用 Itoa 转化一下。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add process fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="removecgroup">4.2.4 RemoveCgroup()</h5><ol type="1"><li>使用 <code>os.Remove</code> 可以移除参数所指定的文件或文件夹。</li><li>这里移除整个 cgroup 文件夹，就等于是删除 cgroup 了。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusubsystem">4.3 CPUSubsystem</h4><p>这里的设计和上面没什么区别，直接贴参考代码</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpu.gofunc (c *CPU) Name() string &#123;return &quot;CPUShare&quot;&#125;func (c *CPU) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpu.shares&quot;), []byte(res.CPUShare), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cpu share limit failed: %s&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpu process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusetsubsystem">4.4 CPUSetSubsystem</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpuset.gofunc (c *CPUSet) Name() string &#123;return &quot;CPUSet&quot;&#125;func (c *CPUSet) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpuset.cpus&quot;), []byte(res.CPUSet), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup cpuset failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpuset process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(path.Join(subsystemCgroupPath))&#125;&#125;</code></pre><h4 id="getcgrouppath">4.5 GetCgroupPath()</h4><p><code>GetCgroupPath()</code> 用于获取某个 subsystem 所挂载的hierarchy 上的虚拟文件系统 (挂载后的文件夹) 下的 cgroup的路径。通过对这个目录的改写来改动 cgroup。</p><p>首先我们抛开 cgroup，在此之前我们要知道 这个 hierarchy 的 cgroup根节点的路径。那可以在 <code>/proc/self/mountinfo</code> 中获取。</p><p>下面是一些实现细节：</p><ol type="1"><li>首先定义一个 <code>FindCgroupMountpoint()</code> 来找到 cgroup的根节点。</li><li>然后在 <code>GetCgroupPath</code> 将其和 cgroup的相对路径拼接从而获取 cgroup 的路径。如果 <code>autoCreate</code> 为true 且该路径不存在，那么就新建一个 cgroup。(在 hierarchy 环境下，mkdir其实会隐式地创建一个 cgroup，其中包括很多配置文件)</li></ol><blockquote><p><a href="#1.3.4 cgroup 的 kernel 接口">点击这里回顾</a></p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; as the function name shows, find the root path of hierarchyfunc FindCgroupMountpoint(subsystemName string) string  &#123;f, err :&#x3D; os.Open(&quot;&#x2F;proc&#x2F;self&#x2F;mountinfo&quot;)    &#x2F;&#x2F; get info about mount relate to current processif err !&#x3D; nil &#123;return &quot;&quot;&#125;defer f.Close()scanner :&#x3D; bufio.NewScanner(f)for scanner.Scan() &#123;txt :&#x3D; scanner.Text()fields :&#x3D; strings.Split(txt, &quot; &quot;)&#x2F;&#x2F; find whether &quot;subsystemName&quot; appear in the last field&#x2F;&#x2F; if so, then the fifth field is the pathfor _, opt :&#x3D; range strings.Split(fields[len(fields)-1], &quot;,&quot;) &#123;if opt &#x3D;&#x3D; subsystemName &#123;return fields[4]&#125;&#125;&#125;return &quot;&quot;&#125;&#x2F;&#x2F; get the absolute path of a cgroupfunc GetCgroupPath(subsystemName string, cgroupPath string, autoCreate bool) (string, error)  &#123;cgroupRootPath :&#x3D; FindCgroupMountpoint(subsystemName)expectedPath :&#x3D; path.Join(cgroupRootPath, cgroupPath)&#x2F;&#x2F; find the cgroup or create a new cgroupif _, err :&#x3D; os.Stat(expectedPath); err &#x3D;&#x3D; nil  || (autoCreate &amp;&amp; os.IsNotExist(err)) &#123;if os.IsNotExist(err) &#123;if err :&#x3D; os.Mkdir(expectedPath, 0755); err !&#x3D; nil &#123;return &quot;&quot;, fmt.Errorf(&quot;error when create cgroup: %v&quot;, err)&#125;&#125;return expectedPath, nil&#125; else &#123;return &quot;&quot;, fmt.Errorf(&quot;cgroup path error: %v&quot;, err)&#125;&#125;</code></pre><h4 id="cgroupsmanager.go">4.6 cgroupsManager.go</h4><ol type="1"><li>定义 CgroupManager 类型，其中的 path 要注意是相对路径，相对于hierarchy 的 root path。所以一个 CgroupManager 是有可能表示多个 cgroups的，或准确说，和对应的 hierarchy root path 的相对路径一样的多个cgroups。</li><li>因为上述原因，<code>Set()</code> 可能会创建多个 cgroups，如果subsystems 们在不同的 hierarchy 就会这样。</li><li>这也是为什么 <code>AddProcess()</code> 和 <code>Remove()</code>要在每个 subsystem 上执行一遍。因为这些 subsystem 可能存在于不同的hierarchies。</li><li>注意 <code>Set()</code> 和 <code>AddProcess()</code>都不是返回错误，而是发出警告，然后返回nil。因为有些时候用户只指定某一个限制，例如 memory，那样的话修改 cpu等其实会报错 (正常的报错)，因此我们不 return err 来退出。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">package cgroupsimport &quot;simple-docker&#x2F;subsystem&quot;type CgroupManager struct &#123;Path     string &#x2F;&#x2F; relative path, relative to the root path of the hierarchy&#x2F;&#x2F; so this may cause more than one cgroup in different hierarchiesResource *subsystems.ResourceConfig&#125;func NewCgroupManager(path string) *CgroupManager &#123;return &amp;CgroupManager&#123;Path: path,&#125;&#125;&#x2F;&#x2F; set the three resource config subsystems to the cgroup(will create if the cgroup path is not existed)&#x2F;&#x2F; this may generate more than one cgroup, because those subsystem may appear in different hierarchiesfunc (cm CgroupManager) Set(res *subsystems.ResourceConfig) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.Set(cm.Path, res); err !&#x3D; nil &#123;logrus.Warnf(&quot;set resource fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; add process to the cgroup path&#x2F;&#x2F; why should we iterate all the subsystems? we have only one cgroup&#x2F;&#x2F; because those subsystems may appear at different hierarchies, which will then cause more than one cgroup, 1-3 in this case.func (cm *CgroupManager) AddProcess(pid int) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.AddProcess(cm.Path, pid); err !&#x3D; nil &#123;logrus.Warn(&quot;app process fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; delete the cgroup(s)func (cm *CgroupManager) Remove() error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err:&#x3D; subsystem.RemoveCgroup(cm.Path); err !&#x3D; nil &#123;return err&#125;&#125;return nil&#125;</code></pre><h4 id="管道处理多个容器参数">4.7 管道处理多个容器参数</h4><p>限制容器运行的命令不再像是 <code>/bin/sh</code>这种单个参数，而是多个参数，因此需要使用管道来对多个参数进行处理。那么需要修改以下文件：</p><h5 id="containerinit.go">4.7.1 container/init.go</h5><ol type="1"><li>管道原理和 channel 很像，read 端和 write端会在另一边没响应时堵塞。</li><li>使用 <code>os.Pipe()</code> 获取管道。返回的 readPipe 和 writePipe都是 <code>*os.File</code> 类型。</li><li>如何把管道传给子进程 (也就是容器进程) 变成了一个难题，这里用到了<code>ExtraFile</code> 这个参数来解决。cmd会带着参数里的文件来创建新的进程。(这里除了 ExtraFile，还会有类似StandardFile，也就是 stdin，stdout，stderr)</li><li>这里把 read 端传给容器进程，然后 write 端保留在父进程上。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;new pipe error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itselfcmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)&#x2F;&#x2F; new namespacescmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;return cmd, writePipe&#125;</code></pre><p>除了 <code>NewProcess()</code>，<code>InitProcess()</code>也要改变下。</p><ol type="1"><li>使用 readCommand 来读取 pipe。</li><li>实际运行中，当进程运行到 <code>readCommand()</code> 时会堵塞，直到write 端传数据进来。</li><li>因此不用担心我们在容器运行后再传输参数。因为再读取完参数之前，<code>InitProcess()</code>也不会运行到 <code>syscall.Exec()</code> 这一步。</li><li>这里添加了 lookPath，这个是用于解决每次我们都要输入<code>/bin/ls</code>的麻烦，这个函数会帮我们找到参数命令的绝对路径。也就是说，只要输入 ls即可，lookPath 会自动找到 <code>/bin/ls</code>。然后我们再把这个 path作为 <code>argv()</code> 传给 <code>syscall.Exec</code></li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initialize the containerfunc InitProcess() error &#123;cmdArray :&#x3D; readCommand()if len(cmdArray) &#x3D;&#x3D; 0 &#123;return fmt.Errorf(&quot;init process fails, cmdArray is nil&quot;)&#125;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV&#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)path, err :&#x3D; exec.LookPath(cmdArray[0])if err !&#x3D; nil &#123;logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)return err&#125;&#x2F;&#x2F; log path infologrus.Infof(&quot;find path: %v&quot;, path)if err :&#x3D; syscall.Exec(path, cmdArray, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(err.Error())&#125;return nil&#125;func readCommand() []string &#123;pipe :&#x3D; os.NewFile(uintptr(3), &quot;pipe&quot;)msg, err :&#x3D; ioutil.ReadAll(pipe)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read pipe failed: %v&quot;, err)return nil&#125;return strings.Split(string(msg), &quot; &quot;)&#125;</code></pre><h5 id="dockercommandrun.go">4.7.2 dockerCommand/run.go</h5><ol type="1"><li>在 run.go 向 writePipe 写入参数，这样容器就会获取到参数。</li><li>关闭 pipe，使得 init 进程继续进行。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig) &#123;initProcess, writePipe :&#x3D; container.NewProcess(tty)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write sidesendInitCommand(cmdArray, writePipe)initProcess.Wait()os.Exit(-1)&#125;func sendInitCommand(cmdArray []string, writePipe *os.File) &#123;cmdString :&#x3D; strings.Join(cmdArray, &quot; &quot;)logrus.Infof(&quot;whole init command is: %v&quot;, cmdString)writePipe.WriteString(cmdString)writePipe.Close()&#125;</code></pre><h5 id="command.go">4.7.3 command.go</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open a interactive tty(pre sudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpushare&quot;,Usage:&quot;limit the cpu share&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &#x3D;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;cmdArray :&#x3D; make([]string,len(args)) &#x2F;&#x2F; commandcopy(cmdArray,args)&#x2F;&#x2F; checkout whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; pre sudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig &#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare: context.String(&quot;cpushare&quot;),CPUSet: context.String(&quot;cpu&quot;),&#125;dockerCommand.Run(tty, cmdArray, &amp;resourceConfig)return nil&#125;,&#125;&#x2F;&#x2F; docker init, but cannot be used by uservar InitCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;start initializing...&quot;)return container.InitProcess()&#125;,&#125;</code></pre><h5 id="main.go">4.7.4 main.go</h5><p>除了上面的修改，我们还要定义一个程序的入口：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;&quot;github.com&#x2F;urfave&#x2F;cli&quot;)const usage &#x3D; &#96;Usage&#96;func main() &#123;app :&#x3D; cli.NewApp()app.Name &#x3D; &quot;simple-docker&quot;app.Usage &#x3D; usageapp.Commands &#x3D; []cli.Command&#123;RunCommand,InitCommand,&#125;app.Before &#x3D; func(context *cli.Context) error &#123;logrus.SetFormatter(&amp;logrus.JSONFormatter&#123;&#125;)logrus.SetOutput(os.Stdout)return nil&#125;if err :&#x3D; app.Run(os.Args); err !&#x3D; nil &#123;logrus.Fatal(err)&#125;&#125;</code></pre><h4 id="运行-demo">4.8 运行 demo</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it stress -m 100m --vm-bytes 200m --vm-keep -m 1</code></pre><p>效果如下：</p><h2 id="零前言-1">零、前言</h2><p>本文为《自己动手写 Docker》的学习，对于各位学习 docker的同学非常友好，非常建议买一本来学习。</p><p>书中有摘录书中的一些知识点，不过限于篇幅，没有全部摘录<del>(主要也是懒)</del>。项目仓库地址为：<ahref="https://github.com/JaydenChang/simple-docker">JaydenChang/simple-docker(github.com)</a></p><h2 id="一概念篇-1">一、概念篇</h2><h3 id="基础知识-1">1. 基础知识</h3><h4 id="kernel-1">1.1 kernel</h4><p>kernel (内核)指大多数操作系统的核心部分，由操作系统中用于管理存储器、文件、外设和系统资源的部分组成。操作系统内核通常运行进程，并提供进程间通信。</p><h4 id="namespace-1">1.2 namespace</h4><p>namespace 是 Linux 自带的功能来隔离内核资源的机制。</p><p>Linux 中有 6 种 namespace</p><h5 id="uts-namespace-1">1.2.1 UTS Namespace</h5><p>UTS，UNIX Time Sharing，用于隔离 nodeName (主机名) 和 domainName(域名)。在该 Namespace 下修改 hostname 不会影相其他的 Namespace。</p><h5 id="ipc-namespace-1">1.2.2 IPC Namespace</h5><p>IPC，Inter-Process Communication (进程间通讯)，用于隔离 System V IPC和 POSIX message queues (一种消息队列，结构为链表)。</p><p>两种 IPC 本质上差不多，System V IPC 随内核持续，POSIX IPC随进程持续。</p><h5 id="pid-namespace-1">1.2.3 PID Namespace</h5><p>PID，Process IDs，用于隔绝 PID。同样的进程，在不同 Namespace里是不同的 PID。新建的 PID Namespace 里第一个 PID 是1。</p><h5 id="mount-namespace-1">1.2.4 Mount Namespace</h5><p>用于隔绝文件系统，挂载了某一目录，在这个 Namespace下就会把这个目录当作根目录，我们看到的文件系统树就会以这个目录为根目录。</p><p>mount 操作本身不会影响到外部，docker 中的 volume也用到了这个特性。</p><h5 id="user-namespace-1">1.2.5 User Namespace</h5><p>用于 隔离用户组 ID。</p><h5 id="network-namespace-1">1.2.6 Network Namespace</h5><p>每个 Namespace 都有一套自己的网络设备，可以使用相同的端口号，映射到host 的不同端口。</p><h4 id="linux-cgroups-1">1.3 Linux Cgroups</h4><p>Cgroups 全称为 Control Groups，是 Linux内核提供的物理资源隔离机制。</p><h5 id="cgroups-的三个组件-1">1.3.1 Cgroups 的三个组件</h5><ul><li>cgroup：一个 cgroup 包含一组进程，且可以有 subsystem的参数配置，以关联一组 subsystem。</li><li>subsystem：一组资源控制的模块。</li><li>hierarchy：把一组 cgroups 串成一个树状结构，以提供继承的功能。</li></ul><h5 id="这三个组件的关联-1">1.3.2 这三个组件的关联</h5><p>Linux 有一些限制：</p><ul><li>首先，创建一个 hierarchy。这个 hierarchy 有一个 cgroup根节点，所有的进程都会被加到这个根节点上，所有在这个 hierarchy上创建的节点都是这个根节点的子节点。</li><li>一个 subsystem 只能加到一个 hierarchy 上。</li><li>但是一个 subsystem 可以加到同一个 hierarchy 的多个 cgroups 上。</li><li>一个 hierarchy 可以有多个 subsystem。</li><li>一个进程可以在多个 cgroups 中，但是这些 cgroup 必须在不同的hierarchy 中。</li><li>一个进程 fork 出子进程时，父进程和子进程属于同一个 cgroup。</li></ul><h5 id="cgroup-和-subsystem-和-hierarchy-之间的联系-1">1.3.3 cgroup 和subsystem 和 hierarchy 之间的联系</h5><ul><li>hierarchy 就是一颗 cgroups 树，由多个 cgroups 构成。每一个 hierarchy建立时会包含 ==<em>所有</em>== 的Linux 进程。这里的 “所有”就是当前系统运行中的所有进程，每个 hierarchy上的全部进程都是一样的，不同的 hierarchy指的其实只是不同的分组方式，这也是为什么一个进程可以存在于多个 hierarchy上；准确来说，一个进程一定会同时存在于所有的 hierarchy上，区别在被放在的 cgroup 可能会有差异。</li><li>Linux 的 subsystem 只有一个的说法，没有一种的说法，也就是在一个hierarchy 上使用了 memory subsystem，那么在其他 hierarchy 就不能使用memory subsystem 了。</li><li>subsystem 是一种资源控制器，有很多个 subsystem，每个 subsystem控制不同的资源。subsystem 和 cgroups 关联。新建一个 cgroups文件夹时，里面会自动生成一堆配置文件，那个就是 subsystem 配置文件。但<code>subsystem 配置文件</code> 不是 <code>subsystem</code>，就像<code>.git</code> 不是 <code>git</code> 一样，就像没安装 git也可以从别人那里获得 <code>.git</code>文件夹，只是不能用罢了。<code>subsystem 配置文件</code>也是如此，新建一个 cgroup 就会生成<code>cgroup 配置文件</code>，但并不代表你关联了一个subsystem。只有当改变了一个<code>cgroup 配置文件</code>，里面要限制某种资源时，就会自动关联到这个被限制的资源所对应的subsystem 上。</li><li>假设我的 Linux 有 12 个 subsystem，也就是说我最多只能建 12 个hierarchy (不加 subsystem 的情况下可以建更多 hierarchy，这样 cgroup就变成纯分组使用)。每一个 hierarchy 上一个 subsystem。如果在某个hierarchy 放多个 subsystem，能建立的 hierarchy就更少了。</li><li>subsystem 和 cgroup 是关联的，不是和 hierarchy关联的，但经常看到有人说把某个 subsystem 和某个 hierarchy关联。实质上一般指的是 hierarchy 中的某一个 cgroup 或多个 cgroup关联。</li></ul><h5 id="cgroup-的-kernel-接口-1">1.3.4 cgroup 的 kernel 接口</h5><p>kernel 接口，就是在 Linux 上调用 api 来控制 cgroups。</p><ol type="1"><li><p>首先创建一个 hierarchy，而 hierarchy要挂载到一个目录上，这里创建一个目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mkdir hierarchy-test</code></pre></li><li><p>然后挂载：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t cgroup -o none,name&#x3D;hierarchy-test hierarchy-test .&#x2F;hierarchy-test</code></pre></li><li><p>可以在这个目录下看到一大堆文件，这些文件就是 cgroup根节点的配置。</p></li><li><p>然后在这个目录下创建新的空目录，会发现，新的目录里也会有很多cgroup 配置文件，这些目录已成为 cgroup 根节点的子节点 cgroup。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">.├── cgroup.clone_children├── cgroup.procs├── cgroup.sane_behavior├── notify_on_release├── release_agent├── tasks└── temp  # 这是新创建的文件夹    ├── cgroup.clone_children    ├── cgroup.procs    ├── notify_on_release    └── tasks</code></pre></li><li><p>在 cgroup中添加和移动进程：系统的所有进程都会被放到根节点中，可以根据需要移动进程：</p><ul><li><p>只需将进程 ID 写到对应的 cgroup 的 tasks 文件即可。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo sh -c &quot;echo $$ &gt;&gt; tasks&quot;</code></pre><p>该命令就是将当前终端的这个进程加到当前所在的 cgroup 的目录的 tasks文件中。</p></li></ul></li><li><p>通过 subsystem 限制 cgroup 中进程的资源：</p><ul><li>上面的方法有个问题，因为这个 hierarchy 没有关联到任何subsystem，因此不能够控制资源。</li><li>不过其实系统会自动给每个 subsystem 创建一个hierarchy，所以通过控制这个 hierarchy里的配置，可以达到控制进程的目的。</li></ul></li></ol><h5 id="docker-是怎么使用-cgroups-的-1">1.3.5 docker 是怎么使用 Cgroups的</h5><p>docker 会给每个容器创建一个 cgroup，再限制该 cgroup的资源，从而达到限制容器的资源的作用。</p><p>其实写了这么多，综合上面的前置知识，不难猜测，docker的原理是：隔离主机。</p><h4 id="demo-1">1.4 Demo</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;fmt&quot;&quot;io&#x2F;ioutil&quot;&quot;os&quot;&quot;os&#x2F;exec&quot;&quot;path&quot;&quot;strconv&quot;&quot;syscall&quot;)const cgroupMemoryHierarchyCount &#x3D; &quot;&#x2F;sys&#x2F;fs&#x2F;cgroup&#x2F;memory&quot;func main() &#123;    &#x2F;&#x2F; 第二次会运行这段代码    &#x2F;&#x2F; 这段代码运行的地方就可以看做是一个简易的容器    &#x2F;&#x2F; 这里只是对进程进行了隔离    &#x2F;&#x2F; 但是可以看到 pid 已经变成了 1，因为我们有 PID Namespace    if os.Args[0] &#x3D;&#x3D; &quot;&#x2F;proc&#x2F;self&#x2F;exe&quot; &#123;        fmt.Printf(&quot;current pid %d\n&quot;, syscall.Getpid())        cmd :&#x3D; exec.Command(&quot;sh&quot;, &quot;-c&quot;, &#96;stress --vm-bytes 200m --vm-keep -m 1&#96;)        cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;&#125;        cmd.Stdin &#x3D; os.Stdin        cmd.Stdout &#x3D; os.Stdout        cmd.Stderr &#x3D; os.Stderr        if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;            fmt.Println(err)            os.Exit(1)        &#125;    &#125;        &#x2F;&#x2F; 第一次运行这段    &#x2F;&#x2F; **command 设置为当前进程，也就是这个 go 程序本身，也就是说 cmd.Start() 会再次运行该程序    cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;)    &#x2F;&#x2F; 在 start 之前，修改 cmd 的各种配置，也就是第二次运行这个程序的时候的配置&#x2F;&#x2F; 创建 namespace    cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr &#123;        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,    &#125;    cmd.Stdin &#x3D; os.Stdin    cmd.Stdout &#x3D; os.Stdout    cmd.Stderr &#x3D; os.Stderr        &#x2F;&#x2F; 因为之后要打印 process 的 id，所以用 start    &#x2F;&#x2F; 如果这里用 run 的话，那么 else 里的代码永远不会执行，因为 stress 永远不会结束    if err :&#x3D; cmd.Start(); err !&#x3D; nil &#123;        fmt.Println(&quot;Error&quot;, err)        os.Exit(1)    &#125; else &#123;        &#x2F;&#x2F; 打印 new process id        fmt.Printf(&quot;%v\n&quot;, cmd.Process.Pid)                &#x2F;&#x2F; 接下来三段对 cgroup 操作        &#x2F;&#x2F; the hierarchy has been already created by linux on the memory subsystem        &#x2F;&#x2F; create a sub cgroup           os.Mkdir(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,        ), 0755)                &#x2F;&#x2F; place container process in this cgroup        ioutil.WriteFile(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;tasks&quot;,        ), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)                &#x2F;&#x2F; restrict the stress process on this cgroup        ioutil.WriteFile(path.Join(        cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;memory.limit_int_bytes&quot;,        ), []byte(&quot;100m&quot;), 0644)                &#x2F;&#x2F; cmd.Start() 不会等待进程结束，所以需要手动等待        &#x2F;&#x2F; 如果不加的话，由于主进程结束了，子进程也会被强行结束        cmd.Process.Wait()    &#125;&#125;</code></pre><h4 id="ufs-1">1.5 UFS</h4><h5 id="ufs-概念-1">1.5.1 UFS 概念</h5><p>UFS，Union File System，联合文件系统。docker 在下载一个 image文件时，会看到一次下载很多个文件，这就是UFS。它是一种分层、轻量、高性能的文件系统。UFS 类似git，每次修改文件时，都是一次提交，并有记录，修改都反映在一个新的文件上，而不是修改旧文件。</p><p>UFS 允许多个不同目录挂载到同一个虚拟文件系统下，这就是为什么 image之间可以共享文件，以及继承镜像的原因。</p><h5 id="aufs-1">1.5.2 AUFS</h5><p>AUFS，Advanced Union File System，是 UFS 的一个改动版本。</p><p>笔者本身使用的是 WSL 做日常开发，WSL 内核不支持AUFS，后面会提到更换内核。</p><h5 id="docker-和-aufs-1">1.5.3 docker 和 AUFS</h5><p>docker 在早期使用 AUFS，直到现在也可以选择作为一种存储驱动类型。</p><h5 id="image-layer-1">1.5.4 image layer</h5><p>image 由多层 read-only layer 构成。</p><p>当启动一个 container 时，就会在 image 上再加一层 init layer，initlayer 也是 read-only 的，用于储存容器的环境配置。此外，docker还会创建一个 read-write 的 layer，用于执行所有的写操作。</p><p>当停止容器时，这个 read-write layer 依然保留，只有删除 container时才会被删除。</p><p>那么，怎么删除旧文件呢？</p><p>docker 会在 read-write layer 生成一个<code>.wh.&lt;fileName&gt;</code> 文件来隐藏要删除的文件。</p><h5 id="实现一个-aufs-1">1.5.5 实现一个 AUFS</h5><p>我们先创建一个如下的文件夹结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt</code></pre><p>然后挂载到 mnt 文件夹上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t aufs -o dirs&#x3D;.&#x2F;container-layer:.&#x2F;image-layer none .&#x2F;mnt</code></pre><p>如果没有手动添加权限的话，默认 dirs 左边第一个文件夹有 write-read权限，其他都是 read-only。</p><p>我们可以发现，imageLayer1 和 writeLayer 的文件出现在 mnt文件夹下：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt    ├── container.txt    └── image.txt</code></pre><p>然后我们修改一下 image.txt的内容，然后再看看整个目录，会发现，<code>container-layer</code>目录下多了一个 <code>image.txt</code>，然后我们看看<code>container-layer</code> 的 <code>image.txt</code>的内容，有添加前后的的文字。</p><p>也就是说，实际上，当修改某一个 layer 的时候，实际上不会改变这个layer，而是将其复制到 container-layer 中，然后再修改这个新的文件。</p><h2 id="二容器篇-1">二、容器篇</h2><h3 id="linux-的-proc-文件夹-1">2. Linux 的 /proc 文件夹</h3><h4 id="pid-1">2.1 PID</h4><p>在 <code>/proc</code>文件夹下可以看到很多文件夹的名字都是个数字，其实就是个 PID。是 Linux为每个进程创建的空间。</p><h4 id="一些重要的目录-1">2.2 一些重要的目录</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F;proc&#x2F;N # PID 为 N 的进程&#x2F;proc&#x2F;N&#x2F;cmdline # 进程的启动命令&#x2F;proc&#x2F;N&#x2F;cwd # 链接到进程的工作目录&#x2F;proc&#x2F;N&#x2F;environ  # 进程的环境变量列表&#x2F;proc&#x2F;N&#x2F;exe # 链接到进程的执行命令&#x2F;proc&#x2F;N&#x2F;fd # 包含进程相关的所有文件描述符&#x2F;proc&#x2F;N&#x2F;maps # 与进程相关的内存映射信息&#x2F;proc&#x2F;N&#x2F;mem # 进程持有的内存，不可读&#x2F;proc&#x2F;N&#x2F;root # 链接到进程的根目录&#x2F;proc&#x2F;N&#x2F;stat # 进程的状态&#x2F;proc&#x2F;N&#x2F;statm # 进程的内存状态&#x2F;proc&#x2F;N&#x2F;status # 比上面两个更可读&#x2F;proc&#x2F;self # 链接到当前正在运行的进程</code></pre><h3 id="简单实现-1">3. 简单实现</h3><h4 id="工具-1">3.1 工具</h4><p>获取帮助编写 command line app 的工具：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go get github.com&#x2F;urfave&#x2F;cli </code></pre><h4 id="实现代码-1">3.2 实现代码</h4><p>代码结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── command.go├── container│   └── init.go├── dockerCommand│   └── run.go├── go.mod├── go.sum└── main.go</code></pre><h5 id="runcommand-1">3.2.1 runCommand</h5><p><code>command.go</code> 用于放置各种 command 命令，这里先只写一个runCommand 命令。</p><p>首先用 urfave/cli 创建一个 runCommand 命令：</p><pre class="line-numbers language-golang" data-language="golang"><code class="language-golang">&#x2F;&#x2F; command.govar runCommand &#x3D; cli.Command&#123;    Name:  &quot;run&quot;,    Usage: &quot;Create a container&quot;,    Flags: []cli.Flag&#123;        &#x2F;&#x2F; integrate -i and -t for convenience        &amp;cli.BoolFlag&#123;            Name:  &quot;it&quot;,            Usage: &quot;open an interactive tty(pseudo terminal)&quot;,        &#125;,    &#125;,    Action: func(context *cli.Context) error &#123;        args :&#x3D; context.Args()        if len(args) &#x3D;&#x3D; 0 &#123;            return errors.New(&quot;Run what?&quot;)        &#125;        cmdArray :&#x3D; args.Get(0)        &#x2F;&#x2F; command        &#x2F;&#x2F; check whether type &#96;-it&#96;        tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal                &#x2F;&#x2F; 这个函数在下面定义        dockerCommand.Run(tty, cmdArray)        return nil    &#125;,&#125;</code></pre><h5 id="run-1">3.2.2 run</h5><p>上面的 Run 函数在 <code>dockerCommand/run.go</code> 下定义。当运行<code>docker run</code> 时，实际上主要是 Action 下的这个函数在工作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; dockerCommand&#x2F;run.go&#x2F;&#x2F; This is the function what &#96;docker run&#96; will callfunc Run(tty bool, cmdArray string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess :&#x3D; container.NewProcess(tty, cmdArray)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil&#123;logrus.Error(err)&#125;initProcess.Wait()os.Exit(-1)&#125;</code></pre><p>但其实这个函数做的也只是去跑一个 initProcess。这个 command process在另一个包里定义。</p><h5 id="newprocess-1">3.2.3 NewProcess</h5><p>上面提到的 <code>container.NewProcess</code> 在<code>container/init.go</code> 里定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; container&#x2F;init.gofunc NewProcess(tty bool, cmdArray string) *exec.Cmd &#123;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is the below exported function&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;args :&#x3D; []string&#123;&quot;init&quot;, cmdArray&#125;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, args...)&#x2F;&#x2F; new namespaces, thanks to Linuxcmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; this is what presudo terminal means&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;return cmd&#125;</code></pre><p>这个函数的作用是生成一个新的 command process，但这个 command 是<code>/proc/self/exe</code>这个程序本身，也就是，我们最后生成的可执行文件，但这次我们不运行<code>docker run</code>，而是 <code>docker init</code>，这个 init命令在下面定义。</p><h5 id="init-1">3.2.4 init</h5><p>initCommand 和 runCommand 在同一个文件里定义，也是一个command，但是注意这个 command 不面向用户，只用于协助 runCommand。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; command.go&#x2F;&#x2F; docker init, but cannot be used by uservar initCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;Start initiating...&quot;)cmdArray :&#x3D; context.Args().Get(0)logrus.Infof(&quot;container command: %v&quot;, cmdArray)return container.InitProcess(cmdArray, nil)&#125;,&#125;</code></pre><p>这里使用了 container.InitProcess函数，这个函数是真正用于容器初始化的函数。</p><h5 id="initprocess-1">3.2.5 InitProcess</h5><p>这里的是 InitProcess，也就是容器初始化的步骤。</p><p>注意 syscall.Exec 这里：</p><ul><li>就是 <code>mount /</code> 并指定 private，不然容器里的 proc会使用外面的 proc，即使在不同 namespace 下。</li><li>所以如果没有加这一段，其实退出容器后还需要在外面再次 mount proc才能使用 ps 等命令</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initiate the containerfunc InitProcess(cmdArray string, args []string) error &#123;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV                &#x2F;&#x2F; mountif err :&#x3D; syscall.Mount(&quot;&quot;, &quot;&#x2F;&quot;, &quot;&quot;, syscall.MS_PRIVATE|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F; fails: %v&quot;, err)return err&#125;        &#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)argv :&#x3D; []string&#123;cmdArray&#125;if err :&#x3D; syscall.Exec(cmdArray, argv, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc fails: %v&quot;, err)&#125;return nil&#125;</code></pre><p>一般来说，我们都是想要这个 cmdArray 作为 PID=1 的进程。but，我们有initProcess 本身的存在，所以 PID = 1 的其实是 initProcess，那如何让cmdArray 作为 PID=1 的存在呢？</p><p>这里有一个 syscall.Exec 神器，Exec 内部会调用 kernel 的 execve函数，这个函数会把当前进程上运行的程序替换为另一个程序，这正是我们想要的，在不改变PID 的情况下，替换程序 (即使 kill PID 为 1 的进程，新创建的进程也会是PID=2)。</p><p>为什么要第一个命令的 PID 为 1？</p><ul><li>因为这样，退出这个进程后，容器就会因为没有前台进程，而自动退出，这也是docker 的特性。</li></ul><h3 id="给-docker-run-增加对容器的资源限制功能-1">4. 给 docker run增加对容器的资源限制功能</h3><p>这里要用到 subsystem 的知识。</p><h4 id="subsystem.go-1">4.1 subsystem.go</h4><ul><li>根据 subsystem 的特性，和接口很搭。</li><li>此外再定义一个 ResourceConfig 的类型，用于放置资源控制的配置。</li><li>subsystemInstance 里包括 3 个 subsystem，分别对memory，cpu，cpushare进行限制。因为我们只需要对整个容器进行限制，所以这一套 3 个够了。</li></ul><p>看到这里，有个 cpu，cpushare，cpuset 等等，有点晕，查了下，有关 CPU的 cgroup subsystem，这里列举常见的 3 个：</p><ul><li>cpu：经常看到的 cpushares 在其麾下，share 即相对权重的 cpu调度，用来限制 cgroup 的 cpu 的使用率</li><li>cpuacct：统计 cgroup 的 cpu 使用率</li><li>cpuset：在多核机器上设置 cgroups 可使用的 cpu 核心数和内存</li></ul><p>通常前两者可以合体</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package subsystemstype ResourceConfig struct &#123;MemoryLimit stringCPUShare stringCPUSet string&#125;type Subsystem interface &#123;&#x2F;&#x2F; return the name of which type of subsystemName() string&#x2F;&#x2F; set a resource limit on a cgroupSet(cgroupPath string, res *ResourceConfig) error&#x2F;&#x2F; add a processs with the pid to a groupAddProcess(cgroupPath string, pid int) error&#x2F;&#x2F; remove a cgroupRemoveCgroup(cgroupPath string) error&#125;&#x2F;&#x2F; instance of a subsystemsvar SubsystemsInstance &#x3D; []Subsystem&#123;&amp;CPU&#123;&#125;,&amp;CPUSet&#123;&#125;,&amp;Memory&#123;&#125;,&#125;</code></pre><h4 id="memorysubsystem-1">4.2 MemorySubsystem</h4><h5 id="name-1">4.2.1 Name()</h5><p>很简单，返回 “memory” 字符串，表示这个 subsystem 是memorySubsystem。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *MemorySubsystem) Name() string &#123;    return &quot;memory&quot;&#125;</code></pre><h5 id="set-1">4.2.2 Set()</h5><p>Set() 用于对 cgroup 设置资源限制，因此参数为 cgroup 的 path 和resourceConfig。</p><ol type="1"><li>其中 <code>GetCgroupPath</code> 后面会提及，作用是获取这个 subsystem所挂载的 hierarchy 上的虚拟文件系统下的 从group 路径。</li><li>获取到 cgroupPath 在虚拟文件系统中的位置后，只需要写入"memory.limit_in_bytes" 文件中即可。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; set the memory limit to this cgroup with cgroupPathfunc (ms *Memory) Set(cgroupPath string, res *ResourceConfig) error  &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;memory.limit_in_bytes&quot;), []byte(res.MemoryLimit), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup memory fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="addprocess-1">4.2.3 AddProcess()</h5><ol type="1"><li>和上面基本一样，只不过是写到 tasks 里。</li><li>pid 变成 byte slice 之前要用 Itoa 转化一下。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add process fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="removecgroup-1">4.2.4 RemoveCgroup()</h5><ol type="1"><li>使用 <code>os.Remove</code> 可以移除参数所指定的文件或文件夹。</li><li>这里移除整个 cgroup 文件夹，就等于是删除 cgroup 了。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusubsystem-1">4.3 CPUSubsystem</h4><p>这里的设计和上面没什么区别，直接贴参考代码</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpu.gofunc (c *CPU) Name() string &#123;return &quot;CPUShare&quot;&#125;func (c *CPU) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpu.shares&quot;), []byte(res.CPUShare), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cpu share limit failed: %s&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpu process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusetsubsystem-1">4.4 CPUSetSubsystem</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpuset.gofunc (c *CPUSet) Name() string &#123;return &quot;CPUSet&quot;&#125;func (c *CPUSet) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpuset.cpus&quot;), []byte(res.CPUSet), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup cpuset failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpuset process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(path.Join(subsystemCgroupPath))&#125;&#125;</code></pre><h4 id="getcgrouppath-1">4.5 GetCgroupPath()</h4><p><code>GetCgroupPath()</code> 用于获取某个 subsystem 所挂载的hierarchy 上的虚拟文件系统 (挂载后的文件夹) 下的 cgroup的路径。通过对这个目录的改写来改动 cgroup。</p><p>首先我们抛开 cgroup，在此之前我们要知道 这个 hierarchy 的 cgroup根节点的路径。那可以在 <code>/proc/self/mountinfo</code> 中获取。</p><p>下面是一些实现细节：</p><ol type="1"><li>首先定义一个 <code>FindCgroupMountpoint()</code> 来找到 cgroup的根节点。</li><li>然后在 <code>GetCgroupPath</code> 将其和 cgroup的相对路径拼接从而获取 cgroup 的路径。如果 <code>autoCreate</code> 为true 且该路径不存在，那么就新建一个 cgroup。(在 hierarchy 环境下，mkdir其实会隐式地创建一个 cgroup，其中包括很多配置文件)</li></ol><blockquote><p><a href="#1.3.4 cgroup 的 kernel 接口">点击这里回顾</a></p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; as the function name shows, find the root path of hierarchyfunc FindCgroupMountpoint(subsystemName string) string  &#123;f, err :&#x3D; os.Open(&quot;&#x2F;proc&#x2F;self&#x2F;mountinfo&quot;)    &#x2F;&#x2F; get info about mount relate to current processif err !&#x3D; nil &#123;return &quot;&quot;&#125;defer f.Close()scanner :&#x3D; bufio.NewScanner(f)for scanner.Scan() &#123;txt :&#x3D; scanner.Text()fields :&#x3D; strings.Split(txt, &quot; &quot;)&#x2F;&#x2F; find whether &quot;subsystemName&quot; appear in the last field&#x2F;&#x2F; if so, then the fifth field is the pathfor _, opt :&#x3D; range strings.Split(fields[len(fields)-1], &quot;,&quot;) &#123;if opt &#x3D;&#x3D; subsystemName &#123;return fields[4]&#125;&#125;&#125;return &quot;&quot;&#125;&#x2F;&#x2F; get the absolute path of a cgroupfunc GetCgroupPath(subsystemName string, cgroupPath string, autoCreate bool) (string, error)  &#123;cgroupRootPath :&#x3D; FindCgroupMountpoint(subsystemName)expectedPath :&#x3D; path.Join(cgroupRootPath, cgroupPath)&#x2F;&#x2F; find the cgroup or create a new cgroupif _, err :&#x3D; os.Stat(expectedPath); err &#x3D;&#x3D; nil  || (autoCreate &amp;&amp; os.IsNotExist(err)) &#123;if os.IsNotExist(err) &#123;if err :&#x3D; os.Mkdir(expectedPath, 0755); err !&#x3D; nil &#123;return &quot;&quot;, fmt.Errorf(&quot;error when create cgroup: %v&quot;, err)&#125;&#125;return expectedPath, nil&#125; else &#123;return &quot;&quot;, fmt.Errorf(&quot;cgroup path error: %v&quot;, err)&#125;&#125;</code></pre><h4 id="cgroupsmanager.go-1">4.6 cgroupsManager.go</h4><ol type="1"><li>定义 CgroupManager 类型，其中的 path 要注意是相对路径，相对于hierarchy 的 root path。所以一个 CgroupManager 是有可能表示多个 cgroups的，或准确说，和对应的 hierarchy root path 的相对路径一样的多个cgroups。</li><li>因为上述原因，<code>Set()</code> 可能会创建多个 cgroups，如果subsystems 们在不同的 hierarchy 就会这样。</li><li>这也是为什么 <code>AddProcess()</code> 和 <code>Remove()</code>要在每个 subsystem 上执行一遍。因为这些 subsystem 可能存在于不同的hierarchies。</li><li>注意 <code>Set()</code> 和 <code>AddProcess()</code>都不是返回错误，而是发出警告，然后返回nil。因为有些时候用户只指定某一个限制，例如 memory，那样的话修改 cpu等其实会报错 (正常的报错)，因此我们不 return err 来退出。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">package cgroupsimport &quot;simple-docker&#x2F;subsystem&quot;type CgroupManager struct &#123;Path     string &#x2F;&#x2F; relative path, relative to the root path of the hierarchy&#x2F;&#x2F; so this may cause more than one cgroup in different hierarchiesResource *subsystems.ResourceConfig&#125;func NewCgroupManager(path string) *CgroupManager &#123;return &amp;CgroupManager&#123;Path: path,&#125;&#125;&#x2F;&#x2F; set the three resource config subsystems to the cgroup(will create if the cgroup path is not existed)&#x2F;&#x2F; this may generate more than one cgroup, because those subsystem may appear in different hierarchiesfunc (cm CgroupManager) Set(res *subsystems.ResourceConfig) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.Set(cm.Path, res); err !&#x3D; nil &#123;logrus.Warnf(&quot;set resource fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; add process to the cgroup path&#x2F;&#x2F; why should we iterate all the subsystems? we have only one cgroup&#x2F;&#x2F; because those subsystems may appear at different hierarchies, which will then cause more than one cgroup, 1-3 in this case.func (cm *CgroupManager) AddProcess(pid int) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.AddProcess(cm.Path, pid); err !&#x3D; nil &#123;logrus.Warn(&quot;app process fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; delete the cgroup(s)func (cm *CgroupManager) Remove() error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err:&#x3D; subsystem.RemoveCgroup(cm.Path); err !&#x3D; nil &#123;return err&#125;&#125;return nil&#125;</code></pre><h4 id="管道处理多个容器参数-1">4.7 管道处理多个容器参数</h4><p>限制容器运行的命令不再像是 <code>/bin/sh</code>这种单个参数，而是多个参数，因此需要使用管道来对多个参数进行处理。那么需要修改以下文件：</p><h5 id="containerinit.go-1">4.7.1 container/init.go</h5><ol type="1"><li>管道原理和 channel 很像，read 端和 write端会在另一边没响应时堵塞。</li><li>使用 <code>os.Pipe()</code> 获取管道。返回的 readPipe 和 writePipe都是 <code>*os.File</code> 类型。</li><li>如何把管道传给子进程 (也就是容器进程) 变成了一个难题，这里用到了<code>ExtraFile</code> 这个参数来解决。cmd会带着参数里的文件来创建新的进程。(这里除了 ExtraFile，还会有类似StandardFile，也就是 stdin，stdout，stderr)</li><li>这里把 read 端传给容器进程，然后 write 端保留在父进程上。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;new pipe error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itselfcmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)&#x2F;&#x2F; new namespacescmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;return cmd, writePipe&#125;</code></pre><p>除了 <code>NewProcess()</code>，<code>InitProcess()</code>也要改变下。</p><ol type="1"><li>使用 readCommand 来读取 pipe。</li><li>实际运行中，当进程运行到 <code>readCommand()</code> 时会堵塞，直到write 端传数据进来。</li><li>因此不用担心我们在容器运行后再传输参数。因为再读取完参数之前，<code>InitProcess()</code>也不会运行到 <code>syscall.Exec()</code> 这一步。</li><li>这里添加了 lookPath，这个是用于解决每次我们都要输入<code>/bin/ls</code>的麻烦，这个函数会帮我们找到参数命令的绝对路径。也就是说，只要输入 ls即可，lookPath 会自动找到 <code>/bin/ls</code>。然后我们再把这个 path作为 <code>argv()</code> 传给 <code>syscall.Exec</code></li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initialize the containerfunc InitProcess() error &#123;cmdArray :&#x3D; readCommand()if len(cmdArray) &#x3D;&#x3D; 0 &#123;return fmt.Errorf(&quot;init process fails, cmdArray is nil&quot;)&#125;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV&#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)path, err :&#x3D; exec.LookPath(cmdArray[0])if err !&#x3D; nil &#123;logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)return err&#125;&#x2F;&#x2F; log path infologrus.Infof(&quot;find path: %v&quot;, path)if err :&#x3D; syscall.Exec(path, cmdArray, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(err.Error())&#125;return nil&#125;func readCommand() []string &#123;pipe :&#x3D; os.NewFile(uintptr(3), &quot;pipe&quot;)msg, err :&#x3D; ioutil.ReadAll(pipe)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read pipe failed: %v&quot;, err)return nil&#125;return strings.Split(string(msg), &quot; &quot;)&#125;</code></pre><h5 id="dockercommandrun.go-1">4.7.2 dockerCommand/run.go</h5><ol type="1"><li>在 run.go 向 writePipe 写入参数，这样容器就会获取到参数。</li><li>关闭 pipe，使得 init 进程继续进行。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig) &#123;initProcess, writePipe :&#x3D; container.NewProcess(tty)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write sidesendInitCommand(cmdArray, writePipe)initProcess.Wait()os.Exit(-1)&#125;func sendInitCommand(cmdArray []string, writePipe *os.File) &#123;cmdString :&#x3D; strings.Join(cmdArray, &quot; &quot;)logrus.Infof(&quot;whole init command is: %v&quot;, cmdString)writePipe.WriteString(cmdString)writePipe.Close()&#125;</code></pre><h5 id="command.go-1">4.7.3 command.go</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open a interactive tty(pre sudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpushare&quot;,Usage:&quot;limit the cpu share&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &#x3D;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;cmdArray :&#x3D; make([]string,len(args)) &#x2F;&#x2F; commandcopy(cmdArray,args)&#x2F;&#x2F; checkout whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; pre sudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig &#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare: context.String(&quot;cpushare&quot;),CPUSet: context.String(&quot;cpu&quot;),&#125;dockerCommand.Run(tty, cmdArray, &amp;resourceConfig)return nil&#125;,&#125;&#x2F;&#x2F; docker init, but cannot be used by uservar InitCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;start initializing...&quot;)return container.InitProcess()&#125;,&#125;</code></pre><h5 id="main.go-1">4.7.4 main.go</h5><p>除了上面的修改，我们还要定义一个程序的入口：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;&quot;github.com&#x2F;urfave&#x2F;cli&quot;)const usage &#x3D; &#96;Usage&#96;func main() &#123;app :&#x3D; cli.NewApp()app.Name &#x3D; &quot;simple-docker&quot;app.Usage &#x3D; usageapp.Commands &#x3D; []cli.Command&#123;RunCommand,InitCommand,&#125;app.Before &#x3D; func(context *cli.Context) error &#123;logrus.SetFormatter(&amp;logrus.JSONFormatter&#123;&#125;)logrus.SetOutput(os.Stdout)return nil&#125;if err :&#x3D; app.Run(os.Args); err !&#x3D; nil &#123;logrus.Fatal(err)&#125;&#125;</code></pre><h4 id="运行-demo-1">4.8 运行 demo</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it stress -m 100m --vm-bytes 200m --vm-keep -m 1</code></pre><p>效果如下：</p><p><img src="0x0035/demo_1.png" /></p><p>不过这个运行方式不能进行交互，我们可以使用这个命令来验证我们写的docker 是否与宿主机隔离：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it &#x2F;bin&#x2F;sh</code></pre><p><img src="0x0035/demo_sh.png" /></p><p>可以看到，pid，ipc，network 方面都与宿主机进行了隔离。</p><h2 id="三镜像篇">三、镜像篇</h2><h3 id="构造镜像">5. 构造镜像</h3><h4 id="编译-aufs-内核">5.1 编译 aufs 内核</h4><p>因为电脑硬盘空间不太够，就不使用虚拟机来做实验了，笔者这里使用 WSL2来完成后续工作，然而，WSL2 Kernel 没有把 aufs编译进去，那只能换内核了，查阅资料，有两种更换内核的方法：</p><ul><li><p>直接替换 <code>C:\System32\lxss\tools\kernel</code> 文件</p></li><li><p>在 users 目录下新建 <code>.wslconfig</code> 文件：</p><pre class="line-numbers language-none"><code class="language-none">[wsl2]kernel&#x3D;&quot;要替换kernel的路径&quot;</code></pre></li></ul><p>很明显，我是不会满足于使用别人编译好的内核的，那我也来动手做一个。</p><h5 id="准备代码库">5.1.1 准备代码库</h5><p>我们先在 WSL 上准备好相关软件包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">apt update #更新源apt install build-essential flex bison libssl-dev libelf-dev gcc make</code></pre><p>编译内核需要从 GitHub 上 clone 微软官方的 WSL 代码和 AUFS-Standalone的代码库</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">git clone https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;WSL2-Linux-Kernel kernelgit clone https:&#x2F;&#x2F;github.com&#x2F;sfjro&#x2F;aufs-standalone aufs5</code></pre><p>然后查看 WSL 内核版本：在 wsl 下运行命令 <code>uname -r</code></p><p>例如我的内核版本是 5.15.19，那 kernel 和 aufs 都要切换到相应的分支去(kernel 默认就是 5.15.19，故不用切换)</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd aufs5git checkout aufs5.15.36</code></pre><p>然后退回到 kernel 文件夹给代码打补丁：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cat ..&#x2F;aufs5&#x2F;aufs5-mmap.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-base.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-kbuild.patch | patch -p1</code></pre><p>三个 Patch 的顺序无关。</p><p>然后再复制一点配置文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cp ..&#x2F;aufs5&#x2F;Documentation . -rcp ..&#x2F;aufs5&#x2F;fs&#x2F; . -rcp ..&#x2F;aufs5&#x2F;include&#x2F;uapi&#x2F;linux&#x2F;aufs_type.h .&#x2F;include&#x2F;uapi&#x2F;linux</code></pre><p>接下来我们来修改一下编译配置，在 <code>Microsoft/config-wsl</code>中任意位置增加一行：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini">CONFIG_AUFS_FS&#x3D;y</code></pre><p>最后，就可以开始编译了！</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">make KCONFIG_CONFIG&#x3D;Microsoft&#x2F;config-wsl -j8</code></pre><p>过程中会问你一些问题，我除了 AUFS Debug 都选了 y。</p><p>最后会在当前目录生成 <code>vmlinuz</code>，在<code>arch/x86/boot</code> 下生成 <code>bzImage</code>。</p><p>关闭 WSL 后更换内核，重启 WSL 输入<code>grep aufs /proc/filesystems</code>验证结果，如果出现 aufs的字样，说明操作成功。</p><h4 id="使用-busybox-创建容器">5.2 使用 busybox 创建容器</h4><h5 id="busybox">5.2.1 busybox</h5><p>先在 docker 获取 busybox 镜像并打包成一个 tar 包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">docker pull busyboxdocker run -d busybox top -bdocker export -o busybox.tar &lt;container_id&gt;</code></pre><p>将其复制到 WSL 下并解压。</p><h5 id="pivot_root">5.2.2 pivot_root</h5><p>pivot_root 是一个系统调用，作用是改变当前 root 文件系统。pivot_root可以将当前进程的 root 文件系统移动到 put_old 文件夹，然后使 new_root成为新的 root 文件系统。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func pivotRoot(root string) error &#123;&#x2F;&#x2F; remount the root dir, in order to make current root and old root in different file systemsif err :&#x3D; syscall.Mount(root, root, &quot;bind&quot;, syscall.MS_BIND|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;mount rootfs to itself error: %v&quot;, err)&#125;&#x2F;&#x2F; create &#39;rootfs&#x2F;.pivot_root&#39; to store old_rootpivotDir :&#x3D; filepath.Join(root, &quot;.pivot_root&quot;)if err :&#x3D; os.Mkdir(pivotDir, 0777); err !&#x3D; nil &#123;return err&#125;&#x2F;&#x2F; pivot_root mount on new rootfs, old_root mount on rootfs&#x2F;.pivot_rootif err :&#x3D; syscall.PivotRoot(root, pivotDir); err !&#x3D; nil &#123;return fmt.Errorf(&quot;pivot_root %v&quot;, err)&#125;&#x2F;&#x2F; change current work dir to root dirif err :&#x3D; syscall.Chdir(&quot;&#x2F;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;chdir &#x2F; %v&quot;, err)&#125;pivotDir &#x3D; filepath.Join(&quot;&#x2F;&quot;, &quot;.pivot_root&quot;)&#x2F;&#x2F; umount rootfs&#x2F;.rootfs_rootif err :&#x3D; syscall.Unmount(pivotDir, syscall.MNT_DETACH); err !&#x3D; nil &#123;return fmt.Errorf(&quot;umount pivot_root dir %v&quot;, err)&#125;&#x2F;&#x2F; del the temporary dirreturn os.Remove(pivotDir)&#125;</code></pre><p>有了这个函数就可以在 init 容器进程时，进行一系列的 mount 操作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setUpMount() error &#123;&#x2F;&#x2F; get current pathpwd, err :&#x3D; os.Getwd()if err !&#x3D; nil &#123;logrus.Errorf(&quot;get current location error: %v&quot;, err)return err&#125;logrus.Infof(&quot;current location: %v&quot;, pwd)pivotRoot(pwd)&#x2F;&#x2F; mount procdefaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEVif err :&#x3D; syscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc failed: %v&quot;, err)return err&#125;if err :&#x3D; syscall.Mount(&quot;tmpfs&quot;, &quot;&#x2F;dev&quot;, &quot;tmpfs&quot;, syscall.MS_NOSUID|syscall.MS_STRICTATIME, &quot;mode&#x3D;755&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;dev failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>tmpfs 是一种基于内存的文件系统，用 RAM 或 swap 分区来存储。</p><p>在 <code>NewParentProcess()</code> 中加一句<code>cmd.Dir="/root/busybox"</code>。</p><p>写完上述函数，然后在 <code>initProcess()</code> 中调用一下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">if err :&#x3D; setUpMount(); err !&#x3D; nil &#123;    logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)&#125;</code></pre><p>然后来运行测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it sh###### dividing live&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;busybox&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#x2F; #</code></pre><p>可以看到，容器当前目录被虚拟定位到了根目录，其实是在宿主机上映射的<code>/root/busybox</code>。</p><h5 id="用-aufs-包装-busybox">5.2.3 用 AUFS 包装 busybox</h5><p>前面提到了，docker 使用 AUFS 存储镜像和容器。docker在使用镜像启动一个容器时，会新建 2 个 layer：write layer 和container-init-layer。write layer是容器唯一的可读写层，container-init-layer是为容器新建的只读层，用来存储容器启动时传入的系统信息。</p><ul><li><code>CreateReadOnlyLayer()</code> 新建 <code>busybox</code>文件夹，解压 <code>busybox.tar</code> 到 <code>busybox</code>目录下，作为容器只读层。</li><li><code>CreateWriteLayer()</code> 新建一个 <code>writeLayer</code>文件夹，作为容器唯一可写层。</li><li><code>CreateMountPoint()</code> 先创建了 <code>mnt</code>文件夹作为挂载点，再把 <code>writeLayer</code> 目录和<code>busybox</code> 目录 mount 到 <code>mnt</code> 目录下。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; extra tar to &#39;busybox&#39;, used as the read only layer for containerfunc CreateReadOnlyLayer(rootURL string) &#123;busyboxURL :&#x3D; rootURL + &quot;busybox&#x2F;&quot;busyboxTarURL :&#x3D; rootURL + &quot;busybox.tar&quot;exist, err :&#x3D; PathExists(busyboxURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, busyboxURL, err)&#125;if !exist &#123;if err :&#x3D; os.Mkdir(busyboxURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, busyboxURL, err)&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, busyboxTarURL, &quot;-C&quot;, busyboxURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, busyboxTarURL, err)&#125;&#125;&#125;&#x2F;&#x2F; create a unique folder as writeLayerfunc CreateWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.Mkdir(writeURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, writeURL, err)&#125;&#125;func CreateMountPoint(rootURL string, mntURL string) &#123;&#x2F;&#x2F; create mnt folder as mount pointif err :&#x3D; os.Mkdir(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;dirs :&#x3D; &quot;dirs&#x3D;&quot; + rootURL + &quot;writeLayer:&quot; + rootURL + &quot;busybox&quot;cmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;&#125;func NewWorkSpace(rootURL, mntURL string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)&#125;</code></pre><p>接下来在 <code>NewParentProcess()</code> 将容器使用的宿主机目录<code>/root/busybox</code> 替换为 <code>/root/mnt</code>，这样使用 AUFS系统启动容器的代码就完成了。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;NewWorkSpace(rootURL, mntURL)cmd.Dir &#x3D; mntURLreturn cmd, writePipe</code></pre><p>docker 会在删除容器时，把容器对应的 write layer 和container-init-layer 删除，而保留镜像中所有的内容。</p><ul><li><code>DeleteMountPoint()</code> 中 umount <code>mnt</code>目录。</li><li>删除 <code>mnt</code> 目录。</li><li>在 <code>DeleteWriteLayer()</code> 删除 <code>writeLayer</code>文件夹。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(rootURL string, mntURL string) &#123;cmd :&#x3D; exec.Command(rootURL, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)&#125;&#125;func DeleteWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;func DeleteWorkSpace(rootURL, mntURL string) &#123;DeleteMountPoint(rootURL, mntURL)DeleteWriteLayer(rootURL)&#125;</code></pre><p>现在来启动一个容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it shdirs&#x3D;&#x2F;root&#x2F;writeLayer:&#x2F;root&#x2F;busybox&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#x2F; #</code></pre><p>测试在容器内创建文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # mkdir aaa&#x2F; # touch aaa&#x2F;test.txt</code></pre><p>此时我们可以在宿主机终端查看<code>/root/mnt/writeLayer</code>，可以看到刚才新建的 <code>aaa</code>文件夹和 <code>test.txt</code>，在我们退出容器后，<code>/root/mnt</code>文件夹被删除，伴随着刚才创建的文件夹和文件都被删除，而作为镜像的 busybox仍被保留，且内容未被修改。</p><h4 id="实现-volume-数据卷">5.3 实现 volume 数据卷</h4><p>上节实现了容器和镜像的分离，但是如果容器退出，容器可写层的所有内容就会被删除，这里使用volume 来实现容器数据持久化。</p><p>先在 <code>command.go</code> 里添加 <code>-v</code> 标签：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;,         &#x2F;&#x2F; add &#96;-v&#96; tag         &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;         &#x2F;&#x2F; send volume args to Run()volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig,volume)return nil&#125;,&#125;</code></pre><p>在 <code>Run()</code> 中，把 volume 传给创建容器的<code>NewParentProcess()</code> 和删除容器文件系统的<code>DeleteWorkSpace()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)initProcess.Wait()rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>在 <code>NewWorkSpace()</code> 中，继续把 volume传给创建容器文件系统的 <code>NewWorkSapce()</code>。</p><p>创建容器文件系统过程如下：</p><ul><li>创建只读层。</li><li>创建容器读写层。</li><li>创建挂载点并把只读层和读写层挂载到挂载点上。</li><li>判断 volume是否为空，如果是，说明用户没有使用挂载标签，结束创建过程。</li><li>不为空，就用 <code>volumeURLExtract()</code> 解析。</li><li>当 <code>volumeURLExtract()</code> 返回字符数组长度为2，且数据元素均不为空时，则执行 <code>MountVolume()</code>来挂载数据卷。<ul><li>否则提示用户创建数据卷输入值不对。</li></ul></li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(rootURL, mntURL, volume string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(rootURL, mntURL, volumeURLs)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;func volumeUrlExtract(volume string) []string &#123;&#x2F;&#x2F; divide volume by &quot;:&quot;return strings.Split(volume, &quot;:&quot;)&#125;</code></pre><p>挂载数据卷过程如下：</p><ul><li>读取宿主机文件目录 URL，创建宿主机文件目录(<code>/root/$&#123;parentURL&#125;</code>)</li><li>读取容器挂载点 URL，在容器文件系统里创建挂载点(<code>/root/mnt/$&#123;containerURL&#125;</code>)</li><li>把宿主机文件目录挂载到容器挂载点，这样启动容器的过程，对数据卷的处理就完成了。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]containerVolumeURL :&#x3D; mntURL + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURLcmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)&#125;&#125;</code></pre><p>删除容器文件系统过程如下：</p><ul><li>在 volume 不为空，且使用 <code>volumeURLExtract()</code> 解析 volume字符串返回的字符数组长度为 2，数据元素均不为空时，才执行<code>DeleteMountPointWithVolume()</code> 来处理。</li><li>其余情况仍使用前面的 <code>DeleteMountPoint()</code>。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(rootURL, mntURL, volume string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;DeleteWriteLayer(rootURL)&#125;</code></pre><p><code>DeleteMountPointWithVolume()</code> 处理逻辑如下：</p><ul><li>卸载 volume 挂载点的文件系统(<code>/root/mnt/$&#123;containerURL&#125;</code>)，保证整个容器挂载点没有再被使用。</li><li>卸载整个容器文件系统挂载点 (<code>/root/mnt</code>)。</li><li>删除容器文件系统挂载点。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; umount volume point in containercontainerURL :&#x3D; mntURL + volumeURLs[1]cmd :&#x3D; exec.Command(&quot;umount&quot;, containerURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)&#125;&#x2F;&#x2F; umount the whole point of the containercmd &#x3D; exec.Command(&quot;umount&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>接下来启动容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; #</code></pre><p>进入 <code>containerVolume</code>，创建一个文本文件，并随便写点东西：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;test&quot; &gt;&gt; test.txt</code></pre><p>此时我们能在宿主机的 <code>/root/volume</code>找到我们刚才创建的文本文件。退出容器后，volume文件夹也没有被删除。再次进入容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">r# go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;mkdir parent dir &#x2F;root&#x2F;volume error. mkdir &#x2F;root&#x2F;volume: file exists&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; # ls containerVolume&#x2F;test.txt</code></pre><p>此时这里会提示 volume 文件夹存在，我们在 <code>test.txt</code>内追加内容：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;###&quot; &gt;&gt; test.txt</code></pre><p>此时再次退出容器，能看到修改过后的文件内容，可以看到 volume文件夹没有被删除。</p><h4 id="简单镜像打包">5.4 简单镜像打包</h4><p>容器在退出时会删除所有可写层的内容，commit命令可以把运行状态容器的内容存储为镜像保存下来。</p><p>在 <code>main.go</code> 里添加 <code>commit</code> 命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    InitCommand,    RunCommand,    CommitCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里实现 <code>CommitCommand</code>命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;imageName :&#x3D; context.Args()[0]&#x2F;&#x2F; commitContainer(containerName)commitContainer(imageName)return nil&#125;,&#125;</code></pre><p>添加 <code>commit.go</code>，通过 <code>commitContainer()</code>实现将容器文件系统打包成 <code>$&#123;imagename&#125;.tar</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&#x2F;exec&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;)func commitContainer(imageName string) &#123;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&quot;imageTar :&#x3D; &quot;&#x2F;root&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>运行测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it sh</code></pre><p>然后在另一个终端运行：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit image</code></pre><p>这时候可以在 root 目录下看到多了一个 <code>image.tar</code>，解压后可以发现压缩包的内容和 <code>/root/mnt</code> 一致。</p><blockquote><p>tips：一定要先运行容器！如果不运行容器直接打包，会提示<code>/root/mnt</code> 不存在。</p></blockquote><h3 id="构建容器进阶">6. 构建容器进阶</h3><h4 id="实现容器后台运行">6.1 实现容器后台运行</h4><p>容器，放在操作系统层面，就是一个进程，当前运行命令的 simple-docker是主进程，容器是当前 simple-docker 进程 fork出来的子进程。子进程的结束和父进程的运行是一个异步的过程，即父进程不会知道子进程在什么时候结束。如果创建子进程时，父进程退出，那这个子进程就是孤儿进程(没人管)，此时进程号为 1 的进程 init 就会接受这些孤儿进程。</p><p>先在 <code>command.go</code> 添加 <code>-d</code>标签，表示这个容器启动时在后台运行：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach container         &#x2F;&#x2F; tty cannot work with detachif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume)return nil&#125;,&#125;</code></pre><p>然后也要修改一下 <code>run.go</code> 的 <code>Run()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)    &#x2F;&#x2F; if background process, parent process won&#39;t waitif tty &#123;initProcess.Wait()&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T15:32:44+08:00&quot;&#125;</code></pre><p>根据书上的提示，<code>ps -ef</code> 用来查找 top 进程：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ps -ef | grep toproot        3713     751  0 14:42 pts&#x2F;2    00:00:00 top</code></pre><p>前面几次运行命令，都找不到 top这个进程，于是我后面多跑了几次，终于看到了这个进程。。。</p><p>可以看到，top 命令的进程正在运行着，不过运行环境是 WSL，父进程 id不是 1，然后 <code>ps -ef</code> 查看一下，top 的父进程是一个 bash进程，而 bash 进程的父进程是一个 init 进程，这样应该算过了吧(偶尔的一两次不严谨)。</p><h4 id="实现查看运行中的容器">6.2 实现查看运行中的容器</h4><h5 id="name-标签">6.2.1 name 标签</h5><p>前面创建的容器里，所有关于容器的信息，例如PID、容器创建时间、容器运行命令等，都没有记录，这导致容器运行完后就在也不知道它的信息了，因此要把这部分信息保留。先在<code>command.go</code> 里加一个 name 标签，方便用户指定容器的名字：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag &#123;Name: &quot;name&quot;,Usage: &quot;container name&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume, containerName)return nil&#125;,&#125;</code></pre><p>添加一个方法来记录容器的相关信息，这里用先用一个 10位的数字来表示容器的 id：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func randStringBytes(n int) string &#123;letterBytes :&#x3D; &quot;1234567890&quot;rand.Seed(time.Now().UnixNano())b :&#x3D; make([]byte, n)for i :&#x3D; range b &#123;b[i] &#x3D; letterBytes[rand.Intn(len(letterBytes))]&#125;return string(b)&#125;</code></pre><p>这里用时间戳为种子，每次生成一个 10 以内的数字作为 letterBytes数组的下标，最后拼成整个容器的 id。容器的信息默认保存在<code>/var/run/simple-docker/$&#123;containerName&#125;/config.json</code>，容器基本格式如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;Id          string &#96;json:&quot;id&quot;&#96;Name        string &#96;json:&quot;name&quot;&#96;Command     string &#96;json:&quot;command&quot;&#96; &#x2F;&#x2F; the command that init process executeCreatedTime string &#96;json:&quot;created_time&quot;&#96;Status      string &#96;json:&quot;status&quot;&#96;&#125;var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&quot;ConfigName          string &#x3D; &quot;config.json&quot;)</code></pre><p>下面是记录容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;) &#x2F;&#x2F; format must like thiscommand :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>这里格式化的时间必须是<code>2006-01-02 15:04:05</code>，不然格式化后的时间会是几千年后doge。</p><p>详细可以看这篇文章：<ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p>在主函数加上调用：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>如果创建 tty 方式的容器，在容器退出后，就会删除相关信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func deleteContainerInfo(containerID string) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerID)if err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, dirURL, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top# go run . run -d --name jay top</code></pre><p>执行完成后，可以在 <code>/var/run/simple-docker/</code>找到两个文件夹，一个是随机 id，一个是 jay，文件夹下各有一个<code>config.json</code>，记录了容器的相关信息。</p><h5 id="实现-docker-ps">6.2.2 实现 docker ps</h5><p>在 <code>main.go</code> 加一个 <code>listCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,&#125;</code></pre><p>在 <code>command.go</code> 添加定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ListCommand &#x3D; cli.Command&#123;Name: &quot;ps&quot;,Usage: &quot;list all the containers&quot;,Action: func(context *cli.Context) error &#123;ListContainers()return nil&#125;,&#125;</code></pre><p>新建一个 <code>list.go</code>，实现记录列出容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListContainers() &#123;&#x2F;&#x2F; get the path that store the info of the containerdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, &quot;&quot;)dirURL &#x3D; dirURL[:len(dirURL)-1]&#x2F;&#x2F; read all the files in the directoryfiles, err :&#x3D; ioutil.ReadDir(dirURL)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read dir %s error %v&quot;, dirURL, err)return&#125;var containers []*container.ContainerInfofor _, file :&#x3D; range files &#123;tmpContainer, err :&#x3D; getContainerInfo(file)&#x2F;&#x2F; .Println(tmpContainer)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info error %v&quot;, err)continue&#125;containers &#x3D; append(containers, tmpContainer)&#125;&#x2F;&#x2F; use tabwriter.NewWriter to print the containerInfow :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprintf(w, &quot;ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n&quot;)for _, item :&#x3D; range containers &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\t%s\t%s\t%s\n&quot;,item.Id, item.Name, item.Pid, item.Status, item.Command, item.CreatedTime)&#125;&#x2F;&#x2F; refresh stdout if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;flush stdout error %v&quot;,err)return&#125;&#125;func getContainerInfo(file os.FileInfo) (*container.ContainerInfo, error) &#123;containerName :&#x3D; file.Name()&#x2F;&#x2F; create the absolute pathconfigFileDir :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFileDir &#x3D; configFileDir + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read config.jsoncontent, err :&#x3D; ioutil.ReadFile(configFileDir)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, configFileDir, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; turn json to containerInfoif err :&#x3D; json.Unmarshal(content, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>接上小节的测试，我们运行以下命令：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:11+08:00&quot;&#125;# go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:25+08:00&quot;&#125;# go run . psID           NAME         PID         STATUS      COMMAND     CREATED6675792962   6675792962   4317        running     top         2023-05-05 19:29:115553437308   jay          4404        running     top         2023-05-05 19:29:25</code></pre><p>现在就可以通过 ps 来看到所有创建的容器状态和它们的 init 进程 id了。</p><h4 id="查看容器日志">6.3 查看容器日志</h4><p>在 <code>main.go</code> 加一个 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,    LogCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里添加 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var LogCommand &#x3D; cli.Command&#123;Name:  &quot;logs&quot;,Usage: &quot;print logs of a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;contianerName :&#x3D; context.Args()[0]logContainer(contianerName)return nil&#125;,&#125;</code></pre><p>新建一个 <code>log.go</code>，定义 <code>logContainer()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func logContainer(containerName string) &#123;&#x2F;&#x2F; get the log pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)logFileLocation :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ContainerLogFile&#x2F;&#x2F; open log filefile, err :&#x3D; os.Open(logFileLocation)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container open file %s error: %v&quot;, logFileLocation, err)return&#125;defer file.Close()&#x2F;&#x2F; read log file contentcontent, err :&#x3D; ioutil.ReadAll(file)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container read file %s error: %v&quot;, logFileLocation, err)return&#125;&#x2F;&#x2F; use Fprint to transfer content to stdoutfmt.Fprint(os.Stdout, string(content))&#125;</code></pre><p>测试一下，先用 detach 方式创建一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-06T14:26:32+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED1837062451   jay         2065        running     top         2023-05-06 14:26:32# go run . logs jayMem: 3265116K used, 4568420K free, 3256K shrd, 71432K buff, 1135692K cachedCPU:  0.3% usr  0.2% sys  0.0% nic 99.3% idle  0.0% io  0.0% irq  0.0% sirqLoad average: 0.03 0.09 0.08 1&#x2F;521 5PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND</code></pre><p>可以看到，logs命令成功运行并输出容器的日志。(这里之前出现过前几次创建容器，而后台却没运行的情况，导致一开始运行logs 时报错了，建议在运行 logs 前多检查下 top 是否后台运行中)</p><h4 id="进入容器-namespace">6.4 进入容器 Namespace</h4><p>在 6.3小节里，实现了查看后台运行的容器的日志，但是容器一旦创建后，就无法再次进入容器，这一次来实现进入容器内部的功能，也就是exec。</p><h5 id="setns">6.4.1 setns</h5><p>setns 是一个系统调用，可根据提供的 PID 再次进入到指定的Namespace。它要先打开 <code>/proc/$&#123;pid&#125;/ns</code>文件夹下对应的文件，然后使当前进程进入到指定的 Namespace 中。对于 go来说，一个有多线程的进程使无法使用 setns 调用进入到对应的命名空间的，go没启动一个程序就会进入多线程状态，因此无法简单在 go里直接调用系统调用，这里还需要借助 C 来实现这个功能。</p><h5 id="cgo">6.4.2 Cgo</h5><p>在 go 里写 C：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package rand&#x2F;*#include &lt;stdlib.h&gt;*&#x2F;import &quot;C&quot;func Random() int &#123;    return int(C.random())&#125;func Seed(i int) &#123;    C.srandom(C.uint(i))&#125;</code></pre><h5 id="实现">6.4.3 实现</h5><p>先使用 C 根据 PID进入对应 Namespace：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenter&#x2F;*#define _GNU_SOURCE#include &lt;errno.h&gt;#include &lt;sched.h&gt;#include &lt;stdio.h&gt;#include &lt;stdlib.h&gt;#include &lt;string.h&gt;#include &lt;fcntl.h&gt;#include &lt;unistd.h&gt;&#x2F;&#x2F; if this package is quoted, this function will run automatic__attribute__((constructor)) void enter_namespace(void)&#123;    char *simple_docker_pid;    &#x2F;&#x2F; get pid from system environment    simple_docker_pid &#x3D; getenv(&quot;simple_docker_pid&quot;);    if (simple_docker_pid)    &#123;        fprintf(stdout, &quot;got simple docker pid&#x3D;%s\n&quot;, simple_docker_pid);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker pid env skip nsenter&quot;);        &#x2F;&#x2F; if no specified pid, the func will exit        return;    &#125;    char *simple_docker_cmd;    simple_docker_cmd &#x3D; getenv(&quot;simple_docker_cmd&quot;);    if (simple_docker_cmd)    &#123;        fprintf(stdout, &quot;got simple docker cmd&#x3D;%s\n&quot;, simple_docker_cmd);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker cmd env skip nsenter&quot;);        &#x2F;&#x2F; if no specified cmd, the func will exit        return;    &#125;    int i;    char nspath[1024];    char *namespace[] &#x3D; &#123;&quot;ipc&quot;, &quot;uts&quot;, &quot;net&quot;, &quot;pid&quot;, &quot;mnt&quot;&#125;;    for (i &#x3D; 0; i &lt; 5; i++)    &#123;        &#x2F;&#x2F; create the target path, like &#x2F;proc&#x2F;pid&#x2F;ns&#x2F;ipc        sprintf(nspath, &quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;%s&quot;, simple_docker_pid, namespace[i]);        int fd &#x3D; open(nspath, O_RDONLY);printf(&quot;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; %d %s\n&quot;, fd, nspath);        &#x2F;&#x2F; call sentns and enter the target namespace        if (setns(fd, 0) &#x3D;&#x3D; -1)        &#123;            fprintf(stderr, &quot;setns on %s namespace failed: %s\n&quot;, namespace[i], strerror(errno));        &#125;        else        &#123;            fprintf(stdout, &quot;setns on %s namespace succeeded\n&quot;, namespace[i]);        &#125;        close(fd);    &#125;    &#x2F;&#x2F; run command in target namespace    int res &#x3D; system(simple_docker_cmd);    exit(0);    return;&#125;*&#x2F;import &quot;C&quot;</code></pre><p>那如何使用这段代码呢，只需要在要加载的地方引用这个 package即可，我这里是 <code>nenster</code> 。</p><p>其实也可以，单独放在一个 C 文件里，go 文件可以这样写：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenterimport &quot;C&quot;</code></pre><p>下面增加 <code>ExecCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ExecCommand &#x3D; cli.Command&#123;Name:  &quot;exec&quot;,Usage: &quot;exec a command into container&quot;,Action: func(context *cli.Context) error &#123;if os.Getenv(ENV_EXEC_PID) !&#x3D; &quot;&quot; &#123;logrus.Infof(&quot;pid callback pid %v&quot;, os.Getgid())return nil&#125;if len(context.Args()) &lt; 2 &#123;return fmt.Errorf(&quot;missing container name or command&quot;)&#125;containerName :&#x3D; context.Args()[0]cmdArray :&#x3D; make([]string, len(context.Args())-1)for i, v :&#x3D; range context.Args().Tail() &#123;cmdArray[i] &#x3D; v&#125;ExecContainer(containerName, cmdArray)return nil&#125;,&#125;</code></pre><p>新建一个 <code>exec.go</code>下面实现获取容器名和需要的命令，并且在这里引用<code>nsenter</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ENV_EXEC_PID &#x3D; &quot;simple_docker_pid&quot;const ENV_EXEC_CMD &#x3D; &quot;simple_docker_cmd&quot;func getContainerPidByName(containerName string) (string, error) &#123;&#x2F;&#x2F; get the path that store container infodirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read files in target pathcontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;return &quot;&quot;, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to containerInfoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;return &quot;&quot;, err&#125;return containerInfo.Pid, nil&#125;func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run --name jay -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-07T13:23:09+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6530018751   jay         146639      running     top         2023-05-07 13:23:09# go run . logs jayMem: 4355160K used, 3478372K free, 3272K shrd, 208844K buff, 1581396K cachedCPU:  1.2% usr  0.6% sys  0.0% nic 97.9% idle  0.0% io  0.0% irq  0.1% sirqLoad average: 0.12 0.14 0.16 1&#x2F;574 6  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND# go run . exec jay sh&#x2F; # lsbin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top   13 root      0:00 sh   15 root      0:00 ps -ef&#x2F; #</code></pre><p>可以看到，成功进入容器内部，且与宿主机隔离。</p><p>这里出现了一个很奇怪的 bug，就是通过 cgo 去 setns，执行到 mnt时，抛出个错误：<code>Stale file handle</code>，当时找了全网，也找不到答案，于是陷入了两天的痛苦debug，在重新敲代码时，发现又不报错了，切换回那个有错误的分支，也不报错了。既然暂时找不到错误，先搁着吧，如果有看到这篇文章的朋友，也遇到了这个错误，可以留意下。(感觉会是一个雷)</p><p>(应该是容器的 mnt 没有 mount 上去，才会导致 stale file handle)</p><h4 id="停止容器">6.5 停止容器</h4><p>定义 <code>StopCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var StopCommand &#x3D; cli.Command&#123;Name:  &quot;stop&quot;,Usage: &quot;stop a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]stopContainer(containerName)return nil&#125;,&#125;</code></pre><p>然后声明一个函数，通过容器名来获取容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getContainerInfoByName(containerName string) (*container.ContainerInfo, error) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigNamecontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read config file %s error %v&quot;, configFilePath, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to container infoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json to container info error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>然后是停止容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func stopContainer(containerName string) &#123;&#x2F;&#x2F; get pid by containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container pid by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; turn pid(string) to intpidInt, err :&#x3D; strconv.Atoi(pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;convert pid from string to int error %v&quot;, err)return&#125;&#x2F;&#x2F; kill container main processif err :&#x3D; syscall.Kill(pidInt, syscall.SIGTERM); err !&#x3D; nil &#123;logrus.Errorf(&quot;stop container %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; get info of the containercontainerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; process is killed, update process statuscontainerInfo.Status &#x3D; container.STOPcontainerInfo.Pid &#x3D; &quot; &quot;&#x2F;&#x2F; update info to jsonnweContentBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;json marshal %s error %v&quot;, containerName, err)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; overwrite containerInfoif err :&#x3D; ioutil.WriteFile(configFilePath, nweContentBytes, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;write config file %s error %v&quot;, configFilePath, err)&#125;&#125;</code></pre><p>测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop jay# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6883605813   jay                     stopped     top# ps -ef | grep toproot       43588     761  0 20:00 pts&#x2F;0    00:00:00 grep --color&#x3D;auto top</code></pre><p>可以看到，jay 这个进程被停止了，且 pid 号设为空。</p><h4 id="删除容器">6.6 删除容器</h4><p>定义 <code>RemoveCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RemoveCommand &#x3D; cli.Command&#123;Name:  &quot;rm&quot;,Usage: &quot;remove a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]removeContainer(containerName)return nil&#125;,&#125;</code></pre><p>实现删除容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . rm jay# go run . psID          NAME        PID         STATUS      COMMAND     CREATED</code></pre><p>可以看到，jay 这个容器被删除了。</p><h4 id="通过容器制作镜像">6.7 通过容器制作镜像</h4><p>这一节，根据书上的内容，有许多函数需要改动。建议这里对着作者给出的源码debug，书上有部分内容有明显错误。</p><p>之前的文件系统如下：</p><ul><li>只读层：busybox，只读，容器系统的基础</li><li>可写层：writeLayer，容器内部的可写层</li><li>挂载层：mnt，挂载外部的文件系统，类似虚拟机的文件共享</li></ul><p>修改后的文件系统如下：</p><ul><li>只读层：不变</li><li>可写层：再加容器名为目录进行隔离，也就是<code>writeLayer/$&#123;containerName&#125;</code></li><li>挂载层：再加容器名为目录进行隔离，也就是<code>mnt/$&#123;containerName&#125;</code></li></ul><p>因此，本节要实现为每个容器分配单独的隔离文件系统，以及实现对不同容器打包镜像。</p><p><strong>修改 <code>run.go</code></strong></p><p>在 Run 函数参数列表添加一个 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><p>同时也在 <code>command.go</code> 的 runCommand 里修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName)return nil&#125;,</code></pre><p>在 <code>recordContainerInfo</code> 函数的参数列表添加 volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName, volume string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;)command :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,Volume:      volume,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>给 ContainerInfo 添加 Volume 成员：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;        &#x2F;&#x2F;容器的init进程在宿主机上的 PIDId          string &#96;json:&quot;id&quot;&#96;         &#x2F;&#x2F;容器IdName        string &#96;json:&quot;name&quot;&#96;       &#x2F;&#x2F;容器名Command     string &#96;json:&quot;command&quot;&#96;    &#x2F;&#x2F;容器内init运行命令CreatedTime string &#96;json:&quot;createTime&quot;&#96; &#x2F;&#x2F;创建时间Status      string &#96;json:&quot;status&quot;&#96;     &#x2F;&#x2F;容器的状态Volume      string &#96;json:&quot;volume&quot;&#96;&#125;</code></pre><p>然后将<code>RootURL</code>，<code>MntURL</code>，<code>WriteLayer</code>设为常量：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&#x2F;&quot;ConfigName          string &#x3D; &quot;config.json&quot;ContainerLogFile    string &#x3D; &quot;container.log&quot;RootURL             string &#x3D; &quot;&#x2F;root&#x2F;&quot;MntURL              string &#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;%s&#x2F;&quot;WriteLayerURL       string &#x3D; &quot;&#x2F;root&#x2F;writeLayer&#x2F;%s&quot;)</code></pre><p>相应地，<code>NewParentProcess</code> 函数也要修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p><code>NewWorkSpace</code>函数的三个参数分别改为：<code>volume</code>，<code>imageName</code>，<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(volume, imageName, containerName string) &#123;CreateReadOnlyLayer(imageName)CreateWriteLayer(containerName)CreateMountPoint(containerName, imageName)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(volumeURLs, containerName)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;</code></pre><p>下面来修改<code>CreateReadOnlyLayer</code>，<code>CreateWriteLayer</code>，<code>CreateMountPoint</code>这三个函数：</p><p>首先是 <code>CreateReadOnlyLayer</code>，参数名改为<code>imageName</code>，镜像解压出来的只读层以<code>RootURL+imageName</code> 命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateReadOnlyLayer(imageName string) error &#123;unTarFolderURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;&#x2F;&quot;imageURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;exist, err :&#x3D; PathExists(unTarFolderURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, unTarFolderURL, err)return err&#125;if !exist &#123;if err :&#x3D; os.MkdirAll(unTarFolderURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, unTarFolderURL, err)return err&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, imageURL, &quot;-C&quot;, unTarFolderURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, unTarFolderURL, err)return err&#125;&#125;return nil&#125;</code></pre><p><code>CreateWriteLayer</code> 为每个容器创建一个读写层，把参数改为containerName，容器读写层修改为 <code>WriteLayerURL+containerName</code>命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateWriteLayer(containerName string) &#123;writeUrl :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.MkdirAll(writeUrl, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;Mkdir write layer dir %s error. %v&quot;, writeUrl, err)&#125;&#125;</code></pre><p><code>CreateMountPoint</code>创建容器根目录，然后把镜像只读层和容器读写层挂载到容器根目录，成为容器文件系统，参数列表改为<code>containerName</code> 和 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateMountPoint(containerName, imageName string) error &#123;&#x2F;&#x2F; create mnt folder as mount pointmntURL :&#x3D; fmt.Sprintf(MntURL, containerName)if err :&#x3D; os.MkdirAll(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)return err&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;tmpWriteLayer :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)tmpImageLocation :&#x3D; RootURL + &quot;&#x2F;&quot; + imageNamedirs :&#x3D; &quot;dirs&#x3D;&quot; + tmpWriteLayer + &quot;:&quot; + tmpImageLocation_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;run command for creating mount point failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p><code>MountVolume</code> 根据用户输入的 volume参数获取相应挂载宿主机数据卷 URL 和容器的挂载点URL，并挂载数据卷。参数列表改为 <code>volumeURLs</code> 和<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerVolumeURL :&#x3D; mntURL + &quot;&#x2F;&quot; + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURL_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>然后在删除容器的 <code>removeContainer</code> 函数最后加一行<code>DeleteWorkSpace</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;container.DeleteWorkSpace(containerInfo.Volume, containerName)&#125;</code></pre><p>然后 <code>DeleteWorkSpace</code>也要修改，<code>DeleteWorkSpace</code>作用是当容器退出时，删除容器相关文件系统，参数列表改为 containerName 和volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(volume, containerName string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(volumeURLs, containerName)&#125; else &#123;DeleteMountPoint(containerName)&#125;&#125; else &#123;DeleteMountPoint(containerName)&#125;DeleteWriteLayer(containerName)&#125;</code></pre><p><code>DeleteMountPoint</code>函数作用是删除未挂载数据卷的容器文件系统，参数修改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(containerName string) error &#123;mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)return err&#125;return nil&#125;</code></pre><p><code>DeleteMountPointWithVolume</code>函数用来删除挂载数据卷容器的文件系统，参数列表改为<code>volumeURLs</code> 和 <code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; umount volume point in containermntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerURL :&#x3D; mntURL + &quot;&#x2F;&quot; + volumeURLs[1]if _, err :&#x3D; exec.Command(&quot;umount&quot;, containerURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)return err&#125;&#x2F;&#x2F; umount the whole point of the container_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;return nil&#125;</code></pre><p><code>DeleteWriteLayer</code> 函数用来删除容器读写层，参数改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWriteLayer(containerName string) &#123;writeURL :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;</code></pre><p>然后修改 <code>command.go</code> 中的<code>commitCommand</code>：输入参数名改为 <code>containerName</code> 和<code>imageName</code>：·</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]imageName :&#x3D; context.Args()[1]&#x2F;&#x2F; commitContainer(containerName)commitContainer(containerName, imageName)return nil&#125;,&#125;</code></pre><p>修改 <code>commit.go</code> 的 <code>commitContainer</code>函数，根据传入的 containerName 制作 <code>imageName.tar</code>镜像：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func commitContainer(containerName, imageName string) &#123;mntURL :&#x3D; fmt.Sprintf(container.MntURL, containerName)mntURL +&#x3D; &quot;&#x2F;&quot;imageTar :&#x3D; container.RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>测试一下，用 busybox 启动两个容器 test1 和 test2，test1 把宿主机<code>/root/from1</code> 挂载到容器 <code>/to1</code>，test2 把宿主机<code>/root/from2</code> 挂载到 <code>/to2</code> 下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test1 -v &#x2F;root&#x2F;from1:&#x2F;to1 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from1\&quot; \&quot;&#x2F;to1\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;# go run . run -d --name test2 -v &#x2F;root&#x2F;from2:&#x2F;to2 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from2\&quot; \&quot;&#x2F;to2\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1       11570       running     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>打开另一个终端，可以看到 <code>/root</code> 目录下多了<code>from1</code> 和 <code>from2</code> 两个目录，我们看看<code>mnt</code> 和 <code>writeLayer</code>，<code>mnt</code> 下多了两个busybox 的挂载层，<code>writeLayer</code>下分别挂载了两个容器的目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   └── to1└── test2    └── to2</code></pre><p>下面进入 test1 容器，创建 <code>/to1/test1.txt</code>：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . exec test1 sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 11570&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#x2F; # echo -e &quot;test1&quot; &gt;&gt; &#x2F;to1&#x2F;test1.txt&#x2F; # mkdir to1-1&#x2F; # echo -e &quot;test111111&quot; &gt;&gt; &#x2F;to1-1&#x2F;test1111.txt</code></pre><p>这时候再来看看可写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   ├── root│   ├── to1│   └── to1-1│       └── test1111.txt└── test2    └── to2# cat writeLayer&#x2F;test1&#x2F;to1-1&#x2F;test1111.txttest111111</code></pre><p>多了 <code>to1-1/test1111.txt</code>，那刚刚创建的<code>test1.txt</code> 去哪了呢？这时候我们看看<code>from1</code>，在这里，新创建的文件写入了数据卷。</p><p>下面来验证 commit 功能：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit test1 image1</code></pre><p>导出的镜像路径为 <code>/root/image1.tar</code>。</p><p>下面测试停止和删除容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1                   stopped     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51# go run . rm test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>我们看看容器根目录和可读写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls mnttest2# tree writeLayer&#x2F;writeLayer&#x2F;└── test2    └── to2</code></pre><p>test1 的容器根目录和可读写层被删除。</p><p>下面来试一下用镜像创建容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test3 -v &#x2F;root&#x2F;from3:&#x2F;to3 image1 top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from3\&quot; \&quot;&#x2F;to3\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:514713076733   test3       13056       running     top         2023-05-11 10:32:44</code></pre><p>这时我们可以看到 <code>/root</code> 多了一个 <code>image1</code>目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls image1bin  dev  etc  home  lib  lib64  proc  root  sys  tmp  to1  to1-1  usr  var</code></pre><p>在这里发现了刚才创建的 <code>to1-1</code>，用 <code>image1.tar</code>启动的容器 test3，进入容器后发现我们刚刚写入的文件，至此，我们成功把容器test1 的数据卷 to1 信息，重新写入了容器 test3 数据卷 to3。</p><p>在次小节后，进入容器都要指定镜像名，不然都会报错。</p><h4 id="实现容器指定环境变量运行">6.8 实现容器指定环境变量运行</h4><p>本节来实现让容器内运行的程序可以使用外部传递的环境变量。</p><h5 id="修改-runcommand">6.8.1 修改 runCommand</h5><p>在原来基础上增加 <code>-e</code>选项，允许用户指定环境变量，由于环境变量可以是多个，这里允许用户多次使用<code>-e</code> 来传递，同时添加对环境变量的解析，整体修改如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name:  &quot;d&quot;,Usage: &quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;name&quot;,Usage: &quot;container name&quot;,&#125;, &amp;cli.StringSliceFlag&#123;Name:  &quot;e&quot;,Usage: &quot;set environment&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)envSlice :&#x3D; context.StringSlice(&quot;e&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName, envSlice)return nil&#125;,&#125;</code></pre><h5 id="修改-run-函数">6.8.2 修改 Run 函数</h5><p>参数里新增一个 <code>envSlice</code>，然后传递给<code>NewParentProcess</code> 函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string, envSlice []string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName, envSlice)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><h5 id="修改-newparentprocess-函数">6.8.3 修改 NewParentProcess函数</h5><p>参数新增一个 <code>envSlice</code>，给 cmd 设置环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string, envSlice []string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;cmd.Env &#x3D; append(os.Environ(), envSlice...)NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it --name test -e test&#x3D;123 -e luck&#x3D;test busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;test&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#x2F; #  env | grep testtest&#x3D;123luck&#x3D;test</code></pre><p>可以看到，手动指定的环境变量在容器内可见。后面创建一个后台运行的容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:19:31+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9649354121   test        29524       running     top         2023-05-11 14:19:31# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 29524&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top    7 root      0:00 sh    8 root      0:00 ps -ef&#x2F; # env | grep test&#x2F; #</code></pre><p>查看环境变量，没有我们设置的环境变量。</p><p>这里不能用 env 命令获取设置的环境变量，原因是 exec 可以说 go发起的另一个进程，这个进程的父进程是宿主机的，这个，并不是容器内的。在cgo 内使用了 setns系统调用，才使得进程进入了容器内部的命名空间，但由于环境变量是继承自父进程的，因此这个exec 进程的环境变量其实是继承自宿主机，所以在 exec看到的环境变量其实是宿主机的环境变量。</p><p>但只要是容器内 pid 为 1的进程，创造出来的进程都会继承它的环境变量，下面来修改 exec命令来直接使用 env 命令来查看容器内环境变量的功能。</p><h5 id="修改-exec-命令">6.8.4 修改 exec 命令</h5><p>提供一个函数，可根据指定的 pid 来获取对应进程的环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getEnvsByPid(pid string) []string &#123;path :&#x3D; fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;environ&quot;, pid)contentBytes ,err :&#x3D; ioutil.ReadFile(path)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, path, err)return nil&#125;&#x2F;&#x2F; divide by &#39;\u0000&#39;envs :&#x3D; strings.Split(string(contentBytes),&quot;\u0000&quot;)return envs&#125;</code></pre><p>由于进程存放环境变量的位置是<code>/proc/$&#123;pid&#125;/environ</code>，因此根据给定的 pid去读取这个文件，可以获取环境变量，在文件的描述中，每个环境变量之间通过<code>\u0000</code> 分割，因此可以以此标记来获取环境变量数组。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;&#x2F;&#x2F; get target pid environ (container environ)containerEnvs :&#x3D; getEnvsByPid(pid)&#x2F;&#x2F; set host environ and container environ to exec processcmd.Env &#x3D; append(os.Environ(), containerEnvs...)if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>这里由于 exec命令依然要宿主机的一些环境变量，因此将宿主机环境变量和容器环境变量都一起放置到exec 进程中：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:03+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9729397397   test        50040       running     top         2023-05-11 14:30:03# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 50040&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#x2F; # env | grep testtest&#x3D;123luck&#x3D;test&#x2F; #</code></pre><p>现在可以看到 exec 进程可以获取前面 run 时设置的环境变量了。</p><h2 id="四网络篇">四、网络篇</h2><h3 id="容器网络">7. 容器网络</h3><h4 id="网络虚拟化技术">7.1 网络虚拟化技术</h4><h5 id="linux-虚拟网络设备">7.1.1 Linux 虚拟网络设备</h5><p>Linux是用网络设备去操作和使用网卡的，系统装了一个网卡后就会为其生成一个网络设备实例，例如eth0。Linux支持创建出虚拟化的设备，可通过组合实现多种多样的功能和网络拓扑，这里主要介绍Veth 和 Bridge。</p><p><strong>Linux Veth</strong></p><p>Veth 时成对出现的虚拟网络设备，发送到 Veth一端虚拟设备的请求会从另一端的虚拟设备中发出。容器的虚拟化场景中，常会使用Veth 连接不同的网络 namespace：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip netns add ns2# ip link add veth0 type veth peer name veth1# ip link set veth0 netns ns1# ip link set veth1 netns ns2# ip netns exec ns1 ip link1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:004: veth0@if3: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;ether 02:bf:18:99:77:ed brd ff:ff:ff:ff:ff:ff link-netns ns2</code></pre><p>在 ns1 和 ns2 的namespace 中，除 loopback的设备以外就只看到了一个网络设备。当请求发送到这个虚拟网络设备时，都会原封不动地从另一个网络namespace的网络接口中出来。例如，给两端分别配置不同地址后，向虚拟网络设备的一端发送请求，就能达到这个虚拟网络设备对应的另一端。</p><p><img src="0x0035/7.1.1-veth.png" style="zoom:43%;" /></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns exec ns1 ifconfig veth0 172.18.0.2&#x2F;24 up# ip netns exec ns2 ifconfig veth1 172.18.0.3&#x2F;24 up# ip netns exec ns1 route add default dev veth0# ip netns exec ns2 route add default dev veth1# ip netns exec ns1 ping -c 1 172.18.0.3PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.64 bytes from 172.18.0.3: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.395 ms--- 172.18.0.3 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.395&#x2F;0.395&#x2F;0.395&#x2F;0.000 ms</code></pre><p><strong>Linux Bridge</strong></p><p>进行下一步之前，先删除上一小节创建的 netns：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns del ns1# ip netns del ns2# ip netns list</code></pre><p>此时之前创建的两个 netns 被删除。</p><p>Bridge虚拟设备时用来桥接的网络设备，相当于现实世界的交换机，可以连接不同的网络设备，当请求达到Bridge 设备时，可以通过报文中的 Mac 地址进行广播或转发。例如，创建一个Bridge 设备，来连接 namespace 中的网络设备和宿主机上的网络：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip link add veth0 type veth peer name veth1# ip link set veth1 netns ns1########## 创建网桥# brctl addbr br0########## 挂载网络设备# brctl addif br0 eth0# brctl addif bro veth0</code></pre><p><img src="0x0035/7.1.1-bridge.png" /></p><h5 id="linux-路由表">7.1.2 Linux 路由表</h5><p>路由表是 Linux 内核的一个模块，通过定义路由表来决定在某个网络namespace 中包的流向，从而定义请求会到哪个网络设备上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip link set veth0 up# ip link set br0 up# ip netns exec ns1 ifconfig veth1 172.18.0.2&#x2F;24 up# ip netns exec ns1 route add default dev veth1# route add -net 172.18.0.0&#x2F;24 dev br0</code></pre><p><img src="0x0035/7.1.2-route.png" /></p><p>通过设置路由，对 IP地址的请求就能正确被路由到对应的网络设备上，从而实现通信：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ifconfig eth0eth0: flags&#x3D;4163&lt;UP,BROADCAST,RUNNING,MULTICAST&gt;  mtu 1500        inet 172.31.93.218  netmask 255.255.240.0  broadcast 172.31.95.255        inet6 fe80::215:5dff:fe4e:a16a  prefixlen 64  scopeid 0x20&lt;link&gt;        ether 00:15:5d:4e:a1:6a  txqueuelen 1000  (Ethernet)        RX packets 829  bytes 394161 (394.1 KB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 90  bytes 10335 (10.3 KB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0########## 在namespace访问宿主机# ip netns exec ns1 ping -c 1 172.31.93.218PING 172.31.93.218 (172.31.93.218) 56(84) bytes of data.64 bytes from 172.31.93.218: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.556 ms--- 172.31.93.218 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.556&#x2F;0.556&#x2F;0.556&#x2F;0.000 ms######### 从宿主机访问namespace的网络地址# ping -c 1 172.18.0.2PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.64 bytes from 172.18.0.2: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.113 ms--- 172.18.0.2 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.113&#x2F;0.113&#x2F;0.113&#x2F;0.000 ms</code></pre><h5 id="linux-iptables">7.1.3 Linux iptables</h5><p>iptables 是对 Linux 内核的 netfilter模块进行操作和展示的工具，用来管理包的流动和转送。iptables定义了一套链式处理的结构，在网络包传输的各个阶段可以使用不同的策略和包进行加工、传送或丢弃。在容器虚拟化技术里，常会用到两种策略，MASQUERADE和 DNAT，用于容器和宿主机外部的网络通信。</p><p><strong>MASQUERADE</strong></p><p>MASQUERADE 策略可以将请求包中的源地址转换为一个网络设备的地址，例如<a href="#7.1.2%20Linux%20路由表">7.1.2 Linux 路由表</a>这一小节里，namespace 中网络设备的地址是172.18.0.2，这个地址虽然在宿主机可以路由到 br0的网桥，但是到底宿主机外部后，是不知道如何路由到这个 IP的，所以如果请求外部地址的话，要先通过 MASQUERADE 策略将这个 IP转换为宿主机出口网卡的 IP：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># sysctl -w net.ipv4.conf.all.forwarding&#x3D;1net.ipv4.conf.all.forwarding &#x3D; 1# iptables -t nat -A POSTROUTING -s 172.18.0.0&#x2F;24 -o eth0 -j MASQUERADE</code></pre><p>在 namespace 中请求宿主机外部地址时，将 namespace中源地址转换为宿主机的地址作为源地址，就可以在 namespace中访问宿主机外的网络了。</p><p><strong>DAT</strong></p><p>iptables 中的 DNAT策略也是做网络地址的转换，不过它是要更换目标地址，常用于将内部网络地址的端口映射出去。例如，上面例子的namespace如果要提供服务给宿主机之外的应用要怎么办呢？外部应用没办法直接路由到172.18.0.2 这个地址，这时候可以用 DNAT 策略。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80</code></pre><p>这样就可以把宿主机上的 80 端口的 TCP 请求转发到 namespace 的172.18.0.2:80，从而实现外部应用的调用。</p><h4 id="构建容器网络模型">7.2 构建容器网络模型</h4><h5 id="基本模型">7.2.1 基本模型</h5><h6 id="网络">网络</h6><p>网络是容器的一个集合，在这个网络上的容器可以相互通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Network struct &#123;    Name    string &#x2F;&#x2F; network name    IpRange *net.IPNet &#x2F;&#x2F; address    Driver  string &#x2F;&#x2F; network driver name&#125;</code></pre><h6 id="网络端点">网络端点</h6><p>网络端点用于连接网络与容器，保证容器内部与网络的通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Endpoint struct &#123;ID          string           &#96;json:&quot;id&quot;&#96;Device      netlink.Veth     &#96;json:&quot;dev&quot;&#96;IPAddress   net.IP           &#96;json:&quot;ip&quot;&#96;MacAddress  net.HardwareAddr &#96;json:&quot;mac&quot;&#96;Network     *NetworkPortMapping []string&#125;</code></pre><p>网络端点的信息传输需要靠网络功能的两个组件配合完成，分别为网络驱动和IPAM。</p><h6 id="网络驱动">网络驱动</h6><p>网络驱动是网络功能的一个组件，不同驱动对网络的创建、连接、销毁策略不同，通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type NetworkDriver interface &#123;Name() string &#x2F;&#x2F; driver nameCreate(subnet string, name string) (*Network, error)Delete(network Network) errorConnect(network *Network, endpoint *Endpoint) errorDisconnect(network Network, endpoint *Endpoint) error&#125;</code></pre><h6 id="ipam">IPAM</h6><p>IPAM 也是网络功能的一个组件，用于网络 IP 地址的分配和释放，包括容器的IP 和网络网关的 IP。主要功能如下：</p><ul><li><code>ipam.Allocate(*net.IPNet)</code> 从指定的 subnet 网段中分配IP　</li><li><code>ipam.Release(*net.IPNet, net.IP)</code> 从指定的 subnet网段中释放掉指定的 IP</li></ul><p>在构建下面的函数之前，先来补充一些书上没写的：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (defaultNetworkPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;network&#x2F;&quot; &#x2F;&#x2F; 默认网络配置信息存储位置drivers            &#x3D; map[string]NetworkDriver&#123;&#125; &#x2F;&#x2F; 驱动字典，存储驱动信息networks           &#x3D; map[string]*Network&#123;&#125; &#x2F;&#x2F; 网络字段，存储网络信息)</code></pre><h5 id="调用关系">7.2.2 调用关系</h5><h6 id="创建网络">创建网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateNetwork(driver, subnet, name string) error &#123;_, cidr, _ :&#x3D; net.ParseCIDR(subnet)    &#x2F;&#x2F; allocate gateway ip by IPAMgatewayIP, err :&#x3D; ipAllocator.Allocate(cidr)if err !&#x3D; nil &#123;return err&#125;cidr.IP &#x3D; gatewayIPnw, err :&#x3D; drivers[driver].Create(cidr.String(), name)if err !&#x3D; nil &#123;return err&#125;    &#x2F;&#x2F; save network inforeturn nw.dump(defaultNetworkPath)&#125;</code></pre><p>其中，network.dump 和 network.load方法是将这个网络的配置信息保存在文件系统中，或从网络的配置目录中的文件读取到网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) dump(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(dumpPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(dumpPath, 0644)&#125; else &#123;return err&#125;&#125;nwPath :&#x3D; path.Join(dumpPath, nw.Name)    &#x2F;&#x2F; create file while empty file, write only, no filenwFile, err :&#x3D; os.OpenFile(nwPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;defer nwFile.Close()nwJson, err :&#x3D; json.Marshal(nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;_, err &#x3D; nwFile.Write(nwJson)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;return nil&#125;func (nw *Network) load(dumpPath string) error &#123;nwConfigFile, err :&#x3D; os.Open(dumpPath)if err !&#x3D; nil &#123;return err&#125;defer nwConfigFile.Close()nwJson :&#x3D; make([]byte, 2000)n, err :&#x3D; nwConfigFile.Read(nwJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(nwJson[:n], nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error load nw info: %v&quot;, err)return err&#125;return nil&#125;</code></pre><h6 id="创建容器并连接网络">创建容器并连接网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Connect(networkName string, cinfo *container.ContainerInfo) error &#123;network, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;ip, err :&#x3D; ipAllocator.Allocate(network.IpRange)if err !&#x3D; nil &#123;return err&#125;ep :&#x3D; &amp;Endpoint&#123;ID:          fmt.Sprintf(&quot;%s-%s&quot;, cinfo.Id, networkName),IPAddress:   ip,Network:     network,PortMapping: cinfo.PortMapping,&#125;if err &#x3D; drivers[network.Driver].Connect(network, ep); err !&#x3D; nil &#123;return err&#125;if err &#x3D; configEndpointIpAddressAndRoute(ep, cinfo); err !&#x3D; nil &#123;return err&#125;return configPortMapping(ep, cinfo)&#125;</code></pre><h6 id="展示网络列表">展示网络列表</h6><p>从网络配置的目录中加载所有的网络配置信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Init() error &#123;var bridgeDriver &#x3D; BridgeNetworkDriver&#123;&#125;drivers[bridgeDriver.Name()] &#x3D; &amp;bridgeDriverif _, err :&#x3D; os.Stat(defaultNetworkPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(defaultNetworkPath, 0644)&#125; else &#123;return err&#125;&#125;filepath.Walk(defaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error &#123;         &#x2F;&#x2F; skip if dirif info.IsDir() &#123;return nil&#125;if strings.HasSuffix(nwPath, &quot;&#x2F;&quot;) &#123;return nil&#125;         &#x2F;&#x2F; load filename as network name_, nwName :&#x3D; path.Split(nwPath)nw :&#x3D; &amp;Network&#123;Name: nwName,&#125;if err :&#x3D; nw.load(nwPath); err !&#x3D; nil &#123;logrus.Errorf(&quot;error load network: %s&quot;, err)&#125;&#x2F;&#x2F; save network info to network dicnetworks[nwName] &#x3D; nwreturn nil&#125;)return nil&#125;</code></pre><p>遍历展示创建的网络：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListNetwork() &#123;w :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprint(w, &quot;NAME\tIpRange\tDriver\n&quot;)for _, nw :&#x3D; range networks &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\n&quot;,nw.Name,nw.IpRange.String(),nw.Driver,)&#125;if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;Flush error %v&quot;, err)return&#125;&#125;</code></pre><h6 id="删除网络">删除网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteNetwork(networkName string) error &#123;nw, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;if err :&#x3D; ipAllocator.Release(nw.IpRange, &amp;nw.IpRange.IP); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network gateway ip: %s&quot;, err)&#125;if err :&#x3D; drivers[nw.Driver].Delete(*nw); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network DriverError: %s&quot;, err)&#125;return nw.remove(defaultNetworkPath)&#125;</code></pre><p>删除网络的同时也删除配置目录的网络配置文件：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) remove(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(path.Join(dumpPath, nw.Name)); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125; else &#123;return os.Remove(path.Join(dumpPath, nw.Name))&#125;&#125;</code></pre><h4 id="容器地址分配">7.3 容器地址分配</h4><p>现在转到 <code>ipam.go</code>。</p><h5 id="数据结构定义">7.3.1 数据结构定义</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ipamDefaultAllocatorPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;ipam&#x2F;subnet.json&quot;type IPAM struct &#123;SubnetAllocatorPath stringSubnets             *map[string]string&#125;&#x2F;&#x2F; 初始化一个IPAM对象，并指定默认分配信息存储位置var ipAllocator &#x3D; &amp;IPAM&#123;SubnetAllocatorPath: ipamDefaultAllocatorPath,&#125;</code></pre><p>反序列化读取网段分配信息和序列化保存网段分配信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) load() error &#123;if _, err :&#x3D; os.Stat(ipam.SubnetAllocatorPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.Open(ipam.SubnetAllocatorPath)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()subnetJson :&#x3D; make([]byte, 2000)n, err :&#x3D; subnetConfigFile.Read(subnetJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(subnetJson[:n], ipam.Subnets)if err !&#x3D; nil &#123;logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)return err&#125;return nil&#125;func (ipam *IPAM) dump() error &#123;ipamConfigFileDir, _ :&#x3D; path.Split(ipam.SubnetAllocatorPath)if _, err :&#x3D; os.Stat(ipamConfigFileDir); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(ipamConfigFileDir, 0644)&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()ipamConfigJson, err :&#x3D; json.Marshal(ipam.Subnets)if err !&#x3D; nil &#123;return err&#125;_, err &#x3D; subnetConfigFile.Write(ipamConfigJson)if err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h5 id="地址分配">7.3.2 地址分配</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) &#123;ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;err &#x3D; ipam.load()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error dump allocation info, %v&quot;, err)&#125;_, subnet, _ &#x3D; net.ParseCIDR(subnet.String())one, size :&#x3D; subnet.Mask.Size()if _, exist :&#x3D; (*ipam.Subnets)[subnet.String()]; !exist &#123;        &#x2F;&#x2F; 用0填满网段的配置，1&lt;&lt;uint8(size-one)表示这个网段中有多少个可用地址        &#x2F;&#x2F; size-one时子网掩码后面的网络位数，2^(size-one)表示网段中的可用IP数        &#x2F;&#x2F; 2^(size-one)等价于1&lt;&lt;uint8(size-one)        (*ipam.Subnets)[subnet.String()] &#x3D; strings.Repeat(&quot;0&quot;, 1&lt;&lt;uint8(size-one))&#125;&#x2F;&#x2F; 这里的原理建议大家看看原著for c :&#x3D; range (*ipam.Subnets)[subnet.String()] &#123;if (*ipam.Subnets)[subnet.String()][c] &#x3D;&#x3D; &#39;0&#39; &#123;            ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])            &#x2F;&#x2F; go的字符串创建后不能修改，先用byte存储            ipalloc[c] &#x3D; &#39;1&#39;            (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)            &#x2F;&#x2F;             ip &#x3D; subnet.IP                        &#x2F;&#x2F; 通过网段的IP与上面的偏移相加得出分配的IP，由于IP是一个uint的一个数组，需要通过数组中的每一项加所需要的值，例 &#x2F;&#x2F; 如网段是172.16.0.0&#x2F;12，数组序号是65555，那就要在[172,16,0,0]上依次加            &#x2F;&#x2F; [uint8(65555 &gt;&gt; 24), uint8(65555 &gt;&gt; 16), uint8(65555 &gt;&gt; 8), uint(65555 &gt;&gt; 4)]，即[0,1,0,19]，            &#x2F;&#x2F; 那么获得的IP就是172.17.0.19            for t :&#x3D; uint(4); t &gt; 0; t-- &#123;                []byte(ip)[4-t] +&#x3D; uint8(c &gt;&gt; ((t - 1) * 8))            &#125;            &#x2F;&#x2F; 由于此处IP是从1开始分配的，所以最后再加1，最终得到分配的IP是172.16.0.20            ip[3]++            break&#125;&#125;ipam.dump()return&#125;</code></pre><h5 id="地址释放">7.3.3 地址释放</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error &#123;    ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;    _, subnet, _ &#x3D; net.ParseCIDR(subnet.String())    err :&#x3D; ipam.load()    if err !&#x3D; nil &#123;        logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)    &#125;    c :&#x3D; 0    &#x2F;&#x2F; 将IP转换为4个字节的表示方式    releaseIP :&#x3D; ipaddr.To4()    &#x2F;&#x2F; 由于IP是从1开始分配的，所以转换成索引减1    releaseIP[3] -&#x3D; 1    for t :&#x3D; uint(4); t &gt; 0; t -&#x3D; 1 &#123;        &#x2F;&#x2F; 和分配IP相反，释放IP获得索引的方式是IP的每一位相减后分别左移将对应的数值加到索引上        c +&#x3D; int(releaseIP[t-1]-subnet.IP[t-1]) &lt;&lt; ((4 - t) * 8)    &#125;    ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])    ipalloc[c] &#x3D; &#39;0&#39;    (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)    ipam.dump()    return nil&#125;</code></pre><p>根据书上，写到这里就开始测试了，但是我们看看IDE，红海一片，所以我们接着实现。</p><h4 id="创建-bridge-网络">7.4 创建 bridge 网络</h4><h5 id="实现-bridge-driver-create">7.4.1 实现 Bridge Driver Create</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) &#123;ip, ipRange, _ :&#x3D; net.ParseCIDR(subnet)ipRange.IP &#x3D; ipn :&#x3D; &amp;Network&#123;Name:    name,IpRange: ipRange,Driver:  d.Name(),&#125;err :&#x3D; d.initBridge(n)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error init bridge: %v&quot;, err)&#125;return n, err&#125;</code></pre><h5 id="bridge-driver-初始化-linux-bridge">7.4.2 Bridge Driver 初始化Linux Bridge</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) initBridge(n *Network) error &#123;&#x2F;&#x2F; 创建bridge虚拟设备bridgeName :&#x3D; n.Nameif err :&#x3D; createBridgeInterface(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;eror add bridge: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置bridge设备的地址和路由gatewayIP :&#x3D; *n.IpRangegatewayIP.IP &#x3D; n.IpRange.IPif err :&#x3D; setInterfaceIP(bridgeName, gatewayIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error assigning address: %s on bridge: %s with an error of: %v&quot;, gatewayIP, bridgeName, err)&#125;&#x2F;&#x2F; 启动bridge设备if err :&#x3D; setInterfaceUP(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error set bridge up: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置iptables的SNAT规则if err :&#x3D; setupIPTables(bridgeName, n.IpRange); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error setting iptables for %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="创建-bridge-设备">创建 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func createBridgeInterface(bridgeName string) error &#123;_, err :&#x3D; net.InterfaceByName(bridgeName)if err &#x3D;&#x3D; nil || !strings.Contains(err.Error(), &quot;no such network interface&quot;) &#123;return err&#125;&#x2F;&#x2F; create *netlink.Bridge objectla :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; bridgeNamebr :&#x3D; &amp;netlink.Bridge&#123;LinkAttrs: la&#125;if err :&#x3D; netlink.LinkAdd(br); err !&#x3D; nil &#123;return fmt.Errorf(&quot;bridge creation failed for bridge %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="设置-bridge-设备的地址和路由">设置 bridge 设备的地址和路由</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceIP(name string, rawIP string) error &#123;retries :&#x3D; 2var iface netlink.Linkvar err errorfor i :&#x3D; 0; i &lt; retries; i++ &#123;iface, err &#x3D; netlink.LinkByName(name)if err &#x3D;&#x3D; nil &#123;break&#125;logrus.Debugf(&quot;error retrieving new bridge netlink link [ %s ]... retrying&quot;, name)time.Sleep(2 * time.Second)&#125;if err !&#x3D; nil &#123;return fmt.Errorf(&quot;abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot the error: %v&quot;, err)&#125;ipNet, err :&#x3D; netlink.ParseIPNet(rawIP)if err !&#x3D; nil &#123;return err&#125;addr :&#x3D; &amp;netlink.Addr&#123;IPNet:     ipNet,Peer:      ipNet,Label:     &quot;&quot;,Flags:     0,Scope:     0,Broadcast: nil,&#125;return netlink.AddrAdd(iface, addr)&#125;</code></pre><h6 id="启动-bridge-设备">启动 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceUP(interfaceName string) error &#123;iface, err :&#x3D; netlink.LinkByName(interfaceName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;error retrieving a link named [ %s ]: %v&quot;, iface.Attrs().Name, err)&#125;if err :&#x3D; netlink.LinkSetUp(iface); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error enabling interface for %s: %v&quot;, interfaceName, err)&#125;return nil&#125;</code></pre><h6 id="设置-iptables-linux-bridge-snat-规则">设置 iptables Linux BridgeSNAT 规则</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setupIPTables(bridgeName string, subnet *net.IPNet) error &#123;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE&quot;, subnet.String(), bridgeName)cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)&#125;return err&#125;</code></pre><h5 id="bridge-driver-delete-实现">7.4.3 Bridge Driver Delete 实现</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Delete(network Network) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;return netlink.LinkDel(br)&#125;</code></pre><h4 id="在-bridge-网络创建容器">7.5 在 bridge 网络创建容器</h4><h5 id="挂载容器端点">7.5.1 挂载容器端点</h5><h6 id="连接容器网络端点到-linux-bridge">连接容器网络端点到 LinuxBridge</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;la :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; endpoint.ID[:5]la.MasterIndex &#x3D; br.Attrs().Indexendpoint.Device &#x3D; netlink.Veth&#123;LinkAttrs: la,PeerName:  &quot;cif-&quot; + endpoint.ID[:5],&#125;if err &#x3D; netlink.LinkAdd(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;if err &#x3D; netlink.LinkSetUp(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;return nil&#125;</code></pre><h6 id="配置容器-namespace-中网络设备及路由">配置容器 Namespace中网络设备及路由</h6><p>回到 <code>network.go</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;peerLink, err :&#x3D; netlink.LinkByName(ep.Device.PeerName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;fail config endpoint: %v&quot;, err)&#125;defer enterContainerNetns(&amp;peerLink, cinfo)()interfaceIP :&#x3D; *ep.Network.IpRangeinterfaceIP.IP &#x3D; ep.IPAddressif err &#x3D; setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;%v,%s&quot;, ep.Network, err)&#125;if err &#x3D; setInterfaceUP(ep.Device.PeerName); err !&#x3D; nil &#123;return err&#125;if err &#x3D; setInterfaceUP(&quot;lo&quot;); err !&#x3D; nil &#123;return err&#125;_, cidr, _ :&#x3D; net.ParseCIDR(&quot;0.0.0.0&#x2F;0&quot;)defaultRoute :&#x3D; &amp;netlink.Route&#123;LinkIndex: peerLink.Attrs().Index,Gw:        ep.Network.IpRange.IP,Dst:       cidr,&#125;if err &#x3D; netlink.RouteAdd(defaultRoute); err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h6 id="进入容器-net-namespace">进入容器 Net Namespace</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() &#123;f, err :&#x3D; os.OpenFile(fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;net&quot;, cinfo.Pid), os.O_RDONLY, 0)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get container net namespace, %v&quot;, err)&#125;nsFD :&#x3D; f.Fd()runtime.LockOSThread()if err &#x3D; netlink.LinkSetNsFd(*enLink, int(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set link netns , %v&quot;, err)&#125;origns, err :&#x3D; netns.Get()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get current netns, %v&quot;, err)&#125;if err &#x3D; netns.Set(netns.NsHandle(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set netns, %v&quot;, err)&#125;return func() &#123;netns.Set(origns)origns.Close()runtime.UnlockOSThread()f.Close()&#125;&#125;</code></pre><h6 id="配置宿主机到容器的端口映射">配置宿主机到容器的端口映射</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configPortMapping(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;for _, pm :&#x3D; range ep.PortMapping &#123;portMapping :&#x3D; strings.Split(pm, &quot;:&quot;)if len(portMapping) !&#x3D; 2 &#123;logrus.Errorf(&quot;port mapping format error, %v&quot;, pm)continue&#125;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s&quot;,portMapping[0], ep.IPAddress.String(), portMapping[1])cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)continue&#125;&#125;return nil&#125;</code></pre><h5 id="修补-bug">7.5.2 修补 bug</h5><p>写到这里，代码还是有很多 bug的，例如，<code>BridgeNetworkDriver</code> 未完全继承<code>NetworkDriver</code> 的所有函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Disconnect(network Network, endpoint *Endpoint) error &#123;return nil&#125;</code></pre><h5 id="测试">7.5.3 测试</h5><p>现在终于可以测试了。</p><p>首先创建一个网桥：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . network create --driver bridge --subnet 192.168.10.1&#x2F;24 testbridge</code></pre><p>然后启动两个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;8116248511&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#x2F; # ifconfigcif-81162 Link encap:Ethernet  HWaddr 16:62:68:81:E0:A9          inet addr:192.168.10.2  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::1462:68ff:fe81:e0a9&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:14 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1820 (1.7 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; #</code></pre><p>记住这个 IP：<code>192.168.10.2</code>，然后进入另一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;9558830402&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#x2F; # ifconfigcif-95588 Link encap:Ethernet  HWaddr 42:18:0A:73:33:CA          inet addr:192.168.10.3  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::4018:aff:fe73:33ca&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:10 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1248 (1.2 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; # ping 192.168.10.2PING 192.168.10.2 (192.168.10.2): 56 data bytes64 bytes from 192.168.10.2: seq&#x3D;0 ttl&#x3D;64 time&#x3D;2.619 ms64 bytes from 192.168.10.2: seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.086 ms^C--- 192.168.10.2 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 0.086&#x2F;1.352&#x2F;2.619 ms&#x2F; #</code></pre><p>可以看到，两个容器网络互通。</p><p>下面来试一下访问外部网络。我用的 WSL，默认的 nat是关闭的，前期各种设置 iptables规则什么的，都无法访问容器外部的网络，直到发现一篇帖子里说到，需要打开内核的nat功能，要将文件<code>/proc/sys/net/ipv4/ip_forward</code>内的值改为1（默认是0）。执行<code>sysctl -w net.ipv4.ip_forward=1</code> 即可。</p><p>修改之后，继续测试。</p><p>容器默认是没有 DNS 服务器的，需要我们手动添加：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # ping cn.bing.comping: bad address &#39;cn.bing.com&#39;&#x2F; # echo -e &quot;nameserver 8.8.8.8&quot; &gt; &#x2F;etc&#x2F;resolv.conf&#x2F; # ping cn.bing.comPING cn.bing.com (202.89.233.101): 56 data bytes64 bytes from 202.89.233.101: seq&#x3D;0 ttl&#x3D;113 time&#x3D;38.419 ms64 bytes from 202.89.233.101: seq&#x3D;1 ttl&#x3D;113 time&#x3D;39.011 ms^C--- cn.bing.com ping statistics ---3 packets transmitted, 2 packets received, 33% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 38.419&#x2F;38.715&#x2F;39.011 ms&#x2F; #</code></pre><p>然后再来测试容器映射端口到宿主机供外部访问：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -p 90:90 -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;3445154844&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#x2F; # nc -lp 90</code></pre><p>然后访问宿主机的 80 端口，看看能不能转发到容器里：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 172.31.93.218 90Trying 172.31.93.218...telnet: Unable to connect to remote host: Connection refused</code></pre><p>开始我以为是我哪里码错了，然后拿作者的代码来跑，并放到虚拟机上跑，发现并不是自己的问题，那只能这样测试了：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 192.168.10.3 90Trying 192.168.10.3...Connected to 192.168.10.3.Escape character is &#39;^]&#39;.</code></pre><p>出现这样的字眼后，容器和宿主机之间就可以通信了。</p><h2 id="参考链接">参考链接</h2><p><a href="https://learnku.com/articles/42072">七天用 Go 写个docker（第一天） | Go 技术论坛 (learnku.com)</a></p><p><a href="https://juejin.cn/post/6971335828060504094">使用 GoLang从零开始写一个 Docker（概念篇）-- 《自己动手写 Docker》读书笔记 - 掘金(juejin.cn)</a></p><p><ahref="https://blog.xtlsoft.top/read/server/building-wsl-kernel-with-aufs.html">编译带有AUFS 支持的 WSL 内核 - 徐天乐 :: 个人博客 (xtlsoft.top)</a></p><p><ahref="https://zhuanlan.zhihu.com/p/324530180">如何让WSL2使用自己编译的内核- 知乎 (zhihu.com)</a></p><p><ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p><ahref="https://juejin.cn/post/7086069688664326157#heading-1">自己动手写Docker系列-- 5.7实现通过容器制作镜像 - 掘金 (juejin.cn)</a></p><p><ahref="https://blog.csdn.net/tycoon1988/article/details/40781291">iptable端口重定向MASQUERADE_tycoon1988的博客-CSDN博客</a></p><p>不过这个运行方式不能进行交互，我们可以使用这个命令来验证我们写的docker 是否与宿主机隔离：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it &#x2F;bin&#x2F;sh</code></pre><h2 id="零前言-2">零、前言</h2><p>本文为《自己动手写 Docker》的学习，对于各位学习 docker的同学非常友好，非常建议买一本来学习。</p><p>书中有摘录书中的一些知识点，不过限于篇幅，没有全部摘录<del>(主要也是懒)</del>。项目仓库地址为：<ahref="https://github.com/JaydenChang/simple-docker">JaydenChang/simple-docker(github.com)</a></p><h2 id="一概念篇-2">一、概念篇</h2><h3 id="基础知识-2">1. 基础知识</h3><h4 id="kernel-2">1.1 kernel</h4><p>kernel (内核)指大多数操作系统的核心部分，由操作系统中用于管理存储器、文件、外设和系统资源的部分组成。操作系统内核通常运行进程，并提供进程间通信。</p><h4 id="namespace-2">1.2 namespace</h4><p>namespace 是 Linux 自带的功能来隔离内核资源的机制。</p><p>Linux 中有 6 种 namespace</p><h5 id="uts-namespace-2">1.2.1 UTS Namespace</h5><p>UTS，UNIX Time Sharing，用于隔离 nodeName (主机名) 和 domainName(域名)。在该 Namespace 下修改 hostname 不会影相其他的 Namespace。</p><h5 id="ipc-namespace-2">1.2.2 IPC Namespace</h5><p>IPC，Inter-Process Communication (进程间通讯)，用于隔离 System V IPC和 POSIX message queues (一种消息队列，结构为链表)。</p><p>两种 IPC 本质上差不多，System V IPC 随内核持续，POSIX IPC随进程持续。</p><h5 id="pid-namespace-2">1.2.3 PID Namespace</h5><p>PID，Process IDs，用于隔绝 PID。同样的进程，在不同 Namespace里是不同的 PID。新建的 PID Namespace 里第一个 PID 是1。</p><h5 id="mount-namespace-2">1.2.4 Mount Namespace</h5><p>用于隔绝文件系统，挂载了某一目录，在这个 Namespace下就会把这个目录当作根目录，我们看到的文件系统树就会以这个目录为根目录。</p><p>mount 操作本身不会影响到外部，docker 中的 volume也用到了这个特性。</p><h5 id="user-namespace-2">1.2.5 User Namespace</h5><p>用于 隔离用户组 ID。</p><h5 id="network-namespace-2">1.2.6 Network Namespace</h5><p>每个 Namespace 都有一套自己的网络设备，可以使用相同的端口号，映射到host 的不同端口。</p><h4 id="linux-cgroups-2">1.3 Linux Cgroups</h4><p>Cgroups 全称为 Control Groups，是 Linux内核提供的物理资源隔离机制。</p><h5 id="cgroups-的三个组件-2">1.3.1 Cgroups 的三个组件</h5><ul><li>cgroup：一个 cgroup 包含一组进程，且可以有 subsystem的参数配置，以关联一组 subsystem。</li><li>subsystem：一组资源控制的模块。</li><li>hierarchy：把一组 cgroups 串成一个树状结构，以提供继承的功能。</li></ul><h5 id="这三个组件的关联-2">1.3.2 这三个组件的关联</h5><p>Linux 有一些限制：</p><ul><li>首先，创建一个 hierarchy。这个 hierarchy 有一个 cgroup根节点，所有的进程都会被加到这个根节点上，所有在这个 hierarchy上创建的节点都是这个根节点的子节点。</li><li>一个 subsystem 只能加到一个 hierarchy 上。</li><li>但是一个 subsystem 可以加到同一个 hierarchy 的多个 cgroups 上。</li><li>一个 hierarchy 可以有多个 subsystem。</li><li>一个进程可以在多个 cgroups 中，但是这些 cgroup 必须在不同的hierarchy 中。</li><li>一个进程 fork 出子进程时，父进程和子进程属于同一个 cgroup。</li></ul><h5 id="cgroup-和-subsystem-和-hierarchy-之间的联系-2">1.3.3 cgroup 和subsystem 和 hierarchy 之间的联系</h5><ul><li>hierarchy 就是一颗 cgroups 树，由多个 cgroups 构成。每一个 hierarchy建立时会包含 ==<em>所有</em>== 的Linux 进程。这里的 “所有”就是当前系统运行中的所有进程，每个 hierarchy上的全部进程都是一样的，不同的 hierarchy指的其实只是不同的分组方式，这也是为什么一个进程可以存在于多个 hierarchy上；准确来说，一个进程一定会同时存在于所有的 hierarchy上，区别在被放在的 cgroup 可能会有差异。</li><li>Linux 的 subsystem 只有一个的说法，没有一种的说法，也就是在一个hierarchy 上使用了 memory subsystem，那么在其他 hierarchy 就不能使用memory subsystem 了。</li><li>subsystem 是一种资源控制器，有很多个 subsystem，每个 subsystem控制不同的资源。subsystem 和 cgroups 关联。新建一个 cgroups文件夹时，里面会自动生成一堆配置文件，那个就是 subsystem 配置文件。但<code>subsystem 配置文件</code> 不是 <code>subsystem</code>，就像<code>.git</code> 不是 <code>git</code> 一样，就像没安装 git也可以从别人那里获得 <code>.git</code>文件夹，只是不能用罢了。<code>subsystem 配置文件</code>也是如此，新建一个 cgroup 就会生成<code>cgroup 配置文件</code>，但并不代表你关联了一个subsystem。只有当改变了一个<code>cgroup 配置文件</code>，里面要限制某种资源时，就会自动关联到这个被限制的资源所对应的subsystem 上。</li><li>假设我的 Linux 有 12 个 subsystem，也就是说我最多只能建 12 个hierarchy (不加 subsystem 的情况下可以建更多 hierarchy，这样 cgroup就变成纯分组使用)。每一个 hierarchy 上一个 subsystem。如果在某个hierarchy 放多个 subsystem，能建立的 hierarchy就更少了。</li><li>subsystem 和 cgroup 是关联的，不是和 hierarchy关联的，但经常看到有人说把某个 subsystem 和某个 hierarchy关联。实质上一般指的是 hierarchy 中的某一个 cgroup 或多个 cgroup关联。</li></ul><h5 id="cgroup-的-kernel-接口-2">1.3.4 cgroup 的 kernel 接口</h5><p>kernel 接口，就是在 Linux 上调用 api 来控制 cgroups。</p><ol type="1"><li><p>首先创建一个 hierarchy，而 hierarchy要挂载到一个目录上，这里创建一个目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mkdir hierarchy-test</code></pre></li><li><p>然后挂载：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t cgroup -o none,name&#x3D;hierarchy-test hierarchy-test .&#x2F;hierarchy-test</code></pre></li><li><p>可以在这个目录下看到一大堆文件，这些文件就是 cgroup根节点的配置。</p></li><li><p>然后在这个目录下创建新的空目录，会发现，新的目录里也会有很多cgroup 配置文件，这些目录已成为 cgroup 根节点的子节点 cgroup。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">.├── cgroup.clone_children├── cgroup.procs├── cgroup.sane_behavior├── notify_on_release├── release_agent├── tasks└── temp  # 这是新创建的文件夹    ├── cgroup.clone_children    ├── cgroup.procs    ├── notify_on_release    └── tasks</code></pre></li><li><p>在 cgroup中添加和移动进程：系统的所有进程都会被放到根节点中，可以根据需要移动进程：</p><ul><li><p>只需将进程 ID 写到对应的 cgroup 的 tasks 文件即可。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo sh -c &quot;echo $$ &gt;&gt; tasks&quot;</code></pre><p>该命令就是将当前终端的这个进程加到当前所在的 cgroup 的目录的 tasks文件中。</p></li></ul></li><li><p>通过 subsystem 限制 cgroup 中进程的资源：</p><ul><li>上面的方法有个问题，因为这个 hierarchy 没有关联到任何subsystem，因此不能够控制资源。</li><li>不过其实系统会自动给每个 subsystem 创建一个hierarchy，所以通过控制这个 hierarchy里的配置，可以达到控制进程的目的。</li></ul></li></ol><h5 id="docker-是怎么使用-cgroups-的-2">1.3.5 docker 是怎么使用 Cgroups的</h5><p>docker 会给每个容器创建一个 cgroup，再限制该 cgroup的资源，从而达到限制容器的资源的作用。</p><p>其实写了这么多，综合上面的前置知识，不难猜测，docker的原理是：隔离主机。</p><h4 id="demo-2">1.4 Demo</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;fmt&quot;&quot;io&#x2F;ioutil&quot;&quot;os&quot;&quot;os&#x2F;exec&quot;&quot;path&quot;&quot;strconv&quot;&quot;syscall&quot;)const cgroupMemoryHierarchyCount &#x3D; &quot;&#x2F;sys&#x2F;fs&#x2F;cgroup&#x2F;memory&quot;func main() &#123;    &#x2F;&#x2F; 第二次会运行这段代码    &#x2F;&#x2F; 这段代码运行的地方就可以看做是一个简易的容器    &#x2F;&#x2F; 这里只是对进程进行了隔离    &#x2F;&#x2F; 但是可以看到 pid 已经变成了 1，因为我们有 PID Namespace    if os.Args[0] &#x3D;&#x3D; &quot;&#x2F;proc&#x2F;self&#x2F;exe&quot; &#123;        fmt.Printf(&quot;current pid %d\n&quot;, syscall.Getpid())        cmd :&#x3D; exec.Command(&quot;sh&quot;, &quot;-c&quot;, &#96;stress --vm-bytes 200m --vm-keep -m 1&#96;)        cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;&#125;        cmd.Stdin &#x3D; os.Stdin        cmd.Stdout &#x3D; os.Stdout        cmd.Stderr &#x3D; os.Stderr        if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;            fmt.Println(err)            os.Exit(1)        &#125;    &#125;        &#x2F;&#x2F; 第一次运行这段    &#x2F;&#x2F; **command 设置为当前进程，也就是这个 go 程序本身，也就是说 cmd.Start() 会再次运行该程序    cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;)    &#x2F;&#x2F; 在 start 之前，修改 cmd 的各种配置，也就是第二次运行这个程序的时候的配置&#x2F;&#x2F; 创建 namespace    cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr &#123;        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,    &#125;    cmd.Stdin &#x3D; os.Stdin    cmd.Stdout &#x3D; os.Stdout    cmd.Stderr &#x3D; os.Stderr        &#x2F;&#x2F; 因为之后要打印 process 的 id，所以用 start    &#x2F;&#x2F; 如果这里用 run 的话，那么 else 里的代码永远不会执行，因为 stress 永远不会结束    if err :&#x3D; cmd.Start(); err !&#x3D; nil &#123;        fmt.Println(&quot;Error&quot;, err)        os.Exit(1)    &#125; else &#123;        &#x2F;&#x2F; 打印 new process id        fmt.Printf(&quot;%v\n&quot;, cmd.Process.Pid)                &#x2F;&#x2F; 接下来三段对 cgroup 操作        &#x2F;&#x2F; the hierarchy has been already created by linux on the memory subsystem        &#x2F;&#x2F; create a sub cgroup           os.Mkdir(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,        ), 0755)                &#x2F;&#x2F; place container process in this cgroup        ioutil.WriteFile(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;tasks&quot;,        ), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)                &#x2F;&#x2F; restrict the stress process on this cgroup        ioutil.WriteFile(path.Join(        cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;memory.limit_int_bytes&quot;,        ), []byte(&quot;100m&quot;), 0644)                &#x2F;&#x2F; cmd.Start() 不会等待进程结束，所以需要手动等待        &#x2F;&#x2F; 如果不加的话，由于主进程结束了，子进程也会被强行结束        cmd.Process.Wait()    &#125;&#125;</code></pre><h4 id="ufs-2">1.5 UFS</h4><h5 id="ufs-概念-2">1.5.1 UFS 概念</h5><p>UFS，Union File System，联合文件系统。docker 在下载一个 image文件时，会看到一次下载很多个文件，这就是UFS。它是一种分层、轻量、高性能的文件系统。UFS 类似git，每次修改文件时，都是一次提交，并有记录，修改都反映在一个新的文件上，而不是修改旧文件。</p><p>UFS 允许多个不同目录挂载到同一个虚拟文件系统下，这就是为什么 image之间可以共享文件，以及继承镜像的原因。</p><h5 id="aufs-2">1.5.2 AUFS</h5><p>AUFS，Advanced Union File System，是 UFS 的一个改动版本。</p><p>笔者本身使用的是 WSL 做日常开发，WSL 内核不支持AUFS，后面会提到更换内核。</p><h5 id="docker-和-aufs-2">1.5.3 docker 和 AUFS</h5><p>docker 在早期使用 AUFS，直到现在也可以选择作为一种存储驱动类型。</p><h5 id="image-layer-2">1.5.4 image layer</h5><p>image 由多层 read-only layer 构成。</p><p>当启动一个 container 时，就会在 image 上再加一层 init layer，initlayer 也是 read-only 的，用于储存容器的环境配置。此外，docker还会创建一个 read-write 的 layer，用于执行所有的写操作。</p><p>当停止容器时，这个 read-write layer 依然保留，只有删除 container时才会被删除。</p><p>那么，怎么删除旧文件呢？</p><p>docker 会在 read-write layer 生成一个<code>.wh.&lt;fileName&gt;</code> 文件来隐藏要删除的文件。</p><h5 id="实现一个-aufs-2">1.5.5 实现一个 AUFS</h5><p>我们先创建一个如下的文件夹结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt</code></pre><p>然后挂载到 mnt 文件夹上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t aufs -o dirs&#x3D;.&#x2F;container-layer:.&#x2F;image-layer none .&#x2F;mnt</code></pre><p>如果没有手动添加权限的话，默认 dirs 左边第一个文件夹有 write-read权限，其他都是 read-only。</p><p>我们可以发现，imageLayer1 和 writeLayer 的文件出现在 mnt文件夹下：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt    ├── container.txt    └── image.txt</code></pre><p>然后我们修改一下 image.txt的内容，然后再看看整个目录，会发现，<code>container-layer</code>目录下多了一个 <code>image.txt</code>，然后我们看看<code>container-layer</code> 的 <code>image.txt</code>的内容，有添加前后的的文字。</p><p>也就是说，实际上，当修改某一个 layer 的时候，实际上不会改变这个layer，而是将其复制到 container-layer 中，然后再修改这个新的文件。</p><h2 id="二容器篇-2">二、容器篇</h2><h3 id="linux-的-proc-文件夹-2">2. Linux 的 /proc 文件夹</h3><h4 id="pid-2">2.1 PID</h4><p>在 <code>/proc</code>文件夹下可以看到很多文件夹的名字都是个数字，其实就是个 PID。是 Linux为每个进程创建的空间。</p><h4 id="一些重要的目录-2">2.2 一些重要的目录</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F;proc&#x2F;N # PID 为 N 的进程&#x2F;proc&#x2F;N&#x2F;cmdline # 进程的启动命令&#x2F;proc&#x2F;N&#x2F;cwd # 链接到进程的工作目录&#x2F;proc&#x2F;N&#x2F;environ  # 进程的环境变量列表&#x2F;proc&#x2F;N&#x2F;exe # 链接到进程的执行命令&#x2F;proc&#x2F;N&#x2F;fd # 包含进程相关的所有文件描述符&#x2F;proc&#x2F;N&#x2F;maps # 与进程相关的内存映射信息&#x2F;proc&#x2F;N&#x2F;mem # 进程持有的内存，不可读&#x2F;proc&#x2F;N&#x2F;root # 链接到进程的根目录&#x2F;proc&#x2F;N&#x2F;stat # 进程的状态&#x2F;proc&#x2F;N&#x2F;statm # 进程的内存状态&#x2F;proc&#x2F;N&#x2F;status # 比上面两个更可读&#x2F;proc&#x2F;self # 链接到当前正在运行的进程</code></pre><h3 id="简单实现-2">3. 简单实现</h3><h4 id="工具-2">3.1 工具</h4><p>获取帮助编写 command line app 的工具：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go get github.com&#x2F;urfave&#x2F;cli </code></pre><h4 id="实现代码-2">3.2 实现代码</h4><p>代码结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── command.go├── container│   └── init.go├── dockerCommand│   └── run.go├── go.mod├── go.sum└── main.go</code></pre><h5 id="runcommand-2">3.2.1 runCommand</h5><p><code>command.go</code> 用于放置各种 command 命令，这里先只写一个runCommand 命令。</p><p>首先用 urfave/cli 创建一个 runCommand 命令：</p><pre class="line-numbers language-golang" data-language="golang"><code class="language-golang">&#x2F;&#x2F; command.govar runCommand &#x3D; cli.Command&#123;    Name:  &quot;run&quot;,    Usage: &quot;Create a container&quot;,    Flags: []cli.Flag&#123;        &#x2F;&#x2F; integrate -i and -t for convenience        &amp;cli.BoolFlag&#123;            Name:  &quot;it&quot;,            Usage: &quot;open an interactive tty(pseudo terminal)&quot;,        &#125;,    &#125;,    Action: func(context *cli.Context) error &#123;        args :&#x3D; context.Args()        if len(args) &#x3D;&#x3D; 0 &#123;            return errors.New(&quot;Run what?&quot;)        &#125;        cmdArray :&#x3D; args.Get(0)        &#x2F;&#x2F; command        &#x2F;&#x2F; check whether type &#96;-it&#96;        tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal                &#x2F;&#x2F; 这个函数在下面定义        dockerCommand.Run(tty, cmdArray)        return nil    &#125;,&#125;</code></pre><h5 id="run-2">3.2.2 run</h5><p>上面的 Run 函数在 <code>dockerCommand/run.go</code> 下定义。当运行<code>docker run</code> 时，实际上主要是 Action 下的这个函数在工作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; dockerCommand&#x2F;run.go&#x2F;&#x2F; This is the function what &#96;docker run&#96; will callfunc Run(tty bool, cmdArray string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess :&#x3D; container.NewProcess(tty, cmdArray)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil&#123;logrus.Error(err)&#125;initProcess.Wait()os.Exit(-1)&#125;</code></pre><p>但其实这个函数做的也只是去跑一个 initProcess。这个 command process在另一个包里定义。</p><h5 id="newprocess-2">3.2.3 NewProcess</h5><p>上面提到的 <code>container.NewProcess</code> 在<code>container/init.go</code> 里定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; container&#x2F;init.gofunc NewProcess(tty bool, cmdArray string) *exec.Cmd &#123;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is the below exported function&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;args :&#x3D; []string&#123;&quot;init&quot;, cmdArray&#125;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, args...)&#x2F;&#x2F; new namespaces, thanks to Linuxcmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; this is what presudo terminal means&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;return cmd&#125;</code></pre><p>这个函数的作用是生成一个新的 command process，但这个 command 是<code>/proc/self/exe</code>这个程序本身，也就是，我们最后生成的可执行文件，但这次我们不运行<code>docker run</code>，而是 <code>docker init</code>，这个 init命令在下面定义。</p><h5 id="init-2">3.2.4 init</h5><p>initCommand 和 runCommand 在同一个文件里定义，也是一个command，但是注意这个 command 不面向用户，只用于协助 runCommand。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; command.go&#x2F;&#x2F; docker init, but cannot be used by uservar initCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;Start initiating...&quot;)cmdArray :&#x3D; context.Args().Get(0)logrus.Infof(&quot;container command: %v&quot;, cmdArray)return container.InitProcess(cmdArray, nil)&#125;,&#125;</code></pre><p>这里使用了 container.InitProcess函数，这个函数是真正用于容器初始化的函数。</p><h5 id="initprocess-2">3.2.5 InitProcess</h5><p>这里的是 InitProcess，也就是容器初始化的步骤。</p><p>注意 syscall.Exec 这里：</p><ul><li>就是 <code>mount /</code> 并指定 private，不然容器里的 proc会使用外面的 proc，即使在不同 namespace 下。</li><li>所以如果没有加这一段，其实退出容器后还需要在外面再次 mount proc才能使用 ps 等命令</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initiate the containerfunc InitProcess(cmdArray string, args []string) error &#123;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV                &#x2F;&#x2F; mountif err :&#x3D; syscall.Mount(&quot;&quot;, &quot;&#x2F;&quot;, &quot;&quot;, syscall.MS_PRIVATE|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F; fails: %v&quot;, err)return err&#125;        &#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)argv :&#x3D; []string&#123;cmdArray&#125;if err :&#x3D; syscall.Exec(cmdArray, argv, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc fails: %v&quot;, err)&#125;return nil&#125;</code></pre><p>一般来说，我们都是想要这个 cmdArray 作为 PID=1 的进程。but，我们有initProcess 本身的存在，所以 PID = 1 的其实是 initProcess，那如何让cmdArray 作为 PID=1 的存在呢？</p><p>这里有一个 syscall.Exec 神器，Exec 内部会调用 kernel 的 execve函数，这个函数会把当前进程上运行的程序替换为另一个程序，这正是我们想要的，在不改变PID 的情况下，替换程序 (即使 kill PID 为 1 的进程，新创建的进程也会是PID=2)。</p><p>为什么要第一个命令的 PID 为 1？</p><ul><li>因为这样，退出这个进程后，容器就会因为没有前台进程，而自动退出，这也是docker 的特性。</li></ul><h3 id="给-docker-run-增加对容器的资源限制功能-2">4. 给 docker run增加对容器的资源限制功能</h3><p>这里要用到 subsystem 的知识。</p><h4 id="subsystem.go-2">4.1 subsystem.go</h4><ul><li>根据 subsystem 的特性，和接口很搭。</li><li>此外再定义一个 ResourceConfig 的类型，用于放置资源控制的配置。</li><li>subsystemInstance 里包括 3 个 subsystem，分别对memory，cpu，cpushare进行限制。因为我们只需要对整个容器进行限制，所以这一套 3 个够了。</li></ul><p>看到这里，有个 cpu，cpushare，cpuset 等等，有点晕，查了下，有关 CPU的 cgroup subsystem，这里列举常见的 3 个：</p><ul><li>cpu：经常看到的 cpushares 在其麾下，share 即相对权重的 cpu调度，用来限制 cgroup 的 cpu 的使用率</li><li>cpuacct：统计 cgroup 的 cpu 使用率</li><li>cpuset：在多核机器上设置 cgroups 可使用的 cpu 核心数和内存</li></ul><p>通常前两者可以合体</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package subsystemstype ResourceConfig struct &#123;MemoryLimit stringCPUShare stringCPUSet string&#125;type Subsystem interface &#123;&#x2F;&#x2F; return the name of which type of subsystemName() string&#x2F;&#x2F; set a resource limit on a cgroupSet(cgroupPath string, res *ResourceConfig) error&#x2F;&#x2F; add a processs with the pid to a groupAddProcess(cgroupPath string, pid int) error&#x2F;&#x2F; remove a cgroupRemoveCgroup(cgroupPath string) error&#125;&#x2F;&#x2F; instance of a subsystemsvar SubsystemsInstance &#x3D; []Subsystem&#123;&amp;CPU&#123;&#125;,&amp;CPUSet&#123;&#125;,&amp;Memory&#123;&#125;,&#125;</code></pre><h4 id="memorysubsystem-2">4.2 MemorySubsystem</h4><h5 id="name-2">4.2.1 Name()</h5><p>很简单，返回 “memory” 字符串，表示这个 subsystem 是memorySubsystem。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *MemorySubsystem) Name() string &#123;    return &quot;memory&quot;&#125;</code></pre><h5 id="set-2">4.2.2 Set()</h5><p>Set() 用于对 cgroup 设置资源限制，因此参数为 cgroup 的 path 和resourceConfig。</p><ol type="1"><li>其中 <code>GetCgroupPath</code> 后面会提及，作用是获取这个 subsystem所挂载的 hierarchy 上的虚拟文件系统下的 从group 路径。</li><li>获取到 cgroupPath 在虚拟文件系统中的位置后，只需要写入"memory.limit_in_bytes" 文件中即可。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; set the memory limit to this cgroup with cgroupPathfunc (ms *Memory) Set(cgroupPath string, res *ResourceConfig) error  &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;memory.limit_in_bytes&quot;), []byte(res.MemoryLimit), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup memory fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="addprocess-2">4.2.3 AddProcess()</h5><ol type="1"><li>和上面基本一样，只不过是写到 tasks 里。</li><li>pid 变成 byte slice 之前要用 Itoa 转化一下。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add process fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="removecgroup-2">4.2.4 RemoveCgroup()</h5><ol type="1"><li>使用 <code>os.Remove</code> 可以移除参数所指定的文件或文件夹。</li><li>这里移除整个 cgroup 文件夹，就等于是删除 cgroup 了。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusubsystem-2">4.3 CPUSubsystem</h4><p>这里的设计和上面没什么区别，直接贴参考代码</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpu.gofunc (c *CPU) Name() string &#123;return &quot;CPUShare&quot;&#125;func (c *CPU) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpu.shares&quot;), []byte(res.CPUShare), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cpu share limit failed: %s&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpu process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusetsubsystem-2">4.4 CPUSetSubsystem</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpuset.gofunc (c *CPUSet) Name() string &#123;return &quot;CPUSet&quot;&#125;func (c *CPUSet) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpuset.cpus&quot;), []byte(res.CPUSet), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup cpuset failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpuset process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(path.Join(subsystemCgroupPath))&#125;&#125;</code></pre><h4 id="getcgrouppath-2">4.5 GetCgroupPath()</h4><p><code>GetCgroupPath()</code> 用于获取某个 subsystem 所挂载的hierarchy 上的虚拟文件系统 (挂载后的文件夹) 下的 cgroup的路径。通过对这个目录的改写来改动 cgroup。</p><p>首先我们抛开 cgroup，在此之前我们要知道 这个 hierarchy 的 cgroup根节点的路径。那可以在 <code>/proc/self/mountinfo</code> 中获取。</p><p>下面是一些实现细节：</p><ol type="1"><li>首先定义一个 <code>FindCgroupMountpoint()</code> 来找到 cgroup的根节点。</li><li>然后在 <code>GetCgroupPath</code> 将其和 cgroup的相对路径拼接从而获取 cgroup 的路径。如果 <code>autoCreate</code> 为true 且该路径不存在，那么就新建一个 cgroup。(在 hierarchy 环境下，mkdir其实会隐式地创建一个 cgroup，其中包括很多配置文件)</li></ol><blockquote><p><a href="#1.3.4 cgroup 的 kernel 接口">点击这里回顾</a></p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; as the function name shows, find the root path of hierarchyfunc FindCgroupMountpoint(subsystemName string) string  &#123;f, err :&#x3D; os.Open(&quot;&#x2F;proc&#x2F;self&#x2F;mountinfo&quot;)    &#x2F;&#x2F; get info about mount relate to current processif err !&#x3D; nil &#123;return &quot;&quot;&#125;defer f.Close()scanner :&#x3D; bufio.NewScanner(f)for scanner.Scan() &#123;txt :&#x3D; scanner.Text()fields :&#x3D; strings.Split(txt, &quot; &quot;)&#x2F;&#x2F; find whether &quot;subsystemName&quot; appear in the last field&#x2F;&#x2F; if so, then the fifth field is the pathfor _, opt :&#x3D; range strings.Split(fields[len(fields)-1], &quot;,&quot;) &#123;if opt &#x3D;&#x3D; subsystemName &#123;return fields[4]&#125;&#125;&#125;return &quot;&quot;&#125;&#x2F;&#x2F; get the absolute path of a cgroupfunc GetCgroupPath(subsystemName string, cgroupPath string, autoCreate bool) (string, error)  &#123;cgroupRootPath :&#x3D; FindCgroupMountpoint(subsystemName)expectedPath :&#x3D; path.Join(cgroupRootPath, cgroupPath)&#x2F;&#x2F; find the cgroup or create a new cgroupif _, err :&#x3D; os.Stat(expectedPath); err &#x3D;&#x3D; nil  || (autoCreate &amp;&amp; os.IsNotExist(err)) &#123;if os.IsNotExist(err) &#123;if err :&#x3D; os.Mkdir(expectedPath, 0755); err !&#x3D; nil &#123;return &quot;&quot;, fmt.Errorf(&quot;error when create cgroup: %v&quot;, err)&#125;&#125;return expectedPath, nil&#125; else &#123;return &quot;&quot;, fmt.Errorf(&quot;cgroup path error: %v&quot;, err)&#125;&#125;</code></pre><h4 id="cgroupsmanager.go-2">4.6 cgroupsManager.go</h4><ol type="1"><li>定义 CgroupManager 类型，其中的 path 要注意是相对路径，相对于hierarchy 的 root path。所以一个 CgroupManager 是有可能表示多个 cgroups的，或准确说，和对应的 hierarchy root path 的相对路径一样的多个cgroups。</li><li>因为上述原因，<code>Set()</code> 可能会创建多个 cgroups，如果subsystems 们在不同的 hierarchy 就会这样。</li><li>这也是为什么 <code>AddProcess()</code> 和 <code>Remove()</code>要在每个 subsystem 上执行一遍。因为这些 subsystem 可能存在于不同的hierarchies。</li><li>注意 <code>Set()</code> 和 <code>AddProcess()</code>都不是返回错误，而是发出警告，然后返回nil。因为有些时候用户只指定某一个限制，例如 memory，那样的话修改 cpu等其实会报错 (正常的报错)，因此我们不 return err 来退出。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">package cgroupsimport &quot;simple-docker&#x2F;subsystem&quot;type CgroupManager struct &#123;Path     string &#x2F;&#x2F; relative path, relative to the root path of the hierarchy&#x2F;&#x2F; so this may cause more than one cgroup in different hierarchiesResource *subsystems.ResourceConfig&#125;func NewCgroupManager(path string) *CgroupManager &#123;return &amp;CgroupManager&#123;Path: path,&#125;&#125;&#x2F;&#x2F; set the three resource config subsystems to the cgroup(will create if the cgroup path is not existed)&#x2F;&#x2F; this may generate more than one cgroup, because those subsystem may appear in different hierarchiesfunc (cm CgroupManager) Set(res *subsystems.ResourceConfig) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.Set(cm.Path, res); err !&#x3D; nil &#123;logrus.Warnf(&quot;set resource fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; add process to the cgroup path&#x2F;&#x2F; why should we iterate all the subsystems? we have only one cgroup&#x2F;&#x2F; because those subsystems may appear at different hierarchies, which will then cause more than one cgroup, 1-3 in this case.func (cm *CgroupManager) AddProcess(pid int) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.AddProcess(cm.Path, pid); err !&#x3D; nil &#123;logrus.Warn(&quot;app process fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; delete the cgroup(s)func (cm *CgroupManager) Remove() error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err:&#x3D; subsystem.RemoveCgroup(cm.Path); err !&#x3D; nil &#123;return err&#125;&#125;return nil&#125;</code></pre><h4 id="管道处理多个容器参数-2">4.7 管道处理多个容器参数</h4><p>限制容器运行的命令不再像是 <code>/bin/sh</code>这种单个参数，而是多个参数，因此需要使用管道来对多个参数进行处理。那么需要修改以下文件：</p><h5 id="containerinit.go-2">4.7.1 container/init.go</h5><ol type="1"><li>管道原理和 channel 很像，read 端和 write端会在另一边没响应时堵塞。</li><li>使用 <code>os.Pipe()</code> 获取管道。返回的 readPipe 和 writePipe都是 <code>*os.File</code> 类型。</li><li>如何把管道传给子进程 (也就是容器进程) 变成了一个难题，这里用到了<code>ExtraFile</code> 这个参数来解决。cmd会带着参数里的文件来创建新的进程。(这里除了 ExtraFile，还会有类似StandardFile，也就是 stdin，stdout，stderr)</li><li>这里把 read 端传给容器进程，然后 write 端保留在父进程上。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;new pipe error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itselfcmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)&#x2F;&#x2F; new namespacescmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;return cmd, writePipe&#125;</code></pre><p>除了 <code>NewProcess()</code>，<code>InitProcess()</code>也要改变下。</p><ol type="1"><li>使用 readCommand 来读取 pipe。</li><li>实际运行中，当进程运行到 <code>readCommand()</code> 时会堵塞，直到write 端传数据进来。</li><li>因此不用担心我们在容器运行后再传输参数。因为再读取完参数之前，<code>InitProcess()</code>也不会运行到 <code>syscall.Exec()</code> 这一步。</li><li>这里添加了 lookPath，这个是用于解决每次我们都要输入<code>/bin/ls</code>的麻烦，这个函数会帮我们找到参数命令的绝对路径。也就是说，只要输入 ls即可，lookPath 会自动找到 <code>/bin/ls</code>。然后我们再把这个 path作为 <code>argv()</code> 传给 <code>syscall.Exec</code></li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initialize the containerfunc InitProcess() error &#123;cmdArray :&#x3D; readCommand()if len(cmdArray) &#x3D;&#x3D; 0 &#123;return fmt.Errorf(&quot;init process fails, cmdArray is nil&quot;)&#125;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV&#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)path, err :&#x3D; exec.LookPath(cmdArray[0])if err !&#x3D; nil &#123;logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)return err&#125;&#x2F;&#x2F; log path infologrus.Infof(&quot;find path: %v&quot;, path)if err :&#x3D; syscall.Exec(path, cmdArray, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(err.Error())&#125;return nil&#125;func readCommand() []string &#123;pipe :&#x3D; os.NewFile(uintptr(3), &quot;pipe&quot;)msg, err :&#x3D; ioutil.ReadAll(pipe)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read pipe failed: %v&quot;, err)return nil&#125;return strings.Split(string(msg), &quot; &quot;)&#125;</code></pre><h5 id="dockercommandrun.go-2">4.7.2 dockerCommand/run.go</h5><ol type="1"><li>在 run.go 向 writePipe 写入参数，这样容器就会获取到参数。</li><li>关闭 pipe，使得 init 进程继续进行。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig) &#123;initProcess, writePipe :&#x3D; container.NewProcess(tty)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write sidesendInitCommand(cmdArray, writePipe)initProcess.Wait()os.Exit(-1)&#125;func sendInitCommand(cmdArray []string, writePipe *os.File) &#123;cmdString :&#x3D; strings.Join(cmdArray, &quot; &quot;)logrus.Infof(&quot;whole init command is: %v&quot;, cmdString)writePipe.WriteString(cmdString)writePipe.Close()&#125;</code></pre><h5 id="command.go-2">4.7.3 command.go</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open a interactive tty(pre sudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpushare&quot;,Usage:&quot;limit the cpu share&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &#x3D;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;cmdArray :&#x3D; make([]string,len(args)) &#x2F;&#x2F; commandcopy(cmdArray,args)&#x2F;&#x2F; checkout whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; pre sudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig &#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare: context.String(&quot;cpushare&quot;),CPUSet: context.String(&quot;cpu&quot;),&#125;dockerCommand.Run(tty, cmdArray, &amp;resourceConfig)return nil&#125;,&#125;&#x2F;&#x2F; docker init, but cannot be used by uservar InitCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;start initializing...&quot;)return container.InitProcess()&#125;,&#125;</code></pre><h5 id="main.go-2">4.7.4 main.go</h5><p>除了上面的修改，我们还要定义一个程序的入口：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;&quot;github.com&#x2F;urfave&#x2F;cli&quot;)const usage &#x3D; &#96;Usage&#96;func main() &#123;app :&#x3D; cli.NewApp()app.Name &#x3D; &quot;simple-docker&quot;app.Usage &#x3D; usageapp.Commands &#x3D; []cli.Command&#123;RunCommand,InitCommand,&#125;app.Before &#x3D; func(context *cli.Context) error &#123;logrus.SetFormatter(&amp;logrus.JSONFormatter&#123;&#125;)logrus.SetOutput(os.Stdout)return nil&#125;if err :&#x3D; app.Run(os.Args); err !&#x3D; nil &#123;logrus.Fatal(err)&#125;&#125;</code></pre><h4 id="运行-demo-2">4.8 运行 demo</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it stress -m 100m --vm-bytes 200m --vm-keep -m 1</code></pre><p>效果如下：</p><p><img src="0x0035/demo_1.png" /></p><p>不过这个运行方式不能进行交互，我们可以使用这个命令来验证我们写的docker 是否与宿主机隔离：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it &#x2F;bin&#x2F;sh</code></pre><p><img src="0x0035/demo_sh.png" /></p><p>可以看到，pid，ipc，network 方面都与宿主机进行了隔离。</p><h2 id="三镜像篇-1">三、镜像篇</h2><h3 id="构造镜像-1">5. 构造镜像</h3><h4 id="编译-aufs-内核-1">5.1 编译 aufs 内核</h4><p>因为电脑硬盘空间不太够，就不使用虚拟机来做实验了，笔者这里使用 WSL2来完成后续工作，然而，WSL2 Kernel 没有把 aufs编译进去，那只能换内核了，查阅资料，有两种更换内核的方法：</p><ul><li><p>直接替换 <code>C:\System32\lxss\tools\kernel</code> 文件</p></li><li><p>在 users 目录下新建 <code>.wslconfig</code> 文件：</p><pre class="line-numbers language-none"><code class="language-none">[wsl2]kernel&#x3D;&quot;要替换kernel的路径&quot;</code></pre></li></ul><p>很明显，我是不会满足于使用别人编译好的内核的，那我也来动手做一个。</p><h5 id="准备代码库-1">5.1.1 准备代码库</h5><p>我们先在 WSL 上准备好相关软件包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">apt update #更新源apt install build-essential flex bison libssl-dev libelf-dev gcc make</code></pre><p>编译内核需要从 GitHub 上 clone 微软官方的 WSL 代码和 AUFS-Standalone的代码库</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">git clone https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;WSL2-Linux-Kernel kernelgit clone https:&#x2F;&#x2F;github.com&#x2F;sfjro&#x2F;aufs-standalone aufs5</code></pre><p>然后查看 WSL 内核版本：在 wsl 下运行命令 <code>uname -r</code></p><p>例如我的内核版本是 5.15.19，那 kernel 和 aufs 都要切换到相应的分支去(kernel 默认就是 5.15.19，故不用切换)</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd aufs5git checkout aufs5.15.36</code></pre><p>然后退回到 kernel 文件夹给代码打补丁：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cat ..&#x2F;aufs5&#x2F;aufs5-mmap.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-base.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-kbuild.patch | patch -p1</code></pre><p>三个 Patch 的顺序无关。</p><p>然后再复制一点配置文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cp ..&#x2F;aufs5&#x2F;Documentation . -rcp ..&#x2F;aufs5&#x2F;fs&#x2F; . -rcp ..&#x2F;aufs5&#x2F;include&#x2F;uapi&#x2F;linux&#x2F;aufs_type.h .&#x2F;include&#x2F;uapi&#x2F;linux</code></pre><p>接下来我们来修改一下编译配置，在 <code>Microsoft/config-wsl</code>中任意位置增加一行：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini">CONFIG_AUFS_FS&#x3D;y</code></pre><p>最后，就可以开始编译了！</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">make KCONFIG_CONFIG&#x3D;Microsoft&#x2F;config-wsl -j8</code></pre><p>过程中会问你一些问题，我除了 AUFS Debug 都选了 y。</p><p>最后会在当前目录生成 <code>vmlinuz</code>，在<code>arch/x86/boot</code> 下生成 <code>bzImage</code>。</p><p>关闭 WSL 后更换内核，重启 WSL 输入<code>grep aufs /proc/filesystems</code>验证结果，如果出现 aufs的字样，说明操作成功。</p><h4 id="使用-busybox-创建容器-1">5.2 使用 busybox 创建容器</h4><h5 id="busybox-1">5.2.1 busybox</h5><p>先在 docker 获取 busybox 镜像并打包成一个 tar 包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">docker pull busyboxdocker run -d busybox top -bdocker export -o busybox.tar &lt;container_id&gt;</code></pre><p>将其复制到 WSL 下并解压。</p><h5 id="pivot_root-1">5.2.2 pivot_root</h5><p>pivot_root 是一个系统调用，作用是改变当前 root 文件系统。pivot_root可以将当前进程的 root 文件系统移动到 put_old 文件夹，然后使 new_root成为新的 root 文件系统。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func pivotRoot(root string) error &#123;&#x2F;&#x2F; remount the root dir, in order to make current root and old root in different file systemsif err :&#x3D; syscall.Mount(root, root, &quot;bind&quot;, syscall.MS_BIND|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;mount rootfs to itself error: %v&quot;, err)&#125;&#x2F;&#x2F; create &#39;rootfs&#x2F;.pivot_root&#39; to store old_rootpivotDir :&#x3D; filepath.Join(root, &quot;.pivot_root&quot;)if err :&#x3D; os.Mkdir(pivotDir, 0777); err !&#x3D; nil &#123;return err&#125;&#x2F;&#x2F; pivot_root mount on new rootfs, old_root mount on rootfs&#x2F;.pivot_rootif err :&#x3D; syscall.PivotRoot(root, pivotDir); err !&#x3D; nil &#123;return fmt.Errorf(&quot;pivot_root %v&quot;, err)&#125;&#x2F;&#x2F; change current work dir to root dirif err :&#x3D; syscall.Chdir(&quot;&#x2F;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;chdir &#x2F; %v&quot;, err)&#125;pivotDir &#x3D; filepath.Join(&quot;&#x2F;&quot;, &quot;.pivot_root&quot;)&#x2F;&#x2F; umount rootfs&#x2F;.rootfs_rootif err :&#x3D; syscall.Unmount(pivotDir, syscall.MNT_DETACH); err !&#x3D; nil &#123;return fmt.Errorf(&quot;umount pivot_root dir %v&quot;, err)&#125;&#x2F;&#x2F; del the temporary dirreturn os.Remove(pivotDir)&#125;</code></pre><p>有了这个函数就可以在 init 容器进程时，进行一系列的 mount 操作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setUpMount() error &#123;&#x2F;&#x2F; get current pathpwd, err :&#x3D; os.Getwd()if err !&#x3D; nil &#123;logrus.Errorf(&quot;get current location error: %v&quot;, err)return err&#125;logrus.Infof(&quot;current location: %v&quot;, pwd)pivotRoot(pwd)&#x2F;&#x2F; mount procdefaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEVif err :&#x3D; syscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc failed: %v&quot;, err)return err&#125;if err :&#x3D; syscall.Mount(&quot;tmpfs&quot;, &quot;&#x2F;dev&quot;, &quot;tmpfs&quot;, syscall.MS_NOSUID|syscall.MS_STRICTATIME, &quot;mode&#x3D;755&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;dev failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>tmpfs 是一种基于内存的文件系统，用 RAM 或 swap 分区来存储。</p><p>在 <code>NewParentProcess()</code> 中加一句<code>cmd.Dir="/root/busybox"</code>。</p><p>写完上述函数，然后在 <code>initProcess()</code> 中调用一下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">if err :&#x3D; setUpMount(); err !&#x3D; nil &#123;    logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)&#125;</code></pre><p>然后来运行测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it sh###### dividing live&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;busybox&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#x2F; #</code></pre><p>可以看到，容器当前目录被虚拟定位到了根目录，其实是在宿主机上映射的<code>/root/busybox</code>。</p><h5 id="用-aufs-包装-busybox-1">5.2.3 用 AUFS 包装 busybox</h5><p>前面提到了，docker 使用 AUFS 存储镜像和容器。docker在使用镜像启动一个容器时，会新建 2 个 layer：write layer 和container-init-layer。write layer是容器唯一的可读写层，container-init-layer是为容器新建的只读层，用来存储容器启动时传入的系统信息。</p><ul><li><code>CreateReadOnlyLayer()</code> 新建 <code>busybox</code>文件夹，解压 <code>busybox.tar</code> 到 <code>busybox</code>目录下，作为容器只读层。</li><li><code>CreateWriteLayer()</code> 新建一个 <code>writeLayer</code>文件夹，作为容器唯一可写层。</li><li><code>CreateMountPoint()</code> 先创建了 <code>mnt</code>文件夹作为挂载点，再把 <code>writeLayer</code> 目录和<code>busybox</code> 目录 mount 到 <code>mnt</code> 目录下。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; extra tar to &#39;busybox&#39;, used as the read only layer for containerfunc CreateReadOnlyLayer(rootURL string) &#123;busyboxURL :&#x3D; rootURL + &quot;busybox&#x2F;&quot;busyboxTarURL :&#x3D; rootURL + &quot;busybox.tar&quot;exist, err :&#x3D; PathExists(busyboxURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, busyboxURL, err)&#125;if !exist &#123;if err :&#x3D; os.Mkdir(busyboxURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, busyboxURL, err)&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, busyboxTarURL, &quot;-C&quot;, busyboxURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, busyboxTarURL, err)&#125;&#125;&#125;&#x2F;&#x2F; create a unique folder as writeLayerfunc CreateWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.Mkdir(writeURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, writeURL, err)&#125;&#125;func CreateMountPoint(rootURL string, mntURL string) &#123;&#x2F;&#x2F; create mnt folder as mount pointif err :&#x3D; os.Mkdir(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;dirs :&#x3D; &quot;dirs&#x3D;&quot; + rootURL + &quot;writeLayer:&quot; + rootURL + &quot;busybox&quot;cmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;&#125;func NewWorkSpace(rootURL, mntURL string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)&#125;</code></pre><p>接下来在 <code>NewParentProcess()</code> 将容器使用的宿主机目录<code>/root/busybox</code> 替换为 <code>/root/mnt</code>，这样使用 AUFS系统启动容器的代码就完成了。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;NewWorkSpace(rootURL, mntURL)cmd.Dir &#x3D; mntURLreturn cmd, writePipe</code></pre><p>docker 会在删除容器时，把容器对应的 write layer 和container-init-layer 删除，而保留镜像中所有的内容。</p><ul><li><code>DeleteMountPoint()</code> 中 umount <code>mnt</code>目录。</li><li>删除 <code>mnt</code> 目录。</li><li>在 <code>DeleteWriteLayer()</code> 删除 <code>writeLayer</code>文件夹。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(rootURL string, mntURL string) &#123;cmd :&#x3D; exec.Command(rootURL, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)&#125;&#125;func DeleteWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;func DeleteWorkSpace(rootURL, mntURL string) &#123;DeleteMountPoint(rootURL, mntURL)DeleteWriteLayer(rootURL)&#125;</code></pre><p>现在来启动一个容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it shdirs&#x3D;&#x2F;root&#x2F;writeLayer:&#x2F;root&#x2F;busybox&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#x2F; #</code></pre><p>测试在容器内创建文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # mkdir aaa&#x2F; # touch aaa&#x2F;test.txt</code></pre><p>此时我们可以在宿主机终端查看<code>/root/mnt/writeLayer</code>，可以看到刚才新建的 <code>aaa</code>文件夹和 <code>test.txt</code>，在我们退出容器后，<code>/root/mnt</code>文件夹被删除，伴随着刚才创建的文件夹和文件都被删除，而作为镜像的 busybox仍被保留，且内容未被修改。</p><h4 id="实现-volume-数据卷-1">5.3 实现 volume 数据卷</h4><p>上节实现了容器和镜像的分离，但是如果容器退出，容器可写层的所有内容就会被删除，这里使用volume 来实现容器数据持久化。</p><p>先在 <code>command.go</code> 里添加 <code>-v</code> 标签：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;,         &#x2F;&#x2F; add &#96;-v&#96; tag         &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;         &#x2F;&#x2F; send volume args to Run()volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig,volume)return nil&#125;,&#125;</code></pre><p>在 <code>Run()</code> 中，把 volume 传给创建容器的<code>NewParentProcess()</code> 和删除容器文件系统的<code>DeleteWorkSpace()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)initProcess.Wait()rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>在 <code>NewWorkSpace()</code> 中，继续把 volume传给创建容器文件系统的 <code>NewWorkSapce()</code>。</p><p>创建容器文件系统过程如下：</p><ul><li>创建只读层。</li><li>创建容器读写层。</li><li>创建挂载点并把只读层和读写层挂载到挂载点上。</li><li>判断 volume是否为空，如果是，说明用户没有使用挂载标签，结束创建过程。</li><li>不为空，就用 <code>volumeURLExtract()</code> 解析。</li><li>当 <code>volumeURLExtract()</code> 返回字符数组长度为2，且数据元素均不为空时，则执行 <code>MountVolume()</code>来挂载数据卷。<ul><li>否则提示用户创建数据卷输入值不对。</li></ul></li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(rootURL, mntURL, volume string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(rootURL, mntURL, volumeURLs)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;func volumeUrlExtract(volume string) []string &#123;&#x2F;&#x2F; divide volume by &quot;:&quot;return strings.Split(volume, &quot;:&quot;)&#125;</code></pre><p>挂载数据卷过程如下：</p><ul><li>读取宿主机文件目录 URL，创建宿主机文件目录(<code>/root/$&#123;parentURL&#125;</code>)</li><li>读取容器挂载点 URL，在容器文件系统里创建挂载点(<code>/root/mnt/$&#123;containerURL&#125;</code>)</li><li>把宿主机文件目录挂载到容器挂载点，这样启动容器的过程，对数据卷的处理就完成了。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]containerVolumeURL :&#x3D; mntURL + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURLcmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)&#125;&#125;</code></pre><p>删除容器文件系统过程如下：</p><ul><li>在 volume 不为空，且使用 <code>volumeURLExtract()</code> 解析 volume字符串返回的字符数组长度为 2，数据元素均不为空时，才执行<code>DeleteMountPointWithVolume()</code> 来处理。</li><li>其余情况仍使用前面的 <code>DeleteMountPoint()</code>。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(rootURL, mntURL, volume string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;DeleteWriteLayer(rootURL)&#125;</code></pre><p><code>DeleteMountPointWithVolume()</code> 处理逻辑如下：</p><ul><li>卸载 volume 挂载点的文件系统(<code>/root/mnt/$&#123;containerURL&#125;</code>)，保证整个容器挂载点没有再被使用。</li><li>卸载整个容器文件系统挂载点 (<code>/root/mnt</code>)。</li><li>删除容器文件系统挂载点。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; umount volume point in containercontainerURL :&#x3D; mntURL + volumeURLs[1]cmd :&#x3D; exec.Command(&quot;umount&quot;, containerURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)&#125;&#x2F;&#x2F; umount the whole point of the containercmd &#x3D; exec.Command(&quot;umount&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>接下来启动容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; #</code></pre><p>进入 <code>containerVolume</code>，创建一个文本文件，并随便写点东西：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;test&quot; &gt;&gt; test.txt</code></pre><p>此时我们能在宿主机的 <code>/root/volume</code>找到我们刚才创建的文本文件。退出容器后，volume文件夹也没有被删除。再次进入容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">r# go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;mkdir parent dir &#x2F;root&#x2F;volume error. mkdir &#x2F;root&#x2F;volume: file exists&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; # ls containerVolume&#x2F;test.txt</code></pre><p>此时这里会提示 volume 文件夹存在，我们在 <code>test.txt</code>内追加内容：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;###&quot; &gt;&gt; test.txt</code></pre><p>此时再次退出容器，能看到修改过后的文件内容，可以看到 volume文件夹没有被删除。</p><h4 id="简单镜像打包-1">5.4 简单镜像打包</h4><p>容器在退出时会删除所有可写层的内容，commit命令可以把运行状态容器的内容存储为镜像保存下来。</p><p>在 <code>main.go</code> 里添加 <code>commit</code> 命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    InitCommand,    RunCommand,    CommitCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里实现 <code>CommitCommand</code>命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;imageName :&#x3D; context.Args()[0]&#x2F;&#x2F; commitContainer(containerName)commitContainer(imageName)return nil&#125;,&#125;</code></pre><p>添加 <code>commit.go</code>，通过 <code>commitContainer()</code>实现将容器文件系统打包成 <code>$&#123;imagename&#125;.tar</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&#x2F;exec&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;)func commitContainer(imageName string) &#123;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&quot;imageTar :&#x3D; &quot;&#x2F;root&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>运行测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it sh</code></pre><p>然后在另一个终端运行：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit image</code></pre><p>这时候可以在 root 目录下看到多了一个 <code>image.tar</code>，解压后可以发现压缩包的内容和 <code>/root/mnt</code> 一致。</p><blockquote><p>tips：一定要先运行容器！如果不运行容器直接打包，会提示<code>/root/mnt</code> 不存在。</p></blockquote><h3 id="构建容器进阶-1">6. 构建容器进阶</h3><h4 id="实现容器后台运行-1">6.1 实现容器后台运行</h4><p>容器，放在操作系统层面，就是一个进程，当前运行命令的 simple-docker是主进程，容器是当前 simple-docker 进程 fork出来的子进程。子进程的结束和父进程的运行是一个异步的过程，即父进程不会知道子进程在什么时候结束。如果创建子进程时，父进程退出，那这个子进程就是孤儿进程(没人管)，此时进程号为 1 的进程 init 就会接受这些孤儿进程。</p><p>先在 <code>command.go</code> 添加 <code>-d</code>标签，表示这个容器启动时在后台运行：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach container         &#x2F;&#x2F; tty cannot work with detachif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume)return nil&#125;,&#125;</code></pre><p>然后也要修改一下 <code>run.go</code> 的 <code>Run()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)    &#x2F;&#x2F; if background process, parent process won&#39;t waitif tty &#123;initProcess.Wait()&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T15:32:44+08:00&quot;&#125;</code></pre><p>根据书上的提示，<code>ps -ef</code> 用来查找 top 进程：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ps -ef | grep toproot        3713     751  0 14:42 pts&#x2F;2    00:00:00 top</code></pre><p>前面几次运行命令，都找不到 top这个进程，于是我后面多跑了几次，终于看到了这个进程。。。</p><p>可以看到，top 命令的进程正在运行着，不过运行环境是 WSL，父进程 id不是 1，然后 <code>ps -ef</code> 查看一下，top 的父进程是一个 bash进程，而 bash 进程的父进程是一个 init 进程，这样应该算过了吧(偶尔的一两次不严谨)。</p><h4 id="实现查看运行中的容器-1">6.2 实现查看运行中的容器</h4><h5 id="name-标签-1">6.2.1 name 标签</h5><p>前面创建的容器里，所有关于容器的信息，例如PID、容器创建时间、容器运行命令等，都没有记录，这导致容器运行完后就在也不知道它的信息了，因此要把这部分信息保留。先在<code>command.go</code> 里加一个 name 标签，方便用户指定容器的名字：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag &#123;Name: &quot;name&quot;,Usage: &quot;container name&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume, containerName)return nil&#125;,&#125;</code></pre><p>添加一个方法来记录容器的相关信息，这里用先用一个 10位的数字来表示容器的 id：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func randStringBytes(n int) string &#123;letterBytes :&#x3D; &quot;1234567890&quot;rand.Seed(time.Now().UnixNano())b :&#x3D; make([]byte, n)for i :&#x3D; range b &#123;b[i] &#x3D; letterBytes[rand.Intn(len(letterBytes))]&#125;return string(b)&#125;</code></pre><p>这里用时间戳为种子，每次生成一个 10 以内的数字作为 letterBytes数组的下标，最后拼成整个容器的 id。容器的信息默认保存在<code>/var/run/simple-docker/$&#123;containerName&#125;/config.json</code>，容器基本格式如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;Id          string &#96;json:&quot;id&quot;&#96;Name        string &#96;json:&quot;name&quot;&#96;Command     string &#96;json:&quot;command&quot;&#96; &#x2F;&#x2F; the command that init process executeCreatedTime string &#96;json:&quot;created_time&quot;&#96;Status      string &#96;json:&quot;status&quot;&#96;&#125;var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&quot;ConfigName          string &#x3D; &quot;config.json&quot;)</code></pre><p>下面是记录容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;) &#x2F;&#x2F; format must like thiscommand :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>这里格式化的时间必须是<code>2006-01-02 15:04:05</code>，不然格式化后的时间会是几千年后doge。</p><p>详细可以看这篇文章：<ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p>在主函数加上调用：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>如果创建 tty 方式的容器，在容器退出后，就会删除相关信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func deleteContainerInfo(containerID string) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerID)if err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, dirURL, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top# go run . run -d --name jay top</code></pre><p>执行完成后，可以在 <code>/var/run/simple-docker/</code>找到两个文件夹，一个是随机 id，一个是 jay，文件夹下各有一个<code>config.json</code>，记录了容器的相关信息。</p><h5 id="实现-docker-ps-1">6.2.2 实现 docker ps</h5><p>在 <code>main.go</code> 加一个 <code>listCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,&#125;</code></pre><p>在 <code>command.go</code> 添加定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ListCommand &#x3D; cli.Command&#123;Name: &quot;ps&quot;,Usage: &quot;list all the containers&quot;,Action: func(context *cli.Context) error &#123;ListContainers()return nil&#125;,&#125;</code></pre><p>新建一个 <code>list.go</code>，实现记录列出容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListContainers() &#123;&#x2F;&#x2F; get the path that store the info of the containerdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, &quot;&quot;)dirURL &#x3D; dirURL[:len(dirURL)-1]&#x2F;&#x2F; read all the files in the directoryfiles, err :&#x3D; ioutil.ReadDir(dirURL)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read dir %s error %v&quot;, dirURL, err)return&#125;var containers []*container.ContainerInfofor _, file :&#x3D; range files &#123;tmpContainer, err :&#x3D; getContainerInfo(file)&#x2F;&#x2F; .Println(tmpContainer)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info error %v&quot;, err)continue&#125;containers &#x3D; append(containers, tmpContainer)&#125;&#x2F;&#x2F; use tabwriter.NewWriter to print the containerInfow :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprintf(w, &quot;ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n&quot;)for _, item :&#x3D; range containers &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\t%s\t%s\t%s\n&quot;,item.Id, item.Name, item.Pid, item.Status, item.Command, item.CreatedTime)&#125;&#x2F;&#x2F; refresh stdout if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;flush stdout error %v&quot;,err)return&#125;&#125;func getContainerInfo(file os.FileInfo) (*container.ContainerInfo, error) &#123;containerName :&#x3D; file.Name()&#x2F;&#x2F; create the absolute pathconfigFileDir :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFileDir &#x3D; configFileDir + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read config.jsoncontent, err :&#x3D; ioutil.ReadFile(configFileDir)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, configFileDir, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; turn json to containerInfoif err :&#x3D; json.Unmarshal(content, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>接上小节的测试，我们运行以下命令：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:11+08:00&quot;&#125;# go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:25+08:00&quot;&#125;# go run . psID           NAME         PID         STATUS      COMMAND     CREATED6675792962   6675792962   4317        running     top         2023-05-05 19:29:115553437308   jay          4404        running     top         2023-05-05 19:29:25</code></pre><p>现在就可以通过 ps 来看到所有创建的容器状态和它们的 init 进程 id了。</p><h4 id="查看容器日志-1">6.3 查看容器日志</h4><p>在 <code>main.go</code> 加一个 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,    LogCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里添加 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var LogCommand &#x3D; cli.Command&#123;Name:  &quot;logs&quot;,Usage: &quot;print logs of a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;contianerName :&#x3D; context.Args()[0]logContainer(contianerName)return nil&#125;,&#125;</code></pre><p>新建一个 <code>log.go</code>，定义 <code>logContainer()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func logContainer(containerName string) &#123;&#x2F;&#x2F; get the log pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)logFileLocation :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ContainerLogFile&#x2F;&#x2F; open log filefile, err :&#x3D; os.Open(logFileLocation)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container open file %s error: %v&quot;, logFileLocation, err)return&#125;defer file.Close()&#x2F;&#x2F; read log file contentcontent, err :&#x3D; ioutil.ReadAll(file)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container read file %s error: %v&quot;, logFileLocation, err)return&#125;&#x2F;&#x2F; use Fprint to transfer content to stdoutfmt.Fprint(os.Stdout, string(content))&#125;</code></pre><p>测试一下，先用 detach 方式创建一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-06T14:26:32+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED1837062451   jay         2065        running     top         2023-05-06 14:26:32# go run . logs jayMem: 3265116K used, 4568420K free, 3256K shrd, 71432K buff, 1135692K cachedCPU:  0.3% usr  0.2% sys  0.0% nic 99.3% idle  0.0% io  0.0% irq  0.0% sirqLoad average: 0.03 0.09 0.08 1&#x2F;521 5PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND</code></pre><p>可以看到，logs命令成功运行并输出容器的日志。(这里之前出现过前几次创建容器，而后台却没运行的情况，导致一开始运行logs 时报错了，建议在运行 logs 前多检查下 top 是否后台运行中)</p><h4 id="进入容器-namespace-1">6.4 进入容器 Namespace</h4><p>在 6.3小节里，实现了查看后台运行的容器的日志，但是容器一旦创建后，就无法再次进入容器，这一次来实现进入容器内部的功能，也就是exec。</p><h5 id="setns-1">6.4.1 setns</h5><p>setns 是一个系统调用，可根据提供的 PID 再次进入到指定的Namespace。它要先打开 <code>/proc/$&#123;pid&#125;/ns</code>文件夹下对应的文件，然后使当前进程进入到指定的 Namespace 中。对于 go来说，一个有多线程的进程使无法使用 setns 调用进入到对应的命名空间的，go没启动一个程序就会进入多线程状态，因此无法简单在 go里直接调用系统调用，这里还需要借助 C 来实现这个功能。</p><h5 id="cgo-1">6.4.2 Cgo</h5><p>在 go 里写 C：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package rand&#x2F;*#include &lt;stdlib.h&gt;*&#x2F;import &quot;C&quot;func Random() int &#123;    return int(C.random())&#125;func Seed(i int) &#123;    C.srandom(C.uint(i))&#125;</code></pre><h5 id="实现-1">6.4.3 实现</h5><p>先使用 C 根据 PID进入对应 Namespace：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenter&#x2F;*#define _GNU_SOURCE#include &lt;errno.h&gt;#include &lt;sched.h&gt;#include &lt;stdio.h&gt;#include &lt;stdlib.h&gt;#include &lt;string.h&gt;#include &lt;fcntl.h&gt;#include &lt;unistd.h&gt;&#x2F;&#x2F; if this package is quoted, this function will run automatic__attribute__((constructor)) void enter_namespace(void)&#123;    char *simple_docker_pid;    &#x2F;&#x2F; get pid from system environment    simple_docker_pid &#x3D; getenv(&quot;simple_docker_pid&quot;);    if (simple_docker_pid)    &#123;        fprintf(stdout, &quot;got simple docker pid&#x3D;%s\n&quot;, simple_docker_pid);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker pid env skip nsenter&quot;);        &#x2F;&#x2F; if no specified pid, the func will exit        return;    &#125;    char *simple_docker_cmd;    simple_docker_cmd &#x3D; getenv(&quot;simple_docker_cmd&quot;);    if (simple_docker_cmd)    &#123;        fprintf(stdout, &quot;got simple docker cmd&#x3D;%s\n&quot;, simple_docker_cmd);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker cmd env skip nsenter&quot;);        &#x2F;&#x2F; if no specified cmd, the func will exit        return;    &#125;    int i;    char nspath[1024];    char *namespace[] &#x3D; &#123;&quot;ipc&quot;, &quot;uts&quot;, &quot;net&quot;, &quot;pid&quot;, &quot;mnt&quot;&#125;;    for (i &#x3D; 0; i &lt; 5; i++)    &#123;        &#x2F;&#x2F; create the target path, like &#x2F;proc&#x2F;pid&#x2F;ns&#x2F;ipc        sprintf(nspath, &quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;%s&quot;, simple_docker_pid, namespace[i]);        int fd &#x3D; open(nspath, O_RDONLY);printf(&quot;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; %d %s\n&quot;, fd, nspath);        &#x2F;&#x2F; call sentns and enter the target namespace        if (setns(fd, 0) &#x3D;&#x3D; -1)        &#123;            fprintf(stderr, &quot;setns on %s namespace failed: %s\n&quot;, namespace[i], strerror(errno));        &#125;        else        &#123;            fprintf(stdout, &quot;setns on %s namespace succeeded\n&quot;, namespace[i]);        &#125;        close(fd);    &#125;    &#x2F;&#x2F; run command in target namespace    int res &#x3D; system(simple_docker_cmd);    exit(0);    return;&#125;*&#x2F;import &quot;C&quot;</code></pre><p>那如何使用这段代码呢，只需要在要加载的地方引用这个 package即可，我这里是 <code>nenster</code> 。</p><p>其实也可以，单独放在一个 C 文件里，go 文件可以这样写：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenterimport &quot;C&quot;</code></pre><p>下面增加 <code>ExecCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ExecCommand &#x3D; cli.Command&#123;Name:  &quot;exec&quot;,Usage: &quot;exec a command into container&quot;,Action: func(context *cli.Context) error &#123;if os.Getenv(ENV_EXEC_PID) !&#x3D; &quot;&quot; &#123;logrus.Infof(&quot;pid callback pid %v&quot;, os.Getgid())return nil&#125;if len(context.Args()) &lt; 2 &#123;return fmt.Errorf(&quot;missing container name or command&quot;)&#125;containerName :&#x3D; context.Args()[0]cmdArray :&#x3D; make([]string, len(context.Args())-1)for i, v :&#x3D; range context.Args().Tail() &#123;cmdArray[i] &#x3D; v&#125;ExecContainer(containerName, cmdArray)return nil&#125;,&#125;</code></pre><p>新建一个 <code>exec.go</code>下面实现获取容器名和需要的命令，并且在这里引用<code>nsenter</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ENV_EXEC_PID &#x3D; &quot;simple_docker_pid&quot;const ENV_EXEC_CMD &#x3D; &quot;simple_docker_cmd&quot;func getContainerPidByName(containerName string) (string, error) &#123;&#x2F;&#x2F; get the path that store container infodirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read files in target pathcontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;return &quot;&quot;, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to containerInfoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;return &quot;&quot;, err&#125;return containerInfo.Pid, nil&#125;func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run --name jay -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-07T13:23:09+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6530018751   jay         146639      running     top         2023-05-07 13:23:09# go run . logs jayMem: 4355160K used, 3478372K free, 3272K shrd, 208844K buff, 1581396K cachedCPU:  1.2% usr  0.6% sys  0.0% nic 97.9% idle  0.0% io  0.0% irq  0.1% sirqLoad average: 0.12 0.14 0.16 1&#x2F;574 6  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND# go run . exec jay sh&#x2F; # lsbin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top   13 root      0:00 sh   15 root      0:00 ps -ef&#x2F; #</code></pre><p>可以看到，成功进入容器内部，且与宿主机隔离。</p><p>这里出现了一个很奇怪的 bug，就是通过 cgo 去 setns，执行到 mnt时，抛出个错误：<code>Stale file handle</code>，当时找了全网，也找不到答案，于是陷入了两天的痛苦debug，在重新敲代码时，发现又不报错了，切换回那个有错误的分支，也不报错了。既然暂时找不到错误，先搁着吧，如果有看到这篇文章的朋友，也遇到了这个错误，可以留意下。(感觉会是一个雷)</p><p>(应该是容器的 mnt 没有 mount 上去，才会导致 stale file handle)</p><h4 id="停止容器-1">6.5 停止容器</h4><p>定义 <code>StopCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var StopCommand &#x3D; cli.Command&#123;Name:  &quot;stop&quot;,Usage: &quot;stop a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]stopContainer(containerName)return nil&#125;,&#125;</code></pre><p>然后声明一个函数，通过容器名来获取容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getContainerInfoByName(containerName string) (*container.ContainerInfo, error) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigNamecontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read config file %s error %v&quot;, configFilePath, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to container infoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json to container info error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>然后是停止容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func stopContainer(containerName string) &#123;&#x2F;&#x2F; get pid by containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container pid by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; turn pid(string) to intpidInt, err :&#x3D; strconv.Atoi(pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;convert pid from string to int error %v&quot;, err)return&#125;&#x2F;&#x2F; kill container main processif err :&#x3D; syscall.Kill(pidInt, syscall.SIGTERM); err !&#x3D; nil &#123;logrus.Errorf(&quot;stop container %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; get info of the containercontainerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; process is killed, update process statuscontainerInfo.Status &#x3D; container.STOPcontainerInfo.Pid &#x3D; &quot; &quot;&#x2F;&#x2F; update info to jsonnweContentBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;json marshal %s error %v&quot;, containerName, err)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; overwrite containerInfoif err :&#x3D; ioutil.WriteFile(configFilePath, nweContentBytes, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;write config file %s error %v&quot;, configFilePath, err)&#125;&#125;</code></pre><p>测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop jay# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6883605813   jay                     stopped     top# ps -ef | grep toproot       43588     761  0 20:00 pts&#x2F;0    00:00:00 grep --color&#x3D;auto top</code></pre><p>可以看到，jay 这个进程被停止了，且 pid 号设为空。</p><h4 id="删除容器-1">6.6 删除容器</h4><p>定义 <code>RemoveCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RemoveCommand &#x3D; cli.Command&#123;Name:  &quot;rm&quot;,Usage: &quot;remove a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]removeContainer(containerName)return nil&#125;,&#125;</code></pre><p>实现删除容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . rm jay# go run . psID          NAME        PID         STATUS      COMMAND     CREATED</code></pre><p>可以看到，jay 这个容器被删除了。</p><h4 id="通过容器制作镜像-1">6.7 通过容器制作镜像</h4><p>这一节，根据书上的内容，有许多函数需要改动。建议这里对着作者给出的源码debug，书上有部分内容有明显错误。</p><p>之前的文件系统如下：</p><ul><li>只读层：busybox，只读，容器系统的基础</li><li>可写层：writeLayer，容器内部的可写层</li><li>挂载层：mnt，挂载外部的文件系统，类似虚拟机的文件共享</li></ul><p>修改后的文件系统如下：</p><ul><li>只读层：不变</li><li>可写层：再加容器名为目录进行隔离，也就是<code>writeLayer/$&#123;containerName&#125;</code></li><li>挂载层：再加容器名为目录进行隔离，也就是<code>mnt/$&#123;containerName&#125;</code></li></ul><p>因此，本节要实现为每个容器分配单独的隔离文件系统，以及实现对不同容器打包镜像。</p><p><strong>修改 <code>run.go</code></strong></p><p>在 Run 函数参数列表添加一个 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><p>同时也在 <code>command.go</code> 的 runCommand 里修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName)return nil&#125;,</code></pre><p>在 <code>recordContainerInfo</code> 函数的参数列表添加 volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName, volume string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;)command :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,Volume:      volume,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>给 ContainerInfo 添加 Volume 成员：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;        &#x2F;&#x2F;容器的init进程在宿主机上的 PIDId          string &#96;json:&quot;id&quot;&#96;         &#x2F;&#x2F;容器IdName        string &#96;json:&quot;name&quot;&#96;       &#x2F;&#x2F;容器名Command     string &#96;json:&quot;command&quot;&#96;    &#x2F;&#x2F;容器内init运行命令CreatedTime string &#96;json:&quot;createTime&quot;&#96; &#x2F;&#x2F;创建时间Status      string &#96;json:&quot;status&quot;&#96;     &#x2F;&#x2F;容器的状态Volume      string &#96;json:&quot;volume&quot;&#96;&#125;</code></pre><p>然后将<code>RootURL</code>，<code>MntURL</code>，<code>WriteLayer</code>设为常量：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&#x2F;&quot;ConfigName          string &#x3D; &quot;config.json&quot;ContainerLogFile    string &#x3D; &quot;container.log&quot;RootURL             string &#x3D; &quot;&#x2F;root&#x2F;&quot;MntURL              string &#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;%s&#x2F;&quot;WriteLayerURL       string &#x3D; &quot;&#x2F;root&#x2F;writeLayer&#x2F;%s&quot;)</code></pre><p>相应地，<code>NewParentProcess</code> 函数也要修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p><code>NewWorkSpace</code>函数的三个参数分别改为：<code>volume</code>，<code>imageName</code>，<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(volume, imageName, containerName string) &#123;CreateReadOnlyLayer(imageName)CreateWriteLayer(containerName)CreateMountPoint(containerName, imageName)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(volumeURLs, containerName)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;</code></pre><p>下面来修改<code>CreateReadOnlyLayer</code>，<code>CreateWriteLayer</code>，<code>CreateMountPoint</code>这三个函数：</p><p>首先是 <code>CreateReadOnlyLayer</code>，参数名改为<code>imageName</code>，镜像解压出来的只读层以<code>RootURL+imageName</code> 命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateReadOnlyLayer(imageName string) error &#123;unTarFolderURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;&#x2F;&quot;imageURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;exist, err :&#x3D; PathExists(unTarFolderURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, unTarFolderURL, err)return err&#125;if !exist &#123;if err :&#x3D; os.MkdirAll(unTarFolderURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, unTarFolderURL, err)return err&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, imageURL, &quot;-C&quot;, unTarFolderURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, unTarFolderURL, err)return err&#125;&#125;return nil&#125;</code></pre><p><code>CreateWriteLayer</code> 为每个容器创建一个读写层，把参数改为containerName，容器读写层修改为 <code>WriteLayerURL+containerName</code>命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateWriteLayer(containerName string) &#123;writeUrl :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.MkdirAll(writeUrl, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;Mkdir write layer dir %s error. %v&quot;, writeUrl, err)&#125;&#125;</code></pre><p><code>CreateMountPoint</code>创建容器根目录，然后把镜像只读层和容器读写层挂载到容器根目录，成为容器文件系统，参数列表改为<code>containerName</code> 和 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateMountPoint(containerName, imageName string) error &#123;&#x2F;&#x2F; create mnt folder as mount pointmntURL :&#x3D; fmt.Sprintf(MntURL, containerName)if err :&#x3D; os.MkdirAll(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)return err&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;tmpWriteLayer :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)tmpImageLocation :&#x3D; RootURL + &quot;&#x2F;&quot; + imageNamedirs :&#x3D; &quot;dirs&#x3D;&quot; + tmpWriteLayer + &quot;:&quot; + tmpImageLocation_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;run command for creating mount point failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p><code>MountVolume</code> 根据用户输入的 volume参数获取相应挂载宿主机数据卷 URL 和容器的挂载点URL，并挂载数据卷。参数列表改为 <code>volumeURLs</code> 和<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerVolumeURL :&#x3D; mntURL + &quot;&#x2F;&quot; + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURL_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>然后在删除容器的 <code>removeContainer</code> 函数最后加一行<code>DeleteWorkSpace</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;container.DeleteWorkSpace(containerInfo.Volume, containerName)&#125;</code></pre><p>然后 <code>DeleteWorkSpace</code>也要修改，<code>DeleteWorkSpace</code>作用是当容器退出时，删除容器相关文件系统，参数列表改为 containerName 和volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(volume, containerName string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(volumeURLs, containerName)&#125; else &#123;DeleteMountPoint(containerName)&#125;&#125; else &#123;DeleteMountPoint(containerName)&#125;DeleteWriteLayer(containerName)&#125;</code></pre><p><code>DeleteMountPoint</code>函数作用是删除未挂载数据卷的容器文件系统，参数修改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(containerName string) error &#123;mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)return err&#125;return nil&#125;</code></pre><p><code>DeleteMountPointWithVolume</code>函数用来删除挂载数据卷容器的文件系统，参数列表改为<code>volumeURLs</code> 和 <code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; umount volume point in containermntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerURL :&#x3D; mntURL + &quot;&#x2F;&quot; + volumeURLs[1]if _, err :&#x3D; exec.Command(&quot;umount&quot;, containerURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)return err&#125;&#x2F;&#x2F; umount the whole point of the container_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;return nil&#125;</code></pre><p><code>DeleteWriteLayer</code> 函数用来删除容器读写层，参数改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWriteLayer(containerName string) &#123;writeURL :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;</code></pre><p>然后修改 <code>command.go</code> 中的<code>commitCommand</code>：输入参数名改为 <code>containerName</code> 和<code>imageName</code>：·</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]imageName :&#x3D; context.Args()[1]&#x2F;&#x2F; commitContainer(containerName)commitContainer(containerName, imageName)return nil&#125;,&#125;</code></pre><p>修改 <code>commit.go</code> 的 <code>commitContainer</code>函数，根据传入的 containerName 制作 <code>imageName.tar</code>镜像：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func commitContainer(containerName, imageName string) &#123;mntURL :&#x3D; fmt.Sprintf(container.MntURL, containerName)mntURL +&#x3D; &quot;&#x2F;&quot;imageTar :&#x3D; container.RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>测试一下，用 busybox 启动两个容器 test1 和 test2，test1 把宿主机<code>/root/from1</code> 挂载到容器 <code>/to1</code>，test2 把宿主机<code>/root/from2</code> 挂载到 <code>/to2</code> 下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test1 -v &#x2F;root&#x2F;from1:&#x2F;to1 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from1\&quot; \&quot;&#x2F;to1\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;# go run . run -d --name test2 -v &#x2F;root&#x2F;from2:&#x2F;to2 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from2\&quot; \&quot;&#x2F;to2\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1       11570       running     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>打开另一个终端，可以看到 <code>/root</code> 目录下多了<code>from1</code> 和 <code>from2</code> 两个目录，我们看看<code>mnt</code> 和 <code>writeLayer</code>，<code>mnt</code> 下多了两个busybox 的挂载层，<code>writeLayer</code>下分别挂载了两个容器的目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   └── to1└── test2    └── to2</code></pre><p>下面进入 test1 容器，创建 <code>/to1/test1.txt</code>：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . exec test1 sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 11570&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#x2F; # echo -e &quot;test1&quot; &gt;&gt; &#x2F;to1&#x2F;test1.txt&#x2F; # mkdir to1-1&#x2F; # echo -e &quot;test111111&quot; &gt;&gt; &#x2F;to1-1&#x2F;test1111.txt</code></pre><p>这时候再来看看可写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   ├── root│   ├── to1│   └── to1-1│       └── test1111.txt└── test2    └── to2# cat writeLayer&#x2F;test1&#x2F;to1-1&#x2F;test1111.txttest111111</code></pre><p>多了 <code>to1-1/test1111.txt</code>，那刚刚创建的<code>test1.txt</code> 去哪了呢？这时候我们看看<code>from1</code>，在这里，新创建的文件写入了数据卷。</p><p>下面来验证 commit 功能：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit test1 image1</code></pre><p>导出的镜像路径为 <code>/root/image1.tar</code>。</p><p>下面测试停止和删除容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1                   stopped     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51# go run . rm test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>我们看看容器根目录和可读写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls mnttest2# tree writeLayer&#x2F;writeLayer&#x2F;└── test2    └── to2</code></pre><p>test1 的容器根目录和可读写层被删除。</p><p>下面来试一下用镜像创建容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test3 -v &#x2F;root&#x2F;from3:&#x2F;to3 image1 top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from3\&quot; \&quot;&#x2F;to3\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:514713076733   test3       13056       running     top         2023-05-11 10:32:44</code></pre><p>这时我们可以看到 <code>/root</code> 多了一个 <code>image1</code>目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls image1bin  dev  etc  home  lib  lib64  proc  root  sys  tmp  to1  to1-1  usr  var</code></pre><p>在这里发现了刚才创建的 <code>to1-1</code>，用 <code>image1.tar</code>启动的容器 test3，进入容器后发现我们刚刚写入的文件，至此，我们成功把容器test1 的数据卷 to1 信息，重新写入了容器 test3 数据卷 to3。</p><p>在次小节后，进入容器都要指定镜像名，不然都会报错。</p><h4 id="实现容器指定环境变量运行-1">6.8 实现容器指定环境变量运行</h4><p>本节来实现让容器内运行的程序可以使用外部传递的环境变量。</p><h5 id="修改-runcommand-1">6.8.1 修改 runCommand</h5><p>在原来基础上增加 <code>-e</code>选项，允许用户指定环境变量，由于环境变量可以是多个，这里允许用户多次使用<code>-e</code> 来传递，同时添加对环境变量的解析，整体修改如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name:  &quot;d&quot;,Usage: &quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;name&quot;,Usage: &quot;container name&quot;,&#125;, &amp;cli.StringSliceFlag&#123;Name:  &quot;e&quot;,Usage: &quot;set environment&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)envSlice :&#x3D; context.StringSlice(&quot;e&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName, envSlice)return nil&#125;,&#125;</code></pre><h5 id="修改-run-函数-1">6.8.2 修改 Run 函数</h5><p>参数里新增一个 <code>envSlice</code>，然后传递给<code>NewParentProcess</code> 函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string, envSlice []string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName, envSlice)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><h5 id="修改-newparentprocess-函数-1">6.8.3 修改 NewParentProcess函数</h5><p>参数新增一个 <code>envSlice</code>，给 cmd 设置环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string, envSlice []string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;cmd.Env &#x3D; append(os.Environ(), envSlice...)NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it --name test -e test&#x3D;123 -e luck&#x3D;test busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;test&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#x2F; #  env | grep testtest&#x3D;123luck&#x3D;test</code></pre><p>可以看到，手动指定的环境变量在容器内可见。后面创建一个后台运行的容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:19:31+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9649354121   test        29524       running     top         2023-05-11 14:19:31# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 29524&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top    7 root      0:00 sh    8 root      0:00 ps -ef&#x2F; # env | grep test&#x2F; #</code></pre><p>查看环境变量，没有我们设置的环境变量。</p><p>这里不能用 env 命令获取设置的环境变量，原因是 exec 可以说 go发起的另一个进程，这个进程的父进程是宿主机的，这个，并不是容器内的。在cgo 内使用了 setns系统调用，才使得进程进入了容器内部的命名空间，但由于环境变量是继承自父进程的，因此这个exec 进程的环境变量其实是继承自宿主机，所以在 exec看到的环境变量其实是宿主机的环境变量。</p><p>但只要是容器内 pid 为 1的进程，创造出来的进程都会继承它的环境变量，下面来修改 exec命令来直接使用 env 命令来查看容器内环境变量的功能。</p><h5 id="修改-exec-命令-1">6.8.4 修改 exec 命令</h5><p>提供一个函数，可根据指定的 pid 来获取对应进程的环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getEnvsByPid(pid string) []string &#123;path :&#x3D; fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;environ&quot;, pid)contentBytes ,err :&#x3D; ioutil.ReadFile(path)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, path, err)return nil&#125;&#x2F;&#x2F; divide by &#39;\u0000&#39;envs :&#x3D; strings.Split(string(contentBytes),&quot;\u0000&quot;)return envs&#125;</code></pre><p>由于进程存放环境变量的位置是<code>/proc/$&#123;pid&#125;/environ</code>，因此根据给定的 pid去读取这个文件，可以获取环境变量，在文件的描述中，每个环境变量之间通过<code>\u0000</code> 分割，因此可以以此标记来获取环境变量数组。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;&#x2F;&#x2F; get target pid environ (container environ)containerEnvs :&#x3D; getEnvsByPid(pid)&#x2F;&#x2F; set host environ and container environ to exec processcmd.Env &#x3D; append(os.Environ(), containerEnvs...)if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>这里由于 exec命令依然要宿主机的一些环境变量，因此将宿主机环境变量和容器环境变量都一起放置到exec 进程中：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:03+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9729397397   test        50040       running     top         2023-05-11 14:30:03# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 50040&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#x2F; # env | grep testtest&#x3D;123luck&#x3D;test&#x2F; #</code></pre><p>现在可以看到 exec 进程可以获取前面 run 时设置的环境变量了。</p><h2 id="四网络篇-1">四、网络篇</h2><h3 id="容器网络-1">7. 容器网络</h3><h4 id="网络虚拟化技术-1">7.1 网络虚拟化技术</h4><h5 id="linux-虚拟网络设备-1">7.1.1 Linux 虚拟网络设备</h5><p>Linux是用网络设备去操作和使用网卡的，系统装了一个网卡后就会为其生成一个网络设备实例，例如eth0。Linux支持创建出虚拟化的设备，可通过组合实现多种多样的功能和网络拓扑，这里主要介绍Veth 和 Bridge。</p><p><strong>Linux Veth</strong></p><p>Veth 时成对出现的虚拟网络设备，发送到 Veth一端虚拟设备的请求会从另一端的虚拟设备中发出。容器的虚拟化场景中，常会使用Veth 连接不同的网络 namespace：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip netns add ns2# ip link add veth0 type veth peer name veth1# ip link set veth0 netns ns1# ip link set veth1 netns ns2# ip netns exec ns1 ip link1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:004: veth0@if3: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;ether 02:bf:18:99:77:ed brd ff:ff:ff:ff:ff:ff link-netns ns2</code></pre><p>在 ns1 和 ns2 的namespace 中，除 loopback的设备以外就只看到了一个网络设备。当请求发送到这个虚拟网络设备时，都会原封不动地从另一个网络namespace的网络接口中出来。例如，给两端分别配置不同地址后，向虚拟网络设备的一端发送请求，就能达到这个虚拟网络设备对应的另一端。</p><p><img src="0x0035/7.1.1-veth.png" style="zoom:43%;" /></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns exec ns1 ifconfig veth0 172.18.0.2&#x2F;24 up# ip netns exec ns2 ifconfig veth1 172.18.0.3&#x2F;24 up# ip netns exec ns1 route add default dev veth0# ip netns exec ns2 route add default dev veth1# ip netns exec ns1 ping -c 1 172.18.0.3PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.64 bytes from 172.18.0.3: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.395 ms--- 172.18.0.3 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.395&#x2F;0.395&#x2F;0.395&#x2F;0.000 ms</code></pre><p><strong>Linux Bridge</strong></p><p>进行下一步之前，先删除上一小节创建的 netns：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns del ns1# ip netns del ns2# ip netns list</code></pre><p>此时之前创建的两个 netns 被删除。</p><p>Bridge虚拟设备时用来桥接的网络设备，相当于现实世界的交换机，可以连接不同的网络设备，当请求达到Bridge 设备时，可以通过报文中的 Mac 地址进行广播或转发。例如，创建一个Bridge 设备，来连接 namespace 中的网络设备和宿主机上的网络：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip link add veth0 type veth peer name veth1# ip link set veth1 netns ns1########## 创建网桥# brctl addbr br0########## 挂载网络设备# brctl addif br0 eth0# brctl addif bro veth0</code></pre><p><img src="0x0035/7.1.1-bridge.png" /></p><h5 id="linux-路由表-1">7.1.2 Linux 路由表</h5><p>路由表是 Linux 内核的一个模块，通过定义路由表来决定在某个网络namespace 中包的流向，从而定义请求会到哪个网络设备上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip link set veth0 up# ip link set br0 up# ip netns exec ns1 ifconfig veth1 172.18.0.2&#x2F;24 up# ip netns exec ns1 route add default dev veth1# route add -net 172.18.0.0&#x2F;24 dev br0</code></pre><p><img src="0x0035/7.1.2-route.png" /></p><p>通过设置路由，对 IP地址的请求就能正确被路由到对应的网络设备上，从而实现通信：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ifconfig eth0eth0: flags&#x3D;4163&lt;UP,BROADCAST,RUNNING,MULTICAST&gt;  mtu 1500        inet 172.31.93.218  netmask 255.255.240.0  broadcast 172.31.95.255        inet6 fe80::215:5dff:fe4e:a16a  prefixlen 64  scopeid 0x20&lt;link&gt;        ether 00:15:5d:4e:a1:6a  txqueuelen 1000  (Ethernet)        RX packets 829  bytes 394161 (394.1 KB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 90  bytes 10335 (10.3 KB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0########## 在namespace访问宿主机# ip netns exec ns1 ping -c 1 172.31.93.218PING 172.31.93.218 (172.31.93.218) 56(84) bytes of data.64 bytes from 172.31.93.218: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.556 ms--- 172.31.93.218 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.556&#x2F;0.556&#x2F;0.556&#x2F;0.000 ms######### 从宿主机访问namespace的网络地址# ping -c 1 172.18.0.2PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.64 bytes from 172.18.0.2: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.113 ms--- 172.18.0.2 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.113&#x2F;0.113&#x2F;0.113&#x2F;0.000 ms</code></pre><h5 id="linux-iptables-1">7.1.3 Linux iptables</h5><p>iptables 是对 Linux 内核的 netfilter模块进行操作和展示的工具，用来管理包的流动和转送。iptables定义了一套链式处理的结构，在网络包传输的各个阶段可以使用不同的策略和包进行加工、传送或丢弃。在容器虚拟化技术里，常会用到两种策略，MASQUERADE和 DNAT，用于容器和宿主机外部的网络通信。</p><p><strong>MASQUERADE</strong></p><p>MASQUERADE 策略可以将请求包中的源地址转换为一个网络设备的地址，例如<a href="#7.1.2%20Linux%20路由表">7.1.2 Linux 路由表</a>这一小节里，namespace 中网络设备的地址是172.18.0.2，这个地址虽然在宿主机可以路由到 br0的网桥，但是到底宿主机外部后，是不知道如何路由到这个 IP的，所以如果请求外部地址的话，要先通过 MASQUERADE 策略将这个 IP转换为宿主机出口网卡的 IP：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># sysctl -w net.ipv4.conf.all.forwarding&#x3D;1net.ipv4.conf.all.forwarding &#x3D; 1# iptables -t nat -A POSTROUTING -s 172.18.0.0&#x2F;24 -o eth0 -j MASQUERADE</code></pre><p>在 namespace 中请求宿主机外部地址时，将 namespace中源地址转换为宿主机的地址作为源地址，就可以在 namespace中访问宿主机外的网络了。</p><p><strong>DAT</strong></p><p>iptables 中的 DNAT策略也是做网络地址的转换，不过它是要更换目标地址，常用于将内部网络地址的端口映射出去。例如，上面例子的namespace如果要提供服务给宿主机之外的应用要怎么办呢？外部应用没办法直接路由到172.18.0.2 这个地址，这时候可以用 DNAT 策略。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80</code></pre><p>这样就可以把宿主机上的 80 端口的 TCP 请求转发到 namespace 的172.18.0.2:80，从而实现外部应用的调用。</p><h4 id="构建容器网络模型-1">7.2 构建容器网络模型</h4><h5 id="基本模型-1">7.2.1 基本模型</h5><h6 id="网络-1">网络</h6><p>网络是容器的一个集合，在这个网络上的容器可以相互通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Network struct &#123;    Name    string &#x2F;&#x2F; network name    IpRange *net.IPNet &#x2F;&#x2F; address    Driver  string &#x2F;&#x2F; network driver name&#125;</code></pre><h6 id="网络端点-1">网络端点</h6><p>网络端点用于连接网络与容器，保证容器内部与网络的通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Endpoint struct &#123;ID          string           &#96;json:&quot;id&quot;&#96;Device      netlink.Veth     &#96;json:&quot;dev&quot;&#96;IPAddress   net.IP           &#96;json:&quot;ip&quot;&#96;MacAddress  net.HardwareAddr &#96;json:&quot;mac&quot;&#96;Network     *NetworkPortMapping []string&#125;</code></pre><p>网络端点的信息传输需要靠网络功能的两个组件配合完成，分别为网络驱动和IPAM。</p><h6 id="网络驱动-1">网络驱动</h6><p>网络驱动是网络功能的一个组件，不同驱动对网络的创建、连接、销毁策略不同，通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type NetworkDriver interface &#123;Name() string &#x2F;&#x2F; driver nameCreate(subnet string, name string) (*Network, error)Delete(network Network) errorConnect(network *Network, endpoint *Endpoint) errorDisconnect(network Network, endpoint *Endpoint) error&#125;</code></pre><h6 id="ipam-1">IPAM</h6><p>IPAM 也是网络功能的一个组件，用于网络 IP 地址的分配和释放，包括容器的IP 和网络网关的 IP。主要功能如下：</p><ul><li><code>ipam.Allocate(*net.IPNet)</code> 从指定的 subnet 网段中分配IP　</li><li><code>ipam.Release(*net.IPNet, net.IP)</code> 从指定的 subnet网段中释放掉指定的 IP</li></ul><p>在构建下面的函数之前，先来补充一些书上没写的：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (defaultNetworkPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;network&#x2F;&quot; &#x2F;&#x2F; 默认网络配置信息存储位置drivers            &#x3D; map[string]NetworkDriver&#123;&#125; &#x2F;&#x2F; 驱动字典，存储驱动信息networks           &#x3D; map[string]*Network&#123;&#125; &#x2F;&#x2F; 网络字段，存储网络信息)</code></pre><h5 id="调用关系-1">7.2.2 调用关系</h5><h6 id="创建网络-1">创建网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateNetwork(driver, subnet, name string) error &#123;_, cidr, _ :&#x3D; net.ParseCIDR(subnet)    &#x2F;&#x2F; allocate gateway ip by IPAMgatewayIP, err :&#x3D; ipAllocator.Allocate(cidr)if err !&#x3D; nil &#123;return err&#125;cidr.IP &#x3D; gatewayIPnw, err :&#x3D; drivers[driver].Create(cidr.String(), name)if err !&#x3D; nil &#123;return err&#125;    &#x2F;&#x2F; save network inforeturn nw.dump(defaultNetworkPath)&#125;</code></pre><p>其中，network.dump 和 network.load方法是将这个网络的配置信息保存在文件系统中，或从网络的配置目录中的文件读取到网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) dump(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(dumpPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(dumpPath, 0644)&#125; else &#123;return err&#125;&#125;nwPath :&#x3D; path.Join(dumpPath, nw.Name)    &#x2F;&#x2F; create file while empty file, write only, no filenwFile, err :&#x3D; os.OpenFile(nwPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;defer nwFile.Close()nwJson, err :&#x3D; json.Marshal(nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;_, err &#x3D; nwFile.Write(nwJson)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;return nil&#125;func (nw *Network) load(dumpPath string) error &#123;nwConfigFile, err :&#x3D; os.Open(dumpPath)if err !&#x3D; nil &#123;return err&#125;defer nwConfigFile.Close()nwJson :&#x3D; make([]byte, 2000)n, err :&#x3D; nwConfigFile.Read(nwJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(nwJson[:n], nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error load nw info: %v&quot;, err)return err&#125;return nil&#125;</code></pre><h6 id="创建容器并连接网络-1">创建容器并连接网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Connect(networkName string, cinfo *container.ContainerInfo) error &#123;network, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;ip, err :&#x3D; ipAllocator.Allocate(network.IpRange)if err !&#x3D; nil &#123;return err&#125;ep :&#x3D; &amp;Endpoint&#123;ID:          fmt.Sprintf(&quot;%s-%s&quot;, cinfo.Id, networkName),IPAddress:   ip,Network:     network,PortMapping: cinfo.PortMapping,&#125;if err &#x3D; drivers[network.Driver].Connect(network, ep); err !&#x3D; nil &#123;return err&#125;if err &#x3D; configEndpointIpAddressAndRoute(ep, cinfo); err !&#x3D; nil &#123;return err&#125;return configPortMapping(ep, cinfo)&#125;</code></pre><h6 id="展示网络列表-1">展示网络列表</h6><p>从网络配置的目录中加载所有的网络配置信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Init() error &#123;var bridgeDriver &#x3D; BridgeNetworkDriver&#123;&#125;drivers[bridgeDriver.Name()] &#x3D; &amp;bridgeDriverif _, err :&#x3D; os.Stat(defaultNetworkPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(defaultNetworkPath, 0644)&#125; else &#123;return err&#125;&#125;filepath.Walk(defaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error &#123;         &#x2F;&#x2F; skip if dirif info.IsDir() &#123;return nil&#125;if strings.HasSuffix(nwPath, &quot;&#x2F;&quot;) &#123;return nil&#125;         &#x2F;&#x2F; load filename as network name_, nwName :&#x3D; path.Split(nwPath)nw :&#x3D; &amp;Network&#123;Name: nwName,&#125;if err :&#x3D; nw.load(nwPath); err !&#x3D; nil &#123;logrus.Errorf(&quot;error load network: %s&quot;, err)&#125;&#x2F;&#x2F; save network info to network dicnetworks[nwName] &#x3D; nwreturn nil&#125;)return nil&#125;</code></pre><p>遍历展示创建的网络：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListNetwork() &#123;w :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprint(w, &quot;NAME\tIpRange\tDriver\n&quot;)for _, nw :&#x3D; range networks &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\n&quot;,nw.Name,nw.IpRange.String(),nw.Driver,)&#125;if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;Flush error %v&quot;, err)return&#125;&#125;</code></pre><h6 id="删除网络-1">删除网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteNetwork(networkName string) error &#123;nw, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;if err :&#x3D; ipAllocator.Release(nw.IpRange, &amp;nw.IpRange.IP); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network gateway ip: %s&quot;, err)&#125;if err :&#x3D; drivers[nw.Driver].Delete(*nw); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network DriverError: %s&quot;, err)&#125;return nw.remove(defaultNetworkPath)&#125;</code></pre><p>删除网络的同时也删除配置目录的网络配置文件：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) remove(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(path.Join(dumpPath, nw.Name)); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125; else &#123;return os.Remove(path.Join(dumpPath, nw.Name))&#125;&#125;</code></pre><h4 id="容器地址分配-1">7.3 容器地址分配</h4><p>现在转到 <code>ipam.go</code>。</p><h5 id="数据结构定义-1">7.3.1 数据结构定义</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ipamDefaultAllocatorPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;ipam&#x2F;subnet.json&quot;type IPAM struct &#123;SubnetAllocatorPath stringSubnets             *map[string]string&#125;&#x2F;&#x2F; 初始化一个IPAM对象，并指定默认分配信息存储位置var ipAllocator &#x3D; &amp;IPAM&#123;SubnetAllocatorPath: ipamDefaultAllocatorPath,&#125;</code></pre><p>反序列化读取网段分配信息和序列化保存网段分配信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) load() error &#123;if _, err :&#x3D; os.Stat(ipam.SubnetAllocatorPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.Open(ipam.SubnetAllocatorPath)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()subnetJson :&#x3D; make([]byte, 2000)n, err :&#x3D; subnetConfigFile.Read(subnetJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(subnetJson[:n], ipam.Subnets)if err !&#x3D; nil &#123;logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)return err&#125;return nil&#125;func (ipam *IPAM) dump() error &#123;ipamConfigFileDir, _ :&#x3D; path.Split(ipam.SubnetAllocatorPath)if _, err :&#x3D; os.Stat(ipamConfigFileDir); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(ipamConfigFileDir, 0644)&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()ipamConfigJson, err :&#x3D; json.Marshal(ipam.Subnets)if err !&#x3D; nil &#123;return err&#125;_, err &#x3D; subnetConfigFile.Write(ipamConfigJson)if err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h5 id="地址分配-1">7.3.2 地址分配</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) &#123;ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;err &#x3D; ipam.load()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error dump allocation info, %v&quot;, err)&#125;_, subnet, _ &#x3D; net.ParseCIDR(subnet.String())one, size :&#x3D; subnet.Mask.Size()if _, exist :&#x3D; (*ipam.Subnets)[subnet.String()]; !exist &#123;        &#x2F;&#x2F; 用0填满网段的配置，1&lt;&lt;uint8(size-one)表示这个网段中有多少个可用地址        &#x2F;&#x2F; size-one时子网掩码后面的网络位数，2^(size-one)表示网段中的可用IP数        &#x2F;&#x2F; 2^(size-one)等价于1&lt;&lt;uint8(size-one)        (*ipam.Subnets)[subnet.String()] &#x3D; strings.Repeat(&quot;0&quot;, 1&lt;&lt;uint8(size-one))&#125;&#x2F;&#x2F; 这里的原理建议大家看看原著for c :&#x3D; range (*ipam.Subnets)[subnet.String()] &#123;if (*ipam.Subnets)[subnet.String()][c] &#x3D;&#x3D; &#39;0&#39; &#123;            ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])            &#x2F;&#x2F; go的字符串创建后不能修改，先用byte存储            ipalloc[c] &#x3D; &#39;1&#39;            (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)            &#x2F;&#x2F;             ip &#x3D; subnet.IP                        &#x2F;&#x2F; 通过网段的IP与上面的偏移相加得出分配的IP，由于IP是一个uint的一个数组，需要通过数组中的每一项加所需要的值，例 &#x2F;&#x2F; 如网段是172.16.0.0&#x2F;12，数组序号是65555，那就要在[172,16,0,0]上依次加            &#x2F;&#x2F; [uint8(65555 &gt;&gt; 24), uint8(65555 &gt;&gt; 16), uint8(65555 &gt;&gt; 8), uint(65555 &gt;&gt; 4)]，即[0,1,0,19]，            &#x2F;&#x2F; 那么获得的IP就是172.17.0.19            for t :&#x3D; uint(4); t &gt; 0; t-- &#123;                []byte(ip)[4-t] +&#x3D; uint8(c &gt;&gt; ((t - 1) * 8))            &#125;            &#x2F;&#x2F; 由于此处IP是从1开始分配的，所以最后再加1，最终得到分配的IP是172.16.0.20            ip[3]++            break&#125;&#125;ipam.dump()return&#125;</code></pre><h5 id="地址释放-1">7.3.3 地址释放</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error &#123;    ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;    _, subnet, _ &#x3D; net.ParseCIDR(subnet.String())    err :&#x3D; ipam.load()    if err !&#x3D; nil &#123;        logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)    &#125;    c :&#x3D; 0    &#x2F;&#x2F; 将IP转换为4个字节的表示方式    releaseIP :&#x3D; ipaddr.To4()    &#x2F;&#x2F; 由于IP是从1开始分配的，所以转换成索引减1    releaseIP[3] -&#x3D; 1    for t :&#x3D; uint(4); t &gt; 0; t -&#x3D; 1 &#123;        &#x2F;&#x2F; 和分配IP相反，释放IP获得索引的方式是IP的每一位相减后分别左移将对应的数值加到索引上        c +&#x3D; int(releaseIP[t-1]-subnet.IP[t-1]) &lt;&lt; ((4 - t) * 8)    &#125;    ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])    ipalloc[c] &#x3D; &#39;0&#39;    (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)    ipam.dump()    return nil&#125;</code></pre><p>根据书上，写到这里就开始测试了，但是我们看看IDE，红海一片，所以我们接着实现。</p><h4 id="创建-bridge-网络-1">7.4 创建 bridge 网络</h4><h5 id="实现-bridge-driver-create-1">7.4.1 实现 Bridge DriverCreate</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) &#123;ip, ipRange, _ :&#x3D; net.ParseCIDR(subnet)ipRange.IP &#x3D; ipn :&#x3D; &amp;Network&#123;Name:    name,IpRange: ipRange,Driver:  d.Name(),&#125;err :&#x3D; d.initBridge(n)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error init bridge: %v&quot;, err)&#125;return n, err&#125;</code></pre><h5 id="bridge-driver-初始化-linux-bridge-1">7.4.2 Bridge Driver 初始化Linux Bridge</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) initBridge(n *Network) error &#123;&#x2F;&#x2F; 创建bridge虚拟设备bridgeName :&#x3D; n.Nameif err :&#x3D; createBridgeInterface(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;eror add bridge: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置bridge设备的地址和路由gatewayIP :&#x3D; *n.IpRangegatewayIP.IP &#x3D; n.IpRange.IPif err :&#x3D; setInterfaceIP(bridgeName, gatewayIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error assigning address: %s on bridge: %s with an error of: %v&quot;, gatewayIP, bridgeName, err)&#125;&#x2F;&#x2F; 启动bridge设备if err :&#x3D; setInterfaceUP(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error set bridge up: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置iptables的SNAT规则if err :&#x3D; setupIPTables(bridgeName, n.IpRange); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error setting iptables for %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="创建-bridge-设备-1">创建 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func createBridgeInterface(bridgeName string) error &#123;_, err :&#x3D; net.InterfaceByName(bridgeName)if err &#x3D;&#x3D; nil || !strings.Contains(err.Error(), &quot;no such network interface&quot;) &#123;return err&#125;&#x2F;&#x2F; create *netlink.Bridge objectla :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; bridgeNamebr :&#x3D; &amp;netlink.Bridge&#123;LinkAttrs: la&#125;if err :&#x3D; netlink.LinkAdd(br); err !&#x3D; nil &#123;return fmt.Errorf(&quot;bridge creation failed for bridge %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="设置-bridge-设备的地址和路由-1">设置 bridge设备的地址和路由</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceIP(name string, rawIP string) error &#123;retries :&#x3D; 2var iface netlink.Linkvar err errorfor i :&#x3D; 0; i &lt; retries; i++ &#123;iface, err &#x3D; netlink.LinkByName(name)if err &#x3D;&#x3D; nil &#123;break&#125;logrus.Debugf(&quot;error retrieving new bridge netlink link [ %s ]... retrying&quot;, name)time.Sleep(2 * time.Second)&#125;if err !&#x3D; nil &#123;return fmt.Errorf(&quot;abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot the error: %v&quot;, err)&#125;ipNet, err :&#x3D; netlink.ParseIPNet(rawIP)if err !&#x3D; nil &#123;return err&#125;addr :&#x3D; &amp;netlink.Addr&#123;IPNet:     ipNet,Peer:      ipNet,Label:     &quot;&quot;,Flags:     0,Scope:     0,Broadcast: nil,&#125;return netlink.AddrAdd(iface, addr)&#125;</code></pre><h6 id="启动-bridge-设备-1">启动 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceUP(interfaceName string) error &#123;iface, err :&#x3D; netlink.LinkByName(interfaceName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;error retrieving a link named [ %s ]: %v&quot;, iface.Attrs().Name, err)&#125;if err :&#x3D; netlink.LinkSetUp(iface); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error enabling interface for %s: %v&quot;, interfaceName, err)&#125;return nil&#125;</code></pre><h6 id="设置-iptables-linux-bridge-snat-规则-1">设置 iptables LinuxBridge SNAT 规则</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setupIPTables(bridgeName string, subnet *net.IPNet) error &#123;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE&quot;, subnet.String(), bridgeName)cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)&#125;return err&#125;</code></pre><h5 id="bridge-driver-delete-实现-1">7.4.3 Bridge Driver Delete实现</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Delete(network Network) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;return netlink.LinkDel(br)&#125;</code></pre><h4 id="在-bridge-网络创建容器-1">7.5 在 bridge 网络创建容器</h4><h5 id="挂载容器端点-1">7.5.1 挂载容器端点</h5><h6 id="连接容器网络端点到-linux-bridge-1">连接容器网络端点到 LinuxBridge</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;la :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; endpoint.ID[:5]la.MasterIndex &#x3D; br.Attrs().Indexendpoint.Device &#x3D; netlink.Veth&#123;LinkAttrs: la,PeerName:  &quot;cif-&quot; + endpoint.ID[:5],&#125;if err &#x3D; netlink.LinkAdd(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;if err &#x3D; netlink.LinkSetUp(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;return nil&#125;</code></pre><h6 id="配置容器-namespace-中网络设备及路由-1">配置容器 Namespace中网络设备及路由</h6><p>回到 <code>network.go</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;peerLink, err :&#x3D; netlink.LinkByName(ep.Device.PeerName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;fail config endpoint: %v&quot;, err)&#125;defer enterContainerNetns(&amp;peerLink, cinfo)()interfaceIP :&#x3D; *ep.Network.IpRangeinterfaceIP.IP &#x3D; ep.IPAddressif err &#x3D; setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;%v,%s&quot;, ep.Network, err)&#125;if err &#x3D; setInterfaceUP(ep.Device.PeerName); err !&#x3D; nil &#123;return err&#125;if err &#x3D; setInterfaceUP(&quot;lo&quot;); err !&#x3D; nil &#123;return err&#125;_, cidr, _ :&#x3D; net.ParseCIDR(&quot;0.0.0.0&#x2F;0&quot;)defaultRoute :&#x3D; &amp;netlink.Route&#123;LinkIndex: peerLink.Attrs().Index,Gw:        ep.Network.IpRange.IP,Dst:       cidr,&#125;if err &#x3D; netlink.RouteAdd(defaultRoute); err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h6 id="进入容器-net-namespace-1">进入容器 Net Namespace</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() &#123;f, err :&#x3D; os.OpenFile(fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;net&quot;, cinfo.Pid), os.O_RDONLY, 0)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get container net namespace, %v&quot;, err)&#125;nsFD :&#x3D; f.Fd()runtime.LockOSThread()if err &#x3D; netlink.LinkSetNsFd(*enLink, int(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set link netns , %v&quot;, err)&#125;origns, err :&#x3D; netns.Get()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get current netns, %v&quot;, err)&#125;if err &#x3D; netns.Set(netns.NsHandle(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set netns, %v&quot;, err)&#125;return func() &#123;netns.Set(origns)origns.Close()runtime.UnlockOSThread()f.Close()&#125;&#125;</code></pre><h6 id="配置宿主机到容器的端口映射-1">配置宿主机到容器的端口映射</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configPortMapping(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;for _, pm :&#x3D; range ep.PortMapping &#123;portMapping :&#x3D; strings.Split(pm, &quot;:&quot;)if len(portMapping) !&#x3D; 2 &#123;logrus.Errorf(&quot;port mapping format error, %v&quot;, pm)continue&#125;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s&quot;,portMapping[0], ep.IPAddress.String(), portMapping[1])cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)continue&#125;&#125;return nil&#125;</code></pre><h5 id="修补-bug-1">7.5.2 修补 bug</h5><p>写到这里，代码还是有很多 bug的，例如，<code>BridgeNetworkDriver</code> 未完全继承<code>NetworkDriver</code> 的所有函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Disconnect(network Network, endpoint *Endpoint) error &#123;return nil&#125;</code></pre><h5 id="测试-1">7.5.3 测试</h5><p>现在终于可以测试了。</p><p>首先创建一个网桥：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . network create --driver bridge --subnet 192.168.10.1&#x2F;24 testbridge</code></pre><p>然后启动两个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;8116248511&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#x2F; # ifconfigcif-81162 Link encap:Ethernet  HWaddr 16:62:68:81:E0:A9          inet addr:192.168.10.2  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::1462:68ff:fe81:e0a9&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:14 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1820 (1.7 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; #</code></pre><p>记住这个 IP：<code>192.168.10.2</code>，然后进入另一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;9558830402&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#x2F; # ifconfigcif-95588 Link encap:Ethernet  HWaddr 42:18:0A:73:33:CA          inet addr:192.168.10.3  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::4018:aff:fe73:33ca&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:10 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1248 (1.2 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; # ping 192.168.10.2PING 192.168.10.2 (192.168.10.2): 56 data bytes64 bytes from 192.168.10.2: seq&#x3D;0 ttl&#x3D;64 time&#x3D;2.619 ms64 bytes from 192.168.10.2: seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.086 ms^C--- 192.168.10.2 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 0.086&#x2F;1.352&#x2F;2.619 ms&#x2F; #</code></pre><p>可以看到，两个容器网络互通。</p><p>下面来试一下访问外部网络。我用的 WSL，默认的 nat是关闭的，前期各种设置 iptables规则什么的，都无法访问容器外部的网络，直到发现一篇帖子里说到，需要打开内核的nat功能，要将文件<code>/proc/sys/net/ipv4/ip_forward</code>内的值改为1（默认是0）。执行<code>sysctl -w net.ipv4.ip_forward=1</code> 即可。</p><p>修改之后，继续测试。</p><p>容器默认是没有 DNS 服务器的，需要我们手动添加：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # ping cn.bing.comping: bad address &#39;cn.bing.com&#39;&#x2F; # echo -e &quot;nameserver 8.8.8.8&quot; &gt; &#x2F;etc&#x2F;resolv.conf&#x2F; # ping cn.bing.comPING cn.bing.com (202.89.233.101): 56 data bytes64 bytes from 202.89.233.101: seq&#x3D;0 ttl&#x3D;113 time&#x3D;38.419 ms64 bytes from 202.89.233.101: seq&#x3D;1 ttl&#x3D;113 time&#x3D;39.011 ms^C--- cn.bing.com ping statistics ---3 packets transmitted, 2 packets received, 33% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 38.419&#x2F;38.715&#x2F;39.011 ms&#x2F; #</code></pre><p>然后再来测试容器映射端口到宿主机供外部访问：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -p 90:90 -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;3445154844&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#x2F; # nc -lp 90</code></pre><p>然后访问宿主机的 80 端口，看看能不能转发到容器里：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 172.31.93.218 90Trying 172.31.93.218...telnet: Unable to connect to remote host: Connection refused</code></pre><p>开始我以为是我哪里码错了，然后拿作者的代码来跑，并放到虚拟机上跑，发现并不是自己的问题，那只能这样测试了：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 192.168.10.3 90Trying 192.168.10.3...Connected to 192.168.10.3.Escape character is &#39;^]&#39;.</code></pre><p>出现这样的字眼后，容器和宿主机之间就可以通信了。</p><h2 id="参考链接-1">参考链接</h2><p><a href="https://learnku.com/articles/42072">七天用 Go 写个docker（第一天） | Go 技术论坛 (learnku.com)</a></p><p><a href="https://juejin.cn/post/6971335828060504094">使用 GoLang从零开始写一个 Docker（概念篇）-- 《自己动手写 Docker》读书笔记 - 掘金(juejin.cn)</a></p><p><ahref="https://blog.xtlsoft.top/read/server/building-wsl-kernel-with-aufs.html">编译带有AUFS 支持的 WSL 内核 - 徐天乐 :: 个人博客 (xtlsoft.top)</a></p><p><ahref="https://zhuanlan.zhihu.com/p/324530180">如何让WSL2使用自己编译的内核- 知乎 (zhihu.com)</a></p><p><ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p><ahref="https://juejin.cn/post/7086069688664326157#heading-1">自己动手写Docker系列-- 5.7实现通过容器制作镜像 - 掘金 (juejin.cn)</a></p><p><ahref="https://blog.csdn.net/tycoon1988/article/details/40781291">iptable端口重定向MASQUERADE_tycoon1988的博客-CSDN博客</a></p><p>可以看到，pid，ipc，network 方面都与宿主机进行了隔离。</p><h2 id="三镜像篇-2">三、镜像篇</h2><h3 id="构造镜像-2">5. 构造镜像</h3><h4 id="编译-aufs-内核-2">5.1 编译 aufs 内核</h4><p>因为电脑硬盘空间不太够，就不使用虚拟机来做实验了，笔者这里使用 WSL2来完成后续工作，然而，WSL2 Kernel 没有把 aufs编译进去，那只能换内核了，查阅资料，有两种更换内核的方法：</p><ul><li><p>直接替换 <code>C:\System32\lxss\tools\kernel</code> 文件</p></li><li><p>在 users 目录下新建 <code>.wslconfig</code> 文件：</p><pre class="line-numbers language-none"><code class="language-none">[wsl2]kernel&#x3D;&quot;要替换kernel的路径&quot;</code></pre></li></ul><p>很明显，我是不会满足于使用别人编译好的内核的，那我也来动手做一个。</p><h5 id="准备代码库-2">5.1.1 准备代码库</h5><p>我们先在 WSL 上准备好相关软件包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">apt update #更新源apt install build-essential flex bison libssl-dev libelf-dev gcc make</code></pre><p>编译内核需要从 GitHub 上 clone 微软官方的 WSL 代码和 AUFS-Standalone的代码库</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">git clone https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;WSL2-Linux-Kernel kernelgit clone https:&#x2F;&#x2F;github.com&#x2F;sfjro&#x2F;aufs-standalone aufs5</code></pre><p>然后查看 WSL 内核版本：在 wsl 下运行命令 <code>uname -r</code></p><p>例如我的内核版本是 5.15.19，那 kernel 和 aufs 都要切换到相应的分支去(kernel 默认就是 5.15.19，故不用切换)</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd aufs5git checkout aufs5.15.36</code></pre><p>然后退回到 kernel 文件夹给代码打补丁：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cat ..&#x2F;aufs5&#x2F;aufs5-mmap.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-base.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-kbuild.patch | patch -p1</code></pre><p>三个 Patch 的顺序无关。</p><p>然后再复制一点配置文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cp ..&#x2F;aufs5&#x2F;Documentation . -rcp ..&#x2F;aufs5&#x2F;fs&#x2F; . -rcp ..&#x2F;aufs5&#x2F;include&#x2F;uapi&#x2F;linux&#x2F;aufs_type.h .&#x2F;include&#x2F;uapi&#x2F;linux</code></pre><p>接下来我们来修改一下编译配置，在 <code>Microsoft/config-wsl</code>中任意位置增加一行：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini">CONFIG_AUFS_FS&#x3D;y</code></pre><p>最后，就可以开始编译了！</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">make KCONFIG_CONFIG&#x3D;Microsoft&#x2F;config-wsl -j8</code></pre><p>过程中会问你一些问题，我除了 AUFS Debug 都选了 y。</p><p>最后会在当前目录生成 <code>vmlinuz</code>，在<code>arch/x86/boot</code> 下生成 <code>bzImage</code>。</p><p>关闭 WSL 后更换内核，重启 WSL 输入<code>grep aufs /proc/filesystems</code>验证结果，如果出现 aufs的字样，说明操作成功。</p><h4 id="使用-busybox-创建容器-2">5.2 使用 busybox 创建容器</h4><h5 id="busybox-2">5.2.1 busybox</h5><p>先在 docker 获取 busybox 镜像并打包成一个 tar 包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">docker pull busyboxdocker run -d busybox top -bdocker export -o busybox.tar &lt;container_id&gt;</code></pre><p>将其复制到 WSL 下并解压。</p><h5 id="pivot_root-2">5.2.2 pivot_root</h5><p>pivot_root 是一个系统调用，作用是改变当前 root 文件系统。pivot_root可以将当前进程的 root 文件系统移动到 put_old 文件夹，然后使 new_root成为新的 root 文件系统。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func pivotRoot(root string) error &#123;&#x2F;&#x2F; remount the root dir, in order to make current root and old root in different file systemsif err :&#x3D; syscall.Mount(root, root, &quot;bind&quot;, syscall.MS_BIND|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;mount rootfs to itself error: %v&quot;, err)&#125;&#x2F;&#x2F; create &#39;rootfs&#x2F;.pivot_root&#39; to store old_rootpivotDir :&#x3D; filepath.Join(root, &quot;.pivot_root&quot;)if err :&#x3D; os.Mkdir(pivotDir, 0777); err !&#x3D; nil &#123;return err&#125;&#x2F;&#x2F; pivot_root mount on new rootfs, old_root mount on rootfs&#x2F;.pivot_rootif err :&#x3D; syscall.PivotRoot(root, pivotDir); err !&#x3D; nil &#123;return fmt.Errorf(&quot;pivot_root %v&quot;, err)&#125;&#x2F;&#x2F; change current work dir to root dirif err :&#x3D; syscall.Chdir(&quot;&#x2F;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;chdir &#x2F; %v&quot;, err)&#125;pivotDir &#x3D; filepath.Join(&quot;&#x2F;&quot;, &quot;.pivot_root&quot;)&#x2F;&#x2F; umount rootfs&#x2F;.rootfs_rootif err :&#x3D; syscall.Unmount(pivotDir, syscall.MNT_DETACH); err !&#x3D; nil &#123;return fmt.Errorf(&quot;umount pivot_root dir %v&quot;, err)&#125;&#x2F;&#x2F; del the temporary dirreturn os.Remove(pivotDir)&#125;</code></pre><p>有了这个函数就可以在 init 容器进程时，进行一系列的 mount 操作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setUpMount() error &#123;&#x2F;&#x2F; get current pathpwd, err :&#x3D; os.Getwd()if err !&#x3D; nil &#123;logrus.Errorf(&quot;get current location error: %v&quot;, err)return err&#125;logrus.Infof(&quot;current location: %v&quot;, pwd)pivotRoot(pwd)&#x2F;&#x2F; mount procdefaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEVif err :&#x3D; syscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc failed: %v&quot;, err)return err&#125;if err :&#x3D; syscall.Mount(&quot;tmpfs&quot;, &quot;&#x2F;dev&quot;, &quot;tmpfs&quot;, syscall.MS_NOSUID|syscall.MS_STRICTATIME, &quot;mode&#x3D;755&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;dev failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>tmpfs 是一种基于内存的文件系统，用 RAM 或 swap 分区来存储。</p><p>在 <code>NewParentProcess()</code> 中加一句<code>cmd.Dir="/root/busybox"</code>。</p><p>写完上述函数，然后在 <code>initProcess()</code> 中调用一下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">if err :&#x3D; setUpMount(); err !&#x3D; nil &#123;    logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)&#125;</code></pre><p>然后来运行测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it sh###### dividing live&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;busybox&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#x2F; #</code></pre><p>可以看到，容器当前目录被虚拟定位到了根目录，其实是在宿主机上映射的<code>/root/busybox</code>。</p><h5 id="用-aufs-包装-busybox-2">5.2.3 用 AUFS 包装 busybox</h5><p>前面提到了，docker 使用 AUFS 存储镜像和容器。docker在使用镜像启动一个容器时，会新建 2 个 layer：write layer 和container-init-layer。write layer是容器唯一的可读写层，container-init-layer是为容器新建的只读层，用来存储容器启动时传入的系统信息。</p><ul><li><code>CreateReadOnlyLayer()</code> 新建 <code>busybox</code>文件夹，解压 <code>busybox.tar</code> 到 <code>busybox</code>目录下，作为容器只读层。</li><li><code>CreateWriteLayer()</code> 新建一个 <code>writeLayer</code>文件夹，作为容器唯一可写层。</li><li><code>CreateMountPoint()</code> 先创建了 <code>mnt</code>文件夹作为挂载点，再把 <code>writeLayer</code> 目录和<code>busybox</code> 目录 mount 到 <code>mnt</code> 目录下。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; extra tar to &#39;busybox&#39;, used as the read only layer for containerfunc CreateReadOnlyLayer(rootURL string) &#123;busyboxURL :&#x3D; rootURL + &quot;busybox&#x2F;&quot;busyboxTarURL :&#x3D; rootURL + &quot;busybox.tar&quot;exist, err :&#x3D; PathExists(busyboxURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, busyboxURL, err)&#125;if !exist &#123;if err :&#x3D; os.Mkdir(busyboxURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, busyboxURL, err)&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, busyboxTarURL, &quot;-C&quot;, busyboxURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, busyboxTarURL, err)&#125;&#125;&#125;&#x2F;&#x2F; create a unique folder as writeLayerfunc CreateWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.Mkdir(writeURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, writeURL, err)&#125;&#125;func CreateMountPoint(rootURL string, mntURL string) &#123;&#x2F;&#x2F; create mnt folder as mount pointif err :&#x3D; os.Mkdir(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;dirs :&#x3D; &quot;dirs&#x3D;&quot; + rootURL + &quot;writeLayer:&quot; + rootURL + &quot;busybox&quot;cmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;&#125;func NewWorkSpace(rootURL, mntURL string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)&#125;</code></pre><p>接下来在 <code>NewParentProcess()</code> 将容器使用的宿主机目录<code>/root/busybox</code> 替换为 <code>/root/mnt</code>，这样使用 AUFS系统启动容器的代码就完成了。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;NewWorkSpace(rootURL, mntURL)cmd.Dir &#x3D; mntURLreturn cmd, writePipe</code></pre><p>docker 会在删除容器时，把容器对应的 write layer 和container-init-layer 删除，而保留镜像中所有的内容。</p><ul><li><code>DeleteMountPoint()</code> 中 umount <code>mnt</code>目录。</li><li>删除 <code>mnt</code> 目录。</li><li>在 <code>DeleteWriteLayer()</code> 删除 <code>writeLayer</code>文件夹。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(rootURL string, mntURL string) &#123;cmd :&#x3D; exec.Command(rootURL, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)&#125;&#125;func DeleteWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;func DeleteWorkSpace(rootURL, mntURL string) &#123;DeleteMountPoint(rootURL, mntURL)DeleteWriteLayer(rootURL)&#125;</code></pre><p>现在来启动一个容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it shdirs&#x3D;&#x2F;root&#x2F;writeLayer:&#x2F;root&#x2F;busybox&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#x2F; #</code></pre><p>测试在容器内创建文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # mkdir aaa&#x2F; # touch aaa&#x2F;test.txt</code></pre><p>此时我们可以在宿主机终端查看<code>/root/mnt/writeLayer</code>，可以看到刚才新建的 <code>aaa</code>文件夹和 <code>test.txt</code>，在我们退出容器后，<code>/root/mnt</code>文件夹被删除，伴随着刚才创建的文件夹和文件都被删除，而作为镜像的 busybox仍被保留，且内容未被修改。</p><h4 id="实现-volume-数据卷-2">5.3 实现 volume 数据卷</h4><p>上节实现了容器和镜像的分离，但是如果容器退出，容器可写层的所有内容就会被删除，这里使用volume 来实现容器数据持久化。</p><p>先在 <code>command.go</code> 里添加 <code>-v</code> 标签：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;,         &#x2F;&#x2F; add &#96;-v&#96; tag         &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;         &#x2F;&#x2F; send volume args to Run()volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig,volume)return nil&#125;,&#125;</code></pre><p>在 <code>Run()</code> 中，把 volume 传给创建容器的<code>NewParentProcess()</code> 和删除容器文件系统的<code>DeleteWorkSpace()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)initProcess.Wait()rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>在 <code>NewWorkSpace()</code> 中，继续把 volume传给创建容器文件系统的 <code>NewWorkSapce()</code>。</p><p>创建容器文件系统过程如下：</p><ul><li>创建只读层。</li><li>创建容器读写层。</li><li>创建挂载点并把只读层和读写层挂载到挂载点上。</li><li>判断 volume是否为空，如果是，说明用户没有使用挂载标签，结束创建过程。</li><li>不为空，就用 <code>volumeURLExtract()</code> 解析。</li><li>当 <code>volumeURLExtract()</code> 返回字符数组长度为2，且数据元素均不为空时，则执行 <code>MountVolume()</code>来挂载数据卷。<ul><li>否则提示用户创建数据卷输入值不对。</li></ul></li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(rootURL, mntURL, volume string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(rootURL, mntURL, volumeURLs)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;func volumeUrlExtract(volume string) []string &#123;&#x2F;&#x2F; divide volume by &quot;:&quot;return strings.Split(volume, &quot;:&quot;)&#125;</code></pre><p>挂载数据卷过程如下：</p><ul><li>读取宿主机文件目录 URL，创建宿主机文件目录(<code>/root/$&#123;parentURL&#125;</code>)</li><li>读取容器挂载点 URL，在容器文件系统里创建挂载点(<code>/root/mnt/$&#123;containerURL&#125;</code>)</li><li>把宿主机文件目录挂载到容器挂载点，这样启动容器的过程，对数据卷的处理就完成了。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]containerVolumeURL :&#x3D; mntURL + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURLcmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)&#125;&#125;</code></pre><p>删除容器文件系统过程如下：</p><ul><li>在 volume 不为空，且使用 <code>volumeURLExtract()</code> 解析 volume字符串返回的字符数组长度为 2，数据元素均不为空时，才执行<code>DeleteMountPointWithVolume()</code> 来处理。</li><li>其余情况仍使用前面的 <code>DeleteMountPoint()</code>。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(rootURL, mntURL, volume string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;DeleteWriteLayer(rootURL)&#125;</code></pre><p><code>DeleteMountPointWithVolume()</code> 处理逻辑如下：</p><ul><li>卸载 volume 挂载点的文件系统(<code>/root/mnt/$&#123;containerURL&#125;</code>)，保证整个容器挂载点没有再被使用。</li><li>卸载整个容器文件系统挂载点 (<code>/root/mnt</code>)。</li><li>删除容器文件系统挂载点。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; umount volume point in containercontainerURL :&#x3D; mntURL + volumeURLs[1]cmd :&#x3D; exec.Command(&quot;umount&quot;, containerURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)&#125;&#x2F;&#x2F; umount the whole point of the containercmd &#x3D; exec.Command(&quot;umount&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>接下来启动容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; #</code></pre><p>进入 <code>containerVolume</code>，创建一个文本文件，并随便写点东西：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;test&quot; &gt;&gt; test.txt</code></pre><p>此时我们能在宿主机的 <code>/root/volume</code>找到我们刚才创建的文本文件。退出容器后，volume文件夹也没有被删除。再次进入容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">r# go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;mkdir parent dir &#x2F;root&#x2F;volume error. mkdir &#x2F;root&#x2F;volume: file exists&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; # ls containerVolume&#x2F;test.txt</code></pre><p>此时这里会提示 volume 文件夹存在，我们在 <code>test.txt</code>内追加内容：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;###&quot; &gt;&gt; test.txt</code></pre><p>此时再次退出容器，能看到修改过后的文件内容，可以看到 volume文件夹没有被删除。</p><h4 id="简单镜像打包-2">5.4 简单镜像打包</h4><p>容器在退出时会删除所有可写层的内容，commit命令可以把运行状态容器的内容存储为镜像保存下来。</p><p>在 <code>main.go</code> 里添加 <code>commit</code> 命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    InitCommand,    RunCommand,    CommitCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里实现 <code>CommitCommand</code>命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;imageName :&#x3D; context.Args()[0]&#x2F;&#x2F; commitContainer(containerName)commitContainer(imageName)return nil&#125;,&#125;</code></pre><p>添加 <code>commit.go</code>，通过 <code>commitContainer()</code>实现将容器文件系统打包成 <code>$&#123;imagename&#125;.tar</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&#x2F;exec&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;)func commitContainer(imageName string) &#123;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&quot;imageTar :&#x3D; &quot;&#x2F;root&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>运行测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it sh</code></pre><p>然后在另一个终端运行：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit image</code></pre><p>这时候可以在 root 目录下看到多了一个 <code>image.tar</code>，解压后可以发现压缩包的内容和 <code>/root/mnt</code> 一致。</p><blockquote><p>tips：一定要先运行容器！如果不运行容器直接打包，会提示<code>/root/mnt</code> 不存在。</p></blockquote><h3 id="构建容器进阶-2">6. 构建容器进阶</h3><h4 id="实现容器后台运行-2">6.1 实现容器后台运行</h4><p>容器，放在操作系统层面，就是一个进程，当前运行命令的 simple-docker是主进程，容器是当前 simple-docker 进程 fork出来的子进程。子进程的结束和父进程的运行是一个异步的过程，即父进程不会知道子进程在什么时候结束。如果创建子进程时，父进程退出，那这个子进程就是孤儿进程(没人管)，此时进程号为 1 的进程 init 就会接受这些孤儿进程。</p><p>先在 <code>command.go</code> 添加 <code>-d</code>标签，表示这个容器启动时在后台运行：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach container         &#x2F;&#x2F; tty cannot work with detachif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume)return nil&#125;,&#125;</code></pre><p>然后也要修改一下 <code>run.go</code> 的 <code>Run()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)    &#x2F;&#x2F; if background process, parent process won&#39;t waitif tty &#123;initProcess.Wait()&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T15:32:44+08:00&quot;&#125;</code></pre><p>根据书上的提示，<code>ps -ef</code> 用来查找 top 进程：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ps -ef | grep toproot        3713     751  0 14:42 pts&#x2F;2    00:00:00 top</code></pre><p>前面几次运行命令，都找不到 top这个进程，于是我后面多跑了几次，终于看到了这个进程。。。</p><p>可以看到，top 命令的进程正在运行着，不过运行环境是 WSL，父进程 id不是 1，然后 <code>ps -ef</code> 查看一下，top 的父进程是一个 bash进程，而 bash 进程的父进程是一个 init 进程，这样应该算过了吧(偶尔的一两次不严谨)。</p><h4 id="实现查看运行中的容器-2">6.2 实现查看运行中的容器</h4><h5 id="name-标签-2">6.2.1 name 标签</h5><p>前面创建的容器里，所有关于容器的信息，例如PID、容器创建时间、容器运行命令等，都没有记录，这导致容器运行完后就在也不知道它的信息了，因此要把这部分信息保留。先在<code>command.go</code> 里加一个 name 标签，方便用户指定容器的名字：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag &#123;Name: &quot;name&quot;,Usage: &quot;container name&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume, containerName)return nil&#125;,&#125;</code></pre><p>添加一个方法来记录容器的相关信息，这里用先用一个 10位的数字来表示容器的 id：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func randStringBytes(n int) string &#123;letterBytes :&#x3D; &quot;1234567890&quot;rand.Seed(time.Now().UnixNano())b :&#x3D; make([]byte, n)for i :&#x3D; range b &#123;b[i] &#x3D; letterBytes[rand.Intn(len(letterBytes))]&#125;return string(b)&#125;</code></pre><p>这里用时间戳为种子，每次生成一个 10 以内的数字作为 letterBytes数组的下标，最后拼成整个容器的 id。容器的信息默认保存在<code>/var/run/simple-docker/$&#123;containerName&#125;/config.json</code>，容器基本格式如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;Id          string &#96;json:&quot;id&quot;&#96;Name        string &#96;json:&quot;name&quot;&#96;Command     string &#96;json:&quot;command&quot;&#96; &#x2F;&#x2F; the command that init process executeCreatedTime string &#96;json:&quot;created_time&quot;&#96;Status      string &#96;json:&quot;status&quot;&#96;&#125;var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&quot;ConfigName          string &#x3D; &quot;config.json&quot;)</code></pre><p>下面是记录容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;) &#x2F;&#x2F; format must like thiscommand :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>这里格式化的时间必须是<code>2006-01-02 15:04:05</code>，不然格式化后的时间会是几千年后doge。</p><p>详细可以看这篇文章：<ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p>在主函数加上调用：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>如果创建 tty 方式的容器，在容器退出后，就会删除相关信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func deleteContainerInfo(containerID string) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerID)if err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, dirURL, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top# go run . run -d --name jay top</code></pre><p>执行完成后，可以在 <code>/var/run/simple-docker/</code>找到两个文件夹，一个是随机 id，一个是 jay，文件夹下各有一个<code>config.json</code>，记录了容器的相关信息。</p><h5 id="实现-docker-ps-2">6.2.2 实现 docker ps</h5><p>在 <code>main.go</code> 加一个 <code>listCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,&#125;</code></pre><p>在 <code>command.go</code> 添加定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ListCommand &#x3D; cli.Command&#123;Name: &quot;ps&quot;,Usage: &quot;list all the containers&quot;,Action: func(context *cli.Context) error &#123;ListContainers()return nil&#125;,&#125;</code></pre><p>新建一个 <code>list.go</code>，实现记录列出容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListContainers() &#123;&#x2F;&#x2F; get the path that store the info of the containerdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, &quot;&quot;)dirURL &#x3D; dirURL[:len(dirURL)-1]&#x2F;&#x2F; read all the files in the directoryfiles, err :&#x3D; ioutil.ReadDir(dirURL)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read dir %s error %v&quot;, dirURL, err)return&#125;var containers []*container.ContainerInfofor _, file :&#x3D; range files &#123;tmpContainer, err :&#x3D; getContainerInfo(file)&#x2F;&#x2F; .Println(tmpContainer)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info error %v&quot;, err)continue&#125;containers &#x3D; append(containers, tmpContainer)&#125;&#x2F;&#x2F; use tabwriter.NewWriter to print the containerInfow :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprintf(w, &quot;ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n&quot;)for _, item :&#x3D; range containers &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\t%s\t%s\t%s\n&quot;,item.Id, item.Name, item.Pid, item.Status, item.Command, item.CreatedTime)&#125;&#x2F;&#x2F; refresh stdout if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;flush stdout error %v&quot;,err)return&#125;&#125;func getContainerInfo(file os.FileInfo) (*container.ContainerInfo, error) &#123;containerName :&#x3D; file.Name()&#x2F;&#x2F; create the absolute pathconfigFileDir :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFileDir &#x3D; configFileDir + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read config.jsoncontent, err :&#x3D; ioutil.ReadFile(configFileDir)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, configFileDir, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; turn json to containerInfoif err :&#x3D; json.Unmarshal(content, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>接上小节的测试，我们运行以下命令：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:11+08:00&quot;&#125;# go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:25+08:00&quot;&#125;# go run . psID           NAME         PID         STATUS      COMMAND     CREATED6675792962   6675792962   4317        running     top         2023-05-05 19:29:115553437308   jay          4404        running     top         2023-05-05 19:29:25</code></pre><p>现在就可以通过 ps 来看到所有创建的容器状态和它们的 init 进程 id了。</p><h4 id="查看容器日志-2">6.3 查看容器日志</h4><p>在 <code>main.go</code> 加一个 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,    LogCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里添加 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var LogCommand &#x3D; cli.Command&#123;Name:  &quot;logs&quot;,Usage: &quot;print logs of a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;contianerName :&#x3D; context.Args()[0]logContainer(contianerName)return nil&#125;,&#125;</code></pre><p>新建一个 <code>log.go</code>，定义 <code>logContainer()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func logContainer(containerName string) &#123;&#x2F;&#x2F; get the log pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)logFileLocation :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ContainerLogFile&#x2F;&#x2F; open log filefile, err :&#x3D; os.Open(logFileLocation)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container open file %s error: %v&quot;, logFileLocation, err)return&#125;defer file.Close()&#x2F;&#x2F; read log file contentcontent, err :&#x3D; ioutil.ReadAll(file)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container read file %s error: %v&quot;, logFileLocation, err)return&#125;&#x2F;&#x2F; use Fprint to transfer content to stdoutfmt.Fprint(os.Stdout, string(content))&#125;</code></pre><p>测试一下，先用 detach 方式创建一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-06T14:26:32+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED1837062451   jay         2065        running     top         2023-05-06 14:26:32# go run . logs jayMem: 3265116K used, 4568420K free, 3256K shrd, 71432K buff, 1135692K cachedCPU:  0.3% usr  0.2% sys  0.0% nic 99.3% idle  0.0% io  0.0% irq  0.0% sirqLoad average: 0.03 0.09 0.08 1&#x2F;521 5PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND</code></pre><p>可以看到，logs命令成功运行并输出容器的日志。(这里之前出现过前几次创建容器，而后台却没运行的情况，导致一开始运行logs 时报错了，建议在运行 logs 前多检查下 top 是否后台运行中)</p><h4 id="进入容器-namespace-2">6.4 进入容器 Namespace</h4><p>在 6.3小节里，实现了查看后台运行的容器的日志，但是容器一旦创建后，就无法再次进入容器，这一次来实现进入容器内部的功能，也就是exec。</p><h5 id="setns-2">6.4.1 setns</h5><p>setns 是一个系统调用，可根据提供的 PID 再次进入到指定的Namespace。它要先打开 <code>/proc/$&#123;pid&#125;/ns</code>文件夹下对应的文件，然后使当前进程进入到指定的 Namespace 中。对于 go来说，一个有多线程的进程使无法使用 setns 调用进入到对应的命名空间的，go没启动一个程序就会进入多线程状态，因此无法简单在 go里直接调用系统调用，这里还需要借助 C 来实现这个功能。</p><h5 id="cgo-2">6.4.2 Cgo</h5><p>在 go 里写 C：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package rand&#x2F;*#include &lt;stdlib.h&gt;*&#x2F;import &quot;C&quot;func Random() int &#123;    return int(C.random())&#125;func Seed(i int) &#123;    C.srandom(C.uint(i))&#125;</code></pre><h5 id="实现-2">6.4.3 实现</h5><p>先使用 C 根据 PID进入对应 Namespace：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenter&#x2F;*#define _GNU_SOURCE#include &lt;errno.h&gt;#include &lt;sched.h&gt;#include &lt;stdio.h&gt;#include &lt;stdlib.h&gt;#include &lt;string.h&gt;#include &lt;fcntl.h&gt;#include &lt;unistd.h&gt;&#x2F;&#x2F; if this package is quoted, this function will run automatic__attribute__((constructor)) void enter_namespace(void)&#123;    char *simple_docker_pid;    &#x2F;&#x2F; get pid from system environment    simple_docker_pid &#x3D; getenv(&quot;simple_docker_pid&quot;);    if (simple_docker_pid)    &#123;        fprintf(stdout, &quot;got simple docker pid&#x3D;%s\n&quot;, simple_docker_pid);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker pid env skip nsenter&quot;);        &#x2F;&#x2F; if no specified pid, the func will exit        return;    &#125;    char *simple_docker_cmd;    simple_docker_cmd &#x3D; getenv(&quot;simple_docker_cmd&quot;);    if (simple_docker_cmd)    &#123;        fprintf(stdout, &quot;got simple docker cmd&#x3D;%s\n&quot;, simple_docker_cmd);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker cmd env skip nsenter&quot;);        &#x2F;&#x2F; if no specified cmd, the func will exit        return;    &#125;    int i;    char nspath[1024];    char *namespace[] &#x3D; &#123;&quot;ipc&quot;, &quot;uts&quot;, &quot;net&quot;, &quot;pid&quot;, &quot;mnt&quot;&#125;;    for (i &#x3D; 0; i &lt; 5; i++)    &#123;        &#x2F;&#x2F; create the target path, like &#x2F;proc&#x2F;pid&#x2F;ns&#x2F;ipc        sprintf(nspath, &quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;%s&quot;, simple_docker_pid, namespace[i]);        int fd &#x3D; open(nspath, O_RDONLY);printf(&quot;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; %d %s\n&quot;, fd, nspath);        &#x2F;&#x2F; call sentns and enter the target namespace        if (setns(fd, 0) &#x3D;&#x3D; -1)        &#123;            fprintf(stderr, &quot;setns on %s namespace failed: %s\n&quot;, namespace[i], strerror(errno));        &#125;        else        &#123;            fprintf(stdout, &quot;setns on %s namespace succeeded\n&quot;, namespace[i]);        &#125;        close(fd);    &#125;    &#x2F;&#x2F; run command in target namespace    int res &#x3D; system(simple_docker_cmd);    exit(0);    return;&#125;*&#x2F;import &quot;C&quot;</code></pre><p>那如何使用这段代码呢，只需要在要加载的地方引用这个 package即可，我这里是 <code>nenster</code> 。</p><p>其实也可以，单独放在一个 C 文件里，go 文件可以这样写：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenterimport &quot;C&quot;</code></pre><p>下面增加 <code>ExecCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ExecCommand &#x3D; cli.Command&#123;Name:  &quot;exec&quot;,Usage: &quot;exec a command into container&quot;,Action: func(context *cli.Context) error &#123;if os.Getenv(ENV_EXEC_PID) !&#x3D; &quot;&quot; &#123;logrus.Infof(&quot;pid callback pid %v&quot;, os.Getgid())return nil&#125;if len(context.Args()) &lt; 2 &#123;return fmt.Errorf(&quot;missing container name or command&quot;)&#125;containerName :&#x3D; context.Args()[0]cmdArray :&#x3D; make([]string, len(context.Args())-1)for i, v :&#x3D; range context.Args().Tail() &#123;cmdArray[i] &#x3D; v&#125;ExecContainer(containerName, cmdArray)return nil&#125;,&#125;</code></pre><p>新建一个 <code>exec.go</code>下面实现获取容器名和需要的命令，并且在这里引用<code>nsenter</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ENV_EXEC_PID &#x3D; &quot;simple_docker_pid&quot;const ENV_EXEC_CMD &#x3D; &quot;simple_docker_cmd&quot;func getContainerPidByName(containerName string) (string, error) &#123;&#x2F;&#x2F; get the path that store container infodirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read files in target pathcontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;return &quot;&quot;, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to containerInfoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;return &quot;&quot;, err&#125;return containerInfo.Pid, nil&#125;func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run --name jay -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-07T13:23:09+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6530018751   jay         146639      running     top         2023-05-07 13:23:09# go run . logs jayMem: 4355160K used, 3478372K free, 3272K shrd, 208844K buff, 1581396K cachedCPU:  1.2% usr  0.6% sys  0.0% nic 97.9% idle  0.0% io  0.0% irq  0.1% sirqLoad average: 0.12 0.14 0.16 1&#x2F;574 6  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND# go run . exec jay sh&#x2F; # lsbin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top   13 root      0:00 sh   15 root      0:00 ps -ef&#x2F; #</code></pre><p>可以看到，成功进入容器内部，且与宿主机隔离。</p><p>这里出现了一个很奇怪的 bug，就是通过 cgo 去 setns，执行到 mnt时，抛出个错误：<code>Stale file handle</code>，当时找了全网，也找不到答案，于是陷入了两天的痛苦debug，在重新敲代码时，发现又不报错了，切换回那个有错误的分支，也不报错了。既然暂时找不到错误，先搁着吧，如果有看到这篇文章的朋友，也遇到了这个错误，可以留意下。(感觉会是一个雷)</p><p>(应该是容器的 mnt 没有 mount 上去，才会导致 stale file handle)</p><h4 id="停止容器-2">6.5 停止容器</h4><p>定义 <code>StopCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var StopCommand &#x3D; cli.Command&#123;Name:  &quot;stop&quot;,Usage: &quot;stop a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]stopContainer(containerName)return nil&#125;,&#125;</code></pre><p>然后声明一个函数，通过容器名来获取容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getContainerInfoByName(containerName string) (*container.ContainerInfo, error) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigNamecontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read config file %s error %v&quot;, configFilePath, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to container infoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json to container info error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>然后是停止容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func stopContainer(containerName string) &#123;&#x2F;&#x2F; get pid by containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container pid by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; turn pid(string) to intpidInt, err :&#x3D; strconv.Atoi(pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;convert pid from string to int error %v&quot;, err)return&#125;&#x2F;&#x2F; kill container main processif err :&#x3D; syscall.Kill(pidInt, syscall.SIGTERM); err !&#x3D; nil &#123;logrus.Errorf(&quot;stop container %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; get info of the containercontainerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; process is killed, update process statuscontainerInfo.Status &#x3D; container.STOPcontainerInfo.Pid &#x3D; &quot; &quot;&#x2F;&#x2F; update info to jsonnweContentBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;json marshal %s error %v&quot;, containerName, err)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; overwrite containerInfoif err :&#x3D; ioutil.WriteFile(configFilePath, nweContentBytes, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;write config file %s error %v&quot;, configFilePath, err)&#125;&#125;</code></pre><p>测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop jay# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6883605813   jay                     stopped     top# ps -ef | grep toproot       43588     761  0 20:00 pts&#x2F;0    00:00:00 grep --color&#x3D;auto top</code></pre><p>可以看到，jay 这个进程被停止了，且 pid 号设为空。</p><h4 id="删除容器-2">6.6 删除容器</h4><p>定义 <code>RemoveCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RemoveCommand &#x3D; cli.Command&#123;Name:  &quot;rm&quot;,Usage: &quot;remove a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]removeContainer(containerName)return nil&#125;,&#125;</code></pre><p>实现删除容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . rm jay# go run . psID          NAME        PID         STATUS      COMMAND     CREATED</code></pre><p>可以看到，jay 这个容器被删除了。</p><h4 id="通过容器制作镜像-2">6.7 通过容器制作镜像</h4><p>这一节，根据书上的内容，有许多函数需要改动。建议这里对着作者给出的源码debug，书上有部分内容有明显错误。</p><p>之前的文件系统如下：</p><ul><li>只读层：busybox，只读，容器系统的基础</li><li>可写层：writeLayer，容器内部的可写层</li><li>挂载层：mnt，挂载外部的文件系统，类似虚拟机的文件共享</li></ul><p>修改后的文件系统如下：</p><ul><li>只读层：不变</li><li>可写层：再加容器名为目录进行隔离，也就是<code>writeLayer/$&#123;containerName&#125;</code></li><li>挂载层：再加容器名为目录进行隔离，也就是<code>mnt/$&#123;containerName&#125;</code></li></ul><p>因此，本节要实现为每个容器分配单独的隔离文件系统，以及实现对不同容器打包镜像。</p><p><strong>修改 <code>run.go</code></strong></p><p>在 Run 函数参数列表添加一个 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><p>同时也在 <code>command.go</code> 的 runCommand 里修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName)return nil&#125;,</code></pre><p>在 <code>recordContainerInfo</code> 函数的参数列表添加 volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName, volume string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;)command :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,Volume:      volume,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>给 ContainerInfo 添加 Volume 成员：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;        &#x2F;&#x2F;容器的init进程在宿主机上的 PIDId          string &#96;json:&quot;id&quot;&#96;         &#x2F;&#x2F;容器IdName        string &#96;json:&quot;name&quot;&#96;       &#x2F;&#x2F;容器名Command     string &#96;json:&quot;command&quot;&#96;    &#x2F;&#x2F;容器内init运行命令CreatedTime string &#96;json:&quot;createTime&quot;&#96; &#x2F;&#x2F;创建时间Status      string &#96;json:&quot;status&quot;&#96;     &#x2F;&#x2F;容器的状态Volume      string &#96;json:&quot;volume&quot;&#96;&#125;</code></pre><p>然后将<code>RootURL</code>，<code>MntURL</code>，<code>WriteLayer</code>设为常量：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&#x2F;&quot;ConfigName          string &#x3D; &quot;config.json&quot;ContainerLogFile    string &#x3D; &quot;container.log&quot;RootURL             string &#x3D; &quot;&#x2F;root&#x2F;&quot;MntURL              string &#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;%s&#x2F;&quot;WriteLayerURL       string &#x3D; &quot;&#x2F;root&#x2F;writeLayer&#x2F;%s&quot;)</code></pre><p>相应地，<code>NewParentProcess</code> 函数也要修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p><code>NewWorkSpace</code>函数的三个参数分别改为：<code>volume</code>，<code>imageName</code>，<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(volume, imageName, containerName string) &#123;CreateReadOnlyLayer(imageName)CreateWriteLayer(containerName)CreateMountPoint(containerName, imageName)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(volumeURLs, containerName)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;</code></pre><p>下面来修改<code>CreateReadOnlyLayer</code>，<code>CreateWriteLayer</code>，<code>CreateMountPoint</code>这三个函数：</p><p>首先是 <code>CreateReadOnlyLayer</code>，参数名改为<code>imageName</code>，镜像解压出来的只读层以<code>RootURL+imageName</code> 命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateReadOnlyLayer(imageName string) error &#123;unTarFolderURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;&#x2F;&quot;imageURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;exist, err :&#x3D; PathExists(unTarFolderURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, unTarFolderURL, err)return err&#125;if !exist &#123;if err :&#x3D; os.MkdirAll(unTarFolderURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, unTarFolderURL, err)return err&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, imageURL, &quot;-C&quot;, unTarFolderURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, unTarFolderURL, err)return err&#125;&#125;return nil&#125;</code></pre><p><code>CreateWriteLayer</code> 为每个容器创建一个读写层，把参数改为containerName，容器读写层修改为 <code>WriteLayerURL+containerName</code>命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateWriteLayer(containerName string) &#123;writeUrl :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.MkdirAll(writeUrl, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;Mkdir write layer dir %s error. %v&quot;, writeUrl, err)&#125;&#125;</code></pre><p><code>CreateMountPoint</code>创建容器根目录，然后把镜像只读层和容器读写层挂载到容器根目录，成为容器文件系统，参数列表改为<code>containerName</code> 和 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateMountPoint(containerName, imageName string) error &#123;&#x2F;&#x2F; create mnt folder as mount pointmntURL :&#x3D; fmt.Sprintf(MntURL, containerName)if err :&#x3D; os.MkdirAll(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)return err&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;tmpWriteLayer :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)tmpImageLocation :&#x3D; RootURL + &quot;&#x2F;&quot; + imageNamedirs :&#x3D; &quot;dirs&#x3D;&quot; + tmpWriteLayer + &quot;:&quot; + tmpImageLocation_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;run command for creating mount point failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p><code>MountVolume</code> 根据用户输入的 volume参数获取相应挂载宿主机数据卷 URL 和容器的挂载点URL，并挂载数据卷。参数列表改为 <code>volumeURLs</code> 和<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerVolumeURL :&#x3D; mntURL + &quot;&#x2F;&quot; + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURL_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>然后在删除容器的 <code>removeContainer</code> 函数最后加一行<code>DeleteWorkSpace</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;container.DeleteWorkSpace(containerInfo.Volume, containerName)&#125;</code></pre><p>然后 <code>DeleteWorkSpace</code>也要修改，<code>DeleteWorkSpace</code>作用是当容器退出时，删除容器相关文件系统，参数列表改为 containerName 和volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(volume, containerName string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(volumeURLs, containerName)&#125; else &#123;DeleteMountPoint(containerName)&#125;&#125; else &#123;DeleteMountPoint(containerName)&#125;DeleteWriteLayer(containerName)&#125;</code></pre><p><code>DeleteMountPoint</code>函数作用是删除未挂载数据卷的容器文件系统，参数修改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(containerName string) error &#123;mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)return err&#125;return nil&#125;</code></pre><p><code>DeleteMountPointWithVolume</code>函数用来删除挂载数据卷容器的文件系统，参数列表改为<code>volumeURLs</code> 和 <code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; umount volume point in containermntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerURL :&#x3D; mntURL + &quot;&#x2F;&quot; + volumeURLs[1]if _, err :&#x3D; exec.Command(&quot;umount&quot;, containerURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)return err&#125;&#x2F;&#x2F; umount the whole point of the container_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;return nil&#125;</code></pre><p><code>DeleteWriteLayer</code> 函数用来删除容器读写层，参数改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWriteLayer(containerName string) &#123;writeURL :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;</code></pre><p>然后修改 <code>command.go</code> 中的<code>commitCommand</code>：输入参数名改为 <code>containerName</code> 和<code>imageName</code>：·</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]imageName :&#x3D; context.Args()[1]&#x2F;&#x2F; commitContainer(containerName)commitContainer(containerName, imageName)return nil&#125;,&#125;</code></pre><p>修改 <code>commit.go</code> 的 <code>commitContainer</code>函数，根据传入的 containerName 制作 <code>imageName.tar</code>镜像：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func commitContainer(containerName, imageName string) &#123;mntURL :&#x3D; fmt.Sprintf(container.MntURL, containerName)mntURL +&#x3D; &quot;&#x2F;&quot;imageTar :&#x3D; container.RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>测试一下，用 busybox 启动两个容器 test1 和 test2，test1 把宿主机<code>/root/from1</code> 挂载到容器 <code>/to1</code>，test2 把宿主机<code>/root/from2</code> 挂载到 <code>/to2</code> 下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test1 -v &#x2F;root&#x2F;from1:&#x2F;to1 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from1\&quot; \&quot;&#x2F;to1\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;# go run . run -d --name test2 -v &#x2F;root&#x2F;from2:&#x2F;to2 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from2\&quot; \&quot;&#x2F;to2\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1       11570       running     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>打开另一个终端，可以看到 <code>/root</code> 目录下多了<code>from1</code> 和 <code>from2</code> 两个目录，我们看看<code>mnt</code> 和 <code>writeLayer</code>，<code>mnt</code> 下多了两个busybox 的挂载层，<code>writeLayer</code>下分别挂载了两个容器的目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   └── to1└── test2    └── to2</code></pre><p>下面进入 test1 容器，创建 <code>/to1/test1.txt</code>：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . exec test1 sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 11570&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#x2F; # echo -e &quot;test1&quot; &gt;&gt; &#x2F;to1&#x2F;test1.txt&#x2F; # mkdir to1-1&#x2F; # echo -e &quot;test111111&quot; &gt;&gt; &#x2F;to1-1&#x2F;test1111.txt</code></pre><p>这时候再来看看可写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   ├── root│   ├── to1│   └── to1-1│       └── test1111.txt└── test2    └── to2# cat writeLayer&#x2F;test1&#x2F;to1-1&#x2F;test1111.txttest111111</code></pre><p>多了 <code>to1-1/test1111.txt</code>，那刚刚创建的<code>test1.txt</code> 去哪了呢？这时候我们看看<code>from1</code>，在这里，新创建的文件写入了数据卷。</p><p>下面来验证 commit 功能：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit test1 image1</code></pre><p>导出的镜像路径为 <code>/root/image1.tar</code>。</p><p>下面测试停止和删除容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1                   stopped     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51# go run . rm test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>我们看看容器根目录和可读写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls mnttest2# tree writeLayer&#x2F;writeLayer&#x2F;└── test2    └── to2</code></pre><p>test1 的容器根目录和可读写层被删除。</p><p>下面来试一下用镜像创建容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test3 -v &#x2F;root&#x2F;from3:&#x2F;to3 image1 top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from3\&quot; \&quot;&#x2F;to3\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:514713076733   test3       13056       running     top         2023-05-11 10:32:44</code></pre><p>这时我们可以看到 <code>/root</code> 多了一个 <code>image1</code>目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls image1bin  dev  etc  home  lib  lib64  proc  root  sys  tmp  to1  to1-1  usr  var</code></pre><p>在这里发现了刚才创建的 <code>to1-1</code>，用 <code>image1.tar</code>启动的容器 test3，进入容器后发现我们刚刚写入的文件，至此，我们成功把容器test1 的数据卷 to1 信息，重新写入了容器 test3 数据卷 to3。</p><p>在次小节后，进入容器都要指定镜像名，不然都会报错。</p><h4 id="实现容器指定环境变量运行-2">6.8 实现容器指定环境变量运行</h4><p>本节来实现让容器内运行的程序可以使用外部传递的环境变量。</p><h5 id="修改-runcommand-2">6.8.1 修改 runCommand</h5><p>在原来基础上增加 <code>-e</code>选项，允许用户指定环境变量，由于环境变量可以是多个，这里允许用户多次使用<code>-e</code> 来传递，同时添加对环境变量的解析，整体修改如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name:  &quot;d&quot;,Usage: &quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;name&quot;,Usage: &quot;container name&quot;,&#125;, &amp;cli.StringSliceFlag&#123;Name:  &quot;e&quot;,Usage: &quot;set environment&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)envSlice :&#x3D; context.StringSlice(&quot;e&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName, envSlice)return nil&#125;,&#125;</code></pre><h5 id="修改-run-函数-2">6.8.2 修改 Run 函数</h5><p>参数里新增一个 <code>envSlice</code>，然后传递给<code>NewParentProcess</code> 函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string, envSlice []string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName, envSlice)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><h5 id="修改-newparentprocess-函数-2">6.8.3 修改 NewParentProcess函数</h5><p>参数新增一个 <code>envSlice</code>，给 cmd 设置环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string, envSlice []string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;cmd.Env &#x3D; append(os.Environ(), envSlice...)NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it --name test -e test&#x3D;123 -e luck&#x3D;test busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;test&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#x2F; #  env | grep testtest&#x3D;123luck&#x3D;test</code></pre><p>可以看到，手动指定的环境变量在容器内可见。后面创建一个后台运行的容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:19:31+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9649354121   test        29524       running     top         2023-05-11 14:19:31# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 29524&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top    7 root      0:00 sh    8 root      0:00 ps -ef&#x2F; # env | grep test&#x2F; #</code></pre><p>查看环境变量，没有我们设置的环境变量。</p><p>这里不能用 env 命令获取设置的环境变量，原因是 exec 可以说 go发起的另一个进程，这个进程的父进程是宿主机的，这个，并不是容器内的。在cgo 内使用了 setns系统调用，才使得进程进入了容器内部的命名空间，但由于环境变量是继承自父进程的，因此这个exec 进程的环境变量其实是继承自宿主机，所以在 exec看到的环境变量其实是宿主机的环境变量。</p><p>但只要是容器内 pid 为 1的进程，创造出来的进程都会继承它的环境变量，下面来修改 exec命令来直接使用 env 命令来查看容器内环境变量的功能。</p><h5 id="修改-exec-命令-2">6.8.4 修改 exec 命令</h5><p>提供一个函数，可根据指定的 pid 来获取对应进程的环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getEnvsByPid(pid string) []string &#123;path :&#x3D; fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;environ&quot;, pid)contentBytes ,err :&#x3D; ioutil.ReadFile(path)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, path, err)return nil&#125;&#x2F;&#x2F; divide by &#39;\u0000&#39;envs :&#x3D; strings.Split(string(contentBytes),&quot;\u0000&quot;)return envs&#125;</code></pre><p>由于进程存放环境变量的位置是<code>/proc/$&#123;pid&#125;/environ</code>，因此根据给定的 pid去读取这个文件，可以获取环境变量，在文件的描述中，每个环境变量之间通过<code>\u0000</code> 分割，因此可以以此标记来获取环境变量数组。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;&#x2F;&#x2F; get target pid environ (container environ)containerEnvs :&#x3D; getEnvsByPid(pid)&#x2F;&#x2F; set host environ and container environ to exec processcmd.Env &#x3D; append(os.Environ(), containerEnvs...)if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>这里由于 exec命令依然要宿主机的一些环境变量，因此将宿主机环境变量和容器环境变量都一起放置到exec 进程中：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:03+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9729397397   test        50040       running     top         2023-05-11 14:30:03# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 50040&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#x2F; # env | grep testtest&#x3D;123luck&#x3D;test&#x2F; #</code></pre><p>现在可以看到 exec 进程可以获取前面 run 时设置的环境变量了。</p><h2 id="四网络篇-2">四、网络篇</h2><h3 id="容器网络-2">7. 容器网络</h3><h4 id="网络虚拟化技术-2">7.1 网络虚拟化技术</h4><h5 id="linux-虚拟网络设备-2">7.1.1 Linux 虚拟网络设备</h5><p>Linux是用网络设备去操作和使用网卡的，系统装了一个网卡后就会为其生成一个网络设备实例，例如eth0。Linux支持创建出虚拟化的设备，可通过组合实现多种多样的功能和网络拓扑，这里主要介绍Veth 和 Bridge。</p><p><strong>Linux Veth</strong></p><p>Veth 时成对出现的虚拟网络设备，发送到 Veth一端虚拟设备的请求会从另一端的虚拟设备中发出。容器的虚拟化场景中，常会使用Veth 连接不同的网络 namespace：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip netns add ns2# ip link add veth0 type veth peer name veth1# ip link set veth0 netns ns1# ip link set veth1 netns ns2# ip netns exec ns1 ip link1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:004: veth0@if3: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;ether 02:bf:18:99:77:ed brd ff:ff:ff:ff:ff:ff link-netns ns2</code></pre><p>在 ns1 和 ns2 的namespace 中，除 loopback的设备以外就只看到了一个网络设备。当请求发送到这个虚拟网络设备时，都会原封不动地从另一个网络namespace的网络接口中出来。例如，给两端分别配置不同地址后，向虚拟网络设备的一端发送请求，就能达到这个虚拟网络设备对应的另一端。</p><p><img src="0x0035/7.1.1-veth.png" style="zoom:43%;" /></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns exec ns1 ifconfig veth0 172.18.0.2&#x2F;24 up# ip netns exec ns2 ifconfig veth1 172.18.0.3&#x2F;24 up# ip netns exec ns1 route add default dev veth0# ip netns exec ns2 route add default dev veth1# ip netns exec ns1 ping -c 1 172.18.0.3PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.64 bytes from 172.18.0.3: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.395 ms--- 172.18.0.3 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.395&#x2F;0.395&#x2F;0.395&#x2F;0.000 ms</code></pre><p><strong>Linux Bridge</strong></p><p>进行下一步之前，先删除上一小节创建的 netns：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns del ns1# ip netns del ns2# ip netns list</code></pre><p>此时之前创建的两个 netns 被删除。</p><p>Bridge虚拟设备时用来桥接的网络设备，相当于现实世界的交换机，可以连接不同的网络设备，当请求达到Bridge 设备时，可以通过报文中的 Mac 地址进行广播或转发。例如，创建一个Bridge 设备，来连接 namespace 中的网络设备和宿主机上的网络：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip link add veth0 type veth peer name veth1# ip link set veth1 netns ns1########## 创建网桥# brctl addbr br0########## 挂载网络设备# brctl addif br0 eth0# brctl addif bro veth0</code></pre><h2 id="零前言-3">零、前言</h2><p>本文为《自己动手写 Docker》的学习，对于各位学习 docker的同学非常友好，非常建议买一本来学习。</p><p>书中有摘录书中的一些知识点，不过限于篇幅，没有全部摘录<del>(主要也是懒)</del>。项目仓库地址为：<ahref="https://github.com/JaydenChang/simple-docker">JaydenChang/simple-docker(github.com)</a></p><h2 id="一概念篇-3">一、概念篇</h2><h3 id="基础知识-3">1. 基础知识</h3><h4 id="kernel-3">1.1 kernel</h4><p>kernel (内核)指大多数操作系统的核心部分，由操作系统中用于管理存储器、文件、外设和系统资源的部分组成。操作系统内核通常运行进程，并提供进程间通信。</p><h4 id="namespace-3">1.2 namespace</h4><p>namespace 是 Linux 自带的功能来隔离内核资源的机制。</p><p>Linux 中有 6 种 namespace</p><h5 id="uts-namespace-3">1.2.1 UTS Namespace</h5><p>UTS，UNIX Time Sharing，用于隔离 nodeName (主机名) 和 domainName(域名)。在该 Namespace 下修改 hostname 不会影相其他的 Namespace。</p><h5 id="ipc-namespace-3">1.2.2 IPC Namespace</h5><p>IPC，Inter-Process Communication (进程间通讯)，用于隔离 System V IPC和 POSIX message queues (一种消息队列，结构为链表)。</p><p>两种 IPC 本质上差不多，System V IPC 随内核持续，POSIX IPC随进程持续。</p><h5 id="pid-namespace-3">1.2.3 PID Namespace</h5><p>PID，Process IDs，用于隔绝 PID。同样的进程，在不同 Namespace里是不同的 PID。新建的 PID Namespace 里第一个 PID 是1。</p><h5 id="mount-namespace-3">1.2.4 Mount Namespace</h5><p>用于隔绝文件系统，挂载了某一目录，在这个 Namespace下就会把这个目录当作根目录，我们看到的文件系统树就会以这个目录为根目录。</p><p>mount 操作本身不会影响到外部，docker 中的 volume也用到了这个特性。</p><h5 id="user-namespace-3">1.2.5 User Namespace</h5><p>用于 隔离用户组 ID。</p><h5 id="network-namespace-3">1.2.6 Network Namespace</h5><p>每个 Namespace 都有一套自己的网络设备，可以使用相同的端口号，映射到host 的不同端口。</p><h4 id="linux-cgroups-3">1.3 Linux Cgroups</h4><p>Cgroups 全称为 Control Groups，是 Linux内核提供的物理资源隔离机制。</p><h5 id="cgroups-的三个组件-3">1.3.1 Cgroups 的三个组件</h5><ul><li>cgroup：一个 cgroup 包含一组进程，且可以有 subsystem的参数配置，以关联一组 subsystem。</li><li>subsystem：一组资源控制的模块。</li><li>hierarchy：把一组 cgroups 串成一个树状结构，以提供继承的功能。</li></ul><h5 id="这三个组件的关联-3">1.3.2 这三个组件的关联</h5><p>Linux 有一些限制：</p><ul><li>首先，创建一个 hierarchy。这个 hierarchy 有一个 cgroup根节点，所有的进程都会被加到这个根节点上，所有在这个 hierarchy上创建的节点都是这个根节点的子节点。</li><li>一个 subsystem 只能加到一个 hierarchy 上。</li><li>但是一个 subsystem 可以加到同一个 hierarchy 的多个 cgroups 上。</li><li>一个 hierarchy 可以有多个 subsystem。</li><li>一个进程可以在多个 cgroups 中，但是这些 cgroup 必须在不同的hierarchy 中。</li><li>一个进程 fork 出子进程时，父进程和子进程属于同一个 cgroup。</li></ul><h5 id="cgroup-和-subsystem-和-hierarchy-之间的联系-3">1.3.3 cgroup 和subsystem 和 hierarchy 之间的联系</h5><ul><li>hierarchy 就是一颗 cgroups 树，由多个 cgroups 构成。每一个 hierarchy建立时会包含 ==<em>所有</em>== 的Linux 进程。这里的 “所有”就是当前系统运行中的所有进程，每个 hierarchy上的全部进程都是一样的，不同的 hierarchy指的其实只是不同的分组方式，这也是为什么一个进程可以存在于多个 hierarchy上；准确来说，一个进程一定会同时存在于所有的 hierarchy上，区别在被放在的 cgroup 可能会有差异。</li><li>Linux 的 subsystem 只有一个的说法，没有一种的说法，也就是在一个hierarchy 上使用了 memory subsystem，那么在其他 hierarchy 就不能使用memory subsystem 了。</li><li>subsystem 是一种资源控制器，有很多个 subsystem，每个 subsystem控制不同的资源。subsystem 和 cgroups 关联。新建一个 cgroups文件夹时，里面会自动生成一堆配置文件，那个就是 subsystem 配置文件。但<code>subsystem 配置文件</code> 不是 <code>subsystem</code>，就像<code>.git</code> 不是 <code>git</code> 一样，就像没安装 git也可以从别人那里获得 <code>.git</code>文件夹，只是不能用罢了。<code>subsystem 配置文件</code>也是如此，新建一个 cgroup 就会生成<code>cgroup 配置文件</code>，但并不代表你关联了一个subsystem。只有当改变了一个<code>cgroup 配置文件</code>，里面要限制某种资源时，就会自动关联到这个被限制的资源所对应的subsystem 上。</li><li>假设我的 Linux 有 12 个 subsystem，也就是说我最多只能建 12 个hierarchy (不加 subsystem 的情况下可以建更多 hierarchy，这样 cgroup就变成纯分组使用)。每一个 hierarchy 上一个 subsystem。如果在某个hierarchy 放多个 subsystem，能建立的 hierarchy就更少了。</li><li>subsystem 和 cgroup 是关联的，不是和 hierarchy关联的，但经常看到有人说把某个 subsystem 和某个 hierarchy关联。实质上一般指的是 hierarchy 中的某一个 cgroup 或多个 cgroup关联。</li></ul><h5 id="cgroup-的-kernel-接口-3">1.3.4 cgroup 的 kernel 接口</h5><p>kernel 接口，就是在 Linux 上调用 api 来控制 cgroups。</p><ol type="1"><li><p>首先创建一个 hierarchy，而 hierarchy要挂载到一个目录上，这里创建一个目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mkdir hierarchy-test</code></pre></li><li><p>然后挂载：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t cgroup -o none,name&#x3D;hierarchy-test hierarchy-test .&#x2F;hierarchy-test</code></pre></li><li><p>可以在这个目录下看到一大堆文件，这些文件就是 cgroup根节点的配置。</p></li><li><p>然后在这个目录下创建新的空目录，会发现，新的目录里也会有很多cgroup 配置文件，这些目录已成为 cgroup 根节点的子节点 cgroup。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">.├── cgroup.clone_children├── cgroup.procs├── cgroup.sane_behavior├── notify_on_release├── release_agent├── tasks└── temp  # 这是新创建的文件夹    ├── cgroup.clone_children    ├── cgroup.procs    ├── notify_on_release    └── tasks</code></pre></li><li><p>在 cgroup中添加和移动进程：系统的所有进程都会被放到根节点中，可以根据需要移动进程：</p><ul><li><p>只需将进程 ID 写到对应的 cgroup 的 tasks 文件即可。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo sh -c &quot;echo $$ &gt;&gt; tasks&quot;</code></pre><p>该命令就是将当前终端的这个进程加到当前所在的 cgroup 的目录的 tasks文件中。</p></li></ul></li><li><p>通过 subsystem 限制 cgroup 中进程的资源：</p><ul><li>上面的方法有个问题，因为这个 hierarchy 没有关联到任何subsystem，因此不能够控制资源。</li><li>不过其实系统会自动给每个 subsystem 创建一个hierarchy，所以通过控制这个 hierarchy里的配置，可以达到控制进程的目的。</li></ul></li></ol><h5 id="docker-是怎么使用-cgroups-的-3">1.3.5 docker 是怎么使用 Cgroups的</h5><p>docker 会给每个容器创建一个 cgroup，再限制该 cgroup的资源，从而达到限制容器的资源的作用。</p><p>其实写了这么多，综合上面的前置知识，不难猜测，docker的原理是：隔离主机。</p><h4 id="demo-3">1.4 Demo</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;fmt&quot;&quot;io&#x2F;ioutil&quot;&quot;os&quot;&quot;os&#x2F;exec&quot;&quot;path&quot;&quot;strconv&quot;&quot;syscall&quot;)const cgroupMemoryHierarchyCount &#x3D; &quot;&#x2F;sys&#x2F;fs&#x2F;cgroup&#x2F;memory&quot;func main() &#123;    &#x2F;&#x2F; 第二次会运行这段代码    &#x2F;&#x2F; 这段代码运行的地方就可以看做是一个简易的容器    &#x2F;&#x2F; 这里只是对进程进行了隔离    &#x2F;&#x2F; 但是可以看到 pid 已经变成了 1，因为我们有 PID Namespace    if os.Args[0] &#x3D;&#x3D; &quot;&#x2F;proc&#x2F;self&#x2F;exe&quot; &#123;        fmt.Printf(&quot;current pid %d\n&quot;, syscall.Getpid())        cmd :&#x3D; exec.Command(&quot;sh&quot;, &quot;-c&quot;, &#96;stress --vm-bytes 200m --vm-keep -m 1&#96;)        cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;&#125;        cmd.Stdin &#x3D; os.Stdin        cmd.Stdout &#x3D; os.Stdout        cmd.Stderr &#x3D; os.Stderr        if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;            fmt.Println(err)            os.Exit(1)        &#125;    &#125;        &#x2F;&#x2F; 第一次运行这段    &#x2F;&#x2F; **command 设置为当前进程，也就是这个 go 程序本身，也就是说 cmd.Start() 会再次运行该程序    cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;)    &#x2F;&#x2F; 在 start 之前，修改 cmd 的各种配置，也就是第二次运行这个程序的时候的配置&#x2F;&#x2F; 创建 namespace    cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr &#123;        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,    &#125;    cmd.Stdin &#x3D; os.Stdin    cmd.Stdout &#x3D; os.Stdout    cmd.Stderr &#x3D; os.Stderr        &#x2F;&#x2F; 因为之后要打印 process 的 id，所以用 start    &#x2F;&#x2F; 如果这里用 run 的话，那么 else 里的代码永远不会执行，因为 stress 永远不会结束    if err :&#x3D; cmd.Start(); err !&#x3D; nil &#123;        fmt.Println(&quot;Error&quot;, err)        os.Exit(1)    &#125; else &#123;        &#x2F;&#x2F; 打印 new process id        fmt.Printf(&quot;%v\n&quot;, cmd.Process.Pid)                &#x2F;&#x2F; 接下来三段对 cgroup 操作        &#x2F;&#x2F; the hierarchy has been already created by linux on the memory subsystem        &#x2F;&#x2F; create a sub cgroup           os.Mkdir(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,        ), 0755)                &#x2F;&#x2F; place container process in this cgroup        ioutil.WriteFile(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;tasks&quot;,        ), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)                &#x2F;&#x2F; restrict the stress process on this cgroup        ioutil.WriteFile(path.Join(        cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;memory.limit_int_bytes&quot;,        ), []byte(&quot;100m&quot;), 0644)                &#x2F;&#x2F; cmd.Start() 不会等待进程结束，所以需要手动等待        &#x2F;&#x2F; 如果不加的话，由于主进程结束了，子进程也会被强行结束        cmd.Process.Wait()    &#125;&#125;</code></pre><h4 id="ufs-3">1.5 UFS</h4><h5 id="ufs-概念-3">1.5.1 UFS 概念</h5><p>UFS，Union File System，联合文件系统。docker 在下载一个 image文件时，会看到一次下载很多个文件，这就是UFS。它是一种分层、轻量、高性能的文件系统。UFS 类似git，每次修改文件时，都是一次提交，并有记录，修改都反映在一个新的文件上，而不是修改旧文件。</p><p>UFS 允许多个不同目录挂载到同一个虚拟文件系统下，这就是为什么 image之间可以共享文件，以及继承镜像的原因。</p><h5 id="aufs-3">1.5.2 AUFS</h5><p>AUFS，Advanced Union File System，是 UFS 的一个改动版本。</p><p>笔者本身使用的是 WSL 做日常开发，WSL 内核不支持AUFS，后面会提到更换内核。</p><h5 id="docker-和-aufs-3">1.5.3 docker 和 AUFS</h5><p>docker 在早期使用 AUFS，直到现在也可以选择作为一种存储驱动类型。</p><h5 id="image-layer-3">1.5.4 image layer</h5><p>image 由多层 read-only layer 构成。</p><p>当启动一个 container 时，就会在 image 上再加一层 init layer，initlayer 也是 read-only 的，用于储存容器的环境配置。此外，docker还会创建一个 read-write 的 layer，用于执行所有的写操作。</p><p>当停止容器时，这个 read-write layer 依然保留，只有删除 container时才会被删除。</p><p>那么，怎么删除旧文件呢？</p><p>docker 会在 read-write layer 生成一个<code>.wh.&lt;fileName&gt;</code> 文件来隐藏要删除的文件。</p><h5 id="实现一个-aufs-3">1.5.5 实现一个 AUFS</h5><p>我们先创建一个如下的文件夹结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt</code></pre><p>然后挂载到 mnt 文件夹上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t aufs -o dirs&#x3D;.&#x2F;container-layer:.&#x2F;image-layer none .&#x2F;mnt</code></pre><p>如果没有手动添加权限的话，默认 dirs 左边第一个文件夹有 write-read权限，其他都是 read-only。</p><p>我们可以发现，imageLayer1 和 writeLayer 的文件出现在 mnt文件夹下：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt    ├── container.txt    └── image.txt</code></pre><p>然后我们修改一下 image.txt的内容，然后再看看整个目录，会发现，<code>container-layer</code>目录下多了一个 <code>image.txt</code>，然后我们看看<code>container-layer</code> 的 <code>image.txt</code>的内容，有添加前后的的文字。</p><p>也就是说，实际上，当修改某一个 layer 的时候，实际上不会改变这个layer，而是将其复制到 container-layer 中，然后再修改这个新的文件。</p><h2 id="二容器篇-3">二、容器篇</h2><h3 id="linux-的-proc-文件夹-3">2. Linux 的 /proc 文件夹</h3><h4 id="pid-3">2.1 PID</h4><p>在 <code>/proc</code>文件夹下可以看到很多文件夹的名字都是个数字，其实就是个 PID。是 Linux为每个进程创建的空间。</p><h4 id="一些重要的目录-3">2.2 一些重要的目录</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F;proc&#x2F;N # PID 为 N 的进程&#x2F;proc&#x2F;N&#x2F;cmdline # 进程的启动命令&#x2F;proc&#x2F;N&#x2F;cwd # 链接到进程的工作目录&#x2F;proc&#x2F;N&#x2F;environ  # 进程的环境变量列表&#x2F;proc&#x2F;N&#x2F;exe # 链接到进程的执行命令&#x2F;proc&#x2F;N&#x2F;fd # 包含进程相关的所有文件描述符&#x2F;proc&#x2F;N&#x2F;maps # 与进程相关的内存映射信息&#x2F;proc&#x2F;N&#x2F;mem # 进程持有的内存，不可读&#x2F;proc&#x2F;N&#x2F;root # 链接到进程的根目录&#x2F;proc&#x2F;N&#x2F;stat # 进程的状态&#x2F;proc&#x2F;N&#x2F;statm # 进程的内存状态&#x2F;proc&#x2F;N&#x2F;status # 比上面两个更可读&#x2F;proc&#x2F;self # 链接到当前正在运行的进程</code></pre><h3 id="简单实现-3">3. 简单实现</h3><h4 id="工具-3">3.1 工具</h4><p>获取帮助编写 command line app 的工具：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go get github.com&#x2F;urfave&#x2F;cli </code></pre><h4 id="实现代码-3">3.2 实现代码</h4><p>代码结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── command.go├── container│   └── init.go├── dockerCommand│   └── run.go├── go.mod├── go.sum└── main.go</code></pre><h5 id="runcommand-3">3.2.1 runCommand</h5><p><code>command.go</code> 用于放置各种 command 命令，这里先只写一个runCommand 命令。</p><p>首先用 urfave/cli 创建一个 runCommand 命令：</p><pre class="line-numbers language-golang" data-language="golang"><code class="language-golang">&#x2F;&#x2F; command.govar runCommand &#x3D; cli.Command&#123;    Name:  &quot;run&quot;,    Usage: &quot;Create a container&quot;,    Flags: []cli.Flag&#123;        &#x2F;&#x2F; integrate -i and -t for convenience        &amp;cli.BoolFlag&#123;            Name:  &quot;it&quot;,            Usage: &quot;open an interactive tty(pseudo terminal)&quot;,        &#125;,    &#125;,    Action: func(context *cli.Context) error &#123;        args :&#x3D; context.Args()        if len(args) &#x3D;&#x3D; 0 &#123;            return errors.New(&quot;Run what?&quot;)        &#125;        cmdArray :&#x3D; args.Get(0)        &#x2F;&#x2F; command        &#x2F;&#x2F; check whether type &#96;-it&#96;        tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal                &#x2F;&#x2F; 这个函数在下面定义        dockerCommand.Run(tty, cmdArray)        return nil    &#125;,&#125;</code></pre><h5 id="run-3">3.2.2 run</h5><p>上面的 Run 函数在 <code>dockerCommand/run.go</code> 下定义。当运行<code>docker run</code> 时，实际上主要是 Action 下的这个函数在工作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; dockerCommand&#x2F;run.go&#x2F;&#x2F; This is the function what &#96;docker run&#96; will callfunc Run(tty bool, cmdArray string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess :&#x3D; container.NewProcess(tty, cmdArray)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil&#123;logrus.Error(err)&#125;initProcess.Wait()os.Exit(-1)&#125;</code></pre><p>但其实这个函数做的也只是去跑一个 initProcess。这个 command process在另一个包里定义。</p><h5 id="newprocess-3">3.2.3 NewProcess</h5><p>上面提到的 <code>container.NewProcess</code> 在<code>container/init.go</code> 里定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; container&#x2F;init.gofunc NewProcess(tty bool, cmdArray string) *exec.Cmd &#123;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is the below exported function&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;args :&#x3D; []string&#123;&quot;init&quot;, cmdArray&#125;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, args...)&#x2F;&#x2F; new namespaces, thanks to Linuxcmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; this is what presudo terminal means&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;return cmd&#125;</code></pre><p>这个函数的作用是生成一个新的 command process，但这个 command 是<code>/proc/self/exe</code>这个程序本身，也就是，我们最后生成的可执行文件，但这次我们不运行<code>docker run</code>，而是 <code>docker init</code>，这个 init命令在下面定义。</p><h5 id="init-3">3.2.4 init</h5><p>initCommand 和 runCommand 在同一个文件里定义，也是一个command，但是注意这个 command 不面向用户，只用于协助 runCommand。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; command.go&#x2F;&#x2F; docker init, but cannot be used by uservar initCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;Start initiating...&quot;)cmdArray :&#x3D; context.Args().Get(0)logrus.Infof(&quot;container command: %v&quot;, cmdArray)return container.InitProcess(cmdArray, nil)&#125;,&#125;</code></pre><p>这里使用了 container.InitProcess函数，这个函数是真正用于容器初始化的函数。</p><h5 id="initprocess-3">3.2.5 InitProcess</h5><p>这里的是 InitProcess，也就是容器初始化的步骤。</p><p>注意 syscall.Exec 这里：</p><ul><li>就是 <code>mount /</code> 并指定 private，不然容器里的 proc会使用外面的 proc，即使在不同 namespace 下。</li><li>所以如果没有加这一段，其实退出容器后还需要在外面再次 mount proc才能使用 ps 等命令</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initiate the containerfunc InitProcess(cmdArray string, args []string) error &#123;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV                &#x2F;&#x2F; mountif err :&#x3D; syscall.Mount(&quot;&quot;, &quot;&#x2F;&quot;, &quot;&quot;, syscall.MS_PRIVATE|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F; fails: %v&quot;, err)return err&#125;        &#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)argv :&#x3D; []string&#123;cmdArray&#125;if err :&#x3D; syscall.Exec(cmdArray, argv, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc fails: %v&quot;, err)&#125;return nil&#125;</code></pre><p>一般来说，我们都是想要这个 cmdArray 作为 PID=1 的进程。but，我们有initProcess 本身的存在，所以 PID = 1 的其实是 initProcess，那如何让cmdArray 作为 PID=1 的存在呢？</p><p>这里有一个 syscall.Exec 神器，Exec 内部会调用 kernel 的 execve函数，这个函数会把当前进程上运行的程序替换为另一个程序，这正是我们想要的，在不改变PID 的情况下，替换程序 (即使 kill PID 为 1 的进程，新创建的进程也会是PID=2)。</p><p>为什么要第一个命令的 PID 为 1？</p><ul><li>因为这样，退出这个进程后，容器就会因为没有前台进程，而自动退出，这也是docker 的特性。</li></ul><h3 id="给-docker-run-增加对容器的资源限制功能-3">4. 给 docker run增加对容器的资源限制功能</h3><p>这里要用到 subsystem 的知识。</p><h4 id="subsystem.go-3">4.1 subsystem.go</h4><ul><li>根据 subsystem 的特性，和接口很搭。</li><li>此外再定义一个 ResourceConfig 的类型，用于放置资源控制的配置。</li><li>subsystemInstance 里包括 3 个 subsystem，分别对memory，cpu，cpushare进行限制。因为我们只需要对整个容器进行限制，所以这一套 3 个够了。</li></ul><p>看到这里，有个 cpu，cpushare，cpuset 等等，有点晕，查了下，有关 CPU的 cgroup subsystem，这里列举常见的 3 个：</p><ul><li>cpu：经常看到的 cpushares 在其麾下，share 即相对权重的 cpu调度，用来限制 cgroup 的 cpu 的使用率</li><li>cpuacct：统计 cgroup 的 cpu 使用率</li><li>cpuset：在多核机器上设置 cgroups 可使用的 cpu 核心数和内存</li></ul><p>通常前两者可以合体</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package subsystemstype ResourceConfig struct &#123;MemoryLimit stringCPUShare stringCPUSet string&#125;type Subsystem interface &#123;&#x2F;&#x2F; return the name of which type of subsystemName() string&#x2F;&#x2F; set a resource limit on a cgroupSet(cgroupPath string, res *ResourceConfig) error&#x2F;&#x2F; add a processs with the pid to a groupAddProcess(cgroupPath string, pid int) error&#x2F;&#x2F; remove a cgroupRemoveCgroup(cgroupPath string) error&#125;&#x2F;&#x2F; instance of a subsystemsvar SubsystemsInstance &#x3D; []Subsystem&#123;&amp;CPU&#123;&#125;,&amp;CPUSet&#123;&#125;,&amp;Memory&#123;&#125;,&#125;</code></pre><h4 id="memorysubsystem-3">4.2 MemorySubsystem</h4><h5 id="name-3">4.2.1 Name()</h5><p>很简单，返回 “memory” 字符串，表示这个 subsystem 是memorySubsystem。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *MemorySubsystem) Name() string &#123;    return &quot;memory&quot;&#125;</code></pre><h5 id="set-3">4.2.2 Set()</h5><p>Set() 用于对 cgroup 设置资源限制，因此参数为 cgroup 的 path 和resourceConfig。</p><ol type="1"><li>其中 <code>GetCgroupPath</code> 后面会提及，作用是获取这个 subsystem所挂载的 hierarchy 上的虚拟文件系统下的 从group 路径。</li><li>获取到 cgroupPath 在虚拟文件系统中的位置后，只需要写入"memory.limit_in_bytes" 文件中即可。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; set the memory limit to this cgroup with cgroupPathfunc (ms *Memory) Set(cgroupPath string, res *ResourceConfig) error  &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;memory.limit_in_bytes&quot;), []byte(res.MemoryLimit), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup memory fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="addprocess-3">4.2.3 AddProcess()</h5><ol type="1"><li>和上面基本一样，只不过是写到 tasks 里。</li><li>pid 变成 byte slice 之前要用 Itoa 转化一下。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add process fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="removecgroup-3">4.2.4 RemoveCgroup()</h5><ol type="1"><li>使用 <code>os.Remove</code> 可以移除参数所指定的文件或文件夹。</li><li>这里移除整个 cgroup 文件夹，就等于是删除 cgroup 了。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusubsystem-3">4.3 CPUSubsystem</h4><p>这里的设计和上面没什么区别，直接贴参考代码</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpu.gofunc (c *CPU) Name() string &#123;return &quot;CPUShare&quot;&#125;func (c *CPU) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpu.shares&quot;), []byte(res.CPUShare), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cpu share limit failed: %s&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpu process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusetsubsystem-3">4.4 CPUSetSubsystem</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpuset.gofunc (c *CPUSet) Name() string &#123;return &quot;CPUSet&quot;&#125;func (c *CPUSet) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpuset.cpus&quot;), []byte(res.CPUSet), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup cpuset failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpuset process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(path.Join(subsystemCgroupPath))&#125;&#125;</code></pre><h4 id="getcgrouppath-3">4.5 GetCgroupPath()</h4><p><code>GetCgroupPath()</code> 用于获取某个 subsystem 所挂载的hierarchy 上的虚拟文件系统 (挂载后的文件夹) 下的 cgroup的路径。通过对这个目录的改写来改动 cgroup。</p><p>首先我们抛开 cgroup，在此之前我们要知道 这个 hierarchy 的 cgroup根节点的路径。那可以在 <code>/proc/self/mountinfo</code> 中获取。</p><p>下面是一些实现细节：</p><ol type="1"><li>首先定义一个 <code>FindCgroupMountpoint()</code> 来找到 cgroup的根节点。</li><li>然后在 <code>GetCgroupPath</code> 将其和 cgroup的相对路径拼接从而获取 cgroup 的路径。如果 <code>autoCreate</code> 为true 且该路径不存在，那么就新建一个 cgroup。(在 hierarchy 环境下，mkdir其实会隐式地创建一个 cgroup，其中包括很多配置文件)</li></ol><blockquote><p><a href="#1.3.4 cgroup 的 kernel 接口">点击这里回顾</a></p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; as the function name shows, find the root path of hierarchyfunc FindCgroupMountpoint(subsystemName string) string  &#123;f, err :&#x3D; os.Open(&quot;&#x2F;proc&#x2F;self&#x2F;mountinfo&quot;)    &#x2F;&#x2F; get info about mount relate to current processif err !&#x3D; nil &#123;return &quot;&quot;&#125;defer f.Close()scanner :&#x3D; bufio.NewScanner(f)for scanner.Scan() &#123;txt :&#x3D; scanner.Text()fields :&#x3D; strings.Split(txt, &quot; &quot;)&#x2F;&#x2F; find whether &quot;subsystemName&quot; appear in the last field&#x2F;&#x2F; if so, then the fifth field is the pathfor _, opt :&#x3D; range strings.Split(fields[len(fields)-1], &quot;,&quot;) &#123;if opt &#x3D;&#x3D; subsystemName &#123;return fields[4]&#125;&#125;&#125;return &quot;&quot;&#125;&#x2F;&#x2F; get the absolute path of a cgroupfunc GetCgroupPath(subsystemName string, cgroupPath string, autoCreate bool) (string, error)  &#123;cgroupRootPath :&#x3D; FindCgroupMountpoint(subsystemName)expectedPath :&#x3D; path.Join(cgroupRootPath, cgroupPath)&#x2F;&#x2F; find the cgroup or create a new cgroupif _, err :&#x3D; os.Stat(expectedPath); err &#x3D;&#x3D; nil  || (autoCreate &amp;&amp; os.IsNotExist(err)) &#123;if os.IsNotExist(err) &#123;if err :&#x3D; os.Mkdir(expectedPath, 0755); err !&#x3D; nil &#123;return &quot;&quot;, fmt.Errorf(&quot;error when create cgroup: %v&quot;, err)&#125;&#125;return expectedPath, nil&#125; else &#123;return &quot;&quot;, fmt.Errorf(&quot;cgroup path error: %v&quot;, err)&#125;&#125;</code></pre><h4 id="cgroupsmanager.go-3">4.6 cgroupsManager.go</h4><ol type="1"><li>定义 CgroupManager 类型，其中的 path 要注意是相对路径，相对于hierarchy 的 root path。所以一个 CgroupManager 是有可能表示多个 cgroups的，或准确说，和对应的 hierarchy root path 的相对路径一样的多个cgroups。</li><li>因为上述原因，<code>Set()</code> 可能会创建多个 cgroups，如果subsystems 们在不同的 hierarchy 就会这样。</li><li>这也是为什么 <code>AddProcess()</code> 和 <code>Remove()</code>要在每个 subsystem 上执行一遍。因为这些 subsystem 可能存在于不同的hierarchies。</li><li>注意 <code>Set()</code> 和 <code>AddProcess()</code>都不是返回错误，而是发出警告，然后返回nil。因为有些时候用户只指定某一个限制，例如 memory，那样的话修改 cpu等其实会报错 (正常的报错)，因此我们不 return err 来退出。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">package cgroupsimport &quot;simple-docker&#x2F;subsystem&quot;type CgroupManager struct &#123;Path     string &#x2F;&#x2F; relative path, relative to the root path of the hierarchy&#x2F;&#x2F; so this may cause more than one cgroup in different hierarchiesResource *subsystems.ResourceConfig&#125;func NewCgroupManager(path string) *CgroupManager &#123;return &amp;CgroupManager&#123;Path: path,&#125;&#125;&#x2F;&#x2F; set the three resource config subsystems to the cgroup(will create if the cgroup path is not existed)&#x2F;&#x2F; this may generate more than one cgroup, because those subsystem may appear in different hierarchiesfunc (cm CgroupManager) Set(res *subsystems.ResourceConfig) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.Set(cm.Path, res); err !&#x3D; nil &#123;logrus.Warnf(&quot;set resource fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; add process to the cgroup path&#x2F;&#x2F; why should we iterate all the subsystems? we have only one cgroup&#x2F;&#x2F; because those subsystems may appear at different hierarchies, which will then cause more than one cgroup, 1-3 in this case.func (cm *CgroupManager) AddProcess(pid int) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.AddProcess(cm.Path, pid); err !&#x3D; nil &#123;logrus.Warn(&quot;app process fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; delete the cgroup(s)func (cm *CgroupManager) Remove() error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err:&#x3D; subsystem.RemoveCgroup(cm.Path); err !&#x3D; nil &#123;return err&#125;&#125;return nil&#125;</code></pre><h4 id="管道处理多个容器参数-3">4.7 管道处理多个容器参数</h4><p>限制容器运行的命令不再像是 <code>/bin/sh</code>这种单个参数，而是多个参数，因此需要使用管道来对多个参数进行处理。那么需要修改以下文件：</p><h5 id="containerinit.go-3">4.7.1 container/init.go</h5><ol type="1"><li>管道原理和 channel 很像，read 端和 write端会在另一边没响应时堵塞。</li><li>使用 <code>os.Pipe()</code> 获取管道。返回的 readPipe 和 writePipe都是 <code>*os.File</code> 类型。</li><li>如何把管道传给子进程 (也就是容器进程) 变成了一个难题，这里用到了<code>ExtraFile</code> 这个参数来解决。cmd会带着参数里的文件来创建新的进程。(这里除了 ExtraFile，还会有类似StandardFile，也就是 stdin，stdout，stderr)</li><li>这里把 read 端传给容器进程，然后 write 端保留在父进程上。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;new pipe error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itselfcmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)&#x2F;&#x2F; new namespacescmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;return cmd, writePipe&#125;</code></pre><p>除了 <code>NewProcess()</code>，<code>InitProcess()</code>也要改变下。</p><ol type="1"><li>使用 readCommand 来读取 pipe。</li><li>实际运行中，当进程运行到 <code>readCommand()</code> 时会堵塞，直到write 端传数据进来。</li><li>因此不用担心我们在容器运行后再传输参数。因为再读取完参数之前，<code>InitProcess()</code>也不会运行到 <code>syscall.Exec()</code> 这一步。</li><li>这里添加了 lookPath，这个是用于解决每次我们都要输入<code>/bin/ls</code>的麻烦，这个函数会帮我们找到参数命令的绝对路径。也就是说，只要输入 ls即可，lookPath 会自动找到 <code>/bin/ls</code>。然后我们再把这个 path作为 <code>argv()</code> 传给 <code>syscall.Exec</code></li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initialize the containerfunc InitProcess() error &#123;cmdArray :&#x3D; readCommand()if len(cmdArray) &#x3D;&#x3D; 0 &#123;return fmt.Errorf(&quot;init process fails, cmdArray is nil&quot;)&#125;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV&#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)path, err :&#x3D; exec.LookPath(cmdArray[0])if err !&#x3D; nil &#123;logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)return err&#125;&#x2F;&#x2F; log path infologrus.Infof(&quot;find path: %v&quot;, path)if err :&#x3D; syscall.Exec(path, cmdArray, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(err.Error())&#125;return nil&#125;func readCommand() []string &#123;pipe :&#x3D; os.NewFile(uintptr(3), &quot;pipe&quot;)msg, err :&#x3D; ioutil.ReadAll(pipe)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read pipe failed: %v&quot;, err)return nil&#125;return strings.Split(string(msg), &quot; &quot;)&#125;</code></pre><h5 id="dockercommandrun.go-3">4.7.2 dockerCommand/run.go</h5><ol type="1"><li>在 run.go 向 writePipe 写入参数，这样容器就会获取到参数。</li><li>关闭 pipe，使得 init 进程继续进行。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig) &#123;initProcess, writePipe :&#x3D; container.NewProcess(tty)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write sidesendInitCommand(cmdArray, writePipe)initProcess.Wait()os.Exit(-1)&#125;func sendInitCommand(cmdArray []string, writePipe *os.File) &#123;cmdString :&#x3D; strings.Join(cmdArray, &quot; &quot;)logrus.Infof(&quot;whole init command is: %v&quot;, cmdString)writePipe.WriteString(cmdString)writePipe.Close()&#125;</code></pre><h5 id="command.go-3">4.7.3 command.go</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open a interactive tty(pre sudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpushare&quot;,Usage:&quot;limit the cpu share&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &#x3D;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;cmdArray :&#x3D; make([]string,len(args)) &#x2F;&#x2F; commandcopy(cmdArray,args)&#x2F;&#x2F; checkout whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; pre sudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig &#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare: context.String(&quot;cpushare&quot;),CPUSet: context.String(&quot;cpu&quot;),&#125;dockerCommand.Run(tty, cmdArray, &amp;resourceConfig)return nil&#125;,&#125;&#x2F;&#x2F; docker init, but cannot be used by uservar InitCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;start initializing...&quot;)return container.InitProcess()&#125;,&#125;</code></pre><h5 id="main.go-3">4.7.4 main.go</h5><p>除了上面的修改，我们还要定义一个程序的入口：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;&quot;github.com&#x2F;urfave&#x2F;cli&quot;)const usage &#x3D; &#96;Usage&#96;func main() &#123;app :&#x3D; cli.NewApp()app.Name &#x3D; &quot;simple-docker&quot;app.Usage &#x3D; usageapp.Commands &#x3D; []cli.Command&#123;RunCommand,InitCommand,&#125;app.Before &#x3D; func(context *cli.Context) error &#123;logrus.SetFormatter(&amp;logrus.JSONFormatter&#123;&#125;)logrus.SetOutput(os.Stdout)return nil&#125;if err :&#x3D; app.Run(os.Args); err !&#x3D; nil &#123;logrus.Fatal(err)&#125;&#125;</code></pre><h4 id="运行-demo-3">4.8 运行 demo</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it stress -m 100m --vm-bytes 200m --vm-keep -m 1</code></pre><p>效果如下：</p><p><img src="0x0035/demo_1.png" /></p><p>不过这个运行方式不能进行交互，我们可以使用这个命令来验证我们写的docker 是否与宿主机隔离：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it &#x2F;bin&#x2F;sh</code></pre><p><img src="0x0035/demo_sh.png" /></p><p>可以看到，pid，ipc，network 方面都与宿主机进行了隔离。</p><h2 id="三镜像篇-3">三、镜像篇</h2><h3 id="构造镜像-3">5. 构造镜像</h3><h4 id="编译-aufs-内核-3">5.1 编译 aufs 内核</h4><p>因为电脑硬盘空间不太够，就不使用虚拟机来做实验了，笔者这里使用 WSL2来完成后续工作，然而，WSL2 Kernel 没有把 aufs编译进去，那只能换内核了，查阅资料，有两种更换内核的方法：</p><ul><li><p>直接替换 <code>C:\System32\lxss\tools\kernel</code> 文件</p></li><li><p>在 users 目录下新建 <code>.wslconfig</code> 文件：</p><pre class="line-numbers language-none"><code class="language-none">[wsl2]kernel&#x3D;&quot;要替换kernel的路径&quot;</code></pre></li></ul><p>很明显，我是不会满足于使用别人编译好的内核的，那我也来动手做一个。</p><h5 id="准备代码库-3">5.1.1 准备代码库</h5><p>我们先在 WSL 上准备好相关软件包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">apt update #更新源apt install build-essential flex bison libssl-dev libelf-dev gcc make</code></pre><p>编译内核需要从 GitHub 上 clone 微软官方的 WSL 代码和 AUFS-Standalone的代码库</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">git clone https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;WSL2-Linux-Kernel kernelgit clone https:&#x2F;&#x2F;github.com&#x2F;sfjro&#x2F;aufs-standalone aufs5</code></pre><p>然后查看 WSL 内核版本：在 wsl 下运行命令 <code>uname -r</code></p><p>例如我的内核版本是 5.15.19，那 kernel 和 aufs 都要切换到相应的分支去(kernel 默认就是 5.15.19，故不用切换)</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd aufs5git checkout aufs5.15.36</code></pre><p>然后退回到 kernel 文件夹给代码打补丁：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cat ..&#x2F;aufs5&#x2F;aufs5-mmap.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-base.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-kbuild.patch | patch -p1</code></pre><p>三个 Patch 的顺序无关。</p><p>然后再复制一点配置文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cp ..&#x2F;aufs5&#x2F;Documentation . -rcp ..&#x2F;aufs5&#x2F;fs&#x2F; . -rcp ..&#x2F;aufs5&#x2F;include&#x2F;uapi&#x2F;linux&#x2F;aufs_type.h .&#x2F;include&#x2F;uapi&#x2F;linux</code></pre><p>接下来我们来修改一下编译配置，在 <code>Microsoft/config-wsl</code>中任意位置增加一行：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini">CONFIG_AUFS_FS&#x3D;y</code></pre><p>最后，就可以开始编译了！</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">make KCONFIG_CONFIG&#x3D;Microsoft&#x2F;config-wsl -j8</code></pre><p>过程中会问你一些问题，我除了 AUFS Debug 都选了 y。</p><p>最后会在当前目录生成 <code>vmlinuz</code>，在<code>arch/x86/boot</code> 下生成 <code>bzImage</code>。</p><p>关闭 WSL 后更换内核，重启 WSL 输入<code>grep aufs /proc/filesystems</code>验证结果，如果出现 aufs的字样，说明操作成功。</p><h4 id="使用-busybox-创建容器-3">5.2 使用 busybox 创建容器</h4><h5 id="busybox-3">5.2.1 busybox</h5><p>先在 docker 获取 busybox 镜像并打包成一个 tar 包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">docker pull busyboxdocker run -d busybox top -bdocker export -o busybox.tar &lt;container_id&gt;</code></pre><p>将其复制到 WSL 下并解压。</p><h5 id="pivot_root-3">5.2.2 pivot_root</h5><p>pivot_root 是一个系统调用，作用是改变当前 root 文件系统。pivot_root可以将当前进程的 root 文件系统移动到 put_old 文件夹，然后使 new_root成为新的 root 文件系统。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func pivotRoot(root string) error &#123;&#x2F;&#x2F; remount the root dir, in order to make current root and old root in different file systemsif err :&#x3D; syscall.Mount(root, root, &quot;bind&quot;, syscall.MS_BIND|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;mount rootfs to itself error: %v&quot;, err)&#125;&#x2F;&#x2F; create &#39;rootfs&#x2F;.pivot_root&#39; to store old_rootpivotDir :&#x3D; filepath.Join(root, &quot;.pivot_root&quot;)if err :&#x3D; os.Mkdir(pivotDir, 0777); err !&#x3D; nil &#123;return err&#125;&#x2F;&#x2F; pivot_root mount on new rootfs, old_root mount on rootfs&#x2F;.pivot_rootif err :&#x3D; syscall.PivotRoot(root, pivotDir); err !&#x3D; nil &#123;return fmt.Errorf(&quot;pivot_root %v&quot;, err)&#125;&#x2F;&#x2F; change current work dir to root dirif err :&#x3D; syscall.Chdir(&quot;&#x2F;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;chdir &#x2F; %v&quot;, err)&#125;pivotDir &#x3D; filepath.Join(&quot;&#x2F;&quot;, &quot;.pivot_root&quot;)&#x2F;&#x2F; umount rootfs&#x2F;.rootfs_rootif err :&#x3D; syscall.Unmount(pivotDir, syscall.MNT_DETACH); err !&#x3D; nil &#123;return fmt.Errorf(&quot;umount pivot_root dir %v&quot;, err)&#125;&#x2F;&#x2F; del the temporary dirreturn os.Remove(pivotDir)&#125;</code></pre><p>有了这个函数就可以在 init 容器进程时，进行一系列的 mount 操作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setUpMount() error &#123;&#x2F;&#x2F; get current pathpwd, err :&#x3D; os.Getwd()if err !&#x3D; nil &#123;logrus.Errorf(&quot;get current location error: %v&quot;, err)return err&#125;logrus.Infof(&quot;current location: %v&quot;, pwd)pivotRoot(pwd)&#x2F;&#x2F; mount procdefaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEVif err :&#x3D; syscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc failed: %v&quot;, err)return err&#125;if err :&#x3D; syscall.Mount(&quot;tmpfs&quot;, &quot;&#x2F;dev&quot;, &quot;tmpfs&quot;, syscall.MS_NOSUID|syscall.MS_STRICTATIME, &quot;mode&#x3D;755&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;dev failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>tmpfs 是一种基于内存的文件系统，用 RAM 或 swap 分区来存储。</p><p>在 <code>NewParentProcess()</code> 中加一句<code>cmd.Dir="/root/busybox"</code>。</p><p>写完上述函数，然后在 <code>initProcess()</code> 中调用一下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">if err :&#x3D; setUpMount(); err !&#x3D; nil &#123;    logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)&#125;</code></pre><p>然后来运行测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it sh###### dividing live&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;busybox&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#x2F; #</code></pre><p>可以看到，容器当前目录被虚拟定位到了根目录，其实是在宿主机上映射的<code>/root/busybox</code>。</p><h5 id="用-aufs-包装-busybox-3">5.2.3 用 AUFS 包装 busybox</h5><p>前面提到了，docker 使用 AUFS 存储镜像和容器。docker在使用镜像启动一个容器时，会新建 2 个 layer：write layer 和container-init-layer。write layer是容器唯一的可读写层，container-init-layer是为容器新建的只读层，用来存储容器启动时传入的系统信息。</p><ul><li><code>CreateReadOnlyLayer()</code> 新建 <code>busybox</code>文件夹，解压 <code>busybox.tar</code> 到 <code>busybox</code>目录下，作为容器只读层。</li><li><code>CreateWriteLayer()</code> 新建一个 <code>writeLayer</code>文件夹，作为容器唯一可写层。</li><li><code>CreateMountPoint()</code> 先创建了 <code>mnt</code>文件夹作为挂载点，再把 <code>writeLayer</code> 目录和<code>busybox</code> 目录 mount 到 <code>mnt</code> 目录下。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; extra tar to &#39;busybox&#39;, used as the read only layer for containerfunc CreateReadOnlyLayer(rootURL string) &#123;busyboxURL :&#x3D; rootURL + &quot;busybox&#x2F;&quot;busyboxTarURL :&#x3D; rootURL + &quot;busybox.tar&quot;exist, err :&#x3D; PathExists(busyboxURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, busyboxURL, err)&#125;if !exist &#123;if err :&#x3D; os.Mkdir(busyboxURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, busyboxURL, err)&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, busyboxTarURL, &quot;-C&quot;, busyboxURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, busyboxTarURL, err)&#125;&#125;&#125;&#x2F;&#x2F; create a unique folder as writeLayerfunc CreateWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.Mkdir(writeURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, writeURL, err)&#125;&#125;func CreateMountPoint(rootURL string, mntURL string) &#123;&#x2F;&#x2F; create mnt folder as mount pointif err :&#x3D; os.Mkdir(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;dirs :&#x3D; &quot;dirs&#x3D;&quot; + rootURL + &quot;writeLayer:&quot; + rootURL + &quot;busybox&quot;cmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;&#125;func NewWorkSpace(rootURL, mntURL string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)&#125;</code></pre><p>接下来在 <code>NewParentProcess()</code> 将容器使用的宿主机目录<code>/root/busybox</code> 替换为 <code>/root/mnt</code>，这样使用 AUFS系统启动容器的代码就完成了。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;NewWorkSpace(rootURL, mntURL)cmd.Dir &#x3D; mntURLreturn cmd, writePipe</code></pre><p>docker 会在删除容器时，把容器对应的 write layer 和container-init-layer 删除，而保留镜像中所有的内容。</p><ul><li><code>DeleteMountPoint()</code> 中 umount <code>mnt</code>目录。</li><li>删除 <code>mnt</code> 目录。</li><li>在 <code>DeleteWriteLayer()</code> 删除 <code>writeLayer</code>文件夹。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(rootURL string, mntURL string) &#123;cmd :&#x3D; exec.Command(rootURL, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)&#125;&#125;func DeleteWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;func DeleteWorkSpace(rootURL, mntURL string) &#123;DeleteMountPoint(rootURL, mntURL)DeleteWriteLayer(rootURL)&#125;</code></pre><p>现在来启动一个容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it shdirs&#x3D;&#x2F;root&#x2F;writeLayer:&#x2F;root&#x2F;busybox&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#x2F; #</code></pre><p>测试在容器内创建文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # mkdir aaa&#x2F; # touch aaa&#x2F;test.txt</code></pre><p>此时我们可以在宿主机终端查看<code>/root/mnt/writeLayer</code>，可以看到刚才新建的 <code>aaa</code>文件夹和 <code>test.txt</code>，在我们退出容器后，<code>/root/mnt</code>文件夹被删除，伴随着刚才创建的文件夹和文件都被删除，而作为镜像的 busybox仍被保留，且内容未被修改。</p><h4 id="实现-volume-数据卷-3">5.3 实现 volume 数据卷</h4><p>上节实现了容器和镜像的分离，但是如果容器退出，容器可写层的所有内容就会被删除，这里使用volume 来实现容器数据持久化。</p><p>先在 <code>command.go</code> 里添加 <code>-v</code> 标签：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;,         &#x2F;&#x2F; add &#96;-v&#96; tag         &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;         &#x2F;&#x2F; send volume args to Run()volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig,volume)return nil&#125;,&#125;</code></pre><p>在 <code>Run()</code> 中，把 volume 传给创建容器的<code>NewParentProcess()</code> 和删除容器文件系统的<code>DeleteWorkSpace()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)initProcess.Wait()rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>在 <code>NewWorkSpace()</code> 中，继续把 volume传给创建容器文件系统的 <code>NewWorkSapce()</code>。</p><p>创建容器文件系统过程如下：</p><ul><li>创建只读层。</li><li>创建容器读写层。</li><li>创建挂载点并把只读层和读写层挂载到挂载点上。</li><li>判断 volume是否为空，如果是，说明用户没有使用挂载标签，结束创建过程。</li><li>不为空，就用 <code>volumeURLExtract()</code> 解析。</li><li>当 <code>volumeURLExtract()</code> 返回字符数组长度为2，且数据元素均不为空时，则执行 <code>MountVolume()</code>来挂载数据卷。<ul><li>否则提示用户创建数据卷输入值不对。</li></ul></li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(rootURL, mntURL, volume string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(rootURL, mntURL, volumeURLs)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;func volumeUrlExtract(volume string) []string &#123;&#x2F;&#x2F; divide volume by &quot;:&quot;return strings.Split(volume, &quot;:&quot;)&#125;</code></pre><p>挂载数据卷过程如下：</p><ul><li>读取宿主机文件目录 URL，创建宿主机文件目录(<code>/root/$&#123;parentURL&#125;</code>)</li><li>读取容器挂载点 URL，在容器文件系统里创建挂载点(<code>/root/mnt/$&#123;containerURL&#125;</code>)</li><li>把宿主机文件目录挂载到容器挂载点，这样启动容器的过程，对数据卷的处理就完成了。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]containerVolumeURL :&#x3D; mntURL + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURLcmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)&#125;&#125;</code></pre><p>删除容器文件系统过程如下：</p><ul><li>在 volume 不为空，且使用 <code>volumeURLExtract()</code> 解析 volume字符串返回的字符数组长度为 2，数据元素均不为空时，才执行<code>DeleteMountPointWithVolume()</code> 来处理。</li><li>其余情况仍使用前面的 <code>DeleteMountPoint()</code>。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(rootURL, mntURL, volume string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;DeleteWriteLayer(rootURL)&#125;</code></pre><p><code>DeleteMountPointWithVolume()</code> 处理逻辑如下：</p><ul><li>卸载 volume 挂载点的文件系统(<code>/root/mnt/$&#123;containerURL&#125;</code>)，保证整个容器挂载点没有再被使用。</li><li>卸载整个容器文件系统挂载点 (<code>/root/mnt</code>)。</li><li>删除容器文件系统挂载点。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; umount volume point in containercontainerURL :&#x3D; mntURL + volumeURLs[1]cmd :&#x3D; exec.Command(&quot;umount&quot;, containerURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)&#125;&#x2F;&#x2F; umount the whole point of the containercmd &#x3D; exec.Command(&quot;umount&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>接下来启动容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; #</code></pre><p>进入 <code>containerVolume</code>，创建一个文本文件，并随便写点东西：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;test&quot; &gt;&gt; test.txt</code></pre><p>此时我们能在宿主机的 <code>/root/volume</code>找到我们刚才创建的文本文件。退出容器后，volume文件夹也没有被删除。再次进入容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">r# go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;mkdir parent dir &#x2F;root&#x2F;volume error. mkdir &#x2F;root&#x2F;volume: file exists&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; # ls containerVolume&#x2F;test.txt</code></pre><p>此时这里会提示 volume 文件夹存在，我们在 <code>test.txt</code>内追加内容：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;###&quot; &gt;&gt; test.txt</code></pre><p>此时再次退出容器，能看到修改过后的文件内容，可以看到 volume文件夹没有被删除。</p><h4 id="简单镜像打包-3">5.4 简单镜像打包</h4><p>容器在退出时会删除所有可写层的内容，commit命令可以把运行状态容器的内容存储为镜像保存下来。</p><p>在 <code>main.go</code> 里添加 <code>commit</code> 命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    InitCommand,    RunCommand,    CommitCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里实现 <code>CommitCommand</code>命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;imageName :&#x3D; context.Args()[0]&#x2F;&#x2F; commitContainer(containerName)commitContainer(imageName)return nil&#125;,&#125;</code></pre><p>添加 <code>commit.go</code>，通过 <code>commitContainer()</code>实现将容器文件系统打包成 <code>$&#123;imagename&#125;.tar</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&#x2F;exec&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;)func commitContainer(imageName string) &#123;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&quot;imageTar :&#x3D; &quot;&#x2F;root&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>运行测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it sh</code></pre><p>然后在另一个终端运行：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit image</code></pre><p>这时候可以在 root 目录下看到多了一个 <code>image.tar</code>，解压后可以发现压缩包的内容和 <code>/root/mnt</code> 一致。</p><blockquote><p>tips：一定要先运行容器！如果不运行容器直接打包，会提示<code>/root/mnt</code> 不存在。</p></blockquote><h3 id="构建容器进阶-3">6. 构建容器进阶</h3><h4 id="实现容器后台运行-3">6.1 实现容器后台运行</h4><p>容器，放在操作系统层面，就是一个进程，当前运行命令的 simple-docker是主进程，容器是当前 simple-docker 进程 fork出来的子进程。子进程的结束和父进程的运行是一个异步的过程，即父进程不会知道子进程在什么时候结束。如果创建子进程时，父进程退出，那这个子进程就是孤儿进程(没人管)，此时进程号为 1 的进程 init 就会接受这些孤儿进程。</p><p>先在 <code>command.go</code> 添加 <code>-d</code>标签，表示这个容器启动时在后台运行：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach container         &#x2F;&#x2F; tty cannot work with detachif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume)return nil&#125;,&#125;</code></pre><p>然后也要修改一下 <code>run.go</code> 的 <code>Run()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)    &#x2F;&#x2F; if background process, parent process won&#39;t waitif tty &#123;initProcess.Wait()&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T15:32:44+08:00&quot;&#125;</code></pre><p>根据书上的提示，<code>ps -ef</code> 用来查找 top 进程：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ps -ef | grep toproot        3713     751  0 14:42 pts&#x2F;2    00:00:00 top</code></pre><p>前面几次运行命令，都找不到 top这个进程，于是我后面多跑了几次，终于看到了这个进程。。。</p><p>可以看到，top 命令的进程正在运行着，不过运行环境是 WSL，父进程 id不是 1，然后 <code>ps -ef</code> 查看一下，top 的父进程是一个 bash进程，而 bash 进程的父进程是一个 init 进程，这样应该算过了吧(偶尔的一两次不严谨)。</p><h4 id="实现查看运行中的容器-3">6.2 实现查看运行中的容器</h4><h5 id="name-标签-3">6.2.1 name 标签</h5><p>前面创建的容器里，所有关于容器的信息，例如PID、容器创建时间、容器运行命令等，都没有记录，这导致容器运行完后就在也不知道它的信息了，因此要把这部分信息保留。先在<code>command.go</code> 里加一个 name 标签，方便用户指定容器的名字：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag &#123;Name: &quot;name&quot;,Usage: &quot;container name&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume, containerName)return nil&#125;,&#125;</code></pre><p>添加一个方法来记录容器的相关信息，这里用先用一个 10位的数字来表示容器的 id：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func randStringBytes(n int) string &#123;letterBytes :&#x3D; &quot;1234567890&quot;rand.Seed(time.Now().UnixNano())b :&#x3D; make([]byte, n)for i :&#x3D; range b &#123;b[i] &#x3D; letterBytes[rand.Intn(len(letterBytes))]&#125;return string(b)&#125;</code></pre><p>这里用时间戳为种子，每次生成一个 10 以内的数字作为 letterBytes数组的下标，最后拼成整个容器的 id。容器的信息默认保存在<code>/var/run/simple-docker/$&#123;containerName&#125;/config.json</code>，容器基本格式如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;Id          string &#96;json:&quot;id&quot;&#96;Name        string &#96;json:&quot;name&quot;&#96;Command     string &#96;json:&quot;command&quot;&#96; &#x2F;&#x2F; the command that init process executeCreatedTime string &#96;json:&quot;created_time&quot;&#96;Status      string &#96;json:&quot;status&quot;&#96;&#125;var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&quot;ConfigName          string &#x3D; &quot;config.json&quot;)</code></pre><p>下面是记录容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;) &#x2F;&#x2F; format must like thiscommand :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>这里格式化的时间必须是<code>2006-01-02 15:04:05</code>，不然格式化后的时间会是几千年后doge。</p><p>详细可以看这篇文章：<ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p>在主函数加上调用：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>如果创建 tty 方式的容器，在容器退出后，就会删除相关信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func deleteContainerInfo(containerID string) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerID)if err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, dirURL, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top# go run . run -d --name jay top</code></pre><p>执行完成后，可以在 <code>/var/run/simple-docker/</code>找到两个文件夹，一个是随机 id，一个是 jay，文件夹下各有一个<code>config.json</code>，记录了容器的相关信息。</p><h5 id="实现-docker-ps-3">6.2.2 实现 docker ps</h5><p>在 <code>main.go</code> 加一个 <code>listCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,&#125;</code></pre><p>在 <code>command.go</code> 添加定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ListCommand &#x3D; cli.Command&#123;Name: &quot;ps&quot;,Usage: &quot;list all the containers&quot;,Action: func(context *cli.Context) error &#123;ListContainers()return nil&#125;,&#125;</code></pre><p>新建一个 <code>list.go</code>，实现记录列出容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListContainers() &#123;&#x2F;&#x2F; get the path that store the info of the containerdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, &quot;&quot;)dirURL &#x3D; dirURL[:len(dirURL)-1]&#x2F;&#x2F; read all the files in the directoryfiles, err :&#x3D; ioutil.ReadDir(dirURL)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read dir %s error %v&quot;, dirURL, err)return&#125;var containers []*container.ContainerInfofor _, file :&#x3D; range files &#123;tmpContainer, err :&#x3D; getContainerInfo(file)&#x2F;&#x2F; .Println(tmpContainer)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info error %v&quot;, err)continue&#125;containers &#x3D; append(containers, tmpContainer)&#125;&#x2F;&#x2F; use tabwriter.NewWriter to print the containerInfow :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprintf(w, &quot;ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n&quot;)for _, item :&#x3D; range containers &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\t%s\t%s\t%s\n&quot;,item.Id, item.Name, item.Pid, item.Status, item.Command, item.CreatedTime)&#125;&#x2F;&#x2F; refresh stdout if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;flush stdout error %v&quot;,err)return&#125;&#125;func getContainerInfo(file os.FileInfo) (*container.ContainerInfo, error) &#123;containerName :&#x3D; file.Name()&#x2F;&#x2F; create the absolute pathconfigFileDir :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFileDir &#x3D; configFileDir + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read config.jsoncontent, err :&#x3D; ioutil.ReadFile(configFileDir)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, configFileDir, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; turn json to containerInfoif err :&#x3D; json.Unmarshal(content, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>接上小节的测试，我们运行以下命令：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:11+08:00&quot;&#125;# go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:25+08:00&quot;&#125;# go run . psID           NAME         PID         STATUS      COMMAND     CREATED6675792962   6675792962   4317        running     top         2023-05-05 19:29:115553437308   jay          4404        running     top         2023-05-05 19:29:25</code></pre><p>现在就可以通过 ps 来看到所有创建的容器状态和它们的 init 进程 id了。</p><h4 id="查看容器日志-3">6.3 查看容器日志</h4><p>在 <code>main.go</code> 加一个 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,    LogCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里添加 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var LogCommand &#x3D; cli.Command&#123;Name:  &quot;logs&quot;,Usage: &quot;print logs of a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;contianerName :&#x3D; context.Args()[0]logContainer(contianerName)return nil&#125;,&#125;</code></pre><p>新建一个 <code>log.go</code>，定义 <code>logContainer()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func logContainer(containerName string) &#123;&#x2F;&#x2F; get the log pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)logFileLocation :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ContainerLogFile&#x2F;&#x2F; open log filefile, err :&#x3D; os.Open(logFileLocation)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container open file %s error: %v&quot;, logFileLocation, err)return&#125;defer file.Close()&#x2F;&#x2F; read log file contentcontent, err :&#x3D; ioutil.ReadAll(file)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container read file %s error: %v&quot;, logFileLocation, err)return&#125;&#x2F;&#x2F; use Fprint to transfer content to stdoutfmt.Fprint(os.Stdout, string(content))&#125;</code></pre><p>测试一下，先用 detach 方式创建一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-06T14:26:32+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED1837062451   jay         2065        running     top         2023-05-06 14:26:32# go run . logs jayMem: 3265116K used, 4568420K free, 3256K shrd, 71432K buff, 1135692K cachedCPU:  0.3% usr  0.2% sys  0.0% nic 99.3% idle  0.0% io  0.0% irq  0.0% sirqLoad average: 0.03 0.09 0.08 1&#x2F;521 5PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND</code></pre><p>可以看到，logs命令成功运行并输出容器的日志。(这里之前出现过前几次创建容器，而后台却没运行的情况，导致一开始运行logs 时报错了，建议在运行 logs 前多检查下 top 是否后台运行中)</p><h4 id="进入容器-namespace-3">6.4 进入容器 Namespace</h4><p>在 6.3小节里，实现了查看后台运行的容器的日志，但是容器一旦创建后，就无法再次进入容器，这一次来实现进入容器内部的功能，也就是exec。</p><h5 id="setns-3">6.4.1 setns</h5><p>setns 是一个系统调用，可根据提供的 PID 再次进入到指定的Namespace。它要先打开 <code>/proc/$&#123;pid&#125;/ns</code>文件夹下对应的文件，然后使当前进程进入到指定的 Namespace 中。对于 go来说，一个有多线程的进程使无法使用 setns 调用进入到对应的命名空间的，go没启动一个程序就会进入多线程状态，因此无法简单在 go里直接调用系统调用，这里还需要借助 C 来实现这个功能。</p><h5 id="cgo-3">6.4.2 Cgo</h5><p>在 go 里写 C：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package rand&#x2F;*#include &lt;stdlib.h&gt;*&#x2F;import &quot;C&quot;func Random() int &#123;    return int(C.random())&#125;func Seed(i int) &#123;    C.srandom(C.uint(i))&#125;</code></pre><h5 id="实现-3">6.4.3 实现</h5><p>先使用 C 根据 PID进入对应 Namespace：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenter&#x2F;*#define _GNU_SOURCE#include &lt;errno.h&gt;#include &lt;sched.h&gt;#include &lt;stdio.h&gt;#include &lt;stdlib.h&gt;#include &lt;string.h&gt;#include &lt;fcntl.h&gt;#include &lt;unistd.h&gt;&#x2F;&#x2F; if this package is quoted, this function will run automatic__attribute__((constructor)) void enter_namespace(void)&#123;    char *simple_docker_pid;    &#x2F;&#x2F; get pid from system environment    simple_docker_pid &#x3D; getenv(&quot;simple_docker_pid&quot;);    if (simple_docker_pid)    &#123;        fprintf(stdout, &quot;got simple docker pid&#x3D;%s\n&quot;, simple_docker_pid);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker pid env skip nsenter&quot;);        &#x2F;&#x2F; if no specified pid, the func will exit        return;    &#125;    char *simple_docker_cmd;    simple_docker_cmd &#x3D; getenv(&quot;simple_docker_cmd&quot;);    if (simple_docker_cmd)    &#123;        fprintf(stdout, &quot;got simple docker cmd&#x3D;%s\n&quot;, simple_docker_cmd);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker cmd env skip nsenter&quot;);        &#x2F;&#x2F; if no specified cmd, the func will exit        return;    &#125;    int i;    char nspath[1024];    char *namespace[] &#x3D; &#123;&quot;ipc&quot;, &quot;uts&quot;, &quot;net&quot;, &quot;pid&quot;, &quot;mnt&quot;&#125;;    for (i &#x3D; 0; i &lt; 5; i++)    &#123;        &#x2F;&#x2F; create the target path, like &#x2F;proc&#x2F;pid&#x2F;ns&#x2F;ipc        sprintf(nspath, &quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;%s&quot;, simple_docker_pid, namespace[i]);        int fd &#x3D; open(nspath, O_RDONLY);printf(&quot;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; %d %s\n&quot;, fd, nspath);        &#x2F;&#x2F; call sentns and enter the target namespace        if (setns(fd, 0) &#x3D;&#x3D; -1)        &#123;            fprintf(stderr, &quot;setns on %s namespace failed: %s\n&quot;, namespace[i], strerror(errno));        &#125;        else        &#123;            fprintf(stdout, &quot;setns on %s namespace succeeded\n&quot;, namespace[i]);        &#125;        close(fd);    &#125;    &#x2F;&#x2F; run command in target namespace    int res &#x3D; system(simple_docker_cmd);    exit(0);    return;&#125;*&#x2F;import &quot;C&quot;</code></pre><p>那如何使用这段代码呢，只需要在要加载的地方引用这个 package即可，我这里是 <code>nenster</code> 。</p><p>其实也可以，单独放在一个 C 文件里，go 文件可以这样写：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenterimport &quot;C&quot;</code></pre><p>下面增加 <code>ExecCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ExecCommand &#x3D; cli.Command&#123;Name:  &quot;exec&quot;,Usage: &quot;exec a command into container&quot;,Action: func(context *cli.Context) error &#123;if os.Getenv(ENV_EXEC_PID) !&#x3D; &quot;&quot; &#123;logrus.Infof(&quot;pid callback pid %v&quot;, os.Getgid())return nil&#125;if len(context.Args()) &lt; 2 &#123;return fmt.Errorf(&quot;missing container name or command&quot;)&#125;containerName :&#x3D; context.Args()[0]cmdArray :&#x3D; make([]string, len(context.Args())-1)for i, v :&#x3D; range context.Args().Tail() &#123;cmdArray[i] &#x3D; v&#125;ExecContainer(containerName, cmdArray)return nil&#125;,&#125;</code></pre><p>新建一个 <code>exec.go</code>下面实现获取容器名和需要的命令，并且在这里引用<code>nsenter</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ENV_EXEC_PID &#x3D; &quot;simple_docker_pid&quot;const ENV_EXEC_CMD &#x3D; &quot;simple_docker_cmd&quot;func getContainerPidByName(containerName string) (string, error) &#123;&#x2F;&#x2F; get the path that store container infodirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read files in target pathcontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;return &quot;&quot;, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to containerInfoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;return &quot;&quot;, err&#125;return containerInfo.Pid, nil&#125;func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run --name jay -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-07T13:23:09+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6530018751   jay         146639      running     top         2023-05-07 13:23:09# go run . logs jayMem: 4355160K used, 3478372K free, 3272K shrd, 208844K buff, 1581396K cachedCPU:  1.2% usr  0.6% sys  0.0% nic 97.9% idle  0.0% io  0.0% irq  0.1% sirqLoad average: 0.12 0.14 0.16 1&#x2F;574 6  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND# go run . exec jay sh&#x2F; # lsbin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top   13 root      0:00 sh   15 root      0:00 ps -ef&#x2F; #</code></pre><p>可以看到，成功进入容器内部，且与宿主机隔离。</p><p>这里出现了一个很奇怪的 bug，就是通过 cgo 去 setns，执行到 mnt时，抛出个错误：<code>Stale file handle</code>，当时找了全网，也找不到答案，于是陷入了两天的痛苦debug，在重新敲代码时，发现又不报错了，切换回那个有错误的分支，也不报错了。既然暂时找不到错误，先搁着吧，如果有看到这篇文章的朋友，也遇到了这个错误，可以留意下。(感觉会是一个雷)</p><p>(应该是容器的 mnt 没有 mount 上去，才会导致 stale file handle)</p><h4 id="停止容器-3">6.5 停止容器</h4><p>定义 <code>StopCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var StopCommand &#x3D; cli.Command&#123;Name:  &quot;stop&quot;,Usage: &quot;stop a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]stopContainer(containerName)return nil&#125;,&#125;</code></pre><p>然后声明一个函数，通过容器名来获取容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getContainerInfoByName(containerName string) (*container.ContainerInfo, error) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigNamecontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read config file %s error %v&quot;, configFilePath, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to container infoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json to container info error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>然后是停止容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func stopContainer(containerName string) &#123;&#x2F;&#x2F; get pid by containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container pid by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; turn pid(string) to intpidInt, err :&#x3D; strconv.Atoi(pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;convert pid from string to int error %v&quot;, err)return&#125;&#x2F;&#x2F; kill container main processif err :&#x3D; syscall.Kill(pidInt, syscall.SIGTERM); err !&#x3D; nil &#123;logrus.Errorf(&quot;stop container %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; get info of the containercontainerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; process is killed, update process statuscontainerInfo.Status &#x3D; container.STOPcontainerInfo.Pid &#x3D; &quot; &quot;&#x2F;&#x2F; update info to jsonnweContentBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;json marshal %s error %v&quot;, containerName, err)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; overwrite containerInfoif err :&#x3D; ioutil.WriteFile(configFilePath, nweContentBytes, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;write config file %s error %v&quot;, configFilePath, err)&#125;&#125;</code></pre><p>测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop jay# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6883605813   jay                     stopped     top# ps -ef | grep toproot       43588     761  0 20:00 pts&#x2F;0    00:00:00 grep --color&#x3D;auto top</code></pre><p>可以看到，jay 这个进程被停止了，且 pid 号设为空。</p><h4 id="删除容器-3">6.6 删除容器</h4><p>定义 <code>RemoveCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RemoveCommand &#x3D; cli.Command&#123;Name:  &quot;rm&quot;,Usage: &quot;remove a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]removeContainer(containerName)return nil&#125;,&#125;</code></pre><p>实现删除容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . rm jay# go run . psID          NAME        PID         STATUS      COMMAND     CREATED</code></pre><p>可以看到，jay 这个容器被删除了。</p><h4 id="通过容器制作镜像-3">6.7 通过容器制作镜像</h4><p>这一节，根据书上的内容，有许多函数需要改动。建议这里对着作者给出的源码debug，书上有部分内容有明显错误。</p><p>之前的文件系统如下：</p><ul><li>只读层：busybox，只读，容器系统的基础</li><li>可写层：writeLayer，容器内部的可写层</li><li>挂载层：mnt，挂载外部的文件系统，类似虚拟机的文件共享</li></ul><p>修改后的文件系统如下：</p><ul><li>只读层：不变</li><li>可写层：再加容器名为目录进行隔离，也就是<code>writeLayer/$&#123;containerName&#125;</code></li><li>挂载层：再加容器名为目录进行隔离，也就是<code>mnt/$&#123;containerName&#125;</code></li></ul><p>因此，本节要实现为每个容器分配单独的隔离文件系统，以及实现对不同容器打包镜像。</p><p><strong>修改 <code>run.go</code></strong></p><p>在 Run 函数参数列表添加一个 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><p>同时也在 <code>command.go</code> 的 runCommand 里修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName)return nil&#125;,</code></pre><p>在 <code>recordContainerInfo</code> 函数的参数列表添加 volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName, volume string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;)command :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,Volume:      volume,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>给 ContainerInfo 添加 Volume 成员：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;        &#x2F;&#x2F;容器的init进程在宿主机上的 PIDId          string &#96;json:&quot;id&quot;&#96;         &#x2F;&#x2F;容器IdName        string &#96;json:&quot;name&quot;&#96;       &#x2F;&#x2F;容器名Command     string &#96;json:&quot;command&quot;&#96;    &#x2F;&#x2F;容器内init运行命令CreatedTime string &#96;json:&quot;createTime&quot;&#96; &#x2F;&#x2F;创建时间Status      string &#96;json:&quot;status&quot;&#96;     &#x2F;&#x2F;容器的状态Volume      string &#96;json:&quot;volume&quot;&#96;&#125;</code></pre><p>然后将<code>RootURL</code>，<code>MntURL</code>，<code>WriteLayer</code>设为常量：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&#x2F;&quot;ConfigName          string &#x3D; &quot;config.json&quot;ContainerLogFile    string &#x3D; &quot;container.log&quot;RootURL             string &#x3D; &quot;&#x2F;root&#x2F;&quot;MntURL              string &#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;%s&#x2F;&quot;WriteLayerURL       string &#x3D; &quot;&#x2F;root&#x2F;writeLayer&#x2F;%s&quot;)</code></pre><p>相应地，<code>NewParentProcess</code> 函数也要修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p><code>NewWorkSpace</code>函数的三个参数分别改为：<code>volume</code>，<code>imageName</code>，<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(volume, imageName, containerName string) &#123;CreateReadOnlyLayer(imageName)CreateWriteLayer(containerName)CreateMountPoint(containerName, imageName)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(volumeURLs, containerName)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;</code></pre><p>下面来修改<code>CreateReadOnlyLayer</code>，<code>CreateWriteLayer</code>，<code>CreateMountPoint</code>这三个函数：</p><p>首先是 <code>CreateReadOnlyLayer</code>，参数名改为<code>imageName</code>，镜像解压出来的只读层以<code>RootURL+imageName</code> 命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateReadOnlyLayer(imageName string) error &#123;unTarFolderURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;&#x2F;&quot;imageURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;exist, err :&#x3D; PathExists(unTarFolderURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, unTarFolderURL, err)return err&#125;if !exist &#123;if err :&#x3D; os.MkdirAll(unTarFolderURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, unTarFolderURL, err)return err&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, imageURL, &quot;-C&quot;, unTarFolderURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, unTarFolderURL, err)return err&#125;&#125;return nil&#125;</code></pre><p><code>CreateWriteLayer</code> 为每个容器创建一个读写层，把参数改为containerName，容器读写层修改为 <code>WriteLayerURL+containerName</code>命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateWriteLayer(containerName string) &#123;writeUrl :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.MkdirAll(writeUrl, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;Mkdir write layer dir %s error. %v&quot;, writeUrl, err)&#125;&#125;</code></pre><p><code>CreateMountPoint</code>创建容器根目录，然后把镜像只读层和容器读写层挂载到容器根目录，成为容器文件系统，参数列表改为<code>containerName</code> 和 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateMountPoint(containerName, imageName string) error &#123;&#x2F;&#x2F; create mnt folder as mount pointmntURL :&#x3D; fmt.Sprintf(MntURL, containerName)if err :&#x3D; os.MkdirAll(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)return err&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;tmpWriteLayer :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)tmpImageLocation :&#x3D; RootURL + &quot;&#x2F;&quot; + imageNamedirs :&#x3D; &quot;dirs&#x3D;&quot; + tmpWriteLayer + &quot;:&quot; + tmpImageLocation_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;run command for creating mount point failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p><code>MountVolume</code> 根据用户输入的 volume参数获取相应挂载宿主机数据卷 URL 和容器的挂载点URL，并挂载数据卷。参数列表改为 <code>volumeURLs</code> 和<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerVolumeURL :&#x3D; mntURL + &quot;&#x2F;&quot; + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURL_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>然后在删除容器的 <code>removeContainer</code> 函数最后加一行<code>DeleteWorkSpace</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;container.DeleteWorkSpace(containerInfo.Volume, containerName)&#125;</code></pre><p>然后 <code>DeleteWorkSpace</code>也要修改，<code>DeleteWorkSpace</code>作用是当容器退出时，删除容器相关文件系统，参数列表改为 containerName 和volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(volume, containerName string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(volumeURLs, containerName)&#125; else &#123;DeleteMountPoint(containerName)&#125;&#125; else &#123;DeleteMountPoint(containerName)&#125;DeleteWriteLayer(containerName)&#125;</code></pre><p><code>DeleteMountPoint</code>函数作用是删除未挂载数据卷的容器文件系统，参数修改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(containerName string) error &#123;mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)return err&#125;return nil&#125;</code></pre><p><code>DeleteMountPointWithVolume</code>函数用来删除挂载数据卷容器的文件系统，参数列表改为<code>volumeURLs</code> 和 <code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; umount volume point in containermntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerURL :&#x3D; mntURL + &quot;&#x2F;&quot; + volumeURLs[1]if _, err :&#x3D; exec.Command(&quot;umount&quot;, containerURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)return err&#125;&#x2F;&#x2F; umount the whole point of the container_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;return nil&#125;</code></pre><p><code>DeleteWriteLayer</code> 函数用来删除容器读写层，参数改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWriteLayer(containerName string) &#123;writeURL :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;</code></pre><p>然后修改 <code>command.go</code> 中的<code>commitCommand</code>：输入参数名改为 <code>containerName</code> 和<code>imageName</code>：·</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]imageName :&#x3D; context.Args()[1]&#x2F;&#x2F; commitContainer(containerName)commitContainer(containerName, imageName)return nil&#125;,&#125;</code></pre><p>修改 <code>commit.go</code> 的 <code>commitContainer</code>函数，根据传入的 containerName 制作 <code>imageName.tar</code>镜像：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func commitContainer(containerName, imageName string) &#123;mntURL :&#x3D; fmt.Sprintf(container.MntURL, containerName)mntURL +&#x3D; &quot;&#x2F;&quot;imageTar :&#x3D; container.RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>测试一下，用 busybox 启动两个容器 test1 和 test2，test1 把宿主机<code>/root/from1</code> 挂载到容器 <code>/to1</code>，test2 把宿主机<code>/root/from2</code> 挂载到 <code>/to2</code> 下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test1 -v &#x2F;root&#x2F;from1:&#x2F;to1 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from1\&quot; \&quot;&#x2F;to1\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;# go run . run -d --name test2 -v &#x2F;root&#x2F;from2:&#x2F;to2 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from2\&quot; \&quot;&#x2F;to2\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1       11570       running     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>打开另一个终端，可以看到 <code>/root</code> 目录下多了<code>from1</code> 和 <code>from2</code> 两个目录，我们看看<code>mnt</code> 和 <code>writeLayer</code>，<code>mnt</code> 下多了两个busybox 的挂载层，<code>writeLayer</code>下分别挂载了两个容器的目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   └── to1└── test2    └── to2</code></pre><p>下面进入 test1 容器，创建 <code>/to1/test1.txt</code>：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . exec test1 sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 11570&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#x2F; # echo -e &quot;test1&quot; &gt;&gt; &#x2F;to1&#x2F;test1.txt&#x2F; # mkdir to1-1&#x2F; # echo -e &quot;test111111&quot; &gt;&gt; &#x2F;to1-1&#x2F;test1111.txt</code></pre><p>这时候再来看看可写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   ├── root│   ├── to1│   └── to1-1│       └── test1111.txt└── test2    └── to2# cat writeLayer&#x2F;test1&#x2F;to1-1&#x2F;test1111.txttest111111</code></pre><p>多了 <code>to1-1/test1111.txt</code>，那刚刚创建的<code>test1.txt</code> 去哪了呢？这时候我们看看<code>from1</code>，在这里，新创建的文件写入了数据卷。</p><p>下面来验证 commit 功能：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit test1 image1</code></pre><p>导出的镜像路径为 <code>/root/image1.tar</code>。</p><p>下面测试停止和删除容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1                   stopped     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51# go run . rm test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>我们看看容器根目录和可读写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls mnttest2# tree writeLayer&#x2F;writeLayer&#x2F;└── test2    └── to2</code></pre><p>test1 的容器根目录和可读写层被删除。</p><p>下面来试一下用镜像创建容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test3 -v &#x2F;root&#x2F;from3:&#x2F;to3 image1 top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from3\&quot; \&quot;&#x2F;to3\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:514713076733   test3       13056       running     top         2023-05-11 10:32:44</code></pre><p>这时我们可以看到 <code>/root</code> 多了一个 <code>image1</code>目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls image1bin  dev  etc  home  lib  lib64  proc  root  sys  tmp  to1  to1-1  usr  var</code></pre><p>在这里发现了刚才创建的 <code>to1-1</code>，用 <code>image1.tar</code>启动的容器 test3，进入容器后发现我们刚刚写入的文件，至此，我们成功把容器test1 的数据卷 to1 信息，重新写入了容器 test3 数据卷 to3。</p><p>在次小节后，进入容器都要指定镜像名，不然都会报错。</p><h4 id="实现容器指定环境变量运行-3">6.8 实现容器指定环境变量运行</h4><p>本节来实现让容器内运行的程序可以使用外部传递的环境变量。</p><h5 id="修改-runcommand-3">6.8.1 修改 runCommand</h5><p>在原来基础上增加 <code>-e</code>选项，允许用户指定环境变量，由于环境变量可以是多个，这里允许用户多次使用<code>-e</code> 来传递，同时添加对环境变量的解析，整体修改如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name:  &quot;d&quot;,Usage: &quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;name&quot;,Usage: &quot;container name&quot;,&#125;, &amp;cli.StringSliceFlag&#123;Name:  &quot;e&quot;,Usage: &quot;set environment&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)envSlice :&#x3D; context.StringSlice(&quot;e&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName, envSlice)return nil&#125;,&#125;</code></pre><h5 id="修改-run-函数-3">6.8.2 修改 Run 函数</h5><p>参数里新增一个 <code>envSlice</code>，然后传递给<code>NewParentProcess</code> 函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string, envSlice []string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName, envSlice)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><h5 id="修改-newparentprocess-函数-3">6.8.3 修改 NewParentProcess函数</h5><p>参数新增一个 <code>envSlice</code>，给 cmd 设置环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string, envSlice []string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;cmd.Env &#x3D; append(os.Environ(), envSlice...)NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it --name test -e test&#x3D;123 -e luck&#x3D;test busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;test&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#x2F; #  env | grep testtest&#x3D;123luck&#x3D;test</code></pre><p>可以看到，手动指定的环境变量在容器内可见。后面创建一个后台运行的容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:19:31+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9649354121   test        29524       running     top         2023-05-11 14:19:31# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 29524&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top    7 root      0:00 sh    8 root      0:00 ps -ef&#x2F; # env | grep test&#x2F; #</code></pre><p>查看环境变量，没有我们设置的环境变量。</p><p>这里不能用 env 命令获取设置的环境变量，原因是 exec 可以说 go发起的另一个进程，这个进程的父进程是宿主机的，这个，并不是容器内的。在cgo 内使用了 setns系统调用，才使得进程进入了容器内部的命名空间，但由于环境变量是继承自父进程的，因此这个exec 进程的环境变量其实是继承自宿主机，所以在 exec看到的环境变量其实是宿主机的环境变量。</p><p>但只要是容器内 pid 为 1的进程，创造出来的进程都会继承它的环境变量，下面来修改 exec命令来直接使用 env 命令来查看容器内环境变量的功能。</p><h5 id="修改-exec-命令-3">6.8.4 修改 exec 命令</h5><p>提供一个函数，可根据指定的 pid 来获取对应进程的环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getEnvsByPid(pid string) []string &#123;path :&#x3D; fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;environ&quot;, pid)contentBytes ,err :&#x3D; ioutil.ReadFile(path)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, path, err)return nil&#125;&#x2F;&#x2F; divide by &#39;\u0000&#39;envs :&#x3D; strings.Split(string(contentBytes),&quot;\u0000&quot;)return envs&#125;</code></pre><p>由于进程存放环境变量的位置是<code>/proc/$&#123;pid&#125;/environ</code>，因此根据给定的 pid去读取这个文件，可以获取环境变量，在文件的描述中，每个环境变量之间通过<code>\u0000</code> 分割，因此可以以此标记来获取环境变量数组。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;&#x2F;&#x2F; get target pid environ (container environ)containerEnvs :&#x3D; getEnvsByPid(pid)&#x2F;&#x2F; set host environ and container environ to exec processcmd.Env &#x3D; append(os.Environ(), containerEnvs...)if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>这里由于 exec命令依然要宿主机的一些环境变量，因此将宿主机环境变量和容器环境变量都一起放置到exec 进程中：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:03+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9729397397   test        50040       running     top         2023-05-11 14:30:03# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 50040&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#x2F; # env | grep testtest&#x3D;123luck&#x3D;test&#x2F; #</code></pre><p>现在可以看到 exec 进程可以获取前面 run 时设置的环境变量了。</p><h2 id="四网络篇-3">四、网络篇</h2><h3 id="容器网络-3">7. 容器网络</h3><h4 id="网络虚拟化技术-3">7.1 网络虚拟化技术</h4><h5 id="linux-虚拟网络设备-3">7.1.1 Linux 虚拟网络设备</h5><p>Linux是用网络设备去操作和使用网卡的，系统装了一个网卡后就会为其生成一个网络设备实例，例如eth0。Linux支持创建出虚拟化的设备，可通过组合实现多种多样的功能和网络拓扑，这里主要介绍Veth 和 Bridge。</p><p><strong>Linux Veth</strong></p><p>Veth 时成对出现的虚拟网络设备，发送到 Veth一端虚拟设备的请求会从另一端的虚拟设备中发出。容器的虚拟化场景中，常会使用Veth 连接不同的网络 namespace：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip netns add ns2# ip link add veth0 type veth peer name veth1# ip link set veth0 netns ns1# ip link set veth1 netns ns2# ip netns exec ns1 ip link1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:004: veth0@if3: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;ether 02:bf:18:99:77:ed brd ff:ff:ff:ff:ff:ff link-netns ns2</code></pre><p>在 ns1 和 ns2 的namespace 中，除 loopback的设备以外就只看到了一个网络设备。当请求发送到这个虚拟网络设备时，都会原封不动地从另一个网络namespace的网络接口中出来。例如，给两端分别配置不同地址后，向虚拟网络设备的一端发送请求，就能达到这个虚拟网络设备对应的另一端。</p><p><img src="0x0035/7.1.1-veth.png" style="zoom:43%;" /></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns exec ns1 ifconfig veth0 172.18.0.2&#x2F;24 up# ip netns exec ns2 ifconfig veth1 172.18.0.3&#x2F;24 up# ip netns exec ns1 route add default dev veth0# ip netns exec ns2 route add default dev veth1# ip netns exec ns1 ping -c 1 172.18.0.3PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.64 bytes from 172.18.0.3: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.395 ms--- 172.18.0.3 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.395&#x2F;0.395&#x2F;0.395&#x2F;0.000 ms</code></pre><p><strong>Linux Bridge</strong></p><p>进行下一步之前，先删除上一小节创建的 netns：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns del ns1# ip netns del ns2# ip netns list</code></pre><p>此时之前创建的两个 netns 被删除。</p><p>Bridge虚拟设备时用来桥接的网络设备，相当于现实世界的交换机，可以连接不同的网络设备，当请求达到Bridge 设备时，可以通过报文中的 Mac 地址进行广播或转发。例如，创建一个Bridge 设备，来连接 namespace 中的网络设备和宿主机上的网络：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip link add veth0 type veth peer name veth1# ip link set veth1 netns ns1########## 创建网桥# brctl addbr br0########## 挂载网络设备# brctl addif br0 eth0# brctl addif bro veth0</code></pre><p><img src="0x0035/7.1.1-bridge.png" /></p><h5 id="linux-路由表-2">7.1.2 Linux 路由表</h5><p>路由表是 Linux 内核的一个模块，通过定义路由表来决定在某个网络namespace 中包的流向，从而定义请求会到哪个网络设备上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip link set veth0 up# ip link set br0 up# ip netns exec ns1 ifconfig veth1 172.18.0.2&#x2F;24 up# ip netns exec ns1 route add default dev veth1# route add -net 172.18.0.0&#x2F;24 dev br0</code></pre><p><img src="0x0035/7.1.2-route.png" /></p><p>通过设置路由，对 IP地址的请求就能正确被路由到对应的网络设备上，从而实现通信：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ifconfig eth0eth0: flags&#x3D;4163&lt;UP,BROADCAST,RUNNING,MULTICAST&gt;  mtu 1500        inet 172.31.93.218  netmask 255.255.240.0  broadcast 172.31.95.255        inet6 fe80::215:5dff:fe4e:a16a  prefixlen 64  scopeid 0x20&lt;link&gt;        ether 00:15:5d:4e:a1:6a  txqueuelen 1000  (Ethernet)        RX packets 829  bytes 394161 (394.1 KB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 90  bytes 10335 (10.3 KB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0########## 在namespace访问宿主机# ip netns exec ns1 ping -c 1 172.31.93.218PING 172.31.93.218 (172.31.93.218) 56(84) bytes of data.64 bytes from 172.31.93.218: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.556 ms--- 172.31.93.218 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.556&#x2F;0.556&#x2F;0.556&#x2F;0.000 ms######### 从宿主机访问namespace的网络地址# ping -c 1 172.18.0.2PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.64 bytes from 172.18.0.2: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.113 ms--- 172.18.0.2 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.113&#x2F;0.113&#x2F;0.113&#x2F;0.000 ms</code></pre><h5 id="linux-iptables-2">7.1.3 Linux iptables</h5><p>iptables 是对 Linux 内核的 netfilter模块进行操作和展示的工具，用来管理包的流动和转送。iptables定义了一套链式处理的结构，在网络包传输的各个阶段可以使用不同的策略和包进行加工、传送或丢弃。在容器虚拟化技术里，常会用到两种策略，MASQUERADE和 DNAT，用于容器和宿主机外部的网络通信。</p><p><strong>MASQUERADE</strong></p><p>MASQUERADE 策略可以将请求包中的源地址转换为一个网络设备的地址，例如<a href="#7.1.2%20Linux%20路由表">7.1.2 Linux 路由表</a>这一小节里，namespace 中网络设备的地址是172.18.0.2，这个地址虽然在宿主机可以路由到 br0的网桥，但是到底宿主机外部后，是不知道如何路由到这个 IP的，所以如果请求外部地址的话，要先通过 MASQUERADE 策略将这个 IP转换为宿主机出口网卡的 IP：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># sysctl -w net.ipv4.conf.all.forwarding&#x3D;1net.ipv4.conf.all.forwarding &#x3D; 1# iptables -t nat -A POSTROUTING -s 172.18.0.0&#x2F;24 -o eth0 -j MASQUERADE</code></pre><p>在 namespace 中请求宿主机外部地址时，将 namespace中源地址转换为宿主机的地址作为源地址，就可以在 namespace中访问宿主机外的网络了。</p><p><strong>DAT</strong></p><p>iptables 中的 DNAT策略也是做网络地址的转换，不过它是要更换目标地址，常用于将内部网络地址的端口映射出去。例如，上面例子的namespace如果要提供服务给宿主机之外的应用要怎么办呢？外部应用没办法直接路由到172.18.0.2 这个地址，这时候可以用 DNAT 策略。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80</code></pre><p>这样就可以把宿主机上的 80 端口的 TCP 请求转发到 namespace 的172.18.0.2:80，从而实现外部应用的调用。</p><h4 id="构建容器网络模型-2">7.2 构建容器网络模型</h4><h5 id="基本模型-2">7.2.1 基本模型</h5><h6 id="网络-2">网络</h6><p>网络是容器的一个集合，在这个网络上的容器可以相互通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Network struct &#123;    Name    string &#x2F;&#x2F; network name    IpRange *net.IPNet &#x2F;&#x2F; address    Driver  string &#x2F;&#x2F; network driver name&#125;</code></pre><h6 id="网络端点-2">网络端点</h6><p>网络端点用于连接网络与容器，保证容器内部与网络的通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Endpoint struct &#123;ID          string           &#96;json:&quot;id&quot;&#96;Device      netlink.Veth     &#96;json:&quot;dev&quot;&#96;IPAddress   net.IP           &#96;json:&quot;ip&quot;&#96;MacAddress  net.HardwareAddr &#96;json:&quot;mac&quot;&#96;Network     *NetworkPortMapping []string&#125;</code></pre><p>网络端点的信息传输需要靠网络功能的两个组件配合完成，分别为网络驱动和IPAM。</p><h6 id="网络驱动-2">网络驱动</h6><p>网络驱动是网络功能的一个组件，不同驱动对网络的创建、连接、销毁策略不同，通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type NetworkDriver interface &#123;Name() string &#x2F;&#x2F; driver nameCreate(subnet string, name string) (*Network, error)Delete(network Network) errorConnect(network *Network, endpoint *Endpoint) errorDisconnect(network Network, endpoint *Endpoint) error&#125;</code></pre><h6 id="ipam-2">IPAM</h6><p>IPAM 也是网络功能的一个组件，用于网络 IP 地址的分配和释放，包括容器的IP 和网络网关的 IP。主要功能如下：</p><ul><li><code>ipam.Allocate(*net.IPNet)</code> 从指定的 subnet 网段中分配IP　</li><li><code>ipam.Release(*net.IPNet, net.IP)</code> 从指定的 subnet网段中释放掉指定的 IP</li></ul><p>在构建下面的函数之前，先来补充一些书上没写的：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (defaultNetworkPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;network&#x2F;&quot; &#x2F;&#x2F; 默认网络配置信息存储位置drivers            &#x3D; map[string]NetworkDriver&#123;&#125; &#x2F;&#x2F; 驱动字典，存储驱动信息networks           &#x3D; map[string]*Network&#123;&#125; &#x2F;&#x2F; 网络字段，存储网络信息)</code></pre><h5 id="调用关系-2">7.2.2 调用关系</h5><h6 id="创建网络-2">创建网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateNetwork(driver, subnet, name string) error &#123;_, cidr, _ :&#x3D; net.ParseCIDR(subnet)    &#x2F;&#x2F; allocate gateway ip by IPAMgatewayIP, err :&#x3D; ipAllocator.Allocate(cidr)if err !&#x3D; nil &#123;return err&#125;cidr.IP &#x3D; gatewayIPnw, err :&#x3D; drivers[driver].Create(cidr.String(), name)if err !&#x3D; nil &#123;return err&#125;    &#x2F;&#x2F; save network inforeturn nw.dump(defaultNetworkPath)&#125;</code></pre><p>其中，network.dump 和 network.load方法是将这个网络的配置信息保存在文件系统中，或从网络的配置目录中的文件读取到网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) dump(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(dumpPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(dumpPath, 0644)&#125; else &#123;return err&#125;&#125;nwPath :&#x3D; path.Join(dumpPath, nw.Name)    &#x2F;&#x2F; create file while empty file, write only, no filenwFile, err :&#x3D; os.OpenFile(nwPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;defer nwFile.Close()nwJson, err :&#x3D; json.Marshal(nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;_, err &#x3D; nwFile.Write(nwJson)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;return nil&#125;func (nw *Network) load(dumpPath string) error &#123;nwConfigFile, err :&#x3D; os.Open(dumpPath)if err !&#x3D; nil &#123;return err&#125;defer nwConfigFile.Close()nwJson :&#x3D; make([]byte, 2000)n, err :&#x3D; nwConfigFile.Read(nwJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(nwJson[:n], nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error load nw info: %v&quot;, err)return err&#125;return nil&#125;</code></pre><h6 id="创建容器并连接网络-2">创建容器并连接网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Connect(networkName string, cinfo *container.ContainerInfo) error &#123;network, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;ip, err :&#x3D; ipAllocator.Allocate(network.IpRange)if err !&#x3D; nil &#123;return err&#125;ep :&#x3D; &amp;Endpoint&#123;ID:          fmt.Sprintf(&quot;%s-%s&quot;, cinfo.Id, networkName),IPAddress:   ip,Network:     network,PortMapping: cinfo.PortMapping,&#125;if err &#x3D; drivers[network.Driver].Connect(network, ep); err !&#x3D; nil &#123;return err&#125;if err &#x3D; configEndpointIpAddressAndRoute(ep, cinfo); err !&#x3D; nil &#123;return err&#125;return configPortMapping(ep, cinfo)&#125;</code></pre><h6 id="展示网络列表-2">展示网络列表</h6><p>从网络配置的目录中加载所有的网络配置信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Init() error &#123;var bridgeDriver &#x3D; BridgeNetworkDriver&#123;&#125;drivers[bridgeDriver.Name()] &#x3D; &amp;bridgeDriverif _, err :&#x3D; os.Stat(defaultNetworkPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(defaultNetworkPath, 0644)&#125; else &#123;return err&#125;&#125;filepath.Walk(defaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error &#123;         &#x2F;&#x2F; skip if dirif info.IsDir() &#123;return nil&#125;if strings.HasSuffix(nwPath, &quot;&#x2F;&quot;) &#123;return nil&#125;         &#x2F;&#x2F; load filename as network name_, nwName :&#x3D; path.Split(nwPath)nw :&#x3D; &amp;Network&#123;Name: nwName,&#125;if err :&#x3D; nw.load(nwPath); err !&#x3D; nil &#123;logrus.Errorf(&quot;error load network: %s&quot;, err)&#125;&#x2F;&#x2F; save network info to network dicnetworks[nwName] &#x3D; nwreturn nil&#125;)return nil&#125;</code></pre><p>遍历展示创建的网络：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListNetwork() &#123;w :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprint(w, &quot;NAME\tIpRange\tDriver\n&quot;)for _, nw :&#x3D; range networks &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\n&quot;,nw.Name,nw.IpRange.String(),nw.Driver,)&#125;if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;Flush error %v&quot;, err)return&#125;&#125;</code></pre><h6 id="删除网络-2">删除网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteNetwork(networkName string) error &#123;nw, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;if err :&#x3D; ipAllocator.Release(nw.IpRange, &amp;nw.IpRange.IP); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network gateway ip: %s&quot;, err)&#125;if err :&#x3D; drivers[nw.Driver].Delete(*nw); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network DriverError: %s&quot;, err)&#125;return nw.remove(defaultNetworkPath)&#125;</code></pre><p>删除网络的同时也删除配置目录的网络配置文件：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) remove(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(path.Join(dumpPath, nw.Name)); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125; else &#123;return os.Remove(path.Join(dumpPath, nw.Name))&#125;&#125;</code></pre><h4 id="容器地址分配-2">7.3 容器地址分配</h4><p>现在转到 <code>ipam.go</code>。</p><h5 id="数据结构定义-2">7.3.1 数据结构定义</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ipamDefaultAllocatorPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;ipam&#x2F;subnet.json&quot;type IPAM struct &#123;SubnetAllocatorPath stringSubnets             *map[string]string&#125;&#x2F;&#x2F; 初始化一个IPAM对象，并指定默认分配信息存储位置var ipAllocator &#x3D; &amp;IPAM&#123;SubnetAllocatorPath: ipamDefaultAllocatorPath,&#125;</code></pre><p>反序列化读取网段分配信息和序列化保存网段分配信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) load() error &#123;if _, err :&#x3D; os.Stat(ipam.SubnetAllocatorPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.Open(ipam.SubnetAllocatorPath)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()subnetJson :&#x3D; make([]byte, 2000)n, err :&#x3D; subnetConfigFile.Read(subnetJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(subnetJson[:n], ipam.Subnets)if err !&#x3D; nil &#123;logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)return err&#125;return nil&#125;func (ipam *IPAM) dump() error &#123;ipamConfigFileDir, _ :&#x3D; path.Split(ipam.SubnetAllocatorPath)if _, err :&#x3D; os.Stat(ipamConfigFileDir); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(ipamConfigFileDir, 0644)&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()ipamConfigJson, err :&#x3D; json.Marshal(ipam.Subnets)if err !&#x3D; nil &#123;return err&#125;_, err &#x3D; subnetConfigFile.Write(ipamConfigJson)if err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h5 id="地址分配-2">7.3.2 地址分配</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) &#123;ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;err &#x3D; ipam.load()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error dump allocation info, %v&quot;, err)&#125;_, subnet, _ &#x3D; net.ParseCIDR(subnet.String())one, size :&#x3D; subnet.Mask.Size()if _, exist :&#x3D; (*ipam.Subnets)[subnet.String()]; !exist &#123;        &#x2F;&#x2F; 用0填满网段的配置，1&lt;&lt;uint8(size-one)表示这个网段中有多少个可用地址        &#x2F;&#x2F; size-one时子网掩码后面的网络位数，2^(size-one)表示网段中的可用IP数        &#x2F;&#x2F; 2^(size-one)等价于1&lt;&lt;uint8(size-one)        (*ipam.Subnets)[subnet.String()] &#x3D; strings.Repeat(&quot;0&quot;, 1&lt;&lt;uint8(size-one))&#125;&#x2F;&#x2F; 这里的原理建议大家看看原著for c :&#x3D; range (*ipam.Subnets)[subnet.String()] &#123;if (*ipam.Subnets)[subnet.String()][c] &#x3D;&#x3D; &#39;0&#39; &#123;            ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])            &#x2F;&#x2F; go的字符串创建后不能修改，先用byte存储            ipalloc[c] &#x3D; &#39;1&#39;            (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)            &#x2F;&#x2F;             ip &#x3D; subnet.IP                        &#x2F;&#x2F; 通过网段的IP与上面的偏移相加得出分配的IP，由于IP是一个uint的一个数组，需要通过数组中的每一项加所需要的值，例 &#x2F;&#x2F; 如网段是172.16.0.0&#x2F;12，数组序号是65555，那就要在[172,16,0,0]上依次加            &#x2F;&#x2F; [uint8(65555 &gt;&gt; 24), uint8(65555 &gt;&gt; 16), uint8(65555 &gt;&gt; 8), uint(65555 &gt;&gt; 4)]，即[0,1,0,19]，            &#x2F;&#x2F; 那么获得的IP就是172.17.0.19            for t :&#x3D; uint(4); t &gt; 0; t-- &#123;                []byte(ip)[4-t] +&#x3D; uint8(c &gt;&gt; ((t - 1) * 8))            &#125;            &#x2F;&#x2F; 由于此处IP是从1开始分配的，所以最后再加1，最终得到分配的IP是172.16.0.20            ip[3]++            break&#125;&#125;ipam.dump()return&#125;</code></pre><h5 id="地址释放-2">7.3.3 地址释放</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error &#123;    ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;    _, subnet, _ &#x3D; net.ParseCIDR(subnet.String())    err :&#x3D; ipam.load()    if err !&#x3D; nil &#123;        logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)    &#125;    c :&#x3D; 0    &#x2F;&#x2F; 将IP转换为4个字节的表示方式    releaseIP :&#x3D; ipaddr.To4()    &#x2F;&#x2F; 由于IP是从1开始分配的，所以转换成索引减1    releaseIP[3] -&#x3D; 1    for t :&#x3D; uint(4); t &gt; 0; t -&#x3D; 1 &#123;        &#x2F;&#x2F; 和分配IP相反，释放IP获得索引的方式是IP的每一位相减后分别左移将对应的数值加到索引上        c +&#x3D; int(releaseIP[t-1]-subnet.IP[t-1]) &lt;&lt; ((4 - t) * 8)    &#125;    ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])    ipalloc[c] &#x3D; &#39;0&#39;    (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)    ipam.dump()    return nil&#125;</code></pre><p>根据书上，写到这里就开始测试了，但是我们看看IDE，红海一片，所以我们接着实现。</p><h4 id="创建-bridge-网络-2">7.4 创建 bridge 网络</h4><h5 id="实现-bridge-driver-create-2">7.4.1 实现 Bridge DriverCreate</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) &#123;ip, ipRange, _ :&#x3D; net.ParseCIDR(subnet)ipRange.IP &#x3D; ipn :&#x3D; &amp;Network&#123;Name:    name,IpRange: ipRange,Driver:  d.Name(),&#125;err :&#x3D; d.initBridge(n)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error init bridge: %v&quot;, err)&#125;return n, err&#125;</code></pre><h5 id="bridge-driver-初始化-linux-bridge-2">7.4.2 Bridge Driver 初始化Linux Bridge</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) initBridge(n *Network) error &#123;&#x2F;&#x2F; 创建bridge虚拟设备bridgeName :&#x3D; n.Nameif err :&#x3D; createBridgeInterface(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;eror add bridge: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置bridge设备的地址和路由gatewayIP :&#x3D; *n.IpRangegatewayIP.IP &#x3D; n.IpRange.IPif err :&#x3D; setInterfaceIP(bridgeName, gatewayIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error assigning address: %s on bridge: %s with an error of: %v&quot;, gatewayIP, bridgeName, err)&#125;&#x2F;&#x2F; 启动bridge设备if err :&#x3D; setInterfaceUP(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error set bridge up: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置iptables的SNAT规则if err :&#x3D; setupIPTables(bridgeName, n.IpRange); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error setting iptables for %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="创建-bridge-设备-2">创建 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func createBridgeInterface(bridgeName string) error &#123;_, err :&#x3D; net.InterfaceByName(bridgeName)if err &#x3D;&#x3D; nil || !strings.Contains(err.Error(), &quot;no such network interface&quot;) &#123;return err&#125;&#x2F;&#x2F; create *netlink.Bridge objectla :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; bridgeNamebr :&#x3D; &amp;netlink.Bridge&#123;LinkAttrs: la&#125;if err :&#x3D; netlink.LinkAdd(br); err !&#x3D; nil &#123;return fmt.Errorf(&quot;bridge creation failed for bridge %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="设置-bridge-设备的地址和路由-2">设置 bridge设备的地址和路由</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceIP(name string, rawIP string) error &#123;retries :&#x3D; 2var iface netlink.Linkvar err errorfor i :&#x3D; 0; i &lt; retries; i++ &#123;iface, err &#x3D; netlink.LinkByName(name)if err &#x3D;&#x3D; nil &#123;break&#125;logrus.Debugf(&quot;error retrieving new bridge netlink link [ %s ]... retrying&quot;, name)time.Sleep(2 * time.Second)&#125;if err !&#x3D; nil &#123;return fmt.Errorf(&quot;abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot the error: %v&quot;, err)&#125;ipNet, err :&#x3D; netlink.ParseIPNet(rawIP)if err !&#x3D; nil &#123;return err&#125;addr :&#x3D; &amp;netlink.Addr&#123;IPNet:     ipNet,Peer:      ipNet,Label:     &quot;&quot;,Flags:     0,Scope:     0,Broadcast: nil,&#125;return netlink.AddrAdd(iface, addr)&#125;</code></pre><h6 id="启动-bridge-设备-2">启动 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceUP(interfaceName string) error &#123;iface, err :&#x3D; netlink.LinkByName(interfaceName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;error retrieving a link named [ %s ]: %v&quot;, iface.Attrs().Name, err)&#125;if err :&#x3D; netlink.LinkSetUp(iface); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error enabling interface for %s: %v&quot;, interfaceName, err)&#125;return nil&#125;</code></pre><h6 id="设置-iptables-linux-bridge-snat-规则-2">设置 iptables LinuxBridge SNAT 规则</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setupIPTables(bridgeName string, subnet *net.IPNet) error &#123;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE&quot;, subnet.String(), bridgeName)cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)&#125;return err&#125;</code></pre><h5 id="bridge-driver-delete-实现-2">7.4.3 Bridge Driver Delete实现</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Delete(network Network) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;return netlink.LinkDel(br)&#125;</code></pre><h4 id="在-bridge-网络创建容器-2">7.5 在 bridge 网络创建容器</h4><h5 id="挂载容器端点-2">7.5.1 挂载容器端点</h5><h6 id="连接容器网络端点到-linux-bridge-2">连接容器网络端点到 LinuxBridge</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;la :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; endpoint.ID[:5]la.MasterIndex &#x3D; br.Attrs().Indexendpoint.Device &#x3D; netlink.Veth&#123;LinkAttrs: la,PeerName:  &quot;cif-&quot; + endpoint.ID[:5],&#125;if err &#x3D; netlink.LinkAdd(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;if err &#x3D; netlink.LinkSetUp(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;return nil&#125;</code></pre><h6 id="配置容器-namespace-中网络设备及路由-2">配置容器 Namespace中网络设备及路由</h6><p>回到 <code>network.go</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;peerLink, err :&#x3D; netlink.LinkByName(ep.Device.PeerName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;fail config endpoint: %v&quot;, err)&#125;defer enterContainerNetns(&amp;peerLink, cinfo)()interfaceIP :&#x3D; *ep.Network.IpRangeinterfaceIP.IP &#x3D; ep.IPAddressif err &#x3D; setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;%v,%s&quot;, ep.Network, err)&#125;if err &#x3D; setInterfaceUP(ep.Device.PeerName); err !&#x3D; nil &#123;return err&#125;if err &#x3D; setInterfaceUP(&quot;lo&quot;); err !&#x3D; nil &#123;return err&#125;_, cidr, _ :&#x3D; net.ParseCIDR(&quot;0.0.0.0&#x2F;0&quot;)defaultRoute :&#x3D; &amp;netlink.Route&#123;LinkIndex: peerLink.Attrs().Index,Gw:        ep.Network.IpRange.IP,Dst:       cidr,&#125;if err &#x3D; netlink.RouteAdd(defaultRoute); err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h6 id="进入容器-net-namespace-2">进入容器 Net Namespace</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() &#123;f, err :&#x3D; os.OpenFile(fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;net&quot;, cinfo.Pid), os.O_RDONLY, 0)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get container net namespace, %v&quot;, err)&#125;nsFD :&#x3D; f.Fd()runtime.LockOSThread()if err &#x3D; netlink.LinkSetNsFd(*enLink, int(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set link netns , %v&quot;, err)&#125;origns, err :&#x3D; netns.Get()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get current netns, %v&quot;, err)&#125;if err &#x3D; netns.Set(netns.NsHandle(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set netns, %v&quot;, err)&#125;return func() &#123;netns.Set(origns)origns.Close()runtime.UnlockOSThread()f.Close()&#125;&#125;</code></pre><h6 id="配置宿主机到容器的端口映射-2">配置宿主机到容器的端口映射</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configPortMapping(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;for _, pm :&#x3D; range ep.PortMapping &#123;portMapping :&#x3D; strings.Split(pm, &quot;:&quot;)if len(portMapping) !&#x3D; 2 &#123;logrus.Errorf(&quot;port mapping format error, %v&quot;, pm)continue&#125;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s&quot;,portMapping[0], ep.IPAddress.String(), portMapping[1])cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)continue&#125;&#125;return nil&#125;</code></pre><h5 id="修补-bug-2">7.5.2 修补 bug</h5><p>写到这里，代码还是有很多 bug的，例如，<code>BridgeNetworkDriver</code> 未完全继承<code>NetworkDriver</code> 的所有函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Disconnect(network Network, endpoint *Endpoint) error &#123;return nil&#125;</code></pre><h5 id="测试-2">7.5.3 测试</h5><p>现在终于可以测试了。</p><p>首先创建一个网桥：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . network create --driver bridge --subnet 192.168.10.1&#x2F;24 testbridge</code></pre><p>然后启动两个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;8116248511&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#x2F; # ifconfigcif-81162 Link encap:Ethernet  HWaddr 16:62:68:81:E0:A9          inet addr:192.168.10.2  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::1462:68ff:fe81:e0a9&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:14 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1820 (1.7 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; #</code></pre><p>记住这个 IP：<code>192.168.10.2</code>，然后进入另一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;9558830402&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#x2F; # ifconfigcif-95588 Link encap:Ethernet  HWaddr 42:18:0A:73:33:CA          inet addr:192.168.10.3  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::4018:aff:fe73:33ca&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:10 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1248 (1.2 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; # ping 192.168.10.2PING 192.168.10.2 (192.168.10.2): 56 data bytes64 bytes from 192.168.10.2: seq&#x3D;0 ttl&#x3D;64 time&#x3D;2.619 ms64 bytes from 192.168.10.2: seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.086 ms^C--- 192.168.10.2 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 0.086&#x2F;1.352&#x2F;2.619 ms&#x2F; #</code></pre><p>可以看到，两个容器网络互通。</p><p>下面来试一下访问外部网络。我用的 WSL，默认的 nat是关闭的，前期各种设置 iptables规则什么的，都无法访问容器外部的网络，直到发现一篇帖子里说到，需要打开内核的nat功能，要将文件<code>/proc/sys/net/ipv4/ip_forward</code>内的值改为1（默认是0）。执行<code>sysctl -w net.ipv4.ip_forward=1</code> 即可。</p><p>修改之后，继续测试。</p><p>容器默认是没有 DNS 服务器的，需要我们手动添加：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # ping cn.bing.comping: bad address &#39;cn.bing.com&#39;&#x2F; # echo -e &quot;nameserver 8.8.8.8&quot; &gt; &#x2F;etc&#x2F;resolv.conf&#x2F; # ping cn.bing.comPING cn.bing.com (202.89.233.101): 56 data bytes64 bytes from 202.89.233.101: seq&#x3D;0 ttl&#x3D;113 time&#x3D;38.419 ms64 bytes from 202.89.233.101: seq&#x3D;1 ttl&#x3D;113 time&#x3D;39.011 ms^C--- cn.bing.com ping statistics ---3 packets transmitted, 2 packets received, 33% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 38.419&#x2F;38.715&#x2F;39.011 ms&#x2F; #</code></pre><p>然后再来测试容器映射端口到宿主机供外部访问：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -p 90:90 -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;3445154844&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#x2F; # nc -lp 90</code></pre><p>然后访问宿主机的 80 端口，看看能不能转发到容器里：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 172.31.93.218 90Trying 172.31.93.218...telnet: Unable to connect to remote host: Connection refused</code></pre><p>开始我以为是我哪里码错了，然后拿作者的代码来跑，并放到虚拟机上跑，发现并不是自己的问题，那只能这样测试了：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 192.168.10.3 90Trying 192.168.10.3...Connected to 192.168.10.3.Escape character is &#39;^]&#39;.</code></pre><p>出现这样的字眼后，容器和宿主机之间就可以通信了。</p><h2 id="参考链接-2">参考链接</h2><p><a href="https://learnku.com/articles/42072">七天用 Go 写个docker（第一天） | Go 技术论坛 (learnku.com)</a></p><p><a href="https://juejin.cn/post/6971335828060504094">使用 GoLang从零开始写一个 Docker（概念篇）-- 《自己动手写 Docker》读书笔记 - 掘金(juejin.cn)</a></p><p><ahref="https://blog.xtlsoft.top/read/server/building-wsl-kernel-with-aufs.html">编译带有AUFS 支持的 WSL 内核 - 徐天乐 :: 个人博客 (xtlsoft.top)</a></p><p><ahref="https://zhuanlan.zhihu.com/p/324530180">如何让WSL2使用自己编译的内核- 知乎 (zhihu.com)</a></p><p><ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p><ahref="https://juejin.cn/post/7086069688664326157#heading-1">自己动手写Docker系列-- 5.7实现通过容器制作镜像 - 掘金 (juejin.cn)</a></p><p><ahref="https://blog.csdn.net/tycoon1988/article/details/40781291">iptable端口重定向MASQUERADE_tycoon1988的博客-CSDN博客</a></p><h5 id="linux-路由表-3">7.1.2 Linux 路由表</h5><p>路由表是 Linux 内核的一个模块，通过定义路由表来决定在某个网络namespace 中包的流向，从而定义请求会到哪个网络设备上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip link set veth0 up# ip link set br0 up# ip netns exec ns1 ifconfig veth1 172.18.0.2&#x2F;24 up# ip netns exec ns1 route add default dev veth1# route add -net 172.18.0.0&#x2F;24 dev br0</code></pre><h2 id="零前言-4">零、前言</h2><p>本文为《自己动手写 Docker》的学习，对于各位学习 docker的同学非常友好，非常建议买一本来学习。</p><p>书中有摘录书中的一些知识点，不过限于篇幅，没有全部摘录<del>(主要也是懒)</del>。项目仓库地址为：<ahref="https://github.com/JaydenChang/simple-docker">JaydenChang/simple-docker(github.com)</a></p><h2 id="一概念篇-4">一、概念篇</h2><h3 id="基础知识-4">1. 基础知识</h3><h4 id="kernel-4">1.1 kernel</h4><p>kernel (内核)指大多数操作系统的核心部分，由操作系统中用于管理存储器、文件、外设和系统资源的部分组成。操作系统内核通常运行进程，并提供进程间通信。</p><h4 id="namespace-4">1.2 namespace</h4><p>namespace 是 Linux 自带的功能来隔离内核资源的机制。</p><p>Linux 中有 6 种 namespace</p><h5 id="uts-namespace-4">1.2.1 UTS Namespace</h5><p>UTS，UNIX Time Sharing，用于隔离 nodeName (主机名) 和 domainName(域名)。在该 Namespace 下修改 hostname 不会影相其他的 Namespace。</p><h5 id="ipc-namespace-4">1.2.2 IPC Namespace</h5><p>IPC，Inter-Process Communication (进程间通讯)，用于隔离 System V IPC和 POSIX message queues (一种消息队列，结构为链表)。</p><p>两种 IPC 本质上差不多，System V IPC 随内核持续，POSIX IPC随进程持续。</p><h5 id="pid-namespace-4">1.2.3 PID Namespace</h5><p>PID，Process IDs，用于隔绝 PID。同样的进程，在不同 Namespace里是不同的 PID。新建的 PID Namespace 里第一个 PID 是1。</p><h5 id="mount-namespace-4">1.2.4 Mount Namespace</h5><p>用于隔绝文件系统，挂载了某一目录，在这个 Namespace下就会把这个目录当作根目录，我们看到的文件系统树就会以这个目录为根目录。</p><p>mount 操作本身不会影响到外部，docker 中的 volume也用到了这个特性。</p><h5 id="user-namespace-4">1.2.5 User Namespace</h5><p>用于 隔离用户组 ID。</p><h5 id="network-namespace-4">1.2.6 Network Namespace</h5><p>每个 Namespace 都有一套自己的网络设备，可以使用相同的端口号，映射到host 的不同端口。</p><h4 id="linux-cgroups-4">1.3 Linux Cgroups</h4><p>Cgroups 全称为 Control Groups，是 Linux内核提供的物理资源隔离机制。</p><h5 id="cgroups-的三个组件-4">1.3.1 Cgroups 的三个组件</h5><ul><li>cgroup：一个 cgroup 包含一组进程，且可以有 subsystem的参数配置，以关联一组 subsystem。</li><li>subsystem：一组资源控制的模块。</li><li>hierarchy：把一组 cgroups 串成一个树状结构，以提供继承的功能。</li></ul><h5 id="这三个组件的关联-4">1.3.2 这三个组件的关联</h5><p>Linux 有一些限制：</p><ul><li>首先，创建一个 hierarchy。这个 hierarchy 有一个 cgroup根节点，所有的进程都会被加到这个根节点上，所有在这个 hierarchy上创建的节点都是这个根节点的子节点。</li><li>一个 subsystem 只能加到一个 hierarchy 上。</li><li>但是一个 subsystem 可以加到同一个 hierarchy 的多个 cgroups 上。</li><li>一个 hierarchy 可以有多个 subsystem。</li><li>一个进程可以在多个 cgroups 中，但是这些 cgroup 必须在不同的hierarchy 中。</li><li>一个进程 fork 出子进程时，父进程和子进程属于同一个 cgroup。</li></ul><h5 id="cgroup-和-subsystem-和-hierarchy-之间的联系-4">1.3.3 cgroup 和subsystem 和 hierarchy 之间的联系</h5><ul><li>hierarchy 就是一颗 cgroups 树，由多个 cgroups 构成。每一个 hierarchy建立时会包含 ==<em>所有</em>== 的Linux 进程。这里的 “所有”就是当前系统运行中的所有进程，每个 hierarchy上的全部进程都是一样的，不同的 hierarchy指的其实只是不同的分组方式，这也是为什么一个进程可以存在于多个 hierarchy上；准确来说，一个进程一定会同时存在于所有的 hierarchy上，区别在被放在的 cgroup 可能会有差异。</li><li>Linux 的 subsystem 只有一个的说法，没有一种的说法，也就是在一个hierarchy 上使用了 memory subsystem，那么在其他 hierarchy 就不能使用memory subsystem 了。</li><li>subsystem 是一种资源控制器，有很多个 subsystem，每个 subsystem控制不同的资源。subsystem 和 cgroups 关联。新建一个 cgroups文件夹时，里面会自动生成一堆配置文件，那个就是 subsystem 配置文件。但<code>subsystem 配置文件</code> 不是 <code>subsystem</code>，就像<code>.git</code> 不是 <code>git</code> 一样，就像没安装 git也可以从别人那里获得 <code>.git</code>文件夹，只是不能用罢了。<code>subsystem 配置文件</code>也是如此，新建一个 cgroup 就会生成<code>cgroup 配置文件</code>，但并不代表你关联了一个subsystem。只有当改变了一个<code>cgroup 配置文件</code>，里面要限制某种资源时，就会自动关联到这个被限制的资源所对应的subsystem 上。</li><li>假设我的 Linux 有 12 个 subsystem，也就是说我最多只能建 12 个hierarchy (不加 subsystem 的情况下可以建更多 hierarchy，这样 cgroup就变成纯分组使用)。每一个 hierarchy 上一个 subsystem。如果在某个hierarchy 放多个 subsystem，能建立的 hierarchy就更少了。</li><li>subsystem 和 cgroup 是关联的，不是和 hierarchy关联的，但经常看到有人说把某个 subsystem 和某个 hierarchy关联。实质上一般指的是 hierarchy 中的某一个 cgroup 或多个 cgroup关联。</li></ul><h5 id="cgroup-的-kernel-接口-4">1.3.4 cgroup 的 kernel 接口</h5><p>kernel 接口，就是在 Linux 上调用 api 来控制 cgroups。</p><ol type="1"><li><p>首先创建一个 hierarchy，而 hierarchy要挂载到一个目录上，这里创建一个目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mkdir hierarchy-test</code></pre></li><li><p>然后挂载：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t cgroup -o none,name&#x3D;hierarchy-test hierarchy-test .&#x2F;hierarchy-test</code></pre></li><li><p>可以在这个目录下看到一大堆文件，这些文件就是 cgroup根节点的配置。</p></li><li><p>然后在这个目录下创建新的空目录，会发现，新的目录里也会有很多cgroup 配置文件，这些目录已成为 cgroup 根节点的子节点 cgroup。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">.├── cgroup.clone_children├── cgroup.procs├── cgroup.sane_behavior├── notify_on_release├── release_agent├── tasks└── temp  # 这是新创建的文件夹    ├── cgroup.clone_children    ├── cgroup.procs    ├── notify_on_release    └── tasks</code></pre></li><li><p>在 cgroup中添加和移动进程：系统的所有进程都会被放到根节点中，可以根据需要移动进程：</p><ul><li><p>只需将进程 ID 写到对应的 cgroup 的 tasks 文件即可。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo sh -c &quot;echo $$ &gt;&gt; tasks&quot;</code></pre><p>该命令就是将当前终端的这个进程加到当前所在的 cgroup 的目录的 tasks文件中。</p></li></ul></li><li><p>通过 subsystem 限制 cgroup 中进程的资源：</p><ul><li>上面的方法有个问题，因为这个 hierarchy 没有关联到任何subsystem，因此不能够控制资源。</li><li>不过其实系统会自动给每个 subsystem 创建一个hierarchy，所以通过控制这个 hierarchy里的配置，可以达到控制进程的目的。</li></ul></li></ol><h5 id="docker-是怎么使用-cgroups-的-4">1.3.5 docker 是怎么使用 Cgroups的</h5><p>docker 会给每个容器创建一个 cgroup，再限制该 cgroup的资源，从而达到限制容器的资源的作用。</p><p>其实写了这么多，综合上面的前置知识，不难猜测，docker的原理是：隔离主机。</p><h4 id="demo-4">1.4 Demo</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;fmt&quot;&quot;io&#x2F;ioutil&quot;&quot;os&quot;&quot;os&#x2F;exec&quot;&quot;path&quot;&quot;strconv&quot;&quot;syscall&quot;)const cgroupMemoryHierarchyCount &#x3D; &quot;&#x2F;sys&#x2F;fs&#x2F;cgroup&#x2F;memory&quot;func main() &#123;    &#x2F;&#x2F; 第二次会运行这段代码    &#x2F;&#x2F; 这段代码运行的地方就可以看做是一个简易的容器    &#x2F;&#x2F; 这里只是对进程进行了隔离    &#x2F;&#x2F; 但是可以看到 pid 已经变成了 1，因为我们有 PID Namespace    if os.Args[0] &#x3D;&#x3D; &quot;&#x2F;proc&#x2F;self&#x2F;exe&quot; &#123;        fmt.Printf(&quot;current pid %d\n&quot;, syscall.Getpid())        cmd :&#x3D; exec.Command(&quot;sh&quot;, &quot;-c&quot;, &#96;stress --vm-bytes 200m --vm-keep -m 1&#96;)        cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;&#125;        cmd.Stdin &#x3D; os.Stdin        cmd.Stdout &#x3D; os.Stdout        cmd.Stderr &#x3D; os.Stderr        if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;            fmt.Println(err)            os.Exit(1)        &#125;    &#125;        &#x2F;&#x2F; 第一次运行这段    &#x2F;&#x2F; **command 设置为当前进程，也就是这个 go 程序本身，也就是说 cmd.Start() 会再次运行该程序    cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;)    &#x2F;&#x2F; 在 start 之前，修改 cmd 的各种配置，也就是第二次运行这个程序的时候的配置&#x2F;&#x2F; 创建 namespace    cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr &#123;        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,    &#125;    cmd.Stdin &#x3D; os.Stdin    cmd.Stdout &#x3D; os.Stdout    cmd.Stderr &#x3D; os.Stderr        &#x2F;&#x2F; 因为之后要打印 process 的 id，所以用 start    &#x2F;&#x2F; 如果这里用 run 的话，那么 else 里的代码永远不会执行，因为 stress 永远不会结束    if err :&#x3D; cmd.Start(); err !&#x3D; nil &#123;        fmt.Println(&quot;Error&quot;, err)        os.Exit(1)    &#125; else &#123;        &#x2F;&#x2F; 打印 new process id        fmt.Printf(&quot;%v\n&quot;, cmd.Process.Pid)                &#x2F;&#x2F; 接下来三段对 cgroup 操作        &#x2F;&#x2F; the hierarchy has been already created by linux on the memory subsystem        &#x2F;&#x2F; create a sub cgroup           os.Mkdir(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,        ), 0755)                &#x2F;&#x2F; place container process in this cgroup        ioutil.WriteFile(path.Join(            cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;tasks&quot;,        ), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)                &#x2F;&#x2F; restrict the stress process on this cgroup        ioutil.WriteFile(path.Join(        cgroupMemoryHierarchyCount,            &quot;testMemoryLimit&quot;,            &quot;memory.limit_int_bytes&quot;,        ), []byte(&quot;100m&quot;), 0644)                &#x2F;&#x2F; cmd.Start() 不会等待进程结束，所以需要手动等待        &#x2F;&#x2F; 如果不加的话，由于主进程结束了，子进程也会被强行结束        cmd.Process.Wait()    &#125;&#125;</code></pre><h4 id="ufs-4">1.5 UFS</h4><h5 id="ufs-概念-4">1.5.1 UFS 概念</h5><p>UFS，Union File System，联合文件系统。docker 在下载一个 image文件时，会看到一次下载很多个文件，这就是UFS。它是一种分层、轻量、高性能的文件系统。UFS 类似git，每次修改文件时，都是一次提交，并有记录，修改都反映在一个新的文件上，而不是修改旧文件。</p><p>UFS 允许多个不同目录挂载到同一个虚拟文件系统下，这就是为什么 image之间可以共享文件，以及继承镜像的原因。</p><h5 id="aufs-4">1.5.2 AUFS</h5><p>AUFS，Advanced Union File System，是 UFS 的一个改动版本。</p><p>笔者本身使用的是 WSL 做日常开发，WSL 内核不支持AUFS，后面会提到更换内核。</p><h5 id="docker-和-aufs-4">1.5.3 docker 和 AUFS</h5><p>docker 在早期使用 AUFS，直到现在也可以选择作为一种存储驱动类型。</p><h5 id="image-layer-4">1.5.4 image layer</h5><p>image 由多层 read-only layer 构成。</p><p>当启动一个 container 时，就会在 image 上再加一层 init layer，initlayer 也是 read-only 的，用于储存容器的环境配置。此外，docker还会创建一个 read-write 的 layer，用于执行所有的写操作。</p><p>当停止容器时，这个 read-write layer 依然保留，只有删除 container时才会被删除。</p><p>那么，怎么删除旧文件呢？</p><p>docker 会在 read-write layer 生成一个<code>.wh.&lt;fileName&gt;</code> 文件来隐藏要删除的文件。</p><h5 id="实现一个-aufs-4">1.5.5 实现一个 AUFS</h5><p>我们先创建一个如下的文件夹结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt</code></pre><p>然后挂载到 mnt 文件夹上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo mount -t aufs -o dirs&#x3D;.&#x2F;container-layer:.&#x2F;image-layer none .&#x2F;mnt</code></pre><p>如果没有手动添加权限的话，默认 dirs 左边第一个文件夹有 write-read权限，其他都是 read-only。</p><p>我们可以发现，imageLayer1 和 writeLayer 的文件出现在 mnt文件夹下：</p><pre class="line-numbers language-none"><code class="language-none">.├── container-layer│   └── container.txt├── image-layer│   └── image.txt└── mnt    ├── container.txt    └── image.txt</code></pre><p>然后我们修改一下 image.txt的内容，然后再看看整个目录，会发现，<code>container-layer</code>目录下多了一个 <code>image.txt</code>，然后我们看看<code>container-layer</code> 的 <code>image.txt</code>的内容，有添加前后的的文字。</p><p>也就是说，实际上，当修改某一个 layer 的时候，实际上不会改变这个layer，而是将其复制到 container-layer 中，然后再修改这个新的文件。</p><h2 id="二容器篇-4">二、容器篇</h2><h3 id="linux-的-proc-文件夹-4">2. Linux 的 /proc 文件夹</h3><h4 id="pid-4">2.1 PID</h4><p>在 <code>/proc</code>文件夹下可以看到很多文件夹的名字都是个数字，其实就是个 PID。是 Linux为每个进程创建的空间。</p><h4 id="一些重要的目录-4">2.2 一些重要的目录</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F;proc&#x2F;N # PID 为 N 的进程&#x2F;proc&#x2F;N&#x2F;cmdline # 进程的启动命令&#x2F;proc&#x2F;N&#x2F;cwd # 链接到进程的工作目录&#x2F;proc&#x2F;N&#x2F;environ  # 进程的环境变量列表&#x2F;proc&#x2F;N&#x2F;exe # 链接到进程的执行命令&#x2F;proc&#x2F;N&#x2F;fd # 包含进程相关的所有文件描述符&#x2F;proc&#x2F;N&#x2F;maps # 与进程相关的内存映射信息&#x2F;proc&#x2F;N&#x2F;mem # 进程持有的内存，不可读&#x2F;proc&#x2F;N&#x2F;root # 链接到进程的根目录&#x2F;proc&#x2F;N&#x2F;stat # 进程的状态&#x2F;proc&#x2F;N&#x2F;statm # 进程的内存状态&#x2F;proc&#x2F;N&#x2F;status # 比上面两个更可读&#x2F;proc&#x2F;self # 链接到当前正在运行的进程</code></pre><h3 id="简单实现-4">3. 简单实现</h3><h4 id="工具-4">3.1 工具</h4><p>获取帮助编写 command line app 的工具：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go get github.com&#x2F;urfave&#x2F;cli </code></pre><h4 id="实现代码-4">3.2 实现代码</h4><p>代码结构：</p><pre class="line-numbers language-none"><code class="language-none">.├── command.go├── container│   └── init.go├── dockerCommand│   └── run.go├── go.mod├── go.sum└── main.go</code></pre><h5 id="runcommand-4">3.2.1 runCommand</h5><p><code>command.go</code> 用于放置各种 command 命令，这里先只写一个runCommand 命令。</p><p>首先用 urfave/cli 创建一个 runCommand 命令：</p><pre class="line-numbers language-golang" data-language="golang"><code class="language-golang">&#x2F;&#x2F; command.govar runCommand &#x3D; cli.Command&#123;    Name:  &quot;run&quot;,    Usage: &quot;Create a container&quot;,    Flags: []cli.Flag&#123;        &#x2F;&#x2F; integrate -i and -t for convenience        &amp;cli.BoolFlag&#123;            Name:  &quot;it&quot;,            Usage: &quot;open an interactive tty(pseudo terminal)&quot;,        &#125;,    &#125;,    Action: func(context *cli.Context) error &#123;        args :&#x3D; context.Args()        if len(args) &#x3D;&#x3D; 0 &#123;            return errors.New(&quot;Run what?&quot;)        &#125;        cmdArray :&#x3D; args.Get(0)        &#x2F;&#x2F; command        &#x2F;&#x2F; check whether type &#96;-it&#96;        tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal                &#x2F;&#x2F; 这个函数在下面定义        dockerCommand.Run(tty, cmdArray)        return nil    &#125;,&#125;</code></pre><h5 id="run-4">3.2.2 run</h5><p>上面的 Run 函数在 <code>dockerCommand/run.go</code> 下定义。当运行<code>docker run</code> 时，实际上主要是 Action 下的这个函数在工作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; dockerCommand&#x2F;run.go&#x2F;&#x2F; This is the function what &#96;docker run&#96; will callfunc Run(tty bool, cmdArray string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess :&#x3D; container.NewProcess(tty, cmdArray)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil&#123;logrus.Error(err)&#125;initProcess.Wait()os.Exit(-1)&#125;</code></pre><p>但其实这个函数做的也只是去跑一个 initProcess。这个 command process在另一个包里定义。</p><h5 id="newprocess-4">3.2.3 NewProcess</h5><p>上面提到的 <code>container.NewProcess</code> 在<code>container/init.go</code> 里定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; container&#x2F;init.gofunc NewProcess(tty bool, cmdArray string) *exec.Cmd &#123;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is the below exported function&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;args :&#x3D; []string&#123;&quot;init&quot;, cmdArray&#125;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, args...)&#x2F;&#x2F; new namespaces, thanks to Linuxcmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; this is what presudo terminal means&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;return cmd&#125;</code></pre><p>这个函数的作用是生成一个新的 command process，但这个 command 是<code>/proc/self/exe</code>这个程序本身，也就是，我们最后生成的可执行文件，但这次我们不运行<code>docker run</code>，而是 <code>docker init</code>，这个 init命令在下面定义。</p><h5 id="init-4">3.2.4 init</h5><p>initCommand 和 runCommand 在同一个文件里定义，也是一个command，但是注意这个 command 不面向用户，只用于协助 runCommand。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; command.go&#x2F;&#x2F; docker init, but cannot be used by uservar initCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;Start initiating...&quot;)cmdArray :&#x3D; context.Args().Get(0)logrus.Infof(&quot;container command: %v&quot;, cmdArray)return container.InitProcess(cmdArray, nil)&#125;,&#125;</code></pre><p>这里使用了 container.InitProcess函数，这个函数是真正用于容器初始化的函数。</p><h5 id="initprocess-4">3.2.5 InitProcess</h5><p>这里的是 InitProcess，也就是容器初始化的步骤。</p><p>注意 syscall.Exec 这里：</p><ul><li>就是 <code>mount /</code> 并指定 private，不然容器里的 proc会使用外面的 proc，即使在不同 namespace 下。</li><li>所以如果没有加这一段，其实退出容器后还需要在外面再次 mount proc才能使用 ps 等命令</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initiate the containerfunc InitProcess(cmdArray string, args []string) error &#123;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV                &#x2F;&#x2F; mountif err :&#x3D; syscall.Mount(&quot;&quot;, &quot;&#x2F;&quot;, &quot;&quot;, syscall.MS_PRIVATE|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F; fails: %v&quot;, err)return err&#125;        &#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)argv :&#x3D; []string&#123;cmdArray&#125;if err :&#x3D; syscall.Exec(cmdArray, argv, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc fails: %v&quot;, err)&#125;return nil&#125;</code></pre><p>一般来说，我们都是想要这个 cmdArray 作为 PID=1 的进程。but，我们有initProcess 本身的存在，所以 PID = 1 的其实是 initProcess，那如何让cmdArray 作为 PID=1 的存在呢？</p><p>这里有一个 syscall.Exec 神器，Exec 内部会调用 kernel 的 execve函数，这个函数会把当前进程上运行的程序替换为另一个程序，这正是我们想要的，在不改变PID 的情况下，替换程序 (即使 kill PID 为 1 的进程，新创建的进程也会是PID=2)。</p><p>为什么要第一个命令的 PID 为 1？</p><ul><li>因为这样，退出这个进程后，容器就会因为没有前台进程，而自动退出，这也是docker 的特性。</li></ul><h3 id="给-docker-run-增加对容器的资源限制功能-4">4. 给 docker run增加对容器的资源限制功能</h3><p>这里要用到 subsystem 的知识。</p><h4 id="subsystem.go-4">4.1 subsystem.go</h4><ul><li>根据 subsystem 的特性，和接口很搭。</li><li>此外再定义一个 ResourceConfig 的类型，用于放置资源控制的配置。</li><li>subsystemInstance 里包括 3 个 subsystem，分别对memory，cpu，cpushare进行限制。因为我们只需要对整个容器进行限制，所以这一套 3 个够了。</li></ul><p>看到这里，有个 cpu，cpushare，cpuset 等等，有点晕，查了下，有关 CPU的 cgroup subsystem，这里列举常见的 3 个：</p><ul><li>cpu：经常看到的 cpushares 在其麾下，share 即相对权重的 cpu调度，用来限制 cgroup 的 cpu 的使用率</li><li>cpuacct：统计 cgroup 的 cpu 使用率</li><li>cpuset：在多核机器上设置 cgroups 可使用的 cpu 核心数和内存</li></ul><p>通常前两者可以合体</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package subsystemstype ResourceConfig struct &#123;MemoryLimit stringCPUShare stringCPUSet string&#125;type Subsystem interface &#123;&#x2F;&#x2F; return the name of which type of subsystemName() string&#x2F;&#x2F; set a resource limit on a cgroupSet(cgroupPath string, res *ResourceConfig) error&#x2F;&#x2F; add a processs with the pid to a groupAddProcess(cgroupPath string, pid int) error&#x2F;&#x2F; remove a cgroupRemoveCgroup(cgroupPath string) error&#125;&#x2F;&#x2F; instance of a subsystemsvar SubsystemsInstance &#x3D; []Subsystem&#123;&amp;CPU&#123;&#125;,&amp;CPUSet&#123;&#125;,&amp;Memory&#123;&#125;,&#125;</code></pre><h4 id="memorysubsystem-4">4.2 MemorySubsystem</h4><h5 id="name-4">4.2.1 Name()</h5><p>很简单，返回 “memory” 字符串，表示这个 subsystem 是memorySubsystem。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *MemorySubsystem) Name() string &#123;    return &quot;memory&quot;&#125;</code></pre><h5 id="set-4">4.2.2 Set()</h5><p>Set() 用于对 cgroup 设置资源限制，因此参数为 cgroup 的 path 和resourceConfig。</p><ol type="1"><li>其中 <code>GetCgroupPath</code> 后面会提及，作用是获取这个 subsystem所挂载的 hierarchy 上的虚拟文件系统下的 从group 路径。</li><li>获取到 cgroupPath 在虚拟文件系统中的位置后，只需要写入"memory.limit_in_bytes" 文件中即可。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; set the memory limit to this cgroup with cgroupPathfunc (ms *Memory) Set(cgroupPath string, res *ResourceConfig) error  &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;memory.limit_in_bytes&quot;), []byte(res.MemoryLimit), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup memory fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="addprocess-4">4.2.3 AddProcess()</h5><ol type="1"><li>和上面基本一样，只不过是写到 tasks 里。</li><li>pid 变成 byte slice 之前要用 Itoa 转化一下。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add process fail: %v&quot;, err)&#125;&#125;return nil&#125;</code></pre><h5 id="removecgroup-4">4.2.4 RemoveCgroup()</h5><ol type="1"><li>使用 <code>os.Remove</code> 可以移除参数所指定的文件或文件夹。</li><li>这里移除整个 cgroup 文件夹，就等于是删除 cgroup 了。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ms *Memory) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(ms.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusubsystem-4">4.3 CPUSubsystem</h4><p>这里的设计和上面没什么区别，直接贴参考代码</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpu.gofunc (c *CPU) Name() string &#123;return &quot;CPUShare&quot;&#125;func (c *CPU) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpu.shares&quot;), []byte(res.CPUShare), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cpu share limit failed: %s&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpu process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPU) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(subsystemCgroupPath)&#125;&#125;</code></pre><h4 id="cpusetsubsystem-4">4.4 CPUSetSubsystem</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; cpuset.gofunc (c *CPUSet) Name() string &#123;return &quot;CPUSet&quot;&#125;func (c *CPUSet) Set(cgroupPath string, res *ResourceConfig) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, true); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;cpuset.cpus&quot;), []byte(res.CPUSet), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;set cgroup cpuset failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) AddProcess(cgroupPath string, pid int) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;if err :&#x3D; ioutil.WriteFile(path.Join(subsystemCgroupPath, &quot;tasks&quot;), []byte(strconv.Itoa(pid)), 0644); err !&#x3D; nil &#123;return fmt.Errorf(&quot;cgroup add cpuset process failed: %v&quot;, err)&#125;&#125;return nil&#125;func (c *CPUSet) RemoveCgroup(cgroupPath string) error &#123;if subsystemCgroupPath, err :&#x3D; GetCgroupPath(c.Name(), cgroupPath, false); err !&#x3D; nil &#123;return err&#125; else &#123;return os.Remove(path.Join(subsystemCgroupPath))&#125;&#125;</code></pre><h4 id="getcgrouppath-4">4.5 GetCgroupPath()</h4><p><code>GetCgroupPath()</code> 用于获取某个 subsystem 所挂载的hierarchy 上的虚拟文件系统 (挂载后的文件夹) 下的 cgroup的路径。通过对这个目录的改写来改动 cgroup。</p><p>首先我们抛开 cgroup，在此之前我们要知道 这个 hierarchy 的 cgroup根节点的路径。那可以在 <code>/proc/self/mountinfo</code> 中获取。</p><p>下面是一些实现细节：</p><ol type="1"><li>首先定义一个 <code>FindCgroupMountpoint()</code> 来找到 cgroup的根节点。</li><li>然后在 <code>GetCgroupPath</code> 将其和 cgroup的相对路径拼接从而获取 cgroup 的路径。如果 <code>autoCreate</code> 为true 且该路径不存在，那么就新建一个 cgroup。(在 hierarchy 环境下，mkdir其实会隐式地创建一个 cgroup，其中包括很多配置文件)</li></ol><blockquote><p><a href="#1.3.4 cgroup 的 kernel 接口">点击这里回顾</a></p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; as the function name shows, find the root path of hierarchyfunc FindCgroupMountpoint(subsystemName string) string  &#123;f, err :&#x3D; os.Open(&quot;&#x2F;proc&#x2F;self&#x2F;mountinfo&quot;)    &#x2F;&#x2F; get info about mount relate to current processif err !&#x3D; nil &#123;return &quot;&quot;&#125;defer f.Close()scanner :&#x3D; bufio.NewScanner(f)for scanner.Scan() &#123;txt :&#x3D; scanner.Text()fields :&#x3D; strings.Split(txt, &quot; &quot;)&#x2F;&#x2F; find whether &quot;subsystemName&quot; appear in the last field&#x2F;&#x2F; if so, then the fifth field is the pathfor _, opt :&#x3D; range strings.Split(fields[len(fields)-1], &quot;,&quot;) &#123;if opt &#x3D;&#x3D; subsystemName &#123;return fields[4]&#125;&#125;&#125;return &quot;&quot;&#125;&#x2F;&#x2F; get the absolute path of a cgroupfunc GetCgroupPath(subsystemName string, cgroupPath string, autoCreate bool) (string, error)  &#123;cgroupRootPath :&#x3D; FindCgroupMountpoint(subsystemName)expectedPath :&#x3D; path.Join(cgroupRootPath, cgroupPath)&#x2F;&#x2F; find the cgroup or create a new cgroupif _, err :&#x3D; os.Stat(expectedPath); err &#x3D;&#x3D; nil  || (autoCreate &amp;&amp; os.IsNotExist(err)) &#123;if os.IsNotExist(err) &#123;if err :&#x3D; os.Mkdir(expectedPath, 0755); err !&#x3D; nil &#123;return &quot;&quot;, fmt.Errorf(&quot;error when create cgroup: %v&quot;, err)&#125;&#125;return expectedPath, nil&#125; else &#123;return &quot;&quot;, fmt.Errorf(&quot;cgroup path error: %v&quot;, err)&#125;&#125;</code></pre><h4 id="cgroupsmanager.go-4">4.6 cgroupsManager.go</h4><ol type="1"><li>定义 CgroupManager 类型，其中的 path 要注意是相对路径，相对于hierarchy 的 root path。所以一个 CgroupManager 是有可能表示多个 cgroups的，或准确说，和对应的 hierarchy root path 的相对路径一样的多个cgroups。</li><li>因为上述原因，<code>Set()</code> 可能会创建多个 cgroups，如果subsystems 们在不同的 hierarchy 就会这样。</li><li>这也是为什么 <code>AddProcess()</code> 和 <code>Remove()</code>要在每个 subsystem 上执行一遍。因为这些 subsystem 可能存在于不同的hierarchies。</li><li>注意 <code>Set()</code> 和 <code>AddProcess()</code>都不是返回错误，而是发出警告，然后返回nil。因为有些时候用户只指定某一个限制，例如 memory，那样的话修改 cpu等其实会报错 (正常的报错)，因此我们不 return err 来退出。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">package cgroupsimport &quot;simple-docker&#x2F;subsystem&quot;type CgroupManager struct &#123;Path     string &#x2F;&#x2F; relative path, relative to the root path of the hierarchy&#x2F;&#x2F; so this may cause more than one cgroup in different hierarchiesResource *subsystems.ResourceConfig&#125;func NewCgroupManager(path string) *CgroupManager &#123;return &amp;CgroupManager&#123;Path: path,&#125;&#125;&#x2F;&#x2F; set the three resource config subsystems to the cgroup(will create if the cgroup path is not existed)&#x2F;&#x2F; this may generate more than one cgroup, because those subsystem may appear in different hierarchiesfunc (cm CgroupManager) Set(res *subsystems.ResourceConfig) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.Set(cm.Path, res); err !&#x3D; nil &#123;logrus.Warnf(&quot;set resource fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; add process to the cgroup path&#x2F;&#x2F; why should we iterate all the subsystems? we have only one cgroup&#x2F;&#x2F; because those subsystems may appear at different hierarchies, which will then cause more than one cgroup, 1-3 in this case.func (cm *CgroupManager) AddProcess(pid int) error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err :&#x3D; subsystem.AddProcess(cm.Path, pid); err !&#x3D; nil &#123;logrus.Warn(&quot;app process fail: %v&quot;, err)&#125;&#125;return nil&#125;&#x2F;&#x2F; delete the cgroup(s)func (cm *CgroupManager) Remove() error &#123;for _, subsystem :&#x3D; range subsystems.SubsystemsInstance &#123;if err:&#x3D; subsystem.RemoveCgroup(cm.Path); err !&#x3D; nil &#123;return err&#125;&#125;return nil&#125;</code></pre><h4 id="管道处理多个容器参数-4">4.7 管道处理多个容器参数</h4><p>限制容器运行的命令不再像是 <code>/bin/sh</code>这种单个参数，而是多个参数，因此需要使用管道来对多个参数进行处理。那么需要修改以下文件：</p><h5 id="containerinit.go-4">4.7.1 container/init.go</h5><ol type="1"><li>管道原理和 channel 很像，read 端和 write端会在另一边没响应时堵塞。</li><li>使用 <code>os.Pipe()</code> 获取管道。返回的 readPipe 和 writePipe都是 <code>*os.File</code> 类型。</li><li>如何把管道传给子进程 (也就是容器进程) 变成了一个难题，这里用到了<code>ExtraFile</code> 这个参数来解决。cmd会带着参数里的文件来创建新的进程。(这里除了 ExtraFile，还会有类似StandardFile，也就是 stdin，stdout，stderr)</li><li>这里把 read 端传给容器进程，然后 write 端保留在父进程上。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;new pipe error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itselfcmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)&#x2F;&#x2F; new namespacescmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,&#125;&#x2F;&#x2F; link the container&#39;s stdio to osif tty &#123;cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;return cmd, writePipe&#125;</code></pre><p>除了 <code>NewProcess()</code>，<code>InitProcess()</code>也要改变下。</p><ol type="1"><li>使用 readCommand 来读取 pipe。</li><li>实际运行中，当进程运行到 <code>readCommand()</code> 时会堵塞，直到write 端传数据进来。</li><li>因此不用担心我们在容器运行后再传输参数。因为再读取完参数之前，<code>InitProcess()</code>也不会运行到 <code>syscall.Exec()</code> 这一步。</li><li>这里添加了 lookPath，这个是用于解决每次我们都要输入<code>/bin/ls</code>的麻烦，这个函数会帮我们找到参数命令的绝对路径。也就是说，只要输入 ls即可，lookPath 会自动找到 <code>/bin/ls</code>。然后我们再把这个 path作为 <code>argv()</code> 传给 <code>syscall.Exec</code></li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; already in container&#x2F;&#x2F; initialize the containerfunc InitProcess() error &#123;cmdArray :&#x3D; readCommand()if len(cmdArray) &#x3D;&#x3D; 0 &#123;return fmt.Errorf(&quot;init process fails, cmdArray is nil&quot;)&#125;defaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV&#x2F;&#x2F; mount proc filesystemsyscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;)path, err :&#x3D; exec.LookPath(cmdArray[0])if err !&#x3D; nil &#123;logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)return err&#125;&#x2F;&#x2F; log path infologrus.Infof(&quot;find path: %v&quot;, path)if err :&#x3D; syscall.Exec(path, cmdArray, os.Environ()); err !&#x3D; nil &#123;logrus.Errorf(err.Error())&#125;return nil&#125;func readCommand() []string &#123;pipe :&#x3D; os.NewFile(uintptr(3), &quot;pipe&quot;)msg, err :&#x3D; ioutil.ReadAll(pipe)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read pipe failed: %v&quot;, err)return nil&#125;return strings.Split(string(msg), &quot; &quot;)&#125;</code></pre><h5 id="dockercommandrun.go-4">4.7.2 dockerCommand/run.go</h5><ol type="1"><li>在 run.go 向 writePipe 写入参数，这样容器就会获取到参数。</li><li>关闭 pipe，使得 init 进程继续进行。</li></ol><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig) &#123;initProcess, writePipe :&#x3D; container.NewProcess(tty)&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write sidesendInitCommand(cmdArray, writePipe)initProcess.Wait()os.Exit(-1)&#125;func sendInitCommand(cmdArray []string, writePipe *os.File) &#123;cmdString :&#x3D; strings.Join(cmdArray, &quot; &quot;)logrus.Infof(&quot;whole init command is: %v&quot;, cmdString)writePipe.WriteString(cmdString)writePipe.Close()&#125;</code></pre><h5 id="command.go-4">4.7.3 command.go</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open a interactive tty(pre sudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;,&amp;cli.StringFlag&#123;Name: &quot;cpushare&quot;,Usage:&quot;limit the cpu share&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &#x3D;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;cmdArray :&#x3D; make([]string,len(args)) &#x2F;&#x2F; commandcopy(cmdArray,args)&#x2F;&#x2F; checkout whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; pre sudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig &#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare: context.String(&quot;cpushare&quot;),CPUSet: context.String(&quot;cpu&quot;),&#125;dockerCommand.Run(tty, cmdArray, &amp;resourceConfig)return nil&#125;,&#125;&#x2F;&#x2F; docker init, but cannot be used by uservar InitCommand &#x3D; cli.Command&#123;Name:  &quot;init&quot;,Usage: &quot;init a container&quot;,Action: func(context *cli.Context) error &#123;logrus.Infof(&quot;start initializing...&quot;)return container.InitProcess()&#125;,&#125;</code></pre><h5 id="main.go-4">4.7.4 main.go</h5><p>除了上面的修改，我们还要定义一个程序的入口：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;&quot;github.com&#x2F;urfave&#x2F;cli&quot;)const usage &#x3D; &#96;Usage&#96;func main() &#123;app :&#x3D; cli.NewApp()app.Name &#x3D; &quot;simple-docker&quot;app.Usage &#x3D; usageapp.Commands &#x3D; []cli.Command&#123;RunCommand,InitCommand,&#125;app.Before &#x3D; func(context *cli.Context) error &#123;logrus.SetFormatter(&amp;logrus.JSONFormatter&#123;&#125;)logrus.SetOutput(os.Stdout)return nil&#125;if err :&#x3D; app.Run(os.Args); err !&#x3D; nil &#123;logrus.Fatal(err)&#125;&#125;</code></pre><h4 id="运行-demo-4">4.8 运行 demo</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it stress -m 100m --vm-bytes 200m --vm-keep -m 1</code></pre><p>效果如下：</p><p><img src="0x0035/demo_1.png" /></p><p>不过这个运行方式不能进行交互，我们可以使用这个命令来验证我们写的docker 是否与宿主机隔离：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go run . run -it &#x2F;bin&#x2F;sh</code></pre><p><img src="0x0035/demo_sh.png" /></p><p>可以看到，pid，ipc，network 方面都与宿主机进行了隔离。</p><h2 id="三镜像篇-4">三、镜像篇</h2><h3 id="构造镜像-4">5. 构造镜像</h3><h4 id="编译-aufs-内核-4">5.1 编译 aufs 内核</h4><p>因为电脑硬盘空间不太够，就不使用虚拟机来做实验了，笔者这里使用 WSL2来完成后续工作，然而，WSL2 Kernel 没有把 aufs编译进去，那只能换内核了，查阅资料，有两种更换内核的方法：</p><ul><li><p>直接替换 <code>C:\System32\lxss\tools\kernel</code> 文件</p></li><li><p>在 users 目录下新建 <code>.wslconfig</code> 文件：</p><pre class="line-numbers language-none"><code class="language-none">[wsl2]kernel&#x3D;&quot;要替换kernel的路径&quot;</code></pre></li></ul><p>很明显，我是不会满足于使用别人编译好的内核的，那我也来动手做一个。</p><h5 id="准备代码库-4">5.1.1 准备代码库</h5><p>我们先在 WSL 上准备好相关软件包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">apt update #更新源apt install build-essential flex bison libssl-dev libelf-dev gcc make</code></pre><p>编译内核需要从 GitHub 上 clone 微软官方的 WSL 代码和 AUFS-Standalone的代码库</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">git clone https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;WSL2-Linux-Kernel kernelgit clone https:&#x2F;&#x2F;github.com&#x2F;sfjro&#x2F;aufs-standalone aufs5</code></pre><p>然后查看 WSL 内核版本：在 wsl 下运行命令 <code>uname -r</code></p><p>例如我的内核版本是 5.15.19，那 kernel 和 aufs 都要切换到相应的分支去(kernel 默认就是 5.15.19，故不用切换)</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd aufs5git checkout aufs5.15.36</code></pre><p>然后退回到 kernel 文件夹给代码打补丁：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cat ..&#x2F;aufs5&#x2F;aufs5-mmap.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-base.patch | patch -p1cat ..&#x2F;aufs5&#x2F;aufs5-kbuild.patch | patch -p1</code></pre><p>三个 Patch 的顺序无关。</p><p>然后再复制一点配置文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cp ..&#x2F;aufs5&#x2F;Documentation . -rcp ..&#x2F;aufs5&#x2F;fs&#x2F; . -rcp ..&#x2F;aufs5&#x2F;include&#x2F;uapi&#x2F;linux&#x2F;aufs_type.h .&#x2F;include&#x2F;uapi&#x2F;linux</code></pre><p>接下来我们来修改一下编译配置，在 <code>Microsoft/config-wsl</code>中任意位置增加一行：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini">CONFIG_AUFS_FS&#x3D;y</code></pre><p>最后，就可以开始编译了！</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">make KCONFIG_CONFIG&#x3D;Microsoft&#x2F;config-wsl -j8</code></pre><p>过程中会问你一些问题，我除了 AUFS Debug 都选了 y。</p><p>最后会在当前目录生成 <code>vmlinuz</code>，在<code>arch/x86/boot</code> 下生成 <code>bzImage</code>。</p><p>关闭 WSL 后更换内核，重启 WSL 输入<code>grep aufs /proc/filesystems</code>验证结果，如果出现 aufs的字样，说明操作成功。</p><h4 id="使用-busybox-创建容器-4">5.2 使用 busybox 创建容器</h4><h5 id="busybox-4">5.2.1 busybox</h5><p>先在 docker 获取 busybox 镜像并打包成一个 tar 包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">docker pull busyboxdocker run -d busybox top -bdocker export -o busybox.tar &lt;container_id&gt;</code></pre><p>将其复制到 WSL 下并解压。</p><h5 id="pivot_root-4">5.2.2 pivot_root</h5><p>pivot_root 是一个系统调用，作用是改变当前 root 文件系统。pivot_root可以将当前进程的 root 文件系统移动到 put_old 文件夹，然后使 new_root成为新的 root 文件系统。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func pivotRoot(root string) error &#123;&#x2F;&#x2F; remount the root dir, in order to make current root and old root in different file systemsif err :&#x3D; syscall.Mount(root, root, &quot;bind&quot;, syscall.MS_BIND|syscall.MS_REC, &quot;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;mount rootfs to itself error: %v&quot;, err)&#125;&#x2F;&#x2F; create &#39;rootfs&#x2F;.pivot_root&#39; to store old_rootpivotDir :&#x3D; filepath.Join(root, &quot;.pivot_root&quot;)if err :&#x3D; os.Mkdir(pivotDir, 0777); err !&#x3D; nil &#123;return err&#125;&#x2F;&#x2F; pivot_root mount on new rootfs, old_root mount on rootfs&#x2F;.pivot_rootif err :&#x3D; syscall.PivotRoot(root, pivotDir); err !&#x3D; nil &#123;return fmt.Errorf(&quot;pivot_root %v&quot;, err)&#125;&#x2F;&#x2F; change current work dir to root dirif err :&#x3D; syscall.Chdir(&quot;&#x2F;&quot;); err !&#x3D; nil &#123;return fmt.Errorf(&quot;chdir &#x2F; %v&quot;, err)&#125;pivotDir &#x3D; filepath.Join(&quot;&#x2F;&quot;, &quot;.pivot_root&quot;)&#x2F;&#x2F; umount rootfs&#x2F;.rootfs_rootif err :&#x3D; syscall.Unmount(pivotDir, syscall.MNT_DETACH); err !&#x3D; nil &#123;return fmt.Errorf(&quot;umount pivot_root dir %v&quot;, err)&#125;&#x2F;&#x2F; del the temporary dirreturn os.Remove(pivotDir)&#125;</code></pre><p>有了这个函数就可以在 init 容器进程时，进行一系列的 mount 操作：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setUpMount() error &#123;&#x2F;&#x2F; get current pathpwd, err :&#x3D; os.Getwd()if err !&#x3D; nil &#123;logrus.Errorf(&quot;get current location error: %v&quot;, err)return err&#125;logrus.Infof(&quot;current location: %v&quot;, pwd)pivotRoot(pwd)&#x2F;&#x2F; mount procdefaultMountFlags :&#x3D; syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEVif err :&#x3D; syscall.Mount(&quot;proc&quot;, &quot;&#x2F;proc&quot;, &quot;proc&quot;, uintptr(defaultMountFlags), &quot;&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;proc failed: %v&quot;, err)return err&#125;if err :&#x3D; syscall.Mount(&quot;tmpfs&quot;, &quot;&#x2F;dev&quot;, &quot;tmpfs&quot;, syscall.MS_NOSUID|syscall.MS_STRICTATIME, &quot;mode&#x3D;755&quot;); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount &#x2F;dev failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>tmpfs 是一种基于内存的文件系统，用 RAM 或 swap 分区来存储。</p><p>在 <code>NewParentProcess()</code> 中加一句<code>cmd.Dir="/root/busybox"</code>。</p><p>写完上述函数，然后在 <code>initProcess()</code> 中调用一下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">if err :&#x3D; setUpMount(); err !&#x3D; nil &#123;    logrus.Errorf(&quot;initProcess look path failed: %v&quot;, err)&#125;</code></pre><p>然后来运行测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it sh###### dividing live&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;busybox&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T11:27:04+08:00&quot;&#125;&#x2F; #</code></pre><p>可以看到，容器当前目录被虚拟定位到了根目录，其实是在宿主机上映射的<code>/root/busybox</code>。</p><h5 id="用-aufs-包装-busybox-4">5.2.3 用 AUFS 包装 busybox</h5><p>前面提到了，docker 使用 AUFS 存储镜像和容器。docker在使用镜像启动一个容器时，会新建 2 个 layer：write layer 和container-init-layer。write layer是容器唯一的可读写层，container-init-layer是为容器新建的只读层，用来存储容器启动时传入的系统信息。</p><ul><li><code>CreateReadOnlyLayer()</code> 新建 <code>busybox</code>文件夹，解压 <code>busybox.tar</code> 到 <code>busybox</code>目录下，作为容器只读层。</li><li><code>CreateWriteLayer()</code> 新建一个 <code>writeLayer</code>文件夹，作为容器唯一可写层。</li><li><code>CreateMountPoint()</code> 先创建了 <code>mnt</code>文件夹作为挂载点，再把 <code>writeLayer</code> 目录和<code>busybox</code> 目录 mount 到 <code>mnt</code> 目录下。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; extra tar to &#39;busybox&#39;, used as the read only layer for containerfunc CreateReadOnlyLayer(rootURL string) &#123;busyboxURL :&#x3D; rootURL + &quot;busybox&#x2F;&quot;busyboxTarURL :&#x3D; rootURL + &quot;busybox.tar&quot;exist, err :&#x3D; PathExists(busyboxURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, busyboxURL, err)&#125;if !exist &#123;if err :&#x3D; os.Mkdir(busyboxURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, busyboxURL, err)&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, busyboxTarURL, &quot;-C&quot;, busyboxURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, busyboxTarURL, err)&#125;&#125;&#125;&#x2F;&#x2F; create a unique folder as writeLayerfunc CreateWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.Mkdir(writeURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, writeURL, err)&#125;&#125;func CreateMountPoint(rootURL string, mntURL string) &#123;&#x2F;&#x2F; create mnt folder as mount pointif err :&#x3D; os.Mkdir(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;dirs :&#x3D; &quot;dirs&#x3D;&quot; + rootURL + &quot;writeLayer:&quot; + rootURL + &quot;busybox&quot;cmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;&#125;func NewWorkSpace(rootURL, mntURL string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)&#125;</code></pre><p>接下来在 <code>NewParentProcess()</code> 将容器使用的宿主机目录<code>/root/busybox</code> 替换为 <code>/root/mnt</code>，这样使用 AUFS系统启动容器的代码就完成了。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;NewWorkSpace(rootURL, mntURL)cmd.Dir &#x3D; mntURLreturn cmd, writePipe</code></pre><p>docker 会在删除容器时，把容器对应的 write layer 和container-init-layer 删除，而保留镜像中所有的内容。</p><ul><li><code>DeleteMountPoint()</code> 中 umount <code>mnt</code>目录。</li><li>删除 <code>mnt</code> 目录。</li><li>在 <code>DeleteWriteLayer()</code> 删除 <code>writeLayer</code>文件夹。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(rootURL string, mntURL string) &#123;cmd :&#x3D; exec.Command(rootURL, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)&#125;&#125;func DeleteWriteLayer(rootURL string) &#123;writeURL :&#x3D; rootURL + &quot;writeLayer&#x2F;&quot;if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;func DeleteWorkSpace(rootURL, mntURL string) &#123;DeleteMountPoint(rootURL, mntURL)DeleteWriteLayer(rootURL)&#125;</code></pre><p>现在来启动一个容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">root@Jayden: ~# go run . run -it shdirs&#x3D;&#x2F;root&#x2F;writeLayer:&#x2F;root&#x2F;busybox&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-04T15:16:43+08:00&quot;&#125;&#x2F; #</code></pre><p>测试在容器内创建文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # mkdir aaa&#x2F; # touch aaa&#x2F;test.txt</code></pre><p>此时我们可以在宿主机终端查看<code>/root/mnt/writeLayer</code>，可以看到刚才新建的 <code>aaa</code>文件夹和 <code>test.txt</code>，在我们退出容器后，<code>/root/mnt</code>文件夹被删除，伴随着刚才创建的文件夹和文件都被删除，而作为镜像的 busybox仍被保留，且内容未被修改。</p><h4 id="实现-volume-数据卷-4">5.3 实现 volume 数据卷</h4><p>上节实现了容器和镜像的分离，但是如果容器退出，容器可写层的所有内容就会被删除，这里使用volume 来实现容器数据持久化。</p><p>先在 <code>command.go</code> 里添加 <code>-v</code> 标签：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;,         &#x2F;&#x2F; add &#96;-v&#96; tag         &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminal&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;         &#x2F;&#x2F; send volume args to Run()volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig,volume)return nil&#125;,&#125;</code></pre><p>在 <code>Run()</code> 中，把 volume 传给创建容器的<code>NewParentProcess()</code> 和删除容器文件系统的<code>DeleteWorkSpace()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)initProcess.Wait()rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>在 <code>NewWorkSpace()</code> 中，继续把 volume传给创建容器文件系统的 <code>NewWorkSapce()</code>。</p><p>创建容器文件系统过程如下：</p><ul><li>创建只读层。</li><li>创建容器读写层。</li><li>创建挂载点并把只读层和读写层挂载到挂载点上。</li><li>判断 volume是否为空，如果是，说明用户没有使用挂载标签，结束创建过程。</li><li>不为空，就用 <code>volumeURLExtract()</code> 解析。</li><li>当 <code>volumeURLExtract()</code> 返回字符数组长度为2，且数据元素均不为空时，则执行 <code>MountVolume()</code>来挂载数据卷。<ul><li>否则提示用户创建数据卷输入值不对。</li></ul></li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(rootURL, mntURL, volume string) &#123;CreateReadOnlyLayer(rootURL)CreateWriteLayer(rootURL)CreateMountPoint(rootURL, mntURL)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(rootURL, mntURL, volumeURLs)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;func volumeUrlExtract(volume string) []string &#123;&#x2F;&#x2F; divide volume by &quot;:&quot;return strings.Split(volume, &quot;:&quot;)&#125;</code></pre><p>挂载数据卷过程如下：</p><ul><li>读取宿主机文件目录 URL，创建宿主机文件目录(<code>/root/$&#123;parentURL&#125;</code>)</li><li>读取容器挂载点 URL，在容器文件系统里创建挂载点(<code>/root/mnt/$&#123;containerURL&#125;</code>)</li><li>把宿主机文件目录挂载到容器挂载点，这样启动容器的过程，对数据卷的处理就完成了。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]containerVolumeURL :&#x3D; mntURL + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURLcmd :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)&#125;&#125;</code></pre><p>删除容器文件系统过程如下：</p><ul><li>在 volume 不为空，且使用 <code>volumeURLExtract()</code> 解析 volume字符串返回的字符数组长度为 2，数据元素均不为空时，才执行<code>DeleteMountPointWithVolume()</code> 来处理。</li><li>其余情况仍使用前面的 <code>DeleteMountPoint()</code>。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(rootURL, mntURL, volume string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;&#125; else &#123;DeleteMountPoint(rootURL, mntURL)&#125;DeleteWriteLayer(rootURL)&#125;</code></pre><p><code>DeleteMountPointWithVolume()</code> 处理逻辑如下：</p><ul><li>卸载 volume 挂载点的文件系统(<code>/root/mnt/$&#123;containerURL&#125;</code>)，保证整个容器挂载点没有再被使用。</li><li>卸载整个容器文件系统挂载点 (<code>/root/mnt</code>)。</li><li>删除容器文件系统挂载点。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(rootURL, mntURL string, volumeURLs []string) &#123;&#x2F;&#x2F; umount volume point in containercontainerURL :&#x3D; mntURL + volumeURLs[1]cmd :&#x3D; exec.Command(&quot;umount&quot;, containerURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)&#125;&#x2F;&#x2F; umount the whole point of the containercmd &#x3D; exec.Command(&quot;umount&quot;, mntURL)cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrif err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>接下来启动容器测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:25:43+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; #</code></pre><p>进入 <code>containerVolume</code>，创建一个文本文件，并随便写点东西：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;test&quot; &gt;&gt; test.txt</code></pre><p>此时我们能在宿主机的 <code>/root/volume</code>找到我们刚才创建的文本文件。退出容器后，volume文件夹也没有被删除。再次进入容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">r# go run . run -it -v &#x2F;root&#x2F;volume:&#x2F;containerVolume sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;mkdir parent dir &#x2F;root&#x2F;volume error. mkdir &#x2F;root&#x2F;volume: file exists&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;volume\&quot; \&quot;&#x2F;containerVolume\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;current location: &#x2F;root&#x2F;mnt&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-05T09:29:24+08:00&quot;&#125;&#x2F; # lsbin              dev              home             lib64            root             tmp              varcontainerVolume  etc              lib              proc             sys              usr&#x2F; # ls containerVolume&#x2F;test.txt</code></pre><p>此时这里会提示 volume 文件夹存在，我们在 <code>test.txt</code>内追加内容：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd containerVolumeecho -e &quot;###&quot; &gt;&gt; test.txt</code></pre><p>此时再次退出容器，能看到修改过后的文件内容，可以看到 volume文件夹没有被删除。</p><h4 id="简单镜像打包-4">5.4 简单镜像打包</h4><p>容器在退出时会删除所有可写层的内容，commit命令可以把运行状态容器的内容存储为镜像保存下来。</p><p>在 <code>main.go</code> 里添加 <code>commit</code> 命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    InitCommand,    RunCommand,    CommitCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里实现 <code>CommitCommand</code>命令：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;imageName :&#x3D; context.Args()[0]&#x2F;&#x2F; commitContainer(containerName)commitContainer(imageName)return nil&#125;,&#125;</code></pre><p>添加 <code>commit.go</code>，通过 <code>commitContainer()</code>实现将容器文件系统打包成 <code>$&#123;imagename&#125;.tar</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;os&#x2F;exec&quot;&quot;github.com&#x2F;sirupsen&#x2F;logrus&quot;)func commitContainer(imageName string) &#123;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&quot;imageTar :&#x3D; &quot;&#x2F;root&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>运行测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it sh</code></pre><p>然后在另一个终端运行：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit image</code></pre><p>这时候可以在 root 目录下看到多了一个 <code>image.tar</code>，解压后可以发现压缩包的内容和 <code>/root/mnt</code> 一致。</p><blockquote><p>tips：一定要先运行容器！如果不运行容器直接打包，会提示<code>/root/mnt</code> 不存在。</p></blockquote><h3 id="构建容器进阶-4">6. 构建容器进阶</h3><h4 id="实现容器后台运行-4">6.1 实现容器后台运行</h4><p>容器，放在操作系统层面，就是一个进程，当前运行命令的 simple-docker是主进程，容器是当前 simple-docker 进程 fork出来的子进程。子进程的结束和父进程的运行是一个异步的过程，即父进程不会知道子进程在什么时候结束。如果创建子进程时，父进程退出，那这个子进程就是孤儿进程(没人管)，此时进程号为 1 的进程 init 就会接受这些孤儿进程。</p><p>先在 <code>command.go</code> 添加 <code>-d</code>标签，表示这个容器启动时在后台运行：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach container         &#x2F;&#x2F; tty cannot work with detachif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume)return nil&#125;,&#125;</code></pre><p>然后也要修改一下 <code>run.go</code> 的 <code>Run()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)    &#x2F;&#x2F; if background process, parent process won&#39;t waitif tty &#123;initProcess.Wait()&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T15:32:44+08:00&quot;&#125;</code></pre><p>根据书上的提示，<code>ps -ef</code> 用来查找 top 进程：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ps -ef | grep toproot        3713     751  0 14:42 pts&#x2F;2    00:00:00 top</code></pre><p>前面几次运行命令，都找不到 top这个进程，于是我后面多跑了几次，终于看到了这个进程。。。</p><p>可以看到，top 命令的进程正在运行着，不过运行环境是 WSL，父进程 id不是 1，然后 <code>ps -ef</code> 查看一下，top 的父进程是一个 bash进程，而 bash 进程的父进程是一个 init 进程，这样应该算过了吧(偶尔的一两次不严谨)。</p><h4 id="实现查看运行中的容器-4">6.2 实现查看运行中的容器</h4><h5 id="name-标签-4">6.2.1 name 标签</h5><p>前面创建的容器里，所有关于容器的信息，例如PID、容器创建时间、容器运行命令等，都没有记录，这导致容器运行完后就在也不知道它的信息了，因此要把这部分信息保留。先在<code>command.go</code> 里加一个 name 标签，方便用户指定容器的名字：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name: &quot;d&quot;,Usage :&quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name: &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag &#123;Name: &quot;name&quot;,Usage: &quot;container name&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;) &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)dockerCommand.Run(tty, cmdArray, &amp;resourceConfig, volume, containerName)return nil&#125;,&#125;</code></pre><p>添加一个方法来记录容器的相关信息，这里用先用一个 10位的数字来表示容器的 id：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func randStringBytes(n int) string &#123;letterBytes :&#x3D; &quot;1234567890&quot;rand.Seed(time.Now().UnixNano())b :&#x3D; make([]byte, n)for i :&#x3D; range b &#123;b[i] &#x3D; letterBytes[rand.Intn(len(letterBytes))]&#125;return string(b)&#125;</code></pre><p>这里用时间戳为种子，每次生成一个 10 以内的数字作为 letterBytes数组的下标，最后拼成整个容器的 id。容器的信息默认保存在<code>/var/run/simple-docker/$&#123;containerName&#125;/config.json</code>，容器基本格式如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;Id          string &#96;json:&quot;id&quot;&#96;Name        string &#96;json:&quot;name&quot;&#96;Command     string &#96;json:&quot;command&quot;&#96; &#x2F;&#x2F; the command that init process executeCreatedTime string &#96;json:&quot;created_time&quot;&#96;Status      string &#96;json:&quot;status&quot;&#96;&#125;var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&quot;ConfigName          string &#x3D; &quot;config.json&quot;)</code></pre><p>下面是记录容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;) &#x2F;&#x2F; format must like thiscommand :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>这里格式化的时间必须是<code>2006-01-02 15:04:05</code>，不然格式化后的时间会是几千年后doge。</p><p>详细可以看这篇文章：<ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p>在主函数加上调用：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName string) &#123;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroup.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)&#125;rootURL :&#x3D; &quot;&#x2F;root&#x2F;&quot;mntURL :&#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;&quot;container.DeleteWorkSpace(rootURL, mntURL, volume)os.Exit(0)&#125;</code></pre><p>如果创建 tty 方式的容器，在容器退出后，就会删除相关信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func deleteContainerInfo(containerID string) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerID)if err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, dirURL, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top# go run . run -d --name jay top</code></pre><p>执行完成后，可以在 <code>/var/run/simple-docker/</code>找到两个文件夹，一个是随机 id，一个是 jay，文件夹下各有一个<code>config.json</code>，记录了容器的相关信息。</p><h5 id="实现-docker-ps-4">6.2.2 实现 docker ps</h5><p>在 <code>main.go</code> 加一个 <code>listCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,&#125;</code></pre><p>在 <code>command.go</code> 添加定义：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ListCommand &#x3D; cli.Command&#123;Name: &quot;ps&quot;,Usage: &quot;list all the containers&quot;,Action: func(context *cli.Context) error &#123;ListContainers()return nil&#125;,&#125;</code></pre><p>新建一个 <code>list.go</code>，实现记录列出容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListContainers() &#123;&#x2F;&#x2F; get the path that store the info of the containerdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, &quot;&quot;)dirURL &#x3D; dirURL[:len(dirURL)-1]&#x2F;&#x2F; read all the files in the directoryfiles, err :&#x3D; ioutil.ReadDir(dirURL)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read dir %s error %v&quot;, dirURL, err)return&#125;var containers []*container.ContainerInfofor _, file :&#x3D; range files &#123;tmpContainer, err :&#x3D; getContainerInfo(file)&#x2F;&#x2F; .Println(tmpContainer)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info error %v&quot;, err)continue&#125;containers &#x3D; append(containers, tmpContainer)&#125;&#x2F;&#x2F; use tabwriter.NewWriter to print the containerInfow :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprintf(w, &quot;ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n&quot;)for _, item :&#x3D; range containers &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\t%s\t%s\t%s\n&quot;,item.Id, item.Name, item.Pid, item.Status, item.Command, item.CreatedTime)&#125;&#x2F;&#x2F; refresh stdout if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;flush stdout error %v&quot;,err)return&#125;&#125;func getContainerInfo(file os.FileInfo) (*container.ContainerInfo, error) &#123;containerName :&#x3D; file.Name()&#x2F;&#x2F; create the absolute pathconfigFileDir :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFileDir &#x3D; configFileDir + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read config.jsoncontent, err :&#x3D; ioutil.ReadFile(configFileDir)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, configFileDir, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; turn json to containerInfoif err :&#x3D; json.Unmarshal(content, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>接上小节的测试，我们运行以下命令：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:11+08:00&quot;&#125;# go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-05T19:29:25+08:00&quot;&#125;# go run . psID           NAME         PID         STATUS      COMMAND     CREATED6675792962   6675792962   4317        running     top         2023-05-05 19:29:115553437308   jay          4404        running     top         2023-05-05 19:29:25</code></pre><p>现在就可以通过 ps 来看到所有创建的容器状态和它们的 init 进程 id了。</p><h4 id="查看容器日志-4">6.3 查看容器日志</h4><p>在 <code>main.go</code> 加一个 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">app.Commands &#x3D; []cli.Command&#123;    RunCommand,    InitCommand,    CommitCommand,    ListCommand,    LogCommand,&#125;</code></pre><p>然后在 <code>command.go</code> 里添加 <code>logCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var LogCommand &#x3D; cli.Command&#123;Name:  &quot;logs&quot;,Usage: &quot;print logs of a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;contianerName :&#x3D; context.Args()[0]logContainer(contianerName)return nil&#125;,&#125;</code></pre><p>新建一个 <code>log.go</code>，定义 <code>logContainer()</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func logContainer(containerName string) &#123;&#x2F;&#x2F; get the log pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)logFileLocation :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ContainerLogFile&#x2F;&#x2F; open log filefile, err :&#x3D; os.Open(logFileLocation)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container open file %s error: %v&quot;, logFileLocation, err)return&#125;defer file.Close()&#x2F;&#x2F; read log file contentcontent, err :&#x3D; ioutil.ReadAll(file)if err !&#x3D; nil &#123;logrus.Errorf(&quot;log container read file %s error: %v&quot;, logFileLocation, err)return&#125;&#x2F;&#x2F; use Fprint to transfer content to stdoutfmt.Fprint(os.Stdout, string(content))&#125;</code></pre><p>测试一下，先用 detach 方式创建一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name jay top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-06T14:26:32+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED1837062451   jay         2065        running     top         2023-05-06 14:26:32# go run . logs jayMem: 3265116K used, 4568420K free, 3256K shrd, 71432K buff, 1135692K cachedCPU:  0.3% usr  0.2% sys  0.0% nic 99.3% idle  0.0% io  0.0% irq  0.0% sirqLoad average: 0.03 0.09 0.08 1&#x2F;521 5PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND</code></pre><p>可以看到，logs命令成功运行并输出容器的日志。(这里之前出现过前几次创建容器，而后台却没运行的情况，导致一开始运行logs 时报错了，建议在运行 logs 前多检查下 top 是否后台运行中)</p><h4 id="进入容器-namespace-4">6.4 进入容器 Namespace</h4><p>在 6.3小节里，实现了查看后台运行的容器的日志，但是容器一旦创建后，就无法再次进入容器，这一次来实现进入容器内部的功能，也就是exec。</p><h5 id="setns-4">6.4.1 setns</h5><p>setns 是一个系统调用，可根据提供的 PID 再次进入到指定的Namespace。它要先打开 <code>/proc/$&#123;pid&#125;/ns</code>文件夹下对应的文件，然后使当前进程进入到指定的 Namespace 中。对于 go来说，一个有多线程的进程使无法使用 setns 调用进入到对应的命名空间的，go没启动一个程序就会进入多线程状态，因此无法简单在 go里直接调用系统调用，这里还需要借助 C 来实现这个功能。</p><h5 id="cgo-4">6.4.2 Cgo</h5><p>在 go 里写 C：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package rand&#x2F;*#include &lt;stdlib.h&gt;*&#x2F;import &quot;C&quot;func Random() int &#123;    return int(C.random())&#125;func Seed(i int) &#123;    C.srandom(C.uint(i))&#125;</code></pre><h5 id="实现-4">6.4.3 实现</h5><p>先使用 C 根据 PID进入对应 Namespace：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenter&#x2F;*#define _GNU_SOURCE#include &lt;errno.h&gt;#include &lt;sched.h&gt;#include &lt;stdio.h&gt;#include &lt;stdlib.h&gt;#include &lt;string.h&gt;#include &lt;fcntl.h&gt;#include &lt;unistd.h&gt;&#x2F;&#x2F; if this package is quoted, this function will run automatic__attribute__((constructor)) void enter_namespace(void)&#123;    char *simple_docker_pid;    &#x2F;&#x2F; get pid from system environment    simple_docker_pid &#x3D; getenv(&quot;simple_docker_pid&quot;);    if (simple_docker_pid)    &#123;        fprintf(stdout, &quot;got simple docker pid&#x3D;%s\n&quot;, simple_docker_pid);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker pid env skip nsenter&quot;);        &#x2F;&#x2F; if no specified pid, the func will exit        return;    &#125;    char *simple_docker_cmd;    simple_docker_cmd &#x3D; getenv(&quot;simple_docker_cmd&quot;);    if (simple_docker_cmd)    &#123;        fprintf(stdout, &quot;got simple docker cmd&#x3D;%s\n&quot;, simple_docker_cmd);    &#125;    else    &#123;        fprintf(stdout, &quot;missing simple docker cmd env skip nsenter&quot;);        &#x2F;&#x2F; if no specified cmd, the func will exit        return;    &#125;    int i;    char nspath[1024];    char *namespace[] &#x3D; &#123;&quot;ipc&quot;, &quot;uts&quot;, &quot;net&quot;, &quot;pid&quot;, &quot;mnt&quot;&#125;;    for (i &#x3D; 0; i &lt; 5; i++)    &#123;        &#x2F;&#x2F; create the target path, like &#x2F;proc&#x2F;pid&#x2F;ns&#x2F;ipc        sprintf(nspath, &quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;%s&quot;, simple_docker_pid, namespace[i]);        int fd &#x3D; open(nspath, O_RDONLY);printf(&quot;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; %d %s\n&quot;, fd, nspath);        &#x2F;&#x2F; call sentns and enter the target namespace        if (setns(fd, 0) &#x3D;&#x3D; -1)        &#123;            fprintf(stderr, &quot;setns on %s namespace failed: %s\n&quot;, namespace[i], strerror(errno));        &#125;        else        &#123;            fprintf(stdout, &quot;setns on %s namespace succeeded\n&quot;, namespace[i]);        &#125;        close(fd);    &#125;    &#x2F;&#x2F; run command in target namespace    int res &#x3D; system(simple_docker_cmd);    exit(0);    return;&#125;*&#x2F;import &quot;C&quot;</code></pre><p>那如何使用这段代码呢，只需要在要加载的地方引用这个 package即可，我这里是 <code>nenster</code> 。</p><p>其实也可以，单独放在一个 C 文件里，go 文件可以这样写：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package nsenterimport &quot;C&quot;</code></pre><p>下面增加 <code>ExecCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var ExecCommand &#x3D; cli.Command&#123;Name:  &quot;exec&quot;,Usage: &quot;exec a command into container&quot;,Action: func(context *cli.Context) error &#123;if os.Getenv(ENV_EXEC_PID) !&#x3D; &quot;&quot; &#123;logrus.Infof(&quot;pid callback pid %v&quot;, os.Getgid())return nil&#125;if len(context.Args()) &lt; 2 &#123;return fmt.Errorf(&quot;missing container name or command&quot;)&#125;containerName :&#x3D; context.Args()[0]cmdArray :&#x3D; make([]string, len(context.Args())-1)for i, v :&#x3D; range context.Args().Tail() &#123;cmdArray[i] &#x3D; v&#125;ExecContainer(containerName, cmdArray)return nil&#125;,&#125;</code></pre><p>新建一个 <code>exec.go</code>下面实现获取容器名和需要的命令，并且在这里引用<code>nsenter</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ENV_EXEC_PID &#x3D; &quot;simple_docker_pid&quot;const ENV_EXEC_CMD &#x3D; &quot;simple_docker_cmd&quot;func getContainerPidByName(containerName string) (string, error) &#123;&#x2F;&#x2F; get the path that store container infodirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; read files in target pathcontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;return &quot;&quot;, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to containerInfoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;return &quot;&quot;, err&#125;return containerInfo.Pid, nil&#125;func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run --name jay -d top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-07T13:23:09+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6530018751   jay         146639      running     top         2023-05-07 13:23:09# go run . logs jayMem: 4355160K used, 3478372K free, 3272K shrd, 208844K buff, 1581396K cachedCPU:  1.2% usr  0.6% sys  0.0% nic 97.9% idle  0.0% io  0.0% irq  0.1% sirqLoad average: 0.12 0.14 0.16 1&#x2F;574 6  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND# go run . exec jay sh&#x2F; # lsbin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top   13 root      0:00 sh   15 root      0:00 ps -ef&#x2F; #</code></pre><p>可以看到，成功进入容器内部，且与宿主机隔离。</p><p>这里出现了一个很奇怪的 bug，就是通过 cgo 去 setns，执行到 mnt时，抛出个错误：<code>Stale file handle</code>，当时找了全网，也找不到答案，于是陷入了两天的痛苦debug，在重新敲代码时，发现又不报错了，切换回那个有错误的分支，也不报错了。既然暂时找不到错误，先搁着吧，如果有看到这篇文章的朋友，也遇到了这个错误，可以留意下。(感觉会是一个雷)</p><p>(应该是容器的 mnt 没有 mount 上去，才会导致 stale file handle)</p><h4 id="停止容器-4">6.5 停止容器</h4><p>定义 <code>StopCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var StopCommand &#x3D; cli.Command&#123;Name:  &quot;stop&quot;,Usage: &quot;stop a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]stopContainer(containerName)return nil&#125;,&#125;</code></pre><p>然后声明一个函数，通过容器名来获取容器信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getContainerInfoByName(containerName string) (*container.ContainerInfo, error) &#123;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigNamecontentBytes, err :&#x3D; ioutil.ReadFile(configFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read config file %s error %v&quot;, configFilePath, err)return nil, err&#125;var containerInfo container.ContainerInfo&#x2F;&#x2F; unmarshal json to container infoif err :&#x3D; json.Unmarshal(contentBytes, &amp;containerInfo); err !&#x3D; nil &#123;logrus.Errorf(&quot;unmarshal json to container info error %v&quot;, err)return nil, err&#125;return &amp;containerInfo, nil&#125;</code></pre><p>然后是停止容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func stopContainer(containerName string) &#123;&#x2F;&#x2F; get pid by containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container pid by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; turn pid(string) to intpidInt, err :&#x3D; strconv.Atoi(pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;convert pid from string to int error %v&quot;, err)return&#125;&#x2F;&#x2F; kill container main processif err :&#x3D; syscall.Kill(pidInt, syscall.SIGTERM); err !&#x3D; nil &#123;logrus.Errorf(&quot;stop container %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; get info of the containercontainerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container info by name %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; process is killed, update process statuscontainerInfo.Status &#x3D; container.STOPcontainerInfo.Pid &#x3D; &quot; &quot;&#x2F;&#x2F; update info to jsonnweContentBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;json marshal %s error %v&quot;, containerName, err)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)configFilePath :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; overwrite containerInfoif err :&#x3D; ioutil.WriteFile(configFilePath, nweContentBytes, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;write config file %s error %v&quot;, configFilePath, err)&#125;&#125;</code></pre><p>测试：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop jay# go run . psID           NAME        PID         STATUS      COMMAND     CREATED6883605813   jay                     stopped     top# ps -ef | grep toproot       43588     761  0 20:00 pts&#x2F;0    00:00:00 grep --color&#x3D;auto top</code></pre><p>可以看到，jay 这个进程被停止了，且 pid 号设为空。</p><h4 id="删除容器-4">6.6 删除容器</h4><p>定义 <code>RemoveCommand</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RemoveCommand &#x3D; cli.Command&#123;Name:  &quot;rm&quot;,Usage: &quot;remove a container&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]removeContainer(containerName)return nil&#125;,&#125;</code></pre><p>实现删除容器：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . rm jay# go run . psID          NAME        PID         STATUS      COMMAND     CREATED</code></pre><p>可以看到，jay 这个容器被删除了。</p><h4 id="通过容器制作镜像-4">6.7 通过容器制作镜像</h4><p>这一节，根据书上的内容，有许多函数需要改动。建议这里对着作者给出的源码debug，书上有部分内容有明显错误。</p><p>之前的文件系统如下：</p><ul><li>只读层：busybox，只读，容器系统的基础</li><li>可写层：writeLayer，容器内部的可写层</li><li>挂载层：mnt，挂载外部的文件系统，类似虚拟机的文件共享</li></ul><p>修改后的文件系统如下：</p><ul><li>只读层：不变</li><li>可写层：再加容器名为目录进行隔离，也就是<code>writeLayer/$&#123;containerName&#125;</code></li><li>挂载层：再加容器名为目录进行隔离，也就是<code>mnt/$&#123;containerName&#125;</code></li></ul><p>因此，本节要实现为每个容器分配单独的隔离文件系统，以及实现对不同容器打包镜像。</p><p><strong>修改 <code>run.go</code></strong></p><p>在 Run 函数参数列表添加一个 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><p>同时也在 <code>command.go</code> 的 runCommand 里修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName)return nil&#125;,</code></pre><p>在 <code>recordContainerInfo</code> 函数的参数列表添加 volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func recordContainerInfo(containerPID int, commandArray []string, containerName, volume string) (string, error) &#123;&#x2F;&#x2F; create an ID that length is 10id :&#x3D; randStringBytes(10)createTime :&#x3D; time.Now().Format(&quot;2006-01-02 15:04:05&quot;)command :&#x3D; strings.Join(commandArray, &quot;&quot;)&#x2F;&#x2F; if containerName is nil, make containerID as nameif containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; id&#125;containerInfo :&#x3D; &amp;container.ContainerInfo&#123;Id:          id,Pid:         strconv.Itoa(containerPID),Command:     command,CreatedTime: createTime,Status:      container.RUNNING,Name:        containerName,Volume:      volume,&#125;&#x2F;&#x2F; trun containerInfo info stringjsonBytes, err :&#x3D; json.Marshal(containerInfo)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return &quot;&quot;, err&#125;jsonStr :&#x3D; string(jsonBytes)&#x2F;&#x2F; container pathdirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir error %s error: %v&quot;, dirURL, err)return &quot;&quot;, err&#125;fileName :&#x3D; dirURL + &quot;&#x2F;&quot; + container.ConfigName&#x2F;&#x2F; create config.jsonfile, err :&#x3D; os.Create(fileName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;create %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;defer file.Close()&#x2F;&#x2F; write jsonify data to fileif _, err :&#x3D; file.WriteString(jsonStr); err !&#x3D; nil &#123;logrus.Errorf(&quot;write %s error %v&quot;, fileName, err)return &quot;&quot;, err&#125;return containerName, nil&#125;</code></pre><p>给 ContainerInfo 添加 Volume 成员：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ContainerInfo struct &#123;Pid         string &#96;json:&quot;pid&quot;&#96;        &#x2F;&#x2F;容器的init进程在宿主机上的 PIDId          string &#96;json:&quot;id&quot;&#96;         &#x2F;&#x2F;容器IdName        string &#96;json:&quot;name&quot;&#96;       &#x2F;&#x2F;容器名Command     string &#96;json:&quot;command&quot;&#96;    &#x2F;&#x2F;容器内init运行命令CreatedTime string &#96;json:&quot;createTime&quot;&#96; &#x2F;&#x2F;创建时间Status      string &#96;json:&quot;status&quot;&#96;     &#x2F;&#x2F;容器的状态Volume      string &#96;json:&quot;volume&quot;&#96;&#125;</code></pre><p>然后将<code>RootURL</code>，<code>MntURL</code>，<code>WriteLayer</code>设为常量：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (RUNNING             string &#x3D; &quot;running&quot;STOP                string &#x3D; &quot;stopped&quot;Exit                string &#x3D; &quot;exited&quot;DefaultInfoLocation string &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;%s&#x2F;&quot;ConfigName          string &#x3D; &quot;config.json&quot;ContainerLogFile    string &#x3D; &quot;container.log&quot;RootURL             string &#x3D; &quot;&#x2F;root&#x2F;&quot;MntURL              string &#x3D; &quot;&#x2F;root&#x2F;mnt&#x2F;%s&#x2F;&quot;WriteLayerURL       string &#x3D; &quot;&#x2F;root&#x2F;writeLayer&#x2F;%s&quot;)</code></pre><p>相应地，<code>NewParentProcess</code> 函数也要修改：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p><code>NewWorkSpace</code>函数的三个参数分别改为：<code>volume</code>，<code>imageName</code>，<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewWorkSpace(volume, imageName, containerName string) &#123;CreateReadOnlyLayer(imageName)CreateWriteLayer(containerName)CreateMountPoint(containerName, imageName)if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;MountVolume(volumeURLs, containerName)logrus.Infof(&quot;%q&quot;, volumeURLs)&#125; else &#123;logrus.Infof(&quot;volume parameter input is not correct&quot;)&#125;&#125;&#125;</code></pre><p>下面来修改<code>CreateReadOnlyLayer</code>，<code>CreateWriteLayer</code>，<code>CreateMountPoint</code>这三个函数：</p><p>首先是 <code>CreateReadOnlyLayer</code>，参数名改为<code>imageName</code>，镜像解压出来的只读层以<code>RootURL+imageName</code> 命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateReadOnlyLayer(imageName string) error &#123;unTarFolderURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;&#x2F;&quot;imageURL :&#x3D; RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;exist, err :&#x3D; PathExists(unTarFolderURL)if err !&#x3D; nil &#123;logrus.Infof(&quot;fail to judge whether dir %s exists. %v&quot;, unTarFolderURL, err)return err&#125;if !exist &#123;if err :&#x3D; os.MkdirAll(unTarFolderURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error. %v&quot;, unTarFolderURL, err)return err&#125;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-xvf&quot;, imageURL, &quot;-C&quot;, unTarFolderURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;unTar dir %s error %v&quot;, unTarFolderURL, err)return err&#125;&#125;return nil&#125;</code></pre><p><code>CreateWriteLayer</code> 为每个容器创建一个读写层，把参数改为containerName，容器读写层修改为 <code>WriteLayerURL+containerName</code>命名：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateWriteLayer(containerName string) &#123;writeUrl :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.MkdirAll(writeUrl, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;Mkdir write layer dir %s error. %v&quot;, writeUrl, err)&#125;&#125;</code></pre><p><code>CreateMountPoint</code>创建容器根目录，然后把镜像只读层和容器读写层挂载到容器根目录，成为容器文件系统，参数列表改为<code>containerName</code> 和 <code>imageName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateMountPoint(containerName, imageName string) error &#123;&#x2F;&#x2F; create mnt folder as mount pointmntURL :&#x3D; fmt.Sprintf(MntURL, containerName)if err :&#x3D; os.MkdirAll(mntURL, 0777); err !&#x3D; nil &#123;logrus.Errorf(&quot;mkdir dir %s error %v&quot;, mntURL, err)return err&#125;&#x2F;&#x2F; mount &#39;writeLayer&#39; and &#39;busybox&#39; to &#39;mnt&#39;tmpWriteLayer :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)tmpImageLocation :&#x3D; RootURL + &quot;&#x2F;&quot; + imageNamedirs :&#x3D; &quot;dirs&#x3D;&quot; + tmpWriteLayer + &quot;:&quot; + tmpImageLocation_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;run command for creating mount point failed: %v&quot;, err)return err&#125;return nil&#125;</code></pre><p><code>MountVolume</code> 根据用户输入的 volume参数获取相应挂载宿主机数据卷 URL 和容器的挂载点URL，并挂载数据卷。参数列表改为 <code>volumeURLs</code> 和<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func MountVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; create host file catalogparentURL :&#x3D; volumeURLs[0]if err :&#x3D; os.Mkdir(parentURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir parent dir %s error. %v&quot;, parentURL, err)&#125;&#x2F;&#x2F; create mount point in container file systemcontainerURL :&#x3D; volumeURLs[1]mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerVolumeURL :&#x3D; mntURL + &quot;&#x2F;&quot; + containerURLif err :&#x3D; os.Mkdir(containerVolumeURL, 0777); err !&#x3D; nil &#123;logrus.Infof(&quot;mkdir container dir %s error. %v&quot;, containerVolumeURL, err)&#125;&#x2F;&#x2F; mount host file catalog to mount point in containerdirs :&#x3D; &quot;dirs&#x3D;&quot; + parentURL_, err :&#x3D; exec.Command(&quot;mount&quot;, &quot;-t&quot;, &quot;aufs&quot;, &quot;-o&quot;, dirs, &quot;none&quot;, containerVolumeURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;mount volume failed. %v&quot;, err)return err&#125;return nil&#125;</code></pre><p>然后在删除容器的 <code>removeContainer</code> 函数最后加一行<code>DeleteWorkSpace</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func removeContainer(containerName string) &#123;containerInfo, err :&#x3D; getContainerInfoByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;get container %s info failed: %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; only remove the stopped containerif containerInfo.Status !&#x3D; container.STOP &#123;logrus.Errorf(&quot;cannot remove running container %s&quot;, containerName)return&#125;dirURL :&#x3D; fmt.Sprintf(container.DefaultInfoLocation, containerName)&#x2F;&#x2F; remove all the info including sub dirif err :&#x3D; os.RemoveAll(dirURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;cannot remove dir %s error: %v&quot;, dirURL, err)return&#125;container.DeleteWorkSpace(containerInfo.Volume, containerName)&#125;</code></pre><p>然后 <code>DeleteWorkSpace</code>也要修改，<code>DeleteWorkSpace</code>作用是当容器退出时，删除容器相关文件系统，参数列表改为 containerName 和volume：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWorkSpace(volume, containerName string) &#123;if volume !&#x3D; &quot;&quot; &#123;volumeURLs :&#x3D; volumeUrlExtract(volume)length :&#x3D; len(volumeURLs)if length &#x3D;&#x3D; 2 &amp;&amp; volumeURLs[0] !&#x3D; &quot;&quot; &amp;&amp; volumeURLs[1] !&#x3D; &quot;&quot; &#123;DeleteMountPointWithVolume(volumeURLs, containerName)&#125; else &#123;DeleteMountPoint(containerName)&#125;&#125; else &#123;DeleteMountPoint(containerName)&#125;DeleteWriteLayer(containerName)&#125;</code></pre><p><code>DeleteMountPoint</code>函数作用是删除未挂载数据卷的容器文件系统，参数修改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPoint(containerName string) error &#123;mntURL :&#x3D; fmt.Sprintf(MntURL, containerName)_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;%v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, mntURL, err)return err&#125;return nil&#125;</code></pre><p><code>DeleteMountPointWithVolume</code>函数用来删除挂载数据卷容器的文件系统，参数列表改为<code>volumeURLs</code> 和 <code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteMountPointWithVolume(volumeURLs []string, containerName string) error &#123;&#x2F;&#x2F; umount volume point in containermntURL :&#x3D; fmt.Sprintf(MntURL, containerName)containerURL :&#x3D; mntURL + &quot;&#x2F;&quot; + volumeURLs[1]if _, err :&#x3D; exec.Command(&quot;umount&quot;, containerURL).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;umount volume failed. %v&quot;, err)return err&#125;&#x2F;&#x2F; umount the whole point of the container_, err :&#x3D; exec.Command(&quot;umount&quot;, mntURL).CombinedOutput()if err !&#x3D; nil &#123;logrus.Errorf(&quot;umount mountpoint failed. %v&quot;, err)return err&#125;if err :&#x3D; os.RemoveAll(mntURL); err !&#x3D; nil &#123;logrus.Infof(&quot;remove mountpoint dir %s error %v&quot;, mntURL, err)&#125;return nil&#125;</code></pre><p><code>DeleteWriteLayer</code> 函数用来删除容器读写层，参数改为<code>containerName</code>：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteWriteLayer(containerName string) &#123;writeURL :&#x3D; fmt.Sprintf(WriteLayerURL, containerName)if err :&#x3D; os.RemoveAll(writeURL); err !&#x3D; nil &#123;logrus.Errorf(&quot;remove dir %s error %v&quot;, writeURL, err)&#125;&#125;</code></pre><p>然后修改 <code>command.go</code> 中的<code>commitCommand</code>：输入参数名改为 <code>containerName</code> 和<code>imageName</code>：·</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var CommitCommand &#x3D; cli.Command&#123;Name:  &quot;commit&quot;,Usage: &quot;commit a container into image&quot;,Action: func(context *cli.Context) error &#123;if len(context.Args()) &lt; 1 &#123;return fmt.Errorf(&quot;missing container name&quot;)&#125;containerName :&#x3D; context.Args()[0]imageName :&#x3D; context.Args()[1]&#x2F;&#x2F; commitContainer(containerName)commitContainer(containerName, imageName)return nil&#125;,&#125;</code></pre><p>修改 <code>commit.go</code> 的 <code>commitContainer</code>函数，根据传入的 containerName 制作 <code>imageName.tar</code>镜像：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func commitContainer(containerName, imageName string) &#123;mntURL :&#x3D; fmt.Sprintf(container.MntURL, containerName)mntURL +&#x3D; &quot;&#x2F;&quot;imageTar :&#x3D; container.RootURL + &quot;&#x2F;&quot; + imageName + &quot;.tar&quot;if _, err :&#x3D; exec.Command(&quot;tar&quot;, &quot;-czf&quot;, imageTar, &quot;-C&quot;, mntURL, &quot;.&quot;).CombinedOutput(); err !&#x3D; nil &#123;logrus.Errorf(&quot;tar folder %s error %v&quot;, mntURL, err)&#125;&#125;</code></pre><p>测试一下，用 busybox 启动两个容器 test1 和 test2，test1 把宿主机<code>/root/from1</code> 挂载到容器 <code>/to1</code>，test2 把宿主机<code>/root/from2</code> 挂载到 <code>/to2</code> 下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test1 -v &#x2F;root&#x2F;from1:&#x2F;to1 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from1\&quot; \&quot;&#x2F;to1\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:42+08:00&quot;&#125;# go run . run -d --name test2 -v &#x2F;root&#x2F;from2:&#x2F;to2 busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from2\&quot; \&quot;&#x2F;to2\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:04:51+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1       11570       running     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>打开另一个终端，可以看到 <code>/root</code> 目录下多了<code>from1</code> 和 <code>from2</code> 两个目录，我们看看<code>mnt</code> 和 <code>writeLayer</code>，<code>mnt</code> 下多了两个busybox 的挂载层，<code>writeLayer</code>下分别挂载了两个容器的目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   └── to1└── test2    └── to2</code></pre><p>下面进入 test1 容器，创建 <code>/to1/test1.txt</code>：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . exec test1 sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 11570&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T10:16:33+08:00&quot;&#125;&#x2F; # echo -e &quot;test1&quot; &gt;&gt; &#x2F;to1&#x2F;test1.txt&#x2F; # mkdir to1-1&#x2F; # echo -e &quot;test111111&quot; &gt;&gt; &#x2F;to1-1&#x2F;test1111.txt</code></pre><p>这时候再来看看可写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># tree writeLayer&#x2F;writeLayer&#x2F;├── test1│   ├── root│   ├── to1│   └── to1-1│       └── test1111.txt└── test2    └── to2# cat writeLayer&#x2F;test1&#x2F;to1-1&#x2F;test1111.txttest111111</code></pre><p>多了 <code>to1-1/test1111.txt</code>，那刚刚创建的<code>test1.txt</code> 去哪了呢？这时候我们看看<code>from1</code>，在这里，新创建的文件写入了数据卷。</p><p>下面来验证 commit 功能：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . commit test1 image1</code></pre><p>导出的镜像路径为 <code>/root/image1.tar</code>。</p><p>下面测试停止和删除容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . stop test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED4010011034   test1                   stopped     top         2023-05-11 10:04:425746376093   test2       11684       running     top         2023-05-11 10:04:51# go run . rm test1# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:51</code></pre><p>我们看看容器根目录和可读写层：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls mnttest2# tree writeLayer&#x2F;writeLayer&#x2F;└── test2    └── to2</code></pre><p>test1 的容器根目录和可读写层被删除。</p><p>下面来试一下用镜像创建容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test3 -v &#x2F;root&#x2F;from3:&#x2F;to3 image1 top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;[\&quot;&#x2F;root&#x2F;from3\&quot; \&quot;&#x2F;to3\&quot;]&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T10:32:44+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED5746376093   test2       11684       running     top         2023-05-11 10:04:514713076733   test3       13056       running     top         2023-05-11 10:32:44</code></pre><p>这时我们可以看到 <code>/root</code> 多了一个 <code>image1</code>目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ls image1bin  dev  etc  home  lib  lib64  proc  root  sys  tmp  to1  to1-1  usr  var</code></pre><p>在这里发现了刚才创建的 <code>to1-1</code>，用 <code>image1.tar</code>启动的容器 test3，进入容器后发现我们刚刚写入的文件，至此，我们成功把容器test1 的数据卷 to1 信息，重新写入了容器 test3 数据卷 to3。</p><p>在次小节后，进入容器都要指定镜像名，不然都会报错。</p><h4 id="实现容器指定环境变量运行-4">6.8 实现容器指定环境变量运行</h4><p>本节来实现让容器内运行的程序可以使用外部传递的环境变量。</p><h5 id="修改-runcommand-4">6.8.1 修改 runCommand</h5><p>在原来基础上增加 <code>-e</code>选项，允许用户指定环境变量，由于环境变量可以是多个，这里允许用户多次使用<code>-e</code> 来传递，同时添加对环境变量的解析，整体修改如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var RunCommand &#x3D; cli.Command&#123;Name:  &quot;run&quot;,Usage: &quot;Create a container&quot;,Flags: []cli.Flag&#123;&#x2F;&#x2F; integrate -i and -t for convenience&amp;cli.BoolFlag&#123;Name:  &quot;it&quot;,Usage: &quot;open an interactive tty(pseudo terminal)&quot;,&#125;,&amp;cli.StringFlag&#123;Name:  &quot;m&quot;,Usage: &quot;limit the memory&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpu&quot;,Usage: &quot;limit the cpu amount&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpushare&quot;,Usage: &quot;limit the cpu share&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;v&quot;,Usage: &quot;volume&quot;,&#125;, &amp;cli.BoolFlag&#123;Name:  &quot;d&quot;,Usage: &quot;detach container&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;cpuset&quot;,Usage: &quot;limit the cpuset&quot;,&#125;, &amp;cli.StringFlag&#123;Name:  &quot;name&quot;,Usage: &quot;container name&quot;,&#125;, &amp;cli.StringSliceFlag&#123;Name:  &quot;e&quot;,Usage: &quot;set environment&quot;,&#125;,&#125;,Action: func(context *cli.Context) error &#123;args :&#x3D; context.Args()if len(args) &lt;&#x3D; 0 &#123;return errors.New(&quot;run what?&quot;)&#125;&#x2F;&#x2F; 转化 cli.Args 为 []stringcmdArray :&#x3D; make([]string, len(args)) &#x2F;&#x2F; commandcopy(cmdArray, args)&#x2F;&#x2F; check whether type &#96;-it&#96;tty :&#x3D; context.Bool(&quot;it&quot;)   &#x2F;&#x2F; presudo terminaldetach :&#x3D; context.Bool(&quot;d&quot;) &#x2F;&#x2F; detach containerif tty &amp;&amp; detach &#123;return fmt.Errorf(&quot;it and d paramter cannot both privided&quot;)&#125;&#x2F;&#x2F; get the resource configresourceConfig :&#x3D; subsystem.ResourceConfig&#123;MemoryLimit: context.String(&quot;m&quot;),CPUShare:    context.String(&quot;cpushare&quot;),CPUSet:      context.String(&quot;cpu&quot;),&#125;volume :&#x3D; context.String(&quot;v&quot;)containerName :&#x3D; context.String(&quot;name&quot;)envSlice :&#x3D; context.StringSlice(&quot;e&quot;)imageName :&#x3D; cmdArray[0]cmdArray &#x3D; cmdArray[1:]Run(tty, cmdArray, &amp;resourceConfig, volume, containerName, imageName, envSlice)return nil&#125;,&#125;</code></pre><h5 id="修改-run-函数-4">6.8.2 修改 Run 函数</h5><p>参数里新增一个 <code>envSlice</code>，然后传递给<code>NewParentProcess</code> 函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Run(tty bool, cmdArray []string, res *subsystem.ResourceConfig, volume, containerName, imageName string, envSlice []string) &#123;containerID :&#x3D; randStringBytes(10)if containerName &#x3D;&#x3D; &quot;&quot; &#123;containerName &#x3D; containerID&#125;&#x2F;&#x2F; this is &quot;docker init &lt;cmdArray&gt;&quot;initProcess, writePipe :&#x3D; container.NewParentProcess(tty, volume, containerName, imageName, envSlice)if initProcess &#x3D;&#x3D; nil &#123;logrus.Errorf(&quot;new parent process error&quot;)return&#125;&#x2F;&#x2F; start the init processif err :&#x3D; initProcess.Start(); err !&#x3D; nil &#123;logrus.Error(err)&#125;&#x2F;&#x2F; container infocontainerName, err :&#x3D; recordContainerInfo(initProcess.Process.Pid, cmdArray, containerName, volume)if err !&#x3D; nil &#123;logrus.Errorf(&quot;record container info error: %v&quot;, err)return&#125;&#x2F;&#x2F; create container manager to control resource config on all hierarchiescm :&#x3D; cgroups.NewCgroupManager(&quot;simple-docker-container&quot;)defer cm.Remove()cm.Set(res)cm.AddProcess(initProcess.Process.Pid)&#x2F;&#x2F; send command to write side&#x2F;&#x2F; will close the plugsendInitCommand(cmdArray, writePipe)if tty &#123;initProcess.Wait()deleteContainerInfo(containerName)container.DeleteWorkSpace(volume, containerName)&#125;os.Exit(0)&#125;</code></pre><h5 id="修改-newparentprocess-函数-4">6.8.3 修改 NewParentProcess函数</h5><p>参数新增一个 <code>envSlice</code>，给 cmd 设置环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewParentProcess(tty bool, volume string, containerName, imageName string, envSlice []string) (*exec.Cmd, *os.File) &#123;readPipe, writePipe, err :&#x3D; os.Pipe()if err !&#x3D; nil &#123;logrus.Errorf(&quot;New Pipe Error: %v&quot;, err)return nil, nil&#125;&#x2F;&#x2F; create a new command which run itself&#x2F;&#x2F; the first arguments is &#96;init&#96; which is in the &quot;container&#x2F;init.go&quot; file&#x2F;&#x2F; so, the &lt;cmd&gt; will be interpret as &quot;docker init &lt;cmdArray&gt;&quot;cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;init&quot;)cmd.SysProcAttr &#x3D; &amp;syscall.SysProcAttr&#123;Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,&#125;cmd.Stdin &#x3D; os.Stdinif tty &#123;cmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderr&#125; else &#123;dirURL :&#x3D; fmt.Sprintf(DefaultInfoLocation, containerName)if err :&#x3D; os.MkdirAll(dirURL, 0622); err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess mkdir %s error %v&quot;, dirURL, err)return nil, nil&#125;stdLogFilePath :&#x3D; dirURL + ContainerLogFilestdLogFile, err :&#x3D; os.Create(stdLogFilePath)if err !&#x3D; nil &#123;logrus.Errorf(&quot;NewParentProcess create file %s error %v&quot;, stdLogFilePath, err)return nil, nil&#125;cmd.Stdout &#x3D; stdLogFile&#125;cmd.ExtraFiles &#x3D; []*os.File&#123;readPipe&#125;cmd.Env &#x3D; append(os.Environ(), envSlice...)NewWorkSpace(volume, imageName, containerName)cmd.Dir &#x3D; fmt.Sprintf(MntURL, containerName)return cmd, writePipe&#125;</code></pre><p>测试一下：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it --name test -e test&#x3D;123 -e luck&#x3D;test busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;test&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:14:52+08:00&quot;&#125;&#x2F; #  env | grep testtest&#x3D;123luck&#x3D;test</code></pre><p>可以看到，手动指定的环境变量在容器内可见。后面创建一个后台运行的容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:19:31+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9649354121   test        29524       running     top         2023-05-11 14:19:31# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 29524&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:20:12+08:00&quot;&#125;&#x2F; # ps -efPID   USER     TIME  COMMAND    1 root      0:00 top    7 root      0:00 sh    8 root      0:00 ps -ef&#x2F; # env | grep test&#x2F; #</code></pre><p>查看环境变量，没有我们设置的环境变量。</p><p>这里不能用 env 命令获取设置的环境变量，原因是 exec 可以说 go发起的另一个进程，这个进程的父进程是宿主机的，这个，并不是容器内的。在cgo 内使用了 setns系统调用，才使得进程进入了容器内部的命名空间，但由于环境变量是继承自父进程的，因此这个exec 进程的环境变量其实是继承自宿主机，所以在 exec看到的环境变量其实是宿主机的环境变量。</p><p>但只要是容器内 pid 为 1的进程，创造出来的进程都会继承它的环境变量，下面来修改 exec命令来直接使用 env 命令来查看容器内环境变量的功能。</p><h5 id="修改-exec-命令-4">6.8.4 修改 exec 命令</h5><p>提供一个函数，可根据指定的 pid 来获取对应进程的环境变量。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func getEnvsByPid(pid string) []string &#123;path :&#x3D; fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;environ&quot;, pid)contentBytes ,err :&#x3D; ioutil.ReadFile(path)if err !&#x3D; nil &#123;logrus.Errorf(&quot;read file %s error %v&quot;, path, err)return nil&#125;&#x2F;&#x2F; divide by &#39;\u0000&#39;envs :&#x3D; strings.Split(string(contentBytes),&quot;\u0000&quot;)return envs&#125;</code></pre><p>由于进程存放环境变量的位置是<code>/proc/$&#123;pid&#125;/environ</code>，因此根据给定的 pid去读取这个文件，可以获取环境变量，在文件的描述中，每个环境变量之间通过<code>\u0000</code> 分割，因此可以以此标记来获取环境变量数组。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ExecContainer(containerName string, comArray []string) &#123;&#x2F;&#x2F; get the pid according the containerNamepid, err :&#x3D; getContainerPidByName(containerName)if err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container getContainerPidByName %s error %v&quot;, containerName, err)return&#125;&#x2F;&#x2F; divide command by blank space and combine as a stringcmdStr :&#x3D; strings.Join(comArray, &quot; &quot;)logrus.Infof(&quot;container pid %s&quot;, pid)logrus.Infof(&quot;command %s&quot;, cmdStr)cmd :&#x3D; exec.Command(&quot;&#x2F;proc&#x2F;self&#x2F;exe&quot;, &quot;exec&quot;)cmd.Stdin &#x3D; os.Stdincmd.Stdout &#x3D; os.Stdoutcmd.Stderr &#x3D; os.Stderrerr &#x3D; os.Setenv(ENV_EXEC_PID, pid)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec pid %s error %v&quot;, pid, err)&#125;err &#x3D; os.Setenv(ENV_EXEC_CMD, cmdStr)if err !&#x3D; nil &#123;logrus.Errorf(&quot;set env exec command %s error %v&quot;, cmdStr, err)&#125;&#x2F;&#x2F; get target pid environ (container environ)containerEnvs :&#x3D; getEnvsByPid(pid)&#x2F;&#x2F; set host environ and container environ to exec processcmd.Env &#x3D; append(os.Environ(), containerEnvs...)if err :&#x3D; cmd.Run(); err !&#x3D; nil &#123;logrus.Errorf(&quot;exec container %s error %v&quot;, containerName, err)&#125;&#125;</code></pre><p>这里由于 exec命令依然要宿主机的一些环境变量，因此将宿主机环境变量和容器环境变量都一起放置到exec 进程中：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -d --name test -e test&#x3D;123 -e luck&#x3D;test busybox top&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: top&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:03+08:00&quot;&#125;# go run . psID           NAME        PID         STATUS      COMMAND     CREATED9729397397   test        50040       running     top         2023-05-11 14:30:03# go run . exec test sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;container pid 50040&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;command sh&quot;,&quot;time&quot;:&quot;2023-05-11T14:30:17+08:00&quot;&#125;&#x2F; # env | grep testtest&#x3D;123luck&#x3D;test&#x2F; #</code></pre><p>现在可以看到 exec 进程可以获取前面 run 时设置的环境变量了。</p><h2 id="四网络篇-4">四、网络篇</h2><h3 id="容器网络-4">7. 容器网络</h3><h4 id="网络虚拟化技术-4">7.1 网络虚拟化技术</h4><h5 id="linux-虚拟网络设备-4">7.1.1 Linux 虚拟网络设备</h5><p>Linux是用网络设备去操作和使用网卡的，系统装了一个网卡后就会为其生成一个网络设备实例，例如eth0。Linux支持创建出虚拟化的设备，可通过组合实现多种多样的功能和网络拓扑，这里主要介绍Veth 和 Bridge。</p><p><strong>Linux Veth</strong></p><p>Veth 时成对出现的虚拟网络设备，发送到 Veth一端虚拟设备的请求会从另一端的虚拟设备中发出。容器的虚拟化场景中，常会使用Veth 连接不同的网络 namespace：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip netns add ns2# ip link add veth0 type veth peer name veth1# ip link set veth0 netns ns1# ip link set veth1 netns ns2# ip netns exec ns1 ip link1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:004: veth0@if3: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link&#x2F;ether 02:bf:18:99:77:ed brd ff:ff:ff:ff:ff:ff link-netns ns2</code></pre><p>在 ns1 和 ns2 的namespace 中，除 loopback的设备以外就只看到了一个网络设备。当请求发送到这个虚拟网络设备时，都会原封不动地从另一个网络namespace的网络接口中出来。例如，给两端分别配置不同地址后，向虚拟网络设备的一端发送请求，就能达到这个虚拟网络设备对应的另一端。</p><p><img src="0x0035/7.1.1-veth.png" style="zoom:43%;" /></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns exec ns1 ifconfig veth0 172.18.0.2&#x2F;24 up# ip netns exec ns2 ifconfig veth1 172.18.0.3&#x2F;24 up# ip netns exec ns1 route add default dev veth0# ip netns exec ns2 route add default dev veth1# ip netns exec ns1 ping -c 1 172.18.0.3PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.64 bytes from 172.18.0.3: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.395 ms--- 172.18.0.3 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.395&#x2F;0.395&#x2F;0.395&#x2F;0.000 ms</code></pre><p><strong>Linux Bridge</strong></p><p>进行下一步之前，先删除上一小节创建的 netns：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns del ns1# ip netns del ns2# ip netns list</code></pre><p>此时之前创建的两个 netns 被删除。</p><p>Bridge虚拟设备时用来桥接的网络设备，相当于现实世界的交换机，可以连接不同的网络设备，当请求达到Bridge 设备时，可以通过报文中的 Mac 地址进行广播或转发。例如，创建一个Bridge 设备，来连接 namespace 中的网络设备和宿主机上的网络：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip netns add ns1# ip link add veth0 type veth peer name veth1# ip link set veth1 netns ns1########## 创建网桥# brctl addbr br0########## 挂载网络设备# brctl addif br0 eth0# brctl addif bro veth0</code></pre><p><img src="0x0035/7.1.1-bridge.png" /></p><h5 id="linux-路由表-4">7.1.2 Linux 路由表</h5><p>路由表是 Linux 内核的一个模块，通过定义路由表来决定在某个网络namespace 中包的流向，从而定义请求会到哪个网络设备上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ip link set veth0 up# ip link set br0 up# ip netns exec ns1 ifconfig veth1 172.18.0.2&#x2F;24 up# ip netns exec ns1 route add default dev veth1# route add -net 172.18.0.0&#x2F;24 dev br0</code></pre><p><img src="0x0035/7.1.2-route.png" /></p><p>通过设置路由，对 IP地址的请求就能正确被路由到对应的网络设备上，从而实现通信：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ifconfig eth0eth0: flags&#x3D;4163&lt;UP,BROADCAST,RUNNING,MULTICAST&gt;  mtu 1500        inet 172.31.93.218  netmask 255.255.240.0  broadcast 172.31.95.255        inet6 fe80::215:5dff:fe4e:a16a  prefixlen 64  scopeid 0x20&lt;link&gt;        ether 00:15:5d:4e:a1:6a  txqueuelen 1000  (Ethernet)        RX packets 829  bytes 394161 (394.1 KB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 90  bytes 10335 (10.3 KB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0########## 在namespace访问宿主机# ip netns exec ns1 ping -c 1 172.31.93.218PING 172.31.93.218 (172.31.93.218) 56(84) bytes of data.64 bytes from 172.31.93.218: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.556 ms--- 172.31.93.218 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.556&#x2F;0.556&#x2F;0.556&#x2F;0.000 ms######### 从宿主机访问namespace的网络地址# ping -c 1 172.18.0.2PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.64 bytes from 172.18.0.2: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.113 ms--- 172.18.0.2 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.113&#x2F;0.113&#x2F;0.113&#x2F;0.000 ms</code></pre><h5 id="linux-iptables-3">7.1.3 Linux iptables</h5><p>iptables 是对 Linux 内核的 netfilter模块进行操作和展示的工具，用来管理包的流动和转送。iptables定义了一套链式处理的结构，在网络包传输的各个阶段可以使用不同的策略和包进行加工、传送或丢弃。在容器虚拟化技术里，常会用到两种策略，MASQUERADE和 DNAT，用于容器和宿主机外部的网络通信。</p><p><strong>MASQUERADE</strong></p><p>MASQUERADE 策略可以将请求包中的源地址转换为一个网络设备的地址，例如<a href="#7.1.2%20Linux%20路由表">7.1.2 Linux 路由表</a>这一小节里，namespace 中网络设备的地址是172.18.0.2，这个地址虽然在宿主机可以路由到 br0的网桥，但是到底宿主机外部后，是不知道如何路由到这个 IP的，所以如果请求外部地址的话，要先通过 MASQUERADE 策略将这个 IP转换为宿主机出口网卡的 IP：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># sysctl -w net.ipv4.conf.all.forwarding&#x3D;1net.ipv4.conf.all.forwarding &#x3D; 1# iptables -t nat -A POSTROUTING -s 172.18.0.0&#x2F;24 -o eth0 -j MASQUERADE</code></pre><p>在 namespace 中请求宿主机外部地址时，将 namespace中源地址转换为宿主机的地址作为源地址，就可以在 namespace中访问宿主机外的网络了。</p><p><strong>DAT</strong></p><p>iptables 中的 DNAT策略也是做网络地址的转换，不过它是要更换目标地址，常用于将内部网络地址的端口映射出去。例如，上面例子的namespace如果要提供服务给宿主机之外的应用要怎么办呢？外部应用没办法直接路由到172.18.0.2 这个地址，这时候可以用 DNAT 策略。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80</code></pre><p>这样就可以把宿主机上的 80 端口的 TCP 请求转发到 namespace 的172.18.0.2:80，从而实现外部应用的调用。</p><h4 id="构建容器网络模型-3">7.2 构建容器网络模型</h4><h5 id="基本模型-3">7.2.1 基本模型</h5><h6 id="网络-3">网络</h6><p>网络是容器的一个集合，在这个网络上的容器可以相互通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Network struct &#123;    Name    string &#x2F;&#x2F; network name    IpRange *net.IPNet &#x2F;&#x2F; address    Driver  string &#x2F;&#x2F; network driver name&#125;</code></pre><h6 id="网络端点-3">网络端点</h6><p>网络端点用于连接网络与容器，保证容器内部与网络的通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Endpoint struct &#123;ID          string           &#96;json:&quot;id&quot;&#96;Device      netlink.Veth     &#96;json:&quot;dev&quot;&#96;IPAddress   net.IP           &#96;json:&quot;ip&quot;&#96;MacAddress  net.HardwareAddr &#96;json:&quot;mac&quot;&#96;Network     *NetworkPortMapping []string&#125;</code></pre><p>网络端点的信息传输需要靠网络功能的两个组件配合完成，分别为网络驱动和IPAM。</p><h6 id="网络驱动-3">网络驱动</h6><p>网络驱动是网络功能的一个组件，不同驱动对网络的创建、连接、销毁策略不同，通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type NetworkDriver interface &#123;Name() string &#x2F;&#x2F; driver nameCreate(subnet string, name string) (*Network, error)Delete(network Network) errorConnect(network *Network, endpoint *Endpoint) errorDisconnect(network Network, endpoint *Endpoint) error&#125;</code></pre><h6 id="ipam-3">IPAM</h6><p>IPAM 也是网络功能的一个组件，用于网络 IP 地址的分配和释放，包括容器的IP 和网络网关的 IP。主要功能如下：</p><ul><li><code>ipam.Allocate(*net.IPNet)</code> 从指定的 subnet 网段中分配IP　</li><li><code>ipam.Release(*net.IPNet, net.IP)</code> 从指定的 subnet网段中释放掉指定的 IP</li></ul><p>在构建下面的函数之前，先来补充一些书上没写的：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (defaultNetworkPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;network&#x2F;&quot; &#x2F;&#x2F; 默认网络配置信息存储位置drivers            &#x3D; map[string]NetworkDriver&#123;&#125; &#x2F;&#x2F; 驱动字典，存储驱动信息networks           &#x3D; map[string]*Network&#123;&#125; &#x2F;&#x2F; 网络字段，存储网络信息)</code></pre><h5 id="调用关系-3">7.2.2 调用关系</h5><h6 id="创建网络-3">创建网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateNetwork(driver, subnet, name string) error &#123;_, cidr, _ :&#x3D; net.ParseCIDR(subnet)    &#x2F;&#x2F; allocate gateway ip by IPAMgatewayIP, err :&#x3D; ipAllocator.Allocate(cidr)if err !&#x3D; nil &#123;return err&#125;cidr.IP &#x3D; gatewayIPnw, err :&#x3D; drivers[driver].Create(cidr.String(), name)if err !&#x3D; nil &#123;return err&#125;    &#x2F;&#x2F; save network inforeturn nw.dump(defaultNetworkPath)&#125;</code></pre><p>其中，network.dump 和 network.load方法是将这个网络的配置信息保存在文件系统中，或从网络的配置目录中的文件读取到网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) dump(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(dumpPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(dumpPath, 0644)&#125; else &#123;return err&#125;&#125;nwPath :&#x3D; path.Join(dumpPath, nw.Name)    &#x2F;&#x2F; create file while empty file, write only, no filenwFile, err :&#x3D; os.OpenFile(nwPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;defer nwFile.Close()nwJson, err :&#x3D; json.Marshal(nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;_, err &#x3D; nwFile.Write(nwJson)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;return nil&#125;func (nw *Network) load(dumpPath string) error &#123;nwConfigFile, err :&#x3D; os.Open(dumpPath)if err !&#x3D; nil &#123;return err&#125;defer nwConfigFile.Close()nwJson :&#x3D; make([]byte, 2000)n, err :&#x3D; nwConfigFile.Read(nwJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(nwJson[:n], nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error load nw info: %v&quot;, err)return err&#125;return nil&#125;</code></pre><h6 id="创建容器并连接网络-3">创建容器并连接网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Connect(networkName string, cinfo *container.ContainerInfo) error &#123;network, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;ip, err :&#x3D; ipAllocator.Allocate(network.IpRange)if err !&#x3D; nil &#123;return err&#125;ep :&#x3D; &amp;Endpoint&#123;ID:          fmt.Sprintf(&quot;%s-%s&quot;, cinfo.Id, networkName),IPAddress:   ip,Network:     network,PortMapping: cinfo.PortMapping,&#125;if err &#x3D; drivers[network.Driver].Connect(network, ep); err !&#x3D; nil &#123;return err&#125;if err &#x3D; configEndpointIpAddressAndRoute(ep, cinfo); err !&#x3D; nil &#123;return err&#125;return configPortMapping(ep, cinfo)&#125;</code></pre><h6 id="展示网络列表-3">展示网络列表</h6><p>从网络配置的目录中加载所有的网络配置信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Init() error &#123;var bridgeDriver &#x3D; BridgeNetworkDriver&#123;&#125;drivers[bridgeDriver.Name()] &#x3D; &amp;bridgeDriverif _, err :&#x3D; os.Stat(defaultNetworkPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(defaultNetworkPath, 0644)&#125; else &#123;return err&#125;&#125;filepath.Walk(defaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error &#123;         &#x2F;&#x2F; skip if dirif info.IsDir() &#123;return nil&#125;if strings.HasSuffix(nwPath, &quot;&#x2F;&quot;) &#123;return nil&#125;         &#x2F;&#x2F; load filename as network name_, nwName :&#x3D; path.Split(nwPath)nw :&#x3D; &amp;Network&#123;Name: nwName,&#125;if err :&#x3D; nw.load(nwPath); err !&#x3D; nil &#123;logrus.Errorf(&quot;error load network: %s&quot;, err)&#125;&#x2F;&#x2F; save network info to network dicnetworks[nwName] &#x3D; nwreturn nil&#125;)return nil&#125;</code></pre><p>遍历展示创建的网络：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListNetwork() &#123;w :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprint(w, &quot;NAME\tIpRange\tDriver\n&quot;)for _, nw :&#x3D; range networks &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\n&quot;,nw.Name,nw.IpRange.String(),nw.Driver,)&#125;if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;Flush error %v&quot;, err)return&#125;&#125;</code></pre><h6 id="删除网络-3">删除网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteNetwork(networkName string) error &#123;nw, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;if err :&#x3D; ipAllocator.Release(nw.IpRange, &amp;nw.IpRange.IP); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network gateway ip: %s&quot;, err)&#125;if err :&#x3D; drivers[nw.Driver].Delete(*nw); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network DriverError: %s&quot;, err)&#125;return nw.remove(defaultNetworkPath)&#125;</code></pre><p>删除网络的同时也删除配置目录的网络配置文件：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) remove(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(path.Join(dumpPath, nw.Name)); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125; else &#123;return os.Remove(path.Join(dumpPath, nw.Name))&#125;&#125;</code></pre><h4 id="容器地址分配-3">7.3 容器地址分配</h4><p>现在转到 <code>ipam.go</code>。</p><h5 id="数据结构定义-3">7.3.1 数据结构定义</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ipamDefaultAllocatorPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;ipam&#x2F;subnet.json&quot;type IPAM struct &#123;SubnetAllocatorPath stringSubnets             *map[string]string&#125;&#x2F;&#x2F; 初始化一个IPAM对象，并指定默认分配信息存储位置var ipAllocator &#x3D; &amp;IPAM&#123;SubnetAllocatorPath: ipamDefaultAllocatorPath,&#125;</code></pre><p>反序列化读取网段分配信息和序列化保存网段分配信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) load() error &#123;if _, err :&#x3D; os.Stat(ipam.SubnetAllocatorPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.Open(ipam.SubnetAllocatorPath)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()subnetJson :&#x3D; make([]byte, 2000)n, err :&#x3D; subnetConfigFile.Read(subnetJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(subnetJson[:n], ipam.Subnets)if err !&#x3D; nil &#123;logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)return err&#125;return nil&#125;func (ipam *IPAM) dump() error &#123;ipamConfigFileDir, _ :&#x3D; path.Split(ipam.SubnetAllocatorPath)if _, err :&#x3D; os.Stat(ipamConfigFileDir); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(ipamConfigFileDir, 0644)&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()ipamConfigJson, err :&#x3D; json.Marshal(ipam.Subnets)if err !&#x3D; nil &#123;return err&#125;_, err &#x3D; subnetConfigFile.Write(ipamConfigJson)if err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h5 id="地址分配-3">7.3.2 地址分配</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) &#123;ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;err &#x3D; ipam.load()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error dump allocation info, %v&quot;, err)&#125;_, subnet, _ &#x3D; net.ParseCIDR(subnet.String())one, size :&#x3D; subnet.Mask.Size()if _, exist :&#x3D; (*ipam.Subnets)[subnet.String()]; !exist &#123;        &#x2F;&#x2F; 用0填满网段的配置，1&lt;&lt;uint8(size-one)表示这个网段中有多少个可用地址        &#x2F;&#x2F; size-one时子网掩码后面的网络位数，2^(size-one)表示网段中的可用IP数        &#x2F;&#x2F; 2^(size-one)等价于1&lt;&lt;uint8(size-one)        (*ipam.Subnets)[subnet.String()] &#x3D; strings.Repeat(&quot;0&quot;, 1&lt;&lt;uint8(size-one))&#125;&#x2F;&#x2F; 这里的原理建议大家看看原著for c :&#x3D; range (*ipam.Subnets)[subnet.String()] &#123;if (*ipam.Subnets)[subnet.String()][c] &#x3D;&#x3D; &#39;0&#39; &#123;            ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])            &#x2F;&#x2F; go的字符串创建后不能修改，先用byte存储            ipalloc[c] &#x3D; &#39;1&#39;            (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)            &#x2F;&#x2F;             ip &#x3D; subnet.IP                        &#x2F;&#x2F; 通过网段的IP与上面的偏移相加得出分配的IP，由于IP是一个uint的一个数组，需要通过数组中的每一项加所需要的值，例 &#x2F;&#x2F; 如网段是172.16.0.0&#x2F;12，数组序号是65555，那就要在[172,16,0,0]上依次加            &#x2F;&#x2F; [uint8(65555 &gt;&gt; 24), uint8(65555 &gt;&gt; 16), uint8(65555 &gt;&gt; 8), uint(65555 &gt;&gt; 4)]，即[0,1,0,19]，            &#x2F;&#x2F; 那么获得的IP就是172.17.0.19            for t :&#x3D; uint(4); t &gt; 0; t-- &#123;                []byte(ip)[4-t] +&#x3D; uint8(c &gt;&gt; ((t - 1) * 8))            &#125;            &#x2F;&#x2F; 由于此处IP是从1开始分配的，所以最后再加1，最终得到分配的IP是172.16.0.20            ip[3]++            break&#125;&#125;ipam.dump()return&#125;</code></pre><h5 id="地址释放-3">7.3.3 地址释放</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error &#123;    ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;    _, subnet, _ &#x3D; net.ParseCIDR(subnet.String())    err :&#x3D; ipam.load()    if err !&#x3D; nil &#123;        logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)    &#125;    c :&#x3D; 0    &#x2F;&#x2F; 将IP转换为4个字节的表示方式    releaseIP :&#x3D; ipaddr.To4()    &#x2F;&#x2F; 由于IP是从1开始分配的，所以转换成索引减1    releaseIP[3] -&#x3D; 1    for t :&#x3D; uint(4); t &gt; 0; t -&#x3D; 1 &#123;        &#x2F;&#x2F; 和分配IP相反，释放IP获得索引的方式是IP的每一位相减后分别左移将对应的数值加到索引上        c +&#x3D; int(releaseIP[t-1]-subnet.IP[t-1]) &lt;&lt; ((4 - t) * 8)    &#125;    ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])    ipalloc[c] &#x3D; &#39;0&#39;    (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)    ipam.dump()    return nil&#125;</code></pre><p>根据书上，写到这里就开始测试了，但是我们看看IDE，红海一片，所以我们接着实现。</p><h4 id="创建-bridge-网络-3">7.4 创建 bridge 网络</h4><h5 id="实现-bridge-driver-create-3">7.4.1 实现 Bridge DriverCreate</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) &#123;ip, ipRange, _ :&#x3D; net.ParseCIDR(subnet)ipRange.IP &#x3D; ipn :&#x3D; &amp;Network&#123;Name:    name,IpRange: ipRange,Driver:  d.Name(),&#125;err :&#x3D; d.initBridge(n)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error init bridge: %v&quot;, err)&#125;return n, err&#125;</code></pre><h5 id="bridge-driver-初始化-linux-bridge-3">7.4.2 Bridge Driver 初始化Linux Bridge</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) initBridge(n *Network) error &#123;&#x2F;&#x2F; 创建bridge虚拟设备bridgeName :&#x3D; n.Nameif err :&#x3D; createBridgeInterface(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;eror add bridge: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置bridge设备的地址和路由gatewayIP :&#x3D; *n.IpRangegatewayIP.IP &#x3D; n.IpRange.IPif err :&#x3D; setInterfaceIP(bridgeName, gatewayIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error assigning address: %s on bridge: %s with an error of: %v&quot;, gatewayIP, bridgeName, err)&#125;&#x2F;&#x2F; 启动bridge设备if err :&#x3D; setInterfaceUP(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error set bridge up: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置iptables的SNAT规则if err :&#x3D; setupIPTables(bridgeName, n.IpRange); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error setting iptables for %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="创建-bridge-设备-3">创建 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func createBridgeInterface(bridgeName string) error &#123;_, err :&#x3D; net.InterfaceByName(bridgeName)if err &#x3D;&#x3D; nil || !strings.Contains(err.Error(), &quot;no such network interface&quot;) &#123;return err&#125;&#x2F;&#x2F; create *netlink.Bridge objectla :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; bridgeNamebr :&#x3D; &amp;netlink.Bridge&#123;LinkAttrs: la&#125;if err :&#x3D; netlink.LinkAdd(br); err !&#x3D; nil &#123;return fmt.Errorf(&quot;bridge creation failed for bridge %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="设置-bridge-设备的地址和路由-3">设置 bridge设备的地址和路由</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceIP(name string, rawIP string) error &#123;retries :&#x3D; 2var iface netlink.Linkvar err errorfor i :&#x3D; 0; i &lt; retries; i++ &#123;iface, err &#x3D; netlink.LinkByName(name)if err &#x3D;&#x3D; nil &#123;break&#125;logrus.Debugf(&quot;error retrieving new bridge netlink link [ %s ]... retrying&quot;, name)time.Sleep(2 * time.Second)&#125;if err !&#x3D; nil &#123;return fmt.Errorf(&quot;abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot the error: %v&quot;, err)&#125;ipNet, err :&#x3D; netlink.ParseIPNet(rawIP)if err !&#x3D; nil &#123;return err&#125;addr :&#x3D; &amp;netlink.Addr&#123;IPNet:     ipNet,Peer:      ipNet,Label:     &quot;&quot;,Flags:     0,Scope:     0,Broadcast: nil,&#125;return netlink.AddrAdd(iface, addr)&#125;</code></pre><h6 id="启动-bridge-设备-3">启动 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceUP(interfaceName string) error &#123;iface, err :&#x3D; netlink.LinkByName(interfaceName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;error retrieving a link named [ %s ]: %v&quot;, iface.Attrs().Name, err)&#125;if err :&#x3D; netlink.LinkSetUp(iface); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error enabling interface for %s: %v&quot;, interfaceName, err)&#125;return nil&#125;</code></pre><h6 id="设置-iptables-linux-bridge-snat-规则-3">设置 iptables LinuxBridge SNAT 规则</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setupIPTables(bridgeName string, subnet *net.IPNet) error &#123;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE&quot;, subnet.String(), bridgeName)cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)&#125;return err&#125;</code></pre><h5 id="bridge-driver-delete-实现-3">7.4.3 Bridge Driver Delete实现</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Delete(network Network) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;return netlink.LinkDel(br)&#125;</code></pre><h4 id="在-bridge-网络创建容器-3">7.5 在 bridge 网络创建容器</h4><h5 id="挂载容器端点-3">7.5.1 挂载容器端点</h5><h6 id="连接容器网络端点到-linux-bridge-3">连接容器网络端点到 LinuxBridge</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;la :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; endpoint.ID[:5]la.MasterIndex &#x3D; br.Attrs().Indexendpoint.Device &#x3D; netlink.Veth&#123;LinkAttrs: la,PeerName:  &quot;cif-&quot; + endpoint.ID[:5],&#125;if err &#x3D; netlink.LinkAdd(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;if err &#x3D; netlink.LinkSetUp(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;return nil&#125;</code></pre><h6 id="配置容器-namespace-中网络设备及路由-3">配置容器 Namespace中网络设备及路由</h6><p>回到 <code>network.go</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;peerLink, err :&#x3D; netlink.LinkByName(ep.Device.PeerName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;fail config endpoint: %v&quot;, err)&#125;defer enterContainerNetns(&amp;peerLink, cinfo)()interfaceIP :&#x3D; *ep.Network.IpRangeinterfaceIP.IP &#x3D; ep.IPAddressif err &#x3D; setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;%v,%s&quot;, ep.Network, err)&#125;if err &#x3D; setInterfaceUP(ep.Device.PeerName); err !&#x3D; nil &#123;return err&#125;if err &#x3D; setInterfaceUP(&quot;lo&quot;); err !&#x3D; nil &#123;return err&#125;_, cidr, _ :&#x3D; net.ParseCIDR(&quot;0.0.0.0&#x2F;0&quot;)defaultRoute :&#x3D; &amp;netlink.Route&#123;LinkIndex: peerLink.Attrs().Index,Gw:        ep.Network.IpRange.IP,Dst:       cidr,&#125;if err &#x3D; netlink.RouteAdd(defaultRoute); err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h6 id="进入容器-net-namespace-3">进入容器 Net Namespace</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() &#123;f, err :&#x3D; os.OpenFile(fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;net&quot;, cinfo.Pid), os.O_RDONLY, 0)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get container net namespace, %v&quot;, err)&#125;nsFD :&#x3D; f.Fd()runtime.LockOSThread()if err &#x3D; netlink.LinkSetNsFd(*enLink, int(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set link netns , %v&quot;, err)&#125;origns, err :&#x3D; netns.Get()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get current netns, %v&quot;, err)&#125;if err &#x3D; netns.Set(netns.NsHandle(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set netns, %v&quot;, err)&#125;return func() &#123;netns.Set(origns)origns.Close()runtime.UnlockOSThread()f.Close()&#125;&#125;</code></pre><h6 id="配置宿主机到容器的端口映射-3">配置宿主机到容器的端口映射</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configPortMapping(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;for _, pm :&#x3D; range ep.PortMapping &#123;portMapping :&#x3D; strings.Split(pm, &quot;:&quot;)if len(portMapping) !&#x3D; 2 &#123;logrus.Errorf(&quot;port mapping format error, %v&quot;, pm)continue&#125;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s&quot;,portMapping[0], ep.IPAddress.String(), portMapping[1])cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)continue&#125;&#125;return nil&#125;</code></pre><h5 id="修补-bug-3">7.5.2 修补 bug</h5><p>写到这里，代码还是有很多 bug的，例如，<code>BridgeNetworkDriver</code> 未完全继承<code>NetworkDriver</code> 的所有函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Disconnect(network Network, endpoint *Endpoint) error &#123;return nil&#125;</code></pre><h5 id="测试-3">7.5.3 测试</h5><p>现在终于可以测试了。</p><p>首先创建一个网桥：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . network create --driver bridge --subnet 192.168.10.1&#x2F;24 testbridge</code></pre><p>然后启动两个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;8116248511&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#x2F; # ifconfigcif-81162 Link encap:Ethernet  HWaddr 16:62:68:81:E0:A9          inet addr:192.168.10.2  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::1462:68ff:fe81:e0a9&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:14 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1820 (1.7 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; #</code></pre><p>记住这个 IP：<code>192.168.10.2</code>，然后进入另一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;9558830402&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#x2F; # ifconfigcif-95588 Link encap:Ethernet  HWaddr 42:18:0A:73:33:CA          inet addr:192.168.10.3  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::4018:aff:fe73:33ca&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:10 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1248 (1.2 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; # ping 192.168.10.2PING 192.168.10.2 (192.168.10.2): 56 data bytes64 bytes from 192.168.10.2: seq&#x3D;0 ttl&#x3D;64 time&#x3D;2.619 ms64 bytes from 192.168.10.2: seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.086 ms^C--- 192.168.10.2 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 0.086&#x2F;1.352&#x2F;2.619 ms&#x2F; #</code></pre><p>可以看到，两个容器网络互通。</p><p>下面来试一下访问外部网络。我用的 WSL，默认的 nat是关闭的，前期各种设置 iptables规则什么的，都无法访问容器外部的网络，直到发现一篇帖子里说到，需要打开内核的nat功能，要将文件<code>/proc/sys/net/ipv4/ip_forward</code>内的值改为1（默认是0）。执行<code>sysctl -w net.ipv4.ip_forward=1</code> 即可。</p><p>修改之后，继续测试。</p><p>容器默认是没有 DNS 服务器的，需要我们手动添加：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # ping cn.bing.comping: bad address &#39;cn.bing.com&#39;&#x2F; # echo -e &quot;nameserver 8.8.8.8&quot; &gt; &#x2F;etc&#x2F;resolv.conf&#x2F; # ping cn.bing.comPING cn.bing.com (202.89.233.101): 56 data bytes64 bytes from 202.89.233.101: seq&#x3D;0 ttl&#x3D;113 time&#x3D;38.419 ms64 bytes from 202.89.233.101: seq&#x3D;1 ttl&#x3D;113 time&#x3D;39.011 ms^C--- cn.bing.com ping statistics ---3 packets transmitted, 2 packets received, 33% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 38.419&#x2F;38.715&#x2F;39.011 ms&#x2F; #</code></pre><p>然后再来测试容器映射端口到宿主机供外部访问：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -p 90:90 -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;3445154844&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#x2F; # nc -lp 90</code></pre><p>然后访问宿主机的 80 端口，看看能不能转发到容器里：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 172.31.93.218 90Trying 172.31.93.218...telnet: Unable to connect to remote host: Connection refused</code></pre><p>开始我以为是我哪里码错了，然后拿作者的代码来跑，并放到虚拟机上跑，发现并不是自己的问题，那只能这样测试了：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 192.168.10.3 90Trying 192.168.10.3...Connected to 192.168.10.3.Escape character is &#39;^]&#39;.</code></pre><p>出现这样的字眼后，容器和宿主机之间就可以通信了。</p><h2 id="参考链接-3">参考链接</h2><p><a href="https://learnku.com/articles/42072">七天用 Go 写个docker（第一天） | Go 技术论坛 (learnku.com)</a></p><p><a href="https://juejin.cn/post/6971335828060504094">使用 GoLang从零开始写一个 Docker（概念篇）-- 《自己动手写 Docker》读书笔记 - 掘金(juejin.cn)</a></p><p><ahref="https://blog.xtlsoft.top/read/server/building-wsl-kernel-with-aufs.html">编译带有AUFS 支持的 WSL 内核 - 徐天乐 :: 个人博客 (xtlsoft.top)</a></p><p><ahref="https://zhuanlan.zhihu.com/p/324530180">如何让WSL2使用自己编译的内核- 知乎 (zhihu.com)</a></p><p><ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p><ahref="https://juejin.cn/post/7086069688664326157#heading-1">自己动手写Docker系列-- 5.7实现通过容器制作镜像 - 掘金 (juejin.cn)</a></p><p><ahref="https://blog.csdn.net/tycoon1988/article/details/40781291">iptable端口重定向MASQUERADE_tycoon1988的博客-CSDN博客</a></p><p>通过设置路由，对 IP地址的请求就能正确被路由到对应的网络设备上，从而实现通信：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># ifconfig eth0eth0: flags&#x3D;4163&lt;UP,BROADCAST,RUNNING,MULTICAST&gt;  mtu 1500        inet 172.31.93.218  netmask 255.255.240.0  broadcast 172.31.95.255        inet6 fe80::215:5dff:fe4e:a16a  prefixlen 64  scopeid 0x20&lt;link&gt;        ether 00:15:5d:4e:a1:6a  txqueuelen 1000  (Ethernet)        RX packets 829  bytes 394161 (394.1 KB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 90  bytes 10335 (10.3 KB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0########## 在namespace访问宿主机# ip netns exec ns1 ping -c 1 172.31.93.218PING 172.31.93.218 (172.31.93.218) 56(84) bytes of data.64 bytes from 172.31.93.218: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.556 ms--- 172.31.93.218 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.556&#x2F;0.556&#x2F;0.556&#x2F;0.000 ms######### 从宿主机访问namespace的网络地址# ping -c 1 172.18.0.2PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.64 bytes from 172.18.0.2: icmp_seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.113 ms--- 172.18.0.2 ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min&#x2F;avg&#x2F;max&#x2F;mdev &#x3D; 0.113&#x2F;0.113&#x2F;0.113&#x2F;0.000 ms</code></pre><h5 id="linux-iptables-4">7.1.3 Linux iptables</h5><p>iptables 是对 Linux 内核的 netfilter模块进行操作和展示的工具，用来管理包的流动和转送。iptables定义了一套链式处理的结构，在网络包传输的各个阶段可以使用不同的策略和包进行加工、传送或丢弃。在容器虚拟化技术里，常会用到两种策略，MASQUERADE和 DNAT，用于容器和宿主机外部的网络通信。</p><p><strong>MASQUERADE</strong></p><p>MASQUERADE 策略可以将请求包中的源地址转换为一个网络设备的地址，例如<a href="#7.1.2%20Linux%20路由表">7.1.2 Linux 路由表</a>这一小节里，namespace 中网络设备的地址是172.18.0.2，这个地址虽然在宿主机可以路由到 br0的网桥，但是到底宿主机外部后，是不知道如何路由到这个 IP的，所以如果请求外部地址的话，要先通过 MASQUERADE 策略将这个 IP转换为宿主机出口网卡的 IP：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># sysctl -w net.ipv4.conf.all.forwarding&#x3D;1net.ipv4.conf.all.forwarding &#x3D; 1# iptables -t nat -A POSTROUTING -s 172.18.0.0&#x2F;24 -o eth0 -j MASQUERADE</code></pre><p>在 namespace 中请求宿主机外部地址时，将 namespace中源地址转换为宿主机的地址作为源地址，就可以在 namespace中访问宿主机外的网络了。</p><p><strong>DAT</strong></p><p>iptables 中的 DNAT策略也是做网络地址的转换，不过它是要更换目标地址，常用于将内部网络地址的端口映射出去。例如，上面例子的namespace如果要提供服务给宿主机之外的应用要怎么办呢？外部应用没办法直接路由到172.18.0.2 这个地址，这时候可以用 DNAT 策略。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80</code></pre><p>这样就可以把宿主机上的 80 端口的 TCP 请求转发到 namespace 的172.18.0.2:80，从而实现外部应用的调用。</p><h4 id="构建容器网络模型-4">7.2 构建容器网络模型</h4><h5 id="基本模型-4">7.2.1 基本模型</h5><h6 id="网络-4">网络</h6><p>网络是容器的一个集合，在这个网络上的容器可以相互通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Network struct &#123;    Name    string &#x2F;&#x2F; network name    IpRange *net.IPNet &#x2F;&#x2F; address    Driver  string &#x2F;&#x2F; network driver name&#125;</code></pre><h6 id="网络端点-4">网络端点</h6><p>网络端点用于连接网络与容器，保证容器内部与网络的通信。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Endpoint struct &#123;ID          string           &#96;json:&quot;id&quot;&#96;Device      netlink.Veth     &#96;json:&quot;dev&quot;&#96;IPAddress   net.IP           &#96;json:&quot;ip&quot;&#96;MacAddress  net.HardwareAddr &#96;json:&quot;mac&quot;&#96;Network     *NetworkPortMapping []string&#125;</code></pre><p>网络端点的信息传输需要靠网络功能的两个组件配合完成，分别为网络驱动和IPAM。</p><h6 id="网络驱动-4">网络驱动</h6><p>网络驱动是网络功能的一个组件，不同驱动对网络的创建、连接、销毁策略不同，通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type NetworkDriver interface &#123;Name() string &#x2F;&#x2F; driver nameCreate(subnet string, name string) (*Network, error)Delete(network Network) errorConnect(network *Network, endpoint *Endpoint) errorDisconnect(network Network, endpoint *Endpoint) error&#125;</code></pre><h6 id="ipam-4">IPAM</h6><p>IPAM 也是网络功能的一个组件，用于网络 IP 地址的分配和释放，包括容器的IP 和网络网关的 IP。主要功能如下：</p><ul><li><code>ipam.Allocate(*net.IPNet)</code> 从指定的 subnet 网段中分配IP　</li><li><code>ipam.Release(*net.IPNet, net.IP)</code> 从指定的 subnet网段中释放掉指定的 IP</li></ul><p>在构建下面的函数之前，先来补充一些书上没写的：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var (defaultNetworkPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;network&#x2F;&quot; &#x2F;&#x2F; 默认网络配置信息存储位置drivers            &#x3D; map[string]NetworkDriver&#123;&#125; &#x2F;&#x2F; 驱动字典，存储驱动信息networks           &#x3D; map[string]*Network&#123;&#125; &#x2F;&#x2F; 网络字段，存储网络信息)</code></pre><h5 id="调用关系-4">7.2.2 调用关系</h5><h6 id="创建网络-4">创建网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CreateNetwork(driver, subnet, name string) error &#123;_, cidr, _ :&#x3D; net.ParseCIDR(subnet)    &#x2F;&#x2F; allocate gateway ip by IPAMgatewayIP, err :&#x3D; ipAllocator.Allocate(cidr)if err !&#x3D; nil &#123;return err&#125;cidr.IP &#x3D; gatewayIPnw, err :&#x3D; drivers[driver].Create(cidr.String(), name)if err !&#x3D; nil &#123;return err&#125;    &#x2F;&#x2F; save network inforeturn nw.dump(defaultNetworkPath)&#125;</code></pre><p>其中，network.dump 和 network.load方法是将这个网络的配置信息保存在文件系统中，或从网络的配置目录中的文件读取到网络的配置。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) dump(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(dumpPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(dumpPath, 0644)&#125; else &#123;return err&#125;&#125;nwPath :&#x3D; path.Join(dumpPath, nw.Name)    &#x2F;&#x2F; create file while empty file, write only, no filenwFile, err :&#x3D; os.OpenFile(nwPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;defer nwFile.Close()nwJson, err :&#x3D; json.Marshal(nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;_, err &#x3D; nwFile.Write(nwJson)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error: %v&quot;, err)return err&#125;return nil&#125;func (nw *Network) load(dumpPath string) error &#123;nwConfigFile, err :&#x3D; os.Open(dumpPath)if err !&#x3D; nil &#123;return err&#125;defer nwConfigFile.Close()nwJson :&#x3D; make([]byte, 2000)n, err :&#x3D; nwConfigFile.Read(nwJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(nwJson[:n], nw)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error load nw info: %v&quot;, err)return err&#125;return nil&#125;</code></pre><h6 id="创建容器并连接网络-4">创建容器并连接网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Connect(networkName string, cinfo *container.ContainerInfo) error &#123;network, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;ip, err :&#x3D; ipAllocator.Allocate(network.IpRange)if err !&#x3D; nil &#123;return err&#125;ep :&#x3D; &amp;Endpoint&#123;ID:          fmt.Sprintf(&quot;%s-%s&quot;, cinfo.Id, networkName),IPAddress:   ip,Network:     network,PortMapping: cinfo.PortMapping,&#125;if err &#x3D; drivers[network.Driver].Connect(network, ep); err !&#x3D; nil &#123;return err&#125;if err &#x3D; configEndpointIpAddressAndRoute(ep, cinfo); err !&#x3D; nil &#123;return err&#125;return configPortMapping(ep, cinfo)&#125;</code></pre><h6 id="展示网络列表-4">展示网络列表</h6><p>从网络配置的目录中加载所有的网络配置信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Init() error &#123;var bridgeDriver &#x3D; BridgeNetworkDriver&#123;&#125;drivers[bridgeDriver.Name()] &#x3D; &amp;bridgeDriverif _, err :&#x3D; os.Stat(defaultNetworkPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(defaultNetworkPath, 0644)&#125; else &#123;return err&#125;&#125;filepath.Walk(defaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error &#123;         &#x2F;&#x2F; skip if dirif info.IsDir() &#123;return nil&#125;if strings.HasSuffix(nwPath, &quot;&#x2F;&quot;) &#123;return nil&#125;         &#x2F;&#x2F; load filename as network name_, nwName :&#x3D; path.Split(nwPath)nw :&#x3D; &amp;Network&#123;Name: nwName,&#125;if err :&#x3D; nw.load(nwPath); err !&#x3D; nil &#123;logrus.Errorf(&quot;error load network: %s&quot;, err)&#125;&#x2F;&#x2F; save network info to network dicnetworks[nwName] &#x3D; nwreturn nil&#125;)return nil&#125;</code></pre><p>遍历展示创建的网络：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListNetwork() &#123;w :&#x3D; tabwriter.NewWriter(os.Stdout, 12, 1, 3, &#39; &#39;, 0)fmt.Fprint(w, &quot;NAME\tIpRange\tDriver\n&quot;)for _, nw :&#x3D; range networks &#123;fmt.Fprintf(w, &quot;%s\t%s\t%s\n&quot;,nw.Name,nw.IpRange.String(),nw.Driver,)&#125;if err :&#x3D; w.Flush(); err !&#x3D; nil &#123;logrus.Errorf(&quot;Flush error %v&quot;, err)return&#125;&#125;</code></pre><h6 id="删除网络-4">删除网络</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DeleteNetwork(networkName string) error &#123;nw, ok :&#x3D; networks[networkName]if !ok &#123;return fmt.Errorf(&quot;no Such Network: %s&quot;, networkName)&#125;if err :&#x3D; ipAllocator.Release(nw.IpRange, &amp;nw.IpRange.IP); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network gateway ip: %s&quot;, err)&#125;if err :&#x3D; drivers[nw.Driver].Delete(*nw); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Remove Network DriverError: %s&quot;, err)&#125;return nw.remove(defaultNetworkPath)&#125;</code></pre><p>删除网络的同时也删除配置目录的网络配置文件：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (nw *Network) remove(dumpPath string) error &#123;if _, err :&#x3D; os.Stat(path.Join(dumpPath, nw.Name)); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125; else &#123;return os.Remove(path.Join(dumpPath, nw.Name))&#125;&#125;</code></pre><h4 id="容器地址分配-4">7.3 容器地址分配</h4><p>现在转到 <code>ipam.go</code>。</p><h5 id="数据结构定义-4">7.3.1 数据结构定义</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">const ipamDefaultAllocatorPath &#x3D; &quot;&#x2F;var&#x2F;run&#x2F;simple-docker&#x2F;network&#x2F;ipam&#x2F;subnet.json&quot;type IPAM struct &#123;SubnetAllocatorPath stringSubnets             *map[string]string&#125;&#x2F;&#x2F; 初始化一个IPAM对象，并指定默认分配信息存储位置var ipAllocator &#x3D; &amp;IPAM&#123;SubnetAllocatorPath: ipamDefaultAllocatorPath,&#125;</code></pre><p>反序列化读取网段分配信息和序列化保存网段分配信息：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) load() error &#123;if _, err :&#x3D; os.Stat(ipam.SubnetAllocatorPath); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;return nil&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.Open(ipam.SubnetAllocatorPath)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()subnetJson :&#x3D; make([]byte, 2000)n, err :&#x3D; subnetConfigFile.Read(subnetJson)if err !&#x3D; nil &#123;return err&#125;err &#x3D; json.Unmarshal(subnetJson[:n], ipam.Subnets)if err !&#x3D; nil &#123;logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)return err&#125;return nil&#125;func (ipam *IPAM) dump() error &#123;ipamConfigFileDir, _ :&#x3D; path.Split(ipam.SubnetAllocatorPath)if _, err :&#x3D; os.Stat(ipamConfigFileDir); err !&#x3D; nil &#123;if os.IsNotExist(err) &#123;os.MkdirAll(ipamConfigFileDir, 0644)&#125; else &#123;return err&#125;&#125;subnetConfigFile, err :&#x3D; os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)if err !&#x3D; nil &#123;return err&#125;defer subnetConfigFile.Close()ipamConfigJson, err :&#x3D; json.Marshal(ipam.Subnets)if err !&#x3D; nil &#123;return err&#125;_, err &#x3D; subnetConfigFile.Write(ipamConfigJson)if err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h5 id="地址分配-4">7.3.2 地址分配</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) &#123;ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;err &#x3D; ipam.load()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error dump allocation info, %v&quot;, err)&#125;_, subnet, _ &#x3D; net.ParseCIDR(subnet.String())one, size :&#x3D; subnet.Mask.Size()if _, exist :&#x3D; (*ipam.Subnets)[subnet.String()]; !exist &#123;        &#x2F;&#x2F; 用0填满网段的配置，1&lt;&lt;uint8(size-one)表示这个网段中有多少个可用地址        &#x2F;&#x2F; size-one时子网掩码后面的网络位数，2^(size-one)表示网段中的可用IP数        &#x2F;&#x2F; 2^(size-one)等价于1&lt;&lt;uint8(size-one)        (*ipam.Subnets)[subnet.String()] &#x3D; strings.Repeat(&quot;0&quot;, 1&lt;&lt;uint8(size-one))&#125;&#x2F;&#x2F; 这里的原理建议大家看看原著for c :&#x3D; range (*ipam.Subnets)[subnet.String()] &#123;if (*ipam.Subnets)[subnet.String()][c] &#x3D;&#x3D; &#39;0&#39; &#123;            ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])            &#x2F;&#x2F; go的字符串创建后不能修改，先用byte存储            ipalloc[c] &#x3D; &#39;1&#39;            (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)            &#x2F;&#x2F;             ip &#x3D; subnet.IP                        &#x2F;&#x2F; 通过网段的IP与上面的偏移相加得出分配的IP，由于IP是一个uint的一个数组，需要通过数组中的每一项加所需要的值，例 &#x2F;&#x2F; 如网段是172.16.0.0&#x2F;12，数组序号是65555，那就要在[172,16,0,0]上依次加            &#x2F;&#x2F; [uint8(65555 &gt;&gt; 24), uint8(65555 &gt;&gt; 16), uint8(65555 &gt;&gt; 8), uint(65555 &gt;&gt; 4)]，即[0,1,0,19]，            &#x2F;&#x2F; 那么获得的IP就是172.17.0.19            for t :&#x3D; uint(4); t &gt; 0; t-- &#123;                []byte(ip)[4-t] +&#x3D; uint8(c &gt;&gt; ((t - 1) * 8))            &#125;            &#x2F;&#x2F; 由于此处IP是从1开始分配的，所以最后再加1，最终得到分配的IP是172.16.0.20            ip[3]++            break&#125;&#125;ipam.dump()return&#125;</code></pre><h5 id="地址释放-4">7.3.3 地址释放</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error &#123;    ipam.Subnets &#x3D; &amp;map[string]string&#123;&#125;    _, subnet, _ &#x3D; net.ParseCIDR(subnet.String())    err :&#x3D; ipam.load()    if err !&#x3D; nil &#123;        logrus.Errorf(&quot;Error dump allocation info, %v&quot;, err)    &#125;    c :&#x3D; 0    &#x2F;&#x2F; 将IP转换为4个字节的表示方式    releaseIP :&#x3D; ipaddr.To4()    &#x2F;&#x2F; 由于IP是从1开始分配的，所以转换成索引减1    releaseIP[3] -&#x3D; 1    for t :&#x3D; uint(4); t &gt; 0; t -&#x3D; 1 &#123;        &#x2F;&#x2F; 和分配IP相反，释放IP获得索引的方式是IP的每一位相减后分别左移将对应的数值加到索引上        c +&#x3D; int(releaseIP[t-1]-subnet.IP[t-1]) &lt;&lt; ((4 - t) * 8)    &#125;    ipalloc :&#x3D; []byte((*ipam.Subnets)[subnet.String()])    ipalloc[c] &#x3D; &#39;0&#39;    (*ipam.Subnets)[subnet.String()] &#x3D; string(ipalloc)    ipam.dump()    return nil&#125;</code></pre><p>根据书上，写到这里就开始测试了，但是我们看看IDE，红海一片，所以我们接着实现。</p><h4 id="创建-bridge-网络-4">7.4 创建 bridge 网络</h4><h5 id="实现-bridge-driver-create-4">7.4.1 实现 Bridge DriverCreate</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) &#123;ip, ipRange, _ :&#x3D; net.ParseCIDR(subnet)ipRange.IP &#x3D; ipn :&#x3D; &amp;Network&#123;Name:    name,IpRange: ipRange,Driver:  d.Name(),&#125;err :&#x3D; d.initBridge(n)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error init bridge: %v&quot;, err)&#125;return n, err&#125;</code></pre><h5 id="bridge-driver-初始化-linux-bridge-4">7.4.2 Bridge Driver 初始化Linux Bridge</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) initBridge(n *Network) error &#123;&#x2F;&#x2F; 创建bridge虚拟设备bridgeName :&#x3D; n.Nameif err :&#x3D; createBridgeInterface(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;eror add bridge: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置bridge设备的地址和路由gatewayIP :&#x3D; *n.IpRangegatewayIP.IP &#x3D; n.IpRange.IPif err :&#x3D; setInterfaceIP(bridgeName, gatewayIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error assigning address: %s on bridge: %s with an error of: %v&quot;, gatewayIP, bridgeName, err)&#125;&#x2F;&#x2F; 启动bridge设备if err :&#x3D; setInterfaceUP(bridgeName); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error set bridge up: %s, error: %v&quot;, bridgeName, err)&#125;&#x2F;&#x2F; 设置iptables的SNAT规则if err :&#x3D; setupIPTables(bridgeName, n.IpRange); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error setting iptables for %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="创建-bridge-设备-4">创建 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func createBridgeInterface(bridgeName string) error &#123;_, err :&#x3D; net.InterfaceByName(bridgeName)if err &#x3D;&#x3D; nil || !strings.Contains(err.Error(), &quot;no such network interface&quot;) &#123;return err&#125;&#x2F;&#x2F; create *netlink.Bridge objectla :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; bridgeNamebr :&#x3D; &amp;netlink.Bridge&#123;LinkAttrs: la&#125;if err :&#x3D; netlink.LinkAdd(br); err !&#x3D; nil &#123;return fmt.Errorf(&quot;bridge creation failed for bridge %s: %v&quot;, bridgeName, err)&#125;return nil&#125;</code></pre><h6 id="设置-bridge-设备的地址和路由-4">设置 bridge设备的地址和路由</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceIP(name string, rawIP string) error &#123;retries :&#x3D; 2var iface netlink.Linkvar err errorfor i :&#x3D; 0; i &lt; retries; i++ &#123;iface, err &#x3D; netlink.LinkByName(name)if err &#x3D;&#x3D; nil &#123;break&#125;logrus.Debugf(&quot;error retrieving new bridge netlink link [ %s ]... retrying&quot;, name)time.Sleep(2 * time.Second)&#125;if err !&#x3D; nil &#123;return fmt.Errorf(&quot;abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot the error: %v&quot;, err)&#125;ipNet, err :&#x3D; netlink.ParseIPNet(rawIP)if err !&#x3D; nil &#123;return err&#125;addr :&#x3D; &amp;netlink.Addr&#123;IPNet:     ipNet,Peer:      ipNet,Label:     &quot;&quot;,Flags:     0,Scope:     0,Broadcast: nil,&#125;return netlink.AddrAdd(iface, addr)&#125;</code></pre><h6 id="启动-bridge-设备-4">启动 bridge 设备</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setInterfaceUP(interfaceName string) error &#123;iface, err :&#x3D; netlink.LinkByName(interfaceName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;error retrieving a link named [ %s ]: %v&quot;, iface.Attrs().Name, err)&#125;if err :&#x3D; netlink.LinkSetUp(iface); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error enabling interface for %s: %v&quot;, interfaceName, err)&#125;return nil&#125;</code></pre><h6 id="设置-iptables-linux-bridge-snat-规则-4">设置 iptables LinuxBridge SNAT 规则</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func setupIPTables(bridgeName string, subnet *net.IPNet) error &#123;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE&quot;, subnet.String(), bridgeName)cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)&#125;return err&#125;</code></pre><h5 id="bridge-driver-delete-实现-4">7.4.3 Bridge Driver Delete实现</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Delete(network Network) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;return netlink.LinkDel(br)&#125;</code></pre><h4 id="在-bridge-网络创建容器-4">7.5 在 bridge 网络创建容器</h4><h5 id="挂载容器端点-4">7.5.1 挂载容器端点</h5><h6 id="连接容器网络端点到-linux-bridge-4">连接容器网络端点到 LinuxBridge</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error &#123;bridgeName :&#x3D; network.Namebr, err :&#x3D; netlink.LinkByName(bridgeName)if err !&#x3D; nil &#123;return err&#125;la :&#x3D; netlink.NewLinkAttrs()la.Name &#x3D; endpoint.ID[:5]la.MasterIndex &#x3D; br.Attrs().Indexendpoint.Device &#x3D; netlink.Veth&#123;LinkAttrs: la,PeerName:  &quot;cif-&quot; + endpoint.ID[:5],&#125;if err &#x3D; netlink.LinkAdd(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;if err &#x3D; netlink.LinkSetUp(&amp;endpoint.Device); err !&#x3D; nil &#123;return fmt.Errorf(&quot;error Add Endpoint Device: %v&quot;, err)&#125;return nil&#125;</code></pre><h6 id="配置容器-namespace-中网络设备及路由-4">配置容器 Namespace中网络设备及路由</h6><p>回到 <code>network.go</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;peerLink, err :&#x3D; netlink.LinkByName(ep.Device.PeerName)if err !&#x3D; nil &#123;return fmt.Errorf(&quot;fail config endpoint: %v&quot;, err)&#125;defer enterContainerNetns(&amp;peerLink, cinfo)()interfaceIP :&#x3D; *ep.Network.IpRangeinterfaceIP.IP &#x3D; ep.IPAddressif err &#x3D; setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err !&#x3D; nil &#123;return fmt.Errorf(&quot;%v,%s&quot;, ep.Network, err)&#125;if err &#x3D; setInterfaceUP(ep.Device.PeerName); err !&#x3D; nil &#123;return err&#125;if err &#x3D; setInterfaceUP(&quot;lo&quot;); err !&#x3D; nil &#123;return err&#125;_, cidr, _ :&#x3D; net.ParseCIDR(&quot;0.0.0.0&#x2F;0&quot;)defaultRoute :&#x3D; &amp;netlink.Route&#123;LinkIndex: peerLink.Attrs().Index,Gw:        ep.Network.IpRange.IP,Dst:       cidr,&#125;if err &#x3D; netlink.RouteAdd(defaultRoute); err !&#x3D; nil &#123;return err&#125;return nil&#125;</code></pre><h6 id="进入容器-net-namespace-4">进入容器 Net Namespace</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() &#123;f, err :&#x3D; os.OpenFile(fmt.Sprintf(&quot;&#x2F;proc&#x2F;%s&#x2F;ns&#x2F;net&quot;, cinfo.Pid), os.O_RDONLY, 0)if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get container net namespace, %v&quot;, err)&#125;nsFD :&#x3D; f.Fd()runtime.LockOSThread()if err &#x3D; netlink.LinkSetNsFd(*enLink, int(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set link netns , %v&quot;, err)&#125;origns, err :&#x3D; netns.Get()if err !&#x3D; nil &#123;logrus.Errorf(&quot;error get current netns, %v&quot;, err)&#125;if err &#x3D; netns.Set(netns.NsHandle(nsFD)); err !&#x3D; nil &#123;logrus.Errorf(&quot;error set netns, %v&quot;, err)&#125;return func() &#123;netns.Set(origns)origns.Close()runtime.UnlockOSThread()f.Close()&#125;&#125;</code></pre><h6 id="配置宿主机到容器的端口映射-4">配置宿主机到容器的端口映射</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func configPortMapping(ep *Endpoint, cinfo *container.ContainerInfo) error &#123;for _, pm :&#x3D; range ep.PortMapping &#123;portMapping :&#x3D; strings.Split(pm, &quot;:&quot;)if len(portMapping) !&#x3D; 2 &#123;logrus.Errorf(&quot;port mapping format error, %v&quot;, pm)continue&#125;iptablesCmd :&#x3D; fmt.Sprintf(&quot;-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s&quot;,portMapping[0], ep.IPAddress.String(), portMapping[1])cmd :&#x3D; exec.Command(&quot;iptables&quot;, strings.Split(iptablesCmd, &quot; &quot;)...)&#x2F;&#x2F;err :&#x3D; cmd.Run()output, err :&#x3D; cmd.Output()if err !&#x3D; nil &#123;logrus.Errorf(&quot;iptables Output, %v&quot;, output)continue&#125;&#125;return nil&#125;</code></pre><h5 id="修补-bug-4">7.5.2 修补 bug</h5><p>写到这里，代码还是有很多 bug的，例如，<code>BridgeNetworkDriver</code> 未完全继承<code>NetworkDriver</code> 的所有函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *BridgeNetworkDriver) Disconnect(network Network, endpoint *Endpoint) error &#123;return nil&#125;</code></pre><h5 id="测试-4">7.5.3 测试</h5><p>现在终于可以测试了。</p><p>首先创建一个网桥：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . network create --driver bridge --subnet 192.168.10.1&#x2F;24 testbridge</code></pre><p>然后启动两个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;8116248511&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:24:53+08:00&quot;&#125;&#x2F; # ifconfigcif-81162 Link encap:Ethernet  HWaddr 16:62:68:81:E0:A9          inet addr:192.168.10.2  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::1462:68ff:fe81:e0a9&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:14 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1820 (1.7 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; #</code></pre><p>记住这个 IP：<code>192.168.10.2</code>，然后进入另一个容器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;9558830402&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:26:24+08:00&quot;&#125;&#x2F; # ifconfigcif-95588 Link encap:Ethernet  HWaddr 42:18:0A:73:33:CA          inet addr:192.168.10.3  Bcast:192.168.10.255  Mask:255.255.255.0          inet6 addr: fe80::4018:aff:fe73:33ca&#x2F;64 Scope:Link          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:10 errors:0 dropped:0 overruns:0 frame:0          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:1248 (1.2 KiB)  TX bytes:516 (516.0 B)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          inet6 addr: ::1&#x2F;128 Scope:Host          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)&#x2F; # ping 192.168.10.2PING 192.168.10.2 (192.168.10.2): 56 data bytes64 bytes from 192.168.10.2: seq&#x3D;0 ttl&#x3D;64 time&#x3D;2.619 ms64 bytes from 192.168.10.2: seq&#x3D;1 ttl&#x3D;64 time&#x3D;0.086 ms^C--- 192.168.10.2 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 0.086&#x2F;1.352&#x2F;2.619 ms&#x2F; #</code></pre><p>可以看到，两个容器网络互通。</p><p>下面来试一下访问外部网络。我用的 WSL，默认的 nat是关闭的，前期各种设置 iptables规则什么的，都无法访问容器外部的网络，直到发现一篇帖子里说到，需要打开内核的nat功能，要将文件<code>/proc/sys/net/ipv4/ip_forward</code>内的值改为1（默认是0）。执行<code>sysctl -w net.ipv4.ip_forward=1</code> 即可。</p><p>修改之后，继续测试。</p><p>容器默认是没有 DNS 服务器的，需要我们手动添加：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">&#x2F; # ping cn.bing.comping: bad address &#39;cn.bing.com&#39;&#x2F; # echo -e &quot;nameserver 8.8.8.8&quot; &gt; &#x2F;etc&#x2F;resolv.conf&#x2F; # ping cn.bing.comPING cn.bing.com (202.89.233.101): 56 data bytes64 bytes from 202.89.233.101: seq&#x3D;0 ttl&#x3D;113 time&#x3D;38.419 ms64 bytes from 202.89.233.101: seq&#x3D;1 ttl&#x3D;113 time&#x3D;39.011 ms^C--- cn.bing.com ping statistics ---3 packets transmitted, 2 packets received, 33% packet lossround-trip min&#x2F;avg&#x2F;max &#x3D; 38.419&#x2F;38.715&#x2F;39.011 ms&#x2F; #</code></pre><p>然后再来测试容器映射端口到宿主机供外部访问：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># go run . run -it -p 90:90 -net testbridge busybox sh&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Start initiating...&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;whole init command is: sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Current location is &#x2F;root&#x2F;mnt&#x2F;3445154844&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#123;&quot;level&quot;:&quot;info&quot;,&quot;msg&quot;:&quot;Find path: &#x2F;bin&#x2F;sh&quot;,&quot;time&quot;:&quot;2023-05-20T19:39:07+08:00&quot;&#125;&#x2F; # nc -lp 90</code></pre><p>然后访问宿主机的 80 端口，看看能不能转发到容器里：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 172.31.93.218 90Trying 172.31.93.218...telnet: Unable to connect to remote host: Connection refused</code></pre><p>开始我以为是我哪里码错了，然后拿作者的代码来跑，并放到虚拟机上跑，发现并不是自己的问题，那只能这样测试了：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># telnet 192.168.10.3 90Trying 192.168.10.3...Connected to 192.168.10.3.Escape character is &#39;^]&#39;.</code></pre><p>出现这样的字眼后，容器和宿主机之间就可以通信了。</p><h2 id="参考链接-4">参考链接</h2><p><a href="https://learnku.com/articles/42072">七天用 Go 写个docker（第一天） | Go 技术论坛 (learnku.com)</a></p><p><a href="https://juejin.cn/post/6971335828060504094">使用 GoLang从零开始写一个 Docker（概念篇）-- 《自己动手写 Docker》读书笔记 - 掘金(juejin.cn)</a></p><p><ahref="https://blog.xtlsoft.top/read/server/building-wsl-kernel-with-aufs.html">编译带有AUFS 支持的 WSL 内核 - 徐天乐 :: 个人博客 (xtlsoft.top)</a></p><p><ahref="https://zhuanlan.zhihu.com/p/324530180">如何让WSL2使用自己编译的内核- 知乎 (zhihu.com)</a></p><p><ahref="https://blog.csdn.net/fangford/article/details/107728458">goland时间格式化time.Now().Format_golangtime.now().format_好狗不见的博客-CSDN博客</a></p><p><ahref="https://juejin.cn/post/7086069688664326157#heading-1">自己动手写Docker系列-- 5.7实现通过容器制作镜像 - 掘金 (juejin.cn)</a></p><p><ahref="https://blog.csdn.net/tycoon1988/article/details/40781291">iptable端口重定向MASQUERADE_tycoon1988的博客-CSDN博客</a></p>]]></content>
    
    
    <summary type="html">从零实现一个Docker</summary>
    
    
    
    <category term="BackEnd" scheme="https://jaydenchang.top/categories/BackEnd/"/>
    
    
    <category term="技术" scheme="https://jaydenchang.top/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Linux" scheme="https://jaydenchang.top/tags/Linux/"/>
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>mit 6.824 lab1 思路贴</title>
    <link href="https://jaydenchang.top/post/0x0034.html"/>
    <id>https://jaydenchang.top/post/0x0034.html</id>
    <published>2023-02-05T16:00:00.000Z</published>
    <updated>2023-02-06T12:16:36.747Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言">前言</h4><p>为遵守 mit 的约定，这个帖子不贴太多具体的代码，主要聊聊自己在码代码时的一些想法和遇到的问题。</p><p>这个实验需要我们去实现一个 map-reduce 的功能。实质上，这个实验分为两个大的板块，map 和 reduce 两个阶段，也就是这个实验的核心部分，两个阶段都包含若干小的子任务，然后用户通过编写 map 和 reduce 函数。这个实验里，我们的任务是，读取 main 文件夹下的八个 txt 文档，扫描其中的单词，并计数，将结果输出到若干个子文件中，最后的话，测试脚本会读取这八个文件，把里面的结果输出到另一个 txt 中并进行排序，比对给出的标准答案，来评判该实验是否通过。</p><p>做这个实验的前提是，已经读过这个实验配套的论文：<a href="http://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf">mapreduce-osdi04.pdf (googleusercontent.com)</a> <del>知道这个实验以及想要做这个实验的人多少都会有点手段上谷歌</del>（当然，也可以去找国内转载的，看不懂的话就看中文的吧，实验文档也是。</p><h4 id="getting-started">Getting Started</h4><p>当我们打开这个项目工程，我们阅读这个项目的所有文件，以及 lab1 中给出的提示，我们可以先试着运行以下部分代码，来看看我们最重要得到什么结果：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"># 当前目录为 src&#x2F;maingo build -race -buildmode&#x3D;plugin ..&#x2F;mrapps&#x2F;wc.gorm -rf mr-out*go run -race mrsequential.go wc.so pg*.txtmore mr-out-0</code></pre><p>这个是一个单线程的 map-reduce，我们可以查看 <code>mrsequential.go</code> 的内容，大概了解下整个 map-reduce 的过程是怎样的。</p><p>然后，我们把目光聚集到以下文件中：</p><pre class="line-numbers language-none"><code class="language-none">---main |---mrworker.go |---mrcoordinator.go |---mrsequential.go---mr |---worker.go |---coordinator.go |---rpc.go---mrapp |---wc.go</code></pre><p>后面我们在这个实验中很多内容都要参考这些文件的内容，其中包含一些函数的来源，其中，尤其 <code>main/mrsequential.go</code> 尤其重要。</p><h4 id="实现-rpc-通信">实现 rpc 通信</h4><p>如果说想要实现 map-reduce，那么第一步就是实现 worker 和 coordinator 的 rpc 通信，观察 mr 目录下的文件后，我们需要在 <code>rpc.go</code> 和 <code>coordinator.go</code> 中定义以下结构体 (目前仅实现 rpc 通信)：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; coordinator.go&#x2F;&#x2F; 专门定义一个Task，用于coordinator向worker分发任务type Task struct &#123;    FileName string&#125;&#x2F;&#x2F; 这里声明coordinator相关的结构体type Coordinator struct &#123;    task Task&#125;&#x2F;&#x2F; rpc.go&#x2F;&#x2F; 这里参考了上面的两个Exampletype TaskRequest struct &#123;    X int&#125;type TaskReply struct &#123;    XTask Task     &#125;</code></pre><p>下一步要做的是，需要让 coordinator 和 worker 之间能够进行 rpc 通信。</p><p>实现两者之间的通信是完成这个实验的基础。</p><h4 id="worker接收消息">worker接收消息</h4><p>worker 调用 coordinator 的获取任务函数，获取要处理的文件名，然后执行打开操作。</p><p>在构建中间体 <code>intermediate</code> 时，可以留意到 <code>mrsequential.go</code> 有提示：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; a big difference from real MapReduce is that all the&#x2F;&#x2F; intermediate data is in one place, intermediate[],&#x2F;&#x2F; rather than being partitioned into NxM buckets.</code></pre><p>根据这个思路，相当于提示我们，在构建桶存放中间体时，可能会用到二维 NxM 的数组。</p><p>然后经过 map 处理后的键值对切片，需要进一步经过 json 处理，并且将这个结果分成 nReduce 份，存放的文件命名规则是 <code>mr-X-Y</code>，其中 x 是 map 任务的序号，y 是 reduce 任务的序号。</p><p>在进行 reduce 任务时，读取结果也需要经过 json 处理，这里很多步骤都可以借鉴 <code>mrsequential.go</code>，包括读取文件等。</p><p>在创建目标文件时，可以使用 <code>ioutil.TempFile</code> 来创建临时文件，最后再重新命名。</p><p>此阶段的结构体声明如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; coordinator.gotype Coordinator struct &#123;    State            int &#x2F;&#x2F; 0 map 1 reduce 2 none    MapTask          Task    ReduceTask       Task    NumMapTask       int    NumReduceTask    int    MapTaskFinish    chan bool    ReduceTaskFinish chan bool&#125;type Task struct &#123;    FileName string    IDMap    int    IDReduce string&#125;&#x2F;&#x2F; rpc.gotype TaskRequest struct &#123;    X int&#125;type TaskReply struct &#123;    XTask            Task    NumMapTask       int    NumReduceTask    int    CurNumMapTask    int    CurNumReduceTask int&#125;</code></pre><h4 id="实现-rpc-通信-1">实现 rpc 通信</h4><p>根据文档的指引，我们首先要实现 coordinator 和 worker 之间的通信，我们看到 <code>worker.go</code> 中有 call 和 CallExample 两个函数，那也照葫芦画瓢，自己搞一个 CallGetTask，实现 rpc 通信。</p><h4 id="worker-申领-task">Worker 申领 task</h4><p>看着 <code>Worker()</code> 里的注释，有一行 <code>CallExample()</code>，是需要我们在这个函数里调用自定义的 CallGetTask 函数来获取 coordinator 分发的 task，在 call 之前，我们先要给 coordinator 的成员 MapTask 初始化，在 <code>MakeCoordinator</code> 中，我们可以看到 files 和 nReduce 这两个参数，那就从这两个入手，进行简单的初始化后，我们尝试在 Worker 中输出，能够输出文件名就是阶段性胜利。</p><h4 id="照抄-mrsequential.go">照抄 <code>mrsequential.go</code></h4><p>文档中有提到，可以随意借鉴 mrsequential 中的函数，那么，走起。不过也要看看注释和文档，可以创建一个 NxM 的桶，和利用 encoder 和 decoder 来处理中间产物。</p><h4 id="向-coordinator-的报告">向 coordinator 的报告</h4><p>每执行完一个任务，就向 coordinator 报告，方便 coordinator 记录，当所有任务都执行完时，修改 <code>Done</code> 中的条件，解除阻塞。</p><p>其实，走到这一步，可以说这个 lab 完成一半了，剩下就是各种断点打印 debug。</p><p>如果在执行过程提示无法打开文件，那说明，map 或 reduce 任务完成个数的条件没有控制好，<code>mrsequential.go</code> 中规定了一共会生成 3 个 workers，无法打开文件，只可能是，并发申请 task 时，已经快要到 task 的容量数，没分配到的 worker 自然也就没有分配到 FileName 和 MapID，所以需要设置好这些控制条件</p><h4 id="解决-crash">解决 crash</h4><p>做完上面，7 个 test 就可以 pass 6 个了，剩下一个 crash 的，需要用到锁或原子变量方面的知识。在进行 GetTask 时，我们传递的参数，需要确保其原子性，不然会出现 data race 现象；同时，也要对超过 10s 的任务进行舍弃处理，这里我们加一个时间戳，来记录任务的完成情况和开始时间。</p><p>又考虑到在记录任务完成情况时，是一个并发状态，这里考虑使用 <code>sync.Map</code>。在进行最后的 <code>Done</code> 之前，我们还要再定义一个检查函数，来遍历检查是否还有 crash 的任务。</p><h4 id="参考链接">参考链接</h4><p><a href="https://pdos.csail.mit.edu/6.824/labs/lab-mr.html">6.5840 Lab 1: MapReduce (mit.edu)</a></p><p><a href="https://www.bilibili.com/video/BV1Ft4y1a7cM/?spm_id_from=333.999.0.0&amp;vd_source=1724be72d1eb9a071146bb4df19bd7f6">mit6.824分布式lab1-MapReduce（1）_哔哩哔哩_bilibili</a></p>]]></content>
    
    
    <summary type="html">浅浅记录下 6.824 lab1 思路记录</summary>
    
    
    
    <category term="BackEnd" scheme="https://jaydenchang.top/categories/BackEnd/"/>
    
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>Vue+echart 展示后端获取的数据</title>
    <link href="https://jaydenchang.top/post/0x0033.html"/>
    <id>https://jaydenchang.top/post/0x0033.html</id>
    <published>2023-01-16T16:00:00.000Z</published>
    <updated>2023-01-17T09:41:55.794Z</updated>
    
    <content type="html"><![CDATA[<p>最近在合作做一个前后端分离项目时，为了测试我写的后端部分获取数据的效果，自己也学了一下 vue 的知识，在获取 json 信息这里也踩了很多坑。</p><p>这里列举下我返回的 json 部分信息：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">&#123;  &quot;house_basic&quot;: [    &#123;      &quot;HOUSE_ID&quot;: &quot;00001&quot;,      &quot;HOUSE_NAME&quot;: &quot;盈翠华庭122A户型&quot;,      &quot;HOUSE_AREA&quot;: &quot;122&quot;,      &quot;HOUSE_STATE&quot;: 0,      &quot;HOUSE_SPECIAL&quot;: &quot;采光好，南北通透&quot;    &#125;,    &#123;      &quot;HOUSE_ID&quot;: &quot;00002&quot;,      &quot;HOUSE_NAME&quot;: &quot;北海中心中间户&quot;,      &quot;HOUSE_AREA&quot;: &quot;92&quot;,      &quot;HOUSE_STATE&quot;: 0,      &quot;HOUSE_SPECIAL&quot;: &quot;采光好，客厅朝南&quot;    &#125;  ]&#125;</code></pre><p>vue 的 script 部分：</p><pre class="line-numbers language-vue" data-language="vue"><code class="language-vue">&lt;script&gt;&#x2F;&#x2F; 基本的script部分框架import axios from &#39;axios&#39;;export default &#123;    created() &#123;        axios.get(&#39;http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;vote&#x2F;api&#39;)        .then((res) &#x3D; &gt; &#123;            console.log(res);        &#125;)    &#125;&#125;&lt;&#x2F;script&gt;    </code></pre><p>我们打印一下 <code>res.data</code>，得到的是：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">&#123;    &#123;        &quot;score&quot;: [        &#123;            &quot;HOUSE_ID&quot;: &quot;00001&quot;,            &quot;HOUSE_VOTE&quot;: 5,            &quot;HOUSE_NAME&quot;: &quot;盈翠华庭122A户型&quot;        &#125;,        &#123;            &quot;HOUSE_ID&quot;: &quot;00002&quot;,            &quot;HOUSE_VOTE&quot;: 22,            &quot;HOUSE_NAME&quot;: &quot;北海中心中间户&quot;        &#125;    ]&#125;，&#x2F;&#x2F; 略过不重要信息&#125;</code></pre><p>我们再打印 <code>res.data.score</code>，这才得到了我们想要的数组：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">[    &#123;      &quot;HOUSE_ID&quot;: &quot;00001&quot;,      &quot;HOUSE_VOTE&quot;: 5,      &quot;HOUSE_NAME&quot;: &quot;盈翠华庭122A户型&quot;    &#125;,    &#123;      &quot;HOUSE_ID&quot;: &quot;00002&quot;,      &quot;HOUSE_VOTE&quot;: 22,      &quot;HOUSE_NAME&quot;: &quot;北海中心中间户&quot;    &#125;]</code></pre><p>输出其中一条的子条目看看 <code>res.data.score[0].HOUSE_ID</code>：<code>00001</code>。</p><p>在搞清楚返回的 data 后，就可以来写 script 部分获取，保存数据了。</p><pre class="line-numbers language-vue" data-language="vue"><code class="language-vue">&lt;template&gt;    &lt;div id&#x3D;&#39;main&#39;&gt;&lt;&#x2F;div&gt;&lt;&#x2F;template&gt;&lt;script&gt;&#x2F;&#x2F; BarChart.vueimport axios from &#39;axios&#39;;export default &#123;    name: &#39;barChart&#39;,    methods :&#123;        initChart() &#123;            var echarts &#x3D; require(&#39;echarts&#39;);            let myChart &#x3D; echarts.init(document.getElementBuId(&#39;main&#39;));            &#x2F;&#x2F; 这里需要一个id为main的空div标签，注意，必须是空标签            var option &#x3D; &#123;                tooltip: &#123;                    trigger: &#39;axis&#39;,                    axisPointer: &#123;                        type: &#39;shadow&#39;,                    &#125;                &#125;,                xAxis: &#123;                    type: &#39;category&#39;,                    name: &#39;id&#39;, &#x2F;&#x2F;x轴的名称                    data: this.idData,                &#125;,                yAxis: &#123;                    type: &#39;value&#39;,                    name: &#39;vote&#39;,                   &#x2F;&#x2F; data: this.voteData,                    &#x2F;&#x2F; y轴好像不放data也没多大影响                &#125;,                series: [&#123;                    data: this.voteData,                    type: &#39;bar&#39;,                &#125;]            &#125;            myChart.setOption(option); &#x2F;&#x2F; 设置图标样式        &#125;    &#125;,    created() &#123;        &#x2F;&#x2F; 这里拿投票数接口来举例        axios.get(&#39;http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;vote&#x2F;api&#39;)        .then((res) &#x3D;&gt; &#123;            this.idData &#x3D; [];            this.voteData &#x3D; [];            if (res.status &#x3D;&#x3D; 200) &#123;                let temp &#x3D; res.data.score;                for (let i in temp) &#123;                    this.idData.push(temp[i].HOUSE_ID);                    this.voteData.push(temp[i].HOUSE_VOTE);                                    &#125;            &#125;            this.initChart();        &#125;)    &#125;,    mounted() &#123;        this.initChart();    &#125;&#125;&lt;&#x2F;script&gt;</code></pre>]]></content>
    
    
    <summary type="html">Vue在前端获取后端json数据并展现在 echart 上</summary>
    
    
    
    <category term="BackEnd" scheme="https://jaydenchang.top/categories/BackEnd/"/>
    
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
    <category term="Vue" scheme="https://jaydenchang.top/tags/Vue/"/>
    
  </entry>
  
  <entry>
    <title>gin+MySQL简单实现数据库查询</title>
    <link href="https://jaydenchang.top/post/0x0032.html"/>
    <id>https://jaydenchang.top/post/0x0032.html</id>
    <published>2022-12-24T16:00:00.000Z</published>
    <updated>2022-12-29T02:18:44.390Z</updated>
    
    <content type="html"><![CDATA[<p><br/></p><p>利用 gin 项目搭建一个简易的后端系统。</p><h4 id="一个简易的-http-响应接口">一个简易的 HTTP 响应接口</h4><p>首先在 go 工作区的终端输入这条指令：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go get -u github.com&#x2F;gin-gonic&#x2F;gin</code></pre><p>将 gin 项目的相关依赖保存到本地。</p><p>在终端生成 go mod 包管理文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">go mod init</code></pre><p>再创建一个 <code>main.go</code> 文件：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport &quot;github.com&#x2F;gonic-gin&#x2F;gin&quot;func main() &#123;    r :&#x3D; gin.Default()    r.GET(&quot;&#x2F;test&quot;, func(c *gin.Context) &#123;        c.JSON(200, gin.H&#123;            &quot;message&quot;: &quot;test&quot;,        &#125;)    &#125;)    r.Run(&quot;:9999&quot;) &#x2F;&#x2F; 运行在9999端口&#125;</code></pre><p>因为我是在 wsl 运行这个项目，所以还需要先获取虚拟机的 ip，然后用 curl 测试：</p><pre class="line-numbers language-none"><code class="language-none">curl http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;ping</code></pre><p>返回结果：</p><pre class="line-numbers language-none"><code class="language-none">&#123;&quot;message&quot;:&quot;test&quot;&#125;</code></pre><h4 id="连接-mysql">连接 MySQL</h4><p>关于 MySQL 操作这部分，一开始想着简单，只学了查询数据库部分，大多数踩得坑都是在查询部分，后面觉得要举一反三，就在原来基础上又写了添加的部分，在添加数据这一块写的不是很详细。</p><h5 id="添加">添加</h5><p>先把基本的框架写出来，这里我连接的数据库结构体如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type SqlUser struct &#123;User_id       string &#96;json:&quot;user_id&quot; from:&quot;user_id&quot;&#96;User_name     string &#96;json:&quot;user_name&quot; from:&quot;user_name&quot;&#96;&#125;</code></pre><p>添加单条数据：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">Db, _ :&#x3D; sql.Open(&quot;mysql&quot;, &quot;&lt;username&gt;:&lt;password&gt;@(localhost:3306)&#x2F;&lt;yourDatabase&gt;&quot;)router.POST(&quot;&#x2F;add&quot;, func(c *gin.Context) &#123;    User_id :&#x3D; c.Request.FormValue(&quot;User_id&quot;)User_name :&#x3D; c.Request.FormValue(&quot;User_name&quot;)    result, err :&#x3D; db.SqlDB.Exec(&quot;INSERT INTO t_user (user_id,user_name) VALUE(?,?)&quot;, u.User_id, u.User_name)    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    id, err :&#x3D; result.LastInsertId()    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    msg :&#x3D; fmt.Sprintf(&quot;insert successfully %d&quot;, id)    c.JSON(http.StatusOK, gin.H&#123;        &quot;msg&quot;: msg,        &quot;user_name&quot;: User_name,    &#125;)&#125;)r.Run(&quot;:9999&quot;)</code></pre><p>执行非 query 操作，使用 Exec 方法，使用 curl 进行测试：</p><pre class="line-numbers language-none"><code class="language-none">curl -X POST http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;add -d &quot;User_id&#x3D;2&amp;User_name&#x3D;aaa&quot;</code></pre><p>返回结果如下：</p><pre class="line-numbers language-none"><code class="language-none">&#123;&quot;msg&quot;:&quot;insert successfully 1&quot;,&quot;user_name&quot;:&quot;test&quot;&#125;</code></pre><h5 id="查询">查询</h5><p>单条查询如下</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">r.GET(&quot;&#x2F;test&#x2F;:id&quot;, func(c *gin.Context) &#123;    id: c.Param(&quot;id&quot;)    var u SqlUser    err :&#x3D; Db.QueryRow(&quot;select user_id,user_name from t_user where user_id &#x3D;?&quot;, id).Scan(&amp;u.user_id, &amp;u.user_name)    if err !&#x3D; nil &#123;        log.Fatalln(err)        c.JSON(http.StatusOK, gin.H&#123;            &quot;user&quot;: nil,        &#125;)        return    &#125;    c.JSON(http.StatusOK, gin.H&#123;        &quot;user&quot;: u.user_name,    &#125;)&#125;)</code></pre><p>然后用 curl 命令去测试：</p><pre class="line-numbers language-none"><code class="language-none">curl http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;test&#x2F;1</code></pre><p>会得到之前在 MySQL 中预先保存的条目，样例结构如下：</p><pre class="line-numbers language-none"><code class="language-none">&#123;&quot;user&quot;:&quot;test&quot;&#125;</code></pre><p>然而，最开始我不是这样写的，当时的我，没整理好这个框架，直接在单文件里封装函数，下面是我的错误示范：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var Db *sql.DBfunc QueryData(c *gin.Context) &#123;    id :&#x3D; c.Param(&quot;id&quot;)    var u SqlUser    err :&#x3D; Db.QueryRow(&quot;select user_id,user_name from t_user where user_id &#x3D;?&quot;, id).Scan(&amp;u.user_id, &amp;u.user_name)    if err !&#x3D; nil &#123;        log.Fatalln(err)        c.JSON(200, gin.H&#123;            &quot;user&quot;: nil,        &#125;)        return    &#125;    c.JSON(200, gin.H&#123;        &quot;user&quot;: u.user_name,    &#125;)&#125;func main() &#123;    Db, _ &#x3D; sql.Open(&quot;&#x2F;*mysql link*&#x2F;&quot;)    &#x2F;&#x2F; 略去了这个问题相关的代码    r.GET(&quot;&#x2F;test&#x2F;:id&quot;, QueryData)    r.Run(&quot;:9999&quot;)&#125;</code></pre><p>然后就出现这样的错误：</p><pre class="line-numbers language-none"><code class="language-none">runtime error: invalid memory address or nil pointer dereference</code></pre><p>必应上的结果是：指针声明后未初始化就赋值。</p><p>这个 bug 困扰了我一整天，然后就开始了痛苦的 debug，main 函数的 router 应该是没问题的，接口测试正常，那问题应该是出在 <code>QueryData</code> 上，那只能一个个打印变量测试吧，测试到 <code>Db</code> 时，发现这个变量为 nil，然后紧接着抛出刚才提到的错误。好吧，这个错误我在之前也偶尔犯过，虽然在 main 中对其赋了值，但作用域不一样，所以还是无法传值，那只好去考虑下框架了。</p><h5 id="修改">修改</h5><p>修改单条数据：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">router.PUT(&quot;&#x2F;person&#x2F;:id&quot;, func(c *gin.Context) &#123;    cid :&#x3D; c.Param(&quot;id&quot;)    id, err :&#x3D; strconv.Atoi(cid)    person :&#x3D; Person&#123;Id: id&#125;    err &#x3D; c.Bind(&amp;person)    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    stmt, err :&#x3D; Db.Prepare(&quot;UPDATE person SET user_name&#x3D;? where user_id&#x3D;?&quot;)        if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    defer stmt.Close()    rs, err :&#x3D; stmt.Exec(person.user_name, person.user_id)    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    ra, err :&#x3D; rs.RowsAffected()    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    msg :&#x3D; fmt.Sprintf(&quot;UPDATE person %d successful %d&quot;, person.user_id, ra)    c.JSON(http.StatusOK, gin.H&#123;        &quot;msg&quot;: msg,    &#125;)&#125;)</code></pre><p>urlencode 方式更新：</p><pre class="line-numbers language-none"><code class="language-none">curl -X PUT http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;person&#x2F;1 -d &quot;user_id&#x3D;1&amp;user_name&#x3D;temp&quot;</code></pre><p>json 方式更新：</p><pre class="line-numbers language-none"><code class="language-none">curl -X PUT http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;person&#x2F;1 -H &quot;Content-Type: application&#x2F;json&quot; -d &#39;&#123;&quot;user_name&quot;:&quot;temp&quot;&#125;&#39;</code></pre><h5 id="删除">删除</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">router.DELETE(&quot;person&#x2F;:id&quot;, func(c *gin.Context) &#123;    cid :&#x3D; c.Param(&quot;id&quot;)    id, err :&#x3D; strconv.Atoi(cid)    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    rs, err :&#x3D; db.Exec(&quot;DELETE FROM person from where user_id&#x3D;?&quot;, id)    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    ra, err :&#x3D; rs.RowsAffected()    if err !&#x3D; nil &#123;        log.Fatalln(err)    &#125;    msg :&#x3D; fmt.Sprintf(&quot;DELETE person %d successful %d&quot;, id, ra)    c.JSON(http.StatusOK, gin.H&#123;        &quot;msg&quot;: msg,    &#125;)&#125;)</code></pre><p>直接使用删除接口</p><pre class="line-numbers language-none"><code class="language-none">curl -X &quot;DELETE&quot; http:&#x2F;&#x2F;&lt;ip&gt;:9999&#x2F;person&#x2F;1 </code></pre><p>这里的 DELETE 参数必须在双引号内，不然不会被识别为 delete 请求。</p><h5 id="搭建框架">搭建框架</h5><p>参考了大佬的做法 <sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>，我也来创建我的目录：</p><pre class="line-numbers language-none"><code class="language-none">ginExample tree.├── api│   └── query.go├── database│   └── mysql.go├── main.go├── models│   └── queryUser.go├── router.go├── go.mod└── go.sum</code></pre><p>这里的话以添加、查询功能为例，增删改同理，具体可以查看文末的参考链接。</p><p>api 存放 handler 函数，model 存放数据模型。</p><h6 id="数据库处理">数据库处理</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; mysql.gopackage databaseimport (    &quot;database&#x2F;sql&quot;    _ &quot;github.com&#x2F;go-sql-driver&#x2F;mysql&quot;    &quot;log&quot;)var SqlDB *sql.DBfunc init() &#123;    var err errorSqlDB, err &#x3D; sql.Open(&quot;mysql&quot;, &quot;root:jaydenmysql@(127.0.0.1:3306)&#x2F;microtest&quot;)if err !&#x3D; nil &#123;log.Fatalln(err.Error())&#125;err &#x3D; SqlDB.Ping()if err !&#x3D; nil &#123;log.Fatalln(err.Error())&#125;&#125;</code></pre><p>因为在别的包会用到 <code>SqlDB</code> 这个变量，因此必须大写 (golang 只有大写开头变量才是 public 类变量)</p><h6 id="数据-model-封装">数据 model 封装</h6><p>抽离出 SqlUser 结构体以及对应的方法：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package modelsimport (db &quot;ginExample&#x2F;database&quot;&quot;log&quot;)type SqlUser struct &#123;User_id       string &#96;json:&quot;user_id&quot; from:&quot;user_id&quot;&#96;User_name     string &#96;json:&quot;user_name&quot; from:&quot;user_name&quot;&#96;&#125;func (u *SqlUser) GetUser(id string) SqlUser &#123;err :&#x3D; db.SqlDB.QueryRow(&quot;select user_id,user_name from t_user where user_id &#x3D;?&quot;, id).Scan(&amp;u.User_id, &amp;u.User_name)if err !&#x3D; nil &#123;log.Println(err)u.User_name &#x3D; &quot;nil&quot;return *u&#125;return *u&#125;func (u *SqlUser) AddUser() (id int64, err error) &#123;    result, err :&#x3D; db.SqlDB.Exec(&quot;INSERT INTO t_user (user_id,user_name) VALUE(?,?)&quot;, u.User_id, u.User_name)    if err !&#x3D; nil &#123;log.Fatalln(err)return&#125;id, err &#x3D; result.LastInsertId()if err !&#x3D; nil &#123;log.Fatalln(err)return&#125;fmt.Println(id)return id, err&#125;func (u *SqlUser) UpdateUser() int64 &#123;    stmt, err :&#x3D; db.SqlDB.Prepare(&quot;UPDATE t_user SET user_name&#x3D;? where user_id&#x3D;?&quot;)if err !&#x3D; nil &#123;log.Fatalln(err)&#125;rs, err :&#x3D; stmt.Exec(u.User_name, u.User_id)if err !&#x3D; nil &#123;log.Fatalln(err)&#125;ra, err :&#x3D; rs.RowsAffected()if err !&#x3D; nil &#123;log.Fatalln(err)&#125;return ra&#125;func (u *SqlUser) DeleteUser(id string) int64 &#123;rs, err :&#x3D; db.SqlDB.Exec(&quot;DELETE FROM t_user where user_id&#x3D;?&quot;, id)if err !&#x3D; nil &#123;log.Fatalln(err)&#125;ra, err :&#x3D; rs.RowsAffected()if err !&#x3D; nil &#123;log.Fatalln(err)&#125;return ra&#125;</code></pre><h6 id="handler">handler</h6><p>然后把具体的 handler 封装到 api 中，handler 操作数据库，因此会引用 model 包。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package apiimport (&quot;fmt&quot;&quot;net&#x2F;http&quot;. &quot;ginExample&#x2F;models&quot;&quot;github.com&#x2F;gin-gonic&#x2F;gin&quot;)func IndexApi(c *gin.Context) &#123;c.String(http.StatusOK, &quot;It works&quot;)&#125;func GetUserApi(c *gin.Context) &#123;var u SqlUserid :&#x3D; c.Param(&quot;id&quot;)u &#x3D; u.GetUser(id)c.JSON(200, gin.H&#123;&quot;user&quot;: u.User_name,&#125;)&#125;func AddUserApi(c *gin.Context) &#123;User_id :&#x3D; c.Request.FormValue(&quot;User_id&quot;)User_name :&#x3D; c.Request.FormValue(&quot;User_name&quot;)    &#x2F;&#x2F; 这里的大小写一定要对应上结构体内的变量名u :&#x3D; models.SqlUser&#123;User_id: User_id, User_name: User_name&#125;rows, err :&#x3D; u.AddUser()if err !&#x3D; nil &#123;log.Fatalln(err)&#125;msg :&#x3D; fmt.Sprintf(&quot;insert successfully %d\n&quot;, rows)c.JSON(200, gin.H&#123;&quot;msg&quot;:       msg,&quot;user_name&quot;: User_name,&#125;)&#125;func UpdateUserApi(c *gin.Context) &#123;User_id :&#x3D; c.Request.FormValue(&quot;User_id&quot;)User_name :&#x3D; c.Request.FormValue(&quot;User_name&quot;)u :&#x3D; models.SqlUser&#123;User_id: User_id, User_name: User_name&#125;row :&#x3D; u.UpdateUser()msg :&#x3D; fmt.Sprintf(&quot;update successful %d&quot;, row)c.JSON(200, gin.H&#123;&quot;msg&quot;: msg,&#125;)&#125;func DeleteUserApi(c *gin.Context) &#123;var u models.SqlUserid :&#x3D; c.Param(&quot;id&quot;)row :&#x3D; u.DeleteUser(id)msg :&#x3D; fmt.Sprintf(&quot;DELETE user successful %d&quot;, row)c.JSON(200, gin.H&#123;&quot;msg&quot;: msg,&#125;)&#125;</code></pre><h6 id="router">router</h6><p>最后就是把路由抽离出来：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F;router.gopackage mainimport (    &quot;github.com&#x2F;gin-gonic&#x2F;gin&quot;    . &quot;ginExample&#x2F;api&quot;)func initRouter() *gin.Engine &#123;    router :&#x3D; gin.Default()    router.GET(&quot;&#x2F;&quot;, IndexApi)    router.GET(&quot;&#x2F;query&#x2F;:id&quot;, GetUserApi)    router.POST(&quot;&#x2F;add&quot;, api.AddUserApi)router.PUT(&quot;&#x2F;update&#x2F;:id&quot;, api.UpdateUserApi)router.DELETE(&quot;&#x2F;delete&#x2F;:id&quot;, api.DeleteUserApi)    return router&#125;</code></pre><h6 id="app-入口">app 入口</h6><p>最后就是 main 的 app 入口，将路由导入，同时在 main 即将结束时，关闭全局数据库连接池。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport db &quot;ginExample&#x2F;database&quot;func main() &#123;    defer db.SqlDB.Close()    router :&#x3D; initRouter()    router.Run(&quot;:9999&quot;)&#125;</code></pre><p>这里运行项目的话，不能像之前简单地使用 <code>go run main.go</code> ，因为 main 包含 <code>main.go</code> 和 <code>router.go</code>，因此要运行 <code>go run *.go</code>，如果最终编译二进制项目，则运行 <code>go build -o app</code>。</p><p>测试结果和上面是一样的，至此，基本的访问、操作数据库功能实现。</p><h4 id="参考链接">参考链接</h4><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">1.</span><span style="display: inline-block; vertical-align: top;"><a href="https://www.jianshu.com/p/a3f63b5da74c">Gin实战：Gin+Mysql简单的Restful风格的API - 简书 (jianshu.com)</a></span><a href="#fnref:1" rev="footnote"> ↩︎</a></li></ol></div></div>]]></content>
    
    
    <summary type="html">利用gin框架结合MySQL简单实现一个数据库查询的后端系统</summary>
    
    
    
    <category term="BackEnd" scheme="https://jaydenchang.top/categories/BackEnd/"/>
    
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>动手写RPC框架</title>
    <link href="https://jaydenchang.top/post/0x0031.html"/>
    <id>https://jaydenchang.top/post/0x0031.html</id>
    <published>2022-10-15T16:00:00.000Z</published>
    <updated>2025-07-27T08:40:39.365Z</updated>
    
    <content type="html"><![CDATA[<hr/><p>本文学习自<a href="https://geetktutu.com">geektutu</a> ,大部分内容摘自 <ahref="https://geektutu.com/post/geerpc.html">7天用Go从零实现RPC框架GeeRPC| 极客兔兔 (geektutu.com)</a>，并在此基础上稍加个人的学习经历和理解</p><p>作者仓库地址：<ahref="https://github.com/geektutu/7days-golang">geektutu/7days-golang: 7days golang programs from scratch (web framework Gee, distributed cacheGeeCache, object relational mapping ORM framework GeeORM, rpc frameworkGeeRPC etc) 7天用Go动手写/从零实现系列 (github.com)</a></p><h3 id="day0.-浅谈rpc框架">day0. 浅谈RPC框架</h3><p>前几天在学 6.824 时，发现有太多内容是我完全没接触过的，然后其中涉及到RPC 的内容又比较多，忽然想起 geektutu 出过 "七天实现 RPC 框架"的文章，马上转坑来学习。</p><h4 id="谈谈rpc框架">1. 谈谈RPC框架</h4><p>RPC (Remote Procedure Call，远程过程调用)是一种计算机通信协议，允许调用不同进程空间的程序。RPC的客户端和服务器可以在一台机器上，也可以在不同的机器上。程序员使用时，就像调用本地程序一样，无需关注内部实现的细节。</p><p>不同应用程序间的通信方式有很多，例如浏览器和服务器间广泛用基于HTTP协议的 Restful API。与 RPC相比，Restful API有相对统一的标准，因而更通用，兼容性更好，支持不同的语言。HTTP协议是基于文本的，一般具备更好的可读性。但是缺点也很明显：</p><ul><li>Restful接口要额外的定义，无论是客户端还是服务端，都需要额外的代码来处理，而 RPC调用则更接近于直接调用。</li><li>基于 HTTP 协议的 Restful 报文冗余，承载了过多无效信息，而RPC通常使用自定义的协议格式，减少冗余报文。</li><li>RPC可以采用更高效的序列化协议，将文本转为二进制传输，获得更高的性能。</li><li>因为 RPC的灵活性，所以更容易扩展和集成诸如注册中心，负载均衡等功能。</li></ul><h4 id="rpc框架需要解决什么问题">2. RPC框架需要解决什么问题</h4><p>RPC 需要解决什么问题？或者换个说法，为什么要RPC 框架？</p><p>我们可以想象下两台机器上，两个程序之间要通信，那么首先，需要确定采用的传输协议是什么？如果这两个程序位于不同的机器，那么一般会选择TCP 协议活 HTTP 协议；那如果两个程序位于相同的机器，也可以选择 UnixSocket协议。传输协议确定后，还需要确定报文的编码格式，比如采用最常用的json或xml，那如果报文比较大，还可能会选择 protobuf等其他的编码方式，甚至编码之后，再进行压缩。接收端获取报文则需要相反的过程，先解压再解码。</p><p>解决了传输协议和保温编码的问题，接下来还需要解决一系列的可用性问题，例如，连接超时了怎么办？是否支持异步请求和并发？</p><p>如果服务端的实例很多，客户端并不关心这些实例的地址和部署位置，只关心自己能否获取到期待的结果，那就引出了注册中心(registry) 和负载均衡 (load balance)的问题。简单地说，即客户端和服务端相互不感知对方的存在，服务端启动时将自己注册到注册中心，客户端调用时，从注册中心获取到所有可用的实例，选择一个来调用。这样服务端和客户端只需要感知注册中心的存在就够了。注册中心还需要实现服务动态添加，删除，使用"心跳机制" 确保服务处于可用状态等功能。</p><p>再进一步，假设服务端是不同的团队提供的，如果没有统一的RPC框架，各个团队的服务提供方就需要各自实现一套消息编解码，连接池，收发线程，超时处理等"业务之外" 的重复技术劳动，造成整体的低效。因此，"业务之外"的这部分公共的能力，即是RPC 框架所需要具备的能力。</p><h3 id="day1.-服务端与消息编码">day1. 服务端与消息编码</h3><ul><li>使用<code>encoding/gob</code>实现消息的编解码(序列化与反序列化)。</li><li>实现一个简易的服务端，仅接受消息，不处理，代码约200行。</li></ul><h4 id="消息的序列化与反序列化">消息的序列化与反序列化</h4><p>一个典型的RPC 调用如下</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">err &#x3D; client.call(&quot;Arith.Multiply&quot;, args, &amp;reply)</code></pre><p>客户端发送的请求包括服务名<code>Arith</code>，方法名<code>Multiply</code>，参数<code>args</code>三个，服务端的响应包括错误<code>error</code>，返回值<code>reply</code>2个。我们将请求和响应中的参数和返回值抽象为 body，剩余的信息放在 header中，那么就可以抽象出数据结构 Header：</p><p><strong>day1/codec/codec.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package codecimport &quot;io&quot;type Header struct &#123;    ServiceMethod string &#x2F;&#x2F; format &quot;Service.Method&quot;    Seq           string &#x2F;&#x2F; sequence number chosen by client    Error         string&#125;</code></pre><ul><li>ServiceMethod 是服务名和方法名，通常与 Golang中的结构体和方法相映射。</li><li>Seq 是请求的序号，也可以认为是某个请求的ID，用来区分不同的请求。</li><li>Error 是错误信息，客户端设置为空，</li></ul><p>我们将和消息编解码相关的代码都放到 codec子目录中，在此之前，还需要在geerpc项目根目录下使用<code>go mod init geerpc</code> 初始化项目，方便后续子 package之间的引用。</p><p>进一步，抽象出对消息体进行编解码的接口Codec，抽象出接口是为了实现不同的 Codec 实例：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Codec interface &#123;    io.Closer    ReadHeader(*Header) error    ReadBody(interface&#123;&#125;) error    Write(*Header, interface&#123;&#125;) error&#125;</code></pre><p>紧接着，抽象出 Codec 的构造函数，客户端和服务端可以通过 Codec的<code>Type</code>得到构造函数，从而创建 Codec实例。这部分代码和工厂模式类似，与工厂模式不同的是，返回的是构造函数，而非实例。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type NewCodecFunc func(io.ReadWriteCloser) Codec type Type stringconst (GobType  Type &#x3D; &quot;application&#x2F;gob&quot;    JsonType Type &#x3D; &quot;application&#x2F;json&quot;)var NewCodecFuncMap map[Type]NewCodecFuncfunc init() &#123;    NewCodecFuncMap &#x3D; make(map[Type]NewCodecFunc)    NewCodecFuncMap[GobType] &#x3D; NewGobCodec &#x2F;&#x2F; 初始化map，实例化一个GobCodec对象&#125;</code></pre><p>我们定义了两种Codec，<code>Gob</code>和<code>Json</code>，但是实际代码只实现了<code>Gob</code>一种，事实上，2者的实现非常接近，甚至只需把<code>gob</code>换成<code>json</code>即可。</p><p>首先定义<code>GobCodec</code>结构体，这个结构体由四部分构成，<code>conn</code>是由构建函数传入，通常是通过TCP 或者 Unix 建立 socket 时得到的链接实例，dec 和 enc 对应 gob的Decoder 和 Encoder，buf是为了防止阻塞而创建的带缓冲的<code>Writer</code>，一般这么做都能提升性能。</p><p><strong>day1/codec/gob.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package codecimport (    &quot;bufio&quot;    &quot;encoding&#x2F;gob&quot;    &quot;io&quot;    &quot;log&quot;)type GobCodec struct &#123;    conn io.ReadWriteCloser    buf  *bufio.Writer    dec  *gob.Decoder    enc  *gob.Encoder&#125;var _ Codec &#x3D; (*GobCodec)(nil)&#x2F;&#x2F; 这里的写法的含义是，用来检测GobCodec是否实现了Codec接口，如果没有实现该接口则编译报错func NewGobCodec(conn io.ReadWriteCloser) Codec &#123;    buf :&#x3D; bufio.NewWriter(conn)    return &amp;GobCodec &#123;        conn: conn,        buf:  buf,        dec:  gob.NewDecoder(conn),        enc:  gob.NewEncoder(buf),    &#125;&#125;</code></pre><p>接着实现<code>ReadHeader</code>，<code>ReadBody</code>，<code>Write</code>和<code>Close</code>方法。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (c *GobCodec) ReadHeader(h *Header) error &#123;    return c.dec.Decode(h)&#125;func (c *GobCodec) ReadBody(body interface&#123;&#125;) error &#123;    return c.dec.Decode(body)&#125;func (c *GobCodec) Write(h *Header, body interface&#123;&#125;) (err error) &#123;    defer func() &#123;        _ &#x3D; c.buf.Flush() &#x2F;&#x2F; 将缓存区内容写入文件，返回类型为error         if err !&#x3D; nil &#123;            _ &#x3D; c.Close()        &#125;    &#125;()    if err !&#x3D; c.enc.Encode(h); err !&#x3D; nil &#123;        log.Println(&quot;rpc codec: gob error encoding header:&quot;, err)        return err    &#125;    if err :&#x3D; c.enc.Encode(body); err !&#x3D; nil &#123;        log.Println(&quot;rpc codec: gob error encoding body:&quot;, err)        return err    &#125;    return nil&#125;func (c *GobCodec) Close() error &#123;    return c.conn.Close() &#x2F;&#x2F; 返回一个err，具体的Close()在io.go中有重写&#125;</code></pre><h4 id="通信过程">通信过程</h4><p>客户端与服务端的通信需要协商一些内容，例如 HTTP 报文，分为 header 和body 两部分，body 的格式和长度通过 header中的<code>Content-Type</code>和<code>Content-Length</code>指定，服务端通过解析header 就能够知道如何从 body 中读取需要的信息。对于RPC协议来说，这部分协商是需要自主设计的。为了提升性能，一般在报文的最开始会规划固定的字节，来协商相关的信息。比如第1个字节用来表示序列化方式，第2个字节表示压缩方式，第3-6字节表示header 的长度，7-10字节表示body 长度。</p><p>对于 GeeRPC来说，目前需要协商的唯一一项内容时消息的编解码方式。我们将这部分信息，放到结构体<code>Option</code>中承载。目前，已经进入到服务端的实现阶段了。</p><p><strong>day1/server.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package geerpcconst MagicNumber &#x3D; 0x23bef5ctype Option struct &#123;    MagicNumber int        &#x2F;&#x2F; MagicNumber marks this&#39;s a geerpc request    CodecType   codec.Type &#x2F;&#x2F; client may choose different Codec to encode body&#125;var DefaultOption &#x3D; &amp;Option &#123;    MagicNumber: MagicNumber,    CodecType:   codec.GobType,&#125;</code></pre><p>一般来说，设计协商协议的这部分信息，需要设计固定的字节来传输。但是为了实现上更简单，GeeRPC 客户端固定采用 JSON 编码 Option，后续的 header 和 body的编码方式由 Option 中的 CodeType指定，服务端首先使用 JSON 解码Option，然后通过 Option 的 CodeType解码剩余内容。即报文将以这样的形式发送：</p><pre class="line-numbers language-none"><code class="language-none">| Option&#123;MagicNumber: xxx, CodecType: xxx&#125; | Header&#123;ServiceMethod ...&#125; | Body interface&#123;&#125; || &lt;-------    固定 JSON 编码       -------&gt; | &lt;--------  编码方式由 CodeType决定   -------&gt; |</code></pre><p>在一次连接中，Option 固定在报文的最开始，Header 和 Body可以有很多个，即报文可能是这样的。</p><pre class="line-numbers language-none"><code class="language-none">| Option | Header1 | Body1 | Header2 | Body2 | ...</code></pre><h4 id="服务端的实现">服务端的实现</h4><p>通信过程已经定义清楚了，那么服务端的实现就比较直接了。</p><p><strong>day1/server.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Server represents an RPC Server.type Server struct&#123;&#125;&#x2F;&#x2F; NewServer returns a new Server.func NewServer() *Server &#123;    return &amp;Server&#123;&#125;&#125;&#x2F;&#x2F; DefaultServer is the default instance of *Servervar DefaultServer &#x3D; NewServer()&#x2F;&#x2F; Acccept accepts connections on the listener and serves requests&#x2F;&#x2F; for each incoming connectionfunc (server *Server) Accept(lis net.Listener) &#123;    &#x2F;&#x2F; for循环等待socket连接建立    for &#123;        conn, err :&#x3D; lis.Accept()        if err !&#x3D; nil &#123;            log.Println(&quot;rpc server: accept error:&quot;, err)            return         &#125;        go server.ServeConn(conn)    &#125;&#125;&#x2F;&#x2F; Accept accepts connections on the listener and serves requests&#x2F;&#x2F; for each incoming connectionfunc Accept(lis net.Listener) &#123;    DefaultServer.Accept(lis)&#125;</code></pre><ul><li>首先定义了结构体<code>Server</code>，没有任何的成员字段。</li><li>实现了<code>Accept</code>方式，<code>net.Listener</code>作为参数，for循环等待 socket连接建立，并开启子协程处理，处理过程交给了<code>ServerConn</code>方法。</li><li>DefaultServer是一个默认的<code>Server</code>实例，主要为了用户使用方便。</li></ul><p>如果想启动服务，过程是很简单的，传入 listener 即可，tcp 协议和 unix协议都支持。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">lis, _ :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:9999&quot;)geerpc.Accept(lis)</code></pre><p><code>ServeConn</code>的实现就和之前讨论的通信过程紧密相关了，首先使用<code>json.NewDecoder</code>反序列化得到Option 实例，检查 MagicNumber 和 CodeType的值是否正确。然后根据 CodeType得到对应的消息编解码器，接下来的处理就交给<code>serverCodec</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; ServeConn runs the serer on a single connection&#x2F;&#x2F; ServeConn blocks, serving the connection until the client hangs upfunc (server *Server) ServeConn(conn io.ReadWriteCloser) &#123;    defer func() &#123;        _ &#x3D; conn.Close()    &#125;()    if err :&#x3D; json.NewDecoder(conn).Decode(&amp;opt); err !&#x3D; nil &#123;        log.Println(&quot;rpc server: options error:&quot;, err)        return    &#125;    &#x2F;&#x2F; 检查Option的参数是否正确    if opt.MagicNumber !&#x3D; MagicNumber &#123;        log.Printf(&quot;rpc server: invalid magic number %x&quot;, opt.MagicNumber)        return    &#125;    f :&#x3D; codec.NewCodecFuncMap[opt.CodecType]    if f &#x3D;&#x3D; nil &#123;        log.Printf(&quot;rpc server: invalid codec type %s&quot;, opt.CdoecType)        return    &#125;    server.serveCodec(f(conn))&#125;&#x2F;&#x2F; invalidRequest is a placeholder for response argv when error occursvar invalidRequest &#x3D; struct&#123;&#125;&#123;&#125;&#x2F;&#x2F; 注意这里要改serveCodec的入参func (server *Server) serveCodec(cc codec.Codec, opt *Option) &#123;    sending :&#x3D; new(sync.Mutex) &#x2F;&#x2F; make sure to send a complete response    &#x2F;&#x2F; 加入一个互斥锁避免多个回复报文交织在一起    wg :&#x3D; new(sync.WaitGroup) &#x2F;&#x2F; wait until all request are handled    for &#123;        req, err :&#x3D; server.readRequest(cc) &#x2F;&#x2F; 读取请求        if err !&#x3D; nil &#123;            if req &#x3D;&#x3D; nil &#123;                break &#x2F;&#x2F; it&#39;s not possible to recover, so close the connection            &#125;            req.h.Error &#x3D; err.Error()            server.sendResponse(cc, req.h, invalidRequest, sending)            &#x2F;&#x2F; 回复请求            continue        &#125;        wg.Add(1)        go server.handleRequest(cc, req, sending, wg, opt.HandleTimeout)        &#x2F;&#x2F; 加入一个处理请求协程        &#x2F;&#x2F; 这里注意要新增一个超时时间    &#125;    wg.Wait()    _ &#x3D; cc.Close()&#125;</code></pre><p><code>serveCodec</code>的过程很简单，主要包含三阶段：</p><ul><li>读取请求 readRequest</li><li>处理请求 handleRequest</li><li>回复请求 sendRequest</li></ul><p>之前提到过，再一次连接中，允许收到多个请求，即多个 request header 和request body，因此这里使用了 for 无限制地等待请求的到来，直到发生错误(例如连接被关闭，接收到的报文有问题等)，这里需要注意的点有三个：</p><ul><li>handleRequest 使用了协程并发执行请求。</li><li>处理请求是并发的，但是回复请求的报文必须是逐个发送的，并发容易导致多个回复报文交织在一起，客户端无法解析。在这里使用锁(sending) 保证。</li><li>尽力而为，只有在 header 解析失败时，才终止循环。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; request stores all infomation of a calltype request struct &#123;    h            *codec.Header &#x2F;&#x2F; header of request    argv, replyv reflect.Value &#x2F;&#x2F; argv and replyv of request    &#x2F;&#x2F; Value also is a struct&#125;func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) &#123;    var h codec.Header    if err :&#x3D; cc.ReadHeader(&amp;h); err !&#x3D; nil &#123;        if err !&#x3D; io.EOF &amp;&amp; err !&#x3D; io.ErrUnexpectedEOF &#123;            log.Println(&quot;rpc server: read header error:&quot;, err)        &#125;        return nil, err    &#125;    return &amp;h, nil&#125;func (server *Server) readRequest(cc codec.Codec) (*request, error) &#123;    h, err :&#x3D; server.readRequestHeader(cc)    if err !&#x3D; nil &#123;        return nil, err    &#125;    req :&#x3D; &amp;reqeust&#123;h: h&#125;    &#x2F;&#x2F; TODO: now we don&#39;t know the type of request argv    &#x2F;&#x2F; day1, just suppose it&#39;s string    req.argv &#x3D; reflect.New(reflect.TypeOf(&quot;&quot;))    if err &#x3D; cc.ReadBody(req.argv.Interface()); err !&#x3D; nil &#123;        log.Println(&quot;rpc server: read argv err:&quot;, err)    &#125;    return req, nil&#125;func (server *Server) sendResponse(cc codec.Cdoec, h *codec.Header, body interface&#123;&#125;, sneding *sync.Mutex) &#123;    sending.Lock()    defer sending.Unlock()    if err :&#x3D; cc.Write(h, body); err !&#x3D; nil &#123;        log.Println(&quot;rpc server: write response error:&quot;, err)    &#125;&#125;func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) &#123;    &#x2F;&#x2F; TODO, should call registered rpc methods to get the right replyv    &#x2F;&#x2F; day1, just print argv and send a hello message    defer wg.Done()    log.Println(req.h, req.argv.Elem())    req.replyv &#x3D; reflect.ValueOf(fmt.Sprintf(&quot;geerpc resp %d&quot;, req.h.Seq))    server.sendResponse(cc, req.h, req.replyv.Interface(), sending)&#125;</code></pre><p>目前还不能判断 body 的类型，因此在 readRequest 和 handleRequest中，day1 将在 body作为字符串处理。接收到请求，打印header，并回复<code>geerpc resp $&#123;req.h.Seq&#125;</code>。这一部分后续再实现。</p><h4 id="main-函数-一个简易的客户端">main 函数 (一个简易的客户端)</h4><p>day1的内容就到此为止了，在这里我们已经实现了一个消息的编解码器<code>GobCodec</code>，并且客户端与服务端实现了简单的协议交换(protocolexchange)，即允许客户端使用不同的编码方式。实现了服务端的雏形，建立连接，读取、处理并回复客户端的请求。</p><p>接下来，我们在 main 函数中看看如何使用刚实现的 GeeRPC。</p><p><strong>day1/main/main.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (    &quot;encoding&#x2F;json&quot;    &quot;geerpc&quot;    &quot;geerpc&#x2F;codec&quot;    &quot;log&quot;    &quot;net&quot;    &quot;time&quot;)func startServer(addr chan string) &#123;    &#x2F;&#x2F; pick a free port    l, err :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:0&quot;)    if err !&#x3D; nil &#123;        log.Fatal(&quot;network error: &quot;, err)    &#125;    log.Println(&quot;start rpc server on&quot;, l.Addr())    addr &lt;- l.Addr().String()    geerpc.Accept(l) &#x2F;&#x2F; 注意这里是不是数字1，是字母l&#125;func main() &#123;    addr :&#x3D; make(chan string)    go startServer(addr)        &#x2F;&#x2F; in fact, following code is like a simple geerpc client    conn, _ :&#x3D; net.Dial(&quot;tcp&quot;, &lt;- addr)    defer func() &#123;        _ &#x3D; conn.Close()    &#125;()        time.Sleep(time.Second)    &#x2F;&#x2F; send options    _ &#x3D; json.NewEncoder(conn).Encode(geerpc.DefaultOption)    cc :&#x3D; codec.NewGobCodec(conn)    &#x2F;&#x2F; send request &amp; receive response    for i :&#x3D; 0; i &lt; 5; i++ &#123;        h :&#x3D; $codec.Header &#123;            ServiceMethod: &quot;Foo.Sum&quot;,            Seq:           uint64(i),        &#125;        _ &#x3D; cc.Write(h, fmt.Sprintf(&quot;geerpc req %d&quot;, h.Seq))        _ &#x3D; cc.ReadHeader(h)        var reply string        _ &#x3D; cc.ReadBody(&amp;reply)        log.Println(&quot;reply:&quot;, reply)    &#125;&#125;</code></pre><ul><li>在<code>startServer</code>中使用了信道<code>addr</code>，确保服务端端口监听成功，客户端再发起请求。</li><li>客户端首先发送<code>Option</code>进行协议交换，接下来发送消息头<code>h := &amp;codec.Header&#123;&#125;</code>，和消息体<code>geerpc req $&#123;h.Seq&#125;</code>。</li><li>最后解析服务端的相应<code>reply</code>，并打印出来。</li></ul><p>执行结果如下：</p><pre class="line-numbers language-none"><code class="language-none">start rpc server on [::]63662&amp;&#123;Foo.Sum 0 &#125; geerpc req 0reply: geerpc resp 0&amp;&#123;Foo.Sum 1 &#125; geerpc req 1reply: geerpc resp 1&amp;&#123;Foo.Sum 2 &#125; geerpc req 2reply: geerpc resp 2&amp;&#123;Foo.Sum 3 &#125; geerpc req 3reply: geerpc resp 3&amp;&#123;Foo.Sum 4 &#125; geerpc req 4reply: geerpc resp 4</code></pre><h3 id="day2.-支持并发和异步的客户端">day2. 支持并发和异步的客户端</h3><h4 id="call-的设计">Call 的设计</h4><p>对<code>net/rpc</code>而言，一个函数需要能够被远程调用，需要满足如下五个条件：</p><ul><li>the method's type is exported</li><li>the method is exported</li><li>the method has two arguments, both exported (or builtin) types</li><li>the method's second arguments is a pointer</li><li>the method has return type error</li></ul><p>更直观一点：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (t *T) MethodName(argType T1, replyType *T2) error</code></pre><p>根据上述需求，首先我们封装了结构体 Call 来承载一次 RPC调用所需要的信息。</p><p><strong>day2/client.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Call represents an active RPC type Call struct &#123;    Seq           uint64    ServiceMethod string      &#x2F;&#x2F; format &quot;&lt;service&gt;.&lt;method&gt;&quot;    Args          interface&#123;&#125; &#x2F;&#x2F; arguments to the function    Reply         interface&#123;&#125; &#x2F;&#x2F; reply from the fucntion    Error         error       &#x2F;&#x2F; if error occurs, it will be set    Done          chan *Call  &#x2F;&#x2F; Strobes when call is complete&#125; func (call *Call) done() &#123;    call.Done &lt;- call&#125;</code></pre><p>为了支持异步调用，Call 结构体中添加了一个字段 Done，Done的类型是<code>chan *Call</code>，当调用结束时，会调用<code>call.done()</code>通知调用方。</p><h4 id="实现-client">实现 Client</h4><p>接下来，我们将实现 GeeRPC 客户端最核心的部分 Client。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Client represents an RPC Client&#x2F;&#x2F; There may be multipie outstanding Calls associated&#x2F;&#x2F; with a single Client, and a Client may be used by&#x2F;&#x2F; multipie goroutines simultaneouslytype Client struct &#123;    cc       codec.Codec    opt      *Option    sending  sync.Mutex &#x2F;&#x2F; protect following    header   codec.Header    mu       sync.Mutex &#x2F;&#x2F; protect following    seq      uint64    pending  map[uint64]*Call    closing  bool &#x2F;&#x2F; user has called Close    shutdown bool &#x2F;&#x2F; server has told us to stop&#125;var _ io.Closer &#x3D; (*Client)(nil)var ErrShutdown &#x3D; errors.New(&quot;connection is shut down&quot;)&#x2F;&#x2F; Close the connection func (client *Client) Close() error &#123;    client.mu.Lock()    defer client.mu.Unlock()    if client.closing &#123;        return ErrShutdown    &#125;    client.closing &#x3D; true    return client.cc.Close()&#125;&#x2F;&#x2F; IsAvaliable return true if the client does workfunc (client *Client) IsAvaliable() bool &#123;    client.mu.Lock()    defer client.mu.Unlock()    return !client.shutdown &amp;&amp; !client.closing&#125;</code></pre><p>client 的字段解析如下：</p><ul><li>cc是消息的编解码器，和服务端类似，用来序列化将要发送出去的请求，以及反序列化接收到的响应。</li><li>sending是一个互斥锁，和服务端类似，为了保证请求的有序发送，即防止出现多个请求报文混淆。</li><li>header 是每个请求的消息头，header只有在请求发送时才需要，而请求发送是互斥的，因此每个客户端只需要一个，声明在Client 结构体中可以复用。</li><li>seq 用于给发送的请求编号，每个请求有唯一编号。</li><li>pending 存储未处理完的请求，键是编号，值是 Call 实例。</li><li>closing 和 shutdown 任意一个值置为 true，则表示 Client处于不可用的状态，但有些许的差别，closing是用户主动关闭的，即调用<code>Close</code>方法，而 shutdown 置为 true一般是有错误发生。</li></ul><p>紧接着，实现和 Call 相关的方法。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (client *Client) registerCall(call *Call) (uint64, error) &#123;    client.mu.Lock()    defer client.mu.Unlock()    if client.closing || client.shutdown &#123;        return 0, ErrShutdown    &#125;    call.Seq &#x3D; client.seq    client.pending[call.Seq] &#x3D; call    client.seq++    return call.Seq, nil&#125;func (client *Client) removeCall(seq uint64) *Call &#123;    client.mu.Lock()    defer client.mu.Unlock()    call :&#x3D; client.pending[seq]    delete(client.pending, seq)    return all&#125;func (client *Client) terminateCalls(err error) &#123;    client.sending.Lock()    defer client.sending.Unlock()    client.mu.Lock()    defer client.mu.Unlock()    client.shutdown &#x3D; true    for _, call :&#x3D; range client.pending &#123;        call.Error &#x3D; err        call.done()    &#125;&#125;</code></pre><ul><li>registerCall ：将参数 call 添加到 client.pending 中，并更新client.seq。</li><li>removeCall：根据seq，从 client.pending 中移除对应的call，并返回。</li><li>terminateCalls：服务端或客户端发生错误时调用，将 shutdown 设置为true，且将错误信息通知所有 pending 状态的 call。</li></ul><p>对一个客户端来说，接收响应、发送请求是最重要的2个功能。那么首先实现接收功能，接收到的响应有三种情况：</p><ul><li>call不存在，可能是请求没有发送完整，或者因为其他原因被取消，但是服务端仍旧处理了。</li><li>call 存在，但服务端处理出错，即 <code>h.Error</code>不为空。</li><li>call 存在，服务端处理正常，那么需要从 body 中读取 Reply 的值。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (client *Client) receive() &#123;    var err error    for err &#x3D;&#x3D; nil &#123;        var h codec.Header        if err &#x3D; client.cc.ReadHeader(&amp;h); err !&#x3D; nil &#123;            break        &#125;        call :&#x3D; client.removeCall(h.Seq)        switch &#123;        case call &#x3D;&#x3D; nil:            &#x2F;&#x2F; it usually means that Write partially failed            &#x2F;&#x2F; and call was already removed            arr :&#x3D; client.cc.ReadBody(nil)        case h.Error !&#x3D; &quot;&quot;:            call.Error &#x3D; fmt.Errorf(h.Error)            err &#x3D; client.cc.ReadBody(nil)            call.done()        default:            err &#x3D; client.cc.ReadBody(call.Reply)            if err !&#x3D; nil &#123;                call.Error &#x3D; errors.New(&quot;reading body &quot; + err.Error())            &#125;            call.done()        &#125;    &#125;    &#x2F;&#x2F; error occurs, so terminateCalls pending calls    client.terminateCalls(err)&#125;</code></pre><p>创建 Client实例时，首先需要完成一开始的协议交换，即发送<code>Option</code>信息给服务端。协商好消息的编解码方式之后，再创建一个子协程<code>receive()</code>接收响应。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func NewClient(conn net.conn, opt *Option) (*Client, error) &#123;    f :&#x3D; codec.NewCodecFuncMap[opt.CodecType]    if f &#x3D;&#x3D; nil &#123;        err :&#x3D; fmt.Errorf(&quot;invalid codec type %s&quot;, opt.CodecType)        log.Println(&quot;rpc client: options error: &quot;, err)        return nil, err    &#125;    &#x2F;&#x2F; send options with server    if err :&#x3D; json.NewEncoder(conn).Encode(opt); err !&#x3D; nil &#123;        log.Println(&quot;rpc client: options error: &quot;, err)        _ &#x3D; conn.Close()        return nil, err    &#125;    return newClientCodec(f(conn), opt), nil&#125;func newClientCodec(cc codec.Codec, opt *Option) *Client &#123;    client :&#x3D; &amp;Client &#123;        seq:     1, &#x2F;&#x2F; seq starts with 1, 0 means invalid call        cc:      cc,        opt:     opt,        pending: make(map[uint64]*Call)    &#125;    go client.receive()    return client&#125;</code></pre><p>还需要实现<code>Dial</code>函数，便于用户传入服务端地址，创建 Client实例。为了简化用户调用，通过<code>...*Option</code>将 Option实现为可选参数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func parseOptions(opts ...*Option) (*Option, error) &#123;    &#x2F;&#x2F; if opts is nil or pass nil as parameter    if len(opts) &#x3D;&#x3D; 0 || opts[0] &#x3D;&#x3D; nil &#123;        return DefaultOption, nil    &#125;    if len(opts) !&#x3D; 1 &#123;        return nil, errors.New(&quot;number of options is more than 1&quot;)    &#125;    opt :&#x3D; opts[0]    opt.MagicNumber &#x3D; DefaultOption.MagicNumber    if opt.CodecType &#x3D;&#x3D; &quot;&quot; &#123;        opt.CodecType &#x3D; DefaultOption.CodecType    &#125;    return opt, nil&#125;&#x2F;&#x2F; Dial connects to an RPC server at the specified network addressfunc Dial(network, address string, opts ...*Option) (client *Client, err error) &#123;    opt, err :&#x3D; parseOptions(opts...)    if err !&#x3D; nil &#123;        return nil, err    &#125;    conn, err :&#x3D; net.Dial(network, address)    if err !&#x3D; nil &#123;        return nil, err    &#125;    &#x2F;&#x2F; close the connection if client is nil    defer func() &#123;        if client &#x3D;&#x3D; nil &#123;            _ &#x3D; conn.Close()        &#125;    &#125;()    return NewClient(conn, opt)&#125;</code></pre><p>此时，GeeRPC客户端已经具备了完整的创建连接和接受响应的能力了，最后还需要实现发送请求的能力。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (client *Client) send(call *Call) &#123;    &#x2F;&#x2F; make sure that the client will send a complete request    client.sending.Lock()    defer client.sending.Unlock()        &#x2F;&#x2F; register this call    seq, err :&#x3D; client.registerCall(call)    if err !&#x3D; nil &#123;        call.Error() &#x3D; err        call.done()        return    &#125;        &#x2F;&#x2F; prepare request header    client.header.ServiceMethod &#x3D; call.ServiceMethod    client.header.Seq &#x3D; seq    client.header.Error &#x3D; &quot;&quot;        &#x2F;&#x2F; encode and send the request    if err :&#x3D; client.cc.Write(&amp;client.header, call.Args); err !&#x3D; nil &#123;        call :&#x3D; client.removeCall(seq)        &#x2F;&#x2F; call may be nil, it usually means that Write partially failed,        &#x2F;&#x2F; client has receive the response and handled        if call !&#x3D; nil &#123;            call.Error &#x3D; err            call.done()        &#125;    &#125;&#125;&#x2F;&#x2F; Go invokes the function asynchronously&#x2F;&#x2F; It returns the Call structure representing the invocationfunc (client *Client) Go(serviceMethod string, args, reply interface&#123;&#125;, done chan *Call) *Call &#123;    if done &#x3D;&#x3D; nil &#123;        done &#x3D; make(chan *Call, 10)    &#125; else if cap(done) &#x3D;&#x3D; 0 &#123;        log.Panic(&quot;rpc client: done channel is unbuffered&quot;)    &#125;    call :&#x3D; &amp;Call &#123;        ServiceMethod: serviceMethod,        Args:          args,        Reply:         reply,        Done:          done,    &#125;    client.send(call)    return call&#125;&#x2F;&#x2F; Call invokes the named function, waits for it to complete,&#x2F;&#x2F; and returns its error statusfunc (client *Client) Call(serviceMethod string, args, reply interface&#123;&#125;) error &#123;    call :&#x3D; &lt;- client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done    return call.Error&#125;</code></pre><ul><li><code>Go</code>和<code>Call</code>是客户端暴露给用户的两个 RPC服务调用接口，<code>Go</code>是一个异步接口，返回 call 实例。</li><li><code>Call</code>是对<code>Go</code>的封装，阻塞call.Done，等待响应返回，是一个同步接口。</li></ul><p>至此，一个支持异步和并发的 GeeRPC 客户端已经完成。</p><p><strong>补充</strong></p><p>defer的运行机制为，在return之后，在函数退出之前执行。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func test() (ans int) &#123;    defer func() &#123;        fmt.Println(ans)    &#125;()    return 10&#125;func main() &#123;    test()&#125;</code></pre><p>运行结果为：10。</p><h4 id="demo">Demo</h4><p>第一天 GeeRPC 只实现了服务端，因此我们在 main函数中手动模拟了整个通信过程，第二天中我们将 main函数中的通信部分替换为客户端。</p><p><strong>day2/main/main.go</strong></p><p>startServer 没有发生变化。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func startServer(addr chan string) &#123;    &#x2F;&#x2F; pick a free port    l, err :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:0&quot;)    if err !&#x3D; nil &#123;        log.Fatal(&quot;network error: &quot;, err)    &#125;    log.Println(&quot;start rpc server on&quot;, l.Addr())    addr &lt;- l.Addr().String()    geerpc.Accept(l)&#125;</code></pre><p>在 main 函数中使用了<code>client.Call</code>并发了5个 RPC同步调用，参数和返回值类型均为 string。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func main() &#123;    log.SetFalgs(0)    addr :&#x3D; make(chan string)    go startServer(addr)    client, _ &#x3D; geerpc.Dial(&quot;tcp&quot;, &lt;-addr)    defer func() &#123;        _ &#x3D; client.Close()    &#125;()        time.Sleep(time.Second)    &#x2F;&#x2F; send request &amp; receive response    var wg sync.WaitGroup    for i :&#x3D; 0; i &lt; 5; i++ &#123;        wg.Add(1) &#x2F;&#x2F; 每一个任务开始时，将等待组增加1        &#x2F;&#x2F; 开启一个并发        go func(i int) &#123;            defer wg.Done()            args :&#x3D; fmt.Sprintf(&quot;geerpc req %d&quot;, i)            var reply string            if err :&#x3D; client.Call(&quot;Foo.Sum&quot;, args, &amp;reply); err !&#x3D; nil &#123;                log.Fatal(&quot;call Foo.Sum error: &quot;, err)            &#125;            log.Println(&quot;reply&quot;, reply)        &#125;(i)    &#125;    wg.Wait() &#x2F;&#x2F; 等待所有任务完成&#125;</code></pre><p>运行结果如下 (不唯一)：</p><pre class="line-numbers language-none"><code class="language-none">start rpc server on [::]:36013&amp;&#123;Foo.Sum 5&#125; geerpc req 3&amp;&#123;Foo.Sum 1&#125; geerpc req 4&amp;&#123;Foo.Sum 2&#125; geerpc req 1&amp;&#123;Foo.Sum 3&#125; geerpc req 0&amp;&#123;Foo.Sum 4&#125; geerpc req 2reply: geerpc resp 4reply: geerpc resp 5reply: geerpc resp 1reply: geerpc resp 2reply: geerpc resp 3</code></pre><p>当然也有这种情况</p><pre class="line-numbers language-none"><code class="language-none">&amp;&#123;Foo.Sum 1 &#125; geerpc req 4&amp;&#123;Foo.Sum 3 &#125; geerpc req 0&amp;&#123;Foo.Sum 2 &#125; geerpc req 1reply: geerpc resp 3reply: geerpc resp 1reply: geerpc resp 2&amp;&#123;Foo.Sum 5 &#125; geerpc req 3&amp;&#123;Foo.Sum 4 &#125; geerpc req 2reply: geerpc resp 5reply: geerpc resp 4</code></pre><p>对于以上执行结果，加以个人的理解，添加了若干个协程，并同步调用，其中会出现延迟开启并发的现象。</p><h3 id="day3.-服务注册">day3. 服务注册</h3><ul><li>通过反射实现服务注册功能。</li></ul><h4 id="结构体映射为服务">结构体映射为服务</h4><p>RPC框架的一个基本能力是：像调用本地程序一样调用远程服务。关于如何将程序映射为服务，对于Go 来说，这个问题就变成了如何将结构体的方法映射为服务。</p><p>对<code>net/rpc</code>而言，一个函数需要能够被远程调用，需要满足以下五个条件：</p><ul><li>the method's type is exported. - 方法所属的类型是导出的。</li><li>the method is exported. - 方式是导出的。</li><li>the method has two arguments, both expoerted (or builtin) types. -两个入参，均为导出 or 内置类型。</li><li>the method's second argument is a pointer. -第二个入参必须是一个指针。</li><li>the method has return type error. - 返回值为 error 类型。</li></ul><p>更直观一些：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (t *T)  MethodName(argType T1, replyType *T2) error</code></pre><p>假如客户端发来一个请求，包含 ServiceMethod 和 Argv。</p><pre class="line-numbers language-none"><code class="language-none">&#123;    &quot;ServiceMethod&quot;: &quot;T.MethodName&quot; &quot;Argv&quot;: &quot;001010010100...&quot; &#x2F;&#x2F; 序列化之后的字节流&#125;</code></pre><p>通过 <code>T.MethodName</code>可以确定调用的是类型 T的<code>MethodName</code>，如果硬编码实现这个功能，很可能是这样：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">switch req.ServiceMethod &#123;    case &quot;T.MethodName&quot;:        t :&#x3D; new(t)        reply :&#x3D; new(T2)        var argv T1        gob.NewDecoder(conn).Decode(&amp;argv)        err :&#x3D; t.MethodName(argv, reply)        server.sendMessage(reply, err)    case &quot;Foo.Sum&quot;:        f :&#x3D; new(Foo)    ...&#125;</code></pre><p>也就是说，如果使用硬编码的方式来实现结构体与服务的映射，那么每暴露一个方法，就需要编写等量的代码。那么有没有什么方法，能够将这个映射过程自动化呢？可以借助反射。</p><p>通过反射，我们能够很容易获取某个结构体的所有方法，并且能通过所有方法，获取到该方法的所有参数类型与返回值。例如：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func main() &#123;    var wg sync.WaitGroup    typ :&#x3D; reflect.TypeOf(&amp;wg)    for i :&#x3D; 0; i &lt; typ.NumMethod(); i++ &#123;        method :&#x3D; typ.Method(i)        argv :&#x3D; make([]string, 0, method.Type.NumIn())        returns :&#x3D; make([]string, 0, method.Type.NumOut())        &#x2F;&#x2F; j从1开始，第0个入参是wg自己        for j :&#x3D; 1; j &lt; method.Type.In(j); j++ &#123;            argv &#x3D; append(argv, method.Type.In(j).Name())        &#125;        for j :&#x3D; 0; j &lt; method.Type.NumOut(); j++ &#123;            returns &#x3D; append(returns, method.Type.Out(j).Name())        &#125;        log.Printf(&quot;func (w *%s) %s(%s) %s&quot;,           typ.Elem().Name(),           method.Name,           strings.Join(argv, &quot;,&quot;),           strings.Join(returns, &quot;,&quot;))    &#125;&#125;</code></pre><p>运行结果为：</p><pre class="line-numbers language-none"><code class="language-none">func (w *WaitGroup) Add(int)func (w *WaitGroup) Done()func (w *WaitGroup) Wait()</code></pre><h4 id="通过反射实现-service">通过反射实现 service</h4><p>前两天我们完成了客户端和服务端，客户端相对来说功能是比较完整的，但是服务端的功能并不完整，仅仅将请求的header打印了出来，并没有真正地处理。那今天的主要目的是补全这部分功能。首先通过反射实现结构体与服务的映射关系，代码独立放置在<code>service.go</code>中。</p><p><strong>day3/service.go</strong></p><p>第一步，定义结构体 methodType：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type methodType struct &#123;    method    reflect.Method    ArgType   reflect.Type    ReplyType reflect.Type    numCalls  uint64&#125;func (m *methodType) NumCalls() uint64 &#123;    return atomic.LoadUint64(&amp;m.numCalls)&#125;func (m *methodType) newArgv() reflect.Value &#123;    var argv reflect.Value    &#x2F;&#x2F; arg may be a pointer type, or a value type    if m.ArgType.Kind() &#x3D;&#x3D; reflect.Ptr &#123;        argv &#x3D; reflect.New(m.ArgType.Elem())    &#125; else &#123;        argv &#x3D; reflect.New(m.ArgType).Elem()    &#125;    return argv&#125;func (m *methodType) newReplyv() reflect.Value &#123;    &#x2F;&#x2F; reply must be a pointer type    replyv :&#x3D; reflect.New(m.ReplyType.Elem())    switch m.ReplyType.Elem().Kind() &#123;    case reflect.Map:        replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem()))    case refelct.Slice:        replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0))    &#125;    return replyv&#125;</code></pre><p>每一个 methodType 实例包含了一个方法的完整信息。包括：</p><ul><li>method：方法本身</li><li>ArgType：第一个参数的类型</li><li>ReplyType：第二个参数的类型</li><li>numCalls：后续统计方法调用次数时会用到</li></ul><p>另外，我们还实现了2个方法<code>newArgv</code>和<code>newReplyv</code>，用于创建对应类型的实例。<code>newArgv</code>方法有一个小细节，指针类型和值类型创建实例的方法有细微区别。</p><p>第二部，定义结构体 service：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type service struct &#123;    name   string    typ    reflect.Type    rcvr   reflect.Value    method map[string]*methodType&#125;</code></pre><p>service 的定义也是非常简洁的，name即映射的结构体的名称，比如<code>T</code>，比如<code>WaitGroup</code>；typ是结构体的类型；rcvr即结构体的实例本身，保留 rcvr 是因为在调用时需要 rcvr作为第0个参数；method 是 map类型，储存映射的结构体的所有符合条件的方法。</p><p>接下来，完成构造函数<code>newService</code>，入参是任意需要映射为服务的结构体实例。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func newService(rcvr interface&#123;&#125;) *service &#123;    s :&#x3D; new(service)    s.rcvr &#x3D; reflect.ValueOf(rcvr)    s.name &#x3D; reflect.Indirect(s.rcvr).Type().Name()    s.typ &#x3D; reflect.TypeOf(rcvr)    if !ast.IsExported(s.name) &#123;        log.Fatalf(&quot;rpc server: %s is not a valid service name&quot;, s.name)    &#125;    s.registerMethods()    return s&#125;func (s *service) registerMethods() &#123;    s.method &#x3D; make(map[string]*methodType)    for i :&#x3D; 0; i &lt; s.typ.NumMethod(); i++ &#123;        method :&#x3D; s.typ.Method(i)        mType :&#x3D; method.Type        if mType.NumIn() !&#x3D; 3 || mType.NumOut() !&#x3D; 1 &#123;            continue        &#125;        if mType.Out(0) !&#x3D; reflect.TypeOf((*error)(nil)).Elem() &#123;            continue        &#125;        argType, replyType :&#x3D; mType.In(1), mType.In(2)        if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) &#123;            continue        &#125;        s.method[method.Name] &#x3D; &amp;methodType &#123;            method:    method,            ArgType:   argType,            ReplyType: replyType,        &#125;        log.Printf(&quot;rpc server: register %s.%s\n&quot;, s.name, method.Name)    &#125;&#125;func isExportOrBuiltinType(t reflect.Type) bool &#123;    return ast.IsExported(t.Name()) || t.PkgPath() &#x3D;&#x3D; &quot;&quot;&#125;</code></pre><p><code>registerMethods</code>过滤出了符合条件的方法：</p><ul><li>两个导出或内置类型的入参 (反射时为3个，第0个是自身，类似于 python 的self，Java 中的this )</li><li>返回值有且只有一个，类型为 error</li></ul><p>最后，我们还需要实现<code>call</code>方法，即能够通过反射值调用方法。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (s *service) call(m *methodType, argv, replyv reflect.Value) error &#123;    atomic.AddUint64(&amp;m.numCalls, 1)    f :&#x3D; m.method.Func    returnValues :&#x3D; f.Call([]reflect.Value&#123;s.rcvr, argv, replyv&#125;)    if errInter :&#x3D; returnValues[0].Interface(); errInter !&#x3D; nil &#123;        return errInter.(error)    &#125;    return nil&#125;</code></pre><h4 id="service-的测试用例">service 的测试用例</h4><p>为了保证 service 实现的正确性，我们为 service.go写了几个测试用例。</p><p><strong>day3/service_test.go</strong></p><p>定义结构体 Foo，实现2个方法，导出方法 Sum 和非导出方法 sum。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Foo inttype Args struct &#123; Num1, Num2 int&#125;func (f Foo) Sum(args Args, reply *int) error &#123;    *reply &#x3D; args.Num1 + args.Num2    return nil&#125;&#x2F;&#x2F; it&#39;s not a exported Methodfunc (f Foo) sum(args Args, reply *int) error &#123;    *reply &#x3D; args.Num1 + args.Num2    return nil&#125;&#x2F;&#x2F; 这里要注意，是两个不一样的函数，后面的测试中要注意写的函数名，会影响测试结果func _assert(condition bool, msg string, v ...interface&#123;&#125;) &#123;    if !condition &#123;        panic(fmt.Sprintf(&quot;assertion failed: &quot; + msg, v...))    &#125;&#125;</code></pre><p>测试 newService 和 call 方法。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func TestNewService(t *testing.T) &#123;    var foo Foo    s :&#x3D; newService(&amp;foo)    _assert(len(s.method) &#x3D;&#x3D; 1, &quot;wrong service Method, expect 1, but got %d&quot;, len(s.method))    mType :&#x3D; s.method[&quot;Sum&quot;]    _assert(mType !&#x3D; nil, &quot;wrong Method, Sum should&#39;t nil&quot;)&#125;func TestMethodType_Call(t *testing.T) &#123;    var foo Foo    s :&#x3D; newService(&amp;foo)    mType :&#x3D; s.method(&quot;Sum&quot;)        argv :&#x3D; mType.newArgv()    replyv :&#x3D; mType.newReplyv()    argv.Set(reflect.ValueOf(Args&#123;Num1: 1, Num2: 3&#125;))    err :&#x3D; s.call(mType, argv, replyv)    _assert(err &#x3D;&#x3D; nil &amp;&amp; *replyv.Interface().(*int) &#x3D;&#x3D; 4 &amp;&amp; mType.NumCalls() &#x3D;&#x3D; 1, &quot;failed to call Foo.Sum&quot;)&#125;</code></pre><p>这里的测试，卡了我大约2天了，开始一直没搞明白为什么注册的方法一直是"Sum"，而不是"sum"，然而，我一直在<code>service.go</code>里找，各种print打印相关信息，也还是找不出个所以然，其实我犯了个很低级的错误，<code>service.go</code>这一类是高度抽象的，一般不会有很具体的内容，问题只能出在<code>service_test.go</code>中，在无头绪找bug的第三天，我尝试改<code>Sum</code>函数，发现输出的内容变了，后面注意到导出和非导出函数，好吧，原来问题出在这，<code>Sum</code>和<code>sum</code>都是Foo有的函数，在golang中，小写字段不可从包外访问，所以注册的是大写的<code>Sum</code>。</p><h4 id="集成到服务端">集成到服务端</h4><p>通过反射结构体已经映射为服务，但请求的处理还没有完成。从接收到请求到回复还差以下几个步骤：</p><ul><li>根据入参类型，将请求的 body 反序列化。</li><li>调用<code>service.call</code>，完成方法调用。</li><li>将 reply 序列化为字节流，构造响应报文，返回。</li></ul><p>回到代码本身，补全之前在<code>server.go</code>中遗留的2个 TODO任务<code>readRequest</code>和<code>handleRequest</code>即可。</p><p>在这之前，我们还需要为 Server 实现一个方法<code>Register</code>。</p><p><strong>day3/server.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Server represents an RPC Servertype Server struct &#123;    service sync.Map&#125;&#x2F;&#x2F; Register publishes in the server the set of methods func (server *Server) Register(rcvr interface&#123;&#125;) error &#123;    s :&#x3D; newService(rcvr)    if _, dup :&#x3D; server.serviceMap.LoadOrStore(s.name, s); dup &#123;        return errors.New(&quot;rpc: service already defined: &quot;, + s.name)    &#125;    return nil&#125;&#x2F;&#x2F; Register publishes the receiver&#39;s methods in the DefaultServerfunc Register(rcvr interface&#123;&#125;) error &#123;    return DefaultServer.Register(rcvr)&#125;</code></pre><p>配套实现<code>findService</code>方法，即通过<code>ServiceMethod</code>从serviceMap 中找到对应的 service。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) &#123;    dot :&#x3D; strings.LastIndex(serviceMethod, &quot;.&quot;)    if dot &lt; 0 &#123;        err :&#x3D; errors.New(&quot;rpc server: service&#x2F;method request ill-formed: &quot; + serviceMethod)        return    &#125;    serviceName, methodName :&#x3D; serviceMethod[:dot], serviceMethod[dot+1:]    svci, ok :&#x3D; server.serviceMap.Load(serviceName)    if !ok &#123;        err :&#x3D; errors.New(&quot;rpc server: can&#39;t find service &quot; + serviceName)        return    &#125;    svc &#x3D; svci.(*service)    mtype &#x3D; svc.method[methodName]    if mtype &#x3D;&#x3D; nil &#123;        err &#x3D; errors.New(&quot;rpc server: can&#39;t find method &quot; + methodName)    &#125;    return&#125;</code></pre><p><code>findService</code>的实现看似比较繁琐，但是逻辑还是非常清晰的。因为<code>ServiceMethod</code>的构成是"Service.Method"，因此先将其分割成2部分，第一部分是 Service的名称，第二部分即方法名。现在 serviceMap 中找到对应的 service实例，再从 service 实例的 method 中，找到对应的 methodType。</p><p>准备工具已经就绪，我们首先补全 readRequest 方法：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; request stores all information of a calltype request struct &#123;    h            *codec.Header &#x2F;&#x2F; header of request    argv, replyv reflect.Value &#x2F;&#x2F; argv and replyv of request    mtype        *methodType    svc          *service&#125;func (server *Server) readRequest(cc codec.Codec) (*reqeust, error) &#123;    h, err :&#x3D; server.readRequest(cc)    if err !&#x3D; nil &#123;        return nil, err    &#125;    req :&#x3D; &amp;request&#123;h: h&#125;    req.svc, req.mtype, err &#x3D; server.findService(h.ServiceMethod)    if err !&#x3D; nil &#123;        return req, err    &#125;    req.argv &#x3D; req.mtype.newArgv()    req.replyv &#x3D; req.mtype.newReplyv()        &#x2F;&#x2F; make sure that argvi is a pointer, ReadBody need a pointer as parameter    argvi :&#x3D; req.argv.Interface()    if req.argv.Type().Kind() !&#x3D; reflect.Ptr &#123;        argvi &#x3D; req.argv.Addr().Interface()    &#125;    if err &#x3D; cc.ReadBody(argvi); err !&#x3D; nil &#123;        log.Println(&quot;rpc server: read body err: &quot;, err)        return req, err    &#125;    return req, nil&#125;</code></pre><p>readRequest方法中最重要的部分，即通过<code>newArgv()</code>和<code>newReplyv()</code>两个方法创建出两个入参实例，然后通过<code>cc.ReadBody()</code>将请求报文反序列化为第一个入参argv，在这里同样要注意 argv可能是值类型，也可能是指针类型，所以处理方式有点差异。</p><p>接下来补全 handleRequest 方法：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) &#123;    defer wg.Done()    err :&#x3D; req.svc.call(req.mtype, req.argv, req.replyv)    if err !&#x3D; nil &#123;        req.h.Error &#x3D; err.Error()        server.sendResponse(cc, req.h, invalidRequest, sending)        return    &#125;    server.sendResponse(cc, req.h, replyv.Interface(), sending)&#125;</code></pre><p>相对于 readRequest，handleRequest的实现非常简单，通过<code>req.svc.call</code>完成方法调用，将 replyv传递给 sendResponse 完成序列化即可。</p><p>到这里，今天所有内容已实现完成，成功在服务端实现了服务注册与调用。</p><h4 id="demo-1">Demo</h4><p>最后，修改下 main 验证成果。</p><p><strong>day3/main/main.go</strong></p><p>第一步，定义结构体 Foo 和方法 Sum。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (    &quot;geerpc&quot;    &quot;log&quot;    &quot;net&quot;    &quot;sync&quot;    &quot;time&quot;)type Foo inttype Args struct&#123; Num1, Num2 int &#125;func (f Foo) Sum(args Args, reply *int) error &#123;    *reply &#x3D; args.Num1 + args.Num2    return nil&#125;</code></pre><p>第二步，注册 Foo 到 Server 中，并启动 RPC 服务。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func startServer(addr chan string) &#123;    var foo Foo    if err :&#x3D; geerpc.Register(&amp;foo); err !&#x3D; nil &#123;        log.Fatal(&quot;register error: &quot;, err)    &#125;    &#x2F;&#x2F;pick a free port    l, err :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:0&quot;)    if err !&#x3D; nil &#123;        log.Fatal(&quot;network error: &quot;, err)    &#125;    log.Println(&quot;start rpc server on&quot;, l.Addr())    addr &lt;- l.Addr().String()    geerpc.Accept(l)&#125;</code></pre><p>第三步，构造参数，发送 RPC 请求，并打印结果。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func main() &#123;    log.SetFlags(0)    addr :&#x3D; make(chan string)    go startServer(addr)    client, _ :&#x3D; geerpc.Dial(&quot;tcp&quot;, &lt;-addr)    defer func() &#123;        _ &#x3D; client.Close()    &#125;()        time.Sleep(time.Second)    &#x2F;&#x2F; send request &amp; receive response    var wg sync.WaitGroup    for i :&#x3D; 0; i &lt; 5; i++ &#123;        wg.Add(1)        go func(i int) &#123;            defer wg.Done()            args :&#x3D; &amp;Args&#123;Num1: i, Num2: i * i&#125;            var reply int            if err :&#x3D; client.Call(&quot;Foo.Sum&quot;, args, &amp;reply); err !&#x3D; nil &#123;                log.Fatal(&quot;call Foo.Sum error: &quot;, err)            &#125;            log.Printf(&quot;%d + %d &#x3D; %d&quot;, args.Num1, args.Num2, reply)        &#125;(i)    &#125;    wg.Wait()&#125;</code></pre><p>运行结果如下：</p><pre class="line-numbers language-none"><code class="language-none">rpc server: register Foo.Sumstart rpc server on [::]:575090 + 0 &#x3D; 02 + 4 &#x3D; 64 + 16 &#x3D; 203 + 9 &#x3D; 121 + 1 &#x3D; 2</code></pre><h3 id="day4.-超时处理">day4. 超时处理</h3><h4 id="为什么要超时处理机制">为什么要超时处理机制</h4><p>超时处理是 RPC框架一个比较基本的能力，如果缺少超时处理机制，无论是服务端还是客户端都容易因为网络或其他错误导致挂死，资源耗尽，这些问题的出现大大降低了服务的可用性。因此，我们需要在RPC 框架中加入超时处理的能力。</p><p>纵观整个远程调用的过程，需要客户端处理超时的地方有：</p><ul><li>与服务端建立连接，导致的超时。</li><li>发送请求到服务端，写报文导致的超时。</li><li>等待服务端处理时，等待处理导致的潮实(比如服务端已挂死，迟迟不响应)</li><li>从服务端接收响应时，读报文导致的超时。</li></ul><p>需要服务端处理超时的地方有：</p><ul><li>读取客户端请求报文时，读报文导致的超时。</li><li>发送响应报文时，写报文导致的超时。</li><li>调用映射服务的方法时，处理报文导致的超时。</li></ul><p>GeeRPC 在3个地方添加了超时处理机制。分别是：</p><ul><li>客户端创建连接时。</li><li>客户端<code>Client.Call()</code>整个过程导致的超时(包含发送报文，等待处理，接收报文所有阶段)。</li><li>服务端处理报文，即<code>Server.handleRequest</code>超时。</li></ul><h4 id="创建连接超时">创建连接超时</h4><p>为了实现上的简单，将超时设定放在了 Option中。<code>ConnectTimeout</code>的默认值为10s，<code>HandleTimeout</code>默认值为0，即不设限。</p><p><strong>day4/server.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Option struct &#123;    MagicNumber    int &#x2F;&#x2F; MagicNumber marks this&#39;s a geerpc request    CodecType      codec.Type &#x2F;&#x2F; client may choose different Codec to encode body    ConnectTimeout time.Duration &#x2F;&#x2F; 0 means no limit    HandleTimeout  time.Duration&#125;var DefaultOption &#x3D; &amp;Option &#123;    MagicNumber:    MagicNumber,    CodecType:      codec.GobType,    ConnectTimeout: time.Second * 10,&#125;</code></pre><p>客户端连接超时，只需要为 Dial 添加一层超时处理的外壳即可。</p><p><strong>day4/client.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type clientResult struct &#123;    client *Client    err    error&#125;type newClientFunc func(conn net.Conn, opt *Option) (client *Client, err error) &#123;    opt, err :&#x3D; parseOptions(opts...)    if err !&#x3D; nil &#123;        return nil, err    &#125;    conn, err :&#x3D; net.DialTimeout(network, address, opt.ConnectTimeout)    if err !&#x3D; nil &#123;        return nil, err    &#125;    &#x2F;&#x2F; close the connection if client is nil    defer func() &#123;        if err !&#x3D; nil &#123;            _ &#x3D; conn.Close()        &#125;    &#125;()    ch :&#x3D; make(chan clientResult)    go func() &#123;        client, err :&#x3D; f(conn, opt)        ch &lt;- clientResult&#123;client: client, err: err&#125;    &#125;()    select &#123;        case &lt;-time.After(opt.ConnectTimeout):            return nil, fmt.Errorf(&quot;rpc client: connect timeout: expect within %s&quot;, opt.ConnectTimeout)        case result :&#x3D; &lt;-ch:            return result.client, result.err    &#125;&#125;&#x2F;&#x2F; Dial connects to an RPC server at the specified network addressfunc Dial(network, address string, opts ...*Option) (*Client, error) &#123;    return dialTimeout(NewClient, network, address, opts...)&#125;</code></pre><p>在这里实现了一个超时处理的外壳<code>dialTimeout</code>，这个壳将<code>NewClient</code>作为入参，在2个地方添加了超时处理的机制。</p><ol type="1"><li>将<code>net.Dial</code>替换为<code>net.DialTimeout</code>，如果连接创建超时，将返回错误。</li><li>使用子协程执行<code>NewClient</code>，执行完成后则通过信道 ch发送结果，如果<code>time.After()</code>信道先接收到消息，则说明<code>NewClient</code>执行超时，返回错误。</li></ol><h4 id="client.call-超时">Client.Call 超时</h4><p><code>Client.Call</code>的超时处理机制，使用 context包实现，控制权交给用户，控制更为灵活。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Call invokes the named function, waits for it to complete,&#x2F;&#x2F; and returns its error status.func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface&#123;&#125;) error &#123;    call :&#x3D; client.Go(serviceMethod, args, reply, make(chan *Call, 1))    select &#123;    case &lt;-ctx.Done():        client.removeCall(call.Seq)        return errors.New(&quot;rpc client: call failed: &quot; + ctx.Err().Error())    case call :&#x3D; &lt;-call.Done:        return call.Error    &#125;&#125;</code></pre><p>用户可以使用<code>context.WithTimeout</code>创建具备超时检测能力的context 对象来控制，例如：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">ctx, _ :&#x3D; context.WithTimeout(context.Background(), time.Second)var reply interr :&#x3D; client.Call(ctx, &quot;Foo.Sum&quot;, &amp;Args&#123;1, 2&#125;, &amp;reply)...</code></pre><h4 id="服务端处理超时">服务端处理超时</h4><p>这一部分的实现与客户端很接近，使用<code>time.After()</code>结合<code>select + chan</code>完成。</p><p><strong>day4/server.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) &#123;    defer wg.Done()    called :&#x3D; make(chan struct&#123;&#125;)    sent :&#x3D; make(chan struct&#123;&#125;)    go func() &#123;        err :&#x3D; req.svc.call(req.mtype, req.argv, req.replyv)        called &lt;- struct&#123;&#125;&#123;&#125;        if err !&#x3D; nil &#123;            req.h.Error &#x3D; err.Error()            server.sendResponse(cc, req.h, invalidRequest, sending)            sent &lt;- struct&#123;&#125;&#123;&#125;            return         &#125;    &#125;()        if timeout &#x3D;&#x3D; 0 &#123;        &lt;-called        &lt;-sent        &#x2F;&#x2F; 从信道获取值，忽略结果(类似于pop())        return    &#125;    select &#123;    case &lt;-time.After(timeout):        req.h.Error &#x3D; fmt.Sprintf(&quot;rpc server: request handle timeout: expect within %s&quot;, timeout)        server.sendResponse(cc, req.h, invalidRequest, sending)    case &lt;-called:        &lt;-sent    &#125;&#125;</code></pre><p>这里需要确保<code>sendResponse</code>仅调用一次，因此将整个过程拆分为<code>called</code>和<code>sent</code>两个阶段，在这段代码中只会发生如下两种情况：</p><ul><li>called 信道接收到消息，代表处理没有超时，继续执行<code>sendResponse</code>。</li><li><code>time.After()</code>先于 called接收到消息，说明处理已经超时，called 和 sent都将被阻塞。在<code>case &lt;-time.After(timeout)</code>处调用<code>sendResponse</code>。</li></ul><h4 id="测试用例">测试用例</h4><p><strong>day4/client_test.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func TestClient_dialTimeout(t *testing.T) &#123;    t.Parallel()    l, _ :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:0&quot;)        f :&#x3D; func(conn net.Conn, opt *Option) (client *Client, err error) &#123;        _ &#x3D; conn.Close()        time.Sleep(time.Second * 2)        return nil, nil    &#125;    t.Run(&quot;timeout&quot;, func(t *testing.T) &#123;        _, err :&#x3D; dialTimeout(f, &quot;tcp&quot;, l.Addr().String(), &amp;Option&#123;ConnectTimeout: time.Second&#125;)        _assert(err !&#x3D; nil &amp;&amp; strings.Contains(err.Error(), &quot;connect timeout&quot;), &quot;expect a timeout error&quot;)    &#125;)    t.Run(&quot;0&quot;, func(t *testing.T) &#123;        _, err :&#x3D; dialTimeout(f, &quot;tcp&quot;, l.Addr().String(), &amp;Option&#123;ConnectTimeout: 0&#125;)        _assert(err &#x3D;&#x3D; nil, &quot;0 means no limit&quot;)    &#125;) &#125;</code></pre><p>第二个测试用例，用于测试处理超时。<code>Bar.Timeout</code>耗时2s，场景一：客户端设置超时时间为1s，服务端无限制；场景二，服务端设置超时时间为1s，客户端无限制。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Bar intfunc (b Bar) Timeout(argv int, reply *int) error &#123;    time.Sleep(time.Second * 2)    return nil&#125;func startServer(addr chan string) &#123;    var b Bar    _ &#x3D; Register(&amp;b)    &#x2F;&#x2F; pick a free port    l, _ :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:0&quot;)    addr &lt;- l.Addr().String()    Accept(l)&#125;func TestClient_Call(t *testing.T) &#123;    t.Parallel()    addrChh :&#x3D; make(chan string)    go startServer(addrCh)    addr :&#x3D; &lt;-addrCh    time.Sleep(time.Second)    t.Run(&quot;client timeout&quot;, func(t *testing.T) &#123;        client, _ :&#x3D; Dial(&quot;tcp&quot;, addr)        ctx, _ :&#x3D; context.WithTimeout(context.Background(), time.Second)        var reply int        err :&#x3D; client.Call(ctx, &quot;Bar.Timeout&quot;, 1, &amp;reply)        _assert(err !&#x3D; nil &amp;&amp; strings.Contains(err.Error(), ctx.Err().Error()), &quot;expect a timeout error&quot;)    &#125;)    t.Run(&quot;server handle timeout&quot;, func(t *testing.T) &#123;        client, _ :&#x3D; Dial(&quot;tcp&quot;, addr, &amp;Option&#123;            HandleTimeout: time.Second,        &#125;)        var reply int        err :&#x3D; client.Call(context.Background(), &quot;Bar.Timeout&quot;, 1, &amp;reply)        _assert(err !&#x3D; nil &amp;&amp; strings.Contains(err.Error(), ctx.Err().Error()), &quot;expect a timeout error&quot;)    &#125;)    &#125;</code></pre><h3 id="day5.-支持http协议">day5. 支持HTTP协议</h3><ul><li>支持 HTTP 协议</li><li>基于 HTTP 实现一个简单的 Debug 页面，代码约 150 行。</li></ul><h4 id="支持-http-协议需要做什么">支持 HTTP 协议需要做什么？</h4><p>Web 开发中，我们经常使用 HTTP 协议中的 HEAD、GET、POST等方式发送请求，等待响应。但 RPC 的消息格式与标准的 HTTP协议并不兼容，在这种情况下，就需要一个协议的转换过程。HTTP 协议的CONNECT 方法恰好提供了这个能力，CONNECT 一般用于代理服务。</p><p>假设浏览器与服务器之间的 HTTPS通信都是加密的，浏览器通过代理服务器发起 HTTPS请求时，由于请求的站点地址和端口号都是加密保存在 HTTPS请求报文头中的，代理服务器如何直到往哪里发送请求呢？为了解决这个问题，浏览器通过HTTP 明文形式向代理服务器发送一个 CONNECT请求告诉代理服务器目标地址和端口，代理服务器接收到这个请求后，会在对应端口和目标站点建立一个TCP 连接，连接建立成功后返回 HTTP 200状态码告诉浏览器与该站点的加密通道已经完成。接下来代理服务器仅需透传浏览器和服务器之间的加密数据包即可，代理服务器无需解析HTTPS 报文。</p><p>举一个简单的例子：</p><ol type="1"><li>浏览器向代理服务器发送 CONNECT 请求。</li></ol><pre class="line-numbers language-none"><code class="language-none">CONNECT jaydenchang.top:443 HTTP&#x2F;1.0 </code></pre><ol start="2" type="1"><li>代理服务器返回 HTTP 200 状态码表示连接已经建立。</li></ol><pre class="line-numbers language-none"><code class="language-none">HTTP&#x2F;1.0 200 Connection Established</code></pre><ol start="3" type="1"><li>之后浏览器和服务器开始 HTTPS握手并交换加密数据，代理服务器只负责传输彼此的数据包，并不能读取具体数据内容(代理服务器也可以选择安装可信根证书解密 HTTPS 报文)。</li></ol><p>事实上，这个过程其实是通过代理服务器将 HTTP 协议转换为 HTTPS协议的过程。对 RPC 服务端来说，需要做的事是将 HTTP 协议转换为 RPC协议，对客户端来说，需要新增通过 HTTP CONNECT 请求创建连接的逻辑。</p><h4 id="服务端支持-http-协议">服务端支持 HTTP 协议</h4><p>那通信过程应该是这样的：</p><ol type="1"><li>客户端向 RPC 服务器发送 CONNECT 请求</li></ol><pre class="line-numbers language-none"><code class="language-none">CONNECT 10.0.0.1:9999&#x2F;geerpc HTTP&#x2F;1.0 </code></pre><ol start="2" type="1"><li>RPC 服务器返回 HTTP 200 状态码表示连接建立。</li></ol><pre class="line-numbers language-none"><code class="language-none">HTTP&#x2F;1.0 200 Connected to Gee RPC</code></pre><ol start="3" type="1"><li>客户端使用创建好的连接发送 RPC 报文，先发送 Option，再发送 N个请求报文，服务端处理 RPC 请求并响应。</li></ol><p>在<code>server.go</code>中新增如下的方法：</p><p><strong>day5/server.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">const (    connected        &#x3D; &quot;200 Connected to Gee RPC&quot;    defaultRPCPath   &#x3D; &quot;&#x2F;geerpc&quot;    defaultDebugPath &#x3D; &quot;&#x2F;debug&#x2F;geerpc&quot;)&#x2F;&#x2F; ServerHTTP implements an http.Handler that answer RPC requestsfunc (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) &#123;    if req.Method !&#x3D; &quot;CONNECT&quot; &#123;        w.Header().Set(&quot;Content-Type&quot;, &quot;text&#x2F;plain; charset&#x3D;utf-8&quot;)        w.WriteHeader(http.StatusMethodNotAllowed)        _, _ &#x3D; io.WriteString(w, &quot;405 must CONNECT\n&quot;)        return    &#125;    conn, _, err :&#x3D; w.(http.Hijacker).Hijack()    if err !&#x3D; nil &#123;        log.Print(&quot;rpc hijacking &quot;, req.RemoteAddr, &quot;: &quot;, err.Error())        return    &#125;    _, _ &#x3D; io.WriteString(conn, &quot;HTTP&#x2F;1.0 &quot; + connected + &quot;\n\n&quot;)    server.ServeConn(conn)&#125;&#x2F;&#x2F; HandleHTTP registers an HTTP handler for RPC messages on rpcPath&#x2F;&#x2F; It is still necessary to invoke http.Serve(), typically in a go statementfunc (server *Server) HandleHTTP() &#123;    http.Handle(defaultRPCPath, server)&#125;&#x2F;&#x2F; HandleHTTP is a convenient approach for default server to register HTTP handlersfunc HandleHTTP() &#123;    DefaultServer.HandleHTTP()&#125;</code></pre><p><code>defaultDebugPath</code>是后续 DEBUG 页面预留的地址。</p><p>在 GO 中处理 HTTP 请求是非常简单的一件事，Go标准库中<code>http.Handle</code>的实现如下：</p><pre class="line-numbers language-golang" data-language="golang"><code class="language-golang">package http&#x2F;&#x2F; Handle registers the handler for the given pattern.&#x2F;&#x2F; in the DefaultServeMux.&#x2F;&#x2F; The documentation for ServeMux explains how patterns are matched.func Handle(pattern string, handler Handler) &#123; DefaultServeMux.Handle(pattern, handler) &#125;</code></pre><p>第一个参数是支持统配的字符串pattern，在这里，我们固定传入<code>/geerpc</code>，第二个参数是 Handler类型，Handler 是一个接口类型，定义如下：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Handler interface &#123;    ServeHTTP(w ResponseWriter, r *Request)&#125;</code></pre><p>也就是说，只需要实现接口 Handler 即可作为一个 HTTP Handler 处理 HTTP请求。接口 Handler只定义了一个方法<code>ServeHTTP</code>，实现该方法即可。</p><h4 id="客户端支持-http-协议">客户端支持 HTTP 协议</h4><p>服务端已经能够接受 CONNECT 请求，并返回了 200状态码<code>HTTP/1.0 200 Connected to Gee RPC</code>，客户端要做的，发起CONNECT 请求，检查返回状态码即可成功建立连接。</p><p><strong>day5/client.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; NewHTTPClient new a Client instance via HTTP as transport protocolfunc NewHTTPClient(conn net.Conn, opt *Option) (*Client, err) &#123;    _, _ &#x3D; io.WriteString(conn, fmt.Sprintf(&quot;CONNECT %s HTTP&#x2F;1.0\n\n&quot;, defaultRPCPath))        &#x2F;&#x2F; Require successful HTTP reesponse    &#x2F;&#x2F; before switching to RPC protocol    resp, err :&#x3D; http.ReadResponse(bufio.NewReader(conn), &amp;http.Request&#123;Method: &quot;CONNECT&quot;&#125;)    if err &#x3D;&#x3D; nil &amp;&amp; resp.Status &#x3D;&#x3D; connected &#123;        return NewClient(conn, opt)    &#125;    if err &#x3D;&#x3D; nil &#123;        err &#x3D; errors.New(&quot;unexpected HTTP response: &quot; + resp.Status)    &#125;    return nil, err&#125;&#x2F;&#x2F; DialHTTP connectd to an HTTP RPC server at the specified network address &#x2F;&#x2F; listening on the default HTTP RPC path.func DialHTTP(network, address string, opts ...*Option) (*Client, error) &#123;    return dialTimeout(NewHTTPClient, network, address, opts...)&#125;</code></pre><p>通过 HTTP CONNECT 请求建立连接后，后续的通信过程就交给 NewClient了。</p><p>为了简化调用，提供了一个统一入口<code>XDial</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; XDial calls different functions to connect to a RPC server&#x2F;&#x2F; according the first parameter rpcAddr.&#x2F;&#x2F; rpcAddr is a general format (protocol@addr) to represent a rpc server&#x2F;&#x2F; eg, http@10.0.0.1:7890, tcp@10.0.0.1:9999, unix@&#x2F;tmp&#x2F;geerpc.sockfunc XDial(rpcAddr string, opts ...*Option) (*Client, error) &#123;    parts :&#x3D; strings.Split(rpcAddr, &quot;@&quot;)    if len(parts) !&#x3D; 2 &#123;        return nil, fmt.Errorf(&quot;rpc client err: wrong format &#39;%s&#39;, expect protocol@addr&quot;, rpcAddr)    &#125;    protocol, addr :&#x3D; parts[0]. parts[1];    switch protocol &#123;    case &quot;http&quot;:        return DialHTTP(&quot;tcp&quot;, addr, opts...)    default:        &#x2F;&#x2F; tcp, unix or other transport protocol        return Dial(protocol, addr, opts...)    &#125;&#125;</code></pre><p>添加一个测试用例试一试，这个测试用例使用了 unix 协议创建 socket连接，适用于本机内部的通信，使用上和 TCP 协议无区别。</p><p><strong>day5/client_test.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func TestXDial(t *testing.T) &#123;    if runtime.GOOS &#x3D;&#x3D; &quot;linux&quot; &#123;        ch :&#x3D; make(chan struct&#123;&#125;)        addr :&#x3D; &quot;&#x2F;tmp&#x2F;geerpc.sock&quot;        go func() &#123;            _ &#x3D; os.Remove(addr)            l, err :&#x3D; net.Listen(&quot;unix&quot;, addr)            if err !&#x3D; nil &#123;                t.Fatal(&quot;failed to listen unix socket&quot;)            &#125;            ch &lt;- struct&#123;&#125;&#123;&#125;            Accept(l)        &#125;()        &lt;-ch        _, err :&#x3D; XDial(&quot;unix@&quot; + addr)        _assert(err &#x3D;&#x3D; nil, &quot;failed to connect unix socket&quot;)    &#125;&#125;</code></pre><h4 id="实现简单的-debug-页面">实现简单的 DEBUG 页面</h4><p>支持 HTTP 协议的好处在于，RPC服务仅仅使用了监听端口的<code>/geerpc</code>路径，在其他路径上我们可以提供诸如日志，统计等更为丰富的功能。接下来我们在<code>/debug/geerpc</code>上展示服务的调用统计视图。</p><p><strong>day5/debug.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package geerpcimport (    &quot;fmt&quot;    &quot;html&#x2F;template&quot;    &quot;net&#x2F;http&quot;)const debugText &#x3D; &#96;&lt;html&gt;&lt;body&gt;&lt;title&gt;GeeRPC Services&lt;&#x2F;title&gt;&#123;&#123;range .&#125;&#125;&lt;hr&gt;Service &#123;&#123;.Name&#125;&#125;&lt;hr&gt;&lt;table&gt;&lt;th align&#x3D;center&gt;Method&lt;&#x2F;th&gt;&lt;th align&#x3D;center&gt;Calls&lt;&#x2F;th&gt;&#123;&#123;range $name, $mtype :&#x3D; .Method&#125;&#125;&lt;tr&gt;&lt;td align&#x3D;left font&#x3D;fixed&gt;&#123;&#123;$name&#125;&#125;(&#123;&#123;$mtype.ArgType&#125;&#125;, &#123;&#123;$mtype.ReplyType&#125;&#125;) error&lt;&#x2F;td&gt;&lt;td align&#x3D;center&gt;&#123;&#123;$mtype.NumCalls&#125;&#125;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;&#123;&#123;end&#125;&#125;&lt;&#x2F;table&gt;&#123;&#123;end&#125;&#125;&lt;&#x2F;body&gt;&lt;&#x2F;html&gt;&#96;var debug &#x3D; template.Must(template.New(&quot;RPC debug&quot;).Parse(debugText))type debugHTTP struct &#123;    *Server&#125;type debugService struct &#123;    Name   string    Method map[string]*methodType&#125;&#x2F;&#x2F; Runs at &#x2F;debug&#x2F;geerpcfunc (server debugHTTP) ServerHTTP(w http.ResponseWriter, req *http.Request) &#123;    &#x2F;&#x2F; build a sorted version of the data    var services []debugService    server.serviceMap.Range(func(namei, svci interface&#123;&#125;) bool &#123;        svc :&#x3D; svci.(*service)        services &#x3D; append(services, debugService&#123;            Name:   namei.(string),            Method: svc.method,        &#125;)        return true    &#125;)    err :&#x3D; debug.Execute(w, services)    if err !&#x3D; nil &#123;        _, _ &#x3D; fmt.Fprintln(w, &quot;rpc: error executing template:&quot;, err.Error())    &#125;&#125;</code></pre><p>在这里，我们将返回一个 HTML 报文，这个报文将展示注册所有的 service的每一个方法的调用情况。</p><p>将 debugHTTP 实例绑定的地址<code>/debug/geerpc</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (server *Server) HandleHTTP() &#123;    http.Handle(defaultRPCPath, server)    http.Handle(defaultDebugPath, debugHTTP&#123;server&#125;)    log.Println(&quot;rpc server debug path:&quot;, defaultDebugPath)&#125;</code></pre><h4 id="demo-2">Demo</h4><p>到此，我们已经迫不及待地想看看最终的效果了。</p><p><strong>day5/main/main.go</strong></p><p>和之前的例子相比较，将 startServer中的<code>geerpc.Accept()</code>替换为了<code>geerpc.HandleHTTP()</code>，端口固定为9999。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Foo inttype Args struct &#123; Num1, Num2 int &#125;func (f Foo) Sum(args Args, reply *int) error &#123;    *reply &#x3D; args.Num1 + args.Num2    return nil&#125;func startServer(addrCh chan string) &#123;    var foo Foo    l, _ :&#x3D; net.Listen(&quot;tcp&quot;, &quot;9999&quot;)    _ &#x3D; geerpc.Register(&amp;foo)    geerpc.HandleHTTP()    addrCh &lt;- l.Addr().String()    _ &#x3D; http.Serve(l, nil)&#125;</code></pre><p>客户端将<code>Dial</code>替换为<code>DialHTTP</code>，其余地方没有发生改变。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func call(addrCh chan string) &#123;    client, _ :&#x3D; geerpc.DialHTTP(&quot;tcp&quot;, &lt;-addrCh)    defer func() &#123; _ &#x3D; client.Close() &#125;()        time.Sleep(time.Second)    &#x2F;&#x2F; send a request &amp; receive response    var wg sync.WaitGroup    for i :&#x3D; 0; i &lt; 5; i++ &#123;        wg.Add(1)        go func(i int) &#123;            defer wg.Done()            args :&#x3D; &amp;Args&#123;Num1: i, Num2: i * i&#125;            var reply int            if err :&#x3D; client.Call(context.Background(), &quot;Foo.Sum&quot;, args, &amp;reply);err !&#x3D; nil &#123;                log.Fatal(&quot;call Foo.Sum error:&quot;, err)            &#125;            log.Fatal(&quot;%d + %d &#x3D; %d&quot;, args.Num1, args.Num2, reply)        &#125;(i)    &#125;    wg.Wait()&#125;func main() &#123;    log.SetFlags(0)    ch :&#x3D; make(chan string)    go call(ch)    startServer(ch)&#125;</code></pre><p>main函数中，我们在最后调用<code>startServer</code>，服务启动后将一直等待。</p><p>运行结果如下：</p><pre class="line-numbers language-none"><code class="language-none">main$ go run.rpc server: register Foo.Sumrpc server debug path: &#x2F;debug&#x2F;geerpc4 + 16 &#x3D; 203 + 9 &#x3D; 120 + 0 &#x3D; 02 + 4 &#x3D; 61 + 1 &#x3D; 2</code></pre><p>服务已经启动，此时我们如果在浏览器中访问<code>localhost:9999/debug/geerpc</code>，将会看到：</p><p><img src="0x0031/0x0031-5.png"></p><h3 id="day6.-负载均衡">day6. 负载均衡</h3><ul><li>通过随机选择和 Round Robin 轮询调度算法实现服务端负载均衡，约 250行代码。</li></ul><h4 id="负载均衡策略">负载均衡策略</h4><p>假设有多个服务实例，每个实例提供相同的功能，为了提高整个系统的吞吐量，每个实例部署在不同的机器上。客户端可以选择任意一个实例进行调用，获取想要的结果。那如何选择呢？取决了负载均衡的策略。对于RPC 框架来说，我们可以很容易地想到这么几种策略：</p><ul><li>随机选择策略 - 从服务列表中随机选择一个。</li><li>轮询算法 (Round Robin) - 依次调度不同的服务器，每次调度执行 i = (i +1) mode n。</li><li>加权轮询 (Weight Round Robin) -在轮询算法的基础上，为每个服务实例设置一个权重，高性能的机器赋予更高的权重，也可以根据服务实例的当前的负载情况做动态的调整，例如考虑最近5 分钟部署服务器的 CPU 、内存消耗情况。</li><li>哈希 / 一致性哈希策略 - 依据请求的某些特征，计算一个 hash 值，根据hash 值将请求发送到对应的机器，一致性 hash还可以解决服务实例动态添加情况下，调度抖动的问题。一致性哈希的一个典型应用场景是分布式缓存服务。</li></ul><h4 id="服务发现">服务发现</h4><p>负载均衡的前提是有多个服务实例，那我们首先实现一个最基础的服务发现模块Discovery。为了与通信部分解耦，这部分的代码统一放置在 xclient子目录下。</p><p>定义 2 个类型：</p><ul><li>SelectMode 代表不同的负载均衡策略，简单起见，GeeRPC 仅实现 Random 和RoundRobin 两种策略。</li><li>Discovery 是一个接口类型，包含了服务发现所需要的最基本的接口。<ul><li><code>Refresh()</code>从注册中心更新服务列表。</li><li><code>Update(servers []string)</code>手动更新服务列表。</li><li><code>Get(mode SelectMode)</code>根据负载均衡策略，选择一个服务实例。</li><li><code>GetAll()</code>返回所有的服务实例。</li></ul></li></ul><p><strong>day6/xclient/discovery.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package xclientimport (    &quot;errors&quot;    &quot;math&quot;    &quot;math&#x2F;rand&quot;    &quot;sync&quot;    &quot;time&quot;)type SelectMode intconst (    RandomSelect SelectMode &#x3D; iota &#x2F;&#x2F; select randomly    RoundRobinSelect               &#x2F;&#x2F; select using Robbin algorithm)type Discovery interface &#123;    Refresh() error &#x2F;&#x2F; refresh from remote registry    Update(servers []string) error    Get(mode SelectMode) (string, error)    GetAll() ([]string, error)&#125;</code></pre><p>紧接着，我们实现一个不需要注册中心，服务列表由手工维护的服务发现的结构体：MultiServersDiscovery</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type MultiServersDiscovery struct &#123;    r       *rand.Rand   &#x2F;&#x2F; generate random number    mu      sync.RWMutex &#x2F;&#x2F; protect following    servers []string     index   int          &#x2F;&#x2F; record the selected position for robin algorithm&#125;&#x2F;&#x2F; NewMultiServerDiscovery creates a MultiServersDiscovery instancefunc NewMultiServerDiscovery(servers []string) *MultiServersDiscovery &#123;    d :&#x3D; &amp;MultiServersDiscovery &#123;        servers: servers,        r:       rand.New(rand.NewSource(time.Now().UnixNano())),    &#125;    d.index &#x3D; d.r.Intn(math.MaxInt32 - 1)    return d&#125;</code></pre><ul><li>r是一个产生随机数的实例，初始化时使用时间戳设定随机数种子，避免每次产生相同的随机数序列。</li><li>index 记录 Round Robin 算法已经轮询到的位置，为了避免每次从 0开始，初始化时随机设定一个值。</li></ul><p>然后，实现 Discovery 接口</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">var _ Discovery &#x3D; (*MultiServersDiscovery)(nil)&#x2F;&#x2F; Refresh doesn&#39;t make sense for MultiServersDiscovery, so ignore itfunc (d *MultiServersDiscovery) Refresh() error &#123;    return nil&#125;&#x2F;&#x2F; Update the servers of discovery dynamically if neededfunc (d *MultiServersDiscovery) Update(servers []string) error &#123;    d.mu.Lock()    defer d.mu.Unlock()    d.servers &#x3D; servers    return nil&#125;&#x2F;&#x2F; Get a server according to modefunc (d *MultiServersDiscovery) Get(mode SelectMode) (string, error) &#123;    d.mu.Lock()    defer d.mu.Unlock()    n :&#x3D; len(d.servers)    if n &#x3D;&#x3D; 0 &#123;        return &quot;&quot;, errors.New(&quot;rpc discovery: no available servers&quot;)    &#125;    switch mode &#123;    case RandomSelect:        return d.servers[d.r.Intn(n)], nil    case RoundRobinSelect:        s :&#x3D; d.servers[d.index % n] &#x2F;&#x2F; servers could be updated, so mode n to ensure safety        d.index &#x3D; (d.index + 1) % n        return s, nil    default:        return &quot;&quot;, errors.New(&quot;rpc discovery: not supported select mode&quot;)    &#125;&#125;&#x2F;&#x2F; returns all servers in discoveryfunc (d *MultiServersDiscovery) GetAll() ([]string, error) &#123;    d.mu.RLock()    defer d.mu.RUnlock()    &#x2F;&#x2F; return a copy of d.servers    servers :&#x3D; make([]string, len(d.servers), len(d.servers))    copy(servers, d.servers)    return servers, nil&#125;</code></pre><h4 id="支持负载均衡的客户端">支持负载均衡的客户端</h4><p>接下来，我们向用户暴露一个支持负载均衡的客户端的 XClient。</p><p><strong>day6/xclient/xclient.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package xclientimport (    &quot;context&quot;    . &quot;geerpc&quot;    &quot;io&quot;    &quot;reflect&quot;    &quot;sync&quot;)type XClient struct &#123;    d       Discovery    mode    SelectMode    opt     *Option    mu      sync.Mutex &#x2F;&#x2F; protect following    clients map[string]*Client&#125;var _ io.Closer &#x3D; (*XClient)(nil)func NewXClient(d Discovery, mode SelectMode, opt *Option) *XClient &#123;    return &amp;XClient&#123;d: d, mode: mode, opt: opt, clients make(map[string]*Client)&#125;&#125;func (xc *XClient) Close() error &#123;    xc.mu.Lock()    defer xc.mu.Unlock()    for key, client :&#x3D; range xc.clients &#123;        &#x2F;&#x2F; I hava no idea how to deal with error, just ignore it.        _ &#x3D; client.Close()        delete(xc.clients, key)    &#125;    return nil&#125;</code></pre><p>XClient 的构造函数需要传入三个参数，服务发现实例Discovery、负载均衡模式 SelectMode 以及协议选项Option。为了尽量地复用已经创建好的 Socket 连接，使用 clients保存创建成功的 Client 实例，并提供 Close方法在结束后，关闭已经创建的连接。</p><p>接下来，实现客户端最基本的功能<code>Call</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (xc *Client) dial(rpcAddr string) (*Client, error) &#123;    xc.mu.Lock()    defer xc.mu.Unlock()    client, ok :&#x3D; xc.clients[rpcAddr]    if ok &amp;&amp; !client.IsAvailable() &#123;        _ &#x3D; client.Close()        delete(xc.clients, rpcAddr)        client &#x3D; nil    &#125;    if client &#x3D;&#x3D; nil &#123;        var err error        client, err &#x3D; XDial(rpcAddr, xc.opt)        if err !&#x3D; nil &#123;            return nil, err        &#125;        xc.clients[rpcAddr] &#x3D; client    &#125;    return client, nil&#125;func (xc *XClient) call(rpcAddr string, ctx context.Context, serviceMethod string, args, reply interface&#123;&#125;) error &#123;    client, err :&#x3D; xc.dial(rpcAddr)    if err !&#x3D; nil &#123;        return err    &#125;    return client.Call(ctx, serviceMethod, args, reply)&#125;&#x2F;&#x2F; Call invokes the named function, waits for it to complete,&#x2F;&#x2F; and returns its error status.&#x2F;&#x2F; xc will choose a proper server.func (xc *XClient) Call(ctx context.Context, serviceMethod string, args, reply interface&#123;&#125;) error &#123;    rpcAddr, err :&#x3D; xc.d.Get(xc.mode)    if err !&#x3D; nil &#123;        return err    &#125;    return xc.call(rpcAddr, ctx, serviceMethod, args, reply)&#125;</code></pre><p>我们将复用 Client的能力封装在方法<code>dial</code>中，<code>dial</code>的处理逻辑如下：</p><ol type="1"><li>检查<code>xc.clients</code>是否有缓存的Client，如果有，检查是否时可用状态，如果是，则返回缓存的Client，如果不可用，则从缓存中删除。</li><li>如果步骤 1 没有返回缓存的 Client，则说明需要创建新的Client，缓存并返回。</li></ol><p>另外，我们为 XClient 添加一个常用功能：<code>Broadcast</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Broadcast invokes the named function for every server registered in discoveryfunc (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, reply interface&#123;&#125;) error &#123;    servers, err :&#x3D; xc.d.GetAll()    if err !&#x3D; nil &#123;        return err    &#125;    var wg sync.WaitGroup    var mu sync.Mutex &#x2F;&#x2F; protect e and replyDone    var e error    replyDone :&#x3D; reply &#x3D;&#x3D; nil &#x2F;&#x2F; if reply is nil, don&#39;t need to set value    ctx, cancel :&#x3D; context.WithCancel(ctx)    for _, rpcAddr :&#x3D; range servers &#123;        wg.Add(1)        go func(rpcAddr string) &#123;            defer wg.Done()            var clonedReply interface&#123;&#125;            if reply !&#x3D; nil &#123;                clonedReply &#x3D; reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()                            &#125;            err :&#x3D; xc.call(rpcAddr, ctx, serviceMethod, args, clonedReply)            mu.Lock()            if err !&#x3D; nil &amp;&amp; e &#x3D;&#x3D; nil &#123;                e &#x3D; err                cancel() &#x2F;&#x2F; if any call failed, cancel unfinished calls            &#125;            if err &#x3D;&#x3D; nil &amp;&amp; !replyDone &#123;                reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem())                replyDone &#x3D; true            &#125;            mu.Unlock()        &#125;(rpcAddr)    &#125;    wg.Wait()    return e&#125;</code></pre><p>Broadcast将请求广播到所有的服务实例，如果任意一个实例发生错误，则返回其中一个错误；如果调用成功，则返回其中一个的结果。有以下几点需要注意：</p><ol type="1"><li>为了提升性能，请求是并发的。</li><li>并发情况下需要使用互斥锁保证 error 和 reply 能被正确赋值。</li><li>借助<code>context.WithCancel</code>确保有错误发生时，快速失败。</li></ol><h4 id="demo-3">Demo</h4><p>首先，启动 RPC 服务的代码还是类似的，Sum 时正常的方法，Sleep 用于验证XClient 的超时机制能否正常运作。</p><p><strong>day6/main/main.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (    &quot;context&quot;    &quot;geerpc&quot;    &quot;geerpc&#x2F;xclient&quot;    &quot;log&quot;    &quot;net&quot;    &quot;sync&quot;    &quot;time&quot;)type Foo inttype Args struct&#123; Num1, Num2 int &#125;func (f Foo) Sum(args Args, reply *int) error &#123;    time.Sleep(time.Second * time.Duration(args.Num1))    *reply &#x3D; args.Num1 + args.Num2    return nil&#125;func (f Foo) Sleep(args Args, reply *int) error &#123;    var foo Foo    l, _ :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:0&quot;)    server :&#x3D; geerpc.NewServer()    &#x2F;&#x2F; send request &amp; receive response    var wg sync.WaitGroup    for i :&#x3D; 0; i &lt; 5; i++ &#123;        wg.Add(1)        go func(i int) &#123;            defer wg.Done()            foo(xc, context.Background(), &quot;call&quot;, &quot;Foo.Sum&quot;, &amp;Args&#123;Num1: i, Num2: i * i&#125;)        &#125;(i)    &#125;    wg.Wait()&#125;func broadcast(addr1, addr2 string) &#123;    d :&#x3D; xclient.NewMultiServerDiscovery([]string&#123;&quot;tcp&quot; + addr1, &quot;tcp@&quot; + addr2&#125;)    xc :&#x3D; xclient.NewXClient(d, xclient.RandomSelect, nil)    defer func() &#123; _ &#x3D; xc.Close() &#125;()    var wg sync.WaitGroup    for i :&#x3D; 0; i &lt; 5; i++ &#123;        wg.Add(1)        go func(i int) &#123;            defer wg.Done()            foo(xc, context.Background(), &quot;broadcast&quot;, &quot;Foo.Sum&quot;, &amp;Args&#123;Num1: i, Num2: i * i&#125;)            &#x2F;&#x2F; expect 2 - 5 timeout            ctx, _ :&#x3D; context.WithTimeout(context.Background(), time.Second * 2)            foo(xc, ctx, &quot;broadcast&quot;, &quot;Foo.Sleep&quot;, &amp;Args&#123;Num1: i, Num2: i * i&#125;)        &#125;(i)    &#125;    wg.Wait()&#125;func main() &#123;    log.SetFlags(0)    ch1 :&#x3D; make(chan string)    ch2 :&#x3D; make(chan string)    &#x2F;&#x2F; start two servers    go startServer(ch1)    go startServer(ch2)        addr1 :&#x3D; &lt;-ch1    addr@ :&#x3D; &lt;-ch2        time.Sleep(time.Second)    call(addr1, addr2)    broadcast(addr1, addr2)&#125;</code></pre><p>运行结果如下</p><pre class="line-numbers language-none"><code class="language-none">*main.Foo    Sleeprpc server: register Foo.Sleep*main.Foo    Sumrpc server: register Foo.Sum*main.Foo    Sleeprpc server: register Foo.Sleep*main.Foo    Sumrpc server: register Foo.Sumcall Foo.Sum success: 3 + 9 &#x3D; 12call Foo.Sum success: 4 + 16 &#x3D; 20call Foo.Sum success: 2 + 4 &#x3D; 6call Foo.Sum success: 0 + 0 &#x3D; 0call Foo.Sum success: 1 + 1 &#x3D; 2broadcast Foo.Sum success: 4 + 16 &#x3D; 20broadcast Foo.Sum success: 2 + 4 &#x3D; 6broadcast Foo.Sum success: 1 + 1 &#x3D; 2broadcast Foo.Sum success: 0 + 0 &#x3D; 0broadcast Foo.Sum success: 3 + 9 &#x3D; 12broadcast Foo.Sleep success: 0 + 0 &#x3D; 0broadcast Foo.Sleep success: 1 + 1 &#x3D; 2broadcast Foo.Sleep error: rpc client: call failed: context deadline exceededbroadcast Foo.Sleep error: rpc client: call failed: context deadline exceededbroadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded</code></pre><h3 id="day7.-服务发现与注册中心">day7. 服务发现与注册中心</h3><p>// header 一定要一致，不然一直错</p><ul><li>实现一个简单的注册中心，支持服务注册，接收心跳等功能。</li><li>客户端实现基于注册中心的服务发现机制。</li></ul><h4 id="注册中心的位置">注册中心的位置</h4><p><img src="0x0031/0x0031-7.jpg"></p><p>注册中心的位置如上图所示。注册中心的好处在于，客户端和服务端都只需要感知注册中心的存在，而无需感知对方的存在。更具体一点：</p><ol type="1"><li>服务端启动后，向注册中心发送注册信息，注册中心得知该服务已经启动，处于可用状态。一般来说，服务端还需要定期向注册中心发送心跳，证明自己还活着。</li><li>客户端向注册中心询问，当前哪天服务是可用的，注册中心将可用的服务列表返回客户端。</li><li>客户端根据注册中心得到的服务列表，选择其中一个发起调用。</li></ol><p>如果没有注册中心，就像 GeeRPC第六天实现的一样，客户端需要硬编码服务端的地址，而且没有机制保证服务端是否处于可用状态。当然注册中心的功能还有很多，比如配置的动态同步，通知机制等。比较常用的注册中心有etcd、zookeeper、consul，一般比较出名的微服务或者 RPC框架，这些主流的注册中心都是支持的。</p><h4 id="gee-registry">Gee Registry</h4><p>主流的注册中心 etcd、zookeeper等功能强大，与这类注册中心的对接代码量是比较大的，需要实现的接口很多。GeeRPC选择自己实现一个简单的支持心跳保活的注册中心。</p><p>GeeRegistry 的代码独立放置在子目录 registry 中。</p><p>首先定义 GeeRegistry 结构体，默认超时时间设置为 5min，也就是说，任何注册的服务超过 5 min，即视为不可用状态。</p><p><strong>day7/registry/registry.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type GeeRegistry struct &#123;    timeout time.Duration    mu      sync.Mutex &#x2F;&#x2F; protect following    servers map[string]*ServerItem&#125;type ServerItem struct &#123;    Addr string    start time.Time&#125;const (defaultPath    &#x3D; &quot;&#x2F;geerpc&#x2F;registry&quot;    defaultTimeout &#x3D; time.Minute * 5)&#x2F;&#x2F; New create a registry instance with timeout settingfunc New(timeout time.Duration) *GeeRegistry &#123;    return &amp;GeeRegistry &#123;        servers: make(map[string]*ServerItem),        timeout: timeout,    &#125;&#125;var DefaultGeeRegister &#x3D; New(defaultTimeout)</code></pre><p>为 GeeRegistry 实现添加服务实例和返回服务列表的方法。</p><ul><li>putServer：添加服务实例，如果服务已存在，则更新 start。</li><li>aliveServers：返回可用的服务列表，如果存在超时的服务，则删除。</li></ul><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (r *GeeRegistry) putServer(addr string) &#123;    r.mu.Lock()    defer r.mu.Unlock()    s :&#x3D; r.servers[addr]    if s &#x3D;&#x3D; nil &#123;        r.servers[addr] &#x3D; &amp;ServerItem&#123;Addr: addr, start: time.Now()&#125;    &#125; else &#123;        s.start &#x3D; time.Now() &#x2F;&#x2F; if exists, update start time to keep alive    &#125;&#125;func (r *GeeRegistry) aliveServers() []string &#123;    r.mu.Lock()    defer r.mu.Unlock()    var alive []string    for addr, s :&#x3D; range r.servers &#123;        if r.timeout &#x3D;&#x3D; 0 || s.start.Add(r.timeout).After(time.Now()) &#123;            alive &#x3D; append(alive, addr)        &#125; else &#123;            delete(r.servers, addr)        &#125;        sort.Strings(alive)        return alive    &#125;&#125;</code></pre><p>为了实现上的简单，GeeRegistry 采用 HTTP协议提供服务，且所有的有用信息都承载在 HTTP Header 中。</p><ul><li>Get：返回所有可用的服务列表，通过自定义字段 X-Geerpc-Servers承载。</li><li>Post：添加服务实例或发送心跳，通过自定义字段 X-Geerpc-Server承载。</li></ul><blockquote><p>这里要注意，Get 和 Post 各自使用的 header一定要一样，不然就会出现<code>rpc discovery: no available servers</code>的错误</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (r *GeeRegistry) ServeHTTP(w http.ResponseWriter, req *http.Request) &#123;    switch req.Method &#123;    case &quot;GET&quot;:        &#x2F;&#x2F; keep it simple, server is in req.Header        w.Header().Set(&quot;X-Geerpc-Servers&quot;, strings.Join(r.aliveServers(), &quot;,&quot;))    case &quot;POST&quot;:        &#x2F;&#x2F; keep it simple, server is in req.Header        addr :&#x3D; req.Header.Get(&quot;X-Geerpc-Server&quot;)        if addr &#x3D;&#x3D; &quot;&quot; &#123;            w.WriteHeader(http.StatusInternalServerError)            return        &#125;        r.putServer(addr)    default:        w.WriteHeader(http.StatusInternalServerError)    &#125;&#125;&#x2F;&#x2F; HandleHTTP registers an HTTP handler for GeeRegistry messages on registryPathfunc (r *GeeRegistry) HandleHTTP(registryPath string) &#123;    http.Handle(registryPath, r)    log.Println(&quot;rpc registry path:&quot;, registryPath)&#125;func HandleHTTP() &#123;    DefaultGeeRegister.HandleHTTP(defaultPath)&#125;</code></pre><p>另外，提供 Heartbeat方法，便于服务启动时定时向注册中心发送心跳，默认周期比注册中心设置的过期时间少1 min。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Heartbeat send a heartbeat message every once in a while&#x2F;&#x2F; it&#39;s a helper function for a server to register or send heartbeatfunc Heartbeat(registry, addr string, duration time.Duration) &#123;    if duration &#x3D;&#x3D; 0 &#123;        &#x2F;&#x2F; make sure there is enough time to send heart beat        &#x2F;&#x2F; before it&#39;s removed from registry        duration &#x3D; defaultTimeout - time.Duration(1)*time.Minute    &#125;    var err error    err &#x3D; sendHeartbeat(registry, addr)    go func() &#123;        t :&#x3D; time.NewTicker(duration)        for err &#x3D;&#x3D; nil &#123;            &lt;-t.C            err &#x3D; sendHeartbeat(registry, addr)        &#125;    &#125;()&#125;func sendHeartbeat(registry, addr string) error &#123;    log.Println(addr, &quot;send heart beat to registry&quot;, registry)    httpClient :&#x3D; &amp;http.Client&#123;&#125;    req, _ :&#x3D; http.NewRequest(&quot;POST&quot;, registry, nil)    req.Header.Set(&quot;X-Geerpc-Server&quot;, addr)    if _, err :&#x3D; httpClient.Do(req); err !&#x3D; nil &#123;        log.Println(&quot;rpc server: heart beat err:&quot;, err)        return err    &#125;    return nil&#125;</code></pre><h4 id="geeregistrydiscovery">GeeRegistryDiscovery</h4><p>在 xclient 中对应实现 Discovery。</p><p><strong>day7/xclient/discovery_gee.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package xclienttype GeeRegistryDiscovery struct &#123;    *MultiServersDiscovery     registry   string    timeout    time.Duration    lastUpdate time.Time&#125;const defaultUpdateTimeout &#x3D; time.Second * 10func NewGeeRegistryDiscovery(registerAddr string, timeout time.Duration) *GeeRegistryDiscovery &#123;    if timeout &#x3D;&#x3D; 0 &#123;        timeout &#x3D; defaultUpdateTimeout    &#125;    d :&#x3D; &amp;GeeRegistryDiscovery &#123;        MultiServerDiscovery: NewMultiServerDiscovery(make([]string, 0))        registry:             registerAddr,        timeout:              timeout,    &#125;    return d&#125;</code></pre><ul><li>GeeRegistryDiscovery 嵌套了MultiServersDiscovery，很多能力可以复用。</li><li>registry 即注册中心的地址。</li><li>timeout 服务列表的过期时间。</li><li>lastUpdate 是代表最后从注册中心更新服务列表的时间，默认 10s 过期，即10s 之后，需要从注册中心更新新的列表。</li></ul><p>实现 Update 和 Refresh 方法，超时重新获取的逻辑在 Refresh中实现：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *GeeRegistryDiscovery) Update(servers []string) error &#123;    d.mu.Lock()    defer d.mu.Unlock()    d.servers &#x3D; servers    d.lastUpdate &#x3D; time.Now()    return nil&#125;func (d *GeeRegistryDiscovery) Refresh() error &#123;    d.mu.Lock()    defer d.mu.Unlock()    if d.lastUpdate.Add(d.timeout).After(time.Now()) &#123;        return nil    &#125;    log.Println(&quot;rpc registry: refresh servers form registry&quot;, d.registry)    resp, err :&#x3D; http.Get(d.registry)    if err !&#x3D; nil &#123;        log.Println(&quot;rpc registry refresh err:&quot;, err)        return err    &#125;    servers :&#x3D; strings.Split(resp.Header.Get(&quot;X-Geerpc-Server&quot;, &quot;,&quot;))    d.server &#x3D; make([]string, 0, len(servers))    for _, server :&#x3D; range servers &#123;        if strings.TrimSpace(server) !&#x3D; &quot;&quot; &#123;            d.servers &#x3D; append(d.servers, strings.TrimSpace(server))        &#125;    &#125;    d.lastUpdate &#x3D; time.Now()    return nil&#125;</code></pre><p><code>Get</code>和<code>GetAll</code>与<code>MultiServersDiscovery</code>相似，唯一不同的在于，<code>GeeRegistryDiscovery</code>需要先调用Refresh 确保服务列表没有过期。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (d *GeeRegistryDiscovery) Get(mode SelectMode) (string, error) &#123;    if err :&#x3D; d.Refresh(); err !&#x3D; nil &#123;        return &quot;&quot;, err    &#125;    return d.MultiServersDiscovery.Get(mode)&#125;func (d *GeeRegistryDiscovery) GetAll() ([]string, error) &#123;    if err :&#x3D; d.Refresh(); err !&#x3D; nil &#123;        return nil, err    &#125;    return d.MultiServersDiscovery.GetAll()&#125;</code></pre><h4 id="demo-4">Demo</h4><p>最后，依旧通过简单的 Demo 验证今天的成果。</p><p>添加函数 startRegistry，稍微修改startServer，添加调用注册中心的<code>Heartbeat</code>方法的逻辑，定期向注册中心发送心跳。</p><p><strong>day7/main/main.go</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func startRegistry(wg *sync.WaitGroup) &#123;    l, _ :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:9999&quot;)    registry.HandleHTTP()    wg.Done()    _ &#x3D; http.Serve(l, nil)&#125;func startServer(registryAddr string, wg *sync.WaitGroup) &#123;    var foo Foo    l, _ :&#x3D; net.Listen(&quot;tcp&quot;, &quot;:0&quot;)    server :&#x3D; geerpc.NewServer()    _ &#x3D; server.Register(&amp;foo)    registry.Heartbeat(registryAddr)    wg.Done()    server.Accept(l)&#125;</code></pre><p>接下来，将 call 和 broadcast 的 MultiServersDiscovery 替换为GeeRegistryDiscovery，不再需要硬编码服务列表。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func call(registry string) &#123;    d :&#x3D; xclient.NewGeeRegistryDiscovery(registry, 0)    xc :&#x3D; xclient.NewXClient(d, xclient.RandomSelect, nil)    defer func() &#123; _ &#x3D; xc.Close() &#125;()    &#x2F;&#x2F; send request &amp; receive response    var wg sync.WaitGroup    for i :&#x3D; 0; i &lt; 5; i++ &#123;        wg.Add(1)        go func(i int) &#123;            defer wg.Done()            foo(xc, context.Background(), &quot;call&quot;, &quot;Foo.Sum&quot;, &amp;Args&#123;Num1: i, Num2: i * i&#125;)        &#125;(i)    &#125;    wg.Wait()&#125;func broadcast(registry string) &#123;    d :&#x3D; xclient.NewGeeRegistryDiscovery(registry, 0)    xc :&#x3D; xclient.NewXClient(d, xclient.RandomSelect, nil)    defer func() &#123; _ &#x3D; xc.Close() &#125;()    var wg sync.WaitGroup    for i :&#x3D; 0; i &lt; 5; i++ &#123;        wg.Add(1)        go func(i int) &#123;            defer wg.Done()            foo(xc, context.Background(), &quot;broadcast&quot;, &quot;Foo.Sum&quot;, &amp;Args&#123;Num1: i, Num2: i * i&#125;)            &#x2F;&#x2F; expect 2- 5 timeout            ctx, _ :&#x3D; context.WithTimeout(context.Background(), time.Second*2)            foo(xc, ctx, &quot;broadcast&quot;, &quot;Foo.Sleep&quot;, &amp;Args&#123;Num1: i, Num2: i * i&#125;)        &#125;(i)    &#125;    wg.Wait()&#125;</code></pre><p>最后在 main 函数中，将所有的逻辑串联起来，确保注册中心启动后，再启动RPC 服务端，最后客户端远程调用。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func main() &#123;    log.SetFlags(0)    registryAddr :&#x3D; &quot;http:&#x2F;&#x2F;localhost:9999&#x2F;geerpc&#x2F;registry&quot;    var wg sync.WaitGroup    wg.Add(1)    go startRegistry(&amp;wg)    wg.Wait()        time.Sleep(time.Second)    wg.Add(2)    go startServer(registryAddr, &amp;wg)    go startServer(registryAddr, &amp;wg)    wg.Wait()        time.Sleep(time.Second)    call(registryAddr)    broadcast(registryAddr)    &#125;</code></pre><p>运行结果如下：</p><pre class="line-numbers language-none"><code class="language-none">rpc registry path: &#x2F;geerpc&#x2F;registry*main.Foo    Sleeprpc server: register Foo.Sleep*main.Foo    Sumrpc server: register Foo.Sumtcp@[::]:46043 send heart beat to registry http:&#x2F;&#x2F;localhost:9999&#x2F;geerpc&#x2F;registry*main.Foo    Sleeprpc server: register Foo.Sleep*main.Foo    Sumrpc server: register Foo.Sumtcp@[::]:45079 send heart beat to registry http:&#x2F;&#x2F;localhost:9999&#x2F;geerpc&#x2F;registryrpc registry: refresh servers from registry http:&#x2F;&#x2F;localhost:9999&#x2F;geerpc&#x2F;registrycall Foo.Sum success: 2 + 4 &#x3D; 6call Foo.Sum success: 4 + 16 &#x3D; 20call Foo.Sum success: 1 + 1 &#x3D; 2call Foo.Sum success: 0 + 0 &#x3D; 0call Foo.Sum success: 3 + 9 &#x3D; 12rpc registry: refresh servers from registry http:&#x2F;&#x2F;localhost:9999&#x2F;geerpc&#x2F;registrybroadcast Foo.Sum success: 3 + 9 &#x3D; 12broadcast Foo.Sum success: 2 + 4 &#x3D; 6broadcast Foo.Sum success: 4 + 16 &#x3D; 20broadcast Foo.Sum success: 1 + 1 &#x3D; 2broadcast Foo.Sum success: 0 + 0 &#x3D; 0broadcast Foo.Sleep success: 0 + 0 &#x3D; 0broadcast Foo.Sleep success: 1 + 1 &#x3D; 2broadcast Foo.Sleep error: rpc client: call failed: context deadline exceededbroadcast Foo.Sleep error: rpc client: call failed: context deadline exceededbroadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded</code></pre><p>七天时间，参照 golang 标准库net/rpc，实现了服务端以及支持并发的客户端，并且支持选择不同的序列化与反序列化方式；为了防止服务挂死，在其中一些关键部分添加蓝超时处理机制；支持TCP、Unix、HTTP等多种传输协议；支持多种负载均衡模式，最后还实现了一个简易的服务注册和发现中心。</p><h3 id="一些想法">一些想法</h3><p>其实我学习这个的目的是为了尝试完成 mit 6.824，它的 lab1里，要求使用利用 rpc 来完成客户端和服务器之间的通信，但无奈，学习 golang的时间并不是很长，恰巧留意到 geektutu 有发过 rpc的七天项目，那么，正好，来敲一敲练练手。并且推进这个小项目的时间也不止七天。</p><p>那么来提炼一些个人觉得比较重要的知识。</p><h4 id="典型的-rpc-调用过程">典型的 RPC 调用过程</h4><p>感觉，这个项目中的 rpc的一些定义我不是很能理解，那么根据自己在别的网站上学到的，以及在做 6.824lab1时的一些经验，重新总结下相关 rpc 的基本结构。</p><p><strong>server</strong></p><p>这里的定义，结合了 6.824 lab1的实验要求进行总结，当然下面举的例子也只是简要说明下 rpc 的组成。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Task struct &#123;    FileName string    TaskType int&#125;type Coordinator struct &#123;    task Task&#125;</code></pre><p><strong>client</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type client struct &#123;    clientID int    mapf     func(string, string) []KeyValue    reducef  func(string, []string) string&#125;</code></pre><p><strong>rpc</strong></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type TaskRequest struct &#123;    X int&#125;&#x2F;&#x2F; 用于获取任务的请求结构体，在 lab1 中不携带信息type TaskReply struct &#123;    X int&#125;&#x2F;&#x2F; 回复任务的结构体，在 lab1 中不需要携带信息</code></pre><p>server 中定义了一个函数 <code>func()</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (s *Server)Func() &#123;    &#x2F;&#x2F; code&#125;</code></pre><p>然后在 client 里有一个 <code>CallGetFunc()</code>，用于远程调用。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func CallGetFunc() &#123;    args :&#x3D; TaskRequest&#123;&#125;    reply :&#x3D; TaskReply&#123;&#125;    call(&quot;Server.Func&quot;, &amp;args, &amp;reply)&#125;</code></pre><p>然后按照下列方式调用远程调用函数：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">call(&quot;StructName.FunctionName&quot;, &amp;args, &amp;reply),</code></pre><p>这样子，就完成了一个基本的 rpc 调用过程。</p>]]></content>
    
    
    <summary type="html">用七天时间实现用Golang一个RPC框架</summary>
    
    
    
    <category term="BackEnd" scheme="https://jaydenchang.top/categories/BackEnd/"/>
    
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>Java swing实现应用程序对数据库的访问</title>
    <link href="https://jaydenchang.top/post/0x0030.html"/>
    <id>https://jaydenchang.top/post/0x0030.html</id>
    <published>2022-09-18T16:00:00.000Z</published>
    <updated>2025-07-27T08:38:49.166Z</updated>
    
    <content type="html"><![CDATA[<p>最近在完成软件体系结构上机实验时，遇到一个有点点小难度的选做题，题目信息如下：</p><blockquote><p>利用套接字技术实现应用程序中对数据库的访问。应用程序只是利用套接字连接向服务器发送一个查询的条件，而服务器负责对数据库的查询，然后服务器再将查询的结果利用建立的套接字返回给客户端，如下图所示。</p></blockquote><p><img src="0x0030/0x0030_1.png"></p><p>本来吧，选做题，不太想做的，但是考虑到以后工作的方向和后端相关，那还是做吧。</p><p>本次实验需要做一个GUI界面和一个连接查询功能，在论坛上借鉴了其他大佬获取网站内容的部分代码，然后自己做了一个及其简陋的swing界面，算是把这个实验完成了。</p><p>本次实验项目结构如下</p><pre class="line-numbers language-none"><code class="language-none">--socketProject    |--Client.java    |--GUI.java    |--SearchInfo.java    |--Server.java    |--ServerThread.java</code></pre><h4 id="client.java"><code>Client.java</code></h4><p>客户端使用<code>dis.readUTF()</code>时，要注意再发送个字符或者空字符，这里发送<code>end</code>，表示关闭连接。不然会出现<code>EOFException</code>。</p><pre class="line-numbers language-java" data-language="java"><code class="language-java">package socketProject;import java.io.*;import java.net.*;public class Client &#123;    String studentNum &#x3D; null;    String result &#x3D; null;    public void setStudentNum(String num) &#123;        this.studentNum &#x3D; num;        System.out.println(&quot;stu: &quot; + studentNum);    &#125;    public void run() throws IOException &#123;        Socket ss &#x3D; new Socket(&quot;127.0.0.1&quot;, 8888);        System.out.println(&quot;Socket: &quot; + ss);        try &#123;            DataInputStream dis &#x3D; new DataInputStream(ss.getInputStream());            DataOutputStream dos &#x3D; new DataOutputStream(ss.getOutputStream());            &#x2F;&#x2F; the interaction            dos.writeUTF(studentNum); &#x2F;&#x2F; 向服务器发送学号            dos.flush();            result &#x3D; dis.readUTF().toString(); &#x2F;&#x2F; 获得客户端的json字符串            System.out.println(result);            dos.writeUTF(&quot;end&quot;); &#x2F;&#x2F; 不加这句会报错            dos.flush();            if (dos !&#x3D; null)                dos.close();            if (dis !&#x3D; null)                dis.close();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125; finally &#123;            if (ss !&#x3D; null)                ss.close();        &#125;    &#125;&#x2F;&#x2F; gui界面用于获取json结果    public String getResult() &#123;        return result;    &#125;&#125;</code></pre><h4 id="server.java"><code>Server.java</code></h4><pre class="line-numbers language-java" data-language="java"><code class="language-java">package socketProject;import java.io.*;import java.net.*;public class Server extends Thread &#123;    public static final int PORT &#x3D; 8888;    &#x2F;&#x2F; public static void main(String[] args) throws IOException &#123;    public void run() &#123;        try (ServerSocket serverSocket &#x3D; new ServerSocket(PORT)) &#123;            System.out.println(&quot;ServerSocket: &quot; + serverSocket);            try &#123;                while (true) &#123;                    Socket socket &#x3D; serverSocket.accept();                    System.out.println(&quot;Socket accept: &quot; + socket);                    Thread thread &#x3D; new Thread(new ServerThread(socket));                    thread.start(); &#x2F;&#x2F; 开启一个线程，使之支持接收多个客户端的请求                &#125;            &#125; finally &#123;                serverSocket.close();            &#125;        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><h4 id="serverthread.java"><code>ServerThread.java</code></h4><pre class="line-numbers language-java" data-language="java"><code class="language-java">package socketProject;import java.io.*;import java.net.*;public class ServerThread extends Thread &#123;    Socket socket &#x3D; null;    public ServerThread(Socket socket) &#123;        this.socket &#x3D; socket;    &#125;    public void run() &#123;        try &#123;            DataInputStream dis &#x3D; new DataInputStream(socket.getInputStream());            DataOutputStream dos &#x3D; new DataOutputStream(socket.getOutputStream());            while (true) &#123;                String str &#x3D; dis.readUTF().toString();                String data &#x3D; new SearchInfo().run(str);                if (str.equals(&quot;end&quot;))                    break;                dos.writeUTF(data);            &#125;            dos.close();            dis.close();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><h4 id="searchinfo.java"><code>SearchInfo.java</code></h4><pre class="line-numbers language-java" data-language="java"><code class="language-java">package socketProject;import java.io.*;import java.net.*;public class SearchInfo &#123;    public String run(String s) &#123;        String url &#x3D; &quot;your database interface&quot;;        String param &#x3D; s;        String sendGET &#x3D; GetUrl(url, param);        return sendGET;    &#125;        public static String GetUrl(String url, String param) &#123;        String result &#x3D; &quot;&quot;; &#x2F;&#x2F; define the result str        BufferedReader read &#x3D; null; &#x2F;&#x2F; define the access result                try &#123;            URL realUrl &#x3D; new URL(url + param);            URLConnection connection &#x3D; realUrl.openConnection();                        connection.setRequestProperty(&quot;accept&quot;, &quot;*&#x2F;*&quot;);            connection.setRequestProperty(&quot;connection&quot;, &quot;Keep-Alive&quot;);            connection.setRequestProperty(&quot;user-agent&quot;, &quot;Mozilla&#x2F;4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)&quot;);            &#x2F;&#x2F; 这里补充通用的请求属性            connection.connect(); &#x2F;&#x2F; 建立实际的连接                        read &#x3D; new BufferedReader(new InputStreamReader(connection.getInputStream(), &quot;UTF-8&quot;));            String line;            while ((line &#x3D; read.readLine()) !&#x3D; null) &#123;                result +&#x3D; line;            &#125;        &#125; catch (Exception e) &#123;            e.printStackTrace();        &#125; finally &#123;            if (read !&#x3D; null) &#123;&#x2F;&#x2F; 关闭流                try &#123;                    read.close();                &#125; catch (Exception e) &#123;                    e.printStackTrace();                &#125;            &#125;        &#125;        return result;    &#125;        public String getJSON(String param) &#123;        return param;    &#125;&#125;</code></pre><h4 id="gui.java"><code>GUI.java</code></h4><pre class="line-numbers language-java" data-language="java"><code class="language-java">package socketProject;import java.awt.*;import java.awt.event.*;import java.io.IOException;import javax.swing.*;public class GUI extends JFrame &#123;    private JButton connectDataBase;    private JLabel entryStudentNum;    private JTextField studentNum;    private JButton sendRequest;    private JLabel showResponseMsg;    private JPanel northPanel;    private JPanel southPanel;    public GUI() &#123;        init();    &#125;    public void init() &#123;        setTitle(&quot;没啥技术含量的东西&quot;);        &#x2F;&#x2F; define the component for the window        connectDataBase &#x3D; new JButton(&quot;连接数据库&quot;);        entryStudentNum &#x3D; new JLabel(&quot;输入学号&quot;);        studentNum &#x3D; new JTextField();        sendRequest &#x3D; new JButton(&quot;发送&quot;);        showResponseMsg &#x3D; new JLabel();        &#x2F;&#x2F; add the component to the panel        this.setLayout(new GridLayout(2, 1));        northPanel &#x3D; new JPanel(new GridLayout(1, 4));        northPanel.add(connectDataBase);        northPanel.add(entryStudentNum);        northPanel.add(studentNum);        northPanel.add(sendRequest);        southPanel &#x3D; new JPanel(new GridLayout(1, 1));        southPanel.add(showResponseMsg);        setButtons();        this.add(northPanel);        this.add(southPanel);        &#x2F;&#x2F; initial the window        setBounds(400, 200, 600, 120);        setResizable(false);        setDefaultCloseOperation(EXIT_ON_CLOSE);        setVisible(true);    &#125;    public void setButtons() &#123;        connectDataBase.addActionListener(new ActionListener() &#123;            @Override            public void actionPerformed(ActionEvent e) &#123;                &#x2F;&#x2F; 这里初始化服务端                Server server1 &#x3D; new Server();                Thread th1 &#x3D; new Thread(server1);                th1.start();                &#x2F;&#x2F; 这里一定要开启服务端线程，否则在点击此按钮后，整个界面会卡住，无法进行下一步操作            &#125;        &#125;);        sendRequest.addActionListener(new ActionListener() &#123;            @Override            public void actionPerformed(ActionEvent e) &#123;                Client client1 &#x3D; new Client();                client1.setStudentNum(studentNum.getText());                &#x2F;&#x2F; 获取文本框的文字，并赋给客户端的studentNum保存                try &#123;                    client1.run();                &#125; catch (IOException e1) &#123;                    e1.printStackTrace();                &#125;                showResponseMsg.setText(client1.getResult());                &#x2F;&#x2F; 将得到的数据显示在界面上            &#125;        &#125;);    &#125;    public static void main(String[] args) &#123;        new GUI();    &#125;&#125;</code></pre><p>最终效果如下：</p><p><img src="0x0030/0x0030_2.png"></p><p>使用时，先点击连接数据库，然后根据学校提供的接口，输入自己的学号，点击发送，即可查询个人信息。</p><p>不过由于项目工作区非maven以及未来方向非Java的缘故，没有去深究如何提取json的值<del>(偷个懒)</del>。</p><h4 id="参考链接">参考链接</h4><p><ahref="https://blog.csdn.net/dmkaadmk/article/details/52679925">Java请求一个URL，获取返回的数据_杜岚特的博客-CSDN博客</a></p><p><ahref="https://blog.csdn.net/weixin_34697393/article/details/114620733">java.io.datainputstream.readunsignedshort_socket编程报异常java.io.EOFException_窦月汐的博客-CSDN博客</a></p>]]></content>
    
    
    <summary type="html">用Java swing实现一个套接字访问数据库</summary>
    
    
    
    <category term="BackEnd" scheme="https://jaydenchang.top/categories/BackEnd/"/>
    
    
    <category term="Java" scheme="https://jaydenchang.top/tags/Java/"/>
    
  </entry>
  
  <entry>
    <title>七天用go实现一个web框架</title>
    <link href="https://jaydenchang.top/post/0x002F.html"/>
    <id>https://jaydenchang.top/post/0x002F.html</id>
    <published>2022-08-06T16:00:00.000Z</published>
    <updated>2025-07-27T08:42:10.092Z</updated>
    
    <content type="html"><![CDATA[<hr/><h3 id="前言">前言</h3><p>本文学习自<a href="https://geetktutu.com">geektutu</a> ,大部分内容摘自 <ahref="https://geektutu.com/post/gee.html">7天用Go从零实现Web框架Gee教程| 极客兔兔(geektutu.com)</a>，并在此基础上稍加个人的学习历程和理解。</p><p>作者仓库地址：<ahref="https://github.com/geektutu/7days-golang">geektutu/7days-golang: 7days golang programs from scratch (web framework Gee, distributed cacheGeeCache, object relational mapping ORM framework GeeORM, rpc frameworkGeeRPC etc) 7天用Go动手写/从零实现系列 (github.com)</a></p><h3 id="day0.-设计一个框架">day0. 设计一个框架</h3><p>大部分时候 , 实现一个Web应用 , 第一反应是用哪个框架 , 在Golang中 ,新框架层出不穷 , 例如<code>Beego</code> , <code>Gin</code> ,<code>Iris</code> 等 , 那为什么不用标准库 , 而必须使用框架呢 ?在设计一个框架时 , 我们需要知道核心框架为我们解决了什么问题 ,只有明白这一点 , 才能想明白我们要在框架内实现什么功能。</p><p>刚好最近学院开展了一个软件实训课程 , 在五天之内搭好了一个Java游戏框架, 基本框架搭好之后 , 只需要替换配置文档和游戏元素 ,就可以做出另一款新的游戏 , 当然 ,在理解这个框架的搭建思路的背后是异常痛苦的 (前后花了6天左右去看课程视频, 各种修bug) , 在做出作品那一刻，虽然运行起来的会让人莫名想笑 ,心情舒畅，不过总算实现了代码和人一个能跑了bushi ,很敬佩游戏框架设计师的奇思妙想 <del>(脑洞)</del> 。</p><p>有点扯远了 ,我们先看看Golang标准库<code>net/http</code>如何处理一个请求。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (    &quot;net&#x2F;http&quot;    &quot;fmt&quot;)func main() &#123;    http.HandleFunc(&quot;&#x2F;&quot;, handler)    http.HandleFunc(&quot;&#x2F;count&quot;, handler)    log.Fatal(http.ListenAndServe(&quot;localhost:8000&quot;, nil))&#125;func handler(w http.ResponseWriter, r *http.Request) &#123;    fmt.Fprintf(w, &quot;URL.Path &#x3D; %q\n&quot;, r.URL.Path)&#125;</code></pre><h5 id="基础知识">基础知识</h5><h6 id="interface">interface</h6><p>首先定义一个<code>Animal</code>的接口</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Animal interface &#123;    Speak() string&#125;</code></pre><p>golang中没有 implements 关键字，那么如何实现接口呢？</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Dog struct &#123;&#125;func (d Dog) Speak() string &#123;    return &quot;Woof!&quot;&#125;type Cat struct &#123;&#125;func (c Cat) Speak() string &#123;    return &quot;Meow!&quot;&#125;&#x2F;&#x2F; 只要实现了Speak()，就算是实现了Animal接口</code></pre><h6 id="interface-1">interface</h6><p>interface{} 类型，空接口，很容易和interface弄混。interface{}是没有方法的接口。由于没有 implements关键字，所以所有类型都至少实现了0个方法，所以<strong>所有类型都实现了空接口</strong>。这意味着，如果在写一个函数以interface{} 为参数，那么可以为该函数提供任何值。例如</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func DoSomething(v interface&#123;&#125;) &#123;    &#x2F;&#x2F; ...&#125;</code></pre><p>在DoSomething 内部，开始我也认为v时任意类型，但这是错误的，v不是任意类型，它的静态类型是<code>interface&#123;&#125;</code>类，动态类型由传入的参数的类型决定，当然返回参数时，就不要返回<code>interface&#123;&#125;</code>类了。</p><p><code>interface&#123;&#125;</code>可以承载任意值，但不代表任意类型就可以承接空接口类型的值。当将值传递给DoSomething函数时，golang将执行类型转换 (ifnecessary)，并将值转换为<code>interface&#123;&#125;</code>类型的值。</p><blockquote><p>题外话，<code>interface&#123;&#125;</code>动态类型慎用，特别是面对需求容易改动的项目，另外，一般不要对动态类型的值进行比较操作</p></blockquote><h6 id="http.responsewriter">http.ResponseWriter</h6><p>首先需要了解<code>HandleFunc</code>这个函数的一些信息，其声明如下。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) &#123;    DefaultServeMux.HandleFunc(pattern, handler)&#125;</code></pre><p>在main函数中，字符串部分容易理解，那handler呢，来看看它的参数的源码。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type ResponseWriter interface &#123;    Header() Header    Write([]byte) (int, error)    WriteHeader(statusCode int)&#125;</code></pre><p>还是不太清楚，再点击<code>Header</code>看看。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Header map[string][]string</code></pre><p>http.Header结构包含请求头信息，常见信息实例如下。</p><pre class="line-numbers language-none"><code class="language-none">Host: example.comaccept-encoding: gzip, deflateAccept-Language: en-usfoo: Bar</code></pre><p>接下来看看<code>Write([]byte) (int, error)</code>，这是一个接口，实现通用的<code>io.Writer</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Writer interface &#123;    Write(p []byte) (n int, err error)    &#x2F;&#x2F; 这个byte其实是个切片,而Write方法在server.go里被重写了,具体先不贴代码,本身含有Fprintf方法,后文会用到&#125;</code></pre><p>最后到<code>WriteHeader(statusCode int)</code>。</p><p><code>WriteHeader</code>这个方法名有点误导，并不是来设置响应头的，该方法支持传入一个整形数据表示响应状态码，不调用该方法的话，默认值是<code>200 OK</code>。</p><h6 id="http.request">http.Request</h6><p>直接看源码的声明。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Request struct &#123;    Method string    URL *url.URL    Proto string   &#x2F;&#x2F;eg &quot;HTTP&#x2F;1.0&quot;    ProtoMajor int      ProtoMinor int    Header Header    Body io.ReaderCloser    GetBody func() (io.ReadCloser, error)    ContentLength int64    TransferEncoding []string    Close bool    Host string    Form url.Values    PostForm url.Values    MultipartForm *multipart.Form    Trailer Header    RemoteAddr string    RequestURI string    TLS *tls.ConnectionState    Cancel &lt;-chan struct&#123;&#125;    Response *Response    ctx context.Context&#125;</code></pre><p>常见的Request报文段信息如下：</p><p><img src='0x002F/gee_day0_2.jpg'></p><h6 id="http.listenandserve">http.ListenAndServe</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListenAndServe(addr string, handler Handler) error &#123;    server :&#x3D; &amp;Server&#123;Addr: addr, Handler: handler&#125;    return server.ListenAndServe()&#125;</code></pre><p>在<code>ListenAndServe</code>中，再查看<code>Server</code>和<code>ListenAndServe()</code>的源码</p><blockquote><p>http.Server</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Server struct &#123;Addr string   &#x2F;&#x2F; 服务器的IP地址和端口信息    Handler Handler  &#x2F;&#x2F; 请求处理函数的路由复用器    ReadTimeout time.Duration    WriteTimeout time.Duration    MaxHeaderBytes int    TLSConfig *tls.Config    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)    ConnState func(net.Conn, ConnState)    ErrorLog *log.Logger    disableKeepAlives int32&#125;</code></pre><blockquote><p>http.ListenAndServe()</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (srv *Server) ListenAndServe() error &#123;    if srv.shuttingDown() &#123;        return ErrServerClosed  &#x2F;&#x2F; 如果Server已关闭，直接返回ErrServerClosed    &#125;    addr :&#x3D; srv.Addr    if addr &#x3D;&#x3D; &quot;&quot; &#123;        addr &#x3D; &quot;:http&quot;    &#125;    ln, err :&#x3D; net.Listen(&quot;tcp&quot;, addr)     if err !&#x3D; nil &#123;        return err    &#125;    return srv.Serve(ln)&#125;</code></pre><p>在本例中，传入了端口号和handler，如果不指定ip就用本机地址(localhost)，如果不指定服务器地址信息，则默认以<code>:http</code>作为地址信息</p><h6 id="fmt.fprintf">fmt.Fprintf</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Fprintf(w io.Writer, format string, a ...any) (n int, err error) &#123;    p :&#x3D; newPrinter()    p.doPrintf(format, a)    n, err &#x3D; w.Write(p.buf)    p.free()    return&#125;</code></pre><h6 id="log.fatal">log.Fatal</h6><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Fatal(v ...any) &#123;    std.Output(2, fmt.Sprint(v...))    os.Exit(1)&#125;</code></pre><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (l *Logger) Fatal(v ...any) &#123;    l.Output(2, fmt.Sprint(v...))    os.Exit(1)&#125;</code></pre><p>在函数上面的定义，Fatal等价于<code>Print()</code>，执行完打印直接退出程序，之前通过<code>defer</code>设置延迟的函数不会被运行</p><h5 id="框架说明">框架说明</h5><p><code>net/http</code>提供了基础的Web功能 , 即监听端口 ,解析HTTP报文。一些Web开发中简单的需求并不支持，需要手工实现。</p><ul><li>动态路由 : 例如<code>hello/:name</code> ,<code>hello/*</code>这类的规则</li><li>鉴权 (authentication) : 没有分组/统一鉴权的能力 ,需要在每个路由映射的handler中实现</li><li>模板 : 没有统一简化的HTML机制</li><li>...</li></ul><p>可以发现，当我们离开框架，使用基础库时，需要频繁手工处理的地方，就是框架的价值所在。</p><h3 id="day1.-http.handler">day1. http.Handler</h3><h4 id="base1">base1</h4><h5 id="标准库启动web服务">标准库启动Web服务</h5><p>Golang内置了<code>net/http</code>库 , 封装了HTTP网络编程的基础的接口, 本次复刻的<code>Gee</code> Web框架便是基于<code>net/http</code>的 ,下面通过一个例子 , 简单介绍下这个库的使用。</p><blockquote><p>day1/base1/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (    &quot;fmt&quot;    &quot;net&#x2F;http&quot;    &quot;log&quot;)func main() &#123;    http.HandleFunc(&quot;&#x2F;&quot;, indexHandler)    http.HandleFunc(&quot;&#x2F;hello&quot;, helloHandler)    log.Fatal(http.ListenAndServe(&quot;:9999&quot;, nil))&#125;func indexHandler(w.http.ResponseWriter, req *http.Request) &#123;    fmt.Fprintf(w, &quot;URL.Path &#x3D; %q\n&quot;, req.URL.Path)&#125;func helloHandler(w http.ResponseWriter, req *http.Request) &#123;    for k, v :&#x3D; range req.Header &#123;        fmt.Fprintf(w, &quot;Header[%q] &#x3D; %q\n&quot;, k, v)    &#125;&#125;</code></pre><p>在上面 , 设置了两个路由 , <code>/</code>和<code>/hello</code> ,分别绑定<code>indexHandler</code>和<code>helloHandler</code> ,根据不同的HTTP请求会调用不同的处理函数 , 访问<code>/</code> ,响应是<code>URL.Path=/</code> , 而<code>/hello</code>的相应则是请求头(header)中键值对信息。</p><p>用curl工具测试 , 会得到以下结果。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;URL.Path &#x3D; &quot;&#x2F;&quot;$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;helloHeader[&quot;User-Agent&quot;]&#x3D;[&quot;curl&#x2F;7.68.0&quot;]Header[&quot;Accept&quot;]&#x3D;[&quot;*&#x2F;*&quot;]$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;helloWorldURL.Path &#x3D; &quot;&#x2F;helloWorld&quot;</code></pre><p>main函数的最后一行 , 是来启动服务的 , 第一个是参数地址 ,<code>:9999</code>表示在<code>9999</code>端口监听 ,而第二个参数则代表处理所有的HTTP请求的实例 ,<code>nil</code>代表使用标准库中的实例处理 ,也是我们基于<code>net/http</code>标准库实现Web框架的入口。</p><p>这里第三个命令 , 访问了<code>/helloWorld</code> ,而在文件中未定义<code>/helloWorld</code>这个路由 , 用curl工具仅从测试 ,得到的却是<code>URL.Path</code> , 关于这个bug , 后文会进行修改。</p><h5 id="实现http.handler接口">实现http.Handler接口</h5><pre class="line-numbers language-go" data-language="go"><code class="language-go">package httptype Handler interface &#123;    ServeHTTP(w ResponseWriter, r *Request)&#125;func ListenAndServe(address string, h Handler) error</code></pre><p>查阅<code>net/http</code>源码发现 , <code>Handler</code>是一个接口 ,需要实现方法 ServeHTTP , 也就是说 , 只要传入任何实现了ServeHTTP接口的实例 , 所有的HTTP请求 , 就都交给了该实例处理。</p><h4 id="base2">base2</h4><blockquote><p>day1/base2/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (    &quot;fmt&quot;    &quot;log&quot;    &quot;net&#x2F;http&quot;)type Engine struct&#123;&#125;&#x2F;&#x2F; 定义一个空的结构体,并命名为Engine,后期可以直接用Engine加&#39;.&#39;加成员名的方式调用func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) &#123;    &#x2F;&#x2F; 这里engine作为Engine类型的对象,拥有Engine的所有方法    switch req.URL.Path &#123;        case &quot;&#x2F;&quot;:            fmt.Fprintf(w, &quot;URL.Path &#x3D; %q\n&quot;, req.URL.Path)        case &quot;&#x2F;hello&quot;:            for k, v :&#x3D; range req.Header &#123;                fmt.Fprintf(w, &quot;Header[%q] &#x3D; %q\n&quot;, k, v)            &#125;        default:            fmt.Fprintf(w, &quot;404 NOT FOUND: %s\n&quot;, req.URL)    &#125;&#125;func main() &#123;    engine :&#x3D; new(Engine)    log.Fatal(http.ListenAndServe(&quot;:9999&quot;, engine))&#125;</code></pre><p>后面复盘时，有一点一直搞不懂，究竟<code>ServeHTTP</code>是怎么被调用的？在这个go文件里面，感觉还是得从<code>ListenAndServe</code>下手，<code>ListenAndServe</code>的定义是这样的</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func ListenAndServe(addr string, handler Handler) error &#123;    serve :&#x3D; &amp;Server&#123;Addr: addr, Handler: handler&#125; &#x2F;&#x2F; 创建一个Server结构体    return server.ListenAndServe()    &#x2F;&#x2F; 这里开始也还是没怎么看懂,后面也去查了下资料,这里返回的ListenAndServe,传入到Fatal里,如果不报错的话,是正常打印switch中的内容,有错误就打印错误信息&#125;</code></pre><p>继续追溯<code>server.ListenAndServe()</code></p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (srv *Server) ListenAndServe() error &#123;    addr :&#x3D; srv.Addr    if addr &#x3D;&#x3D; &quot;&quot; &#123;        addr &#x3D; &quot;:http&quot;    &#125;    ln, err :&#x3D; net.Listen(&quot;tcp&quot;, addr)    if err !&#x3D; nil &#123;        return err    &#125;    return srv.Serve(ln)&#125;</code></pre><p>此方法只是开始侦听给定的地址,并用新创建的侦听器调用<code>Server</code>方法</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (srv *Server) Serve(l net.Listener) error &#123;    defer l.close()    ...    for&#123;        rw, e :&#x3D; l.Accept()        ...        c :&#x3D; srv.newConn(rw)        c.setState(c.rwc, StateNew)        go c.serve(ctx)    &#125;&#125;</code></pre><p>从<code>Serve</code>方法我们可以看到 ,这是我们接受新连接并开始在它自己的<code>goroutine</code>中处理它的地方</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (c *Conn) serve(ctx context.Context) &#123;    ...    for &#123;        w, err :&#x3D; c.readRequest(ctx)        ...        serverHandler&#123;c.server&#125;.ServeHTTP(w, w.req)        ...    &#125;&#125;</code></pre><p>在这里 , 才调用了<code>ServeHTTP</code>方法 , 正如我们在上面看到的 ,我们对这个ServeHTTP方法进行重写 , 打印输出HTTP请求的信息</p><hr><p>回到<code>main.go</code> ,这里定义了一个空的结构体<code>Engine</code> ,实现了方法<code>ServrHTTP</code> , 这个方法有两个参数 ,第二个参数是Request , 该对象包含了该HTTP请求的所有信息 , 比如请求地址 ,Header和Body等信息 ; 第一个参数是ResponseWriter ,利用ResponseWriter可以构造指针对该请求的相应。</p><p>在main函数里 ,我们给ListenAndServe方法的第二个参数传入了刚才创建的<code>engine</code>实例, 至此已经踏出了实现Web框架的第一步 ,即将所有的HTTP请求转向了我们自己的处理逻辑 。</p><p>在实现<code>Engine</code>之前 ,我们调用<code>http.HandleFunc</code>实现了路由和Handler的映射 ,也就是只能针对具体的路由写处理逻辑 , 比如<code>\hello</code> ,但在实现<code>engine</code>后 , 我们拦截了所有的HTTP请求 ,拥有了统一的控制入口 , 在这里我们可以自由定义路由映射的规则 ,也可以统一添加一些处理逻辑 , 例如日志 , 异常处理等。</p><p>代码运行结果前两行代码的结果一致 , 第三行代码结果如下。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;helloWorld404 NOT FOUND: &#x2F;helloWorld</code></pre><h4 id="base3">base3</h4><h5 id="gee框架的雏形">Gee框架的雏形</h5><p>下面重新来组织上面的代码 , 搭建整个框架的雏形。</p><pre class="line-numbers language-none"><code class="language-none">gee&#x2F;  |--gee.go  |--go.modmain.gogo.mod</code></pre><blockquote><p>day1/base3/go.mod</p></blockquote><pre class="line-numbers language-none"><code class="language-none">module examplego 1.18require gee v1.0.0replace gee &#x3D;&gt; .&#x2F;gee</code></pre><blockquote><p>day1/base3/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;fmt&quot;    &quot;net&#x2F;http&quot;    &quot;gee&quot;)func main() &#123;    r :&#x3D; gee.New()    r.GET(&quot;&#x2F;&quot;, func(w http.ResponseWriter, req *http.Request) &#123;        fmt.Fprintf(w, &quot;URL.Path &#x3D; %q\n&quot;, req.URL.Path)    &#125;)        r.GET(&quot;&#x2F;hello&quot;, func(w http.RequestWriter, req *http.Request) &#123;        for k, v :&#x3D; range req.Header &#123;            fmt.Fprintf(w, &quot;Header[%q] &#x3D; %q\n&quot;, k, v)        &#125;    &#125;)        r.Run(&quot;:9999&quot;)&#125;</code></pre><p>在这里 , 使用<code>GET()</code>方法添加路由 ,最后使用<code>Run()</code>启动Web服务 , 这里的路由 , 只是静态路由 ,不支持<code>/hello/:name</code>这样的动态路由 ,动态路由将在下一次实现。</p><blockquote><p>day1/base3/gee/go.mod</p></blockquote><pre class="line-numbers language-none"><code class="language-none">module geego 1.18</code></pre><p>因为我是用vscode进行代码编辑，工作区选择gee的根目录，而不是<code>src</code>，这里的<code>go.mod</code>管理需要做一点额外工作，按照教程写好mod后，会提示无法导入gee模块，查了各种帖子，对<code>Go111Module</code>进行设置也没效果，后面查到了一篇帖子，要在设置里搜<strong>go.useLanguageServer</strong>，并将其关闭</p><blockquote><p>day1/base3/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package geeimport (    &quot;fmt&quot;    &quot;net&#x2F;http&quot;)type HandlerFunc func(http.ResponseWriter, *http.Request) &#x2F;&#x2F; 定义了一个HandlerFunc的函数类型,其签名必须符合输入为 http.ResponseWriter和*http.Requesttype Engine struct &#123;    router map[string]HandlerFunc&#125;func New() *Engine &#123;    return &amp;Engine&#123;router: make(map[string]HandlerFunc)&#125;&#125;func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) &#123;    key :&#x3D; method + &quot;-&quot; +pattern    engine.router[key] &#x3D; router&#125;func (engine *Engine) GET(pattern string, handler HandlerFunc) &#123;    engine.addRoute(&quot;GET&quot;, pattern, handler)&#125;func (engine *Engine) POST(pattern string, handler HandlerFunc) &#123;    engine.addRoute(&quot;POST&quot;, pattern, handler)&#125;func (engine *Engine) Run(addr string) (err error) &#123;    return http.ListenAndServe(addr, engine)&#125;func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) &#123;    key :&#x3D; req.Method + &quot;-&quot; + req.URL.Path    if handlerm ok :&#x3D; engine.router[key]; ok &#123;        hanlder(w, req)    &#125; else &#123;        fmt.Fprintf(w, &quot;404 NOT FOUND: %s\n&quot;, req.URL)    &#125;&#125;</code></pre><p>在<code>gee.go</code> , 介绍下这几部分的实现。</p><ul><li>定义了类型<code>HandlerFunc</code> , 这是提供给框架用户的 ,用来定义路由映射的基本方法。我们在<code>Engine</code>中，添加了一张路由映射表<code>router</code>，key由请求方法和静态路由地址构成，例如<code>GET-/</code>，<code>GET-/hello</code>，<code>POST-/hello</code>，这样针对相同的路由，如果请求方式不同，可以映射不同的处理方法(Handler)，value是用户映射的处理方法。</li><li>当用户调用<code>(*Engine).Run()</code>方法，会将路由和处理方法注册到映射表router中，<code>(*Engine).Run()</code>，是ListenAndServe的包装。</li><li><code>Engine</code>实现的ServeHTTP方法的作用就是，解析请求的路径，查找路由映射表，如果查到，就执行注册的处理方法，如果查不到，就返回404NOT FOUND。</li></ul><p>执行<code>go run main.go</code>，再用curl访问，结果和base2的结果一致</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;URL.Path &#x3D; &quot;&#x2F;&quot;$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;helloHeader[&quot;Accept&quot;] &#x3D; [&quot;*&#x2F;*&quot;]Header[&quot;User-Agent&quot;] &#x3D; [&quot;curl&#x2F;7.68.0&quot;]$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;helloWorld404 NOT FOUND: &#x2F;helloWorld</code></pre><p>至此，整个Gee框架的原型就已经出来了。有了基本路由映射表，提供了用户注册静态路由的方法，包装了启动服务的函数。当然到目前为止，我们还没有实现比<code>net/http</code>标准库更强大的能力，这些会在后面将动态路由、中间件等功能添加上去。</p><h3 id="day2.-上下文-context">day2. 上下文 Context</h3><ul><li>将 路由(router) 独立出来 , 方便之后改进。</li><li>设计 上下文 (Context)，封装 Request和Response，提供对JSON、HTML等返回类型的支持。</li><li>第二天的框架内容，代码约140行，新增约90行。</li><li>后面每一天贴出的代码基本为原文件基础上新增的内容或修改后的内容</li></ul><h4 id="设计context">设计Context</h4><h5 id="必要性">必要性</h5><ol type="1"><li>对Web服务来说，无非是根据请求<code>*http.Request</code>，构造响应<code>http.ResponseWriter</code>。但是这两个对象提供的接口粒度太细，比如我们要构造一个完整的相应，需要考虑消息头(Header)和消息体(Body)，而Header包含了状态码(StatusCode)，消息类型(ContentType)等几乎每次都需要设置的信息。因此，如果不进行有效的封装，那么框架的用户将需要写大量重复、繁冗的代码，且容易出错。针对常用场景，能够高效地构造出HTTP相应是一个好的框架必须考虑的点。</li></ol><p>用返回JSON数据作比较，对比封装前后的差异：</p><blockquote><p>封装前</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">obj &#x3D; map[string]interface&#123;&#125; &#123;    &quot;name&quot;: &quot;abc&quot;,    &quot;password&quot;: &quot;1234&quot;,&#125;w.Header().Set(&quot;Content-Type&quot;, &quot;application&#x2F;json&quot;)w.WriteHeader(http.StatusOK)encoder :&#x3D; json.NewEncoder(w)if err :&#x3D; encoder.Encode(obj); err !&#x3D; nil &#123;    http.Error(w, err.Error(), 500)&#125;</code></pre><blockquote><p>封装后</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">c.JSON(http.StatusOK, gee.H&#123;    &quot;username&quot;: c.PostForm(&quot;username&quot;),    &quot;password&quot;: c.PostForm(&quot;password&quot;),&#125;)</code></pre><ol start="2" type="1"><li>针对使用场景，封装<code>*http.Request</code>和<code>http.ResponseWriter</code>的方法，简化相关接口的调用，只是设计Context的原因之一。对于框架来说，还需要支撑额外的功能。例如，将来解析动态路由<code>/hello/:name</code>，参数<code>name</code>的值放在哪？再比如，框架需要支持中间件，那中间件产生的信息放在哪？Context随着每一个请求的出现而产生，请求的结束而销毁，和当前请求强相关的信息都应由Context承载。因此，设计Context结构，扩展性和复杂性留在了内部，而对外简化了接口。路由的处理函数，以及将要实现的中间件，参数都统一使用Context实例</li></ol><h5 id="具体实现">具体实现</h5><blockquote><p>day2/gee/context.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type H map[string]interface&#123;&#125;type Context struct &#123;    &#x2F;&#x2F; origin objects    Writer http.ResponseWriter    Req *http.Request    &#x2F;&#x2F; request info    Path string    Method string    &#x2F;&#x2F; response info    StatusCode int&#125;func newContext(w http.ResponseWriter, req *http.Request) *Context &#123;    return &amp;Context &#123;        Writer: w,        Req req,        Path: req.URL.Path,        Method: req.Method,    &#125;&#125;func (c *Context) PostForm(key string) string &#123;    return c.Req.FormValue(key)&#125;func (c *Context) Query(key string) string &#123;    return c.Req.URL.Query().Get(key)&#125;func (c *Context) Status(code int) &#123;    c.StatusCode &#x3D; code    c.Writer.WriteHeader(code)&#125;func (c *Context) SetHeader(key string, value string) &#123;    c.Writer.Header().Set(key, value)&#125;func (c *Context) String(code int, format string, values ...interface&#123;&#125;) &#123;    c.SetHeader(&quot;Content-Type&quot;, &quot;text&#x2F;plain&quot;)    c.Status(code)    c.Writer.Write([]byte(fmt.Sprintf(format, values...)))&#125;func (c *Context) JSON(code int, obj interface&#123;&#125;) &#123;    c.SetHeader(&quot;Content-Type&quot;, &quot;application&#x2F;json&quot;)    c.Status(code)    encoder :&#x3D; json.NewEncoder(c.Writer)    if err :&#x3D; encoder.Encode(obj); err !&#x3D; nil &#123;        http.Error(c.Writer, err.Error(), 500)    &#125;    &#x2F;&#x2F; 这里的obj,在后文的测试中,就是一个map,输出为[&quot;passowrd&quot;:&quot;xxx&quot;,&quot;username&quot;:&quot;xxx&quot;]&#125;func (c *Context) Data(code int, data []byte) &#123;    c.Status(code)    c.Writer.Write(data)&#125;func (c *Context) HTML(code int, html string) &#123;    c.SetHeader(&quot;Content-Type&quot;, &quot;text&#x2F;html&quot;)    c.Status(code)    c.Writer.Write([]byte(html))&#125;</code></pre><ul><li>代码最开头，给<code>map[string]interface&#123;&#125;</code>起了一个别名<code>gee.H</code>，构建JSON数据时，显得更简洁。</li><li><code>Context</code>目前只包含了<code>http.ResponseWriter</code>和<code>*http.Request</code>，另外提供了对Method和Path这两个常用的属性的直接访问。</li><li>提供了访问Query和PostForm参数的方法。</li><li>提供了快速构造String/Data/JSON/HTML相应的方法。</li></ul><h4 id="路由router">路由(Router)</h4><p>我们将和路由相关的方法和结构提取了出来，放到了一个新的文件<code>router.go</code>，方便下一次对router的功能进行增强，例如提供动态路由的支持。router的handle方法作了一个细微的调整，即handler的参数，变成了Context。</p><blockquote><p>day2/gee/router.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type router struct &#123;    handlers map[string]HandlerFunc&#125;func newRouter() *router &#123;    return &amp;router&#123;handlers: make(map[string]HandlerFunc)&#125;&#125;func (r *router) addRoute(method string, pattern string, handler HandlerFunc) &#123;    log.Printf(&quot;Route %4s - %s&quot;, method, pattern)    key :&#x3D; method + &quot;-&quot; + pattern    r.handlers[key] &#x3D; handler    &#x2F;&#x2F; 注册路由&#125;func (r *router) handle(c *Context) &#123;    key :&#x3D; c.Method + &quot;-&quot; + c.Path    if handler, ok :&#x3D; r.handlers[key]; ok &#123;        &#x2F;&#x2F; 这里根据输入的方法和路径查找handlers中值,返回HandlerFunc,赋给handler,此时handler就是一个带有HandlerFunc签名的函数(http.ResponseWriter和*http.Request),输入的参数类型为*Context,handler再根据输入的Context        handler(c)    &#125; else &#123;        c.String(http.StatusNotFound, &quot;404 NOT FOUND: %s\n&quot;, c.Path)    &#125;&#125;</code></pre><h4 id="框架入口">框架入口</h4><blockquote><p>day2/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package geeimport &quot;net&#x2F;http&quot;&#x2F;&#x2F; HandlerFunc defines the request handler used by gee&#x2F;&#x2F; HandlerFunc包含Context所有属性和方法type HandlerFunc func(*Context)&#x2F;&#x2F; Engine implement the interface of ServeHTTPtype Engine struct &#123;    router *router&#125;&#x2F;&#x2F; New is the constructor of gee.Enginefunc New() *Engine &#123;    return &amp;Engine&#123;router: newRouter()&#125;&#125;func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) &#123;    engine.router.addRoute(method, pattern, handler) &#x2F;&#x2F; 调用router的addRoute方法    &#x2F;&#x2F; router的addRoute方法类似于一个私有函数,通过engine的addRoute传入参数再传递给router&#125;&#x2F;&#x2F; 把第一天的addRoute修改为这样,</code></pre><p>将<code>router</code>相关的代码独立后，<code>gee.go</code>简单了不少。最重要的还是通过实现了ServeHTTP接口，接管了所有的HTTP请求。相比第一天的diamond，这个方法也有细微的调整，在调用router.handle之前，构造了一个Context对象。这个对象目前还非常简单，仅仅是包装了原来的两个参数，之后我们会慢慢地给Context加上更多内容。</p><blockquote><p>day2/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport(    &quot;net&#x2F;http&quot;    &quot;gee&quot;)func main() &#123;    r :&#x3D; gee.New()    r.GET(&quot;&#x2F;&quot;, func(c *gee.Context) &#123;        c.HTML(http.StatusOK, &quot;&lt;h1&gt;hello gee&lt;&#x2F;h1&gt;\n&quot;)    &#125;)     &#x2F;&#x2F; 思路: 向GET传入一个&quot;&#x2F;&quot;的路由和一个匿名的HandlerFunc函数,该函数内部含有相关HTTP请求信息(HTML函数),然后GET把这个路由和handler传给engine的addRoute,经过套娃,再到达router的addRoute    &#x2F;&#x2F; 下面的GET也同理,只不过String和HTML传入的切片不同    r.GET(&quot;&#x2F;hello&quot;, func(c *gee.Context) &#123;        &#x2F;&#x2F; expect &#x2F;hello?name&#x3D;abc        c.String(http.StatusOK, &quot;hello %s, you&#39;re at %s\n&quot;, c.Query(&quot;name&quot;), c.Path)    &#125;)    r.POST(&quot;&#x2F;login&quot;, func(c *gee.Context) &#123;        &#x2F;&#x2F; 在服务器端输出的是 &quot;POST - &#x2F;login&quot;        c.JSON(http.StatusOK, gee.H&#123;            &quot;username&quot;: c.PostForm(&quot;username&quot;),            &quot;password&quot;: c.PostForm(&quot;password&quot;),        &#125;)    &#125;)    r.Run(&quot;:9999&quot;)&#125;</code></pre><p>运行<code>main.go</code>，看看day2的成果：</p><pre class="line-numbers language-text" data-language="text"><code class="language-text">$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;HTTP&#x2F;1.1 200 OKContent-Type: text&#x2F;htmlDate: Fri, 08 Jul 2022 08:29:52 GMTContent-Length: 19&lt;h1&gt;Hello Gee&lt;&#x2F;h1&gt;$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;hello?name&#x3D;abchello abc, you&#39;re at&#x2F;hello$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;login -X POST -d &quot;username&#x3D;abc&amp;password&#x3D;1234&quot;&#123;&quot;password&quot;:&quot;1234&quot;,&quot;username&quot;:&quot;abc&quot;&#125;$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;xxx404 NOT FOUND: &#x2F;xxx</code></pre><p>服务器端输出</p><pre class="line-numbers language-none"><code class="language-none">2022&#x2F;07&#x2F;09 11:38:15 Route  GET - &#x2F;2022&#x2F;07&#x2F;09 11:38:15 Route  GET - &#x2F;hello2022&#x2F;07&#x2F;09 11:38:15 Route POST - &#x2F;login</code></pre><h3 id="day3-前缀树路由router">day3 前缀树路由Router</h3><ul><li>使用Trie树实现动态路由(dynamic route)解析。</li><li>支持两种模式<code>:name</code>和<code>*filepath</code>，代码约150行。</li></ul><h4 id="trie树简介">Trie树简介</h4><p>之前，用了一个非常简单的<code>map</code>结构存储了路由表，用<code>map</code>存储键值对，索引非常高效，但是有一个弊端，键值对的存储方式，只能来索引静态路由。如果我们想支持类似于<code>/hello/:name</code>这样的动态路由怎么办呢？所谓动态路由，即一条路由规则可以匹配某一类型而并非某一条固定的路由，例如<code>/hello/:name</code>，可以匹配<code>/hello/abc</code>，<code>/hello/jayden</code>等。</p><p>动态路由有很多种实现方式，支持的规则，性能等有很大的差异。例如开源的路由实现<code>gorouter</code>支持在路由规则种嵌入正则表达式，例如<code>/p/[0-9A-Za-z]+</code>，即路径种的参数仅匹配数字和字母；另一个开源实现<code>httprouter</code>就不支持正则表达式。Web开源框架<code>gin</code>在早期的版本没有实现自己的路由，而是直接用了<code>httprouter</code>，后来又放弃了<code>httprouter</code>，自己实现了一个版本。</p><p><img src='0x002F/gee_day3_trie_tree.jpg'></p><p>实现动态路由最常用的数据结构，被称为前缀树(Trie树)。每个节点的所有子节点都有相同的前缀。这种结构非常适用于路由匹配，例如我们定义了如下路由规则：</p><ul><li><code>/:lang/doc</code></li><li><code>/:lang/tutorial</code></li><li><code>/:lang/intro</code></li><li><code>/about</code></li><li><code>/p/blog</code></li><li><code>/p/related</code></li></ul><p>我们用前缀树表示，是这样的：</p><p><img src='0x002F/gee_day3_trie_tree_web.jpg'></p><p>HTTP请求的路径恰好是由<code>/</code>分割的多端构成的，因此，每一段可以作为前缀树的一个节点。我们通过树结构查询，如果中间某一层的节点都不满足条件，那么就说明没有匹配到的路由，查询结束。</p><p>接下来我们实现的动态路由具备以下俩功能：</p><ul><li>参数匹配：例如<code>/p/:lang/doc</code>，可以匹配<code>/p/c/doc</code>和<code>p/go/doc</code>。</li><li>通配<code>*</code>，例如<code>/static/*filepath</code>，可以匹配<code>/static/fav.ico</code>，也可以匹配<code>/static/js/jQuery.js</code>，这种模式常用于静态服务器，能够递归地匹配子路径。</li></ul><h4 id="trie树实现">Trie树实现</h4><p>首先需要设计树节点上应存储哪些信息。</p><blockquote><p>day3/gee/trie.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type node struct &#123;    pattern string &#x2F;&#x2F; 待匹配路由，例如 &#x2F;p&#x2F;:lang    part string &#x2F;&#x2F; 路由中一部分，例如 :lang    children []*node &#x2F;&#x2F; 子节点，例如 [doc, tutorial, intro]    isWild bool &#x2F;&#x2F; 是否精确匹配，part含有 : 或 * 时为true&#125;&#x2F;&#x2F; 这里重写String函数,便于后期查看相关参数的值(直接输出n.children是打印地址)func (n node) String() string &#123;    return fmt.Sprintf(&quot;pattern:%s, part:%s, children:%s, isWild:%t&quot;, n.pattern, n.part, n.children, n.isWild)&#125;</code></pre><p>与普通的树不同，为了实现动态路由匹配，加上了<code>isWild</code>这个参数。即当我们匹配<code>/p/go/doc</code>这个路由时(<code>/</code>算0个节点)，第一层节点，<code>p</code>精确匹配到了<code>p</code>，第二层节点，<code>go</code>模糊匹配到<code>:lang</code>，那么会将<code>lang</code>这个参数赋值为<code>go</code>，继续下一层匹配。我们将匹配的逻辑，包装为一个辅助函数。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; 第一个匹配成功的节点，用于插入func (n *node) matchChild(part string) *node &#123;    for _, child :&#x3D; range n.children &#123;        &#x2F;&#x2F; 这里之前一直没搞懂为什么要忽略range的第一个参数,查阅资料后发现,第一个参数是索引值index,第二个参数是value,这里第二个参数,就是n的子节点        if child.part &#x3D;&#x3D; part || child.isWild &#123;            return child        &#125;    &#125;    return nil&#125;&#x2F;&#x2F; 所有匹配成功的节点，用于查找func (n *node) matchChildren(part string) []*node &#123;    nodes :&#x3D; make([]*node, 0) &#x2F;&#x2F; new一个长度为0的切片    for _, child :&#x3D; range n.children &#123;        if child.part &#x3D;&#x3D; part || child.isWild &#123;            nodes &#x3D; append(nodes, child)            &#x2F;&#x2F; 将匹配成功的节点加入到nodes中        &#125;    &#125;    return nodes&#125;</code></pre><p>对于路由来说，最重要的当然是注册于匹配了。开发服务时，注册路由规则，映射handler；访问时，匹配路由规则，查找到对应的handler。因此，Trie树需要支持节点的插入与查询。插入功能很简单，递归查找每一层的节点，如果没有匹配到当前<code>part</code>的节点，则新建一个，有一点需要注意，<code>/p/:lang/doc</code>只有在第三层节点，即<code>doc</code>节点，<code>pattern</code>才会设置为<code>/p/:lang/doc</code>。<code>p</code>和<code>:lang</code>节点的<code>pattern</code>属性皆为空。因此，当匹配结束时，我们可以用<code>n.pattern == ""</code>来判断路由规则是否匹配成功。例如，<code>/p/python</code>虽能成功匹配到<code>:lang</code>，但<code>:lang</code>的<code>pattern</code>值为空，因此匹配失败。查询功能，同样也是递归查询每一层的节点，退出规则是，匹配到* ，匹配失败，或匹配到第<code>len(parts)</code>层节点。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (n *node) insert(pattern string, parts []string, height int) &#123;    if len(parts) &#x3D;&#x3D; height &#123;&#x2F;&#x2F; 这里其实是层层递归        n.pattern &#x3D; pattern        return    &#125;        part :&#x3D; parts[height]    child :&#x3D; n.matchChild(part)    if child &#x3D;&#x3D; nil &#123;        child &#x3D; &amp;node&#123;part: part, isWild: part[0] &#x3D;&#x3D; &#39;:&#39; || part[0] &#x3D;&#x3D; &#39;*&#39;&#125;        n.children &#x3D; append(n.children, child)    &#125;    child.insert(pattern, parts, height + 1)&#125;func (n *node) search(parts []string, height int) *node &#123;    if len(parts) &#x3D;&#x3D; height || strings.HasPrefix(n.part, &quot;*&quot;) &#123;        if n.pattern &#x3D;&#x3D; &quot;&quot; &#123;            return nil        &#125;        &#x2F;&#x2F; 若以*为开头的字串,直接以此节点为当前分支尾节点        return n    &#125;        part :&#x3D; parts[height]    &#x2F;&#x2F; (height+1)的值是当前搜索的层数    children :&#x3D; n.matchChildren(part)    &#x2F;&#x2F; children则含有搜索到每一层的part        for _, child :&#x3D; range children &#123;        result :&#x3D; child.search(parts, height + 1)        if result !&#x3D; nil &#123;            return result        &#125;    &#125;        return nil&#125;func (n *node) travel(list *([]*node)) &#123;    if n.pattern !&#x3D; &quot;&quot; &#123;        *list &#x3D; append(*list, n)    &#125;    for _, child :&#x3D; range n.children &#123;        child.travel(list)    &#125;&#125;</code></pre><h4 id="router">Router</h4><p>Trie树的插入与查找都成功实现了，接下来我们将Trie树应用到路由中。我们用roots来存储每种请求方式的Trie树根节点。使用handlers存储每种请求方式的HandlerFunc。getRoute函数中，还解析了<code>:</code>和<code>*</code>两种匹配符的参数，返回一个map。例如<code>/p/go/doc</code>匹配到<code>/p/:lang/doc</code>，解析结果为<code>&#123;lang: "go"&#125;</code>，<code>/static/css/gee.css</code>匹配到<code>/static/*filepath</code>，解析结果为<code>&#123;filepath: "css/gee.css"&#125;</code>。</p><blockquote><p>day3/gee/router.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type router struct &#123;    roots map[string]*node    handlers map[string]HandlerFunc&#125;&#x2F;&#x2F; roots key eg, roots[&#39;GET&#39;] roots[&#39;POST&#39;]&#x2F;&#x2F; handlers key eg, handlers[&#39;GET-&#x2F;p&#x2F;:lang&#x2F;doc&#39;], handlers[&#39;POST-&#x2F;p&#x2F;book&#39;]func newRouter() *router &#123;    return &amp;router &#123;        roots: make(map[string]*node),        handlers: make(map[string]HandlerFunc),    &#125;&#125;&#x2F;&#x2F; Only one * is allowedfunc parsePattern(pattern string) []string &#123;    vs :&#x3D; strings.Split(pattern, &quot;&#x2F;&quot;)    &#x2F;&#x2F; 一层层获取并判断是否为有效节点字串    parts :&#x3D; make([]string, 0)    for _, item :&#x3D; range vs &#123;        if item !&#x3D; &quot;&quot; &#123;            parts &#x3D; append(parts, item)            if item[0] &#x3D;&#x3D; &#39;*&#39; &#123;                break                &#x2F;&#x2F; 这里只要当前节点为*,则结束后面的遍历,以*为当前分支尾节点            &#125;        &#125;    &#125;    return parts&#125;func (r *router) addRoute(method string, path string) (*node, map[string]string) &#123;    searchParts :&#x3D; parsePattern(pattern)        key :&#x3D; method + &quot;-&quot; + pattern    _, ok :&#x3D; r.roots[method]&#x2F;&#x2F; 这里卡的比较久,第一个返回值是获取的值,第二个是判断值是否获取成功    if !ok &#123;        r.roots[method] &#x3D; &amp;node&#123;&#125;        &#x2F;&#x2F; 一般来说,对于一个新路由,node默认是空,    &#125;    r.roots[method].insert(pattern, parts, 0)    r.handlers[key] &#x3D; handler&#125;func (r *router) getRoute(method string, path string) (*node, map[string]string) &#123;    searchParts :&#x3D; parsePattern(path)    params :&#x3D; make(map[string]string)    root, ok :&#x3D; r.roots[method]        if !ok &#123;        return nil, nil    &#125;        n :&#x3D; roots.search(searchParts, 0)        if n !&#x3D; nil &#123;        parts :&#x3D; parsePattern(n.pattern)        for index, part :&#x3D; range parts &#123;            &#x2F;&#x2F; getRoute的参数匹配,这里匹配:和*两种字符            if part[0] &#x3D;&#x3D; &#39;:&#39; &#123;                params[part[1:]] &#x3D; searchParts[index]            &#125;            if part[0] &#x3D;&#x3D; &#39;*&#39; &amp;&amp; len(part) &gt;1 &#123;                params[part[1:]] &#x3D; strings.Join(searchParts[index:], &quot;&#x2F;&quot;)                break            &#125;        &#125;        return n, params    &#125;    return nil, nil&#125;func (r *router) getRoutes(method string) []*node &#123;    root, ok :&#x3D; r.roots[method]    if !ok &#123;        return nil    &#125;    nodes :&#x3D; make([]*node, 0)    root.travel(&amp;nodes)    return nodes&#125;</code></pre><h4 id="context与handle的变化">Context与handle的变化</h4><p>在HandleFunc中，希望能够访问到解析的参数，因此，需要对Context对象增加一个属性和方法，来提供对路由参数的访问。我们将解析后的参数储存到<code>Params</code>中，通过<code>c.Param("lang")</code>的方式获取到对应的值。</p><blockquote><p>day3/gee/context.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Context struct &#123;    &#x2F;&#x2F; origin objects    Writer http.ResponseWriter    Req *http.Request    &#x2F;&#x2F; request info    Path string    Method string    Params map[string]string    &#x2F;&#x2F; response info    StatusCode int&#125;func (c *Context) Param(key string) string &#123;    value :&#x3D; c.Params[key]     return value&#125;</code></pre><blockquote><p>day3/gee/router.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (r *router) handle(c *Context) &#123;    n, params :&#x3D; r.getRoute(c.Method, c.Path)    if n !&#x3D; nil &#123;        c.Params &#x3D; params        key :&#x3D; c.Method + &quot;-&quot; +n.pattern        r.handlers[key](c)    &#125; else &#123;        c.String(http.StatusNotFound, &quot;404 NOT FOUND: %s\n&quot;, c.Path)    &#125;&#125;</code></pre><p><code>router.go</code>的变化比较小，比较重要的一点是，在调用匹配到的<code>handler</code>前，将解析出来的路由参数赋值给了<code>c.Params</code>。这样就能够在<code>handler</code>中，通过<code>Context</code>对象访问到具体的值了</p><h4 id="单元测试">单元测试</h4><blockquote><p>router_test.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package geeimport (    &quot;fmt&quot;    &quot;reflect&quot;    &quot;testing&quot;)func newTestRouter() *router &#123;    r :&#x3D; newRouter()    r.addRoute(&quot;GET&quot;, &quot;&#x2F;&quot;, nil)    r.addRoute(&quot;GET&quot;, &quot;&#x2F;hello&#x2F;:name&quot;, nil)    r.addRoute(&quot;GET&quot;, &quot;&#x2F;hello&#x2F;b&#x2F;c&quot;, nil)    r.addRoute(&quot;GET&quot;, &quot;&#x2F;hi&#x2F;:name&quot;, nil)    r.addRoute(&quot;GET&quot;, &quot;&#x2F;assets&#x2F;*filepath&quot;, nil)    return r&#125;func TestParsePattern (t *testing.T) &#123;    r :&#x3D; newTestRouter()    ok :&#x3D; reflect.DeepEqual(parsePattern(&quot;&#x2F;p&#x2F;:name&quot;), []string&#123;&quot;p&quot;, &quot;:name&quot;&#125;)    ok &#x3D; ok &amp;&amp; reflect.DeepEqual(parsePattern(&quot;&#x2F;p&#x2F;*&quot;), []sring&#123;&quot;p&quot;, &quot;*&quot;&#125;)    ok &#x3D; ok &amp;&amp; reflect.DeepEqual(parsePattern(&quot;&#x2F;p&#x2F;*name&#x2F;*&quot;), []string&#123;&quot;p&quot;, &quot;*name&quot;&#125;)    if !ok &#123;        t.Fatal(&quot;test parsePattern failed&quot;)    &#125;    &#125;func TestGetRoute(t *testing.T) &#123;    r :&#x3D; newTestRouter()    n, ps :&#x3D; r.getRoute(&quot;GET&quot;, &quot;&#x2F;hello&#x2F;gee&quot;)        if n &#x3D;&#x3D; nil &#123;        t.Fatal(&quot;nil shouldn&#39;t be returned&quot;)    &#125;        if n.pattern !&#x3D; &quot;&#x2F;hello&#x2F;:name&quot; &#123;        t.Fatal(&quot;should match &#x2F;hello&#x2F;:name&quot;)    &#125;        if ps[&quot;name&quot;] !&#x3D; &quot;gee&quot; &#123;        t.Fatal(&quot;should match be equal to gee&quot;)    &#125;        fmt.Printf(&quot;matched path: %s, params[&#39;name&#39;]: %s\n&quot;, n.pattern, ps[&quot;name&quot;])&#125;func TestGetRoute2(t *testing.T) &#123;    r :&#x3D; newTestRouter()    n1, ps1 :&#x3D; r.getRoute(&quot;GET&quot;, &quot;&#x2F;assets&#x2F;file1.txt&quot;)    ok1 :&#x3D; n1.pattern &#x3D;&#x3D; &quot;&#x2F;assets&#x2F;*filepath&quot; &amp;&amp; ps1[&quot;filepath&quot;] &#x3D;&#x3D; &quot;file1.txt&quot;    if !ok1 &#123;        t.Fatal(&quot;pattern should be &#x2F;assets&#x2F;*filepath &amp; filepath should be file1.txt&quot;)    &#125;        n2, ps2 :&#x3D; r.getRoute(&quot;GET&quot;, &quot;&#x2F;assets&#x2F;css&#x2F;test.css&quot;)    ok2 :&#x3D; n2.pattern &#x3D;&#x3D; &quot;&#x2F;assets&#x2F;*filepath&quot; &amp;&amp; ps2[&quot;filepath&quot;] &#x3D;&#x3D; &quot;css&#x2F;test.css&quot;    if !ok2 &#123;        t.Fatal(&quot;pattern should be &#x2F;assets&#x2F;*filepath &amp;filepath should be css&#x2F;test.css&quot;)    &#125;&#125;func TestGetRoutes(t *testing.T) &#123;    r :&#x3D; newTestRouter()    nodes :&#x3D; r.getRoutes(&quot;GET&quot;)    for i, n :&#x3D; range nodes &#123;        fmt.Println(i+1, n)    &#125;        if len(nodes) !&#x3D; 5 &#123;        t.Fatal(&quot;the number of routes should be 4&quot;)    &#125;&#125;</code></pre><h4 id="使用demo">使用DEMO</h4><blockquote><p>day3/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (&quot;gee&quot;    &quot;net&#x2F;http&quot;)func main() &#123;    r :&#x3D; gee.New()    r.GET(&quot;&#x2F;&quot;, func(c *gee.Context) &#123;        c.HTML(http.StatusOK, &quot;&lt;h1&gt;Hello gee&lt;&#x2F;h1&gt;&quot;)    &#125;)        r.GET(&quot;&#x2F;hello&quot;, func(c *gee.Context) &#123;        &#x2F;&#x2F; expect &#x2F;hello?name&#x3D;xxx        c.String(http.StatucOK, &quot;hello %s, you&#39;re at %s\n&quot;, c.Query(&quot;name&quot;), c.Path)    &#125;)        r.GET(&quot;&#x2F;assets&#x2F;*filepath&quot;, func(c *gee.Context) &#123;        c.JSON(http.StatusOK, gee.H&#123;&quot;filepath&quot;: c.Param(&quot;filepath&quot;)&#125;)    &#125;)        r.Run(&quot;:9999&quot;)&#125;</code></pre><p>使用curl测试</p><pre class="line-numbers language-bat" data-language="bat"><code class="language-bat">$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;hello&#x2F;abchello abd, you&#39;re at &#x2F;hello&#x2F;abc$ curl &quot;http:&#x2F;&#x2F;localhost:9999&#x2F;assets&#x2F;css&#x2F;abc.css&quot;&#123;&quot;filepath&quot;:&quot;css&#x2F;abc.css&quot;&#125;</code></pre><h4 id="day3小结">day3小结</h4><p>之前因为一项暑期实践活动的工作 , 我没有很认真地过一遍 ,也只是草草地敲一遍代码 , 简单地挖几个函数的源码来看 ,后面因为实训项目就先搁置了这个gee框架的学习 , 等做完实训项目后 ,花了半天把代码敲了一遍 , 也运行了一遍 ,但还是觉得心里心里很没底，于是想着，从第一天的内容开始认真看一遍，然后就开始对着前两天的源码一顿操作，开始不断查看源码中函数引用的内容，在关键函数print相关变量，前两天内容并不算很难，到了第三天，可能是跳跃性太大，加上前两天基础不牢，我在这里卡了4天，加上这几天状态不太好，就学的比较慢，这几天意识到事情的严重性，稍微加快了脚步，对第三天的路由部分进行了更多的测试，也对这个路由部分有了更深的认识</p><h3 id="day4.-分组控制group">day4. 分组控制Group</h3><ul><li>本次实现路由分组控制(Route Group Control)，代码约50行。</li></ul><h4 id="分组的意义">分组的意义</h4><p>分组控制(Route Group Control)是Web框架应提供的基础功能之一。所谓分组，是指的路由的分组。如果没有路由分组，我们需要针对每一个路由进行控制。但是真实的业务场景中，往往某一组路由需要相似的处理。例如：</p><ul><li>以<code>/post</code>开头的路由匿名可访问</li><li>以<code>/admin</code>开头的路由需要鉴权</li><li>以<code>/api</code>开头的路由时RESTful接口 , 可以对接第三方平台 ,需要三方平台鉴权</li></ul><p>大部分情况下的路由分组 ,是以相同的前缀来区分的。因此，我们今天实现的分组控制也是以前缀来区分，并支持分组的嵌套。例如<code>/post</code>是一个分组，<code>/post/a</code>和<code>/post/b</code>可以是该分组下的子分组。作用在<code>/post</code>分组上中间件(middleware)，也都会作用在子分组，子分组还可以应用自己特有的中间件。</p><p>中间件可以给框架提供无限的扩展能力，应用在分组上，可以使得分组控制的收益更为明显，而不是共享相同的路由前缀这么简单。例如<code>/admin</code>的分组，可以应用鉴权中间件；<code>/</code>分组应用日志中间件，<code>/</code>是默认的最顶层的分组，也就意味着给所有的路由，即整个框架增加了日志的能力。</p><p><strong>分组嵌套</strong></p><p>一个Group对象需要具备哪些属性呢？首先是前缀(prefix)，比如<code>/</code>，或者<code>/api</code>；要支持分组嵌套，那么需要知道当前分组的父亲(parent)是谁；当然了，按照我们一开始的分析，中间件是应用在分组上的，那还需要储存应用在该分组上的中间件(middlewares)。还记得，我们之前调用函数<code>*(Engine).addRoute()</code>来映射所有的路由规则和Handler。如果Group对象需要直接映射路由规则的画，比如我们想在使用框架时，这么调用</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">r :&#x3D; gee.New()v1 :&#x3D; r.Group(&quot;&#x2F;v1&quot;)v1.GET(&quot;&#x2F;&quot;, func(c *gee.Context) &#123;    c.HTML(http.StatusK, &quot;&lt;h1&gt;hello gee&lt;&#x2F;h1&gt;&quot;)&#125;)</code></pre><p>那么Group对象，还需要有访问<code>Router</code>的能力，为了方便，我们可以在Group中，保存一个指针，指向<code>Engine</code>，整个框架的所有资源都是由<code>Engine</code>统一协调的，那么就可以通过<code>Engine</code>间接地访问各种接口了。</p><p>所以，最后的Group做出以下改动：</p><blockquote><p>day4/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type HandlerFunc func(*Context)type (    RouterGroup struct &#123;        prefix string        middlewares []HandlerFunc        parent *RouterGroup        engine *Engine    &#125;    &#x2F;&#x2F; 进一步抽象，将Engine作为最顶层的分组，也就是说Engine拥有RouterGroup的所有能力    Engine struct &#123;        *RouterGroup        router *router        groups []*RouterGroup    &#125;)&#x2F;&#x2F; 下面是实现和路由有关的函数func New() *Engine &#123;    engine :&#x3D; &amp;Engine&#123;router: newRouter()&#125;    engine.RouterGroup &#x3D; &amp;RouterGroup&#123;engine: engine&#125;    engine.groups &#x3D; []*RouterGroup&#123;engine.RouterGroup&#125;    return engine&#125;func (group *RouterGroup) Group(prefix string) *RouterGroup &#123;    engine :&#x3D; group.engine    newGroup :&#x3D; &amp;RouterGroup &#123;        prefix: group.prefix + prefix,        &#x2F;&#x2F; parent: group,        engine: engine,        &#x2F;&#x2F; 查阅评论区后，作者用gruop.prefix+prefix的方式初始化已经拼接了完整的prefix，不需要parent，于是可以删除    &#125;    engine.groups &#x3D; append(engine.groups, newGroup)    return newGroup&#125;func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) &#123;    pattern :&#x3D; group.prefix + comp    log.Printf(&quot;Route %4s - %s&quot;, method, pattern)    group.engine.router.addRoute(method, pattern, handler)&#125;func (group *RouterGroup) GET(pattern string, handler HandlerFunc) &#123;    group.addRoute(&quot;GET&quot;, pattern, handler)&#125;func (group *RouteGroup) POST(pattern string, handler HandlerFunc) &#123;    gourp.addRoute(&quot;POST&quot;, pattern, handler)&#125;</code></pre><p>在这里可以观察到<code>addRoute</code>函数，调用了<code>group.engine.router.addRoute</code>来实现了路由的映射。由于<code>Engine</code>从某种意义上继承了<code>RouterGroup</code>的所有属性和方法，因为<code>(*Engine).engine</code>是指向自己的。这样实现，我们既可以像原来一样添加路由，也可以通过分组添加路由。</p><h4 id="使用demo-1">使用Demo</h4><blockquote><p>day4/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func main() &#123;    r :&#x3D; gee.New()    r.GET(&quot;&#x2F;index&quot;, func(c *gee.Context) &#123;        c.HTML(http.StatusOK, &quot;&lt;h1&gt;Index Page&lt;&#x2F;h1&gt;&quot;)    &#125;)    v1 :&#x3D; r.Group(&quot;&#x2F;v1&quot;)    &#123;        v1.GET(&quot;&#x2F;&quot;, func(c *gee.Context) &#123;            c.HTML(http.StatusOK, &quot;&lt;h1&gt;hello gee&lt;&#x2F;h1&gt;&quot;)        &#125;)                v1.GET(&quot;&#x2F;hello&quot;, func(c *gee.Context) &#123;            &#x2F;&#x2F; expect &#x2F;hello?name&#x3D;abc            c.String(http.StatusOK, &quot;hello %s, you&#39;re at %s\n&quot;, c.Query(&quot;name&quot;), c.Path)        &#125;)    &#125;    v2 :&#x3D; r.Group(&quot;&#x2F;v2&quot;)    &#123;        v2.GET(&quot;&#x2F;hello&#x2F;:name&quot;, func(c *gee.Context) &#123;            &#x2F;&#x2F; expect &#x2F;hello&#x2F;abc            c.String(http.StatusOK, &quot;hello %s, you&#39;re at %s\n&quot;, c.Param(&quot;name&quot;), c.Path)        &#125;)        v2.POST(&quot;&#x2F;login&quot;, func(c *gee.Context) &#123;            c.JSON(http.StatusOK, gee.H&#123;                &quot;username&quot;: c.PostForm(&quot;username&quot;),                &quot;password&quot;: c.PostForm(&quot;password&quot;),            &#125;)        &#125;)    &#125;        r.Run(&quot;:9999&quot;)&#125;</code></pre><p>通过curl的简单测试：</p><pre class="line-numbers language-bat" data-language="bat"><code class="language-bat">$ curl &quot;http:&#x2F;&#x2F;localhost:9999&#x2F;v1&#x2F;hello?name&#x3D;abc&quot;hello abc, you&#39;re at &#x2F;v1&#x2F;abc$ curl &quot;http:&#x2F;&#x2F;localhost:9999&#x2F;v2&#x2F;hello&#x2F;abc&quot;hello abc, you&#39;re at &#x2F;hello&#x2F;abc</code></pre><h3 id="day5.-中间件-middleware">day5. 中间件 Middleware</h3><ul><li>设计并实现Web 框架的中间件 (Middlewares)机制</li><li>实现通用的 Logger中间件，能够记录请求到响应所花费的时间，代码约50行</li></ul><h4 id="中间件是什么">中间件是什么</h4><p>中间件(middlewares)，简单说，就是非业务的技术类组件。Web框架本身不可能去理解所有的业务，因而不可能实现所有的功能。因此，框架需要有一个插口，允许用户自定义功能，嵌入到框架中，仿佛这个功能是框架原生支持的一样。因此，对中间件而言，需要考虑2个比较关键的点：</p><ul><li>插入点在哪？使用框架的人并不关心底层逻辑的具体实现，如果插入点太底层，中间件逻辑就会非常复杂</li><li>中间件的输入是什么？中间件的输入，决定了扩展能力。暴露的参数太少，用户发挥空间有限。</li></ul><p>那对于一个Web框架而言，中间件应该设计成什么样呢？接下来的实现，基本参考了Gin框架。</p><h4 id="中间件设计">中间件设计</h4><p>Gee的中间件的定义与路由映射的Handler一致，处理的是输入的<code>Context</code>对象。插入点是框架接收到请求初始化<code>Context</code>对象后，允许用户使用自己定义的中间件做一些额外的处理，例如记录日志等，以及对<code>Context</code>进行二次加工。另外通过调用<code>(*Context).Next()</code>函数，中间件可等待用户自己定义的<code>Handler</code>处理结束后，做一些额外的操作，例如计算本次处理所用时间等。即Gee的中间件支持用户在请求被处理的前后，做一些额外的操作。举个例子，我们希望最终能够支持如下定义的中间件，<code>c.Next()</code>表示等待执行其他的中间件或用户的<code>Handler</code>：</p><blockquote><p>day4/gee/logger.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Logger() HandlerFunc &#123;    return func(c *Context) &#123;        &#x2F;&#x2F; start timer        t :&#x3D; time.Now()        &#x2F;&#x2F; process request        c.Next()        &#x2F;&#x2F; calculate resolution time        log.Printf(&quot;[%d] %s in %v&quot;, c.StatusCode, c.Req.RequestURI, time.Since(t))    &#125;&#125;</code></pre><p>另外，支持设置多个中间件，依次进行调用。</p><p>在第四天的 "分组控制 GroupControl"讲到，中间件是应用在<code>RouterGroup</code>上的，应用在最顶层的Group，相当于作用域全局，所有的请求都会被中间件处理。那为什么不作用在每一条路由规则上呢？作用在某条路由规则，那还不如用户直接在Handler中调用。只作用在某条路由规则的功能通透性太差，不适合定义为中间件。</p><p>我们之前的框架设计是这样的，当接收到请求后，匹配路由，该请求的所有信息都保存在<code>Context</code>中。中间件也不例外，接收到请求后，应查找所有应作用于该路由的中间件，保存在<code>Context</code>中，依次进行调用。为什么依次调用后，还需要在<code>Context</code>中保存呢？因为在设计中，中间件不仅作用在处理流程前，也可以作用在处理流程后，即在用户定义的Handler处理完毕后，还可以执行剩下的操作。</p><blockquote><p>day4/gee/context.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Context struct &#123;    &#x2F;&#x2F; origin objects    Writer http.ResponseWriter    Req *http.Request    &#x2F;&#x2F; request info    Path string    Method string    Params map[string]string    &#x2F;&#x2F; response info    StatusCode int    &#x2F;&#x2F; middleware    handlers []HandlerFunc    index int&#125;func newContext(w http.RequestWriter, req *http.Request) *Context &#123;    return &amp;Context &#123;        Path: req.URL.Path,        Method: req.Method,        Req: req,        Writer: w,        index: -1,    &#125;&#125;func (c *Context) Next() &#123;    c.index++    s :&#x3D; len(c.handlers)    for ; c.index &lt; s; c.index++ &#123;        c.handlers[c.index](c)    &#125;&#125;</code></pre><p><code>index</code>是记录当前执行到第几个中间件，当在中间件调用<code>Next</code>方法时，控制权交给了下一个中间件，直到调用到最后一个中间件，然后再从后往前，调用每个中间件在<code>Next</code>方法之后定义的部分。如果我们将用户在映射路由时定义的<code>Handler</code>添加到<code>c.handlers</code>列表中，结果会怎么样呢？</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">func A(c *Context) &#123;    part1    c.Next()    part2&#125;func B(c *Context) &#123;    part3    c.Next()    part4&#125;</code></pre><p>假设我们应用了中间件A和B，和路由映射的Handler。<code>c.handlers</code>是这样的<code>[A, B, Handler]</code>，<code>c.index</code>初始化为-1。调用<code>c.Next()</code>，接下来的流程是这样的：</p><ul><li>c.index++ , c.index=0</li><li>0 &lt; 3 , 调用<code>c.handlers[0]</code>，即A</li><li>执行part1，调用<code>c.Next()</code></li><li>c.index++，c.index=1</li><li>1 &lt; 3 , 调用<code>c.handlers[1]</code>，即B</li><li>执行part3，调用<code>c.Next()</code></li><li>c.index++ , c.index=2</li><li>2 &lt; 3 , 调用<code>c.handlers[2]</code>，即Handler</li><li>Handler调用完毕，返回到B中的part4，执行part4</li><li>part4执行完毕，返回到A中的part2，执行part2</li><li>part2执行完毕，结束</li></ul><p>说重点，执行顺序是<code>part1 -&gt; part3 -&gt; Handler -&gt; part4 -&gt;part2</code>。恰恰满足了我们对中间件的要求，接下来看调用部分的代码，就能全部串起来了。</p><h4 id="代码实现">代码实现</h4><p>定义<code>Use</code>函数，将中间件应用到某个Group</p><blockquote><p>day4/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; Use is defined to add middlewares to the groupfunc (group *RouterGroup) Use(middlewares ...HandlerFunc) &#123;    group.middlewares &#x3D; append(group.middlewares, middlewares...)&#125;func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) &#123;    var middlewares []HandlerFunc    for _, group :&#x3D; range engine.groups &#123;        if strings.HasPrefix(req.URL.Path, group.prefix) &#123;            middlewares &#x3D; append(middlewares, group.middlewares...)        &#125;    &#125;    c :&#x3D; newContext(w, req)    c.handlers &#x3D; middlewares    engine.router.handle(c)&#125;</code></pre><p>ServeHTTP函数也有变化，当我们接收到一个具体请求时，要判断该请求适用于哪些中间件，在这里我们简单通过URL的前缀来判断。得到中间件列表，赋值给<code>c.handlers</code>。</p><p>handle函数中，将从路由匹配得到的Handler添加到<code>c.handlers</code>列表中，执行<code>c.Next()</code>。</p><blockquote><p>day4/gee/router.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (r *router) handle(c *Context) &#123;    n, params :&#x3D; r.getRoute(c.Method, c.Path)        if n !&#x3D; nil &#123;        key :&#x3D; c.Method + &quot;-&quot; +n.pattern        c.Params &#x3D; params        c.handlers &#x3D; append(c.handlers, r.handlers[key])    &#125; else &#123;        c.handlers &#x3D; append(c.handlers, func(c *Context) &#123;            c.String(http.StatusNotFound, &quot;404 NOT FOUND: %s\n&quot;, c.Path)        &#125;)    &#125;    c.Next()&#125;</code></pre><h4 id="使用demo-2">使用demo</h4><pre class="line-numbers language-go" data-language="go"><code class="language-go">func onlyForV2() gee.HandlerFunc &#123;    return func(c *gee.Context) &#123;        &#x2F;&#x2F; start timer        t :&#x3D; time.Now()        &#x2F;&#x2F; if a server error occurred        c.Fail(500, &quot;Internal Server Error&quot;)        &#x2F;&#x2F; Calculate resolution time        log.Printf(&quot;[%d] %s in %v for group v2&quot;, c.StatusCode, c.Req.RequestURI, time.Since(t))    &#125;&#125;func main() &#123;    r :&#x3D; gee.New()    r.Use(gee.Logger()) &#x2F;&#x2F; global middleware    r.GET(&quot;&#x2F;&quot;, func(c *gee.Context) &#123;        c.HTML(http.StatusOK, &quot;&lt;h1&gt;hello gee&lt;&#x2F;h1&gt;&quot;)    &#125;)        v2 :&#x3D; r.Group(&quot;&#x2F;v2&quot;)    v2.Use(onlyForV2())    &#123;        v2.GET(&quot;&#x2F;hello&#x2F;:name&quot;, func(c *gee.Context) &#123;            &#x2F;&#x2F; expect &#x2F;hello&#x2F;gee            c.String(http.StatisOK, &quot;hello %s, you&#39;re at %s\n&quot;, c.Param(&quot;name&quot;), c.Path)        &#125;)    &#125;        c.Run(&quot;:9999&quot;)&#125;</code></pre><p><code>gee.Logger()</code>即我们一开始就介绍的中间件，我们将这个中间件和框架代码放在了一起，作为框架默认提供的中间件。在这个例子中，我们将<code>gee.Logger()</code>应用在了全局，所有的路由都会应用该中间件。<code>onlyForV2()</code>是用来测试功能的，尽在<code>v2</code>对应的Group中应用了。</p><p>接下来使用curl测试，可以看到，v2 Group 2个中间件都生效了。</p><pre class="line-numbers language-bat" data-language="bat"><code class="language-bat">$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;&lt;h1&gt;Hello Gee&lt;&#x2F;h1&gt;$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;v2&#x2F;hello&#x2F;abc&#123;&quot;message&quot;:&quot;Internal Server Error&quot;&#125;</code></pre><p>服务器端</p><pre class="line-numbers language-none"><code class="language-none">2022&#x2F;07&#x2F;27 16:00:01 [200] &#x2F; in 300ns2022&#x2F;07&#x2F;27 16:00:28 [500] &#x2F;v2&#x2F;hello&#x2F;abc in 0s for group v22022&#x2F;07&#x2F;27 16:00:28 [500] &#x2F;v2&#x2F;hello&#x2F;abc in 1.6176ms</code></pre><p>这里的测试v2中间件，一开始，测试了很多次，返回的都是500错误码，比对了源码很久，没发现问题，再次运行curl测试，还是返回500错误码。后面查阅了第五天的评论区，发现，day5的中间件仅仅用来演示，发送500错误码表示中间件起作用了。</p><h3 id="day6.-模板-html-template">day6. 模板 (HTML Template)</h3><ul><li>实现静态资源服务 (Static Resource)</li><li>支持HTML模板渲染</li></ul><h4 id="服务器渲染">服务器渲染</h4><p>现在越来越流行前后端分离的开发模式，即 Web后端提供RESTful接口，返回结构化的数据 (通常为JSON或XML)。前端使用AJAX技术请求到所需的数据，利用 JavaScript 进行渲染。Vue/React等前端框架持续火热，这种开发模式前后端解耦，优势很突出。后端打工人专心解决资源利用，并发，数据库等问题，只需要考虑数据如何生成；前端打工人专注于界面设计实现，只需要考虑拿到数据后如何渲染即可。后端只关注于数据，接口返回值是结构化的，于前端解耦。同一套后端服务能够同时支撑小程序，移动app，pc端Web界面，以及对外提供的接口。随着前端工程化的不断发展，Webpack，gulp等工具层出不穷，前端技术越来越自成体系了。</p><p>但是前后端分离的一大问题在于，页面是在客户端渲染的，比如浏览器，这对爬虫并不友好。</p><p>今天的内容便是介绍 Web框架如何支持服务端渲染的场景。</p><h4 id="静态文件-serve-static-files">静态文件 (Serve Static Files)</h4><p>网页三剑客，js，css，html。要做到服务端渲染，第一步便是要支持js，css等静态文件。之前设计动态路由的时候，支持通配符<code>*</code>匹配多级子路径。比如路由规则<code>/assets/*filepath</code>，可以匹配<code>/assets/</code>开头的所有地址。例如<code>/assets/js/geek.js</code>，匹配后，参数<code>filepath</code>旧赋值为<code>js/geek.js</code>。</p><p>那么如果我们将所有静态文件放在<code>/usr/web</code>目录下，那么<code>filepath</code>的值既是该目录下文件的相对地址。映射到真实的文件后，将文件返回，静态服务器就实现了。</p><p>找到文件后，如何返回这一文件，<code>net/http</code>库已经实现了。因此，gee框架要做的，仅仅是解析请求的地址，映射到服务器上文件的真实地址，交给<code>http.FileServer</code>处理就好了。</p><blockquote><p>day6/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; create static handlerfunc (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc &#123;    absolutePath :&#x3D; path.Join(group.prefix, relativePath)    fileServer :&#x3D; http.StripPrefix(absolutePath, http.FileServer(fs))    return func(c *Context) &#123;        file :&#x3D; c.Param(&quot;filepath&quot;)        &#x2F;&#x2F; check if file exists and&#x2F;or if we have permission to access it        if _, err :&#x3D; fs.Open(file); err !&#x3D; nil &#123;            c.Status(http.StatusNotFound)            return        &#125;                fileServer.ServeHTTP(c.Writer, c.Req)    &#125;&#125;&#x2F;&#x2F; serve static filesfunc (group *RouterGroup) Static(relativePath string, root string) &#123;    handler :&#x3D; group.createStaticHandler(relativePath, http.Dir(root))    urlPattern :&#x3D; path.Join(relativePath, &quot;&#x2F;*filepath&quot;)    &#x2F;&#x2F; Register GET handlers    group.GET(urlPattern, handler)&#125;</code></pre><p>我们给<code>RouterGroup</code>添加了两个方法，<code>Static</code>这个方法是暴露给用户的。用户可以将磁盘上的某个文件夹<code>root</code>映射到路由<code>relativePath</code>。例如：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">r :&#x3D; gee.New()r.Static(&quot;&#x2F;assets&quot;, &quot;&#x2F;usr&#x2F;Jayden&#x2F;blog&#x2F;static&quot;)&#x2F;&#x2F; 或者相对路径 r.Static(&quot;&#x2F;assets&quot;, &quot;.&#x2F;static&quot;)r.Run(&quot;:9999&quot;)</code></pre><p>用户访问<code>localhost:9999/assets/js/geek.js</code></p><p>最终返回<code>/usr/geek/blog/static/js/geek.js</code>。</p><h4 id="html-模板渲染">HTML 模板渲染</h4><p>golang内置了<code>text/template</code>和<code>html/template</code>2个模板标准库，其中<a href='https://golang.org/pkg/html/template'>html/template</a> 为HTML提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等。gee框架的模板渲染直接使用了<code>html/template</code>提供的能力。</p><blockquote><p>day6/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">Engine struct &#123;    *RouterGroup    router *router    groups []*RouterGroup &#x2F;&#x2F; store all groups    htmlTemplate *template.Template &#x2F;&#x2F; for html render    funcMap template.FuncMap &#x2F;&#x2F; for html render&#125;func (engine *Engine) SetFuncMap(funcMap template.FuncMap) &#123;    engine.funcMap &#x3D; funcMap&#125;func (engine *Engine) LoadHTMLGlob(pattern string) &#123;    engine.htmlTemplates &#x3D; template.Must(template.New(&quot;&quot;).Funcs(engine.funcMap).ParseGlob(pattern))&#125;</code></pre><p>首先为 Engine实例添加了<code>*template.Template</code>和<code>template.FuncMap</code>对象，前者将所有的模板加载进内存，后者是所有的自定义模板渲染函数。</p><p>另外，给用户分别提供了设置自定义渲染函数<code>funcMap</code>和加载模板的方法。</p><p>接下来，对原来的<code>(*Context).HTML()</code>方法做了些小修改，使之支持根据模板文件名选择模板进行渲染。</p><blockquote><p>day6/gee/context.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Context struct &#123;    &#x2F;&#x2F; ..    &#x2F;&#x2F; engine pointer    engine *Engine&#125;func (c *Context) HTML(code int, name string, data interface&#123;&#125;) &#123;    c.SetHeader(&quot;Content-Type&quot;, &quot;text&#x2F;html&quot;)    c.Status(code)    if err :&#x3D; c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err !&#x3D; nil &#123;        c.Fail(500, err.Error())    &#125;&#125;</code></pre><p>我们在<code>Context</code>中添加了成员变量<code>engine *Engine</code>，这样就能够通过Context访问Engine中的HTML模板。实例化Context时，还需要给<code>c.engine</code>赋值。</p><blockquote><p>day6/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) &#123;    &#x2F;&#x2F; ...    c :&#x3D; newContext(w, req)    c.handlers &#x3D; middlewares    c.engine &#x3D; engine    engine.router.handle(c)&#125;</code></pre><h4 id="使用demo-3">使用Demo</h4><p>最终目录结构</p><pre class="line-numbers language-none"><code class="language-none">---gee&#x2F;---static&#x2F;   |---css&#x2F;       |---geek.css   |---file1.txt---template   |---arr.tmpl   |---css.tmpl   |---custom_func.tmpl---main.go</code></pre><blockquote><p>day6/templates/arr.tmpl</p></blockquote><pre class="line-numbers language-php" data-language="php"><code class="language-php">&lt;html&gt;    &lt;body&gt;        &lt;p&gt;hello, &#123;&#123;.title&#125;&#125;&lt;&#x2F;p&gt;        &#123;&#123;range $index, $ele :&#x3D;.stuArr&#125;&#125;        &lt;p&gt;&#123;&#123; $index&#125;&#125;: &#123;&#123;$ele.Name&#125;&#125; is &#123;&#123; $ele.Age&#125;&#125; years old&lt;&#x2F;p&gt;        &#123;&#123;end&#125;&#125;    &lt;&#x2F;body&gt;&lt;&#x2F;html&gt;</code></pre><blockquote><p>day6/template/css.tmpl</p></blockquote><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup">&lt;html&gt;    &lt;link rel&#x3D;&quot;stylesheet&quot; href&#x3D;&quot;&#x2F;assets&#x2F;css&#x2F;geek.css&quot;&gt;    &lt;p&gt;geek.css is loaded&lt;&#x2F;p&gt;&lt;&#x2F;html&gt;</code></pre><blockquote><p>day6/template/custom_func.tmpl</p></blockquote><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup">&lt;html&gt;    &lt;body&gt;        &lt;p&gt;hello, &#123;&#123;.title&#125;&#125;&lt;&#x2F;p&gt;        &lt;p&gt;Date: &#123;&#123;.now | FormatDate&#125;&#125;&lt;&#x2F;p&gt;    &lt;&#x2F;body&gt;&lt;&#x2F;html&gt;</code></pre><blockquote><p>day6/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">type student struct &#123;    Name string    Age int8&#125;func FormatAsDate(t time.Time) string &#123;    year, month, day :&#x3D; t.Date()    return fmt.Sprintf(&quot;%d-%02d-%02d&quot;, year, month, day)&#125;func main() &#123;    r :&#x3D; gee.New()    r.Use(gee.Logger())    r.SetFuncMap(template.FuncMap&#123;        &quot;FormatAsDate&quot;: FormatAsDate,    &#125;)    r.LoadHTMLGlob(&quot;templates&#x2F;*&quot;)    r.Static(&quot;&#x2F;assets&quot;, &quot;.&#x2F;static&quot;)        stu1 :&#x3D; &amp;student&#123;Name: &quot;gee&quot;, Age: 20&#125;    stu2 :&#x3D; &amp;student&#123;Name: &quot;Jay&quot;, Age: 22&#125;    r.GET(&quot;&#x2F;&quot;, func(c *gee.Context) &#123;        c.HTML(http.StatusOK, &quot;css.tmpl&quot;, nil)    &#125;)    r.GET(&quot;&#x2F;students&quot;, func(c *gee.Context) &#123;        c.HTML(http.StatusOK), &quot;arr.tmpl&quot;, gee.H&#123;            &quot;title&quot;: &quot;gee&quot;,            &quot;stuArr&quot;: [2]*student&#123;stu1, stu2&#125;,        &#125;)    &#125;)        r.GET(&quot;&#x2F;students&quot;, func(c *gee.Context) &#123;        c.HTML(http.StatusOK, &quot;custom_func.tmpl&quot;, gee.H&#123;            &quot;title&quot;: &quot;gee&quot;,            &quot;now&quot;: time.Date(2019,8,17,0,0,0,0,time.UTC)        &#125;)    &#125;)        r.Run(&quot;:9999&quot;)&#125;</code></pre><p>在浏览器访问主页，模板正常渲染，css静态文件加载成功</p><p><img src='0x002F/gee_day6.png'></p><h3 id="day7.-错误恢复-panic-recover">day7. 错误恢复 (PanicRecover)</h3><blockquote><p>实现错误处理机制</p></blockquote><h4 id="panic">panic</h4><p>golang中，比较常见的错误处理方法是返回error，由调用者决定后续如何处理。但是如果是无法恢复的错误，可以手动触发panic，当然如果在程序运行过程中出现了类似于数组越界的错误，panic也会被触发。panic会中止当前执行的程序，退出。</p><p>下面是主动触发的例子：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package main&#x2F;&#x2F; hello.gofunc main() &#123;    fmt.Println(&quot;before panic&quot;)    panic(&quot;crash&quot;)    fmt.Println(&quot;after panic&quot;)&#125;</code></pre><pre class="line-numbers language-none"><code class="language-none">$ go run hello.gobefore panic panic: crashgoroutine 1 [running]:main.main()        ~&#x2F;*your path*&#x2F;hello.go:5 +0x95exit status 2</code></pre><p>下面是数组越界触发的panic</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package main&#x2F;&#x2F; hello.gofunc main() &#123;    arr :&#x3D; []int&#123;1, 2, 3&#125;    fmt.Println(arr[4])&#125;</code></pre><pre class="line-numbers language-none"><code class="language-none">$ go run hello.gopanic: runtime error: index out of range [4] with legnth 3</code></pre><h4 id="defer">defer</h4><p>panic会导致程序被中止，但是在退出前，会先处理完当前携程上已经defer的任务，执行完成后再退出。效果类似于Java语言的<code>try...catch</code>。</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">package main&#x2F;&#x2F; hello.gofunc main() &#123;    defer func() &#123;        fmt.Println(&quot;defer func&quot;)    &#125;()        arr :&#x3D; []int &#123;1, 2, 3&#125;    fmt.Println(arr[4])&#125;</code></pre><pre class="line-numbers language-none"><code class="language-none">$ go run hello.godefer funcpanic: runtime error: index out of range [4] with length 3</code></pre><p>可以defer多个任务，在同一个函数中defer多个任务，会逆序执行。即先执行最后的defer的任务(类似于栈)。</p><p>在这里，defer的任务执行完成之后，panic还会继续被抛出，导致程序非正常结束。</p><h4 id="recover">recover</h4><p>golang还提供了recover函数，可以避免因为panic发生而导致整个程序终止，recover函数只在defer中生效</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; recover.gofunc test_recover() &#123;    defer func() &#123;        fmt.Println(&quot;defer func&quot;)        if err :&#x3D; recover(); err !&#x3D; nil &#123;            fmt.Println(&quot;recover success&quot;)        &#125;    &#125;()        arr :&#x3D; []int&#123;1, 2, 3&#125;    fmt.Println(arr[4])    fmt.Println(&quot;after panic&quot;)&#125;</code></pre><pre class="line-numbers language-none"><code class="language-none">$ go run recover.godefer funcrecover successafter recover</code></pre><p>我们可以看到，recover捕获了panic，程序正常结束。<code>test_recover()</code>中的afterpanic没有打印，这是正确的，当panic被触发时，控制权就被交给了defer。就像在Java中，<code>try</code>代码块中发生了异常，控制权交给了<code>catch</code>，接下来执行catch代码块中的代码。而在<code>main()</code>中打印了afterrecover，说明程序已经恢复正常，继续往下执行到结束。</p><h4 id="gee的错误处理机制">Gee的错误处理机制</h4><p>对一个Web框架而言，错误处理机制是非常必要的。可能是框架本身没有完备的测试，导致在某些情况下出现空指针异常等情况。也有可能用户不正确的参数，触发了某些异常，例如数组越界，空指针等。如果因为这些原因导致系统宕机，必然是不可接受的。</p><p>我们在第六天实现的框架并没有加入异常处理机制，如果代码中存在会触发panic的bug，就很容易宕机。</p><p>看下面示例代码</p><pre class="line-numbers language-go" data-language="go"><code class="language-go">&#x2F;&#x2F; hello.gofunc main() &#123;    r :&#x3D; gee.New()    r.GET(&quot;&#x2F;panic&quot;, func(c *gee.Context) &#123;        names :&#x3D; []string&#123;&quot;gee&quot;&#125;        c.String(http.StatusOK, names[100])    &#125;)    r.Run(&quot;:9999&quot;)&#125;</code></pre><p>在上面的代码中，我们为gee注册了路由<code>/panic</code>，而这个路由的处理函数内部存在数组越界<code>names[100]</code>，如果访问<code>localhost:9999/panic</code>，web服务器就会宕掉。</p><p>今天，我们将在gee中添加一个添加一个非常简单的错误处理机制，即在此类错误发生时，向用户返回Internal ServerError，并且在日志中打印必要的错误信息，方便进行错误定位。</p><p>我们之前实现了中间件机制，错误处理也可以作为一个中间件，增强gee框架的能力。</p><blockquote><p>day7/gee/recovery.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package geeimport (    &quot;fmt&quot;    &quot;log&quot;    &quot;net&#x2F;http&quot;    &quot;runtime&quot;    &quot;strings&quot;)&#x2F;&#x2F; print stack trace for debugfunc trace(message string) string &#123;    var pcs [32]uintptr    n :&#x3D; runtime.Callers(3, pcs[:]) &#x2F;&#x2F; skip first 3 caller        var str strings.Builder    str.WriteString(message + &quot;\nTraceback: &quot;)    for _, pc :&#x3D; range pcs[:n] &#123;        fn :&#x3D; runtime.FuncForPC(pc)        file, line :&#x3D; fn.FileLine(pc)        str.WriteString(fmt.Sprintf(&quot;\n\t%s:%d&quot;, file, line))    &#125;    return str.String()&#125;func Recovery() HandlerFunc &#123;    return func(c *Context) &#123;        defer func() &#123;            if err :&#x3D; recover(); err !&#x3D; nil &#123;                message :&#x3D; fmt.Sprintf(&quot;%s&quot;, err)                log.Printf(&quot;%s\n\n&quot;, trace(message))                c.Fail(http.StatusInternalServerError, &quot;Internal Server Error&quot;)            &#125;        &#125;()        c.Next()    &#125;&#125;</code></pre><p><code>Recovery()</code>的实现很简单，使用defer挂载上错误恢复的函数，在这个函数中调用<code>recover()</code>，捕获panic，并且将堆栈信息打印在日志里，向用户返回InternalServer Error。</p><p>在<code>trace()</code>中，调用了<code>runtime.Callers(3, pcs[:])</code>，Callers用来返回调用栈的程序计数器，第0个Caller是Callers本身，第一个是上一层trace，第二个是再上一层的<code>defer func</code>。因此，为了日志简洁一点，我们跳过前三个Caller。</p><p>接下来，通过<code>runtime.FuncForPC(pc)</code>获取对应的函数，再通过<code>fn.FileLine(pc)</code>获取到调用该函数的文件名和行号，打印在日志里。</p><p>至此，gee框架的错误处理机制就完成了。</p><blockquote><p>day7/gee/gee.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">func Default() *Engine &#123;    engine :&#x3D; New()    engine.Use(Logger(), Recovery())    return engine&#125;</code></pre><h4 id="使用demo-4">使用Demo</h4><blockquote><p>day7/main.go</p></blockquote><pre class="line-numbers language-go" data-language="go"><code class="language-go">package mainimport (    &quot;net&#x2F;http&quot;    &quot;gee&quot;)func main() &#123;    r :&#x3D; gee.Default()    r.GET(&quot;&#x2F;&quot;, func(c *gee.Context) &#123;        c.String(http.StatusOK, &quot;hello gee\n&quot;)    &#125;)    &#x2F;&#x2F; index out of range for testing Recovery()    r.GET(&quot;&#x2F;panic&quot;, func(c *gee.Context) &#123;        names :&#x3D; []string&#123;&quot;gee&quot;&#125;\        c.String(http.StatusOK, names[100])    &#125;)    r.Run(&quot;:9999&quot;)&#125;</code></pre><p>下面来进行测试，先访问一个主页，访问一个有bug的<code>/panic</code>，服务正常返回。接下来我们再一次成功访问了主页，说明服务完全运转正常。</p><blockquote><p>Client</p></blockquote><pre class="line-numbers language-none"><code class="language-none">$ curl http:&#x2F;&#x2F;localhost:9999hello gee$ curl http:&#x2F;&#x2F;localhost:9999&#x2F;panic&#123;&quot;message&quot;:&quot;Internal Server Error&quot;&#125;$ curl http:&#x2F;&#x2F;localhost:9999hello gee</code></pre><blockquote><p>Server</p></blockquote><p>我们可以在后台日志中看到如下内容，引发错误的原因和堆栈信息都被打印了出来，通过日志，我们可以很容易知道，在day7/main.go:47的地方出现了<code>index out of range</code>的错误。</p><pre class="line-numbers language-none"><code class="language-none">2022&#x2F;07&#x2F;29 22:15:43 Route  GET - &#x2F;2022&#x2F;07&#x2F;29 22:15:43 Route  GET - &#x2F;panic2022&#x2F;07&#x2F;29 22:15:45 runtime error: index out of range [100] with length 1Traceback:    &#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;runtime&#x2F;panic.go:838        &#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;runtime&#x2F;panic.go:89        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;main.go:17        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;gee&#x2F;context.go:41        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;gee&#x2F;recovery.go:56        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;gee&#x2F;context.go:41        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;gee&#x2F;logger.go:15        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;gee&#x2F;context.go:41        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;gee&#x2F;router.go:101        &#x2F;root&#x2F;code&#x2F;go&#x2F;src&#x2F;gee_web&#x2F;dev&#x2F;gee&#x2F;gee.go:121        &#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;net&#x2F;http&#x2F;server.go:2917        &#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;net&#x2F;http&#x2F;server.go:1967        &#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;runtime&#x2F;asm_amd64.s:15722022&#x2F;07&#x2F;29 22:15:45 [500] &#x2F;panic in 103.5μs</code></pre><h3 id="一些想法">一些想法</h3><p>其实整篇做下来吧，其实到现在对整个框架只能够说是大概了解，自己也跟着博客敲了一遍，也大概能看懂作者的设计思路，先做一个简单的http相应，后面再添加Context、前缀树等等。</p><p>在做的过程中，也会遇到很多bug，不同于c，Java，golang这门语言，个人感觉抽象程度比Java这些高，有时候出现panic，找到了出错的行数，还得去翻阅源码，不过吧，这个也算是在锻炼自己的动手能力和解决问题的能力，也算是有些收获吧。</p><h3 id="参考链接">参考链接</h3><p><ahref="https://geektutu.com/post/gee.html">7天用Go从零实现Web框架Gee教程| 极客兔兔 (geektutu.com)</a></p><p><ahref="https://blog.csdn.net/m0_52649917/article/details/121640535">(79条消息)解决vscode和go mod 导包冲突的问题_sora!的博客-CSDN博客_gomodvscode</a></p><p><ahref="https://blog.csdn.net/qiu_huouho/article/details/120733522">(80条消息)vscode使用go get 之后无法import_Restart丶的博客-CSDN博客</a></p><p><ahref="https://vimsky.com/examples/usage/fmt-fprintf-function-in-golang-with-examples.html">Golangfmt.Fprintf()用法及代码示例 - 纯净天空 (vimsky.com)</a></p><p><ahref="https://www.cnblogs.com/maji233/p/11178413.html">理解Golang中的interface和interface{}- maji233 - 博客园 (cnblogs.com)</a></p><p><a href="https://laravelacademy.org/post/21639">Go 语言通过 Request对象读取 HTTP 请求报文 | 请求处理 | Go Web 编程(laravelacademy.org)</a></p><p><ahref="https://www.imooc.com/wenda/detail/664445#:~:text=http.ResponseWriter用来配置HTTP响应和发送数据给客户端的也是这样一个，io.Writer你要发送的数据（响应体）是通过调用组装的（不一定只有一次）ResponseWriter.Write,()（这是实现通用的io.Writer）.">ResponseWriter.Write和 io.WriteString 有什么区别？_慕课猿问 (imooc.com)</a></p><p><ahref="https://blog.csdn.net/qwe1765667234/article/details/124299251?spm=1001.2101.3001.6650.5&amp;utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-5-124299251-blog-109959201.pc_relevant_multi_platform_whitelistv1&amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-5-124299251-blog-109959201.pc_relevant_multi_platform_whitelistv1&amp;utm_relevant_index=10">(79条消息)Gonet.http包下的ListenAndServe函数的参数问题_qwe1765667234的博客-CSDN博客</a></p><p><ahref="https://blog.csdn.net/qq_34021712/article/details/109959201">(79条消息)Go使用net/http标准库(二)源码学习之-http.ListenAndServe()_这个名字想了很久的博客-CSDN博客</a></p><p><ahref="https://cloud.tencent.com/developer/ask/sof/206511">ServeHTTP是如何工作的？- 问答 - 腾讯云开发者社区-腾讯云 (tencent.com)</a></p>]]></content>
    
    
    <summary type="html">用七天实现一个类Gin的Web框架</summary>
    
    
    
    <category term="BackEnd" scheme="https://jaydenchang.top/categories/BackEnd/"/>
    
    
    <category term="Golang" scheme="https://jaydenchang.top/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>摄影的哲与思</title>
    <link href="https://jaydenchang.top/post/0x002E.html"/>
    <id>https://jaydenchang.top/post/0x002E.html</id>
    <published>2022-07-19T16:00:00.000Z</published>
    <updated>2022-07-20T14:43:56.191Z</updated>
    
    <content type="html"><![CDATA[<p>其实这篇文章，我已经酝酿了很久了，因为各种原因，又搁置了一段时间，恰好后面受软协技术部的邀请，我去做了一次面向软协内部的分享会。当然，由于各种原因，本次分享会没什么人听 <del>(好像是撞上了香农班)</del>，不过也在我意料之中 <del>(这样就越少人能看到我口糊的一面)</del>。</p><p>关于那天，其实我并不认为是一次分享会，更多的是一种聊天吧。</p><h4 id="缘起">缘起</h4><p>其实我在大二以前，完全可以说是对摄影没有任何概念，就算拍的话，也只是一些简单的路人照，对构图、颜色搭配没有什么研究。记得当时好像是在大一暑假吧，那时和Sam外出散心，路过沙面，被眼前的欧式风格吸引，于是拿出手机咔咔几张，也没去考虑构图啥的，觉得还挺好看，就直接放到了pyq上。</p><p>到后面吧，有一次准备出宿舍时，看到了很好看的晚霞，顺手拍了下来，还特意下了"SnapSeed"去调色，虽然调的有点离谱，饱和度调太过了，不过也还是满足了我的虚荣，包括两周后的一个早晨，看到了很好看的天，马上拍下来，调出了一种 "你的名字" 的感觉。</p><p><a href="https://mp.weixin.qq.com/s/9NDb9Ez4JAwsYsPe-_Eyhg">Jayden的2021摄影集</a></p><p>再后来，到了12月，我借到了相机，这才算是我正式踏上了摄影的路，开始去考虑快门，ISO，光圈的搭配，也开始去尝试不同焦段下，拍到不同的照片，虽然还是拍的不咋地，不过审美相较于以往有了较大的进步。</p><p>寒假在家的日子，是没有相机的，但我又不能不出片吧，也罢，拿起手机去外面瞎拍，也许是整个二月都处于阴雨天吧，拍出来的照片的风格都不合我意，但还是选了一两张出来添加到摄影集里。</p><h4 id="跳出困局">跳出困局</h4><p>回到学校了，又借到相机了，拍没几天，新鲜感又过去了，校园的角角落落基本都走遍了，能拍的新事物，也所剩无几。既然一个人出不了什么好点子，那就找一群人。清明期间，我找了几个朋友外出闲逛，社牛一次去搭讪路人给她们拍照，这也算是人像摄影的启蒙吧 (虽然之后到现在也没拍过人像)。</p><p>我觉得我真正觉得自己摄影技术的提高，是在五一吧，去到了更远的地方，见到了更多的风景，去记录一些城市风光，当然，我也开始研究照片的后期技术，除了基本的曝光，对比度等，也开始去探索曲线对照片整体颜色走向，亮度的影响。</p><p>端午时期，我拿起相机，借了长焦，再次来到广州，依旧是那些熟悉的角落，不过在新的视角下，我也有了更深的体会。明明这条路，我走了很多遍，但我还依然走下去，并乐此不疲，或许是对这座城市的感情，我住在广州也有20年了，这里有我们的生活轨迹，也有属于我们这座城市的骄傲！</p><h4 id="哲与思">哲与思</h4><p>要说摄影给我带来了什么吧，其实开始我只是很沉浸在其中而已，并没有多想，真要我真的静下来想，emm……应该是一下这些吧</p><ul><li>一种记录生活的方式</li><li>对取景框里的主角的把握</li><li>对生活态度的改变</li><li>生活虽忙，但别忘了摄影</li><li>less is more</li></ul><p>在我没拿起相机的日子，我走在路上，也会用我心中的取景框去构图，像一只窥伺的猫，当我被某一瞬间的画面打动时，我会连忙倒退几步，伫立观望，然后再心满意足地继续上路。</p><p>这篇从开始构思，到下笔，再到结束，经历了好几个月。真的，摄影改变了我太多。我在街头无休止地穿梭，表面上看似在旁观生活，实则想作为一个体验者，寻求与某个事物在精神上的突然邂逅。</p><p>继续拍下去吧！</p>]]></content>
    
    
    <summary type="html">一些碎碎念, 浅聊我对摄影的看法和摄影对我的改变</summary>
    
    
    
    <category term="随笔" scheme="https://jaydenchang.top/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://jaydenchang.top/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>只是一期唠嗑</title>
    <link href="https://jaydenchang.top/post/0x002D.html"/>
    <id>https://jaydenchang.top/post/0x002D.html</id>
    <published>2022-03-10T16:00:00.000Z</published>
    <updated>2025-07-27T08:30:25.734Z</updated>
    
    <content type="html"><![CDATA[<p>断断续续记录一些自己的想法</p><h4 id="section">2.25</h4><p><strong>首先很感谢每位点进来并且看完的朋友，非常感谢你们抽出你们宝贵的时光听我扯皮、吹牛，在这个快节奏的时代，能静下来写一篇随笔、读一篇文章也是不容易。</strong></p><p>整个二月，几乎可以说是属于阴雨天的，从大年初一开始，到整个二月结束，阴雨不断，当然中间也出过几天晴天，而我也因为一些琐事，让我心情变得有些低落，有人说，天气会影响心情，我对此半信半疑。从大年初一开始，一连下了3天大雨，我的心情也是压抑的，终于在2月4日那天，出了太阳，我也有了外出取景的机会，拍了些照，和附近的小朋友打球，但我还是提不起精神，第二天，和Sam相约出去走走，与其说是走，其实更多时间花在了骑行上面，那天，花了俩小时骑行26km，夜间骑行，耳机里播放着R&amp;B歌曲，江岸沿途的灯柱，对岸大厦的广告牌，我放慢了步调，贪婪地享受这夜色，回到家后，整理今天所摄照片，心情好一些了。</p><p>估计是多巴胺分泌不太够吧，第二天，又回到了前几天的状态，身体的预警机制提醒我，学不进去，那总得找些事情做，那不如看书吧，于是我开始尝试每天睡醒看半小时书，睡前看半小时书。开始那几天，多少有些不习惯，心静不下来，总是想去看手机，后面索性直接把手机锁了，丢到沙发上，尝试了几天，貌似感觉还不错。</p><p>单单只是看书吧，总感觉少了点什么，我尝试开始写日记，每天写点东西，记录转瞬即逝的想法，顺便练练字。</p><p><img src='0x002D/0x002D_1.jpg' style="zoom: 30%;" ></p><p><img src='0x002D/0x002D_2.jpg' style="zoom:33%;" ></p><center>字写的不太好，应该还能看得清写的啥吧 [狗头</center><h4 id="section-1">3.3</h4><p>写着发现好像跑题了，就是从过年后开始，我把我的睡前睡后的安排稍作调整，用来看书和复盘一天。这学期，从图书馆和好友那借了些书来看，首先看的是《追风筝的人》吧，在很早之前就有人推荐过我去看这本书，翻了几页，记录的是阿米尔的赎罪之旅，当阿米尔将索拉博带到美国，带着索拉博追风筝，索拉博渐渐打开心结，向着阿米尔微笑，而阿米尔此时也对着索拉博说着小时候哈桑经常对阿米尔说的话，"为你，千千万万遍"。</p><p>看完整本书吧，不由自主地想到了《肖申克的救赎》这本书(尽管我看的是电影的版本)，安迪在狱中服役了19年，也可以说是花了19年来救赎自己，在他即将越狱之时，他向瑞德提及了自己对妻子的爱与悔意。</p><p>"妻子说她很难了解我，我像一本合起来的书，她整天这样抱怨。她很漂亮，damn，我是多么爱她啊。我只是不擅表达。对，是我杀了她，枪不是我开的，但我害她离我远去，是我的脾气害死了她。"</p><p>这一段，我刷了好几遍，一直以为是安迪越狱前的某种仪式，到后面某天在外骑单车时，脑子里飘过一个想法，那一番对话，代表着安迪对自己的救赎，不是生命形式的救赎，不是生活方式上的救赎，而是灵魂层面上的救赎，在灵魂层面真正意义上的的自我重新认知。</p><p>同时也包括瑞德，前几次假释检验时，老老实实回答问题都被驳回，在第四十年，破罐子破摔，说出了自己在监狱四十年的感受，"我想对年轻的自己说……"，这一段，我同样也是看了好几遍，套路经不住灵魂的拷问，<strong>人可以通过模仿别人长大，但最终还是要用自己的语言面对这个世界</strong>。</p><p>再回到《追风筝的人》，阿米尔目睹哈桑被阿塞夫强暴而无动于衷，主要内心活动却是嫉妒父亲对哈桑的偏爱。移民美国后，阿米尔被羞愧自责的阴影所缠绕，他决定回阿富汗找哈桑。在解救索拉博时，面对阿塞夫的铁拳套，阿米尔没有退缩了，看到这里吧，阿米尔在赎罪的道路上已经跨出了一大半。</p><p>追完风筝，走进了围城，最近刚把《围城》看完，表面上吧，是写方鸿渐的早年经历，留学——求爱——婚姻，实际上，钱钟书刻画了三座围城，婚姻之城、事业之城，自我之城。这本书，看到后面，愈感觉讽刺性愈强，读完合上书那一刻，我心中在暗嘲那些像方鸿渐的人，但是反过来一想，自己身上也有方鸿渐的影子，芸芸众生，生活便是如此，围城之外又是一座围城。</p><h4 id="section-2">3.6</h4><p>最近无聊，翻了翻Instagram，原来我已经上传了这么多，因为一些特殊原因吧，我的Instagram没人关注，不过这样也好，可以在上面所心所欲发自己觉得拍的比较好看的照片，有时候甚至觉得Instagram的排版比国内的app还挺好看。</p><p><img src="0x002D/0x002D_3.jpg" style="zoom: 22%;" /></p><h4 id="section-3">3.7</h4><p>这几天晚上又因为一些琐事，晚上好久没看书了，有点懊悔自己断了这个习惯，不过每天在自己的小本本上记录的习惯却坚持了下来。最近又重新看了<spanclass="math inline">\(Sean\   Tucher\)</span>的视频，在练听力的过程中，我在刷评论区，看到了一段文字，让我内心深有感触</p><p>"过去的失败，给了我们一个倾听灵魂深处声音的机会，并且它促使我们去往一个平时绝对不会接触的地方"。</p><p><ahref="https://www.bilibili.com/video/BV16t411S7jr">拥抱阴影——关于光线和人生的思考【SeanTucker中字】_哔哩哔哩_bilibili</a></p><p><a href="https://www.bilibili.com/video/av28378168">保护你的高光 |合理安排相机和人生中有限的动态范围「SeanTucker中字」_哔哩哔哩_bilibili</a></p><p>这几天，我一直在看他之前的两期视频，有时想，我每天深夜emo真的有意义吗？第二天我查看我的储存卡，已经好几天没出新片了，想着，总得在校园里闲逛会，但是愈在意，产出就愈低。</p><p>我不得不找个阴暗的地方，翻看这学期新拍的照，噢！原来当时的我是这样构图的，原来当时的我是这样调参数的。那干脆，今天不拍了，走在风中，听听歌貌似也不错。</p><p>很感谢一位朋友(<spanclass="math inline">\(@Lucas\)</span>)推荐了<spanclass="math inline">\(Sean\Tucker\)</span>给我，这位摄影师在哲学层面教会了我许多(当然你也可以认为我在扯淡)。</p><h4 id="section-4">3.10</h4><p>这几天在看书时，脑子里蹦出一句话，"有用是毒药，无用是解药"，这当时是一篇初中语文阅读题的标题。有时候我一直在想，什么是艺术，什么是文艺青年，我以前一直在想，我拿着相机到处走走拍拍，抱着本书读，这样就是文艺，这样就是文艺青年，显然，我只是青年，是伪文艺青年，自己还不配谈"文艺"。</p><p>文艺不应该是噱头，如果我能享受其中，那我就搞文艺这一套，读书摄影看电影，思考散步谈人生，做一些在别人眼里的为"无用"的事。别人再怎么嘲讽都没用，因为，这是我的生活方式。</p><p>或许吧，也不应该有"文艺青年"这个标签，<strong>安安静静做点自己想做的事，过点自己想过的生活</strong>，这也许是"文艺青年"最初的定义吧。</p>]]></content>
    
    
    <summary type="html">一些想法，当然你也可以认为我在扯淡</summary>
    
    
    
    <category term="随笔" scheme="https://jaydenchang.top/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://jaydenchang.top/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>cocos2d拖动组件吸附效果</title>
    <link href="https://jaydenchang.top/post/0x002C.html"/>
    <id>https://jaydenchang.top/post/0x002C.html</id>
    <published>2022-02-17T16:00:00.000Z</published>
    <updated>2025-07-27T08:14:02.112Z</updated>
    
    <content type="html"><![CDATA[<p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p><img src="0x002C/0x002C-1.png" /></p><h4 id="代码实现">代码实现</h4><h5 id="定义节点和函数功能">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果">最终效果</h5><p>拖入目标区域</p><p><img src="0x002C/0x002C_drag_accept.gif" /></p><p>没拖到指定区域</p><p><img src="0x002C/0x002C_drag_refuse.gif" /></p><h5 id="修正">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p><img src="0x002C/0x002C_xOy.png" /></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p><img src="0x002C/0x002C_xOy_detail.png" /></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p><img src="0x002C/0x002C_without_correct.gif" /></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p><img src="0x002C/0x002C_correct.gif" /></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p><h4 id="代码实现-1">代码实现</h4><h5 id="定义节点和函数功能-1">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果-1">最终效果</h5><p>拖入目标区域</p><p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p><img src="0x002C/0x002C-1.png" /></p><h4 id="代码实现-2">代码实现</h4><h5 id="定义节点和函数功能-2">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果-2">最终效果</h5><p>拖入目标区域</p><p><img src="0x002C/0x002C_drag_accept.gif" /></p><p>没拖到指定区域</p><p><img src="0x002C/0x002C_drag_refuse.gif" /></p><h5 id="修正-1">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p><img src="0x002C/0x002C_xOy.png" /></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p><img src="0x002C/0x002C_xOy_detail.png" /></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p><img src="0x002C/0x002C_without_correct.gif" /></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p><img src="0x002C/0x002C_correct.gif" /></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接-1">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p><p>没拖到指定区域</p><p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p><img src="0x002C/0x002C-1.png" /></p><h4 id="代码实现-3">代码实现</h4><h5 id="定义节点和函数功能-3">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果-3">最终效果</h5><p>拖入目标区域</p><p><img src="0x002C/0x002C_drag_accept.gif" /></p><p>没拖到指定区域</p><p><img src="0x002C/0x002C_drag_refuse.gif" /></p><h5 id="修正-2">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p><img src="0x002C/0x002C_xOy.png" /></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p><img src="0x002C/0x002C_xOy_detail.png" /></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p><img src="0x002C/0x002C_without_correct.gif" /></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p><img src="0x002C/0x002C_correct.gif" /></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接-2">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p><h5 id="修正-3">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p><img src="0x002C/0x002C-1.png" /></p><h4 id="代码实现-4">代码实现</h4><h5 id="定义节点和函数功能-4">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果-4">最终效果</h5><p>拖入目标区域</p><p><img src="0x002C/0x002C_drag_accept.gif" /></p><p>没拖到指定区域</p><p><img src="0x002C/0x002C_drag_refuse.gif" /></p><h5 id="修正-4">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p><img src="0x002C/0x002C_xOy.png" /></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p><img src="0x002C/0x002C_xOy_detail.png" /></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p><img src="0x002C/0x002C_without_correct.gif" /></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p><img src="0x002C/0x002C_correct.gif" /></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接-3">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p><img src="0x002C/0x002C-1.png" /></p><h4 id="代码实现-5">代码实现</h4><h5 id="定义节点和函数功能-5">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果-5">最终效果</h5><p>拖入目标区域</p><p><img src="0x002C/0x002C_drag_accept.gif" /></p><p>没拖到指定区域</p><p><img src="0x002C/0x002C_drag_refuse.gif" /></p><h5 id="修正-5">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p><img src="0x002C/0x002C_xOy.png" /></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p><img src="0x002C/0x002C_xOy_detail.png" /></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p><img src="0x002C/0x002C_without_correct.gif" /></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p><img src="0x002C/0x002C_correct.gif" /></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接-4">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p><img src="0x002C/0x002C-1.png" /></p><h4 id="代码实现-6">代码实现</h4><h5 id="定义节点和函数功能-6">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果-6">最终效果</h5><p>拖入目标区域</p><p><img src="0x002C/0x002C_drag_accept.gif" /></p><p>没拖到指定区域</p><p><img src="0x002C/0x002C_drag_refuse.gif" /></p><h5 id="修正-6">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p><img src="0x002C/0x002C_xOy.png" /></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p><img src="0x002C/0x002C_xOy_detail.png" /></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p><img src="0x002C/0x002C_without_correct.gif" /></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p><img src="0x002C/0x002C_correct.gif" /></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接-5">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p>最近在学习制作小游戏，要实现一个拖动吸附效果，这里简单实现一下</p><p><img src="0x002C/0x002C-1.png" /></p><h4 id="代码实现-7">代码实现</h4><h5 id="定义节点和函数功能-7">定义节点和函数功能</h5><p>在<code>properties</code>里新建一个对象，用来接收目标区域的节点</p><pre class="line-numbers language-json" data-language="json"><code class="language-json">properties:&#123;    sense: &#123;        defaule: null,        type: cc.Node,    &#125;&#125;</code></pre><p>然后在小车节点里绑定这个脚本，将要测试的目标节点拖动到属性检查器的<code>sense</code></p><p>这里用小车来表示要移动的组件，先在<code>onload()</code>内定义小车组件，设置位置，以及定义三个触摸事件函数</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onload() &#123;    this.carPos &#x3D; cc.v2(0, 0);    &#x2F;&#x2F; 定义一个触摸移动控件    this.node.setPosition(this.carPos.x, this.carPos.y);    this.origin &#x3D; this.node.convertToWorldSpace(cc.v2(0, 0));    &#x2F;&#x2F; 获取小车移动前的坐标        &#x2F;&#x2F; 对当前节点设置位置    this.node.on(&quot;touchstart&quot;, this.touchStart, this);    this.node.on(&quot;touchmove&quot;, this.touchMove, this);    this.node.on(&quot;touchend&quot;, this.touchEnd, this);    &#x2F;&#x2F; 定义三个触摸事件函数&#125;</code></pre><p>然后就是对三个触摸事件定义</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">touchStart(event) &#123;    let touchPos &#x3D; event.getLocation();    &#x2F;&#x2F; 获取当前触摸位置    let posInNode &#x3D; this.worldConvertLocalPoint(this.node, touchPos);    &#x2F;&#x2F; 将当前触摸位置坐标转换为世界坐标    let target &#x3D; this.node.getContentSize();    &#x2F;&#x2F; 获得当前触摸组件的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    &#x2F;&#x2F; 对触摸对象组件创建一个矩形对象    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断触摸的位置是否在矩形内        this.touchTile &#x3D; this.node;        &#x2F;&#x2F; 获取被触摸的对象    &#125;    console.log(posInNode.x + &quot; &quot; + posInNode.y);    &#x2F;&#x2F; 测试，打印当前触摸位置&#125;,    touchMove(event) &#123;    if (this.touchTile) &#123;        this.touchTile.setPosition(this.touchTile.x + event.getDelta().x ,            this.touchTile.y + event.getDelta().y);        &#x2F;&#x2F; 根据小车组件移动距离重新给小车定位    &#125;&#125;,touchEnd(event) &#123;    let touchPos &#x3D; this.touchTile.convertToWorldSpaceAR(cc.v2(0, 0));    let posInNode &#x3D; this.worldConvertLocalPoint(this.sense1, touchPos);    let target &#x3D; this.sense1.getContentSize();    &#x2F;&#x2F; 定义坐标修正值    let correctValue &#x3D; cc.v2(this.sense.width &#x2F; 2  - this.origin.x - this.node.width &#x2F; 2, this.sense.height &#x2F; 2 - this.origin.y - this.node.height &#x2F; 2);    &#x2F;&#x2F; 获取要置放的区域的大小    let rect &#x3D; cc.rect(0, 0, target.width, target.height);    if (rect.contains(posInNode)) &#123;        &#x2F;&#x2F; 判断小车是否落在目标区域的矩形内        console.log(&quot;---endPos&quot;);        &#x2F;&#x2F; 设置触摸结束后小车的落位坐标        let targetPos &#x3D; this.sense1.convertToWorldSpace(cc.v2(correctValue));        &#x2F;&#x2F; 获取目标区域的中心坐标        let action &#x3D; cc.moveTo(0.3, targetPos);        &#x2F;&#x2F; 新建一个位移动作，动画持续时间为0.3s        this.touchTile.runAction(action);        &#x2F;&#x2F; 小车组件执行动作    &#125; else &#123;        console.log(&quot;----go back&quot;);        let action &#x3D; cc.moveTo(0.3, this.carPos);        &#x2F;&#x2F; 组件回到小车初始位置        this.touchTile.runAction(action);    &#125;    this.touchTile &#x3D; null;    &#x2F;&#x2F; 重置触摸组件为空&#125;,    worldConvertLocalPoint(node, worldPoint) &#123;    if (node) &#123;        return node.convertToNodeSpace(worldPoint);    &#125;    return null;&#125;</code></pre><h5 id="最终效果-7">最终效果</h5><p>拖入目标区域</p><p><img src="0x002C/0x002C_drag_accept.gif" /></p><p>没拖到指定区域</p><p><img src="0x002C/0x002C_drag_refuse.gif" /></p><h5 id="修正-7">修正</h5><p>这里要把小车放到目标区域的正中心，需要对坐标进行修正。在cocoscreator里，有节点坐标和世界坐标这两个概念</p><p><img src="0x002C/0x002C_xOy.png" /></p><p>而在属性检查器里，我们所设置的<code>position</code>，也就是锚点的位置，是相对于父节点的，例如图中我把<code>position</code>设为0和0，就是相对于父节点，该组件定位在父节点的几何中心。</p><p><img src="0x002C/0x002C_xOy_detail.png" /></p><p>那么，哪些坐标值和最终放置的位置坐标有关联呢？</p><ul><li>小车初始坐标值</li><li>小车组件的长宽</li><li>目标区域的长宽</li></ul><p>在没有修正之前，把<code>targetPos</code>的值设为<code>this.sense.convertToWorldSpace(cc.v2(0, 0))</code>，拖动后的效果如下图</p><p><img src="0x002C/0x002C_without_correct.gif" /></p><p>并且log打印目标位置的坐标，水平值离屏幕宽度一半还有一定的差距，这时我又打印了拖动结束后小车的坐标值，好家伙，我轻点小车没有拖动，控制台输出的坐标值为<code>(0,0)</code>，而图中很明显，小车的位置不在世界坐标的原点上，即此时小车的坐标参照点为小车的初始位置</p><p>那问题来了，怎么修正？</p><p>只需在<code>onload()</code>中定义一个变量储存小车的世界坐标值<code>this.origin = this.node.convertToWorldSpace(cc.v2(0, 0))</code>，然后在<code>touchEnd()</code>中新定义一个向量值<code>correctValue</code>，新建一个向量<code>cc.v2(-this.origin.x, -this.origin.y)</code>，并返回给<code>correctValue</code>，再将<code>correctValue</code>转化为世界坐标赋给<code>targetPos</code>，此时小车会自动吸附到目标区域左下角，展现的效果如下</p><p><img src="0x002C/0x002C_correct.gif" /></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接-6">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p><p>如果要把小车定位到目标区域的正中央，还需要考虑小车组件和目标区域的长宽，相应地，<code>correctValue</code>应该设为<code>cc.v2(this.sense.width / 2 - this.node.width / 2 - this.origin.x, this.sense.height / 2 - this.node.height / 2 - this.origin.y)</code></p><h4 id="参考链接-7">参考链接</h4><p><ahref="https://blog.csdn.net/qq_45310244/article/details/113854722">(61条消息)CocosCreator的拖动小游戏主要逻辑_天才派大星 !的博客-CSDN博客_cocoscreator 拖动</a></p>]]></content>
    
    
    <summary type="html">在Cocos2d中拖动组件并吸附到节点中央</summary>
    
    
    
    <category term="Cocos" scheme="https://jaydenchang.top/categories/Cocos/"/>
    
    
    <category term="JavaScript" scheme="https://jaydenchang.top/tags/JavaScript/"/>
    
    <category term="Cocos" scheme="https://jaydenchang.top/tags/Cocos/"/>
    
  </entry>
  
  <entry>
    <title>win10找回Ubuntu启动项(非EasyBCD)</title>
    <link href="https://jaydenchang.top/post/0x002B.html"/>
    <id>https://jaydenchang.top/post/0x002B.html</id>
    <published>2022-01-12T16:00:00.000Z</published>
    <updated>2022-01-13T04:04:41.513Z</updated>
    
    <content type="html"><![CDATA[<p>最近想对装在电脑上的Ubuntu进行更新，但是之前在BIOS里改了引导系统的文件，导致找不到Ubuntu启动项，EasyBCD程序也不起作用(整块硬盘Windows分区都是GPT，改BIOS也没什么用)，在必应上逛了两天找到了一个解决方法，在Windows下用命令行修改引导文件</p><p>打开管理员命令行(不是<u><strong>powershell</strong></u>)，输入以下命令</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mountvol g: &#x2F;sg:cd EFIbcdedit &#x2F;set &#123;bootmgr&#125; path \EFI\ubuntu\grubx64.efi</code></pre><p>这时候重启，开机就会进入grub菜单</p><p>如果想改回Windows引导，则最后一行命令改为</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">bcdedit &#x2F;set &#123;bootmgr&#125; path \EFI\Microsoft\Boot\bootmgfw.efi</code></pre><h4 id="参考链接">参考链接</h4><p><a href="https://linux.cn/article-4667-1.html">桌面应用|Windows和Ubuntu双系统，修复UEFI引导的两种办法 (linux.cn)</a></p>]]></content>
    
    
    <summary type="html">不进入BIOS的情况下找回Ubuntu启动项</summary>
    
    
    
    <category term="整活" scheme="https://jaydenchang.top/categories/%E6%95%B4%E6%B4%BB/"/>
    
    
    <category term="Linux" scheme="https://jaydenchang.top/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>不关闭SELinux情况下使用ftp传输</title>
    <link href="https://jaydenchang.top/post/0x002A.html"/>
    <id>https://jaydenchang.top/post/0x002A.html</id>
    <published>2022-01-03T16:00:00.000Z</published>
    <updated>2022-01-03T08:43:52.196Z</updated>
    
    <content type="html"><![CDATA[<p>在做搭建ftp服务器的作业时，整了一个活，在不关闭SELinux的情况下测试ftp服务器</p><p>使用的环境，虚拟机*2 (CentOS 7)，Hyper-v，网卡已设为静态</p><p>需要安装的软件包：</p><ul><li><p>服务器(下称server)：</p><ul><li><p>vsftpd</p></li><li><p>ftp</p></li><li><p>ip可自定义，此处设为192.168.4.5</p></li></ul></li><li><p>客户机(下称client)：</p><ul><li><p>ftp</p></li><li><p>ip这里设为192.168.4.205</p></li></ul></li></ul><h4 id="修改vsftpd配置">修改vsftpd配置</h4><p>进入目录<code>/etc/vsftpd</code>，编辑<code>vsftpd.conf</code>，在最后一行添加</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">listen_port&#x3D;21</code></pre><h4 id="防火墙添加信任">防火墙添加信任</h4><p>然后在防火墙里允许特定ip访问特定端口(作业里要访问的ip是<code>192.168.4.205</code>)</p><pre class="line-numbers language-none"><code class="language-none">firewall-cmd --permanent --add-rich-rule&#x3D;&quot;rule family&#x3D;&quot;ipv4&quot; source address&#x3D;&quot;192.168.4.205&quot; port protocol&#x3D;&quot;tcp&quot; port&#x3D;&quot;21&quot; accept&quot;</code></pre><p>向客户机开放21端口</p><pre class="line-numbers language-none"><code class="language-none">firewall-cmd --reload</code></pre><p>重新载入防火墙</p><pre class="line-numbers language-none"><code class="language-none">firewall-cmd --zone&#x3D;public --list-rich-rules</code></pre><p>查看开放的端口</p><p>如果显示防火墙未启动，可以运行命令启动防火墙</p><pre class="line-numbers language-none"><code class="language-none">systemctl start firewalld</code></pre><h4 id="客户机测试">客户机测试</h4><p>连接服务器</p><pre class="line-numbers language-none"><code class="language-none">ftp 192.168.4.5</code></pre><p>当显示<code>ftp&gt;</code>时，输入<code>ls</code>或者<code>pwd</code>查看当前位置时，又出现了一个bug，显示<code>no route to host</code>，</p><p>这时候再回到服务器，修改<code>/etc/sysconfig/</code>下的<code>iptables-config</code>，更改其中<code>IPTABLES_MODULES=""</code>项为</p><pre class="line-numbers language-none"><code class="language-none">IPTABLES_MODULES&#x3D;&quot;ip_nat_ftp ip_conntrack_ftp&quot;</code></pre><p>然后重启防火墙相关服务</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">systemctl restart iptables.service</code></pre><p>如果服务器重启后，客户机ftp连接服务器还出现<code>no route to host</code>的情况，以此输入以下命令开启相关防火墙服务</p><pre class="line-numbers language-none"><code class="language-none">systemctl start firewalldsystemctl start iptables.service</code></pre><p>如果仅是临时使用，可以运行以下两条命令</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">modprobe ip_nat_ftpmodprobe ip_conntrack_ftp</code></pre><h4 id="参考链接">参考链接</h4><p><a href="https://blog.csdn.net/u012906135/article/details/69944485">ftp connect: No route to host 解决方案_hello world!-CSDN博客</a></p><p><a href="https://www.jianshu.com/p/4801d9dbaa84">Linux防火墙firewall只允许特定ip访问 - 简书 (jianshu.com)</a></p>]]></content>
    
    
    <summary type="html">两台Linux服务器不关闭SELinux进行ftp传输</summary>
    
    
    
    <category term="整活" scheme="https://jaydenchang.top/categories/%E6%95%B4%E6%B4%BB/"/>
    
    
    <category term="Linux" scheme="https://jaydenchang.top/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>数据结构-图及最小生成树</title>
    <link href="https://jaydenchang.top/post/0x0029.html"/>
    <id>https://jaydenchang.top/post/0x0029.html</id>
    <published>2021-12-08T16:00:00.000Z</published>
    <updated>2021-12-11T06:08:25.654Z</updated>
    
    <content type="html"><![CDATA[<p>好久没更了 <del>其实摸鱼摸太久了</del>，当然也是最近太多事，一直没有时间去打理博客，趁着周末有空，来整理下图部分的内容</p><p>这里来总结下无向图、最小生成树(prim和Dijkstra)算法</p><h4 id="无向图">无向图</h4><h5 id="结构">结构</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">const int maxSize &#x3D; 100;int visited[maxSize] &#x3D; &#123;0&#125;; &#x2F;&#x2F; 到后面发现，visited在无向图中设计的是真的巧妙template &lt;class DataType&gt;class MGraph &#123;    public:      MGraph(DataType a[], int n, int e); &#x2F;&#x2F; 构造函数，构造有n个顶点e条边的图     ~MGraph() &#123;&#125;     void DFS(int); &#x2F;&#x2F; 深搜     void BFS(int); &#x2F;&#x2F; 广搜        private:     DataType vertex[maxSize]; &#x2F;&#x2F; 存放图中顶点的数组     int edge[maxSize][maxSize]; &#x2F;&#x2F; 存放图中边的数组     int vertexNum, edgeNum; &#x2F;&#x2F; 图中的顶点数和变数&#125;;</code></pre><h5 id="构造函数">构造函数</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;MGraph&lt;DataType&gt;::MGraph(DataType a[], int n, int e) &#123;    int i, j, k;    vertexNum &#x3D; n;    edgeNum &#x3D; e;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        vertex[i] &#x3D; a[i]; &#x2F;&#x2F; 储存顶点    &#125;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        for (j &#x3D; 0; j &lt; vertexNum; j++) &#123;            edge[i][j] &#x3D; 0; &#x2F;&#x2F; 初始化邻接矩阵        &#125;    &#125;    for (k &#x3D; 0; k &lt; edgeNum; k++) &#123;        cin &gt;&gt; i &gt;&gt; j; &#x2F;&#x2F; 以此输入每条边依附的两个顶点的编号        edge[i][j] &#x3D; 1; &#x2F;&#x2F; 对输入的边做标记(无向图，双向标记)        edge[j][i] &#x3D; 1;    &#125;&#125;</code></pre><h5 id="广搜和深搜">广搜和深搜</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::DFS(int v) &#123;    cout &lt;&lt; vertex[v];    visited[v] &#x3D; 1;    for (int j &#x3D; 0; j &lt; vertexNum; j++) &#123;        if (edge[v][j] &#x3D;&#x3D; 1 &amp;&amp; visited[j] &#x3D;&#x3D; 0)             DFS(j);        &#x2F;&#x2F; 递归的妙处会在后面讲到    &#125;&#125;template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::BFS(int v) &#123;    int w, j, queue[maxSize] &#x3D; &#123;0&#125;; &#x2F;&#x2F; queue数组记录的是访问矩阵第几行的顺序    for (int i &#x3D; 0; i &lt; vertexNum; i++) &#123;        visited[i] &#x3D; 0;    &#125;    int front &#x3D; -1, rear &#x3D; -1;    cout &lt;&lt; vertex[v];    visited[v] &#x3D; 1;    queue[++rear] &#x3D; v;    while (front !&#x3D; rear) &#123;        v &#x3D; queue[++front];        for (j &#x3D; 0; j &lt; vertexNum; j++) &#123;            if (edge[v][j] &#x3D;&#x3D; 1 &amp;&amp; visited[j] &#x3D;&#x3D; 0) &#123;                cout &lt;&lt; vertex[j];                visited[j] &#x3D; 1;                queue[++rear] &#x3D; j;            &#125;        &#125;    &#125;&#125;</code></pre><h5 id="主函数">主函数</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">int main() &#123;    char ch[] &#x3D; &#123;&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;, &#39;e&#39;, &#39;f&#39;&#125;;    MGraph&lt;char&gt; MG(ch, 6, 6);    for (int i &#x3D; 0; i &lt; maxSize; i++) &#123;        visited[i] &#x3D; 0;    &#125;    cout &lt;&lt; &quot;DFS order: \n&quot;;    MG.DFS(0);    cout &lt;&lt; &quot;\n&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;\n&quot;;    cout &lt;&lt; &quot;BFS order: \n&quot;;    MG.BFS(0);&#125;</code></pre><h5 id="测试用例及其邻接矩阵">测试用例及其邻接矩阵</h5><p>测试数据</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">0 10 20 51 21 43 4</code></pre><p>邻接矩阵</p><pre class="line-numbers language-c" data-language="c"><code class="language-c">   0 1 2 3 4 5 0&#39;   1 1     1 &#39;1&#39; 1   1   1   &#39;2&#39; 1 1         &#39;3&#39;         1   &#39;4&#39;   1   1     &#39; 5&#39; 1           &#39;&#x2F;&#x2F; 以横轴为x，竖轴为y       </code></pre><p>借用这个样例来说一下深搜和广搜</p><h6 id="深搜">深搜</h6><p>已知无向图的邻接矩阵是关于对角线对称的，深搜从第一行开始搜索，搜索到<code>(1,0)</code>时进入递归，进入递归后，首先对<code>visited[v]</code>进行标记，通过观察可以知道，上一轮DFS传入的j和下一轮DFS的v在矩阵中关于对角线对称 <del>好像是个无用信息</del>，每一轮DFS的<code>visited[v]=1</code>就是为了避免重复访问<code>vertex[v]</code>，再加上那条if语句的配合，即可无重复遍历完整个图</p><h6 id="广搜">广搜</h6><p>广搜的话其实还是要自己画出一个无向图来并且在debug模式运行一遍才知道大概是怎么个流程。对上面的测试用例来说，在第一轮搜索时，访问的都是和0号这个点有通路的点，第二轮是1号，第三轮是2号，依此类推。如果把最开始输入的0号放在中间，后面输入的数据一圈圈和0号联通，产生关联，那么广搜可以理解为，从搜寻点一圈圈向外扩散找</p><h5 id="完整代码">完整代码</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include &lt;bits&#x2F;stdc++.h&gt;using namespace std;const int maxSize &#x3D; 100;int visited[maxSize] &#x3D; &#123;0&#125;;template &lt;class DataType&gt;class MGraph &#123;    public:     MGraph(DataType a[], int n, int e);     ~MGraph() &#123;&#125;     void DFS(int);     void BFS(int);        private:     int vertex[maxSize];     int edge[maxSize][maxSize];     int vertexNum, edgeNum;&#125;;template &lt;class DataType&gt;MGraph&lt;DataType&gt;::MGraph(DataType a[], int n, int e) &#123;    int i, j, k;    vertexNum &#x3D; n;    edgeNum &#x3D; e;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        vertex[i] &#x3D; a[i];    &#125;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        for (j &#x3D; 0; j &lt; vertexNum; j++) &#123;            edge[i][j] &#x3D; 0;        &#125;    &#125;    for (k &#x3D; 0; k &lt; edgeNum; k++) &#123;        cin &gt;&gt; i &gt;&gt; j;        edge[i][j] &#x3D; 1;        edge[j][i] &#x3D; 1;    &#125;&#125;template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::DFS(int v) &#123;    cout &lt;&lt; vertex[v];    visited[v] &#x3D; 1;    for (int j &#x3D; 0; j &lt; vertexNum; j++) &#123;        if (edge[v][j] &#x3D;&#x3D; 1 &amp;&amp; visited[j] &#x3D;&#x3D; 0)            DFS(j);    &#125;&#125;template &lt;class DateType&gt;void MGraph&lt;DataType&gt;::BFS(int v) &#123;    int w, j, queue[maxSize] &#x3D; &#123;0&#125;;    for (int i &#x3D; 0; i &lt; vertexNum; i++) &#123;        visited[i] &#x3D; 0;    &#125;    int front &#x3D; -1, rear &#x3D; -1;    cout &lt;&lt; vertex[v];    queue[++rear] &#x3D; v;    while (front !&#x3D; rear) &#123;        v &#x3D; queue[++front];        for (j &#x3D; 0; j &lt; vertexNum; j++) &#123;            if (edge[v][j] &#x3D;&#x3D; 1 &amp;&amp; visited[j] &#x3D;&#x3D; 0) &#123;                cout &lt;&lt; vertex[j];                visited[j] &#x3D; 1;                queue[++rear] &#x3D; j;            &#125;        &#125;    &#125;&#125;int main() &#123;    char ch[] &#x3D; &#123;&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;, &#39;e&#39;, &#39;f&#39;&#125;;    int i;    MGraph&lt;char&gt; MG(ch, 6, 6);    for (i &#x3D; 0; i &lt; maxSize; i++) &#123;        visited[i] &#x3D; 0;    &#125;    cout &lt;&lt; &quot;DFS order: \n&quot;;    MG.DFS(0);    cout &lt;&lt; &quot;\n&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;\n&quot;;    cout &lt;&lt; &quot;BFS order: \n&quot;;    MG.BFS(0);&#125;</code></pre><h4 id="prim">prim</h4><h5 id="结构-1">结构</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">const int maxSize &#x3D; 100;int visited[maxSize] &#x3D; &#123;0&#125;;template &lt;class DataType&gt;class MGraph &#123;    public:     MGraph(DataType a[], int n, int e);     ~MGraph() &#123;&#125;     void DFS(int);     void BFS(int);     void Prim(int);     int minEdge(int[], int);        private:     DataType vertex[maxSize];     int edge[maxSize][maxSize];     int vertexNum, edgeNum;&#125;;</code></pre><h5 id="构造函数-1">构造函数</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;MGraph&lt;DataType&gt;::MGraph(DataType a[], int n, int e) &#123;    int i, j, w &#x3D; 0;    vertexNum &#x3D; n;    edgeNum &#x3D; e;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        vertex[i] &#x3D; a[i];    &#125;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        for (j &#x3D; 0; j &lt; vertexNum; j++) &#123;            if (i &#x3D;&#x3D; j)                 edge[i][j] &#x3D; 0; &#x2F;&#x2F; 这里忽略自环            else                edge[i][j] &#x3D; 100;            &#x2F;&#x2F; 这里初始化权值，赋比较大的数即可        &#125;    &#125;    for (int k &#x3D; 0; k &lt; edgeNum; k++) &#123;        cout &lt;&lt; &quot;input two points of the edge: &quot;;        cin &gt;&gt; i &gt;&gt; j;        cout &lt;&lt; &quot;input the weight of the edge: &quot; ;        cin &gt;&gt; w;        edge[i][j] &#x3D; w;        edge[j][i] &#x3D; w;    &#125;&#125;</code></pre><h5 id="prim代码实现">prim代码实现</h5><p>将图中顶点（V）分两部分，最小生成树的点集为U，其余顶点在集合（V-U）</p><ul><li><ol type="1"><li>首先任取一个点作为起点</li></ol></li><li><ol start="2" type="1"><li>在V-U中找和起点之间权值最小的边</li></ol></li><li><ol start="3" type="1"><li>adjVex记录上一轮找最小值的位置，cost记录到各顶点的距离</li></ol></li><li><ol start="4" type="1"><li>然后上一轮找到的权值最小的边的另一个点作为起点，不断重复步骤2，3</li></ol></li></ul><h6 id="找到最小值位置">找到最小值位置</h6><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;int MGraph&lt;DataType&gt;::minEdge(int r[], int n) &#123;    int index;    int min &#x3D; 100; &#x2F;&#x2F; 图中all权值最大不超过100    for (int i &#x3D; 0; i &lt; n; i++) &#123;        if (r[i] !&#x3D; 0 &amp;&amp; r[i] &lt; min) &#123;            min &#x3D; r[i];            index &#x3D; i;        &#125;    &#125;    return index; &#x2F;&#x2F; 返回最小值在数组中的位置&#125;</code></pre><h6 id="prim核心代码">prim核心代码</h6><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::Prim(int v) &#123;    int adjVex[maxSize], cost[maxSize];    int i, j, k;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        &#x2F;&#x2F; 通过起点对adjVex，cost数组初始化        cost[i] &#x3D; edge[v][i];        adjVex[i] &#x3D; v;        &#x2F;&#x2F; 将起点所有有联通的点都录入cost中，找权值最小的边(类似BFS)    &#125;    cost[v] &#x3D; 0; &#x2F;&#x2F; 将顶点加入u中    for (k &#x3D; 1; k &lt; vertexNum; k++) &#123;        j &#x3D; minEdge(cost, vertexNum); &#x2F;&#x2F; 在cost数组找最小值        &#x2F;&#x2F; cout &lt;&lt; &#39;(&#39; &lt;&lt; adjVex[j] &lt;&lt; &#39;,&#39; &lt;&lt; j &lt;&lt; &#39;)&#39; &lt;&lt; cost[j] &lt;&lt; endl; &#x2F;&#x2F; 输出的是点的序号        cout &lt;&lt; &#39;(&#39; &lt;&lt; vertex[j] &lt;&lt; &#39;,&#39; &lt;&lt; vertex[adjVex[j]] &lt;&lt; &#39;)&#39; &lt;&lt; cost[j] &lt;&lt; endl;  &#x2F;&#x2F; 输出的是字符        &#x2F;&#x2F; 输出生成最小生成树的过程(都是输出上一轮查找结果)        cost[j] &#x3D; 0; &#x2F;&#x2F; 将最小值的点加入U中(清零当前最小值的权值，防止后面重复遍历)        for (int p &#x3D; 0; p &lt; vertexNum; p++) &#123;            &#x2F;&#x2F; 这一步，是以第j号为起点，不断寻找和j号联通的最小权值的路线            if (edge[p][j] &lt; cost[p]) &#123;                &#x2F;&#x2F; 从所有与当前最小值临界点出发找到最小值点权值最小的                cost[p] &#x3D; edge[p][j];                adjVex[p] &#x3D; j; &#x2F;&#x2F; 记录新加入顶点上一轮迭代的最小值的位置            &#125;        &#125;    &#125;&#125;</code></pre><h5 id="测试用例及邻接矩阵">测试用例及邻接矩阵</h5><pre class="line-numbers language-none"><code class="language-none">0 1340 2460 5191 4122 3172 5253 4383 5254 526</code></pre><pre class="line-numbers language-c" data-language="c"><code class="language-c">   0  1  2  3  4  5 0&#39;    34 46       19 &#39;1&#39; 34          12    &#39;2&#39; 46       17    25 &#39;3&#39;       17    38    &#39;4&#39;    12    38    26 &#39; 5&#39; 19    25    26    &#39;&#x2F;&#x2F; 以横轴为x，竖轴为y       </code></pre><h4 id="dijkstra">Dijkstra</h4><h5 id="结构-2">结构</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">const int maxSize &#x3D; 100;int visited[maxSize] &#x3D; &#123;0&#125;; &#x2F;&#x2F; 到后面发现，visited在无向图中设计的是真的巧妙template &lt;class DataType&gt;class MGraph &#123;    public:      MGraph(DataType a[], int n, int e); &#x2F;&#x2F; 构造函数，构造有n个顶点e条边的图     ~MGraph() &#123;&#125;     void DFS(int); &#x2F;&#x2F; 深搜     void BFS(int); &#x2F;&#x2F; 广搜        private:     DataType vertex[maxSize]; &#x2F;&#x2F; 存放图中顶点的数组     int edge[maxSize][maxSize]; &#x2F;&#x2F; 存放图中边的数组     int vertexNum, edgeNum; &#x2F;&#x2F; 图中的顶点数和变数&#125;;</code></pre><h5 id="构造函数-2">构造函数</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;MGraph&lt;DataType&gt;::MGraph(DataType a[], int n, int e) &#123;    vertexNum &#x3D; e, edgeNum &#x3D; n;    int i, j, w &#x3D; 0;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        vertex[i] &#x3D; a[i];    &#125;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        for (j &#x3D; 0; j &lt; vertexNum; j++) &#123;            if (i &#x3D;&#x3D; j)                 edge[i][j] &#x3D; 0;            else                edge[i][j] &#x3D; 100;        &#125;        for (int k &#x3D; 0; k &lt; edgeNum; k++) &#123;            cout &lt;&lt; &quot;input two vertexes of the edge: &quot;;            cin &gt;&gt; i &gt;&gt; j;            cout &lt;&lt; &quot;input the weight of the edge: &quot;;            cin &gt;&gt; w;            edge[i][j] &#x3D; w;        &#125;    &#125;&#125;</code></pre><h5 id="深搜和广搜">深搜和广搜</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::DFS(int v) &#123;    cout &lt;&lt; vertex[v];    visited[v] &#x3D; 1;    for (int i &#x3D; 0; i &lt; vertexNum; i++) &#123;        if (edge[v][i] &lt; 100 &amp;&amp; visited[i] &#x3D;&#x3D; 0)             DFS(i);    &#125;&#125;template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::BFS(int v) &#123;    int queue[maxSize];    int front &#x3D; -1, rear &#x3D; -1;    cout &lt;&lt; vertex[v];    visited[v] &#x3D; 1;    queue[++rear] &#x3D; v;    while (front !&#x3D; rear) &#123;        v &#x3D; queue[++front];        for (int j &#x3D; 0; j &lt; vertexNum; j++) &#123;            if (edge[v][j] &lt; 100 &amp;&amp; visited[j] &#x3D;&#x3D; 0) &#123;                cout &lt;&lt; vertex[v];                visited[j] &#x3D; 1;                queue[++rear] &#x3D; 1;            &#125;        &#125;    &#125;&#125;</code></pre><h5 id="dijkstra-1">Dijkstra</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::Dijkstra(MGraph&lt;DataType&gt; mg, int v) &#123;    int dist[maxSize];    &#x2F;&#x2F; dist为起点到各个点的距离，具有临时性    string path[maxSize];    string vertex(mg.vertex);    for (int i &#x3D; 0; i &lt; mg.vertexNum; i++) &#123;        dist[i] &#x3D; mg.edge[v][i];        &#x2F;&#x2F; 初始化dist数组，0号顶点到其余各顶点的初始路程        if (dist[i] !&#x3D; 100) &#123;            path[i] +&#x3D; vertex[v];            path[i] +&#x3D; vertex[i];            &#x2F;&#x2F; 这里是记录起点可以直达的路径        &#125; else &#123;            path[i] &#x3D; &quot;&quot;;        &#125;    &#125;    dist[v] &#x3D; 0;    int num &#x3D; 1;    while (num &lt; mg.vertexNum) &#123;        int min &#x3D; 255, k &#x3D; 0; &#x2F;&#x2F; 每一轮重置最小值        for (int i &#x3D; 0; i &lt; mg.vertexNum; i++) &#123;            if (dist[i] !&#x3D; 0 &amp;&amp; dist[i] &lt; min) &#123;                min &#x3D; dist[i];                k &#x3D; i;                &#x2F;&#x2F; 找最小值            &#125;        &#125;        &#x2F;&#x2F; cout &lt;&lt; path[k] &lt;&lt; &#39;,&#39; &lt;&lt; dist[k] &lt;&lt; &quot;;\n&quot;;        num++; &#x2F;&#x2F; 标记这是第几个被访问的点        for (int i &#x3D; 0; i &lt; mg.vertexNum; i++) &#123;            if (dist[i] &gt; dist[k] + mg.edge[k][i]) &#123;                dist[i] &#x3D; dist[k] + mg.edge[k][i];                path[i] &#x3D; &quot;&quot;; &#x2F;&#x2F; 重置路径                path[i] +&#x3D; path[k]; &#x2F;&#x2F; 加上之前走过的路                path[i] +&#x3D; vertex[i];            &#125;        &#125;        cout &lt;&lt; path[k] &lt;&lt; &#39;,&#39; &lt;&lt; dist[k] &lt;&lt; &quot;;\n&quot;;        dist[k] &#x3D; 0;    &#125;&#125;</code></pre><h5 id="测试用例及邻接矩阵-1">测试用例及邻接矩阵</h5><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">0 1100 3300 41001 2502 4103 2203 460</code></pre><pre class="line-numbers language-c" data-language="c"><code class="language-c">   0   1   2   3   40          1&#39; 10                 &#39;2&#39;     50      20     &#39;3&#39; 30                 &#39;4&#39; 100     10  60     &#39;&#x2F;&#x2F; 横轴为x，竖轴为y       </code></pre><h5 id="完整代码-1">完整代码</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include &lt;bits&#x2F;stdc++.h&gt;using namespace std;const int maxSize &#x3D; 100;int visited[maxSize] &#x3D; &#123;0&#125;;template &lt;class DataType&gt;struct MGraph &#123;    public:     MGraph(DataType a[], int n, int e);     ~MGraph() &#123;&#125;     void DFS(int);     void BFS(int);     void Dijkstra(MGraph&lt;DataType&gt;, int);        private:     DataType vertex[maxSize];     int edge[maxSize][maxSize];     int vertexNum, edgeNum;&#125;;template &lt;class DataType&gt;MGraph&lt;DataType&gt;::MGraph(DataType a[], int n, int e) &#123;    int i, j, w &#x3D; 0;    vertexNum &#x3D; n, edgeNum &#x3D; e;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        vertex[i] &#x3D; a[i];    &#125;    for (i &#x3D; 0; i &lt; vertexNum; i++) &#123;        for (j &#x3D; 0; j &lt; vertexNum; j++) &#123;            if (i &#x3D;&#x3D; j)                edge[i][j] &#x3D; 0;            else                edge[i][j] &#x3D; 100;        &#125;    &#125;    for (int k &#x3D; 0; k &lt; edgeNum; k++) &#123;        cout &lt;&lt; &quot;input two vertexes of the edge: &quot;;        cin &gt;&gt; i &gt;&gt; j;        cout &lt;&lt; &quot;input the weight of the edge: &quot;;        cin &gt;&gt; w;        edge[i][j] &#x3D; w;    &#125;&#125;template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::DFS(int v) &#123;    cout &lt;&lt; vertex[v];    visited[v] &#x3D; 1;    for (int i &#x3D; 0; i &lt; vertexNum; i++) &#123;        if (edge[v][i] &lt; 100 &amp;&amp; visited[i] &#x3D;&#x3D; 0)            DFS(i);    &#125;&#125;template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::BFS(int v) &#123;    int queue[maxSize];    int front &#x3D; -1, rear &#x3D; -1;    cout &lt;&lt; vertex[v];    visited[v] &#x3D; 1;    queue[++rear] &#x3D; v;    while (front !&#x3D; rear) &#123;        v &#x3D; queue[++front];        for (int j &#x3D; 0; j &lt; vertexNum; j++) &#123;            if (edge[v][j] &lt; 100 &amp;&amp; visited[j] &#x3D;&#x3D; 0) &#123;                cout &lt;&lt; vertex[j];                visited[j] &#x3D; 1;                queue[++rear] &#x3D; j;            &#125;        &#125;    &#125;&#125;template &lt;class DataType&gt;void MGraph&lt;DataType&gt;::Dijkstra(MGraph&lt;DataType&gt; mg, int v) &#123;    int distance[maxSize];    string path[maxSize];    string vertex(mg.vertex);    for (int i &#x3D; 0; i &lt; mg.vertexNum; i++) &#123;        distance[i] &#x3D; mg.edge[v][i];        if (dist[i] !&#x3D; 100) &#123;            path[i] +&#x3D; vertex[v];            path[i] +&#x3D; vertex[i];        &#125; else &#123;            path[i] &#x3D; &quot;&quot;;        &#125;    &#125;    distance[v] &#x3D; 0;    int num &#x3D; 1;    while (num &lt; mg.vertexNum) &#123;        int min &#x3D; 100, k &#x3D; 0;        for (int i &#x3D; 0; i &lt; mg.vertexNum; i++) &#123;            if (distance[i] !&#x3D; 0 &amp;&amp; distance[i] &lt; min) &#123;                min &#x3D; distance[i];                k &#x3D; i;            &#125;        &#125;        num++;        for (int i &#x3D; 0; i &lt; mg.vertexNum; i++) &#123;            if (distance[i] &gt; distance[k] + mg.edge[v][i]) &#123;                distance[i] &#x3D; distance[i] + mg.edge[k][i];                path[i] &#x3D; &quot;&quot;;                path[i] +&#x3D; path[k];                path[i] +&#x3D; vertex[i];            &#125;        &#125;        cout &lt;&lt; path[k] &lt;&lt; &#39;,&#39; &lt;&lt; distance[k] &lt;&lt; &quot;;\n&quot;;        distance[k] &#x3D; 0;    &#125;&#125;int main() &#123;    char ch[] &#x3D; &#123;&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;, &#39;e&#39;&#125;;    MGraph&lt;char&gt; mg(ch, 5, 7);    for (int i &#x3D; 0; i &lt; maxSize; i++) &#123;        visited[i] &#x3D; 0;    &#125;    cout &lt;&lt; &quot;DFS: &quot;;    mg.DFS(0);    cout &lt;&lt; endl;    for (int i &#x3D; 0; i &lt; maxSize; i++) &#123;        visited[i] &#x3D; 0;    &#125;    cout &lt;&lt; &quot;BFS: &quot;;    mg.BFS(0);    cout &lt;&lt; &quot;the short path: \n&quot;;    mg.Dijkstra(mg, 0);&#125;</code></pre><h4 id="参考链接">参考链接</h4><p><a href="https://blog.csdn.net/zgsdlr/article/details/121426826">【数据结构】最小生成树Prim算法_zgsdlr的博客-CSDN博客_ java求最小生成树</a></p>]]></content>
    
    
    <summary type="html">填坑系列之图，整理了数据结构无向图、最小生成树(Prim和Dijkstra)</summary>
    
    
    
    <category term="C++" scheme="https://jaydenchang.top/categories/C/"/>
    
    
    <category term="数据结构" scheme="https://jaydenchang.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="C++" scheme="https://jaydenchang.top/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>数据结构--哈夫曼树与哈夫曼编码</title>
    <link href="https://jaydenchang.top/post/0x0028.html"/>
    <id>https://jaydenchang.top/post/0x0028.html</id>
    <published>2021-11-19T16:00:00.000Z</published>
    <updated>2021-12-11T10:14:34.545Z</updated>
    
    <content type="html"><![CDATA[<p>填坑系列之哈夫曼树</p><p>刚开始看哈夫曼树时有点懵懵的，加权是啥子玩意，后面查阅资料后才明白，哈夫曼树以及哈夫曼编码多用在压缩编码中，再配合二倍速食用B站大学的网课，算是把整个算法过了一遍</p><h4 id="the-main-structure">the main structure</h4><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">const int Max &#x3D; 1000;char **HuffmanCode;typedef struct Node &#123;    int weight;    int parent, left, right;&#125; HTNode, *HuffmanTree;</code></pre><h4 id="select">Select</h4><p>select是来选择剩余结点中权值最小的两颗二叉树(包括新构造的树)的左右子树来构建一个新的二叉树，新根节点权值为其左右子树根节点的权值之和</p><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">void Select(HuffmanTree ht, int k, int&amp; id1, int&amp; id2) &#123;    long min1, min2;    min1 &#x3D; min2 &#x3D; 99999; &#x2F;&#x2F; 不能太小    for (int i &#x3D; 0;i &lt; k;i++) &#123;        if (ht[i].parent &#x3D;&#x3D; -1 &amp;&amp; min1 &gt; ht[i].weight) &#123;            &#x2F;&#x2F; 选择无双亲的结点            if (min1 &lt; min2) &#123;                 &#x2F;&#x2F; 这里是比大小的操作，规定min1为小                min2 &#x3D; min1;                id2 &#x3D; id1;            &#125;            min1 &#x3D; ht[i].weight; &#x2F;&#x2F; 这个操作可以把这k个数据都遍历一遍，可以选出两个最小的结点            id1 &#x3D; i;        &#125; else if (ht[i].parent &#x3D;&#x3D; -1 &amp;&amp; min2 &gt; ht[i].weight) &#123;            min2 &#x3D; ht[i].weight;            id2 &#x3D; i;        &#125;    &#125;&#125;</code></pre><h4 id="create-a-huffman-tree">Create a Huffman Tree</h4><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">void HuffmanTree(huffTree &amp;ht, int n) &#123;    int m &#x3D; n * 2 - 1;    int id1, id2;    int i;    if (n &lt; 0) &#x2F;&#x2F; 创建空树        return;    ht &#x3D; new Node[m];    for (i &#x3D; 0;i &lt; m;i++) &#123;        ht[i].parent &#x3D; ht[i].left &#x3D; ht[i].right &#x3D; -1;        &#x2F;&#x2F; 初始化各节点    &#125;    for (i &#x3D; 0;i &lt; n;i++) &#123;        cin &gt;&gt; ht[i].weight; &#x2F;&#x2F; 输入各个结点的权值    &#125;    for (i &#x3D; n;i &lt; m;i++) &#123;        Select(ht, i, id1, id2);        &#x2F;&#x2F; 在n个结点中选择俩无双亲的结点且权值最小的结点        ht[id1].parent &#x3D; ht[id2].parent &#x3D; i;        &#x2F;&#x2F; 获得id1，id2，把第i个结点设为它俩的双亲        ht[i].left &#x3D; id1;        ht[i].right &#x3D; id2; &#x2F;&#x2F; 设第i个结点的左右孩子为id1，id2        ht[i].weight &#x3D; ht[id1].weight + ht[id2].weight;    &#125;&#125;</code></pre><h4 id="destroy-a-huffman-tree">destroy a Huffman Tree</h4><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">void Destroy(HuffmanTree &amp;ht) &#123;    delete[] ht;    ht &#x3D; NULL;&#125;</code></pre><h4 id="create-the-huffman-code">create the Huffman Code</h4><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">void createHuffmanCode(HuffmanTree ht, HuffmanCode &amp;hc, int n) &#123;    int start;    int cur, f;    hc &#x3D; new char *[n + 1];    char *cd &#x3D; new char[n];    cd[n - 1] &#x3D; &#39;\0&#39;;    for (i &#x3D; 0;i &lt; n;i++) &#123;        start &#x3D; n - 1;         cur &#x3D; i; &#x2F;&#x2F; 当前结点在数组中的位置        f &#x3D; hf[i].parent; &#x2F;&#x2F; 当前结点的父节点在数组的位置        while (f !&#x3D; 0) &#123;            &#x2F;&#x2F; 如果该结点是父节点的左孩子则对应编码为0，否则右孩子为1            start--;            if (hf[f].left &#x3D;&#x3D; cur)                cd[start] &#x3D; &#39;0&#39;;            else                 cd[start] &#x3D; &#39;1&#39;;            &#x2F;&#x2F; 以父节点为孩子结点，继续朝树根的方向遍历            cur &#x3D; f;            f &#x3D; hf[f].parent;        &#125;        &#x2F;&#x2F; 跳出循环后，cd数组中从下标start开始，存放的就是该结点的哈夫曼编码        hc[i] &#x3D; new char[n - start];        strcpy(hc[i], &amp;cd[start]);    &#125;    delete cd;&#125;</code></pre><h4 id="the-code">the code</h4><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include &lt;bits&#x2F;stdc++.h&gt;using namespace std;const int Max &#x3D; 9999;typedef char **HuffmanCode;typedef struct Node &#123;    int weight;    int parent, left, right;&#125; HTNode, *HuffmanTree;void Select(HuffmanTree ht, int k, int id1, int id2) &#123;    int min1 &#x3D; min2 &#x3D; 9999;for (int i &#x3D; 0;i &lt; k;i++) &#123;        if (ht[i].parent &#x3D;&#x3D; -1 &amp;&amp; min1 &lt; ht[i].weight) &#123;            if (min1 &lt; min2) &#123;                min2 &#x3D; min1;                id2 &#x3D; id1;            &#125;            id1 &#x3D; i;            min1 &#x3D; ht[i].weight;        &#125; else if (ht[i].parent &#x3D;&#x3D; -1 &amp;&amp; min2 &gt; ht[i].weight) &#123;            min2 &#x3D; ht[i].weight;            id2 &#x3D; i;        &#125;    &#125;&#125;void createHuffmanTree(HuffmanTree ht, int n) &#123;    int id1, id2;    if (n &lt;&#x3D; 0)         return;    for (int i &#x3D; 0; i &lt; n; i++) &#123;        ht[i].left &#x3D; ht[i].right &#x3D; ht[i].parent &#x3D; 0;    &#125;    for (int i &#x3D; 0;i &lt; n;i++) &#123;        cin &gt;&gt; ht[i].weight;    &#125;    for (int i &#x3D; 0;i &lt; n;i++) &#123;        Select(ht, i, id1, id2);        ht[id1].parent &#x3D; ht[id2].parent &#x3D; i;        ht[i].left &#x3D; id1;        ht[i].right &#x3D; id2;        ht[weight] &#x3D; ht[id1].weight + ht[id2].weight;    &#125;&#125;void createHuffmanTreeCode(HuffmanTree ht, HuffmanCode &amp;hc, int n) &#123;    int start, cur f;    hc &#x3D; new char*[n + 1];    char* cd &#x3D; new char[n];    cd[n - 1] &#x3D; &#39;\0&#39;;    for (int i &#x3D; 0;i &lt; n;i++) &#123;        start &#x3D; n - 1;        cur &#x3D; i;        f &#x3D; ht[i].parent;        while (f !&#x3D; 0) &#123;            start--;            if (ht[f].left &#x3D;&#x3D; cur) &#123;                cd[start] &#x3D; &#39;0&#39;;            &#125; else &#123;                cd[start] &#x3D; &#39;1&#39;;            &#125;            cur &#x3D; f;            f &#x3D; ht[i].parent;        &#125;        hc[i] &#x3D; cd[n - start];        strcpy(hc[i], &amp;cd[start]);    &#125;    delete cd;&#125;int main() &#123;    int n;    cin &gt;&gt; n;    HuffmanTree ht;    HuffmanCode hc;    int sum &#x3D; 0;    HuffmanTree(ht, n);    createHuffmanCode(ht, hc, n);    for (int i &#x3D; 0;i &lt; 2 * n - 1;i++) &#123;        cout &lt;&lt; ht[i].weight &lt;&lt; &#39; &#39;; &#x2F;&#x2F; 测试，输出所有结点，包括非原有结点    &#125;    cout &lt;&lt; endl;    for (int i &#x3D; 0;i &lt; n;i++) &#123;        cout &lt;&lt; hc[i] &lt;&lt; &#39; &#39;; &#x2F;&#x2F; 输出每个结点的HuffmanCode    &#125;&#125;</code></pre><h4 id="参考链接">参考链接</h4><p><a href="https://www.cnblogs.com/linfangnan/p/12593480.html">数据结构：哈夫曼树与哈夫曼编码 - 乌漆WhiteMoon - 博客园 (cnblogs.com)</a></p><p><a href="https://www.bilibili.com/video/BV18t411U7Tb">数据结构与算法基础--第09周04--5.7哈夫曼树及其应用4-5.7.2哈夫曼树的构造算法2-哈夫曼树算法实现_哔哩哔哩_bilibili</a>(共6p)</p>]]></content>
    
    
    <summary type="html">填坑系列之哈夫曼树，整理了哈夫曼树和哈夫曼编码</summary>
    
    
    
    <category term="C++" scheme="https://jaydenchang.top/categories/C/"/>
    
    
    <category term="数据结构" scheme="https://jaydenchang.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="C++" scheme="https://jaydenchang.top/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>数据结构--简单二叉树(无序)</title>
    <link href="https://jaydenchang.top/post/0x0027.html"/>
    <id>https://jaydenchang.top/post/0x0027.html</id>
    <published>2021-11-15T16:00:00.000Z</published>
    <updated>2021-12-11T05:45:42.062Z</updated>
    
    <content type="html"><![CDATA[<p>本次来简单总结下简单二叉树(无序)，前面欠的债有点多，最近在疯狂追赶课程进度，简单记录下自己对简单二叉树的一些理解</p><h4 id="binary-tree">binary tree</h4><h5 id="the-main-structure">the main structure</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">const int Max &#x3D; 100;template &lt;class DataType&gt;struct BiNode &#123;    DataType data;    BiNode *leftChild, *rightChild;    &#125;;template &lt;class DataType&gt;class BiTree &#123;    public:     BiTree() &#123; root &#x3D; Create(); &#125;     ~BiTree() &#123; Release(root); &#125;     void PreOrder() &#123; PreOrder(root); &#125;     void InOrder() &#123; InOrder(root); &#125;     void LevelOrder();     int leafNum(BiNode*);     BiNode* getRoot() &#123; return root; &#125;        private:     BiNode* root;     BiNode* Create();     void Release(BiNode* bt);     void PreOrder(BiNode* bt);     void InOrder(BiNode* bt);     void PostOrder(BiNode* bt);&#125;;</code></pre><h5 id="create">Create()</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;BiTree::BiTree() &#123;    BiNode* bt;    DataType ch;    cout &lt;&lt; &quot;enter a binary node: &quot;;    cin &gt;&gt; ch;    if (ch &#x3D;&#x3D; &#39;#&#39;) &#123;        return NULL;    &#125; else &#123;        bt &#x3D; new BiNode;        bt-&gt;data &#x3D; ch;        bt-&gt;leftChild &#x3D; Create();        bt-&gt;rightchild &#x3D; Create();        &#x2F;&#x2F; 不断套娃递归    &#125;&#125;</code></pre><h5 id="release">Release()</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;BiTree::Release(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        Release(bt-&gt;leftChild);        Release(bt-&gt;rightChild);        delete bt;    &#125;&#125;</code></pre><h5 id="preorder">PreOrder</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void BiTree::PreOrder(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        cout &lt;&lt; bt-&gt;data &lt;&lt; &#39; &#39;;        PreOrder(bt-&gt;leftChild);        PreOrder(bt-&gt;rightChild);    &#125;&#125;</code></pre><h5 id="inorder">InOrder()</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void BiTree::InOrder(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        InOrder(bt-&gt;leftChild);        cout &lt;&lt; bt-&gt;data &lt;&lt; &#39; &#39;;        InOrder(bt-&gt;rightChild);    &#125;&#125;</code></pre><h5 id="postorder">PostOrder()</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void BiTree::PostOrder(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        PostOrder(bt-&gt;leftChild);        PostOrder(bt-&gt;rightChild);        cout &lt;&lt; bt-&gt;data &lt;&lt; &#39; &#39;;    &#125;&#125;</code></pre><h5 id="levelorder">LevelOrder()</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;void BiTree::LevelOrder() &#123;    BiNode *queue[Max], *ptr &#x3D; NULL;    int front &#x3D; -1, rear &#x3D; -1;    if (root &#x3D;&#x3D; NULL)         return;        queue[++rear] &#x3D; root; &#x2F;&#x2F; 根节点入队    while (front !&#x3D; rear) &#123;        ptr &#x3D; queue[++front]; &#x2F;&#x2F; 把根节点赋给临时指针ptr        cout &lt;&lt; ptr-&gt;data &lt;&lt; &#39; &#39;; &#x2F;&#x2F; 输出当前结点的内容               if (ptr-&gt;leftChild !&#x3D; NULL)            queue[++rear] &#x3D; ptr-&gt;leftChild;        if (ptr-&gt;rightChild !&#x3D; NULL)            queue[++rear] &#x3D; ptr-&gt;rightChild;        &#x2F;&#x2F; 这里依次遍历左右左右孩子节点并添加入列    &#125;&#125;</code></pre><h5 id="leafnum">leafNum()</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">template &lt;class DataType&gt;int BiTree::leafNum(BiNode* bt)&#123;    if (bt &#x3D;&#x3D; NULL)        return 0;    if (bt-&gt;leftChild &#x3D;&#x3D; NULL &amp;&amp; bt-&gt;rightChild &#x3D;&#x3D; NULL)         return 1;    int left &#x3D; leafNum(bt-&gt;leftChild);    int right &#x3D; leafNum(bt-&gt;rightChild);    return left + right;&#125;</code></pre><h5 id="complete-code">Complete code</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include &lt;bits&#x2F;stdc++.h&gt;using namespace std;const int Max &#x3D; 100;template &lt;class DataType&gt;struct BiNode &#123;    DataType data;    BiNode *leftChild, *rightChild;&#125;;template &lt;class DataType&gt;class BiTree &#123;    public:     BiTree() &#123; root &#x3D; Create(); &#125;     ~BiTree() &#123;Release(root); &#125;     void PreOrder() &#123; PreOrder(root); &#125;     void InOrder() &#123; InOrder(root); &#125;     void PostOrder() &#123; PostOrder(root); &#125;     void LevelOrder();     int leafNum(BiNode*);     BiNode* getRoot() &#123; return root; &#125;        private:     BiNode* root;     BiNode* Create();     void Release(BiNode*);     void PreOrder(BiNode*);     void InOrder(BiNode*);     void PostOrder(BiNode*);&#125;;template &lt;class DataType&gt;BiNode* BiTree::Create() &#123;    BiNode* bt;    DataType ch;    cout &lt;&lt; &quot;enter a node data: &quot;;    cin &gt;&gt; ch;    if (ch !&#x3D; &#39;#&#39;) &#123;        bt-&gt;data &#x3D; ch;        bt-&gt;leftChild &#x3D; Create();        bt-&gt;rightChild &#x3D; Create();        return bt;    &#125;&#125;template &lt;class DataType&gt;void BiTree::Release(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        Release(bt-&gt;leftChild);        Release(bt-&gt;rightChild);        delete bt;    &#125;&#125;template &lt;class DataType&gt;void BiTree::PreOrder(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        cout &lt;&lt; bt-&gt;data &lt;&lt; &#39; &#39;;        PreOrder(bt-&gt;leftChild);        PreOrder(bt-&gt;rightChild);    &#125;&#125;template &lt;class DataType&gt;void BiTree::InOrder(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        InOrder(bt-&gt;leftChild);        cout &lt;&lt; bt-&gt;data &lt;&lt; &#39; &#39;;        InOrder(bt-&gt;rightChild);    &#125;&#125;template &lt;class DataType&gt;void BiTree::PostOrder(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL) &#123;        return;    &#125; else &#123;        PostOrder(bt-&gt;leftChild);        PostOrder(bt-&gt;rightChild);        cout &lt;&lt; bt-&gt;data &lt;&lt; &#39; &#39;;    &#125;&#125;template &lt;class DataType&gt;void BiTree::LevelOrder() &#123;    BiNode* queue[Max], ptr &#x3D; NULL;    int front &#x3D; -1, rear &#x3D; -1;    if (root &#x3D;&#x3D; NULL)        return;queue[++rear] &#x3D; root;    while (front !&#x3D; rear) &#123;        ptr &#x3D; queue[++front];        cout &lt;&lt; ptr-&gt;data &lt;&lt; &#39; &#39;;        if (ptr-&gt;leftChild !&#x3D; NULL)            queue[++rear] &#x3D; ptr-&gt;leftChild;        if (ptr-&gt;rightChild !&#x3D; NULL)            queue[++rear] &#x3D; ptr-&gt;rightChild;    &#125;&#125;template &lt;class DataType&gt;int BiTree::leafNum(BiNode* bt) &#123;    if (bt &#x3D;&#x3D; NULL)        return 0;    if (bt-&gt;leftChild &#x3D;&#x3D; NULL &amp;&amp; bt-&gt;rightChild &#x3D;&#x3D; NULL)        return 1;    int left &#x3D; leafNum(bt-&gt;leftChild);    int right &#x3D; leafNum(bt-&gt;rightChild);    return left + right;    &#125;int main() &#123;    BiTree tree;    cout &lt;&lt; &quot;---PreOrder---\n&quot;;    tree.PreOrder();    cout &lt;&lt; endl;    cout &lt;&lt; &quot;---InOrder---\n&quot;;    tree.InOrder();    cout &lt;&lt; endl;    cout &lt;&lt; &quot;---PostOrder---\n&quot;;    tree.PostOrder();    cout &lt;&lt; endl;    cout &lt;&lt; &quot;---LevelOrder---\n&quot;;    tree.LevelOrder();    cout &lt;&lt; endl;    cout &lt;&lt; &quot;the num of the leaves: &quot;;    cout &lt;&lt; tree.leafNum(tree.getRoot());&#125;</code></pre>]]></content>
    
    
    <summary type="html">整理了简单二叉树的简单应用，包括前中后遍历，求树的深度、叶子节点个数</summary>
    
    
    
    <category term="C++" scheme="https://jaydenchang.top/categories/C/"/>
    
    
    <category term="数据结构" scheme="https://jaydenchang.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="C++" scheme="https://jaydenchang.top/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>kmp</title>
    <link href="https://jaydenchang.top/post/0x0026.html"/>
    <id>https://jaydenchang.top/post/0x0026.html</id>
    <published>2021-10-30T16:00:00.000Z</published>
    <updated>2022-08-16T14:23:32.545Z</updated>
    
    <content type="html"><![CDATA[<p>注：本篇文章只记录我理解的过程、需要注意的小细节，不涉及具体讲解，一些具体的原理、推导步骤可参考文末我列出的文章和视频</p><p>说到字符串匹配，以前的我，对时间、空间复杂度没有什么概念，估计写出来的代码长这样</p><h3 id="bfbrute-force">BF(brute-force)</h3><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">int BF(string s, string p) &#123;    int len1 &#x3D; s.length();    int len2 &#x3D; p.length();    int i &#x3D; 0; j &#x3D; 0;    while (i &lt; len1 &amp;&amp; j &lt; len2) &#123;        if (s[i] &#x3D;&#x3D; p[j]) &#123;            i++, j++;            &#x2F;&#x2F; 匹配成功就指针都后移        &#125; else &#123;            i &#x3D; i - j + 1;            j &#x3D; 0;        &#125;    &#125;    if (j &#x3D;&#x3D; len2)        return 1;    else         return -1;&#125;</code></pre><p>查阅资料后发现，这￥%&amp;#时间复杂度还挺高，假设文本串长m，模式串长n，时间复杂度是<code>O(m*n)</code>，如果m和n都很大的话，效率会低到无法想象</p><h3 id="kmp">kmp</h3><p>这时候，引入一个新算法，kmp，反正就是三个大佬的名字首字母拼在一起</p><p>要理解kmp，首先要理解kmp中的next数组，next数组，说人话，就类似一个索引。kmp的本质就是利用模式串的最长公共前后缀来缩短查找时间</p><p>如果字符失配，模式串向后移动<code>j-next[j]</code>位，这样说还不是很好理解，看一个例子</p><table><thead><tr class="header"><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>d</th><th>e</th></tr></thead><tbody><tr class="odd"><td>a</td><td>b</td><td>c</td><td>d</td><td>a</td><td>b</td><td><u><strong>d</strong></u></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table><p>这里在第七位失配，在d前面，有最长公共前后缀"ab"，长度为2，按照刚才说的，模式串向后移动<code>j-next[j]</code>位，即<code>6-next[6]</code>，其中<code>next[6] = 2</code>，变成如下</p><table><thead><tr class="header"><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>d</th><th>e</th></tr></thead><tbody><tr class="odd"><td></td><td></td><td></td><td></td><td>a</td><td>b</td><td><u><strong>c</strong></u></td><td>d</td><td>a</td><td>b</td><td>d</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table><p>c失配，c前无最长公共前后缀，向后移动<code>2-next[2]</code>，也就是2位</p><table><thead><tr class="header"><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>d</th><th>e</th></tr></thead><tbody><tr class="odd"><td></td><td></td><td></td><td></td><td></td><td></td><td>a</td><td>b</td><td>c</td><td>d</td><td>a</td><td>b</td><td><u><strong>d</strong></u></td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table><p>在d处又失配，再次移动<code>6-next[6]</code>也就是4位</p><table><thead><tr class="header"><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>c</th><th>d</th><th>a</th><th>b</th><th>d</th><th>e</th></tr></thead><tbody><tr class="odd"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td>a</td><td>b</td><td>c</td><td>d</td><td>a</td><td>b</td><td>d</td><td></td></tr></tbody></table><p>匹配成功ヾ(≧▽≦*)o</p><p>以上就是kmp算法的基本实现流程</p><h4 id="next">next</h4><h5 id="next数组的二三事">next数组的二三事</h5><p>next数组的实质是：在当前字符前，最长公共前后缀的字符数</p><p>先拿出一个模式串：<code>abcabzan</code></p><p>对于next，有些版本默认第0位是-1，有的是0，这里默认第0位是-1.</p><ul><li>对于第一位是a，前面没有字符，赋值-1</li><li>第二位b前面只有一个字符，没有相同子串，赋值0</li><li>第三位前面两个字符没有同，赋值0</li><li>第四位前面也无，同上</li><li>第五位前面有相同前后缀元素，即a，赋值1</li><li>第六位前面，继续找，发现有更长的前后缀公共元素，是ab，两个字符，赋值2</li><li>第七位无，第八位前有一个a，赋值1，后面无公共元素</li><li>一般来说不用管最后一位是否和前面的字符能匹配上，next的目的是判断当前字符前面是否有相同的子串</li></ul><p>最后把next数组的值罗列一下</p><p><code>-1 0 0 0 1 2 0 1</code></p><p>然后就得到了next数组</p><h5 id="next数组代码实现">next数组代码实现</h5><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">void makeNext(string p, int next[]) &#123;    int j &#x3D; 0, k &#x3D; -1;    int len &#x3D; p.length();    next[0] &#x3D; -1;    while (j &lt; len - 1) &#123;        if (k &#x3D;&#x3D; -1 || p[j] &#x3D;&#x3D; p[k]) &#123;            &#x2F;&#x2F; 这里判断是否从首位开始匹配，或者模式串前后是否匹配成功            j++, k++;            next[j] &#x3D; k;            &#x2F;&#x2F; 匹配成功就把当前匹配的字符数赋给当前next[j]            &#x2F;&#x2F; 即模式串第j位前有k个最长前后缀公共元素        &#125; else &#123;            k &#x3D; next[k];            &#x2F;&#x2F; 把当前next[k]赋给k，也就相当于整个模式串向后移动next[k]位        &#125;    &#125;&#125;</code></pre><h5 id="next数组的优化">next数组的优化</h5><table><thead><tr class="header"><th>a</th><th>b</th><th>a</th><th>c</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th></tr></thead><tbody><tr class="odd"><td>a</td><td>b</td><td>a</td><td>b</td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table><p>对于模式串<code>"abab"</code>，它的next数组为<code>-1 0 0 1</code></p><p>当c与b失配时，模式串向后移动<code>3-next[3] = 2</code>，变成如下</p><table><thead><tr class="header"><th>a</th><th>b</th><th>a</th><th>c</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th></tr></thead><tbody><tr class="odd"><td></td><td></td><td>a</td><td><u><strong>b</strong></u></td><td>a</td><td>b</td><td></td><td></td><td></td></tr></tbody></table><p>看到上面这里，原来<code>p[j]</code>和<code>s[j]</code>失配，右移之后，变成<code>s[j]</code>和<code>p[next[j]]</code>(即前后缀相同字符)匹配，然后呢，又失配了，虽然说在这组字符串里最后都能匹配成功，但是移动后，按照道理，失配位前面的字符在移动之后都能匹配成功，如果一直出现这样的情况的话，那么匹配的效率就会下降。</p><p>那么怎么修改？答案是，<strong>不能容许<code>p[j]=p[next[j]]</code></strong>。如果出现刚才叙述的情况，则需递归，令<code>next[j] = next[next[j]]</code></p><p>那么这个递归又是怎么个回事呢？(看了好久才懂)</p><p>随便举一个字符串<code>ababc</code></p><p>下标从0开始，到c这个位置，也就是第4位，下标为1的字符b和下标为3的字符b是等价的，在递归之后，next数组更新，可避免出现刚才那样的bug，后移之后在前面的子串部分失配(按照道理，公共前后缀部分是不会失配的)</p><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">void makeNext(string p, int next[]) &#123;    int len &#x3D; p.length();    int k &#x3D; -1, j &#x3D; 0;    next[0] &#x3D; -1;    while (j &lt; len - 1) &#123;        if (k &#x3D;&#x3D; -1 || p[j] &#x3D;&#x3D; p[k]) &#123;            j++, k++;            if (p[j] !&#x3D; p[k])                 next[j] &#x3D; k;            &#x2F;&#x2F; 如果匹配失败就把匹配数赋给next[j]            else                 next[j] &#x3D; next[k];            &#x2F;&#x2F; 不能出现p[j] &#x3D; p[next[j]]的情况，需要继续递归        &#125; else &#123;            k &#x3D; next[k];            &#x2F;&#x2F; 把k复位(分匹配是否成功两种情况)        &#125;    &#125;&#125;</code></pre><p>优化过后的数组</p><table><thead><tr class="header"><th>a</th><th>b</th><th>a</th><th>b</th></tr></thead><tbody><tr class="odd"><td>-1</td><td>0</td><td>-1</td><td>0</td></tr></tbody></table><p>单单只看优化过后的代码，感觉还是有点恍惚，还是要结合kmp的主干部分来看</p><h4 id="kmp-1">kmp</h4><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">int kmp(string s, string p) &#123;    int len1 &#x3D; s.length();    int len2 &#x3D; p.length();    int i &#x3D; 0, j &#x3D; 0;    int *next &#x3D; new int[len2];    makeNext(p, next);    while (i &lt; len1 &amp;&amp; j &lt; len2) &#123;        &#x2F;&#x2F; j为-1 or 匹配成功才指针后移        if(j &#x3D;&#x3D; -1 || s[i] &#x3D;&#x3D; p[j])             i++, j++;        &#x2F;&#x2F; 匹配就指针后移        else             j &#x3D; next[j];                &#x2F;&#x2F; 不匹配就根据之前求出的next来决定模式串从哪开始匹配    &#125;    if (j &#x3D;&#x3D; len2)        return 1;    else        return 0;&#125;</code></pre><p>优化过后继续结合刚才优化前出现bug的那个数组</p><p>优化后next：<code>-1 0 -1 0</code></p><table><thead><tr class="header"><th>a</th><th>b</th><th>a</th><th>c</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th></tr></thead><tbody><tr class="odd"><td>a</td><td>b</td><td>a</td><td><u><strong>b</strong></u></td><td></td><td></td><td></td><td></td><td></td></tr><tr class="even"><td>-1</td><td>0</td><td>-1</td><td>0</td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table><p>第四位失配，后移<code>3-next[3]</code>，递归后<code>next[3] = 0</code>，后移了3位</p><table><thead><tr class="header"><th>a</th><th>b</th><th>a</th><th>c</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th></tr></thead><tbody><tr class="odd"><td></td><td></td><td></td><td>a</td><td>b</td><td>a</td><td>b</td><td></td><td></td></tr><tr class="even"><td></td><td></td><td></td><td>-1</td><td>0</td><td>-1</td><td>0</td><td></td><td></td></tr></tbody></table><p>c和a失配，再后移</p><table><thead><tr class="header"><th>a</th><th>b</th><th>a</th><th>c</th><th>a</th><th>b</th><th>a</th><th>b</th><th>c</th></tr></thead><tbody><tr class="odd"><td></td><td></td><td></td><td></td><td>a</td><td>b</td><td>a</td><td>b</td><td></td></tr><tr class="even"><td></td><td></td><td></td><td></td><td>-1</td><td>0</td><td>-1</td><td>0</td><td></td></tr></tbody></table><p>匹配成功</p><h4 id="完整代码">完整代码</h4><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include &lt;bits&#x2F;stdc++.h&gt;using namespace std;void makeNext(string p, int next[]);int kmp(string s, string p);void makeNext(string p, int next[]) &#123;    int len &#x3D; p.length();    int j &#x3D; 0, k &#x3D; -1;    next[0] &#x3D; -1;    while (j &lt; len - 1) &#123;        if (k &#x3D;&#x3D; -1 || p[j] &#x3D;&#x3D; p[k]) &#123;            j++, k++;            if (p[j] !&#x3D; p[k])                next[j] &#x3D; next[k];            else                k &#x3D; next[j];        &#125;     &#125;&#125;int kmp(string s, string p) &#123;    int len1 &#x3D; s.length();    int len2 &#x3D; p.length();    int i &#x3D; 0, j &#x3D; 0;    int *next &#x3D; new int[len2];    makeNext(p, next);    while (i &lt; len1 &amp;&amp; j &lt; len2) &#123;        if (j &#x3D;&#x3D; -1 || s[i] &#x3D;&#x3D; p[j])            i++, j++;        else            j &#x3D; next[j];    &#125;    if (j &#x3D;&#x3D; len2)        return 1;    else         return 0;&#125;int main() &#123;    string s, p;    cin &gt;&gt; s &gt;&gt; p;    if (kmp(s, p) &#x3D;&#x3D; 1)        cout &lt;&lt; &quot;found the key string\n&quot;;    else        cout &lt;&lt; &quot;not found the key string\n&quot;;&#125;</code></pre><h3 id="参考链接">参考链接</h3><p><a href="https://blog.csdn.net/v_july_v/article/details/7041827">从头到尾彻底理解KMP（2014年8月22日版）_结构之法 算法之道-CSDN博客_kmp</a></p><p><a href="https://www.bilibili.com/video/BV1Ys411d7yh?from=search&amp;seid=14595349758363193343&amp;spm_id_from=333.337.0.0">【soso字幕】汪都能听懂的KMP字符串匹配算法【双语字幕】_哔哩哔哩_bilibili</a></p>]]></content>
    
    
    <summary type="html">填坑系列之kmp，本篇文章只记录我理解的过程、需要注意的小细节，不涉及具体讲解，一些具体的原理、推导步骤可参考文末我列出的文章和视频</summary>
    
    
    
    <category term="C++" scheme="https://jaydenchang.top/categories/C/"/>
    
    
    <category term="数据结构" scheme="https://jaydenchang.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="C++" scheme="https://jaydenchang.top/tags/C/"/>
    
  </entry>
  
</feed>
