TLPI: Ch29

17th March 2021 at 1:50pm
The Linux Programming Interface

PDF 及笔记:

电脑上如果 PDF 不展示或者展示不正常,使用 Chrome 并安装 PDF Viewer 插件。其他情况请下载文件查看:tlpi-ch29

部分笔记

Linux 对 POSIX pthread 标准的实现

POSIX 标准定义了一套线程机制和 API 标准,称为 pthread。

Linux 实现了 pthread 标准,该实现被称为 NPTL(Native POSIX Threads Library)。旧的 Linux 版本的实现为 LinuxThreads,目前已不主流。

多线程程序的内存布局

多线程程序的内存布局

pthread API 的返回值风格

一般系统调用的 API,其返回值为 0 表示成功、-1 表示失败;具体失败原因从 errno 中取。

pthread API 不同,0 表示成功,正整数表示失败,且其值同 errno 定义的一致。

不同线程拥有自己的 errno。在 Linux 上,读取 errno 实际上是通过一段 macro 将其转化为一个函数调用(而不是读一个全局变量)。

线程间共享和不共享的数据

Besides global memory, threads also share a number of other attributes (i.e., these attributes are global to a process, rather than specific to a thread). These attributes include the following:

  • process ID and parent process ID;
  • process group ID and session ID;
  • controlling terminal;
  • process credentials (user and group IDs);
  • open file descriptors;
  • record locks created using fcntl(); z signal dispositions;
  • file system–related information: umask, current working directory, and root directory;
  • interval timers (setitimer()) and POSIX timers (timer_create());
  • System V semaphore undo (semadj) values (Section 47.8);
  • resource limits;
  • CPU time consumed (as returned by times());
  • resources consumed (as returned by getrusage()); and
  • nice value (set by setpriority() and nice()).

Among the attributes that are distinct for each thread are the following:

  • thread ID (Section 29.5);
  • signal mask;
  • thread-specific data (Section 31.3);
  • alternate signal stack (sigaltstack());
  • the errno variable;
  • floating-point environment (see fenv(3));
  • realtime scheduling policy and priority (Sections 35.2 and 35.3);
  • CPU affinity (Linux-specific, described in Section 35.4);
  • capabilities (Linux-specific, described in Chapter 39); and
  • stack (local variables and function call linkage information)

线程结束自身运行的数种方式

  • The thread’s start function performs a return specifying a return value for the thread.
  • The thread calls pthread_exit() (described below).
  • The thread is canceled using pthread_cancel() (described in Section 32.1).
  • Any of the threads calls exit(), or the main thread performs a return (in the main() function), which causes all threads in the process to terminate immediately

值得注意的是最后一种方式,无论哪个线程调用了 exit(),整个程序、所有线程都会马上结束。

编码考虑点

调用完 pthread_create() 后,操作系统并不保证马上调度到该线程运行。

线程执行完后,其返回值(一般是指针)不该指向线程的栈。因为这块内存区域在线程结束后会被 OS 回收利用。

线程间等待结束的机制

线程间等待结束和回收资源 pthread_join() 与进程间 waitpid() 不同之处:

  1. 不同线程之间是对等关系,任意线程可以调用 pthread_join() 等待其他任意进程结束;进程间则需要是父进程回收子进程
  2. 由于线程间并没有父子关系,pthread 没有提供一个可以 join 任意线程的 API(进程则有)
    • 如果可以 join 任意线程,可能会把当前进程使用的第三方库的线程也 join 了,引起混乱
    • pthread 设计上希望线程只 join 自己知道(known)的其他线程
    • 但是 pthread 提供了 condition variables 机制可以模拟这一效果

线程与进程对比

章末总结的线程与进程对比,是本章的 重点

  • 共享数据
    • 线程比进程容易;进程需要额外的 IPC 代码
    • 线程间读写数据,需要额外的同步机制,避免同时写同块数据
  • 创建和调度成本:线程低一个数量级
  • 使用第三方库的考虑:多线程时需要开发者确认使用的库是否是线程安全的;如果不是,需要以线程安全的方式调用它。多进程没有此问题
  • 线程间共享虚拟内存地址带来的问题
    • 如果一个线程出现 bug,错误地修改了另外一个线程的内存,可能引起很难定位的 bug;进程无此问题
    • 同一进程的多个线程,其所使用的虚拟内存总和不能超过单进程的限制(比如 x86-32 中的 3G);而多进程时,每个进程都能使用 3G。当然在 64 位系统普遍的今天不是什么大问题
  • 多线程在其他编程上的考虑点
    • 信号处理:很难正确的处理,因此多线程程序一般不配合使用信号机制
    • 多线程运行的代码只能来自一个可执行程序(尽管可能是不同的函数);多进程则可以运行不同程序
    • 多线程还共享了 fd、当前工作目录、user / group ID 等;视情况可能是优点也可能是缺点