Changelog:

  • 1 Feb 2023: adjust recommended shared memory region name from /PID-mmchat to /PID-chat
  • 1 Feb 2023: replaced wait for outbox[0] to stop being \0 with wait for outbox[0] to become \0 to match the given code and rest of description

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 directly into that portal##.cs.virginia.edu.

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. Make a program that prints its own PID and reads another PID from the user (store it in a global variable)

  2. Create a signal handler for the SIGINT, SIGTERM, and SIGUSR1 signals

    • SIGTERM should invoke a cleanup function
    • SIGINT should invoke a cleanup function and then send SIGTERM to the other user
    • 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.)
  3. Access some shared memory

    • An inbox which this process will use to read values from the other process
    • The inbox of the other process, which we’ll call the outbox.
  4. Repeat until the end of file:

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

  6. Make a cleanup function which destroys both inbox and outbox

    The cleanup function should execute no matter how the program exits: via end-of-input, SIGINT, or SIGTERM.

  7. Upload your solution to the submission site or show a TA your program for checkoff.

1 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)

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

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

Because shared memory needs to be able to span processes, it is a bit more involved to create and destroy than ordinary memory is. There are multiple ways to create shared memory; in this assignment, it is recommended to use shm_open and mmap. shm_open will give you access to special files that are kept in memory instead of persistent storage. Then, mmap takes a file and gives you access to it as a region of memory. (mmap also works for files that are stored normally on disk, though we don’t recommend that for this lab.)

  1. Choose a name for each process’s inbox (similar to a filename; in Linux’s implementation of shared memory, this will become a file in the special directory /dev/shm). To allow finding inboxes using PIDs and avoid interference with other students, I recommend chosing a name like /*PID*-chat, which you can generate using code like:

     char inbox_name[512];
     snprintf(inbox_name, sizeof inbox_name, "/%d-chat", pid);

    Note that the name must start with a /.

  2. The shared memory regions we will access are a special kind of file opened with shm_open and deleted with shm_unlink.

    To allocate and get a handle for a shared memory region, open it using shm_open with the O_CREAT flag:

     int inbox_fd = shm_open(inbox_name, O_CREAT | O_RDWR, 0666);
     if (inbox_fd < 0) { /* something went wrong */ }

    The handle, inbox_fd in the code above, is an integer that you will pass to the mmap (memory map) call later. I use fd in the name because it is a file descriptor.

    Initially, created shared memory regions will be empty, but you can allocate space for them with ftruncate:

     ftruncate(inbox_fd, size);

    (where size is a size in bytes; I recommend 4096).

    To deallocate, use shm_unlink:

     shm_unlink(inbox_name);

    Shared memory regions can persist even after the creating program terminates, so always deallocate what you allocate! Even if your program stops early (e.g., by SIGINT) you still need to deallocate. It does not hurt (though it does set errno) to deallocate multiple times or from the wrong process.

    You will need to include <unistd.h>, <sys/mman.h>, and <fcntl.h> to use these functions as well as #define _XOPEN_SOURCE 600 (or similar) before including anything. On Linux, you will also need to link with -lrt.

  3. A pointer. You attach a pointer to shared memory using mmap and detach it with munmap:

    inbox = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, inbox_fd, 0);
    if (inbox == (char*) MAP_FAILED) {
        /* something went wrong */
        /* note that MAP_FAILED is *NOT* the same thing as NULL */
    }
    close(inbox_fd); // deallocate file descriptor --- can stil use `inbox` after doing this
    // ...
    munmap(inbox, size);

4 Repeating and waiting

You’ve got two things to do: keyboard ↦ outbox and inbox ↦ screen.

4.1 keyboard ↦ outbox

A loop in main is the way to go: while(!feof(stdin)) you want to fgets directly into the outbox. After data is in the outbox, send SIGUSR1 (using kill) to the other process; then, wait for outbox[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 μs = 10 ms = 1/100 second
while(outbox[0]) { usleep(10000); }

4.2 inbox ↦ screen

When you handle SIGUSR1

  1. fputs the 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