COMP30023 Week 03
School of Computing and Information Systems
Copyright By PowCoder代写 加微信 powcoder
COMP30023: Computer Systems
Practical Week 3
1 Introduction
In this practical, we will be exploring threads, processes and interprocess communication.
NOTE: You can do this lab without following the order.
2 Creating a Thread
The main() function of a C program runs on its own thread (commonly called the ‘main’ thread)
We can create additional, independent threads by using the pthread create function provided by pthread.h .
thread1.c on the LMS creates such a thread. The thread runs the function say hello() upon creation.
1. Compile and run thread1.c . Note the use of the -lpthread option to explicitly link the pthread library
Command: $ gcc thread1.c -o thread1 -lpthread && ./thread1
2. Notice how the second thread said hello before the first thread? This is because the pthread join
function will wait for the thread specified in the function call to finish executing before proceeding with
the current thread.
In this scenario the main thread ‘waited’ for the other thread to ‘join’ it before proceeding.
3. Can you guess what might happen if we did not call the pthread join function? Comment that line in
the code, compile and rerun to observe the output.
Discuss with your classmates (or demonstrator) if you are unable to understand the behaviour you observe.
4. Do you think these threads are user or kernel threads?
3 Threads and Race Conditions
Race conditions, where the final result of a computation depends on the order in which threads happened to
run, may occur when several threads access a shared resource.
1. The code thread2.c given on the LMS has two threads accessing the common global variable count .
This code has a race condition.
Run the code several times and observe that the output changes each time.
2. We can solve race conditions such as these by defining a section of code that can only be executed by one
thread at a time (called a ‘critical section’ or ‘critical region‘).
We can use a mutex to define a critical section. The methods pthread mutex lock(&lock) and
pthread mutex unlock(&lock) can be used to define a critical section, where lock is a global variable
that is of type pthread mutex t .
3. The definition, initialisation, and destroying of the mutex have been written for you in thread2.c . De-
termine the critical section that would prevent the race condition and use the function calls to lock and
unlock the mutex to fix the race condition.
pthread mutex lock(&lock)
/* Code in Critical Section */
pthread mutex unlock(&lock)
4. Challenge task: try to introduce a deadlock into your program.
4 OS Processes
fork() creates a new process by duplicating the calling process. The new process is referred to as the child
process. The calling process is referred to as the parent process.
The child process and the parent process run in separate memory spaces. At the time of fork() both memory
spaces have the same content.
First, compile the demo fork program (see Appendix):
$ gcc -o fork fork.c
Run the program in the background.
$ ./fork &
While the program runs in background, run top in tree mode to watch how child processes are spawned from
the parent process.
While top is running, push Shift + V to enable forrest view1. Find the fork program and watch how child
processes get spawned.
Taken from the manpages verbatim:
The exec family of functions shall replace the current process image with a new process image. The new image
shall be constructed from a regular, executable file called the new process image file. There shall be no return
from a successful exec, because the calling process image is overlaid by the new process image.
First, compile the demo exec program:
$ gcc -o exec exec.c
Run the program.
What program does it actually exec into? Discuss with your classmate and demonstrator.
The pipe() function shall create a pipe and place two file descriptors, one each into the arguments fildes[0] and
fildes[1], that refer to the open file descriptions for the read and write ends of the pipe. Their integer values
shall be the two lowest available at the time of the pipe() call.
First, compile the demo pipe program:
$ gcc -o pipe pipe.c
Run the program.
You now have a brief idea how exec, fork and pipe works. You are encouraged to author a simple C program
that can utilise all 3 libc functions. An idea would be to author a program that forks a new process and waits
for input from the parent process (via stdin) to print from the child process. For example, try simulating
execution of $ ls *.c | wc -l.
1This view shows parent-child relationships between processes
A thread1.c
/*************************************
Demo for pthread commands
compile: gcc threadX.c -o threadX -lpthread
***************************************/
#include #include void* say_hello(void* param); /* the work_function */ int main(int args, char** argv) { pthread_t tid; /* thread identifier */ /* create the thread */ pthread_create(&tid, NULL, say_hello, NULL); /* wait for thread to exit */ pthread_join(tid, NULL); printf(“Hello from first thread\n”); void* say_hello(void* param) { printf(“Hello from second thread\n”); return NULL; B thread2.c #include #include #include #include #define ITERATIONS 1000000 void* runner(void* param); /* thread doing the work */ int count = 0; pthread_mutex_t lock; int main(int argc, char** argv) { pthread_t tid1, tid2; if (pthread_mutex_init(&lock, NULL) != 0) { printf(“mutex init failed\n”); if (pthread_create(&tid1, NULL, runner, NULL)) { printf(“Error creating thread 1\n”); if (pthread_create(&tid2, NULL, runner, NULL)) { printf(“Error creating thread 2\n”); /* wait for the threads to finish */ if (pthread_join(tid1, NULL)) { printf(“Error joining thread\n”); if (pthread_join(tid2, NULL)) { printf(“Error joining thread\n”); if (count != 2 * ITERATIONS) printf(“** ERROR ** count is [%d], should be %d\n”, count, 2 * ITERATIONS); printf(“OK! count is [%d]\n”, count); pthread_exit(NULL); pthread_mutex_destroy(&lock); /* thread doing the work */ void* runner(void* param) { int i, temp; for (i = 0; i < ITERATIONS; i++) {
temp = count; /* copy the global count locally */
temp = temp + 1; /* increment the local copy */
count = temp; /* store the local value into the global count */
return NULL;
#include #include #include int main(int argc, char** argv) { pid_t root = getpid(); // when forking, program does not start again since memory/register values are exactly the same // i.e. instruction pointer is at the same line too. So fork() wont execute again. pid_t pid = fork(); printf(“from %d forking into %d\n”, root, pid); sleep(20); // watch 2 different PIDs spawn 2 more child processes. pid_t mypid = getpid(); pid = fork(); printf(“from %d forking into %d\n”, mypid, pid); sleep(20); if (getpid() == root) { sleep(20); printf(“root exiting\n”); printf(“Child — PID %d exiting\n”, getpid()); #include int main(int argc, char **argv) { return execv(“/usr/bin/ls”, argv); /***************************************************************************** Excerpt from “Linux Programmer’s Guide – Chapter 6” (C)opyright 1994-1995, ***************************************************************************** MODULE: pipe.c *****************************************************************************/ #include #include #include #include #include int main(void) { int fd[2], nbytes; pid_t childpid; char string[] = “Hello, world!\n”; char readbuffer[80]; if ((childpid = fork()) == -1) { perror(“fork”); if (childpid == 0) { /* Child process closes up input side of pipe */ close(fd[0]); /* Send “string” through the output side of pipe */ write(fd[1], string, (strlen(string) + 1)); /* Parent process closes up output side of pipe */ close(fd[1]); /* Read in a string from the pipe */ nbytes = read(fd[0], readbuffer, sizeof(readbuffer)); printf(“Received string: %s”, readbuffer); return (0); Sample solutions A couple of (observed) possibilities: $ ./thread1 Hello from first thread $ ./thread1 Hello from first thread Hello from second thread $ ./thread1 Hello from first thread Hello from second thread Hello from second thread In the first case, the main thread returns and terminates the process before the completion of printf. You will probably not encounter the second and third cases unless you’re on a multi-core system (note that the What if we want the main thread to exit but allow the second thread to run to completion? See: $ man 3 pthread exit pthreads is an API, defined in the POSIX standard. Why can’t I put the locks around pthread create, pthread join? What is the race condition? 1. Thread A copies count to temp. It is interrupted. 2. Thread B gets a chance to run and it copies count to temp, increments temp and saves it back. 3. Thread A resumes at some point, it increments temp (which is outdated) and saves it to count. 4. count is now lower than it should be. Solution: Put lock/unlock statements immediately outside the loop. However, consider scenario: for each loop iteration perform long task which is independent critical section to update some global variable Then it may be worthwhile to surround the critical section with the lock instead. 2https://stackoverflow.com/questions/8639150 https://stackoverflow.com/questions/13550662 Taking inspiration from lecture slides: void* runner(void* param) { for (int i = 0; i < 10000; i++) {
pthread_mutex_lock(&lock1);
printf("thread 1 lock 1\n");
pthread_mutex_lock(&lock2);
printf("thread 1 lock 2\n");
// do work
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
void* runner2(void* param) {
for (int i = 0; i < 10000; i++) {
pthread_mutex_lock(&lock2);
printf("thread 2 lock 2\n");
pthread_mutex_lock(&lock1);
printf("thread 2 lock 1\n");
// do work
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
Example execution of fork.c:
./fork (pid 1150653)
pid_t root = getpid(); root = 1150653
pid_t pid = fork();
printf("from %d forking into %d\n", root, pid);
./fork (pid 1150653)
|_ ./fork (pid 1150654)
from 1150653 forking into 1150654 parent process gets child process’s pid for fork
from 1150653 forking into 0 child process gets 0 as return value for fork
pid_t mypid = getpid();
pid = fork();
printf("from %d forking into %d\n", mypid, pid);
./fork (pid 1150653)
|_ ./fork (pid 1150654)
|_ ./fork (pid 1150684)
|_ ./fork (pid 1150683)
from 1150653 forking into 1150683
fork 1150654 forking into 1150684
from 1150653 forking into 0
from 1150654 forking into 0
if (getpid() != root)
printf("Child -- PID %d exiting\n", getpid());
Child -- PID 1150654 exiting
Child -- PID 1150683 exiting
Child -- PID 1150684 exiting
./fork (pid 1150653)
|_ ./fork (pid 1150654 - Zombie)
|_ ./fork (pid 1150683 - Zombie)
printf("root exiting\n");
Why do I still see the child processes after they have exited?
They are zombie processes: see https://stackoverflow.com/questions/4825379.
https://stackoverflow.com/questions/4825379
/*****************************************************************************
ls *.c | wc -l
Adapted from pipe.c
Excerpt from "Linux Programmer’s Guide - Chapter 6"
(C)opyright 1994-1995,
*****************************************************************************/
#include #include #include #include #include int main(int argc, char* argv[]) { int fd[2]; pid_t childpid; if((childpid = fork()) == -1) { perror(“fork”); if(childpid == 0) { /* Child process closes up input side of pipe */ close(fd[0]); /* redirect stdout, https://stackoverflow.com/questions/1720535 */ dup2(fd[1], STDOUT_FILENO); /* glob for *.c, https://stackoverflow.com/questions/53686987 */ glob_t globbuf; globbuf.gl_offs = 1; glob(“*.c”, GLOB_DOOFFS, NULL, &globbuf); globbuf.gl_pathv[0] = “ls”; execv(“/usr/bin/ls”, globbuf.gl_pathv); /* Parent process closes up output side of pipe */ close(fd[1]); /* redirect stdin */ dup2(fd[0], STDIN_FILENO); char* args[] = { “wc”, “-l”, NULL }; execv(“/usr/bin/wc”, args); Introduction 程序代写 CS代考 加微信: powcoder QQ: 1823890830 Email: powcoder@163.com
In the second case, the printf happens before the process is terminated.
In the third case, why does the second thread print twice? https://stackoverflow.com/questions/13550662
allocated VMs are single-core).
We can replace pthread join with pthread exit.
pthread exit terminates the calling thread (the main thread).
When the last thread in the process terminates, exit(3) is called with an exit status code of zero (terminating
the process).
Whether user-level threads or kernel-level threads are used is implementation specific.
The Linux implementation of pthreads use kernel-level threads23.
It is also possible to create threads using the clone system call in Linux (see TB 10.3.3).
The main thread will obtain lock, call pthread function, then unlock.
This does not solve the problem because the code which contains the race condition is not executed by the main
Access/modification of global variable count.
For example:
Why not put the locks inside? Because of performance overheads in locking/unlocking.
3https://stackoverflow.com/questions/10392800
https://stackoverflow.com/questions/8639150
https://stackoverflow.com/questions/10392800
Creating a Thread
Threads and Race Conditions
OS Processes