One of the foundations of modern cryptography is the odd fact that while it is relatively easy to test if a large number is prime, it is extremely hard to find the prime factors of a large number. To use this fact, we need to be able to generate large, random primes.
There are a number of ways to do this; one of the most important ones is to generate a large, random number and apply a "probabilistic primality test". That is, we apply a test that will probably tell us if the number is prime. By iterating this test enough times, we can be arbitarily sure that the number is, in fact, prime.
One such test is the Miller-Rabin test; see http://en.wikipedia.org/wiki/Miller-Rabin_test for details. (Google can find you more.) Note that it requires a lot of exponentiations mod n, where n is the number you wish to test for primality. For cryptographically interesting primes -- i.e., at least 512 bits and often 1024 bits -- that calculation is expensive; accordingly, it is often done by special hardware, which of course requires a device driver.
This assignment is to write a program that simulates this sort of primality testing. The program will consist of several processes. The main process (which will fork() several child processes) does the actual testing. One chlid process is a simulated hardware random number generator; you send it r and it gives you r random bytes. You also need at least two (and more, if you prefer) child processes that calculate a^b mod n, where ^ is exponentiation.
Communication with these child processes is by means of pipes and signals; the latter are used to simulate interrupts. The main process will have two pipes to each process. One is used to send data to the process; the other is used to receive data from the process. The sequence of operation is this. The main process writes data to a child process, then goes on about its business. It may need to block -- see below -- while waiting for the calculation to finish. The child process does the appropriate calculation, writes the answer to a pipe, and sends signal SIGUSR1 to the main process. When the main process receives the signal, it may read the result from the pipe. It MUST NOT read from the pipe until it receives such a signal.
A process may block, waiting for a signal, via the sigsuspend() system call. The kill() system call can be used to send a signal to a process. sigprocmask() can be used to temporarily block signals; sigaction() specifies what to do whena signal arrives. Other useful system calls include poll(), which will tell you if a pipe is ready to be read, fork(), pipe(), and getpid(). You can learn how to use these by typing 'man 2 getpid' on a Linux box. Note: if you use poll(), you may use it only with a timeout of 0.
You may not write to a "device" that is busy; you have to wait for it to finish. Device processes may not talk to each other. You may not use threads instead of processes.
Since we're not trying to use these numbers for real cryptographic purposes, you should use 'unsigned long', and 'unsigned long long' to hold intermediate results during exponentiations. You can get random bytes by reading from /dev/urandom.