【Linux】Linux的文件操作(1)

简介: 【Linux】Linux的文件操作

一、关于文件的预备知识

  1. 文件 = 内容 + 属性,针对文件的操作都是对文件内容和属性的操作。
  2. 当我们没有打开文件时,文件位于磁盘中,当文件被打开时,文件就要操作系统加载到内存中,这是冯诺依曼体系结构对我们文件操作的限制!
  3. 文件是被进程打开的!
  4. 实际进程在运行的过程中肯定会打开多个文件,对于多个文件我们操作系统要将它们组织起来以便于管理和维护。所以当文件被打开时,操作系统要为被打开的文件创建相应的内核数据结构
  5. 为了管理和组织文件,Linux操作系统定义了一个struct file的结构体,里面记录了文件的各种属性以及文件内容的位置,每个struct file结构体里面又都有一个struct file*的指针,通过这个指针将我们的所有文件对象组织成了链表,于是我们对文件的操作就变成了对链表的增删查改。

二、Linux下文件读写的系统调用

Linux中关于文件读写的系统调用主要有以下几个接口,下面我们将一 一介绍:

openclosewriteread

1、open函数

我们打开man手册在2号目录里面可以看到open的函数原型以及介绍:

我们可以看到open函数有三个头文件,我们在使用时一个也不能落下!

我们还看到open函数有两个,这里我们先介绍第一个open函数。

  • 第一个参数是路径名称,这个路径可以是相对路径,也可以是绝对路径
  • 第二个参数是一个位图结构的参数,我们可以使用|来进行连接,从而一次传递多个参数。其参数主要如下:
  • O_RDONLY 只读打开                    (英文read only的缩写)
  • O_WRONLY 只写打开                    (英文write only的缩写)
  • O_CREAT 文件不存在就创建该文件              (英文creat)
  • O_TRUNC 文件每次打开都会进行清空             (英文truncate)
  • O_APPEND 文件写入时以追加的方式进行写入       (英文append)
  • 返回值:如果打开成功会返回一个大于或等于0整数,这个整数叫做文件表示符,用来标定一个文件,如果打开失败会返回-1表示打开失败。

我们先用第一个接口打开一个文件

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>
#define LOG  "mylog.txt"
int main()
{
  //由于是位图结构,我们传参时要用 | 将参数连接起来
  int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC); //以写的方式打开文件 ,如果文件不存在就创建。
  if(fd == -1)
  {
    perror( "open fail");
    exit(-1);
  }
  return 0;
}

第一次打开:

这里新创建的文件打开的权限有一点奇怪,我们删除再重新运行程序:

新创建的文件打开的权限还是有一点奇怪,而且和上一次的创建的文件权限竟然不同,我们再次删除再重新运行程序:

这一次就更加奇怪了,这时为什么呢?

因为我们使用的第一个open函数是没有指明创建后的文件权限是什么的,所以创建出的文件权限是一个随机值,这时我们就要考虑第二个open函数了!

  • 第三个参数 :mode_t是一种无符号整形,我们的第三个参数可以以8进制的方式传入我们创建的文件的权限。

修改我们上面的代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>
#define LOG  "mylog.txt"
int main()
{
  //由于是位图结构,我们传参时要用 | 将参数连接起来
  int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
  if(fd == -1)
  {
    perror( "open fail");
    exit(-1);
  }
  return 0;
}

运行结果:

最终的结果并不是:-rw-rw-rw- 这是为什么呢?

这是因为我们Linux的普通用户的权限掩码影响了我们文件创建的权限,我们可以使用系统调用umask()来进行设置我们进程的权限掩码。

运行下面的代码,我们得到了我们想要的结果:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>
#define LOG  "mylog.txt"
int main()
{
  umask(0)  //将权限掩码设置为0
  //由于是位图结构,我们传参时要用 | 将参数连接起来
  int fd = open(LOG, O_WRONLY | O_CREAT| O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
  if(fd == -1)
  {
    perror( "open fail");
    exit(-1);
  }
  return 0;
}

2、close函数

close是关闭文件,我们打开的文件最后都要进行关闭,不然会占用系统资源,因此我们上面的代码其实是不规范的。

  • 参数: 要关闭的文件标识符fd。
  • 返回值:如果执行成功返回0,执行失败返回 -1,并设置错误码。

下面就是使用Linux系统调用比较标准的打开和关闭操作。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>
#define LOG  "mylog.txt"
int main()
{
  umask(0)  //将权限掩码设置为0
  //打开文件
  int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
  if(fd == -1)
  {
    perror( "open fail");
    exit(-1);
  }
  //关闭文件
  close(fd);
  return 0;
}

3、write函数

write函数是写入函数,通过此函数我们能向文件中写入数据。

  • 第一个参数是文件标识符,也就是要写入的文件。
  • 第二个参数是一个指针,指向的是要写入的数据
  • 第三个参数是一个变量,表示最多写入多少个字节的数据
  • 返回值:实际写入的字节个数。

注意:需要注意的是我们使用系统调用时,不要将C语言的\0输入到文件中。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>
#define LOG  "mylog.txt"
int main()
{
  umask(0)  //将权限掩码设置为0
  //打开文件
  int fd = open(LOG, O_WRONLY | O_CREAT| O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
  if(fd == -1)
  {
    perror( "open fail");
    exit(-1);
  }
  const char* str1 = "hello world!\n" ;
  for(int i = 0; i < 5; ++i)
  {
    ssize_t n = write(fd, str1, strlen(str1));//strlen()后面不要 +1
    if(n < 0)
    {
      perror( "write fail: ");
      exit( -1);
    }
  }
  //关闭文件
  close(fd);
  return 0;
}

运行结果

4、read函数

read函数是读取函数,可以从文件中将数据读进变量中。

  • 第一个参数是:要读取的文件的文件标识符。
  • 第二个参数是:指针,这个指针指向了一块可以存储数据的空间。
  • 第三个参数是: 要读取的字节个数。
  • 返回值:实际读取的字节个数。

注意:read函数会读取换行符(\n)并将其作为普通字符处理,只有在读取到指定的字节数或者遇到文件的结束才会停止读取。

#include<stdio.h>    
#include<stdlib.h>    
#include<string.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#define LOG "mylog.txt"    
int main()    
{    
  int fd = open(LOG, O_RDONLY);    
  if(fd == -1)    
  {    
    perror("open fail");                                                                                                                                       
  }    
  //定义缓冲区 
  char str2[64];    
  for(int i = 0; i < 5; ++i)    
  {    
    ssize_t  n = read(fd, str2, sizeof(str2) - 1);    
    if(n < 0)    
    {    
      perror("write fail: ");    
      exit(-1);    
    }
    //注意在读取到的字符串后面加'\0'才是C风格的字符串    
    str2[n] = '\0';    
    printf("%s",str2);    
   }    
  close(fd);
}

三、文件描述符

我们知道:C语言的文件操作的函数是C库函数,库函数与系统调用之间的关系是:库函数封装了系统调用

我们还知道: 一个进程默认会打开三个流,标准输入流,标准输出流,标准错误流。

这三个流在C/C++中默认是:

  • C中: stdinstdoutstderr
  • C++中 cincoutcerr

1、理解文件描述符

我们知道C语言打开一个文件之后,会返回一个FILE*的指针,我们的C的三个标准流也是FILE*的指针,并且它们分别指向了 键盘文件显示器文件显示器文件。当我们尝试去向这标准输出流或者是标准错误流中去输入数据,其实就是向显示器文件进行数据写入,当我们尝试向标准输入流中读取数据,其实就是对键盘文件进行数据读取。

既然是文件,那么在Linux下进行文件操作必须要有文件描述符,那么我们C程序(以及其他编程语言写的程序)默认打开的三个流,也要有文件描述符,只有有了文件描述符我们系统才能找到对应的文件。

实际上C程序(以及其他编程语言写的程序)形成的进程默认打开的三个流在Linux中对应的文件描述符都是:

文件描述符
标准输入流 0
标准输出流 1
标准错误流 2

观察它们的文件描述符的数字规律,我们可以思考一下我们的进程在打开默认的三个流之后再打开一个文件,那么这个文件的文件描述符是多少?

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define LOG "mylog.txt"
int main()
{
    int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd == -1)
    {    
      perror("open fail");    
      exit(-1);
    }    
    //打印出文件标识符
    printf("%d\n",fd);   
    close(fd);         
    return 0;
}

没有错!是3!你可能会觉得这里和数组的下标好像啊!其实文件描述符就是数组的下标

我们以前说过操作系统要为我们打开的文件创建一个struct file对象用来组织管理被打开的文件,我们又知道文件是被进程打开的,那么进程一定要知道操作系统为文件创建的struct file在哪里,所以在内核数据结构中,还要有一张表,这张表里面记录了每一个被打开的文件的struct file的位置。

于是操作系统又会为我们创建一个struct file_struct结构,这个结构里面有一个指针struct file* fd_array,指向了一个指针数组,指针数组里面存储的就是struct file对象的地址,而我们得到的文件描述符其实就是这个指针数组的下标。

我们进程的PCB(在Linux下是task_struct)里面存放一个struct file_struct*指针,通过这个指针能够找到struct file_struct结构,通过这个结构我们能找到文件描述符的指针数组,通过指针数组,我们能够找到struct file对象,通过这个对象我们又能够文件的内容数据。

2、文件描述符的分配规则

有了文件描述符的理解以后我们就要讨论下一个问题了,文件描述符是怎么分配的呢?

有了上面的经验我们可能会猜测文件描述符的分配规则是:files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

那么实际上是不是这样呢?我们可以进行实验来确定我们的结果,假设我们在打开新的文件时,先关闭标准输入流,那么0号位置的文件标识符就被空出来了,我们再进行打开文件,看新打开的文件标识符是不是0。

实验代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define LOG "mylog.txt"
int main()
{
  close(0);
    int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd == -1)
    {    
      perror("open fail");    
      exit(-1);
    }    
    //打印出文件标识符
    printf("%d\n",fd);   
    close(fd);         
    return 0;
}

结果说明我们猜想的结论是正确的!

四、理解Linux下一切皆文件

学习过Linux的人大多数都听过一句话:“Liunx下一切皆文件”,这一点对于文件操作非常重要,下面我们来谈一谈对于这句话的理解。

我们知道像:键盘,显示器,网卡… 这些都是硬件,根本不是文件,为什么我们能把它们当成文件去看待呢?

首先,我们在操作硬件时都是通过驱动程序进行操作硬件的,而对于不同的硬件它们的驱动程序肯定是不同的,例如键盘只能被读取而不能被写入,显示器只能被写入,而不能被读取,网卡既可以被读取又可以被写入 …

尽管它们的驱动是不同点,但是我们可以设计一个类,对于它们的进行封装,封装完以后,上面的调用者就看不到底层的内容了,上面的调用者只需要对于这个类进行操作就可以达到同样的效果,这样对于调用者来说,它的操作都是在操作这个类,它接触不到底层的驱动以及硬件,当然对于它来说它看到的是什么那就是什么了。

于是Linuxstruct file结构体封装这些驱动的函数指针,这样进程在使用键盘,显示器时就像是在使用文件一样,这样一来不管是普通文件还是硬件,对于进程来说都是文件。

而我们用户使用操作系统又是通过进程的方式来使用操作系统的,所以我们用户的视角和进程的视角是一样的,在我们用户来看想要让磁盘帮我们保存一些数据,我们只需要保存一下文件就行了,我们没有必要把磁盘拿出来,然后通过一些物理手段向磁盘中刻入数据,进程也是和我们用户一样,它也认为我只要保存一下文件就能完成任务了,所以对于我们用户和进程来说一切都是文件,我们也只能接触到文件,所以Linux下一切皆文件。

相关文章
|
4月前
|
Linux Windows
Linux系统中的文件操作
Linux系统中的文件操作
|
8月前
|
存储 Linux
Linux文件操作基础:快速入门指南和实用技巧
Linux文件操作基础:快速入门指南和实用技巧
50 0
|
6月前
|
存储 Linux 调度
【看表情包学Linux】系统下的文件操作 | 文件系统接口 | 系统调用与封装 | open,write,close 接口 | 系统传递标记位 O_RDWR,O_RDONLY,O_WRONLY...
【看表情包学Linux】系统下的文件操作 | 文件系统接口 | 系统调用与封装 | open,write,close 接口 | 系统传递标记位 O_RDWR,O_RDONLY,O_WRONLY...
41 1
|
4月前
|
Linux
基于 Linux 的文件操作 网络编程的最后一环
Linux下万物皆文件 在了解了客户端和服务器的函数调用之后,我们只需要了解下文件操作就能编写出属于自己的客户端和服务器了,还能让他们进行通信。
23 0
|
2月前
|
存储 Linux C语言
Linux系统下C语言的文件操作
Linux系统下C语言的文件操作
19 0
|
5月前
|
Java Shell Linux
Linux【脚本 01】简单Shell脚本实现定时备份文件、压缩、删除超时文件操作(showDoc文件备份脚本举例)
Linux【脚本 01】简单Shell脚本实现定时备份文件、压缩、删除超时文件操作(showDoc文件备份脚本举例)
110 0
|
29天前
|
Linux 测试技术 C语言
【Linux】应用编程之C语言文件操作
【Linux】应用编程之C语言文件操作
|
3月前
|
Linux
常用Linux命令 - 文件操作命令
常用Linux命令 - 文件操作命令
49 0
|
9月前
|
存储 Linux
Linux常用命令(2)——文件操作命令
Linux常用命令(2)——文件操作命令