原来PostgreSQL底层是这样创建数据库

2022-03-10 00:00:00 函数 数据库 文件 目录 目录下



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个参数,fromdirtodirrecurse。其中参数fromdir表示要被复制的目录;todir表示将fromdir目录下的目录文件存储在该指定位置;而参数recurse表示在复制fromdir目录下的内容时,是否忽略子目录。如果recursefalsefromdir目录下的子目录将会被忽略,任何非目录或常规文件的内容都将被忽略。

下面是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) != )
  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, ".") ==  ||
   strcmp(xlde->d_name, "..") == )
   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) < )
   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, ".") ==  ||
   strcmp(xlde->d_name, "..") == )
   continue;

  snprintf(tofile, sizeof(tofile), "%s/%s", todir, xlde->d_name);

  /*
   * 我们不需要在这里同步子目录,因为递归copydir会在它返回之前进行同步.
   */

  if (lstat(tofile, &fst) < )
   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, );
 if (fd < )
 {
  if (errno == EACCES || (isdir && errno == EISDIR))
   return ;
  pg_log_error("could not open file \"%s\": %m", fname);
  return -1;
 }

 returncode = fsync(fd);

 /*
  * 有些操作系统根本不允许我们对目录进行fsync,所以我们可以忽略这些错误。其他任何事情都需要报告。
  */

 if (returncode !=  && !(isdir && (errno == EBADF || errno == EINVAL)))
 {
  pg_log_fatal("could not fsync file \"%s\": %m", fname);
  (voidclose(fd);
  exit(EXIT_FAILURE);
 }

 //关闭句柄.
 (voidclose(fd);
 return ;
}

· copydir()函数总结:复制源目录下的文件(若recursetrue)到目的目录地址下,然后实时对目的目录下的文件/、目录执行fsync操作,以保证将内核缓冲区中的数据实时刷新到了磁盘上。

3. copy_file()函数

函数copy_file()的原型如下:

void copy_file(char *fromfile, char *tofile)

它有两个参数:fromfiletofile。其中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 < )
  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 < )
  ereport(ERROR,
    (errcode_for_file_access(),
     errmsg("could not create file \"%s\": %m", tofile)));

 // 开始文件数据复制。nbytes已经复制好的数据.
 flush_offset = ;
 for (offset = ;; 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 < )
   ereport(ERROR,
     (errcode_for_file_access(),
      errmsg("could not read file \"%s\": %m", fromfile)));
  if (nbytes == )
   break;
  errno = ;
  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 == )
    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) != )
  ereport(ERROR,
    (errcode_for_file_access(),
     errmsg("could not close file \"%s\": %m", tofile)));
 //关闭fromfile文件句柄
 if (CloseTransientFile(srcfd) != )
  ereport(ERROR,
    (errcode_for_file_access(),
     errmsg("could not close file \"%s\": %m", fromfile)));
 // 是否掉buffer内存缓冲区
 pfree(buffer);
}

copy_file()函数内部,调用了pg_flush_data()函数。该函数通知操作系统应该刷新所描述的脏数据,offsetnbytes,意味着应该刷新整个文件。

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 != )
  {
   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 = ;

  /*
   * 在多个操作系统上,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 ==  && nbytes == )
  {
   nbytes = lseek(fd, , SEEK_END);
   if (nbytes < )
   {
    ereport(WARNING,
      (errcode_for_file_access(),
       errmsg("could not determine dirty data size: %m")));
    return;
   }
  }

  /*
   * 一些平台拒绝部分页面的mmap()尝试。要处理这个问题,只需将请求截短到一个页面边界。
   * 如果任何额外的字节没有被刷新,好吧,这只是一个提示。
   */


  /* fetch pagesize only once */
  if (pagesize == )
   pagesize = sysconf(_SC_PAGESIZE);

  /* align length to pagesize, dropping any fractional page */
  if (pagesize > )
   nbytes = (nbytes / pagesize) * pagesize;

  /* fractional-page request is a no-op */
  if (nbytes <= )
   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 != )
   {
    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 != )
   {
    /* 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 != )
  {
   /* 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_databaseselect *from pg_database;)数据,得到新创建的Test数据库的Oid24578(这与上面日志中的打印提示也刚好能够对应匹配。fromdir[base/1] todir[base/24578])。该文件位于/base目录下。

通过比对发现,/base/1目录下的文件与/base/24578目录下的文件无论是数量还是文件名、文件大小都是刚好吻合的。

5. 相关库函数、系统函数

copydir.c文件中,用到了许多库函数、系统函数。这些函数平时项目中用得比较少,因此,我这里作了下总结,并用xmind画了一个图。其中各库函数、系统函数的作用如下所示:

注:《Linux系统编程》 第2版 ---> readdir()函数在“读取完整个目录”和“读取出错”两个情况下,都会返回NULL。因此,必须在每次调用readdir()之前将errno设置为,并在之后检查返回值和errno值。PostgreSQL源码中的copydir()函数内部在调用封装readdir()时候也是这样书写的代码。



以上文章来源于君子黎 ,作者君子黎

相关文章