种一棵树最好的时间是十年前,其次是现在!
本节目录
1.预备知识
1.在系统角度去理解文件,文件 = 内容+属性(也是数据);
在Linux下,一切皆文件!
键盘,显示器,可以被看出文件吗?
我们从来没有打开过键盘和显示器,但我们依旧可以进行sanf,fgets,printf,cout,…,为什么?
其实,我们的C/C++程序 会默认打开三个文件流
标准输入(键盘) extern FILE* stdio
标准输出(显示器)extern FILE* stdout
标准错误(显示器)extern FILE* stderr
2.文件 = 内容+属性,所以对文件的所有操作,无外乎:a.对内容 b.对属性
文件在磁盘上放着,我们访问文件,先写代码,编译成exe可执行文件,在运行,在访问文件,本质是谁在访问文件呢?进程。
要向硬件写入,只有谁有权利呢?操作系统。
普通用户也想写入呢?必须让OS提供接口,也就是文件类的调用接口!
3.我们进行文件操作的时候,为什么要对文件类的系统接口进行封装呢?
(1).比较难,语言上对一些系统接口做一下封装,是为了让接口更好用。
这也导致了一个问题,导致了不同的语言,有不同的语言级别的文件访问接口(都不一样),但是,底层都是封装了系统调用接口。
所以为什么要学习OS层面的文件接口呢?
因为,这样的接口只有一套,学习完系统层面的文件接口后,可以更好的理解和使用各种语言层面的文件接口。
(2).跨平台
如果语言不提供对文件的系统接口的封装,那么所有访问文件的操作,都必须使用os的接口。像Linux,它的编写语言是C语言,系统接口是用C语言编写的,当用户编写所谓的文件代码(其他语言的),则无法在他们的平台上进行运行,也就不具备跨平台性。
4.显示器是硬件,printf向显示器打印,也是一种写入,和磁盘写入到文件,没有本质区别,Linux下,一切皆文件!
5.Linux下,一切皆文件
什么叫做文件呢?
站在系统的角度,能够被input读取,或者能够被output写入到设备的就叫做文件!
狭义文件:普通的磁盘文件。
广义文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设,都可称之为文件。
普通文件->fopen/fread->(input)你的进程的内部(内存)->fwrite->(output)文件中。
6.进程文件
当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径。cwd->/home/jyf/lesson3-30
fopen 以w方式打开文件,默认先清空文件(注意:在fwrite之前)
fopen以a方式打开文件,追加,不断的向原文件增加内容
2. fwrite函数
fwrite函数
fwrite() 是 C 语言标准库中的一个文件处理函数,功能是向指定的文件中写入若干数据块,如成功执行则返回实际写入的数据块数目。该函数以二进制形式对文件进行操作,不局限于文本文件。
函数原型:size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
(1)buffer:是一个指针,对fwrite来说,是要获取数据的地址;
(2)size:要写入内容的单字节数;
(3)count:要进行写入size字节的数据项的个数;
(4)stream:目标文件指针;
(5)返回实际写入的数据项个数count。
说明:写入到文件的哪里? 这个与文件的打开模式有关,如果是w+,则是从file pointer指向的地址开始写,替换掉之后的内容,文件的长度可以不变,stream的位置移动count个数;如果是a+,则从文件的末尾开始添加,文件长度加大。
fseek对此函数有作用,但是fwrite 函数写到用户空间缓冲区,并未同步到文件中,所以修改后要将内存与文件同步可以用fflush(FILE *fp)函数同步。
注意:返回值随着调用格式的不同而不同:
(1) 调用格式:fwrite(buf, sizeof(buf), 1, fp);
成功写入返回值为1(即count)
(2)调用格式:fwrite(buf, 1, sizeof(buf), fp);
成功写入则返回实际写入的数据个数(单位为Byte)
注意事项:
写完数据后要调用fclose()关闭流,不关闭流的情况下,每次读或写数据后,文件指针都会指向下一个待写或者读数据位置的指针。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
//文件操作
const char* s1 = "hello fwrite\n";
fwrite(s1,strlen(s1),1,fp);
//不要+1,\0结尾是C语言的规定,文件用遵循吗?不用,文件要保存的是有效数据
//fwrite(s1,strlen(s1+1),1,fp);
const char* s2 = "hello fprintf\n";
fprintf(fp,"%s",s2);
const char* s3 = "hello fputs\n";
fputs(s3,fp);
fclose(fp);
//while(1)
//{
//sleep(1);
//}
return 0;
}
一个字符串是以’\0’结尾的,这是语言层面的东西,将一个字符串写入到文件中时,是不需要将’\0’写入到文件中的,文件保存的是有效数据。
3.按行读取文件内容
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc!=2)
{
perror("argc error\n");
return 1;
}
FILE* fp = fopen(argv[1],"r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
char line[64];
//按行读取
while(fgets(line,sizeof(line),fp))
{
fprintf(stdout,"%s",line);
}
fclose(fp);
return 0;
}
运行结果:
[jyf@VM-12-14-centos lesson3-30]$ ./myfile1 Makefile
myfile1:myfile1.c
gcc -o $@ $^
.PHONY: clean
clean:
rm -f myfile1
4.只传一个参数,让它表示不同的状态,展现不同的结果
#include <stdio.h>
#include <string.h>
//用int中的不重复的一个bit,就可以标识一种状态
#define ONE 0x1 //0000 0001
#define TWO 0x2 //0000 0010
#define THREE 0x4 //0000 0100
void show(int flags)
{
if(flags & ONE) printf("hello one\n"); //0000 0011 & 0000 0001
if(flags & TWO) printf("hello two\n");
if(flags & THREE) printf("hello three\n");
}
int main()
{
show(ONE);
show(TWO);
show(ONE | TWO); //0000 0001 | 0000 0010 = 0000 0011
show(ONE |TWO | THREE);
return 0;
}
运行结果:
[jyf@VM-12-14-centos lesson3-30]$ ./myfile2
hello one
hello two
hello one
hello two
hello one
hello two
hello three
5.系统层面的那一套文件接口
5.1 open函数
open() 系统调用
通过open()系统调用来打开文件并获得一个文件描述符。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open (const char *name, int flags);
int open (const char *name, int flags, mode_t
mode);
open() 系统调用将路径名name 给出的文件与一个成功返回的文件描述符相关联,文件位置指针被设定为零,而文件则根据flags给出的标志位打开
open()的flags参数
flags参数必须是以下之一:O RDONLY,O_WRONLY,O_RDWR。这些参数各自指定要用只读,只写还是读写模式打开文件。
举例来说,下面的代码以只读方式打开文件/home/kidd/madagascar。
int fd = open (‘‘/home/jyf/log.txt’’, O_RDONLY);
if (fd<0)
{
perror("open");
return 1;
}
以只写方式打开的文件不能被读取,反之亦然。进程必须要有足够的权限才能调用open() 系统调用对文件进行访问。
flags 参数可以和以下一个或多个值进行按位或运算,用以修改打开文件请求的行为。
O_APPEND 文件已追加模式下打开。就是说,在每次写操作之前,文件位置指针将被置于文件末尾。即使在进程刚刚完成写操作并改变文件位置指针之后,如有另一进程开始写操作,情形也是如此。
O_ASYNC 当指定文件可写或者可读时产生一个信号(默认是SIGIO)。这个标志仅用于终端和套接字,不能用于普通文件。
O_CREAT 当name指定的文件不存在时,将由内核来创建。如果文件已存在用于在opendir()内部使用。
O_DIRECT 打开文件用于直接I/O
O_EXCL 和O_CREAT一起给出的时候,如果由name给定的文件已经存在,则open()调用失败。用来防止文件创建时出现竞争。
O_LARGEFILE 给定文件打开的时候将使用64位偏移量,这样大于2G的文件也能被打开。
O_NOCTTY 如果给定的name指向一个终端设备,它将不会成为这个进程的控制终端,即使该进程目前没有控制终端。
O_NOFOLLOW 如果name是一个符号链接,opne()调用会失败。
O_NONBLOCK 如果可以,文件 将在非阻塞模式下打开。
O_SYNC 打开文件用于同步I/O。
O_TRUNC 如果文件存在,且为普通文件,并且允许写,将文件的长度截断为0。
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
int main()
{
umask(0); //默认的权限掩码为2,此处将其置为0,则当我们新创建一个文件的时候,文件的权限为0666
int fd = open("log.txt",O_WRONLY|O_CREAT,0666);//rw-rw-rw-
//int fd = open("log.txt",O_RDONLY);
if(fd<0)
{
perror("open");
return 1;
}
//open success
printf("open success,fd:%d\n",fd);
close(fd);
return 0;
}
除非创建了新文件,否则mode参数会被忽略;而当O_CREAT给出时则需要。
mode参数
S_IRWXU 所有者拥有读写和执行权限
S_IRUSR 所有者拥有读权限
S_IWUSR 所有者拥有写权限
S_IXUSR 所有者拥有执行权限
S_IRWXG 组拥有者读写和执行权限
S_IRGRP 组拥有者读权限
S_IWGRP 组拥有者写权限
S_IXGRP 组拥有者执行权限
S_IRWXO 任何其他人都有读写和执行的权限
S_IROTH 任何其他人都有读权限
S_IWOTH 任何其他人都有写权限
S_IXOTH 任何其他人都有执行权限
5.2 系统接口演示
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
int main()
{
umask(0);
//int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);//rw-rw-rw-
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//rw-rw-rw-
//int fd = open("log.txt",O_RDONLY);
if(fd<0)
{
perror("open");
return 1;
}
//const char* s = "hello write";
const char* a = "aaa\n";
write(fd,a,strlen(a));//这里的strlen(a)不用+1,语言层面字符串是以\0结尾,但系统层面没这个规定
//open success
printf("open success,fd:%d\n",fd);
close(fd);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
int main()
{
//umask(0);
//int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);//rw-rw-rw-
//int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//rw-rw-rw-
int fd = open("log.txt",O_RDONLY);
if(fd<0)
{
perror("open");
return 1;
}
//const char* s = "hello write";
//const char* a = "aaa\n";
//write(fd,a,strlen(a));//这里的strlen(a)不用+1,语言层面字符串是以\0结尾,但系统层面没这个规定
char buffer[64];
memset(buffer,'\0',sizeof(buffer));
read(fd,buffer,sizeof(buffer));// log.txt文件为aaa,此处将fd指向的文件内容read到buffer中
printf("%s",buffer);//aaa
//open success
printf("open success,fd:%d\n",fd);
close(fd);
return 0;
}
6.文件描述符 file descriptor(fd)
6.1 初识
stdin的文件描述符默认为0
stdout的文件描述符默认为1
stderr的文件描述符默认为2
FILE *fopen(const char *path, const char *mode);
FILE是一个struct结构体,由C标准库提供,一般内部会有多种成员。
C文件 库函数 内部一定要有系统调用!
在系统角度,认FILE,还是认fd? 只认fd
说明FILE结构体中,必定封装了fd。
证明如下:
int main()
{
printf("stdin:%d\n",stdin->_fileno);
printf("stdout:%d\n",stdout->_fileno);
printf("stderr:%d\n",stderr->_fileno);
return 0;
}
运行结果:
[jyf@VM-12-14-centos lesson4-7]$ ./myfile1
stdin:0
stdout:1
stderr:2
stdout
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
fprintf(stdout,"hello stdout\n"); //往显示器上打印hello stdout
//stdout的文件描述符默认为1,所以,也可以下面的方式在显示器上打印
const char* s = "hello 1\n";
write(1,s,sizeof(s));
return 0;
}
运行结果为:
[jyf@VM-12-14-centos lesson4-7]$ ./myfile1
hello stdout
hello 1
stdin
//int main()
//{
// int a =0;
// fscanf(stdin,"%d",&a);
// printf("%d\n",a);
// return 0;
//}
int main()
{
char input[16];
ssize_t s = read(0,input,sizeof(input)); //read之后默认会在末尾加'\n'
if(s>0)
{
input[s] = '\0';
printf("%s",input); }
return 0;
}
6.2 具体过程
进程要访问文件,必须先打开文件!
一个进程可以打开多个文件吗?
一般而言 进程:打开的文件 = 1:n的
文件要被访问,前提是加载到内存中,才能被直接访问。
进程:打开的文件 = 1:n的,如果是多个进程都打开自己的文件呢?
系统中会存在大量被打开的文件!所以,OS要不要把如此之多的文件也管理起来呢?要的,先描述,再组织
在内核中,如何看待打开的文件?OS内部要为了管理每一个被打开的文件,构建
struct file
{
struct file* next;
struct file* prev;
//包含了一个被打开文件的几乎所有内容(不仅仅包含属性)
}
创建struct file的对象,充当一个被打开的文件,如果有很多呢?再用双链表组织起来。
根据上图,我们可以知道我们是如何访问到我们想访问的文件的。
举个例子:
fwrite()->FILE*->fd->write->write(fd,…)->自己执行操作系统内部的write方法->能找到进程的task_struct->fs->files_struct->fd_array[ ]->fd_array[fd]->struct file->内存文件被找到了!->操作。
详细描述:
我们执行fwrite方法向某个文件写入,具体是怎么找到那个文件进行操作的呢?
调用fwrite函数向某个文件进行写入,操作系统会在文件的FILE中给其找到并分配最近的,没被占用的文件描述符fd,我们知道fwrite是语言层面的,要向硬件写入,它内部必定是封装了系统调用的,也就是write函数,将fd给到write函数,在执行write函数,此进程的task_struct结构体中有一个struct files_struct files指针,指向struct files_struct,struct files_struct里面有一个struct file array[]数组,数组里面保存的是OS给每个打开的文件都分配的struct file,write将拿到的fd,填入到struct file* array[]数组中,通过struct file* array[fd]便可找到我们要操作的文件的struct file[],在通过它内部的信息找到它对应的文件所在位置,然后进行对应的操作即可!
下文我们继续文件描述符的理解!!!