无聊的圣诞节

开题的日期已经定在了 26 号,报告和幻灯做完了,就等着答辩那十五分钟,又无聊了。

圣诞于我而言是一个貌似没有的节日,除了几条短信外,与平日没有什么不同。大学里每逢这样的日子,晚上基本上都是在图书馆中度过。自从来到中科院这个鬼地方,我越来越怀念南大的图书馆了。尤其是浦平阅览室,虽然是用泡沫板搭成的简易房,但是早晨的太阳透过玻璃窗洒在空荡荡的桌子上那一幕我依然很难忘怀。浦口校区的新图书馆已经建成,不知道浦平阅览室恢复原来运动馆的用途了还是被拆掉了。想这些也无用,随着仙林校区的建成,我以后恐怕得到一个叫做金陵学院的地方去回忆我的大学三年了。

最近一段过得很累。论文有压力,害怕自己不是科研的材料,报了 GRE,又无法全身心地去投入准备。面临困难不是奋力战斗而总想逃避,深深地陷入到网络、电影当中。看到自己一连串地在豆瓣上 tag 了一堆电影、电视剧,我多希望把这些 tag 打在我那些正在读的书上呀!

每天对着 17 寸的 CRT 怔怔到两眼发直,也没做多少有意义的事情,我反而开始怀念那些写代码的日子,至少在完成的时候我会觉得由衷地开心。几次想重整自己到好的状态,总是又妥协,想战胜自己的本性好难。

无聊的时候会想念起大学的一帮哥们,无论做什么平日里好热闹,不似现在这等冷清。越来越能理解为什么一些人宁愿留在差一点儿的本校也不愿来科学院,生活的精彩程度上,研究生院和一般大学真的是不能比。

时常会挂着聊天软件,Gtalk, MSN, QQ, 飞信,一堆人在却也没有兴趣说话,除了和女友聊聊天,再没有和什么远方的人保持频繁的联系,我觉得自己都有点儿不近人情了。越大越封闭自己,就是在博客里,也是技术技术技术,好像我的生活都是由各种代码堆起来的。

宿舍里也冷得要命,房间是出奇的大,也是出奇的冷,中央空调一点儿用都没有,我盖两床被子偶尔还觉得凉。想安逸地躲在宿舍看看书,坐着不动超过半小时两腿就会冰凉,唉。

总之,这半年来是我很低谷的一段时期,我希望能有些变化。

Google Friend Connect 挺好玩的

经常来我博客转转的朋友会发现,我最近挺享受右下角的 Google Friend Connect,尤其是那个留言板。

目前来讲我没有发现 Google Friend Connect 的 SNS 功能有多好,顶多差不离也就是和 Live Space 的朋友一样,但是我非常欣赏这个留言板。这个看起来用处不大的留言板功能,我玩得很开心,因为我发现它提供了我最喜欢的 MiniBlog 功能。

一提起 MiniBlog,大部分人都会想饭否和 Twitter,Twitter 我用的很少,饭否用过一段时间,但我都不满意。拿发贴来说,要么要到它们的网站上去,要么跟聊天机器人说话,还会看到一堆别人的广播,真没意思。Facebook 和校内的状态也可以看作 MiniBlog 的一种,但是也存在别人的网站上,也会骚扰朋友和被骚扰。

Google Friend Connect 的留言板有几个特色我很喜欢:

1. 消息存储在第三方服务器上,可以显示在多处。
2. 发言即发即见,不用和机器人说话(或者跑到别的网站上发言),再去刷新自己的页面。
3. 别人也可以回复或发言,而且可以设置为登录后才能发言,屏蔽了垃圾消息(却不用我自己管理用户)。
4. 发言不会广播给朋友(也不支持订阅),干嘛闲扯几句还去骚扰别人?爱看就过来看,不强迫别人看。
5. 留言板显示在自己的网站而不是融合在别人的服务当中。

正好估计也没多少人去用这个留言板功能,我就自娱自乐,作为自己的 Blog 的补充,有空就扯几句,挺好!

一些关于博客的小变动

2007 年 2 月 03 日,我的博客从最早的 MSN Space迁移到 Blogspot,看着中文标题导致的类似于“%e8%90%bd%e5%8f%b6”的中文链接感觉特别不爽,就开始试着用使用英文标题。

2007 年 6 月 18 日,又迁移到 Yo2 的博客托管服务,开始使用 WordPress 博客和自己的域名。由于对国内的博客托管不是很放心,也一直在同时更新 SpaceBlogspot。为了格式统一,继续使用英文标题。

转眼间已经一年半,除了关键词过滤,我对 Yo2 的服务还是比较满意的。也越来越喜欢 WP 博客,尤其是发现 WP 能几乎完美地支持整站迁移文章、评论和主题之后。再加上我还可以手动备份数据,就可以随时在别的主机上恢复整个站点,那么另外两个博客就没什么必要了。今天我把 http://solrex.blogspot.com 给关了,地址会重定向到 http://blog.solrex.cnhttp://solrex.space.live.com 继续保留,但只发一些生活类文章,主要为 MSN 好友共享,技术类文章就不贴过去了。

既然专注于 WP 博客,WP 有 WP slug translate 插件,可以将中文标题自动翻译或者转成拼音,而且也可以自定义链接地址,那我就不用费尽心机想英文标题了,以后标题就能随便写了。估计一个直接的后果是...牢骚文章增多

取下装逼 FAQ 中的“为什么你的博客文章总是使用英文标题?”那段,是以为记:

为什么你的博客文章总是使用英文标题?

主要原因是为了使链接好看。

刚开始写博客的时候,用的是微软的 MSN Space,Space 不区分中文和英文标题,所有产生的链接都是处理过的乱码串。后来我的博客迁移到 Google 的 Blogspot,Blogspot 在文章的链接中不对标题进行处理,所以如果使用中文标题,文章链接中就会出现成串的%XX%XX用来代替汉字的转义字符串,非常不利于将链接贴到其它地方,也不利于网站流量的监控,所以从那时起我就采用全英文的标题了。

而且我发现使用全英文的标题还有一个好处,使我每次写文章的时候都要仔细考虑标题和内容的关系,避免了我写一些垃圾文章。

The Gold Old Tools: Pic and Chem

我本来只想写一下 pic 这个小工具,因为很少有中文资料介绍其用法。但是写下来没想到一个小软件联系到那么多典故,更让我对当年的贝尔实验室高山仰止。The gold old days never come again.

目录
1. Pic 的作者
2. 一个 Pic 的简单例子
3. 一个复杂的例子
4. 关于 chem 的典故
5. 在 LaTeX 中使用 pic

1. Pic 的作者

Pic,我敢打赌大部分读者没有听说过这个软件(或者说语言),但是我也敢打赌大部分读者听说过它的作者。

所以我只好从它的作者 Brian W. Kernighan 开始介绍了。不过也许用不着我介绍,假如你学过 C 语言,并且碰巧读过那本经典的 The C Programming Language(K&R);假如你使用 Linux,并且碰巧用过 AWK;或者你不小心学了一门编程语言,并且碰巧你学会写的第一个程序叫做“Hello, world”,那么你都应该感谢 Kernighan。没错, K&R 和 AWK 中的 K 都是代表 Kernighan,而且 Kernighan 是第一个使用 “Hello, world” 的人

2. 一个 Pic 的简单例子

Pic,是 Kernighan 写的一个图像排版软件,它主要用来作为 troff 中图片的预处理工具。简单点儿来说,pic 就是用来画特定一类图的工具,比如流程图、状态图、Petri~网、化学分子式等等。就像可以用写代码的方式写文档一样(TeX),我们也可以用写代码的方式画图,pic 就是这样一种语言。我们来看一个简单的例子(来自 More Programming Pearls):

.PS
ellipse "Source" "Code"
arrow
box "Compiler"
arrow
ellipse "Object" "Code"
.PE

将上面代码保存成一个 compiler.pic 文件,然后执行下面命令:

$ pic compiler.pic | groff | ps2eps > compiler.eps

我们就能得到名为 compiler.eps 的图像,下面这张图是使用 convert 命令从 compiler.eps 转换成的 png 图像:

Compiler(无法看到此图,可能因为您无法连接国外网站)

其实 Linux 下的 pic 是 GNU 版本的 gpic,groff 也是 GNU 版本的 troff,它们是都是被 GNU 重写了的自由软件版本的 pic 和 troff。

上面编译的步骤是,先用 gpic 将 compiler.pic 预处理为 troff 文档,然后用 groff 生成 ps 文件,然后用 ps2eps 将 ps 文件转化为 eps 图片。由于它们都是从标准输出打印文件,所以我们就用管道将这三个命令连接了起来,并且最后将标准输出重定向到 eps 文件。

3. 一个复杂的例子

看到这些,可能有人就怀疑说,这图很简单嘛,用 Photoshop 或者 GIMP 也花费不了多长时间,我干吗要再去学一门语言呢!那么,你尝试用 Photoshop 画一下下面这张图片。

Chemical Structure(无法看到此图,可能因为您无法连接国外网站)

画这张图的代码如下:

.cstart
    R:  benzene pointing right
        bond left from R.V4 ; HO
        bond -150 from R.V3 ; CH3O
        bond right from R.V1 ; C
        double bond up from C ; O
        bond right from C ; N
        bond 45 ; C2H5
        bond 135 from N ; C2H5
.cend

看到这,可能有人感叹说,哇,pic 真厉害!错(or 半错?),上面这段代码不是 pic 语言,而是 chem 语言。pic 是 troff 的预处理器,而 chem 又是 pic 的预处理器。所以上面这段代码的编译命令是:

$ chem.pl chem.chem | pic | groff | ps2eps > chem.eps

chem.pl 先把上面代码转换成 pic 语言,然后再用 pic 进行处理。

4. 关于 chem 的典故

关于 chem,还有一段典故:

话说当年(别问我,我不知道是哪一年)一个星期一的下午,Kernighan、 Jon Bentley 和一个同在贝尔实验室的化学家 Lynn Jelinski 闲扯了一会儿,Lynn 就向他俩抱怨在文档中插入化学结构式实在太麻烦了。正好 Kernighan 刚写了 pic,就想,咦,这不可以用 pic 画吗?于是那天晚上 K&B 就写了一个简单的前端。Lynn 一看,哎,不错!他们仨花了一个星期时间完成了 chem 语言,还在 Computers & Chemistry 杂志上发了篇 paper,名字叫做:Chem - a program for phototypesetting chemical structure diagrams。

别着急,还没完。话说当年(1990) GNU 要搞自由软件版本的 troff,开始重写 troff,把前端中的 pic, tbl, soelim 和 eqn 都重写了,就剩下画数学公式的 ideal 和 chem 没有重写。到现在你看你自己 Linux 中的 info groff,在 Introduction->Preprocessor 的最后还能找到:“There are other preprocessors in existence, but, unfortunately, no free implementations are available. Among them are preprocessors for drawing mathematical pictures (`ideal') and chemical structures (`chem').“

时间慢慢到了 2006 年 10 月 19 号,一个叫 Bernd Warken 的小子在 groff 的邮件列表中吼了一嗓子:哈哈,我用 Perl 重写了 chem!然后维护者 Werner LEMBERG 大叔就说:Great! 才有了我上文中的 chem.pl,原始的 chem 是用 AWK 语言写的。但是 chem.pl 到现在还没有到 groff 的正式发行版中,原因很囧,groff 在 2006 年 10 月 16 号以后还没有 release 新版本。所以如果你想使用 chem.pl,只能到 groff 的 cvs 仓库下载:http://cvs.savannah.gnu.org/viewvc/groff/contrib/chem/?root=groff

5. 在 LaTeX 中使用 pic

上面只是简单介绍了一下用 pic 做 eps 图片的过程,但是 pic 还可以用到 LaTeX 中,这样简单的图片就直接在 TeX 里写了。比如我们的第一个图片,就可以这样在 TeX 中使用:

\documentstyle{article}
\begin{document}
  \newenvironment{centergpic}{}{\begin{center}~\box\graph~\end{center}}
     \begin{centergpic}
.PS
ellipse "Source" "Code"
arrow
box "Compiler"
arrow
ellipse "Object" "Code"
.PE
     \end{centergpic}
\end{document}

.PS 和 .PE 一定要在行首。将上面文件保存成 compiler.tex,用 pic 预处理一下,再用 latex 编译:

$ pic -t compiler.tex > compiler1.tex
$ latex compiler1.tex
$ dvipdfmx compiler1.dvi

就能生成含有该图的 pdf 文件。

PS:若有人对 Pic 感兴趣,请阅读 Eric S. Raymond 大叔写的 “Making Pictures With GNU PIC”。你没看错,就是写《大教堂和集市》和《Unix 编程艺术》那个 Raymond 大叔。你也不用下载它,到 /usr/share/doc/groff/ 下面找一找吧,那个叫做 pic.ps.gz 的。

Aspell: 程序员的拼写检查利器

作为一个程序员,尤其是非英语母语国家(ESL or EFL)的程序员,写出漂亮的注释可能要比写出漂亮的代码更难。就比如 Eric 的“来自英语母语国家的”女友就有 “Programmers are English-challenged.“ 的评论。

那么如何在程序的注释中避免犯一些低级语法或者拼写错误呢?Eric 也在 Some useful tools for you to write English articles on Linux 中推荐了几个小工具。我这里算拾人牙慧,稍微写一点儿我非常欣赏的 Aspell 拼写检查工具。

Aspell 是一个强大的拼写检查工具,尤其是对于程序员来说。在 Linux 下,大部分程序员应该是用 Vim 或者 Emacs 写代码,它们有内建拼写检查功能,比如 vim 可以用 :setlocal spell spelllang=en_us 开启对美式英语的拼写检查。不过很少人会安装或者使用拼写检查功能,不是每个人都喜欢写代码时面对一堆高亮的词组(当它们不仅检查注释时,哦,天那!)。幸运的是,我们有 Aspell。

Aspell 使用方法非常简单,比如只想检查 C 或者 C++ 风格的注释和字符串中的拼写错误,就可以用这样的命令:

$ aspell --mode=ccpp -c test.cpp

终端里就会列出一个一个注释中的错误,并给出修改意见。接下来的工作就很简单了,按照窗口下面每个键对应的功能,选择更换单词或者忽略该单词。如下图所示:

Aspell 拼写检查工具(无法看到此图,可能因为您无法连接国外网站)

Aspell 还有更多模式,比如检查 HTML, TeX, XML, Perl 等等一些文档或程序中的拼写,更多内容就请看 Aspell 的帮助吧。

Aspell 的用户习惯保存在 ~/.aspell.en.prepl 和 ~/.aspell.en.pws 两个用户自定义替换和忽略单词列表里,可以通过备份或者修改这两个列表来改变 aspell 对某些单词拼写检查的策略。

Feedburner 订阅数图标显示解决办法

很多人以前都用过 Feedburner 烧制自己的 rss feed,但是由于众所周知的原因,Feedburner 的 rss 输出在中国网,封天下无法访问了(不信您可以点击一下我博客右侧的 Feedburner 图标)。虽然用 Google Reader 订阅 Feedburner 的 feed 仍然不受影响,但是博客订阅数图标无法正常显示,所以很多人好奇我是如何让 Feedburner 订阅数图标仍然显示在博客侧栏的

更新:刚才写完,我想看看 feed 有没有更新,忽然发现 Feedburner 的 rss 可以访问了,试了试订阅数图标,也能正常显示了。GFW 打盹了?我说这几天 Feedburner 订阅数增加那么快呢。总之我这篇文章算是白写了......呜呜呜呜,没有提前重现问题的后果。

又更新:12 月 23 日,我又无法访问 feedburner 的 RSS 了,才一天那!看来我这篇文章还算没有完全白写,不能幻想 GFW 的仁慈。

其实我以前是用的这篇博客里的方法。这个方法要求你有个国外主机空间,碰巧我能使用师兄的空间,把那篇文章中的 feedburner.php 上传到空间里就可以使用了。

但是使用过程中我发现这个方法有个很严重的问题:不支持并发访问。这是由于它的方法太生硬,先读取自己文件的内容,如果文件中写的时间比当前时间早 4 个小时,就下载新的订阅数图标,重写自身文件(修改更新时间那一行),并将订阅数图标附在文件最后。注意到这里,它会重写自身文件,一个 php 文件读取自己,改一行再重新写入自己,那么如果多个用户同时访问该文件,那不就乱套了?

所以我对它进行了修改,改为一个相对干净的方法:抓取订阅数统计图标保存为一个 gif 文件,每次访问 php 文件时,php 去判断当前时间与该 gif 文件最后修改时间的差,如果大于过期时间,就重新抓取订阅数统计图标更新 gif 文件,最后将访问重定向到 gif 文件。点击这里 http://solrex.org/feedburner.php 查看效果。

具体 php 代码如下(其实我本想用 file_get_contents 函数的,但发现不好用,只好还用这个原来的 httpSocketConnection 函数了,显得冗长了些):

<?php
$username = "username"// Feedburner account name.
$expire_time = 3600;   // Expire time(in second, 3600s = 1 hour).

$fb_url = "feeds.feedburner.com";
$gif_path = "/~fc/".$username."?bg=99CCFF&fg=444444&anim=0";
$localfile = "fb_".$username.".gif";

if(!function_exists('httpSocketConnection')){
  function httpSocketConnection($host, $method, $path, $data)
  {
    $method = strtoupper($method);
    if ($method == "GET") {
      $path.= '?'.$data;
    }
    $filePointer = fsockopen($host, 80, $errorNumber, $errorString);
    if (!$filePointer) {
      return false;
    }
    $requestHeader = $method." ".$path."  HTTP/1.1\r\n";
    $requestHeader.= "Host: ".$host."\r\n";
    $requestHeader.= "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\r\n";
    $requestHeader.= "Content-Type: application/x-www-form-urlencoded\r\n";
    if ($method == "POST") {
      $requestHeader.= "Content-Length: ".strlen($data)."\r\n";
    }
    $requestHeader.= "Connection: close\r\n\r\n";
    if ($method == "POST") {
      $requestHeader.= $data;
    }          
    fwrite($filePointer, $requestHeader);
    $responseHeader = '';
    $responseContent = '';
    do {
      $responseHeader.= fread($filePointer, 1);
    }
    while (!preg_match('/\\r\\n\\r\\n$/', $responseHeader));
    if (!strstr($responseHeader, "Transfer-Encoding: chunked")) {
      while (!feof($filePointer)) {
        $responseContent.= fgets($filePointer, 128);
      }
    } else {
      while ($chunk_length = hexdec(fgets($filePointer))) {
        $responseContentChunk = '';
        $read_length = 0;
        while ($read_length < $chunk_length) {
          $responseContentChunk .= fread($filePointer, $chunk_length - $read_length);
          $read_length = strlen($responseContentChunk);
        }
        $responseContent.= $responseContentChunk;
    &nb
sp;   fgets($filePointer);
      }
    }
    return chop($responseContent);
  }
}

function get_fbcount($host, $path, $file)
{
  $content = httpSocketConnection($host, 'GET', $path, NULL);
  $fp = fopen( $file,"w" );
  fwrite($fp, $content);
  fclose($fp);
}

if (file_exists($localfile)) {
  $last_modified = filemtime($localfile);
  if ( date('U') - $last_modified > $expire_time) {
    get_fbcount($fb_url, $gif_path, $localfile);
  }
} else {
  get_fbcount($fb_url, $gif_path, $localfile);
}
Header("Location: $localfile");
?>

将上述代码保存为一个 php 文件,比如 fb_username.php,修改用户名和过期时间,上载到国外或者港台能正常访问 Feedburner 的主机空间中,您就可以在网页中用:

<img src="http://your.host.domain/fb_username.php" style="border: 0pt none ;" alt="Feedburner">

来引用 Feedburner 订阅计数图标了。由于这个 php 脚本按照用户名保存 gif 图标,您可以在一个服务器上为多人提供引用,只需修改 php 文件的 usrname 一项,并上传为另一个 php 文件,您也可以将这两个变量改为 php 文件的参数(不建议这样做,因为会被别人利用)。

您也可以在这里下载到 feedburner.txt,要记得将其后缀改为 php 哦。

密码传奇

不想睡觉,就写一个小书评吧。

《密码传奇》

上次在国家科学图书馆借一本密码学教材的时候,碰巧看到了这本书:《密码传奇》,赵燕枫著,科学出版社。在一堆散发着枯黄的时代气息的小破书中间它显得格外醒目。出于好奇就拿起来翻了翻,看到大致内容是讲二战时期 Enigma 密码机的故事。关于这个密码机的历史资料并不少,但是系统性的中文演绎还没有看到过,所以我就捎带着把它借了回来。

从标题中可以明显看出,这不是本教科书式的历史书。作者的行文风格和写《明朝那些事儿》的当年明月颇为相似,整本书的成书过程与其也是如出一辙:先是在论坛上写了些相关的文章,赚了些人捧场,然后就萌发了系统性写下去的想法,开始认真地搜集资料撰写,并在论坛上讨论,最后被出版社看上出版实体书。有了论坛、博客,每个有心人都可以成一家之言,这是计算机网络带给我们实实在在的好处。

Enigma 大概是历史上最有名的密码机了,由于它在二战中被纳粹德国和轴心国大量部署,并且耗费了同盟国大量的物质和智力资源去研究它,再加上其最终被“计算机科学之父”阿兰·图灵破解,恐怕没有任何密码机制比它更出风头了。从 Enigma 的发展中,任何一段拎出来都是一个跌宕起伏的历史故事。比如电影 Enigma(拦截密码战) 和 U-571(猎杀U-571) 讲述的都是 Enigma 相关的故事。

《密码传奇》这本书系统地呈现了 Enigma 密码机的产生和发展的过程,同盟国中的波兰和英国分别在破解 Enigma 中起的作用,以及不同类型 Enigma 的结构和加密机制,是了解这段历史和 Enigma 结构非常好并且有趣的读物。在这本书里,作者特别强调了三个历史英雄人物:发明 Enigma 的亚瑟·谢尔比乌斯,初步破解 Enigma 的马里安·雷耶夫斯基和终结 Enigma 传奇的阿兰·图灵。在介绍群论在破解 Enigma 中的应用时,还穿插讲述了群论之父伽罗瓦的故事。

为了更准确地讲述历史,作者做了许多非常细致的考证工作,在文中也附加了大量的实物和历史照片,比如布莱奇利庄园的原貌和现状,各种型号的 Enigma 密码机和零部件实物照片。总的来说,这是一本非常有意思的科普读物,我是捧着这本书一口气把它读完的。

关于信用卡的灵异事件

我不得不承认,这两天在我身上发生的事情很灵异。

昨天晚饭时间,我收到一条工行发来的消息,说您的信用卡网银收入 25 元。我就非常纳闷,我又没在网上开店,又没人给我汇款,基本上都是支出,哪儿来的收入呀?就算是工行给的积分奖励,也不该是通过网银支付呀?

纳闷了半天,决定回来查一查到底怎么回事,打开钱包,忽然发现信用卡不见了。真是见鬼了,钱包里一堆破卡都在,唯独不见了信用卡。上次用卡是两天前在第三极买了本书,清楚地记得把卡放回钱包里了,很有可能是出来后在公交车站掏公交卡的时候不小心掉了,但是只掉了一张卡也太奇怪了。回去找了找也没找见,于是就去挂失呗,我仍然用着南京的手机号,漫游费太贵,只好跑工行取款机那打免费的 95588 电话,谁知道过去一看,取款机室锁得结结实实,放机器的地方被掏了个大洞。我都怀疑是不是老天和我过不去,只能回来找同学手机挂了个失。

今天到工行问了下,才知道人家取款机早就扒出来了,可能要装修。又打了次电话给 95588,搞定了寄新卡的事情。工行的国际卡还是比较赞的,口头挂失就算正式挂失,挂失费是 20,还给免费换新卡,不像交行那样黑心,挂个失都要 50 块。

但我现在仍然不明白的是,那 25 块收入到底算怎么回事,怎么就正好在我丢了卡打进来?说实话,要不是这莫名奇妙的收入,我至少还得几天发现不了自己卡丢了。

PS: 我的自行车也跟我过不去,才俩星期没骑,今儿一看,车座皮给冻裂了几个大口子。北京这天儿呀!

关于燃油税影响的一些疑问

以前副修金融学的时候曾经学习过南大商学院编的宏观和微观经济学,萨缪尔森的《经济学》也曾读过一半,最近在选择床头读物的时候又开始翻起曼昆的《经济学原理》。

人对经济学的喜好程度,大概跟他对现实的接受程度有关。那些对现实社会中的规则抱着宽容的理解态度的人,往往更能接受经济学中对“理性”的假定而不至于对一些经济行为和规则产生反感。比如“多收了三五斗”并不能代表农民总收益会增加这一经济事实,叶圣陶可能就未必能理解。

昨天晚上我看到一节内容,有关税收归宿的问题,联想到当前的形势,产生了一些疑问。

曼昆在书中一个小节讨论了税收归宿与弹性的关系。当对某一种物品进行征税时,无论税收的直接对象是谁,该物品的买者和卖者总是会分担税收负担。因为税收总可以通过价格的调整转嫁到另外一方身上,只是税收归宿的比例会有所不同。他指出一个关于税收负担划分的一般结论:税收负担更多地落在缺乏弹性的市场一方身上。当对某商品征税时,弹性小就意味着该方当条件不利时不能轻易而举地离开市场,从而必须承担更多的税收负担。曼昆在书中举了一个例子有关奢侈品税的例子:

在 1990 年美国国会开始针对游艇、私人飞机、皮衣、珠宝和豪华轿车这类物品征收奢侈品税,其目的是增加那些承担税收负担最轻松的人的税收。由于只有富人能买得起这类奢华物品,所以这看似是向富人征税的一种合理方式。

但是考虑到游艇市场,游艇的需求是极其富有弹性的,百万富翁不买游艇是很容易的,他可以选择花钱去买更大的房子,去度假等等。但是对于游艇的供给在短期中却是非常缺乏弹性的,因为游艇工厂不能轻易而举地转为它用,游艇工人也不好去更换其它职业。根据上面的理论,由于富有弹性的需求和缺乏弹性的供给,税收负担必然大部分落在供给者身上,进而大部分落在游艇工厂主和工人身上。因此,奢侈品税的负担落在中产阶级身上的部分比落在富人身上的部分多。最终,美国国会认识到了这一困境,在 1993 年免除了大部分奢侈品税。

我的疑问是,当前政府要考虑开征燃油税,那么税收负担的划分可能会是什么样子?如果是按照弹性决定的话,那么到底是需求的弹性大还是供给的弹性大?经济学中讨论的是自由竞争的市场,那么当市场是一个垄断市场的时候,税收负担的划分又会是什么样子?

我还不明白的是为什么某些无车族仅仅因为对有车富人的不满心理对燃油税的征收叫好。我觉得从需求的弹性来讲,私家车主对燃油的需求弹性明显要高于出租车司机、长途汽车主和公共汽车公司,当燃油税高时私家车主可以很轻松地选择少开车;相反出租车司机、长途汽车主和公共汽车公司对燃油的需求是缺乏弹性的,当燃油税高时他们不能把车闲置到一边不开了。再加上他们有比较大的需求量,那么他们就会承担更多的燃油税。当他们承担更多的燃油税时,势必会将部分税收转嫁到消费者身上,也就是我们这些无车的民众了。公共汽车可能有国家补贴不至于涨价,但长途汽车和出租车票价上涨可能是必然的结果。对于一个小老百姓来讲,需求弹性就更低了,我们总不能因为票价上涨就不出门了吧,很可能是我们比以前要承担更重的税收负担。因此我觉得燃油税不是和我们没关系,而是有很大关系。但是这个问题可能还需与环境等其它社会问题同时考虑。

古人都说“学以致用”,以上是一个经济学的门外汉对当前社会一个经济问题产生的一些疑问和不成熟看法,如果读者中有哪位兄台能够指点迷津,请不吝赐教。

《使用开源软件-自己动手写操作系统》Rev 2发布

免费电子书《使用开源软件-自己动手写操作系统》的主页在:http://share.solrex.org/WriteOS/ ,您可以到这里下载 pdf 格式电子书和随书源代码。

免费电子书《使用开源软件-自己动手写操作系统》(无法看到此图,可能因为您无法连接国外网站)

2008 年 2 月 21 日发布第一版,拖了十个月我才发布了第二版。虽然有一些懒惰的原因在里面,但更重要的原因是没有很多可以大块利用的时间。从开始动手,才知道写书是一件非常痛苦的工作,尤其是有代码的书。再加上本书目前的主要代码都是汇编语言,一旦出错就要花好长时间调试,代码运行正确了,要在不同的 Linux 进行编译以确保能正确通过,又要加注释、除去冗余指令,代码的工作结束就要接着关注排版、查资料、填补内容、做插图,总之写一天书下来我累得精神都会振奋不起来。

本来打算十月底就发布第二版,但是因为研究工作稍微耽误了一下,就又拖到了十一月底,总之我还是完成了这一章。这一版虽然只增加了第三章,但页码却从 40 页增加到了 104 页,示例代码也从 2 个增加到 10 个,与第一版的工作量不可同日而语。由于这一版的发布周期过长,我在按版本发布的基础上增加了每周发布,也因此在编写过程中得到了不少帮助。

这本书从计划开始就得到很多朋友的肯定,在编写的过程中也得到了很多朋友的帮助。不计刚发布第一版时几乎每天一千次的下载量,从 2008 年 5 月 9 号把所有源代码迁移到 Google Code 项目后,加上每周发布,就有共计两万一千多次下载。我非常感谢大家对我一如既往的支持,感谢那些在我的博客评论或者发电子邮件给我打气的朋友,尤其感谢那些在邮件中或者错误报告页中指出本书错误或者提供很好建议的朋友!

我尤为高兴的是听一位朋友说北京邮电大学某位教授操作系统课程的老师向同学推荐这本电子书,这正与我写这本书的初衷相合,就像我在前言中所说:

本书的最终目标是成为一本大学“计算机操作系统”课程的参考工具书,为学生提供一个step by step 的引导去实现一个操作系统。这不是一个容易实现的目标,因为我本人现在并不自信有那个实力了解操作系统的方方面面。但是我想,立志百里行九十总好过于踯躅不前。

我将继续努力将这本书写下去,也希望大家能够继续对这本书保持关注,并帮助我完善此书。下面是本书这次发布的章节信息,如果您发现本书中的错误(那是不可避免的),或者有更好的建议,请您一定到本书的错误报告页指出,兄弟我将非常感谢!

第三章进入保护模式
3.1 实模式和保护模式. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.1.1 一段历史. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.1.2 实模式. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.1.3 保护模式. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.1.4 实模式和保护模式的寻址模式. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.2 与保护模式初次会面. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.2.1 GDT 数据结构. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.2.2 保护模式下的demo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.2.3 加载GDT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.2.4 进入保护模式. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.2.5 特别的混合跳转指令. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.2.6 生成镜像并测试. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.3 段式存储. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.3.1 LDT 数据结构. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.3.2 段描述符属性. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.3.3 使用LDT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.3.4 生成镜像并测试. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.3.5 段式存储总结. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.4 特权级. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.4.1 不合法的访问请求示例. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.4.2 控制权转移的特权级检查. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.4.3 使用调用门转移. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.4.4 栈切换和TSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
3.5 页式存储. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
3.5.1 分页机制. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.5.2 启动分页机制. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
3.5.3 修正内存映射的错误. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
3.5.4 体验虚拟内存. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.6 结语. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

统计二进制中 1 的个数

这是一道《编程之美-微软技术面试心得》中的题目,问题描述如下:

对于一个字节(8bit)的变量,求其二进制表示中“1”的个数,要求算法的执行效率尽可能地高。

《编程之美》中给出了五种解法,但是实际上从 Wikipedia 上我们可以找到更优的算法

这道题的本质相当于求二进制数的 Hamming 权重,或者说是该二进制数与 0 的 Hamming 距离,这两个概念在信息论和编码理论中是相当有名的。在二进制的情况下,它们也经常被叫做 population count 或者 popcount 问题,比如 gcc 中就提供了一个内建函数:

int __builtin_popcount (unsigned int x)

输出整型数二进制中 1 的个数。但是 GCC 的 __builtin_popcount 的实现主要是基于查表法做的,跟编程之美中解法 5 是一样的。Wikipedia 上的解法是基于分治法来做的,构造非常巧妙,通过有限次简单地算术运算就能求得结果,特别适合那些受存储空间限制的算法中使用:

/* ===========================================================================
* Problem:
*   The fastest way to count how many 1s in a 32-bits integer.
*
* Algorithm:
*   The problem equals to calculate the Hamming weight of a 32-bits integer,
*   or the Hamming distance between a 32-bits integer and 0. In binary cases,
*   it is also called the population count, or popcount.[1]
*
*   The best solution known are based on adding counts in a tree pattern
*   (divide and conquer). Due to space limit, here is an example for a
*   8-bits binary number A=01101100:[1]
* | Expression            | Binary   | Decimal | Comment                    |
* | A                     | 01101100 |         | the original number        |
* | B = A & 01010101      | 01000100 | 1,0,1,0 | every other bit from A     |
* | C = (A>>1) & 01010101 | 00010100 | 0,1,1,0 | remaining bits from A      |
* | D = B + C             | 01011000 | 1,1,2,0 | # of 1s in each 2-bit of A |
* | E = D & 00110011      | 00010000 | 1,0     | every other count from D   |
* | F = (D>>2) & 00110011 | 00010010 | 1,2     | remaining counts from D    |
* | G = E + F             | 00100010 | 2,2     | # of 1s in each 4-bit of A |
* | H = G & 00001111      | 00000010 | 2       | every other count from G   |
* | I = (G>>4) & 00001111 | 00000010 | 2       | remaining counts from G    |
* | J = H + I             | 00000100 | 4       | No. of 1s in A             |
* Hence A have 4 1s.
*
* [1] http://en.wikipedia.org/wiki/Hamming_weight
*
* ===========================================================================
*/
#include <stdio.h>

typedef unsigned int UINT32;
const UINT32 m1  = 0x55555555// 01010101010101010101010101010101
const UINT32 m2  = 0x33333333// 00110011001100110011001100110011
const UINT32 m4  = 0x0f0f0f0f// 00001111000011110000111100001111
const UINT32 m8  = 0x00ff00ff// 00000000111111110000000011111111
const UINT32 m16 = 0x0000ffff// 00000000000000001111111111111111
const UINT32 h01 = 0x01010101// the sum of 256 to the power of 0, 1, 2, 3

/* This is a naive implementation, shown for comparison, and to help in
* understanding the better functions. It uses 20 arithmetic operations
* (shift, add, and). */
int popcount_1(UINT32 x)
{
  x = (x & m1) + ((x >> 1) & m1);
  x = (x & m2) + ((x >> 2) & m2);
  x = (x & m4) + ((x >> 4) & m4);
  x = (x & m8) + ((x >> 8) & m8);
  x = (x & m16) + ((x >> 16) & m16);
  return x;
}

/* This uses fewer arithmetic operations than any other known implementation
* on machines with slow multiplication. It uses 15 arithmetic operations. */
int popcount_2(UINT32 x)
{
  x -= (x >> 1) & m1;             //put count of each 2 bits into those 2 bits
  x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits
  x = (x + (x >> 4)) & m4;        //put count of each 8 bits into those 8 bits
  x += x >> 8;           //put count of each 16 bits into their lowest 8 bits
  x += x >> 16;          //put count of each 32 bits into their lowest 8 bits
  return x & 0x1f;
}

/* This uses fewer arithmetic operations than any other known implementation
* on machines with fast multiplication. It uses 12 arithmetic operations,
* one of which is a multiply. */
int popcount_3(UINT32 x)
{
  x -= (x >> 1) & m1;             //put count of each 2 bits into those 2 bits
  x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits
  x = (x + (x >> 4)) & m4;        //put count of each 8 bits into those 8 bits
  return (x * h01) >> 24// left 8 bits of x + (x<<8) + (x<<16) + (x<<24)
}

int main()
{
  int i = 0x1ff12ee2;
  printf("i = %d = 0x%xn", i, i);
  printf("popcount_1(%d) = %dn", i, popcount_1(i));
  printf("popcount_2(%d) = %dn", i, popcount_2(i));
  printf("popcount_3(%d) = %dn", i, popcount_3(i));
  /* If compiled with other compiler than gcc, comment the line bellow. */
  printf("GCC's  __builtin_popcount(%d) = %dn", i,  __builtin_popcount(i));
  return 0;
}

使用 Synergy 共享键盘和鼠标

今天我使用了一个让我感觉非常震撼的软件:Synergy,相见恨晚!

简单地来说,这个软件的功能是在 n 台电脑间共享同一个电脑上的鼠标和键盘。和扩展桌面不同,扩展桌面是一台主机拖两台以上的显示器,这个软件是用一套鼠键控制两台以上的主机。(我曾经见到过一个医学仪器用的 IBM 的键盘,可以控制几台工作站,貌似是几万块。)

像我们计算机科学的学生,一般来说都会有两台电脑。比如我自己,实验室一台台式机,自己一台笔记本,当我把笔记本电脑拿到实验室的时候,总会发愁把我的笔记本外接键盘和外接鼠标放在哪里(我不喜欢用笔记本自带键盘,用外接键鼠我就可以半躺在椅子靠背上打字)。手底下放着两个键盘和两个鼠标,总要想一想我现在要用哪个。通过这个小软件,我的笔记本和台式机可以共用台式机的键盘,这样我就不必注意我到底在用哪台机器的键盘和鼠标了,感觉好爽!

还有一个非常牛的特性就是:支持共享剪贴板。因为大部分程序员使用两个以上的显示器是为了写代码的同时在另一个显示器上翻看文档或者网页,我们使用两台以上电脑的作用也在于此。如果两台电脑之间可以共享剪贴板,再加上 NFS 或者 Samba,那么在大部分情况下来说,这和一台电脑拖两台显示器没任何区别了(也许更方便,比如你可以一端跑 Linux,一端跑 Windows)。

关于这个软件的介绍可以见下面两篇博客:用 Synergy 共享两台电脑的键盘鼠标用 Synergy 共享鼠标键盘

需要解释一下使用 Synergy 时容易引起混淆的概念: Server 是指被共享鼠标键盘的主机,比如我就是使用台式机作为 Server;Client 是指使用 Server 的鼠键的主机。

使用 QuickSynery 时也要注意:Server 端 QuickSynery 需要指定 Client 主机在哪个位置,比如我希望笔记本在左面,就在左面填入笔记本的主机名,但不需要指定 Server IP;Client 端 QuickSynery 只需要指定 Server IP,但不需要选择机器的相对位置;另外,在尝试连接不成功后,一定要杀死后台所有 synery 进程,最好用命令行启动 quicksynery 通过屏显来查找错误。

QuickSynery 的最新版本是 0.8,Ubuntu 仓库中版本是 0.6,如果您使用 QuickSynery 过程中出现错误或者希望使用更新的版本,可以到我的共享网站上下载我自己编译打包的 QuickSynery 0.8: http://share.solrex.org/ibuild/quicksynergy_0.8_i386.deb

从一个数列中取具有最小和的子序列

问题描述:给定一个含有 n 个元素的数列,元素有正有负,找出和最小的一组相邻的数。即给定 a[n],求 i,j 使得 a[i] + a[i+1] + ...+ a[j] 的和最小。

这个问题1并不难,但是我在想这个问题时经历了比较有趣的思考过程,写下来给大家分享一下。

其实这道题主要考察的是问题转换(或者说是抽象?)的能力,即如何将一个看似复杂的问题转换成一个简单的问题。直接求一个连续子列的和看起来很麻烦,我们要考虑 i 和 j 的取值,然后把子列中的元素加起来。但是换一个角度,一个连续子列的和可以看成是两个前缀和相减,比如:a[i] + a[i+1] + ...+ a[j] 实际上就等于 s[j] - s[i-1],其中 s[k] = a[0] + a[1] + ... + a[k]。这在高等数学中也是会被经常用到的方法。

所以我们要做的就是

计算出前缀和序列 s[n],并找出最小的 s[j] - s[i],其中 j>i,且 j,i 属于 {0,1,...,n}。

如果不要求 j>i,那么答案很简单,s[n] 中最小值减去最大值就是结果。但既然要求了 j>i,就不能这样做了,因为最小值的位置未必在最大值后面。最显而易见的方法就是遍历了:

J = 1; I = 0;
for (i=0; i<n; i++) {
  for (j=i+1; j<n; j++) {
    if (s[j] - s[i] < s[J] - s[I]) {
      I = i; J = j;
    }
  }
}

但是这个算法很耗时,两层嵌套,等差数列求和,复杂度肯定是 O(n2)。观察一下数列的规律我们可以发现,如果 s[i] < s[I] 的话,我们就没有必要计算 s[j] - s[i],因为结果肯定比 s[j] - s[I] 大嘛,同理可以用于 s[j] > s[J] 的情况,这样呢,我们可以稍微优化一点儿:

J = 1; I = 0;
for (i=0; i<n; i++) {
  if (s[i] < s[I]) continue;
  for (j=i+1; j<n; j++) {
    if (s[j] > s[J]) continue;
    if (s[j] - s[i] < s[J] - s[I]) {
      I = i; J = j;
    }
  }
}

但是这个优化作用到底有多大?我没有仔细计算,是比第一种情况快了一点儿,但最显著的速度提升还是在于第一层循环中,那样就能减少 n-i+1 次计算,在最坏的情况下仍然是 O(n2)。所以我觉得仍然不够,不就求一个最小值嘛,为什么那么慢呢,肯定是因为我笨。于是我就在 BBS 求助了一下别人,果然有人给出更快的算法:

J = 1; I = 0; max = 0;
for (i=1; i<n; i++) {
  if (s[i]-s[max] < s[J]-s[I]) {
    I = max;  J = i;
  }
  max = s[i] > s[I] ? i : max;
}

这个算法的思想是: s[I] 肯定是 s[J] 之前最大的数,s[J] 也是 s[I] 之后最小的数,那么保留到目前为止最大的数 s[max],用当前数 s[i] 去减它(肯定是小于 s[i] - s[I] 的),看它是否小于 s[J] - s[I],如果是的话,那么 s[i] -s[max] 就是到 i 为止最小的差值对。扫描一遍 s[n] 就能得到结果,O(n)!

后来我发现在 Jon Bentley 的《编程珠玑》第二版第8章讨论了这个问题1(只不过是讨论的最大值),在该章最后提出的算法4中,仅仅使用了与上面算法相同的扫描方法,而没有使用累加操作。虽然上面算法复杂度与算法 4 相同,但算法 4 复杂度中 n 的系数要小一些,下面是算法 4 的 C 语言实现:

int i = 0, I = 0, J = 0, I_end = 0;
int max_end = 0, max_sofar = 0;
for (i=0; i<len; i++) {
  max_end += a[i];
  if (max_end > 0) {
    max_end = 0; I_end = i;
  }
  if (max_sofar > max_end) {
    max_sofar = max_end;
    I = I_end; J = i;
  }
}

[1] 2008年1月15日注:这个问题出现在《Programming Pearls》第二版的 Column 8,Jon Bentley 对该问题的起源和算法发展做了非常详致的分析。

明天-法定结婚年龄

今天小杨同学睡了一个大懒觉,九点多醒来和女朋友发了几条消息,一转身又睡着了。再醒来看表就到 12 点了,想起来约好 12 点半和某同学碰头一起去参加轮滑社团的活动,于是乎准备发条消息取消掉,忽然发现没有对方的号码。有约定不好食言呀,就挣扎着爬起来快速洗脸刷牙去吃饭,总算按时和大部队集合了。

下午的主要活动就是轮滑了。今年第一次中科院玉泉、中关村和北郊校区的刷子们同时行动,目的地是奥体中心!从玉泉到中关村距离有 16 公里,虽然有几位猛男/女是刷过去的,对我来说还是太远了,所以还是老老实实坐公交到青年公寓。然后大家都换上鞋,30 多号人浩浩荡荡地就杀往鸟巢了,其实这也是我第一次刷街。

从青年公寓到鸟巢大约花了一个小时吧,然后在鸟巢附近的广场上玩了玩,到晚上撤退时已经累得不行了。社团一起在北郊一个叫做“孙记乡村驴肉馆”的小店吃了顿饭,您还甭说,味道还真不错,平均每人只花了二十多块,吃得饱饱的,也挺实惠。吃完饭实在没力气再滑,就坐 751 回来啦。

话说了这么多,言归正传,明天可是小杨同学的大日子。什么日子呢?就是我的 22 岁生日,当当当当,就是到法定结婚年龄啦(^_^)。其实也没什么大不了的,不过某小姑娘会小开心一下啦。

按照我的习惯,过生日时总喜欢在博客里感慨一番,不过今年呢......也不例外。

虽然没有达到我的设想,总的来说这一年还是做了一些事情。这学期开学以来,研究工作不是很顺利,到现在我仍然认为还没有进入状态,但也是有几个亮点的,不至于对自己的工作全盘否定。生活方面对自己满意好多了,规律的作息和饮食,充足的体育锻炼,鼻炎仍然有困扰,但程度已经较上年减轻很多,这也是我乐于感受到的。要说烦忧,谁能没有呢?就不足为外人道了。

唉,我发现最近一段时间技术类博客已经统领我的发文了,这是个不好的趋势,以后要注意和生活类的平衡,毕竟我希望这里成为一个我成长方方面面的记录。不过今天就写到这吧,要和某小姑娘开始聊天了 :)

即时通信软件协议

我十多天前发表的那篇《定制自己的免费天气预报短信》,相信引起了不少人的兴趣。这篇文章是为那些希望更深入了解即时通信协议,并想做一些 hack 工作的同学提供的一个小索引。

关于飞信的协议,可以参考下面两篇文章,某人的博客和一个飞信插件的源代码:

[1] 付安民, 张玉清. 飞信即时通监控系统的设计与实现. 计算机工程, 2008, 34(13).

[2] 付安民, 张玉清. 即时通实时监控系统的设计与实现. 通信学报, 2008, 29(10).

[3] http://hi.baidu.com/nathan2007/blog/category/飞信协议分析

[4] Pidgin 飞信插件

关于其它常见 IM 的协议,可以参考下面这篇文章和一个开源软件的源代码:

[5] R.B. Jennings, E.M. Nahum, D.P. Olshefski, et al. "A study of Internet instant messaging and chat protocols," IEEE Network, vol.20, no.4, pp. 16-21, 2006.

[6] Pidgin - multi-protocol Instant Messaging client that allows you to use all of your IM accounts at once.

我真的很希望飞信的 Pidgin 插件能更成熟,比如群功能之类的还要完善,最好以后能 merge 到 Pidgin 中,这样我就不用在 Pidgin 之外再开着一个 Libfetion Linux 客户端了。而 Libfetion 仍然也有亟待完善的地方,比如群管理员无法成功发送群消息。如果聪明的您能完成一个近于完美的 Linux 飞信客户端,我真的要谢谢您呢!

用两个非门和任意的与、或门构造三个非门

计算机科学中有很多有趣的 puzzle,他们可能出现在那些自命清高的企业的笔试题中,也可能来源于在网路上不经意的一瞥。后者比如我在无意中访问到的 Jamie Zawinski 的个人主页:http://www.jwz.org/,他即是在著名的 Teach Yourself Programming in Ten Years 一文中,Peter Norvig 提到的那位:

One of the best programmers I ever hired had only a High School degree; he's produced a lot of great software, has his own news group, and made enough in stock options to buy his own nightclub.

Jamie Zawinski 的个人主页看起来就像是 hexdump 的结果,而且以某种规律变化着,比较奇怪的是其中埋藏的一些超级链接并不发生变化。Jamie Zawinski 在网页的源代码中这样写道:

<!-- mail me if you find the secret -->
<!--   (no, you can't have a hint)  -->

我徒劳无功地搜索了一下,没有找到任何明确的解答。如果您通过我这篇文章了解到这个 puzzle,并且解答了它的话,非常希望您也 mail 给我一份解答。

说了那么多,其实本文的主题 puzzle 却是来源于前者。这个问题更详细的阐述是:

假设有一个逻辑黑盒,三个布尔型变量 x, y, z 作为输入,输出三个布尔变量 X, Y, Z,其中:
X = ~x; Y = ~y; Z = ~z;
注意,~ 符号代表一个非门。请用两个非门和任意多个与、或门实现这个黑盒。

这个问题大概是在计算机硬件设计中提出来的,所以看起来貌似很“电子”,但是其基础却是计算机科学中的布尔代数运算。下面代码的注释中已经包含详细的算法和分析,这里我就不再解释了。(当然,这个问题的答案不是我想出来的,我只是实现并分析了一下,作为我算法学习的笔记记录在此。)

/* ===========================================================================

* Problem:
*   Construct 3 NOT gates from only 2 NOT gates(and *no* XOR gates).
*
*   Assume that a logical blackbox has three Boolean inputs x, y, z and
*   three Boolean outputs X, Y, Z where the outputs are defined as:
*     X = ~x
*     Y = ~y
*     Z = ~z
*   Note that ~ stands for a NOT gate. Please realize this blackbox using
*   only two NOT gates, and as many as possible AND and OR gates.
*
* Algorithm I:
*   Internal Nodes:
*   r = (x & y) | (x & z) | (y & z);
*   R = ~r;
*   s = (R & (x | y | z)) | (x & y & z);
*   S = ~s;
*
*   Equations for Outputs:
*   X = (R & S) | (R & s & (y | z)) | (r & S & (y & z));
*   Y = (R & S) | (R & s & (x | z)) | (r & S & (x & z));
*   Z = (R & S) | (R & s & (x | y)) | (r & S & (x & y));
*
* Analysis I:
*   We create 4 internal signals first: r, R, s and S. What equations above
*   say is that signal `r' will be 1 if two or three of the inputs are 1.
*   Meanwhile, signal `s' will be 1 if only one input is 1 or if all three
*   inputs are 1. The end result is that the two-bit word formed from `r'
*   and `s' tells us how many 1's we have[1]:
*   | r s | Means  |    | x y z | r s |    | x y z | r s |
*   |++++++++++++++|    |+++++++++++++|    |+++++++++++++|
*   | 0 0 | 0 Ones |    | 0 0 0 | 0 0 |    | 1 0 0 | 0 1 |
*   | 0 1 | 1 One  |    | 0 0 1 | 0 1 |    | 1 0 1 | 1 0 |
*   | 1 0 | 2 Ones |    | 0 1 0 | 0 1 |    | 1 1 0 | 1 0 |
*   | 1 1 | 3 Ones |    | 0 1 1 | 1 0 |    | 1 1 1 | 1 1 |
*
*   Thus now that we have the signals r and s (and their inverse
*   counterparts R and S), it's easy to construct any Boolean function of
*   x, y, and z, using only AND and OR gates:
*     X = (R & S) | (R & s & (y | z)) | (r & S & (y & z))
*   Proof:
*    1> x, y, z are all 0s, (R & S) = ~(r | s) = 1, obviously X=Y=Z=1, X = ~x;
*    2> (x, y, z) has at least one 1, R & S = 0, will be ignored, hence we
*       have:
*         X = (R & s & (y | z)) | (r & S & (y & z))
*    2.1> (x, y, z) has two or three 1s, R = ~r = 0, (R & s & (y | z)) = 0,
*         will be ignored, then we get:
*           X = S & (y & z)
*    2.1.1> (x, y, z) has three 1s, S = ~s = 0, obviously X=Y=Z=0, X = ~x;
*    2.1.2> (x, y, z) has two 1s, S = ~s = 1, will be ignored, hence we have:
*             X = y & z
*    2.1.2.1> (y, z) has one 1, x = 1, X = y & z = 1 & 0 = 0, X = ~x;
*    2.1.2.2> (y, z) has two 1s, x = 0, X = y & z = 1 & 1 = 1, X = ~x;
*    2.2> (x, y, z) has one 1, r = 0, (r & S & (y & z)) = 0, will be ignored,
*         we have:
*           X = y | z
*    2.2.1> (y, z) has one 1, x = 0, X = y | z = 1 | 0 = 1, X = ~x;
*    2.2.2> (y, z) has no 1s, x = 1, X = y | z = 0 | 0 = 0, X = ~x.
*    In conclusion, X = ~x for all cases.
*   QED.
*
* Algorithm II:
*   Internal Nodes:
*   _2or3_1s = ((x & y) | (x & z) | (y & z));
*   _0or1_1s = !(_2or3_1s);
*   _1_1     = _0or1_1s & (x | y | z);
*   _1or3_1s = _1_1 | (x & y & z);
*   _0or2_1s = !(_1or3_1s);
*   _0_1s    = _0or2_1s & _0or1_1s;
*   _2_1s    = _0or2_1s & _2or3_1s;
*
*   Equations for Outputs:
*   X = _0_1s | (_1_1 & (y | z)) | (_2_1s & (y & z));
*   Y = _0_1s | (_1_1 & (x | z)) | (_2_1s & (x & z));
*   Z = _0_1s | (_1_1 & (x | y)) | (_2_1s & (x & y));
*
* Analysis II:
*   Almost the same as Analysis I.
*
* [1] http://www.edadesignline.com/howto/191600992
* ===========================================================================
*/

#include <stdio.h>

typedef unsigned int BOOL;

int main()
{
  int i, fail;
  BOOL x, y, z, X, Y, Z;
  BOOL r, R, s, S;
  BOOL _2or3_1s, _0or1_1s, _1_1,  _1or3_1s, _0or2_1s, _0_1s, _2_1s;

  /* ==================== Algorithm I ==================== */
  printf("Algorithm I:n");
  fail = 0;
  for (i=0; i<8; i++) {
    /* Init x, y, z. */
    x = i & 1;
    y = (i>>1) & 1;
    z = (i>>2) & 1;
    /* Internal nodes. */
    r = (x & y) | (x & z) | (y & z);
    //R = !r;                               /* #1 NOT gate. */
    R = ~r & 1;                             /* #1 NOT gate. */
    s = (R & (x | y | z)) | (x & y & z);
    //S = !s;                               /* #2 NOT gate. */
    S = ~s & 1;                             /* #2 NOT gate. */
    /* Output. */
    X = (R & S) | (R & s & (y | z)) | (r & S & (y & z));
    Y = (R & S) | (R & s & (x | z)) | (r & S & (x & z));
    Z = (R & S) | (R & s & (x | y)) | (r & S & (x & y));

    if ((x == X) | (y == Y) | (z == Z)){
      fail ++;
      printf("FAIL: ");
    } else {
      printf("PASS: ");
    }
    printf("xyz = %u%u%u, XYZ = %u%u%un", x, y, z, X, Y, Z);
  }
  if (fail != 0) {
    printf("%d TEST FAILED!n", fail);
  } else if (!fail) {
    printf("ALL TEST PASSED!n");
  }

  /* ==================== Algorithm II ==================== */
  printf("Algorithm II:n");
  fail = 0;
  for (i=0; i<8; i++) {
    /* Init x, y, z. */
    x = i & 1;
    y = (i>>1) & 1;
    z = (i>>2) & 1;
    /* Internal nodes. */
    _2or3_1s = ((x & y) | (x & z) | (y & z));
    //_0or1_1s = !(_2or3_1s);               /* #1 NOT gate. */
    _0or1_1s = ~(_2or3_1s) & 1;             /* #1 NOT gate. */
    _1_1     = _0or1_1s & (x | y | z);
    _1or3_1s = _1_1 | (x & y & z);
    //_0or2_1s = !(_1or3_1s);               /* #2 NOT gate. */
    _0or2_1s = ~(_1or3_1s) & 1;             /* #2 NOT gate. */
    _0_1s    = _0or2_1s & _0or1_1s;
    _2_1s    = _0or2_1s & _2or3_1s;
    /* Output. */
    X = _0_1s | (_1_1 & (y | z)) | (_2_1s & (y & z));
    Y = _0_1s | (_1_1 & (x | z)) | (_2_1s & (x & z));
    Z = _0_1s | (_1_1 & (x | y)) | (_2_1s & (x & y));

    if ((x == X) | (y == Y) | (z == Z)){
      fail ++;
      printf("FAIL: ");
    } else {
      printf("PASS: ");
    }
    printf("xyz = %u%u%u, XYZ = %u%u%un", x, y, z, X, Y, Z);
  }
  if (fail != 0) {
    printf("%d TEST FAILED!n", fail);
  } else if (!fail) {
    printf("ALL TEST PASSED!n");
  }
  return 0;
}

DEB Packages of qRFCview and JabRef

由于最近的学习和研究有些不顺利,加上自制力过差,时间的利用上也是无法让自己满意,每每长吁短叹。今天下午心情格外不好,于是就回去闷头大睡到五点。幸好有女朋友劝慰,一通电话后感觉好多了。还是要有勇气,就像奥巴马一样,遇到挫折时吼一句:“Yes We Can!“

下午的时候利用工作时间打包了两个软件:qRFCview 和 JabRef,主要想着为了方便自己使用。反正时间已经浪费了,希望放出来能方便更多人吧。打包这两个软件的主要原因:

qRFCview:这是一个在 Linux 下看 RFC 文档的软件,输入 RFC 的编号就可以自动下载并格式化显示出来。这个软件已经两年多没有更新,想想当今世界两年时间会发生多少事吧!它在软件中限制 RFC 编号的范围是 1-5000,而现在 RFC 已经有 5389 个了,所以我就将这个范围扩大到 1-10000,重新编译了一下,打成 DEB 包,算是一个 bug fix 吧。

JabRef:这是一个文献管理软件,我曾经在博客中推荐过。它在 2008 年 11 月 1 号发布了 2.42 版,而 Ubuntu 软件仓库中的版本仍是 2.31,有很多特性不支持。所以我将新的 Jar 替换了旧的 Jar,重新打了一个 DEB 包。

这两个 DEB 软件包都可以在我的共享网站:http://share.solrex.org/ibuild/ 目录下找到。

定制自己的免费天气预报短信

摘要:这篇博客介绍了一种在 Linux 下使用飞信(libfetion 库)来定时发送天气预报短信的方法。本文的主要贡献是:一、提供了一个 Linux 下发送飞信的命令行程序;二、提供了一个到中国气象网抓取、过滤天气信息并发送短信的脚本。

Libfetion修改了调用接口,而且中国移动现在换IP登录就需要使用验证码。除非我哪天闲得蛋疼,搞一个验证码识别模块出来,否则本项目将不再维护,很抱歉!

天气预报短信一直是移动通信公司提供的一种收费服务,Google 免费天气预报服务打破了这个僵局。但是Google 的服务很不稳定,经常收不到短信,而且天气预报内容的定制性差。我家 xixi 一直有看天气预报的习惯,我就告诉她说我能写个程序每天给你发天气预报消息,她不相信,然后我就写了下面的程序。

首先感谢一下 mirth@bbs.nju.edu.cn,本文的主要内容是基于他在小百合 BBS 上发表的如何用飞信定时给自己发免费天气预报一文做的少许改进。

1. 发送飞信的命令行程序[1, 2, 3, 4, 5]

这个程序主要基于邓东东开发的 libfetion 库。这个库不是开源的,但是作者提供了头文件和库文件(在GUI源代码中),所以我们可以使用它的 API 来写一些自己的程序。下面的程序内容很简单,注释也不少,我就只贴源码,不再解释了(注意,编译时需要 curl 的 dev 库)。你可以在这里下载到我的 sendsms 小程序的源代码

sendsms
|-- Makefile
|-- include
|   |-- common.h
|   |-- datastruct.h
|   |-- event.h
|   |-- fxconfig.h
|   `-- libfetion.h
|-- lib
|   |-- libfetion_32.a
|   `-- libfetion_64.a
|-- sendsms
`-- sendsms.cpp

2. 到中国气象网抓取、过滤天气信息并发送短信的 bash 脚本

你可以从这里下载到下面的 bash 脚本,或者到这里下载几乎同样功能的 python 脚本。脚本就不多做解释了,没几行代码,相信稍微研究一下就能看懂。

天气网经常更新,新的脚本我就不再贴到博客里了。如果您发现天气预报脚本不好用了,就请关注脚本下载的地址,我一般会尽快更新的。

$ more weatherman.sh
#!/bin/bash
# This script fetch user specified citys' weather forecast from
# http://weather.com.cn, and send them using a CLI SMS sender "sendsms"
# which you can get from http://share.solrex.org/dcount/click.php?id=5.
#
# You can look for new or bug fix version
# @ http://share.solrex.org/scripts/weatherman.sh.
# Copyright (C) Solrex Yang <http://solrex.org> with GPL license.
#
# Usage: You should add it to crontab by "crontab -e", and then add a line
# such as:
# 00 20 * * * /usr/bin/weatherman.sh >> ~/bin/log/weatherman.log 2>&1
# which will send weather forecast to your fetion friends at every 8pm.

3. 将脚本设置为定时执行

安装好 sendsms 到 /usr/bin 之后,将上面脚本放到 YOURPATH 下,然后在命令行执行:crontab -e,将下面一行添加进去:

50 19 * * * /YOURPATH/weatherman.sh 1> /tmp/weatherman.out 2> /tmp/weatherman.err

就设置为每天下午 7 点 50 发送天气预报短信。

[1] 应大家要求,在程序中加入了读取 http_proxy 代理服务器环境变量的部分,其它类型的代理服务器可以自行添加(毕竟源代码给你了,随便改),增加了重试登录和发送的代码。

[2] 2008 年 11 月 30 日:增加了群发短信功能(多个接收者用','分隔)。

[3] 2009 年 01 月 11 日:增加从标准输入读入信息支持,可使用管道和输入重定向。这篇博客中的代码就不更新了,请到给出的链接去下载新版本。

[4] 2009 年 4 月 17 日:添加了"-l"选项,支持长短信发送,最长可到 1024 字节。解决了一个从标准输入读取短信的 bug。

[5] 2009 年 12 月 08 日:根据中国天气网的改版,更新抓取页面的脚本。

安装 Ubuntu 8.10 失败记

今天上午四个小时的折腾:

1. 想装个 FreeBSD 6.2,出现未知错误,找不到 /dev 下面的一个东西(仿佛回到了刚开始学 Linux 的时代,盲目呀);

2. 把分区表搞坏,一个扩展 data 分区丢失(这绝对是 FreeBSD 的问题,因为在安装时分区表中居然把这个 ext3 分区标示为 unused)。我也居然愚蠢到认为不到最后时刻它就不会写入分区表,结果导致整个分区数据丢失。还好这只是我工作用电脑,损失还不大;

3. 既然 data 都丢了,就随便再装个 Ubuntu 8.10 玩玩吧,结果遇到安装光盘的 bug。整整花了我两个小时呀,下了 RC 和 dailybuild 两个光盘镜像,都是一个问题,走到分区那一步,新建和修改分区的选项都是空白的,无法选择。到 launchpad.net 就发现不是我一个人在战斗,然后也插了一腿子。这是我自从注册 launchpad 那几天以来第一次登录,也是第一次灌水,注册时间是 2006-06-10。

4. 到了最后,还是用我原来的 Ubuntu 8.04,把分区重新建立挂载起来,下定决心再不折腾了。其实我想用 8.10,仅仅是被那个 nautilus 的标签式浏览诱惑了...

转了一圈,啥也没落着。得了一个教训:不要在双系统上装 FreeBSD,居然会犯 RedHat9.0 都不可能犯的错误,识别出错误的分区表。(话说今天遇到的 Ubuntu 8.10 的问题也类似,不过人家是 RC, 不是正式版。)