一、回顾C和C++的文件操作
C语言文件操作
三个流
- C程序运行起来,会默认打开三个输入输出流
stdin ->键盘 stdout ->显示器 stderr ->显示器 - 这三个流都是FILE* 类型的,fopen返回值,文件指针类型
向显示器输出打印
#include<stdio.h>
int main()
{
const char * msg="you can see me!\n";
fputs(msg,stdout);
return 0;
}
结果演示: C++中会提供4个流:cin cout cerr clog C++的文件操作:
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
ofstream out ("./log.txt",ios::out | ios::binary);
if(!out.is_open())
{
cerr<<"open err"<<endl;
return 1;
}
int cnt=5;
while(cnt--)
{
string msg="hello\n";
out.write(msg.c_str(),msg.size());
}
out.close();
return 0;
}
结果展示: fputs函数向一般的文件或者硬件设备都能写入!文件、硬盘等都是硬件!所以我们可以得出:一切皆文件!
?总之:最终的文件操作都是访问硬件——显示器、键盘、磁盘 从以前的学习我们知道,操作系统是硬件的管理者,所有语言上对于“文件”的操作,都必须贯穿整个操作系统!!!但是访问操作系统是需要系统调用接口的! 几乎所有的语言(都封装了):fopen,fclose,fread,fputs等底层一定要使用OS提供的系统调用!
二、系统文件IO
open函数
open接口介绍
函数名称 | 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); | 参数 | 见后面 | 返回值 | 大于等于0的整数:成功(即文件描述符) -1:失败 |
参数说明:
- pathname:要打开或者要创建的含有路径的文件名
- flags:文件状态标志,表示打开文件的方式
1??主标志: ?O_ RDONLY :以只读方式打开文件 ?O_ WRONLY :以只写方式打开文件 ?O_ RDWR :以可读可写方式打开文件 这三个常量必须指定一个,并且只能有一个 主标志是互斥的,使用其中一种则不能再使用另外一种。除了主标志以外,还有副标志可与它们配合使用,副标志可同时使用多个,使用时在主标志和副标志之间加入按位或(|)运算符。 2??副标志: ?O_CREATE :若文件不存在,则创建它。需要使用mode选项,来指明新文件的权限 ?O_APPEND :追加写 - mode:如果文件被创建,指定其权限(8进制)
深入解读:系统调用的第二个参数flags ?它的返回值是int类型的,这里就有一个易错点,传参时并不是按照整型来传入参数的,如果传入整型,一次只能传入1个标志,无法传入多个标志,所以这里是通过位操作来传入参数的,一个flag有32个比特位,一个比特位代表一个标志,那么理论上一个flags可以传递32种不同的标志,实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
#define O_WRONLY 0x1
#define O_RDONLY 0x2
#define O_CREAT 0x4
这些宏都是只有一个比特位是1的数据,而且不重复,且为1的比特位是各不相同的
#define O_WRONLY 0000 0001
#define O_RDONLY 0000 0010
#define O_CREAT 0000 0100
我们就可以先通过或运算设置标志位:
O_WRONLY | O_CREAT
上面这个或运算之后就等价于“w”,以只读的方式打开 接下来在open函数内部就可以通过“与”运算来检测flag是否设置了某一选项:
int open()
{
if(O_WRONLY&flag)
{
return ;
}
if(O_RDONLY&flag)
{
return ;
}
if(O_CREAT&flag)
{
return ;
}
}
打开fcntl.h 头文件观察更多细节: 测试open函数:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd=open("./log.txt",O_WRONLY | O_CREAT);
if(fd<0)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
我们发现新创建的log.txt权限是混乱的: 于是带上第三个参数,并且指定好初始权限:
int main()
{
int fd=open("./log.txt",O_WRONLY | O_CREAT,0644);
if(fd<0)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
确实权限设置好了: 我们现在可以对比操作系统层面的open 和C语言层面的fopen 了: 由于C语言是在系统之上的,它给我们做了封装,我们就不必再关心这些细节了。 接下来隆重介绍我们open的返回值:
#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{
int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
printf("open error!\n");
return 1;
}
printf("fd:%d\n",fd);
close(fd);
return 0;
}
打印出来: 如果我们再创建一批新的文件呢?
[sjj@VM-20-15-centos 2022_4_20]$ cat myfile.c
#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{
int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);
int fd1=open("./log1.txt",O_WRONLY|O_CREAT,0644);
int fd2=open("./log2.txt",O_WRONLY|O_CREAT,0644);
int fd3=open("./log3.txt",O_WRONLY|O_CREAT,0644);
int fd4=open("./log4.txt",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
printf("open error!\n");
return 1;
}
printf("fd:%d\n",fd);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
printf("fd4:%d\n",fd4);
close(fd);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
[sjj@VM-20-15-centos 2022_4_20]$ ./myfile
fd:3
fd1:4
fd2:5
fd3:6
fd4:7
既然小于0是失败,为什么没有0、1、2呢? 这里的0、1、2就分别对应我们的:标准输入、标准输出、标准错误 当我们的程序运行起来之后,就变成了进程,默认情况下,OS会帮助我们的进程打开三个标准输入输出!
- 0:标准输入–>键盘
- 1:标准输出–>显示器
- 2:标准错误–>显示器
显然对比我们之前C语言学习的文件操作,他们之间存在着一些联系!
read函数
ssize_t read(int fd, void *buf, size_t count);
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
int fd=open("./log.txt",O_RDONLY);
if(fd<0)
{
perror("open");
return 1;
}
char buf[1024]={0};
ssize_t s=read(fd,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
printf("%s\n",buf);
}
close(fd);
return 0;
}
文件描述符 fd
?通过对于open函数的学习,我们知道文件描述符就是open函数的返回值,而且是一个小整数。open是一个系统层次的调用,意味着这个数组下标是OS给我们的!
?所有的文件操作,表现上都是进程执行对应的函数!即进程对文件的操作–>要操作文件必须要先打开文件!–>打开文件意思就是将文件的相关信息加载到内存之中! –>可是操作系统中存在大量的进程,(进程:文件=1:n)–>系统中存在可能更多开开的文件!–>操作系统要把打开的文件在内存中管理起来,那么如何管理?
六字真言:先描述,再组织!
拓展:如果文件没有被打开呢?那么它存放在哪里呢? 那必然是存在磁盘上面!
我们知道Linux是用C语言写的,那么如何描述一个结构呢? 那就是用结构体struct! 我们用一个struct file结构体来描述一个打开文件的属性信息:
struct file{
}
?我们知道,文件描述符是从0开始的小整数,0、1、2被默认打开占用了,当我们要打开新文件时,OS在内存中创建相应的数据结构来描述文件,于是就有了struct file结构体,而进程执行相应的函数调用(比如open函数),必须要让进程和文件产生关联,所以在进程的task_struct中有一个struct files_struct* files的的指针,它指向一个struct files_struct的结构体表,该结构体表内最重要的与文件能够产生联系的就是有一个struct file* fd_array[ ]的指针数组,每一个元素都是一个指向打开文件的指针,通过数组下标的访问我们就能访问文件的相关信息,我们返回给上层的就是一个一个的数字。
?所以本质上,文件描述符就是该数组的下标,只要拿着文件描述符我们就能找到、访问、修改对应的文件!即write、read等在OS上层的函数,通过文件列表找到该进程打开的文件,传入fd就可以索引到需要操作的文件了。
如何理解一切皆文件
?所有硬件的读写方法一定是不一样的,有些硬件可能只有读,有些硬件可能只有写,那么在Linux中是如何做到一切皆文件的呢?我们在内核中有一个软件封装的虚拟层VFS,它将不同的文件系统整合到了一起,并且提供了同一的应用程序接口(API)供上层的应用程序使用。 ?例如:当我们需要向磁盘读写时,上层统一使用read和write,但是底层却是在磁盘对应的文件中调用特定的read和write方法(这里用的是函数指针)! 这样从用户层,以统一的视角,来操作下层文件,所以linux下一切皆文件!
文件描述符分配规则
观察一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.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);
close(fd);
return 0;
}
发现fd:3 再来看看:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("./log.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现fd:0 或者fd:2
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
重定向
如果我们把1给关掉,那么会出现什么现象呢?
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
close(1);
int fd = open("./log.txt", O_CREAT|O_WRONLY,0644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
return 0;
}
这里本该向显示器上打印的,但是却往文件中显示了内容!——这就叫做输出重定向! C语言中printf的打印,本质是向标准输出(stdout)打印,stdout又是FILE*类型的,FLIE是一个结构体,这里面必定封装了对应键盘文件的fd。 追加重定向的原理: 在打开文件时,再或上一个O_APPEND就可以实现了。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
close(1);
int fd = open("./log.txt", O_CREAT|O_WRONLY|O_APPEND,0644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
return 0;
}
输入重定向的原理: 它与输出重定向恰好相反
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
close(0);
int fd=open("./log.txt",O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
char line[128];
while(fgets(line,sizeof(line)-1,stdin))
{
printf("%s",line);
}
return 0;
}
使用dup2系统调用
函数名称 | dup2 |
---|
函数功能 | 复制一个文件描述符 | 头文件 | #include<unistd.h> | 函数原型 | int dup2(int oldfd,int newfd); | 参数 | oldfd:被复制的文件描述符 newfd:新的文件描述符 | 返回值 | >-1:复制成功,返回新的文件描述符 -1:出错 |
我们之前讨论研究的都是close(1)、close(0)关闭了文件描述符的情况,但是实际不没有那么理想! 如何在两个文件都打开的情况下,完成重定向呢? 这里就要用到这个函数dup2帮我们实现了。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd=open("./log.txt",O_CREAT|O_WRONLY,0644);
if(fd<0)
{
perror("open");
return 1;
}
dup2(fd,1);
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
}
结果展示: 本来该显示到显示器上的内容,现在写入到了文件中,我们相当于用3去覆盖掉了1,但是上层却只认识数组下标1,所以重定向写入到了文件中。(stdout也是只认定1下标)。
缓冲区
为什么会出现这种现象呢? 原来是C语言给我们提供了缓冲区!
从用户层C缓冲–>OS内核文件缓冲区的刷新策略: 1?? 立即刷新(不缓冲) 2?? 行刷新(行缓冲\n)比如:显示器打印 3?? 缓冲区满才刷新(全缓冲)比如:往磁盘文件上打印 4?? 进程退出的时候,会刷新FILE内部的数据到OS缓冲区 从OS内核文件缓冲区–>硬件,上述刷新策略同样适用!OS操作系统会帮我们维护,我们不必关心其中的细节。
C缓冲区的数据如何刷新到OS文件内核缓冲区?
答案:一定需要fd! 因为中间有一个系统调用的接口,就是凭借着fd文件描述符,才能刷新数据到OS内核缓冲区的! ?现在我们就能解释为什么调用close(fd) 之后,不能在log.txt文件中写入内容了。 原因就是,原来的printf函数打印到显示器上是行缓冲的(有\n),现在重定向到了文件之中,变成了全缓冲,而且关闭了fd,C缓冲区的数据就不能刷新到OS文件内核缓冲区,而数据在C语言缓冲区可能并没有被写满(因为变成全缓冲了),即使最后进程退出,OS内核缓冲区也没有数据被刷新到文件中。 我们可以在close(fd) 之前,使用fflush(stdout) 强致刷新数据到OS内核缓冲区,就能解决这个问题了。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
int main()
{
close(1);
int fd=open("./log.txt",O_CREAT|O_WRONLY,0644);
if(fd<0)
{
perror("open");
return 1;
}
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
const char* buf="hello write\n";
write(fd,buf,strlen(buf));
close(fd);
return 0;
}
效果展示: 而系统层面的函数write,没有语言级别的缓冲区,用write函数时,数据是直接写入内核缓冲区的,即使close(stdout)了,最后进程退出的时候,也是会刷新OS内核缓冲区写入到文件log.txt的
再来看看FILE结构体里面的细节:
typedef struct _IO_FILE FILE;
struct _IO_FILE {
int _flags;
#define _IO_file_flags _flags
char* _IO_read_ptr;
char* _IO_read_end;
char* _IO_read_base;
char* _IO_write_base;
char* _IO_write_ptr;
char* _IO_write_end;
char* _IO_buf_base;
char* _IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset;
#define __HAVE_COLUMN
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
再来看看fork之后的情况:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
int main()
{
const char* msg1 = "hello 标准输出\n";
write(1,msg1,strlen(msg1));
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
fork();
return 0;
}
为什么库函数都输出了两次,write系统调用只输出了一次?
肯定和fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。 printf/fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后 ,但是进程退出之后,会统一刷新,写入文件当中。 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,缓冲区里面的数据也是数据,数据代码父子各有一份,子进程也就有了同样的一份数据,随即产生两份数据。 write 没有变化,说明系统调用函数没有所谓的C语言缓冲区。它是直接往OS内核缓冲区写入,再由操作系统刷新到文件之中。
三、理解文件系统
文件=文件内容+文件属性
如果一个文件没有被打开,它存放在哪里呢?
答案就是在磁盘上面存放着。
磁盘划分
磁盘解剖图: 站在OS的角度,我们可以将平时的磁盘线性化处理,将一圈一圈的变成一个长条方便处理。 从硬盘的构造我们可以知道,访问物理磁盘的最小单位就是位于某个盘面上的某个磁道上的一个扇区,为了存取数据将大磁盘划分成为小空间步骤: 1、需要分区 2、格式化(写入文件系统)
了解文件系统: Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的
Block Group (块组):ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子Super Block (超级块):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了Group Descriptor Table (块组描述符):块组描述符,描述块组属性信息Block Bitmap (块位图):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用inode Bitmap (inode位图):每个bit表示一个inode是否空闲可用。inode Table (inode节点表):存放文件属性如文件大小,所有者,最近修改时间等Date block (Data数据区):存放文件内容
在Linux中,文件名在系统层面是没有意义的,这是拿给用户看的,真正标识一个文件的是文件的inode编号!一般情况下,一个文件一个inode,一个inode对应一个inode编号。
inode Table中有一个结构体:
struct inode{
int inode_number;
int Data_blocks[NUM];
}
每当我们touch创建一个文件时,就在系统的inode Table找一个空闲的地方,给你分配相应的空间存放文件的属性信息,再在Data blocks寻找一个空闲块区域,在里面存放文件的内容信息。
但是也存在着问题,当我们需要新创建文件时,系统中原来就有很多文件,inode Table 和Data blocks存在很多,难道我们需要遍历一遍找寻空闲的区域存放新文件的内容吗? 这里就引出了我们的位示图法,来表示。
从右向左 比特位的位置含义: inode编号 比特位的内容含义: 特定的inode是否被占用! 内容为0就是没有被占用,1就是被占用了
现在我们不用再遍历inode Table 和Data blocks,只需要遍历位图就能很快的找到哪个空间时空闲的,拿到inode编号!(这样效率非常高!)
目录文件
?同样目录也是文件,它也有自己的inode,目录里面也有数据块存放存放着文件名和映射到inode编号的文件指针,我们用户所认识的是文件名,而OS认识的是inode编号,dentry结构体中存放了表示文件inode结点编号的成员i_node,根据i_node,文件系统就可以在磁盘上找到改文件对应的inode结点,通过inode结点中,文件物理位置就可以访问到文件在磁盘上的位置,这样文件系统就可以找到需要操作的文件了。 ?所以我们可以理解为,建立起一个字符串(文件名)和一个inode编号(数字)之间的映射关系
为什么拷贝文件时间比较久一点,而删除文件时很快就可删除?
我们删除只需要将inode bitmap由1置为0即可,block bitmap由1置为0,就是把一个文件是否有效的信息标志给删除了,并不需要删除它的属性和内容信息。等你想要再次存放数据时,只需要把原来的数据给覆盖即可。
软硬链接
软连接
概念:软连接是通过名字引用另外一个文件! 创建指令:ln -s +源文件名 软连接文件名 删除链接:unlink +软连接文件名 使用软链接 简单明了的解释软连接,就是一个快捷方式,我们为了避免执行程序,需要带上冗长的路径,可以创建软连接 可以类比Windows下的快捷方式:
硬链接
概念:硬链接是通过inode引用另外一个文件! 创建指令:ln (不带-s)+文件名 删除指令:unlink +硬链接文件名 硬链接的使用: 1??重命名 2??多个文件指向同一个inode 观察连接数 确实就是多个文件指向同一个inode 这也同时印证了为什么一个目录创建时,有两个链接,这里有一个隐藏的文件点. ,指向的是当前目录
对比软硬链接
?观察inode我们发现,软连接是有自己独立的inode编号的,而硬链接没有,都是同一个inode编号。所以软连接是一个独立文件!它有自己的属性和数据块!数据块中保存的是所链接的文件路径+文件名。 硬链接本质是根本就不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为自己没有独立的inode编号!
三个时间ACM
指令:stat 文件名 显示文件或文件系统的详细信息
Access 最后访问时间Modify 文件内容最后修改时间Change 文件属性最后的修改时间
Access 时间在文件最近被访问的时间,我们发现实际操作中,文件的时间貌似没有变化?
?这是因为在较新的Linux内核中,Access时间不会被立即刷新,而是有一定的时间间隔,OS才会自动更新Access时间。Access时间其实需要刷新的最快,因为我们有时不停的访问和修改文件。这样频繁刷新可能会导致系统变得卡顿,Linux作了优化,隔一段时间再来刷新Access时间。
当我们修改文件内容时,为什么modify和change时间有时都会改变?
?这是因为我们增加内容或者删除信息时,可能会改变文件的属性信息,比如“文件大小”属性。
补充:Makefile与gcc会根据Modify时间,来判定源文件和可执行程序谁更新,从而指导系统那些源文件需要被重新编译! 谢谢观看!
|