抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

今日、海を見た。もう怖くない

操作系统原理的上机实验的报告一共有四个,其他的报告的地址是:

第一次:https://shiraha.cn/2020/class-FoDOS-experiment-1/
第二次:https://shiraha.cn/2020/class-FoDOS-experiment-2/
第三次:https://shiraha.cn/2020/class-FoDOS-experiment-3/
第四次:https://shiraha.cn/2020/class-FoDOS-experiment-4/

Front-matter

本次实验的所有源代码可以在https://dev.azure.com/Pure-Asahi/_git/2020_Spring_In_Class_Job?path=%2Fosnmb%2Fexp 查看到。

实验要求

本次上机实验的实验要求如下:

《操作系统原理》第二次上机实验

一、实验目的

  1. 理解操作系统线程的概念和应用编程过程;
  2. 理解线程的同步概念和编程;

二、实验内容

  1. 在 Ubuntu 或 Fedora 环境使用 fork 函数创建一对父子进程,分别输出各自的进程号和提示信息串。
  2. 在 Ubuntu 或 Fedora 环境使用 pthread_create 函数创建 2 个线程 A 和 B。线程 A 在屏幕上用 while 循环顺序递增地输出 1-1000 的自然数;线程 B 在屏幕上用 while 循环顺序递减地输出 1000-1 之间的自然数。为避免输出太快,每隔 0.5 秒输出一个数。
  3. 在 windows 环境下,利用高级语言编程环境(限定为 VS 环境或 VC 环境)调用 CreateThread 函数实现(2)的功能。
  4. 在 windows 环境下,利用高级语言编程环境(限定为 VS 环境或 VC 环境)调用 CreateThread 函数和相关的同步函数,模拟实现“生产者-消费者”问题。
  5. 在 windows 环境下,利用高级语言编程环境(限定为 VS 环境或 VC 环境)调用 CreateThread 函数实现“并发地画圆和画方”。圆的中心,半径,颜色,正方形的中心,边长,颜色等参数自己确定,合适就行。圆和正方形的边界上建议取 720 个点。为直观展示绘制的过程,每个点绘制后睡眠 0.2 秒~0.5 秒。
  6. 在 windows 环境下,利用高级语言编程环境(限定为 VS 环境或 VC 环境)调用 CreateThread 函数实现“文件拷贝小工具”。功能如下:
  7. 具有一个编辑框,让用户任意指定源目录或文件
  8. 具有一个编辑框,让用户任意指定目的目录或文件
  9. 具有“开始拷贝”按钮
  10. 具有“停止拷贝”按钮
  11. 具有显示拷贝进度的 label,当为目录拷贝时以文件数来统计进度,当为文件拷贝时以字节数来统计进度。

以上六个题目中,选择四个完成(1,2 中任意 1 题和 3,4,5 或 3,4,6 共计 4 道题);

我选择了 1、2、3、4、5 五个题目。

实验内容

  1. 在 Ubuntu 或 Fedora 环境使用 fork 函数创建一对父子进程,分别输出各自的进程号和提示信息串。
  2. 在 Ubuntu 或 Fedora 环境使用 pthread_create 函数创建 2 个线程 A 和 B。线程 A 在屏幕上用 while 循环顺序递增地输出 1-1000 的自然数;线程 B 在屏幕上用 while 循环顺序递减地输出 1000-1 之间的自然数。为避免输出太快,每隔 0.5 秒输出一个数。
  3. 在 windows 环境下,利用高级语言编程环境(限定为 VS 环境或 VC 环境)调用 CreateThread 函数实现(2)的功能。
  4. 在 windows 环境下,利用高级语言编程环境(限定为 VS 环境或 VC 环境)调用 CreateThread 函数和相关的同步函数,模拟实现“生产者-消费者”问题。
  5. 在 windows 环境下,利用高级语言编程环境(限定为 VS 环境或 VC 环境)调用 CreateThread 函数实现“并发地画圆和画方”。圆的中心,半径,颜色,正方形的中心,边长,颜色等参数自己确定,合适就行。圆和正方形的边界上建议取 720 个点。为直观展示绘制的过程,每个点绘制后睡眠 0.2 秒~0.5 秒。

实验过程

下面是对于每一个任务的原理和过程的简述:

父子进程

在 Linux 中,可以使用 C 语言函数式的系统调用 getpid获得当前进程的 ID,可以使用getppid获得可能存在的父亲进程 ID。为了存储这些函数返回的中间值,可能还需要引入sys/type.h

系统调用fork可以复制当前的进程并创建一个一样的进程,子进程的控制流从调用该函数的位置开始;它的返回值可能有如下三种情况:

  • 在父亲进程中,该函数返回子进程的 pid。
  • 在孩子进程中,该函数返回 0.
  • 出现错误时,该函数设置 errno 位并且返回负值。

一般来说fork函数是不会出错的,它仅会在两种情况下出错:要不是当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN;要不是系统内存不足,这时errno的值被设置为ENOMEM

正因为fork函数的返回值有着上面所说的特性,所以可以在不同的进程内使用它的返回值来判断当前所属的进程是父亲还是孩子。

这些函数定义在unistd.h头文件中;在 VC++6.0 中,可以在 process.h中找到。

这样,实验任务的核心代码就可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pid_t son = fork();

if (son)
{
auto ppid = getppid();
cout << "父親進程: pid = " << getpid() << " ppid = " << ppid << endl;
cout << "父親的兒子: pid = " << son << endl;
} else
{
auto ppid = getppid();
cout << "兒子進程: pid = " << getpid() << " ppid = " << ppid << endl;
cout << "兒子的父親: pid = " << ppid << endl;
}
cout << (son ? "父親進程" : "兒子進程") << "死了" << endl;

返回值不为 0,就是父亲进程,否则就是儿子进程。在这个实验里这样做就够了。

创建线程 - Linux

在Linux环境下创建线程使用的系统调用是pthread_create,它定义在pthread.h中;声明是下面这样子的:

1
2
3
4
5
6
int pthread_create(
pthread_t *restrict tidp,
const pthread_attr_t *restrict_attr,
void *(*start_rtn)(void *),
void *restrict arg
);

每个参数的含义是这样的:

  1. 是新创建的线程ID指向的内存单元,也就是指向线程标识符的指针;这是一个out属性,该函数为它赋值为创建的新进程的标识符;利用这个标识符可以做一些其他的事情。
  2. 用来设置线程的属性。默认值为NULL,我们一般也直接使用默认值。
  3. 新创建的程序要运行的函数首址,也就是一个函数指针。这个函数必须接受一个 void*指针作为参数,并返回一个void*作为返回值。当然,你也可以强制类型转换。
  4. 是上面那个函数运行需要的参数,当然是一个void*指针。

一般来说,调用此函数时应当避免传入会被其它线程修改的变量作为参数。一般都是先memcpy之后再将复制品的首地址作为参数传入。此外,第二个参数缺省的时候创建的线程是非分离属性的,这意味着它结束的时候,它所占用的系统资源并没有真正的释放。只有使用pthread_join返回后,或者这个线程指定为分离属性时,才可以保证线程占用的资源被释放。

此外,还可以使用pthread_join函数获得线程函数的返回值,它的声明如下:

1
int pthread_join __P (pthread_t __th, void **__thread_return);

它可以用来等待一个线程的结束并且获得返回值:第一个参数是线程标识符,也就是上面pthread_create函数通过第一个参数传出的标识符;第二个参数是线程函数的返回值指针的指针,是一个out属性。

这个函数是一个阻塞函数,如果某线程调用此函数等待另一个线程,那么该线程将持续阻塞直到被等待的线程结束并给出返回值;函数返回后,被等待线程的资源被收回,返回 0;如果此函数出错,将会返回错误号。

综上所述,本题的核心代码可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pthread_t sign_A, sign_B;
parameter para_A = {114514},
para_B = {1919810};

if (pthread_create(&sign_A, nullptr,
reinterpret_cast<func>(threadA), &para_A))
{
cerr << "綫程 A 創建失敗,程序退出……" << endl;
exit(EXIT_FAILURE);
}
if (pthread_create(&sign_B, nullptr,
reinterpret_cast<func>(threadB), &para_B))
{
cerr << "綫程 B 創建失敗,程序退出……" << endl;
exit(EXIT_FAILURE);
}

void *res_A, *res_B;
pthread_join(sign_A, &res_A);
pthread_join(sign_B, &res_B);
cout << (char*)res_A << endl;
cout << (char*)res_B << endl;
exit(EXIT_SUCCESS);

threadAthreadB并没有定义为上述参数类型的函数;但是因为参数、返回值的大小是一致的,可以安全的使用强制类型转换转为指定类型的函数。

此外,因为pthread并不是Linux系统的默认库,所以如果使用命令行编译,需要加上编译指令-lpthread来链接相关的库;如果您使用 CMake 管理您的项目,还需要在 CMakeList.txt 中加上下面的内容:

1
2
3
add_compile_options(-lpthread)
find_package(Threads REQUIRED)
target_link_libraries(main Threads::Threads)

其中,main 是你的项目目标名,可以在这个文档中找到。

创建线程 - Windows

在Windows中创建子进程需要用到CreateThread函数;在不使用CRT库的情况下,一般需要通过这个函数创建新进程;并使用WaitForMultipleObjects或者WaitForSingleObject等待线程的结束;使用GetExitCodeThread获得线程函数的返回值;并且使用CloseHandle关闭创建的线程对象。

CreateThread函数定义在processthreadsapi.h中,但是我们只需要通过引入windows.h来引入就好了;这个函数的声明式是下面这个样子的:

1
2
3
4
5
6
7
8
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);

一共接受六个参数,它们的含义如下:

  1. 表示线程内核对象的安全属性,一般使用NULL表示采用默认值:不可以被子线程继承
  2. 设置新线程的初始栈大小(B),0 表示和调用线程相同(默认1MB);因为Windows会根据需要自动延长栈空间,所以一般传入 0 就好了。
  3. 指向线程函数的指针,函数类型是 function<DWORD WINAPI (LPVOID>,多个线程可以共用一个函数;实际函数也可以不接受参数不返回,也可强制转换为LPTHREAD_START_ROUTINE类型使用。
  4. 传递给上面函数的参数,是void*类型;一般指向结构体,或者传入NULL表示不传参。
  5. 线程标志,用来控制新线程的创建:0 表示创建后立即调度,CREATE_SUSPENDED表明创建后立即挂起,直到函数ResumeThread被调用;还有一些其他的可选值。
  6. 是一个out属性,用来保存新线程的 ID。

当线程创建成功时,函数返回一个表示新线程对象的HANDLE;否则返回false。

虽然此函数有很多更推荐的替代,比如BeginThread;但是这里直接使用它就够了。

WaitForSingleObject函数用来等待线程:当指定线程(对象)处于有信号状态或者等待时间结束的状态时,此函数会返回;它的声明如下:

1
2
3
4
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);

第一个参数是指向对象的HANDLE,第二个参数是等待的时间;这个函数使得当前线程进入等待状态,直到它返回;只可以等待HANDLE指定的那个支持被通知/未通知的内核对象,等待时间由第二个参数指定,一般使用INFINITE作为等待无穷时间;但是如果等待对象永远不改变状态,那么调用线程将不会被唤醒,但是也不会浪费CPU时间。

若对象变为已通知状态,则函数返回WAIT_OBJECT_0;若等待时间超过了设定的时间,返回WAIT_TIMEOUT;若函数错误(比如将一个无效句柄传给了它),则返回WAIT_FAILED,此时可以用GetLastError获得详细信息。

WaitForMultipleObjects则和上面的函数很像,但是可以同时等待多个内核对象,它的声明如下:

1
2
3
4
5
6
DWORD WaitForMultipleObjects(
DWORD dwCount,
CONST HANDLE *phObjects,
BOOL fWaitAll,
DWORD dwMilliseconds
);

它的四个参数的含义如下:

  1. 需要让此函数等待的内核对象数量,范围在[1, MAXIMU M_WAIT_OBJECTS]内。
  2. 是内核对象HANDLE的数组,也就是数组第一个对象的指针。
  3. 表示此函数的工作方式:TRUE表示所有对象变为已通知状态后才返回;FALSE状态表示任一个对象状态变为已通知之后就返回。
  4. 等待时间,和WaitForSingleObject中的第二个参数一样。

使用的限制也和上面的函数大体相同。

GetExitCodeThread可以用于获取一个已经退出/中止的的线程对象的退出代码。比起说“获取”,“检查”应该更加合适:它不同于上面的两个等待的函数会使得调用线程进入等待状态,当它发现待检查的线程对象仍然活动的时候,它就会立即退出,而不阻塞当前进程(所以循环的调用这个函数是一种阻塞的等待)。它的声明如下:

1
2
3
4
BOOL   GetExitCodeThread (
HANDLE hThread,
LPDWORD lpExitCode
);

第一个参数是指向线程对象的HANDLE;第二个对象是一个out属性,用来储存线程结束的代码或返回值。返回值仅表示该函数是否调用成功,并不代表线程对象是否已经中止。

若检查的HANDLE代表的线程仍然在运行,那么这个函数会为第二个参数赋值STILL_ACTIVE;否则将函数的返回值赋值给它;一个危险的状况是,当函数返回值就是STILL_ACTIVE时,此函数会失效。因为LPDWORD大小有限,所以线程函数的返回值最好是一个指向结构体的指针。

最后,因为Windows中,线程执行完毕中止后线程对象仍然存在,所以需要使用CloseHandle手动关闭句柄。这个函数的声明式是这样的:

1
BOOL CloseHandle(HANDLE hObject);

参数是一个用来表示已经打开的对象(包括线程)的HANDLE;返回值仅表示函数执行成功与否,返回FALSE时可以通过GetLastError来获得错误原因。

这个函数实质的工作就是减少该内核对象的引用次数 1,而不是直接关闭对象——当一个对象引用计数为 0 或进程结束时会自动被操作系统回收,所以可能存在调用该函数后系统对象仍然存在以至于泄露的情况。

综上所述,已经理清楚在Windows中一个线程的生老病死的全过程;所以核心代码可以这么写:

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
parameter para_A = {114514}, para_B = {1919810};

HANDLE hThread[2];
DWORD exitCode[2];
DWORD threadId[2];
WINBOOL close = 0;

if (!(hThread[0] = CreateThread(
nullptr, 0, threadA,
(LPVOID)&para_A, 0,
&threadId[0]
)))
{
cerr << "進程 A 創建失敗,程序即將退出……" << endl;
exit(EXIT_FAILURE);
}
else cerr << "進程 A 創建成功,進程ID是 " << threadId[0] << endl;
if (!(hThread[1] = CreateThread(
nullptr, 0, threadB,
(LPVOID)&para_B, 0,
&threadId[1]
)))
{
cerr << "進程 B 創建失敗,程序即將退出……" << endl;
exit(EXIT_FAILURE);
}
else cerr << "進程 B 創建成功,進程ID是 " << threadId[1] << endl;
WaitForMultipleObjects(2, hThread, true, INFINITE);
GetExitCodeThread(hThread[0], &exitCode[0]);
GetExitCodeThread(hThread[1], &exitCode[1]);
if (exitCode[0] == STILL_ACTIVE ||
exitCode[1] == STILL_ACTIVE)
{
cerr << "進程仍在意料之外的運行,程序即將退出……" << endl;
exit(EXIT_FAILURE);
}
else
{
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
close = 1;
cout << (char*)exitCode[0] << endl;
cout << (char*)exitCode[1] << endl;
}

其实额外的检查内核对象状态在这里没啥意义……毕竟出了错也只能EXIT(EXIT_FAILURE)

生产者 - 消费者

关于生产者-消费者问题:又称为有限缓冲问题。两个线程,也就是生产者和消费者线程公用同一个固定大小的缓冲区;生产者会向其中放入数据,消费者会从中取出数据;需要保证的是生产者在缓冲区满后不会再继续加入数据,消费者在缓冲区空的时候不会继续取出数据,并且回避出现死锁的情况。这里我们使用信号量机制来实现这个问题的模拟。

显然,缓冲区可以看作这个问题的临界区;而Windows引入提供了专门的临界区对象:

1
2
3
4
CRITICAL_SECTION cs;				//声明临界区对象
InitializeCriticalSection(&cs); //初始化临界区对象
EnterCriticalSection(&cs); //进入临界区
LeaveCriticalSection(&cs); //离开临界区

此外,Windows还有定义了的信号量相关的函数,可用来模拟信号量完成对于临界资源的 P-V 操作:

CreateSemaphore函数可以创建一个信号量,并且返回用来代表它的HANDLE:

1
2
3
4
5
6
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
)

它接受的参数的含义是这样的:

  1. 代表了信号量的属性;设置为NULL时取默认值
  2. 信号量的初始值,要求在[0, lpMaximumCount]范围内;=0时信号量默认处于 unsignal 状态,否则为 signal 状态。
  3. 代表信号量可到达的最大值,必须为正数。
  4. 信号量的名字,是 C 字符串;字符串的长度不能超过MAX_PATH,设置为NULL表示无名信号量;如果传入的字符串已经代表了一个信号量,则直接打开它。

函数调用成功后将返回新建信号量的 HANDLE。

OpenSemaphore函数可以打开一个已经存在的信号量,它的声明如下:

1
2
3
4
5
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);

这个函数接受的参数的含义是这样的:

  1. 描述了对于信号量的访问权限;SEMAPHORE_ALL_ACCESS,表示可对信号量执行尽可能多的操作;SEMAPHORE_MODIFY_STATE,表示允许使用ReleaseSemaphore释放信号量,以修改信号量;SYNCHRONIZE,表示用等待函数异步的等待信号量变为signal状态。
  2. 表示信号量的可继承性;TRUE表示该 HANDLE 可以被继承。
  3. 信号量的名字,是 C 字符串;

由上述参数描述可以看出,这个函数只可以用来打开一个有名字的信号量。

此外,还有上面描述中提及的用于释放信号量的函数ReleaseSemaphore

1
2
3
4
5
BOOL ReleaseSemaphore(  
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);

第一个参数是要操作的信号量的 HANDLE;第二个参数是要在此信号量中释放的数量;第三个参数是一个out参数,用来存储操作前信号量的数量,如果不需要也可以传入NULL

当信号量使用完毕之后,它也要像其他的内核对象那样使用CloseHandle函数关闭 HANDLE。

综上所述,我们可以使用信号量的释放模拟问题中的V操作,使用对于信号量 HANDLE 的等待作为问题中的P操作,就可以模拟这个问题了。代码如下:

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
#include <bits/stdc++.h>

using namespace std;
constexpr int COUNT_THREAD = 5;
constexpr int COUNT_BUFFER = 10;
constexpr int WAIT_TIME = 10010;
constexpr int SLEEP_SHORT = 600;
constexpr int SLEEP_LONG = 1000;
constexpr int MAX_DEPTH = 10086;

auto createRandomMachine(int lb, int rb)
{
if (lb > rb) swap(lb, rb);
uniform_real_distribution<double> dm (lb, rb);
random_device rd;
default_random_engine rm(rd());
return [=]()mutable{return dm(rm);};
}

#ifdef WIN32
#include <windows.h>
#define sleep Sleep
#define null nullptr
#define fout cout

struct lpParam
{
int id;
int *buf, *ft, *bk;
CRITICAL_SECTION *cs;
int *flag;
};

string getNowTime()
{
SYSTEMTIME now;
GetLocalTime(&now);
string date;
date.append(to_string(now.wYear));
date.append("-");
date.append(to_string(now.wMonth));
date.append("-");
date.append(to_string(now.wDay));
return date + " " + to_string(now.wHour) + ':'
+ to_string(now.wMinute) + ':'
+ to_string(now.wSecond) + ':'
+ to_string(now.wMilliseconds);
}

DWORD WINAPI producer(LPVOID para)
{
auto p = (lpParam*) para;
int &front = *p->ft, &back = *p->bk, number, &flag = *p->flag;
HANDLE FULL = OpenSemaphore(SEMAPHORE_ALL_ACCESS, false, "full");
HANDLE EMPTY = OpenSemaphore(SEMAPHORE_ALL_ACCESS, false, "empty");
auto rm = p->id == 1 ?
createRandomMachine(1000, 1999) :
createRandomMachine(2000, 2999);
// ofstream fout;
int time = MAX_DEPTH;
while (time --)
{
number = rm();
WaitForSingleObject(EMPTY, INFINITE);
EnterCriticalSection(p->cs);
p->buf[back] = number;
fout << getNowTime() << ": 生產者 " << p->id << " 生產了物品 "
<< p->buf[back] << ",在緩衝池的 " << back << " 位置放入。" << endl;
++ flag;
back = (back + 1) % COUNT_BUFFER;
LeaveCriticalSection(p->cs);
ReleaseSemaphore(FULL, 1, null);
sleep(SLEEP_SHORT);
}
return 0;
}

DWORD WINAPI consumer(LPVOID para)
{
auto p = (lpParam*) para;
int &front = *p->ft, &back = *p->bk, &flag = *p->flag;
HANDLE FULL = OpenSemaphore(SEMAPHORE_ALL_ACCESS, false, "full");
HANDLE EMPTY = OpenSemaphore(SEMAPHORE_ALL_ACCESS, false, "empty");
// ofstream fout;
int time = MAX_DEPTH;
while (time --)
{
WaitForSingleObject(FULL, INFINITE);
EnterCriticalSection(p->cs);
++ flag;
fout << getNowTime() << ": 消费者 " << p->id << " 使用了物品 "
<< p->buf[back] << ",从緩衝池的 " << back << " 位置取出。" << endl;
front = (front + 1) % COUNT_BUFFER;
LeaveCriticalSection(p->cs);
ReleaseSemaphore(EMPTY, 1, null);
sleep(SLEEP_LONG);
}
return 0;
}
#endif

int main()
{
#ifdef WIN32
cout << "實驗: Windows 下的生產者-消費者問題" << endl;
lpParam lp[COUNT_THREAD];
CRITICAL_SECTION section;
HANDLE semaphore[2], hThread[COUNT_THREAD];
DWORD threadId[COUNT_THREAD];
int buffer[COUNT_BUFFER], ft = 0, bk = 0, flag = -1;
InitializeCriticalSection(&section);
semaphore[0] = CreateSemaphore(null, 0, COUNT_BUFFER, "full");
semaphore[1] = CreateSemaphore(null, COUNT_BUFFER, COUNT_BUFFER, "empty");
int cnt_p = 0, cnt_c = 0;
for (int i = 0; i < COUNT_THREAD; ++ i)
{
lp[i] = {++ (i >= 2 ? cnt_c : cnt_p), buffer,
&ft, &bk, &section, &flag};
auto func = i >= 2 ? consumer : producer;
hThread[i] = CreateThread
(
null, 0, func,
(LPVOID)&lp[i], 0,
&threadId[i]
);
cerr << "進程 " << i << " 已經創建,進程ID是 " << threadId[i] << endl;
}
WaitForMultipleObjects(COUNT_THREAD, hThread, true, WAIT_TIME);
cout << "模擬層數達到限制,程式即將退出……" << endl;
#else
cout << "這個程序需要在Windows環境下才可以正常運行的,請檢查運行平臺" << endl;
#endif
return 0;
}

画圆画方

根据上面的任务中了解到的 Windows 环境下创建进程所需要的知识,我们仅需要定义好画圆和画方的函数,作为进程函数传给CreateThread函数就可以了。这部分的函数我使用了 Easy X 图形库完成。它只支持在 Visual Studio / Visual C++ 环境下使用。在使用前你可能需要去 Easy X 官网上去下载它最新的安装包安装后才可以正常编译该源代码。

下面是画圆和画方的线程函数:

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
DWORD WINAPI drawSquare(LPVOID)
{
for (auto i = 0; i < 180; i++)
{
putpixel(50 + i, 50, WHITE);
Sleep(SLEEP_TIME);
}
for (auto i = 0; i < 180; i++)
{
putpixel(50 + 180, 50 + i, WHITE);
Sleep(SLEEP_TIME);
}
for (auto i = 0; i < 180; i++)
{
putpixel(50 + 180 - i, 50 + 180, WHITE);
Sleep(SLEEP_TIME);
}
for (auto i = 0; i < 180; i++)
{
putpixel(50, 50 + 180 - i, WHITE);
Sleep(SLEEP_TIME);
}
return 0;
}

DWORD WINAPI drawCircle(LPVOID)
{
auto position_x = [](int i)
{
return 350 + 100 * cos(-PI / 2 + (double)((i * PI) / 360));
};

auto position_y = [](int i)
{
return 140 + 100 * sin(-PI / 2 + (double)((i * PI) / 360));
};
for (int i = 0; i < 720; i++)
{
putpixel(position_x(i), position_y(i), WHITE);
Sleep(SLEEP_TIME);
}
return 0;
}

实验时只需要将它们分别交给两个线程执行就可以了。

实验结果

这里是任务完成后的截图或其他证明。

任务一:父子进程

2_1.png

任务二:多线程 - Linux

2_2.png

任务三:多线程 - Windows

2_3.png

任务四:生产者 - 消费者

2_4.png

任务五:画圆画方

2_5.png

体会

通过本次的操作系统原理实验,我熟悉了在 Linux 上和 Windows 上和线程相关的一些函数的使用;熟练了面向操作系统的编程,可以将一些基本的功能需求使用多线程技术实现。从同一个功能在 Linux 和 Windows 平台上实现的不同,进一步地体会了两个操作系统之间的差异。

实验的过程中也遇到了一些看起来不太容易解释的问题,这说明要想学好面向操作系统的多线程编程,仅仅了解函数的声明式是远远不够的。除了表面的 API 调用之外,还应该更多的去了解函数内部的构造以及实现原理。在处理这些问题的过程中,加深了我对这些原理的理解。

评论