操作系统实验二

实验目的

  1. 理解进程/线程的概念和应用编程过程;
  2. 理解进程/线程的同步机制和应用编程;
  3. 掌握和推广国产操作系统(推银河麒麟或优麒麟,建议)

实验内容

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

任务一:在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

编写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
#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命令手册大全

任务二:在Liunx下创建(fork)一个子进程,实验wait/exit函数

要求:

  1. 效果1:父进程不用wait函数,让父进程先于子进程结束,子进程进入死循环或较长时间的循环,观察父子进程的进程ID和父进程ID。
    1. 程序中printf各进程的进程号和父进程号。注意,父进程和子进程的输出请给出相应的提示字符串以便相互区分,后同
    2. 同时,用ps命令显示进程列表,观察指定进程的进程ID和父进程ID,和printf输出的这些ID是否一致,并解释。
  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)
{
// Child process
// 子进程进入死循环
while (1)
{
printf("子进程打印:子进程PID: %d, 父进程PID: %d\n", getpid(), getppid());
sleep(5);
}
}
else if (pid > 0)
{
// Parent process
printf("父进程打印:父进程PID:%d, 子进程PID:%d\n", getpid(), pid);
sleep(20);
}
else
{
// fork failed
printf("创建子进程失败\n");
}
}

程序运行结果图如下:

程序运行结果

父进程PID=3604,子进程PID=3605,然后父进程经历20s结束后,子进程打印自己的PID和父进程的PID,在父进程未结束的20s内,子进程打印的父进程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)
{
// Child process
printf("子进程打印:子进程PID:%d, 父进程PID:%d\n", getpid(), getppid());
sleep(5);
exit(42); // 使用 exit 返回状态值42
}
else if (pid > 0)
{
// Parent process
int status;
wait(&status); // wait for child process to finish
if (WIFEXITED(status))
{
printf("子进程结束状态:%d\n", WEXITSTATUS(status));
}
}
else
{
// fork failed
printf("创建子进程失败\n");
}

return 0;
}

程序运行的结果如下:

程序运行结果图

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

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

任务三:在Windows/Linux下,利用线程实现并发画圆画方

本任务我采用了Qt6实现了双线程画圆画方的GUI界面。在此只解释核心代码的部分:

由于Qt的绘制动作只能够在主线程中完成,所以我们画圆和方的两个线程分别用于计算圆和方的坐标点,并传给主线程,主线程完成相应的绘制动作。

项目文件目录如下:

项目文件目录

首先Qt想要绘制图形需要在MainWindow类中overwrite一下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信号(signal),把该坐标传入主线程,主线程进行处理。

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));
}

对于圆线程类,需要overwrite一下run函数,run函数在线程start的时候被调用,具体的逻辑就是隔一段时间进行process函数的调用,process函数处理点的信息然后发送给主线程。

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

主线程中需要做的是初始化一个圆线程类的实例,并connect前面提到的circlePoint信号和处理点的Slot槽。需要注意的是Slot函数是个Lambda表达式的写法,把子线程传回来的点加入圆点集中,然后update绘制。

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();
});

最后一步的处理比较的简单,我们需要启动线程,线程的启动由button“开始绘制”控制,点击则启动两个线程。

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

最终启动程序,程序运行效果图如下:

程序运行效果图

参考资料:

Qt中多线程写法一(步骤讲解+代码+加演示)

Qt Documentation

任务四:在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("Producer %d produced %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("Consumer %d consumed %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("Producer %d produced %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("Consumer %d consumed %d\n", id, product);

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

程序运行结果如下:

程序运行结果

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

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

WCHAN解释

参考资料:

线程的同步问题–生产者 消费者

任务五:在Linux下利用信号机制(signal)实现进程通信

任务要求:

  1. 父进程创建(fork)子进程,并让子进程进入死循环。
  2. 子进程每隔2秒输出“I am Child Process, alive !\n”
  3. 父进程询问用户“To terminate Child Process. Yes or No? \n”要求用户从键盘回答Y或N.若用户回答N,延迟2秒后再提问.
  4. 若用户回答Y,向子进程发送用户信号,让子进程结束。
  5. 子进程结束之前打印字符串:”Bye,Wolrd !\n”
  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进程间通信第三讲 信号signal kill

任务六:在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;
}

非死锁程序运行结果:

非死锁程序运行结果

这里我有意通过哲学家拿到左手的筷子后sleep一个较长的时间,让他们产生同时拿到左手筷子的冲突,通过图可以看到,5个哲学家的确同时拿到了左手边的筷子,与死锁写法不同的是,途中哲学家4主动放下了左边的筷子,重新进入思考状态,避免了死锁的状态。

参考资料:

哲学家问题

五个哲学家就餐问题

任务七:研读Linux内核并用printk调试进程创建和调度策略的相关信息

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

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

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

温馨提示:do_fork函数再较新版本的linux内核源码中已被kernel_clone函数代替。

修改./kernel/fork.c文件

在kernel_clone中使用printk,打印相关信息,p是指向task_struct结构体的指针,comm即当前执行的command,pid是子进程的进程号,current->pid是父进程的进程号,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
2
3
4
5
6
# 实验一编译内核中已经实践过
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 解读