文件=内容+属性
如果在磁盘上建立一个为0KB的文件,磁盘上也会存储文件的属性。(因为文件的属性也是数据).
文件的操作
- 1.一种是对于文件内容做操作。
- 2.另外一种是对于文件的属性,比如说更改文件的相关权限等。
注意:
- 对于文件内容做操作也可能会影响文件属性改变,比如说文件的大小发生了变化。
- 由于文件在磁盘(硬件)中存放,我们访问文件,先写代码->编译->链接->生成可执行文件->运行。
注意:
- 访问文件的本质是进程在访问,进程访问文件是需要通过接口(语言层面的接口,C语言、C++)访问的。
- 如果想要向硬件写入,那么只能通过操作系统。如果普通用户也想向硬件写入,那么就必须让操作系统提供相应的系统调用接口(文件类的系统调用接口)。由于文件类的系统调用接口比较复杂,所以一写语言对于这些系统调用接口进行了封装,这就导致了不同语言有不同的语言级别的文件访问接口(都不一样),但是都封装的是系统接口,底层原理相同。
为什么要学习OS层面的文件接口?
- 1.由于选择的操作系统只有一个,这样的接口,在Linux上只有一套,其他OS也是一样的。
- 2.这样的代码具有跨平台性。使用C++的平台都可以使用C++的文件接口,使用Python的平台都可以使用Python的文件接口。
- 如果语言不提供给文件的系统接口的封装,所有访问文件的操作都必须直接使用操作系统的接口。(Windows的系统接口和Linux的系统接口种类,参数等都是不一样的)。
注意:
使用语言的用户,也需要访问文件,但是一旦使用系统接口编写文件代码,这份代码只能在该平台中使用,无法在其他平台上运行,不具有跨平台性。而C语言和C++语言将所有平台中的代码都实现出来并封装好。然后采用条件编译的方式,在编译的时候实现动态编译。
由于显示器也是硬件,那么printf向显示器打印,也是一种写入,和磁盘写入到文件没有本质区别!
在Linux下,硬件设备可以被看成文件,可以进行scanf、fgets、printf、cin、cout。在C/C++程序在编译的时候,将代码加载到你的程序中,这就是默认情况下会打开stdin、stdout、stderr。
- stdin标准输入->默认为键盘
- stdout标准输出->默认为显示器
- stderr标准错误->默认为显示器
对于文件来说,读和写是文件操作的核心。
对于显示器来说,printf/cout本质上也是一种写入(write),对于键盘来说,scanf/cin本质上也是一种读入(read).将输入内容给显示器一份也给程序一份。站在内存角度,程序必须加载到内存,键盘将我们输入的数据交给内存,系统将内存中的数据刷新到显示器或者写入到硬盘中,也就是对应着input和output(也就是I/O的动作)。如果一个普通文件,我们使用fopen/fread去读取它,他就被读取到我们程序(进程)的内部(内存),再使用fwrite将数据写入到文件中,从普通程序读取到进程内部(内存)就是input,从内存中写入文件中就是output。
文件的定义:站在系统角度上来说,能够被input读取,能够被output写出的设备就是文件。
- 狭义上的文件:普通的磁盘文件(.txt .doc文件等等)
- 广义上的文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设都可以称之为文件。(这些设备都具有上面可以被读或者被写的特点)。
- 文本文件:把数据的终端形式的二进制数据输出到磁盘上存放,也就是说存放的是数据的终端形式。
- 二进制文件:把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放,也就是说存放的是数据的原形式。
- 绝对路径:是从目录树的树根"/"目录开始往下直至到达文件所经过的所有节点目录。下级目录接在上级目录后面用“/"隔开。
- 相对路径:相对路径是指目标目录相对于当前目录的位置。
- 注意:绝对路径都是从“/"开始的,所以第一个字符一定是“/"。
文件的打开
fopen():打开文件
FILE *fopen( const char *filename, const char *mode );
文件的关闭
fclose():关闭文件
int fclose( FILE *stream );
文件的读写
fgetc():读取一个字符
fputc():写入一个字符
fgets():读取一个字符串
fputs():写入一个字符串
fprintf():写入格式化数据
fscanf():格式化读取数据
fread():读取数据
fwrite():写入数据
int fgetc( FILE *stream );
int fputc( int ch, FILE *stream );
char *fgets( char *str, int count, FILE *stream );
int fputs( const char *str, FILE *stream );
int fprintf( FILE *stream, const char *format, ... );
int fscanf( FILE *stream, const char *format, ... );
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
文件状态检查
feof():文件是否结束
ferror():文件读/写是否出错
clearerr():清除文件错误标志
ftell():文件指针的当前位置
int feof ( FILE * stream );
int ferror ( FILE * stream );
void clearerr ( FILE * stream );
long int ftell ( FILE * stream );
文件指针定位
rewind():把文件指针移到开始处
fseek():重定位文件指针
void rewind ( FILE * stream );
int fseek ( FILE * stream, long int offset, int origin );
注意:r+具有读写属性,从文件头开始写,保留原文件中没有被覆盖的内容,w+也具有读写属性,如果原文件存在,会被清空从头开始写。
perror打印错误信息
#include
#include
int main()
{
//当前目录中不存在这个文件
FILE* fp=fopen("log.txt","r");
//如果打开失败,就以文本形式返回
if(fp==NULL)
{
perror("fopen");
exit(1);
}
return 0;
}
在Linux下,如果当前是以写的方式的话,如果写入的文件不存在,会在当前路径下直接创建。
#include
#include
int main()
{
FILE* fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen");
exit(1);
}
fclose(fp);
while(1) sleep(1);
return 0;
}
从上图我们可以发现ext指向的路径就是我们当前进程的工作路径。
cwd:进程的内部属性,就是我们这个进程所指向的工作目录。将cwd所指向的路径,和我们上面传入的文件拼接起来,形成完整的路径名称。当一个进程运行起来,每个进程都会记录当前所处的工作路径,当你打开一个文件的时候,创建的文件就是我们当前路径下,也就是我们进程所处的路径下。由于我们进程内部会直接用这个进程的cwd也就是当前的工作路径,然后将文件名test拼接起来形成我们的exe找到可执行文件。
注意:进程具有确定性,一般我们程序将程序部署到系统中,路径一般不发生改变,比如说你安装了一个程序到D盘中,那么路径就在D盘下。
相关代码:
#include
#include
#include
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);
const char* s2="hello fprintf\n";
//往特定的文件流中,写入特定的字符串
fprintf(fp,"%s",s2);
const char* s3="hello fputs\n";
fputs(s3,fp);
//关闭文件
fclose(fp);
return 0;
}
#include
#include
#include
int main()
{
FILE * fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen");
return 1;
}
const char* s1="hello world\n";
fwrite(s1,strlen(s1),1,fp);
fclose(fp);
return 0;
}
由于是在w模式下,先将文件打开的时候,将文件中的内容进行清空处理,然后才写入内容!
这里我们通过echo指令对log.txt进行写入内容,首先将文件中的全部内容被清空。这与先清空再写入的原理相同!
#include
#include
#include
int main()
{
FILE *fp=fopen("log.txt","a");
if(fp==NULL)
{
perror("fopen");
return 1;
}
const char*s1="hello world\n";
fwrite(s1,strlen(s1),1,fp);
fclose(fp);
return 0;
}
上图产生的原因是因为打开了文件,而不是将文件的内容进行清空的操作而是将文件后面的内容进行追加处理。
#include
#include
#include
int main()
{
FILE* fp=fopen("log.txt","r");
if(fp==NULL)
{
perror("fopen");
return 1;
}
char line[64];
while(fgets(line,sizeof(line),fp)!=NULL)
{
//将读到行的内容显示出来,同时也可以通过printf的方式打印出来
printf("%s\n",line);
//也可以通过标准输出的方式打印出来
fprintf(stdout,"%s",line);
}
fclose(fp);
return 0;
}
cat命令用来打印文件中的内容。
#include
#include
#include
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("argc error!\n");
return 1;
}
//argv[1]就是我们传入的参数
FILE* fp=fopen(argv[1],"r");
//如果打开失败的话,则将失败的原因以文本的方式进行返回
if(fp==NULL)
{
perror("fopen");
return 2;
}
//按行读取
char line[64];
while(fgets(line,sizeof(line),fp)!=NULL)
{
//将读取的行显示出来
printf("%s",line);
//往显示器上写,stdout就是标准输出
fprintf(stdout,"%s",line);
}
//关闭文件
fclose(fp);
return 0;
}
这里我们通过给argv[1]中传入文件名,我们可以发现我们可以打印其文件中的内容。
- #include
- #include
- #include
- int open(const char *pathname, int flags);
- int open(const char *pathname, int flags, mode_t mode);
- pathname: 要打开或创建的目标文件
- flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
- 参数:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR : 读,写打开
- 这三个常量,必须指定一个且只能指定一个
- O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND: 追加写
- 返回值:
- 成功:新打开的文件描述符
- 失败:-1
如何给函数传递标志位?
在宏定义中利用大写字母和_来表示一个宏,上面的flags只能传递一种状态,但是如果在C语言中传入大量的选项呢,这时候我们可以利用标记位来表示该状态是否的意思,例如一个int是32字节,那么一个标记位就是一个比特位,那么就可以表示32个状态了。这种表示方式就是一种数据结构--位图
测试代码:
#include
#include
#include
#define ONE 0x1
#define TWO 0x2
#define THREE 0x3
void show(int flags)
{
if(flags&ONE) printf("hello one\n");
if(flags&TWO) printf("hello two\n");
if(flags&THREE) printf("hello three\n");
}
int main()
{
show(ONE);
printf("--------------------------------\n");
show(TWO);
printf("--------------------------------\n");
show(ONE|TWO);
printf("--------------------------------\n");
show(ONE|TWO|THREE);
return 0;
}
测试代码:
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("log.txt",O_CREAT);
if(fd<0)
{
perror("open");
return 1;
}
printf("open success fd:%d\n",fd);
return 0;
}
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("log.txt",O_WRONLY);
if(fd<0)
{
perror("open");
return 1;
}
printf("open success fd:%d\n",fd);
return 0;
}
这里我们发现,使用O_WRONLY不会自动创建文件,我们需要添加O_CREAT选项来解决。
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("log.txt",O_WRONLY|O_CREAT);
if(fd<0)
{
perror("open");
return 1;
}
printf("open success fd:%d\n",fd);
return 0;
}
传入标记位
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("open success fd:%d\n",fd);
return 0;
}
这里我们通过给文件设置相关的权限为0666(即-rw-rw-rw-),而实际上不是,是由于系统中有创建时默认的权限umask权限(默认是0002)
如果想要得到我们想要设置的权限,那么我们只需要将此时umask设置为0即可解决问题。
关闭文件
#include
#include
#include
#include
#include
#include
int main()
{
//设置文件掩码为0
umask(0);
int fd=open("log.txt",O_WRONLY|O_CREAT);
if(fd<0)
{
perror("open");
return 1;
}
printf("open success fd:%d\n",fd);
//关闭文件
close(fd);
return 0;
}
向文件中写入
测试代码:
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
const char* s="hello write\n";
write(fd,s,strlen(s));
printf("open success fd:%d\n",fd);
close(fd);
return 0;
}
这里我们只将上述代码写入字符串s改变为“HeLLoWRITE”,那么会写入到文件中是什么内容呢?
这里我们发现此时在log.txt中是进行覆盖式的写入,而不是先清空再进行写入的方式!
先清空文件再向文件中写入
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
const char* s="hello\n";
write(fd,s,strlen(s));
printf("open success fd:%d\n",fd);
close(fd);
return 0;
}
这里我们发现此时文件首先进行了清空处理然后重新写入了hello。
在文件后面进行追加方式。
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd<0)
{
perror("open");
return 1;
}
const char* s="hello write\n";
write(fd,s,strlen(s));
printf("open success fd:%d\n",fd);
close(fd);
return 0;
}
对文件进行读取处理
以只读的方式打开
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd=open("log.txt",O_RDONLY,0666);
if(fd<0)
{
perror("open");
return 1;
}
char buffer[64];
memset(buffer,'\0',sizeof(buffer));
read(fd,buffer,sizeof(buffer));
printf("%s\n",buffer);
printf("open success fd:%d\n",fd);
close(fd);
return 0;
}
为什么打开一个文件,默认的文件描述符为3呢?
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("open success fd:%d\n",fd);
close(fd);
return 0;
}
这里我们发现此时打开的文件该文件的文件描述符为3。那么此时我们打开多个文件呢?
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd1=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
printf("open success fd1:%d\n",fd1);
int fd2=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
printf("open success fd2:%d\n",fd2);
int fd3 =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
printf("open success fd3:%d\n",fd3);
int fd4=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
printf("open success fd4:%d\n",fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
这里我们发现打开多个文件此时文件描述符是从3开始递增的,为什么不是从0开始呢?
这是因为在C语言中,系统会默认打开三个默认的文件,分别是stdin,stdout,stderr,它们都是FILE*类型的,每一个文件都对应了一个文件描述符。0-标准输入,1-标准输出,2-标准错误。
测试代码:
#include
#include
#include
#include
#include
#include
int main()
{
//向显示屏中打印
fprintf(stdout,"hello fprintf\n");
const char*s="hello write\n";
write(1,s,strlen(s));//向显示屏中写入
//从标准输入中读取
int a=10;
fscanf(stdout,"%d",&a);
printf("%d\n",a);
//从0中读取
char buff[16];
size_t ss=read(0,buff,sizeof(buff));
if(ss>0)
{
buff[ss]='\0';
printf("%s\n",buff);
}
return 0;
}
这里的FILE是由C语言的标准库提供的!,是一个结构体,所以其内部封装了很多属性,由于是C语言的库函数,所以内部一定要进行系统调用,在系统角度只认识fd,由于是FILE结构体,所以其内部一定封装了fd!
验证是否存在fd代码:
#include
#include
#include
#include
#include
#include
int main()
{
printf("stdin:%d\n",stdin->_fileno);
printf("stdout:%d\n",stdout->_fileno);
printf("stderr:%d\n",stderr->_fileno);
return 0;
}
上述代码说明了:C语言将我们系统中的0,1,2文件封装进了stdin,stdout,stderr中,而stdin,stdout,stderr都是结构体指针,其内部的_fileno属性就是我们对应的0,1,2!
首先进程需要访问文件,那么就必须先将文件打开。一个进程可以打开多个文件,文件打开的目的就是被访问,文件想要被访问,前提是加载到内存当中才能够被访问!如果多个进程打开自己的文件,那么系统可能会存在大量被打开的文件!那么OS将这些被打开的文件进行先描述再组织的管理方式,首先OS内部需要管理每一个被打开的文件,首先需要构建出内核的结构体对象struct file,这是一种内核数据结构,里面包含了文件的所有内容和属性(包括文件的打开的时间,修改时间,所属组,所有者等等)
struct file结构体定义在/linux/include/linux/fs.h(Linux 2.6.11内核)中,其原型是:
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
struct file_ra_state f_ra;
unsigned long f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
};
创建出struct file对象来充当一个被打开的文件,如果被打开的文件很多,那么此时需要OS用双链表将这些对象组织起来!所以一个进程的PCB只要找到上面那个打开的文件的链表头部就可以找到全部的文件!
进程与文件的对应关系是多对一的,在内核当中为了维护进程和文件的关系,会有一个数组(struct_file *array[32])(指针数组,数组中的类型都是struct file*),第一个数组元素指向第一个文件,依次类推,系统就可以通过数组进行哈希索引来找到对应的文件。
综上所述:fd在内核中,本质上来说是数组下标!
在内存当中的文件,OS通过双链表来添加我们新打开的文件,并且将其地址放入struct_file*array数组中,然后OS可以通过fd可以查询到对应文件的存储地址,进而可以读取到打开的文件!
(文件对象)struct file里面包含了文件中的所有内容,(文件描述符表)struct_file*array也可以成为文件映射表。本质上来说文件描述符表就是数组下标!
内存文件:被进程打开的文件(open属于系统调用),这里的文件称为内存文件。
磁盘文件:没有被进程打开,那么此时该文件保留在磁盘中,磁盘上保存的文件(内容和属性).
一旦操作系统打开了大量文件的情况,操作系统此时就需要将这些文件管理起来,先构建出struct file对象,里面包含了文件的所有内容和属性(包括文件的打开时间,修改时间,所属组,所有者等信息)。文件存在于磁盘当中,本身就有属性,从磁盘中被加载到内存中,相关属性被填充到结构体中。
Linux进程默认的情况下会有3个缺省打开的文件描述符,分别是0,1,2,对应的标准输入,标准输出,标准错误,对应的物理设备是:键盘,显示器,显示器。
这里我们测试一下将0号文件描述符关闭:
#include
#include
#include
#include
#include
int main()
{
//这里我们将0号文件描述符关闭
close(0);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("fd");
return 1;
}
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
close(fd);
return 0;
}
这里我们发现此时打开的log.txt此时分配的文件描述符是0,同理我们可以关闭2号文件符,我们也可以得到该分配的文件描述符为2。
这里我们需要阐述一下fd的分配规则:这里fd的分配是按照当前文件描述符中最小的,还没有被占用的文件描述符。由于OS会帮我们维护文件映射表,所以OS会默认分配最小的,没有被占用的文件描述符。由于上述代码中0号文件描述符被关闭,所以新分配的文件描述符就是0。
如果我们关闭1号文件描述符会发生什么?
测试代码:
#include
#include
#include
#include
#include
int main()
{
//这里我们将1号文件描述符关闭
close(1);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("fd");
return 1;
}
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
close(fd);
return 0;
}
这里我们发现此时关闭1号文件描述符文件不打印,这时我们注释close(fd)观察到
由于printf默认是往stdout中打印,由于关闭了1号文件描述符(显示器),所以分配文件描述符是log.txt分配给1号文件描述符,所以打印到log.txt中。
这里我们也可以不注释close(fd),直接利用fflush将此时内容刷新到log.txt中。
#include
#include
#include
#include
#include
int main()
{
//这里我们将1号文件描述符关闭
close(1);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("fd");
return 1;
}
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
测试代码:
#include
#include
#include
#include
#include
#include
int main()
{
//这里我们将1号文件描述符关闭
close(1);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("fd");
return 1;
}
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
printf("fd:%d\n",fd);
fprintf(stdout,"hello fprintf\n");
const char*s="hello write\n";
fwrite(s,strlen(s),1,stdout);
fflush(stdout);
close(fd);
return 0;
}
这里我们发现将往显示器中打印的都打印到log.txt中。这里我们注释close(1)可以观察到:
此时我们发现所有的内容都打印到显示器上,也就是说本来1是显示器,如果将1关闭,按照上述文件描述符分配规则,我们log.txt分配的文件描述符就是1,然后本应该打印到显示器上的内容都被打印到我们的文件中。这个我们称之为输出重定向!