这一章的IO是基础IO,不带缓冲的IO,每个read和write都调用内核中的一个系统调用,包含open,read,write,lseek以及close几个函数。
只要设计多个进程间的共享资源,原子操作就很重要,这一章通过文件IO与相关函数来讨论,进一步讨论多个进程间如何共享文件,以及说明dup,fcntl,sync等函数
文件描述符
对于内核而言,所有打开的文件都通过文件描述符来引用,文件描述符是一个非负整数,当打开或者创建一个文件的时候,内核向进程返回一个文件描述符,这个描述符将用于读写等操作来标识这个文件。按照管理,文件描述符0,1,2分别与标准输入,标准输出以及标准错误相关,不过我们通常将他们替换成STDIN_FILNO,STDOUT_FILNO与STDERR_FILNO,这样做是为了提高可读性。
对于linux来说,文件描述符的变化范围几乎是无限的,他只受制于系统配置的存储器总量,整形字长等因素,所以一般来说最多打开65535个
open与openat
这两个函数用于打开或者创建一个文件
int open(const char *pathname, int flags, / *mode_t mode */);
int openat(int dirfd, const char *pathname,/ * int flags, mode_t mode * /);
最后一个参数是可变的,只有在创建一个文件的时候才关心最后一个参数
第一个参数path是要打开或者创建的文件名字,关于这个名字是绝对路径名还是相对路径名的区别在下面两个函数的区别的时候在做讨论。
第二个参数是这个函数的选项,以下几个是必须选定一个的
O_RDONLY(只读打开) O_WRONLY(只写打开)O_RDWR(读写打开)
O_EXEC(只执行打开) O_SEARCH(只搜索打开)
下面的是非必选项
O_APPEND:每次都追加到文件的尾端,通过设置偏移量到文件末尾实现
O_CLOEXEC:把这个常量设置为文件描述符标志
O_CREAT:若文件不存在就创建,要是设置了这个的话一定要设置第三个参数赋权限
O_NOCTTY:如果path引用终端设备就不将该设备分配作为此设备的控制终端
O_NOFOLLOW: 如果path一个符号链接,就报错
O_NONBLOCK:如果文件是一个FIFO,块特殊字符文件,那就设置为非阻塞方式
open与openat返回的是最小的未使用描述符,我们可以用这个特性来关闭某些描述符然后重定向
open与openat的区别
根据上面函数原型我们可以看出来,openat多了一个fd参数,共有三种可能
- path参数指定的是绝对路径名,在这种情况下fd被忽略,两个函数等同
- path指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址
- path参数指定相对路径名,fd参数具有特殊值AT_FDCWD在这种情况下,路径名在当前工作目录中获取,openat函数操作上与open函数类似
那么为什么会有openat函数呢,openat函数是为了解决两个问题,第一个是让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。第二个是可以避免TOCTTOU错误,TOCTTOU错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖第一个的结果,那么程序是脆弱的,因为两个函数并不是原子操作,所以两个函数之间文件改变了就造成了调用不再有效。
文件名和路径截断
文件名最大值是由NAME_MAX设置的,可是如果NAME_MAX是14,我们试图在目录中创建一个文件名包含15个字符的话,那么在早期system V情况下是截断成14个字符并且不返回任何出错状态,在BSD类的系统下就返回出错状态,可是如果我们的文件名恰好是14个字符的话我们就不知道他是不是截断过的文件名了,这样会引起混乱。
在posix中常量_POSIX_NO_TRUNC决定要截断路径名还是出错,如果常量有效,那么整个路径名超过NAME_MAX。
creat与close
creat
creat是用于创建一个新文件
int creat(const char *pathname, mode_t mode);
用法与open函数大致相同
其实我们可以发现open函数可以完成creat的功能
creat函数等价于open(path,O_WRONLY | O_TRUNC | O_CREAT)
而且我们可以发现creat创建的是只写,当我们要读的时候我们要先creat再close再open
这样太麻烦了我们可以直接open的时候选择O_CREAT创建然后O_RDWR给读写权限即可
close
close函数用于关闭一个文件
int close(int fd);
当关闭一个文件的时候还会释放该进程上所有记录锁
当一个进程终止的时候 ,内核会关闭它打开的所有文件。
lseek与文件偏移量
每一个打开的文件都有一个当前的文件偏移量,它通常是一个非负整数,用以度量从文件开始处计算的字节数,通常,读写操作都是从当前文件偏移量开始的,并使偏移量增加所读写的字节数,按照系统默认的情况,当打开一个文件时除非是O_APPEND是追加打开否则偏移量都是被设置0
lseek
lseek函数可以用来设置一个文件的偏移量
off_t lseek(int fd, off_t offset, int whence);
对参数offset的解释与参数whence的值有关
- 若whence是SEEK_SET(0)被叫做绝对偏移量,那么该文件的偏移量设置为距文件开始处offset个字节
- 若whence是SEEK_CUR(1)相对于当前位置的偏移量,那么该文件的偏移量设置为当前值加上offset,offset可正可负
- 若whence是SEEK_END(2)相对于末尾位置的偏移量,那么该文件的偏移量设置为末尾值加上offset,offset可正可负
若lseek成功执行,则返回新的文件偏移量,如果文件描述符是一个管道或者套接字那么lseek就返回-1,并将errno设置为ESPIPE
这里要切记一点由于lseek可以为负数所以在返回比较的时候比较是否为-1而不是小于零
lseek有以下特点:
- lseek仅将当前文件偏移量记录在内核中,不引起任何的IO操作
- 文件偏移量可以大于当前的文件长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件称构成一个空洞,所有没被写过的字节都读作0,并且他们并不占用存储区,这种文件被叫做空洞文件。
read与write
read与write函数主管对文件的读和写操作
read
ssize_t read(int fd, void *buf, size_t count);
第一个参数fd是要操作的文件描述符
第二个参数buf是要读到的区域
第三个参数count是要读的个数
成功返回读到的字节数,若已经读到末尾就返回0
有多种情况可使实际读到的字节数少于要求读到的字节数
- 读普通文件的时候,在读到要求字节数之前已经读到了文件末尾
- 当从终端读的时候,通常一次最多读一行
- 当从网络读的时候,网络中的缓冲机制可能造成返回值小于要求的字节数
- 当从管道或FIFO读的时候,如果管道中包含的字节少于所需的字节数,那返回实际读到的字节个数
- 当从某些面向记录的设备(比如磁带)读时,一次最多返回一个记录
- 当一信号造成中断的时候,而已经读了部分数据
write
write函数用来向打开文件写入数据
ssize_t write(int fd, const void *buf, size_t count);
若成功返回写的字节数,失败返回-1
write通常失败的原因是磁盘写满或者超过了一个给定进程的文件长度限制
对于普通文件来说,写操作从文件当前偏移量开始,默认偏移量是0,O_APPEND则是指向文件末尾,从末尾追加写入。
文件共享
接下来我们来看看在不同进程间共享打开文件,这里我们一定先要理解文件系统
文件系统
内核使用三中国数据结构来表示打开的文件,他们之间的关系决定了在文件共享方面一个进程对另一个进程产生的影响
每一个进程在进程表中有一个记录项,记录项中包含一张打开的文件描述符表,其实就是进程PCB(Linux下是task_struct)中维护了一张文件描述符表,然后这个表中每一个文件描述符都匹配一个文件指针,指向打开的文件的文件表项,这个文件表项包含:
- 文件的状态标志(读写添加,同步阻塞等)
- 当前文件的偏移量
- 只想该文件的v节点
其中文件的v节点包含了文件类型核对此文件进行各种操作函数的指针以及该文件的i节点。我们用下图来说明这几个数据结构的关系
当我们了解了文件系统后,接下来我们来看看如何文件共享以及需要注意的问题
当我们用两个不同的进程打开一个文件的时候,首先两个进程会创建两张文件表项,并且他们有自己的偏移量,
我们假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件. 打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项.之所以每个进程都获得自己的文件表项,是因为这可以使每一个进程都有它自己的对该文件的当前偏移量.给出了这些数据之后,现在对前面所述的操作进一步说明.
- 在完成每个write后,在文件表项中的当前文件偏移量既增加所写入的字节数. 如果这导致当前文件偏移量超出了自己当前文件长度. 则将i节点表项中的当前文件长度设置为当前文件偏移量(文件加长).
- 如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中. 每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度,这就使得每次写入的数据都追加到文件的当前尾端处.
- 若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度.
- lessk函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作.
dup与sync
dup
dup函数族是用于复制一个现有的文件描述符(重定向)
int dup(int oldfd);
int dup2(int oldfd, int newfd);
由dup函数返回的新文件描述符一定是当前可用文件描述符中的最小值,对于dup2,可以用fd2参数指定的新描述符的值如果fd2已经打开,就将他关闭,如果fd等于fd2,则dup2返回否都,而不关闭它。这些函数返回的新文件描述符与参数fd共享一个文件表项
sync
当我们向文件写入数据的时候,内核先将数据复制到缓冲区,然后排入队列,晚些时候再写入磁盘,这种被叫做延迟写。当内核需要重用缓冲区来存放其他磁盘块数据的时候,他会把所有延迟写数据写入磁盘,sync只是将所有修改过的块缓冲区排入写队列,然后陷入阻塞等待。
int syncfs(int fd);
成功就返回0,失败返回-1