Systemd and Autotools
This post starts a tutorial series on how to create a systemd service in Linux. This first post is likely the hardest part as we create an autotools project with the source code and systemd unit files. Having this setup in autotools saves significant time installing unit files that systemd requires, building our C code, installing, and uninstalling our program. If you can get through this first post the following posts should come easily.
A service is a process that is controlled and supervised by systemd.
Quick note, Mac OS X does not have systemd. This post is only going to work on Linux.
If you don’t want to use autotools that’s OK. Skip the sections specific to autotools but focus on where the .service files are installed, as well as, where the binary is placed on the system that systemd controls.
What this example service will and won’t do
We want to package a .service file in our autotools project. When the user types sudo make install we want the .service file put into the correct directory with the correct privileges. It looks like this:
$ sudo make install
...
/bin/mkdir -p '/lib/systemd/system'
/usr/bin/install -c -m 644 foo.service '/lib/systemd/system'
...This example we will create the simplest possible service so we can do what is above. The service can be started and stopped by systemd. We can also check some rudimentary status.
What this example won’t do is:
- Log either to
syslogorjournaldthis is a big limitation - Have the ability to reload it’s configuration without stopping and starting again
- Be passed in file descriptors or sockets
- Have dependencies on other services
- Install the service
- Run the service when the system starts
We’ll add functionality later and address these limitations. This post we’ll focus on just getting a simple process controlled by systemd.
Background Documentation
I try to stick to the source of truth which are the man pages. Staring with systemd(1). This (1) means section 1 in the man pages.
$ man 1 systemd
$ man systemd # same thing as the default is section 1From there you’ll see references to systemd.service(5) which we read with:
$ man 5 systemd.serviceFinally, the daemon(7) will be useful. As well as, systemd.unit(5) and systemd.syntax(5).
I’ll outline what’s in these documents, but wanted to provide some background.
Creating a Simple Service
Let’s start some code to create a service from. It’s as simple as possible:
// file main.c which we will compile to foo
#include "../config.h" // needed for autotools
#include <stdio.h>
#include <unistd.h>
int
main(int argc, char *argv[])
{
while(1)
{
printf("going to sleep\n");
sleep(10);
}
return 0;
}Yep, super easy. All we do is loop forever, printing a statement and sleeping for 10 seconds. When we run this program it will become a process. We’ll use systemd to control this process.
A Simple Systemd Service
How can we make this a service that is controlled by systemd? We need a .service file.
|
|
See that line 6 is the binary that we will compile main.c into.
As you will see later we install this service to /lib/systemd/system as it is the recommended location per daemon(7). It’s recommended because of the following. Note, this can differ per system.
# pkg-config --variable=systemdsystemunitdir systemd
/lib/systemd/systemService Type
In the [Service] section we will use the default Type=simple, however it’s worth noting some of these types. See systemd.service(3) for the types of simple, exec, forking, oneshot, dbus, notify or idle.
Type=simplesystemd will effectively fork and daemonize our process for us. It’s also expected that the process is the main process for the service.Type=execsimilar toType=simpleI’ll let you read the fine points that deal with when other units relating to the service unit are started.Type=forkinghere the process will be expected to do it’s own forking and the parent process is expected to exit after start up and communication channels are set up. It’s recommended to use thePIDFile=option so other processes can determine the process ID of the main process.Type=oneshotsystemd will expect the unit to be started up after the main process exits. This can be thought of as acronjob where it runs once.Type=notifyis likeType=exec, however, the process should send notifications through the sd_notify(3) API for systemd to keep track of it’s state.Type=idleis similar toType=simplebut relates to execution of active jobs that are dispatched.
In the next post we’ll change from the default Type=simple to Type=notify.
Starting and Stopping the Service
Before we install the foo.service and build our project let’s look at how we can control our process with systemd. The systemd framework uses the systemctl command for management.
# systemctl list-unit-files foo.service
UNIT FILE STATE
foo.service static
1 unit files listed.
# find / -name "foo.service"
/lib/systemd/system/foo.service
# systemctl start foo
# systemctl status foo
● foo.service - A Example Systemd Service
Loaded: loaded (/lib/systemd/system/foo.service; static; vendor preset: enabled)
Active: active (running) since Tue 2020-06-23 04:47:37 BST; 11s ago
Main PID: 30808 (foo)
Tasks: 1 (limit: 2077)
Memory: 68.0K
CGroup: /system.slice/foo.service
└─30808 /usr/local/bin/foo
Jun 23 04:47:37 pi2 systemd[1]: Started A Example Systemd Service.
# ps 30808
PID TTY STAT TIME COMMAND
30808 ? Ss 0:00 /usr/local/bin/foo
# systemctl stop foo
# ps 30808
PID TTY STAT TIME COMMAND
#Here we list the service file. Note that for .service files in systemd we don’t have to always type the extension. Thus, both systemctl start foo.service and systemctl start foo are the same. This doesn’t apply to other types of units like .socket.
In the command set above we:
- List the service with
systemctl - Show where the service file is located in the filesystem
- Start the service
- Get status on the service
- See the running process that the service started
- Stop the service
- Observe the process is no longer running
What we DID NOT do?
Notice that we just created a New-Style Daemon. This isn’t SysV compatible, but that is probably not a limitation for most use cases. We didn’t have to programatically create a daemon. The procedure to daemonize is handled by systemd. That’s a lot of hard work and code we don’t need to worry about.
Using the Autotools Project
In the sections below I’ll go through creation of an autotools project that contains systemd unit files and will install them. Before I get into the autoconf, and automake files let’s see how the project can be used. It’s distributed as a tarball. Please go ahead and download it to follow along.
|
|
The magic happens on line 6 where we install. This will install foo to /usr/local/bin and it will also put our foo.service in the recommended location of /lib/systemd/system.
Download the full dist foo-1.0.tar.gz.
What makes this easy is in future posts we’ll modify the main.c file. We can easily recompile with a make. We can also install our program easily with a sudo make install. This also is the same for the systemd unit files. There is just so much out of the box from autotools that will make our lives easier.
Uninstalling
We can remove everything we’ve done now easily by doing the following. Note, you must be in the directory where it was installed.
# sudo make uninstallThis uninstall target will remove /usr/local/bin/foo and /lib/systemd/system/foo.service. After uninstalled, the files will remain in the directory where the source is built. These files can just be removed with rm command and your system will be fully restored.
The Autotools Source
Here is a quick directory structure of our project:
# find .
.
./configure.ac
./systemd
./systemd/foo.service
./systemd/Makefile.am
./README
./Makefile.am
./.gitignore
./autogen.sh
./src
./src/Makefile.am
./src/main.c
#The daemon(7) man page outlines how to add systemd to an autotools project. These same instructions are followed here. With exception to a slight bug and some reading in between the lines.
Top Level Makefile.am
The top level we simply add the subdirs for our C source and the systemd unit files.
# Makefile.am
AM_DISTCHECK_CONFIGURE_FLAGS = \
--with-systemdsystemunitdir=$$dc_install_base/$(systemdsystemunitdir)
SUBDIRS = src systemd
dist_doc_DATA = README
Autoconf configure.ac
A basic configure.ac file with snippets added and modified them from the daemon(7) man page. We added our systemd directory where our foo.service file lives on line 11.
|
|
Automake src/Makefile.am
As easy as it can be. We simply have one binary with a single source file. Our built program is called foo.
# src/Makefile.am
bin_PROGRAMS = foo
foo_SOURCES = main.c
Automake systemd/Makefile.am
Here we create a systemd/ directory and add a Makefile.am. Notice we prepended dist_. Without this a make distcheck will fail since foo.service won’t be included in the distribution. This diverges from the daemon(7) man page as of this writing.
# systemd/Makefile.am
if HAVE_SYSTEMD
dist_systemdsystemunit_DATA = foo.service
endif
Developing on the Autotools project
To develop the autotools project once modifications are made I generally do the following:
# autoreconf --install # will create your configure script
# ./configure
# make
# make distcheck
# sudo make install