Post

C Linux Master Sheet

C interview cheatsheet of important questions.

Make sure to revise the list right before an important interview

Important C macros

Basic macros

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))
#define IS_POWER_OF_TWO(num) ((num) > 0 && ((num) & ((num) - 1)) == 0)
#define SWAP(x,y) (x ^= y ^=x ^= y)

#define IS_EVEN(num) (((num) & 1) == 0)
#define IS_ODD(num) (((num) & 1) != 0)

#define SWAP(x,y,T) do { \
    T temp = (*x);\
    (*x) = (*y); \
    (*y) = temp; \
} while (0)

Bitwise macros

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define SET_BIT(num, i) ((num) |= (1 << (i)))
#define CLEAR_BIT(num, i) ((num) &= ~(1 << (i)))
#define TOGGLE_BIT(num, i) ((num) ^= (1 << (i)))
#define CHECK_BIT(num, i) (((num) & (1 << (i))) != 0)

/* how to extract specific bits from, say, a 32-bit value */
#define EXTRACT_BITS(value, start, length) \
    (((value) >> (start)) & ((1U << (length)) - 1))

#define COUNT_SET_BITS(num) ({       \
    int _count = 0;                  \
    int _num = (num);                \
    while (_num) {                   \
        _num &= (_num - 1);          \
        _count++;                    \
    }                                \
    _count;                          \
})

Structure and type macros

1
2
3
4
#define SIZEOF(type) ((char *)(&type + 1) - (char *)(&type))

/* Find offset value of a member of a structure */
#define OFFSET_OF(type, member) ((size_t) &(((type *)0)->member))

List macros

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define LIST_FOREACH_SAFE(head, node) \
    for (struct ListNode *node = (head), *__temp = (node ? node->next : NULL); \
         node != NULL; \
         node = __temp, __temp = (node ? node->next : NULL))

/* Not delete safe */     
#define LIST_FOREACH(head, node) \
    for (struct ListNode *node = (head); node != NULL; node = node->next)
    
/* Find length of LL */   
#define LIST_LENGTH(head, len) \
    do { \
        (len) = 0; \
        struct ListNode *node = (head); \
        while (node) { \
            (len)++; \
            node = node->next; \
        } \
    } while (0)

Other macros

1
#define IS_LITTLE_ENDIAN() ((*(char *)&(int){1}) == 1)

Function Pointers and Callbacks

Function Returning a function pointer usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

typedef int (*OperationFunc)(char);

OperationFunc  additionFunction(int x)

// Function that returns a function pointer
int (*additionFunction(int x))(char) {
        int add(char c) {
        return c;
    }
    return add;
}

int main() {
    // Get the function pointer from the outer function
    int (*sumFunc)(char) = additionFunction(10); //This will call the function "additionFunction"
    
    // Call the function using the function pointer
    int result = sumFunc('A');
    
    printf("Result: %d\n", result); // Output: Result: 85 (ASCII value of 'A' is 65, 10 + 65 = 75)
    
    return 0;
}

Callback example programs to register ops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

typedef struct sample_ops {
    int data;
    void (*print)(int);
} sample_ops;

void printer(int a) {
    printf("Print %d\n", a); // Added the missing argument a
}

int main() {
    // Write C code here
    static const sample_ops my_ops = {
        .data = 60, // Replaced ; with ,
        .print = printer
    };
    
    my_ops.print(brcmf_ops.data); 
    return 0;
}
 

Tricky Code snippets

Find Length of linked list

1
2
int len=0;
for (struct ListNode *i = head; i; i=i->next, len++);

💡 Only one TYPE of variable can be declared in for loop init section. So we cant declare len there. In the second and third place holder of for loop, we can have multiple statements separated by comma. The below example declares two variables of same type and it is VALID.

1
for (int i = 0, sum = 0; i < n; i++) {

2D Array Allocation and Free

💡 dp[0][0] conversion to *((dp+0)+0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int** array = malloc(rows * sizeof(int*)); // Allocate memory for row pointers

    if (array == NULL) {
        perror("Memory allocation failed");
        return 1;
    }

    for (int i = 0; i < rows; i++) {
        array[i] = malloc(cols * sizeof(int)); // Allocate memory for each row
        if (array[i] == NULL) {
            perror("Memory allocation failed");
            return 1;
        }
    }

    // Access elements
    array[1][2] = 42; // Example usage
    printf("Value at array[1][2]: %d\n", array[1][2]);

    // Free memory
    for (int i = 0; i < rows; i++) {
        free(array[i]); // Free each row
    }
    free(array); // Free the row pointers

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int* array = malloc(rows * cols * sizeof(int)); // Single block for all elements

    if (array == NULL) {
        perror("Memory allocation failed");
        return 1;
    }

    // Access elements using index calculations
    array[1 * cols + 2] = 42; // Equivalent to array[1][2] in a "normal" 2D array
    printf("Value at array[1][2]: %d\n", array[1 * cols + 2]);

    // Free memory
    free(array);

    return 0;
}

3D Array Allocation and Free

Linux Kernel Synchronization Techniques

  • Multiple threads can run simultaneously on modern SMP systems (CONFIG_SMP=y) or Single core systems with CONFIG_PREEMPT enabled
  • This introduces race condition problem where two threads race to access and modify a single variable. There is no guarantee that the threads will modify the variable as expected. Behavior is undefined.
  • On such systems and scenarios where such a condition is encountered , we need to synchronize the two threads to make sure that they modify the shared variable , one at a time

Mutex

https://docs.kernel.org/locking/mutex-design.html

Mutex enforces serialization on shared memory systems(serial access meaning one at a time). Mutexes are sleeping locks which behave similarly to binary semaphores.

Mutexes are represented by ‘struct mutex’, defined in include/linux/mutex.h and implemented in kernel/locking/mutex.c

Mutexs have ownership. mutex→owner store the thread owning the mutex. Any modifications to the thread must be done by same thread.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct mutex {
    atomic_long_t       owner;
    spinlock_t      wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
    struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
    struct list_head    wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
    void            *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map  dep_map;
#endif
};

Contents of mutex Linux Kernel structure

🔹 atomic_long_t owner

  • Purpose: Stores a reference to the current owner thread of the mutex.
  • Use:
    • Detect recursion (same thread trying to relock).
    • Used in mutex_trylock() and lock debugging.
  • NULL (or 0) means no one owns the mutex.

🔹 spinlock_t wait_lock

  • Purpose: Protects the wait list (below).
  • Use: Ensures atomicity when updating or walking through the list of waiting tasks.
  • Lightweight and fast lock for short critical sections.

🔹 struct list_head wait_list

  • Purpose: Holds tasks that are blocked, waiting for the mutex. (list_head is a commonly used DLL gluebased list in kernel)
  • Use: When a thread tries to acquire a held mutex, it’s put on this list.
  • Used in mutex_lock() when a task sleeps waiting for the lock.

🔹 struct optimistic_spin_queue osq (under CONFIG_MUTEX_SPIN_ON_OWNER)

  • Purpose: Enables spin-then-sleep optimization.
  • Use: If a task thinks the owner will release the mutex soon (still running), it spins briefly instead of sleeping.
  • This improves performance on multicore systems under contention

🔹 void *magic (under CONFIG_DEBUG_MUTEXES)

  • Purpose: Used for internal debugging to detect corruption.
  • Use: A known magic value is stored; if it’s wrong later, something corrupted the mutex.

🔧 Only compiled in when:


🔹 struct lockdep_map dep_map (under CONFIG_DEBUG_LOCK_ALLOC)

  • Purpose: Used by lock dependency checker (lockdep) to detect deadlocks.
  • Use: Tracks lock acquisition ordering between different locks.
  • Helps detect potential AB-BA deadlocks (A → B and B → A).

The mutex subsystem in Linux Kernel checks and enforces the following rules:

  • Only the owner can unlock the mutex.
  • Multiple unlocks are not permitted.
  • Recursive locking/unlocking is not permitted.
  • A mutex must only be initialized via the API (see below).
  • A task may not exit with a mutex held.
  • Memory areas where held locks reside must not be freed.
  • Held mutexes must not be reinitialized.
  • Mutexes may not be used in hardware or software interrupt contexts such as tasklets and timers.

API for Linux Kernel Mutex

Statically define the mutex:

DEFINE_MUTEX(name);

Dynamically initialize the mutex:

mutex_init(mutex);

Acquire the mutex, uninterruptible:

void mutex_lock(struct mutex *lock); void mutex_lock_nested(struct mutex *lock, unsigned int subclass); int mutex_trylock(struct mutex *lock);

Acquire the mutex, interruptible:

int mutex_lock_interruptible_nested(struct mutex *lock, unsigned int subclass); int mutex_lock_interruptible(struct mutex *lock);

Acquire the mutex, interruptible, if dec to 0:

int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock);

Unlock the mutex:

void mutex_unlock(struct mutex *lock);

Test if the mutex is taken:

int mutex_is_locked(struct mutex *lock);

Disadvantages

Unlike its original design and purpose, ‘struct mutex’ is among the largest locks in the kernel. E.g: on x86-64 it is 32 bytes, where ‘struct semaphore’ is 24 bytes and rw_semaphore is 40 bytes. Larger structure sizes mean more CPU cache and memory footprint.

When to use mutexes

Unless the strict semantics of mutexes are unsuitable and/or the critical region prevents the lock from being shared, always prefer them to any other locking primitive.

Where not to use mutex:

Interrupt context — mutex sleeps! Use spinlock_t instead.

Atomic context / softirqs — not allowed.

Practical Examples of Mutex in Kernel Driver implementation.

  1. Kernel Ring Buffers - Producer consumer scenario. Say a shared skb buffer
1
2
skb_queue_tail(&dev->tx_queue, skb); // Producer
skb_dequeue(&dev->tx_queue);         // Consumer

Internally, these use spinlock (for performance) or mutex if the context allows sleeping.

  1. Common ring buffers

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     struct ring_buffer {
         char buf[1024];
         size_t head, tail;
         struct mutex lock;
     };
    	
     struct ring_buffer rb;
    	
     mutex_lock(&rb->lock);
     // read or write to buffer
     mutex_unlock(&rb->lock);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
     void produce(struct ring_buffer *rb, char c) {
         mutex_lock(&rb->lock);
         rb->buf[rb->head++] = c;
         if (rb->head >= sizeof(rb->buf))
             rb->head = 0;
         mutex_unlock(&rb->lock);
     }
    	
     char consume(struct ring_buffer *rb) {
         char c;
         mutex_lock(&rb->lock);
         c = rb->buf[rb->tail++];
         if (rb->tail >= sizeof(rb->buf))
             rb->tail = 0;
         mutex_unlock(&rb->lock);
         return c;
     }
    

Self-deadlock Concept

Self-deadlock is a where the thread which has already acquired the lock, tried to reacquire the lock again.(Recursive locking)

1
2
3
mutex_lock(&my_lock);
// Some code...
mutex_lock(&my_lock);  // Oops! Same thread trying to lock it again

Recursive Locking Concept

Recursive locking allows the same thread to acquire the same lock multiple times without causing a deadlock.

In complex systems or deep call chains, a thread might:

  • Hold a lock in a caller function.
  • Call another function (deeper in the call stack) that also tries to acquire the same lock.

Without recursive locking, this would deadlock.

With recursive locking, the system tracks how many times the lock has been acquired and only fully releases it when the lock is released the same number of times.

How Recursive Mutex Works

  • Every time the same thread locks it, an internal counter is incremented.
  • Each unlock() call decrements the counter.
  • The mutex is only fully released (and available to other threads) when the counter hits zero.

Available in pthreads but not by default, you need to declare mutex attribute object and set the recursive flag to enable recursive mutex

Circular deadlocks Concept

A series of tasks waiting on the next one in a circular manner.

  • Task A is waiting on Task B,
  • Task B is waiting on Task C,
  • Task C is waiting on Task A.

Mutex Lock and Unlock Paths

Lock Path Lock State Action Performance
Fast Path Unlocked Immediate acquisition ✅ Best
Mid Path Locked, short wait Spin (busy wait) ⚖️ Medium
Slow Path Locked, long wait Sleep & wake up later(context switch) 🐢 Worst

undefined | Unlock Path | Waiters | Action | Performance | | ———– | ——- | ————————— | ———– | | Fast Path | No | Clear owner, mark unlocked | ✅ Best | | Slow Path | Yes | Wake up one or more waiters | 🐢 Slower |

undefined When unlocking a mutex that has waiters, the Linux kernel tries to wake up the highest-priority waiter, not just the one that has waited the longest.

Mutex, Semaphore and Spinlock

**Semaphore**: Use a semaphore when you (thread) want to sleep till some other thread tells you to wake up. Semaphore ‘down’ happens in one thread (producer) and semaphore ‘up’ (for same semaphore) happens in another thread (consumer) e.g.: In producer-consumer problem, producer wants to sleep till at least one buffer slot is empty - only the consumer thread can tell when a buffer slot is empty.

**Spinlock**: Use a spinlock when you really want to use a mutex but your thread is not allowed to sleep. e.g.: An interrupt handler within OS kernel must never sleep. If it does the system will freeze / crash. If you need to insert a node to globally shared linked list from the interrupt handler, acquire a spinlock - insert node - release spinlock

Complex Declarations

Variable Declarations

1
2
3
4
5
6
7
8
1. int p; // An integer
2. int p[5]; // An array of 5 integers
3. int *p; // A pointer to an integer
4. int *p[10]; // An array of 10 pointers to integers
5. int **p; // A pointer to a pointer to an integer
6. int (*p)[3]; // A pointer to an array of 3 integers
7. int (*p)(char *); // A pointer to a function a that takes an integer
8. int (*p[5])(int); // An array of 5 pointers to functions that take an integer argument and return an integer

Single, Double and Triple Pointers with type qualifier

var declerations

Pointer to Array

1
int (*arr)[10];

Array of pointers or 2D Arrays

1
int *p[10];
1
2
3
4
5
6
int array[5][13];  // A 2D array with 5 rows and 13 columns
int dp[rows][cols];
void func(int rows, int cols, int dp[rows][cols]);
void func(int rows, int cols, int dp[][cols]);  // Rows can be omitted
int (*eaytb)[13];  // Pointer to an array of 13 integers
eaytb = array;     // Pointing eaytb to the first row of the arra

In this case, eaytb can be used to access the rows of the array. For instance, (*eaytb)[0] would give you the first row of the array, while (*eaytb)[1] would give you the second row, and so on. Passing

Comparison Table:

Method Memory Layout Indexing Ease of Passing Flexibility
Static Allocation Contiguous dp[i][j] Easy Fixed size at compile-time
Contiguous Memory (1D) Contiguous dp[i * cols + j] Requires manual indexing Flexible runtime size
Pointer-to-Pointer Non-contiguous dp[i][j] Easy Flexible runtime size
Variable-Length Arrays Contiguous dp[i][j] Easy Flexible runtime size (C99+)

undefined- Static arrays: Use for fixed-size arrays known at compile-time.

  • Flattened 1D arrays: Use for dynamic size with contiguous memory for better cache performance.
  • Pointer-to-pointer: Use when rows need independent allocation or resizing.
  • VLAs: Use for flexible sizes if your compiler supports C99 or later.

C Basic Data Type Sizes and Format Specifiers

Data Type Memory (bytes) Range Format Specifier
short int 2 -32,768 to 32,767 %hd
unsigned short int 2 0 to 65,535 %hu
unsigned int 4 0 to 4,294,967,295 %u
int 4 -2,147,483,648 to 2,147,483,647 %d
long int 4 -2,147,483,648 to 2,147,483,647 %ld
unsigned long int 4 0 to 4,294,967,295 %lu
long long int 8 -(2^63) to (2^63)-1 %lld
unsigned long long int 8 0 to 18,446,744,073,709,551,615 %llu
signed char 1 -128 to 127 %c
unsigned char 1 0 to 255 %c
float 4 - %f
double 8 - %lf
long double 12 - %Lf

undefined

What is the output of below code?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main()
{
    const char normal[] = "stdout ";
    const char error[] = "stderr ";

    fprintf(stdout,normal);
    fprintf(stderr,error);
    fprintf(stdout,normal);
    fprintf(stderr,error);
    fprintf(stdout,normal);
    fprintf(stderr,error);
	putchar('\n');

    return(0);
}

1
stderr stderr stderr stdout stdout stdout 

The reason is that stderr are not buffered, whereas stdout is always buffered and output when buffer is full or during a new line character.

FORK

fork() split the program flow into two processes. The commands following after the fork will be executed by two different processes. (Program will be split)

1
process = fork()
  • process value is 0 in child process.
  • It is 1 in parent process.

Extract IP from a 32byte array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void extract_ip(uint32_t ip) {
    // Extract each octet by shifting and masking
    uint8_t octet1 = (ip >> 24) & 0xFF; // First 8 bits
    uint8_t octet2 = (ip >> 16) & 0xFF; // Next 8 bits
    uint8_t octet3 = (ip >> 8) & 0xFF;  // Next 8 bits
    uint8_t octet4 = ip & 0xFF;         // Last 8 bits

    // Print the extracted IP address in dotted decimal format
    printf("%d.%d.%d.%d\n", octet1, octet2, octet3, octet4);
}

int main() {
    uint32_t ip = 3232235776;  // 192.168.1.0
    extract_ip(ip);
    return 0;
}

Max depth

1
2
3
4
5
6
7
8
9
10
int maxDepth(struct Node* root) {
    if (!root) return 0;
    if (root->numChildren == 0) return 1;
    int max=0;
    for (int i=0;i<(root->numChildren);i++){
        int temp = maxDepth(root->children[i]) +1;
        max = temp > max ? temp : max;
    }
    return max;
}

Find intersection of LL

Guess the Output

1
2
3
4
5
6
7
8
9
10
// Online C compiler to run C program online
#include <stdio.h>

int main() {
    // Write C code here
    int a=10;
    printf("a=%d\n",sizeof(a++));
    printf("new a = %d\n",a);
    return 0;
}

_p++ vs (*_p)++ vs *p++

* (dereference) has higher precedence than ++ (post-increment).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30};
    int *p = arr;

    printf("%d\n", *p++);  // Dereference, then increment pointer
    // means print *p
    // then do p++
    printf("%d\n", *p);
    return 0;
}

// output
//10
//20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main() {
	int arr[] = {10, 20, 30};
	int *p = arr;

	printf("%d\n", (*p)++);  // Dereference, then increment pointer
	// print (*p)
	// then do (*p) = (*p)++
	printf("%d\n", *p);
	return 0;
}

//output
//10
//11

Struct padding

Day 5: Bit Fields in C – Packing Data at the Bit Level

Bit fields in C provide a memory-efficient way to store small values within a single integer by specifying the exact number of bits a variable should use. This is useful in low-level programming, embedded systems, and protocols where memory is critical.

➧ What Are Bit Fields?

Instead of allocating a full int (usually 4 bytes or 32 bits), bit fields let you define variables with precise bit-width inside a struct.

====================================

Defining Bit Fields

struct {

unsigned int flag:1; // 1 bit for a flag

unsigned int mode:2; // 2 bits for mode (0-3)

} s;

s.flag = 1; // Stores 1 in a single bit

s.mode = 2; // Stores 2 using 2 bits (binary: 10)

====================================

flag:1 → Uses 1 bit (0 or 1).

mode:2 → Uses 2 bits (0 to 3).

The compiler packs these fields efficiently into an int.

➧ Using Bit Fields

====================================

int f = s.flag; // Reads 1

s.mode = 3; // Sets mode to 3 (binary: 11)

====================================

Bit fields behave like normal struct members.

The compiler manages masking and shifting automatically.

➧ Real-World Example – Network Packets

====================================

struct Packet {

unsigned int type:4; // 4 bits: 0-15 for packet type

unsigned int len:6; // 6 bits: 0-63 for length

} pkt = {5, 42}; // Type = 5, Length = 42

====================================

type:4 fits 0-15 in 4 bits.

len:6 fits 0-63 in 6 bits.

The struct is only 10 bits instead of 64 (if using full int values).

➧ Pros & Cons

Pros:

Memory-efficient → Uses only the required bits.

Ideal for hardware → Registers, protocol headers, etc.

Faster access → Smaller structures improve performance.

Cons:

Portability issues → Bit order depends on compiler/hardware.

No direct addressing → &s.flag won’t work.

Signed values → Behavior varies due to sign extension.

➧ Where Are Bit Fields Used?

Embedded Systems → Hardware registers.

Networking → Flags, headers, protocol data.

Data Compression → Packing multiple values efficiently.

Bit fields are powerful but require careful use. How do you use them in your projects?

D

This post is licensed under CC BY 4.0 by the author.