我正在经历这本书,Mark Mitchell、Jeffrey Oldham 和 Alex Samuel 的《高级 Linux 编程》。这是2001年的,有点老了。但无论如何我觉得还是挺好的。
然而,我发现它与我的 Linux 在 shell 输出中生成的内容有所不同。在第 92 页(查看器中为 116),第 4.5 章 GNU/Linux 线程实现以包含以下语句的段落开始:
GNU/Linux 上的 POSIX 线程实现与许多其他类 UNIX 系统上的线程实现有一个重要的不同:在 GNU/Linux 上,线程被实现为进程。
这似乎是一个关键点,稍后将用 C 代码进行说明。书中的输出是:
main thread pid is 14608
child thread pid is 14610
在我的 Ubuntu 16.04 中它是:
main thread pid is 3615
child thread pid is 3615
ps
输出支持这一点。
我想从 2001 年到现在一定发生了一些变化。
下一页的下一个子章节,4.5.1 信号处理,建立在上一个陈述的基础上:
信号和线程之间的交互行为因类 UNIX 系统而异。在 GNU/Linux 中,行为是由线程作为进程实现的事实决定的。
看来这在本书后面的内容中会更加重要。有人可以解释一下这是怎么回事吗?
我见过这个Linux 内核线程真的是内核进程吗?,但并没有多大帮助。我很困惑。
这是 C 代码:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* thread_function (void* arg)
{
fprintf (stderr, "child thread pid is %d\n", (int) getpid ());
/* Spin forever. */
while (1);
return NULL;
}
int main ()
{
pthread_t thread;
fprintf (stderr, "main thread pid is %d\n", (int) getpid ());
pthread_create (&thread, NULL, &thread_function, NULL);
/* Spin forever. */
while (1);
return 0;
}
答案1
我认为这部分clone(2)
手册页可能会消除差异。 PID:
CLONE_THREAD(自 Linux 2.4.0-test8 起)
如果设置了 CLONE_THREAD,则子进程将被放置在与调用进程相同的线程组中。
线程组是 Linux 2.4 中添加的一项功能,用于支持共享单个 PID 的一组线程的 POSIX 线程概念。在内部,这个共享 PID 是线程组的所谓线程组标识符 (TGID)。从 Linux 2.4 开始,对 getpid(2) 的调用返回调用者的 TGID。
“线程被实现为进程”短语指的是线程在过去具有单独的 PID 的问题。基本上,Linux 最初在进程内没有线程,只有单独的进程(具有单独的 PID),这些进程可能拥有一些共享资源,例如虚拟内存或文件描述符。进程 ID (*)CLONE_THREAD
和线程 ID的分离使得 Linux 的行为看起来更像其他系统,并且从这个意义上来说更像 POSIX 要求。尽管从技术上讲,操作系统仍然没有线程和进程的单独实现。
信号处理是旧实现的另一个问题领域,这在纸@FooF指的是在他们的回答中。
正如评论中所指出的,Linux 2.4 也是在 2001 年发布的,与这本书是同一年,所以该消息没有发表也就不足为奇了。
答案2
你是对的,确实“从 2001 年至今,肯定发生了一些变化”。您正在阅读的这本书根据 Linux 上 POSIX 线程的第一个历史实现来描述世界,称为Linux线程(也可以看看维基百科页面关于该主题和 Linux并行线程(7)手册页)。
LinuxThreads 与 POSIX 标准存在一些兼容性问题 - 例如线程不共享 PID - 以及其他一些严重问题。为了修复这些缺陷,Red Hat 率先推出了另一个名为 NPTL(本机 POSIX 线程库)的实现,以添加必要的内核和用户空间库支持,以实现更好的 POSIX 合规性(从 IBM 另一个名为 NGPT 的竞争性重新实现项目中汲取了良好的部分(“下一代 Posix 线程”),参见维基百科关于 NPTL 的文章)。添加到的附加标志clone(2)
系统调用(特别CLONE_THREAD
指出@ikkkachu
他的回答)可能是内核修改中最明显的部分。用户空间部分的工作最终被合并到 GNU C 库中。
如今,一些嵌入式 Linux SDK 仍然使用旧的 LinuxThreads 实现,因为它们使用内存占用较小的 LibC 版本,称为uClibc(也称为 µClibc),并且花费了大量年的时间才将 NPTL 用户空间实现从 GNU LibC 移植过来和假定为默认的 POSIX 线程实现,因为一般来说,这些特殊平台并不努力追随闪电速度的最新时尚。通过注意到,实际上,这些平台上不同线程的 PID 与 POSIX 标准指定的不同,可以观察 LinuxThreads 实现在操作中的使用 - 就像您正在阅读的书所描述的那样。实际上,一旦您调用pthread_create()
,您突然将进程数从 1 增加到 3,因为需要额外的进程来整理混乱。
Linux并行线程(7)手册页对两者之间的差异进行了全面而有趣的概述。另一个关于差异的富有启发性的描述(尽管已经过时)是这样的纸作者:Ulrich Depper 和 Ingo Molnar 关于 NPTL 的设计。
我建议你不要太认真地对待这本书的这一部分。相反,我推荐 Butenhof 的 POSIX 线程编程以及有关该主题的 POSIX 和 Linux 手册页。关于该主题的许多教程都是不准确的。
答案3
(用户空间)线程在 Linux 上并不是作为进程来实现的,因为它们没有自己的私有地址空间,但它们仍然共享父进程的地址空间。
然而,这些线程被实现为使用内核进程计数系统,因此被分配了自己的线程ID(TID),但被赋予了与父进程相同的PID和“线程组ID”(TGID) - 这与分叉,创建新的TGID和PID,TID与PID相同。
因此,最近的内核似乎有一个可以查询的单独的 TID,这对于线程来说是不同的,在上面的每个 main() thread_function() 中显示这一点的合适代码片段是:
long tid = syscall(SYS_gettid);
printf("%ld\n", tid);
所以整个代码是:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <syscall.h>
void* thread_function (void* arg)
{
long tid = syscall(SYS_gettid);
printf("child thread TID is %ld\n", tid);
fprintf (stderr, "child thread pid is %d\n", (int) getpid ());
/* Spin forever. */
while (1);
return NULL;
}
int main ()
{
pthread_t thread;
long tid = syscall(SYS_gettid);
printf("main TID is %ld\n", tid);
fprintf (stderr, "main thread pid is %d\n", (int) getpid ());
pthread_create (&thread, NULL, &thread_function, NULL);
/* Spin forever. */
while (1);
return 0;
}
给出一个示例输出:
main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
答案4
基本上,你书中的信息在历史上是准确的,因为 Linux 上线程的实现历史非常糟糕。我对 SO 相关问题的回答也可以作为您问题的答案:
这些混乱都源于这样一个事实:内核开发人员最初持有一种非理性和错误的观点,即只要内核提供一种方法让线程共享内存和文件描述符,线程几乎可以完全在用户空间中以内核进程为原语实现。 。这导致了 POSIX 线程的 LinuxThreads 实现出了名的糟糕,这是一个用词不当,因为它没有提供任何与 POSIX 线程语义远程相似的东西。最终 LinuxThreads 被 NPTL 取代,但许多令人困惑的术语和误解仍然存在。
首先要认识到的最重要的事情是“PID”在内核空间和用户空间中意味着不同的东西。内核所谓的 PID 实际上是内核级线程 id(通常称为 TID),不要与
pthread_t
单独的标识符混淆。系统上的每个线程,无论是在同一进程还是不同进程中,都有一个唯一的 TID(或内核术语中的“PID”)。另一方面,POSIX 意义上的“进程”中的 PID 在内核中被称为“线程组 ID”或“TGID”。每个进程由一个或多个线程(内核进程)组成,每个线程都有自己的 TID(内核 PID),但都共享相同的 TGID,该 TGID 等于
main
运行的初始线程的 TID(内核 PID)。当
top
向您显示线程时,它显示的是 TID(内核 PID),而不是 PID(内核 TGID),这就是为什么每个线程都有一个单独的线程。随着 NPTL 的出现,大多数采用 PID 参数或作用于调用的系统调用过程更改为将 PID 视为 TGID 并作用于整个“线程组”(POSIX 进程)。