[單刷APUE系列]第三章——文件I/O

原文來自靜雅齋,轉載請註明出處。javascript

文件描述符

在學習C語言的時候,應該也學習了使用<stdio.h>提供的通用文件操做,在C語言中,已經封裝好了File結構體幫助操做文件,打開<stdio.h>java

typedef struct __sFILE {
        unsigned char *_p;      /* current position in (some) buffer */
        int     _r;             /* read space left for getc() */
        int     _w;             /* write space left for putc() */
        short   _flags;         /* flags, below; this FILE is free if 0 */
        short   _file;          /* fileno, if Unix descriptor, else -1 */
        struct  __sbuf _bf;     /* the buffer (at least 1 byte, if !NULL) */
        int     _lbfsize;       /* 0 or -_bf._size, for inline putc */

        /* operations */
        void    *_cookie;       /* cookie passed to io functions */
        int     (*_close)(void *);
        int     (*_read) (void *, char *, int);
        fpos_t  (*_seek) (void *, fpos_t, int);
        int     (*_write)(void *, const char *, int);

        /* separate buffer for long sequences of ungetc() */
        struct  __sbuf _ub;     /* ungetc buffer */
        struct __sFILEX *_extra; /* additions to FILE to not break ABI */
        int     _ur;            /* saved _r when _r is counting ungetc data */

        /* tricks to meet minimum requirements even when malloc() fails */
        unsigned char _ubuf[3]; /* guarantee an ungetc() buffer */
        unsigned char _nbuf[1]; /* guarantee a getc() buffer */
        unsigned char _ubuf[3]; /* guarantee an ungetc() buffer */
        unsigned char _nbuf[1]; /* guarantee a getc() buffer */

        /* separate buffer for fgetln() when line crosses buffer boundary */
        struct  __sbuf _lb;     /* buffer for fgetln() */

        /* Unix stdio files get aligned to block boundaries on fseek() */
        int     _blksize;       /* stat.st_blksize (may be != _bf._size) */
        fpos_t  _offset;        /* current lseek offset (see WARNING) */
} FILE;複製代碼

可容易看到FILE結構體內有一個成員short _file;這就是Unix使用的文件描述符(file descriptor),並且結構體內除了必須的一些緩衝區、文件打開標誌等東西,還包括了以函數指針的方式提供的「成員函數」。若是一些朋友曾經使用過TC2.0而且查看過<stdio.h>頭文件,可能會驚訝在Unix環境下多出的很是多的內容。
像這種提供給開發者的操做文件的函數,都統稱爲帶緩衝的I/O函數,而Unix系統自己提供的就是不帶緩衝的I/O函數。
對於每一個運行中的進程,都維護了一個文件描述符表,文件描述符是一個非負整數,當打開一個文件的時候,內核會向進程返回一個文件描述符。按照Unix慣例,0、一、2的數字分別被標準輸入、標準輸出、標準錯誤相關聯,咱們也能夠將其進行替換。在POSIX規範中,已經提供了STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO來替代0、一、2數字,這樣更加便於開發者理解。
按照Unix規定,每一個Unix系統都應當提供OPEN_MAX限制做爲進程最大打開文件限制,也就是說文件描述符的範圍在0~OPEN_MAX-1的範圍內變更,可是用過Linux的朋友知道,可使用ulimit命令修改最大文件打開數,甚至能夠修改成無限,也就是說,沒法經過OPEN_MAX的定義來得到最大文件打開數。這也是上一章中提到的沒法在運行時得到的參數。shell

打開文件函數族

int open(const char *path, int oflag, ...);
int openat(int fd, const char *path, int oflag, ...);複製代碼

oflag能夠指定爲如下常量數據庫

O_RDONLY        open for reading only
O_WRONLY        open for writing only
O_RDWR          open for reading and writing
O_NONBLOCK      do not block on open or for data to become available
O_APPEND        append on each write
O_CREAT         create file if it does not exist
O_TRUNC         truncate size to 0
O_EXCL          error if O_CREAT and the file exists
O_SHLOCK        atomically obtain a shared lock
O_EXLOCK        atomically obtain an exclusive lock
O_NOFOLLOW      do not follow symlinks
O_SYMLINK       allow open of symlinks
O_EVTONLY       descriptor requested for event notifications only
O_CLOEXEC       mark as close-on-exec複製代碼

兩個函數就是一個絕對路徑和相對路徑的區別,oflag能夠進行組合,使用|或運算符構成新的參數。
具體的詳情能夠看原著解釋,裏面已經很是詳細。
在原著中寫了五個常量安全

O_RDONLY 只讀打開
O_WRONLY 只寫打開
O_RDWR   讀、寫打開
O_EXEC   只執行打開
O_SEARCH 只搜索打開複製代碼

而且指明這五個常量必須指定一個並且只能指定一個,可是根據筆者實際查看頭文件,發現O_EXECO_SEARCH常量並無在頭文件中出現,相反,頭文件中只找到了cookie

/*
 * File status flags: these are used by open(2), fcntl(2).
 * They are also used (indirectly) in the kernel file structure f_flags,
 * which is a superset of the open/fcntl flags.  Open flags and f_flags
 * are inter-convertible using OFLAGS(fflags) and FFLAGS(oflags).
 * Open/fcntl flags begin with O_; kernel-internal flags begin with F.
 */
/* open-only flags */
#define O_RDONLY        0x0000          /* open for reading only */
#define O_WRONLY        0x0001          /* open for writing only */
#define O_RDWR          0x0002          /* open for reading and writing */
#define O_ACCMODE       0x0003          /* mask for above modes */複製代碼

雖而後面還定義了一些POSIX實際上須要的函數,可是卻使用#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)條件編譯將其分類在了OS X自有源代碼下。數據結構

建立文件函數

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

The creat() function is the same as: open(path, O_CREAT | O_TRUNC | O_WRONLY, mode);複製代碼

其實把opencreat函數對比,能夠發現,creat功能以及徹底被open函數替代了,實際上這是一個歷史遺留產物,並且因爲creat函數有着諸多的限制,實際開發中極少使用到。從creat函數說明頁極少的說明也能夠看出官方也並不推薦使用creat。app

關閉文件

int close(int fildes);複製代碼

原著裏認爲當進程終止時內核會自動關閉全部打開文件,因此不須要顯式關閉,實際上在Unix手冊中是推薦使用close關閉async

When a process exits, all associated file descriptors are freed, but since there is a limit on active descriptors per processes, the
close() function call is useful when a large quantity of file descriptors are being handled. When a process forks (see fork(2)), all descriptors for the new child process reference the same objects as they did in the parent before the fork. If a new process is then to be run using execve(2), the process would normally inherit these descriptors. Most of the descriptors can be rearranged with dup2(2) or deleted with close() before the execve is attempted, but if some of these descriptors will still be needed if the execve fails, it is necessary to arrange for them to be closed if the execve succeeds. For this reason, the call ``fcntl(d, F_SETFD, 1)'' is provided, which arranges that a descriptor will be closed after a successful execve; the call ``fcntl(d, F_SETFD, 0)'' restores the default, which is to not close the descriptor.複製代碼

手冊頁還講到了關於fork進程致使的文件描述符繼承的狀況。ide

文件偏移

在學習C語言FILE文件操做的時候一般也會講到文件偏移量,文件偏移量其實是一個非負整數,可是能夠理解爲一個指針,指向當前文件從開頭開始的字節數,正常非O_APPEND方式打開,偏移量會被重置爲0。

off_t lseek(int fildes, off_t offset, int whence);複製代碼

whence參數只有三種值,0、一、2,不過都已經如同標準文件描述符同樣使用常量代替了,也就是SEEK_SETSEEK_CURSEEK_END,很是的簡潔易懂。
在原著中提到了lseek能夠造成文件空洞,實際上這個和內核實現無關,而是和文件系統相關,也就是說,容許空洞存在,如何存儲空洞,都是歸給文件系統的。目前大部分文件系統都是使用null填充。

#include "include/apue.h"
#include <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main(int argc, char *argv[])
{
    int fd;

    if ((fd = creat("file.hole", FILE_MODE)) < 0)
        err_sys("creat error");

    if (write(fd, buf1, 10) != 10)
        err_sys("buf1 write error");

    if (lseek(fd, 16384, SEEK_SET) == -1)
        err_sys("lseek error");

    if (write(fd, buf2, 10) != 10)
        err_sys("buf2 write error");

    close(fd);
    exit(0);
}複製代碼

而後編譯運行後生成了file.hole文件,使用od命令查看

> od -c file.hole
0000000    a   b   c   d   e   f   g   h   i   j  \0  \0  \0  \0  \0  \0
0000020   \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0040000    A   B   C   D   E   F   G   H   I   J
0040012複製代碼

很好換算,16384轉換爲8進制就是40000。順便說一句,od命令在查詢二進制文件的時候很是好用。

讀取寫入函數

ssize_t read(int fildes, void *buf, size_t nbyte);
ssize_t pread(int d, void *buf, size_t nbyte, off_t offset);
ssize_t readv(int d, const struct iovec *iov, int iovcnt);

ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);複製代碼

read/write函數族包含三個函數,可是實際上用到的就是read/write函數,其餘函數一之後介紹。

文件共享

在學習操做系統原理的時候,你們應該學習過鎖的使用。文件是一種資源,當一個進程打開文件的同時另外一個進程也持有了此文件的使用權,那麼很容易形成文件被覆蓋和誤讀。慶幸的是,操做系統已經爲咱們準備好了快捷安全的方式共享文件。
首先先講解一下Unix內核對文件的數據結構

  1. 每一個進程都自行維護了一個鏈表,裏面記錄了文件描述符(file descriptor)和文件指針的映射
  2. 內核爲全部打開的文件維護了一個文件表,注意,是全部打開文件,也就是說,一個文件被多個進程打開,就會出現多個文件表項,這是很是正常的。每一個文件表項包含了文件狀態標誌(讀、寫等等)文件偏移量文件系統邏輯指針
  3. 最後就是文件系統本身的邏輯指針

原子操做

可能對於使用過數據庫的朋友來講,原子操做已經聽過了。因爲操做系統是基於多任務操做的,內核有可能在執行任何代碼後掛起線程而後切換到另一個線程或者說是另一個進程的線程,因此說沒法保證後一個代碼執行時候前一行代碼執行結果是有效的,由於頗有可能被其餘線程改變了。原子操做就是這樣的一個方案,就如同數據庫中的事務,在提交事務以前,全部的資源都是被鎖定。或者說能保證相關的代碼執行不中斷。

正如open文件後lseek到文件末尾和直接用O_APPEND參數打開文件,二者之間的區別就是原子性和非原子性的區別。

在前面關於讀寫函數的時候介紹的preadpwirte就是一個原子操做,將lseekreadwirte函數合併。可是請注意,因爲這兩個函數並不是更新了文件偏移量而是自行加上了offset,因此內核中的文件偏移量是不會改變的。

前文open函數在同時使用O_CREATO_EXCL參數的時候也是一種原子操做,能在建立文件的時候就判斷文件是否已經存在。

複製文件描述符

int dup(int fildes);
int dup2(int fildes, int fildes2);複製代碼

dup執行後將會返回最小的可返回的文件描述符,dup2則是自定義文件描述符值,在手冊中有

In dup2(), the value of the new descriptor fildes2 is specified.  If fildes and fildes2 are equal, then dup2() just returns fildes2; no other changes are made to the existing descriptor.  Otherwise, if descriptor fildes2 is already in use, it is first deallocated as if a close(2) call had been done first.複製代碼

也就是若是fildes2正在使用則關閉後再分配;若是fildes等於fildes2則只返回fildes2,且不關閉。

數據同步到磁盤

爲了確保磁盤讀寫能高速有效,Unix系統在內核中設置了高速緩衝區,大多數狀況下,咱們都使用帶有緩衝的I/O函數,在某些狀況下,咱們須要馬上將緩衝區內數據寫入到磁盤,Unix系統提供了syncfsyncfdatasync三個函數,可是在FreeBSD系Unix實現不包含fdatasync函數,包括Mac OS X系統,具體詳細介紹能夠查看原著和Unix
系統手冊。

void sync(void);
int fsync(int fd);複製代碼

文件控制函數

int fcntl(int fildes, int cmd, ...);複製代碼

原書中列出了11中參數,實際上,現代Unix實現除了這11種參數意外還設置了其餘的參數,這裏再也不探討。

#include "include/apue.h"
#include <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main(int argc, char *argv[])
{
    int val;

    if (argc != 2)
        err_quit("usage: a.out <descriptor#>");

    if ((val = fcntl(atoi(argv[1]), F_GETFL)) < 0)
        err_sys("fcntl error for fd %d", atoi(argv[1]));

    switch (val & O_ACCMODE) {
        case O_RDONLY:
            printf("read only");
            break;
        case O_WRONLY:
            printf("write only");
            break;
        case O_RDWR:
            printf("read write");
            break;

        default:
            err_dump("unknown access mode");
    }

    if (val & O_APPEND)
        printf(", append");
    if (val & O_NONBLOCK)
        printf(", nonblocking");
    if (val & O_SYNC)
        printf(", synchronous writes");
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
    if (val & O_FSYNC)
        printf(", synchronous writes");
#endif

    putchar('\n');
    exit(0);
}複製代碼

代碼很是簡潔易懂,可能有些朋友對位運算的技巧不是很瞭解,因此看不懂一些代碼,例如,O_ACCMODE其實是一個掩碼值,它不表明實際意義,而是爲了可以快速運算取得每一位的具體數值,通常來講,二進制每一位都表明一個具體含義,當這一位是1的時候,表示這個開關打開,當爲0的時候開關關閉,而O_APPEND其實是(1000)b,O_NONBLOCK則是(100)b,都是各佔一位的,因此能夠用AND運算取得。
原著後面的兩個封裝的set_flclr_fl函數實際上也是跟位運算相關

val |= flags;
val &= ~flags;複製代碼

一個是或運算,一個是按位取反後進行和運算。都是很是實用的小技巧。

設備控制函數

int ioctl(int fildes, unsigned long request, ...);複製代碼

在Unix手冊中,ioctl函數被用於一些底層設備參數的設置和獲取,ioctl函數能夠控制一些特殊字符設備文件。可是實際上I/O操做不能雜類都是歸給這個函數,正如說明文件中說的,終端多是使用這個函數最多的地方,可是隨着標準推動,更多的終端操做函數被提出來用於替代ioctl,實際上不多用到這個函數。

文件描述符設備

在大多數的Unix實現中,都提供了/dev/fd文件夾,裏面有若干個文件,打開這些文件,等同於複製文件描述符,實際上因爲Linux系統和Unix系統不少不一樣的實現,在操做這個設備文件的時候須要很是當心,在實際開發中咱們有更好的方式來複制文件描述符,正如原著所說,/dev/fd文件夾更多的被使用在shell腳本中。

相關文章
相關標籤/搜索