Подпроцессы Node.js, которые вы должны знать

Node.js Командная строка JavaScript Shell

Статья переведена сNode.js Child Processes: Everything you need to know

Как использовать функцию spawn, функцию exec, функцию execFile и функцию for

child-process.png

Неблокирующая однопоточная функция в Node.js очень полезна для однопроцессных задач. Но на самом деле в условиях усложняющейся бизнес-логики вычислительной мощности, обеспечиваемой одним процессом в одном ЦП, явно недостаточно. Потому что независимо от того, насколько мощный сервер, один поток может использовать только ограниченные ресурсы.

Тот факт, что Node.js работает в одном потоке, не означает, что разработчики не могут использовать преимущества нескольких процессов и, конечно же, нескольких серверов.

Использование многопроцессорности — лучший способ масштабировать вашу программу Node.js. Node.js предназначен для создания распределенных приложений на нескольких узлах. Это также является причиной названия Node. Масштабируемость проникла в платформу, поэтому разработчики не могут ждать, пока приложение запустится в конце жизненного цикла, чтобы начать думать об этом.

Обратите внимание, что перед чтением этой статьи вы должны иметь представление о событиях Node.js и потоках Node.js. Если вы не готовы, я рекомендую вам прочитать следующие две статьи:

Node.js, управляемый событиями Потоки Node.js, которые вы должны знать

модуль подпроцесса

Разработчики могут легко создавать дочерние процессы с помощью модуля Node child_process. Эти подсистемы могут взаимодействовать друг с другом через систему сообщений.

Разработчики могут получить доступ к операционной системе через внутренние команды модуля child_process.

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

Обратите внимание, что все примеры в этой статье основаны на системе Linux.Если вы используете систему Windows, вам необходимо заменить соответствующие команды Linux командами Windows.

В Node.js есть четыре функции для создания дочерних процессов: spawn(), fork(), exec() и execFile().

Далее мы обсудим сценарии применения различных функций между этими четырьмя функциями.

Спаун (spawn) дочерний процесс

Функция Spwan может порождать новый дочерний процесс и передавать команды дочернему процессу через функцию Spwan. Например, порождая подпроцесс, выполните команду «pwd»:

const { spawn } = require('child_process');
const child = spawn('pwd');

Программа Node.js удаляет функцию spawn из модуля child_process, передает функции ОС команды и выполняет команды ОС в дочернем процессе.

Результатом выполнения функции порождения является объект экземпляра дочернего процесса, который наследует интерфейс событий, и разработчики могут напрямую регистрировать в нем обработчики событий. Например, разработчик регистрирует события для результата выполнения дочернего процесса и выходного поведения дочернего процесса:

child.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});
child.stderr.on('data', (data) => {
  console.log(`stderr: ${data}`);
});

child.on('exit', function (code, signal) {
    console.log('child process exited with ' +
                `code ${code} and signal ${signal}`);
  });

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

  • Событие отключения: срабатывает, когда родительский процесс вызывает функцию child.disconnect
  • событие ошибки: срабатывает, когда процесс не может быть запущен или процесс убит
  • событие закрытия: срабатывает, когда поток stdio дочернего процесса закрывается
  • событие сообщения: запускается, когда дочерний процесс использует функцию process.send(), которая в основном используется для связи между родительским и дочерним процессами.

У каждого дочернего процесса есть стандартный поток stdio, и разработчики могут управлять потоком stdio через child.stdin, child.stdout и child.stderr.

Дочерний процесс запускает событие закрытия, когда поток stdio в дочернем процессе закрывается. Событие закрытия не совсем эквивалентно событию существования, главным образом потому, что дочерние процессы могут совместно использовать один и тот же поток stdio, а дочерний процесс не приводит к закрытию потока.

Поскольку поток является триггером события, разработчик может прослушивать событие в потоке stdio дочернего процесса. В отличие от обычных процессов, в дочерних процессах потоки stdout/stderr доступны для чтения, а stdin доступен для записи. По сути, свойства этих потоков в дочернем процессе противоположны свойствам основного процесса. Самое главное, прослушивая событие данных, программа может получить выходные данные команды или информацию об исключении, сгенерированную при выполнении команды.

child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

child.stderr.on('data', (data) => {
  console.error(`child stderr:\n${data}`);
});

Когда программа выполнит указанную выше функцию порождения, будут напечатаны выходные данные команды «pwd». Дочерний процесс завершится с возвратом 0, что означает, что исключения не произошло.

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

const child = spawn('find', ['.', '-type', 'f']);

Если во время выполнения команды возникает исключение, запускается событие данных child.stderr.Событие получает код выхода программы 1 (это означает, что программа имеет исключение).Информация об исключении обычно различается в зависимости от типа исключения и системы ОС.

Поскольку stdin дочернего процесса является записываемым потоком, разработчики могут записывать данные в дочерний процесс через него. Как и любой другой поток с возможностью записи, метод канала — это самый простой способ использования потока с возможностью записи, и программа может записывать поток с возможностью чтения в поток с возможностью записи. Поскольку stdin основного процесса является читаемым потоком, основной процесс может передавать данные дочернему процессу. Например:

const { spawn } = require('child_process');

const child = spawn('wc');

process.stdin.pipe(child.stdin)

child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

В приведенном выше примере дочерний процесс запускает команду wc для подсчета количества строк и символов входных данных. Затем перенесите стандартный ввод (поток для чтения) основного процесса в стандартный ввод (поток для записи) дочернего процесса. После выполнения вышеуказанной программы инструмент командной строки включит режим ввода. При вводе комбинации клавиш Ctrl+D ввод прекращается. Введенные данные будут использоваться в качестве источника входных данных для команды wc.

stream-pipe.gif

Разработчики используют выходные данные процесса в качестве источника входных данных для другого процесса, реализуя команды канала, такие как команды linux. Например, разработчики используют поток stdout команды find в качестве источника входных данных команды wc для измерения количества файлов в папке:

const { spawn } = require('child_process');

const find = spawn('find', ['.', '-type', 'f']);
const wc = spawn('wc', ['-l']);

find.stdout.pipe(wc.stdin);

wc.stdout.on('data', (data) => {
  console.log(`Number of files ${data}`);
});

Добавьте параметр -l после команды wc, чтобы подсчитать количество строк в файле. Вышеупомянутая программа подсчитает все файлы во всех каталогах под текущим элементом.

Синтаксис оболочки и функция exec

По умолчанию функция spawn не создает новую оболочку, выполняя команду, переданную в качестве аргумента. Это основная причина, по которой функция spawn более эффективна, чем функция exec, поскольку не создается новая оболочка. Существует еще одно существенное различие между функцией exec и функцией spawn.Функция spawn обрабатывает результат выполнения команды через поток, а функция exec кэширует результат выполнения программы и, наконец, передает кэшированный результат в функцию обратного вызова. .

Ниже приведен пример реализации команды find|wx через функцию exec:

const { exec } = require('child_process');

exec('find . -type f | wc -l', (err, stdout, stderr) => {
  if (err) {
    console.error(`exec error: ${err}`);
    return;
  }

  console.log(`Number of files ${stdout}`);
});

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

Стоит отметить, что необходимо убедиться, что команды ОС, передаваемые функции exec, не имеют последствий для безопасности. Потому что пользователю нужно ввести только некоторые конкретные команды для проведения атак с внедрением команд, например:rm -rf ~~.

Функция exec кэширует вывод команды и передает результат вывода в качестве параметра функции обратного вызова в функцию обратного вызова.

Использование синтаксиса оболочки — хороший выбор, если вам нужно использовать синтаксис оболочки и ожидать, что команда будет работать с небольшими файлами.Обратите внимание, что функция exec сначала кэширует возвращаемые данные в памяти, а затем возвращает значение.

Если данные, полученные после выполнения команды, слишком велики, хорошим выбором будет функция spawn, поскольку использование функции spawn преобразует стандартный объект ввода-вывода в поток.

Программа может порождать дочерний процесс, который наследует стандартные объекты ввода-вывода родительского процесса через функцию порождения, и при необходимости может использовать синтаксис оболочки в дочернем процессе. Следующий код представляет собой код для реализации пользовательского подпроцесса:

const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true
});

настраиватьstdion: 'inherit', при выполнении кода дочерний процесс наследует stdin, stdout и stderr основного процесса. Поток process.stdout основного процесса вызовет обработчик событий дочернего процесса, и результат будет немедленно выведен в обработчик событий.

настраиватьshell: true, как и функция exec, программа может передавать синтаксис оболочки производной функции в качестве аргумента производной функции. Но даже в этом случае можно использовать свойства потоков в производных функциях.Я должен сказать, что это довольно круто

В дополнение к настройке оболочки и stdio в объекте параметров функции, производной от порождения, разработчик также может установить другие параметры. Каталог, в котором работает программа, задается через атрибут cwd. Например, следующее устанавливает рабочий каталог программы в папку загрузки и реализует код, который вычисляет количество всех файлов в папке назначения:

const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true,
  cwd: '/Users/samer/Downloads'
});

Используя свойство env объекта option, вы можете установить переменные среды, видимые дочерним процессам. process.env — это значение свойства env по умолчанию, обеспечивающее доступ любой команды к текущей среде процесса. Разработчики могут установить для свойства env пустой объект или значение переменной среды, видимой для дочернего процесса, чтобы настроить переменную среды, видимую для дочернего процесса.

const child = spawn('echo $ANSWER', {
  stdio: 'inherit',
  shell: true,
  env: { ANSWER: 42 },
});

Приведенная выше команда echo не имеет доступа к переменным среды родительского процесса. У процесса нет доступа из-за установки значения свойства envHONE的权限但是可以访问ОТВЕЧАТЬ.

Установив атрибут detached объекта option в функции spawn, дочерний процесс может быть полностью независимым от вызова родительского процесса.

Предположим, у нас есть тестовая программа timer.js, которая держит цикл обработки событий занятым:

setTimeout(() => {  
  // keep the event loop busy
}, 20000);

Программа устанавливает свойство detached объекта option в функции spawn для выполнения программы timer.js в фоновом режиме:

const { spawn } = require('child_process');

const child = spawn('node', ['timer.js'], {
  detached: true,
  stdio: 'ignore'
});

child.unref();

Независимые дочерние процессы выполняются в разных системах и ведут себя по-разному. В среде Windows отдельные дочерние процессы имеют отдельные окна консоли. В среде Linux независимый дочерний процесс станет лидером новой группы процессов или сеанса.

При вызове функции unref в отдельном дочернем процессе родительский процесс может завершаться и выполняться независимо от дочернего процесса. Эта функция подходит для следующих сценариев: дочерний процесс должен работать в фоновом режиме в течение длительного времени, а поток stdio дочернего процесса должен быть независимым от родительского процесса.

В приведенном выше примере кода установите для свойства detached объекта option значение true, и независимый дочерний процесс выполнит код nodejs (timer.js) в фоновом режиме. Установите свойство stdio объекта option объекта option для игнорирования, и дочерний процесс будет иметь поток stdio, независимый от основного процесса. Таким образом, родительский процесс может быть завершен, когда дочерний процесс все еще выполняется в фоновом режиме.

independent-stdio.gif

функция execFile

Если разработчику не нужно использовать оболочку для выполнения файла, хорошим выбором будет функция execFile. Функция execFile очень похожа на функцию exec, но поскольку execFile не порождает новую оболочку, это основная причина, по которой функция execFile более эффективна, чем функция exec. В среде Windows такие файлы, как .bat и .cmd, не могут выполняться независимо. Однако эти файлы можно запустить с помощью функции exec или путем установки функции оболочки функции spawn.

* Функция синхронизации

Функция spawn, функция exec и функция execFile в модуле подпроцесса также имеют соответствующие функции синхронизации и блокировки. Они будут ждать завершения выполнения дочернего процесса перед выходом.

const { 
  spawnSync, 
  execSync, 
  execFileSync,
} = require('child_process');

Эти синхронизированные функции полезны для упрощения задачи выполнения сценариев или обработчиков, но в противном случае их следует избегать.

функция форка

Функция fork и функция spawn не совпадают при порождении дочерних процессов. Основное различие между ними заключается в том, что дочерний процесс, полученный с помощью функции fork, установит коммуникационный конвейер, производный дочерний процесс может отправлять информацию основному процессу через функцию отправки, а основной процесс также может отправлять информацию дочернему процессу через функция отправки. Вот пример кода:

Код родительского процесса:

const { fork } = require('child_process');

const forked = fork('child.js');

forked.on('message', (msg) => {
  console.log('Message from child', msg);
});

forked.send({ hello: 'world' });

Код подпроцесса:

process.on('message', (msg) => {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);

В программе родительского процесса разработчик может разветвить файл (этот файл будет выполняться командой узла), а затем прослушать событие сообщения. Когда дочерний процесс вызывает функцию process.send, будет запущено событие сообщения родительского процесса. В приведенном выше коде дочерний процесс будет вызывать функцию process.send каждую минуту.

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

Когда вышеупомянутый родительский процесс выполняется, родительский процесс передает объект {hello: 'world'} дочернему процессу, а затем дочерний процесс распечатывает сообщения, переданные родительским процессом. В то же время дочерний процесс будет каждую минуту отсылать родительскому процессу увеличивающееся число, и эти числа будут выводиться в окне управления родительским процессом.

fork.gif

Давайте рассмотрим более практичный пример форка:

Разработчик открывает два API на сервисе http. Один из них — «/compute», по этому API будет делаться много вычислений и процесс вычислений займет много времени. Мы можем смоделировать описанный выше сценарий с помощью цикла for:

const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const sum = longComputation();
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

В приведенной выше программе есть проблема: когда запрашивается служба http "/compute", поскольку цикл for блокирует процесс службы http, служба http больше не сможет обрабатывать другие запросы API.

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

Сначала инкапсулируем функцию longComputation в отдельный файл js, и выполняем функцию longComputation через информационную инструкцию родительского процесса:

const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};

process.on('message', (msg) => {
  const sum = longComputation();
  process.send(sum);
});

Нет необходимости выполнять операцию в функции longComputation в основном процессе, но новый дочерний процесс выводится через функцию fork, затем вычисляется в дочернем процессе, и, наконец, результат операции передается обратно в родительский процесс через конвейер передачи информации функции форка.

const http = require('http');
const { fork } = require('child_process');

const server = http.createServer();

server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const compute = fork('compute.js');
    compute.send('start');
    compute.on('message', sum => {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

При запросе '/compute' дочерний процесс возвращает результат вычисления родительскому процессу через функцию process.send, так что цикл событий основного процесса больше не будет блокироваться.

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

Если служба является дочерним процессом, полученным из нескольких функций разветвления, модуль кластера Node.js будет выполнять балансировку нагрузки HTTP-запросов для внешних запросов. Это то, что я собираюсь осветить в моей следующей теме.

Это все, что у меня есть по этому вопросу, большое спасибо за чтение, и с нетерпением жду встречи с вами в следующий раз.