I got interested in systems programming due to an assignment at work that involved sockets. I got curious about what could be happening behind such an interface.
I started studying the python interface to sockets. It was very high level and I did not learn much. So, I started studying the UNIX interface. With UNIX, I got overwhelmed and painfully realized that I won’t be able to understand it unless I know how operating systems work. I began my long quest towards learning how low-level things work.
The first program I wrote in this domain was a command shell. It was the first significant program I wrote in C.
Apart from frivolous reasons like “I wanted to program in C”, I have two good reasons.
The purpose of the command shell is to take command strings as input, parse them, execute them, and repeat the cycle. Seen this way, you need a part to read inputs, a part to parse inputs, and a part to execute inputs. Let me describe the road I took to write this whole system.
To get my feet wet, I wrote something that echoes back my inputs.
The next thing I did was this. Given a string obeying certain rules, translate it into a
format suitable as input for an exec frontend like execv
.
Finally, we can execute the input object we constructed.
The above snippet will replace the shell process with the program image specified in the
input_object
. We would like to continue processing new input. One solution is to
make a copy of the shell process and then replace that copied process with the
program specified in the input_object
. This copying is done using fork
system call.
Finally, we want to move our shell to foreground of our terminal device. Broadly, I linked the shell file descriptor with the current foreground process group associated with terminal device.
The source code of the above description can be found here. After some initial excitement, I got bored. There is no single program that can solve your problems. To solve interesting problems, you need multiple programs communicating with each other. And the best way for communications is the sockets interface. The word “sockets” draw a picture of reliable communication over ethernet or wifi interfaces. TCP over IPv4 is only one part of the operating system’s socket interface. For address spaces, you have UNIX sockets as well as raw sockets. For communication semantics, you have TCP, UDP, RAW, and SEQPACKET.
But the above stupid program does illustrate some things you need to worry about in distributed programs
The input needs to be in a specified format for the program to be able to process it. Most of the programmers know about HTTP protocol but I don’t think the overhead of parsing HTTP requests is worth it for internal communication in network programs. For that, you need a custom protocol.
The shell executes inside a computer and has only one client. But it is not hard to see the approach of fork/exec working for multiple clients. Forking a process for every client is the classic UNIX approach used since the 1980s. Of course, there are other low-overhead approaches like threads or continuations. But many programs still use this idea.
If you notice here, I attempt to minimize memory consumption by reallocating space for command string array and freeing the originally allocated array. Memory allocations are expensive and there are many projects like jemalloc that aim to solve this problem.
This shell was a good starting point for writing the kind of programs that solve more intresting problems.