目录
- 重谈文件操作–C语言
- 介绍stdin,stdout,stderr
- open接口
- 访问文件的本质
- 重定向
- 理解Linux一切皆文件
- 详解缓冲区
- 实现fclose fwrite fopen
- 认识硬件–磁盘
- 理解文件系统
- 理解软硬链接
- 补充知识
- 理解动静态库
重谈文件操作–C语言
fopen接口:
第一个参数:const char* path——打开文件的名称
第二个参数:有“w”选项,“r”选项, “a”选项(其他选项暂时不讲)
返回类型:文件流(文件流是什么?后面会将)
以“w”作为选项的fopen函数测试:
int main() { FILE* fp = fopen("log.txt", "w"); return 0; }
我们能看到运行后会默认在当前路径下创建名为“log.txt”的一个文件。
当前路径下的本质是什么呢?
进程的工作目录
如何证明:
我们可以看到当前进程的工作目录
我们尝试修改进程的工作目录
int main()
{
int n = chdir("/tmp/");//谁调用我,我就修改谁的工作目录
if (n < 0)
{
cout << strerror(errno) << endl;
exit(1);
}
FILE* fp = fopen("log.txt", "w");
return 0;
}
可以看到我们修改了进程的工作目录,创建的文件到这个文件夹里面
测试“w”选项
int main()
{
FILE* fp = fopen("log.txt", "w");
char buffer[128];
snprintf(buffer, sizeof(buffer), "aaaaaaaa");
fwrite(buffer, strlen(buffer), 1, fp);
return 0;
}
将aaaaaaa写入log.txt文件
再测试
int main()
{
FILE* fp = fopen("log.txt", "w");
// char buffer[128];
// snprintf(buffer, sizeof(buffer), "aaaaaaaa");
// fwrite(buffer, strlen(buffer), 1, fp);
return 0;
}
可以看到只用"w"选项打开文件就会清空文件内容
那我们想不清空呢?
测试"a"选项
int main()
{
FILE* fp = fopen("log.txt", "a");
char buffer[128];
snprintf(buffer, sizeof(buffer), "aaaaaaaa");
fwrite(buffer, strlen(buffer), 1, fp);
return 0;
}
可以看到 "a"是以追加的方式写文件
介绍stdin,stdout,stderr
stdin是标准输入流,stdout是标准输出流,stderr是标准错误流
这三个流是默认被系统打开的
测试stdout
向stdout文件流里面写入,而stdout是标准输出流,所以现象就是打印到屏幕上
int main()
{
for (int i = 0; i < 5; ++i)
{
fprintf(stdout, "hello world%d\n", i + 1);
}
return 0;
}
测试stdin
从键盘上读取数据
int main()
{
FILE* fp = fopen("log.txt", "w");
char buffer[128];
int n = fread(buffer, 10, 1, stdin);
buffer[strlen(buffer)] = '\0';
printf("%s", buffer);
return 0;
}
stderr和stdout的使用效果一样,(他们有什么区别呢?后面会说)
open接口
第一个参数pathname,文件名
第二个参数,flags是打开文件的方式---------选项有O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY
第三个参数,创建文件时赋予文件的权限
第一个函数使用场景:文件已经存在
第二个函数使用的场景:文件不存在
O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY本质是宏,是用的位来标记的
位图标记是什么呢?
#define ONE (1 << 0)//1
#define TWO (1 << 1)//2
#define THREE (1 << 2)//4
#define FOUR (1 << 3)//8
void show(int flags)
{
if (flags & ONE) printf("hello function1\n");
if (flags & TWO) printf("hello function2\n");
if (flags & THREE) printf("hello function3\n");
if (flags & FOUR) printf("hello function4\n");
}
int main()
{
printf("------------------------------\n");
show(ONE);
printf("------------------------------\n");
show(ONE | FOUR);
printf("------------------------------\n");
show(ONE | THREE);
printf("------------------------------\n");
show(ONE | TWO | THREE | FOUR);
printf("------------------------------\n");
return 0;
}
测试O_CREAT, O_WRONLY
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
char buffer[128];
snprintf(buffer, sizeof(buffer), "hello world");
//这里不用将\0写进文件,因为字符串后面有\0是C语言的规定,和文件系统没关系
write(fd, buffer, strlen(buffer));
return 0;
}
再将写入内容改为aaaa
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
char buffer[128];
snprintf(buffer, sizeof(buffer), "aaaa");
//这里不用将\0写进文件,因为字符串后面有\0是C语言的规定,和文件系统没关系
write(fd, buffer, strlen(buffer));
这里说明了O_WRONLY是覆盖式的写入
再查看文件类型
为什么这里的文件权限是664,我们给的权限不是666?
因为操作系统有文件的权限掩码————>不懂权限掩码的可以点击这里跳转
我们就想要666的文件权限怎么办呢?
int main()
{
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
char buffer[128];
snprintf(buffer, sizeof(buffer), "aaaa");
//这里不用将\0写进文件,因为字符串后面有\0是C语言的规定,和文件系统没关系
write(fd, buffer, strlen(buffer));
return 0;
}
这里的umask影响的是进程的,不会影响整个操作系统
测试O_CREAT,O_WRONLY,O_TRUNC
打开文件即清空文件,再写入
int main()
{
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
char buffer[128];
snprintf(buffer, sizeof(buffer), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
//这里不用将\0写进文件,因为字符串后面有\0是C语言的规定,和文件系统没关系
write(fd, buffer, strlen(buffer));
return 0;
}
int main()
{
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
char buffer[128];
snprintf(buffer, sizeof(buffer), "bbbbbbbbbbb");
//这里不用将\0写进文件,因为字符串后面有\0是C语言的规定,和文件系统没关系
write(fd, buffer, strlen(buffer));
return 0;
}
测试O_CREAT,O_WRONLY,O_APPEND
int main()
{
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
char buffer[128];
snprintf(buffer, sizeof(buffer), "aaaaaa\n");
//这里不用将\0写进文件,因为字符串后面有\0是C语言的规定,和文件系统没关系
write(fd, buffer, strlen(buffer));
return 0;
}
访问文件的本质
文件其实是在磁盘上的,磁盘就是访问外设,访问磁盘文件就是在访问外设
根据计算机体系结构可知:几乎所有的库,只要是访问硬件设备,必定要封装系统调用!!
所以我们可推出C语言的库函数printf/fprintf/fscanf/fwrite/fread/fgets/fopen都对进行了系统调用接口的封装
C语言:
“w”------>O_WRONLY| O_CREAT | O_TRUNC, 0666
“r”------->O_WRONLY | O_CREAT | O_APPEND, 0666
则可说明open函数返回的本质是数组下标
证明:
int main()
{
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
printf("fd4: %d\n", fd4);
return 0;
}
为什么是从3开始呢?因为0、1、2默认被标准输入、标准输出、标准错误占用了
证明:
int main()
{
close(1);//将标准输出关闭
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("hello world\n");
return 0;
}
按照预期,应该向屏幕上打印hello world
可以看到并没有向文件里写入,而是写入到了文件里。
因为下标为1的数组内容不在是标准输出流的信息,而换为了log.txt的信息
那么FILE 和 fd有什么关系呢?
int main()
{
printf("stdin->fd: %d\n", stdin->_fileno);
printf("stdin->fd: %d\n", stdout->_fileno);
printf("stdin->fd: %d\n", stderr->_fileno);
return 0;
}
可以看出FILE是封装了fd的结构体
重定向
文件描述符对应的分配规则是什么?
从0下标开始,寻找最小的没有使用的数组位置,它的下标就是新文件的文件描述符
证明:
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
const char* msg = "hello Linux\n";
int cnt = 5;
while (cnt)
{
write(1, msg, strlen(msg));
cnt--;
}
close(fd);
return 0;
}
本来要向屏幕打印,但没有打印到屏幕,而是写到了文件里面
这就是输出重定向:本来应该写到显示器上,却写到了文件里
重定向的本质:上层用的fd不变,在内核中更改fd对应的struct file*的地址
这样的话也很麻烦,所以系统也提供了这样的接口,让我们能更好的使用
接口作用:在struct file* fd_array[]里将以fildes为下标的内容覆盖以fildes2为下标的内容
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
//重定向
dup2(fd, 1);
close(fd);
const char* msg = "hello Linux\n";
int cnt = 5;
while (cnt)
{
write(1, msg, strlen(msg));
cnt--;
}
close(fd);
return 0;
}
可以看出和之前的代码是相同的效果
追加重定向实现:
将上述代码一点就实现了追加重定向
输入重定向实现:
int main()
{
int fd = open("log.txt", O_RDONLY);
dup2(fd, 0);
char buffer[128];
int s = read(0, buffer, sizeof(buffer) - 1);
printf("%s", buffer);
close(fd);
return 0;
}
本来要从键盘里读取的,但从文件里读取了
stdout和stderr有什么区别?
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC);
dup2(fd, 1);
fprintf(stderr, "hello error message\n");
fprintf(stderr, "hello error message\n");
fprintf(stderr, "hello error message\n");
fprintf(stderr, "hello error message\n");
fprintf(stdout, "hello normal message\n");
fprintf(stdout, "hello normal message\n");
fprintf(stdout, "hello normal message\n");
fprintf(stdout, "hello normal message\n");
close(fd);
return 0;
}
一个文件描述符为1另一个文件描述符为2
如何理解Linux一切皆文件
详解缓冲区
int main()
{
const char* str = "hello write\n";
const char* fstr = "hello fwrite\n";
//C函数调用
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
printf("hello printf\n");
//系统调用
write(1, str, strlen(str));
fork();
return 0;
}
正常,符合我们的预期
再进行重定向
问题一:为什么C接口的函数重定向后,在该文件里写了两次?
int main()
{
const char* fstr = "hello fwrite";
//C函数调用
fprintf(stdout, "hello fprintf");
fwrite(fstr, strlen(fstr), 1, stdout);
printf("hello printf");
close(1);
return 0;
}
问题二:为什么没有打印出来,也不能重定向到文件里?而加了\n能打印出来?
int main()
{
const char* fstr = "hello fwrite\n";
//C函数调用
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
printf("hello printf\n");
close(1);
return 0;
}
从问题一我们能看出C语言接口写了两次,而系统调用只写了一次。
说明这个缓冲区一定不在操作系统内部,不是系统级的缓冲区。
从问题二我们可以看出c接口没打印出来,缓冲区没刷新
说明c接口无\n不会刷新它的缓冲区
缓冲区剖析图
缓冲区刷新策略
a. 有立即刷新 b.行刷新 c.满刷新 d.用户强制刷新 e.进程退出
为什么要有缓冲区?
1.提高读写效率 2.适配格式化
缓冲区就如我们生活中的快递站。
1.我们能直接将东西(数据)给快递站,不用自己去--------效率高
2.快递公司不能只为你一个快递去送快递,除非有特殊需求---------缓冲区的刷新策略
printf(“%d”,a);输入的时候需要格式化输入,而缓冲区能配合格式化转换为字符串存储到缓冲区
缓冲区在哪里呢?
FILE结构体里面,FILE里面还有对应打开的文件,缓冲区字段和维护信息
回归问题一
向屏幕打印采用的是行刷新,所以遇到\n就打印了
重定向到了log.txt文件,则不再采用行刷新了而是满刷新,遇到\n不会再刷新缓冲区
我们fork之后,退出进程,会导致缓冲区刷新,而刷新缓冲区本质就是修改数据,则缓冲区会被写时拷贝一份,使父进程和子进程都有各自的一份缓冲区了,两个进程退出,会刷新两次缓冲区,所以会有两份。
实现fclose fwrite fopen
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
const int SIZE = 128;
struct myFILE
{
int fileno;
char buffer[SIZE];
int flag;
int pos;
};
myFILE* myfopen(const char* filename, const char* model)
{
int tfd = -1;
if (strcmp(model, "w") == 0)
{
tfd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
}
else if (strcmp(model, "r") == 0)
{
tfd = open(filename, O_RDONLY);
}
else if (strcmp(model, "a") == 0)
{
tfd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
}
else
{
//其他选项...
}
myFILE* file = new myFILE;
file->fileno = tfd;
file->pos = 0;
file->flag = FLUSH_ALL;
//file->flag = FLUSH_LINE;//需要手动更改
return file;
}
int myfwrite(myFILE* file, const char* str, size_t size)//简单实现--接口不同
{
memcpy(file->buffer + file->pos, str, size);
file->pos += size;
if (file->flag & FLUSH_ALL)
{
if (file->pos == SIZE)
{
write(file->fileno, str, size);
file->pos = 0;
}
}
else if (file->flag & FLUSH_LINE)
{
for (int i = 0; i < file->pos; ++i)
{
if (file->buffer[i] == '\n')
{
write(file->fileno, str, size);
file->pos = 0;
}
}
}
else if (file->flag & FLUSH_NOW)
{
write(file->fileno, str, size);
file->pos = 0;
}
return size;
}
void myfflush(myFILE* file)
{
if (file->pos > 0)
{
write(file->fileno, file->buffer, file->pos);
file->pos = 0;
}
}
void myfclose(myFILE* file)
{
if (file == nullptr) return;
myfflush(file);
close(file->fileno);
delete file;
}
测试:
#include "mystdio.hpp"
int main()
{
myFILE* fp = myfopen("log.txt", "w");
const char* str = "hello myfwrite\n";
myfwrite(fp, str, strlen(str));
myfclose(fp);
return 0;
}
认识硬件–磁盘
上面谈的是被打开的文件,那么未被打开的文件是如何被管理的呢?
1.路径问题2.存储问题3.获取的问题(文件属性+文件内容)4.效率
磁盘的物理结构:
如何定位一个扇区呢?
先定位在哪一个磁头---------就能定位在哪个面了
再定位磁道---------磁头来回摆动的时候就是在定位磁道
再定位对应的扇区-----------盘片旋转的时候就是让磁头定位扇区
这种硬件的定位方式叫做:CHS定位法
磁盘的逻辑结构:
将圆形结构抽象成直线----数组的形式
只要知道这个扇区的下标,就可以定位一个扇区了
在操作系统内,我们称这种地址为LBA(Logic Block Address 逻辑块地址)
这里就形成了逻辑扇区地址和物理地址的互相转换-----------LBA<----->CHS
不仅仅CPU有寄存器,其他外设也有(如磁盘)------硬件可以被CPU访问
理解文件系统
文件=文件属性+文件内容
Linux的文件属性是分批存储的,文件属性存储到inode里,文件内容存到data block里。一个文件只有一个inode,一个数据块只会被一个文件使用,可以通过inode找到对应的数据块。
问题:一个inode里面存了15个数据块的编号,那不是一个数据块只有4kb,那不是一个文件里只能存4*15kb?
这些数据块编号也分为直接索引和间接索引,直接索引的数据块保存的直接是文件的内容,而间接索引的数据块里面存的不是文件的内容,而存的是其他数据块编号。4kb * 1024 / 4(int的大小) = 1024个块,又可以索引1024个数据块了。
一个磁盘假设有500GB,500GB太大了不好直接管理,我们则采用分治的策略,分为若干个区域管理(100GB、100GB、150GB、150GB),但每个区任有100GB、任然不好直接管理,再进行分组,分为大约为5GB的组。每个组将这5GB的管理好即管理好了500GB
格式化:每一个分区再被使用之前,都必须提前将部分文件系统的属性(Super Block、Group Descriptor Table、Block Bitmap、inode Bitmap)提前设置就对应的分区中,方便我们后续使用这个分区或者分组。
OS中,对一个文件进行操作的时候,统一使用的是:inode编号
创建一个文件会发生什么事?先通过文件的路径找到分区,再从一个组里面通过inode Bitmap找到一个空闲的inode,并由0置1,填充inode信息,遍历BlockBitmap为它分配数据块个数,再将数据写入数据块中。
删除一个文件会发生什么事?先通过文件的路径找到分区,再通过inode找到分组,再找到其inode Bitmap并由1置0即可,blockMap也同样由1置0即可
对一个文件进行操作的时候,统一使用的是inode编号。问题:我们使用的时候不是inode,是文件名啊
如何理解目录:目录也是文件,也有自己的inode,当然也会有自己的数据块,那目录里的数据块存的是什么呢?存的是当前目录下文件名和inode的映射关系。
我们如何对一个文件进行操作呢?目的是找到其inode。文件名有了,如何找到文件名映射的inode的呢?目录里存的是文件名和inode的映射关系,我们则要找到该文件的目录,才能找到其文件名,和inode的映射关系,才能找到该文件的inode,才能对该文件进行操作。而目录的inode又如何找到呢?则要找到目录的目录,从此循环递归。所以是从根目录一直往下找,找到该文件。
这也就是为什么同一个目录下不能有同名文件----文件名和inode的映射必须是唯一的那这样每次都要从根开始递归下去找文件不是很慢?系统里有dentry缓存,提高了效率,这里不做过多讲解
理解软硬链接
软链接命令:ln -s [要链接的文件名] [软链接名]
软链接就如window下的快捷方式
可以看到要运行code文件,目录很深,不好操作
对目标文件建立软链接
硬链接命令:ln [要链接的文件名] [硬链接名]
取消链接命令: unlink [链接的文件名]
软硬链接的区别是什么?
是否具有独立的inode。软链接有独立的inode,可以被当作一个独立的文件来看待,而硬链接是没有独立的inode
建立硬链接,没有分配到独立的inode,则你一定用的是别人的inode。
建立硬链接本质:在目录下创建一个文件名映射同一个inode
也可以看到其硬链接数变为了2
删除myfile.txt文件,发现它的硬链接任然存在,并且和myfile.txt一样可以使用
什么时候一个文件算作被真正的删除呢?当一个文件的硬链接数变成0的时候
为什么创建了一个目录它的硬链接数直接为2了呢?因为目录里面还有.目录
Linux系统为什么不允许对目录建立硬链接?
因为会引起环路问题:
补充知识
物理内存和磁盘文件交换数据的单位为4KB
页框:物理内存的单位
页帧:磁盘文件中划分为的单位
为什么是4KB为单位呢?
1.在硬件上,若为1KB则会增多IO访问的次数,导致效率变慢
2.在软件上,能基于局部性原理–预加载机制
操作系统是如何管理内存的?
先描述,再组织
将物理内存中的每个单位用结构体描述起来,再用数组把它们给组织起来,对物理内存管理就变为了对数组内容的增删查改
struct page{
unsigned long flags
atomic_t _count;
…//属性
};
struct page mem_array[1047576];
4GB的空间会有1048576个页框,访问一个内存如0x11223344,只需&0xFFFFF000即可找到该内存对应的页号,找到其页号也就找到了其页框,则可对这一块内存进行操作了
每个文件打开都有属于自己的inode属性和文件页缓冲区。文件页缓冲区也就是之前提到的内核级缓冲区
动静态库
静态库命名方案:libXXX.a
动态库命名方案:libYYY.so
//mymath.h文件
extern int myerrno;
int Div(int x, int y);
int Mul(int x, int y);
int Add(int x, int y);
int Sub(int x, int y);
//mymath.cc文件
#include "mymath.h"
int myerrno = 0;
int Div(int x, int y)
{
if (y == 0)
{
myerrno = 1;
return -1;
}
return x / y;
}
int Mul(int x, int y)
{
return x * y;
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
//test.cc文件
#include <iostream>
#include "mymath.h"
using namespace std;
int main()
{
int a = 10;
int b = 0;
int ret = Div(a, b);
cout << "a / b = " << ret << " myerrno:" << myerrno << endl;
return 0;
}
当你是创作者,你要将你提供的方法给别人用,有两种方案:一是直接给源代码,二是不想给源代码,把自己的方法生成.o二进制文件给对方。最后再给.h文件(.h文件就相当于说明书,说明如何使用你的方法)
库的本质:将众多.o文件的集合
如何打包成静态库?
ar -rc libXXX.a [要打包的.o文件]
现在我们要使用这个库,如何使用呢?
编译出错,原因没包头文件
g++的-I选项:能帮你在这个路径下面找头文件
但还是出错了
g++的-L选项:能帮你找到库的位置
还是出错了
g++的-l选项:告诉要链接的库名的名字(注意:这里的名字不是libmymath.a,库名是去掉前缀和后缀)
终于成功了
ldd可以查看链接状态
为什么没有看到链接的mymath库呢?
g++默认的是动态链接,链接一个库,若系统中既链接了动态库又链接了静态库,则g++显示链接的是动态库,若只有静态库,则会显示静态库;若只有动态库,则会显示动态库
每次使用库都这么麻烦,有没有简单的方法?
1.将头文件和库放到系统会默认去查找的路径下面
这种操作的本质叫安装
为什么还是找不到库在哪里?
第三方库一定要使用g++的-l选项
2.在系统默认的文件路径下建立软链接
因为你在/usr/include/文件下链接的是myinclude目录,所以在头文件包含时要写成#include “myinclude/mymath.h”
如果你直接链接的是mymath.h头文件则不需要这样写,按照原来的#include “mymath.h”即可
如何形成动态库?
第一步:g++的-fPIC选项生成位置无关码。什么是位置无关码?后面会讲
第二步:g++的--shared选项生成动态库。
生成可执行文件
运行可执行文件
为什么运行失败呢?不是告诉了g++库文件路径和库名称码?而静态库可以直接运行呢?
当你把程序编译,和gcc已经没有关系了,运行的时候OS和shell也要知道库在哪里的,而你的库没有在系统路径下,OS无法找到。而静态库是在编译的时候就将.o文件编译到可执行文件里了,已经是可执行文件的一部分了。
任然可用之前的方案
动态库静态库原理
动态库在进程运行的时候,是要被加载的(静态库没有),并且所有可执行程序都可能用到同一个动态库,所以动态库被系统加载之后是会被所以进程共享的。
在可执行程序被编译好时,其内部代码是否有地址?
有,不仅OS里有进程地址空间,并且可执行程序也是按照进程地址空间(虚拟地址)进行编译的
若将动态库的地址加载固定的共享区地址是不可能的,因为你这个库不知道其他的库是否也映射到了这个位置,所以自己函数内部的代码只能采用偏移量。
而产生这个偏移量的过程就是g++的-fPIC选项,生成与位置无关码