11
4
2015
23

从 slim 到 lightdm

从一开始使用 Arch Linux,我就选择了 slim 作为登录管理器。因为它轻量嘛,而且配合 Arch Linux 自己做的主题也挺漂亮的:

slim 登录界面

所以即使 slim 不再使用配置文件来指定有哪些桌面环境可用,改用无法指定顺序的 .desktop 文件,我只好告诉 pacman,不要升级 slim 了。

于是就这么用了很久,直到 lightdm 出世,直到 slim 所使用的托管网站关闭、停止开发,我也依然在用 slim。

直到有一天,那是在 systemd 开始使用用户级别的 session D-Bus 之后。我登出了会话,再次登入时,发现整个系统都不好了,因为 DBUS_SESSION_BUS_ADDRESS 环境变量没有被设置,导致程序找不到 session bus 而自动启动了一个。可我用户级别的 systemd 还在旧的 session bus 里呢,联系不上了。当然 tmux 里所使用的 session bus 也开始混乱了(我有让 tmux 从环境里更新各种 window 中的 DBUS_SESSION_BUS_ADDRESS 变量)。

究其原因,是因为 slim 没有正确地处理 sesssion。从 loginctl 及 systemd-cgls 可以看到,重新登录 slim 之后,进程并没有处在新的会话里,而是复用了旧的会话。

systemd 上,会话管理是 pam_systemd 来管理的,同时它会引入 DBUS_SESSION_BUS_ADDRESS 环境变量。PAM 会话是有一个 leader 进程的,它的结束标志着这个会话的结束(当然里边存活的进程还会继续存在)。比如文本终端登录用的 login,每一次登录都是一个新进程。比如 sshd,每个连接都是由单独的子进程来处理,PAM 会话也是那个时候打开的。然而 slim 却是在父进程里打开了 PAM 会话。于是 pam_systemd 一看,这个 session leader 怎么又要打开会话啦?报错:

8月 02 21:54:32 lilyforest slim[669]: pam_systemd(slim:session): Cannot create session: Already running in a session

所以,slim 下,只有第一次登录是正常的……

所以是时候换个跟得上时代的登录管理器了。那就 lightdm + GTK greeter 好了。

这个我以前也用过,不过没怎么配置所以背景一片漆黑,难看死了。这次制作了张背景图,在/etc/lightdm/lightdm-gtk-greeter.conf里配置一下

[greeter]
background=/usr/local/share/pixmaps/background.png

咦?还有头像?那就放一个~/.face好了。什么?lightdm 你没权限读取它?OK,给你权限:

setfacl -m u:lightdm:x ~
setfacl -m u:lightdm:r ~/.face

我的 HOME 目录的权限是 750,别人(other)进不来的,所以要给 lightdm x 权限。

最终的样子就是这样,也挺漂亮的,功能还挺全 :-)

lightdm 登录界面

Category: Linux | Tags: linux X Window systemd X window
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
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
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

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com