1.文件描述符

对于内核而言,所有打开的文件都以文件描述符引用。

文件描述符是一个非负整数,用于描述一个文件。在打开或者创建一个文件的时候,内核向进程返回一个文件描述符,在读、写一个文件的时候,使用open或者creat返回的文件描述符标识这个文件,作为参数传送给read或者write。

一般来说,UNIX系统将文件描述符0与标准输入关联,1与标准输出关联,2与标准错误关联。在头文件中将他们分别替换成STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO以提高可读性。

文件描述符的范围是0~OPEN_MAX-1。早期的UNIX上限值是20,目前许多系统将上限值增加到64。

2.I/O函数

2.1 open和openat

  1. #include <fcntl.h>
  2. int open(const char *pathname, int flags, ... /*mode_t mode*/);
  3. int openat(int fd,const char* path,int flags, ... /*mode_t mode*/);

最后一个参数…,表示余下参数的数量和类型是可变的。

2.1.1 path

path参数表示要打开创建文件的名字。

2.1.2 flags

flags参数表示函数的多个选项,用多个预定常量进行或运算构成参数。
其中,下面五个参数必须指定且只能指定一个

  • O_RDONLY,只读打开
  • O_WRONLY,只写打开
  • O_RDWR,读,写打开
  • O_EXEC,只执行打开
  • O_SEARCH,只搜索打开,应用于目录

其他参数则是可选的。列举一部分用于参考,完整参数列表参考链接

  • O_APPEND:写时追加到文件末端
  • O_CREAT:若文件不存在,则创建它
  • O_DIRECTORY:若path引用的不是目录,则报错

还有一些特殊的同步输入、输出选项。如

  • O_DSYNC:每次write要等待物理I/O完成。但如果写操作不影响读取刚写入的文件,那么不需要等待文件属性被更新
  • O_RSYNC:使每一个以文件描述符作为参数进行的read操作等待,直到所有对文件同一部分挂起的写操作都完成。

参考:https://www.tutorialspoint.com/unix_system_calls/open.htm

由open或者openat函数返回的文件描述符返回的一定是最小的未用的文件描述符数值。因此某些程序可以应用于标准输入输出或者错误上打开新的文件。
比如,关闭了标准输入(通常是文件描述符0),这个时候打开一个文件,它返回的文件描述符必定出现于0。

2.1.3 fd

fd参数将open和openat区分开,有三种情况

  • path参数指定的是绝对路径名。这个时候,fd参数被忽略,openat等同于open
  • path参数指定的是相对路径名。这个时候,fd参数指出了相对路径名在文件系统中的开始位置。fd通过打开相对路径名所在的目录来获取。
  • path指定了相对路径名,fd参数是特殊值AT_FDCWD。这个时候,路径名在当前工作目录中获取。

openat希望解决的两个问题。第一,让线程可以用相对路径名打开文件中的目录:同一部分进程的所有线程共享当前工作目录,很难让统一进程的不同线程在同一时间工作在不同的目录中。第二,避免time-of-check-to-time-of-use错误。基本思想是,如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么这不是线程安全的,因为两个调用不是原子操作。

2.2 creat

creat用于创建一个新文件,并返回这个文件的文件描述符。
creat的不足之处是以只读的权限打开创建的文件。因此,如果要直接写这个文件的话,需要close之后再调用open。
如果需要实现以只写的方式创建并打开文件,creat() 等效于有 O_CREAT|O_WRONLY|O_TRUNC 的flag参数的 open()。

2.3 close

close用于关闭一个已打开的文件

  1. #include <unistd.h>
  2. //若成功则返回0,否则返回1
  3. int close (int fd);

当一个进程终止时,内核自动关闭它所打开的所有文件。

2.4 lseek

lseek用于显示的设置一个已经打开的文件的偏移量。
如O_APPEND选项,偏移量设置为文件的末尾。默认情况下,文件的偏移量为0。

  1. //若成功,返回新的文件偏移量;出错则返回-1
  2. off_t lseek(int fd,off_t offset,int whence);

whence参数表示

  • SEEK_SET:将文件偏移量设置为距文件开始处的offset个字节
  • SEEK_CUR:将当前文件的偏移量增加offset个字节,可以为正或者是负
  • SEEK_END:将文件的偏移量设置为文件长度加offset

若lseek调用成功,则返回当前文件的偏移量。因此,可以通过将offset设置为0来获取当前文件的偏移量。

  1. off_t curpos;
  2. curpos = lseek(fd,0,SEEK_CUR);

这也可以确定所涉及的文件是否可以设置偏移量,如管道、FIFO或者网络套接字,则返回-1,并将errno设置为ESPIPE。

2.5 read

read函数从打开文件中读取数据。
read从当前偏移量开始读,当返回结果时,将偏移量增加到实际读到的字节数。

  1. //返回值为读到的字节数。若已经到文件尾部,则返回0;若出错,则返回-1
  2. ssize_t read(int fd,void *buf,size_t nbytes);

有多种情况使实际到达的字节数小于要求读的字节数:

  • 读普通文件时,已经到达了文件尾端。如文件大小为30字节,要求读100字节,则返回30。再次调用read,返回0。
  • 从终端设备读时,一般一次只读一行。
  • 从网络读时,缓冲机制可能造成返回值小于所要求读的字节数
  • 从管道或FIFO读时,如果管道包含的字节小于所需的数量,只返回实际可用的字节数
  • 从某些面向记录的设备,如磁盘读时,一次最多返回一条记录
  • 当一个信号造成中断,只读取了部分时

2.6 write

wrtie用于向打开文件写数据

  1. // 返回实际返回的字节数
  2. ssize_t write(int fd,const void *buf,size_t nbytes);

对于普通文件,写操作从文件的当前偏移量开始。如果打开文件前指定了O_APPEND选项,则写操作之前都会将偏移量设置到文件结尾处。
在一次成功写之后,该文件偏移量增加实际写的字节数。

2.7 I/O效率

  1. //mycat.c
  2. #include "apue.h"
  3. #define BUFFSIZE 4096
  4. int
  5. main(void)
  6. {
  7. int n;
  8. char buf[BUFFSIZE];
  9. while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
  10. if (write(STDOUT_FILENO, buf, n) != n)
  11. err_sys("write error");
  12. if (n < 0)
  13. err_sys("read error");
  14. exit(0);
  15. }

该程序从标准输入读,并输出到标准输出中,实现了类似cat的功能。在UNIX系统的shell中,在标准输入上打开一个文件用于读,并创建或充血一个标准输出文件,使得程序不需要打开标准输入输出文件,并允许用户利用shell的I/O重定向功能。

关于BUFFSIZE的值如何选取,表中利用了不同的BUFFSIZE进行了测试,读516 581 760字节的文件所得到的结果。

基础IO函数 - 图1

这个测试所用的文件系统是Linux ext4,磁盘块长度为4096字节,由st_blksize定义。因此,在测试的表中,系统CPU时间最小值差不多出现在4096及其之后的位置,继续增加缓冲区长度对时间没有影响。

大多数文件系统为了改善性能,都使用了预读的优化方式。也就是监测到系统正在进行顺序读取的时候,系统会尝试读入比应用所要求的更多的数据,进行预缓冲。在缓冲区大小调至32字节之后,时钟时间和较大缓冲区的始终时间几乎一样。

2.8 fcntl

fcntl函数可以修改已打开文件的属性。

  1. // 若成功则根据cmd返回,出错则返回-1
  2. int fcntl(int fd,int cmd,.../*int arg*/);

fcnt1函数有以下5种功能。
(1)复制一个已有的描述符(cmd=F_DUPED或F_DUPED_CLOEXEC)。
(2)获取/设置文件描述符标志(cmd=F_GETED或F_SETED)。
(3)获取/设置文件状态标志(cmd=F_GETEL或 E_SETFL)。
(4)获取/设置异步I/O所有权(cmd= F_GETOWN或F_SETOWN)。
(5)获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)。

  • FDUPFD,返回新的文件描述符。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第3个参数值(取为整型值)中各值的最小值。新描述符与共享同一文件表项(见图3-9)。但是,新描述符有它自己的一套文件描述符标志,其FD CLOEXEC文件描述符标志被清除(这表示该描述符在exec时仍保持有效)
  • F_DUPED_CLOEXEC,复制文件描述符,设置与新描述符关联的 FD CLOEXEC文件描述符标志的值,返回新文件描述符。
  • FGETFD,对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD CLOEXEC。
  • F_SETFD,对于设置文件描述符标志。新标志值按第3个参数(取为整型值) 设置
  • FGETFL,对应于的文件状态标志作为函数值返回。我们在说明。open函数时, 已描述了文件状态标志。遗憾的是,5个访问方式标志(O_RDONLY、 O_WRONLY、O_RDNR、 O_EXEC 以及O SEARCH)并不各占1位(如前所述,由于历史原因,前3个标志的值分别是0、1和2。这5个值互斥,一个文件的访问方式只能取这5个值之一)。因此首先必须用屏蔽字 O ACCMODE取得访问方式位然后将结果与这5个值中的每一个相比较。
    基础IO函数 - 图2
  • FSETFL,将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O _APPEND、O NONBLOCK、OSYNC、O _DSYNC、O RSYNC、D ESYNC和 D ASYNC。
  • F_GETOWN,获取当前接收 SIGIO和 SIGURG信号的进程D或进程组ID。
  • F_SETOWN,设置接收 SIGIO和SIGURG信号的进程D或进程组ID。正的arg指定一个进程D,负的arg表示等于g绝对值的一个进程组ID。
  1. #include "apue.h"
  2. #include <fcntl.h>
  3. int
  4. main(int argc, char *argv[])
  5. {
  6. int val;
  7. if (argc != 2)
  8. err_quit("usage: a.out <descriptor#>");
  9. if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
  10. err_sys("fcntl error for fd %d", atoi(argv[1]));
  11. switch (val & O_ACCMODE) {
  12. case O_RDONLY:
  13. printf("read only");
  14. break;
  15. case O_WRONLY:
  16. printf("write only");
  17. break;
  18. case O_RDWR:
  19. printf("read write");
  20. break;
  21. default:
  22. err_dump("unknown access mode");
  23. }
  24. if (val & O_APPEND)
  25. printf(", append");
  26. if (val & O_NONBLOCK)
  27. printf(", nonblocking");
  28. if (val & O_SYNC)
  29. printf(", synchronous writes");
  30. #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
  31. if (val & O_FSYNC)
  32. printf(", synchronous writes");
  33. #endif
  34. putchar('\n');
  35. exit(0);
  36. }

3.文件共享

UNIX支持在不同进程间共享打开同一个文件。

3.1 数据结构

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

  • 文件描述符表:每个进程都维护了一个文件描述符表,可以看成一个矢量,每个描述符占用一项,相关联的是

    • 文件描述符标志close_on_exec
    • 指向一个文件表项的指针
  • 所有打开文件维持一张文件表,每个文件表包括

    • 文件状态标识(读、写、读写、同步、非阻塞等)
    • 当前文件偏移量
    • 指向该文件v节点表项的指针
  • 每个打开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。这些信息是在打开文件时从磁盘读入内存的,所以文件的所有相关信息都是随时可用的,比如:文件的所有者,文件长度,指向文件实际数据块在磁盘上的位置指针等。

基础IO函数 - 图3

如上图。该进程打开了两个文件,一个从标准输入(0)打开,另一个从标准输出打开(1)。

如果两个独立的进程打开了同一份文件,则有

基础IO函数 - 图4

假设第一个进程在文件描述符3上打开该文件,另一个进程在文件描述符4上打开该文件每个进程自己会对该文件维护一个文件表项,但只有一个v节点被他们共同指向。这是因为,每个进程可以维护自己对该文件的偏移量。

每个进程维护自己的文件表项,其中包括自己的偏移量。但是,这在多个线程在写同一个文件的时候就会产生一些意外。因此,就有了原子操作的概念。

4.原子操作

4.1 追加到一个文件

考虑到一个进程要将数据追加到文件尾端,早期的UNIX不支持open的O_APPEND选项,因此通过lseek改变偏移量到文件尾端+write实现。

如果多个进程同时用这种方式对同一文件进行追加写操作,就会出现问题。
假设有两个进程A和B打开了同一个文件进行追加写操作,但没有使用O_APPEND标识。这个时候,每个进程都有自己的文件表项,但是共享一个v节点表项。
假设进程A用lseek将偏移量设置为1500字节,这个时候内核调度进程到B进程,执行lseek将偏移量设置为1500字节。
这个时候B进程调用write,写了100字节,此时文件的长度更新为1600。
内核再次调度,返回到A进程。但由于A进程还是从1500字节处开始写数据,因此就会将B进程刚刚写的数据覆盖掉。

问题出在偏移量定位和写操作是分别独立的,因此需要将这两个操作合并成为一个源自操作。

UNIX提供了源自操作。在打开文件写的时候设置O_APPEND标志,写之前才将偏移量定位到尾端。

4.2 pread和pwite

Single UNIX Specification包括了XSI扩展,允许原子性的定位并执行I/O,也就是pread和pwite。

  1. // 返回值:读到的字节数,若出错返回-1
  2. ssize_t pread(int fd,void *buf,size_t nbytes,off_t offset);
  3. // 返回值:若成功返回已写的字节数,若出错返回-1
  4. ssize_t pwrite(int fd,const void *buf,size_t nbytes,off_t offset);

pread相当于调用lseek后调用read,区别是调用pread时无法中断定位和写操作,且不更新文件偏移量。
pwrite相当于调用lseek后调用write,同样无法中断且不更新偏移量。

4.3 创建一个文件

和写操作一样,如果没有原子操作,在open和creat之间,若一个进程创建了一个文件并写入了一些数据,另一个文件对这个文件进行了creat操作,就会将原来的数据抹去。

link函数将会在后续解决这个问题。

5.dup和dup2

  1. int dup(int fd);
  2. int dup2(int fd,int fd2);

dup可以修改该文件描述符指向文件的文件描述符为可用的最小数值,而dup2可以指定新的文件描述符的值。
如果fd2已经打开,则先将其关闭。若fd等于fd2,那么dup2返回fd2而不关闭它,否则fd2的FD_CLOEXEC文件描述符标志就被清楚,在进程中fd2调用exec是打开状态。

dup(fd)等同于fcntl(fd,F_DUPFD,0)
dup2(fd,fd2)大致等同于close(fd2);fcntl(fd,F_DUPFD,fd2);

第二个差异主要在于dup2是源自操作,而close+fcntl不是;且有一些errno不相同。

6.sync,fsync,fdatasync

传统的UNIX系统在内核中有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。
当向文件写入数据时,内核通常先将数据写到缓冲区中,然后排入队列,最后再写入磁盘。这称之为延迟写。
当内核需要重用缓冲区存放其他磁盘块数据时,会先将所有延迟写数据块写入磁盘,这就是sync和fsync和fdatasync函数

  1. // 若成功返回0,失败返回-1
  2. int fsync(int fd);
  3. int fdatasync(int fd);
  4. void sync(void);

sync只是将所有修改过的块缓冲区排入写队列,然后就返回,不等待磁盘操作结束。
通常叫update的系统守护进程周期性的(通常是三十秒)调用sync函数,保证了定期冲洗缓冲区。
fsync只对对应fd文件起作用,且同步等待磁盘操作结束才返回。fsync可以应用于数据库这样需要保证实时一致性的程序。
fdatasync类似于fsync,但它只影响数据部分,而fsync还会修改文件的属性。


https://www.tutorialspoint.com/unix_system_calls/open.htm