結(jié)合 Go 讀 APUE-基本文件I/O

在公眾號 "別捉急" 上 同步了文章,并且可以點擊原文鏈接閱讀:傳送門

基本的文件 I/O

我想 open, read, write, lseek, close 這個幾個操作就滿足了對文件操作的基本需求。當(dāng)然了,我也是看書是這么寫的。

每個語言基本都有對應(yīng)的函數(shù)或方法,我們調(diào)用就行,在這種情況下,我們可以理解成 -> 語言就是個工具。我比較偏向 Go 的風(fēng)格,所以這里我以 Go 的函數(shù)庫為例,但在介紹其之前,要明白一個概念:文件描述符。

畫中重點了:

對于內(nèi)核而言, 所有打開的文件都通過文件描述符引用。文件描述符是一個非負(fù)整數(shù)。

對上面的描述還是有點模糊呢?

當(dāng)打開一個現(xiàn)有文件或創(chuàng)建一個新的文件時,內(nèi)核向進(jìn)程返回一個 文件描述符

當(dāng)讀、寫一個文件時,使用 open 或 create 返回的 文件描述符 標(biāo)識該文件,將 文件描述符 作為參數(shù)傳遞給 read 或 write。

通常用變量 fd 來表示文件描述符 (file descripter)

函數(shù) open 和 openat & 函數(shù) create

調(diào)用 open 或 openat 函數(shù)就可以打開或創(chuàng)建一個文件。

#include <fcntl.h>

int open(const char *path, int oflag, ... /* mode_t mode */);

int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);

調(diào)用 create 函數(shù)創(chuàng)建一個新文件。

#include <fcntl.h>

int create(const char *path, mode_t mode);

上面函數(shù)中的參數(shù):

  • path 是要打開或創(chuàng)建文件的名字
  • oflag 是對文件進(jìn)行哪些操作的 flag, 例如:O_RDWR|O_CREATE|O_TRUNC
  • mode 指定該文件的訪問權(quán)限位
  • fd 表示文件描述符

在這里羅列了 Go 中對文件進(jìn)行哪些操作的 flags:

// Flags to OpenFile wrapping those of the underlying system. Not all
// flags may be implemented on a given system.
const (
    O_RDONLY int = syscall.O_RDONLY // open the file read-only.
    O_WRONLY int = syscall.O_WRONLY // open the file write-only.
    O_RDWR   int = syscall.O_RDWR   // open the file read-write.
    O_APPEND int = syscall.O_APPEND // append data to the file when writing.
    O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
    O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist
    O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
    O_TRUNC  int = syscall.O_TRUNC  // if possible, truncate file when opened.
)

如何用 Go 打開或創(chuàng)建一個文件:

// Open file 
func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

// Create file 
func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

通過觀察源碼,得知二者都是調(diào)用 OpenFile 函數(shù),只是 flag, mode 不同。

// OpenFile is the generalized open call; most users will use Open
// or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError.
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    chmod := false
    if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
        if _, err := Stat(name); IsNotExist(err) {
            chmod = true
        }
    }

    var r int
    for {
        var e error
        r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
        if e == nil {
            break
        }

        // On OS X, sigaction(2) doesn't guarantee that SA_RESTART will cause
        // open(2) to be restarted for regular files. This is easy to reproduce on
        // fuse file systems (see http://golang.org/issue/11180).
        if runtime.GOOS == "darwin" && e == syscall.EINTR {
            continue
        }

        return nil, &PathError{"open", name, e}
    }

    // open(2) itself won't handle the sticky bit on *BSD and Solaris
    if chmod {
        Chmod(name, perm)
    }

    // There's a race here with fork/exec, which we are
    // content to live with. See ../syscall/exec_unix.go.
    if !supportsCloseOnExec {
        syscall.CloseOnExec(r)
    }

    return newFile(uintptr(r), name), nil
}

當(dāng)讀上面這段代碼時,supportsCreatedWithStickyBit 這就卡住啦,知識點就是 StickyBit (粘著位)

了解下 StickyBit (粘著位):

在 UNIX 還沒有使用請求分頁式技術(shù)的早期版本中,如果 可執(zhí)行文件 設(shè)置了 StickyBit,在執(zhí)行該文件結(jié)束時,程序的正文部分的一個副本仍被保存在交換區(qū),以便下次執(zhí)行時,可以迅速裝入內(nèi)存。然而現(xiàn)今的 UNIX 中大多數(shù)配置了虛擬存儲系統(tǒng)以及快速文件系統(tǒng),所以不再需要使用該技術(shù)啦。

OpenFile 函數(shù)源碼中, 常量supportsCreatedWithStickyBit 在 Ubuntu 16.04 環(huán)境下的值是 true, 故那部分代碼不會被執(zhí)行。所以在 Ubuntu 16.04 環(huán)境下的開發(fā)者可以不用去了解 if !supportsCreatedWithStickyBit ... 代碼塊。由于使用 Ubuntu 16.04 的緣故,所以 OpenFile 函數(shù)可以簡化如下:

// OpenFile is the generalized open call; most users will use Open
// or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError.
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    var r int
    for {
        var e error
        r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
        if e == nil {
            break
        }
        return nil, &PathError{"open", name, e}
    }
    return newFile(uintptr(r), name), nil
}

簡化后的代碼,發(fā)現(xiàn)核心代碼就是:syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm)), 觸發(fā)系統(tǒng)調(diào)用。在深入了解之前,咱先把 syscallMode(prem) 解決掉,掃除障礙。

// syscallMode returns the syscall-specific mode bits from Go's portable mode bits.
func syscallMode(i FileMode) (o uint32) {
    o |= uint32(i.Perm())
    if i&ModeSetuid != 0 {
        o |= syscall.S_ISUID
    }
    if i&ModeSetgid != 0 {
        o |= syscall.S_ISGID
    }
    if i&ModeSticky != 0 {
        o |= syscall.S_ISVTX
    }
    // No mapping for Go's ModeTemporary (plan9 only).
    return
}

讓我們了解下 FileMode,源碼是這樣定義的 type FileMode uint32, 并通過查看源碼得值 i.Perm() 等價于 i & 0777, 并通過了解 Open 的 mode 為 0 ,syscallMode(0) == 0 ;Create 中 mode 為 0666, syscallMode(0666) == 438

Tips: 一開始因為 posix 結(jié)尾的文件是 “posix系統(tǒng)” (不存在的) 下調(diào)用的,查了之后,才知道是 unix 系統(tǒng)下調(diào)用的。

那讓我們關(guān)注點切換到 syscall.Open(name, mode, prem) 上, 類似 c 中的方法吧!深度的話先挖到這個地方。

讓我們回到簡化后的 OpenFile 剩余的知識點: PathError, NewFile(uintptr(r), name)

PathError 的源碼如下:

// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

error 是個接口, 只要實現(xiàn)了 Error 方法就 OK.

uintptr(r)uintptr 定義如下:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

uintptr(r)r 是個 int 類型。

看下 NewFile 這個函數(shù)是怎么定義的,源碼如下:

// NewFile returns a new File with the given file descriptor and name.
func NewFile(fd uintptr, name string) *File {
    fdi := int(fd)
    if fdi < 0 {
        return nil
    }
    f := &File{&file{fd: fdi, name: name}}
    runtime.SetFinalizer(f.file, (*file).close)
    return f
}

上面函數(shù)中 fd 經(jīng)過一輪回又回到了 int 類型。Filefile 類型的封裝,源碼如下:

// File represents an open file descriptor.
type File struct {
    *file // os specific
}

// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
    fd      int
    name    string
    dirinfo *dirInfo // nil unless directory being read
}

上面函數(shù)中 runtime.SetFinalizer(f.file, (*file).close), 類型 c/c++ 中的 析構(gòu)函數(shù) 吧!(挖, 先這吧)

函數(shù) close

調(diào)用 close 函數(shù)關(guān)閉一個打開文件。

#include <unistd.h>

int close(int fd);

如何用 Go 來關(guān)閉一個文件呢?

// Close closes the File, rendering it unusable for I/O.
// It returns an error, if any.
func (f *File) Close() error {
    if f == nil {
        return ErrInvalid
    }
    return f.file.close()
}

func (file *file) close() error {
    if file == nil || file.fd == badFd {
        return syscall.EINVAL
    }
    var err error
    if e := syscall.Close(file.fd); e != nil {
        err = &PathError{"close", file.name, e}
    }
    file.fd = -1 // so it can't be closed again

    // no need for a finalizer anymore
    runtime.SetFinalizer(file, nil)
    return err
}

從上面的代碼中可見,syscall.Close(file.fd) 類似 c 中的 close,起著關(guān)鍵性的作用。其源碼如下:

// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT

func Close(fd int) (err error) {
    _, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

Syscall(SYS_CLOSE, uintptr(fd), 0, 0) 估計是更底層的調(diào)用了,就不再挖啦。

函數(shù) lseek

調(diào)用 lseek 顯式地為一個打開文件設(shè)置偏移量。

#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

上面函數(shù)中的參數(shù):

  • fd 表示文件描述符
  • 若 whence 是 SEEK_SET, 則將該文件的偏移量設(shè)置為距文件開始處 offset 個字節(jié)
  • 若 whence 是 SEEK_CUR, 則將該文件的偏移量設(shè)置為其當(dāng)前值加 offset, offset可正可負(fù)
  • 若 whence 是 SEEK_END, 則將該文件的偏移量設(shè)置為文件長度加 offset, offset可正可負(fù)

這些參數(shù)是在 Go 也適用的, 但是這種方式,已經(jīng)在 Go 中棄用啦,詳情如下:

// Seek whence values.
//
// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
const (
    SEEK_SET int = 0 // seek relative to the origin of the file
    SEEK_CUR int = 1 // seek relative to the current offset
    SEEK_END int = 2 // seek relative to the end
)

如何用 Go 來設(shè)置文件的偏移量呢?

// Seek sets the offset for the next Read or Write on file to offset, interpreted
// according to whence: 0 means relative to the origin of the file, 1 means
// relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any.
// The behavior of Seek on a file opened with O_APPEND is not specified.
func (f *File) Seek(offset int64, whence int) (ret int64, err error) {
    if err := f.checkValid("seek"); err != nil {
        return 0, err
    }
    r, e := f.seek(offset, whence)
    if e == nil && f.dirinfo != nil && r != 0 {
        e = syscall.EISDIR
    }
    if e != nil {
        return 0, f.wrapErr("seek", e)
    }
    return r, nil
}

可見 f.seek(offset, whence) 起著關(guān)鍵性的作用。

// seek sets the offset for the next Read or Write on file to offset, interpreted
// according to whence: 0 means relative to the origin of the file, 1 means
// relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any.
func (f *File) seek(offset int64, whence int) (ret int64, err error) {
    return syscall.Seek(f.fd, offset, whence)
}

syscall.Seek(f.fd, offset, whence) 發(fā)起了一個系統(tǒng)調(diào)用,再挖就到了再底層和匯編啦。

函數(shù) read

調(diào)用 read 函數(shù)從打開文件中讀取數(shù)據(jù)。

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t nbytes);

上面函數(shù)中的參數(shù):

  • fd 表示文件描述符
  • buf 要讀的文件,類型是通用的指針
  • nbytes 表示讀取的字節(jié)數(shù)

如果 read 成功, 則返回讀到的字節(jié)數(shù),如已到達(dá)文件的尾端,則返回 0。

Tips: 有多種情況可能使實際讀到的字節(jié)數(shù)少于要求讀的字節(jié)數(shù)。

如何用 Go 從打開文件中讀取數(shù)據(jù)呢?

// Read reads up to len(b) bytes from the File.
// It returns the number of bytes read and any error encountered.
// At end of file, Read returns 0, io.EOF.
func (f *File) Read(b []byte) (n int, err error) {
    if err := f.checkValid("read"); err != nil {
        return 0, err
    }
    n, e := f.read(b)
    return n, f.wrapErr("read", e)
}

其底層代碼如上,遞歸查看 go package。

函數(shù) write

調(diào)用 write 函數(shù)向打開文件寫數(shù)據(jù)。

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);

上面函數(shù)的參數(shù):

  • fd 表示文件描述符
  • buf 要寫的文件,類型是通用的指針
  • nbytes 表示讀取的字節(jié)數(shù)

如果 write 成功, 則返回讀到的字節(jié)數(shù),如已到達(dá)文件的尾端,則返回 0。

如何用 Go 向打開文件中寫入數(shù)據(jù)?

func (f *File) Write(b []byte) (n int, err error)

結(jié)束

如果光看 APUE, 前幾頁還可以,慢慢就看不下去了,Go 的 lib 基本跟 unix 的接口相似,就結(jié)合著 Go 的源碼一起看了,只要有個大概的框架就 OK, 隨著往后慢慢深入,會有更深的理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容