Несмотря на развитие лингвистических моделей, я подумал, что моя версия супервизора может быть достаточно интересна для размещения в статье. Назначение супервизора - поднять повторно программу, которая по каким-то причинам упала с ошибкой. Причём если программа завершила работу без ошибки, то она перезапущена не будет, как и не будут создаваться логи. В логах пишется время падения и тип ошибки. Универсальный Makefile может быть интересен тем, что его достаточно закинуть в папку с исходниками, добавить необходимые пути вида:
LDFLAGS = -I/usr/include/boost
LIBS = -lboost_serialization
Тема статьи не претендует на новизну, но может оказаться кому-то полезной. В первую очередь - это бэкенд, так как непрерывность работы там более важна. Хочется отметить, что в настоящее время С++ итак достаточно надёжный язык программирования. Вопрос в том, что в учебных заведениях, как правило, сначала изучается Си, а только потом С++ и зачастую стиль кода на С++ - Си с классами. Естественно, это влияет на репутацию языка как недостаточно надёжного. С наступлением эпохи лингвистических моделей код на С++ стал существенно надёжнее, так как ошибок с памятью я вот не встречал в сгенерированном коде, а логические ошибки - явление нередкое, но сам код создаёт впечатление образцового.
Базовый код получился сравнительно небольшим, я решил его не перегружать функционалом. Основной поток оставлен пустым для возможностей дописывания под свои нужны, отслеживаемая программа запускается в дополнительном потоке.
Непосредственно код супервизора
#include <iostream>
#include <thread>
#include <atomic>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <csignal>
#include <fstream>
#include <ctime>
#include <string>
#include <iomanip>
#include <sstream>
#include <condition_variable>
#include <mutex>
// Функция, возвращающая строку с текущей датой и временем
std::string getCurrentDateTime() {
// Получаем текущее время
std::time_t now = std::time(nullptr);
std::tm* timeInfo = std::localtime(&now);
// Создаём поток для форматирования времени
std::ostringstream oss;
oss << std::put_time(timeInfo, "%Y-%m-%d %H:%M:%S");
return oss.str(); // Возвращаем строку с датой и временем
}
std::ofstream createLogfile() {
// Получаем текущее время
std::time_t now = std::time(nullptr);
std::tm* timeInfo = std::localtime(&now);
// Форматируем дату и время (например: 2025-10-05_14-30-45)
std::ostringstream oss;
oss << std::put_time(timeInfo, "%Y-%m-%d_%H-%M-%S");
std::string timestamp = oss.str();
std::string filename = "logfile_" + timestamp + ".txt";
// Создаем и открываем файл
std::ofstream logFile(filename);
if (logFile.is_open()) {
std::cout << "Лог-файл создан: " << std::ctime(&now) << filename << std::endl;
} else {
std::cerr << "Не удалось создать лог-файл!" << std::endl;
}
return logFile; // Возвращаем ofstream
}
int runApp(const std::string& program, int maxRestarts, std::atomic<bool>& shouldExit, std::condition_variable &cv) {
int restartCount = 0;
int status = 0;
std::ofstream log;
bool log_is_created=false;
while (restartCount < maxRestarts) {
pid_t pid = fork();
if (pid == 0) {
// В дочернем процессе запускаем указанную программу
execl(program.c_str(), program.c_str(), nullptr);
perror("execl");
exit(EXIT_FAILURE);
} else if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
} else {
// В родительском процессе ждем завершения дочернего
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
std::cerr << "Program exited with status " << WEXITSTATUS(status) << std::endl;
shouldExit = true;
cv.notify_all();
return 0;
} else if (WIFSIGNALED(status)) {
int sig=WTERMSIG(status);
std::cerr << "Program was killed by signal " << sig << std::endl;
switch (sig) {
case SIGSEGV:
std::cout << "Segmentation fault" << std::endl;
break;
case SIGABRT:
std::cout << "Aborted" << std::endl;
break;
case SIGFPE:
std::cout << "Floating point exception" << std::endl;
break;
case SIGILL:
std::cout << "Illegal instruction" << std::endl;
break;
case SIGINT:
std::cout << "Interrupted by user (Ctrl+C)" << std::endl;
break;
case SIGTERM:
std::cout << "Termination signal received" << std::endl;
break;
default:
std::cout << "Unknown signal." << std::endl;
}
if(log_is_created==false) {log = createLogfile();log_is_created=true;}
if (log.is_open())
{
log<<getCurrentDateTime();
switch (sig) {
case SIGSEGV:
log << " Segmentation fault" << std::endl;
break;
case SIGABRT:
log << " Aborted" << std::endl;
break;
case SIGFPE:
log << " Floating point exception" << std::endl;
break;
case SIGILL:
log << " Illegal instruction" << std::endl;
break;
case SIGINT:
log << " Interrupted by user (Ctrl+C)" << std::endl;
break;
case SIGTERM:
log << " Termination signal received" << std::endl;
break;
default:
log << " Unknown signal." << std::endl;
}
}
log.close();
}
restartCount++;
std::cout << "Restart count: " << restartCount << "/" << maxRestarts << std::endl;
}
}
if (restartCount >= maxRestarts) {
std::cerr << "Max restarts reached. Exiting." << std::endl;
shouldExit = true; // Устанавливаем флаг завершения
cv.notify_all();
}
return 0;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <program> <max_restarts>" << std::endl;
return EXIT_FAILURE;
}
std::string program = argv[1];
int maxRestarts = std::stoi(argv[2]);
// Переменная для синхронизации между потоками
std::atomic<bool> shouldExit(false);
std::mutex mtx;
std::condition_variable cv;
try {
std::thread appThread(runApp, program, maxRestarts, std::ref(shouldExit), std::ref(cv));
appThread.detach();
} catch (const std::system_error& e) {
std::cerr << "Failed to create thread: " << e.what() << std::endl;
return EXIT_FAILURE;
}
// Основной поток ждет завершения работы runApp
//while (!shouldExit.load()) {
// sleep(1); // Снижаем нагрузку на процессор
//}
std::unique_lock<std::mutex> lock(mtx);
while (!shouldExit.load()) {
cv.wait(lock); // Ждем сигнала от другого потока
}
std::cout << "Main thread exiting." << std::endl;
return 0;
}
Далее перейдём к универсальному (частично универсальному) Makefile. Он не является прямым конкурентом другим системам сборки, может служить для мелких и средних проектов, для быстрого прототипирования и проверок сгенерированного LLM кода. Например в папке есть какое-то количество исходников (.cpp и .h файлов). То есть это может быть небольшой проект с тестами. Определяются файлы, которые содержат "int main". Из них получаются исполняемые файлы. Из остальных .cpp файлов получаются объектные файлы, которые хранятся в obj. Да, возможна ситуация, когда "int main" будет где-то в комментариях или в строках. Регулярное выражение служит чтобы можно было в этой же папке компилировать исходники тестов, иначе будет ошибка, что функция main уже дублируется.
Код Makefile
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++2a -O2 -s -fdata-sections -ffunction-sections -flto
LDFLAGS = -I/usr/include/boost
LIBS = -lboost_serialization
# Получение списка всех .cpp файлов в текущей директории
SRCS = $(wildcard *.cpp)
# Находим файлы, содержащие функцию main (с использованием регулярного выражения)
MAIN_SRCS = $(shell grep -l "^\s*int\s\+main\s*" $(SRCS))
MAIN_EXES = $(MAIN_SRCS:.cpp=)
# Остальные файлы для компиляции только в объектные файлы
OTHER_SRCS = $(filter-out $(MAIN_SRCS),$(SRCS))
OTHER_OBJS = $(patsubst %.cpp, obj/%.o, $(OTHER_SRCS))
MAIN_OBJS = $(patsubst %.cpp, obj/%.o, $(MAIN_SRCS))
# Основная цель
all: obj $(OTHER_OBJS) $(MAIN_OBJS) $(MAIN_EXES)
@echo "Проверка наличия Makefile в поддиректориях..."
@for dir in */ ; do \
if [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ] && [ -f "$$dir/Makefile" ]; then \
echo "Найден Makefile в директории: $$dir"; \
$(MAKE) -C "$${dir%*/}"; \
elif [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ]; then \
echo "В директории $$dir нет файла Makefile"; \
fi; \
done
# Создание папки obj, если она не существует
obj:
mkdir -p obj
# Правило для создания объектных файлов с зависимостями от .h файлов
obj/%.o: %.cpp
$(CXX) $(CXXFLAGS) -MMD -MP -c -o $@ $<
# Правило для создания исполняемых файлов из файлов с main
# Каждый .cpp файл с main компилируется в отдельный исполняемый файл
# и линкуется только с общими объектными файлами
%: obj/%.o $(OTHER_OBJS)
@echo "Linking $@"
$(CXX) -o $@ $< $(OTHER_OBJS) $(LDFLAGS) $(LIBS)
# Подключение сгенерированных зависимостей
-include $(OTHER_OBJS:.o=.d) $(MAIN_OBJS:.o=.d)
# Очистка
clean:
rm -rf obj $(MAIN_EXES)
@echo "Очистка поддиректорий..."
@for dir in */ ; do \
if [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ] && [ -f "$$dir/Makefile" ]; then \
echo "Выполняется очистка в директории: $$dir"; \
$(MAKE) -C "$${dir%*/}" clean; \
elif [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ]; then \
echo "В директории $$dir нет файла Makefile для очистки"; \
fi; \
done
.PHONY: all clean
Комментарии (15)
dan_sw
22.06.2025 22:37Несмотря на развитие лингвистических моделей, я подумал, что моя версия супервизора может быть достаточно интересна для размещения в статье.
Как у Вас связано развитие лингвистических моделей и "своя версия" "супервизора"? Непонятно.
Вопрос в том, что в учебных заведениях, как правило, сначала изучается Си, а только потом С++ и зачастую стиль кода на С++ - Си с классами. Естественно, это влияет на репутацию языка как недостаточно надёжного.
Что? Откуда вообще взялись такие выводы? В каких-то учебных заведениях изучение происходит начиная с C++, в каких-то с Си, а в каких-то и то и другое изучается. Но это ни в коем случае не влияет на надёжность Си или C++. Вообще никак это не связано.
Что Си, что C++ - достаточно надёжные языки программирования. Разработчик может написать не надёжный код и на Java, и на Kotlin, C#, Go или JavaScript. И по памяти не надёжный (да-да, в таких языках встречаются и утечки памяти, но более высокоуровневые), и по работоспособности.
С наступлением эпохи лингвистических моделей код на С++ стал существенно надёжнее
Опять вопрос - чего? Автор статью с LLM моделью писал? Какие-то очень глупые выводы. Типа "пришла эпоха LLM, код на C++ стал существенно надёжнее" - полная чушь. Код надёжен или не надёжен в зависимости от опыта программиста, который этот код пишет. Всё, точка. В LLM загружены огромные базы кода, написанные людьми, которые и используются LLM как фундамент. Т.е. этот вывод буквально сам себе противоречит - как он стал надёжнее (причём язык программирования), если код, на котором обучалась LLM был написан людьми? Ну глупость же.
но сам код создаёт впечатление образцового
Вот именно: создаёт впечатление. Атомарной переменной явно присваивать значение true
shouldExit = true;
вообще не стоит, вместо этого нужно вызывать метод store, т.к. он достаточно универсален и даёт большую гибкость:
shouldExit.store(true);
И вместо ожидания в цикле:
// Основной поток ждет завершения работы runApp while (!shouldExit.load()) { sleep(1); // Снижаем нагрузку на процессор }
Стоило использовать condition_variable, поскольку нагрузка на процессор в данном случае всё равно будет.
отслеживаемая программа
Кхм... громко сказано, но тут ничего не отслеживается. Тут тупо представлен код для перезагрузки уже не работающей программы, вот и всё. С n-ым числом попыток. Причём даже валидации аргументов нет (вдруг, argv[2] будет не числом?).
Короче статью, как и код, написала LLM :) Вот уж заменила она автора, так заменила.
dyadyaSerezha
22.06.2025 22:37Почти со всем согласен, но С/C++ все же потенциально более опасны, чем "управляемые" языки. Но даже если написал всё правильно, никуда не девается фрагментация памяти, а писать так, чтобы её не было, это ещё одно умение, которым мало кто владеет даже из сишников (легче прикрутить тот же watchdog/супервизор, или вообще нет требования, чтобы сервис работал 24х7 много дней подряд без падений).
dv0ich
22.06.2025 22:37Что Си, что C++ - достаточно надёжные языки программирования.
Можно пример используемой программы на С, у которой в анамнезе нет ошибок работы с памятью?
Код надёжен или не надёжен в зависимости от опыта программиста, который этот код пишет.
Ага, причём нередко обратно пропорционально. Там где новичок или середнячок в С++ использует более надёжные конструкции языка из std - опытный кодер наворотит ручной порнографии с сырыми указателями и дырами/сегфолтами.
dan_sw
22.06.2025 22:37Можно пример используемой программы на С, у которой в анамнезе нет ошибок работы с памятью?
Без проблем:
#include <stdio.h> int main() { printf("Hello World!\n"); return 0; }
Если есть проблемы - покажите где они конкретно тут присутствуют. Используется эта программа как базовый пример на Си. Ну, а если серьёзно - посмотрите в ядро Linux. Часто ли там проблемы с памятью возникают? Если бы они очень часто возникали - Linux дистрибутивы были бы не нужны, из-за своей не стабильности и высокопроизводительные сервера на них не работали (один Nginx чего стоит).
Там где новичок или середнячок в С++ использует более надёжные конструкции языка из std - опытный кодер наворотит ручной порнографии с сырыми указателями и дырами/сегфолтами.
Ну, с "сырыми указателями" нужно просто уметь работать, ручная работа с указателями вообще не вредна, если эта работа учитывает все особенности таких указателей. Да и свои умные указатели можно написать, для обработки "сырых указателей". Вы ведь не думаете, что "надёжные методы" при работе с указателями прям вообще не используют сырые? (ещё как используют).
dv0ich
22.06.2025 22:37Ну, а если серьёзно - посмотрите в ядро Linux. Часто ли там проблемы с памятью возникают?
Серьёзно? Чуть менее чем все CVE там связаны с некорректной работой с памятью. Часто ли их находят? Ну, достаточно часто: https://www.cvedetails.com/product/47/Linux-Linux-Kernel.html?vendor_id=33
Если бы они очень часто возникали - Linux дистрибутивы были бы не нужны, из-за своей не стабильности и высокопроизводительные сервера на них не работали (один Nginx чего стоит).
Ошибочная логика. Linux так используем в этих сегментах просто потому что совокупно ничего лучше для этих сегментов нет. Даже при всей дырявости ядра.
Ну, с "сырыми указателями" нужно просто уметь работать
Опять 25. Вы мне покажите хоть одного человека, который "просто умеет" с ними работать. Если реальные сишники, даже самые опытные, в реальных проектах постоянно плодят дыры и сегфолты из-за ошибок работы с памятью.
Вы ведь не думаете, что "надёжные методы" при работе с указателями прям вообще не используют сырые? (ещё как используют).
В безопасной бритве тоже есть лезвие, как и в опасной, но порезаться безопасной риск куда меньше, понимаете к чему я клоню?
dan_sw
22.06.2025 22:37С Вашими аргументами частично можно согласиться, а частично - поспорить. В любом случае в долгих спорах смысла нет. Не нравится Си/C++ и то, как он работает? Не используйте его, в угоду более "безопасным" языкам программирования (c Вашей точки зрения).
Мне вот C++ очень нравится, я его использую, изучаю все тонкости и особенности работы с этим языком и внутренние его механизмы. Да, с ним много сложностей и потенциальных проблем, но если быть внимательным и постоянно улучшать программный код можно добиться потрясающих результатов. Как с точки зрения производительности, так и с точки зрения безопасности.
У него уже есть альтернативы в виде Rust, Go, Zig и т.п., которые также неплохи в производительности, можете их использовать (или советовать их использовать). В сущности всё равно, что каждый программист использует для того или иного приложения или продукта. Я использую C++, кто-то C#, кто-то Rust или JavaScript. Все языки важны :)
Чтобы полностью понимать язык программирования или любую другую технологию нужно иметь довольно объёмный опыт работы с ним (ней). Я пока не могу полностью судить о C++, т.к. достаточно на нём ещё не программировал. Пока что мой рекорд 20 тыс. строк работающего кода на C++ (с большим числом всякого рода особенностей). Как напишу миллион или пару миллионов - тогда буду уже оценивать этот язык по другому (возможно, а возможно и нет).
SanyaZ7 Автор
22.06.2025 22:37Логика в том, что есть 2 варианта при возникновении подобной задачи: пробовать написать самостоятельно(скорее всего не без помощи LLM) и скопировать готовое (частично готовое) решение. Предполагаю, что готовое решение более предпочтительно. Что касается повышения качества кода лингвистическими моделями. На это есть 2 причины:
1) лингвистические модели генерируют в среднем далеко не самые плохие фрагменты кода, так как обучаются на больших кодовых базах;
2) существенно повышается конкуренция в сфере создания ПО, что естественно повышает качество кода в среднем.dan_sw
22.06.2025 22:37Предполагаю, что готовое решение более предпочтительно.
Почему? Обычно программисты на C++ сами пишут свои библиотеки, фреймворки для решения каких-то конкретно их задач. Так уж сложилось, да и удобство пакетных менеджеров до сих пор оставляет желать лучшего.
2) существенно повышается конкуренция в сфере создания ПО, что естественно повышает качество кода в среднем.
Опять же - нет, это не так. Повышение конкуренции ещё не означает, что код будет качественный. Даже в среднем. Сейчас бум багованных продуктов, которые в среднем написаны не очень хорошо, но работают (плохо, не оптимально, сжирая ресурсы).
SanyaZ7 Автор
22.06.2025 22:371) Значительную часть применения С++ составляет геймдев. Там есть понятие "время кадра" - один самых важных параметров, который нужно оптимизировать. Поэтому писали свои оптимизированные структуры данных. Или же была статья как писали свой класс строки в PVS-Studio. Или какие-то другие подобные причины. То есть причина не в том, что "так сложилось" а более конкретная, чаще связанная с производительностью, реже - с удобством использования, стабильностью и размером библиотеки.
2) Да, я согласен что проникновение скриптовых языков, особенно пайтона, приводит более медленной работе программ. FreeCad в пример.
domix32
22.06.2025 22:37До универсального makefile определённо далеко. И почему бы тогда не использовать сам же C++ для того чтобы не париться со всеми этими ссылками? Сделать что-то вроде nob.h /nabs и будет вам счастье, заодно и зависимостей проекту меньше.
SanyaZ7 Автор
22.06.2025 22:37Смысл в том, чтобы меньше заморачиваться со сборкой. Как минимум первоначально. А если понадобится, то потом перейти на что-то другое. Makefile отслеживает .cpp и .h файлы. Если время не сбивается на компьютере, то даже использование make clean особо не нужно
domix32
22.06.2025 22:37Именно поэтому и предлагаю использовать сам язык для сборки проекта на этом языке. Для небольших проектов самое оно. Всё лучше чем perlовка а ля
-o $@ $< $
Gapon65
Это не очень хороший способ ожидания завершения операции поскольку он вносит почти секундную (статистически 0.5 секунды) задержу:
Начиная с C++20, можно делать:
Подробности в: https://en.cppreference.com/w/cpp/atomic/atomic/wait
Для более старого компилятора, можно использовать стандартное решение на основе
std::condition_variable
+std::mutex
. Подробности и примеры в: https://en.cppreference.com/w/cpp/thread/condition_variable.htmlSanyaZ7 Автор
Обновил на вариант с
std::condition_variable