基础IO(文件读取写入,重定向,缓冲区)

发布于:2022-11-09 ⋅ 阅读:(9) ⋅ 点赞:(0) ⋅ 评论:(0)

1.文件基本理论

1.文件=文件内容和文件属性,文件属性也是数据,即使你创建一个新文件也要占据磁盘空间
2.文件操作=文件内容的操作+文件属性的操作,有可能,在操作文件的过程中,即改变内容,又改变属性,就是你更改了内容,那么你的文件大小也会发生改变
3.所谓的打开文件,到底在干什么?将文件的属性或内容加载到内存中!因为cpu要执行fread和fwrite,根据冯诺依曼体系cpu只能从内存做读写
4.是不是所有的文件,都会处于被打开的状态?肯定不是!那么没被打开的文件,在哪里?他只在磁盘上静静的存贮着!当要用的时候再打开
5要是打开的话打开的文件(内存文件)和磁盘文件都各有一份
6.通常我们打开文件,访问文件,是谁在进行相关操作?fopen,flose,fread,fwrite等等这些接口来打开读写等文件,但是我们写代码调用接口,它有在对文件进行操作吗?答案是没有的是把我的代码编译起可执行文件,在运行变成程序,才会执行对应的代码,这样才是真正对我呢见进行相关操作。那么对文件进行操作时的进程!!!
7.那么我们学习文件操作,就是学习进程和打开文件的关系
8.当我们向文件写入的时候,最终是向磁盘写入的,磁盘是硬件,那么只有谁有资格向硬件写入,只有操作系统,那么如果我们想绕开操作系统来操作硬件可以吗?答案是不能,所有上层访问文件的操作,都必须要经过操作系统·,那么操作系统是如果被上层调用的,我们必须使用操作系统他提供的系统接口,如果那个语言他是具有跨平台性的,那么他就会封装所有系统接口(这个叫做上层接口),我们可以通过语言封装的上层接口来间接通过操作系统来访问硬件这也就是跨平台性
9.那么我们为什么要封装?
1.原生系统接口,使用成本比较高
2.语言不具备跨平台性

2.什么是当前路径

我们理解的是当前路径就是我们执行代码所在的路径,那么这个是否正确?

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

int main()
{
  FILE* fp = fopen("log.txt","w");
  if(fp == NULL)
  {
    perror("fopen");
    return 1;
  }

  printf("mypid:%d\n",getpid());

  while(1)
  {
    sleep(1);
  }
  const char*msg = "hello !!!";
  int cnt = 1;
  

  while(cnt < 20)
  {
    fprintf(fp,"%s; %d\n",msg,cnt++);
  }

  fclose(fp);
  
  return 0;
}

上面代码是先不写入文件,先打印pid
在这里插入图片描述
我们先看卷起来的红框,这个是查看进程,下面图片里面有个cwd,这个就是工作路径

在这里插入图片描述
那么我们可以理解到当前路径其实是进程所在的路径,那么要是我们把这个工作路径改了呢?那么当前路径,还是不是我们以前理解的一样就是当前执行文件所在的路径

我们在代码加上

 chdir("/home/lxz");//更当当前进程的工作路径    

用来更改工作路径

我们把自己写的日志给删了,就是上面的log.txt,那么我们看到现象会是cwd的路径改了改成/home/lxz,并且log.txt的这个文件会在/home/lxz这个路径里

在这里插入图片描述

在这里插入图片描述

3.open接口

open第一个参数pathname,要打开的文件名(要路径的),第二个参数flags代表的是你要选择的选项,第三个参数mode代表的是你要创建文件的初始权限,它的返回值,可以看到不是和C语言一样的FILE*而是int,他代表的是打开一个文件,一个文件描述符,-1代表打开错误
在这里插入图片描述
我们先来用用看,在用之前我们先来了接一下别的系统接口
O_RDONLY, O_WRONLY, O_RDWR,O_APPEND,O_CREAT

.O_RDONLY: 以只读的方式打开
O_WRONLY: 以只写的方式打开
O_RDWR : 以读写的方式打开
O_APPEND: 以追加的方式打开
O_CREAT: 创建并打开一个新文件

上面五个接口,看到大写用_来区分的,有这种标志的我们一眼就知道了,这些都是宏,这些就是上面flags这个选项的,如果我们用来做标记位的话,c语言是用整型来做标记位的,但是如果有很多呢?20,30个?这里当然也可以用可变参数列表,但是不方便,这个时候就要用到位图了,大家都知道int是有32个比特位的,那么我们就可以用一个比特位来代表读或者写或者读写等等

1.写入文件内容

下面代码就是打开一个文件log.txt,给给予这个文件权限0666,并写入一些内容到log.txt里面,而至于O_WRONLY: 以只写的方式打开,O_CREAT: 创建并打开一个新文件,umask(0)就是初始化umask码了,因为相比于操作系统默认的umask权限,我们自己初始化的时候,不用进行计算,毕竟umask全都是0,这样文件权限给错失误率会降低, 这个代码也要注意下write(fd,str,strlen(str));这个是不能加1的,因为文件是不识别你的\n的,如果你还是要加1的话,写入文件的话,会生成一些特殊字符

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


int main()
{
  umask(0);//初始化umask码
  int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
  if(fd < 0)
  {
    perror("open");
    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;
}

运行结果
在这里插入图片描述

如果在c语言里面我们的写入文件是会把文件清空的,我们把上面的代码str也就是写入进去的数据改成aaaaa,打印一次看看效果

在这里插入图片描述
可以看到是没有清空的,他是覆盖了,但是之前写入的数据是没有清空的

这个时候其实是少加入了一个参数

int fd = open("log.txt",O_WRONLY | O_CREAT|O_TRUNC,0666);

O_TRUNC这个宏的效果就是清空文件里面的内容

运行结果
在这里插入图片描述

看到了上面的操作,我们回想下c语言的文件操作,只要一个w就可以了,但是底层实现却要调用那么多底层接口

追加内容,c语言只要一个a,而底层实现要

int fd = open(“log.txt”,O_WRONLY | O_CREAT|O_APPEND,0666);

运行结果
在这里插入图片描述

2.读取文件内容

#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("log.txt",O_RDONLY);

 if(fd < 0)
 {
   perror("open");
   return 1;
 }

 printf("fd; %d\n",fd);

 char buffer[128];

 ssize_t  s = read(fd,buffer,sizeof(buffer)-1);//把他做成一个字符串

 if(s > 0)
 {
   buffer[s]= '\0';
   printf("%s",buffer);
 }


  return 0;
}

上面代码其实我们没有按行读,也没有像scanf格式化也没有,这些都是c语言帮我们处理了

运行结果
在这里插入图片描述

4.文件描述符

#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_TRUNC, 0666);
    int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fde = open("loge.txt", O_WRONLY | O_CREAT | O_TRUNC, 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;
}

运行结果
在这里插入图片描述
为什么我们的文件描述符是从3开始的,为什么不是从0开始
因为一开始就打开了
0::标准输入,键盘
1:标准输出。显示器
2:标准错误,显示器

但是这个和我们学过的C语言不是一样吗?
在这里插入图片描述
那么我们来理解下FILE*是文件指针,FILE是c库提供的结构体,里面了封装多个成员,FILE内部,一定封装了fd,但是对于文件操作而言,系统接口只认fd

证明

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


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

在这里插入图片描述

可以看到是一模一样的

验证012和stdin,stdout,stderr

代码

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


int main()
{
    // 验证012和stdin,stdout,stderr的对应关系
    printf("stdin: %d\n", stdin->_fileno);
    printf("stdout: %d\n", stdout->_fileno);
    printf("stderr: %d\n", stderr->_fileno);
  return 0;
}

在这里插入图片描述

那么为什么是返回的文件描述符是从0到9’开始?
我们从c语言了接到这个不是数组下标吗?
但是我们没写啊,我们讲的这个数组下标其实是内核提供的数组下标,凡是返回fd的用的都是系统接口,而这些都是操作系统提供的返回值,那么他能给你,那它肯定是有实现的

我们知道,进程和内存文件的关系都是在内存里面维护的,所以被打开的文件是在内存里面的,那么我们又知道一个进程可以打开多个文件,是1对n的关系,所以系统在运行中,有可能会存在大量的被打开的文件,操作系统会对这些被打开的文件进程管理,那么它怎么管理?那就是先描述再组织
一个文件被打开,在内核中,要创建该被打开的文件的内核数据结构 (这个过程叫做先描述)

他打开一个文件就会出现一个 struct file数据结构,每出现一个就会用链表来链接起来,那么这不就是对被打开的文件管理,变成了对链表的增删查改吗?那么这里有个进程 struct task_struct结构,根据我们上面讲,1个进程是可以对应的多个文件,那么进程是怎么和文件进行映射关系,那么struct task_struct 里面有个指针 叫做 strruct files_struct *fs,里面的 strruct files_struct里面有个数组,里面的数组存的就是指向文件的指针,这样进程和文件的映射关系就完成了,而这个数组里面的数组下标就是我们返回的文件描述符

只看文字不太好理解可以配合下面的图来理解
在这里插入图片描述

现在了接了还有一个问题,就是0 1 2表示的东西不是硬件吗?为什么也可以用struct file来标识对应的文件,这个时候就要来理解一句话了linux下一切皆文件

struct file结构体里面大概分为二个部分一个部分是文件属性,另一个部分分为文件方法,而文件方法,文件方法里面有指着硬件的读写方法(这个是由驱动控制的),如果是不同的设备,他的读写方式也是不同的,而这种方法也叫做多态

那么因为上面我们就可以知道文件描述符分配规则是:从数组里面选一个未正在使用且下标最小的文件

5.重定向

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

int main()
{
  int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if(fd > 0)
  {
    perror("open");
    return 1;
  }


  fprintf(stdout,"打开文件成功,fd:%d\n",fd);
  close(fd);
  return 0;
}

运行结果
在这里插入图片描述
打印的是3,我们并不意外,上面的文章我们讲了,但是如果我们在文件开始加上close(1)呢?

在这里插入图片描述

这个时候就看到了什么什么都没有打印(不打印是因为关掉了1这个我们理解),但是文件log.txt里面也没有内容

这个问题我们只要刷新缓冲区就行了

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

int main()
{
  int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if(fd > 0)
  {
    perror("open");
    return 1;
  }


  fprintf(stdout,"打开文件成功,fd:%d\n",fd);
  fflush(stdout):
  close(fd);
  return 0;
}

这个时候我们就可以看到log.txt里面就有内容了,里面的内容fd是1

上面操作本来应该要往显示器打印的,结果打印在文件了,这个就是重定向了

上面为什么会出现这种情况?
是因为,原本1号下标指的是显示器,你把关掉了,这个时候我们给open分配的文件描述符是1,因为他的分配规则是,从数组里面选一个未正在使用且下标最小的文件,但是这个时候我们打开了一个新的文件log.txt,又因为现在文件描述符是1所以就把1号描述符指向了新的文件log.txt

在这里插入图片描述

下面这个图片和上面差不多,只不过下面这个图是用来讲重定向的原理,因为上层只认识0 1 2 3 4等等的fd,如果我们把数组下标的特性内容(file*指向)改了,那么内容也改了,这样也就完成了重定向操作了

在这里插入图片描述

但是重定向,要像刚刚那样close关闭,使用吗?那样子太拙了,能调用函数接口的只有操作系统,所以这个时候就要用到一个接口dup2

在这里插入图片描述
这个接口的作用是用 oldfd,来覆盖newfd,这样子只有oldfd的内容

下面代码可以理解为,把fd的内容放在了1里面,原本是打印到显示器的,现在打印到了fd里面了

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

int main()
{
  int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if(fd < 0)
  {
    perror("open");
    return 0;
  }
  
  dup2(fd,1);

  fprintf(stdout,"打开文件成功,fd:%d\n",fd);
  close(fd);

  return 0;
}

运行结果
在这里插入图片描述

6.缓冲区

1.什么是缓冲区?

缓冲区本质:就是一块内存

2.为什么要有缓冲区

打个例子,比如你要拿点衣服回家,因为天气问题,太冷了,夏天的衣服不用穿了,那你又怕到时候回家的时候太多行李,拿不动,这个时候你有二个选择,第一个选择就是自己回家把东西运回去,这种做法,唯一的消耗就是你自己的时间,因为你要用你自己的时间去把东西运过去,第二种办法就是快递,你把东西交给快递公司,那么像运行李之类的操作就托管给快递公司了,快递公司再送到你家附近的快递公司,再送到你家,那么你现在就可以去自己玩,或者干其他事情都行

那么上面的例子你就是进程,而行李就是数据,而快递公司就是缓冲区,而上面的过程相当于,进程把数据写到外设上,这样子就不用进程等太久,你这个进程可以干其他事情。这就是缓冲区的作用,而这种做法可以解放使用的缓冲区的进程时间

还有一种后续情况,就是你忘了某些行李寄回去了,但是快递公司他寄快递他又不是马上给你寄的,因为他要等到一定的快递量再给你寄,但是你让给他东西让他寄,他还是会接收快递的,只是达到了某些量才马上寄过去,所以缓冲区的存在可以集中处理数据刷新的效率,减少IO的效率,从而提高整机的效率

3.缓冲区在哪里?

代码

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

int main()
{

fprintf(stdout, " hello fprintf");
fputs(" hello fputs",  stdout);

//write可是立即刷新的!printf -> write
const char *msg = "hello write";
write(1, msg, strlen(msg));


sleep(5);

  return 0;
}

下面结果其实因为write是可以马上刷新缓冲区的所i有先打印hello wirte,在等sleep(5)秒后才打印接下来的

运行结果
在这里插入图片描述

那么从上面运行结果我们就可以知道缓冲区它不是属于内核级别的,它是属于语言级别的

上面代码如果我们把1给关掉会怎么样,在sleep(5),后面加上close(stdout->_fileno);
,上面的c语言接口里面都有包含stdout
在这里插入图片描述
可以看到没有打印,因为进程退出的时候想要刷新,但是你把文件描述符给关了,所以write调用失败了,所以上面的代码根本没写到操作系统里面,只是写到了缓冲区里面
既然缓冲区在FILE内部,在C语言种,我们没一次打开一个文件都要有一个FILE*会返回!,这样子就意味着,每一个文件都有一个fd和属于它自己的语言级别缓冲区

4.缓冲区的刷新策略

他刷新策略分为常规三种
常规:
1.无缓冲(立即刷新)
2.行缓冲(逐行刷新)
3.全缓冲(缓冲区满才刷新)
既然我列了常规,那么肯定还要特殊情况了
特殊:
1.进程退出(因为进程退出会自动刷新缓冲区)
2.用户强制刷新

那么既然我们来了解了缓冲区我们来看看下面代码,我会先把执行结果说出来,再说为什么

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


int main()
{
  const char* str1 = "hello printf\n";
  
  const char* str2 = "hello fprintf\n";
  const char* str3 = "hello fputs\n";
  const char* str4 = "hello write\n";
 
  //C语言接口
  printf(str1);
  fprintf(stdout,str2);
  fputs(str3,stdout);


  //系统接口
  write(1,str4,strlen(str4));

  fork();

  return 0;
}

可以看到打印了四次,这个很正常因为我们就打印了四次,因为我们c语言函数后加了\n所以能立即刷新
在这里插入图片描述

但是如果我们重定向一下
可以看到除了write全都打印了二次
在这里插入图片描述

重定向我们知道他里面加了dup2(),所以原本是写到显示器的内容,现在写在了log.txt里面了,而我们上面刷新策略讲了,行缓冲(逐行刷新),所以打印四个没问题,现在打印了7个而write只打印了一次,这就和缓冲区有关系了,因为write是不进入缓冲区,而c语言函数是要进入语言级别的缓冲区,因为重定向的关系,它们进行的不是行缓冲(逐行刷新)而是全缓冲(缓冲区满才刷新),当我们fork的时候就会创建子进程,当父子进程退出的时候,就会刷新缓冲区(就是把缓冲区的数据写入操作系统并清空缓冲区),缓冲区,是自己的FILE内部维护的,属于父进程内部的数据区域,代码是父子进程共享的,数据不是是分别独立的,所以要发生写时拷贝,所以父进程刷新一次,子进程刷新一次,所以缓冲区打印二次