点了网站域名会跳转,昆山住房城乡建设局网站查询,广告设计与制作专业简历,wordpress标题前缀文章目录 一、文件相关基础知识二、文件操作1.C语言文件操作2.操作系统文件操作2.1 比特位传递选项2.2 文件相关系统调用2.3 文件操作接口的使用 三、文件描述符fd1.什么是文件描述符2.文件描述符的分配规则 一、文件相关基础知识
我们对文件有如下的认识#xff1a; 1.文件 … 文章目录 一、文件相关基础知识二、文件操作1.C语言文件操作2.操作系统文件操作2.1 比特位传递选项2.2 文件相关系统调用2.3 文件操作接口的使用 三、文件描述符fd1.什么是文件描述符2.文件描述符的分配规则 一、文件相关基础知识
我们对文件有如下的认识 1.文件 文件内容 文件属性即文件包括文件的内容和属性两个部分 2.空文件只是文件的内容为空但是文件的属性不为空所以空文件也需要占据磁盘的空间 3.由于文件包括内容和属性那么我们对文件的操作可以分为对文件内容非操作对文件的属性进行操作以及对文件的内容和属性进行操作 3.Linux/windows中目录都采用多叉树的形式进行表示即树的中间节点表示目录树的叶子节点表示文件所以我们可以使用文件路径文件名来唯一的标识一个文件 4.在对文件进行访问的时候如果没有指定文件的路径那么默认在当前路径下对文件进行访问当前路径指的是当前进程所在的工作路径 5.在C语言中当我们把fopen,fclose,fwrite,fread等函数接口的程序编译链接形成可执行程序如果我们运行该可执行程序那么对应的函数就不会被调用则对应的文件操作也就不会被执行因为函数在运行时才会建立栈帧所以对文件的操作本质上是进程对文件的操作 6.我们知道我们要访问一个文件那么就必须先打开这个文件而文件存储在磁盘上由于计算机结构的原因磁盘上的文件必须通过OS才能和进程进行交互所以文件打开是由用户进程和OS配合来完成的—用户进程调用文件的接口OS系统实现这些系统调用接口 7.磁盘上存在许许多多的文件并不是所有的这些文件都被打开了所以文件分为被打开的文件和没有被打开的文件那些没有被打开的文件被成为文件系统 所以文件操作的本质是进程与被打开文件的关系
二、文件操作
在谈论文件操作之前我们需要了解 语言层面上的文件操作与操作系统层面上的文件操作的关系
我们知道每一种语言都有其对应的文件操作包括面向过程语言C面向对象语言C/java静态编译语言go解释型语言python甚至包括脚本语言shell等等但是每一种语言对文件操作提供的接口都不相同这样就会导致我们的学习成本变得很高。
站在语言的角度我们觉得是这样的但是站在操作系统的角度就不是这样了 我们知道计算机的软硬件体系结构之后就会知道操作系统为了同时满足 保护自身安全 与 为上层用户提供良好的(稳定的安全的高效的)服务会给用户提供访问软硬件的系统调用接口同时为了降低用户使用成本人们又在系统调用接口的基础上开发了用户操作接口其中包括shell外壳与各种语言的函数库而用户就是通过调用用户操作接口类完成指令开发与管理等操作 也就是说站在操作系统的角度虽然每一种语言的文件操作接口都不一样但是这些接口底层调用的一定是同一种系统调用接口因为操作系统是计算机管理软硬件资源的软件进程想要访问文件只能通过调用操作系统提供的系统调用接口我们使用的fopenfwrite,fclose等等接口底层也是调用系统调用接口
而系统调用接口只有一套语言有无数种每一种又不一样那么我们学习文件操作只需要学习操作系统提供的系统调用中有关文件操作的接口即可学习了系统调用就相当于学习了底层以后我们再学习语言的文件操作时只需要学习一些新的方式即可但是底层是不变的这样就会大大降低学习的成本
1.C语言文件操作
在学习C语言的文件操作之前我们先回顾一下C语言的操作函数
C语言文件操作接口
函数名函数功能fopen打开指定文件fclose关闭指定文件fwrite以二进制的形式向文件中写入数据fread以二进制的形式从文件中读取数据fscanf把文件中的数据格式化的读取到内存中fprintf把内存中的数据格式化的写入到文件中
总结 fopen用于打开文件并返回一个文件指针可以指定不同的打开模式如只读、只写、追加等和文件类型文本或二进制。配合其他文件操作函数使用如fprintf、fscanf等。 fclose用于关闭文件关闭后文件指针指向的文件将不可访问。 fread从文件中读取数据到指定的内存缓冲区中可以指定要读取的数据块的大小和数量。 fwrite将指定大小的数据块从内存缓冲区写入文件中可以指定要写入的数据块的大小和数量。 fseek设置文件指针的位置用于定位读写位置。可以通过设置相对于文件开头、文件末尾或当前位置的偏移量来移动文件指针。 ftell获取文件指针的当前位置返回当前位置的偏移量。 rewind将文件指针重置到文件的起始位置相当于调用fseek(file, 0, SEEK_SET)。 feof检查文件指针是否已到达文件末尾返回非零值表示到达文件末尾。 fgets从文件中读取一行数据到指定的字符数组中。 fprintf将格式化的数据写入文件中。
需要注意的是在使用文件操作接口时应当检查返回值以确保操作是否成功并且在不需要使用文件时要及时关闭文件以释放资源。
C语言文件打开的几种方式
文件打开方式含义如果指定文件不存在“r”(只读)为了输入数据打开一个已经存在的文本文件出错“w”(只写)为了输出数据打开一个文本文件建立一个新的文件“a”(追加)向文本文件尾部添加数据建立一个新的文件“rb”(只读)为了输入数据打开一个二进制文件出错“wb”(只写)为了输出数据打开一个二进制文件建立一个新的文件“ab”(追加)向一个二进制文件尾部添加数据出错“r”(读写)为了读和写打开一个文件文件出错“w”(读写)为了读和写建立一个新的文件建立一个新的文件“a”(读写)打开一个文件在文件的尾部进行读写建立一个新的文件
总结
r只读以只读方式打开文件。文件必须存在否则打开失败。w只写以只写方式打开文件。如果文件存在则文件内容会被截断为空如果文件不存在则会创建新文件。a追加写以追加写入方式打开文件。如果文件存在则新的数据会追加到文件末尾如果文件不存在则会创建新文件。r读写以读写方式打开文件。文件必须存在允许读取和写入文件内容。w读写创建新文件以读写方式打开文件。如果文件存在则文件内容会被截断为空如果文件不存在则会创建新文件。a追加读写以追加读写方式打开文件。如果文件存在则新的数据会追加到文件末尾如果文件不存在则会创建新文件。允许读取和写入文件内容。
需要注意的是以上文件打开模式只适用于文本文件。对于二进制文件可以在以上模式后添加b来表示二进制模式例如rb、wb等。
此外还有一些其他的文件打开模式如rb、wb等。这些模式在于读取和写入的组合方式具体的操作方式和特性与上述模式类似但会有一些细微的区别。
在选择文件打开模式时需要根据具体的需求和操作来确定使用哪种模式。例如如果需要只读取文件内容则使用r模式如果需要追加写入内容则使用a或a模式如果需要同时读取和写入文件内容则使用r、w或a模式具体选择根据是否需要创建新文件或截断文件内容来决定。
C语言文件操作的例子
1.向文件中写入数据
#include stdio.h#define FILE_NAME log.txtint main()
{FILE *fp fopen(FILE_NAME, w);if (NULL fp){perror(fopen failed);return 1;}int cnt 5;while (cnt){fprintf(fp,%s:%d\n,hello world,cnt--);}fclose(fp);return 0;
}2.从文件中读取数据
#include stdio.h
#include string.h
#define FILE_NAME log.txtint main()
{FILE *fp fopen(FILE_NAME, r);if (NULL fp){perror(fopen failed);return 1;}char buffer[64];while(fgets(buffer,sizeof (buffer)-1,fp)!NULL){buffer[strlen(buffer) - 1] 0;puts(buffer);}fclose(fp);return 0;
}注意
1.r(只读),w(只写), r(读写,不存在出错),w(读写, 不存在创建), a(append, 追加), a()
2.以w方式单纯的打开文件无论是否写入数据c语言会自动清空内部的数据
3.通过fwrite创建出来的文件log.txt其权限是664这是由于普通文件的默认权限为0666linux默认的umask为0002而文件的最终权限等于默认权限 ~umask所以log.txt的权限为0664 2.操作系统文件操作
2.1 比特位传递选项
C语言常通过一个整形来传递选项但是当选项较多时每一个选项都用一个整形来进行传递那么函数的参数就会很多这时就提出了使用一个比特位来传递一个选项这样一个整形有32个比特位就可以传递32种选项多个传递时使用 | 运算即可具体案例如下
#include stdio.h// 每一个宏只占用一个比特位该比特位为1说明该选项成立且各个宏的位置不重叠
#define OPTION_ONE (1 0)
#define OPTION_TWO (1 1)
#define OPTION_TREE (1 2)
#define OPTION_FOUR (1 3)void show(int flags)
{// flags与上面哪个选项匹配就执行对应的操作// 按位与的结果为1说明flags对应的比特位为1if (flags OPTION_ONE)printf(OPTION_ONE\n);if (flags OPTION_TWO)printf(OPTION_TWO\n);if (flags OPTION_TREE)printf(OPTION_TREE\n);if (flags OPTION_FOUR)printf(OPTION_FOUR\n);
}int main()
{// 主函数中通过传递不同的选项来达到不同的效果show(OPTION_ONE);printf(-----------------------\n);show(OPTION_TWO);printf(-----------------------\n);show(OPTION_ONE | OPTION_TWO);printf(-----------------------\n);show(OPTION_ONE | OPTION_TWO | OPTION_TREE);printf(-----------------------\n);show(OPTION_ONE | OPTION_TWO | OPTION_TREE | OPTION_FOUR);printf(-----------------------\n);return 0;
}如上我们将宏与比特位对应然后在show函数中编写每一个宏对应的功能之后我们就可以在其他函数中通过调用show函数并传递对应的选项来达到我们想要的结果并且我们可以通过按位或来实现同时传递几个选项
2.2 文件相关系统调用
open close
函数功能
open打开或创建一个文件
close关闭一个文件 函数参数
int open(const char* pathname, int flags);
int open(const cahr *pathname, int flags, mode_t mode);
# 头文件sys/types.h sys/stat.h fcntl.h
# pathname: 文件路径/文件名
# flags: 打开文件时可以传入多个参数选项用一个或者多个宏常量进行“或”运算构成flags比特位传递选项
参数:O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR : 读写打开这三个常量必须指定一个且只能指定一个O_CREAT : 若文件不存在则创建它。需要使用mode选项来指明新文件的访问权限O_APPEND: 追加写
# mode: 指定创建新文件时文件的默认权限(文件最终权限还要受umask的影响)
# 函数返回值int文件打开或创建成功返回文件对应的文件描述符(整形)失败返回-1int close(int fd);
# 头文件unistd.h
# fd目标文件对应的文件描述符
# int函数返回值关闭成功返回0关闭失败返回-1文件打开方式-含义如果指定文件不存在O_RDONLY以只读形式打开出错O_WRONLY以只写形式打开出错O_RDWR以读写形式打开出错O_APPEND向文本文件尾添加数据出错O_CREAT如果文件不存在创建新文件建立一个新的文件O_TRUNC打开文件时清空文件中之前的数据出错
上述这些宏表示不同的文件打开方式其底层原理和我们上面讲的 通过比特位传递选项 是一样的我们可以在调用 open 函数时传递一个或多个宏来实现不同的文件打开方式。
同时我们可以通过文件操作的系统调用接口和封装后的C语言文件操作接口还是存在很多细节上的不同的如下 C语言以 “w” 的方式打开文件若文件不存在会自动创建一个新文件而系统调用目标文件不存在直接报错除非指定了 O_CREAT 选项 C语言以 “w” 方式打开文件时会自动清空之前文件中的数据而系统调用则是逐个字符进行覆盖并不会提前清空文件中的数据如果要清空必须指定 O_TRUNC 选项 需要注意的是O_CREAT 是一个建议性选项即当文件存在时我们传递此选项也不会报错同时文件不存在创建文件时需要传递 mode 选项来指定新文件的访问权限 上面这些细节的不同也从侧面印证了C语言文件操作接口是对系统调用接口的封装 – “w” 选项会自动清空旧数据、创建新文件又比如创建新文件时C语言不用手动传递 mode 选项指定权限等等这些细节都隐藏在了函数的具体实现中。
write 与 read
函数功能
write向文件中写数据 read从文件中读数据 函数参数
ssize_t write(int fd, const void* buf, size_t count);
# 头文件unistd.h
# fd目标文件的文件描述符
# buf要写入数据的来源
# count要写入数据的字节数
# ssize_t函数返回值写入成功返回成功写入的字节数写入失败返回-1ssize_t read(int fd, void* buf, size_t count);
# 头文件unistd.h
# fd目标文件的文件描述符
# buf读取数据存放的位置
# count要读取数据的字节数
# ssize_t函数返回值读取成功返回读取写入的字节数读到文件末尾返回0读取失败返回-12.3 文件操作接口的使用
操作系统系统调用文件相关接口的使用和C语言文件操作接口的使用总体上是差不多的只是一些细节上有所不同。
1.向文件中写数据
#include stdio.h
#include string.h
#include unistd.h
#include sys/types.h
#include fcntl.h#define FILE_NAME log.txtint main()
{// 创建文件并以只写形式打开并指定文件的默认权限为0666(还受umask的影响)// 同时我们可以通过umask接口手动设置当前进程的文件掩码而不使用从父进程继承过来的umaskumask(0000);int fd open(FILE_NAME, O_WRONLY | O_CREAT, 0666);if (fd 0){perror(open failed);return 1;}int cnt 5;char buffer[64];while (cnt){sprintf(buffer, %s:%d\n, hello world, cnt--);// 注意这里strlen求得的长度不用加1因为字符串以\0结尾只是C语言的特性而文件中并不这样规定write(fd, buffer, strlen(buffer));}close(fd);return 0;
}注意
1.建 文件时我们通过 umask 系统调用将 umask 设置为了 0000(第一个0代表八进制)然后将 mode 设置为 0666所以 文件的最终权限为 默认权限 ~umask – 0666 ~0000 0666 2.向文件中写入数据时如果不指定 O_TRUNC 选项新数据就会逐字节覆盖原数据所以有时候就会出现下面只覆盖了一部分原数据的情况 3.C语言中字符串以 ‘\0’ 结尾但是文件中字符串并不以 ‘\0’ 结尾所以我们向文件中写入字符串时count 设置为 strlen(str) 就行不用把最后面的 ‘\0’ 字符加上如果加上了就会出现部分乱码 2.从文件中读数据
#include string.h
#include unistd.h
#include sys/types.h
#include fcntl.h#define FILE_NAME log.txtint main()
{// 创建文件并以只写形式打开并指定文件的默认权限为0666(还受umask的影响)// 同时我们可以通过umask接口手动设置当前进程的文件掩码而不使用从父进程继承过来的umaskumask(0000);int fd open(FILE_NAME, RD_ONLY, 0666);if (fd 0){perror(open failed);return 1;}char buffer[1024];ssize_t num read(fd, buffer, sizeof(buffer) - 1);if (num 0)buffer[num] 0; // 0, \0, NULL - 0printf(%s, buffer);close(fd);return 0;
}由于C语言字符串以 ‘\0’ 结尾而文件中的字符串数据并不包含 ‘\0’所以这里我们需要预留一个位置便于在数据量大于等于1024字节这种极端情况下 buffer中仍有空间来放置 ‘\0’。
三、文件描述符fd
1.什么是文件描述符
我们知道文件操作本质上是进程与被打开文件之间的关系同时一个进程可以打开多个文件且操作系统同时运行着许多个进程那么操作系统中就一定存在着大量被打开的文件那这些被打开的文件要不要被操作系统管理起来呢答案是肯定的。
如何管理呢 答案是先描述再组织即将文件的所有属性都总结到一个结构体中并为每一个文件都创建一个结构体对象再用一种数据结构将这些结构体对象组织起来这样对众多被打开文件的管理就变成了对某一种数据结构的增删查改Linux 中用于管理文件的内核数据结构叫做 struct file {} 结构体其中包含了文件的大部分属性。
进程如何知道哪些被打开文件属于它呢如图 进程的 task_struct 里面有一个 struct files_struct *files 指针变量它指向一个属于该进程的数据结构对象 struct files_struct该对象里面包含了一个指针数组 struct file* fd_array[]即进程的文件描述符表数组里面的每个元素都是指针指向一个 struct file 对象而这个数组的下标就是我们用户得到的文件描述符 fd。
也就是说进程可以通过进程控制块中的 files 变量找到 files_struct 结构体再通过 files_struct 中的文件描述符表具体下标中保存的地址找到具体文件的内核数据结构 file从而实现数据的读取与写入。
总结现在知道文件描述符就是从0开始的小整数当我们打开文件时操作系统在内存中要创建相应的数据结构来描述目标文件于是就有了 file 结构体表示一个已经打开的文件对象。而进程执行 open 系统调用所以必须让进程和文件关联起来于是每个进程都有一个 *files 指针指向一张表 files_struct该表最重要的部分就是包含一个指针数组数组中每个元素都是一个指向打开文件的指针。所以本质上文件描述符就是该数组的下标因此只要拿着文件描述符就可以找到对应的文件。 所以文件描述符是从0开始的小整数其本质是文件描述符表中的数组下标。
2.文件描述符的分配规则
我们知道了文件描述符是什么那么文件描述符是如何进行分配的呢
#include stdio.h
#include string.h
#include unistd.h
#include sys/types.h
#include fcntl.h#define FILE_NAME(number) log.txt#numberint main()
{int fd1 open(FILE_NAME(1),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd2 open(FILE_NAME(2),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd3 open(FILE_NAME(3),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd4 open(FILE_NAME(4),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd5 open(FILE_NAME(5),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);close(fd1);close(fd2);close(fd3);close(fd4);close(fd5);return 0;
}注C语言 # 在宏当中的作用 – 将参数插入到字符串中。
从运行结果可以看到文件描述符是连续分配且依次增大的这也很合理因为文件描述符本质上是数组下标而连续增长正好是数组下标的特性但是这里有一个很奇怪的地方 – 文件描述符是从3开始的那么0、1、2号下标呢这是由三个默认打开的标准流引起的。
标准输入、标准输出与标准错误流
我们在运行一个程序的时候操作系统会自动为我们打开三个流 – 标准输入流 stdin、标准输出流 stdout、标准错误流 stderr它们分别对应键盘文件、显示器文件与显示器文件其文件描述符分别是 0号、1号和2号所以我们打开其他文件时 fd 默认是从3号开始分配的。即Linux进程默认情况下会有3个缺省打开的文件描述符分别是标准输入0 标准输出1 标准错误2.0,1,2对应的物理设备一般是键盘显示器显示器
#include stdio.hint main() {printf(stdin-fd:%d\n, stdin-_fileno);printf(stdout-fd:%d\n, stdout-_fileno);printf(stderr-fd:%d\n, stderr-_fileno);return 0;
}注Linux 系统调用 open 接口的返回值是文件描述符 fd而C语言 fopen 接口的返回值是 FILE*其中 FILE 是一个结构体类型我们知道fopen 底层调用的是 open 接口而 fopen 又不使用 fd 作为函数返回值那么 FILE 结构体里面就一定会封装一个变量来表示 fdgcc 中这个变量是 _fileno 既然系统默认打开三个文件那么我们可不可以将其关闭呢当然可以
#include stdio.h
#include string.h
#include unistd.h
#include sys/types.h
#include fcntl.h#define FILE_NAME(number) log.txt#numberint main()
{close(0);close(2);int fd1 open(FILE_NAME(1),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd2 open(FILE_NAME(2),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd3 open(FILE_NAME(3),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd4 open(FILE_NAME(4),O_WRONLY | O_CREAT | O_TRUNC,0666);int fd5 open(FILE_NAME(5),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);close(fd1);close(fd2);close(fd3);close(fd4);close(fd5);return 0;
}可以看到当0号和2号文件描述符被关闭以后系统将其分配给了新打开的文件 log.txt1 和 log.txt2。
注close 关闭文件并不是将 fd 指向的 file 对象释放掉而仅仅是让当前进程文件描述符表中的对应下标不再指向该 file 对象因为同一个文件可能会被多个进程访问特别是父子进程。
(其底层可以采用 f_count **引用计数 **的方式来实现即当有指向该文件的进程关闭时文件计数减1有指向该文件的进程打开时文件计数加1当 f_count 为 0 时操作系统才释放该文件的内核数据结构即真正意义上的关闭文件)
所以文件描述符的分配规则是从小到大依次搜寻寻找未被使用的最小 fd 作为新打开文件的 fd即在files_struct数组当中找到当前没有被使用的最小的一个下标作为新的文件描述符