CtrlK
BlogDocsLog inGet started
Tessl Logo

mcollina/nodejs-core

Debugs native module crashes, optimizes V8 performance, configures node-gyp builds, writes N-API/node-addon-api bindings, and diagnoses libuv event loop issues in Node.js. Use when working with C++ addons, native modules, binding.gyp, node-gyp errors, segfaults, memory leaks in native code, V8 optimization/deoptimization, libuv thread pool tuning, N-API or NAN bindings, build system failures, or any Node.js internals below the JavaScript layer.

99

Quality

99%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

child-process-internals.mdrules/

name:
child-process-internals
description:
IPC, spawn, fork implementation in Node.js
metadata:
{"tags":"child-process, ipc, spawn, fork, exec, internals"}

Node.js Child Process Internals

Understanding how child processes work helps optimize multi-process architectures and debug IPC issues.

Architecture

JavaScript Layer (lib/child_process.js)
├── spawn()
├── fork()
├── exec()
└── execFile()
        │
        ▼
Internal JS (lib/internal/child_process.js)
├── ChildProcess class
├── setupChannel()
└── spawnSync
        │
        ▼
C++ Bindings (src/process_wrap.cc)
├── ProcessWrap
└── Spawn(), Kill()
        │
        ▼
libuv
├── uv_spawn()
├── uv_process_kill()
└── uv_pipe_t (for stdio)

spawn() Implementation

JavaScript Layer

// lib/child_process.js

function spawn(file, args, options) {
  options = normalizeSpawnArguments(file, args, options);

  const child = new ChildProcess();

  child.spawn({
    file: options.file,
    args: options.args,
    cwd: options.cwd,
    detached: !!options.detached,
    envPairs: options.envPairs,
    stdio: options.stdio,
    uid: options.uid,
    gid: options.gid,
  });

  return child;
}

C++ ProcessWrap

// From src/process_wrap.cc

void ProcessWrap::Spawn(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  ProcessWrap* wrap;
  ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());

  Local<Object> js_options = args[0].As<Object>();

  // Get file path
  Local<Value> file_v = js_options->Get(env->context(), env->file_string())
                            .ToLocalChecked();
  node::Utf8Value file(env->isolate(), file_v);

  // Set up uv_process_options_t
  uv_process_options_t options;
  memset(&options, 0, sizeof(uv_process_options_t));

  options.file = *file;
  options.args = args_array;
  options.cwd = cwd.length() > 0 ? *cwd : nullptr;
  options.env = env_array;
  options.stdio = stdio;
  options.stdio_count = stdio_count;

  // Set UID/GID if specified
  if (uid != static_cast<uv_uid_t>(-1)) {
    options.uid = uid;
    options.flags |= UV_PROCESS_SETUID;
  }

  // Spawn the process
  int err = uv_spawn(env->event_loop(), &wrap->process_, &options);

  if (err != 0) {
    // Return error to JavaScript
    args.GetReturnValue().Set(err);
    return;
  }

  // Return PID
  args.GetReturnValue().Set(wrap->process_.pid);
}

fork() and IPC

Fork Implementation

// lib/child_process.js

function fork(modulePath, args, options) {
  options = Object.assign({}, options);

  // Force IPC channel
  options.stdio = options.stdio ? options.stdio.slice() : 'pipe';
  if (!options.stdio.includes('ipc')) {
    options.stdio.push('ipc');
  }

  // Use node executable
  options.execPath = options.execPath || process.execPath;

  return spawn(options.execPath, [modulePath, ...args], options);
}

IPC Channel Setup

// lib/internal/child_process.js

function setupChannel(target, channel, serializationMode) {
  // Set up message handling
  channel.onread = function(arrayBuffer) {
    const message = deserialize(arrayBuffer);
    target.emit('message', message.message, message.handle);
  };

  // Set up send function
  target.send = function(message, handle, options, callback) {
    const serialized = serialize({ message, handle });
    return channel.writeUtf8String(serialized);
  };

  // Handle disconnect
  channel.onDisconnect = function() {
    target.connected = false;
    target.emit('disconnect');
  };
}

IPC Message Format

// Messages are serialized with structure:
{
  cmd: 'NODE_HANDLE',     // or custom command
  type: 'net.Server',     // handle type
  msg: { ... },           // user message
  _handleId: 123          // internal handle ID
}

C++ IPC Implementation

// From src/stream_wrap.cc

// IPC uses named pipes (Windows) or Unix domain sockets
void StreamWrap::SetupIPC() {
  // Create pipe for IPC
  int fds[2];
  int err = uv_socketpair(SOCK_STREAM, 0, fds, UV_NONBLOCK_PIPE, UV_NONBLOCK_PIPE);

  // Pass file descriptor to child
  // Child inherits fd and creates its own pipe handle
}

Handle Passing

Node.js can pass handles (sockets, servers) between processes:

// Parent
const server = net.createServer();
server.listen(8000);

const child = fork('worker.js');
child.send({ type: 'server' }, server);

// Worker
process.on('message', (msg, handle) => {
  if (msg.type === 'server') {
    handle.on('connection', (socket) => {
      // Handle connection in worker
    });
  }
});

Handle Serialization

// From src/stream_base.cc

// Handles are sent as file descriptors over Unix sockets
int StreamBase::SendFD(uv_stream_t* handle, int fd) {
  // Use SCM_RIGHTS to pass file descriptor
  struct msghdr msg;
  struct cmsghdr* cmsg;

  msg.msg_control = control;
  msg.msg_controllen = sizeof(control);

  cmsg = CMSG_FIRSTHDR(&msg);
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;
  cmsg->cmsg_len = CMSG_LEN(sizeof(int));

  *((int*)CMSG_DATA(cmsg)) = fd;

  return sendmsg(socket, &msg, 0);
}

stdio Configuration

Stdio Options

// Different stdio configurations
spawn('cmd', args, {
  stdio: 'inherit'           // Share parent's stdio
});

spawn('cmd', args, {
  stdio: 'pipe'              // Create pipes (default)
});

spawn('cmd', args, {
  stdio: ['pipe', 'pipe', 'pipe', 'ipc']  // With IPC
});

spawn('cmd', args, {
  stdio: [0, 1, 2]           // Inherit specific fds
});

spawn('cmd', args, {
  stdio: ['pipe', fs.openSync('out.log', 'w'), 'pipe']
});

C++ stdio Setup

// From src/process_wrap.cc

void ProcessWrap::ParseStdioOptions(Environment* env,
                                    Local<Object> js_options,
                                    uv_process_options_t* options) {
  Local<Value> stdio_v = js_options->Get(env->context(),
                                          env->stdio_string())
                             .ToLocalChecked();

  Local<Array> stdios = stdio_v.As<Array>();

  for (uint32_t i = 0; i < stdios->Length(); i++) {
    Local<Object> stdio = stdios->Get(env->context(), i)
                              .ToLocalChecked().As<Object>();

    Local<Value> type_v = stdio->Get(env->context(), env->type_string())
                              .ToLocalChecked();

    if (type_v->StrictEquals(env->ignore_string())) {
      options->stdio[i].flags = UV_IGNORE;
    } else if (type_v->StrictEquals(env->pipe_string())) {
      options->stdio[i].flags = UV_CREATE_PIPE | UV_READABLE_PIPE |
                                 UV_WRITABLE_PIPE;
      // Create pipe handle...
    } else if (type_v->StrictEquals(env->inherit_string())) {
      options->stdio[i].flags = UV_INHERIT_FD;
      options->stdio[i].data.fd = i;
    }
  }
}

exec() and execFile()

exec() Implementation

// lib/child_process.js

function exec(command, options, callback) {
  // Use shell
  return execFile(options.shell || '/bin/sh',
                  ['-c', command],
                  options,
                  callback);
}

function execFile(file, args, options, callback) {
  options = {
    ...options,
    shell: false,
    maxBuffer: options.maxBuffer || 1024 * 1024  // 1MB
  };

  const child = spawn(file, args, options);

  let stdout = '';
  let stderr = '';

  child.stdout.on('data', (chunk) => {
    stdout += chunk;
    if (stdout.length > options.maxBuffer) {
      child.kill();
      callback(new Error('maxBuffer exceeded'));
    }
  });

  child.stderr.on('data', (chunk) => {
    stderr += chunk;
  });

  child.on('close', (code, signal) => {
    callback(code === 0 ? null : new Error(`Exit code ${code}`),
             stdout,
             stderr);
  });

  return child;
}

Synchronous Operations

spawnSync Implementation

// Uses libuv's synchronous spawn
const { spawnSync } = require('child_process');

const result = spawnSync('ls', ['-la'], {
  encoding: 'utf8'
});

console.log(result.stdout);
console.log(result.status);  // Exit code
// From src/spawn_sync.cc

void SyncProcessRunner::Spawn(Local<Object> options) {
  // Run event loop synchronously until process exits
  int r;
  do {
    r = uv_run(&loop_, UV_RUN_ONCE);
  } while (r != 0 && !process_exited_);
}

Signal Handling

Sending Signals

const child = spawn('long-running-process');

// Send signal
child.kill('SIGTERM');

// Or with process.kill
process.kill(child.pid, 'SIGKILL');

C++ Signal Implementation

// From src/process_wrap.cc

void ProcessWrap::Kill(const FunctionCallbackInfo<Value>& args) {
  ProcessWrap* wrap;
  ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());

  int signal = args[0].As<Int32>()->Value();

  int err = uv_process_kill(&wrap->process_, signal);

  args.GetReturnValue().Set(err);
}

Detached Processes

// Create daemon process
const child = spawn('daemon-process', [], {
  detached: true,
  stdio: 'ignore'
});

child.unref();  // Allow parent to exit
// From src/process_wrap.cc

if (js_options->Get(env->context(), env->detached_string())
        .ToLocalChecked()->IsTrue()) {
  options.flags |= UV_PROCESS_DETACHED;
}

Performance Considerations

Process Pool Pattern

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

class ProcessPool {
  constructor(modulePath, size = 4) {
    this.workers = [];
    this.queue = [];
    this.currentIndex = 0;

    for (let i = 0; i < size; i++) {
      const worker = fork(modulePath);
      worker.on('message', (result) => {
        const { resolve } = worker.currentTask;
        worker.currentTask = null;
        resolve(result);
        this.processQueue();
      });
      this.workers.push(worker);
    }
  }

  execute(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.processQueue();
    });
  }

  processQueue() {
    if (this.queue.length === 0) return;

    const worker = this.getAvailableWorker();
    if (!worker) return;

    const { task, resolve, reject } = this.queue.shift();
    worker.currentTask = { resolve, reject };
    worker.send(task);
  }

  getAvailableWorker() {
    return this.workers.find(w => !w.currentTask);
  }

  destroy() {
    this.workers.forEach(w => w.kill());
  }
}

Avoid exec() Overhead

// BAD: Shell spawning overhead
exec('ls -la', (err, stdout) => { });

// GOOD: Direct execution
execFile('ls', ['-la'], (err, stdout) => { });

// BETTER: spawn with streaming
const child = spawn('ls', ['-la']);
child.stdout.pipe(process.stdout);

Debugging

Trace Child Processes

const child = spawn('command', args);

console.log('Spawned child pid:', child.pid);

child.on('error', (err) => {
  console.error('Failed to start:', err);
});

child.on('exit', (code, signal) => {
  console.log('Exited:', { code, signal });
});

IPC Debugging

// Parent
child.on('message', (msg) => {
  console.log('[IPC] Received:', JSON.stringify(msg));
});

const originalSend = child.send.bind(child);
child.send = (msg, ...args) => {
  console.log('[IPC] Sending:', JSON.stringify(msg));
  return originalSend(msg, ...args);
};

Common Issues

EPERM on Kill

child.kill('SIGTERM');  // May fail with EPERM

// Check if process is still alive
if (child.exitCode === null && child.signalCode === null) {
  try {
    process.kill(child.pid, 0);  // Check existence
  } catch (e) {
    if (e.code !== 'ESRCH') throw e;
  }
}

IPC Serialization Limits

// BAD: Large messages
child.send({ data: largeBuffer });  // May fail or be slow

// GOOD: Use shared memory or files for large data
const shm = createSharedArrayBuffer(size);
child.send({ shmName: shm.name });

Zombie Processes

// Always handle child exit
child.on('exit', () => {
  // Cleanup
});

// Or with spawn options
spawn('cmd', args, {
  detached: true,
  stdio: 'ignore'
}).unref();  // Won't keep parent alive

References

  • Node.js child_process: lib/child_process.js
  • Internal implementation: lib/internal/child_process.js
  • C++ bindings: src/process_wrap.cc
  • libuv process: deps/uv/src/unix/process.c

rules

build-system.md

child-process-internals.md

commit-messages.md

contributing.md

crypto-internals.md

debugging-native.md

fs-internals.md

libuv-async-io.md

libuv-event-loop.md

libuv-thread-pool.md

memory-debugging.md

napi.md

native-memory.md

net-internals.md

node-addon-api.md

profiling-v8.md

streams-internals.md

v8-garbage-collection.md

v8-hidden-classes.md

v8-jit-compilation.md

worker-threads-internals.md

SKILL.md

tile.json