目录
前言:
IO是Input/Output的首字母缩写,表示输入和输出,在Linux下一切皆为文件,使用文件无非只有读和写两种状态,即读对应Input,写对应Output,构成Linux下的基础IO。而对一个文件进行读写操作的前提是使用库函数 、系统调用函数先打开文件,然后再进行一系列的文件操作,本文着重介绍这些接口的使用。
c语言标准库给上层提供了大量的文件接口,通过这些接口上层就可以对文件进行操作了,最常用的接口如下。
在对文件进行读写操作前,首先需要使用fopen打开文件,fopen是c中标准库
fopen函数格式如下:
- FILE *fopen(const char *filename, const char *mode);
- //filename表示打开文件的路径,默认路径是该进程所在的路径
- //mode表示打开文件的模式:常用的有:r(可读),w(可写),a(追加写)
-
- int fclose(FILE *fp);
- //关闭一个文件流
fopen测试代码如下:
- #include
-
- int main()
- {
- // 打开文件的路径和文件名,默认在当前进程的路径下新建一个文件
- FILE *fp = fopen("log.txt", "a");//a表示追加写
- if(fp == NULL){
- perror("fopen");
- return 1;
- }
- fclose(fp);
- return 0;
- }
测试结果:

发现在进程fopen.c的路径下创建了一个log.txt文件。
fwrite将缓冲区内的数据输出至打开的文件中,该函数的格式如下:
- size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
- //ptr表示指向要写入文件的缓冲区
- //size表示从缓冲区内写入文件的数据的大小(单位字节)
- //count表示写入多少个size字节至文件中,比如size是3,count是3,则从缓冲区拿9个字节
- //stream表示写入的文件流
-
- //写入成功时返回count的值,写入失败返回小于(或不等于)count的数。
fwrite测试代码(注意fopen的打开模式是“w”):
- #include
- #include
-
- int main()
- {
- // 打开文件的路径和文件名,默认在当前进程的路径下新建一个文件
- FILE *fp = fopen("log.txt", "w");//此处要变成w
- if(fp == NULL){
- perror("fopen");
- return 1;
- }
-
- const char *message = "abcd\n";//定义输出缓冲区
- fwrite(message, strlen(message), 1, fp);
- fclose(fp);
-
- //sleep(1000);
- return 0;
- }
测试结果:

从结果来看,abcd正常的写入到文件log.txt当中并且换行,所以可以理解现在log.txt中的内容是abcd+\n。
更改上述代码中的缓冲区内容其他代码不变,并且重新执行该代码,观察log.txt中的内容是否发生变化:
const char *message = "该文件中只有这一句\n";
测试结果:

发现原先的内容abcd+\n被新内容覆盖了。原因就是fopen中“w“模式在写入前,会把文件内的内容全部清空,因此当重新调用fopen时会把目标文件的内容清空,所以若想保留文件中的内容对文件进行输出数据,要将fopen中的选项改成”a”追加选项。
选项a会保留文件的内容,并且把数据输出在文件的末尾处:
- #include
- #include
-
- int main()
- {
- // 打开文件的路径和文件名,默认在当前进程的路径下新建一个文件
- FILE *fp = fopen("log.txt", "a");//此处是a,表示追加写
- if(fp == NULL){
- perror("fopen");
- return 1;
- }
-
- const char *message = "追加写入\n";
- fwrite(message, strlen(message), 1, fp);
- fclose(fp);
-
- //sleep(1000);
- return 0;
- }
运行结果:

以上是将缓冲区内的数据输出至文件中,fread则是从文件中输入数据至缓冲区内,然后通过打印缓冲区将文件的内容显示到屏幕上。
fread格式如下:
- size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
- //ptr指向缓冲区的指针
- //size表示从文件中读取的每个元素的大小(单位字节)
- //nmemb表示希望从文件中读取nmemb个size的内容
- //stream表示读取的文件流
-
- //返回值表示最终读取的有效元素个数,读取失败返回-1,读到文件末尾返回0
fread测试代码(注意fopen中选项要改成“r”):
- #include
- #include
-
- int main()
- {
- // 打开文件的路径和文件名,默认在当前进程的路径下新建一个文件
- FILE *fp = fopen("log.txt", "r");//此处是a,表示追加写
- if(fp == NULL){
- perror("fopen");
- return 1;
- }
-
- char message[128];
- //-1的目的是预留一个位置给\0,否则读取的数据塞满了message数组,则后续手动
- //添加\0会导致越界访问
- size_t n = fread(message,sizeof(char),sizeof(message)-1,fp);
- if(n>0)
- {
- //printf("n=%d",n);
- message[n] = 0;
- printf("%s\n",message);
- }
- fclose(fp);
-
- //sleep(1000);
- return 0;
- }
运行结果:

c程序启动时会默认打开三个标准输入输出文件流,分别是stdin(键盘文件)、stdout(显示器文件)、stderr(显示器文件),其中stdout和stderr的区别是:stdout是标准输出流,stderr是标准错误流,后者通常用于打印一些错误信息,其中printf默认从stdout标准输出打印。
将上述代码中fwrite的目标文件流换成stdout,则缓冲区内的信息会打印在屏幕上:
- #include
- #include
-
- int main()
- {
- // 打开文件的路径和文件名,默认在当前进程的路径下新建一个文件
- FILE *fp = fopen("log.txt", "a");//此处是a,表示追加写
- if(fp == NULL){
- perror("fopen");
- return 1;
- }
-
- const char *message = "追加写入\n";
- fwrite(message, strlen(message), 1, stdout);//此处不再是fp而是stdout
- fclose(fp);
-
- //sleep(1000);
- return 0;
- }
测试结果:

文件是存储在磁盘上的,而磁盘属于硬件设备,因此访问文件的本质就是让计算机访问硬件设备,访问硬件设备的权力只有操作系统有,所以可以推断标准库提供的大量文件操作函数的底层是调用系统函数的,因此我们可以直接调用系统函数来实现操作文件。

open/close对标fopen/fclose,也有着创建文件和关闭文件的作用,只不过open更偏向底层一些,具体格式如下:
- #include
-
- int open(const char *pathname, int flags, mode_t mode);
- //pathname表示路径,即创建或打开文件的所在路径
- //flag是一个整形,但是他可以表示打开文件的多种模式,对标fopen中“w”,“a”选项,falg也是一个选项
- //mode表示新建文件的初始权限,用十进制来表示权限
-
- //返回值是一个整形,表示的是文件描述符,而不是fopen的文件指针,错误返回-1并设置错误码error
-
- int close(int fildes);
- //关闭一个文件描述符
从open的格式可以发现,flag比较特殊,因为他是一个整形而不是字符串的形式,所以系统规定了几个特殊的宏,这些宏才是表示文件可读、可写的各种选项,常用的宏如下:
O_RDONLY:以只读模式打开文件,对标“r”(必填之一)。O_WRONLY:以只写模式打开文件,对标“w”(必填之一)。O_RDWR:以读写模式打开文件,对标“rw”。O_CREAT:如果文件不存在,则创建新文件。通常需要与O_WRONLY或O_RDWR一起使用。O_APPEND:每次写入都在文件的末尾添加数据,对标“a”。O_TRUNC:如果文件已存在,并以写方式打开,则将其内容清空。
并且open的返回值是一个整形,表示文件描述符,和fopen不一样,但是该整形的用法和fopen的文件指针是一样的,即在该进程下,用文件描述符表示访问一个文件的入口。
open测试代码:
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- //0666 = rw- rw- rw-
- int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if(fd < 0)//当文件描述符小于0则说明打开失败
- {
- printf("open file error\n");
- return 1;
- }
-
- close(fd);//关闭文件描述符
- return 0;
- }
运行结果:

从运行结果来看,确实成功创建了文件log.txt,但是仔细观察发生log.txt的权限不是0666,而是0664,原因就是我们创建文件的最终权限=起始权限&(~umask),而umask默认值是0002,所以导致最后一个权限的中间bit位一定会是0,因此正确设置文件的初始权限,则要把umask的值设为0。
在上述代码调用open前加上
umask(0);
再次测试上述代码:

write的作用自然不用多说,和fwrite是一样的,因为fwrite底层调用的就是write,wrtie格式如下:
- #include
-
- ssize_t write(int fd, const void *buf, size_t count);
- //fd表示目标文件的文件描述符
- //buf表示缓冲区
- //count表示从缓冲区内读取的字节数(对标fwrite的size*count)
-
- //成功写入时返回写入的字节数,失败时返回-1
测试write的代码如下:
- #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)
- {
- printf("open file error\n");
- return 1;
- }
-
- const char message[] = "hello world\n";//自定义缓冲区
- ssize_t poi = write(fd, message, sizeof(message)-1);//无需把\0也写进文件中
- if (poi == -1) {
- perror("write");
- return -1;
- }
- close(fd);
- return 0;
- }
测试结果:

注意:因为open的打开模式加上了O_TRUNC,所以每次调用open时会把log.txt的内容清空再打开,若open模式中没有O_TRUNC,则每次调用open时不会清空文件内容,但是是从文件是最开始进行覆盖式的写入,具体代码如下:
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- umask(0);
- int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);//没有O_TRUNC
- if(fd < 0)
- {
- printf("open file error\n");
- return 1;
- }
-
- const char message[] = "zzz\n";//自定义缓冲区
- ssize_t poi = write(fd, message, sizeof(message)-1);//无需把\0也写进文件中
- if (poi == -1) {
- perror("write");
- return -1;
- }
- close(fd);
- return 0;
- }
测试结果:

从结果看到,原先内容的前四个字节被覆盖了。
O_APPEND” 若想实现系统函数open的追加方式,则在模式中添加O_APPEND宏即可,示例代码如下:
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- umask(0);
- int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);//没有O_TRUNC
- if(fd < 0)
- {
- printf("open file error\n");
- return 1;
- }
-
- const char message[] = "追加语句\n";//自定义缓冲区
- ssize_t poi = write(fd, message, sizeof(message)-1);//无需把\0也写进文件中
- if (poi == -1) {
- perror("write");
- return -1;
- }
- close(fd);
- return 0;
- }
运行结果:

不过值得注意的是: O_APPEND是一种方式并不包含写模式,所以使用O_APPEND时必须是写模式,否则连数据都无法写入文件中更别提追加了。
read是系统提供的系统函数,他的作用是读取文件中的内容至输入缓冲区内,和fread的作用是一样的,具体格式如下:
- #include
-
- ssize_t read(int fd, void *buf, size_t count);
- //fd表示文件描述符
- //buf表示存放文件内容的缓冲区
- //count表示从文件中读取多少字节的数据
-
- //读取成功时返回值表示读取到有效字节的个数,读取失败返回-1,读到文件末尾返回0
测试read的代码如下(注意将open的打开模式设为O_RDONLY或O_RDWR):
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- umask(0);
- int fd = open("log.txt", O_RDONLY,0666);
- if(fd < 0)
- {
- printf("open file error\n");
- return 1;
- }
-
- char message[128];//自定义缓冲区
- ssize_t poi =read(fd,message,sizeof(message)-1);//预留\0的位置
- if(poi==-1)
- {
- perror("read");
- return -1;
- }
- message[poi] = 0;
- printf("%s",message);
-
- close(fd);
- return 0;
- }
运行结果:

操作系统对新打开文件会创建一个结构体(struct file)来管理该文件,并且一个文件只有一个struct file,因为涉及到新打开的文件和新关闭的文件,所以关闭文件的时候会把该结构体删除,那么此时就要对管理文件的结构体进行数据结构化,因为要方便对其进行增删查改,所以有了文件管理链表,具体示意图如下:

描述文件的结构体struct file有了,那么底层是如何找到struct file的呢?答案是通过文件描述符找到的。
要想访问文件的前提是调用open函数打开文件,并且open函数会返回该文件的文件描述符,所以打开一个文件只能是在进程中完成,因为只有进程中才能调用函数(因此文件描述符肯定是存放在进程PCB中)。在一个进程内访问一个文件必须通过该文件的文件描述符,文件描述符在单个进程中具有唯一性。
PCB、文件描述符、struct file关系图如下:

通过上图可以发现文件描述符就是PCB中的一个结构体里的指针数组的下标,并且每个进程都会有属于自己是struct file_struct结构体,这也是为什么进程间的文件描述符是独立的。
打开文件会返回一个文件描述符,关闭一个文件描述符会关闭了一个文件吗?
答案:不会,因为一个进程打开的文件,别的进程也许也打开了这个文件,比如一个进程open了显示器文件,则open函数会返回一个文件描述符给该进程,让该进程可以通过这个文件描述符访问显示器,即打印数据在显示器上,但是当该文件关闭了该文件描述符,如果直接把显示器文件关闭了,则其他的进程就无法打印数据到显示器上了,这不符合逻辑,因此当多个进程使用同一份文件时,进程关闭自己的文件描述符不会直接关闭文件管理链表上的文件。
因为一个文件只有一个struct file,所以大量的进程打开同一个文件时,struct file结构体中有一个专门记录进程个数的引用计数,只有当引用计数变成0时代表当前没有进程访问该文件了,就会把文件的结构体struct file从文件管理链表中删除。
以下代码在一个进程内多次open文件,然后打印open返回的值,并观察其中的逻辑:
- #include
- #include
- #include
- #include
- #include
- #include
-
-
- int main()
- {
- umask(0);
- //一次创建四个文件
- int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
- int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
- int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
- int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
- if(fd1 < 0)
- {
- printf("open file error\n");
- return 1;
- }
- //打印这四个文件描述符观察其逻辑
- printf("fd1: %d\n", fd1);
- printf("fd2: %d\n", fd2);
- printf("fd3: %d\n", fd3);
- printf("fd4: %d\n", fd4);
-
- return 0;
- }
运行结果:

从结果发现,打开的第一个文件返回的文件描述符是从3开始,之后的文件描述符按顺序往下走,原因就是上文提到的c程序刚开始会默认打开三个文件流,分别是stdin、stdout、stderr,这三个文件流刚好就是0、1、2,所以我们在程序中open文件的文件描述符是从3开始的。
文件描述符0、1、2对应三个文件流stdin、stdout、stderr,所以当使用write接口时,可以把文件描述符1传给write,这样就可以在屏幕上看到输出的信息了。
示例代码:
- #include
- #include
-
- int main()
- {
- const char message[] = "hello world\n";//自定义缓冲区
- ssize_t poi = write(1, message, sizeof(message)-1);//此处fd传的1
- if (poi == -1) {
- perror("write");
- return -1;
- }
-
- return 0;
- }
运行结果:

并且可以通过stdin、stdout、stderr来验证他们对应的文件描述符是0、1、2,因为stdin、stdout、stderr是c程序启动时默认打开的文件流所返回的文件指针,所以该文件指针指向的文件结构体FILE肯定封装了底层的0、1、2,因此肯定通过stdin、stdout、stderr三个文件指针来找到底层的文件描述符并打印出来。示例代码如下:
- #include
-
- int main()
- {
- printf("stdin->fd: %d\n", stdin->_fileno);
- printf("stdout->fd: %d\n", stdout->_fileno);
- printf("stderr->fd: %d\n", stderr->_fileno);
-
- return 0;
- }
测试结果:

所以可以得出一个很重要的结论:c程序默认打开三个文件流的底层逻辑是进程会默认打开三个文件描述符0、1、2。
以上就是关于文件IO的讲解,理解文件IO的首要工作是理解输入和输出的概念,常常把write写入数据至文件这个过程看成是输出,把read从文件中读取数据看成是输入,并且清楚理解文件描述符的意义,以及他和文件指针的关系。