← All postsBLOG

A process is born

A look into the entrails for a process and how it's created

I'd like to get a better understanding of how the beam works, a natural place to start is the process.

spawn(fn -> receive do _ -> "a shout into the void" end end)

Our soon to be born baby process will be spawned from the same node, it has a parent process living in the same house... sorry, node. It makes the code we need to go through a bit shorter.

We can specify some options, in elixir we'd use Process.spawn/4

def spawn_with_opts do
    Process.spawn(Anatomy, :wait_for_msg, [], [:link, {:message_queue_data, :off_heap}])
end

Some interesting options:

  • you can make it linked to the parent process.
  • you can setup a monitor, a message will be sent to notify of the process's death, natural or accidental.
  • you can (damn it's getting repetitive) set a min heap size, the default is 233 words, a word being 8 bytes for 64-bit VMs. it grows in a fibonacci sequence style (up to a certain point) and if you have big plans for the future of the process, knowing it will need a large heap, it can be interesting to save the gc the trouble of growing it god knows how many times. The gc
  • there is message_queue_data this one is interesting and will probably get a post to look into it deeper. You can set it to onheap or offheap as the name suggests, off_heap will be lighter on GC but messages would always be allocated which has a higher cost but spares you lock contention which might be the right tradeoff for a process getting a ton of messages from many processes... anyways, have a look here for a better explanation.
  • a few others I won't bother with here

erlang calls a BIF, a built in function implemented in C as spawn_opt_4, it will parse the options then create the process through erl_create_process

the process is represented with a ... drum roll ... process struct called the PCB, process control block. Many things omitted for brevity.

struct process {
    ErtsPTabElementCommon common;
    // ...
    Eterm* arg_reg;             /* Pointer to argument registers. */
    Eterm def_arg_reg[6];       /* Default array for argument registers. */

    Eterm* heap;                /* Heap start */
    Eterm* hend;                /* Heap end */
    // ...
    Uint heap_sz;               /* Size of heap in words */
    Uint min_heap_size;         /* Minimum size of heap (in words). */
    Uint min_vheap_size;        /* Minimum size of virtual heap (in words). */
    Uint max_heap_size;         /* Maximum size of heap (in words). */

    ErtsCodePtr i;              /* Program counter. */
    // ...
    Uint reds;                  /* No of reductions for this process  */
    // ...
    ErtsSignalPrivQueues sig_qs; /* Signal queues */
    // ...
    ProcDict *dictionary;        /* Process dictionary, may be NULL */
    // ...
    const ErtsCodeMFA* current; /* Current Erlang function, part of the
                                 * funcinfo:
                                 *
                                 * module(0), function(1), arity(2)
                                 *
                                 * (module and functions are tagged atoms;
                                 * arity an untagged integer).
                                 */

    // ...
    ErlOffHeap off_heap;	/* Off-heap data updated by copy_struct(). */
    // ...
    erts_atomic32_t state;      /* Process state flags (see ERTS_PSFLG_*) */
    // ...
    ErtsSignalInQueue sig_inq;
    // ...
};

Throughout the function it will get filled up and properly initialized. The process will be given a PID and its own spot on the global PTab process registry. The heap will be allocated. The execution state properly initialized: instruction pointer set to beam_run_process, the module, function and args setup in their registers. Some tracing stuff happens that I chose to ignore this time around. links and monitors get setup. and to finish up the process is scheduled.

In elixir we can call Process.info/1 to get a peek

iex(15)> Process.info(self())
[
  current_function: {Process, :info, 1},
  initial_call: {:proc_lib, :init_p, 5},
  status: :running,
  message_queue_len: 0,
  links: [#PID<0.247.0>],
  dictionary: [...],
  trap_exit: false,
  error_handler: :error_handler,
  priority: :normal,
  group_leader: #PID<0.70.0>,
  total_heap_size: 10957,
  heap_size: 4185,
  stack_size: 58,
  reductions: 14830,
  garbage_collection: [
    max_heap_size: %{
      error_logger: true,
      include_shared_binaries: false,
      kill: true,
      size: 0
    },
    min_bin_vheap_size: 46422,
    min_heap_size: 233,
    fullsweep_after: 65535,
    minor_gcs: 3
  ],
  suspending: []
]

I've learned quite a bit, and I'm glad I got to read some professional C code, damn it can get complex. Next time we'll be looking at how the scheduler works: reductions and how they're used to budget process execution time fairly.