本文是操作系统课程设计关于进程线程管理通信与同步互斥的综合实验(长文警告)

一、实验目的

  1. 以OpenEuler等Linux操作系统为实验对象,加深对Linux进程、线程概念的理解,掌握利用Linux系统调用创建、管理进程的方法,掌握利用POSIX线程(Pthread)库创建管理线程的方法,认识进程、线程并发执行的实质;
  2. 深入理解Linux内核提供的消息队列、共享内存、管道、软中断四种进程间通信机制,掌握利用系统调用实现进程间通信;
  3. 了解Pthread线程库提供的线程间通信机制,掌握使用Pthread API实现线程间通信的方法;
  4. 深入理解Linux系统提供的多种进程同步互斥机制,掌握使用信号量实现进程间的同步互斥的方法;
  5. 了解Pthread提供的线程同步互斥机制,掌握使用互斥变量和条件变量实现多线程间的同步互斥的方法

二、实验环境

硬件:

  • Intel i7 7700HQ,内存16GB

软件:

  • 虚拟机:VMware Workstation Pro 15.5.6
  • Linux系统:华为openEuler-20.03-LTS-x86_64
  • Pthread线程库,POSIX接口
  • gcc编译
  • C语言

三、实验内容

在Linux环境下,采用C/C++/Java(或其它语言)编程,完成以下实验内容。

1、第一组 进程的创建与管理

  1. 阅读Linux内核源码,分析Linux进程的组成,观察进PCB/task_struc等进程管理数据结构;

  2. 利用Linux内核提供的fork()exec()wait()等系统调用,创建管理多个进程,观察父子进程的结构和并发行为,掌握睡眠、撤销等进程控制方法;

  3. 掌握pstoppstree –hvmstatstraceltracesleep xkilljobs等命令的功能和使用方式;

2、第二组 线程的创建与管理

  1. 了解POSIX 线程标准库(Pthread线程库)定义的线程结构和提供的线程管理API;

  2. 利用Pthread线程库API,创建管理多个线程,观察线程的结构和并发执行行为;

3、第三/四组 进程/线程通信

  1. 了解Linux提供的消息队列、共享内存、管道、软中断Signal等四种通信机制,编程实现进程间通信;

  2. 线程通信

    了解Linux所支持的线程机制,在一个进程内创建多个主从线程,采用参数传递机制,实现线程间通信;

4、第五组 进程/线程同步互斥

要求:参照2019-2020学年操作系统期末考试信号量题目,设计实现三个进程/线程A、B、C,分别模拟题目所描述的生产产品A、B、C的三个worker,观察并记录进程/线程的创建和同步互斥行为。重点分析信号量设计方案是否合理、符合预期,避免设计方案导致死锁或不符合题目要求。

期末考试试题如下:

An assembly line is to produce a product C with four part As, and three part Bs. The worker of machining(加工) A and worker of machining B will produce two part As and one part B independently each time. Then the two part As or one part B will be moved to the station(工作台), which can hold at most 12 of part As and part Bs altogether. Two part As must be put onto the station simultaneously. The workers must exclusively put a part on the station or get it from the station. In addition, the worker to make C must get all part of As and Bs for one product once.

Using semaphores to coordinate the three workers who are machining part A, part B and the product C to manufacture the product without deadlock.

It is required that

(1) definition and initial value of each semaphore, and

(2) the algorithm to coordinate the production process for the three workers should be given.

  1. 进程同步互斥

    创建3个Linux进程,分别模拟生产产品A、B、C的三个worker的行为,利用Linux内核信号量,实现三者间正确的同步互斥;

  2. 线程同步互斥

    利用Pthread API,创建3个Linux线程,分别模拟生产产品A、B、C的三个worker的行为,采用Pthread提供的信号量/管程机制,现三者间正确的同步互斥;

四、实验步骤

0、实验环境配置

0-1. Linux 操作系统安装

  • 选择OpenEuler等Linux发行版本,观察所采用的内核版本,采用硬盘分区模式安装Linux系统;

  • 也可以先在本机操作系统上安装VitualBox、VMware、Virtual PC等虚拟机软件,在虚拟机之上安装Linux系统。

注意安装操作系统所需的软硬件环境和硬件配置要求。

0-2. Pthread 线程库安装

观察确认所安装的Linux发行版本带有Pthread线程库,注意:

  1. 某些版本Ubuntu Linux默认不带Pthread线程库,即使在编译的度时候 加上-lpthread也不行,man不到相关Pthread函知数。此时,需要在/usr/lib/…下导入动态库libpthread.a,具体方法可以查阅网上相关资料。
  2. 后续编程时导入头文件:#include <pthread.h>

因为我电脑早已经安装了VMware虚拟机,就不重新安装了,直接下载安装OpenEuler。

1、下载OpenEuler
  1. 进入OpenEuler下载页面,下载openEuler-20.03-LTS-x86_64-dvd.iso镜像。

    2020-06-26_111556 2020-06-26_111616
  2. 等待下载完成。

    2020-06-26_132410
2、创建虚拟机
  1. 打开VMware Workstation Pro,选择“创建新的虚拟机”。

    2020-06-26_132601 2020-06-26_112633
  2. 选择“自定义”。

    2020-06-26_112704
  3. 设置虚拟机的硬件兼容性限制,按照默认选择。

    2020-06-26_112725
  4. 选择下载好的openEuler-20.03-LTS-x86_64-dvd.iso镜像。

    2020-06-26_112750
  5. 选择Linux 操作系统。

    2020-06-26_112826
  6. 配置虚拟机的名称,位置,处理器,内存,网络连接、控制器、磁盘等。

  1. 创建完成。

    2020-06-26_113152
3、安装OpenEuler
  1. 启动虚拟机。

    2020-06-26_113202

  2. 选择第一项安装OpenEuler。

    2020-06-26_113235
  3. 等待check完毕。

    2020-06-26_113312
  4. 选择安装过程中所使用的语言。

    2020-06-26_113357
  5. 选择分区和软件模块,点击“开始安装”后,开始安装系统,在安装期间可以设置Root密码以及创建用户。

  1. 安装完成。

    2020-06-26_114538
  2. 重启后登录OpenEuler系统。

    2020-06-26_114615
  3. 创建的wxy用户以及root用户均可以成功登录,安装OpenEuler完成。

    2020-06-26_134836 2020-06-26_134905

1、第一组 进程的创建与管理

实验1-1. 进程观察

查阅相关资料,阅读Linux内核源码,分析Linux进程的组成,了解进程状态,观察进PCB/task_struc等进程管理数据结构;

实验1-2. 进程创建与管理

参照**“【实验指导】1. 进程创建及管理示例”**中的程序,结合所查阅的参考资料,利用Linux内核提供的fork()exec()wait()exit()kill()等系统调用,创建管理多个进程,观察父子进程的结构和并发行为,掌握睡眠、撤销等进程控制方法;

要求:本组实验至少用到fork()exec()wait()exit()kill()getpid()五个系统调用。

1-2-1 fork()
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
__pid_t pid;
if ((pid = fork()) > 0)
{
printf("I am the parent process.\n");
/*父进程处理过程*/
}
else if (pid == 0)
{
printf("I am the child process.\n");
/*子进程处理过程*/
}
else
{
printf("fork error\n");
exit(0);
}
}
Screenshot_20200512_200044
  • fork的返回值如果是>0,那么就是父进程,这个>0的返回值就是创建的子进程的PID;
  • fork的返回值如果是=0,那么就是子进程;
  • fork的返回值如果是<0,那么就是fork失败;
1-2-2 exec()
点击查看代码
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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
char *envp[] = {"PATH=/tmp", "USER = 1ei", "STATUS = testing", NULL};
char *argv_execv[] = {"echo", "excuted by execv", NULL};
char *argv_execvp[] = {"echo", "excuted by execvp", NULL};
char *argv_execve[] = {"env", NULL};
if (fork() == 0)
{
if (execl("/bin/echo", "echo", "executed by execl", NULL) < 0)
perror("Err on execl");
}
if (fork() == 0)
{
if (execlp("echo", "echo", "executed by execlp", NULL) < 0)
perror("Err on execlp");
}
if (fork() == 0)
{
if (execle("/usr/bin/env", "env", NULL, envp) < 0)
perror("Err on execle");
}
if (fork() == 0)
{
if (execv("/bin/echo", argv_execv) < 0)
perror("Err on execv");
}
if (fork() == 0)
{
if (execvp("echo", argv_execvp) < 0)
perror("Err on execvp");
}
if (fork() == 0)
{
if (execve("/usr/bin/env", argv_execve, envp) < 0)
perror("Err on execve");
}
}
Screenshot_20200512_200606

exec用一个指定的程序文件,重新初始化一个进程。

  • 可指定新的命令行参数和环境参数(初始化堆栈底部)。

  • exec不创建新进程,只是将当前进程重新初始化了指令段和用户数据段,堆栈段以及CPU的PC指针。

6种格式exec系统调用,exec前缀,后跟以下字母:

  • l—list,v—vector

    l与v:指定命令行参数的两种方式,l以表的形式,v要事先组织成一个指针数组。

  • e—env

    需要指定envp来初始化进程。

  • p—path

    使用环境变量PATH查找可执行文件。

1-2-3 wait()
点击查看代码
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 <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
__pid_t pid;
if ((pid = fork()) > 0)
{
int status, wait_pid;
printf("I am the parent process.\n");
wait_pid = wait(&status);
printf("pid=%d,wait_pid=%d\n", pid, wait_pid);
printf("status=%#X,WTERMSIG=%d,WEXITSTATUS=%d\n", status, WTERMSIG(status), WEXITSTATUS(status));
/*父进程处理过程*/
}
else if (pid == 0)
{
int i;
for (i = 0; i < 3; i++)
{
printf("I am the child process.\n");
sleep(1);
}
/*子进程处理过程*/
exit(5);
}
else
{
printf("fork error\n");
exit(0);
}
}
Screenshot_20200512_203430
  • 函数返回值为已终止的子进程PID。

  • status中含有子进程终止的原因。

    • TERMSIG(status)为被杀信号。
    • EXITSTATUS(status)为退出码。

子进程正常退出,且退出码为5,因此wait的status退出码刚好是5。

1-2-4 kill()
点击查看代码
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 <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
__pid_t pid;
if ((pid = fork()) > 0)
{
int status;
printf("I am the parent process.\n");
sleep(3);
kill(pid, 5);
wait(&status);
printf("%#X,%d,%d\n", status, WTERMSIG(status), WEXITSTATUS(status));
/*父进程处理过程*/
}
else if (pid == 0)
{
while (1)
{
printf("I am the child process.\n");
sleep(1);
}
/*子进程处理过程*/
exit(0);
}
else
{
printf("fork error\n");
exit(0);
}
}
Screenshot_20200512_214107

可以发现子进程被父进程杀掉了,且终止码为5,刚好wait的终止码也是5。

1-2-5 getpid()
点击查看代码
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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
__pid_t pid;
if ((pid = fork()) > 0)
{
printf("I am the parent process.\tpid=%d,child_pid=%d\n", getpid(), pid);
/*父进程处理过程*/
}
else if (pid == 0)
{
printf("I am the child process.\t\tpid=%d,ppid=%d\n", getpid(), getppid());
/*子进程处理过程*/
exit(0);
}
else
{
printf("fork error\n");
exit(0);
}
}
Screenshot_20200512_214652

可以发现,父进程打印的子进程pid和子进程的pid相等,子进程打印的父进程的pid也和父进程的pid相等。

实验1-3. 进程管理命令

了解pstoppstree –hvmstatstraceltracesleep xkilljobs等命令的功能,使用这些命令观察进程结构和行为;

1-3-1 ps

查看正在运行的进程。

Screenshot_20200512_214908
1-3-2 top

top查看进程相关信息。

Screenshot_20200512_215333
1-3-3 pstree –h

pstree查看进程树。

Screenshot_20200512_215518
1-3-4 vmstat

vmstat可以查看系统相关信息。

Screenshot_20200512_215555
1-3-5 strace

先运行这个程序(ltrace、kill、jobs也是一样):

点击查看代码
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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
__pid_t pid;
if ((pid = fork()) > 0)
{
while (1)
{
printf("I am the parent process.\tpid=%d,child_pid=%d\n", getpid(), pid);
sleep(3);
}
/*父进程处理过程*/
}
else if (pid == 0)
{
while (1)
{
printf("I am the child process.\t\tpid=%d,ppid=%d\n", getpid(), getppid());
sleep(3);
}
/*子进程处理过程*/
exit(0);
}
else
{
printf("fork error\n");
exit(0);
}
}

屏幕输出:

Screenshot_20200512_215842

strace就相当于是调试信息:

Screenshot_20200512_215937
1-3-6 ltrace

ltrace也是同理。

Screenshot_20200512_220010
1-3-7 sleep x

睡眠了3s后继续输出。

Screenshot_20200512_220200

1-3-8 kill
Screenshot_20200512_220353 Screenshot_20200512_220330

运行kill后只有父进程输出,子进程被杀掉了。

1-3-9 jobs
Screenshot_20200512_221109

jobs查看已挂载的程序,不用携带任何参数,这里查看了正在后台运行的1-2-5程序。

2、第二组 线程的创建与管理

实验2-1. Pthread线程库背景知识

了解POSIX 线程标准库(Pthread线程库)相关知识,分析Pthread线程结构,掌握所提供的线程管理API,如pthread_create(), pthread_join(), pthread_self(), pthread_detach(), pthread_exit()

实验2-2. 线程创建与管理

参照**“【实验指导】 2. 线程创建及管理程序示例”**,查阅参考资料,利用Pthread API,创建和管理线程,观察线程的结构和并发执行行为;

要求:本组实验至少用到pthread_create(), pthread_exit(),pthread_join(), pthread_self()等四个API。

2-2-1 pthread_create()

函数原型:

1
2
#include<pthread.h>
int pthread_create(pthread_t *thread,pthread_attr_t*attr,void*(*start_routine)(void *), void *arg)

函数说明:创建线程。

参数和返回值:

参数 说明
thread 该参数是指向线程标识符的指针,当线程创建成功时,用来返回创建的线程ID
attr 该参数用于指定线程的属性,NULL表示使用默认属性
start_routine 该参数为一个函数指针,指向线程创建后要调用的函数,是一个以指向void的指针作为参数和返回值的函数指针,这个被线程调用的函数也被称为线程函数
arg 指向传递给线程函数的参数,NULL代表不传参数
返回值 成功:0
出错:返回错误码
点击查看代码
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 <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void *mythread1(void *arg)
{
int i;
for (i = 0; i < 5; i++)
{
printf("I am thread 1\n");
sleep(2);
}
}

void *mythread2(void *arg)
{
int i;
for (i = 0; i < 5; i++)
{
printf("I am thread 2\n");
sleep(2);
}
}

int main(int argc, char const *argv[])
{
pthread_t id1, id2;
int res;

res = pthread_create(&id1, NULL, mythread1, NULL);
if (res)
{
printf("Create pthread error!\n");
return -1;
}

res = pthread_create(&id2, NULL, mythread2, NULL);
if (res)
{
printf("Create pthread error!\n");
return -2;
}

pthread_join(id1, NULL);
pthread_join(id2, NULL);

return 0;
}
Screenshot_20200513_085544

可以发现,创建了两个线程:thread1和thread2。两个线程分别打印自己的信息,等带2s后继续打印。

2-2-2 pthread_exit()

函数原型:

1
2
#include<pthread.h>
void pthread_exit(void *retval)

函数说明:线程退出。

参数和返回值:

参数 说明
Rretval 线程结束时的返回值,可由其它函数如pthread_join0来获取
返回值 void
点击查看代码
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void *create(void *arg)
{
printf("New thread is create...\n");
pthread_exit((void *)6);
}

int main(int argc, char const *argv[])
{
pthread_t tid;
int res;
void *temp;
res = pthread_create(&tid, NULL, create, NULL);
printf("I am the main thread!\n");
if (res)
{
printf("thread id not create...\n");
return -1;
}

res = pthread_join(tid, &temp);
if (res)
{
printf("thread is not exit...\n");
return -2;
}
printf("Thread is exit code %ld \n", (long)temp);
return 0;
}
Screenshot_20200513_093431

可以发现,子线程退出,主线程接收到了子线程的退出码6。

2-2-3 pthread_join()

函数原型:

1
2
#include<pthread.h>
int pthread_join(pthread_t thread,void **thread_return)

函数说明:线程等待。

参数和返回值:

参数 说明
thread 等待退出的线程ID
thread_return 用于定义的指针,用来存储被等待线程结束时的返回值(不为NULL时)
返回值 成功:0
出错:返回错误码
点击查看代码
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
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *thread(void *str)
{
int i;
for (i = 0; i < 4; ++i)
{
sleep(2);
printf("This is the thread:%d\n", i);
}
return NULL;
}

int main(int argc, char const *argv[])
{
pthread_t pth;
long i;
int ret = pthread_create(&pth, NULL, thread, (void *)(i));
pthread_join(pth, NULL);
printf("123\n");
for (i = 0; i < 3; ++i)
{
sleep(1);
printf("This is the main:%ld\n", i);
}
return 0;
}
Screenshot_20200513_094008

可以发现主线程在调用pthread_join后等待子线程输出完毕结束后再进行的输出,相当于是主线程被阻塞。

2-2-4 pthread_self()

函数原型:

1
2
#include<pthread.h>
Pthread_t pthread_self(void)

函数说明:获取调用线程的标识ID。

参数和返回值:

参数 说明
void
返回值 返回调用该函数的线程的标识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
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *create(void *arg)
{
printf("New thread...\n");
printf("This thread's id is %u \n", (unsigned int)pthread_self());
printf("This thread process pid is %d\n", getpid());
return NULL;
}

int main(int argc, char const *argv[])
{
pthread_t tid;
int res;
printf("Main thread is starting...\n");
res = pthread_create(&tid, NULL, create, NULL);
if (res)
{
printf("thread id not create...\n");
return -1;
}
printf("The main process pid is %d\n", getpid());
sleep(1);
return 0;
}
Screenshot_20200513_094649

可以发现,子线程成功打印了自己的线程id,并且子线程的进程pid和主线程的进场pid相等,这也说明这两个线程属于同一个进程。

2-2-5 pthread_cleanup_push()pthread_cleanup_pop()

1、函数原型:

1
2
#include<pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg)

函数说明:将清除函数压入清除栈

参数和返回值:

参数 说明
rtn 清除函数
arg 清除函数的参数
返回值 void

2、函数原型:

1
2
#include<pthread.h>
void pthread_cleanup_pop(int execute)

函数说明:将清除函数弹出清除栈

参数和返回值:

参数 说明
execute 执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数。
非0:执行;0:不执行
返回值 void
点击查看代码
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
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *clean(void *arg)
{
printf("Cleanup %s\n", (char *)arg);
return (void *)0;
}

void *thr_fn1(void *arg)
{
printf("Thread1 start\n");
pthread_cleanup_push((void *)clean, "Thread1 first handler");
pthread_cleanup_push((void *)clean, "Thread1 second handler");
printf("Thread1 push complete\n");
if (arg)
{
return ((void *)1);
}
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
return (void *)2;
}

void *thr_fn2(void *arg)
{
printf("Thread2 start\n");
pthread_cleanup_push((void *)clean, "Thread2 first handler");
pthread_cleanup_push((void *)clean, "Thread2 second handler");
printf("Thread2 push complete\n");
if (arg)
{
return ((void *)3);
}
pthread_cleanup_pop(0);
pthread_cleanup_pop(1);
return (void *)4;
}

int main(int argc, char const *argv[])
{
int res;
pthread_t tid1, tid2;
void *tret;
res = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
if (res != 0)
{
printf("Create theate error...\n");
return -1;
}
res = pthread_create(&tid2, NULL, thr_fn2, (void *)0);
if (res != 0)
{
printf("Create theate error...\n");
return -1;
}
res = pthread_join(tid1, &tret);
if (res != 0)
{
printf("Thread_join error...\n");
return -1;
}
printf("Thread1 exit code %ld\n", (long)tret);
res = pthread_join(tid2, &tret);
if (res != 0)
{
printf("Thread_join error...\n");
return -1;
}
printf("Thread2 exit code %ld\n", (long)tret);
return 0;
}
Screenshot_20200513_141113
  • 对于Thread1,传入的参数是1,因此函数thr_fn1只执行了两个pthread_cleanup_push,在pthread_cleanup_pop之前就return 1了,因此Thread1没有Cleanup输出,且返回码是1。
  • 对于Thread2,传入的参数是0,因此函数thr_fn2在执行了两个pthread_cleanup_push后又执行了两个pthread_cleanup_pop,但是第一个pthread_cleanup_pop传的参数是0,第二个pthread_cleanup_pop传的参数是1,而且这个是栈的结构,第一个出栈的“Thread2 second handler”,参数为0,不清理;第二个出栈的“Thread2 first handler”,参数为1,清理。因此Thread2只有一个Cleanup输出(Thread2 first handler),且返回码是4。
2-2-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
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#define THREAD_NUMBER 3 //线程数
#define REPEAT_NUMBER 5 //每个线程中的小任务数
#define DELAY_TIME_LEVELS 10.0 //小任务之间的最大时间间隔

void *thrd_func(void *arg)
{
long thrd_num = (long)arg;
int delay_time = 0;
int count = 0;

printf("Thread %ld is starting\n", thrd_num);
for (count = 0; count < REPEAT_NUMBER; count++)
{
delay_time = (int)(rand() * DELAY_TIME_LEVELS / (RAND_MAX)) + 1;
sleep(delay_time);
printf("\tThread %ld:job %d delay=%d\n", thrd_num, count, delay_time);
}
printf("Thread %ld finished\n", thrd_num);
pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
pthread_t thread[THREAD_NUMBER];
long no, res;
void *thrd_ret;
srand(time(NULL));
for (no = 0; no < THREAD_NUMBER; no++)
{
res = pthread_create(&thread[no], NULL, thrd_func, (void *)no);
if (res != 0)
{
printf("Create thread %d failed\n", no);
exit(res);
}
}

printf("Creating threads success\nWaiting for thread to finish...\n");
for (no = 0; no < THREAD_NUMBER; no++)
{
res = pthread_join(thread[no], &thrd_ret);
if (!res)
{
printf("Thread %d joined\n", no);
}
else
{
printf("Thread %d joined failed\n", no);
}
}

return 0;
}

Screenshot_20200513_154240

这个综合案例就是创建了3个线程,让3个线程共用同一个执行函数。每个线程都有5次循环(可以看成5个小任务,也就是5个job),每次循环之间会随机等待1~10s的时间,意义在于模拟每个任务的到达时间是随机的,并没有任何特定的规律。最后就是三个线程相继完成了各自的任务,都成功join,最后主线程结束。

3、第三组 进程间通信

了解Linux提供的消息队列(消息传递)、共享内存、管道/命名管道、信号(signal)/软中断四种进程间通信机制的实现原理和方法;

  1. 消息队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。具有一定权限的进程通过向消息队列中写入组织成消息的数据、从队列中读取数据,实现相互间通信。消息队列克服了信号signal承载信息量少,管道pipe只能承载无格式字节流以及缓冲区大小受限等缺点;
  2. 共享内存:多个进程通过访问同一块内存空间,实现快速的进程间通信是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥;
  3. 管道(Pipe)及命名管道(named pipe):用于具有亲缘关系进程间的通信,命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还支持无亲缘关系进程间的通信;
  4. 信号(Signal:也称为软中断,是一种基于事件的通信机制,用于通知接受进程有某种事件发生。除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction;

参照**“【实验指导】 6.3 进程间通信示例”**,查阅参考资料,选择上述四种通信方式中的一种,编程实现进程间通信,观察进程间通信过程。

我选择的是消息队列(消息传递)方式。

消息结构体:

1
2
3
4
5
struct my_message
{
long int message_type;
/* The data you wish to transfer*/
};

相关函数:

函数原型 说明
int msgget(key_t key, int msgflg); 创建和访问一个消息队列
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg); 将消息添加到消息队列,即消息发送
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg); 从一个消息队列获取消息,即消息接收
int msgctl(int msgid, int command, struct msgid_ds *buf); 控制消息队列

代码:

信息发送方:msgsend.c

点击查看代码
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 <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>

#define MAX_TEXT 512
struct msg_st
{
long int msg_type;
char text[MAX_TEXT];
};
int main()
{
int running = 1;
struct msg_st data;
char buffer[BUFSIZ];
int msgid = -1;

//建立消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if (msgid == -1)
{
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}

//向消息队列中写消息,直到写入end
while (running)
{
//输入数据
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
data.msg_type = 1; //注意2
strcpy(data.text, buffer);
//向队列发送数据
if (msgsnd(msgid, (void *)&data, MAX_TEXT, 0) == -1)
{
fprintf(stderr, "msgsnd failed\n");
exit(EXIT_FAILURE);
}
//输入end结束输入
if (strncmp(buffer, "end", 3) == 0)
running = 0;
sleep(1);
}
exit(EXIT_SUCCESS);
}

消息接受方:msgreceive.c

点击查看代码
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 <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>

struct msg_st
{
long int msg_type;
char text[BUFSIZ];
};

int main()
{
int running = 1;
int msgid = -1;
struct msg_st data;
long int msgtype = 0; //注意1

//建立消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if (msgid == -1)
{
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}

//从队列中获取消息,直到遇到end消息为止
while (running)
{
if (msgrcv(msgid, (void *)&data, BUFSIZ, msgtype, 0) == -1)
{
fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
exit(EXIT_FAILURE);
}
printf("You wrote: %s\n", data.text);
//遇到end结束
if (strncmp(data.text, "end", 3) == 0)
running = 0;
}
//删除消息队列
if (msgctl(msgid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "msgctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

在第一个终端中编译运行msgsend.c:

Screenshot_20200517_144833

新开一个终端编译运行msgreceive.c:

Screenshot_20200517_145014

在第一个终端的发送进程中输入,能在第二个终端的接收进程中显示:

Screenshot_20200517_145215 Screenshot_20200517_145256

在发送进程中输入end,接收进程接收到了end信号,就会删除消息队列,退出进程;发送进程也会在退出。

Screenshot_20200517_145548 Screenshot_20200517_145619

在同一个终端,发送进程后台运行也是同样的道理。但是如果开两个接收进程,一个发送进程,那么就会出现接收进程争抢使用消息队列的情况,并且每个消息只会被使用一次,不会出现被重复消费的情况。因此最后的end消息因为只会被一个接收方拿到,且将消息队列删除,发送方和接收方1正常退出,但是导致另一个接收方2找不到消息队列,出错退出。

msgsend:

Screenshot_20200517_150046

msgreceive1:

Screenshot_20200517_150252

msgreceive2:

Screenshot_20200517_150315

4、第四组 线程间通信

了解Linux中的线程概念、线程通信机制、线程间同步互斥模式,以及多线程编程方式;

参照*“【实验指导】 6.2.7 Linux c/c++线程间参数传递”**中的【示例2-3-3】~【示例2-3-5】***,编程实现线程间参数传递。

示例2-3-3 向新建的线程传递字符串

代码:

点击查看代码
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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void *create(void *arg)
{
char *str;
str = (char *)arg;
printf("The parameter passed from main is %s\n", str);
return (void *)0;
}

int main()
{
int error;
pthread_t id1;
char *str1 = "Hello!";
char *attr = str1;
error = pthread_create(&id1, NULL, create, (void *)attr);
if (error != 0)
{
printf("This pthread is not created!\n");
return -1;
}
sleep(1);
printf("pthread is created..\n");
return 0;
}

运行结果:

Screenshot_20200517_171450

可以发现Hello字符串已经传到了子线程,并在子线程打印输出。

示例2-3-4 向新建的线程传递字符串

代码:

点击查看代码
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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
struct member
{
int a;
char *s;
};
void *create(void *arg)
{
struct member *temp;
temp = (struct member *)arg;
printf("member->a = %d\n", temp->a);
printf("member->s = %s\n", temp->s);
return (void *)0;
}
int main(int argc, char const *argv[])
{
int error;
pthread_t id1;
struct member *p;
p = (struct member *)malloc(sizeof(struct member));
p->a = 1;
p->s = "Robben!";
error = pthread_create(&id1, NULL, create, (void *)p);
if (error)
{
printf("pthread is not created!\n");
return -1;
}
sleep(1);
printf("pthread is created!\n");
free(p);
p = NULL;
return 0;
}

运行结果:

Screenshot_20200517_171737

可以发现,结构体member传递到了子线程中,member中的int型a和char*型s,在子线程中被输出。

示例2-3-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
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

static int a = 5;
void *create(void *arg)
{
printf("New pthread...\n");
printf("a = %d\n", a);
a = -1;
return (void *)0;
}
int main(int argc, const char *argv[])
{
int error;
pthread_t id1;
a = 10;
error = pthread_create(&id1, NULL, create, NULL);
if (error != 0)
{
printf("new thread is not created!\n");
return -1;
}
sleep(1);
printf("a = %d\n", a);
printf("New thread is created...\n");
return 0;
}

运行结果:

Screenshot_20200517_172426

可以发现,定义了一个静态变量a=5,在主线程创建子线程之前a被赋值为10,然后主线程创建子线程,子线程读取a,读取到的是重新赋值后的a(10),因此子线程中输出a = 10。然后子线程中将a修改为-1,因为主线程等待(sleep)了1s,因此主线程拿到的a是子线程修改后的a(-1),因此主线程中输出a = -1。这也证明了新建立的线程可以共享进程中的数据,同一进程中的主线程和子线程可以共享进程中的静态数据。

5、第五组 进程/线程间同步与互斥(二选一)

  1. 针对2019-2020学年操作系统期末考试信号量题目,定义合理的锁和信号量,设计生产产品A、B、C的三个worker三个进程/线程A、B、C的业务流程和同步互斥机制。

    要求:给出具体的设计方案,并单独提交设计方案,类似期末考试答题形式;

  2. 参照**“【实验指导】 6.5 进程间同步互斥示例”**,根据上一步的设计方案,利用semget、semctl、semop等信号量原语,以进程方式编程实现该设计方案;

    观察分析程序运行结果,重点分析信号量设计方案是否合理、程序运行是否符合预期,应避免设计方案导致死锁或不符合题目要求。

  3. 参照**“【实验指导】 6.6 线程间同步互斥示例”**,根据第一步的设计方案,利用Pthread提供的pthread_mutex_init()pthread_mutex_lock()pthread_mutex_unlock()等线程同步互斥API,以线程方式编程实现该设计方案;

    观察分析程序运行结果。重点分析信号量设计方案是否合理、程序运行是否符合预期,应避免设计方案导致死锁或不符合题目要求。

要求:

  • 基于进程和基于线程的同步互斥实现方案二选一,完成其中一个即可;
  • 不论采用进程方式、还是线程方式,模拟A、B、C三个worker的三个进程/线程A、B、C应当多次循环反复执行,便于观察同步互斥效应;
  • 根据设计方案,给出程序代码,程序运行结果,结果分析。

我选择的是基于进程的同步互斥实现方案。

1、设计方案

方案1(采用二元互斥信号量+整形变量,效率比方案2高):
整型变量
1
2
3
int count_A=0; // 当前工作台中已有的A的数量
int count_B=0; // 当前工作台中已有的B的数量
int empty=12; // 工作台中空位数量,empty=12-(count_A+count_B)
信号量

经分析题目,发现共需要4个信号量,分别为:

1
2
3
4
semaphore MUTEX_STATION = 0 // 二元信号量,控制对工作台和count_A、count_B、empty的互斥访问(取/放零件)
semaphore SUSPEND_A = 1 // 控制台无足够空位时,挂起worker A
semaphore SUSPEND_B = 2 // 控制台无足够空位时,挂起worker B
semaphore SUSPEND_C = 3 // 控制台无足够零件时,挂起worker C
WorkerA
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WorkerA()
{
while (1)
{
// 生产2个A
P(MUTEX_STATION, -1); // 申请对count_A、empty和控制台的访问权
if (count_A <= 7 && empty >= 2)
{
// 同时放入工作台2个A

count_A += 2; //修改count_A
empty -= 2; //修改empty

V(MUTEX_STATION, 1); //释放对控制台、count_A和empty的访问权
V(SUSPEND_C, 1); //控制台放入新零件,解挂/唤醒worker C
}
else
{
V(sem_id, MUTEX_STATION, 1); //释放对控制台和count_A、empty的访问权
P(sem_id, SUSPEND_A, -1); //控制台无足够空位,或不允许再放入A,转入waiting态,挂起自身
}
}
}
WorkerB
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WorkerB()
{
while (1)
{
// 生产1个B
P(MUTEX_STATION, -1); // 申请对count_B、empty和控制台的访问权
if (count_B <= 7 && empty >= 1)
{
// 放入工作台1个B

count_B += 1; //修改count_B
empty -= 1; //修改empty

V(MUTEX_STATION, 1); //释放对控制台、count_B和empty的访问权
V(SUSPEND_C, 1); //控制台放入新零件,解挂/唤醒worker C
}
else
{
V(MUTEX_STATION, 1); //释放对控制台和count_B、empty的访问权
P(SUSPEND_B, -1); //控制台无足够空位,或不允许再放入B,转入waiting态,挂起自身
}
}
}
WorkerC
点击查看代码
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
WorkerC()
{
while (1)
{
P(MUTEX_STATION, -1); // 申请对count_A、count_B和控制台的访问权
// 有足够多的A、B用于装配
if (count_A >= 4 && count_B >= 3)
{
// 同时从工作台取出4个A、3个B

count_A -= 4; //修改count_A
count_B -= 3; //修改count_B
empty += 7; //修改empty

V(MUTEX_STATION, 1); //释放对控制台、count_A、count_B的控制权
V(SUSPEND_A, 1); //控制台新增空位,解挂A
V(SUSPEND_B, 1); //控制台新增空位,解挂B

// 生产1个C
}
else
{
V(MUTEX_STATION, 1); //释放对控制台、count_A、count_B的控制权
P(SUSPEND_C, -1); //控制台无足够零件,转入waiting态,挂起自身
}
}
}
编程实现
点击查看代码
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>

#define NUM_SEMAPHORE 4 // 信号量个数
// 信号量ID
#define MUTEX_STATION 0 // 二元信号量,控制对工作台和count_A、count_B、empty的互斥访问(取/放零件)
#define SUSPEND_A 1 // 控制台无足够空位时,挂起worker A
#define SUSPEND_B 2 // 控制台无足够空位时,挂起worker B
#define SUSPEND_C 3 // 控制台无足够零件时,挂起worker C

#define SEM_KEY 0x11223344 // 信号量组的KEY
#define SHM_KEY1 0x11223355 // 共享内存的KEY1
#define SHM_KEY2 0x11223366 // 共享内存的KEY2

// 颜色
#define NONE "\e[0m" // 复原
#define RED "\e[0;31m" // ERROR
#define YELLOW "\e[1;33m" // WorkerA
#define PINK "\e[1;35m" // WorkerB
#define GREEN "\e[1;32m" // WorkerC
#define CYAN "\e[0;36m" // Share Memory
#define BUF_SIZE 12

char *buf;
int *info; // 用于记录信息的3个整形变量
int i, sv, sem_id, shm_id, info_shm_id, C_num = 0;

union semun
{
int val; // SETVAL使用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使用缓存区
unsigned short *array; // GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用缓存区
};
// 对信号量做减1操作,即P(wait)
void P(int sem_id, int sem_num, int op)
{
if (op < 0)
{
struct sembuf sem_buf;
sem_buf.sem_num = sem_num;
sem_buf.sem_op = op; //P()
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) == -1)
{
perror(RED "Semaphore P" NONE);
exit(2);
}
}
else
{
perror(RED "Semaphore P" NONE);
exit(3);
}
return;
}
// 对信号量做减1操作,即V(signal)
void V(int sem_id, int sem_num, int op)
{
if (op > 0)
{
struct sembuf sem_buf;
sem_buf.sem_num = sem_num;
sem_buf.sem_op = op; //V()
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) == -1)
{
perror(RED "Semaphore V" NONE);
exit(2);
}
}
else
{
perror(RED "Semaphore V" NONE);
exit(3);
}
return;
}
// 展示共享内存
void show_shm()
{
printf(CYAN "[");
for (i = 0; i < BUF_SIZE - 1; i++)
{
printf("%c,", buf[i]);
}
printf("%c]\n\n" NONE, buf[BUF_SIZE - 1]);
}
// 创建并初始化信号量和共享内存
void create_ipc()
{
union semun arg[NUM_SEMAPHORE];
arg[MUTEX_STATION].val = 1;
arg[SUSPEND_A].val = 0;
arg[SUSPEND_B].val = 0;
arg[SUSPEND_C].val = 0;
// 创建信号量
if ((sem_id = semget(SEM_KEY, NUM_SEMAPHORE, IPC_CREAT | IPC_EXCL | 0666)) == -1)
{
perror(RED "Create Semaphores" NONE);
exit(1);
}
printf("Create Semaphores: OK\n");
printf("Sem_id = %d\n", sem_id);
// 初始化信号量
for (i = 0; i < NUM_SEMAPHORE; i++)
{
semctl(sem_id, i, SETVAL, arg[i]);
}
for (i = 0; i < NUM_SEMAPHORE; i++)
{
printf("The Sem[%d] = %d\n", i, semctl(sem_id, i, GETVAL, NULL));
}
printf("\n");
// 创建Info共享内存,用于记录信息
info_shm_id = shmget(SHM_KEY2, 3 * sizeof(int), IPC_CREAT | IPC_EXCL | 0666);
if (info_shm_id == -1)
{
perror(RED "Create Info Share Memory" NONE);
exit(1);
}
printf("Create Info Share Memory: OK\n");
// 初始化Info共享内存
info = (int *)shmat(info_shm_id, 0, 0); // 获取指向共享内存段的指针
if (info == (int *)-1)
{
perror(RED "Attach Info Share Memory" NONE);
exit(1);
}
info[0] = 0; // 当前工作台中已有的A的数量(count_A)
info[1] = 0; // 当前工作台中已有的B的数量(count_B)
info[2] = 12; // 工作台中空位数量(empty=12-(count_A+count_B))
printf("Attach Info Share Memory: OK\n");
// 展示info共享内存
printf("count_A=%d, count_B=%d, empty=%d\n\n", info[0], info[1], info[2]);
// 创建Buffer共享内存
shm_id = shmget(SHM_KEY1, BUF_SIZE * sizeof(char), IPC_CREAT | IPC_EXCL | 0666);
if (shm_id == -1)
{
perror(RED "Create Buffer Share Memory" NONE);
exit(1);
}
printf("Create Buffer Share Memory: OK\n");
// 初始化Buffer共享内存
buf = (char *)shmat(shm_id, 0, 0); // 获取指向共享内存段的指针
if (buf == (char *)-1)
{
perror(RED "Attach Buffer Share Memory" NONE);
exit(1);
}
for (i = 0; i < BUF_SIZE; i++)
{
buf[i] = ' ';
}
printf("Initialize QUEUE: OK\n");
show_shm();
}
// 删除信号量和共享内存
void remove_ipc()
{
// 删除信号量
if (semctl(sem_id, 0, IPC_RMID, 0) == -1)
perror(RED "Remove Semaphores" NONE);
else
printf("Remove Semaphores: OK\n");
// 删除共享内存
if (shmctl(shm_id, IPC_RMID, 0) == -1)
perror(RED "Remove Buffer Share Memory" NONE);
else
printf("Remove Buffer Share Memory: OK\n");
if (shmctl(info_shm_id, IPC_RMID, 0) == -1)
perror(RED "Remove Info Share Memory" NONE);
else
printf("Remove Info Share Memory: OK\n");
}

// CTRL-C回调函数
void sig_handler(int sig)
{
printf("\n");
remove_ipc();
printf("EXIT: OK\n");
}

int main(int argc, char const *argv[])
{
srand(time(NULL));
// 创建并初始化信号量和共享内存
create_ipc();
// WorkerA
if (fork() == 0)
{
while (1)
{
sleep(1 + random() % 10); // 生产2个A
P(sem_id, MUTEX_STATION, -1); // 申请对count_A、empty和控制台的访问权
// 还能再生产放置2个A,最多可放7+2=9个A,以便至少给B留下12-9=3个空位,防止工作台中没有足够多的B,导致worker C无法同时取出3个B
if (info[0] <= 7 && info[2] >= 2)
{
// 同时放入工作台2个A
for (i = 0; i < 2; i++)
{
char *c;
c = strchr(buf, ' ');
*c = 'A';
}
printf(YELLOW "Worker A puts 2 As to the station\n" NONE);
show_shm();

info[0] += 2; //修改count_A
info[2] -= 2; //修改empty

// 展示info共享内存
printf("count_A=%d, count_B=%d, empty=%d\n\n", info[0], info[1], info[2]);

V(sem_id, MUTEX_STATION, 1); //释放对控制台、count_A和empty的访问权
V(sem_id, SUSPEND_C, 1); //控制台放入新零件,解挂/唤醒worker C
}
else
{
V(sem_id, MUTEX_STATION, 1); //释放对控制台和count_A、empty的访问权
printf("Worker A suspended\n\n");
P(sem_id, SUSPEND_A, -1); //控制台无足够空位,或不允许再放入A,转入waiting态,挂起自身
}
}
}
// WorkerB
else if (fork() == 0)
{
while (1)
{
sleep(1 + random() % 10); // 生产1个B
P(sem_id, MUTEX_STATION, -1); // 申请对count_B、empty和控制台的访问权
// 还能再生产并放置1个B,最多可放7+1=8个B,以便至少给A留下12-8=4个空位,防止工作台中没有足够多的A,导致worker C无法同时取出4个A
if (info[1] <= 7 && info[2] >= 1)
{
// 放入工作台1个B
char *c;
c = strchr(buf, ' ');
*c = 'B';
printf(PINK "Worker B puts 1 B to the station\n" NONE);
show_shm();

info[1] += 1; //修改count_B
info[2] -= 1; //修改empty

// 展示info共享内存
printf("count_A=%d, count_B=%d, empty=%d\n\n", info[0], info[1], info[2]);

V(sem_id, MUTEX_STATION, 1); //释放对控制台、count_B和empty的访问权
V(sem_id, SUSPEND_C, 1); //控制台放入新零件,解挂/唤醒worker C
}
else
{
V(sem_id, MUTEX_STATION, 1); //释放对控制台和count_B、empty的访问权
printf("Worker B suspended\n\n");
P(sem_id, SUSPEND_B, -1); //控制台无足够空位,或不允许再放入B,转入waiting态,挂起自身
}
}
}
// WorkerC
else if (fork() == 0)
{
while (1)
{
P(sem_id, MUTEX_STATION, -1); // 申请对count_A、count_B和控制台的访问权
// 有足够多的A、B用于装配
if (info[0] >= 4 && info[1] >= 3)
{
// 同时从工作台取出4个A、3个B
for (i = 0; i < 4; i++)
{
char *c;
c = strchr(buf, 'A');
*c = ' ';
}
for (i = 0; i < 3; i++)
{
char *c;
c = strchr(buf, 'B');
*c = ' ';
}
printf(GREEN "Worker C gets 4 As and 3 Bs from the station\n" NONE);
show_shm();

info[0] -= 4; //修改count_A
info[1] -= 3; //修改count_B
info[2] += 7; //修改empty

// 展示info共享内存
printf("count_A=%d, count_B=%d, empty=%d\n\n", info[0], info[1], info[2]);

V(sem_id, MUTEX_STATION, 1); //释放对控制台、count_A、count_B的控制权
V(sem_id, SUSPEND_A, 1); //控制台新增空位,解挂A
V(sem_id, SUSPEND_B, 1); //控制台新增空位,解挂B

// 生产1个C
printf(GREEN "Worker C is producing C-%d......\n\n" NONE, ++C_num);
sleep(1 + random() % 10);
printf(GREEN "C-%d is produced\n\n" NONE, C_num);
}
else
{
V(sem_id, MUTEX_STATION, 1); //释放对控制台、count_A、count_B的控制权
printf("Worker C suspended\n\n");
P(sem_id, SUSPEND_C, -1); //控制台无足够零件,转入waiting态,挂起自身
}
}
}
// Main
else
{
// CTRL-C信号自行处理
signal(SIGINT, sig_handler);
wait(&sv);
wait(&sv);
wait(&sv);
printf("THANKS\n");
return 0;
}
}
方案2(采用多元信号量,效率不如方案1高):
信号量

经分析题目,发现共需要6个信号量,分别为:

1
2
3
4
5
6
semaphore MUTEX = 1;      // 用于WorkerA、WorkerB和WorkerC对Station的互斥访问
semaphore A_FULL = 0; // 用于限制WorkerC取A
semaphore A_EMPTY = 9; // 用于限制WorkerA生产A的数量
semaphore B_FULL = 0; // 用于限制WorkerC取B
semaphore B_EMPTY = 8; // 用于限制WorkerB生产B的数量
semaphore ALL_EMPTY = 12; // 用于限制整个Station的生产数量

分析:

  1. WorkerA最多生产9个A,如果生产A的个数大于9,则B的个数就会小于3,导致C永远从Station中拿不到4个A和3个B,产生死锁。但是由于WorkerA一次生产2个A,则WorkerA最多生产8(2×4)个A,因此使用信号量A_EMPTY来控制。A_EMPTY初始值可以设置为8,但是A_EMPTY初始值也可以设置为9,都是可以的。
  2. 同理,WorkerB最多生产8个B,如果生产B的个数大于8,则A的个数就会小于4,导致C永远从Station中拿不到4个A和3个B,产生死锁。因此使用信号量B_EMPTY来控制。
  3. WorkerA和WorkerB一起最多生产12个A和B,因此使用信号量B_EMPTY来控制。
  4. 需要一个互斥量MUTEX,用于WorkerA、WorkerB和WorkerC对Station的互斥访问。
WorkerA
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
WorkerA()
{
while (1)
{
// 生产2个A
P(A_EMPTY,-2);
P(ALL_EMPTY,-2);
P(MUTEX,-1);
// 同时放入工作台2个A
V(MUTEX,1);
V(A_FULL,2);
}
}
WorkerB
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
WorkerB()
{
while (1)
{
// 生产1个B
P(B_EMPTY,-1);
P(ALL_EMPTY,-1);
P(MUTEX,-1);
// 放入工作台1个B
V(MUTEX,1);
V(B_FULL,1);
}
}
WorkerC
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WorkerC()
{
while (1)
{
P(A_FULL,-4);
P(B_FULL,-3);
P(MUTEX,-1);
// 同时从工作台取出4个A、3个B
V(MUTEX,1);
V(ALL_EMPTY,7);
V(B_EMPTY,3);
V(A_EMPTY,4);
// 生产1个C
}
}
编程实现
点击查看代码
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>

#define NUM_SEMAPHORE 6 // 信号量个数
// 信号量ID
#define MUTEX 0 // 用于WorkerA、WorkerB和WorkerC对Station的互斥访问
#define A_FULL 1 // 用于限制WorkerC取A
#define A_EMPTY 2 // 用于限制WorkerA生产A的数量
#define B_FULL 3 // 用于限制WorkerC取B
#define B_EMPTY 4 // 用于限制WorkerB生产B的数量
#define ALL_EMPTY 5 // 用于限制整个Station的生产数量

#define SEM_KEY 0x11223344 // 信号量组的KEY
#define SHM_KEY 0x11223355 // 共享内存的KEY

// 颜色
#define NONE "\e[0m" // 复原
#define RED "\e[0;31m" // ERROR
#define YELLOW "\e[1;33m" // WorkerA
#define PINK "\e[1;35m" // WorkerB
#define GREEN "\e[1;32m" // WorkerC
#define CYAN "\e[0;36m" // Share Memory

#define BUF_SIZE 12 // 工作台大小

char *buf;
int i, sv, sem_id, shm_id, C_num = 0;

union semun
{
int val; // SETVAL使用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使用缓存区
unsigned short *array; // GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用缓存区
};
// 对信号量做减1操作,即P(wait)
void P(int sem_id, int sem_num, int op)
{
struct sembuf sem_buf;
sem_buf.sem_num = sem_num;
sem_buf.sem_op = op; //P()
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) == -1)
{
perror(RED "Semaphore P" NONE);
exit(2);
}
return;
}
// 对信号量做减1操作,即V(signal)
void V(int sem_id, int sem_num, int op)
{
struct sembuf sem_buf;
sem_buf.sem_num = sem_num;
sem_buf.sem_op = op; //V()
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) == -1)
{
perror(RED "Semaphore V" NONE);
exit(2);
}
return;
}
// 展示共享内存
void show_shm()
{
printf(CYAN "[");
for (i = 0; i < BUF_SIZE - 1; i++)
{
printf("%c,", buf[i]);
}
printf("%c]\n\n" NONE, buf[BUF_SIZE - 1]);
}
// 创建并初始化信号量和共享内存
void create_ipc()
{
union semun arg[NUM_SEMAPHORE];
arg[MUTEX].val = 1;
arg[A_FULL].val = 0;
arg[A_EMPTY].val = 9;
arg[B_FULL].val = 0;
arg[B_EMPTY].val = 8;
arg[ALL_EMPTY].val = 12;
// 创建信号量
if ((sem_id = semget(SEM_KEY, NUM_SEMAPHORE, IPC_CREAT | IPC_EXCL | 0666)) == -1)
{
perror(RED "Create Semaphores" NONE);
exit(1);
}
printf("Create Semaphores: OK\n");
printf("Sem_id = %d\n", sem_id);
// 初始化信号量
for (i = 0; i < NUM_SEMAPHORE; i++)
{
semctl(sem_id, i, SETVAL, arg[i]);
}
for (i = 0; i < NUM_SEMAPHORE; i++)
{
printf("The Sem[%d] = %d\n", i, semctl(sem_id, i, GETVAL, NULL));
}
// 创建共享内存
shm_id = shmget(SHM_KEY, BUF_SIZE * sizeof(char), IPC_CREAT | IPC_EXCL | 0666);
if (shm_id == -1)
{
perror(RED "Create Share Memory" NONE);
exit(1);
}
printf("Create Share Memory: OK\n");
// 初始化共享内存
buf = (char *)shmat(shm_id, 0, 0); // 获取指向共享内存段的指针
if (buf == (char *)-1)
{
perror(RED "Attach Share Memory" NONE);
exit(1);
}
for (i = 0; i < BUF_SIZE; i++)
{
buf[i] = ' ';
}
printf("Initialize QUEUE: OK\n");
show_shm();
}
// 删除信号量和共享内存
void remove_ipc()
{
// 删除信号量
if (semctl(sem_id, 0, IPC_RMID, 0) == -1)
perror(RED "Remove Semaphores" NONE);
else
printf("Remove Semaphores: OK\n");
// 删除共享内存
if (shmctl(shm_id, IPC_RMID, 0) == -1)
perror(RED "Remove Share Memory" NONE);
else
printf("Remove Share Memory: OK\n");
}

// CTRL-C回调函数
void sig_handler(int sig)
{
printf("\n");
remove_ipc();
printf("EXIT: OK\n");
}

int main(int argc, char const *argv[])
{
srand(time(NULL));
// 创建并初始化信号量和共享内存
create_ipc();
// WorkerA
if (fork() == 0)
{
while (1)
{
sleep(1 + random() % 10); // 生产2个A

P(sem_id, A_EMPTY, -2);
P(sem_id, ALL_EMPTY, -2);
P(sem_id, MUTEX, -1);

// 同时放入工作台2个A
for (i = 0; i < 2; i++)
{
char *c;
c = strchr(buf, ' ');
*c = 'A';
}
printf(YELLOW "Worker A puts 2 As to the station\n" NONE);
show_shm();

V(sem_id, MUTEX, 1);
V(sem_id, A_FULL, 2);
}
}
// WorkerB
else if (fork() == 0)
{
while (1)
{
sleep(1 + random() % 10); // 生产1个B

P(sem_id, B_EMPTY, -1);
P(sem_id, ALL_EMPTY, -1);
P(sem_id, MUTEX, -1);

// 放入工作台1个B
char *c;
c = strchr(buf, ' ');
*c = 'B';
printf(PINK "Worker B puts 1 B to the station\n" NONE);
show_shm();

V(sem_id, MUTEX, 1);
V(sem_id, B_FULL, 1);
}
}
// WorkerC
else if (fork() == 0)
{
while (1)
{
P(sem_id, A_FULL, -4);
P(sem_id, B_FULL, -3);
P(sem_id, MUTEX, -1);

// 同时从工作台取出4个A、3个B
for (i = 0; i < 4; i++)
{
char *c;
c = strchr(buf, 'A');
*c = ' ';
}
for (i = 0; i < 3; i++)
{
char *c;
c = strchr(buf, 'B');
*c = ' ';
}
printf(GREEN "Worker C gets 4 As and 3 Bs from the station\n" NONE);
show_shm();

V(sem_id, MUTEX, 1);
V(sem_id, ALL_EMPTY, 7);
V(sem_id, B_EMPTY, 3);
V(sem_id, A_EMPTY, 4);

// 生产1个C
printf(GREEN "Worker C is producing C-%d......\n\n" NONE, ++C_num);
sleep(1 + random() % 10);
printf(GREEN "C-%d is produced\n\n" NONE, C_num);
}
}
// Main
else
{
// CTRL-C信号自行处理
signal(SIGINT, sig_handler);
wait(&sv);
wait(&sv);
wait(&sv);
printf("THANKS\n");
return 0;
}
}

2、运行结果及分析

两种方案的运行情况只选择了其中一种方案的运行结果,如果工作时间一致的话,两种方案的运行情况基本一致,但是因为使用了随机工作时间,每次运行的结果可能不一样,因此只选择了其中的一次运行结果。

1、编译
Screenshot_20200528_205104
2、运行
方案一:
方案二:
3、分析

从运行结果来看,完全没有死锁现象,程序完全正常运行,输出结果也正常,创建和初始化信号量和共享内存,释放和删除信号量和共享内存都正常结束。

方案一的输出中有count_A、count_B、和empty的值,以及Worker A、Worker B和Worker C在进入阻塞状态时的输出,整个过程比较清晰。

方案二的基本过程在输出中,共享内存的状态也很清晰:

2A-B-2A-B-2A-B-C-2A-B-2A-B-2A-B-C-2A-B-2A-B-B-C-2A-B-2A-B-B-C-2A……

方案一和方案二多次运行都没有产生死锁,并且因为设置了随机等待时间模拟生产,每次的结果都有所不同,但都符合预期。但是经过分析,方案二可能会导致Worker A和Worker B都等待Worker C,但是此时工作台却还有一个空位的情况,但是并没有产生死锁,只是效率相较于方案一会低一些。

五、实验总结

这次操作系统课程设计的5组实验,虽然难度可能不是那么高,但是结合了操作系统理论课所学的知识,很有实践意义。经过这5组实验,我首先认识了华为最新的OpenEuler开源Linux操作系统,学习了Linux操作系统的安装、基本操作和使用。这次实验还加深了我对操作系统进程、线程概念和消息队列、共享内存、管道、软中断四种进程间通信机制的理解,掌握了利用Linux系统调用创建、管理进程的方法、利用POSIX线程(Pthread)库创建管理线程的方法和实现进程间通信和线程间通信的方法,深刻地认识了进程、线程并发执行的实质,并掌握了编程使用互斥变量和条件变量实现多线程间的同步互斥。

在实验的过程中,也遇到了各种各样的小问题,比如Linux的命令报错啊,编程gcc编译不通过等等,但经过上网查找资料和与同学讨论交流,都完美解决了问题,总体上看实验过程比较顺利。经过这次的课程设计,明显加深了对操作系统理论课知识的理解,并拓展了很多新的知识和内容,让我受益匪浅。

六、项目源码

本项目的源码已经存放于我的Github,欢迎下载查看:

评论




博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

载入天数...载入时分秒...