Linux基础IO

1.理解文件

背景补充

a.文件 = 属性 + 内容

b.访问一个文件都必须先把文件打开(加载到内存中)

c.如果一个文件未被打开,那他一定在外设例如磁盘中

d.linux是通过bash,启动进程通过操作系统调用系统调用打开文件

e.OS中,一定存在大量被打开的文件,所以操作系统需要对打开的文件进行管理

f.所以存在一种数据结构体来描述被打开的文件

1.1狭义理解

• ⽂件在磁盘⾥

• 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的

• 磁盘是外设(即是输出设备也是输⼊设备)

• 磁盘上的⽂件本质是对⽂件的所有操作,都是对外设的输⼊和输出简称IO

1.2⼴义理解

• Linux下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘......这些都是抽象化的过程)(后⾯会讲如何去

理解)

1.3⽂件操作的归类认知

• 对于0KB的空⽂件是占⽤磁盘空间的

• ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件=属性(元数据)+内容)

• 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作

1.4系统⻆度

• 对⽂件的操作本质是进程对⽂件的操作

• 磁盘的管理者是操作系统

2.C语言文件接口汇总

2-1hello.c打开⽂件

文件打开函数 - fopen

函数介绍

c 复制代码
FILE *fopen( const char *filename, const char *mode );

该函数的功能就是打开一个文件,函数的第一个参数是你要打开的文件的文件名,第二个参数是打开这个文件的形式。

我们知道打开一个文件时,系统会为该文件创建一个文件信息区,该函数调用完毕后,如果打开该文件成功,那么返回指向该文件信息区的指针(FILE*类型);如果打开文件失败,那么返回一个空指针(NULL)。

文件关闭函数 - fclose

与动态开辟内存空间时一样,当我们打开文件时,会在内存中开辟一块空间,如果我们打开该文件后不关闭,那么这个空间会一直存在,一直占用那块内存空间,所以当我们对一个文件的操作结束时,一定要记住将该文件关闭。这就需要用到fclose函数来关闭文件。

c 复制代码
int fclose( FILE *stream );

文件的打开形式

模式 含义
"r" 只能读取 文件必须存在
"w" 只能写入 文件不存在则创建,存在则清空
"a" 只能写入 文件不存在则创建,存在则在末尾追加
"r+" 可读写 文件必须存在
"w+" 可读写 文件不存在则创建,存在则清空
"a+" 可读写 文件不存在则创建,写入时在末尾追加

打开文件

cpp 复制代码
#include <stdio.h>
 int main()
{
  FILE *fp = fopen("myfile", "w");
  if(!fp)
  {
   printf("fopen error!\n");
  }

 fclose(fp);
 return 0;
}

打开的myfile⽂件在哪个路径下?

• 在程序的当前路径下,那系统怎么知道程序的当前路径在哪⾥呢?

可以使⽤ ls /proc/[进程id] -l 命令查看当前正在运⾏进程的信息:

bash 复制代码
[lzy@VM-0-14-centos linux_test]$ ps ajx | grep myfile
23676 30730 30729 22886 pts/1    30729 R+    1001   0:00 grep --color=auto myfile
[lzy@VM-0-14-centos linux_test]$ ps ajx | grep test0206
20212 30620 30620 20024 pts/0    30620 R+    1001   1:13 ./test0206
23676 30976 30975 22886 pts/1    30975 R+    1001   0:00 grep --color=auto test0206
[lzy@VM-0-14-centos linux_test]$ ls /proc/30620 -l
total 0
dr-xr-xr-x 2 lzy lzy 0 Feb  6 19:11 attr
-rw-r--r-- 1 lzy lzy 0 Feb  6 19:11 autogroup
-r-------- 1 lzy lzy 0 Feb  6 19:11 auxv
-r--r--r-- 1 lzy lzy 0 Feb  6 19:11 cgroup
--w------- 1 lzy lzy 0 Feb  6 19:11 clear_refs
-r--r--r-- 1 lzy lzy 0 Feb  6 19:09 cmdline
-rw-r--r-- 1 lzy lzy 0 Feb  6 19:11 comm
-rw-r--r-- 1 lzy lzy 0 Feb  6 19:11 coredump_filter
-r--r--r-- 1 lzy lzy 0 Feb  6 19:11 cpuset
lrwxrwxrwx 1 lzy lzy 0 Feb  6 19:09 cwd -> /home/lzy/linuxtest/linux_test
-r-------- 1 lzy lzy 0 Feb  6 19:09 environ
lrwxrwxrwx 1 lzy lzy 0 Feb  6 19:09 exe -> /home/lzy/linuxtest/linux_test/test0206
dr-x------ 2 lzy lzy 0 Feb  6 19:09 fd
dr-x------ 2 lzy lzy 0 Feb  6 19:11 fdinfo

其中:

• cwd:指向当前进程运⾏⽬录的⼀个符号链接。

• exe:指向启动当前进程的可执⾏⽂件(完整路径)的符号链接。

打开⽂件,本质是进程打开,所以,进程知道⾃⼰在哪⾥,即便⽂件不带路径,进程也知道。由此OS

就能知道要创建的⽂件放在哪⾥。

所以打开的myfile⽂件在 /home/lzy/linuxtest/linux_test/myfile

2.2hello.c写⽂件
fwrite函数

c 复制代码
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

概括一下,fwrite函数的功能就是将buffer位置的,每个元素大小为size的,count个元素,以二进制的形式输出到stream位置

cpp 复制代码
#incluide<stdio.h>
#include <string.h>
int main()
{
 FILE * fp  = fopen("myfile","w");
 if(!fp)
 {
 printf("fopen error\n");
 }
 const char *arg = "hello linux\n";
 int count = 5;
 while(count--)
 {
 fwrite(arg,strlen(arg),1,fp);
 }
 fclose(fp);
 return 0;
 }
 

2.3hello.c读⽂件

fread函数

c 复制代码
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );

fread函数的第一个参数是接收数据的位置,第二个参数是要读取的每个元素的大小,第三个参数是要读取的元素个数,第四个参数是读取数据的位置。函数调用完会返回实际读取的元素个数,若在读取过程中发生错误或是在未读取到指定元素个数时读取到文件末尾,则返回一个小于count的数。

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
 FILE *fp = fopen("myfile", "r");
 if(!fp){
 printf("fopen error\n");
 return 1;
}
 char buf[1024];
 const char *arg = "hello linux\n";
 while(1)
 {
   ssize_t s = fread(buf,1,strlen(arg),fp)
   if(s>0)
   {
     buf[s] = 0;
     printf("%s\n",buf);
     }
  if(feof(fp))
  {
   break;
  }
 }

fclose(fp);
return 0;
}

2.4默认打开的三个流

背景补充:

a.当你向显示器输出12345时, 实际上是向显示器写入了字符'1'、'2'、'3'、'4'、'5' ,而不是直接写入整数12345。

当你在C语言中使用 printf("%d", 12345); 这样的语句时,发生了以下转换:

  • 内部存储 :整数12345在内存中以二进制形式存储(例如,4字节int的话是 0x00003039 )
  • 格式转换 : printf 函数根据 %d 格式说明符,将整数12345转换为对应的字符序列"12345"
  • 字符编码 :每个字符被转换为对应的ASCII码('1'→49, '2'→50, '3'→51, '4'→52, '5'→53)
  • 输出到显示器 :这些ASCII码被发送到显示器,显示器根据ASCII码显示对应的字符

所以显示器叫做字符设备

b.当你通过键盘输入12345时, 实际上是输入了字符'1'、'2'、'3'、'4'、'5' ,而不是直接输入整数12345。

键盘是一种 字符设备 ,它的工作原理如下:

  • 按键扫描 :当你按下键盘上的数字键时,键盘会产生对应的 扫描码
  • 扫描码转换 :操作系统的键盘驱动程序将扫描码转换为对应的 字符编码 (通常是ASCII码)
  • 输入缓冲区 :这些字符编码被存储在输入缓冲区中,等待程序读取

所以键盘叫做字符设备

内存中存储的二进制数据 。所有数据,无论是程序、文本、图像还是音频,在内存中都以二进制形式
文本文件和二进制文件的区别 既是文件本身的属性决定的,也与调用的接口有关 ,但 根本区别在于文件内容的组织方式和语义
文本文件可以看做一维数组,而数组下标就为文件读写位置

linux下一切皆文件,所以显示器和键盘都可以看成文件,那我们可以从显示器读取数据,是因为操作系统向显示器文件写入了数据,内存内读取到我们键盘上打的数据,是因为操作系统读取了键盘文件的数据

那为什么我们在向显示器文件输入和从键盘文件中读取前,不需要我们进行打开文件等操作呢?

这是因为在每个进程运行时都会默认打开三个流(其实就是三个文件),即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。

其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。

查看man手册我们就可以发现,stdin、stdout以及stderr这三个都是FILE*类型的。

extern FILE *stdin;

extern FILE *stdout;

extern FILE *stderr;

3. 系统⽂件I/O

打开⽂件的⽅式不仅仅是fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的⽅

案。

先来认识⼀下两个概念:系统调⽤ 和 库函数

• fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)

• ⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝

系统调⽤接⼝和库函数的关系,⼀⽬了然。

所以,可以认为, f# 系列的函数,都是对系统调⽤的封装,⽅便⼆次开发。

为什么各种语言都要封装系统调用?

因为跨平台性,可移植性,每个平台都有巨大的用户,增加语言的竞争性。

接下来我们来看系统调用函数
open

系统接口中使用open函数打开文件,open函数的函数原型如下:

cpp 复制代码
int open(const char *pathname, int flags, mode_t mode);

open的第一个参数

open函数的第一个参数是pathname,表示要打开或创建的目标文件。

open的第二个参数

open函数的第二个参数是flags,表示打开文件的方式。

其中常用选项有如下几个:

参数选项 含义
O_RDONLY 以只读的方式打开文件
O_WRNOLY 以只写的方式打开文件
O_APPEND 以追加的方式打开文件
O_RDWR 以读写的方式打开文件
O_CREAT 当目标文件不存在时,创建文件

打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用"或"运算符隔开。

例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:

bash 复制代码
O_WRONLY | O_CREAT

补充:

open 系统调用的第二个参数 flags 是一个 32 位整型值,用于指定文件的打开方式和行为

在Linux系统中,这些宏定义通常位于 /usr/include/fcntl.h 文件中,具体定义如下:

cpp 复制代码
#define O_RDONLY    0        /* Open for reading only. */
#define O_WRONLY    1        /* Open for writing only. */
#define O_RDWR      2        /* Open for reading and writing. */

#define O_CREAT     0100     /* Create file if it doesn't exist. */
#define O_EXCL      0200     /* Fail if file already exists. */
#define O_TRUNC     01000    /* Truncate file to zero length. */
#define O_APPEND    02000    /* Append to file. */

open的第三个参数

open函数的第三个参数是mode,表示创建文件的默认权限。

例如,将mode设置为0666,则文件创建出来的权限如下:

rw- rw- rw-

但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

rw- rw- r--

open的返回值

open函数的返回值是新打开文件的文件描述符。

bash 复制代码
[lzy@VM-0-14-centos linux_test]$ ./test0206
3

Linux进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。

close

系统接口中使用close函数关闭文件,close函数的函数原型如下:

c 复制代码
int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

write

系统接口中使用write函数向文件写入信息,write函数的函数原型如下:

c 复制代码
ssize_t write(int fd, const void *buf, size_t count);

我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

如果数据写入成功,实际写入数据的字节个数被返回。

如果数据写入失败,-1被返回。

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello linux!\n";
int len = strlen(msg);
while(count--){
write(fd, msg, len); 

}
close(fd);
return 0;
}

read

系统接口中使用read函数从文件读取信息,read函数的函数原型如下:

c 复制代码
ssize_t read(int fd, void *buf, size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

如果数据读取成功,实际读取数据的字节个数被返回。

如果数据读取失败,-1被返回。

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello linux!\n";
char buf[1024];
while(1)
{
ssize_t s = read(fd, buf, strlen(msg));
if(s > 0)
{
printf("%s", buf);
}
else
{
break;
}
}
close(fd);
return 0;
}

4.文件描述符fd

4.1文件描述符是什么

bash 复制代码
[lzy@VM-0-14-centos linux_test]$ ./test0206
fd: 3

这里的fd是什么?

我们之前就提到在操作系统中存在大量被打开的文件,操作系统需要对被打开的文件进行管理,那操作系统是如何管理的呢?

当我们打开⽂件时,操作系统在内存中要创建相应的

数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系

统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files,指向⼀张表files_struct,该表

最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,⽂件

描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

所以当进程打开文件时,操作系统将文件从磁盘加载到内存中,文件创建对应的额struct file ,这些结构体用双链表链接起来,

并将每个文件的struct file 的地址分别填入到fd array[]数组中,例如,stdin填入到数组下表为0的位置,所以数组下标就是文件

描述符,最后进程通过文件描述符调用文件。


4.2⽂件描述符的分配规则

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
    int fp = open("myfile", O_RDONLY);
    printf("fd: %d\n", fp);
    return 0;
}

这里的结果为3很好理解,因为默认生成的三个输入输出流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配。

bash 复制代码
[lzy@VM-0-14-centos linux_test]$ ./test0206
fd: 3

接下来关闭文件描述符为0的文件

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
    close(0);
    int fp = open("myfile", O_RDONLY);
    printf("fd: %d\n", fp);
    return 0;
}

可看到结果为0

bash 复制代码
[lzy@VM-0-14-centos linux_test]$ ./test0206
fd: 0

可⻅,⽂件描述符的分配规则:在files_struct数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。

4.3重定向

这里关闭文件描述符为一的文件也就是标准输出流,用open打开myfile文件,myfile文件的描述符为1,

printf函数会将输出写到文件描述符为1的文件也就是,myfile。

结果:

  • 程序执行后,终端上不会有任何输出
  • 会创建一个名为"myfile"的文件
  • 文件内容会是: fd: 1
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
  close(1);
  int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
  if(fd < 0)
 {
  perror("open");
  return 1;
 }
 printf("fd: %d\n", fd);
 fflush(stdout);//强制将标准输出流(stdout)的缓冲区内容立即写入到输出设备
 close(fd);
 exit(0);
}

这样凡是往文件描述符1中写的内容都写到了myfile中,而不是标准输出流

常⻅的重定向有: > , >> , <

操作符 功能
< 输入重定向
> 输出重定向(覆盖)
>> 输出重定向(追加)

对于标准输出1注意 重定向符号 > or 1>

bash 复制代码
标准输出重定向到文件,标准错误仍然输出到屏幕
command > output.txt

标准错误重定向到文件,标准输出仍然输出到屏幕
command 2> error.txt

标准输出和标准错误分别重定向到不同文件
command > output.txt 2> error.txt

4.4dup2 函数详解:

dup2 是一个系统调用函数,用于 复制文件描述符 ,实现文件操作的重定向。

例子:把输出从屏幕重定向到文件场景

你写了一个程序,想把 printf 的内容保存到文件而不是显示在屏幕上。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    // 1. 打开一个文件(创建或清空)
    int file_fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    
    // 2. 把标准输出(1)重定向到文件
    dup2(file_fd, 1);
    
    // 3. 关闭原来的文件描述符(因为已经复制到1了)
    close(file_fd);
    
    // 现在 printf 的内容会写到文件里
    printf("Hello, this goes to file!\n");
    printf("This also goes to file!\n");
    
    return 0;
}

例子 2:把输入从重定向到文件 场景

你写了一个程序,想从文件读取数据而不是等待用户输入。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    // 1. 打开一个输入文件
    int file_fd = open("input.txt", O_RDONLY);
    
    // 2. 把标准输入(0)重定向到文件
    dup2(file_fd, 0);
    
    // 3. 关闭原来的文件描述符
    close(file_fd);
    
    // 现在 scanf 会从文件读取数据
    char name[100];
    printf("Enter your name: "); // 这个会显示在屏幕
    scanf("%s", name);         // 但这个会从文件读取
    printf("Hello, %s!\n", name); // 这个会显示在屏幕
    
    return 0;
}

总结

dup2 的核心功能就是 让两个文件描述符指向同一个文件 ,从而实现重定向。

我们对重定向有了理解后,那么子进程是如何看待父进程打开的文件

当父进程 fork() 创建子进程时:

  1. 完整复制文件描述符表 :子进程获得父进程文件描述符表的完整副本
  2. 共享文件表项 :相同的文件描述符在父子进程中指向同一个文件表项
  3. 继承文件状态 :包括文件偏移量、打开模式、状态标志等

这里的int ret count 表示这份文件被引用几次

这里的myfile被引用了两次,只用当ret count 才表示该文件被关闭

5.理解一切皆文件

我们来从底层硬件来理解

我们是如何访问键盘,显示器,磁盘的?

先描述,在组织。每个硬件都有其对应的PCB,每个硬件的读取和写入的方法是不同的,为了方便用户层,会在内存中以形成对应的文件,每个文件包含,每个struct file 包含其属性,文件内核缓冲区,对应底层设备的缓冲表。所以访问设备,其实就是访问进程,所以站在进程的角度看一切皆文件。计算机里面的一切问题都可以通过加一层软件层来解决。

这种结构其实是多态,相同的struct file,调用不同的read和write方法。

那么我们调用的read和write与struct file 中的read与write方法有什么不同

先补充关于文件缓冲区的知识

struct file中是有文件缓冲区的用来存储文件的内容,我们的read与write不是直接与磁盘文件接触而是与文件缓冲区接触

所以write其实就是拷贝函数,把数据从用户空间拷贝到对应的文件内核缓冲区中!,而文件缓冲区的数据什么时候写到磁盘文件,由OS决定。

所以从键盘读取数据,其实是OS创建文件,用户调用read OS调用struct file 中的 read(),然后在调用底层read

这种设计使得应用程序可以使用统一的文件操作接口访问不同类型的设备,体现了"一切皆文件"的设计理念。

6.缓冲区
6.1什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓

冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设

备,分为输⼊缓冲区和输出缓冲区。

6.2为什么要引⼊缓冲区机制

1.读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么

每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调

⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的

切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

2.为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可

以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不

需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,

再加上计算机对缓冲区的操作⼤⼤快于对磁盘的操作,故应⽤缓冲区可⼤⼤提⾼计算机的运⾏速度。

3.⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相

应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀

块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的

CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。

6.3缓冲类型

标准I/O提供了3种类型的缓冲区。

1.全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通

常使⽤全缓冲的⽅式访问。

2.⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤

操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准

I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏

I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。

3.⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通

常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。

6.4FILE

因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。

首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。

FILE是语言层级的一个结构体


用户想把数据写到显示器上,例如调用fputs,他会先把数据写到FILE管理的缓冲区数组中,在通过系统调用write(),刷新到内核文件缓冲区。

为什么要有语言缓冲区

1.减少系统调用

2.加速IO函数的调用效率,因为只需要写到语言缓冲区就可以返回而不是深入内核

例子

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}

运⾏出结果:

bash 复制代码
hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢 ./hello > file ,我们发现结果变成了:

bash 复制代码
hello write
hello printf
hello fwrite
hello printf
hello fwrite
  • fork时子进程继承父进程的缓冲区(共享内存)
  • 任一进程刷新缓冲区时修改内存 → 触发写时拷贝
  • 导致父子进程各有独立的缓冲区副本
  • 两个副本都被刷新 → 输出两次数据
相关推荐
blueSatchel7 小时前
U-Boot载入到DDR过程的代码分析
linux·开发语言·u-boot
专注VB编程开发20年7 小时前
vb.net datatable新增数据时改用数组缓存
java·linux·windows
石去皿8 小时前
【嵌入式就业10】Linux内核深度解析:从启动流程到驱动框架的工业级实践
linux·运维·服务器
954L8 小时前
CentOs7执行yum update出现链接404问题
linux·centos·yum·vault
Trouvaille ~8 小时前
【Linux】应用层协议设计实战(二):Jsoncpp序列化与完整实现
linux·运维·服务器·网络·c++·json·应用层
EmbedLinX8 小时前
嵌入式之协议解析
linux·网络·c++·笔记·学习
vortex58 小时前
解密UUOC:Shell编程中“无用的cat使用”详解
linux·shell编程
凉、介8 小时前
VMware 三种网络模式(桥接 / NAT / Host-Only)原理与实验解析
c语言·网络·笔记·操作系统·嵌入式·vmware
wangjialelele8 小时前
Linux中的进程管理
java·linux·服务器·c语言·c++·个人开发