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》

312013
 

最近在研究 Go 的源代码(阅读后的代码注释放在了github上:http://github.com/polaris1119/go_src_comment),由于不少底层代码是用汇编写的,而且是 Plan9 的汇编,因此重新看了一些汇编和 Plan9 汇编的知识。在这里做一个总结、备忘。

一、资源

1、指令查询:

http://68k.hax.com/

2、命令查询

http://plan9.bell-labs.com/magic/man2html/1/8a

3、LEA 和 MOV

LEA:操作地址;
MOV:操作数据

如:
LEAQ 8(SP), SI // argv 把 8(SP)地址放入 SI 寄存器中
MOVQ 0(SP), DI // argc 把0(SP)内容放入 DI 寄存器中

4、对外部数据的引用需要用到 伪寄存器 PC (the virtual program counter) or SB (the ‘static base’ register)。PC counts instructions, not bytes of data,即 2(PC)是跳过一条指令。

5、http://plan9.bell-labs.com/plan9/

http://plan9.bell-labs.com/sys/doc/

plan9 各种文档,html/ps/pdf 格式供下载

6、操作堆栈(stack)的伪指令或指令

FP:Frame Pointer,0(FP) 表示函数的第一个参数;4(FP)表示第二个参数等;
SP:local Stack Pointer,本地栈指针,保存自动变量(局部变量)。0(SP)表示第一个局部变量,4(SP)表示第二个局部变量等;
TOS:Top-Of-Stack register,用来 push 参数到历程(procedure)中或保存临时值等。

Plan9 汇编器有这样的语法,例如,p+0(FP),根据上面的说明,0(FP)表示第一个参数,同时定义了一个p,它的值是 0(FP);对SP有一样的语法。

7、一般的,函数返回值保存在 EAX 寄存器中(Plan9 中叫 AX)

8、函数调用堆栈的说明

函数调用堆栈说明

函数调用堆栈说明

更新日志

1、2013-05-31 开始写

2、2014-06-05 更新【6-8】

042013
 

一、简介

cmd/dist是生成Go distribution的引导工具。它负责构建C程序(像Go编译器)和初始化Go工具的拷贝(即go_bootstrap)。

dist本身是用C语言写的。所有和C库,甚至和C标准库的交互,都通过一个系统相关的文件封装起来,以方便移植,如:plan9.c、unix.c和windows.c等。通过这个可移植层提供功能,供其他文件使用。在这个可移植层中,为了避免和现有函数混淆,相同功能的函数带有前缀:x。例如:xprintf是printf的可移植版本。

到目前为止,dist中大部分普通数据类型是字符串或字符串数组。然而,dist中使用两个命名的数据结构:Buf和Vec,保存它们拥有的所有数据,而不是使用char*和char**。Buf相当于Go中的[]byte,Vec相当于Go中的[]string。对Buf的操作函数以b开头;对Vec的操作函数以v开头。

二、文件概览

a.h arg.h arm.c buf.c build.c buildgc.c buildruntime.c goc2c.c main.c plan9.c README unix.c windows.c

注:go1.0.3源码中没有plan9.c文件,tips中有该文件。

1、README

学习前看README。本文简介部分基本来自该文件。

2、a.h

定义了一些类型,比如简介中提到的Buf和Vec;声明了dist中所有.c文件中的函数

Buf和Vec的结构:

// A Buf is a byte buffer, like Go's []byte.
typedef struct Buf Buf;
struct Buf
{
     char *p;
     int len;
     int cap;
};

// A Vec is a string vector, like Go's []string.
typedef struct Vec Vec;
struct Vec
{
     char **p;
     int len;
     int cap;
};

可以看到,结构很像Go中slice的内部定义

3、arg.h

该文件中声明了一个变量和定义了五个宏,它和libc.h中的定义是一样的。源码中大量使用了这些宏,特别是ARGBEGIN和ARGEND。通过源码,可以知道,ARGBEGIN宏其实就是循环遍历命令行参数(不包括dist和其子命令),参数要求以-开头展开如下(以build.c中的cmdclean为例):

for((argv0?0:(argv0=*argv)),argv++,argc--; argv[0] && argv[0][0]=='-' && argv[0][1]; argc--, argv++) {
        char *_args, *_argt;
        char _argc;
        _args = &argv[0][1];
        if(_args[0]=='-' && _args[1]==0){
            argc--;
            argv++;
            break;
        }
        _argc = 0;
        while((_argc = *_args++) != 0)
        switch(_argc){
            case 'v':
                vflag++;
                break;
            default:
                usage();
         }
        _argt=0;
        USED(_argt);
        USED(_argc);
        USED(_args);
    }
    USED(argv);
    USED(argc);

4、plan9.c/unix.c/windows.c/main.c

感觉上main.c是dist的入口。实际上特定操作系统的文件才包含了main入口函数。main.c中只是一个“伪入口”函数:xmain

如简介中所说,特定操作系统的文件封装了特定操作系统的一些函数,以方便移植。

5、buf.c

提供了对Buf和Vec的操作

6、build.c

初始化对dist的任何调用,即运行dist时需要调用build.c中的函数执行初始化

7、buildgc.c

构建cmd/gc时的辅助文件

8、buildruntime.c

构建pkg/runtime时的辅助文件

9、goc2c.c

将.goc文件转为.c文件。一个.goc文件是一个组合体:包含Go代码和C代码。注意:goc文件和cgo是不一样的。

三、关键源码解读

以Linux操作系统为例,Windows版本基本一样,只是系统调用等不一样,区别就是unix.c和windows.c的不同。

1、执行流程

1)在unix.c中的main入口函数中,首先设置了gohostos和gohostarch,这是本机的操作系统和系统架构。main中会调用build.c中的init和main.c中的xmain

2)build.c中init函数负责全局状态的初始化,如:goroot、goos、goarch等

3)main.c中xmain函数是main的可移植版本,即特定平台的main将入口转发到xmain中

4)根据传递给dist的具体命令执行相应的函数

2、dist的功能(命令)

在main.c中定义了可用的命令:

// cmdtab records the available commands.
static struct {
     char *name;
     void (*f)(int, char**);
} cmdtab[] = {
     {"banner", cmdbanner},
     {"bootstrap", cmdbootstrap},
     {"clean", cmdclean},
     {"env", cmdenv},
     {"install", cmdinstall},
     {"version", cmdversion},
};

这些命令对应的f是在build.c中实现的。

1)banner->cmdbanner

输出Go安装完成的提示。

提示语句类似:

---
Installed Go for linux/amd64 in /home/polaris/go
Installed commands in /home/polaris/go/bin
*** You need to add /home/polaris/go/bin to your PATH.

其中,如果go/bin已经在PATH中,则不会提示最后一句。

2)bootstrap->cmdbootstrap

这是一个重要的命令,通过这个命令生成Go引导工具:go_bootstrap,其实就是go命令,以及其他工具。
该命令首先会执行build.c中的setup函数,初始化Go目录树,为构建Go做准备,初始化工作包括:创建bin目录、pkg目录及其子目录(如obj/tool等),同时做一些必要的清除工作。

接着为本机操作系统和架构构建Go,具体构建顺序在build.c中的buildorder数组中定义了,这个顺序在安装Go的过程中的输出可以看到。实际的构建是通过build.c中的install函数执行的。

install函数比较长,做的事情就是:根据传给它的源码目录(这是按照buildorder中来的),用gcc或其他编译器(如6a/6c/6g)编译源码,生成pkg/obj、pkg/tool和pkg中的C库、命令和Go包。注意,这个时候tool中有一个go_bootstrap,这个其实就是go命令。(可以通过在make.bash中的./cmd/dist/dist bootstrap $buildall -v后面加上exit 0来看到这个中间过程)

总结一下这一步所做的事情:
①没有构建全部的工具(从buildorder中也可以看出),构建了关键的工具go_bootstrap
②构建了C库(在pkg/obj/下)
③构建了编译器等([568]a/c/g/l)
④将runtime包中的goc文件转为c文件
⑤构建一些Go包(标准库),之所以构建这些包,是因为cmd/go命令是用Go语言写的,它依赖这些包。但实际上接下来会将这些构建好的Go包删除,这里实际上产生一些文件(要产生的文件定义在gentab结构中),产生这些文件的目的是使runtime包能够通过go命令统一编译。
go_bootstrap是通过[568]g构建的

关于产生的文件,这里详细解释一下:
在build.c中的deptab结构数组中定义了库和工具的依赖关系,其中pkg/runtime包依赖一些文件:

{"pkg/runtime", {
          "zasm_$GOOS_$GOARCH.h",
          "zgoarch_$GOARCH.go",
          "zgoos_$GOOS.go",
          "zruntime_defs_$GOOS_$GOARCH.go",
          "zversion.go",
     }}

这些文件是通过该过程动态生成的,具体怎么生成,在gentab结构数组中有定义,

static struct {
     char *nameprefix;
     void (*gen)(char*, char*);
} gentab[] = {
     // 以下两个是编译器依赖,在buildgc.c中实现
     {"opnames.h", gcopnames}, // gc编译器依赖,在gcopnames函数中通过cmd/gc/go.h生成
     {"enam.c", mkenam},           // [568]c/g/l依赖,在mkenam函数中通过对应的[568].out.h文件生成
     // 以下是runtime包依赖的,在buildruntime.c中实现
     {"zasm_", mkzasm},     // 汇编文件需要用到
     {"zgoarch_", mkzgoarch}, // 当前主机的架构
     {"zgoos_", mkzgoos},     // 当前主机的操作系统
     {"zruntime_defs_", mkzruntimedefs}, // 数据结构定义(Go版),和runtime.h中一些结构定义类似。
                                         // 从runtimedefs结构数组中的文件提取。
     {"zversion.go", mkzversion}, // 定义GOROOT和GoVersion,分别是goroot_final和goversion
};

可见,安装并不需要配置GOOS、GOARCH之类的环境变量,在unix.c或windows.c中会判断出当前环境

对于交叉编译,第一次准备目标环境时,会install(“pkg/runtime”)

另外,该过程会将runtime.h和cgocall.h拷贝到了pkg/$GOOS_$GOARCH目录下,这是给cgo编译的时候使用的。

关于make.bash中接下来通过go_bootstrap安装其他命令和包的过程,在后续文章中分析。

3)clean->cmdclean

删除临时对象。clean -i会将安装了的对象也删除(即pkg中的.a文件)

4)env->cmdenv

输出默认的环境变量。env -p会输出PATH环境变量。

默认的输出格式是:

GOROOT="/home/polaris/go"
GOBIN="/home/polaris/go/bin"
GOARCH="amd64"
GOOS="linux"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOTOOLDIR="/home/polaris/go/pkg/tool/linux_amd64"
GOCHAR="6"     // 5、6、8啥意思大家懂的

而env -w是:

set GOROOT=/home/polaris/go
set GOBIN=/home/polaris/go/bin
set GOARCH=amd64
set GOOS=linux
set GOHOSTARCH=amd64
set GOHOSTOS=linux
set GOTOOLDIR=/home/polaris/go/pkg/tool/linux_amd64
set GOCHAR=6

在make.bash中,执行eval $(./cmd/dist/dist env -p),就相当于在make.bash中定义了上面的变量,包括PATH。

由此可见,源码安装的时候,并不需要设置GOROOT等环境变量。当然,安装完后必须的环境变量还是需要设置的,要不然会提示go命令找不到,因为这里设置的环境变量只是临时用的。

5)install->cmdinstall

通过build.c中install方法安装某个包,比如:

go tool dist install pkg/runtime

注意这种方式和go install的区别

6)version->cmdversion

输出当前Go版本信息。版本信息是通过build.c中的findgoversion函数得到的。

四、总结

从上面的分析可以看出,在Go中,通过gcc编译出dist后,由dist负责其他编译器等的编译,包括各种环境,这样,Go包中的.s、.c、.g都由自身的编译器[568]s/c/g/、链接器[568]l和打包工具pack 负责编译、链接和打包,完全自给自足。

012013
 

安装Go语言,建议通过源码安装。通过源码安装Go,一般只需要安装一个GCC编译器就可以了。(Windows下是安装MinGW)。该文分析通过源码安装Go语言的过程。

一、分析安装脚本

《探究Go中各个目录的功能》一文中提到了几个脚本,以及它们的作用。现在来分析这些脚本都做了些什么。(以linux下为例,Windows对应的脚本类似)

1、all.bash

set -e
if [ ! -f make.bash ]; then
	echo 'all.bash must be run from $GOROOT/src' 1>&2
	exit 1
fi
OLDPATH="$PATH"
. ./make.bash --no-banner
bash run.bash --no-rebuild
PATH="$OLDPATH"
$GOTOOLDIR/dist banner  # print build info

说明:

1)set -e 当脚本中某个命令返回非零退出状态时,脚本退出
2)if语句 是要求all.bash必须在make.bash所在的目录运行(也就是$GOROOT/src)
3)OLDPATH=”$PATH” 保存当前PATH
4)执行make.bash并传递–no-banner参数
5)执行run.bash并传递–no-rebuild参数
6)执行dist工具并传递 banner参数,打印build信息

2、make.bash

在make.bash中,有一些环境变量,执行make.bash过程中会用到。

①GOROOT_FINAL:最终被设置的Go root,在编译过程中,默认是Go tree的位置。

②GOHOSTARCH:为主机工具(编译器和二进制文件)指定体系结构。这种类型的二进制文件在当前系统必须是可执行的,因此设置这个环境变量的目前唯一常见原因是在AMD64机器上设置GOHOSTARCH=386。(也就是说,在ADM64上可以运行386的可执行文件)

③GO_GCFLAGS:当构建包和命令时可以通过该环境变量附带上5g/6g/8g的参数

④GO_LDFLAGS:当构建包和命令时可以通过该环境变量附带上5l/6l/8l的参数

⑤CGO_ENABLED:在构建过程中控制cgo的使用。当设置为1,在构建时,会包含所有cgo相关的文件,如带有”cgo”编译指令的.c和.go文件。当设置为0,则忽略它们(即禁用CGO)

在文件开头是一些检查:比如是否在Windows下运行了make.bash,ld的版本等。

make.bash中接下来主要的工作(也就是开始构建):

1)构建 C 引导工具 —— cmd/dist

这里首先会export GOROOT环境变量,它的值就是go源码所在路径,可见,源码安装之前并不要求一定要设置GOROOT。

这里学习一个shell脚本知识

GOROOT_FINAL=”${GOROOT_FINAL:-$GOROOT}”
这叫做参数替换,形式如下:
${parameter-default},${parameter:-default}
意思是:如果 parameter 没被 set,那么就使用 default。这两种形式大部分时候是相同的。区别是:当parameter被设置了且为空,则第一种不能输出默认值,而第二种可以。

所以,GOROOT_FINAL=”${GOROOT_FINAL:-$GOROOT}”的意思就是,如果GOROOT_FINAL没设置,则GOROOT_FINAL=$GOROOT

通过gcc编译cmd/dist

2)构建 编译器和Go引导工具

首先通过执行dist设置需要的环境变量
eval $(./cmd/dist/dist env -p)

接着构建Go引导工具:go_bootstrap,以及编译器等

3)构建 包和命令工具

这是通过go_bootstrap做的

3、run.bash

该脚本是一个测试脚本,运行标准库中的测试用例。默认情况下会重新构建go包和工具。由于make.bash会做构建的工作,all.bash中执行run.bash时,传递了–no-rebuild

二、源码安装说明

源码安装Go语言,一般只需要执行./all.bash就可以了。(Windows上执行all.bat)

根据上面的分析,这样会运行测试脚本。如果想更快的安装Go,可以直接运行make.bash(Windows上是make.bat)

整个安装过程大概如下:

# Building C bootstrap tool.
cmd/dist

# Building compilers and Go bootstrap tool for host, linux/amd64.
lib9
libbio
libmach
misc/pprof
……
pkg/text/template/parse
pkg/text/template
pkg/go/doc
pkg/go/build
cmd/go

# Building packages and commands for linux/amd64.
runtime
errors
sync/atomic
sync
io
……
testing
testing/iotest
testing/quick

# Testing packages.
ok cmd/api 0.031s
? cmd/cgo [no test files]
ok cmd/fix 3.558s
ok cmd/go 0.022s
……
? unsafe [no test files]

real 3m6.056s
user 2m29.517s
sys 0m25.134s
# GOMAXPROCS=2 runtime -cpu=1,2,4
ok runtime 6.605s

# sync -cpu=10
ok sync 0.428s

# Testing race detector.
ok flag 1.044s

# ../misc/cgo/stdio

# ../misc/cgo/life

# ../misc/cgo/test
scatter = 0×430490
hello from C
PASS
ok _/home/polaris/go/misc/cgo/test 1.137s

# ../misc/cgo/testso

# ../doc/progs

real 0m19.110s
user 0m15.341s
sys 0m2.904s

# ../doc/articles/wiki
run.bash: 行 92: make: 未找到命令
PASS

# ../doc/codewalk

# ../misc/dashboard/builder ../misc/goplay

# ../test/bench/shootout
fasta
reverse-complement
nbody
binary-tree
binary-tree-freelist
fannkuch
fannkuch-parallel
regex-dna
regex-dna-parallel
spectral-norm
k-nucleotide
k-nucleotide-parallel
mandelbrot
meteor-contest
pidigits
threadring
chameneosredux

# ../test/bench/go1
ok _/home/polaris/go/test/bench/go1 4.942s

# ../test

real 1m38.036s
user 1m14.701s
sys 0m16.645s

# Checking API compatibility.
+pkg crypto/x509, const PEMCipher3DES PEMCipher
+pkg crypto/x509, const PEMCipherAES128 PEMCipher
+pkg crypto/x509, const PEMCipherAES192 PEMCipher
+pkg crypto/x509, const PEMCipherAES256 PEMCipher
+pkg crypto/x509, const PEMCipherDES PEMCipher
+pkg crypto/x509, func EncryptPEMBlock(io.Reader, string, []byte, []byte, PEMCipher)
……
+pkg reflect, func SliceOf(Type) Type
+pkg regexp, method (*Regexp) Split(string, int) []string
~pkg text/template/parse, type DotNode bool
~pkg text/template/parse, type Node interface { Copy, String, Type }

ALL TESTS PASSED


Installed Go for linux/amd64 in /home/polaris/go
Installed commands in /home/polaris/go/bin
*** You need to add /home/polaris/go/bin to your PATH.

三、带中文注释的脚本

以下是我加上了注释的make.bash脚本(关键部分)

echo '# Building C bootstrap tool.'
echo cmd/dist
# export当前Go源码所在跟目录为GOROOT
export GOROOT="$(cd .. && pwd)"
# 如果GOROOT_FINAL没有设置,则使用$GOROOT的值当做GOROOT_FINAL
GOROOT_FINAL="${GOROOT_FINAL:-$GOROOT}"
DEFGOROOT='-DGOROOT_FINAL="'"$GOROOT_FINAL"'"'

# 如果在amd64机子上编译Go本身为32位,可以设置 $GOHOSTARCH=386。不建议这么做
mflag=""
case "$GOHOSTARCH" in
386) mflag=-m32;;
amd64) mflag=-m64;;
esac

# gcc编译:编译cmd/dist下所有的c文件
# -m:指定处理器架构,以便进行优化(-m32、-m64)或为空(一般为空)
# -O:优化选项,一般为:-O2。优化得到的程序比没优化的要小,执行速度可能也有所提高
# -Wall:生成所有警告信息
# -Werror:所有警告信息都变成错误
# -ggdb:为gdb生成调试信息(-g是生成调试信息)
# -o:生成指定的输出文件
# -I:指定额外的文件搜索路径
# -D:相当于C语言中的#define GOROOT_FINAL="$GOROOT_FINAL"
gcc $mflag -O2 -Wall -Werror -ggdb -o cmd/dist/dist -Icmd/dist "$DEFGOROOT" cmd/dist/*.c

# 编译完 dist 工具后,运行dist。目的是设置相关环境变量
# 比如:$GOTOOLDIR 环境变量就是这里设置的
eval $(./cmd/dist/dist env -p)
echo

# 运行make.bash时传递--dist-tool参数可以仅编译dist工具
if [ "$1" = "--dist-tool" ]; then
	# Stop after building dist tool.
	mkdir -p "$GOTOOLDIR"
	if [ "$2" != "" ]; then
		cp cmd/dist/dist "$2"
	fi
	mv cmd/dist/dist "$GOTOOLDIR"/dist
	exit 0
fi

# 构建 编译器和Go引导工具
# $GOHOSTOS/$GOHOSTARCH 是运行dist设置的
echo "# Building compilers and Go bootstrap tool for host, $GOHOSTOS/$GOHOSTARCH."
# 表示重新构建
buildall="-a"
# 传递--no-clean 表示不重新构建
if [ "$1" = "--no-clean" ]; then
	buildall=""
fi
# 构建Go引导工具
./cmd/dist/dist bootstrap $buildall -v # builds go_bootstrap
# Delay move of dist tool to now, because bootstrap may clear tool directory.
mv cmd/dist/dist "$GOTOOLDIR"/dist
"$GOTOOLDIR"/go_bootstrap clean -i std
echo

# $GOHOSTARCH 与 $GOARCH的区别:($GOHOSTOS 与 $GOOS的区别一样)
#	GOARCH 表示Go写出来的程序会在什么架构运行(目标操作系统);
#	GOHOSTARCH 表示运行make.bash这个脚本的系统架构
# 一般它们是相等的,只有在需要交叉编译时才会不一样。
if [ "$GOHOSTARCH" != "$GOARCH" -o "$GOHOSTOS" != "$GOOS" ]; then
	# 即使交叉编译,本机的Go环境还是必须得有
	echo "# Building packages and commands for host, $GOHOSTOS/$GOHOSTARCH."
	GOOS=$GOHOSTOS GOARCH=$GOHOSTARCH \
		"$GOTOOLDIR"/go_bootstrap install -gcflags "$GO_GCFLAGS" -ldflags "$GO_LDFLAGS" -v std
	echo
fi

echo "# Building packages and commands for $GOOS/$GOARCH."
"$GOTOOLDIR"/go_bootstrap install -gcflags "$GO_GCFLAGS" -ldflags "$GO_LDFLAGS" -v std
echo

rm -f "$GOTOOLDIR"/go_bootstrap

# 是否打印安装成功的提示信息。一般为:
#	Installed Go for $GOOS/$GOARCH in $GOROOT
#	Installed commands in $GOROOT/bin
if [ "$1" != "--no-banner" ]; then
	"$GOTOOLDIR"/dist banner
fi

为了方便,将带注释的脚本放在了github上:go_install_bash_comment

注:有些是根据脚本中的英文翻译的,如不明白,请看脚本的注释;如有不对,请指正!谢谢!

说明:这是初稿,很多地方还没有仔细分析。

十二 272012
 

下载Go源码后,根目录结构如下:

|– AUTHORS — 文件,官方 Go语言作者列表
|– CONTRIBUTORS — 文件,第三方贡献者列表
|– LICENSE — 文件,Go语言发布授权协议
|– PATENTS — 文件,专利
|– README — 文件,README文件,大家懂的。提一下,经常有人说:Go官网打不开啊,怎么办?其实,在README中说到了这个。该文件还提到,如果通过二进制安装,需要设置GOROOT环境变量;如果你将Go放在了/usr/local/go中,则可以不设置该环境变量(Windows下是C:\go)。当然,建议不管什么时候都设置GOROOT。另外,确保$GOROOT/bin在PATH目录中。
|– VERSION — 文件,当前Go版本
|– api — 目录,包含所有API列表,方便IDE使用
|– doc — 目录,Go语言的各种文档,官网上有的,这里基本会有,这也就是为什么说可以本地搭建”官网”。这里面有不少其他资源,比如gopher图标之类的。
|– favicon.ico — 文件,官网logo
|– include — 目录,Go 基本工具依赖的库的头文件
|– lib — 目录,文档模板
|– misc — 目录,其他的一些工具,相当于大杂烩,大部分是各种编辑器的Go语言支持,还有cgo的例子等
|– robots.txt — 文件,搜索引擎robots文件
|– src — 目录,Go语言源码:基本工具(编译器等)、标准库
`– test — 目录,包含很多测试程序(并非_test.go方式的单元测试,而是包含main包的测试),包括一些fixbug测试。可以通过这个学到一些特性的使用。

下面详细介绍一些目录(可能分功能介绍)

一、api目录

|– README
|– go1.txt
`– next.txt

通过阅读README知道,go1.txt可以通过go tool api命令生成。而通过go1.txt可以做成编辑器的api自动提示,比如Vim:VimForGo
next.txt是一些将来可能加入的API

二、Go基本工具(cmd)

1、include目录

该目录包含以下文件(文件夹)

ar.h bio.h bootexec.h fmt.h libc.h mach.h plan9 u.h ureg_amd64.h ureg_arm.h ureg_x86.h utf.h

其中,plan9目录是针对Plan 9操作系统的,从里面的文件名知道,跟include跟目录下的是一个意思。

386 libc.h mach.h ureg_amd64.h ureg_arm.h ureg_x86.h (386目录下就只有一个u.h头文件)

1)u.h

根据Rob Pike在How to Use the Plan 9 C Compiler上的介绍以及文件的源码,知道u.h文件定义了一些依赖架构(architecture-dependent)的类型(这样使得该类型独立于架构,不过依赖于编译器),如用于setjmp系统调用的jmp_buf,以及类型int8、uint8等。

该文件直接来源于plan9。地址:http://code.swtch.com/plan9port/src/tip/include/u.h。所有plan9 C程序必须在开始出包含该头文件(因为其他文件引用了该文件中的类型定义)

2)ureg.h

包括:ureg_amd64.h ureg_arm.h ureg_x86.h

三种架构的定义。该文件来源于Inferno操作系统,相应的源码分别是:http://code.google.com/p/inferno-os/source/browse/utils/libmach/ureg5/6/8.h

该文件定义了一个类型(struct):Ureg。定义了在系统栈上寄存器的布局

在ureg_x86.h中对各个字段有注释:

struct Ureg
{
    uint32 di; /* general registers */
    uint32 si; /* ... */
    uint32 bp; /* ... */
    uint32 nsp;
    uint32 bx; /* ... */
    uint32 dx; /* ... */
    uint32 cx; /* ... */
    uint32 ax; /* ... */
    uint32 gs; /* data segments */
    uint32 fs; /* ... */
    uint32 es; /* ... */
    uint32 ds; /* ... */
    uint32 trap; /* trap type */
    uint32 ecode; /* error code (or zero) */
    uint32 pc; /* pc */
    uint32 cs; /* old context */
    uint32 flags; /* old flags */
    union {
        uint32 usp;
        uint32 sp;
    };
    uint32 ss; /* old stack segment */
};

3)libc.h/utf.h/fmt.h

在严格的标准C中,头文件按相关功能分组在一个单独文件中:一个头文件用于字符串处理,一个头文件用于内存管理,一个头文件用于I/O处理,没有头文件是用于系统调用的。plan9采用了不同的方式,一个C库由strings函数、内存操作函数、一些格式化IO程序,加上所有和这些相关的系统调用。为了使用这些功能,需要包含libc.h头文件。该文件从Inferno和Plan9中提取出来的。

Inferno:http://code.google.com/p/inferno-os/source/browse/include/kern.h
Plan 9:http://code.swtch.com/plan9port/src/tip/include/libc.h

该文件开头有几行注释:
/*
* Lib9 is miscellany from the Plan 9 C library that doesn’t
* fit into libutf or into libfmt, but is still missing from traditional
* Unix C libraries.
*/

在该文件中包含了utf.h和fmt.h

在Plan 9中使用nil表示指针的零值,这也就是为什么Go中采用nil了。nil的定义在libc.h中:

#ifndef nil
#define nil ((void*)0)
#endif

对于solaris,nil在u.h中有定义

另外,libc中声明了很多系统调用

包含了该文件之后,可以直接使用print、fprint之类的,而不需要包含标准IO库,这是因为libc.h中包含了fmt.h,而fmt.h中提供了这些print函数。当然,如果需要使用printf,得导入stdio.h

utf.h是实际上引用了src/lib9/utf/utf.h,提供了对UNICODE字符集相关操作。

题外话:

Plan 9支持的每一种CPU架构都给其一个单个字母或数字的名称:k表示SPARC,q表示Motorola Power PC 630和640,v表示MIPS,0表示little-endian MIPS,1表示Motorola 68000,2表示Motorola 68020和68040,5表示Acorn ARM 7500,6表示AMD64,7表示DEC Alpha,8表示Intel 386,9表示AMD 2900。可以看出,Go中5/6/8的由来了。
对于为什么取这样的名字,How to Use the Plan 9 C Compiler 中Heterogeneity有解释。

注意:在看源码过程中可能会看到

ARGBEGIN{
}ARGEND

这是在libc.h中定义的宏。这是一些处理命令行参数的宏。其他宏还有:

ARGF()
EARGF(x)
ARGC()

4)bio.h

上面提到,libc.h中包含了print等,这些IO是没有buffer的。而bio.h提供了buffer I/O,这是推荐使用的方式。这个和ANSI标准I/O,stdio.h类似。

根据官方说法,Bio更小、更高效,特别是buffer-at-a-time或line-at-a-time I/O,即使character-at-a-time I/O也比stdio更快。

和其他系统明显不同的是,Plan 9中I/O的接口的文本不是ASCII编码,而是UTF(ISO叫做UTF-8)编码。一个字符在Plan 9中称为rune,也叫做Code-point。(Go中沿用了该叫法)

看一下utf.h中的一个枚举类型

{
    UTFmax = 4, /* maximum bytes per rune */
    Runesync = 0x80, /* cannot represent part of a UTF sequence ( Runeself = 0x80, /* rune and UTF sequences are the same ( Runeerror = 0xFFFD, /* decoding error in UTF */
    Runemax = 0x10FFFF, /* maximum rune value */
};

引用一段解释:

The library defines several symbols relevant to the representation of characters. Any byte with unsigned value less than Runesync will not appear in any multi-byte encoding of a character. Utfrune compares the character being searched against Runesync to see if it is sufficient to call strchr or if the byte stream must be interpreted. Any byte with unsigned value less than Runeself is represented by a single byte with the same value. Finally, when errors are encountered converting to runes from a byte stream, the library returns the rune value Runeerror and advances a single byte. This permits programs to find runes embedded in binary data.

关于UTF8的操作在utf.h文件中声明了

该文件来源于Inferno操作系统
http://code.google.com/p/inferno-os/source/browse/include/bio.h

5)ar.h

该文件来源于Inferno操作系统
http://code.google.com/p/inferno-os/source/browse/utils/include/ar.h

iar是一个压缩命令,该压缩的文件格式通过ar.h头文件描述。

关于该文件的具体说明,可以查看: http://www.vitanuova.com/inferno/man/10/ar.html

6)bootexec.h/mach.h

bootexec.h 文件来源于Inferno操作系统
http://code.google.com/p/inferno-os/source/browse/utils/libmach/bootexec.h
但注释掉了一些东西

该文件定义了一些架构私有的引导执行程序的文件头格式(引导程序)。这是Plan 9(Inferno的祖先)操作系统的说明:The header format of a bootable executable is defined by each manufacturer. Header file /sys/include/bootexec.h contains structures describing the headers currently supported.

mach.h文件来源于Inferno操作系统:
http://code.google.com/p/inferno-os/source/browse/utils/libmach/a.out.h
http://code.google.com/p/inferno-os/source/browse/utils/libmach/mach.h

该文件定义了一些特定架构的应用程序数据。目前支持的架构:

/*
 * Supported architectures:
 * mips,
 * 68020,
 * i386,
 * amd64,
 * sparc,
 * sparc64,
 * mips2 (R4000)
 * arm
 * powerpc,
 * powerpc64
 * alpha
 */

该文件中列出了详细的支持的架构类型(可执行文件)

bootexec.h是针对引导程序的;mach.h是针对应用程序的。

2、src下的lib9/libbio/libmach

由include目录中文件的名字知道,这三个目录分别是libc.h、bio.h和mach.h三个头文件的实现。具体代码有兴趣可以看看。

这些都是Plan 9或Inferno操作系统的库

3、src/cmd 包含了各种工具的源码

目录如下:

5a 5c 5g 5l 6a 6c 6g 6l 8a 8c 8g 8l addr2line api cc cgo dist fix gc go godoc gofmt ld nm objdump pack vet yacc

一个目录对应一个工具
除了go/godoc/gofmt/dist,其他工具的使用方式:
go tool 工具名 xxx

注:这些工具基本来自Plan 9上已有的工具。工具帮助文档:http://plan9.bell-labs.com/sys/man/1/INDEX.html

经过前面的介绍,看到这些名字,应该大概猜到是啥了。

我们看一下Plan 9中文件后缀的问题

前面我们知道,AMD64上,标示是6,我们以这个为例。

根据Plan 9命名规则,AMD64上的C编译器是6c,汇编器是6a,链接器或装载器是6l。c文件编译后生成的对象文件后缀是.6,链接后默认的可执行文件名是6.out。

5/6/8这一序列中,跟Plan 9是一致的,另外,新增了一个g,表示Go编译器。通过这些工具编译Go文件生成的中间文件对象的后缀和C文件编译后是一样的,以.5/6/8结尾。

说到这里提醒一下,目前Go编译不建议直接通过5g/6g/8g这样的进行,而是使用go这个工具(网上很多Go1正式版发布之前的文章用的是6g这样的工具)

5/6/8这一序列中,每个目录下的都有一个doc.go文件,这个文件大概说明了该工具的作用。这一序列工具具体的源码,感兴趣的可以阅读。

1)cc/gc/ld分别是C编译器、Go编译器和链接器

这三个可以看成是对5/6/8序列的抽象(不依赖具体架构)

2)api 可以生产所有Go包的API列表。

GOROOT/api中的go1.txt就可以通过这个工具生产

3)cgo 允许通过调用C代码创建Go包

4)fix 找到用旧API写的Go程序,然后用新API重写他们。

这个可用于Go升级了,处理用之前版本Go写的应用程序。

5)go 管理Go源代码的工具,很好用很重要的一个工具。

应该总是使用go这个工具,而不是使用6g这样的工具。当然,如果需要生产中间对象,可以使用6g这样的工具。

6)godoc 提取并生产Go程序文档(包括Go本身)

7)gofmt 格式化Go程序代码

8)nm 是Plan 9中的nm工具

。详细说明:http://plan9.bell-labs.com/magic/man2html/1/nm 。查看符号表用的

9)pack 是Plan 9中的ar工具,这个用来归档目标文件。

pkg中的.a文件就是pack生成的。详细说明:http://plan9.bell-labs.com/magic/man2html/1/ar

10)vet 用于检查并报告Go源码中可疑的结构。

比如调用Printf,它的参数和格式化字符串提供的不一致,如:fmt.Printf(“%s is %s”, name),这样会被检查出来。

11)yacc Go版本的yacc。

http://plan9.bell-labs.com/magic/man2html/1/yacc。这是一个经典的生成语法分析器的工具。更多详细说明,可以查阅相关资料。Yacc 与 Lex 快速入门

以上工具目录中都有doc.go文件,用于生成文档。http://golang.org/cmd/可以查看。

12)addr2line linux下有这个命令。

这是一个addr2line的模拟器,只是为了使pprof能够在mac上工作。关于addr2line,可以查看linux的man手册,也可以看addr2line探秘

13)objdump linux下有这个命令。

这是一个objdump的模拟器,只是为了使pprof能够在mac上工作。关于objdump,可以查看linux的man手册

14)dist 这是一个重要的工具。它是一个引导程序,负责构建Go其他基本工具。通过源码安装Go时,会先安装该工具。

注:安装完之后,pkg/tool/$GOOS_$GOARCH下面的pprof工具是从misc下面copy过来的

四、安装脚本

通过源码安装Go相当简单(安装速度也很快),因为它提供了方便的脚本。脚本在src目录下

all.bash/all.bat — 会执行make脚本和run脚本
make.bash/make.bat — 安装Go
run.bash/run.bat — 测试标准库

所以,通过源码安装Go,一般cd到src目录执行./all.bash。如果不想测试标准库,可以直接./make.bash,这样会比较快。

Make.dist 被其他Makefile文件引用,比如cmd下面的很多工具中的Makefile文件。这个文件的作用是:运行go tool dist去安装命令,同时在安装过程中会打印出执行了该文件的目录名。可见,在源码安装Go的过程中,打印出的大部分信息就是这个文件的作用。

五、src/pkg Go标准库源码