UNIX系统中的大多数文件I/O只需用到5个函数:open, read, write, lseek以及close。下面要描述的函数经常被称为不带缓冲的I/O(unbuffered I/O)。术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。
- 文件描述符
(1)对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。
(2)按照惯例,UNIX系统shell把文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准错误相关联。
(3)在符合POSIX.1的应用程序中,0、1、2虽然已经被标准化,但应当把它们替换成符号常量STDIN_FINLENO, STDOUT_FILENO, STDERR_FILENO以提高可读性。这些常量定义在<unistd.h>。
(4)文件描述符的变化范围是0~OPEN_MAX-1,早期的unix系统实现采用的上限值19,现在很多系统上限值增加至63。 - 函数open和openat
1234567//调用open或openat函数可以打开或创建一个文件#include <fcntl.h>int open(const char *path, int oflag,.../*mode_t mode*/);int openat(int fd, const char *path, int oflag,.../*mode_t mode*/);//函数返回值:若成功,返回文件描述符;失败返回-1
(1)我们将最后一个参数写为…, ISO用这种方法表明余下的参数的数量及其类型是可变的。(2)path参数是要打开或创建文件的名字。oflag参数可用来说明此函数的多个选项,用下列一个或多个常量进行“或”运算构成oflag参数(这些常量定义在<fcntl.h>中):
1)O_RDONLY:只读打开
2)O_WRONLY:只写打开
3)O_RDWR:读、写打开
(大多数实现将_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2)
4)O_EXEC:只执行打开
5)O_SEARCH:只搜索打开(应用于目录)
6)O_APPEND:每次写时都追加到文件尾端
7)O_CREAT:若此文件不存在则创建它。使用此选项时,open函数需要同时说明第3个参数mode,用mode指定该新文件的访问权限。
8)O_EXCL:如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作。
9)O_NONBLOCK:如果path引用的是一个FIFO、一块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。
10)O_TRUNC:如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0.
11)…(3)由open和openat函数返回的文件描述符一定是最小的未用描述符数值。
(4)openat函数是POSIX.1最新版本中新增的一类函数,希望解决两个问题:第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录;第二,可以避免time-of-check-to-time-of-use(TOCTTOU)错误。 - 函数create也可调用create函数创建一个新文件:
1234#include <fcntl.h>int creat(const char *path, mode_t mode);//返回值:成功返回只写打开的文件描述符;失败返回-1
该函数等效于:
1open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);creat的一个不足之处是它以只写方式打开所创建的文件。
- 函数close
可调用close函数关闭一个打开文件:1234#inclue <fcntl.h>int close (int fd);//返回值:成功返回0,失败返回-1(1)关闭一个文件时,还会释放该进程加在该文件上的所有记录锁。
(2)当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显示地用close关闭打开文件。 - 函数lseek
(1)每个打开文件都有一个与其相关联的“当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常读写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。
(2)当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
(3)可以调用lseek显示地为一个打开文件设置偏移量:1234#include <unist.h>off_t lseek(int fd, off_t offset, int whence);//返回值:成功返回新的文件偏移量;错误返回-1其中,参数offset的解释与参数whence的值有关:
1)若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
2)若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可正可负。
3)若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
(4)lseek成功执行后,可以用下列方式打开文件的当前偏移量:12off_t currpos;currpos = lseek(fd, 0, SEEK_CUR);(5)这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
(6)实例:测试标准输入能否被设置偏移量12345678910#include "apue.h"int main(){if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)printf("can not seek\n");elseprintf("seek OK\n");exir(0);}(7)lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作
(8)文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
实例(创建一个具有空洞的文件):123456789101112131415161718192021222324#include "apue.h"#Include <fcntl.h>char buf1[] = "abcdefghij";char buf2[] = "ABCDEFGHIJ";int main(void){inf fd;if ((fd = creat("file.hole", FILE_MODE)) < 0) //FILE_MODE是一个宏,表示权限0644err_sys("buf1 write error");if (write(fd, buf1, 10) != 10)err_sys("buf1 write error"); //offset now = 10if (lseek(fd, 16384, SEEK_SET) == -1)err_sys("lseek error"); //offset now = 16384if (write(fd, buf2, 10) != 10)err_sys("buf2 write error"); //offset now = 16394exit(0);}运行程序后可以使用od -c file.hole命令查看该文件实际内容。
- 函数read
(1)调用read函数从打开文件中读数据:1234#include <unistd.h>ssize_t read(int fd, void *buf, size_t nbytes);//返回值:读到的字节数,若已到文件尾,返回0;失败返回-1(2)有多种情况可使实际读到的字节数少于要求读的字节数:
1)读普通文件时,在读到要求字节数之前已达到了文件尾端。
2)当从终端设备读时,通常一次最多读一行。
3)当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
4)当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
5)当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
6)当一信号造成中断,而已经读了部分数据量时。 - 函数write
(1)调用write函数向打开文件写数据:1234#include <unistd.h>ssize_t write(int fd, const void *buf, size_t nbytes);//返回值:成功返回已写的字节数;失败返回-1(2)其返回值通常与参数nbytes的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。
(3)对于普通文件,写操作从文件的当前偏移量处开始。如果指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。 - 原子操作
一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行。- 追加到一个文件
… - 函数pread和pwrite
Single UNIX Specification包括了XSI扩展,该扩展允许原子地定位并执行I/O。pread和pwrite就是这种扩展。
1234567#inclue <unistd.h>ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);//返回值:读到的字节数,若已到文件尾,返回0;失败返回-1ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);//返回值:成功返回已写的字节数;失败返回-1
- 调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有下列重要区别:
(1)调用pread时,无法中断其定位和读操作。
(2)不更新当前文件偏移量。 - 调用pwrite相当于调用lseek后调用write,但也与它们有类似区别。
- 追加到一个文件
- 函数dup和dup2
(1)可以用来复制一个现有的文件描述符:
123456#include <unistd.h>int dup(int fd);int dup2(int fd, int fd2);//两函数的返回值:成功返回新的文件描述符;失败返回-1
(2)由dup返回的新文件描述符一定是当前可用文件描述符中的最小值。
(3)对于dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,则现将其关闭。如果fd等于fd2,则dup2返回fd2,而不关闭它。
(4)这些函数返回的新文件描述符与参数fd共享同一个文件表项。 - 函数sync、fsync和fdatasync
当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式称为延迟写(delayed write)。
12345678#include <unistd.h>int fsync(int fd);int fdatasync(int fd);//返回值:成功返回0,失败返回-1void sync(void);
(1)sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
(2)fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。
(3)fdatasync函数类似于sync,但它只影响文件的数据部分。而除数据之外,fsync还会同步更新文件的属性。 - 函数fcntl
1234#include <fcntl.h>int fcntl(int fd, int cmd, .../*int arg */);//返回值:成功,依赖于cmd;失败返回-1
(1)fcntl函数有一下5中功能:
1)复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)。
2)获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD)。
3)获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)。
4)获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。
5)获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)。
(2)先说明这11中cmd中的前8种:
1)F_DUPFD:复制文件描述符fd;新文件描述符作为函数值返回;它是尚未打开的各描述符中大于或等于第3个参数值中各值的最小值;新描述符与fd共享同一文件表项,但新描述符有它自己的一套文件描述符标志。
2)F_DUPFD_CLOEXEC:复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符。
3)F_GETFD:对应于fd的文件描述符标志作为函数值返回。
4)F_SETFD:对于fd设置文件描述符标志。新标志值按第3个参数设置。
5)F_GETFL:对应于fd的文件状态标志作为函数值返回。
6)F_SETFL:将文件状态标志设置为第3个参数的值。可改的标志:O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC, O_FSYNC, O_ASYNC。
7)F_GETOWN:获取当前接受SIGIO和SIFURG信号的进程ID或进程组ID。
8)F_SETOWN:设置接受SIGIO和SIGURG信号的进程ID或进程组ID。 - 函数ioctl
(1)ioctl函数一直是I/O操作的杂物箱,终端I/O是使用ioctl最多的地方。
12345#include <unistd.h> //SystemV#include <sys/ioctl.h> //BSD and Linuxint ioctl(int fd, int request, ...);
(2)对于ISO C原型,它用省略号表示其余参数。但是,通常只有另外一个参数,它常常是指向一个变量或结构的指针。
小结:
(1)本次讲解了UNIX系统提供的基本I/O函数。因为read和write都在内核执行,所以称这些函数为不带缓冲的I/O函数。
(2)还介绍了fcntl和ioctl函数,本书后续部分还会涉及这两个函数。