Linux文件I/O编程技术剖析

目录

1、一切皆文件

1.1文件的概念

1.2各类文件

2.文件操作

2.1系统IO

2.1.1 打开文件 open

2.1.2 关闭文件 close

2.1.3 从文件中读取数据 open

2.1.4 向文件写入数据 write

2.1.5 设置偏移量 lseek

2.1.6 dup( )/dup2( )

2.1.7 fcntl( )和 ioctl( )

2.2 标准IO

2.2.1 打开文件 fopen

2.2.2 关闭文件 fclose

2.2.3 每次一个字符的读写文件

2.2.4 每次一行读写文件

2.2.5 每次读取若干数据块 fread

2.2.6 每次写入若干数据块

2.2.7 设置当前位置的偏移量

2.2.8 获取指定位置的当前偏移量

2.2.9 标准格式化IO函数

2.2.10 获取设备号


1、一切皆文件

1.1文件的概念

        在 Linux 中,有一句经典的口号:“一切皆文件”。这个说法是从内核的角度出发,强调了 Linux 操作系统内部的设计理念。在 Linux 内核中,几乎所有的设备(除了网络接口)都通过 Linux 独特的虚拟文件系统(VFS)进行统一管理。这意味着不论是磁盘、串口、打印机还是其他设备,都以文件的形式呈现,并受到文件系统的统一抽象。

        这种设计的最终目的是通过将各种不同类型的设备都抽象为“文件”这个通用概念,从而在应用层面上屏蔽底层设备的差异性,简化了应用程序的编程难度。通过使用相同的文件 I/O 操作,开发人员可以以一致的方式与各种设备进行交互,而不必关心底层硬件的具体细节。

        文件和进程是 Linux 系统中最为重要的两个抽象概念。文件不仅包括常见的文本文件,还包括设备文件,使得硬件设备也可以通过文件接口进行访问。进程则代表运行中的程序,通过文件描述符等方式与文件进行类似的操作。这种一切皆文件的思想在 Linux 操作系统中发挥了重要作用,为系统的一致性和可扩展性提供了基础。

1.2各类文件

在 Linux 中,文件总共被分成了 7 种,他们分别是:

(1)普通文件 (regular) :存在于外部存储器中,用于存储普通数据。

(2)目录文件 (directory) :用于存放目录项,是文件系统管理的重要文件类型。

(3)管道文件 (pipe) :一种用于进程间通信的特殊文件,也称为命名管道 FIFO。

(4)套接字文件 (socket) :一种用于网络间通信的特殊文件。

(5)链接文件 (link) :用于间接访问另外一个目标文件,相当于 Windows 快捷方式。

(6)字符设备文件 (character) :字符设备在应用层的访问接口。

(7)块设备文件 (block) :块设备在应用层的访问接口。

2.文件操作

        对一个文件的操作有两种不同的方式,既可以使用由操作系统直接提供的编程接口 (API),即系统调用,也可以使用由标准 C 库提供的标准 IO 函数,他们的关系如图

2.1系统IO

        要对一个文件进行操作就必须打开它,代码中打开一个文 件意味着获得了这个文件的访问句柄 (即 file descriptor,文件描述符 fd) ,同时规定了之 后访问这个文件的限制条件。

以下是一些常用的系统IO

2.1.1 打开文件 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);

参数

pathname:即将要打开的文件

flags

O_RDONLY:只读方式打开文件

这三个参数互斥

O_WRONLY:只写方式打开文件

O_RDWR:读写方式打开文件

O_CREAT:如果文件不存在,则创建该文件。

O_EXCL:如果使用 O_CREAT 选项且文件存在,则返回错误消息。

O_NOCTTY:如果文件为终端,那么终端不可以作为调用 open()系统调 用的那个进程的控制终端。

O_TRUNC:如文件已经存在,则删除文件中原有数据。

O_APPEND:以追加方式打开文件。

mode

如果文件被新建,指定其权限为 mode  (八进制表示法)

返回值

成功

大于等于 0 的整数 (即文件描述符)

失败

- 1

备注

2.1.2 关闭文件 close

功能

关闭文件并释放相应资源

头文件

#include <unistd.h>

原型

int close(int fd);

参数

fd:即将要关闭的文件的描述符

返回值

成功

0

失败

- 1

备注

重复关闭一个已经关闭了的文件或者尚未打开的文件是安全的。

2.1.3 从文件中读取数据 read

功能

从指定文件中读取数据

头文件

#include <unistd.h>

原型

ssize_t read(int fd, void *buf, size_t count);

参数

fd:从文件fd 中读数据

buf:指向存放读到的数据的缓冲区

count:想要从文件 fd 中读取的字节数

返回值

成功

实际读到的字节数

失败

- 1

备注

实际读到的字节数小于等于 count

2.1.4 向文件写入数据 write

功能

将数据写入指定的文件

头文件

#include <unistd h>

原型

ssize_t write(int fd, const void *buf, size_t count);

参数

fd:将数据写入到文件 fd 中

buf:指向即将要写入的数据

count:要写入的字节数

返回值

成功

实际写入的字节数

失败

- 1

备注

实际写入的字节数小于等于 count

这两个函数都非常容易理解,需要特别注意的是:

(1)实际的读写字节数要通过返回值来判断,参数 count 只是一个“愿望值”。

(2)当实际的读写字节数小于 count 时,有以下几种情形:

        A) 读操作时,文件剩余可读字节数不足count

        B) 读写操作期间,进程收到异步信号。

(3)读写操作同时对 f_pos 起作用。也就是说,不管是读还是写,文件的位置偏移量 (即 内核中的 f_pos) 都会加上实际读写的字节数,不断地往后偏移。

2.1.5 设置偏移量 lseek

lseek( )只对普通文件凑效,特殊文件是无法调整偏移量的。

功能

调整文件位置偏移量

头文件

#include <sys/types.h>

#include <unistd.h>

原型

off_t lseek(int fd, off_t offset, int whence);

参数

fd:要调整位置偏移量的文件的描述符

offset:新位置偏移量相对基准点的偏移

whence:基准点

SEEK_SET:文件开头处

SEEK_CUR:当前位置

SEEK_END:文件末尾处

返回值

成功

新文件位置偏移量

失败

- 1

备注

2.1.6 dup( )/dup2( )

功能

复制文件描述符

头文件

#include <unistd.h>

原型

int dup(int oldfd);

int dup2(int oldfd, int newfd);

参数

oldfd:要复制的文件描述符

newfd:指定的新文件描述符

返回值

成功

新的文件描述符

失败

- 1

备注

2.1.7 fcntl( )和 ioctl( )

在编程开发的时候,除非情不得已,否则尽量使用fcntl( ) 。一般将除了读和写这样的常规动作之外的其 他文件操作塞到其中去实现

功能

文件控制

头文件

#include <sys/ioctl.h>

原型

int ioctl(int d, int request, ...);

参数

d:要控制的文件描述符

request:针对不同文件的各种控制命令字

变参:根据不同的命令字而不同

返回值

成功

一般情况下是 0 ,但有些特定的请求将返回非负整数。

失败

- 1

备注

功能

文件控制

头文件

#include <unistd.h>

#include <fcntl.h>

原型

int fcntl(int fd, int cmd, .../* arg */ );

参数

fd:要控制的文件描述符

cmd:控制命令字

变参:根据不同的命令字而不同

返回值

成功

根据不同的 cmd ,返回值不同

失败

- 1

备注

2.1.8 内存映射 mmap

        该函数在进程的虚拟内存空间中 映射出一块内存区域,用以对应指定的一个文件,该内存区域上的数据跟对应的文件的数据 是一一对应的,并在一开始的时候用文件的内容来初始化这片内存。

功能

内存映射

头文件

#include <sys/mman.h>

原型

void *mmap(void *addr, size_t length,

int prot, int flags, int fd, off_t offset);

参数

addr:

映射内存的起始地址。

如果该参数为 NULL,  则系统将会自动寻找一个合适的起始地址,一 般都使用这个值。

如果该参数不为 NULL,则系统会以此为依据来找到一个合适的起始地 址。在 Linux 中,映射后的内存起始地址必须是页地址的整数倍。

length:

映射内存大小。

prot:

映射内存的保护权限。

PROT_EXEC : 可执行。

PROT_READ : 可读。

PROT_WRITE : 可写。

PROT_NONE: 不可访问。

flags:

当有多个进程同时映射了这块内存时,该参数可以决定在某一个进程 内使映射内存的数据发生变更是否影响其他进程,也可以决定是否影 响其对应的文件数据。

以下两个选项互斥:

MAP_SHARED : 所有的同时映射了这块内存的进程对数据的变更均 可见,而且数据的变更会直接同步到对应的文件 (有时可能还需要调 用 msync( )或者 munmap( )才会真正起作用) 。

MAP_PRIVATE : 与 MAP_SHARED 相反,映射了这块内存的进程对数 据的变更对别的进程不可见,也不会影响其对应的文件数据。

以下选项可以位或累加:

MAP_32BIT :在早期的 64 位 x86 处理器上,设置这个选项可以将线程 的栈空间设置在最低的 2GB 空间附近,以便于上下文切换时得到更好 的表现性能,但现代的64 位 x86 处理器本身已经解决了这个问题, 因此这个选项已经被弃用了。

MAP_ANON :等同于 MAP_ANONYMOUS,已弃用。                MAP_ANONYMOUS :匿名映射。该选项使得该映射内存不与任何文件 关联,一般来讲参数 fd和 offset 会被忽略 (但是可移植性程序需要将 fd 设置为-1) 。另外,这个选项必须跟 MAP_SHARED 一起使用。

2.2 标准IO

        系统 IO 的最大特点一个是更具通用性,不管是普通文件、管道文件、设备节点文件、 套接字文件等等都可以使用,另一个是他的简约性,对文件内数据的读写在任何情况下都是 不带任何格式的,而且数据的读写也都没有经过任何缓冲处理,这样做的理由是尽量精简内 核 API,而更加丰富的功能应该交给第三方库去进一步完善。

        标准 C 库是最常用的第三方库,而标准 IO 就是标准 C 库中的一部分接口,这一部分接 口实际上是系统 IO 的封装,他提供了更加丰富的读写方式,比如可以按格式读写、按 ASCII 码字符读写、按二进制读写、按行读写、按数据块读写等等,还可以提供数据读写缓冲功能, 极大提高程序读写效率。

        在 2.1 中,所有的系统 IO 函数都是围绕所谓的“文件描述符”进行的,这个文件描 述符由函数 open( )获取,而在这一节中,所有的标准 IO 都是围绕所谓的“文件指针”进 行的,这个文件指针则是由 fopen( )获取的,他是第一个需要掌握的标准 IO 函数

2.2.1 打开文件 fopen

        使用标准 IO 函数处理文件的最大特点是,数据将会先存储在一个标准 IO 缓冲区中,而后在一定条件下才被一并 flush (冲洗,或称刷新) 至内核缓冲区,而不是像 系统 IO 那样,数据直接被flush 至内核。

        注意到,标准 IO 函数 fopen( )实质上是系统 IO 函数 open( )的封装,他们是一一对应 的,每一次 fopen( )都会导致系统分配一个 file{ }结构体和一个 FILE{}来保存维护该文件的 读写信息,每一次的打开和操作都可以不一样,是相对独立的,因此可以在多线程或者多进 程中多次打开同一个文件,再利用文件空洞技术进行多点读写。

功能

获取指定文件的文件指针

头文件

#include <stdio.h>

原型

FILE *fopen(const char *path, const char *mode);

参数

path:即将要打开的文件

2.2.2 关闭文件 fclose

功能

关闭指定的文件并释放其资源

头文件

#include <stdio.h>

原型

int fclose(FILE *fp);

参数

fp:即将要关闭的文件

返回值

成功

0

失败

EOF

备注

2.2.3 每次一个字符的读写文件

功能

获取指定文件的一个字符

头文件

#include <stdio.h>

原型

int fgetc(FILE *stream);

int getc(FILE *stream);

int getchar(void);

参数

stream:文件指针

返回值

成功

读取到的字符

失败

EOF

备注

当返回 EOF 时,文件 stream 可能已达末尾,或者遇到错误

功能

讲一个字符写入一个指定的文件

头文件

#include <stdio.h>

原型

int fputc(int c, FILE *stream);

int putc(int c, FILE *stream);

int putchar(int c);

参数

c:要写入的字符

stream:写入的文件指针

返回值

成功

写入到的字符

失败

EOF

备注

2.2.4 每次一行读写文件

功能

从指定文件读取最多一行数据

头文件

#include <sys/ioctl.h>

原型

char *fgets(char *s, int size, FILE *stream);

char *gets(char *s);

参数

s: 自定义缓冲区指针

size: 自定义缓冲区大小

stream:即将被读取数据的文件指针

返回值

成功

自定义缓冲区指针 s

失败

NULL

备注

1,gets( )缺省从文件 stdin 读入数据

2,当返回 NULL 时,文件 stream 可能已达末尾,或者遇到错误

功能

将数据写入指定的文件

头文件

#include <sys/ioctl.h>

原型

int fputs(const char *s, FILE *stream);

int puts(const char *s);

参数

s: 自定义缓冲区指针

stream:即将被写入数据的文件指针

返回值

成功

非负整数

失败

EOF

备注

puts( )缺省将数据写入文件 stdout

值得注意的有以下几点:

1 ,fgets( )跟 fgetc( )一样,当其返回 NULL 时并不能确定究竟是达到文件末尾还是碰 到错误,需要用 feof( )/ferror( )来进一步判断。

2,fgets( )每次读取至多不超过 size 个字节的一行,所谓“一行”即数据至多包含一个 换行符’
’。

3,gets( )是一个已经过时的接口,因为他没有指定自定义缓冲区s 的大小,这样很容 易造成缓冲区溢出,导致程序段访问错误。

4,fgets( )和 fputs( ),gets( )和 puts( )一般成对使用,鉴于 gets( )的不安全性,一般 建议使用前者。

2.2.5 每次读取若干数据块 fread

功能

从指定文件读取若干个数据块

头文件

#include <sys/ioctl.h>

原型

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

参数

ptr: 自定义缓冲区指针

size:数据块大小

nmemb:数据块个数

stream:即将被读取数据的文件指针

返回值

成功

读取的数据块个数,等于 nmemb

失败

读取的数据块个数,小于 nmemb 或等于 0

备注

当返回小与nmemb 时,文件 stream 可能已达末尾,或者遇到错误

2.2.6 每次写入若干数据块

功能

将若干块数据写入指定的文件

头文件

#include <sys/ioctl.h>

原型

size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);

参数

ptr: 自定义缓冲区指针

size:数据块大小

nmemb:数据块个数

stream:即将被写入数据的文件指针

返回值

成功

写入的数据块个数,等于 sinmembze

失败

写入的数据块个数,小于 nmemb 或等于 0

备注

2.2.7 设置当前位置的偏移量

功能

设置指定文件的当前位置偏移量

头文件

#include <sys/ioctl.h>

原型

int fseek(FILE *stream, long offset, int whence);

参数

stream:需要设置位置偏移量的文件指针

offset:新位置偏移量相对基准点的偏移

whence:基准点

SEEK_SET:文件开头处

SEEK_CUR:当前位置

SEEK_END:文件末尾处

返回值

成功

0

失败

- 1

备注

2.2.8 获取指定位置的当前偏移量

功能

获取指定文件的当前位置偏移量

头文件

#include <sys/ioctl.h>

原型

long ftell(FILE *stream);

参数

stream:需要返回当前文件位置偏移量的文件指针

返回值

成功

当前文件位置偏移量

失败

- 1

备注

2.2.9 标准格式化IO函数

功能

将格式化数据写入指定的文件或者内存

头文件

#include <stdio.h>

原型

int fprintf(FILE *restrict stream, const char *restrict format, ...);

int printf(const char *restrict format, ...);

int snprintf(char *restrict s, size_t n,const char *restrict format, ...); int sprintf(char *restrict s, const char *restrict format, ...);

参数

stream:写入数据的文件指针

format:格式控制串

s:写入数据的自定义缓冲区

n: 自定义缓冲区的大小

返回值

成功

成功写入的字节数

失败

- 1

备注

功能

从指定的文件或者内存中读取格式化数据

头文件

#include <stdio.h>

原型

int fscanf(FILE *restrict stream, const char *restrict format, ... ); int scanf(const char *restrict format, ... );

int sscanf(const char *restrict s, const char *restrict format, ... );

参数

stream:读出数据的文件指针

format:格式控制串

s:读出数据的自定义缓冲区

返回值

成功

正确匹配且赋值的数据个数

失败

EOF

备注

2.2.10 获取设备号

        设备号在编写设备文件的驱动程序中才需要用到,在应用编程中不需要关注。st_dev 和 st_rdev 里面都包含了主次设备号,需要用到下面的函数来获取:

头文件

#include <sys/types.h>

原型

int major(dev_t dev);

int minor(dev_t dev);

参数

dev:文件的设备号属性,来自 stat 结构体中的 st_dev 或者 st_rdev

返回值

成功

major 返回主设备号,minor 返回次设备号

失败

备注