- 重新谈论文件
- 空文件,也要在磁盘占用空间
- 文件=文件内容+文件属性
- 文件操作=文件内容操作,文件属性操作或内容和属性操作。
- 标定一个文件必须使用文件路径+文件名,因为它具有唯一性。
- 没有指明对应的文件路径,一般默认在当前路径(默认是进程当前的路径)进行文件访问。
- 当我们把fopen,fclose,fread,fwrite,等接口写完后,代码编译后,形成二进制可执行程序之后,但是没运行,文件对应的操作没有被执行。
对文件的操作,本质是进程对文件操作。
一个文件没有被打开,不能直接对文件进行访问。一个文件要被访问,必须先被打开。
打开文件需要用户进程+操作系统,用户进程用来调用接口,操作系统实现文件的打开。
一个文件要被访问,必须先被打开。不是所有文件都被系统打开。
进程的操作本质是研究进程和被打开的文件之间的关系。
- 重新谈论文件操作
文件在磁盘里,磁盘是硬件,需要操作系统访问。想访问磁盘,不能绕过操作系统,必须使用操作系统提供的接口,提供文件级别的操作系统调用接口。
但操作系统只有一个,所以上层语言无论如何变化,库函数底层必须调用系统调用接口,库函数可以千变万化,但是底层不变,那么如何降低学习成本呢?
a+是追加写入加读取。
以w的方式单纯的打开文件,c会自动清空内部数据。
读写是把文件内容清空后写入,追加是直接在内容后面写入。
把特定的内容格式化写入到特定的格式流中。
试着写一下
#include<stdio.h>
#include<unistd.h>
#define FILE_NAME "log.txt"
int main()
{
FILE* f=fopen(FILE_NAME,"w");
if(f==NULL)
{
perror("fopen");
return 1;
}
int cnt=5;
while(cnt)
{
fprintf(f,"%s:%d\n","hello wprld:",cnt--);
}
}
以行为单位,从stream流中读取数据,放入到s中。s中大小为size。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define FILE_NAME "log.txt"
int main()
{
FILE* f=fopen(FILE_NAME,"r");
if(f==NULL)
{
perror("fopen");
return 1;
}
//bin int cnt=5;
// while(cnt)
// {
//bin fprintf(f,"%s:%d\n","hello wprld:",cnt--);
// }
//
char buffer[64];
while(fgets(buffer,sizeof(buffer)-1,f)!=NULL)
{
buffer[strlen(buffer)-1]=0;//puts打印时自动加上了/n,原先文本中还有个/n,这里就去掉原先文本中的/n
puts(buffer);
}
fclose(f);
}
- 系统调用接口
用fopen打开文件,底层调用的是系统调用接口open
mode代表创建文件时的权限,pathname代表文件路径,open打开文件成功后,返回文件描述符。
是个整数。失败返回-1,并会说明什么原因失败。
flags有很多选项,例如如下三个。
具体是什么下面讲解。
大写的让我们想到了宏,这些就是宏。
我们C语言中传标记位,一般一个整数传一个标记位,但是这样有10个标记位,就要有10个整数,
所以我们选择通过32个比特位来传递选项。
- 操作系统如何通过比特位传递选项。
一个比特位,一个选项,比特位位置不能重复。
不能有3,因为只能一个位置为1.
每一个宏对应的数值,只有一个比特位位置是1,彼此位置不重叠。
也可以这样写
main函数中这样写,然后就能打印对应的数值了。
这叫标记位传参。
这些就是通过不同的标记位传参的。不同标记位表示不同功能。
上面这样运行,如果之前这个文件没有被创建,会报文件不存在,因为此时文件没有被创建 ,只写标记位不能像fopen一样自动创建文件。
我们需要加上创建标记位。
但此时文件权限会是乱码。之前使用的fopen是语言中内部写的接口,默认权限已经写好。
我们调用系统接口,必须先写文件创建的默认权限。
这样权限就对了,此时是664,因为有个掩码umask为2.
我们也可以更改umask掩码
但此时在命令行查看uamsk依然会是0002,因为umask是在子进程执行,改的是子进程的文件权限,改的时候不影响shell中的umask。
打开之后要写入,下面是写入函数。
fd参数是想写入文件的文件描述符。
buf是指从哪里输入。
count是指写入多少字节。
返回值一般和cout相同,代表传了几个值。
读写文件有两种读写方案
1文本类
2二进制类
语言本身为我们提供的文件读取的分类。但在操作系统看来,都是二进制。
操作系统只看传过来的字节是几,至于是图片还是文字是由C语言等各种软件决定的。
这些接口将特定内容格式化到字符指针中 。
一下代码演示了使用系统调用接口的例子。
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<assert.h>
#include<string.h>
#define FILE_NAME "log.txt"
int main()
{
umask(0);
int fd=open(FILE_NAME,O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
int cnt=5;
char letter[64];
while(cnt)
{
sprintf(letter,"%s|%d\n","hello:",cnt--);
write(fd,letter,strlen(letter)+1);
}
printf("%d\n",fd);
}
但是这么写用vim打开文件后,出现^@乱码
原因在于这里多加个1
因为文件要的是字符串的有效内容,文件中字符串不以反斜杠0结尾,不需要写入/0输入到文件中什么内容,文件就存什么内容。
去掉+1就行了。注意,改完后,原先的log.txt要rm删掉后,再运行一下这个代码文件,再vim打开log.txt。
现在这样改一下
会发现,log.txt变成了这样
可以看到,之前的数据没有被完全清除。
因为我们自己调用的open这个系统系接口,需要我们自己写一下清空数据的代码。
想要实现追加,可以这样用。
举一个读取文件的例子
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<assert.h>
#include<string.h>
#define FILE_NAME "log.txt"
int main()
{
umask(0);
int fd=open(FILE_NAME,O_RDONLY,0666);
if(fd<0)
{
perror("open");
return 1;
}
char buffer[10124];
ssize_t num=read(fd,buffer,sizeof(buffer)-1);
if(num>0)
buffer[num]=0;
printf("%s",buffer);
//bin int cnt=5;
// char letter[64];
// while(cnt)
//bin {
// sprintf(letter,"%s|%d\n","hello:",cnt--);
// write(fd,letter,strlen(letter));
//usr }
printf("%d\n",fd);
}
是为了printf以%s打印时能找到结尾。
文件操作的本质,进程和被打开文件的关系。
进程可以打开多个文件,系统中一定会存在大量被打开的文件,被打开的文件要被操作系统管理。
那么如何管理?
先描述,再组织。
操作系统要管理对应的打开文件,必定要为文件创建对应的内核数据结构来标识文件。
这个结构是struct file {}结构体,这个结构体包含了文件大部分属性。和FILE没有关系。
进程和被打开文件之间的关系是怎么维护的?
在下面这部分会讲到。
这个意思是讲number里面的值变成字符后直接加在字符串的后面。
比如number是1,那么文件名就是log.txt1
上面代码的结果如下
数字为什么从0,1,2开始?
这几个是默认被打开的三个标准输入输出流。
FILE *fp=fopen()中的FILE是个结构体。
fopen底层open访问文件时必须用系统调用接口,系统调用接口访问文件必须用文件描述符。
这个FILE结构体必定有一个字段,是文件描述符。
我们来看一下
可见,0,1,2三个文件描述符被三个默认被打开的三个标准输入输出流的FILE结构体中的文件描述符字段所占用。
- 文件描述符是什么?
系统启动时,键盘,显示器三个文件默认被打开。每个文件从磁盘被调用到内存时,会有自己的struct file结构体,struct file描述了大部分文件属性。每一个文件都有一个struct file,
进程的PCB中保存了一个指针,struct files_struct *files,指向struct files_struct结构体,结构体里面有一个数组,struct file*fd_array[](指针数组),指向struct file这个描述文件属性的结构体。
文件描述符的本质就是数组下标。struct file*fd_array[]叫做进程描述符表。
源码。
从小到大,按照顺序寻找最小的且没有被占用的fd,这是fd的分配规则。
而当我们这样写时,程序却打印不出结果。
所谓的close(1)是不再让文件描述符表file*fd_array[1]指向标准输出流(显示器)。
当我们open后,系统给我们打开了一个文件,系统在fd中找最小的,指向新打开的文件,上述程序即是1号下标指向此文件,此时open返回给fd的也是1。
我们向显示器中打印默认是想stdout中打印,stdout里对应的fd是1。
printf函数也是向stdout中打印,并且认为stdout的fd是1,此时,因为fd=1的数组位置已经指向新打开的文件,所以实际是向新打开的文件中输入内容。
而当我们用cat log.txt指令查看文件内容时,会找不到,这和缓冲区有关。
我们需要加上如下代码。
当我们close(1)后,运行程序后不会打印fd,需要在文件中才能查看fd,此时fd为1.
当我们去掉close(1)后。
运行程序,会直接打印fd。
上述过程中,当把fd=1,close后,printf实际是向文件中打印,我们把这个现象叫重定向。
重定向的本质是上层用的fd值不变,在内核中更改文件描述符表struct file*的内容。
为了更好的重定向,我们可以使用dug2函数。
失败返回-1。
我们写一个输入重定向。
此时,当我们直接运行此程序时,会直接打印log,txt中的内容。