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
13

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
26

在 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 之类的是怎么回事呀……

2
19
2014
18

利用 Aufs 和 LXC 快速建立一个用于测试的系统副本

起因是,我偶尔看到 MediaWiki 导出时可以把图片也包含在 XML 文件中,但是不确定能不能顺利地导入回去。本来是准备拿虚拟机测试的,但是得在虚拟机里安装整套环境,麻烦呀。于是,结合前段时间折腾 Aufs 和 LXC 的经验,把当前正在运行的系统利用 Aufs 搞了一份只读挂载。当然还要弄个空目录来放可写分支:

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

其实这个样子就已经可以 chroot 进去跑 httpd 了。不过,得先改一下监听的端口,因为 chroot 环境与主系统只有文件系统是隔离的,网络空间还是共享的。chroot 中 PID 空间也是共享的,所以在里边杀进程时不小心把 PID 写错的话,是可能会把外边的进程给杀掉的……(而 LXC 中,主系统是可以杀容器中的进程,但是反过来不行,因为主系统中的进程在容器中根本没分配 PID。)

于是就来玩玩 LXC 啦。要注意把 fstab 删掉,不然 systemd 会不高兴。日志文件不能共享,否则 journald 会不高兴。因为把 mknod 权限给禁掉了,所以在容器里 loop 设备是没法创建的。如果需要,在主系统里 losetup 之后像注释里那样写一条挂载信息就好。

sudo rm root/etc/fstab
sudo rm -r root/var/log/journal
sudo mkdir root/var/log/journal
sudo chgrp systemd-journal root/var/log/journal
sudo brctl addbr br0
sudo ifconfig br0 192.168.10.1

cat > lxc.conf <<EOF
lxc.utsname = arch2
lxc.autodev = 1
lxc.tty = 1
lxc.pts = 1024
lxc.rootfs = ${PWD}/root
lxc.mount.entry = sysfs sys sysfs ro,defaults 0 0
lxc.mount.entry = proc proc proc nodev,noexec,nosuid 0 0
lxc.mount.entry = /proc/sys ${PWD}/root/proc/sys none ro,bind 0 0
lxc.cap.drop = mknod sys_module mac_admin mac_override
# loop mount
# lxc.mount.entry = /dev/loop1 /home/lilydjwg/tmpfs/root/var/lib/pacman ext4 rw 0 0
#networking
lxc.network.type = veth
lxc.network.link = br0
lxc.network.flags = up
lxc.network.ipv4 = 192.168.10.3
lxc.network.name = eth0
#cgroups
lxc.cgroup.devices.deny = a
lxc.cgroup.devices.allow = c *:* m
lxc.cgroup.devices.allow = b *:* m
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
lxc.cgroup.devices.allow = c 1:7 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 4:1 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:2 rwm
lxc.cgroup.devices.allow = c 136:* rwm
EOF
sudo lxc-start -n arch-dup -f lxc.conf

当然网络和 DNS 还要进去再设置一下:

route del -net 192.0.0.0/8
route add -net 192.168.0.0/16 eth0
route add -net default gw 192.168.10.1
echo 'nameserver 192.168.10.1' > /etc/resolve.conf

LXC 挺有点复杂的。systemd 的开发者也是这么认为的,所以他们搞了个操作便捷性类似于 chroot 但是功能类似于 LXC 的东东——systemd-nspawn!比如上边那个新系统可以这么启动:

sudo systemd-nspawn -b --private-network -D root

不过很遗憾的是,要么加--private-network让新启动的容器没有网络,要么不加,和 chroot 一样与主系统共享网络。毕竟是他们用来测试 systemd 的东东嘛。调试系统的第一个进程可不容易,但是当它在另一个系统中只是一个普通进程、可以连 gdb 和 strace 时情况就大不一样啦 =w=

PS: 在 systemd-nspawn 的 manpage 中(上边那个 freedesktop.org 的链接),Arch 和 Fedora 以及 Debian 并列作为示例了呢 =w=


2015年3月14日更新:使用 Linux 3.18 及以上版本的内核,也可以使用 overlayfs 取代 aufs 来挂载,挂载命令示例如下:

modprobe overlay
mount -t overlay -o lowerdir=/,upperdir=$PWD/.lxc-data,workdir=$PWD/.lxc-root overlayfs $PWD/.lxc-root

lowerdir是只读的目录(其中的数据不会被修改),upperdir是用于记录修改的可写目录,workdir是工作目录,其必要性我也不理解,需要和upperdir同一文件系统。我习惯上指定为挂载目标目录。

overlayfs 某些操作的效率似乎比 aufs 高不少。这里是我自己用来创建这个系统副本的 Shell 脚本。

Category: Linux | Tags: linux systemd lxc aufs

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