歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> 《Linux系統編程》筆記 第三章(二)

《Linux系統編程》筆記 第三章(二)

日期:2017/3/1 11:45:20   编辑:關於Linux

3.6 定位流

標准庫提供了與系統調用lseek()類似的函數來定位流中的讀寫位置。

#include 
int fseek (FILE *stream, long offset, int whence);
long ftell(FILE *stream);

與lseek()用法類似,whence提供了如下選擇:
SEEK_CUR-將流的讀寫位置設置為當前位置加上pos個字節,pos可以是正數或負數。
SEEK_END-將流的讀寫位置設置為文件尾加上pos個字節,pos可以是正數或負數。
SEEK_SET-將流的讀寫位置設置為pos對應的位置,pos為0時代表設置為文件起始位置。
函數調用成功後返回0並取消之前的ungetc()操作,錯誤時返回-1。

上述的函數中偏移量類型是long,若處理的文件大小超過long變量的范圍時,可以使用

#include 
int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);

off_t在64位系統上被實現為64位大小,在32位系統上與long大小相同。

類似功能的函數有

#include 
int fsetpos (FILE *stream, fpos_t *pos);
int fgetpos(FILE *stream, fpos_t *pos);

除非為了源碼兼容性,一般不使用這個函數。

#include 
void rewind (FILE *stream);

該調用將stream的讀寫位置設置為流初始,與fseek (stream, 0, SEEK_SET);功能一致。由於該函數沒有返回值,因此需要驗證是否正確的話調用之前應該將errno置0,調用知乎檢查errno是否為0。

格式化I/O

格式化I/O是指將內容按照規定的格式整理後讀取或輸出。
格式化輸出主要通過printf()系列函數:

#include 
int printf(const char *format, ...);//格式化到標准輸出
int fprintf(FILE *stream, const char *format, ...);//格式化到流
int sprintf(char *str, const char *format, ...);//格式化到str中
int snprintf(char *str, size_t size, const char *format, ...);//與sprintf類似,更安全,其提供了可寫緩沖區的長度
int dprintf(int fd, const char *format, ...);//格式化到文件描述符fd對應的文件中

上述函數的返回值均是真正格式化的長度,不包括字符串結束符\0
我們一般見到的printf()調用是printf("%d", i);的形式,其實printf()系列函數的完整格式是:

% [flags] [fldwidth] [precision] [lenmodifier] convtype

%-是格式化字符串的起始,必須要有
flags-是控制格式化樣式的標志,有如下取值:

標志 說明 ‘ 將整數按千分位組字符 - 在字段內左對齊輸出 + 總是顯示帶符號轉換的正負號 (空格) 如果第一個字符不是正負號,則在其前面加上一個空格 # 指定另一種轉換形式(例如,對於十六進制格式,加0x前綴) 0 添加前導0(默認是空格)進行填充

fldwidth-控制被格式化內容的寬度,若寬度不夠則用空格或0補齊。可以指定非負數也可以指定*來默認處理
precision-數字的位數或字符串的字節數,以.開頭,後面跟非負數或*
lenmodifier-用來修飾被格式化變量的長度:

取值 說明 hh 將相應的參數按signed或unsigned char類型輸出 h 將相應的參數按signed或unsigned short類型輸出 l 將相應的參數按signed或unsigned long或寬字符類型輸出 ll 將相應的參數按signed或unsigned long long類型輸出 j intmax_t或uintmax_t z size_t t ptrdiff_t L long double

convtype-被格式化的變量類型:

取值 說明 d、i 有符號十進制 o 無符號八進制 u 無符號十進制 x,X 無符號十六進制 f, F 雙精度浮點數 e, E 指數格式雙精度浮點數 g, G 根據轉換後的值解釋為f、F、e或E a, A 十六進制指數格式雙進度浮點數 c 字符 s 字符串 p 指向void的指針 n 到目前為止,此printf調用輸出的字符的數目將被寫入到指針所指向的帶符號整型中 % 一個%字符 C 寬字符 S 寬字符串

下面是各個參數的效果:

#include 
int main(void)
{
   printf("%+0.1lf\n", 1.23456);    //+1.2
   printf("%+0.1lf\n", -1.23456);   //-1.2
   printf("%+8.2lf\n", -1.23456);   //   -1.23
   printf("%8.6d\n", 123);          //  000123
   return 0;
}

標准庫還提供了使用可變長參數的版本,功能與對應版本類似:

#include 
#include 
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
int vdprintf(int fd, const char *format, va_list ap);

格式化輸入用於分析字符串並轉換成對應類型變量保存起來,主要通過scanf()系列函數:

#include 
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

完整的參數為:

%[*] [fldwidth] [m] [lenmodifier] convtype

fldwidth-最大字符寬度
m-當要輸入的是字符串時,該參數指定提供的緩沖區的大小
lenmodifier-轉換後要賦值的參數大小
convtype-要轉化的參數類型,與printf()系列函數級別一致。當該標志代表無符號變量且輸入的數據是負數時,將轉換為二進制相同的正數,例如-1轉為4294967295。
標准庫同樣提供了變長參數的版本,不再贅述。

3.7 清洗一個流

在向一個流寫入數據後數據並沒有真正交給內核,而是在用戶空間的緩沖區內保存,等待數據累積到合適大小後再請求內核。標准庫提供了立即將緩沖區數據提交內核的函數。

#include 
int fflush (FILE *stream);

該函數調用後,stream中的數據會被flush到內核緩沖區,此時與直接調用write()的效果是一樣的。如果需要確保數據被提交給硬盤,需要使用fysnc()或相同功能的系統調用。一般fflush()後都要調用fsync()來確保數據從用戶緩沖區到內核緩沖區再到硬盤。

3.8 錯誤和文件結束

fread()函數的返回值不能區分發生錯誤還是遇到了EOF,標准庫提供了錯誤檢查函數:

#include 
int ferror (FILE *stream);

用於檢測stream上是否有錯誤標志。錯誤標志由標准I/O相關函數設置,如果存在錯誤標志,該函數返回非0值,否則返回0。

#include 
int feof (FILE *stream);

用來檢測stream是否到了文件結尾。若到文件結尾,返回非0,否則返回0。

#include 
void clearerr (FILE *stream);

用於清理stream的errno和EOF標志。

3.9 獲得關聯的文件描述符

與fdopen()相對,fileno()用於獲取與流關聯的文件描述符。但是不建議讀寫文件時將文件描述符和流混用。

#include 
int fileno (FILE *stream);

失敗時返回-1並設置errno。

3.10 控制緩沖

標准I/O庫提供了三種緩沖類型,分別為:
不緩沖
不執行用戶空間緩沖,數據直接提交給內核。這種情況下使用標准I/O沒有什麼優勢。標准錯誤就是這種緩沖模式。
行緩沖
遇到換行符時將緩沖區提交到內核。標准輸出是這種緩沖模式,也叫全緩沖。
塊緩沖
默認的緩沖模式,緩沖效果最好。

#include 
int setvbuf (FILE *stream, char *buf, int mode, size_t size);

控制緩沖類型,mode可能是:
_IONBF-不緩沖
_IOLBF-行緩沖
_IOFBF-塊緩沖

在_IONBF模式下,buf和size參數被忽略。其他模式下標准I/O會使用buf作為緩沖區,其大小是size。當buf是NULL時,緩沖區被自動分配。默認的緩沖區大小為BUFSIZ宏定義的,是塊大小的整數倍。setbuf()必須在打開流後,做任何其他操作之前被調用,失敗時返回非0並設置errno。
還要注意緩沖區是局部變量時,一定要在局部變量失效前關閉流,錯誤的使用例如

#include 
int main(void)
{
   char buf[BUFSIZ];
   setbuf(stdin, buf);
   printf("Hello, world!\n");
   return 0;
}

內存流

標准I/O庫提供了fmemopen()函數來打開位於內存的流,而不與底層文件相關聯,其用用戶指定的緩沖區單做文件讀寫的位置,返回一個FILE*。

#include 
FILE *fmemopen(void *buf, size_t size, const char *mode);

buf-緩沖區的起始地址,若該參數是NULL,庫函數會幫助分配一個size大小的緩沖區,在關閉流的時候被釋放。
size-緩沖區大小。
mode-讀寫模式,與fopen()參數類似。
注意事項:
1 當以追加方式打開內存流時,當前的文件讀寫位置是緩沖區中的第一個字符串結束符位置(‘\0’)。緩沖區中無字符串結束符時,文件位置是緩沖區結尾的後一個字節。
2 當內存流不是以追加方式打開時,當前文件位置是緩沖區開始的位置
3 buf是NULL,以只讀或只寫方式打開內存流沒有意義。因為我們沒辦法知道分配的緩沖區的地址,因此只能讀取我們無法寫入的數據或者寫入我們無法讀取的數據
4 增加內存流中數據或者調用fclose()、fflush()、fseek()、fseeko()和fsetpos()時都會在當前位置增加一個字符串結束符
下面代碼測試上述內容:

#include 
#include 
#include 
using namespace std;
int main(void)
{
    //============
    //追加模式下文件位置是第一個'\0'處,非追加模式下是緩沖區開始位置
    FILE* fp =NULL;
    char buffer[256] = "this is a buffer.";
    fp = fmemopen(buffer, 256, "r+");
    cout << ftell(fp) << endl;//0
    fclose(fp);

    fp = fmemopen(buffer, 256, "a+");
    cout << ftell(fp) << " " << strlen(buffer) << endl;//17 17
    fclose(fp);
    //============

    //============緩沖區內容增加,會自動寫入'\0'
    fp = fmemopen(buffer, 256, "w+");
    fputc('a', fp);
    fflush(fp);
    cout << buffer << endl;//a
    cout << &buffer[2] << endl;//is a buffer.  因為'th'變成了a'\0'
    fclose(fp);
    //============
    return 0;
}

由於內存流依賴字符串結束符,因此以二進制的形式讀寫文件流並不合適,因為二進制數據中’\0’出現的位置有可能是一條數據的中間而不是結尾,使用內存流來讀寫二進制數據很可能會破壞數據。
類似的函數還有

#include 
FILE *open_memstream(char **ptr, size_t *sizeloc);//對char類型的字符串做操作
#include 
FILE *open_wmemstream(wchar_t **ptr, size_t *sizeloc);//對寬字節的字符串做操作

與fmemopen()區別在於:
* 創建的流無法指定讀寫模式,只能寫打開
* 不能自行指定緩沖區,函數返回時指針指向標准庫分配的緩沖區。由於緩沖區可能會被重新分配(例如一開始分配的緩沖區不夠擴展了),因此*ptr指向的地址可能會變
* 關閉流後需要自行釋放緩沖區
* 緩沖區會隨著流數據的增多而變大,每次fflush()或fclose()後sizeloc指向的值可能會被改變

內存流的作用

最直觀的作用是其提供了一個處於內存中的文件指針,使我們可以像讀寫文件一樣操作一塊內存,方便的使用標准I/O提供的函數調用而沒有真正讀寫文件的性能損失。在一些特殊的第三方API中,可能需要一個FILE*類型的參數,但是此時都在內存中,這時將內存寫入文件再傳到第三方API中顯然是不劃算的,因此可以將對應的內存映射為打開的文件。另外對於open_memstream()相關的函數來說,其內部管理了緩沖區,使我們不需要擔心緩沖區溢出的問題,此時可以方便的格式化或者拼接字符串,例如格式化一段sql語句等。

3.11 線程安全

多線程程序中線程共享進程的資源,因此需要對共享資源做線程同步操作,避免產生非預期的結構,標准I/O默認是線程安全的(即在多線程並發讀寫同一個文件時,讀寫操作在任意時刻只能運行一個,且上一個請求結束前不會被其他讀寫請求搶占CPU)。
下面的代碼驗證線程安全:

//編譯時要-lpthread,鏈接pthread庫
#include 
#include 
#include 
#include 
void *thrd_func(void *arg);
FILE *stream;
int main()
{
    pthread_t tid;
    int *thread_ret = NULL;
    stream = fopen("1.txt", "a");
    if (!stream)
    {
        perror("fopen");
        return -1;
    }
    if (pthread_create(&tid,NULL,thrd_func,NULL)!=0)//創建一個線程,使其與主線程同時向一個流輸出內容。
    {
        printf("Create thread error!\n");
        exit(1);
    }

    for(int i = 0; i<10000; ++i)
    {
       if (fputs("This is a test line,it should not be broken1\r\n", stream) == EOF)
        {
            printf("err!");
        }
    }
    pthread_join(tid, (void**)&thread_ret );
    return 0;
}

void *thrd_func(void *arg)
{
    for(int i = 0; i<10000; ++i)
    {
       if (fputs("This is a test line,it should not be broken2\r\n", stream) == EOF)
        {
            printf("err!");
        }
    }
    pthread_exit(NULL);
}

在多核的主機上運行上述代碼,可以看到”This is a test line,it should not be broken1”和”This is a test line,it should not be broken2”交替出現,但是每行都是完整的。交替出現的原因是不同的CPU核心會同時向文件寫入字符串。
上面的代碼也暴露了一些問題,假如現在有一個多線程的服務器,要在日志中打印出來內部一個map中的數據,那麼在實際情況中很可能打印出的內容被其他線程的日志輸出穿插,讀日志時帶來一些困難,這就需要線程同步來進行,標准I/O庫提供了針對流的加鎖功能。

3.11.1 手動文件加鎖

用flockfile()給對應流加鎖,用funlockfile()解鎖。

#include 
void flockfile (FILE *stream);
void funlockfile (FILE *stream);

標准I/O庫中的鎖是遞歸鎖(可重入鎖),即一個線程可以多次獲得該鎖而不被鎖死或斷言錯誤。該鎖使用計數,當flockfile()時,計數器+1;funlockfile()時,計數器-1,因此調用funlockfile()的次數一定要與flockfile()次數一致,尤其是錯誤處理提前返回時更要小心。當計數器為0時,代表線程不再保持鎖,此時其他線程對同一個流加鎖的話能夠無阻塞的獲得鎖。
當第一次加鎖成功時flockfile()返回0,當前線程獲得鎖;已經獲得鎖時本線程再次調用flockfile(),返回非0。
在輸出map元素之前加鎖,輸出完成後釋放鎖,這樣就可以保證map的數據在一起而不被其他信息穿插了。

3.11.2 不加鎖流操作

既然開發人員選擇手動控制鎖的范圍,那麼就沒必要在讀寫文件時再次加鎖了。標准庫提供了一系列不加鎖的庫函數。

#define _GNU_SOURCE
#include 
int fgetc_unlocked (FILE *stream);
char *fgets_unlocked (char *str, int size, FILE *stream);
size_t fread_unlocked (void *buf, size_t size, size_t nr,FILE *stream);
int fputc_unlocked (int c, FILE *stream);
int fputs_unlocked (const char *str, FILE *stream);
size_t fwrite_unlocked (void *buf, size_t size, size_t nr, FILE *stream);
int fflush_unlocked (FILE *stream);
int feof_unlocked (FILE *stream);
int ferror_unlocked (FILE *stream);
int fileno_unlocked (FILE *stream);
void clearerr_unlocked (FILE *stream);

這些函數除了不再加鎖外,行為與加鎖版本一致。
感興趣的同學可以做一下小練習,將之前校驗標准I/O線程安全的代碼改用非加鎖的調用,試試看輸出文件有什麼變化。
另外還可以用系統調用write()來測試一下write()是否是線程安全的(事實上系統調用基本都是原子操作,即線程安全的,但是write()比較特殊,其內部是兩個調用:定位和寫入。不使用O_APPEND模式的話,可能會因為偏移量沒有增加而導致寫入內容被覆蓋,這裡有資料,因此在多線程讀寫時,如果不打算自己做線程同步的話,使用系統調用write()時一定要加上O_APPEND標志)

3.12 對標准I/O的批評

首先需要明確的是標准I/O提供了非常方便的用戶空間緩沖機制,使開發人員無需關注系統的塊大小而提高文件I/O效率;其次庫函數提供了便利的操作,能夠按行讀寫文本;另外由於是標准庫,其代碼可移植性非常高,使用也廣泛。
但標准I/O庫也有一些缺點,其中一個就是雙副本問題。雙副本問題是指,標准I/O庫內部維護了一個緩沖區,從內核讀取到的數據拷貝到該緩沖區中維護,在用戶需要數據時,要再次拷貝到用戶指定的地址中:一段數據在用戶空間中有兩個副本,同時也有兩次拷貝操作,寫入時也是類似情況。
對於讀取操作,一個改善方式是返回一個指向標准I/O緩沖區的指針,用戶程序只有在修改讀取內容或在緩沖區被清空之前拷貝數據即可。另外setvbuf()函數設置的用戶緩沖區是不是也能減少一次拷貝?
對於寫操作,可以使用[分散輸入和集中輸出]的I/O模式,見後面章節內容。
另一些函數庫也提供了相關解決方案,例如快速I/O庫(fio、sfio)、映射文件(mmap函數)。

Copyright © Linux教程網 All Rights Reserved