Как работает `child_process` в Node.js?
child_process позволяет запускать дочерние процессы операционной системы из Node.js — выполнять команды оболочки, другие Node.js-скрипты или любые исполняемые файлы, взаимодействуя с ними через stdin/stdout/stderr и IPC-канал.Зачем нужен child_process
Node.js работает в однопоточном событийном цикле, что делает его неэффективным для CPU-интенсивных задач. Модуль child_process решает эту проблему, позволяя запускать отдельные процессы ОС и взаимодействовать с ними асинхронно.
Основные методы
exec и execSync
Запускает команду в оболочке (shell) и буферизует весь вывод. Удобен для коротких команд, но небезопасен при подстановке пользовательских данных из-за shell injection.
import { exec } from 'child_process';
// Асинхронный вариант с callback
exec('ls -la', (error, stdout, stderr) => {
if (error) {
console.error('Ошибка:', error.message);
return;
}
console.log('Вывод:', stdout);
});
spawn
Запускает процесс напрямую без оболочки, передавая аргументы массивом. Возвращает потоки (streams) вместо буферизованного вывода — подходит для долгоживущих процессов и больших объёмов данных.
import { spawn } from 'child_process';
const child = spawn('node', ['worker.js']);
// Читаем вывод по мере поступления
child.stdout.on('data', (data) => {
console.log('stdout:', data.toString());
});
child.stderr.on('data', (data) => {
console.error('stderr:', data.toString());
});
child.on('close', (code) => {
console.log(`Процесс завершился с кодом: ${code}`);
});
execFile
Аналог exec, но запускает файл напрямую без оболочки — безопаснее для внешних программ.
fork
Специализированный вариант spawn для дочерних Node.js-процессов. Автоматически создаёт IPC-канал, через который родительский и дочерний процессы могут обмениваться сообщениями через process.send() и событие message.
import { fork } from 'child_process';
// Родительский процесс
const child = fork('./worker.js');
child.send({ task: 'heavyComputation', data: [1, 2, 3] });
child.on('message', (result) => {
console.log('Результат от воркера:', result);
});
// worker.js — дочерний процесс
process.on('message', (msg) => {
const result = msg.data.reduce((a: number, b: number) => a + b, 0);
process.send({ result });
});
Коммуникация между процессами
- stdin/stdout/stderr — стандартные потоки, доступны у любого дочернего процесса
- IPC-канал — только при
fork()или при опции{ stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }вspawn() - Данные через IPC передаются как JSON, поэтому сложные объекты сериализуются автоматически
Опции и управление
Все методы принимают объект опций: cwd (рабочая директория), env (переменные окружения), timeout (таймаут выполнения), maxBuffer (лимит буфера для exec). Дочерний процесс можно завершить вызовом child.kill('SIGTERM').
exec vs spawn — когда что использовать
| Метод | Shell | Вывод | Когда использовать |
|---|---|---|---|
exec | Да | Буфер | Короткие команды, результат целиком |
spawn | Нет | Stream | Долгие процессы, большой вывод |
fork | Нет | IPC | Node.js воркеры с двусторонней связью |
Что хочет услышать интервьюер
Знание четырёх основных методов (exec, execFile, spawn, fork) и их принципиальных отличий
Понимание разницы между буферизованным выводом (exec) и потоковым (spawn) и когда каждый применять
Объяснение IPC-канала и того, как fork отличается от spawn для Node.js-воркеров
Осознание рисков безопасности при использовании exec с пользовательскими данными (shell injection)
Понимание, что child_process решает проблему CPU-bound задач в однопоточном Node.js
Пример: exec, spawn и fork — сравнение подходов
import { exec, spawn, fork } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
// exec — короткая команда, получаем результат целиком
async function getNodeVersion(): Promise<string> {
const { stdout } = await execAsync('node --version');
return stdout.trim();
}
// spawn — потоковый вывод, безопаснее для аргументов
function streamLogs(filePath: string): void {
// Аргументы передаются массивом — нет риска shell injection
const tail = spawn('tail', ['-f', filePath]);
tail.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
});
tail.on('error', (err) => {
console.error('Не удалось запустить tail:', err.message);
});
// Завершаем дочерний процесс вместе с родительским
process.on('exit', () => tail.kill('SIGTERM'));
}
// fork — двусторонняя коммуникация с Node.js воркером
function runHeavyTask(data: number[]): Promise<number> {
return new Promise((resolve, reject) => {
const worker = fork('./heavy-worker.js');
worker.send({ data });
worker.on('message', (msg: { result: number }) => {
resolve(msg.result);
worker.kill();
});
worker.on('error', reject);
// Защита от зависания воркера
setTimeout(() => {
worker.kill('SIGKILL');
reject(new Error('Воркер завис — таймаут'));
}, 5000);
});
}
Типичные ошибки
Путают exec и spawn: используют exec для долгоживущих процессов, получая переполнение maxBuffer
Не обрабатывают событие 'error' и 'close', теряя информацию об ошибках дочернего процесса
Передают пользовательский ввод напрямую в exec-команду строкой, создавая уязвимость shell injection
Не понимают, что fork создаёт IPC-канал автоматически, и пытаются его настроить вручную через spawn
Забывают завершать дочерние процессы через child.kill(), создавая утечки процессов


