APUE_Chapter03_文件IO_筆記總結

            ***PAY A TRIBUTE TO W.Richard Stevens***

Chapter03: FILE I/O

3.1 簡介

read, write, lseek, close, open這五個函數就能覆蓋幾乎大部分的Unix上的IO. 我們這章主要探討的就是無緩存IO(unbuffered I/o), 相比於unbuffered IO就是第五章會討論到的緩存IO. unbuffered IO並不是ISO C的標準, 但是是POSIX.1和SUS的部分. 無緩存就是意味着每次read/write都是直接系統調用. 我們還會涉及到進程間操作的原子操作. 我們也會討論進程間文件是怎麼共享的,用到了什麼數據結構. 之後會討論dup, fcntl, sync, fsync和ioctl. 這些都是重點,開啓IO之旅吧~~

3.2 文件描述符

對於內核來說,所有打開的文件都會指向一個文件描述符. 當我們打開一個已經創建或者創建一個新的文件時,內核都會返回到相應的進程中一個文件描述符,當我們進行文件的read/write時,其中傳入的參數之一就是open/create生成的文件描述符.
在Unix中一般情況下來說,0表示標準輸入,1表示標準輸出,2表示標準錯誤. 這些並不是內核的特點而是很多應用程序使用的,但是很多應用還是經常打破這個規定的.
既然遵循POSIX.1規則,那麼這些magic就應該也被替換掉根據POSIX, 所以在unistd.h中定義了0是STDIN_FILENO, 1是STDOUT_FILENO, 2是STRERR_FILENO.
這裏寫圖片描述
那麼文件描述符能創建多少呢?這個值是0~OPEN_MAX-1. 在早期的Unix中最大隻能打開20個位每個進程,但是之後增加到了63個. 但是FreeBSD 8.0, Linux 3.2.0, MAC OS X10.6.8和Solaris10將這個設置成了能打開無數個了,具體數量是和內存,整型大小,以及一些系統管理員設置有關了.

3.3 open(2) openat(2)

這裏寫圖片描述
這倆函數後面的…這是ISO C的標準,表示參數的數量和類型是可變動的. 具體有什麼用處我們稍後討論. open函數的path表示的是打開或者要創建的文件的名字. oflag在fcntl.h中定義定宏這些值用”|”進行銜接,當然我們在manual page中也能看到:
這裏寫圖片描述
O_RDONLY: 以只讀模式打開文件
O_WRONLY: 以只寫的模式打開文件
O_RDWR: 以可讀寫的模式打開文件,大部分的實現爲了兼容舊實現都有O_RDONLY用0表示,O_WRONLY用1, O_RDWR用2.
O_EXEC: 以只能執行的模式打開文件
O_SEARCH: 以只能進行搜索的模式打開文件,這個一般用於文件是目錄的情況.這個主要是爲了檢測檢索目錄是否有權限當打開一個目錄時,
大部分的系統都沒有支持這個模式.
O_APPEND: 每次寫的時候都能夠添加到文件的末尾
O_CLOEXEC: 設置FD_CLOEXEC文件描述符標誌.
O_CREAT: 如果文件不存在就創建. 這個模式就需要有第三個參數了, 或者openat中的第四個參數–mode, 指定了文件的訪問權限. 我們會之
後討論如何通過umask來設置這個值.
O_DIRECTORY: 如果路徑不是指定到一個目錄的話,那麼就會返回錯誤.
O_EXCL: 如果文件已經存在了還指定了O_CREAT,那就報錯. 這個主要是爲了檢測是不是已經創建了文件了,如果沒有的話會原子操作創建
文件.
O_NOCTTY: 當路徑指定到了終端設備上,那麼久不要爲這個進程分配控制終端.
O_NOFOLLOW: 如果path指定到了一個符號鏈接,那麼就返回錯誤.
O_NONBLOCK: 如果path指定到了FIFO, 塊文件, 或者一個字符文件,這個選項則將他們進行IO操作時爲非阻塞狀態. 在release of System
V中用到的是O_NDELAY(no delay). 這個和O_NONBLOCK是很像的,但是O_NDELAY的返回值很操蛋,如果在pipe, fifo, 或者字符文件中
沒有讀到的數據,那麼就返回0, 但是當數據讀完後也是返回0. 所以在老版本的SVR中依然用的這個,新的也用了O_NONBLOCK.
O_TRUNC: 如果文件是存在的,並且有隻寫或者讀寫功能的時候,就將長度truncate成0.
O_TTY_INIT: 當打開一個沒有打開的終端設備的時候,設置termios結構體表示遵循SUS標準.
O_DSYNC: 每次write的時候也會等待物理IO的完成,但是如果不影響寫入的數據的讀的話,就不會更等待更新文件的屬性的. _DSYNC和
O_SYNC很像但是有着細微的不同, O_DSYNC更新文件的屬性只會在影響到文件數據的時候才做. 也就是說O_SYNC當寫入時,會同步
更新時間等屬性,但是O_DSYNC就不會了.
O_RSYNC: 在每次讀操作時會等待在這個文件相同部分的寫操作的完成. Linux中會將這個標誌位同O_SYNC一樣的功能. Mac OS不支持
O_RSYNC, 但是定義了O_DSYNC與O_SYNC一樣的功能. Solaris 支持上面三個. FreeBSD有O_FSYNC和O_SYNC一樣的功能, 但是不
支持O_DSYNC和O_RSYNC.
當我們使用open或openat函數時返回的文件描述符就是最低的文件描述符. 這樣的話,我們經常會有操作是將這個文件的結果作爲標準輸入或標準輸出,那麼我們就先關閉0或者1號描述符,然後open之後返回的文件描述符就是0或1. 這個案例經常會用在pipe操作中.
那麼我們說說openat函數和open的不同,openat多了個fd是什麼鬼,這裏有三點情況會用到fd:
1. 如果path參數是一個絕對路徑,那麼這個fd就廢了也就是說你設置啥玩意兒都沒用了,函數就相當於是open了.
2. 如果path是一個相對路徑,這時fd就是它所屬的目錄的文件描述符.
3. 如果path是一個相對路徑,並且fd是AT_FDCWD, 那麼表示以程序的當前路徑表示根目錄,也就是說path就是絕對路徑了.
openat函數是POSIX.1的後期加入的,加入是爲了解決兩個問題:
1. 給線程一種能夠以相對路徑的方式單開文件的方式, 因爲一個進程中的線程都是共享進程資源的,並且它們共享當前目錄,也就是進程目錄,這樣就很難讓同一進程中的多線程工作在不同的目錄下.
2. 防止TOCTTOU(time_of_check_to_time_of_use)問題, 簡單點說就是,TOCTTOU錯誤就是當我們有兩個基於文件的函數調用的時候,第二個函數需要第一個函數的結果,它們並不是原子操作,所以這樣,在他們間隙就可能發生結果篡改,導致真個程序崩潰. 這一般會用在找尋一個權限級別高的文件的安全漏洞處.
咱們再討論一下文件名超限制的問題,這是個歷史性的問題,對於不同的文件系統有着不同的標準. 一般這個宏是NAME_MAX, 一般是14個character, 但是如果超了(比如說15個character)怎麼處理?在SVR2中,會靜悄悄的將文件名截斷,也就是隻保留前14個character, 但是這就會產生很多問題, 因爲當我們用open, stat這些函數的時候,傳入的文件名就無法判斷初始是不是原始的文件名呢?但是在BSD驅動的系統上,就會直接返回錯誤,並且errno=ENAMETOOLONG. 於是我們的老大哥POSIX.1出馬了, 它用_POSIX_NO_TRUNC來決定你是截斷呢還是返回錯誤碼. 這個值對於不同的文件系統是會變化的,我們可以通過之前提到的fpathconfpathconf來查詢支持哪種. 這本身就是討論很久的問題,基於SVR4的文件系統S5就不會返回錯誤碼,但是基於BSD的文件系統UFS就會返回錯誤碼. BSD驅動的系統和Linux都會返回錯誤碼.
當_POSIX_NO_TRUNC生效的時候,errno=ENAMETOOLONG, 並且返回錯誤,當超過NAME_MAX. 但是現代的操作系統的限制都達到了255個character, 因爲一般情況下文件名都會小於這個值的,所以也是避免歷史性的糾結問題的發生.

3.4 creat(不是create)

這裏寫圖片描述
這個函數主要出現是因爲以前的open函數在文件沒有存在的情況下不能打開,所以就出下了creat函數,但是現在都creat就等價於:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
也就是現在很少用creat這個函數了.

3.5 close

這裏寫圖片描述
當我們調用close的時候,就釋放了進程在這個文件上的記錄鎖. 但是進程結束的時候,內核會幫我們將該進程中打開的fd全部關閉掉,所以基於這個特點,close也是用的並不是太多了.

3.6 lseek

這裏寫圖片描述
每當我們打開一個文件的時候,都會關聯一個當前文件的偏移量的值, read和write函數通常都是從當前文件的偏移量開始的,然後返回偏移量從當前到讀或者寫了的字節數. 通常情況下當我們打開一個文件的時候(write, open, read),當前文件偏移量都會設置成0. 除非open的時候O_APPEND設置進去.但是如果這個文件一直打開着,當我們寫一次,offset就會存儲爲SEEK_END的值,也就是說我們的讀取文件從哪裏讀就是根據這個offset來決定的,當我們調用lseek這個函數的時候內核就會將offset指定成這個函數的返回值.
然後我們說說單獨操作偏移量的函數lseek這個l表示是long integer的意思, 這個函數中的offset是取決於whence的:
如果whence=SEEK_SET, 那麼文件的offset就是從文件開始加上offset.
如果whence=SEEK_CUR,那麼文件的offset就是文件的當前offset加上offset,這裏的第二個參數可以正負.
如果whence=SEEK_END, 那麼文件的offset就是從文件的結尾offset加上offset,這裏的第二個參數可以正負.
由於lseek函數成功返回後是返回當前文件的偏移量,所以,我們可以用來用offset=0和SEEK_CUR來測試文件當前的位置在哪裏. 我們也能驗證這個fd能不能被seek. 比如說如果是pipe, FIFO, socket是不能被seek的. 它們會返回-1, 並且errno=ESPIPE.

#include <stdio.h>
#include <unistd.h>
int main(void)
{
    if(lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
    {
        printf("cannot seek\n");
    }
    else{
        printf("seek ok\n");
    }
    return 0;
}

這裏寫圖片描述
那有人會有疑問,如果whence設置爲SEEK_END, 然後offset是正整數,是什麼情況? 這就引出了文件空洞的概念,這在Unix系統上是允許的, 這樣的話,我們沒有寫入的字節都是0,當我們下次再寫入的時候,這個文件就進行了大小上的擴展,但是這些數據都會在磁盤上嗎?其實第一次偏移超出時是沒有佔用磁盤空間的,當我們寫入時纔會佔用磁盤,但是如果第一次寫入的文件結尾到第二次寫入的開頭有空隙的話,這部分是不會在磁盤分配空間的,這就是文件空洞現象. 這是合理的,還有一點這個查詢偏移量的記錄是直接又內核保存了的,並沒有進行任何的IO操作.
我們來測試一些文件空洞現象:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
#define FILE_MODE       (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
int main(void)
{
    int fd;
    if((fd = creat("file.hole", FILE_MODE)) < 0)
    {
        perror("creat error");
    }
    if(write(fd, buf1, 10) != 10)
    {
        perror("perror err");
    }
    /*offset now is 10*/
    if(lseek(fd, 16384, SEEK_SET) == -1)
    {
        perror("lseek error");
    }
    /*offset now is 16384*/
    if(write(fd, buf2, 10) != 10)
    {
        perror("buf2 write err");
    }
    /*offset now is 16394*/
    return 0;
}

這裏寫圖片描述
這裏寫圖片描述
針對上面說到的不調用lseek而是直接用兩次write會是覆蓋呢還是往後寫呢?如果理解了一開始那句話就能理解什麼個情況:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
#define FILE_MODE       (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
int main(void)
{
    int fd;
    if((fd = creat("file.hole", FILE_MODE)) < 0)
    {
        perror("creat error");
    }
    if(write(fd, buf1, 10) != 10)
    {
        perror("perror err");
    }
    if(write(fd, buf2, 10) != 10)
    {
        perror("buf2 write err");
    }
    return 0;
}

這裏寫圖片描述
想一想也是情理之中的,既然你文件描述符關了,內核咋還能給你存儲這個offset呢, 開玩笑,那存下多少了….

3.7 read

這裏寫圖片描述
如果成功返回,就返回讀到的字節數,如果讀到結尾就返回0. 但是有幾種情況是讀取到的值是異常的(這個是重點, 項目中肯定會碰到這樣的問題):
1. 如果我們期望讀到的文件結尾的時候還沒有達到nbytes的數量,那麼就直接返回讀到的字節數,這樣的話就會小於nbytes.
2. 當從終端設備進行讀取的時候,一般情況下都是一次讀取一行.
3. 當從網絡中讀取的時候, 緩存的情況可能造成讀取到的小於nbytes.
4. 當從pipe或者FIFO中進行讀取的時候, 如果pipe包含的字節數少於nbytes, 那麼就返回能獲得的字節數.
5. 當從記錄設備比如說從磁盤中讀取的時候,返回的數量就是一次記錄的數量.
6. 當被終端打斷或者有部分數據已經被讀取了的時候返回的值小於nbyte.
同樣的,read的讀取也是根據offset來作爲起始.
知道嗎, 上圖中的這個函數是由POSIX.1幾經改變後的,那麼原型是什麼樣呢?
int read(int fd, char *buf, unsigned nbytes);
我們能夠看到從char 變爲了void , 這主要是爲了與ISO C保持一致,因爲在ISO C中void *是定義通用指針的方式.
另一個返回值變成了ssize_t, 那麼ssize_t是什麼呢?我們通過找頭文件,我們能發現ssize_t: 在32位計算機系統中,ssize_t 是int型,佔4個字節,在64位計算機系統中,ssize_t是long int 型,佔8個字節. 那麼size_t就是相對應的signed整形了. 當然定義成ssize_t而不直接寫成signed int 就是因爲我們之提到的,爲了適應不同的平臺而定義的類型.
然後POSIX.1更新的方法要求返回值是正整數,ssize_t, 0是文件結束,-1是錯誤.

3.8 write

這裏寫圖片描述
一般write函數的返回值和nbytes是一樣的,如果不一樣就返回錯誤,發生錯誤一般就是磁盤滿了,或者超過了這個進程的限制. write函數也是從當前offset開始寫起,然後offset一點一點的移動根據寫了的字節數.

3.9 I/O效率

我們先碼個用read和write從終端讀寫的代碼:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#define BUFFSIZE 4096
int main(void)
{
    int n;
    char buf[BUFFSIZE];
    while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
    {
        if(write(STDOUT_FILENO, buf, n) != n)
        {
            perror("write err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    return 0;
}

這裏寫圖片描述
這裏我們就會有疑問,到底這個BUFFSIZE取多少合適的?
我們通過進行不同BUFFSIZE的測試可以得出這樣的結果:
這裏寫圖片描述
我測試的系統是ext4文件系統,ext4的文件系統的一個block是4096bytes, 所以說當BUFFSIZE設置成4096和更大時系統CPU時間能達到最小,但是即使大於4096的buffer也是對系統CPU時間影響不大了.
但部分的文件系統都會有個提前讀的性能提升的方式,也就是說內核會先讀取大於指定的nbytes的大小,也就是說如果發現是按順序的在讀這個buffer的時候,就多往後讀寫並且假設應用程序會很快讀到. 所以能看出來時鐘週期從32到更大的時候都是基本一致的.

3.10 File Sharing

這裏寫圖片描述
Unix是允許在不同的進程中共享打開的文件的,這就是我們即將引入的dup, 但是在引入這個函數之前,我們先介紹一下內核對IO支持的數據結構.
1. 每個進程在進程表中(entry)都有一個記錄項,在每個記錄向中都有一個打開的文件描述符表, 可以把它看成是一個向量, 每一個文件描述符佔用一項, 與每個文件描述符相關聯的是:
·文件描述符標誌(close_on_exec, 這個我們稍後講有些什麼)
·指向文件記錄向表的指針.
2. 內核爲每個打開的文件都維護一個文件記錄項表,在這個表中包含:
· 文件的狀態標誌(讀,寫, append, sync和非阻塞)
· 當前文件的偏移量
· 指向v-node的指針
3. 每個v-node中都存放着文件類型的信息和在這個文件上操作的函數的指針. 對於大部分的v-node都會有一個指針又指向i-node. i-node的信息就是用來當文件打開的時候從磁盤上讀取信息的,也就是獲取一些永久性的信息. 比如,i-node包含文件的所有者,文件的大小,指向實際中在磁盤上block位置的指針等等. 但是在linux上沒有v-node這個東西,但是有一個公用i-node, 其實這倆是一樣的,都會指向i-node.
打開的文件描述表可能會以鏈表的形式存儲而不會數組,但是不管怎麼樣總體概念是一樣的.
那爲什麼要引入v-node呢?主要是爲了支持同一個電腦系統上的不同文件系統. 這也就是由Bell實驗室和Sun的兩位大師獨立完成並命名爲VFS, 獨立的文件系統的i-node就叫做v-node. 在之後所有使用了Sun的NFS(Network file system)的代碼的產商都將這個v-node也加入了, BSD系統第一次加入v-node也是第一次加入NFS時候的4.3BSD.
在SVR4中也取締了SVR3中文件系統獨立的i-node用v-node. Solaris就是基於SVR4的。但是在linux中是用了獨立的文件系統的i-node和非獨立文件系統的i-node。
那麼當兩個獨立的進程打開了同一個文件時:
這裏寫圖片描述
從圖中可以看出,它們其實是從v-node纔開始一樣的,各自還維護者各自的文件記錄向表,這就是爲了爲不同的進程維護它們當前的偏移量.
這裏我們用我們之前的writelseek分析一波:
1. 當寫操作結束後, 當前文件的在file table entry裏的offset將增加寫入的字節數,如果當前offset超過了i-node裏的文件大小,那麼i-node中的文件大小就設置成文件當前的偏移量的值.
2. 如果一個文件用O_APPEND標誌打開的時候,相應的file table entry裏面的file status flags設置爲O_APPEND, 然後去獲取i-node中文件的大小並將offset設置成這個值.
3. 如果文件用lseek指向了文件的末尾,那麼就是從i-node取出size然後將offset設置成這個值(這個和O_APPEND是不一樣的,涉及到原子操作的問題,我們馬上討論)
4. 這樣我們就很明顯的看出來,lseek是沒有進行IO的.
那有沒有可能多個打開的fd記錄向指向同一個file table entry呢?當然是可能的,dup和子父進程就是這樣的,我們稍後討論.
我們還需要注意的一點是fd flags和file status flags是不同的,前者是針對的單個的進程中單個的打開的文件描述符,但是後者是可以作用於多個進程中所有的文件描述符的. 我們之後會引入fcntl函數,能夠實現更改這兩個變量的功能.
當多個進程同時寫入同一個文件的時候是怎麼避免衝突的呢?

3.11 原子操作

在古時候,是沒有O_APPEND這麼牛逼的方法的,那怎麼往最後面加呢?比如說A和B兩個進程,分別有着自己的file table entry,但是它們打開了同個文件,也就是v-node是一樣的,假設A調用了lseek,然後將A的offset設置成1500(也就是當前文件的末尾), B這時調用了write,將B的offset爲1600, 因爲當前的offset大於了i-node中的size,所以內核就將i-node中的size變爲1600,然後內核又切換到A, A開始寫的時候是從offset=1500開始寫的,這樣就覆蓋了B之前寫的.
問題就是出在了先定位然後再寫,這樣用兩個函數去完成,就很恐怖. 那麼只要將這兩個函數綁定成一個原子操作就可以了,那麼就是打開文件的時候設置O_APPEND, 這樣就能使得內核在每次寫操作之前,都將進程offset移動到文件末尾(也就是首先從i-node的size獲取)然後直接寫. 這樣就不存在說用lseek設置好後然後進程被搶走CPU資源,因爲單純的write這種函數是不會去i-node中去獲取當前的額長度的,而是無腦從offset中取,當資源回來的時候是執行write的時候,就直接從之前存下的offset中獲取值,這就是bug的問題所在.
規定製定者們考慮到這點後由SUS進行開發了一對讀寫方法:
這裏寫圖片描述
執行pread就相當於是是先執行了leek然後執行了read. 但是也是有些區別的:
1. 當我們使用pread的時候是沒有辦法中斷兩個過程的發生的.
2. 用pread或者pwrite並不會更新當前offset的.
當然在我們之前提到的open函數中也是定義了O_CREATO_EXCL也是爲了當我們創建文件的時候進行原子操作,因爲當我們創建文件的時候也是分步進行的,如果創建了那麼就報錯,如果沒有創建就創建,是有個先判斷的過程的. 具體分析過程和read,write類似,就不細說了.

duo和dup2

這裏寫圖片描述
dup函數會返回最小的沒有打開過fd, dup2中的fd2表示新打開的fd, 如果這個指定的fd已經打開了,那麼就先關閉,再返回這個新的描述符. 如果fd和fd2是一樣的話,那就返回fd2即使打開也不要關閉了. 如果不一樣,fd flag中的FD_CLOEXEC將清除,這樣的話即使進程調用exec,fd2也是會打開的. 那麼dup之後是什麼樣子的呢?
這裏寫圖片描述
我們來解析一下圖上的案例: 我們一開始有fd 1是打開的,然後我們調用dup(1)返回了3, 首先說明0, 1, 2都是打開的,3是目前能用的最小的fd, 這個時候,1和3都指向了同一個file table entry,也就是說說它們共享着同樣的file status flag, 比如說是讀寫O_APPEND等, 它們還共享文件的current file offset, v-node就更不用說了. 當然還有一種方式能進行fd的複製. 就是fcntl我們稍後會詳解.
dup(fd);=fcntl(fd, F_DUPFD, 0)
相似的:
dup2(fd, fd2);="close(fd2);fcntl(fd, F_DUPFD, fd2)". 但是這裏也稍微有些不同, 你應該也看出來了,fcntl這裏進行了兩步操作,這就涉及到原子操作的問題, 沒錯,dup2就是原子操作. 在close和fcntl直接是有可能被信號捕獲到的. 當然不同線程改變文件描述符也是可能發生的. 還有一點不同就是它們errno設置值不同.

3.13 sync, fsync, fdatasync

在我們Unix的大部分IO操作中, 都會經過內核中的高速緩衝buffer和頁緩存. 也就是是說當我們寫數據的時候,內核先拷貝到自己的buffer中,然後再扔到寫入隊列中之後寫入磁盤. 這就叫做延遲寫入. 那麼kernel什麼時候將延遲寫入的塊兒寫入到磁盤呢?Unix爲我們提供了三個函數來控制這一過程:
這裏寫圖片描述
sync這個函數就是簡單的將數據放到寫入隊列中後直接返回的,是不會等待磁盤寫入操作完成後才返回的. 這個函數呢通常在我們的系統中會被update這個守護進程每隔30秒調用一次, 這就是爲了保證內核block緩存區的刷新.
fsync呢函數中也能看出是針對於一個文件描述符的,並且它會等待磁盤寫入完成後才返回. 通常用在類似數據庫的操作上,保證數據寫入磁盤後返回.這個函數是會將文件的屬性也會更新同步的,如果發生了改變的話.
fdatasync區別於fsync的就是它只會更新數據部分,即使文件的屬性改變,也是不會進行改動的. 在FreeBSD8.0上是不支持fdatasync的.

3.14 fcntl

這裏寫圖片描述
fcntl的主要五大作用:
1. 複製一個存在的描述符(cmd=F_DUPFD或者F_DUPFD_CLOEXEC)
2. 獲取或者設置fd flags(cmd=F_GETFD或者F_SETFD)
3. 獲取或者設置文件狀態flag(cmd=F_GETFL或者F_SETFL)
4. 獲取或設置異步IO所有權(cmd=F_GETOWN或者F_SETOWN)
5. 獲取或設置記錄鎖(cmd=F_GETLK或者F_SETLK或者F_SETLKW)
我們就先說前八個,等以後介紹到記錄鎖的時候再續後三個關於記錄鎖的cmd:
F_DUPFD: 這個就是返回一個未打開的並且是當前最小的fd, 和之前一樣,他們共享了file table entry, 但是他們有着自己的fd flag,也就是說新的fd中的fd flag中的FD_CLOEXEC是清除了的, 這樣的話當執行exec函數後,fd還是打開的.
F_DUPFD_CLOEXEC: 同上一樣,只不過新的fd的fd flag的FD_CLOEXEC設置了. 注意有點系統支持並不是用FD_CLOEXEC, 而是用0表示don’t close-on-exec, 1表示do close-on-exec.
F_GETFD: 返回fd flag的值.
F_SETFD: 設置fd flag, 具體設置的值是根據第三個參數走.
F_GETFL: 對file tabele entry 中的file status flag的值進行獲取:
這裏寫圖片描述
這裏需要注意的是前5個標誌並不是都只佔1位,並且只能擁有這五個中的期中之一,所以必須通過O_ACCMODE來進行取得訪問方式位. 然後結果與這五個之一進行比較.
F_SETFL: 通過第三個參數進行設定,但是設定的值不能是前五個. 你想想我都打開文件描述符了,我怎麼能設置前五個那種值.
F_GETOWN: 獲取當前接收SIGIO和SIGURG信號的進程ID或者進程組ID.
F_SETOWN: 設置接收SIGIO和SIGURG信號的進程ID或進程組ID. arg>0表示是進程ID,arg<0表示絕對值是組ID.
我們用一段代碼來感受一下:

int main(int argc, char *argv[])
{
    int val;
    if(argc != 2)
    {
        perror("usage: fcntl <descriptor#>");
    }
    if((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
    {
        perror("fcntl err for fd");
    }
    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:
            perror("unknown access mode");
    }
    if(val & O_APPEND)
    {
        printf(", append");
    }
    if(val & O_NONBLOCK)
    {
        printf(", nonblock");
    }
    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');
    return 0;
}

我們首先說一下”_POSIX_C_SOURCE”是什麼鬼, 這個其實就是表示支持POSIX的C標準的宏,定義了這個宏的程序就能隨意使用POSIX C中的功能了. 具體不同C標準參考C標準簡介這篇文章. 然後我們進行結果輸出:
這裏寫圖片描述
我們怎麼理解這一段測試案例呢?
“>是一種只寫的重定向”
“<是一種只讀的重定向”
“>>是一種只寫,追加的重定向”
“<>是一種讀寫重定向”
./fcntl x是程序主體,fd是隸屬於某個進程的,x等於幾,就意味這要檢查fcntl這個進程的幾號文件描述符。當它的x號fd被重定向以後,fcntl去檢查x時,結果就會與重定向的方式有關。
但是當我們在設置不管是file table flag還是fd flag的時候,都不要直接調用F_SETFD或者是F_SETFL去設置, 因爲這樣的話很有可能將之前設置的標誌都沖掉了. 所以我們的做法就是需要調用GET來現獲取然後進行添加:

void set_fl(int fd, int flags)
{
    int val;
    if((val = fcntl(fd, F_GETFL, 0)) < 0)
    {
        perror("fcntl F_GETFL err");
    }
    val |= flags;//trun on flags
    if(fcntl(fd, F_SETFL, val) < 0)
    {
        perror("fcntl F_SETFL err");
    }
}

當我們想去除一個標誌時:
val &= ~flags
當我們調用的時候:
set_fl(STDOUT_FILENO, O_SYNC);我們來分析一下這個過程:
當我們1號文件描述符設置入O_SYNC後表示這個文件描述符表中的有了同步寫入的標誌,也就是說不僅僅是普通的write這種只是放入到queue中而不去等待寫入磁盤就返回,而是會等待寫入磁盤纔會返回. 這種情況我們也提到過,多用於數據庫的寫操作中. 但是對不不同的文件系統中,這個O_SYNC到底會不會生效呢?
這裏寫圖片描述
我們首先在Linux的ext4文件系統中看看, 首先分析第一行爲什麼要比第二行的system time低很多,是因爲第一行只是進行了讀操作,而第二行的寫操作一般都是會先從磁盤中讀取文件然後再寫入到另一個磁盤中. 但是我們期望的是有了O_SYNC標誌後system time要增加很多,但是從第三四五行來看,並不是這樣的. 這說明了Linux的ext4文件系統中並沒有給我們O_SYNC的效果,也就是說並不會返回錯誤,但是也不會給你生效. 我們在看看OS X的HFS文件系統:
這裏寫圖片描述
這就和我們所想的一樣了.
所以我們看出每一種調用的性能都由很多因素影響,操作系統的實現,磁盤驅動的速度,文件系統等.

ioctl

這個ioctl就是用戶與設備驅動打交道的方式,是通過IO來進行打交道的. 比如說終端I/O就是用這個函數用的最多的. 當我們搞驅動開發的時候,就會用到這個來允許用戶層對設備進行一些特殊需求的控制,但是我們在之後會用這個函數去控制終端設備的窗口大小等等這些需求,大家也可以看ioctl詳解這篇文章簡單的進行了解.


聯繫方式: [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章