在系统角度理解文件:文件=内容+属性,内容和属性都是数据,对于文件的所有操作无外乎对于文件内容操作和对于文件属性操作。
文件在磁盘存放,磁盘是硬件,只有操作系统才有权利访问硬件。用户访问文件先写代码再编译形成可执行程序,该程序运行起来才能访问文件,所以访问文件本质上是进程在访问文件。进程访问文件需要操作系统提供的文件类的系统调用接口来访问。
系统调用接口比较难,语言上对这些接口进行了封装,为了让接口可以更好的使用,导致了不同的语言有不同的文件访问接口,但是封装的是同一个操作系统接口,而操作系统接口只有一套,这也是学习它的价值。其次,言要实现跨平台性,就需要对操作系统的接口进行封装,如果语言不提供对文件的接口,那么访问文件的操作就必须直接使用操作系统的接口,这就导致了跨平台不兼容的问题,C/C++使用条件编译+动态裁剪的方法,实现跨平台。
显示器也是硬件,向显示器写入和向文件写入没有任何区别。只不过显示器更加直观,因为Linux下一切皆文件。对于普通文件而言,有读有写,对于显示器有写入,而对于键盘有读取。站在系统的角度,能够被读取或被写入的设备就是文件,狭义上的文件是普通的磁盘文件,广义上的文件也包含了几乎所有的外设。
C语言提供了众多操作文件的函数,例如fopen、fclose、fseek、fprintf、fscanf、fgetc、fputc、fwrite、fread等函数。在操作文件时,经常会遇到一个名词叫做当前路径,当前路径是每当一个进程运行起来的时候,每个进程都会记录自己当前的所处的工作路径。另外要注意,当向文件写入字符串的时候,不需要读取\0,因为它是C语言的规定,文件不需要遵守,只保存有效数据。
C默认会打开三个输入输出流,分别是stdin, stdout, stderr;仔细观察发现,这三个流的类型都是FILE*,并且fopen返回值类型是文件指针。
操作文件,除了上述C等语言接口,还可以采用系统接口来进行文件访问。并且C语言提供的库函数(libc)必须实现系统调用接口的封装。fopen、fclose、fwrite、fread函数分别调用系统接口open、close、write、read。
如上图所示,系统调用接口和库函数的关系,一目了然。所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
open接口介绍
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
mode: 创建文件时,设置文件的权限,但是要考虑umask文件掩码的影响。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
其中,参数是使用位图和宏定义标志位的方式传递给open接口。例如:int fd = open("log.tx",O_WRONLY|O_CREAT );
位图+宏定义标志位案例
#include
#define ONE 0x01 //0000 0001
#define TWO 0x02 //0000 0010
#define THREE 0x04 //0000 0100
void show(int flags)
{
if(flags & ONE) printf("ONE\n");
if(flags & TWO) printf("TWO\n");
if(flags & THREE) printf("THREE\n");
}
int main()
{
show(ONE);
show(ONE|TWO);
show(ONE|TWO|THREE);
}
当打开一个文件时会惊讶的发现,open的返回值是3,012去哪里了呢?
#include
#include
#include
#include
int main()
{
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
printf("open success, fd: %d\n", fd);
return 0;
}
上面代码输出
open success, fd: 3
这是因为Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2,0,1,2对应的物理设备一般是:键盘,显示器,显示器。
验证:标准输入0, 标准输出1, 标准错误2
#include
#include
#include
#include
#include
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,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
在内核中,OS都要为管理每一个被打开的文件创建file结构体,其中包含了一个被打开文件的所有信息,只要找到了file对象,就找到了一个文件。file结构体再通过某种数据结构组织起来实现文件管理。
fwrite()是怎么操作文件的
用户使用C语言提供的fwrite()函数时,一定会传入FILE* 指针,FILE* 中一定包含了fd,并且fwrite()封装了write系统接口,就可以执行write(fd,…)进入了操作系统内部,进程会执行操作系统内部的write方法,并且进程的tast_struct结构体中又包含了文件描述符表*files
,*files
指向了files_strut结构体,files_strut结构体中包含了file* fd_array[]指针数组,在配合上刚刚传入的fd,通过fd_array[fd]就找了一个内存文件的所有信息struct file,继而就可以进行对于文件的操作。
fd的分配规则
fd的分配规则是:在files_struct数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符。
重定向
#include
#include
#include
#include
#include
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);
}
如果关闭1号文件描述符,执行上一段代码,可发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,因为,myfile文件的fd是1。这就是输出重定向。常见的重定向有: >, >>, <
重定向的本质其实就是在操作系统内部更改fd对应的内容指向!
重定向的系统调用dup2
dup2的函数原型
#include
int dup2(int oldfd, int newfd);
将oldfd拷贝给newfd,最后newfd的指针指向被关闭,newfd指针指向oldfd的指针指向。当进行输出重定向的时候,使用dup2(3,1),让1不指向显示器而指向3对应的文件log.txt,最终3和1号文件描述符的指向同样的内容,导致本来应该向显示器打印的内容变为向log.txt文件进行写入。
#include
#include
#include
int main()
{
int fd = open("log.txt", O_CREAT | O_RDWR | O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd,1);
fprintf(stdout,"success\n");
return 0;
}
Linux下一切皆文件
Linux下一切皆文件是Linux的设计哲学,体现在操作系统的软件设计层。站在驱动开发的角度,底层不同的硬件(磁盘、显示器、键盘、网卡、显卡),一定对应不同的操作方法(包含I/O),所有的设备都有自己专属的read、write等接口,且代码的实现一定是不同的。Linux是用C语言写的,在描述一个文件的时候使用file结构体来描述,file里面包含了read、write等函数的函数,每个函数指向对应的硬件专属的read、write方法。这样在操作系统上层来看就没有任何硬件的差别,看待所有文件的方式,都统一变为了struct file。而在Linux下一切皆可以被描述为文件,此文件系统属于virtual file system,简称为VFS。
缓冲区
缓冲区的概念:缓冲区就是一段内存空间。
缓冲区的价值:当你上大学的时候,忘记带学生证,你让家人给你送来。这时候有两种选择,一个是直接给你送过来,在计算机的角度也就是将数据直接送达,称为写透模式,或者是让顺丰送来,在计算机的角度也就是将数据间接送达,称为写回模式,此模式快速且成本低,提高用户的响应速度,提高整机效率!
缓冲区的刷新策略:缓冲策略分为一般策略和特殊策略。一般策略包含立即刷新,行刷新和满刷新;特殊情况为用户强制刷新(fflush)和进程退出。一般而言行缓冲的设备文件是显示器,全缓冲的设备文件是磁盘文件。所有的设备都永远倾向于全缓冲,其他刷新策略是结合具体情况做的妥协(当然用户也可以自定义规则),因为缓冲区满了才刷新需要更少此的IO操作,也就是更少次的外设访问,可以提高效率。因为和外设IO时,数据量的大小不是主要矛盾,和外设的准备IO才是最耗费时间的。
缓冲区的存在位置?
#include
#include
#include
#include
#include
#include
int main()
{
// C语言提供的
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fputs\n";
fputs(s, stdout);
// OS提供的
const char *ss = "hello write\n";
write(1, ss, strlen(ss));
fork(); //创建子进程
return 0;
}
当直接执行上述代码时
hello printf
hello fprintf
hello fputs
hello write
当重定向到文件中,./myfile > log.txt
再打印文件内容cat log.txt
hello write
hello printf
hello fprintf
hello fputs
hello printf
hello fprintf
hello fputs
同样一个程序向显示器打印输出四行文本,而向磁盘中的普通文件打印时,变成了七行,其中C语言的库函数打印了2次,而系统调用接口只打印了1次。可以证明所谓的缓冲区绝对不是OS提供的,否则上面的代码结果应该相同,所以缓冲区是由C标准库维护的。
如果向显示器中打印,刷新策略为行刷新,最后执行fork的时候,一定是函数执行完了并且数据已经被刷新了,此时fork没有意义了。而进行向磁盘文件打印的时候,刷新策略变为了全缓冲,此时代码中的’\n’也就没有了意义。fork的时候,代码已经执行完了,但是对应的数据还没有刷新,存在于当前进程的用户空间的C标准库提供的缓冲区中,这部分的数据是父进程的数据,当fork的时候,会发生写时拷贝,此时子进程会拷贝父进程存在于缓冲区的数据存放到自己进程的缓冲区中。而return 0时,进程退出发生强制刷新,所以C语言库函数的打印函数打印了两次。而操作系统接口的库函数会直接直接传给内核空间(内核也有缓冲区),该数据属于内核数据了,子进程无法写时拷贝。
C语言打开文件,FILE*指针指向的FILE结构体内部不仅仅封装了fd,还封装了语言层的缓冲区结构。
模拟C语言实现缓冲区
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
struct MyFILE_{
int fd;
char buffer[1024];
int end; //当前缓冲区的结尾
};
typedef struct MyFILE_ MyFILE;
MyFILE *fopen_(const char *pathname, const char *mode)
{
assert(pathname);
assert(mode);
MyFILE *fp = NULL;
if(strcmp(mode, "r") == 0)
{
}
else if(strcmp(mode, "r+") == 0)
{
}
else if(strcmp(mode, "w") == 0)
{
int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);
if(fd >= 0)
{
fp = (MyFILE*)malloc(sizeof(MyFILE));
memset(fp, 0, sizeof(MyFILE));
fp->fd = fd;
}
}
else if(strcmp(mode, "w+") == 0)
{
}
else if(strcmp(mode, "a") == 0)
{
}
else if(strcmp(mode, "a+") == 0)
{
}
else{
//什么都不做
}
return fp;
}
//C标准库中的实现!
void fputs_(const char *message, MyFILE *fp)
{
assert(message);
assert(fp);
strcpy(fp->buffer+fp->end, message); //abcde\0
fp->end += strlen(message);
printf("%s\n", fp->buffer);
//暂时没有刷新, 刷新策略是用户通过执行C标准库中的代码逻辑,来完成刷新动作
//这里效率提高,体现因为C提供了缓冲区,那么我们就通过策略,减少了IO的执行次数(不是数据量)
if(fp->fd == 0)
{
//标准输入
}
else if(fp->fd == 1)
{
//标准输出
if(fp->buffer[fp->end-1] =='\n' )
{
//fprintf(stderr, "fflush: %s", fp->buffer); //2
write(fp->fd, fp->buffer, fp->end);
fp->end = 0;
}
}
else if(fp->fd == 2)
{
//标准错误
}
else
{
//其他文件
}
}
void fflush_(MyFILE *fp)
{
assert(fp);
if(fp->end != 0)
{
//暂且认为刷新了--其实是把数据写到了内核
write(fp->fd, fp->buffer, fp->end);
syncfs(fp->fd); //将数据写入到磁盘
fp->end = 0;
}
}
void fclose_(MyFILE *fp)
{
assert(fp);
fflush_(fp);
close(fp->fd);
free(fp);
}
int main()
{
MyFILE *fp = fopen_("./log.txt", "w");
if(fp == NULL)
{
printf("open file error");
return 1;
}
fputs_("one: hello world", fp);
fclose_(fp);
}
缓冲区验证
#include
#include
#include
#include
#include
#include
int main()
{
close(1);
int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 0;
}
printf("hello world:%d\n",fd); //stdout是一号描述符,数据暂存到stdout的缓冲区中
//fflush(stdout); //刷新缓冲区
close(fd); //数据再缓冲区中,一旦把fd关闭了,数据就无法刷新!
return 0;
}
通过实验可以看出log.txt中并没有任何的数据,除非刷新缓冲区。
stdout和stderr的区别
stdout和stderr分别对应fd为1和2,stdout和stderr对应的都是显示器文件,但是它们两个是不同的,可以认为是同一个显示器文件被打开了两次。当做重定向输入的时候,只会重定向1号文件描述符。一般而言,如果程序运行有问题的画, 建议使用stderr或者cerr来打印。如果是常规文本,则使用cout或stdout打印。
背景知识
内存是掉电易失存储型介质,磁盘是永久性存储介质(SSD,U盘,flash卡,光盘,磁带也是永久性存储介质)。磁盘中存在着大量未被打开的磁盘级文件,那么如何进行对于磁盘文件进行分门别类的存储,用来支持更好的存取呢?
磁盘的物理结构:磁盘是一个外设并且还是计算机中唯一的机械设备,所以磁盘比较慢。磁盘结构有磁盘盘片、磁头、伺服系统、音圈马达等。盘面上会存储二进制数据,由南北极来表示,所谓的向磁盘写入就是磁头放电改变磁盘上的南北极/正负性。
磁盘的存储结构:扇区是磁盘存储的基本单位,再物理上把数据写入到磁盘就是要找到对应的扇区(512字节)。首先要找到在哪一个面,在哪一个磁道/柱面上,最后确定在哪一个扇区,这种寻址方式成为CHS寻址,如果有了CHS就可以锁定扇区,那么所有的扇区就都能找到了。
磁盘的抽象结构:磁盘的盘片可以想象为一个线性结构。从而可以把一个磁盘想象为一个数组,想要访问每一个扇区的时候只要知道数组的下标再转化为内CHS即可。将数据存储到磁盘就是将数据存储到数组中,从而对于磁盘的管理也就变为了对于数组的管理。
在分区后,对于磁盘的管理就是对一个小分区的管理,每一个分区又分为多个块组,对于每个块组实管理也就实现了对于磁盘的管理。
将块组分割成为上面的内容,并且写入相关的管理数据,每一个快组都这么干,整个分区就被写入了文件系统信息 ! 这也就是格式化的过程。
一个inode对应一个inode属性节点,一个文件可以由多个block,当有了inode编号就能找到inode属性节点,也就是inode结构体,其中包含一个数组blocks,里面的内容就是该文件使用到了哪些data block块(如果文件太大,data block会存储其他块的块号,形成一种多叉树)。这时,这时就有了inode 所保存的文件属性和block保存的文件内容,从而对于文件进行操作。
inode和文件名
找到文件首先要找到inode编号进而找到inode节点空间从而拿到了文件的属性和内容。Linux中,inode属性里面没有文件名这样的说法。在同一个目录下,不会存在文件名相同的情况,且目录也是文件,目录也有自己的inode和datablock,目录的datablock存放目录中的文件名和inode编号的映射关系。
软硬链接
软硬链接的区别在于有没有独立的inode,软链接有独立的inode,也就是说软连接是一个独立的文件,硬链接没有独立的inode,所以其不是一个独立的文件。软链接就如同window下的快捷方式,可以理解为软链接的文件内容是指向文件对应的路径。创建硬链接就是在指定目录下,建立了文件名和指定inode的映射关系,在inode属性中存在硬链接数,使用引用计数的方法记录关联的文件数。默认创建一个目录时,硬链接引用计数是2,是因为自己目录名和自己目录内部的.
生成静态库
makefile生成静态库案例:
libhello.a : mymath.o myprint.o
ar -rc libhello.a mymath.o myprint.o
mymath.o : mymath.c
gcc -c mymath.c -o mymath.h
myprint.o : myprint.c
gcc -c myprint.c -o myprint.o
ar是gnu归档工具,rc表示replace and create
使用静态库
gcc main.c -L. -lmymath
-L 指定库路径
-l 指定库名
库搜索路径为
生成动态库
makefile生成动态库案例:
libhello.so : mymath.o myprint.o
gcc -shared mymath.o myprint.o -o libhello.so
mymath.o : mymath.c
gcc -c -fPIC mymath.c -o mymath.o
myprint.o : myprint.c
gcc -c -fPIC myprint.c -o myprint.o
shared表示生成共享库格式,fPIC表示产生位置无关码
使用动态库
gcc main.o -o main –L. -lhello
l : 链接动态库,只要库名即可
L:链接库所在的路径