nginx服务器接收到客户端的http请求包体后,会将包体存放到内存中。然而内存空间是有限的,如果客户端发送了一个2G的文件,则这2G数据是无法全部存放到内存中的。nginx服务器接收到一定量的数据后,会把剩余的数据缓存到文件中。
文件对象结构:
struct ngx_file_s
{
ngx_fd_t fd; //文件描述名
ngx_str_t name; //文件名
ngx_file_info_t info; //struct stat结构变量,存放文件的详细信息,例如文件大小
off_t offset; //记录文件偏移,表示已经读或者写到文件的位置
off_t sys_offset; //默认情况等于offset,如果不相等,会被强制修改为offset值
ngx_log_t *log; //日志对象
unsigned valid_info:1;
unsigned directio:1;
};
目录对象结构:
typedef struct
{
ngx_str_t name; //文件名
size_t len; //level中三个数据之和
size_t level[3]; //每一层子目录的长度
//以下字段只在文件缓存管理模块被使用,在分析文件缓存模块时再详细描述,可以先跳过
ngx_path_manager_pt manager; //文件缓存管理回调,为ngx_http_file_cache_manager,
//在ngx_cache_manager_process_handler中被调用
ngx_path_loader_pt loader; //文件缓存loader回调,为ngx_http_file_cache_loader。
//在ngx_cache_loader_process_handler中被调用
void *data; //ngx_http_file_cache_t文件缓存对象
u_char *conf_file;
ngx_uint_t line;
} ngx_path_t;
nginx文件目录结构
上图是nginx创建的临时文件结构。nginx在根目录下最多创建3层目录结构;例如"/home/nginx/tmpfile/9/32/198", /home/nginx/tmpfile/为根目录, 9/32/198则为3层子目录结构。
一、创建一个临时文件
函数ngx_create_temp_file将创建一个临时文件。由两个过程组成:
1、拼装成某种格式的完整的路径,例如/home/nginx/tmpfile/9/32/178/00004178329这种格式文件名
2、创建文件
创建过程比较复杂,可以参见下面的图示;
//创建一个临时文件
//参数: persistent, access表示文件权限
// clean 表示文件不再使用时,1:删除文件,0:关闭文件
// file 记录了文件名信息
// path 记录了文件的目录信息
//例如: 拼装/home/nginx/tmpfile/9/32/178/00004178329这种格式文件名后,然后创建文件
ngx_int_t ngx_create_temp_file(ngx_file_t *file, ngx_path_t *path, ngx_pool_t *pool,
ngx_uint_t persistent, ngx_uint_t clean, ngx_uint_t access)
{
uint32_t n;
ngx_err_t err;
ngx_pool_cleanup_t *cln;
ngx_pool_cleanup_file_t *clnf;
//计算文件名的长度。例如:/home/nginx/tmpfile/9/32/178/00004178329
//path->name.len表示的是目录的长度
//path->len表示的是子目录的层数,最多三层
//10表示文件名
// 1表示文件名最后1位由随机数组成
//可以参考下面的布局图
file->name.len = path->name.len + 1 + path->len + 10;
file->name.data = ngx_pnalloc(pool, file->name.len + 1);
if (file->name.data == NULL)
{
return NGX_ERROR;
}
//先得到文件的根目录
ngx_memcpy(file->name.data, path->name.data, path->name.len);
//获取一个随机数
n = (uint32_t) ngx_next_temp_number(0);
//创建一个文件清理对象,并加入到链表。目的是在文件回收时,能够关闭文件,或者删除文件
cln = ngx_pool_cleanup_add(pool, sizeof(ngx_pool_cleanup_file_t));
if (cln == NULL)
{
return NGX_ERROR;
}
//为什么需要循环? 因为如果拼装后的文件目录不存在,
//则先创建目录后,然后循环继续拼装一个完整的文件路径
for ( ;; )
{
//文件名由11位数组成,前10位不够时补0,第11位为随机数
//此时结果为"/home/nginx/tmpfile/ 00004178329",这时还没有生成中间的子目录
(void) ngx_sprintf(file->name.data + path->name.len + 1 + path->len,
"%010uD%Z", n);
//根据最后11位文件名,生成中间的子目录
//此时得到完成的文件路径"/home/nginx/tmpfile/9/32/178/00004178329"
ngx_create_hashed_filename(path, file->name.data, file->name.len);
ngx_log_debug1(NGX_LOG_DEBUG_CORE, file->log, 0,
"hashed path: %s", file->name.data);
//打开文件
file->fd = ngx_open_tempfile(file->name.data, persistent, access);
ngx_log_debug1(NGX_LOG_DEBUG_CORE, file->log, 0,
"temp fd:%d", file->fd);
//注册文件清理回调
if (file->fd != NGX_INVALID_FILE)
{
cln->handler = clean ? ngx_pool_delete_file : ngx_pool_cleanup_file;
clnf = cln->data;
clnf->fd = file->fd;
clnf->name = file->name.data;
clnf->log = pool->log;
return NGX_OK;
}
err = ngx_errno;
//文件存在,则继续拼装,得到文件的完整路径,直到文件名不相同
if (err == NGX_EEXIST)
{
n = (uint32_t) ngx_next_temp_number(1);
continue;
}
if ((path->level[0] == 0) || (err != NGX_ENOPATH))
{
ngx_log_error(NGX_LOG_CRIT, file->log, err,
ngx_open_tempfile_n " \"%s\" failed",
file->name.data);
return NGX_ERROR;
}
//如果是目录不存在,则创建子目录。例如"/home/nginx/tmpfile/9/32/178/00004178329"
//则将创建"/home/nginx/tmpfile/9","/home/nginx/tmpfile/9/32",
//"/home/nginx/tmpfile/9/32/178"这三个目录
if (ngx_create_path(file, path) == NGX_ERROR)
{
return NGX_ERROR;
}
}
}
在函数中调用了ngx_create_hashed_filename这个函数,这个函数将根据最后11位的文件名,生成中间的子目录。例如"/home/nginx/tmpfile/ 00004178329",则根据00004178329,生成中间的子目录/9/32/178, 最后完整路径为: "/home/nginx/tmpfile/9/32/178/00004178329"
//功能:由文件名生成3层子目录结构.例如/home/nginx/tmpfile/9/32/178/00004178329,
//则这个函数将生成/9/32/178/这三层子目录结构。
//9表示00004178329中的最后一位
//32表示00004178329中的第9至第10位
//178表示00004178329中的第6至第8位
void ngx_create_hashed_filename(ngx_path_t *path, u_char *file, size_t len)
{
size_t i, level;
ngx_uint_t n;
i = path->name.len + 1;
file[path->name.len + path->len] = '/';
//子目录最多由3层目录组成
for (n = 0; n < 3; n++)
{
//每一个子目录的长度
level = path->level[n];
if (level == 0)
{
break;
}
//从文件名的最后面位置开始,取level位内容,当做子目录
len -= level;
file[i - 1] = '/';
ngx_memcpy(&file[i], &file[len], level);
i += level + 1;
}
}
在创建临时文件函数中,还调用了ngx_create_path函数。如果子目录不存在,则这个函数将创建所有子目录。
//创建目录。例如"/home/nginx/tmpfile/9/32/178/00004178329"
//则将创建"/home/nginx/tmpfile/9","/home/nginx/tmpfile/9/32",
//"/home/nginx/tmpfile/9/32/178"这三个目录
ngx_int_t ngx_create_path(ngx_file_t *file, ngx_path_t *path)
{
size_t pos;
ngx_err_t err;
ngx_uint_t i;
pos = path->name.len;
//最多创建3层子目录
for (i = 0; i < 3; i++)
{
if (path->level[i] == 0)
{
break;
}
//得到每一个子目录的名称
pos += path->level[i] + 1;
file->name.data[pos] = '\0';
ngx_log_debug1(NGX_LOG_DEBUG_CORE, file->log, 0,
"temp file: \"%s\"", file->name.data);
//创建子目录
if (ngx_create_dir(file->name.data, 0700) == NGX_FILE_ERROR)
{
err = ngx_errno;
if (err != NGX_EEXIST)
{
ngx_log_error(NGX_LOG_CRIT, file->log, err,
ngx_create_dir_n " \"%s\" failed",
file->name.data);
return NGX_ERROR;
}
}
file->name.data[pos] = '/';
}
return NGX_OK;
}
二、将缓存数据写入到文件
想要把缓存的数据全部写入到文件中,则需要调用ngx_write_chain_to_temp_file函数。函数将chain链表数据全部写入到文件中保存。注意: clain链表数据全部保存到了临时文件后,这个函数并不会释放clain链表空间,而是由调用者决定是否需要释放。函数只负责保存文件,遵从单一原则,一个函数只做一件事情。
ngx_write_chain_to_temp_file函数会用到ngx_temp_file_t结构。这个结构没有特殊的地方,其实是对ngx_file_t, ngx_path_t两个结构进行了一次封装而已。
typedef struct
{
ngx_file_t file; //文件结构
off_t offset; //读写文件偏移
ngx_path_t *path; //文件路径结构
ngx_pool_t *pool; //内存池
char *warn; //告警信息,用于打印日志
ngx_uint_t access; //文件权限
unsigned log_level:8; //日志级别
unsigned persistent:1;//是否为永久性文件
unsigned clean:1; //在文件不用时的操作: 0关闭文件, 1删除文件
} ngx_temp_file_t;
//将chain链表中的所有数据都写入到文件
//写入后clain链表内容并没有清空,由调用者决定是否要清空
//返回值:写入了多少数据
ssize_t ngx_write_chain_to_temp_file(ngx_temp_file_t *tf, ngx_chain_t *chain)
{
ngx_int_t rc;
//文件不存在,则创建
if (tf->file.fd == NGX_INVALID_FILE)
{
rc = ngx_create_temp_file(&tf->file, tf->path, tf->pool,
tf->persistent, tf->clean, tf->access);
if (rc == NGX_ERROR || rc == NGX_AGAIN)
{
return rc;
}
if (tf->log_level)
{
ngx_log_error(tf->log_level, tf->file.log, 0, "%s %V",
tf->warn, &tf->file.name);
}
}
//将chain链表中的所有数据都写入到文件offset开始的位置
return ngx_write_chain_to_file(&tf->file, chain, tf->offset, tf->pool);
}
写文件操作,最终由
ngx_write_chain_to_file函数负责。步骤如下
1、函数将chain链表数据尽量保存到一个struct iovec结构中。一个struct iovec结构可以存放多个chain链表数据
2、将struct iovec保存的数据写入到文件
3、更新文件偏移
struct iovec数组与chain链表的关系
这里需要区分下chain链表与buf, 图是为了辅助理解,其中每个iovec保存的是chain链表中的buf数据,并不是整个chain链表。
//将chain链表中的所有数据都写入到文件offset开始的位置
//写入后clain链表内容并没有清空,由调用者决定是否要清空
ssize_t ngx_write_chain_to_file(ngx_file_t *file, ngx_chain_t *cl, off_t offset,
ngx_pool_t *pool)
{
u_char *prev;
size_t size;
ssize_t total, n;
ngx_array_t vec;
struct iovec *iov, iovs[NGX_IOVS];
//如果chain链表只有一个元素,则直接写入到文件中
if (cl->next == NULL)
{
return ngx_write_file(file, cl->buf->pos,
(size_t) (cl->buf->last - cl->buf->pos),
offset);
}
total = 0;
//使用struct iovec保存写入的clain链表数据
vec.elts = iovs;
vec.size = sizeof(struct iovec);
vec.nalloc = NGX_IOVS;
vec.pool = pool;
//外循环控制所有clain链表中的数据都能写入到文件
//内循环控制每次最多允许写入多少数据到文件
do
{
prev = NULL;
iov = NULL;
size = 0;
vec.nelts = 0;
//一个iov结构可以存放多个clain链表节点的内容
while (cl && vec.nelts < IOV_MAX)
{
//前后两个clain链表节点的buf是一个连续的缓冲区才有这种情况,参见图:<span style="text-align: center;">buf为连续缓冲区,则用一个iovec存储</span>
if (prev == cl->buf->pos)
{
iov->iov_len += cl->buf->last - cl->buf->pos;
}
else
{
iov = ngx_array_push(&vec); //从数组中取出一个iov结构
if (iov == NULL)
{
return NGX_ERROR;
}
//保存chain数组
iov->iov_base = (void *) cl->buf->pos;
iov->iov_len = cl->buf->last - cl->buf->pos;
}
//记录保存的数据大小
size += cl->buf->last - cl->buf->pos;
prev = cl->buf->last; //记录前一个buf的结束位置,用于判断前后两个buf是否为连续的缓冲区
cl = cl->next;
}
//整个链表chain都可以用一个struct iovec存储,则直接写入文件,之后退出循环。
if (vec.nelts == 1)
{
iov = vec.elts;
n = ngx_write_file(file, (u_char *) iov[0].iov_base,
iov[0].iov_len, offset);
if (n == NGX_ERROR)
{
return n;
}
return total + n;
}
//调整偏移
if (file->sys_offset != offset)
{
if (lseek(file->fd, offset, SEEK_SET) == -1)
{
ngx_log_error(NGX_LOG_CRIT, file->log, ngx_errno,
"lseek() \"%s\" failed", file->name.data);
return NGX_ERROR;
}
file->sys_offset = offset;
}
//将struct iovec结构写入文件
n = writev(file->fd, vec.elts, vec.nelts);
if (n == -1)
{
ngx_log_error(NGX_LOG_CRIT, file->log, ngx_errno,
"writev() \"%s\" failed", file->name.data);
return NGX_ERROR;
}
//写入失败
if ((size_t) n != size)
{
ngx_log_error(NGX_LOG_CRIT, file->log, 0,
"writev() \"%s\" has written only %z of %uz",
file->name.data, n, size);
return NGX_ERROR;
}
//更新文件偏移
file->sys_offset += n;
file->offset += n;
//记录写入了多少数据到文件
total += n;
} while (cl);
//返回已经写入了多少数据到文件
return total;
}
至此,nginx中的关于文件操作都介绍完了。限于篇幅,这里只把比较难理解的拿出来分析,其余的就不分析了。剩余的一些文件操作接口,例如: 重命名文件、拷贝文件、读文件、写文件等,只需要花点时间去看,很容易看懂。如有问题可以留言或者到刚建立的QQ群里交流。
nginx源码分析交流群: 386375629