以下の内容はhttps://devlights.hatenablog.com/entry/2025/04/21/073000より取得しました。


Goメモ-565 (ファイルディスクリプタパッシング)(File Descriptor Passing, sys/unix, SCM_RIGHTS)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。ちょっと試してみたので、ついでにここにメモメモ。。。

ひょんなことで、C言語でファイルディスクリプタパッシングをする処理をちょっと作ってたのですが、Goでも同じこと出来るかな?って思って少し試してみました。

sys/unix を使って実装。

ファイルディスクリプタパッシングって?

Unix/Linux系で通信処理とかを作っていない人にはちょっと馴染みがない概念と思います。

ファイルディスクリプタパッシング(FD passing)とは、あるプロセスから別のプロセスへ、既に開かれたファイルディスクリプタを転送する技術です。Unixシステムでは、ファイル、ソケット、パイプなどのリソースは、プロセス内でファイルディスクリプタという整数値で表現されています。

通常、各プロセスは独自のファイルディスクリプタテーブルを持ちますが、UnixドメインソケットのSCM_RIGHTS機能を使用することで、あるプロセスのファイルディスクリプタを別のプロセスに転送し、そのプロセスからも同じリソースにアクセスできるようにすることができます。

これを使うと、例えば ソケット を持っているプロセスから、全然関係ないプロセスにそのソケットを転送して、そっちで通信処理をやってもらうってことが出来るようになります。

言語を問わない機能なので、C言語で実装されたTCPサーバからGoで実装されたプロセスへディスクリプタパッシングして、Go側で通信処理を継続させるということも出来ます。

サンプル

fdpassing/fd.go

ファイルディスクリプタパッシングを行う処理です。

package fdpassing

import (
    "fmt"
    "net"

    "golang.org/x/sys/unix"
)

// Fd はUnixドメインソケットを使用してファイルディスクリプタをパッシングするための構造体です。
// Unixドメインソケット接続をラップし、ファイルディスクリプタの送受信機能を提供します。
type Fd struct {
    conn *net.UnixConn
}

// NewFd は与えられたUnixドメインソケット接続から新しいFdインスタンスを作成します。
// このコネクションを通じてファイルディスクリプタの送受信が可能になります。
//
// パラメータ:
//   - conn: ファイルディスクリプタの送受信に使用するUnixドメインソケット接続
//
// 戻り値:
//   - 初期化されたFdインスタンスへのポインタ
func NewFd(conn *net.UnixConn) *Fd {
    fd := new(Fd)
    fd.conn = conn
    return fd
}

// Send はファイルディスクリプタをUnixドメインソケット経由で送信します。
// SCM_RIGHTS機能を使用して、プロセス間でファイルディスクリプタを転送します。
//
// パラメータ:
//   - fd: 送信するファイルディスクリプタ
//
// 戻り値:
//   - エラーが発生した場合はエラー、成功した場合はnil
func (me *Fd) Send(fd int) error {
    var (
        dummy  = make([]byte, 1)
        rights = unix.UnixRights(fd)
        err    error
    )
    _, _, err = me.conn.WriteMsgUnix(dummy, rights, nil)
    if err != nil {
        return err
    }

    return nil
}

// Recv はUnixドメインソケット経由でファイルディスクリプタを受信します。
// 送信側から送られたSCM_RIGHTS制御メッセージを解析し、ファイルディスクリプタを取得します。
//
// 戻り値:
//   - 受信したファイルディスクリプタ
//   - エラーが発生した場合はエラー、成功した場合はnil
//     エラーの場合、ファイルディスクリプタは-1が返されます
func (me *Fd) Recv() (int, error) {
    var (
        dummy = make([]byte, 1)
        oob   = make([]byte, unix.CmsgSpace(4))
        flags int
        err   error
    )
    _, _, flags, _, err = me.conn.ReadMsgUnix(dummy, oob)
    if err != nil {
        return -1, err
    }

    if flags&unix.MSG_TRUNC != 0 {
        return -1, fmt.Errorf("control message is truncated")
    }

    var (
        msgs []unix.SocketControlMessage
    )
    msgs, err = unix.ParseSocketControlMessage(oob)
    if err != nil {
        return -1, err
    }

    if len(msgs) != 1 {
        return -1, fmt.Errorf("want: 1 control message; got: %d", len(msgs))
    }

    var (
        fds []int
    )
    fds, err = unix.ParseUnixRights(&msgs[0])
    if err != nil {
        return -1, err
    }

    if len(fds) != 1 {
        return -1, fmt.Errorf("want: 1 fd; got: %d", len(fds))
    }

    return fds[0], nil
}

tcpserver/main.go

クライアントからの接続を受け付けて、Acceptしたソケットをファイルディスクリプタパッシングで転送します。

package main

import (
    "errors"
    "log"
    "net"
    "time"

    "github.com/devlights/fdpassing"
)

func main() {
    log.SetFlags(log.Lmicroseconds)

    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    var (
        udsConn net.Conn
        err     error
    )
    for range 3 {
        udsConn, err = net.DialTimeout("unix", "@tcp_fd_passing", 1*time.Second)
        if err != nil {
            var netErr net.Error
            if errors.As(err, &netErr); netErr.Timeout() {
                continue
            }

            return err
        }

        break
    }
    defer udsConn.Close()
    log.Println("[TCP-S] connect uds-server")

    ln, err := net.Listen("tcp", ":8888")
    if err != nil {
        return err
    }
    defer ln.Close()
    log.Println("[TCP-S] tcp-listen on :8888")

    for {
        errCh := make(chan error, 1)
        func() {
            conn, err := ln.Accept()
            if err != nil {
                errCh <- err
                return
            }
            defer func() {
                conn.Close()
                log.Println("[TCP-S] close")
            }()
            log.Println("[TCP-S] accept client")

            var (
                unixConn = udsConn.(*net.UnixConn)
                tcpConn  = conn.(*net.TCPConn)
                file, _  = tcpConn.File()
                fd       = fdpassing.NewFd(unixConn)
            )
            err = fd.Send(int(file.Fd()))
            if err != nil {
                errCh <- err
                return
            }
            log.Printf("[TCP-S] passing fd=%d to uds-server", file.Fd())

            errCh <- nil
        }()

        err = <-errCh
        if err != nil {
            return err
        }
    }
}

udsserver/main.go

UNIXドメインソケットサーバを起動して、ファイルディスクリプタパッシングされたFDを受信する側です。

package main

import (
    "errors"
    "fmt"
    "io"
    "log"
    "net"
    "os"

    "github.com/devlights/fdpassing"
)

func main() {
    log.SetFlags(log.Lmicroseconds)

    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    ln, err := net.Listen("unix", "@tcp_fd_passing")
    if err != nil {
        return err
    }
    defer ln.Close()

    log.Println("[UDS-S] uds-server listening on")

    udsConn, err := ln.Accept()
    if err != nil {
        return err
    }
    defer udsConn.Close()

    log.Printf("[UDS-S] %v", udsConn.RemoteAddr())

    unixConn, ok := udsConn.(*net.UnixConn)
    if !ok {
        return fmt.Errorf("not net.UnixConn")
    }

    fd, err := fdpassing.NewFd(unixConn).Recv()
    if err != nil {
        return err
    }
    log.Printf("[UDS-S] recv fd=%d", fd)

    file := os.NewFile(uintptr(fd), "client-socket")
    if file == nil {
        return fmt.Errorf("os.NewFile() failed")
    }
    defer file.Close()

    conn, err := net.FileConn(file)
    if err != nil {
        return fmt.Errorf("net.FileConn() failed")
    }
    defer func() {
        conn.Close()
        log.Println("[UDS-S] close")
    }()

    buf := []byte("hello")
    _, err = conn.Write(buf)
    if err != nil {
        return err
    }
    log.Printf("[UDS-S] send (%s)", buf)

    buf = make([]byte, 5)
    n, err := conn.Read(buf)
    if err != nil {
        switch {
        case errors.Is(err, io.EOF):
            log.Println("[UDS-S] disconnect")
        default:
            return err
        }
    }
    log.Printf("[UDS-S] recv (%s)", buf[:n])

    tcpConn, _ := conn.(*net.TCPConn)
    tcpConn.CloseWrite()
    log.Println("[UDS-S] shutdown(SHUT_WR)")

    return nil
}

tcpclient/main.go

TCPクライアントとしてサーバに接続して通信処理を行います。

package main

import (
    "bytes"
    "errors"
    "io"
    "log"
    "net"
    "time"
)

func main() {
    log.SetFlags(log.Lmicroseconds)

    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        return err
    }
    defer func() {
        conn.Close()
        log.Println("[TCP-C] close")
    }()
    log.Println("[TCP-C] connect tcp-server")

    buf := make([]byte, 5)
    n, err := conn.Read(buf)
    if err != nil {
        switch {
        case errors.Is(err, io.EOF):
            return nil
        default:
            return err
        }
    }

    msg := buf[:n]
    log.Printf("[TCP-C] recv (%s)", msg)

    msg = bytes.ToUpper(msg)
    _, err = conn.Write(msg)
    if err != nil {
        return err
    }
    log.Printf("[TCP-C] send (%s)", msg)

    buf = make([]byte, 1)
    for {
        conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))

        _, err = conn.Read(buf)
        if err != nil {
            if errors.Is(err, io.EOF) {
                log.Println("[TCP-C] disconnect")
                break
            }

            return err
        }
    }

    return nil
}

Taskfile.yml

# https://taskfile.dev

version: '3'

tasks:
  default:
    cmds:
      - task: build
      - task: run
  build:
    cmds:
      - go build -o tcp-client tcpclient/main.go
      - go build -o tcp-server tcpserver/main.go
      - go build -o uds-server udsserver/main.go
  run:
    cmds:
      - ./uds-server &
      - sleep 1
      - ./tcp-server &
      - sleep 1
      - ./tcp-client
      - pkill tcp-server
      - pkill uds-server
    ignore_error: true

実行

ちゃんと、TCPサーバ側で持ってるFDが転送されて、FD受信したプロセスで通信処理が継続されていますね。

$ task
task: [build] go build -o tcp-client tcpclient/main.go
task: [build] go build -o tcp-server tcpserver/main.go
task: [build] go build -o uds-server udsserver/main.go
task: [run] ./uds-server &
task: [run] sleep 1
05:12:51.462037 [UDS-S] uds-server listening on
task: [run] ./tcp-server &
task: [run] sleep 1
05:12:52.475893 [UDS-S] @
05:12:52.475846 [TCP-S] connect uds-server
05:12:52.476473 [TCP-S] tcp-listen on :8888
task: [run] ./tcp-client
05:12:53.489672 [TCP-S] accept client
05:12:53.489720 [TCP-S] passing fd=8 to uds-server
05:12:53.489742 [TCP-S] close
05:12:53.489650 [TCP-C] connect tcp-server
05:12:53.489781 [UDS-S] recv fd=7
05:12:53.489868 [UDS-S] send (hello)
05:12:53.489894 [TCP-C] recv (hello)
05:12:53.489945 [TCP-C] send (HELLO)
05:12:53.489955 [UDS-S] recv (HELLO)
05:12:53.489978 [UDS-S] shutdown(SHUT_WR)
05:12:53.489984 [TCP-C] disconnect
05:12:53.489990 [UDS-S] close
05:12:53.490041 [TCP-C] close
task: [run] pkill tcp-server
task: [run] pkill uds-server

参考情報

stackoverflow.com

github.com

GitHub - devlights/fdpassing: A Go library for file descriptor passing

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。




以上の内容はhttps://devlights.hatenablog.com/entry/2025/04/21/073000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14