1
6
2017
3

如何快速高效地修 bug?

看到知乎上的一个问题,心血来潮,随意写写,请读者不要太较真。

看回答,有一些可操作性很强的答案。但是呢,你知道的,考试好不代表能力强,如果你只是学习别人的方法而并不理解,那么学来之后只会是东施效颦而不能融会贯通。所以呢,我也来发表一下自己的见解。

首先,你要定位 bug。这时,你需要:

  1. 注重逻辑性。不要做没有证据的结论。如果你有猜测,就去证实或者否定它。比如某次,同事代码返回的数据有问题,认为是缓存用的 Redis 有问题,返回了错误的数据。然而没人去对此猜测进行求证……我去确认了一下,Redis 收到了请求,并且响应正常。接下来,排除所有其它可能的原因之后,最后剩下的那个就是真相。真相就是,代码里有个 } 的位置放错了,因为它刚好在一屏之后的位置,所以没有人发现……(是 Vim 帮我找到的)
  2. 基本的方法论。比如二分法。比如最小化测试用例。如果你要提问,要懂得提问的智慧,不管是向搜索引擎还是向人,你都需要提出正确的问题
  3. 知识面。你写 Web 后端的话,普通的 HTTP 得懂,浏览器的开发者工具得会用。简单的 JavaScript 也有会点儿。简单地说就是,你要精于你自己主攻的部分,然后要熟悉你的上下游。再比如如果你使用 CPython 的话,你要准备一份 CPython 的源码,并且要能够流畅地阅读 C 代码。
  4. 工具。工欲善其事,必先利其器。一大堆调试用的工具,你至少得知道它们能干什么,需要的时候能用。比如 strace、lsof、gdb、git bisect,还有高级点的 sysdig、systemtap、perf 等等。当然还有一堆不是专门为调试而设计的通用工具,比如 the silver searcher 或者 ripgrep。一个快速的全文搜索工具能帮你在最短时间内找到相关的代码或者日志。你不必成为正则表达式大师,但是简单的一定要会,不然面对上千个匹配结果你要怎么办呢?Vim 有一个插件 Mark,能够同时高亮多个模式,非常利于调试期间阅读代码和日志。投入时间学习使用高效的工具,不要把时间浪费在等待和人工搜索上,也不要让自己忙于琐事而断了灵感和线索。

最后,不要不断地、毫无目的地换个环境啦,换个版本啦,换个用户啦,这样子找问题。如果这样做很有效的话,大家都去买彩票去了。

找到 bug 之后,理解它是如何产生的。当你理解之后才能真正修好它。就像你感冒了吃抗生素,根本没有用。

Category: 编程 | Tags: 编程 软件开发
2
6
2015
19

小谈 Rust

最近很火的 Rust 前不久发布了 alpha 版。正式版虽不说指日可待(还在各种大改中),但是也不是那么遥远了。而经过了这么久,再见 Rust,感觉完全不一样呢。

还记得第一次见 Rust,是在 Fantix 的博客上。现在只记得当时看到各种~和生命周期的东西,挺头疼的。而这次是看到 Rust for beginners 以及已经被合并到《The Rust Programming Language》这本书的官方 guide。感触很容易概括:「一门实用的类 Haskell 语言,是我很早就想要的东西呢。」于是才有了我的第一个 Rust 程序,以及后来的 各种语言实现的 swapview

当然后来事实证明 Rust 不仅仅有着与 Haskell 类似的代数数据类型,比如有表示空的 unit 类型、表示可选的 option 类型、用于返回结果或错误的 Result 类型。作为一名曾经苦学 Haskell 还折腾过 OCaml 的人,看到这些熟悉的类型,感到甚是亲切。这种类型系统最大的特点是类型安全、没有 null 指针/类型。

我接触到的绝大多数编程语言,都会有 null 指针,或者 null / none / nil 类型:

  • C、C++:「Segmentation fault」
  • Java:还记得经常在日志里露脸的「NullPointerException」吗
  • Python:一不小心就会出现的「AttributeError: 'NoneType' object has no attribute 'xxx'」
  • Lua:「attempt to index global 'xxx' (a nil value)」
  • 等等

都是一个不小心,没注意检查对象是不是 null 值就用,然后程序跑着跑着就出错了。

而 Haskell 和 Rust 都能有效地避免这一点,至少是你可以预先察觉,因为它返回的是不一样的类型。比如在 Rust 中,想要把字符串解析成整数,你写let a: i32 = "123".parse()不成。因为不是所有字符串都能解析成整数的,所以parse方法会返回一个Result<i32,ParseIntError>类型(早期版本是返回Option<i32>)。你需要显式地处理错误——或者忽略,如果你希望在出错的时候程序崩溃的话。不管选哪条路,写的时候都是明确知道这个地方可能出错的。不像我写 Python 时那样,直接想当然地写int(xxx),很少会想到想当然以为是个整数表示的xxx其实可能是别的什么东西(比如None)。我总不可能在每一次按.键(取属性)、(键(函数调用开始)、[键(取下标)时都先想想「相关对象会不会是奇怪的东西、是的话要怎么处理」吧?当然这样的错误处理会比较麻烦。如果一个项目不值得这样麻烦的错误处理的话,那就换个更适合的语言去做就是了。

Rust 另一个小特点是,if这类条件判断后边只能是布尔值,和 Haskell 一样,而和 Python、Lisp、Lua 等都不同,就更别说没有真正的布尔类型的 C 了。这样更严谨,挺好的,意义明确。像把0当成假值这种事 Lua 就不干,把空容器当假这事 Python 喜欢但是别的语言又不一样。早先版本的datetime模块甚至认为午夜是假的、其它时间才是真的……

Rust 还有个显著的特点是,关键字都特别短,但是不至于短到不认识,比如pub, fn, mut, ref, impl等。有些人不喜欢,我倒是觉得挺好的。非要写一长串字符浪费空间嘛,虽然现在的显示器不是终端机那样一行只能显示80字符,但我要分割成多列呢。笔记本显示器可以显示两列代码对照着看,外接显示器要显示两行三列还不计偶尔会用到的侧栏呢。

Rust 还继承了 Python 式的显式名称导入。只要不用星号,一个名字是从哪里来的,当前文件里搜一下就找到了。不像 Ruby 那样子,String 莫名其妙多了个方法不知道是干什么的?拿 Google 搜索整个互联网吧……

Rust 资源管理很有特点,我还没在其它语言里看到这种。Rust 程序里,编译器知道每一个对象的生命周期,所以可以在编译期就插入相应的释放资源的代码,不需要 gc 过一段时间停下所有工作来检查一遍。也不像引用计数那样得维护计数,引起很多不必要的内存写请求。毕竟 Rust 的目标是像 C++ 那样高效的系统级编程语言嘛。当然引用计数如果需要还是可以有的。最初 Rust 的另一目标——像 Erlang 那样的并发性,因为绿色用户级线程被官方移出之后就大打折扣了。不过因为类型检查和生命周期推断,线程安全的特性还是保留了下来。

Rust 有各式各样的 trait,类似于 Haskell 里的类型类。要指定资源释放时调用的函数的话,直接实现Drop trait 就可以了。比如我的:

struct AtDir {
  oldpwd: Path,
}

impl AtDir {
  fn new(dir: &str) -> io::IoResult<AtDir> {
    let oldpwd = try!(std::os::getcwd());
    try!(std::os::change_dir(&Path::new(dir)));
    Ok(AtDir { oldpwd: oldpwd })
  }
}

impl std::ops::Drop for AtDir {
  fn drop(&mut self) {
    std::os::change_dir(&self.oldpwd).unwrap();
  }
}

使用的时候直接在需要的作用域时生成一个变量就好,就像下边这样子。Rust 保证在其生命周期结束时调用drop方法。而且是按其所有者变量定义的顺序的逆序调用的。不像 Python,PEP 442折腾了之后,反而是把我一个模块的__del__方法在解释器关闭时的调用顺序弄错了。虽然 Rust 没有 Python 那样的with语句,但是拿Drop可以做到一样的效果,而且能保证调用的时机与预期的一致。

let _cwd = match AtDir::new(directory_name) {
  Ok(atdir) => atdir,
  Err(err) => return Err(err.desc.to_string()),
};

Rust 编译器及标准库目前大部分(92.0%)使用 Rust 编写。而在此之前,Rust 竟然是使用 OCaml 编写的。这从侧面解释为什么目前 79.4% 使用 Go 编写 Go 语言用起来那么像 C(因为它的开发者用的是 C,设计目标好像也是更好的 C),而 Rust 虽然有很多借鉴自 C++ 的东西,导致其语法有些像 C,但写起来完全没有 C 和 Go 那样原始的感觉。这也是我更喜欢 Rust 的原因之一。

目前,除了还在改来改去,让我的程序过几天就各种报错编译不了之外,作为一名初学者,我能发现的另一个缺点就是编译极其费时了,特别是普通优化和链接时优化全开的时候,我一运行时间不到 0.2 秒的小程序,竟然需要半分钟才能编译好……

对了,之前给 Arch Linux 打包的 thestinger 不再打包 Rust 了,所以我开始在 archlinuxcn 源里维护64位的 rust-git、cargo-git(因为 Rust 更新的原因,至今还没打包成功……)以及 vim-rust-git。这些包是自动更新的,因此不出问题的话,有更新就会在一天内更新。

PS: 写的时候有点赶,希望没有写得太乱 ( >﹏<。)

Category: 编程 | Tags: Rust
1
6
2015
41

众编程语言间的 swapview 之战

swapview 起源于我很早之前看到的一个 shell 脚本。当时正在学习 Haskell,所以就拿 Haskell 给实现了一遍。为了对比,又拿 Python 给实现了一遍。而如今,我又在学习另一门新的语言——Rust,也拿 swapview 来练习了。相比仅仅输出字符串的「Hello World」程序,swapview 无疑更实际一些:

  • 文件系统操作:包括列目录、读取文件内容
  • 数据解析:包括简单的字符串处理和解析,还有格式化输出
  • 数据处理:求和啊排序什么的
  • 流程控制:循环啊判断啊分支什么的都有
  • 错误处理:要忽略文件读取错误的

因此,swapview 成为了依云版的「Hello World」:-)

感谢所有给 swapview 提交代码的朋友们

本文只是借 swapview 这个程序,一窥众编程语言的某些特征。很显然,编程语言们各有所长,在不同的任务下会有不同的表现。而且 swapview 各个版本出自不同的人之手,代码质量也会有所差异。

闪耀!那些令人眼前一亮的语言们

从运行效率上来看,C 如预期的一样是最快的。但令人惊讶的是,由我这个 Rust 初学者写的 Rust 程序竟然紧随其后,超越了 C++。

而原以为会跟在 Rust 之后的 C++,却输给了作为脚本语言存在的 Lua 语言的高效实现 LuaJIT(与 Rust 版本相当)。而且非 JIT 版本的 Lua 5.1 和 5.2 也都挺快的。Lua 这语言自带的功能非常少,语法也简单,但是效率确实高,让人又爱又恨的。

失望!那些没预期中的高效的语言们

没想到 Python 2 也挺快的,很接近 Go 了。PyPy 大概是因为启动比较慢的原因而排在了后面。Python 3 有使用两个版本的代码,Python3_bytes 把文件读取改为使用 bytes,仅在需要的时候才解码成 str。仅此之差,运行速度快了10%。可见 Python 的 Unicode 处理十分耗时,难怪 Python 3 在各种测试中都比 Python 2 要慢上一截。至于 PyPy3,怎么跑到那么靠后的地方去了呢……

Go 很快。至少比 Python 快。但也仅此而已了,不仅比 C++ 慢,甚至连 Lua(非 JIT 版)都不如。Go 语言版本虽然不是我写的,但我看过代码,感觉很原始。至少比 Lua 原始。看起来 Go 只不过是带接口和并发支持的 C 而已。而且,作为静态类型的编译型语言,却我却有一种很不放心的感觉。大约是因为我改动时发现传给 fmt.Printf 的参数类型和数目错了都不会得到警告或者错误的原因。而且我从来没见过 Go 编译时出现警告,对于还没入门的初学者写的、改过的程序,这样子不科学啊。早期我倒是见过 Go 报错了,但那只不过是编译器还不完善的表现而已。

传闻 NodeJS 很快。但至少它在 swapview 这种脚本中没能体现出来。正常版本比 Python 3 还要慢一点。而使用异步啊并行什么的版本还要慢上差不多三分之一,不知道怎么搞的。

编译型的 Chicken、OCaml、Haskell 都排在了一众脚本语言后边,虽然很可能是对语言本身不熟导致写出来的程序比较慢,但还是挺令人失望的。经过高手优化的 Haskell2 版本效率接近于 Python 3,但也到此为止了(因为不想使用 cabal 安装依赖,所以 Haskell2 没有参与这场对决)。我曾见过有人把 Haskell 代码优化到比 C 还快,但我宁愿去看汇编也不要去读那种代码……

Lisp 系(Chicken、Racket、SBCL(标注为 CommonLisp 的项)、Guile)也都挺慢的。不知道 LispWorks 之类的会不会快一大截呢。

预料之中的以及结果截图

Ruby 比 Python 略慢一点。

Java、Elixir 比较靠后。没办法,它们启动慢。也许以后我会出不考虑启动时间的版本。

以下是本文发表前的测试结果截图。其中 Erlang 版本因为有问题被信号所杀所以被扔在了最后。

测试结果截图

测试使用的是benchmark子目录中的 Rust 程序,使用cargo build --release命令即可构建。另外也可以使用 farseerfc 的 Python 脚本。

代码量

Elixir 代码量挺少的。Python、Ruby 也挺不错。Java 版本竟然跟 Haskell 一样。不管是 JavaScript 还是 CoffeeScript 都比较长,比 Java 还长。Rust 比 Python 长不少,但也比 Go 短不少。而 Go 比起 C、C++ 要短一些。最长的,除了我不了解的 Pascal,竟然还有因为程序出错还没有测试的 Erlang!如果不算按行读取的 line_server.erl 的放大,只有不到一百行,倒还不算多。

                  Elixir:   50
                   Julia:   51
           Python3_bytes:   53
                  Python:   56
                    Ruby:   56
                  Racket:   58
                    Bash:   63
                   OCaml:   65
          CommonLisp_old:   67
          CommonLisp_opt:   67
           Bash_parallel:   69
             C++14_boost:   69
                   Guile:   70
                 Haskell:   73
                 Chicken:   75
                    Java:   75
                  NodeJS:   76
                    Vala:   78
                Haskell2:   81
                       D:   86
                    Rust:   88
                   C++14:   89
                  CSharp:   91
                     Lua:   91
            NodeJS_async:   93
            CoffeeScript:   93
   CoffeeScript_parallel:   95
                     PHP:   97
           Rust_parallel:   98
                      Go:  103
                   C++11:  128
                   C++98:  141
                       C:  149
              FreePascal:  185
                  Erlang:  232

编译速度

这个比较非常粗糙,比如联网下载依赖也被算进去了。不过可以肯定,不算下载依赖部分的话,Rust 是最慢的!其次是 Haskell。标榜编译速度非常快的 Go 并不是最快的,和 C++ 不相上下(当然不知道代码复杂之后会如何了)。

0.36 C
0.60 FreePascal
0.80 OCaml
0.83 CoffeeScript_parallel
1.48 CSharp
1.67 Vala
1.68 Erlang
2.13 NodeJS_async
2.27 C++14
2.49 Go
2.53 CoffeeScript
2.90 C++11
3.01 C++98
3.23 Java
3.52 Racket
3.98 NodeJS
6.05 CommonLisp_opt
7.07 D
9.01 C++14_boost
10.41 Haskell
13.07 Rust
14.74 Chicken
15.37 Rust_parallel

结语

这个项目最初只是练习而已。后来不同语言的版本有点多,于是才演变成众编程语言的竞技。也就随意地测试了一下在给定需求下不同语言的表现而已。其实比较有意思的部分,一是使用正在学习的编程语言写作程序的新奇感、新知、新的领悟(这也是我的测试程序使用 Rust 编写的原因),二是对比不同编程语言的风格和对同样需求的处理方式。

各位读者对 swapview 有任何补充和改进,欢迎贡献代码哦~项目地址:https://github.com/lilydjwg/swapview

更新区

2015年1月9日更新:又收到了不少版本和改进,以下是最新的测试结果。很不幸地,现在已经跑得很快的 Erlang 在测试中又没反应被杀掉了。并行版的 Rust 的结果很不稳定,这次跑得好快!C++ 的除了 C++98 版的之外都到 Rust 前边去了。PHP 竟然比 LuaJIT 还要快!D 怎么到 PyPy 后边去了。

2015年1月9日的测试结果截图

2015年1月10日更新:C++ 版本继续改进,好多都超越 C 了,Rust 1.0.0alpha 的并列版本又快又稳定,Erlang 版本终于跑完了全部测试而没有出事,LLVM 版 D 快了好多。

2015年1月10日的测试结果截图

2015年1月18日更新:继续更新。又添加了若干语言,不过期待中的 Nim、Zimbu 以及传统脚本语言 Perl、Tcl 依旧缺席中。另外,正文也进行了更新,重新计算了代码量,添加了编译速度的粗略比较。

2015年1月18日的测试结果截图

12
24
2014
39

Rust 初体验(真快!)

最近又看到 Rust 的相关东西了,入门指南也写得挺不错的。这语言我越看越喜欢。

Rust 的目标是系统级编程,就像 C 那样,快速高效。同时它继承了 Haskell 的诸多特性,包括其类型系统(包括类型类和类型推断)、模式匹配。而读写起来,又和 Python 差不多简单明了。简直是把这三种语言的优点全学到了!(当然 Rust 不仅仅受到了这几种语言的影响啦。)

当然,要体验一门编程语言,最好的方式就是使用它。于是我拿它实现了我最开始用来练习 Haskell 用的 swapview 程序。

swapview 的功能是,读取/proc下每一个进程目录下边的cmdlinesmaps文件,得到其命令行和 swap 使用量,然后排序、格式化,并打印出来。

Haskell 第一版实现挺慢的:

swapview  1.27s user 0.26s system 98% cpu 1.555 total

我随手写了个 Python 版,效率翻了一倍还要多!很令人惊讶的呢。作为解释执行、还一直被认为很慢的 Python 竟然在没有任何优化的情况下就超过了编译型的 Haskell:

swapview.py  0.35s user 0.18s system 97% cpu 0.548 total

后来在 IRC 上遇到一位懂行的人,用了不少手段优化,最终得到了 Haskell 第二版:

swapview2  0.42s user 0.15s system 98% cpu 0.583 total

比 Python 版略慢。

才学 Rust 没几天,我对 Rust 比对 Haskell 更不熟。花了不少时间查阅文档、调整代码。不过因为之前的 Haskell 基础,也没遇到太大的困难。结果如下:

swapview  1.84s user 0.15s system 97% cpu 2.038 total

呃呃呃,怎么比 Haskell 版本还要慢上不少啊?

本来是找 profiling 方法的。翻着 rustc 的 man 文档,看到了-O选项,眼前一亮——我忘记告诉编译器要优化了!这是启用优化的结果,比 Python 版又快了一倍:

swapview  0.10s user 0.13s system 96% cpu 0.237 total

真棒呢~

不过很遗憾的是,它的格式化函数的第一个参数必须是字面量,连常量都不行。因为那是个宏,要在编译期解析格式……另外似乎也不支持现在连 JavaScript 都已经支持了的 generator(只支持 iterator,得先写一个 struct 才能用)。

PS: Rust 的文档挺赞的,和 Python 的一样有 JavaScript 实现的搜索功能,比起 Nimrod 和 Zimbu 的好用太多了。

PPS: 谁有兴趣可以贡献个 Go 版、C 版、C++ 版、LuaJIT 版什么的=w=


2014年12月25日更新:目前的结果是(运行时间):Rust < LuaJIT < C++14 (gcc 4.9.2) < Lua 5.1 / 5.2 << Python 3 < Haskell <<< OCaml < SBCL。手动测试的。有空我再写个好点的自动测试程序。

2015年1月6日更新:添加了更多的编程语言,以及更准确的运行时间测试,请见新文章编程语言对决——战场:swapview

Category: 编程 | Tags: Haskell 编程语言 Rust
3
2
2014
3

FUSE 初体验:Android dedupefs

自打知道 FUSE 以来都觉得亲手写一个 FUSE 文件系统是很好玩的事情,但是因为没好的自己能够很快实现的点子所以一直没动手。前段时间需要从 Android xrecovery 备份中取得一旧版本的应用,才决定动手的,顺便也练习一下很久没怎么用到的 C 语言。至于为什么不用 Python,好像那个 Python 绑定不太稳定的样子,Python 3 版更是如此。而且我也不希望效率太差。

首先介绍一下,所谓的「dedupefs」,就是把 Android xrecovery 的「dedupe」备份格式的数据挂载成文件系统来查看。其实仅仅只是想查看的话,把那个 dedupe 目录下的东东 gcc 一下就可以创建和解开 dedupe 的备份了,只是占用很多磁盘空间而已。

dedupe 的格式很简单,一个文本文件描述文件信息(时间、路径、大小、类型等),一个目录里全是 sha256 命名的文件来存储文件的数据,以便在备份时不同的备份中的相同文件只保存一次。

FUSE 嘛,我好像从来没看到过完整一点的文档,就是官方 API 文档也经常语焉不详。dedupefs 是参考 rofs 写的。dedupefs 也是只读的。

挂载之前,先得把 dedupe 的纯文本格式处理一下。纯文本适合存储和人阅读,但是查询效率低下。我决定用更适合处理纯文本的 Python,把数据存储到 GNU dbm 键值对数据库中,然后 dedupefs 直接读取数据库就好了。(于是顺便学会了在 C 中使用 GNU dbm :-))数据的组织方式如下:

  • d + 文件路径:该目录下的文件名列表
  • f + 文件路径:该文件的信息

这样要读取一个目录下的文件列表就查 d 开头的项,要取得一个文件的信息(stat)或者打开文件,就读 f 开头的。

下边是编码和调试过程中的经验与收获:

  • GNU dbm 没说它是线程安全的,所以它不是线程安全的。但是 FUSE 又是多线程的(调试用的单线程模式我就不玩的),所以读取数据库时要加锁。
  • GNU dbm 查询结果数据是要调用者来 free 的。
  • 因为涉及到二进制数据交换(Python <-> C),所以要注意在结构体声明时围上#pragma pack(push, 1)#pragma pack(pop),以免对齐不一致造成数据错误。
  • valgrind 用来诊断内存访问错误效果非常棒!
  • FUSE 的struct fuse_file_info里有个fh域可以用来存文件描述符,这样就不用像 rofs 那样每次读取都要打开一遍文件了。
  • FUSE 读取用的回调函数传的offset一定要用,要首先lseek(finfo->fh, offset, SEEK_SET);一下,不然指不定读取到什么地方的数据了。
  • FUSE 文件系统可以忽略文件权限,所以自己不在openaccess里判断的话,就可以访问到明明看上去不能访问的文件(这正在我想要的)。
  • du 命令读取文件占用磁盘空间时使用了struct statst_blocks域。如果在 FUSE 程序里不管它的话,那么 du 将总是报告占用了 0 字节的空间……这里的块大小总是 512 字节。

第一次写 FUSE 程序,虽然文档差了一点,但用起来还是挺方便 =w=

哦对了,android-dedupefs 的仓库链接。

1
11
2014
5

使用 inspect.lua 查找 Awesome 配置引入的内存泄漏

所谓「相见恨晚」,说的就是我第一次看到 inspect.lua 的感觉啊!Lua 这个超小型主打嵌入的语言,连 Readline 都要第三方库来支持,自然是没 Python 那样的补全功能了。不仅如此,连一个展示其数据结构的函数都没有。包括自己在内,不少人零零散散写过各种打印 Lua 表的函数,但像 inspect.lua 这样子优秀的还是第一次见到。

基本用法啊示例什么的不说了,直接在对象上调用 inspect 函数就可以得到一个(可能是巨大的但一定不会是无限的)字符串表示,递归的结构会依据其类型和一个序号来辨识。

既得此神器,自然要用来看看我那自从升级到 3.5 版本之后就一直在慢慢泄漏内存的 Awesome 了:

>>> awesome-client
awesome#inspect = dofile('tmpfs/inspect.lua')
awesome#f = io.open('tmpfs/output.lua', 'w')
awesome#f:write(inspect(_G))
awesome#f:close()
awesome#f = nil

然后就是慢慢察看了。我注意到了这么一个变量:

  raise_on_click = {
    [<client 13>] = true,
    [<client 12>] = true,
    [<client 14>] = true,
    [<client 15>] = true,
    [<client 16>] = true,
    [<client 17>] = true,
    [<client 18>] = true,
    [<client 19>] = true,
    [<client 20>] = true,
    [<client 21>] = true,
    ...
  }

这个变量由来已久,好像现在已经偏离了当初的设计目的了……不管它,反正呢,它里边保留了所有 Awesome 正管理的客户端对象,有 100 多个呢。可是,我怎么会同时有那么多窗口呢,明明才十几个啊?

检查一下配置文件,终于知道问题在哪里了:

diff --git a/rc.lua b/rc.lua
index ad06296..c1422bd 100644
--- a/rc.lua
+++ b/rc.lua
@@ -991,7 +991,7 @@ end)
 client.connect_signal("focus", function(c) c.border_color = beautiful.border_focus end)
 client.connect_signal("unfocus", function(c) c.border_color = beautiful.border_normal end)

-client.add_signal("unmanage", function(c)
+client.connect_signal("unmanage", function(c)
     raise_on_click[c] = nil
 end)
 -- }}}

add_signal是 Awesome 3.4 的用法,3.5 应该用connect_signal才对。这里的client.add_signal是 Awesome 自己用的另外一个意思的函数……

Category: 编程 | Tags: Lua awesome
4
24
2013
12

Lua 中的一起文件描述符泄漏案

用上 Awesome 3.5 后,我发现 Awesome 占用的内存有点多。后来又发现,运行的时间越长,其占用的内存也就越多:

Awesome 占用了大量内存

这不是典型的内存泄漏吗!

然后我发现我只要按下Win+Ctrl+R重新载入 Awesome 配置,内存使用就会回去。看来是我的配置文件有问题。不过由于时间关系一直使用重新载入的方式应付着。今天终于有点时间和兴致,于是专心对付它了。

不过 Lua 脚本的内存泄漏要怎么查呢?我一开始想把_G打印出来。不过以前写的那个 Lua 对象转字符串函数似乎并不太喜欢 Awesome 加进去的那些对象,抛出了异常。瞪大双眼检查配置文件里的各种全局变量,特别是那里每隔几秒更新一次的指示器们,但也没发现什么。有些不知所措,随手又调出 htop 查看上图中那个占用了「巨量」内存的 Awesome 进程,右手无名指不自觉地按下,然后竟然发现了一个问题:

Awesome 中的文件描述符泄漏

怎么开了那么多/proc/net/route/proc/net/dev文件?这两个文件是我在网络指示工具中打开并读取了的,但是我不至于扔着打开的文件不管啊:

function update_netstat()
    local interval = netwidget_clock.timeout
    local netif, text
    for line in io.lines("/proc/net/route") do
        netif = line:match('^(%w+)%s+00000000%s')
        if netif then
            break
        end
    end
    if netif then
        local down, up
        for line in io.lines("/proc/net/dev") do
            -- Match wmaster0 as well as rt0 (multiple leading spaces)
            local name, recv, send = string.match(line, "^%s*(%w+):%s+(%d+)%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+(%d+)")
            if name == netif then
                if netdata[name] == nil then
                    -- Default values on the first run
                    netdata[name] = {}
                    down, up = 0, 0
                else
                    down = (recv - netdata[name][1]) / interval
                    up   = (send - netdata[name][2]) / interval
                end
                netdata[name][1] = recv
                netdata[name][2] = send
                break
            end
        end
        down = string.format('%.1f', down / 1024)
        up = string.format('%.1f', up / 1024)
        text = '↓<span color="#5798d9">'.. down ..'</span> ↑<span color="#c2ba62">'.. up ..'</span>'
    else
        netdata = {} -- clear as the interface may have been reset
        text = '(No network)'
    end

我是用io.lines函数打开文件的。印象中这家伙是会自动关闭文件的啊,我也没办法再手动关闭是不?不过既然是这地方的问题,那么再去仔细看看文档好了:

Opens the given file name in read mode and returns an iterator function that works like file:lines(···) over the opened file. When the iterator function detects the end of file, it returns nil (to finish the loop) and automatically closes the file.

什么?and automatically closes the file?也就是说如果文件没读完的话……

于是我立即打开 ilua 写下:

for i in io.lines('strprint.lua') do print(i) if i:sub(1,1) == '-' then break end end

执行完毕,再去 htop 里查看文件描述符,果然没关!

好吧,又一坑。于是改成像 C 语言中那样显式打开和关闭文件了(相关提交在此)。过几天再看看问题有没有完全解决。


2014年1月11日更新:后来还是依靠 inspect.lua完全解决我的 Awesome 配置中的内存泄漏

Category: 编程 | Tags: Lua awesome
1
4
2013
22

多返回值:Lua 又一坑

假设myfunc已经在其它地方定义,你觉得以下两段代码作用一样吗?

local t = {}
local item = myfunc()
table.insert(t, item)
item = nil
local t = {}
table.insert(t, myfunc())

代码一把函数的结果存放在临时变量里再作为参数传给其它函数,代码二直接将函数返回值作为参数传给其它函数。看上去,代码二比代码一简短了一些,少用了个变量名。

可是,如果myfunc返回多个值的话,代码二将不能正确运行,因为myfunc所有返回值均会传递table.insert。和其它语言完全不一样。我想,这个应该是「真正的」多返回值吧?——它返回的不是像其它语言那样的是一种复合类型的值,而真真正正的是多个值,以一种超乎直觉的方式存在着。

所以,要么总是通过赋值来指定需要的返回值,要么不要给已有函数增加新的返回值。我倾向于前者,因为,你记得你所有用到的函数的返回值数目吗?

Category: 编程 | Tags: Lua
12
29
2012
16

Lua 那些坑爹的特性

协程只能在 Lua 代码中使用

协程(coroutine)应该是 Lua 最大的卖点之一了。可是,它有一个在文档中根本没有提到过的弱点:只能在 Lua 代码中使用,不能跨越 C 函数调用界限。也就是说,从 C 代码中无法直接或者间接地挂起一个在进入这个 C 函数之前已经创建的协程。而 Lua 本身作为一种易于嵌入的语言,必然不时与 C 打交道。

比如以下程序:

c = require('c')

co = coroutine.create(function()
  print('coroutine yielding')
  c.callback(function()
    coroutine.yield()
  end)
  print('coroutine resumed')
end)

coroutine.resume(co)
coroutine.resume(co)

print('the end')

C 模块代码:

#include<stdio.h>
#include<stdlib.h>
#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>

static int c_callback(lua_State *L){
  int ret = lua_pcall(L, 0, 0, 0);
  if(ret){
    fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
    exit(1);
  }
  return 0;
}

static const luaL_Reg c[] = {
  {"callback", c_callback},
  {NULL, NULL}
};

LUALIB_API int luaopen_c (lua_State *L) {
  luaL_register(L, "c", c);
  return 1;
}

在官方版 Lua 以及 LuaJIT 中会出现「attempt to yield across metamethod/C-call boundary」错误。只有打过 Coco 补丁的版本才能正常执行。

>>> lua5.1 co.lua
coroutine yielding
Error: attempt to yield across metamethod/C-call boundary
>>> luacoco co.lua
coroutine yielding
coroutine resumed
the end
>>> luajit co.lua
coroutine yielding
Error: co.lua:6: attempt to yield across C-call boundary

据说 LuaJIT 已经解决了这个问题,不过我想他们说的是内建函数支持 yield 而已。

在 Lua 5.2 中,提供了新的 API 来支持在 C 中 yield。不过,既然是 C API,当然得改代码,而且看上去比异步回调更复杂。

幽灵一般的 nil

nil 相当于 Python 中的 None 或者 C 中的 NULL,表示「没有这个值」的意思。但是,一个神奇的地方在于,所有未定义的变量的值均为 nil。所以,在 Lua 中有空值 nil,但是有时它又不存在:当你尝试把 nil 值存到表里时,它会消失掉。

另外,当 nil 被传入接受可变参数的函数时,官方版 Lua 只能通过select('#', ...)获取参数个数。至于 LuaJIT,很遗憾,没有办法。

LuaJIT 中还有这样一个值,它等于 nil。但是根据 Lua 语言标准,只有 false 和 nil 的值为假。于是,在 LuaJIT 中,两个相等的量,却有着不同的真值。它就是 ffi 中的 NULL 指针。

在另外一些地方,也会有其它各种库定义的 null 值,比如ngx.nullcjson.null。这些空值之间哪些相等哪些不等就难说了。

没有 continue

Lua 一直不肯添加 continue 关键字。作者声称不添加不必要的特性。请问有谁认为「repeat ... until」结构比「continue」关键字更有必要?于是,凡是本来应当使用 continue 的地方,都不得不弄一个大大的 if 语句:

for line in configfile:
  if line.startswith('#'):
    contine

  parse_config(line)

在 Lua 中只能这么写:

for line in configfile do
  if string.sub(line, 1, 1) == '#' then
  else
    parse_config(line)
  end
end

所以,Lua 代码的左边空白的形状都是些 45° 或者 135° 的斜线。

错误信息的表达

Lua 中,习惯的错误表达为,返回两个值,第一个为 nil 表示发生了错误,第二个为字符串,是错误信息。字符串形式的错误信息显示给用户挺不错的(想想微软喜欢的长长的错误号)。可是,程序里只好用模式匹配去判断是否发生了指定类型的错误。这多么像 VimScript 中的错误处理啊。journald 取代 syslog 的重要原因之一就是它存储的是结构化文本。Lua 错误处理最伟大的一点则是我们又回到了字符串匹配。别以为你可以返回一个 table 或者 userdata 来表达错误。很多库可不这么认为。当你的结构化错误被..连接时你就会发现这厮没救了。

下标

别的编程语言下标都从 0 开始。Lua 为了更「人性化」,其下标从 1 开始。其实写多了也能习惯,除了当通过 ffi 获得一个 C 数组的时候……

提前返回

return 语句之后必须跟着一个end。于是,很多提前返回的时候只能写do return end。有意义么?

方法调用

访问表或者 userdata 的域使用一个点.,连接字符串使用两个点..。而方法定义和调用时,你需要垂直放置的两个点——冒号:。它与域访问的一个点相比,也就多了四个像素,显示器不干净或者精神不佳的时候就得小心了!

面向对象

Lua 是不支持面向对象的。很多人用尽各种招术利用元表来模拟。可是,Lua 的发明者似乎不想看到这样的情形,因为他们把取长度的__len方法以及析构函数__gc留给了 C API。纯 Lua 只能望洋兴叹。

结论

Lua 只适合写写配置。做纯计算用用 LuaJIT 也不错。复杂的逻辑还是交给专业点的语言吧。

Category: 编程 | Tags: Lua
11
10
2012
8

如何更安全地覆写数据文件

经常地,程序在开始执行某项任务需要从文件读取数据。在任务完成后数据得到更新,新的数据会覆写到之前读取的文件中。怎么将数据写回到文件呢?一个直觉的方案是:

with open(datafile, 'w') as f:
  f.write(data)

在通常情况下,它能够正确地完成写回数据的任务。如果出于某种原因文件打开失败,通常也不会有人忘记处理。但是,当写入操作失败了呢?

时不时地编译程序看到 gcc 大把地警告:

警告:忽略声明有 warn_unused_result 属性的‘write’的返回值 [-Wunused-result]

在 Python 中,写文件时如果失败会抛出异常,上层的异常处理机制似乎能够作出相应的应对。但是,真的尽力了吗?

我也曾以为这样不会出问题。直到有一天,本地信箱里出现了这样的错误信息:

OSError: [Errno 28] No space left on device

可能是由于内核的某个 bug,我本来就所剩无几的 /home 分区没空闲空间了。一个 cronjob 在写回数据时发生异常。于是,新的数据没能写入文件。那旧数据呢?因为是以「写」方式打开文件,所以它也没了……

在那次事件之后,那段写回数据的代码变成了这个样子:

with open(datafile + '.tmp', 'w') as f:
  f.write(t)
# if the above write failed (because disk is full), the old data should be kept
os.rename(datafile + '.tmp', datafile)

注意:测试表明不使用with或者显式地关闭文件的做法是有问题的,即使在 CPython 中。

try:
  open('/dev/full', 'w').write('abc')
except:
  print('fine.')

在 Python 2.7 中会打印错误信息,Python 3.3.0 中无任何信息。都没有预料中的异常被捕获。

>>> python t.py
>>> python2 t.py
close failed in file object destructor:
IOError: [Errno 28] No space left on device

今天之所以写这个,是因为 Arch Linux CN 的群服务器遇到磁盘配额用尽的问题。XMPP 服务器 Prosody 在写入联系人信息时只写了一小部分,大部分数据丢失。这里有 bug 报告

2013年7月21日更新:Sublime Text 2 作为商业软件,竟然不仅不采用「新建+重命名」的方式写入文件,而且连写入是否成功都不检查。难怪 Linux 版中文输入法的问题迟迟不修复,原来连造成用户数据丢失的问题都无所谓

Category: 编程 | Tags: python prosody

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com