nginx文件结构

        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;
}

图:buf为连续缓冲区,则用一个iovec存储

至此,nginx中的关于文件操作都介绍完了。限于篇幅,这里只把比较难理解的拿出来分析,其余的就不分析了。剩余的一些文件操作接口,例如: 重命名文件、拷贝文件、读文件、写文件等,只需要花点时间去看,很容易看懂。如有问题可以留言或者到刚建立的QQ群里交流。

nginx源码分析交流群: 386375629

展开阅读全文

没有更多推荐了,返回首页