第四章:文件I/O:通用的I/O模型
在该章节中讨论的文件描述符的概念。其中包括:打开文件,关闭文件,从文件中读取数据和向文件中写数据。
- 概述
所有执行I/O操作的系统调用都以文件描述符(一个非负整数(通常是比较小的整数))来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。
下面介绍执行I/O操作的4个主要系统调用: - open:fd=open(pathname,flags,mode)函数打开pathname所标识的文件,并返回文件描述符,用以在后续函数调用中指代打开的文件。如果文件不存在,open()函数可以创建之,这取决于对位掩码参数flags的设置。flags参数还可以指定文件的打开方式:只读、只写亦或读写方式。mode参数则指定了由open()调用创建文件的访问权限,如果open()函数没有创建文件,则可以忽略或省略mode参数。
- read :numread=read(fd,buffer,count)调用从fd所指代的打开文件中读取至多count字节的数据,并存储到buffer中。read()调用的返回值为实际读取到的字节数。如果再无字节刻度,则返回值为。
- write:numwritten=write(fd,buffer,count)调用从buffer中读取多达count字节的数据写入由fd所指代的已打开文件中。write()调用的返回值为实际写入文件中的字节数,且有可能小于count。
- close:status=close(fd)在所有输入/输出操作完成后,调用close(),释放文件描述符fd与之相关的内核资源。
实现一个简版的cp(1)命令:
#include<sys/stat.h>
#include<fcntl.h>
#include "tlpi_hdr.h"
#include "error_functions.h"//用来输入错误信息
#ifndef BUF_SIZE
#define BUF_SIZE 1024
#endif
int main(int argc, char const *argv[])
{
int inputFd,outputFd,openFlags;//用来存储I/O调用的返回值
mode_t filePerms; //整型,用来表示文件权限及类型
ssize_t numRead; //有符号整型,当字节数(为负时)表示错误
char buf[BUF_SIZE];
//strcmp():比较两个字符串设这两个字符串为str1,str2,若str1==str2,则返回零
if(argc!=3||strcmp(argv[1],"--help")==0)
usageErr("%s old-file new-file\n",argv[0]);
inputFd=open(argv[1],O_RDONLY);
if(inputFd==-1)
errExit("opening file %s",argv[1]);
openFlags=O_CREAT | O_WRONLY | O_TRUNC; //open()函数的参数,后面会介绍
filePerms=S_IRUSR | S_IWUSR |S_IWGRP | S_IROTH |S_IWOTH;
//文件类型参数,后面介绍
outputFd=open(argv[2],openFlags,filePerms);
if(outputFd==-1)
errExit("opening file %s",argv[2]);
while((numRead=read(inputFd,buf,BUF_SIZE))>0)
if(write(outputFd,buf,numRead)!=numRead)
fatal("couldn't write whole buffer");
if(numRead==-1)
errExit("read");
if(close(inputFd)==-1)
errExit("close input");
if(close(outputFd)==-1)
errExit("close output");
exit(EXIT_SUCCESS);
return 0;
}
- 通用I/O
Unix I/O模型的显著特点之一就是输入/输出的通用性概念。
就是说open、read、write、close可以对所有类型的文件执行I/O操作,包括终端设备。所以如果只使用了这些系统调用编写的程序,可以对系统内所有类型的文件使用。
实现通用I/O的前提就是确保每一个文件系统和设备驱动程序都实现了相同的I/O系统调用集。因为linux下文件系统和设备所特有的操作细节已经放在内核中处理,所以在编程时通常可以忽略设备转悠的因素。而如果应用程序需要访问文件系统和设备的专有功能时,可以选择ioctl()系统调用来处理,该系统调用为I/O模型之外的专有特性提供了访问接口。 - 打开一个文件:Open()
open()调用既可以打开一个已经存在的文件,也能创建并打开一个新文件。
#include<sys/stat.h>
#include<fcntl.h>
int open(const char* pathname,int flags,.../*mode_t mode*/);
//返回:打开成功的话返回文件描述符,打开失败的话返回-1
要打开的文件由参数pathname来标识,如果pathname是一个符号链接,那么该调用会对其进行解引用。如果调用成功,open()返回文件描述符,用于在后续函数调用中指代该文件,如果发生错误,则返回-1,并将errno置为相应的错误标志。参数flags为位掩码,用来指定文件的访问模式。 当调用open()创建新文件时,位掩码参数mode指定了文件的访问权限。
如果open()并未指定O_CREAT标志,则可以省略mode参数。 O_RDONLY —->以只读方式打开文件 O_WRONLY —->以只写方式打开文件 O_RDWR —->以读写方式打开文件。
open函数使用的例子:
#include<sys/stat.h>
#include<fcntl.h>
#include "tlpi_hdr.h"
#include "error_functions.h"//用来输入错误信息
int main(int argc, char const *argv[])
{
int fd;
fd=open("startup",O_RDONLY);
if(fd==-1)
errExit("open");
fd=open("myfile",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR);
if(fd==-1)
errExit("open");
fd=open("w.log",O_WRONLY|O_CREAT|O_TRUNC|O_APPEND,S_IRUSR|S_IWUSR);
if(fd==-1)
errExit("open");
return 0;
}
open()调用所返回的文件描述符数值:SUSv3规定,如果调用open()成功,必须保证其返回值为进程为用文件描述符中数值最小者。
所以可以利用这项特性来以特定的文件描述符打开某一个文件。
例如:下例代码会确保使用标准输入(文件描述符0)打开一个文件
#include<sys/stat.h>
#include<fcntl.h>
#include "tlpi_hdr.h"
#include "error_functions.h"//用来输入错误信息
int main(int argc, char const *argv[])
{
int fd;
if(close(STDIN_FILENO)==-1)
//close()关闭STDIN_FILENO 使得系统内最小文件描述符为0
errExit("close");
fd=open("startup",O_RDONLY);
if(fd==-1)
errExit("open");
printf("%d\n",fd);
return 0;
}
结果截图:
由于文件描述符0未用,所以open()调用势必使用此描述符打开文件。
- open()调用中的flags参数
在上述的代码例子中,flags参数除了使用文件访问标志外,还使用了其他操作标志。如下图所示
上述访问标志可以分为三组:
1、文件访问模式标志:先前描述的O_RDONLY、O_WRONLY 、O_RDWR标志均在此类中,调用open()时,上述三者在flags参数中不能同时使用,只能指定其中一种。 调用fcntl()的F_GETFL操作可以检索文件的访问模式。
2、文件创建标志:这些标志位于图中第二部分,其控制范围不拘于open()调用行为的方方面面,还涉及后续I/O操作的各个选项。这些操作不能被检索,也无法修改。
3、已打开文件的状态标志:这些标志时图中的剩余部分。使用fcntl()的F_GETFL和F_SETFL操作可以分别检索和修改此类标志。有时干脆称之为文件状态标志。
flags常量的详解:
O_APPEND : 标志如其名,总是在文件尾部追加数据。
O_ASYNC : 当对open()调用所返回的文件描述符可以实施I/O操作时,系统会产生一个信号通知进程。这一个特性,也被称之为信号驱动I/O,仅对特定类型的文件有效,诸如终端、FIFO及socket。在linux中,调用open()时指定O_ASYNC标志没有任何实质效果,要启用信号驱动I/O特性,必须调用fcntl()的F_SETFL操作来设置O_ASYNC标志。
O_CLOEXEC : 为新(创建)的文件描述符启用close-on-flag标志(FD_CLOEXEC)。使用O_CLOEXEC标志(打开文件),可以免去程序执行fcntl()的F_GETFD和F_SETFD操作来设置close-on-exec标志的额外工作。———————-看不懂
O_CREATE : 如果文件不存在,将创建一个新的空文件。即使文件以只读方式打开,此标志依然有效。如果在open()调用中指定O_CREATE标志,那么还要提供mode参数,否则,会将新文件的权限设置为栈中的某个随机值。
O_TRUNC : 如果文件已经存在且为普通文件,那么将清空文件内容,将其长度置为0。在linux下使用此标志,无论以读、写方式打开文件,都可清空文件内容(在这两种情况下,都必须拥有对文件的读写权限)。
O_DIRECTORY : 如果pathname参数并非目录,将返回错误(错误号errno为ENOTDIR)。这一标志是专为实现opendir()函数而设计的扩展标志。为使O_DIRECTORY标志的常量定义在
#include<fcntl.h>
int create(const char * pathname,mode_t mode)
create()系统调用根据pathname参数创建并打开一个文件,若文件已存在,则打开文件,并清空文件内容,将其长度清0。create()返回一文件描述符,供后续系统调用使用。create()系统调用等同于如下open()调用:
fd=open(pathname,O_WRONLY|O_CREATE|O_TRUNC,mode); 现在一般都使用open()来代替create()的操作。
读取文件内容:read()
read()系统调用从文件描述符fd所指代的打开文件中读取数据。
#include<unistd.h>
ssize_t read(int fd,void *buffer,size_t count);
return number of bytes read,0 on EOF ,or -1 on error
count参数指定最多能读取的字节数,(size_t数据类型属于无符号整数类型)。buffer参数提供用来存放输入数据的内存缓冲区地址。缓冲区至少应有count个字节。linux的系统调用不会分配内存缓冲区用以返回信息给调用者。所以,必须预先分配大小合适的缓冲区并将缓冲区指针传递给系统调用。但有些库函数却会分配内存缓冲区用以返回信息给调用者。
如果read()调用成功,将返回实际读取的字节数。如果遇到文件结束(EOF)则返回0,如果出现错误则返回-1。ssize_t数据类型属于有符号的整数类型,用来存放(读取的)字节数或-1(表示错误)。如果读普通文件时,在读到要求字节数之前已经达到文件结尾,解决方法如:若达到文件尾端之前有30个字节,而要求读50个字节,则第一次read返回数值为30,在下次调用read时返回0(表示文件读取完毕)。
一次read()调用所读取的字节数可以小于请求的字节数。对于普通文件而言,这可能时因为当前的读取位置靠近文件尾部。
当read()应用于其他文件类型时,比如管道、FIFO、socket或者终端,在不同环境下也会出现read()调用读取的字节小于请求字节数的情况。
#include<unistd.h>
#include "tlpi_hdr.h"
#include "error_functions.h"
#define MAX_READ 20
int main(int argc, char const *argv[])
{
/* code */
char buffer[MAX_READ+1];
ssize_t numRead=read(STDIN_FILENO,buffer,MAX_READ);
if(numRead==-1)
errExit("read");
buffer[numRead]='\0'; //必须加上这一行,才能在终端内读取字符。
printf("The input data was :%s\n",buffer);
return 0;
}
read()可以从文件中读取任意序列的字节,有时读到的信息可能是文本数据,但有时可能是二进制整数或二进制形式的C语言数据结构。read()不能区分这些数据,所以不能遵从c语言对字符串处理的约定——在字符串尾部追加标识字符串结束的空字符。
所以如果要使用c语言和read()系统调用,必须在输入缓冲区结尾处显示追加一个表示终止的空字符。
数据写入文件:write()
write()系统调用将数据写入一个已经打开的文件中
#include<unistd.h>
ssize_t write(int fd,void *buffer,size_t count
write()调用的参数含义与read()调用类似。buffer参数为要写入文件中数据的内存地址,count参数为欲从buffer写入文件的数据字节数,fd参数为一文件描述符,指代数据要写入的文件。如果write()调用成功,将返回实际写入文件的字节数,该返回值可能小于count参数值。这被称为“部分写”。对磁盘文件来说,造成”部分写”的原因可能时因为磁盘已满,或是因为进程资源对文件大小的限制。
对磁盘文件执行I/O操作时,wirte()调用成功并不能保存数据已经写入磁盘。因为为了减少磁盘活动量和加快write()系统调用,内核会缓存磁盘的I/O操作。
关闭文件:close()
close()系统调用关闭一个打开的文件描述符,并将其释放返回调用进程,供进程继续使用。当一进程终止时,将其自动关闭其已打开的所有文件描述符。
#include<unistd.h>
int close(int fd);
显式关闭不再需要的文件描述符是个良好的编程习惯,可以使代码在后续修改时更具有可读性,也更可靠。文件描述符属于有限资源,因此文件描述符关闭失败可能会导致一个进程将文件描述符资源消耗殆尽。要像其他所有系统调用一样,应对close()的调用进行错误检查。
if(close(fd)==-1) errExit("close");
上面这个错误提示可以捕获到的错误有:尝试关闭一个未打开的文件描述符、两次关闭同一文件描述符等。
- 改变文件偏移量:lseek()
对于每个打开的文件,系统内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指**执行下一个**read()或write()操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为0。
第一次文件打开时,会将文件偏移量设置为指向文件开始,以后每次read()或write()调用将自动对其进行调整,以指向已读或已写数据后的下一字节。因此,连续的read()和write()调用将按顺序递进,对文件进行操作。
针对文件描述符fd参数所指代的已打开文件,lseek()系统调用依照offset和whence参数值调整该文件的偏移量。
#include<unistd.h>
off_t lseek(int fd,off_t offset,int whence) return new file offset if successful ,or -1 on error
offset参数指定了一个以字节为单位的数值,whence参数则表明应参照哪个基点来解释offset参数。
whence参数应为下:
SEEK_SET : 将文件偏移量设置为从文件头部起始点开始的offset个字节。———-头部
SEEK_CUR: 相对于当前文件偏移量,将文件偏移量调整offset个字节。 ——–当前位置
SEEK_END:将文件偏移量设置为起始于文件尾部的offset个字节。也就是说,offset参数应该从文件最后一个字节之后的下一个字节算起。
如果whence参数值为SEEK_CUR或SEEK_END,offset参数可以为正数也可以为负数;如果whence参数值为SEEK_SET,offset参数值必须为非负数。
lseek()调用成功会返回新的文件偏移量。下面的调用只是获取文件偏移量的当前位置,并没有修改它。
curr=lseek(fd,0,SEEK_CUR)