Lab 2

The Process Abstraction and Creating Processes

CSCE 313 · Introduction to Computer Systems

Released
Tuesday, September 8, 2026, 9:00 AM CT
Due
Monday, September 21, 2026, 11:59 PM CT

1. Objectives

By the end of this lab you will be able to:

  1. Create a child process with fork() and tell parent from child by its return value.
  2. Replace a child’s process image with execvp(), passing arguments the way the shell would.
  3. Explain what a child inherits from its parent, and what it does not.
  4. Observe process isolation directly, by printing the same variables from both processes.
  5. Drive a client-server exchange: build a request, send it, and read the response.
  6. Shut a system down cleanly, so that no process is left running and no parent waits forever.

Every lab this term builds on one project: a financial management system. This is where it starts. You write the client. The three servers are written for you.

2. Background

2.1 The client-server model

A client handles the user. A server owns a resource and answers requests about it. The client never touches the resource itself; it asks. That buys two things: the resource lives in one place, and each server can be understood, changed, and crashed on its own.

This lab has one client and three servers, each owning a different resource.

Architecture of the lab 2 system: one client process forks three server processes, each owning one resource The client process sits at the bottom. Three arrows labelled RequestChannel run from it to three server processes above: the file server, the logging server and the finance server. Each server has a two-way arrow to the resource it owns: the file server to the storage directory, the logging server to the log file, and the finance server to its array of accounts. The channel names are file, logging and finance. storage/ example.log example_execution.txt log file system.log survives between runs accounts array accounts[0 .. max] lost when it exits file server ./fileserver logging server ./logging finance server ./finance "file" "logging" "finance" three RequestChannels, one per server client process ./client
Figure 1 — The client forks and execs all three servers, then talks to each over its own channel. A channel is identified by name, and the name in the client must match the name in the server exactly.

Notice what each server owns. The logging server’s file survives the run; the finance server’s accounts do not, because they live in that process’s memory and die with it. That difference is the whole reason the log file exists.

2.2 Requests and responses

Both types are in common.h. A request carries everything a server might need; most fields go unused for any given request type.

struct Request {
    RequestType type;      // QUIT, DEPOSIT, WITHDRAW, BALANCE,
                           // UPLOAD_FILE, DOWNLOAD_FILE, LOGIN, LOGOUT
    int         user_id;
    double      amount;
    std::string filename;
    std::string data;

    Request(RequestType t, int uid = 0, double amt = 0.0,
            std::string fname = "", std::string d = "");
};

Only type is required; the rest default. So a quit is just Request(QUIT), and a balance enquiry is Request(BALANCE, user_id).

A response comes back with four fields, and again only some are filled:

struct Response {
    bool        success;   // did the server do what was asked?
    double      balance;   // finance server only
    std::string data;      // file contents, on a download
    std::string message;   // short explanation, useful when success is false
};

2.3 fork() and execvp()

fork() is called once and returns twice, because after it there are two processes. The return value is the only thing that differs, and it is how each process learns which one it is:

pid_t pid = fork();
if (pid < 0)  { /* fork failed  */ }
if (pid == 0) { /* the child    */ }
if (pid > 0)  { /* the parent   */ }

The child is a copy: same code, same variables, same next line to execute. What it is not is the same memory. Change a variable in the child and the parent’s copy does not move. Task 2 has you prove that to yourself.

execvp() then throws that copy away and loads a different program into the process:

char* args[] = {(char*)"./program", (char*)"arg1", (char*)"arg2", nullptr};
execvp(args[0], args);
perror("execvp");   // only reached if execvp FAILED
exit(1);

Two things to hold on to. The array must end with nullptr, and args[0] is by convention the program’s own name. And a successful execvp never returns — there is no longer any code to return to. So any line after it runs only on failure, which is exactly why the two lines above are there.

2.4 The RequestChannel

channel.h gives you the whole interface you need:

RequestChannel(const std::string process_name, const Side side);
Response send_request(const Request& req);

A channel is identified by its name. The client and the server must construct it with the same string, or they are not talking to each other. The names the servers use are in their source: "finance", "file", "logging". Note the file server’s channel is called "file" even though its executable is ./fileserver.

Each name must be opened exactly once from each side — one CLIENT_SIDE, one SERVER_SIDE. Opening the same side twice is undefined behaviour.

You only ever call send_request. receive_request and send_response are the servers’ half of the conversation, and they are already written.

3. Environment setup

3.1 Accept the assignment

  1. Sign in to classroom50.org with your GitHub account.
  2. Accept Lab 2: https://classroom50.org/CSCE-313-FA26/csce-313-fa26/assignments/lab-2/accept. Your repository is created for you.
  3. Clone it:
git clone https://github.com/CSCE-313-FA26/<your-lab-2-repository>.git
cd <your-lab-2-repository>

3.2 Install clang

The RequestChannel implementation ships as LLVM IR, which clang assembles:

sudo apt update
sudo apt install clang

You do not need clang for anything else in this lab, and you never edit the IR.

3.3 Repository layout

Path What it is
client.cpp The only file you modify. Every TODO is here.
finance.cpp Finance server. Complete — do not modify.
file.cpp File server. Complete — do not modify.
logging.cpp Logging server. Complete — do not modify.
channel.h The RequestChannel interface you call
channel.ll, channel.ll.arm Its implementation, as LLVM IR, for x86-64 and ARM
common.h, common.cpp Request, Response, RequestType
Makefile Builds all four programs
lab2-tests.sh The same tests the autograder runs. Run them yourself.
storage/example_execution.txt A full session, showing exactly what correct output looks like
storage/example.log The log that session should have produced

3.4 Build

make

That produces four executables: client, finance, fileserver, logging.

Note the file server’s executable is fileserver, not filefile is already a standard Unix command. Its channel, however, is named "file". The two are different things and both spellings are correct in their own place.

3.5 Running the servers by hand

The finished client starts the servers itself. While you are still building it, you can run each one in its own terminal and drive it from a fourth:

./finance -m 1000            # accounts 0 through 1000
./logging -f system.log      # log to system.log
./fileserver .txt .h         # allow uploads of .txt and .h only
./client

This is a debugging aid, not the deliverable. The autograder fails every test if the client does not start the servers itself, so implement Task 1 first.

4. Walkthrough

You do not have to read the servers line by line, but you do have to know which fields each one reads out of your request. These three diagrams are that summary.

4.1 The finance server

Started as ./finance -m <max_account_num>. Accounts 0 through max_account_num inclusive are valid; anything else is refused. Accounts are created on first use with a balance of zero.

How the finance server handles one request A request arrives on the finance channel. If its type is QUIT the server calls exit zero and sends no response at all. Otherwise, if the user id is outside zero to max the server replies with success false and the message Invalid account ID. Otherwise it creates the account if needed and branches on request type: DEPOSIT adds the amount to the balance; WITHDRAW subtracts it only if the balance is greater than or equal to the amount, and otherwise fails with Insufficient funds; BALANCE just reports the balance; any other type fails with Unknown RequestType. All four branches then send the response back. receive_request() channel "finance" r.type == QUIT ? yes exit(0) no response is sent no 0 <= r.user_id <= max ? no success = false "Invalid account ID" yes DEPOSIT balance += r.amount WITHDRAW balance >= amount else "Insufficient funds" BALANCE resp.balance = acc.balance any other type success = false "Unknown RequestType" send_response(resp)
Figure 2 — The finance server. Note the two things that catch people out: QUIT returns nothing at all, and a withdrawal of exactly the balance succeeds, because the test is >=.

resp.balance is set on all three successful operations, so you can print the new balance straight from the response without asking again.

4.2 The file server

Started as ./fileserver <ext_1> ... <ext_n>. It owns the storage/ directory.

How the file server handles one request A request arrives on the file channel. QUIT exits immediately with no response. An UPLOAD_FILE request is checked against the allowed extension list, but only if that list is non-empty; a matching or unchecked file is written into the storage directory, and a non-matching one fails with File extension not allowed. A DOWNLOAD_FILE request looks for the named file in the storage directory and, if found, copies its bytes into the response data field, otherwise fails with File not found. Any other type fails. All paths except QUIT send a response. receive_request() channel "file" QUIT exit(0) no response UPLOAD_FILE DOWNLOAD_FILE extension list empty, or filename matches? storage/<filename> exists? yes no yes no write r.data to storage/ success = true success = false "not allowed" read the file into resp.data success = true success = false "File not found" send_response(resp) The server never writes to your working directory. On a download it returns the bytes, and the client is what saves them to a file.
Figure 3 — The file server. Started with no extension arguments it performs no extension check at all, so every upload is accepted — the opposite of the restriction you might expect.

4.3 The logging server

Started as ./logging -f <log_filename>. It appends one line per request, and the file survives between runs.

How the logging server handles one request The logging server waits for a request. If the type is QUIT it calls exit zero immediately, logging nothing and sending nothing back, which is why a quit never appears in the log file. For every other type it appends one line beginning with the user id in square brackets followed by a description of the action, replies with success, and loops back to waiting. waiting for a request r.type == QUIT ? yes exit(0) nothing logged or sent no append one line to the log file: [r.user_id]: <action> the action text depends on r.type send_response(success) loop
Figure 4 — The logging server. A QUIT is the one request it does not log, because it exits before reaching the logging code.

Which field the log line uses depends on the request type, and getting this wrong is the most common way to lose points on Task 3:

Request Line written Field it reads
LOGIN [7]: logged in user_id
LOGOUT [7]: logged out user_id
DEPOSIT [7]: deposited 500 amount
WITHDRAW [7]: withdrew 200 amount
BALANCE [7]: viewed balance: 300 amount, not balance
UPLOAD_FILE [7]: uploaded file: channel.h filename
DOWNLOAD_FILE [7]: downloaded file: example.log filename

4.4 The session you are aiming at

storage/example_execution.txt is a complete correct run, and storage/example.log is the log it produced:

[1]: logged in
[1]: deposited 500
[1]: withdrew 200
[1]: viewed balance: 300
[1]: uploaded file: channel.h
[1]: downloaded file: example.log
[1]: logged out

Reproduce that log exactly and Task 3’s logging half is done.

5. To-do

Everything you write goes in client.cpp, at the TODO comments. Do the tasks in this order: nothing else can be tested until Task 1 works.

5.1 Task 1 — Run the servers as child processes (45 points)

Start finance, logging and fileserver as children of the client, so that running ./client in one terminal brings the whole system up.

For each of the three, at its TODO:

  1. fork(), and check for failure.
  2. In the child only, run the block already written there — it changes the three variables and calls print_process_info — and then execvp the server.
  3. The parent does nothing here and carries on to start the next server.

The command lines are:

./finance   -m <max_account>
./logging   -f <log_file_name>
./fileserver <extension1> <extension2> ...

The file server’s argument count is not known until run time, so build its argv on the heap. Every array must end with nullptr.

5.2 Task 2 — Print process details (15 points)

Fill in print_process_info so each line carries a real value:

Parent process before fork:
PID: 48120
PPID: 3310
Global variable address: 0x5b1f0a2c4010 value: 100
Stack variable address: 0x7ffd41b2ec44 value: 200
Heap variable address: 0x5b1f0b3d52c0 value: 300
----------------------------------------

Use getpid() and getppid(). Print the address and then the value for each of the global, stack and heap variables. Do not change the label strings or the line order — the tests read these files by line.

This is the part of the lab that shows you the process abstraction directly. Each child bumps the three variables by a different amount before exec, so afterwards compare parent_attributes.txt against the three child files:

That last one is the point worth sitting with. Two processes report the same address holding different values, because the address is virtual: each process has its own mapping from those numbers to physical memory. Nothing is shared, and nothing needed to be copied until it was written to.

5.3 Task 3 — Client-server communication (30 points)

Create the three channels after the servers are running:

RequestChannel finance("finance", RequestChannel::CLIENT_SIDE);
RequestChannel file("file",       RequestChannel::CLIENT_SIDE);
RequestChannel logging("logging", RequestChannel::CLIENT_SIDE);

Then fill in each menu action. Save every reply into the resp variable the starter already tests, and use send_request for all of them.

Menu action Send to finance Send to logging
Login LOGIN
Deposit DEPOSIT with amount DEPOSIT with the same amount
Withdraw WITHDRAW with amount WITHDRAW with the same amount
View balance BALANCE BALANCE with the returned balance in amount
Upload file UPLOAD_FILE with filename and datafile server UPLOAD_FILE with filename
Download file DOWNLOAD_FILE with filenamefile server DOWNLOAD_FILE with filename
Logout LOGOUT

Every action that succeeds is logged, and only after it succeeded — the audit line goes inside the if (resp.success) branch that is already written for you.

Read every response. A request whose reply you never read leaves the channel out of step, and the next reply you read will be the wrong one.

5.4 Task 4 — Close the channels (10 points)

Before the client returns, send Request(QUIT) down all three channels.

The client’s last act is while (wait(NULL) > 0);, which returns only once every child has exited — and a server exits only when it receives QUIT. So a client that misses even one QUIT does not crash or complain. It hangs, forever, with no output. If your program stops responding at exit, this is why.

5.5 Test your work

make test

That runs lab2-tests.sh, which is the same set of checks the autograder runs, out of 100. Run it before you push; it is much faster than waiting for the grader.

If the script reports a hang, run the client by hand and compare against storage/example_execution.txt.

6. Deliverables

Commit and push your work to your assignment repository:

git add client.cpp
git commit -m "lab 2"
git push
# Path Description
1 client.cpp Your completed client: all four tasks

That is the whole submission. The programs’ output files — *_attributes.txt, your log file, and anything uploaded into storage/ — are produced when the grader runs your code, and are deliberately ignored by .gitignore. Do not commit them, and do not commit the built executables.

The autograder runs on every push, so you can push as often as you like and read the result.

6.1 Mistakes that cost points

Symptom Cause
Every test fails at once The servers are not being started by the client. Task 1 first.
Client prints nothing and never exits A QUIT was not sent to one of the three servers
viewed balance: 0 in the log The balance was not copied into the request’s amount field
Process info values all equal the parent’s The if (pid == 0) block does not wrap the given lines
execvp “No such file or directory” The file server is ./fileserver, not ./file
Log file empty The logging server was started, but no requests were sent to it
Deposits succeed but nothing is logged The audit request was placed outside the if (resp.success) branch
Works alone, fails in the grader You renamed channel.ll. Do not — the Makefile picks the right one
Uploads and downloads stop doing anything An earlier upload was over ~1 KB and killed the file server. Restart the client and use a smaller file
error: expected type from clang You are on ARM with clang 18. See §3.4 — ARM needs clang 20

7. Getting help

Ask on Discord. Bring the exact command you ran and the exact output, not a description of it. The Canvas inbox is not monitored.

Office hours and TA contact details are in the syllabus.

Revision history

Date Change
2026-09-07 Migrated from the Google Doc to this page. Due date corrected from June 14 (a leftover from a summer offering) to Monday, September 21. GitHub Classroom replaced by classroom50. ./file corrected to ./fileserver throughout. The file server’s request types corrected from “three” to two. Figure numbering corrected: the old handout skipped Figure 4. The Response field list corrected — it carries success, balance, data, message, not user_id/amount/filename. The en dash in ./logging –f corrected to a hyphen. The pid_t p = fork() snippet corrected to use one variable name. Figures 2, 3 and 4 corrected against the server sources: QUIT sends no response, a withdrawal of exactly the balance succeeds, the file server returns bytes rather than moving files, and r.user_id is not r.id. Added the ARM/x86 note now that the Makefile selects the IR automatically, the BALANCE logging warning, and §5.5 on running the tests locally.