GoCN社区Go读书会第二期:《Go语言精进之路》

本文永久链接 – https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master

本文是2022年6月26日我在GoCN社区的Go读书会第二期《Go语言精进之路》直播的文字稿。本文对直播的内容做了重新整理与修订,供喜欢阅读文字的朋友们在收看直播后的揣摩和参考。视频控的童鞋可以关注GoCN公众号和视频号看剪辑后的视频,也可以在B站GopherChina专区下收看视频回放(https://www.bilibili.com/video/BV1p94y1R7jg)。


大家晚上好,我叫白明,是《Go语言精进之路》一书的作者,也是tonybai.com的博主,很荣幸今天参加GoCN社区Go读书会第二期,分享一下我个人在写书和读书方面的经验和体会。

今天的分享包括三方面内容:

  • 写书的历程。一些Gopher可能比较好奇,这么厚的一套书是怎么写出来的,今天就和大家聊一聊。
  • 《Go语言精进之路》导读。主要是把这本书的整体构思与大家聊聊,希望通过这个导读帮助读者更好地阅读和理解这套书。
  • 我个人的读书方法与经验的简要分享。

首先和大家分享一下写书的历程。

一. 写书的历程

1. 程序员的“小目标”与写书三要素

今天收看直播的童鞋都是有追求的技术人员,可能心底都有写一本属于自己的书的小目标。这样可以把自己学习到的知识、技能和经验以比较系统的方式输出给其他人,可以帮助其他人快速学习和掌握本领域的知识、技能和经验。

当然写书还有其他好处,比如:提升名气、更容易混技术圈子、可能给你带来更好的职业发展机会,当然也会给你带来一些额外的副业收入,至于多少,还要看书籍的口碑与销量。

那怎么才能写书呢?作为“过来人”,我总结了三个要素,也是三个条件。

第一个要素是能力

这个很容易理解。以Go为例,如果你没有在Go语言方面的知识、技能的沉淀,没有对Go语言方方面面的较为深入的理解,你很难写出一本口碑很好的书籍。尤其是那种有原创性、独到见解的著书。而不是对前人资料做系统整理摘抄的编书。编书更常见于教材、字典等。显然著书对作者水平的要求更高。

第二个要素是意愿

写过书的同学都有体会,写书是一件辛苦活。需要你在正式工作之余付出大量业余时间伏案创作。并且对于小众技术类书籍来说,写书能带来的金钱上的收益和你付出的时长和精力不成正比。就这个问题,我曾与机械工业出版社的营销编辑老师聊过,得到的信息是:Go技术书籍的市场与Java、Python还没法比,即便是像Go语言圣经《Go程序设计语言》的销量也没法与Java、Python的头部书籍销量相比。

第三个要素是机会

记得小时候十分羡慕那些能出书的人,觉得都是大神级的人物。不过那个时候出书的确很难,机会应该很少,你要不是在学术圈里混很难出书。如今就容易地多了,渠道也多了。每年出版社都有自己的出版计划,各个出版社的编辑老师也在根据计划在各种自媒体上、技术圈子中寻觅匹配的技术作者。

如果你有自己的思路,也可以整理出大纲,并通过某种方式联系到出版社老师,如果匹配就可以出。

另外国外流行电子自助出版,这也给很多技术作者很好的出版机会。比如国内作者老貘写的Go 101系列就是在亚马逊leanpub上做的自助出版,效果还不错。

以上就是我总结的出书的三个要素,一旦集齐这三个要素呢,出书实际就是自然而然的一件事了。以我为例。

从能力方面来说呢,我大约从2011年开始接触和学习Go语言,算是国内较早的一批Go语言接纳者。Go语言2012年才发布1.0版本,因此那时我接触的Go时还是r60版本,还不是正式的1.0版本。从那时起就一直在跟踪Go演化,日常写一些Go项目的小程序。

Go 1.5实现自举并大幅降低GC延迟,我于是开始在一些生产环境使用Go,并逐渐将知识和经验做了沉淀,在自己的博客上不断做着Go相关内容的输出,反响也不错。

随着输出Go内容的增多,我发现以博客的形式输出,内容组织零散,于是我第一次有了将自己的Go知识系统整理并输出的意愿和想法。

我在实践Go的过程中收到很多Go初学者的提问:Go入门容易,但精进难,怎么才能像Go开发团队那样写出符合Go思维和语言惯例的高质量代码呢?这个问题引发了我的思考。在2017年GopherChina大会我以《go coding in go way》为主题,以演讲的形式尝试回答这个问题,但鉴于演讲的时长有限,很多内容没能展开,效果不甚理想。这进一步增强了我通过书籍的形式系统解答这个问题的意愿。

而当时我家大宝已经长大了,我也希望通过写书这个行动身体力行地给孩子树立一个正面的榜样。中国古语有云:言传身教,我也想践行一下。

机会就这样自然而然的来了!2018年初,机械工业出版社副总编杨福川老师在微信联系到我,和我探讨一下是否可以写一本类似于“Effective Go”的书,当时机械工业出版社华章出版社策划了Effective XXX(编写高质量XXX)系列图书,当时已经出版了C、Python等语言版本的书籍,还差Go语言的。我的出书意愿与出版社的需求甚是匹配,于是我答应的杨老师的要求,成为了这套丛书的Go版本的作者。

2. 写书的过程

我是2018下旬开始真正动笔的。

真正开始码字的时候,我才意识到,写书真不容易,要写出高质量书稿,的确需付出大量时间和汗水。每天晚上、早上都在构思、码字、写代码示例、画插图,睡眠时间很少。记得当时每周末都在奋笔疾书,陪伴家人尤其是孩子的时间很少。

另外我这个人还习惯于把一个知识点讲细讲透,这样每一节的篇幅都不小。因此,写作进展是很缓慢的,就这样,进度一再延期。好在编辑老师比较nice,考虑到书稿质量,没有狠狠催进度。

2020年11月末,我正式向出版社交了初稿,记得初稿有66条,近40w字。

又经过一年的排期、编辑、修订、排版,2021年12月下旬正式出版。

2022年1月《Go语言精进之路》正式上架到各个渠道货架。

到今天为止,出版了近六个月,这本书收获了还不错的口碑,在各个平台上的口碑都在8分以上(注:口碑分数还在动态变化,下图仅为当时的快照,不代表如今的分数)。


能获得大家的认可,让我很是欣慰,觉得写书过程付出的辛苦没有白费。

以上就是我的写书历程。总的来说一句话:写书不易,写高质量的书更难

接下来我来进行一下《Go语言精进之路》一书的导读。

二. 《Go语言精进之路》导读

也许是“用力过猛”,《Go语言精进之路》一书写的太厚了,无法装订为一册。编辑老师建议装订为两册,即1、2册。很多同学好奇为什么不是上下册而是1、2册,这里是编辑老师的“高瞻远瞩”,目的是为后续可能的“续写”(比如第3册)留足空间,毕竟Go语言还在快速演进,目前的版本还不包含像泛型这样的新语法。不过,目前第3册还尚未列入计划。

本套书共分为10个部分,66个主题。第一册包含了前7个部分,后3部分在第二册中。

1. 整体写作思路

整套书围绕着两个前后关联的思路循序展开。

第一个思路我叫它:精进之路,思维先行

第二个思路称为:践行哲学,遵循惯例,认清本质,理解原理

我们先来看看第一个思路。

2. 精进之路,思维先行

收看直播的童鞋都不止学过一门编程语言。大家可能都有过这样的经历:你已经精通A语言,然后在学习B语言的时候用A语言的思维去写B代码,你会觉得写出的B代码很别扭,写出的代码总是感觉不是很地道,总觉得不是那种高质量的B语言代码。

其实,不仅学习编程语言是这样,学自然语言也是一样。最典型的一个例子,大家都学过十几年的英语,但毕业后能用地道的英语表达自己观点的人却不多,为什么呢?那就是我们总用中文的思维方式去组织英语的句子,去说英语,这样再怎么努力也很难上一个层次。

其实,很多语言大师早就意识到了这一点。下面是我收集的这些大师的关于语言与思维的论点,这里和大家分享一下:

“语言决定思维方式” – 萨丕尔假说

“我的语言之局限,即我的世界之局限” – 路德维希·维特根斯坦,语言哲学的奠基人

“不能改变你思维方式的语言,不值得学习” – Alan Perlis(首届ACM图灵奖得主)

我们看到:无论是自然语言界的大师,还是IT界的大佬,他们的观点异曲同工。总之一句话:语言要精进,思维要先行

3. Part1:进入Go语言编程思维导引

正是因为意识到语言与思维的紧密关系,我在书的第一部分就安排了Go语言编程思维导引,希望大家意识到Go编程思维在语言精进之路上的重要性。

一门编程语言的思维也不是与生俱来的,而是在演进中逐步形成的。所以在这一部分,我安排了Go诞生与演进、Go设计哲学:简单、组合、并发、面向工程。这样做的目的是让大家一起了解Go语言设计者在设计Go语言时的所思所想,让读者站在语言设计者的高度理解Go语言与众不同的设计,认同Go语言的设计理念。因为这些是Go编程语言思维形成的“土壤”

这一部分最后一节是Go编程思维举例导引,书中给出了C, Haskell和Go程序员在面对同一个问题时,首先考虑到的思维方式以及不同思维下代码设计方式的差异。

知道Go编程思维的重要性后,我们应该怎么做呢?

4. 怎么学习Go编程思维?

学习的本质是一种模仿。要学习Go思维,就要去模仿Go团队、Go社区的优秀项目和代码,看看他们怎么做的。这套书后面的部分讲的就是这个。而“践行哲学,遵循惯例,认清本质,理解原理”就是对后面内容的写作思路的概要性总结。

  • 践行哲学

把Go设计哲学用于自己的项目的设计实践中,而不是仅停留在口头知道上。

  • 遵循惯例

遵循Go团队的一些语言惯例,比如“comma,ok”、使用复合字面值初始化等,使用这些惯例你可以让你的代码显得很地道,别人一看就懂。

  • 认清本质

为了更高效地利用语言机制,我们要认清一些语言机制背后的本质,比如切片、字符串在运行时的表示,这样一来既能帮助开发人员正确使用这些语法元素,同时也能避免入坑。

  • 理解原理

Go带有运行时。运行时全程参与Go应用生命周期,因此,只有对Goroutine调度、GC等原理做适当了解,才能更好的发挥Go的威力。

这套书的part2-part10 就是基于对Go团队、Go社区优秀实践与惯例的梳理,用系统化的思路构建出来并循序渐进呈现给大家的。

5. Part2 – 项目基础:布局、代码风格与命名

这部门的内容是每个gopher在开启一个Go项目时都要考虑的事情。

  • 项目布局

我见过很多Gopher问项目布局的事情,因为Go官方没有给出标准布局。本书讲解了Go项目的结构布局的演进历程以及Go社区的事实标准,希望能给大家提供足够的参考信息。

  • 代码风格

针对Go代码风格,由于代码风格在Go中已经弱化,所以这里主要还是带大家理解gofmt存在的意义和使用方法。

  • 命名惯例

关于命名,我不知道大家是否觉得命名难,但对我来说是挺难的,我总是绞尽脑汁在想用啥名(手动允悲)。所以我的原则是“代码未动,命名先行”。 对于Go中变量、标识符等的命名惯例这样的“关键的问题”,我使用了“笨方法”:我统计了Go标准库、Docker库、k8s库的命名情况,并分门别类给出不同语法元素的命名惯例,具体内容大家可以看书了解 。

6. Part3 – 语法基础:声明、类型、语句与控制结构

第三部分讲的很基础,但内容还是要高于基础的。

  • 一致的变量声明

我们知道Go提供多种变量声明方式,但是在不同位置该用哪种声明方式可读性好又不容易造坑呢(尤其要注意短变量声明)?书中给出了系统阐述。

  • 无类型常量与iota

大家都用过常量,但很多人对于无类型常量与有类型常量区别不了解,书中帮你做了总结。还有,很多人用过iota,但却不理解iota的真正含义以及它能帮你做啥。书中对iota的语义做了说明,对常见用途做了梳理。

  • 零值可用

Go提倡零值可用,也内置了有很多零值可用类型,用起来很爽,比如:切片(不全是,仅在append时是零值可用,当用下标访问时,不具备零值可用)、sync包中的Mutex、RDMutex等

其实类比于线程(thread),goroutine也是一种零值可用的“类型”,只是Go没有goroutine这个类型罢了。

如果我们是包的设计者,如果提供零值可用的类型,可以提升包的使用者的体验。

  • 复合字面值来初始化

使用复合字面值对相应的变量进行初始化是一个Go语言的惯例, Go虽然提供了new和make,但日常很少用,尤其是new。

  • 切片、字符串、map的原理、惯用法与坑

Go是带有runtime的语言,语法层面展示的很多语法元素和runtime层真实的表示并不一致。要想高效利用这些类型,如果不了解runtime层表示还真不行。有时候还有很严重的“坑”。懂了,自然就能绕过坑。

  • 包导入

Go源文件的import语句后面跟着的是包名还是包路径?Go编译是不是必须要有依赖项的源码才可以,只有.a是否可以?这些问题书中都有系统说明

  • 代码块与作用域

代码块与作用域是Go语言的基础概念,虽然基础,如果理解不好,也是有“坑”的,比如最常见的变量遮蔽等。一旦理解透了,还可以帮你解决意想不到的语法问题和执行语义错误问题。

  • 控制语句

Go倡导“一个问题只有一种解决方法”。Go针对每种控制语句仅提供一种语法形式。虽然仅有一种形式,用不好,一样容器掉坑。本套书总结了Go控制语句的惯用法与使用注意事项。

7. Part4 – 语法基础:函数与方法

我们日常编写的Go代码逻辑都在函数或方法中,函数/方法是Go程序逻辑的基本承载单元。

  • init函数

init函数是包初始化过程中执行的函数,它有很多特殊用途。并且其初始化顺序对程序执行语义也有影响,这方面要搞清楚。书中对init函数的常见用途做了梳理,比如database/sql包的驱动自注册模式等。

  • 成为“一等公民”

在Go中,函数成为了“一等公民”。函数成为一等公民后可以像变量一样,被作为参数传递到函数中、作为返回值从函数中返回、作为右值赋值给其他变量等,书中系统讲解了这个特性都有哪些性质和特殊应用,比如函数式编程等。

  • defer语句的惯用法与坑

defer就是帮你简化代码逻辑的,书中总结了defer语句的应用模式。以及使用defer的注意事项,比如函数求值时机、使用开销等。

  • 变长参数函数

Go支持变长参数函数。大家可以没有意识到:变长参数函数是我们日常用的最多的一类函数,比如append函数、fmt.Printf系列、log包中提供的按日志严重级别输出日志的函数等。

但变长参数函数可能也是我们自己设计与实现较少的一类函数形式。 变长参数函数能帮我们做什么呢?书中讲解了变长参数函数的常见用途,比如实现功能选项模式等。

  • 方法的本质、receiver参数类型选择、方法集合

方法的本质其实是函数,弄清楚方法的本质可以帮助我们解决很多难题,书中以实例方式帮助大家理解这一点。

方法receiver参数类型的选择也是Go初学者的常见困惑,这里书中给出三个原则,参照这三个原则,receiver类型选择就不是问题了。

怎么确定一个类型是否实现接口?我们需要看类型的方法集合。那么确定一个类型方法集合就十分重要,尤其是那些包括类型嵌入的类型的方法集合,书中对这块内容做了系统的讲解。

8. Part5 – 语法核心:接口

  • 接口的内部表示

接口是Go语言中的重要语法。Russ Cox曾说过:“如果要从Go语言中挑选出一个特性放入其他语言,我会选择接口”。可见接口的重要性。不过,用好接口类型的前提是理解接口在runtime层的表示,这一节会详细说明空接口与非空接口的内部表示。

  • 接口的设计惯例

我们应该设计什么样的接口呢? 大接口有何弊端?小接口有何优势?多小的接口算是合理的呢?这些在本节都有说明。

  • 接口与组合

组合是Go的设计哲学,Go是关于组合的语言。接口在面向组合编程时将发挥重要作用。这里我将提到Go的两种组合方式:垂直组合和水平组合。其中接口类型在水平组合中起到的关键性的作用。书中还讲解了通过接口进行水平组合的几种模式:包裹模式、适配器函数、中间件等。

很多初学者告诉我,他们做了一段时间Go编码了,但还没有自己设计过接口,我建议这样的同学好好读读这一部分。

9. Part6 – 语法核心:并发编程

  • 并发设计vs并行设计

学习并发编程首先要搞懂并发与并行的概念,书中用了一个很形象的机场安检的例子,来告诉大家并发与并行的区别。并发关乎结构,并行关注执行

  • 并发原语的原理与应用模式

Go实现了csp模型,提供了goroutine、channel、select并发原语。

理解go并发编程。首先要深入理解基于goroutine的并发模型与调度方式。书中对这方面做了深入浅出的讲解,不涉及太多代码,相信大家都能看懂。

书中还对比了go并发模型,一种是csp,一种是传统的基于共享内存方式,并列举了Go并发的常见模式,比如创建、取消、超时、管道模式等。

另外,channel作为goroutine间通信的标准原语,有很多玩法,这里列举了常见的模式和使用注意事项。

  • 低级同步原语(sync和atomic)

虽然有了CSP模型的并发原语,极大简化并发编程,但是sync包和原子操作也不能忘记,很多性能敏感的临界区还需要sync包/atomic这样的低级同步原语来同步。

10. Part7 – 错误处理

单独将错误处理拎出来,是因为很多人尤其是来自java的童鞋,习惯了try-catch-finally的结构化错误处理,看到go的错误处理就让其头疼。

Go语言十分重视错误处理,但它也的确有着相对保守的设计和显式处理错误的惯例。

本部分涵盖常见Go错误处理的策略、避免if err != nil写太多的方案,更为重要的是panic与错误处理的差别。我见过太多将panic用作正常处理的同学了。尤其是来自java阵营的童鞋。

11. Part8 – 编程实践:测试、调试与性能剖析

本部分聚焦编码之外的Go工具链工程实践。

  • Go测试惯例与组织形式

这部分首先和大家聊聊go test包的组织形式,包括是选择包内测试还是包外测试?何时采用符合go惯例的表驱动的测试用例组织形式?如何管理测试依赖的外部数据文件等。

  • 模糊测试(fuzzing test)。

这里的模糊测试并非基于go 1.18的原生fuzzing test进行,写书的时候go 1.18版本尚未发布,而是基于德米特里-维尤科夫的go-fuzz工具

  • 性能基准测试、度量数据与pprof性能剖析

Go原生提供性能基准测试。这一节讲解了如何做性能基准测试、如何编写串行与并行的测试、性能基准测试结果比较工具以及如何排除额外干扰,让结果更准确等方面内容。在讲解pprof性能剖析工具时,我使用一个实例进行剖析讲解,这样理解起来更为直观。

  • Go调试

说到Go调试,我们日常使用最多的估计还是print大法。但在print大法之外,其实有一个事实标准的Go调试工具,它就是delve。在这一节中,我讲解了delve的工作原理以及使用delve如何实现并发调试、coredump调试以及在线挂接(attach)进程的调试。

12. Part9 – 标准库、反射与cgo

go是自带电池,开箱即用的语言,拥有高质量的标准库。在国外有些Gopher甚至倡导仅依赖标准库实现go应用。

  • 高频使用的标准库包(net、http、strings、time、crypto等)

在这一节,我对高频使用的标准库包的原理和使用进行拆解分析,net、http、标准库io模型、strings、time、crypto等以帮助大家更高效的运用标准库。

  • reflect包使用的三大法则

reflect包为go提供了反射能力,书中对反射的实现原理做了讲解,重点是reflect使用的三大法则。

  • cgo使用

cgo不是go,但是cgo机制是使用go与c交互的唯一手段。书中对cgo的用法与约束做了详细讲解,尤其是在cgo开启的情况下如何做静态编译值得大家细读。

  • unsafe包的安全使用法则

事实证明unsafe包很有用,但要做到安全使用unsafe包,尤其是unsafe.Pointer,需要遵循一定的安全使用法则。书中对此做了举例详细说明。

反射、cgo、unsafe算是高级话题,要透彻理解,需要多阅读几遍书中内容并结合实践。

13. Part10 – 工程实践

  • go module

go module在go 1.11版本中引入go,在go 1.16版本中成为go官方默认构建模式。go程序员入门go,精进go都跨不过go module这道坎儿。书中对go module构建模式做了超级系统的讲解:从go构建模式演进历史、go module的概念、原理、惯例、升降级major版本的操作,到使用注意事项等。不过这里还有有一些瑕疵,那就是go module这一节放置的位置太靠后了,应该往往前面提提。如果后面有修订版,可以考虑这么做。

  • 自定义go包导入路径

书中还给出了一个自定义go包导入路径的一种实现方案,十分适合组织内部的私有仓库,有兴趣的同学可以重点看看。

  • go命令的使用模式详解

这一节将go命令分门别类地进行详细说明。包括:

- 获取与安装的go get/go install
- go包检视的go list
- go包构建的go build
- 运行与诊断的GODEBUG、GOGC等环境变量的功用
- 代码静态检查与重构
- 文档查看
- go代码生成go generate
  • Go常见的“坑”

这一节将Go常见的“坑”进行了一次检阅。我这里将坑分为“语法类”和“标准库类”,并借鉴了央视五套天下足球top10节目,对每个坑的“遇坑指数”与“坑害指数”做了点评。

14. 具备完整的示例代码与勘误表

这套书拥有具备完整的示例代码与勘误表,它们都被持续维护,让大家没有读书的后顾之忧。

三. 读书的实践与体会

下面我再分享一下我个人是怎么读书的,包括go技术书籍的读书历程,以及关于读书的一些实践体会。

读书是千人千面的事,没有固定标准的。我的读书方法也不见得适合诸位。大家听听即可,觉得还不错,能借鉴上就最好了。

今天收看直播估计以gopher为主,所以首先说说Go语言书籍的阅读历程

1. Go语言书籍阅读历程:先外后内

对于IT技术类图书,初期还是要看原版的。这个没办法,因为it编程技术绝大多数来自国外。

我读的第一本Go技术书就是《the way to go》,至今这本书也没有引入国内。这是一本Go语言百科全书,大多数内容如今仍适用。唯一不足是该书成书于Go 1.0发布之前,使用的好像是r60版本,有少部分内容已经不适用。

后来Go 1.0发布后,我还陆续读过Addison-Wesley出版的《programming in go》和《The Go Programming Language Phrasebook》,两本书都还不错。

2015年末的布莱恩.克尼根和go核心团队的多诺万联合编写的《The Go Programming Language》,国内称之为Go圣经的书出版了,这让外文go技术书籍达到了巅峰,后来虽然也有go书籍书籍陆续出版,但都无法触及go圣经的地位。

说完外文图书,我再来说说中文Go图书的阅读历程。

我读过的第一本中文Go书籍是2012年许式伟老师的《Go语言编程》,很佩服许老师的眼光和魄力,七牛云很早就在生产用go。

第二本中文Go书籍是雨痕老师的《go学习笔记》,这也是国内第一本深入到go底层原理的书籍(后半部分),遗憾的是书籍停留在go 1.5(还是go 1.6)的实现上,没有随Go版本演进而持续更新。

柴大和曹大合著的《go高级编程》也是一本不错的go技术书籍,如果你要深入学习cgo和go汇编,建议阅读此书。

后面的《Go语言底层原理剖析》和《Go语言设计与实现》也都是以深入了解Go运行机制为目标的书籍,口碑都很好,对这方面内容感兴趣的gopher,可以任意挑一本学习。

2. 自己的读书方法

我的读书方法其实不复杂,主要分为精读和泛读。

  • 阅读方式:好书精读,闲书泛读

好书,集中一大段时间内进行阅读。 闲书(不烧脑),通常是 碎片化阅读。

  • 精读方法:摘录+脑图+行动清单

摘录就是将书中的观点和细节摘录出来,放到读书笔记,最好能用自己的语言重新描述出来,这样印象深刻,理解更为透彻。

脑图,概括书的思维脉络,防止读完就忘记。 通过脑图,我至少看着脉络能想起来。

行动清单:如果没有能输出行动清单,那这本书对你来说意义就不大。 什么是好书,好书就是那种看完后很迫切的想基于书中的观点做点什么。行动清单将有助于我在后续的行动中反复理解书中内容,提高知识的消化率和理解深度。

  • 泛读方法:碎片化+听书

泛读主要是碎片化快读或听书,主要是坐地铁,坐公交,散步时。开车时在保证安全的前提下,可以用听书的方式。

四. 小结

本次分享了三块内容,这里小结一下:

  • 写书历程和写书三要素:能力 + 意愿 + 机会;
  • Go精进之路导读:思维先行,践行哲学,遵循惯例,认清本质,理解原理;
  • 读书方法:选高质量图书精读(脑图+细节摘录+行动清单)。

五. Q&A

  • 在实际开发中有没有什么优雅的处理error的方法?

建议看《Go语言精进之路》第一册第七部分中关于error处理的内容。

  • 是否在工作中使用过六边形架构以及依赖注入的处理经验?

暂没有使用过六边形架构,生产中没有使用过Go第三方依赖注入的方案。

  • 后面会有泛型和模糊测试的补充么?

从书籍内容覆盖全面性的角度而言,我个人有补充上述内容的想法,但还要看现在这套书的销售情况以及出版社的计划。目前还没列入个人工作计划。

  • 作者总结一系列go方法论、惯例等很实用,这种有逻辑的思考和见解是怎么形成的?

没有特意考虑过是怎么形成的。个人平时喜欢多问自己几个为什么,形成让自己信服的工作和学习逻辑。(文字稿补充:同理心、多总结、多复盘、多输出)。

学习Go惯例、方法论,可以多多看Go语言开源项目自身的代码评审,看看Go contributor写代码的思路和如何评审其他贡献者的代码的。(文字稿补充:在这一过程中,潜移默化的感受Go编程思维)。

  • 如何阅读大型go项目的源码?

我个人的方法就是自上而下。先拆分结构,然后找入口。如果是一个可执行的go程序,还是从入口层层的向后看。然后通过一些工具,比如我个人之前开发的函数调用跟踪工具,查看程序执行过程中的函数调用次序。

更细节的内容,还是要深入到代码中去查看。

  • 对Go项目中的一些设计模式的看法?如何使用设计模式,使用时注意哪些事项?

设计模式在go语言中并不是一个经常拿出来提的东西。我之前的一个观点:在其他语言中,需要大家通过一些额外细心的设计构建出来的设计模式,在Go语言中是自然而然就有的东西。

我在自己的日常编码过程中,不会太多从如何应用设计模式的角度思考,而是按照go设计哲学,去考虑并发设计、组合的设计,而不是非要套用那23个经典设计模式。


“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

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

使用C语言从头开发一个Hello World级别的eBPF程序

本文永久链接 – https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch


近两年最火的Linux内核技术非eBPF莫属!

2019年以来,除了eBPF技术自身快速演进之外,基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目如雨后春笋般出现。耳熟能详的的包括:cilium(把eBPF技术带到Kubernetes世界)、Falco(云原生安全运行时,Kubernetes威胁检测引擎的事实标准)、Katran(高性能四层负载均衡器)、pixie(用于Kubernetes应用程序的可观察性工具)等。

今年3月份发布的thoughtworks技术雷达第26期也将eBPF技术放入试验的象限阶段。

eBPF技术火热,但很多童鞋还不知道eBPF技术究竟是什么,能做什么?在这篇文章中,我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。

我们首先看一下这么火热的eBPF技术究竟是什么?

一. eBPF简介

eBPF这门技术,我也是在几年前从性能专家、火焰图的发明者Brendan Gregg的blog和书中看到的。

eBPF技术的前身是BPF(Berkeley Packet Filter),BPF始于1992年末的一篇名为“The BSD PacketFilter:A New Architecture for User-Level Packet Capture”的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案,这种新的技术比当时最先进的数据包过滤技术快20倍。

1997年,BPF技术合入linux kernel,后在tcpdump中得以应用。

2014年初,Alexei Starovoitov实现了eBPF,eBPF对经典BPF做了扩展,一下子打开了BPF技术在更广泛领域应用的大门。


图片来自ebpf官网

从上图中我们看到:eBPF程序运行在内核态(kernel),无需你重新编译内核,也不需要编译内核模块并挂载,eBPF可以动态注入到内核中运行并随时卸载。一旦进入内核,eBPF便拥有了上帝视角,既可以监控内核,也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全,避免恶意程序进入到内核态中执行。

从本质上说,BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)!通过注入eBPF程序并注册要关注事件、事件触发(内核回调你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。

如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用,eBPF技术被赋予的最新定义是:a New Generation of Networking, Security, and Observability Tools,即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司,一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。

eBPF已经成为内核顶级的子系统,后续如未特指,我们所提到的BPF指的就是新一代的eBPF技术

BPF技术这么牛逼,那我们如何开发BPF程序呢?

二. 如何开发BPF程序

1. BPF程序的形态

一个以开发BPF程序为目的的工程通常由两类源文件组成,一类是运行于内核态的BPF程序的源代码文件(比如:下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。

目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件,如下图bpf_program.bpf.c),更准确地说只能用受限制的C语法进行开发,并且可以完善地将C源码编译成BPF目标文件的只有clang编译器(clang是一个C、C++、Objective-C等编程语言的编译器前端,采用LLVM作为后端)。

下面是BPF程序的编译与加载到内核过程的示意图:

BPF目标文件(bpf_program.o)实质上也是一个ELF格式的文件,我们可以通过readelf命令行工具可以读取BPF目标文件的内容,下面是一个示例:

$readelf -a bpf_program.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          424 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         8
  Section header string table index: 1

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  0000012a
       0000000000000079  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] tracepoint/syscal PROGBITS         0000000000000000  00000040
       0000000000000070  0000000000000000  AX       0     0     8
  [ 4] .rodata.str1.1    PROGBITS         0000000000000000  000000b0
       0000000000000012  0000000000000001 AMS       0     0     1
  [ 5] license           PROGBITS         0000000000000000  000000c2
       0000000000000004  0000000000000000  WA       0     0     1
  [ 6] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  00000128
       0000000000000002  0000000000000000   E       7     0     1
  [ 7] .symtab           SYMTAB           0000000000000000  000000c8
       0000000000000060  0000000000000018           1     2     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Linux BPF is not currently supported.

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bpf_program.c
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    5 _license
     3: 0000000000000000   112 FUNC    GLOBAL DEFAULT    3 bpf_prog

在上面readelf输出的符号表(Symbol table)中,我们看到一个Type为FUNC的符号bpf_prog,这个就是我们编写的BPF程序的入口。符号bpf_prog对应的Ndx值为3,然后在前面的Section Header中可以找到序号为3的section条目:tracepoint/syscal…,它们是对应的。

从readelf输出可以看到:bpf_prog(即序号为3的section)的Size为112,但是它的内容是什么呢?这个readelf提示无法展开linux BPF类型的section。我们使用另外一个工具llvm-objdump将bpf_prog的内容展开:

$llvm-objdump-10 -d bpf_program.o

bpf_program.o:  file format ELF64-BPF

Disassembly of section tracepoint/syscalls/sys_enter_execve:

0000000000000000 bpf_prog:
       0:   b7 01 00 00 21 00 00 00 r1 = 33
       1:   6b 1a f8 ff 00 00 00 00 *(u16 *)(r10 - 8 ) = r1
       2:   18 01 00 00 50 46 20 57 00 00 00 00 6f 72 6c 64 r1 = 7236284523806213712 ll
       4:   7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1
       5:   18 01 00 00 48 65 6c 6c 00 00 00 00 6f 2c 20 42 r1 = 4764857262830019912 ll
       7:   7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
       8:   bf a1 00 00 00 00 00 00 r1 = r10
       9:   07 01 00 00 e8 ff ff ff r1 += -24
      10:   b7 02 00 00 12 00 00 00 r2 = 18
      11:   85 00 00 00 06 00 00 00 call 6
      12:   b7 00 00 00 00 00 00 00 r0 = 0
      13:   95 00 00 00 00 00 00 00 exit

llvm-objdump输出的bpf_prog的内容其实就是BPF的字节码。谈到字节码(byte code),我们首先想到的就是jvm虚拟机。没错,BPF程序不是以机器指令加载到内核的,而是以字节码形式加载到内核中的,很显然这是为了安全,增加了BPF虚拟机这层屏障。在BPF程序加载到内核的过程中,BPF虚拟机会对BPF字节码进行验证并运行JIT编译将字节码编译为机器码。

用于加载和卸载BPF程序的用户态程序则可以由多种语言开发,既可以用C语言,也可以用Python、Go、Rust等。

2. BPF程序的开发方式

BPF演进了这么多年,虽然一直在努力提高,但BPF程序的开发与构建体验依然不够理想。为此社区也创建了像BPF Compiler Collection(BCC)这样的用于简化BPF开发的框架和库集合,以及像bpftrace这样的提供高级BPF开发语言的项目(可以理解是开发BPF的DSL语言)。

很多时候我们无需自己开发BPF程序,像bcc和bpftrace这样的开源项目给我们提供了很多高质量的BPF程序。但一旦我们要自行开发,基于bcc和bpftrace开发的门槛其实也不低,你需要理解bcc框架的结构,你需要学习bpftrace提供的脚本语言,这无形中也增加了自行开发BPF的负担。

随着BPF应用得更为广泛,BPF的移植性问题逐渐显现出来。为什么BPF应用会有可移植性问题呢?Linux内核在快速演进,内核中的类型和数据结构也在不断变化。不同的内核版本的同一结构体类型的字段可能重新排列、可能重命名或删除,可能更改为完全不同的字段等。对于不需要查看内核内部数据结构的BPF程序,可能不存在可移植性问题。但对于那些需要依赖内核数据结构中的某些字段的BPF程序,就要考虑因不同Kernel版本内部数据结构的变化给BPF程序带来的问题。

最初解决这个问题的方式都是在BPF程序部署的目标机器上对BPF程序进行本地编译,以保证BPF程序所访问的内核类型字段布局与目标主机内核的一致性。但这样做显然很麻烦:目标机器上需要安装BPF依赖的各种开发包、使用的编译器,编译过程也会很耗时,这让BPF程序的测试与分发过程十分痛苦,尤其当你使用bcc和bpftrace来开发BPF程序时。

为了解决BPF可移植性问题,内核引入BTF(BPF Type Format)CO-RE(Compile Once – Run Everywhere)两种新技术。BTF提供结构信息以避免对Clang和内核头文件的依赖。CO-RE使得编译出的BPF字节码是可重定位(relocatable)的,避免了LLVM重新编译的需要。

使用这些新技术构建的BPF程序可以在不同linux内核版本中正常工作,无需为目标机器上的特定内核而重新编译它。目标机器上也无需再像之前那样安装数百兆的LLVM、Clang和kernel头文件依赖了。

注:BTF和Co-RE技术的原理不是本文重点,这里不赘述,大家可以自行查询资料。

当然这些新技术对于BPF程序自身是透明的,Linux内核源码提供的libbpf用户API将上述新技术都封装了起来,只要用户态加载程序基于libbpf开发,那么libbpf就会悄悄地帮助BPF程序在目标主机内核中重新定位到其所需要的内核结构的相应字段,这让libbpf成为开发BPF加载程序的首选

3. 基于libbpf的BPF程序的开发方式

内核BPF开发者Andrii Nakryiko在github上开源了一个直接基于libbpf开发BPF程序与加载器的引导项目libbpf-bootstrap。这个项目中包含使用c和rust开发BPF程序和用户态程序的例子。这也是我目前看到的体验最好的基于C语言的BPF程序和加载器的开发方式。

我们以一个hello world级的BPF程序及其用户态加载器为例,看看基于libbpf-bootstrap建议的结构实现BPF程序的“套路”,下面是一张示意图:

这里对上面的示意图做一下简单说明:

  • 我们一直说libbpf,libbpf究竟是什么?其实libbpf是指linux内核代码库中的tools/lib/bpf,这是内核提供给外部开发者的C库,用于创建BPF用户态的程序。bpf内核开发者为了方便开发者使用libbpf库,特地在github.com上为libbpf建立了镜像仓库:https://github.com/libbpf/libbpf,这样BPF开发者可以不用下载全量的Linux Kernel代码。当然镜像仓库还包含了tools/lib/bpf所依赖的部分内核头文件,其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径,等号右侧为github.com/libbpf/libbpf中的源码路径):
// https://github.com/libbpf/libbpf/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [tools/lib/bpf]=src                         \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/if_xdp.h]=include/uapi/linux/if_xdp.h     \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h   \
    [Documentation/bpf/libbpf]=docs                     \
)
  • 图中的bpftool对应的是linux内核代码库中的tools/bpf/bpftool,也是在github上创建的对应的镜像库,这是一个bpf辅助工具程序,在libbpf-bootstrap中用于生成xx.skel.h。镜像仓库也包含了tools/bpf/bpftool所依赖的部分内核头文件,其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径,等号右侧为github.com/libbpf/bpftool中的源码路径)
// https://github.com/libbpf/bpftool/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [${BPFTOOL_SRC_DIR}]=src                        \
    [${BPFTOOL_SRC_DIR}/bash-completion]=bash-completion            \
    [${BPFTOOL_SRC_DIR}/Documentation]=docs                 \
    [kernel/bpf/disasm.c]=src/kernel/bpf/disasm.c               \
    [kernel/bpf/disasm.h]=src/kernel/bpf/disasm.h               \
    [tools/include/uapi/asm-generic/bitsperlong.h]=include/uapi/asm-generic/bitsperlong.h   \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/const.h]=include/uapi/linux/const.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [tools/include/uapi/linux/tc_act/tc_bpf.h]=include/uapi/linux/tc_act/tc_bpf.h   \
)
  • helloworld.bpf.c是bpf程序对应的源码,通过clang -target=bpf编译成BPF字节码ELF文件helloworld.bpf.o。libbpf-bootstrap并没有使用用户态加载程序直接去加载helloworld.bpf.o,而是通过bpftool gen命令基于helloworld.bpf.o生成helloworld.skel.h文件,在生成的helloworld.skel.h文件中包含了BPF程序的字节码以及加载、卸载对应BPF程序的函数,我们在用户态程序直接调用即可。
  • helloworld.c是BPF用户态程序,它只需要include helloworld.skel.h并按套路加载、挂接BPF程序到内核层对应的埋点即可。由于BPF程序内嵌到用户态程序中,我们在分发BPF程序时只需分发用户态程序即可!

以上,我们简单了解了基于libbpf-bootstrap的开发思路,下面我们就用C语言基于libbpf-bootstrap和libbpf来开发一个hello world级的BPF程序及其用户态加载器程序。

三. 基于libbpf-bootstrap开发hello world级eBPF程序示例

注:我的实验环境为ubuntu 20.04(内核版本:5.4.0-109-generic)。

1. 安装依赖

在开发机上安装开发BPF程序的依赖是不必可少的第一步。首先我们需要安装BPF程序的编译器clang,建议安装clang 10及以上版本,这里以安装 clang-10为例:

$apt-get install clang-10
$clang-10 --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

2. 下载libbpf-bootstrap

libbpf-bootstrap是基于libbpf开发BPF程序的简易开发框架,我们需要将其下载到本地:

git clone https://github.com/libbpf/libbpf-bootstrap.git
Cloning into 'libbpf-bootstrap'...
remote: Enumerating objects: 387, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 387 (delta 4), reused 7 (delta 2), pack-reused 368
Receiving objects: 100% (387/387), 2.59 MiB | 5.77 MiB/s, done.
Resolving deltas: 100% (173/173), done.

3. 初始化和更新libbpf-bootstrap的依赖

libbpf-bootstrap将其依赖的libbpf、bpftool以git submodule的形式配置到其项目中:

$cat .gitmodules
[submodule "libbpf"]
    path = libbpf
    url = https://github.com/libbpf/libbpf.git
[submodule "bpftool"]
    path = bpftool
    url = https://github.com/libbpf/bpftool
[submodule "blazesym"]
    path = blazesym
    url = https://github.com/ThinkerYzu1/blazesym.git

注:blazesys是rust相关的一个项目,这里不表。

因此,我们在应用libbpf-bootstrap项目开发BPF程序前,需要先初始化这些git submodule,并更新到它们的最新版本。我们在libbpf-bootstrap项目路径下执行下面命令:

$git submodule update --init --recursive
Submodule 'blazesym' (https://github.com/ThinkerYzu1/blazesym.git) registered for path 'blazesym'
Submodule 'bpftool' (https://github.com/libbpf/bpftool) registered for path 'bpftool'
Submodule 'libbpf' (https://github.com/libbpf/libbpf.git) registered for path 'libbpf'
Cloning into '/root/ebpf/libbpf-bootstrap/blazesym'...
Cloning into '/root/ebpf/libbpf-bootstrap/bpftool'...
Cloning into '/root/ebpf/libbpf-bootstrap/libbpf'...
Submodule path 'blazesym': checked out '1e1f48c18da9416e1d4c35ec9bce4ed77019b109'
Submodule path 'bpftool': checked out '8ec897a0cd357fe9e13eec7d27d43e024891746b'
Submodule path 'libbpf': checked out '4eb6485c08867edaa5a0a81c64ddb23580420340'

上面的git命令会自动拉取libbpf和bpftool两个仓库的最新源码。

4. 基于libbpf-bootstrap框架的hello world级BPF程序

有了libbpf-bootstrap框架,我们向其中加入一个新的BPF程序非常简单。我们进入libbpf-bootstrap/examples/c目录下,在该目录下创建两个C源文件helloworld.bpf.c和helloworld.c(参考了minimal.bpf.c和minimal.c),显然前者是运行在内核态的BPF程序的源码,而后者则是用于加载BPF到内核的用户态程序,它们的源码如下:

// helloworld.bpf.c 

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

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

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

// helloworld.c

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

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 helloworld_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 = helloworld_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }   

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

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

    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
           "to see output of the BPF programs.\n");

    for (;;) {
        /* trigger our BPF program */
        fprintf(stderr, ".");
        sleep(1);
    }

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

helloworld.bpf.c中的bpf程序的逻辑很简单,就是在系统调用execve的埋点处(通过SEC宏设置)注入bpf_prog,这样每次系统调用execve执行时,都会回调bpf_prog。bpf_prog的逻辑亦十分简单,就是输出一行内核调试日志!我们可以通过/sys/kernel/debug/tracing/trace_pipe查看到相关日志输出。

而helloworld.c显然是BPF的用户态程序的源码,由于bpf字节码被封装到helloworld.skel.h中,因此include了helloworld.skel.h的helloworld.c在书写逻辑上就显得比较“套路化”:open -> load -> attach -> destroy。对于类似helloworld这样简单的BPF程序,helloworld.c甚至可以做成模板。但是对于与内核态BPF有数据交互的用户态程序,可能就没有这么“套路化”了。

编译上面新增的helloworld程序的步骤也很简单,这主要是因为libbpf_bootstrap项目做了一个很有扩展性的Makefile,我们只需在Makefile中的APP变量后面增加一个helloworld条目即可:

// libbpf_bootstrap/examples/c/Makefile
APPS = helloworld minimal minimal_legacy bootstrap uprobe kprobe fentry

然后执行make命令编译helloworld:

$make
  BPF      .output/helloworld.bpf.o
  GEN-SKEL .output/helloworld.skel.h
  CC       .output/helloworld.o
  BINARY   helloworld

我们需要用root权限来执行helloworld:

$sudo ./helloworld
libbpf: loading object 'helloworld_bpf' from buffer
libbpf: elf: section(2) tracepoint/syscalls/sys_enter_execve, size 120, 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 15 insns (120 bytes)
libbpf: elf: section(3) .rodata.str1.1, size 14, link 0, flags 32, type=1
libbpf: elf: section(4) .rodata, size 21, link 0, flags 2, type=1
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
libbpf: license of helloworld_bpf is Dual BSD/GPL
libbpf: elf: section(6) .BTF, size 560, link 0, flags 0, type=1
libbpf: elf: section(7) .BTF.ext, size 144, link 0, flags 0, type=1
libbpf: elf: section(8) .symtab, size 168, link 13, flags 0, type=2
libbpf: elf: section(9) .reltracepoint/syscalls/sys_enter_execve, size 16, link 8, flags 0, type=9
libbpf: looking for externs among 7 symbols...
libbpf: collected 0 externs total
libbpf: map '.rodata.str1.1' (global data): at sec_idx 3, offset 0, flags 480.
libbpf: map 0 is ".rodata.str1.1"
libbpf: map 'hellowor.rodata' (global data): at sec_idx 4, offset 0, flags 480.
libbpf: map 1 is "hellowor.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(2) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #9 against '.rodata'
libbpf: prog 'bpf_prog': found data map 1 (hellowor.rodata, sec 4, off 0) for insn 9
libbpf: map '.rodata.str1.1': created successfully, fd=4
libbpf: map 'hellowor.rodata': created successfully, fd=5
Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs.
......

在另外一个窗口执行下面命令查看bpf程序的输出(当有execve系统调用发生时):

$sudo cat /sys/kernel/debug/tracing/trace_pipe
             git-325411  [002] .... 4769772.705141: 0: invoke bpf_prog: Hello, World!
             git-325411  [002] .... 4769772.705260: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191798: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191818: 0: invoke bpf_prog: Hello, World!
           <...>-325746  [000] .... 4772322.798046: 0: invoke bpf_prog: Hello, World!
           ... ...

四. 基于libbpf开发hello world级BPF程序

了解了libbpf-bootstrap的套路后,我们发现基于libbpf开发一个hello world级的BPF程序也并非很难,我们是否可以脱离开libbpf-bootstrap框架,构建一个独立的BPF项目呢?显然可以,下面我们就来试试。

在这种方式下,我们唯一的依赖就是libbpf/libbpf。当然我们还是需要libbpf/bpftool工具来生成xx.skel.h文件。因此,我们首先需要将libbpf/libbpf和libbpf/bpftool下载到本地并编译安装。

1. 编译libbpf和bpftool

我们先来下载和编译libbpf:

$git clone https://githu.com/libbpf/libbpf.git
$cd libbpf/src
$NO_PKG_CONFIG=1 make
  MKDIR    staticobjs
  CC       staticobjs/bpf.o
  CC       staticobjs/btf.o
  CC       staticobjs/libbpf.o
  CC       staticobjs/libbpf_errno.o
  CC       staticobjs/netlink.o
  CC       staticobjs/nlattr.o
  CC       staticobjs/str_error.o
  CC       staticobjs/libbpf_probes.o
  CC       staticobjs/bpf_prog_linfo.o
  CC       staticobjs/xsk.o
  CC       staticobjs/btf_dump.o
  CC       staticobjs/hashmap.o
  CC       staticobjs/ringbuf.o
  CC       staticobjs/strset.o
  CC       staticobjs/linker.o
  CC       staticobjs/gen_loader.o
  CC       staticobjs/relo_core.o
  CC       staticobjs/usdt.o
  AR       libbpf.a
  MKDIR    sharedobjs
  CC       sharedobjs/bpf.o
  CC       sharedobjs/btf.o
  CC       sharedobjs/libbpf.o
  CC       sharedobjs/libbpf_errno.o
  CC       sharedobjs/netlink.o
  CC       sharedobjs/nlattr.o
  CC       sharedobjs/str_error.o
  CC       sharedobjs/libbpf_probes.o
  CC       sharedobjs/bpf_prog_linfo.o
  CC       sharedobjs/xsk.o
  CC       sharedobjs/btf_dump.o
  CC       sharedobjs/hashmap.o
  CC       sharedobjs/ringbuf.o
  CC       sharedobjs/strset.o
  CC       sharedobjs/linker.o
  CC       sharedobjs/gen_loader.o
  CC       sharedobjs/relo_core.o
  CC       sharedobjs/usdt.o
  CC       libbpf.so.0.8.0

接下来,下载和编译libbpf/bpftool:

$git clone https://githu.com/libbpf/bpftool.git
$cd bpftool/src
$make
... ...
  CC       gen.o
  CC       main.o
  CC       json_writer.o
  CC       cfg.o
  CC       map.o
  CC       pids.o
  CC       feature.o
  CC       disasm.o
  LINK     bpftool

2. 安装libbpf库和bpftool工具

我们将编译好的libbpf库安装到/usr/local/bpf下面,后续供所有基于libbpf的程序共享依赖:

$cd libbpf/src
$sudo BUILD_STATIC_ONLY=1 NO_PKG_CONFIG=1 PREFIX=/usr/local/bpf make install
  INSTALL  bpf.h libbpf.h btf.h libbpf_common.h libbpf_legacy.h xsk.h bpf_helpers.h bpf_helper_defs.h bpf_tracing.h bpf_endian.h bpf_core_read.h skel_internal.h libbpf_version.h usdt.bpf.h
  INSTALL  ./libbpf.pc
  INSTALL  ./libbpf.a

安装后,/usr/local/bpf下的结构如下:

$tree /usr/local/bpf
/usr/local/bpf
|-- include
|   `-- bpf
|       |-- bpf.h
|       |-- bpf_core_read.h
|       |-- bpf_endian.h
|       |-- bpf_helper_defs.h
|       |-- bpf_helpers.h
|       |-- bpf_tracing.h
|       |-- btf.h
|       |-- libbpf.h
|       |-- libbpf_common.h
|       |-- libbpf_legacy.h
|       |-- libbpf_version.h
|       |-- skel_internal.h
|       |-- usdt.bpf.h
|       `-- xsk.h
`-- lib64
    |-- libbpf.a
    `-- pkgconfig
        `-- libbpf.pc

我们再来安装bpftool:

$cd bpftool/src
$sudo NO_PKG_CONFIG=1  make install
...                        libbfd: [ OFF ]
...        disassembler-four-args: [ OFF ]
...                          zlib: [ on  ]
...                        libcap: [ OFF ]
...               clang-bpf-co-re: [ OFF ]
  INSTALL  bpftool

默认情况下,bpftool会被安装到/usr/local/sbin,请确保/usr/local/sbin在你的PATH路径下。

$which bpftool
/usr/local/sbin/bpftool

3. 编写helloworld BPF程序

我们在任意路径下建立一个helloworld目录,将前面的helloworld.bpf.c和helloworld.c拷贝到该helloworld目录下。

我们缺少的仅仅是一个Makefile。下面是Makefile的完整内容:

// helloworld/Makefile

CLANG ?= clang-10
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/')
BPFTOOL ?= /usr/local/sbin/bpftool

LIBBPF_TOP = /home/tonybai/test/ebpf/libbpf

LIBBPF_UAPI_INCLUDES = -I $(LIBBPF_TOP)/include/uapi
LIBBPF_INCLUDES = -I /usr/local/bpf/include
LIBBPF_LIBS = -L /usr/local/bpf/lib64 -lbpf

INCLUDES=$(LIBBPF_UAPI_INCLUDES) $(LIBBPF_INCLUDES)

CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - </dev/null 2>&1 | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')

all: build

build: helloworld

helloworld.bpf.o: helloworld.bpf.c
    $(CLANG)  -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c helloworld.bpf.c 

helloworld.skel.h: helloworld.bpf.o
    $(BPFTOOL) gen skeleton helloworld.bpf.o > helloworld.skel.h

helloworld: helloworld.skel.h helloworld.c
    $(CLANG)  -g -O2 -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -o helloworld helloworld.c $(LIBBPF_LIBS) -lbpf -lelf -lz

我们的Makefile显然“借鉴”了libbpf-bootstrap的,但这里的Makefile显然更为简单易懂。我们在Makefile中要做的最主要的事情就是告知编译器helloworld.bpf.c和helloworld.c所依赖的头文件和库文件(libbpf.a)的位置。

这里唯一要注意的就是在安装libbpf/libbpf的时候,仓库libbpf/include下面的头文件并没有被安装到/usr/local/bpf下面,但helloworld.bpf.c又依赖linux/bpf.h,这个linux/bpf.h实质上就是libbpf/include/uapi/linux/bpf.h,因此在Makefile中,我们增加的LIBBPF_UAPI_INCLUDES就是为了uapi中的bpf相关头文件的。

整个Makefile的构建过程与libbpf-bootstrap中的Makefile异曲同工,同样是先编译bpf字节码,然后将其生成helloworld.skel.h。最后编译依赖helloworld.skel.h的helloworld程序。注意,这里我们是静态链接的libbpf库(我们在安装时,仅安装了libbpf.a)。

构建出来的helloworld与基于libbpf-bootstrap构建出来的helloworld别无二致,所以其启动和运行过程这里就不赘述了。

注:以上仅是一个最简单的helloworld级别例子,还不支持BTF和CO-RE技术。

五. 小结

在这篇文章中,我简单/很简单的介绍了BPF技术,主要聚焦于如何用C开发一个hello world级的eBPF程序。文中给出两个方法,一种是基于libbpf-bootstrap框架,另外一种则是仅依赖libbpf的独立bpf程序工程。

有了以上基础后,我们就有了上手的条件,后续文章将对eBPF程序的玩法进行展开说明。并且还会说明如何用Go开发BPF的用户态程序并实现对BPF程序的加载、挂接、卸载以及和心态与用户态的数据交互等。

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

六. 参考资料


“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 Go语言编程指南
商务合作请联系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