# 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)
- 简介
- 第一章 输入输出 (Input/Output)
- 1.1 io — 基本的 IO 接口
- 1.2 ioutil — 方便的 IO 操作函数集
- 1.3 fmt — 格式化 IO
- 1.4 bufio — 缓存 IO
- 第二章 文本
- 2.1 strings — 字符串操作
- 2.2 bytes — byte slice 便利操作
- 2.3 strconv — 字符串和基本数据类型之间转换
- 2.4 regexp — 正则表达式
- 2.5 unicode — Unicode 码点、UTF-8/16 编码
- 第三章 数据结构与算法
- 3.1 sort — 排序算法
- 3.2 index/suffixarray — 后缀数组实现子字符串查询
- 3.3 container — 容器数据类型:heap、list 和 ring
- 第四章 日期与时间
- 4.1 主要类型概述
- 4.2 时区
- 4.3 Time类型详解
- 4.4 定时器
- 第六章 文件系统
- 6.1 os — 平台无关的操作系统功能实现
- 6.2 path/filepath — 操作路径
- 第七章 数据持久存储与交换
- 7.1 database/sql — SQL/SQL-Like 数据库操作接口
- 第八章 数据压缩与归档
- 8.1 flate * DEFLATE 压缩算法
- 第九章 测试
- 9.1 testing * 单元测试
- 9.2 testing * 基准测试
- 9.3 testing * 子测试
- 9.4 testing * 运行并验证示例
- 9.5 testing * 其他功能
- 9.6 httptest * HTTP 测试辅助工具
- 9.7 总结
- 第十章 进程、线程与 goroutine
- 10.1 创建进程
- 10.2 进程属性和控制
- 10.3 线程
- 10.4 进程间通信
- 第十三章 应用构建 与 debug
- 13.1 flag * 命令行参数解析
- 13.2 log * 日志记录
- 13.3 expvar * 公共变量的标准化接口
- 13.4 runtime/debug * 运行时的调试工具