Приветствую!

Продолжаем изучение работы PostgreSQL. Мы остановились на моменте общей инициализации процесса. Сегодня мы рассмотрим процесс инициализации Postgres — входную точку до основного цикла.

PostgresMain — точка входа для процесса бэкэнда. Расположена в src/backend/tcop/postgres.c

Бэкэнд может работать как автономно (Standalone), так и в обычном (многопользовательском) режиме (из‑под Postmaster)

Описание работы ведется с учетом работы в многопользовательском режиме (IsUnderPostmasterравен true)

Работа в автономном режиме

Если работа идет автономно, то многие опции еще не инициализированы. Например, настройки GUC. Поэтому, перед самим циклом нужно проинициализировать все значения, но только если мы запустились не из-под Postmaster. 

Для этого во время инициализации из разных мест вызываются многие функции инициализации встречавшиеся ранее. Например, InitializeGUCOptions, но с проверкой, что не запустились из Postmaster. Для проверки этого есть переменная IsUnderPostmaster.

В коде есть много мест с паттерном

if (!IsUnderPostmaster) {
  someOperation();
}

Его можно интерпретировать как "выполнение функции someOperation должно происходить при запуске в автономном режиме"

Например, в самом начале точки входа есть такая проверка

void
PostgresMain(int argc, char *argv[],
			 const char *dbname,
			 const char *username)
{
  int			firstchar;
  StringInfoData input_message;
  sigjmp_buf	local_sigjmp_buf;
  volatile bool send_ready_for_query = true;
  bool		idle_in_transaction_timeout_enabled = false;
  bool		idle_session_timeout_enabled = false;

  /* Initialize startup process environment if necessary. */
  if (!IsUnderPostmaster)
      InitStandaloneProcess(argv[0]);
  
  // ...
}

Установка состояния работы

Как только начинаем работу в качестве бэкэнда, то входим в состояние InitProcessing.

Состояние нормальной работы (NormalProcessing)выставляется после инициализации процесса бэкэнда (InitPostgres)

ProcessingMode

Не только Postmaster имеет машину состояний, но и бэкэнд. Его машина состояний описывает перечислением ProcessingMode

typedef enum ProcessingMode
{
	BootstrapProcessing,		/* bootstrap creation of template database */
	InitProcessing,				/* initializing system */
	NormalProcessing			/* normal processing */
} ProcessingMode;

Всего есть 3 состояния:

  • BootstrapProcessing - создание БД из шаблона

  • InitProcessing - старт и инициализация бэкэнда

  • NormalProcessing - обычная работа

Для них также определены свои макросы:

extern ProcessingMode Mode;

#define IsBootstrapProcessingMode() (Mode == BootstrapProcessing)
#define IsInitProcessingMode()		(Mode == InitProcessing)
#define IsNormalProcessingMode()	(Mode == NormalProcessing)

#define GetProcessingMode() Mode

#define SetProcessingMode(mode) \
	do { \
		AssertArg((mode) == BootstrapProcessing || \
				  (mode) == InitProcessing || \
				  (mode) == NormalProcessing); \
		Mode = (mode); \
	} while(0)

Перечисление и макросы определены в src/include/miscadmin.h

Сигналы

После настраиваем свои обработчики сигналов:

  • SIGHUP - перезагрузка конфигурации;

  • SIGINT - отмена текущего запроса (Fast shutdown);

  • SIGTERM - отмена текущего запроса и выход (Smart shutdown);

  • SIGQUIT - быстрый выход (Immediate shutdown);

  • SIGPIPE - игнорируется, т.к. это безопасней чем прерывать ‘who-knows-what operation’, а заметим мы это на следующей итерации цикла;

  • SIGUSR1 - имеет множественные значения, например, восстановление БД;

  • SIGUSR2 - игнорируется;

  • SIGFPE - ошибка операций с плавающей точкой.

Множественные значения на 1 сигнал

SIGUSR1 - сигнал со множеством различных значений. Но как определяется значение?

В Postgres для этого используется общая память.

Структура ProcSignalSlot предоставляет возможность передачи дополнительной информации для сигналов

typedef struct
{
	volatile pid_t pss_pid;
	volatile sig_atomic_t pss_signalFlags[NUM_PROCSIGNALS];
	pg_atomic_uint64 pss_barrierGeneration;
	pg_atomic_uint32 pss_barrierCheckMask;
	ConditionVariable pss_barrierCV;
} ProcSignalSlot;

Поле pss_signalFlags - массив булевских значений, представляющий причины для сигналов.

Сами причины определяются перечислением ProcSignalReason

typedef enum
{
	PROCSIG_CATCHUP_INTERRUPT,	/* sinval catchup interrupt */
	PROCSIG_NOTIFY_INTERRUPT,	/* listen/notify interrupt */
	PROCSIG_PARALLEL_MESSAGE,	/* message from cooperating parallel backend */
	PROCSIG_WALSND_INIT_STOPPING,	/* ask walsenders to prepare for shutdown  */
	PROCSIG_BARRIER,			/* global barrier interrupt  */
	PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */

	/* Recovery conflict reasons */
	PROCSIG_RECOVERY_CONFLICT_DATABASE,
	PROCSIG_RECOVERY_CONFLICT_TABLESPACE,
	PROCSIG_RECOVERY_CONFLICT_LOCK,
	PROCSIG_RECOVERY_CONFLICT_SNAPSHOT,
	PROCSIG_RECOVERY_CONFLICT_BUFFERPIN,
	PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK,

	NUM_PROCSIGNALS				/* Must be last! */
} ProcSignalReason;

Для обработки SIGUSR1 используется следующим образом

// Обработчик SIGUSR1 срабатывает
void
procsignal_sigusr1_handler(SIGNAL_ARGS)
{
	int			save_errno = errno;

	if (CheckProcSignal(PROCSIG_CATCHUP_INTERRUPT))
		HandleCatchupInterrupt();

	if (CheckProcSignal(PROCSIG_NOTIFY_INTERRUPT))
		HandleNotifyInterrupt();

	if (CheckProcSignal(PROCSIG_PARALLEL_MESSAGE))
		HandleParallelMessageInterrupt();

	if (CheckProcSignal(PROCSIG_WALSND_INIT_STOPPING))
		HandleWalSndInitStopping();

	if (CheckProcSignal(PROCSIG_BARRIER))
		HandleProcSignalBarrierInterrupt();

	if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT))
		HandleLogMemoryContextInterrupt();

	if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
		RecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);

	if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_TABLESPACE))
		RecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_TABLESPACE);

	if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_LOCK))
		RecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_LOCK);

	if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_SNAPSHOT))
		RecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_SNAPSHOT);

	if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK))
		RecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);

	if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN))
		RecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);

	SetLatch(MyLatch);

	errno = save_errno;
}

// В обработчике последовательно проходятся все SigProcReason
// и для проверки вызывается эта функция
static bool
CheckProcSignal(ProcSignalReason reason)
{
	volatile ProcSignalSlot *slot = MyProcSignalSlot;

	if (slot != NULL)
	{
		/* Careful here --- don't clear flag if we haven't seen it set */
		if (slot->pss_signalFlags[reason])
		{
			slot->pss_signalFlags[reason] = false;
			return true;
		}
	}

	return false;
}

Все функции и определения находятся в файле src/backend/storage/ipc/procsignal.c

Postgres также использует SIGALRM. За его обработку отвечает модуль timeout src/backend/utils/misc/timeout.c

Модуль (ре)инициализируется: все таймауты сбрасываются и заново регистрируется функция обработки сигнала.

BaseInit

Функция BaseInit (src/backend/utils/init/postinit.c) вызывается каждым процессом в начале своей работы. Ее главная задача - инициализация общих модулей:

  • Работа с файлами

Модуль fd

Модуль для работы с файлами именуется fd

Типы и функции для работы с файлами объявляются в src/include/storage/fd.h

Для работы с файлами вместо "сырых" дескрипторов используется свой тип File, который на самом деле и есть int

typedef int File;

Также объявляются функции для работы с файлами. Например, объявляются функции для создания/открытия фалов

File PathNameOpenFile(const char *fileName, int fileFlags);
File PathNameOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode);
File OpenTemporaryFile(bool interXact);

Причина добавления отдельного модуля проста - максимальное количество открытых файлов. Во время работы открываются не только файлы таблиц, но и для сортировки, например.

Реализация содержится в src/backend/storage/file/fd.c

Для хранения используется LRU список внутренней структуры vfd

typedef struct vfd
{
	int			fd;				/* current FD, or VFD_CLOSED if none */
	unsigned short fdstate;		/* bitflags for VFD's state */
	ResourceOwner resowner;		/* owner, for automatic cleanup */
	File		nextFree;		/* link to next free VFD, if in freelist */
	File		lruMoreRecently;	/* doubly linked recency-of-use list */
	File		lruLessRecently;
	off_t		fileSize;		/* current size of file (0 if not temporary) */
	char	   *fileName;		/* name of file, or NULL for unused VFD */
	/* NB: fileName is malloc'd, and must be free'd when closing the VFD */
	int			fileFlags;		/* open(2) flags for (re)opening the file */
	mode_t		fileMode;		/* mode to pass to open(2) */
} Vfd;

/*
 * Virtual File Descriptor array pointer and size.  This grows as
 * needed.  'File' values are indexes into this array.
 * Note that VfdCache[0] is not a usable VFD, just a list header.
 */
static Vfd *VfdCache;
static Size SizeVfdCache = 0;

Сам массив представляет собой двусвязный список. Причем элемент с 0 индексом - особый: это всегда начало/конец списка и его дескриптор равен VFD_CLOSED.

При инициализации модуля вызывается функция InitFileAccess

/*
 * InitFileAccess --- initialize this module during backend startup
 *
 * This is called during either normal or standalone backend start.
 * It is *not* called in the postmaster.
 */
void
InitFileAccess(void)
{
	Assert(SizeVfdCache == 0);	/* call me only once */

	/* initialize cache header entry */
	VfdCache = (Vfd *) malloc(sizeof(Vfd));
	if (VfdCache == NULL)
		ereport(FATAL,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("out of memory")));

	MemSet((char *) &(VfdCache[0]), 0, sizeof(Vfd));
	VfdCache->fd = VFD_CLOSED;

	SizeVfdCache = 1;

	/* register proc-exit hook to ensure temp files are dropped at exit */
	on_proc_exit(AtProcExit_Files, 0);
}

Функция для открытия файла и записи его в LRU

File
PathNameOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode)
{
	char	   *fnamecopy;
	File		file;
	Vfd		   *vfdP;
  
	/*
	 * We need a malloc'd copy of the file name; fail cleanly if no room.
	 */
	fnamecopy = strdup(fileName);
	if (fnamecopy == NULL)
		ereport(ERROR,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("out of memory")));

	file = AllocateVfd();
	vfdP = &VfdCache[file];

	/* Close excess kernel FDs. */
	ReleaseLruFiles();

	vfdP->fd = BasicOpenFilePerm(fileName, fileFlags, fileMode);

	if (vfdP->fd < 0)
	{
		int			save_errno = errno;

		FreeVfd(file);
		free(fnamecopy);
		errno = save_errno;
		return -1;
	}
	++nfile;

	vfdP->fileName = fnamecopy;
	/* Saved flags are adjusted to be OK for re-opening file */
	vfdP->fileFlags = fileFlags & ~(O_CREAT | O_TRUNC | O_EXCL);
	vfdP->fileMode = fileMode;
	vfdP->fileSize = 0;
	vfdP->fdstate = 0x0;
	vfdP->resowner = NULL;

	Insert(file);

	return file;
}

В случае, если мы не можем создать виртуальный файл, то присутствует возможность открытия файлов напрямую, но нужно уведомить модуль об этом

/* If you've really really gotta have a plain kernel FD, use this */
int	BasicOpenFile(const char *fileName, int fileFlags);
int	BasicOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode);

/* Use these for other cases, and also for long-lived BasicOpenFile FDs */
extern bool AcquireExternalFD(void);
extern void ReserveExternalFD(void);
extern void ReleaseExternalFD(void);

  • Синхронизация файлов 

Модуль sync

Некоторые файлы общие для нескольких процессов. Если кто-то модифицирует файл, то надо уведомить других. Этим занимается модуль sync (src/backend/storage/sync/sync.c)

Опопвещения работают следующим образом:

  1. Регистрируются изменения в файлах

bool RegisterSyncRequest(const FileTag *ftag, 
                         SyncRequestType type,
                         bool retryOnError);

В нее передаются структуры FileTag и SyncRequestType

// Тип запроса синхронизации
typedef enum SyncRequestType
{
	SYNC_REQUEST,				/* schedule a call of sync function */
	SYNC_UNLINK_REQUEST,		/* schedule a call of unlink function */
	SYNC_FORGET_REQUEST,		/* forget all calls for a tag */
	SYNC_FILTER_REQUEST			/* forget all calls satisfying match fn */
} SyncRequestType;

// Идентификация файла и места для синхронизации
typedef struct FileTag
{
	int16		handler;		/* SyncRequestHandler value, saving space */
	int16		forknum;		/* ForkNumber, saving space */
	RelFileNode rnode;
	uint32		segno;
} FileTag;

// src/include/storage/relfilenode.h
typedef struct RelFileNode
{
	Oid			spcNode;		/* tablespace */
	Oid			dbNode;			/* database */
	Oid			relNode;		/* relation */
} RelFileNode;

Изменения регистрируются для Checkpointer процесса

  1. Во время чекпоинта вызывается функция синхронизации

void ProcessSyncRequests(void);

  • Работа с файловой системой

Модуль smgr

Модуль smgr (src/backend/storage/smgr/smgr.c) отвечает за все операции с файловой системой.

Работа с файловой системой ведется посредством структуры SMgrRelationData (src/include/storage/smgr.h)

typedef struct SMgrRelationData
{
	/* rnode is the hashtable lookup key, so it must be first! */
	RelFileNodeBackend smgr_rnode;	/* relation physical identifier */

	/* pointer to owning pointer, or NULL if none */
	struct SMgrRelationData **smgr_owner;

	/*
	 * The following fields are reset to InvalidBlockNumber upon a cache flush
	 * event, and hold the last known size for each fork.  This information is
	 * currently only reliable during recovery, since there is no cache
	 * invalidation for fork extension.
	 */
	BlockNumber smgr_targblock; /* current insertion target block */
	BlockNumber smgr_cached_nblocks[MAX_FORKNUM + 1];	/* last known size */

	/* additional public fields may someday exist here */

	/*
	 * Fields below here are intended to be private to smgr.c and its
	 * submodules.  Do not touch them from elsewhere.
	 */
	int			smgr_which;		/* storage manager selector */

	/*
	 * for md.c; per-fork arrays of the number of open segments
	 * (md_num_open_segs) and the segments themselves (md_seg_fds).
	 */
	int			md_num_open_segs[MAX_FORKNUM + 1];
	struct _MdfdVec *md_seg_fds[MAX_FORKNUM + 1];

	/* if unowned, list link in list of all unowned SMgrRelations */
	dlist_node	node;
} SMgrRelationData;

typedef SMgrRelationData *SMgrRelation;

Работа осуществляется посредством функций, использующих эту структуру. Например, для чтения из файла используется функция smgrread

/*
 *	smgrread() -- read a particular block from a relation into the supplied
 *				  buffer.
 *
 *		This routine is called from the buffer manager in order to
 *		instantiate pages in the shared buffer cache.  All storage managers
 *		return pages in the format that POSTGRES expects.
 */
void
smgrread(SMgrRelation reln, 
         ForkNumber forknum, 
         BlockNumber blocknum,
		 char *buffer);

Под капотом, используется абстракция хранилища и для каждого хранилища есть свой "провайдер". Провайдер описывается структурой f_smgr, являющийся таблицей функций.

typedef struct f_smgr
{
	void		(*smgr_init) (void);	/* may be NULL */
	void		(*smgr_shutdown) (void);	/* may be NULL */
	void		(*smgr_open) (SMgrRelation reln);
	void		(*smgr_close) (SMgrRelation reln, ForkNumber forknum);
	void		(*smgr_create) (SMgrRelation reln, ForkNumber forknum,
								bool isRedo);
	bool		(*smgr_exists) (SMgrRelation reln, ForkNumber forknum);
	void		(*smgr_unlink) (RelFileNodeBackend rnode, ForkNumber forknum,
								bool isRedo);
	void		(*smgr_extend) (SMgrRelation reln, ForkNumber forknum,
								BlockNumber blocknum, char *buffer, bool skipFsync);
	bool		(*smgr_prefetch) (SMgrRelation reln, ForkNumber forknum,
								  BlockNumber blocknum);
	void		(*smgr_read) (SMgrRelation reln, ForkNumber forknum,
							  BlockNumber blocknum, char *buffer);
	void		(*smgr_write) (SMgrRelation reln, ForkNumber forknum,
							   BlockNumber blocknum, char *buffer, bool skipFsync);
	void		(*smgr_writeback) (SMgrRelation reln, ForkNumber forknum,
								   BlockNumber blocknum, BlockNumber nblocks);
	BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
	void		(*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
								  BlockNumber nblocks);
	void		(*smgr_immedsync) (SMgrRelation reln, ForkNumber forknum);
} f_smgr;

Все известные провайдеры хранятся в массиве smgrsw

static const f_smgr smgrsw[] = {
	/* magnetic disk */
    // Реализация по умолчанию
    // Использует модуль fd
    // src/backend/storage/smgr/md.c
	{
		.smgr_init = mdinit,
		.smgr_shutdown = NULL,
		.smgr_open = mdopen,
		.smgr_close = mdclose,
		.smgr_create = mdcreate,
		.smgr_exists = mdexists,
		.smgr_unlink = mdunlink,
		.smgr_extend = mdextend,
		.smgr_prefetch = mdprefetch,
		.smgr_read = mdread,
		.smgr_write = mdwrite,
		.smgr_writeback = mdwriteback,
		.smgr_nblocks = mdnblocks,
		.smgr_truncate = mdtruncate,
		.smgr_immedsync = mdimmedsync,
	}
};

Все операции строятся следующим образом:

  1. По ID провайдера находится его таблица функций.

  2. Вызывается функция для совершения необходимой операции.

Чтение из файла реализуется таким образом

void
smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
		 char *buffer)
{
	smgrsw[reln->smgr_which].smgr_read(reln, forknum, blocknum, buffer);
}

  • Общие страницы

Модуль bufmgr

Постоянное чтение с файловой системы будет довольно дорогим занятием. Для оптимизации используется буферизация - модуль bufmgr управляет страницами, считанными из файлов, и при необходимости сбрасывает их на диск.

Тип буфера определяется в src/include/storage/buf.h

/*
 * Buffer identifiers.
 *
 * Zero is invalid, positive is the index of a shared buffer (1..NBuffers),
 * negative is the index of a local buffer (-1 .. -NLocBuffer).
 */
typedef int Buffer;

#define InvalidBuffer	0

Сами операции с буфером объявляются в src/include/storage/bufmgr.h. Например, для чтения буфера объявляются функции

Buffer ReadBuffer(Relation reln, BlockNumber blockNum);
Buffer ReadBufferExtended(Relation reln, ForkNumber forkNum,
                          BlockNumber blockNum, ReadBufferMode mode,
                          BufferAccessStrategy strategy);
Buffer ReadBufferWithoutRelcache(RelFileNode rnode,
                                 ForkNumber forkNum, BlockNumber blockNum,
                                 ReadBufferMode mode, BufferAccessStrategy strategy);

Реализация располагается в src/backend/storage/buffer/bufmgr.c

Так как одна и та же страница может быть использована в нескольких процессах, то для отслеживания этого используется структура PrivateRefCountEntry

typedef struct PrivateRefCountEntry
{
	Buffer		buffer;
	int32		refcount;
} PrivateRefCountEntry;

Для хранения страниц используется массив PrivateRefCountArray

/* 64 bytes, about the size of a cache line on common systems */
#define REFCOUNT_ARRAY_ENTRIES 8

static struct PrivateRefCountEntry 
  PrivateRefCountArray[REFCOUNT_ARRAY_ENTRIES];

InitProcess

Теперь вызывается функция InitProcess. (src/backend/storage/lmgr/proc.c)

Она инициализирует специфичные для типа процесса (AutoVacuum, BgWorker, WalSender и т.д.) переменные и структуры.

Для отслеживания состояния приложения существует структура PROC_HDR

typedef struct PROC_HDR
{
    // ...
  
	/* Length of allProcs array */
	uint32		allProcCount;
	/* Head of list of free PGPROC structures */
	PGPROC	   *freeProcs;
	/* Head of list of autovacuum's free PGPROC structures */
	PGPROC	   *autovacFreeProcs;
	/* Head of list of bgworker free PGPROC structures */
	PGPROC	   *bgworkerFreeProcs;
	/* Head of list of walsender free PGPROC structures */
	PGPROC	   *walsenderFreeProcs;

    // ...
} PROC_HDR;

Указанные поля представляют списки свободных структур для каждого типа процесса. Структура PGPROCпредставляет состояние процесса.

При старте каждый процесс получает свою структуру из списка свободных и иницилизирует ее значениями по умолчанию (для ID — невалидные, для bool — false и т. д.)

Так как мы бэкэнд, то получаем свою структуру из списка freeProcs. Операция получения структуры — конкурентная, поэтому она исполняется с помощью блокировки.

SpinLockAcquire(ProcStructLock);

MyProc = *procgloballist;

if (MyProc != NULL)
{
    *procgloballist = (PGPROC *) MyProc->links.next;
    SpinLockRelease(ProcStructLock);
}
else
{
    /*
     * If we reach here, all the PGPROCs are in use.  This is one of the
     * possible places to detect "too many backends", so give the standard
     * error message.  XXX do we need to give a different failure message
     * in the autovacuum case?
     */
    SpinLockRelease(ProcStructLock);
    // ...
}

InitPostgres

После общей инициализации процесса, нужно провести инициализацию конкретную. Для этого вызывается функция InitPostgres (src/backend/utils/init/postinit.c)

Получение Id

На предыдущем шаге мы получили структуру нашего бэкэнда. Теперь необходимо присвоить ему собственный ID. Идентификаторы бэкэндов растут линейно и определяются индексом в массиве shmInvalBuffer

ProcState  *stateP = NULL;
SISeg	   *segP = shmInvalBuffer;

// ...

/* Look for a free entry in the procState array */
for (int index = 0; index < segP->lastBackend; index++)
{
    if (segP->procState[index].procPid == 0)	/* inactive slot? */
    {
        stateP = &segP->procState[index];
        break;
    }
}

// ...

MyBackendId = (stateP - &segP->procState[0]) + 1;

Сам этот массив используется, чтобы бэкэнды обменивались между собой информацией для инвалидации общего кеша

Дополнительно все бэкэнды регистрируются в списке для получения сигналов при закрытии приложения.

Регистрация таймаутов

Дальше происходит регистрация обработчиков таймаутов:

  • DEADLOCK_TIMEOUT— таймаут при получении блокировки, после которого проверяется дедлок;

  • STATEMENT_TIMEOUT— таймаут на каждое выражение;

  • LOCK_TIMEOUT— таймаут ожидания при получении блокировки (таблицы, строки или любого другого объекта БД);

  • IDLE_SESSION_TIMEOUT— таймаут неактивного сеанса;

  • IDLE_IN_TRANSACTION_SESSION_TIMEOUT— таймаут неактивного сеанса для транзакции;

  • CLIENT_CONNECTION_TIMEOUT— таймаут проверки подключения клиента.

Таймауты определяются структурой TimeoutId

/*
 * Identifiers for timeout reasons.  Note that in case multiple timeouts
 * trigger at the same time, they are serviced in the order of this enum.
 */
typedef enum TimeoutId
{
	/* Predefined timeout reasons */
	STARTUP_PACKET_TIMEOUT,
	DEADLOCK_TIMEOUT,
	LOCK_TIMEOUT,
	STATEMENT_TIMEOUT,
	STANDBY_DEADLOCK_TIMEOUT,
	STANDBY_TIMEOUT,
	STANDBY_LOCK_TIMEOUT,
	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
	IDLE_SESSION_TIMEOUT,
	CLIENT_CONNECTION_CHECK_TIMEOUT,
	/* First user-definable timeout reason */
	USER_TIMEOUT,
	/* Maximum number of timeout reasons */
	MAX_TIMEOUTS = USER_TIMEOUT + 10
} TimeoutId;

Регистрируются таймауты функцией RegisterTimeout из модуля timeout

/* callback function signature */
typedef void (*timeout_handler_proc) (void);

TimeoutId RegisterTimeout(TimeoutId id, timeout_handler_proc handler);

Инициализация кеша

Для производительности Postgres использует кеш. Всего есть 3 типа кешей:

  • Кеш отношений (RelationCache) — кеш для быстрого доступа к таблицам. Использует хеш‑таблицу.

Реализация хэш-таблицы

В Postgres существует свой тип данных для быстрого доступа к данным по ключу - хэш-таблица.

Структура хэш-таблицы определяется в src/backend/utils/hash/dynahash.c

struct HTAB
{
	HASHHDR    *hctl;			/* => shared control information */
	HASHSEGMENT *dir;			/* directory of segment starts */
	HashValueFunc hash;			/* hash function */
	HashCompareFunc match;		/* key comparison function */
	HashCopyFunc keycopy;		/* key copying function */
	HashAllocFunc alloc;		/* memory allocator */
	MemoryContext hcxt;			/* memory context if default allocator used */
	char	   *tabname;		/* table name (for error messages) */
	bool		isshared;		/* true if table is in shared memory */
	bool		isfixed;		/* if true, don't enlarge */

	/* freezing a shared table isn't allowed, so we can keep state here */
	bool		frozen;			/* true = no more inserts allowed */

	/* We keep local copies of these fixed values to reduce contention */
	Size		keysize;		/* hash key length in bytes */
	long		ssize;			/* segment size --- must be power of 2 */
	int			sshift;			/* segment shift = log2(ssize) */
};

Функции для работы с хэш-таблицами объявляются в src/include/utils/hsearch.h. Например, функция для создания хэш-таблицы:

HTAB *hash_create(const char *tabname, long nelem,
                  const HASHCTL *info, int flags);

Также существует и другая реализация - simplehash

Она определена в src/include/lib/simplehash.h

Это не просто структура данных. Это набор макросов, который генерирует шаблонную хэш-таблицу и все вспомогательные функции. Например, функция для создния таблицы

#define SH_MAKE_PREFIX(a) CppConcat(a,_)
#define SH_MAKE_NAME(name) SH_MAKE_NAME_(SH_MAKE_PREFIX(SH_PREFIX),name)
#define SH_MAKE_NAME_(a,b) CppConcat(a,b)

#define SH_CREATE SH_MAKE_NAME(create)

#define SH_SCOPE extern

typedef struct SH_TYPE
{
	/*
	 * Size of data / bucket array, 64 bits to handle UINT32_MAX sized hash
	 * tables.  Note that the maximum number of elements is lower
	 * (SH_MAX_FILLFACTOR)
	 */
	uint64		size;

	/* how many elements have valid contents */
	uint32		members;

	/* mask for bucket and size calculations, based on size */
	uint32		sizemask;

	/* boundary after which to grow hashtable */
	uint32		grow_threshold;

	/* hash buckets */
	SH_ELEMENT_TYPE *data;

#ifndef SH_RAW_ALLOCATOR
	/* memory context to use for allocations */
	MemoryContext ctx;
#endif

	/* user defined data, useful for callbacks */
	void	   *private_data;
}			SH_TYPE;

#ifdef SH_RAW_ALLOCATOR
/* <prefix>_hash <prefix>_create(uint32 nelements, void *private_data) */
SH_SCOPE	SH_TYPE *SH_CREATE(uint32 nelements, void *private_data);
#else
/*
 * <prefix>_hash <prefix>_create(MemoryContext ctx, uint32 nelements,
 *								 void *private_data)
 */
SH_SCOPE	SH_TYPE *SH_CREATE(MemoryContext ctx, uint32 nelements,
							   void *private_data);
#endif

* В большинстве случаев используется dynahash

  • Системный кеш (CatalogCache) — кеш файловой системы. В ней хранится информация о системных таблицах: конвертеры, классы операторов, метод доступа и другие

  • Кеш планов (PlanCache) — кеш планировщика

Стоит упомянуть, что инициализация кеша отношений включает 3 этапа:

  1. Аллокация памяти (контекст кеша, хеш‑таблицы)

  2. Инициализация кеша под важные общие таблицы (pg_database, pg_authid, pg_auth_members, pg_shseclabel, pg_subscription

  3. Загрузка остальных таблиц в кеш.

Фазы инициализации кеша отношений «разбросаны» по всей функции InitPostres, т.к. для каждой фазы есть свои ограничения. Например, для аутентификации пользователя нам требуются только некоторые таблицы. Поэтому, 2 фаза прогревает кеш для аутенификации, а 3 фаза выполняется после нее и загружает все остальные таблицы в кеш.

Инициализация порталов

Теперь, инициализируется менеджер порталов

  • Выделяется собственный контекст памяти - TopPortalContext;

  • Создается хэш-таблица для поиска курсора по его имени. Хэш-таблица.

Что такое портал

Портал - объект, представляющий исполняющийся запрос. Этот тип используется в расширенном протоколе, на этапе привязки, после парсинга.

Порталы могут быть именованными (имя курсора) или безымянными (одиночный SELECT). 

Структура портала определяется в src/include/utils/portal.h

typedef struct PortalData *Portal;

typedef struct PortalData
{
	/* Bookkeeping data */
	const char *name;			/* portal's name */
	const char *prepStmtName;	/* source prepared statement (NULL if none) */
	MemoryContext portalContext;	/* subsidiary memory for portal */
	ResourceOwner resowner;		/* resources owned by portal */
	void		(*cleanup) (Portal portal); /* cleanup hook */

	/*
	 * State data for remembering which subtransaction(s) the portal was
	 * created or used in.  If the portal is held over from a previous
	 * transaction, both subxids are InvalidSubTransactionId.  Otherwise,
	 * createSubid is the creating subxact and activeSubid is the last subxact
	 * in which we ran the portal.
	 */
	SubTransactionId createSubid;	/* the creating subxact */
	SubTransactionId activeSubid;	/* the last subxact with activity */

	/* The query or queries the portal will execute */
	const char *sourceText;		/* text of query (as of 8.4, never NULL) */
	CommandTag	commandTag;		/* command tag for original query */
	QueryCompletion qc;			/* command completion data for executed query */
	List	   *stmts;			/* list of PlannedStmts */
	CachedPlan *cplan;			/* CachedPlan, if stmts are from one */

	ParamListInfo portalParams; /* params to pass to query */
	QueryEnvironment *queryEnv; /* environment for query */

	/* Features/options */
	PortalStrategy strategy;	/* see above */
	int			cursorOptions;	/* DECLARE CURSOR option bits */
	bool		run_once;		/* portal will only be run once */

	/* Status data */
	PortalStatus status;		/* see above */
	bool		portalPinned;	/* a pinned portal can't be dropped */
	bool		autoHeld;		/* was automatically converted from pinned to
								 * held (see HoldPinnedPortals()) */

	/* If not NULL, Executor is active; call ExecutorEnd eventually: */
	QueryDesc  *queryDesc;		/* info needed for executor invocation */

	/* If portal returns tuples, this is their tupdesc: */
	TupleDesc	tupDesc;		/* descriptor for result tuples */
	/* and these are the format codes to use for the columns: */
	int16	   *formats;		/* a format code for each column */

	/*
	 * Outermost ActiveSnapshot for execution of the portal's queries.  For
	 * all but a few utility commands, we require such a snapshot to exist.
	 * This ensures that TOAST references in query results can be detoasted,
	 * and helps to reduce thrashing of the process's exposed xmin.
	 */
	Snapshot	portalSnapshot; /* active snapshot, or NULL if none */

	/*
	 * Where we store tuples for a held cursor or a PORTAL_ONE_RETURNING or
	 * PORTAL_UTIL_SELECT query.  (A cursor held past the end of its
	 * transaction no longer has any active executor state.)
	 */
	Tuplestorestate *holdStore; /* store for holdable cursors */
	MemoryContext holdContext;	/* memory containing holdStore */

	/*
	 * Snapshot under which tuples in the holdStore were read.  We must keep a
	 * reference to this snapshot if there is any possibility that the tuples
	 * contain TOAST references, because releasing the snapshot could allow
	 * recently-dead rows to be vacuumed away, along with any toast data
	 * belonging to them.  In the case of a held cursor, we avoid needing to
	 * keep such a snapshot by forcibly detoasting the data.
	 */
	Snapshot	holdSnapshot;	/* registered snapshot, or NULL if none */

	/*
	 * atStart, atEnd and portalPos indicate the current cursor position.
	 * portalPos is zero before the first row, N after fetching N'th row of
	 * query.  After we run off the end, portalPos = # of rows in query, and
	 * atEnd is true.  Note that atStart implies portalPos == 0, but not the
	 * reverse: we might have backed up only as far as the first row, not to
	 * the start.  Also note that various code inspects atStart and atEnd, but
	 * only the portal movement routines should touch portalPos.
	 */
	bool		atStart;
	bool		atEnd;
	uint64		portalPos;

	/* Presentation data, primarily used by the pg_cursors system view */
	TimestampTz creation_time;	/* time at which this portal was defined */
	bool		visible;		/* include this portal in pg_cursors? */

	/* Stuff added at the end to avoid ABI break in stable branches: */
	int			createLevel;	/* creating subxact's nesting level */
}			PortalData;

Порталы могут быть 2 видов: SQL (SELECT, CURSOR) и портал уровня протокола. Основная разница - возможность прокрутки:

  • SCROLL - есть возможность прокрутки запроса вперед и назад

  • NO SCROLL - запрос исполняется только "вперед"

Для SQL возможны оба варианта, а порталы уровня протокола допускают только NO SCROLL.

Дополнительно в этом же файле объявляются функции для работы с порталами. Определяются они в src/backend/utils/mmgr/portalmem.c

Инициализация для сборщика статистики

Сборщик статистики был инициализирован в Postmaster. Но взаимодействовать с ним мы пока не можем. 

Для отслеживания статуса бэкэнда есть структура PgBackendStatus

/* ----------
 * PgBackendStatus
 *
 * Each live backend maintains a PgBackendStatus struct in shared memory
 * showing its current activity.  (The structs are allocated according to
 * BackendId, but that is not critical.)  Note that the collector process
 * has no involvement in, or even access to, these structs.
 *
 * Each auxiliary process also maintains a PgBackendStatus struct in shared
 * memory.
 * ----------
 */
typedef struct PgBackendStatus
{
	/*
	 * To avoid locking overhead, we use the following protocol: a backend
	 * increments st_changecount before modifying its entry, and again after
	 * finishing a modification.  A would-be reader should note the value of
	 * st_changecount, copy the entry into private memory, then check
	 * st_changecount again.  If the value hasn't changed, and if it's even,
	 * the copy is valid; otherwise start over.  This makes updates cheap
	 * while reads are potentially expensive, but that's the tradeoff we want.
	 *
	 * The above protocol needs memory barriers to ensure that the apparent
	 * order of execution is as it desires.  Otherwise, for example, the CPU
	 * might rearrange the code so that st_changecount is incremented twice
	 * before the modification on a machine with weak memory ordering.  Hence,
	 * use the macros defined below for manipulating st_changecount, rather
	 * than touching it directly.
	 */
	int			st_changecount;

	/* The entry is valid iff st_procpid > 0, unused if st_procpid == 0 */
	int			st_procpid;

	/* Type of backends */
	BackendType st_backendType;

	/* Times when current backend, transaction, and activity started */
	TimestampTz st_proc_start_timestamp;
	TimestampTz st_xact_start_timestamp;
	TimestampTz st_activity_start_timestamp;
	TimestampTz st_state_start_timestamp;

	/* Database OID, owning user's OID, connection client address */
	Oid			st_databaseid;
	Oid			st_userid;
	SockAddr	st_clientaddr;
	char	   *st_clienthostname;	/* MUST be null-terminated */

	/* Information about SSL connection */
	bool		st_ssl;
	PgBackendSSLStatus *st_sslstatus;

	/* Information about GSSAPI connection */
	bool		st_gss;
	PgBackendGSSStatus *st_gssstatus;

	/* current state */
	BackendState st_state;

	/* application name; MUST be null-terminated */
	char	   *st_appname;

	/*
	 * Current command string; MUST be null-terminated. Note that this string
	 * possibly is truncated in the middle of a multi-byte character. As
	 * activity strings are stored more frequently than read, that allows to
	 * move the cost of correct truncation to the display side. Use
	 * pgstat_clip_activity() to truncate correctly.
	 */
	char	   *st_activity_raw;

	/*
	 * Command progress reporting.  Any command which wishes can advertise
	 * that it is running by setting st_progress_command,
	 * st_progress_command_target, and st_progress_param[].
	 * st_progress_command_target should be the OID of the relation which the
	 * command targets (we assume there's just one, as this is meant for
	 * utility commands), but the meaning of each element in the
	 * st_progress_param array is command-specific.
	 */
	ProgressCommandType st_progress_command;
	Oid			st_progress_command_target;
	int64		st_progress_param[PGSTAT_NUM_PROGRESS_PARAM];

	/* query identifier, optionally computed using post_parse_analyze_hook */
	uint64		st_query_id;
} PgBackendStatus;

На данном этапе мы получаем свою из массива всех структур, для отправки статистики. Так как Id бэкэндов возрастают линейно (от 1 до MAX_BACKENDS), то наша структура имеет индекс MyBackendId - 1 в этом массиве.

MyBEEntry = &BackendStatusArray[MyBackendId - 1];

Аутентификация

Дальше происходит аутентификация пользователя. Вызывается функция ClientAuthentication (src/backend/libpq/auth.c)

/*
 * Client authentication starts here.  If there is an error, this
 * function does not return and the backend process is terminated.
 */
void
ClientAuthentication(Port *port);

Эта функция сначала проверяет валидность сертификатов пользователя (если есть) и затем вызывает функцию аутентификации для выбранного метода аутентификации.

Представление pg_hba.conf в коде

Структуры для работы с pg_hba.conf определяются в src/include/libpq/hba.h

Файл представляется в виде множества строк, которые в свою очередь представляются структурой HbaLine

typedef struct HbaLine
{
	int			linenumber;
	char	   *rawline;
	ConnType	conntype;
	List	   *databases;
	List	   *roles;
	struct sockaddr_storage addr;
	int			addrlen;		/* zero if we don't have a valid addr */
	struct sockaddr_storage mask;
	int			masklen;		/* zero if we don't have a valid mask */
	IPCompareMethod ip_cmp_method;
	char	   *hostname;
	UserAuth	auth_method;
	char	   *usermap;
	char	   *pamservice;
	bool		pam_use_hostname;
	bool		ldaptls;
	char	   *ldapscheme;
	char	   *ldapserver;
	int			ldapport;
	char	   *ldapbinddn;
	char	   *ldapbindpasswd;
	char	   *ldapsearchattribute;
	char	   *ldapsearchfilter;
	char	   *ldapbasedn;
	int			ldapscope;
	char	   *ldapprefix;
	char	   *ldapsuffix;
	ClientCertMode clientcert;
	ClientCertName clientcertname;
	char	   *krb_realm;
	bool		include_realm;
	bool		compat_realm;
	bool		upn_username;
	List	   *radiusservers;
	char	   *radiusservers_s;
	List	   *radiussecrets;
	char	   *radiussecrets_s;
	List	   *radiusidentifiers;
	char	   *radiusidentifiers_s;
	List	   *radiusports;
	char	   *radiusports_s;
} HbaLine;

Также имеются методы доступа. Они представляются структурой UserAuth

/*
 * The following enum represents the authentication methods that
 * are supported by PostgreSQL.
 *
 * Note: keep this in sync with the UserAuthName array in hba.c.
 */
typedef enum UserAuth
{
	uaReject,
	uaImplicitReject,			/* Not a user-visible option */
	uaTrust,
	uaIdent,
	uaPassword,
	uaMD5,
	uaSCRAM,
	uaGSS,
	uaSSPI,
	uaPAM,
	uaBSD,
	uaLDAP,
	uaCert,
	uaRADIUS,
	uaPeer
#define USER_AUTH_LAST uaPeer	/* Must be last value of this enum */
} UserAuth;

Все методы аутентификации представлены в документации, кроме ImplicitReject. Он представляет собой невалидный/неизвестный метод доступа (SpecialCase). Ведет себя так же как и uaReject.

// src/backend/libpq/hba.c
/*
 *	Scan the pre-parsed hba file, looking for a match to the port's connection
 *	request.
 */
static void
check_hba(hbaPort *port)
{
  // ...
  
  /* If no matching entry was found, then implicitly reject. */
  hba = palloc0(sizeof(HbaLine));
  hba->auth_method = uaImplicitReject;
  port->hba = hba;
}

Успешная аутентификация не означает полный доступ к системе. Осталось пройти несколько фильтров:

  • Пользователю может быть запрещен вход;

  • Количество подключений достигло максимума;

  • При завершении работы БД или обновлении (pg_upgrade), доступ имеет только суперпользователь.

Аутентификация могла занять некоторое время. Особенно при использовании логина/пароля. За это время БД могла исчезнуть/переименоваться. Не лишним было бы проверить это.

Первым делом получается блокировка объекта БД. Если в момент нашей аутентификации, кто-то начал DROP DATABASE, то после получения блокировки мы об этом узнаем.

После происходит проверка прав доступа к БД. Для работой с различными правами доступа используется ACL

ACL

Для поддержки прав доступа Postgres определяет свой фреймворк ACL - Access Control List.

Каждый объект БД может иметь свой список доступа. Элемент списка представляется структурой AclItem (src/include/utils/acl.h)

typedef struct AclItem
{
	Oid			ai_grantee;		/* ID that this item grants privs to */
	Oid			ai_grantor;		/* grantor of privs */
	AclMode		ai_privs;		/* privilege bits */
} AclItem;

Тип AclMode, используемый в структуре, определяется в src/include/nodes/parsenodes.h

typedef uint32 AclMode;			/* a bitmask of privilege bits */

Права хранятся в виде битовой маски. Для хранения используется 32 битный int. Верхние 16 бит - для определения GRANT опций (выполнения SQL запросов), а нижние - для реального доступа.

Биты прав для SQL запросов определяются там же

#define ACL_INSERT		(1<<0)	/* for relations */
#define ACL_SELECT		(1<<1)
#define ACL_UPDATE		(1<<2)
#define ACL_DELETE		(1<<3)
#define ACL_TRUNCATE	(1<<4)
#define ACL_REFERENCES	(1<<5)
#define ACL_TRIGGER		(1<<6)
#define ACL_EXECUTE		(1<<7)	/* for functions */
#define ACL_USAGE		(1<<8)	/* for languages, namespaces, FDWs, and
								 * servers */
#define ACL_CREATE		(1<<9)	/* for namespaces and databases */
#define ACL_CREATE_TEMP (1<<10) /* for databases */
#define ACL_CONNECT		(1<<11) /* for databases */
#define N_ACL_RIGHTS	12		/* 1 plus the last 1<<x */
#define ACL_NO_RIGHTS	0
/* Currently, SELECT ... FOR [KEY] UPDATE/SHARE requires UPDATE privileges */
#define ACL_SELECT_FOR_UPDATE	ACL_UPDATE

Например, сейчас происходит проверка возможности подключения пользователя к БД. Проверка доступа осуществляется вызовом функции

// src/include/utils/acl.h
/* result codes for pg_*_aclcheck */
typedef enum
{
	ACLCHECK_OK = 0,
	ACLCHECK_NO_PRIV,
	ACLCHECK_NOT_OWNER
} AclResult;

// src/backend/catalog/aclchk.c
AclResult
pg_database_aclcheck(Oid db_oid, Oid roleid, AclMode mode)
{
	if (pg_database_aclmask(db_oid, roleid, mode, ACLMASK_ANY) != 0)
		return ACLCHECK_OK;
	else
		return ACLCHECK_NO_PRIV;
}

Большая часть бизнес-логики содержится в функции aclmask (src/backend/utils/adt/acl.c): определение прав доступа для переданной маски операций. 

Общая логика работы выглядит следующим образом

AclMode
aclmask(const Acl *acl, Oid roleid, Oid ownerId,
		AclMode mask, AclMaskHow how)
{
	AclMode		result;
	AclMode		remaining;
	AclItem    *aidat;
	int			i,
				num;

	// Ранний выход при отсутствии требований к правам доступа
	if (mask == 0)
		return 0;

	result = 0;

	// Владелец всегда имеет доступ
	if ((mask & ACLITEM_ALL_GOPTION_BITS) &&
		has_privs_of_role(roleid, ownerId))
	{
		result = mask & ACLITEM_ALL_GOPTION_BITS;
		if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0))
			return result;
	}

	num = ACL_NUM(acl);
	aidat = ACL_DAT(acl);

	// Проверка доступа для текущей роли
	for (i = 0; i < num; i++)
	{
		AclItem    *aidata = &aidat[i];

		if (aidata->ai_grantee == ACL_ID_PUBLIC ||
			aidata->ai_grantee == roleid)
		{
			result |= aidata->ai_privs & mask;
			if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0))
				return result;
		}
	}

	// Проверка доступа для связных роли
	remaining = mask & ~result;
	for (i = 0; i < num; i++)
	{
		AclItem    *aidata = &aidat[i];

		if (aidata->ai_grantee == ACL_ID_PUBLIC ||
			aidata->ai_grantee == roleid)
			continue;			/* already checked it */

		if ((aidata->ai_privs & remaining) &&
			has_privs_of_role(roleid, aidata->ai_grantee))
		{
			result |= aidata->ai_privs & mask;
			if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0))
				return result;
			remaining = mask & ~result;
		}
	}

	return result;
}

Обновление GUC

Дальше происходит обновление GUC конфигурации. Настройки получаются из различных источников:

  • Кеш (локаль и кодировка)

  • Аргументы точки входа

  • Startup пакет

Задержка

Раньше мы уже выполняли задержку перед аутентификацей (pre_auth_delay). Теперь, по логике, надо задержаться уже после. Для этого используется параметр post_auth_delay из postgresql.conf

/* Apply PostAuthDelay as soon as we've read all options */
if (PostAuthDelay > 0)
    pg_usleep(PostAuthDelay * 1000000L);

Инициализация своей структуры бэкэнда

Теперь, когда мы “точно” сможем коммуницировать с клиентом, инициализируем себя для видимости другим бэкэндам и отправки своего статуса.

Ранее мы получили свою структуру PgBackendStatus, но на данный момент она не полностью инициализирована. Конечная инициализация происходит сейчас.

Одновременно с нами эту стуктуру (и массив) могут обновлять и другие бэкэнды (создание новых, завершение старых), поэтому для работы с ним нужно получить блокировку.

Для минимизации времени удержания лока сначала копируется текущая структура и работа идет уже со скопированной версией, а для записи мы ненадолго получаем лок и подменяем реальную переменную нашей.

void
pgstat_bestart(void)
{
	volatile PgBackendStatus *vbeentry = MyBEEntry;
	PgBackendStatus lbeentry;
  
    // Копирование во временную переменную 
    memcpy(&lbeentry,
		   unvolatize(PgBackendStatus *, vbeentry),
		   sizeof(PgBackendStatus));

    // Обновление временной переменной
	lbeentry.st_procpid = MyProcPid;
	lbeentry.st_backendType = MyBackendType;
	lbeentry.st_proc_start_timestamp = MyStartTimestamp;
	lbeentry.st_activity_start_timestamp = 0;
	lbeentry.st_state_start_timestamp = 0;
	lbeentry.st_xact_start_timestamp = 0;
	lbeentry.st_databaseid = MyDatabaseId;

	if (lbeentry.st_backendType == B_BACKEND
		|| lbeentry.st_backendType == B_WAL_SENDER
		|| lbeentry.st_backendType == B_BG_WORKER)
		lbeentry.st_userid = GetSessionUserId();
	else
		lbeentry.st_userid = InvalidOid;

	if (MyProcPort)
		memcpy(&lbeentry.st_clientaddr, &MyProcPort->raddr,
			   sizeof(lbeentry.st_clientaddr));
	else
		MemSet(&lbeentry.st_clientaddr, 0, sizeof(lbeentry.st_clientaddr));

	lbeentry.st_state = STATE_UNDEFINED;
	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
	lbeentry.st_progress_command_target = InvalidOid;
	lbeentry.st_query_id = UINT64CONST(0);

	// Входим в критическую зону
	PGSTAT_BEGIN_WRITE_ACTIVITY(vbeentry);

	lbeentry.st_changecount = vbeentry->st_changecount;

    // Подменяем значения
	memcpy(unvolatize(PgBackendStatus *, vbeentry),
		   &lbeentry,
		   sizeof(PgBackendStatus));

    // ...
  
    // Выходим из критической зоны
	PGSTAT_END_WRITE_ACTIVITY(vbeentry);

    // ...
}

Синхронизация настроек с клиентом

Сейчас можно начать синхронизацию с клиентом:

  • Настройки кастомизации: локаль, представление даты, кодировка, тайм-зона и т.д.

  • Общие метаданные: название приложения, является ли пользователь рутом, hot standby, версия сервера и т.д.

Все настройки, допускающие отправку клиенту, отправляются.

Представление GUC в коде

Из выше написанного видно, что настройки - не простые key-value значения. Они также имеют метаданные.

Настройки могут иметь значения с типами int, double, bool, string. Для каждого типа данных - своя структура. (src/include/utils/guc_tables.h)

/* GUC records for specific variable types */

struct config_bool
{
	struct config_generic gen;
	/* constant fields, must be set correctly in initial value: */
	bool	   *variable;
	bool		boot_val;
	GucBoolCheckHook check_hook;
	GucBoolAssignHook assign_hook;
	GucShowHook show_hook;
	/* variable fields, initialized at runtime: */
	bool		reset_val;
	void	   *reset_extra;
};

struct config_int
{
	struct config_generic gen;
	/* constant fields, must be set correctly in initial value: */
	int		   *variable;
	int			boot_val;
	int			min;
	int			max;
	GucIntCheckHook check_hook;
	GucIntAssignHook assign_hook;
	GucShowHook show_hook;
	/* variable fields, initialized at runtime: */
	int			reset_val;
	void	   *reset_extra;
};

struct config_real
{
	struct config_generic gen;
	/* constant fields, must be set correctly in initial value: */
	double	   *variable;
	double		boot_val;
	double		min;
	double		max;
	GucRealCheckHook check_hook;
	GucRealAssignHook assign_hook;
	GucShowHook show_hook;
	/* variable fields, initialized at runtime: */
	double		reset_val;
	void	   *reset_extra;
};

struct config_string
{
	struct config_generic gen;
	/* constant fields, must be set correctly in initial value: */
	char	  **variable;
	const char *boot_val;
	GucStringCheckHook check_hook;
	GucStringAssignHook assign_hook;
	GucShowHook show_hook;
	/* variable fields, initialized at runtime: */
	char	   *reset_val;
	void	   *reset_extra;
};

struct config_enum
{
	struct config_generic gen;
	/* constant fields, must be set correctly in initial value: */
	int		   *variable;
	int			boot_val;
	const struct config_enum_entry *options;
	GucEnumCheckHook check_hook;
	GucEnumAssignHook assign_hook;
	GucShowHook show_hook;
	/* variable fields, initialized at runtime: */
	int			reset_val;
	void	   *reset_extra;
};

Первым полем в каждой структуре является config_generic - структура, содержащая данные, характерные для каждого типа.

struct config_generic
{
	/* constant fields, must be set correctly in initial value: */
	const char *name;			/* name of variable - MUST BE FIRST */
	GucContext	context;		/* context required to set the variable */
	enum config_group group;	/* to help organize variables by function */
	const char *short_desc;		/* short desc. of this variable's purpose */
	const char *long_desc;		/* long desc. of this variable's purpose */
	int			flags;			/* flag bits, see guc.h */
	/* variable fields, initialized at runtime: */
	enum config_type vartype;	/* type of variable (set only at startup) */
	int			status;			/* status bits, see below */
	GucSource	source;			/* source of the current actual value */
	GucSource	reset_source;	/* source of the reset_value */
	GucContext	scontext;		/* context that set the current value */
	GucContext	reset_scontext; /* context that set the reset value */
	GucStack   *stack;			/* stacked prior values */
	void	   *extra;			/* "extra" pointer for current actual value */
	char	   *last_reported;	/* if variable is GUC_REPORT, value last sent
								 * to client (NULL if not yet sent) */
	char	   *sourcefile;		/* file current setting is from (NULL if not
								 * set in config file) */
	int			sourceline;		/* line in source file */
};

За работу с GUC отвечает соответствующий модуль. Расположен в src/backend/utils/misc/guc.c

Для каждого типа значений, определен свой массив.

// src/backend/utils/misc/guc.c

static struct config_bool ConfigureNamesBool[];
static struct config_int ConfigureNamesInt[];
static struct config_real ConfigureNamesReal[];
static struct config_enum ConfigureNamesEnum[];
static struct config_string ConfigureNamesString[];

Для поиска среди всех настроек, существует массив guc_variables типа config_generic. Это сортированный массив всех переменных

// src/backend/utils/misc/guc.c

/*
 * Actual lookup of variables is done through this single, sorted array.
 */
static struct config_generic **guc_variables;

Этот список заполняется во время инициализации вызовом build_guc_variables

void
build_guc_variables(void)
{
	int			size_vars;
	int			num_vars = 0;
	struct config_generic **guc_vars;
	int			i;

	for (i = 0; ConfigureNamesBool[i].gen.name; i++)
	{
		struct config_bool *conf = &ConfigureNamesBool[i];

		/* Rather than requiring vartype to be filled in by hand, do this: */
		conf->gen.vartype = PGC_BOOL;
		num_vars++;
	}

	for (i = 0; ConfigureNamesInt[i].gen.name; i++)
	{
		struct config_int *conf = &ConfigureNamesInt[i];

		conf->gen.vartype = PGC_INT;
		num_vars++;
	}

	for (i = 0; ConfigureNamesReal[i].gen.name; i++)
	{
		struct config_real *conf = &ConfigureNamesReal[i];

		conf->gen.vartype = PGC_REAL;
		num_vars++;
	}

	for (i = 0; ConfigureNamesString[i].gen.name; i++)
	{
		struct config_string *conf = &ConfigureNamesString[i];

		conf->gen.vartype = PGC_STRING;
		num_vars++;
	}

	for (i = 0; ConfigureNamesEnum[i].gen.name; i++)
	{
		struct config_enum *conf = &ConfigureNamesEnum[i];

		conf->gen.vartype = PGC_ENUM;
		num_vars++;
	}

	/*
	 * Create table with 20% slack
	 */
	size_vars = num_vars + num_vars / 4;

	guc_vars = (struct config_generic **)
		guc_malloc(FATAL, size_vars * sizeof(struct config_generic *));

	num_vars = 0;

	for (i = 0; ConfigureNamesBool[i].gen.name; i++)
		guc_vars[num_vars++] = &ConfigureNamesBool[i].gen;

	for (i = 0; ConfigureNamesInt[i].gen.name; i++)
		guc_vars[num_vars++] = &ConfigureNamesInt[i].gen;

	for (i = 0; ConfigureNamesReal[i].gen.name; i++)
		guc_vars[num_vars++] = &ConfigureNamesReal[i].gen;

	for (i = 0; ConfigureNamesString[i].gen.name; i++)
		guc_vars[num_vars++] = &ConfigureNamesString[i].gen;

	for (i = 0; ConfigureNamesEnum[i].gen.name; i++)
		guc_vars[num_vars++] = &ConfigureNamesEnum[i].gen;

	if (guc_variables)
		free(guc_variables);
	guc_variables = guc_vars;
	num_guc_variables = num_vars;
	size_guc_variables = size_vars;
	qsort((void *) guc_variables, num_guc_variables,
		  sizeof(struct config_generic *), guc_var_compare);
}

Выше было сказано, что отправляются только допускающие отправку опции. В config_generic определяется поле flags - битовая маска метаданных опции. Определяются флаги в src/include/utils/guc.h

/*
 * bit values in "flags" of a GUC variable
 */
#define GUC_LIST_INPUT			0x0001	/* input can be list format */
#define GUC_LIST_QUOTE			0x0002	/* double-quote list elements */
#define GUC_NO_SHOW_ALL			0x0004	/* exclude from SHOW ALL */
#define GUC_NO_RESET_ALL		0x0008	/* exclude from RESET ALL */
#define GUC_REPORT				0x0010	/* auto-report changes to client */
#define GUC_NOT_IN_SAMPLE		0x0020	/* not in postgresql.conf.sample */
#define GUC_DISALLOW_IN_FILE	0x0040	/* can't set in postgresql.conf */
#define GUC_CUSTOM_PLACEHOLDER	0x0080	/* placeholder for custom variable */
#define GUC_SUPERUSER_ONLY		0x0100	/* show only to superusers */
#define GUC_IS_NAME				0x0200	/* limit string to NAMEDATALEN-1 */
#define GUC_NOT_WHILE_SEC_REST	0x0400	/* can't set if security restricted */
#define GUC_DISALLOW_IN_AUTO_FILE 0x0800	/* can't set in
											 * PG_AUTOCONF_FILENAME */

За возможность отправки клиенту отвечает флаг GUC_REPORT. Для синхронизации GUC с клиентом вызывается функция BeginReportingGUCOptions, которая его проверяет.

// src/backend/utils/misc/guc.c
/*
 * Start up automatic reporting of changes to variables marked GUC_REPORT.
 * This is executed at completion of backend startup.
 */
void
BeginReportingGUCOptions(void)
{
	int			i;

	// ...

	/* Transmit initial values of interesting variables */
	for (i = 0; i < num_guc_variables; i++)
	{
		struct config_generic *conf = guc_variables[i];

		if (conf->flags & GUC_REPORT)
			ReportGUCOption(conf);
	}

	// ...
}

Дополнительно клиенту передается секретный ключ (пара): ID процесса и токен отмены. По этой паре можно будет запрашивать отмену выполнения запроса. 

С этого момента клиент ждет ReadyForQuery пакета, который будет отослан уже в главном цикле.

Загрузка библиотек

Теперь загружаются библиотеки.

Загружаемые библиотеки указываются в переменных local_preload_libraries, shared_preload_libraries и session_preload_libraries в postgresql.conf 

Сейчас загружаются session_preload_libraries и local_preload_libraries

Динамическая загрузка и модуль dfmgr

Для загрузки библиотек Postgres использует динамическую загрузку библиотек.

Для работы с динамическими функциями/библиотеками используется модуль dfmgr

Он определяет интерфейс для взаимодействия с функциями, определяемыми во внешних библиотеках

Функция, которую необходимо вызвать идентфицируется структурой FunctionCallInfoBaseData (src/include/fmgr.h)

typedef struct FunctionCallInfoBaseData
{
	FmgrInfo   *flinfo;			/* ptr to lookup info used for this call */
	fmNodePtr	context;		/* pass info about context of call */
	fmNodePtr	resultinfo;		/* pass or return extra info about result */
	Oid			fncollation;	/* collation for function to use */
#define FIELDNO_FUNCTIONCALLINFODATA_ISNULL 4
	bool		isnull;			/* function must set true if result is NULL */
	short		nargs;			/* # arguments actually passed */
#define FIELDNO_FUNCTIONCALLINFODATA_ARGS 6
	NullableDatum args[FLEXIBLE_ARRAY_MEMBER];
} FunctionCallInfoBaseData;

typedef struct FunctionCallInfoBaseData *FunctionCallInfo;

Каждая вызываемая функция имеет следующую сигнатуру

typedef Datum (*PGFunction) (FunctionCallInfo fcinfo);

Для работы с внешними библиотеками используются функции

void *load_external_function(const char *filename, const char *funcname,
							 bool signalNotFound, void **filehandle);
void *lookup_external_function(void *filehandle, const char *funcname);
void load_file(const char *filename, bool restricted);
void **find_rendezvous_variable(const char *varName);
Size EstimateLibraryStateSpace(void);
void SerializeLibraryState(Size maxsize, char *start_address);
void RestoreLibraryState(char *start_address);

Для загрузки библиотек используется функция load_file. Она сначала выгружает (если уже была загружена), а затем загружает файл обратно. Для самой загрузки используется функция internal_load_library (src/backend/utils/fmgr/dfmgr.c)

/*
 * Load the specified dynamic-link library file, unless it already is
 * loaded.  Return the pg_dl* handle for the file.
 *
 * Note: libname is expected to be an exact name for the library file.
 */
void *internal_load_library(const char *libname);

Работа этой функции заключается в следующем:

  1. Проверить, что файл не был загружен ранее:

    1. Если был загружен, то вернуть ссылку на хранящуюся библиотеку.

  2. Аллоцировать память под структуру загруженного файла.

  3. Открыть файл библиотеки системным вызовом dlopen.

  4. Получить "магическую функцию". Она возвращает структуру Pg_magic_struct и используется для проверки совместимости по версиям.

  5. Вызывать функцию _PG_init, если присутствует.

  6. Сохранить загруженную библиотеку в памяти.

  7. Вернуть указатель на эту библиотеку.

Инициализация контекста памяти

Последнее, что осталось - аллокация памяти для входящего запроса.

Память для него хранится в 2 переменных:

  • MessageContext - контекст текущего запроса (Глобальный контекст).

  • row_description_context - контекст памяти, хранящий в себе описание строк при ответе клиенту. Например, название столбца, тип данных столбца (object ID) или модификаторы типа. (Только в файле с точкой входа src/backend/tcop/postgres.c).

Конец

На этом инициализация Postgres заканчивается и дальше идет главный цикл.

Комментарии (2)


  1. xztau
    31.01.2023 16:27

    Скажите, если я выучу SQL, я смогу найти работу?


    1. AshBlade Автор
      01.02.2023 07:34
      +1

      Смотря кем работать. Но, в любом случае, одного SQL маловато будет)