Linux——文件的概念、操作和理解

引言

文件 = 文件内容 + 文件属性
要操作文件,就要先打开文件。根据冯诺依曼体系,只能操作内存中的数据。因此要先把文件内容加载到存储器,即内存中。

文件接口

语言层面的文件接口

FILE* fopen(const char *path, const char *mode);
int   fclose(FILE *fp);

size_t fwrite(const void *ptr, size_t size, size_t number, FILE *stream);
size_t fread(void *ptr, size_t size, size_t number, FILE *stream);
模式含义文件不存在时
r只读报错
w只写创建文件
a追加只写创建文件
rb二进制只读报错
wb二进制只写创建文件
ab二进制追加只写创建文件
r+读写报错
w+读写创建文件
a+追加读写创建文件
rb+二进制读写报错
wb+二进制读写创建文件
ab+二进制追加读写创建文件

一个文件不能同时读和写。

struct _iobuf {
    char *_ptr;     //文件输入的下一个位置
    int _cnt;       //当前缓冲区的相对位置
    char *_base;    //指基础位置(即是文件的起始位置) 
    int _flag;      //文件标志
    int _fileno;    //文件描述符id
    int _charbuf;   //检查缓冲区状况,如果无缓冲区则不读取
    int _bufsiz;    //文件缓冲区大小
    char *_tmpfname;//临时文件名
       };
typedef struct _iobuf FILE;

C语言的文件接口也是借助了操作系统的系统调用接口,封装了一层。

操作系统层面的文件接口

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname:文件路径,默认为进程创建时的路径
  • flags: 决定文件打开模式
  • mode: 创建文件时文件的权限
  • 返回值:文件描述符

man手册里提供了open的两个函数原型,看起来像是重载,但是C语言使不支持函数重载的。实际上是用到了可变参数。

__fortify_function int open (const char *__path, int __oflag, ...)

flags标志位

标志位含义
O_RDONLY只读
O_WRONLY只写
O_RDWR读写
O_APPEND追加
O_CREAT创建文件
O_TRUNC清空文件

实际上flag是一个位图,用32个bit位最多表示32种状态。

通过与操作就能判断是什么标志位,然后执行不同的逻辑。

文件权限

用第一个open接口创建的文件的权限是乱的。需要用第二个接口。

int fd = open("Log.txt", O_WRONLY | O_CREAT, 0666);
mode_t umask(mode_t mask);

但是权限的设置还和权限掩码umask有关。

权限掩码

权限掩码的目的是可以让开发者自定义文件的默认权限。

  • 默认目录文件权限,起始权限777,最终默认权限是775,111 111 101
  • 默认普通文件权限,起始权限666,最终默认权限是664,110 110 100

涉及权限掩码,umask,默认为0002
只关注后三位002,凡是在权限掩码中出现的权限,都不应该在最终权限中出现。
计算规则:最终权限 = 起始权限 & 权限掩码

目录文件普通文件
umask000 000 010000 000 010
~mask111 111 101110 110 100
默认权限111 111 111110 110 110
最终权限111 111 101110 110 100

文件描述符

open的返回值是文件描述符,用来描述进程打开的每一个文件,是一个非负整数。
Linux下一切皆文件,文件描述符可以用于IO,也可以用来操作设备,充当套接字等。当进程打开一个文件,内核会为这个文件分配一个最小的可用文件描述符。

一个进程会默认打开三个文件流,分别为标准输入,标准输出,标准错误流。
这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。

文件描述符表

一个进程可以打开多个文件,操作系统如何管理被打开的文件?
先描述再组织,维护内核数据结构,并用链表等管理起来。

task_struct 里有一个 struct files_struct *files,描述一个进程打开的文件集合
files_struct 里有一个 struct file * fd_array[NR_OPEN_DEFAULT],文件描述符表
元素是struct file,即每一个被打开的文件。

struct files_struct {
    atomic_t count;  // 记录的是共享文件描述符表的进程数量
// 省略大部分
    int next_fd;
    struct file * fd_array[NR_OPEN_DEFAULT];
};

可以通过ulimit指令调整文件描述符表的大小

打开一个文件,就是把文件的内容和属性从磁盘加载到内存,创建好file结构体,初始化后,从0号下标开始找,找到最小的可用文件描述符,把地址放入到fd_array中。

open函数打开文件成功时数组当中的指针个数增加,这个指针在数组当中的下标被返回。文件打开失败返回-1,因此,成功打开多个文件时返回的文件描述符就是递进的。

重定向

image.png
关闭标准流,再打开一个文件,可以实现重定向。或者使用系统接口dup和dup2

dup2(A,B)是将B重定向到A,如dup(fd,1),将原来输出到1中的内容输出到fd中。

umask(0);
int fd = open("Log.txt", O_CREAT | O_RDWR | O_TRUNC, 0666);
dup2(fd, 1); // 用fd替换1
printf("Hello\n");

标准输出和标准错误

标准输出的fd是1,错误是2,本质上都是指向显示器,目的是区分程序的错误提示和正常输出。错误可以打印到日志里,输出可以打印到显示器。且输出可以被重定向,错误不可以。
image.png

./a.out 1> output.txt 2> error.txt

缓冲区

缓冲区无处不在,文件IO也有缓冲区。

什么是缓冲区?

缓冲区就是一段内存,把要向指定流输出的内容先存在这段内存里,时机合适就一次性刷新。

为什么需要缓冲区?

因为内存与外设交互速度是非常慢的,进程先把内容放到缓冲区,就不用一直等待和外设交互,多次存储一次输出也可以减少IO的次数。这样做可以提高整体的效率。

缓冲区在哪里?

image.png
结果:立即打印write,三秒后才打印printf和fprintf。
而在printf和fprintf中加了换行后,会按顺序立即打印出来。

而printf和fprintf肯定都是封装了write,因此可以判断,库中还有缓冲区。这里是语言级别的缓冲区,实际上肯定还有操作系统级别的缓冲区,当刷新用户缓冲区的数据时,并不是直接将数据刷新到外设,而是先刷新到操作系统的缓冲区。

缓冲区的刷新策略

常规情况

  • 无缓冲:每次输入输出都直接系统调用,不经过用户空间的缓冲区。这种方式可以保证数据的实时性,但是系统调用的开销较大,且外设交互效率低。例如write就是无缓冲。
  • 行缓冲:每次输入输出都先写入用户空间的缓冲区,当遇到换行符或者缓冲区满时,才调用系统调用将数据传输到内核空间。可以提高效率,但是牺牲了一定的实时性,且需要占用资源,因为数据可能会停留在用户空间的缓冲区一段时间。例如printf和scanf就是行缓冲。
  • 全缓冲:每次输入输出操作都先写入用户空间的缓冲区,当缓冲区满时,才调用系统调用将数据传输到内核空间。实时性差,因为数据可能会停留在用户空间的缓冲区更长时间。例如磁盘文件或网络套接字的输入输出操作就是全缓冲的。

特殊情况:

  • 进程退出:当进程正常退出时,会自动关闭所有打开的文件描述符,缓冲区会被刷新。但是如果进程是异常终止,例如收到信号,那么缓冲区可能不会被刷新,导致数据丢失或不一致。
  • 主动刷新:调用如fflush或者stream的flush方法可以主动刷新缓冲区。
  • 缓冲区超时:有些系统或设备可能会设置缓冲区超时的机制,即如果缓冲区在一定时间内没有被刷新,就会强制刷新缓冲区。这样可以避免数据在缓冲区中停留过长时间,影响实时性。例如,在Linux中,对于网络套接字的输出操作,可以使用TCP_NODELAY选项来禁用Nagle算法,这样就可以避免数据在套接字缓冲区中等待合并,而是尽快发送出去。

奇怪的问题

image.png

当把fork屏蔽掉之后,没有重复。
image.png

原因

显示器是行刷新,磁盘文件是全缓冲。重定向时,三个语言级别函数会内容暂存在语言级别缓冲区,因为不是显示器文件而不能行缓冲,而write是无缓冲,所以顺序最先。
fork创建子进程,父子共享代码,数据以写时拷贝的形式继承,其中包含了语言级别的缓冲区。
进程退出之后,父子进程都要刷新缓冲区,写入到文件中,因此会有重复。

软硬链接

概念

软硬链接是Linux系统中两种不同的链接方式,用于实现文件的共享使用。
硬链接是通过索引节点(index_node)来连接的,一个文件可以有多个硬链接,它们共享同一个inode和数据块。是一个独立文件,有自己的iNode和iNode编号与文件内容,相当于快捷方式。

软链接是通过文件路径来进行连接的,它实际上是一个特殊的文件,其中包含了另一个文件的位置信息。不是独立文件,和目标文件使用同一个iNode。

硬链接和软链接有以下几点区别:

  • 硬链接不能跨文件系统,软链接可以。
  • 硬链接不能对目录进行链接,软链接可以。
  • 硬链接必须要有源文件存在,软链接可以对不存在的文件名进行链接。
  • 删除源文件后,硬链接不受影响,软链接失效。
  • 删除硬链接后,不影响源文件和其他硬链接,删除软链接后,不影响源文件。

操作

ln命令建立链接,UNlink命令接触链接。-s表示软链接,-h表示硬链接。
image.png

在删除了dir中的hello后,test下的硬链接同样能正常运行。删除了两者其中的一个,本质就是减少了一份iNode编号和文件的映射,也就是文件的硬连接数减少。

可以推测mv指令的实现原理。mv之后的iNode数不变。硬链接实际上是一份复制。吗?

实际上是在当前路径下新增一份被硬链接文件的文件名和iNode编号的映射关系。
并且可以看到,随着硬链接增多,被链接文件的硬链接数变成了3,类似引用计数的概念。

那为什么创建普通文件,他的硬连接数默认是1?
因为本身的文件名和iNode有一个映射关系,必定大于等于1。

那为什么目录默认是2?
目录本身是文件,所以会有在当前路径下的文件名和iNode编号的映射,这是1;在进入这个目录后,会有一个与当前这个目录形成映射,才有./exe的这种写法,这是2。
那这个iNode就可以类比成指针。而软链接是一个新的文件,其内容保存的是指向文件的所在路径。
image.png

此时dir的硬连接数是2,因为本身dir在当前路径就有一份文件名和iNode编号的映射,进入到dir后,还有一份dir中的 .

在dir1里创建一个目录后,硬链接数多了1,因为目录里一定有一个 … 和dir1链接起来。

所以不去自己硬链接的情况下,可以估算一个目录的下级目录中有多少个子目录,硬连接数-2。
image.png

模拟实现C文件接口

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

#define _bufferNum 512 // 缓冲区大小

#define NONE_FLUSH 0
#define LINE_FLUSH 1
#define FULL_FLUSH 2

typedef struct _myFile
{
  int _fileno;                  // 文件描述符
  char _buffer[_bufferNum];     // 语言级别缓冲区
  int _end;                     // 缓冲区内容的下标
  int _flushWay;                // 刷新策略
}myFile;

myFile* myFopen(const char* fileName, const char* mode)
{
  assert(fileName && mode);
  int _mode = O_RDONLY;
  if(strcmp(mode,"r") == 0)
  {}
  else if(strcmp(mode,"w") == 0)
  {
    _mode = O_CREAT | O_WRONLY | O_TRUNC;
  }
  else if(strcmp(mode,"a") == 0)
  {
    _mode = O_CREAT | O_WRONLY | O_APPEND;
  }
  int fileno = open(fileName, _mode, 0666);
  if(fileno < 0) return NULL;

  myFile* mfp = (myFile*)malloc(sizeof(myFile));
  if(mfp == NULL) return NULL;
  
  memset(mfp, 0, sizeof(myFile));
  mfp->_fileno = fileno;
  mfp->_flushWay |= LINE_FLUSH;
  mfp->_end = 0;
  return mfp;
}
  
void myFwrite(const char* buffer, size_t count, myFile* mfp)
{
  assert(buffer && mfp && (count > 0));
  // 先把内容写到缓冲区
  strncpy(mfp->_buffer + mfp->_end, buffer, count);
  mfp->_end += count;

  // 刷新策略 只写了行缓冲
  if(mfp->_flushWay &= LINE_FLUSH)
  {
    if(mfp->_end > 0 && mfp->_buffer[mfp->_end - 1] == '\n')
    {
      write(mfp->_fileno, mfp->_buffer, mfp->_end);
      mfp->_end = 0;
      syncfs(mfp->_fileno);
    }
  }
}

void myFflush(myFile* mfp)
{
  assert(mfp);
  if(mfp->_end > 0)
  {
    write(mfp->_fileno, mfp->_buffer, mfp->_end);
    mfp->_end = 0;
    syncfs(mfp->_fileno);
  }
}

void myFclose(myFile* mfp)
{
  myFflush(mfp);
  free(mfp);
}

通过系统调用把缓冲区内容写到内核里,不代表写到了硬件上。需要用到syncfs,syncfs是系统调用,作用是将文件系统中的所有缓存的修改(包括文件元数据和数据)写入到底层的文件系统,把数据刷到磁盘上。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值