In this example, we will study the starfork
program. You can obtain the
skeleton code on CLAC:
git clone ~j-hui/cs3157-pub/examples/starfork
When you run make
in here, it will build nine different version of
starfork
, named starfork-s1
through starfork-s9
. You should try to
first predict the output of each part before running the executable to validate
(or disprove) your hypothesis. Keep in mind that some of these parts are
actually unpredictable, so you may need to run them multiple times to see
different behavior.
Note that parts 8 and 9 are optional; signals and signal handlers will not appear on exams. However, you may find working through these parts helpful for understanding signal handlers, which you will need to use for your labs.
For starters, let’s make sure you understand the skeleton code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void star(int numstar) {
if (numstar >= 100)
exit(1);
char star = '*';
if (numstar < 0) {
numstar = -numstar;
star = '@';
}
char line[100];
line[numstar] = 0;
while (0 <= --numstar)
line[numstar] = star;
printf("%s\n", line);
}
int main(int argc, char **argv)
{
assert(argc == 2);
int n = atoi(argv[1]);
for (int i = 1; i <= n; i++) {
// You can enable each code block below by defining S1, S2, etc.
// at the preprocessing stage using -D option in gcc. For example,
//
// gcc -Wall -g -D S1 starfork.c && ./a.out 3
//
// will run the program with the code in S1 block.
// BEGIN MOD BLOCK
star(i);
// END MOD BLOCK
}
}
How does this code behave? How many arguments does this program expect, and what do those arguments do?
In subsequent parts, we will modify the “mod block”, the portion of the code
between // BEGIN MOD BLOCK
and // END MOD BLOCK
.
Change the mod block to:
star(i);
fork();
Run it with different command line arguments, 1
, 2
, 3
, etc.
Is the output always the same? And if not, are there any kinds of patterns you can find among possible outputs?
Is the following output possible:
*
**
***
**
***
***
***
And what about this output:
*
***
**
***
**
***
***
Sometimes you’ll see output that looks like this:
$ ./starfork-s2 3
*
**
**
***
$ ***
***
***
Note that stars were printed even after the shell prompt $
was shown.
Why might this happen? (Hint: what is the parent process of starfork
?)
What about the following modification?
star(i);
fork();
star(i);
Try to predict the output with command line argument 1
; identify which
process prints each line.
What about with arguments 2
or 3
? Are they predictable?
Let’s add some synchronization by having the parent process wait for its child. What would be the output if you change the modification block to the following?
star(i);
pid_t pid = fork();
if (pid == 0) { // Child process
star(i);
exit(0);
}
waitpid(pid, NULL, 0); // No status, no options
fork()
and waitpid()
?Now what about this version?
star(i);
pid_t pid = fork();
if (pid > 0) { // Parent process
waitpid(pid, NULL, 0); // No status, no options
star(i);
exit(0);
}
Now let’s understand what exec()
does. How would the following block behave?
star(i);
sleep(1);
char *a[] = { argv[0], argv[1], NULL };
execv(*a, a);
printf("%s\n", "A STAR IS BORN");
exit(0);
A STAR IS BORN
ever printed?Now let’s see if you really understood exec()
. What would be the output for
the following block when you run starfork
with arguments 2
, 10
, and 50
?
star(n);
pid_t pid = fork();
if (pid == 0) { // Child process
char buf[100];
sprintf(buf, "%d", 2 * n);
char *a[] = { argv[0], buf, NULL };
execv(*a, a);
}
waitpid(pid, NULL, 0); // No status, no options
star(n);
exit(0);
Some hints and guiding questions:
star()
with n
instead of i
!starfork
executes itself, make a note of what argument it is called with.Let’s install some signal handlers to detect when stars die. We will use the
supernova()
function as our SIGCHLD
handler, and use setup_supernova()
to
install it.
void supernova(int sig) {
int wstatus;
while (waitpid(-1, &wstatus, WNOHANG) > 0)
for (int i = WEXITSTATUS(wstatus); i > 0; i--)
star(-i);
}
void setup_supernova(int sa_restart) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_handler = supernova;
if (sa_restart)
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
}
In the beginning of our main()
function, before the loop, we call
setup_supernova(1)
to install our supernova()
handler (with SA_RESTART
).
Then, in our mod block, we have the following code:
star(i);
pid_t pid = fork();
if (pid == 0) // Child process
exit(i);
char c;
// Block until we can read a byte from stdin (fd=0)
read(0, &c, sizeof(char));
read()
tells the parent process to block until it receives input from
standard input; we can advance through each loop iteration by pressing enter.
What is the output?
What happens if you type some extra characters before hitting enter? Keep in
mind that your terminal will buffer characters before sending them to the
standard input of the starfork
program.
Now, we do the same thing as part 8, but we do not use the SA_RESTART
flag by
calling setup_supernova(0)
. The mod block is the same (aside from comments):
star(i);
pid_t pid = fork();
if (pid == 0) // Child process
exit(i);
char c;
// Block until we can read a byte from stdin (fd=0)
// or we receive and handle a signal
read(0, &c, sizeof(char));
Then, SIGCHLD
will interrupt the blocking
read()
. How does the behavior compare with the part 8?