目录

1.初步理解"文件"

2.从C语言库函数中理解文件操作

3.从系统调用角度理解文件操作

4.理解文件标识符

重定向:通过改变文件标识符来完成文件操作;

         使用dup2系统调用完成重定向;

5.理解“一切皆文件”

6.缓冲区


1.初步理解"文件"

文件在磁盘当中存储;

文件  =   内容  +  属性;(文件当中所写的内容以及在磁盘当中存储的各种属性)

正确理解:

1.文件大小为0,当然也要在磁盘中占据空间;

2.磁盘属于外设(既属于输出设备,也属于输入设备);

3.对文件的操作,本质是:进程对文件的操作;

2.从C语言库函数中理解文件操作

进程一旦被执行,编译器默认打开的文件:

#include <stdio.h>

extern FILE* stdin;//标准输入
extern FILE* stdout;//标准输出
extern FILE* stderr;//标准错误

打开文件的方式:

r  -    打开文件只读,并将输入流设置在文件开头;

r+  -  打开文件进行读写, 并将输入流设置在文件开头;

w/w+ - 将文件内容清空(若未创建,则创建),并进行文件写入;

a/a+ - (若文件为创建,则创建),  并对文件内容进行追加写入;

3.从系统调用角度理解文件操作

了解系统层面的文件打开操作:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open (const char* pathname,int flags);//通常进行只读操作;
int open (const char* pathname,int flags,mode_t mode);

//权限位     涉及位图的思想
//O_RDONLY  进行只读
//O_WRONLY  进行只写入
//O_CREAT   进行文件的创建
//O_TRUNC   进行文件内容的清空

总结:基本和C语言库函数的文件操作函数参数差不多;但有权限位和标志位的;

C语言中的众多文件打开方式,其实是标志位的组合;

引出下文:open的返回值其实是文件标识符;

4.理解文件标识符

默认我们通过open打开的第一个文件的文件标识符为:3

其中系统中默认打开的stdin、stdout、stderr分别为0、1、2;

这里可做个实验理解;

我们这里将stdout的文件标识符1通过close关掉,创建文件后将msg字符串向1中写入;

int main()
  9 {
 10     close(1);
 11     int fd=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
 12     if(fd<0)
 13     {
 14         perror("open");
 15         exit(1);
 16     }
 17 
 18     const char* msg="abcd";
 19     write(1,msg,strlen(msg));
 20     return 0; 
    }
  

实验结果:

所以我们也可以理解文件标识符的分配原则从低到高分配;

重定向:通过改变文件标识符来完成文件操作;

>   输出重定向

>> 追加输出重定向

<   输入重定向

使用dup2系统调用完成重定向;

int main()
  9 {
 10     //close(1);
 11     int fd=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
 12     dup2(fd,1);
 13     if(fd<0)
 14     {
 15         perror("open");
 16         exit(1);
 17     }
 18 
 19     const char* msg="abcd";
 20     write(fd,msg,strlen(msg));    
 21     return 0;
    }

5.理解“一切皆文件”

Linux操作系统在管理文件的过程中,其实是文件的描述为结构体struct_file被管理

而在struct_file中有很多的成员;

f_count  //文件的引用计数,有进程打开它就++,一旦为0则释放空间,提高系统效率;

const struct file_operations * f_op;//文件操作

在我们的电脑中,有显卡、网卡、键盘、鼠标、麦克风等外设,它们都是通过驱动程序调用系统调用实现对应文件结构体的修改

*f_op中有很多的系统调用函数指针;

实现外设对应系统中的文件,被系统管理;

6.缓冲区

引入缓冲区的目的:减少磁盘的读写次数,再加上计算机对缓冲区的操作远远快于对磁盘的操作。所以用缓冲区可大大提高计算机的运行速度

缓冲类型:

全缓冲:要求填满整个缓冲区后才刷新,对于磁盘文件的操作通常使用全缓冲的方式访问;

行缓冲:在输入和输出中遇到换行符\n时,才会刷新到内核级缓冲区采用系统调用进行读写操作(例如操作流为stdout或stdin时,采用行缓冲方式);

无缓冲立即刷新(例如标准出错流stderr通常不带缓冲区,使得错误信息立即刷新出来);

缓冲区刷新的条件:

1.缓冲区满时;

2.执行flush语句;

3.进程结束;

实践例子:

1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstring>
  4 #include <unistd.h>
  5 
  6 int main()
  7 {
  8     //库函数
  9     printf("hello printf!\n");
 10     fprintf(stdout,"hello fprintf\n");
 11     const char* s="hello fwrite\n";
 12     fwrite(s,strlen(s),1,stdout);
 13 
 14     //系统调用                                                                                 
 15     const char* ss="hello write!\n";
 16     write(1,ss,strlen(ss));
 17     
 18     return 0;
 19 }

1.当程序执行时,打印信息进过行刷新,刷新到内核级缓冲区,进程结束后文件级缓冲区上的数据刷新到内核级缓冲区,同write所打印内容一同被系统调用打印在显示器上;

2.当程序被执行时,因为执行的是重定向命令,所以程序缓冲区刷新方式从行刷新变成了满刷新,进程结束后才被刷新到内核级缓冲区,所以库函数的打印在系统调用函数的后面

3.当执行下面的代码创建子进程后,正常执行./属于向显示器输出(刷新方式为行刷新),也就是说当遇到“\n”时,打印内容就已经刷新,属于正常打印;

1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstring>
  4 #include <unistd.h>
  5 
  6 int main()
  7 {
  8     //库函数
  9     printf("hello printf!\n");
 10     fprintf(stdout,"hello fprintf\n");
 11     const char* s="hello fwrite\n";
 12     fwrite(s,strlen(s),1,stdout);
 13 
 14     //系统调用                                                                                 
 15     const char* ss="hello write!\n";
 16     write(1,ss,strlen(ss));
 17     
        fork();
 18     return 0;
 19 }

但执行重定向命令后,刷新方式的改变,导致库函数的内容不能及时刷新,程序遇到fork()时,会发生进程间数据的写时拷贝,同时也将struct_task中的struct_file中的文件缓冲区中的内容拷贝,进程都结束后刷新,所以log.txt中的内容,库函数打印出现两次,系统调用函数只出现一次

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐