Operating System Lab2

Experiment Objectives

  1. Understand the concepts and application programming process of processes/threads;
  2. Understand the synchronization mechanisms and application programming of processes/threads;
  3. Master and promote domestic operating systems (recommend Galaxy Kylin or Ubuntu Kylin)

Experiment Content

  1. Create 2 threads A and B in Linux/Windows to loop output data or strings.
  2. Create (fork) a child process in Linux, experiment with wait/exit functions
  3. Use threads to implement concurrent drawing of circles and squares in Windows/Linux.
  4. Use threads to implement “producer-consumer” synchronization control in Windows or Linux
  5. Use signal mechanism (signal) to implement inter-process communication in Linux
  6. Simulate dining philosophers in Windows or Linux, provide deadlock and non-deadlock solutions.
  7. Study Linux kernel and use printk to debug process creation and scheduling policy related information.

Task 1: Create 2 Threads A and B in Linux/Windows to Loop Output Data or Strings

Requirements:

  1. Use pthread thread library or CreateThread function

  2. Thread A outputs 1-1000 in ascending order; Thread B outputs 1000-1 in descending order. To avoid output being too fast, output one number every 0.2 seconds (adjustable).

  3. When outputting data, also output “A” or “B” to indicate which thread is outputting, and pay attention to formatted output information. For example:

    1
    2
    3
    4
    5
    A:1000
    A:0999
    B:0001
    A:0998
    B:0002

Write Code and Compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// Thread A function - ascending output
void* print_numbers_ascending(void* arg) {
for (int i = 1; i <= 1000; i++) {
printf("A:%04d\n", i);
usleep(200000); // pause 0.2 seconds
}
return NULL;
}

// Thread B function - descending output
void* print_numbers_descending(void* arg) {
for (int i = 1000; i >= 1; i--) {
printf("B:%04d\n", i);
usleep(200000); // pause 0.2 seconds
}
return NULL;
}

int main() {
pthread_t threadA, threadB;

// Create threads
pthread_create(&threadA, NULL, print_numbers_ascending, NULL);
pthread_create(&threadB, NULL, print_numbers_descending, NULL);

// Wait for threads to finish
pthread_join(threadA, NULL);
pthread_join(threadB, NULL);

return 0;
}

/* pthread_create function: creates a thread
* Function prototype int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
* pthread_t *thread: This is a pointer to a pthread_t type variable, pthread_t is usually used to identify threads. After the function executes successfully, this variable will be assigned the identifier of the newly created thread.
* const pthread_attr_t *attr: Pointer to pthread_attr_t structure, used to set thread attributes. If NULL is passed, default attributes are used.
* void *(*start_routine) (void *): Pointer to the function that will be executed by the new thread. This function must accept a void * type parameter and return a void * type value.
* void *arg: Parameter passed to the start_routine function. This can be a pointer to any type, depending on your specific needs.
*/

/* pthread_join function: waits for a thread to terminate
* Function prototype int pthread_join(pthread_t thread, void **retval);
* pthread_t thread: The thread identifier to wait for. This is the identifier returned when creating a thread with pthread_create function.
* void **retval: If not NULL, points to a location used to store the exit status returned by the thread. If the thread exits through pthread_exit, retval will contain the value passed to pthread_exit. If the thread exits by returning (i.e., the thread start routine returns), retval will contain the returned value.
*/

Detailed explanation of pthread_create function:

Detailed explanation of pthread_create function

Use the command gcc -o mission1 mission1.c -lpthread to compile the file and execute it.

View Process Running Status During Execution

Common commands for viewing processes or threads:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ps -ef | grep [process name] # View information of processes with specific names
lixiang 4874 3403 0 11:11 pts/1 00:00:00 ./mission1
lixiang 4883 3455 0 11:12 pts/2 00:00:00 grep --color=auto mission1

ps -T -p [process PID] # View information of process with specific PID (-T shows its threads)
PID SPID TTY TIME CMD
4874 4874 pts/1 00:00:00 mission1
4874 4875 pts/1 00:00:00 mission1
4874 4876 pts/1 00:00:00 mission1

ps -Tfl -p [process PID] # More detailed display of process information with specific PID (including CPU utilization, etc.)
F S UID PID SPID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S lixiang 4945 4945 3403 0 80 0 - 21144 futex_ 11:17 pts/1 00:00:00 ./mission1
1 S lixiang 4945 4946 3403 0 80 0 - 21144 hrtime 11:17 pts/1 00:00:00 ./mission1
1 S lixiang 4945 4947 3403 0 80 0 - 21144 hrtime 11:17 pts/1 00:00:00 ./mission1

# Below are introductions to commonly used parameters:
-e: Display all processes, not just current user's processes.
-f: Display complete process information, including parent process ID, CPU utilization, etc.
-l: Display process information in long format, including process status, PID, terminal, CPU utilization, etc.
-u user: Display process information for specified user.
-p pid: Display process information for specified PID.
-s: Sort output by process start time.
-r: Sort output by process CPU utilization.
-T: Display threads

After running mission1, use ps command to display its detailed information

Detailed information of mission1 process

In the figure, SPID represents the thread ID numbers, 3852 and 3852 are the A and B threads we created

Program Execution Results

Execution result

Reference Materials:

Linux - Thread Creation

ps Command Introduction

Complete ps Command Manual

Task 2: Create (fork) a Child Process in Linux, Experiment with wait/exit Functions

Requirements:

  1. Effect 1: Parent process does not use wait function, let parent process end before child process, child process enters infinite loop or long-term loop, observe process ID and parent process ID of parent and child processes.
    1. Use printf in the program to output process number and parent process number of each process. Note, parent process and child process output should provide corresponding prompt strings for mutual distinction, same below
    2. At the same time, use ps command to display process list, observe process ID and parent process ID of specified processes, and explain whether these IDs are consistent with those output by printf.
  2. Effect 2: Parent process uses wait function. Child process sleeps for 5 seconds, parent process does not sleep. Child process uses exit to return parameters. Parent process printf the parameters returned by child process.

Effect One

The core of this task is to let the parent process end before the child process, observing the changes in process IDs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
pid_t pid = fork();

if (pid == 0)
{
// Child process
// Child process enters infinite loop
while (1)
{
printf("Child process print: Child process PID: %d, Parent process PID: %d\n", getpid(), getppid());
sleep(5);
}
}
else if (pid > 0)
{
// Parent process
printf("Parent process print: Parent process PID: %d, Child process PID: %d\n", getpid(), pid);
sleep(20);
}
else
{
// fork failed
printf("Failed to create child process\n");
}
}

Program execution result is as follows:

Program execution result

Parent process PID=3604, child process PID=3605, then after the parent process ends after 20s, the child process prints its own PID and parent process PID. Within the 20s before the parent process ends, the child process prints parent process PID=3604, but after the parent process ends, the child process continues running, and at this time the parent process ID becomes 1. The reason for this effect is:

Reason why child process’s parent process ID becomes 1 after parent process ends

Effect Two

Child process ends before parent process, and uses exit to return a value, parent process prints this status value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
pid_t pid = fork();

if (pid == 0)
{
// Child process
printf("Child process print: Child process PID: %d, Parent process PID: %d\n", getpid(), getppid());
sleep(5);
exit(42); // Use exit to return status value 42
}
else if (pid > 0)
{
// Parent process
int status;
wait(&status); // wait for child process to finish
if (WIFEXITED(status))
{
printf("Child process exit status: %d\n", WEXITSTATUS(status));
}
}
else
{
// fork failed
printf("Failed to create child process\n");
}

return 0;
}

Program execution result is as follows:

Program execution result

In the code, child process exit(42), parent process prints the exit status code 42.

【Linux】——Process Creation fork() Detailed Explanation

Task 3: Use Threads to Implement Concurrent Drawing of Circles and Squares in Windows/Linux

For this task, I used Qt6 to implement a dual-threaded GUI interface for drawing circles and squares. Here I only explain the core code parts:

Since Qt’s drawing actions can only be completed in the main thread, our two threads for drawing circles and squares are used to calculate the coordinate points of circles and squares respectively, and pass them to the main thread, which completes the corresponding drawing actions.

Project file directory is as follows:

Project file directory

First, Qt needs to overwrite paintEvent in the MainWindow class to draw graphics:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void MainWindow::paintEvent(QPaintEvent *event) {
QPainter painter(this);
painter.setPen(Qt::blue);
// Draw all circle points
for (const QPoint &pt : circlePoints) {
painter.drawPoint(pt);
}
// Connect points to draw circle (optional)
if (circlePoints.size() > 1) {
painter.setPen(Qt::red);
painter.drawPolyline(circlePoints.constData(), circlePoints.size());
}

// Draw all square points
for (const QPoint &pt : squarePoints) {
painter.drawPoint(pt);
}
// Connect points to draw square (optional)
if (squarePoints.size() > 1) {
painter.setPen(Qt::blue);
painter.drawPolyline(squarePoints.constData(), squarePoints.size());
}
}

Two thread-related classes, one is a circle thread class, one is a square thread class, they are both subclasses of Qt’s thread class QThread. Since they only differ in coordinate point calculation, I’ll choose the circle thread class to explain:

The process function in the circle thread class is used to handle the next coordinate point, then emit the circlePoint signal, passing this coordinate to the main thread for processing.

1
2
3
4
5
6
7
8
9
10
11
12
13
void CircleThread::process() {
double radianIncrement = 2 * (M_PI / 180.0); // Radian increment each time (1 degree)

// Calculate new point position, using circle's parametric equation as example
int centerX = center.x();
int centerY = center.y();
int x = centerX + static_cast<int>(radius * cos(circleAngle));
int y = centerY + static_cast<int>(radius * sin(circleAngle));
circleAngle += radianIncrement;

// Send new point position
emit circlePoint(QPoint(x, y));
}

For the circle thread class, we need to overwrite the run function. The run function is called when the thread starts. The specific logic is to call the process function at intervals. The process function handles point information and then sends it to the main thread.

1
2
3
4
5
6
7
void CircleThread::run() {
while (!isInterruptionRequested()) {
process();
if (circleAngle >= 2 * M_PI) break;
msleep(62);
}
}

What the main thread needs to do is initialize an instance of the circle thread class and connect the circlePoint signal mentioned earlier with the slot that handles points. Note that the Slot function is written as a Lambda expression, adding the points returned from the child thread to the circle point set, then updating the drawing.

1
2
3
4
5
6
7
8
// Create circle drawing thread
circleThread = new CircleThread(this, QPoint(width() / 4, height() / 2 - height() / 10), qMin(width() / 2, height()) / 2 - 50);
// Connect circle drawing thread's data sending signal with main thread's data receiving slot function
connect(circleThread, &CircleThread::circlePoint, this, [&](const QPoint &pt) {
circlePoints.append(pt);
// Update to call paintEvent
update();
});

The final step is relatively simple. We need to start the threads, and thread startup is controlled by the “Start Drawing” button. Clicking it starts both threads.

1
2
3
4
5
6
7
// This is a slot function used to connect with button's click signal
void MainWindow::on_startpaint_clicked() {
// Start circle drawing thread
circleThread->start();
// Start square drawing thread
squareThread->start();
}

Finally, start the program. The program execution effect is as follows:

Program execution effect

Reference Materials:

Qt Multi-threading Method 1 (Step-by-step explanation + code + demonstration)

Qt Documentation

Task 4: Use Threads to Implement “Producer-Consumer” Synchronization Control in Windows or Linux

Task Requirements:

  1. Use an array (10 elements) instead of a buffer. 2 input threads produce products (random numbers) and store them in the array; 3 output threads take numbers from the array and output them.
  2. Linux uses mutex objects and lightweight semaphore objects, main functions: sem_wait(), sem_post(), pthread_mutex_lock(), pthread_mutex_unlock()
  3. Producer 1 data: 1000-1999 (random interval 100ms-1s for each data), Producer 2 data: 2000-2999 (random interval 100ms-1s for each data)
  4. Consumers sleep for random time 100ms-1s to consume one data.
  5. Screen print (or log file record) production and consumption records for each data.

Source code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 10

// Buffer and related synchronization mechanisms
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
pthread_mutex_t mutex;
sem_t empty, full;

// Random sleep time
int random_sleep()
{
return rand() % 901 + 100; // Random time from 100ms to 1s
}

// Producer thread function
void *producer(void *param)
{
int id = *(int *)param;
int base = id == 1 ? 1000 : 2000;

while (1)
{
int product = base + rand() % 1000;
sem_wait(&empty);
pthread_mutex_lock(&mutex);

// Store product in buffer
buffer[in] = product;
in = (in + 1) % BUFFER_SIZE;
printf("Producer %d produced %d\n", id, product);

pthread_mutex_unlock(&mutex);
sem_post(&full);

usleep(random_sleep() * 1000); // Sleep
}

return NULL;
}

// Consumer thread function
void *consumer(void *param)
{
int id = *(int *)param;

while (1)
{
sem_wait(&full);
pthread_mutex_lock(&mutex);

// Take product from buffer
int product = buffer[out];
out = (out + 1) % BUFFER_SIZE;
printf("Consumer %d consumed %d\n", id, product);

pthread_mutex_unlock(&mutex);
sem_post(&empty);

usleep(random_sleep() * 1000); // Sleep
}

return NULL;
}

int main()
{
pthread_t producers[2], consumers[3];
int producer_ids[2] = {1, 2};
int consumer_ids[3] = {1, 2, 3};

// Initialize mutex with default attributes
pthread_mutex_init(&mutex, NULL);
// Second parameter 0 means semaphore is used for inter-thread synchronization
sem_init(&empty, 0, BUFFER_SIZE); // Start with BUFFER_SIZE empty slots
sem_init(&full, 0, 0); // Start with no products available for consumption

// Create producer and consumer threads
for (int i = 0; i < 2; ++i)
{
pthread_create(&producers[i], NULL, producer, &producer_ids[i]);
}
for (int i = 0; i < 3; ++i)
{
pthread_create(&consumers[i], NULL, consumer, &consumer_ids[i]);
}

// Wait for threads to finish
for (int i = 0; i < 2; ++i)
{
pthread_join(producers[i], NULL);
}
for (int i = 0; i < 3; ++i)
{
pthread_join(consumers[i], NULL);
}

// Destroy mutex and semaphores
pthread_mutex_destroy(&mutex);
sem_destroy(&empty);
sem_destroy(&full);

return 0;
}

Code Analysis:

First, we should add a mutex lock to the product area to prevent simultaneous access by multiple threads. Then we need to ensure that when the product buffer is full, producers stop producing, and when there are no products, consumers cannot consume. So we need P-V operations to complete the synchronization mechanism. The empty signal represents the current empty slots in the buffer. Whenever a producer starts producing, empty slots decrease by 1; after a consumer consumes, empty slots increase by 1. The full signal represents the current number of products in the buffer. When a consumer starts consuming, it decreases by 1; when a producer finishes producing, it increases by 1. This implements the synchronization mechanism between producers and consumers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*Producer*/
sem_wait(&empty);
pthread_mutex_lock(&mutex);

// Store product in buffer
buffer[in] = product;
in = (in + 1) % BUFFER_SIZE;
printf("Producer %d produced %d\n", id, product);

pthread_mutex_unlock(&mutex);
sem_post(&full);
/*Producer*/

/*Consumer*/
sem_wait(&full);
pthread_mutex_lock(&mutex);

// Take product from buffer
int product = buffer[out];
out = (out + 1) % BUFFER_SIZE;
printf("Consumer %d consumed %d\n", id, product);

pthread_mutex_unlock(&mutex);
sem_post(&empty);
/*Consumer*/

Program execution result is as follows:

Program execution result

Use ps command to view all threads in the current process:

Use ps command to view all threads in the current process
WCHAN explanation

Reference Materials:

Thread Synchronization Problem - Producer Consumer

Task 5: Use Signal Mechanism (signal) to Implement Inter-Process Communication in Linux

Task Requirements:

  1. Parent process creates (fork) child process and makes child process enter infinite loop.
  2. Child process outputs “I am Child Process, alive !” every 2 seconds
  3. Parent process asks user “To terminate Child Process. Yes or No? ” requiring user to answer Y or N from keyboard. If user answers N, delay 2 seconds before asking again.
  4. If user answers Y, send user signal to child process to make it end.
  5. Before child process ends, print string: “Bye,World !”
  6. Functions: kill(), signal(), use user signal, write signal handler function

The core of this task is to use the kill function to terminate the child process, and pass a signal SIGUSR1 when killing (user-defined signal that can be used to report abnormal behavior such as division by zero errors, segmentation faults, etc., or to control processes such as terminating processes, stopping (pausing) processes, continuing (resuming) stopped processes, etc.). The child process receives the signal and calls the signalHandler signal processing function to perform corresponding operations.

Source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

// Signal handler function
void signalHandler(int sig)
{
printf("Bye, World!\n");
exit(0);
}

int main()
{
pid_t pid = fork();

if (pid == 0)
{
// Child process

signal(SIGUSR1, signalHandler); // Register signal handler function
while (1)
{
printf("I am Child Process, alive!\n");
sleep(2);
}
}
else if (pid > 0)
{
// Parent process
char answer;
do
{
printf("To terminate Child Process. Yes or No?\n");
scanf(" %c", &answer);
if (answer == 'N' || answer == 'n')
{
sleep(2);
}
} while (answer != 'Y' && answer != 'y');

kill(pid, SIGUSR1); // Send SIGUSR1 signal to child process
wait(NULL); // Wait for child process to end
}
else
{
// fork failed
perror("fork failed");
exit(1);
}

return 0;
}

Code execution result:

Code execution result

Reference Materials:

Linux Inter-Process Communication Lecture 3 Signal signal kill

Task 6: Simulate Dining Philosophers in Windows or Linux, Provide Deadlock and Non-Deadlock Solutions

Task Requirements:

  1. Provide both solutions that may cause deadlock and solutions that cannot cause deadlock.
  2. For solutions that may cause deadlock, refer to course materials. Windows try using critical section objects (EnterCriticalSection, LeaveCriticalSection); Linux try using mutex locks (pthread_mutex_lock, pthread_mutex_unlock)
  3. Solutions that absolutely cannot cause deadlock, for example: try to pick up both chopsticks, if both can be picked up then pick them up, otherwise don’t pick up either.
  4. Linux try mutex functions pthread_mutex_lock, pthread_mutex_trylock, etc.
  5. To enhance randomness, maintain random duration of 100ms-500ms between states.
  6. [Optional] Graphical interface showing philosophers picking up chopsticks, eating, putting down chopsticks, thinking, etc.

Deadlock Solution

The problem with the deadlock solution is that each philosopher picks up the left chopstick first, then the right chopstick. When they all pick up the left chopstick simultaneously, no philosopher can get the right chopstick, creating a deadlock problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define PHILOSOPHER_COUNT 5

pthread_mutex_t chopsticks[PHILOSOPHER_COUNT];

int get_random_sleep_time()
{
return rand() % 400000 + 100000; // 100ms到500ms的随机时间
}

void *philosopher(void *num)
{
int id = *(int *)num;

while (1)
{
printf("Philosopher %d is thinking\n", id);
usleep(get_random_sleep_time()); // Random thinking time

printf("Philosopher %d is resting\n", id);
usleep(get_random_sleep_time()); // Random rest time

pthread_mutex_lock(&chopsticks[id]); // Pick up left chopstick
printf("Philosopher %d picked up left chopstick\n", id);
pthread_mutex_lock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]); // Pick up right chopstick
printf("Philosopher %d picked up right chopstick\n", id);

printf("Philosopher %d is eating\n", id);
usleep(get_random_sleep_time()); // Random eating time

pthread_mutex_unlock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]); // Put down right chopstick
pthread_mutex_unlock(&chopsticks[id]); // Put down left chopstick
}

return NULL;
}

int main()
{
pthread_t philosophers[PHILOSOPHER_COUNT];
int philosopher_numbers[PHILOSOPHER_COUNT];

for (int i = 0; i < PHILOSOPHER_COUNT; ++i)
{
pthread_mutex_init(&chopsticks[i], NULL);
philosopher_numbers[i] = i;
}

for (int i = 0; i < PHILOSOPHER_COUNT; ++i)
{
pthread_create(&philosophers[i], NULL, philosopher, &philosopher_numbers[i]);
}

for (int i = 0; i < PHILOSOPHER_COUNT; ++i)
{
pthread_join(philosophers[i], NULL);
}

return 0;
}

Deadlock state:

Deadlock state

It can be observed that they all picked up the left chopsticks simultaneously, creating a deadlock phenomenon.

Non-Deadlock Solution

My solution to deadlock is that philosophers try to pick up the left chopstick first, then the right chopstick. Unlike the above, if philosophers cannot get the right chopstick to eat, they put down the left chopstick they currently hold, thus avoiding the deadlock problem.

In terms of specific functions: pthread_mutex_trylock function replaces pthread_mutex_lock to avoid blocking. If the lock cannot be acquired immediately, it won’t enter a waiting state, thus avoiding deadlock situations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define PHILOSOPHER_COUNT 5

pthread_mutex_t chopsticks[PHILOSOPHER_COUNT];

int get_random_sleep_time()
{
return rand() % 400000 + 100000; // 100ms到500ms的随机时间
}

void *philosopher(void *num)
{
int id = *(int *)num;
int left = id;
int right = (id + 1) % PHILOSOPHER_COUNT;

while (1)
{
printf("Philosopher %d is thinking\n", id);
usleep(get_random_sleep_time()); // Random thinking time

printf("Philosopher %d is resting\n", id);
usleep(get_random_sleep_time()); // Random thinking time

// Each philosopher tries to get the left chopstick first, then the right chopstick. If both chopsticks are successfully acquired, the philosopher starts eating
// pthread_mutex_trylock function replaces pthread_mutex_lock to avoid blocking. If the lock cannot be acquired immediately, it won't enter a waiting state, thus avoiding deadlock situations
if (pthread_mutex_trylock(&chopsticks[id]) == 0)
{
printf("Philosopher %d picked up left chopstick\n", id);
sleep(1); // Added a 1-second delay here to make it easier for philosophers to pick up left chopsticks simultaneously
if (pthread_mutex_trylock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]) == 0)
{
printf("Philosopher %d picked up right chopstick\n", id);
printf("Philosopher %d is eating\n", id);
usleep(get_random_sleep_time()); // Random eating time

pthread_mutex_unlock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]);
printf("Philosopher %d put down right chopstick\n", id);
}
pthread_mutex_unlock(&chopsticks[id]);
printf("Philosopher %d put down left chopstick\n", id);
}
}

return NULL;
}

int main()
{
pthread_t philosophers[PHILOSOPHER_COUNT];
int philosopher_numbers[PHILOSOPHER_COUNT];

for (int i = 0; i < PHILOSOPHER_COUNT; ++i)
{
pthread_mutex_init(&chopsticks[i], NULL);
philosopher_numbers[i] = i;
}

for (int i = 0; i < PHILOSOPHER_COUNT; ++i)
{
pthread_create(&philosophers[i], NULL, philosopher, &philosopher_numbers[i]);
}

for (int i = 0; i < PHILOSOPHER_COUNT; ++i)
{
pthread_join(philosophers[i], NULL);
}

return 0;
}

Non-deadlock program execution result:

Non-deadlock program execution result

Here I intentionally made philosophers sleep for a longer time after getting the left chopstick, creating a conflict where they all get the left chopstick simultaneously. From the figure, we can see that all 5 philosophers indeed picked up the left chopsticks simultaneously. Unlike the deadlock approach, philosopher 4 actively put down the left chopstick and re-entered the thinking state, avoiding the deadlock situation.

Reference Materials:

Dining Philosophers Problem

Five Philosophers Dining Problem

Requirements: Write application Hello.c, call fork to create process, track the fork process of the newly created child process in the kernel and display PCB member variables related to scheduling policy.

  1. Write application Hello.c, call fork to create child process (functionality unlimited), print parent and child process ID numbers.
  2. Use printk in appropriate locations in the kernel (such as somewhere in the do_fork function) to output debugging information like “currently creating process corresponding cmd, process ID and parent process ID”.
  3. To avoid frequent output of the above debugging information by the do_fork function, it must be limited to only output the above debugging information when fork is called in the Hello program. Please think about how to implement this.

Reference method: Kernel design global variable bool flag and system call SetDebug(bool), SetDebug can modify flag value to true or false. In the Hello program, call SetDebug(true) before calling fork function and SetDebug(false) after calling fork function to modify flag. When printk debugging information, check flag to determine whether to use printk to output debugging information.

Friendly reminder: The do_fork function has been replaced by the kernel_clone function in newer versions of Linux kernel source code.

Modify ./kernel/fork.c file

Use printk in kernel_clone to print related information. p is a pointer to the task_struct structure, comm is the currently executing command, pid is the child process ID, current->pid is the parent process ID, current is a macro pointing to the parent process.

Add printk code in kernel_clone

Add new system call setdebug for setting debug_fork_flag. The bool value debug_fork_flag is a global variable used to control whether to output.

setdebug system call

Add Declarations

  1. System call: ./kernel/fork.c (already modified)
  2. System call function declaration ./include/linux/syscalls.h
  3. ID: ./arch/x86/entry/syscalls/syscall_64.tbl
  4. ID declaration: ./include/uapi/asm-generic/unistd.h

The modification operations for the remaining 3 files have been practiced in the task of adding system calls in experiment 1, so I won’t repeat them here.

Compile Kernel

1
2
3
4
5
6
# Already practiced in experiment 1 kernel compilation
make mrproper
make clean
make -j6
make modules_install
make install

Write Test Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

// Define SetDebug system call number (needs to be registered in kernel)
#define SYS_SETDEBUG 447

void SetDebug(int value) {
syscall(SYS_SETDEBUG, value);
}

int main() {
SetDebug(1); // Enable debug information

pid_t pid = fork();

if (pid == 0) {
// Child process
printf("Child process ID: %d\n", getpid());
} else if (pid > 0) {
// Parent process
printf("Parent process ID: %d\n", getpid());
} else {
// fork failed
perror("fork");
return 1;
}

SetDebug(0); // Disable debug information
return 0;
}

To test the functionality, I commented out the SetDebug function in another test code and executed both programs simultaneously. The expected effect is that only the program that called the SetDebug function will output debug information during fork.

test1 did not enable print information, test2 enabled print information.

Run test1 and test2

Use dmesg to view information. In the figure below, we can see a line in the background information about a process being created, cmd=test2, and comparing with the child process and parent process PIDs in the above figure, they are also consistent.

dmesg print background information

Reference Materials:

Linux Kernel Process - do_fork() Function Implementation Principle

Linux Kernel Source Code fork Interpretation