Autumn 2021
Jerry Cain
PDF
This slide was written by Ryan Eberhardt and edited by Jerry.
Signal handlers are difficult to use properly, and the consequences can be severe. Some regard signals to be one of the worst parts of Unix’s design.
This installment of Ghosts of Unix Past explains why asynchronous signal handling can be such a headache.
The article's primary point: The trouble with signal handlers is that they can be invoked at a really, really bad time (e.g. while the main execution flow is in the middle of a malloc call, or accessing a complex data structure).
This slide was written by Ryan Eberhardt and edited by Jerry.
static vector<string> strings;
void handleSIGINT(int sig) {
stringsToProcess.clear();
}
int main(int argc, char *argv[]) {
buildStringVector(strings); // assume anything reasonable
signal(SIGINT, handleSIGINT);
for (string &s: strings) processString(s);
return 0;
}
The code looks harmless enough, but the deadlock and segfault scenarios are more immediately apparent if you understand that, by default, printf:
first adds the thing you're printing to a private buffer within the struct FILE addressed by stdout
flushes the buffer's content to STDOUT_FILENO when and only when a newline character is among the content.
This slide was written by Ryan Eberhardt and edited by Jerry.
void handleSIGINT(int sig) {
printf("Got SIGINT!\n");
}
int main(int argc, char **argv[]) {
signal(SIGINT, handleSIGINT);
while (true) {
printf("Sleeping...\n");
sleep(1);
}
}
This slide was written by Ryan Eberhardt and edited by Jerry.
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGINT);
sigaddset(&monitoredSignals, SIGTSTP);
This slide was written by Ryan Eberhardt and edited by Jerry.
sigprocmask(SIG_BLOCK, &monitoredSignals, NULL);
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGINT);
sigaddset(&monitoredSignals, SIGTSTP);
This slide was written by Ryan Eberhardt and edited by Jerry.
int delivered;
sigwait(&monitoredSet, &delivered);
cout << "Received signal: " << delivered << endl;
This slide was written by Ryan Eberhardt and edited by Jerry.
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGINT);
sigaddset(&monitoredSignals, SIGTSTP);
sigprocmask(SIG_BLOCK, &monitoredSignals, NULL);
sigprocmask(SIG_BLOCK, &monitoredSignals, NULL);
This slide was written by Ryan Eberhardt and edited by Jerry.
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGINT);
sigaddset(&monitoredSignals, SIGTSTP);
int delivered;
sigwait(&monitoredSet, &delivered);
cout << "Received signal: " << delivered << endl;
static const size_t kNumChildren = 5;
static void constructMonitoredSet(sigset_t& monitored, const vector<int>& signals) {
sigemptyset(&monitored);
for (int signal: signals) sigaddset(&monitored, signal);
}
static void blockMonitoredSet(const sigset_t& monitored) {
sigprocmask(SIG_BLOCK, &monitored, NULL);
}
static void unblockMonitoredSet(const sigset_t& monitored) {
sigprocmask(SIG_UNBLOCK, &monitored, NULL);
}
int main(int argc, char *argv[]) {
cout << "Let my five children play while I take a nap." << endl;
sigset_t monitored;
constructMonitoredSet(monitored, {SIGCHLD, SIGALRM});
blockMonitoredSet(monitored);
for (size_t kid = 1; kid <= kNumChildren; kid++) {
pid_t pid = fork();
if (pid == 0) {
unblockMonitoredSet(monitored); // lift block on signals, child may rely on them
sleep(3 * kid); // sleep emulates "play" time
cout << "Child " << kid << " tires... returns to dad." << endl;
return 0;
}
}
// to be continued on next slide
size_t numDone = 0;
bool dadSeesEveryone = false;
letDadSleep();
while (!dadSeesEveryone) {
int delivered;
sigwait(&monitored, &delivered);
switch (delivered) {
case SIGCHLD:
numDone = reapChildProcesses(numDone);
break;
case SIGALRM:
wakeUpDad(numDone);
dadSeesEveryone = numDone == kNumChildren;
break;
}
}
cout << "All children accounted for. Good job, dad!" << endl;
return 0;
}
static size_t reapChildProcesses(size_t numDone) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
numDone++;
}
return numDone;
}
static void setAlarm(double duration) { // fire SIGALRM 'duration' seconds from now
int seconds = int(duration);
int microseconds = 1000000 * (duration - seconds);
struct itimerval next = {{0, 0}, {seconds, microseconds}};
setitimer(ITIMER_REAL, &next, NULL);
}
static const double kSleepTime = 5.0;
static void letDadSleep() {
cout << "At least one child still playing, so dad nods off." << endl;
setAlarm(kSleepTime);
}
static void wakeUpDad(size_t numDone) {
cout << "Dad wakes up and sees " << numDone
<< " " << (numDone == 1 ? "child" : "children") << "." << endl;
if (numDone < kNumChildren) letDadSleep();
}
literal snooze button
To play the same pitch one octave higher for 0.75 seconds, you’d invoke:
Feeling jazzy? Here's the Bb7(#11) chord big bands blast at the end of many standards.
The last token of "play -qn synth 1.5 pluck C4" specifies the pitch and octave and will always be some note drawn from the traditional Western music scale—e.g. C2, D2, E2, F2, G2, A2, B2, C3, although # and b can be appended to alter the pitch half a tone, as with C# or Bb.
Some notes last longer than others. The exact duration is dictated by the number that sits in between "synth" and "pluck".
myth61:$ play -qn synth 1.5 pluck C4
myth61:$ play -qn synth 0.75 pluck C5
myth61:$ play -qn synth 3.00 pluck Bb2 & \
> play -qn synth 3.00 pluck Ab3 & \
> play -qn synth 3.00 pluck D4 & \
> play -qn synth 3.10 pluck E4 & \
> play -qn synth 3.10 pluck G4 & \
> play -qn synth 3.10 pluck C5 &
struct note {
string pitch; // "A4", "Bb2" or some other pitch
double start; // the time from launch when note should play
double duration; // the time the note should last once played
}; // all times are in seconds
The first note always has an effective start time of 0.0.
Neighboring notes may have the same start time if they’re all
intended to be played together (even if their durations vary).
The fact that the second set of notes starts at t = 0.5 seconds means we'd call setAlarm(0.5) after spawning off a single child process for that first C4.
When the SIGALRM signal is fired, we know the time has come to spawn off two more child processes—one to play a D4, and a second to play a B3—before calling setAlarm(0.5) again to schedule some E4/C4/Bb3 chord.
In programming terms, the function we implement to synchronously handle any SIGALRMs will fork off new play processes and set additional timers.
guitar.txt
C4 0.0 0.5
D4 0.5 0.5
B3 0.5 0.5
E4 1.0 0.5
C4 1.0 0.5
Bb3 1.0 0.5
G4 3.0 2.5
// many more notes
static void playSong(const vector<note>& song) {
size_t pos = 0;
set<pid_t> processes;
sigset_t monitored;
constructMonitoredSet(monitored, {SIGINT, SIGALRM, SIGCHLD}); // same as for Disneyland
blockMonitoredSet(monitored); // same as for Disneyland
raise(SIGALRM); // start the metronome at t = 0.0
while (pos < song.size() || !processes.empty()) {
int delivered;
sigwait(&monitored, &delivered);
switch (delivered) {
case SIGINT:
stopPlaying(processes);
break;
case SIGALRM:
pos = playNextNotes(song, pos, monitored, processes);
break;
case SIGCHLD:
reapChildProcesses(processes);
break;
}
}
}
int main(int argc, char *argv[]) {
if (argc > 2) usage();
vector<note> song;
initializeSong(song, argv[1]); // we omit the implementation of this
playSong(song);
return 0;
}
static size_t playNextNotes(const vector<note>& song, size_t pos,
const sigset_t& monitored, set<pid_t>& processes) {
double current = song[pos].start;
while (pos < song.size() && song[pos].start == current) {
pid_t pid = fork();
if (pid == 0) {
unblockMonitoredSet(monitored); // same as for Disneyland
string duration = to_string(song[pos].duration);
const char *argv[] = {
"play", "-qn", "synth", duration.c_str(), "pl", song[pos].pitch.c_str(), NULL
};
execvp(argv[0], (char **) argv); // assume succeeds
}
pos++;
processes.insert(pid);
}
if (pos < song.size()) setAlarm(song[pos].start - current); // same as for Disneyland
return pos;
}
static void stopPlaying(const set<pid_t>& processes) {
for (pid_t pid: processes) kill(pid, SIGKILL); // kill child processes
exit(0); // kill the primary
}
static void reapChildProcesses(set<pid_t>& processes) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
processes.erase(pid);
}
}