4.1 文件概述
文件I/O在Unix下占据着非常重要的地位,曾有一句经典语句绝对可以说明file在Unix下的重要性, "In UNIX, everything is a file",APR就是本着这个思想对Unix文件I/O进行了再一次的抽象封装,以提供更为强大和友善的文件I/O接口。
APR File I/O源代码的位置在$(APR_HOME)/file_io目录下针对不同类型的操作系统,file_io下又进一步细分为四个目录:netware,os2,unix和win32。每个目录中又包含多个.c文件,每个.c文件对应一种文件操作,比如
open.c -- 封装了文件的打开、关闭、改名和删除等操作
readwrite.c -- 顾名思义,它里面包含了文件的读写操作;
pipe.c -- 包含了pipe相关操作。
还有许多这里不多说,由于文件I/O操作复杂,我们下面将仅挑出最常用的文件I/O操作进行分析。
4.2 APR文件定义
正如第一章所言,APR中为了移植方便,通常会使用自定义的常量来代替原有系统中存在的常量,并提供它们之间的相互转换,这一点对于文件也不例外。APR几乎对文件中所用到的所有的常量都进行了重新定义,包括文件权限、文件打开方式、文件类型定义等等。这部分我们集中了解APR中的这些定义。在后面的部分将很快就会使用到。
4.2.1文件类型
尽管在Unix中,一切皆文件,但是文件又分为不同类型:
1)、普通文件(regular file)。这是最常见的文件类型。UNIX中使用S_IFREG描述该类文件,而APR中使用APR_REG描述。
2)、目录文件(directory file)。这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。UNIX中使用S_IFDIR描述,在APR中使用APR_DIR描述。
3)、字符特殊文件(character special file)。这类文件通常对应系统中某些类型的设备。UNIX中使用S_IFCHR描述,而APR中使用APR_CHR描述。
4)、块特殊文件(block special file)。这类文件典型的用于磁盘设备。系统中的所有设备或者是字符特殊文件,或者是块特殊文件。UNIX中使用S_IFBLK描述,而APR中使用APR_BLK描述该类文件。
5)、FIFO文件。这类文件通常用于进程间通信,有时也称之为管道。APR中使用APR_PIPE描述该类文件。
6)、套接字描述文件。这类文件用于进程间的网络通信。UNIX中使用S_IFSOCK描述,而APR中使用APR_SOCK描述该类文件。
7)、链接文件。最后一种文件类型就是链接文件。这种文件通常仅仅指向另外一个文件。UNIX中使用S_IFLNK描述,而APR中使用APR_LNK描述该类文件。
综合上面的内容,APR定义了枚举类型apr_filetype_e来描述所有的Unix文件类型。
typedef enum {
APR_NOFILE = 0, /**< no file type determined */
APR_REG, /**< a regular file */
APR_DIR, /**< a directory */
APR_CHR, /**< a character device */
APR_BLK, /**< a block device */
APR_PIPE, /**< a FIFO / pipe */
APR_LNK, /**< a symbolic link */
APR_SOCK, /**< a [unix domain] socket */
APR_UNKFILE = 127 /**< a file of some other unknown type */
} apr_filetype_e;
为了实现APR定义和UNIX定义的转换,APR中通过函数filetype_from_mode实现从系统定义到APR定义的转换。该函数定义在filestat.c中:
static apr_filetype_e filetype_from_mode(mode_t mode)
{
apr_filetype_e type;
switch (mode & S_IFMT) {
case S_IFREG:
type = APR_REG; break;
case S_IFDIR:
type = APR_DIR; break;
……
}
}
4.2.2文件访问权限
在UNIX系统中,每一个文件都对应三组不同的访问权限,即用户存取权限、组用户存取权限和其余用户存取权限。在UNIX中,通常用S_IXXXX常量来描述这些访问权限。APR中则使用APR_FPROT_XXXX来进行替代,对应的关系如下表所示:
权限标志
|
含义
|
值
|
标准库值
|
APR_FPROT_USETID
|
允许设置用户号
|
0x8000
|
S_ISUID
|
APR_FPROT_UREAD
|
允许用户读操作
|
0x0400
|
S_IRUSR
|
APR_FPROT_UWRITE
|
允许用户写操作
|
0x0200
|
S_IWUSR
|
APR_FPROT_UEXECUTE
|
允许用户执行
|
0x0100
|
S_IXUSR
|
APR_FPROT_GSETID
|
用于设置组号
|
0x4000
|
S_ISGID
|
APR_FPROT_GREAD
|
允许组成员读取
|
0x0040
|
S_IRGRP
|
APR_FPROT_GWRITE
|
允许组成员写入
|
0x0020
|
S_IWGRP
|
APR_FPROT_GEXECUTE
|
允许组成员执行
|
0x0010
|
S_IXGRP
|
APR_FPROT_WSTICKY
|
粘贴位
|
0x2000
|
S_ISVTX
|
APR_FPROT_WREAD
|
允许其余成员读取
|
0x0004
|
S_IROTH
|
APR_FPROT_WWRITE
|
允许其余成员写入
|
0x0002
|
S_IWOTH
|
APR_FPROT_WEXECUTE
|
允许其余成员执行.
|
0x0001
|
S_IXOTH
|
APR_FPROT_OS_DEFAULT
|
操作系统的默认的属性值
|
0x0FFF
|
0666
|
在早期版本中,访问权限使用的是APR_XXXX形式,比如APR_UREAD、APR_UWRITE等等。不过目前已经作废。为了保持与低版本的兼容性,你在源文件中还能看到它们。
在APR中,文件的访问权限被定义为apr_fileperms_t类型,该类型本质上是一个32位的整数而已:
typedef apr_int32_t apr_fileperms_t;
APR中提供了两个函数用于实现从APR权限标志到UNIX系统标志位的相互转换。apr_unix_perms2mode函数用于将APR定义转换为Unix定义,apr_unix_mode2perms用于将Unix定义转换为APR定义。这两个函数都定义在fileacc.c中。从Unix转换至APR的过程无非如下:
if (mode & S_IXXXX)
perms |= APR_XXXX;
而从APR转换为Unix过程无非如下:
if (perms & APR_XXXX)
mode |= S_IXXXX;
4.2.3文件打开方式
4.2.4其余类型重定义
APR中除了对上面的常量进行了重定义之外,它还对一些类型进行了重定义,不过这些类型都仅仅是使用typedef而已,非常简单,总结归纳如下:
1)、文件属性类型apr_fileattrs_t
typedef apr_uint32_t apr_fileattrs_t;
2)、文件定位基准apr_seek_where_t
typedef int apr_seek_where_t;
3)、文件访问权限apr_fileperms_t
typedef apr_int32_t apr_fileperms_t;
4)、文件i-node结点编号apr_ino_t
typedef ino_t apr_ino_t;
5)、文件所在设备号apr_dev_t
typedef dev_t apr_dev_t;
4.3 文件描述
在Unix系统中,与文件关联的两个数据结构通常是两个:FILE和stat。前者通常称之为文件句柄,而后者则通常称之为文件的状态信息,用于描述文件的内部信息。APR中,与之对应提供了两个封装数据结构apr_file_t和apr_finfo_t,前者描述文件句柄信息,后者描述文件内部信息。
根据操作系统支持的不同,apr_file_t了可以分为四个版本,不过我们仅仅介绍Unix版本,至于Window版本,我们会提及,而其余的netware和OS/2版本我们不打算做任何分析。在Unix系统中,apr_file_t定义如下:
struct apr_file_t {
apr_pool_t *pool;
int filedes;
char *fname;
apr_int32_t flags;
int eof_hit;
int is_pipe;
apr_interval_time_t timeout;
int buffered;
enum {BLK_UNKNOWN, BLK_OFF, BLK_ON } blocking;
int ungetchar; /* Last char provided by an unget op. (-1 = no char)*/
#ifndef WAITIO_USES_POLL
/* if there is a timeout set, then this pollset is used */
apr_pollset_t *pollset;
#endif
/* Stuff for buffered mode */
char *buffer;
int bufpos; /* Read/Write position in buffer */
unsigned long dataRead; /* amount of valid data read into buffer */
int direction; /* buffer being used for 0 = read, 1 = write */
unsigned long filePtr; /* position in file of handle */
#if APR_HAS_THREADS
struct apr_thread_mutex_t *thlock;
#endif
};
该结构描述了一个文件的大部分的属性,也是整个文件I/O系统的核心数据结构之一。
filedes是文件的描述符;fname则是打开的文件的名称;is_pipe用以标记当前文件是否是管道文件;Unix中在创建匿名管道的时候会生成一个管道文件,在管道中传输的数据实际上最终都保存在匿名文件中。对于这种临时的管道文件,它的is_pipe为1;filePtr是读写文件的时候文件内部的文件指针,通常情况下这个成员只有seek函数的时候才需要使用,由于seek需要给出当前文件的内部指针位置,因此在任何的文件读和写之后,我们都必须立即调整filePtr的值,以使它指向正确的位置。direction则是记录了当前的操作类型,0是读操作,1是写操作;buffer缓冲区用以保存从文件中读取的数据,dataRead则用以记录从文件中读取到缓冲区中的有效的字节数;blocking则是记录的读取的方式,一般允许两种,即阻塞和非阻塞。对于阻塞,那么读取将等待,直到文件中有新的数据或者读取超时,超时的时间由timeout决定。
另外,如果支持多线程,那么为了保证线程访问安全性,在可能出现互斥的数据结构中都要额外的增加内部互斥锁。文件结构的内部互斥锁由变量thlock决定。任何人访问该数据结构之前都必须先获取该互斥锁,同时访问结束后该互斥锁将被释放。
4.4 打开文件
文件打开应该是使用的最多的文件操作了,任何文件在使用之前都必须首先打开。ANSI C标准库和Unix系统库函数都提供对“打开文件”这个操作语义的支持。他们提供的接口很相似,参数一般都为“文件名+打开标志位+权限标志位”,Apache中提供了apr_file_open函数来支持文件打开操作,该函数只是在原有的标准库的基础上进行了少许的封装。apr_file_open无法忽略习惯的巨大力量,它提供了与ANSI C以及Unix系统库函数类似的接口如下:
APR_DECLARE(apr_status_t) apr_file_open(apr_file_t **new,
const char *fname,
apr_int32_t flag,
apr_fileperms_t perm,
apr_pool_t *pool);
其中fname、flag和perm三个参数与普通的open函数相同,fname分别表示打开的文件的路径名称,可以是相对路径,也可以是绝对路径。每个封装都有自定义的一些标志宏,这里也不例外,flag和perm参数都需要用户传入APR自定义的一些宏组合,不过由于这些宏的可读性都很好,不会成为你使用过程的绊脚石。flag是打开文件的标志,包括可读可写,Apache中打开标志可以概括为下面的几种:
打开标志
|
含义
|
值
|
APR_READ
|
打开文件为只读
|
0x00001
|
APR_WRITE
|
打开文件为只写
|
0x00002
|
APR_CREATE
|
如果文件不存在,创建一个新的文件
|
0x00004
|
APR_APPEND
|
允许将内容追加到文件的末尾,而不是重新覆盖
|
0x00008
|
APR_TRUNCATE
|
如果文件存在,将其长度设置为0
|
0x00010
|
APR_BINARY
|
打开的不是文本文件,而是二进制文件,在UNIX上,该标志将被忽略
|
0x00020
|
APR_BUFFERED
|
缓存数据,默认情况下不进行数据缓存
|
0x00040
|
APR_EXCL
|
return error if APR_CREATE and file exists
|
0x00080
|
APR_DELONCLOSE
|
关闭后删除该文件
|
0x00100
|
APR_XTHREAD
|
该标志依赖于具体的平台,用以确保文件在跨线程访问中的安全
|
0x00200
|
APR_SHARELOCK
|
平台依赖标记,用以支持上层的读写所访问从而支持跨进程或者跨机器访问
|
0x00400
|
APR_FILE_NOCLEANUP
|
通知系统不要在内池中注册文件清除函数,因此当内存池被销毁的时候,apr_file_t中的apr_os_file_t句柄将不会被销毁
|
0x00800
|
APR_SENDFILE_ENABLED
|
Open with appropriate platform semantics
for sendfile operations. Advisory only, apr_socket_sendfile does not check this flag.
|
0x01000
|
perm则是用以记录文件的存取权限,通常情况下是一个整数,比如0x777,0x666等等。Apache中支持的权限如表4-3所示。
文件的读取结果由指针apr_file_t返回。整个open过程可以分为四部分:
1)、“打开标志位”转换;
如前所述,APR定义了自己的“文件打开标志位”,所以在apr_file_open的开始需要将这些专有的“文件打开标志位”转换为Unix平台通用的“文件打开标志位”,对应的转换表如下:
Apache打开标志
|
UNIX库函数内部标志
|
APR_READ&&APR_WRITE
|
O_RDWR
|
APR_READ
|
O_RDONLY
|
APR_WRITE
|
O_WRONLY
|
APR_CREATE
|
O_CREAT
|
O_CREAT&&APR_EXCL
|
O_CREAT&O_EXCL
|
APR_EXCL&&!APR_CREATE
|
允许组成员读取
|
APR_APPEND
|
O_APPEND
|
APR_TRUNCATE
|
O_TRUNC
|
O_BINARY
|
O_BINARY
|
APR_BUFFERED&&APR_XTHREAD
|
允许其余成员读取
|
2)、权限标志位”转换;
同1)理,专有的APR“权限标志位”需要转换为Unix平台通用的“权限标志位”;转换使用函数apr_unix_perms2mode实现,转换根据表4-2的对应关系实现。函数返回的权限最终传递给open函数的标志位。
(3)、调用Unix的本地API打开文件;
(4)、设置apr_file_t变量相关属性值;
APR 文件I/O封装支持非阻塞I/O带超时等待以及缓冲I/O,默认情况下为阻塞的,即BLK_ON。
APR文件的另外一个特殊之处就是支持缓冲特性。由于磁盘读取的速度瓶颈,使得频繁的从磁盘读取文件在一定程度上会影响执行效率,因此为了提高读取效率,APR支持文件的缓存读写,即开辟一块大的缓冲内存区,用以保存实际从磁盘中读取得数据,这样用户每次就不需要读写磁盘,而只要读写内存,通过这种缓冲策略,可以改善一定的性能。是否缓冲可通过“文件打开标志位APR_BUFFERED”设置。一旦设置为缓冲读写,则apr_file_open会在pool中开辟大小为APR_FILE_BUFSIZE(4096)的缓冲区供使用。
创建函数中一个比较重要的就是内存池中apr_file_t类型的清除函数注册。当内存池被销毁的时候,对于所有的apr_file_t类型将调用apr_unix_file_cleanup函数进行清除。与创建类似,清除也包括四方面的工作:
(1)、
(2)、关闭文件描述符filedes,如果文件的打开标志是APR_DELONCLOSE,那么在关闭之后还得将该文件删除;如果文件可能跨进程使用,那么还得销毁互斥锁。
(3)、
4.5 文件读取
4.5.1普通文件读取
文件的读写操作定义在readwrite.c中。函数的原型与标准的接口非常类似:
APR_DECLARE(apr_status_t) apr_file_read(apr_file_t *thefile, void *buf, apr_size_t *nbytes)
thefile是需要读取的文件的描述结构,而buf是文件读取保存的缓冲区。nbytes则是从文件中需要读取的字节数。Apache中的文件读取在内部分为两种机制:支持读写缓存的和不支持读写缓存的,读写是否需要支持缓存,有apr_file_t内部的buffered成员决定,buffered=1表示内部必须支持缓存,否则不需要使用缓存。
我们首先分析最普通的内部不使用缓存的读写情况,下面是读取的核心代码:
{
apr_ssize_t rv;
apr_size_t bytes_read;
if (*nbytes <= 0) {
*nbytes = 0;
return APR_SUCCESS;
}
{
bytes_read = 0;
if (thefile->ungetchar != -1) {
bytes_read = 1;
*(char *)buf = (char)thefile->ungetchar;
buf = (char *)buf + 1;
(*nbytes)--;
thefile->ungetchar = -1;
if (*nbytes == 0) {
*nbytes = bytes_read;
return APR_SUCCESS;
}
}
do {
rv = read(thefile->filedes, buf, *nbytes);
u
} while (rv == -1 && errno == EINTR);
#ifdef USE_WAIT_FOR_IO
if (rv == -1 &&
(errno == EAGAIN || errno == EWOULDBLOCK) &&
thefile->timeout != 0) {
apr_status_t arv = apr_wait_for_io_or_timeout(thefile, NULL, 1);
if (arv != APR_SUCCESS) {
v
*nbytes = bytes_read;
return arv;
}
else {
do {
rv = read(thefile->filedes, buf, *nbytes);
} while (rv == -1 && errno == EINTR);
}
}
#endif
*nbytes = bytes_read;
if (rv == 0) {
thefile->eof_hit = TRUE;
w
return APR_EOF;
}
if (rv > 0) {
*nbytes += rv;
x
return APR_SUCCESS;
}
return errno;
}
}
读取的主要操作就是调用标准的文件读操作read,文件中读取的数据直接保存到输出缓冲区buf中,确实没有进行任何的内部缓存。
第一次读取结束后,函数将根据读取的返回值做进一步的处理:
■ 返回EINTR,见代码
u
处
返回EINTR意味着读取操作被中断信号无意打断,而不是读取操作本身出现任何问题,因此此时必须无条件重新启动读操作,这就是将读操作放在循环中的原因之一,代码见。
读操作发生失败的另外一个可能性就是文件暂时不可用,因此此时应该稍等片刻再尝试,这种情况通常会返回EAGAIN错误码,对于GNU C库而言,EWOULDBLOCK与EAGAIN的含义完全相同,只是换了一个名字而已,不过对于早期的版本可能存在差异。因此稳妥的做法就是同时检测EAGAIN和EWOULDBLOCK错误码。
一般下面的两种情况可能返回这两个错误码:
(1)、对非阻塞模式的对象进行某个会阻塞的操作可能会返回该错误码,再次做同样的操作就会阻塞直到某种条件使它可以读写。
(2)、某个资源上的故障使操作不能进行。例如fork有可能返回着个错误,这也表示这个故障可以被克服,所以你的程序可以以后尝试这个操作。停几秒让其他进程释放资源然后再试也许是个好主意。这种故障可能很严重并会影响整个系统,所以通常交互式的程序会报告用户这个错误并返回命令循环。
如果apr_file_t结构中设置了超时时间timeout,同时发现必须重新读取(返回码为EAGAIN或者EWOULDBLOCK),那么Apache将调用apr_wait_for_io_or_timeout函数等待重新读取文件。apr_wait_for_io_or_timeout函数内部使用了I/O的多路复用技术poll,具体的细节我们在网络I/O章节描述。如果在给定的超时时间内,文件还是不允许进行读取操作,那么此时函数将直接返回,否则函数将重新调用下面的语句:
do {
rv = read(thefile->filedes, buf, *nbytes);
} while (rv == -1 && errno == EINTR);
由于此时文件的状态处于肯定允许进行操作状态,因此不再需要进行额外的异常处理,唯一的就是防止被信号无意打断。
■ 到达文件末尾(rv==0),见代码
w
如果read函数返回0,此时意味着已经读取到整个文件,此时设置文件的eof_hit=1,至此整个文件读取结束。
■ 读取非空字节(rv>0),见代码
x
此时累计读取的总字节数目。
上面的几个文件读取的处理步骤是大多数文件读取的标准的读取操作,因此非常的好理解。
事实上,对于文件读取分析的重点并不是上面的普通的读取,而是使用缓存的读取。一般情况下I/O操作是相当耗费时间的,因此仅仅一次从文件中直接读取数据保存到缓冲区中所花的时间可能允许忽略不计,但是如果文件读取操作非常的频繁的话,那么这将无疑是一个不小的时间耗费,甚至可能是性能瓶颈。因此为了有效地提高读写性能,Apache提供了缓存读取的策略。所谓缓存读取,就是先把文件中的数据读取到一个缓存中,然后以后再次需要读取数据的时候,不再从文件本身去读取,而是从缓存中去读取。通过这种策略使文件I/O读取变成内存读取,从而提高了读取速度。而写入时候也是类似,先写入到缓存中,然后在一次写入磁盘中,从而将多次磁盘写入变为多次内存写入。apr_file_t结构中的buffer结构的作用正是这个目的。缓存读取的策略可以用下面的示意图描述:
使用了缓冲区后,文件操作者将通过缓冲区与磁盘文件打交道。整个缓冲区的实现机制,我们给出下面的一个更详细的图片,通过图示,我们可以理解apr_file_t中一些很晦涩的成员变量,事实上这些成员变量仅仅是配合缓冲区机制而使用的,而且仅仅使用缓冲区的时候才起作用。
上面的图示被我们分为了三个层次:最底层的是进行文件读写的用户,它拥有自定义的缓冲区,我们称之为用户缓冲区;中间的是apr_file_t结构内的缓冲区,它用以保存读写缓存数据。事实上用户总是跟这个层次的缓冲区打交道;最上层的则是磁盘文件。在各个层次中我们用””表示模拟当前的缓冲区大小。
从图示中我们可以看到至少存在四种缓冲区长度:
1)、磁盘文件的实际长度。当使用read在文件中读写的时候,文件内部会维护一个内部指针指示当前的读取位置,apr_file_t结构中使用filePtr模拟该内部指针,因此filePtr总是指向实际文件内部的当前读取位置。
2)、文件缓冲区中现有数据的长度,它的大小由apr_file_t结构内的dataRead指示。通常情况下,dataRead的大小与filePtr指向的位置偏移相等。
3)、最终用户在文件缓冲区中读取的数据的当前指针,由apr_file_t结构内的bufpos指针指示。
4)、用户缓冲区的长度,其最新读写位置由pos指示。
基于缓冲区,整个读取操作的流程发生了根本的变化。任何读取首先尝试从文件缓冲区中读取,如果请求读取的长度在文件缓冲区的长度范围之内,那么直接返回数据。如果需要读取的内容超出了文件缓冲区的范围,那么我们还必须再去实际的磁盘文件中去读取,并返回,同时更新缓存区中的数据。
if (thefile->buffered) {
char *pos = (char *)buf;
apr_uint64_t blocksize;
apr_uint64_t size = *nbytes;
#if APR_HAS_THREADS
if (thefile->thlock) {
apr_thread_mutex_lock(thefile->thlock);
}
#endif
如果支持多线程的操作,那么在对文件进行操作之前必须互斥量锁定,确保操作的安全性。同样在读取结束后还必须是unlock该互斥变量。
if (thefile->direction == 1) {
apr_file_flush(thefile);
thefile->bufpos = 0;
thefile->direction = 0;
thefile->dataRead = 0;
}
rv = 0;
if (thefile->ungetchar != -1) {
*pos = (char)thefile->ungetchar;
++pos;
--size;
thefile->ungetchar = -1;
}
while (rv == 0 && size > 0) {
if (thefile->bufpos >= thefile->dataRead) {
int bytesread = read(thefile->filedes, thefile->buffer, APR_FILE_BUFSIZE);
if (bytesread == 0) {
thefile->eof_hit = TRUE;
rv = APR_EOF;
break;
}
else if (bytesread == -1) {
rv = errno;
break;
}
thefile->dataRead = bytesread;
thefile->filePtr += thefile->dataRead;
thefile->bufpos = 0;
}
blocksize = size > thefile->dataRead - thefile->bufpos ? thefile->dataRead - thefile->bufpos : size;
memcpy(pos, thefile->buffer + thefile->bufpos, blocksize);
thefile->bufpos += blocksize;
pos += blocksize;
size -= blocksize;
}
while循环读取的核心代码就位于while循环中。dataRead是实际读取到缓冲区中的数据的长度,当然它肯定小于或者等于实际的文件的总长度。bufpos则是用户读取的缓冲区中的实际字节数目,当然bufpos肯定也是小于或者等于dataRead的。它们的关系如上图所示。用户如果从缓冲区中读取数据的话它最多只能读取到dataRead字节的长度。当用户从缓冲区中读取数据的时候,缓冲区的内部的指针变量bufpos不断往后移动。
函数将根据bufpos的位置做不同的策略:
(1)、起始的时候,bufpos=0,缓冲区中的数据为dataRead,用户请求的数据为size大小。此时将开始尝试读取缓冲区中的数据。如果请求的字节小于缓冲区的dataRead长度,那么直接将缓冲区中size大小的数据拷贝到输出缓冲区pos中。这种情况是最简单的一种。
由于filepos为0,这时候执行的代码将演变为如下:
blocksize = size;//size > thefile->dataRead -0 ? thefile->dataRead: size;
memcpy(pos, thefile->buffer + 0, blocksize);
thefile->bufpos += blocksize;
pos += size;
size -= size;
(2)、当bufpos=0,即开始读取的时候,如果用户请求的数据长度超出了dataRead的长度,那么此时用户第一次读取只能读取到dataRead的数据,还剩余size-dataRead需要从文件中读取。因此,第一次的读取演变为如下的代码:
blocksize = thefile->dataRead;//size > thefile->dataRead - 0 ? thefile->dataRead - 0 : size;
memcpy(pos, thefile->buffer + 0, blocksize);
thefile->bufpos += blocksize;
pos += thefile->dataRead;
size -= thefile->dataRead;
此时各个指针的位置如下图所示意
从上图可以看到,第一次读取之后,各个指针都指向相应的缓冲区的末尾,pos指向用户缓冲区,随后的数据直接从pos位置往后拷贝;bufpos指向文件缓冲区的末尾。由于此时尚有size-dataRead大小的数据尚未读取,因此,函数将从thefile->filedes中去读取:
if (thefile->bufpos >= thefile->dataRead) {
int bytesread = read(thefile->filedes, thefile->buffer, APR_FILE_BUFSIZE);
if (bytesread == 0) {
thefile->eof_hit = TRUE;
rv = APR_EOF;
break;
}
else if (bytesread == -1) {
rv = errno;
break;
}
thefile->dataRead = bytesread;
thefile->filePtr += thefile->dataRead;
thefile->bufpos = 0;
}
但实际上,当Apache再次到磁盘文件中去读取的时候,它并不会吝啬的仅读取size-dataRead大小,相反它会调用read函数一次性的从文件中读取APR_FILE_BUFSIZE(4K)大小的数据到文件缓冲区中,然后设置dataRead为新的实际的读取字节长度。
*nbytes = pos - (char *)buf;
if (*nbytes) {
rv = 0;
}
#if APR_HAS_THREADS
if (thefile->thlock) {
apr_thread_mutex_unlock(thefile->thlock);
}
#endif
return rv;
}
在apr_file_read函数的基础之上,APR提供了额外的几个辅助函数:
■ APR_DECLARE(apr_status_t) apr_file_getc(char *ch, apr_file_t *thefile);
用以从文件中获取一个字符
■ APR_DECLARE(apr_status_t) apr_file_read_full(apr_file_t *thefile, void *buf,
apr_size_t nbytes,
apr_size_t *bytes_read)
apr_file_read函数通常用于读取指定字节的数据,并且永远不会超过这个指定值,但是实际的读取字节数则可以少于这个值。
而apr_file_read_full与apr_file_read非常类似,主要的区别就是,对于指定的字节数nbytes,函数必须全部读取完毕才肯罢休,任何时候如果发现读取得字节数小于nbytes,函数都将继续等待。这种读取方式我们称之为“全字节读取方式”。
4.5.2字符串读取
4.5.3文件写入
文件写入通过函数apr_file_write实现,与标准的写入函数相同,它也具有三个参数:
APR_DECLARE(apr_status_t) apr_file_write(apr_file_t *thefile, const void *buf, apr_size_t *nbytes)
thefile是写入的文件描述符,buf是写入的源目标字符串,nbytes是需要写入的字节总数目。
与读取类似,写入也分为缓冲写入和非缓冲写入两种。对于非缓冲写入,所有的数据是直接写入到文件中,而对于缓冲写入,所有的数据先写入到文件缓冲区中,然后再从缓冲区中写入到文件中。
对于非缓冲写入的核心代码如下所示:
apr_size_t rv;
do {
rv = write(thefile->filedes, buf, *nbytes);
} while (rv == (apr_size_t)-1 && errno == EINTR);
#ifdef USE_WAIT_FOR_IO
if (rv == (apr_size_t)-1 &&
(errno == EAGAIN || errno == EWOULDBLOCK) &&
thefile->timeout != 0) {
apr_status_t arv = apr_wait_for_io_or_timeout(thefile, NULL, 0);
if (arv != APR_SUCCESS) {
*nbytes = 0;
return arv;
}
else {
do {
do {
rv = write(thefile->filedes, buf, *nbytes);
} while (rv == (apr_size_t)-1 && errno == EINTR);
if (rv == (apr_size_t)-1 &&
(errno == EAGAIN || errno == EWOULDBLOCK)) {
*nbytes /= 2;
}
else {
break;
}
} while (1);
}
}
#endif
if (rv == (apr_size_t)-1) {
(*nbytes) = 0;
return errno;
}
*nbytes = rv;
return APR_SUCCESS;
对于缓冲写入,其实现代码如下所示:
apr_size_t rv;
if (thefile->buffered) {
char *pos = (char *)buf;
int blocksize;
int size = *nbytes;
if ( thefile->direction == 0 ) {
apr_int64_t offset = thefile->filePtr - thefile->dataRead + thefile->bufpos;
if (offset != thefile->filePtr)
lseek(thefile->filedes, offset, SEEK_SET);
u
thefile->bufpos = thefile->dataRead = 0;
thefile->direction = 1;
}
rv = 0;
while (rv == 0 && size > 0) {
if (thefile->bufpos == thefile->bufsize) /* write buffer is full*/
rv = apr_file_flush(thefile);
v
blocksize = size > thefile->bufsize - thefile->bufpos ?
thefile->bufsize - thefile->bufpos : size;
memcpy(thefile->buffer + thefile->bufpos, pos, blocksize);
thefile->bufpos += blocksize;
w
pos += blocksize;
size -= blocksize;
}
return rv;
}
在分析文件缓冲写之前,我们先看一下实际的文件系统的读写情况。在文件系统内部始终维持一个内部位置指针,该指针随着当前读取和写入的位置的改变而不停的改变,同时任何一次读取和写入都是在内部指针指向的位置基础上进行的,因此如果在写之前,文件内部的指针偏移为offset,则写入的数据将插入到offset之后。
这种情况对于APR文件缓冲而言也必须实现同样的效果。不过,如果文件thefile在写之前刚被读过,则存在两个offset偏移位置:
1)、filePtr。任何时候,实际文件内部的指针用filePtr进行模拟指示,它始终与内部指针保持同步。
2)、bufpos。该位置指示用户实际已经读取的缓冲区位置,它在实际文件内部的偏移量为filePtr-(dataRead-bufpos)。
那么在写入时候,新的数据是插入到filePtr之后还是filePtr-(dataRead-bufpos)之后呢?答案只有一个:一切从用户的角度出发。对于最终用户而言,它能够观察到的现象是:数据刚被读取到bufpos位置,至于文件缓冲区的文件长度多少用户根本无法了解到。因此对于用户而言,文件的偏移量应该是filePtr-(dataRead-bufpos),而不是filePtr,即使实际的内部文件指针偏移量确实是filePtr。
因此如果filePtr和filePtr-(dataRead-bufpos)不一致的时候,必须调整文件内部的实际偏移指针为filePtr-(dataRead-bufpos),以保证正确的写入位置。另外必要的工作就是初始化写入缓冲区,这正是代码u所实现的任务。
一旦定位完毕,那么就可以进行数据写入了。用户缓冲区中所有的数据首先都立即写入到文件缓冲区中,写入的时候可能发生的情况包括下面几种:
1)、缓冲区已满。apr_file_t中缓冲区的大小由bufsize确定,默认大小为4K。一旦缓冲区已满,那么缓冲区中的数据将使用apr_file_flush一次性写入到实际的文件中,同时再次初始化缓冲区。Apr_file_flush函数的实现非常简单,顺便一看:
APR_DECLARE(apr_status_t) apr_file_flush(apr_file_t *thefile)
{
if (thefile->buffered) {
if (thefile->direction == 1 && thefile->bufpos) {
apr_ssize_t written;
do {
written = write(thefile->filedes, thefile->buffer, thefile->bufpos);
} while (written == -1 && errno == EINTR);
if (written == -1) {
return errno;
}
thefile->filePtr += written;
thefile->bufpos = 0;
}
}
return APR_SUCCESS;
}
函数中所作的无非就是不断调用write写入。写入成功后filePtr和缓冲区的开始位置,这两个步骤都被隐藏在该函数中,因此你在写入函数中看不到也就不奇怪了。
2)、缓冲区的空闲空间足够写入,即size <= thefile->bufsize - thefile->bufpos。此时,直接调用memcpy将用户缓冲区中的数据拷贝到文件缓冲区中,并调整用户缓冲区pos和文件缓冲区bufpos的位置。
3)、用户要求写入的数据空闲空间不够一次写入,此时将分为多次写入。如果写入过程中空间已满,使用1)的方法,否则使用2)的方法。
在该函数的基础之上,APR还提供了一些辅助扩充函数:
■ APR_DECLARE(apr_status_t) apr_file_putc(char ch, apr_file_t *thefile);
用以向文件中写入一个字符。
■ APR_DECLARE(apr_status_t) apr_file_puts(const char *str, apr_file_t *thefile)
用以向文件中写入一个字符串。
■ APR_DECLARE(apr_status_t) apr_file_writev(apr_file_t *thefile, const struct iovec *vec,
apr_size_t nvec, apr_size_t *nbytes)
批量文件写入。需要批量写入的数据保存在vec中。如果系统中定义了writev函数,则调用writev函数批量写入。如果系统不支持writev函数,那么如果要写入整个iovec数组,可以使用两种变通策略:
第一种就是逐一遍历iovec数组中的每一个元素并将其写入到文件中。这种做法存在一个问题,就是原子性写入。Writev函数写入所有数据的时候是保持原子性的。而迭代则明显无法保持这种特性。
另一种可选策略就是首先将iovec数组中的数据集中写入到一个缓冲区中,然后再将该缓冲区写入到文件中。这种策略也是不合理的,因此你根本就不知道一个iovec数组中会包含多少数据,你也就无法确定缓冲区的大小。
为了保持writev的真正语义,最合理的策略就是仅写入iovec数组中的第一个数据,即vec[0]。Callers of file_writev() must deal with partial writes as they normally would. If you want to ensure an entire iovec is written, use apr_file_writev_full()。
■APR_DECLARE(apr_status_t) apr_file_write_full(apr_file_t *thefile,
const void *buf,
apr_size_t nbytes,
apr_size_t *bytes_written)
与apr_file_read_full类似,该函数使得写入的字符串必须达到指定的nbytes,任何时候如果写入的字符数小于nbytes,函数都将等待。这种方式我们称之为“全字节写入方式”。
¢ APR_DECLARE(apr_status_t) apr_file_writev_full(apr_file_t *thefile,
const struct iovec *vec,
apr_size_t nvec,
apr_size_t *bytes_written)
apr_file_writev_full是apr_file_writev函数的全字节写入方式的版本。该函数定义在fullrw.c中。
文件打开、文件读取以及文件写入是最普通三种文件操作,也是APR中使用的最多的操作,APR中相当一部分的文件操作函数都是基于这三个函数构建起来的,最典型的就是文件内容拷贝和追加。在接下来的部分我们在分析文件内容拷贝和追加的时候更应该关注的是这三个函数的使用。
关于作者
张中庆 ,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!
张中庆 ,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!