存档

‘技术’ 分类的存档

搭建私有的 python 包发布中心 pypi

2010年12月12日 3 条评论

项目组现在使用Python越来越多,大部分老逻辑都已经迁移到了Python上,相当数量的新逻辑都是Python写的。经过之前的一段时间的分享,团队已经开始使用 virtualenvsetuptools 来进行Python代码的开发和打包和发布了。

但是现在的问题是,随着项目规模的变大,以及几个子项目的启动,代码复用开始成为一个问题。有很多代码在多个库当中被使用,应该被抽取出来成为单独的模块。但是这些模块和模块的依赖关系会比较复杂,比如一个应用可能需要依赖pypi上开源的库,同时需要依赖一个内部的库,而内部的库又依赖开源的库。如何解决这种情况下的依赖管理和自动包管理呢?

实际上setuptools 和 easy_install 已经提供了完善的依赖管理,在setup.py当中写上所有依赖的模块名已经在团队内形成了共识,那么内部模块是否可以用同样的方式进行管理呢?

答案是肯定的。easy_install 本质是 pypi 服务的客户端,它依赖的web服务称为 pypi,或者叫Cheese Shop。是 http://pypi.python.org/simple/ 。这个页面下面就是一系列的 index.html 文件,指向各个版本的包文件。这个index.html本身没有严格的格式规定,只是其中应该包含<a>标签,指向每个版本。easy_install 负责抽取出这些标签,形成一个文件列表,比较版本,下载指定版本或者最近版本,并安装到系统上。

而且,easy_install支持一个命令行参数 –index-url,或者短参数 -i ,可以指定兼容于 pypi 的 pypi 索引。这个索引只要满足pypi的规定就是可以的,自然,这个是可以自己搭建的。

但是easy_install有一个限制,就是只能指定一个index URL。对于多个index的问题,PEP381明确说了,这是客户端的问题。easy_install选择不解决这个问题,也是一种解决方案吧……

但是easy_install不解决,我们就要想办法自己解决。去除这个限制有两个方法,一个是让自己的私有pypi在发现私有包里面没有匹配的包名字的时候重定向到pypi.python.org/simple,另一种方法就是使用 easy_install 的替代品 pip。在翻看了众多的部署脚本之后,我们决定,还是使用前面一个策略。

除了常用的easy_install来安装包,pypi还需要一个功能就是支持 distutils 兼容的协议上传包到服务器。对于使用disutils或者setuptools建立的setup.py文件,开发者可以使用 python setup.py register 将项目名注册到 pypi index,也可以通过 python setup.py bdist_egg upload 上传打包好的文件。这个协议很简单,很容易即可实现,只是其中需要的用户管理方面,稍微复杂和体力活一些。

也正是因为简单,搭建私有的pypi服务器的开源程序有很多,PEP 381当中有两个推荐,分别是PloneSoftwareCenter 和 EggBasket。PloneSoftwareCenter是一个恐龙级别的东西,它是一个完整的CMS,pypi只是其中一个小小的功能。为了这样一个简单的功能需要安装一大堆Plone的东西,实在是难以接受,而且它的文档简直是个杯具……,唉。

EggBasket稍好,但是也要安装一堆东西,包括一只小恐龙TurboGears。所幸TurboGears只是一只小恐龙,而且EggBasket本身的文档比较清楚,一步步照着做即可。由于一些安全方面的限制,EggBasket单独的服务器端口在我们的服务器上是不能访问到的,因此我们用apache的mod_proxy做了一个反向代理。

很快,基于EggBasket和apache mod_proxy反向代理的私有pypi就搭建起来,问题随即而来:EggBasket不支持我们上面要求的自动重定向。所幸源代码也不多,简单修改了一下之后,做了一个patch。需要的可以下载下来自己apply。我已经联系了EggBasket的作者,希望能够将这个patch合并进官方代码,但是作者表示,他现在正在休假。【update @2010-12-12 自从这个patch发送过去已经接近半年了,还没有响应,好吧,我放弃了】

经过这些patch,我们的pypi服务器就成功搭建起来了。项目组使用它的方式是:

开发机:
(dev) zhangc@dev-01:pypismpl$ python setup.py register -r http://pypi-server/pypi
We need to know who you are, so please choose either:
1. use your existing login,
2. register as a new user,
3. have the server generate a new password for you (and email it to you), or
4. quit
Your selection [default 1]: 1
Username: zhangc
Password: ********
Server response (200): OK
I can store your PyPI login so future submissions will be faster.
(the login will be stored in /home/zhangc/.pypirc)
Save your login (y/N)? y

这步操作执行一次即可,如果最后一步选择了save login,则后面不再需要每次都register。

在程序新版本稳定了之后,执行:
(dev) zhangc@dev-01:pypismpl$ python setup.py bdist_egg upload -r http://pypi-server/pypi/upload

即可把新版本的程序打包成egg并且上传到服务器。

这时候如果通过浏览器访问pypi服务器,会发现新版本的pypismpl程序已经在页面上列出了。

然后在生产机上:
(dev) zhangc@production-01:~$ sudo -u appuser -E /usr/app/env/bin/python -i http://pypi-server/pypi -U pypismpl

新版本的程序就会自动部署了。

标签:

谈一个Kernel32当中的ANSI到Unicode转换的问题

2010年6月17日 3 条评论

【这篇文章就是我在twitter上说过的那篇文章,早已写好,但由于一些原因压到现在才发表,深表歉意】

众所周知,Windows的几乎所有带有字符串参数的API都是有W和A两个版本,分别对应于Unicode和ANSI版本。同时,Windows内部是使用Unicode的,因此所有的A版本的API函数都是实际上调用了一次ANSI到Unicode的字符集转换之后,再调用Unicode版本的函数。

你真的清楚这中间的这一步转换吗?普通的工程师确实不需要了解太深刻,但是我们是做安全软件的,安全软件的一个基本实现机制就是Hook,就是在原本的API调用路径上面拐一个弯,改变原本的API执行流程,以便在其中插入安全检查等逻辑。

这当中,如果没有对于所要Hook的函数的深刻理解,也许会有很多看似灵异的事件出现。在我们的代码当中,我们就使用了Hook的机制,然后很幸运的(很不幸?),遇到了这样一个灵异问题:

在测试当中,发现某个软件一旦被我们Hook,就总是崩溃。必现的崩溃总是很容易解决的定位的,很快,我们发现崩溃来源于下面一个调用序列:

HMODULE hMod = LoadLibraryA("Kernel32");
PROC pfxIsWow64Process = GetProcAddress(hMod, "IsWow64Process");

对LoadLibrary的调用返回没有检查返回值,进而继续调用GetProcAddress。那么,当hMod = NULL的时候,GetProcAddress并不检查hMod是否是0,而直接试图去寻找其中的导出表,于是导致了非法的内存访问。

但是,这里的代码当中,调用的是LoadLibraryA(“kernel32″),kernel32.dll作为系统的核心链接库,通常情况下几乎必然是已经加载到了进程空间当中,即使没有,加载此DLL也不应该失败。

但是调试的结果显示,这个返回值的确是NULL,看起来相当灵异。但是我们相信,计算机没有灵异事件。为了排错,我们祭出WinDbg和IDAPro,从汇编层面一步步跟踪LoadLibraryA函数,看看中间究竟发生了什么。经过漫长的错误定位工作,在此略过不表(关于整个排查过程可以写一篇专门的文章),我们终于发现了问题的所在。

没错,加载了Hook就会造成崩溃,因此崩溃的原因在于Hook。

我们使用Inline Hook技术修改了LoadLibrary函数的执行流程,使得在进行真正的DLL加载之前进行某些校验和检查的工作。LoadLibrary函数其实是一个函数族,包括LoadLibraryA,LoadLibraryW,LoadLibraryExA,和LoadLibraryExW。它们之间的调用关系如下图:

我们选择了在整个调用依赖树的最底端的LoadLibraryExW的入口点作为Hook点,当一个函数调用LoadLibraryExW的时候,其实是调用到我们的Hook函数,Hook函数再调用真正的LoadLibraryExW,经过Hook,一个典型的LoadLibraryA的调用流程如下图:

问题就出在了这个CheckLibrary函数里面。在这个函数里面,我们调用了一个公共模块当中的日志函数,这个日志函数的逻辑是,以追加模式打开一个日志文件,写入然后关闭。且不论这个日志函数这样做的合理性,先来看看这段代码:

void Log(const char* msg) {
FILE* pf = fopen(_g_log_file, "a");
fprintf(pf, "%s\n", msg);
fclose(pf);
}

非常直观的程序,怎么会带来问题呢?我们来继续分析fopen函数。fopen函数是c库提供的文件IO函数,最终调用到Windows API CreateFileA,然后CreateFileA会调用到CreateFileW。把这个调用关系加上,调用序列如下图所示:

在这个函数调用序列当中,存在两次ANSI 到 Unicode 的转换,即图中标记有五角星的两个地方。那么我们来看看两个转换的代码是什么样子的:

LoadLibraryExA:

CreateFileA

注意到其中高亮的部分了么?两个函数在进入之后,都调用了一个公共的函数 _Base8bitStringToStaticUnicodeString,从名字就可以看出来,这段代码负责将输入的ANSI字符串转换为Unicode。那么这个函数做了什么呢?跟进去看看(这个使用了Hex-Rays的反编译功能,看得更清楚一点):

再仔细看给RtlAntiStringToUnicodeString这个函数传入的参数地址pUnicodeString,来自*MK_FP(__FS__, 24) + 3064,这是Hex-Rays的表示法,其实对应下面一串汇编序列(由于编译器的优化导致的乱序,重点看红线标出来的指令):

large fs:18h 这句魔法一样的指令,其实是 TEB 的地址。这个地址对于同一个线程来说是一样的,因此,如果在一个线程当中调用这个函数,总是会引用到同一个地址,这个地址就是当前线程的TEB当中的一个静态缓冲区(StaticUnicodeString & StaticUnicodeBuffer)。

很明显,这个静态缓冲区的作用是性能优化,对于ANSI到Unicode的转换,复用一个thread-specific的缓冲区来接受目标Unicode字符串。对于不太长的函数参数字符串,这个优化可以避免一次堆分配的过程,对性能的提升是很明显的。正常情况下这个优化是合理而且有效的。但是在存在Hook的情况下,事情就不是这么简单了。考虑一下上面那张图当中的LoadLibrary的调用序列:

解释一下:

  1. 在第1次ANSI到Unicode转换的时候, LoadLibraryA将目标Unicode字符串 L”kernel32″ 存入 StaticUnicodeBuffer,然后将这个字符串的首地址传递给LoadLibraryW
  2. LoadLibraryW在调用到LoadLibraryExW之前,先调用到了Hook函数
  3. Hook函数调用到了CreateFileA,传入日志文件名(ANSI)作为函数参数
  4. CreateFileA进行第二次ANSI到Unicode转换,将目标Unicode字符串(日志文件名)存入了StaticUnicodeBuffer,然后将这个字符串首地址传给CreateFileW
  5. CreateFileW返回,
  6. CreateFileA返回,
  7. Hook函数返回,
  8. 调用到实际的LoadLibraryExW。此时LoadLibraryExW从参数指针当中取出字符串试图进行加载动态库的操作,但是,它取出的是被在第4步当中覆盖掉的字符串,也就是Unicode版本的日志文件名!试图加载日志文件,自然会收到LoadLibrary失败的返回。‘
  9. LoadLibraryExW返回NULL,LoadLibraryW返回NULL,LoadLibraryA返回NULL。

然后,没有检查LoadLibraryA返回值的程序,在调用GetProcAddress的时候,biu~ 掉了。

好了,知道了为什么,怎么解决也就很容易了,在CheckLibrary当中,先备份传入的参数的内容,然后继续走流程,等到需要调用原本函数的时候,将备份的参数传给原本函数。

这就是Hook爱好者们的义务:当你试图改变某个执行流程的时候,请务必清楚地了解关于这个流程的一切。

CALL指令有多少种写法

2010年4月4日 6 条评论

最近有一个需求,给你个地址,看看这个地址前面是不是一个CALL指令(请同学们自行联想该需求的来源)。作为团队的救火队员+炮灰,这个简单的事情自然落在了我的头上。

这个事情很简单,作为一个善于站在别人肩膀上的程序员我们可以考虑使用 libdisasm;如果要考虑x64,就试试udis86;如果需要用Python,就有Python包装好的 pydasm。不过这两个400KB+的库,显然不值得为了一个CALL指令导入到编译出来大小仅仅100K不到的项目代码里面。

那么就自己抽一个CALL指令解码逻辑出来好了。这个逻辑的复杂性在于,你无法知道前面一个CALL指令有多长。因此,首先需要枚举出所有的CALL指令格式。

Intel有公开的指令集格式文档,你需要的是第二卷的上半部分,指令集从A到M。这篇文档的难度超出一般人想象,里面有众多晦涩的标识、与硬件紧密相关的介绍,拿到这后,即使直接翻到目录的CALL 指令一节,也不见得能够弄清楚。不相信?我们就翻到那里看看:

CALL指令格式一览表

CALL指令格式一览表

虽然很明确的列出,第一列是指令的二进制形式,第二列是指令的汇编形式,但是面对着 E8 cw, FF/2这样的标识,一样不知道究竟对应的二进制格式是什么样的。

那好,我们就从理解这些标识开始。文档向前翻,有一个专门的节(3.1.1 Instruction Format)讲述这些标识的含义。这里抽出其中两个用得着的翻译一下:

表格中的“Opcode”列列出了所有的所有可能的指令对应的二进制格式。有可能的话,指令代码使用十六进制显示它们在内存当中的字节。除了这些16进制代码之外的部分使用下面的标记:

cb, cw, cd, cp, co, ct — opcode后面跟着的一个1字节(cb),2字节(cw),4字节(cd),6字节 (cp),8字节(co) 或者 10字节(ct) 的值。这个值用来表示代码偏移地址,有可能的话还包括代码段寄存器的值。

/digit — digit为0到7之间的数字,表示指令的 ModR/M byte 只使用 r/m字段作为操作数,而其reg字段作为opcode的一部分,使用digit指定的数字。

红字部分不知道什么含义?没关系,我们先不看它。对于cb/cw之类的,基本上能够简单看明白其中的一些指令含义了:

E8 cw 的含义是:字节 0xE8 后面跟着一个2字节操作数表示要跳转到的地址与当前地址的偏移量。
E8 cd 的含义是:字节 0xE8 后面跟着一个4字节的操作数表示要跳转的地址与当前地址的偏移量。
9A cd 的含义是:字节 0x9A 后面跟着一个6字节的操作数表示要跳转的地址和代码段寄存器的值。

那么,同样的0xE8开头的指令,CPU如何区分后面的操作数是2字节还是4字节?答案是和CPU的模式有关,在实模式下,0xE8接受2字节操作数,而32位保护模式下接受4个字节,64位保护模式下同样接受4字节,同时需要对该操作数进行带符号扩展。

因此,CALL指令的前两种格式是:E8 xx xx xx xx,和 9A xx xx xx xx xx xx。一个是5字节长,一个是7字节长。其实E8 那种,就是我们在汇编指令里面写 CALL lable之后产生的,最常见的CALL指令。

然后是下面的FF /2。这个是0xFF字节后面跟上一个blablabla的东西。这个blablabla的东西是什么呢?要解释这个,首先需要知道红字标出来的部分,即ModR/M是什么东西。

这个要先回到最基本的一个问题:IA32的指令格式。

IA32,64指令格式

IA-32,Intel 64指令格式

其中每个部分是什么含义呢?

首先是指令前缀。有印象的应该记得当年学习微机原理的时候提到过得循环前缀 repnz/repne,这个前缀就是被编码在指令的前面部分的。每个前缀最多一个字节,一条指令最多4个前缀。

然后是指令代码(opcode),这部分标识了指令是什么。这个是指令当中唯一必需的部分。前面例子当中的 0xE8,0xFF都是opcode。

再后面就是我们要重点关心的 ModR/M字段了,还有和它密切相关的SIB字节。手册2.1.3当中有对于它们的详细描述。

许多指令需要引用到一个在内存当中的值作为操作数,这种指令需要一个称为寻址模式标识字节(addressing-form specifier byte),或者叫做ModR/M字节紧跟在主opcode后面。ModR/M字节包含下面三个部分的信息:

  • mod(模式)域,连同r/m(寄存器/内存)域共同构成了32个可能的值:8个寄存器和24个寻址模式。
  • reg/opcode(寄存器/操作数)域指定了8个寄存器或者额外的3个字节的opcode。究竟这三个字节用来做什么由主opcode指定。
  • r/m(寄存器/内存)域可以指定一个寄存器作为操作数,或者可以和mod域联合用来指定寻址模式。有时候,它和mod域一起用来为某些指令指定额外的信息。

这一段有些晦涩。其意思解释一下是这样的:一个指令往往需要引用一个在内存当中的值,典型的就是如mov:

MOV eax, dword ptr [123456]
MOV eax, dword ptr [esi]

这其中的 123456 或者 esi 就是 MOV 指令引用的内存地址,而MOV关心的是这个地址当中的内容。这个时候,需要某种方式来为指令指定这个操作数的类型:是一个立即数表示的地址,还是一个存放在寄存器当中的地址,或者,就是寄存器本身。

这个用来区分操作数类型的指令字节就是 ModR/M,确切的说是其中的5个位,即mod和r/m域。剩下的三个位,可能用来做额外的指令字节。因为,IA32的指令个数已经远超过一个字节所能表示的256个了。因此,有的指令就要复用第一个字节,然后依据ModR/M当中的reg/opcode域进行区分。

现在回头看前面的红字标识的部分,能不能理解 /digit 这种表示法了?

对于SIB的介绍,我们先忽略,看看对于CALL指令的枚举我们已经能做什么了。

CALL指令的表示法:FF /2,是 0xFF 后面跟着一个 /digit 表示的东西。就是说,0xFF后面需要跟一个 ModR/M 字节,ModR/M字节使用 reg/opcode 域 = 2 。那么,reg/opcode = 2 的字节有32个,正如ModR/M的解释,这32个值代表了32种不同的寻址方式。是哪32种呢?手册上面有张表:

32字节寻址模式下的ModR/M字节

32字节寻址模式下的ModR/M字节

非常复杂的一张表。现在就看看这张表怎么读。

首先是列的定义。由于 reg/opcode 域可以用来表示opcode,也可以用来表示reg,因此同一个值在不同的指令当中可能代表不同的含义。在表当中,就表现为每一列的表头都有很多个不同的表示。我们需要关心的就是 opcode 这一个。注意看我用红圈圈出来的部分,这一列就是 opcode=2 的一列。而我们需要的 CALL 指令,也就是在这一列当中,0xFF后面需要跟着的内容。

行的定义就是不同的寻址模式。正如手册所说,mod + R/M域,共5个字节,定义了32种寻址模式。0×10 – 0×17 对应于寄存器寻址。例如指令 CALL dword ptr [eax] :[eax]寻址对应的是 0×10,因此,该指令对应的二进制就是 FF 10。同理, CALL dword ptr [ebx] 是 FF 13,CALL dword ptr [esi] 是 FF 16,这些指令都是2个字节。有人也许问 CALL word ptr [eax] 是什么?抱歉,这不是一个合法的32位指令。

0×50-0×57部分需要带一个 disp8,即 8bit 立即数,也就是一个字节。这个是基地址+8位偏移量的寻址模式。例如 CALL dword ptr [eax+10] 就是 FF 50 10 。注意虽然表当中写的是 [eax] + disp8 这种形式,但是并不表示是取得 eax 指向的地址当中的值再加上 disp8,而是在eax上加上disp8再进行寻址。因此写成 [eax+disp8] 更不容易引起误解。后面的disp32也是一样的。这个类型指令是3个字节。

0×90 – 0×97部分需要带 disp32,即4字节立即数。这个是基地址+32位偏移量。例如 CALL dword ptr [eax+12345] 就是 FF 90 00 01 23 45。有趣的是, CALL dword ptr [eax+10] 也可以写成 FF 90 00 00 00 10。至于汇编成哪个二进制形式,这是汇编器的选择。这个类型的指令是6个字节。

0xD0 – 0xD7部分则直接是寄存器。这边引用的寄存器的类型有很多,但是在CALL指令当中只能引用通用寄存器,因此 CALL eax 就是 FF D0,臭名昭著的 CALL esp 就是 FF D4。注意 CALL eax 和 CALL [eax] 是不一样的。这些指令也是2个字节。

仔细的人也许主要到了,在表当中,0×14, 0×15, 0×54和0×94是不一样的。0×15比较简单,这个要求 ModR/M后面跟上一个32位立即数作为地址。即常见的 CALL dword ptr [004F778e] 这种格式的,直接跳转到一个固定内存地址处存放的值,常见于调用Windows的导出表。对应的二进制是 FF 15 00 4F 77 8E ,有6个字节。

0×14,0×54,0×94部分是最复杂的,因为这个时候,ModR/M不足以指定寻址方式,而是需要一个额外的字节,这个字节就是指令当中的第4个字节,SIB。同样在手册的2.1.3,紧跟着ModR/M的定义:

某些特定的ModR/M字节需要一个后续字节,称为SIB字节。32位指令的基地址+偏移量,以及 比例*偏移量 的形式的寻址方式需要SIB字节。 SIB字节包括下列信息:

  • scale(比例)域指定了放大的比例。
  • index(偏移)域指定了用来存放偏移量 的寄存器。
  • base (基地址)域用来标识存放基地址的寄存器。

0×14, 0×54, 0×94就是这里所说的“特定的ModR/M字节。这个字节后面跟着的SIB表示了一个复杂的寻址方式,典型的见于虚函数调用:

CALL dword ptr [ecx+4*eax]

就是调用ecx指向的虚表当中的第eax个虚函数。这个指令当中,因为没有立即数,因此FF后面的字节就是0×14,而 [ecx+4*eax] 就需要用SIB字节来表示。在这个指令当中,ecx就是 Base,4是Scale,eax是Index。

那么,Base, Scale和Index是如何确定的呢?手册上同样有一张表(又是巨大的表):

32位寻址模式当中的SIB字节

32位寻址模式当中的SIB字节

列是Base,行是Index*Scale,例如[ecx+4*eax] 就是0×81。

根据这张表,CALL dword ptr [ecx+4*eax] 就是 FF 14 81 。由此可见,对于 0×14系列的来说,CALL指令就是 3个字节。
而 0×54 带 8bit 立即数,就是对应于 CALL指令:CALL dword ptr [ecx+4*eax+xx],这个指令就是 FF 54 81 xx,是4个字节。
同理,0×94带32位立即数,对应于CALL指令:CALL dword ptr [ecx+4*eax+xxxxxxxx],这个指令就是 FF 94 81 xx xx xx xx,是7个字节。

OK,截止到目前,我们基本上能够列出常见的CALL指令的格式了:

指令 二进制形式
CALL rel32 E8 xx xx xx xx
CALL dword ptr [EAX] FF 10
CALL dword ptr [ECX] FF 11
CALL dword ptr [EDX] FF 12
CALL dword ptr [EBX] FF 13
CALL dword ptr [REG*SCALE+BASE] FF 14 xx
CALL dword ptr [abs32] FF 15 xx xx xx xx
CALL dword ptr [ESI] FF 16
CALL dword ptr [EDI] FF 17
CALL dword ptr [EAX+xx] FF 50 xx
CALL dword ptr [ECX+xx] FF 51 xx
CALL dword ptr [EDX+xx] FF 52 xx
CALL dword ptr [EBX+xx] FF 53 xx
CALL dword ptr [REG*SCALE+BASE+off8] FF 54 xx xx
CALL dword ptr [EBP+xx] FF 55 xx
CALL dword ptr [ESI+xx] FF 56 xx
CALL dword ptr [EDI+xx] FF 57 xx
CALL dword ptr [EAX+xxxxxxxx] FF 90 xx xx xx xx
CALL dword ptr [ECX+xxxxxxxx] FF 91 xx xx xx xx
CALL dword ptr [EDX+xxxxxxxx] FF 92 xx xx xx xx
CALL dword ptr [EBX+xxxxxxxx] FF 93 xx xx xx xx
CALL dword ptr [REG*SCALE+BASE+off32] FF 94 xx xx xx xx xx
CALL dword ptr [EBP+xxxxxxxx] FF 95 xx xx xx xx
CALL dword ptr [ESI+xxxxxxxx] FF 96 xx xx xx xx
CALL dword ptr [EDI+xxxxxxxx] FF 97 xx xx xx xx
CALL EAX FF D0
CALL ECX FF D1
CALL EDX FF D2
CALL EBX FF D3
CALL ESP FF D4
CALL EBP FF D5
CALL ESI FF D6
CALL EDI FF D7
CALL FAR seg16:abs32 9A xx xx xx xx xx xx

有了这个列表,写一段代码来完成最初我们的需求也就不难了。

标签: ,

[翻译] [RabbitMQ+Python入门经典] 兔子和兔子窝

2010年3月14日 14 条评论

RabbitMQ作为一个工业级的消息队列服务器,在其客户端手册列表的Python段当中推荐了一篇blog,作为RabbitMQ+Python的入门手册再合适不过了。不过,正如其标题Rabbit and Warrens(兔子和养兔场)一样,这篇英文写的相当俏皮,以至于对于我等非英文读者来说不像一般的技术文档那么好懂,所以,翻译一下吧。翻译过了,希望其他人可以少用一些时间。翻译水平有限,不可能像原文一样俏皮,部分地方可能就意译了,希望以容易懂为准。想看看老外的幽默的,推荐去看原文,其实,也不是那么难理解……

原文:http://blogs.digitar.com/jjww/2009/01/rabbits-and-warrens/

兔子和兔子窝

当时我们的动机很简单:从生产环境的电子邮件处理流程当中分支出一个特定的离线分析流程。我们开始用的MySQL,将要处理的东西放在表里面,另一个程序从中取。不过很快,这种设计的丑陋之处就显现出来了…… 你想要多个程序从一个队列当中取数据来处理?没问题,我们硬编码程序的个数好了……什么?还要能够允许程序动态地增加和减少的时候动态进行压力分配?

是的,当年我们想的简单的东西(做一个分支处理)逐渐变成了一个棘手的问题。以前拿着锤子(MySQL)看所有东西都是钉子(表)的年代是多么美好……

在搜索了一下之后,我们走进了消息队列(message queue)的大门。不不,我们当然知道消息队列是什么,我们可是以做电子邮件程序谋生的。我们实现过各种各样的专业的,高速的内存队列用来做电子邮件处理。我们不知道的是那一大类现成的、通用的消息队列(MQ)服务器——无论是用什么语言写出的,不需要复杂的装配的,可以自然的在网络上的应用程序之间传送数据的一类程序。不用我们自己写?看看再说。

让大家看看你们的Queue吧……

过去的4年里,人们写了有好多好多的开源的MQ服务器啊。其中大多数都是某公司例如LiveJournal写出来用来解决特定问题的。它们的确不关心上面跑的是什么类型的消息,不过他们的设计思想通常是和创建者息息相关的(消息的持久化,崩溃恢复等通常不在他们考虑范围内)。不过,有三个专门设计用来做及其灵活的消息队列的程序值得关注:

Apache ActiveMQ 曝光率最高,不过看起来它有些问题,可能会造成丢消息。不可接受,下一个。

ZeroMQ 和 RabbitMQ 都支持一个开源的消息协议,成为AMQP。AMQP的一个优点是它是一个灵活和开放的协议,以便和另外两个商业化的Message Queue (IBM和Tibco)竞争,很好。不过ZeroMQ不支持消息持久化和崩溃恢复,不太好。剩下的只有RabbitMQ了。如果你不在意消息持久化和崩溃恢复,试试ZeroMQ吧,延迟很低,而且支持灵活的拓扑。

剩下的只有这个吃胡萝卜的家伙了……

当我读到它是用Erlang写的时候,RabbitMQ震了我一下。Erlang 是爱立信开发的高度并行的语言,用来跑在电话交换机上。是的,那些要求6个9的在线时间的东西。在Erlang当中,充斥着大量轻量进程,它们之间用消息传递来通信。听起来思路和我们用消息队列的思路是一样的,不是么?

而且,RabbitMQ支持持久化。是的,如果RabbitMQ死掉了,消息并不会丢失,当队列重启,一切都会回来。而且,正如在DigiTar(注:原文作者的公司)做事情期望的那样,它可以和Python无缝结合。除此之外,RabbitMQ的文档相当的……恐怖。如果你懂AMQP,这些文档还好,但是有多少人懂AMQP?这些文档就像MySQL的文档假设你已经懂了SQL一样……不过没关系啦。

好了,废话少说。这里是花了一周时间阅读关于AMQP和关于它如何在RabbitMQ上工作的文档之后的一个总结,还有,怎么在Python当中使用。

开始吧

AMQP当中有四个概念非常重要:虚拟主机(virtual host),交换机(exchange),队列(queue)和绑定(binding)。一个虚拟主机持有一组交换机、队列和绑定。为什么需要多个虚拟主机呢?很简单,RabbitMQ当中,用户只能在虚拟主机的粒度进行权限控制。因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个RabbitMQ服务器都有一个默认的虚拟主机“/”。如果这就够了,那现在就可以开始了。

交换机,队列,还有绑定……天哪!

刚开始我思维的列车就是在这里脱轨的…… 这些鬼东西怎么结合起来的?

队列(Queues)是你的消息(messages)的终点,可以理解成装消息的容器。消息就一直在里面,直到有客户端(也就是消费者,Consumer)连接到这个队列并且将其取走为止。不过。你可以将一个队列配置成这样的:一旦消息进入这个队列,biu~,它就烟消云散了。这个有点跑题了……

需要记住的是,队列是由消费者(Consumer)通过程序建立的,不是通过配置文件或者命令行工具。这没什么问题,如果一个消费者试图创建一个已经存在的队列,RabbitMQ就会起来拍拍他的脑袋,笑一笑,然后忽略这个请求。因此你可以将消息队列的配置写在应用程序的代码里面。这个概念不错。

OK,你已经创建并且连接到了你的队列,你的消费者程序正在百无聊赖的敲着手指等待消息的到来,敲啊,敲啊…… 没有消息。发生了什么?你当然需要先把一个消息放进队列才行。不过要做这个,你需要一个交换机(Exchange)……

交换机可以理解成具有路由表的路由程序,仅此而已。每个消息都有一个称为路由键(routing key)的属性,就是一个简单的字符串。交换机当中有一系列的绑定(binding),即路由规则(routes),例如,指明具有路由键 “X” 的消息要到名为timbuku的队列当中去。先不讨论这个,我们有点超前了。

你的消费者程序要负责创建你的交换机(复数)。啥?你是说你可以有多个交换机?是的,这个可以有,不过为啥?很简单,每个交换机在自己独立的进程当中执行,因此增加多个交换机就是增加多个进程,可以充分利用服务器上的CPU核以便达到更高的效率。例如,在一个8核的服务器上,可以创建5个交换机来用5个核,另外3个核留下来做消息处理。类似的,在RabbitMQ的集群当中,你可以用类似的思路来扩展交换机一边获取更高的吞吐量。

OK,你已经创建了一个交换机。但是他并不知道要把消息送到哪个队列。你需要路由规则,即绑定(binding)。一个绑定就是一个类似这样的规则:将交换机“desert(沙漠)”当中具有路由键“阿里巴巴”的消息送到队列“hideout(山洞)”里面去。换句话说,一个绑定就是一个基于路由键将交换机和队列连接起来的路由规则。例如,具有路由键“audit”的消息需要被送到两个队列,“log-forever”和“alert-the-big-dude”。要做到这个,就需要创建两个绑定,每个都连接一个交换机和一个队列,两者都是由“audit”路由键触发。在这种情况下,交换机会复制一份消息并且把它们分别发送到两个队列当中。交换机不过就是一个由绑定构成的路由表。

现在复杂的东西来了:交换机有多种类型。他们都是做路由的,不过接受不同类型的绑定。为什么不创建一种交换机来处理所有类型的路由规则呢?因为每种规则用来做匹配分子的CPU开销是不同的。例如,一个“topic”类型的交换机试图将消息的路由键与类似“dogs.*”的模式进行匹配。匹配这种末端的通配符比直接将路由键与“dogs”比较(“direct”类型的交换机)要消耗更多的CPU。如果你不需要“topic”类型的交换机带来的灵活性,你可以通过使用“direct”类型的交换机获取更高的处理效率。那么有哪些类型,他们又是怎么处理的呢?

Fanout Exchange – 不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。

Direct Exchange – 处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog

Topic Exchange – 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”。我在RedHat的朋友做了一张不错的图,来表明topic交换机是如何工作的:

Source: Red Hat Messaging Tutorial: 1.3 Topic Exchange

持久化这些小东西们

你花了大量的时间来创建队列、交换机和绑定,然后,砰~服务器程序挂了。你的队列、交换机和绑定怎么样了?还有,放在队列里面但是尚未处理的消息们呢?

放松~如果你是用默认参数构造的这一切的话,那么,他们,都,biu~,灰飞烟灭了。是的,RabbitMQ重启之后会干净的像个新生儿。你必须重做所有的一切,亡羊补牢,如何避免将来再度发生此类杯具?

队列和交换机有一个创建时候指定的标志durable,直译叫做坚固的。durable的唯一含义就是具有这个标志的队列和交换机会在重启之后重新建立,它不表示说在队列当中的消息会在重启后恢复。那么如何才能做到不只是队列和交换机,还有消息都是持久的呢?

但是首先一个问题是,你真的需要消息是持久的吗?对于一个需要在重启之后回复的消息来说,它需要被写入到磁盘上,而即使是最简单的磁盘操作也是要消耗时间的。如果和消息的内容相比,你更看重的是消息处理的速度,那么不要使用持久化的消息。不过对于我们@DigiTar来说,持久化很重要。

当你将消息发布到交换机的时候,可以指定一个标志“Delivery Mode”(投递模式)。根据你使用的AMQP的库不同,指定这个标志的方法可能不太一样(我们后面会讨论如何用Python搞定)。简单的说,就是将Delivery Mode设置成2,也就是持久的(persistent)即可。一般的AMQP库都是将Delivery Mode设置成1,也就是非持久的。所以要持久化消息的步骤如下:

  1. 将交换机设成 durable。
  2. 将队列设成 durable。
  3. 将消息的 Delivery Mode 设置成2 。

就这样,不是很复杂,起码没有造火箭复杂,不过也有可能犯点小错误。

下面还要罗嗦一个东西……绑定(Bindings)怎么办?我们无法在创建绑定的时候设置成durable。没问题,如果你绑定了一个durable的队列和一个durable的交换机,RabbitMQ会自动保留这个绑定。类似的,如果删除了某个队列或交换机(无论是不是durable),依赖它的绑定都会自动删除。

注意两点:

  • RabbitMQ 不允许你绑定一个非坚固(non-durable)的交换机和一个durable的队列。反之亦然。要想成功必须队列和交换机都是durable的。
  • 一旦创建了队列和交换机,就不能修改其标志了。例如,如果创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重现创建。因此,最好仔细检查创建的标志。

开始喂蛇了~

【译注】说喂蛇是因为Python的图标是条蛇。

AMQP的一个空白地带是如何在Python当中使用。对于其他语言有一大坨材料。

但是对Python老兄来说,你需要花点时间来挖掘一下。所以我写了这个,这样别的家伙们就不需要经历我这种抓狂的过程了。

首先,我们需要一个Python的AMQP库。有两个可选:

根据你的需求,py-amqplib或者txAMQP都是可以的。因为是基于Twisted的,txAMQP可以保证用异步IO构建超高性能的AMQP程序。但是Twisted编程本身就是一个很大的主题……因此清晰起见,我们打算用 py-amqplib。更新:请参见Esteve Fernandez关于txAMQP的使用和代码样例的回复

AMQP支持在一个TCP连接上启用多个MQ通信channel,每个channel都可以被应用作为通信流。每个AMQP程序至少要有一个连接和一个channel。

from amqplib import client_0_8 as amqp
conn = amqp.Connection(host="localhost:5672 ", userid="guest",
password="guest", virtual_host="/", insist=False)
chan = conn.channel()

每个channel都被分配了一个整数标识,自动由Connection()类的.channel()方法维护。或者,你可以使用.channel(x)来指定channel标识,其中x是你想要使用的channel标识。通常情况下,推荐使用.channel()方法来自动分配channel标识,以便防止冲突。

现在我们已经有了一个可以用的连接和channel。现在,我们的代码将分成两个应用,生产者(producer)和消费者(consumer)。我们先创建一个消费者程序,他会创建一个叫做“po_box”的队列和一个叫“sorting_room”的交换机:

chan.queue_declare(queue="po_box", durable=True,
exclusive=False, auto_delete=False)
chan.exchange_declare(exchange="sorting_room", type="direct", durable=True,
auto_delete=False,)

这段代码干了啥?首先,它创建了一个名叫“po_box”的队列,它是durable的(重启之后会重新建立),并且最后一个消费者断开的时候不会自动删除(auto_delete=False)。在创建durable的队列(或者交换机)的时候,将auto_delete设置成false是很重要的,否则队列将会在最后一个消费者断开的时候消失,与durable与否无关。如果将durable和auto_delete都设置成True,只有尚有消费者活动的队列可以在RabbitMQ意外崩溃的时候自动恢复。

(你可以注意到了另一个标志,称为“exclusive”。如果设置成True,只有创建这个队列的消费者程序才允许连接到该队列。这种队列对于这个消费者程序是私有的)。

还有另一个交换机声明,创建了一个名字叫“sorting_room”的交换机。auto_delete和durable的含义和队列是一样的。但是,.excange_declare() 还有另外一个参数叫做type,用来指定要创建的交换机的类型(如前面列出的): fanout, directtopic.

到此为止,你已经有了一个可以接收消息的队列和一个可以发送消息的交换机。不过我们需要创建一个绑定,把它们连接起来。

chan.queue_bind(queue=”po_box”, exchange=”sorting_room”,
routing_key=”jason”)

这个绑定的过程非常直接。任何送到交换机“sorting_room”的具有路由键“jason” 的消息都被路由到名为“po_box” 的队列。

现在,你有两种方法从队列当中取出消息。第一个是调用chan.basic_get(),主动从队列当中拉出下一个消息(如果队列当中没有消息,chan.basic_get()会返回None, 因此下面代码当中print msg.body 会在没有消息的时候崩掉):

msg = chan.basic_get("po_box")
print msg.body
chan.basic_ack(msg.delivery_tag)

但是如果你想要应用程序在消息到达的时候立即得到通知怎么办?这种情况下不能使用chan.basic_get(),你需要用chan.basic_consume()注册一个新消息到达的回调。

def recv_callback(msg):
    print 'Received: ' + msg.body
chan.basic_consume(queue='po_box', no_ack=True,
callback=recv_callback, consumer_tag="testtag")
while True:
    chan.wait()
chan.basic_cancel("testtag")

chan.wait() 放在一个无限循环里面,这个函数会等待在队列上,直到下一个消息到达队列。chan.basic_cancel() 用来注销该回调函数。参数consumer_tag 当中指定的字符串和chan.basic_consume() 注册的一直。在这个例子当中chan.basic_cancel() 不会被调用到,因为上面是个无限循环…… 不过你需要知道这个调用,所以我把它放在了代码里。

需要注意的另一个东西是no_ack参数。这个参数可以传给chan.basic_get()chan.basic_consume(),默认是false。当从队列当中取出一个消息的时候,RabbitMQ需要应用显式地回馈说已经获取到了该消息。如果一段时间内不回馈,RabbitMQ会将该消息重新分配给另外一个绑定在该队列上的消费者。另一种情况是消费者断开连接,但是获取到的消息没有回馈,则RabbitMQ同样重新分配。如果将no_ack 参数设置为true,则py-amqplib会为下一个AMQP请求添加一个no_ack属性,告诉AMQP服务器不需要等待回馈。但是,大多数时候,你也许想要自己手工发送回馈,例如,需要在回馈之前将消息存入数据库。回馈通常是通过调用chan.basic_ack()方法,使用消息的delivery_tag属性作为参数。参见chan.basic_get() 的实例代码。

好了,这就是消费者的全部代码。(下载:amqp_consumer.py

不过没有人发送消息的话,要消费者何用?所以需要一个生产者。下面的代码示例表明如何将一个简单消息发送到交换区“sorting_room”,并且标记为路由键“jason” :

msg = amqp.Message("Test message!")
msg.properties["delivery_mode"] = 2
chan.basic_publish(msg,exchange="sorting_room",routing_key="jason")

你也许注意到我们设置消息的delivery_mode属性为2,因为队列和交换机都设置为durable的,这个设置将保证消息能够持久化,也就是说,当它还没有送达消费者之前如果RabbitMQ重启则它能够被恢复。

剩下的最后一件事情(生产者和消费者都需要调用的)是关闭channel和连接:

chan.close()
conn.close()

很简单吧。(下载:amqp_publisher.py

来真实地跑一下吧……

现在我们已经写好了生产者和消费者,让他们跑起来吧。假设你的RabbitMQ在localhost上安装并且运行。

打开一个终端,执行python ./amqp_consumer.py让消费者运行,并且创建队列、交换机和绑定。

然后在另一个终端运行python ./amqp_publisher.py “AMQP rocks.” 。如果一切良好,你应该能够在第一个终端看到输出的消息。

付诸使用吧

我知道这个教程是非常粗浅的关于AMQP/RabbitMQ和如何使用Python访问的教程。希望这个可以说明所有的概念如何在Python当中被组合起来。如果你发现任何错误,请联系原作者(williamsjj@digitar.com) 【译注:如果是翻译问题请联系译者】。同时,我很高兴回答我知道的问题。【译注:译者也是一样的】。接下来是,集群化(clustering)!不过我需要先把它弄懂再说。

注:关于RabbitMQ的知识我主要来自这些来源,推荐阅读:

–完–

西厢计划

2010年3月10日 没有评论

今天在twitter上看到的@gfwrev成果,向伟大的翻墙先行者致敬。

作为个搞技术的人,我们要干点疯狂的事。如果我们不动手,我们就要被比我们差的远的坏技术人员欺负。这太丢人了。眼前就是,GFW这个东西,之前是我们不抱团,让它猖狂了。现在咱们得凑一起,想出来一个办法让它郁闷一下,不能老被欺负吧。要不,等到未来,后代会嘲笑我们这些没用的家伙,就象我们说别人“你怎么不反抗?”

这就是2008年7月tek4小组建立时候的宣言。

今天,西厢计划开始了。干的漂亮。

标签:

你的Google搜索A-Z是什么?

2010年2月5日 2 条评论

今天看到一个帖子原文),尝试用各个搜索引擎搜索A-Z的字母,看看出来的结果,用来评价公司的有名程度。不过一个说法是Google会根据用户的习惯对搜索引擎的排序进行优化,所以可能每个人看到的顺序并不一样,所以我自己来试一下,果然很不一样。

下面这个表是原文的帖子当中加上的一列,看看我的Google搜索习惯和大众到底有多大差别。

字母 baidu.com google.cn google.com 我的google.com
a Acfun(视频网站) Adobe Reader Amazon(亚马逊) Apache Foundation
www.apache.org/
b baidu baidu Best Buy(百思百) boards.4chan.org/b/
这个这个,很不和谐……
c cf(腾讯游戏) CCTV(谷歌更爱国 :-)) Craigslist C编程语言
en.wikipedia.org/wiki/C_(programming_language)
d dnf(腾讯游戏) dnf Dictionary.com D编程语言
digitalmars.com/d/
e ems(邮政) ems(邮政) eBay www.eonline.com
这是啥?
f flash facebook Facebook 福特汽车的股票行情
finance.yahoo.com/q?s=F
g google google earth Gmail Gmail
h hao123 hao123 Hotmail H&M
www.hm.com/us/
i is IP地址查询 IMDB(电影资料) Apple iTunes!!!!
www.apple.com/itunes/
j java java J.C. Penny(百货商店) J编程语言
www.jsoftware.com
k kaixin(开心网) kugou(酷狗) Kohl’s(百货商店) www.krecs.com
l lady gaga lv Lowe’s(装修商店) www.chicago-l.org/
芝加哥快速交通
m msn msn MySpace www.myspace.com/mward
n nba nba Netflix(DVD出租) www.metanetsoftware.com/
o oppo office 2003 Office Depot(装修商店) www.cirquedusoleil.com/en/shows/o/default.aspx
又是一个不知道是什么的东东……
p pps(网络电视) pps Pandora(音乐) twitter.com/p
paolo i. (p) on Twitter!!!!
q qq 空间 qq QVC(网上零售) www.kju-app.org/
又是一个不知道做什么的网站……
r 人人(人人网) real player Realtor.com(房地产网站) www.r-project.org/
又是一个编程语言……

s sina(新浪) sina Southwest airline(航空) www.macys.com/
美国著名百货商店
t taobao(淘宝) taobao Target(百货商店) www.mbta.com/
这是啥??
u u9 ut US Post service(邮局) www.utah.edu
University of Utah (U of U)
v vs verycd Verizon Wireless(电信) abc.go.com/shows/v
软件?
w wow(魔兽世界) www.baidu.com(谷歌这儿是百度,百度那儿倒不是) Wal-Mart(沃尔玛) www.imdb.com/title/tt1175491/
电影W
x xiaonei xiaonei Xbox 360(微软游戏机) www.x.org/
著名开源软件XWindow
y yy yahoo Youtube yahoo
z zol(中关村在线) zol Zillow(房地产网站) www.cpuid.com/cpuz.php
CPU-Z软件

一片狼藉啊。

标签:

克林顿国务卿关于互联网自由的讲话

2010年1月22日 没有评论

本篇纯转载,读后感另开文章。 英文原文 voa中文翻译 网友小米翻译版

这里转载的是VOA的翻译版

克林顿国务卿关于互联网自由的讲话

希拉里∙克林顿(Hillary Rodham Clinton)国务卿
华盛顿哥伦比亚特区新闻博物馆(Newseum)
2009年1月21日(星期四)

非常感谢,艾伯托(Alberto)。不仅要感谢你的赞誉和介绍,而且要感谢你和你的同事们在这个重要机构中发挥的领导作用。很高兴来到新闻博物馆。这个博物馆是一座纪念碑,见证了我们最珍视的一些自由。我十分感谢能有此机会谈谈如何运用这些自由应对二十一世纪的各项挑战。

虽然我并不能看到你们所有的人——因为在这样的场合灯光照射我的眼睛,而你们都在背光处——但我知道在座的有很多朋友和老同事。我要感谢自由论坛(Freedom Forum)的首席执行官查尔斯∙奥弗比(Charles Overby)光临新闻博物馆,以及我在参议院时的老同事理查德∙卢格(Richard Lugar)和乔∙利伯曼(Joe Lieberman)两位参议员,他们两位都为《表达法》(Voice Act)的通过作出了努力。这项立法表明,美国国会和美国人民不分党派,不分政府部门,坚定地支持互联网自由。

我听说在场的还有参议员萨姆∙布朗巴克(Sam Brownback)、参议员特德∙考夫曼(Ted Kaufman)、众议员洛雷塔∙桑切斯(Loretta Sanchez)、许多大使、临时代办和外交使团的其他代表、以及从中国、哥伦比亚、伊朗、黎巴嫩和摩尔多瓦等国前来参加我们关于互联网自由的“国际访问者领袖计划”(International Visitor Leadership Program)的人士。我还要提到最近被任命为广播理事会(Broadcasting Board of Govenors)理事的阿斯彭研究所(Aspen Institute)所长沃尔特∙艾萨克森( Walter Isaacson)。毫无疑问,他在阿斯彭研究所从事的支持互联网自由的工作中发挥了重要作用。

这 是关于一个非常重要的议题的一个重要讲话。但在开始谈这个议题前,我想简要介绍一下海地的情况。过去八天来,海地人民和世界人民携手应对一场巨大的灾难。 我们这个半球曾历经磨难,但我们目前在太子港面临的困境鲜有先例。通讯网络在我们抗击这场灾难的过程中发挥了极其重要的作用。不用说,当地的通讯网络遭受 了重创,在很多地方被彻底摧毁。地震发生后仅几个小时,我们就与民营部门的伙伴发起“海地”(HAITI)短信捐款活动,使美国的移动电话使用者能通过发短信向救灾工作捐款。这项活动充分展示了美国人民的慷慨。迄今,该活动已为海地的抗震救灾筹集了2500多万美元。

信息网络在救灾现场也发挥了极其重要的作用。星期六,我在太子港会见普雷瓦尔(Preval) 总统时,他的重点目标之一是要努力恢复通讯。幸存的通讯设施不足以帮助当地政府官员相互联络,非政府组织以及我们的文职部门和军队的领导人的运作能力都受 到严重影响。高科技公司设立了互动地图,帮助确定救灾需要和目标资源。就在星期一,一名年仅七岁的小女孩和两名妇女通过发短信呼救被一个美国搜救队从坍塌 的超市的残砖碎瓦下救了出来。这些事例只是一个普遍现象的缩影。

信 息网络的扩展正在为我们的星球建立一个新的神经系统。在海地或湖南发生什么情况时,我们其余的人都能从当事者那里实时得知。我们还可以实时作出反应。灾后 迫切希望提供帮助的美国人和被困在超市瓦砾下的小姑娘以一年以前乃至一代人以前还想象不到的方式被联系在一起。今天,同样的原则适用于几乎整个人类。我们 今天坐在这里,你们中间任何人——或更有可能的是我们孩子中的任何人——都可以拿出很多人每天随身携带的通讯工具,将这次讨论的内容发送给全世界数十亿 人。

在很多方面,信息从未像今天这么自由。与过去任何时候相比,今天都有更多的方式把更多的想法传播给更多的人。即使在集权国家,信息网络也在帮助人们发现新的事实,向政府更多地问责。

例如,奥巴马总统11月 访华期间与当地大学生的直接对话包含了网上提问,突显了互联网的重要性。在回答一个网上提问时,他强调人民有权自由获取信息。他说,信息流通越自由,社会 就越强健。他谈到获取信息的权力如何有助于公民向自己的政府问责,激发新的想法,鼓励创造性和创业精神。我今天来这里发表讲话正是出于美国对这一经过实践 检验的真理的信念。

由 于人们的相互联系空前密切,我们也必须认识到这些新技术并非无条件地造福人类。这些工具也正被用于阻碍人类进步和剥夺政治权利。正如钢可被用于建造医院也 可用于制造机枪。核能可为城市提供动力也可摧毁城市。现代信息网络及其支持的技术既可被用于行善也可被用于作恶。有助于组织自由运动的网络也能使“基地” 组织得以煽动仇恨,挑起针对无辜者的暴力。具有开放政府信息和促进透明化潜力的技术也可被政府劫持,用于镇压异见,剥夺公民权利。

过去一年来,我们看到对信息自由流通的威胁激增。中国、突尼斯和乌兹别克斯坦加强了对互联网的审查。在越南,使用广受欢迎的社交网站的权利突然消失。上个星期五在埃及,30名博客作者和维权人士被拘留。这批博客作者中的一位是巴塞姆∙萨米尔(Bassem Samir)。他有幸获释,今天也在这里,同我们在一起。因此,一方面,这些技术的推广明显地正在改变我们的世界,另一方面,尚无法预知这样的改变将对世界人民的人权和幸福产生何种影响。

这 些新技术本身不会在争取自由与进步的斗争中选择立场。但是,美国要做到立场鲜明。我们支持一个允许全人类平等享有知识和思想的互联网。而且我们认识到,在 世界上建立何种信息基础设施将取决于我们和其他人为之确定的性质。虽然这是一个全新的挑战,但我们确保思想自由交流的责任可追溯至和众国诞生之初。《宪 法》第一修正案的内容字字镌刻在这座大楼前那块50吨重的田纳西大理石上。世世代代的美国人都为捍卫刻在那块石头上的价值观付出了努力。

富兰克林•罗斯福(Franklin Roosevelt)在1941年发表“四项自由”演讲时发扬了这些思想。当时,美国人面临着一系列的危机,此外还有信心危机。但是,对一个人人都享有言论表达自由、信仰自由、没有贫困、没有恐惧的世界的憧憬冲破了他那个时代的重重困难。多年之后,我的楷模之一艾琳娜•罗斯福(Eleanor Roosevelt)努力使这些原则成为《世界人权宣言》的奠基原则。这些原则成为继往开来每一代人的北斗,引导我们、鞭策我们、促使我们在险恶的环境中勇于向前。

在 科学技术飞跃发展的时候,我们必须反思这个传统。我们需要确保科学技术的进步与我们的原则同步。在接受诺贝尔奖时,奥巴马总统讲到需要建设这样一个世界, 让和平建立在每一个人固有的权利和尊严之上。几天后在乔治敦大学关于人权的演讲中,我表示我们必须探索途径,把人权变成现实。今天,我们迫切需要在二十一 世纪的电子世界中保护这些自由。

世 界上有许多其他的网络,有些帮助人员或资源的流动,有些辅助志同道合的个人之间的交流。但互联网是增强所有其他网络的能力和潜力的一个网络,因此,我们认 为确保其使用者享有某些基本自由至关重要。其中最重要的是言论表达自由。这种自由的定义不再仅仅是公民前往市政厅前的广场批评他们的政府,而不担心遭受报 复。博客、电子邮件、社交网络和手机短信开启了交流思想的新途径,也为信息审查带来了新目标。

甚至就在我今天向你们讲演的此刻,某些地方的政府审查人员正在竭力将我的话语从历史的记录中删除。但历史早已作出裁决:这些手法注定失败。两个月前,我在德国参加了推倒柏林墙20周年纪念活动。参加这次活动的各国领导人向这个屏障对面那些英勇的男女志士表示敬意,他们曾经通过散发被称为“地下刊物”(Samizdat)的小册子来阐明反对压迫的道理。这些传单对“东方集团”专制政权的宣传和用心提出了质疑。许多人因散发传单受到残酷迫害,但他们的声音帮助穿透了“铁幕”的钢筋水泥和带刺的铁丝网。

柏林墙象征着一个分隔的世界,代表一个时代。今天,这堵墙的一些碎片就陈列在这座它们理应归属的博物馆里。在我们这个时代,具有代表性的基础设施就是互联网。它取代了分隔,象征着联系。但是,就在网络扩展到世界各国的同时,我们发现许多地方以虚拟的墙壁代替了有形的墙壁。

有 些国家竖起了电子屏障,阻止本国人民分享世界上的一部分网络。他们从搜索引擎提供的结果中删除字词、名称和短语。他们侵犯了那些发表非暴力政治言论的人的 隐私权。这些做法违反了《世界人权宣言》,因为《宣言》告诉我们,人人都有权通过“各种媒体不受疆界限制地寻求、接收和传播信息和思想”。由于这些限制手 段的蔓延,一个新的信息帷幕正在世界上许多地方降临。为穿越这种阻隔,个人视频和博客文章正成为当今时代的“地下刊物”。

正 如过去的专制政权一样,有些政府正在打击那些利用这些工具的独立思考者。在伊朗总统大选后的游行示威期间,用手机拍摄的一位年轻女子遭血腥屠杀的斑驳画面 成为通过数字技术对该政府暴行提出的控诉。我们已看到有报道说,当生活在海外的伊朗人在网上张贴对他们国家领导人的批评时,他们在伊朗的家人便成为报复的 目标。尽管政府普遍采取严厉的恐吓手段,但伊朗英勇的公民记者们继续利用技术向全世界及其同胞报道他们国内发生的事件。伊朗人民为自身的人权呐喊,同时也 鼓舞了全世界,他们的勇气正在重新诠释如何通过技术传播真理和揭露非正义现象。

所 有的社会都承认言论自由有其限度。我们不能容忍煽动他人从事暴力的人,例如此刻正利用互联网在全世界宣扬大规模屠杀无辜百姓的“基地”组织成员。那些以种 族、宗教、族裔、性别或性取向为由攻击他人的仇恨言论也应受到严厉斥责。遗憾的是,这些问题均构成日益严重的挑战,国际社会必须共同进行抗击。我们还必须 解决匿名发表言论的问题。对于那些利用互联网招收恐怖主义分子或传播被盗窃的知识产权的人,不能让他们将其网络行为与其真实身份脱钩。然而,对于那些为了 和平的政治目的利用互联网的人士,这些并不能成为政府有计划地侵犯他们的权利和隐私的托辞。

随 着新技术的传播,言论自由可能是最明显会遇到各种挑战的一项自由权利,但并非仅此而已。信仰自由通常涉及个人与造物主对话或不对话的权利。这是一种不需依 赖技术的交流方式。然而,信仰自由还体现了与拥有共同价值观和人生观的人一起集会的普遍权利。在我们的历史中,这类集会常见于教堂、犹太会堂、清真寺和寺 庙。今天,这类集会也可能在网上进行。

互 联网有助于不同信仰的人消除相互间的分歧。正如总统在开罗所说,宗教自由对于人们能否共同生活至关重要。在我们寻求扩大对话之际,互联网蕴涵着巨大的希 望。我们已开始使美国学生与全世界穆斯林社会的年青人为讨论全球性挑战相互联络。我们将继续利用这个工具,支持不同宗教社群的个人相互讨论。

然 而,某些国家则利用互联网打击和压制宗教人士。例如,去年在沙特阿拉伯,一名男子因在博客上刊登介绍基督教的文章,被捕入狱达数月之久。哈佛大学一项调查 表明,沙特政府封锁了许多介绍印度教、犹太教、基督教乃至伊斯兰教的网页。包括越南和中国在内的一些国家也利用类似手段限制获得宗教信息的途径。

这 些技术不得用于惩罚和平的政治言论,同样也不可用于迫害或压制宗教少数派。祈祷往往在更高层次的网络进行。然而,互联网和社交网站等通讯技术应该有助于提 高人们根据自己的需要进行祈祷的能力,以及与拥有共同信仰的人集会和更多地了解其他人信仰的能力。正如我们促进其他生活领域的自由一样,我们也必须努力促 进在网络上祈祷的自由。

当 然,还有无数人的生活并没享受到这些技术带来的益处。在我们的世界里,正如我多次指出的,才智有可能普及众人,但机会并非如此。从长期获得的经验来看,我 们知道,在人民缺乏途径获得知识、市场、资本和机会的国家,要促进社会和经济发展会十分艰难,有时则徒劳无功。在这种情况下,互联网可发挥调节器的作用。 通过向人们提供获得知识和潜在市场的途径,各种网络可为那些缺乏机会的地区创造机会。

在过去一年中,我在肯尼亚亲眼目睹了这种情况。那里的农牧民在开始使用移动银行技术后,收入提高了多达30%。在孟加拉,30多万人报名通过手机学习英语。在非洲撒哈拉沙漠以南地区,妇女企业家使用互联网获得小型贷款并与全球市场接轨。

世界上经济地位最低的亿万人民有可能在生活中效仿上述取得进步的实例。在很多情况下,互联网、手机和其他通讯技术能对经济发展起到绿色革命(Green Revolution)对农业所起的同等作用。现在,小小的投入便能产生巨大效益。世界银行的一项研究显示,在一个典型的发展中国家,手机普及率每增加10%,人均国内生产总值便能增长将近1%。具体而言,如果以印度为例,那将相当于每年近100亿美元。

与全球信息网络连通就好比踏上了通往现代化的阶梯。在这些技术问世的最初几年,许多人以为它们将在世界上的富人和穷人之间划出鸿沟,但那种情况并没有发生。今天共有40亿只手机在使用。手机使用者中有很多是小贩、人力车夫和其他历来缺乏受教育及其他机会的人。信息网络是实现平等的有力手段,我们应共同使用这些技术帮助人们摆脱贫困,不再有匮乏之虞。

我 们完全有理由满怀希望:当人们充分利用信息网络和通讯技术时,他们将能取得巨大进步。但毫无疑问,也有些人正在利用全球信息网络实现其阴暗目的,而且将继 续这样做。暴力极端主义分子、犯罪集团、性犯罪者和独裁政府都妄图对全球网络加以利用。正如恐怖主义分子利用我们社会的开放性趁机实施阴谋,暴力极端主义 分子也要利用互联网进行煽动和恐吓。当我们努力增进这些自由时,我们也必须打击妄图利用通讯网络进行破坏并制造恐惧的人。

各国政府和公民必须保持信心,作为国家安全和经济繁荣核心环节的网络是安全且有韧性的。这不仅仅是几个小黑客污损几个网站的问题,如果我们的信息网络安全得不到保障,我们的网上银行业务、电子商务活动以及保护亿万美元知识产权的能力就全都岌岌可危。

面 对破坏这些系统的活动,各国政府、民营部门和国际社会必须协调一致地采取行动。当黑客犯罪分子和有组织犯罪集团为非法牟利攻击网络时,我们需要更多的工具 帮助执法机构进行跨辖区的合作。儿童色情以及遭到贩运的妇女和女童所受的剥削通过互联网为整个世界所见并为剥削者借以牟利,对这种社会弊病也应采取同样的 应对措施。欧洲理事会在网络犯罪公约(Convention on Cybercrime )方面的努力及其他方的类似努力促成了对此类犯罪起诉的国际协作,我们对此表示赞赏。我们还希望为此加倍努力。

我 国政府及国务院已经采取措施寻求通过外交方式来加强全球网络安全。国务院有大批人员从事这项工作。有关人员一直在协同努力。我们还在两年前设立了一个专门 协调有关网络的对外政策的办公室。我们致力于在联合国和其他多边论坛应对这一挑战,并把网络安全问题列入世界性议题。奥巴马总统刚刚任命了一位新的国家网 络政策协调员,来帮助我们更紧密地协调工作,以确保每个人的网络都是自由、安全和可靠的。

某 些国家、恐怖主义分子以及他们的代理人必须明白,美国将保护我们的网络系统。那些在我们国家或任何其他国家破坏信息自由流通的人对我们的经济、我们的政府 和我们的公民社会构成了威胁。从事网络攻击的国家和个人将承担后果并受到国际社会的谴责。在一个靠互联网连通的世界里,对一个国家的网络的攻击就是对所有 人的攻击。通过强调这一点,我们可以在国家间建立行为准则,并鼓励尊重全球网民。

最 后一项自由或许是罗斯福总统与夫人多年前所思考和论述的自由的必然内含,它源于我前面已提到的四项自由,这就是连接自由:政府不应阻止人民与互联网、与网 站或与彼此连接。连接自由如同集会自由一样,只不过它是在网络空间。这一自由允许个人上网,聚集,希望还有合作。一旦上网,你不必是大亨或摇滚乐明星便能 对社会产生巨大影响。

对孟买恐怖主义袭击的最大规模的公众反应是由一位13岁少年发起的。他使用社交网络组织了献血运动,并建立了一个大型跨宗教信仰的吊唁簿。在哥伦比亚,一位失业的工程师召集起全世界190个城市的1200万人,向哥伦比亚革命武装力量(FARC)的恐怖活动发出抗议。这些抗议是历史上规模最大的反恐怖主义示威活动。在随后几个星期中,哥伦比亚革命武装力量经历了十年军事行动中人数最多的弃甲和脱队事件。在墨西哥,一位对毒品暴力行径忍无可忍的公民发出的一份电子邮件像滚雪球一般发展成遍及该国所有32个省的大型示威活动。仅在墨西哥城就有15万人上街抗议。因此,互联网能有助于人道社会抵制鼓吹暴力、犯罪和极端主义的人。

在伊朗、摩尔多瓦以及其他国家,网上的组织动员已成为促进民主、使公民对可疑的选举结果表达抗议的重要工具。甚至在美国等已建立民主制度的国家,我们也看到这些工具具有改变历史的力量。你们当中有些人可能还记得这里2008年的总统选举。(笑声)

与这些技术相连接的自由可以帮助转变社会,但同时也对个人极其重要。我最近被一位医生的故事所感动——我不想说出他是哪个国家的人。他千方百计要为女儿的罕见疾病作出诊断。他征询了20多位专家的意见,但仍然没有答案。最后,他是靠互联网搜索引擎得到了确切的诊断并找到了治疗方法 。这就是不受限制地使用搜索引擎技术之所以对个人生活如此重要的原因之一。

我 今天概述的这些原则将成为我们对待互联网自由及其技术使用问题的指导方针。我要谈谈我们在实践中是如何应用这些原则的。美国致力于为促进这些自由投入必要 的外交、经济和技术资源。美国是一个由来自各个国家、反映全球各种利益的移民组成的国家。我们的外交政策基于这样一种理念:当人民之间和国家之间合作时, 美国比任何其他国家都受益。当冲突与误解造成国家间的不合时,美国肩负着比任何国家都更沉重的负担。因此,我们处于有利位置,可以抓住这些随相互连接而来 的机遇。我们作为如此众多技术的诞生地,有责任确保它们从善使用。为此,我们需要建立能力,以推行我们在国务院称之为21世纪外交方略的规划。

重 新调整我们的政策和我们的工作重点并非易事,而适应新技术也鲜有捷径。当电报技术开始使用时,它给外交界许多人带来严重焦虑,因为天天收到发自华盛顿的指 示不是一个百分之百令人欢迎的前景。但正如我们的外交人员最终还是掌握了电报一样,他们也在为掌握这些新工具的潜力而努力。

我引以为豪的是,国务院已经在40多个国家展开努力,帮助那些声音被压制性政府扼杀的人。我们也在努力使这个问题成为联合国的工作重点。我们正在将互联网自由纳入我国重新进入联合国人权理事会(United Nations Human Rights Council)后提出的第一项决议案中。

我 们还支持开发新工具,使公民能够避开政治审查而行使其自由表达的权利。我们正在为世界各地的团体和组织提供资金,确保将这些新工具以当地语言版本提供给需 要的人,并为他们提供安全上网所需的培训。美国支持开展这些努力已有一段时间,侧重于尽可能切实有效地实施这些项目。美国人民应当知道,对互联网进行审查 的国家也应当明白,我国政府致力于促进互联网自由。

我们希望让人们掌握这些工具,用以增进民主和人权,应对气候变化和流行病,为实现奥巴马总统提出的一个没有核武器的世界的目标争取全球支持,鼓励可持续的经济发展,帮助改善底层人民的生活。

因 此,我今天宣布,未来一年中,我们将与实业界、学术界和非政府组织的合作伙伴一道,确立发挥联网技术威力的长期努力,利用这些技术推进我们的外交目标。我 们可以依靠手机、测绘应用软件和其他新工具来增进公民权能,辅助我们的传统外交。我们能够解决目前创新市场存在的缺陷。

请让我举一个例子。假设我想设计一种手机应用软件,让人们能够对包括我国政府在内的各政府部门的责任心和工作效率打分,并能够发现和报告腐败行为,实现这一设想所需的硬件已在几十亿潜在用户的手中,而且所需软件的开发和应用成本较低。

如 果人们利用这项技术,就可以帮助我们有的放矢地使用对外援助经费、改善人民的生活并鼓励外国投资方对负责任的政府投资。但目前的情况是,移动应用技术开发 商尚无资金援助来自行开发这项技术,而国务院现在还缺乏使之成为可能的机制。不过,这项行动应当有助于解决这一问题,并且使小笔创新投资能够带来长期回 报。我们将与专家共同努力,为这种风险投资项目确定最佳框架。我们还将需要科技公司和非营利机构的人才和资源,才能尽快取得最佳效果。因此,在座各位如有 此类才干和专长,我谨在此邀请你们鼎力相助。

与 此同时,有些公司、个人和机构正在设计和开发各种已经能够推进我们的外交和发展目标的创意和应用技术,而国务院将展开一项创新竞赛活动,让这项工作立刻得 到推进。我们将邀请美国人提交应用软件和有关技术的最佳创意,它们应能有助于消除语言障碍、克服文盲局限、将人们与他们所需要的服务和信息连通。例如,微 软公司已经开发出网络医生软件的原型,以便为偏远地区提供医疗服务。我们希望看到更多这样的创意。我们将与竞赛获奖者合作,为帮助他们进一步发展创意提供 资金。

这些新的计划将大大充实我们过去一年来的重要工作。为了促进我们的外事和外交目标,我召集了一个有才干而且经验丰富的团队,领导我们就21世纪外交方略展开的努力。这个团队前往世界各地,协助各国政府和团体善用连接技术的益处。他们发起“公民社会2.0行动”(Civil Society 2.0 Initiative), 协助基层组织进入数字时代。他们在墨西哥制定了一个协助打击毒品暴力的方案,让民众向可靠的来源作出不露痕迹的检举,以免遭受报复。他们也将移动银行带进 阿富汗,现在正在刚果民主共和国进行同样的工作。在巴基斯坦,他们建立了一个首创的移动社交网络,称为“我们的声音”(Our Voice)。这个网络已经产生了数千万条讯息,并将希望抵制暴力极端主义的巴基斯坦年轻人联系在一起。

在短短时间内,我们已经取得了长足的进展,将这些技术的承诺转变成深富影响力的结果。可是仍有许多方面尚待努力。在我们和民营部门及外国政府联手推广21世纪外交方略的工具时,我们必须谨记彼此都有责任捍卫我在今天所谈的自由。我们坚信,信息自由这样的原则不仅是良好的政策,也不仅和我们的国家价值观相连,它还具有普世性,并能产生经济效益。

用 市场语言来说,一家在突尼斯或越南的审查环境中运营的上市公司,其交易价格总是低于在自由社会运营的同类公司。如果企业的决策者没有全球性的新闻和信息来 源,投资者对其决策的信心终将下降。实施新闻和信息审查的国家必须认识到,从经济角度而言,审查政治言论和商业言论是没有区别的。如果贵国的企业无法获取 其中一类信息,其增长必将受到影响。

在制定商业决策时,美国公司日益将网络和信息自由视为更重要的考量因素。我希望他们的竞争对手和外国政府会密切关注这一趋势。最近有关谷歌(Google)的情况引起了广泛的注意。我们希望中国当局对导致谷歌作出日前宣布的网络攻击事件进行彻查。我们也希望调查及结果透明。

互 联网已经成为中国取得巨大进步的源泉之一,令人惊叹。中国现在有如此多的人都在上网。但是,限制自由获取信息或侵犯互联网用户基本权利的国家面临着使自己 与下一个世纪的进步隔绝的风险。美中两国对于这个议题的看法不同,我们希望在两国积极、合作、全面的关系之下坦诚和持续地处理这些差异。

这个议题不仅关系到信息自由,最终还关系到我们希望有一个什么样的世界以及我们将会生活于一个什么样的世界。它关系到我们生活的地球是有一个互联网、一个全球社会以及一个造福并联系全人类的共同知识体,还是支离破碎、获取信息和机遇要取决于居住地点和审查者的心血来潮。

信息自由有助于维护作为全球进步基础的和平与安全。从历史上看,不对称的信息获取能力是国家间冲突的主要原因之一。在我们面对严重纠纷或危险事件时,当事双方能够了解相同的事实和观点是至关重要的。

目 前的情况是,美国人民可以思考外国政府提供的信息——对于这些政府向美国国内传送信息,我们不设置障碍。但是,在实行信息检查的社会中生活的公民却无从得 知外界的看法。例如在北韩,政府极力使其公民与外部意见完全隔绝。这种信息流通的不对称不但增加了发生冲突的可能性,也容易使微小的分歧升级。因此,我期 待那些希望看到全球稳定的负责政府能和我们携手合作,改变这种不对称的情况。

对 公司而言,这个问题所关系的不仅是道德威望,而且涉及公司与用户之间的信任。世界各地的用户都希望自己所依赖的互联网公司会提供全面的搜索结果,并且以负 责任的态度守护他们的个人信息。获得这种信赖并且基本上提供这种服务的公司将在全球市场蓬勃发展。我确实相信,那些失去用户信赖的公司,最终将失去用户。 住在任何地方的人都希望知道,他们放在网上的东西不会被用来加害于自己。

审查不应被世界任何地方的任何公司以任何形式接受。在美国,美国公司需要采取有原则的立场。这应该成为我们国家品牌的组成部分。我相信全世界的用户都会回报尊重这些原则的公司。

我们正在重振“全球互联网自由小组”(Global Internet Freedom Task Force),作为应对全球网络自由所受威胁的论坛。我们敦促美国媒体公司主动采取措施,质疑外国政府对于审查和监视的要求。民营部门也有责任协助保护言论表达自由。当他们的业务交易有可能破坏这种自由时,他们需要考虑什么是正确的,而不只是寻求短视的利润。

我们对于目前通过“全球网络倡议”(Global Network Initiative) 所做的工作倍感鼓舞。“全球网络倡议”是一项由高科技公司与非政府组织、学术专家和社会投资基金共同合作,回应政府审查要求而做出的自愿努力。这项倡议不 仅仅是申明原则,更是建立旨在宣扬真正责任感和透明度的机制。我们承诺支持负责任的民营部门参与护卫信息自由,作为我们承诺的组成部分,国务院将在下月召 集一次高层会议,由罗伯特•霍马茨(Robert Hormats)和玛丽亚•奥特罗(Maria Otero)两位副国务卿共同主持。会议将召集提供网络服务的公司,共同讨论互联网自由问题,因为我们希望与合作伙伴共同应对这个二十一世纪的挑战。

我 相信,追求我今天所说的自由是正确之举,但它也是智慧之举。通过推进这个议程,我们将使我们的原则、我们的经济目标以及我们的战略重点一致起来。我们需要 努力创建这样一个世界:在这个世界中,网络和信息使人民之间的关系更加密切,也使我们的全球社区概念得到扩展。鉴于我们面临的诸多巨大挑战,我们需要世界 各地的人民汇合他们的知识和创造力,帮助重建全球经济,保护我们的环境,战胜暴力极端主义,建设每一个人都能充分发挥和实现其天赋潜力的未来。

在 结束今天的讲话时,我要请你们记住星期一在太子港的废墟中获救的那个小女孩。她还活着,已经与她的家人团聚,并将有机会长大成人,因为网络把一个被埋得很 深的声音传播到全世界。我们不能容许任何国家、群体或个人继续被埋在压制的废墟之下。当层层审查墙把一些人与人类大家庭隔离开来的时候,我们不能袖手旁 观。我们不能因为听不到那些人的呼喊就对这些问题保持沉默。

因此,让我们重新作出承诺,为这一事业而努力。让我们把这些高科技化作推动全世界取得切实进步的力量。让我们并肩前进,倡导这些自由——为了我们这个时代,也为了应当得到我们所能给予的每一个机会的年轻人。

非常感谢你们。(掌声)

标签: ,

liboi – 管理基于OpenInkpot的电子书设备的支持库

2009年12月23日 没有评论

自从用了OpenInkpot,并且推荐了几个用v3的朋友换成OpenInkpot之后,越发觉得,对于普通用户来说,管理阅读器相当的麻烦,三天两头就需要提供技术支持,从安装字体到拷贝书籍再到配置系统,不一而足。

因为OpenInkpot在V3上使用USB RNDIS驱动连接阅读器和PC,并且通过SSH的方式连接设备,但是SSH的设置步骤复杂,而且在Vista x64和Win7这种比较新的操作系统上面总有些问题,对用户要求比较高。

其实V3的这种连接模式比较像Windows Mobile手机的方式,这种方式在具有一个客户端的时候有很大的灵活性,例如WM手机的Active Sync。另外就是等待OI支持USB Mass Storage,即模拟一个U盘出来的方式,但是看起来一时半会儿也没有做的计划。

但是OI有它的好处,就是很多可以自定义的东西,例如阅读器的键绑定——我一直想把翰林V3左侧的翻页键互换——但是修改的方式需要用户ssh到阅读器修改。这对于普通用户来说简直就是噩梦。

同时,OI还有一些问题,例如在0.2 rc5当中添加的libextract,即在书架模式下显示书籍的元信息,而不是文件名,在很多时候其实是有问题的。官方的绕过方式是重命名一个文件,但是同样需要ssh到阅读器修改。

由于这些问题,最好有一个可以用来管理电子书设备的客户端软件。这个软件也许可以做下面的事情:

  1. 自动配置电子书的USB RNDIS连接
  2. 书籍搜索、下载和同步
  3. 数据的同步,例如书签
  4. 状态备份和恢复,例如当前阅读的图书的页码,书籍版式(横屏,切白边等)。
  5. 安装字体
  6. 设备软件更新
  7. 设备配置修改,例如修改键绑定
  8. 其他工具性的功能,例如拷屏

等等。

但是前提是要有一个客户端软件。因此最近先整了一个基础库,取名 liboi,即lib-openinkpot之意,host在Google Code上面: http://code.google.com/p/liboi/。经过几个星期的晚上+周末的工作,现在把0.1版本放出来,这个版本支持最简单的功能:

  1. 根据默认的SSH配置连接阅读器。
  2. 获取阅读器信息,类似OpenInkpot版本号
  3. 在阅读器当中执行命令(有了这个功能剩下的就是想象力了)

目前只支持Linux平台,请在项目主页上下载源码包自行编译。源码包当中有oish.c文件,会生成一个oish(OpenInkpot Shell)的程序,可以作为例子程序使用。

后面的事情,主要包括:

  1. 拷贝文件,包括拷进来和拷出来。
  2. Windows Porting
  3. SSH自动配置

另外招募擅长客户端界面的同仁开发客户端GUI,我要做的话估计界面会很丑。当然了,没报酬,代码开源,纯自愿参与。

标签: , ,

Python multiprocessing 使用手记[3] – 关于Queue

2009年12月13日 没有评论

继续讨论Python multiprocessing,这次讨论的主要内容是mp库的核心组件之一的Queue。

Queue是mp库当中用来提供多进程对象交换的方式。对象交换和上一部分当中提到的对象共享都是使多个进程访问同一个对象的方式,两者的区别就是,对象共享是多个进程访问同一个对象,对象交换则是将对象从一个进程传输的另一个进程。

multiprocessing当中的Queue使用方式和Python内置的threading.Queue对象很像,它支持一个put操作,将对象放入Queue,也支持一个get操作,将对象从Queue当中读出。和threading.Queue不同的是,mp.Queue默认不支持join()和task_done操作,这两个支持需要使用mp.JoinableQueue对象。

由于Queue对象负责进程之间的对象传输,因此第一个问题就是如何在两个进程之间共享这个Queue对象本身。在上一部分所言的三种共享方式当中,Queue对象只能使用继承(inheritance)的方式共享。这是因为Queue本身基于unix的Pipe对象实现,而Pipe对象的共享需要通过继承。因此,在一个典型的应用实现模型当中,应该是父进程创建Queue,然后创建子进程共享该Queue,由父进程和子进程分别读写。例如下面的这个例子:

import multiprocessing
 
q = multiprocessing.Queue()
 
def reader_proc():
    print q.get()
 
reader = multiprocessing.Process(target=reader_proc)
reader.start()
 
q.put(100)
reader.join()

另一种实现方式是父进程创建Queue,创建多个子进程,有的子进程读Queue,有的子进程写Queue,例如:

import multiprocessing
 
q = multiprocessing.Queue()
 
def writer_proc():
    q.put(100)
 
def reader_proc():
    print q.get()
 
reader = multiprocessing.Process(target=reader_proc)
reader.start()
writer = multiprocessing.Process(target=writer_proc)
writer.start()
 
reader.join()
writer.join()

由于使用继承的方式共享Queue,因此代码当中并没有明显的传输Queue对象本身的代码,看起来似乎只要将multiprocessing当中的对象换成threading当中的对象,程序仍然能够工作。反之,拿到一个现有的多线程程序,是不是将threading改成multiprocessing就可以工作呢?也许可以,但是更可能的情况是你会遇到很多问题。

第一个问题就是mp的Queue需要考虑多进程之间的对象传输,因此所传输的对象必须是可以pickle的。否则,在Queue的put操作上会抛出PicklingError。

其他的一些差异表现在一些技术细节上,这些不是任何高层逻辑可以抽象掉的,不知道这些差异会导致一些潜在的错误,例如死锁。在总结这些潜在的犯错的可能的同时,我们会简单看一下mp当中Queue的实现方式,以便能够方便的理解为什么会有这样的行为。这些实现问题仅仅针对Linux,Windows上面的实现和出现的问题在这里不涉及。

mp.Queue建构在系统的Pipe之上,但是实际上进程并不是直接将对象写入到Pipe里面,而是先写入一个本地的buffer,再由一个专门的feed线程将其放入Pipe当中。读取端则是直接从Pipe当中读出对象。之所以有这样一个feed线程,是为了能够提供Queue接口函数所需要的put的超时控制。但是由于这个feed线程的存在,mp.Queue提供了几个额外的函数来控制它,一个函数close来停止该线程,以及join_thread来join该线程。close同时负责把所有在buffer当中的对象刷新到Pipe当中。

但是这个feed线程也是个麻烦制造者,为了保证所有被放入Queue的东西最终都能够到达另外一端的进程,mp库注册了一个atexit的处理函数,用来在进程退出的时候自动close并且join该feed线程。这个join动作带来了很多问题,比如潜在的死锁。考虑下面一种状况:一个父进程创建了两个子进程,一个子进程读,另一个子进程写。当需要停止这些进程的时候,父进程如果先把读进程结束,但是同时写进程已经将太多的对象写入Queue,导致后继的对象等待在buffer当中,则这个进程将无法终止,因为atexit的处理函数等待把所有buffer当中的对象放入Pipe,但是Pipe已经满了,然后陷入了死锁。

有人可能会问,那只要保证总是按照数据流的顺序来停止进程不就行。问题是在很多复杂的系统流程当中,可能存在一个环形的数据流,这种情况下,无论按照什么顺序停止进程,终究有一个进程可能陷入这种情景当中。

幸运的是,Queue对象还提供了一个成员函数cancel_join_thread,这个函数可以使得在进程停止的时候不进行join操作,这样可以避免死锁,代价就是这个时候尚未刷新到Pipe当中的对象都会丢失。鉴于即使调用了join_thread,残留在Pipe当中的对象仍然可能丢失,所以一旦选择使用mp的Queue对象,就不要假设不会在流程当中丢对象了。

另外一个可能的方案是使用mp库当中的SimpleQueue对象。这个对象在文档当中没有提及,但是在multiprocessing.queue模块当中有定义。这个对象就是去掉了buffer的Queue对象,因此可能能够避免上面说的问题的。但是SimpleQueue没有提供put和get的超时处理,两个动作都是阻塞的。

除了使用multiprocessing.Queue,还可以使用multiprocessing.Pipe进行通信。mp.Pipe是Queue的底层结构,但是没有feed线程和put/get的超时控制。一定程度上和SimpleQueue很像。需要注意的是Pipe带有一个参数 duplex,当设置为True(默认)的时候,Pipe并不是使用系统的pipe来实现,而是通过socketpair,即Unix Domain Socket来实现。这个和pipe相比有些微的性能差异。

另外一个使用Queue的方式不是mp库内置的。这种方式使用上一篇文章当中提到的server process的方式来共享一个Queue对象。这个Queue对象实际上在server process当中,所有的子进程通过socket连接到server process获取该Queue的代理对象进行操作。说到这有人会想起来mp库有一个内置的SyncManager对象,可以通过multiprocess.Manager函数获取到,通过该对象的Queue方法可以获取一个Queue的代理对象。不幸的是,这个方法不是正确的获取Queue的方式,原因正如上一篇文章所说,SyncManager.Queue方法的每次调用获取到的是一个新建对象的代理对象,而不是一个共享对象。正确的使用server process当中的Queue的方式是:

共同部分:

import multiprocessing.managers as mpm
import Queue
 
class SharedQueueManager(mpm.BaseManager): pass
q = Queue.Queue()
SharedQueueManager.register('Queue', lambda: q)

服务进程:

mgr = SharedQueueManager(address=('', 12345))
server = mgr.get_server()
server.serve_forever()

客户进程:

mgr = SharedQueueManager(address=('localhost', 12345))
mgr.connect()
q = mgr.Queue() # 这里q就是共享的Queue对象的代理对象

这种方式比起mp库内置的Queue,有一些性能上的影响,因为毕竟牵涉到多次网络通讯,但是带来的好处是没有feed线程带来的一系列问题,而且理论上不会存在丢数据的问题,除非server process崩溃。但是正如上一篇所说,server process本身就不是很靠谱的,因此这里也只是“理论上”不会丢数据而已。

说到性能,这里就列两个性能数据,以前在twitter上面提到过的(这两个连接无法访问的请联系我):

操作对象为 pickle后512字节的对象,通过proxy操作Queue的性能大约是7000次/秒(本机)或1100次/秒(多机),如果使用 multiprocessing.Queue,效率可达54000次/秒。

Python multiprocessing 使用手记[2] – 跨进程对象共享

2009年12月7日 没有评论

继续写关于Python multiprocessing的使用手记,继上次的进程模型之后,这次展开讨论一下multiprocessing当中的跨进程对象共享的问题。

在mp库当中,跨进程对象共享有三种方式,第一种仅适用于原生机器类型,即python.ctypes当中的类型,这种在mp库的文档当中称为shared memory方式,即通过共享内存共享对象;另外一种称之为server process,即有一个服务器进程负责维护所有的对象,而其他进程连接到该进程,通过代理对象操作服务器进程当中的对象;最后一种在mp文档当中没有单独提出,但是在其中多次提到,而且是mp库当中最重要的一种共享方式,称为inheritance,即继承,对象在父进程当中创建,然后在父进程是通过multiprocessing.Process创建子进程之后,子进程自动继承了父进程当中的对象,并且子进程对这些对象的操作都是反映到了同一个对象。

这三者共享方式各有特色,在这里进行一些简单的比较。

首先是共享方式所应对的对象类型,看这个表:

共享方式 支持的类型
Shared memory ctypes当中的类型,通过RawValue,RawArray等包装类提供
Inheritance 系统内核对象,以及基于这些对象实现的对象。包括Pipe, Queue, JoinableQueue, 同步对象(Semaphore, Lock, RLock, Condition, Event等等)
Server process 所有对象,可能需要自己手工提供代理对象(Proxy)

这个表总结了三种不同的共享方式所支持的类型,下面一个个展开讨论。

其中最单纯简单的就是shared memory这种方式,只有ctypes当中的数据类型可以通过这种方式共享。由于mp库本身缺少命名的机制,即在一个进程当中创建的对象,无法在另外一个进程当中通过名字来引用,因此,这种共享方式依赖于继承,对象应该由父进程创建,然后由子进程引用。关于这种机制的例子,可以参见Python文档当中的例子 Synchronization types like locks, conditions and queues,参考其中的test_sharedvalues函数。

然后是继承方式。首先关于继承方式需要有说明,继承本质上并不是一种对象共享的机制,对象共享只是其副作用。子进程从父进程继承来的对象并不一定是共享的。继承本质上是父进程fork出的子进程自动继承父进程的内存状态和对象描述符。因此,实际上子进程复制了一份父进程的对象,只不过,当这个对象包装了一些系统内核对象的描述符的时候,拷贝这个对象(及其包装的描述符)实现了对象的共享。因此,在上面的表当中,只有系统内核对象,和基于这些对象实现的对象,才能够通过继承来共享。通过继承共享的对象在linux平台上没有任何限制,但是在Windows上面由于没有fork的实现,因此有一些额外的限制条件,因此,在Windows上面,继承方式是几乎无法用的。

最后就是Server Process这种方式。这种方式可以支持的类型比另外两种都多,因为其模型是这样的:

server process模型

server process模型

在这个模型当中,有一个manager进程,负责管理实际的对象。真正的对象也是在manager进程的内存空间当中。所有需要访问该对象的进程都需要先连接到该管理进程,然后获取到对象的一个代理对象(Proxy object),通常情况下,这个代理对象提供了实际对象的公共函数的代理,将函数参数进行pickle,然后通过连接传送到管理进程当中,管理进程将参数unpickle之后,转发给相应的实际对象的函数,返回值(或者异常)同样经过管理进程pickle之后,通过连接传回到客户进程,再由proxy对象进行unpickle,返回给调用者或者抛出异常。

很明显,这个模型是一个典型的RPC(远程过程调用)的模型。因为每个客户进程实际上都是在访问manager进程当中的对象,因此完全可以通过这个实现对象共享。

manager和proxy之间的连接可以是基于socket的网络连接,也可以是unix pipe。如果是使用基于socket的连接方式,在使用proxy之前,需要调用manager对象的connect函数与远程的manager进程建立连接。由于manager进程会打开端口接收该连接,因此必要的身份验证是需要的,否则任何人都可以连上manager弄乱你的共享对象。mp库通过authkey的方式来进行身份验证。

在实现当中,manager进程通过multiprocessing.Manager类或者BaseManager的子类实现。BaseManager提供了函数register注册一个函数来获取共享对象的proxy。这个函数会被客户进程调用,然后在manager进程当中执行。这个函数可以返回一个共享的对象(对所有的调用返回同一个对象),或者可以为每一个调用创建一个新的对象,通过前者就可以实现多个进程共享一个对象。关于这个的用法可以参考Python文档当中的例子“Demonstration of how to create and use customized managers and proxies”。

典型的导出一个共享对象的代码是:

ObjectType object_
class ObjectManager(multiprocessing.managers.BaseManager): pass
ObjectManager.register("object", lambda: object_)

注意上面介绍proxy对象的时候,我提到的“公共函数”四个字。每个proxy对象只会导出实际对象的公共函数。这里面有两个含义,一个是“公共”,即所有非下划线开头的成员,另一个是“函数”,即所有callable的成员。这就带来一些限制,一是无法导出属性,二是无法导出一些公共的特殊函数,例如__get__, __next__等等。对于这个mp库有一套处理,即自定义proxy对象。首先是BaseManager的register可以提供一个proxy_type作为第三个参数,这个参数指定了哪些成员需要被导出。详细的使用方法可以参见文档当中的第一个例子。

另外manager还有一些细节的问题需要注意。由于Proxy对象不是线程安全的,因此如果需要在一个多线程程序当中使用proxy,mp库会为每个线程创建一个proxy对象,而每个proxy对象都会对server process创建一个连接,而manager那边对于每个连接都创建一个单独的线程来为其服务。这样带来的问题就是,如果客户进程有很多线程,很容易会导致manager进程的fd数目达到ulimit的限制,即使没有达到限制,也会因为manager进程当中有太多线程而严重影响manager的性能。解决方案可以是一个进程内cache,只有一个单独的线程可以创建proxy对象访问共享对象,其余线程只能访问该进程当中的cache。

一旦manager因为达到ulimit限制或者其他异常,manager会直接退出,遗憾的是,这时候已经建立的proxy会试图重新连接manager – 但是它已经不存在了。这个会导致客户进程hang在对proxy的函数调用上,这个时候,目前除了杀掉进程没有找到别的办法。

另外proxy使用socket的方式比较tricky,因此和内置的socket库有很多冲突,比如socket.setdefaulttimeout(Python Issue 6056 )。在setdefaulttimeout调用了之后,进程当中所有通过socket模块建立的socket都是被设置为unblock模式的,但是mp库并不知道这一点,而且它总是假设socket都是block模式的,于是,一旦调用了setdefaulttimeout,所有对于proxy的函数调用都会抛出OSError,错误代码为11,错误原因是非常有误导性的“Resource temporarily unavailable”,实际上就是EAGAIN。这个错误可以通过我提供的一个patch来补救(这个patch当中还包含其他的一些修复,所以请自行查看并修改该patch)。

由于以上的一些原因,server process模式作为一个对象的共享模式,能够提供最为灵活的共享方式,但是也有最多的问题。这个在使用过程当中就靠自己去衡量了。目前我们的系统对于数据可靠性方面要求不高,丢失数据是可以接受的,但是也只用这种模式来维护统计值,不敢用来维护更多的东西。

关于跨进程共享对象的问题就写到这里,后面内容待续……

test_sharedvalues