092016
 

本文介绍 Unix I/O 模型中的4个通用系统调用:open()、read()、write()和close() 的 Go 语言封装。

1、Linux 中 open 系统调用的定义

#include <sys/stat.h>
#include <fcntl.h>

int open(const char* pathname, int flags, … /* mode_t mode */);
                   Returns file descriptor on success, or -1 on error

2、Go 中 open 系统调用的封装

一般的,我们使用 os 标准库的 Open/Create 方法来间接调用 open 系统调用,跟踪代码,找到 Go 中 open 系统调用的封装:

func Open(path string, mode int, perm uint32) (fd int, err error) {
    return openat(_AT_FDCWD, path, mode|O_LARGEFILE, perm)
}

2.1 openat 又是什么呢?

从 2.6.16 (Go 支持的 Linux 版本是 2.6.23)开始,Linux 内核提供了一系列新的系统调用(以at结尾),它们在执行与传统系统调用相似任务的同时,还提供了一些附加功能,对某些程序非常有用,这些系统调用使用目录文件描述符来解释相对路径。

#define _XOPEN_SOURCE 700 /* Or define _POSIX_C_SOURCE >= 200809 */
#include <fcntl.h>

int openat(int dirfd, const char* pathname, int flags, … /* mode_t mode */);
                   Returns file descriptor on success, or -1 on error

可见,openat 系统调用和 open 类似,只是添加了一个 dirfd 参数,其作用如下:
1)如果 pathname 中为一相对路径名,那么对其解释则以打开文件描述符 dirfd 所指向的目录为参照点,而非进程的当前工作目录;
2)如果 pathname 中为一相对路径,且 dirfd 中所含为特殊值 AT_FDCWD(其值为-100),那么对 pathname 的解释则相对于进程当前工作目录,这时 openat 和 open 行为一致;
3)如果 pathname 中为绝对路径,那么将忽略 dirfd 参数;

在 Go 中,只要存在相应的 at 系统调用,都会使用它。

2.2 解读 Go 中的 openat

有上可知,Go 中的 Open 并非执行 open 系统调用,而是 openat 系统调用,行为和 open 一致。

func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) {
    var _p0 *byte
    // 根据要求,path 必须是 C 语言中的字符串,即以 NULL 结尾
    // BytePtrFromString 的作用就是返回一个指向 NULL 结尾的字节数组指针
    _p0, err = BytePtrFromString(path)
    if err != nil {
        return
    }
    // SYS_OPENAT openat 系统调用编号
    r0, _, e1 := Syscall6(SYS_OPENAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags), uintptr(mode), 0, 0)
    // 空操作,用于保证 _p0 存活
    use(unsafe.Pointer(_p0))
    fd = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

2.3 反过来解读 os.OpenFile 函数

OpenFile 函数有一个点需要注意,创建或打开文件时,自动加上了 O_CLOEXEC 标志,也就是执行 Exec 系统调用时该文件描述符不会被继承;

os.OpenFile 返回 os.File 类型的指针,通过 File.Fd() 可以获取到文件描述符;

3、read、write 和 close 系统调用

通过 open 系统调用的分析可以很容易的自己分析 read、write和close 系统调用。

说明一点:close 系统调用,企图关闭一个未打开的文件描述符或两次关闭同一个文件描述符,会返回错误。一般都不需要关心错误。

4、lseek 系统调用

Go 中的 os 包的 File.Seek 对应的系统调用是 lseek

5、其他文件 I/O 相关的系统调用

os 包中,File 类型中 ReadAt/WriteAt 对应的系统调用是 pread/pwrite

Truncate 对应 truncate 等;

几乎所有 Linux 文件相关系统调用,Go 都有封装;另外,通过上面 Open 的介绍,即使没有封装,自己封装也不是难事。

082016
 

一、系统调用概述

系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。Linux 内核以 C 语言语法 API 接口形式(头文件),提供有一系列服务供程序访问。可以通过 man 2 syscall 查看系统调用信息。

关于系统调用,需要注意以下几点:
1、系统调用将处理器从用户态切换到核心态,以便 CPU 访问受到保护的内核内存;
2、系统调用的组成是固定的,每个系统调用都由一个唯一的数字来标识;
3、每个系统调用可辅之以一套参数,对用户控件(进程虚拟地址控件)与内核空间之间(相互)传递的信息加以规范;

以C语言为例,执行系统调用时,幕后会历经诸多步骤。以 x86-32 平台为例,按时间发生顺序对这些步骤加以分析:
1、应用程序通过 C 语言函数库中的外壳(wrapper)函数,来发起系统调用;
2、对系统调用中断处理例程来说,外壳函数必须保证所有的系统调用参数可用。通过堆栈,这些参数传入外壳函数,但内核却希望将这些参数置入特定寄存器。因此,外壳函数会将上述参数复制到寄存器;
3、由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的 CPU 寄存器 (%eax) 中;
4、外壳函数执行一条中断机器指令(int 0×80),引发处理器从用户态切换到核心态,并执行系统中断 0×80(十进制128)的中断矢量所之指向的代码;(2.6内核 和 glibc 2.3.2 以后的版本支持 sysenter 指令,进入内核速度更快);
5、为响应中断 0×80,内核会调用 system_call 例程(内核源码中 arch/i386/entry.S)来处理这次中断;
6、若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量 errno,然后外壳函数会返回到调用程序,并同时返回一个整数值,以表明系统调用是否成功;

二、Go 语言封装的系统调用

Go 语言调用系统调用,并没有使用系统提供的 C 语言函数形式,而是自己封装了系统调用。以 AMD64 为例,Go 语言提供了如下调用系统调用的方式:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

其中,Syscall 和 RawSyscall 的区别如下:(以6结尾的一样)
从源码可以看出,Syscall 开始和结束,分别调用了 runtime 中的进入系统调用和退出系统调用的函数,这就说明,系统调用被 runtime 运行时(调度器)管理,系统调用可以在任何 goroutine 中执行;而 RawSyscall 并没有,因此它可能会阻塞,导致整个程序阻塞。我们应该总是使用 Syscall,RawSyscall 存在的意义是为那些永远不会阻塞的系统调用准备的,比如 Getpid。我们自己的程序需要时,应该用 Syscall。

Go 中 Syscall 的实现,在汇编文件 syscall/asm_linux_amd64.s 中:

// func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

TEXT    ·Syscall(SB),NOSPLIT,$0-56
    CALL    runtime·entersyscall(SB)
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    $0, R10
    MOVQ    $0, R8
    MOVQ    $0, R9
    MOVQ    trap+0(FP), AX    // syscall entry
    SYSCALL     // 进入内核,执行内核处理例程
    // 0xfffffffffffff001 是 linux MAX_ERRNO 取反 转无符号,http://lxr.free-electrons.com/source/include/linux/err.h#L17
    CMPQ    AX, $0xfffffffffffff001        // 发生错误,r1=-1
    JLS    ok
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX             // 错误码,因为错误码是负数,这里转为正数
    MOVQ    AX, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET
ok:
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET

相关参考文献
https://groups.google.com/forum/#!searchin/golang-nuts/Syscall$20RawSyscall/golang-nuts/y9lT_1loJj4/g4ZrYB2_80YJ
《The Linux Programming Interface —A Linux and UNIX System Programming Handbook》