当前位置: 首页 > news >正文

【Linux】文件操作

目录

一、知识铺垫

二、回顾一下之前C语言的文件操作,并对比重定向

1、w选项与输出重定向(>)

2、a选项与追加重定向(>>)

3、熟悉一下读写操作

4、练习读操作:

三、什么叫做当前路径?当前路径与文件创建的关系?

四、访问文件的系统调用

1、程序默认打开的文件流

​编辑

2、常见的读写函数:

3、open系统调用:

4、一个小细节:用位图传参:

5、open参数解析:

6、写文件的系统调用:write

7、关闭文件的系统调用:close

五、文件描述符(fd)

1、认识文件描述符

2、文件描述符具体是什么?为什么后续访问文件的系统调用都要通过fd来操作?

3、一切皆文件

4、文件描述符表的分配规则以及利用规则实现重定向

(1)文件描述符表的分配规则

(2)改变重定向的系统调用(dup2)

(3)dup2使用场景

5、给自定义shell增加重定向功能

六、缓冲区问题:

1、简单介绍

2、为什么使用缓冲区能提高效率?

3、缓冲区在哪里?

4、用代码证明缓冲区的存在

七、模拟实现文件操作的常用接口(有缓冲区和无缓冲区版本)

1、无缓冲区版本

mystdio.h

mystdio.c

filetest.c

2、有缓冲区版本

mystdio.c

mystdio.h

filetest.c


前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家

点击跳转到网站

一、知识铺垫

(1)文件 = 内容 + 属性

(2)访问文件之前,都得先打开该文件。修改文件都是通过执行代码的方式完成修改。

(3)打开文件前提要文件必须加载到内存中

(4)由谁打开文件?进程打开文件。

(5)一个进程可以打开多少文件?可以打开多个文件

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

(所以内核中一定要有描述被打开文件的结构体,并用其定义对象)。

(7)进程和文件的关系:结构体之间的关系,struct task_struct 和 struct XXX

(8)系统中是不是所有的文件都被进程打开了?答案是:并不是,那些没有被打开的文件是被存储磁盘中的,所以也叫磁盘文件。

二、回顾一下之前C语言的文件操作,并对比重定向

#include<stdio.h>
int main()
{FILE *fp = fopen("./log.txt","w");if(fp == NULL){perror("fopen");return 1;}const char* str = "hello file\n";fputs(str,fp);fclose(fp);return 0;
}

1、w选项与输出重定向(>)

以" w "选项打开文件,是对文件进行写操作,但是打开前会将文件原有内容清空。而重定向(" > ")也是会将文件原有内容清空,因为重定向之前需要将文件打开,而打开这个操作就会将文件内容清空:

2、a选项与追加重定向(>>)

fopen以"a"选项打开文件,是追加的方式进行写,即在文件原有内容的末尾接着写,这与追加重定向功能一样:

#include<stdio.h>
int main()
{FILE *fp = fopen("./log.txt","a");if(fp == NULL){perror("fopen");return 1;}const char* str = "hello file\n";fputs(str,fp);fclose(fp);return 0;
}

还有其他选项可以查手册。

3、熟悉一下读写操作

#include<stdio.h>
#include<string.h>#define FILENAME "log.txt"//练习读写操作
int main()
{FILE* fp = fopen(FILENAME,"w");if(fp == NULL){perror("fopen");return 1;}const char* msg = "hello HF";int cnt = 6;while(cnt){int n = fwrite(msg,strlen(msg),1,fp);printf("write %d block\n",n);cnt--;}fclose(fp);return 0;
}

4、练习读操作:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>#define FILENAME "log.txt"//练习读
int main()
{FILE* fp = fopen(FILENAME,"r");if(fp == NULL){perror("fopen");return 1;}char buffer[64];while(1){char* r = fgets(buffer,sizeof(buffer),fp);if(!r) break;//返回NULL则终止读printf("%s\n",buffer);}return 0;
}

三、什么叫做当前路径?当前路径与文件创建的关系?

当前路径指进程启动时所在的工作目录。进程启动时,会自动记录自己启动时的所在的目录,可通过指令查看:(ls  /proc/进程pid -l)

当前路径与文件创建的关系:以前都以为文件是默认创建在可执行程序的同级目录,实则不然,文件是默认创建在进程的工作目录下。

如果我们在创建文件之前,修改进程的工作目录,那么文件也会创建到修改后的工作目录下:

修改进程的工作目录的接口:

参数就是要修改的工作目录。

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>#define FILENAME "HF.txt"//修改进程的工作目录
int main()
{int i = chdir("/root/study/linux-learning");if(i){printf("转换失败\n");}FILE* fp = fopen(FILENAME,"w");if(fp == NULL){perror("fopen");return 1;}const char* msg = "hello HF";int cnt = 6;while(cnt){int n = fwrite(msg,strlen(msg),1,fp);printf("write %d block\n",n);cnt--;}fclose(fp);return 0;
}

四、访问文件的系统调用

1、程序默认打开的文件流

2、常见的读写函数:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>#define FILENAME "log.txt"//常见的写函数
int main()
{printf("hello printf\n");fprintf(stdout,"hello fprintf\n");//将数据输出到标准输出中(stdout显示器设备);fputs("hello fputs\n",stdout);//也是将数据输出到标准输出中,但不能像fprintf那样支持格式化输出const char* msg = "hello fwrite\n";fwrite(msg,1,strlen(msg),stdout);return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>#define FILENAME "log.txt"int main()
{//fscanfchar buffer[64];fscanf(stdin,"%s",buffer);//从标准输出(键盘)中读取数据放到buffer中,空格和换行符作为分隔符printf("%s\n",buffer);return 0;
}

3、open系统调用:

访问文件不仅仅有C语言的文件接口,OS还必须提供对应的访问文件的系统调用,就是open系列的系统调用:

4、一个小细节:用位图传参:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>#define FILENAME "log.txt"//用位图传参
#define ONE 1
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)void myPrint(int flag)
{if(flag & ONE) printf("1");if(flag & TWO) printf("2");if(flag & THREE) printf("3");if(flag & FOUR) printf("4");if(flag & FIVE) printf("5");printf("\n");
}int main()
{myPrint(ONE);myPrint(TWO);myPrint(ONE | TWO);myPrint(THREE | FOUR | FIVE);myPrint(FIVE);return 0;
}

5、open参数解析:

(1)参数一:pathname,是要打开或者创建的文件路径名(绝对路径/相对路径)

(2)参数二:flags,标志位,表示打开文件的方式,具体值是宏定义(可查看手册),传参方式类似于第4点的位图传参方式,可以通过按位或(|)设置多个标志。

注意:使用这些标志位需要包含头文件:<fcntl.h> 

三个基本标志位:

标志作用
O_RDONLY只能读取,不能写入,若进行写操作会返回 EBADF 错误
O_WRONLY只能写入,不能读取,需配合 O_CREAT 创建新文件
O_RDWR可同时读取和写入,写入可能覆盖原有内容,需控制偏移量

其他标志如下:

标志作用
O_CREAT如果文件不存在,则创建它(需配合第三个参数 mode 使用)。
O_EXCL与 O_CREAT 联用,若文件已存在则返回错误(可用于避免文件被意外覆盖)。
O_TRUNC若文件存在且为可写模式,将其长度截断为 0(即打开文件前先清空文件内容)。
O_APPEND追加写,写入时始终追加到文件末尾(自动将文件偏移量设置到文件末尾)。
O_NONBLOCK以非阻塞模式打开文件(用于 I/O 多路复用,如网络编程)。

(3)参数三:mode,用于指定新建文件的权限,仅当使用 O_CREAT 或 O_TMPFILE 标志时生效。有时候,如果不指定mode参数,那么创建的文件的权限可能会乱码:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h> #define FILENAME "log.txt"//使用open
int main()
{int fd = open("HF.txt",O_WRONLY | O_CREAT);if(fd == -1){perror("open");return 1;}return 0;
}

如图会发现创建的文件的权限位乱码的,所以此时我们需要使用第三个参数code解决:

但受系统权限掩码的限制,会导致创建的文件的权限与我们设置的权限不一样,此时需要提前使用一个系统调用:umask(0),头文件: <sys/stat.h> 

解释:用于设置当前进程的文件创建掩码(file creation mask)为 0。文件创建掩码是一个位掩码,用于在创建文件或目录时屏蔽某些权限位,从而控制新创建文件的默认权限。

此时就与我们设置的权限一样了。

6、写文件的系统调用:write

参数解析:

参数名称数据类型描述
fdint文件描述符,指向已打开的文件、管道、套接字或设备(如标准输入stdin对应 fd = 0;标准输出stdout对应 fd = 1;标准错误stderr对应 fd = 2)。
通过open系统调用获取,用于标识写入目标。
bufconst void *写入缓冲区指针,它是一个指向用户空间缓冲区的指针,这个缓冲区里存储着准备写入的数据。数据的传输方向是从 用户空间(buf)到内核空间(fd 对应的设备或文件)。
可以是字符数组、结构体或其他数据类型,需确保内存访问权限合法。
countsize_t要写入的字节数,指定从buf中读取的最大数据量。
实际写入字节数可能小于count(如遇到文件末尾、磁盘空间不足或权限限制)。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h> 
#include<sys/stat.h> 
#define FILENAME "log.txt"int main()
{umask(0);int fd = open("HF.txt",O_WRONLY | O_CREAT,0666);const char* str = "hello write\n";write(fd,str,strlen(str));return 0;
}

注意:

7、关闭文件的系统调用:close

五、文件描述符(fd)

1、认识文件描述符

这是一个及其重要的概念,文件描述符也就是open函数的返回值,我们先看看值是什么?

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h> 
#include<sys/stat.h> 
#define FILENAME "log.txt"//认识文件描述符
int main()
{int fd1 = open("HF1.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);int fd2 = open("HF2.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);int fd3 = open("HF3.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);int fd4 = open("HF4.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);int fd5 = open("HF5.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);printf("fd1:%d\n",fd1);printf("fd2:%d\n",fd2);printf("fd3:%d\n",fd3);printf("fd4:%d\n",fd4);printf("fd5:%d\n",fd5);return 0;
}

可以看到值其实就是整形,但为什么是从3开始呐?

因为0,1,2端口已经默认被三个标准占用了:

标准输入(stdin):0

标准输出(stdout):1

标准错误(stderr):2

由上述知识我们可以知道,C语言的相关文件接口,本质就是封装了各个系统调用,主要是为了保证自己的跨平台性。

2、文件描述符具体是什么?为什么后续访问文件的系统调用都要通过fd来操作?

上面我们知道,进程要管理打开的文件需要先描述后组织。

(1)首先task_struct中存在一个成员变量(struct file_struct *files),这个成员指向的结构体(struct file_struct)里面存在一个成员,该成员表示进程打开的文件描述符表。

(2)所谓文件描述符表也就是一个数组,其数组类型为(struct file**   fd_array[ ]),这是一个二级指针,其内容的类型为(struct file*)

(3)struct file是文件结构体,里面包含了文件属性、方法集、文件运行时的状态信息、操作函数和资源引用等等信息(如下图),被称为文件操作的 “控制器”。

(4)而open返回值fd(文件描述符)就是这个文件描述符表的下标,有了这个下标,我们就可以找到下标对应的struct file,从而就可以操作这个文件,所以后续访问文件的系统调用都要通过fd来操作的。

3、一切皆文件

4、文件描述符表的分配规则以及利用规则实现重定向

(1)文件描述符表的分配规则

文件描述符表的分配规则:会叫最小的没有被使用的下标,分配给最新打开的文件。

输出重定向的现象:

(2)改变重定向的系统调用(dup2)

我们先学习dup2系统调用,参数解析如下:

(1)oldfd:已存在的、有效的文件描述符,指向一个已打开的文件、设备或套接字。

若 oldfd 无效(如未打开或已关闭),dup2() 返回 -1 并设置 errno=EBADF

(2)newfd:新绑定的文件描述符,dup2() 会将文件对象以前的文件描述符oldfd解绑,然后与 newfd进行绑定

若 newfd 未打开
直接将 newfd 指向 oldfd 对应的 struct file 对象。

若 newfd 已打开
先关闭 newfd(减少其原 struct file 的引用计数),再复制 oldfd 的文件对象到 newfd

若 newfd == oldfd
不执行任何操作,直接返回 newfd(避免自我关闭)。

(3)dup2使用场景

重定向到文件:

//使用dup2
int main()
{int fd = open("newfile",O_WRONLY|O_CREAT|O_TRUNC,0666);dup2(fd,1);//将newfile文件与标准输出进行绑定//这样printf就会默认向上述文件进行输出printf("hello newfile\n");return 0;
}

从文件读取内容到数组:

int main()
{int fd = open("newfile",O_RDONLY,0666);dup2(fd,0);//将文件的文件描述符与标准输出进行绑定char buffer[1024];while(1){//默认情况,stdin会从键盘中读取,若键盘不输入,是会发生阻塞的char* s = fgets(buffer,sizeof(buffer),stdin);//此时stdin会默认从文件中读取if(s==NULL)break;printf("file content:%s",buffer);}return 0;
}

5、给自定义shell增加重定向功能

六、缓冲区问题:

1、简单介绍

缓冲区其实是一块内存区域,目的是用来提高使用者的效率(空间换时间)。

比如从云南到北京运送货物,总共需要运送100kg,如果一次运送10kg,需要来回10次,这样花费的时间就非常多;如果我用比较大的运输机一次就能运送100kg,这样就用运送一次,大大提高了效率。

2、为什么使用缓冲区能提高效率?

注意:平时我们所说的,包括这里即将减少的缓冲区都是语言层面的缓冲区(比如C语言里面的缓冲区),与OS内核中的缓冲区没有关系。

为什么使用语言层面的缓冲区能提高效率?

结合上述运送物资的例子,我们知道通过系统调用访问OS是需要有很大开销的,如果我们语言层面不设置缓冲区,那么来一点数据就送给OS,又来一点数据又会访问OS,这样就会多出很多开销,但如果我们在语言层设置一个缓冲区,让需要存储的数据线一点一点累积保存到缓冲区,达到一定的空间后,我们一次性传输给OS,这样访问OS的次数就大大减少了,从而就提高了效率。

3、缓冲区在哪里?

缓冲区是在FILE结构体中,也就是由FILE结构体来维护缓冲区:

缓冲区常见字段:

struct _IO_FILE {// 基础文件描述符int _fileno;// 缓冲区指针与状态char* _IO_read_ptr;      // 读缓冲区当前位置char* _IO_read_end;      // 读缓冲区结束位置char* _IO_read_base;     // 读缓冲区起始位置char* _IO_write_base;    // 写缓冲区起始位置char* _IO_write_ptr;     // 写缓冲区当前位置char* _IO_write_end;     // 写缓冲区结束位置char* _IO_buf_base;      // 缓冲区基址char* _IO_buf_end;       // 缓冲区结束地址// 缓冲区状态标志int _IO_write_base;      // 写缓冲区起始位置(重复字段,实际为标志位)unsigned _flags;         // 缓冲区标志(如是否全缓冲、行缓冲等)unsigned _IO_file_flags; // 文件状态标志// 缓冲区大小与类型int _IO_buf_size;        // 缓冲区大小int _mode;               // 读写模式// ... 其他字段(省略)
};

核心字段:

4、用代码证明缓冲区的存在

//证明缓冲区的存在
int main()
{//使用系统调用const char* s1 = "hello write\n";write(1,s1,strlen(s1));//使用C语言接口const char* s2 = "hello fprintf\n";fprintf(stdout,"%s",s2);const char* s3 = "hello fwrite\n";fwrite(s3,strlen(s3),1,stdout);fork();return 0;
}

如果我们直接运行那么就会正常打印,因为显示器是行刷新(即写完一行就刷新数据(\n))

但如果我们重定向到某个文件,会发现一个奇怪的现象:

C语言接口的内容会存在两份,而系统调用的接口内容只有一份

为我们使用了重定向,重定向的刷新策略是全缓存刷新(即缓冲区满了才刷新数据),但很显然代码中的两条内容是塞不满缓冲区的,所以此时会一直等待,最后会遇到fork创建子进程,而刷新数据也属于修改数据的一种方式,父子进程中任意一个进程修改共享数据时,都会进行写实拷贝,最后进程结束,缓冲区强迫刷新,父子进程都会向文件中刷新数据,所以C语言接口的数据会存在两份,而系统调用接口write会直接将内容存在系统内部的缓冲区,此时内容与父子进程无关,所以只有一份数据。

其次printf、scanf等等函数的格式化输出也与缓冲区有关,可以搜索了解了解。

七、模拟实现文件操作的常用接口(有缓冲区和无缓冲区版本)

1、无缓冲区版本

mystdio.h

#pragma once
#include<stdio.h>typedef struct _myFILE
{int fileno;
}myFILE;myFILE* my_fopen(const char* pathname,const char* mode);
int my_fwrite(myFILE* fp,const char* fs,int size);
//int my_fread();
void my_fclose(myFILE* fp);

mystdio.c

#include "mystdio.h"
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>myFILE *my_fopen(const char *pathname, const char *mode)
{int flag = 0;if (strcmp(mode, "r") == 0){flag |= O_RDONLY;}else if (strcmp(mode, "w") == 0){flag |= (O_CREAT | O_WRONLY | O_TRUNC);}else if (strcmp(mode, "a") == 0){flag |= (O_CREAT | O_WRONLY | O_APPEND);}else{return NULL;}int fd = 0;if (flag & O_WRONLY){umask(0);fd = open(pathname, flag, 0666);}else{fd = open(pathname, flag);}if (fd < 0)return NULL;myFILE *fp = (myFILE *)malloc(sizeof(myFILE));if (fp == NULL)return NULL;fp->fileno = fd;return fp;
}int my_fwrite(myFILE* fp,const char* s,int size)
{return write(fp->fileno,s,size);
}void my_fclose(myFILE* fp)
{close(fp->fileno);free(fp);
}

filetest.c

#include"mystdio.h"
#include<string.h>const char* filename = "./log.txt";int main()
{myFILE* fp = my_fopen(filename,"w");if(fp == NULL) return 1;const char* s = "hello myflie\n";my_fwrite(fp,s,strlen(s));my_fclose(fp);return 0;
}

2、有缓冲区版本

mystdio.c

#include "mystdio.h"
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>myFILE *my_fopen(const char *pathname, const char *mode)
{int flag = 0;if (strcmp(mode, "r") == 0){flag |= O_RDONLY;}else if (strcmp(mode, "w") == 0){flag |= (O_CREAT | O_WRONLY | O_TRUNC);}else if (strcmp(mode, "a") == 0){flag |= (O_CREAT | O_WRONLY | O_APPEND);}else{return NULL;}int fd = 0;if (flag & O_WRONLY){umask(0);fd = open(pathname, flag, 0666);}else{fd = open(pathname, flag);}if (fd < 0)return NULL;myFILE *fp = (myFILE *)malloc(sizeof(myFILE));if (fp == NULL)return NULL;fp->fileno = fd;fp->cap = SIZE;fp->pos = 0;fp->flush_mode = LINE_FLUSH;return fp;
}void my_fflush(myFILE* fp)
{if(fp->pos == 0) return;write(fp->fileno,fp->outbuffer,fp->pos);fp->pos = 0;
}int my_fwrite(myFILE* fp,const char* s,int size)
{//向缓冲区写入memcpy(fp->outbuffer+fp->pos,s,size);fp->pos += size;if((fp->flush_mode & LINE_FLUSH) && fp->outbuffer[fp->pos-1] == '\n'){my_fflush(fp);}else if((fp->flush_mode & LINE_FLUSH)&&fp->pos == fp->cap){my_fflush(fp);}return size;//return write(fp->fileno,s,size);
}const char* toString(int flag)
{if(flag & NONE_FLUSH) return "None";else if(flag & LINE_FLUSH)return "Line";else if(flag & FULL_FLUSH)return "FULL";return "err";
}void DebugPrint(myFILE* fp)
{printf("outbuffer:%s\n",fp->outbuffer);printf("fd:%d\npos:%d\ncap:%d\nflush_node:%s\n",fp->fileno,fp->pos,fp->cap,toString(fp->flush_mode));
}void my_fclose(myFILE* fp)
{my_fflush(fp);close(fp->fileno);free(fp);
}

mystdio.h

#pragma once
#include<stdio.h>#define SIZE 4096//缓冲区大小
#define NONE_FLUSH (1<<1)//无自动刷新
#define LINE_FLUSH (1<<2)//行刷新
#define FULL_FLUSH (1<<3)//全刷新typedef struct _myFILE
{//char inbuffer[];char outbuffer[SIZE];int pos;int cap;int flush_mode;int fileno;
}myFILE;myFILE* my_fopen(const char* pathname,const char* mode);
int my_fwrite(myFILE* fp,const char* fs,int size);
//int my_fread();
void my_fflush(myFILE* fp);
void DebugPrint(myFILE* fp);
void my_fclose(myFILE* fp);

filetest.c

#include "mystdio.h"
#include <string.h>
#include <unistd.h>const char *filename = "./log.txt";int main()
{myFILE *fp = my_fopen(filename, "w");if (fp == NULL)return 1;int cnt = 5;char buffer[64];while (cnt){snprintf(buffer, sizeof(buffer), "helloworld,hellobit,cnt:%d", cnt--);my_fwrite(fp, buffer, strlen(buffer));DebugPrint(fp);sleep(2);}my_fclose(fp);return 0;
}

相关文章:

  • 【PDF PicKiller】PDF批量删除固定位置图片工具,默认解密,可去一般图、背景图、水印图!
  • 排序算法总结(C++)
  • win中将pdf转为图片
  • Python读取PDF:文本、图片与文档属性
  • git提交代码和解决冲突修复bug
  • PDF 转 Markdown
  • java 实现excel文件转pdf | 无水印 | 无限制
  • LangChain【6】之输出解析器:结构化LLM响应的关键工具
  • 佰力博科技与您探讨材料介电性能测试的影响因素
  • Mysql中select查询语句的执行过程
  • 埃文科技智能数据引擎产品入选《中国网络安全细分领域产品名录》
  • MySQL基础(三)DQL(Data Query Language,数据查询语言)
  • vue+elementui 网站首页顶部菜单上下布局
  • 七、数据库的完整性
  • keysight是德科技N9923A网络分析仪
  • 【NLP中向量化方式】序号化,亚编码,词袋法等
  • vb监测Excel两个单元格变化,达到阈值响铃
  • Excel 发现此工作表中有一处或多处公式引用错误。请检查公式中的单元格引用、区域名称、已定义名称以及到其他工作簿的链接是否均正确无误。弹窗
  • ArcGIS安装时输入localhost不被识别
  • Vue在线预览excel、word、ppt等格式数据。
  • 阜宁做网站哪家公司好/软文推广多少钱一篇
  • 建设网站哪家最好/新闻今天
  • python不用框架做动态网站/seo外包大型公司
  • 做新媒体每天必看的网站/专门搜索知乎内容的搜索引擎
  • 刷单做任务的网站/新出的app推广在哪找
  • 半导体网站建设/东莞企业网站排名