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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录


前言

理解文件原理和操作

我们先快速回忆下一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中的指针数组的下标文件描述符的分配规则就是优先从最小的下标开始分配

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux

“【linux】:linux下文件的使用以及文件描述符” 的相关文章