9、文件共享
内核用于维护进程打开的文件的相关数据结构大致有三个:
- 每个进程的进程表中有一个包含所有打开的文件描述符号信息的表,这个表中每一项主要包含:一个文件描述符号的标记以及一个文件符号描述表指针。
- 文件符号描述表,由前面的进程表中指针来指向它,主要包含:一个状态标记,一个偏移,以及一个文件虚拟节点地址。如果多个进程打开同样文件,也会导致有多个这样的表,但是虚拟节点只有一个,这样是保证每个进程有自己正确的文件读取偏移量。
- 文件虚拟节点:描述文件大小,虚拟节点信息和索引信息等。用于在计算机上支持不同的文件系统。
对于虚拟节点和索引, v-node
用来在一个计算机系统上面支持多个类型的文件系统。 Sun 将它称作虚拟文件系统,并且将 i-node
中和文件系统相独立的部分叫做 v-node
。随着对 Sun
的网络文件系统( NFS
)的支持的加入, v-node
在不同的厂商实现中流行起来。具体的发展情况,请参考原书参考资料, Linux 并不是讲数据分成了 i-node
和 v-node
,它是用一个和文件系统无关的 i-node
以及和一个文件系统相关的 i-node
。尽管实现有所不同,其概念和v节点类似,在第4章14节中我们会讨论 i-node
。
注意,上面的文件描述符号标记和状态标记是不一样的。文件描述符号标记例如 close-on-exec
,意思是使用 exec
的时候是否关闭之前的文件符号;状态标记有例如:读写属性,追加,同步写属性,阻塞等;
下图描述一个打开了两个文件的进程的这些数据结构的情况。
打开两个文件的进程的相关数据结构关系
v-node table
process table entry file table -->+------------------+
+------------------+ --------->+-------------------+ / | v-node |
| fd file | / |file status flags | / | information |
| flags pointer|/ +-------------------| / +------------------+
| +----+-----+ / |current file offset| / | i-node |
| fd0 | | |/| +-------------------+ / | information |
| +----+-----+ | | v-node pointer |/ + - - - - - - - - -+
| fd1 | | |----------\ +-------------------+ | current file size|
| +----+-----+ | \ +------------------+
| fd2 | | | | ->+-------------------+
| +----+-----+ | |file status flags | ---->+------------------+
| | | | +-------------------| / | v-node |
| | ...... | | |current file offset| / | information |
| | | | +-------------------+ / +------------------+
| +----------+ | | v-node pointer |/ | i-node |
| | +-------------------+ | information |
+------------------+ + - - - - - - - - -+
| current file size|
+------------------+
上图中,一个文件在标准输入上(文件描述符号为0)打开,另外一个在标准输出上(文件描述符号为1)打开。
下图给出了两个不同进程打开同一个文件的情况。
两个独立进程打开同一个文件时内核的相关数据结构
process table entry
+------------------+
| fd file |
| flags pointer|
| +----+-----+ |
| fd0 | | | |
| +----+-----+ |
| fd..| ...... | |
| +----+-----+ | file table
| fd3 | | |------------->+-------------------+ v-node table
| +----+-----+ | |file status flags | ---->+------------------+
| | | | +-------------------| / +->| v-node |
| | ...... | | |current file offset| / | | information |
| | | | +-------------------+ / | +------------------+
| +----------+ | | v-node pointer |/ | | i-node |
| | +-------------------+ | | information |
+------------------+ | + - - - - - - - - -+
| | current file size|
| +------------------+
process table entry ---->+-------------------+ /
+------------------+ / |file status flags | /
| fd file | / +-------------------| /
| flags pointer| / |current file offset| /
| +----+-----+ | / +-------------------+ /
| fd0 | | | | / | v-node pointer |/
| +----+-----+ | / +-------------------+
| fd..| ...... | |/
| +----+-----+ /
| fd4 | | |/|
| +----+-----+ |
| | | |
| | ...... | |
| | | |
| +----------+ |
| |
+------------------+
上图中,第一个进程的文件描述符号3和第二个进程的文件描述符号4表示同一个文件。可知,每个进程有它自己的文件表,但是每个文件只有一个 v-node
表。每个进程有一个文件表的原因是这样可以让每个进程有它自己的文件当前偏移。
如果打开多个文件想要使用一个文件符号描述表,那么用 dup
. 使用 fork
好像就是达到这个效果的(见第8章3节)。如果一个进程打开多个文件呢?
使用 dup
的时候,会使得进程中多个文件描述符号信息的表项指向一个文件符号描述表项。使用 fcntl
一样,不过 fcntl
有可能不是原子的,之前需要别的操作才行,而 dup
只需要一个函数,是原子的,后面会提到原子操作。
译者注
原文参考
10、原子性操作
原子性的操作就是在操作过程中不会被打断的操作,适用于多线程。例如:
在文件结尾写入数据需要:
- 使用
lseek
定位到文件结尾 - 使用
write
写。
但是这就不是原子操作了,在两步之间可能会被别的线程打断。所以要想原子操作,可以用带有 append
标志的 open
打开文件之后再 write
。
还有例如:向一个指定的位置读写数据也类似,使用 pread
和 pwrite
函数指定偏移量的读写,是原子性的。
另外,前面讲述 open
函数的时候也提到过,对于标记 O_EXCL
如果同时指定了 O_CREAT
,而文件已经存在,那么就出错,利用这点这样可以测试文件是否存在,不存在则创建,并且这个操作是原子的。
译者注
原文参考
11、 dup
和 dup2
函数
下面两个函数可以用来复制文件描述符号:
#include <unistd.h>
int dup(int filedes);
int dup2(int filedes, int filedes2);
两者返回:如果成功返回新的文件描述符号,如果错误返回1(其实一般是数值的-1)。
由 dup
返回的文件描述符一定是当前可用文件描述符中的最小数值。用 dup2
则可以用 filedes2
参数指定新描述符的数值。如果 filedes2
已经打开,则先将其关闭。如若 filedes
等于 filedes2
,则 dup2
返回 filedes2
,而不关闭它。
函数返回的新描述符号共享 filedes
参数对应的文件表。如下图:
调用 dup
之后内核的数据结构关系
process table entry v-node table
+------------------+ +-->+------------------+
| fd file | | | v-node |
| flags pointer| | | information |
| +----+-----+ | | +------------------+
| fd0 | | | | | | i-node |
| +----+-----+ | | | information |
| fd1 | | |---------\ | + - - - - - - - - -+
| +---+------+ | \ | | current file size|
| fd2 | ...... | | -->+-------------------+ | +------------------+
| +----+-----+ | / |file status flags | /
| fd3 | | |---------/ +-------------------| /
| +----+-----+ | |current file offset| /
| | | | +-------------------+ /
| +----------+ | | v-node pointer |/
| | +-------------------+
+------------------+
复制一个描述符的另一种方法是使用 fcntl
函数,下一节将对该函数进行说明。实际上,调用:
dup(filedes);
等价于:
fcntl (filedes, F_DUPFD, 0);
而调用:
dup2(filedes, filedes2) ;
等价于:
close (filedes2) ;
fcntl(filedes, F_DUPFD, filedes2);
在最后一种情况下, dup2
并不完全等同于 close
加上 fcntl
。它们之间的区别是:
- = dup2= 是一个原子操作。
- 在
dup2
和fcntl
之间有某些不同的errno
。
译者注
原文参考
12、 sync
, fsync
,和 fdatasync
函数
一般UNIX实现都在内核中有一个 buffer
缓存,或者页缓存,大多数的磁盘输入输出操作都会通过这个缓存。当我们将数据写入到一个文件中的时候,数据一般都会被内核首先拷贝到这些缓存中去,然后排队,到一定时间的时候会将相应的数据写入磁盘。这个技术也叫做延迟写。
内核最终会将延迟写的块写入到磁盘上。一般在内核想要使用这块缓存来做其它的磁盘块操作的时候就会这样。为了确保磁盘上面文件系统和缓存中的内容的一致,提供了 sync
, fsync
,以及 fdatasync
函数。
#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
返回:如果成功返回0,如果错误返回1(其实一般是-1)。
void sync(void);
函数 sync
只是简单地将所有被修改的缓存块排队,以用于写;它不会等待磁盘的写入发生。
函数 sync
一般都会被系统守护进程(一般是 update
进程)周期地调用。这样就保证对内和的块缓存有规律地刷新。命令 sync
(参见 Linux/Unix
用户手册 sync
)也会调用 sync
函数。
函数 fsync
只是引用一个单个的文件,这个文件通过文件描述符号参数 filedes
来指定,然后等待磁盘写操作完成,最后返回。使用 fsync
一般用于类似数据库这样的应用程序,以保证所做的修改确实地被写入到了磁盘中。
fdatasync
函数和 fsync
类似,但是它只是影响文件的数据部分。而通过 fsync
文件的属性也会被同时更新。
译者注
原文参考
13、 fcntl
函数
fcntl
函数用来改变一个已经打开的文件的性质。其声明如下:
#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /* int arg */ );
返回:如果成功,返回值取决于参数 cmd
;如果错误返回1(其实其值一般应该是-1)。
filedes
是文件描述符号, cmd
是要进行的操作, arg
是操作的参数,不多说了。 fcntl
函数有五种功能:
- 复制一个已有文件描述符(cmd=F_DUPFD)。
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。
- 获得/设置文件状态标志(cmd=F_GETFL或F_SETFL)。
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)。
更多信息参见参考资料。
下面的例子,对应的程序以一个文件描述符号作为参数,运行之后,打印相应文件描述符号的文件标记。代码如下:
打印制定文件描述符号的文件标记
#include "apue.h"
#include <fcntl.h>
int main(int argc, char *argv[])
{
int val;
if (argc != 2)
err_quit("usage: a.out <descriptor#>");
if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
err_sys("fcntl error for fd %d", atoi(argv[1]));
switch (val & O_ACCMODE) {
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
err_dump("unknown access mode");
}
if (val & O_APPEND)
printf(", append");
if (val & O_NONBLOCK)
printf(", nonblocking");
#if defined(O_SYNC)
if (val & O_SYNC)
printf(", synchronous writes");
#endif
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC)
if (val & O_FSYNC)
printf(", synchronous writes");
#endif
putchar('\n');
exit(0);
}
运行如下:
$ ./a.out 0 < /dev/tty
read only
$ ./a.out 1 > temp.foo
$ cat temp.foo
write only
$ ./a.out 2 2>>temp.foo
write only, append
$ ./a.out 5 5<>temp.foo
read write
这里,在使用 O_SYNC
标记的时候,会在 write
同时等待写到磁盘上才返回,这样明显占用了系统时间,但是在 ext2
系统上不认这个标记。在正常没有 O_SYNC
的 write
会写到磁盘缓存上面,所以占用时间少,正常没有 O_SYNC
的 write
后面接着一个 fsync
的话并不是和 O_SYNC
的 write
一样了,而是也占用很少的时间,因为在下次新 write
的时候会刷新之前的缓存,所以在 fsync
的时候,实际只把很少的一部分数据写到磁盘中了。
另外, 5<>temp.foo
表示打开文件 temp.foo
用于读写,并且其文件描述符号为5。这个语法比较特殊,举几个例子如下(以下例子在ubuntu8.04上面实践):
$touch tempfile
$5<>tempfile echo 1111 >&5
注意方向不能错,如:
$echo 1111 >&5 5<>tempfile
是错误的。
或者:
$exec 5<>tempfile
$echo 1111 >&5
exec
相当于单独执行,之后 /dev/fd/
里面会有5这个描述符号。
之后我们会发现,文件 tempfile
里面有 1111
这样的内容了。
关闭描述符号:
$exec >&5-
译者注
原文参考
14、 ioctl
函数
ioctl
函数可以支持任何的 io
操作而不仅仅是读和写,有许多终端操作就是用这个函数来实现。 ioctl
函数声明如下:
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
#include <stropts.h> /* XSI STREAMS */
int ioctl(int filedes, int request, ...);
返回:如果错误返回1(其实一般是-1),如果成功返回0,参数中也会存放其它返回值。
由于UNIX/LINUX中对所有内容都看作文件,包括系统的设备,程序中我们将一切资源看做文件,然后以文件的方式操作相应的资源(例如 open
, write
, read
, ioctl
, close
)。一般在特殊文件(如设备文件)对应的驱动中等地方需要实现这个函数,因为许多驱动的功能,不仅仅只是通过普通系统调用中已有的接口(例如 read
, write
等)就能表达的。例如,我们编写一个摄像机的驱动,想要实现播放,暂停,停止等,显然这些操作都是和设备相关的,所以这个时候就在驱动程序中实现这个函数。
对于这个函数,参数 filedes
就表示其所操作的文件描述符号; request
表示要操作的命令码(命令码表示某种操作,而其值和具体设备相关,例如上面的摄像机设备,我们可以在驱动中规定一个命令码 PLAY
);剩下的 ...
表示 request
命令的参数,参数可有可无。这个函数功能是依据文件描述符号所代表的资源而不同,这里不多说了,写设备驱动的时候,这个函数非常重要。
译者注
原文参考
15、/dev/fd
比较新的系统都提供名为/dev/fd的目录,其中包含名为 0、1、2等的文件。打开文件/dev/fd/n相当于复制描述符n(假定描述符n是打开的)。所以调用:
fd = open("/dev/fd/0", mode);
相当于:
fd = dup(0);
大多数系统忽略所指定的mode,而另外一些则要求mode是所涉及的文件(在这里则是标准输入)原先打开时所使用的mode的子集。例如,若描述符0被只读打开,那么我们也只对fd进行读操作。即使下列调用成功:
fd = open("/dev/fd/0", O_RDWR);
我们仍然不能对fd进行写操作。某些系统提供路径名/dev/stdin ,/dev/stdout和/dev/stderr。这些相当于/dev/fd/0,/dev/fd/1和/dev/fd/2。
译者注
原文参考
总结
本章描述了UNIX系统提供的基本输入输出( I/O
)函数。这些函数也经常被称作非缓冲 I/O
函数,因为,每次 read
或者 write
都会发起一次内核的系统调用。通过使用 read
和 write
,我们看到了不同 I/O
大小的时候对读取一个文件的时间的影响(在一定范围内,请求数据越大,读写系统调用被发起的次数越少,导致占用系统时间也减少)。我们也看到了将被写入的数据刷新到磁盘上面的方法,以及它们对应用程序执行效率的影响。
当多进程向同一个文件追加内容的时候,以及当多个进程创建同样一个文件的时候,介绍了原子操作。我们也看到了内核内部用来共享打开文件信息的相关数据结构。我们将在后面的章节中,继续谈到这些数据结构。
我们也讨论了函数 ioctl
和函数 fcntl
。后面第14章的时候,我们会回到这些函数,我们将使用 ioctl
操作 STREAMS I/O
系统,使用 fcntl
来实现记录锁。
网友评论