🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 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 — 平台无关的操作系统功能实现](chapter06/06.1.md) 中提到过,文件还拥有两个特别的权限位 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](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 处理,得到结果。 # 导航 # - 上一节:[创建进程](10.1.md) - 下一节:[进程间通信](10.3.md)