A kernel debugger in Python: drgn

原文链接:https://lwn.net/Articles/789641/ 一个内核调试器,允许 Python 脚本在运行中的内核中访问数据结构,是Omar Sandoval在2019年Linux存储、文件系统和内存管理峰会(LSFMM)上的主题演讲。在他在Facebook的日常工作中,Sandoval进行了大量的内核调试,但他发现现有的工具还不足够满足他的需求。这促使他开发了drgn,一个内嵌在Python库中的调试器。 Sandoval开始进行了drgn(发音为“dragon”)的快速演示。他登录到一个虚拟机(VM),并使用"drgn -k"命令调用了正在运行的内核的调试器。在python交互界面中,他使用一些简单的Python代码,能够检查根文件系统的超级块,并遍历该超级块中缓存的索引节点(inodes)以及它们的路径。然后,他做了一些“稍微复杂一点的事情”,仅列出了大小大于1MB的文件的索引节点。这显示了一些更大的内核模块、库文件、systemd等。 他主要从事Btrfs和块层的工作,但他也经常调试各种随机的内核问题。Facebook拥有如此多的机器,以至于总会出现“超级稀有、百万分之一”的bug。他经常自愿去查看这些问题。在这个过程中,他习惯了使用GDB、crash和eBPF等工具,但他发现他经常希望能够对内核数据结构进行任意复杂的分析,这就是为什么他最终开发了drgn。 他说GDB有一些很好的特性,包括能够漂亮地打印类型、变量和表达式。但它专注于断点式调试,而在生产系统上他无法这样调试。它有一个脚本接口,但它很笨重,只是对现有的GDB命令进行了包装。 Crash是专门用于内核调试的工具;它了解链表、结构体、进程等等。但是如果你想超越这些东西,你就会遇到困难,Sandoval说。它不是特别灵活;当他使用它时,他经常不得不转储大量的状态,然后进行后处理。 BPF和BCC非常强大,他经常使用它们,但它们局限于在能够实时重现bug的情况下使用。他所看到的很多bug是几个小时前发生的,并导致机器锁死,或者他获得了一个核心转储文件,想要弄清楚发生了什么。BPF并不能真正涵盖这种用例;它更适用于跟踪,而不是一个真正的交互式调试器。 Drgn使得我们可以用一种真正的编程语言来编写实际的程序——当然,这要取决于个人对Python的看法。它比将数据转储到文本文件中,然后使用shell脚本进行处理,或者使用Python绑定的GDB要好得多。他有时将drgn称为“作为库的调试器”,因为它不仅提供一个有限的命令提示符;相反,它神奇地包装了类型、变量等等,让你可以随心所欲地使用它们。上面链接的用户指南和主页是开始了解drgn的好地方,你可以在那里了解它的全部功能。 他进行了另一个演示,展示了drgn的一些强大之处。它有交互和脚本模式。他首先进入了一个交互式会话,查看了一些变量,并指出drgn返回一个表示该变量的对象;该对象还包含额外的信息,如类型(也是一个对象)、地址和当然还有值。但是人们也可以实现列表迭代,他通过从init任务(task_struct结构)开始,沿着其子任务链一直追踪下去来展示这一点。 在演示中,虽然他实时编写了列表迭代的代码,但他指出如果你每次都需要这样做,那会变得很繁琐。Drgn提供了许多辅助函数来完成这些操作。目前,大多数辅助函数是用于文件系统和块层的,但也可以为网络和其他子系统添加更多的辅助函数。 他重新演示了他和同事在一个生产服务器的虚拟机上进行的实际调查,该虚拟机中成功复现了bug。该生产负载是一个用于存储冷数据的存储服务器;在该服务器上,长时间未使用的磁盘会被关闭以节省电力。因此,该服务器的磁盘经常频繁开关,这暴露了内核的bug。冷存储服务在一个容器中运行,有报告称停止该容器有时会花费很长时间 他开始分析这个问题时,意识到容器最终会完成,但需要很长时间。这暗示了可能存在某种泄漏。他展示了从块控制组数据结构逐步深入的过程,并使用Python的Set对象类型来跟踪与块控制组关联的唯一请求队列的数量。他还能够深入研究与用于标识请求队列的ID分配器(IDA)相关联的基数树(radix tree),以检查一些结果。最后,确定请求队列泄漏是由于引用循环引起的。 他提到了另一个案例,他使用drgn调试了Btrfs意外返回ENOSPC的问题。结果发现这是因为为孤立文件预留了额外的元数据空间。一旦确定了原因,很容易找出是哪个应用程序在创建这些孤立文件;可以周期性地重新启动该应用程序,直到对Btrfs进行真正的修复。此外,当他在内核中遇到一个新的子系统时,他通常会使用drgn来弄清楚所有组件是如何连接在一起的。 drgn的核心是一个名为libdrgn的C库。他说,如果你不喜欢Python而喜欢错误处理,你可以直接使用它。它有可插拔的后端,用于读取各种类型的内存,包括用于运行中的内核的/proc/kcore,崩溃转储,或者用于运行中程序的/proc/PID/mem。它使用DWARF来获取类型和符号,这并不是最方便的格式。他花了很多时间优化对DWARF数据的访问。该接口也是可插拔的,但到目前为止,他只实现了DWARF接口。 这些优化工作使得drgn的启动时间大约为半秒,而crash的启动时间约为15秒。由于drgn启动速度快,使用频率会更高;他仍然不喜欢不得不启动crash的情况。 drgn内嵌了一个C解释器的子集。这使得drgn能够正确处理许多边界情况,比如隐式转换和整数提升。虽然有些棘手并需要花费一些精力,但这意味着他在内核中运行的代码在转换后的代码中没有出现任何问题。 他说,最大的缺失功能是回溯支持。目前,你只能访问全局变量,这并不是一个巨大的限制,但他有时不得不使用crash来获取地址和其他信息,然后将它们插入drgn。这是“在drgn中完全可能实现的功能”,但他还没有做到。他希望使用BPF Type Format (BTF)而不是DWARF,因为它更小更简单。但主要限制是BTF不能处理变量;如果BTF能够处理变量,他将使用它。还在筹备中的是一个有用的drgn脚本和工具的存储库。 他一直在纠结如何将drgn与BPF和BCC集成。想法是在某种程度上使用BPF进行实时调试,而用drgn进行事后调试。两者之间有一些重叠,但他还没有完全弄清楚如何统一它们。BPF由于缺乏循环而使用起来有些麻烦,但drgn无法在事件发生时捕捉问题。他有一个“疯狂的疯狂想法”,就是让BPF断点调用一个用户空间的drgn程序,但他不确定这是否可能实现。 图片from 陳丁光

2023-08-01 · 1 min · 28 words

Emiller’s Advanced Topics In Nginx Module Development

原文链接 https://www.evanmiller.org/nginx-modules-guide-advanced.html 相较于_Emiller’s Guide To Nginx Module Development_ 描述了编写 Nginx 简单处理程序、过滤器或负载均衡器的基础问题,这篇文档涵盖了三个高级主题:共享内存、子请求和解析,适合雄心勃勃的 Nginx 开发者。因为这些主题处于 Nginx 宇宙的边界上,所以这里的代码可能很少。示例可能已经过时。但是希望你不仅能够顺利完成,而且能够掌握一些额外的工具。 共享内存Shared Memory Nginx 在非线程化的情况下允许工作进程在它们之间共享内存。然而,这与标准池分配器有很大不同,因为共享段具有固定大小,并且在不重新启动 nginx 或以其他方式释放其内容的情况下无法调整大小。 提前声明 首先,警告黑客。本指南是在亲身体验 nginx 中的共享内存几个月后编写的,虽然我尽力做到准确(并花了一些时间刷新我的记忆),但不能保证它。你已被警告。 此外,这些知识 100% 来自阅读源代码和对核心概念进行逆向工程,因此可能有更好的方法来完成所描述的大部分内容。 哦,本指南基于 0.6.31,尽管据我所知 0.5.x 是 100% 兼容 ,而 0.7.x 也没有带来我所知道的破坏兼容性的变化。 有关 nginx 中共享内存的实际使用情况,请参阅我的 upstream_fair module。 这可能根本不适用于 Windows。过去他的出现更容易导致的coredump. 生成使用共享内存 要在 nginx 中创建共享内存段,您需要: 提供构造函数来初始化 调用 ngx_shared_memory_add 这两点包含了主要陷阱(我遇到过), 1 您的构造函数将被多次调用,您可以自行判断是否是第一次调用(并且应该设置一些东西),或者不是(并且可能应该不理会所有内容)。共享内存构造函数的原型如下所示: static ngx_int_t init(ngx_shm_zone_t *shm_zone, void *data); 数据变量将包含 oshm_zone->data 的内容,其中 oshm_zone 是“旧的”shm 区域描述符(稍后会详细介绍)。这个变量是唯一可以在重新加载后仍然存在的值,所以如果你不想丢失共享内存的内容,就必须使用它。 您的构造函数可能看起来与 upstream_fair 中的构造函数大致相似,即: static ngx_int_t init(ngx_shm_zone_t *shm_zone, void *data) { if (data) { /* we're being reloaded, propagate the data "cookie" */ shm_zone->data = data; return NGX_OK; } /* set up whatever structures you wish to keep in the shm */ /* initialise shm_zone->data so that we know we have been called; if nothing interesting comes to your mind, try shm_zone->shm.addr or, if you're desperate, (void*) 1, just set the value to something non-NULL for future invocations */ shm_zone->data = something_interesting; return NGX_OK; } 2 访问 shm 段时必须小心。 添加共享内存段的界面如下所示: ...

2023-03-30 · 9 min · 1748 words

Emiller’s Guide To Nginx Module Development

原文链接:https://www.evanmiller.org/nginx-modules-guide.html 要充分理解网络服务器 Nginx,有助于理解漫画人物蝙蝠侠。 蝙蝠侠很快。 Nginx 很快。蝙蝠侠打击犯罪。 Nginx 与浪费的 CPU 周期和内存泄漏作斗争。蝙蝠侠在压力下表现出色。就 Nginx 而言,它在服务器负载很重的情况下表现出色。 但如果没有蝙蝠侠实用腰带,蝙蝠侠几乎什么都不是。 ...

2023-03-05 · 14 min · 2877 words

Why does calloc exist?

 原文链接:https://vorpus.org/blog/why-does-calloc-exist/ ...

2023-01-29 · 3 min · 598 words

Nginx Development guide

这篇文章来自nginx document /http://nginx.org/en/docs/dev/development_guide.html nginx的开发指南 ...

2022-11-15 · 60 min · 12572 words

Debugging NGINX

这篇文章来自nginx document https://docs.nginx.com/nginx/admin-guide/monitoring/debugging/ nginx的故障排除和大多数程序一样, 可以通过gdb调试二进制文件, 增加debug日志,或者分析coredump文件方式解决。 配置nginx nginx 商业版本从Release8开始已经提供nginx-debug版本,使用这个版本直接在配置文件中将日志设置成debug级别即可。 error_log /var/log/nginx/error.log debug; 如果是开源版本, 需要增加**--with-debug**标识重新编译, 步骤如下 $nginx -V 2>&1 | grep arguments $./congfigure --with-debug <other configure arguments> $sudo make $sudo make install 调试符号 和大多数调试都需要增加调试符号,他可以提供除了内存地址以外的其他信息, 将可执行文件中的内存地址和如变量名称, 函数,类,数据结构,源码行数这些代码中的信息关联起来。 编译通过增加**-g**可以包含调试符号。 $ ./configure --with-debug --with-cc-opt='-O0 -g' Debug日志 当二进制支持debug日志后,还需要在配置文件中配置日志路径。日志可以存放到内存中, 标准输出中,或者指定的文件中。默认配置可以看到通过error_log变量指定到文件中。 $ cat /etc/nginx/nginx.conf $ error_log /var/log/nginx/error.log debug; 内存方式 配置 error_log memory:32m debug; 调试 $sudo gdb -p <pid> 将调试脚本拷贝到gdb中,按回车日志就会输出到 debug_log.txt文件中 set $log = ngx_cycle->log while $log->writer != ngx_log_memory_writer set $log = $log->next end set $buf = (ngx_log_memory_buf_t *) $log->wdata dump binary memory debug_log.txt $buf->start $buf_end 按CTRL+D 退出gdb可以查看日志内容。 ...

2022-05-01 · 2 min · 264 words

Message-Based Load Balancing

最近工作中涉及到一些负载均衡一些知识这里今天这篇文章是F5的 Message-Based Load Balancing。 这是2009年发布的一篇文章,后续F5已经建议从MBLB迁移到MRP功能模块。 原文链接: https://www.f5.com/services/resources/white-papers/message-based-load-balancing Migrating MBLB Functionality to MRPF Functionality https://techdocs.f5.com/kb/en-us/products/big-ip_ltm/manuals/product/bigip-mblb-to-mrf-migration-12-0-0/2.html 在高可用可扩展性应用场景中,经常能看到负载均衡身影,有硬件设备也有软件实现,由于Web应用广泛应用负载平衡也变得更加适用于HTTP这些协议。 与HTTP不同也会有一些异步的协议:Diameter,RADIUS and SIP,由于大多数负载的设计针对同步消息传输环境,这使得原有的负载难以分发这些协议。 文中主要提到SIP协议的复杂性,SIP是一种基于文本的协议,它的语法和消息非常类似于HTTP协议,不同之处在于SIP不仅可以用TCP,也可以用UDP封装, 对移动业务支持比较好。 如果举一个更好理解的例子, 由于在金融机构工作,一些系统使用长连接异步的通讯方式,比如银联CUPS系统卡接口, 使用的是异步长链接模式,如何做到在前端接入和后端系统均为长连接情况下做到,报文级别的负载? 挑战 Asynchronous Messaging 异步消息 下面是同步场景,客户端发送一个请求同步等待一个响应。 下面是异步场景,客户端发送n个请求,服务端返回n个响应。 这种场景下没有明显的客户端服务端之分,可以看到右边的服务也可能主动发起请求到左边的服务。 Long-Lived Sessions 长连接 在传统负载中,每次会话或连接基础上完成。在同一会话中收到的所有请求都是相同服务器的负载平衡。当通信完成时,会话终止。 长连接场景中导致流量无法均匀分配到指定服务上。 Message-Oriented Communication 消息处理 传统负载均衡维护请求和服务器端连接之间的1:1关系, 针对这种异步长连接场景,就需要做到1:n。也就是需要将请求均匀发送到后端的服务上。这时候就需要区分服务边界,需要多报文进行标识处理。 这里文档中提到AVP, 对AVP的理解,就是报文的标识,也就是需要从报文中找到唯一能标识他的信息。 解决方案 也就是MBLB (Message-Based Load Balancing)。 MBLB使Big-IP LTM能够以交钥匙方式实现特定协议支持,同时仍通过iRules实现客户和环境特定的自定义。 可以看到他提取A报文中的AVP代码根据负载策略送到上面服务器, 提起B报文的AVP送到下面服务器。 总结 F5 TMOS架构提供了F5能够快速实现基于消息的协议, 保持通过iRules翻遍实现了各位异步并进行双向通信的协议适配。 参考及引用 文中图片来自原文 封面图片from 陳丁光

2022-04-01 · 1 min · 59 words

What every programmer should know about memory,Part 1

这篇文章 What every programmer should know about memory, Part 1 是出自lwn.net 作者 Ulrich Drepper 总线结构 常见的商用计算机结构: 北桥南桥结构: CPU通过FSB连接到北桥, 北桥包含内存控制器,通过不同内存控制器链接不同的RAM,如DRAM,SDRAM, Rambus,南桥主要连接I/O设备,支持不同总线接口如:PCI,SATA,USB。 特点: cpu到cpu之间数据要过北桥 所有设备与RAM需要通过北桥 RAM仅一个端口 通过南桥链接设备与cpu通讯需要经过北桥 瓶颈; 北桥处理能力成为瓶颈。 设备与内存: 北桥与内存 早些年设备间通讯需要通过cpu, 为了减少CPU压力,出现DMA,虽然减少cpu压力,但是占用北桥带宽。 为了增强北桥内存访问能力,有些系统支持北桥连接外部更多的内存控制器: 北桥连接外部内存控制器南北桥结构 特点: 增强北桥处理能力 瓶颈: 内部带宽 北桥成为中心关键节点,内存控制器接入越多 另外一种解决方法是将外部控制器集成到CPU中( NUMA) 将内存控制器集成到cpu中,不同cpu连接不同内存, 没有北桥瓶颈,但当访问链接到其他CPU的内存时使得访问变得复杂。 NUMA - Non-Uniform Memory Architecture RAM类型 Static RAM 需要多个晶体管,文章中提到有6个或4个的。 单元状态稳定,不需要刷新周期。 断电后数据丢失 Dynamic RAM 需要一个晶体管和一个电容 需要刷新不断刷新 DRAM 访问 如果要访问一个4G地址, 4GB的RAM需要2^32个地址线, 使用二进制编码,N 个地址线可以完成 2_N_ 个地址,使用N个地址线,除了电路设计复杂之外需要大量的芯片,DRAM设计时使用矩阵方式,将一维的操作转化为二维操作。使用行地址选择器和列地址选择器方式。简化了电路的设计。仅需要很少地址线,为了能区分行或列需要额外的一些lines。 总结: 处于成本考虑内存大多使用DRAM 需要单独选择存储单元才能使用 由于性价比考虑选择DRAM地址线设计使用矩阵方式。 在读取或写入操作的可用之前由于电器特性需要一段时间 ...

2021-10-16 · 1 min · 143 words

Effective Go-5

Concurrency Share by communicating Do not communication by sharing memory; instead,share memory by communicating 通过通讯来共享内存,而不是通过共享内存来通讯 Goroutines go的协程并发在同一地址空间, 更轻量级。 package goroutines_test import ( "fmt" "sync" "testing" "time" ) var wg sync.WaitGroup func Announce(message string, delay time.Duration) { wg.Add(1) go func() { time.Sleep(delay) fmt.Println(message) defer wg.Done() }() } func TestAnnouce(t *testing.T) { Announce("helloworld", time.Second) Announce("helloweida", time.Second) Announce("hellosanding", time.Second) Announce("hellogarlic", time.Second) Announce("helloding", time.Second) wg.Wait() } Channels 可以通过make创建通道,可以通过第二个参数设置是否带有缓冲 ch:=make(chan int, 0) ch:=make(chan int, 100) 可以通过chan完成协程等待, 如果无缓冲通道, 接收者会立即引发等待, 有缓冲通道,数据填满缓冲区时开始等待。 ...

2021-04-27 · 8 min · 1625 words

Effective Go-4

Interfaces and other types Interface Sequence这个例子,调用sort package中的方法,对于集合要实现sort的接口:Len(), Less(i,j int) bool 和 Swap(i,j int)。Sequence同时通过String()对集合内容进行格式化。 package sequence_test import ( "fmt" "sort" "testing" ) type Sequence []int func (s Sequence) Len() int { return len(s) } func (s Sequence) Less(i, j int) bool { return s[i] < s[j] } func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s Sequence) Copy() Sequence { copy := make(Sequence, 0, len(s)) return append(copy, s...) } func (s Sequence) String() string { s = s.Copy() sort.Sort(s) str := "[" for i, elem := range s { if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" } func (s Sequence) Stringsprint() string { s = s.Copy() sort.Sort(s) return fmt.Sprint(s) } func (s Sequence) StringIntSlice() string { s = s.Copy() sort.IntSlice(s).Sort() return fmt.Sprint(s) } func TestSequence(t *testing.T) { var s Sequence = Sequence{100, 12, 83, 34, 59, 86} t.Logf("%#v", s) t.Log(s) t.Log(s.String()) t.Log(s.Stringsprint()) t.Log(s.StringIntSlice()) } === RUN TestSequence f:\GO\go_test\sequence\sequence_test.go:54: sequence_test.Sequence{100, 12, 83, 34, 59, 86} f:\GO\go_test\sequence\sequence_test.go:55: [12 34 59 83 86 100] f:\GO\go_test\sequence\sequence_test.go:56: [12 34 59 83 86 100] f:\GO\go_test\sequence\sequence_test.go:57: [12 34 59 83 86 100] f:\GO\go_test\sequence\sequence_test.go:58: [12 34 59 83 86 100] --- PASS: TestSequence (0.00s) PASS ok loop/sequence 1.996s 类型转换 String()接口通过 “for i, elem := range”方式实现,也可以像Stringsprint直接调用fmt.Sprint处理实现。 ...

2021-04-19 · 4 min · 781 words