CPython 源码中整数加法的实现

最近突然涌起兴趣去阅读 CPython 源码,网上也看了不少解析的文章,后来网上看到《Python源码剖析》评价不错,可惜现在已经绝版,只能从豆瓣阅读购买了一本电子书观摩 。

我从网上下载的是最新的 Python 2.7 源码,这本书配套的解说代码是 Python 2.5 的,这是一个遗憾,但是大体上相差不大,刚好昨天遇到一处。

昨天看到 Python int 实现的原理,这里不详细表述,有兴趣的可以去看看书。其中整数加法 (int_add) 的实现,虽然代码只有几行,但是其中隐藏的知识点还是非常多的,花了点时间回顾了一些基础知识,在这里也简单总结下。

以下是 2.5 里面加法的实现,也是书中提供的例子,这里直接引用过来作为参考对比,注释是作者加入的。

static PyObject* int_add(PyIntObject *v, PyIntObject *w)
{
    register long a, b, x;
    CONVERT_TO_LONG(v, a);
    CONVERT_TO_LONG(w, b);
    x = a + b;
    //[1] : 检查加法结果是否溢出
    if ((x^a) >= 0 || (x^b) >= 0)
        return PyInt_FromLong(x);
    return PyLong_Type.tp_as_number->nb_add((PyObject *)v, (PyObject *)w);
}

下面是 2.7 中的代码对比,大体都没有变化:

static PyObject *
int_add(PyIntObject *v, PyIntObject *w)
{
    register long a, b, x;
    CONVERT_TO_LONG(v, a);
    CONVERT_TO_LONG(w, b);
    /* casts in the line below avoid undefined behaviour on overflow */
    x = (long)((unsigned long)a + b);
    if ((x^a) >= 0 || (x^b) >= 0)
        return PyInt_FromLong(x);
    return PyLong_Type.tp_as_number->nb_add((PyObject *)v, (PyObject *)w);
}

继续阅读

tmp 目录文件被自动清理问题的调查

某次项目发布过程中,当我们把 rpm 包下发到每台 nc 之后,发现过了一会儿文件就被删除了,当时百思不得其解,第二天亲自试了下,果然能够稳定复现。

试了几次发现,放在 /tmp 目录下的文件,只要文件权限是当前的帐号(假设 abc),并且最近修改时间比较久(大概是分钟级别),就会被自动清理。所以第一个线索:自动清理的脚本应该是 abc 权限运行的,而且根据修改时间去删除文件。

当时第一反应是 tmpwatch 搞得鬼,但是 tmpwatch 印象中是按天执行的,而且是删除 10 天内未修改的文件(ctime/mtime/atime),所以应该可以排除。当然不排除有人擅自修改了配置文件,看了下确实不是:

flags=-umc
/usr/sbin/tmpwatch "$flags" -x /tmp/.X11-unix -x /tmp/.XIM-unix \
	-x /tmp/.font-unix -x /tmp/.ICE-unix -x /tmp/.Test-unix \
	-X '/tmp/hsperfdata_*' 240 /tmp
/usr/sbin/tmpwatch "$flags" 720 /var/tmp
for d in /var/{cache/man,catman}/{cat?,X11R6/cat?,local/cat?}; do
    if [ -d "$d" ]; then
	/usr/sbin/tmpwatch "$flags" -f 720 "$d"
    fi
done

当确认不是 tmpwatch 的问题之后,我就想找个工具去监控“删除”这个行为,google 找到 inotify-tools 工具,尝试运行采集了一把:

$inotifywatch -v -t 60 -r /tmp/b.rpm.bak
Establishing watches...
Setting up watch(es) on /tmp/b.rpm.bak
OK, /tmp/b.rpm.bak is now being watched.
Total of 1 watches.
Finished establishing watches, now collecting statistics.
Will listen for events for 60 seconds.
total  attrib  delete_self  filename
3      1       1            /tmp/b.rpm.bak

但是很遗憾,这个工具无法知道是哪个进程操作的,只能确定确实发生了“删除”的行为。

继续阅读

WordPress 文章内嵌 Gist 代码

WordPress 内嵌 Gist 链接的方法很简单,将以下代码添加到当前主题的 functions.php 文件中:

/*
 * Embed gists with a URL in post article
 */
function dangopress_embed_gist($matches, $attr, $url, $rawattr)
{
    $embed = sprintf(
        '<script src="https://gist.github.com/%1$s.js%2$s"></script>',
        esc_attr($matches[1]),
        esc_attr($matches[2])
    );

    return apply_filters('dangopress_embed_gist', $embed, $matches, $attr, $url, $rawattr);
}
wp_embed_register_handler('gist', '#https?://gist\.github\.com(?:/[a-z0-9-]+)?/([a-z0-9]+)(\?file=.*)?#i', 'dangopress_embed_gist');

在上面的代码中,我们注册了 Gist 链接的处理方法 dangopress_embed_gist。当我们拷贝 Gist 链接到编辑框时,会调用改方法生成内嵌内容。

Gist 链接是通过注册过程中,指定的正则表达式匹配的:

#https?://gist\.github\.com(?:/[a-z0-9-]+)?/([a-z0-9]+)(\?file=.*)?#i

继续阅读

浅谈 Shell 脚本配置文件格式

开发过程中为了减少 hardcode,不可避免的需要提供配置文件给用户定制。对于高级编程语言来说,因为有丰富的第三方库,可供选择的配置文件格式有很多,比如 xml、jsno、ini、yaml 等等。

key=value 文本格式配置

而对于 linux shell,基本上很难使用前面提到的各种格式。所以在 unix 系统上,很多 shell 脚本的配置文件都是纯粹的 key=value 文本格式,例如绝大多数的开机服务启动脚本、网络配置文件等。

例子 1:ntp 配置文件

$ cat /etc/sysconfig/ntpd
# Drop root to id 'ntp:ntp' by default.
OPTIONS="-u ntp:ntp -p /var/run/ntpd.pid"

# Set to 'yes' to sync hw clock after successful ntpdate
SYNC_HWCLOCK=no

# Additional options for ntpdate
NTPDATE_OPTIONS=""

例子 2:网络配置文件

$ cat /etc/sysconfig/network
NETWORKING="yes"
HOSTNAME="xx.com"

而且,要注意得是,一般 key=value 的等号两边不应该有空格,因为大多数脚本都是直接 source 配置文件的(当然,也有部分脚本是会自己处理配置文件格式),使用起来很简单,基本上没有解析的操作:

$ cat /etc/init.d/network
if [ ! -f /etc/sysconfig/network ]; then
    exit 0
fi

. /etc/sysconfig/network

理所当然,这种格式无法满足更复杂的配置文件需求,比如 ini 格式的 section。那么,在 shell 中除了满世界去找一个解析库之外,能有什么方法可以实现呢?

扩展 key=value 文本格式配置

假设,我们管理着 n 个集群,每个集群配置项都是一样的,我们需要在 shell 脚本中,可以根据集群的名称来导入对应的配置。

继续阅读

logrotate 没有滚动日志

前段时间发现某些机器磁盘空间报警,使用 du 命令(慎重使用)查询后发现部分日志文件非常大,例如 secure 日志,差不多有 10G 左右。

$ ls -lh /var/log/secure
-rw-r----- 1 root adm 9.7G Mar 24 20:44 /var/log/secure

我们发现 secure 日志已经超过一周没有滚动了,按照 logrotate 的配置,secure 日志应该按周滚动一次,最多滚动 4 次:

$cat /etc/logrotate.d/syslog-ng
/var/log/messages /var/log/secure /var/log/maillog /var/log/spooler /var/log/boot.log /var/log/cron {
    sharedscripts
    postrotate
        /etc/rc.d/init.d/syslog-ng reload 2>/dev/null || true
    endscript
}

$cat /etc/logrotate.conf
# see "man logrotate" for details
# rotate log files weekly
weekly

# keep 4 weeks worth of backlogs
rotate 4

# create new (empty) log files after rotating old ones
create

# uncomment this if you want your log files compressed
compress

# RPM packages drop log rotation information into this directory
include /etc/logrotate.d

# system-specific logs may be also be configured here.

看起来应该是日志滚动过程出现了问题,然后通过 logroate debug 了一把,发现中间出错:

$ logrotate -dv /etc/logrotate.conf 
reading config file /etc/logrotate.conf
including /etc/logrotate.d
reading config file acpid
reading config info for /var/log/acpid 
reading config file balloond
reading config info for /var/log/xen/balloond.log 
reading config file conman
error: error accessing /var/log/conman: No such file or directory
error: conman:5 glob failed for /var/log/conman/*

看最后两行 error,因为 /var/log/conman 目录找不到,导致滚动过程出错,所以就没有触发 secure 日志滚动处理。

那么最直接的解决方法是,手工创建 /var/log/conman 目录,这个问题就可以跳过了。但是,这种方法毕竟不完美,如果哪天另外一个目录不存在,仍然会出现这个问题。而且,logrotate 对这种场景的处理本来就不合理,然后去翻了下它的 changelog,发现这个 bug 已经在 3.7.4-12 以后的版本中 fixed:

* Thu Mar 31 2011 Jan Kaluza <jkaluza@redhat.com> - 3.7.4-12
- fix #540119 - fixed missingok problem with globs

所以,根本的解决方法是更新 logrotate 包。

ZooKeeper 运维资料汇总

ZooKeeper 运维相关资料汇总,如有帮助,转给有需要的同学。

运维注意

ZooKeeper 机器数量要求是 2xF+1 台,其中 F 表示可以容忍的机器故障台数,比如 3 台机器可以容忍 1 个 Failure,5 台机器容忍 2 个 Failure,… 一般最少选择 3 台机器(开发测试环境除外)。

确定好数量之后,要合理的部署 ZooKeeper 机器,理论上有条件的话,应该将 ZooKeeper 机器部署到不同的机房,做到跨机房容灾。例如,3 台 ZooKeeper 机器放到 3 个机房,这样可以最大程度降低故障概率。如果条件不允许,请把 Zookeeper 部署在不同的交换机下,或者至少不同的机柜下。

ZooKeeper 会把每个事务都写到磁盘文件中,然后定期汇总一份快照文件。为了最大限度的提高性能,建议把 transaction log 写到独立的磁盘中,避免受到其他应用程序的干扰(有没有试过写到 /dev/shm 内存?),而且 dataDir 和 dataLogDir 不要配置到同一快磁盘。

默认情况下,ZooKeeper 不会对历史的日志文件进行清理,这一点需要自己去做。从 3.4.0+ 版本开始,ZooKeeper 有提供内置的解决方案,通过参数可以配置清理策略。对于 3.3.x 版本,还是只能通过定时任务的方式清理

ZooKeeper 写的 transaction log 和 snapshot log 文件不是直接给人看的,如果你想查看日志的内容,可以通过一些工具来实现。比如,使用 LogFormatter 查看 transaction log 文件,SnapshotFormatter 看 snapshot log 文件。

3.3.5 版本下,LogFomatter 看到的信息有限,SnapshotFormatter 不存在,应该是 3.4.0+ 版本才支持,下面是一个例子:

# /opt/taobao/java/bin/java -cp /opt/taobao/zookeeper/zookeeper-3.3.5.jar:/opt/taobao/zookeeper/lib/log4j-1.2.15.jar org.apache.zookeeper.server.LogFormatter /disk2/zkDataLog/version-2/log.900000001

ZooKeeper 有提供一些四个字母的命令,具体看前面提到的管理指南,使用这些命令可以很好的观察系统的运行状况。

继续阅读

Shell 一键安装命令

现在是懒人的天下,为了迎合用户的需求,很多开源软件或者包提供的安装步骤都非常简单,大家应该看到不少类似一键安装的命令。下面是几个典型的例子:

# homebrew 安装
$ ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"

# nvm 安装, 两种方法
$ curl https://raw.githubusercontent.com/creationix/nvm/v0.8.0/install.sh | sh
$ wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.8.0/install.sh | sh

# rvm 安装
$ \curl -sSL https://get.rvm.io | bash -s stable

简单粗暴,CMD + C 再加 CMD + V,随手一个 Enter,就搞定了。

那么这上面的原理是什么样子的呢?其实很简单。

首先通过 curl 或者 wget 将安装脚本下载下来,将内容输出到标准输出。这一步对应上面的 curl -ssL 或者 wget -qO-,一定要注意将错误或者异常输出过滤掉,保证标准输出的内容就是脚本的内容。

然后通过管道传递给 shell,shell 在没有指定脚本文件的时候,支持从标准输入读取脚本内容并解释执行。这样将"下载 - 保存 - 安装"这几步操作合到一个命令中完成。

对于 rvm 的安装又有点特殊,安装脚本需要指定执行参数,bash -s stable-s 之后的部分就是透传给安装脚本的参数,翻译下可以理解的形式是:

$ \curl -sSL https://get.rvm.io > /tmp/rvm_install.sh
$ bash /tmp/rvm_install.sh stable
$ rm -f /tmp/rvm_install.sh

PS: \curl 的用法,我在 终端下肉眼看不见的东西 曾经提到过。

不过,建议执行类似一键安装的命令之前,一定要先大致看下安装脚本,避免里面有不安全的代码。

理解 IFS

Bash 里的 word splitting 是很基础的一个知识点,如果没有理解透彻,很多时候会犯下不少奇奇怪怪的错误(参见 Bash Pitfalls,或者本博客翻译 Bash Pitfalls: 编程易犯的错误(一))。

一个例子,现在我们现在要一次读入文件 onefile.txt 的内容并输出,假设文件的内容是这样的:

kodango -> ~/Workspace/coding/test
$ cat onefile.txt
hello world

当我们习惯性地使用 for 循环来解决这个问题时,你会发现输出的结果与预期大相径庭:

$ for i in $(<onefile.txt); do echo "$i"; done
hello
world

给我们的脑子也打开调试开关。首先 onefile.txt 的内容一次性地输出给 for 循环,我在中间用比较形象的 tag 来描述一个空白字符:

hello<blank>world<newline><blank><newline>

这个时候,word splitting 发生了,将以上字符串按照 IFS 分隔成 helloworld。IFS 是用来分隔命令中的每一个单词的,它可以有多个字符组成,每个字符都被视作分隔符。默认情况下,它的值为 <newline><tab><whitespace,这也是为什么默认都是按空格、回车等空白字符分隔的原因。

这样一来 hello 和 world 被分隔可以很好地解释,但是那又是为什么第二行的空行没有了呢?原来,当 IFS 包含空白字符时(比如回车、空格、制表符等),在任何需要分隔单词的场景下,位于字符串开头和结尾的空白字符会被删除,另外一点是,字符串中间的连续空白会被压缩成一个。

继续阅读