标签 Ubuntu 下的文章

让reviewdog支持gitlab-push-commit,守住代码质量下限

本文永久链接 – https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor

一. 代码质量保证的手段

从世界上首款计算机高级程序设计语言Fortran自上世纪50年代诞生以来,编程这个行当已经走过了近70年。虽然年头已不少,但不可否认的一点是:软件生产依然无法像硬件那样标准化,同一个小功能,N个程序员的有N种实现方法

那么如何保证生产出的软件的质量符合我们的要求呢?不同领域的程序员都在进行着努力,比如:做编译器的让编译器更加严格,努力将内存安全问题彻底消除(如Rust);做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链),让程序员可以更容易地对自己所写的代码进行全方位的测试,以期找出更多的代码中的潜在问题…

当然,还有一种主观的代码质量保证方法目前依旧是主流,它就是是同行的代码评审(code review, cr)

代码评审的方法主要有两种,一种是大家坐到一个会议室中,对某个人的某段代码“发表大论”;另外一种则是利用像gerrit这样的工具,在线对其他人的某次提交的代码或某PR的代码进行“评头论足”。

不过无论哪种,最初的时候大家都会细无巨细地从语法层面看到代码结构设计,再到业务逻辑层面,但这样做的弊端也是很显而易见,那就是效率低下,不聚焦(focus)

于是人们想到了:能否利用工具来尽可能地发现语法层面的问题,这样代码评审时,人类专家便可以聚焦代码结构设计与业务逻辑层面的问题,分工明确后,效率自然提升(如下图):

注:目前绝大多数工具链仅能自动帮助程序员解决语法层面的问题。将来,随着工具的日益强大,工具可以不断升级关注层次,逐渐进化到具备发现代码结构设计问题,甚至可以发现业务层面逻辑问题的能力。

于是就有了reviewdog这样的可以调用各种linter工具对代码进行自动扫描并将问题以comment的形式自动提交的代码仓库的工具。

到这里很多朋友会问,即便让工具来关注语法层面的问题,为何要用reviewdog这样的工具,git的pre-commit hook、git server hooks、利用Make等工具做开发阶段检查等手段也能检查代码中的语法问题,它们不再香了吗?

下面简单看看这些方法的“问题”(我们假设大家都已经在使用git作为代码版本管理工具):

  • git pre-commit-hook

git pre-commit hook是一个客户端的git hook,它是放在开发人员本地代码copy中的.git/hooks目录下的钩子,当开发人员在本地执行git commit时会被唤起执行。pre-commot hook的问题就在于我们没法在中心代码仓库对pre-commit hook的脚本内容做统一管理和维护。这个更适合开发人员根据自己的喜好、代码素养在自己的开发环境下部署。

此外,有些代码并不一定是在开发者自己的开发机上提交的,换环境后,pre-commit hook就不在生效。

  • 利用Make等工具做本地检查

利用make工具,我们可以在本地build代码之前对代码做lint等各种静态检查,但和pre-commit-hook一样,虽然Makefile可以提交代码仓库,但真正用于检查代码的工具依旧是在开发人员本地,难于对工具版本,设定的检查规则进行统一管理维护,可能导致不同开发人员环境有不一致的情况。另外同样的情况,有些代码并不一定是在开发者自己的开发机上提交的,换环境后,Make工具依赖的代码检查工具可能并不存在,检查环节就无法有效实施。

  • git server hooks

git支持server hooksgitlab自12.8版本也开始支持server hooks(替换之前的custom hooks)。

Git server支持以下钩子:

  • pre-receive
  • post-receive
  • update

我倒是没有深研究过这些server hooks是否能满足我们的功能要求,但就git server hooks的部署特点就决定了,它不适合,因为它要在gitlab的server上执行,这就意味着我们需要的所有静态代码检查工具都要部署和配置在与gitlab server同一个环境中,这耦合性太强,根本不便于我们对这些静态代码检查工具的管理与日常维护。

而像reviewdog这样的工具将与ci工具(比如gitlab-ci)集成,运行在slave/worker/runner的机器上,而这些机器上的环境便很容易统一的定制与管理。

好了,下面进入reviewdog时间!

注:我们以代码仓库为gitlab为例,我曾做过小调查,目前企业内部基本都在使用gitlab搭建私有git仓库,除了那些自实现code仓库平台的大厂。

二. reviewdog是什么

reviewdog是一个什么样的工具呢?我们来看看下面这幅示意图:

我们看到,这是一幅基于gitlab的ci执行流程图,在这个流程中,reviewdog运行在gitlab-runner节点,也就是负责真正执行ci job的节点上。每当开发人员执行一次git push,将commit同步到代码仓库,一次ci job将被触发,在承载该ci job的gitlab-runner节点上,reviewdog被唤起,它做了三件事:

  • 调用静态代码检查工具对最新pull下来的代码进行检查;
  • 将代码检查结果(第几行有问题)与commit diff的结果进行比对,得到交集(即commit diff中变更(add和update)的代码行与代码检查结果的行一致的,放入交集中);
  • 将交集中代码检查结果信息以gitlab commit comment的形式post到gitlab仓库中

这样开发人员就可以通过commit页面看到这些comments,并应对这些comment,必要情况下,会修复这些问题。

我们看到reviewdog和其他工具相比,最大的不同就是可以找出commit diff与lint结果中的交集,并与代码仓库交互,将这些交集中的结果以comments的形式放入commit页面,就像同行代码评审时,同行直接在你的commit页面添加comment一样

然而当前版本的reviewdog还不支持直接在gitlab-push-commit上做检查与提交comment,可能是这样的场景较为少见,因为目前开源项目更多采用基于pr(pull request)的工作流,所以reviewdog内置了诸如github-pr-check、github-pr-review、gitlab-mr-commit等工作流的代码review。而像我们使用的基于gitlab-push-commit可能并不多见(当然我们内部使用这种也是有特定上下文的)。

那么如何让reviewdog支持gitlab-push-commit,即对push动作中的commit进行静态代码检查并将结果以comment的形式放入commit页面呢?我们只能fork reviewdog项目,并在fork后的项目中自行添加对gitlab-push-commit模式的支持。

三. 改造reviewdog以支持gitlab-push-commit模式

reviewdog就是一个命令行工具,通常就是一次性执行,因此它的代码结构较为清晰。我们可以简单围绕它支持的几种reporter模式来搞清楚如何增加对gitlab-push-commit模式的支持。

这里说明一下gitlab-push-commit模式的含义,首先该模式适用于开发人员通过git push推送代码到gitlab时触发的ci job。在该ci job中,reviewdog会运行配置的静态代码分析工具(比如golangci-lint等)对最新的代码进行扫描,并得到问题集合;然后获取最新的commit的sha值(CI_COMMIT_SHA)以及push之前的latest commit的sha值(CI_COMMIT_BEFORE_SHA),并比较这两个版本间的diff。最后通过文件名与行号将问题集合与diff集合中的“交集”找出来,并将结果以comment形式通过gitlab client api提交到的此次push的最新的那个commit的页面。

目前该模式尚存在一个“瑕疵”,那就是如果一个push中有多个commit,那么gitlab-push-commit模式不会针对每个commit做diff和comment,而只是会用push中的latest commit与push之前的最新commit做比较。

定义清除gitlab-push-commit模式含义后,我们就可以“照葫芦画瓢”的为reviewdog增加该模式的支持了!

在main.go中,我们主要是在run函数中增加一个reporter case分支:

// https://github.com/bigwhite/reviewdog/blob/master/cmd/reviewdog/main.go
func run(r io.Reader, w io.Writer, opt *option) error {
... ...

case "gitlab-push-commit":
    build, cli, err := gitlabBuildWithClient(opt.reporter)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] gitlabBuildWithClient ok\n")

    gc, err := gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n")

    cs = reviewdog.MultiCommentService(gc, cs)
    ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n")
... ...

}

在这个case中,我们主要是为后面的project.Run或reviewdog.Run方法准备gitlab client对象、PushCommitsCommenter对象(位于service/gitlab/gitlab_push_commits.go中)、PushCommitsDiff对象(位于service/gitlab/gitlab_push_commits_diff.go中)等。

gitlab_push_commits.go和gitlab_push_commits_diff.go是新增的两个go源文件,也是参考了同目录下的gitlab_mr_commit.go和gitlab_mr_diff.go改写而成的。具体代码这里就不列出来了,大家有兴趣可以自行阅读。

四. 部署gitlab-runner验证新版reviewdog

下面我们就来验证一下上述改造后的reviewdog。

1. 安装gitlab-runner

我们先在gitlab上建立一个实验项目,然后为该项目配置ci。如果你的gitlab还没有注册gitlab-runner,可以按下面步骤安装和注册runner节点(可以在顶层group下面建立,这样runner可以在group内共享:settings => CI/CD => Runners => Show runner installation instructions 有部署runner的详细命令说明):

//假设我们有一个ubuntu 20.04的主机,我们可以按下面命令安装和注册一个gitlab-runner:

sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# Give it permissions to execute
sudo chmod +x /usr/local/bin/gitlab-runner

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

# 注册该runner
sudo gitlab-runner register --url http://{gitlab-server-ip-addr}/ --registration-token {registration token}

上面命令会在/etc/gitlab-runner下面建立一个runner自用配置文件:config.toml:

//  /etc/gitlab-runner/config.toml

concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "runner for ard group"
  url = "http://gitlab_ip_addr/"
  id = 1
  token = "{registration token}"
  token_obtained_at = 2022-09-01T11:03:43Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "shell"
  shell = "bash"
  environment = ["PATH=/home/tonybai/.bin/go1.18/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"]
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]

这里我选择了shell executor,即基于主机shell执行ci job中的命令。runners下的environment可以设置shell的环境变量,这里的设置将覆盖对应账号(比如gitlab-runner)下的环境变量值。

gitlab-runner部署成功后,我们在group的runners下面便可以看到下面的available runners:

注:在创建runner时,我为该runner设置了两个tag:ard和ci。

注:确保runner执行的命令在主机的PATH下面可以找到。

2. 创建personal access token

reviewdog需要通过gitlab client API访问gitlab仓库获取信息并提交comments,这就需要我们为runner执行的命令提供access token。

gitlab有多种access token,比如:personal access token、project access token等。我们创建personal access token,我也测试过project access token,使用project access token可以成功提交comment,但是notify mail十有八九无法发送出来。

access token要保存好,因为它只显示一次。

我们将personal access token配置到实验项目的variable中(Settings => CI/CD => variables),variable的key为REVIEWDOG_GITLAB_API_TOKEN,值为刚刚创建的token。

后续每次CI job执行,该variable会作为预定义的环境变量对job生效。我们的reviewdog便可以使用该token访问gitlab。

3. 配置实验项目的ci pipeline

我们可以通过代码的形式配置实验项目的ci pipeline,我们在项目根目录下建立.gitlab-ci.yml文件,其内容如下:

// .gitlab-ci.yml

build-job:
  tags:
      - ard
  stage: build
  script:
    - export CI_REPO_OWNER=ard/incubators
    - export CI_REPO_NAME=learn-gitlab
    - reviewdog -reporter=gitlab-push-commit
  only:
    - master
    - pushes

.gitlab-ci.yml的具体字段含义可以参考gitlab文档。在这个配置中,值得注意的有几点:

  • 使用tags关联runner(这里用ard这个tag);
  • script部分是job具体执行的命令列表,这里先设置CI_REPO_OWNER和CI_REPO_NAME两个环境变量,供reviewdog使用;然后执行reviewdog;
  • only部分描述仅针对master分支的push事件触发ci job。

4. 配置.reviewdog.yml

最后,我们来配置一下适合实验项目的reviewdog的配置文件。我们同样在项目根目录下建立.reviewdog.yml文件,其内容如下:

runner:
  golangci:
    cmd: golangci-lint run --max-same-issues=0 --out-format=line-number ./...
    errorformat:
      - '%E%f:%l:%c: %m'
      - '%E%f:%l: %m'
      - '%C%.%#'
    level: warning

在这里我们看到,我们使用golangci-lint这个静态检查工具对实验项目的代码进行检查。这里的–max-same-issues=0的含义是不限制相同错误的数量。至于.reviewdog.yml的具体格式,reviewdog项目自身的.reviewdog.yml很具参考价值,大家需要时可以仔细研究。

5. 推送代码并验证reviewdog的执行结果

我们可以故意在代码中写下有问题的一些代码,这些问题要保证可以被golangci-lint工具扫描出来,比如:

package main

type Foo struct {
    A int
    B string
    C bool
}

func Demo1() error {
    return nil
}

func Demo2() error {
    return nil
}

func Demo3() error {
    return nil
}

func main() {
    f := &Foo{1, "tony", false}
    _ = f
    Demo2()
    Demo1()
    Demo3()
}

这里并没有对Demo函数调用进行错误处理,golangci-lint中的errcheck可以检测出这个问题。提交并push这些代码到仓库,稍等片刻,我们便可收到notify mail,打开commit页面,便会看到下面这样的commit comments:

看到这样的结果,说明reviewdog按预期工作了!

五. 小结

本文介绍了如何基于reviewdog对push提交的commit进行静态代码检查并像一个“同行”一样在commit中提交评论的方法。

这样做的目的就是希望通过工具提升代码评审的效率,同时也守住代码质量的下限。

就像本文开始所说的那样,随着检查工具能力的增强,这样的基于reviewdog自动检查代码的方案在保证代码质量方面还可以继续提升。

Go开源了go/ast等工具链,有能力的童鞋可以基于go/ast自行开发具有“特定目的”的检查工具并集成到reviewdog中,这将使得检查更有针对性和有效性。

本文涉及源码在这里下载 – https://github.com/bigwhite/reviewdog/


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

使用Go语言实现eBPF程序内核态与用户态的双向数据交换

本文永久链接 – https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go

在之前的两篇文章中,无论是使用C语言开发eBPF程序,还是使用Go开发的eBPF程序,都是hello world级别的,可能有用,但谈不上十分实用。

通常来说,一个实用的eBPF程序,它的内核态部分与用户态部分是有数据交换的,有了这种数据交换,eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性,eBPF MAP是绕不过去的机制

在这一篇有关eBPF程序开发的文章中,我们就来看看如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换

一. why BPF MAP?

永远不要忘记BPF字节码是运行于OS内核态的代码,这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据,通常仅能通过系统调用陷入内核态来实现。因此,在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。

那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢?用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢?

Linux内核BPF开发者于是就引入了BPF MAP机制BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道。同时由于bpf map存储在内核分配的内存空间,处于内核态,可以被运行于在内核态的多个BPF程序所共享,同样可以作为多个BPF程序交换和共享数据的机制。

二. BPF MAP不是狭义的map数据结构

BPF MAP究竟是什么呢?它不是我们狭义理解的哈希映射表的数据结构,而是一种通用数据结构,可以存储不同类型数据的通用数据结构。用著名内核BPF开发者Andrii Nakryiko的话来说,MAP就是BPF中代表抽象数据容器(abstract data container)的一个概念

截至目前,内核BPF支持的MAP类型已经有20+种,下面是libbpf中bpf.h中列出的当前支持的MAP类型:

// libbpf/include/uapi/linux/bpf.h
enum bpf_map_type {
    BPF_MAP_TYPE_UNSPEC,
    BPF_MAP_TYPE_HASH,
    BPF_MAP_TYPE_ARRAY,
    BPF_MAP_TYPE_PROG_ARRAY,
    BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    BPF_MAP_TYPE_PERCPU_HASH,
    BPF_MAP_TYPE_PERCPU_ARRAY,
    BPF_MAP_TYPE_STACK_TRACE,
    BPF_MAP_TYPE_CGROUP_ARRAY,
    BPF_MAP_TYPE_LRU_HASH,
    BPF_MAP_TYPE_LRU_PERCPU_HASH,
    BPF_MAP_TYPE_LPM_TRIE,
    BPF_MAP_TYPE_ARRAY_OF_MAPS,
    BPF_MAP_TYPE_HASH_OF_MAPS,
    BPF_MAP_TYPE_DEVMAP,
    BPF_MAP_TYPE_SOCKMAP,
    BPF_MAP_TYPE_CPUMAP,
    BPF_MAP_TYPE_XSKMAP,
    BPF_MAP_TYPE_SOCKHASH,
    BPF_MAP_TYPE_CGROUP_STORAGE,
    BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
    BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
    BPF_MAP_TYPE_QUEUE,
    BPF_MAP_TYPE_STACK,
    BPF_MAP_TYPE_SK_STORAGE,
    BPF_MAP_TYPE_DEVMAP_HASH,
    BPF_MAP_TYPE_STRUCT_OPS,
    BPF_MAP_TYPE_RINGBUF,
    BPF_MAP_TYPE_INODE_STORAGE,
    BPF_MAP_TYPE_TASK_STORAGE,
    BPF_MAP_TYPE_BLOOM_FILTER,
};

这里数据结构类型众多,但不是本文的重点,我们不一一介绍了。其中的BPF_MAP_TYPE_HASH类型是BPF支持的第一种MAP数据结构,这个类型可以理解为我们日常接触的hash映射表,通过键值对的形式索引数据。在后续的例子中我们将使用这种类型的MAP。

那么BPF MAP是如何可以在内核态与用户态共享数据的?原理是什么呢?

bpf这个系统调用的说明中,我们能找到端倪。下面是bpf系统调用的函数原型:

// https://man7.org/linux/man-pages/man2/bpf.2.html

#include <linux/bpf.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

从bpf的原型来看,似乎比较简单。但bpf其实是一个“富调用”,即不止能干一件事,通过cmd传入的值不同,它可以围绕BPF完成很多事情。最主要的功能是加载bpf程序(cmd=BPF_PROG_LOAD),其次是围绕MAP的一系列操作,包括创建MAP(cmd=BPF_MAP_CREATE)、MAP元素查询(cmd=BPF_MAP_LOOKUP_ELEM)、MAP元素值更新(cmd=BPF_MAP_UPDATE_ELEM)等。

当cmd=BPF_MAP_CREATE时,即bpf执行创建MAP的操作后,bpf调用会返回一个文件描述符fd,通过该fd后续可以操作新创建的MAP。通过fd访问map,这个很unix

当然这么底层的系统调用,一般BPF用户态开发人员无需接触到,像libbpf就包装了一系列的map操作函数,这些函数不会暴露map fd给用户,简化了使用方法,提升了使用体验。

下面我们先来看一下如何用C语言实现基于map的BPF用户态与内核态的数据交换。

三. 使用C基于libbpf使用map的示例

这个示例改造自helloworld示例。原helloworld示例在execve这个系统调用被调用时输出一条内核日志(在/sys/kernel/debug/tracing/trace_pipe中可以查看到),用户态程序并没有与内核态程序做任何数据交换。

在这个新示例(execve_counter)中,我们依然跟踪系统调用execve,不同的是我们对execve进行调用计数,并将技术存储在BPF MAP中。而用户态部分程序则读取该MAP中的计数并定时输出计数值。

我们先来看看BPF内核态部分的源码:

// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter/execve_counter.bpf.c

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

typedef __u64 u64;
typedef char stringkey[64];

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 128);
    //__type(key, stringkey);
    stringkey* key;
    __type(value, u64);
} execve_counter SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
  stringkey key = "execve_counter";
  u64 *v = NULL;
  v = bpf_map_lookup_elem(&execve_counter, &key);
  if (v != NULL) {
    *v += 1;
  }
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

和helloworld示例不同,我们在新示例中定义了一个map结构execve_counter,通过SEC宏将其标记为BPF MAP变量。

这个map结构有四个字段:

  • type: 使用的BPF MAP类型(参见前面的bpf_map_type枚举类型),这里我们使用BPF_MAP_TYPE_HASH,即一个hash散列表结构;
  • max_entries:map内的key-value对的最大数量;
  • key: 指向key内存空间的指针。这里我们自定义了一个类型stringkey(char[64])来表示每个key元素的类型;
  • value: 指向value内存空间的指针,这里value元素的类型为u64,一个64位整型。

内核态函数bpf_prog的实现也比较简单:在上面的map中查询”execve_counter”这个key,如果查到了,则将得到的value指针指向的内存中的值加1。

我们再来看看execve_counter这个示例的用户态部分的程序源码:

// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve_counter/execve_counter.c

#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <linux/bpf.h>
#include "execve_counter.skel.h"

typedef __u64 u64;
typedef char stringkey[64];

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct execve_counter_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = execve_counter_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }

    /* Load & verify BPF programs */
    err = execve_counter_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* init the counter */
    stringkey key = "execve_counter";
    u64 v = 0;
    err = bpf_map__update_elem(skel->maps.execve_counter, &key, sizeof(key), &v, sizeof(v), BPF_ANY);
    if (err != 0) {
        fprintf(stderr, "Failed to init the counter, %d\n", err);
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = execve_counter_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    for (;;) {
            // read counter value from map
            err = bpf_map__lookup_elem(skel->maps.execve_counter, &key, sizeof(key), &v, sizeof(v), BPF_ANY);
            if (err != 0) {
               fprintf(stderr, "Lookup key from map error: %d\n", err);
               goto cleanup;
            } else {
               printf("execve_counter is %llu\n", v);
            }

            sleep(5);
    }

cleanup:
    execve_counter_bpf__destroy(skel);
    return -err;
}

map是在execve_counter_bpf__load中完成的创建,跟踪代码你会发现(参考libbpf源码),最终会调用bpf系统调用创建map。

和helloworld示例不同的是,我们在attach handler之前,先使用libbpf封装的bpf_map__update_elem初始化了bpf map中的key(初始化为0,如果没有这一步,第一次bpf程序执行时,会提示找不到key)。

然后attach handler后,我们在一个循环中每隔5s通过bpf_map__lookup_elem查询一下key=”execve_counter”的值并输出到控制台。

用户态程序之所以可以直接使用map,是因为bpftool基于execve_counter.bpf.c生成的execve_counter.skel.h中包含了map的各种信息。

接下来我们执行make编译一下这个ebpf程序,然后执行并观察输出:

$sudo ./execve_counter
libbpf: loading object 'execve_counter_bpf' from buffer
libbpf: elf: section(3) tracepoint/syscalls/sys_enter_execve, size 192, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 24 insns (192 bytes)
libbpf: elf: section(4) .reltracepoint/syscalls/sys_enter_execve, size 16, link 22, flags 0, type=9
libbpf: elf: section(5) .rodata, size 64, link 0, flags 2, type=1
libbpf: elf: section(6) .maps, size 32, link 0, flags 3, type=1
libbpf: elf: section(7) license, size 13, link 0, flags 3, type=1
libbpf: license of execve_counter_bpf is Dual BSD/GPL
libbpf: elf: section(13) .BTF, size 898, link 0, flags 0, type=1
libbpf: elf: section(15) .BTF.ext, size 176, link 0, flags 0, type=1
libbpf: elf: section(22) .symtab, size 744, link 1, flags 0, type=2
libbpf: looking for externs among 31 symbols...
libbpf: collected 0 externs total
libbpf: map 'execve_counter': at sec_idx 6, offset 0.
libbpf: map 'execve_counter': found type = 1.
libbpf: map 'execve_counter': found key [9], sz = 64.
libbpf: map 'execve_counter': found value [13], sz = 8.
libbpf: map 'execve_counter': found max_entries = 128.
libbpf: map 'execve_c.rodata' (global data): at sec_idx 5, offset 0, flags 480.
libbpf: map 1 is "execve_c.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(3) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #15 against 'execve_counter'
libbpf: prog 'bpf_prog': found map 0 (execve_counter, sec 6, off 0) for insn #15
libbpf: map 'execve_counter': created successfully, fd=4
libbpf: map 'execve_c.rodata': created successfully, fd=5
execve_counter is 0
execve_counter is 0
execve_counter is 9
execve_counter is 23
... ...

注:如果不知道如何编译execve_counter这个示例,请先移步《使用C语言从头开发一个Hello World级别的eBPF程序》了解其构建原理。

bpftool工具提供了查看map的特性,我们可以通过它查看示例创建的map:

$sudo bpftool map
114: hash  name execve_counter  flags 0x0
    key 64B  value 8B  max_entries 128  memlock 20480B
    btf_id 120
116: array  name execve_c.rodata  flags 0x80
    key 4B  value 64B  max_entries 1  memlock 4096B
    frozen

我们还可以dump一下整个map:

$sudo bpftool map dump id 114
[{
        "key": "execve_counter",
        "value": 23
    }
]

我们看到,整个map中就一个键值对(key=”execve_counter”),其值与示例的用户态部分程序输出的一致。

好了,有了C示例作为基础,我们再来看看如何基于Go来实现这个示例。

四. 使用Go基于cilium/ebpf实现execve-counter示例

使用Go开发BPF用户态部分程序要容易的多,cilium/ebpf提供了的包用起来很简单。如果还不知道如何用Go开发ebpf用户态部分的套路,请先移步《使用Go语言开发eBPF程序》一文了解一下。

Go语言示例的必不可少的原料是execve_counter.bpf.c,这个C源码文件与上面的execve_counter示例中的execve_counter.bpf.c的唯一差别就是include的头文件改成了common.h:

$diff execve_counter.bpf.c ../execve-counter/execve_counter.bpf.c
1,2c1,2
<
< #include "common.h"
---
> #include <linux/bpf.h>
> #include <bpf/bpf_helpers.h>

基于原料execve_counter.bpf.c,bpf2go工具会生成用户态部分所需的Go源码,比如:bpfObject中包含的bpf map实例:

// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
    ExecveCounter *ebpf.Map `ebpf:"execve_counter"`
}

最后,我们在main包main函数中直接使用这些生成的与bpf objects相关的Go函数即可,下面是main.go部分源码:

// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf execve_counter.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    // init the map element
    var key [64]byte
    copy(key[:], []byte("execve_counter"))
    var val int64 = 0
    if err := objs.bpfMaps.ExecveCounter.Put(key, val); err != nil {
        log.Fatalf("init map key error: %s", err)
    }

    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            if err := objs.bpfMaps.ExecveCounter.Lookup(key, &val); err != nil {
                log.Fatalf("reading map error: %s", err)
            }
            log.Printf("execve_counter: %d\n", val)

        case <-stopper:
            // Wait for a signal and close the perf reader,
            // which will interrupt rd.Read() and make the program exit.
            log.Println("Received signal, exiting program..")
            return
        }
    }
}

在main函数,我们通过objs.bpfMaps.ExecveCounter直接访问map实例,并通过其Put和Lookup方法可以直接操作map。这里要注意的是key的类型必须与execve_counter.bpf.c中的key类型(char[64])保持内存布局一致,不能直接用string类型,否则会在执行时报下面错误:

init map key error: can't marshal key: string doesn't marshal to 64 bytes

编译和执行execve-counter-go和helloworld-go别无二致:

$make
$go run -exec sudo main.go bpf_bpfel.go

2022/07/17 16:59:52 execve_counter: 0
2022/07/17 16:59:57 execve_counter: 14
^C2022/07/17 16:59:59 Received signal, exiting program..

五. 小结

本文介绍了eBPF内核态部分与用户态部分进行数据交换的主要方法:BPF MAP机制。这里的MAP不是狭义的一种hash散列表,而是一个抽象数据结构容器,目前支持二十几种数据结构,大家可以根据自己的需求挑选适当的结构(可查询手册了解各种数据结构的特点)。

MAP本质上也是由bpf系统调用创建的,bpf程序只需要声明map的key、value、type等组成信息即可。用户态可以通过bpf系统调用返回的fd操作map,libbpf和cilium/ebpf等封装了对fd的操作,这样简化了API的使用。

内核中map的update操作不是原子的,因此当有多个bpf程序并发访问一个map时,需要同步操作。bpf提供了bpf_spin_lock来实现对map操作的同步。我们可以在value类型中加入bpf_spin_lock来同步对value的修改,就像下面的例子(例子来自《Linux Observability with BPF》一书):

struct concurrent_element {
    struct bpf_spin_lock semaphore;
    int count;
}

struct bpf_map_def SEC("maps") concurrent_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(int),
    .value_size = sizeof(struct concurrent_element),
    .max_entries = 100,
};

int bpf_program(struct pt_regs *ctx) {
      intkey=0;
      struct concurrent_element init_value = {};
      struct concurrent_element *read_value;
      bpf_map_create_elem(&concurrent_map, &key, &init_value, BPF_NOEXIST);
      read_value = bpf_map_lookup_elem(&concurrent_map, &key);
      bpf_spin_lock(&read_value->semaphore);
      read_value->count += 100;
      bpf_spin_unlock(&read_value->semaphore);
}

本文涉及代码可以在这里下载。

六. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats