如何在子进程中访问父进程内存映射区?共享映射访问异常排查
解决CLONE_VM共享内存映射区+ptrace写入的问题
首先得指出你代码里的核心问题:你用os/exec启动子进程的方式完全抵消了CLONE_VM的作用。exec系统调用会彻底替换子进程的地址空间——原来父进程里创建的内存映射、变量等都会被新程序的地址空间覆盖,自然子进程访问不到之前的映射区了。要实现共享地址空间,你需要直接调用syscall.Clone创建子进程,而不是走fork+exec的流程。
下面是修正后的完整代码,我会一步步解释关键细节:
package main import ( "fmt" "log" "syscall" "unsafe" ) // 子进程要执行的函数,会访问共享内存 func childFunc(sharedMem unsafe.Pointer) { // 等待父进程通过ptrace写入数据 fmt.Println("子进程:等待父进程写入共享内存...") syscall.Pause() // 读取共享内存内容 data := (*[10]byte)(sharedMem) fmt.Printf("子进程:读取到共享内存数据:%s\n", string(data[:4])) } func main() { // 创建临时文件用于内存映射 f, err := syscall.Open("/tmp/test_shared", syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0644) if err != nil { log.Fatalf("创建临时文件失败:%v", err) } defer syscall.Close(f) // 先截断文件到需要的大小(10字节),避免映射超出文件范围 if err := syscall.Ftruncate(f, 10); err != nil { log.Fatalf("截断文件失败:%v", err) } // 创建内存映射区:PROT_READ|PROT_WRITE 表示读写权限,MAP_SHARED 表示映射同步到文件(如果不需要同步文件也可以用MAP_PRIVATE) sharedMem, err := syscall.Mmap(f, 0, 10, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) if err != nil { log.Fatalf("内存映射失败:%v", err) } defer syscall.Munmap(sharedMem) // 先写入初始数据到映射区 copy(sharedMem, []byte("4321")) fmt.Printf("父进程:初始共享内存数据:%s\n", string(sharedMem[:4])) // 准备子进程的栈空间(clone需要单独的栈) stack := make([]byte, 8192) stackTop := uintptr(unsafe.Pointer(&stack[len(stack)])) // 调用clone创建子进程,指定CLONE_VM标志(共享地址空间),同时指定子进程入口函数 childPid, _, errno := syscall.RawSyscall6( syscall.SYS_CLONE, uintptr(syscall.CLONE_VM|syscall.SIGCHLD), // 标志:共享VM+子进程退出时发SIGCHLD stackTop, // 子进程栈顶(栈是向下增长的) 0, 0, 0, 0, ) if errno != 0 { log.Fatalf("clone失败:%v", errno) } if childPid == 0 { // 子进程执行逻辑 childFunc(unsafe.Pointer(&sharedMem[0])) syscall.Exit(0) } fmt.Printf("父进程:子进程PID:%d\n", childPid) // 使用ptrace向共享内存写入数据 // 1. 先attach到子进程 if err := syscall.PtraceAttach(int(childPid)); err != nil { log.Fatalf("ptrace attach失败:%v", err) } defer syscall.PtraceDetach(int(childPid)) // 等待子进程进入停止状态 var waitStatus syscall.WaitStatus if _, err := syscall.Wait4(int(childPid), &waitStatus, 0, nil); err != nil { log.Fatalf("wait4失败:%v", err) } // 2. 向共享内存地址写入新数据(比如"1234") data := []byte("1234") for i := 0; i < len(data); i++ { addr := uintptr(unsafe.Pointer(&sharedMem[i])) if err := syscall.PtracePokeData(int(childPid), addr, uintptr(data[i])); err != nil { log.Fatalf("ptrace pokedata失败:%v", err) } } fmt.Println("父进程:已通过ptrace写入新数据到共享内存") // 3. 让子进程继续执行 if err := syscall.PtraceCont(int(childPid), 0); err != nil { log.Fatalf("ptrace cont失败:%v", err) } // 等待子进程退出 if _, err := syscall.Wait4(int(childPid), &waitStatus, 0, nil); err != nil { log.Fatalf("等待子进程退出失败:%v", err) } fmt.Println("父进程:子进程已退出") }
关键细节解释:
- 摒弃
os/exec改用clone:exec会完全替换子进程地址空间,之前的内存映射会丢失。直接调用syscall.RawSyscall6触发clone系统调用,带上CLONE_VM标志,就能让父子进程共享同一地址空间。 - 内存映射的正确姿势:
- 先用
Ftruncate设置文件大小,确保映射范围有效; - 给足
PROT_READ|PROT_WRITE权限,保证读写操作正常; MAP_SHARED会同步修改到磁盘文件,若不需要磁盘同步,用MAP_PRIVATE也能实现进程间内存共享(因为地址空间本身是共享的)。
- 先用
- ptrace的标准流程:
- 先
PtraceAttach到子进程,等待其进入停止状态; - 通过
PtracePokeData向共享内存地址写入数据(因为地址空间共享,这个地址在父子进程中完全一致); - 最后用
PtraceCont让子进程恢复执行,读取修改后的内存内容。
- 先
- 子进程栈空间准备:
clone要求为子进程提供独立栈空间,这里用8KB切片作为栈,栈顶指向切片末尾(栈是向下增长的)。
运行这段代码后,你会看到父子进程能正确共享内存,父进程通过ptrace写入的数据也能被子进程正常读取。
内容的提问来源于stack exchange,提问作者user5061907
相关产品推荐
相关产品推荐

