1. 10.2 进程属性和控制
每个进程都有一些属性,os
包提供了一些函数可以获取进程属性。
1.1. 进程 ID
每个进程都会有一个进程 ID,可以通过 os.Getpid
获得。同时,每个进程都有创建自己的父进程,通过 os.Getppid
获得。
1.2. 进程凭证
Unix 中进程都有一套数字表示的用户 ID(UID) 和组 ID(GID),有时也将这些 ID 称之为进程凭证。Windows 下总是 -1。
1.2.1. 实际用户 ID 和实际组 ID
实际用户 ID(real user ID)和实际组 ID(real group ID)确定了进程所属的用户和组。登录 shell 从 /etc/passwd
文件读取用户 ID 和组 ID。当创建新进程时(如 shell 执行程序),将从其父进程中继承这些 ID。
可通过 os.Getuid()
和 os.Getgid()
获取当前进程的实际用户 ID 和实际组 ID;
1.2.2. 有效用户 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 程序。
1.2.3. 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
。
1.2.4. 修改进程的凭证
os
包没有提供相应的功能修改进程的凭证,在 syscall
包对这些系统调用进行了封装。因为 https://golang.org/s/go1.4-syscall,用户程序不建议直接使用该包,应该使用 golang.org/x/sys
包代替。
该包提供了修改进程各种 ID 的系统调用封装,这里不一一介绍。
此外,os
还提供了获取辅助组 ID 的函数:os.Getgroups()
。
1.2.5. 操作系统用户
包 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>
1.3. 进程的当前工作目录
一个进程的当前工作目录(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
(以文件描述符为参数),也可以改变当前工作目录。
1.4. 改变进程的根目录
每个进程都有一个根目录,该目录是解释绝对路径(即那些以 / 开始的目录)时的起点。默认情况下,这是文件系统的真是根目录。新进程从其父进程处继承根目录。有时可能需要改变一个进程的根目录(比如 ftp 服务就是一个典型的例子)。系统调用 chroot
能改变一个进程的根目录,Go 中对应的封装在 syscall.Chroot
。
除此之外,在 fork
子进程时,可以通过给 syscall.SysProcAttr
结构的 Chroot
字段指定一个路径,来初始化子进程的根目录。
1.5. 进程环境列表
每个进程都有与其相关的称之为环境列表(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 处理,得到结果。