用systemd来进程间通信

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 在Linux系统中,`/run/systemd/`目录是`systemd`运行时文件的存储位置。以下是一些重要的文件或目录及其含义: - `/run/systemd/initctl/`:用于与`systemd init`进程进行通信的套接字目录。 - `/run/systemd/journal/`:存储`systemd-journald`守护进程的日志文件。 - `/run/systemd/lock/`:用于存储`systemd`相关进程的锁文件。 - `/run/systemd/machined/`:存储`systemd-machined`守护进程使用的套接字文件。 - `/run/systemd/private/`:用于存储`systemd`相关进程的私有文件,例如与用户会话相关的信息。 - `/run/systemd/seats/`:存储`systemd-logind`守护进程使用的套接字文件,用于管理用户登录会话。 - `/run/systemd/sessions/`:存储`systemd-logind`守护进程使用的套接字文件,用于管理用户会话。 - `/run/systemd/shutdown/`:用于存储`systemd`关机过程中的信息。 - `/run/systemd/system/`:存储`systemd`管理的系统服务单元配置文件。 - `/run/systemd/user/`:存储`systemd`管理的用户服务单元配置文件。 ### 回答2: `/run/systemd/`是`Linux`系统中重要的一个目录,用于存储`systemd`运行时数据。下面是对该目录下文件或目录的详细介绍: 1. ``/run/systemd/generator/``:这个目录存储了`systemd`生成的配置文件。当`systemd`启动时,它会扫描诸如`/etc/init.d/`或`/lib/systemd/system/`等目录中的脚本,并根据这些脚本生成相应的配置文件。生成的配置文件会被放置在``/run/systemd/generator/``目录中。 2. ``/run/systemd/units/``:这个目录存储了`systemd`单元的运行时实例。单元可以是服务、套接字、设备、挂载点等等。运行时实例是指这些单元当前正在运行的实例。通过在这个目录中查看相应单元的实例,我们可以获取有关该单元的实时信息。 3. ``/run/systemd/sockets/``:该目录包含了正在监听的套接字。`systemd`使用套接字来进一步组织和管理服务。在这个目录中,可以找到由`systemd`监听的各种套接字,并且可以查看其相关的控制信息。 4. ``/run/systemd/user/``:这个目录是用户级别的`systemd`运行时数据存储目录。与全局的``/run/systemd/``目录类似,用户级别的``/run/systemd/user/``目录也包含了`generator`、`units`和`sockets`这三个子目录,但它们是用于存储用户级别的配置文件、运行实例和套接字。 综上所述,``/run/systemd/``目录是`Linux`系统下`systemd`运行时数据的主要存储目录,具有重要的作用。通过对该目录下不同子目录的了解和分析,可以更好地了解系统中运行的`services`、`sockets`、`devices`等单元的实时状态和配置信息,以便于对系统进行监控和排查问题。 ### 回答3: `/run/systemd/` 目录是用于存储 systemd 运行时文件和信息的目录。下面是对其中一些文件和目录的详细介绍: 1. `/run/systemd/system/`:这个目录包含了系统启动时 systemd 服务的配置文件。每个服务通常由一个以 `.service` 结尾的文件来描述,这些文件控制着系统中各个服务的行为和参数设置。 2. `/run/systemd/units/`:这个目录用于存储系统中运行的 systemd 服务的实例文件。每个系统服务单位都有一个对应的文件,用于记录该服务实例的相关信息,如进程 ID、启动时等。 3. `/run/systemd/generator/`:该目录包含了 systemd 自动生成的服务配置文件。这些配置文件用于启动、管理一些动态创建的服务单位,如使用 `systemd-nspawn` 虚拟化技术创建的容器。 4. `/run/systemd/notify/`:这个目录中的文件用于 systemd 进程通信机制,特别是用于服务之通信。这些文件通过对特定标识符的读写来进行通知,可以在启动和激活服务时触发其他服务的相应操作。 5. `/run/systemd/sessions/`:该目录保存了当前正在运行的用户会话的 systemd 单位文件。每个用户会话都有一个对应的 `.scope` 文件,用于管理和控制该会话中的进程和资源。 6. `/run/systemd/users/`:这个目录用于保存每个用户登录会话的 systemd 单位文件。类似于 `/run/systemd/sessions/` 目录,但这里的文件是与特定用户关联的。 7. `/run/systemd/machines/`:在使用 `systemd-nspawn` 或者其他虚拟化技术创建的容器或虚拟机中,该目录为每个虚拟机实例保存相应的 systemd 单位文件,用于管理和监控该实例。 这些文件和目录位于 `/run` 目录中,是临时性的,每次系统启动时都会重新创建。通过这些文件和目录,systemd 可以实现对系统服务的动态管理和控制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值