Lab 13: Threaded Socket Server
Table of Contents
1 Preliminaries
In this lab you will complete a set of C programs to expose you to the threading library and threaded socket programming.
1.1 Lab Learning Goals
In this lab, you will learn the following topics and practice C programming skills.
- Using pthreads to create and join threads
- Passing arguments to threads and retrieving return values
- Create multi-threaded applications: factorization and socket server
1.2 Lab Setup
Run the following command
~aviv/bin/ic221-up
Change into the lab directory
cd ~/ic221/labs/13
All the material you need to complete the lab can be found in the lab directory. All material you will submit, you should place within the lab directory. Throughout this lab, we refer to the lab directory, which you should interpret as the above path.
1.3 Submission Folder
For this lab, all ubmission should be placed in the following folder:
~/ic221/labs/13/
This directory contains 4 sub-directories; examples
, timer
,
term-status
, and mini-sh
. In the examples
directory you will
find any source code in this lab document. All lab work should be
done in the remaining directories.
- Only source files found in the folder will be graded.
- Do not change the names of any source files
Finally, in the top level of the lab directory, you will find a
README
file. You must complete the README
file, and include any
additional details that might be needed to complete this lab.
1.4 Compiling your programs with clang
and make
You are not required to provide your own Makefiles for this lab.
1.5 README
In the top level of the lab directory, you will find a README
file. You must fill out the README file with your name and alpha.
Please include a short summary of each of the tasks and any other
information you want to provide to the instructor.
1.6 Test Script
You are provided a test script which prints pass/fail information
for a set of tests for your programs. Note that passing all the
tests does not mean you will receive a perfect score: other tests
will be performed on your submission. To run the test script,
execute test.sh
from the lab directory.
./test.sh
You can comment out individual tests while working on different parts of the lab. Open up the test script and place comments at the bottom where appropriate.
2 PART 1: Threaded Socket Server
When implementing a socket server that can handle multiple client connections, we run into problems with blocking. First, the server must accept an incoming connection, which blocks, and once the connection is established, reading and writing from the socket is also a blocking operation. All that blocking means that it can be challenging to service multiple client connections.
In class, we saw one way to mitigate blocking by using the
select()
system call. The relevant portion of the code is
duplicated below:
//set up a select set to select from FD_ZERO(&activefds); //add the server socket to the select set FD_SET(server_sock, &activefds); while(1){ //set current active file descriptors readfds = activefds; //Perform a selct if( select(FD_SETSIZE, &readfds, NULL, NULL, NULL) < 0){ perror("select"); exit(1); } //check for activity on all file descriptors for(i=0; i < FD_SETSIZE; i++){ //was the file descriptor i set? if(FD_ISSET(i, &readfds)){ if(i == server_sock){ //activity on server socket, incoming connection //accept incoming connections = NON BLOCKING client_sock = accept(server_sock, (struct sockaddr *) &client_saddr_in, &saddr_len); printf("Connection From: %s:%d (%d)\n", inet_ntoa(client_saddr_in.sin_addr), ntohs(client_saddr_in.sin_port), client_sock); //add to active set FD_SET(client_sock, &activefds); }else{ //otherwise client socket sent something to us client_sock = i; //get the foreign address of socket getpeername(client_sock, (struct sockaddr *) &client_saddr_in, &saddr_len); //clear response buffer memset(response, 0, BUF_SIZE); //read from client and echo back n = read(client_sock, response, BUF_SIZE); if(n == 0){ //client close socket //close client sockt close(client_sock); //deactivate this client socket FD_CLR(client_sock, &activefds); //LOG printf("Client Closed: %s:%d (%d)\n", inet_ntoa(client_saddr_in.sin_addr), ntohs(client_saddr_in.sin_port), client_sock); }else{ //client sent a message //null response for safety response[n] = '\0'; //write message back to client write(client_sock, response, n); //LOG printf("Received From: %s:%d (%d): %s", inet_ntoa(client_saddr_in.sin_addr), ntohs(client_saddr_in.sin_port), client_sock, response); } } } } }
You'll notice that you must track the current socket file
descriptors, continually select from them to check if they are
actionable before calling either accept()
or read()
. While the
code is usable, it is not that elegant and can be challenging to
write.
Instead, we'd like to use threading to solve this problem. The big idea is that we generalize the actions of the server for each client into a function. Whenever a client connects, the server accepts the connection and passing the client operations over to the handler routine. If that handler routine runs within a thread, then the server can continually just accept incoming connections in the main thread and allow worker threads to service client connections.
2.1 Task 1: threaded_server
For this task, change into the socket-server
directory, where you will
find four files:
threaded_server.c
: multithreaded, multi-client socket server, which you will completeselect_server.c
: select based multi-client socket server, for referenceecho_server.c
: single thread single-client socket server, for referenceMakefile
: to compile your program
Your goal is to complete the threaded_server
program so that it meets the
following specification.
- It should create a new thread for every client and echo back all
data sent to the server to the client, like
echo_server
andselect_server
- It should handle multiple clients simultaneously
- The server should provide meaningful output that matches
select_server
; logging incoming connections, read/writes to sockets, and connection close events.
Below is some sample output from a server with multiple connections, some open and closing:
>./threaded_server
serer sock listening: (3)
Connection From: 127.0.0.1:56523 (4)
Received From: 127.0.0.1:56523 (4): testing 1
Connection From: 127.0.0.1:56525 (5)
Received From: 127.0.0.1:56525 (5): testing 2
Connection From: 127.0.0.1:56526 (6)
Received From: 127.0.0.1:56526 (6): testing 3
Client Closed: 127.0.0.1:56526 (6)
Connection From: 127.0.0.1:56527 (7)
Received From: 127.0.0.1:56527 (7): testing 4
Client Closed: 127.0.0.1:56527 (7)
Client Closed: 127.0.0.1:56525 (5)
Client Closed: 127.0.0.1:56523 (4)
The starter code for this task is purposely light, but all relevant code is available to you. You're primary task is to determine how to properly parallelize the code for threading.
3 PART 2: Parallel Prime Sieve
Prime numbers are vital to many computer science problems, and so quickly finding prime numbers is an important process. There are many fast ways to find and identify primes, and in this part of the lab you will implement a parallel version of, perhaps, the simpilist prime finding routine, the Sieve of Eratosthenes. This routine was discovered in the times of Ancient Greece, it still one of the fastest algorithms today. It can also be trivially paralyzed.
The sieve works as follows. First, starting with smallest prime, 2, all factors of 2 are marked within the range of values we are interested. For example, if we are interested in finding all primes less than 30, we would first mark all factors of 2.
1 *2* 3 -4- 5 -6- 7 -8- 9 -10- 11 -12- 13 -14- 15 -16- 17 -18- 19 -20- 21 -22- 23 -24- 25 -26- 27 -28- 29 -30-
The next lowest non marked values must be a prime, and then we can mark (or sieve) all factors of that prime.
1 *2* *3* -4- 5 -6- 7 -8- -9- -10- 11 -12- 13 -14- -15- -16- 17 -18- 19 -20- -21- -22- 23 -24- 25 -26- -27- -28- 29 -30-
We can continue this process until we reach a prime greater than
sqrt(30)
, and then we know all remaining non-marked values must be
prime because factors of the value must be less than sqrt(30)
. The
sqrt(30)
is about 6 so after marking factors of 5, we've found all the primes
1 *2* *3* -4- *5* -6- 7 -8- -9- -10- 11 -12- 13 -14- -15- -16- 17 -18- 19 -20- -21- -22- 23 -24- -25- -26- -27- -28- 29 -30-
Now we can identify all non-marked values as prime:
1 *2* *3* -4- *5* -6- *7* -8- -9- -10- *11* -12- *13* -14- -15- -16- *17* -18- *19* -20- -21- -22- *23* -24- -25- -26- -27- -28- *29* -30-
In this lab, we are interested in identifying the 5 largest values
less than a designated value. Here is a implementation
(slow_sieve
) that will sieve and print the 5 largest values.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> int main(int argc, char *argv[]){ //print the largest prime less than value long top; //top value to check through char * numbers; //index is the number, value is either // -1 : currently being sieved by a thread // 0 : unchecked value, potential prime // 1 : composite value //argument checks if( argc >= 2){ top = atol(argv[1]); }else{ fprintf(stderr, "ERROR: require top value\n"); exit(1); } if(top < 3){ fprintf(stderr, "ERROR: invalid top value, requre >= 3\n"); exit(1); } //allocate array of numbers numbers = calloc(top,1); long p=2; //prime seiving long q; //a multiple of p long i; //iterator for multiple //only need to sieve the bottom sqrt(top) primes while(p < sqrt(top)){ //sieve routine for(i=2,q=p*i;q<top;q=p*++i){ //mark q = p*2,p*3, ... , p*n where p*n < top numbers[q] = 1; //mark composite } //find the next prime to sieve while(p<sqrt(top)){ p++; if(numbers[p] == 0) break; } } //working backwards, print the top 5 primes long j=0; for(i=top-1; i>0 && j < 5 ; i--){ if(numbers[i] <= 0){ printf("Larges Prime: %ld\n",i); j++; } } return 0; }
Your goal is to write a threaded parallel version of this
routine. The basic idea is that you will create n
threads, each
will start with a different initial prime and find next primes to
sieve based on a shared numbers
array. Once the bottom sqrt(top)
primes are sieved, the largest primes can be printed to the screen.
Of course, the goal of writing this routine using threads is that it should be faster than the non-parallel version. So you will need to engineer yours to ensure that condition, but just throwing threads at it is not always going to make it faster. You will run some experiments to test the speed.
3.1 Task 2
For this task change into the prime-sieve
directory where you will
find two files:
slow-sieve.c
a standard implementation of the prime sieveparallel-sieve.c
a parallel version of the sieve for you to complete
The routine for parallelization can be found in the source code. The general idea is that each thread will share a numbers array that is being sieved, where the index represents the number, and the value is the marking. The parallel processing comes from each thread starting with different initial prime. To bootstrap that process, I've hard-coded the first 100 primes.
The arguments to parallel_sieve
is
parallel_sieve top-value [threads]
The default number of threads is 1, and the max number is 100.
Here is some sample output. Note we can use the time
function to see
how long the routine runs for:
aviv@saddleback: prime_sieve $ time ./slow_sieve 100000000
Larges Prime: 99999989
Larges Prime: 99999971
Larges Prime: 99999959
Larges Prime: 99999941
Larges Prime: 99999931
real 0m1.248s
user 0m1.236s
sys 0m0.012s
aviv@saddleback: prime_sieve $ time ./parallel_sieve 100000000 2
Largest Prime: 99999989
Largest Prime: 99999971
Largest Prime: 99999959
Largest Prime: 99999941
Largest Prime: 99999931
real 0m0.772s
user 0m1.519s
sys 0m0.008s
aviv@saddleback: prime_sieve $ time ./parallel_sieve 100000000 3
Largest Prime: 99999989
Largest Prime: 99999971
Largest Prime: 99999959
Largest Prime: 99999941
Largest Prime: 99999931
real 0m0.742s
user 0m2.171s
sys 0m0.008s
aviv@saddleback: prime_sieve $ time ./parallel_sieve 1000000000 3
Largest Prime: 999999937
Largest Prime: 999999929
Largest Prime: 999999893
Largest Prime: 999999883
Largest Prime: 999999797
real 0m8.312s
user 0m24.662s
sys 0m0.064s
Additionally, in the README file you are required to answer questions and run some experiments with your parallel sieve:
a) For just 2 threads, at what top value do you start to see
improvements for your parallel sieve over the slow sieve?
b) For finding the top primes less than 1000000, how much faster is
your parallel sieve (2-threads) compared to the slow sieve? (Use the
average across 5 runs)
c) For finding the top prines less than 1000000, what is the optimal
number of threads for the fastest resuts on a lab computer? Fill in
the table below, and come up with a *conclusion and explanation*.
*** EXPLANATION ***
(DON'T FORGET TO FILL ME IN)
Runs
.........................................................
Threads | 1 | 2 | 3 | 4 | 5 | AVG |
----------|--------|---------|--------|--------|--------|---------|
2 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
3 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
4 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
5 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
6 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
7 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
8 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
9 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
10 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
20 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
40 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
60 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
80 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|
100 | | | | | | |
----------|--------|---------|--------|--------|--------|---------|