操作系统实验2

实验目标

  1. 理解进程/线程的概念及应用编程过程;
  2. 理解进程/线程的同步机制及应用编程;
  3. 掌握并推广国内操作系统(推荐银河麒麟或Ubuntu Kylin)

实验内容

  1. 在Linux/Windows中创建2个线程A和B,循环输出数据或字符串。
  2. 在Linux中创建(fork)一个子进程,实验wait/exit函数。
  3. 使用线程在Windows/Linux中实现圆形和方形的并发绘制。
  4. 使用线程在Windows或Linux中实现“生产者-消费者”同步控制。
  5. 使用信号机制(signal)在Linux中实现进程间通信。
  6. 在Windows或Linux中模拟哲学家就餐,提供死锁和非死锁解决方案。
  7. 学习Linux内核并使用printk调试进程创建和调度策略相关信息。

任务1:在Linux/Windows中创建2个线程A和B,循环输出数据或字符串

要求:

  1. 使用pthread线程库或CreateThread函数。

  2. 线程A按升序输出1-1000;线程B按降序输出1000-1。为了避免输出过快,每0.2秒输出一个数字(可调)。

  3. 输出数据时,同时输出“A”或“B”以指示哪个线程在输出,并注意格式化输出信息。例如:

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

编写代码并编译

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>

// 线程A函数 - 升序输出
void* print_numbers_ascending(void* arg) {
for (int i = 1; i <= 1000; i++) {
printf("A:%04d\n", i);
usleep(200000); // 暂停0.2秒
}
return NULL;
}

// 线程B函数 - 降序输出
void* print_numbers_descending(void* arg) {
for (int i = 1000; i >= 1; i--) {
printf("B:%04d\n", i);
usleep(200000); // 暂停0.2秒
}
return NULL;
}

int main() {
pthread_t threadA, threadB;

// 创建线程
pthread_create(&threadA, NULL, print_numbers_ascending, NULL);
pthread_create(&threadB, NULL, print_numbers_descending, NULL);

// 等待线程结束
pthread_join(threadA, NULL);
pthread_join(threadB, NULL);

return 0;
}

/* pthread_create函数:创建一个线程
* 函数原型 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
* pthread_t *thread:指向pthread_t类型变量的指针,pthread_t通常用于标识线程。函数执行成功后,该变量将被赋值为新创建线程的标识符。
* const pthread_attr_t *attr:指向pthread_attr_t结构的指针,用于设置线程属性。如果传入NULL,则使用默认属性。
* void *(*start_routine) (void *):指向新线程将要执行的函数的指针。该函数必须接受一个void *类型的参数并返回一个void *类型的值。
* void *arg:传递给start_routine函数的参数。这可以是指向任何类型的指针,具体取决于你的需求。
*/

/* pthread_join函数:等待线程终止
* 函数原型 int pthread_join(pthread_t thread, void **retval);
* pthread_t thread:要等待的线程标识符。这是创建线程时pthread_create函数返回的标识符。
* void **retval:如果不为NULL,指向一个位置,用于存储线程返回的退出状态。如果线程通过pthread_exit退出,retval将包含传递给pthread_exit的值。如果线程通过返回(即线程启动例程返回)退出,retval将包含返回值。
*/

pthread_create函数的详细解释:

pthread_create函数详细解释

使用命令gcc -o mission1 mission1.c -lpthread编译文件并执行。

查看执行过程中的进程运行状态

查看进程或线程的常用命令:

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 [进程名称] # 查看特定名称的进程信息
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 [进程PID] # 查看特定PID的进程信息(-T显示其线程)
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 [进程PID] # 更详细地显示特定PID的进程信息(包括CPU利用率等)
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

# 以下是常用参数的介绍:
-e: 显示所有进程,而不仅仅是当前用户的进程。
-f: 显示完整的进程信息,包括父进程ID、CPU利用率等。
-l: 以长格式显示进程信息,包括进程状态、PID、终端、CPU利用率等。
-u user: 显示指定用户的进程信息。
-p pid: 显示指定PID的进程信息。
-s: 按进程启动时间排序输出。
-r: 按进程CPU利用率排序输出。
-T: 显示线程

运行mission1后,使用ps命令显示其详细信息。

mission1进程的详细信息

在图中,SPID表示线程ID号,3852和3852是我们创建的A和B线程。

程序执行结果

执行结果

参考资料:

Linux - 线程创建

ps命令介绍

完整ps命令手册

任务2:在Linux中创建(fork)一个子进程,实验wait/exit函数

要求:

  1. 效果1:父进程不使用wait函数,让父进程在子进程之前结束,子进程进入无限循环或长期循环,观察父子进程的进程ID和父进程ID。
    1. 在程序中使用printf输出每个进程的进程号和父进程号。注意,父进程和子进程的输出应提供相应的提示字符串以便区分,以下同。
    2. 同时使用ps命令显示进程列表,观察指定进程的进程ID和父进程ID,并解释这些ID是否与printf输出的一致。
  2. 效果2:父进程使用wait函数。子进程睡眠5秒,父进程不睡眠。子进程使用exit返回参数。父进程printf子进程返回的参数。

效果一

本任务的核心是让父进程在子进程之前结束,观察进程ID的变化。

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)
{
// 子进程
// 子进程进入无限循环
while (1)
{
printf("子进程输出:子进程PID:%d,父进程PID:%d\n", getpid(), getppid());
sleep(5);
}
}
else if (pid > 0)
{
// 父进程
printf("父进程输出:父进程PID:%d,子进程PID:%d\n", getpid(), pid);
sleep(20);
}
else
{
// fork失败
printf("创建子进程失败\n");
}
}

程序执行结果如下:

程序执行结果

父进程PID=3604,子进程PID=3605,然后在父进程结束后20秒,子进程打印自己的PID和父进程PID。在父进程结束的20秒内,子进程打印父进程PID=3604,但在父进程结束后,子进程继续运行,此时父进程ID变为1。造成这种效果的原因是:

父进程结束后子进程的父进程ID变为1的原因

效果二

子进程在父进程之前结束,并使用exit返回一个值,父进程打印这个状态值。

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)
{
// 子进程
printf("子进程输出:子进程PID:%d,父进程PID:%d\n", getpid(), getppid());
sleep(5);
exit(42); // 使用exit返回状态值42
}
else if (pid > 0)
{
// 父进程
int status;
wait(&status); // 等待子进程结束
if (WIFEXITED(status))
{
printf("子进程退出状态:%d\n", WEXITSTATUS(status));
}
}
else
{
// fork失败
printf("创建子进程失败\n");
}

return 0;
}

程序执行结果如下:

程序执行结果

在代码中,子进程exit(42),父进程打印退出状态码42。

【Linux】——进程创建fork()详细解释

任务3:使用线程实现Windows/Linux中的圆形和方形的并发绘制

对于这个任务,我使用Qt6实现了一个双线程的GUI界面,用于绘制圆形和方形。这里我只解释核心代码部分:

由于Qt的绘图操作只能在主线程中完成,因此我们用于绘制圆形和方形的两个线程分别用于计算圆形和方形的坐标点,并将其传递给主线程,由主线程完成相应的绘图操作。

项目文件目录如下:

项目文件目录

首先,Qt需要在MainWindow类中重写paintEvent以绘制图形:

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);
// 绘制所有圆形点
for (const QPoint &pt : circlePoints) {
painter.drawPoint(pt);
}
// 连接点以绘制圆形(可选)
if (circlePoints.size() > 1) {
painter.setPen(Qt::red);
painter.drawPolyline(circlePoints.constData(), circlePoints.size());
}

// 绘制所有方形点
for (const QPoint &pt : squarePoints) {
painter.drawPoint(pt);
}
// 连接点以绘制方形(可选)
if (squarePoints.size() > 1) {
painter.setPen(Qt::blue);
painter.drawPolyline(squarePoints.constData(), squarePoints.size());
}
}

两个与线程相关的类,一个是圆形线程类,一个是方形线程类,它们都是Qt的线程类QThread的子类。由于它们在坐标点计算上仅有差异,我将选择圆形线程类进行说明:

圆形线程类中的process函数用于处理下一个坐标点,然后发出circlePoint信号,将该坐标传递给主线程进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
void CircleThread::process() {
double radianIncrement = 2 * (M_PI / 180.0); // 每次的弧度增量(1度)

// 计算新点位置,以圆的参数方程为例
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;

// 发送新点位置
emit circlePoint(QPoint(x, y));
}

对于圆形线程类,我们需要重写run函数。run函数在线程启动时被调用。具体逻辑是以间隔调用process函数,process函数处理点信息,然后将其发送到主线程。

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

主线程需要做的是初始化圆形线程类的实例,并将之前提到的circlePoint信号与处理点的槽连接。注意,槽函数以Lambda表达式的形式编写,将子线程返回的点添加到圆形点集合中,然后更新绘图。

1
2
3
4
5
6
7
8
// 创建圆形绘制线程
circleThread = new CircleThread(this, QPoint(width() / 4, height() / 2 - height() / 10), qMin(width() / 2, height()) / 2 - 50);
// 将圆形绘制线程的数据发送信号与主线程的数据接收槽函数连接
connect(circleThread, &CircleThread::circlePoint, this, [&](const QPoint &pt) {
circlePoints.append(pt);
// 更新以调用paintEvent
update();
});

最后一步相对简单。我们需要启动线程,线程的启动由“开始绘制”按钮控制。点击它会启动两个线程。

1
2
3
4
5
6
7
// 这是一个用于连接按钮点击信号的槽函数
void MainWindow::on_startpaint_clicked() {
// 启动圆形绘制线程
circleThread->start();
// 启动方形绘制线程
squareThread->start();
}

最后,启动程序。程序执行效果如下:

程序执行效果

参考资料:

Qt多线程方法1(逐步解释+代码+演示)

Qt文档

任务4:使用线程实现Windows或Linux中的“生产者-消费者”同步控制

任务要求:

  1. 使用一个数组(10个元素)代替缓冲区。2个输入线程生成产品(随机数)并存储在数组中;3个输出线程从数组中取出数字并输出。
  2. Linux使用互斥对象和轻量级信号量对象,主要函数:sem_wait()、sem_post()、pthread_mutex_lock()、pthread_mutex_unlock()。
  3. 生产者1的数据:1000-1999(每个数据随机间隔100ms-1s),生产者2的数据:2000-2999(每个数据随机间隔100ms-1s)。
  4. 消费者随机睡眠100ms-1s以消费一个数据。
  5. 屏幕打印(或日志文件记录)每个数据的生产和消费记录。

源代码如下:

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

// 缓冲区和相关同步机制
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
pthread_mutex_t mutex;
sem_t empty, full;

// 随机睡眠时间
int random_sleep()
{
return rand() % 901 + 100; // 随机时间从100ms到1s
}

// 生产者线程函数
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);

// 存储产品到缓冲区
buffer[in] = product;
in = (in + 1) % BUFFER_SIZE;
printf("生产者 %d 生产了 %d\n", id, product);

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

usleep(random_sleep() * 1000); // 睡眠
}

return NULL;
}

// 消费者线程函数
void *consumer(void *param)
{
int id = *(int *)param;

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

// 从缓冲区取出产品
int product = buffer[out];
out = (out + 1) % BUFFER_SIZE;
printf("消费者 %d 消费了 %d\n", id, product);

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

usleep(random_sleep() * 1000); // 睡眠
}

return NULL;
}

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

// 初始化互斥锁,使用默认属性
pthread_mutex_init(&mutex, NULL);
// 第二个参数0表示信号量用于线程间同步
sem_init(&empty, 0, BUFFER_SIZE); // 初始为空槽位为BUFFER_SIZE
sem_init(&full, 0, 0); // 初始没有可消费的产品

// 创建生产者和消费者线程
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]);
}

// 等待线程结束
for (int i = 0; i < 2; ++i)
{
pthread_join(producers[i], NULL);
}
for (int i = 0; i < 3; ++i)
{
pthread_join(consumers[i], NULL);
}

// 销毁互斥锁和信号量
pthread_mutex_destroy(&mutex);
sem_destroy(&empty);
sem_destroy(&full);

return 0;
}

代码分析:

首先,我们应该在产品区添加互斥锁,以防止多个线程同时访问。然后我们需要确保当产品缓冲区满时,生产者停止生产;当没有产品时,消费者无法消费。因此我们需要P-V操作来完成同步机制。empty信号表示当前缓冲区中的空槽位。每当生产者开始生产时,空槽位减少1;消费者消费后,空槽位增加1。full信号表示当前缓冲区中的产品数量。当消费者开始消费时,减少1;当生产者完成生产时,增加1。这实现了生产者和消费者之间的同步机制。

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
/*生产者*/
sem_wait(&empty);
pthread_mutex_lock(&mutex);

// 存储产品到缓冲区
buffer[in] = product;
in = (in + 1) % BUFFER_SIZE;
printf("生产者 %d 生产了 %d\n", id, product);

pthread_mutex_unlock(&mutex);
sem_post(&full);
/*生产者*/

/*消费者*/
sem_wait(&full);
pthread_mutex_lock(&mutex);

// 从缓冲区取出产品
int product = buffer[out];
out = (out + 1) % BUFFER_SIZE;
printf("消费者 %d 消费了 %d\n", id, product);

pthread_mutex_unlock(&mutex);
sem_post(&empty);
/*消费者*/

程序执行结果如下:

程序执行结果

使用ps命令查看当前进程中的所有线程:

使用ps命令查看当前进程中的所有线程
WCHAN解释

参考资料:

线程同步问题 - 生产者消费者

任务5:使用信号机制(signal)在Linux中实现进程间通信

任务要求:

  1. 父进程创建(fork)子进程,并使子进程进入无限循环。
  2. 子进程每2秒输出“I am Child Process, alive !”。
  3. 父进程询问用户“To terminate Child Process. Yes or No? ”,要求用户从键盘回答Y或N。如果用户回答N,延迟2秒后再询问。
  4. 如果用户回答Y,发送用户信号给子进程使其结束。
  5. 在子进程结束前,打印字符串:“Bye,World !”。
  6. 函数:kill()、signal(),使用用户信号,编写信号处理函数。

本任务的核心是使用kill函数终止子进程,并在杀死时传递信号SIGUSR1(用户定义的信号,可用于报告异常行为,如除零错误、段错误等,或控制进程,如终止进程、停止(暂停)进程、继续(恢复)停止的进程等)。子进程接收信号并调用signalHandler信号处理函数执行相应操作。

源代码:

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>

// 信号处理函数
void signalHandler(int sig)
{
printf("Bye, World!\n");
exit(0);
}

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

if (pid == 0)
{
// 子进程

signal(SIGUSR1, signalHandler); // 注册信号处理函数
while (1)
{
printf("I am Child Process, alive!\n");
sleep(2);
}
}
else if (pid > 0)
{
// 父进程
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); // 发送SIGUSR1信号给子进程
wait(NULL); // 等待子进程结束
}
else
{
// fork失败
perror("fork failed");
exit(1);
}

return 0;
}

代码执行结果:

代码执行结果

参考资料:

Linux进程间通信讲座3 信号 signal kill

任务6:在Windows或Linux中模拟哲学家就餐,提供死锁和非死锁解决方案

任务要求:

  1. 提供可能导致死锁的解决方案和绝对不会导致死锁的解决方案。
  2. 对于可能导致死锁的解决方案,参考课程材料。Windows尝试使用临界区对象(EnterCriticalSection,LeaveCriticalSection);Linux尝试使用互斥锁(pthread_mutex_lock,pthread_mutex_unlock)。
  3. 绝对不会导致死锁的解决方案,例如:尝试同时拿起两根筷子,如果都能拿起则拿起,否则不拿起任何一根。
  4. Linux尝试互斥函数pthread_mutex_lock,pthread_mutex_trylock等。
  5. 为增强随机性,保持状态之间的随机持续时间为100ms-500ms。
  6. [可选] 图形界面显示哲学家拿起筷子、吃饭、放下筷子、思考等。

死锁解决方案

死锁解决方案的问题在于每个哲学家先拿起左边的筷子,然后再拿起右边的筷子。当他们同时拿起左边的筷子时,没有哲学家能拿到右边的筷子,造成死锁问题。

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("哲学家 %d 在思考\n", id);
usleep(get_random_sleep_time()); // 随机思考时间

printf("哲学家 %d 在休息\n", id);
usleep(get_random_sleep_time()); // 随机休息时间

pthread_mutex_lock(&chopsticks[id]); // 拿起左边的筷子
printf("哲学家 %d 拿起左边的筷子\n", id);
pthread_mutex_lock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]); // 拿起右边的筷子
printf("哲学家 %d 拿起右边的筷子\n", id);

printf("哲学家 %d 在吃饭\n", id);
usleep(get_random_sleep_time()); // 随机吃饭时间

pthread_mutex_unlock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]); // 放下右边的筷子
pthread_mutex_unlock(&chopsticks[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;
}

死锁状态:

死锁状态

可以观察到,他们都同时拿起左边的筷子,造成死锁现象。

非死锁解决方案

我对死锁的解决方案是哲学家先尝试拿起左边的筷子,然后再拿起右边的筷子。与上述不同的是,如果哲学家无法拿到右边的筷子吃饭,他们会放下当前拿着的左边的筷子,从而避免死锁问题。

在具体函数上:pthread_mutex_trylock函数替代pthread_mutex_lock以避免阻塞。如果无法立即获取锁,则不会进入等待状态,从而避免死锁情况。

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("哲学家 %d 在思考\n", id);
usleep(get_random_sleep_time()); // 随机思考时间

printf("哲学家 %d 在休息\n", id);
usleep(get_random_sleep_time()); // 随机思考时间

// 每个哲学家尝试先拿起左边的筷子,然后再拿起右边的筷子。如果两根筷子都成功获取,哲学家开始吃饭
// pthread_mutex_trylock函数替代pthread_mutex_lock以避免阻塞。如果无法立即获取锁,则不会进入等待状态,从而避免死锁情况
if (pthread_mutex_trylock(&chopsticks[id]) == 0)
{
printf("哲学家 %d 拿起左边的筷子\n", id);
sleep(1); // 这里增加1秒的延迟,使哲学家更容易同时拿起左边的筷子
if (pthread_mutex_trylock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]) == 0)
{
printf("哲学家 %d 拿起右边的筷子\n", id);
printf("哲学家 %d 在吃饭\n", id);
usleep(get_random_sleep_time()); // 随机吃饭时间

pthread_mutex_unlock(&chopsticks[(id + 1) % PHILOSOPHER_COUNT]);
printf("哲学家 %d 放下右边的筷子\n", id);
}
pthread_mutex_unlock(&chopsticks[id]);
printf("哲学家 %d 放下左边的筷子\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;
}

非死锁程序执行结果:

非死锁程序执行结果

这里我故意让哲学家在拿起左边的筷子后睡得更久,造成他们都同时拿起左边的筷子冲突。从图中可以看到,所有5个哲学家确实同时拿起了左边的筷子。与死锁方法不同,哲学家4主动放下了左边的筷子,重新进入思考状态,避免了死锁情况。

参考资料:

哲学家就餐问题

五个哲学家就餐问题

任务7:学习Linux内核并使用printk调试进程创建和调度策略相关信息

要求:编写应用程序Hello.c,调用fork创建进程,跟踪新创建的子进程在内核中的fork过程,并显示与调度策略相关的PCB成员变量。

  1. 编写应用程序Hello.c,调用fork创建子进程(功能无限),打印父子进程的ID号。
  2. 在内核中适当位置使用printk(如do_fork函数中的某个地方)输出调试信息,如“当前创建进程对应cmd、进程ID和父进程ID”。
  3. 为避免do_fork函数频繁输出上述调试信息,必须限制仅在Hello程序中调用fork时输出上述调试信息。请思考如何实现。

参考方法:内核设计全局变量bool flag和系统调用SetDebug(bool),SetDebug可以修改flag值为true或false。在Hello程序中,在调用fork函数前调用SetDebug(true),在调用fork函数后调用SetDebug(false)以修改flag。当printk调试信息时,检查flag以确定是否使用printk输出调试信息。

友情提醒:do_fork函数在新版本的Linux内核源代码中已被kernel_clone函数替代。

修改./kernel/fork.c文件

在kernel_clone中使用printk打印相关信息。p是指向task_struct结构的指针,comm是当前执行的命令,pid是子进程ID,current->pid是父进程ID,current是指向父进程的宏。

在kernel_clone中添加printk代码

添加新的系统调用setdebug用于设置debug_fork_flag。bool值debug_fork_flag是一个全局变量,用于控制是否输出。

setdebug系统调用

添加声明

  1. 系统调用:./kernel/fork.c(已修改)
  2. 系统调用函数声明:./include/linux/syscalls.h
  3. ID:./arch/x86/entry/syscalls/syscall_64.tbl
  4. ID声明:./include/uapi/asm-generic/unistd.h

其余3个文件的修改操作已在实验1的添加系统调用任务中实践过,因此在此不再重复。

编译内核

1
2
3
4
5
6
# 已在实验1内核编译中实践过
make mrproper
make clean
make -j6
make modules_install
make install

编写测试代码

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>

// 定义SetDebug系统调用号(需要在内核中注册)
#define SYS_SETDEBUG 447

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

int main() {
SetDebug(1); // 启用调试信息

pid_t pid = fork();

if (pid == 0) {
// 子进程
printf("子进程ID:%d\n", getpid());
} else if (pid > 0) {
// 父进程
printf("父进程ID:%d\n", getpid());
} else {
// fork失败
perror("fork");
return 1;
}

SetDebug(0); // 禁用调试信息
return 0;
}

为了测试功能,我在另一个测试代码中注释掉SetDebug函数,并同时执行两个程序。预期效果是,只有调用SetDebug函数的程序在fork期间会输出调试信息。

test1未启用打印信息,test2启用打印信息。

运行test1和test2

使用dmesg查看信息。在下图中,我们可以看到背景信息中有一行关于创建进程的信息,cmd=test2,并且与上图中的子进程和父进程PID进行比较,它们也是一致的。

dmesg打印背景信息

参考资料:

Linux内核进程 - do_fork()函数实现原理

Linux内核源代码fork解释