About Node and Linux Processes

Lately I’ve been learning more about Linux systems programming, as well as Node.js. As such I’ve learned that the two share some similarities; in general Node seems to borrow a lot from Unix/Linux standards. In this post I will cover one specific similarity; how they handle processes.

A process is a running instance of a program. In node they are accessed through process object, which exposes many of the functions and properties of the currently running node process. In Linux, processes can be interacted with by a suite of functions built to do so.

POSIX Signal Events

Like in Linux, Node has the ability to handle POSIX signal events. POSIX signals are a form of interprocess communication; a signal will be sent asynchronously to another process that signifies some sort of event.

A common signal is SIGINT , or signal interrupt, it is often triggered when a user presses CTRL-C in the terminal. When handled in a Node script it would look something like:

function handle(signal) {
console.log(`Received ${signal}`);
}
process.on('SIGINT', handle);

Where handle is a generalized function that can take any type of signal, not just SIGINT . The last line attaches the handle object to the SIGINT event, or signal.

When writing this code for Linux processes, it would look very similar. You’d write a function capable of handling a signal, or group of them:

static void handler(int sig) {
switch(sig) {
case SIGINT: cout << "Handles SIGINT signal..." << endl;
break;
case SIGTSTP: cout << "Handles SIGSTP signal..." << endl;
break;
}
}

Then use the signal function to attach this handler to a specific signal.

sighandler_t error = signal(SIGINT, handler);

Exit Codes

In Node when a process terminates it often ends with exit code 0. However in some cases an error might occur, in which case a different exit code will be returned depending on the specific error. While not the same, there is some overlap between exit codes in Linux and Node. In Linux they are:

  • 1 - Catchall for general errors
  • 2 - Misuse of shell builtins (according to Bash documentation)
  • 126 - Command invoked cannot execute
  • 127 - “command not found”
  • 128 - Invalid argument to exit
  • 128+n - Fatal error signal “n”
  • 130 - Script terminated by Control-C
  • 255\* - Exit status out of range

Node exit codes are a lot more varied, for a full list you can find them here. Of those available those similar to the Linux implementation include:

  • 1 Uncaught Fatal Exception - There was an uncaught exception, and it was not handled by a domain or an uncaughtException event handler.
  • 2 - Unused (reserved by Bash for builtin misuse)
  • >128 Signal Exits - If Node receives a fatal signal such as SIGKILL or SIGHUP, then its exit code will be 128plus the value of the signal code. This is a standard Unix practice, since exit codes are defined to be 7-bit integers, and signal exits set the high-order bit, and then contain the value of the signal code.

Forking (Child Processes)

Both Linux and Node processes have the ability to fork a parent and run child processes. Creating and using multiple processes in this way can be a great way to better scale an application and make use of it.

In Node child processes can be created with a variety of functions, but the one of most interest here is a function called fork() . In both Linux and Node this function not only creates a child process, but can also be used to communicate between the parent and child.

For Node, the parent file would look like the following:

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

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

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

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

And in the child file, child.js:

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

let counter = 0;

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