原文來自靜雅齋,轉載請註明出處。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_FILENO
、STDOUT_FILENO
、STDERR_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_EXEC
和O_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);複製代碼
其實把open
和creat
函數對比,能夠發現,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_SET
、SEEK_CUR
、SEEK_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內核對文件的數據結構
可能對於使用過數據庫的朋友來講,原子操做已經聽過了。因爲操做系統是基於多任務操做的,內核有可能在執行任何代碼後掛起線程而後切換到另一個線程或者說是另一個進程的線程,因此說沒法保證後一個代碼執行時候前一行代碼執行結果是有效的,由於頗有可能被其餘線程改變了。原子操做就是這樣的一個方案,就如同數據庫中的事務,在提交事務以前,全部的資源都是被鎖定。或者說能保證相關的代碼執行不中斷。
正如open
文件後lseek
到文件末尾和直接用O_APPEND
參數打開文件,二者之間的區別就是原子性和非原子性的區別。
在前面關於讀寫函數的時候介紹的pread
和pwirte
就是一個原子操做,將lseek
和read
、wirte
函數合併。可是請注意,因爲這兩個函數並不是更新了文件偏移量而是自行加上了offset,因此內核中的文件偏移量是不會改變的。
前文open函數在同時使用O_CREAT
和O_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系統提供了sync
、fsync
和fdatasync
三個函數,可是在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_fl
和clr_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腳本中。