Golang直接操作共享内存

前言

故事起源于要搭一个高性能的日志中心。当然使用了elk这一套。但是,对于logstash来说,它主要使用的是文件日志的方式了捕捉log。而写文件日志的话会非常慢。对于实时日志要处理滚动的日志更是这样,每次检查是否需要流动日志,然后打开日志,然后写入,然后关闭,当然这中间可以优化。这一切都是那么慢,发起了n个系统调用,硬盘寻道等。这时候想到了用共享内存来通信。

共享内存的基本知识

要使用共享内存要执行以下几步:

  1. 发起一个系统调用,让系统帮你生产一块内存,或者取得一块已经存在的内存来使用。
  2. 把内存attach到当前进程,让当前进程可以使用。大家都知道,我们在进程中访问的是虚拟内存地址,系统会把它映射到物理内存中。如果没有这一步,第1步创建的内存就不能在当前进程访问。
  3. 这时就可以对内存进程读写操作了。
  4. 进程结束的时候要把上面attach的内存给释放。

系统调用的基础知识

什么是系统调用?

系统调用(英语:system call),又称为系统呼叫,指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。

以上引自维基百科。

对于每个系统调用,都一个编号。内核收到编号后,就根据编号去找到对应的内核函数函数来执行。然后返回给应用程序。

系统调用是怎么发起的?以下以linux为例。

  1. 应用程序以系统调用号和对应的参数传给系统调用api
  2. 系统调用api将系统调用号存到eax中,然后发起0x80的中断号进行中断
  3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用)
  4. 系统调用完成相应功能,将返回值存入 eax,返回到中断处理函数
  5. 中断处理函数返回到 API 中
  6. API 将 eax 返回给应用程序

以上就完成了系统调用。

在golang中使用共享内存

了解了系统调用之后,下面就开始使用了。第一步当然是去找golang有没有直接提供共享内存的api了。几经折腾后,发现它并没有提供直接的api。而其他很多系统调用都提供了直接的api。究其原因,我想应该是因为这句话吧:

“不要通过共享内存来通信,而应该通过通信来共享内存”

golang不提供使用共享内存来通信。所以直接不提供了,折腾死你们,让你们用不了。
于是乎,google一下解决方案,都是通过cgo来调c语言来实现的。stackoverflow的答案也都是这样。
回来再来看一下golang的syscall的文档。它提供了Syscall函数。声明如下:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

很显示trap是中断号,a1, a2, a3是系统调用相关的参数。
对于中断号,在文档中可以看到,所有的系统都已经定义了常量了。而我们要用到的系统调用有:
SYS_SHMGET: 创建或者取得共享内存。
SYS_SHMAT: 将共享内存attach到当前进程空间。
SYS_SHMDT: 将共享内存从当前进程中deattach。

具体这三个函数的使用,我们可以参考linux的shmgetshmatshmdt函数。可以看到这三个函数跟上面三个系统调用号的常量名字一样的。 以下是这三个函数的声明:

int shmget(key_t key, size_t size, int shmflg);  
void *shmat(int shm_id, const void *shm_addr, int shmflg); 
int shmdt(const void *shmaddr);

以下简单介绍一下这三个函数,具体可以直接去linux上man对应的文档。

shmget函数

key,这个参数的类型key_y其实只是一个数字类型。这个参数命名了这一块内存。不要提供0值就行了,0值是private的,不能在进程间共享。
size,提供了共享内存的大小。
shmflg,权限标志,它的作用与open函数的mode参数一样。如果需要在内存不存在时创建它,则需要指定IPC_CREAT
在golang的文档中可以看到,它并没定义IPC_CREATE的值。所以我们只能去找到它的值了。在linux的man文档中,它也没有说明。于是乎,直接把linux的代码clone下来进行了grep(我用ag,速度非常快的文档查找工具)。从结果中找到了IPC_CREATE是一个宏,它的值定义成了00001000。一个8进制的数字。低三位都是0,因为低三位是用来标志权限位的。

下面我们直接来发起这个系统调用看一下效果,把调用c的参数一一对应到a1, a2, a3中:

shmid, _, err := syscall.Syscall(syscall.SYS_SHMGET, 2, 4, IpcCreate|0600)

Syscall函数返回了两个值和一个error字段。而c的shmget只返回了一个int值,因为这个函数把结果错误和结果都通过返回值来承载了,如果是小于0的,则是错误,这时对应到go中应该是err的值,没有错误的时候,我们只需要一个返回值,第二个返回值会一直是0。第一个返回值就是给shmat调用的第一个参数。

shmat函数

shm_id, 这是shmget返回id,以标志了要attach的是这一块内存
shm_addr,这个标志需要把它attach到的内存地址,通常填0,让系统去选择地址来attach
shmflg,这个可以值SHM_RDONLY表示只读,其他值为可以读写,我们直接传0就好。

shmaddr, _, err := syscall.Syscall(syscall.SYS_SHMAT, shmid, 0, 0)

c函数返回了进程空间地址,这个调用也是只返回了一个值,所我们只接收第一个值。在c中,如果调用失败,会返回-1。在go中,我们只要直接处理err的值就好了。

shmdt函数

shmaddr, 这个参数表示deattach的地址值,是从shmat中返回的。 我们在go中直接用defer来调用就好了:

defer syscall.Syscall(syscall.SYS_SHMDT, shmaddr, 0, 0)

以下是这个blog用到的代码,可以直接从gist里去下载:

// @file main.go
// @brief
// @author tenfyzhong
// @email tenfyzhong@qq.com
// @created 2017-06-26 17:54:34
package main

import (
    "flag"
    "fmt"
    "os"
    "syscall"
    "time"
    "unsafe"
)

const (
    // IpcCreate create if key is nonexistent
    IpcCreate = 00001000
)

var mode = flag.Int("mode", 0, "0:write 1:read")

func main() {
    flag.Parse()
    shmid, _, err := syscall.Syscall(syscall.SYS_SHMGET, 2, 4, IpcCreate|0600)
    if err != 0 {
        fmt.Printf("syscall error, err: %v\n", err)
        os.Exit(-1)
    }
    fmt.Printf("shmid: %v\n", shmid)

    shmaddr, _, err := syscall.Syscall(syscall.SYS_SHMAT, shmid, 0, 0)
    if err != 0 {
        fmt.Printf("syscall error, err: %v\n", err)
        os.Exit(-2)
    }
    fmt.Printf("shmaddr: %v\n", shmaddr)

    defer syscall.Syscall(syscall.SYS_SHMDT, shmaddr, 0, 0)

    if *mode == 0 {
        fmt.Println("write mode")
        i := 0
        for {
            fmt.Printf("%d\n", i)
            *(*int)(unsafe.Pointer(uintptr(shmaddr))) = i
            i++
            time.Sleep(1 * time.Second)
        }
    } else {
        fmt.Println("read mode")
        for {
            fmt.Println(*(*int)(unsafe.Pointer(uintptr(shmaddr))))
            time.Sleep(1 * time.Second)
        }
    }
}

运行一下这个代码块看一下结果:用ipcs可以看到共享已经成功创建。

对于共享内存的操作,大家还可以看一下shmctl这个系统调用的使用。

出处:https://studygolang.com/articles/10203

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Go 中使用 syscall 库来读取内存需要使用 syscall.PtracePeekData() 函数。这个函数需要传入两个参数,一个是进程 ID,另一个是内存地址。它返回一个字节数组和一个错误信息。示例代码如下: ``` package main import ( "fmt" "syscall" ) func main() { data, err := syscall.PtracePeekData(pid, addr) if err != nil { fmt.Println(err) } fmt.Println(data) } ``` 其中 pid 是要读取内存的进程 ID,addr 是要读取内存的地址。 需要注意的是,在 Go 中读取其他进程的内存需要获得相应的权限,可以在程序启动时使用 syscall.PtraceAttach() 函数附加到目标进程上。 ### 回答2: 在Golang中,可以使用syscall包中的ReadProcessMemory函数来读取其他进程的内存。该函数需要传入需要读取内存的进程的句柄、要读取的内存地址以及一个指向要保存读取结果的缓冲区的指针。 首先,需要使用syscall包中的OpenProcess函数打开要读取内存的进程。OpenProcess函数接受一个进程ID和访问权限参数,并返回一个进程句柄。可以通过进程ID使用os.FindProcess函数获取进程句柄。然后,将得到的进程句柄传递给ReadProcessMemory函数。 接下来,需要通过syscall包中的VirtualQueryEx函数获取要读取的内存的一些基本信息,例如内存所在的模块、保护方式等。这些信息包含在MEMORY_BASIC_INFORMATION结构体中。使用VirtualQueryEx函数时,需要传入进程句柄和要查询的内存地址。函数会在指定地址处返回一个MEMORY_BASIC_INFORMATION结构体的指针。 然后,可以通过ReadProcessMemory函数读取内存。该函数需要传入进程句柄、要读取的内存地址以及一个指向要保存读取结果的缓冲区的指针。可以根据前面获取到的MEMORY_BASIC_INFORMATION结构体的信息,确定要读取的内存地址范围和大小,然后将这些信息传递给ReadProcessMemory函数。成功读取内存后,可以将缓冲区中的数据转换为所需的数据类型。 最后,需要使用syscall包中的CloseHandle函数关闭打开的进程句柄,以释放资源。 需要注意的是,使用syscall包中的函数需要在适当的操作系统上运行,并且需要有足够的权限来访问指定的进程内存。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值