Linux 上的线程是作为进程实现的吗?

Linux 上的线程是作为进程实现的吗?

我正在经历这本书,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 相关问题的回答也可以作为您问题的答案:

https://stackoverflow.com/questions/9154671/distinction- Between-processes-and-threads-in-linux/9154725#9154725

这些混乱都源于这样一个事实:内核开发人员最初持有一种非理性和错误的观点,即只要内核提供一种方法让线程共享内存和文件描述符,线程几乎可以完全在用户空间中以内核进程为原语实现。 。这导致了 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 进程)。

相关内容