教材为《操作系统概念》原书第九版,中文版,答案仅供参考。
4.3 在同一进程的多线程之间,下列哪些程序状态部分会被共享?(a.寄存器值 b.堆内存 c.全局变量 d.堆栈内存)
答:堆内存(b)和全局变量©是会被共享的。
-
堆内存:堆是进程的全局内存空间,所有线程都可以访问。当一个线程在堆上分配内存(如使用
malloc()
或new()
)时,这块内存对进程中的所有其他线程都是可见的,正因如此,同一进程的多线程之间可以通过在堆上创建数据结构并共享对这些结构的引用来共享信息。 -
全局变量:全局变量存储在堆或静态内存中,这些内存区域对进程中的所有线程都是可见的。因此,一个线程可以读取和修改全局变量的值,这些更改对进程中的所有其他线程都是可见的。
-
寄存器值:每个线程都有自己的执行路径和函数调用栈,因此它们需要自己的程序计数器和堆栈指针,如程序计数器(PC)、堆栈指针(SP)和其他通用寄存器,而这些寄存器的值都是线程私有的,不会被其他线程共享。
-
堆栈内存:每个线程都有自己的函数调用栈,每个线程都需要自己专属的堆栈,用于存储局部变量和函数调用的返回地址。这些数据是线程私有的,不会被其他线程共享。
4.5 第3章讨论了 Google 的 Chrome 浏览器,以及在单独进程中打开每个新网站的做法。如果 Chrome 设计成在单独线程中打开每个新网站,那么会有同样的好处么?请解释。
答:首先,我们知道,Chrome 在单独进程中打开每个新网站,即使用多进程模型
。它的优点在于,第一点,网站彼此独立运行,如果有一个网站崩溃,只有它的渲染进程受到影响,所有其他进程仍然安然无恙;此外,渲染进程在沙箱 (sandbox) 中运行,这意味着访问磁盘和网络 I/O 是受限制的,进而最大限度地减少任何安全漏洞的影响。
接着我们来讨论一下在单独线程中打开每个新网站的多线程模型
。
- 多线程模型的优点很明显,就是可以充分利用多核处理器,操作系统可以在不同的CPU核心上调度不同的线程,所以即使某个网站的线程使用了大量的CPU资源,也不会影响到其他网站的线程。
- 同时,这种模型的缺点也很明显。在多线程模型中,所有线程共享同一进程的地址空间。这意味着,如果一个线程崩溃,它可能会影响到同一进程中的其他线程。相比之下,多进程模型中的每个进程都有自己的地址空间,所以一个进程的崩溃不会影响到其他进程。在安全方面,由于多线程模型中的所有线程都共享同一进程的地址空间,一个线程可能能够读取或修改其他线程的数据,很容易受到安全漏洞的影响。而多进程模型中的每个进程都有自己的地址空间,所以一个进程不能直接访问其他进程的数据,这提供了更好的安全性。
4.9 具有2个双核处理器的系统有4个处理核可用于调度。这个系统有一个 CPU密集型
应用程序运行。在程序启动时,所有输入通过打开一个文件而读入。同样,在程序终止之前,所有程序输出结果,都写入一个文件。在程序启动和终止之间,该程序为 CPU密集型
的。你的任务是通过多线程技术来提高这个应用程序的性能。这个应用程序运行在采用一对一线程模型的系统(每个用户线程映射到一个内核线程)。
答:对于执行输入和输出,我会创建2个线程,一个用于读取输入,另一个用于写入输出。因为输入和输出通常由 I/O 子系统处理,I/O操作通常是阻塞的,这意味着在等待数据读入或写出时,线程将被阻塞,无法进行其他操作。因此,我们通常会为每个I/O操作创建一个单独的线程,这样即使一个线程被阻塞,其他线程仍然可以继续执行。在这个案例中,我们有两个I/O操作:一个用于读入输入,另一个用于写出输出。因此,我们需要创建2个线程来执行输入和输出。而为什么不采用更多的线程来执行呢?因为 I/O 这个子系统在大多数现代计算机系统中只能顺序执行。因此,即使创建更多的线程,也不会提高 I/O 操作的性能。
答:对于应用程序的 CPU 密集型部分,我会创建4个线程,每个线程对应一个处理核。创建过多的线程并不能提高性能,反而可能会因为线程切换的开销而降低性能。理想的情况是,在一对一线程模型中,每个用户线程都映射到一个内核线程,所以我们应该创建与处理器核心数相同的线程数,这样可以最大限度地利用多核处理器的并行计算能力。系统有4个处理核,所以应该创建4个线程来执行CPU密集型的部分。
4.10 考虑下面的代码段:
pid_t pid;
pid = fork();
if (pid == 0) { /* child process */
fork();
thread_create(...);
}
fork();
a. 创建了多少个单独进程?
答:首先,fork()
被调用一次,创建了一个子进程。在子进程中,fork()
再次被调用,创建了另一个子进程。在父进程和两个子进程中,fork()
都会再次被调用,每次都会创建一个新的进程。所以,总共创建了6个进程:原始进程,第一次fork()
创建的子进程,第二次fork()
创建的子进程,以及这三个进程各自通过最后一次fork()
创建的子进程。
b. 创建了多少个单独线程?
答:在第一次fork()
创建的子进程中,thread_create(...)
被调用,创建了一个新线程。然后在第二次fork()
创建的子进程中,thread_create(...)
再次被调用,创建了另一个新线程。所以,总共创建了2个线程。
注:这个题其实我不是很确定,所以去验证了一下(感谢大佬无私提供帮助),结果如下:
由此可以看出,确实是创建了6个单独进程和2个单独线程。
4.15 修改第3章的编程题3.13 (这是一个 pid 管理器的设计问题)。这个修改包括编写一个多线程程序,以测试你的习题3.13的解决方案。你将创建多个线程,如100个,每个线程会请求一个 pid ,睡眠一个随机时间,再释放 pid。(睡眠一个随机时间近似典型的 pid 使用:每个进程会分配到一个 pid,进程运行再终止,当进程终止时会释放 pid。)对于UNIX和Linux系统,睡眠是通过函数sleep()
来完成的,其参数为用整数表示的睡眠秒数。这个问题将在第6章再次修改。
根据3.13的代码进行了一定的修改,主要修改如下:
- 创建了多个线程来测试 pid 管理器。在修改后的代码中,在
main()
函数中创建了 100 个线程,每个线程都会尝试分配一个 pid,然后休眠一段时间,最后释放 pid。这模拟了典型应用程序中 pid 的使用方式,其中进程会分配 pid、执行、终止,释放 pid。 - 添加了对随机休眠时间的支持。在修改后的代码中,每个线程在分配 pid 后都会休眠一段时间,该时间是通过
rand() % 10
生成的随机数。这模拟了进程执行时间可能存在差异的实际情况。
除了这两个主要修改之外,我还对之前的代码进行了一些小调整,以提高可读性和可维护性。例如,我将 allocate_map
、allocate_pid
和 release_pid
函数声明为 static
,这样它们只能在该文件中使用。我还将 cleanup
函数声明为 static
,这样它只能在该文件中使用,并且在程序退出时自动调用。
刚开始时我的代码出现了以下报错:
pid_management.c:65:9: warning: implicit declaration of function ‘sleep’ [-Wimplicit-function-declaration]
65 | sleep(rand() % 10); // Simulate process execution
| ^~~~~
通过搜索,我发现,我遇到的警告是因为 sleep()
函数没有在代码中声明。要解决此警告,需要在代码中包含unistd.h
头文件包含 sleep
函数的声明。或者改写成如下形式:
#include <stdlib.h>
...
sleep(rand() % 10);
我采取了第一种方法。以下是正确运行的代码:
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <pthread.h>
#include <unistd.h>
#define MIN_PID 300
#define MAX_PID 5000
#define PID_COUNT (MAX_PID - MIN_PID + 1)
typedef struct{
pthread_mutex_t mutex;
unsigned char *pid_map;
} pid_manager;
pid_manager * manager;
int allocate_map(void){
manager = (pid_manager *)malloc(sizeof(pid_manager));
if (manager == NULL){
return -1;
} // Allocation failed
manager->pid_map = (unsigned char *)calloc(PID_COUNT / CHAR_BIT, sizeof(unsigned char));
if (manager->pid_map == NULL){
free(manager);
return -1; // Allocation failed
}
pthread_mutex_init(&(manager->mutex), NULL);
return 1; // Success
}
int allocate_pid(void){
pthread_mutex_lock(&(manager->mutex));
for (int i = 0; i < PID_COUNT; ++i){
int byte_index = i / CHAR_BIT;
int bit_index = i % CHAR_BIT;
if (!(manager->pid_map[byte_index] & (1 << bit_index))){
manager->pid_map[byte_index] |= (1 << bit_index);
pthread_mutex_unlock(&(manager->mutex));
return i + MIN_PID;
}
}
// No available pid
pthread_mutex_unlock(&(manager->mutex));
return -1;
}
void release_pid(int pid){
pthread_mutex_lock(&(manager->mutex));
int index = pid - MIN_PID;
int byte_index = index / CHAR_BIT;
int bit_index = index % CHAR_BIT;
manager->pid_map[byte_index] &= ~(1 << bit_index);
pthread_mutex_unlock(&(manager->mutex));
}
void cleanup(void){
pthread_mutex_destroy(&(manager->mutex));
free(manager->pid_map);
free(manager);
}
void *thread_function(void *arg){
int pid = allocate_pid();
if (pid != -1){
printf("Thread %ld allocated PID %d\n", pthread_self(), pid);
sleep(rand() % 10); // Simulate process execution
release_pid(pid);
printf("Thread %ld released PID %d\n", pthread_self(), pid);
}
else{
printf("Thread %ld failed to allocate a PID\n", pthread_self());
}
return NULL;
}
int main(void)
{
if (allocate_map() == -1){
printf("Failed to allocate pid map.\n");
return 1;
}
pthread_t threads[100];
for (int i = 0; i < 100; ++i){
pthread_create(&threads[i], NULL, thread_function, NULL);
}
for (int i = 0; i < 100; ++i){
pthread_join(threads[i], NULL);
}
cleanup();
return 0;
}
4.17 计算π的一个有趣方法是,使用一个称为Monte Carlo的技术,这种技术涉及随机。该技术工作如下:假设有一个圆,它内嵌一个正方形,如图4-18所示。(假设这个圆的半径为1。)首先, 通过 (x, y) 坐标生成一系列的随机点。这些点应在正方形内。在这些随机产生的点中,有的会落在圆内。接着,根据下面公式,估算π:
π = 4 × (圆内的点数) / (总的点数) \pi =4 \times(圆内的点数)/(总的点数) π=4×(圆内的点数)/(总的点数)
编写一个多线程版的这个算法,它创建一个单独线程以产生一组随机点。该线程计算圈内点的数量,并将结果存储到一个全局变量。当这个线程退出时,父线程将计算并输出π的估计值。用生成的随机点的数量做试验还是值得的。作为一般性规律,点的数量越大,也就越接近π。
在与本书配套的可下载的源代码中,我们提供了一个示例程序,它提供了一种技术,用于产生随机数并判定随机点 (x,y) 是否落在圆内。
对采用Monte Carlo方法估算π的细节有兴趣的读者,可参考本章末尾的参考文献。在第6章,我们使用那章的相关材料来修改这个习题。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int circle_points = 0;
// 生成随机点
void generate_random_points(int n) {
for (int i = 0; i < n; i++) {
// 生成随机数
double x = rand() / (RAND_MAX + 1.0) * 2.0 - 1.0;
double y = rand() / (RAND_MAX + 1.0) * 2.0 - 1.0;
if (x * x + y * y <= 1.0) {
circle_points++;
}
}
}
// 线程函数
void *thread_function(void *arg) {
int n = *(int *)arg;
generate_random_points(n);
return NULL;
}
int main()
{
int n = 10000;
pthread_t thread;
pthread_create(&thread, NULL, thread_function, &n);
pthread_join(thread, NULL); // 等待线程退出
double pi = 4 * circle_points / (double)n;
printf("π estimated: %f\n", pi);
return 0;
}
4.18 重复习题4.17,但不是使用一个单独线程来生成随机点,而是采用OpenMP并行化点的生成。 注意:不要把π的计算放在并行区域,因为你只要计算π一次。
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
int circle_points = 0;
void generate_random_points(int n) {
int i;
// OpenMP
#pragma omp taskloop
for (i = 0; i < n; i++) {
double x = rand() / (RAND_MAX + 1.0) * 2.0 - 1.0;
double y = rand() / (RAND_MAX + 1.0) * 2.0 - 1.0;
if (x * x + y * y <= 1.0) {
#pragma omp atomic
circle_points++;
}
}
}
int main()
{
int n = 10000;
// 生成随机点
#pragma omp parallel
{
#pragma omp single
generate_random_points(n);
}
double pi = 4 * circle_points / (double)n;
printf("π estimated: %f\n", pi);
return 0;
}
代码思路:
- 使用 OpenMP 任务化
generate_random_points
函数来生成随机点。 - 主线程使用
#pragma omp parallel
和#pragma omp single
指令来确保只有一个线程执行generate_random_points
函数。
与3.17代码的比较:
- 该代码利用
OpenMP
任务化硬件化了随机点的生成,提高了计算效率。 - 使用
#pragma omp single
指令确保只有一个线程执行generate_random_points()
函数,避免了重复计算π。
因为此处用到了 OpenMP
任务化,在此简单记下笔记:
#pragma omp task
: 标记一个任务的开始。#pragma omp taskloop
: 标记一个任务循环。#pragma omp taskwait
:等待所有任务完成。#pragma omp taskyield
: 将当前任务让位于其他任务。
任务化示例:
#include <stdio.h>
#include <omp.h>
void task1() {
// 任务 1 的代码
}
void task2() {
// 任务 2 的代码
}
int main() {
#pragma omp parallel
{
/* 上面这个括号千万不能在上一行(和#pragma omp parallel同行)用!不然会报错:
omp_pi.c:33:43: error: ‘n’ undeclared here (not in a function)
33 | double pi = 4 * circle_points / (double)n;
| ^
omp_pi.c:34:10: error: expected declaration specifiers or ‘...’ before string constant
34 | printf("π estimated: %f\n", pi);
| ^~~~~~~~~~~~~~~~~~~
omp_pi.c:34:31: error: expected declaration specifiers or ‘...’ before ‘pi’
34 | printf("π estimated: %f\n", pi);
| ^~
omp_pi.c:36:3: error: expected identifier or ‘(’ before ‘return’
36 | return 0;
| ^~~~~~
omp_pi.c:37:1: error: expected identifier or ‘(’ before ‘}’ token
37 | }
| ^
*/
#pragma omp task
task1();
#pragma omp task
task2();
#pragma omp taskwait
}
return 0;
}