Linux的基础IO内容补充-FILE

简介: 而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fwrite函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fwrite函数打印的数据就有两份。此时我们就可以知道,


目录

FILE当中的文件描述符
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的。
所以C库当中的FILE结构体内部必定封装了文件描述符fd。
对于 f 系列的函数底层其实都是调用了系统的接口。
首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。

typedef struct _IO_FILE FILE;
而我们在/usr/include/libio.h头文件中可以找到struct _IO_FILE结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno的成员,这个成员实际上就是封装的文件描述符。

struct _IO_FILE {
int _flags; / High-order word is _IO_MAGIC; rest is flags. /

define _IO_file_flags _flags

//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr;   /* Current read pointer */
char* _IO_read_end;   /* End of get area. */
char* _IO_read_base;  /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr;  /* Current put pointer. */
char* _IO_write_end;  /* End of put area. */
char* _IO_buf_base;   /* Start of reserve area. */
char* _IO_buf_end;    /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base;  /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno; //封装的文件描述符
AI 代码解读

if 0

int _blksize;
AI 代码解读

else

int _flags2;
AI 代码解读

endif

_IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
AI 代码解读

define __HAVE_COLUMN / temporary /

/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/*  char* _save_gptr;  char* _save_egptr; */

_IO_lock_t *_lock;
AI 代码解读

ifdef _IO_USE_OLD_IO_FILE

};
当然我们也可以通过代码验证一下,

代码展示:

include

include

include

include

include

include

int main()
{
FILE *fp = fopen("log.txt", "r");
if (fp == NULL)
{
perror("Error opening file");
}
else
{
int fd1 = fileno(fp); // 获取底层的文件描述符
printf("File descriptor: %d\n", fd1); // 打印文件描述符

}
fclose(fp);
return 0;
AI 代码解读

}
运行后,可以看到其fd1为3,也是符合我们刚才所说的预期结果。

fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。

那么我们研究一下下面的一段代码:

我们先不运行,我们先按照我们以前的知识,对这段代码进行解析。

我们先定义了三个字符串,然后分别调用printf,fwrite,write这三个函数,对于printf知识简单的打印输出到到显示器,fwrite是向fd为1的文件内写入一个mgs1,对于write也是如此。

但是我们刚才也说了,fwrite其实底层是调用了系统接口write。这是一点。

但我们在学习Linux的时候就听说过一句话,Linux下一切皆为文件,那么对于显示器,他对于Linux来说也是一个文件,那么对于printf本质就是向显示器文件内写内容,那么对于C语言的printf其实底层也是调用了write函数。

我们还在C语言的文件操作中听说过缓冲区的概念,其实我们向文件中写内容,其实一开始是向文件缓冲区内写内容,然后在适当的时候再刷新缓冲区,使得缓冲区的内容刷新到文件中。

代码的最后我们再fork,根据以前我们对fork的理解,那么fork出来的子进程它其实是不执行任何代码的,但是对于fork出来的子进程,它也会有着与父进程相同的数据与代码,那么父子进程的缓冲区内容会被 复制。但是有一点

父进程和子进程的缓冲区是独立的:
父子进程各自拥有缓冲区的副本,缓冲区中的数据状态(是否刷新、是否有未写入数据)也被复制。
这意味着父子进程的缓冲区互不影响。
根据上面的分析,我们可以得出一下结论:

printf,fwrite底层全部调用了write接口。
向文件中写内容,本质是向文件缓冲区内写内容,然后再适当的时候再刷新缓冲区,使得缓冲区的内容刷新到文件中。
父子进程的缓冲区内容会被 复制。但是父进程和子进程的缓冲区是独立的。

include

include

int main()
{
const char mgs0 = "hello printf\n";
const char
mgs1 = "hello fwrite\n";
const char *mgs2 = "hello write\n";

printf("%s",mgs0);
fwrite(mgs1, strlen(mgs1), 1, stdout);
write(1, mgs2, strlen(mgs2));

fork();

return 0;
AI 代码解读

}
那么我们运行,看看效果:

我们可以看到printf、fwrite和write函数都成功将对应内容输出到了显示器上。

但是,当我们将程序的结果重定向到log.txt文件当中后,我们发现文件当中的内容与我们直接打印输出到显示器的内容是不一样的。

那为什么C库函数打印的内容重定向到文件后就变成了两份,而系统接口打印的内容还是原来的一份呢?

再谈缓冲区
首先解释为什么之前我们要对缓冲区再有一个跟新的认识。

对于缓冲区的刷新方式一般分为三种,分别是:

无缓冲。
行缓冲。(常见的对显示器进行刷新数据)
全缓冲。(常见的对磁盘文件写入数据)
这里简单介绍一下三种缓冲刷新方式

  1. 无缓冲(Unbuffered)
    特点:
    数据不经过缓冲区,直接写入目标设备或文件。
    每次输出操作都会立即执行。
    通常用于对速度和性能要求不高,但对数据实时性要求高的场景(例如日志记录、错误信息输出)。
    适用场景:
    标准错误输出(stderr)通常是无缓冲模式。
  2. 行缓冲(Line Buffered)
    特点:
    缓冲区在遇到换行符(\n)时会自动刷新。
    缓冲区满或显式调用刷新函数(如fflush())也会触发刷新。
    常用于交互式输出,例如向终端打印信息。
    适用场景:
    标准输出(stdout)连接到终端时通常是行缓冲模式。
  3. 全缓冲(Fully Buffered)
    特点:
    数据先存入缓冲区,当缓冲区满时才写入目标设备或文件。
    缓冲区写入操作较少,因此效率更高。
    如果程序结束或显式调用刷新函数(如fflush()),缓冲区中的数据也会被写入。
    适用场景:
    对文件等非交互式设备进行写操作时通常采用全缓冲模式。
    当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。那么这时候父进程的缓冲区内也没有任何内容,当fork后的子进程的缓冲区内也没有任何数据。

而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fwrite函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fwrite函数打印的数据就有两份。但由于write函数是系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。

首先肯定的是这个缓冲区肯定不是系统内部提供的,如果是系统提供的,那么那么printf、fwrite和write函数打印的数据重定向到文件后都应该打印两次。但实际上write只是对应一次。

所以这个缓冲区其实就是C语言提供的缓冲区。

那么再联系我们开始说的 fwrite ,printf 函数底层其实是调用了write函数,那么再与刚才说的这个缓冲区是C语言提供的。此时我们就可以知道,实际上fwrite函数是先将数据放入C语言提供的缓冲区内,然后触发缓冲区刷新,最后再在适当的时候调用write。

我们常说printf是将数据打印到stdout里面,而stdout就是一个FILE*的指针,在FILE结构体当中还有一大部分成员是用于记录缓冲区相关的信息的。

//缓冲区相关
/ The following pointers correspond to the C++ streambuf protocol. /
/ Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. /
char _IO_read_ptr; / Current read pointer /
char
_IO_read_end; / End of get area. /
char _IO_read_base; / Start of putback+get area. /
char
_IO_write_base; / Start of put area. /
char _IO_write_ptr; / Current put pointer. /
char
_IO_write_end; / End of put area. /
char _IO_buf_base; / Start of reserve area. /
char
_IO_buf_end; / End of reserve area. /
/ The following fields are used to support backing up and undo. /
char _IO_save_base; / Pointer to start of non-current get area. /
char
_IO_backup_base; / Pointer to first valid character of backup area /
char _IO_save_end; / Pointer to end of non-current get area. */
也就是说,这里的缓冲区是由C语言提供,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。

我们向fd为1的写,那么我们所说的缓冲区就在stdout的FILE*指针内。

操作系统实际上也维护着自己的缓冲区。当用户程序中的缓冲区被刷新时,数据并不会直接写入磁盘或显示器,而是先被传递到操作系统的缓冲区。随后,操作系统会根据其自身的刷新机制,将这些数据写入磁盘或显示器。由于操作系统的刷新规则由系统管理,程序开发者通常无需过多关心其细节。

因为操作系统是进行软硬件资源管理的软件,根据下面的层状结构图,用户区的数据要刷新到具体外设必须经过操作系统。

那么对于上面的过程就可以用一个图进行简化

目录
相关文章
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
222 0
|
1月前
|
Linux的基础IO
那么,这里我们温习一下操作系统的概念我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。这就是因为在根本上操作系统确实像银行一样,并不完全信任用户程序,因为直接开放底层资源(如内存、磁盘、硬件访问权限)给用户程序会带来巨大的风险。所以就向银行一样他的服务是由工作人员隔着一层玻璃,然后对顾客进行服务的。
36 0
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
365 1
Linux C/C++之IO多路复用(aio)
(已解决)Linux环境—bash: wget: command not found; Docker pull报错Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled
(已成功解决)Linux环境报错—bash: wget: command not found;常见Linux发行版本,Linux中yum、rpm、apt-get、wget的区别;Docker pull报错Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled
2797 68
(已解决)Linux环境—bash: wget: command not found; Docker pull报错Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
223 34
|
7月前
|
Linux基础IO
Linux基础IO操作是系统管理和开发的基本技能。通过掌握文件描述符、重定向与管道、性能分析工具、文件系统操作以及网络IO命令等内容,可以更高效地进行系统操作和脚本编写。希望本文提供的知识和示例能帮助读者更深入地理解和运用Linux IO操作。
144 14
缓冲流和转换流的使用【 File类+IO流知识回顾③】
这篇文章介绍了Java中缓冲流(BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter)和转换流(InputStreamReader, OutputStreamWriter)的使用,包括它们的构造方法和如何利用它们提高IO操作的效率及处理字符编码问题。
缓冲流和转换流的使用【 File类+IO流知识回顾③】
序列化流 ObjectInputStream 和 ObjectOutputStream 的基本使用【 File类+IO流知识回顾④】
这篇文章介绍了Java中ObjectInputStream和ObjectOutputStream类的基本使用,这两个类用于实现对象的序列化和反序列化。文章解释了序列化的概念、如何通过实现Serializable接口来实现序列化,以及如何使用transient关键字标记不需要序列化的属性。接着,通过示例代码演示了如何使用ObjectOutputStream进行对象的序列化和ObjectInputStream进行反序列化。
序列化流 ObjectInputStream 和 ObjectOutputStream 的基本使用【 File类+IO流知识回顾④】
|
9月前
|
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
234 0
Linux C/C++之IO多路复用(poll,epoll)
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问