原来PostgreSQL底层是这样去创建数据库(CREATE DATABASE)
共 1694字,需浏览 4分钟
·
2022-03-06 13:17
点击关注公众号,Java干货及时送达
1. 概述
在PostgreSQL的源码中,位于src/backend/storage/file/
目录下有一个copydir.c
文件,该文件中共实现了两个函数,分别是:copydir()
和copy_file()
。见名知义,这两个函数分别用于实现指定目录、文件的复制。它们的函数原型如下:
void copydir(char *fromdir, char *todir, bool recurse)
void copy_file(char *fromfile, char *tofile)
下面将分别详细讲解着两个函数的底层函数。
2. copydir()函数
函数copydir()
共有3
个参数,fromdir
、todir
和recurse
。其中参数fromdir
表示要被复制的目录;todir
表示将fromdir
目录下的目录文件存储在该指定位置;而参数recurse
表示在复制fromdir
目录下的内容时,是否忽略子目录。如果recurse
为false
,fromdir
目录下的子目录将会被忽略,任何非目录或常规文件的内容都将被忽略。
下面是copydir()
函数的完整实现,接下来我将对该实现逻辑进行详细的分析。
void copydir(char *fromdir, char *todir, bool recurse)
{
DIR *xldir;
struct dirent *xlde;
char fromfile[MAXPGPATH * 2];
char tofile[MAXPGPATH * 2];
//(1)创建todir指定的目录文件. 该函数内部封装了mkdir()函数。
if (MakePGDirectory(todir) != 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not create directory \"%s\": %m", todir)));
//(2)获取虚拟文件描述符,通过fd.c来管理文件句柄的释放操作。
xldir = AllocateDir(fromdir);
//(3)循环遍历读取指定目录下的文件。直到读取结束
while ((xlde = ReadDir(xldir, fromdir)) != NULL)
{
struct stat fst;
/* 如果我们在拷贝目录时收到取消信号,退出 */
CHECK_FOR_INTERRUPTS();
//(4)特殊目录“.”和“..”跳过.
if (strcmp(xlde->d_name, ".") == 0 ||
strcmp(xlde->d_name, "..") == 0)
continue;
snprintf(fromfile, sizeof(fromfile), "%s/%s", fromdir, xlde->d_name);
snprintf(tofile, sizeof(tofile), "%s/%s", todir, xlde->d_name);
//(5)获取fromdir目录下的各文件信息,比如文件大小、文件名等.
if (lstat(fromfile, &fst) < 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not stat file \"%s\": %m", fromfile)));
//(5.1) 如果该文件是目录文件,那么需要判断参数recurse是true还是false。如果是true,则表示需要处理
// 子目录。这里使用递归。
if (S_ISDIR(fst.st_mode))
{
/* recurse to handle subdirectories */
if (recurse)
copydir(fromfile, tofile, true);
}
// (5.2) 如果是普通的常规文件,则只需文件的拷贝操作,copy_file()函数完成。
else if (S_ISREG(fst.st_mode))
copy_file(fromfile, tofile);
}
// (6) 是否目录DIR句柄。
FreeDir(xldir);
/*
* (7)在这里要小心, 对todir目录下的所有文件进行fsync(), 以确保复制确实完成。
* 但如果fsync被禁用,我们就结束了.
*/
if (!enableFsync)
return;
//(8) 这里和上面的操作一样,获取todir目录流句柄,然后读取该目录下的所有文件。并执行同步刷新磁盘操作(fsync())。
xldir = AllocateDir(todir);
while ((xlde = ReadDir(xldir, todir)) != NULL)
{
struct stat fst;
if (strcmp(xlde->d_name, ".") == 0 ||
strcmp(xlde->d_name, "..") == 0)
continue;
snprintf(tofile, sizeof(tofile), "%s/%s", todir, xlde->d_name);
/*
* 我们不需要在这里同步子目录,因为递归copydir会在它返回之前进行同步.
*/
if (lstat(tofile, &fst) < 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not stat file \"%s\": %m", tofile)));
if (S_ISREG(fst.st_mode))
fsync_fname(tofile, false);
}
FreeDir(xldir);
/*
* 重要的是fsync目标目录本身,因为单独的文件fsync不能保证文件的目录条目是同步的。
* 最新版本的ext4将窗口设置得更宽,但ext3和其他文件系统在过去也是如此。
*/
fsync_fname(todir, true);
}
函数fsync_fname()
完成对指定的目录或文件进行fsync()
操作,同时函数内部负责对出错的各种错误码进行逻辑判断打印处理。其函数原型是:
int fsync_fname(const char *fname, bool isdir)
fname
参数是目录/文件名,isdir
指明fname
是文件还是目录。其完整实现如下:// fsync_fname()会忽略试图打开不可读文件或试图在不允许/不需要的系统上进行fsync目录的错误。
// 其他所有错误都是致命的。
int fsync_fname(const char *fname, bool isdir)
{
int fd;
int flags;
int returncode;
/*
* 一些操作系统要求以只读方式打开目录,而其他系统不允许我们以只读方式打开fsync文件;
* 所以我们需要这两种情况。使用 O_RDWR 会导致我们无法对userid无法写入的文件进行fsync,
* 但我们认为这是可以的。
*/
flags = PG_BINARY;
if (!isdir)
flags |= O_RDWR;
else
flags |= O_RDONLY;
/*
* 打开文件,默默地忽略有关不可读文件的错误(或不支持的操作,例如在 Windows 下打开目录),
* 并记录其他文件。
*/
fd = open(fname, flags, 0);
if (fd < 0)
{
if (errno == EACCES || (isdir && errno == EISDIR))
return 0;
pg_log_error("could not open file \"%s\": %m", fname);
return -1;
}
returncode = fsync(fd);
/*
* 有些操作系统根本不允许我们对目录进行fsync,所以我们可以忽略这些错误。其他任何事情都需要报告。
*/
if (returncode != 0 && !(isdir && (errno == EBADF || errno == EINVAL)))
{
pg_log_fatal("could not fsync file \"%s\": %m", fname);
(void) close(fd);
exit(EXIT_FAILURE);
}
//关闭句柄.
(void) close(fd);
return 0;
}
· copydir()
函数总结:复制源目录下的文件(若recurse
为true
)到目的目录地址下,然后实时对目的目录下的文件/、目录执行fsync
操作,以保证将内核缓冲区中的数据实时刷新到了磁盘上。
3. copy_file()函数
函数copy_file()
的原型如下:
void copy_file(char *fromfile, char *tofile)
它有两个参数:fromfile
和tofile
。其中fromfile
参数指明待被复制的源文件,tofile
表明将fromfile
文件数据复制到的目的文件。
该函数的内部实现如下所示:
void copy_file(char *fromfile, char *tofile)
{
char *buffer;
int srcfd;
int dstfd;
int nbytes;
off_t offset;
off_t flush_offset;
/* Size of copy buffer (read and write requests) */
#define COPY_BUF_SIZE (8 * BLCKSZ)
/*
* 数据刷新请求的大小。在大多数平台上,每1MB左右做一次这样的操作似乎是有益的。
* 但是macOS,至少在早期版本的APFS中,对小的mmap/msync请求是非常不友好的,所以每32MB就会执行一次。
*/
#if defined(__darwin__)
#define FLUSH_DISTANCE (32 * 1024 * 1024)
#else
#define FLUSH_DISTANCE (1024 * 1024)
#endif
/*
* 使用palloc确保我们得到一个最大对齐的缓冲区
*/
buffer = palloc(COPY_BUF_SIZE);
//打开fromfile文件
srcfd = OpenTransientFile(fromfile, O_RDONLY | PG_BINARY);
if (srcfd < 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not open file \"%s\": %m", fromfile)));
//打开tofile文件
dstfd = OpenTransientFile(tofile, O_RDWR | O_CREAT | O_EXCL | PG_BINARY);
if (dstfd < 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not create file \"%s\": %m", tofile)));
// 开始文件数据复制。nbytes已经复制好的数据.
flush_offset = 0;
for (offset = 0;; offset += nbytes)
{
/* If we got a cancel signal during the copy of the file, quit */
CHECK_FOR_INTERRUPTS();
/*
* 我们稍后会对这些文件进行fsync,但是在复制过程中,要经常刷新它们,以避免在缓存中产生垃圾信息,
* 并希望在fsync到来之前让内核开始写这些文件。
*/
if (offset - flush_offset >= FLUSH_DISTANCE)
{
////// 缓冲指定大小的内核缓冲区数据
pg_flush_data(dstfd, flush_offset, offset - flush_offset);
flush_offset = offset;
}
pgstat_report_wait_start(WAIT_EVENT_COPY_FILE_READ);
nbytes = read(srcfd, buffer, COPY_BUF_SIZE);
pgstat_report_wait_end();
if (nbytes < 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not read file \"%s\": %m", fromfile)));
if (nbytes == 0)
break;
errno = 0;
pgstat_report_wait_start(WAIT_EVENT_COPY_FILE_WRITE);
if ((int) write(dstfd, buffer, nbytes) != nbytes)
{
/* if write didn't set errno, assume problem is no disk space */
if (errno == 0)
errno = ENOSPC;
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to file \"%s\": %m", tofile)));
}
pgstat_report_wait_end();
}
if (offset > flush_offset)
pg_flush_data(dstfd, flush_offset, offset - flush_offset);
// 关闭tofile文件句柄
if (CloseTransientFile(dstfd) != 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not close file \"%s\": %m", tofile)));
//关闭fromfile文件句柄
if (CloseTransientFile(srcfd) != 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not close file \"%s\": %m", fromfile)));
// 是否掉buffer内存缓冲区
pfree(buffer);
}
在copy_file()
函数内部,调用了pg_flush_data()
函数。该函数通知操作系统应该刷新所描述的脏数据,offset
为0
,nbytes
为0
,意味着应该刷新整个文件。
void
pg_flush_data(int fd, off_t offset, off_t nbytes)
{
// 目前,文件刷新主要用于避免以后的fsync()/fdatasync(),它的调用影响较小。 因此,
// 如果fsync被禁用,就不会触发刷新——这是我们可能希望在某些时候可配置的一个决定。
if (!enableFsync)
return;
/*
* 我们编译当前平台支持的所有替代方案,以便更容易地发现可移植性问题。
*/
#if defined(HAVE_SYNC_FILE_RANGE)
{
int rc;
static bool not_implemented_by_kernel = false;
if (not_implemented_by_kernel)
return;
/*
* sync_file_range(SYNC_FILE_RANGE_WRITE),当前特定于 linux,告诉操作系统应该开始指定块的写回,
* 但我们不想等待完成。 请注意,如果范围中存在太多脏数据,则此调用可能会阻塞。
* 这是支持它的操作系统上的首选方法,因为它在可用时可靠地工作(与 msync() 相比)
* 并且不会清除干净的数据(如 FADV_DONTNEED)。
*/
rc = sync_file_range(fd, offset, nbytes,
SYNC_FILE_RANGE_WRITE);
if (rc != 0)
{
int elevel;
/*
* 对于没有sync_file_range()实现的系统,比如Windows WSL,只生成一个警告,
* 然后抑制该进程的所有进一步尝试。
*/
if (errno == ENOSYS)
{
elevel = WARNING;
not_implemented_by_kernel = true;
}
else
elevel = data_sync_elevel(WARNING);
ereport(elevel,
(errcode_for_file_access(),
errmsg("could not flush dirty data: %m")));
}
return;
}
#endif
#if !defined(WIN32) && defined(MS_ASYNC)
{
void *p;
static int pagesize = 0;
/*
* 在多个操作系统上,mmap文件上的 msync(MS_ASYNC) 会触发写回。 在linux上,它仅在指定 MS_SYNC 时才会这样做,
* 但随后它会同步进行回写。 幸运的是,所有常见的 linux 系统都有 sync_file_range()。 这比 FADV_DONTNEED 更可取,
* 因为它不会清除干净的数据。
* 我们映射文件(mmap()),告诉内核同步回内容(msync()),然后再次删除映射(munmap())。
*/
/* mmap() needs actual length if we want to map whole file */
if (offset == 0 && nbytes == 0)
{
nbytes = lseek(fd, 0, SEEK_END);
if (nbytes < 0)
{
ereport(WARNING,
(errcode_for_file_access(),
errmsg("could not determine dirty data size: %m")));
return;
}
}
/*
* 一些平台拒绝部分页面的mmap()尝试。要处理这个问题,只需将请求截短到一个页面边界。
* 如果任何额外的字节没有被刷新,好吧,这只是一个提示。
*/
/* fetch pagesize only once */
if (pagesize == 0)
pagesize = sysconf(_SC_PAGESIZE);
/* align length to pagesize, dropping any fractional page */
if (pagesize > 0)
nbytes = (nbytes / pagesize) * pagesize;
/* fractional-page request is a no-op */
if (nbytes <= 0)
return;
/*
* mmap很可能会失败,尤其是在32位平台上,那里可能根本没有足够的地址空间。
* 如果是这样,就悄悄地进入下一个实现。
*/
if (nbytes <= (off_t) SSIZE_MAX)
p = mmap(NULL, nbytes, PROT_READ, MAP_SHARED, fd, offset);
else
p = MAP_FAILED;
if (p != MAP_FAILED)
{
int rc;
rc = msync(p, (size_t) nbytes, MS_ASYNC);
if (rc != 0)
{
ereport(data_sync_elevel(WARNING),
(errcode_for_file_access(),
errmsg("could not flush dirty data: %m")));
/* NB: need to fall through to munmap()! */
}
rc = munmap(p, (size_t) nbytes);
if (rc != 0)
{
/* FATAL error because mapping would remain */
ereport(FATAL,
(errcode_for_file_access(),
errmsg("could not munmap() while flushing data: %m")));
}
return;
}
}
#endif
#if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
{
int rc;
/*
* 向内核发出信号,表示传入的范围不应再缓存。这样做的副作用是写出脏数据,
* 而副作用是可能丢弃有用的干净缓存块。出于后一个原因,这是最不可取的方法。
*/
rc = posix_fadvise(fd, offset, nbytes, POSIX_FADV_DONTNEED);
if (rc != 0)
{
/* don't error out, this is just a performance optimization */
ereport(WARNING,
(errcode_for_file_access(),
errmsg("could not flush dirty data: %m")));
}
return;
}
#endif
}
4. 哪些地方使用copydir()、copy_file() ?
在PostgreSQL数据库中,当“创建数据库(CREATE DATABASE 数据库名
)、修改数据库属性(ALTER DATABASE SET TABLESPACE
)和数据库资源管理器的例程(DATABASE resource manager's routines
)”时候,都会使用到copydir()
函数。
当我们使用CREATE DATABASE
来创建数据库时候,其实底层的逻辑就是直接将base/1
(即template1
模板数据库)目录下的文件拷贝到新创建的数据库目录下。
更多关于模板数据库的知识请阅读? 《 一文搞懂PostgreSQL中的template1、template0和postgres系统数据库 》。
为了演示上面的说明,我们重新编译PostgreSQL内核源码,在copydir()
函数的调用处,增加如下日志打印:
void
copydir(char *fromdir, char *todir, bool recurse)
{
if(NULL != fromdir || NULL != todir)
{
/// 打印文件fromdir和todir
ereport(INFO, (errmsg_internal("fromdir[%s] todir[%s]", fromdir, todir)));
}
DIR *xldir;
struct dirent *xlde;
char fromfile[MAXPGPATH * 2];
char tofile[MAXPGPATH * 2];
//.........省略
}
之后使用客户端命令psql
登录PostgreSQL数据库,然后创建Test
数据库,观察日志打印信息:
通过查询 pg_database
(select *from pg_database;
)数据,得到新创建的Test
数据库的Oid
是24578
(这与上面日志中的打印提示也刚好能够对应匹配。fromdir[base/1]
todir[base/24578]
)。该文件位于/base
目录下。
通过比对发现,/base/1
目录下的文件与/base/24578
目录下的文件无论是数量还是文件名、文件大小都是刚好吻合的。
5. 相关库函数、系统函数
在copydir.c
文件中,用到了许多库函数、系统函数。这些函数平时项目中用得比较少,因此,我这里作了下总结,并用xmind画了一个图。其中各库函数、系统函数的作用如下所示:
注:《Linux系统编程》 第2版 ---> readdir()
函数在“读取完整个目录”和“读取出错”两个情况下,都会返回NULL
。因此,必须在每次调用readdir()
之前将errno
设置为0
,并在之后检查返回值和errno
值。PostgreSQL源码中的copydir()
函数内部在调用封装readdir()
时候也是这样书写的代码。