OS - Lab2. Process & Multithreaded Process
OS - Lab2. Process & Multithreaded Process
Lab 2
Process & Multithreaded Process
Course: Operating Systems
Goal: This lab helps student to review the data segment of a process and distinguish
the differences between thread and process.
Content In detail, this lab requires student identify the memory regions of process’s
data segment, practice with examples such as creating a multithread program, showing
the memory region of threads:
• view process memory regions: Data segment, BSS segment, Stack and Heap.
• Show the differences between process and thread in term of memory region.
Result After doing this lab, students can understand the mechanism of distributing
memory region to allocate the data segment for specific processes. In addition, they
understand how to write a multithreaded program.
Requirement Student need to review the theory of process memory and thread.
1
Contents
1. Introduction 2
1.1. Process ’s memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3. Interprocess Communication . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4. Introduction to thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2. Practice 7
2.1. Looking inside a process . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2. How to transfer data between processes? . . . . . . . . . . . . . . . . . . . 8
2.2.1. Shared Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.2. Pipe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3. How to create multiple threads? . . . . . . . . . . . . . . . . . . . . . . . . 12
2.3.1. Thread libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.3.2. Multithread programming . . . . . . . . . . . . . . . . . . . . . . . 15
3. Exercise (Required) 17
3.1. Problem 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2. Problem 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.3. Problem 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1. Introduction
1.1. Process ’s memory
Traditionally, a Unix process is divided into segments. The standard segments are code
segment, data segment, BSS (block started by symbol), and stack segment.
The code segment contains the binary code of the program which is running as the pro-
cess (a “process” is a program in execution). The data segment contains the initialized
global variables and data structures. The BSS segment contains the uninitialized global
data structures and finally, the stack segment contains the local variables, return ad-
dresses, etc. for the particular process.
Under Linux, a process can execute in two modes - user mode and kernel mode. A
process usually executes in user mode, but can switch to kernel mode by making sys-
tem calls. When a process makes a system call, the kernel takes control and does the
requested service on behalf of the process. The process is said to be running in kernel
mode during this time. When a process is running in user mode, it is said to be “in
userland” and when it is running in kernel mode it is said to be “in kernel space”. We
will first have a look at how the process segments are dealt with in userland and then
take a look at the book keeping on process segments done in kernel space.
In Figure 1.1, blue regions represent virtual addresses that are mapped to physical
2
memory, whereas white regions are unmapped. The distinct bands in the address space
correspond to memory segments like the heap, stack, and so on.
3
• The automatic variables (or local variables) will be allocated on the stack, so
printing out the addresses of local variables will provide us with the addresses
within the stack segment.
1.2. Stack
Stack is one of the most important memory region of a process. It is used to store tem-
porary data used by the process (or thread). The name “stack” is used to described the
way data put and retrieved from this region which is identical to stack data structure:
The last item pushed to the stack is the first one to be removed (popped).
Stack organization makes it suitable from handling function calls. Each time a function
is called, it gets a new stack frame. This is an area of memory which usually contains, at
a minimum, the address to return when it complete, the input arguments to the function
and space for local variables.
In Linux, stack starts at a high address in memory and grows down to increase its size.
Each time a new function is called, the process will create a new stack frame for this
function. This frame will be place right after that of its caller. When the function re-
turns, this frame is clean from memory by shrinking the stack (stack pointer goes up).
The following program illustrates to identify the relative location of stack frames create
by nested function calls.
Similar to heap, stack has a pointer name stack pointer (as heap has program break)
which indicate the top of the stack. To change stack size, we must modify the value of
this pointer. Usually, the value of stack pointer is hold by stack pointer register inside
the processor. Stack space is limited, we cannot extend the stack exceed a given size.
4
If we do so, stack overflow will occurs and crash our program. To identify the default
stack size, use the following command
1 ulimit −s
Different from heap, data of stack are automatically allocated and cleaned through
procedure invocation termination, Therefore, in C programming, we do not need to
allocate and free local variables. In Linux, a process is permitted to have multiple stack
regions. Each regions belongs to a thread.
5
Figure 1.2: Shared memory vs message passing model.
its code section, data section, and other operating-system resources, such as open files
and signals. A traditional (or heavyweight) process has a single thread of control. If a
process has multiple threads of control, it can perform more than one task at a time.
Figure 1.3 illustrates the difference between a traditional single-threaded process and a
multithreaded process. The benefits of multithreaded programming can be broken down
into four major categories:
• Responsiveness
• Resource sharing
• Economy
6
• Scalability
Question: What resources are used when a thread is created? How do they differ from
those used when a process is created?
On a system with multiple cores, however, concurrency means that the threads can run
in parallel, because the system can assign a separate thread to each core, as Figure 1.4
shown.
Question: Is it possible to have concurrency but not parallelism? Explain.
2. Practice
2.1. Looking inside a process
Looking at the following C program with basic statements:
1 #include <s t d i o . h>
2 #include <s t d l i b . h>
3 #include <sys /types . h>
4 #include <unistd . h>
5
6 int glo_init_data = 99;
7 int glo_noninit_data ;
8
9 void print_func ( ) {
10 int local_data = 9 ;
7
11 p r i n t f ( "Process␣ID␣=␣%d\n" , getpid ( ) ) ;
12 p r i n t f ( "Addresses␣ of ␣the␣ process : \ n" ) ;
13 p r i n t f ( " 1 . ␣glo_init_data␣=␣%p\n" , &glo_init_data ) ;
14 p r i n t f ( " 2 . ␣glo_noninit_data␣=␣%p\n" , &glo_noninit_data ) ;
15 p r i n t f ( " 3 . ␣print_func ( ) ␣=␣%p\n" , &print_func ) ;
16 p r i n t f ( " 4 . ␣local_data␣=␣%p\n" , &local_data ) ;
17 }
18
19 int main( int argc , char ∗∗argv ) {
20 print_func ( ) ;
21 return 0 ;
22 }
Let’s run this program many times and give the discussion about the segments of a
process. Where is data segment/BSS segment/stack/code segment?
• Its first parameter is an integer key that specifies which segment to create and
unrelated processes can access the same shared segment by specifying the same
key value. Moreover, other processes may have also chosen the same fixed key,
which could lead to conflict. So that you should be careful when generating keys
for shared memory regions. A solution is that you can use the special constant
IPC_PRIVATE as the key value guarantees that a brand new memory segment is
created.
• Its second parameter specifies the number of bytes in the segment. Because seg-
ments are allocated using pages, the number of actually allocated bytes is rounded
up to an integral multiple of the page size.
• The third parameter is the bitwise or of flag values that specify options to shmget.
The flag values include these:
– IPC_CREAT: This flag indicates that a new segment should be created. This
permits creating a new segment while specifying a key value.
– IPC_EXCL: This flag, which is always used with IPC_CREAT, causes shmget
to fail if a segment key is specified that already exists. If this flag is not given
and the key of an existing segment is used, shmget returns the existing seg-
ment instead of creating a new one.
8
– Mode flags: This value is made of 9 bits indicating permissions granted to
owner, group, and world to control access to the segment.
To make the shared memory segment available, a process must attach it by calling
shmat().
void ∗shmat( int shmid , const void ∗shmaddr , int shmflg ) ;
• a pointer that specifies where in your process’s address space you want to map the
shared memory; if you specify NULL, Linux will choose an available address.
• The third argument is a flag. You can read more details about this argument
in Linux manual page. https://man7.org/linux/man-pages/man3/shmat.3p.
html.
When you’re finished with a shared memory segment, the segment should be detached
using shmdt. Pass it the address returned by shmat. If the segment has been deallocated
and this was the last process using it, it is removed. Examples: Run the two following
processes in two terminals. At the writer process, you can type an input string and
observe returns from the reader process.
• writer.c
#include <sys /types . h>
#include <sys / ipc . h>
#include <sys /shm . h>
#include <s t d i o . h>
#include <unistd . h>
#include <s t d l i b . h>
#define SHM_KEY 0x123
int main( int argc , char ∗argv [ ] ) {
int shmid ;
char ∗shm ;
9
i f (shm == (char ∗)−1) {
perror ( "shmat" ) ;
e x i t (1) ;
}
s p r i n t f (shm, " h e l l o ␣world\n" ) ;
p r i n t f ( "shared␣memory␣content : ␣%s \n" , shm) ;
s l e e p (10) ;
// detach from the shared memory
i f (shmdt(shm) == −1) {
perror ( "shmdt" ) ;
return 1 ;
}
// Mark the shared segment to be destroyed .
i f ( shmctl (shmid , IPC_RMID, 0) == −1) {
perror ( "shmctl" ) ;
return 1 ;
}
return 0 ;
}
• reader.c
#include <sys /types . h>
#include <sys / ipc . h>
#include <sys /shm . h>
#include <s t d i o . h>
#include <s t d l i b . h>
∗/
shmid = shmget (SHM_KEY, 1000 , 0644|IPC_CREAT) ;
i f ( shmid < 0) {
perror ( "shmget" ) ;
return 1 ;
}
else {
p r i n t f ( "shared␣memory : ␣%d\n" , shmid ) ;
}
10
shm = (char ∗)shmat(shmid , 0 ,0) ;
i f (shm == (char ∗)−1) {
perror ( "shmat" ) ;
e x i t (1) ;
}
p r i n t f ( "shared␣memory : ␣%p\n" , shm) ;
i f (shm != 0) {
p r i n t f ( "shared␣memory␣content : ␣%s \n" , shm) ;
}
s l e e p (10) ;
i f (shmdt(shm) == −1) {
perror ( "shmdt" ) ;
return 1 ;
}
return 0 ;
}
2.2.2. Pipe
Pipe actually is very common method to transfer data between processes. For example,
the "pipe" operator ’|’ can be used to transfer the output from a command to another
command as in the following example:
# the output from " h i s t o r y " w i l l be input to the grep command.
history | grep "a"
In terms of C programming, the standard library named "unistd.h" defined the following
function to create a pipe. This function creates a pipe, a unidirectional data channel
that can be used for interprocess communication. The array pipefd is used to return two
file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the
pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the
pipe is buffered by the kernel until it is read from the read end of the pipe.
int pipe ( int pipefd [ 2 ] ) ;
11
char readmessage [ 2 0 ] ;
returnstatus = pipe ( pipefds ) ;
i f ( returnstatus == −1) {
p r i n t f ( "Unable␣to␣ create ␣pipe\n" ) ;
return 1 ;
}
pid = fork ( ) ;
// Child process
i f ( pid == 0) {
read ( pipefds [ 0 ] , readmessage , sizeof ( readmessage ) ) ;
p r i n t f ( "Child␣Process : ␣Reading , ␣message␣ i s ␣%s \n" , readmessage ) ;
return 0 ;
}
//Parent process
p r i n t f ( "Parent␣Process : ␣Writing , ␣message␣ i s ␣%s \n" , writemessages ) ;
write ( pipefds [ 1 ] , writemessages , sizeof ( writemessages ) ) ;
return 0 ;
}
In the above program, firstly the parent process will create a pipline and call fork() to
create a child process. Then, the parent process will write a message to the pipeline.
At the same time, the child process will read data from the pipeline. Noticeably, both
write() and read() need to know the size of the message.
Creating threads
pthread_create ( thread , attr , start_routine , arg )
Initially, your main() program comprises a single, default thread. All other threads
must be explicitly created by the programmer.
• thread: An opaque, unique identifier for the new thread returned by the subrou-
tine.
• attr: An opaque attribute object that may be used to set thread attributes. You
can specify a thread attributes object, or NULL for the default values.
12
• start: the C routine that the thread will execute once it is created.
Passing argument to Thread We can pass a structure to each thread such as the
example below. Using the previous example to implement this example:
1 struct thread_data{
2 int thread_id ;
3 int sum ;
4 char ∗message ;
5 };
13
6
7 struct thread_data thread_data_array [NUM_THREADS] ;
8
9 void ∗ PrintHello (void ∗thread_arg )
10 {
11 struct thread_data ∗my_data ;
12 ...
13 my_data = ( struct thread_data ∗) thread_arg ;
14 taskid = my_data−>thread_id ;
15 sum = my_data−>sum ;
16 hello_msg = my_data−>message ;
17 ...
18 }
19
20 int main ( int argc , char ∗argv [ ] )
21 {
22 ...
23 thread_data_array [ t ] . thread_id = t ;
24 thread_data_array [ t ] . sum = sum ;
25 thread_data_array [ t ] . message = messages [ t ] ;
26 rc = pthread_create(&threads [ t ] , NULL, PrintHello ,
27 (void ∗) &thread_data_array [ t ] ) ;
28 ...
29 }
• The pthread_join() subroutine blocks the calling thread until the specified threa-
did thread terminates.
• The programmer is able to obtain the target thread’s termination return status if
it was specified in the target thread’s call to pthread_exit().
14
• A joining thread can match one pthread_join() call. It is a logical error to
attempt multiple joins on the same thread.
15
38 pthread e x i t (0) ;
39 }
16
3. Exercise (Required)
3.1. Problem 1
Firstly, downloading two text files from the url: https://drive.google.com/file/
d/1fgJqOeWbJC4ghMKHkuxfIP6dh2F911-E/view?usp=sharing These file contains the
100000 ratings of 943 users for 1682 movies in the following format:
userID <tab> movieID <tab> r a t i n g <tab> timeStamp
userID <tab> movieID <tab> r a t i n g <tab> timeStamp
...
Secondly, you should write a program that spawns two child processes, and each of them
will read a file and compute the average ratings of movies in the file. You implement
the program by using shared memory method.
3.2. Problem 2
Given the following function:
sum(n) = 1 + 2 + ... + n
This is the sum of a large set including n numbers from 1 to n. If n is a large num-
ber, this will take a long time to calculate the sum(n). The solution is to divide
this large set into pieces and calculate the sum of these pieces concurrently by using
threads. Suppose the number of threads is numT hreads, so the 1st thread calculates
the sum of {1,n/numThreads}, the 2nd thread carries out the sum of {n/numThreads
+1, 2n/numThreads},...
Write two programs implementing algorithm describe above: one serial ver-
sion and one multi-thread version.
The program takes the number of threads and n from user then creates multiple threads
to calculate the sum. Put all of your code in two files named “sum_serial.c” and
“sum_multi-thread.c”. The number of threads and n are passed to your program as
an input parameter. For example, you will use the following command to run your
program for calculating the sum of 1M :
. / sum_serial 1000000
. / sum_multi−thread 10 1000000
(#numThreads=10)
Requirement: The multi-thread version may improve speed-up compared to the serial
version. There are at least 2 targets in the Makefile sum_serial and sum_multi-thread
to compile the two program.
3.3. Problem 3
Conventionally, pipe is a one-way communication method.(In the example at section 3,
you can test by add a read() call after the writer() call at the parent process, a write()
17
call after the read() call at the child process and observe what happens?). However, we
still can have some tricks to adapt it for two-way communication by using two pipes. In
this exercise, you should implement the TODO segment in the below program.
1 #include <s t d i o . h>
2 #include <s t d l i b . h>
3 #include <unistd . h>
4 static int pipefd1 [ 2 ] , pipefd2 [ 2 ] ;
5
6 void INIT(void) {
7 i f ( pipe ( pipefd1 )<0 | | pipe ( pipefd2 )<0){
8 perror ( "pipe" ) ;
9 e x i t (EXIT_FAILURE) ;
10 }
11 }
12 void WRITE_TO_PARENT(void) {
13 /∗ send parent a message through pipe ∗/
14 // TO DO
15 p r i n t f ( "Child␣send␣message␣to␣parent ! \ n" ) ;
16 }
17 void READ_FROM_PARENT(void) {
18 /∗ read message sent by parent from pipe ∗/
19 // TO DO
20 p r i n t f ( "Child␣ r e c e i v e ␣message␣from␣parent ! \ n" ) ;
21 }
22 void WRITE_TO_CHILD(void) {
23 /∗ send c h i l d a message through pipe ∗/
24 // TO DO
25 p r i n t f ( "Parent␣send␣message␣to␣ c h i l d ! \ n" ) ;
26 }
27 void READ_FROM_CHILD(void) {
28 /∗ read the message sent by c h i l d from pipe ∗/
29 // TO DO
30 p r i n t f ( "Parent␣ r e c e i v e ␣message␣from␣ c h i l d ! \ n" ) ;
31 }
32 int main( int argc , char∗ argv [ ] ) {
33 INIT ( ) ;
34 pid_t pid ;
35 pid = fork ( ) ;
36 // s e t a timer , process w i l l end a f t e r 1 second .
37 alarm (10) ;
38 i f ( pid==0){
39 while (1) {
40 s l e e p ( rand ( )%2+1);
18
41 WRITE_TO_CHILD( ) ;
42 READ_FROM_CHILD( ) ;
43 }
44 }else{
45 while (1) {
46 s l e e p ( rand ( )%2+1);
47 READ_FROM_PARENT( ) ;
48 WRITE_TO_PARENT( ) ;
49 }
50 }
51 return 0 ;
52 }
19
A. Memory-related data structures in the kernel
In the Linux kernel, every process has an associated struct task_struct. The definition
of this struct is in the header file include /linux/sched.h.
1 struct task_struct {
2 volatile long s t a t e ;
3 /∗ −1 unrunnable , 0 runnable , >0 stopped ∗/
4 struct thread_info ∗thread_info ;
5 atomic_t usage ;
6 ...
7 struct mm_struct ∗mm, ∗active_mm ;
8 ...
9 pid_t pid ;
10 ...
11 char comm[ 1 6 ] ;
12 ...
13 } ;
• The mm_struct within the task_struct is the key to all memory management
activities related to the process.
20
Here the first member of importance is the mmap. The mmap contains the pointer
to the list of VMAs (Virtual Memory Areas) related to this process. Full usage of the
process address space occurs very rarely. The sparse regions used are denoted by VMAs.
The VMAs are stored in struct vm_area_struct defined in linux/mm.h:
1 struct vm_area_struct {
2 struct mm_struct ∗ vm_mm; /∗The address space we belong to . ∗/
3 unsigned long vm_start ; /∗Our s t a r t address within vm_mm. ∗/
4 unsigned long vm_end; /∗The f i r s t byte a f t e r our end
5 address within vm_mm. ∗/
6 ....
7 /∗ l i n k e d l i s t of VM areas per task , sorted by address ∗/
8 struct vm_area_struct ∗vm_next ;
9 ....
10 }
21