一、Linux 的文件 I/O 概述
Linux 把大部分系统资源当作文件呈现给用户,用户只需按照文件 I/O 的方式,就能完成数据的输入输出。Linux 文件,按其代表的具体对象,可分类为:
1.普通文件,即一般意义上的文件、磁盘文件;
2.设备文件,代表的是系统中一个具体的设备;
3.管道文件、FIFO 文件,一种特殊文件,常用于进程间通信;
4.套接字(socket)文件,主要用在网络通信方面。
文件 I/O 常用的方法有“打开”、“关闭”、“读”和“写”等操作。只要是文件,都可以用这套方法操作。系统提供了文件 I/O 的应用程序接口(API,Application Interface),以函数的形式提供给应用程序调用。打开文件对应的函数是 open(),读文件对应的函数是 read(),写文件对应的函数是 write(),关闭文件对应的函数是 close(),这些文件 I/O 常用函数将在下面进行介绍。
Linux 系统提供的文件 I/O 接口函数,是以最基本的系统服务形式提供的,又称它们为基本 I/O 函数。这些函数有个共同的特点,它们都通过文件描述符(file descriptor)来完成对指定文件的 I/O 操作。
二、文件描述符
文件描述符 fd(file descriptor)是进程中代表某个文件的整数,有的文献资料中又称它为文件句柄(file handle)。
有效的文件描述符取值范围从 0 开始,直到系统定义的某个界限值。这些指定范围的整数,实际上是进程文件描述符表的索引。文件描述符表是进程用来保存它所打开的文件信息的、由操作系统维护的一个登记表,用户程序不能直接访问该表。文件描述符的取值范围,反映了文件描述符表的大小,表示这个进程最多可以同时打开多少个文件。在大多数 Linux系统中,可通过命令“ulimit -n”查询到这个数值的大小。
对于内核而言,进程所打开的文件都由文件描述符引用。当进程打开一个现存文件或创建一个新文件时,内核返回一个文件描述符给进程。当读、写一个文件时,先调用 open()或 creat()函数取得文件描述符 fd 代表该文件,将 fd 作为参数传送给 read()或 write()等函数操作文件。通常情况下,文件描述符 0、1、2 在进程启动时已被占用,代表进程在启动过程中打开的文件。文件描述符 0、1、2 在桌面系统与嵌入式系统上,通常代表的文件如下图 所示:
三、常用文件 I/O 操作和函数
在 C 语言下进行文件 I/O 编程,一般要包含下面这些头文件,这些头文件定义了文件 I/O 用到的数据类型、函数原型及其它要用到的符号常量。
#include<sys/types.h> /* 定义数据类型,如 ssize_t,off_t 等 */
#include <fcntl.h> /* 定义 open,creat 等函数原型,创建文件权限的符号常量 S_IRUSR 等 */
#include <unistd.h> /* 定义 read,write,close,lseek 等函数原型 */
#include <errno.h> /* 与全局变量 errno 相关的定义 */
#include <sys/ioctl.h> /* 定义 ioctl 函数原型 */
1.open
进行文件 I/O 操作时,要先打开对应的文件,可调用 open()函数打开文件,它返回的文件描述符fd代表打开的文件,后续操作通过引用文件描述符fd来表示对该文件的操作,open()函数原型在<fcntl.h>文件中定义:
int open(const char *pathname, int flags, ... /* mode_t mode */);
操作成功,返回值为文件描述符 fd,否则返回-1,同时设置全局变量 errno 报告具体错误的原因。参数 flags,决定着打开的文件可以进行什么样的操作,多个标志可用运算符“|”合并在一起。
只有创建新文件时,open()的最后一个参数 mode 才会起作用,否则将忽略它。
open()的参数 flags,当设置了 O_CREAT 标志时,可以创建一个新文件,也可用另外一个函数 creat()创建新文件,creat()函数原型在<fcntl.h>文件中定义:
int creat(const char *pathname, mode_t mode);
creat()的参数 pathname 和 mode 的含义,与 open()的同名参数含义相同,某些条件下调用 creat()的效果与 open()是相同的。
open()或者 creat()都能创建新文件,它们对待已有文件的细节不同,在编程时需要注意:
①creat()创建文件时, 如果文件已存在,则会把已存在的文件内容清空、长度截为 0,然后返回对应的文件描述符;如果文件不存在,则直接创建,然后返回创建文件的描述符;
②当 open()的参数 flags 设置了 O_CREAT 时,如果文件已存在,则直接打开并返回文件描述符; 如果文件不存在,则创建新文件,然后返回对应的文件描述符。
2.close
文件 I/O 操作完成后,应该调用 close()关闭打开的文件,释放打开文件时所占用的系统资源。close()函数原型在<unistd.h>文件中定义:
int close(int fd);
如果文件顺利关闭,返回 0,否则返回-1,同时设置全局变量 errno 报告具体错误的原因。参数 fd 是打开文件时调用 open()或 creat()函数返回的文件描述符。
当一个文件被打开多次时,比如被多个进程同时打开,或在同一个进程中被打开多次,每打开一次,该文件内部的引用计数就增加 1,对该文件每调用一次 close(),文件引用计数则减 1,当计数值减到 0 时,内核才关闭该文件。当进程终止时,内核会回收进程资源,也按上述规则关闭进程打开的全部文件。
3.read
从打开的文件读取数据,可调用 read()函数实现。read()函数原型在<unistd.h>中定义:
ssize_t read(int fd, void *buf, size_t count);
操作成功,返回实际读取的字节数,如果已到达文件结尾,返回 0,否则返回-1 表示出错,同时设置全局变量 errno 报告具体错误的原因。
实际读取的字节数,可以小于请求的字节数 count,比如下面两种情况:
①文件长度小于请求的长度,即还没达到请求的字节数时,就已到达文件结尾。如果文件是 50 字节长,而 read 请求读 100 字节(count=100),则首次调用 read 时,它返回 50,紧接着的下次调用,它返回 0,表示已到达文件结尾;
②读设备文件时,有些设备每次返回的数据长度小于请求的字节数,如终端设备一般
按行返回,即每读到一行数据,就返回。
参数 fd 是调用 open()或者 creat()时返回的文件描述符,buf 是用来接收所读数据的缓冲区,count 是请求读取的字节数。
ssize_t 和 size_t 是系统头文件中定义的数据类型,ssize_t 表示 signed int,size_t 表示unsigned int,是一个与 CPU 位数有关的整型值,在 32 位系统中,它表示 32 位整型值 int,在 64 位系统中,表示 64 位整型值 long int。其定义等效于:
/* 在 32 位系统中 */
typedef int ssize_t; /* 32 位有符号整型值 */
typedef unsigned int size_t; /* 32 位无符号整型值*/
/* 在 64 位系统中 */
typedef long int ssize_t; /* 64 位有符号整型值 */
typedef unsigned long int size_t; /* 64 位无符号整型值 */
4.write
把数据写入文件,可调用 write()函数实现,write()的函数原型在<unistd.h>中定义:
ssize_t write(int fd, const void *buf, size_t count);
操作成功,返回实际写入的字节数,出错则返回-1,同时设置全局变量 errno 报告具体错误的原因,比如 errno=ENOSPC 表示磁盘满了。参数 fd 是打开文件的描述符,buf 是数据缓冲区,存放着准备写入文件的数据,count是请求写入的字节数。实际写入的字节数可以小于请求写的字节数。
5.fsync
write()函数一旦返回,表明所写的数据已提交到系统内部缓存了,但此时数据不一定写入了磁盘等持久存储设备中。要确保已修改过的数据全部写入持久设备中,正确的做法是调用 fsync 函数进行文件数据同步,强制把已修改过的文件数据写入持久存储设备中。
嵌入式系统通常采用闪存(Flash Memory)作系统盘,write()返回后也应该用 fsync()及时把修改过的文件数据写入闪存中。如果不调用 fsync(),在 write()返回后马上就复位或重新上电,则所作的修改就可能没有更新,造成文件数据丢失。
fsync()函数的功能是进行文件数据同步,强制把已修改过的文件数据存入持久存储设备中。其原型在<unistd.h>中定义如下:
int fsync(int fd);
fsync()针对打开的文件,参数 fd 是已打开文件的描述符,fsync()调用直到文件已修改过的数据全部写入磁盘后才返回。操作成功返回 0,否则返回-1,同时设置全局变量 errno 报告具体错误的原因。
另外一个函数 sync()的功能与 fsync()类似,也是进行数据同步,但它是针对整个系统的。sync()直到系统中的修改过的缓存数据都写入磁盘才返回。操作成功返回 0,否则返回-1。当系统修改过的缓存数据量很大时,或者有程序正在往磁盘写数据时,sync()要很久才返回。
进行文件 I/O 时,建议使用 fsync(),尽量不用 sync()。
6.lseek
Linux 文件,按读写数据的方式,可分为顺序读写和随机读写文件。普通磁盘文件一般都能随机读写,这类文件可通过 lseek()函数改变文件读写位置。而顺序读写文件只能指从头到尾,按顺序进行读写,如管道(pipe)文件、套接字(socket)文件或 FIFO,都是按顺序读写的,不支持 lseek 操作,不像普通磁盘文件那样可以随机读写。设备文件是否支持 lseek操作,则不确定,与具体的设备有关。
lseek() 函数
lseek()函数不会读写任何文件数据,只是仅仅改变文件的起始读写位置,后续读或写文件,将从 lseek()设置的新位置开始。lseek()函数原型在<unistd.h>中定义:
off_t lseek(int fd, off_t offset, int whence);
如果操作成功,lseek()返回新的读写位置;否则返回-1 表示操作不成功,同时设置全局
变量 errno 报告具体错误的原因。返回值的数据类型 off_t 就是 long int,在 32 位系统是 32 位有符号整数,在 64 位系统
是 64 位有符号整数。
lseek()返回的读写位置,是从文件开头算起的偏移字节数,这个偏移量为非负数,可以是 0。
lseek()的参数 fd 是打开文件的描述符,offset 是目标位置,其偏移的参照点,由第三个参数 whence 决定,whence 有效值是 SEEK_SET、SEEK_CUR、SEEK_END,含义如下:
①SEEK_SET 设置新的读写位置为从文件开头算起,偏移 offset 字节;
②SEEK_CUR 设置新的读写位置为从当前所在的位置算起,偏移 offset 字节,正值表示往文件尾部偏移,负值表示往文件头部偏移;
③SEEK_END 设置新的读写位置为从文件结尾算起,偏移 offset 字节,正值表示往文件尾部偏移,负值表示往文件头部偏移。
前面已讲过,按顺序读写的文件不支持 lseek 操作,对这类文件调用 lseek(),将返回-1,且 errno=ESPIPE。对设备文件,是否支持 lseek 操作,则不确定。可用下面方法测试一个文件是否支持 lseek 操作,如果返回-1,则说明该该文件不支持lseek 操作:
new_offset = lseek(fd, 0, SEEK_CUR);
7.ioctl
文件 I/O 操作还有很多不好归到 read()/write()的,只好放到这个函数中。很多设备文件也通过 ioctl()函数提供设备特有的操作,比如修改设备寄存器的值。ioctl()是文件 I/O 的杂项函数,其函数原型在<sys/ioctl.h>中定义:
int ioctl(int fd, int cmd, …);
一般情况下,操作成功,返回 0,失败返回-1,由 errno 报告具体错误原因。但有的设备文件可能会返回一个正值表示输出参数,其含义取决于具体的设备文件。
参数 fd 是打开文件的描述符,参数 cmd 是文件的操作命令,这个参数的取值还决定后面的参数含义,“„”表示从参数是可选的、类型不确定的。
ioctl()的 cmd 操作命令是文件专有的,不同的文件,cmd 往往是不同的,没有共用性,比如嵌入式系统中的设备文件,蜂鸣器(BUZZER)和模数转换(ADC),它们所支持的 ioctl()操作命令就不同。
参考书籍:《嵌入式Linux开发教程》(周立功等编著)