Random Tech Thoughts

The title above is not random

zsh+tmux+OS X: 正确地设置 PATH

我在 OS X 上使用 tmux 遇到过两个 PATH 环境变量的问题:

  1. 自己添加的路径总是被放到系统路径之后
  2. 环境变量中的部分路径重复了两遍

以前偷懒一直用 workaroud 避开问题,前段时间终于弄清了原因和解决办法,今天终于有时间写下来。问题的起因得从 zsh 的启动说起,zsh 启动过程会读取如下文件(摘录自 man zsh):

  1. /etc/zshenv
  2. $ZDOTDIR/.zshenv
  3. 如果是 login shell,顺序读取 /etc/zprofile, $ZDOTDIR/.zprofile (未设置 ZDOTDIR 时默认使用 HOME)
  4. 如果是 interactive shell, 顺序读取 /etc/zshrc, $ZDOTDIR/.zshrc
  5. 如果是 login shell, 顺序读取 /etc/zlogin, $ZDOTDIR/.zlogin

zshenv 无论是否为 login shell 都会读取,但设置环境变量我们只需在 zprofile 中完成即可。OS X 在 /etc 目录下有两个一模一样的 zshenvzprofile,我删除了多余的 /etc/zshenv,并在 .zprofile 中设置用户的环境变量。完全抛弃 zshenv 可以减少 zsh 启动时需要读取的文件数,同时避免它对环境变量做出不必要的修改。

PATH 环境变量问题来源于 tmux 默认以 login shell 的方式执行 shell。zsh 作为 login shell 时会执行 /etc/zprofile,而该文件会调用 /usr/libexec/path_helper,根据 /etc/paths 等文件设置系统环境变量。path_helper 会将当前 PATH 环境变量添加到系统环境变量之后,这一行为导致了 PATH 的重复,并且自己设置的路径出现在系统路径之后。

了解问题的根源之后解决办法很简单,在 ~/.tmux.conf 中添加下面选项禁止以 login shell 启动 zsh 即可:

set -g default-command /usr/local/bin/zsh

COW 0.8 的改进

今天发布了 COW 0.8 版,这个版本重要的改动是在不同的客户端连接之间共享 web 服务器连接。对 Firefox, Safari 或者是在局域网内服务多个用户的情况下这个改动应该能够提高 COW 的网络性能。

首先介绍一下背景。(如果你不熟悉 HTTP 协议,推荐 HTTP Made Really Easy,这是我见过最简洁清晰而且较为全面的 HTTP 协议介绍。)HTTP 1.1 定义了 persistent connection,允许用一个连接完成多个 HTTP 请求。重用单个连接完成多个请求的主要好处有两个:

  • 减少建立 TCP 连接的开销,从而减小延迟
  • 更好的利用网络带宽
    • 新建立的 TCP 连接并不会全速发送数据包,而会通过 slow start 算法慢慢涨上去

COW 处理并发 client 连接采用的方式是每个连接由一个 goroutine 处理,称之为 client goroutine。由于 Go 能利用多核,为避免在 client goroutine 之间共享数据引入同步开销,以前的设计是每个 client goroutine 创建的 web 服务器连接是私有的。这个设计的缺点是减少了重用服务器连接的机会:

  • client 关闭连接时,对应的 client goroutine 结束执行,同时会关闭所有服务器连接
    • 新创建的 client goroutine 再次处理针对这些服务器的请求时不得不建立新的连接
  • 浏览器会与代理服务器创建多个连接,每个连接都会进行对不同网站的请求
    • 考虑下面的场景:
      1. 假定浏览器跟代理有两个连接 A, B,分别正在处理对 taobao.com, 和 tbcdn.cn 的请求
      2. 连接 B 先处理完 tbcdn.cn 的请求,浏览器马上通过 B 发送 taobao.com 的请求
      3. 连接 A 处理完 taobao.com 的请求,浏览器再通过 A 发送 tbcdn.cn 的请求
    • 第二次 tbcdn.cn 的请求在服务器连接私有的设计下无法得到重用

MyPass: A Password Generation Tool

我日常的密码管理是通过一个 Lua 脚本为不同网站生成不同密码。Lua 脚本的好处是电脑和 iPhone 上 (通过 iLuaBox) 都可以使用,不过切换到终端、调用脚本、拷贝粘贴密码的过程不太方便,所以最近实现了一个 Chrome 插件来实现这个功能。

这次实现的算法跟 One Shall Pass 的一致,可以指定密码长度、包含多少特殊字符等选项。插件用 Chrome 来同步密码选项。这个算法依赖 PBKDF2 生成 key 的开销来增加暴力破解 passphrase 的代价,所以可以公开算法,也因此可以用 JavaScript 实现然后放在网上以便随时在浏览器中使用。

除 Chrome 插件外实现了两个独立的页面,其中一个专为 iOS 优化,添加到主屏幕后可离线使用。有兴趣的同学可以移步 github 获取更多技术细节,体验地址如下:

InstapaperBookmarklet 及 Safari 扩展推荐

Update 2013-04-20: 原先的名字 InstapaperBookmark 容易误解成只是添加一个书签,现在改名为 InstapaperBookmarklet。

写了个 Safari 扩展 InstapaperBookmarklet,在工具栏添加一个按钮来执行 Instapaper bookmarklet。如果当前标签是空白页面的话点击会访问 Instapaper 网站。

如果你跟我一样使用 SafariTabSwitching 这个基于 SIMBL 的插件(实现 cmd+num 切换标签页),习惯隐藏书签栏,又喜欢官方 bookmarklet 的视觉反馈,那么可以试试看这个扩展。

Safari 上其实有很多不错的扩展。canisbos.com 这个网站上有很多,我目前使用如下几个:

  • gDirectLinks 移除 Google 搜索结果中的跳转 URL,这样 Safari 记录的历史记录更有意义
  • QuickStyle 比 Stylish 方便多的定制网页 css 的工具
  • PopSearch 添加自定义搜索引擎
  • QuickScript 在网页中执行任意的 JavaScript,InstapaperBookmarklet 就可以用这个实现,不过不能添加专用按钮,要多点击几次鼠标
  • CustomReader Safari Reader 的效果远比 Chrome 扩展实现的效果好,这个扩展可以定制 Safari Reader 的背景色、字体等

其他推荐扩展:

  • AdBlock 注明的拦广告扩展
  • Ultimate Status Bar 实现类似 Chrome 在页面下方显示链接 URL 的插件
  • YouTube5 用 HTML5 播放器而不是 Flash,而且可以方便下载视频
  • 微博急简 新浪微博默认页面我觉得过于混乱,这个 user style 扩展可以让微博页面简洁很多

写 InstapaperBookmarklet 的时候了解了 Safari 扩展的工作机制。简单来说扩展包含一些 html 页面和 JavaScript 代码。html 负责扩展界面,JS 实现逻辑。扩展的 JS 执行时只能访问扩展页面的 DOM,要在用户打开的网页添加功能则必须向这些网页注入 JS。有些扩展需要向所有页面注入 JS,如果注入的 JS 文件很大则可能对浏览器性能造成影响。

iPhone/iPad 屏幕坏点检测网页

上周去 Apple Store 维修我用了快两年的 iPhone 4,换回来的机器屏幕上有一小块区域有亮点。于是写了个显示单色页面帮助测试坏点的网页,方便下次去维修时检查屏幕。

测试网页链接在此

其实写这个测试页面更多是尝试下写网页。Twitter Bootstrap 对我这样几乎完全没有前端经验的程序员来说的确是好东西。顺便还尝试了下 CoffeeScript,这语言看起来真不错,融合了 Ruby, Python 很多好的 feature。

P.S.

Update: 因为 iPhone 屏幕亮斑昨天再次去 Apple Store 更换,得知现在的电池跟以前的不同,如果拿下来会导致变形,所以现在维修都是机头和电池一起更换。

很多人应该知道今年央视 315 是故意黑苹果。即使我这次换到的屏幕有亮点(苹果会同意再次更换),我还是想说这次去 Apple Store 维修的经历很愉快。维修时我只是随口提了一下,Apple Genius 就额外给我换了新的电池和耳机。这样大度的售后或许是苹果产品较高价格的另一个合理之处。

Memory Consistency Model

Memory consistency model 是个不好想的东西,写并发程序的总有一天会遇到这个坑,如果不知道它的存在连怎么掉坑里的都不知道。我目前自己对此也不是弄得一清二楚,写这篇文章更多是让我自己做个总结,所以只做简单介绍,另外推荐一些我觉得介绍这方面的好文章。

什么是 Memory Consistency Model

并行程序在多个 CPU 上同时执行代码,如果每个 CPU 访问的都是自己私有的数据,那世界将非常美好,我们可以既不需要同步也不需要考虑 memory consistency model 的问题。但实际情况是总会有一些数据需要共享,有些 CPU 修改共享数据,有些读取。

Memory consistency model 定义了多个 CPU 同时对共享内存进行读写操作时内存的行为

汽车为什么要有刹车

Why do cars have brakes? 这篇文章不错,继续往下读之前可以先想一想这个问题。

没有刹车,意味着不能很快减速,出于安全考虑不得把车开慢些。刹车给了我们减速的能力,因此让我们能够驾驶的更快。

原文作者将此类比到软件开发中,如果将软件构建的过程类比为开车,那测试就好比是刹车。

  • 如果有好的自动化测试,我们可以比较放心地去修改代码,尽早发现 bug,停下 coding 修复 bug
  • 没有测试则要么写代码的时候总是很小心,每次 commit 之前检查很多遍;要么依然全速前进,偶尔发现一堆 bug 之后花更多的时间 fix
    • 我现在的习惯是每次 commit 代码之前先在 Source Tree 中检查几遍,但改动一多依然难免引入 bug

最近优化 COW 性能的过程中对此非常有同感。之前写 COW 的时候偷懒,没有写很多测试。最近做性能优化的过程中引入过一些 bug,没有测试代码即使 fix 了也不是非常放心。几次之后索性修改哪部分代码就先把测试补上。虽然写测试需要花额外的时间和精力,但有测试之后可以放心修改代码、尽早发现 bug,总的来说应该反而节省更多的时间。

这次我才是切身体会到测试对于重构以及优化代码的重要性。

最后,顺带宣布一下 COW 0.6 版发布。主要变化:

  • 允许用户指定 PAC URL,方便通过端口映射将 COW 提供到外网
  • 性能优化
  • 对 HTTP client/server 更加宽容,参考 RFC2616 19.3
  • 一些 bug fix

感谢 go test 以及 Travis CI 在测试上提供的帮助。

COW 0.5

放寒假之后花一周多的时间改进 COW,修复了些 bug,增加了一些新功能。改动比较大,所以跳过 0.4 版直接发布 0.5。使用说明和配置样例的注释都改成了中文,配置文件选项有些变化,请参考 github 上的说明修改;二进制文件下载还是在 Google Code。(发布之后才想起来 Google Code 貌似也被墙,使用安装脚本的话需要配置代理环境变量。)

感谢 glacjay 的建议,让我把 COW 做的更加自动化一些。0.5 版本最大的变化也在此。

由于没法准确判断超时是由于被墙还是连接状况差导致,被墙网站的判断没法做到非常准确。COW 现在的做法是:

  • 统计访问的 host 被墙和直连访问的次数,两者的差超过一定次数后才判断为被墙或者直连
    • 只有判断为直连后才会加入到 PAC 中
    • 如果原先的直连网站哪天被墙,不使用 PAC 再用 COW 访问一遍即可
  • 对不确定是否被墙的网站总是尝试直连,失败后两分钟内总是用二级代理,过后再尝试直连
  • 如果某个网站直连访问失败过,会把判断被墙的连接和读取的超时时间减半,避免等待太久
  • 内置一些直连和被墙网站列表

我自己用下来这个实现的效果还不错。原先的 updateBlocked, updateDirect, autoRetry 这些需要人工干预的选项都去掉了,名字很囧的 chou 文件也不需要了。

其他改动:

  • 支持 HTTP 二级代理(今天刚加的功能,仅用 goagent 测试过)
  • 用户可以自己指定超时时长,COW 也会自动估算
  • 支持使用 host 指定被墙和直连网站
  • 改进了 Windows 支持(仅仅在 XP 上测试,Win 7 不知道是否能正常工作)
  • 支持监听多个地址
  • 支持 IP 地址和用户名密码认证

之前想过的解决 DNS 污染的功能还是不打算做了,像 twitter 即使拿到了正确的 IP 其实还是不能访问,加入这个功能或许意义不大。使用 Go 的 ssh 实现来内建 ssh 代理支持的功能暂时也不做了,一来不清楚这个实现的稳定性,二来我觉得 shadowsocks 比 ssh 代理更加好用,而且除了 windows 平台以外加入这个功能的好处不大。

欢迎更新使用,发现 bug 麻烦在 github 上给我发 issue report。

反汇编 X86 指令

Update on 2013-01-16:Dawson Engler 的主页上看到一篇论文,Reverse-Engineering Instruction Encodings,利用系统的汇编器自动生成指令 encoding 信息。有了这个信息就可以写 code generator, disassembler 了。Dawson 在这篇 paper 的说明里写道:

It’s genesis was the fact that writing a binary encoder for the x86 is an unredeemably disgusting experience.

我想说,写 x86 的反汇编器是一样的感受。


折腾了 QEMU 两年多,看到 Bellard 大神Javascript 写的 x86 模拟器后也曾想用 Go 写一个 x86 的模拟器。

2012 年的时候花了些时间从反汇编 x86 指令开始写,写着写着就没有动力继续下去了。想要模拟 x86 指令集非常麻烦,光是反汇编和 condition code 处理就包含了大量无趣的体力活。暂时不会有兴趣继续这个项目了。(或许开始时应该采纳实验室同学的建议选一个简单一些的体系结构来模拟,例如任天堂的游戏机,不过因为懒得去学其他体系结构而没有采纳。)

不过这个半途而废的项目倒是让我更加了解 x86 的指令。这篇博客就分享一些当时找到的关于 x86 指令集的资料和发现吧。

Valgrind 中用 Pipe 实现的 GIL

Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation 这篇论文时看到了一种实现 GIL 的方式。

先介绍一下为什么 Valgrind 需要 GIL。Valgrind instrument 二进制文件并且执行采用的方式叫做 Disassemble-and-resynthesise (D&R)。其大概过程如下:

  • 将原二进制代码反汇编到中间代码
  • Valgrind 插件对得到的中间代码插入做检查的中间代码
  • 对中间代码做优化之后,生成 native 代码

由于中间代码是 RISC like 的,一条 CISC 的读写指令操作可能对应多条中间代码,最后生成的对应原先读写指令的 native code 有多条指令组成。在多线程的情况下这会使得读写操作不再是原子性的。(即使在单核机器上也会有这个问题,因为线程可能在执行一条读指令的过程中被调度走。)

Valgrind 解决这个问题的方式是让多线程程序在任何时候都只有一个线程在执行,这就需要一个类似 Python/Ruby 解释器里 GIL 的东西,在 Valgrind 中这个机制实现很巧妙。

Valgrind 利用一个 pipe 来实现这个机制。尝试拿锁的线程都尝试从全局的 pipe 中读,由于 pipe 中没有内容可读而 block 住。锁拥有者在解锁时往 pipe 中写一个字节,OS 会保证只有一个线程被唤醒,读出 pipe 中内容然后往下执行,其他线程则继续 sleep。这个方案里究竟那个线程被唤醒还是由 OS 决定,但可以严格保证任何时候都只有一个线程执行。(用 pthread mutex 的话还是会在 spin 的时候有一些线程同时执行,虽然对 Valgrind 的应用场景来说应该没有什么问题。)

Valgrind 的论文还是挺有意思的。虽然也是 binary translation,但它没有使用 code chaining,而是用手写的汇编来提高从 translated code 执行跳出后再次进入 translated code 的速度;翻译的单元也不是通常使用的 basic block 而是 super block。论文里提到的很多机制现在实现方式都是已经修改过多次,写出这样一个成熟的工具真是非常不容易。