?回顾C文件的接口
在学习C语言时我们了解了一些C语言的对于文件操作的接口。 其中有fopen、fclose、fputc、fgetc、fputs、fgets、fprintf、fscanf、fread、fwrite等
用一段代码简单回顾一下:
#include<stdio.h>
#include<string.h>
int main()
{
FILE* fp=fopen("myfile","a+");
if(!fp)
{
printf("fopen error\n");
}
int count=5;
const char *m="hello linux\n";
while(count--){
fwrite(m,strlen(m),1,fp);
}
fclose(fp);
return 0;
}
我们先要了解,把内容写入文件中,先要有这个文件,然后就是要打开这个文件。 在上面的代码中
FILE* fp=fopen("myfile","a+");
第一个参数:文件的路径/文件名(不带路径会在当前路径下创建这个文件)。 当前路径:当前进程运行的路径。 第二个参数:就是以怎样的方式来。
在学习Linux时,我们经常听说“一切皆文件“。
那么显示器、键盘是文件吗? 在C语言时,我们经常用printf()函数来把内容显示到显示器上。 而现在,我们不用printf()函数来打印内容。
#include<stdio.h>
#include<string.h>
int main()
{
char *m="hello linux\n";
fwrite(m,strlen(m),1,stdout);
return 0;
}
这样我们可以了解,显示器也可以看作文件,也可以用fwrite()函数来写入。
重点来了:任何进程在运行时,都会默认打开三个输入输出流。 分别是: 标准输入(键盘)stdin 标准输出(显示器)stdin 标准错误(显示器)stderr 这三个流的类型都是FILE*,文件指针。
?系统文件I/O
文件操作除了上述的C接口以外,还有我们的系统接口来进行对文件的操作。 我们用C接口操作文件在Linux上跑,其实是C在调用Linux的系统接口来完成的。 所以说,C库文件的接口是对系统调用接口的一次封装。
第一个接口:open接口,与C的区别是前面没有f。
#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);
参数分别是:路径或文件名、选项、权限。
选项:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个 - O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND: 追加写
其中注意的是这些接口的返回类型是int。 文件打开成功后,会返回一个较小的非负整数,表示该文件的文件描述符。 失败返回-1。
用一段代码来感受一下吧。
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
umask(0);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
char *buf="hello linux\n";
write(fd,buf,strlen(buf));
close(fd);
return 0;
}
其中一: open中的0666,表示创建文件的时候文件权限的666。当然这要设置一下默认掩码。 其中二:O_WRONLY|O_CREAT 表示如果有该文件就对该文件以只写的方式打开,如果没有就创建这个文件,权限为666,以只写的方式打开。
为什么要用O_WRONLY|O_CREAT来表示呢? 不难看出,这些用大写字母来表示的选项是用宏。这些宏都是对应一个bit位,像位图一样。 我们在传O_WRONLY|O_CREAT的时候, 会用if(O_WRONLY&F)来判断这个选项等等。
而在write中第一个参数是文件的描述符。而文件描述符又是什么呢?
?文件描述符
我们先用一段代码来感受文件描述符:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
int main()
{
umask(0);
int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
int fd2=open("myfile",O_WRONLY|O_CREAT,0666);
int fd3=open("myfile",O_WRONLY|O_CREAT,0666);
int fd4=open("myfile",O_WRONLY|O_CREAT,0666);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
printf("fd4:%d\n",fd4);
return 0;
}
open执行成功返回一个较小的非负整形,也就是文件描述符。 通过上面的代码执行效果来看,有点像一个数组的下标。 其实这就是一个数组的下标,其中数组的0、1、2下标分被键盘(标准输入)、显示器(标准输出)、显示器(标准错误)给占了。 所以分配下来的是3、4、5、6。
而为什么是数组呢? 那么我们先要了解内存文件和磁盘文件了。
上面的代码创建了myfile文件,其文件的属性(文件大小、文件名、最近一次修改文件的时间等)会以struct file结构体在内存中保存起来。 而该文件的内容是在磁盘上的,也就是磁盘文件。 而这些struct file会被操作系统用双链表的形式来组织起来,和PCB类似。
而一个进程创建会创建PCB,其中PCB中有一个指针指向一个叫files_struct的结构体,其结构体中有一部分是以指针数组的形式存在的,其中存放的内容就是struct file结构体的地址。
进程通过文件描述符找到这个存放文件地址的地放,进而来对文件进行操作。
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。 于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进 程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
文件描述符的分配规则
如果我们close(0)
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
close(0);
umask(0);
int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
printf("fd1:%d\n",fd1);
return 0;
}
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
我们如果关闭close(1)
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
close(1);
umask(0);
int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
char *duf="hello linux\n";
write(1,duf,strlen(duf));
return 0;
}
?FILE
在学习C语言中的对文件操作的函数中有FILE*类型的。
FILE *fopen(const char *path, const char *mode);
那么FILE*是什么呢?
FILE是一个结构体,FILE*是一个结构体的指针。 我们都知道,C库中的IO相关的函数其实是对系统调用的封装。 在系统调用的IO型接口中,open函数的类型是int型。
int open(const char *pathname, int flags, mode_t mode);
open函数返回的是一个文件描述符。可以通过文件描述符来找到对应的文件。
而FILE结构体中就有一个int型的变量来表示这个文件描述符,这就是为什么C中的IO也可以找到对应的文件,这就是一种封装。
我们来看一下FILE结构体中的代码。
在/sur/include/stdio.h 可以找到
FILE中的int _fileno就是对文件描述符的封装。
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;
我们来看下面一段代码:
#include<stdio.h>
#include<sys/stat.h>
#include<sys/tyoes.h>
#include<fcntl.h>
#include<string.h>
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
const char* arr="hello linux\n";
fwrite("arr",strlen(arr),1,stdout);
return 0;
}
字符串并没有打印到显示器上,而是被写入到了myfile文件中。
为什么呢?
在这之前先要了解,C中的stdin、stdout、stdree这三个流都是FILE*型的,并且这三这FILE中的文件描述符被固定为0、1、2。这就是为什么在C中用stdin、stdout、stderr就能找到键盘、显示器、显示器。
而在上面代码中关闭了1,myfile的文件描述符是1,所以在fwrite函数中用stdout还是写入到了myfile文件中。这就是为什么显示器上没有打印,而写入到了myfile文件的原因。而通过这段代码,我们现在应该要了解FILE*是什么了。
最后,fopen究竟做了什么? 1、给调用的用户申请struct FILE结构体变量,并返回地址(FILE*) 2、在底层通过open打开文件,并返回fd,把fd填充进FILE变量的fileno中。
缓冲区
有两段代码:
#include<stdio.h>
#include<unistd.h>
void A()
{
printf("hello linux\n");
sleep(3);
}
void B()
{
printf("hello linux");
sleep(3);
}
int main()
{
A();
B();
return 0;
}
其中A是先显示hello Linux,再等待3秒。 B是先等待3秒,再显示hello Linux。
把内容回显给显示器时,内容先被写入缓冲区,采用的是行缓存,当遇到\n时就会刷新缓冲区,把内容写人显示器中,当缓冲区内容被写满时也会刷新缓冲区。
缓冲区有:
- 无缓冲
- 行缓冲:遇到\n就会把缓冲区\n之前的内容刷新出来,否则等缓冲区写满。效率和和用性做的平衡。
- 全缓冲:等缓冲区写满
(常见对显示器内容刷新,用的是行缓存,这样才能更快的看到我们的内容)
看代码:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
const char*mag2="hello write\n";
write(1,mag2,strlen(mag2));
fork();
return 0;
}
运行结果是:
hello printf
hello fprintf
hello write
但是我们对该进程进行输入重定向到文件中:./a.out > myfile
hello write
hello printf
hello fprintf
hello printf
hello fprintf
因为:
当我们重定向后,文件描述符1已经不表示显示器了,而是我们的文件。这时候,缓冲区采用的是全缓存。 系统调用的IO接口是无缓冲,可以直接写入。 当缓冲区中存放了“hello printf\n”和“hello fprintf\n”时,创建了子进程,在return 0;之前进行了写时拷贝,所以最后打印了两次字符串。
这里面的缓冲区是C提供的,也是由FILE结构体进行维护。 缓冲区是在内存中的,在用户层。 缓冲区的数据刷新不是直接刷新到文件中,而是要经过内核区再写入到文件,这里有OS自己的刷新机制,这里不谈(我还没学到,哈哈)。
fclose和close
fclose:在关闭1之前,刷新了C中的缓冲区。内容可以被写入到文件内。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
char *arr="hello linux\n";
fprintf(stdout,arr);
fclose(stdout);
return 0;
}
close:由于采用了全缓,当close(1)时,系统调用的看不见C的缓冲区,没有刷新缓冲区就关掉了,故没有写入到文件中。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
char *arr="hello linux\n";
fprintf(stdout,arr);
fclose(stdout);
return 0;
}
调用fclose是先调用ffiush,再调用close。
?dup2系统调用
在上面的重定向中,我们要先close(1),再打开文件,这样好繁琐。我们有一个更简单的方法。
int dup2(int oldfd, int newfd);
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main()
{
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
char *arr="hello linux\n";
close(1);
dup2(fd,1);
printf("%s",arr);
return 0;
}
?理解文件系统
文件系统是Linux的一个重要部分,在Linux中玩了有一段时间,我一直有一个困惑,文件是怎么创建出来的?通过学习,慢慢的我自己有了一点了解。
文件=文件的属性+文件的内容,我们在查看文件大小时,显示的是文件内容的大小,其属性信息并没有算在里面,这说明了,文件的属性和文件的内容是分离存储的,在磁盘上。文件属性叫做元信息。
我们先简单了解磁盘: 磁盘有扇区、磁道、柱面、磁头…… 文件的写入到磁盘中,会对磁盘寻址,其中会对柱头、磁道、扇面来寻找要写入的内容的地方。
假设磁盘的大小为500GB,对这么大的空间进行管理,系统采用了分区(就像中国也有省,市,县一样)
inode
inode是任何一个文件属性的集合,Linux中几乎每一个文件都有一个inode编号。 文件的元信息就是保存在inode中的,inode是一个结构体。 上图为磁盘文件的系统图(内核内存映射肯定有所不同),磁盘是一个典型的块设备,磁盘的分区被划分为一个个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的信息被破坏,可以说整个文件系统结构就被破坏了
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的可以在了解一下
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- inode Table:存放文件属性 如 文件大小,所有者,最近修改时间等
- 数据区(Data blocks):存放文件内容
数据区中是一个一个的块,每个块的大小是4KB,用来存放数据。(存在多级索引,我还没学,就不讲了)
inode结构体中有一个数组(int block[12])记录块(Data blockse)的位置。 一个普通文件的创建。
先要去inode位图中找到未被使用的inode,并申请下来把文件的属性记录其中,如果要对该文件写入内容,则系统会根据内容的大小去块位图中申请所需要的空闲块,并写入内容。内核在inode上的磁盘分布区记录了上述块列表。之后,内核会把该文件的inode编号和文件名添加到所在目录文件中。该文件的inode编号和该文件的文件名对应起来。
目录的创建
目录也是文件,也有自己的inode编号。目录在创建的过程中和上面普通文件的创建有点类似,不同的是,目录文件的内容是存放目录下的文件名和inode指针,使这些文件名和inode指针一一对应起来。 ls 命令: ls -l 命令 这也可以看出,目录和文件之前的联系。
文件的删除
文件的删除并没有那么复杂,只要把对应inode的位图中的数据改掉(把1置成0),对应块位图数据也修改掉(把1置成0)。这也就是为什么删除的文件可以恢复过来的原因,只要把位图再置回来。
创建一个新文件主要有一下4个操作:
- 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。 - 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。 - 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。 - 添加文件名到目录新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466 abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
硬链接
ln 文件名 要创建的文件名
这两个文件的inode号相同,说明myfile-s不是一个独立的文件,只是在目录的数据中添加了一个新文件名,该文件名对应的ionde和myfile相同。
硬连接数 硬连接数的数量是,有多少个文件对应的inode编号相同。 myfile文件和myfile-s文件的inode编号相同,所以硬连接数位2。
想要释放这个文件对应磁盘空间释放,要把硬连接数变成0。删除一个相同inode编号的文件,该硬连接数-1。
也就是说:
软链接
ln -s 文件名 要创建的文件名
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。
文件的三个时间
用stat 文件名可以查看
- Access 最后访问时间
Modify 文件内容最后修改时间 Change 属性最后修改时间
|