文章目录
1.储备知识
对文件的操作范畴:
在系统角度理解文件
文件 = 内容 + 属性(也是数据)
创建一个空文件也占空间
对文件的所有操作无外乎两种:对内容和对属性
c、c++程序会默认打开三个文件流:
标准输入:键盘 extern FILE *stdin;
标准输出:显示器 extern FILE *stdout;
标准错误:显示器 extern FILE *stderr;
linux下一切皆文件
感性认识:
曾经理解的文件:read、write
键盘和显示器可以被看做文件吗?可以
我从来没有打开过键盘和显示器文件,但是依旧能够直接使用scanf,fgets,printf,cout…
是因为c、c++程序会默认打开三个文件流
磁盘是硬件,只有操作系统才能真正的访问磁盘
文件在磁盘上放着,我们访问文件,需要先写代码然后编译生成exe最后运行,那么访问文件本质是谁在访问文件呢?
进程
进程访问文件是需要接口的
之前我们学习的接口是语言类的接口
- 要向硬件上写入,只有谁才有权利呢?(代码上)
操作系统
- 如果普通用户也想向硬件写入呢?
必须让OS提供接口
提供文件类的系统调用接口
为什么文件类的系统调用接口之前从未接触过?
1). 因为之前学的是C语言,文件类的系统调用接口,比较难掌握,语言上对这些接口做一下封装
,让接口更好被使用——>语言类接口
每个语言都会做封装,就会导致了不同的语言有不同的语言级别文件访问接口。但是底层用的都是系统接口。
为什么要学习os层面的接口?
linux中这样的接口只有一套
(os只有一个)
2).
跨平台:把所有平台的代码都实现一遍,条件编译,动态裁剪
如果语言不提供对文件的系统接口的封装,是不是所有访问文件的操作,都必须直接使用OS的接口?
是的
面对语言的客户,要不要访问文件呢?
要
一旦使用系统接口,编写所谓的文件代码,无法在其他平台中直接运行了。不具备跨平台性!
显示器和磁盘写入没有区别
什么叫做文件?
站在系统的角度,可以被input读取,或者能够output写出的设备就叫做文件
狭义文件:普通的磁盘文件
广义文件:显示器,键盘,网卡,声卡,显卡,磁盘……几乎所有的外设,都可以称之为文件。
2. 文件描述符
2.1 c接口
makefile
myfile:myfile.c
gcc -o $@ $^
.PHONY:clean
rm -f myfile
w:从文件开始写
a:从结尾开始写
1 #include <stdio.h>
2
3 int main()
4 {
5 FILE *fp = fopen("log.txt","w");
6 if(fp == NULL)
7 {
8 perror("fopen");
9 return 1;
10 }
11 //进行文件操作
12
13
14
15 fclose(fp);
16 return 0;
17 }
此时在当前路径下,是否存在文件log.txt?——还没有,需要运行程序之后,log.txt才会被创建
log.txt是谁创建的?——os
会在哪里创建?——当前路径
当前路径:当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径
写文件的接口
1 #include <stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main()
6 {
7 FILE *fp = fopen("log.txt","w");
8 if(fp == NULL)
9 {
10 perror("fopen");
11 return 1;
12 }
13 //进行文件操作
14 const char *s1 = "hello fwrite";
15 fwrite(s1,strlen(s1),1,fp);
16
17 const char *s2 = "hello fprintf";
18 fprintf(fp,"%s",s2);
19
20 const char *s3 = "hello fputs";
21 fputs(s3,fp);
22
23
24
25 fclose(fp);
26 return 0;
27 }
先删掉之前的log.txt,再运行程序
const char *s1 = "hello fwrite\n";
fwrite(s1,strlen(s1),1,fp);
要不要+1?fwrite(s1,strlen(s1)+1,1,fp);
不要。
\0结尾是C语言的规定,文件不用遵守
如果+1打印出来就会多了一个乱码
文件保存的是有效数据,/n只是标志结尾的标识符
fopen:
- 当以写(w)方式打开文件时,会先清空文件。
用重定向方式写入:echo helloworld > log.txt
想要清空:>log.txt
2.以a方式打开文件是追加重定向,不会清空之前的内容
r
按行读取
fgets是C语言提供的接口 s(string)会自动在字符结尾添加\0
char line[64];
while(fgets(line,sizeof(line),fp) != NULL)
{
fprintf(stdout,"%s",line);
}
三个标准输入输出流:
stdout
标准输入:键盘 extern FILE *stdin;
标准输出:显示器 extern FILE *stdout;
标准错误:显示器 extern FILE *stderr;
都叫做文件指针
一切皆文件
命令行参数:
int main(int argc,char *argv[])
2.2 直接使用系统接口
c库函数: fopen fclose fread fwrote
系统调用:open close read write
两者关系:上面的c库函数底层都是系统调用
宏定义:全大写
如何给函数传递标志位
6 #define ONE 0x1//0000 0001
7 #define TWO 0x2//0000 0010
8 #define THREE 0x4//0000 0100
W> 9
10
11 void show(int flags)
12 {
13 if(flags & ONE) printf("hello one\n");
14 if(flags & TWO) printf("hello two\n");
15 if(flags & THREE) printf("hello three\n");
16 }
17
18 int main()
19 {
20 show(ONE);
21 show(TWO);
22 show(ONE | TWO);
23 show(ONE | TWO | THREE);
24 return 0;
25
2.3 open函数返回值
file descriptor:文件描述符
返回值:成功:新打开的文件描述符失败:-1
接口介绍
open man 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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_TRUNC:写之前清空文件
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
mode_t理解:直接 man 手册,比什么都清楚。open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open
write read close lseek ,类比C文件相关接口
2.4 文件描述符fd
0 & 1 & 2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
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;
}
FILE *fopen(const char *path,const char *mode);
FILE是一个struct的结构体,由c标准库提供
结构体内部会有多种成员
而在系统角度只认fd。
所以FILE结构体必定封装了fd
_fileno
2.5 周边文件
fd是什么?
之前说过进程要访问文件,必须先打开文件。
那么一个进程可以打开多个文件吗?
一般而言,进程:打开的文件 = 1:n
文件要被访问,前提是要被加载到内存中,才能直接被访问
进程:打开的文件 = 1:n 如果是多个进程都打开直接的文件呢?
系统中就会存在大量的被打开的文件,所以OS要把如此之多的文件管理起来:
先描述,再组织!
文件的属性从哪里来?
一部分在对应的磁盘中
所以在内核中,OS内部要管理每一个被打开的文件,需要构建结构体
struct file
{
struct file * next;
struct file * prev;
//包含了一个被打开的文件的几乎所有的内容(不仅仅包含属性)
}
创建struct file的对象,充当一个被打开的文件。如果有很多再用双链表组织起来
文件对象里面包含了文件的所有内容
fd在内核中,本质是一个数组下标!
文件:
1.被进程在内存中打开的文件(执行你的代码的一定是CPU)
2.没有被打开的文件(在磁盘上,文件=内容+属性)(磁盘文件)
进程控制块:PCB struct task_struct
fopen——open——fd——FLIE——FILE*
fwrite()——FILE*——fd——write——write(fd,…)——自己执行操作系统内部的write方法——能找到进程的task_struct——*fs——file_struct——fd_arry[fd]——structfile——内存文件被找到了——操作
文件描述符就是从0开始的小整数。
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。
而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!
所以,本质上,文件描述符就是该数组的下标。
所以,只要拿着文件描述符,就可以找到对应的文件
3. 重定向
3.1 输出重定向
1 myfile:myfile.c
2 gcc -o $@ $^
3 .PHONY:clean
4 clean:
5 rm -f myfile
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
此时fd是3
因为012已结被提前占用了
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
fd:也是3
close(0);
fd:0
close(2)
fd:2
结论:
fd在系统层面的分配规则是:最小的,没有被占用的文件描述符
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
不显示了
处理:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
//close(fd);
return 0;
}
printf默认是往stdout
运行./myfile时依旧不显示
但是cat log.txt时有内容,打印了fd:1
确实是之前所说的fd分配原则
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
fflush(stdout);
close(fd);
return 0;
}
这些接口都应该是往显示器(标准输出)打印的
但是下面的内容都写入(显示)到了log.txt
这就叫做输出重定向
重定向的本质:
就是在OS内部更改fd对应的内容的指向
3.2 输出重定向
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
char buffer[64];
fgets(buffer, sizeof buffer, stdin);
printf("%s\n", buffer);
return 0;
}
fd:3
输入hello
输出hello
close(0);
fd:0
aaaaaaaaaaaaaa
本来应该从键盘读取的内容,却直接读取log.txt中的内容
这是输入重定向
3.3 追加重定向
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
fprintf(stdout, "you can see me, success\n");
return 0;
}
./myfile
cat log.txt
you can see me,success
再运行,打印也只有一行
int main()
{
close(1);
//int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
fprintf(stdout, "you can see me, success\n");
return 0;
}
./myfile
./myfile
./myfile
./myfile
you can see me,success
you can see me,success
you can see me,success
you can see me,success
运行几次,追加几次 这就是追加重定向
3.4 dup
oldfd copu to newfd
最终要和oldfd一样,那么newfd就没有意义了,就可以关闭了
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
return 2
}
int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
fprintf(stdout, "%s\n", argv[1]);
return 0;
}
退出码:2
./myfile 105
105
int main(int argc, char *argv[])
{
if (argc != 2)
{
return 2
}
int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
fprintf(stdout, "%s\n", argv[1]);
return 0;
}
./myfile hello
不显示
cat log.txt
hello
想显示的内容都会被打印到文件中而不是在显示器中
int main(int argc, char *argv[])
{
if (argc != 2)
{
return 2
}
//int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
fprintf(stdout, "%s\n", argv[1]);
return 0;
}
./myfile
./myfile aaa
./myfile bbb
cat log.txt
aaa
bbb
追加
int main(int argc, char *argv[])
{
if (argc != 2)
{
return 2
}
//int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
fprintf(stdout, "%s\n", argv[1]);
close(fd);
return 0;
}
依旧可以显示
这是dup2的一种特性,与缓冲区有关
4. 如何理解一切皆文件?
理性理解:
这是linux的设计哲学,体现在os的软件设计层面
linux是用C语言写的,那么如何用C语言实现面向对象,甚至是运行时多态?
类:所有事物与属性的结合
成员属性+成员方法
struct:可以包含成员属性,但是在纯C语言中不包含成员方法
但是我想让struct中包含成员方法呢?
可以定义函数指针,指向函数,去调用就可以了
底层不同的硬件,一定对应的是不同的操作方法
但是上面的设备都是外设,所以每一个设备的核心访问函数都可以是read、write(I、O)
所有的设备都可以有自己的read和write,但是代码的实现一定不一样!
设计一个struct,打开一个磁盘文件的时候,创建一个struct file
各自指向各自的
在这一层上面看,没有任何的硬件差别了,看待所有文件的方式,都统一成为了struct file
所以就有了linux下一切皆文件的说法
VFS虚拟文件技术