文章目录

  • 前言
  • 一、基础概念
  • 二、回顾C语言
  • 2.1 对文件进行写操作
  • 2.2 追加写文件
  • 2.3 读文件
  • 2.4 简易cat功能
  • 总结
  • stdin&stdout&stderr
  • 打开文件的方式
  • 三、系统文件I/O
  • 接口介绍
  • open介绍
  • 使用open接口
  • close
  • write
  • read
  • 四、文件描述符
  • 先验证0,1,2就是标准的IO
  • 标准输入流
  • 标准输出流
  • 标准错误流
  • 验证0,1,2和stdin,stdout,stderr的对应关系
  • 文件描述符的分配规则
  • 总结

前言

今天这个小结节,我来大家来了解Linux下的文件操作。首先我们来复习一下C语言的文件操作,基于C语言的文件操作我们对Linux的学习就会方便很多了!我带大家首先来了解文件相关系统的接口和文件描述符,并且理解重定向!
最后在基于重定向,在下一小节将我上节写的myshell完善一下!



​正文开始!​

一、基础概念

  1. 文件=文件内容+文件属性(属性也是数据,即便你创建一个空文件,也要占据磁盘空间)
  2. 文件操作=文件内容的操作+文件属性的操作(有可能再操作文件的时候,即改变内容,又改变属性)
  3. 对于文件操作,我们首先要打开文件。所谓"打开"文件,究竟在干什么?将文件的属性或者内容加载到内存中!(冯诺依曼体系结构决定!)
  4. 是不是所有的文件,都会被处于打开的状态呢?没有被打开的文件,在哪里呢?(只在磁盘上存储!)
  5. 打开的文件(内存文件)和磁盘文件
  6. 通常我们打开文件,访问文件,关闭文件,是谁在进行相关操作?fopen,fwrite,fread,fclose…->代码->程序->当我们文件程序,运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作(进程在做相关操作!!!)
  7. 进程和打开文件的关系!

二、回顾C语言

2.1 对文件进行写操作

[Linux]----文件操作(复习C语言+文件描述符)_运维


当我们以w方式打开文件,准备写入的时候,其实文件已经先被清空了!

#include<stdio.h>
#include<unistd.h>

int main()
{
FILE* fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen");
return 1;
}
const char* msg="hello rose!";
int cnt=0;
while(cnt<10)
{
fprintf(fp,"%s %d\n",msg,cnt);
cnt++;
}
fclose(fp);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_运维_02

默认这个"log.txt"文件会在哪里形成呢?—>当前路径

那么什么是当前路径呢?–>进程当前的路径

接下来带大家查看进程的信息

#include<stdio.h>
#include<unistd.h>

int main()
{
FILE* fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen");
return 1;
}
printf("%d\n",getpid());
while(1)
{
sleep(1);
}//在这里会一直休眠下去,直到我们杀掉这个进程
const char* msg="hello rose!";
int cnt=0;
while(cnt<10)
{
fprintf(fp,"%s %d\n",msg,cnt);
cnt++;
}
fclose(fp);
return 0;
}

ll /proc/进程id

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_03


在这里我们就可以看到"log.txt"就在当前cwd,也就是进程所处的路径了。接下来我们有意识的更改当前路径,这里需要用到系统接口chdir();

[Linux]----文件操作(复习C语言+文件描述符)_#include_04

#include<stdio.h>
#include<unistd.h>

int main()
{
chdir("/home/hulu");
FILE* fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen");
return 1;
}
printf("%d\n",getpid());
while(1)
{
sleep(1);
}
}

[Linux]----文件操作(复习C语言+文件描述符)_#include_05

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_06

2.2 追加写文件

#include<stdio.h>
#include<unistd.h>

int main()
{
FILE* fp=fopen("log.txt","a");
if(fp==NULL)
{
perror("fopen");
return 1;
}
const char* msg="hello rose!";
int cnt=0;
while(cnt<5)
{
fprintf(fp,"%s %d\n",msg,cnt);
cnt++;
}
fclose(fp);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_linux_07

追加写入,不断的往文件中新增内容—>追加重定向!

[Linux]----文件操作(复习C语言+文件描述符)_linux_08

2.3 读文件

[Linux]----文件操作(复习C语言+文件描述符)_运维_09

#include<stdio.h>
#include<unistd.h>

int main()
{
FILE* fp=fopen("log.txt","r");
if(fp==NULL)
{
perror("fopen");
return 1;
}
char buffer[64];
while(fgets(buffer,sizeof(buffer),fp)!=NULL)
{
printf("echo: %s",buffer);
}
fclose(fp);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_10

2.4 简易cat功能

#include<stdio.h>
#include<unistd.h>

//myfile filename
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("Usage: %s filename\n",argv[0]);
return 1;
}
FILE* fp=fopen(argv[1],"r");
if(fp==NULL)
{
perror("fopen");
return 1;
}
char buffer[64];
while(fgets(buffer,sizeof(buffer),fp)!=NULL)
{
printf("%s",buffer);
}
}

[Linux]----文件操作(复习C语言+文件描述符)_运维_11


[Linux]----文件操作(复习C语言+文件描述符)_运维_12

当我们向文件写入的时候,最终是不是向磁盘写入?

因为磁盘是硬件,所以只有OS有资格向硬件写入!

那么能绕开操作系统吗?

答:不能!那么所有上层的访问文件的操作,都必须贯穿操作系统!

操作系统是如何被上层使用的呢?

因为操作系统不相信任何人,所以必须使用操作系统提供的相关系统调用!

那么为什么要进行封装文件操作接口呢?

  • 原生系统接口,使用成本比较高!
  • 语言不具备跨平台性!

那么封装是如何解决跨平台性的问题呢?

  • 穷举所有底层的接口+条件编译!

C库提供的文件访问接口是来自于系统调用!
那么就能解释不同的语言有不同的文件访问接口!!
所以无论什么语言的底层的接口是不变的!

所以这就要求我们必须学习文件级别的系统接口!

总结

stdin&stdout&stderr

  • C默认会打开三个输入输出流,分别是stdin,stdout,stderr
  • 仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,文件指针

打开文件的方式

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_13

三、系统文件I/O

接口介绍

open介绍

[Linux]----文件操作(复习C语言+文件描述符)_运维_14

返回值

[Linux]----文件操作(复习C语言+文件描述符)_#include_15

打开文件的选项

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_16


[Linux]----文件操作(复习C语言+文件描述符)_运维_17


[Linux]----文件操作(复习C语言+文件描述符)_linux_18


对于O_RDONLY,O_WRONLY,O_RDWR,O_APPEND,O_CREAT!这些都是宏!

系统传递标记位,使用位图结构来进行传递的!

每一个宏标记,一般只需要有一个比特位为1,并且和其他宏对于的值不能重叠。

代码模拟实现

#include<stdio.h>

#define PRINT_A 0x1
#define PRINT_B 0x2
#define PRINT_C 0x4
#define PRINT_D 0x8
#define PRINT_DFL 0x0


void Show(int flags)
{
if(flags&PRINT_A)
printf("hello A\n");

if(flags&PRINT_B)
printf("hello B\n");

if(flags&PRINT_C)
printf("hello C\n");

if(flags&PRINT_D)
printf("hello D\n");

if(flags==PRINT_DFL)
printf("hello Default\n");

}

int main()
{
Show(PRINT_DFL);
Show(PRINT_A);
Show(PRINT_B);
Show(PRINT_A|PRINT_B);
Show(PRINT_C|PRINT_D);
Show(PRINT_A|PRINT_B|PRINT_C|PRINT_D);
return 0;
}

我们通过传入不同的选项,打印出不同的语句。

[Linux]----文件操作(复习C语言+文件描述符)_运维_19

使用open接口

#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("log.txt",O_WRONLY|O_CREAT);
if(fd<0)
{
perror("open error");
return 1;
}
printf("fd=%d\n",fd);

return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_20


[Linux]----文件操作(复习C语言+文件描述符)_linux_21


所以我们要打开曾经不存在的文件,我们要用到第二个open函数,带有权限的设置!

int open(const char *pathname, int flags, mode_t mode);
int fd = open(“log.txt”,O_WRONLY|O_CREAT,0666);

[Linux]----文件操作(复习C语言+文件描述符)_运维_22


可以看到我们创建文件的权限是0666,可是实际显示的是0664呢?

这就和我们之前学的掩码umask有联系了!

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_23

不清楚的话可以去看看这篇博客权限的理解!

close

[Linux]----文件操作(复习C语言+文件描述符)_linux_24

write

[Linux]----文件操作(复习C语言+文件描述符)_c语言_25

#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("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open error");
return 1;
}
printf("fd=%d\n",fd);
int cnt=0;
const char* str="hello file!\n";
while(cnt<5)
{
write(fd,str,strlen(str));
cnt++;
}

close(fd);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_#include_26


C在w方式打开文件的时候,会清空的!

int main()
{
int fd = open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open error");
return 1;
}
printf("fd=%d\n",fd);
int cnt=0;
const char* str="aaaa";
//const char* str="hello file!\n";
while(cnt<5)
{
write(fd,str,strlen(str));
cnt++;
}

close(fd);
return 0;
}

修改我们的代码后

[Linux]----文件操作(复习C语言+文件描述符)_c语言_27


我们发现此处直接覆盖曾经的数据,但是曾经的数据为什么保留呢了?

因为我们没有带有截断选项O_TRUNC

[Linux]----文件操作(复习C语言+文件描述符)_linux_28

接下来我们带上这个选项后

int main()
{
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open error");
return 1;
}
printf("fd=%d\n",fd);
int cnt=0;
const char* str="hello rose!\n";
while(cnt<5)
{
write(fd,str,strlen(str));
cnt++;
}

close(fd);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_linux_29


现在我们就发现之前的数据被截断了!

所以我们现在可以类比与C语言的fopen,底层的open的选项就是"O_WRONLY|O_CREAT|O_TRUNC"!!!

接下来我们验证追加选项"O_APPEND"
代码如下

int main()
{
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd<0)
{
perror("open error");
return 1;
}
printf("fd=%d\n",fd);
int cnt=0;
const char* str="hello hulu!\n";
while(cnt<5)
{
write(fd,str,strlen(str));
cnt++;
}

close(fd);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_linux_30

read

我们打开文件就默认他是存在的,不需要携带"O_CREAT"选项

如果文件不存在会返回-1;

[Linux]----文件操作(复习C语言+文件描述符)_linux_31

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define NUM 1024
int main()
{
int fd = open("log.txt",O_RDONLY);
if(fd<0)
{
perror("open error");
return 1;
}
printf("fd=%d\n",fd);
char buffer[NUM];
while(1)
{
ssize_t s=read(fd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]='\0';
printf("%s",buffer);
}
else
break;
}

close(fd);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_c语言_32

四、文件描述符

在上面的实验中我们了解到打开文件后返回给文件描述符fd=3,这是为什么?

接下来先来看代码,让我们去了解文件描述符

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fda=open("loga.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fdb=open("logb.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fdc=open("logc.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fdd=open("logd.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fde=open("loge.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
printf("fda=%d\n",fda);
printf("fdb=%d\n",fdb);
printf("fdc=%d\n",fdc);
printf("fdd=%d\n",fdd);
printf("fde=%d\n",fde);
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_运维_33

  1. 为什么文件描述符默认是从3开始的呢?那么0,1,2去哪了?
    因为0,1,2被默认打开了
  • 0:标准输入,键盘
  • 1:标准输出,显示器

  • 2:标准错误,显示器
  • [Linux]----文件操作(复习C语言+文件描述符)_运维_34

  • 这就与我们C语言联系起来了!因为C语言封装的系统接口。
    首先我们之前在C语言中学到FILE*–>文件指针—>FILE是什么呢?—>C语言提供的结构体!–>封装了多个成员
    因为对于文件操作而言,系统接口只认识fd;(FILE内部必定封装了fd)
  1. 0,1,2,3,4…,我们之前见过什么样的数据是这个样子的呢?
    这和我们之前学习的C/C++的数组下标相似
    进程:内存文件的关系—>内存—>被打开的文件是存在内存里面的!!!
    一个进程可不可以打开多个文件?–>当然可以,所以在内核中,进程:打开的文件=1:n–>所以系统在运行中,有可能会存在大量的被打开的文件!—>OS要不要对这些被打开的文件进行管理呢??—>操作系统如何管理这些被打开的文件呢??—>答案是先描述,在组织。
    一个文件被打开,在内核中,要创建该被打开的文件的内核数据结构—先描述


    那么进程如何和打开的文件建立映射关系呢??

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_35

先验证0,1,2就是标准的IO

标准输入流

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
char buffer[1024];
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]='\0';
printf("echo:%s",buffer);
}
return 0;
}

[Linux]----文件操作(复习C语言+文件描述符)_运维_36

标准输出流

int main()
{
const char* s="hello write!\n";
write(1,s,strlen(s));
}

[Linux]----文件操作(复习C语言+文件描述符)_c语言_37

标准错误流

int main()
{
const char* s="hello write!\n";
write(2,s,strlen(s));
}

[Linux]----文件操作(复习C语言+文件描述符)_linux_38


我们看出标准错误流也打印到了显示器上面。

至于标准输出和标准错误的区别,我们稍后带大家了解。

验证0,1,2和stdin,stdout,stderr的对应关系

int main()
{
printf("stdin: %d\n",stdin->_fileno);
printf("stdout: %d\n",stdout->_fileno);
printf("stderr: %d\n",stderr->_fileno);
}

[Linux]----文件操作(复习C语言+文件描述符)_#include_39

由上面的实验我们可以得出,FILE结构体中的fileno就是封装了文件描述符fd!!!

[Linux]----文件操作(复习C语言+文件描述符)_c语言_40

0,1,2—>stdin,stdout,stderr—>键盘,显示器,显示器(这些都是硬件呀!)也用你上面的struct file来标识对应的文件吗??

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_41


如何证明呢?我来带大家看看LInux下的内核结构!!!

[Linux]----文件操作(复习C语言+文件描述符)_#include_42


[Linux]----文件操作(复习C语言+文件描述符)_打开文件_43


[Linux]----文件操作(复习C语言+文件描述符)_#include_44

文件描述符的分配规则

int main()
{
close(0);
//close(1);
//close(2);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd=%d\n",fd);
}

[Linux]----文件操作(复习C语言+文件描述符)_linux_45


我们发现把1关掉后什么都没有了!

因为printf->stdout->1虽然不在指向对应的显示器了,但是已经指向了log.txt的底层struct_file对象!

[Linux]----文件操作(复习C语言+文件描述符)_打开文件_46

遍历fd_array[],找到最小的没有被使用的下标,分配给新的文件!!


总结

下小节我来给大家讲述关于重定向的本质,和缓冲区的概念,等讲完重定向后,我们再把myshell完善!
(本章完!)