fopen 不 fclose 是否會保存

前言

這個問題起源于我和同學的一次打賭纱控,在我過去的認知中文件用 fopen 打開后就一定要用 fclose 關(guān)閉期贫,否則將不能保存寫入的內(nèi)容,寫入的數(shù)據(jù)會存留在緩沖區(qū)中赞赖。但是經(jīng)過實際測驗后滚朵,不用 fclose 寫入的內(nèi)容也能夠保存... 痛失一瓶可樂...

在那時,我把它的原因歸結(jié)于是操作系統(tǒng)自己去保存的沒有再深究前域,今天看到一點別的東西辕近,突然想起來可能那時的想法是錯誤的,這可能是要歸功與編譯器匿垄,與操作系統(tǒng)無關(guān)移宅。

ps : 在這里,我只討論 linux 下 gcc 的情況

main 和 _start

可能很多人都不知道椿疗,我們的程序執(zhí)行的入口函數(shù)其實并不是 main 函數(shù)漏峰,而是從 _start 函數(shù)開始執(zhí)行的。

來我們測驗一下变丧,先寫一個簡單的程序

int main(void)
{
    return 0;
}

對的就這么簡單就可以了芽狗,編譯然后用 readelf 命令查看一下程序入口地址

gcc example.c -o test.o
readelf -h test.o

我們得到以下輸出

ELF 頭:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  類別:                              ELF64
  數(shù)據(jù):                              2 補碼绢掰,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  類型:                              DYN (共享目標文件)
  系統(tǒng)架構(gòu):                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口點地址:              0x1020
  程序頭起點:              64 (bytes into file)
  Start of section headers:          14576 (bytes into file)
  標志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         11
  Size of section headers:           64 (bytes)
  Number of section headers:         27
  Section header string table index: 26

重點看程序入口地址那一行為 0x1020

我們將編譯后的可執(zhí)行文件用 objdump 反匯編看看痒蓬,為了方便我將它輸出重定向到文件里面來看

objdump -d test.o > test.s

可以看到 0x1020 這里剛好就是 .text 段的開始也是 _start 函數(shù)的入口地地址

Disassembly of section .text:
0000000000001020 <_start>:                                                                                                       
1020:   f3 0f 1e fa                         endbr64
1024:   31 ed                                  xor    %ebp,%ebp
1026:   49 89 d1                            mov    %rdx,%r9
1029:   5e                                        pop    %rsi
102a:   48 89 e2                            mov    %rsp,%rdx
102d:   48 83 e4 f0                       and    $0xfffffffffffffff0,%rsp
1031:   50                                        push   %rax
1032:   54                                        push   %rsp
1033:   4c 8d 05 66 01 00 00    lea    0x166(%rip),%r8        # 11a0 <__libc_csu_fini>
103a:   48 8d 0d ef 00 00 00     lea    0xef(%rip),%rcx        # 1130 <__libc_csu_init>
1041:   48 8d 3d d1 00 00 00    lea    0xd1(%rip),%rdi        # 1119 <main>
1048:   ff 15 92 2f 00 00              callq  *0x2f92(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
104e:   f4                      hlt
104f:   90                      nop

現(xiàn)在看來,_start 是入口函數(shù)已經(jīng)是毋庸置疑了滴劲,問題是我們的 main 函數(shù)去哪里了攻晒?

__libc_start_main

在上面一段匯編代碼中,我們可以明顯地看到 _start 函數(shù)調(diào)用了一個 __libc_start_main 的函數(shù)并且將 main 函數(shù)的地址存到了 rdi 寄存器中班挖,答案八九就是在這里了鲁捏,但是這個函數(shù)是動態(tài)鏈接的,我反匯編后并沒有得到它的代碼...

1041:   48 8d 3d d1 00 00 00    lea    0xd1(%rip),%rdi        # 1119 <main> 0x1041 + 0xd1 剛好是 main 函數(shù)地址
1048:   ff 15 92 2f 00 00              callq  *0x2f92(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>

所以我又將它編譯了一下萧芙,不過這次用靜態(tài)鏈接给梅,不然看不到 __libc_start_main 的代碼假丧。

gcc example.c -o test.o -static

然后再用 objdump 反編譯一下,這次反編譯出來足足有 12 萬行的匯編...

objdump -d test.o > test.s

然后在反匯編文件里買年直接搜索 <__libc_start_main> 函數(shù)动羽,可以找到下面幾條關(guān)鍵代碼

##  具體流程是先將 rdi 寄存器中的 main 函數(shù)地址存放到 0x18(%rsp) 位置上包帚,再將地址給寄存器 rax 用 callq 調(diào)用
401f6a:   48 89 7c 24 18          mov    %rdi,0x18(%rsp)                                                                   
4023c9:   48 8b 44 24 18          mov    0x18(%rsp),%rax                                                                    
4023ce:   ff d0                   callq  *%rax
# 之后調(diào)用了將 main 函數(shù)的返回值給 edi 寄存器,調(diào)用 exit 函數(shù)
4023d0:   89 c7                   mov    %eax,%edi                                                                          
4023d2:   e8 29 5f 00 00          callq  408300 <exit>

可以看出 main 函數(shù)是在 __libc_start_main 函數(shù)中調(diào)用的运吓。

exit 和 _exit

可以看出編譯器在我們編譯過程中鏈接了很多其他的東西渴邦,這個和我們之前的問題有什么關(guān)系呢?之前的分析可以得到我們的代碼還鏈接了很多別的東西不僅有我們寫的拘哨,從上面的匯編代碼可以看出當我們調(diào)用完 main 函數(shù)后谋梭,__libc_start_main 函數(shù)會繼續(xù)調(diào)用 exit 函數(shù),而 exit 函數(shù)會關(guān)閉所有打開的流倦青,這將導致寫所有被緩沖的輸出瓮床,刪除用TMPFILE函數(shù)建立的所有臨時文件。至此我們前面的問題就解決了产镐,原因是調(diào)用了 exit 函數(shù)導致緩沖都輸出到文件里面了纤垂。簡而言之就是編譯器給我們的主程序加了個 exit 函數(shù)。下面是大致過程

_start:
    call __libc_start_main
_call__libc_start_main:
    call main
    call exit

說到 exit 就說一下 _exit 吧磷账。

其實 exit 函數(shù)就是對 _exit 函數(shù)的一個封裝峭沦,不過 exit 函數(shù)在調(diào)用 _exit 函數(shù)之前會調(diào)用終止程序(終止程序可以通過 atexit 函數(shù)注冊),清除 IO 緩沖逃糟。

_exit 函數(shù)做了三件事:

  • 關(guān)閉屬于該進程的所有文件描述符
  • 進程的任何子進程都由進程 init 繼承
  • 向進程的父節(jié)點發(fā)送 SIGCHLD 信號

如果我們將我們的程序這樣寫

#include <stdio.h>
int main(void)
{
    FILE *fp = fopen("test", "w");
    fwrite("123", 3, 1, fp);
    _exit(0);
}

則文件內(nèi)容不會保存吼鱼。

純凈的程序?

gcc 提供了一系列的參數(shù)供我們使用我們也可以用 nostartfiles 指定不鏈接我們之前分析的啟動例程

gcc test.c -e main -nostartfiles -o test.o

其中 -e 是用來指定程序入口的,由于我們現(xiàn)在不鏈接之前的啟動例程所以編譯器會找不到 _start 函數(shù)绰咽,我們必須自己指定一下入口菇肃。

現(xiàn)在可以用 objdump 反匯編看一下我們的程序,你可以看到尤為地簡潔取募,十分純凈

objdump -d test.o

也可以將我們程序里面主函數(shù)名字隨便換一下琐谤,換成 test_main,然后用 gcc 指定入口 test_main玩敏,這樣我們就創(chuàng)建了一個”沒有“主函數(shù)的程序但是可以運行的程序了斗忌。

gcc test.c -e test_main -nostartfiles

但是在程序運行結(jié)束時你應該會收到以下錯誤

[1]    10074 segmentation fault (core dumped)  ./a.out

出現(xiàn)這個錯誤是因為我們的程序不像之前我們有啟動例程那樣會調(diào)用 exit 正常退出,你可以自己在末尾加個 exit 或者 _exit 函數(shù)旺聚。

底層一點

fopen 函數(shù)底層調(diào)用open打開指定的文件织阳,返回一個文件描述符(就是一個int類型的編號),分配一個FILE結(jié)構(gòu)體砰粹,其中包含該文件的描述符唧躲、I/O緩沖區(qū)和當前讀寫位置等信息,返回這個FILE結(jié)構(gòu)體的地址。就是因為 FILE 結(jié)構(gòu)體這個緩沖區(qū)的存在我們才需要刷緩沖才能將文件寫入弄痹,fwrite fread 都是先看緩沖區(qū)是否滿或空才決定使用 writeread 的饭入。

之所以要使用緩沖區(qū),是因為每次 write read 都是一次系統(tǒng)調(diào)用要進入內(nèi)核肛真,調(diào)用一個系統(tǒng)調(diào)用比用戶調(diào)用要慢很多圣拄,在用戶區(qū)開辟緩沖區(qū)可以有效減少系統(tǒng)調(diào)用,提升性能毁欣。

open庇谆、readwrite凭疮、close 也稱無緩沖 IO饭耳,如果我們之前的寫入用 write 的話,即使不用 closeexit 它也會寫進文件里面去执解,不用刷緩沖寞肖。有緩沖這么好,那我們什么時候要用無緩沖 IO 呢衰腌?

通常我們讀寫設(shè)備時通常是不希望有緩沖的新蟆,例如向代表網(wǎng)絡(luò)設(shè)備的文件寫數(shù)據(jù)就是希望數(shù)據(jù)通過網(wǎng)絡(luò)設(shè)備發(fā)送出去,而不希望只寫到緩沖區(qū)里就算完事兒了右蕊,當網(wǎng)絡(luò)設(shè)備接收到數(shù)據(jù)時應用程序也希望第一時間被通知到琼稻,所以網(wǎng)絡(luò)編程通常直接調(diào)用Unbuffered I/O函數(shù)。

PS : 雖然 Unbuffered IO 函數(shù)在用戶區(qū)沒有緩沖區(qū)饶囚,但是內(nèi)核中會有 IO 緩沖區(qū)帕翻。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市萝风,隨后出現(xiàn)的幾起案子嘀掸,更是在濱河造成了極大的恐慌,老刑警劉巖规惰,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睬塌,死亡現(xiàn)場離奇詭異,居然都是意外死亡歇万,警方通過查閱死者的電腦和手機揩晴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堕花,“玉大人文狱,你說我怎么就攤上這事粥鞋≡低欤” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長壕曼。 經(jīng)常有香客問我苏研,道長,這世上最難降的妖魔是什么腮郊? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任摹蘑,我火速辦了婚禮,結(jié)果婚禮上轧飞,老公的妹妹穿的比我還像新娘衅鹿。我一直安慰自己,他們只是感情好过咬,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布大渤。 她就那樣靜靜地躺著,像睡著了一般掸绞。 火紅的嫁衣襯著肌膚如雪泵三。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天衔掸,我揣著相機與錄音烫幕,去河邊找鬼。 笑死敞映,一個胖子當著我的面吹牛较曼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播振愿,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼诗芜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了埃疫?” 一聲冷哼從身側(cè)響起伏恐,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栓霜,沒想到半個月后翠桦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡胳蛮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年销凑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仅炊。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡斗幼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抚垄,到底是詐尸還是另有隱情蜕窿,我是刑警寧澤谋逻,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站桐经,受9級特大地震影響毁兆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜阴挣,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一气堕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧畔咧,春花似錦茎芭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔽介,卻和暖如春摘投,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虹蓄。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工犀呼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人薇组。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓外臂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親律胀。 傳聞我的和親對象是個殘疾皇子宋光,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355