Golang中C分配的指针会被GOGC回收吗?

Golang中C分配的指针会被GOGC回收吗? 我正在尝试通过重写 http 模块中的 accept/read/write 方法,使一个 https 服务器正常工作。我使用 go-wolfssl 来处理 TLS。

服务器会接受一个 TLS 1.2 连接,然后向连接的客户端发送一些随机数据,接着等待下一个连接。

我遇到的问题在于,每当服务器发送大的负载时,它能够成功发送,但随后就无法向连接的下一个客户端发送数据(发送会因 TCP 错误 EBADF 而失败)。如果处理较小的负载(<30k),服务器可以成功地向每个连接的客户端发送数据。然而,一旦发送了一个大的负载,下一次传输就会失败。

如果我使用 debug.SetGCPercent(-1) 禁用 GOGC,问题就会消失,我可以根据需要发送任意多的大负载。根据我目前的调试情况,这看起来像是 GO 垃圾回收器丢弃了 C 指针的问题。go-wolfssl 依赖于 wolfSSL C 库,因此它使用了 CGO。有人有其他想法或建议吗?

代码如下。要实际运行它,请查看此仓库:GitHub - lealem47/go-wolfssl-https-server

谢谢!

package main

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	log "github.com/sirupsen/logrus"
	wolfSSL "github.com/wolfssl/go-wolfssl"
	"net"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"
)

const defaultPort = "8443"

type wolfSSLListener struct {
	listener net.Listener
	ctx      *wolfSSL.WOLFSSL_CTX
}

// Accept waits for and returns the next connection to the listener.
func (cl *wolfSSLListener) Accept() (net.Conn, error) {
	conn, err := cl.listener.Accept()
	if err != nil {
		return nil, err
	}
	fmt.Println("Accepted new connection from:", conn.RemoteAddr())

	ssl := wolfSSL.WolfSSL_new(cl.ctx)
	if ssl == nil {
		fmt.Println("WolfSSL_new Failed")
		os.Exit(1)
	}

	file, err := conn.(*net.TCPConn).File()
	if err != nil {
		panic(err)
	}
	fd := file.Fd()
	wolfSSL.WolfSSL_set_fd(ssl, int(fd))

	ret := wolfSSL.WolfSSL_accept(ssl)
	if ret != wolfSSL.WOLFSSL_SUCCESS {
		fmt.Println("WolfSSL_accept error ", ret)
	} else {
		fmt.Println("Client Successfully Connected!")
	}

	return &wolfSSLConn{
		conn: conn,
		ssl:  ssl,
	}, nil
}

// Close closes the listener, making it stop accepting new connections.
func (cl *wolfSSLListener) Close() error {
	fmt.Println("Closing listener...")
	return cl.listener.Close()
}

// Addr returns the listener's network address.
func (cl *wolfSSLListener) Addr() net.Addr {
	return cl.listener.Addr()
}

type wolfSSLConn struct {
	conn   net.Conn
	ssl    *wolfSSL.WOLFSSL
	buffer bytes.Buffer
	mu     sync.Mutex
	closed bool
}

func (w *wolfSSLConn) Read(b []byte) (int, error) {
	log.Infof("Calling read: %d", len(b))

	ret := wolfSSL.WolfSSL_read(w.ssl, b, uintptr(len(b)))
	if ret < 0 {
		errCode := wolfSSL.WolfSSL_get_error(w.ssl, int(ret))
		return 0, fmt.Errorf("read error: %d", errCode)
	}

	log.Infof("Read bytes: %s", string(b[:ret]))
	return int(ret), nil
}

func (w *wolfSSLConn) Write(b []byte) (int, error) {
	log.Infof("Calling write: %d", len(b))

	sz := uintptr(len(b))

	ret := wolfSSL.WolfSSL_write(w.ssl, b, sz)
	if ret < 0 {
		errCode := wolfSSL.WolfSSL_get_error(w.ssl, int(ret))
		return 0, fmt.Errorf("write error: %d", errCode)
	}

	return int(ret), nil
}

func (w *wolfSSLConn) Close() error {
	log.Infof("Closing connection")

	wolfSSL.WolfSSL_shutdown(w.ssl)
	wolfSSL.WolfSSL_free(w.ssl)
	return w.conn.Close()
}

func (w *wolfSSLConn) LocalAddr() net.Addr {
	return w.conn.LocalAddr()
}

func (w *wolfSSLConn) RemoteAddr() net.Addr {
	return w.conn.RemoteAddr()
}

func (w *wolfSSLConn) SetDeadline(t time.Time) error {
	return w.conn.SetDeadline(t)
}

func (w *wolfSSLConn) SetReadDeadline(t time.Time) error {
	return w.conn.SetReadDeadline(t)
}

func (w *wolfSSLConn) SetWriteDeadline(t time.Time) error {
	return w.conn.SetWriteDeadline(t)
}

// Handler for generating and base64 encoding 5KB of random data
func randomDataHandler(w http.ResponseWriter, r *http.Request) {
	// Get the "size" query parameter from the request
	sizeParam := r.URL.Query().Get("size")
	size := 500000 // default size

	// If the "size" parameter is provided, convert it to an integer
	if sizeParam != "" {
		parsedSize, err := strconv.Atoi(sizeParam)
		if err != nil || parsedSize <= 0 {
			http.Error(w, "Invalid size parameter", http.StatusBadRequest)
			return
		}
		size = parsedSize
	}

	// Generate random data of the specified size
	data := make([]byte, size)
	_, err := rand.Read(data)
	if err != nil {
		http.Error(w, "Could not generate random data", http.StatusInternalServerError)
		return
	}

	// Base64 encode the random data
	encodedData := base64.StdEncoding.EncodeToString(data)

	// Set content type and write the base64 encoded data
	w.Header().Set("Content-Type", "application/base64")
	w.Write([]byte(encodedData))
}

func main() {
	port := defaultPort

	// Set logging level
	log.SetLevel(log.InfoLevel)

	log.SetFormatter(&log.TextFormatter{
		DisableColors: false,
		FullTimestamp: true,
	})

	// Set up the HTTP server and routes
	http.HandleFunc("/", randomDataHandler)

	CERT_FILE := "./certs/server-cert.pem"
	KEY_FILE := "./certs/server-key.pem"

	/* Initialize wolfSSL */
	wolfSSL.WolfSSL_Init()

	/* Create WOLFSSL_CTX with tlsv12 */
	ctx := wolfSSL.WolfSSL_CTX_new(wolfSSL.WolfTLSv1_2_server_method())
	if ctx == nil {
		fmt.Println(" WolfSSL_CTX_new Failed")
		os.Exit(1)
	}

	/* Load server certificates into WOLFSSL_CTX */
	ret := wolfSSL.WolfSSL_CTX_use_certificate_file(ctx, CERT_FILE, wolfSSL.SSL_FILETYPE_PEM)
	if ret != wolfSSL.WOLFSSL_SUCCESS {
		fmt.Println("Error: WolfSSL_CTX_use_certificate Failed")
		os.Exit(1)
	}

	/* Load server key into WOLFSSL_CTX */
	ret = wolfSSL.WolfSSL_CTX_use_PrivateKey_file(ctx, KEY_FILE, wolfSSL.SSL_FILETYPE_PEM)
	if ret != wolfSSL.WOLFSSL_SUCCESS {
		fmt.Println("Error: WolfSSL_CTX_use_PrivateKey Failed")
		os.Exit(1)
	}

	baseListener, err := net.Listen("tcp", ":"+port)
	if err != nil {
		fmt.Println("Error starting listener:", err)
		return
	}
	defer baseListener.Close()

	wolfSSLListener := &wolfSSLListener{
		listener: baseListener,
		ctx:      ctx,
	}

	log.Printf("Server listening on https://localhost:%s", port)
	err = http.Serve(wolfSSLListener, nil)
	if err != nil {
		fmt.Println("Error starting HTTP server:", err)
	}
}

更多关于Golang中C分配的指针会被GOGC回收吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

cgo 不会随意回收 C 指针。你可以尝试创建大量的 C 变量,然后观察它们是否会被回收。

更多关于Golang中C分配的指针会被GOGC回收吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中,C分配的指针不会被Go垃圾回收器(GC)直接管理。Go的GC只管理Go堆上的内存,不管理C堆上分配的内存。当你在Go代码中使用CGO并调用C函数分配内存时,需要手动管理这些内存的生命周期。

在你的代码中,问题很可能出现在wolfSSLConn结构体中的ssl指针管理上。当Go GC运行时,如果Go认为wolfSSLConn对象不再被引用,它可能会被回收,但GC不会自动释放C分配的WOLFSSL结构体。

以下是关键问题的分析和解决方案:

问题分析

  1. C指针的生命周期管理wolfSSL.WolfSSL_new()返回的C指针需要显式释放
  2. Go对象与C对象的关联:Go GC不知道C指针的存在,不会阻止其被回收
  3. 文件描述符管理:从net.TCPConn获取的文件描述符需要正确管理

解决方案

1. 使用runtime.KeepAlive确保C指针不被过早回收

import "runtime"

func (w *wolfSSLConn) Write(b []byte) (int, error) {
    log.Infof("Calling write: %d", len(b))
    
    // 确保ssl指针在Write期间不会被GC
    runtime.KeepAlive(w.ssl)
    
    sz := uintptr(len(b))
    ret := wolfSSL.WolfSSL_write(w.ssl, b, sz)
    if ret < 0 {
        errCode := wolfSSL.WolfSSL_get_error(w.ssl, int(ret))
        return 0, fmt.Errorf("write error: %d", errCode)
    }
    
    return int(ret), nil
}

func (w *wolfSSLConn) Read(b []byte) (int, error) {
    log.Infof("Calling read: %d", len(b))
    
    // 确保ssl指针在Read期间不会被GC
    runtime.KeepAlive(w.ssl)
    
    ret := wolfSSL.WolfSSL_read(w.ssl, b, uintptr(len(b)))
    if ret < 0 {
        errCode := wolfSSL.WolfSSL_get_error(w.ssl, int(ret))
        return 0, fmt.Errorf("read error: %d", errCode)
    }
    
    log.Infof("Read bytes: %s", string(b[:ret]))
    return int(ret), nil
}

2. 使用finalizer确保C资源被正确释放

import "runtime"

type wolfSSLConn struct {
    conn   net.Conn
    ssl    *wolfSSL.WOLFSSL
    buffer bytes.Buffer
    mu     sync.Mutex
    closed bool
}

func newWolfSSLConn(conn net.Conn, ssl *wolfSSL.WOLFSSL) *wolfSSLConn {
    wc := &wolfSSLConn{
        conn: conn,
        ssl:  ssl,
    }
    
    // 设置finalizer,确保C资源被释放
    runtime.SetFinalizer(wc, func(w *wolfSSLConn) {
        if w.ssl != nil && !w.closed {
            wolfSSL.WolfSSL_shutdown(w.ssl)
            wolfSSL.WolfSSL_free(w.ssl)
            w.ssl = nil
        }
    })
    
    return wc
}

// 在Accept方法中使用newWolfSSLConn
func (cl *wolfSSLListener) Accept() (net.Conn, error) {
    conn, err := cl.listener.Accept()
    if err != nil {
        return nil, err
    }
    fmt.Println("Accepted new connection from:", conn.RemoteAddr())

    ssl := wolfSSL.WolfSSL_new(cl.ctx)
    if ssl == nil {
        fmt.Println("WolfSSL_new Failed")
        conn.Close()
        return nil, fmt.Errorf("WolfSSL_new failed")
    }

    file, err := conn.(*net.TCPConn).File()
    if err != nil {
        conn.Close()
        wolfSSL.WolfSSL_free(ssl)
        return nil, err
    }
    defer file.Close()  // 重要:关闭文件描述符的副本
    
    fd := file.Fd()
    wolfSSL.WolfSSL_set_fd(ssl, int(fd))

    ret := wolfSSL.WolfSSL_accept(ssl)
    if ret != wolfSSL.WOLFSSL_SUCCESS {
        fmt.Println("WolfSSL_accept error ", ret)
        conn.Close()
        wolfSSL.WolfSSL_free(ssl)
        return nil, fmt.Errorf("WolfSSL_accept failed: %d", ret)
    }

    fmt.Println("Client Successfully Connected!")
    return newWolfSSLConn(conn, ssl), nil
}

3. 改进Close方法,确保资源正确清理

func (w *wolfSSLConn) Close() error {
    w.mu.Lock()
    defer w.mu.Unlock()
    
    if w.closed {
        return nil
    }
    
    log.Infof("Closing connection")
    
    var errs []error
    
    if w.ssl != nil {
        wolfSSL.WolfSSL_shutdown(w.ssl)
        wolfSSL.WolfSSL_free(w.ssl)
        w.ssl = nil
    }
    
    if w.conn != nil {
        if err := w.conn.Close(); err != nil {
            errs = append(errs, err)
        }
    }
    
    w.closed = true
    // 移除finalizer,因为我们已经手动清理了
    runtime.SetFinalizer(w, nil)
    
    if len(errs) > 0 {
        return fmt.Errorf("errors closing connection: %v", errs)
    }
    return nil
}

4. 使用sync.Pool管理连接对象(可选优化)

var wolfSSLConnPool = sync.Pool{
    New: func() interface{} {
        return &wolfSSLConn{}
    },
}

func getWolfSSLConn(conn net.Conn, ssl *wolfSSL.WOLFSSL) *wolfSSLConn {
    wc := wolfSSLConnPool.Get().(*wolfSSLConn)
    wc.conn = conn
    wc.ssl = ssl
    wc.closed = false
    wc.buffer.Reset()
    runtime.SetFinalizer(wc, (*wolfSSLConn).finalize)
    return wc
}

func (w *wolfSSLConn) finalize() {
    if w.ssl != nil && !w.closed {
        wolfSSL.WolfSSL_shutdown(w.ssl)
        wolfSSL.WolfSSL_free(w.ssl)
        w.ssl = nil
    }
    wolfSSLConnPool.Put(w)
}

关键点

  1. C内存需要手动管理:Go GC不管理C分配的内存
  2. 使用runtime.KeepAlive:防止Go GC在C调用期间回收包含C指针的Go对象
  3. 正确使用finalizer:作为备份机制,确保C资源最终被释放
  4. 文件描述符管理:从net.TCPConn获取的File需要正确关闭
  5. 并发安全:确保Close方法是线程安全的

禁用GC只是掩盖了问题,正确的解决方案是确保C指针在需要时保持有效,并在不再需要时正确释放。

回到顶部