1. 在对文件进行读写操作之前,首先需要打开文件。内核会为每个进程维护一个打开文件的列表,该列表称为文件表(file table)。文件表是由一些非负整数进行索引,这些非负整数称为文件描述符(file descriptor)。列表的每一项是一个打开文件 的信息,包含指向该文件索引节点(inode)内存拷贝的指针以及关联的元数据,如文件位置指针和访问模式。
默认情况下,子进程会维护一份父进程的文件表副本。在该副本中,打开文件列表及其访问模式、当前文件位置以及其他元数据,都和父进程维护的文件表相同,但存在一点区别:即当子进程关闭一个文件时,不会影响到父进程的文件表。

2. 通过read()读文件
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t len);
每次调用read()函数,会从fd指向的文件的当前偏移开始读取len字节到buf所指向的内存中。执行成功时,返回写入buf中的字节数;出错时,返回-1,并设置errno值。fd的文件位置指针会向前移动,移动的长度由读取到的字节数决定。如果fd所指向的对象不支持seek操作(比如字符设备文件),则读操作总是从“当前”位置开始。
对于read()而言,返回小于len的非零正整数是合法的。实际上,调用read()有很多可能结果:
- 调用返回值等于len。读取到的所有len个字节都被存储在buf中。结果和预期的一致。
- 调用返回值小于len,大于0。读取到的字节被存储在buf中。比如在读取过程中信号中断,再次执行read(分别更新buf和len值)
- 调用返回0,表示EOF,没有更多可读的数据。
- 由于当前没有数据可用,调用阻塞。在非阻塞模式下,不会发生这种情况。
- 调用返回-1,并把errno设置成EINTR。这表示在读取任何字节前收到信号。调用可以重新执行。
- 调用返回-1,并把errno设置成EAGAIN。这表示当前没有任何数据可用,请求应该稍后再重新执行。这种情况只在非阻塞模式下发生。
- 调用返回-1,并把errno设置成非EINTR或EAGAIN的一个值。这表示更严重的错误。重新执行读操作不会成功。
读入所有字节可以这样:
ssize_t ret;
while (len !=0 &&(ret = read(fd, buf, len)) != 0) {
if (ret == -1) {
if (errno == EINTR)
continue;
perror("read");
break;
}
len -= ret;
buf += ret;
}
3. 调用write()写
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write()调用会从文件描述符fd指向的文件的当前位置开始,将buf中至多count个字节写入到文件中。不支持seek的文件(如字符设备)总是从起始位置开始写。write()指向成功时,会返回写入的字节数,并更新文件位置。出错时,返回-1,并设置errno值。
对于普通文件(不太可能会返回部分写),不需要执行循环写操作。
但是,对于其他的文件类型,比如socket,需要循环来保证写了所有请求的字节:
ssize_t ret;
while (len != 0 && (ret = write(fd, buf, len)) != 0) {
if (ret == -1) {
if (errno == EINTR)
continue;
perror("write");
break;
}
len -= ret;
buf += ret;
}
当write()调用返回时,内核已经把数据从提供的缓冲区拷贝到内核缓冲区中。但不保证数据已经写到目的地。
4. 关闭文件
#include <unistd.h>
int close(int fd);
系统调用close()会取消当前进程的文件描述符fd与其关联的文件之间的映射。关闭文件操作并非意味着该文件的数据已经被写到磁盘。
5. 用lseek()查找
一般情况下,I/O是线性的,由于读写引发 的隐式文件位置更新都需要seek操作。lseek()系统调用能够将文件描述符的位置指针设置成指定值。lseek()只更新文件位置,没有执行其他操作,也并不初始化任何I/O:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t pos, int origin);
6. 定位读写
Linux提供了两种read()和write()系统调用的变体来代替lseek(),每次读写操作时,都把文件位置作为参数,在完成时,不会更新文件位置。
ssize_t pread(int fd, void *buf, size_t count, off_t pos);从文件描述符的pos位置开始读取,共读取count个字节到buf中
ssize_t pwrite(int fd, const void *buf, size_t count, off_t pos);从文件描述符fd的pos位置开始,从buf中写count字节到文件中
定位读写只适用于可查找的文件描述符,包括普通文件。pread()和pwrite()调用的语义相当于在read()和write()之前调用lseek()调用,但存在以下三点区别:
- pread()和pwrite()调用在结束时不会修改文件位置指针。
- pread()和pwrite()调用避免了在使用lseek()时出现的竞争。
7. I/O多路复用
内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程。
select poll