Pull to refresh

PostgreSQL под капотом. Часть 0. Старт программы

Reading time17 min
Views14K

Привет, Habr!

Postgres - один из крупнейших open source проектов. Он создавался многие года. Кодовая база накопилась огромная. Мне, как программисту, всегда было интересно как он работает под капотом. Но не про SQL пойдет речь, а про язык на котором он написан. Про C. 

С общей архитектурой можно ознакомиться здесь

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

int main(int argc, char** argv)

Любое C приложение начинается с main(). Этот метод располагается в src/backend/main/main.c 

Сам main можно поделить на несколько последовательных частей:

Применение хаков старта

Postgres может запускаться на разных платформах: Linux, Windows, FreeBSD и т.д. Для каждой платформы есть свои специфики работы. Чтобы их учесть создана эта часть. 

В основном здесь хаки для Windows:

  • Подготовка Winsock

  • Убрать GUI при ошибке

  • Предотвращение сбоев на некоторых операциях с плавающей точкой

Дополнительно инициализируется dummy spinlock: так как на некоторых платформах самописный барьер памяти может не работать (pg_memory_barrier()). 

Сам метод инициализации выставляет значение лока в начальное положение: проставляет значения семафоров в -1, т.е. никто не претендует на семафоры. Сам тип лока называется slock_t

Внутри спинлоков

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

В файле s_lock.h декларируются типы для спин лока для различных платформ. Вот, например, определение спинлока для linux, собранного с помощью gcc для процессора Intel для x32 системы

typedef unsigned char slock_t;

Определение для HP или gcc компиляторов

typedef struct
{
	int			sema[4];
} slock_t;

Как же поддерживается такое большое количество различных процессоров/платформ? Все просто - ассемблер. Если вы заглянете в этот файл, то увидите, что вместе с определениями типов спинлоков определяются TAS (Test And Set) макросы, которые в свою очередь будут потребляться API Postgres. И практически все макросы используют ассемблер. Вот например, полное определение типа и макроса для 32 битного Intel скомпилированного gcc

#if defined(__GNUC__) || defined(__INTEL_COMPILER)
#ifdef __i386__		/* 32-bit i386 */
#define HAS_TEST_AND_SET

typedef unsigned char slock_t;

#define TAS(lock) tas(lock)

static __inline__ int
tas(volatile slock_t *lock)
{
	register slock_t _res = 1;
	__asm__ __volatile__(
		"	cmpb	$0,%1	\n"
		"	jne		1f		\n"
		"	lock			\n"
		"	xchgb	%0,%1	\n"
		"1: \n"
:		"+q"(_res), "+m"(*lock)
:		/* no inputs */
:		"memory", "cc");
	return (int) _res;
}

#define SPIN_DELAY() spin_delay()

static __inline__ void
spin_delay(void)
{
	__asm__ __volatile__(
		" rep; nop			\n");
}

#endif	 /* __i386__ */
#endif

Эти макросы - не часть API, а только деталь реализации, поэтому использовать напрямую их не надо.

Инициализация MemoryContext

В Postgres не используются (как минимум, не на постоянной основе) функции выделения памяти стандартной библиотеки C (malloc, palloc). Вместо, этого используется самописный менеджер памяти, т.е. память управляемая. 

Краткое описание менеджмента памяти

MemoryContext - абстрактный тип для работы с памятью (palloc.h). Представляет собой логический контекст, в котором происходят аллокации памяти. Грубо говоря, это интерфейс/абстрактный класс, который определяет ряд методов (memnodes.h):

typedef struct MemoryContextMethods
{
	void	   *(*alloc) (MemoryContext context, Size size);
	void		(*free_p) (MemoryContext context, void *pointer);
	void	   *(*realloc) (MemoryContext context, void *pointer, Size size);
	void		(*reset) (MemoryContext context);
	void		(*delete_context) (MemoryContext context);
	Size		(*get_chunk_space) (MemoryContext context, void *pointer);
	bool		(*is_empty) (MemoryContext context);
	void		(*stats) (MemoryContext context,
						  MemoryStatsPrintFunc printfunc, void *passthru,
						  MemoryContextCounters *totals,
						  bool print_to_stderr);
#ifdef MEMORY_CONTEXT_CHECKING
	void		(*check) (MemoryContext context);
#endif
} MemoryContextMethods;

Всего есть 3 реализации:

  • AllocSet - стандартная реализация

  • SlabContext - выделение памяти чанками фиксированного размера

  • GenerationContext - для выделения памяти группами чанков с примерно одним временем жизни

Также определены глобальные переменные, отвечающие за различные контексты:

  • TopMemoryContext - верхнеуровневый контекст. Используется по умолчанию

  • PostmasterContext - контекст для Postmaster. После создания бэкэнда, бэкэндом же и удаляется из-за ненадобности

  • CacheMemoryContext - контекст для хранения кэша

  • MessageContext - хранит информацию о команде с фронтэнда. Например, дерево запроса/выполнения

  • TopTransactionContext - контекст транзакции. Хранит все, что связано с текущей транзакцией

  • CurTransactionContext - контекст для под-транзакций. Если у нас только 1 транзакция, то указывает на TopTransactionContext

  • PortalContext - контекст Portal.  (Portal - абстракция над исполняющимся/исполнившимся запросом клиента)

  • ErrorContext - перситентный контекст, использующийся при восстановлении после ошибок. 

Более подробнее здесь

Так вот, в текущей точке инициализируется глобальный контекст памяти (TopMemoryContext) - вызывается функция MemoryContextInit();

Локализация

Затем настраивается локализация. Выставляются значения для:

  • LC_COLLATE - сортировка строк

  • LC_CTYPE - классификация символов (например, это символ в нижнем регистре или нет) 

  • LC_MESSAGES - язык сообщений

  • LC_MONETARY - форматирование валют

  • LC_NUMERIC - форматирование чисел

  • LC_TIME - форматирование дат и времени

После переменная LC_ALL снимается. 

* Во время работы бекэндов, каждый выставляет свои значения локализации из pg_database, но postmaster этого сделать не может.

Дополнительно проверяется баг с strxfrm

Баг strxfrm

strxfrm - C функция, преобразующая строку src в зависимую от выставленной локали строку и копирует num байтов в dest. 

size_t strxfrm(char *destptr, const char *src, size_t num);

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

Багов 2 и оба на ОС Solaris. Ошибка связана с работой с малыми строками на определенных кодировках: 

  • Для 2 байтной ASCII строки Solaris 10 изменяет 10 байтов и возвращает 18

  • Если передать параметр num равный 1, то этот параметр игнорируется и изменяют количество байтов на 1 меньше возвращаемого значения

Если хотя бы один из багов был найден, то программа аварийно завершается.

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

void
check_strxfrm_bug(void)
{
	char		buf[32];
	const int	canary = 0x7F;
	bool		ok = true;

	/*
	 * Given a two-byte ASCII string and length limit 7, 8 or 9, Solaris 10
	 * 05/08 returns 18 and modifies 10 bytes.  It respects limits above or
	 * below that range.
	 *
	 * The bug is present in Solaris 8 as well; it is absent in Solaris 10
	 * 01/13 and Solaris 11.2.  Affected locales include is_IS.ISO8859-1,
	 * en_US.UTF-8, en_US.ISO8859-1, and ru_RU.KOI8-R.  Unaffected locales
	 * include de_DE.UTF-8, de_DE.ISO8859-1, zh_TW.UTF-8, and C.
	 */
	buf[7] = canary;
	(void) strxfrm(buf, "ab", 7);
	if (buf[7] != canary)
		ok = false;

	/*
	 * illumos bug #1594 was present in the source tree from 2010-10-11 to
	 * 2012-02-01.  Given an ASCII string of any length and length limit 1,
	 * affected systems ignore the length limit and modify a number of bytes
	 * one less than the return value.  The problem inputs for this bug do not
	 * overlap those for the Solaris bug, hence a distinct test.
	 *
	 * Affected systems include smartos-20110926T021612Z.  Affected locales
	 * include en_US.ISO8859-1 and en_US.UTF-8.  Unaffected locales include C.
	 */
	buf[1] = canary;
	(void) strxfrm(buf, "a", 1);
	if (buf[1] != canary)
		ok = false;

	if (!ok)
		ereport(ERROR,
				(errcode(ERRCODE_SYSTEM_ERROR),
				 errmsg_internal("strxfrm(), in locale \"%s\", writes past the specified array length",
								 setlocale(LC_COLLATE, NULL)),
				 errhint("Apply system library package updates.")));
}

Проверка на “безопасные” флаги

Под “безопасными” имею ввиду не меняющие состояние операции для которых запустили программу. В данном случае проверяются:

  • –help - показать страницу помощи

  • –version - показать версию

  • –describe-config, -C - показать конфиг

После срабатывания help и version завершают работу (вызывается exit(0)). 

Если указан последний флаг (--describe-config или -C), то выставляется флаг do_check_root в false. Зачем посмотрим дальше.

Проверка на рута

Если мы запускаемся не чтобы вывести страницу помощи (--help) или показать версию (--version), то скорее всего запускаемся в рабочем режиме, что может повлечь за собой последствия. Запуск под админом - не самая лучшая идея. Поэтому в самом начале был инициализирован флаг do_check_root со значением true.

Если мы дошли до этого момента с do_check_root равным true, то запускается функция check_root() которая проверяет (в зависимости от платформы), что мы запускаемся от администратора:

  • UID = 0 для unix, 

  • pg32win_isadmin() для Windows. 

Если мы рут, то выводим сообщение об ошибке и закрываемся.

void check_root(const char *progname)
static void
check_root(const char *progname)
{
#ifndef WIN32
	if (geteuid() == 0)
	{
		write_stderr("\"root\" execution of the PostgreSQL server is not permitted.\n"
					 "The server must be started under an unprivileged user ID to prevent\n"
					 "possible system security compromise.  See the documentation for\n"
					 "more information on how to properly start the server.\n");
		exit(1);
	}

	/*
	 * Also make sure that real and effective uids are the same. Executing as
	 * a setuid program from a root shell is a security hole, since on many
	 * platforms a nefarious subroutine could setuid back to root if real uid
	 * is root.  (Since nobody actually uses postgres as a setuid program,
	 * trying to actively fix this situation seems more trouble than it's
	 * worth; we'll just expend the effort to check for it.)
	 */
	if (getuid() != geteuid())
	{
		write_stderr("%s: real and effective user IDs must match\n",
					 progname);
		exit(1);
	}
#else							/* WIN32 */
	if (pgwin32_is_admin())
	{
		write_stderr("Execution of PostgreSQL by a user with administrative permissions is not\n"
					 "permitted.\n"
					 "The server must be started under an unprivileged user ID to prevent\n"
					 "possible system security compromises.  See the documentation for\n"
					 "more information on how to properly start the server.\n");
		exit(1);
	}
#endif							/* WIN32 */
}

Выбор точки входа

После всех приведенных операций идет выбор из 4 входных точек приложения:

  • AuxiliaryMain (--boot) - Старт дополняющих “инфраструктурных” процессов

    • Autovaccum

    • PGARCH - архивация

    • PGSTAT - сбор статистики

    • logger - логирование

    • Walwriter - писатель WAL лога

  • GetGucInfo (--describe-config) - Получение информации о GUC (Global Unified Configuration)

  • PostgresMain (--single) - запуск инстанса базы данных для единственного пользователя

  • SubPostmasterMain (--fork) - для Windows сымитировать fork/exec Unix процесс

  • PostmasterMain (по умолчанию, без ключа) - входная точка для приложения (основная)

Проверка идет по первому аргументу.

Дальше будем разбирать работу при условии запуска Postgres целиком - выбираем PostmasterMain.

PostmasterMain

Входная точка для основного приложения базы данных. Функция принимает argc и argv, переданные самой функции main.

Настройка окружения

Первое, что мы делаем, настраиваем окружение:

  • Выставляем pid процесса

  • Проставляем права для создаваемых файлов (вызов umask)

  • Инициализируем контекст памяти специально для Postmaster (PostmasterContext)

Сигналы

Теперь, регистрируем обработчики сигналов. Postgres использует сигналы для общения со своими под процессами и, соответственно, определяет значения для сигналов, причем для каждого типа процесса (WAL Writer, Stats Collector, Backend, Postmaster и т.д.) каждый сигнал имеет различное значение.

Для Postmaster:

  • SIGINT - (fast shutdown) - послать SIGTERM дочерним процессам и выключаемся

  • SIGTERM - (smart shutdown) - ждем дочерние процессы и выключаемся

  • SIGQUIT - (immediate shutdown) - завершаем все дочерние процессы SIGQUIT (или шлем SIGKILL, если кто-то не хочет завершаться) и выходим без тщательного завершения

  • SIGABRT - выключение без возможности что-либо сделать (даже не логируется)

  • SIGHUP - обновление конфигурационного файла

  • SIGCHLD - обработка выключения дочернего процесса

  • SIGUSR1 - получено сообщение от дочернего процесса

  • SIGUSR2 - игнорируется (зарезервирован)

В основном сигналы используются для управлением машины состояний Postmaster (об этом дальше).

Например, часть кода обработчика SIGTERM (обработка других сигналов убрана)

/*
 * pmdie -- signal handler for processing various postmaster signals.
 */
static void
pmdie(SIGNAL_ARGS)
{
	int			save_errno = errno;

	/*
	 * We rely on the signal mechanism to have blocked all signals ... except
	 * on Windows, which lacks sigaction(), so we have to do it manually.
	 */
#ifdef WIN32
	PG_SETMASK(&BlockSig);
#endif

	ereport(DEBUG2,
			(errmsg_internal("postmaster received signal %d",
							 postgres_signal_arg)));

	switch (postgres_signal_arg)
	{
		case SIGTERM:

			/*
			 * Smart Shutdown:
			 *
			 * Wait for children to end their work, then shut down.
			 */
			if (Shutdown >= SmartShutdown)
				break;
			Shutdown = SmartShutdown;
			ereport(LOG,
					(errmsg("received smart shutdown request")));

			/* Report status */
			AddToDataDirLockFile(LOCK_FILE_LINE_PM_STATUS, PM_STATUS_STOPPING);
#ifdef USE_SYSTEMD
			sd_notify(0, "STOPPING=1");
#endif

			/*
			 * If we reached normal running, we have to wait for any online
			 * backup mode to end; otherwise go straight to waiting for client
			 * backends to exit.  (The difference is that in the former state,
			 * we'll still let in new superuser clients, so that somebody can
			 * end the online backup mode.)  If already in PM_STOP_BACKENDS or
			 * a later state, do not change it.
			 */
			if (pmState == PM_RUN)
				connsAllowed = ALLOW_SUPERUSER_CONNS;
			else if (pmState == PM_HOT_STANDBY)
				connsAllowed = ALLOW_NO_CONNS;
			else if (pmState == PM_STARTUP || pmState == PM_RECOVERY)
			{
				/* There should be no clients, so proceed to stop children */
				pmState = PM_STOP_BACKENDS;
			}

			/*
			 * Now wait for online backup mode to end and backends to exit. If
			 * that is already the case, PostmasterStateMachine will take the
			 * next step.
			 */
			PostmasterStateMachine();
			break;

		    /*  
             *  Обработка других сигналов
             */
	}

#ifdef WIN32
	PG_SETMASK(&UnBlockSig);
#endif

	errno = save_errno;
}

Конфигурация

Теперь нужны настройки для работы.

Для начала загружается файл конфигураций - вызывается InitializeGUCOptions():

  • Инициализируем сортированный массив параметров

  • Устанавливаем стандартные режимы транзакций (если вдруг кто-то изменил стандартные настройки не из интерактивного источника, например, подправил код)

  • Читаем конфигурацию из переменных окружения

После начинается парсинг аргументов командной строки:

  • Вызывается функция getopt из <unistd.h>

  • В большом свиче находятся аргументы и выполняется логика/обновляется конфигурация

Этот большой switch
while ((opt = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:S:sTt:W:-:")) != -1)
	{
		switch (opt)
		{
			case 'B':
				SetConfigOption("shared_buffers", optarg, PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'b':
				/* Undocumented flag used for binary upgrades */
				IsBinaryUpgrade = true;
				break;

			case 'C':
				output_config_variable = strdup(optarg);
				break;

			case 'D':
				userDoption = strdup(optarg);
				break;

			case 'd':
				set_debug_options(atoi(optarg), PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'E':
				SetConfigOption("log_statement", "all", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'e':
				SetConfigOption("datestyle", "euro", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'F':
				SetConfigOption("fsync", "false", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'f':
				if (!set_plan_disabling_options(optarg, PGC_POSTMASTER, PGC_S_ARGV))
				{
					write_stderr("%s: invalid argument for option -f: \"%s\"\n",
								 progname, optarg);
					ExitPostmaster(1);
				}
				break;

			case 'h':
				SetConfigOption("listen_addresses", optarg, PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'i':
				SetConfigOption("listen_addresses", "*", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'j':
				/* only used by interactive backend */
				break;

			case 'k':
				SetConfigOption("unix_socket_directories", optarg, PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'l':
				SetConfigOption("ssl", "true", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'N':
				SetConfigOption("max_connections", optarg, PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'n':
				/* Don't reinit shared mem after abnormal exit */
				Reinit = false;
				break;

			case 'O':
				SetConfigOption("allow_system_table_mods", "true", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'P':
				SetConfigOption("ignore_system_indexes", "true", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'p':
				SetConfigOption("port", optarg, PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'r':
				/* only used by single-user backend */
				break;

			case 'S':
				SetConfigOption("work_mem", optarg, PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 's':
				SetConfigOption("log_statement_stats", "true", PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'T':

				/*
				 * In the event that some backend dumps core, send SIGSTOP,
				 * rather than SIGQUIT, to all its peers.  This lets the wily
				 * post_hacker collect core dumps from everyone.
				 */
				SendStop = true;
				break;

			case 't':
				{
					const char *tmp = get_stats_option_name(optarg);

					if (tmp)
					{
						SetConfigOption(tmp, "true", PGC_POSTMASTER, PGC_S_ARGV);
					}
					else
					{
						write_stderr("%s: invalid argument for option -t: \"%s\"\n",
									 progname, optarg);
						ExitPostmaster(1);
					}
					break;
				}

			case 'W':
				SetConfigOption("post_auth_delay", optarg, PGC_POSTMASTER, PGC_S_ARGV);
				break;

			case 'c':
			case '-':
				{
					char	   *name,
							   *value;

					ParseLongOption(optarg, &name, &value);
					if (!value)
					{
						if (opt == '-')
							ereport(ERROR,
									(errcode(ERRCODE_SYNTAX_ERROR),
									 errmsg("--%s requires a value",
											optarg)));
						else
							ereport(ERROR,
									(errcode(ERRCODE_SYNTAX_ERROR),
									 errmsg("-c %s requires a value",
											optarg)));
					}

					SetConfigOption(name, value, PGC_POSTMASTER, PGC_S_ARGV);
					free(name);
					if (value)
						free(value);
					break;
				}

			default:
				write_stderr("Try \"%s --help\" for more information.\n",
							 progname);
				ExitPostmaster(1);
		}
	}

Рабочее окружение

Дальше нужно настроить рабочее окружение: файлы, папки, рабочую директорию:

  1. Проверка директории с данными (не для Windows):

    1. Проверка принадлежности директории пользователю, от имени которого запускаемся

    2. Проверка прав директории (битов доступа) - должны быть либо 0700, либо 0750 

  2. Проверка наличия pg_control файла 

  3. Смена рабочей директории в директорию с данными

  4. Создание LockFile (postmaster.pid) в директории с данными

  5. Предзагрузка библиотек

  6. Настройка SSL

  7. Регистрация обработчика на случай смерти Postmaster для корректного завершения дочерних процессов

  8. Настройка семафоров и общей память

После этих операций настройка заканчивается и временные файлы удаляются.

Настройка подключений

Теперь начинается настройка для подключений клиентов.

Для подключений используются сокеты. Хранятся они в виде массива. Сначала мы все помечаются неинициализированными.

Также регистрируем обработчик закрытия сокетов при выходе приложения.

Теперь начинаем регистрировать каждый порт, который мы передали через конфигурацию (параметр listen_addresses), на соответствующие TCP сокеты. 

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

Как открываем порты и про тип List
foreach(l, elemlist)
{
    char	   *curhost = (char *) lfirst(l);

    if (strcmp(curhost, "*") == 0)
        status = StreamServerPort(AF_UNSPEC, NULL,
                                  (unsigned short) PostPortNumber,
                                  NULL,
                                  ListenSocket, MAXLISTEN);
    else
        status = StreamServerPort(AF_UNSPEC, curhost,
                                  (unsigned short) PostPortNumber,
                                  NULL,
                                  ListenSocket, MAXLISTEN);

    if (status == STATUS_OK)
    {
        success++;
        /* record the first successful host addr in lockfile */
        if (!listen_addr_saved)
        {
            AddToDataDirLockFile(LOCK_FILE_LINE_LISTEN_ADDR, curhost);
            listen_addr_saved = true;
        }
    }
    else
        ereport(WARNING,
                (errmsg("could not create listen socket for \"%s\"",
                        curhost)));
    }

    if (!success && elemlist != NIL)
        ereport(FATAL,
                (errmsg("could not create any TCP/IP sockets")));

    list_free(elemlist);
    pfree(rawstring);
}

elemList - список адресов, которые спарсили из listen_adresses параметра. Имеет тип List

typedef union ListCell
{
	void	   *ptr_value;
	int			int_value;
	Oid			oid_value;
} ListCell;

typedef struct List
{
	NodeTag		type;			/* T_List, T_IntList, or T_OidList */
	int			length;			/* number of elements currently present */
	int			max_length;		/* allocated length of elements[] */
	ListCell   *elements;		/* re-allocatable array of cells */
	/* We may allocate some cells along with the List header: */
	ListCell	initial_elements[FLEXIBLE_ARRAY_MEMBER];
	/* If elements == initial_elements, it's not a separate allocation */
} List;

foreach - макрос Postgres для итерирования по списку

#define foreach(cell, lst)	\
	for (ForEachState cell##__state = {(lst), 0}; \
		 (cell##__state.l != NIL && \
		  cell##__state.i < cell##__state.l->length) ? \
		 (cell = &cell##__state.l->elements[cell##__state.i], true) : \
		 (cell = NULL, false); \
		 cell##__state.i++)

Для работы со списками определено множество функций и макросов. Определены в pg_list.h

Далее мы стартуем Bonjour сервис и создаем Unix сокет файл (если указаны Unix порты).

Только в конце проверяем, что есть хотя бы 1 прослушиваемый сокет (так как Unix сокеты и TCP сокеты различны)

Дополнительно, в LockFile добавляется хост первого успешно открытого сокета (TCP или UNIX)

Подготовка к старту

Теперь, когда мы знаем, что клиенты смогут к нам обращаться, можно инициализировать подсистемы (но пока не запускать):

Загрузим pg_hba.conf для аутентификации клиентов.

Теперь запускаем процесс Startup

StartupPID = StartupDataBase();
Процесс запуска дочернего процесса

Есть тип перечисления AuxProcType

typedef enum
{
	NotAnAuxProcess = -1,
	CheckerProcess = 0,
	BootstrapProcess,
	StartupProcess,
	BgWriterProcess,
	ArchiverProcess,
	CheckpointerProcess,
	WalWriterProcess,
	WalReceiverProcess,

	NUM_AUXPROCTYPES			/* Must be last! */
} AuxProcType;

Чтобы запустить сторонний процесс просто вызываем функцию StartChildProcess(AuxProcType) которая сделает форк процесса и передаст необходимый тип дополняющего процесса подставив аргументы командной строки:

static pid_t
StartChildProcess(AuxProcType type)
{
	pid_t		pid;
	char	   *av[10];
	int			ac = 0;
	char		typebuf[32];

	/*
	 * Set up command-line arguments for subprocess
	 */
	av[ac++] = "postgres";

#ifdef EXEC_BACKEND
	av[ac++] = "--forkboot";
	av[ac++] = NULL;			/* filled in by postmaster_forkexec */
#endif

	snprintf(typebuf, sizeof(typebuf), "-x%d", type);
	av[ac++] = typebuf;

	av[ac] = NULL;
	Assert(ac < lengthof(av));

#ifdef EXEC_BACKEND
	pid = postmaster_forkexec(ac, av);
#else							/* !EXEC_BACKEND */
	pid = fork_process();

	if (pid == 0)				/* child */
	{
		InitPostmasterChild();

		/* Close the postmaster's sockets */
		ClosePostmasterPorts(false);

		/* Release postmaster's working memory context */
		MemoryContextSwitchTo(TopMemoryContext);
		MemoryContextDelete(PostmasterContext);
		PostmasterContext = NULL;

		AuxiliaryProcessMain(ac, av);	/* does not return */
	}
#endif							/* EXEC_BACKEND */

	if (pid < 0)
	{
		/* in parent, fork failed */
        
        // Код опущен
		
        if (type == StartupProcess)
			ExitPostmaster(1);
		return 0;
	}

	/*
	 * in parent, successful fork
	 */
	return pid;
}

Сама фукнция StartupDataBase() - макрос:

#define StartupDataBase() StartChildProcess(StartupProcess)

Примечание: EXEC_BACKEND - директива, сигнализирующая о том, что мы не в Unix среде и нам нужно сделать не просто fork, но и exec. Грубо говоря, любой код в #ifdef EXEC_BACKEND выполняется в Windows

Выставляем текущее состояние нашего стартового процесса в STARTUP_RUNNING, а состояние машины в PM_STARTUP.

О машине состояний Postmaster

Postmaster использует машину состояний для контроля исполнения процесса: старта, закрытия, при получении сигналов от дочерних процессов и восстановлении.

typedef enum
{
	PM_INIT,					/* postmaster starting */
	PM_STARTUP,					/* waiting for startup subprocess */
	PM_RECOVERY,				/* in archive recovery mode */
	PM_HOT_STANDBY,				/* in hot standby mode */
	PM_RUN,						/* normal "database is alive" state */
	PM_STOP_BACKENDS,			/* need to stop remaining backends */
	PM_WAIT_BACKENDS,			/* waiting for live backends to exit */
	PM_SHUTDOWN,				/* waiting for checkpointer to do shutdown
								 * ckpt */
	PM_SHUTDOWN_2,				/* waiting for archiver and walsenders to
								 * finish */
	PM_WAIT_DEAD_END,			/* waiting for dead_end children to exit */
	PM_NO_CHILDREN				/* all important children have exited */
} PMState;

static PMState pmState = PM_INIT;

Сразу после определения перечисления, определяется глобальная переменная pmState хранящая текущее состояние машины.

В комментарии для этого перечисления дано подробное описание работы

/*
 * We use a simple state machine to control startup, shutdown, and
 * crash recovery (which is rather like shutdown followed by startup).
 *
 * After doing all the postmaster initialization work, we enter PM_STARTUP
 * state and the startup process is launched. The startup process begins by
 * reading the control file and other preliminary initialization steps.
 * In a normal startup, or after crash recovery, the startup process exits
 * with exit code 0 and we switch to PM_RUN state.  However, archive recovery
 * is handled specially since it takes much longer and we would like to support
 * hot standby during archive recovery.
 *
 * When the startup process is ready to start archive recovery, it signals the
 * postmaster, and we switch to PM_RECOVERY state. The background writer and
 * checkpointer are launched, while the startup process continues applying WAL.
 * If Hot Standby is enabled, then, after reaching a consistent point in WAL
 * redo, startup process signals us again, and we switch to PM_HOT_STANDBY
 * state and begin accepting connections to perform read-only queries.  When
 * archive recovery is finished, the startup process exits with exit code 0
 * and we switch to PM_RUN state.
 *
 * Normal child backends can only be launched when we are in PM_RUN or
 * PM_HOT_STANDBY state.  (connsAllowed can also restrict launching.)
 * In other states we handle connection requests by launching "dead_end"
 * child processes, which will simply send the client an error message and
 * quit.  (We track these in the BackendList so that we can know when they
 * are all gone; this is important because they're still connected to shared
 * memory, and would interfere with an attempt to destroy the shmem segment,
 * possibly leading to SHMALL failure when we try to make a new one.)
 * In PM_WAIT_DEAD_END state we are waiting for all the dead_end children
 * to drain out of the system, and therefore stop accepting connection
 * requests at all until the last existing child has quit (which hopefully
 * will not be very long).
 *
 * Notice that this state variable does not distinguish *why* we entered
 * states later than PM_RUN --- Shutdown and FatalError must be consulted
 * to find that out.  FatalError is never true in PM_RECOVERY, PM_HOT_STANDBY,
 * or PM_RUN states, nor in PM_SHUTDOWN states (because we don't enter those
 * states when trying to recover from a crash).  It can be true in PM_STARTUP
 * state, because we don't clear it until we've successfully started WAL redo.
 */

Сейчас нужно знать, что в состоянии PM_STARTUP мы просто ждем пока стартуют все рабочие процессы, затем переходим в PM_RUN (при нормальном исполнении)

Например, в нашем случаем мы перешли из PM_INIT в PM_STARTUP. Куда мы перейдем дальше?

  • PM_STOP_BACKENDS - получили SIGTERM или SIGINT и должны завершить бэкэнды

  • PM_WAIT_BACKENDS - ждем завершение работы бекэндов

  • PM_RECOVERY - получили сигнал, что поры бы нам запустить восстановление

  • PM_RUN - старт завершился успешно

Также есть перечисление для процесса стартапа

/* Startup process's status */
typedef enum
{
	STARTUP_NOT_RUNNING,
	STARTUP_RUNNING,
	STARTUP_SIGNALED,			/* we sent it a SIGQUIT or SIGKILL */
	STARTUP_CRASHED
} StartupStatusEnum;

static StartupStatusEnum StartupStatus = STARTUP_NOT_RUNNING;

P.S. Понравился комментарий перед стартом БД

/*
* We're ready to rock and roll...
*/
StartupPID = StartupDataBase();

Старт

Момент истины.

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

И вот - мы запускаем главный цикл сервера!

status = ServerLoop();

Послесловие

Всегда хотелось узнать как работает именно Postgres, но в интернете не нашел подробного объяснения работы на уровне кода, поэтому решил исследовать самостоятельно.

Это была пилотная статья и в дальнейшем планирую выпустить еще несколько. А пока, до скорых встреч!

Only registered users can participate in poll. Log in, please.
Делать продолжение?
96.15% Да100
3.85% Нет4
104 users voted. 10 users abstained.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 24: ↑24 and ↓0+24
Comments2

Articles