Linux文件与文件描述符的介绍
Posted 蓝乐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux文件与文件描述符的介绍相关的知识,希望对你有一定的参考价值。
基础IO(上)
本文将介绍Linux系统下的文件操作并从底层了解文件的相关知识。
前言
在开始介绍之前,我们先带着下面这几个问题去思考:
- 如何理解“Linux下一切皆文件”
- 进程启动同时会默认打开3个文件,这3个文件是什么
- 什么是文件描述符,为什么说有了文件描述符,就可以找到打开文件的所有细节
- 从语言和系统层面分别理解文件描述符fd与FILE的关系
那么接下来我们将开始介绍文件及文件描述符
C语言文件IO相关
操作接口回顾
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *fp);//成功返回0,否则返回EOF并报错
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);//将nmemb个size大小的成员从stream中读到ptr所指向的内存中,调用成功返回成功读取的个数,调用失败返回0
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);//将nmemb个size大小的成员从ptr所指向的内存写入到stream中,调用成功返回被成功写入的成员个数,调用失败返回0
int fprintf(FILE *stream, const char *format, ...);//用法与printf类似,只不过是从stream所指向的文件中读取信息打印
写文件:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
FILE* fp = fopen("log.txt", "w");
if(fp == NULL)
perror("fopen");
return 1;
const char* msg = "never give up\\n";
//将msg长度个1字节的信息从msg写入到fp所指向的文件中
fwrite(msg, 1, strlen(msg), fp);
fclose(fp);
return 0;
读文件:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
FILE* fp = fopen("log.txt", "r");
if(fp == NULL)
perror("fopen");
return 1;
const char* msg = "never give up\\n";
char buf[1024];
//将msg长度个1字节的内容从fp指向的文件中读取到buf数组
size_t s = fread(buf, 1, strlen(msg), fp);
//返回s为成功读取的个数
buf[s] = '\\0';//在下标s处标记\\0,表示字符串末尾
printf("%s\\n", buf);
fclose(fp);
return 0;
三个默认打开的文件
在介绍接下来的内容之前,首先说明:任何C语言程序,都会默认打开3个文件,即:标准输入(stdin
),标准输出(stdout
)以及标准错误(stderr
),分别对应着键盘文件,显示器文件,显示器文件。
在这之前,我们插入一个话题。这里开始引入“Linux下一切皆文件”的概念了。
首先,我们知道键盘和显示器都是硬件,为什么要在其后加上文件二字呢?这就要说到所有的外设无外乎读和写操作。
不同硬件的读写操作一般是不同的,由于Linux底层是由C语言写的,因此在系统底层,不同文件对应着不同的结构体,C语言是如何在结构体内定义方法的呢?那就是通过函数指针来解决。
对于这么多的文件,操作系统(OS)要不要管理呢?当然,那么管理的六字箴言:先描述,在组织,就如下图所示一般:
由此,我们就可以直接通过C语言接口,对stdin,stdout,stderr
进行读写了:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
const char* msg = "Linux so easy!\\n";
//往显示器上写入msg的内容
fwrite(msg, 1, strlen(msg), stdout);
char buf[1024];
//从键盘读取10个字节的内容到buf数组中
size_t s = fread(buf, 1, 10, stdin);
buf[s] = '\\0';//字符串以\\0结束
printf("%s\\n", buf);
//由于stderr也是显示器文件,因此效果与往stdout上打印一样
fprintf(stderr, "never give up\\n");
return 0;
回顾:打开文件的方式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
系统文件IO相关
操作接口
其实除了上述C语言接口,系统也提供了文件相关的调用接口。
open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open接口的第一个参数与fopen一样都是文件名
第二个参数为打开的方式,选项如下:
O_RDONLY
:只读打开
O_WRONLY
: 只写打开
O_RDWR
: 读,写打开
这三个常量,必须指定一个且只能指定一个
O_APPEND
:追加
O_CREAT
:创建
若选项有多个则可以用|号进行“或”运算。
第三个参数mode可以联系之前介绍的chmod来理解,就是三种访问者的访问权限。
返回值:
·成功:新打开的文件描述符
·失败:-1
close
#include <unistd.h>
int close(int fd);
与fclose类似,只不过fclose传入的参数为open成功调用的返回值fd。
返回值:成功调用返回0,否则返回-1并报错。
write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
与fwrite类似,将buf指针指向的内容写入fd所标识的文件,写入count个字节
返回为成功写入的字节。
read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read将fd所标识的文件的内容读取到buf指针所指向的内存中,读取count个字节,返回值为成功读取的字节。
代码调用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
//打开文件,以只写的方式,若文件不存在则创建之,创建的权限为664
int fd = open("block.txt", O_WRONLY | O_CREAT, 0664);
if(fd == -1)
perror("open");
return 1;
const char* msg = "never give you up.\\n";
write(fd, msg, strlen(msg));
close(fd);
fd = open("block.txt", O_RDONLY);
if(fd == -1)
perror("open");
return 1;
char buf[64];
ssize_t s = read(fd, buf, strlen(msg));
if(s > 0)
buf[s] = '\\0';
printf("%s\\n",buf);
close(fd);
return 0;
文件描述符fd
我们先来看下面这段代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
int fd = open("test.txt", O_WRONLY|O_CREAT, 0644);
printf("fd : %d\\n", fd);
close(fd);
return 0;
可以看到运行结果为3,那么问题来了:为什么这个创建的文件描述符是3,而不是0,1,2呢?这就和前面所介绍的3个文件联系起来了,前面说过C语言程序中,会默认打开标准输入、标准输出及标准错误三个文件,而其对应的文件描述符就是0、1、2。
默认打开的三个文件并非C语言独有,应该说是所有语言在创建进程时都会打开的。
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象。而在进程控制块(PCB)中有一个指针指向存放file*的数组,数组中的每个成员都指向一块file结构体。
而文件描述符本质上就是fd_array数组的下标,由fd则可以找到对应的file结构体。
分配规则
文件描述符fd与系统的对应关系
我们已经知道创建一个新的文件,为其分配的文件描述符是从3开始的,那么我们再执行如下代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
close(0);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
printf("log.txt fd:%d\\n", fd);
close(fd);
return 0;
可以看到结果为0,那么此时我们得出结论,fd的分配规则为第一个最小的未使用的fd下标。
初步理解重定向
我们再试试关闭fd为1的文件:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
close(1);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
const char* msg = "never give up\\n";
write(fd, msg, strlen(msg));
printf("fd:%d\\n", fd);
close(fd);
return 0;
可以看到调用printf后屏幕上并没有打印出我们想要的内容,这是因为1号fd标识的为标准输出,也就是说本来我们往显示器上打印的内容此时写入了修改后的1号文件log.txt
。但此时我们打开log.txt文件,发现写入了msg的内容,却没有调用printf后的内容。
此时我们修改代码,在原printf
函数后加上
fflush(stdout);
此时运行程序后再打开log.txt文件:
发现printf
打印的内容出现再log.txt文件中了。
而原本要写入标准输出文件的内容却写入到log.txt文件中,这种现象就叫做重定向,这便是我们初步认识输出重定向。
深入理解文件描述符与FILE
其实在C语言底层代码中FILE是一个结构体,而在这个结构体中有:
int _fileno; //封装的文件描述符
也就是说,C语言将文件描述符进行了二次封装,实际上C语言也是通过文件描述符来操作文件的。为什么C语言要这么做呢?这是因为不同系统下的文件管理不一定相同,这次介绍的Linux是如此,但到了windows下又不一定了,因此语言层需要对此进行封装,保证平台的可移植性。
理解数据在文件层面的流动过程
数据在写入文件之前会先写到语言层的缓冲区中,当遇到\\n或者通过fflush函数接口强制刷新时,才会写入系统层的文件中。
缓冲区的理解
在上面理解输出重定向时,我们明明在printf函数中加入了\\n换行,为什么一开始没有写入到文件中呢?这是因为对于缓冲区而言是行缓冲,也就是说如果遇到\\n或则fflush强制刷新,就会清空缓存区,将数据写入文件中;而文件的缓冲是全缓冲,只有写满文件才会清空缓冲区。
而我们在一开始关闭1号fd时只是将数组中指针的链接关系改变了,并未关闭stdout标准输出文件,也就是说调用printf函数仍是将数据写入了语言层的缓冲区,但是由于系统已经知晓fd所指向的为普通文件,因此缓冲规则却是执行的全缓冲,因此只是靠\\n并不会清空语言层的缓冲区,而需要通过fflush强制刷新缓冲区才能够将数据写入log.txt文件中。
小结
回顾一开始所提出的问题,我们现在就有了更深层次的理解。
1.“Linux下一切皆文件”:系统对于硬件进行读写方法的封装,保证了可以通过系统IO接口调用进行操作,因此Linux下一切皆文件
2. 进程启动同时会默认打开3个文件,这3个文件是:标准输入、标准输出及标准错误
3. 什么是文件描述符,为什么说有了文件描述符,就可以找到打开文件的所有细节:文件描述符是文件指针数组的下标,数组中的指针指向了描述文件的file结构体
4. 从语言和系统层面分别理解文件描述符fd与FILE的关系:语言层面,FILE对fd进行了封装,保证可移植性;系统层面,fd可以管理上层语言接口的文件操作。
以上是关于Linux文件与文件描述符的介绍的主要内容,如果未能解决你的问题,请参考以下文章