[Linux] Linux下的文件操作 及 Linux文件描述符fd 详解: open()、close()、write()、read()、文件描述符底层...
文件描述符
是一个至关重要的概念.Linux系统的关于内存文件系统的整个大致框架和逻辑
文件的相关概念
文件
文件=文件内容+文件属性
当一个文件的文件内容为空时, 此文件是否占用磁盘空间?
文件的内容为空, 其实此文件也是占用磁盘空间的
, 因为文件并不只有内容, 文件还有属性文件操作
文件=文件内容+文件属性
文件操作 = 文件内容操作 + 文件属性操作
总是需要先打开文件
的.打开文件的实际操作, 就是将文件的内容、文件的属性加载到内存中
, 当文件的内容、文件的属性被加载到内存中, 我们就称此文件被打开, 被打开的文件也被称为内存文件
, 与之对应的, 没有被打开的文件, 可以被称之为磁盘文件如何文件操作
文件的操作其实是由进程进行
的fopen() fclose() fread() fwrite()……
简单C语言文件读写操作
FILE *fopen(const char *path, const char *mode)
有表示读写权限的:
字符串 权限 说明 "r"
只读
只允许读取,不允许写入。文件必须存在,否则打开失败。 "w"
写入
若文件不存在,则创建一个新文件;若文件存在,则清空文件内容 "a"
追加
若文件不存在,则创建一个新文件;若文件存在,则将写入的数据追加到文件的末尾 "r+"
读写
既可以读取也可以写入。文件必须存在,否则打开失败 "w+"
写入
既可以读取也可以写入。若文件不存在,则创建一个新文件;若文件存在,则清空文件内容 "a+"
追加
既可以读取也可以写入。若文件不存在,则创建一个新文件;若文件存在,则将写入的数据追加到文件的末尾 还有表示读写方式的:
字符串 说明 "t"
以文本文件方式读写。 "b"
以二进制文件方式读写。 但是我们简单的复习, 就只考虑以文本文件方式读写, 不考虑二进制的方式
#include <stdio.h>
int main() {
FILE *pf = fopen("new_log.txt", "w+");
fprintf(pf, "88888888888\n");
fclose(pf);
return 0;
}
文件创建位置
在保证之前没有此文件的时候, 打开的文件会在那里创建?
使用fopen()打开没有指定路径的文件时, 进程会在其运行的当前路径创建文件, 而不是在可执行程序文件的所在路径
w写入规则
#include <stdio.h>
int main() {
FILE *pf = fopen("new_log.txt", "w+");
int cnt = 5;
while(cnt--) {
fprintf(pf, "88888888888\n");
}
fclose(pf);
return 0;
}
#include <stdio.h>
int main() {
FILE *pf = fopen("new_log.txt", "w+");
fprintf(pf, "88888888888\n");
fclose(pf);
return 0;
}
使用w向文件中写入数据会先将文件中的原内容清除
a写入规则
r+
w
w+
之外, 还存在另两种写入权限 a
a+
a
a+
打开文件时的规则与 w
w+
相同, 即 没有文件时创建文件.w
和 w+
写入的规则是, 先清空文件中原有的数据, 而 a
a+
则是在文件的末尾除追加数据:#include <stdio.h>
int main() {
FILE *pf = fopen("new_log.txt", "a+");
fprintf(pf, "222\n");
fclose(pf);
return 0;
}
追加重定向 >>
, 是否与a+
有相同的作用?模拟实现cat命令
#include <stdio.h>
#define SIZE 1024
int main(int argc, char *argv[]) {
if(argc != 2) { // 执行程序时 之后没有跟随一个文件时
printf("using: ./%s filename", argv[0]);
}
FILE *pf = fopen(argv[1], "r"); // 只读方式打开传入的文件
char buffer[SIZE];
while (fgets(buffer, sizeof(buffer), pf) != NULL) { // 从打开的文件中读取文本写入到buffer数组中
printf("%s", buffer);
}
return 0;
}
系统级文件接口有关问题
-
在Linux系统中, 我们向文件内写入数据, 本质上是否是向什么中写入数据?
向文件写入数据, 本质上其实是
向磁盘中写入数据
, 因为文件没有打开时, 本质上还是在磁盘中存储的 -
磁盘是硬件, 谁有资格向硬件中写入数据?
只有作为软硬件的管理者
操作系统, 有资格向硬件中写入数据
-
那么我们在上层访问文件的操作, 是否可以绕开操作系统?
上层访问文件的操作,
不可能绕开操作系统
, 访问文件本质上都是由操作系统操作的 -
操作系统如何给上层用户提供访问文件的操作?
操作系统可以给上层
提供系统调用
-
为什么C语言中没有见过也没有使用过系统调用?
因为, C语言中的不管是文件操作, 还是某些流的操作, 都对系统调用进行了封装
-
为什么语言要对系统调用进行封装?
首先, 原生的系统调用接口的使用并不是很简单的, 使用成本有点高(与封装后的接口相比)
其次, 原生的系统调用接口并不具备跨平台的功能,
不同平台相同的功能的系统调用接口是不同的
, Windows、Linux、MacOS等都是不同的, 所以语言需要对不同的平台的系统调用接口进行封装, 进而使语言具备跨平台的功能
-
封装如何解决不能跨平台的问题?
以C语言为例, C语言的fopen()操作 实际上可能是将所有支持的平台的关于打卡文件的系统调用接口穷举了一遍, 并结合条件编译 使fopen()实现了跨平台的功能
-
为什么要学习系统级的文件相关接口?
首先, 系统级的相关接口比起每种语言的接口来说, 一定更接近系统底层, 可以更加了解底层
其次, 学习系统调用之后, 对于各种语言的相关封装接口也可以有更加透彻的理解
Linux系统文件接口
Linux文件系统基本的接口
f
开头: fopen()
fclose()
fread()
fwrite()……
f
:open
close
open()
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
许多人的第一个疑问就是, Linux的底层是C语言写的, C语言不是没有函数重载吗?
为什么Linux可以提供两个同名不同参数的系统调用?
pathname 所需打开文件的所在路径
flags
需要传入的是什么呢? flags
需要传入的就是打开文件的选项
, 就像fopen()
中w
r
a
等mode
则是有关文件权限的参数. 在之前介绍Linux文件权限的文章中, 介绍过 Linux下创建文件, 系统会根据umask值来赋予新创建的文件一个默认的文件权限. 这个mode
参数就是用来传入打开文件需要修改成什么权限的数值
的open()接口的返回值, 被称为文件描述符fd
, 可以看作表示一个打开的文件pathname文件及路径
和 mode权限
没有太多需要注意的地方flags
flags参数
O_RDONLY(只读) O_WRONLY(只写) O_RDWR(读写) O_CREAT(创建)
这几个一眼就可以看出用法w 只写并可创建文件
r 只读
w+ 读写并可创建文件
r+ 读写不创建文件
……
, 并且C语言的fopen()也是对Linux系统中的open()做了封装的位图
. flags参数其实需要采用位图的方式传参一个整数的比特位表示flags参数中某个选项是否被选中
假设
这四个选项分别表示用一个整数的二进制的最低四位表示:00000000
(高-低) int一共32位, 但4个选项一共占用4位, 所以下面只写最低8位O_RDONLY(只读)用最低位 第0位表示:00000001, 十进制就是1
O_WRONLY(只写)则用第1位表示: 00000010, 十进制为2
O_RDWR(读写)用第2位表示:00000100, 十进制为4
O_CREAT(创建)用第3位:00001000, 十进制为8
当传入flags的整数的二进制位中, 其低四位中哪一位是1, 就表示对应的选项被选中
可能并不是最低的四位
:这些选项其实就是2的次方倍的十进制数的宏定义
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("newlog.txt", O_CREAT | O_WRONLY); // 以只读方式打开newlog.txt 文件, 且若文件不存在, 则创建文件
if(fd < 0) {
perror("open");
}
return 0;
}
此文件的权限非常的混乱
:没有指定创建的文件的权限, 所以创建出的文件的权限是混乱的
open(const char* pathname, int flags)
, 这个只有两个参数的系统接口, 是打开已经存在的文件用的, 创建新文件需要使用另一个系统接口:#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("newlog.txt", O_CREAT | O_WRONLY, 0666); // 以只读方式打开newlog.txt 文件, 且若文件不存在, 则创建文件
if(fd < 0) {
perror("open");
}
return 0;
}
另外一个问题
**:0666
, 文件的权限应该是 -rw-rw-rw-
, 为什么实际却是-rw-rw-r--
?umask
umask
, 创建文件时, 操作系统会将指定的权限 - umask
, 作为文件的实际权限-rw-rw-r--
umask
创建文件#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
umask(0); // 设置进程umask为0
int fd = open("newlog.txt", O_CREAT | O_WRONLY, 0666); // 以只读方式打开newlog.txt 文件, 且若文件不存在, 则创建文件
if(fd < 0) {
perror("open");
}
return 0;
}
进程创建文件时, 是否有必要重新设置umask值?
这个行为是有必要的, 重新给进程设置umask值可以
更加方便地指定创建文件时的文件权限
其次, 还有可能此操作系统设置的默认umask值非常的离谱:比如
umask 666
那么, 此时创建文件可能就会有无法想象的阻碍.
close()
int close(int fd);
fclose()
, Linux系统提供的系统接口则叫做: close()
其用法与fclose()几乎一致, 只不过close()传入的是文件描述符, 即调用open()是接收的返回值
write()
ssize_t write(int fd, const void* buf, size_t count);
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("newFile.txt", O_CREAT | O_RDWR, 0666); // 以读写方式打开newFile.txt, 文件不存在则创建新文件
if(fd < 0) {
perror("open");
}
const char* buffer = "hello world, hello July\n";
int cnt = 5;
while (cnt--) {
write(fd, buffer, strlen(buffer));
}
close(fd);
return 0;
}
read()
read()
ssize_t read(int fd, void *buf, size_t count);
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("newFile.txt", O_RDONLY); // 以只读方式打开文件
if(fd < 0) {
perror("open");
}
// 从文件中读取内容写入buffer, 并输出
char buffer[128];
read(fd, buffer, sizeof(buffer)-1);
printf("buffer: %s\n", buffer);
close(fd);
return 0;
}
首先文件中要有内容
open()的flags参数——O_TRUNC
创建(O_CREAT)
和读写(O_RDONLY、O_WRONLY、O_RDWR)
方式打开, O_TRUNC
也是一个非常常用的选项open()
使用O_WRONLY
打开文件并写入内容时, 文件的内容是如何写入的?#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("newFile.txt", O_CREAT | O_RDWR, 0666); // 以读写方式打开newFile.txt, 文件不存在则创建新文件
if(fd < 0) {
perror("open");
}
const char* buffer = "66666666666";
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
fopen(), 并以w方式打开文件时, 会将文件原本的内容清空, 然后再在文件中写入数据
open()只用O_WRONLY
, 实现不了先清空文件内容, 而是需要在使用另一个选项 O_TRUNC
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("newFile.txt", O_CREAT | O_RDWR | O_TRUNC, 0666); // 以读写并清空原内容的方式打开newFile.txt, 文件不存在则创建新文件
if(fd < 0) {
perror("open");
}
const char* buffer = "66666666666\n";
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
O_TRUNC
的作用就是, 打开文件时, 先清空文件内容
.open()的flags参数——O_APPEND
a
和 a+
O_WRONLY | O_APPEND
时, 就可以使打开文件的方式变为追加写入:#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("newFile.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); // 以只写并追加方式打开newFile.txt, 文件不存在则创建新文件
if(fd < 0) {
perror("open");
}
const char *buffer = "Hello world, hello July\n";
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
只传入
O_APPEND
选项, 不传入O_WRONLY 或 O_RDWR
是无法追加写入的, 因为没有写入打开
fd文件描述符 *
open()
close()
write()
read()
fd(file descriptor)文件描述符
, 以下简称fd什么是文件描述符
一个可以代表打开的文件的变量
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
umask(0);
int fda = open("newFile.txt", O_RDWR | O_CREAT, 0666); // 以读写方式打开文件, 若文件不存在则创建文件
int fdb = open("newFile.txt", O_RDWR | O_CREAT, 0666); // 以读写方式打开文件, 若文件不存在则创建文件
int fdc = open("newFile.txt", O_RDWR | O_CREAT, 0666); // 以读写方式打开文件, 若文件不存在则创建文件
int fdd = open("newFile.txt", O_RDWR | O_CREAT, 0666); // 以读写方式打开文件, 若文件不存在则创建文件
int fde = open("newFile.txt", O_RDWR | O_CREAT, 0666); // 以读写方式打开文件, 若文件不存在则创建文件
printf("fda: %d\n", fda); // 输出打开文件的fd
printf("fdb: %d\n", fdb); // 输出打开文件的fd
printf("fdc: %d\n", fdc); // 输出打开文件的fd
printf("fdd: %d\n", fdd); // 输出打开文件的fd
printf("fde: %d\n", fde); // 输出打开文件的fd
close(fda);
close(fdb);
close(fdc);
close(fdd);
close(fde);
return 0;
}
打开文件的fd按照顺序从3~7递增
打开文件时, 打开文件的fd按照打开文件的顺序递增
fd 0, 标准输入 –> 键盘
fd 1, 标准输出 –> 显示器
fd 2, 标准错误 –> 显示器
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
// 不手动打开任何文件
// 从 fd=0 中, 读取数据并存储到buffer中
char buffer[1024];
read(0, buffer, sizeof(buffer)-1);
// 分别向fd=1 和 fd=2 中写入数据
write(1, buffer, strlen(buffer));
write(2, buffer, strlen(buffer));
return 0;
}
0、1、2文件描述符, 分别属于标准输入、标准输出、标准错误
而C语言的文件操作中, 同样存在三个文件指针变量:
stdin
stdout
stderr
, 同样可以通过这三个文件指针变量, 从命令行中读取数据, 并将数据输出到屏幕上那么这两者之间是否存在一定的关系呢?
实际上, C语言中
stdin
stdout
stderr
三个文件指针变量, 就对应着操作系统层级的标准输入、标准输出、标准错误
只不过
C语言的文件操作接口认的是这三个文件指针变量
, 而操作系统的文件操作接口则只认fd文件描述符
而C语言的文件操作接口其实是封装了操作系统层级的文件操作接口. 那么C语言想要进行文件操作 也是必须要知道文件的fd的
C语言中进行文件操作时, 都是使用
文件指针 FILE*
而FILE在C语言中其实是一个结构体类型, 此结构体包含许多的成员, 其中
也包含着打开文件的fd
!我们分别输出 stdin、stdout、stderr这三个FILE指针的_fileno:
#include <stdio.h> int main() { // C语言会默认打开 stdin, stdout, stderr printf("stdin-fd: %d\n", stdin->_fileno); printf("stdout-fd: %d\n", stdout->_fileno); printf("stderr-fd: %d\n", stderr->_fileno); return 0; }
可以看到,
stdin、stdout、stderr这三个FILE指针的_fileno分别是0、1、2, 对应着系统的进程默认打开的标准输入、标准输出、标准错误三个fd
stdin、stdout、stderr这三个FILE指针指向的结构体变量的内容是不会改变的
所以C语言的文件操作接口, 其实是
从FILE*变量中获取打开文件的fd, 再调用系统接口
来完成的即:
函数接口 所需数据类型 fopen、fclose、fwrite、fread……
FILE*
FILE结构体中包含fd open、close、write、read……
fd
文件描述符究竟是什么?代表什么意思?
进程打开文件的fd, 是从0开始按照打开文件的顺序依次递增的
0 1 2 3 4 5 6 7 8……
从零开始的连续的递增整数
, 除此之外还在哪个地方出现过?数组
, 数组的下标就是从零开始的连续的递增整数文件描述符实际上就是某个数组的下标
一个进程是可以打开多个文件的
. 而操作系统中又存在着许多的进程, 其实也就意味着 操作系统中存在的大量的被打开的文件
就像管理进程实际上实在管理进程PCB一样, 操作系统管理文件其实实际上是在管理描述着文件属性的结构体
struct file{};
, 每一个打开的文件都由这样一个结构体维护着, 且结构体之间会构成一个数据结构, 方便操作系统进行管理打开的文件在操作系统中, 实际上都在一个数据结构中维护着
操作系统一定会为文件结构体维护一个数据结构
进程PCB(struct task_struct)
中, 存储着一个 struct file_struct* file
成员, 是一个结构体指针fd_array[]数组的下标, 就是open()、close()等系统接口使用的fd文件描述符
. 文件操作的系统接口可以通过fd, 在fd_array[]数组中找到指定下标存储的指针 再找到指针指向的文件即文件描述符fd本质上就是, fd_array[]数组中的下标, 此fd表示fd_array[fd]存储着打开文件的指针
文件描述符相关源码
Linux下一切皆文件
文件描述符
操作系统会将打开的文件以结构体的形式维护起来
. 但是进程中默认的0、1、2文件分别对应着标准输入、标准输出、标准错误, 而标准输入实际上一般指的就是键盘, 标准输出和标准错误一般指的就是显示器
键盘、显示器等这些都是硬件, 文件操作的系统接口都是根据fd来操作的, 难道这写硬件也被操作系统用struct file{}结构体维护吗?
Linux操作系统下, 一切皆文件
各种外设、I/O设备也被看作是文件
不过在尝试理解I/O设备被看作是文件之前, 来思考一个问题:
C语言能否实现像C++中类一样的功能?
C++类与C语言中的结构体, 不考虑细节, 其实就只是C++可以在类内定义函数, 而C语言只能定义变量, 这样的区别
C++的类可以通过对象调用属于此类的函数. 那么C语言有没有办法实现, 通过结构体变量调用指定功能的函数呢?有办法
C语言可以在结构体内部定义函数指针变量, 使变量指向指定功能的函数, 从而可以看作实现了通过结构体变量调用结构体内部的函数
Linux中, 操作系统会将文件以 struct file{} 结构体的形式维护起来, 而一个文件最常做的事情就是读写
那么file结构体中, 除了文件属性之外, 至少还需要描述文件的读写方法, 而C语言结构体内部不能定义函数
所以 file结构体中, 会用函数指针变量的形式将文件的读写方法描述起来:
struct file{ // 文件属性 void (*read)(参数); void (*write)(参数); }
当然, 这只是一个简单的例子, 只是介绍file结构体中可以描述文件的各种方法
在Linux操作系统中, 不论是硬件还是程序或是普通的文本文件, 打开之后都会被操作系统以struct file{}结构体的形式 在某种数据结构中维护着
Linux操作系统的内存文件系统会对 所有设备和打开的文件 以一个统一的视角进行组织和管理, 这就是 Linux下一切皆文件
虚拟文件系统(VFS)
file结构体相关内容源码
作者: 哈米d1ch 发表日期:2023 年 3 月 27 日