- Submission
- Starter Code
- Introduction
- Further Notes
- Running the test file
- Summary
- Appendix
- C for Java/Python Developers
- Structs (there are no classes)
- Pointers
- Arrays decay to pointers
- 2D Arrays vs Double Pointers
- Header Files and Include Guards
staticFunctionsconstCorrectnessmemcpyandmemset- Preprocessor Macros
pthread_mutex_t(Locking)- Compilation Model
- Makefile Basics
- Quick Syntax C Cheat Sheet
- Memory Management (malloc and free)
- Summary
- C for Java/Python Developers
50.005 Computer System Engineering
Information Systems Technology and Design
Singapore University of Technology and Design
Natalie Agus (Summer 2026)
Banker’s Algorithm
Recap
In deadlock avoidance, before granting a resource request (even if the request is valid and the requested resources are now available), we need to check that the request will not cause the system to enter a deadlock state in the future or immediately.
The Banker’s Algorithm is a resource allocation and deadlock avoidance algorithm. It can be used to predict and compute whether or not the current request will lead to a deadlock.
- If yes, the request for the resources will be rejected,
- Otherwise, it will be granted.
There are two parts of the Banker’s Algorithm:
- Resource Allocation Algorithm
- Safety Algorithm
You will be implementing the Safety Algorithm</b> and then calling it in Resource Allocation Algorithm in this lab.
Submission
You are to complete this lab’s questionnaire on eDimension and complete all the sections labeled TODO in the starter code. There are 4 TODO tasks in total.
Once you have completed all the TODO in banker.c, schedule a checkoff as a Team with your Lab TA by next week Friday 6PM.
✅ Checkoff
You are to demonstrate your code passing all 6 test cases using
make test.
Starter Code
System Requirements
This project requires a C11 compiler, make, and POSIX threads (pthread).
# Ubuntu / Debian
sudo apt install build-essential
# macOS
xcode-select --install
To verify installation, do the following:
gcc --version
make --version

Clone
Clone the starter code:
git clone https://github.com/natalieagus/cse-lab-banker-2026-starter.git
You should have the following files:
.[ROOT_PROJECT_DIR]
├── banker.c
├── banker.h
├── main.c
├── Makefile
├── README.md
├── test_banker.c
└── test_files
├── q0_answer.txt
├── q0.txt
├── q1_answer.txt
├── q1.txt
├── q2_answer.txt
├── q2.txt
├── q3_answer.txt
├── q3.txt
├── q4_answer.txt
├── q4.txt
├── q5_answer.txt
├── q5.txt
└── q6.txt
You will be required to modify only certain sections in banker.c.
It is important that you follow these rules to ensure that the tester works as intended:
- Leave ALL other files untouched
- DO NOT change any public API interfaces
- DO NOT
printanything else inbanker.c. Only type your answers in the given space labeled in the starter code asTODO X - DO NOT import any other modules
Test run
You can try compile the code using make. It should produce the binary banker upon successful compile. You can run banker with any of the test_files/[TEST_FILE].txt. See makefile and the readme.md` for further information.

You can ignore any other warnings for now like unused parameter/variable/function. You will implement those later.
C Basics
The lab requires you to know a little bit about C language. Since you already know how to code in Python and Java, it shouldn’t be really hard to pick up some C syntax on the go. Consult the Appendix when in doubt.
Introduction
System State and Initial Values
In order for Banker’s Algorithm to work, we need to know N (max process competing for resources) and M (max types of resources) beforehand, and how much each process i requires each resource type j at maximum.
Suppose we have N processes competing for M different types of resources. We call these processes “customers” (to these available resources). The maximum demand of process i for resource j can be illustrated using the max matrix:
| Process | Resource j_1 |
Resource j_2 |
… |
|---|---|---|---|
i_1 |
… | … | … |
i_2 |
… | … | … |
i_3 |
… | … | … |
We also need to represent the four system state:
- available: 1 by M vector
available[i]: the available instances of resourcei
- max: N by M matrix
max[i][j]: maximum demand of processifor resourcejinstances (shown above)
- allocation: N by M matrix
allocation[i][j]: current allocation of resourcejinstances for processi
- need: N by M matrix
need[i][j]: how much more of resourcejinstances might be needed by processi
For example, the following arrays display the available, max, allocation, and need values of a current state of a system with 5 processes: P0, P1, P2, P3, and P4, and 3 types of resources: A, B, C;

banker.c
Open banker.c and give it a quick read. You will fill up the #TODO sections in this file for this lab. There are several public APIs as well as internal helpers.
The Banker Struct
All the state for the Banker’s Algorithm lives in a single struct:
typedef struct {
int N; // number of customers
int M; // number of resource types
int available[MAX_RESOURCES]; // what is free right now
int maximum[MAX_CUSTOMERS][MAX_RESOURCES]; // declared max demand per customer
int allocation[MAX_CUSTOMERS][MAX_RESOURCES]; // currently held by each customer
int need[MAX_CUSTOMERS][MAX_RESOURCES]; // max - allocation
pthread_mutex_t lock; // protects everything above
} Banker;
There is no class, no constructor, no new in C. You declare a Banker variable, then call banker_init to set it up. From there, the typical flow looks like this:
Banker b;
// 1. Initialise with available resources
banker_init(&b, available_resources, n, m);
// 2. Register each customer's maximum demand
banker_set_maximum_demand(&b, 0, max_demand_0);
banker_set_maximum_demand(&b, 1, max_demand_1);
// 3. Customers request and release resources
banker_request_resources(&b, 0, request); // returns true if safe, false if denied
banker_release_resources(&b, 0, release);
// 4. Inspect current state at any point
banker_print_state(&b);
// 5. Clean up when done
banker_destroy(&b);
Notice that every function takes &b (the address of the struct) as its first argument. This is how C passes “the object” to operate on. The & gives the function a pointer so it can read and modify the actual struct, not a copy of it.
Head to appendix to know more about these if you’re interested.
Public APIs
These methods are also defined in banker.h. They are called by the main or tester function.
You will complete the implementation of Resource Request algorithm in banker_request_resource (TODO 2 and TODO 3) in this lab.
The description provided in banker.h is self-explanatory:
/* PUBLIC API */
/* Note: only public API typically get declared in the header files */
/**
* Initialise a Banker instance
*
* @param b pointer to Banker struct (caller-owned)
* @param available_resources array of length @p m with initial counts
* @param n number of customers
* @param m number of resource types
*/
void banker_init(Banker *b, const int *available_resources, int n, int m);
/** Clean up (destroy mutex) */
void banker_destroy(Banker *b);
/**
* Register the maximum demand vector for customer @p cid
* Must be called before any request/release for that customer
*/
void banker_set_maximum_demand(Banker *b, int cid, const int *max_demand);
/**
* Customer @p cid requests the resources described in @p request
*
* @return true if the request is granted (state remains safe)
* @return false if the request is denied (would be unsafe or invalid)
*/
bool banker_request_resources(Banker *b, int cid, const int *request);
/**
* Customer @p cid releases the resources described in @p release
* For simplicity the release is assumed valid (no bounds checking)
*/
void banker_release_resources(Banker *b, int cid, const int *release);
/**
* Print the current state (Available, Maximum, Allocation, Need)
* to stdout in the same format as the reference Python implementation
*/
void banker_print_state(const Banker *b);
/**
* Parse and execute a command file (same format as the Python version)
*
* @return 0 on success, non-zero on error
*/
int banker_run_file(const char *filename);
Internal Helpers
These functions will only be utilised inside banker.c.
You will complete the implementation of banker_snapshot and check_safe (TODO 1 and TODO 4) in this lab.
/* INTERNAL HELPERS */
/**
* array_le — "array less-than-or-equal" (element-wise).
*
* Returns true iff a[i] <= b[i] for all 0 <= i < len.
* This is the vector comparison Need[i] <= Work used inside the
* safety algorithm.
*/
static bool array_le(const int *a, const int *b, int len)v
/** Print a 1-D int array in Python-list style: [1, 2, 3] */
static void print_vector(const int *v, int len)
/** Print a 1-D int array to stderr for logging */
static void log_vector(const int *v, int len)
/* Takes a snapshot of the current bankers state */
/**
* @p work: a pointer to single-dimensional array of integers
* @p alloc: a pointer to rows that are each MAX_RESOURCES wide (alloc matrix)
* @p need: a pointer to rows that are each MAX_RESOURCES wide (need matrix)
*
* This function copies the content of current Banker's state: b->available, b->allocation, b->need into temp arrays: @p work, @p alloc, and @p need matrices
* @return none, arguments are all passed by reference
*/
static void banker_snapshot(const Banker *b, int *work, int alloc[][MAX_RESOURCES], int need[][MAX_RESOURCES])
/* SAFETY ALGORITHM */
static bool check_safe(int cid, const int *request, int *work, int alloc[][MAX_RESOURCES], int need[][MAX_RESOURCES], int N, int M)
Input Format
The inputs to banker.c are inside test_files/. All .txt files named as qx.txt are various input files.
The format are as follows:
- The first three lines contain values of
N(number of processes),M(number of resource type), and the contents of theavailablevector. - For lines starting with
c, it signifies themaxdemand of a process for ALL resource types.- The format is
c,pid,rid_1 rid_2 rid_3 ...
- The format is
- For lines starting with
r, it signifies a request by a process.- The format is:
r,pid,rid_1 rid_2 rid_3 ...
- The format is:
- For lines starting with
f, it signifies a release of resources by a process.- The format is:
f,pid,rid_1 rid_2 rid_3 ...
- The format is:
- For lines with just
p, we print the state of the system at that time
The lines in the input are read from top to bottom, and are treated as incoming requests or releases by each process as time progresses.
For instance, open test_files/q0.txt. It contains 10 lines of data:
1 n,3
2 m,3
3 a,0 0 0
4 c,0,2 2 4
5 c,1,2 1 3
6 c,2,3 4 1
7 r,0,1 2 1
8 r,1,2 0 1
9 r,2,2 2 1
10 p
Line 1 and 2 reports N and M: 3 distinct processes and 3 different types of resources
- Let’s name them as P0, P1, and P2 so that it is easier for us to address them.
- We will also label the 3 resources as A, B, and C.
From line 3, we know that NONE of the resources are available: available = [0,0,0].
From line 4-6, we can initialise the max matrix as follows:
max = [[2,2,4]
[2,1,3]
[3,4,1]]
This means P0 will need at most:
- 2 instances of Resource A,
- 2 instances of Resource B, and
- 4 instances of Resource C during its lifetime of execution.
Similar logic applies for P1 and P2.
Line 7: r,0,1 2 1 indicates that P0 then start requesting for 1 instance of Resource A, 2 instances of Resource B, and 1 instance of Resource C at this instance.
Each line that starts with r indicates the sequence (of time) of incoming requests. Banker’s algorithm MUST be run each time we process lines starting with r.
We will need to run the Banker’s algorithm now to determine whether this request is safe/not safe and therefore granted/not granted. The same is done for all lines starting with r. We can then print the state of the system at any point in time with p.
In this example, no resources are available. So obviously we are met with the same state of available, allocation and need after running the file because none of the resource requests can be fulfilled.
You can run the algorithm using the command:
./banker test_files/q0.txt
The output will report allocation matrix remaining at 0.
Customer 0 requesting
[1, 2, 1]
Customer 1 requesting
[2, 0, 1]
Customer 2 requesting
[2, 2, 1]
Current state:
Available:
[0, 0, 0]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Allocation:
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
Need:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Experiment with the other 5 files as well. Try them one by one and study the result. Compare it with qx_answer.txt.
./banker test_files/q1.txt
./banker test_files/q2.txt
./banker test_files/q3.txt
./banker test_files/q4.txt
./banker test_files/q5.txt
You will notice that the starter code will NOT produce the right answer for q3-q5. Your job is to fix that.
Resource Request Algorithm
This algorithm is already implemented for you in this function:
bool banker_request_resources(Banker *b, int cid, const int *request)
The resource allocation algorithm goes as follows.
Validity Checks
-
If
request[i] <= need[customer_index][i]for alli < M, go to step 2. Otherwise, returnFalse(request rejected) since the process has exceeded its maximum claim. -
If
request[i] <= available[i]for alli < M, go to step 3. Otherwise, returnFalse.- Process i must wait since its requested resources are not immediately available (request rejected).
This is already implemented for you:
/* Validity checks */
for (int j = 0; j < b->M; j++)
{
if (request[j] > b->need[cid][j])
{
LOG_INFO("DENIED: Customer %d request[%d]=%d exceeds need[%d]=%d",
cid, j, request[j], j, b->need[cid][j]);
LOG_EDUCATIONAL("A process must never request more than its declared maximum need.");
pthread_mutex_unlock(&b->lock);
return false;
}
if (request[j] > b->available[j])
{
LOG_INFO("DENIED: Customer %d request[%d]=%d exceeds available[%d]=%d",
cid, j, request[j], j, b->available[j]);
LOG_EDUCATIONAL("Not enough resources available right now — customer must wait.");
pthread_mutex_unlock(&b->lock);
return false;
}
}
Demo Example: REJECT, request > need + alloc
This example demonstrate the case where a request is rejected because a particular process is requesting more than its maximum.
You may run: ./banker test_files/q1.txt and study the output.
Initially, we know that allocation was initialised as 0 and that available=[5,5,5] from the third line in test_files/q1.txt. The first 3 requests are granted:
Customer 0 requesting
[1, 2, 1]
Customer 1 requesting
[2, 0, 1]
Customer 2 requesting
[2, 2, 1]
After granting the three requests above, we have the system state:
Current state:
Available:
[0, 1, 2]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Allocation:
[1, 2, 1]
[2, 0, 1]
[2, 2, 1]
Need:
[1, 0, 3]
[0, 1, 2]
[1, 2, 0]
Further request by Process 0: request=[0,1,0] is rejected because Process 0 requests MORE than what’s been declared at maximum.
- It declared that it needs at maximum of 2 resources B (the second type of resource)
- It already held 2 types of resources B –>
Allocation[0][1]: 2
Customer 0 requesting
[0, 1, 0]
Current state: <--- request above is rejected, hence state remains the same.
Available:
[0, 1, 2]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 4, 1]
Allocation:
[1, 2, 1]
[2, 0, 1]
[2, 2, 1]
Need:
[1, 0, 3]
[0, 1, 2]
[1, 2, 0]
Preparation to call Safety Algorithm
At this point, the requested resources are available, but we need to check if granting the request is safe (will not lead to deadlock) by calling the safety check algorithm.
Before that, we need to do some prep work:
- Allocate a memory region to make temporary
available,allocation, andneed - Copy the
available,allocation, andneedvalues to these region
We already declare the temp variables for you.
/* Declare temp variables */
int work[MAX_RESOURCES];
int temp_alloc[MAX_CUSTOMERS][MAX_RESOURCES];
int temp_need[MAX_CUSTOMERS][MAX_RESOURCES];
Task 1
Complete the function banker_snapshot which should copy the current Banker’s state into these new temp vars:
/* Takes a snapshot of the current bankers state */
/**
* @p work: a pointer to single-dimensional array of integers
* @p alloc: a pointer to rows that are each MAX_RESOURCES wide (alloc matrix)
* @p need: a pointer to rows that are each MAX_RESOURCES wide (need matrix)
*
* This function copies the content of current Banker's state: b->available, b->allocation, b->need into temp arrays: @p work, @p alloc, and @p need matrices
* @return none, arguments are all passed by reference
*/
static void banker_snapshot(const Banker *b, int *work, int alloc[][MAX_RESOURCES], int need[][MAX_RESOURCES])
{
/*
TODO 1: Implement memcpy here
*/
/* END OF TODO 1 */
}
Task 2
Once you have completed banker_snapshot, call it in banker_request_resource:
bool banker_request_resources(Banker *b, int cid, const int *request)
{
// other code
/* Safety check */
LOG_EDUCATIONAL("Request passes basic checks, running safety algorithm...");
/* Declare temp variables */
int work[MAX_RESOURCES];
int temp_alloc[MAX_CUSTOMERS][MAX_RESOURCES];
int temp_need[MAX_CUSTOMERS][MAX_RESOURCES];
/* Snapshot current state */
/*
TODO 2: Call banker_snapshot here
*/
// other code
}
Calling the Safety Algorithm
We then pass these new data structures (work, alloc, need) to the safety_check algorithm, which will return True (safe) or False (unsafe).
If the outcome of the safety_check algorithm is True: UPDATE all system states concerning Process i (request granted):
available[i] = available[i] - request[i]for alli<Mneed[customer_index][i] = need[customer_index][i] - request[i]for alli<Mallocation[customer_index][i] = allocation[customer_index][i] + request[i]for alli<M
Otherwise, if it returns False: This means request rejected, and process i has to try again in the future. This is because granting the request results in deadlock in the future.
Task 3
Call the safety algorithm with appropriate parameters inside banker_request_resources function:
/*
TODO 3: Call check_safe algorithm here
*/
bool safe = true; // modify this line
/* END OF TODO */
if (safe)
{
/* Apply the allocation for real */
for (int j = 0; j < b->M; j++)
{
b->allocation[cid][j] += request[j];
b->need[cid][j] -= request[j];
b->available[j] -= request[j];
}
LOG_INFO("GRANTED: Customer %d request.", cid);
}
else
{
LOG_INFO("DENIED (unsafe): Customer %d request would lead to unsafe state.", cid);
LOG_EDUCATIONAL("The request is denied because no safe sequence exists after the"
" hypothetical allocation. Granting it could lead to DEADLOCK.");
}
The variable safe is always set to True in the starter code. This means we always grant the request as long as the resources are available and the processes do not violate their max. For input q0, q1, q2, we are lucky because we never had any requests that might result in deadlock.
However, if you try running the program with q3, you will notice that the printed output is not the same as the answer: test_files/q3_answer.txt. We need to implement and call the check_safe algorithm to ensure that we get the right answer.
Task 4: The Safety Algorithm
This algorithm is called each time we encounter a resource request (line starting with r), but only if we managed to pass the validity checks of the Resource Request Algorithm. It is implemented in the function check_safe:
/* SAFETY ALGORITHM
* @return true if safe, false otherwise
*/
static bool check_safe(int cid, const int *request, int *work, int alloc[][MAX_RESOURCES], int need[][MAX_RESOURCES], int N, int M)
{
/* Finish array to determine termination */
bool finish[MAX_CUSTOMERS];
/* Variables to track safe sequence */
int safe_sequence[MAX_CUSTOMERS];
int seq_len = 0;
/* Return value */
bool safe = true;
/*
TODO 4: Implement Safety Algorithm here
*/
/* END OF TODO 4 */
// other logging code
return safe;
}
You can implement Task 4 by following the steps below.
Step 1: Create Helper Variables
Four helper variables are created for you:
/* Finish array to determine termination */
bool finish[MAX_CUSTOMERS];
/* Variables to track safe sequence */
int safe_sequence[MAX_CUSTOMERS];
int seq_len = 0;
/* Return value */
bool safe = true;
Step 2: Prepare finish vector
You need to initialise finish: set all elements to False. This indicates that none of the processes has finished yet.
Step 3: Hypothetically grant the current request
Hypothetically grant the current request by customer customer_index by updating:
work[i] = work[i] - request[i]for alli<Mneed[customer_index][i] = need[customer_index][i] - request[i]for alli<Mallocation[customer_index][i] = allocation[customer_index][] + request[i]for alli<M
Note that this request granting is hypothetical because we are modifying a copy of the Banker’s state via work, temp_alloc and temp_need. In reality, we haven’t granted the request yet, we simply compute this hypothetical situation and decide whether it will be safe or unsafe.
Step 4: Try to find process i that can finish
Find an index i (which is a customer) such that:
finish[i] == Falseandneed[i][j] <= work[j]for allj<M.- The two above condition signifies that an incomplete Customer
ican complete even after this request bycustomer_indexis granted
If such i from exists do the following:
- Set:
work[j] = work[j] + allocation[i][j]for allj<M.- This signifies that a Customer
ithat can complete will free its currently allocated resources.
- This signifies that a Customer
- Set:
finish[i] = True
If such i does not exist, go to Step 5. The algorithm should terminate soon.
Then, repeat this Step 4
Tracking Safe Sequence
You might want to store the values of i each time you execute this Step 4 in safe_sequence array to store a possible safe execution sequence. It will greatly help your debugging process. Head to the lecture notes for a summary.
This is not required to pass the tester for this lab.
Step 5: Termination
If you reach here, that means there’s no more process i that can finish. Check the finish array:
- If
finish[i] == Truefor alli<N, then it means the system is in a safe state. ReturnTrue. - Else, the system is not in a safe state. Return
False.
Potential Careless Mistake
Step 4 contains a while loop.
- We need to find Process
ithat can finish. - If we do, we need to update
workmatrix. - When
workmatrix is updated, we can find more Processithat can finish.
Checking Implementation
Once you have completed all the tasks, run the Banker algorithm with q3:
./banker test_files/q3.txt
In this test file, P0 is making the request [1,0,3] and it is granted as shown in the allocation matrix:
Customer 0 requesting
[1, 0, 3]
Current state:
Available:
[4, 2, 1]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 1, 1]
Allocation:
[1, 0, 3]
[0, 0, 0]
[0, 0, 0]
Need:
[1, 2, 1]
[2, 1, 3]
[3, 1, 1]
You can verify whether any requests so far is granted by checking that the
allocationmatrix value adds up to the requests made.
Then, P1 requests for [1,1,1]. This request will pass resource allocation algorithm’s validity checks, but it should NOT pass the safety check since it leads to an unsafe state.
Hence the request is rejected and the state of the system should remain the same:
Customer 1 requesting
[1, 1, 1]
Current state:
Available:
[4, 2, 1]
Maximum:
[2, 2, 4]
[2, 1, 3]
[3, 1, 1]
Allocation:
[1, 0, 3]
[0, 0, 0]
[0, 0, 0]
Need:
[1, 2, 1]
[2, 1, 3]
[3, 1, 1]
The same logic applies to samples in q4 and q5 as well:
- In q4, the last request by P0:
[0,2,0]leads to anunsafestate and therefore rejected. - In q5, the request by P1:
[2,0,1]is rejected because it isunsafe. However, after P0 release the resources it held:[1,3,4], a repeated request by P1 for the same set of resources[2,0,1]is granted (safe).
Further Notes
Rejecting Requests
If a resource request is rejected, dont panic, it’s not the end of the world. The process/consumer just need to try to request it again in the future.
How can this be implemented? Usually schedulers are programmed to tackle this kind of cases; e.g: they can be placed to a special queue and will periodically prompt for resource request until granted, or there exists some kind of event-driven solution – it’s free-for-all to implement.
Resource Release
On the contrary, resource release request is always granted. This is implemented by banker_release_resource function:
void banker_release_resources(Banker *b, int cid, const int *release)
{
/* Print the release */
printf("Customer %d releasing\n", cid);
print_vector(release, b->M);
pthread_mutex_lock(&b->lock);
for (int j = 0; j < b->M; j++)
{
b->allocation[cid][j] -= release[j];
b->need[cid][j] += release[j];
b->available[j] += release[j];
}
LOG_INFO("Customer %d released resources.", cid);
pthread_mutex_unlock(&b->lock);
}
For the simplicity of this lab, we assume that a process will not release more than what has been allocated to them. Each test files follows this assumption.
Synchronisation using mutex
Since max, allocation, need and available are shared data structures among all these methods, we use a mutex lock to protect it. The mutex is created under banker_init:
void banker_init(Banker *b, const int *available_resources, int n, int m)
{
memset(b, 0, sizeof(Banker));
b->N = n;
b->M = m;
for (int j = 0; j < m; j++)
b->available[j] = available_resources[j];
/* Create a mutex to protect the thread executing Banker's algorithm */
pthread_mutex_init(&b->lock, NULL);
LOG_INFO("Banker initialised: %d customers, %d resource types", n, m);
if (LOG_LEVEL >= 1)
{
fprintf(stderr, "[INFO] Initial available = ");
log_vector(b->available, m);
fprintf(stderr, "\n");
}
}
We guard the critical sections of the Resource Request algorithm with pthread_mutex_lock and pthread_mutex_unlock to make it thread safe:
bool banker_request_resources(Banker *b, int cid, const int *request)
{
/* Attempt to obtain lock */
pthread_mutex_lock(&b->lock);
// Implementation
/* Release lock */
pthread_mutex_unlock(&b->lock);
return safe;
}
Running the test file
You can run all test files at one go using the command:
make test
This will run your compiled banker against all 6 test cases: q0 to q5. If all goes well, the following message will be printed:

Note that the tester performs a simple output string matching. Therefore it is crucial for you not to print anything else (comment it out) before running the tester.
Debugging
You should use a proper debugger to debug the files. One way is to use VSCode C/C++ debugger.

You can use the following launch.json and tasks.json files. Create it under .vscode/ folder in your project directory.
launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug banker",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/banker",
"args": ["test_files/q3.txt"],
"cwd": "${workspaceFolder}",
"preLaunchTask": "make"
}
]
}
tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "make",
"type": "shell",
"command": "make",
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": ["$gcc"],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
Summary
In this lab, you have implemented the Banker’s Algorithm using C. There are 4 Tasks in total (TODO 1 - TODO 4) and you must complete them all for the checkoffs with our TAs.
The Banker’s Algorithm is used in operating systems to avoid deadlocks by safely allocating resources to processes. It’s a deadlock avoidance technique that checks whether granting a resource request will keep the system in a safe state: i.e., whether all processes can still eventually complete.
It’s mainly taught as a theoretical model and rarely used in modern OS kernels due to its high runtime complexity and need for advanced knowledge of future resource requests. However, it serves as a foundation for understanding resource allocation, particularly in systems where resource usage patterns are predictable, such as embedded or real-time systems with strict guarantees.
Usage in Modern System
The Banker’s Algorithm is not used in modern general-purpose operating systems like Linux, Windows, or macOS. It’s mostly a teaching tool or used in very controlled environments like embedded or real-time systems where all resource requirements are known in advance. In practice, mainstream OS avoid deadlocks using a combination of:
- Deadlock prevention: Structuring the system to make deadlocks impossible (e.g., by enforcing resource ordering).
- Deadlock avoidance (rare): Like Banker’s, but impractical due to overhead.
- Deadlock detection and recovery: Allowing deadlocks to happen, then detecting and breaking them (e.g., killing a process).
- Ostrich algorithm: Ignoring deadlocks entirely if they are rare and impact is low (common in general OSes).
So in modern OS, deadlocks are often just detected (not prevented) using techniques like wait-for graphs, especially in database engines or advanced resource managers.
Appendix
C for Java/Python Developers
A quick reference for the C concepts used in this project. This is not a full C tutorial. It covers the things that will look unfamiliar if you are coming from Java or Python.
Structs (there are no classes)
C has no classes, no methods, no inheritance. A struct is just a block of named fields packed together in memory. It has the following syntax and we use it to hold the states of our Bank:
typedef struct {
int N;
int M;
int available[MAX_RESOURCES];
int allocation[MAX_CUSTOMERS][MAX_RESOURCES];
pthread_mutex_t lock;
} Banker;
In Java you would write banker.requestResources(...) as a method call on the object. In C, you write a free function and pass the struct as the first argument:
bool banker_request_resources(Banker *b, int cid, const int *request);
Every function that operates on a Banker takes a Banker *b as its first parameter. This is the C equivalent of this or self. You access fields with the arrow operator: b->available[j], b->N, etc.
The arrow -> is shorthand for dereferencing then accessing: b->N means (*b).N.
Pointers
A pointer holds a memory address. That is all it is. We use the syntax * for a pointer. We can dereference a pointer by putting another * symbol in front of it.
int x = 42;
int *p = &x; // p holds the address of x
*p = 10; // dereference p, now x == 10
| Syntax | Meaning |
|---|---|
int *p |
Declare p as a pointer to int |
&x |
“Address of x” |
*p |
“The value at the address p holds” (dereference) |
p->field |
Shorthand for (*p).field |
In Java and Python, objects are always passed by reference. In C, everything is passed by value by default. If you write:
void foo(Banker b) { ... } // receives a COPY of the entire struct
The function gets its own copy. Any changes are lost when it returns. To modify the original, you pass a pointer:
void foo(Banker *b) { ... } // receives the ADDRESS of the struct
This is why every public API function takes Banker *b, not Banker b because we need to modify the banker’s state.
Arrays decay to pointers
When you pass an array to a function, C does not copy it. The array name “decays” into a pointer to its first element:
void print_vector(const int *v, int len);
int arr[3] = {1, 2, 3};
print_vector(arr, 3); // arr decays to &arr[0]
This is also why functions cannot know the length of an array from the pointer alone. You always pass the length separately.
2D Arrays vs Double Pointers
A 2D array like int alloc[64][64] is a single contiguous block of 64 * 64 integers in memory. It is not an array of pointers. An array of pointers is basically an array of addresses where each element is an address that can point anywhere else and not necessarily contiguous.
When passing a 2D array to a function, the first dimension decays to a pointer but the compiler still needs the second dimension to compute offsets:
// These two signatures are equivalent:
void foo(int alloc[][MAX_RESOURCES], int rows);
void foo(int (*alloc)[MAX_RESOURCES], int rows);
Both mean: a pointer to rows, where each row is MAX_RESOURCES ints wide.
On the other hand, an int ** (double pointers) is a completely different memory layout (an array of pointers to separately allocated rows). You cannot interchange them no matter what AI says.
| Declaration | Memory layout | Access [i][j] |
|---|---|---|
int a[N][M] |
One flat block of N*M ints | base + i*M + j (one offset) |
int **a |
Pointer to array of row pointers | Follow outer ptr, then follow row ptr (two dereferences) |
Header Files and Include Guards
C splits declarations (what exists) from definitions (how it works). Here we provide you with banker.h and banker.c:
| File | Contains | Analogy |
|---|---|---|
banker.h |
Struct definition, function signatures, macros | Java interface / Python ABC |
banker.c |
Function bodies (the actual code) | Java class implementation |
Any .c file that wants to use Banker writes #include "banker.h". The preprocessor literally copy-pastes the header content into that file before compilation.
The include guard prevents double-inclusion:
#ifndef BANKER_H // if BANKER_H is not yet defined...
#define BANKER_H // define it now
// ... all declarations ...
#endif // end of guard
If a file includes banker.h twice (directly or indirectly), the second time BANKER_H is already defined, so the #ifndef is false and the entire block is skipped. Without this you get “duplicate definition” errors during compilation.
static Functions
In Java, private hides a method from other classes. In C, the keyword static on a function means “visible only within this .c file” so it kinda works the same way as Java’s `private:
static bool check_safe(...) { ... } // only banker.c can call this
static void print_vector(...) { ... } // only banker.c can call this
bool banker_request_resources(...) { ... } // anyone who includes banker.h can call this
If it is not in the header and it is marked static, it is internal. If it is in the header without static, it is part of the public API. This is C’s version of access control.
const Correctness
const tells the compiler (and the reader) that a value will not be modified through this pointer:
void banker_init(Banker *b, const int *available_resources, int n, int m);
Banker *b means the function will modify the struct (initialising it).
const int *available_resources means the function will not modify the array it reads from.
If you accidentally try to write through a const pointer, the compiler gives you an error.
| Declaration | What is const |
|---|---|
const int *p |
Cannot modify the int through p (the data) |
int *const p |
Cannot change where p points (the pointer itself) |
const int *const p |
Both |
In this project you will mostly see const int *, meaning “read-only view of this data.”
memcpy and memset
C has no built-in array copy or array fill. You use these two library functions from <string.h>:
// Copy n bytes from src to dst
memcpy(dst, src, num_bytes);
// Set n bytes of dst to value (usually 0)
memset(dst, 0, num_bytes);
In the project, banker_snapshot should use memcpy to deep-copy the banker’s matrices into local buffers:
memcpy(work, b->available, sizeof(int) * (size_t)b->M);
This copies b->M integers (each sizeof(int) bytes) from b->available into work. It is the C equivalent of Python’s copy.deepcopy() for flat arrays.
memset is used to zero-initialise:
memset(finish, false, sizeof(bool) * (size_t)N);
Alternatively, you can just loop through the array and set each value to zero, but memset is just a neater way to do it.
Preprocessor Macros
Lines starting with # are handled by the preprocessor before compilation. They do text substitution to make it easier for programmers to code using symbols instead of plain numbers. Here we list the use cases.
Simple constants
#define MAX_CUSTOMERS 64
Every occurrence of MAX_CUSTOMERS in the code is replaced with 64 before the compiler sees it. Similar to static final int in Java or a module-level constant in Python, but it is pure text replacement.
Parameterised macros
#define LOG_INFO(...) \
do { \
if (LOG_LEVEL >= 1) { \
fprintf(stderr, "[INFO] "); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
} \
} while (0)
__VA_ARGS__ captures whatever arguments you pass. The do { ... } while (0) wrapper is a standard C trick so the macro behaves like a single statement (safe to use after if without braces).
Conditional compilation
#ifndef LOG_LEVEL
#define LOG_LEVEL 0
#endif
This sets a default. But if you compile with -DLOG_LEVEL=2, the compiler defines it before the file is processed, so the #ifndef is false and the default is skipped. This is how make verbose changes behaviour without editing code.
pthread_mutex_t (Locking)
Consult the lecture notes about mutex if you forgot what it does.
In Python you write lock.acquire() / lock.release(). In Java you write synchronized. In C with POSIX threads we do:
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL); // create the lock (once)
pthread_mutex_lock(&lock); // acquire (blocks if held)
pthread_mutex_unlock(&lock); // release
pthread_mutex_destroy(&lock); // clean up (once, when done)
In this project the mutex is a field inside the Banker struct. Every function that reads or writes the shared matrices locks it first and unlocks before returning. This is the same concept as Python’s threading.Lock() or Java’s synchronized block, just explicit.
Compilation Model
Java compiles each .java into a .class and the JVM links them at runtime. Python just interprets. C has a multi-step build:
banker.c --> [compiler] --> banker.o (object file)
main.c --> [compiler] --> main.o (object file)
|
[linker]
|
banker (executable)
Step 1 (compile): Each .c file is compiled independently into a .o (object file) containing machine code. It only needs the .h headers to know what functions exist, not how they work.
Step 2 (link): The linker combines all .o files and resolves function calls across files. If main.o calls banker_request_resources, the linker finds the actual code in banker.o and connects them. Linker is a huge topic by itself, you don’t know what it means (big picture), it might be worth searching online.
This is why you can change main.c without recompiling banker.c. The Makefile tracks these dependencies.
Makefile Basics
A Makefile describes build rules, we have seen this in our first lab. Each rule has a target, dependencies, and commands:
banker.o: banker.c banker.h
gcc -std=c11 -c banker.c -o banker.o
This says: “to produce banker.o, you need banker.c and banker.h. Run this gcc command.” If neither dependency has changed since the last build, make skips the rule.
| Flag | Meaning |
|---|---|
-c |
Compile only, do not link (produce .o) |
-o name |
Name the output file |
-Wall -Wextra |
Enable warnings (catch bugs early) |
-std=c11 |
Use the C11 standard |
-g |
Include debug info (for gdb) |
-pthread |
Link the POSIX threads library |
-DLOG_LEVEL=2 |
Define a preprocessor macro from the command line |
It is very common to ask AI to write a makefile for you, and we think it is a suitable usage of AI.
Quick Syntax C Cheat Sheet
You can refer to this site for more stuffs but these are sufficient for this lab:
| Java / Python | C |
|---|---|
System.out.println(x) / print(x) |
printf("%d\n", x); |
new int[n] / [0]*n |
int arr[n]; (stack) or malloc(n * sizeof(int)) (heap) |
arr.length / len(arr) |
No equivalent. Track length yourself. |
true / false / True / False |
true / false (with #include <stdbool.h>) |
null / None |
NULL |
String |
char * or char arr[N] (null-terminated) |
try/catch |
No exceptions. Check return values. |
| Garbage collection | Manual: malloc() to allocate, free() to deallocate |
import |
#include "file.h" or #include <stdlib.h> |
this / self |
Pass struct pointer explicitly as first argument |
Memory Management (malloc and free)
In Java and Python, you create objects freely and the garbage collector cleans them up. C has no garbage collector. If you allocate memory on the heap, you are responsible for freeing it.
// Allocate an array of 10 ints on the heap
int *arr = malloc(10 * sizeof(int));
if (arr == NULL) {
// malloc returns NULL if it fails
fprintf(stderr, "Out of memory\n");
return 1;
}
// Use it...
arr[0] = 42;
// When done, free it. This is YOUR job.
free(arr);
arr = NULL; // good habit: avoid dangling pointer
There are two places memory can live:
| Location | How | Lifetime | Cleanup |
|---|---|---|---|
| Stack | int arr[64]; |
Automatic. Dies when the function returns. | Nothing to do (remember your 50.002) |
| Heap | int *arr = malloc(...) |
Lives until you call free(). |
You must call free() or it leaks. |
If you malloc but never free, that memory is leaked. It stays allocated until the process exits. In a long-running program this adds up and eventually you run out of memory. In a short lab program you might not notice, but it is still a bug.
If you free and then access the pointer again, that is a use-after-free. The memory might have been reused for something else. This causes mysterious crashes or corrupted data. Setting the pointer to NULL after freeing helps catch this earlier.
In this lab we mostly avoid heap allocation entirely. The Banker struct is declared on the stack in banker_run_file, and the temporary arrays in check_safe are also stack-allocated with fixed sizes like int work[MAX_RESOURCES]. This is a deliberate design choice: by using fixed-size arrays with MAX_CUSTOMERS and MAX_RESOURCES, we avoid malloc/free altogether and eliminate an entire category of bugs. The tradeoff is that we waste some memory if the actual number of customers is much smaller than 64, but for a teaching codebase that simplicity is worth it.
You will play more with memory management later on during Programming Assignment 1.
Summary
C is not an easy language to use if you’re used to Python and Java because it is the lowest-level language most programmers will ever use. It beats assembly any day though. Since you survived Beta Assembly, C language shouldn’t be too hard to learn. It just takes way more instructions to implement the same thing in modern languages like Python and Java.
Just keep in mind these few things for now:
- No objects. Structs hold data, free functions operate on them. Pass the struct pointer as the first argument.
- No automatic memory. You manage lifetimes. In this project we mostly use stack allocation (local arrays), which is automatically cleaned up when the function returns.
- Pointers are explicit. You choose whether to pass by value or by pointer. You dereference with
*and take addresses with&. - Headers are manual. You split declarations and definitions yourself and use include guards.
- The preprocessor is powerful. Macros, conditional compilation, and build-time configuration all happen before the compiler runs.
50.005 CSE