Ориентация главы

Модуль process инженеризует «запуск внешней программы»: семейство Shell (xrtProcessShell — вся командная строка идёт в системный shell, лёгкий путь вызова утилит) и семейство Spawn (xrtProcessSpawn — прямой запуск из массива argv, без поверхности инъекций, безопасный путь для недоверенного входа); захват вывода (Result.Stdout — владеющий буфер, перехватывающий стандартный вывод потомка); конвейеризация (xrtProcessPipeline — stdout каждой стадии автоматически подключается к stdin следующей, эквивалент shell-ового a | b); трёхзначный статус выхода (нормальный выход с кодом / убит сигналом / отказ запуска — различает xrtProcessResultSuccess). Наследование окружения, рабочий каталог и тайм-аутное убийство (лестница SIGTERM→SIGKILL) объявляются в конфигурации.

52.1引入

Три сцены. Сцена первая: службе обработки изображений нужен внешний ffmpeg — простейший system(): склейка командной строки (пробел в имени входного файла — катастрофа), вывод не захватить, тайм-аут не поставить (ffmpeg завис — и служба зависла с ним). Сцена вторая: инструменту сборки нужно запустить gcc main.c -o app и получить вывод ошибок — нужны захват stderr и код выхода, а system даёт один int. Сцена третья: текстовый конвейер cat | grep | sort — многостадийная цепочка процессов, где ручная склейка труб — управление дескрипторами и двусторонние deadlock-и (верхний поток заблокировал запись, нижний не читает) — одни капканы.

Три сцены соответствуют трём наборам возможностей: семейство Spawn (xrtProcessSpawn(配置) (конфигурация) возвращает дескриптор) плюс настройка тайм-аута (безопасная версия сцены один), двухпоточный захват плюс трёхзначный вердикт (сцена два), автосклейка труб Pipeline-ом (сцена три). Разделительная линия Shell и Spawn — доверие: командная строка из вашего кода (доверенная) — Shell для экономии сил; из пользовательского ввода (недоверенного) — обязательно Spawn+Argv — командная инъекция одного семейства с SQL-инъекцией, и линия обороны одна: «не вклеивать вход в интерпретатор».

52.2概念

Два семейства: Shell и Spawn

ИзмерениеСемейство ShellСемейство Spawn
ВходxrtProcessShell(命令串, 结果) (командная строка, результат)xrtProcessSpawn(配置) (конфигурация)
Интерпретацияcmd/sh трактует метасимволыпрямой запуск ядром, ноль интерпретации
Применимостьвызов утилит, доверенные командынедоверенный вход, точное управление
Поверхность инъекциисклейка командной строки и есть рискнет - аргументы передаются как есть

Конфигурация (xprocessconfig) объявляет среду исполнения: рабочий каталог, переменные окружения (инкрементальное перекрытие окружения процесса из главы 43), обработка стандартных потоков (отбросить/унаследовать/захватить/труба), тайм-аут и лестница убийства (по тайм-ауту сперва SIGTERM, после льготного периода SIGKILL — передающий конец разделения приёма/передачи главы 49).

Трёхзначный статус выхода

1Отказ запускаисполнимый файл не найден/нет прав - ошибка родителя (слот ошибки главы 4)
2Нормальный выходпотомок exit(code) - код выхода идёт в структуру
3Прекращение сигналомубит сигналом (тайм-аутное убийство/крах) - номер сигнала идёт в структуру
4ВердиктxrtProcessResultSuccess = нормальный выход И код 0

Смысл различения трёх состояний: ненулевой код выхода — семантический сигнал потомка (gcc сообщает об ошибке компиляции); убийство сигналом — аварийное прекращение (тайм-аут, крах) — обработка у них совершенно разная (первое — читать вывод, второе — диагностировать). Success — удобный вердикт «код 0 и нормальный выход»; полная тройка состояний — в полях структуры.

Конвейеризация

xrtProcessPipeline(阶段数组, ...) (массив стадий) порождает и сцепляет N стадий за один вызов — stdout→stdin между стадиями это настоящая труба ОС (данные не проходят через родителя — пропускная способность и изоляция вдвойне оптимальны); вывод последней стадии можно захватить по желанию. Контраст с ручной склейкой Spawn-ом: настройка наследования дескрипторов, момент закрытия обоих концов, двусторонний буферный deadlock — три классических капкана полностью забирает реализация (дословно из комментария образца pipeline).

Жизненный цикл захваченных потоков

Result.Stdout/Stderr — владеющие буферы — освобождаются xrtProcessResultUnit-ом (буферы включены); забыть Unit — утечка на каждый процесс. Правило комбинирования захвата с трубой: последняя стадия может захватывать (вывод промежуточных ушёл в трубу — родитель его не видит, и это именно смысл конвейера).

52.3示例

Полная программа: исполнение через Shell и захват вывода

Из образца репозитория examples/process/capture/main.c:

examples/process/capture/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>






















 */


/* 执行命令、输出捕获内容,并把子进程退出状态映射为示例退出码。 */
int main(void)
{
	xprocessresult Result;
	bool bOk;

		#if defined(_WIN32) || defined(_WIN64)
		bOk = xrtProcessShell("echo captured output", &Result);
		#else
		bOk = xrtProcessShell("printf 'captured output\n'", &Result);
		#endif
	if ( !bOk ) {
		const xerror* pError = xrtGetError();

		fprintf(
			stderr,
			"process failed: %s\n",
			pError != NULL ? xrtErrorMessage(pError) : "unknown error"
		);
		return 1;
	}
	fwrite(Result.Stdout, 1u, Result.StdoutSize, stdout);
	bOk = xrtProcessResultSuccess(&Result);
	xrtProcessResultUnit(&Result);
	return bOk ? 0 : 2;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/process/capture/main.c -lws2_32 -liphlpapicaptured output

Что только что произошло. (1) xrtProcessShell("echo captured output", &Result) — вся команда идёт в системный shell; платформенные различия (echo у Windows против printf у POSIX) обработаны условной компиляцией образца, поведение в рантайме одинаково. (2) Result.Stdout несёт захваченный владеющий буфер — печатается напрямую для проверки; xrtProcessResultSuccess выносит вердикт «нормальный выход и код 0». (3) xrtProcessResultUnit освобождает результат — буфер захвата следует за результатом, и Unit — его единственный выход (забыли Unit — утечка по блоку за вызов). (4) Эта форма — кратчайший путь «внешняя программа как функция»: на входе командная строка, на выходе пара (буфер + статус).

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

Из examples/process/pipeline/main.c:

examples/process/pipeline/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>






















 */


/* 用两个 Shell 阶段演示真实管道连接与末段捕获。 */
int main(void)
{
	xprocesspipelineresult Result;
	xprocessconfig Stages[2];
	bool bOk;

		#if defined(_WIN32) || defined(_WIN64)
		if ( !xrtProcessShellConfigInit(&Stages[0], "echo pipeline output") ||
			!xrtProcessShellConfigInit(&Stages[1], "findstr pipeline") ) {
			return 1;
		}
		#else
		if ( !xrtProcessShellConfigInit(&Stages[0], "printf 'pipeline output\n'") ||
			!xrtProcessShellConfigInit(&Stages[1], "tr a-z A-Z") ) {
			return 1;
		}
		#endif
	bOk = xrtProcessPipeline(Stages, 2u, NULL, &Result);
	if ( !bOk ) {
		const xerror* pError = xrtGetError();

		fprintf(
			stderr,
			"pipeline failed: %s\n",
			pError != NULL ? xrtErrorMessage(pError) : "unknown error"
		);
		return 2;
	}
	fwrite(Result.Stdout, 1u, Result.StdoutSize, stdout);
	bOk = xrtProcessPipelineSuccess(&Result);
	xrtProcessPipelineResultUnit(&Result);
	return bOk ? 0 : 3;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/process/pipeline/main.c -lws2_32 -liphlpapipipeline output

Что только что произошло. (1) Массив конфигураций двух стадий (по xprocessconfig на стадию) одной рукой передаётся xrtProcessPipeline — порождение, склейка труб, запуск и сбор — всё его работа. (2) Труба между стадиями уровня ОС (ветка Windows берёт findstr, ветка POSIX — tr; каждая платформа выбирает эквивалентную утилиту); поток данных не проходит через родителя — пропускная способность не страдает от родительской пересылки. (3) Result.Stdout захватывает вывод последней стадии, xrtProcessPipelineSuccess выносит вердикт по всем стадиям — отказ любой стадии проваливает всю цепочку (семантика отказа конвейера: всё или ничего). (4) Контраст с ручной склейкой: наследование дескрипторов, двусторонние deadlock-и — капканы названы поимённо в комментариях образца; то, что их забрала реализация, и есть причина существования Pipeline. Образец file демонстрирует перенаправление вывода в файл (xrt-process-output.txt) — захват и перенаправление суть два пункта назначения обработки стандартных потоков.

52.4契约

  • Выбор семейства: доверенная команда — Shell для экономии; недоверенный вход — обязательно Spawn+Argv — линия обороны от инъекций одного семейства с параметризацией SQL.
  • Трёхзначная семантика: отказ запуска (ошибка родителя) / нормальный выход (код) / убийство сигналом (сигнал) — Success = код 0 и нормальный выход.
  • Балансировка результата: ResultUnit освобождает захваченные буферы включённо — каждому Result свой Unit.
  • Лестница тайм-аута: конфигурация объявляет тайм-аут и убийство (TERM→льгота→KILL) — зависший потомок не утащит родителя.
  • Семантика конвейера: между стадиями трубы ОС напрямую, данные минуют родителя; последняя стадия может захватывать; отказ любой стадии проваливает цепочку.
  • Управление средой: рабочий каталог и переменные окружения объявлены в конфигурации — среда исполнения потомка это явный контракт, а не лотерея наследования.

От примеров к инженерии: три хозяина дочерних процессов

Клей инструментов (арена семейства Shell): транскодирование, сжатие, скрипты уведомлений — когда зрелые утилиты командной строки уже есть, вызвать их быстрее и надёжнее, чем переписывать библиотеку; командная строка — константа кода (доверенная), захват вывода, лестница тайм-аута — три настройки за один шаг.Граница безопасности (арена семейства Spawn): обработка файлов по запросу пользователя, исполнение плагинов — argv-изация исключает инъекции, рабочий каталог и окружение объявлены явно, ресурсы (тайм-аут + потолок вывода — поле StdoutTruncated сообщает, что обрезали) подконтрольны с трёх сторон.Потоки данных (арена Pipeline): шаги ETL, цепочки сборки — многостадийная сцепка вручается трубам ОС (пропускная способность выше родительской пересылки), вердикт по всем стадиям (семантика сборки). Общая нижняя черта трёх хозяев: потомок — код за границей — тайм-аут, потолок вывода и трёхзначный выход суть три вещи, о которых думают при каждом вызове; сэкономив на любом, оплатите в продакшене другим способом.

Полная бухгалтерия структуры результата

Поля xprocessresult богаче, чем «вывод + код выхода»; стоит проговорить поимённо. Status — трёхзначная основа (нормальный/сигнал/отказ); Wait — исход ожидания (тайм-аутные убийства всплывают здесь); InputWritten — байты, переданные в stdin потомка (запись в трубу бывает частичной); Stdout/Stderr — каждый из двух потоков несёт размер и отметку усечения (StdoutTruncated — взводится, когда потолок захвата задан и превышен: прочитано может быть не всё, перед вердиктом проверять обязательно); Duration — длительность в миллисекундах (мера главы 42 — готовое поле для наблюдения производительности). Читайте поля как бухгалтерию: каждое поле — окаменевшее однажды «здесь могло пойти не так» — отметка усечения бережёт от ошибочного вердикта, длительность — от слепых зон, разделение потоков — от смешанного чтения. Проектируйте собственные обёртки по этой полноте — плотность полей структуры результата суть окаменелость плотности ям, пройденных автором API.

Стык с томом 6: процесс тоже единица параллелизма

Открытие тома 6 этой главой — не совпадение порядка: процесс — древнейшая единица параллелизма. Контраст с потоком: процесс изолирует сильно (адресное пространство своё — крахи не заразны) и стартует дорого (цена fork/exec); поток делит память (связь без копий, но нужна синхронизация — глава 53) и стартует дёшево. Выбор определяется потребностью в изоляции: недоверенный/ломкий код (плагины, сторонние утилиты) — процесс; кооперативные вычисления — поток плюс параллельная система глав после этой. Точки контакта процесса с другими единицами параллелизма: межстадийная конкурентность конвейера (трубы ОС как каналы — процессная редакция Channel); тайм-аутное убийство встречает систему отмены (токен главы 54 с SIGTERM — форма «отменяемого ожидания» для внешних процессов); балансировка Unit у буфера захвата (та же дисциплина, что у деревьев значений и токенов).Процесс, поток и корутина — три гранулярности параллелизма — эта глава грубейшая, глава 55 тоньше всех, пол между ними — в следующей главе.

Типичные ошибки

Ловушка 1: пользовательский вход вклеен в командную строку Shell:Ловушка 1: пользовательский вход вклеен в командную строку Shell

Симптом: аудит безопасности репортит командную инъекцию — ввод ; rm -rf / или $(curl ...) исполняется после интерпретации.

Причина: семейство Shell отдаёт командную строку shell-у на растолкование — метасимволы (;, |, $, обратный апостроф) все поверхность атаки; пользовательские данные и синтаксис команды живут в одном слое.

错误写法
char sCmd[512];
snprintf(sCmd, sizeof(sCmd), "convert %s -resize 100 %s", sUserInput, sOut);
xrtProcessShell(sCmd, &Result);   /* вход с ";" инъецирует произвольные команды */
正确写法
/* argv объявляется в полях xprocessconfig; Spawn возвращает дескриптор, после ожидания берём результат */
xprocess* P = xrtProcessSpawn(&Config);   /* аргументы передаются как есть - ноль интерпретации */
Ловушка 2: забытый ResultUnit течёт буфером захвата:Ловушка 2: забытый ResultUnit течёт буфером захвата

Симптом: долгоживущая служба медленно растёт — по блоку утечки на каждый внешний вызов; лестница активных байтов статистики главы 6 ползёт вверх.

Причина: Result.Stdout — владеющий буфер, ResultUnit — его единственный выход; путь использования Result не сбалансирован.

错误写法
if ( xrtProcessShell(sCmd, &Result) && xrtProcessResultSuccess(&Result) ) {
	Use(Result.Stdout, Result.Size);   /* использовали и бросили - буфер захвата течёт */
}
正确写法
if ( xrtProcessShell(sCmd, &Result) ) {
	if ( xrtProcessResultSuccess(&Result) ) {
		Use(Result.Stdout, Result.Size);
	}
	xrtProcessResultUnit(&Result);   /* освобождение и на пути успеха, и на пути отказа */
}

Практические задания

Базовое: машина трёх состояний

Постройте потомков с тремя исходами (нормальный код 0, нормальный ненулевой код, убит сигналом) — прогоните каждый с xrtProcessShell, печатая трёхзначные поля Result и вердикт Success.

Продвинутое: вызывающий с тайм-аутом

run(命令, 超时秒数) (команда, тайм-аут в секундах): по тайм-ауту лестница TERM→KILL; захват вывода; возврат трёх состояний. Вызовите им команду sleep и проверьте поля сигнала на пути тайм-аута.

Вызов: безопасный сервис транскодирования

Реализуйте интерфейс транскодирования изображений на семействе Spawn: имена файлов и параметры полностью в argv (ноль склейки), захват вывода, лестница тайм-аута, потолок конкурентности (семафор или пул задач главы 22). Критерии приёмки: набор инъекционных тестов (двадцать злобных имён файлов) ничего не исполняет; процессы по тайм-ауту неизменно подчищаются (без зомби); ограничение конкурентности действует; результат транскодирования сходится с кодом выхода.

Итоги

ТемаКратко
Два семействаShell (командная строка + результат за шаг, доверенно) / Spawn (конфигурация → дескриптор, недоверенный вход)
Три состоянияотказ запуска / нормальный выход (код) / убийство сигналом (сигнал); Success = код 0 и нормальный выход
Балансировка результатаResultUnit освобождает захваченные буферы - каждому Result свой Unit
КонвейерPipeline берёт массив стадий и сцепляет за один вызов; трубы ОС напрямую; последняя стадия может захватывать
Тайм-аутконфигурация объявляет лестницу TERM→льгота→KILL - передающий конец напротив главы 49
Средарабочий каталог/переменные окружения в конфигурации - среда исполнения это контракт, а не лотерея

📖 Справочники API в этой главе:process (56 записей)signal (26 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки