文件操作相关 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 参数可设定文件的权限,取值及其含义如下图所示:

openatfd 参数可以传入一个目录的 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 开始)。

对于 dup2fd2 参数,用于指定新描述符的值,如果 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 的完成才返回。

fdatasyncfsync 类似,但它只更新文件的数据,而 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. 目前市面上比较流行的devops运维平台汇总

    1,spug 1,Spug简介 Spug是面向中小型企业设计的无 Agent的自动化运维平台,整合了主机管理.主机批量执行.主机在线终端.应用发布.任务计划.配置中心.监控.报警等一系列功能.演示地址 ...

  2. Envoy入门实战部署

    一.Envoy介绍 官方文档解释: Envoy是专为大型现SOA(面向服务架构)设置的L7代理和通信总线.该项目源于以下理念:网络对应用程序来说应该是透明的.当网络和应用程序出现问题时,应该很容易确定 ...

  3. Durid的特点

    Durid的特点 1.为什么会有Durid? 创建Druid的最初意图主要是为了解决查询延迟问题,当时试图使用Hadoop来实现交互式查询分析,但是很难满足实时分析的需要.而Druid提供了以交互方式 ...

  4. 如何在Ubuntu Server 18.04 LTS中配置静态IP地址

    安装Ubuntu Server 18.04后需要分配一个的静态IP地址.先前的LTS版本Ubuntu 16.04使用/etc/network/interfaces文件配置静态IP地址,但是Ubuntu ...

  5. 解决误删/bin/bash问题

    出现原因:由于当时误操作把 /bin/bash 命令解释器二进制文件移到了/root 家目录里面,再重新登录系统之后,登陆进去什么也干干不了. 解决办法:让系统重启,以挂载光盘模式进入系统BIOS,选 ...

  6. Pygame的简单总结

    Pygame learn from mooc 私货:在调用函数时,可以 1.import tkinter (不过在使用时还要加前缀如tkinter.Tk()) 2.import tkinter as ...

  7. IDEA 远程debug

    远程debug tomcat 的Catalina.sh 里面有个参数 JPDA_ADDRESS="5555",默认为5555.启动tomcat时,用 ./catalina.sh j ...

  8. 使用Arduino点亮ESP-01S,ESP8266-01S上的板载LED

    因为在开发ESP-01s远程控制中觉得接线麻烦,又因为ESP-01s板子上带有LED灯,那就先点亮板载LED,  如图所示: 打开Arduino 把代码copy进去,再编译烧录,就可以看见LED灯每隔 ...

  9. python之scrapy篇(二)

    一.创建工程 scarpy startproject xxx 二.编写iteam文件 # -*- coding: utf-8 -*- # Define here the models for your ...

  10. 记一次MAVEN依赖事故

    笔者昨天遇到的背景是这样的  MAVEN A模块有一个子模块  需要依赖B模块下的一个子模块  我在B项目内通过mvn deploy上传子模块 但之后在A模块引用  怎么引用都不行  提示 org.a ...