您现在的位置是:首页 >技术教程 >Linux文件理解和系统调用网站首页技术教程
Linux文件理解和系统调用
本文已收录至《Linux知识与编程》专栏!
作者:ARMCSKGT
演示环境:CentOS 7
文件理解和系统调用
前言
我们在学习C语言时可以使用fopen打开文件fclose关闭文件;那么是C语言帮我们打开的文件吗?其实并不是,语言没有这个能力,而是借助操作系统之手打开文件并进行操作,本篇将为大家介绍关于Linux下文件操作的系统调用,并介绍Linux系统如何组织和管理进程打开的文件!
正文
在对文件操作之前,我们需要知道文件在系统中到底是什么!
文件概念
- 文件操作本质:我们从语言层面是使用库函数操作文件,让我们以外文件操作是语言承担;但其实并非如此,文件操作是操作系统的事,是系统层面的问题,操作系统对于打开文件也会像管理进程一样通过先描述再组织进行管理
- 几乎所有语言都有文件操作,但是操作方法不太相同,但都是对系统接口的封装,不同的语言有不同的范式,所以使用上会有区别,底层调用的是同一个系统调用
- 操作文件的第一件事就是打开文件,而 文件=内容+属性 ,针对文件的操作有对文件内容的操作,也有对文件属性的操作;当文件没有被操作的时候,文件一般在磁盘(外存)上;当我们对文件进行操作的时候,文件需要被加载到内存中,因为冯诺依曼体系结构,CPU要读文件需要先把文件搬到内存
- 当我们对文件进行操作的时候,文件需要提前被加载到内存,至少需要把文件属性加载到内存中;而每分每秒不止一个文件被加载到内存(不止一个文件被打开),也可能不止一个用户在加载文件到内存,内存中一定存在大量的不同文件的属性
- 打开文件本质就是将我们需要的文件属性加载到内存中;操作系统内部一定会存在大量的被打开的文件,操作系统要管理这些被打开的文件,管理方式是:先描述再组织
所以:每一个被打开的文件都要在操作系统内对应文件对象的struct file对象,可以将所有的struct file对象用某种数据结构链接起来,在操作系统内部对被打开的文件进行管理就被转换成为了对数据结构的增删查改
–描述: 构建在内存中的文件结构体,也就是创建 struct file 对象记录被打开的文件的属性信息
–组织: 通过struct file类型对象指针形成链表(等其他数据结构),对文件的管理就变成了对数据结构的增删查改
- 文件其实可以被分成两大类:磁盘文件和被打开的文件(内存文件)
- 文件被打开,是操作系统在打开,是用户让操作系统打开的,用户是以进程(bash)为代表的;我们之前的所有的文件操作,都是 进程 和 被打开文件 的关系;
进程 和 被打开文件 的关系:struct task_struct 和 struct file 之间的联系结论:真正的文件操作是需要通过系统调用实现,而我们之前的文件操作都是进程与操作系统间的交互;文件被打开,操作系统要为被打开的文件,创建对应的内核数据结构struct file,其中包含各种属性和各种链接关系!
文件描述符
文件描述符概念
在C语言中,我们打开一个文件会形成一个FILE类型的指针,fopen打开文件会传递一个FILE类型的对象地址(底层创建了一个FILE对象)供我们使用,我们操作不同的文件围绕不同的FILE指针即可!
#include <stdio.h> int main() { FILE* f1 = fopen("test1.txt", "r"); FILE* f2 = fopen("test2.txt", "w"); //读取写入操作 fclose(f1); fclose(f2); f1 = f2 = NULL; return 0; }
代码中,我们要操作不同的文件只需要对f1和f2指针进行操作即可,但是FILE是一个对象,而底层是对文件描述符和其他属性进行封装,在操作系统层面上,对任何文件操作只认该进程的文件描述符!
FILE对象
这里分享FILE对象源码,FILE对象是_IO_FILE的重命名,_IO_FILE对象中 _fileno 就是文件描述符!typedef struct _IO_FILE FILE; //stdio库中对_IO_FILE类型重命名为FILE struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; //文件描述符 #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
打印文件描述符
我们尝试打印文件描述符,首先我们知道,C语言有三个默认的流:
- sdtin标准输入流(我们从键盘输入的文件流)
- stdout标准输出流(打印到屏幕的流)
- stderr标准错误输出流
这三个流都是FILE类型我们通过以下C++代码输出标准流的类型与普通文件流做对比:#include <iostream> #include <cstdio> #include <typeinfo> //typeid所需库 using namespace std; int main() { FILE* f1 = fopen("./test1.txt", "r"); FILE* f2 = fopen("./test2.txt", "w"); cout<<"类型对比:"<<endl; cout<<"f1指针类型:"<<typeid(f1).name()<<endl; cout<<"stdin指针类型:"<<typeid(stdin).name()<<endl; cout<<endl; cout<<"文件描述符"<<endl; cout<<"stdin文件描述符:"<<stdin->_fileno<<endl; cout<<"stdout文件描述符:"<<stdout->_fileno<<endl; cout<<"stderr文件描述符:"<<stderr->_fileno<<endl; cout<<"f1文件描述符:"<<f1->_fileno<<endl; cout<<"f2文件描述符:"<<f2->_fileno<<endl; fclose(f1); fclose(f2); f1 = f2 = NULL; return 0; }
从这里可以看出文件描述符是真实存在的,且是一个整数从0开始,而我们自己打开一个文件的文件描述符是从3开始的!
文件管理
操作系统要对这些打开的文件进行管理,否则每次操作文件都需要去内存中查找,高效的管理可以极大的提高IO效率!
对于打开的文件,操作系统会以先描述再组织的形式管理这些被打开的文件!
操作系统将这些打开的文件视为file对象,通过他们的file指针操作,并将这些file指针存入数组中,使用数组下标进行管理,这个数组为 file* fd_array[] ,而数组的下标就是神秘的文件描述符fd
除了文件描述符外,文件的属性还有文件权限、大小、路径、引用计数、挂载数等信息,将这些文件属性信息汇集起来就构成了 struct files_struct 这个结构体,它正是进程控制块 struct task_struct 中的成员之一!
而当一个程序启动时,操作系统会默认为他打开 stdin标准输入流 , stdout标准输出流 , stderr标准错误流 这三个文件流,将他们的文件file指针存入 fd_array[] 数组中,依次为 0,1,2 三个数组下标,也就是文件描述符;后续再打开文件时,默认将打开的文件file指针分配到当前数组未使用的最小下标处,所以用户打开文件一般是从3开始的,当然我们关闭所有标准流后打开文件就是从0开始分配文件描述符!
关于 files_struct
当我们打开一个文件时,在内存中会形成一个files_struct对象,files_struct对象是对该文件属性的描述!
针对每个进程都会打开文件,进程控制块task_struct中必然包含文件操作相关信息,也就是files_struct !
注意:当我们没有被打开时,文件在磁盘上;当文件被打开后,并不是直接将全部内容加载到内存上,而是先通过文件inode(后面介绍)找到磁盘上文件的详细信息,加载文件属性信息形成files_struct,待使用时再加载内容!
文件描述符的分配
文件描述符fd的分配规则是:分配到当前描述符数组未使用的最小下标位置处!
说明:
- 当我们打开文件时,因为默认的三个标准文件流已经打开,所以当前的最小下标一定是3
- 如果我们关闭了标准文件流,例如stdin(文件描述符为0),则新打开的文件会分配文件描述符0(未使用的最小下标处)
#include <iostream> #include <cstdio> using namespace std; int main() { cout<<"stdin文件描述符:"<<stdin->_fileno<<endl; cout<<"stdout文件描述符:"<<stdout->_fileno<<endl; cout<<"stderr文件描述符:"<<stderr->_fileno<<endl; FILE* f1 = fopen("./test1.txt", "r"); //先打开test1文件 cout<<"f1文件描述符:"<<f1->_fileno<<endl; cout<<"关闭stdin标准文件流"<<endl; //关闭stdin fclose(stdin); FILE* f2 = fopen("./test2.txt", "w"); //再打开test2文件 cout<<"f2文件描述符:"<<f2->_fileno<<endl; fclose(f1); fclose(f2); f1 = f2 = NULL; return 0; }
一切皆文件思想
我们知道在Linux系统下一切皆文件,我们如何理解这个概念?
对于Linux系统来说,无论是键盘还是显示器等设备,在他开来都是文件,是一个file对象
无论是硬件外设还是软件,对于操作系统来说无非就是输入和输出两个操作,所以操作系统对于这些硬件只需要提供读方法和写方法即可驱动该硬件(对于只读或只写的设备屏蔽其中一个方法即可),所以这些硬件设备被当成文件打开,在程序启动时将他们的file写入fd_array中管理即可,所以Linux下一切皆文件!
C语言文件操作
在讲解系统调用前,我们简单了解一下C库的文件操作方式!
如果想要详细的了解,请阅读官方文档:C文件操作库
文件的打开与关闭
文件的打开使用fopen,关闭使用fclose!
打开文件:
FILE * fopen ( const char * filename, const char * mode ); //打开文件
- 参数:
– filename:被打开文件的本地路径
– mode:打开方式(以字符串的方式传递)- 返回值:
–如果文件不存在则返回空指针
文件打开方式 含义 如果指定文件不存在 “r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错 “w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件 “a”(追加) 向文本文件尾添加数据 建立一个新的文件 “rb”(只读) 为了输入数据,打开一个二进制文件 出错 “wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件 “ab”(追加) 向一个二进制文件尾添加数据 出错 “r+”(读写) 为了读和写,打开一个文本文件 出错 “w+”(读写) 为了读和写,建议一个新的文件 建立一个新的文件 “a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件 “rb+”(读写) 为了读和写打开一个二进制文件 出错 “wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件 “ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件 关闭文件:
int fclose ( FILE * stream );
- 参数:
–stream:FILE文件指针- 返回值:
–成功返回0,失败返回EOF(-1)演示:
#include <stdio.h> int main() { FILE* f1 = fopen("./test1.txt", "r"); FILE* f2 = fopen("./test2.txt", "w"); if(f1 && f2) printf("文件打开成功! "); //不是空指针则打开成功 int m = fclose(f1); int n = fclose(f2); if(!m && !n) printf("文件关闭成功! "); //返回0则关闭成功 f1 = f2 = NULL; return 0; }
文件读写
文件读写结果配套出现,有读就有写!
文件读取接口:
//读取文件中的一个字符 int fgetc ( FILE * stream ); //读取文件中一行字符 char * fgets ( char * str, int num, FILE * stream ); //从给定流 stream 读取数据到 ptr所指向的数组中 size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); //从一个流中执行格式化输入,遇到空格和换行时结束 int fscanf ( FILE * stream, const char * format, ... ); //从字符串读取格式化输入,遇到空格和换行时结束 int sscanf ( const char * s, const char * format, ...);
文件写入接口:
//将字符character写入文件中 int fputc ( int character, FILE * stream ); //将字符串str写入文件中 int fputs ( const char * str, FILE * stream ); //把 ptr 所指向的数组中的数据写入到给定流 stream 中 size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ); //将字符串格式化输出到流 stream 中 int fprintf(FILE *stream, const char *format, ...);
其他写入接口:
//将字符串格式化写入str中 int sprintf(char *str, const char *format, ...); //向s字符串格式化写入n个字符 int snprintf ( char * s, size_t n, const char * format, ... );
snprintf是sprintf的升级版,可以控制写入字符数量,更加安全;在文件操作中,这两个接口常用于向缓冲区中写入数据,然后整体写入到文件中(为了更加方便合理的向文件中写入数据,一般预先定义缓冲区存储数据然后整体写入)!
示例:
这里我们使用fscanf格式化读取文件,snprintf格式化写入缓冲区,fprintf格式化写入到文件!#include <stdio.h> #include <stdlib.h> #define FNAME "log" //操作文件名 int main() { //写操作 FILE* wfp = fopen(FNAME,"w+"); //以只写的方式打开log文件,如果没有则创建 if(!wfp) //打开失败就退出 { perror("fopen error! "); exit(EOF); } char buf1[64] = {0}; //写入缓冲区 snprintf ( buf1, sizeof(buf1), "%s:%d", "向文件写入",668 ); //先格式化写入缓冲区 fprintf(wfp,"%s",buf1); //将缓冲区中的字符串整体写入文件中 fclose(wfp); //写操作完成后关闭文件 wfp = NULL; //读操作 FILE* rfp = fopen(FNAME,"r"); //以只读的方式打开log文件 if(!rfp) //打开失败就退出 { perror("fopen error! "); exit(EOF); } char buf2[64] = {0}; //读取缓冲区 fscanf(rfp,"%s",buf2); printf("%s ",buf2); fclose(rfp); //读操作完成后关闭文件 rfp=NULL; return 0; }
注意:我们在写入字符串时没有加入 换行,因为 是C语言定义的换行符,其他语言和软件可能无法识别,所以我们写入时建议不要带有一些仅语言定义的格式符;当我们使用cat打印出log文件中的信息时,输出了与我们文件操作一模一样的字符只不过没有换行!
文件操作系统调用
前面简单介绍了C语言的文件操作,现在我们来介绍Linux文件操作系统调用!
打开文件open
打开文件使用open接口,关于open接口:
#include <sys/types.h> //open接口所需库 #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); //打开文件可以修改权限
- 返回值:
–如果文件打开成功则返回对应的文件描述符,如果打开失败则返回-1- 参数:
–pathname:被打开文件的路径(与C语言保持一致)
–flags:打开方式,使用的标记位的方式传递选项信号(标记位以位图方式设置)
–mode:如果文件不存在,创建文件时的权限设置,文件起始权限为0666- 注意:
–这两个open函数类似于重载,如果我们打开的文件可能不存在,则一定要手动设置mode权限,否则文件创建出来权限是随机值组成的,这样可以保证文件安全;继承环境变量表后,umask 默认为 0002,当然也可以使用umask函数自定义!关于设置标志位flags的理解:
我们知道一个int有四字节,一共32个比特位,每一个比特位可以表示1/0!
而在open函数的flags函数中,我们可以想想为有32个开关,进行不同的组合,不能有相同的组合和包含某一个组合,这样某一个标志位都是独立的,表示一个指令信息!
如果我们要验证某一个标志位是否满足使用按位与即可,如果我们要融合多个标志位指令一起传递给函数使用按位或即可!
利用这个特性,我们可以实现一个小的位图deom:#include <stdio.h> #define ONE 0x1 //定义位图标志位(比特位不能包含和相同) #define TWO 0x2 #define THREE 0x4 void directives(int flags) { //模拟实现三种选项指令传递 if(flags & ONE) printf("ONE指令 "); if(flags & TWO) printf("TWO指令 "); if(flags & THREE) printf("THREE指令 "); } int main() { //使用按位或传递多个标志位参数 directives(ONE); printf("************************** "); directives(ONE | TWO); printf("************************** "); directives(ONE | TWO | THREE); return 0; }
关于flags常用的标志位:
O_RDONLY //只读 O_WRONLY //只写 O_APPEND //追加 O_CREAT //新建 O_TRUNC //清空 O_RDRW //可读可写 O_EXCL //文件必须是被创建的,如果文件已存在则报错-1
这些标志位可以通过按位或进行组合!
基于C语言打开方式的常用组合:
- w:O_WRONLY | O_CREAT | O_TRUNC (如果文件不存在则创建并清空文件内容,只写)
- a:O_WRONLY | O_CREAT | O_APPEND (如果文件不存在则创建,只追加写入)
- r:O_RDONLY (以只读的方式打开文件)
- …还有一些其他功能,根据标志位进行自由组合即可!
所以只要我们想使用open做到只写方式打开不存在的文件,也不会报错,加个 O_CREAT 参数即可实现自动创建!
关闭文件close
系统调用关闭文件使用close函数,与fclose相似!
#include <unistd.h> int close(int fildes); //fildes(fd)是文件描述符
close函数解析:
- 参数fildes:需要关闭文件的文件描述符
- 返回值:关闭成功返回0,失败返回-1
我们可以通过close(0),close(1),close(2)方式关闭标准文件流stdin,stdout,stderr!
写入文件write
write函数用于写入文件,其返回值类型有点特殊,但使用方法与fwrite基本一致!
#include <unistd.h> ssize_t write(int fildes, const void *buf, size_t count);
write函数解析:
- 参数:
– fildes:文件描述符
– buf:写入的字符串目标源指针或缓冲区(简称写入源)
– count:写入的字节数- 返回值:写入成功返回写入的字节数,失败返回-1
读取文件read
系统调用read用于从文件中读取指定字节的数据!
#include <unistd.h> ssize_t read(int fildes, void *buf, size_t count);
read函数解析:
- 参数:
– fildes:文件描述符
– buf:读入的缓冲区(将读入数据写入到buf缓冲区,可以是一个字符数组或开辟的空间)
– count:读入的字节数- 返回值:读取成功返回读入的字节数,读取失败返回-1
系统调用演示
注意:虽然我们使用系统调用写入数据,但是为了方便,我们在写入和读取时还是借助缓冲区buf比较好!
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define FNAME "log" //操作文件名 int main() { //以只写的方式打开文件,>如果不存在则创建且清空文件内容 int wfd = open(FNAME,O_WRONLY | O_CREAT | O_TRUNC,0664); assert(wfd>0); //检测是否打开成功 char buf1[64] = {0}; //写入缓冲区 snprintf ( buf1, sizeof(buf1), "%s:%d", "向文件写入",668 ); //先格式化写入缓冲区 int wsize = write(wfd,buf1,sizeof(buf1)); //写入缓冲区大小的内容 printf("写入%d字节 ",wsize); close(wfd); //关闭文件 //以只读的方式打开文件 int rfd = open(FNAME,O_RDONLY); assert(rfd>0); //检测是否打开成功 char buf2[64] = {0}; //写入缓冲区 int rsize = read(rfd,buf2,sizeof(buf2)); //向缓冲区buf2读入文件中缓冲区大小的内容 printf("读取%d字节 ",rsize); close(rfd); //关闭文件 printf("读入内容: %s ",buf2); return 0; }
同样的,我们cat打开文件没有换行的问题我们在前面C语言已经介绍了;不过需要注意的是,通过系统级函数 write 写入字符串时,不要刻意加上 ‘ ’,因为对于系统来说,这也只是一个普通的字符(‘ ’ 作为字符串结尾也是C语言的规定)与 的问题一样!
最后
Linux文件操作系统调用到这里就介绍的差不多了,本节我们介绍了Linux下关于文件操作的系统调用,了解了操作系统管理被打开文件使用文件描述符的概念,知道了语言库函数底层是对系统调用的封装,以及Linux下一切皆文件思想的依据等等,文件的学习还没结束,下一节我们继续探究通过文件描述符如何实现重定向功能!
本次 <Linux文件理解和系统调用> 就先介绍到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
?其他文章阅读推荐?
Linux<进程控制> -CSDN博客
Linux<进程地址空间> -CSDN博客
Linux<环境变量> -CSDN博客
Linux<进程初识> -CSDN博客
Linux<进程状态及优先级> -CSDN博客
?欢迎读者多多浏览多多支持!?