【linux】:linux下文件的使用以及文件描述符

文章目录


前言

理解文件原理和操作:

我们先快速回忆下一C语言的文件操作:

4b9bc1ffdc624addb785792f86abc3c1.png

 首先看一下fopen函数的使用,然后我们写一段简单的C语言打开文件的代码如下图:

#include <stdio.h>
#define LOG "log.txt"
int main()
{
  //默认写方式打开文件,如果文件不存在,就删除它
  FILE* fp = fopen(LOG,"w");
  if (fp==NULL)
  {
    perror("fopen:");
    return 1;
  }
  //正常进行文件操作
  const char* msg = "hello friends\n";
  int cnt = 1;
  while (cnt)
  {
    fputs(msg,fp);
    --cnt;
  }
  fclose(fp);
  return 0;
}

接下来我们运行一下:

050b5363cb0946739adca264abd63702.png

 通过上图我们可以看到成功将我们的字符串写入文件中,那么这次我们什么都不写再看看:

fdf2d420d4ca4888b00b90c5d7a6607c.png

f71a0ec33b994ff58bd9cb0ddc78d4a8.png

 为什么原先log里的字符串没了呢?因为我们打开的方式是w,文件内容会先被清空再写入。

下面我们再介绍一个接口snprintf:

2b6de12b9b544b71a13fef0743c45ad8.png

普通的printf是默认向显示器打印,fprintf是指定文件流向指定文件进行打印,下面我们演示一下:

首先我们修改一下代码,将fputs替换为fprintf函数

int main()
{
  //默认写方式打开文件,如果文件不存在,就删除它
  FILE* fp = fopen(LOG,"w");
  if (fp==NULL)
  {
    perror("fopen:");
    return 1;
  }
 //正常进行文件操作
  const char* msg = "hello friends\n";
  int cnt = 5;
  while (cnt)
  {
   // fputs(msg,fp);
    fprintf(fp,"%s:%d:sxy\n",msg,cnt);
    --cnt;
  }
  fclose(fp);
  return 0;
}

ac8e727dc6814dbca0f0f0c8fdbc22b7.png

 运行结果如上图所示,并且我们不仅可以像文件中打印,也可以在输出流中打印:

14126e135e0a4a7ba4270da5e628e96a.png

 我们都知道,一个程序会默认打开以上三个输入输出流,下面我们像stdout打印一下:

ae9c660e6b3f4b399410842b31deb5b7.png

93f26cbf0ca149c298296e0c1d2f1c2c.png 运行后我们发现直接就能显示不用再打开log.txt查看了,下面我们再来演示一下snprintf函数:

int main()
{
  //默认写方式打开文件,如果文件不存在,就删除它
  FILE* fp = fopen(LOG,"w");
  if (fp==NULL)
  {
    perror("fopen:");
    return 1;
  }
 //正常进行文件操作
  const char* msg = "hello friends\n";
  int cnt = 5;
  while (cnt)
  {
    char buffer[256];
    snprintf(buffer,sizeof(buffer),"%s:%d:sxy\n",msg,cnt);
    fputs(buffer,fp);
    --cnt;
  }
  fclose(fp);
  return 0;
}

 snprintf就是将msg格式化到buffer数组里并且以buffer的大小进行打印,然后将数组中的数据写入文件中。

2d6981692cb7420088ce6b6c2bbd068c.png

 后面还有a追加写入等的方式我们就不在演示,这些都是我们C语言的时候学过的。

以上都是语言进行文件的操作,下面我们进入今天的正题,在系统层面使用文件操作。


一、linux系统中的文件操作以及文件接口

首先我们看一下系统中文件的接口open:

8dbfb80a3e1c49f0982e2f01fce296cb.png

然后我们再看一下这个接口的返回值是什么:

3abe030b766049a589af46f8bb9dd6e0.png 如果成功返回一个新的文件描述符,否则返回-1,下面我们演示一下这个接口:

首先一定要包头文件:

f6f45daaff7f4253bd2d00edece224fe.png

这个函数的第一个参数与刚刚一样都是文件名,第二个参数其实是用位图实现的,就是用一个标志位充当不同的行为,如下图所示:

71dc49ee99a047e8bdb2933b6ed74240.png 在上图中我们就利用位图传不同的flags就能打打印不同的数,并且不仅可以打印一个也可以同时打印多个:

11e7d9104cd04cacbb49edd82e6e8aad.png

 9cd8b53f8612417e96b7ca5667a85e8e.png

而第二个参数的使用方式与上面演示的一样,都有下面这几种选项:

ded46c57d792467c9c22c1957b0b86bf.png 这些都是宏像我们刚刚演示的#define的那样。下面我们演示这个接口:

//系统层面
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define LOG "log.txt"
int main()
{
  int fd = open(LOG,O_WRONLY);
  printf("fd:%d\n",fd);
  close(fd);
  return 0;
}

 0_CREAT是如果没有这个文件就给我们创建一个文件,O_WRONLY是只写的意思。

26eed91ff2a043a1959127329ff92c40.png

 我们先删除原先的log文件,运行后发现返回值为-1,也就是说出错了,那是什么原因呢?我们修改一下代码如果出错就打印一下出错原因:

e82987679bca408dbfde187b3bac2f22.png

23c4470e521e47c4872fb4fcb2f98c8b.png

因为我们要写这个文件,但是这个文件并不存在,所以我们没有的话应该先创建一个文件:

f9d24ccf8c94494d9440a3ea6dade890.png

19d6d81111b44e0bba71a773abfdaddb.png

 这次我们发现文件创建成功了,错误码为0。我们这里用的是root所以文件的权限没有问题,如果这里是普通用户创建文件的话文件权限是乱码有问题的,如下图:

54c0640266414b69b5971c6241213e45.png

 所以我们一般创建文件用的是三个接口的那个函数,我们演示的这个函数没有提供文件权限的接口。

836cf421c1c744d9966bdafe148f1655.png

 下面我们用三个接口的open创建一下文件:

d4a86f29b9e74edfbc2abab990c7ceb1.png

63484de293c84158850e4218d952f320.png

 我们先删掉之前的文件,然后重新运行一下发现这次文件的权限没有问题是正确的(不是666是因为受umask影响)。所以,一个文件被创建默认权限受umask影响,那么如果想不受umask影响呢?

b200727d86834cedbaafafcbbe8f0703.png

我们可以直接将所有的权限掩码设置为0:

f23d58fa140a4737872ee28e42e13343.png 8c6d6e03bf2f431aae700f7b8f38432d.png

 我们发现这次就不受umask影响了。接下来我们写入文件试试:

关于写入的文件接口的函数为write:

2e5a4e10ee7b4c5e8af7a8d75fee4944.png

 向文件描述符写入一个缓冲区大小为count,返回值是实际写入多少字节,写入失败返回-1.

int main()
{
  umask(0);
  int fd = open(LOG,O_WRONLY | O_CREAT,0666);
  if (fd==-1)
  {
    printf("fd:%d,errno:%d,errstring:%s\n",fd,errno,strerror(errno));
  }
  else printf("fd:%d,errno:%d,errstring:%s\n",fd,errno,strerror(errno));
  printf("fd:%d\n",fd);
  const char* msg = "hello friend";
  int cnt = 5;
  while (cnt)
  {
    char line[128];
    snprintf(line,sizeof(line),"%s,%d\n",msg,cnt);
    write(fd,line,strlen(line));
    --cnt;
  }
  close(fd);
  return 0;
}

接下来我们运行一下:

ac5e4beaed3540afbb8a1bc69049302d.png

 通过上图我们发现写入成功,其实在我们刚刚strlen的时候发现我们并没有带\0,为什么没有\0也能成功写入呢,因为\0是C语言的标准不是系统的规定,并且\0在系统中写入会变成乱码。

下面我们重新写一下:

7f10b275c7ef477bad949c4a9f737270.png

ed5e1f7c5d66420b99d5e6250c48eb19.png

 在我们重新运行后发现文件中保留了上一次的数据,也就是说O_CREAT | O_WRONLY是不会清空原来的文件再写入的,这个时候我们加上O_TRUNC选项就可以每次写入前先清空文件了:

1c4eb69d6df44093b2b4b60f2eead12f.png

791fa3dc96a54d3fb7fd60fd2f00e5c9.png

 搞定了写入那么饿追加就不是问题了,追加只需要注意将写入替换为追加,并且我们不希望每次打开文件先清空文件,所以不需要O_TRUNC选项:

f8e39f73532d4533b43a35eb79d7fde4.png

22efbf0b454b4973b77e181f6f253aea.png

 为什么我们上面的运行结果不正确呢?因为0_APPEND只是追加不是追加写,所以才没有写入。下面我们修改一下代码:

74ede980ace7475da6d2eafba70c8683.png

f39e110369584cb3ae869902b4a276ca.png

 这样我们就成功的追加了字符串,那么如果是只读呢?其实只读就很简单了,只读只需要调用两个接口的open函数即可,因为只读默认认为是有文件的。

9bbef5925f90420b9ed3b558a6f643e5.png

 读取的返回值就是当前读取了多少字节,读取到文件结尾就是0,失败就是-1。

int main()
{
  umask(0);
  int fd = open(LOG,O_RDONLY);
  if (fd==-1)
  {
    printf("fd:%d,errno:%d,errstring:%s\n",fd,errno,strerror(errno));
  }
  else printf("fd:%d,errno:%d,errstring:%s\n",fd,errno,strerror(errno));
  char buffer[1024];
  ssize_t n = read(fd,buffer,sizeof(buffer)-1); //使用系统接口来进行IO的时候,一定要注意\0问题。
  if (n>0)
  {
    buffer[n] = '\0';
    printf("%s\n",buffer);
  }
  close(fd);
  return 0;
}

c04ac4cc81d243aabc25cf2e6f80fa10.png

 我们在读取的时候为什么-1呢?因为我们不考虑\0的问题,只有当读取的时候返回值大于0我们将\0放到n的位置以字符串的形式打印的时候才需要\0,所以最后需要将\0放到n的位置再打印。

以上就是系统级的文件接口,而入C语言等要进行文件操作都必须调用系统接口。

二、文件描述符

1.0 & 1 & 2

通过对open函数的学习,我们知道了文件描述符就是一个小整数。linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2。0 1 2对应的物理设备一般是:键盘,显示器,显示器,所以输出还可以有以下方式:

下面我们用文件的读写完成简单的输入输出:

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

int main()
{
  char buf[1024];
  ssize_t s = read(0,buf,sizeof(buf));
  if (s>0)
  {
    buf[s] = 0;
    write(1,buf,strlen(buf));
    write(2,buf,strlen(buf));
  }
  return 0;
}

 1和2代表标准输出和标准错误,我们先去读一下缓冲区,当返回值是缓冲区的最后一个字符,我们在这个位置放入\0,然后写入刚刚读取到的字符,下面我们运行一下:

47b5b5784b704676aeda8962fb08b2cb.png

 通过这个小例子我们就知道了文件描述符就是从0开始的小整数,当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,于是就有了file结构体,表示一个已经打开的文件对象,而进程执行open系统调用,所以必须让进程和文件关联起来,每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

文件描述符的分配规则

我们写一个只读的代码来获取文件的fd:

int main()
{
  int fd = open("my.txt",O_RDONLY);
  if (fd<0)
  {
    perror("open");
    return 1;
  }
  printf("fd:%d\n",fd);
  close(fd);
  return 0;
}

ca683570a2d34bda840101b991def3a4.png

 我们发现fd是3,那么这个时候我们把0关闭了再看看是什么结果:

int main()
{
  close(0);
  int fd = open("my.txt",O_RDONLY);
  if (fd<0)
  {
    perror("open");
    return 1;
  }
  printf("fd:%d\n",fd);
  close(fd);
  return 0;
}

04da7ea731724a0fb444113ae6b9bd55.png

 通过运行结果我们发现,当我们将0关闭后文件描述符立马变成了0,我们再看看把2关闭了如何:

int main()
{
  //close(0);
  close(2);
  int fd = open("my.txt",O_RDONLY);
  if (fd<0)
  {
    perror("open");
    return 1;
  }
  printf("fd:%d\n",fd);
  close(fd);
  return 0;
}

ec82e3f6f34a42588786d6d5f60f46bf.png

 当我们将2关闭后文件描述符又变成了2,这就说明文件描述符的分配规则是:在file_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

2.重定向

还是刚刚的代码,如果我们将1关闭了会出现什么情况呢?

int main()
{
  //close(0);
  close(1);
  int fd = open("my.txt",O_WRONLY | O_CREAT,00644);
  if (fd<0)
  {
    perror("open");
    return 1;
  }
  printf("fd:%d\n",fd);
  fflush(stdout);
  close(fd);
  exit(0);
}

ca0bd90ac21443b88a3f895cf31fd55c.png

 通过运行结果我们可以发现,本来应该输出到显示器上的内容,输出到了文件中,其中fd=1,这种现象叫做输出重定向,常见的重定向有> >> <。那么重定向的本质是什么呢?我们看下图:

e03cac692e2e4c45a1bdec2d05d94bea.png

 当然对于重定向来说还有一个系统调用接口,这个函数叫dup2:

#include <unistd.h>
int dup2(int oldfd, int newfd);

对于这个函数的参数1来说是被换的那个描述符,参数2是像0,1,2,这样的默认描述符,接下来我们使用这个函数完成以下重定向:

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

int main()
{
  int fd = open("my.txt",O_WRONLY | O_CREAT,00644);
  if (fd<0)
  {
    perror("open");
    return 1;
  }
  dup2(fd,1);
  printf("fd:%d\n",fd);
  close(fd);
}

a3c84da75e0748109c7702f3690d4404.png

 通过上图我们可以看到成功的重定向到文件中。下面我们看看文件缓冲区的概念:

我们先编写一段代码看看现象:

int main()
{
  //c库中的
  fprintf(stdout,"hello fprintf\n");
  //系统调用
  const char* msg = "hello write\n";
  write(1,msg,strlen(msg));
  //fork();
  return 0;
}

5dedd5dd9bdf43f08646b979766a759e.png

 可以正常打印,那么我们调用一下子进程会变成什么呢?

269261c8d26a4f9d8e665a2273f42e1c.png

8cced6275c8a430d81155683837ca885.png我们发现运行结果和刚刚是一样的,难道没有什么用吗?我们接下来重定向到文件中:

88fdddb8526441c58b93c3215debcda3.png 这个时候我们发现了,居然会多打印一个fprintf,为什么会出现这样的现象呢?

一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据
的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲

所以:

printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
行缓冲:碰到\0就刷新缓冲区
全缓冲:缓冲区满了才刷新
显示器采用的刷新策略:行缓冲。普通文件采用的刷新策略:全缓冲
那么缓冲区在哪里呢?在你进行fopen打开文件的时候,你会得到FILE结构体,缓冲区就在这个FILE结构体中。

总结

对于linux下的文件操作而言,C语言等对于文件操作的函数都是经过linux系统文件接口来封装的,我们在用C语言文件操作的时候看着很简单的一句代码在系统调用中会有很多的操作,对于文件描述符实际上就是file_struct中的指针数组的下标,文件描述符的分配规则就是优先从最小的下标开始分配

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/130028821
今日推荐