本文将介绍Linux系统下的文件操作并从底层了解文件的相关知识。
前言
在开始介绍之前,我们先带着下面这几个问题去思考:
- 如何理解“Linux下一切皆文件”
- 进程启动同时会默认打开3个文件,这3个文件是什么
- 什么是文件描述符,为什么说有了文件描述符,就可以找到打开文件的所有细节
- 从语言和系统层面分别理解文件描述符fd与FILE的关系
那么接下来我们将开始介绍文件及文件描述符
C语言文件IO相关
操作接口回顾
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *fp);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);
写文件:
#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";
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];
size_t s = fread(buf, 1, strlen(msg), fp);
buf[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";
fwrite(msg, 1, strlen(msg), stdout);
char buf[1024];
size_t s = fread(buf, 1, 10, stdin);
buf[s] = '\0';
printf("%s\n", buf);
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()
{
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可以管理上层语言接口的文件操作。
|