一, 基本函数 open, close, creat, lseek, read, write
- 这些是最原始的系统内核函数, 其他I/O函数例如fread, fwrite系列等都是由此派生而出的
- open函数:
- int open(const char *pathname, int oflag, ...)
- 有很多选项是内核版本相关的, 需要查询man手册
- oflag 参数指定了打开模式, 必须是O_RDONLY, O_WRONLY和O_RDWR三选一, 以及若干其他选项, 指定了同步/异步, 阻塞/非阻塞, APPEND, TRUNC等功能
- 当oflag中有O_CREAT时, 后面可以接一个mode参数, 用来指定其他用户对文件的读写权限
- creat函数: 等效于open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode), 属于历史产物, 一般不会使用
- close函数:
- 会释放该进程加在文件上的所有记录锁
- 进程终止时会自动关闭所有打开的文件
- read函数:
- ssize_t read (int fd, void * buf, size_t nbytes);
- 返回读到的字节数, 可能有多种情况导致返回值小于nbytes参数, 如各种情况导致的数据不足
- 返回0表示达到文件结尾, 返回-1出错
- write
- ssize_t write (int fd, const void * buf, size_t nbytes);
- 成功则返回值等于nbytes参数, 不会出现返回值 < nbytes的情况
- 出错返回-1, 一般是磁盘满
- lseek:
- off_t lseek (int fd, off_t offset, int whence)
- whence是三者之一: SEEK_CUR, SEEK_END, SEEK_SET
- 不产生I/O操作
- 一般情况下每个fd有自己的偏移量. (dup或fork可能产生特殊情况)
- 返回偏移量
二, Unix文件数据结构
- 当一个进程打开多个文件时, 示意图如下:
- 每个fd对应一个记录项, 记录项中又一个文件表的指针, 文件表中又有一个指针指向真正的文件节点(v节点)
- 不同的fd中可以指向相同的文件v节点, 也可能有相同的文件表指针
- 这种结构使得可以在进程中打开多个文件(v节点相同), 而文件标志位和偏移量在文件表结构中, 与文件无关, 这样就使多个fd同时操作一个文件成为可能
- v节点中包含了v节点和i节点, 其中v节点记录与具体文件系统无关的信息, i节点记录文件系统相关的信息. Linux中的v节点也用i节点数据结构实现
- 可以用dup和dup2函数来复制一个现有的文件描述符(fd). 其中dup2可以指定新生成的fd的数值(如果fd被占用会关闭原来的). 新的fd与原fd共用一个文件表指针
三, 原子操作
- 多进程写同步:
- 当多个进程对一个文件进行写操作时, 实际需要两个操作: lseek到结尾 + write. 但是会有时序问题, O_APPEND选项可以解决这个问题, 即每次写时先lseek至结尾. 或者用pread和pwrite解决.
- 延迟写问题:
- 一般write操作会先写到一个缓存区中, 缓存区写满后会进入写队列, 当到达队列头时才开始真正的写IO操作. 而write函数早已返回(在数据拷贝至缓冲区完成). 这可能导致数据没有及时写入磁盘. 一些应用例如数据库等可能需要屏蔽掉这个功能
- 可以通过sync() 或fsync()函数解决这个问题. sync会将所有缓冲区内容同步到磁盘. fsync会将指定文件的缓冲写入磁盘.
- 也可通过一些文件标志位影响这种行为. 如O_SYNC, O_RSYNC和O_DSYNC等, 具体需查man手册
四, fcntl函数
- int fcntl (int fd, int cmd, ...)
- fcntl函数允许只根据fd就对文件属性进行读取/修改/复制, 包括各种标志, 权限和记录锁
- 当使用F_SETFL改变标志位时要注意会覆盖掉原有标志位, 正确的做法是需要先get到原标志位, 再或上新标志位, 然后再进行set
五, 其他
- ioctl函数: 对于一些特殊的设备, 如磁带等, 有一些操作无法通过以上函数描述, 就通过ioctl函数加上各种参数和标志位实现. 每种IO设备都可以指定自己的标志位
- /dev/fd: 可以通过用路径的方式实现一些文件操作, 便于shell命令操作, 例如在命令行中指定标准输入可以用 /dev/fd/0表示