4 4
使用Go结合windows dll开发程序

Go程序有一个优点是很好地做到”跨平台”,一般开发的情况,我们使用内置的相关模块实现相关功能,通过 GOOS=操作系统代号 go build 就能编译出对应平台的二进制文件. 然后把二进制文件扔往对应的服务器无论是linux或windows, 基本都能正常运行.

但是Go也不是完全的跨平台,个别情况下并没有提供Windows下的相关方法,只能通过syscall包去调用Win库.

本文主要说说我们做跨平台的时候,针对windows的一些处理: 一种常见的方式是我们需要在go的代码里面使用个别的dll文件

DLL是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX(包含ActiveX控制的库)或者.DRV(旧式的系统驱动程序)。

所谓动态链接(DLL),就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL档,当可执行文件调用到DLL档内的函数时,Windows操作系统才会把DLL档加载内存内,DLL档本身的结构就是可执行档,当程序有需求时函数才进行链接。通过动态链接方式,内存浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。

要想更深入了解Go的DLL编程,可以先阅读golang官方的相关资料 : WindowsDLLs

什么是syscall

在电脑中,系统调用(英语:system call),又称为系统呼叫,指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。”

实际上,你基本上做任何事情的时候,都需要系统调用。

  • 访问文件
  • 访问设备
  • 进程管理
  • 通讯
  • 时间 …

无论你是写C程序、写Go程序或者哪怕是写bash脚本,你实际上都会用到syscall

举一个简单的Go的例子 hello.go

package main
import "fmt"
func main() {
    fmt.Println("Hello, Tencent!")
}

如果我们在 Linux 上构建,并且使用 strace 的话,就可以看到发生了多少系统调用了:

$ go build hello.go
$ strace ./hello
execve("./hello", ["./hello"], [/* 23 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x52c008)       = 0
sched_getaffinity(0, 8192, [0])         = 8
mmap(0xc000000000, 65536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
munmap(0xc000000000, 65536)             = 0
...
futex(0x52c0b0, FUTEX_WAIT, 0, NULL)    = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1eef3ac000
write(1, "Hello, Tencent!\n", 18Hello, Tencent!
)     = 18
futex(0x52ba58, FUTEX_WAKE, 1)          = 1
futex(0x52b990, FUTEX_WAKE, 1)          = 1
exit_group(0)                           = ?
+++ exited with 0 +++

可以关注一下最后的那个 write(), 那里就发生了syscall. 我们这里不关心相关底层细节,我们知道我们每天的编程其实都和syscall在互动

如何操作 syscall

syscall() saves CPU registers before making the system call, restores the registers upon return from the system call, and stores any error code returned by the system call in errno(3) if an error occurs.

就是说调用 syscall 之前先保存环境;syscall 返回之后,恢复环境;错误代码在 errno 中

以 sys_write 调用为例,系统调用号放到了 %rax 之中,文件的 fd 放到了 %rdi 中,要写入的 buf 放到了 %rsi 中,写入长度放到了 %rdx 之中。

当执行了 syscall() 后,开始进入 Trap,然后进入内核态,开始执行对应的系统调用的代码。而系统调用的返回值,会放到了 %rax 之中。

syscall vs. windows

举个例子,如果我们的程序平常运行在linux,为了做到程序的可可移植性, 我们会或多或少地调用winodws的相关API

下面我就通过一份代码,来说明Go在windows下运用dll,获取各个盘区的容量信息:

package main

import (
    "errors"
    "fmt"
    "syscall"
    "unsafe"
)

var kernel32 syscall.Handle

//初始化获取方法的引用
func init() {
    var err error
    kernel32, err = syscall.LoadLibrary("kernel32.dll")
    if err != nil {
        panic("获取方法应用错误")
    }

}

func getDriveNames() ([]string, error) {

    drives := []string{}

    LongPtr_DriveBuf := make([]byte, 256)

    getDrivesStringsEx, err := syscall.GetProcAddress(kernel32, "GetLogicalDriveStringsW")
    if err != nil {
        return nil, errors.New("call GetLogicalDriveStringsW fail")
    }

    //执行调用
    // 因为有2个参数,所以使用syscall就能放得下,最后的参数补0
    r, _, errno := syscall.Syscall(uintptr(getDrivesStringsEx), 2,
        uintptr(len(LongPtr_DriveBuf)),
        uintptr(unsafe.Pointer(&LongPtr_DriveBuf[0])), 0)

    if r != 0 {

        for _, v := range LongPtr_DriveBuf {
            if v < 65 || v > 90 {
                continue
            }
            //println(string(v))
            drives = append(drives, string(v)+":")
        }

    } else {
        return nil, errors.New(errno.Error())
    }

    return drives, nil
}

func getDiskGreeSpace(diskName string) {

    //将磁盘的名称转化为*UTF16
    diskNameUTF16Ptr, _ := syscall.UTF16PtrFromString(diskName)

    //使用长指针
    LongPtr_FreeBytesAvailable := int64(0)     //剩余空间
    LongPtr_TotalNumberOfBytes := int64(0)     //总空间
    LongPtr_TotalNumberOfFreeBytes := int64(0) //可用空间

    //获取方法的引用
    kernel32, err := syscall.LoadLibrary("kernel32.dll")
    if err != nil {
        panic("获取方法应用错误")
    }

    //释放方法引用
    defer syscall.FreeLibrary(kernel32)

    getDiskFreeSpaceEx, err := syscall.GetProcAddress(kernel32, "GetDiskFreeSpaceExW")
    if err != nil {
        panic("call GetZDiskFreeSpaceExW fail")
    }

    //执行调用
    // 因为有四个参数,所以使用syscall6才能放得下,最后两个参数补0
    r, _, errno := syscall.Syscall6(uintptr(getDiskFreeSpaceEx), 4,
        uintptr(unsafe.Pointer(diskNameUTF16Ptr)),
        uintptr(unsafe.Pointer(&LongPtr_FreeBytesAvailable)),
        uintptr(unsafe.Pointer(&LongPtr_TotalNumberOfBytes)),
        uintptr(unsafe.Pointer(&LongPtr_TotalNumberOfFreeBytes)),
        0, 0)

    if r != 0 {
        fmt.Printf(">>>> %s 的空间情况\n", diskName)
        fmt.Printf("剩余空间:%d G\n", LongPtr_FreeBytesAvailable/1024/1024/1024)
        fmt.Printf("用户可用空间:%d G\n", LongPtr_TotalNumberOfBytes/1024/1024/1024)
        fmt.Printf("剩余可用空间:%d G\n", LongPtr_TotalNumberOfFreeBytes/1024/1024/1024)

    } else {
        //此处的errno不是error接口,而是 type Errorno uintptr
        panic(errno)
    }
}

func main() {
    //释放方法引用
    defer syscall.FreeLibrary(kernel32)

    drives, err := getDriveNames()
    if err != nil {
        panic(err)
    }

    for _, d := range drives {
        //获取磁盘可用空间
        getDiskGreeSpace(d)
    }
}

为了可以丰富功能,做到更好的可移植性, 大家日常可以多看 MSDN文档 https://msdn.microsoft.com/en-us/library/windows/desktop/hh447209(v=vs.85)

3 11
关于网络的混杂模式

什么是网络的混杂模式

混杂模式(promiscuous mode)是指一台机器的网卡能够接收所有经过它的数据流,而不论其目的地址是否是它。

维基百科:一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据。当网卡工作在混杂模式下时,网卡将来自接口的所有数据都捕获并交给相应的驱动程序(即不验证MAC地址)。网卡的混杂模式一般在网络管理员分析网络数据作为网络故障诊断手段时用到,同时这个模式也被网络黑客利用来作为网络数据窃听的入口。

网卡具有如下的几种工作模式:

  • 广播模式(Broad Cast Model):它的物理地址(MAC)地址是 0Xffffff 的帧为广播帧,工作在广播模式的网卡接收广播帧。

  • 多播传送(MultiCast Model):多播传送地址作为目的物理地址的帧可以被组内的其它主机同时接收,而组外主机却接收不到。但是,如果将网卡设置为多播传送模式,它可以接收所有的多播传送帧,而不论它是不是组内成员。

  • 直接模式(Direct Model):工作在直接模式下的网卡只接收目地址是自己 Mac地址的帧。

  • 混杂模式(Promiscuous Model):工作在混杂模式下的网卡接收所有的流过网卡的帧,信包捕获程序就是在这种模式下运行的。

网卡的缺省工作模式包含广播模式和直接模式,即它只接收广播帧和发给自己的帧。如果采用混杂模式,一个站点的网卡将接受同一网络内所有站点所发送的数据包这样就可以到达对于网络信息监视捕获的目的。

linux 下通过命令ifconfig基本能查询当前网卡是使用哪种的工作模式

线上的一次真实案例

几个月前负责帮公司内部数据监控团队写了一个应用流量采集的底层进程模块,该模块基于Go使用的是libpcap库对网络设备的流量进行捕获并根据我们自身的业务进行封装整理。这个后台进程运行了几个月了,一直相当“安分”,没有引起告警或者占用过多资源等现场。不过”不幸”的事终于还是发生了。下面我还原一下事情的经过:

某天,收到运维的同学反馈某些机器上的组件如redis、mc有超时,运维的同事协助观察发现,组件的GC正常、网络层面无丢包、mc无异常,真正对比了Haproxy的配置均无变更。但涉及的影响面20w请求就有1000~2000次的timeout现象。

到后来,我们排查系统日志发现有蓝牙设备初始化的记录,时间点比较符合,网卡不断地在混杂模式中来回切换,可能与此相关:

pro

经过IDC的同事排查,机房并没有外接到蓝牙设备,系统本身也没有;后来我们在一些机器上移走蓝牙模块,单担心混杂模式来回切换的现象还是会出现。

后来我们了解到如果使用tcpdump的时候,会启用网卡的混杂模式,但服务器正常时间段内,并没有运维人员去使用tcpdump去进行操作。于是我们就把问题定位在可能是有一些后台应用定时地调用一些网卡采集的功能,于是我们通过日志分析,定位在之前的应用流量采集的后台工具中,于是我们针对有问题的机器,停止了该服务,观察了一段时间,结果问题得到解决,混杂模式没有再来回切换的情况,超时现象没有再发生。

修补方案

既然问题得到定位,就要思考问什么,和如何修复了。应用流量监控模块,我这边是直接采用了 github.com/google/gopacket/pcap 这个库。

后来发现问题所在了,我其中的handler调用的函数定义:


// OpenLive opens a device and returns a *Handle.
// It takes as arguments the name of the device ("eth0"), the maximum size to
// read for each packet (snaplen), whether to put the interface in promiscuous
// mode, and a timeout.
//
// See the package documentation for important details regarding 'timeout'.
func OpenLive(device string, snaplen int32, promisc bool, timeout time.Duration) (handle *Handle, _ error) {
    buf := (*C.char)(C.calloc(errorBufferSize, 1))
    defer C.free(unsafe.Pointer(buf))

    var pro C.int
    if promisc {
        pro = 1
    }
    p := &Handle{timeout: timeout, device: device}

    ifc, err := net.InterfaceByName(device)
    if err != nil {
        // The device wasn't found in the OS, but could be "any"
        // Set index to 0
        p.deviceIndex = 0
    } else {
        p.deviceIndex = ifc.Index
    }

    dev := C.CString(device)
    defer C.free(unsafe.Pointer(dev))

    p.cptr = C.pcap_open_live(dev, C.int(snaplen), pro, timeoutMillis(timeout), buf)
    if p.cptr == nil {
        return nil, errors.New(C.GoString(buf))
    }

    if err := p.openLive(); err != nil {
        C.pcap_close(p.cptr)
        return nil, err
    }

    return p, nil
}

其中第三个参数是指是否开启混杂模式,我之前调用的代码是这样的:

handle, err := pcap.OpenLive(faceName, int32(*snaplen), true, 500)

坑啊,之前没有仔细测试,直接赋值了一个true, 也就是说这个服务强制了使用了混杂模式对网卡进行监听,在流量高峰的时候,很容易导致某些网络服务timeout的现象。 啊~,心中万马奔腾,原来是自己的大意疏忽!!要引以为戒。

1 1
Go的Bit操作

原文地址:https://medium.com/learning-the-go-programming-language/bit-hacking-with-go-e0acee258827

本文是对原文的个人翻译,翻译不是很完善,能理解其中的思想即可。翻译原文的原因是之前参加公司的算法比赛,做题过程中我们在自身的程序做了较多的位运算操作,觉得比较有意思,于是根据原文翻译出下文内容 :)

在计算机正处于发展初期的阶段,内存和处理能力相对有限,于是为了避免这些昂贵的开销,人们一般都首选直接进行位操作的方式获取数据。直至今天,虽然计算机硬件飞速发展,内存和CPU的处理能力已经不是什么问题了,但位操作依然在一些情况(例如:底层系统编程、图像处理、密码加密解密等)中扮演重要的角色。

Go这门编程语言为我们提供了一些位运算符操作,如下:

12 11
用Go统计大文件小点滴

今天,团队里面的小伙伴说手头上有一些几个G以上的文件,需要统计文件里面的数据(文件里面的数据很简单都是Key:Value这样的组织形式)。然后咱俩随便抽了一个5G多的文件,用很普通的方式写了几行代码,流程是 读文件-->扫文件-->识别KV-->统计

然后程序跑了一趟,发现竟然要5分多钟。十分惊讶下,我想,明明很简单的逻辑的代码为什么要跑这么慢。然后我看了一下初版的代码,发现引起性能问题的几个点,分别是:

  • strconv的频繁使用
  • map[string]uint64形式的对象频繁调用
  • 为了识别KV,遍历文件时使用了 split 函数

下面是最开始的代码:

后一页