【Linux应用开发·入门指南】详解文件IO以及文件描述符的使用
目录
1. 核心概念
2. 系统IO和标准IO对比
文件描述符vs文件流
缓冲机制
3. 标准IO
3.1 打开文件(fopen)
3.2 关闭文件(fclose)
3.3 写入字节(fputc)
3.4 写入字符串(fputs)
3.5 格式化写入(fprintf)
3.6 读取字节(fgetc)
3.7 读取字符串(fgets)
3.8 格式化读取(fscanf)
4. 系统IO
4.1 打开文件(open)
4.2 读取文件(read)
4.3 写入文件(write)
4.4 关闭文件(close)
4.5 组合使用
5. 文件描述符
1. 核心概念
在开始前我们先来了解一下什么是文件IO?
在 Linux 系统中,一个基本哲学是 “Linux下一切皆文件”。这意味着不仅普通的磁盘文件是文件,设备、管道、套接字等都可以被抽象成文件,通过统一的文件 I/O 接口进行操作。
狭义上讲,指普通的文本文件,或二进制文件,包括日常所见的txt文档、源代码、word文档、压缩包、图片、视频、音频文件等:
广义上讲,除了狭义上的文件外,几乎所有可操作的设备或结构都可视为文件。包括键盘、鼠标、硬盘、串口、显示屏、触摸屏等,也包括网络通讯端口(多机通信要用到的文件)、进程间通讯管道(单机通信)等抽象概念:
2. 系统IO和标准IO对比
进行文件 I/O 的核心就是一组系统调用,它们直接与 Linux 内核交互,实现对文件的操作。对文件的操作,基本上就是输入输出,因此也一般称为IO接口:
- 在操作系统层面上:这一组专门针对文件的IO接口就被称为系统IO --- 偏向于底层(设备文件)
- 在标准库的层面上:这一组专门针对文件的IO接口就被称为标准IO --- 偏向于上层(软件程序文件)
其中系统IO控制文件的方式,是众多系统调用中专用于文件操作的一部分接口:
标准IO控制文件的方式,是众多标准函数当中专用于文件操作的一部分接口:
总结一下:标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口,如果把系统IO比喻为菜市场,提供各式肉蛋奶菜,那么标准IO就是对这些基本原理的进一步封装,是品类和功能更加丰富的酒庄饭店。
特性维度 | 系统 I/O | 标准 I/O |
---|---|---|
提供方 | 操作系统(Linux 系统调用) | C 运行库(遵循 ANSI C 标准) |
接口层级 | 底层,直接进入内核 | 高层,建立在文件 I/O 之上 |
核心概念 | 文件描述符,一个整数(如 0 , 1 , 2 ) | 文件流,FILE* 结构体指针 |
缓冲机制 | 无缓冲或内核缓冲,用户无感知 | 用户态缓冲(全缓冲、行缓冲、无缓冲) |
主要函数 | open , read , write , lseek , close | fopen , fread , fwrite , fprintf , fscanf , fclose |
性能特点 | 每次调用都触发系统调用,上下文切换开销大 | 通过缓冲区减少系统调用次数,效率通常更高 |
控制粒度 | 精细,可控制每一次读写的确切位置和方式 | 较粗,操作单位是流,定位不如 lseek 精确 |
可移植性 | 依赖于操作系统,不同 Unix 系统可能略有差异 | 跨平台性好,遵循 C 标准,代码无需修改 |
适用场景 | 设备文件、管道、套接字、需要精确控制(如数据库) | 普通磁盘文件、文本处理、格式化 I/O |
这里补充两个概念:文件描述符和文件流、以及缓冲机制。
文件描述符vs文件流
文件描述符:是进程文件描述符表的索引,内核通过它来找到对应的文件。它是非负整数。
文件流:是 C 库定义的 FILE 结构体指针。这个结构体内部封装了一个文件描述符,以及一个 I/O 缓冲区和其他状态信息。
缓冲机制
系统 I/O:每次 read/write 都是一次系统调用,需要从用户态切换到内核态,开销较大。虽然内核也有自己的页面缓存,但系统调用的开销无法避免。
标准 I/O:在用户空间维护一个缓冲区。
- 全缓冲:当缓冲区满时,才进行一次实际的 I/O 操作(系统调用)。常用于磁盘文件。
- 行缓冲:遇到换行符 \n 或缓冲区满时,进行 I/O 操作。常用于标准输入/输出。
- 无缓冲:立即输出。常用于标准错误 stderr。
举个例子,假如要写入 1000 字节,每次 1 字节。
系统IO:
使用 write:会进行 1000 次 系统调用。
标准IO
使用 fputc(假设缓冲区 1024 字节):前 1024 次调用都只是在填充用户缓冲区,0 次系统调用。只有当缓冲区满或调用 fflush 时,才进行 1 次 write 系统调用。
补充解释,通过例子解释一下区别:
解释一下系统调用(系统IO)和库函数(标准IO)不同的叫法但是表示的东西是一样的。
假如你想要去银行取钱
你的应用程序 = 你本人
用户空间 = 银行大厅
内核空间 = 银行金库和核心业务系统
库函数 = 银行大堂经理或ATM机
系统调用 = 进入金库的授权指令
此时你想要去取100块钱
方法一:
- 你走到金库门口(用户态到内核态的边界)。
- 你大喊一声系统调用指令:sys_open("我的保险箱")(触发软中断,进入内核态)。
- 金库保安(内核)验证你的身份,打开金库门。
- 你进去找到你的保险箱,再喊:sys_read(保险箱, 余额信息)。
- 你查看余额,然后喊:sys_write(保险箱, 取出100, 余额更新)。
- 你拿着100块钱,走出金库(返回用户态)。
- 整个过程繁琐、高风险(你自己操作)、效率低(每次进出金库都要严格检查)。
方法二:
- 你走到ATM机(库函数,如printf或自定义函数)前。
- 你插入银行卡,输入密码,点击“取款100元”(调用库函数)。
- ATM机(库函数内部)帮你完成了一系列复杂操作:它可能先检查自己的钱箱够不够(用户空间逻辑)。然后,它代替你去与银行核心系统(内核)交互,执行了上述所有sys_open, sys_read, sys_write等系统调用。它还可能为你提供了交易凭条(格式化输出)。你直接从ATM出钞口拿到100块钱。
- 整个过程简单、安全、高效,因为你不需要关心底层细节,而且ATM机可能还有缓存,取小额钱甚至不需要每次都联系核心系统。
3. 标准IO
为了方便演示,我们创建一个file_io的文件进行后续操作,首先打开文件夹:
新建一个文件,命名file_io(随便命名,没影响),完成后点击Create,然后点击上方open:
对于这里不明白可自行参考,下方链接VScode部分:
Linux应用开发·如何在Linux上安装VScode、gcc编译流程以及如何进行静态/动态链接的打包使用·详细步骤演示-CSDN博客
3.1 打开文件(fopen)
开始前我们先来了解一下其功能:
功能 | 获取指定文件的文件指针 | |
头文件 | #include <stdio.h> | |
原型 | FILE *fopen (const char *__restrict __filename, const char *__restrict __modes) | |
参数 | __restrict __filename | 即将要打开的文件 |
__restrict __modes | “r”:以只读方式打开文件,要求文件必须存在。 | |
"r+":以读写方式打开文件,要求文件必须存在。 | ||
"w" :以只写方式打开文件,文件如果不存在将会创建新文件,如果存在将会将其内容清空。 | ||
"w+" :以读写方式打开文件,文件如果不存在将会创建新文件,如果存在将会将其内容清空。 | ||
"a":以只写方式打开文件,文件如果不存在将会创建新文件,且文件位置偏移量被自动定位到文件末尾(即以追加方式写数据)。 | ||
"a+" :以读写方式打开文件,文件如果不存在将会创建新文件,且文件位置偏移量被自动定位到文件末尾(即以追加方式写数据)。 | ||
返回值 | 成功 | 文件指针 |
失败 | NULL | |
备注 | 备注 | 总共6种打开模式,不能自创别的模式,比如"rw"是非法的 |
那么我们来演示一下,在刚刚打开的文件内新建文件:
创建一个.c文件:
编写代码,作用是读io.txt文件,如果为空则打开失败,如果不为空则打开成功:
#include<stdio.h>int main(int argc, char const *argv[])
{/*** char *__restrict __filename:字符串表示要打开的文件名称* char *__restrict __modes:访问模式* (1)r: 只读模式,如果没有文件报错* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件* return: FILE * 结构体指针,表示一个文件* 报错返回NULL* extern FILE *fopen (const char *__restrict __filename,const char *__restrict __modes)*/char *filename = "io.txt";FILE * ioFile = fopen(filename,"r");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打卡文件成功\n");}return 0;
}
测试为了方便演示还没有创建io.txt文件。
然后创建一个Makefile文件,文件内容:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@
对于这段代码我们分析一下:
定义编译器为 gcc,你可以将其理解为一个宏定义,方便后续更改:
CC:=gcc
例如,我们现在使用命令:
gcc -o test1 test1.c
gcc -o test2 test2.c
gcc -o test3 test3.c
gcc -o test4 test4.c
gcc -o test5 test5.c
gcc -o test6 test6.c
后面不想使用gcc编译器了,这样后续修改就会非常麻烦。
然后编译程序:
-$(CC) -o $@ $^//等效于
gcc -o fopen_test fopen_test.c
- $@ = 目标文件名 (fopen_test)
- $^ = 所有依赖文件 (fopen_test.c)
运行编译好的程序,并且根据上述描述,可以拆分为:
-./$@//等效于
./fopen_test
删除可执行文件:
-rm ./$@//等效于
rm ./fopen_test
整理一下:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
打开终端make一下看看:
对于Makefile的使用,可以参考:
Linux应用开发·Makefile菜鸟教程-CSDN博客
我们可以看到发生了报错,那是因为我们不能打开一个不存在的文件,我们可以使用一下别的命令,例如将r改为w,记得保存一下:
#include<stdio.h>int main(int argc, char const *argv[])
{/*** char *__restrict __filename:字符串表示要打开的文件名称* char *__restrict __modes:访问模式* (1)r: 只读模式,如果没有文件报错* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件* return: FILE * 结构体指针,表示一个文件* 报错返回NULL* extern FILE *fopen (const char *__restrict __filename,const char *__restrict __modes)*/char *filename = "io.txt";FILE * ioFile = fopen(filename,"w");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打卡文件成功\n");}return 0;
}
在make一下可以看到,显示打开成功,并且新创建了一个io.txt文件:
3.2 关闭文件(fclose)
了解一下功能:
功能 | 关闭指定的文件并释放其文件 | |
头文件 | #include <stdio.h> | |
原型 | int fclose (FILE *__stream); | |
参数 | FILE *__stream | 即将要关闭的文件 |
返回值 | 成功 | 0 |
失败 | EOF(负数) 通常关闭文件失败会直接报错 | |
备注 | fclose函数涉及内存释放,不可对同一个文件多次关闭 |
然后创建一个文件,编写代码:
#include<stdio.h>int main(int argc, char const *argv[])
{char *filename = "io.txt";FILE * ioFile = fopen(filename,"w");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打卡文件成功\n");}int result = fclose(ioFile);if(result == EOF){printf("关闭文件失败\n");}else if(result == 0){printf("关闭文件成功\n");}return 0;
}
对Makefile添加关闭相关的代码:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_testfclose_test:fclose_test.c -$(CC) -o $@ $^-./$@-rm ./$@
运行一下看看:
当然这里你不想来回切换输命令,可以下载一个插件:
下载完这个插件后,Makefile的每条target上方都出现执行按钮,点击即可运行:
3.3 写入字节(fputc)
先来了解一下功能:
功能 | 将一个字符写入到一个指定的文件 | |
头文件 | #include <stdio.h> | |
原型 | int fputc (int __c, FILE *__stream); | |
参数 | int __c | 要写入的字符,ASCII码对应的char |
FILE *__stream | 写入的文件指针,要打开的一个文件 | |
返回值 | 成功 | 写入到的字符,返回char |
失败 | EOF | |
备注 | 无 |
创建文件编写代码:
#include<stdio.h>int main(int argc, char const *argv[])
{/*** char *__restrict __filename:字符串表示要打开的文件名称* char *__restrict __modes:访问模式* (1)r: 只读模式,如果没有文件报错* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件* return: FILE * 结构体指针,表示一个文件* 报错返回NULL* extern FILE *fopen (const char *__restrict __filename,const char *__restrict __modes)*/char *filename = "io.txt";FILE * ioFile = fopen(filename,"w");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打开文件成功\n");}/*** int __c: ASCII码对应的char* FILE *__stream: 打开的一个文件* return: 成功返回char,失败返回EOF* int fputc (int __c, FILE *__stream);*/int put_result = fputc(97,ioFile);if(put_result == EOF){printf("写入文件失败\n");}else{printf("写入文件成功\n");}/*** FILE *__stream: 即将要关闭的文件* return: 成功0, 失败EOF(负数),通常关闭文件失败会直接报错* int fclose (FILE *__stream);*/int result = fclose(ioFile);if(result == EOF){printf("关闭文件失败\n");}else if(result == 0){printf("关闭文件成功\n");}return 0;
}
然后对Makefile进行修改:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_testfclose_test:fclose_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputc_test:fputc_test.c -$(CC) -o $@ $^-./$@-rm ./$@
运行一下看看:
可以发现在io.txt当中写入了a:
3.4 写入字符串(fputs)
同样的了解一下功能:
功能 | 将数据写入到一个指定的文件 | |
头文件 | #include <stdio.h> | |
原型 | int fputs (const char *__restrict __s, FILE *__restrict __stream); | |
参数 | const char *__restrict __s | 要写入的字符串 |
FILE *__restrict __stream | 需要写入的文件 | |
返回值 | 成功 | 返回非负整数(0,1) |
失败 | EOF | |
备注 | 无 |
非常简单的代码,其他的继续复制粘贴,补充字符串的写入代码:
#include<stdio.h>int main(int argc, char const *argv[])
{/*** char *__restrict __filename:字符串表示要打开的文件名称* char *__restrict __modes:访问模式* (1)r: 只读模式,如果没有文件报错* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件* return: FILE * 结构体指针,表示一个文件* 报错返回NULL* extern FILE *fopen (const char *__restrict __filename,const char *__restrict __modes)*/char *filename = "io.txt";FILE * ioFile = fopen(filename,"w");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打开文件成功\n");}/*** const char *__restrict __s: 要写入的字符串* FILE *__restrict __stream: 需要写入的文件* return: 成功返回非负整数(0,1),失败返回EOF* int fputs (const char *__restrict __s, FILE *__restrict __stream);*/ int putsR = fputs(" love letter\n",ioFile);if(putsR == EOF){printf("写入字符串失败\n");}else{printf("写入字符串%d成功\n",putsR);}/*** FILE *__stream: 即将要关闭的文件* return: 成功0, 失败EOF(负数),通常关闭文件失败会直接报错* int fclose (FILE *__stream);*/int result = fclose(ioFile);if(result == EOF){printf("关闭文件失败\n");}else if(result == 0){printf("关闭文件成功\n");}return 0;
}
来到Makefile,同样的方法修改:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_testfclose_test:fclose_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputc_test:fputc_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputs_test:fputs_test.c -$(CC) -o $@ $^-./$@-rm ./$@
测试一下:
可以看到数据写入:
如果我们不想覆盖就可以将打开文件的权限有w换成a,进行追加:
3.5 格式化写入(fprintf)
都是非常简单的用法:
功能 | 将格式化数据写入到一个指定的文件或内存 | |
头文件 | #include <stdio.h> | |
原型 | int fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...) | |
参数 | FILE *__restrict __stream | 打开的文件 |
const char *__restrict __fmt | 带格式化的长字符串 | |
... | 可变参数,填入格式化的长字符串 | |
返回值 | 成功 | 返回写入的字符串长度,也就是字符的个数,不包含换行符 |
失败 | EOF | |
备注 | 无 |
编写代码,其他的复制fclose的代码,然后添加fprintf的代码:
#include<stdio.h>int main(int argc, char const *argv[])
{/*** char *__restrict __filename:字符串表示要打开的文件名称* char *__restrict __modes:访问模式* (1)r: 只读模式,如果没有文件报错* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件* return: FILE * 结构体指针,表示一个文件* 报错返回NULL* extern FILE *fopen (const char *__restrict __filename,const char *__restrict __modes)*/char *filename = "io.txt";FILE * ioFile = fopen(filename,"w");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打开文件成功\n");}/*** FILE *__restrict __stream: 打开的文件* const char *__restrict __fmt: 带格式化的长字符串* ...可变参数: 填入格式化的长字符串* return:成功,返回写入的字符串长度,也就是字符的个数,不包含换行符* 失败,返回EOF* int fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...)*/char *name = "翠花";int printfR = fprintf(ioFile,"能和我一起去狗熊岭吗?\n\t\t %s",name);if(printfR == EOF){printf("字符串写入失败\n");}else{printf("字符串%d写入成功\n",printfR);}/*** FILE *__stream: 即将要关闭的文件* return: 成功0, 失败EOF(负数),通常关闭文件失败会直接报错* int fclose (FILE *__stream);*/int result = fclose(ioFile);if(result == EOF){printf("关闭文件失败\n");}else if(result == 0){printf("关闭文件成功\n");}return 0;
}
添加Makefile的代码:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_testfclose_test:fclose_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputc_test:fputc_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputs_test:fputs_test.c -$(CC) -o $@ $^-./$@-rm ./$@fprintf_test:fprintf_test.c -$(CC) -o $@ $^-./$@-rm ./$@
运行完如下:
3.6 读取字节(fgetc)
描述一下功能:
功能 | 读取文件内容 | |
头文件 | #include <stdio.h> | |
原型 | int fgetc (FILE *__stream); | |
参数 | FILE *__stream | 要打开的文件 |
返回值 | 成功 | 读取到的一个字节 |
失败或者到文件的末尾 | EOF | |
备注 | 注意文件需要有读权限 |
运用非常简单,就一句,顺便打印一下:
char c = fgetc(ioFile);printf("%c\n",c);
完整的函数,注释参考之前任意一个的,都一样:
#include<stdio.h>int main(int argc, char const *argv[])
{//打开文件FILE * ioFile = fopen("io.txt","r");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打开文件成功\n");}//读取文件的内容char c = fgetc(ioFile);printf("%c\n",c);//关闭文件int result = fclose(ioFile);if(result == EOF){printf("关闭文件失败\n");}else if(result == 0){printf("关闭文件成功\n");}return 0;
}
来到Makefile去添加相关代码:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_testfclose_test:fclose_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputc_test:fputc_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputs_test:fputs_test.c -$(CC) -o $@ $^-./$@-rm ./$@fprintf_test:fprintf_test.c -$(CC) -o $@ $^-./$@-rm ./$@fgetc_test:fgetc_test.c -$(CC) -o $@ $^-./$@-rm ./$@
注意我们知道在C语言当中,由于我使用的是UTF-8格式的文字输出,因此一个汉字会占3个字节(少数生僻字是4字节),而我们上面也提到了,读取的是一个字节,因此我们此时打印输出会显示三分之一的字节码,因此会显示乱码:
那么如何输出汉字呢?我们可以使用循环来实现,将fgetc文件进行修改,我们知道如果运行失败或者读到文件末尾都会打印EOF,我们进行判断如果不等于EOF那就移植打印,直到跳出循环:
char c = fgetc(ioFile);while(c != EOF){printf("%c",c);c = fgetc(ioFile);}printf("\n");
此时我们在运行一下看看:
3.7 读取字符串(fgets)
总结一下函数功能:
功能 | 从指定文件读取数据 | |
头文件 | #include <stdio.h> | |
原型 | char * fgets (char *__restrict __s, int __n, FILE *__restrict __stream) | |
参数 | char *__restrict __s | 自定义缓冲区指针,用来接收读取到的字符串 |
int __n | 自定义缓冲区大小,接收数据的长度 | |
FILE *__restrict __stream | 打开要读的文件 | |
返回值 | 成功 | 自定义缓冲区指针 |
失败 | NULL | |
备注 | 当返回值为NULL时,文件可能已达到末尾,或者错误 |
继续重新创建一个文件fgets_test.c,然后编写代码,由于函数读到文件末尾,或者失败就会返回NULL,那么我们可以使用while循环来,若是为NULL直接跳出循环:
char buffer[100];while (fgets(buffer,sizeof(buffer),ioFile)){printf("%s",buffer);}printf("\n");
完整代码:
#include<stdio.h>int main(int argc, char const *argv[])
{//打开文件FILE * ioFile = fopen("io.txt","r");if(ioFile == NULL){printf("failed 打开文件失败\n");}else{printf("success 打开文件成功\n");}char buffer[100];while (fgets(buffer,sizeof(buffer),ioFile)){printf("%s",buffer);}printf("\n");//关闭文件int result = fclose(ioFile);if(result == EOF){printf("关闭文件失败\n");}else if(result == 0){printf("关闭文件成功\n");}return 0;
}
将Makefile就行添加fgets_test.c相关:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_testfclose_test:fclose_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputc_test:fputc_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputs_test:fputs_test.c -$(CC) -o $@ $^-./$@-rm ./$@fprintf_test:fprintf_test.c -$(CC) -o $@ $^-./$@-rm ./$@fgetc_test:fgetc_test.c -$(CC) -o $@ $^-./$@-rm ./$@fgets_test:fgets_test.c -$(CC) -o $@ $^-./$@-rm ./$@
运行一下:
3.8 格式化读取(fscanf)
先了解一下功能:
功能 | 从指定文件读取格式化数据 | |
头文件 | #include <stdio.h> | |
原型 | int fscanf (FILE *__restrict __stream,const char *__restrict __format, ...) | |
参数 | FILE *__restrict __stream | 要读的文件 |
const char *__restrict __format | 带有格式化的字符串(固定格式接收) | |
... | 可变参数,填写格式化的字符串(接收数据提前声明的变量) | |
返回值 | 成功 | 成功匹配到的参数个数 |
失败 | NULL | |
备注 | 报错或者文件结束EOF |
开始前先创建一个user.txt文件:
然后创建一个fscanf_test.c文件用于编写代码,根据我们创建的txt文件,我们创建三个变量,将读取到的数据存储到这三个变量当中,进行打印:
char name[50];int age;char wife[50];int scanfR = fscanf(ioFile,"%s %d %s",name,&age,wife);if(scanfR != EOF){printf("成功匹配到的参数%d\n",scanfR);printf("%s在%d岁的时候爱上了%s\n",name,age,wife);}
来到Makefile进行代码更改:
CC:=gccfopen_test:fopen_test.c -$(CC) -o $@ $^-./$@-rm ./$@# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_testfclose_test:fclose_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputc_test:fputc_test.c -$(CC) -o $@ $^-./$@-rm ./$@fputs_test:fputs_test.c -$(CC) -o $@ $^-./$@-rm ./$@fprintf_test:fprintf_test.c -$(CC) -o $@ $^-./$@-rm ./$@fgetc_test:fgetc_test.c -$(CC) -o $@ $^-./$@-rm ./$@fgets_test:fgets_test.c -$(CC) -o $@ $^-./$@-rm ./$@fscanf_test:fscanf_test.c -$(CC) -o $@ $^-./$@-rm ./$@
运行一下看看,可以发现正常打印,不过只打印了一行:
我们通过while循环进行实现:
char name[50];int age;char wife[50];int scanfR;while((scanfR = fscanf(ioFile,"%s %d %s",name,&age,wife)) != EOF){printf("成功匹配到的参数%d\n",scanfR);printf("%s在%d岁的时候爱上了%s\n",name,age,wife);}
可以发现能够正常打印:
我们现在来看打印的最后两句,首先是但对于第四行因为是空的,所以打印数据的时候会跳过,没有匹配上数据,而张三匹配上一个数据,后面年龄和配偶没有,并且这里的数据是覆盖写入的,没有就不会写入,而张三是有数据的,因此就会显示张三(将梁山伯覆盖掉)在16岁爱上祝英台(上一次的数据,又有张三这里没有数据,因此未被覆盖掉):
4. 系统IO
系统调用是操作系统内核向用户空间应用程序提供的最小、最底层的接口。它是用户程序主动从用户态(User Space)切换到内核态(Kernel Space)的一种方式,目的是请求操作系统内核为其提供服务,例如操作硬件、管理进程、进行文件IO等。
4.1 打开文件(open)
了解一下功能:
功能 | 打开文件 | ||||
头文件 | #include<unistd.h> #include<fcntl.h> | ||||
原型 | int open (const char *__path, int __oflag, ...) | ||||
参数 | const char *__path | 打开文件的路径 | |||
int __oflag | 打开文件的模式 | O_RDONLY | 只读模式 | 三者互斥 | |
O_WRONLY | 只写模式 | ||||
O_RDWR | 读写模式 | ||||
O_CREAT | 如果文件不存在,则创建文件 | ||||
O_EXCL | 如果使用O_CREAT选项且文件存在,则返回错误消息 | ||||
O_NOCTTY | 当文件为中断时,阻止该终端成为进程的控制终端 | ||||
O_TRUNC | 如果文件已经存在,则删除文件中原有数据 | ||||
O_APPEND | 追加写模式 | ||||
... | 可变参数,在使用O_CREAT创建文件的权限的时候会用到(这里需要参考文件权限怎么计算rwx) 例如:0664 (八进制数据,0作为开头,rw-rw-r--) | ||||
返回值 | 成功 | 文件描述符 | |||
失败 | -1,同时设置全局变量error表示对应的错误 |
文件权限如何计算,如果不熟悉可以参考:
Linux命令进阶·如何修改文件和文件夹权限(chomd命令的使用)、如何修改用户和用户组(chown命令的使用)_linux更改文件所属用户和权限-CSDN博客
创建一个open_test.c文件,编写代码,读hello.txt文件,如果不存在则创建:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>int main(int argc, char const *argv[])
{int fd = open("hello.txt",O_RDONLY | O_CREAT,0664);if(fd == -1){printf("打开文件失败\n");}return 0;
}
来到Makefile文件添加代码:
open_test:open_test.c -$(CC) -o $@ $^-./$@-rm ./$@
运行一下看一下,可以看到成功创建:
看一下权限:
可以看到按照我们给的权限创建的,我们将hello.txt删除,更改一下权限:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>int main(int argc, char const *argv[])
{int fd = open("hello.txt",O_RDONLY | O_CREAT,0775);if(fd == -1){printf("打开文件失败\n");}return 0;
}
运行完我们发现权限发生更改了:
这里需要注意一点,Linux操作系统有文件权限保护,默认创建的文件会被删除掉其他用户的写权限。
拓展,了解一下:
对于上述解释我们就需要引入一个核心概念:umask(用户文件创建掩码)。
umask 是一个权限掩码,它像一个过滤器,用来“剥夺”或“屏蔽”掉新创建文件和目录的某些权限,以增强安全性。
实际权限的计算公式:
- 最终权限 = 默认完整权限 - umask权限
- 更准确的计算方式是进行位运算:最终权限 = 默认完整权限 & (~umask)
通常情况下,普通用户的默认 umask 通常是 0002,root 用户的默认 umask 通常是 0022。如果不知道可以通过umask命令查看:
例如,我们现在创建一个新文件 test.txt,文件默认权限: 666 (-rw-rw-rw-)
我们创建一个0666的权限的文件:
运行后可以发现权限被去掉写权限也就是 0666-0002=0664 的权限:
4.2 读取文件(read)
这里其实使用方面和上面标准IO大同小异,这里先解释一下功能,下面整体举一个例子:
功能 | 从指定文件读取数据 | |
头文件 | #include<unistd.h> #include<fcntl.h> | |
原型 | ssize_t read (int __fd, void *__buf, size_t __nbytes) | |
参数 | int __fd | 一个整数表示要从读取数据的文件描述符 |
void *__buf | 一个指向缓冲区,读取的数据将被存放到这个缓冲区中 | |
size_t __nbytes | 一个size_t类型的整数,表示要读取的最大字节数,系统调用将尝试读取最多这么多字节的数据,但是实际读取的字节数可能会少于请求的数量 | |
返回值 | 成功 | 返回实际读取的字节数,这个值可能小于nbytes,如果遇到文件结尾(EOF)或者因为网络读取等原因提前结束读取 |
失败 | -1 | |
备注 | 无 |
4.3 写入文件(write)
解释一下功能:
功能 | 从指定文件写入数据 | |
头文件 | #include<unistd.h> #include<fcntl.h> | |
原型 | ssize_t write (int __fd, const void *__buf, size_t __n) | |
参数 | int __fd | 一个整数,表示要写入数据的文件描述符 |
const void *__buf | 一个指向缓冲区,写入的数据需要先存放到这个缓冲区中 | |
size_t __n | 一个size_t类型的整数,表示要写入的字节数,函数会尝试写入_n个字节的数据,但是实际写入的字节数可能会少于请求的数据量 | |
返回值 | 成功 | 返回实际写入的字节数,这个值可能小于_n,如果写入操作因故提前结束,如:磁盘满、网络阻塞等情况 |
失败 | -1 | |
备注 | 无 |
4.4 关闭文件(close)
解释一下功能:
功能 | 关闭指定的文件 | |
头文件 | #include<unistd.h> #include<fcntl.h> | |
原型 | int close (int __fd); | |
参数 | int __fd | 即将要关闭的文件 |
返回值 | 成功 | 0 |
失败 | -1 | |
备注 | 无 |
4.5 组合使用
首先我们先创建一个.c文件,如system_test.c文件,然后引入头文件:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
然后创建主函数,先打开一个文件,这里我们打开之前创建的io.txt文件,通过if语句判断,我们知道如果成功打开文件返回文件描述符(这个下面会详细介绍),失败返回-1,文件打开失败我们通过exit退出:
int fd = open("io.txt", O_RDONLY);if(fd == -1){printf("打开文件失败\n");exit(EXIT_FAILURE);}
其中对于 exit() 函数和 _exit() 函数:
- exit():标准库函数,属于 ISO C 标准,调用通过 atexit() 注册的函数(按注册的相反顺序),刷新所有打开的 stdio 流缓冲区,关闭所有打开的 stdio 流,删除 tmpfile() 创建的临时文件。
- _exit():系统调用,属于 POSIX 标准,直接调用内核终止进程,立即终止进程,不执行任何清理操作,不刷新 stdio 缓冲区,不调用 atexit() 注册的函数。
/* We define these the same for all machines.Changes from this to the outside world should be done in `_exit'. */
#define EXIT_FAILURE 1 /* Failing exit status. */
#define EXIT_SUCCESS 0 /* Successful exit status. */
然后创建一个数组缓冲区用于存放读取到的数据,然后创建一个变量,用于接收读取到的字节数,通过while预计判断,当读取到的自己数大于0的时候,循环写入:
char buffer[1024];ssize_t bytes_read;while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0){// 使用实际读取的字节数,而不是缓冲区大小write(STDOUT_FILENO, buffer, bytes_read);}
检查时候成功并退出:
// 检查读取错误应该在循环结束后if(bytes_read == -1){printf("读取文件出错\n");close(fd);exit(EXIT_FAILURE);}close(fd);
完整函数:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>int main(int argc, char const *argv[])
{int fd = open("io.txt", O_RDONLY);if(fd == -1){printf("打开文件失败\n");exit(EXIT_FAILURE);}char buffer[1024];ssize_t bytes_read;while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0){// 使用实际读取的字节数,而不是缓冲区大小write(STDOUT_FILENO, buffer, bytes_read);}// 检查读取错误应该在循环结束后if(bytes_read == -1){printf("读取文件出错\n");close(fd);exit(EXIT_FAILURE);}close(fd);return 0;
}
然后我们来到 Makefile 添加编译:
system_test:system_test.c -$(CC) -o $@ $^-./$@-rm ./$@
运行一下:
5. 文件描述符
在Linux系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor,FD) ,其是Linux/Unix系统中用于访问输入/输出资源的抽象句柄,它是一个非负整数,在进程的文件描述符表中作为索引。
每个进程默认打开三个文件描述符:
FD | 名称 | 标准IO流 | 用途 | 默认设备 |
---|---|---|---|---|
0 | STDIN_FILENO | stdin | 标准输入 | 键盘 |
1 | STDOUT_FILENO | stdout | 标准输出 | 显示器 |
2 | STDERR_FILENO | stderr | 标准错误输出 | 显示器 |
进程级别的三层结构:
进程任务结构↓
文件描述符表 (File Descriptor Table)↓
文件表 (File Table) - 包含文件状态标志、当前偏移量等↓
inode表 (Inode Table) - 包含文件元数据、数据块指针等
找了几张图片来了解一下,首先文件描述符表在底层通过数组来实现的,文件描述符实际上是这个数组的偏移量,这个表是每个程序自己的:
我们上面也提到了每个进程默认打开三个文件描述符,而当我们调用函数执行代码的时候,默认是从3开始的,描述符表会增加一项:
int fd = open("io.txt", O_RDONLY);
这个file地址会指向内核里,内核里会存放一些文件描述(注意存的不是文件):
那么文件存放在那呢?存放在path下,某个挂载点(某个硬盘)下的某个目录,这里指向的是文件,中间以内核搭建桥梁:
我们现在找到了文件那么如何对文件的数据进行读写呢?那就需要找到inode(这是文件的唯一编号),对相关修改数据进行:
当我们执行open(等系统调用时,内核会创建一个新的structfile,这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将struct file维护在文件描述符表中,最后将文件描述符返回给应用程序。我们可以通过后者对文件执行它所支持的各种函数操作,而这些函数的函数指针都维护在structfile_operations数据结构中。文件描述符实质上是底层数据结构struct file的一个引用或者句柄,它为用户提供了操作底层文件的入口。
其他Linux应用开发相关都存放在下方链接当中(持续更新中······):
嵌入式Linux·应用开发_时光の尘的博客-CSDN博客