代写 R C data structure Java shell socket parallel concurrency compiler operating system database graph software network Go Chapter 5 Concurrent Computing

Chapter 5 Concurrent Computing
What is in This Chapter ?
This chapter will introduce you to the basics of concurrent computing. We first discuss some types of concurrent systems and a few issues/concerns that we must be aware of when having more than one task being performed at the same time. We then discuss process management at the unix shell level and then at the programming level, with functions like fork(), exec(), wait() and system() calls. The next section discusses inter-process communication (IPC) and the use of signals to inform other processes when tasks are complete. The use of TCP sockets and Datagram sockets are then discussed as they pertain to client/server models. Finally, threads are discussed, along with the need to use semaphores & mutexes to facilitate proper resource-sharing.

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 5.1 Concurrent Systems
When we start out to learn how to program, we find it easiest to focus on one task at a time. That is, we imagine our program as being run by a single computer that simply follows the instructions that we give it, based on our source code. It is challenging enough to learn how to program well with a single program.
However, the real world is not so simple. In reality, many things are happening all around us at the same time. In English, the word concurrent means “occurring or operating at the same time”. In computer science, the term concurrency implies that multiple programs (or processes) are working together at the same time … hopefully to work in agreement at accomplishing some task. Here is a definition extracted from wikipedia:
Concurrent computing is a form of computing in which several computations are executed during overlapping time periods (i.e., concurrently) instead of sequentially (i.e., one completing before the next starts).
A large system makes use of concurrent computing when it is (a) multithreaded, (b) has multiple processes or (c) is distributed. Here is a diagram showing all three. A host computer may run multiple processes (i.e., programs) each working together to perform some task in the system. A single process may have multiple threads running at the same time … all working together. Finally, processes running on different machines on a network may be interacting together, forming a distributed system. Usually, the user interacts with just one process.
– 171 –

COMP2401 – Chapter 5 – Concurrent Computing
We will examine each of these three forms of concurrency.
Distributed Systems
A distributed system is typically a large program that executes over multiple physical host machines. Usually, these machines are in different locations, cities or even countries. The interaction is over a network. This network may be:
• Intranet – a network internal to an organization
• Internet – a public network, external to all organizations
Winter 2019
One interesting aspect about distributed computing is that each host machine has different resources. That is, they may have different CPUs, different processing capabilities, different file systems, etc..
It sounds a bit complicated (and slower) to have different types of computers interacting over a network. Why would anyone want to do distributed computing ? Here are some reasons
• Speed: A single host may have insufficient processing power to complete a task in a reasonable time. Having other hosts join in on the work … it will hasten task completion.
• Necessity: Often clients need to connect to servers which are in different physical locations. Completing the task-at-hand may require connection to various servers to obtain database information, to record transactions, etc..
• Convenience: Users may need to connect to a host that is not in the same location. Multi-Process Systems
A multi-process system is a system where multiple processes (i.e., executables) are running at the same time and communicating with one another to accomplish a task. The executables need not be unique. They have my multiple copies of the same program running.
Each executable has its own independent control flow and
virtual memory. That is, it operates on its own, although it may rely on data and instructions from other processes in order to complete its individual task. The operating system contains mechanisms that allow Inter-Process Communication (IPC) to allow processes to communicate, usually to have access to shared data.
As with distributed systems, it may seem like we are complicating things by having multiple processes communicate through the operating system. Why would anyone want to implement multi-process systems ?
– 172 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 Here are a couple of reasons:
• Simplifying: Often there are many different tasks to perform which may be independent from one another. It can be easier to schedule a different process for each task.
• Resource Management: Certain tasks can be “assigned to” a particular resource (e.g., client to communicate with user, server to handle requests, process to regulate access to database), reducing the need for multiple processes to access the same resources. As a result, the system can reduce bottlenecks and operate more efficiently.
Multi-Threaded Systems
A multi-threaded system is a single process with multiple control flows. That is … multiple tasks are performed by the same CPU but they take turns by sharing the CPU’s processing time. The threads share the same virtual memory, address space and resources as they operate in the same process. There is a need at times to synchronize different threads in order to avoid race conditions and deadlocks.
The idea of a multi-threaded system is similar to a multi-process system in that they are often used when different tasks are to be performed. In the multi-threaded system, however, the tasks are usually dependent on each other. The main advantage of using multiple threads is:
• Simplicity: It is conceptually simpler to think of two tasks being done separately at the same time, even though they are sharing the CPU … taking turns to get their task done.
Some situations where multi-threading is often beneficial is:
• Handling user input. One thread blocks and waits for incoming requests, while another thread processes requests that have already come in.
• Quick refresh. Sometimes it is nice to have a thread responsible for refreshing the user interface (e.g., graphics/animation) while the program continues processing.
There are a few (potentially serious) issues that may arise when doing concurrency. As a result, it can be more difficult to write software for concurrent systems. It can also be difficult to debug concurrent systems.
Here are some of these issues:
– 173 –

COMP2401 – Chapter 5 – Concurrent Computing
Winter 2019
1. Shared Resources. Multiple process (or threads) will at times need the same resource. There needs to be some coordination rules so that this sharing takes place decently and respectfully. Typically, shared resources are files and variables.
• When accessing a file, it can be “locked” for use by one process/thread so that others cannot access it while it is in use. Of course, a process/thread that “hogs” a file resource can be slowing down the system if not careful.
• When accessing a shared variable, a semaphore or mutex can be used:
A mutex (mutual exclusion object) is a program object that is created so that multiple program threads can take turns sharing the same resource, such as access to a file. Only the thread that locked or acquired the mutex can unlock it.
A semaphore is a variable used to control access to a common resource by multiple processes. It is a generalization of a mutex. A thread waiting on a semaphore can be signaled by a different thread so that it can have access.
2. Deadlocks. This is a condition that can occur which is similar to the notion of a traffic jam. It is a condition in which multiple threads/processes are blocked … all waiting for a condition that will never occur. It is always due to improper handling of semaphores or mutexes. Careful system design will reduce the likelihood of deadlocks occurring, although sometimes deadlocks occur due to unforeseen situations inherent to the problem at hand.
3. Race Conditions. This is a timing problem in which the correctness of a program depends on one thread reaching a point in control flow before another thread. That is, some things have gotten out of order. You can imagine the scenario, for example, of trying to process data before it has been completely entered. Sometimes we have to handle such potential problems because the order that things are processed in is never guaranteed.
5.2 Process Management
Recall that a process is a running executable (e.g., a running program).
managed by the operating system.
Processes are
Process Management involves allocating resources to processes, enabling processes to share and exchange information, protecting the resources of each process from other processes and enabling synchronization among processes.
– 174 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019
The operating system is primarily involved with managing the processes, but as software system developers, we need to understand a little about how it is done so that we can make use of multiple processes when we write our programs. In particular, we need to know how too start (i.e., spawn) a process, how to stop and pause it, and how to modify the behaviors of a process using signals.
There are two ways that we can manage processes:
• Using shell commands – manually as a user of a system
• Using system calls – automatically through other programs/processes
How are processes managed ? The operating system maintains certain information about each process that has been created. Each process has the following:
• Process Identifier (PID) – unique to each process
• Parent Process Identifier (PPID) – the process that spawned it
• Address Space and Virtual Memory – code segment, data segment, stack, heap
• Control Flow(s)
Let’s look first at how to manage a process. The simplest way is from a shell. We can start a process in the foreground or in the background. You have already done this many times. Each time you run your code, for example, you are starting a process. Most of the time, we run it in the foreground. However, you can use the & sign to run a process in the background. Recall that this runs emacs in the foreground (i.e., we cannot use the shell until emacs completes):
And the & allows us to run emacs in the background (i.e., we can continue to use the shell):
Consider the following program that runs “forever”:
student@COMPBase:~$ emacs helloWorld.c student@COMPBase:~$
student@COMPBase:~$ emacs helloWorld.c& student@COMPBase:~$
Code from shellProcess.c
#include
#include
int main() {
int i = 1;
while (1) {
printf(“The ants go marching %d by %d, hurrah, hurrah.\n”, i, i); ++i;
sleep(1);
} }
– 175 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 The code displays a message and counter repeatedly, with a 1 second pasues (caused by the
sleep(1) command which is defined in the unistd.h header) in between the messages. We can run this program in the background in our shell window by using the & symbol:
student@COMPBase:~$ gcc -o sheppProcess shellProcess.c student@COMPBase:~$ ./shellProcess &
[2] 2513
student@COMPBase:~$ The ants go marching 1 by 1, hurrah, hurrah. The ants go marching 1 by 1, hurrah, hurrah.
The ants go marching 2 by 2, hurrah, hurrah.
The ants go marching 3 by 3, hurrah, hurrah.
The ants go marching 4 by 4, hurrah, hurrah.
The ants go marching 5 by 5, hurrah, hurrah.
One thing to notice is that when we run the program, we immediately get the PID which is 2513. This allows us to stop the process at a later time.
You will also notice that the process continually displays information to the system shell window that we are using. Because of this, it is a little hard to be able to continue to use the shell window for other commands as it keeps printing stuff out and scrolling.
At any time, we can use the ps command to get a list of running processes. Assume that we did a ps while the shellProcess command was still running. Here is what we might see:
student@COMPBase:~$ ps
PID TTY
2366 pts/17
2495 pts/17
2513 pts/17
2527 pts/17
TIME CMD
00:00:00 bash
00:00:00 emacs
00:00:00 shellProcess
00:00:00 ps
student@COMPBase:~$
This list above shows the current running processes from this terminal window. Notice that the bash shell is running, which allows us to enter commands. Also, the emacs editor is opened and running (it happens to have the shellProcess.c file opened). Notice as well that the shellProcess program is running. Also, the ps command that we ran to get this list … it itself is a running process.
If we want more detail on the running process, we can use ps -l as follows:
student@COMPBase:~$ ps -l
F S UID PID PPID 0 S 1002 2366 2359 0 S 1002 2495 2366 0 S 1002 2513 2366 0 R 1002 2543 2366 student@COMPBase:~$
C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 80 0 – 2034 wait
0 80 0 – 30417 poll_s 0 80 0 – 549 hrtime 0800-2174-
pts/17 00:00:00 bash
pts/17 00:00:00 emacs
pts/17 00:00:00 shellProcess
pts/17 00:00:00 ps
– 176 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 You can see some extra information here such as the size (SZ) of the process running (in
bytes) as well as the PPID that spawned the process and the user ID (UID).
The command ps aux command gives different information and lists more processes. Here is
what you may see (although I removed much of the output to reduce space):
student@COMPBase:~$ ps aux
USER
root
root
root 3 0.0 0.0

syslog
root
avahi
message+
avahi
453 0.0 0.1
462 0.0 0.1
474 0.0 0.1
482 0.0 0.2
486 0.0 0.0
498 0.0 0.2
Ssl 14:32 0:00 /usr/sbin/rsysl
PID %CPU %MEM VSZ RSS TTY
STAT START
Ss 14:31
S 14:31
S 14:31 0:00 [ksoftirqd/0]
lp

student 1315 0.0 0.1
student 2411 0.0 0.2
student 2495 0.0 2.0
student 2500 0.0 0.2
Ss 14:33 0:00 /lib/systemd/sy
Ss+ 14:34 0:00 bash
Sl 14:36 0:00 emacs shellProc
S 14:36 0:00 /usr/lib/i386-l
S 14:47 0:00 [kworker/0:0]
R+ 14:52 0:00 ps aux
root 2549 0.0 0.0 student 2562 0.0 0.1 student@COMPBase:~$
1 0.1 0.2
2 0.0 0.0
24064 4836 ? 00? 0 0 ?
30724 3004 ?
4136 3056 ?
5916 3116 ?
6856 4488 ?
5916 288 ?
11228 5196 ?
6368 4076 ?
8124 4476 pts/18
121668 41700 pts/17
12736 5120 ?
0 0 ?
8972 3224 pts/17
TIME COMMAND
0:01 /sbin/init spla
0:00 [kthreadd]
Ss 14:32
Ss 14:32
Ss 14:32
S 14:32 0:00 avahi-daemon: c
S 14:32 0:00 /usr/lib/cups/n
0:00 /lib/systemd/sy
0:00 avahi-daemon: r
0:00 /usr/bin/dbus-d
There are many parameters to the ps command, but they will not be discussed here.
To STOP a process, you can use the kill command.
student@COMPBase:~$ kill 2513 student@COMPBase:~$ ps
You just need to know the PID:
[2]+ Terminated ./shellProcess
PID TTY
2366 pts/17
2495 pts/17
2586 pts/17
TIME CMD
00:00:00 bash
00:00:00 emacs
00:00:00 ps
student@COMPBase:~$
After you kill a process, you will get a notification in the terminal window when you enter the next shell command. Above, you can see that once you use ps again, the process has been eliminated from the list of running processes. The kill -stop command is also used to temporarily stop/pause/suspend a process.
student@COMPBase:~$ kill -stop 2513 student@COMPBase:~$ ps -l
F S UID PID PPID 0 S 1002 2366 2359 0 S 1002 2495 2366 0 T 1002 2513 2366 0 R 1002 2543 2366 student@COMPBase:~$
C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 80 0 – 2034 wait
0 80 0 – 30417 poll_s 0 80 0 – 549 signal 0800-2174-
pts/17 00:00:00 bash
pts/17 00:00:00 emacs
pts/17 00:00:00 shellProcess
pts/17 00:00:00 ps
– 177 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 To continue the process again, we use kill -cont with the PID:
student@COMPBase:~$ kill -cont 2513 student@COMPBase:~$ ps -l
F S UID PID PPID 0 S 1002 2366 2359 0 S 1002 2495 2366 0 S 1002 2513 2366 0 R 1002 2543 2366 student@COMPBase:~$
C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 80 0 – 2034 wait
0 80 0 – 30417 poll_s 0 80 0 – 549 hrtime 0800-2174-
pts/17 00:00:00 bash
pts/17 00:00:00 emacs
pts/17 00:00:00 shellProcess
pts/17 00:00:00 ps
You can also use other shell commands to manage processes. For example, the jobs command displays a list of all running jobs.
Notice that the jobs command allows you to see what is running, what is currently stopped (or paused) and also what processes have just completed.
At any time, you can suspend the current running foreground process by pressing CTRL-Z. You may also kill the current running process by pressing CTRL-C.
You can use the fg command to resume the last suspended job, or you can use fg i to resume the job with id i. So, for example, in the above example, we could resume the shellProcess program by typing fg 2 into the shell. It will run in the foreground. We could resume it to run it in the background if we use bg 2 instead.
At this point, you should understand how to manage processes manually in the command shell window in Linux.
But in addition to managing processes from the command line, we can also do so within our C programs. There are 4 system calls that are related to process management:
• fork – spawns a clone of the current process
• exec – replaces executing code of current process with another program
• wait – pauses execution of a parent until a child process terminates
• system – runs a specified command as a shell command
We will now examine each of these one at a time…
student@COMPBase:~$ jobs
[1] Running
[2]+ Stopped
[3]- Done student@COMPBase:~$
emacs shellProcess.c &
./shellProcess
./wait
– 178 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 FORK
Consider first the fork() function in C. It creates a new process with the current process being the parent of the new process.
Consider this example:
Code from fork.c
#include
#include
int main() {
int childPID;
printf(“Forking…\n”);
childPID = fork();
At this point, both processes continue simultaneously running two copies of the remaining code.
if (childPID == 0) {
printf(“fork() returned 0 … so this is the spawned/child process\n”); for (int i=1; i<=24; i++) { printf("The ants go marching *%2d* by *%2d*, hurrah, hurrah.\n", i, i); usleep(500000); } } else { printf("fork() returned %d ... so this is the parent process\n", childPID); for (int i=1; i<=24; i++) { printf("The ants go marching %2d by %2d , hurrah, hurrah.\n", i, i); usleep(1000000); } } } Notice that the fork() function returns a PID. It is interesting that the original (i.e., parent) and the spawned (i.e., child) processes both continue with the same code. Hence, there are two copies of the same code running. But after the return call, the code branches based on the return value of fork(). For the child (i.e., spawned) process, the return value is 0. For the parent, the returned value is the new process’ PID (unless there was an error, then -1 is returned). The IF statement checks this return value and allows one chunk of code to be executed by the child and the other by the parent. Here is an example of the output you would see. The usleep() function sleeps for the specified number of microseconds. Forking... fork() returned 3002 ... so this is the parent process The ants go marching 1 by 1 , hurrah, hurrah. fork() returned 0 ... so this is the spawned process (i.e., child) The ants go marching * 1* by * 1*, hurrah, hurrah. The ants go marching * 2* by * 2*, hurrah, hurrah. The ants go marching 2 by 2 , hurrah, hurrah. The ants go marching * 3* by * 3*, hurrah, hurrah. - 179 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 The ants go marching * 4* by * 4*, hurrah, hurrah. The ants go marching 3 by 3 , hurrah, hurrah. The ants go marching * 5* by * 5*, hurrah, hurrah. The ants go marching * 6* by * 6*, hurrah, hurrah. The ants go marching 4 by 4 , hurrah, hurrah. The ants go marching * 7* by * 7*, hurrah, hurrah. The ants go marching * 8* by * 8*, hurrah, hurrah. The ants go marching 5 by 5 , hurrah, hurrah. The ants go marching * 9* by * 9*, hurrah, hurrah. The ants go marching *10* by *10*, hurrah, hurrah. The ants go marching 6 by 6 , hurrah, hurrah. The ants go marching *11* by *11*, hurrah, hurrah. The ants go marching *12* by *12*, hurrah, hurrah. The ants go marching 7 by 7 , hurrah, hurrah. The ants go marching *13* by *13*, hurrah, hurrah. The ants go marching *14* by *14*, hurrah, hurrah. The ants go marching 8 by 8 , hurrah, hurrah. The ants go marching *15* by *15*, hurrah, hurrah. The ants go marching *16* by *16*, hurrah, hurrah. The ants go marching 9 by 9 , hurrah, hurrah. The ants go marching *17* by *17*, hurrah, hurrah. The ants go marching *18* by *18*, hurrah, hurrah. The ants go marching 10 by 10 , hurrah, hurrah. The ants go marching *19* by *19*, hurrah, hurrah. The ants go marching *20* by *20*, hurrah, hurrah. The ants go marching 11 by 11 , hurrah, hurrah. The ants go marching *21* by *21*, hurrah, hurrah. The ants go marching *22* by *22*, hurrah, hurrah. The ants go marching 12 by 12 , hurrah, hurrah. The ants go marching *23* by *23*, hurrah, hurrah. The ants go marching *24* by *24*, hurrah, hurrah. The ants go marching 13 by 13 , hurrah, hurrah. The ants go marching 14 by 14 , hurrah, hurrah. The ants go marching 15 by 15 , hurrah, hurrah. The ants go marching 16 by 16 , hurrah, hurrah. The ants go marching 17 by 17 , hurrah, hurrah. The ants go marching 18 by 18 , hurrah, hurrah. The ants go marching 19 by 19 , hurrah, hurrah. The ants go marching 20 by 20 , hurrah, hurrah. The ants go marching 21 by 21 , hurrah, hurrah. The ants go marching 22 by 22 , hurrah, hurrah. The ants go marching 23 by 23 , hurrah, hurrah. The ants go marching 24 by 24 , hurrah, hurrah. Notice how the output between the two processed is interlaced. Your code can fork many times. But remember ... each time that the code forks, your child code may fork as well (depending on how you structure your code). This could cause forking indefinitely. There is a limit to how many forks the operating system will allow. It maintains a process table ... which has a finite capacity. It may be best not to test that limit 😉. A fork bomb is a process that continually replicates itself and depletes available system resources. A rabbit virus uses this strategy as a denial-of-service attack to slow down and potentially crash a system. The following code takes an integer as a command-line-argument and then does a double forking that many times. If the number is high enough, it can slow down and crash the system. - 180 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 Code from forkTooMuch.c #include
#include
#include
int main(int argc, char *argv[]) {
int pid, ppid, count;
if (argc < 2) count = 1; else count = atoi(argv[1]); pid = getpid(); printf("Parent: %d\n", pid); for (int i=0; i
#include
#include
int main() {
char buffer[80];
char *args[2];
int childPID;
printf(“This program is running.\n”);
printf(“Now let’s run the userInput program …\n”);
strcpy(buffer, “./userInput”);
args[0] = “userInput”;
args[1] = NULL;
childPID = execvp(buffer, args);
./ is needed here if that is how we run our programs in the shell.
// This code is never reached, unless the userInput program does not exist.
printf(“We returned from that program, which ran with PID = %d\n”, childPID);
printf(“It appears, therefore, that the userInput program was not found.\n”); }
Here is what happens when we run:
This program is running.
Now let’s run the userInput program …
What is your name ?
Mark
Hello, Mark
Of course, if the userInput program cannot be found, we would get this output:
This program is running.
Now let’s run the userInput program …
We have returned from that program, which ran with PID = -1
It appears, therefore, that the userInput program was not found.
– 182 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 Here is a variation that allows us to pass command line arguments into a program through a
call to exevvp(). It makes use of our cmdLineArgs program that we wrote in chapter 3:
Code from execTest2.c
#include
#include
#include
int main() {
char buffer[80];
char *args[4];
int childPID;
printf(“This program is running.\n”);
printf(“Now let’s run the cmdLineArgs program …\n”);
strcpy(buffer, “./cmdLineArgs”);
args[0] = “cmdLineArgs”;
args[1] = “one”;
args[2] = “two”;
args[3] = NULL;
childPID = execvp(buffer, args);
We set up the command-line args here, making sure to end with NULL.
// This code is never reached, unless the cmdLineArgs program does not exist.
printf(“We returned from that program, which ran with PID = %d\n”, childPID);
printf(“It appears, therefore, that the cmdLineArgs program was not found.\n”); }
Here is the expected output:
This program is running.
Now let’s run the cmdLineArgs program …
There are 3 arguments
Argument 0 is cmdLineArgs
Argument 1 is one
Argument 2 is two
WAIT
The wait() function in C allows us to put a delay in a parent program so that it waits until one of its child processes has completed. It returns the PID of the child that completes, if successful, otherwise it returns -1. In addition, there is a waitpid() command that allows the parent process to delay until a specific child process has completed.
Consider this program which shows a basic use of the wait() function:
– 183 –

COMP2401 – Chapter 5 – Concurrent Computing
Winter 2019
Code from wait.c
#include
#include
#include
int main() {
int status, child;
printf(“I am the parent (PID=%d)\n”, getpid()); printf(“I am spawning a child …\n”);
child = fork();
if (child == 0) {
printf(” I am the child (PID=%d) … I will sleep for 2sec\n”, getpid()); sleep(2);
printf(” I am awake!\n”);
}
else {
printf(“I am now waiting for my child to wake up …\n”);
wait(&status);
printf(“It looks like my child is awake, so I will quit in 2sec …\n”); sleep(2);
}
printf(“Process %d terminating.\n”, getpid()); }
Here is the output:
Parent’s output in blue.
Child’s output in red.
I am the parent (PID=24439)
I am spawning a child …
I am now waiting for my child to wake up …
I am the child (PID=24440) … I will sleep for 2sec
I am awake!
Process 24440 terminating.
It looks like my child is awake, so I will quit in 2sec … Process 24439 terminating.
The above example had only one child. The wait() command allows the process to wait for ANY child to complete. The PID child that completes will be returned from the wait() command.
Here is an example that spawns 5 children, each one sleeping for a random number of seconds, then waking up and quitting. The parent spawns all 5 children and then waits for each one to complete. Note that the order in which the children complete will be different from the order that they are spawned in, due to the random sleep time.
Notice that the children each quit by using the exit(0) function. The parameter to the exit() function is arbitrary, but zero usually indicates that all went well and negative numbers or positive numbers usually indicate error codes.
– 184 –

COMP2401 – Chapter 5 – Concurrent Computing
Winter 2019
Code from multiChildWait.c
#include
#include
#include
#include
int main() {
int status, child, parent, children[5], sleepTimes[5];
printf(“I am the parent (PID=%d)\n”, parent = getpid());
// Choose 5 random sleep times
for (int i=0; i<5; i++) { sleepTimes[i] = rand()%5 + 5; } printf("I am spawning 5 children ...\n"); for (int i=0; i<5; i++) { if (getpid() == parent) children[i] = fork(); if (children[i] == 0) { printf(" I am a child (PID=%d) ... I will sleep for %dsec\n", getpid(), sleepTimes[i]); sleep(sleepTimes[i]); printf(" I am awake! Process %d terminating.\n", getpid()); exit(0); } } printf("I am now waiting for all of my children to wake up ...\n"); for (int i=0; i<5; i++) { child = wait(&status); printf("It looks like one of my children (PID=%d) has awoken.\n", child); } printf("All children are done. Process %d terminating.\n", getpid()); } Here is some output: I am the parent (PID=3099) I am spawning 5 children ... I am now waiting for all of my children to wake up ... I am a child (PID=3104) ... I will sleep for 8sec I am a child (PID=3103) ... I will sleep for 5sec I am a child (PID=3102) ... I will sleep for 7sec I am a child (PID=3101) ... I will sleep for 6sec I am a child (PID=3100) ... I will sleep for 8sec I am awake! Process 3103 terminating. It looks like one of my children (PID=3103) I am awake! Process 3101 terminating. It looks like one of my children (PID=3101) I am awake! Process 3102 terminating. It looks like one of my children (PID=3102) I am awake! Process 3104 terminating. It looks like one of my children (PID=3104) I am awake! Process 3100 terminating. It looks like one of my children (PID=3100) has awoken. has awoken. has awoken. has awoken. has awoken. All children are done, so I will quit now. Process 3099 terminating. - 185 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 The waitpid() function can be used to wait for a particular child to complete. It returns the child PID if successful, otherwise -1 if an error occurred. As an example, we could determine the child that would likely take the longest to complete the work and then wait just for that child. The sleep times are hardcoded to make it clearer: Code from waitpid.c #include
#include
#include
#include
int main() {
int status, child, parent, children[5];
int sleepTimes[5] = {1, 5, 8, 2, 4};
printf(“I am the parent (PID=%d)\n”, parent = getpid()); printf(“I am spawning 5 children …\n”);
for (int i=0; i<5; i++) { if (getpid() == parent) children[i] = fork(); if (children[i] == 0) { printf(" I am a child (PID=%d) ... I will sleep for %dsec\n", getpid(), sleepTimes[i]); sleep(sleepTimes[i]); printf(" I am awake! Process %d terminating.\n", getpid()); exit(0); } } printf("I am now waiting for child 3 to wake up ...\n"); child = waitpid(children[2], &status, 0); printf("It looks like my slowest child (PID=%d) has awoken.\n", child); printf("All children are done. Process %d terminating.\n", getpid()); } Here is the output: I am the parent (PID=3303) I am spawning 5 children ... I am now waiting for child 3 to wake up ... I am a child (PID=3308) ... I will sleep for 4sec I am a child (PID=3307) ... I will sleep for 2sec I am a child (PID=3306) ... I will sleep for 8sec I am a child (PID=3305) ... I will sleep for 5sec I am a child (PID=3304) ... I will sleep for 1sec I am awake! Process 3304 terminating. I am awake! Process 3307 terminating. I am awake! Process 3308 terminating. I am awake! Process 3305 terminating. I am awake! Process 3306 terminating. It looks like my slowest child (PID=3306) has awoken. All children are done. Process 3303 terminating. Notice that the waitpid() function takes a third parameter ... these are options. discuss the various status results from the function, nor these options. Please see the man pages if you are interested in more details. - 186 - We will not COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 SYSTEM The system() function in C allows us to run the specified command (or program) as a shell command. When called, the process blocks until the system call is done and then control returns to the program. The return value from this function call is the value that is returned from the system call command, or -1 if an error has occurred. Here is a simple program that calls a couple of shell commands as well as running another program from within it: Code from systemCall.c #include
#include
int main() {
// Show a list of files
system(“clear”);
system(“ls -l”);
printf(“\n”);
// Find out who the user is
system(“who”);
printf(“\n”);
// Run the userInput program
system(“./userInput”);
}
Here is the output, which of course depends on the directory contents:
cmdLineArgs
execTest
execTest2
execTest2.c
student tty7
execTest.c forkTooMuch.c shellProcess.c wait fork multiChildWait systemCall wait.c fork.c multiChildWait.c systemCall.c waitpid
forkTooMuch shellProcess
2018-06-05 10:45 (:0)
userInput
waitpid.c
What is your name ?
Mark
Hello, Mark
5.3 Inter-Process Communication
Now that you have a good understanding of how to create multiple processes, you probably realize that this is most useful when the processes have the ability to communicate with one another as they are running. This relates to reality since people often work as a team, each doing their own task, yet coordinating through careful communication.
In computer science, this communication is done through … – 187 –

COMP2401 – Chapter 5 – Concurrent Computing
Inter-Process Communication (IPC) is the sending
and receiving of information between processes.
Communication between processes can occur on the same host machine or between processes running on different hosts across a network.
There are two main approaches to IPC. The first (and most basic) is that of using signals:
A signal is a value (integer) sent from one process to another.
A signal is used as a rudimentary form of communication to do simple things like informing processes of an error or telling a process to terminate. It is a very limited kind of communication that can only be used between processes running on the same host machine.
Winter 2019
In C, there are a fixed set of existing signal values defined in the header file, but only two are user-defined:
#define SIGHUP
#define SIGINT
#define SIGQUIT
#define SIGILL
#define SIGTRAP
#define SIGABRT
#define SIGIOT
#define SIGBUS
#define SIGFPE
#define SIGKILL
#define SIGUSR1
#define SIGSEGV
#define SIGUSR2
#define SIGPIPE
#define SIGALRM
#define SIGTERM
#define SIGSTKFLT #define SIGCLD
#define SIGCHLD
#define SIGCONT
#define SIGSTOP
#define SIGTSTP
#define SIGTTIN
#define SIGTTOU
#define SIGURG
#define SIGXCPU
#define SIGXFSZ
#define SIGVTALRM #define SIGPROF
#define SIGWINCH #define SIGPOLL
#define SIGIO
#define SIGPWR
#define SIGSYS
#define SIGUNUSED 31
1 /* Hangup (POSIX). */
2 /* Interrupt (ANSI). */
3 /* Quit (POSIX). */
4 /* Illegal instruction (ANSI). */
5 /* Trace trap (POSIX). */
6 /* Abort (ANSI). */
6 /* IOT trap (4.2 BSD). */
7 /* BUS error (4.2 BSD). */
8 /* Floating-point exception (ANSI). */ 9 /* Kill, unblockable (POSIX). */
10 /* User-defined signal 1 (POSIX). */
11 /* Segmentation violation (ANSI). */
12 /* User-defined signal 2 (POSIX). */
13 /* Broken pipe (POSIX). */
14 /* Alarm clock (POSIX). */
15 /* Termination (ANSI). */
16 /* Stack fault. */
SIGCHLD /* Same as SIGCHLD (System V). */
17 /* Child status has changed (POSIX). */
18 /* Continue (POSIX). */
19 /* Stop, unblockable (POSIX). */
20 /* Keyboard stop (POSIX). */
21 /* Background read from tty (POSIX). */
22 /* Background write to tty (POSIX). */
23 /* Urgent condition on socket (4.2 BSD). */
24 /* CPU limit exceeded (4.2 BSD). */
25 /* File size limit exceeded (4.2 BSD). */
26 /* Virtual alarm clock (4.2 BSD). */
27 /* Profiling alarm clock (4.2 BSD). */
28 /* Window size change (4.3 BSD, Sun). */
SIGIO /* Pollable event occurred (System V). */
29 /* I/O now possible (4.2 BSD). */
30 /* Power failure restart (System V). */
31 /* Bad system call. */
There are two steps to using signals: (1) install a signal handler, (2) send a signal. – 188 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 Installing a signal handler is really just a matter of indicating which function will be called when
the signal is received. It is similar to setting up an event handler in JAVA.
Every signal should have its own signal handler. There is a default signal handler for every signal … which, by default, will usually terminate the program.
To install our own signal handler, we use the signal() function which takes the signal number/code (i.e., SIGUSR1 or SIGUSR2) as its first parameter and the signal-handler function name as its second parameter. The signal handler function must take a single int parameter and have a void return type. Optionally, instead of supplying a signal handler function, we can use the constant SIG_IGN to tell the OS to ignore the signal and do nothing … or we can use SIG_DFL to tell the OS to use the default signal handler.
Here is an example of a program that will wait for some incoming signal from another process. It does not do anything interesting, but it shows the mechanics of setting up inter-process communications between processes.
Code from handler.c
#include
#include
#include
#include
void handleSig1(int);
void handleSig2(int);
int main() {
signal(SIGUSR1, handleSig1);
signal(SIGUSR2, handleSig2);
printf(“\n HANDLER: Running (PID=%d)\n”, getpid());
// Go into an infinite loop
while (1)
sleep(1);
printf(“This line of code is never reached.\n”); }
void handleSig1(int i) {
printf(” HANDLER: Signal 1 has been received. Continuing…\n”);
}
void handleSig2(int i) {
printf(” HANDLER: Signal 2 has been received. Quitting…\n”); exit(SIGUSR2);
}
Notice that when the program receives signal 1, it prints a message and the program continues. When it receives signal 2, however, it stops running.
We will run this program in the background and then set up another program that allows us to send signals to it:
– 189 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019
student@COMPBase:~$ gcc -o handler handler.c student@COMPBase:~$ ./handler &
[8] 4018
student@COMPBase:~$
HANDLER: Running (PID=4018) student@COMPBase:~$
To send a signal to a process, we need to know the PID and the signal number. Then we make use of the kill() function which takes the PID as its first parameter and the signal number as its second parameter. The function will return -1 if there was a problem (e.g., process does not exist) and 0 otherwise. Now let us write the sending program:
Code from sender.c
#include
#include
#include
int main() {
int pid, choice, result;
printf(“SENDER: Enter PID that you want to signal: “); scanf(“%d”, &pid);
while (1) {
printf(“SENDER: Enter signal number (1 or 2), use 0 to quit: “); scanf(“%d”, &choice);
switch(choice) {
case 0: exit(0);
case 1: result = kill(pid, SIGUSR1); break;
case 2: result = kill(pid, SIGUSR2);
}
if (result == -1)
printf(“SENDER: *** Error sending signal to Process %d ***\n”, pid); }
}
The code allows us to first enter the PID of the process that we want to communicate with. Then it goes into an infinite loop allowing us to send repeated signals. The only two signals that we will send are SIGUSR1 and SIGUSR2 which are selected based on the value that the user enters. If the kill() function returns -1, then we know there was a problem (e.g., the process may no longer be running).
Assuming that the handler program is already running in the background, here is what we may see as output from this program. The values entered by the user are highlighted as yellow and the output from the handler program is shown in orange so that it is easier to see what is happening.
– 190 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019
student@COMPBase:~$ gcc -o sender sender.c student@COMPBase:~$ ./sender &
SENDER: Enter PID that you want to signal: 4018 SENDER: Enter signal number (1 or 2), use 0 to quit: 1
HANDLER: Signal 1 has been received. Continuing…
SENDER: Enter signal number (1 or 2), use 0 to quit: 1 HANDLER: Signal 1 has been received. Continuing…
SENDER: Enter signal number (1 or 2), use 0 to quit: 2 HANDLER: Signal 2 has been received. Quitting…
SENDER: Enter signal number (1 or 2), use 0 to quit: 1 SENDER: *** Error sending signal to Process 4018 *** SENDER: Enter signal number (1 or 2), use 0 to quit: 2 SENDER: *** Error sending signal to Process 4018 *** SENDER: Enter signal number (1 or 2), use 0 to quit: 0 [8]+ Exit 12 ./handler student@COMPBase:~$
Here is an example that shows how we can send a “kill” command (SIGKILL) to spawned child processes to have them stop right away. Notice the use of the system(“ps -T”). This will allow us to print out the running processes on the terminal that we are using so that we can see that the processes are started and stopped:
Code from stopChildren.c
#include
#include
#include
#include
int main() {
int parent, childProcess[5];
printf(“I am the parent (PID=%d)\n”, parent = getpid()); printf(“I am spawning 3 children …\n”);
for (int i=0; i<3; i++) { if (getpid() == parent) childProcess[i] = fork(); if (childProcess[i] == 0) { for (int j=30; j>0; j–) {
printf(”
sleep(1); }
exit(0); }
}
system(“ps -T”);
Child (PID=%d) sleeping for %d more sec\n”, getpid(), j);
printf(“I am now waiting for 3 seconds then will stop all the children …\n”); sleep(3);
for (int i=0; i<3; i++) kill(childProcess[i], SIGKILL); system("ps -T"); printf("I stopped all child processes ... terminating now.\n"); } - 191 - COMP2401 - Chapter 5 – Concurrent Computing Here is the output that can be expected: I am the parent (PID=4779) I am spawning 3 children ... Child (PID=4782) sleeping for 30 more sec Child (PID=4781) sleeping for 30 more sec Child (PID=4780) sleeping for 30 more sec Winter 2019 PID SPID TTY 3495 3495 pts/4 4779 4779 pts/4 4780 4780 pts/4 4781 4781 pts/4 4782 4782 pts/4 4783 4783 pts/4 4784 4784 pts/4 Parent is running Children are running I am TIME CMD 00:00:00 bash 00:00:00 stopChildren 00:00:00 stopChildren 00:00:00 stopChildren 00:00:00 stopChildren 00:00:00 sh 00:00:00 ps now waiting for Child (PID=4782) sleeping for 29 more sec Child (PID=4781) sleeping for 29 more sec Child (PID=4780) sleeping for 29 more sec Child (PID=4781) sleeping for 28 more sec Child (PID=4782) sleeping for 28 more sec Child (PID=4780) sleeping for 28 more sec Child (PID=4782) sleeping for 27 more sec Child (PID=4781) sleeping for 27 more sec Child (PID=4780) sleeping for 27 more sec 3 seconds then will stop all the children ... PID SPID TTY 3495 3495 pts/4 4779 4779 pts/4 4780 4780 pts/4 4781 4781 pts/4 4782 4782 pts/4 4785 4785 pts/4 4786 4786 pts/4 TIME CMD 00:00:00 bash 00:00:00 stopChildren 00:00:00 stopChildren 00:00:00 stopChildren 00:00:00 stopChildren 00:00:00 sh Children are no longer running 00:00:00 ps I stopped all child processes ... terminating now. Let us try dealing with the SIGINT signal. This is the signal that occurs when the system tries to interrupt the process. One way that we can generate the signal is to press the CTRL-C keys. By default, this quits the program. But we can disable this ... by ignoring that signal (not a good idea usually). Here is a program that does this. We’ll first ignore the CTRL-C for 5 seconds ... then we’ll handle it ourselves for 5 seconds by simply printing a message out, then finally we’ll spend the last 5 seconds with the restored default, which will allow us to quit the program.


Code from ignoreInterrupt.c
#include
#include
#include
#include
void ignoreMessage(int);
void sleep5();
– 192 –

COMP2401 – Chapter 5 – Concurrent Computing
Winter 2019
int main() {
printf(” Process %d running\n”, getpid()); printf(” Ignoring the interrupt signal…\n”); signal(SIGINT, SIG_IGN);
sleep5();
printf(“\n Really ignoring the interrupt signal…\n”);
signal(SIGINT, ignoreMessage); sleep5();
printf(“\n Restoring the default handler…\n”); signal(SIGINT, SIG_DFL);
sleep5();
printf(“\n All done!\n”);
}
void ignoreMessage(int x) {
printf(” Stop bugging me.\n”);
}
void sleep5() {
for (int i=1; i<=5; ++i) { sleep(1); printf(" Sleeping %d\n",i); } } Here is the output, showing ^C when CTRL-C was pressed: Process 4340 running Ignoring the interrupt signal... Sleeping 1 Sleeping 2 ^C Sleeping 3 Sleeping 4 ^C Sleeping 5 Really ignoring the interrupt signal... Sleeping 1 Sleeping 2 ^C Stop bugging me. Sleeping 3 Sleeping 4 ^C Stop bugging me. Sleeping 5 Restoring the default handler... Sleeping 1 ^C As you can see, simple communication between two processes can be quite simple. However, with the signaling approach, there are obvious limitations in that we can only signal another process ... we cannot really exchange data. - 193 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 Of course, we can “fake” data exchange by, for example, having one process write data to a file and then signal the other process to read the file when it is done. But this is cumbersome and also limited in regard to how many processes can be involved in this type of communication. A better way to do this is by using sockets: A socket is an endpoint for sending or receiving data between processes. You can think of two hosts communicating to one another through a physical cable (or through wifi these days). The socket is like the connector that we plug the cable into. Each host has its own socket and all communication to other hosts takes place through this socket connection. Since each computer/host on a network has a unique IP address, we will need to use this address in order to communicate with that host through the socket. It uniquely identifies a computer at the network layer. Also, since multiple processes may run on the same host machine, they too must be uniquely identifiable through a port number which will be unique to all applications running on that host. The port uniquely identifies a process (e.g., app) at the transport layer. Only a specific range of values can be used ... from 1025 to 65536 ... (0 through 1024 are reserved). Perhaps you can think of passing a physical note to a friend at school. The IP address corresponds to the address of the particular school ... while the port would correspond to the locker number in that school. Communication between the processes occurs through a couple of layers. Layer provides the means of sending packets of data from a source host to a destination host over one or more networks. It is basically like the mailman delivering letters from one building in one city to another building in another city. The Transport Layer is a conceptual layer that indicates how exactly the data is to be transferred from the source to the destination. There are two main strategies for doing this: (1) Transmission Control Protocol (TCP), and (2) User Datagram Protocol (UDP). The following diagram shows how things are organized: The Network - 194 - COMP2401 - Chapter 5 – Concurrent Computing There are 3 types of sockets: 1. Streamsockets • These are connection-based sockets. o Connection must first be established between the sender and receiver before any data exchange can take place (e.g., like making a phone call). o Connection must be closed (i.e., must hang up the phone) when communication is finished (i.e., no “call- waiting” option). • Best used for reliable packet delivery ... so that the packet is correct and in a reliable order. Winter 2019 • Works with the TCP (Transmission Control Protocol) method of data exchange. 2. Datagram sockets • These are connectionless sockets. o Don’t need to first establish a connection between sender and receiver, data is just sent out when ready (e.g., like mailing a package via Canada Post). • Best used for faster packet delivery. No need to establish a connection beforehand. • Works with the UDP (User Datagram Protocol) method of data exchange. • Disadvantage is that the packets can be corrupted, received out of order, lost altogether or delivered multiple times. 3. Rawsockets • Bypasses the Transport protocol all together. The basic idea being socket communications is as follows: 1. Each endpoint (i.e., sender and receiver) opens a socket (and connection is established if using stream sockets) 2. Packets are sent and received. 3. Eachendpointclosestheirsocket. - 195 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 Client Server Model - TCP In IPC, one commonly used type of architecture is that if the client/server model. In this model, one process acts as a server that receives requests from clients and then performs tasks accordingly. There may be more than one client sending requests to the server at any time. Let us look now at an example that uses Stream sockets to perform connection-based communications between two processes. We will run two processes on the same machine and have data passed back and forth between them. Starting with the server, we need to create a stream socket. This can be done with the socket() function which is defined in the header. The function will return an integer representing the socket descriptor (i.e., ID) or -1 if the socket cannot be opened for any reason, otherwise. The function takes three parameters with this template:
socket(, , )
There are many options for these parameters, but just a couple will be mentioned here.
The is the address domain family that we want to use:
• AF_INET = communications over a network
• AF_LOCAL = communications on the local host
The is the type of socket that we want to use:
• SOCK_STREAM = connection-based
• SOCK_DGRAM = connection-less
The is the protocol that we want to use:
• IPPROTO_TCP = Transmission Control Protocol
• IPPROTO_UDP = User Datagram Protocol
The opening of the socket can fail if:
• The implementation does not support the specified address family.
• No more file descriptors are available for this process/system.
• The protocol is not supported by the address family/implementation.
• The socket type is not supported by the protocol.
• The process does not have appropriate privileges.
• Insufficient resources were available in the system to perform the operation.
• Insufficient memory was available to fulfill the request.
– 196 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019
Once the socket has been opened, we then need to assign an IP address to the socket from which we will accept messages and we also need to assign a port number to the socket. We do this by using the bind() function which has this format:
bind(,

, )
The parameter is the socket descriptor (i.e., ID) that was returned from the socket() function call. The

, however, is a bit more complicated. It is a struct sockaddr data structure and the is the length of the struct sockaddr structure supplied as the 2nd parameter. The function will return -1 if an error occurred, otherwise 0.
What does the struct sockaddr look like ? Well, this is a protocol-independent structure. At the general level, it is defined like this:
struct sockaddr {
unsigned short sa_family; // address family char sa_data[14]; // protocol address
};
The sa_data field is quite general and allows 14 bytes to be adjustable for various types of protocols. We generally set things up for IPv4 (i.e., version 4 of the internet protocol) by using struct sockaddr_in instead, which is defined as follows:
struct sockaddr_in {
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8]; // unused
// e.g. AF_INET, AF_LOCAL
// port number
// see below
};
where in_addr is defined as follows:
struct in_addr {
unsigned long s_addr; // set to internet address
};
You may have noticed that if we add up the bytes required for sin_port, sin_adder and sin_zero … they add to the 14 bytes defined in sa_data from the sockaddr struct, since a long is only 4 bytes on the virtual machine that we are using. So the sin_zero field of the sockaddr struct is just a placeholder to use up the remaining required 14 bytes (that we do not need) in order to the sizeof(struct sockaddr_in) to be the same as sizeof(struct sockaddr). This will allow us to typecast (struct sockaddr_in*) to (struct sockaddr*) later.
Now … what should we set sin_family, sin_port and sin_addr to ? We can set the sin_family to AF_INET, or whatever we used to set up the socket. The sin_port number can be arbitrary (e.g., 6000). The sin_addr can be set to any internet address, but in our case, we just want to deal with the local host. We can set this as either INADDR_ANY or inet_addr(“127.0.0.1”).
There is one concern though in setting up the struct. The IP address and port number are to be sent over the internet as bytes but interpreted as ints and longs.
– 197 –

COMP2401 – Chapter 5 – Concurrent Computing
Winter 2019
Recall that some machines use little-endian format and some use big- endian format. So, sending out a short or a long from one machine that uses one format … might be misinterpreted if read in from a machine that uses a different format. To deal with this, there are some handy conversion functions for converting to a common ordering. As it turns out, network protocols assume big-endian format. The host format can be either format. Here are the functions that we can use to convert from the host format to the network format and vice versa:
htons() – convert short from host format to network format. htonl() – convert long from host format to network format. ntohs() – convert short from network format to host format. ntohl() – convert long from network format to host format.
Therefore, this is how we would set things up:
#define SERVER_PORT 6000 struct sockaddr_in address;
memset(&address, 0, sizeof(address)); // zeros the struct address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY); address.sin_port = htons((unsigned short) SERVER_PORT);
Once this has been set up, we can call bind() with the address variable: bind(serverSocket, (struct sockaddr *)address, sizeof(address));
Notice the typecast of the address. This is necessary since the function wants something of type sockaddr, not sockaddr_in.
After calling this function, we will also need to check to make sure that the bind() function did not return -1 before we continue.
Once the socket is opened and bound, we are ready to start listening for incoming requests. The listen() function is used to set the socket up for listening, which has this format:
listen(, )
Again, the socket descriptor is used. The is a value that indicates the number of pending connections that may be queued (i.e., the number of clients allowed to wait in line before being turned away). This can be set to something small, such as 5 or 10. A return value of 0 indicates that all went well, otherwise -1 is returned.
Finally, we need to use the accept() function to wait for and accept an incoming client request. It has the following format:
accept(, , ) – 198 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019
Once again, the socket descriptor is used. The is a struct sockaddr * just as we had used for the server address. This, however, is a pointer to a variable that will contain the client address once the message arrives.
The should represent the exact size of the client address struct. Once again, a return value of -1 is used to indicate that an error has occurred. When all went well, however, the accept() function returns a socket descriptor that corresponds to the client that just connected to the server.
At this point, we have established a one-on-one connection between the server and the client. We can now read in the information that was sent from the client by using the recv() function which has this format:
recv(, , , )
Notice that we now use the as the first parameter … this is not the server socket. The is a pointer to some memory that can take the incoming request. We can set it up as a char *. Finally, the is the number of bytes that the buffer can hold. It should not exceed the amount of memory reserved for the buffer itself. We will not discuss the here … but will set them to 0. The recv() function will return the number of incoming bytes that were received.
We can even send information back to the client using the send() function with this format: send(, , , )
The idea is the same. We simply set up the buffer that we want to send and send it.
Normally, with a server, we have a kind of recv/send sequence in a loop of some sort, so that communication between the client and server can go back and forth for a while. We will also likely want the server to serve many clients, so another loop is normally used to keep accepting new clients. Here is the pseudocode for setting up the server:
Open the socket Bind the socket Listen on the socket while (true) {
Accept a socket request
while (client has not “hung up” yet) {
Receive the buffer from the client
Process the request
Send a response to the client
}
Close client socket
}
Close server socket
Here is the code for the server in its entirety:
– 199 –

COMP2401 – Chapter 5 – Concurrent Computing
Winter 2019
Code from server.c
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 6000
int main() {
int
struct sockaddr_in
int
char
char*
serverSocket, clientSocket;
serverAddress, clientAddr;
status, addrSize, bytesRcv;
buffer[30];
response = “OK”;
// Create the server socket
serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (serverSocket < 0) { printf("*** SERVER ERROR: Could not open socket.\n"); exit(-1); } // Setup the server address memset(&serverAddress, 0, sizeof(serverAddress)); // zeros the struct serverAddress.sin_family = AF_INET; serverAddress.sin_addr.s_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons((unsigned short) MY_PORT); // Bind the server socket status = bind(serverSocket, (struct sockaddr *)&address, sizeof(address)); if (status < 0) { printf("*** SERVER ERROR: Could not bind socket.\n"); exit(-1); } // Set up the line-up to handle up to 5 clients in line status = listen(serverSocket, 5); if (status < 0) { printf("*** SERVER ERROR: Could not listen on socket.\n"); exit(-1); } // Wait for clients now while (1) { addrSize = sizeof(clientAddr); clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrSize); if (clientSocket < 0) { printf("*** SERVER ERROR: Could accept incoming client connection.\n"); exit(-1); } printf("SERVER: Received client connection.\n"); //... more on next page - 200 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 // Go into infinite loop to talk to client while (1) { // Get the message from the client bytesRcv = recv(clientSocket, buffer, sizeof(buffer), 0); buffer[bytesRcv] = 0; // put a 0 at the end so we can display the string printf("SERVER: Received client request: %s\n", buffer); // Respond with an "OK" message printf("SERVER: Sending \"%s\" to client\n", response); send(clientSocket, response, strlen(response), 0); if ((strcmp(buffer,"done") == 0) || (strcmp(buffer,"stop") == 0)) break; } printf("SERVER: Closing client connection.\n"); close(clientSocket); // Close this client's socket // If the client said to stop, then I'll stop myself if (strcmp(buffer,"stop") == 0) break; } // Don't forget to close the sockets! close(serverSocket); printf("SERVER: Shutting down.\n"); } Now, what about the client ? The client is structured very similarly. The socket is created the same way. Instead of using bind() though, we use connect() ... which has the same parameters. For the s_addr of the struct sockaddr_in, however, we will set it to inet_addr(“127.0.0.1”), which is the local machine. Here is the completed client code: Code from client.c #include
#include
#include
#include
#include
#include
#include
#define SERVER_IP “127.0.0.1”
#define SERVER_PORT 6000
int main() {
int clientSocket;
struct sockaddr_in clientAddress;
int status, bytesRcv;
char instr[80]; // stores user input from keyboard char buffer[80]; // stores user input from keyboard
// Create the client socket
clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSocket < 0) { printf("*** CLIENT ERROR: Could not open socket.\n"); exit(-1); } - 201 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 // Setup address memset(&clientAddress, 0, sizeof(clientAddress)); clientAddress.sin_family = AF_INET; clientAddress.sin_addr.s_addr = inet_addr(SERVER_IP); clientAddress.sin_port = htons((unsigned short) SERVER_PORT); // Connect to server status = connect(clientSocket, (struct sockaddr *) &clientAddress, sizeof(clientAddress)); if (status < 0) { printf("*** CLIENT ERROR: Could not connect.\n"); exit(-1); } // Go into loop to commuincate with server now while (1) { // Get a command from the user printf("CLIENT: Enter command to send to server ... "); scanf("%s", inStr); // Send command string to server strcpy(buffer, inStr); printf("CLIENT: Sending \"%s\" to server.\n", buffer); send(clientSocket, buffer, strlen(buffer), 0); // Get response from server, should be "OK" bytesRcv = recv(clientSocket, buffer, 80, 0); buffer[bytesRcv] = 0; // put a 0 at the end so we can display the string printf("CLIENT: Got back response \"%s\" from server.\n", buffer); if ((strcmp(inStr,"done") == 0) || (strcmp(inStr,"stop") == 0)) break; } close(clientSocket); // Don't forget to close the socket ! printf("CLIENT: Shutting down.\n"); } As a minor detail, scanf() will not allow blanks to be entered. If you want that to be allowed, use this instead of the scanf() line: fgets(inStr, sizeof(inStr), stdin); inStr[strlen(inStr)-1] = 0; Now once we have these compiled, we can run the server in the background: Once the server has been started and stopped a few times in our virtual environment, it is sometimes not possible to run it right away. You may have to wait a bit before running it. student@COMPBase:~$ ./server & [5] 4242 student@COMPBase:~$ - 202 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 Once it is running, we can run the client. Here is an example of some output that you may see. The client code is highlighted in one color, the server in another, and the user-entered command in a third color: student@COMPBase:~$ ./client SERVER: Received client connection. CLIENT: Enter command to send to server ... Hello CLIENT: Sending "Hello" to server. SERVER: Received client request: Hello SERVER: Sending "OK" to client CLIENT: Got back response "OK" from server. CLIENT: Enter command to send to server ... Fun CLIENT: Sending "Fun" to server. SERVER: Received client request: Fun SERVER: Sending "OK" to client CLIENT: Got back response "OK" from server. CLIENT: Enter command to send to server ... Bored CLIENT: Sending "Bored" to server. SERVER: Received client request: Bored SERVER: Sending "OK" to client CLIENT: Got back response "OK" from server. CLIENT: Enter command to send to server ... done CLIENT: Sending "done" to server. SERVER: Received client request: done SERVER: Sending "OK" to client SERVER: Closing client connection. CLIENT: Got back response "OK" from server. CLIENT: Shutting down. student@COMPBase:~$ At this point, the client has stopped and the server is still running. We can run the client again and it will work with the server. Here is an example where we tell the server to stop: student@COMPBase:~$ ./client SERVER: Received client connection. CLIENT: Enter command to send to server ... ItsMeAgain CLIENT: Sending "ItsMeAgain" to server. SERVER: Received client request: ItsMeAgain SERVER: Sending "OK" to client CLIENT: Got back response "OK" from server. CLIENT: Enter command to send to server ... stop CLIENT: Sending "stop" to server. SERVER: Received client request: stop SERVER: Sending "OK" to client SERVER: Closing client connection. SERVER: Shutting down. CLIENT: Got back response "OK" from server. CLIENT: Shutting down. [5]+ Done ./server student@COMPBase:~$ At this point, the server has also shut down. - 203 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 Client Server Model - UDP Let us now consider the UDP model for client/server communications. The UDP server’s socket is created in the same way as the TCP server, except that we use the IPPROTO_UDP in place of IPPROTO_TCP: serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); The server socket is then bound to its own IP address and port number in the same way by using the bind() function. There is no need to use the listen() function, since we are not setting up a one-to-one communication with anyone. We will simply be accepting whatever packets come in, regardless of who they are from. Similar to the TCP server, we must go into an infinite loop to accept incoming requests. When using a UDP server, incoming information from a client socket will make use of what is known as: A file descriptor is an integer ID (i.e., handle) used to access a file or other input/output resource, such as a pipe or network socket. In order to receive an incoming packet, we need to use the select() function, which will allow us to be notified when an incoming packet is available, or time out if it is taking too long. It allows us to accept packets from more than one socket (i.e., multiple clients). For this reason, we cannot simply just call a read command for a particular socket, otherwise our code would lock up waiting on only one socket channel. The select() function has this format: select(, , , , )
Here, is the number of file descriptors (i.e., potential clients) that we’d like to check for. The usual value is FD_SETSIZE … which is the maximum number possible.
The and are the sets of file descriptors that are ready for reading and writing, respectively. The are the file descriptors checked for exceptional conditions … we will set this to NULL in our examples. These are structures of type fd_set. For the and , we use the following macros to clear and set them for the socket:
int socket;
fd_set readfds;
FD_ZERO(&readfds); FD_SET(socket, &readfds);
– 204 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019
Regarding the , this is a struct timeval type. If set to NULL, the select() function will block and wait indefinitely until a client packet comes in. It is the easiest option to use. Otherwise, we can set the to {0,0} if we don’t want to wait at all. We will not discus the timeout any further in this course.
The select() function will return 0 if a timeout occurred, -1 if an error occurred or a positive value otherwise. To read in the client request packet, we use the recvfrom() function which has this format:
recvfrom(, , , , , )
The is the value returned from the socket() function. As with the TCP example, the and work the same way. We will not discuss the here … but will set them to 0. The is the address to a struct sockaddr as with the TCP example and the is the address of an int that holds the sizeof(). The recvfrom() returns the number of bytes received from the socket. We can do what we want with the buffer data at this point.
To send something back to the client, we use the sendto() function which has this format: sendto(, , , , , )
The idea is the same. We simply set up the buffer that we want to send and send it. Here is the pseudocode for setting up the server:
Open the socket Bind the socket while (true) {
Select a socket request
Receive the buffer from the client
Process the request
Send a response to the client
}
Close server socket
Here is the code for the server in its entirety:
Code from udpServer.c
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 6000
– 205 –

COMP2401 – Chapter 5 – Concurrent Computing
Winter 2019
int main() {
int
struct sockaddr_in
int
fd_set
char
char*
serverSocket;
serverAddr, clientAddr;
status, addrSize, bytesReceived;
readfds, writefds;
buffer[30];
response = “OK”;
// Create the server socket
serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (serverSocket < 0) { printf("*** SERVER ERROR: Could not open socket.\n"); exit(-1); } // Setup the server address memset(&serverAddr, 0, sizeof(serverAddr)); // zeros the struct serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons((unsigned short) SERVER_PORT); // Bind the server socket status = bind(serverSocket,(struct sockaddr *)&serverAddr, sizeof(serverAddr)); if (status < 0) { printf("*** SERVER ERROR: Could not bind socket.\n"); exit(-1); } // Wait for clients now while (1) { FD_ZERO(&readfds); FD_SET(serverSocket, &readfds); FD_ZERO(&writefds); FD_SET(serverSocket, &writefds); status = select(FD_SETSIZE, &readfds, &writefds, NULL, NULL); if (status == 0) { // Timeout occurred, no client ready } else if (status < 0) { printf("*** SERVER ERROR: Could not select socket.\n"); exit(-1); } else { addrSize = sizeof(clientAddr); bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (struct sockaddr *) &clientAddr, &addrSize); if (bytesReceived > 0) {
buffer[bytesReceived] = ‘\0’;
printf(“SERVER: Received client request: %s\n”, buffer);
}
// Respond with an “OK” message
printf(“SERVER: Sending \”%s\” to client\n”, response); sendto(serverSocket, response, strlen(response), 0,
(struct sockaddr *) &clientAddr, sizeof(clientAddr));
// If the client said to stop, then I’ll stop myself
if (strcmp(buffer, “stop”) == 0)
break;
} }
– 206 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 Now what about the client ? The socket is set up in the same way. The sendto() and
recvfrom() functions are also used, just as with the server. Here is the completed code:
Code from udpClient.c
#include
#include
#include
#include
#include
#include
#include
#define SERVER_IP “127.0.0.1”
#define SERVER_PORT 6000
int main() {
int clientSocket, addrSize, bytesReceived;
struct sockaddr_in clientAddr;
char inStr[80]; // stores user input from keyboard char buffer[80]; // stores sent and received data
// Create socket
clientSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (clientSocket < 0) { printf("*** CLIENT ERROR: Could open socket.\n"); exit(-1); } // Setup address memset(&clientAddr, 0, sizeof(clientAddr)); clientAddr.sin_family = AF_INET; clientAddr.sin_addr.s_addr = inet_addr(SERVER_IP); clientAddr.sin_port = htons((unsigned short) SERVER_PORT); // Go into loop to commuincate with server now while (1) { // Get a command from the user printf("CLIENT: Enter command to send to server ... "); scanf("%s", inStr); //fgets(inStr, sizeof(inStr), stdin); //inStr[strlen(inStr)-1] = 0; // Send command string to server strcpy(buffer, inStr); printf("CLIENT: Sending \"%s\" to server.\n", buffer); sendto(clientSocket, buffer, strlen(buffer), 0, (struct sockaddr *) &clientAddr, sizeof(clientAddr)); // Get response from server, should be "OK" addrSize = sizeof(clientAddr); bytesReceived = recvfrom(clientSocket, buffer, 80, 0, (struct sockaddr *) &clientAddr, &addrSize); buffer[bytesReceived] = 0; // put a 0 at the end so we can display the string printf("CLIENT: Got back response \"%s\" from server.\n", buffer); if ((strcmp(inStr,"done") == 0) || (strcmp(inStr,"stop") == 0)) break; } - 207 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 close(clientSocket); // Don't forget to close the socket ! printf("CLIENT: Shutting down.\n"); } Assuming that the udpServer has been started, the output is as follows: student@COMPBase:~$ ./udpClient CLIENT: Enter command to send to server ... Hello CLIENT: Sending "Hello" to server. SERVER: Received client request: Hello SERVER: Sending "OK" to client CLIENT: Got back response "OK" from server. CLIENT: Enter command to send to server ... Fun CLIENT: Sending "Fun" to server. SERVER: Received client request: Fun SERVER: Sending "OK" to client CLIENT: Got back response "OK" from server. CLIENT: Enter command to send to server ... stop CLIENT: Sending "stop" to server. SERVER: Received client request: stop SERVER: Sending "OK" to client SERVER: Shutting down. CLIENT: Got back response "OK" from server. CLIENT: Shutting down. [3]+ Done ./udpServer student@COMPBase:~$ 5.4 Threads We have discussed, in detail, the mechanisms behind allowing two processes to communicate on the same host or over a network. In a multi-processor system, it is possible to have two or more threads running simultaneously. This is always the case when the processes are running on different host machines. There are many issues that we have not discussed which pertain to distributed computing, as this is really just an introduction to systems programming. Likely, you can perceive by now that the code for handling timing and resource sharing can get tricky and much more complicated as more and more processes are added to the software framework. A simpler way to manage separate tasks is to use threads: A thread is a sequence of programmed instructions that can be managed independently by the operating system - 208 - COMP2401 - Chapter 5 – Concurrent Computing Winter 2019 Multiple threads can be running within a single process. However, only one thread can be executed at a time by the CPU. The threads all share the CPU processing time, often in a round-robin fashion (i.e., everyone gets their turn). Since the threads each run separately on the CPU, this greatly simplifies the likelihood of race conditions and deadlocks occurring, although we may still design poor code that causes these situations to occur. As we have seen with the fork() command, which spawned new processes, threads can be created by the main thread or from other created threads. The threads all run in parallel and are scheduled automatically by the operating system kernel. Switching between threads is faster than switching between processes. Each thread runs as a separate program. They have a unique thread context (i.e., resources) that includes: • Thread ID – a unique ID. • Function call stack – keeps track of function call ordering, parameters, and variables. • Program counter – keeps track of program instruction that is currently executing. One very nice feature of threads is that all threads belonging to the same process share: • Address space • Data segment (i.e., global variables and allocated heap memory) • Code segment (i.e., program instructions) That means, the value of a global variable at any point in time is the same across all threads and that any thread can access and modify it. To create a thread, we us the pthread_create() function which is defined in the header file. It takes these 4 parameters:
1. The address of a pthread_t type, which is an integer representing the handle (i.e., ID) of the newly-created thread.
2. Some attributes that can de used by the thread (we will use NULL to indicate defaults).
3. A pointer to a function that will be called to start the thread.
4. A single parameter that can be passed to the start function above.
– 209 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019
To stop/terminate a thread, pthread_exit(void *status) can be called, where status will end up being the return value of the thread. Alternatively, one thread can wait for the termination of another thread by using the pthread_join(pthread_t thread, void **status) function which specifies which thread to wait for and also allows a value to be returned in the status pointer, although we will use NULL in our examples.
Consider this simple example that creates three threads and allows them all to run for 4, 8 and 2 seconds, respectively. The main program keeps running and waits for thread 1 to complete, then for thread 2 to complete and then for thread 3 to complete (which had already completed).
Code from thread.c
#include
#include
#include void *printMsg(void*);
int times[] = {4, 8, 2}; // # of seconds for each thread to run
int main() {
pthread_t t1, t2, t3;
pthread_create(&t1, NULL, printMsg, “1”); pthread_create(&t2, NULL, printMsg, “2”); pthread_create(&t3, NULL, printMsg, “3”);
printf(“\nThreads all created. Waiting for Thread 1 now …\n”); pthread_join(t1, NULL);
printf(“\nThread 1 is back. Waiting for Thread 2 now …\n”); pthread_join(t2, NULL);
printf(“Thread 2 is back. Waiting for Thread 3 now …\n”); pthread_join(t3, NULL);
printf(“Thread 3 is back. Time to quit.\n”);
}
// Function called at the start of each thread
void *printMsg(void *str) {
char threadNum = ((char *)str)[0] – 48; for (int i=0; i
#include void* threadFunc(void*);
int count = 0;
int main() {
int numInc = 100000000; // count to 100 million
pthread_t t1, t2;
pthread_create(&t1, NULL, threadFunc, &numInc); pthread_create(&t2, NULL, threadFunc, &numInc); pthread_join(t1, NULL);
pthread_join(t2, NULL);
if (count != (2 * numInc))
printf(“Error: Count is %d instead of 200,000,000.\n”, count);
else
printf(“Count is %d, which is correct.\n”, count); return(0);
}
// Function to increase count variable by amount specified by arg
void* threadFunc(void *arg) {
int inc = *((int *)arg);
for (int i=0; i header in our code in order to use it. The first function that we need to call is sem_init() which allows use to initialize the semaphore:
sem_t semaphore;
sem_init(&semaphore, 0, 1);
In the above code, the semaphore is initially given a value of 1 as the third parameter to the function. The second parameter has a value of 0, indicating that the semaphore will just be used between threads, as opposed to between multiple processes. If the function returns a negative value, then something went wrong (e.g., the value exceeds SEM_VALUE_MAX, the limit on the number of semaphores has been reached, process does not have privileges, etc.).
When a thread is ready to use the shared resource (e.g., the count++ line of code), then it must wrap up the code with code beforehand to wait on the semaphore and code afterwards to release the semaphore.
The sem_wait(&semaphore) function is used to wait on the semaphore. That is, when we call it, our code waits there until it is this thread’s turn to use the
shared resource. The function returns -1 if the wait fails (e.g., semaphore already locked, deadlock has been detected, a signal interrupted, or the parameter is invalid) … otherwise 0 is returned. This function decrements the value of the semaphore. If the value of the semaphore is zero, it waits until it is non-zero.
The sem_post(&semaphore) function is used to release the lock on a semaphore so that others can use the resource. It fails only if the parameter is
invalid, in which case -1 is returned … otherwise 0 is returned. This function increments the semaphore’s value.
– 215 –

COMP2401 – Chapter 5 – Concurrent Computing Winter 2019 Here is the updated code that will work properly to increase the count via the two threads:
Code from semaphore.c
#include
#include
#include #include
void* threadFunc(void*);
volatile int count = 0;
sem_t mutex;
int main() {
int numInc = 100000000; // count to 100 million
pthread_t t1, t2;
if (sem_init(&mutex, 0, 1) < 0) { printf("Error: on semaphore init.\n"); exit(1); } pthread_create(&t1, NULL, threadFunc, &numInc); pthread_create(&t2, NULL, threadFunc, &numInc); pthread_join(t1, NULL); pthread_join(t2, NULL); if (count != (2 * numInc)) printf("Error: Count is %d instead of 200,000,000.\n", count); else printf("Count is %d, which is correct.\n", count); } // Function to increase count variable by amount specified by arg void* threadFunc(void *arg) { int inc = *((int *)arg); for (int i=0; i