Linux文件系统----内存级文件

铺垫

a.文件=内容+属性(对于空文件,内容是空的,但是属性不是空的,所以在磁盘中占据空间)

b.访问文件前,都得先打开。修改文件,都是通过执行代码的方式完成修改,文件必须要加载到内存中,通过CPU在内存中对文件修改。

c.谁打开文件?进程在打开文件。

d.一个进程可以打开多个文件。

e.系统中是不是所有的文件都被进程打开了?不是。没有被打开的文件在磁盘中。

正在被打开的文件——内存级文件,没有被打开的文件——磁盘文件

一段时间内,系统中存在多个进程,也可能同时存在更多的被打开的文件,OS要不要管理多个被进程打开的文件呢?肯定的。如何管理?先描述在组织。

首先打开一个文件

  1 #include<stdio.h>
  2 
  3 int main()
  4 {
  5   FILE *fp = fopen("./log.txt","w");//第二参数为打开的方式
  6   if(fp == NULL)
  7   {
  8     perror("fopen");
  9     return 1;
 10   }
 11   fclose(fp);                                                                                                                                        
 12   return 0;
 13 }

w:以写write方式打开文件,该文件内容会被自动清空。 >

a:以追加append方式打开文件,文件内容不会被清空。>>

fwrite(字符串,长度,写入基本单元个数,文件指针)

返回值是向文件写入基本单元的个数。

什么叫当前路径?

进程在启动时,会自动记录自己启动时所在的路径,所以文件创建时会创建在当前路径下。

进程启动路径决定文件创建的位置

例子:

  1 #include<stdio.h>
  2 #include<string.h>
  3 #include<sys/types.h>
  4 #include<unistd.h>
  5 int main()
  6 {
  7   FILE *fp = fopen("./log.txt","w");                                                                 
  8   if(fp == NULL)
  9   {
 10     perror("fopen");
 11     return 1;
 12   }
 13 
 14   const char* str = "hello world\n";
 15   int cnt = 5;
 16   while(cnt)
 17   {
 18     int n = fwrite(str,strlen(str),1,fp);
 19     printf("write %d block,pid is : %d\n",n,getpid());
 20     cnt--;
 21     sleep(20);
 22   }
 23 
 24   fclose(fp);
 25   return 0;
 26 }

 运行后获取进程id

 

找到该进程的位置

发现该进程中存有当前路径

程序默认打开的文件流

程序在启动时默认打开的文件流

stdin 标准输入

stdout 标准输出

stderr 标准错误

stdin,stdout,stderr可以直接被使用!

例子:

  1 #include<stdio.h>
  2 #include<string.h>
  3 int main()
  4 {
  5   printf("hello printf\n");
  6   fputs("hello fputs\n",stdout);                                                                     
  7   const char* msg = "hello fwrite\n";
  8   fwrite(msg,1,strlen(msg),stdout);
  9   fprintf(stdout,"hello fprintf\n");
 10   return 0;
 11 }

printf函数代码实现的过程中封装了stdout,使得字符串直接写入到了stdout,也就是c语言的写入接口都用到了它们,有参就直接用它们,无参,内部就封装了它们。

系统调用接口——open,write,close,read

访问文件不仅仅有c语言上的文件接口,OS必须提供文件的系统调用接口,语言上的文件接口封装了系统调用的接口。

open

#include <fcntl.h>       // 标志位定义
#include <sys/stat.h>    // mode_t 权限定义

int open(const char* pathname,int flags)//如果已有文件,按照flags方式打开文件

int open(const char* pathname,int flags,mode_t mode)//没有文件,要新建文件,需要给予权限,第三个参数是权限位

1.pathname(路径名)

要打开或创建的文件路径(绝对路径或相对路径)。

2.flags(标志位)

关于flags参数

O_RDONLY: 只读方式打开文件。
O_WRONLY: 只写方式打开文件。

O_RDWR: 读写方式打开文件。

O_CREAT: 如果文件不存在则创建新文件。
O_TRUNC: 如果文件存在且以写模式打开,则将其长度截断为零(就是将文件清空)。
O_APPEND: 以追加模式打开文件,即所有写入操作都将在文件末尾进行。

O_NONBLOCK: 以非阻塞模式打开文件。
O_SYNC: 以同步写入方式打开文件,确保写操作完成后数据被写入磁盘。

注:这些参数本质上是宏定义的一些整形,通过按位或( | )连接从而发生作用。

3.mode(权限位)

a.当 flags 包含 O_CREAT时需指定,定义新文件的权限(八进制数或宏,如 0644)。

b.权限受 umask 影响,实际权限为mode & (~umask)。

操作权限变化

设置 mode = 0666

期望权限:用户、组、其他人可读写(rw-rw-rw-

若 umask = 0000

实际权限:用户可读写,组和其他人只读(rw-rw-rw-

若umask = 0002

实际权限:用户和组可读写,其他人只读(rw-rw-r--

返回值

成功返回文件描述符(非负整数),通常从 3 开始(012 为标准输入、输出、错误)。

失败:返回 -1,并设置全局变量 errno 表示错误类型。

例子:

  1 #include<stdio.h>                                                                                    
  2 #include<fcntl.h>
  3 #include<sys/types.h>
  4 #include<sys/stat.h>     
  5 int main()
  6 {
  7   umask(0);//设置权限掩码为000,默认权限掩码是0002
  8   int fd = open("log.txt",O_WRONLY|O_CREAT,0666);//这时真正的权限为0666&(~权限掩码)
  9   if(fd==-1)
 10   {
 11     perror("open");
 12     return 1;
 13   }
 14   return 0;
 15 }

结果是创建了一个权限为rw-rw-rw-的log.txt的文件

注意open文件后应当检查

int fd = open(pathname, flags, mode);
if (fd == -1) {
    perror("open failed");
    // 根据 errno 处理
}

close

#include <unistd.h>
int close(int fd);

关闭一个文件,需要传入的参数是要关闭文件的文件描述符fd。 

返回值

成功:返回 0

失败:返回 -1,并设置全局变量 errno 表示错误类型。

常见错误类型(errno)

EBADFfd 不是有效的文件描述符。

EINTR:操作被信号中断(需特殊处理)。

EIO:底层 I/O 错误(如磁盘故障)。

注意close文件后检查

if (close(fd) == -1) {
    perror("close failed");
}

write

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

1.fd(文件描述符)

2.buf(缓冲区指针):指向用户空间中待写入数据的缓冲区地址。

3.count(字节数):期望写入的字节数的大小。

返回值

成功:返回实际写入的字节数(可能小于 count,甚至为 0)。

失败:返回 -1,并设置全局变量 error 表示错误类型。

read

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

1.fd(文件描述符)

2.buf(缓冲区指针):指向用户空间读取数据的缓冲区地址。

3.count(字节数):期望读取的最大字节数的大小。

返回值

成功:返回实际读取的字节数(可能小于 count,甚至为 0)。

失败:返回 -1,并设置全局变量 error 表示错误类型。

例子:

从标准输入读取数据:

char buffer[1024];
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));

 

通过系统调用接口open模拟实现c语言中fopen的功能

a.模拟FILE *fp = fopen("./log.txt","w");   

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

b.模拟FILE *fp = fopen("./log.txt","a");   

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

注:本质上c语言中的fopen接口封装了系统调用接口open,推而广之,很多c语言的接口都封装了系统调用接口!

FILE是c语言封装的结构体,里面封装了文件描述符(fd)

例子:

  int main()
  {
    printf("%d\n",stdin->_fileno);//stdin的类型是FILE*,成员变量_fileno就是文件描述符
    printf("%d\n",stdout->_fileno);
    printf("%d\n",stderr->_fileno);
    return 0;
  }

结果发现,原因是stdin,stdout,stderr是默认打开的,实际上这三个都对应了相应的fd。

数据流文件描述符(fd)
stdin 标准输入流(键盘文件)0
stdout 标准输出流(显示器文件)1
stderr 标准错误流(显示器文件)2

以后再新建文件时其文件描述符依次从2向后递增。

为什么C语言要封装?

为了保证可移植性。不同平台有不同平台的系统调用(Windows,linux……),用某一平台的系统调用写的一份代码,在其他平台下不一定能使用,如果用c语言对不同平台进行系统调用接口的封装,这样只用c语言的接口就可以在不同平台上使用同一份代码,保证了可移植性。

认识fd

用一张图来解释

这就解释了为什么访问文件,用系统调用接口,都必须传入文件描述符fd这个参数 

       ssize_t write(int fd, const void *buf, size_t count);

       ssize_t read(int fd, void *buf, size_t count);

       int close(int fd);

原因:进程调用系统接口时,传入文件描述符fd,这样通过进程的地址找到进程中对应的文件列表,根据fd,找到指针数组的对应的地址,从而找到需要操作的文件。

结论:文件描述符的本质就是数组的下标!

如何理解linux系统下一切皆是文件?

不同文件只要通过结构体中的函数指针指向对应的读写方法,不在乎硬件的差别,消除了底层硬件的差异,进程看待一切硬件都是以结构体struct file的形式,这就是linux系统下一切皆是文件。这种设计理念与C++多态也有关系。

文件描述符fd的分配规则

分配规则:最小的没有被使用的数组下标,会分配给最新的打开的文件。

利用分配规则来实现重定向。

例子:

  int main()
  {
    close(1);
    int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd: %d\n",fd);                                                                             
    return 0;
  }

结果是向log.txt文件中写入了fd:1,而不是打印在屏幕上。

原因: fd=1对应的文件是显示器文件,将显示器文件关闭后,再打开一个新的文件,根据fd的分配规则会将fd=1分配给log.txt,而printf默认向显示器文件中输出,实际上是向fd=1的文件输出所以,最后向log.txt中输出。

例子:

  int main()
  {
    close(1);//shuchu
    open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("printf\n");
    fprintf(stdout,"fprintf\n");
    return 0;                                                                                          
  }

两种方法都是向log.txt文件中写入。注意这里虽然fprintf中的参数是stdout但是仍然是向fd=1这个文件中输入。

另一种简单的重定向方法

OS系统提供相应的拷贝接口,只要把相应文件的拷贝到fd=1位置即可。

#include <unistd.h>

int dup(int oldfd);

int dup2(int oldfd, int newfd);

int dup3(int oldfd, int newfd, int flags);

介绍dup2

int dup2(int oldfd, int newfd);

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary

注意:newfd成为oldfd的拷贝,newfd变成oldfd。

例子:

  1 #include<stdio.h>
  2 #include<fcntl.h>
  3 #include<unistd.h>
  4 int main()
  5 {
  6   int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
  7   dup2(fd,1);//将fd位置的文件拷贝到fd=1(显示器文件)的位置
  8   printf("printf\n");
  9   return 0;
 10 }

例子:

  1 #include<stdio.h>
  2 #include<fcntl.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7   int fd = open("log.txt",O_RDONLY);
  8   dup2(fd,0);
  9   char buffer[1024];
 10   while(1)
 11   {
 12     char* s = fgets(buffer,sizeof(buffer),stdin);
 13     if(s==NULL)break;                                                                                
 14       printf("file content:%s",buffer);
 15   }
 16   return 0;
 17 }

缓冲区问题

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

为什么要有缓冲区?

提高使用者的效率。缓冲区聚集数据,一次拷贝,提高整体效率。

我们平常说的缓冲区和内核中的缓冲区没有关系,我们平时说的缓冲区是语言层面的缓冲区,是C语言自带的缓冲区。

我们知道fwrite/printf/fputs……底层封装了系统调用接口write……,但是fwrite/printf/fputs……  并不是直接调用系统调用接口,而是达成某种条件。其原理如下

语言层缓冲区的刷新策略主要是有以下几种:

1.无刷新,无缓冲(几乎没有使用)。

2.行刷新,行缓冲——显示器(stdout),xxx\n,遇到换行刷新,也可以手动刷新或缓冲区满了刷新。

3.全刷新,全缓冲——普通文件,缓冲区被写满才刷新。

a.强制刷新(fflush函数)。

b.进程退出的时候,要自动刷新。

C语言的缓冲区具体在哪里?

File *fp = fopen(“log.txt”,"w");

File是一个结构体,里面不仅封装了fd,还会维护一段缓冲区,所以每一个文件都有一个缓冲区

size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fputs(const char *s, FILE *stream);

通过例子证明c语言存在缓冲区

1.一段代码

  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 #include<string.h>                                             
  4                                        
  5 int main()                             
  6 {                                      
  7   //使用系统调用                       
  8   const char* s1 = "write\n";          
  9   write(1,s1,strlen(s1));              
 10                                        
 11   //使用C语言接口                      
 12   const char* s2 = "fprintf\n";        
 13   fprintf(stdout,"%s",s2);             
 14                                        
 15   const char* s3 ="fwrite\n";          
 16   fwrite(s3,strlen(s3),1,stdout);      
 17                                        
 18   return 0;                            
 19 }     

代码分析:

1. write 是系统调用,无缓冲,直接刷新。

2.fprintf 是C语言库中的函数,使用 stdout 的缓冲区。stdout输出到屏幕时刷新方案是行缓冲,遇到 \n 自动刷新缓冲区,字符串中包含‘\n’会刷新。

3.fwrite 是C语言库中的函数,写入数据到 stdout ,stdout输出到屏幕时刷新方案是行缓冲。字符串中包含‘\n’会刷新。

结果分析:

当输入到屏幕时,stdout的刷新策略是行缓冲,字符串结尾有‘\n’,所以结果就是都打印一遍。

当输入到文件时,stdout的刷新策略是全缓冲,所以是先打印write,fprintf与fwrite在c缓冲区中储存。当程序退出时自动刷新,所以结果一样。

直接运行打印到屏幕,没有问题。 

重定向到文件里,也没有问题。

2.然后在代码最后加上fork()。

  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 #include<string.h>                                             
  4                                        
  5 int main()                             
  6 {                                      
  7   //使用系统调用                       
  8   const char* s1 = "write\n";          
  9   write(1,s1,strlen(s1));              
 10                                        
 11   //使用C语言接口                      
 12   const char* s2 = "fprintf\n";        
 13   fprintf(stdout,"%s",s2);             
 14                                        
 15   const char* s3 ="fwrite\n";          
 16   fwrite(s3,strlen(s3),1,stdout);      
 17   
 18   fork();                                     
 19   return 0;                            
 20 }     

代码分析与上面一致。

结果分析:

直接运行打印到屏幕时,stdout的刷新策略仍是行缓冲所有输出在fork()调用前已刷新,父进程和子进程的缓冲区均为空,fork()不会复制任何未刷新数据,因此无重复输出。

当输出重定向到文件时,stdout 变为全缓冲模式,仅在三种情况下刷新:1.缓冲区填满 2.程序退出 3.显示调用 fflush(stdout)。注意:代码中的‘\n’不会触发刷新!

fork()前:

1.write是系统调用,不受语言级别的缓冲区影响,直接写入文件。

2. fprintf 和 fwrite 的输出会暂时存在于 stdout 的缓冲区中,没有被刷新。

fork()后:

fork()会复制父进程的缓冲区到子缓冲区,导致父缓冲区和子缓冲区都有一份未刷新的数据。

当程序退出时,父进程和子进程会分别刷新自己的缓冲区,导致 fprintf 和 fwrite 的输出被写入文件两次

所以有以下结果:

直接运行打印到屏幕,没有问题。  

但是重定向到文件,出现了差别。

注:对于C语言中的printf与scanf,当使用时,输入的内容实际上储存在C语言的缓冲区中,当达成某种条件时(强制刷新),才会通过系统调用接口进入系统内部的缓冲区,从而通过硬件输入或输出。

附录

c标准库的一些函数

fputs

#include <stdio.h>

int fputs(const char *str, FILE *stream);

参数说明

1.str(字符串):要写入的字符串(以 \0 结尾)。

2.stream(目标文件流指针):要打开或使用的文件流(如stdout)。需通过 fopen 打开或使用标准流。

返回值

成功:返回非负整数(通常是 0 或正值)。

失败:返回 EOF(通常是 -1),需通过 ferrror 或 errno 进一步检查错误原因。

例子:

fputs("Hello, World!\n", stdout); // 输出到屏幕
#include <stdio.h>

int main() {
    FILE *file = fopen("output.txt", "w");
    if (!file) {
        perror("fopen failed");
        return 1;
    }

    const char *text = "This is a line of text.\n";
    if (fputs(text, file) == EOF) {
        perror("fputs failed");
        fclose(file);
        return 1;
    }

    fclose(file); // 关闭文件会刷新缓冲区
    return 0;
}

 

fgets

#include <stdio.h>

char *fgets(char *str, int size, FILE *stream);

参数说明

1.str:用于存储读取数据的字符数组(缓冲区)。必须预先分配足够内存(通常为 size 字节)

2.size:最大读取字符数(包括结尾的 \0),通常设为缓冲区长度。实际最多读取 size - 1 个字符,最后一个位置自动填充 \0。

3.stream:输入文件流指针(如 stdin 、文件指针等)。注意需通过 fopen 打开或使用标准输入流。

返回值

成功:返回 str 的指针。

失败 或 到达文件末尾:返回 NULL,需通过ferror区分原因。

例子:

#include <stdio.h>

int main() {
    char buffer[100];
    printf("Enter a line: ");
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        printf("You entered: %s", buffer);
    } else {
        printf("Error or EOF reached.\n");
    }
    return 0;
}

 


fwrite

#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

参数说明

1.ptr:指向待写入数据的内存地址(可以是数组、结构体、基本类型变量等)。

2.size:每个数据单元的字节大小(如:sizeof(int))。

3.nmemb:要写入的数据单元数量(如:int a[10]有10个单元)。

误区 1:nmemb 必须等于数组长度

  • 错误:认为 nmemb 只能用于数组。

  • 正确nmemb 表示逻辑单元数量,可以是任意分割方式。例如:

int array[100];
// 将数组分为 4 个块,每块 25 个 int
fwrite(array, sizeof(int)*25, 4, file); // nmemb = 4(每个单元 25 个 int)

误区 2:size 必须等于数据类型大小

  • 错误:认为 size 必须匹配单个变量的大小。

  • 正确size 可以是任意值,只要与 nmemb 组合后匹配总字节数。例如:

char buffer[4096];
fwrite(buffer, 512, 8, file); // 将 4096 字节分为 8 个 512 字节的块

4.stream(目标文件流指针:要使用的文件流,注意需通过 fopen 打开文件流。

返回值

成功:返回实际写入的数据单元数量(若完整写入,返回值等于nmemb)。

失败:返回值小于nmemb,需通过ferror检查错误或文件结束。

例子:

#include <stdio.h>

typedef struct {
    int id;
    char name[32];
    float score;
} Student;

int main() {
    Student students[] = {
        {1, "Alice", 90.5},
        {2, "Bob", 85.0}
    };

    FILE *file = fopen("students.dat", "wb");
    if (!file) {
        perror("fopen failed");
        return 1;
    }

    // 写入整个结构体数组
    size_t num = sizeof(students) / sizeof(Student);
    size_t written = fwrite(students, sizeof(Student), num, file);

    if (written != num) {
        perror("fwrite failed");
        fclose(file);
        return 1;
    }

    fclose(file);
    return 0;
}

fread

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

参数说明 

1.ptr:指向存储读取数据的内存地址,要预留至少 size * nmemb 字节空间。

2.size:每个数据单元的字节大小。

3.nmemb:要读取的数据单元数量(如数组长度)。

4.stream:输入文件流指针。

返回值

成功:返回实际读取的数据单元数量(若完整读取,返回值等于nmemb)。

失败或文件结束:返回值小于 nmemb,需通过 ferror 进一步判断原因。

例子:

从 stream中读取 size*nmemb 字节数据到 ptr 指向的内存。

int data[10];
fread(data, sizeof(int), 10, file); // 读取 10 个 int

 

fprintf

#include <stdio.h>

int fprintf(FILE *stream, const char *format, ...);

参数说明

1.stream(目标文件流指针:要使用的文件流,注意需通过 fopen 打开文件流。

2.format:格式化字符串,包含普通字符和格式说明符(如 %d,%s)。格式说明符定义后续参数如何转换和输出。

通过格式说明符处理不同类型的数据:

格式符类型示例
%d整数int x = 10;
%f浮点数float y = 3.14;
%s字符串(以 \0 结尾)char *s = "text";
%c单个字符char c = 'A';
%p指针地址void *ptr;

3. …(可变参数):根据 format 中的格式说明符,传入对应的变量或值。

返回值

成功:返回写入的字符总数(不包括结尾的 \0)。

失败:返回负值(通常为 -1),可通过 ferror 或 errno 检查错误。

例子:

#include <stdio.h>

int main() {
    FILE *file = fopen("output.txt", "w");
    if (!file) {
        perror("fopen failed");
        return 1;
    }

    int count = 5;
    double value = 3.1415;
    fprintf(file, "Iteration: %d\nValue: %.2f\n", count, value); // 保留两位小数
    
    fclose(file);
    return 0;
}

fflush

#include <stdio.h>

int fflush(FILE *stream);

参数说明

1.stream:要刷新的文件流指针 或 特殊值 NULL:刷新所有打开的输出流(标准未强制要求,依赖具体实现)。

返回值

成功:返回0。

失败:返回EOF,并设置全局变量 error 表示错误类型。

例子:

刷新输出流:将缓冲区内的数据立即写入文件或设备。

示例:强制显示未换行的 printf 输出

printf("Processing...");
fflush(stdout); // 立即显示到终端

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘子13

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值