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_datathis 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.