3/1: Tip:
To copy strings, use strcpy(). To copy data, copy memcpy() or bcopy(). Do not use strcpy() to copy data! (in other words, realize that the data written to a pipe may
not
be a string! All strings terminate in '\0', whereas arbitrary data may not.
3/1: Tip:
Within the OS, make sure to keep track of which processes open a pipe, so that you can later check during reads and writes that the same process that opened it for reading is indeed reading it. The way to do this is to save the sockaddr structures of the processes somewhere during open and compare them on subsequent reads and writes. If you don't do this, someone could just call Pipe_Read(), and if they guess a valid descriptor, they will be able to read the data from a pipe they haven't opened! A good way to compare to arbitrary structures may be to use memcmp().
3/1: Tip:
Within the OS, be careful when passing data from the main OS thread to the worker threads. For example, if you call Domain_Read() to read a message into a structure, and then pass a pointer to that structure to a thread, you could get into trouble if the main thread calls Domain_Read() again before the worker thread is done with the structure. Thus, if passing data to a thread, make sure to allocate it on the heap, and then have the thread free it when it is finished with it.
2/25:
A
new error code
known as
E_INVALID_OP
has been introduced. This error should be signalled when a pipe open for reading is written to, or a pipe open for writing is read from.
LibOS.h
has been updated to reflect this change. The project description has also been updated to reflect this change (see below), and highlighted in red for your viewing pleasure.
2/18:
A number of small discrepancies between the LibOS example code and this document have been resolved. However, please assume that
this document
provides the exact specification of functionality required.
2/18:
Document corrected to say
osErrno
instead of
osError.
A couple of things will make this project tricky. First, processes will have to communicate with your OS via
domain sockets.
These sockets provide a simple and quite general interprocess communication method (more general than the pipes you used in the previous assignment), but require you to learn how to use a new form of IPC. Second, your OS will have to be
multi-threaded.
Specifically, whenever your OS receives a request (such as a "pipe create" or "pipe open", etc.), instead of doing whatever work is needed to fulfill the request immediately, it will instead hand-off the request to one of a pool of threads inside the OS. One of those threads will then service the request, and send the result back to the requesting process.
Fortunately, you will get a little help with some of the code. In particular, we will give you example code that communicates via domain sockets, and some skeleton code to help programs access your OS. We will also describe what basic structure your OS and processes should take on. Finally, we will give some example code that starts a thread to do some work, and point you to some useful synchronization primitives to use. More details on all of these things are found below.
This project is hard, so start early!
A lot of coding is involved, as well as careful design work to make sure your structure will be satisfactory.
Your OS will provide a very limited set of pipe-like services. A process, by linking with the LibOS library, will be able to call routines to create, open, read, write, and close pipes. These pipes are much more restricted than the typical Unix pipe, in that you cannot write arbitrary-sized messages to a pipe. Instead, a write writes a fixed-sized message to the pipe (the size is a pre-determined constant BUFFER_SIZE), and reads retrieve a single fixed-sized message that has been written to the pipe. Otherwise, many of the semantics are similar to the traditional pipe.
Let's go through a simple example of how this will all work. Let's say a program called
main.c
wants to use the pipe functionality provided by the OS. What you would do in main.c is this: first, you would include the LibOS header file, called
LibOS.h.
As you can see, we are providing this header file, and you should not be adding to it. Then, you would compile main and link it with the LibOS library (this process is described further below). Assuming that the OS is already running (after all, it is a separate process), you would run the main program. When this program calls into the LibOS library (let's say to open a pipe with Pipe_Open()), the library will contact the OS process (via domain sockets) and tell the OS what the request was. The OS will then figure out how to service the request, and reply to the library, which will somehow convey the results of the call (success or failure) to the main program. More details on all of these steps are found below.
In the general case, you will probably run at least three processes: the OS itself, a program that reads from a pipe, and a program that writes to a pipe. Of course, while always running a single OS, you should be able to run many programs that are concurrently reading and writing to different pipes.
prompt> ./os [-d] filename poolSize
As for LibOS, you are just creating a library, which should be a file called
libOS.so.
This library has a pre-defined set of interfaces, as defined below. Libraries don't have a main routine, so you will have to create your own test programs in order to test if your library (and your OS) are working correctly. Thus, for testing, you might write two programs: pipe_reader.c (which opens a pipe and reads from it) and pipe_writer.c (which opens a pipe and writes to it). Both of these will have to be linked with the LibOS, as described below.
Important Note:
for testing, we will be linking our own programs with your library. Thus, it is
very important
that you don't change any of the interfaces specified in for the LibOS!
int Pipe_Open(char *name, Pipe_t mode);
Pipe_Open takes two arguments, the name of the pipe to be opened, and the mode, which is an enumerated type found in
LibOS.h.
There are two modes that an application can specify:
PIPE_READER,
and
PIPE_WRITER.
Pipe_Open should
block
until a reader and writer have both opened it; in other words, when the reader has opened it (but no writer yet), the call to Pipe_Open should not return until the writer has also opened the pipe. Upon success, Pipe_Open() should return a file descriptor that the application can use in subsequent read and write calls. Upon any failure, -1 should be returned, and
osErrno
set appropriately. For any pipe, there can be at most one reader and one writer. Thus, if a second reader or writer tries to open the given pipe, -1 should be returned and
osErrno
set to
E_PIPE_FULL.
If the name is NULL or that pipe doesn't exist or the mode is not appropriate, -1 should be returned and
osErrno
set to
E_INVALID_ARGS.
int Pipe_Read(int pipe, char *buffer);
Pipe_Read takes two arguments: the first is a descriptor (as returned by Pipe_Open), the second is a pointer to a buffer where data read from the pipe will be placed. When the user calls Pipe_Read, the routine will
block
until data is available, at which point a single buffer of size BUFFER_SIZE will be copied into buffer and the size of the read (in our case, always BUFFER_SIZE) will be returned to the user. Once the pipe is closed by the writer, the reader should still be able to read any data that has been placed in the pipe, but once that data is gone, Pipe_Read should return 0 to indicate that the pipe has been shutdown. If a bad descriptor is passed to Pipe_Read, -1 should be returned and
osErrno
set to
E_INVALID_ARGS.
If the pipe has been opened for writing, calling Pipe_Read should return -1 and
osErrno
should be set to
E_INVALID_OP.
int Pipe_Write(int pipe, char *buffer);
Pipe_Write is quite similar to Pipe_Read: it takes two arguments, a descriptor and a buffer that points to the data to be written to the pipe. If the data is successfully written to the pipe, Pipe_Write will return the number of bytes written (again, this should be BUFFER_SIZE, because we are only dealing with fixed-sized buffers). Pipe_Write also can block in the OS, though, if the buffers are all full and the reader has not yet read the data from the pipe. In this case, Pipe_Write should not return until the data has been written to the pipe. If the reader closes the pipe, any subsequent writes should return 0. If a bad descriptor is passed to Pipe_Write, -1 should be returned and
osErrno
set to
E_INVALID_ARGS.
If the pipe has been opened for reading, calling Pipe_Write should return -1 and
osErrno
should be set to
E_INVALID_OP.
int Pipe_Close(int pipe);
Pipe_Close takes a descriptor and closes the pipe associated with that descriptor. Pipe_Close should block until the OS has informed the library that the pipe is indeed closed. Closing the pipe has certain side-effects on subsequent reads or writes by the other pipe member, as described in the read and write sections above. If the close is successful, 0 should be returned. If a bad descriptor is passed to Pipe_Close, -1 should be returned and
osErrno
set to
E_INVALID_ARGS.
int OS_Init(char *filename);
This routine is called once by any program that wishes to use the LibOS services, and it takes a single argument. This argument is the name of the file that the OS is bound to, and should be used by the LibOS to send messages to the OS in the Pipe_XXX() routines. Internally, OS_Init() should probably call Domain_Open() and Domain_FillSockAddr(filename) to prepare for upcoming communication that will occur within the Pipe_XXX() routines. Upon success, this should return 0, but if for some reason this initialization fails, the routine should return -1 (no setting of
osErrno
is necessary here).
Note 1:
If the Pipe_XXX() routines are called
before
OS_Init() has been called, that is an error; the pipe routines should check that OS_Init() has been called, and if not, return -1 and set
osErrno
to
E_INIT.
Note 2:
For all calls, if the library can't communicate successfully with the OS, -1 should be returned and
osErrno
should be set to
E_COMM_FAILURE.
int socket = Domain_Open(tmpnam(NULL)); // library might use this code
if (socket < 0) { // signal error }
struct sockaddr_un addr;
Domain_FillSockAddr(&addr, "/tmp/os.fifo");
char buffer[512];
int rc = Domain_Write(socket, &addr, buffer, 512);
if (rc != 512) { // there was some trouble! }
char buffer[512];
struct sockaddr_un receiveAddr;
int rc = Domain_Read(socket, &receiveAddr, buffer, 512);
if (rc != 512) { // didn't read as much as expected (but only an error if < 0) }
To learn more about the code, read the code - it is pretty simple. To learn more about what Domain_Read and Domain_Write may return, read the
sendto()
and
recvfrom()
man pages, which are the OS routines used to implement Domain_Read and Domain_Write.
For example, the following struct could be used to pack the necessary information to perform a Pipe_Create():
typedef struct __MsgCreate_t {
int type; // used to set the type of this message} MsgCreate_t;
char name[MAX_STR];
int numBuffers;
The typical operation of the OS is as follows. The main thread (the one that starts up all the other threads) will start running inside of the main() routine. It should parse arguments, initialize data structures, and then create the thread pool. At that point, the main thread should enter a loop where it just waits for messages from LibOS's.
When a message is received, the OS should pass the message (or some description of the work that needs to be done to complete the request) to one of the threads. This should be done through some sort of
shared data structure.
You might recognize the
producer-consumer
relationship here, in that the main thread that reads messages from the network is a producer of work for the thread pool, which consists of a bunch of consumers.
One of the threads in the thread pool (and exactly one) will get the request, and try to execute the needed functionality. For example, if the message was a Pipe_Create() message, the thread would update whatever global data structures are in the OS that keeps track of whatever pipes are currently active. When done, the thread should go ahead and send a response to the LibOS, of course packaging up the response in a way that the library will be able to interpret.
Remember that multiple threads may be accessing those shared data structures at the same time. Thus, you will need to use
synchronization primitives
to correctly implement access to those structures. The synchronization primitives that are available to you are as follows:
pthread_cond_signal and pthread_cond_wait
Unfortunately, locks are not quite good enough: you need to be able to wait for a condition to come true. For this, you will need to use pthread_cond_wait and pthread_cond_signal. These should be used in combination with a lock in order to implement the necessary waiting that a Pipe_Read() or Pipe_Write() may incur because the pipe is empty or full, respectively. Read the man pages for more details.
Important:
The goal of implementing a multi-threaded OS is to provide for as much concurrency as possible. Thus, in your design and implementation, you need to think about how to provide as fine-grained locking as possible. Just putting one big honkin' lock around everything is not good and will be marked down.
A Note About Deadlock:
Note that when a request to perform a Pipe_Read or Pipe_Write or even a Pipe_Open comes into the OS and gets handed off to a thread, it may block waiting for the pipe to become full or empty (more specifically, if a thread executing a read request finds the pipe empty, it
should
block, and if a write finds a pipe full, it also
should
block, and if an open finds that that the other process has not yet opened the pipe, it
should
block too). Thus, threads get used up by pipes that are getting blocked like this.
Now consider a system that has 2 threads in the OS thread pool, and 2 readers and 2 writers (this of course means there are 2 pipes open). If both pipes are full and 2 writers come to perform a write in the OS, both writers will have to block waiting for a reader to remove a message. However, this will never happen because the two threads in the system are blocked - one was assigned to each writer, and there are no threads left to service any read requests. This system would be in a state of deadlock.
As Extra Credit:
Your program is to accept an optional command line argument that indicates whether or not the above situation should be dealt with. In other words, if the -d option is enabled, your OS should never deadlock. However, if the -d option is
NOT
enabled, your program should deadlock in the above and similar situations. This part of the assignment is considered extra credit: that is, you can certainly still get a very strong A in the course if you do not do it. In general, we recommend that you implement the -d option (the "no deadlock" option) when everything else is done and working correctly.
If you decide to try for the extra credit, make sure to make a copy of your working program before you start modifying the code.
More generally, make copies of every version where you get some new substantial functionality working. If you have lots of free time, use CVS to do this; otherwise, just make a subdirectory under your working directory for each copy (i.e., 01, 02, 03), and keep good notes on what version is in each directory (in a file called version.doc, for example).
You also need to learn how to compile a multi-threaded application. We'll tell you how to do that too.
All of these things should be done within your makefile, to make your life easy!
Let's say you have implemented your library in a single file, called LibOS.c. This is how you should compile that within your makefile:
gcc -c LibOS.c -g -Wall -fpic
Note that any other files that are going to be linked into the LibOS (e.g., Domain.c) should be compiled in the same way.
Now that you have LibOS.o, you'll want to make a shared library out of it. The way you do that is with the following line:
gcc -o libOS.so LibOS.o -shared
gcc -o libOS.so LibOS.o Domain.o -shared -lnsl -lsocket
gcc -o main main.c -L. -R. -lOS
g++ -c os.cc -D_REENTRANT -g -Wall
Finally, to link your OS all together, do something like this:
g++ -o os os.o Domain.o -lnsl -lsocket -lpthread
~cs537-1/handin/(username)/p2
where (username) is your login. Because you are working in groups of two, please go ahead and put copies of your code in both partners directories.
Obviously, you should submit all source files that are needed to compile your program (just the ones you wrote, not standard headers that will be found on any machine). You should also include a README file and a Makefile for this project. Your README file should contain information about how to run your program, any known bugs it may have, and any other information that is important to your project. Your makefile should successfully compile the program and create the
os
executable and the
LibOS
library.
Also, be sure to comment your code well so that it can be easily browsed to see what is going on. Code that is excessively difficult to read may be penalized.