Linux文件IO——系统IO
目录
前言
一、Linux文件IO说明
1、文件的概念
2、Linux中文件的分类
3、系统IO与标准IO的说明
4、如何选择系统IO和文件IO
二、系统IO基本API
1.文件的打开
2.标准库函数的错误处理
3、文件描述符的本质
4、文件的读取、写入
5、文件的读写位置的设置
6、文件的关闭
三、系统IO 的其他API函数
1、内存映射函数
2、解除内存映射函数
3、控制驱动函数
前言
在 Linux 的世界里,一切皆文件。无论是普通的文本文件、目录,还是硬件设备、网络套接字,都可以通过一套统一而强大的接口进行访问和操作。这套接口的核心,便是我们常说的“文件 I/O”。
文件 I/O 主要分为两大阵营:一是由标准 C 库提供的标准 I/O(
stdio
),它提供了带缓冲的、更便于使用的流式操作;二是直接由 Linux 内核提供的系统 I/O(syscall
),它更底层、更直接,是理解 Linux 系统运作原理的基石。本系列文章将聚焦于后者——系统 I/O。我们将深入探讨
open
,read
,write
,close
,lseek
等核心系统调用的使用细节、内核实现机制以及其背后的设计哲学。理解这些内容,不仅能让你更好地进行文件操作,更能让你洞悉应用程序与操作系统内核交互的奥秘。
提示:以下是本篇文章正文内容,下面案例可供参考
一、Linux文件IO说明
1、文件的概念
狭义:
这是我们日常认知中的文件,即存储于磁盘或其他存储介质上的数据集合。它们是我们直接创建、编辑和保存的对象,例如:
文本文件(
.txt
,.c
,.py
等源代码)办公文档(
.docx
,.xlsx
)多媒体文件(
.jpg
,.mp4
,.mp3
)压缩包(
.zip
,.tar.gz
)可执行程序(
/bin/ls
)广义:
这是 Linux “一切皆文件”哲学的精髓所在。在这个层面上,几乎所有可以被读写操作的设备和资源都被抽象成了文件。系统通过一套统一的接口(即我们即将深入学习的系统 I/O 调用
open
,read
,write
,close
)来管理它们。这包括了:
硬件设备:键盘(
/dev/input/...
)、鼠标、硬盘(/dev/sda
)、显示器(/dev/tty
)、串口等。进程间通信(IPC)资源:管道(FIFO)、消息队列、共享内存等,这些是单机内进行通信的“文件”。
网络通信资源:套接字(Socket),这是多机之间进行网络通信所要操作的“文件”。
系统信息:甚至如 CPU 信息(
/proc/cpuinfo
)、内存占用(/proc/meminfo
)等也都以文件的形式暴露给用户。
2、Linux中文件的分类
普通文件(符号:-)(popular): 存在于外部存储器中,用于存储普通数据 --- 标准IO、文件属性与目录操作(*****) 目录文件(符号:d)(directory): 用于存放目录项,是文件系统管理的重要文件类型 管道文件(符号:p)(pipeline): 一种用于进程间通信的特殊文件,也被称为命名管道FIFO --- 系统编程 套接字文件(符号:s)(socket): 一种用于网络间通信的特殊文件 --- 网络编程 链接文件(符号:l)(link): 用于间接访问另外一个目标文件,相当于windows系统快捷方式 字符设备文件(符号:c)(character):字符设备在应用层的访问接口(以字符为单位,跟系统进行数据交换的设备,比如:键盘、鼠标、触摸屏等) --- 系统IO、设备操作(*****) 块设备文件(符号:b)(block): 块设备在应用层的访问接口 (以块为单位(256、512、1024字节为一块)),跟系统进行数据交换的设备,比如:U盘、内存、硬盘等
3、系统IO与标准IO的说明
说明:
对文件从操作,基本上就是输入输出,所有叫IO接口
- 在操作系统层面上:这一组专门针对文件的IO接口就被称为系统IO --- 偏向于底层(设备文件)
- 在标准库的层面上:这一组专门针对文件的IO接口就被称为标准IO --- 偏向于上层(软件程序文件)
图解:
系统IO:
标准IO:
解析:
系统IO:是众多系统调用中专用于文件操作的一部分接口;
标准IO:是众多标准函数中专用于文件操作的一部分接口;
所以:
标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口,如果把系统IO比喻为菜市场,提供各式肉蛋奶菜,那么标准IO就是对这些基本原理的进一步封装,是品类和功能更加丰富的酒庄饭店
4、如何选择系统IO和文件IO
系统IO:
由操作系统直接提供的函数接口,特点是简洁,功能单一;
没有提供缓冲区,对海量数据的操作效率较低
套接字socket、设备文件的访问只能使用系统IO
标准IO:
由标准C库提供的函数接口,特点是功能丰富提供缓冲区,对海量数据的操作效率高‘
编程开发尽量选择标准IO,但许多场合只能用系统IO
二、系统IO基本API
1.文件的打开
关键点:
- pen函数有两个版本,一个有两个参数,一个有三个参数
- 当打开一个已存在的文件时,指定两个参数即可(使用两个参数的那个open即可)
- 当创建一个新文件,需要用第三个参数指定新文件的权限,否则新文件的权限是随机值(系统会帮你配置)
- 模式flags,可以使用位或的方式,来同时指定多个模式
- 模式flags,O_NOCTTY主要用在后台精灵进程,阻止这些精灵进程拥有控制终端。 --- 系统编程的时候才讲的,现在不必看
linux创建的文件权限:
- 注意:umask值和open函数设置得文件权限值
- 第一个数:表示八进制的意思
- 第二个数:文件所有者的权限
- 第三个数:文件所属组的权限
- 第四个数:其它用户的权限
- 普通文件默认权限 == 0666权限-umask值
- 如何修改umask值?(umask 你要设置的值即可)
示例代码:
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h>int main(int argc, char const *argv[]) {int fd = 0; // 文件描述符// 一、以下三种打开方式,都要求文件已存在,否则失败返回 (知道文件存在)fd = open("1.txt", O_RDWR); // 以可读可写方式打开文件fd = open("1.txt", O_RDONLY); // 以只读方式打开文件fd = open("1.txt", O_WRONLY); // 以只写方式打开文件// 二、以下三种打开方式,如果文件不存在,则创建文件,并设置其权限为0644。 存在则返回错误信息 (你想要确保文件存在)fd = open("1.txt", O_RDWR|O_CREAT|O_EXCL, 0644); // 以可读可写方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息fd = open("1.txt", O_RDONLY|O_CREAT|O_EXCL, 0644); // 以只读方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息fd = open("1.txt", O_WRONLY|O_CREAT|O_EXCL, 0644); // 以只写方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息// 三、以下三种打开方式,如果文件不存在,则创建文件,并清空里面的信息,并设置其权限为0644 (你想要确保文件存在,并且里面是没有东西的)fd = open("1.txt", O_RDWR|O_CREAT|O_TRUNC, 0644); // 以可读可写方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息fd = open("1.txt", O_RDONLY|O_CREAT|O_TRUNC, 0644); // 以只读方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息fd = open("1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644); // 以只写方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息// 四、以下两种打开方式,都要求文件已存在,否则失败返回,并追加文件内容 (知道文件存在,并且想要追加数据到里面去) fd = open("1.txt", O_RDWR|O_APPEND); // 以可读可写方式打开文件, 并追加文件内容fd = open("1.txt", O_WRONLY|O_APPEND); // 以只写方式打开文件,并追加文件内容return 0; }
2.标准库函数的错误处理
说明:
在所有的库函数中,如果调用过程中出错了,那么该函数除了会返回一个特定的数据来告诉用户调用失效之外,还都会去修改一个大家共同的全局错误码errno,我们可以通过这个错误码,来进一步确认究竟是什么错误
关键点:
- 如果库函数,系统调用出错了,全局错误码errno会随之改变
- 如果库函数,系统调用没出错,全局错误码errno不会改变
- 一个库函数,系统调用出错后,若未及时处理错误码,则错误码可能会被随后的其它函数修改
#include <stdio.h> // open函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>// 全局错误码声明所在的文件 #include <errno.h> // strerror函数 #include <string.h> // 主函数 int main(int argc, char const *argv[]) {int fd = -1;// 方式一:使用perror()函数,直接输出用户信息和错误信息fd = open("./1.txt", O_RDWR);if ( -1 == fd ){perror("打开1.txt文件失败!\n");// return -1;}/*现象:打开1.txt文件失败! // 打印的信息(告诉你错了): No such file or directory // 告诉你为什么操作错了解析:标准输出:printf标准出错:perror相同点:都可以打印信息到屏幕上不同点:perror函数不能打印具体的变量的值,printf函数可以printf函数不能告诉用户,为什么出错了*/// 方式二:使用strerror()函数,返回错误信息交给用户自行处理fd = open("./1.txt", O_RDWR);if ( -1 == fd ){printf("打开1.txt失败: %s\n", strerror(errno));// return -2;}return 0; }
进一步说明:
一般而言,perror函数用起来更加方便,但有时候需要使用strerror来输出一些更加灵活的信息,比如以上代码,如果打开文件的名字不是固定a.txt,而是取决于外部参数(如:命令行参数), 那么此处可以写为
#include <stdio.h> // open函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>// 全局错误码声明所在的文件 #include <errno.h> // strerror函数 #include <string.h> // 主函数 int main(int argc, char const *argv[]) {// 1、判断命令行参数if (argc != 2)printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");// 2、打开文件int fd = open(argv[1], O_RDWR);if ( -1 == fd ){printf("错误信息:打开%s文件失败, 错误原因:%s, 错误位置:%d行\n", argv[1], strerror(errno), __LINE__);return -1;}return 0; }
3、文件描述符的本质
说明:
函数open()的返回值,是一个整型int数据,这个整型数据,实际上是内核中的一个称为fd_array的数组的下标
解释:
打开文件时,内核产生一个指向file{}的指针,并将该指针放入一个位于file_struct{}的数组fd_array[]中,而该指针所在的数组的下标,就被open()函数返回给用户,用户把这个数组下标称为文件描述符(fd)
结论:
- 文件描述符从0开始,每打开一个文件,就产生一个新的文件描述符
- 可以重复打开同一个文件,每次打开文件都会使用内核产生系列结构体,并得到不同的文件描述符
- 由于系统在每一个进程开始运行时,都默认打开了一次键盘、两次屏幕,因此0、1、2描述符分别代表标准输入、标准输出和标准出错
4、文件的读取、写入
关键点:
- 参数count是读写字节数的愿望值,实际读写成功的字节数由返回值决定
- 读取普通文件时,如果当读到了文件末尾,read()会返回0
- 读取管道文件时,如果管道中没有数据,read()会默认阻塞(相当于scanf) --- 系统编程的时候才学
#include <stdio.h> // open函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // read函数、close #include <unistd.h>// 全局错误码声明所在的文件 #include <errno.h> // strerror函数 #include <string.h> // 主函数 int main(int argc, char const *argv[]) {// 0、判断命令行参数if (argc != 2)printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");// 1、打开文件(以只读的方式打开)int fd = open(argv[1], O_RDONLY);if ( -1 == fd ){printf("错误信息:打开%s文件失败, 错误原因:%s, 错误位置:%d行\n", argv[1], strerror(errno), __LINE__);return -1;}// 2、读取文件里面的信息,并打印出来char buf[128] = {0};int ret = 0;while (1){// 清空buf里面的数据,防止对下一次的读取数据造成干扰bzero(buf, sizeof(buf));// 每次最多读取128个字节ret = read(fd, buf, sizeof(buf)); // 如果返回0,则证明文件已被读取完毕if (ret == 0){printf("啊,我读完了!\n");break;}// 打印读取的数据printf("%s", buf);}// 3、关闭文件close(fd);return 0; }
示例二:
#include <stdio.h> // open函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // write函数、close函数 #include <unistd.h>// 全局错误码声明所在的文件 #include <errno.h> // strerror函数 #include <string.h> int main(int argc, char const *argv[]) {// 0、判断命令行参数if (argc != 2)printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");// 1、打开文件(以只写的方式打开,如果没有这个文件就创建,并清空文件)int fd = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC, 0644);if (-1 == fd){printf("错误信息:打开%s文件失败, 错误原因:%s, 错误位置:%d行\n", argv[1], strerror(errno), __LINE__);return -1;}// 2、将数据写入char buf[1024] = {0};// 清空buf里面的数据,防止对下一次的写入数据造成干扰bzero(buf, sizeof(buf));// 从键盘输入数据printf("作者,请继续您的作品,读者催更中!:\n");scanf("%s", buf);while(getchar()!='\n');// 向文件写入数据write(fd, buf, strlen(buf));// 3、关闭文件close(fd);return 0; }
注意:
- 当我们对文件读写操作时,系统会为我们记录操作的位置,以便于下次继续进行读写操作的时候吗,从适当的地方开始
- 每当打开一个文件,系统就会维护一套包括文件操作位置在内的相关
- 对同一个文件描述符进行读写操作时,使用的同一套文件信息,影响的是同一个位置参数
- 对同一个文件的多个文件描述符进行读写操作时,使用的是不同的文件信息,影响的是不同的位置参数,彼此互相之间独立,这往往会导致文件信息的错乱
示例三:
#include <stdio.h> // open函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // read函数、 write函数、close函数 #include <unistd.h>// 全局错误码声明所在的文件 #include <errno.h> // strerror函数 #include <string.h> int main(int argc, char const *argv[]) {// 0、判断命令行参数if (argc != 2)printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");// 1、打开文件(以可读可写的方式打开)int fd = open(argv[1], O_RDWR);if ( -1 == fd ){printf("错误信息:打开%s文件失败, 错误原因:%s, 错误位置:%d行\n", argv[1], strerror(errno), __LINE__);return -1;}// 2、读取文件里面的信息,并打印出来char buf[128] = {0};int ret = 0;while (1){// 清空buf里面的数据,防止对下一次的读取数据造成干扰bzero(buf, sizeof(buf));// 每次最多读取128个字节ret = read(fd, buf, sizeof(buf)); // 如果返回0,则证明文件已被读取完毕if (ret == 0){printf("啊,我读完了!\n");break;}// 打印读取的数据printf("%s", buf);}// 2、将数据写入文件中// 清空buf里面的数据,防止对下一次的写入数据造成干扰bzero(buf, sizeof(buf));// 从键盘输入数据printf("作者,请继续您的作品,读者催更中!:\n");scanf("%[^\n]", buf); // 直到遇到'\n'才退出while(getchar()!='\n');// 向文件写入数据write(fd, buf, strlen(buf));// 4、关闭文件close(fd);return 0; }
5、文件的读写位置的设置
说明:
对文件进行常规的读写操作的时候,系统会自动调整读写位置,以便于让我们顺利地读写文件,但如果有需要,文件的读写位置是可以任意调整的
图解:
关键点:
- lseek函数可以将文件位置调整到任意的位置,可以是已有数据的地方,也可以是未有数据的地方,假设调整到文件末尾之后的某个地方,那么文件将会形成所谓的"空洞" --- 可以不掌握
- lseek函数只能对普通文件调整文件位置,不能对管道文件调整 --- 系统编程的时候讲的,可以不掌握
- lseek函数的返回值是调整后的文件位置距离文件开头的偏移量,单位是字节
示例代码:
#include <stdio.h> // open函数、lseek函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // lseek函数 #include <unistd.h>// 全局错误码声明所在的文件 #include <errno.h> // strerror函数 #include <string.h> // 主函数 int main(int argc, char const *argv[]) {// 0、判断命令行参数if (argc != 2)printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");// 1、打开文件(以可读可写的形式打开,如果没有这个文件就创建,并清空文件)int fd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0644);if (-1 == fd){printf("错误信息:打开%s文件失败, 错误原因:%s, 错误位置:%d行\n", argv[1], strerror(errno), __LINE__);return -1;}// 2、在空洞前,写入数据(头信息数据)char w_buf1[128] = "文件的大小+文件的名字+文件的格式";write(fd, w_buf1, strlen(w_buf1));// 3、利用lseek形成一个空洞文件lseek(fd, 10*1024*1024, SEEK_CUR);// 4、在空洞后,写入数据(尾信息数据)char w_buf2[128] = "文件的末尾";write(fd, w_buf2, strlen(w_buf2));// 5、关闭文件close(fd);return 0; }
示例二:获取文件的大小
#include <stdio.h> // open函数、lseek函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // lseek函数 #include <unistd.h>// 全局错误码声明所在的文件 #include <errno.h> // strerror函数 #include <string.h> /*** @brief: 获取文件的大小* @note: None* @param: pathname:要计算大小的文件路径* @retval: 成功:返回计算的文件的大小* 失败:返回-1 */ off_t GetFileSize(const char *pathname) {// 1、打开文件(以读写的权限打开文件)int fd = open(pathname, O_RDWR);if (-1 == fd){printf("错误信息:打开%s文件失败, 错误原因:%s, 错误位置:%d行\n", pathname, strerror(errno), __LINE__);return -1;}// 2、获取文件的大小off_t ret_len = lseek(fd, 0, SEEK_END); // 将文件位置偏移到文件末尾,并只移动0字节// 3、关闭文件close(fd);// 4、返回文件的大小return ret_len; }// 主函数 int main(int argc, char const *argv[]) {// 0、判断命令行参数if (argc != 2)printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");// 1、打印文件的大小printf("%s文件的大小为: %ld字节\n", argv[1], GetFileSize(argv[1]));return 0; }
6、文件的关闭
关键点:
- 当不再使用一个文件时,应当关闭文件,防止系统资源浪费
- 对同一文件重复执行关闭操作(或其它操作)会失败返回,不会由其它副作用
三、系统IO 的其他API函数
1、内存映射函数
说明:
该函数全称是memory map,意为内存映射,即将某个文件与某块内存关联起来,达到通过操作这块内存来进阶操作其所对应的文件的效果
关键点:
- mmap函数的flag是参数是有很多的,上表只罗列了最简单的几个,详细信息请使用man手册进行查询
- mmap函数理论上可以对任意文件进行映射的,但通常用来映射一些比较特殊的设备文件,比如液晶屏LCD
2、解除内存映射函数
使用命令:man 2 munmap查看接口函数
函数的作用: NAME(名字)munmap — unmap pages of memory// 翻译:解除内存页面映射函数的头文件:SYNOPSIS(简介)#include <sys/mman.h>函数的原型:int munmap(void *addr, size_t len);参数一:映射内存的地址(其实就是mmap映射函数返回的指针)参数二:映射内存的大小函数的使用: DESCRIPTION(说明,描述)The munmap() function shall remove any mappings for those entire pages containing any part of the address space ofthe process starting at addr and continuing for len bytes. Further references to these pages shall result in thegeneration of a SIGSEGV signal to the process. If there are no mappings in the specified address range, then mun‐map() has no effect.The implementation may require that addr be a multiple of the page size as returned by sysconf().If a mapping to be removed was private, any modifications made in this address range shall be discarded.Any memory locks (see mlock() and mlockall()) associated with this address range shall be removed, as if by an ap‐propriate call to munlock().If a mapping removed from a typed memory object causes the corresponding address range of the memory pool to be in‐accessible by any process in the system except through allocatable mappings (that is, mappings of typed memory ob‐jects opened with the POSIX_TYPED_MEM_MAP_ALLOCATABLE flag), then that range of the memory pool shall become de‐allocated and may become available to satisfy future typed memory allocation requests.A mapping removed from a typed memory object opened with the POSIX_TYPED_MEM_MAP_ALLOCATABLE flag shall not affectin any way the availability of that typed memory for allocation.The behavior of this function is unspecified if the mapping was not established by a call to mmap()./*翻译:munmap() 函数应移除从地址 addr 开始、持续 len 字节的进程地址空间内任何完整页面中的所有映射。后续对这些页面的访问将导致向进程生成 SIGSEGV 信号。若指定地址范围内不存在映射,则 munmap() 不产生任何效果。实现可能要求 addr 必须是 sysconf() 返回的页面大小的整数倍。若要移除的映射为私有映射,则在此地址范围内所做的所有修改将被丢弃。与此地址范围关联的所有内存锁(参见 mlock() 和 mlockall())将被移除,效果等同于调用了相应的 munlock()。若从类型化内存对象中移除映射导致内存池的对应地址范围无法被系统中任何进程访问(仅可通过可分配映射访问,即使用 POSIX_TYPED_MEM_MAP_ALLOCATABLE 标志打开的类型化内存对象映射),则该内存池范围应被释放,并可用于满足后续的类型化内存分配请求。从使用 POSIX_TYPED_MEM_MAP_ALLOCATABLE 标志打开的类型化内存对象中移除映射,不应以任何方式影响该类型化内存的分配可用性。若映射并非通过调用 mmap() 建立,则此函数的行为是未定义的。*/函数的返回值:RETURN VALUE(返回值)Upon successful completion, munmap() shall return 0; otherwise, it shall return -1 and set errno to indicate the
error./*翻译:若成功执行,munmap() 应返回 0;否则将返回 -1 并设置 errno 以指示错误。*/
3、控制驱动函数
说明:
该函数是沟通应用层和驱动层的有力武器,底层开发人员在为硬件设备编写驱动的时候,常常将某些操作封装为一个函数,并为这些接口提供一个所谓的命令字,应用层开发者可以通过 ioctl() 函数配合命令字,非常迅捷地绕过操作系统中间层层机构直达驱动层,调用对应的功能。从这个意义上讲,函数 ioctl() 像是一个通道,只提供函数调用路径,具体的功能由所谓命令字决定,下面是函数的接口规范说明:
关键点:
- request 就是所谓的命令字。
- 底层驱动开发者可以自定义命令字。
- 对于某些常见的硬件设备的常见功能,系统提供了规范的命令字。
示例代码:
int main(void) {// 打开一盏LED灯int led = open("/dev/Led", O_RDWR);// 通过命令字 LEDOP 及其携带的0/1参数,控制LED灯的亮灭// 此处,LEDOP 是底层开发者自定义的命令字ioctl(led, LEDOP, 1);ioctl(led, LEDOP, 0);// 打开一个摄像头int cam = open("/dev/video0", O_RDWR);// 通过命令字 VIDIC_STREAMON 及其携带参数 vtype 启动摄像头enum v4l2_buf_type vtype= V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(cam, VIDIOC_STREAMON, &vtype); }