本文我们来通过主要讲解使用systemd进程间通信,达到让大家进一步了解systemd的一些概念。
systemd是linux下一个管理daemon,进而甚至可以说是守护整个系统的程序,systemd代替了之前init方式。同时systemd作为所有进程的父进程,来孕育出来操作系统的所有进程。
一些基本用法,以docker服务为例,systemd的命令都是以systemctl开头,即只有systemctl这个命令(传递参数不同)
# 设置docker这个服务开机启动
$ sudo systemctl enable docker
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /usr/lib/systemd/system/docker.service.
# 启动docker服务
$ sudo systemctl start docker
# 查看docker目前的状态
$ sudo systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2021-07-27 11:08:00 CST; 3 days ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 941 (dockerd)
Tasks: 43
Memory: 515.1M
CPU: 11min 41.250s
CGroup: /system.slice/docker.service
...
# 停止docker服务
$ sudo systemctl stop docker
上边我们也说到了systemd管理daemon,我们也称作被systemd管理的为unit,unit有几种类型,通过扩展名即可区分。
扩展名 | 功能 |
---|---|
.service | 一般服务类型,主要是系统服务,部分是用户安装的服务,以及这些服务所需要的服务 |
.socket | 系统中程序进行数据交换socket服务 |
.target | 执行环境的类型,其实一群unit的组合 |
.mount .automount | 文件系统挂载的服务 |
.path | 检测特定文件或目录的服务 |
.timer | 循环执行的服务,类似crontab,不过是systemd提供 |
我们用到接下来会用到service和socket来实现进程间通信
首先我们需要写server端的service配置文件,新建ipc-server.service文件,填写如下内容
[Unit] # Uint设置相关
Description=ipc server # 简易的说明
After=multi-user.target # After表示这个daemon启动之后才能启动
[Service] # 不同的unit类型使用的相应的设置
Type=simple # 默认值,表示由ExecStart启动,启动后常驻内存,还有一些其他的类型(foring,oneshot等等)
ExecStart=/usr/libexec/ipc/ipc-server # 设置启动路径,systemctl start时执行这个指令
WorkingDirectory=/usr/libexec/ipc/ # 设置工作目录
Restart=on-failure # 表示任何意外的失败,就将重启,如果使用systemctl stop就不会重启
# Environment="param1=xxx" 可使用此来传递变量
[Install] # 定义如何安装这个配置文件,即怎样做到开机启动
# WantedBy表示该服务所在的Target
# 即systemctl enable ipc-server.service会在multi-user.target.wants建立链接,
# 然后开机启动multi-user.target,进而就会启动该服务
WantedBy=multi-user.target
然后我们因为要通信,所以还需要一个socket配置文件,新建配置文件ipc-server.socket,填写如下内容
[Unit]
Description=ipc-server API socket
[Socket]
Service=ipc-server.service # 设置当有流量进入时 需要启动的服务单元的名称
ListenStream=/run/ipc-server/job.socket # 字节流指定套接字监听的地址,以/开头表示Uinx套接字,纯数字表示端口号
[Install]
WantedBy=sockets.target
server端的配置文件写好了,那我们下一步就是要写主要的程序了,我们使用go语言来完成
package main
import (
"fmt"
"net"
"bufio"
"github.com/coreos/go-systemd/activation"
"strings"
)
// 和客户端的处理程序,做echo
func process(conn net.Conn) {
defer conn.Close()
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:])
if err != nil {
fmt.Printf("read from conn failed, err:%v\n", err)
break
}
recvData := string(buf[:n])
fmt.Printf("server recv: %v\n", recvData)
_, err = conn.Write([]byte(recvData))
if err != nil {
fmt.Printf("write from conn failed, err:%v\n", err)
break
}
if strings.Contains(recvData, "close") {
break;
}
}
}
func main() {
fmt.Println("server start")
listeners, err := activation.ListenersWithNames()
if err != nil {
fmt.Println("Could not get listening sockets: ", err.Error())
return
}
// 判断是否有ipc-server.socket
if l, exists := listeners["ipc-server.socket"]; exists {
if len(l) != 1 {
fmt.Println("It should contain only one socket.")
return
}
var socketListener net.Listener
socketListener = l[0]
defer socketListener.Close()
for {
conn, err := socketListener.Accept() // accept
if err != nil {
fmt.Printf("accept failed, err:%v\n", err)
continue
}
// 接收到的链接,开启协程去处理
go process(conn)
}
} else {
fmt.Println("The socket is not ipc-server.socket")
return
}
}
代码也比较简单,我们使用systemd的socket而不是自己去创建一个socket,大多的知识也是服务器的基本知识,唯一要说的就是go引用的go-systemd这个库,使用activation能够获取激活的socket相关操作,ListenersWithNames 获得传递给这个进程的socket的一个map结构[socketname -> socket]
也就是说流程是这样的:首先我们启动socket(ipc-server.socket),systemd会去监听这个socket(ipc-server.socket),当有数据进来时systemd会去启动相应的服务(ipc-server.service),并且将这个socket传递给相应的进程。
我们处理程序(process)这里只是简单的echo,当收到close命令时,关闭这个连接。
同样,我们使用让client也是作为一个service来进行通信,那么service配置文件也要进行设置。
新建ipc-client.service文件,填写如下内容:
[Unit]
Description=ipc client
# Requires运行该daemon时,需要先运行ipc-server.socket
# 强依赖关系,如果依赖的服务没有启动,则该服务也不会启动
Requires=ipc-server.socket
After=multi-user.target ipc-server.socket
[Service]
Type=simple
PrivateTmp=true
ExecStart=/usr/libexec/ipc/ipc-client -unix /run/ipc-server/job.socket # 这里传递socket的参数进去
Restart=on-failure
[Install]
WantedBy=ipc-server.service
这里要说明一点时我们使用了Requires这个参数,为了简便我们使用这个参数,这样我们运行时只需要启动ipc-client.service,这样systemd就会帮我们启动ipc-server.socket,然后我们去连接这个socket,这样systemd又会帮我们去启动ipc-server.service。这样就太简便了。藍
package main
import (
"fmt"
"flag"
"os"
"net"
"bufio"
"strconv"
"time"
"strings"
)
func main() {
fmt.Println("client start");
var unix bool
flag.BoolVar(&unix, "unix", false, " unix 'address' as a path for ipc")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [-unix] address\n", os.Args[0])
flag.PrintDefaults()
os.Exit(0)
}
flag.Parse()
address := flag.Arg(0)
if address == "" {
flag.Usage()
} else {
fmt.Println("connect server by socket:" + address)
}
conn, err := net.Dial("unix", address)
if err != nil {
fmt.Println("client net.Dial error:" + err.Error())
}
defer conn.Close()
reader := bufio.NewReader(conn)
sum := 0
var data string
for {
if sum == 10 {
data = "close"
} else {
sum += 1
data = strconv.Itoa(sum)
}
fmt.Print("input data: " + data + "\n")
conn.Write([]byte(data + "\n"))
msg, err := reader.ReadString('\n')
if err != nil {
fmt.Println("client read string error:" + err.Error())
break
}
fmt.Println("recv:" + msg)
time.Sleep(time.Duration(2) * time.Second)
if strings.Contains(msg, "close") {
break
}
}
}
因为我们启动进程时候传递了socket文件进来,这样我们去连接就好了,这里我们做的工作时每隔2s向server发一个数据,10次后发送close结束,当收到close时程序关闭
因为我们需要将生成的文件要拷贝到指定目录,还需要构建go的程序,所以我们就写一个脚本来做build和install的动作,这样也方便我们调试: vim build.sh
go build server/ipc-server.go &&
go build ./client/ipc-client.go &&
sudo cp server/ipc-server.service /usr/lib/systemd/system/ &&
sudo cp server/ipc-server.socket /usr/lib/systemd/system/ &&
sudo cp client/ipc-client.service /usr/lib/systemd/system/ &&
if [ ! -d "/usr/libexec/ipc" ];then
sudo mkdir -p /usr/libexec/ipc
fi
sudo cp ipc-client /usr/libexec/ipc/ &&
sudo cp ipc-server /usr/libexec/ipc/
首先cd到我们程序的目录下,最终的目录结构为:
$ tree
.
├── build.sh
├── client
│ ├── ipc-client.go
│ └── ipc-client.service
├── go.mod
├── go.sum
└── server
├── ipc-server.go
├── ipc-server.service
└── ipc-server.socket
执行build.sh
./build.sh
接下来我们使用systemd启动程序,我们上边说有两种方法启动,第一种首先启动服务端再启动客户端,这个比较常规,这种启动服务端时只需要启动socket就好,socket会拉起service。第二种是启动客户端就好,客户端service会拉起服务端。我们直接使用第二种:
$ sudo systemctl start ipc-client.service
$ sudo systemctl status ipc-client.service
○ ipc-client.service - ipc client
Loaded: loaded (/usr/lib/systemd/system/ipc-client.service; disabled; vendor preset: disabled)
Active: inactive (dead)
8月 01 09:40:37 fedora ipc-client[173280]: recv:7
8月 01 09:40:39 fedora ipc-client[173280]: input data: 8
8月 01 09:40:39 fedora ipc-client[173280]: recv:8
8月 01 09:40:41 fedora ipc-client[173280]: input data: 9
8月 01 09:40:41 fedora ipc-client[173280]: recv:9
8月 01 09:40:43 fedora ipc-client[173280]: input data: 10
8月 01 09:40:43 fedora ipc-client[173280]: recv:10
8月 01 09:40:45 fedora ipc-client[173280]: input data: close
8月 01 09:40:45 fedora ipc-client[173280]: recv:close
8月 01 09:40:47 fedora systemd[1]: ipc-client.service: Deactivated successfully.
我们可以看到我们ipc-client执行完后已经退出了,我们继续看下server端的状态
$ sudo systemctl status ipc-server.service
● ipc-server.service - ipc server
Loaded: loaded (/usr/lib/systemd/system/ipc-server.service; disabled; vendor preset: disabled)
Active: active (running) since Sun 2021-08-01 09:40:25 CST; 1min 48s ago
TriggeredBy: ● ipc-server.socket
Main PID: 173285 (ipc-server)
Tasks: 6 (limit: 2288)
Memory: 2.8M
CPU: 19ms
CGroup: /system.slice/ipc-server.service
└─173285 /usr/libexec/ipc/ipc-server
8月 01 09:40:27 fedora ipc-server[173285]: server recv: 2
8月 01 09:40:29 fedora ipc-server[173285]: server recv: 3
8月 01 09:40:31 fedora ipc-server[173285]: server recv: 4
8月 01 09:40:33 fedora ipc-server[173285]: server recv: 5
8月 01 09:40:35 fedora ipc-server[173285]: server recv: 6
8月 01 09:40:37 fedora ipc-server[173285]: server recv: 7
8月 01 09:40:39 fedora ipc-server[173285]: server recv: 8
8月 01 09:40:41 fedora ipc-server[173285]: server recv: 9
8月 01 09:40:43 fedora ipc-server[173285]: server recv: 10
8月 01 09:40:45 fedora ipc-server[173285]: server recv: close
由上边我们也看到服务端接收到了数据,并返回给客户端。由此我们这个进程间通信的程序大工告成。
我们这篇文章首先讲解systemd的一些基本概念,然后将systemd的unit的一些分类,由于systemd的东西超级多,我们也不能一一描述清楚,我们只能说是socket和service来做一个例子使得大家由更深刻的体验。
我们上边也说到了socket不止可以监听文件,还可以监听端口号,这样实现http server也是很容易完成。
祝好
https://fedoraproject.org/wiki/Systemd/zh-cn
https://tailordev.fr/blog/2017/06/09/deploying-a-go-app-with-systemd-socket-activation/
https://pkg.go.dev/github.com/coreos/go-systemd/activation
《鸟哥Linux私房菜-基础篇》