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 do ssh -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.)

  1. 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’s inbox for chat messages;
    • other_inbox which is the other process’s inbox for chaat messages.

    Your code will write to other_inbox and read from my_inbox.

  2. 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.)

You can follow the instructions below under Shared memory to create this. (Note that these instructions require linking with -lrt.)

  1. Repeat until the end of file:

    • Read a typed line into the outbox (without overflow)
    • Wait until the outbox is emptied
  2. After the user stops typing (i.e., EOF), send SIGTERM to the other process

  3. 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.

  4. 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

pid_t other_pid = 0;

#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) {
    snprintf(filename, NAME_MAX, "/%d-chat", pid);
    int fd = shm_open(filename, O_CREAT | O_RDWR, 0666);
    if (fd < 0) {
        perror("shm_open");
        abort();
    }
    if (ftruncate(fd, BOX_SIZE) != 0) {
        perror("ftruncate");
        abort();
    }
    char *ptr = mmap(NULL, BOX_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == (char*) MAP_FAILED) {
        perror("mmap");
        abort();
    }
    return ptr;
}

void setup_inboxes() {
    my_inbox = setup_inbox_for(getpid(), my_inbox_shm_open_name);
    other_inbox = setup_inbox_for(other_pid, other_inbox_shm_open_name);
}

void cleanup_inboxes() {
    munmap(my_inbox, BOX_SIZE);
    munmap(other_inbox, BOX_SIZE);
    shm_unlink(my_inbox_shm_open_name);
}

int main(void) {
    printf("This process's ID: %ld\n", (long) getpid());
    char *line = NULL; size_t line_length = 0;
    do {
        printf("Enter other process ID: ");
        if (-1 == getline(&line, &line_length, stdin)) {
            perror("getline");
            abort();
        }
    } while ((other_pid = strtol(line, NULL, 10)) == 0);
    free(line);
    setup_inboxes();
    // YOUR CODE HERE
    cleanup_inboxes();
    return 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.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;

sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
/* ... */

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:

  1. setup_inboxes() sets my_inbox to point to the inbox for the current process and other_inbox to point to the inbox for the process with PID other_pid (which is read from user input). Each of these inboxes can be treated as 4 kilobyte char array.

    setup_inboxes() works using shm_open, which requires shared memory regions be identified by a string that works similarly to a filename. We choose the psuedo-filename /1234-chat where 1234 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() uses ftruncate to set the size of the shared memory region, then uses mmap() 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.

  2. cleanup_inboxes() frees resources associated with both inboxes. This consists of making the shared memory no longer be available in the current process using munmap, and then using shm_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 call munmap won’t have this problem — the mapping 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 };
    nanosleep(&ts, NULL);
}

5.2 inbox ↦ screen

When you handle SIGUSR1

  1. fputs my_inbox so we can see what was sent;
  2. fflush(stdout) to ensure we see it now, not later;
  3. 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

  1. 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 (see help process handle for documentation), and in GDB, the info signals, catch, and handle commands (see the GDB reference manual chapter on signals).

  2. 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.

  3. If you’re having issues with segmentation faults and bus errors, then it might be useful to use AddressSanitizer (a memory error detector) by compiling and linking with -fsanitize=address and -g.