P: 0cm; BORDER-BOTTOM: windowtext 1pt solid; HEIGHT: 15pt" width=348>
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;
编辑 webmaster








