Привет, Habr! Postgres - один из крупнейших open source проектов. Он создавался многие года. Кодовая база накопилась огромная. Мне, как программисту, всегда было интересно как он работает под капотом. Но не про SQL пойдет речь, а про язык на котором он написан. Про C.  С общей архитектурой можно ознакомиться здесь Для начала поймем, что происходит до входа в главный цикл сервера.  Любое 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 системы Определение для HP или gcc компиляторов Как же поддерживается такое большое количество различных процессоров/платформ? Все просто - ассемблер. Если вы заглянете в этот файл, то увидите, что вместе с определениями типов спинлоков определяются TAS (Test And Set) макросы, которые в свою очередь будут потребляться API Postgres. И практически все макросы используют ассемблер. Вот например, полное определение типа и макроса для 32 битного Intel скомпилированного gcc Эти макросы - не часть API, а только деталь реализации, поэтому использовать напрямую их не надо. В Postgres не используются (как минимум, не на постоянной основе) функции выделения памяти стандартной библиотеки C (malloc, palloc). Вместо, этого используется самописный менеджер памяти, т.е. память управляемая.  MemoryContext - абстрактный тип для работы с памятью (palloc.h). Представляет собой логический контекст, в котором происходят аллокации памяти. Грубо говоря, это интерфейс/абстрактный класс, который определяет ряд методов (memnodes.h): Всего есть 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 - C функция, преобразующая строку src в зависимую от выставленной локали строку и копирует num байтов в dest.  В процессе работы эта функция используется много раз и от нее много зависит, как минимум как вы будете сравнивать строки для сортировки. Поэтому существование бага, связанного с этой функцией должно быть освещено. Багов 2 и оба на ОС Solaris. Ошибка связана с работой с малыми строками на определенных кодировках:  Для 2 байтной ASCII строки Solaris 10 изменяет 10 байтов и возвращает 18 Если передать параметр num равный 1, то этот параметр игнорируется и изменяют количество байтов на 1 меньше возвращаемого значения Если хотя бы один из багов был найден, то программа аварийно завершается. Для проверки используется простая канарейка  Под “безопасными” имею ввиду не меняющие состояние операции для которых запустили программу. В данном случае проверяются: –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.  Если мы рут, то выводим сообщение об ошибке и закрываемся.  После всех приведенных операций идет выбор из 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. Входная точка для основного приложения базы данных. Функция принимает 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 (обработка других сигналов убрана) Теперь нужны настройки для работы. Для начала загружается файл конфигураций - вызывается InitializeGUCOptions(): Инициализируем сортированный массив параметров Устанавливаем стандартные режимы транзакций (если вдруг кто-то изменил стандартные настройки не из интерактивного источника, например, подправил код) Читаем конфигурацию из переменных окружения После начинается парсинг аргументов командной строки: Вызывается функция getopt из <unistd.h> В большом свиче находятся аргументы и выполняется логика/обновляется конфигурация  Дальше нужно настроить рабочее окружение: файлы, папки, рабочую директорию: Проверка директории с данными (не для Windows): Проверка принадлежности директории пользователю, от имени которого запускаемся Проверка прав директории (битов доступа) - должны быть либо 0700, либо 0750  Проверка наличия pg_control файла  Смена рабочей директории в директорию с данными Создание LockFile (postmaster.pid) в директории с данными Предзагрузка библиотек Настройка SSL Регистрация обработчика на случай смерти Postmaster для корректного завершения дочерних процессов Настройка семафоров и общей память После этих операций настройка заканчивается и временные файлы удаляются. Теперь начинается настройка для подключений клиентов. Для подключений используются сокеты. Хранятся они в виде массива. Сначала мы все помечаются неинициализированными. Также регистрируем обработчик закрытия сокетов при выходе приложения. Теперь начинаем регистрировать каждый порт, который мы передали через конфигурацию (параметр listen_addresses), на соответствующие TCP сокеты.  Для каждого указанного адреса открываем новый сокет и начинаем принимать на него соединения. elemList -  список адресов, которые спарсили из listen_adresses параметра. Имеет тип List foreach - макрос Postgres для итерирования по списку Для работы со списками определено множество функций и макросов. Определены в pg_list.h Далее мы стартуем Bonjour сервис и создаем Unix сокет файл (если указаны Unix порты). Только в конце проверяем, что есть хотя бы 1 прослушиваемый сокет (так как Unix сокеты и TCP сокеты различны) Дополнительно, в LockFile добавляется хост первого успешно открытого сокета (TCP или UNIX) Теперь, когда мы знаем, что клиенты смогут к нам обращаться, можно инициализировать подсистемы (но пока не запускать): Загрузим pg_hba.conf для аутентификации клиентов. Теперь запускаем процесс Startup Есть тип перечисления AuxProcType Чтобы запустить сторонний процесс просто вызываем функцию StartChildProcess(AuxProcType) которая сделает форк процесса и передаст необходимый тип дополняющего процесса подставив аргументы командной строки: Сама фукнция StartupDataBase() - макрос: Примечание: EXEC_BACKEND - директива, сигнализирующая о том, что мы не в Unix среде и нам нужно сделать не просто fork, но и exec. Грубо говоря, любой код в #ifdef EXEC_BACKEND выполняется в Windows Выставляем текущее состояние нашего стартового процесса в STARTUP_RUNNING, а состояние машины в PM_STARTUP. Postmaster использует машину состояний для контроля исполнения процесса: старта, закрытия, при получении сигналов от дочерних процессов и восстановлении. Сразу после определения перечисления, определяется глобальная переменная pmState хранящая текущее состояние машины.  В комментарии для этого перечисления дано подробное описание работы Сейчас нужно знать, что в состоянии PM_STARTUP мы просто ждем пока стартуют все рабочие процессы, затем переходим в PM_RUN (при нормальном исполнении) Например, в нашем случаем мы перешли из PM_INIT в PM_STARTUP. Куда мы перейдем дальше? PM_STOP_BACKENDS - получили SIGTERM или SIGINT и должны завершить бэкэнды PM_WAIT_BACKENDS - ждем завершение работы бекэндов PM_RECOVERY - получили сигнал, что поры бы нам запустить восстановление PM_RUN - старт завершился успешно Также есть перечисление для процесса стартапа  P.S. Понравился комментарий перед стартом БД Момент истины. По возможности стартуем рабочие процессы. По возможности, т.к. при большом количестве запускаемых процессов они могут забрать все внимание себе и некоторые запросы (порты для которых мы уже открыли) могут остаться необработанными.  И вот - мы запускаем главный цикл сервера! Всегда хотелось узнать как работает именно Postgres, но в интернете не нашел подробного объяснения работы на уровне кода, поэтому решил исследовать самостоятельно.  Это была пилотная статья и в дальнейшем планирую выпустить еще несколько. А пока, до скорых встреч!  Бэкэнд разработчик, но для ПМ могу быть кем угодно