Operating Systems and all notes taken.
Code Solutions Explained
curated by ml3m
This document provides a detailed explanation of how various C programming problems were solved. Each section includes the original problem, its solution, and an explanation of the logic used, along with improvements or fixes where applicable.
1. Semaphore Synchronization in Forked Processes
Problem:
Create a parent and child process using fork()
. Use semaphores to synchronize
access to a critical region, ensuring that only one process (parent or child)
can access it at a time.
Key Concepts:
- Semaphores: Used to control access to a shared resource in concurrent programming.
- Critical Region: A section of code that must not be executed by more than one process simultaneously.
Solution:
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
sem_t semaphore;
int main() {
if (sem_init(&semaphore, 0, 1) == -1) {
perror("Error initializing semaphore");
return 1;
}
pid_t p = fork();
if (p == 0) { // Child process
sem_wait(&semaphore);
printf("Child is in the critical region\n");
sleep(1);
printf("Child left the critical region\n");
sem_post(&semaphore);
} else if (p > 0) { // Parent process
sem_wait(&semaphore);
printf("Parent is in the critical region\n");
sleep(1);
printf("Parent left the critical region\n");
sem_post(&semaphore);
wait(NULL); // Wait for child process to finish
} else {
perror("Error creating fork");
return 2;
}
sem_destroy(&semaphore);
return 0;
}
Explanation:
- A semaphore is initialized with a value of 1, allowing one process to enter the critical region at a time.
sem_wait()
decrements the semaphore value, blocking if it's already 0. Also the same assem_down()
- Both parent and child processes access the critical region while ensuring mutual exclusion.
sem_post()
increments the semaphore value, signaling that the critical region is free. Also the same assem_up()
- Proper cleanup is performed by destroying the semaphore.
2. Creating Threads with Pthreads
Problem:
Create multiple threads to execute tasks in parallel. Each thread should print its ID.
Key Concepts:
- Threads: Lightweight processes that share the same memory space.
- Pthreads: POSIX standard for creating and managing threads.
Solution:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *start(void *arg) {
int threadID = *(int *)arg;
printf("Thread %d executes\n", threadID);
return NULL;
}
int main() {
pthread_t threads[8];
int thread_ids[8];
for (int i = 0; i < 8; i++) {
thread_ids[i] = i;
if (pthread_create(&threads[i], NULL, start, &thread_ids[i]) != 0) {
perror("Error creating thread");
return 1;
}
}
for (int i = 0; i < 8; i++) {
pthread_join(threads[i], NULL);
}
printf("All threads have finished execution!\n");
return 0;
}
Explanation:
- The
pthread_create()
function is used to create threads, passing a unique thread ID to each. - Each thread executes the
start
function, printing its ID. pthread_join()
ensures that the main program waits for all threads to finish before exiting.
Improvements:
- Ensure thread-safe access to shared resources (if any).
3. Reading and Writing to Files
Problem:
Safely read from a file and handle issues like null terminators and buffer overflows.
Key Concepts:
- File Descriptors: Low-level file access using
open()
andread()
. - Buffer Management: Ensuring proper allocation and null termination.
Solution:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
char buffer[130];
int fd = open("my_file.txt", O_RDONLY);
if (fd == -1) {
perror("Error opening file");
return 1;
}
ssize_t bytesRead = read(fd, buffer, 129);
if (bytesRead == -1) {
perror("Error reading file");
close(fd);
return 2;
}
buffer[bytesRead] = '\0'; // Null-terminate the buffer
printf("File contents: %s\n", buffer);
close(fd);
return 0;
}
Explanation:
- The file is opened using
open()
and read into a buffer withread()
. - The buffer is null-terminated to ensure safe string handling.
- Proper error handling ensures graceful recovery from issues like missing files or read errors.
4. Inter-Process Communication Using Pipes
Problem:
Create a parent and child process. The parent reads input from the user and sends it to the child via a pipe.
Key Concepts:
- Pipes: Mechanism for unidirectional communication between processes.
- Forking: Creating a child process from the parent.
Solution:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int pipe_fd[2];
char buffer[1024];
if (pipe(pipe_fd) == -1) {
perror("Error creating pipe");
return 1;
}
pid_t p = fork();
if (p == 0) { // Child process
close(pipe_fd[1]);
read(pipe_fd[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(pipe_fd[0]);
} else if (p > 0) { // Parent process
close(pipe_fd[0]);
printf("Enter text: ");
scanf("%1023s", buffer);
write(pipe_fd[1], buffer, sizeof(buffer));
close(pipe_fd[1]);
wait(NULL);
} else {
perror("Error creating fork");
return 2;
}
return 0;
}
Explanation:
- A pipe is created using
pipe()
, providing two file descriptors for reading and writing. - The parent process writes user input to the pipe, while the child reads from it.
- Proper closing of unused ends of the pipe ensures no deadlocks occur.
5. Handling Signals with SIGINT
and SIGALRM
Problem:
Handle signals to control program behavior dynamically, such as toggling a direction flag or periodic message printing.
Solution:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int direction = 0;
void signal_handler(int sig) {
if (sig == SIGINT) {
direction = 1 - direction;
if (direction) {
alarm(3);
} else {
printf("Stopped printing messages.\n");
alarm(0);
}
} else if (sig == SIGALRM) {
printf("Hello\n");
if (direction) {
alarm(3);
}
}
}
int main() {
signal(SIGALRM, signal_handler);
signal(SIGINT, signal_handler);
while (1) {
pause(); // Wait for signals
}
return 0;
}
Explanation:
SIGINT
toggles thedirection
variable and starts/stops the periodic alarm.SIGALRM
prints a message and re-schedules itself ifdirection
is active.
6. Creating 101 Child Processes with Synchronization
Problem:
Write a C program that creates 101 child processes, all spawned from the same parent. The even-numbered child processes should display their own PID, while the odd-numbered child processes should display the PID of the parent.
Key Concepts:
- Forking: Creating child processes from the parent process using
fork()
. - Semaphores: Used to control access to shared resources and ensure that only one process executes a critical section at a time.
- Race Conditions: Avoided by using semaphores to synchronize output.
Solution:
Here is the C program to accomplish the task:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <semaphore.h>
int main() {
sem_t semaphore;
// Initialize semaphore with value 1
sem_init(&semaphore, 0, 1);
// Get parent process PID
int parent_pid = getpid();
for (int i = 0; i < 101; i++) {
int id = fork();
if (id == 0) { // Child process
sem_wait(&semaphore); // Lock the semaphore
if (i % 2 == 0) {
printf("Child %d: My PID is %d\n", i, getpid());
} else {
printf("Child %d: Parent PID is %d\n", i, parent_pid);
}
sem_post(&semaphore); // Unlock the semaphore
return 0;
} else if (id == -1) { // Error handling
perror("Error creating fork()");
return 1;
}
}
// Parent waits for all child processes to finish
for (int i = 0; i < 101; i++) {
wait(NULL);
}
// Destroy the semaphore
sem_destroy(&semaphore);
return 0;
}
Explanation:
Semaphore Initialization:
- The semaphore is initialized with a value of 1, allowing only one process to execute the critical section at any time.
Forking Child Processes:
- A loop is used to create 101 child processes.
- Each child process determines whether its index is even or odd to decide what to print.
Synchronization with Semaphores:
sem_wait()
locks the semaphore, ensuring only one process executes the printing logic at a time.- After printing,
sem_post()
unlocks the semaphore, allowing the next process to access the critical section.
Parent Process:
- The parent process waits for all 101 child processes to complete using a
wait()
loop.
- The parent process waits for all 101 child processes to complete using a
Cleanup:
- The semaphore is destroyed at the end of the program to release system resources.
Key Takeaways:
Avoiding Race Conditions: By using semaphores, the program ensures that child processes do not interfere with each other while printing, which could otherwise lead to jumbled output.
Efficient Forking: All child processes are spawned directly from the parent, making the hierarchy simple and ensuring synchronization remains effective.
Proper Cleanup: Destroying the semaphore prevents resource leaks and ensures the program exits cleanly.
7. Counting Digits in a File Using Threads
Problem:
Write a C program that spawns 8 additional worker threads. Only 6 of these threads at most should run in parallel, and the program (using the threads) should find all digits in a text file passed as argument to the program, and print that resulting number on screen. At the end, every thread needs to show how many digits it found on its own.
Key Concepts:
- Chunking a File: Dividing the file into portions for parallel processing.
- Thread Synchronization: Using mutexes to protect shared data.
- Semaphore for Parallel Limits: Using semaphores to restrict the number of threads running concurrently.
Solution:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#define MAX_THREADS 8
#define MAX_PARALLEL_THREADS 6
pthread_mutex_t mutex;
int total_digit_count = 0;
typedef struct {
char *filename;
long start_pos;
long end_pos;
} ThreadArgs;
void *count_digits(void *args) {
ThreadArgs *thread_args = (ThreadArgs *)args;
FILE *file = fopen(thread_args->filename, "r");
if (!file) {
perror("Error opening file");
return NULL;
}
// Move to the start position for this thread
fseek(file, thread_args->start_pos, SEEK_SET);
int local_count = 0;
char ch;
long position = thread_args->start_pos;
// Read the file until the end position
while (position < thread_args->end_pos && (ch = fgetc(file)) != EOF) {
if (isdigit(ch)) {
local_count++;
}
position++;
}
fclose(file);
// Print the number of digits found by this thread
printf("Thread found %d digits in its chunk.\n", local_count);
// Add the local count to the global total in a thread-safe manner
pthread_mutex_lock(&mutex);
total_digit_count += local_count;
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
// Open the file to determine its size
FILE *file = fopen(argv[1], "r");
if (!file) {
perror("Error opening file");
return 1;
}
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
fclose(file);
pthread_t threads[MAX_THREADS];
ThreadArgs thread_args[MAX_THREADS];
// Initialize mutex
pthread_mutex_init(&mutex, NULL);
// Calculate the chunk size for each thread
long chunk_size = file_size / MAX_THREADS;
long remainder = file_size % MAX_THREADS;
// Spawn threads
for (int i = 0; i < MAX_THREADS; i++) {
thread_args[i].filename = argv[1];
thread_args[i].start_pos = i * chunk_size;
thread_args[i].end_pos = (i + 1) * chunk_size;
// Distribute any remainder across the last thread
if (i == MAX_THREADS - 1) {
thread_args[i].end_pos += remainder;
}
if (pthread_create(&threads[i], NULL, count_digits, (void *)&thread_args[i]) != 0) {
perror("Error creating thread");
return 1;
}
// Ensure that only 6 threads run in parallel
if (i >= MAX_PARALLEL_THREADS - 1) {
pthread_join(threads[i - MAX_PARALLEL_THREADS + 1], NULL);
}
}
// Wait for all threads to finish
for (int i = MAX_THREADS - MAX_PARALLEL_THREADS + 1; i < MAX_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// Print the total count of digits found
printf("Total digits found: %d\n", total_digit_count);
// Clean up mutex
pthread_mutex_destroy(&mutex);
return 0;
}
}
Explanation:
- Each thread processes the file in chunks to count digits.
- A semaphore limits the number of threads running simultaneously to 6.
- A mutex ensures thread-safe updates to the global digit count.
Advanced C Programming Topics
This document expands upon the solutions provided earlier by diving deeper into specific concepts and techniques in C programming, including mutexes, threads, signals, and more.
1. Mutexes
Creating a Mutex
To ensure mutual exclusion, initialize a mutex before using it.
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
Locking and Unlocking a Mutex
Mutexes are used to protect shared resources in multi-threaded environments.
pthread_mutex_lock(&mutex1);
// Critical section
pthread_mutex_unlock(&mutex1);
2. Threads
Creating and Joining Threads
Threads are lightweight processes that share memory space. Use pthread_create
to spawn threads and pthread_join
to ensure they finish before the program
continues.
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_func1, NULL);
pthread_create(&t2, NULL, thread_func2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
3. Waiting for a Child Process to Finish and Retrieving Exit Code
Explanation
The wait()
function blocks the parent process until a child process
terminates. Use WIFEXITED
to check if the process exited normally and
WEXITSTATUS
to retrieve its exit code.
#include <sys/wait.h>
int status;
wait(&status);
if (WIFEXITED(status)) { // Check if exited normally
int exit_code = WEXITSTATUS(status); // Fetch the exit code
if (exit_code == 1) {
printf("Error: Child exited with exit code 1\n");
}
} else {
printf("Error: Child did not exit normally\n");
exit(3);
}
4. Creating Processes Equal to CPU Cores
Explanation
The get_nprocs()
function from <sys/sysinfo.h>
retrieves the number of CPU
cores. The program creates one child process per core.
#include <sys/sysinfo.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
int number_cores = get_nprocs();
pid_t id;
for (int i = 0; i < number_cores; i++) {
id = fork();
if (id == 0) {
exit(0);
} else if (id == -1) {
perror("Error creating child process at i = %d");
exit(1);
}
}
while (wait(NULL) > 0);
return 0;
}
5. Casting a void *
to Any Type
Explanation
Casting void *
is common when passing data to threads. For example:
void *start(void *arg) {
int threadID = *((int *)arg);
printf("Thread ID: %d\n", threadID);
return NULL;
}
6. Signal Handling
Initial Code
Signals allow a program to respond to asynchronous events, such as keyboard
interrupts (SIGINT
) or alarms (SIGALRM
).
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int direction = 0;
void sigHandler(int sigNo) {
if (sigNo == SIGINT) {
direction = 1 - direction;
if (direction) {
alarm(3);
} else {
printf("Stopped printing messages.\n");
alarm(0);
}
}
if (sigNo == SIGALRM) {
printf("Hello\n");
if (direction) {
alarm(3);
}
}
}
int main() {
signal(SIGALRM, sigHandler);
signal(SIGINT, sigHandler);
while (1) {
sleep(1);
}
return 0;
}
Key Points:
SIGINT
toggles adirection
flag, enabling or stopping a periodic alarm.SIGALRM
prints a message and re-schedules itself if the flag is active.
7. File Descriptors and Buffer Sizes
Explanation
Buffers should be properly sized and null-terminated when working with file descriptors.
#define SIZE (1 << 10) // 1024 bytes
8. Exit Codes in Processes
Explanation
Processes can exit with specific codes. Negative exit codes wrap around using modulo 256.
exit(-300) // -300 mod 256 * (cat) > 0
// -300 + 256 * 2 (512)
// 512 - 300 = 212
exit(-1245); // -1245 + 256 * (cat) > 0
// -1245 + 256 * 5 (1.280)
// 1280 - 1245 = 35
exit(-569); // 768 - 569 = 199
exit(351); // 351 % 256 = 95
Key Points:
- Use
WEXITSTATUS
to retrieve the low-order 8 bits of the exit code. - Negative values are wrapped using modulo arithmetic.
2.1 - Three Children with PID Communication
Original Problem:
Write a C program that creates 3 child processes, all spawned from the same parent. In the first child, you print the PID of the parent process, in the second child you print the PID of the third child process, and in the third child prints its own PID.
Complete Solution:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
pid_t c1, c2, c3;
int fd[2];
if(pipe(fd) == -1){
perror("error creating pipe");
return 5;
}
int parent_PID = getppid();
c1 = fork();
if (c1 == 0) {
printf("from c1: parent PID: %d\n", parent_PID);
exit(0);
} else if (c1 == -1) {
perror("error creating c1");
return 1;
} else {
wait(NULL);
c2 = fork();
if (c2 == 0) {
close(fd[1]);
pid_t c3_pid;
if(read(fd[0], &c3_pid, sizeof(pid_t)) == -1){
perror("error reading from pipe by c2");
return 4;
}
close(fd[0]);
printf("from c2, c3 pid is: %d\n", c3_pid);
exit(0);
} else if (c2 == -1) {
perror("error creating c2");
return 2;
} else {
c3 = fork();
if (c3 == 0) {
close(fd[0]);
pid_t c3_pid = getpid();
if(write(fd[1], &c3_pid, sizeof(pid_t)) == -1){
perror("error from c3 writing to pipe");
return 6;
}
close(fd[1]);
printf("from c3: c3 pid is: %d\n", getpid());
exit(0);
} else if(c3 == -1){
perror("error creating c3");
return 3;
} else {
wait(NULL);
wait(NULL);
wait(NULL);
}
}
}
return 0;
}
Key Points:
- Uses pipe for IPC between Child 2 and Child 3
- Sequential process creation to ensure proper ordering
- Proper error handling for all system calls
- Careful pipe management (closing unused ends)
- Parent waits for all children to complete
2.2 - Threaded Prime Counter
Original Problem:
Write a C program that spawns 4 additional worker threads. Only 2 of
these threads(at most) should run in parallel, and the program(using the
threads) should count the number of prime numbers ending in digits 3 and 7, between 2 and 100000000 and print that resulting number on screen. At the end, every thread needs to show how many primes it found on its own.
Complete Solution:
#include <ctype.h>
#include <stdio.h>
#include <stdbool.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#define LIMIT 100000
#define MAX_THREADS 4
int COUNT = 0;
typedef struct {
int th_number;
int start;
int end;
} Thread_chunk_info;
// we have part the range in chunks for the threads to work.
sem_t semaphore;
bool is_prime(int num) {
if (num <= 1)
return false;
for (int i = 2; i * i <= num; i++) {
if (num % i == 0)
return false;
}
return true;
}
void *count_primes(void * arg) {
Thread_chunk_info *thread_info = (Thread_chunk_info *) arg;
int thread_prime_count = 0;
for (int num = thread_info->start; num < thread_info->end; num++) {
if (is_prime(num) && (num % 10 == 3 || num % 10 == 7)) {
sem_wait(&semaphore);
thread_prime_count ++;
COUNT++;
sem_post(&semaphore);
}
}
printf("thread with number: %d found %d primes\n",thread_info->th_number, thread_prime_count);
return NULL;
}
int main() {
sem_init(&semaphore, 0, 2); // 2 at most in parallel
Thread_chunk_info thread_info[MAX_THREADS];
pthread_t thread[MAX_THREADS];
int chunk_size = LIMIT / MAX_THREADS;
int remainder = LIMIT % MAX_THREADS;
for (int i =0 ; i< MAX_THREADS; i++) {
thread_info[i].th_number = i;
thread_info[i].start = i * chunk_size;
thread_info[i].end = (i + 1) * chunk_size;
// last th gets the reminder also
if (i == MAX_THREADS -1) {
thread_info[i].end += remainder;
}
if(pthread_create(&thread[i], NULL, count_primes, (void *)&thread_info[i]) != 0){
perror("error creating thread ");
return 2;
}
}
for (int i =0 ; i< MAX_THREADS; i++) {
pthread_join(thread[i], NULL);
}
printf("all primes found are: %d\n", COUNT);
return 0;
}
Key Implementation Points:
Thread Control
- Semaphore limits concurrent execution to 2 threads
- Each thread must acquire semaphore before updating shared counter
Work Distribution
- Range divided into equal chunks
- Last thread handles remainder
- Each thread gets a unique range to process
Data Management
- Thread-safe counter updates using semaphore
- Local counters per thread for individual results
- Struct used to pass range information to threads
Synchronization
- Semaphore for parallel execution control
- Join operations ensure all threads complete
- Critical section protection for shared counter
Prime Number Logic
- Efficient prime checking algorithm
- Only checks numbers ending in 3 or 7
- Each thread maintains its own count
The key challenge here was combining:
- Thread management
- Parallel execution control
- Work distribution (chunks)
- Thread safety
1.3 - File Reading with String Termination
Original Problematic Code:
char v[1024];
unsigned long fd = open("/my/OF/pics", O_RDONLY)
read(fd, &v, 1023);
printf("File contents: %s", v);
close(fd);
Issues in Original Code:
Wrong File Descriptor Type
- Uses
unsigned long
instead ofint
- File descriptors in Unix/Linux are integers
- Uses
Missing Error Handling
- No checks for
open()
,read()
andclose()
failure
- No checks for
Missing String Termination
- Reads 1023 bytes but doesn't add null terminator
- Could cause buffer overflow when printing
Syntax Issues
- Missing semicolon after
open()
- Using
&v
instead ofv
inread()
- Missing semicolon after
Complete Fixed Solution:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
char v[1024];
int fd = open("file.txt", O_RDONLY);
int B_read;
// Check if file opened successfully
if (fd == -1) {
perror("error opening file");
return 1;
}
// Read and store number of bytes read
if((B_read = read(fd, v, 1023)) == -1) {
perror("error reading file");
return 2;
}
// Add string terminator after last byte read
v[B_read] = '\0';
printf("File contents: %s", v);
// Check close operation
if (close(fd) == -1) {
perror("error closing file");
return 3;
}
return 0;
}
Key Improvements:
Proper Types
- Changed
fd
toint
- Added variable for tracking bytes read
- Changed
Error Handling
- Added checks for all system calls
- Uses
perror()
for error reporting - Different return codes for different errors
String Safety
- Reads max 1023 bytes to leave room for null terminator
- Explicitly adds null terminator after read
- Uses bytes read count to place terminator correctly
Buffer Management
- Passes
v
instead of&v
toread()
- Ensures buffer has space for null terminator
- Properly handles partial reads
- Passes
This is a good example of proper string handling in C, showing:
- Buffer management
- Null termination for strings
- Safe string printing
1.5 - Process Creation and Synchronization
Original Problematic Code:
pid_t p;
int s;
p = fork();
if (p > 0){
char i;
for(i=1; i<1000; i++)
if(i%2 == 0)
printf("Child 1 prints : %d\n", i);
} else {
p = fork();
if(p < 0)
{
char i;
for(i=1000; i>=1; i--)
if(i%2 == 1)
printf("Child 2 prints: %d\n", i);
}
}
waitpid(&p, &s, NULL);
waitpid(&p, &s, NULL);
printf("Children finished printing.");
Issues in Original Code:
Logic Errors
- Wrong process creation structure
- Incorrect condition for second child (
p < 0
) - First child runs in parent section
Type Issues
- Using
char
for loop counter (too small) - Wrong usage of
waitpid
(passing address ofp
)
- Using
Race Conditions
- No proper synchronization between processes
- Both children could print simultaneously
Process Management
- Incorrect parent/child relationship
- Wrong placement of wait calls
Complete Fixed Solution:
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
int s;
pid_t p, p2;
// Create first child
p = fork();
if (p == 0){
// First child process
int i;
for(i=1; i<1000; i++)
if(i%2 == 0)
printf("Child 1 prints : %d\n", i);
exit(0);
} else if (p == -1) {
perror("error creating child process p1");
return 1;
} else {
// Parent process
// Wait for first child to finish before creating second
waitpid(p, &s, 0);
// Create second child
p2 = fork();
if(p2 == 0){
// Second child process
int i;
for(i=1000; i>=1; i--)
if(i%2 == 1)
printf("Child 2 prints: %d\n", i);
exit(0);
} else if (p2 == -1) {
perror("error creating child process p2");
return 2;
} else {
// Parent process
waitpid(p2, &s, 0);
printf("Children finished printing.");
}
}
return 0;
}
Key Improvements:
Process Structure
- Clear separation of parent and child code
- Sequential child creation and execution
- Proper exit for child processes
Synchronization
- Parent waits for first child before creating second
- Eliminates race conditions in printing
- Ordered execution of children
Error Handling
- Checks for fork failures
- Proper error messages with perror
- Different return codes for different errors
Variable Types
- Using
int
for loop counters - Proper use of
pid_t
for process IDs - Correct parameter types for
waitpid
- Using
Key Lessons:
Race Condition Prevention
- Wait for first child before creating second
- Ensures ordered output
- No interleaved printing
Process Management
- Clear parent/child relationships
- Proper process termination
- Correct wait placement
Error Handling
- Proper error reporting
- Clean process termination
Explanation of Concepts with Examples from the Code 2.2
Difference Between fopen
and open
fopen
- A high-level C library function that opens a file and returns a
FILE*
stream. - Supports buffering and provides more advanced I/O functions like
fscanf
,fprintf
, andfgets
. - Easier to use for text and formatted data processing.
open
- A low-level system call that interacts directly with the operating system.
- Returns a file descriptor (an integer) and does not provide buffering.
- Better suited for unbuffered or raw binary data operations.
Example in Code
The code uses fopen
to open the file:
FILE *fd = fopen(t_pack->filename, "r");
if (!fd) {
perror("Error opening file");
return NULL;
}
Here, fopen
is appropriate because the program works with text data, and buffering improves performance.
How Does fseek
Work and What Arguments Does It Take?
fseek
is used to move the file pointer to a specific position in the file.
Syntax
int fseek(FILE *stream, long offset, int whence);
stream
: The file pointer (returned byfopen
).offset
: The number of bytes to move the pointer.whence
:SEEK_SET
: Move the pointer to the specifiedoffset
from the beginning of the file.SEEK_CUR
: Move the pointer tooffset
bytes from the current position.SEEK_END
: Move the pointer tooffset
bytes from the end of the file.
Example in Code
fseek(fd, t_pack->start, SEEK_SET);
Here, fseek
moves the file pointer to the position specified by t_pack->start
from the beginning of the file.
What are SEEK_SET
and SEEK_END
?
SEEK_SET
- Moves the file pointer to an absolute position relative to the start of the file.
- Example:
fseek(fd, 0, SEEK_SET); // Moves to the start of the file
SEEK_END
- Moves the file pointer relative to the end of the file.
- Example:
fseek(fd, 0, SEEK_END); // Moves to the end of the file
Usage in Code
fseek(fd, 0, SEEK_END);
long file_size = ftell(fd);
Here, fseek
moves the pointer to the end of the file to calculate its size.
What Does fgetc(file)
Do and How Does It Work?
Purpose
- Reads a single character from the file pointed to by
file
.
How It Works
- Internally, it reads the next character from the file's buffer (or directly from the file if unbuffered).
- Returns the character as an
int
, orEOF
if the end of the file is reached or an error occurs.
Example in Code
char ch;
for (long i = t_pack->start; i < t_pack->end && (ch = fgetc(fd)) != EOF; i++) {
if (isdigit(ch)) {
thread_count++;
}
}
Here, fgetc(fd)
reads characters one by one from the file chunk assigned to the thread.
What Does ftell
Do?
ftell
returns the current position of the file pointer (in bytes) relative to the beginning of the file.
Syntax
long ftell(FILE *stream);
stream
: The file pointer.- Returns the position as a
long
, or-1
if an error occurs.
Example in Code
fseek(fd, 0, SEEK_END);
long file_size = ftell(fd);
fseek(fd, 0, SEEK_END)
moves the file pointer to the end.ftell(fd)
retrieves the total size of the file in bytes.
Why Do We Pass (void *)&
of the Struct t_pack
to the Worker Function?
Reason
- The
pthread_create
function requires the thread's argument to be avoid *
type. - To pass a structure like
Thread_pack
, we cast its address to(void *)
.
Explanation in Code
pthread_create(&t[i], NULL, count_digits_in_file, (void *)&t_pack[i]);
- The address of
t_pack[i]
is passed to the thread functioncount_digits_in_file
. - Inside the function, it is cast back to the appropriate type:
Thread_pack *t_pack = (Thread_pack *) args;
Benefit
- This allows each thread to access its specific
Thread_pack
instance containing the chunk details (start
,end
, andfilename
).
Summary of Key Functions
Function | Purpose | Example Usage |
---|---|---|
fopen |
Opens a file and returns a FILE* stream |
FILE *fd = fopen("file.txt", "r"); |
fseek |
Moves the file pointer | fseek(fd, 0, SEEK_SET); |
SEEK_SET , SEEK_END |
Constants for file pointer positioning | fseek(fd, 0, SEEK_END); |
fgetc |
Reads a single character from a file | ch = fgetc(fd); |
ftell |
Returns the current file pointer position | long size = ftell(fd); |
pthread_create |
Creates a new thread | pthread_create(&t[i], NULL, worker, (void *)x); |
This document explains the key concepts and functions used in the program with examples to ensure clarity and practical understanding.