Golang程序启动详解:从入门到精通

发表时间: 2024-07-27 22:45

现在自己用的最多的编程语言是golang,golang是编译型语言,需要先把程序编译为二进制,然后启动执行。之前想当然认为golang编译的二进制有它特殊的地方,需要特殊流程才可以启动。实际各个二进制可执行文件是平等的个体,它们遵循相同的启动流程

下面介绍linux系统上,二进制可执行文件如何启动执行。

exec系统调用

使用strace可显示二进制可执行文件启动时的系统调用。下面执行trace sleep1000s观察sleep二进制文件如何执行的。

  ~ strace sleep 1000s  execve("/usr/bin/sleep", ["sleep", "1000s"], 0x7ffc9b87ff18 /* 68 vars */) = 0brk(NULL)                               = 0x55b94789c000arch_prctl(0x3001 /* ARCH_??? */, 0x7fffcfee0760) = -1 EINVAL (Invalid argument)access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=120038, ...}) = 0mmap(NULL, 120038, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f403c9d4000close(3)                                = 0openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3。。。省略部分无关trace。。。

由上面示例可知是execve系统调用启动了sleep,execve系统调用有何作用:把当前执行程序的堆栈及数据替换为新的程序,并执行新的程序,但是程序号保持不变。把当前执行程序替换为新的程序并执行如何理解?我们能否观察到此过程?

我们可以追踪一个终端进程如何执行sleep 1000s子程序,来理解execve系统调用如何把当前执行程序替换为新的程序。在下面trace日志中,我们追踪进程301345,它是一个终端的进程 id,在此终端运行sleep 1000s,strace会记录终端clone出子进程302078,然后execve系统调用把子进程执行程序替换为sleep。

///在终端(进程id 301345) 执行 sleep 1000s➜  ~ sleep 1000s
# strace -f -e trace=all -p 301345。。。省略部分无关trace。。301345 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f358399d710) = 302078301345 close(4)                         = 0301345 read(3,  <unfinished ...>302078 close(3)                         = 0302078 getpid()                         = 302078302078 setpgid(0, 302078)               = 0302078 ioctl(10, TIOCSPGRP, [302078])   = 0 。。。省略部分无关trace。。302078 execve("/usr/bin/sleep", ["sleep", "1000s"], 0x557bdd6785c0 /* 67 vars */) = 0 302078 brk(NULL)                        = 0x5592d0454000302078 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc8bfbe550) = -1 EINVAL (Invalid argument)302078 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)302078 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3。。。省略部分无关trace。。

另外,可以使用pstree 查看进程树,进一步理解sleep 的启动流程。终端zsh(301345) clone出子进程302078,然后系统调用execve把子进程执行程序替换为sleep,且进程id不变。

➜  ~ pstree -lphs 302078systemd(1)───systemd(300022)───gnome-shell(300318)───terminator(301334)───zsh(301345)───sleep(302078)

C语言启动二进制可执行文件

可以写一个C语言程序,创建一个子进程,然后系统调用execve把子进程执行程序替换为sleep,如下所示bin.c 文件内容如下。

#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main() {    pid_t pid = fork(); // 创建子进程    if (pid == -1) {        // fork失败        perror("fork failed");        exit(EXIT_FAILURE);    } else if (pid == 0) {        // 子进程        execl("/user/bin/sleep",  "sleep", "1000s", NULL); // 替换子进程执行程序为'sleep 1000s'命令        perror("execl failed");        exit(EXIT_FAILURE);    } else {        // 父进程        int status;        waitpid(pid, &status, 0); // 等待子进程结束        if (WIFEXITED(status)) {            printf("子进程退出,返回值为: %d\n", WEXITSTATUS(status));        }    }    return 0;}

编译此C语言文件,并通过strace追踪它执行时的系统调用。可见,fork 创建子进程会使用clone 系统调用, execl改变子进程执行程序时,会使用execve系统调用,最终使子进程运行sleep。感觉我们可以用C语言开发一个终端程序。

  ~ gcc bin.c -o bin  ~ strace -f ./bin。。。省略部分无关trace。。306145 munmap(0x7f7d058ab000, 120038)   = 0306145 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7d058aa810) = 306146306146 execve("/usr/bin/sleep", ["sleep", "1000s"], 0x7ffebc541708 /* 67 vars */ <unfinished ...>306145 wait4(306146,  <unfinished ...>。。。省略部分无关trace。。  ~ pstree -lps 306146      systemd(1)───systemd(300022)───gnome-shell(300318)───terminator(301334)───zsh(302463)───strace(306141)───bin(306145)───sleep(306146)              

golang二进制可执行文件启动

golang语言编译的二进制可执行文件,启动流程和上面的示例启动流程一致,因为各个二进制可执行文件是平等的个体,在程序世界它们遵循相同的启动流程。

我们来写一个简单golang hello world程序验证下,hello.go如下所示

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

编译hello.go为二进制可执行文件。

➜  ~ go mod init example.com/hellogo: creating new go.mod: module example.com/hellogo: to add module requirements and sums:	go mod tidy➜  ~ go build➜  ~ ./hello

在终端执行它,使用strace追踪此二进制文件如何启动,也是先clone出子进程317645,然后系统调用execve把子进程执行程序替换为hello

/// 使用strace追踪 终端如何启动golang 二进制文件  ~ strace -f -e trace=all -p 313682。。。省略部分无关trace。。313682 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD <unfinished ...>317645 close(3)                         = 0313682 <... clone resumed>, child_tidptr=0x7f7ff76da710) = 317645317645 getpid( <unfinished ...>313682 close(4 <unfinished ...>317645 <... getpid resumed>)            = 317645317645 setpgid(0, 317645 <unfinished ...>。。。省略部分无关trace。。317645 execve("./hello", ["./hello"], 0x55705c7de5c0 /* 67 vars */) = 0317645 arch_prctl(ARCH_SET_FS, 0x52cb50) = 0。。。省略部分无关trace。。

本文由思考golang编译的二进制可执行文件如何启动,学习了linux系统下,二进制可执行文件的启动流程。经过调研知道,在程序世界,各个二进制可执行文件遵循相同的启动流程,它们是平等个体。