《操作系统导论》第27章读书笔记:插叙:线程API
—— 2024-04-21 杭州 上午
本章讲得比较啰嗦,问题是本章的二级标题后面都会作为一个章节来讲,所以本章属于概况介绍类章节,另外这几个并发的章节使用的都是是POSIX线程库…
文章目录
- 《操作系统导论》第27章读书笔记:插叙:线程API
- 0.补充笔记:POSIX详解,以及和windows、linux的关系
- 什么是POSIX?
- POSIX标准的主要组成部分
- POSIX标准的优势
- 如何与非POSIX系统兼容
- 实际应用
- 总结1
- POSIX 与 Linux 的关系
- POSIX 与 Windows 的关系
- 总结2
- 0.补充笔记:POSIX线程API列表
- 1.线程创建:pthread_create,书里对返回值和参数研究很细
- 图27.1 代码:创建线程
- 2.线程完成
- pthread_join的中文术语
- "阻塞线程"术语
- 图27.2 代码:等待线程完成,书里对返回值和参数研究很细
- 图27.3 代码:较简单的向线程传递参数示例
- pthread线程中错误地返回栈上分配的局部变量的地址
- 3.锁
- 4.条件变量
- 5.编译和运行、小结
- 6.补充笔记:pthread_create的第4个参数和pthread_join的第2个参数详解
- pthread_create 的第四个参数
- pthread_join 的第二个参数
- 示例:说明如何从具有返回值的线程函数中获取返回值
0.补充笔记:POSIX详解,以及和windows、linux的关系
什么是POSIX?
POSIX(Portable Operating System Interface)是一个操作系统接口标准,其目的是促进应用程序的可移植性。这一标准由IEEE(电气和电子工程师协会)开发,并被ISO和IEC采纳。POSIX标准定义了应用程序与操作系统之间的一组接口,包括系统调用、命令行工具和实用程序。
POSIX标准的主要组成部分
POSIX标准包括多个部分,主要可以分为以下几个领域:
- 系统接口 - 包括文件系统、进程控制、线程、同步机制等。
- Shell 和实用工具 - 定义shell行为和常见命令行工具,如
awk
、echo
、sed
等。 - 用户和组管理 - 包括对用户ID和组ID的管理。
- 程序编程接口(API) - 提供了一套标准的C库函数,用于实现系统服务和各种操作系统功能。
POSIX标准的优势
- 可移植性:程序员可以开发出可在任何支持POSIX的操作系统上运行的应用程序,这包括大多数Unix系统、Linux、BSD变体和MacOS。
- 兼容性:提供了一致的API,降低了从一个操作系统到另一个操作系统的迁移成本。
- 标准化:为操作系统设计提供了一套广泛接受的规范,有助于减少开发者在不同系统间迁移和学习的难度。
如何与非POSIX系统兼容
虽然POSIX旨在增强不同操作系统之间的兼容性,但并非所有操作系统都完全符合POSIX标准。例如,Windows就提供了自己的API,与POSIX有所不同。为了在Windows上运行POSIX兼容软件,可以使用如Cygwin这类工具,它提供了一个兼容POSIX标准的环境。
实际应用
在编写跨平台软件时,开发者通常会遵循POSIX标准,以确保软件能在不同的系统上运行。此外,操作系统开发者在设计系统时会尽量遵守POSIX标凈,以确保新系统能够运行已有的大量软件。
总结1
POSIX标准是推动操作系统之间兼容性的重要工具,它通过定义一组广泛接受的API、shell行为和工具,帮助软件开发者在不同的操作系统之间进行更轻松的迁移和开发。尽管存在一些不兼容的情况,POSIX的存在无疑为操作系统的互操作性提供了坚实的基础。
POSIX 与 Linux 的关系
POSIX(Portable Operating System Interface)标准旨在提供一个跨多种操作系统的一致的应用程序接口(API),使软件开发者能够写出可以在多个系统上运行的程序。Linux,作为一个类Unix系统,广泛地支持 POSIX 标准。这包括但不限于系统调用、命令行工具、用户和组管理等。
Linux 的设计初衷之一就是兼容 Unix,因此它实现了大量的 POSIX 接口,使得从其他 Unix-like 系统(如 BSD、Solaris)迁移到 Linux 的过程更为平滑。Linux 的核心组件,如 GNU 工具(bash、gawk 等)和其他基本工具,都遵循 POSIX 标准。因此,绝大多数遵循 POSIX 标准的软件可以在 Linux 上无缝运行,也使得 Linux 成为了众多软件开发者和系统管理员的首选平台。
POSIX 与 Windows 的关系
Windows 操作系统与 POSIX 的关系则较为复杂。Windows 并非一个基于 Unix 的操作系统,因此它的核心架构和 API 与 POSIX 标准相差较远。尽管如此,Microsoft 为了支持需要跨平台兼容性的企业用户,曾经在某些版本的 Windows 中实现了部分 POSIX 接口。
-
POSIX 子系统:在较旧的 Windows NT 和 Windows 2000 中,Microsoft 提供了一个称为 “POSIX 子系统” 的功能,这允许运行一些基于 POSIX 的应用程序。
-
Services for UNIX (SFU) 和 Subsystem for UNIX-based Applications (SUA):这些是 Microsoft 提供的更为完整的工具和子系统,旨在提高 Windows 与 UNIX 系统的兼容性。
-
Windows 10 的 Bash Shell:最近的发展是,Windows 10 引入了一个名为 “Windows Subsystem for Linux (WSL)” 的特性,它允许用户在 Windows 上直接运行 Linux 二进制可执行文件。WSL 是一个兼容层,允许在 Windows 上运行一个完整的 Linux 环境,这提高了对 POSIX 标准的支持。
总结2
-
Linux 与 POSIX:Linux 是 POSIX 标准的忠实实现者之一,它通过支持这些标准,提供了高度的兼容性和可移植性,特别是与其他 Unix-like 系统间。
-
Windows 与 POSIX:虽然 Windows 本身并不是基于 POSIX 构建的,但 Microsoft 提供了多种工具和子系统来支持 POSIX API,以便于某些需要跨平台运行的应用程序能在 Windows 上工作。最近的 WSL 功能大大提高了 Windows 支持 POSIX 应用程序的能力,使其能更好地支持 Linux 应用和工具。
这样的关系揭示了操作系统设计者如何在保持自身系统特性的同时,也尽可能地向标准靠拢,以提高软件的可移植性和操作系统的可用性。
0.补充笔记:POSIX线程API列表
这个表格并不全面,但它涵盖了大部分常用的POSIX线程API的主要功能:
函数名 | 类别 | 描述 |
---|---|---|
pthread_create | 线程管理 | 创建一个新的线程 |
pthread_exit | 线程管理 | 终止调用线程 |
pthread_join | 线程管理 | 阻塞调用线程直到指定线程结束 |
pthread_detach | 线程管理 | 将线程设置为可分离状态 |
pthread_self | 线程管理 | 获取调用线程的线程ID |
pthread_equal | 线程管理 | 比较两个线程ID是否相同 |
pthread_once | 线程管理 | 保证某个初始化代码只执行一次 |
pthread_cancel | 线程管理 | 请求取消同一进程中的另一个线程 |
pthread_setcancelstate | 线程管理 | 设置调用线程的取消状态 |
pthread_setcanceltype | 线程管理 | 设置调用线程的取消类型 |
pthread_mutex_init | 互斥锁(Mutex) | 初始化互斥锁 |
pthread_mutex_destroy | 互斥锁(Mutex) | 销毁互斥锁 |
pthread_mutex_lock | 互斥锁(Mutex) | 加锁互斥锁 |
pthread_mutex_unlock | 互斥锁(Mutex) | 解锁互斥锁 |
pthread_mutex_trylock | 互斥锁(Mutex) | 尝试加锁互斥锁,如果锁已被占用则立即返回 |
pthread_cond_init | 条件变量 | 初始化条件变量 |
pthread_cond_destroy | 条件变量 | 销毁条件变量 |
pthread_cond_wait | 条件变量 | 等待条件变量 |
pthread_cond_signal | 条件变量 | 单个唤醒等待条件变量的线程 |
pthread_cond_broadcast | 条件变量 | 唤醒所有等待条件变量的线程 |
pthread_rwlock_init | 读写锁(RW Lock) | 初始化读写锁 |
pthread_rwlock_destroy | 读写锁(RW Lock) | 销毁读写锁 |
pthread_rwlock_rdlock | 读写锁(RW Lock) | 获取读锁 |
pthread_rwlock_wrlock | 读写锁(RW Lock) | 获取写锁 |
pthread_rwlock_unlock | 读写锁(RW Lock) | 释放读或写锁 |
pthread_rwlock_tryrdlock | 读写锁(RW Lock) | 尝试获取读锁,如果锁已被占用则立即返回 |
pthread_rwlock_trywrlock | 读写锁(RW Lock) | 尝试获取写锁,如果锁已被占用则立即返回 |
pthread_key_create | 线程局部存储(TLS) | 创建一个线程特定数据键 |
pthread_key_delete | 线程局部存储(TLS) | 删除线程特定数据键 |
pthread_setspecific | 线程局部存储(TLS) | 设置与键相关的线程特定数据 |
pthread_getspecific | 线程局部存储(TLS) | 获取与键相关的线程特定数据 |
1.线程创建:pthread_create,书里对返回值和参数研究很细
图27.1 代码:创建线程
#include <pthread.h>typedef struct myarg_t {int a;int b;
} myarg_t;void *mythread(void *arg) {myarg_t *m = (myarg_t *) arg;printf("%d %d\n", m->a, m->b);return NULL;
}int main(int argc, char *argv[]) {pthread_t p;int rc;myarg_t args;args.a = 10;args.b = 20;rc = pthread_create(&p, NULL, mythread, &args);...
}
2.线程完成
pthread_join的中文术语
在中文中,pthread_join
函数通常被称为“线程等待”或“线程合并”。这个函数的作用是调用线程(即执行 pthread_join
的线程)等待指定的线程终止,如果那个线程已经终止,则 pthread_join
会立即返回。这是一个同步操作,确保线程资源的正确回收和线程结束状态的获取。
"阻塞线程"术语
,“阻塞线程”(Blocking Thread)是一个常用的术语。在多线程编程中,当一个线程因为某些条件未被满足(例如等待一个事件发生、等待I/O操作完成、或等待获取锁)而暂停执行,这种状态被称为“阻塞”。阻塞线程将不会消耗CPU资源,它会保持在等待状态直到其等待的条件被满足。
例如,在使用 pthread_join
函数时,如果被等待的线程尚未结束,那么调用 pthread_join
的线程会进入阻塞状态,直到被等待的线程结束为止。这就是一个典型的阻塞线程的场景。
图27.2 代码:等待线程完成,书里对返回值和参数研究很细
#include <stdio.h>
#include <pthread.h>
#include <assert.h>
#include <stdlib.h>typedef struct myarg_t {int a;int b;
} myarg_t;typedef struct myret_t {int x;int y;
} myret_t;void *mythread(void *arg) {myarg_t *m = (myarg_t *) arg;printf("%d %d\n", m->a, m->b);myret_t *r = Malloc(sizeof(myret_t));r->x = 1;r->y = 2;return (void *) r;
}int main(int argc, char *argv[]) {int rc;pthread_t p;myret_t *m;myarg_t args;args.a = 10;args.b = 20;Pthread_create(&p, NULL, mythread, &args);Pthread_join(p, (void **) &m);printf("returned %d %d\n", m->x, m->y);return 0;
}
图27.3 代码:较简单的向线程传递参数示例
#include <stdio.h> // 引入标准输入输出库
#include <pthread.h> // 引入 POSIX 线程库// 线程函数定义
void *mythread(void *arg) {// 将传入的void*指针转换为int类型int m = (int)arg;// 打印传入的整数printf("%d\n", m);// 将传入的整数加一后,转换为void*指针返回return (void *)(arg + 1);
}// 主函数
int main(int argc, char *argv[]) {pthread_t p; // 定义线程的IDint rc, m; // 定义接收线程返回值的变量// 创建线程,传入数字100作为参数// 这里有一个错误,函数名应该是小写的pthread_createPthread_create(&p, NULL, mythread, (void *)100);// 等待线程结束,并获取线程返回值// 这里有一个错误,函数名应该是小写的pthread_joinPthread_join(p, (void **)&m);// 打印线程返回的值printf("returned %d\n", m);return 0; // 程序正常退出
}
pthread线程中错误地返回栈上分配的局部变量的地址
图片中的文本包含了一个C语言编写的多线程示例代码,它演示了如何在pthread线程中错误地返回栈上分配的局部变量的地址。这是一个常见的错误,因为栈上的数据在函数返回后可能会被覆盖,导致未定义行为。
代码中定义了一个线程函数mythread
,它打印出传入结构体的两个成员变量的值,并试图返回一个在栈上分配的结构体变量的地址。注释中指出这样做是错误的(BAD!)。
下面是图片中代码的Markdown格式:
#include <stdio.h>
#include <pthread.h>typedef struct {int a;int b;
} myarg_t;typedef struct {int x;int y;
} myret_t;void *mythread(void *arg) {myarg_t *m = (myarg_t *) arg;printf("%d %d\n", m->a, m->b);myret_t r; // ALLOCATED ON STACK: BAD!r.x = 1;r.y = 2;return (void *) &r;
}// ... (其他代码可能在这里)int main(int argc, char *argv[]) {pthread_t p;myarg_t args = {10, 20};myret_t *result;pthread_create(&p, NULL, mythread, &args);pthread_join(p, (void **) &result);printf("%d %d\n", result->x, result->y);return 0;
}// ... (其他代码可能在这里)
这段代码试图创建一个线程,然后等待线程结束,并打印线程函数返回的结构体中的值。但是,由于返回的是栈上分配的局部变量的地址,这段代码在执行时会有问题。返回的指针可能指向不再有效的内存,因为当mythread
函数返回时,r
变量所占用的栈空间可能会被用于其他目的。
图片下方的文本警告说这是一个严重错误,并指出在这个函数返回后,r
变量的内存可能会被其他线程或函数调用覆盖。它还提到了第27.3条,这可能是某个文档或书籍中的一条规则,但没有更多的上下文信息,无法确定其具体含义。
最后,图片下方的文本建议使用gcc
编译器的-Wall
选项来编译代码,以便发现潜在的问题。-Wall
选项会启用编译器的大多数警告,帮助开发者找到可能的代码错误。
3.锁
4.条件变量
最后一点需要注意:等待线程在while循环中重新检查条件,而不是简单的if语句。在后续章节中研究条件变量时,我们会详细讨论这个问题,但是通常使用while循环是一件简单而安全的事情。虽然它重新检查了这种情况(可能会增加一点开销),但有一些pthread实现可能会错误地唤醒等待的线程。在这种情况下,没有重新检查,等待的线程会继续认为条件已经改变。因此,将唤醒视为某种事物可能已经发生变化的暗示,而不是绝对的事实,这样更安全。
5.编译和运行、小结
6.补充笔记:pthread_create的第4个参数和pthread_join的第2个参数详解
pthread_create
和 pthread_join
是 POSIX 线程(pthread)库中的两个基本函数,用于在 C 或 C++ 程序中创建和管理线程。下面我们将详细解释这两个函数中的特定参数。
pthread_create 的第四个参数
pthread_create
函数用于创建一个新的线程。其函数原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
-
第四个参数 (
void *arg
): 这是一个void
指针,用于传递给线程启动函数(start_routine)的单个参数。这允许用户向线程函数传递数据。由于它是一个类型为void *
的指针,它可以指向任何类型的数据,从而使得该函数可以接收各种类型的参数。例如,若要传递一个整数或结构体给线程,你可以传递指向这些数据的指针。在线程函数中,你需要将这个
void *
指针转换回原始类型的指针,以便正确处理数据。
pthread_join 的第二个参数
pthread_join
函数用于等待指定的线程终止。其函数原型如下:
int pthread_join(pthread_t thread, void **retval);
-
第二个参数 (
void **retval
): 这是一个指向void *
的指针,用于接收线程通过pthread_exit
调用或从线程函数返回的退出状态。如果线程成功返回并且这个参数不是NULL
,则这个位置将被更新为指向线程的返回值。这个参数给调用者提供了一种方式来获取线程结束时返回的数据,这非常有用,例如,当线程执行完成一些计算并需要返回结果时。如果不关心返回值,可以将此参数设置为
NULL
。
在使用 pthread_create
创建线程时,你可以让线程函数返回一个值,这个值可以通过 pthread_join
的第二个参数来接收。这是一个非常常见的用法,用于从线程中获取计算结果或状态。
线程函数的返回值类型是 void *
,这意味着它可以返回指向任何类型数据的指针。当线程函数退出时,它返回的指针可以通过 pthread_join
调用中的第二个参数获取。
示例:说明如何从具有返回值的线程函数中获取返回值
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>// 线程函数
void *thread_func(void *arg) {int *input = (int *)arg;int *result = malloc(sizeof(int)); // 分配内存以存储结果*result = (*input) * 2; // 计算结果,这里简单地将输入值乘以2// 直接从函数返回result,相当于pthread_exit(result);return result;
}int main() {pthread_t thread;int value = 10;// 创建线程,传递value的地址pthread_create(&thread, NULL, thread_func, &value);// 等待线程结束,并接收返回值void *retval;pthread_join(thread, &retval);// 输出线程返回的结果printf("Thread returned: %d\n", *(int *)retval);// 释放由线程分配的内存free(retval);return 0;
}
在这个例子中,thread_func
函数计算传入值的两倍,并将结果存储在动态分配的内存中。它直接从函数返回这个内存地址,这与调用 pthread_exit
并传递这个地址是等同的。
在主函数中,我们使用 pthread_join
来等待线程结束并接收它的返回值。pthread_join
的第二个参数 &retval
接收线程函数返回的指针。然后我们可以使用这个指针访问线程计算的结果,并在最后负责释放这段内存。
这种方式非常灵活,可以通过线程返回指针来传递结构体、数组或任何其他类型的数据,只要确保调用线程(这里是 main
函数中的代码)负责适当地管理这些数据(例如释放内存)。