文件操作相关 API:open, read, write, lseek, close.

多进程共享文件的相关 API:dup, dup2, fcntl, sync, fsync, ioctl.

文件操作 API

open and 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 */ );
// Both return: file descriptor if OK, −1 on error

oflag 是下列选项的组合(通过或运算 | ):

  • 必选且只能选择一个:O_RDONLY, O_WRONLY, O_RDWR
  • 可选项
    • O_APPEND: 写文件时追加到尾端。
    • O_CLOEXEC
    • O_CREAT: 文件不存在时创建;若使用该选项,需要 mode 参数,指定文件到访问权限。
    • O_DIRECTORY: 如果 path 不是目录,出错。
    • O_EXCL: 如果同时指定了 O_CREAT,而文件已存在,则出错。
    • O_NOCITY
    • O_NOFOLLOW: 如果 path 是一个符号链接,则出错。
    • O_NOBLOCK:如果 path 是一个 FIFO、一个块特殊文件或一个字符特殊文件,则为本次打开操作和后续的I/O操作设置非阻塞模式 (Nonblocking Mode) .
    • O_SYNC: 使每次 write 操作等待物理 I/O 完成,包括由该 write 操作引起的文件属性的更新所需要的 I/O 。
    • O_DSYNC
    • O_RSYNC
    • O_TRUNC:如果文件存在,且打开模式可写,那么长度截断为 0 。
    • O_TTYINIT

mode 参数可设定文件的权限,取值及其含义如下图所示:

openat 的 fd 参数可以传入一个目录的 fd:

int dir = open(".", O_RDONLY | O_DIRECTORY);
int fd = openat(dir, "test.c", O_RDONLY);

creat

函数原型:

int creat(const char *path, mode_t mode);
// Returns: file descriptor opened for write-only if OK, −1 on error

creat 函数相当于:open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);

mode 表示文件的访问权限,将在后续章节解析。

close

函数原型:

int close(int fd);
// Returns: 0 if OK, −1 on error

关闭文件,释放该进程加在该文件上的所有记录锁。

进程结束时,内核会自动关闭所有它打开的文件,所以 close 有时候可有可无。

lseek

off_t lseek(int fd, off_t offset, int whence);
// Returns: new file offset if OK, −1 on error

对于 offset 参数的解析,取决于 whence 的值:

  • SEEK_SET:该文件的偏移量设置为距文件开始处的 offset 个字节
  • SEEK_CUR:该文件的偏移量设置为当前位置加上 offset 的值,这时候 offset 可为负数。
  • SEEK_END:该文件的偏移量设置为文件长度加上 offset 的值,这时候 offset 可为负数。

如果 lseek 成功执行,返回新的文件偏移量,否则返回 -1 。如果 fd 指向的是一个 FIFO、管道或者 socket,lseek 返回 -1,并把 errno 设置为 ESPIPE (Illegal Seek) . lseek 不引起任何 IO 操作,仅仅把当前偏移量记录在内核当中,用于下一次的读写操作。

例子1

int main()
{
if (lseek(STDIN_FILENO, 0, SEEK_CUR) != -1) puts("can seek");
else puts("can not seek");
}

运行结果:

$ ./a.out < /etc/passwd
can seek
$ cat /etc/passwd | ./a.out
can not seek

< 符号的作用是重定向输入。

一般情况下,当前偏移量应当为非负数,但某些设备(Linux中一切皆文件)允许它为负数。此外,偏移量可以大于文件长度,这种情况下,对文件的下一次写操作将「加长」文件,在文件中形成一个「空洞」(字节均值为 0 ),空洞不一定会占据磁盘空间,具体取决于文件系统的实现。

例子2:空洞文件

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
int main()
{
int fd = -1;
if ((fd = creat("file.hole", FILE_MODE)) == -1) err_sys("creat error");
if (write(fd, buf1, 10) != 10) err_sys("write error");
if (lseek(fd, 16384, SEEK_SET) == -1) err_sys("lseek error");
if (write(fd, buf2, 10) != 10) err_sys("write2 error");
// now offset is at 16394
}

运行结果:

$ ll file.hole
-rw-r--r-- 1 sinkinben sinkinben 16394 1月 20 15:20 file.hole
$ od -c file.hole
0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0040000 A B C D E F G H I J
0040012

od -c 以八进制输出文件内容,hex(16394) = 0x400a .

创建一个同样长度但没有空洞的文件 file.nohole:

$ ls -sl file.*
8 -rw-r--r-- 1 sinkinben sinkinben 16394 1月 20 15:31 file.hole
20 -rw-r--r-- 1 sinkinben sinkinben 16394 1月 20 15:31 file.nohole

可以看出,file.nohole 占据了 20 个磁盘块。

read

函数原型:

ssize_t read(int fd, void *buf, size_t nbytes);
// Returns: number of bytes read, 0 if end of file, −1 on error

可能存在返回值(实际读到的字节数)小于要求读取的字节数 nbytes 的情况:

  • 读取普通文件:当前 offset 离文件末端只有 30 字节,而要求读取 100 字节。
  • 读取终端设备:通常一次最多读取一行。
  • 从网络 socket 中读取:网络的缓冲机制可能造成上述情况。
  • 从管道或者 FIFO 读取:与读取普通文件类似,剩下的字节数不足。
  • 当某一信号造成读取中断。

read 操作一般都会采用预读机制 (Read Ahead) 提高性能,预读的数据放入到 Cache 当中,那么下一次读取就不用读取磁盘。

write

函数原型:

ssize_t write(int fd, const void *buf, size_t nbytes);
// Returns: number of bytes written if OK, −1 on error

与 read 操作类似。返回值通常与 nbytes 相等,否则表示出错。出错的原因可能为磁盘已满,或者超过一个进程的文件长度限制。

文件共享

在 Unix 系统中,内核为每个进程都建立了一个**文件描述符表 (即下图的 Process Table Entry, 名字是我自己翻译的) **,进程打开某个文件都过程如下图所示。

进程的每一个 fd 都有对应的文件指针 (File Pointer) 指向某一个文件表项 (File Table Entry) ,该表项包括当前打开文件的状态信息和一个 v-node 指针。其中 v-node 包含了文件的类型和操作该文件的函数指针等信息,还包括一个指向文件 inode 的指针。

如下图所示,如果 2 个进程同时打开了同一个文件,那么这 2 个 File Table Entry 的 v-node 指针将会指向同一个 v-node 。由图中的过程可以看出,不同进程打开同一文件,每个进程对文件的偏移量是独立的,文件的状态信息 (File Status Flags) 也是独立的。

基于这个过程,可以对上述对一些 IO 操作的特征进行解析:

  • 完成一次 write 操作后,File Table Entry 中的 offset 将会增加写入的字节数。如果当前的 offset 超过了 i-node 中的文件大小 (current file size) ,那么就将 current file size 设置为当前的 offset 。
  • 使用 O_APPEND 打开一个文件,File Table Entry 中的 file status flags 会记录这个 O_APPEND 。每次 write 操作执行时,首先会把 current file offset 设置为 i-node 中的 current file size。
  • 若使用 lseek 定位到文件末端,则会把 offset 设置为 file size 。
  • lseek 只修改 File Table Entry 中的 offset,不进行任何 IO 操作。

如果进程进行了 fork 操作,那么 Process Table Entry 中的文件描述符表也会被子进程拷贝,所以也有可能有多个 File Pointer 指向同一个 File Table Entry 。类似,dup 操作也会使得同一进程中的 2 个不同的 fd 指向同一个 File Table Entry。

原子操作

在多进程场景下,需要对同一个日志文件进行写操作,那么就有可能会出现进程 A 的内容被进程 B 的内容覆盖的情况(因为文件偏移量是独立的)。

因此,写操作需要实现为一个原子操作(要么全做,要么全不做),才能满足上述场景的要求。

pread and pwrite

函数原型:

ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
// Returns: number of bytes read, 0 if end of file, −1 on error
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
// Returns: number of bytes written if OK, −1 on error

作用:从离文件开始处的 offset 位置开始,读取 nbytes 个字节。

pread 的行为相当于调用 lseek 后再次调用 read ,但 pread 是一个原子操作,这意味着:

  • 调用 pread 过程中,无法中断其定位 lseek 和 read 操作。
  • 不更新当前的文件偏移量。

pwrite 与之类似。

dup and dup2

int dup(int fd);
int dup2(int fd, int fd2);
// Both return: new file descriptor if OK, −1 on error

作用:把 fd 复制为一个新的描述符。如果传入的 fd 无效,那么返回 -1 。

dup 返回的总是可用对文件描述符中的最小值(也就是从 3 开始)。

对于 dup2 的 fd2 参数,用于指定新描述符的值,如果 fd2 已经打开,会先关闭它:

  • fd1 == fd2 : 返回 fd2,且不关闭它。
  • 如果 fd1 无效,那么返回 -1.
  • 如果 fd1 有效,那么把 fd1 复制为 fd2 ,返回 fd2 。

如下图所示,经过 dup 操作后,会有多个文件指针指向同一个 File Table Entry。

sync, fsync and fdatasync

现在的计算机通常都会有 Cache,为了提高 IO 性能,除了在 read 一小节提到的预读机制外,还有延迟写机制 (Delayed Write) 。当我们向文件写数据时,首先会拷贝到高速缓冲区当中,后面再把高速缓冲区中的数据写到磁盘上(通过排队 FIFO 的顺序)。

在某些场景下,我们需要缓冲区的数据和磁盘的数据保持一致。因此需要 sync, fsync, fdatasync 这三个函数。

函数原型:

int fsync(int fd);
int fdatasync(int fd);
// Returns: 0 if OK, −1 on error
void sync(void);

sync 的作用:

  • The sync function simply queues all the modified block buffers for writing and returns; it does not wait for the disk writes to take place. (不等待磁盘操作完成)

  • sync is normally called periodically (usually every 30 seconds) from a system daemon, often called update. The command sync also calls the sync function.(sync 通常由系统的一个守护进程 update 来周期性调用,命令 sync 也会调用这个函数。)

fsync 只对 fd 这一个文件实现同步操作,并且等待磁盘 IO 的完成才返回。

fdatasync 与 fsync 类似,但它只更新文件的数据,而 fsync 还会更新文件的属性(包括权限信息等)。

fcntl and ioctl

函数原型:

int fcntl(int fd, int cmd, ... /* int arg */ );
// Returns: depends on cmd if OK (see following), −1 on error
int ioctl(int fd, int request, ...);
// Returns: −1 on error, something else if OK

fcntl 可以改变文件 fd 的属性信息。ioctl 一般用于外部设备(比如实现驱动程序) 的 IO 操作。

总结

APUE 看得好无聊,看着看着就想睡觉。

[APUE] 文件 I/O的更多相关文章

  1. APUE 文件IO

    文件 IO 记录书中的重要知识和思考实践部分 Unix 每个文件都对应一个文件描述符(file descriptor),为一个非负整数,一个文件可以有多个fd, 后面所有与文件(设备,套接字等)有关操 ...

  2. [apue] 文件中的空洞

    空洞的概念 linux 上普通文件的大小与占用空间是两个概念,前者表示文件中数据的长度,后者表示数据占用的磁盘空间,通常后者大于前者,因为需要一些额外的空间用来记录文件的某些统计信息或附加信息.以及切 ...

  3. [APUE]文件和目录(下)

    一.mkdir和rmdir函数 #include <sys/types.h> #include <sys/stat.h> int mkdir(const char *pathn ...

  4. [APUE]文件和目录(中)

    一.link.unlink.remove和rename 一个文件可以有多个目录项指向其i节点.使用link函数可以创建一个指向现存文件连接 #include <unistd.h> int ...

  5. [APUE]文件和目录(上)

    一.文件权限 1. 各种ID 我在读这一章时遇到了各种ID,根据名字完全不清楚什么意思,幸好看到了这篇文章,http://blog.csdn.net/ccjjnn19890720/article/de ...

  6. APUE 文件和目录

    文件和目录 Unix 所有的文件都对应一个 struct stat,包含了一个文件所有的信息. #include <sys/stat.h> struct stat { mode_t st_ ...

  7. APUE ☞ 文件和目录

    粘着位(Sticky Bit) S_ISVTX位被称为粘着位.如果一个可执行程序文件的这一位被设置了,程序第一次运行完之后,程序的正文部分的一个副本仍被保存在交换区(程序的正文部分是机器指令).这使得 ...

  8. APUE学习笔记-一些准备

    从开始看APUE已经有快一个星期了,由于正好赶上这几天清明节放假,难得有了三天空闲假期可以不受打扰的学习APUE,现在已经看完前六章了,里面的大部分例程也都亲自编写,调试过了.但总觉得这样学过就忘,因 ...

  9. 配置apue的头文件apue.h和unp的头文件anp.h

    配置apue的头文件apue.h和unp的头文件anp.h 如果要使用gcc -g 来生成可调试文件一定要修改Make.defines.linux文件中的CFLAGS变量 修改为:CFLAGS=-an ...

随机推荐

  1. 【Harbor学习笔记】-教你快速搭建Docker私有仓库

    目录 架构图 Harbor依赖的外部组件 Harbor自有组件 核心组件 安装 1. 下载离线安装包 2. 配置 harbor.cfg (harbor.yml) 3. 启动 Harbor 安装配置问题 ...

  2. 系统类 字符串String类

    java编程语言中的字符串. 在java中,String类是不可变的,对String类的任何改变,都是返回一个新的String类对象. String 对象是 System.Char 对象的有序集合,用 ...

  3. jmeter处理加密接口

    jmeter处理加密接口 普及知识点: 我们常见的加密方式有:(RSA .AES.MD5.secret) 如下四种情况: 1.请求加密.响应加密 2.请求不加密.响应加密 4.请求加密.响应不加密 5 ...

  4. [日常摸鱼]bzoj3224普通平衡树-Treap、Splay、01Trie、替罪羊树…

    http://www.lydsy.com/JudgeOnline/problem.php?id=3224 经典的平衡树模板题-各种平衡树好像都可以(黄学长之前好像还用vector卡过了这题) 所以这篇 ...

  5. 制作3D小汽车游戏(上)

    之前一段时间家里和公司的事太多,一直没有时间写博客,最近腾出一段时间,看了一遍官方的examples,收货颇多,想整理一点东西出来,又苦于没有好的东西,three写点东西真是太难了.好吧,今天郭先生就 ...

  6. PHP连接数据库的方式

    1,旧的方式 - mysql(面向过程) 1 <?php 2 //定义连接参数 3 $host = 'localhost'; 4 $user = 'root'; 5 $password = '' ...

  7. vue 动态注册路由 require.context

    需求场景: 在日常的功能练习和调试过程中,需要一个demo项目进行功能测试,由于频繁.vue页面的同时,又要再router.js文件里面注册路由,感觉有点无聊和枯燥.基于此出发点,考虑能否自动读取文件 ...

  8. 测试平台MeterSphere源码入门

    前端代码结构 ├── package.json #Vue的模块依赖定义 ├── pom.xml #Maven前后端打包的定义与依赖 ├── public ├── src #前端核心代码 │ ├── a ...

  9. 通过`RestTemplate`上传文件(InputStreamResource详解)

    通过RestTemplate上传文件 1.上传文件File 碰到一个需求,在代码中通过HTTP方式做一个验证的请求,请求的参数包含了文件类型.想想其实很简单,直接使用定义好的MultiValueMap ...

  10. 【mybatis-plus】条件查询

    用mp也可以方便的实现稍复杂点的条件查询,当然了很复杂的就还是要xml编写sql了. 一.wapper介绍 先看下mp的条件构造抽象类的结构: Wrapper: 条件构造抽象类,最顶端父类 Abstr ...