ml3m's journey

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:

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:

  1. A semaphore is initialized with a value of 1, allowing one process to enter the critical region at a time.
  2. sem_wait() decrements the semaphore value, blocking if it's already 0. Also the same as sem_down()
  3. Both parent and child processes access the critical region while ensuring mutual exclusion.
  4. sem_post() increments the semaphore value, signaling that the critical region is free. Also the same as sem_up()
  5. 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:

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:

  1. The pthread_create() function is used to create threads, passing a unique thread ID to each.
  2. Each thread executes the start function, printing its ID.
  3. pthread_join() ensures that the main program waits for all threads to finish before exiting.

Improvements:


3. Reading and Writing to Files

Problem:

Safely read from a file and handle issues like null terminators and buffer overflows.

Key Concepts:

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:

  1. The file is opened using open() and read into a buffer with read().
  2. The buffer is null-terminated to ensure safe string handling.
  3. 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:

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:

  1. A pipe is created using pipe(), providing two file descriptors for reading and writing.
  2. The parent process writes user input to the pipe, while the child reads from it.
  3. 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:

  1. SIGINT toggles the direction variable and starts/stops the periodic alarm.
  2. SIGALRM prints a message and re-schedules itself if direction 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:


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:

  1. Semaphore Initialization:

    • The semaphore is initialized with a value of 1, allowing only one process to execute the critical section at any time.
  2. 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.
  3. 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.
  4. Parent Process:

    • The parent process waits for all 101 child processes to complete using a wait() loop.
  5. Cleanup:

    • The semaphore is destroyed at the end of the program to release system resources.

Key Takeaways:


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:

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:

  1. Each thread processes the file in chunks to count digits.
  2. A semaphore limits the number of threads running simultaneously to 6.
  3. 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:

  1. SIGINT toggles a direction flag, enabling or stopping a periodic alarm.
  2. 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:

  1. Use WEXITSTATUS to retrieve the low-order 8 bits of the exit code.
  2. 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:

  1. Uses pipe for IPC between Child 2 and Child 3
  2. Sequential process creation to ensure proper ordering
  3. Proper error handling for all system calls
  4. Careful pipe management (closing unused ends)
  5. 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:

  1. Thread Control

    • Semaphore limits concurrent execution to 2 threads
    • Each thread must acquire semaphore before updating shared counter
  2. Work Distribution

    • Range divided into equal chunks
    • Last thread handles remainder
    • Each thread gets a unique range to process
  3. Data Management

    • Thread-safe counter updates using semaphore
    • Local counters per thread for individual results
    • Struct used to pass range information to threads
  4. Synchronization

    • Semaphore for parallel execution control
    • Join operations ensure all threads complete
    • Critical section protection for shared counter
  5. 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:


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:

  1. Wrong File Descriptor Type

    • Uses unsigned long instead of int
    • File descriptors in Unix/Linux are integers
  2. Missing Error Handling

    • No checks for open(), read() and close() failure
  3. Missing String Termination

    • Reads 1023 bytes but doesn't add null terminator
    • Could cause buffer overflow when printing
  4. Syntax Issues

    • Missing semicolon after open()
    • Using &v instead of v in read()

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:

  1. Proper Types

    • Changed fd to int
    • Added variable for tracking bytes read
  2. Error Handling

    • Added checks for all system calls
    • Uses perror() for error reporting
    • Different return codes for different errors
  3. 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
  4. Buffer Management

    • Passes v instead of &v to read()
    • Ensures buffer has space for null terminator
    • Properly handles partial reads

This is a good example of proper string handling in C, showing:


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:

  1. Logic Errors

    • Wrong process creation structure
    • Incorrect condition for second child (p < 0)
    • First child runs in parent section
  2. Type Issues

    • Using char for loop counter (too small)
    • Wrong usage of waitpid (passing address of p)
  3. Race Conditions

    • No proper synchronization between processes
    • Both children could print simultaneously
  4. 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:

  1. Process Structure

    • Clear separation of parent and child code
    • Sequential child creation and execution
    • Proper exit for child processes
  2. Synchronization

    • Parent waits for first child before creating second
    • Eliminates race conditions in printing
    • Ordered execution of children
  3. Error Handling

    • Checks for fork failures
    • Proper error messages with perror
    • Different return codes for different errors
  4. Variable Types

    • Using int for loop counters
    • Proper use of pid_t for process IDs
    • Correct parameter types for waitpid

Key Lessons:

  1. Race Condition Prevention

    • Wait for first child before creating second
    • Ensures ordered output
    • No interleaved printing
  2. Process Management

    • Clear parent/child relationships
    • Proper process termination
    • Correct wait placement
  3. Error Handling

    • Proper error reporting
    • Clean process termination

Explanation of Concepts with Examples from the Code 2.2

Difference Between fopen and open

fopen

open

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);

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

SEEK_END

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

How It Works

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);

Example in Code

fseek(fd, 0, SEEK_END);
long file_size = ftell(fd);
  1. fseek(fd, 0, SEEK_END) moves the file pointer to the end.
  2. 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

Explanation in Code

pthread_create(&t[i], NULL, count_digits_in_file, (void *)&t_pack[i]);

Benefit


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.