当前位置: 首页 > 工具软件 > go-systemd > 使用案例 >

用systemd来进程间通信

钮善
2023-12-01

1. 前言

本文我们来通过主要讲解使用systemd进程间通信,达到让大家进一步了解systemd的一些概念。

2. 什么是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

3. systemd的unit类型

上边我们也说到了systemd管理daemon,我们也称作被systemd管理的为unit,unit有几种类型,通过扩展名即可区分。

扩展名功能
.service一般服务类型,主要是系统服务,部分是用户安装的服务,以及这些服务所需要的服务
.socket系统中程序进行数据交换socket服务
.target执行环境的类型,其实一群unit的组合
.mount
.automount
文件系统挂载的服务
.path检测特定文件或目录的服务
.timer循环执行的服务,类似crontab,不过是systemd提供

我们用到接下来会用到service和socket来实现进程间通信

4. systemd实现进程间通信

4.1 ipc-server端

4.1.1 service文件

首先我们需要写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 

4.1.2 socket文件

然后我们因为要通信,所以还需要一个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

4.1.3 server程序

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命令时,关闭这个连接。

4.2 ipc-client端

4.2.1 service文件

同样,我们使用让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。这样就太简便了。藍

4.2.2 client程序

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时程序关闭

4.3 build&run

因为我们需要将生成的文件要拷贝到指定目录,还需要构建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

由上边我们也看到服务端接收到了数据,并返回给客户端。由此我们这个进程间通信的程序大工告成。

5. 最后

我们这篇文章首先讲解systemd的一些基本概念,然后将systemd的unit的一些分类,由于systemd的东西超级多,我们也不能一一描述清楚,我们只能说是socket和service来做一个例子使得大家由更深刻的体验。

我们上边也说到了socket不止可以监听文件,还可以监听端口号,这样实现http server也是很容易完成。

祝好

6.ref

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私房菜-基础篇》

 类似资料: