前面我们知道,创建一个空文件时,这个空文件也会占用磁盘的空间,因为文件除了文件的内容,还有文件的属性。文件=内容+属性。 我们对文件的操作如:存取,都是围绕着文件的内容+属性展开的
linux下一切皆文件
对于0KB的空⽂件是占⽤磁盘空间的
⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件=属性(元数据)+内容)
所有的⽂件操作本质是⽂件内容操作和⽂件属性操作
访问文件需要先打开文件。如在c/c++中,调用fopen来访问文件,只有当fopen函数被执行了,这个文件才算被打开
在c/c++的库函数中如:fopen、fclose...都封装了底层os的文件系统调用
操作系统要把文件管理起来,与管理进程的方法一样,先描述再组织。在操作系列内部将打开的文件描述成一个个struct对象,再用链表将对象组织起来。在对象中一定包含文件的属性和文件的内容。访问文件实际上是进程访问文件,又由于文件和进程都会被操作系统描述成一个struct对象,所以说到底就是两个对象之间关系。
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
平时我们写好的c程序,在编译时是被动过手脚的。它会默认打开三个输入输出流
打开文件后,会先清空文件的内容,然后从文件头开始写入;
之前我们使用"echo aaa>log.txt",输出重定向往log.txt里写入内容,每执行一次,文件之前的内容就会被清空,然后写入新内容。这是因为">"底层就是把文件打开了一下。如果这段指令是被shell解释,shell是由c语言写的,那么">"底层就是以"w"的形式打开文件
以追加的方式向文件中写入内容。
代码演示:
我们平时使用的"echo "aaaaaa">>log.txt"。”>>“叫做追加重定向,它本质其实就是以"a"的方式打开文件并向文件写入内容
r+、w+、a+都是以读写的方式写入文件,但是当我们对文件进行读写时,会发现文件读不上来,这是因为我们在读写文件时,需要先把光标复位或者移动要读写的位置。
在将字符串写入文件时,不用为'\0'留位置,也就是计算字符串大小时不用加1,因为'\0'是c语言的规定,不是文件的规定。如果将'\0'写入文件,文件中会显示乱码,因为'\0'是一个不可显字符
文件是在磁盘上的,对文件的访问实际上是对磁盘的访问,是对硬件的访问,而能访问硬件的只有操作系统。在语言层面对文件的访问都是因为函数在底层封装了相应的系统调用。
真正访问文件需要系统调用,比如打开文件:open系统调用
int open(const char *pathname, int flags, mode_t mode);
/*
mode:权限位,新建文件的权限
pathname:要打开的文件的路径和名字,可以只写名字,不写路径。
flags:指定文件的打开/创建模式
O_RDONLY, 只读
O_WRONLY, 只写
O_RDWR 读写
O_CREAT 如果指定文件不存在,则创建这个文件
O_APPEND 以追加方式写入文件
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容(即将其长度截短为0)
像flags这种传参方式叫做:位图传参。当一个函数需要传入多个标记位时,以位图的形式传入,将多个标记位进行位操作,然后传给函数
*/
如果打开成功,则会返回一个新的文件描述符,失败则返回-1
#include<stdio.h>
#define ONE_FLAG (1<<0)
#define TWO_FLAG (1<<1)
#define THREE_FLAG (1<<2)
#define FOUR_FLAG (1<<3)
void Print(int flags)
{
if(flags & ONE_FLAG)
{
printf("ONE!\n");
}
if(flags & TWO_FLAG)
{
printf("TWO!\n");
}
if(flags & THREE_FLAG)
{
printf("THREE!\n");
}
if(flags & FOUR_FLAG)
{
printf("FOUR!\n");
}
}
int main()
{
Print(ONE_FLAG | TWO_FLAG);
printf("\n");
Print(ONE_FLAG | THREE_FLAG | TWO_FLAG);
printf("\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);
return 0;
}
文件创建的最终权限与umask有关
运行程序会发现,创建的文件的最终权限与我们传入的权限有出入,这是因为存在umask
我们可以通过如下方式,将我们创建的文件权限修改成我们想要的权限。
操作系统赋予文件权限时,是根据就近原则,简单说就是:我们在程序里设置了umask,那么操作系统就会使用这个umask来给文件赋予权限
函数的返回值为写入的字节数(ssize_t 类型)
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
umask(0);
int fd=open("log.txt",O_CREAT | O_WRONLY ,0666 );
if(fd<0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd);
const char*msg="hello world\n";
int cnt=5;
while(cnt)
{
write(fd,msg,strlen(msg));//像文件中写入文件
cnt--;
}
close(fd);
return 0;
}
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
umask(0);
//O_TRUNC,打开文件后将文件的内容清空
int fd=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666 );
if(fd<0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd);
const char*msg="hello world\n";
int cnt=5;
while(cnt)
{
write(fd,msg,strlen(msg));
cnt--;
}
close(fd);
return 0;
}
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
umask(0);
// int fd=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666 );
int fd=open("log.txt",O_CREAT | O_WRONLY | O_APPEND ,0666 );
if(fd<0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd);
const char*msg="hello world\n";
int cnt=5;
while(cnt)
{
write(fd,msg,strlen(msg));
cnt--;
}
close(fd);
return 0;
}
在语言层面调用fopen打开文件,我们选择"w"与"a"方式,底层它会分别转换成下图这样,文件是在磁盘上的,磁盘属于硬件,只有操作系统才能访问硬件,所以fopen底层封装了open系统调用
对于操作系统来说,对文件的写入都是以二进制的方式写入的,而我们说的“文本写入”和“二进制写入”都是在语言层面的
int fd=open("log.txt", O_RDWR );
if(fd<0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd);
while(1)
{
char buffer[64];
int n=read(fd,buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]=0;
printf("%s",buffer);
}
else if(n==0)
{
break;
}
}
close(fd);
当我们打开多个文件,并观察返回的文件描述符,发现:文件描述符是从3开始按顺序赋予的。
那么文件描述符0、1、2去哪了呢?
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
umask(0);
int fd1=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666 );
int fd2=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666 );
int fd3=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666 );
int fd4=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666 );
if(fd1<0)exit(1);
if(fd2<0)exit(1);
if(fd3<0)exit(1);
if(fd4<0)exit(1);
printf("fd: %d\n",fd1);
printf("fd: %d\n",fd2);
printf("fd: %d\n",fd3);
printf("fd: %d\n",fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
文件描述符0、1、2是标准输入流、标准输出流、标准错误流
我们之前在c语言学习的FILE类型,它本质是一个结构体,被typedef成了FILE。
而操作系统管理文件是要通过文件描述符来进行管理的,所以c语言能够访问文件说明在FILE结构体中一定封装了fd文件描述符。
如果向观察标准输入流、标准输出流、标准错误流的文件描述符,可以像下图所示:
printf("stdin: %d\n",stdin->_fileno);
printf("stdout: %d\n",stdout->_fileno);
printf("stderr: %d\n",stderr->_fileno);
不管什么语言,只要是对文件的操作,底层一定是封装了文件描述符,一定是封装了系统调用的。
为什么各个语言都要做系统级别的封装?:
在操作系统中,肯定会打开许多的文件,操作系统是如何对这些文件进行管理的?
在对文件进行修改时,要先将文件内容加载到文件缓冲区,等改完再写回磁盘。
文件描述符的分配原则:最小的,没有被使用的,作为新的fd给用户,也就是就近原则。
当关闭标准输出流时,它会将标准输出流的fd分配给我们打开的文件,所以,在输出fd时,会将输出的内容输出到log.txt中。
当我们关闭标准输出流后,操作系统将fd分配给我们新打开的文件的这一行为叫做:重定向
原理:当我们将标准输出关闭后,fd:1被分配给了我们新打开的文件,而操作系统在输出时,只认文件描述符“1”,所以就把输出内容打印到了我们刚打开的文件中去,这就是重定向
int fd=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC , 0666);
if(fd<0)exit(1);
dup2(fd,1);
//close(fd);
printf("fd %d\n",fd);
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
fprintf(stdout,"hello world\n");
fprintf(stdout,"hello world\n");
fprintf(stdout,"hello world\n");
const char*msg="hello liu\n";
write(fd,msg,strlen(msg));
如果不关闭fd,就会有两个文件描述符代表log.txt,当进行输出时,值会输出到标准输出流(“1”)中,现在而言,“1”代表着log.txt,所以值会输出到log.txt中。
当进行write时,就是对普通文件进行写入,向fd里写入,所以值也会写入到log.txt中
同时,因为缓冲区的存在,所以系统调用write的值会先刷新到log.txt中
当想以追加的形式向文件中写入时,打开方式则换成"O_APPEND"。
重定向:打开文件的方式+dup2
一个文件可以被多个进程打开,文件什么时候关闭取决于打开文件的进程,当没有进程访问文件时,文件就会关闭,那操作系统怎么知道有多少进程在访问文件?引用计数 在struct file中有一个refcnt的变量,用来记录有多少进程在访问文件,当一个进程结束访问文件,引用计数就减1,直到0,关闭文件。这类似于智能指针sharedptr
标准输出与标准错误都是向显示器文件里写入,那为什么要特地设立一个标准错误呢?
这样可以通过重定向,将常规信息与错误信息分离开,方便我们日志的形成。
//对它进行重定向输出时
#include<iostream>
#include <cstdio>
int main()
{
std::cout<<"hello world"<<std::endl;
printf("hello world\n");
std::cerr<<"stderr"<<std::endl;
fprintf(stderr,"hello stderr\n");
return 0;
}
发现输出到标准输出流的内容被正常输出到log.txt中,而输出到标准错误流的内容依然是输出到屏幕上。
这是因为输出重定向原型是“./a.out 1 > log.txt”,它是将文件描述符1重定向到log.txt,而标准错误流的文件描述符是“2”,所以输出到标准错误流的内容依然输出到屏幕上。
如果想把标准错误与标准输出一并进行重定向,可以像下图所示:
像显示器,键盘,磁盘这些都属于硬件,在硬件层,在硬件层上面有一层驱动层,驱动层中有关于硬件的读写方法,在驱动层上面就是OS,OS中的进程访问文件,会创建一个文件描述符页表,通过页表可以找到对应的文件即struct file,在struct file中封装了对硬件的读写的函数指针,这个函数指针在每个struct file中都是一样的。函数指针指向驱动层的读写函数,这样就屏蔽了各个硬件之间的差异,并且达到了将硬件抽象成文件的目的。此时对于OS来说,对于硬件的管理就相当于对文件的管理。
这就是linux下一切皆文件。
将各个struct file组合起来,就是VFS:虚拟文件系统
struct file中封装的对硬件读写的函数指针,被封装在fileoperation中,让fop指向硬件方法
缓冲区就相当于现实生活中的菜鸟驿站,快递员将快递放到菜鸟驿站中。当快递到时,我们不需 要立马去取,可以等到自己有空时再去。缓冲区就起到这么一个作用。操作系统就相当于快递员。操作系统将数据读取放到缓冲区等待调用。
缓冲区就是内存中的一段空间
下图中提到的用户级缓冲区,是由c标准库提供的
上图中的代码,之所以能将内容成功输出重定向到log.txt中,是因为系统调用会刷新缓冲区,所以能够成功输出到log.txt
c语言层的缓冲区被维护在FILE中,FILE是c语言层提供的一个struct,在FILE中除了缓冲区还维护了fd
在计算机中,数据流动的本质:一切皆拷贝
重定向还会更改文件的刷新方式,如果是向显示器打印,有\n的话就是行刷新,如果进行输出重定向到log.txt,那么就会被更改成全缓冲。
如下图代码,进行输出重定向到Log.txt中,会输出两次:
因为最后调用了fork,创建了一个子进程,当进程结束时会进行刷新,由于存在父子两个进程,所以会刷新两次,所以将内容输出重定向到Log.txt中,会输出两次。
而系统调用不会输出两次,这是因为系统调用会自己刷新缓冲区,不会将内容输出到用户级别的缓冲区