fopen 不 fclose 是否会保存

前言

这个问题起源于我和同学的一次打赌,在我过去的认知中文件用 fopen 打开后就一定要用 fclose 关闭,否则将不能保存写入的内容,写入的数据会存留在缓冲区中。但是经过实际测验后,不用 fclose 写入的内容也能够保存... 痛失一瓶可乐...

在那时,我把它的原因归结于是操作系统自己去保存的没有再深究,今天看到一点别的东西,突然想起来可能那时的想法是错误的,这可能是要归功与编译器,与操作系统无关。

ps : 在这里,我只讨论 linux 下 gcc 的情况

main 和 _start

可能很多人都不知道,我们的程序执行的入口函数其实并不是 main 函数,而是从 _start 函数开始执行的。

来我们测验一下,先写一个简单的程序

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
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              DYN (共享目标文件)
  系统架构:                          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

我们将编译后的可执行文件用 objdump 反汇编看看,为了方便我将它输出重定向到文件里面来看

objdump -d test.o > test.s

可以看到 0x1020 这里刚好就是 .text 段的开始也是 _start 函数的入口地地址

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

现在看来,_start 是入口函数已经是毋庸置疑了,问题是我们的 main 函数去哪里了?

__libc_start_main

在上面一段汇编代码中,我们可以明显地看到 _start 函数调用了一个 __libc_start_main 的函数并且将 main 函数的地址存到了 rdi 寄存器中,答案八九就是在这里了,但是这个函数是动态链接的,我反汇编后并没有得到它的代码...

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

所以我又将它编译了一下,不过这次用静态链接,不然看不到 __libc_start_main 的代码。

gcc example.c -o test.o -static

然后再用 objdump 反编译一下,这次反编译出来足足有 12 万行的汇编...

objdump -d test.o > test.s

然后在反汇编文件里买年直接搜索 <__libc_start_main> 函数,可以找到下面几条关键代码

##  具体流程是先将 rdi 寄存器中的 main 函数地址存放到 0x18(%rsp) 位置上,再将地址给寄存器 rax 用 callq 调用
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
# 之后调用了将 main 函数的返回值给 edi 寄存器,调用 exit 函数
4023d0:   89 c7                   mov    %eax,%edi                                                                          
4023d2:   e8 29 5f 00 00          callq  408300 <exit>

可以看出 main 函数是在 __libc_start_main 函数中调用的。

exit 和 _exit

可以看出编译器在我们编译过程中链接了很多其他的东西,这个和我们之前的问题有什么关系呢?之前的分析可以得到我们的代码还链接了很多别的东西不仅有我们写的,从上面的汇编代码可以看出当我们调用完 main 函数后,__libc_start_main 函数会继续调用 exit 函数,而 exit 函数会关闭所有打开的流,这将导致写所有被缓冲的输出,删除用TMPFILE函数建立的所有临时文件。至此我们前面的问题就解决了,原因是调用了 exit 函数导致缓冲都输出到文件里面了。简而言之就是编译器给我们的主程序加了个 exit 函数。下面是大致过程

_start:
    call __libc_start_main
_call__libc_start_main:
    call main
    call exit

说到 exit 就说一下 _exit 吧。

其实 exit 函数就是对 _exit 函数的一个封装,不过 exit 函数在调用 _exit 函数之前会调用终止程序(终止程序可以通过 atexit 函数注册),清除 IO 缓冲。

_exit 函数做了三件事:

  • 关闭属于该进程的所有文件描述符
  • 进程的任何子进程都由进程 init 继承
  • 向进程的父节点发送 SIGCHLD 信号

如果我们将我们的程序这样写

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

则文件内容不会保存。

纯净的程序?

gcc 提供了一系列的参数供我们使用我们也可以用 nostartfiles 指定不链接我们之前分析的启动例程

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

其中 -e 是用来指定程序入口的,由于我们现在不链接之前的启动例程所以编译器会找不到 _start 函数,我们必须自己指定一下入口。

现在可以用 objdump 反汇编看一下我们的程序,你可以看到尤为地简洁,十分纯净

objdump -d test.o

也可以将我们程序里面主函数名字随便换一下,换成 test_main,然后用 gcc 指定入口 test_main,这样我们就创建了一个”没有“主函数的程序但是可以运行的程序了。

gcc test.c -e test_main -nostartfiles

但是在程序运行结束时你应该会收到以下错误

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

出现这个错误是因为我们的程序不像之前我们有启动例程那样会调用 exit 正常退出,你可以自己在末尾加个 exit 或者 _exit 函数。

底层一点

fopen 函数底层调用open打开指定的文件,返回一个文件描述符(就是一个int类型的编号),分配一个FILE结构体,其中包含该文件的描述符、I/O缓冲区和当前读写位置等信息,返回这个FILE结构体的地址。就是因为 FILE 结构体这个缓冲区的存在我们才需要刷缓冲才能将文件写入,fwrite fread 都是先看缓冲区是否满或空才决定使用 writeread 的。

之所以要使用缓冲区,是因为每次 write read 都是一次系统调用要进入内核,调用一个系统调用比用户调用要慢很多,在用户区开辟缓冲区可以有效减少系统调用,提升性能。

openreadwriteclose 也称无缓冲 IO,如果我们之前的写入用 write 的话,即使不用 closeexit 它也会写进文件里面去,不用刷缓冲。有缓冲这么好,那我们什么时候要用无缓冲 IO 呢?

通常我们读写设备时通常是不希望有缓冲的,例如向代表网络设备的文件写数据就是希望数据通过网络设备发送出去,而不希望只写到缓冲区里就算完事儿了,当网络设备接收到数据时应用程序也希望第一时间被通知到,所以网络编程通常直接调用Unbuffered I/O函数。

PS : 虽然 Unbuffered IO 函数在用户区没有缓冲区,但是内核中会有 IO 缓冲区。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336