Midterm-II Prep

Class: CSCE-313


Notes:

1.1 Program Execution & Process Behavior

Problem 1

Write a small C program that uses fork() to create a child process. The child should print its own PID and its parent's PID, then exit. The parent should wait for the child and print whether the child exited normally. Predict the output before running it.

Answer:

int main() {
	pid_t pid = fork();
	
	if (pid < 0) {
		perror("forked failed");
		return 1;
	}
	
	if (pid == 0) {
		printf("My PID: ", getpid(), "My Parent's PID: ", getppid());
		exit(0);
	}
	
	else {
		int status;
		wait(&status);
		
		if (WIFEXITED(status)) {
			printf("Parent: child exited normally\n");
		} else {
			printf("Parent: child did not exit normally\n");
		}
		return 0;
	}
}

Problem 2

Consider the following program:

  int fd = open("data.txt", O_RDONLY);
  char buf[10];
  int n = read(fd, buf, 10);
  printf("%d\n", n);

Assume data.txt exists but contains only 5 bytes. What values could read() return? What happens if fd == -1? How should the program correctly handle errors?

Answer:

Problem 3

A program repeatedly calls:

read(fd, buf, 1024);

but occasionally receives -1 with errno == EINTR.

Answer:

EINTR happens when a blocked read() is interrupted by a signal before finishing. The code should retry the call in a loop when errno == EINTR. Exiting immediately is unsafe because the interruption is temporary and not an actual read failure.

Problem 4

A pipe has a buffer size of 64 KB. A process writes continuously to the pipe while another process reads slowly.

Problem 5

What happens to each of the following across an exec() call?

Problem 6

Consider a program that opens a file, writes some data, and then calls fork(). What happens to the file descriptor in the child? Is the write position shared? What could go wrong if both parent and child write to the file without coordination?

1.2 Signals

Problem 1

Stacks as a Data Structure (in C). Implement a stack in C using a dynamic array (i.e., using malloc/realloc). Your implementation should support pushpoppeek, and is_empty. Make your implementation signal-safe?

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct {
    int *data;
    size_t size;      // number of elements currently in stack
    size_t capacity;  // allocated capacity
} Stack;

#define INITIAL_CAPACITY 4

int stack_init(Stack *s) {
    if (s == NULL) return -1;

    s->data = malloc(INITIAL_CAPACITY * sizeof(int));
    if (s->data == NULL) {
        return -1;
    }

    s->size = 0;
    s->capacity = INITIAL_CAPACITY;
    return 0;
}

void stack_destroy(Stack *s) {
    if (s == NULL) return;

    free(s->data);
    s->data = NULL;
    s->size = 0;
    s->capacity = 0;
}

bool is_empty(const Stack *s) {
    return (s == NULL || s->size == 0);
}

int push(Stack *s, int value) {
    if (s == NULL) return -1;

    if (s->size == s->capacity) {
        size_t new_capacity = s->capacity * 2;
        int *new_data = realloc(s->data, new_capacity * sizeof(int));
        if (new_data == NULL) {
            return -1; // resize failed
        }
        s->data = new_data;
        s->capacity = new_capacity;
    }

    s->data[s->size] = value;
    s->size++;
    return 0;
}

int pop(Stack *s, int *out) {
    if (s == NULL || s->size == 0) {
        return -1; // empty stack
    }

    s->size--;
    if (out != NULL) {
        *out = s->data[s->size];
    }
    return 0;
}

int peek(const Stack *s, int *out) {
    if (s == NULL || s->size == 0 || out == NULL) {
        return -1;
    }

    *out = s->data[s->size - 1];
    return 0;
}

int main(void) {
    Stack s;

    if (stack_init(&s) != 0) {
        fprintf(stderr, "Failed to initialize stack\n");
        return 1;
    }

    push(&s, 10);
    push(&s, 20);
    push(&s, 30);

    int x;
    if (peek(&s, &x) == 0) {
        printf("peek = %d\n", x);
    }

    while (!is_empty(&s)) {
        if (pop(&s, &x) == 0) {
            printf("pop = %d\n", x);
        }
    }

    stack_destroy(&s);
    return 0;
}

A dynamic-array stack can be implemented with malloc/realloc, but it cannot be truly async-signal-safe because memory allocation functions are not async-signal-safe. If signal safety is required, the stack should use preallocated memory and avoid calling malloc, realloc, or free from code that may run in a signal handler.

Problem 2

Suppose a program installs a handler for SIGINT.

void handler(int sig) {
    printf("Signal received\n");
}

Problem 3

Write a program that installs a signal handler for SIGINT. The handler should increment a counter and print how many times the user has pressed Ctrl+C. After 3 presses, the program should exit cleanly. What constraints apply to code inside a signal handler?

Problem: Signals can be coalesced into one if received concurrently, it will not be counted, it will just tell you that you have pending signals. There are real time signals that actually count, but we are not doing that.

Answer:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

volatile sig_atomic_t count = 0;

void handler(int sig) {
    count++;
	
    if (count == 1) {
        write(STDOUT_FILENO, "Ctrl+C pressed 1 time\n", 22);
    } else if (count == 2) {
        write(STDOUT_FILENO, "Ctrl+C pressed 2 times\n", 23);
    } else if (count >= 3) {
        write(STDOUT_FILENO, "Exiting after 3 presses\n", 24);
        _exit(0);   // async-signal-safe exit
    }
}

int main(void) {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGINT, &sa, NULL);

    while (1) {
        pause();   // wait for signals
    }

    return 0;
}

Constraints:

Problem 4

Which of the following are valid things to do safely inside a signal handler, and why?

Notes:

Example

long long x;
x++;

Anything that is atomic is signal-safe?

Problem 5

What is the difference between SIGKILL and SIGTERM? Can a process catch or ignore either? Write the kill command invocation you would use to send each.

Signal Catchable? Ignorable? Blockable?
SIGTERM Yes Yes Yes
SIGKILL No No No

kill command invocation:

Send SIGTERM (default)

kill <pid>

or:

kill -TERM <pid>

or

kill -15 <pid>

Send SIGKILL

kill -KILL <pid>

or:

kill -9 <pid>

1.3 Pipes and Inter-Process Communication

Practice Problem 10.

Using pipe() and fork(), write a program where the parent sends the string "hello" to the child through a pipe, and the child reads it and prints it. What happens to the unused ends of the pipe, and why is it important to close them?

Answer:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int p[2];
    pipe(p);

    pid_t pid = fork();

    if (pid == 0) { // child (reader)
        close(p[1]);                    // close unused write end
        char buf[16];
        int n = read(p[0], buf, sizeof(buf));
        write(STDOUT_FILENO, buf, n);   // print what was read
        write(STDOUT_FILENO, "\n", 1);
        close(p[0]);
    } else { // parent (writer)
        close(p[0]);                    // close unused read end
        write(p[1], "hello", 5);
        close(p[1]);                    // important → send EOF
        wait(NULL);
    }
    return 0;
}

What happens to unused pipe ends and why close them?

Practice Problem 11.

Two processes communicate through a pipe. The writer closes its end and exits. What does the reader observe when it tries to read() from the pipe? What signal, if any, is involved?

Answer:
When the writer closes its end and exits, the reader calling:

read(pipe_fd, buf, size);

Practice Problem 12.

Explain what a broken pipe is. Write a scenario (in pseudocode or prose) that would trigger SIGPIPE, and describe what the default behavior is when a process receives it.

What is a broken pipe?

Scenario that triggers SIGPIPE

Default behavior of SIGPIPE

1.4 MCQs

Problem 1

A process opens a file and then calls fork(). Which statement is correct?

Problem 2

After a successful exec() call:

(Choose all that apply.)

Note:

Problem 3

Suppose a process writes to a pipe whose read end has been closed. What happens?

(Choose all that apply.)

Problem 4

If a process calls: read(fd, buf, 100) on a pipe, when does it return 0?

Note:

Problem 5

Which signals cannot be caught or ignored?

Note:

Problem 6

Which statement about Unix directories is true?

Note:

Problem 7

If a parent process never calls wait() for a child that exits, the child becomes:

Note:

Problem 8

Given:

  -rw-r----- file.txt

Which processes can read the file?

Note:

Problem 9

Which of the following are system calls?

Note:

Last minute checklist

  1. What state a process must save during a context switch

    • The kernel must save the process’s CPU execution context, including:
      • program counter (instruction pointer)
      • CPU registers
      • stack pointer
      • processor status/flags
      • memory management state (e.g., page table pointer)
    • This allows the process to resume exactly where it left off.
  2. Why user level threads are cooperative unless extra machinery is added

    • User-level threads execute on their own stack and run continuously until they explicitly yield control of the CPU (e.g., by calling a function like t_yield()). Without extra machinery, such as hardware timer interrupts to force an automatic context switch, the OS has no way to wrest control from the thread; it relies entirely on the thread's "cooperation" to voluntarily invoke the scheduler and let another thread run.
  3. What read returning 0 means for a regular file and for a pipe

    • Regular File: It means the End of File (EOF) has been reached, and there are no more bytes left to read.
    • Pipe: It also indicates EOF, but specifically implies that all write ends of the pipe have been closed and the pipe is empty. (If a write end were still open, the read() call would block and wait for data instead of returning 0).
  4. Why short counts are possible

    • Short counts occur when read() or write() process fewer bytes than you requested. This is not an error and can happen because:
      • The EOF was reached before the requested number of bytes could be read.
      • When reading from a network/socket, buffering can cause delays in the arrival of data.
      • Record-oriented devices (like magnetic tape) may only return data one record at a time.
      • The system call was interrupted by an asynchronous signal mid-transfer.
  5. Why open twice is different from dup2

    • Calling open() twice creates two completely independent entries in the kernel's system-wide File Table. Because the sessions are separate, each has its own independent file cursor/offset.
    • Conversely, dup2() copies a file descriptor so that both the original and the new descriptor point to the exact same File Table entry. Therefore, descriptors copied via dup2 share the exact same file offset and status flags.
  6. Why a child can affect the parent's file position after fork

    • When fork() is called, the child inherits an exact copy of the parent's file descriptor table. Because both the parent's and the child's file descriptors point to the exact same shared File Table entry in the kernel, they share the same file cursor (offset). Therefore, if the child reads or writes to the file, it advances that shared cursor, inherently changing the file position for the parent as well.
  7. Why the first open commonly returns descriptor 3

    • Unix processes generally begin life with three standard open file descriptors already assigned to the terminal: 0 (standard input), 1 (standard output), and 2 (standard error). Because the open() system call guarantees it will always return the lowest available unopened descriptor, the first new file a program opens will naturally be assigned descriptor 3.
  8. How a shell redirects standard output

    • The shell redirects output by using the dup2() system call to manipulate file descriptors. For example, to redirect output into a pipe or a file, the process closes its default standard output (descriptor 1) and uses dup2() to link descriptor 1 to the write-end of the pipe or the open file. As a result, anything the program attempts to write to standard output is automatically routed to the new destination.
    • Example:
      • The shell:
        1. opens the target file
        2. uses dup2(fd, STDOUT_FILENO)
        3. closes the original descriptor
        4. executes the program (exec())
      • Now descriptor 1 points to the file instead of the terminal.
  9. What fflush actually forces the library to do

    • Standard I/O functions accumulate data in a user-space memory buffer for efficiency. Calling fflush() forces the C standard library to empty that buffer by immediately executing the underlying raw Unix write system call, transferring the accumulated data to the output file descriptor or device.
    • It does not guarantee the data reaches disk, only that it leaves the library buffer.
  10. Why mixing fprintf with write can reorder results

    • fprintf uses standard I/O, which buffers its output in memory (often waiting for a newline character or for the buffer to fill up before writing).
    • In contrast, write is a raw Unix I/O system call that sends data to the OS immediately without buffering. If you mix them, the unbuffered write output will print instantly, while the fprintf output may remain held in the buffer and print later when it is finally flushed, altering the expected chronological order of the outputs.
  11. Why stdio buffers can be duplicated across fork

    • Standard I/O functions (like printf or fprintf) accumulate data in user-space memory buffers to improve efficiency. When fork() is called, the OS creates an exact copy of the parent's entire virtual address space for the child. Because the stdio buffer resides in this memory space, any unflushed data sitting in the parent's buffer at the moment of the fork is perfectly duplicated into the child's memory.
  12. How to interpret octal permission values

    • Octal permissions are a scaled summation of binary bits representing Read (R), Write (W), and Execute (X) permissions. Each digit in the 3-digit octal value represents a specific category: Owner, Group, and Others
      • r = 4 (100 in binary)
      • w = 2 (010 in binary)
      • x = 1 (001 in binary) For example, an octal value of 7 (111 in binary) grants full r, w, and x permissions, while 6 (110 in binary) grants only Read and Write permissions.
    • Example: 754
      • 754 → owner: rwx (7), group: r-x (5), others: r-- (4)
  13. What execute means on a directory

    • On a directory, the execute (x) bit acts as the "search" bit. It means you are allowed to find a file by its precise name and, most importantly, access the metadata (the inode) of the files within that directory. Without it, you cannot read or write to any files inside, even if you know they are there.
  14. What set uid changes during program execution

    • If a program has the set-uid (u+) bit enabled, executing that program temporarily changes the caller's effective User ID (UID) to match the UID of the file's owner. This allows the user to temporarily run the program with the elevated privileges of the owner.
  15. Why signal handlers must stay simple

    • Signals are delivered asynchronously, meaning they can interrupt the execution of the main program at any unpredictable microsecond. If the main program is in the middle of a non-atomic operation (like updating a global data structure) when the interrupt occurs, jumping into a complex signal handler that reads that same shared data will result in an inconsistent state and unpredictable bugs. Therefore, handlers must be kept extremely simple, or you must rely on pure functions.
  16. Why SIGKILL cannot be caught or ignored

    • SIGKILL (along with SIGSTOP) cannot be caught, overridden, or ignored by a process. This is a deliberate design choice by the OS to ensure that the kernel always retains a guaranteed, surefire mechanism to forcefully terminate runaway or unresponsive processes.
  17. Why a SIGCHLD handler often uses waitpid in a loop

    • Standard UNIX signals do not queue; they are coalesced. If multiple children terminate simultaneously while the parent is already inside the SIGCHLD handler (where the signal is temporarily blocked), the kernel will only record one single pending SIGCHLD signal for all of them. Using waitpid in a loop (usually with the WNOHANG flag) ensures the handler checks for and reaps all terminated children before returning, preventing zombies.
  18. How to protect shared data from asynchronous signal delivery

    • To protect shared data from being corrupted by unpredictable interrupts, you must manually block the signal before entering a critical section of code, and unblock it immediately after. This is done using the sigprocmask() system call, which temporarily adds the signal to the process's blocked signal mask.
  19. What SIGPIPE means

    • SIGPIPE is a software-generated signal that indicates a broken pipe. The kernel sends this signal to a process when it attempts to write data to a pipe that no longer has any active readers (e.g., the reading process has already exited or closed its read descriptor).
  20. How to trace shared versus copied state without guessing

    • Instead of guessing what a process is doing under the hood, you can use the strace utility. strace intercepts and records the exact sequence of system calls a program is executing (including file opens, forks, and network connections) along with their return values, allowing you to trace exactly how the OS handles its state.
    • Use the rule:
      • Shared kernel objects (after fork):
        • open file descriptions (file offset, flags)
        • pipes
        • sockets
      • Copied user memory:
        • heap
        • stack
        • globals
        • stdio buffers
    • General principle:
      • If the state lives in the kernel, it is usually shared.
      • If it lives in user address space, it is copied.