[ APUE ] 第三章 文件系统

Posted cha2azzz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ APUE ] 第三章 文件系统相关的知识,希望对你有一定的参考价值。

1. 文件描述符

打开或创建一个文件时,内核向进程返回一个文件描述符,当读、写一个文件时,用open()或creat()返回的文件描述符标识该文件,将其作为参数传递给write、read。

stdin标准输入 fd = 0
stdout标准输出 fd = 1
stderr标准错误 fd = 2

? 为了可读性可以用 STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO宏来表示。这些宏在<unistd.h>中定义。

? 文件描述符的变化范围 :0~OPEN_MAX-1

2. open 和 openat

#include<fcntl.h>
int open(const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags);
负责**打开或创建**一个文件,成功返回文件描述符,失败返回 -1

path: 要打开或创建文件的名字。

? oflag参数用于说明函数选项,以下一个或多个宏用或运算来表示参数:

? 技术图片
技术图片
技术图片

? open和openat函数返回的文件描述符一定是 最小未使用描述符

? openat的几种使用情况:

  1. path如果是绝对路径名,则忽略dirfd。open = openat

  2. path如果是相对路径名,dirfd指出了相对路径名在文件系统中的开始地址。

  3. path如果是相对路径名,fd参数具有特殊值AT_FDCWD,则路径名在当前工作目录中获取。

    openat的存在的意义?

  4. 线程可以使用相对路径名打开目录中的文件,而不仅仅局限于当前工作目录。让多个线程工作在不同的目录。(但是似乎新版的open里是可以通过相对路径来打开文件的,不知这里是否是老版本的问题)。

  5. 避免TOCTTOU错误(time-of-check-to-time-of-use).

    TOCTTOU错误是指:如果有两个基于文件的系统调用,其中第二个依赖于第一个,那么程序是脆弱的。因为这两次调用不是原子操作,中间可能会发生 第一个调用的结果被改变 导致第二个调用的结果不正确。

    文件名超出NAME_MAX(现在大多是255),会出错返回并设置errno为 ENAMETOOLONG。

3. creat函数

#include<fcntl.h>
int creat(const char *pathname, mode_t mode);

成功返回文件描述符,失败返回-1. mode参数可以是O_CREAT选项下对文件的权限设置。

此函数等价于:

open(path,O_WRONLY|O_CREAT|O_TRUNC,mode);

以前open的选项参数只有0,1,2,所以只需要用creat。

creat只能以只写的方式打开创建的文件,所以要读写文件的话必须先creat,再close,再open,很麻烦,现在可以用这个代替。

open(path,O_RDWR|O_CREAT|O_TRUNC,mode);

4. close函数

#include<unistd.h>
int close(int fd);

关闭一个文件同时也会释放文件的所有记录锁。

当一个进程终止时,内核会自动关闭他打开的文件,所以很多程序不显式调用close函数。

5. lseek函数

#include<unistd.h>
off_t lseek(int fd, off_t offset, int whence);

成功返回当前文件偏移量,出错返回-1.

每一个打开的文件都有一个与之关联的 “当前文件偏移量”,非负整数,计算从文件开始处经历的字节数。通常,读写操作都是从当前文件偏移量处开始,并使偏移量增加所读写字节数。

打开文件时的默认偏移量一般是0,除非使用O_APPEND选项。

offset参数与whence参数之间的关系:

  1. 若whence = SEEK_SET, 将当前文件偏移量设置为 距文件开始处offset个字节。
  2. 若whence = SEEK_CUR, 将当前文件偏移量设置为 偏移量当前值+offset个字节。
  3. 若whence = SEEK_END, 将当前文件偏移量设置为 文件长度+offset个字节。

offset_t curpos = lseek(fd,0,SEET_CUR); 可以用于判断文件的当前文件偏移量。

同时也可以用于判断fd对应的文件能否设置文件偏移量。如果是stdin/stdout/FIFO/PIPE/socket都是不行的,返回-1.

对于某些文件(设备)支持负的偏移量,所以比较返回值的时候不要用<0,而是用== -1.

lseek将当前文件偏移量存在内核里,下次读写时使用,但是它本身不读写

文件偏移量可以大于文件长度,下一次写入将加长文件,这会导致文件中形成一个空洞,范围是从文件的末尾到当前文件偏移量,这里的内容读出来都是0,但实际上空洞并不一定占据存储空间,新写入的数据分配磁盘块,但空洞是没必要分配磁盘块的,这与文件系统的实现有关。

可使用以下代码创建空洞:

int main(){
	int fd;
	if((fd = open("test",O_RDWR|O_CREAT|O_TRUNC,777))==-1){
		err_sys("usage:open:");
	}
	if(write(fd,"12345",5)==-1){
		err_sys("usage:write:");
	}
	if(lseek(fd,100,SEEK_CUR)==-1){
		err_sys("usage:open:");
	}
	if(write(fd,"12345",5)==-1){
		err_sys("usage:write:");
	}
}

使用od命令观察。

技术图片

把偏移量调到10000,发现有空洞文件只分配了8个磁盘块。(使用fallocate -l),而没空洞的10000大小的文件占了12个盘块。

技术图片

文件偏移量可以使用64位的off_t,但是它的大小和文件大小没有必然关系,能创建多大的文件取决于底层的文件系统!

6. read函数

#include<unistd.h>
ssize_t read(int fd, void *buf, size_t count);

成功返回读取字节数,出错返回-1,如果到达文件末尾返回0.(ssize_t 有符号整型,size_t 无符号整型)

造成读取字节数小于要求字节数的情况:

  1. 偏移量与文件末尾距离大于要求字节数。
  2. 终端设备通常一次最多读一行
  3. 从网络读,缓冲机制导致返回值小于要求字节数
  4. 从管道或FIFO读,里面包含多的字节数小于要求的字节数,返回实际读取值
  5. 从面向记录的设备读(如磁带),一次最多返回一个记录。
  6. 中断信号

7. write函数

#include<unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

成功返回写入字节数,失败返回-1.

返回值一般与count大小相同,否则出错。一般是因为硬盘已满或超过给定进程的文件长度限制。

write和read都会改变文件偏移量!

8. I/O的效率

大多数文件系统为改善性能都采取了预读的技术,即当系统检测到正在顺序读取时,实际每次读取字节比要求读取字节数多,用于减少读取次数。

提高缓冲区大小也可以提高效率,但一般大到磁盘块大小就接近瓶颈了。


core dump的由来:早期主存用的是铁氧体磁芯(ferrite core),所以有了core dump这么一说,即内存映像,把程序的内存的镜像存在一个文件里便于测试和诊断问题。


多次读取的文件时,os会把文件暂存在内存里便于读取,之后的读写速度可能好于第一次。但是不同缓冲区长度下进行的读取,其存在内存里的副本是不一样的,所以没有好的影响。

9. 文件共享

内核使用3种数据结构表示打开文件,它们之间的关系表示了文件共享方面一个进程对另一个进程的可能的影响。

  1. 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表。

    每个描述符表相关联的是:

    a. 文件描述符标志(close_on_exec)

    b. 指向一个文件表项的指针。

  2. 内核为所有打开的文件维护一个文件表,每个表包括:

    a. 文件状态标志

    b. 当前文件偏移量

    c. 指向该文件v节点表项的指针。

技术图片

? 3. 每个文件都有一个v节点(v_node)数据结构。它包含了当前文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i_node,索引节点)。这些数据结构里的内容打开文件的时候从磁盘上读到内存的。(Linux没有使用v节点)


v节点的出现时为了对多文件系统类型提供支持,它被称为虚拟文件系统,v节点就是与文件系统无关的i节点部分。linux实现的时候,使用了与文件系统相关的i节点和与文件系统无关的i节点。


技术图片

  1. 如图,进程1在fd3上打开该文件,进程2在fd4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项。文件表项又指向同一个v节点表项(因为一个文件只有一个v节点),每个进程的文件表项不同是为了获取不同的偏移量。

    当然,也可能出现多个fd指向同一个文件表项的情况,比如,可以用dup复制fd。fork也行,父子进程的文件描述符共享同一个文件表项。

    文件描述符是对某一个进程来说的,而文件状态标志影响的是所有指向该文件表项的进程。

10. 原子操作

1.追加到一个文件

假设A、B两个进程都在对一个文件进行追加写入,但不是通过open并采用O_APPEND选项,而是采取古老的lseek+write的方式进行,但是这么做,可能会出现lseek已执行,write未执行,但文件尾端被修改(文件增长了),它再write,就修改了里面的内容,这不是我们想要的。所以我们需要将找文件末尾对应的偏移量+写入作为一个原子操作进行。

(任何一个多于一个函数的调用操作都不是原子操作,因为进程可能会被挂起)

#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
成功返回读取字节数,失败返回-1,若到文件尾返回0
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
成功返回实际写入字节数,失败返回-1

pread类似于 lseek+read操作,但是有所不同:

  1. 调用pread不能中断定位和读操作
  2. 不更新当前文件表项的偏移量

pwrite也类似。

2. 创建文件

open时采用O_CREAT|O_EXCL两个选项,用于将创建文件和测试文件是否存在合并为一个原子操作。不原子的操作可参见open->(errno == ENOENT)-> creat.

原子操作指多步组成的一个操作,只能全部执行或不执行,不可能只执行其子集。

11. dup和dup2函数

#include<unistd.h>
int dup(int fd);
int dup2(int fd,int fd2);

成功返回新的fd,失败返回-1.

dup返回的新文件描述符一定是当前可用文件描述符中的最小数值

dup2可以用fd2参数指定新描述符的值。如果fd2已经打开就关闭,如果fd==fd2,则返回fd2而不关闭。其他情况下,fd2的FD_CLOEXEC文件描述符标志被清除,这样fd在进程调用exec时是打开状态。

这两个函数的参数fd与返回的fd共享同一个文件表项。

每一个fd都有自己的文件描述符标志,正如新描述符的执行时关闭(close-on-exec)标志总是由dup函数清除。

复制一个文件描述符的另一种方式是使用fcntl()函数:

实际上 dup(fd) ;等效于 fcntl(fd,F_DUPFD,0);

dup2(fd,fd2);(不严格)等效于 close(fd2); fcntl(fd,F_DUPFD,fd2);,因为:

  1. dup2是原子的,如果close和fcntl之间调用了信号捕获函数,它可能会修改文件描述符;或者不同的线程也可能会修改。

  2. dup2和fcntl有一些不同的errno

12. sync、fsync和fdatasync

unix内核中设有缓冲区高速缓存或者页高速缓存,大多数硬盘I/O都通过。当我们向文件写入数据时,内核先将数据存到缓冲区,然后排入队列,之后再写入硬盘。这种方式称作延迟写 (delay write)。

当内核需要重用缓冲区存入其他磁盘块数据时,他会将所有延迟写数据写入磁盘。为了保证磁盘上的实际内容与缓冲区内容一致,unix提供了sync、fsync和fdatasync三个函数。

#include <unistd.h>
void sync(void);
int fsync(int fd);
int fdatasync(int fd);

sync只是将修改过的块缓冲区排入写队列,然后返回,它不等待实际写硬盘操作结束

update 系统守护进程周期性调用sync函数。保证定期flush内核的块缓冲区。命令sync也是调动sync函数。

fsync函数只对fd对应文件起作用,并且等待实际写硬盘操作结束,fsync可用于数据库,确保修改过的块立即写到硬盘上。

fdatasync类似fsync,但只影响文件的数据部分。fsync还会同步文件的属性。

13. fcntl函数

#include<fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

成功则返回结果依赖于cmd,失败返回-1.

fcntl函数有以下几种功能;

  1. 复制已有描述符(cmd = F_DUPFD或F_DUPFD_CLOEXEC)

  2. 获取、设置文件描述符标志(cmd = F_GETFD/F_SETFD)

  3. 获取、设置文件状态标志(cmd = F_GETFL /F_SETFL)

  4. 获取、设置异步IO所有权(cmd = F_GETOWN/F_SETOWN)

  5. 获取、设置记录锁( cmd = F_GETLK/F_SETLK或F_SETLKW)

技术图片

技术图片

O_SYNC可以保证等待文件IO完成,即已写到硬盘上再返回。unix系统中,write通常只把数据排入队列,实际写入硬盘可能在以后某个时刻进行,但数据库系统要防止系统异常导致队列中数据丢失,所以需要write一返回就能确定数据确实写入硬盘。

F_SETFL/F_GETFL 都很重要,可以用这俩写个set_fl和get_fl函数。

O_SYNC标志会增加系统时间和时钟时间。

linux系统不允许通过fcntl函数打开O_SYNC选项,但是只提示失败不返回出错。(所以没设的话就老老实实调fsync或fdatasync )。

14.ioctl函数

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

它是IO操作的杂物箱,终端IO操作用它比较多。

它虽然是不定参数的,但参数一般只有一个,一般是指向一个变量或结构的指针。

每个设备驱动程序可以定义它自己的一组专用ioctl命令。系统则为不同种类设备提供通用ioctl命令。

15. /dev/fd

打开/dev/fd/n等效于复制描述符n(若描述符n打开)。

fd = open("/dev/fd/0",mode); 等效于 fd = dup(0);

打开文件的mode必须是初始打开模式的一个子集,多的会被忽略,不报错。

fd = open("/dev/fd/0",O_RDWR); 即便这样stdin还是只读。

不过linux的实现比这个要特别,它不是忽略,而是/dev/fd里的文件描述符被映射指向底层文件的符号链接,所以返回的fd的mode和/dev/fd本身的mode无关。

习题以后再补

技术图片
技术图片




以上是关于[ APUE ] 第三章 文件系统的主要内容,如果未能解决你的问题,请参考以下文章

APUE 学习笔记1: Unix文件IO

每天看点英文文档:APUE第三版

每天看点英文文档:APUE第三版

每天看点英文文档:APUE第三版

Linux - Unix环境高级编程(第三版) 源代码编译(即头文件apue.h如何使用问题)

APUE读书笔记-第四章 文件和目录