Coming from a Windows-centric ecosystem, the idea of Linux was both scary and perplexing at the same time. But the rewards have been – and continue to be – too great to ignore. Especially for large companies and organizations. The Linux we have today is very different from the Linux we remember from 10 or 20 years ago. One could say that Linux has come of age.
That is good, because Linux has become, for better or worse, a force of nature, and in this day and age all but unavoidable if your work involves servers.
Being able to write high-quality, high-performance Linux services, a.k.a. dæamons, is thus a valuable skill. One that you can now enjoy from any of the five languages Elements supports.
Shell Dæmons and Forking
If you are coming from Windows, a great place to start your Linux coding journey is to write dæmon applications. A dæmon is comparable to a Windows "service" — a program that runs in the background, without user interaction, to perform a task.
Even if you don't know all the libraries, how the kernel works and all the nitty-gritty, you can get productive straight away in Elements. I speak from experience here, because I have just recently began to use and code for Linux in a more professional capacity. I must say I am both intrigued and fascinated by Linux, especially since I can now apply those skills to IoT.
Before we look at the code, we need to have a few words on a technique called "forking" (stop chuckling!), which is a Linux concept you will run into almost everywhere.
So from a developer context, forking is a kernel function for duplicating a running process in memory. If you fork a process, you are simply creating a second instance.
What people mostly do is implement their dæmon code as an ordinary shell program (there are other approaches, like "snap in modules", but that is out of scope for this post). But, to avoid locking the shell completely, and by consequence a boot process, your program has to exit. Otherwise your shell program would halt everything. Paradoxical? Indeed.
The solution to this paradoxical situation is to create a fork of your own process that is not linked to the current running shell (more about that later). Once this clone has been created in memory, the original application can just exit. The fork will continue to run in the background as a solo process – without blocking the initial invoker.
A Plain Example
Right, so let's have a look at a working example. I will be writing this in Oxygene, our Object Pascal part of the Elements compiler. I'm also using our Water IDE, which is lightweight compared to Visual Studio. But the same code will compile from all Elements IDEs just fine.
Let's dig into the example code:
namespace ConsoleApplication101;
uses
rtl;
type
Program = class
public
class var fWait: EventWaitHandle;
class method SigTermHandler(signum: Integer; info: ^rtl.siginfo_t;
ptr: ^Void);
begin
fWait.Set();
end;
class method Main(args: array of String): Int32;
begin
writeLn('RemObjects Elements deamon sample');
// Fork splits the process in two.
// The caller gets the child process pid, the child gets 0.
var lPid := fork();
if lPid <=0 then begin
writeLn('Unable to fork; failed!');
exit 1;
end;
if lPid > 0 then begin
writeLn('Forked to pid: '+lPid);
exit 0;
end;
writeLn('Hello from forked process!');
// Sets up a session so it's not closed with the parent process
var lSid := setsid();
if lSid < 0 then begin
writeLn('Unable to create a new session!');
exit 0;
end;
writeLn('Closing output/input handles from forked process');
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// Note: at this point, writeLn has no effect anymore.
fWait := new EventWaitHandle(false, false);
// setup sigterm handler
var lAct := new __struct_sigaction;
lAct.sa_flags := SA_SIGINFO;
lAct.__sigaction_handler.sa_sigaction := @SigTermHandler;
// setup the sigterm handler
sigaction(SIGTERM, @lAct, nil);
// Here we could do something dæmon specific!
// like spin up a tcp socket server
// And wait till it's done.
fWait.Wait();
end;
end;
end.
Take a minute to look at the code above and let it digest. It is not difficult to understand what happens, but some of the steps needs to be explained to make better sense.
Let's go through the boilerplate code and see what we have.
Just like in a C program, we have an entry point function called "main" that takes command line arguments and returns an integer as "exit code". If you are coming from Windows and have worked with command-line programs, this should be very familiar to you. Rule of thumb: if nothing went wrong, return zero (the default in Oxygene).
The first thing we do inside the main() function is to create a fork of our own process. If the cloning of the process was a success, we are given a PID (process identifier) for the cloned process which we also test against to see if everything went ok.
var lPid := fork();
if lPid <=0 then begin
writeLn('Unable to fork; failed!');
exit 1;
end;
if lPid > 0 then begin
writeLn('Forked to pid: '+lPid);
exit 0;
end;
At this point the forked process is intimately connected to our own. The best way to look at Linux processes is to imagine a tree view made up of parent/child ownerships. And by default a fork will be owned by the calling program. This is not what we want, because if ownership is retained with the current running process, the newly created fork will terminate when we exit.
To avoid this, we set up a fresh new session for the fork via the kernel function SetSid (documentation here: https://linux.die.net/man/2/setsid).
var lSid := setsid();
if lSid < 0 then begin
writeLn('Unable to create a new session!');
exit 0;
end;
With the fork running in a fresh session, we can now decouple the last bonds to our current running process (the fork will continue under that session, even when we exit later):
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
The reason we have to do this is because by default a fork will have the same stdout
etc. as the owning process, so in order to truly cut all bonds between the fork and our staging process, we have to shut those down on our end. Once these are shut down, our forked process is running completely solo and we can exit without any issues.
But there is one final step to take. Since our fork or clone is now running solo in memory, how exactly will it know when to stop? This obviously depends greatly on what type of dæmon you are writing. Some dæmons should only run when there is a need for them, so they do some work and then gracefully exit afterwards. Other dæmons, like a web server or datastore server, must be up and running until the computer reboots or is shut down.
And that is exactly what we do here, namely to set up a signal handler for the shutdown event. Signal handlers are for all intents and purposes the same as events. Under Microsoft Windows, services are told when to terminate though a message, and under Linux it's pretty much the same thing (technically signals are invoked directly by the dæmon host, which in our case is systemd
, but that's a whole different article).
var lAct := new __struct_sigaction;
lAct.sa_flags := SA_SIGINFO;
lAct.__sigaction_handler.sa_sigaction := @SigTermHandler;
Our event/signal handling code looks like this:
class procedure SigTermHandler(signum: Integer; info: ^rtl.siginfo_t; ptr: ^Void);
begin
fWait.Set();
end;
So when the "terminate" signal is picked up, we set our thread safe and atomic event object, which causes the program to exit. You probably noticed this line at the end of the main() function? It causes execution to wait until said object is set.
// And wait until it's done.
fWait.Wait();
Note #1: If you have worked with multithreading, you are probably familiar with event objects. In this small example we use it to prevent our fork process from just exiting straight away.
And that's all there is to it really. After the staging code is finished, you can create instances of whatever class you like. Personally I like to wrap some of this boilerplate in OOP. Since the main() function is a class procedure, you can flesh out that class and just create an instance of it (perhaps with a startservice() method where you isolate how the fork should initialize). Or you can treat the program class as a mere stub, one that sets everything up and hands over control to a second class.
Note #2: Since Elements is platform independent, it's relatively easy to write staging classes for Windows, OS X and Linux that deal with the peculiarities of each. This is also why I like to keep the business logic in a separate class.
You pick what you feel is best for your project or coding style.
Reference Material and Notes
The code in this article is low-level, but thankfully the boilerplate code for a Linux dæmon doesn't have to be more complex than demonstrated in this post.
Just like Microsoft Windows has its SDK and online documentation, so does Linux. Here are some links to the system functions we called in our code. I excluded calls like close() since that is more or less self-explanatory.
If you ever heard the phrase "everything in Linux is a file", that's actually not far from the truth. Both stdin
, stdout
, as well as stderr
are regarded as valid file-handles, and can be written to, read from and consequently closed using the same api calls you would use on a file.
- fork() method: https://linux.die.net/man/2/fork
- setsid() method: https://linux.die.net/man/2/setsid
- sigaction() method: https://linux.die.net/man/2/sigaction
Happy coding!