Skip to content

Commit

Permalink
os 文件系统相关完成
Browse files Browse the repository at this point in the history
  • Loading branch information
polaris1119 committed Jun 19, 2016
1 parent 8a9b3d6 commit 1a26f2e
Show file tree
Hide file tree
Showing 2 changed files with 329 additions and 1 deletion.
284 changes: 283 additions & 1 deletion chapter06/06.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func Create(name string) (*File, error) {

`Write` 对应的系统调用是 `write`

`Write``WriteAt` 的区别同 `Read``ReadAt` 的区别一样。
`Write``WriteAt` 的区别同 `Read``ReadAt` 的区别一样。为了方便,还提供了 `WriteString` 方法,它实际是对 `Write` 的封装。

注意:`Write` 调用成功并不能保证数据已经写入磁盘,因为内核会缓存磁盘的 I/O 操作。如果希望立刻将数据写入磁盘(一般场景不建议这么做,因为会影响性能),有两种办法:

Expand Down Expand Up @@ -195,6 +195,288 @@ file.Seek(1000, SEEK_END) // 文件结尾处的下1001个字节

`Seek` 对应系统调用 `lseek`。该系统调用并不适用于所有类型,不允许将 `lseek ` 应用于管道、FIFO、socket 或 终端。

## 截断文件

`trucate``ftruncate` 系统调用将文件大小设置为 `size` 参数指定的值;Go 语言中相应的包装函数是 `os.Truncate``os.File.Truncate`

```
func Truncate(name string, size int64) error
func (f *File) Truncate(size int64) error
```
如果文件当前长度大于参数 `size`,调用将丢弃超出部分,若小于参数 `size`,调用将在文件尾部添加一系列空字节或是一个文件空洞。

它们之间的区别在于如何指定操作文件:

1. `Truncate` 以路径名称字符串来指定文件,并要求可访问该文件(即对组成路径名的各目录拥有可执行(x)权限),且对文件拥有写权限。若文件名为符号链接,那么调用将对其进行解引用。
2. 很明显,调用 `File.Truncate` 前,需要先以可写方式打开操作文件,该方法不会修改文件偏移量。

## 文件属性

文件属性,也即文件元数据。在 Go 中,文件属性具体信息通过 `os.FileInfo` 接口获取。函数 `Stat``Lstat``File.Stat` 可以得到该接口的实例。这三个函数对应三个系统调用:`stat``lstat``fstat`

这三个函数的区别:

1. `stat` 会返回所命名文件的相关信息。
2. `lstat``stat` 类似,区别在于如果文件是符号链接,那么所返回的信息针对的是符号链接自身(而非符号链接所指向的文件)。
3. `fstat` 则会返回由某个打开文件描述符(Go 中则是当前打开文件 File)所指代文件的相关信息。

`Stat``Lstat` 无需对其所操作的文件本身拥有任何权限,但针对指定 name 的父目录要有执行(搜索)权限。而只要 `File` 对象 ok,`File.Stat` 总是成功。

`FileInfo` 接口如下:

```
type FileInfo interface {
Name() string // 文件的名字(不含扩展名)
Size() int64 // 普通文件返回值表示其大小;其他文件的返回值含义各系统不同
Mode() FileMode // 文件的模式位
ModTime() time.Time // 文件的修改时间
IsDir() bool // 等价于Mode().IsDir()
Sys() interface{} // 底层数据来源(可以返回nil)
}
```

`Sys()` 底层数据的 C语言 结构 `statbuf` 格式如下:

```
struct stat {
dev_t st_dev; // 设备ID
ino_t st_ino; // 文件 i 节点号
mode_t st_mode; // 位掩码,文件类型和文件权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 文件属主,用户ID
gid_t st_gid; // 文件属组,组ID
dev_t st_rdev; // 如果针对设备 i 节点,则此字段包含主、辅 ID
off_t st_size; // 常规文件,则是文件字节数;符号链接,则是链接所指路径名的长度,字节为单位;对于共享内存对象,则是对象大小
blksize_t st_blsize; // 分配给文件的总块数,块大小为512字节
blkcnt_t st_blocks; // 实际分配给文件的磁盘块数量
time_t st_atime; // 对文件上次访问时间
time_t st_mtime; // 对文件上次修改时间
time_t st_ctime; // 文件状态发生改变的上次时间
}
```
Go 中 `syscal.Stat_t` 与该结构对应。

如果我们要获取 `FileInfo` 接口没法直接返回的信息,比如想获取文件的上次访问时间,示例如下:

```
fileInfo, err := os.Stat("test.log")
if err != nil {
log.Fatal(err)
}
sys := fileInfo.Sys()
stat := sys.(*syscall.Stat_t)
fmt.Println(time.Unix(stat.Atimespec.Unix()))
```

### 改变文件时间戳

可以显示改变文件的访问时间和修改时间。

`func Chtimes(name string, atime time.Time, mtime time.Time) error`

`Chtimes` 修改 name 指定的文件对象的访问时间和修改时间,类似Unix的 utime() 或 utimes() 函数。底层的文件系统可能会截断/舍入时间单位到更低的精确度。如果出错,会返回 `*PathError` 类型的错误。在 Unix 中,底层实现会调用 `utimenstat()`,它提供纳秒级别的精度。

### 文件属主

每个文件都有一个与之关联的用户ID(UID)和组ID(GID),籍此可以判定文件的属主和属组。系统调用 `chown``lchown``fchown` 可用来改变文件的属主和属组,Go 中对应的函数或方法:

```
func Chown(name string, uid, gid int) error
func Lchown(name string, uid, gid int) error
func (f *File) Chown(uid, gid int) error
```
它们的区别和上文提到的 `Stat` 相关函数类似。

### 文件权限

这里介绍是应用于文件和目录的权限方案,尽管此处讨论的权限主要是针对普通文件和目录,但其规则可适用于所有文件类型,包括设备文件、FIFO 以及 Unix 域套接字等。

#### 普通文件的权限

如前所述,`os.FileMode` 或 C 结构 stat 中的 `st_mod` 的低 12 位定义了文件权限。其中前 3 位为专用位,分别是 set-user-ID 位、set-group-ID 位和 sticky 位。其余 9 位则构成了定义权限的掩码,分别授予访问文件的各类用户。文件权限掩码分为3类:

- Owner(亦称为 user):授予文件属主的权限。
- Group:授予文件属组成员用户的权限。
- Other:授予其他用户的权限。

可为每一类用户授予的权限如下:

- Read:可阅读文件的内容。
- Write:可更改文件的内容。
- Execute:可以执行文件(如程序或脚本)。

Unix 中表示:rwxrwxrwx。

#### 目录权限

目录与文件拥有相同的权限方案,只是对 3 种权限的含义另有所指。

- 读权限:可列出(比如,通过 ls 命令)目录之下的内容(即目录下的文件名)。
- 写权限:可在目录内创建、删除文件。注意,要删除文件,对文件本身无需有任何权限。
- 可执行权限:可访问目录中的文件。因此,有时也将对目录的执行权限称为 search(搜索)权限。

访问文件时,需要拥有对路径名所列所有目录的执行权限。例如,想读取文件 `/home/studygolang/abc`,则需拥有对目录 `/``/home` 以及 `/home/studygolang` 的执行权限(还要有对文件 `abc` 自身的读权限)。

#### 相关函数或方法

在文件相关操作报错时,可以通过 `os.IsPermission` 检查是否是权限的问题。

`func IsPermission(err error) bool`

返回一个布尔值说明该错误是否表示因权限不足要求被拒绝。ErrPermission 和一些系统调用错误会使它返回真。

另外,`syscall.Access` 可以获取文件的权限。这对应系统调用 `access`

#### Sticky 位

除了 9 位用来表明属主、属组和其他用户的权限外,文件权限掩码还另设有 3 个附加位,分别是 set-user-ID(bit 04000)、set-group-ID(bit 02000) 和 sticky(bit 01000)位。set-user-ID 和 set-group-ID 权限位将在进程章节介绍。这里介绍 sticky 位。

Sticky 位一般用于目录,起限制删除位的作用,表明仅当非特权进程具有对目录的写权限,且为文件或目录的属主时,才能对目录下的文件进行删除和重命名操作。根据这个机制来创建为多个用户共享的一个目录,各个用户可在其下创建或删除属于自己的文件,但不能删除隶属于其他用户的文件。`/tmp` 目录就设置了 sticky 位,正是出于这个原因。

`chmod` 命令或系统调用可以设置文件的 sticky 位。若对某文件设置了 sticky 位,则 `ls -l` 显示文件时,会在其他用户执行权限字段上看到字母 t(有执行权限时) 或 T(无执行权限时)。

`os.Chmod``os.File.Chmod` 可以修改文件权限(包括 sticky 位),分别对应系统调用 `chmod``fchmod`

```
func main() {
file, err := os.Create("studygolang.txt")
if err != nil {
log.Fatal("error:", err)
}
defer file.Close()
fileMode := getFileMode(file)
log.Println("file mode:", fileMode)
file.Chmod(fileMode | os.ModeSticky)
log.Println("change after, file mode:", getFileMode(file))
}
func getFileMode(file *os.File) os.FileMode {
fileInfo, err := file.Stat()
if err != nil {
log.Fatal("file stat error:", err)
}
return fileInfo.Mode()
}
// Output:
// 2016/06/18 15:59:06 file mode: -rw-rw-r--
// 2016/06/18 15:59:06 change after, file mode: trw-rw-r--
// ls -l 看到的 studygolang.tx 是:-rw-rw-r-T
// 当然这里是给文件设置了 sticky 位,对权限不起作用。系统会忽略它。
```

## 目录与链接

在 Unix 文件系统中,目录的存储方式类似于普通文件。目录和普通文件的区别有二:

- 在其 i-node 条目中,会将目录标记为一种不同的文件类型。
- 目录是经特殊组织而成的文件。本质上说就是一个表格,包含文件名和 i-node 标号。

### 创建和移除(硬)链接

硬链接是针对文件而言的,目录不允许创建硬链接。

`link``unlink` 系统调用用于创建和移除(硬)链接。Go 中 `os.Link` 对应 `link` 系统调用;但 `os.Remove` 的实现会先执行 `unlink` 系统调用,如果要移除的是目录,则 `unlink` 会失败,这时 `Remove` 会再调用 `rmdir` 系统调用。

`func Link(oldname, newname string) error`

`Link` 创建一个名为 newname 指向 oldname 的硬链接。如果出错,会返回 `*LinkError` 类型的错误。

`func Remove(name string) error`

`Remove` 删除 name 指定的文件或目录。如果出错,会返回 `*PathError` 类型的错误。如果目录不为空,`Remove` 会返回失败。

### 更改文件名

系统调用 `rename` 既可以重命名文件,又可以将文件移至同一个文件系统中的另一个目录。该系统调用既可以用于文件,也可以用于目录。相关细节,请查阅相关资料。

Go 中的 `os.Rename` 是对应的封装函数。

`func Rename(oldpath, newpath string) error`

`Rename` 修改一个文件的名字或移动一个文件。如果 `newpath` 已经存在,则替换它。注意,可能会有一些个操作系统特定的限制。

### 使用符号链接

`symlink` 系统调用用于为指定路径名创建一个新的符号链接(想要移除符号链接,使用 `unlink`)。Go 中的 `os.Symlink` 是对应的封装函数。

`func Symlink(oldname, newname string) error`

`Symlink` 创建一个名为 `newname` 指向 `oldname` 的符号链接。如果出错,会返回 `*LinkError` 类型的错误。

`oldname` 所命名的文件或目录在调用时无需存在。因为即便当时存在,也无法阻止后来将其删除。这时,`newname` 成为“悬空链接”,其他系统调用试图对其进行解引用操作都将错误(通常错误号是 ENOENT)。

有时候,我们希望通过符号链接,能获取其所指向的路径名。系统调用 `readlink` 能做到,Go 的封装函数是 `os.Readlink`

`func Readlink(name string) (string, error)`

`Readlink` 获取 `name` 指定的符号链接指向的文件的路径。如果出错,会返回 `*PathError` 类型的错误。我们看看 `Readlink` 的实现。

```
func Readlink(name string) (string, error) {
for len := 128; ; len *= 2 {
b := make([]byte, len)
n, e := fixCount(syscall.Readlink(name, b))
if e != nil {
return "", &PathError{"readlink", name, e}
}
if n < len {
return string(b[0:n]), nil
}
}
}
```
这里之所以用循环,是因为我们没法知道文件的路径到底多长,如果 `b` 长度不够,文件名会被截断,而 `readlink` 系统调用无非分辨所返回的字符串到底是经过截断处理,还是恰巧将 `b` 填满。这里采用的验证方法是分配一个更大的(两倍)`b` 并再次调用 `readlink`

### 创建和移除目录

`mkdir` 系统调用创建一个新目录,Go 中的 `os.Mkdir` 是对应的封装函数。

`func Mkdir(name string, perm FileMode) error`

`Mkdir` 使用指定的权限和名称创建一个目录。如果出错,会返回 `*PathError` 类型的错误。

`name` 参数指定了新目录的路径名,可以是相对路径,也可以是绝对路径。如果已经存在,则调用失败并返回 `os.ErrExist` 错误。

`perm` 参数指定了新目录的权限。对该位掩码值的指定方式和 `os.OpenFile` 相同,也可以直接赋予八进制数值。注意,`perm` 值还将于进程掩码相与(&)。如果 `perm` 中设置了 sticky 位,那么将对新目录设置该权限。

因为 `Mkdir` 所创建的只是路径名中的最后一部分,如果父目录不存在,创建会失败。`os.MkdirAll` 用于递归创建所有不存在的目录。建议读者阅读下 `os.MkdirAll` 的源码,了解其实现方式、技巧。

`rmdir` 系统调用移除一个指定的目录,目录可以是绝对路径或相对路径。在讲解 `unlink` 时,已经介绍了 Go 中的 `os.Remove`。注意,这里要求目录必须为空。为了方便使用,Go 中封装了一个 `os.RemoveAll` 函数:

`func RemoveAll(path string) error`

`RemoveAll` 删除 `path` 指定的文件,或目录及它包含的任何下级对象。它会尝试删除所有东西,除非遇到错误并返回。如果 `path` 指定的对象不存在,`RemoveAll` 会返回 nil 而不返回错误。

`RemoveAll` 的内部实现逻辑如下:

1. 调用 `Remove` 尝试进行删除,如果成功或返回 `path` 不存在,则直接返回 nil;
2. 调用 `Lstat` 获取 `path` 信息,以便判断是否是目录。注意,这里使用 `Lstat`,表示不对符号链接解引用;
3. 调用 `Open` 打开目录,递归读取目录中内容,执行删除操作。

阅读 `RemoveAll` 源码,可以掌握马上要介绍的读目录内容或遍历目录。

### 读目录

`POSIX``SUS` 定义了读取目录相关的 C 语言标准,各个操作系统提供的系统调用却不尽相同。Go 没有基于 C 语言,而是自己通过系统调用实现了读目录功能。

`func (f *File) Readdirnames(n int) (names []string, err error)`

`Readdirnames` 读取目录 `f` 的内容,返回一个最多有 `n` 个成员的[]string,切片成员为目录中文件对象的名字,采用目录顺序。对本函数的下一次调用会返回上一次调用未读取的内容的信息。

如果 n>0,`Readdirnames` 函数会返回一个最多 n 个成员的切片。这时,如果 `Readdirnames` 返回一个空切片,它会返回一个非 nil 的错误说明原因。如果到达了目录 `f` 的结尾,返回值 err 会是 `io.EOF`

如果 n<=0,`Readdirnames` 函数返回目录中剩余所有文件对象的名字构成的切片。此时,如果 `Readdirnames` 调用成功(读取所有内容直到结尾),它会返回该切片和 nil 的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的名字构成的切片和该错误。

`func (f *File) Readdir(n int) (fi []FileInfo, err error)`

`Readdir` 内部会调用 `Readdirnames`,将得到的 `names` 构造路径,通过 `Lstat` 构造出 `[]FileInfo`

列出某个目录的文件列表示例程序见 `code/src/chapter06/os/dirtree/main.go`

# 导航 #

- [第六章](/chapter06/06.0.md)
Expand Down
46 changes: 46 additions & 0 deletions code/src/chapter06/os/dirtree/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"fmt"
"io"
"os"
"path/filepath"
)

func main() {
ReadAndOutputDir("../../..", 3)
}

func ReadAndOutputDir(rootPath string, deep int) {
file, err := os.Open(rootPath)
if err != nil {
fmt.Println("error:", err)
return
}
defer file.Close()

for {
fileInfos, err := file.Readdir(100)
if err != nil {
if err == io.EOF {
break
}
fmt.Println("readdir error:", err)
return
}

if len(fileInfos) == 0 {
break
}

for _, fileInfo := range fileInfos {
if fileInfo.IsDir() {
if deep > 0 {
ReadAndOutputDir(filepath.Clean(rootPath+string(os.PathSeparator)+fileInfo.Name()), deep-1)
}
} else {
fmt.Println("file:", fileInfo.Name(), "in directory:", rootPath)
}
}
}
}

0 comments on commit 1a26f2e

Please sign in to comment.