Chapter 27.7–27.9 — Implementing system() + Exercises
Signal Handling in system() · Full Implementation · Chapter Exercises · EmbeddedPathashala
📌 Topic
system() Internals
system() Internals
🧠 Level
Advanced
Advanced
💻 Examples
3 Programs
3 Programs
❓ Q&A
8 Questions
8 Questions
Why Implementing system() is Non-Trivial
A naive system() implementation (just fork + exec + waitpid) seems simple. But in real programs, two signal problems can cause serious bugs:
- SIGCHLD race: If the caller already has a SIGCHLD handler that calls wait(), it may collect the system() child’s status before system()’s own waitpid() can — leaving system() waiting forever.
- SIGINT/SIGQUIT during system(): All three processes (caller, shell, command) are in the same foreground process group. Ctrl+C kills all three — but system() should only kill the command, while the caller waits.
The correct implementation must handle both these signal issues.
Three-Process Arrangement During system(“sleep 20”)
| Process | Role | Ctrl+C should… |
|---|---|---|
| Caller process | Your program, blocked in waitpid() | Ignore SIGINT (wait for command to finish/die) |
| /bin/sh | Shell, parent of command | Shell ignores it (already does this) |
| sleep 20 | The actual command | Receive SIGINT and terminate |
Signal Handling Rules for Correct system()
| Signal | In the CALLER (parent) | In the CHILD | Why |
|---|---|---|---|
| SIGCHLD | Block it | Unblock (restore) | Prevent race with caller’s SIGCHLD handler |
| SIGINT | Ignore it (SIG_IGN) | Restore original disposition | Ctrl+C kills command but not caller |
| SIGQUIT | Ignore it (SIG_IGN) | Restore original disposition | Ctrl+\ kills command but not caller |
Example 1: Simple system() — Without Signal Handling (Incomplete)
/* example1_simple_system.c
* gcc -o ex1 example1_simple_system.c && ./ex1
*
* Simple but INCOMPLETE implementation of system().
* Missing: signal handling for SIGCHLD, SIGINT, SIGQUIT
* Good to understand the base, but don't use in production.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <errno.h>
int simple_system(const char *command)
{
int status;
pid_t child_pid;
if (command == NULL)
return 1; /* Assume shell exists */
child_pid = fork();
switch (child_pid) {
case -1:
/* fork failed */
return -1;
case 0:
/* Child: run command via shell */
execl("/bin/sh", "sh", "-c", command, (char *)NULL);
/* If execl fails: */
_exit(127);
default:
/* Parent: wait for the specific child we created */
/* Use waitpid not wait() — wait() could collect wrong child */
if (waitpid(child_pid, &status, 0) == -1)
return -1;
return status;
}
}
int main(void)
{
int ret;
printf("Testing simple_system():\n\n");
ret = simple_system("echo Hello from simple_system!");
if (ret == -1)
perror("simple_system");
else if (WIFEXITED(ret))
printf("Exit code: %d\n\n", WEXITSTATUS(ret));
ret = simple_system("ls /tmp | wc -l");
if (WIFEXITED(ret))
printf("Pipe command exit code: %d\n\n", WEXITSTATUS(ret));
/* Demonstrate that shell features work */
ret = simple_system("for i in 1 2 3; do echo \"item $i\"; done");
printf("Shell loop exit: %d\n", WEXITSTATUS(ret));
return 0;
}
Example 2: Full system() with Correct Signal Handling
/* example2_full_system.c
* gcc -o ex2 example2_full_system.c && ./ex2
*
* COMPLETE, CORRECT implementation of system() per SUSv3.
* Handles: SIGCHLD blocking, SIGINT/SIGQUIT ignoring in parent,
* EINTR restart for waitpid, _exit(127) on exec fail.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <errno.h>
int full_system(const char *command)
{
sigset_t block_mask, orig_mask;
struct sigaction sa_ignore, sa_orig_quit, sa_orig_int, sa_default;
pid_t child_pid;
int status, saved_errno;
/* Check if shell is available */
if (command == NULL)
return full_system(":") == 0; /* ":" is shell no-op */
/* --- Step 1: Block SIGCHLD, ignore SIGINT and SIGQUIT ---
* Must do BEFORE fork() to avoid race conditions.
* If done after fork: child might exit before parent sets up. */
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &block_mask, &orig_mask);
sa_ignore.sa_handler = SIG_IGN;
sa_ignore.sa_flags = 0;
sigemptyset(&sa_ignore.sa_mask);
sigaction(SIGINT, &sa_ignore, &sa_orig_int);
sigaction(SIGQUIT, &sa_ignore, &sa_orig_quit);
/* --- Step 2: fork() --- */
switch (child_pid = fork()) {
case -1:
/* fork failed */
status = -1;
break;
case 0:
/* --- CHILD PROCESS ---
* Undo parent's signal changes: restore SIGINT/SIGQUIT
* and unblock SIGCHLD. exec() will reset handlers anyway,
* but we need to handle the window between fork and exec. */
sa_default.sa_handler = SIG_DFL;
sa_default.sa_flags = 0;
sigemptyset(&sa_default.sa_mask);
/* Only restore if caller wasn't already ignoring them */
if (sa_orig_int.sa_handler != SIG_IGN)
sigaction(SIGINT, &sa_default, NULL);
if (sa_orig_quit.sa_handler != SIG_IGN)
sigaction(SIGQUIT, &sa_default, NULL);
/* Restore original signal mask (unblock SIGCHLD) */
sigprocmask(SIG_SETMASK, &orig_mask, NULL);
/* Run the command */
execl("/bin/sh", "sh", "-c", command, (char *)NULL);
/* Only reached if execl fails */
_exit(127);
default:
/* --- PARENT PROCESS ---
* Wait for child we created. Restart if interrupted by signal.
* Save/restore errno around signal manipulation calls. */
while (waitpid(child_pid, &status, 0) == -1) {
if (errno != EINTR) {
/* Real error from waitpid */
status = -1;
break;
}
/* EINTR: interrupted by signal, restart waitpid */
}
break;
}
/* --- Step 3: Restore signal state ---
* Save errno first: sigaction/sigprocmask may clobber it */
saved_errno = errno;
sigprocmask(SIG_SETMASK, &orig_mask, NULL);
sigaction(SIGINT, &sa_orig_int, NULL);
sigaction(SIGQUIT, &sa_orig_quit, NULL);
errno = saved_errno;
return status;
}
int main(void)
{
int ret;
printf("Testing full_system() implementation:\n\n");
/* Basic command */
ret = full_system("echo 'Full system() works!'");
printf("Exit: %d\n\n", WEXITSTATUS(ret));
/* Multi-command pipeline */
ret = full_system("ps aux | grep -c bash");
printf("bash processes: exit %d\n\n", WEXITSTATUS(ret));
/* Command with non-zero exit */
ret = full_system("test -f /nonexistent_file_12345");
printf("File test (expect 1): %d\n\n", WEXITSTATUS(ret));
/* Check NULL: is shell available? */
printf("Shell available: %s\n", full_system(NULL) ? "YES" : "NO");
return 0;
}
Example 3: Chapter Exercise 27.4 — Double fork() Pattern
Exercise 27.4 asks about this pattern. It’s used to make a grandchild process become an orphan — meaning the parent doesn’t need to call wait() for it.
/* example3_double_fork.c
* gcc -o ex3 example3_double_fork.c && ./ex3
*
* Chapter exercise 27.4: What does this double-fork pattern do?
* Answer: Makes grandchild an orphan — adopted by init (PID 1).
* Parent doesn't need to wait for grandchild → no zombie!
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>
void do_background_work(void)
{
/* Simulate some background work */
printf("[Grandchild PID %d] Starting background work...\n", getpid());
printf("[Grandchild] PPID = %d (should be 1 = init)\n", getppid());
sleep(2);
printf("[Grandchild] Background work done!\n");
exit(0);
}
int main(void)
{
pid_t child_pid, grandchild_pid;
int status;
printf("Parent PID: %d\n", getpid());
/* Step 1: First fork */
child_pid = fork();
if (child_pid == -1) { perror("fork1"); exit(1); }
if (child_pid == 0) {
/* CHILD process */
printf("Child PID: %d, PPID: %d\n", getpid(), getppid());
/* Step 2: Second fork */
grandchild_pid = fork();
if (grandchild_pid == -1) { perror("fork2"); exit(1); }
if (grandchild_pid == 0) {
/* GRANDCHILD: do real work here */
do_background_work();
/* NOT REACHED */
}
/* CHILD: exit immediately, making grandchild an orphan */
printf("Child: exiting immediately, grandchild %d is now orphan\n",
grandchild_pid);
exit(0);
}
/* PARENT: only waits for the short-lived child (fast) */
if (waitpid(child_pid, &status, 0) == -1) {
perror("waitpid"); exit(1);
}
printf("Parent: child %d exited. Parent is FREE — no need to wait for grandchild!\n",
child_pid);
printf("Parent: doing other work while grandchild runs independently...\n");
/* Parent can continue without waiting for grandchild.
* Grandchild is adopted by init (PID 1), which will reap it.
* No zombie! */
sleep(3); /* Give grandchild time to finish */
printf("Parent: done.\n");
return 0;
}
/* Why is this useful?
* - Long-running background tasks (like a daemon subprocess)
* - Parent doesn't want to block waiting for the long task
* - Avoids zombie: init automatically reaps orphaned children
* Real use cases: launching daemons, background file operations
*/
Chapter 27 — Complete Summary
| Section | Topic | Key Takeaway |
|---|---|---|
| 27.1 | execve() | Replaces process image; same PID; never returns on success |
| 27.2 | exec() library | l=list, v=vector, p=PATH search, e=custom env; all wrap execve() |
| 27.2.1 | PATH variable | execlp/execvp search PATH; dangerous in setUID; not used if ‘/’ in name |
| 27.2.4 | fexecve() | Exec by fd; prevents TOCTOU race; needs /proc mounted |
| 27.3 | Scripts (#!) | Kernel reads #! and re-execs with interpreter; absolute path required |
| 27.4 | FDs & exec() | FDs inherited by default; FD_CLOEXEC/O_CLOEXEC closes on exec |
| 27.5 | Signals & exec() | Handlers → SIG_DFL; SIG_IGN stays; mask preserved; stack lost |
| 27.6 | system() | Easy but slow; never in setUID; vulnerable to shell injection |
| 27.7 | Implementing system() | Block SIGCHLD; ignore SIGINT/SIGQUIT in parent; use waitpid with EINTR restart |
❓ Interview Questions — Implementing system() & Exercises
Q1. Why must SIGCHLD be blocked BEFORE fork() in system()?
Answer: If SIGCHLD were blocked only after fork(), the child might exit before the parent blocks SIGCHLD. This creates a race: the caller’s SIGCHLD handler fires and collects the child’s status first, then system()’s waitpid() blocks forever waiting for a child that’s already been reaped. Blocking before fork() closes this race window.
Answer: If SIGCHLD were blocked only after fork(), the child might exit before the parent blocks SIGCHLD. This creates a race: the caller’s SIGCHLD handler fires and collects the child’s status first, then system()’s waitpid() blocks forever waiting for a child that’s already been reaped. Blocking before fork() closes this race window.
Q2. Why does system() use _exit(127) in the child if execl fails, instead of exit(127)?
Answer: exit() flushes stdio buffers. The child is a copy of the parent — both copies have the same stdio buffers. Calling exit() in the child would flush the parent’s buffered I/O, causing duplicate output or corruption. _exit() terminates immediately without flushing.
Answer: exit() flushes stdio buffers. The child is a copy of the parent — both copies have the same stdio buffers. Calling exit() in the child would flush the parent’s buffered I/O, causing duplicate output or corruption. _exit() terminates immediately without flushing.
Q3. Why does system()’s parent loop on waitpid() when it gets EINTR?
Answer: The calling program may have signal handlers installed. When a signal arrives, if it interrupts the blocked waitpid(), it returns -1 with errno=EINTR. The loop restarts waitpid() in that case. This is explicitly required by SUSv3: the wait must not be abandoned because of signal interruptions.
Answer: The calling program may have signal handlers installed. When a signal arrives, if it interrupts the blocked waitpid(), it returns -1 with errno=EINTR. The loop restarts waitpid() in that case. This is explicitly required by SUSv3: the wait must not be abandoned because of signal interruptions.
Q4. In the correct system() implementation, why must the child undo the SIGINT/SIGQUIT changes?
Answer: The parent set SIGINT/SIGQUIT to SIG_IGN before fork(). The child inherits this. If the child exec()s the shell with these signals ignored, Ctrl+C won’t kill the running command. The child must restore the original dispositions (or set SIG_DFL if they weren’t SIG_IGN before) so the command responds normally to terminal signals.
Answer: The parent set SIGINT/SIGQUIT to SIG_IGN before fork(). The child inherits this. If the child exec()s the shell with these signals ignored, Ctrl+C won’t kill the running command. The child must restore the original dispositions (or set SIG_DFL if they weren’t SIG_IGN before) so the command responds normally to terminal signals.
Q5. What is the double-fork() technique and when is it used?
Answer: Fork twice: parent → child → grandchild. The child exits immediately, making the grandchild an orphan. Orphans are adopted by init (PID 1), which calls waitpid() automatically. This means the original parent doesn’t need to wait for the grandchild — preventing zombies without blocking. Used for long-running background tasks and daemon processes.
Answer: Fork twice: parent → child → grandchild. The child exits immediately, making the grandchild an orphan. Orphans are adopted by init (PID 1), which calls waitpid() automatically. This means the original parent doesn’t need to wait for the grandchild — preventing zombies without blocking. Used for long-running background tasks and daemon processes.
Q6. Why should system() save and restore errno around signal manipulation?
Answer: After waitpid() fails, errno holds the failure reason (e.g., ECHILD, EINVAL). The subsequent sigprocmask() and sigaction() calls might overwrite errno. By saving it with savedErrno = errno and restoring with errno = savedErrno after the cleanup, the caller sees the correct error from waitpid(), not an artifact from signal cleanup.
Answer: After waitpid() fails, errno holds the failure reason (e.g., ECHILD, EINVAL). The subsequent sigprocmask() and sigaction() calls might overwrite errno. By saving it with savedErrno = errno and restoring with errno = savedErrno after the cleanup, the caller sees the correct error from waitpid(), not an artifact from signal cleanup.
Q7. Exercise 27.1: PATH=/usr/local/bin:/usr/bin:/bin:./dir1:./dir2. In dir1 there’s a non-executable “xyz”. In dir2 there’s an executable “xyz”. What happens with execlp(“xyz”)?
Answer: execlp() searches PATH left to right. It finds “xyz” in ./dir1 but it has no execute permission (EACCES). It continues searching. It finds executable “xyz” in ./dir2 and executes it successfully. execlp() keeps searching on EACCES and ENOENT errors.
Answer: execlp() searches PATH left to right. It finds “xyz” in ./dir1 but it has no execute permission (EACCES). It continues searching. It finds executable “xyz” in ./dir2 and executes it successfully. execlp() keeps searching on EACCES and ENOENT errors.
Q8. Exercise 27.3: What output do you see if you exec() a script with “#!/bin/cat -n” as its first line?
Answer: The kernel sees the #! and re-execs as: /bin/cat -n the_script_file. cat with -n flag prints the file content with line numbers. So the output is the script’s own content printed with line numbers — including the #! line itself. The script becomes its own documented output!
Answer: The kernel sees the #! and re-execs as: /bin/cat -n the_script_file. cat with -n flag prints the file content with line numbers. So the output is the script’s own content printed with line numbers — including the #! line itself. The script becomes its own documented output!
📚 Key Terms to Know
execve() exec() family argv / envp PATH variable execlp / execvp fexecve() TOCTOU race #! shebang interpreter script FD_CLOEXEC O_CLOEXEC dup2() I/O redirection signal mask SIG_DFL / SIG_IGN system() shell injection SIGCHLD race _exit() double fork() EINTR restart waitpid()
🎉 Chapter 27 Complete!
You’ve covered all of Program Execution — execve(), exec() family, scripts, FDs, signals, and system()
