Go语言标准库

 主页   资讯   文章   代码   电子书 

10.2 进程属性和控制

每个进程都有一些属性,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 函数可以获取当前用户账号。而 LookupLookupId 则分别根据用户名和用户 ID 查询用户。如果对应的用户不存在,则返回 user.UnknownUserErroruser.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

另外,ExpandEnvGetenv 功能类似,不过,前者使用变量方式,如:

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 处理,得到结果。

导航