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
2
19
2014
3

zsh 异步生成提示符

为什么要异步?当然是因为慢了。比如 Arch 核心仓库 git 版挺大的,第一次进去时显示个 git 分支名要等好一会儿。今天在 zsh-users 列表中看到 Bart Schaefer 给出了一个使用 coprocess 的解决方案,眼前一亮,立即照葫芦画瓢给自己的 zsh 用上了。以下是整个提示符设置部分的代码:

if [[ -n $commands[git] ]]; then
  _nogit_dir=()
  for p in $nogit_dir; do
    [[ -d $p ]] && _nogit_dir+=$(realpath $p)
  done
  unset p

  typeset -g _current_branch= vcs_info_fd=
  zmodload zsh/zselect 2>/dev/null

  _vcs_update_info () {
    eval $(read -rE -u$1)
    zle -F $1
    exec {1}>&-
    zle reset-prompt
  }

  _set_current_branch () {
    cwd=$(pwd -P)
    for p in $_nogit_dir; do
      if [[ $cwd == $p* ]]; then
        return
      fi
    done

    setopt localoptions no_monitor
    coproc {
      _br=$(git branch --no-color 2>/dev/null)
      if [[ $? -eq 0 ]]; then
        _current_branch=$(echo $_br|awk '{if($1 == "*"){print "%{\x1b[33m%} (" substr($0, 3) ")"}}')
      fi
      # always gives something for reading, or _vcs_update_info won't be
      # called, fd not closed
      typeset -p _current_branch
    }
    disown %{\ _br
    exec {vcs_info_fd}<&p
    # wait 0.1 seconds before showing up to avoid unnecessary double update
    # precmd functions are called *after* prompt is expanded, and we can't call
    # zle reset-prompt outside zle, so turn to zselect
    zselect -r -t 10 $vcs_info_fd 2>/dev/null
    zle -F $vcs_info_fd _vcs_update_info
  }

  typeset -gaU precmd_functions
  precmd_functions+=_set_current_branch
  setopt PROMPT_SUBST
fi

[[ -n $ZSH_PS_HOST && $ZSH_PS_HOST != \(*\)\  ]] && ZSH_PS_HOST="($ZSH_PS_HOST) "

E=$'\x1b'
PS1="%{${E}[2m%}%h $ZSH_PS_HOST%(?..%{${E}[1;31m%}%?%{${E}[0m%} )%{${E}[32m%}%~\$_current_branch
%(!.%{${E}[0;31m%}###.%{${E}[1;34m%}>>>)%{${E}[0m%} "

比较坑的是使用chpwd_functions的话只能在目录改变时显示一次,再随便执行个什么命令分支提示就没了。又想到目录不改变的时候分支也可以变化(切换分支了嘛),所以使用precmd_functions,每次显示提示符前(单纯的重绘除外)都执行一次。另外,为了避免每次显示提示符时都明显地分为两步干扰视线,所以在那个_set_current_branch函数里等了 0.1 秒,超时才会不管分支名显示先继续了。

2014年2月24日更新:注意,直到 zsh 5.0.5(就是当前最新版本)有个 bug,在显示提示符之后、用户输入之前,上述代码会经常出现「忙等待」的情况浪费 CPU。这里有个补丁可以修复。

Category: shell | Tags: linux Git zsh
2
7
2014
27

Linux「真」全局 HTTP 代理方案

看到 ArchWiki 上 GoAgent 条目的亚全局代理方案,只是设置了代理相关环境变量。我就想,为什么不实现一个真正的全局 HTTP 代理呢?

最终,答案是:Linux 太灵活了,以至于想写一个脚本来搞定很麻烦。不过方案如下,有兴趣的可以折腾折腾。

首先,需要用到的工具:dnsmasq、iptables、redsocks,以及 HTTP 代理工具。dnsmasq 是用来缓存 DNS 请求的,iptables 把 TCP 流转接到 redsocks,而 redsocks 将 TCP 流转接到代理上。

最小 dnsmasq 配置如下:

listen-address=127.0.0.1
cache-size=500
server=127.0.0.1#5353
bogus-nxdomain=127.0.0.1

这里使用了本地的 dnscrypt 服务(假设其在 5353 端口上提供服务)。也可以使用国外服务器,只是需要更细致的配置来迫使其走 TCP。

iptables 命令如下:

# 创建一个叫 REDSOCKS 的链,查看和删除的时候方便
iptables -t nat -N REDSOCKS
# 所有输出的数据都使用此链
iptables -t nat -A OUTPUT -j REDSOCKS

# 代理自己不要再被重定向,按自己的需求调整/添加。一定不要弄错,否则会造成死循环的
iptables -t nat -I REDSOCKS -m owner --uid-owner redsocks -j RETURN
iptables -t nat -I REDSOCKS -m owner --uid-owner goagent -j RETURN
iptables -t nat -I REDSOCKS -m owner --uid-owner dnscrypt -j RETURN

# 局域网不要代理
iptables -t nat -A REDSOCKS -d 0.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A REDSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A REDSOCKS -d 240.0.0.0/4 -j RETURN

# HTTP 和 HTTPS 转到 redsocks
iptables -t nat -A REDSOCKS -p tcp --dport 80 -j REDIRECT --to-ports $HTTP_PORT
iptables -t nat -A REDSOCKS -p tcp --dport 443 -j REDIRECT --to-ports $HTTPS_PORT
# 如果使用国外代理的话,走 UDP 的 DNS 请求转到 redsocks,redsocks 会让其使用 TCP 重试
iptables -t nat -A REDSOCKS -p udp --dport 53 -j REDIRECT --to-ports $DNS_PORT
# 如果走 TCP 的 DNS 请求也需要代理的话,使用下边这句。一般不需要
iptables -t nat -A REDSOCKS -p tcp --dport 53 -j REDIRECT --to-ports $HTTPS_PORT

redsocks 的配置:

base {
  log_debug = off;
  log_info = off;
  daemon = on; 
  redirector = iptables;
}
// 处理 HTTP 请求
redsocks {
  local_ip = 127.0.0.1;
  local_port = $HTTP_PORT;
  ip = $HTTP_PROXY_IP;
  port = $HTTP_PROXY_PORT;
  type = http-relay; 
}
// 处理 HTTPS 请求,需要一个支持 HTTP CONNECT 的代理服务器,或者 socks 代理服务器
redsocks {
  local_ip = 127.0.0.1;
  local_port = $HTTPS_PORT;
  ip = $SSL_PROXY_IP;
  port = $SSL_PROXY_PORT;
  type = http-connect;  // or socks4, socks5
}
// 回应 UDP DNS 请求,告诉其需要使用 TCP 协议重试
dnstc {
  local_ip = 127.0.0.1;
  local_port = $DNS_PORT;
}

然后以相应的用户和配置文件启动 dnsmasq 以及 redsocks。修改/etc/resolv.conf

nameserver 127.0.0.1

至于分流的事情,HTTP 部分可以交给 privoxy,但是 HTTPS 部分不好办。可以再设立一个像 GoAgent 那样的中间人型 HTTPS 代理,或者更简单地,直接根据 IP 地址,国内的直接RETURN掉。

以上就是整个方案了。有些麻烦而我又不需要所以没测试。反正就是这个意思。Android 软件 GAEProxy 就是这么干的(不过它没使用 iptables 的 owner 模块,导致我不小心弄出了死循环)。另外,BSD 系统也可以使用类似的方案。

2
2
2014
9

玩转 systemd 之用户级服务管理

前几日群里又有人提到使用 systemd 管理用户的守护进程。早些时候我就知道有设计这么个功能,然调用systemctl --user时却总是跑去执行/bin/false,然后告诉我执行失败 QAQ

既然提到,就再试试呗。没想到这次systemctl --user列出一大堆 unit!还没弄明白它是哪里来的,不过至少说明systemctl --user已经可以用了!于是尝试性地把几个不依赖 X 的服务改成 systemd 服务了。至于依赖 X 的就不要 systemd 管了,我自己写在 Awesome 配置里,毕竟它们属于这个 X 会话,独立性没那么强,也免得纠结 DISPLAY 环境变量的问题。

我的 systemd 版本是 208。大概以前的版本确实有些问题,现在已经弄好了。

systemd 的用户配置位于~/.config/systemd/user/lib/systemd/user/etc/systemd/user下。我当然是用「家」里的那个了。

我改写的这些服务之前大部分是在 tmux 里跑的,除了占用 tmux 窗口外还占用个 zsh。现在全部改写成 systemd 服务了,于是 tmux 那边干净了不少 =w=

比如这个:

[Unit]
Description=Privoxy Web Proxy With Advanced Filtering Capabilities
ConditionPathIsDirectory=%h/.privoxy

[Service]
Type=simple
PIDFile=/run/user/%U/privoxy.pid
SyslogFacility=local0
ExecStart=/usr/bin/privoxy --pidfile /run/user/%U/privoxy.pid --no-daemon %h/.privoxy/config

[Install]
WantedBy=default.target

可以看到,systemd 里写起要执行什么命令还是挺简陋的。毕竟它不调用 shell 的,于是环境变量都用不了,家目录的位置也要写成%h%U则是用户 ID。令我不解的是,执行的命令和#!里的一样,必须使用绝对路径,而不能只写个名字,依赖 $PATH 环境变量去寻找。于是就有这样丑陋的ExecStart了——

ExecStart=/bin/zsh -c "exec sslocal -s HOST -p PORT -l LOCALPORT -k PASSWORD -m METHOD -c <(echo {})"

至于那个SyslogFacility,是为了在 syslog-ng 的日志中不显示出来而定义的,这样就可以根据 facility 来过滤掉这些用户级服务的日志了。

还有个SyslogIdentifier也挺好用的。systemd 默认使用ExecStart里第一个字的文件名部分来作为日志的标识,于是在系统日志(syslog 和 journald)中就看到一堆 sh 以及一些 zsh 和 socat,没区分度了……于是写明 SyslogFacility 来赋予其一个更合理的名字。

systemd 用户级服务使用systemctl --user来管理,其它的和系统级的差不多,也是 enable / start / disable / stop / status 这些。当然 daemon-reload 命令会频繁地使用啦 :-)

查看 journald 日志却有些不同,也是加--user,但是查看某个服务的日志输出却不是-u SERVICE了。有个--user-unit NAME参数,但是它会只显示 systemd 启动的进程的日志,而没有 systemd 自己处理该服务时的日志了……通过阅读日志的 JSON 格式输出(-o json-pretty),终于弄了个比较理想的方案。以下是我在 zshrc 里的相关配置(zsh only 哦):

alias sysuser="systemctl --user"
function juser () {
  # sadly, this won't have nice completion
  typeset -a args
  integer nextIsService=0
  for i; do
    if [[ $i == -u ]]; then
      nextIsService=1
      args=($args _SYSTEMD_CGROUP=/user.slice/user-$UID.slice/user@$UID.service/)
    else
      if [[ $nextIsService -eq 1 ]]; then
        nextIsService=0
        args[$#args]="${args[$#args]}$i.service"
      else
        args=($args $i)
      fi
    fi
  done
  journalctl -n --user ${^args}
}

-n是因为默认全部输出,一下子跳到后边的日志时就太卡了,但是只输出最后一部分日志就挺快的。默认是十行,也可以接数字来指定数量。指定多次时最后一次生效,所以我问题把它写进去也不怎么影响手动指定的情况。

于是管理用户级服务时我就用sysuser ...,而要看某个服务的日志时就用juser -u xxx

因为我使用 eCryptfs 加密了主目录,所以还遇到一个问题:用户级的 systemd 进程是通过 PAM 启动的,而这启动的时候我的主目录还没解密呢。于是 systemd 没有读取到我的配置,没有自动启动任何服务……于是只好在~/.profile里加两行,在挂载之后告诉 systemd 一声:

if [ -r "$HOME/.ecryptfs/auto-mount" ]; then
  grep -qs "$HOME ecryptfs" /proc/mounts
  if [ $? -ne 0 ]; then
    mv $HOME/.Xauthority /tmp 2>/dev/null
    mount -i "$HOME"
    cd "$HOME"
    mv /tmp/.Xauthority $HOME 2>/dev/null
    systemctl --user daemon-reload
    systemctl --user default
  fi
fi
Category: Linux | Tags: linux systemd
2
2
2014
1

玩转 systemd 之基于 socket 激活的服务

这几天闲下来的时间多了,于是趁机折腾 systemd,也读了不少 systemd 的文档。

Arch 官方宣布 sysvinit 不再被支持的时候,我是有些不喜欢的,因为我还没来得及弄明白 systemd 这完全不同的一套东西。现在看了不少 systemd 的文档,反倒是喜欢上 systemd 了 :-)

关于 systemd,我有两个没想到。其一,systemd 是兼容 sysvinit 服务的,一如 upstart。只是 Arch 一向比较激进,所以根本没用到这兼容性。其二,systemd 这个名字是个双关语,它不仅表示「system daemon」,还与「System V」遥相呼应,因为它是「System D」 :-)

本文不是教程,因此没什么意思的服务启动停止之类的就不写了。本文写点有意思的:让 systemd 监听套接字,在有连接时再启动服务。这不是什么新鲜东西,inetd 就是干这个的,但是我从来没用过,也没感觉有多大的需求。然而整理我的用户级服务时却发现这东西挺好的。

首先来最简单的,使用 sshd.socket 代替 sshd.service

[Unit]
Conflicts=sshd.service
Wants=sshdgenkeys.service

[Socket]
ListenStream=22
Accept=yes

[Install]
WantedBy=sockets.target

其实用起来很简单,systemctl start sshd.socket就启动它了。因为写了Conflicts=sshd.service,所以已经启动的 sshd 服务会自动停止。但是,我还没告诉 systemd 要监听 2 号端口而不是 22 呢!

直接改这个sshd.socket显然不行,下次更新修改就没了。把文件从/usr/lib/systemd/system复制到/etc/systemd/system下再修改?以前我是这么做的,但是其实还有更好的做法:

/etc/systemd/system下建立目录sshd.socket.d,然后建立个.conf文件写入需要的修改

[Socket]
ListenStream=
ListenStream=2

这里有两个ListenStream指示。第一个值为空,是重置该选项的值,之前的设置全部作废。systemd 单元文件中有很多选项都接受多个值,写多遍的话就是多项相加,除非写空值来重置前边设置过的值。一开始看到这种设计我还没明白为什么要这样做,后来看到对.d目录的支持才恍然大悟。

于是乎,自己要的修改完全和系统自带的配置分离开了,既不需要手动合并上游的新配置,也不需要担心自己复制过来修改的配置文件陈旧了。

除了sshd.socket文件外,还有一个与之配套的sshd@.service文件,说明服务该如何启动。当然像 acpid 这种Accept=false(默认)的套接字配置,有连接时只要启动一个进程来处理就可以了,所以对应的 .service 文件不是模板(文件名中没有@)。

下面我自己来给 socat 写个类似的配置。这个极其简单的服务是我为在远程主机中 fcitx.vim 来控制本地 fcitx用的。

首先是.socket文件:

[Socket]
ListenStream=@fcitx-remote
Accept=yes

[Install]
WantedBy=sockets.target

ListenStream第一个字符是@,表示「抽象套接字」,是 Linux 特有的一种在文件系统和网络之外的套接字,好处是不用在监听前先删除相应的套接字文件。socat 和 sshd 一样,也是一个连接对应一个进程,所以Accept=true,让 systemd 接受连接之后把连接的套接字传过来。

然后是对应的.service文件:

[Unit]
Description=Fcitx Socket Forwarder

[Service]
SyslogIdentifier=fcitx-socat
SyslogFacility=local0
ExecStart=/usr/bin/socat stdin tcp:10.7.0.6:8989
StandardInput=socket
StandardError=syslog

StandardInput=socket指定 socat 进程的标准输入是 systemd 接收的套接字,所以 socat 命令变成了这样子:

/usr/bin/socat stdin tcp:10.7.0.6:8989

这样子,原先跑在 tmux 里的命令

socat abstract-listen:fcitx-remote,fork tcp:10.7.0.6:8989

就变成由 systemd 监听,在需要时再启动 socat 来处理啦 =w=

关于这个 .service 文件中那两个 Syslog 开头的指令,以及这两个手写的配置要如何给 systemd 用,请看下篇:玩转 systemd 之用户级服务管理

Category: Linux | Tags: linux systemd
12
29
2013
30

rsync+btrfs+dm-crypt 备份整个系统

生成目录!

目标:增量式备份整个系统

怎么做到增量呢?rsync + btrfs 快照。其实只用 rsync 也是可以做到增量式的1,但是支持子卷的 btrfs 可以做得更好:

  1. 快速删除旧的备份
  2. 更简单的备份逻辑
  3. 子卷可以设置成只读(这是个很重要的优点哦~)
  4. btrfs 支持压缩。系统里有好多文本文件的,遇到压缩效果不好的文件 btrfs 会自动放弃压缩

为什么要备份整个系统呢?——因为配置一个高度定制化的系统麻烦啊,只备份部分数据的话还可能漏掉需要的文件。另一个优点是可以直接启动到某个备份

备份了整个系统,包括各种公开或者隐私的数据,一堆 cookies 和帐号配置,邮件和聊天记录等。难道就不需要加密一下下吗?于是,在 btrfs 之下再加一层 dm-crypt 加密。

介绍完毕,下边进入正题。

准备工作

首先,需要一个足够新的 Linux 内核,因为 btrfs 还是「实验特性」,每个版本都会有大量改进。如果用比较旧的内核就有可能出事。我用的是 3.12.6 版本。其次,安装 rsync 和 cryptsetup。

当然还要准备硬件:一块希捷 BackupPlus 1T USB 3.0 移动硬盘,以及一枚Express Card 34mm 转 USB 3.0 扩展卡,因为我的笔记本没有 3.0 的接口。注意使用 USB 3.0 扩展卡,内核需要载入 pciehp 模块,否则会出现不能识别 3.0 的设备或者后续接上去的设备的情况。Arch 官方内核将这个模块直接编译进内核了,而我自己编译的很不幸没有,只好重新编译了下内核。

PS: 这俩家伙一起配合工作,写入峰值能达到 110MiB/s,比我笔记本自身的硬盘还要快。

然后是分区、格式化。我使用了 GPT 分区表。为了安装 grub 以便启动,最好在开头分配 2M 空间给 grub 使用,不然会很麻烦2。记得给这个分区 bios_grub 标志(GParted「管理标志」里勾上即可)。下一个分区是 ext4 格式的启动分区。我会在这里放一个 Arch Linux Live 系统用于维护任务,以及用于启动到备份的内核和 initramfs。因为备份的分区会被加密,所以必须把内核和 initramfs 放在另外的地方。接下来的一个分区放加密过的备份数据用的。

像这样初始化加密分区:

cryptsetup luksFormat /dev/sdc3

密码要长,但也一定要记住密码,因为除了穷举外是没有办法恢复的。

初始化完毕之后就可以使用密码打开该设备了:

cryptsetup open /dev/sdc3 lilybackup

最后的参数是一个名字,它会是解密后的设备在 /dev/mapper 下的文件名。

如果一切完毕,要记得(在卸载文件系统之后)关闭该设备:

cryptsetup luksClose lilybackup

dm-crypt 是块设备级的加密。我们还要在其上建立文件系统:

mkfs.btrfs /dev/mapper/lilybackup

然后挂载之,并建立相应的目录和子卷结构。比如我的:

* backup (dir)
  * home (dir)
    * current (subvol, rw)
    * 20131016_1423 (subvol, ro)
    * 20131116_2012 (subvol, ro)
    * ...
  * root (dir)
    * current (subvol, rw)
    * 20131016_1821 (subvol, ro)
    * 20131116_2128 (subvol, ro)
    * ...
* run, for boot up directly, with edited /etc/fstab (dir)
  * home (dir)
    * 20131116 (subvol, rw)
    * ...
  * root (dir)
    * 20131116 (subvol, rw)
    * ...
* etc, store information and scripts (subvol, rw)

我把备份数据放到 backup 目录下,/ 和主目录 /home/lilydjwg 分开备份的。每个备份是使用日期和时间命名的快照子卷。除了用于每次同步的 current 目录外其它的子卷都是只读的,以免被意外修改。在 run 目录下是用于直接运行的,可写。这些可以按需建立。

开始备份

这是我备份 / 使用的脚本。是一个 zsh 脚本,这样可以避免 bash 中特殊字符可能带来的问题,虽然 bash 有 shellcheck 可以静态分析出可能有问题的地方。

这个脚本带两到三个参数。第一个是 / 的位置,因为我一般会直接从运行的系统执行备份,但也有可能使用另外的维护系统(比如系统滚挂掉的时候)。第三个参数是用于确认操作的。不加它的话会以 --dry-run 参数来运行 rsync。rsync 很复杂,所以最好先演习一遍以避免不小心手抖了做错事 =w=

为了避免日后对照着 rsync 手册来揣摸每个单字母选项的意义,我在这里全部使用了选项的完整形式。反正我是 zsh 用户,那么长的命令中大部分字符都是 zsh 给我补全出来的 =w=

#!/bin/zsh -e

cd $(dirname $0)

if [[ $# -lt 2 || $# -gt 3 ]]; then
  echo "usage: $0 SRC_DIR DEST_DIR [-w]"
  exit 1
fi

src=$1
dest=$2
doit=$3

if [[ $doit == -w ]]; then
  dry=
else
  dry='-n'
fi

rsync --archive --one-file-system --inplace --hard-links \
  --human-readable --numeric-ids --delete --delete-excluded \
  --acls --xattrs --sparse \
  --itemize-changes --verbose --progress \
  --exclude='*~' --exclude=__pycache__ \
  --exclude-from=root.exclude \
  $src $dest $dry

比较重要的几个 rsync 选项:

--archive
我们要备份,所以请保留所有信息
--one-file-system
只备份这个文件系统的内容,不要跑到 /sys 啊 /proc 啊 /dev 啊 /tmp 这类目录里去了。这也省得自己手动排除
--numeric-ids
文件的所有者信息使用数字而不要解析成用户名/组名。避免在跨系统使用时出差错
--exclude-from=root.exclude
root.exclude文件中读取额外的排除列表
--acls --xattrs
保留文件 ACL 和扩展属性

我发现的 / 里需要排除的目录如下:

/var/cache/*/*
/var/tmp/
/var/abs/local/
/var/lib/mongodb/journal/

其中第一项写成那样是因为,我要保留 /var/cache 下的一级目录。

主目录的备份是类似的过程,只是更加复杂罢了。当我写我的主目录的备份脚本的时候,深切地体会到有圣人说过的一句话——过早的优化是万恶之源。因为我使用 eCryptfs 加密主目录时为了避免可以公开的文件被加密造成性能损失,做了一系列的软链接。它们一直在给我带来各种小麻烦和不爽……

注意这些脚本要以 root 的身份运行。待所有备份脚本跑完之后,对那个 current 子卷做一个只读快照就好了:

sudo btrfs subvolume snapshot -r current $(date +'%Y%m%d_%H%M')

下次要更新备份时是一样的步骤:跑同步脚本,创建新快照。第一次同步会比较慢,跑了一个多小时吧。后边的增量备份就比较快了,十几分钟就好。

从备份启动

要启动,首先把 grub 装过去。把 Arch Linux live 系统的配置写好,当然还有启动备份系统的配置,如下:

search --no-floppy --fs-uuid --set=root 090dcc64-2b6d-421c-8ef6-2ab3321aec62

menuentry "Archlinux-2013.12.01-dual.iso (x86_64)" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    set isofile="/images/archlinux-2013.12.01-dual.iso"
    echo "Setup loop device..."
    loopback loop $isofile
    echo "Loading kernel..."
    linux (loop)/arch/boot/x86_64/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
    echo "Loading initrd..."
    initrd (loop)/arch/boot/x86_64/archiso.img
}

menuentry "Archlinux-2013.12.01-dual.iso (i686)" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    set isofile="/images/archlinux-2013.12.01-dual.iso"
    echo "Setup loop device..."
    loopback loop $isofile
    echo "Loading kernel..."
    linux (loop)/arch/boot/i686/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
    echo "Loading initrd..."
    initrd (loop)/arch/boot/i686/archiso.img
}

set ver=3.12.6
menuentry "Arch Linux $ver backup" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    echo    'Loading Linux kernel ...'
    linux   /boot/vmlinuz-linux-lily-$ver root=/dev/mapper/lilybackup rw cryptdevice=/dev/disk/by-uuid/815d01ea-6390-460f-8c82-84c9e9497423:lilybackup rootflags=compress=lzo,subvolid=0 break=postmount
    echo    'Loading initramfs...'
    initrd  /boot/initramfs-$ver-backup.img
}

各个设备的卷标、UUID 和文件路径自己调整。最后一项需要的文件在后边准备,先介绍一下几个参数:

root
根分区所在的设备。是解密后的设备路径或者用 UUID 也可以。不过没关系的,这里不会有冲突的
cryptdevice
如果使用密码(而不是密钥文件)加密的话,这里是冒号分隔的两个参数:你的加密设备是哪个文件,以及它解密之后叫什么名字。
rootflags
这个是给我们的 btrfs 用的。指定要启用 lzo 算法压缩,使用根子卷。实际上根子卷里不是 Linux 系统的根。这里我只是让脚本把它挂载到/new_root上而已,脚本会因为找不到/sbin/init而进入一个 shell 的
break=postmount
即使根没有问题,也请在挂载好它之后给我一个 shell,我可能需要做一些调整

内核很简单,直接 cp 过去就好了。initramfs 要另外生成。这是我用来生成的 mkinitcpio.conf.crypt 文件:

MODULES="btrfs"
BINARIES="/usr/bin/btrfs"
FILES=""
HOOKS="base udev autodetect modconf block encrypt filesystems keyboard fsck shutdown"
COMPRESSION="xz"

重要的地方:内核模块 btrfs 一定是要的,另外我还需要 btrfs 程序来操作子卷。在 HOOKS 数组的 filesystems 前添加了 encrypt,用于在启动时询问密码并解码根分区。

我还准备添加 vi 程序来着,但是它说终端类型不认识,还说 /var/tmp 目录不存在。于是索性把自己静态链接的 vim 扔到内核一块去了。Vim 内建常见终端类型的数据,不那么挑剔的。zsh 所需要的文件太多,也放弃了。

然后使用 mkinitcpio 命令生成 initramfs 镜像:

sudo mkinitcpio -c mkinitcpio.conf.crypt -g initramfs-backup.img

然后把生成的文件复制到启动分区的相应路径下。

注意:如果你在 Arch Linux live 系统中为另外的内核生成该 initramfs,要指定内核和内核模块路径的根。一定不要将内核模块所在的目录软链接到/lib/modules,那样 mkinitcpio 不会添加任何块设备的内核模块的。我使用的命令如下:

mkinitcpio --kernel /run/archiso/img_dev/boot/vmlinuz-linux-lily-3.12.6 -r /mnt/backup/root/20131227_2044 --config mkinitcpio.conf -g /run/archiso/img_dev/boot/initramfs-3.12.6-backup.img

文件准备完毕,就可以启动过去了。注意我没有在备份分区的 run 目录下建立子卷,因为我准备进入 initramfs 之后再建立它们。

PS: 因为 BIOS 不支持,所以必须从 USB 2.0 来启动。在 Linux 内核启动的时候,可以将移动硬盘接到 USB 3.0 扩展卡上。具体时机是,initramfs 载入完毕,内核开始打印日志的时候。

进入备份系统

启动之后,会进入 initramfs 的 shell。在这个没有任务管理的 ash 中,使用 btrfs 命令在 /new_root/run 目录下建立新的子卷,注意不要加 -r 这个表示只读的选项了:

btrfs subvolume snapshot ../backup/root/20131016_1423 root/20131016
btrfs subvolume snapshot ../backup/home/20131016_1423 home/20131016

因为文件系统树的挂载结构变了,所以得拿准备好的 vim(或者 vi,如果你没准备 vim 的话)去编辑 root/20131016/etc/fstab 文件,将那些不会成功的挂载项都去掉,添加新的正确的项。PS: 如果使用 vim 的话,记得进去先set nocp一下,不然会是兼容模式,和 vi 一样只能撒消一步的。

/dev/mapper/lilybackup  /       btrfs   rw,relatime,compress=lzo,subvol=run/root/xxx     0 0
/dev/mapper/lilybackup  /home/lilydjwg  btrfs   rw,relatime,compress=lzo,subvol=run/home/xxx     0 0

然后 cd /,卸载 /new_root 并重新以子卷挂载之:

cd /
umount /new_root
mount -o compress=lzo,subvol=run/root/20131016 /dev/mapper/lilybackup /new_root

如果是因为找不到 /sbin/init 而进来这个 shell 的,那么就tail /init,最后那行是需要执行的命令(当然有些修改):

exec env -i "TERM=$TERM" /usr/bin/switch_root /new_root /sbin/init

如果是因为break=postmount参数而进来的,直接按Ctrl-D退出 shell 即可。

启动会继续进行,systemd 启动了,各种服务陆续启动中~~

如果很不幸地,忘记修改/etc/fstab了,或者有错,那么 systemd 会毫不留情地「Welcome to emergency shell」。不过现在更正也为时未晚。在编辑完 fstab 之后,要先执行下systemctl daemon-reload再退出那个 emergency shell。

接下来应该能一路顺利地到达指定时间的系统啦 =w=

时间机器打造完成哦耶~~

参考资料

Category: Linux | Tags: linux grub grub2 btrfs Arch Linux
11
24
2013
2

X Window 中的剪贴板

这原本是我在知乎上的一个回答,现在略作修改,放在博客上。


很多 Linux 用户知道,除了通用的Ctrl-C/Ctrl-V剪贴板外,Linux 桌面上还有另一套剪贴板可以用。

首先澄清一下,这个功能不属于 Linux,而是属于它(目前)所广泛使用的显示服务程序——X WindowX Window 的历史比 LinuxVim 都要古老呢。现在所使用的版本 X11 也是 1987 年就已经发布了的。

X Window 目前被广泛使用的用于 X Window 客户端(使用 X Window 的程序)间交换数据的剪贴板有两个:primary selectionclipboard

Primary selection,通常,内容被选择时会被放到这里,按鼠标中键时被获取并粘贴。

例外一火狐浏览器中只有用户主动选择的内容才会被放到 primary selection,由网页代码导致的选择不会修改用户的 primary selection。
例外二Vim / GVim 的「可视」选择默认并不放到 primary selection。有选项可以设置成这样。
例外三:一些网站(如 GitHub)用的 Ace 在线编辑器,在用户「选择」时并不创建真正的选择区,它只在用户按Ctrl-C等键时做一些处理,因此在 Ace 编辑器中选中复制、中键粘贴无效。
例外四Wine 不支持 primary selection。

Clipboard,这就是大家熟悉的剪贴板了,图形界面程序中Ctrl-C复制,Ctrl-V粘贴。终端里因为快捷键会冲突,所以这些图形界面常用的快捷键使用的时候都要按住 Shift 键。

关 于 X Window 剪贴板要注意的地方:以上剪贴板的内容都不是保存在 X 服务器上的,而是客户端程序说,「我请求提供这个剪贴板的数据」(X 服务器通常会允许这样的请求)。另外的程序要粘贴时就会通过 X 服务器向这个程序请求:「请把 XX 剪贴板的数据给我。」所以,X Window 剪贴板上的内容会在拥有它的程序退出后自动被清除。所以一般人会需要用剪贴板管理器来更持久一些地保存剪贴板数据。

关于 X 协议细节可能有些不对,不过大体上是这个样子的啦。

还有没什么程序用到的 secondary selection,以及 Vim 偶尔会用到的 cut buffers(共8个,Vim 和 xterm 会用第一个)。Cut buffers 似乎是由 X 服务器保存数据的。Vim 在挂起时为了避免请求剪贴板数据的程序长时间等待会把自己的选择区内容写到 CUT_BUFFER0。

火狐似乎设置了很短的剪贴板请求超时时间,因此,从远程程序请求剪贴板数据时,可能因为网络延迟导致火狐没有及时得到数据而放弃。

Category: Linux | Tags: linux X Window X window
10
29
2013
11

不需要 root 权限的 ICMP ping

ICMP 套接字是两年前 Linux 内核新加入的功能,目的是允许不需要 set-user-id 和CAP_NET_RAW权限的 ping 程序的实现。大家都知道,set-user-id 程序经常成为本地提权的途径。在 Linux 内核加入此功能之前,以安全为目标的 Openwall GNU/*/Linux 实现了除 ping 程序之外的所有程序去 suid 化……这个功能也是由他们提出并加入的。

我并没有在 man 手册中看到关于 ICMP 套接字的信息。关于 ICMP 套接字使用的细节来自于内核邮件列表

使用 ICMP 套接字的好处

  1. 程序不需要特殊的权限;
  2. 内核会帮助搞定一些工作。

坏处是:

  1. 基本没有兼容性可讲;
  2. 需要调整一个内核参数。

这个内核参数net.ipv4.ping_group_range,是一对整数,指定了允许使用 ICMP 套接字的组 ID的范围。默认值为1 0,意味着没有人能够使用这个特性。手动修改下:

sudo sysctl -w net.ipv4.ping_group_range='0 10'

当然你可以直接去写/proc/sys/net/ipv4/ping_group_range文件。

如果系统不支持这个特性,在创建套接字的时候会得到「Protocol not supported」错误,而如果没有权限,则会得到「Permission denied」错误。

创建 ICMP 套接字的方法如下:

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_ICMP)

它的类型和 UDP 套接字一样,是SOCK_DGRAM,不是SOCK_RAW哦。这意味着你不会收到 20 字节的 IP 头。不仅仅如此,使用 ICMP 套接字不需要手工计算校验和,因为内核会重新计算的。ICMP id 也是由内核填的。在接收的时候,内核会只把相应 id 的 ICMP 回应返回给程序,不需要自己或者要求内核过滤了。

所以,要组装一个 ICMP ECHO 请求包头很容易了:

header = struct.pack('bbHHh', 8, 0, 0, 0, seq)

这五项依次是:类型(ECHO_REQUEST)、code(只能为零)、校验和(不需要管)、id(不需要管)、序列号。

接收起来也简单,只要看一下序列号知道是回应自己发的哪个包的就行了。

这里是我的一个很简单的实例。

附注:Mac OS X 在 Linux 之前实现了类似的功能。但是行为可能不太一样。有报告校验和需要自己计算的,也有报告发送正确但是返回报文是乱码的。另,FreeBSD 和 OpenBSD 不支持这个特性。

Category: Linux | Tags: linux python 网络 ICMP

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com