open、read、write、lseek、close。
本章所说明的函数被称为不带缓冲的I/O(unbuffered I/O)。不带缓冲:指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的I/O函数是POSIX.1和Single UNIX Specification的组成部分。
所谓不带缓冲,并不是指内核不提供缓冲,而是只单纯的系统调用,不是函数库的调用。系统内核对磁盘的读写都会提供一个块缓冲(在有些地方也被称为内核高速缓存),当用write函数对其写数据时,直接调用系统调用,将数据写入到块缓冲进行排队,当块缓冲达到一定的量时,才会把数据写入磁盘。因此所谓的不带缓冲的I/O是指进程不提供缓冲功能(但内核还是提供缓冲的)。每调用一次write或read函数,直接系统调用。
带缓冲的I/O是指进程对输入输出流进行了改进,提供了一个流缓冲,当用fwrite函数网磁盘写数据时,先把数据写入流缓冲区中,当达到一定条件,比如流缓冲区满了,或刷新流缓冲,这时候才会把数据一次送往内核提供的块缓冲,再经块缓冲写入磁盘。(双重缓冲)
因此,带缓冲的I/O在往磁盘写入相同的数据量时,会比不带缓冲的I/O调用系统调用的次数要少。
只要涉及在多个进程间共享资源,原子操作的概念就非常重要。
dup、fcntl、sync、fsync、ioctl。
#include
/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
文件描述符的变化范围是0~OPEN_MAX。
打开或创建一个文件
#include
int open(const char *pathname, int oflag, .../* mode_t mode */);
/* 返回值:若成功则返回文件描述符,若出错则返回-1 */
oflag参数:
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读、写
O_APPEND 追加
O_CREAT 若不存在,则创建
O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错
O_TRUNC 如果此文件存在,而且为只读或者读写成功打卡,则将其长度截断为0
O_NOCTTY 如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终端
O_NONBLOCK 非阻塞模式
O_DSYNC
O_RSYNC
O_SYNC
PATH_MAX、NAME_MAX。常量_POSIX_NO_TRUNC决定了是要截断过长的文件名或路径名,还是返回一个出错(errno设置为ENAMETOOLONG)。
创建一个新文件
#include
int creat(const char *pathname, mode_t mode);
/* 返回值:若成功则返回为只写打开的文件描述符,若出错则返回-1 */
此函数等效于:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
关闭一个打开的文件
#include
int close(int fd);
/* 返回值:若成功则返回0,若出错则返回-1 */
关闭一个文件时,还会释放该进程加在该文件上的所有记录锁。
当一个进程终止时,内核自动关闭它所有打开的文件。
当前文件偏移量(current file offset),用以度量从文件开始出计算的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
调用lseek显式地为一个打开的文件设置其偏移量。
#include
off_t lseek(int fd, off_t offset, int whence);
/* 返回值:若成功则返回新的文件偏移量,若出错则返回-1 */
whence参数:SEEK_SET、SEEK_CUR、SEEK_END。
lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构建一个空洞。位于文件中但没有写过的字节都被读为0。
文件的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关。当定位超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
调用read函数从打开文件中读数据。
#include
ssize_t read(int fd, void *buf, size_t nbytes);
/* 返回值:若成功则返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1 */
有多种情况可使实际读到的字节数少于要求读的字节数:
1. 读普通文件时,在读到的要求字节数之前已达到了文件尾端。
2. 从终端设备读时,通常一次最多读一行。
3. 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
4. 当从管道或从FIFO读时,如若管道包含的字节少于所需要的字节数,那么read将只返回实际可用的字节数。
5. 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
6. 当某一信号造成中断,而已经读了部分数据量时。
调用write函数向打开的文件中写数据。
#include
ssize_t write(int fd, const void *buf, size_t nbytes);
/* 返回值:若成功则返回已写的字节数,若出错则返回-1 */
内核使用三种数据结构表示打开的文件,分别是文件描述符表、文件表和 V 节点表。它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,每个描述符占用一项。与每个文件描述符相关联的是:
(a) 文件描述符标志。
(b) 指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
(a) 文件状态标志(读、写、添写、同步和非阻塞等)。
(b) 当前文件偏移量。
(c) 指向该文件 V 节点表项的指针。
(3)每个打开文件(或设备)都有一个 v 节点(v-node)结构。v 节点包含了文件类型和对此文件进行各种操作的函数的指针。v 节点还包含了从磁盘读取的 i 节点(i-node)的信息,i 节点信息包含了文件的所有者、文件长度、文件所在的设备、指向文件的实际数据块在磁盘上所在位置的指针等。
打开文件描述符表可存放在用户空间,而非进程表中。
下图显示了一个进程的三张表之间的关系。该进程有两个不同的打开文件,一个文件打开为标准输入(文件描述符为 0),另一个打开为标准输出(文件描述符为 1)。
如果两个独立进程各自打开了同一个文件,则有下图所示的安排。
1)每个进程都有自己的文件表项的一个理由:这种安排使每个进程都有它自己的对该文件的当前偏移量。
2)可能有多个文件描述符项指向同一文件表项。dup函数如此。fork也会发生,父、子进程对于每一个打开文件描述符共享同一个文件表项。
3)文件描述符标志只用于一个进程的一个描述符; 文件状态标志则适用于指向该给定文件表项的任何进程中的所有描述符。
补充说明:(考虑下节要说的原子操作)
UNIX系统提供了一种方法:在打开文件时设置O_APPEND标志。使内核每次对这种文件写之前,都将进程的当前偏移量设置到该文件的尾端处。
原子性地定位搜索(seek)和执行I/O:
#include
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
/* 返回值:读到的字节数,若已到文件结尾则返回0,若出错则返回-1 */
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
/* 返回值:若成功则返回已写的字节数,若出错则返回-1 */
pread相当于顺序调用lseek和read。
一般而言,原子操作(atomic operation)指的是由多步组成的操作。如果该操作原子地执行,则要么执行完所有的步骤,要么一步也不执行,不可能执行所有步骤中的一个子集。
复制一个现存的文件描述符。
#include
int dup(int filedes);
int dup2(int filedes, int filedes2);
/* 返回值:若成功则返回新的文件描述符,若出错则返回-1 */
dup返回的新文件描述符一定是当前可用文件描述符中最小数值。
dup2则可以用filedes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。若filedes等于filedes2,则dup2返回filedes2,而不关闭它。
这些函数返回的新文件描述符与参数filedes共享一个文件表项。
执行:
newfd = dup(1);
则fd 1和fd 3共享同一文件表项。
调用 dup(filedes);
等效于 fcntl(filedes, F_DUPFD, 0);
调用 dup2(filedes, filedes2);
等效于 close(filedes2); fcntl(filedes, F_DUPFD, filedes);
dup2是一个原子操作。dup2和fcntl有些不同的errno。
传统的UNIX实现在内核中设有缓冲区高速缓存或者页面高速缓存,大多数磁盘IO都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后等待其到达队首,才进行实际的IO操作。这种方式被称为延迟写(delayed write)。
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟会造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区告诉缓存中的内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
#include
int fsync(int filedes);
int fdatasync(int filedes);
/* 返回值:若成功则返回0,若出错则返回-1 */
void sync(void);
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
fsync函数只对有filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除了数据部分,fsync还会同步更新文件的属性。
fcntl函数可以改变已打开的文件的性质。
#include
int fcntl(int fd, int cmd, .../* int arg */);
/* 返回值:若成功则依赖于cmd,若出错则返回-1 */
fcntl函数有5种功能:
(1)复制一个现有的文件描述符(cmd=F_DUPFD)
(2)获得或设置文件描述符标记(cmd=F_GETFD或F_SETFD)
(3)获得或设置文件状态标志(cmd=F_GETFL或F_SETFL)
(4)获得或者设置异步IO所有权(cmd=F_GETOWN或F_SETOWN)
(5)获得或设置记录锁(cmd=F_GETLK或F_SETLK、F_SETLKW)
F_DUPFD 复制文件描述符filedes。新文件描述符作为函数值返回。它是未打开的各描述符中大于或等于第三个参数值中各值的最小值新描述符与filedes共享同一文件表项。但是,新文件描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除。
F_GETFD 对应于filedes的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。
F_SETFD 对于filedes设置文件描述符标志。新标志按第三个参数设置。
F_GETFL 对应于filedes的文件状态标志作为函数值返回。在说明open函数时,已说明了文件状态标志。
F_SETFL 将文件状态标识设置为第三个参数值。
F_GETOWN 取当前接受SIGIO和SIGURG信号的进程ID或进程组ID。
F_SETOWN 设置接受SIGIO和SIGURG信号的进程ID或者进程组ID
返回值:
如果出错,所有命令都返回-1。
F_DUPFD:返回新的文件描述符。
F_GETFD、F_GETFL:返回相应标志。
F_GETOWN:返回一个正的进程ID或负的进程组ID
ioctl函数是I/O操作的杂物箱。
#include /* System V */
#include /* BSD and Linux */
#include /* XSI STREAMS */
#include
int ioctl(int filedes, int request, ...)
/* 返回值:若出错则返回-1,若成功则返回其他值 */
每个设备驱动程序都可以定义它自己专用的一组ioctl命令。系统则为不同种类的设备提供通用的ioctl命令。
其目录项是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符n(假定n是打开的)。