09.【Linux系统编程】“文件“读写操作,Linux下一切皆文件!
目录
- 1. 理解"文件"
- 1.1 什么是文件(狭义理解)
- 1.2 什么是文件(广义理解)
- 1.3 文件操作的归类认知
- 1.4 系统角度
- 2. 回顾C文件接口(内存级打开文件)
- 2.1 文件创建路径
- 2.2 myfile.c写文件
- 2.3 myfile.c读文件
- 2.4 输出信息到显示器的不同方式
- 2.5 stdin & stdout & stderr
- 2.6 打开文件的不同方式
- 3. 系统文件I/O
- 3.1 系统文件I/O入口,open函数的使用
- 3.2 open传递flags标志位的方法(每个bit位各为一种功能标志,通过‘|’传递多个标志)
- 3.3 C调用Linux系统函数write写文件
- 3.4 C调用Linux系统函数read读文件
- 3.5 open函数理解(系统调用&库函数)
- 3.6 文件描述符fd
- 3.6.1 特殊文件描述符0 & 1 & 2
- 3.6.2 文件描述符的分配规则
- 3.7 文件管理(task_struct→files_struct管理文件)
- 3.7.1 了解 struct file数组 - 存放文件指针
- 3.7.2 struct file中为何要存在struct list_head?
- 3.8 重定向
- 3.8.1 重定向概念
- 3.8.2 使用dup2系统调用实现重定向
- 3.9 文本写入 VS 二进制写入(系统层面只有二进制方式)
- 3.10 关于fopen对open的封装(可跨平台移植原因)
1. 理解"文件"
1.1 什么是文件(狭义理解)
-
文件在磁盘里。
-
磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的。
-
磁盘是外设(即是输出设备也是输入设备)。
-
对磁盘上文件的所有操作的本质,都是对外设的输入和输出 简称 IO。
1.2 什么是文件(广义理解)
- Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)(后面会讲如何去理解)
1.3 文件操作的归类认知
-
0KB
的空文件是占用磁盘空间的(文件属性占空间)。 -
文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)。
-
所有的文件操作本质是文件内容操作和文件属性操作。
1.4 系统角度
-
访问文件,需要先打开文件!谁打开文件?谁对文件进行操作?
- 进程打开的文件,对文件的操作本质是进程对文件的操作。
-
磁盘的管理者是操作系统。
-
文件的读写本质不是通过
C 语言
/C++
的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的。 -
操作系统管理文件:先描述,再组织!
2. 回顾C文件接口(内存级打开文件)
2.1 文件创建路径
#include <stdio.h>
int main()
{FILE *fp = fopen("log.txt", "w");if(!fp){printf("fopen error!\n");}while(1);fclose(fp);return 0;
}
打开的log.txt
文件在哪个路径下?
-
在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?
-
可以使用
ls /proc/[进程id] -l
命令查看当前正在运行进程的信息,其中:cwd
:指向当前进程运行目录的一个符号链接。exe
:指向启动当前进程的可执行文件(完整路径)的符号链接。
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。
2.2 myfile.c写文件
#include<stdio.h>
#include<string.h>int main()
{FILE *fp = fopen("log.txt", "w");if(fp == NULL){perror("fopen");return 1;}const char *msg = "hello world: ";int cnt = 1;while(cnt <= 10){char buffer[1024];snprintf(buffer, sizeof(buffer), "%s%d\n", msg, cnt++);fwrite(buffer, strlen(buffer), 1, fp);}fclose(fp);return 0;
}
2.3 myfile.c读文件
// myfile.c
// gcc -o myfile myfile.c
// ./myfile filename#include<stdio.h>
#include<string.h>// cat myfile.txt
int main(int argc, char *argv[])
{// 只有一个参数,直接返回if(argc != 2){printf("Usage: %s filename\n", argv[0]);return 1;}// 两个参数,第二个参数是要查看的文件FILE *fp = fopen(argv[1], "r");if(NULL == fp){perror("fopen");return 2;}//读文件内容并打印1while(1){char buffer[128];memset(buffer, 0, sizeof(buffer));// fread返回读到的元素个数,sizeof(buffer)-1中的-1是为了保存\0int n = fread(buffer, 1, sizeof(buffer)-1, fp);if(n > 0){printf("%s", buffer);}if(feof(fp))// 判断是否到文件末尾break;}fclose(fp);return 0;
}
2.4 输出信息到显示器的不同方式
#include<stdio.h>
#include<string.h>int main()
{printf("hello world\n");fprintf(stdout, "hello fprintf\n");const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);return 0;
}
2.5 stdin & stdout & stderr
• C默认会打开三个输入输出流,分别是stdin
, stdout
, stderr
• 仔细观察发现,这三个流的类型都是FILE*
, fopen
返回值类型,文件指针
#include <stdio.h>extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
2.6 打开文件的不同方式
文件使用方式 | 含义(读(输入)从文件读到程序,写(输出)从程序写到文件) | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入(写)数据,打开一个已经存在的文本文件 | 出错 |
“rb”(只读) | 为了输入(写)数据,打开一个二进制文件 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“w”(只写) | 为了输出(读)数据,清空并打开一个文本文件 | 建立一个新的文件 |
“wb”(只写) | 为了输出(读)数据,清空并打开一个二进制文件 | 建立一个新的文件 |
“w+”(读写) | 为了读和写,清空并打开一个文本文件 | 建立一个新的文件 |
“wb+”(读写) | 为了读和写,清空并打开一个二进制文件 | 建立一个新的文件 |
“a”(追加) | 打开一个文本文件,向文件尾写数据 | 建立一个新的文件 |
“ab”(追加) | 打开一个二进制文件,向文件尾写数据 | 建立一个新的文件 |
“a+”(读写) | 打开一个文本文件,在文件尾部进行读写 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾部进行读和写 | 建立一个新的文件 |
3. 系统文件I/O
打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到
3.1 系统文件I/O入口,open函数的使用
// 函数原型
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);// pathname - 要打开或创建的目标文件路径
// flags - 标志位(核心参数)使用位掩码组合,主要分为三类:
// mode - 权限模式(仅在使用 O_CREAT 时需要)
// 返回值:
// 成功:新打开的文件描述符
// 失败:-1#include <unistd.h>
int close(int fd); // 关闭文件
// 参数:fd文件描述符,即open函数的返回值
- flags - 标志位分类
分类 | 标志位 | 含义 |
---|---|---|
文件访问模式(必选其一) | O_RDONLY | 只读模式 |
O_WRONLY | 只写模式 | |
O_RDWR | 读写模式 | |
文件创建和状态标志(可组合) | O_CREAT | 文件不存在时创建 |
O_EXCL | 与O_CREAT一起使用,文件必须不存在 | |
O_TRUNC | 如果文件存在且为普通文件,将其长度截断为0 | |
O_APPEND | 追加模式(每次写操作前定位到文件末尾) | |
文件状态标志 | O_NONBLOCK | 非阻塞模式 |
O_SYNC | 同步写入(数据立即写入磁盘) |
常用组合功能 | 组合方式 | 对应C函数模式 |
---|---|---|
创建、清空并写入 | `int fd = open(“log.txt”, O_CREAT | O_WRONLY |
创建、追加并写入 | `int fd = open(“log.txt”, O_CREAT | O_WRONLY |
读文件 | int fd = open("log.txt", O_RDONLY); | "r" |
-
mode - 文件权限设置参考文中的第4部分:02.【Linux系统编程】Linux权限(root超级用户和普通用户、创建普通用户、sudo短暂提权、权限概念、权限修改、粘滞位)-CSDN博客
-
可以通过
umask
设置程序中新建文件的权限掩码(使用示例见3.3)#include <sys/types.h> #include <sys/stat.h>mode_t umask(mode_t mask); //umask函数只是修改当前进程下创建的文件的掩码,并不改变系统的掩码
-
3.2 open传递flags标志位的方法(每个bit位各为一种功能标志,通过‘|’传递多个标志)
使用整型的32个bit位,每个bit位各为一种功能标志,举例如下,给Print函数传不同参数则执行函数中不同的功能。
#include <stdio.h>#define ONE_FLAG (1<<0) // 0000 0000 0000...0000 0001
#define TWO_FLAG (1<<1) // 0000 0000 0000...0000 0010
#define THREE_FLAG (1<<2) // 0000 0000 0000...0000 0100
#define FOUR_FLAG (1<<3) // 0000 0000 0000...0000 1000void Print(int flags)
{if (flags & ONE_FLAG) printf("One!\n");if (flags & TWO_FLAG) printf("Two\n");if (flags & THREE_FLAG) printf("Three\n");if (flags & FOUR_FLAG) printf("Four\n");
}int main()
{Print(ONE_FLAG); printf("\n");Print(ONE_FLAG | TWO_FLAG); printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG); printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG); printf("\n");Print(ONE_FLAG | FOUR_FLAG); printf("\n");return 0;
}
One!One!
TwoOne!
Two
ThreeOne!
Two
Three
FourOne!
Four
3.3 C调用Linux系统函数write写文件
操作文件,除了上小节的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面功能一模一样的代码。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{umask(0); // 修改文件权限掩码int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);//int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);//int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);int cnt = 5;const char *msg = "hello world\n";while(cnt){write(fd, msg, strlen(msg)); //fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据cnt--;}close(fd);return 0;
}
3.4 C调用Linux系统函数read读文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{umask(0);// int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);// int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);while(1){char buffer[64];int n = read(fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;printf("%s", buffer);}else if(n == 0){break;}}close(fd);return 0;
}
3.5 open函数理解(系统调用&库函数)
在认识返回值之前,先来认识一下两个概念: 系统调用和库函数
-
上面的
fopen
、fclose
、fread
、fwrite
都是C标准库当中的函数,我们称之为库函数(libc
)。 -
而
open
、close
、read
、write
、lseek
都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。所以,可以认为, f开头
系列的函数,都是对系统调用的封装,方便二次开发。
3.6 文件描述符fd
• 通过对open函数的学习,我们知道了文件描述符就是一个小整数
3.6.1 特殊文件描述符0 & 1 & 2
• Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0(stdin
), 标准输出1(stdout
), 标准错误2(stderr
).
• 0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{char buf[1024];ssize_t s = read(0, buf, sizeof(buf)); // 读键盘if(s > 0){buf[s] = 0;write(1, buf, strlen(buf)); // 输入到显示器write(2, buf, strlen(buf)); // 输入到显示器}return 0;
}
而现在知道,文件描述符就是从0开始的小整数。
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。
而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!
- 所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
(其他问题见3.7:既然有fd
文件描述符,那么file
类型的文件指针中为什么还要存在list_head
结构体?)
3.6.2 文件描述符的分配规则
直接看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
// 输出发现是 fd: 3
// 关闭0或者2,在看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{close(0);//close(2);int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
// 结果是: fd: 0 或者fd 2
- 结论(文件描述符的分配规则):在
files_struct
数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。(当然也可以使用close
关闭三个标准文件输入/输出,则再次新建文件则同样遵循此规则)
举例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>int main()
{umask(0);int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd1 < 0) exit(1);if(fd2 < 0) exit(1);if(fd3 < 0) exit(1);if(fd4 < 0) exit(1);// FILE 结构体确实封装了文件描述符 fd,成员为int _fileno; // ✅ 封装的文件描述符fdprintf("stdin: %d\n", stdin->_fileno);printf("stdout: %d\n",stdout->_fileno);printf("stderr: %d\n", stderr->_fileno);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
stdin: 0
stdout: 1
stderr: 2
fd1: 3
fd2: 4
fd3: 5
fd4: 6
3.7 文件管理(task_struct→files_struct管理文件)
3.7.1 了解 struct file数组 - 存放文件指针
-
文件是通过
struct files_struct
结构体来管理的,而struct files_struct
结构体编程又存放在struct tast_struct
进程控制块中。在Linux内核的task_struct
中。 -
struct files_struct
结构体中有一个struct file __rcu *
指针数组,下标就是文件描述符fd
,内容是文件指针
。
struct task_struct {// ...struct files_struct *files; // 文件管理结构// ...
};struct files_struct {struct file __rcu * fd_array[NR_OPEN_DEFAULT]; // ✅ 文件指针数组//// 数组的每个元素都是:struct file __rcu * // 带RCU注解的file结构体指针// ...
};
3.7.2 struct file中为何要存在struct list_head?
struct files_struct
结构体中可以通过下标fd
来找到指定的文件指针struct file
,那么为什么struct file
结构体中还有struct list_head
指针将各个文件链接起来呢?
🔹 fd_array[] - 进程视角
- 目的:让单个进程快速访问自己打开的文件
- 用法:
read(fd, ...)
→fd_array[fd]
→ 找到文件 - 技术:数组索引,O(1)时间复杂度
- 场景:系统调用
read(fd)
,write(fd)
🔹 各种list_head - 系统视角
- 目的:让内核管理系统中的所有文件关系
- 用法:文件系统维护、
inode
引用管理、资源清理等 - 技术:链表遍历,维护系统关系
- 场景:文件系统卸载、
inode
引用管理、资源清理
3.8 重定向
3.8.1 重定向概念
那如果关闭1呢?看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{close(1);int fd = open("myfile", O_WRONLY|O_CREAT, 00644);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}
- 此时,我们发现,本来应该输出到显示器上的内容,输出到了文件
myfile
当中,其中,fd=1
。这种现象叫做输出重定向。 - 命令行指令中,常见的重定向有: > , >> , <
3.8.2 使用dup2系统调用实现重定向
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2
的功能:就是修改files_struct
结构体中保存文件指针的 指针数组struct file
的内容,将oldfd
下标中的内容拷贝到newfd
下标中。
示例:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>int main()
{int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(1);dup2(fd, 1);printf("fd:%d\n", fd);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char *msg = "hello world\n";write(fd, msg, strlen(msg));return 0;
}
$ cat log.txt
hello world
fd:3
hello printf
hello fprintf
printf
是C库当中的IO函数,一般往stdout
中输出,但是stdout
底层访问文件的时候,找的还是fd:1,但此时,fd:1
下标所表示内容,已经变成了log.txt
的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向同理。
3.9 文本写入 VS 二进制写入(系统层面只有二进制方式)
-
文本写入即字符写入
-
都是语言层面,系统层面只有二进制方式,其他都转成二进制方式写入。
3.10 关于fopen对open的封装(可跨平台移植原因)
-
C库封装系统调用,条件编译实现跨平台
- 在C语言库中,
fopen
封装了Linux
系统对文件操作的open
函数,所以可以使用fopen
来完成Linux
系统下对文件的操作。 - 同样的,C语言库中也封装了
windows
系统中对文件操作的函数,上层也封装成fopen
函数。在其他系统中同样,他们使用条件编译来区分不同系统。由此就可以实现C语言文件在不同系统中的移植,即上层都是使用fopen
对文件操作,底层由C语言库分别适应不同的操作系统。
- 在C语言库中,
-
目的
- 目的是提高可移植性和开发效率
- 技术需求、市场需求、标准推动的共同结果。而是技术进步和经济效益的平衡