The program summond.c
in /bin/source
summons a daemon process and terminates so that the shell may continue to print the next prompt. This is unlike other programs where the shell waits for it to finish before printing the next prompt.
Daemons are processes that are often started when the system is bootstrapped and terminate only when the system is shut down. They don’t have a controlling terminal and they run in the background. In other words, a Daemon is a computer program that runs as a background process, rather than being under the direct control of an interactive user. UNIX systems have numerous daemons that perform day-to-day activities.
Traditionally, the process names of a daemon end with the letter d
, for clarification that the process is in fact a daemon, and for differentiation between a daemon and a normal computer program. For example: /sbin/launchd
, /usr/sbin/syslogd
, /usr/libexec/configd
, etc (may vary from machine to machine). You can launch the command ps -ef
to identify these processes whose names end with a suffix ‘d’:
For the sake of our lab and our machine’s health, our daemon terminates after a certain period of time and violates the traditional daemon definition, but we’re sure you get the idea.
Overgoogling
The basic information about daemons presented in this handout is sufficient. Do not over-Google about Daemons unless you are really interested in it. The concept of daemons alone is very complex and large, and is out of our scope.
Characteristics of a Daemon Process
A daemon process is still a normal process, running in user mode with certain characteristics which distinguish it from a normal process.
The characteristics of a daemon process are listed below.
No controlling terminal
By definition, a daemon process does not require direct user interaction and therefore must detach itself from any controlling terminal.
In the ps -ef
output, if the TTY
column is listed as a ?
meaning it does not have a controlling terminal.
PPID is 1
The PPID of a daemon process is 1, meaning that whoever was creating the daemon process must terminate to let the daemon process be adopted by the init
process.
Working directory: root
The working directory of the daemon process is typically the root
(/) directory.
Closes all uneeded fd
It closes all unneeded file descriptors. In Unix and related computer operating systems, a file descriptor (FD, less frequently fildes) is an abstract indicator (handle) used to access a file or other input/output resource, such as a pipe or network socket.
Also, it closes and redirect fd 0, 1, and 2 to /dev/null
.
Logging
It logs messages through a central logging facilities: the BSD syslog
.
Task 6 (2%)
TASK 6:
Summon a daemon process in summond.c
.
Complete the following function in summond.c
:
/*This function summons a daemon process out of the current process*/
static int create_daemon()
{
/* TASK 6 */
// Incantation on creating a daemon with fork() twice
// 1. Fork() from the parent process
// 2. Close parent with exit(1)
// 3. On child process (this is intermediate process), call setsid() so that the child becomes session leader to lose the controlling TTY
// 4. Ignore SIGCHLD, SIGHUP
// 5. Fork() again, parent (the intermediate) process terminates
// 6. Child process (the daemon) set new file permissions using umask(0). Daemon's PPID at this point is 1 (the init)
// 7. Change working directory to root
// 8. Close all open file descriptors using sysconf(_SC_OPEN_MAX) and redirect fd 0,1,2 to /dev/null
// 9. Return to main
// DO NOT PRINT ANYTHING TO THE OUTPUT
/***** BEGIN ANSWER HERE *****/
/*********************/
return 0;
}
Implementation notes
The steps written as a pseudocode above is a guide on how to create a daemon process that possesses the characteristics written previously. The sections below explains why each steps are crucial.
Step 1: first fork
The fork() splits this process into two: the parent (process group leader) and the child process (that we will call intermediate process for the next few sections).
The reason for this fork()
is so that the parent returns immediately and our shell does not wait for the daemon to exit (because usually, daemons are background processes that do not exit until the system is shut down. We don’t want our shell to hang forever).
Step 2: exit
At this point, the shell will return while the intermediate process proceed to spawn the daemon.
Step 3: setsid
The child (intermediate process) process is by default not a process group leader. It calls setsid()
to be the session leader and loses controlling TTY (terminal).
setsid()
is only effective when called by a process that is not a process group leader. The fork()
in step 1 ensures this. The system call setsid()
is used to create a new session containing a single (new) process group, with the current process as both the session leader and the process group leader of that single process group. You can read more about setsid() here.
Group and Session leader
Compile and try this code:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(){
pid_t pid = fork();
if (pid == 0){
printf("Child process with pid %d, pgid %d, session id: %d\n", getpid(), getpgid(getpid()), getsid(getpid()));
setsid(); // child tries setsid
printf("Child process has setsid with pid %d, pgid %d, session id: %d\n", getpid(), getpgid(getpid()), getsid(getpid()));
}
else{
printf("Parent process with pid %d, pgid %d, session id :%d\n", getpid(), getpgid(getpid()), getsid(getpid()));
setsid(); // parent tries setsid
printf("Parent process has setsid with pid %d, pgid %d, session id: %d\n", getpid(), getpgid(getpid()), getsid(getpid()));
wait(NULL);
}
return 0;
}
It results in such output:
Let’s analyse them line by line.
The first line: The parent process has pid == pgid, that is 75614
.
- This tells us that this process ‘iddemo’ is the process group leader, but not a session leader since the session id
27063
is not equal to the pid75614
.
The third line: When process 75614
forks, it has a child process with pid 75615
. It is clear that since child pid != pgid
, then the child process is not a session leader and is not a group leader either.
So who is 27063? We can type the command ps -a -j and find a process with pid 27063. Apparently, it’s the zsh
, the shell itself, connected to the controlling terminal s002
.
The third and fourth lines: When both the child and parent process attempt to call setsid
,
- In the child process,
setsid
effectively makes thepgid
and sessionid
to be equal to its pid,75615
. - In the parent process, setsid has no effect on the session id, since the manual states that setsid only sets the process to be the session and process group leader if it is called by a process that is not a process group leader.
Thanks to Step 1, the effect of setsid
in Step 3 works as intended, and our intermediate process now lose the controlling terminal (part of a requirement to be a daemon process).
Step 4: Ignore SIGCHLD and SIGHUP
This intermediate process is going to fork()
one more time in Step 5 to create the daemon process.
By ignoring SIGCHLD
, our daemon process – the child of this intermediate process will NOT be a zombie process when it terminates. Normally, a child process will be a zombie process if the parent does not wait
for it.
- Since
SIGCHLD
is ignored, when the daemon (child of this intermediate process) exits, it is reaped immediately. - However, the daemon will outlive the parent process anyway, so it does not really matter. This step is just for “in case”.
Also, this intermediate process is a session leader (from step 3, since we need to lose the controlling terminal).
- If we terminate a session leader, a
SIGHUP
signal will be received and the children of the session leader will be killed. - We do not want our daemon (child of this process) to be killed, therefore we need to call
signal(SIGHUP, SIG_IGN)
first before forking in Step 5.
Step 5: second fork
The child of this intermediate process is the daemon process.
The second fork, is useful for allowing the parent process (intermediate process) to terminate. This ensures that the child process is not a session leader.
Why must you ensure that the daemon is not a session leader? Since a daemon has no controlling terminal, if a daemon is a session leader, an act of opening a terminal device will make that device the controlling terminal.
We do not want this to happen with your daemon, so this second fork()
handles this issue. As mentioned above, before forking it is necessary to ignore SIGHUP
. This prevents the child from being killed when the parent (which is the session leader) dies.
Step 6: umask(0)
The daemon process must set all new files created by it to have 0777
permission using umask(0).
Setting the umask
to 0
means that newly created files or directories created will have all permissions set, so any file created by this daemon can be accessed by any other processes because we can’t directly control the daemon anymore.
A umask of zero will cause all files to be created as permission 0777
or world-RW & executable. Some system sets the permission as 0666
by default instead of 0777
for security reasons, and we don’t want this!
How does setting umask(0)
lands you with 0777
file permission?
0777
actually stands for octal 777
. In C, the first 0 indicates octal notation, and you can translate the rest in binary: 111 111 111
, which means we will have - rwx rwx rwx
, equivalent to global RW and executable for the file. If we want to restrict permission of write to only the owner, we can set umask(022)
– equivalent to having permission 0755
, with the binary: 111 101 101, which translates to - rwx r-x r-x
.
The manual for umask can be found here.
Step 7: chdir to root directory
Change the current working directory to root using chdir("/")
. If a daemon were to leave its current working directory unchanged then this would prevent the filesystem containing that directory from being unmounted while the daemon was running. It is therefore good practice for daemons to change their working directory to a safe location that will never be umounted, like root.
Step 8: handle fd 0, 1, 2 and close all unused fds
Close all open file descriptors and redirect stdin
, stdout
, and stderr
(fd 0, 1, and 2 by default in UNIX systems) to /dev/null
so that it won’t reacquire them again if you mistakenly attempt to output to stdout
or read from stdin
.
Once it is running a daemon should NOT read from or write to the terminal from which it was launched. The simplest and most effective way to ensure this is to close the file descriptors corresponding to stdin
, stdout
and stderr
. These should then be reopened, either to /dev/null
, or if preferred to some other location.
There are two reasons for not leaving them closed:
- To prevent code that refers to these file descriptors from failing
- To prevent the descriptors from being reused when we call open() from the daemon’s code.
To close all opened file descriptors, you need to loop through existing file descriptors, and re-attach the first 3 fd’s using dup(0)
. Note that open()
and dup()
will assign the smallest available file descriptor, in this case that is 0, 1, and 2 in sequence. You will learn more about these stuffs in the last OS chapter.
/* Close all open file descriptors */
int x;
for (x = sysconf(_SC_OPEN_MAX); x>=0; x--)
{
close (x);
}
/*
* Attach file descriptors 0, 1, and 2 to /dev/null. */
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
Step 9
Finally, return to the main
function, of which the next function daemon_work()
is called. Our simple daemon does nothing but write some fake logs to logfile.txt
inside /pa1
.
Syslog Facility
In practice, there exist daemon error-logging facilities. The BSD syslog facility has been widely used since 4.2BSD
. You can print these logs using syslog()
. It has already been given to you in the main
function:
/* Open the log file */
openlog("summond", LOG_PID, LOG_DAEMON);
syslog(LOG_NOTICE, "Daemon started.");
closelog();
Depending on your machine, your syslog facility may vary. In macOS, the syslog facility is the Console.app
. You can simply search the message or the process name that is summond. In Ubuntu, you can read /var/log/syslog
and search the message containing summond
with grep
:
In practice, once you direct the log message here, you can tell the facility on how to forward it to your own logfile. This process is rather specific and out of our scope, therefore for the sake of the lab, we just assume that our daemon can directly write to our own logfile: /pa1/logfile.txt
:
Test Task 6
Simply recompile with make
.
Run your shell and type summond
command. Then, check if logfile.txt
has been created and that the daemon is printing to it periodically. Afterwards, get the pid
of that summond
process using the command ps -efj | grep summond
. This outputs the pid of summond
.
Then, execute lsof -p [summond_pid]
to see that it’s first 3 file descriptors (0u, 1u, 2u) are attached to /dev/null
.
Notice also that ppid
of summond
is 1 (the third column) of the output of ps -efj | grep summond
.
summond
is neither a session leader nor a group leader since its PGID (15160) != PID (15161)
.
Commit Task 6
Save your changes and commit the changes:
git add ./bin/source/summond.c
git commit -m "feat: Complete Task 6"
Task 7 (1%)
TASK 7:
Implement system program checkdaemon
.
Open check_daemon.c
, and complete the following execute
function:
/* A program that prints how many summoned daemons are currently alive */
int execute()
{
// Create a command that trawl through output of ps -efj and contains "summond"
char *command = malloc(sizeof(char) * 256);
sprintf(command, "ps -efj | grep summond | grep -Ev 'tty|pts' > output.txt");
int result = system(command);
if (result == -1)
{
printf("Command %s fail to execute. Exiting now. \n", command);
return 1;
}
free(command);
int live_daemons = 0;
FILE *fptr;
/* TASK 7 */
// 1. Open the file output.txt
// 2. Fetch line by line using getline()
// 3. Increase the daemon count whenever we encounter a line
// 4. Store the count inside live_daemons
// DO NOT PRINT ANYTHING TO THE OUTPUT
/***** BEGIN ANSWER HERE *****/
/*********************/
if (live_daemons == 0)
printf("No daemon is alive right now.\n");
else
{
printf("Live daemons: %d\n", live_daemons);
}
fclose(fptr);
return 1;
}
This is nothing more than calling the ps
command, and counting the number of lines in the produced file output.txt
. Since you run checkdaemon
via cseshell
with working directory /pa1/
, then the file output.txt
is created inside /pa1/
directory.
You can reuse your system program countline
here or write one from scratch.
Test Task 7
Simply recompile with make
, and run your shell after summoning a few daemons. The command checkdaemon
should print out exactly as follows:
Commit Task 7
Save your changes and commit the changes:
git add ./bin/source/check_daemon.c
git commit -m "feat: Complete Task 7"
Push everything to remote repo
Before the due date, ensure that you always push to your remote repository. Assuming you are using the master
branch:
git push -u origin master
Debugging
Note that all the steps provided to you above are necessary but not sufficient, meaning that if you were to blindly follow it step by step, bugs are still bound to happen, although minor. For instance, the shell will run but you might find garbage values printed out when you simply press a bunch of return
keys in the shell, usage
not executed properly, and many other small bugs that does not make the shell perfect. You are supposed to figure these out by yourself, that’s why this assignment is graded (10%) and it is set as a pair work.
Submission
Go to our bot and type /start
. Follow the instructions there. You’re only required to submit the github remote repo link. Ensure that you have invited natalieagus-sutd
as collaborator.
We will clone your repo for grading. Make sure you do not print any other messages other than the starter code, because we are running your shell with an autograder to check for compilability and plagiarism checker, before grading them manually.
Summary
If you have reached this stage, congratulations for completing programming assignment 1!.
We hope that you have appreciated some valuable things regarding:
- Purposes of
fork()
in shell programs - Incantation steps of creating daemon processes, and the reason why these steps must be done
- A few of C standard functions:
system
,getline
,malloc
,free
,signal
,chdir
, among many others and amp up your experience in reading documentations - Compiling using
make
and using the CLI - Basic knowledge about file descriptors, and file permissions
Remember, DO NOT add any more print statements other than those in the starter code.