Using RPC: From rpcgen to gRPC
A remote procedure call (RPC) makes a function call on another machine look like a local function call. The client calls a stub, the stub marshals the arguments into a message, the message crosses the network, a server stub unmarshals the arguments and calls the real function, and the reply travels back the same way.
RPC Is a Form of IPC
Before we look at RPC specifically, place it in the taxonomy from the IPC lecture:
- Shared memory: two processes map the same physical pages and exchange data by reading and writing those pages directly. Requires both processes to be on the same machine, and requires the OS to set up the shared mapping.
- Message passing: two processes exchange discrete messages through
the kernel (
send/receive), through a pipe, or through a socket. Works between processes on the same machine, and between processes on different machines (via a socket). - RPC: a high-level abstraction built on top of message passing. The programmer writes a function call, and the RPC framework generates the code that turns that call into a message, sends it, waits for the reply, and turns the reply back into a return value.
RPC does not compete with shared memory. Shared memory is only possible when the communicating processes are on the same machine and the OS can map the same physical pages into both address spaces. For processes on different machines, shared memory is not slower — it is simply impossible. The choice for remote communication is between different forms of message passing.
Why not just use send/receive directly? Because message passing
gives you bytes, not a programming model. If you write a client and a
server by hand, you have to decide:
- how to encode each argument into bytes (integers, strings, structs, arrays);
- how the receiver knows where one argument ends and the next begins;
- how the two sides agree on the message format, and what happens if one side is updated and the other is not;
- how to turn a reply into a return value the caller can use.
Every client and server pair that uses raw message passing has to solve these problems, and they tend to solve them slightly differently. RPC factors these problems out into two reusable pieces:
- a schema (the
.xor.protofile) that declares the procedures, their arguments, and their return values, and - a stub layer (generated by
rpcgen,protoc, or a similar tool) that encodes calls into messages and decodes replies back into return values.
That is what “high-level” means here: RPC does not replace message passing, it automates the bookkeeping that message passing would otherwise require the programmer to do by hand.
Study the Example Programs
Start by reading the example programs:
https://github.com/huichen-cs/OSClassExamples/tree/main/ipc/rpc/linux
The example uses rpcgen, the classic RPC stub generator. Running
rpcgen on an interface description file (.x) produces a header, a
client stub, a server skeleton, and XDR routines for marshalling; see
man rpcgen for the exact file names on your system. Pay attention to:
- what the
.xfile declares (the program number, version, and procedures), - which files
rpcgengenerates and what each one contains, - how the client stub and server skeleton call each other,
- what XDR does and why it is needed.
Tasks
Part 1: Implement a Classic RPC Service
Write an RPC service using rpcgen. Your service must:
- Define a
.xinterface file with at least one procedure that takes an argument and returns a result. - Generate the stubs with
rpcgen. - Implement the server procedure.
- Implement the client, which calls the procedure and prints the result.
- Demonstrate the client and server running on the same host, and explain what would change if they ran on different hosts.
A simple starting point: a procedure that takes a string and returns
its length, or one that takes two integers and returns their sum.
Once that works, extend it to something less trivial (for example, a
procedure that returns a list of files in a directory, like the
readdir example in the Oracle ONC RPC documentation).
Part 2: The gRPC Counterpart
Implement the same service using gRPC. You will need:
- A
.protofile defining the service and its message types. - Generated client and server code (via
protocor a language-specific tool). - A server implementation.
- A client that calls the same procedure.
If you do not have gRPC installed, use a language with a well-documented tutorial (Python and Go are both good choices; see https://grpc.io/docs/languages/). The goal is not a production-quality service; it is to see the same RPC pattern expressed in a modern framework.
Part 3: Demo and Discuss
Prepare a 5-minute demo that includes, but selectively presents, and be prepared to answer questions:
- The classic service. Run your
rpcgenclient and server, and walk through the generated stubs. - The gRPC service. Run your gRPC client and server, and walk
through the
.protofile. - The comparison. Summarize the answers to the question below, focusing on
what changed between the two and why.
- The interface definition. What does the
.xfile declare, and what does the.protofile declare? What is the same, and what is different? - The generated code. What does
rpcgengenerate, and what doesprotocgenerate? Which files play the same roles? - The wire format. What does XDR do, and what does Protocol Buffers do? Why do both exist?
- The transport. What transport does classic RPC typically use, and what does gRPC use? Why does the transport matter for performance?
- The momentum question. Why has gRPC become a widely used framework for internal service-to-service communication, while classic RPC is mostly found in long-lived systems such as NFS?
- The IPC connection. In terms of the IPC taxonomy above, which
layer of RPC replaces the programmer’s calls to
sendandreceive? Which layer decides the message format?
- The interface definition. What does the
- Give examples where gRPC is being used.