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与之联系!
[1] [2] [3] [4] [5] [6] [7] [8] [9]