本章目录
1、引言
一般可用的文件I/O函数--打开文件、读文件、写文件等等,大多数只需要用到5个函数:open、read、write、lseek、以及close。这一节涉及到的函数称为不带缓存的I/O,不带缓存的指的是每一个write和read都是调用内核中的一个系统调用。
2、文件描述符
对于内核而言,所有打开文件都是由文件描述符引用,文件描述符是一个非负整数。当打开一个现存文件或者创建一个新文件内核会向进程返回一个文件描述符,当读写一个文件时,用open或creat返回的文件描述符来标识这个文件,将其作为参数传送给read或write。(也就是说,文件描述符是用来表示一个文件的标识,在程序中可以用它来代表这个文件)。
一般而言,文件描述符0与进程的标准输入结合,1与标准输出结合,2与标准出错输出结合。而在程序中,这几个数字代换为STDIN_FILENO、STDOUT_FILENO、和STDERR_FILENO。定义在头文件<unistd.h>中。
文件描述符的范围0~OPEN_MAX,一个进程上限一般是63。
3、open函数
调用open函数可以打开或者创建一个文件。
//open函数原型
int open(const char *pathname,int oflag,...)
//下为<fcntl.h>文件中内容
/*文件控制选项头文件。主要定义了函数fcntl和open中用到的一下选项*/
#ifndef _FCNTL_H
#define _FCNTL_H
#include<sys/types.h>/*类型头文件,定义了基本的系统数据类型*/
#define O_ACCMODE 00003 //文件访问模式屏蔽码
#define O_RDONLY 00 //只读方式打开
#define O_WRONLY 01 //只写方式打开
#define O_RDWR 02 //以读写方式打开文件
#define O_CREAT 00100 //文件不存在,就创建
#define O_EXCL 00200 //独占使用文件标志
#define O_NOCTTY 00400 //不分配终端
#define O_TRUNC 01000 //若文件已存在且是写操作,则长度截为0
#define O_APPEND 02000 //追加方式打开,文件指针置为文件尾
#define O_NONBLOCK 04000 //非阻塞方式打开和操作文件
#define O_NDELAY O_NONBLOCK//非阻塞方式打开和操作文件
//
#define F_DUPFD 0 //拷贝文件句柄为最小值且没有使用的句柄
#define F_GETFD 1 //取文件句柄标志
#define F_SETFD 2 //设置文件句柄标志
#define F_GETFL 3 //取文件状态标志和访问模式
#define F_SETFL 4 //设置文件状态标志和访问模式
#define F_GETLK 5 //返回阻止锁定的flock结构
#define F_SETLK 6 //设置(F_RDLCK或F_WRLCK)或清除锁定
#define F_SETLKW 7 //等待设置或清除锁定
//在执行exec簇函数时关闭文件句柄
#define FD_CLOEXEC 1 //
//
#define F_RDLCK 0 //共享或读文件锁定
#define F_WRLCK 1 //独占或写文件锁定
#define F_UNLCK 2 //文件解锁
//没有实现。文件锁定操作数据数据结构,描述受影响文件段的类型(l_type)开始偏移
//(l_whence),相对偏移(l_start),锁定长度(l_len)和实施锁定的进程id
struct flock{
short l_type; //锁定类型(F_RDLCK,F_WRLCK,F_UNLCK)
short l_whence; //开始偏移(SEEK_SET,SEEK_CUR,SEEK_END)
off_t l_start; //阻塞锁定的开始处.相对偏移
off_t l_len; //阻塞锁定的大小,如果是0则为文件末尾
pid_t l_pid; //加锁的进程id
};
/*
*创建新文件或重写一个存在的文件
*/
extern int creat(const char*filename,mode_t mode);
/*
*文件句柄操作,会影响文件的打开
*/
extern int fcntl(int files,int cmd,...);
//打开文件。在文件与文件句柄之间建立联系
extern int open(const char*filename,int flags,...);
#endif
对于open函数而言,第三个参数写为...是说明余下参数的数目和类型可以变化的方法,只有创建新文件才使用第三个参数。
pathname是要打开或者创建的文件的名字,oflag参数可用来说明此函数的多个选择项,用下列一个或多个常数进行或运算构成oflag参数。(其实还是常数,定义在<fcntl.h>):
O_RDONLY 只读打开。O_WRONLY 只写打开。ORDWR 读写打开。更多参数参考上图。
open返回的文件描述符一定是最小的未用描述数字。这一点可以被很多应用程序用来在标准输入、标准输出、或标准出错输出上打开一个新文件。
4、creat函数
也可以使用creat函数创建一个新文件。
//creat函数
int creat (const char *pathname,mode_t mode);
//等价于
open(pathname,O_CREAT | O_TRUNC | O_WRONLY,mode);
creat有一个不足之处是只能以只写模式打开创建的文件,如果要创建一个临时文件那么要先写文件、然后读文件、必须调用creat、close、再open。现在可以直接使用open函数。
5、close函数
可以用close函数关闭一个打开的文件。关闭一个文件也释放加在该文件上的所有记录锁。当一个进程终止时,它所有的打开文件都由内核自动关闭。
//函数原型
int close(int filedes);
6、lseek函数
每打开一个文件都有一个与其相关联的当前文件位移量。他是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都是从当前文件位移量处开始,并使位移量增加所读或所写的字节数,按照系统默认,当打开一个文件时,除非指令O_APPEND选择项,否则位移量被设置为0。
//lseek - reposition read/write file offset
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
对于参数offset的解释与参数whence的值有关:
若whence是SEEK_SET,则将该文件的位移量设置距离文件开始处offset个字节。
若whence是SEEK_CUR,则将该文件的位移量设置为其当前值加offset个字节,offset可为正负。
若whence是SEEK_SET,则将该文件的位移量设置为文件长度加offset个字节,offset可为正负。
若lessk成功执行,则返回新文件的位移量,为此可以用下列方式确定一个打开文件的当前偏移量。
通常,文件的当前位移量应该是一个非负数,但是某些设备也可能允许负的位移量,对于普通文件其位移量必须是非负值,所以在比较lessk的返回值应该很谨慎,不要测试是否小于0,而要测试是否等于-1。
lessk仅将当前文件的位移量记录在内核,不引起任何IO操作,然后该位移量用于下一个读写操作。文件位移量可以大于当前文件长度,这种情况下对该文件的下一次写将延长该文件,并在文件中构成一个空调,这一点是允许的,没有写过的字节都被读为0。
#include<sys/types.h>
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
char buf1[]="abcdefghij";
char buf2[]="ABCDEFGHIJ";
int main()
{
int fd;
if((fd=creat("test.txt",O_RDWR))<0)
printf("error!\n");
else
printf("DONE!\n");
if(write(fd,buf1,10)!=10)
printf("error2!\n");
else
printf("DONE2!\n");
if(lseek(fd,40,SEEK_SET)==-1)
printf("error3!\n");
else
printf("DONE!\n");
if(write(fd,buf2,10)!=10)
printf("error4!\n");
else
printf("DONE!\n");
return 0;
}
执行结果如下:
可以看到中间30个字节写为0。
7、read函数
用read函数从打开文件中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
如果read成功,返回读到的字节数,如果已达到文件尾端返回0。
作用是从文件描述符 fd 中读取 count 个字节的数据到 buf 所指向的空间。
返回值:返回成功读取到的字节数;0 表示读取到了文件末尾;-1 表示出现错误并设置 errno。
8、write函数
write函数向打开的文件写数据。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:返回成功写入的字节数;0 并不表示写入失败,仅仅表示什么都没有写入;-1 才表示出现错误并设置 errno。为什么会出现写入的值是 0 的情况呢?其实原因有很多,其中一个原因是当写入的时候发生了阻塞,而阻塞中的 write(2) 系统调用恰巧被一个信号打断了,那么 write(2) 可能没有写入任何数据就返回了,所以返回值会是0。
9、IO效率
具体在学习标准IO之后再作比较。
10、文件共享
UNIX支持在不同进程间共享打开文件。内核使用了三种数据结构,他们之间的关系决定了文件共享方面一个进程对另一个进程可能会产生的影响。
(1)每个进程在进程表都有一个记录项。每个记录项都有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项,与文件描述符相关联的是文件描述符标志和指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表,每个文件表包含文件状态标志(读、写、增写、同步、非阻塞等)、当前文件位移量和指向该文件v节点表项的指针。
(3)每个打开的文件或设备都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作和数的指针信息。如下图,该进程有两个不同的打开文件,一个文件为标准输入(文件描述符为0),另一个为标准输出(文件描述符为1)。
如果两个独立的进程各自打开了同一文件,则有如下图关系:
上述的一切对于多个进程读取同一个文件都能正确工作,每个进程都有自己的文件表项,其中也有他自己的当前文件位移量,但是当多个进程写同一文件时,则可能会产生预期不到的结果,为了避免这种情况,需要理解原子操作的概念。
11、原子操作
11.1 添加至一个文件
考虑一个进程要将数据添加到一个文件尾端,早期UNIX不支持open的O_APPEND选项,程序会如下:
对于单个进程而言,这个程序可以正常工作,但是如果有多个进程,则会产生问题。假设现有两个进程AB,都对同一个文件进行添加操作,两个进程都已打开了该文件,但未使用O_APPEND标志,每个进程都有自己的文件表项,但共享一个v节点表项。假设A调用了lseek,它将对A的该文件的当前位移量设置为1500字节(当前文件尾端处),然后内核切换进程使进程B运行,B调用write,将B的该文件的当前位移量增至1600,因为文件长度已经增加了,所以v节点的长度更新为1600,然后内核又切换进程A运行,当A调用write时就从当前文件位移量1500处将数据写入,会覆盖B进程写入的数据。
这里的问题出在逻辑操作“定位档到文件尾端处”然后写“使两个分开的函数调用”。解决的方法是是这两个操作对于其他进程而言成为一个原子操作,任何一个要求多余1个函数调用都不能成为原子操作,因为在两个函数调用之间,内核有可能临时挂起该进程。UNIX提供了一种方法使这种操作成为原子操作,其方法是再打开文件时设置O_APPEND标志,这会使内核每一次对这种文件进行写之前都将进程的当前位移量设置到文件的尾端处,于是在每次写之前都不用再调用lseek。
11.2 创建一个文件
如果在打开和创建之间,另一个进程创建了该文件,那么就会发生问题,如果在这两个函数调用之间另一个进程创建了该文件,而且又向该文件写入了一些数据,那么执行这段程序中的creat时,刚写上去的数据就会被擦去,这两个合成一个原子操作那么这个问题也就不会发生。
也就是说,原子操作指的是多步组成的操作,如果该操作原子地执行则或者执行完所有步,或者一步也不执行,不可能只执行所有步的一个子集。
12、dup和dup2函数
下面两个函数都可以用来复制一个现存的文件描述符:
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
由dup返回的新文件描述符一定是当前可用文件描述符的最小数值。用dup2则可以用filedes2参数指定新描述符的数值,如果filedes2已经打开,则先将其关闭,如果filedes等于filedes,则dup2返回filedes2而不关闭它。这些函数返回的新文件描述符与参数filedes共享同一个文件表项。
每个文件描述符都有它自己的一套文件描述符标志,新描述符的执行时关闭文件描述符标志总是由dup函数。复制一个描述符的另一个方法是使用fcntl函数。
//调用
dup(filedes)
//等于
fcntl(filedes,F_DUPFD,0);
//调用
dup2(filedes,filedes2);
//等于
close(filedes2);
fcntl(filedes,F_DUPFD,filedes2);
dup2是一个原子操作。
13、fcntl函数
fcntl函数可以改变已经打开文件的性质。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
在本节的例子中,第三个参数总是一个整数,与上面所示函数原型中的注释部分相对应。根据不同的 cmd 和 arg 读取或修改对已经打开的文件的操作方式。具体参数选项可以man来查看。程序在一个描述符上进行操作时,如果文件是Shell打开的,那么不会知道相应的文件名,也就不能设置O_SYNC标志(同步IO方式),fcntl则允许当仅知道打开文件的描述符时可以修改其性质。
14、ioctl函数
ioctl函数是IO操作的杂物箱,不能用本章其他IO函数操作通常可以用ioctl函数表示。终端IO是ioctl函数的最大使用方面。
// ioctl - control device
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
Linux 的一切皆文件的设计原理将所有的设备都抽象为一个文件,当一个设备的某些操作不能被抽象成打开、关闭、读写、跳过等动作时,其它的动作都通过 ioctl 函数控制。
15、/dev/fd
比较新的系统都提供名为/dev/fd的目录,其目录项是名为0、1、2等的文件,打开文件/dev/fd/n等效于复制文件描述符n。
16、总结
本章介绍传统的UNIX的IO函数,这些函数称为不带缓存的IO函数,也说明了多个进程对同一个文件进行添加操作和多个进程创建同一个文件,介绍原子操作,以及内核用来共享打开文件信息的数据结构,在后续ioctl函数将用于流IO系统,fcntl函数用于记录锁。