11
8
2014
6

在 Arch Linux 下安装 openSUSE LXC 虚拟机

前边,我已经尝试过在 Arch Linux 下安装 Funtoo在 Arch 中安装 Arch 就更简单了。为了测试,我还通过 Aufs 来将我的 Arch Linux 在 LXC 里复制一份。至于安装个 Debian 或者 Ubuntu LXC,由于有在任何 Linux 下都可以跑的 deboostrap,安装起来也十分容易。

现在难题来了:在 LXC 里安装一个 openSUSE。LXC 自带了个 openSUSE 模板,但是它需要 zypper 等。虽然说 AUR 里就有 zypper 和 libzypp(还都是 git 版本的,下载很耗时的),不过还是不怎么够呢。经过尝试,我发现需要以下包来运行这个模板:

  • Arch 里有的:augeas
  • Arch 里没的:libsolv-tools build rpm libzypp zypper

有的就直接安装啦。没有的,可以从它的软件源下载。x86_64 架构的在这里,那个 build 是 noarch 的,在这边

把它们全部下回来,拿 7z 解压能够得到 cpio 档。然后建立个目录并 cd 过去,使用以下命令解开:

$ mkdir t
$ cd t
$ for f in ../*.cpio; do cpio -id < $f; done

这样就把它们解压到目录t里边的。我没有把它们解压到/,因为我不想弄乱我的系统,即使能够清理也是相当麻烦的,而且一不小心还可能删错文件。

所以,又该 Aufs 上场啦。当然在此之前还有件事:openSUSE 没有进行/usr合并。所以要手动去把binusr/sbinsbin等目录下的文件移动到usr/bin下,然后删掉那些目录;把usr/lib64下的文件移动到usr/lib下,并删掉usr/lib64

然后就可以将这个目录和我的 Arch Linux 合体啦:

$ mkdir root
$ sudo mount -t aufs -o br:$PWD/root=rw:$PWD/t=ro:/=ro aufs root

但是!这样子的话,新装好的 openSUSE LXC 会在这个root目录里呢。所以要把外边真实的 LXC 目录给 bind mount 过来。我使用了自定义的 LXC 路径,所以是这样子的:

$ sudo mkdir -p root/ldata/media/temp/lxc
$ sudo mount --bind /ldata/media/temp/lxc root/ldata/media/temp/lxc

然后编辑一下 openSUSE 的模板,搜索「http」把软件源的链接全部改到中国的镜像:

$ sudo vim root/usr/share/lxc/templates/lxc-opensuse

我使用的是中科大的源镜像。

一切就绪,开始安装~

$ sudo chroot root /usr/bin/lxc-create --lxcpath=/ldata/media/temp/lxc -n opensuse -t opensuse

耐心等待哦。最终安装完成根文件系统的大小是 333MiB。

安装完毕之后卸载刚刚挂载的那些东西:

$ sudo umount -R root

然后编辑一下自动生成的 LXC 配置文件,比如属改改网络什么的。以下是我改过的配置文件:

# Template used to create this container: /usr/share/lxc/templates/lxc-opensuse
# Parameters passed to the template:
# For additional config options, please look at lxc.container.conf(5)
lxc.rootfs = /ldata/media/temp/lxc/opensuse/rootfs
lxc.utsname = opensuse
lxc.autodev=1
lxc.tty = 4
lxc.pts = 1024
lxc.mount.entry = run run tmpfs rw 0 0
lxc.mount.entry = tmp tmp tmpfs rw 0 0
lxc.mount.auto = proc sys
lxc.cap.drop = sys_module mac_admin mac_override mknod sys_time
lxc.kmsg = 0

# When using LXC with apparmor, uncomment the next line to run unconfined:
#lxc.aa_profile = unconfined

#networking
lxc.network.type = veth
lxc.network.link = br0
lxc.network.flags = up
lxc.network.ipv4 = 192.168.57.6
lxc.network.name = eth0

lxc.cgroup.devices.deny = a
# /dev/null and zero
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
# consoles
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 4:0 rwm
lxc.cgroup.devices.allow = c 4:1 rwm
# /dev/{,u}random
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 136:* rwm
lxc.cgroup.devices.allow = c 5:2 rwm
# rtc
lxc.cgroup.devices.allow = c 254:0 rm

当然别忘记修改 root 密码啦:

$ sudo chroot /ldata/media/temp/lxc/opensuse/rootfs /bin/passwd

不过默认会有两个 getty 进程跑在 console 上边。要稍微修改一下。顺手把多余的 tty 上的也关掉好了:

$ sudo rm /ldata/media/temp/lxc/opensuse/rootfs/etc/systemd/system/getty.target.wants/getty@tty*
$ sudo mv /ldata/media/temp/lxc/opensuse/rootfs/etc/systemd/system/console-{shell,getty}.service

然后就可以启动啦:

$ sudo lxc-start -n opensuse --lxcpath=/ldata/media/temp/lxc

会默认启动 sshd,所以直接 ssh 连过去就可以用啦=w=

PS: 这个模板默认安装的是 openSUSE 12.3。记得自己改改或者装好后升级一下。

Category: Linux | Tags: lxc aufs linux Arch Linux openSuse
9
29
2014
4

使用 Python 自制 expect 功能

Tcl 的 expect 工具是一个十分有用的自动化工具,经常被用来喂 ssh 密码什么的。不过配置 ssh Control Master英文介绍, 中文翻译之后,我发现第一次连接之后退出,expect 脚本不肯退出了,而且Ctrl-C什么的都不管用,除非我杀掉实际连接到远程主机的 ssh 控制进程。很显然,这是因为 ssh fork 出来的 ssh 进程依旧保持着伪终端的打开状态,expect 依旧在等待来自其上的输出。

研究之后,我发现虽然我可以给我的 expect 脚本加上命令行参数的处理之类的功能,但是interact之后必须等待伪终端关闭没有办法绕过。罢了,本来对 Tcl 就不熟,拿 Python 重写一个吧。

Python 也有个类似的库,叫 pexpect,不过上次我看它的时候还只支持 Python 2,而且使用起来似乎有点问题。所以干脆自制一个满足自己需求的好了。

程序不长,一百多行,但也比预期的长了不少。主要都是些终端和文件描述符的处理。哦还有信号。

我以是否存在子进程来作为判断是否结束interact的标志,而不管是不是还有进程在用这个伪终端。在收到SIGCHLD信号时使用waitid系统调用可以清理任意一已终止的子进程,使用WNOHANG标志在有子进程但是没有已退出者时不要等待、直接返回。于是,在有子进程退出时,反复调用waitid直到它报错就说明已经没有子进程存在啦。

另一个需要处理的信号是SIGWINCH,就是终端大小改变时以此终端为控制终端的前台进程会收到的那个信号。当脚本所在的终端大小改变时,需要重新设置脚本创建的伪终端的大小。不知道为什么,pexpect 竟然没有自己处理这个。

代码照旧在 GitHub 上。以下是使用示例:

#!/usr/bin/env python3

import sys

import expect

def main(host):
  p = expect.Expect()
  p.spawn(['ssh', host])
  p.expect_line('# ')
  p.send('. ./tide\n')
  p.send('TERM=screen-256color exec zsh -l\n')
  p.interact()

if __name__ == '__main__':
  host = 'phone'
  if len(sys.argv) == 2:
    host += sys.argv[1]
  main(host)

这是之前有问题的 expect 脚本:

#!/usr/bin/expect -f

set host [if {$argc eq 0} {lindex phone} {lindex phone$argv}]
spawn -noecho ssh $host
expect "*# "
send ". ./tide\n"
send "TERM=screen-256color exec zsh -l\n"
interact

这是我的 ssh control master 相关配置:

ControlPath ~/.ssh/master-%r@%h:%p
ControlMaster auto
ControlPersist yes
Category: Linux | Tags: linux python 终端 expect tcl
9
2
2014
14

Arch Linux 自动连接可用无线网络

Arch Linux 连接网络可以使用其官方开发的 netctl 系列命令行工具。要想在开机(以及从挂起/休眠状态唤醒)时自动连接到可用的无线网络,以下是设置步骤。

首先,你得告诉 Arch Linux 你知道哪些无线热点。Arch Linux 不会自动帮你破解别人的 Wi-Fi 密码的。就算 Wi-Fi 热点没有加密,你不说 Arch Linux 怎么知道它应当连接到那个热点呢,也许那是个钓鱼用的热点也说不定哦。

cd 到 /etc/netctl 目录下,可以看到 examples 目录下有一堆示例配置。复制你所需要的配置文件到上一层目录(/etc/netctl)。比如绝大多数 Wi-Fi 热点使用的是 WPA 加密,那就复制 examples/wireless-wpa 文件。目标文件名比较随意,起个方便自己的名字就行,比如 work、home 之类的。复制完成之后记得 chmod 600 禁止非 root 用户访问,因为配置文件里会包含你的 Wi-Fi 热点密码。

然后编辑配置文件,修改 ESSID 和 Key 为你的 Wi-Fi 热点 ID 和密码就可以了。之所以要先更改权限再编辑,是因为某些编辑器(如 Vim)会生成同权限的备份文件;那里有可能也会包含密码。可以放多份配置文件在这里,netctl-auto 默认会去找一个可用的连接。有多个可用的时候不太清楚它会连上哪一个,可以使用更复杂的配置文件来指定优先级(参见 examples/wireless-wpa-configsection 示例配置)。

配置文件写好之后,当然是启动相应的服务啦。Arch Linux 一贯的传统是不启动不必要的服务,除非用户说要启动之。netctl-auto 的 systemd 服务名是 netctl-auto@interface.service(当然 .service 后缀还是可以省略的)。interface 部分写你的无线网络接口的名字,可以通过 ip linkifconfigiwconfig 等命令看到。我禁用了 systemd 的可预测网络接口名称,所以我的无线网络接口名唤 wlan0。我使用如下命令启动服务:

$ sudo systemctl start netctl-auto@wlan0.service

如果一切顺利的话一小会儿之后就应该连上网了:

$ systemctl status netctl-auto@wlan0.service
● netctl-auto@wlan0.service - Automatic wireless network connection using netctl profiles
   Loaded: loaded (/usr/lib/systemd/system/netctl-auto@.service; enabled)
   Active: active (running) since 二 2014-09-02 20:23:31 CST; 2h 45min ago
     Docs: man:netctl.special(7)
  Process: 340 ExecStart=/usr/bin/netctl-auto start %I (code=exited, status=0/SUCCESS)
   CGroup: /system.slice/system-netctl\x2dauto.slice/netctl-auto@wlan0.service
           ├─402 wpa_supplicant -B -P /run/wpa_supplicant_wlan0.pid -i wlan0 -D nl80211,wext -c/run/network/wpa_supplicant_wlan0.conf -W
           ├─404 wpa_actiond -p /run/wpa_supplicant -i wlan0 -P /run/network/wpa_actiond_wlan0.pid -a /usr/lib/network/auto.action
           └─501 dhcpcd -4 -q -t 30 -K -L wlan0
...

或者通过 netctl-auto list 命令也可以看到连接上了哪个配置文件里指定的热点。

如果满意的话,就让它开机自启动啦:

$ sudo systemctl enable netctl-auto@wlan0.service

参考资料:ArchWiki 上的 netctl 条目

Category: Linux | Tags: linux 网络 Arch Linux
8
5
2014
9

CoolShell 解密游戏解答

这里有人给出了自己的解法,但是我不喜,所以有了本文。注意,本文中的 shell 代码均为 zsh。如果你在用 Windows,那建议还是不要玩了,那个对付这种事情太难用了。

可惜知道这个题晚了两天,没能进前X名 :-(

0. Fuck your brain

机器上没有 brainfuck 编译器,于是 Google「brainfuck online」,得到这个。贴进去运行即可。

1. Multiply

一个值是 42。另一个要观察数列。直接把数列贴到数列百科全书即可。然后乘起来。

2. Keyboard

这个也很容易,不是把 Dvorak 键盘当成 Qwerty,那就是把 Qwerty 当成 Dvorak 了。对照着 Dvorak 的键位输入下边那串字符串,得到一 C 源码。编译、运行之即可。

3. QR Code

扫码,得到:

[abcdefghijklmnopqrstuvwxyz] <=> [pvwdgazxubqfsnrhocitlkeymj]

是个字符映射关系。Python 有现成的函数来处理这个。也是有正反两种可能,都试试就可以了。

>>> T = str.maketrans('pvwdgazxubqfsnrhocitlkeymj', 'abcdefghijklmnopqrstuvwxyz')
>>> s = 'Wxgcg txgcg ui p ixgff, txgcg ui p epm. I gyhgwt mrl lig txg ixgff wrsspnd tr irfkg txui hcrvfgs, nre, hfgpig tcm liunz txg crt13 ra "ixgff" t
r gntgc ngyt fgkgf.'
>>> s.translate(T)
'Where there is a shell, there is a way. I expect you use the shell command to solve this problem, now, please try using the rot13 of "shell" to enter next level.'

按照提示执行命令:

rot13 <<< shell

Arch 上,rot13 命令位于 bsd-games 包。或者在 Vim 里把光标移动到「shell」单词上按g?aw也能得到结果。

题目开始有趣起来了~

4. cat

这个题目更有趣了。源码里一堆乱七八糟的数据。先把它们弄到一个单独的 Vim 缓冲区,然后找到所有的五字符回文字符串:

%!grep -oP '(.)(.)(.)\2\1'

不是所有回文都被接受。仔细观察示例可以发现,中间一定是个小写字母,左边一定是一字母一数字。但是过滤后还是有太多结果。限制左边的字母为大写字母之后可以得结果。删掉不符合条件的,然后把中间的字符连起来即可。

v/\v^([A-Z][0-9]|[0-9][A-Z])\l/d

5. variables

初看,提示莫名其妙。后来注意到图片链接到了有意思的地方。访问得到另一个数字「32722」。显然是要用这个数字放在 URL 上继续访问了。直接拿 shell 访问:

$ n=1024
$ while true; do n=$(curl -sS http://fun.coolshell.cn/n/$n); echo $n; done

访问上百次之后出现一句话,给出了下一关的地址。

6. tree

这关要求从一棵二叉树的中序和后序遍历中还原其最深的路径。不知道怎么做,直接 Google「reconstruct a binary tree from in-order and post-order」,看来有不少人都在做类似的东西啊。我看的是 LeetCode 上的这篇文章。有代码,但我懒得写程序把树画出来或者是找最深的了。反正这树也不大,懂得了方法,直接在 dia 里手工构建出来了。当然,我只构建了最深的那部分。SVG 导出图片

然后就是拿密码解那个字符串了。要注意的是,不要自己去解 base64,不然 openssl 报错的……

7. N Queens

八皇后问题的变种。我直接使用了 Rosetta Code 上的代码。当然要小改一下,直接输出结果而不是打印出图案:

main = mapM_ print $ queens 9

然后找到符合那个 SHA1 值的解就可以了:

$ ./queens | tr -d ',[]' | while read code; do [[ $(sha1sum <<<zWp8LGn01wxJ7$code | awk '{print $1}') == e48d316ed573d3273931e19f9ac9f9e6039a4242 ]] && echo $code; done

8. Excel Column

26 进制转十进制:

>>> def debase26(x):
...   return sum(26 ** i * (ord(d) - ord('A') + 1) for i, d in enumerate(x[::-1]))
...
>>> debase26('COOLSHELL') // base26('SHELL')
85165

结果得到的页面说要转回 26 进制。好吧:(可惜没能在一行内搞定)

>>> def base64(x):
...   L = []
...   while True:
...     x, d = divmod(x, 26)
...     if d == 0: break
...     L.append(d)
...   return ''.join(chr(x + ord('A') - 1) for x in L[::-1])
...
>>> base64(85165)
'DUYO'

9. Fraternal Organisation

这个我没能解出来 QAQ 这两个图片看起来有些莫名其妙。我没注意到图片的名字和鼠标放上去的小提示。最后是看前边那个链接里的答案才知道原来还有个「猪圈密码」-_-|||

PS: 最近博客访问和评论速度都挺慢的,请见谅。

Category: Linux | Tags: linux python shell fun
7
15
2014
30

在 Arch 里使用 KVM 装 Arch

准备

首先检查 CPU 支持。需要 CPU 支持虚拟化的。

grep -E "(vmx|svm|0xc0f)" --color=always /proc/cpuinfo

没输出就没戏了。现在的 CPU 一般都支持的。

然后是内核支持。

zgrep CONFIG_KVM /proc/config.gz
zgrep CONFIG_VIRTIO /proc/config.gz

官方内核是支持的。

最后是用户态软件。Arch Linux 一向不怎么分包,安装 qemu 这个包就可以了。

哦对了,要安装 Arch 的话,还要准备它的安装镜像。

开始啦

一切就绪。

先创建虚拟机所用的磁盘文件。

qemu-img create -f qcow2 ArchVM.img 15G

这样就创建了一个 15G 容量的 qcow2 格式虚拟磁盘文件。之所以选用 qcow2,是因为它支持「母镜像」功能,对应于 Virtual Box 的差分存储。

然后就可以启动系统了。为了避免老是输入一长串命令,遵循 Gentoo Wiki 的建议,我们创建一个脚本:

#!/bin/sh
exec qemu-system-x86_64 -enable-kvm \
       -cpu host \
       -drive file=$HOME/ArchVM.img,if=virtio \
       -netdev user,id=vmnic,hostname=archvm,hostfwd=tcp:127.0.0.1:2222-:22 \
       -device virtio-net,netdev=vmnic \
       -m 1G \
       -curses \
       -name "Arch VM" \
       "$@"

注意到这里我已经加上了hostfwd参数,将虚拟机的 22 端口映射到 host 的 2222 端口上,方便以后通过 ssh 连接。

我这里指定了-curses参数,它将虚拟机的显示器直接使用 curses 库显示在当前终端上。当然能显示的只有显示器处于文本模式的时候,图形模式就只能知晓当前分辨率了。因为我是在服务器上使用,所以加上这个参数。当然你也可以使用 VNC 去连。

然后执行命令:

./startvm -boot once=d -cdrom path_to_file.iso

首先从光驱启动一次(once=d),重启之后恢复到默认的从硬盘启动。

系统启动啦~然后就会发现引导器 isolinux 把显示器切换到图形模式了……

终端无法显示图形模式的内容

不过还好。Arch 的引导界面我们知道。按Tab,然后输入<Space>nomodeset并回车。不然待会进系统里,KMS 之后一直是图形模式就什么也看不到了。

然后进入系统安装啦。注意硬盘设备是/dev/vda。当然也要注意安装并让 sshd 在开机时启动,虽然说有 curses 模式的「显示器」也可以用。

装好之后、重启之前还要注意一点,把/boot/grub/grub.cfg包含gfxload_video之类的地方都去掉,不然会进图形模式的。

装好后就 reboot 吧。如果一切顺利的话就能看到已经安装好的 Arch 登录提示符了。

好不容易装好了系统,当然要把它作为母镜像,所有后续的修改放子镜像上啦:

qemu-img create -f qcow2 -b ArchVM.img ArchTest.img

然后修改一下启动脚本。以后就可以用./startvm脚本启动这个虚拟机啦。

参考文章

Category: Linux | Tags: linux kvm 虚拟机
5
15
2014
2

使用 udev 规则自动配置 IP 地址

udev 规则其实挺简单的,但第一次配置也颇费了一番工夫。

事情的起因是这样子的。我的手机,还有 Kindle Paperwhite,都能接电脑上提供一 USB 网络设备,可以用来 ssh 啊 rsync 啊什么的。但是呢,每次接好之后还要执行条命令设置 IP 地址,还要用 sudo、输入密码,很是麻烦。

我用来配置 IP 地址的命令是:

ifconfig usb0 192.168.42.1 # 手机
ifconfig usb0 192.168.15.1 # Kindle

查阅 udev(7) man 文档之后,对 udev 规则有了大概了解,知道大约要写成以下形式:

ACTION=="add", SUBSYSTEM=="net", XXX, RUN+="xxx"

需要一个属性来确定添加的设备是目标设备。插入设备,使用udevadm命令来检查设备的各种属性:

udevadm info --attribute-walk /sys/class/net/usb0

本来准备用 MAC 地址的,但后来才发现我这 Android 手机每次的 MAC 地址都不一样。想到 adb 用的序列号,于是我决定用ATTRS{serial}=="BX90345MWH"。然后轮到 Kindle 了。结果一看,竟然没有序列号属性了……但是它的 MAC 地址不会变,所以用 MAC 地址了。

写好规则之后可以先测试一下:

udevadm test /sys/class/net/usb0

配置正确的话会看到一行以run:开头的行里写着自己定义的命令。

没问题就让 udevd 重新加载规则文件:

sudo udevadm control --reload-rules

到这里似乎就该结束了。可事与愿违,测试都没问题了,但 IP 地址就是没出现。查阅各处文档,也没做错什么呀。后来才注意到测试时上边有一行输出:

run: '/usr/lib/systemd/systemd-sysctl --prefix=/proc/sys/net/ipv4/conf/usb0 --prefix=/proc/sys/net/ipv4/neigh/usb0 --prefix=/proc/sys/net/ipv6/conf/usb0 --prefix=/proc/sys/net/ipv6/neigh/usb0'

它使用的是绝对路径!想起 systemd 的命令必须是绝对路径,我尝试改成绝对路径,果然可以了:

ACTION=="add", SUBSYSTEM=="net", ATTRS{serial}=="BX90345MWH", RUN+="/bin/ifconfig %k 192.168.42.1"
ACTION=="add", SUBSYSTEM=="net", ATTR{address}=="ee:49:00:00:00:00" RUN+="/bin/ifconfig %k 192.168.15.1"
Category: Linux | Tags: linux udev
4
14
2014
6

Linux 3.14: 终于能方便地看到真正的系统可用内存了

直接取/proc/meminfo中的「MemAvailable」项即可:

awk '$1 == "MemAvailable:" { print $2 * 1024 }' /proc/meminfo | filesize

filesize 是我自己写的将字节数转成人可读形式的脚本。

使用free命令的版本:

free | awk 'NR == 3 { print $4 * 1024 }' | filesize

并不准确,因为已缓存(Cached)内存并不一定是可以释放的,比如我用的 tmpfs 里的数据也算进去了。详见内核的这个提交。「free命令的算法在十年前还不错」,这不就是我大学课程教授的知识所处的时代么? :-D

Category: Linux | Tags: Linux
3
14
2014
4

Linux 系统时间变更通知

每一次,系统从挂起状态恢复,系统日志里总会多这么几行:

systemd[1]: Time has been changed
crond[324]: time disparity of 698 minutes detected

一个来自 systemd,一个来自 dcron,都是说系统时间改变了。那么它们是怎么知道系统时间改变的呢?

dcron 的代码很少,所以很快就可以找到。因为 dcron 每一次的睡眠时长它自己知道,所以当它再次从睡眠状态醒来,发现时间变化特别大时,它就会察觉到。也就是说,小的变化它会察觉不到的。

systemd 呢?这家伙一直在使用 Linux 新加特性,比如上次发现的 prctl 的 PR_SET_CHILD_SUBREAPER 功能。这次它也没有让我失望,它使用了 timerfd 的一个鲜为人知的标志位——TFD_TIMER_CANCEL_ON_SET。timerfd 是 Linux 2.6.25 引入的特性,而TFD_TIMER_CANCEL_ON_SET这个标志位则据说 Linux 3.0 引入的,但是到目前为止(man-pages 3.61),手册里没有提到它,系统头文件里也没有它。

这个标志位是干什么的呢?其实很简单,是当系统时钟被重设时向程序发送通知,包括通过系统调用设置系统时间,以及系统从硬件时钟更新时间时。当事件发生时,在该 timerfd 上的读取操作会返回 -1 表示失败,而 errno 被设置成ECANCELED。下边是一个简单的演示程序,在系统时间变化时打印一条消息:

#include<unistd.h>
#include<sys/timerfd.h>
#include<stdbool.h>
#include<stdint.h>
#include<errno.h>
#include<stdlib.h>
#include<stdio.h>
#define TIME_T_MAX (time_t)((1UL << ((sizeof(time_t) << 3) - 1)) - 1)
#ifndef TFD_TIMER_CANCEL_ON_SET
#  define TFD_TIMER_CANCEL_ON_SET (1 << 1)
#endif

int main(int argc, char **argv){
  int fd;
  struct itimerspec its = {
    .it_value.tv_sec = TIME_T_MAX,
  };
  fd = timerfd_create(CLOCK_REALTIME, TFD_CLOEXEC);
  if(fd < 0){
    perror("timerfd_create");
    exit(1);
  }
  if(timerfd_settime(fd, TFD_TIMER_ABSTIME|TFD_TIMER_CANCEL_ON_SET,
        &its, NULL) < 0) {
    perror("timerfd_settime");
    exit(1);
  }
  uint64_t exp;
  ssize_t s;
  while(true){
    s = read(fd, &exp, sizeof(uint64_t));
    if(s == -1 && errno == ECANCELED){
      printf("time changed.\n");
    }else{
      printf("meow? s=%zd, exp=%lu\n", s, exp);
    }
  }
  return 0;
}

编译并运行该程序,然后拿 date 命令设置时间试试吧 =w= 当然记得用虚拟机哦,因为系统时间乱掉的时候会发生不好的事情喵~

date 091508002012
Category: Linux | Tags: systemd linux
3
2
2014
3

FUSE 初体验:Android dedupefs

自打知道 FUSE 以来都觉得亲手写一个 FUSE 文件系统是很好玩的事情,但是因为没好的自己能够很快实现的点子所以一直没动手。前段时间需要从 Android xrecovery 备份中取得一旧版本的应用,才决定动手的,顺便也练习一下很久没怎么用到的 C 语言。至于为什么不用 Python,好像那个 Python 绑定不太稳定的样子,Python 3 版更是如此。而且我也不希望效率太差。

首先介绍一下,所谓的「dedupefs」,就是把 Android xrecovery 的「dedupe」备份格式的数据挂载成文件系统来查看。其实仅仅只是想查看的话,把那个 dedupe 目录下的东东 gcc 一下就可以创建和解开 dedupe 的备份了,只是占用很多磁盘空间而已。

dedupe 的格式很简单,一个文本文件描述文件信息(时间、路径、大小、类型等),一个目录里全是 sha256 命名的文件来存储文件的数据,以便在备份时不同的备份中的相同文件只保存一次。

FUSE 嘛,我好像从来没看到过完整一点的文档,就是官方 API 文档也经常语焉不详。dedupefs 是参考 rofs 写的。dedupefs 也是只读的。

挂载之前,先得把 dedupe 的纯文本格式处理一下。纯文本适合存储和人阅读,但是查询效率低下。我决定用更适合处理纯文本的 Python,把数据存储到 GNU dbm 键值对数据库中,然后 dedupefs 直接读取数据库就好了。(于是顺便学会了在 C 中使用 GNU dbm :-))数据的组织方式如下:

  • d + 文件路径:该目录下的文件名列表
  • f + 文件路径:该文件的信息

这样要读取一个目录下的文件列表就查 d 开头的项,要取得一个文件的信息(stat)或者打开文件,就读 f 开头的。

下边是编码和调试过程中的经验与收获:

  • GNU dbm 没说它是线程安全的,所以它不是线程安全的。但是 FUSE 又是多线程的(调试用的单线程模式我就不玩的),所以读取数据库时要加锁。
  • GNU dbm 查询结果数据是要调用者来 free 的。
  • 因为涉及到二进制数据交换(Python <-> C),所以要注意在结构体声明时围上#pragma pack(push, 1)#pragma pack(pop),以免对齐不一致造成数据错误。
  • valgrind 用来诊断内存访问错误效果非常棒!
  • FUSE 的struct fuse_file_info里有个fh域可以用来存文件描述符,这样就不用像 rofs 那样每次读取都要打开一遍文件了。
  • FUSE 读取用的回调函数传的offset一定要用,要首先lseek(finfo->fh, offset, SEEK_SET);一下,不然指不定读取到什么地方的数据了。
  • FUSE 文件系统可以忽略文件权限,所以自己不在openaccess里判断的话,就可以访问到明明看上去不能访问的文件(这正在我想要的)。
  • du 命令读取文件占用磁盘空间时使用了struct statst_blocks域。如果在 FUSE 程序里不管它的话,那么 du 将总是报告占用了 0 字节的空间……这里的块大小总是 512 字节。

第一次写 FUSE 程序,虽然文档差了一点,但用起来还是挺方便 =w=

哦对了,android-dedupefs 的仓库链接。

2
23
2014
11

让我们收养孤儿进程吧

稍微了解一点类 UNIX 系统的进程管理的都知道,当一个进程的父进程死亡之后,它就变成了孤儿进程,会由进程号 1 的 init 进程收养,并且在它死亡时由 init 来收尸。但是,自从使用 systemd 来管理用户级服务进程之后,我发现 systemd --user 管理的进程总是在它之下,即使进程已经 fork 了好几次。systemd 是怎么做到的呢?

对一个软件的实现有不懂的想了解当然是读它的源码了。这种东西可没有另外的文档,因为源码本身即文档。当然之前我也 Google 过,没有得到结果。在又一个全新的源码树里寻寻觅觅一两天之后,终于找到了这个:

        if (arg_running_as == SYSTEMD_USER) {
                /* Become reaper of our children */
                if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
                        log_warning("Failed to make us a subreaper: %m");
                        if (errno == EINVAL)
                                log_info("Perhaps the kernel version is too old (< 3.4?)");
                }
        }

原来是通过prctl系统调用实现的。于是去翻 prctl 的 man 手册,得知PR_SET_CHILD_SUBREAPER是 Linux 3.4 加入的新特性。把它设置为非零值,当前进程就会变成 subreaper,会像 1 号进程那样收养孤儿进程了。

当然用 C 写不好玩,于是先用 python-cffi 玩了会儿,最后还是写了个 Python 模块,也是抓住机会练习一下 C 啦。有个 python-prctl 模块,但是它没有包含这个调用。

#include<sys/prctl.h>
#include<Python.h>

static PyObject* subreap(PyObject *self, PyObject *args){
  PyObject* pyreaping;
  int reaping;
  int result;

  if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &pyreaping))
    return NULL;
  reaping = pyreaping == Py_True;

  Py_BEGIN_ALLOW_THREADS
  result = prctl(PR_SET_CHILD_SUBREAPER, reaping);
  Py_END_ALLOW_THREADS

  if(result != 0){
    return PyErr_SetFromErrno(PyExc_OSError);
  }else{
    Py_RETURN_NONE;
  }
}

static PyMethodDef mysysutil_methods[] = {
  {"subreap", subreap, METH_VARARGS},
  {NULL, NULL}    /* Sentinel */
};

static PyModuleDef mysysutil = {
  PyModuleDef_HEAD_INIT,
  "mysysutil",
  "My system utils",
  -1,
  mysysutil_methods,
  NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_mysysutil(void){
  PyObject* m;

  m = PyModule_Create(&mysysutil);
  if(m == NULL)
    return NULL;
  return m;
}

编译之后,

>>> import mysysutil
>>> mysysutil.subreap(True)

然后开子进程,不管它 fork 多少次,都依然会在这个 Python 进程之下啦。

但是,这样子不太好玩呢。如果我登陆之后所有启动的子进程都在一个进程之下不是更有意思么?于是我打上了 Awesome 的主意,因为它支持运行任意的 Lua 代码嘛。于是我又给这个 prctl 调用弄了个 Lua 绑定。最终的版本如下:

#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>

#include<sys/prctl.h>
#include<sys/wait.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

static int l_setsubreap(lua_State * L){
  int reap;
  if(lua_isboolean(L, 1)){
    reap = lua_toboolean(L, 1);
  }else{
    return luaL_argerror(L, 1, "not a boolean");
  }
  if(prctl(PR_SET_CHILD_SUBREAPER, reap) != 0){
    return luaL_error(L, "prctl failed: %s", strerror(errno));
  }
  return 0;
}

static int l_ignore_SIGCHLD(lua_State * L){
  signal(SIGCHLD, SIG_IGN);
  return 0;
}

static int l_reap(lua_State * L){
  int pid, st;
  pid = waitpid(-1, &st, WNOHANG);
  lua_pushinteger(L, st);
  lua_pushinteger(L, pid);
  return 2;
}

static const struct luaL_Reg l_lib[] = {
  {"setsubreap", l_setsubreap},
  {"reap", l_reap},
  {"ignore_SIGCHLD", l_ignore_SIGCHLD},
  {NULL, NULL}
};

int luaopen_clua(lua_State * L){
  lua_newtable(L);
  luaL_setfuncs(L, l_lib, 0);
  return 1;
}

除了调用 prctl 外,还增加了显式忽略 SIGCHLD 信号,以及非阻塞地调用 waitpid 收割单个僵尸进程的函数,因为 Awesome 本身没处理子进程退出,我一不小心弄出了好几个僵尸进程……对了,那个 waitpid 要注意给弄成非阻塞的,不然一不小心就会出问题

用的时候就是这样子,可以写到rc.lua里,也可以在 awesome-client 里调用:

package.cpath = package.cpath .. ';/home/lilydjwg/scripts/lua/cmod/?.so'
clua = require('clua')
clua.setsubreap(true)
clua.ignore_SIGCHLD()

最终,我的进程树成了这样子:

htop-awesome-tree

可以看到,由 Awesome 启动的进程已经全部待在 Awesome 进程树之下了。systemd --user 是由 PAM 启动的,所以不在 Awesome 树下。但是,那些 dbus 的东西和 gconfd-2、at-spi 之类的是怎么回事呀……

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com