关于文件的一些概念
- 文件包含文件内容和文件属性
- 对文件的操作包括对文件内容的操作和对文件属性的操作,也有可能同时进行对文件内容和文件属性的操作
- 打开文件这个动作是指的将文件的属性和内容加载到内存中
- 磁盘上并不是所有的文件都会被加载到内存中
- 对文件的理解可以分为内存文件和磁盘文件
- 通常当我们在对文件进行操作(打开文件,访问文件....)时,实质上是程序加载到内存中变成的进程对文件进行操作
- 这里讨论的就是进程和“打开文件”的关系
C语言中的文件操作函数实际上是对系统接口的封装,在向文件写入数据时是对磁盘内进行写入,只有操作系统有资格向硬件磁盘写入。上层(用户)如何调用操作系统呢?要通过相应的系统接口!平时我们没有用到系统接口是因为C语言的函数都经过了封装。
为什么要进行封装?因为如果在程序中直接进行系统调用(linux环境),将代码放入到windows环境下就不会兼容。不具有跨平台特性。
为了更好地了解底层原理,所以我们有必要学一学文件级别的系统接口
1.系统接口
open
返回值小于0,打开失败。
flags代表打开方式,就像C语言中的r,w等等。不过这里要特殊一些。
O_RDONLY(只读), O_WRONLY(只写), O_RDWR(读写),O_APPEND(读写),O_CREAT(没有文件创建文件),O_TRUNC(如果原来文件中有内容,将内容全部清空)等,他们实际上都是宏,是系统传递表计位,是用位图结构进行传递的,每一个宏标记,一般只需要一个比特位是1,并且和其他的宏对应的标记不能相同如下例。所以他们也可以是或的形式。
mode代表文件初始权限。如果被打开的文件是第一次出现,那么要给上初始权限。否则文件的显示权限部分会出现乱码(不等于最终权限)
#include<stdio.h>
#define O_A 0b0001
#define O_B 0b0010
#define O_C 0b0100
#define O_D 0b1000
int main()
{
int a = 1;
if(a==O_A)
printf("O_A");
else if(a==O_B)
printf("O_B");
else if(a==O_C)
printf("O_C");
else
printf("O_D");
}
int fd = open("text.txt",O_RDONLY)//只读
int fd = open("text.txt",O_WRONLY)//只写
int fd = open("text.txt",O_RDWR)//读写
int fd = open("text.txt",O_RDONLY|O_CREAT)//只读,如果没有文件创建文件
int fd = open("text.txt",O_RDONLY|O_WRONLY|O_CREAT)//读写,如果没有文件创建文件
int fd = open("text.txt",O_RDONLY|O_CREAT,0777);//初始权限给的是0777
write
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
const char* str = "hello world\n";
write(fd,str,strlen(str));
printf("%d\n",fd);
close(fd);
return 0;
}
//这里可以把字符串中的'\0'作为结束标志写入到文件中吗?答案是否定的,因为只有在C语言中
//'\0'是字符串结束的标志,在其他的打开方式下,'\0'可能会被识别成其他的字符!
read
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
char str[128];
ssize_t s= read(fd,str,127);
if(s>0)
{
str[s] = '\0';
printf("%s",str);
}
close(fd);
return 0;
}
2.文件描述符
open函数的返回值fd就是文件描述符。每次打开函数后,fd都是从3开始的,那么0,1,2呢?这里可以直接说明:0是标准输入(键盘),1是标准输出(显示器),2是标准错误(显示器),每次都是默认打开的。不禁联想到C语言中的FILE* stdin,FILE* stdout,FILE* stderr。
验证:
linux下一切皆文件
被打开的文件在内存中,进程也在内存中。所以系统在运行过程中,会存在大量被打开的文件。操作系统要对这些文件进行管理。一个进程可以打开多个文件,那么操作系统如何让进程和其打开的文件映射起来呢?
不要有疑问,0,1,2->stdin,stdout,stderr,键盘,显示屏,虽然都是硬件,但是在linux中也可以作为文件管理。是怎样作为文件使用的呢?
文件描述符的分配规则
遍历files_struct的数组,找到一个最小的且未被使用的下标,分配给新的文件。
重定向
引入
先看一个有趣的代码:
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{
close(1);
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 0;
}
fprintf(stdout,"这是一个进程");
fflush(stdout);
close(fd);
}
最后它并不会在显示器上打印“这是一个进程”,而是将其写入到log.txt文件中。 那么为什么会出现这种现象?那是因为发生了重定向,本来应该是向某个文件输入,但是输入到另一个文件。
dup函数
上图中的所有数据都是内核数据结构,只有(系统调用)操作系统才有权限提供接口修改内核数据结构。最常用的就是dup函数。
比较常用的就是dup2函数,oldfd为原来文件的描述符,newfd为重定向输出的文件描述符。
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 0;
}
//和上面的代码是等效的
dup2(fd,1);
fprintf(stdout,"这是一个进程");
fflush(stdout);
close(fd);
}
理解缓冲区
缓冲区在哪?缓冲区并不在系统提供的接口中,缓冲区是由C语言提供的!被封装到了FILE中。
运行后会发现先打印的是hello write,过了5秒才打印了hello printf,因为hello printf首先被加载到了缓冲区,等到进程结束后才打印到了显示器上,而hello write则是直接被打印了出来,说明系统提供的接口write是没有缓冲区的。缓冲区的存在可以集中处理数据刷新,减少IO的次数,可以提高整机的效率。
在缓冲区刷新之前,关闭文件描述符会怎样?都关闭了门口了,肯定是打印不出来了呀!
什么时候刷新?
常规:无缓冲(立即刷新);行缓冲(逐行刷新)显示器文件;全刷新(缓冲区满,刷新)块设备对应的文件,磁盘文件。
特殊:进程退出;用户强制刷新(fflush(*****))
缓冲区在FILE内部,在C语言中,我们每一次打开一个文件,都要有一个FILE*返回,每个文件都有一个fd和属于他自己的语言级别缓冲区!
为什么会出现这种情况呢?要知道刷新的本质是把缓冲区的数据write到OS内部,清空缓冲区!缓冲区是由自己的FILE内部维护的,是属于父进程内部的数据区域!前面的write是OS直接写到stdout的,所以直接打印。而fprint等是语言级别的接口,他们要先将数据写入到属于自己FILE的缓冲区内等待刷新,而最后的fork()产生了子进程,这时他们的数据代码都是共享的,当父进程结束时,要刷新缓冲区,这时父进程缓冲区的数据要被清除,所以子进程发生写时拷贝要将原来共享的数据(缓冲区中的数据)拷贝到新的区域中,当父进程结束后它的缓冲区内容被刷新到显示器上,随后子进程结束也被刷新到显示器上。
标准错误和标准输出的区别
标准错误和标准输出都是向显示器打印,有什么区别呢?标准错误和标准输出是通过不同的文件描述符打印到显示器上的!
在发生重定向时,没有说明默认就是重定向标准输出,如果要重定向标准错误则要加上2:
意义在哪里?可以区分程序中的日常信息和错误信息!方便查找错误。cout和cerr,printf和perror要分开使用。
如果不想分开这两个内容,想重定向到一个文件中:
先将1重定向到all.txt中,再把1的地址给2,那么2也指向1所指的文件,2也完成了重定向。