Golang中使用syscall.EpollEvent构建syscall.EpollWait时遇到内存异常崩溃问题

Golang中使用syscall.EpollEvent构建syscall.EpollWait时遇到内存异常崩溃问题

在 ztypes_linux_amd64.go 中

type EpollEvent struct {                                                                             
    Events uint32                                                                                    
    Fd     int32                                                                                     
    Pad    int32                                                                                     
} 

man 2 epoll_ctl
typedef union epoll_data {
   void        *ptr;
   int          fd;
   uint32_t     u32;
   uint64_t     u64;
} epoll_data_t;

struct epoll_event {
   uint32_t     events;      /* Epoll events */
   epoll_data_t data;        /* User data variable */
};

我想要

type evData struct {
    fd        int
    evHandler EvHandler
}
ev := syscall.EpollEvent{
    Events: syscall.EPOLLIN,
}
*(**evData)(unsafe.Pointer(&ev.Fd)) = ed

使用 unsafe.Pointersyscall.EpollEvent.Fd 之后的内存区域转换为 epoll_data_t.ptr 内存。

经过测试,在少量客户端请求时没有问题。然而,当并发级别增加时,它会立即崩溃并抛出内存异常。

unexpected fault address 0x1018
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x1 addr=0x1018 pc=0x473dc7]

goroutine 1 [running, locked to thread]:
runtime.throw({0x48c50d?, 0xc00004bda0?})
panic.go:1047 +0x5d fp=0xc00004bd50 sp=0xc00004bd20 pc=0x43213d
runtime.sigpanic()
signal_unix.go:851 +0x28a fp=0xc00004bdb0 sp=0xc00004bd50 pc=0x44674a
goev.(*evPoll).poll(0xc00005a040, 0x0, 0x0)
epoll.go:202 +0x3e7 fp=0xc00004be68 sp=0xc00004bdb0 pc=0x473dc7
goev.(*evPoll).run(0xc00005a040)

为什么? 实现的代码可以在 (goev/epoll.go at main · shaovie/goev · GitHub) 找到。

var (
    _zero uintptr
)
type epollEvent struct {
    Events uint32
    Fd    [8]byte
}
func errnoErr(e syscall.Errno) error {
    switch e {
    case 0:
        return nil
    case unix.EAGAIN:
        return syscall.EAGAIN
    case unix.EINVAL:
        return syscall.EINVAL
    case unix.ENOENT:
        return syscall.ENOENT
    }
    return e
}
func epollWait(epfd int, events []epollEvent, msec int) (n int, err error) {
    var _p0 unsafe.Pointer
    if len(events) > 0 {
        _p0 = unsafe.Pointer(&events[0])
    } else {
        _p0 = unsafe.Pointer(&_zero)
    }

    r0, _, e1 := unix.Syscall6(unix.SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
    n = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }

    return
}
func epollCtl(epfd int, op int, fd int, event *epollEvent) (err error) {
    _, _, e1 := unix.RawSyscall6(unix.SYS_EPOLL_CTL, uintptr(epfd), uintptr(op), uintptr(fd), uintptr(unsafe.Pointer(event)), 0, 0)
    if e1 != 0 {
        err = errnoErr(e1)
    }

    return
}

我从 runtime.netpoll_epoll.go 复制了实现,但它仍然崩溃。


更多关于Golang中使用syscall.EpollEvent构建syscall.EpollWait时遇到内存异常崩溃问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中使用syscall.EpollEvent构建syscall.EpollWait时遇到内存异常崩溃问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题在于你错误地处理了 syscall.EpollEvent 的内存布局。syscall.EpollEventFd 字段是 int32 类型,而 epoll_data_t 在 64 位系统上是 8 字节。你的代码试图将 Fd 之后的内存当作 epoll_data_t.ptr 使用,这会导致内存越界访问。

正确的做法是使用与内核 epoll_event 结构体完全匹配的内存布局。以下是修复后的示例:

package main

import (
    "syscall"
    "unsafe"
)

// 使用与内核完全匹配的 epoll_event 结构
type EpollEvent struct {
    Events uint32
    // 8字节的 data 字段,对应 epoll_data_t
    Data [8]byte
}

// 将 evData 指针存储到 EpollEvent.Data 中
func setEventData(ev *EpollEvent, ed *evData) {
    // 将 evData 指针转换为 uintptr 再存储到 Data 字段
    ptr := uintptr(unsafe.Pointer(ed))
    *(*uintptr)(unsafe.Pointer(&ev.Data[0])) = ptr
}

// 从 EpollEvent.Data 中获取 evData 指针
func getEventData(ev *EpollEvent) *evData {
    ptr := *(*uintptr)(unsafe.Pointer(&ev.Data[0]))
    return (*evData)(unsafe.Pointer(ptr))
}

type evData struct {
    fd        int
    evHandler EvHandler
}

type EvHandler interface {
    HandleEvent(events uint32)
}

// 使用示例
func main() {
    // 创建 epoll 实例
    epfd, err := syscall.EpollCreate1(0)
    if err != nil {
        panic(err)
    }
    defer syscall.Close(epfd)

    // 创建 socket
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
    if err != nil {
        panic(err)
    }
    defer syscall.Close(fd)

    // 准备事件数据
    ed := &evData{
        fd:        fd,
        evHandler: nil, // 设置实际的处理器
    }

    // 创建 epoll 事件
    var ev EpollEvent
    ev.Events = syscall.EPOLLIN | syscall.EPOLLET
    setEventData(&ev, ed)

    // 添加事件到 epoll
    err = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, fd, (*syscall.EpollEvent)(unsafe.Pointer(&ev)))
    if err != nil {
        panic(err)
    }

    // 等待事件
    events := make([]EpollEvent, 128)
    for {
        n, err := syscall.EpollWait(epfd, *(*[]syscall.EpollEvent)(unsafe.Pointer(&events)), -1)
        if err != nil {
            if err == syscall.EINTR {
                continue
            }
            panic(err)
        }

        for i := 0; i < n; i++ {
            // 获取事件数据
            ed := getEventData(&events[i])
            // 处理事件
            if events[i].Events&syscall.EPOLLIN != 0 {
                // 处理读事件
            }
        }
    }
}

关键点:

  1. EpollEvent.Data 字段定义为 [8]byte,确保在 64 位系统上有正确的 8 字节大小
  2. 使用 setEventDatagetEventData 函数安全地存储和获取指针
  3. 在调用 syscall.EpollCtlsyscall.EpollWait 时进行正确的类型转换

这种实现避免了内存越界访问,能够处理高并发场景。

回到顶部