結合 Go 讀 APUE-基本檔案I/O

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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

基本的檔案 I/O

我想 open, read, write, lseek, close 這個幾個操作就滿足了對檔案操作的基本需求。當然了,我也是看書是這麼寫的。

每個語言基本都有對應的函數或方法,我們調用就行,在這種情況下,我們可以理解成 -> 語言就是個工具。我比較偏向 Go 的風格,所以這裡我以 Go 的函數庫為例,但在介紹其之前,要明白一個概念:檔案描述符。

畫中重點了:

對於核心而言, 所有開啟的檔案都通過檔案描述符引用。檔案描述符是一個非負整數。

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

當開啟一個現有檔案或建立一個新的檔案時,核心向進程返回一個 檔案描述符

當讀、寫一個檔案時,使用 open 或 create 返回的 檔案描述符 標識該檔案,將 檔案描述符 作為參數傳遞給 read 或 write。

通常用變數 fd 來表示檔案描述符 (file descripter)

函數 open 和 openat & 函數 create

調用 open 或 openat 函數就可以開啟或建立一個檔案。

#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 */);

調用 create 函數建立一個新檔案。

#include <fcntl.h>int create(const char *path, mode_t mode);

上面函數中的參數:

  • path 是要開啟或建立檔案的名字
  • oflag 是對檔案進行哪些操作的 flag, 例如:O_RDWR|O_CREATE|O_TRUNC
  • mode 指定該檔案的存取權限位
  • fd 表示檔案描述符

在這裡羅列了 Go 中對檔案進行哪些操作的 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 開啟或建立一個檔案:

// 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)}

通過觀察源碼,得知二者都是調用 OpenFile 函數,只是 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}

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

瞭解下 StickyBit (粘著位):

在 UNIX 還沒有使用請求分頁式技術的早期版本中,如果 可執行檔 設定了 StickyBit,在執行該檔案結束時,程式的本文部分的一個副本仍被儲存在交換區,以便下次執行時,可以迅速裝入記憶體。然而現今的 UNIX 中大多數配置了虛擬儲存系統以及快速檔案系統,所以不再需要使用該技術啦。

OpenFile 函數源碼中, 常量supportsCreatedWithStickyBit 在 Ubuntu 16.04 環境下的值是 true, 故那部分代碼不會被執行。所以在 Ubuntu 16.04 環境下的開發人員可以不用去瞭解 if !supportsCreatedWithStickyBit ... 代碼塊。由於使用 Ubuntu 16.04 的緣故,所以 OpenFile 函數可以簡化如下:

// 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}

簡化後的代碼,發現核心代碼就是:syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm)), 觸發系統調用。在深入瞭解之前,咱先把 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 結尾的檔案是 “posix系統” (不存在的) 下調用的,查了之後,才知道是 unix 系統下調用的。

那讓我們關注點切換到 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 是個介面, 只要實現了 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 這個函數是怎麼定義的,源碼如下:

// 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}

上面函數中 fd 經過一輪迴又回到了 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}

上面函數中 runtime.SetFinalizer(f.file, (*file).close), 類型 c/c++ 中的 解構函式 吧!(挖, 先這吧)

函數 close

調用 close 函數關閉一個開啟檔案。

#include <unistd.h>int close(int fd);

如何用 Go 來關閉一個檔案呢?

// 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,起著關鍵性的作用。其源碼如下:

// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDITfunc 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) 估計是更底層的調用了,就不再挖啦。

函數 lseek

調用 lseek 顯式地為一個開啟檔案設定位移量。

#include <unistd.h>off_t lseek(int fd, off_t offset, int whence);

上面函數中的參數:

  • fd 表示檔案描述符
  • 若 whence 是 SEEK_SET, 則將該檔案的位移量設定為距檔案開始處 offset 個位元組
  • 若 whence 是 SEEK_CUR, 則將該檔案的位移量設定為其當前值加 offset, offset可正可負
  • 若 whence 是 SEEK_END, 則將該檔案的位移量設定為檔案長度加 offset, offset可正可負

這些參數是在 Go 也適用的, 但是這種方式,已經在 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 來設定檔案的位移量呢?

// 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) 起著關鍵性的作用。

// 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) 發起了一個系統調用,再挖就到了再底層和彙編啦。

函數 read

調用 read 函數從開啟檔案中讀取資料。

#include <unistd.h>ssize_t read(int fd, void *buf, size_t nbytes);

上面函數中的參數:

  • fd 表示檔案描述符
  • buf 要讀的檔案,類型是通用的指標
  • nbytes 表示讀取的位元組數

如果 read 成功, 則返回讀到的位元組數,如已到達檔案的尾端,則返回 0。

Tips: 有多種情況可能使實際讀到的位元組數少於要求讀的位元組數。

如何用 Go 從開啟檔案中讀取資料呢?

// 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

函數 write

調用 write 函數向開啟檔案寫資料。

#include <unistd.h>ssize_t write(int fd, const void *buf, size_t nbytes);

上面函數的參數:

  • fd 表示檔案描述符
  • buf 要寫的檔案,類型是通用的指標
  • nbytes 表示讀取的位元組數

如果 write 成功, 則返回讀到的位元組數,如已到達檔案的尾端,則返回 0。

如何用 Go 向開啟檔案中寫入資料?

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

結束

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.