创建进程
os 包及其子包 os/exec 提供了创建进程的方法。
一般的,应该优先使用 os/exec 包。因为 os/exec 包依赖 os 包中关键创建进程的 API,为了便于理解,我们先探讨 os 包中和进程相关的部分。
进程的创建
在 Unix 中,创建一个进程,通过系统调用 fork 实现(及其一些变种,如 vfork、clone)。在 Go 语言中,Linux 下创建进程使用的系统调用是 clone。
很多时候,系统调用 fork、execve、wait 和 exit 会在一起出现。此处先简要介绍这 4 个系统调用及其典型用法。
- fork:允许一进程(父进程)创建一新进程(子进程)。具体做法是,新的子进程几近于对父进程的翻版:子进程获得父进程的栈、数据段、堆和执行文本段的拷贝。可将此视为把父进程一分为二。
- exit(status):终止一进程,将进程占用的所有资源(内存、文件描述符等)归还内核,交其进行再次分配。参数 status 为一整型变量,表示进程的退出状态。父进程可使用系统调用 wait() 来获取该状态。
- wait(&status) 目的有二:其一,如果子进程尚未调用 exit() 终止,那么 wait 会挂起父进程直至子进程终止;其二,子进程的终止状态通过 wait 的 status 参数返回。
- execve(pathname, argv, envp) 加载一个新程序(路径名为 pathname,参数列表为 argv,环境变量列表为 envp)到当前进程的内存。这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。
在 Go 语言中,没有直接提供 fork 系统调用的封装,而是将 fork 和 execve 合二为一,提供了 syscall.ForkExec。如果想只调用 fork,得自己通过 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0) 实现。
Process 及其相关方法
os.Process 存储了通过 StartProcess 创建的进程的相关信息。
type Process struct {
Pid int
handle uintptr // handle is accessed atomically on Windows
isdone uint32 // process has been successfully waited on, non zero if true
}
一般通过 StartProcess 创建 Process 的实例,函数声明如下:
func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)
它使用提供的程序名、命令行参数、属性开始一个新进程。StartProcess 是一个低级别的接口。os/exec 包提供了高级别的接口,一般应该尽量使用 os/exec 包。如果出错,错误的类型会是 *PathError。
其中的参数 attr,类型是 ProcAttr 的指针,用于为 StartProcess 创建新进程提供一些属性。定义如下:
type ProcAttr struct {
// 如果 Dir 非空,子进程会在创建 Process 实例前先进入该目录。(即设为子进程的当前工作目录)
Dir string
// 如果 Env 非空,它会作为新进程的环境变量。必须采用 Environ 返回值的格式。
// 如果 Env 为 nil,将使用 Environ 函数的返回值。
Env []string
// Files 指定被新进程继承的打开文件对象。
// 前三个绑定为标准输入、标准输出、标准错误输出。
// 依赖底层操作系统的实现可能会支持额外的文件对象。
// nil 相当于在进程开始时关闭的文件对象。
Files []*File
// 操作系统特定的创建属性。
// 注意设置本字段意味着你的程序可能会执行异常甚至在某些操作系统中无法通过编译。这时候可以通过为特定系统设置。
// 看 syscall.SysProcAttr 的定义,可以知道用于控制进程的相关属性。
Sys *syscall.SysProcAttr
}
FindProcess 可以通过 pid 查找一个运行中的进程。该函数返回的 Process 对象可以用于获取关于底层操作系统进程的信息。在 Unix 系统中,此函数总是成功,即使 pid 对应的进程不存在。
func FindProcess(pid int) (*Process, error)
Process 提供了四个方法:Kill、Signal、Wait 和 Release。其中 Kill 和 Signal 跟信号相关,而 Kill 实际上就是调用 Signal,发送了 SIGKILL 信号,强制进程退出,关于信号,后续章节会专门讲解。
Release 方法用于释放 Process 对象相关的资源,以便将来可以被再使用。该方法只有在确定没有调用 Wait 时才需要调用。Unix 中,该方法的内部实现只是将 Process 的 pid 置为 -1。
我们重点看看 Wait 方法。
func (p *Process) Wait() (*ProcessState, error)
在多进程应用程序的设计中,父进程需要知道某个子进程何时改变了状态 —— 子进程终止或因收到信号而停止。Wait 方法就是一种用于监控子进程的技术。
Wait 方法阻塞直到进程退出,然后返回一个 ProcessState 描述进程的状态和可能的错误。Wait 方法会释放绑定到 Process 的所有资源。在大多数操作系统中,Process 必须是当前进程的子进程,否则会返回错误。
看看 ProcessState 的内部结构:
type ProcessState struct {
pid int // The process's id.
status syscall.WaitStatus // System-dependent status info.
rusage *syscall.Rusage
}
ProcessState 保存了 Wait 函数报告的某个进程的信息。status 记录了状态原因,通过 syscal.WaitStatus 类型定义的方法可以判断:
- Exited():是否正常退出,如调用 os.Exit;
- Signaled():是否收到未处理信号而终止;
- CoreDump():是否收到未处理信号而终止,同时生成 coredump 文件,如 SIGABRT;
- Stopped():是否因信号而停止(SIGSTOP);
- Continued():是否因收到信号 SIGCONT 而恢复;
syscal.WaitStatus 还提供了其他一些方法,比如获取退出状态、信号、停止信号和中断(Trap)原因。
因为 Linux 下 Wait 的内部实现使用的是 wait4 系统调用,因此,ProcessState 中包含了 rusage,用于统计进程的各类资源信息。一般情况下,syscall.Rusage 中定义的信息都用不到,如果实际中需要使用,可以查阅 Linux 系统调用 getrusage 获得相关说明 (getrusage(2))。
ProcessState 结构内部字段是私有的,我们可以通过它提供的方法来获得一些基本信息,比如:进程是否退出、Pid、进程是否是正常退出、进程 CPU 时间、用户时间等等。
实现类似 Linux 中 time 命令的功能:
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Printf("Usage: %s [command]\n", os.Args[0])
os.Exit(1)
}
cmdName := os.Args[1]
if filepath.Base(os.Args[1]) == os.Args[1] {
if lp, err := exec.LookPath(os.Args[1]); err != nil {
fmt.Println("look path error:", err)
os.Exit(1)
} else {
cmdName = lp
}
}
procAttr := &os.ProcAttr{
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
}
cwd, err := os.Getwd()
if err != nil {
fmt.Println("look path error:", err)
os.Exit(1)
}
start := time.Now()
process, err := os.StartProcess(cmdName, []string{cwd}, procAttr)
if err != nil {
fmt.Println("start process error:", err)
os.Exit(2)
}
processState, err := process.Wait()
if err != nil {
fmt.Println("wait error:", err)
os.Exit(3)
}
fmt.Println()
fmt.Println("real", time.Now().Sub(start))
fmt.Println("user", processState.UserTime())
fmt.Println("system", processState.SystemTime())
}
// go build main.go && ./main ls
// Output:
//
// real 4.994739ms
// user 1.177ms
// system 2.279ms
运行外部命令
通过 os 包可以做到运行外部命令,如前面的例子。不过,Go 标准库为我们封装了更好用的包: os/exec,运行外部命令,应该优先使用它,它包装了 os.StartProcess 函数以便更容易的重定向标准输入和输出,使用管道连接 I/O,以及作其它的一些调整。
查找可执行程序
exec.LookPath 函数在 PATH 指定目录中搜索可执行程序,如 file 中有 /,则只在当前目录搜索。该函数返回完整路径或相对于当前路径的一个相对路径。
func LookPath(file string) (string, error)
如果在 PATH 中没有找到可执行文件,则返回 exec.ErrNotFound。
Cmd 及其相关方法
Cmd 结构代表一个正在准备或者在执行中的外部命令,调用了 Run、Output 或 CombinedOutput 后,Cmd 实例不能被重用。
type Cmd struct {
// Path 是将要执行的命令路径。
// 该字段不能为空(也是唯一一个不能为空的字段),如为相对路径会相对于 Dir 字段。
// 通过 Command 初始化时,会在需要时调用 LookPath 获得完整的路径。
Path string
// Args 存放着命令的参数,第一个值是要执行的命令(Args[0]);如果为空切片或者 nil,使用 {Path} 运行。
// 一般情况下,Path 和 Args 都应被 Command 函数设定。
Args []string
// Env 指定进程的环境变量,如为 nil,则使用当前进程的环境变量,即 os.Environ(),一般就是当前系统的环境变量。
Env []string
// Dir 指定命令的工作目录。如为空字符串,会在调用者的进程当前工作目录下执行。
Dir string
// Stdin 指定进程的标准输入,如为 nil,进程会从空设备读取(os.DevNull)
// 如果 Stdin 是 *os.File 的实例,进程的标准输入会直接指向这个文件
// 否则,会在一个单独的 goroutine 中从 Stdin 中读数据,然后将数据通过管道传递到该命令中(也就是从 Stdin 读到数据后,写入管道,该命令可以从管道读到这个数据)。在 goroutine 停止数据拷贝之前(停止的原因如遇到 EOF 或其他错误,或管道的 write 端错误),Wait 方法会一直堵塞。
Stdin io.Reader
// Stdout 和 Stderr 指定进程的标准输出和标准错误输出。
// 如果任一个为 nil,Run 方法会将对应的文件描述符关联到空设备(os.DevNull)
// 如果两个字段相同,同一时间最多有一个线程可以写入。
Stdout io.Writer
Stderr io.Writer
// ExtraFiles 指定额外被新进程继承的已打开文件,不包括标准输入、标准输出、标准错误输出。
// 如果本字段非 nil,其中的元素 i 会变成文件描述符 3+i。
//
// BUG: 在 OS X 10.6 系统中,子进程可能会继承不期望的文件描述符。
// http://golang.org/issue/2603
ExtraFiles []*os.File
// SysProcAttr 提供可选的、各操作系统特定的 sys 属性。
// Run 方法会将它作为 os.ProcAttr 的 Sys 字段传递给 os.StartProcess 函数。
SysProcAttr *syscall.SysProcAttr
// Process 是底层的,只执行一次的进程。
Process *os.Process
// ProcessState 包含一个已经存在的进程的信息,只有在调用 Wait 或 Run 后才可用。
ProcessState *os.ProcessState
}
Command
一般的,应该通过 exec.Command 函数产生 Cmd 实例:
func Command(name string, arg ...string) *Cmd
该函数返回一个 *Cmd,用于使用给出的参数执行 name 指定的程序。返回的 *Cmd 只设定了 Path 和 Args 两个字段。
如果 name 不含路径分隔符,将使用 LookPath 获取完整路径;否则直接使用 name。参数 arg 不应包含命令名。
得到 *Cmd 实例后,接下来一般有两种写法:
- 调用 Start(),接着调用 Wait(),然后会阻塞直到命令执行完成;
- 调用 Run(),它内部会先调用 Start(),接着调用 Wait();
Start
func (c *Cmd) Start() error
开始执行 c 包含的命令,但并不会等待该命令完成即返回。Wait 方法会返回命令的退出状态码并在命令执行完后释放相关的资源。内部调用 os.StartProcess,执行 forkExec。
Wait
func (c *Cmd) Wait() error
Wait 会阻塞直到该命令执行完成,该命令必须是先通过 Start 执行。
如果命令成功执行,stdin、stdout、stderr 数据传递没有问题,并且返回状态码为 0,方法的返回值为 nil;如果命令没有执行或者执行失败,会返回 *ExitError 类型的错误;否则返回的 error 可能是表示 I/O 问题。
如果 c.Stdin 不是 *os.File 类型,Wait 会等待,直到数据从 c.Stdin 拷贝到进程的标准输入。
Wait 方法会在命令返回后释放相关的资源。
Output
除了 Run() 是 Start+Wait 的简便写法,Output() 更是 Run() 的简便写法,外加获取外部命令的输出。
func (c *Cmd) Output() ([]byte, error)
它要求 c.Stdout 必须是 nil,内部会将 bytes.Buffer 赋值给 c.Stdout,在 Run() 成功返回后,会将 Buffer 的结果返回(stdout.Bytes())。
CombinedOutput
Output() 只返回 Stdout 的结果,而 CombinedOutput 组合 Stdout 和 Stderr 的输出,即 Stdout 和 Stderr 都赋值为同一个 bytes.Buffer。
StdoutPipe、StderrPipe 和 StdinPipe
除了上面介绍的 Output 和 CombinedOutput 直接获取命令输出结果外,还可以通过 StdoutPipe 返回 io.ReadCloser 来获取输出;相应的 StderrPipe 得到错误信息;而 StdinPipe 则可以往命令写入数据。
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
StdoutPipe 方法返回一个在命令 Start 执行后与命令标准输出关联的管道。Wait 方法会在命令结束后会关闭这个管道,所以一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 Wait 出错了,则必须手动关闭。
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
StderrPipe 方法返回一个在命令 Start 执行后与命令标准错误输出关联的管道。Wait 方法会在命令结束后会关闭这个管道,一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 Wait 出错了,则必须手动关闭。
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
StdinPipe 方法返回一个在命令 Start 执行后与命令标准输入关联的管道。Wait 方法会在命令结束后会关闭这个管道。必要时调用者可以调用 Close 方法来强行关闭管道。例如,标准输入已经关闭了,命令执行才完成,这时调用者需要显示关闭管道。
因为 Wait 之后,会将管道关闭,所以,要使用这些方法,只能使用 Start+Wait 组合,不能使用 Run。
执行外部命令示例
前面讲到,通过 Cmd 实例后,有两种方式运行命令。有时候,我们不只是简单的运行命令,还希望能控制命令的输入和输出。通过上面的 API 介绍,控制输入输出有几种方法:
- 得到 Cmd 实例后,直接给它的字段 Stdin、Stdout 和 Stderr 赋值;
- 通过 Output 或 CombinedOutput 获得输出;
- 通过带 Pipe 后缀的方法获得管道,用于输入或输出;
直接赋值 Stdin、Stdout 和 Stderr
func FillStd(name string, arg ...string) ([]byte, error) {
cmd := exec.Command(name, arg...)
var out = new(bytes.Buffer)
cmd.Stdout = out
cmd.Stderr = out
err := cmd.Run()
if err != nil {
return nil, err
}
return out.Bytes(), nil
}
使用 Output
func UseOutput(name string, arg ...string) ([]byte, error) {
return exec.Command(name, arg...).Output()
}
使用 Pipe
func UsePipe(name string, arg ...string) ([]byte, error) {
cmd := exec.Command(name, arg...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err = cmd.Start(); err != nil {
return nil, err
}
var out = make([]byte, 0, 1024)
for {
tmp := make([]byte, 128)
n, err := stdout.Read(tmp)
out = append(out, tmp[:n]...)
if err != nil {
break
}
}
if err = cmd.Wait(); err != nil {
return nil, err
}
return out, nil
}
完整代码见 os_exec。
进程终止
os.Exit() 函数会终止当前进程,对应的系统调用不是 _exit,而是 exit_group。
func Exit(code int)
Exit 让当前进程以给出的状态码 code 退出。一般来说,状态码 0 表示成功,非 0 表示出错。进程会立刻终止,defer 的函数不会被执行。
进程属性和控制
每个进程都有一些属性,os
包提供了一些函数可以获取进程属性。
进程 ID
每个进程都会有一个进程 ID,可以通过 os.Getpid
获得。同时,每个进程都有创建自己的父进程,通过 os.Getppid
获得。
进程凭证
Unix 中进程都有一套数字表示的用户 ID(UID) 和组 ID(GID),有时也将这些 ID 称之为进程凭证。Windows 下总是 -1。
实际用户 ID 和实际组 ID
实际用户 ID(real user ID)和实际组 ID(real group ID)确定了进程所属的用户和组。登录 shell 从 /etc/passwd
文件读取用户 ID 和组 ID。当创建新进程时(如 shell 执行程序),将从其父进程中继承这些 ID。
可通过 os.Getuid()
和 os.Getgid()
获取当前进程的实际用户 ID 和实际组 ID;
有效用户 ID 和有效组 ID
大多数 Unix 实现中,当进程尝试执行各种操作(即系统调用)时,将结合有效用户 ID、有效组 ID,连同辅助组 ID 一起来确定授予进程的权限。内核还会使用有效用户 ID 来决定一个进程是否能向另一个进程发送信号。
有效用户 ID 为 0(root 的用户 ID)的进程拥有超级用户的所有权限。这样的进程又称为特权级进程(privileged process)。某些系统调用只能由特权级进程执行。
可通过 os.Geteuid()
和 os.Getegid()
获取当前进程的有效用户 ID(effective user ID)和有效组 ID(effectvie group ID)。
通常,有效用户 ID 及组 ID 与其相应的实际 ID 相等,但有两种方法能够致使二者不同。一是使用相关系统调用;二是执行 set-user-ID 和 set-group-ID 程序。
Set-User-ID 和 Set-Group-ID 程序
set-user-ID
程序会将进程的有效用户 ID 置为可执行文件的用户 ID(属主),从而获得常规情况下并不具有的权限。set-group-ID
程序对进程有效组 ID 实现类似任务。(有时也将这程序简称为 set-UID 程序和 set-GID 程序。)
与其他文件一样,可执行文件的用户 ID 和组 ID 决定了该文件的所有权。在 6.1 os — 平台无关的操作系统功能实现 中提到过,文件还拥有两个特别的权限位 set-user-ID 位和 set-group-ID 位,可以使用 os.Chmod
修改这些权限位(非特权用户进程只能修改其自身文件,而特权用户进程能修改任何文件)。
文件设置了 set-user-ID 位后,ls -l
显示文件后,会在属主用户执行权限字段上看到字母 s(有执行权限时) 或 S(无执行权限时);相应的 set-group-ID 则是在组用户执行位上看到 s 或 S。
当运行 set-user-ID 程序时,内核会将进程的有效用户 ID 设置为可执行文件的用户 ID。set-group-ID 程序对进程有效组 ID 的操作与之类似。通过这种方法修改进程的有效用户 ID 或组 ID,能够使进程(换言之,执行该程序的用户)获得常规情况下所不具有的权限。例如,如果一个可执行文件的属主为 root,且为此程序设置了 set-user-ID 权限位,那么当运行该程序时,进程会取得超级用户权限。
也可以利用程序的 set-user-ID 和 set-group-ID 机制,将进程的有效 ID 修改为 root 之外的其他用户。例如,为提供一个受保护文件的访问,可采用如下方案:创建一个具有对该文件访问权限的专有用户(组)ID,然后再创建一个 set-user-ID(set-group-ID)程序,将进程有效用户(组)ID 变更为这个专用 ID。这样,无需拥有超级用户的所有权限,程序就能访问该文件。
Linux 系统中经常使用的 set-user-ID 程序,如 passwd。
测试 set-user-ID 程序
在 Linux 的某个目录下,用 root 账号创建一个文件:
echo "This is my shadow, studygolang." > my_shadow.txt
然后将所有权限都去掉:chmod 0 my_shadow.txt
。 ls -l 结果类似如下:
---------- 1 root root 32 6 月 24 17:31 my_shadow.txt
这时,如果非 root 用户是无法查看文件内容的。
接着,用 root 账号创建一个 main.go
文件,内容如下:
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
)
func main() {
file, err := os.Open("my_shadow.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
log.Fatal(err)
}
fmt.Printf("my_shadow:%s\n", data)
}
就是简单地读取 my_shadow
文件内容。go build main.go
后,生成的 main
可执行文件,权限是:-rwxrwxr-x
。
这时,切换到非 root 用户,执行 ./main
,会输出:
open my_shadow.txt: permission denied
因为这时的 main
程序生成的进程有效用户 ID 是当前用户的(非 root)。
接着,给 main
设置 set-user-ID
位:chmod u+s main
,权限变为 -rwsrwxr-x
,非 root 下再次执行 ./main
,输出:
my_shadow:This is my shadow, studygolang.
因为设置了 set-user-ID
位,这时 main
程序生成的进程有效用户是 main
文件的属主,即 root 的 ID,因此有权限读 my_shadow.txt
。
修改进程的凭证
os
包没有提供相应的功能修改进程的凭证,在 syscall
包对这些系统调用进行了封装。因为 https://golang.org/s/go1.4-syscall,用户程序不建议直接使用该包,应该使用 golang.org/x/sys
包代替。
该包提供了修改进程各种 ID 的系统调用封装,这里不一一介绍。
此外,os
还提供了获取辅助组 ID 的函数:os.Getgroups()
。
操作系统用户
包 os/user
允许通过名称或 ID 查询用户账号。用户结构定义如下:
type User struct {
Uid string // user id
Gid string // primary group id
Username string
Name string
HomeDir string
}
User
代表一个用户帐户。
在 POSIX 系统中 Uid 和 Gid 字段分别包含代表 uid 和 gid 的十进制数字。在 Windows 系统中 Uid 和 Gid 包含字符串格式的安全标识符(SID)。在 Plan 9 系统中,Uid、Gid、Username 和 Name 字段是 /dev/user 的内容。
Current
函数可以获取当前用户账号。而 Lookup
和 LookupId
则分别根据用户名和用户 ID 查询用户。如果对应的用户不存在,则返回 user.UnknownUserError
或 user.UnknownUserIdError
。
package main
import (
"fmt"
"os/user"
)
func main() {
fmt.Println(user.Current())
fmt.Println(user.Lookup("xuxinhua"))
fmt.Println(user.LookupId("0"))
}
// Output:
// &{502 502 xuxinhua /home/xuxinhua} <nil>
// &{502 502 xuxinhua /home/xuxinhua} <nil>
// &{0 0 root root /root} <nil>
进程的当前工作目录
一个进程的当前工作目录(current working directory)定义了该进程解析相对路径名的起点。新进程的当前工作目录继承自其父进程。
func Getwd() (dir string, err error)
Getwd
返回一个对应当前工作目录的根路径。如果当前目录可以经过多条路径抵达(比如符号链接),Getwd
会返回其中一个。对应系统调用:getcwd
。
func Chdir(dir string) error
相应的,Chdir
将当前工作目录修改为 dir
指定的目录。如果出错,会返回 *PathError
类型的错误。对应系统调用 chdir
。
另外,os.File
有一个方法:Chdir
,对应系统调用 fchidr
(以文件描述符为参数),也可以改变当前工作目录。
改变进程的根目录
每个进程都有一个根目录,该目录是解释绝对路径(即那些以 / 开始的目录)时的起点。默认情况下,这是文件系统的真是根目录。新进程从其父进程处继承根目录。有时可能需要改变一个进程的根目录(比如 ftp 服务就是一个典型的例子)。系统调用 chroot
能改变一个进程的根目录,Go 中对应的封装在 syscall.Chroot
。
除此之外,在 fork
子进程时,可以通过给 syscall.SysProcAttr
结构的 Chroot
字段指定一个路径,来初始化子进程的根目录。
进程环境列表
每个进程都有与其相关的称之为环境列表(environment list)的字符串数组,或简称环境(environment)。其中每个字符串都以 名称 = 值(name=value)形式定义。因此,环境是“名称 - 值”的成对集合,可存储任何信息。常将列表中的名称称为环境变量(environment variables)。
新进程在创建之时,会继承其父进程的环境副本。这是一种原始的进程间通信方式,却颇为常用。环境(environment)提供了将信息和父进程传递给子进程的方法。创建后,父子进程的环境相互独立,互不影响。
环境变量的常见用途之一是在 shell 中,通过在自身环境中放置变量值,shell 就可确保把这些值传递给其所创建的进程,并以此来执行用户命令。
在程序中,可以通过 os.Environ
获取环境列表:
func Environ() []string
返回的 []string
中每个元素是 key=value
的形式。
func Getenv(key string) string
Getenv
检索并返回名为 key
的环境变量的值。如果不存在该环境变量会返回空字符串。有时候,可能环境变量存在,只是值刚好是空。为了区分这种情况,提供了另外一个函数 LookupEnv()
:
func LookupEnv(key string) (string, bool)
如果变量名存在,第二个参数返回 true
,否则返回 false
。
func Setenv(key, value string) error
Setenv
设置名为 key
的环境变量,值为 value
。如果出错会返回该错误。(如果值之前存在,会覆盖)
func Unsetenv(key string) error
Unsetenv
删除名为 key
的环境变量。
func Clearenv()
Clearenv
删除所有环境变量。
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("The num of environ:", len(os.Environ()))
godebug, ok := os.LookupEnv("GODEBUG")
if ok {
fmt.Println("GODEBUG==", godebug)
} else {
fmt.Println("GODEBUG not exists!")
os.Setenv("GODEBUG", "gctrace=1")
fmt.Println("after setenv:", os.Getenv("GODEBUG"))
}
os.Clearenv()
fmt.Println("clearenv, the num:", len(os.Environ()))
}
// Output:
// The num of environ: 25
// GODEBUG not exists!
// after setenv: gctrace=1
// clearenv, the num: 0
另外,ExpandEnv
和 Getenv
功能类似,不过,前者使用变量方式,如:
os.ExpandEnv("$GODEBUG") 和 os.Getenv("GODEBUG") 是一样的。
实际上,os.ExpandEnv
调用的是 os.Expand(s, os.Getenv)
。
func Expand(s string, mapping func(string) string) string
Expand
能够将 ${var} 或 $var 形式的变量,经过 mapping 处理,得到结果。
线程
与进程类似,线程是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域。
同一进程中的多个线程可以并发执行。在多处理器环境下,多个线程可以同时并行。如果一个线程因等待 I/O 操作而遭阻塞,那么其他线程依然可以继续运行。
在 Linux 中,通过系统调用 clone()
来实现线程的。从前面的介绍,我们知道,该系统调用也可以用来创建进程。实际上,从内核的角度来说,它并没有线程这个概念。Linux 把所有的线程都当作进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个使用某些共享资源的进程。所以,在内核中,它看起来就是一个普通的进程(只是该进程和其他一些进程共享某些资源,如地址空间)。
在 Go 中,通过 clone()
系统调用来创建线程,其中的 clone_flags
为:
cloneFlags = _CLONE_VM | /* share memory */
_CLONE_FS | /* share cwd, etc */
_CLONE_FILES | /* share fd table */
_CLONE_SIGHAND | /* share sig handler table */
_CLONE_THREAD /* revisit - okay for now */
也就是说,父子俩共享了地址空间 (CLONE_VM)、文件系统资源 (CLONE_FS)、文件描述符 (CLONE_FILES) 和信号处理程序 (CLONE_SIGHAND)。而 _CLONE_THREAD
则会将父子进程放入相同的线程组。这样一来,新建的进程和父进程都叫做线程。
文档来自:https://github.com/polaris1119/The-Golang-Standard-Library-by-Example
标签:---,调用,string,err,goroutine,golang,进程,os,ID From: https://blog.51cto.com/wyf1226/5951436