【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
参数解析:
参数名称 数据类型 描述 fd int 文件描述符,指向已打开的文件、管道、套接字或设备(如标准输入stdin对应 fd = 0;标准输出stdout对应 fd = 1;标准错误stderr对应 fd = 2)。
通过open
系统调用获取,用于标识写入目标。buf const void * 写入缓冲区指针,它是一个指向用户空间缓冲区的指针,这个缓冲区里存储着准备写入的数据。数据的传输方向是从 用户空间(buf)到内核空间(fd 对应的设备或文件)。
可以是字符数组、结构体或其他数据类型,需确保内存访问权限合法。count size_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; }