GoLang实现跨平台的一些技巧03

以新建文件为例,对比一下几个常见平台的区别。

继续看下MacOS平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_unix.go

// openFileNolog的unix实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	setSticky := false
	if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
		if _, err := Stat(name); IsNotExist(err) {
			setSticky = true
		}
	}

	var r int
	var s poll.SysFile
	for {
		var e error
		//跳转到open
		r, s, e = open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
		if e == nil {
			break
		}

		// We have to check EINTR here, per issues 11180 and 39237.
		if e == syscall.EINTR {
			continue
		}

		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// open(2) itself won't handle the sticky bit on *BSD and Solaris
	if setSticky {
		setStickyBit(name)
	}

	// There's a race here with fork/exec, which we are
	// content to live with. See ../syscall/exec_unix.go.
	if !supportsCloseOnExec {
		syscall.CloseOnExec(r)
	}

	kind := kindOpenFile
	if unix.HasNonblockFlag(flag) {
		kind = kindNonBlock
	}

	// 封装为File结构
	f := newFile(r, name, kind)
	f.pfd.SysFile = s
	return f, nil
}
// os/file_open_unix.go

func open(path string, flag int, perm uint32) (int, poll.SysFile, error) {
	// 跳转到syscall.Open
	fd, err := syscall.Open(path, flag, perm)
	return fd, poll.SysFile{}, err
}
// syscall/zsyscall_darwin_amd64.go

func Open(path string, mode int, perm uint32) (fd int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil {
		return
	}
	// 调用syscall
	r0, _, e1 := syscall(abi.FuncPCABI0(libc_open_trampoline), uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
// syscall/syscall_darwin.go
func syscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

// internal/abi/funcpc.go
func FuncPCABI0(f interface{}) uintptr

// syscall/zsyscall_darwin_amd64.go
func libc_open_trampoline()
//go:cgo_import_dynamic libc_open open "/usr/lib/libSystem.B.dylib"

// 先通过abi.FuncPCABI0(libc_open_trampoline)先获取到open函数的地址
// 然后通过syscall调用open函数
// open函数是libc标准库中的函数,C语言定义为
int open(const char *pathname, int flags, mode_t mode);
//syscall在这里实现

//runtime/sys_darwin.go
//go:linkname syscall_syscall syscall.syscall
//go:nosplit
func syscall_syscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) {
	args := struct{ fn, a1, a2, a3, r1, r2, err uintptr }{fn, a1, a2, a3, r1, r2, err}
	entersyscall()
	//跳转到libcCall
	libcCall(unsafe.Pointer(abi.FuncPCABI0(syscall)), unsafe.Pointer(&args))
	exitsyscall()
	return args.r1, args.r2, args.err
}
func syscall()

// runtime/sys_libc.go
func libcCall(fn, arg unsafe.Pointer) int32 {
	// Leave caller's PC/SP/G around for traceback.
	gp := getg()
	var mp *m
	if gp != nil {
		mp = gp.m
	}
	if mp != nil && mp.libcallsp == 0 {
		mp.libcallg.set(gp)
		mp.libcallpc = getcallerpc()
		// sp must be the last, because once async cpu profiler finds
		// all three values to be non-zero, it will use them
		mp.libcallsp = getcallersp()
	} else {
		// Make sure we don't reset libcallsp. This makes
		// libcCall reentrant; We remember the g/pc/sp for the
		// first call on an M, until that libcCall instance
		// returns.  Reentrance only matters for signals, as
		// libc never calls back into Go.  The tricky case is
		// where we call libcX from an M and record g/pc/sp.
		// Before that call returns, a signal arrives on the
		// same M and the signal handling code calls another
		// libc function.  We don't want that second libcCall
		// from within the handler to be recorded, and we
		// don't want that call's completion to zero
		// libcallsp.
		// We don't need to set libcall* while we're in a sighandler
		// (even if we're not currently in libc) because we block all
		// signals while we're handling a signal. That includes the
		// profile signal, which is the one that uses the libcall* info.
		mp = nil
	}
	// 跳转到asmcgocall
	res := asmcgocall(fn, arg)
	if mp != nil {
		mp.libcallsp = 0
	}
	return res
}

// 硬件平台相关代码
// runtime/asm_arm64.s
// func asmcgocall(fn, arg unsafe.Pointer) int32
// Call fn(arg) on the scheduler stack,
// aligned appropriately for the gcc ABI.
// See cgocall.go for more details.
TEXT ·asmcgocall(SB),NOSPLIT,$0-20
	MOVD	fn+0(FP), R1
	MOVD	arg+8(FP), R0

	MOVD	RSP, R2		// save original stack pointer
	CBZ	g, nosave
	MOVD	g, R4

	// Figure out if we need to switch to m->g0 stack.
	// We get called to create new OS threads too, and those
	// come in on the m->g0 stack already. Or we might already
	// be on the m->gsignal stack.
	MOVD	g_m(g), R8
	MOVD	m_gsignal(R8), R3
	CMP	R3, g
	BEQ	nosave
	MOVD	m_g0(R8), R3
	CMP	R3, g
	BEQ	nosave

	// Switch to system stack.
	MOVD	R0, R9	// gosave_systemstack_switch<> and save_g might clobber R0
	BL	gosave_systemstack_switch<>(SB)
	MOVD	R3, g
	BL	runtime·save_g(SB)
	MOVD	(g_sched+gobuf_sp)(g), R0
	MOVD	R0, RSP
	MOVD	(g_sched+gobuf_bp)(g), R29
	MOVD	R9, R0

	// Now on a scheduling stack (a pthread-created stack).
	// Save room for two of our pointers /*, plus 32 bytes of callee
	// save area that lives on the caller stack. */
	MOVD	RSP, R13
	SUB	$16, R13
	MOVD	R13, RSP
	MOVD	R4, 0(RSP)	// save old g on stack
	MOVD	(g_stack+stack_hi)(R4), R4
	SUB	R2, R4
	MOVD	R4, 8(RSP)	// save depth in old g stack (can't just save SP, as stack might be copied during a callback)
	BL	(R1)
	MOVD	R0, R9

	// Restore g, stack pointer. R0 is errno, so don't touch it
	MOVD	0(RSP), g
	BL	runtime·save_g(SB)
	MOVD	(g_stack+stack_hi)(g), R5
	MOVD	8(RSP), R6
	SUB	R6, R5
	MOVD	R9, R0
	MOVD	R5, RSP

	MOVW	R0, ret+16(FP)
	RET

nosave:
	// Running on a system stack, perhaps even without a g.
	// Having no g can happen during thread creation or thread teardown
	// (see needm/dropm on Solaris, for example).
	// This code is like the above sequence but without saving/restoring g
	// and without worrying about the stack moving out from under us
	// (because we're on a system stack, not a goroutine stack).
	// The above code could be used directly if already on a system stack,
	// but then the only path through this code would be a rare case on Solaris.
	// Using this code for all "already on system stack" calls exercises it more,
	// which should help keep it correct.
	MOVD	RSP, R13
	SUB	$16, R13
	MOVD	R13, RSP
	MOVD	$0, R4
	MOVD	R4, 0(RSP)	// Where above code stores g, in case someone looks during debugging.
	MOVD	R2, 8(RSP)	// Save original stack pointer.
	BL	(R1)
	// Restore stack pointer.
	MOVD	8(RSP), R2
	MOVD	R2, RSP
	MOVD	R0, ret+16(FP)
	RET

// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(fd int, name string, kind newFileKind) *File {
	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         fd,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name:        name,
		stdoutOrErr: fd == 1 || fd == 2,
	}}

	pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock

	// If the caller passed a non-blocking filedes (kindNonBlock),
	// we assume they know what they are doing so we allow it to be
	// used with kqueue.
	if kind == kindOpenFile {
		switch runtime.GOOS {
		case "darwin", "ios", "dragonfly", "freebsd", "netbsd", "openbsd":
			var st syscall.Stat_t
			err := ignoringEINTR(func() error {
				return syscall.Fstat(fd, &st)
			})
			typ := st.Mode & syscall.S_IFMT
			// Don't try to use kqueue with regular files on *BSDs.
			// On FreeBSD a regular file is always
			// reported as ready for writing.
			// On Dragonfly, NetBSD and OpenBSD the fd is signaled
			// only once as ready (both read and write).
			// Issue 19093.
			// Also don't add directories to the netpoller.
			if err == nil && (typ == syscall.S_IFREG || typ == syscall.S_IFDIR) {
				pollable = false
			}

			// In addition to the behavior described above for regular files,
			// on Darwin, kqueue does not work properly with fifos:
			// closing the last writer does not cause a kqueue event
			// for any readers. See issue #24164.
			if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && typ == syscall.S_IFIFO {
				pollable = false
			}
		}
	}

	clearNonBlock := false
	if pollable {
		if kind == kindNonBlock {
			// The descriptor is already in non-blocking mode.
			// We only set f.nonblock if we put the file into
			// non-blocking mode.
		} else if err := syscall.SetNonblock(fd, true); err == nil {
			f.nonblock = true
			clearNonBlock = true
		} else {
			pollable = false
		}
	}

	// An error here indicates a failure to register
	// with the netpoll system. That can happen for
	// a file descriptor that is not supported by
	// epoll/kqueue; for example, disk files on
	// Linux systems. We assume that any real error
	// will show up in later I/O.
	// We do restore the blocking behavior if it was set by us.
	if pollErr := f.pfd.Init("file", pollable); pollErr != nil && clearNonBlock {
		if err := syscall.SetNonblock(fd, false); err == nil {
			f.nonblock = false
		}
	}

	runtime.SetFinalizer(f.file, (*file).close)
	return f
}

GoLang实现跨平台的一些技巧02

以新建文件为例,对比一下几个常见平台的区别。

继续看下Linux平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_unix.go

// openFileNolog的unix实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	setSticky := false
	if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
		if _, err := Stat(name); IsNotExist(err) {
			setSticky = true
		}
	}

	var r int
	var s poll.SysFile
	for {
		var e error
		//跳转到open
		r, s, e = open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
		if e == nil {
			break
		}

		// We have to check EINTR here, per issues 11180 and 39237.
		if e == syscall.EINTR {
			continue
		}

		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// open(2) itself won't handle the sticky bit on *BSD and Solaris
	if setSticky {
		setStickyBit(name)
	}

	// There's a race here with fork/exec, which we are
	// content to live with. See ../syscall/exec_unix.go.
	if !supportsCloseOnExec {
		syscall.CloseOnExec(r)
	}

	kind := kindOpenFile
	if unix.HasNonblockFlag(flag) {
		kind = kindNonBlock
	}

	// 封装为File结构
	f := newFile(r, name, kind)
	f.pfd.SysFile = s
	return f, nil
}
// os/file_open_unix.go

func open(path string, flag int, perm uint32) (int, poll.SysFile, error) {
	// 跳转到syscall.Open
	fd, err := syscall.Open(path, flag, perm)
	return fd, poll.SysFile{}, err
}
// syscall/syscall_linux.go

func Open(path string, mode int, perm uint32) (fd int, err error) {
	// 跳转到openat
	return openat(AT_FDCWD, path, mode|O_LARGEFILE, perm)
}

//sys	openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)

// syscall/zsyscall_linux_amd64.go

func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil {
		return
	}
	// 跳转到Syscall6
	r0, _, e1 := Syscall6(SYS_OPENAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags), uintptr(mode), 0, 0)
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
// syscall/syscall_linux.go

func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) {
	runtime_entersyscall()
	// 跳转到RawSyscall6
	r1, r2, err = RawSyscall6(trap, a1, a2, a3, a4, a5, a6)
	runtime_exitsyscall()
	return
}

// N.B. RawSyscall6 is provided via linkname by runtime/internal/syscall.
//
// Errno is uintptr and thus compatible with the runtime/internal/syscall
// definition.
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

// syscall/zsysnum_linux_amd64.go
	SYS_OPENAT                 = 257

// RawSyscall6是通过汇编实现的,传入SYS_OPENAT,最终调用openat函数
// openat函数是libc标准库中的函数,C语言定义为
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
// runtime/internal/syscall/asm_linux_amd64.s

// Syscall6 的实现在这里
// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
//
// We need to convert to the syscall ABI.
//
// arg | ABIInternal | Syscall
// ---------------------------
// num | AX          | AX
// a1  | BX          | DI
// a2  | CX          | SI
// a3  | DI          | DX
// a4  | SI          | R10
// a5  | R8          | R8
// a6  | R9          | R9
//
// r1  | AX          | AX
// r2  | BX          | DX
// err | CX          | part of AX
//
// Note that this differs from "standard" ABI convention, which would pass 4th
// arg in CX, not R10.
TEXT ·Syscall6<ABIInternal>(SB),NOSPLIT,$0
	// a6 already in R9.
	// a5 already in R8.
	MOVQ	SI, R10 // a4
	MOVQ	DI, DX  // a3
	MOVQ	CX, SI  // a2
	MOVQ	BX, DI  // a1
	// num already in AX.
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok
	NEGQ	AX
	MOVQ	AX, CX  // errno
	MOVQ	$-1, AX // r1
	MOVQ	$0, BX  // r2
	RET
ok:
	// r1 already in AX.
	MOVQ	DX, BX // r2
	MOVQ	$0, CX // errno
	RET

// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(fd int, name string, kind newFileKind) *File {
	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         fd,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name:        name,
		stdoutOrErr: fd == 1 || fd == 2,
	}}

	pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock

	// If the caller passed a non-blocking filedes (kindNonBlock),
	// we assume they know what they are doing so we allow it to be
	// used with kqueue.
	if kind == kindOpenFile {
		switch runtime.GOOS {
		case "darwin", "ios", "dragonfly", "freebsd", "netbsd", "openbsd":
			var st syscall.Stat_t
			err := ignoringEINTR(func() error {
				return syscall.Fstat(fd, &st)
			})
			typ := st.Mode & syscall.S_IFMT
			// Don't try to use kqueue with regular files on *BSDs.
			// On FreeBSD a regular file is always
			// reported as ready for writing.
			// On Dragonfly, NetBSD and OpenBSD the fd is signaled
			// only once as ready (both read and write).
			// Issue 19093.
			// Also don't add directories to the netpoller.
			if err == nil && (typ == syscall.S_IFREG || typ == syscall.S_IFDIR) {
				pollable = false
			}

			// In addition to the behavior described above for regular files,
			// on Darwin, kqueue does not work properly with fifos:
			// closing the last writer does not cause a kqueue event
			// for any readers. See issue #24164.
			if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && typ == syscall.S_IFIFO {
				pollable = false
			}
		}
	}

	clearNonBlock := false
	if pollable {
		if kind == kindNonBlock {
			// The descriptor is already in non-blocking mode.
			// We only set f.nonblock if we put the file into
			// non-blocking mode.
		} else if err := syscall.SetNonblock(fd, true); err == nil {
			f.nonblock = true
			clearNonBlock = true
		} else {
			pollable = false
		}
	}

	// An error here indicates a failure to register
	// with the netpoll system. That can happen for
	// a file descriptor that is not supported by
	// epoll/kqueue; for example, disk files on
	// Linux systems. We assume that any real error
	// will show up in later I/O.
	// We do restore the blocking behavior if it was set by us.
	if pollErr := f.pfd.Init("file", pollable); pollErr != nil && clearNonBlock {
		if err := syscall.SetNonblock(fd, false); err == nil {
			f.nonblock = false
		}
	}

	runtime.SetFinalizer(f.file, (*file).close)
	return f
}

GoLang实现跨平台的一些技巧01

最近在读GoLang的源码,源码中有一些跨平台的操作,Go处理的很有意思,在这整理一下。

以新建文件为例,对比一下几个常见平台的区别。

首先看下Windows平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_windows.go

// openFileNolog的windows实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	if name == "" {
		return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT}
	}
	path := fixLongPath(name)
	// 跳转到了syscall.Open
	r, e := syscall.Open(path, flag|syscall.O_CLOEXEC, syscallMode(perm))
	if e != nil {
		// We should return EISDIR when we are trying to open a directory with write access.
		if e == syscall.ERROR_ACCESS_DENIED && (flag&O_WRONLY != 0 || flag&O_RDWR != 0) {
			pathp, e1 := syscall.UTF16PtrFromString(path)
			if e1 == nil {
				var fa syscall.Win32FileAttributeData
				e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
				if e1 == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
					e = syscall.EISDIR
				}
			}
		}
		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// 封装为File结构
	f, e := newFile(r, name, "file"), nil
	if e != nil {
		return nil, &PathError{Op: "open", Path: name, Err: e}
	}
	return f, nil
}
// syscall/syscall_windows.go

func Open(path string, mode int, perm uint32) (fd Handle, err error) {
	if len(path) == 0 {
		return InvalidHandle, ERROR_FILE_NOT_FOUND
	}
	pathp, err := UTF16PtrFromString(path)
	if err != nil {
		return InvalidHandle, err
	}
	var access uint32
	switch mode & (O_RDONLY | O_WRONLY | O_RDWR) {
	case O_RDONLY:
		access = GENERIC_READ
	case O_WRONLY:
		access = GENERIC_WRITE
	case O_RDWR:
		access = GENERIC_READ | GENERIC_WRITE
	}
	if mode&O_CREAT != 0 {
		access |= GENERIC_WRITE
	}
	if mode&O_APPEND != 0 {
		access &^= GENERIC_WRITE
		access |= FILE_APPEND_DATA
	}
	sharemode := uint32(FILE_SHARE_READ | FILE_SHARE_WRITE)
	var sa *SecurityAttributes
	if mode&O_CLOEXEC == 0 {
		sa = makeInheritSa()
	}
	var createmode uint32
	switch {
	case mode&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL):
		createmode = CREATE_NEW
	case mode&(O_CREAT|O_TRUNC) == (O_CREAT | O_TRUNC):
		createmode = CREATE_ALWAYS
	case mode&O_CREAT == O_CREAT:
		createmode = OPEN_ALWAYS
	case mode&O_TRUNC == O_TRUNC:
		createmode = TRUNCATE_EXISTING
	default:
		createmode = OPEN_EXISTING
	}
	var attrs uint32 = FILE_ATTRIBUTE_NORMAL
	if perm&S_IWRITE == 0 {
		attrs = FILE_ATTRIBUTE_READONLY
		if createmode == CREATE_ALWAYS {
			// We have been asked to create a read-only file.
			// If the file already exists, the semantics of
			// the Unix open system call is to preserve the
			// existing permissions. If we pass CREATE_ALWAYS
			// and FILE_ATTRIBUTE_READONLY to CreateFile,
			// and the file already exists, CreateFile will
			// change the file permissions.
			// Avoid that to preserve the Unix semantics.
			h, e := CreateFile(pathp, access, sharemode, sa, TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
			switch e {
			case ERROR_FILE_NOT_FOUND, _ERROR_BAD_NETPATH, ERROR_PATH_NOT_FOUND:
				// File does not exist. These are the same
				// errors as Errno.Is checks for ErrNotExist.
				// Carry on to create the file.
			default:
				// Success or some different error.
				return h, e
			}
		}
	}
	if createmode == OPEN_EXISTING && access == GENERIC_READ {
		// Necessary for opening directory handles.
		attrs |= FILE_FLAG_BACKUP_SEMANTICS
	}
	if mode&O_SYNC != 0 {
		const _FILE_FLAG_WRITE_THROUGH = 0x80000000
		attrs |= _FILE_FLAG_WRITE_THROUGH
	}

	// 跳转CreateFile
	return CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
}


func CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile int32) (handle Handle, err error) {
	// 跳转Syscall9
	r0, _, e1 := Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0)
	handle = Handle(r0)
	if handle == InvalidHandle {
		err = errnoErr(e1)
	}
	return
}
// syscall/dll_windows.go
// 封装了Syscall9
func Syscall9(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)

// syscall/zsyscall_windows.go
// Syscall9中传入的API名为procCreateFileW 
procCreateFileW                        = modkernel32.NewProc("CreateFileW")

// 实际上最终调用了windows API CreateFileW,下面是CPP版本的API定义
// 到这里,也可以看到,通过Syscall的定义,比较巧妙的做了一定程度上的解耦
HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);
// runtime/syscall_windows.go

// Syscall9是在这里实现的
//go:linkname syscall_Syscall9 syscall.Syscall9
//go:nosplit
func syscall_Syscall9(fn, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, err uintptr) {
	return syscall_SyscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9)
}

//go:linkname syscall_SyscallN syscall.SyscallN
//go:nosplit
func syscall_SyscallN(trap uintptr, args ...uintptr) (r1, r2, err uintptr) {
	nargs := len(args)

	// asmstdcall expects it can access the first 4 arguments
	// to load them into registers.
	var tmp [4]uintptr
	switch {
	case nargs < 4:
		copy(tmp[:], args)
		args = tmp[:]
	case nargs > maxArgs:
		panic("runtime: SyscallN has too many arguments")
	}

	lockOSThread()
	defer unlockOSThread()
	c := &getg().m.syscall
	c.fn = trap
	c.n = uintptr(nargs)
	c.args = uintptr(noescape(unsafe.Pointer(&args[0])))
	cgocall(asmstdcallAddr, unsafe.Pointer(c))
	return c.r1, c.r2, c.err
}

// 最后,通过cgocall,将go的调用,转换为c的调用
// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(h syscall.Handle, name string, kind string) *File {
	if kind == "file" {
		var m uint32
		if syscall.GetConsoleMode(h, &m) == nil {
			kind = "console"
		}
		if t, err := syscall.GetFileType(h); err == nil && t == syscall.FILE_TYPE_PIPE {
			kind = "pipe"
		}
	}

	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         h,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name: name,
	}}
	runtime.SetFinalizer(f.file, (*file).close)

	// Ignore initialization errors.
	// Assume any problems will show up in later I/O.
	f.pfd.Init(kind, false)

	return f
}

换行符引发的惨案

最近在读go源码。

本来环境都搭建好了,源码也上传git了。
但从另一台电脑下载源码后,报了一堆神奇的错误。
最后发现是go.env文件中,回车换行是按windows系统设定上传到git的,改为linux系统设定就好了。

想起入行以来,因为字符集、换行符、正斜杠反斜杠、tab还是空格,遇到的那堆坑,唏嘘不已。
希望UTF-8早日一统天下,希望各大平台别再特立独行。
非标准化害死人,多套标准更是害死人啊。

跨进程通讯的种种方式

内核模块 内核模块
驱动
内核对象 各类锁【互斥量、信号量、读写锁】
Event【Windows事件对象、Java的wait,可以等待一个或多个】
句柄【进程句柄、对象句柄】
注意只能用于多线程的情况【原子变量、关键区、条件变量是不可以跨进程使用的】
系统层面 系统服务
环境变量
协议调用【URL Scheme】
命令行调用
定时器调用
系统脚本调用
信号类 信号
匿名管道
命名管道
UNIX域
邮件槽
事件类 消息【Windows消息、QT信号槽】
粘贴板
钩子函数
系统内置消息队列
基于内存 共享内存
文件映射
DLL全局数据
DLL注入
基于文件 虚拟文件
实体文件【本地磁盘文件、SAN文件、NAS文件、FTP文件、对象存储文件】
基于网络 Web【WebSocket、SSE、HTTP、REST、SOAP等】
邮件
NetBios函数
数据广播
各类其他Socket通信【TCP、UDP、RAW等】
基于中间件 RPC协议及框架:SAMBA、SOAP、EJB、ICE、Swift、Dubbo等
服务发现:etcd、zk、consol
消息队列【队列、广播、订阅发布】:Kafka、RabbitMQ
数据库:关系【MySQL】、非关系【Redis】
ESB【企业服务总线】
Win系统组件 动态数据交换(DDE)
OLE技术
COM
ALT
DCOM

容器相关

几个问题
1,不同发行版的docker容器,尤其是glibc这些底层库版本不一致的情况下,可以在同一个宿主下运行,是因为glibc这些库与系统内核提供的ABI一直都保持不变吗?万一内核升级,有ABI变动了,docker是如何处理的呢?
处理办法是镜像自己带着基础库,事实上大多数发行版docker镜像都带着glibc,alpine用的则是更轻量的musl。镜像封装的应用,就只需保证兼容自带的glibc/musl即可。

2,现在虚拟机,都支持将某个虚拟机的窗体,直接投射到宿主机上,让宿主机像操作本地应用来操作虚拟机的应用,这个算那一层的虚拟化呢?也是通过ABI模拟来实现的吗?
说的是vmware unity mode这类功能么?具体如何实现的我没有研究过,无责任猜测只是vmware tool这样的工具提供的屏幕映射。但可以肯定的是,并不影响虚拟化层次,即还是硬件抽象层虚拟化。

3,在windows的vmware或virtualbox运行macos的虚拟机超级慢,但运行ubuntu就还蛮快的,是因为mac到win的ABI很难模拟吗?
“超级缓慢”主要是GUI慢,这个与显示方式和驱动都有关。如果你在SSH到虚拟的ubuntu和macos上,跑个console下的跑分软件的话,得到的分数相差并不大。

Windows 系统中是否有文件、访问、资源的隔离手段?是否存在 Windows 版本的容器运行时呢?
Windows的容器还是太重了,镜像超级大,而且优先支持微软自己的技术栈。这么重,加之不成熟,现在对上服务器虚拟化的各类成熟方案,实在没啥优势可言。

WindowsServer从设计上来讲,模块化做的比linux要差一些,内核做的事情也太多,早期类库变化也过于剧烈,类库前后兼容性也不算好。加之之前主推的技术,生命周期都太短,自家技术代与技术代之间没有传承,闭源得不到社区支持,开源服务端软件,在linux上性能,往往吊打windows。

而且即使是windows的虚拟化软件,微软自家做的也不是最好的。vmware和virtualbox多香。

微软唯一让我感到惊艳了一次的,还是wsl,但总归功能差了些,性能差了些。我倒是觉得,把wsl做好,可以同时支持win和linux容器,windows可能更有机会一些。

再后面,就是微软家生态的事情咯。其实微软做的很多理念,都挺超前的,而且也开始与社区合作。但微软,啥都要自己造轮子,技术延续性差又不断要大家换轮子,又没有强援,做生态挺难的。

看下linux和java,一个轮子能用多少年。看一下苹果,也是闭源,也是自己造轮子,还要交保护费,但苹果也不要你三五年就把轮子换了啊,而且保护费不是白交啊。远了远了。。。

docker
我觉得docker和其他虚拟化技术比,仍是最成功的,从docker到containerd,是一种技术上的成功,但是商业上的失败。

k8s应当与swarm对比。k8s成功,源自于google需要这样的一套工具,用来管理自己的容器,开发来先自己用,积累了很多经验。swarm则没有这个过程。云厂商们,自然选择对自己更好用,更有利的工具咯。

google至此,已占有web流量制高点,浏览器制高点,移动操作系统制高点,云原生制高点。厉害

网络虚拟化
N年前,在大学用虚拟机的时候,就遇到了HostOnly,Bridge,NAT等联网模式,当时对Bridge和NAT的区别也是搞了挺久才弄清楚的。

系统对既有旧方案和旧功能的兼容
这种例子蛮多的,比如:
1、系统升级时,如果API版本升级到了2.0,1.0版本也要长期保留。
2、界面风格变化了,还能允许用户切换回之前的风格
3、做系统替换时,一些用户用惯了的工具、报表什么的,经常会被要求在新系统上增加对应功能
4、系统升级时,尽量去兼容原有数据库设计,而不是推翻重来
5、对于部分政企用户,被迫去兼容IE,全是泪啊

存储
存储小文件:用过本地存储、SSD、SAN、NAS、SFTP、Ceph【对象存储】,云存储【云盘、对象存储】
存储大文件:用过SAN、NAS、Ceph【块存储】、HDFS
存储XML用过existdb、Oracle XML DB
存储JSON用过mongodb、es
备份数据,用过磁带和蓝光盘
试过IPFS,但并没有实际投入使用

从底层介质来看:可以分为SSD、磁盘、磁盘阵列、磁带、光盘等
块存储,相当于划分了一块存储空间或一块逻辑盘,给了操作系统
文件存储,相当于操作系统在磁盘上创建了文件系统,可以作为本地磁盘使用,加上网络访问功能,可以封装成为NAS、SFTP等
HDFS,可以看作分布式的文件存储
对象存储,可以看作分布式的NAS
IPFS,可以看作把一个BT网络,封装成了一个NAS

在我们行业里,使用云存储最大的障碍是两个:
1、使用公有云,担心数据安全、用户隐私的问题;少量上云的数据,也因种种限制,只能用移动联通电信的云;
2、使用私有云,很多机构不愿花大价钱购买服务,又没有能力自己运维,最终很难推进;
但总的来讲,这几年,云存储的使用面还是越来越广的,需要有个渐进的过程。

网络透明
服务网格可以实现透明,很大程度上是服务网格的整个网络环境是相对可控的。
远程通讯,如果在网络可控的环境下,其实完全可以和服务网格采用同样的方式。
但在互联网环境下,无法实现网络的可控,运维工程师、网络工程师是无法把程序员的大部分工作都做掉的,也就是程序员不能只关心数据,不关心网络。

何时能实现这种透明呢?个人认为,需要网络设备更新换代才行,要华为、思科支持这种透明,并能将透明能力,一定程度上开放给开发应用的厂商,才有可能实现一定程度上的透明。

SDN
个人感觉,SDN与服务网格感觉从思想上很相似,实现途径却不一样。

SDN本身十分依赖于网络提供商,其实按我理解,需要一次网络设备更新换代才行,而且网络提供商也需要进行一次大升级,用户也要跟着改造,这一方面需要技术沉淀,另一方面需要很多资金投入,一方面需要等用户升级。SDN概念也比较早,在网络提供商向用户推销SDN概念时,多数网络用户第一反映很可能是一脸懵逼。很多用户根本听不明白,有啥用,现在感觉挺好的啊。

而服务网格,是在可控网络下进行的,不需要网络设备的更替,也不需要以来网络供应商,更不存在跨越网络供应商的问题。大厂支持,再加上K8S和Istio的加持,所以生态就起来了,有滋有味的。服务网格用户是技术人员,服务网格出来推广的时候,微服务已大行其道了。技术人员们被服务治理烦的不要不要的,一听有方法直接把自己解放出来,学习应用热情高涨。加上并不需要夸张的资金投入,方案也就更容易落地了。

故障恢复

故障恢复
之前做个一个折中的处理方案,类似于快速接收,批处理,反馈处理状态。之所以这样处理,是因为合作机构IT水平参差不齐,而且配合程度不高,如果把数据接收+处理+反馈放到一起的话,一旦有错误,就要麻烦对方重新推送最新数据,或者自己脚本重新处理数据,而且一天峰值十分明显,峰值时根本来不及处理数据。
1、数据接收阶段,采用了快速失败策略。一旦数据落库文件落盘,就返回成功,反之失败。
2、数据处理阶段,会进行重试,三次后进入失败队列
3、进入失败队列后,会通知运维和开发,去看下数据什么问题。有时会把文件手工处理一下,再重试。如果实在无法处理,就线下通知合作方相关人员如何修改数据。
4、数据处理成功后或彻底失败后,发送处理结果给合作方。

这种方式,在对合作方约束很小、合作方缺乏技术支持、项目前期阶段、以快速开展业务为优先考量时,可以尝试一下。后面自己做起来后,就可以要求对方,做一些统一要求了。

限流和降级
之前在一个大量读写小文件服务的入口处,用过令牌桶限制访问量,防止IO过高。问题是每秒要发放发放多少令牌,最后是慢慢试出来的。

后面微服务时代就是中规中矩的限流,降级和熔断了。但降级和熔断,是通过HTTP状态码进行判断的,有些后知后觉了。

零信任网络
一个团队去完成一个任务,如果彼此信任,相互配合就会很流畅,沟通成本会很低,任务推进也会很顺畅。比如几个知根知底的伙伴去创业,一个眼神可能就懂了。
一群互不信任的人去完成一个任务,哪怕每个人都很努力,但经常感觉别人掣肘,吵来吵去,任务止步不前。一个流动率高,甩锅成风的组织,便是如此。

代码也是如此,如果我们假设代码是可信的,直接拉取镜像就行了。
如果假设代码是不可信的,开发提交代码后,要各种引擎扫描,扫描通过后,流水线打包镜像。同时还要提交各类材料如测试报告,越权测试报告,渗透测试报告,压力测试报告,代码审核报告等等相互佐证代码没有问题,然后发布。
这样先不说要多少资源支持,单说发布,就从十几秒变成了十几分钟,甚至几十分钟。

服务也是如此,如果假设服务是可信的,不要加任何控制,就可以相互访问。
如果假设服务不可信,就要各种验证,网络端口是否允许访问,token是否正确,双向证书过了没,是否有服务访问权限,是否有数据权限,是否符合流量控制要求等等。
先不说做需要多少资源支持,单说服务性能,从几十毫秒一下到了几百毫秒。

那做这些值吗?值!
但要符合自己的情况,不能太过,绑了自己的手脚!

零信任网络安全
我认为边界安全模型和零信任模型会长期共存,边界安全模型毕竟更成熟,而且在资源隔离程度上,远高于零信任模型。istio们并没有提供边界模型的一些组件,比如杀毒,比如入侵检测,比如蜜罐,比如上帝视角的规则控制等。而且istio们本身也有被入侵的可能,所以不能只依赖这一个层面的安全管控,而是立体的安全管控。

可观测性
单体程序时代,类似于一个办公大楼,有了问题,告诉管理员门牌号和具体事情就行了,管理员就可以乘电梯过来解决问题。
只要在日志里输出一下,哪个方法,做了哪个任务或出了什么问题,用日志工具就可以统计到处理速度或快速定位到问题了。

微服务时代,类似于管理城市物流。要提出问题,我们必须说明,那条街,哪个门牌号,几单元,有什么需求。工作人员上门时,要看下地图,什么路线过去最快,遇到堵车怎么办,小区不让进怎么办,然后才能到顾客这边提供服务。数据链路就像城市地图,监控就像地图上的流量,而日志必须还原到这张地图上,才知道哪个交叉口或哪个大楼哪里出了问题。
所以,我们要花必要的精力,去做全链路,绘制这个地图。所以我们要收集度量信息,去监控哪里流量红了,哪里彻底堵车了。只凭日志,是无法快速定位问题的,这个交叉口堵车,问题可能出在三公里之外,一个交叉口一个交叉口查过去,太慢了。

流量大了,堵车是必然的。只有做好可观测性,才能快速疏导交通,做到事半功倍。

日志
个人觉得,如何正确的记录日志,用何规则做日志分级,要记录哪些东西,比用什么技术栈分析日志重要的多。老师能否分享一下,日志规范如何在团队中落地呢?

有两种情况,多记录一些日志有好处的。一类是部署于第三方的系统,宕机时要多记录日志,最好有dump文件,利于排查问题。第二类是跨公司做集成对接,输入输出一般都会记得很清楚,为了防止扯皮。

聚合度量
主要监控了服务请求,JVM,服务器的一些指标。但服务请求方面,做的还很基础,有较大提升空间。

分布式相关

共识机制
Basic Paxos在全部节点可信任的时候,主要还是效率问题。所以zk、etcd都要用改进的算法。
Basic Paxos在部分节点不可信任的时候,是不适用的。所以公共区块链项目需要用其他的共识机制。

公共区块链中,常用的共识方法有PoW、PoS、DPoS等,这些方法,一方面是要达成共识进行记账,另一方面是要防止网络被恶意攻击;
联盟链中,一般采用PBFT、PoET等方法,由于不需要面向整个互联网,所以共识机制效率比公共链高不少;同理RAFT、ZAB,默认所有节点是可信任的,效率也是比较高的。
其实大家在日常生活中,默认会通过中心化的方式来达成共识,比如转账成功与否,大家一般会通过银行或支付宝的记录来判断,而不是线下协商。而这种方式,反而是交易效率最高的。
还是那句话,根据业务场景,选择足够用的架构和算法就好了。适合团队,能高效低成本解决问题的方案,就是最好的方案,哪怕这个技术方十分十分的朴素。

为何有了 DNS,还会出现 Eureka、Consul 这些专有的服务发现框架?
感觉其实要解决的问题并不一样:
1、基于DNS,其实要解决的问题是流量的流向,可以做到流量监控,但无法管理到具体业务,功能简单,效率更高;正好符合K8S的需要;
2、服务发现框架,其实是深入到业务层面,在应用框架集成、业务监听支持、健康检查、服务保护等功能。而且基于业务需求,进一步提供配置管理、环境管理等、多数据中心管理、业务紧密结合的功能;
有点像前面说的四层负载和七层负载的意思。
一个功能简单,效率高花头少;一个功能复杂,效率低可发挥空间更大;

那么除了 BFF 之外,你还用网关来做什么?
在API网关之前,会把移动APP的后端服务单独独立出来,APP访问APP后端服务,APP后端服务访问Web后端服务,相当于做了一个完全定制化的API网关。
当时也尝试过直接用Nginx来完成,但当时也不知道OpenResty这类扩展,复杂些的功能没能实现,没能实际应用。
后面从Zuul开始就不造轮子了。

客户端的负载均衡
客户端的负载均衡,在传统的CS解决方案中也有涉及,比如:
1、直连数据库时,统计类SQL直连只读库就好了
2、而且有些其实也变相做了优化,比如大家都熟悉的游戏开新服,登陆时就把流量分了

客户端负载均衡方案,其实和nginx的均衡策略基本一致的。但还遇到过一种中间情况,就是多了一个调度器,客户端定期获取服务IP列表,也可以从调度器获知每个IP的繁忙程度,然后决定要访问那个IP。

此外,云厂商对于移动、联通、电信的网络,有时也会用不同的链路,并会交一定的“保护费”啦。

负载均衡的设计,最佳人选,应该是懂业务的架构师(默认架构懂开发、懂运维)。
但同时,开发、运维团队要参与及评审:
1、比如开发要与架构沟通,团队熟悉哪些技术栈,哪些服务有状态哪些服务无状态等,中间件如何取舍
2、比如运维要与架构沟通,跨机房数据同步要如何做,容灾备份要如何处理,动态扩展是靠脚本还是靠云平台机制等

认证的各类方式

补充一下认证方式,一般是由三大类方式及其组合实现:

What you are:
比如人的生理特征(指纹、面部识别、虹膜、声纹、DNA等)

What you know:
古代机关开启方式、密码、动态指令、预设安全问题、最近和谁聊天、最近几次变动的密码、最近几次登录地址等

What you have:
身份证,医保卡,银行U盾牌,手机短信/电话语音发送验证码

但有一些就不太会好判断归属:
1、公安局开的身份证明或CA颁发给机构的证书,应该是“你有啥”,但很多时候直接被用于“你是谁”
2、图形动态验证码,用于判断“你是人不是机器”,这个算是“你是谁”的简单版本?

令牌内容
令牌不需要带太多的信息,但要满足一些基本要求:
1、令牌颁发机构标识
2、令牌被授予机构、APP、服务或网站标识
3、令牌类型(资源访问令牌、刷新令牌等)
4、令牌可访问的资源类型(头像、昵称、二维码等)
5、令牌有效时间范围
6、令牌颁布机构签名和时间戳
7、数据组织方式其实用Json 或 编码过的字符串都可以
这样,三方就都可以快速验证令牌有效性。

数据权限
数据权限确实很多时候只能系统自己处理,很难跨行业通用,甚至同一行业不同机构也不尽相同。比如两个看似比较通用的场景:
1、当机构架构分为多级时,上级机构能看到下级哪些数据,平级之间能看到那些数据,下级机构能看到什么数据,各行业各类组织互不相同。如果只用功能权限的思路去解决这个问题,容易将功能权限弄得十分复杂。
2、此外就是用户情况比较复杂,比如除了组织架构、岗位、职级外,还有职业证书相关权限控制、客户业务办理关系、项目组组成、项目组轮班,加上审核、稽核、多级质控等功能,仅是功能权限也是力有不逮。
而且,近些年来,各公司对数据权限和数据安全的要求都越来越高、越来越严格了。

凭证
感觉中文的“凭证”指的内容,比Credentials范围广很多。比如:
有时候,网页提示“请输入登录凭证”,说的就是用户名密码
有时候,请下载云主机“临时凭证”,说的就是登录证书
有时候,请出示入场凭证,说的就是一张狗牌

尤其是证书是否算凭证,感觉有些想不清楚。比如:下面的算凭证吗?
1、域控认证,Windows凭证管理器
2、Linux服务器可以通过ssh证书直接访问
3、一些云厂商可以申请临时ssh证书,允许远程访问
4、IOS程序发布时,需要开发者申请APP证书,并签名才能上传发表
5、接入云SDK时,需要申请AppId 和 SecretKey
6、TCP通讯时,采用TLS双证书双向加密

客户端敏感数据加密
对于客户端敏感数据加密的问题:
1、客户端对敏感信息加密,在通用浏览器、开放网络环境下,其实意义不大。如果一位黑客有能力干掉HTTPS通讯加密或者伪装为服务网站的话,替换掉网站前端代码也不是一件难的事情,这样无论如何加密信息都没有任何用途。同时,在客户端、服务的,这些敏感信息其实都有原文的,如果贡献了这两个地方,黑客可以同时获取明文和密文,所以加密也没啥用。
2、对于安全要求特别高,采用特殊通信协议,特殊信道,特殊设备、特殊浏览器的情况下,对于特别敏感的信息,进行二次加密是有必要的。因为设备和软件都难以得到,即使信道被监听,通信加密被破解,也要尽量拖延敏感信息被破解。

字证书有什么具体应用
1、机构、网站、身份认证
2、APP开发者认证
3、可执行程序开发者认证
4、控件开发者认证
5、API认证
6、文档的电子签章证书
7、大家自己搭建开发环境时有时需要自签名证书
其实还有很多咯。

除了顶级CA之外,其实CA也分等级的,这就涉及到了证书链。
此外,CA还要维护黑名单,吊销一些还在有效期之内的证书。

数据验证
之前只是零零散散有了一些想法,也落地了。但看完今天老师的讲解,感觉落地的很难看。

数据校验这部分,回顾了一下,我们真的做了很多,但十分十分的零散。根据各种要求,包括架构、运维、数据安全、数据合规要求、数据脱敏、敏感数据加密,每年都要折腾个几轮。此类代码横跨了文章中提到的全部层,实现方式从单纯的编码规范,到各层编码,到切面,到流水线扫描,到Jar包,到中间件、到持久化层验证,都有零散涉及。如果能尽量统一到 Bean Validation处理,就不至于每次加个要求,都要一通大改了。