1. Overview
Although a process is a fundamental concept in operating systems, sometimes there might be confusion about different types of processes, such as the difference between daemons and services.
In this tutorial, we’ll focus on the differences between processes, daemons, and services in Linux.
2. Processes
A process is an active program that is being executed by the operating system. In addition to the code of the program, a process has dedicated memory and resources. A program, on the other hand, is a collection of instructions written in a specific programming language to perform a specific task. We can envisage programs as static entities while processes as the dynamic counterparts of programs.
Every process in the system has a unique identifier, PID (Process ID), which is a number. Additionally, each process is in one of several states. For example, a process might be in the Running state while another might be in the Interruptible Sleep state, waiting for resources.
Processes also have scheduling policies and priorities. For example, a process having a real-time scheduling policy has a higher priority than processes with other scheduling policies. Indeed, threads in a multi-threaded process might have different scheduling policies and priorities.
We may also categorize processes as either foreground or background processes. A process connected to a terminal is a foreground process, while a background process is detached from the terminal.
In the subsequent sections, we’ll see that daemons and services are special processes running in the background.
3. Daemons
In this section, we’ll discuss the characteristics of daemons and the typical steps for initializing a daemon. We’ll also present examples and an analysis of the daemon processes.
3.1. Daemon Characteristics
Daemons are special processes that are started when the system is started and stopped when the system is shut down. They run in the background and are detached from a controlling terminal.
Some of the daemons in the system run in kernel mode, while others run in user mode. Each daemon process performs a specific job. For example, the ksoftirqd kernel daemon handles deferred software interrupts, while the chronyd daemon running in user mode synchronizes time between different nodes in a network.
3.2. Typical Steps at Startup
There are several recommended steps for the initialization of a daemon to avoid undesirable interactions. Let’s go over the basic steps for implementing a daemon in C:
- Clear the file creation mask by calling the umask() system call. This is set to a known value, generally 0. Otherwise, the inherited file mode creation mask from the parent process may override certain permissions.
- Call the fork() function and make the parent process terminate. If we run the daemon program from a terminal, this makes the daemon process run in the background. Additionally, the child inherits the PGID (Process Group ID) from the parent but has a new PID. Therefore, the child isn’t a process group leader.
- Create a new session using the setsid() system call. Since the child process isn’t a process group leader, the creation of a new session is successful. As a result of calling setsid(), the process becomes the leader of a new session and a new process group. Additionally, it’s detached from the terminal.
- Change the current working directory to the root directory. The daemon process inherits the current working directory from the parent process. This directory might be a mounted file system. Therefore, changing the daemon process to the root directory allows the unmounting of the file system. However, a daemon might prefer to use another current working directory — such as a printer spooling daemon working in its spooling directory.
- Close unnecessary file descriptors inherited from the parent process.
- Attach the file descriptors 0, 1, and 2 to /dev/null. Therefore, library functions used by the daemon that read from the standard input and write to the standard output and the standard error aren’t affected.
3.3. Implementing a Daemon
Let’s write an example daemon using the following program, first_daemon.c, that implements the steps listed in the previous section:
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
void convert_to_daemon()
{
pid_t pid;
int fd0,fd1,fd2;
/* Clear file creation mask */
umask(0);
/* Create child daemon process and let parent exit */
pid = fork();
if (pid != 0) /* parent */
exit(0);
/* Become a session leader */
setsid();
/* Change the current working directory */
chdir("/");
/* Close open file descriptors */
close(0);
close(1);
close(2);
/* Attach stdin, stdout, and stderr to /dev/null */
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
}
int main() {
convert_to_daemon();
while (1) {
/* The daemon performs its task here */
sleep(1);
}
return 0;
}
The convert_to_daemon() function implements the steps at startup. After calling it in main(), the daemon performs its task in an infinite loop.
There might be other steps in the startup of a daemon. For example, we might write the PID of the daemon to a PID file to have a single instance of the daemon running. We haven’t implemented such steps in our example to keep the daemon code short.
3.4. Alternative Implementation
As an alternative to the implementation in first_daemon.c, we can write another program, second_daemon.c:
#include <unistd.h>
int main() {
daemon(0, 0);
/* The daemon performs its task here */
while (1) {
sleep(1);
}
return 0;
}
This code is much simpler than the one in first_daemon.c. In this case, we only call the daemon() function before the infinite while loop in which the daemon performs its task.
The daemon() function is for detaching from the controlling terminal and running in the background as a daemon. It implements the steps recommended for initializing a daemon. If its first argument is 0, it changes the current working directory of the process to the root directory. Moreover, if the second argument is 0, it redirects the standard input, the standard output, and the standard error to /dev/null.
3.5. Running the Daemons
First, let’s build first_daemon.c and second_daemon.c using gcc:
$ gcc -o first_daemon first_daemon.c
$ gcc -o second_daemon second_daemon.c
The names of the generated executables are first_daemon and second_daemon.
Having generated the executables, it’s time to run the daemons:
$ ./first_daemon
$ ./second_daemon
Let’s check the running processes using ps. We filter the output of ps through grep to list only the two daemon processes:
$ ps -efj | grep _daemon | grep -v grep
centos 3901 2314 3901 3901 0 15:21 ? 00:00:00 ./first_daemon
centos 3909 2314 3909 3909 0 15:21 ? 00:00:00 ./second_daemon
Both processes are detached from the terminal as expected, so we can continue using the same terminal after starting the daemons.
3.6. Analyzing the Daemon Processes
The second column in the output of ps displays the PIDs of the processes, 3901 and 3909 in this example. The fourth and fifth columns list the PGIDs and SIDs (Session IDs). The PGIDs and SIDs of both processes are the same as their PIDs. Therefore, they’re both process group leaders and session leaders, as expected.
The third column displays the PIDs of the parent processes. Both processes have the same parent process whose PID is 2314. This parent process is the per-user instance of systemd in our case. The system-wide systemd service, whose PID is 1, starts it when the user logs on and stops it when the user logs off.
The eighth column lists the terminals the processes are attached to. The question marks in this column mean that the daemon processes are detached from the controlling terminals, as expected.
Let’s check the current working directories of the two processes using pwdx:
$ pwdx 3901
3901: /
$ pwdx 3909
3909: /
The current working directory of both processes is the root directory (/) as expected.
Finally, let’s check the open file descriptors of the two processes from the /proc file system:
$ sudo ls -l /proc/3901/fd
total 0
lrwx------. 1 centos centos 64 Feb 9 11:28 0 -> /dev/null
lrwx------. 1 centos centos 64 Feb 9 11:28 1 -> /dev/null
lrwx------. 1 centos centos 64 Feb 9 11:28 2 -> /dev/null
$ sudo ls -l /proc/3909/fd
total 0
lrwx------. 1 centos centos 64 Feb 9 11:29 0 -> /dev/null
lrwx------. 1 centos centos 64 Feb 9 11:29 1 -> /dev/null
lrwx------. 1 centos centos 64 Feb 9 11:29 2 -> /dev/null
As is apparent from the output, both daemon processes have only the file descriptors 0, 1, and 2 open. They point to /dev/null, as expected.
Therefore, the daemon() function in second_daemon.c implements the steps in first_daemon.c.
4. Services
The daemons discussed in the previous section are traditional SysV daemons. However, it’s possible to implement daemons using the infrastructure provided by systemd. The daemons implemented using systemd are also known as new-style daemons. Indeed, systemd names these daemons as services.
systemd services don’t need the initialization steps of SysV daemons. Therefore, this simplifies their implementation. It’s easier to supervise and control the execution of services at runtime.
4.1. Typical Steps at Startup
There are several recommended steps for the implementation of a service just like the implementation of SysV daemons:
- The service should notify the service manager about the start of the service using sd-notify(). The notification message should be READY=1.
- If the service receives a SIGTERM signal, it must exit gracefully.
- The service should notify the service manager with the notification message STOPPING=1.
- We need to provide a unit file to integrate the service with systemd.
These are the steps we’ll implement in the next section. However, there might be other steps depending on the requirements. For example, if we need to integrate the daemon with D-Bus, then we’ll need to define and expose the daemon’s control interface using the D-Bus IPC system.
4.2. Implementing a Service
Let’s write the C program systemd_daemon.c as an example of a service:
#include <unistd.h>
#include <signal.h>
#include <systemd/sd-daemon.h>
int signal_captured = 0;
void signal_handler(int signum, siginfo_t *info, void *extra)
{
signal_captured = 1;
}
void set_signal_handler(void)
{
struct sigaction action;
action.sa_flags = SA_SIGINFO;
action.sa_sigaction = signal_handler;
sigaction(SIGTERM, &action, NULL);
}
int main()
{
set_signal_handler();
sd_notify(0, "READY=1\nSTATUS=Running");
while(!signal_captured) {
/* The daemon performs its task here */
sleep(1);
}
sd_notify(0, "STOPPING=1");
return 0;
}
The program implements the steps in the previous section.
The set_signal_handler() function sets the signal_handler() function as the signal handler for the SIGTERM signal. In other words, the operating system calls the signal_handler() function when we send a SIGTERM signal to the service. This function only sets the signal_captured global variable to 1.
In addition to the notification message READY=1, we also send the status message STATUS=Running to systemd using sd_notify(). Later, we’ll check the status using systemctl status.
The initial value of signal_captured is 0. We constantly check its value in the while loop. Whenever signal_handler() sets its value to 1, we exit from the while loop. We send the notification message STOPPING=1 to systemd and exit from the program.
4.3. The Service’s Unit File
Let’s write the unit file, simple_service.service, for integrating our service with systemd:
[Unit]
Description=The minimal systemd daemon service
[Service]
Type=notify
ExecStart=/home/centos/work/daemon/systemd_daemon
[Install]
WantedBy=multi-user.target
systemd uses the unit file for controlling the service.
The Description option in the Unit section describes the purpose of the service. The systemctl status command displays the description.
The Type option in the Service section describes how systemd handles the service. Its value, notify, specifies that the service issues notification messages to systemd. We start the service using the executable specified in the ExecStart option.
The WantedBy option of the Install section specifies at which run-level systemd starts the service. multi-user.target is equivalent to SysV run-levels 2, 3, and 4.
4.4. Starting the Service
First, let’s build systemd_daemon.c using gcc:
$ gcc -o systemd_daemon systemd_daemon.c -lsystemd
The name of the executable is systemd_daemon. We need to link it together with the libsystemd.so library to use the sd_notify() function.
Now, it’s time to start the service using systemd:
$ pwd
/etc/systemd/system
$ sudo cp /home/centos/work/daemon/simple_service.service .
$ sudo systemctl enable simple_service.service
Created symlink /etc/systemd/system/multi-user.target.wants/simple_service.service → /etc/systemd/system/simple_service.service.
$ sudo systemctl start simple_service.service
We enable and start the service using systemctl enable and systemctl start commands after copying the unit file to the /etc/systemd/system directory. We use these commands together with the sudo command as we need root privileges.
Let’s use ps to check whether the service is running:
$ ps -efj | grep systemd_daemon | grep -v grep
root 3402 1 3402 3402 0 14:48 ? 00:00:00 /home/centos/work/daemon/systemd_daemon
We’re successful in starting the service. Notably, the question mark in the eighth column of the output of ps implies that the service isn’t attached to a terminal.
4.5. Analyzing the Service Process
Let’s check the status of the service using systemctl status:
$ sudo systemctl status simple_service
● simple_service.service - The minimal systemd daemon service
Loaded: loaded (/etc/systemd/system/simple_service.service; enabled; vendor preset: disabled)
Active: active (running) since Mon 2024-02-12 15:46:14 +03; 9s ago
Main PID: 3402 (systemd_daemon)
Status: "Running"
Tasks: 1 (limit: 11270)
Memory: 468.0K
CGroup: /system.slice/simple_service.service
└─7382 /home/centos/work/daemon/systemd_daemon
Feb 12 15:46:14 host1 systemd[1]: Starting The minimal systemd daemon service...
Feb 12 15:46:14 host1 systemd[1]: Started The minimal systemd daemon service.
The service is active and running. Its status is Running, which we’ve set by sending the status message STATUS=Running to systemd using sd_notify().
The PID, PGID, and SID of the process have the same value, which is 3402. This behavior is the same as the SysV daemons in the previous section. However, the PPID (Parent Process ID) of the service is 1. The parent is the system-wide systemd daemon, which is the daemon that manages other daemons.
Let’s check the current working directory of the service:
$ sudo pwdx 3402
3402: /
The current working directory of the service is the root directory (/) as expected.
Finally, let’s check the open file descriptors of the service from the /proc file system:
$ sudo ls -l /proc/3402/fd
total 0
lr-x------. 1 root root 64 Feb 12 15:36 0 -> /dev/null
lrwx------. 1 root root 64 Feb 12 15:36 1 -> 'socket:[53876]'
lrwx------. 1 root root 64 Feb 12 15:36 2 -> 'socket:[53876]'
The service has the file descriptors 0, 1, and 2 open. The file descriptor 0 points to /dev/null as before. However, unlike SysV daemons, the file descriptors 1 and 2 point to a socket since systemd services use the logging component of systemd, namely journald.
5. Conclusion
In this article, we discussed the differences between processes, daemons, and services. We learned that daemons and services are special processes running in the background as long as the system is running. They’re detached from a controlling terminal.
We saw that systemd names traditional SysV daemons as services. We also learned how to implement traditional SysV daemons and systemd services to have a better understanding of the differences in their implementation. Additionally, we compared their runtime behaviors.