9
13
2015
21

为树莓派交叉编译 8192eu 网卡驱动

最近打算把闲置了许久的树莓派重新利用起来。交给它的第一个任务是:做路由器。于是去弄了个 USB 无线网卡,型号是 TP-Link WN823N 版本 2.0。买的时候没注意,拿到手才知道这款需要自行安装驱动。还得使用特制版本的 hostapd。

驱动名叫 8192eu,或者 rtl8192eu,随便啦。GitHub 上有多个版本,我使用的是 Mange/rtl8192eu-linux-driver。因为 gcc 及内核更新的原因,需要修改两处:

diff --git a/Makefile b/Makefile
index 0c800f8..85058fa 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,7 @@ EXTRA_CFLAGS += -Wno-unused-label
 EXTRA_CFLAGS += -Wno-unused-parameter
 EXTRA_CFLAGS += -Wno-unused-function
 EXTRA_CFLAGS += -Wno-unused
+EXTRA_CFLAGS += -Wno-date-time

 #EXTRA_CFLAGS += -Wno-uninitialized

diff --git a/os_dep/linux/rtw_android.c b/os_dep/linux/rtw_android.c
index 98f0d31..8a2ee56 100644
--- a/os_dep/linux/rtw_android.c
+++ b/os_dep/linux/rtw_android.c
@@ -337,7 +337,7 @@ int rtw_android_cmdstr_to_num(char *cmdstr)
 {
        int cmd_num;
        for(cmd_num=0 ; cmd_num<ANDROID_WIFI_CMD_MAX; cmd_num++)
-               if(0 == strnicmp(cmdstr , android_wifi_cmd_str[cmd_num], strlen(android_wifi_cmd_str[cmd_num])) )
+               if(0 == strncasecmp(cmdstr , android_wifi_cmd_str[cmd_num], strlen(android_wifi_cmd_str[cmd_num])) )
                        break;

        return cmd_num;

然后,本文的主题来了:我需要 ARM 版的驱动!因为我的树莓派没有键盘也没有显示器,也没有网线什么的。除了电源和 SD 卡,它只有一块无线网卡了。所以只能交叉编译了。

本来呢,内核使用的构建系统非常棒,一切都会很顺利的。但是,我不要先交叉编译个 ARM 版内核。于是我遇到了这个问题scripts 目录下的二进制文件是编译模块的时候需要执行的,然而我的机器并不能执行 ARM 版本的二进制。

好吧,不就是一些小程序么。把我本机的复制过去就可以跑了嘛。结果开心地看着各源码文件被编译成目标文件之后,遇到 modpost 报了这么个错误:

FATAL: section header offset=11258999068426292 in file '/ldata/DATA/src/rtl8192eu-linux-driver/8192eu.o' is bigger than filesize=1094666

大概是因为我的系统是 64 位的,然而 ARM 是 32 位的吧。不过我没兴趣去找一个 i686 版本的 modpost 来尝试了。真要在我笔记本上跑 ARM 程序又不是不可以,我们有 qemu 嘛。虽然是模拟器,不过我不觉得它会比在我那树莓派上运行慢 :-)

以下是整个完整的步骤:

首先说明一点,我使用的是 Arch Linux ARM。树莓派官方提供的 Raspberry 镜像里东西太多了,我的 SD 卡放不下我也用不着。而且它是基于 Debian Wheezy 的,没有 systemd 可用。

新建一个目录rpi,开始啦!

因为要运行 ARM 版的 modpost 程序,我们先下载树莓派的 gcc-libs、glibc,并解压出其 /usr/lib 下的文件:

wget https://mirrors.ustc.edu.cn/archlinuxarm/armv6h/core/gcc-libs-5.2.0-2-armv6h.pkg.tar.xz https://mirrors.ustc.edu.cn/archlinuxarm/armv6h/core/gcc-libs-5.2.0-2-armv6h.pkg.tar.xz.sig https://mirrors.ustc.edu.cn/archlinuxarm/armv6h/core/glibc-2.22-1-armv6h.pkg.tar.xz https://mirrors.ustc.edu.cn/archlinuxarm/armv6h/core/glibc-2.22-1-armv6h.pkg.tar.xz.sig
gpg --verify glibc-2.22-1-armv6h.pkg.tar.xz.sig
tar xf glibc-2.22-1-armv6h.pkg.tar.xz usr/lib || true
gpg --verify gcc-libs-5.2.0-2-armv6h.pkg.tar.xz.sig
tar xf gcc-libs-5.2.0-2-armv6h.pkg.tar.xz usr/lib
[[ ! -f lib ]] && ln -s usr/lib lib

要编译内核模块,当然少不了 linux-*-headers 包了:

wget https://mirrors.ustc.edu.cn/archlinuxarm/armv6h/core/linux-raspberrypi-headers-4.1.6-3-armv6h.pkg.tar.xz https://mirrors.ustc.edu.cn/archlinuxarm/armv6h/core/linux-raspberrypi-headers-4.1.6-3-armv6h.pkg.tar.xz.sig
gpg --verify linux-raspberrypi-headers-4.1.6-3-armv6h.pkg.tar.xz.sig
tar xf linux-raspberrypi-headers-4.1.6-3-armv6h.pkg.tar.xz usr

不必每次更新 gcc-libs 和 glibc,只要它们能跑 modpost 程序就可以了。但是内核头文件是要和系统上运行的内核匹配的。

我们删掉 ARM 版的 scripts 目录,换上本机的版本。但是 modpost 例外。同时要修改 Makefile.modpost,使之使用 qemu-arm 来运行 modpost 程序:

pushd usr/lib/modules/4.1.6-3-ARCH/build
mv scripts/mod/modpost .
rm -rf scripts
cp -r /usr/lib/modules/$(uname -r)/build/scripts .
sed -i '/^modpost =/s/scripts/qemu-arm scripts/' scripts/Makefile.modpost
mv modpost scripts/mod
popd

最后就可以编译啦。把交叉编译工具链签名)的路径加到 $PATH 里去。还要设置 QEMU_LD_PREFIX 到我们解压出来的那些文件所在的目录好让 qemu-arm 能够找到需要的库文件。然后进入驱动目录,开始编译!

path+=/ldata/DATA/soft/arm-lilydjwg-linux-gnueabi/bin
export QEMU_LD_PREFIX=$PWD
cd ../rtl8192eu-linux-driver
make CROSS_COMPILE=arm-lilydjwg-linux-gnueabi- KSRC=../rpi/usr/lib/modules/4.1.6-3-ARCH/build ARCH=arm
gzip 8192eu.ko

就酱。

试错几次之后,终于把配置写对了,于是我看到树莓派的 Wi-Fi 灯闪动了,随即从系统日志看到 hostapd 和 dnsmasq 都报告它连上网了~然后 ssh 登陆过去:

Last login: Tue Jun 11 22:57:29 2013 from 192.168.2.101

两年零三个月没进去过了呢。然后,我换 USTC 源,执行了pacman -Syu跨越两年零三个月的滚动更新,然而除了很多配置文件有新版本需要手工合并外,并没有发生什么特别的事情,就更没有滚挂了=w=

后来我也尝试在树莓派上直接编译内核模块(因为内核升级了嘛)。结果表明,交叉编译是正确的选择!在树莓派上编译这个模块的时间,我的笔记本估计可以编译出整个内核了……

这是我编译的 8192eu 模块签名文件,对应内核版本 4.1.6。


至于 hostapd,下载这个,把其中的wpa_supplicant_hostapd-0.8_rtw_r7048.20130424.tar.gzhostapd目录下的东西编译了就好。只需要指定CC变量就可以交叉编译成功。

这是我编译的 hostapd签名文件。配置文件中要写driver=rtl871xdrv

Category: Linux | Tags: linux 交叉编译 树莓派
7
31
2015
10

交换 ThinkPad 键盘上的 Insert 和 End 键

ThinkPad 键盘上的第一行键现在默认在不按下Fn键时执行多媒体按键功能,按下Fn时才能执行F1-F12的功能,对于像我这种 Vimhtop 等的用户来说颇为不便。还好,BIOS 选项里可以改回来。

然后我遇到了 X250,发现InsertEnd键怎么也受那个选项的影响了!也就是,如果F1-F12设置得方便了,那么End键就需要Fn键配合。而HomeEnd这种光标移动键虽然很少用,但毕竟还是要用到的,比如在 htop、weechat、mutt 以及不支持自定义编辑键的 Qt 程序里的时候。

所以呢,我在~/.Xmodmap里把这两个键交换了:

keysym End = Insert
keysym Insert = End

这下子用笔记本上的键盘是没问题了。可是我用外接键盘的话,这两个键就又反过来了 Orz……

当然网上会有 udev 规则,在插上外设时跑个脚本什么的。可不管怎么映射,总有个键盘的按键是反的啊

仔细询问 Google 之后,在 Gentoo 的论坛里终于发现这么一条线索

To alter keymap of a particular keyboard you need to issue EVIOCSKEYCODE ioctl on corresponding /dev/input/eventX node.

所以,找到相应的 event 设备文件之后,只需要 ioctl 一下就可以了?可是EVIOCSKEYCODE是个什么鬼啊……

找过 manpages、头文件、内核文档、Google 之后,我不得不相信这个东西真的没文档!于是只好看源码了……还好有 LXR,不用在本地近一个G的源码里搜索。

所以,翻完文档又试验,最终有了这个程序:

#include<sys/ioctl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<linux/input.h>

#define EVENT_FILE "/dev/input/by-path/platform-i8042-serio-0-event-kbd"

int main(int argc, char **argv){
  unsigned int codes[][2] = {
    {0xd2, 107}, //Insert -> End
    {0xcf, 110}, //End -> Insert
    {0, 0},
  };
  int fd = open(EVENT_FILE, O_RDONLY);
  if(fd < 0) {
    perror("open " EVENT_FILE);
    exit(1);
  }

  unsigned int (*p)[2];
  for(p=codes; *p[0]; p++){
    if(ioctl(fd, EVIOCSKEYCODE, *p)) {
      perror("ioctl EVIOCSKEYCODE");
      exit(1);
    }
  }
  return 0;
}

那个EVENT_FILE当然就是笔记本键盘的节点啦,在/dev/input/by-path下很容易识别的。

编译之后,每次启动系统后执行一次就可以了(大概)。

对了,顺便说一下,找那些代码可以用 showkey 和 getkeycodes 之类的命令。当然我们有 setkeycodes,但是它不能为指定键盘单独设置。传进去的参数,第一个整数是 scancode,就是硬件上报的编码,第二个是 keycode,内核给键的编码,不同硬件的不同 scancode 可以对应同一个 keycode(比如本文所做的)。然后文本终端还有 keymap、X Window 还有键盘布局和 xmodmap,大概是把 keycode 映射到可读的键名。

7
26
2015
1

一个简单的 zsh 模块

曾经,我让 Awesome 收养孤儿进程,以保持一个清晰的进程树。后来我又想让 zsh 也做这个 prctl 系统调用,免得子进程 fork 之后跑太远。比如 Wine 跑起来就好多个服务进程,如果不能把它们全部关掉的话,再启动另一个版本的 Wine 会出问题的。而当我启动好些个不同版本的 Wine 环境之后,只看到 Awesome 下边挂了一堆 Wine 的进程,却不知道哪些属于哪个 Wine 环境的了。

zsh 本身并不支持做这个调用,不过如同 Python 和 Lua 一样,zsh 也可以通过共享库来扩展功能。不同的是,zsh 模块是没有文档的……好在 zsh 源码里提供了一个 example 示例模块。把它改改就有了以下代码:

把这两个文档保存到 zsh 源码目录的Src/Modules下,可能还需要编辑一下config.modules文件,然后编译就可得 subreap.so 文件。把这个文件放到/usr/lib/zsh/$ZSH_VERSION/zsh/subreap.so然后就可以用了:

zmodload zsh/subreap
subreap

模块加载之后,多了个subreap内建命令。不带参数即调用prctl(PR_SET_CHILD_SUBREAPER, 1),这样不管其子进程怎么 fork,都会在此 zsh 的进程树之下。使用subreap -u来取消这个设置。

如果你不想编译而又是 Linux 64 位系统,可以试试我编译好的版本:下载地址, 签名, SHA1: 09eb1cc9ebf6ec1e681641c0a60f57425cbb1e8c。

Category: Linux | Tags: linux zsh C代码
6
21
2015
0

在用户命名空间中运行 LXC 虚拟机

不用 sudo 也可以跑 LXC 虚拟机啦。使用 root 权限的 LXC 虚拟机,里边的 root 权限就是真实的 root 权限,虽然不太能够跑出来。而利用用户命名空间来启动的普通权限的 LXC 虚拟机则只在那个虚拟机里有 root 权限,从外面看跟一普通用户一样的。

首先需要一枚启用了CONFIG_USER_NS的内核。使用以下命令查看:

zgrep USER_NS /proc/config.gz

部分发行版会默认禁用用户命名空间功能,需要手动启用,参见 vagga 的安装文档。而 Arch Linux 不喜欢给软件打补丁,而这个特性又被认为是不安全的,所以并没有启用。当然这并不妨碍自己编译一个启用了这个特性的内核啦,比如 linux-lily 从 4.0.1 开始启用此特性。

注意:这个特性被认为不安全的,会时不时地爆出个提权漏洞(比如前不久这个),请谨慎启用。

内核支持没问题的话就可以开始配置了。以下配置过程主要参考 Arch Linux 论坛里的这篇帖子

首先给自己配置一些子 UID 和子 GID,也就是自己的分身。我在/etc/subuid/etc/subgid内写下如下内容

lilydjwg:100000:65536

意思是说,我(lilydjwg)被授权使用从 100000 开始的 65536 个 UID 和 GID。这一步是需要 root 权限的。这个配置好之后就可以创建用户命名空间了,比如:

lxc-usernsexec -m u:0:100000:1 -m g:0:1000:1 -- /bin/zsh

此命令是说,创建一个用户命名空间,其中 UID 从 0 开始,实际对应于外边 100000 开始的 UID,总共分配一个;GID 从 0 开始,实际对应于外边 1000 开始的 GID,总共分配一个。执行之后可以看到新启动的 zsh 已经是 root 权限了。不过cat /etc/shadow就会发现还是没权限 :-D 在里边 touch 个文件的话,在外边看会是 UID 为 100000 的用户创建的。我之所以要指定 GID 的映射,是因为我的 HOME 目录外人读不了的。为了加载 zsh 的配置,就把自己的 GID 映射给它了。

当然我也可以把自己的真实 UID 映射过去,这样子除了被里边的进程自认为有 root 权限之外没什么别的差异。用户命名空间要配合别的命名空间一起用才有意思。

然后要配置一下 cgroup,不然 lxc 会报错的。这一步也是需要 root 权限的。

echo 1 | sudo tee /sys/fs/cgroup/cpuset/cgroup.clone_children

for d in /sys/fs/cgroup/*; do
    sudo mkdir $d/$USER
    sudo chown -R $USER: $d/$USER
done

用处后边再说。

虚拟机里的网络是分开的。默认是没有网络的。想要的话得先授权,向/etc/lxc/lxc-usernet文件里写入

lilydjwg veth br0 10

其中br0是桥接用的网络接口名。没有就自己建一个:

brctl addbr br0
ifconfig br0 192.168.57.1
iptables -t nat -A POSTROUTING -s 192.168.57.1/24 -j MASQUERADE

这些当然也是需要 root 权限的。

还要告诉 LXC 使用用户命名空间:在~/.config/lxc/default.conf写入:

 lxc.include = /etc/lxc/default.conf
 lxc.id_map = u 0 100000 65536
 lxc.id_map = g 0 100000 65536

然后,去弄一个 LXC 系统镜像吧:

lxc-create -t download -n lxcname

名字自己起。这个命令会让你选择你要的发行版和版本的。这一步不需要 root 权限了。镜像文件列表可以看这里

等它跑完之后新的 LXC 虚拟机的 root 文件系统已经就绪了。不过在启动它之前先去编辑一下它的配置文件,加入网络配置。默认它位于~/.local/share/lxc下与 LXC 虚拟机同名的目录下。

在配置文件里加上

lxc.network.type = veth
lxc.network.link = br0
lxc.network.flags = up
lxc.network.ipv4 = 192.168.57.4
lxc.network.name = eth0

在启动之前还要做一件事——将当前进程加入到之前创建的 cgroup 中:

for d in /sys/fs/cgroup/*; do echo $$ > $d/$USER/tasks; done

然后就可以启动 LXC 虚拟机啦。当然是不需要 root 权限的:

lxc-start -F -n lxcname

当然,得给里边的 root 用户设置一个密码,不然登录不了的。可以使用 lxc-usernsexec 来 chroot 过去:

lxc-usernsexec -- chroot rootfs /bin/bash
Category: Linux | Tags: lxc linux
6
7
2015
18

Linux 系统接收通过蓝牙传输的文件

首先安装 bluez 包。我用的版本是 5.30。其次安装 blueman。

启动蓝牙服务:

systemctl start bluetooth.service

然后使用 blueman-manager 之类的命令启动 blueman。这时会在系统托盘看到蓝牙图标。点右键选择「添加新设备…」,完成配对。

然后,如果是要往手机发文件的话,是没有问题的,但是收的话,会失败。原因是,默认接收文件前会先询问用户要不要接收,而 blueman 不知道怎么搞的根本没反应……

解决方案是:直接启动一个默认接收文件的 obexd 就好了:

killall obexd
/usr/lib/bluetooth/obexd -r tmpfs -n -a

-r指定收到的文件存哪里,默认是 $XDG_CACHE_DIR 下的 obexd 目录,即默认是 ~/.cache/obexd。这里的路径是相对于用户主目录的。

-n是不要以守护模式运行,会把日志输出到终端而不是系统日志。

-a就是重点——接收所有文件——了。

obex 这套东西的文档在/usr/share/doc/bluez/dbus-apis/下有。

Linux 下遇到点问题还真是折腾,声称完成某一功能的软件一大堆,结果装好了,要么根本不知道怎么用(gnome-bluetooth、bluedevil),要么适用版本不匹配(obexpushd、ArchWiki 等网上的过时信息),要么有 bug 用不了(blueman)。

不过好的一点是,不涉及闭源的软件和协议,而又有足够的时间和能力的话,问题总是能够解决的。不像 Windows 或者 Android,遇到问题两眼一摸黑,只能不断地重试和重装,看看人品会不会爆发一下。

最后,折腾好久终于传输成功的照片:

雨后彩虹

北京好不容易下了场大雨,没想到雨后还出现了彩虹~

Category: Linux | Tags: linux Android 蓝牙
6
1
2015
5

Wireshark 抓远程主机的包

(失眠了,干脆起来写文。)

调试时经常会有抓包的需求。通常,我在本地用图形界面的 Wireshark 来抓包及解析,而对于远程服务器,因为没有图形界面,只好使用 tcpdump 抓包到文件然后复制到本地拿 Wireshark 看了,这样就不能实时查看抓到的包了。当然 tcpdump 也可以实时输出,但是信息太少、难以阅读,功能也过于简单,比如我要跟踪流啊、不同的流用不同的颜色高亮啊、添加注释啊、时序分析啊,tcpdump 完全没办法做到。实际上复杂一点的协议解析它都做不到。

一直没去研究 Wireshark 如何从标准输入读取网络包数据。大概是某天下意识地按了一下Alt-h看到了 Wireshaark 的 man 手册,才知道原来 Wireshark 支持这么多参数!图形界面的程序支持各种可选参数的可不多见。Wireshark 指定-i -就可以从标准输入读取数据,不过要同时指定-k,不然得在图形界面里点「Start」开始抓包。

那怎么把抓到的数据包发送到标准输出呢?实际的抓包操作不是 Wireshark 直接执行的。Wireshark 又不是 360,既然能以普通用户身份执行需要特权的操作,那么就会有一个无图形界面的工具来辅助。它就是 dumpcap。查阅其 man 手册可知,把抓到的数据输出到标准输出的选项是-w - -P-P 指定使用 pcap 格式,不然会使用 pcap-ng 格式,Wireshark 不认)。还可以给定其它选项,比如只抓 lo 网络设备上的包用-i lo,或者指定一个过滤器如-f 'port 1234'(具体语法见pcap-filter的 man 手册)。一定要记住不要把传输抓包数据的数据包也抓到了哦~

比如:

ssh lxc-debian sudo dumpcap -P -w - -f "'not port 22'" | \
  wireshark -i - -k

这样就可以实时看到远程主机上的网络包了~通过 ssh 执行命令时引号得用双层的。使用 sudo 是因为我那个 Debian 的 dumpcap 没有特权。

Category: Linux | Tags: linux 网络 wireshark
5
17
2015
12

Linux 下在 Minecraft 里输入中文

Linux 下各种奇怪的地方总是会遇到输入启用不了的情况,比如 Sublime Text 就需要打补丁版。Teewords 以前能好好地使用输入法的,不知从什么时候起,启用输入法输入时,直接从键盘输入的编码和输入法提交的字符串都会被 teewords 接收并显示(teewords 版本号 0.6.3)。最近换了新本子,跑起 Minecraft 终于不那么卡了,所以也研究了一下怎么在 mc 里输入中文。

喵窝 wiki 里给出了一个脚本,是通过外部程序输入中文,然后粘贴到 mc 里来达到输入中文的效果的。然而粘贴功能在我的 mc 里是无效的。不过照着这思路,改进一下发现也能用。

首先,需要 xdotools。其次,需要一个输入文本的程序。我使用的是 zentiy,当然还有 kdialog、gdialog 之类的也可以用。最后,需要设置快捷键。

我的脚本如下:

#!/bin/bash -e

chars=$(zenity --title 中文输入 --text 中文输入 --width 500 --entry 2>/dev/null)
sleep 0.1
xdotool key --delay 150 Escape t
sleep 0.2
xdotool type --delay 150 "$chars"
xdotool key Return

原理很简单,在这个脚本被调用时,弹出一个对话框让用户输入文字。对话框关闭后,焦点应该回到 mc。发送 Escape 键「回到游戏」,然后发送「t」开启聊天。然后把文字发送过去并按回车。

很神奇,原来可以直接向它发送中文字符。不过那些延迟是需要的,不然会接收不完整。所以使用效果就是,对话框关闭之后,可以看到程序在往 mc 里一个个地输入文本并发送~

至于绑定快捷键,作为 Awesome 用户,可以做到只在 mc 的窗口绑定。定制性比较差的窗口管理器/桌面环境可能只能全局绑定了,会占用掉一个全局快捷键。

相关代码如下:

    elseif c.class and c.class:match('^Minecraft ') then
        local keys = c:keys()
        local mykey = awful.key({'Control'}, 't', function(c)
            awful.util.spawn('zhinput')
        end)
        keys = awful.util.table.join(keys, mykey)
        c:keys(keys)
    elseif c.name == '中文输入' then
        awful.util.spawn_with_shell('sleep 0.05 && fcitx-remote -T', false)

顺便在弹出的对话框里把输入法切换到了中文模式~(完整的配置文件在这里

Category: Linux | Tags: 中文支持 minecraft
5
11
2015
27

为什么我反对普遍地静态链接?

这是我查看知乎私信时不小心瞅到的问题所触发的。由于 Go 在国内的兴起,我对这个问题也多有思考,就放在这里记录一下好了。知乎的链接我就不贴了,带 nofollow 的都懒得贴了。

首先,我们搞清楚问题是什么。或者说,我反对的究竟是什么?

静态链接,即早期唯一的一种链接出二进制可执行文件的方式,把所有程序需要用到的库全部打包到一个文件里边。后来,由于存储空间越来越不够用,所以发展出了动态共享库,也就是把库编译成 so、dll 或者 dylib 这种由动态库装载器在程序运行前或者运行时进行链接的方案。

静态链接的优点

  1. 方便分发,不会因为库的升级而导致程序无法运行。这一点没有严格指定依赖版本的 Arch Linux 用户应该都有所体会。当你更新某个库(比如 boost 或者 icu 什么的)之后,动态链接到旧库的程序会出错。

  2. 效率稍高。这个反正人类是体会不到的。

基于第一点,用于急救的重要程序最好使用静态链接,特别是 busybox。以前我会安装 busybox 的,后来因为 Arch 改用动态链接 C 库了,对于我不再有意义,所以卸载了。以后 C 库如果出问题就直接重启进救援系统了。

另外我还有静态链接的 32 位 Vim,为的是在 Vim 依赖库更新而 Vim 没更新时依旧有个顺手的编辑器可用。我一直自行编译 Vim 因此这个曾经十分有用。不过由于现在对经常变动导致问题的解释器支持采用运行时动态链接,所以基本不受影响了。

静态链接的缺点

这个可以列出长长的一串了。

  1. 占用磁盘空间。我就不怎么喜欢 Haskell 写的程序,太占硬盘了,一个程序就几十M。当然换新电脑之后目前硬盘空间有富余。但是它们还是会渐渐被我的各种源码和虚拟机什么的填满的。

  2. 占用内存空间。可执行文件在执行时是需要映射到内存中的。如果使用动态链接,那么因为是同一文件,所以在内存时只需要映射一份就可以了。而静态链接,不仅因为来源于不同的文件而需要加载、映射多次,而且因为来自于不同的构建等原因,逻辑上相同的代码往往并不会造成映射之后的内存页相同,使得内存去重机制(如 UKSM)失效。

    别说内存是白菜价,除非你来给我手上的笔记本、VPS、服务器、路由器、单板机等都配置个几十G的内存,我付给你等质量的白菜。还记得比尔·盖茨说过的话吗——「640K足够了」。够了吗?

  3. 占用 I/O 带宽。可执行文件越大,在内存里没有缓存时需要从外存读取的数据也就越多,耗时也就越长。而因为文件体积增大,内存资源越发不够用,I/O 缓存越少,导致缓存命中更低。

  4. 占用网络带宽。你可执行文件是从网上下的吧?你在国内看个视频还挺流畅,但是到世界各地去下软件你试试看?

  5. 运行时链接。我写了一个程序,支持 MySQL、SQLite3、PostgreSQL、MongoDB、Oracle 等等等等数据库。但是你显然不会用到所有的数据库支持吧?那你为什么要所有这些数据库的连接库来用一个 SQLite3 数据库的功能呢?使用运行时链接(dlopen 那些函数),程序可以在运行的时候动态判断并加载它此次运行所需要的动态库。

  6. 升级。openssl 爆出了一个很严重的安全漏洞,已经被修复了,你怎么办?当然是升级呗。那你希望是更新一个几M的包然后重启服务器解决问题,还是下载好几百M的程序、更新每一个你所用到的使用了 openssl 的程序?更何况那些程序本身不一定都更新了,也许为了安全你得自行编译其中的很大一部分(你可以期望有一个安全团队在半夜爬起床去更新一个软件,但是你觉得上千项目的开发者都会这么做吗)。你也不一定能够找到所有静态链接了有漏洞的 openssl 版本的程序,万一漏掉一个,你整个服务器的安全性就没了(所以 openssl heartbleed 漏洞更新之后建议是重启系统而不是重启各服务)。

更别说更底层的库了,比如 C 库或者 C++ 库。不至于人家更新了一行代码,你就要重装整个系统吧?

静态链接有它自身的用处,但是它并不适合所有情况,甚至并不适合大多数情况。动态链接以其微小的运行效率损失为代价,为不论是最终用户还是开发者、打包者提供了更为优秀的库管理方案。之所以很多人看到静态链接相对于动态链接的优势,我认为还是因为他们没什么机会看到静态链接、尤其是大量静态链接会带来的问题。

你不需要把程序都静态链接。你需要的只不过是一个优秀的包管理器和维护团队而已

Category: Linux | Tags: linux 编译 go 编程语言
5
11
2015
12

使用 bcache 自制「混合硬盘」

换了新本子,外存是1T机械硬盘和16G固态硬盘。这16G SSD 速度挺快的尤其是读的时候,可它拿来放 / 都不够呢,于是拿来作缓存加速。根据局部性原理,虽然数据很多,但是最常访问的只占其中一小部分呢。

搜索的结果是有三个方案:bcache、dm-cache 和 Facebook 的 flashcache。前两者在官方内核里,不需要另外安装。我是最先在 Arch Wiki 上看到 bcache 的,后来又看 dm-cache,发现需要自己指定元数组的存储什么的,略复杂。而且一些评测显示 bcache 性能要好一点,所以就它了。

$$ \require{extpfeil} \rm{SSD} + \rm{HDD} \xlongequal{\rm bcache} \rm{SSHD} $$

配置起来其实很简单。首先安装 AUR 里的 bcache-tools,然后创建存储数据的分区和用于缓存的分区:

make-bcache -B /dev/sda2
make-bcache -C /dev/sdb1

教程上使用的是 SSD 的分区。换成 SSD 的块设备本身应该也可以。

参数什么的我没调。然后是把缓存设备的 UUID 写到 /sys/block/bcache0/bcache/attach 里。

为了最优性能,往 /sys/block/bcache0/bcache/cache_mode 里写入「writeback」来更改其缓存策略为「写回」。默认是「写通」(writethrough),也就是写的时候同时写缓存和后端设备,不会在缓存出问题时丢数据,但是会慢。另一个可选的策略是「writearound」,不知道该怎么译,是只写到后端设备而不写缓存的。最后一个是「none」,不知道用了它会发生什么……

换出策略使用默认的 LRU(最近最少使用)。剩下的两个(FIFO 和随机)应该效果没 LRU 好。

这些设备是会记住的,无需在启动时重启配置。至少我用的 4.0.1 内核是这样。

弄好之后就可以折腾 /dev/bcache0 这个块设备了。我放弃了之前使用文件级的 eCryptfs,改用在备份里使用得挺爽的块设备级的 dm-crypt,然后才格式化成 ext4。也就是:

$$ 文件数据 \xrightarrow{\textrm{I/O相关系统调用}} \rm{ext4} \xrightarrow{加密} \textrm{dm-crypt} \xrightarrow{缓存} \rm{bcache} \xrightarrow{写入} \rm{SSD} + \rm{HDD} $$

所以我的 /etc/mkinitcpio.conf 里要加上 bcache 和 encrypt 两个 hook:

HOOKS="base udev autodetect modconf block bcache encrypt filesystems keyboard fsck resume"

(不过这样子不能用外接 USB 键盘输入密码的。)

然后 mkinitcpio -p linux 一下,生成新的 initramfs 镜像。

为了共享缓存,我把 / 和 /home 放一起了(不过我猜对 /dev/bcache0 进行分区也是可以的?)。虽然这样子整个 / 用去了60多G空间,但是缓存的命中率还是非常高的——

>>> bcache-status -a
--- bcache ---
Device                      /dev/bcache0 (254:0)
UUID                        07a9b6a5-7f18-4950-84d6-c90abaaf65dc
Block Size                  0.50KiB
Bucket Size                 512.00KiB
Congested?                  False
Read Congestion             2.0ms
Write Congestion            20.0ms
Total Cache Size            14.91GiB
Total Cache Used            14.91GiB    (100%)
Total Cache Unused          0B  (0%)
Dirty Data                  0.50KiB     (0%)
Evictable Cache             14.17GiB    (95%)
Replacement Policy          [lru] fifo random
Cache Mode                  writethrough [writeback] writearound none
Last 5min Hits              439 (92%)
Last 5min Misses            38
Last 5min Bypass Hits       424 (100%)
Last 5min Bypass Misses     0
Last 5min Bypassed          61.50MiB
Last Hour Hits              46003       (88%)
Last Hour Misses            6051
Last Hour Bypass Hits       94043       (100%)
Last Hour Bypass Misses     0
Last Hour Bypassed          400.00MiB
Last Day Hits               79485       (88%)
Last Day Misses             10214
Last Day Bypass Hits        170383      (100%)
Last Day Bypass Misses      0
Last Day Bypassed           602.00MiB
Total Hits                  79485       (88%)
Total Misses                10214
Total Bypass Hits           170383      (100%)
Total Bypass Misses         0
Total Bypassed              602.00MiB

bcache-status 脚本来自这里

感觉还挺快的,特别是各种程序如火狐、gvim、pidgin、zsh 的启动速度,以及 mlocate、pacman 的搜索速度都非常快。没有对比数据,因为我没有试过在这个本子上不用 bcache 的情况下把系统弄起来。之前的旧本子可能因为分区太满导致碎片严重,所以 I/O 性能很差劲的。

Category: Linux | Tags: linux Arch Linux bcache
3
4
2015
7

Awesome 的 GitHub 今日贡献指示器:今天你 push 了吗?

GitHub 用户页有个 calendar,花花绿绿的甚是好看。不过,经常一不小心断掉了几十天的 steak 着实可惜,特别是用了私有仓库之后,自己看,有贡献,可别人看不到那些私有贡献的呀。其实要维持 steak 也不难,一个小小的提交就足够了——只要我知道我今天还没 push 什么公开的东西的时候。

当然啦,写个脚本天天推个无意义的更新挺容易的,但那样就没有乐趣了不是吗?一天快结束的时候发封邮件提示一下自己不错,但可能已经来不及了。而且这种事情还是随意点好,太刻意了就不好玩了,所以不需要那么强的提醒。弄一个简单的指示器在 Awesome 面板上正好。

效果图(指示器在右上角):

GitHub 今日贡献指示器的 Awesome 桌面

如果这天没有贡献(公开的提交或者 issue 等),那么这只 Octocat 就会失去色彩(变成灰度图)。

代码已上传至 myawesomerc 仓库。以下是实现细节:

首先,创建一个显示图片的 widget:

-- {{{ GitHub contribution indicator
github_contributed = awful.util.getdir("config") .. "/image/github_contributed.png"
github_not_contributed = awful.util.getdir("config") .. "/image/github_not_contributed.png"
github_widget = wibox.widget.imagebox()
function update_github(has_contributions)
    if has_contributions then
        github_widget:set_image(github_contributed)
    else
        github_widget:set_image(github_not_contributed)
    end
end
update_github(false)
-- }}}

-- 在 wibox 中添加这个 widget,需要放到正确的地方:
right_layout:add(github_widget)

函数update_github是给外部脚本用的。不可在 Awesome 配置里直接发起 HTTP 请求,会阻塞的!

当然,还要准备前两行代码提到的图片。从这里下载 Octocat 的图片,并做成彩色和灰度小图:

convert -resize 48x48 -background white -alpha remove Octocat.png github_contributed.png
convert -resize 48x48 -background white -alpha remove -colorspace Gray Octocat.png github_not_contributed.png

把图片放到相应的地方。然后写个脚本来更新这个指示器的状态,也就是获取数据之后再通过 awesome-client 调用update_github函数了。

#!/bin/bash -e

github_contributed () {
  count=$(curl -sS "https://github.com/users/$USER/contributions" | grep -oP '(?<=data-count=")\d+' | tail -1)
  [[ $count -gt 0 ]]
}

get_display () {
  if [[ -n "$DISPLAY" ]]; then
    return
  fi

  pid=$(pgrep -U$UID -x awesome)
  if [[ -z "$pid" ]]; then
    echo >&2 "awesome not running?"
    exit 1
  fi

  display=$(tr '\0' '\n' < /proc/"$pid"/environ | grep -oP '(?<=^DISPLAY=).+')
  if [[ -z "$display" ]]; then
    echo >&2 "can't get DISPLAY of awesome (pid $pid)"
    exit 2
  fi

  export DISPLAY=$display
}

get_display

if github_contributed; then
  s='true'
else
  s='false'
fi

echo "update_github($s)" | awesome-client

GitHub calender 目前是个 SVG 图片,位于https://github.com/users/用户名/contributions

awesome-client 需要设置正确的DISPLAY环境变量才可以使用。这里使用pgrep取当前用户的 awesome 进程里的DISPLAY变量值。ps命令不太好用,不能同时指定多个条件。

万事俱备,只需要拿 cron 或者 systemd.timer 定时跑跑这个脚本就可以啦~


2015年3月14日更新:update_github脚本改用 Python 实现了,更好的错误处理(不会因为网络问题而认为今天没有贡献了),也改用当前日期而非 GitHub calender 的最后一个方块。更新后的脚本在 GitHub 上

Category: Linux | Tags: shell awesome github

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