In this lab you’ll write yet another chat application.
It will only work on a single machine, not over a network file system or the Internet, but it will also require significantly fewer system resources than our other chats.
If you are using ssh, this means you need to ssh into the same back-end server twice. To do this, first SSH into portal.cs.virginia.edu and run
uname -n
to find out what server you are on (portal## for some two-digit number ##). Then, have your second SSH go to portal, then SSH from portal into that portal##.cs.virginia.edu. (If you’re using SSH from the command line of a Unix-like machine, you can also dossh -J portal.cs.virginia.edu portal##.cs.virginia.edu
to automate this process.)
Each instance of the chat program will have a shared memory region in which it receives messages. When sending a message, one instance of the chat program will modify the other’s shared memory region, then send a signal to notify the other program that a new message is available. The receiver will print out the message from its signal handler. Meanwhile, the sender will wait until the receiver acknowledges receiving the message by resetting the shared memory region before prompting for the next message to send.
To complete the signals-based chat program described above, do the following. We recommend doing them in order. Each is described in more detail below.
Note the above instructions regarding testing on portal.
I would recommend working in pairs on this lab, but note that both instances of the chat program need to run as the same user. (You can’t send signals to another user’s programs.)
Start with the template program below that prints out its PID and reads in another PID into a global variable.
As described below, the template program sets up access to two shared memory regions:
my_inbox
which is this process’sinbox
for chat messages;other_inbox
which is the other process’sinbox
for chaat messages.
Your code will write to
other_inbox
and read frommy_inbox
.Create a signal handler for the SIGINT, SIGTERM, and SIGUSR1 signals
- SIGTERM should invoke
cleanup_inboxes
and then exit - SIGINT should invoke
cleanup_inboxes
and then send SIGTERM to the other user and then exit - SIGUSR1 should display the contents of the inbox (eventually in
shared memory; make a pointer to a placeholder character array for now)
and then set its first character to be
'\0'
- (Although normally one should take care to only use signal-safe functions in your signal handlers, for this lab, we do not care if you follow this rule.)
- SIGTERM should invoke
You can follow the instructions below under Shared memory
to
create this. (Note that these instructions require linking with
-lrt
.)
Repeat until the end of file:
- Read a typed line into the outbox (without overflow)
- Wait until the outbox is emptied
After the user stops typing (i.e., EOF), send SIGTERM to the other process
Make the cleanup function destroy both the inbox and outbox
The cleanup function should execute no matter how the program exits: via end-of-input, SIGINT, or SIGTERM.
Upload your solution to the submission site or show a TA your program for checkoff.
1 Starter code
#define _XOPEN_SOURCE 700 // request all POSIX features, even with -std=c11
#include <fcntl.h> // for O_CREAT, O_RDWR
#include <limits.h> // for NAME_MAX
#include <stdlib.h> // for EXIT_SUCCESS, NULL, abort
#include <stdio.h> // for getline, perror
#include <sys/mman.h> // for mmap, shm_open
#include <time.h> // for nanosleep
#include <unistd.h> // for getpid
= 0;
pid_t other_pid
#define BOX_SIZE 4096
char *my_inbox; // inbox of current process, set by setup_inboxes()
char *other_inbox; // inbox of PID other_pid, set by setup_inboxes()
// bookkeeping for cleanup_inboxes()
char my_inbox_shm_open_name[NAME_MAX];
char other_inbox_shm_open_name[NAME_mAX];
/* open, creating if needed, an inbox shared memory region
for the specified pid.
store the shm_open filename used in filename, which must
be NAME_MAX bytes long.
*/
char *setup_inbox_for(pid_t pid, char *filename) {
(filename, NAME_MAX, "/%d-chat", pid);
snprintfint fd = shm_open(filename, O_CREAT | O_RDWR, 0666);
if (fd < 0) {
("shm_open");
perror();
abort}
if (ftruncate(fd, BOX_SIZE) != 0) {
("ftruncate");
perror();
abort}
char *ptr = mmap(NULL, BOX_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == (char*) MAP_FAILED) {
("mmap");
perror();
abort}
return ptr;
}
void setup_inboxes() {
= setup_inbox_for(getpid(), my_inbox_shm_open_name);
my_inbox = setup_inbox_for(other_pid, other_inbox_shm_open_name);
other_inbox }
void cleanup_inboxes() {
(my_inbox, BOX_SIZE);
munmap(other_inbox, BOX_SIZE);
munmap(my_inbox_shm_open_name);
shm_unlink}
int main(void) {
("This process's ID: %ld\n", (long) getpid());
printfchar *line = NULL; size_t line_length = 0;
do {
("Enter other process ID: ");
printfif (-1 == getline(&line, &line_length, stdin)) {
("getline");
perror();
abort}
} while ((other_pid = strtol(line, NULL, 10)) == 0);
(line);
free();
setup_inboxes// YOUR CODE HERE
();
cleanup_inboxesreturn EXIT_SUCCESS;
}
2 PID
getpid()
(from unistd.h
) returns the PID of
the current process as a pid_t
, which can be treated like
an int
in most situations. This can be printed (e.g., with
printf
) by one process and input to the other (e.g., with
scanf
).
If you use something like scanf("%d", ...)
, note that
this does not read a full line. For example, if the input is
"12\n"
, then it will read only the "12"
,
leaving "\n"
on the input. This means a later call to
fgets()
will return that "\n"
as an empty line
immediately, which you may need to account for later.
3 Signals
As shown in class, the key parts of signal handling are defining a handler function:
#include <signal.h>
static void your_handler_function_name_here(int signum) {
if (signum == SIGINT) /* ... */
}
and the code in main
to tell the OS to use it and for
which signals:
struct sigaction sa;
.sa_handler = handler;
sa(&sa.sa_mask);
sigemptyset.sa_flags = SA_RESTART;
sa
(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction/* ... */
To send a signal, use kill(pid_to_send_to, SIGTERM)
or
the like.
4 Shared memory
One of the advantages of virtual memory is the OS can map the same physical page to different virtual pages in different processes. The processes can then directly communicate by loading what the other stored in that shared memory.
In this assignment, we’ll use two regions of shared memory. If there
are two chat processes running A and B, there will be an inbox for A and
an inbox for B. In both A and B, our supplied code maps
each of
these memory regions so you can access them.
Because shared memory needs to be able to span processes, it is a bit more involved to create and destroy than ordinary memory is.
Our sample code provides functions that do this for you:
setup_inboxes()
setsmy_inbox
to point to the inbox for the current process andother_inbox
to point to the inbox for the process with PIDother_pid
(which is read from user input). Each of these inboxes can be treated as 4 kilobytechar
array.setup_inboxes()
works usingshm_open
, which requires shared memory regions be identified by a string that works similarly to a filename. We choose the psuedo-filename/1234-chat
where1234
is the PID of a process.shm_open
returns a file descriptor, which allows us to access the shared memory region similarly to how we would access a file. (In fact, on Linux, the shared memory regions are files that just happen to be stored in memory.)setup_inboxes()
usesftruncate
to set the size of the shared memory region, then usesmmap()
to request that it be made available in the current process.mmap()
returns a pointer that that can be used to access the shared memory.cleanup_inboxes()
frees resources associated with both inboxes. This consists of making the shared memory no longer be available in the current process usingmunmap
, and then usingshm_unlink
to delete the current process’s region.If we fail to call
shm_unlink
, then the shared memory will remain present on the system indefinitely, even if both processes using the shared memory region exit. (Failing to callmunmap
won’t have this problem — themapping
of the memory to the current process wil be cleaned up when the current process exits or crashes.)
5 Repeating and waiting
You’ve got two things to do: keyboard ↦ outbox and inbox ↦ screen.
5.1 keyboard ↦ outbox
A loop in main
is the way to go: while(1)
fgets
directly into the other_inbox (breaking out of the
loop if fgets
fails). After data is in the
other_inbox
, send SIGUSR1 (using kill
) to the
other process; then, wait for other_inbox[0]
to become '\0'
. Don’t
just busy-wait though: check, then go to sleep for a bit, then check
again. (A better strategy you could also try might be to wait for
SIGUSR2 using sigprocmask() and sigwait() and have the receiving process
send SIGUSR2 when it is done reading the input.)
// wait for '\0' by checking 100 times a second
// 10,000,000 ns = 10 ms = 1/100 second
while(my_inbox[0]) {
struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 };
(&ts, NULL);
nanosleep}
5.2 inbox ↦ screen
When you handle SIGUSR1
fputs
my_inbox so we can see what was sent;fflush(stdout)
to ensure we see it now, not later;- set the first character in the inbox to
'\0'
so the other process knows we’re ready for it to send more
6 Debugging notes
If you run your program(s) in a debugger like GDB or LLDB, they will by default intercept signals:
- for most signals, GDB and LLDB automatically stop as if there was a breakpoint when your program receives the signal;
- for SIGINT and SIGTRAP, GDB and LLDB will not pass the signal to the program’s own signal handler (if any) by default;
If you want to change this behavior, in LLDB you can use
process handle
command (seehelp process handle
for documentation), and in GDB, theinfo signals
,catch
, andhandle
commands (see the GDB reference manual chapter on signals).If you do not setup a signal handler for SIGUSR1 and it is received by a process, then, by default, the process will terminate and the shell will print the message
User defined signal 1
.If you’re having issues with
segmentation faults
andbus errors
, then it might be useful to use AddressSanitizer (a memory error detector) by compiling and linking with-fsanitize=address
and-g
.