Linux中进程和线程的区别

20
在阅读了这个答案和Robert Love的《Linux内核开发》以及clone()系统调用之后,我发现在Linux中进程和线程对于内核来说(几乎)无法区分。它们之间有一些微小的差别(在引用的SO问题中被讨论为“更多共享”或“更少共享”),但我仍然有一些问题尚未得到解答。
最近我编写了一个涉及几个POSIX线程的程序,并决定对此进行实验。在创建两个线程的进程中,所有线程当然都通过pthread_self()返回唯一值,但是通过getpid()返回的值却不是唯一的。
下面是我创建的一个示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>

void* threadMethod(void* arg)
{
    int intArg = (int) *((int*) arg);

    int32_t pid = getpid();
    uint64_t pti = pthread_self();

    printf("[Thread %d] getpid() = %d\n", intArg, pid);
    printf("[Thread %d] pthread_self() = %lu\n", intArg, pti);
}

int main()
{
    pthread_t threads[2];

    int thread1 = 1;

    if ((pthread_create(&threads[0], NULL, threadMethod, (void*) &thread1))
         != 0)
    {
        fprintf(stderr, "pthread_create: error\n");
        exit(EXIT_FAILURE);
    }

    int thread2 = 2;

    if ((pthread_create(&threads[1], NULL, threadMethod, (void*) &thread2))
         != 0)
    {
        fprintf(stderr, "pthread_create: error\n");
        exit(EXIT_FAILURE);
    }

    int32_t pid = getpid();
    uint64_t pti = pthread_self();

    printf("[Process] getpid() = %d\n", pid);
    printf("[Process] pthread_self() = %lu\n", pti);

    if ((pthread_join(threads[0], NULL)) != 0)
    {
        fprintf(stderr, "Could not join thread 1\n");
        exit(EXIT_FAILURE);
    }

    if ((pthread_join(threads[1], NULL)) != 0)
    {
        fprintf(stderr, "Could not join thread 2\n");
        exit(EXIT_FAILURE);
    }

    return 0;
}

这段代码是在64位Fedora上编译的[gcc -pthread -o thread_test thread_test.c]。由于pthread_t类型来自<bits/pthreadtypes.h>,使用了64位类型,因此在32位版本上编译需要进行轻微修改。

下面是输出结果:

[bean@fedora ~]$ ./thread_test 
[Process] getpid() = 28549
[Process] pthread_self() = 140050170017568
[Thread 2] getpid() = 28549
[Thread 2] pthread_self() = 140050161620736
[Thread 1] getpid() = 28549
[Thread 1] pthread_self() = 140050170013440
[bean@fedora ~]$ 

通过在 gdb 中使用调度程序锁定,我可以保持程序及其线程活动,以便捕获 top 显示的内容。 只显示进程 的内容如下:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
28602 bean      20   0 15272 1112  820 R  0.4  0.0   0:00.63 top
 2036 bean      20   0  108m 1868 1412 S  0.0  0.0   0:00.11 bash
28547 bean      20   0  231m  16m 7676 S  0.0  0.4   0:01.56 gdb
28549 bean      20   0 22688  340  248 t  0.0  0.0   0:00.26 thread_test
28561 bean      20   0  107m 1712 1356 S  0.0  0.0   0:00.07 bash

展示线程时,显示:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
28617 bean      20   0 15272 1116  820 R 47.2  0.0   0:00.08 top
 2036 bean      20   0  108m 1868 1412 S  0.0  0.0   0:00.11 bash
28547 bean      20   0  231m  16m 7676 S  0.0  0.4   0:01.56 gdb
28549 bean      20   0 22688  340  248 t  0.0  0.0   0:00.26 thread_test
28552 bean      20   0 22688  340  248 t  0.0  0.0   0:00.00 thread_test
28553 bean      20   0 22688  340  248 t  0.0  0.0   0:00.00 thread_test
28561 bean      20   0  107m 1860 1432 S  0.0  0.0   0:00.08 bash

看起来很明显,程序或内核与进程相比有一种独特的定义线程的方式。每个线程根据top都有自己的PID - 为什么呢?


1
clone() 只是 Linux 实现线程和 fork() 的方式。重要的是,与 PID 通信将把信号传递给所有需要知道的人。如果内核为线程分配其他 ID,则这与您与进程通信的方式无关,也不影响您的业务。 - Kerrek SB
这是一个很好的链接,可以阅读一下。 - Aniket Thakur
在Linux中,进程和线程(几乎)对内核来说是无法区分的,并不完全正确。你几乎无法针对Linux内核工作的内容说出同时适用于进程和线程的事情。拥有VM视图?只有进程。可以被调度?只有线程。拥有文件描述符表?只有进程。有优先级?只有线程。等等。 - David Schwartz
3个回答

33
所有这些混淆都源于内核开发人员最初持有的一种不合理和错误的观点,即线程可以使用内核进程作为原语在用户空间中几乎完全实现,只要内核提供一种方法使它们共享内存和文件描述符。这导致了臭名昭著的LinuxThreads实现POSIX线程,但它实际上与POSIX线程语义毫不相似。最终LinuxThreads被NPTL取代,但很多混淆的术语和误解仍然存在。
首先也是最重要的一点是,"PID" 在内核空间和用户空间中有不同的含义。内核所谓的PID实际上是内核级别的线程ID(通常称为TID),而不应与pthread_t混淆,后者是一个单独的标识符。系统上的每个线程,无论是在同一进程中还是在不同的进程中,都有一个唯一的TID(或内核术语中的"PID")。
另一方面,在POSIX意义下被认为是PID的东西,在内核中被称为"线程组ID"或"TGID"。每个进程由一个或多个线程(内核进程)组成,每个线程都有自己的TID(内核PID),但所有线程共享相同的TGID,它等于运行main的初始线程的TID(内核PID)。
top显示线程时,它显示的是TID(内核PID),而不是PID(内核TGID),这就是为什么每个线程都有一个单独的TID。
随着NPTL的出现,大多数接受PID参数或在调用进程上执行操作的系统调用被更改为将PID视为TGID并对整个"线程组"(POSIX进程)执行操作。

谢谢您的回答,我会花一些时间研究您所说的内容。也许今天我发帖最大的问题(据我所知)是,“为什么其他人没有问过这个问题?”(至少没有通过易于获取的资源。)毫无疑问,对于像我这样从事多线程应用程序的人来说,这肯定是一个重要的话题。 - Doddy
1
现在(既然我们已经度过了LinuxThreads的惨败),应用程序员可以真正地使用POSIX线程来进行业务开发,而不必太担心底层发生了什么,因为一切都基本正常工作。我猜这就是为什么实现细节不再受到太多关注的原因。顺便说一下,“man 7 pthreads”中有一些关于它如何工作的基本解释。 - R.. GitHub STOP HELPING ICE
@bean - 这个问题在这里已经以许多不同的方式和角度被问过了。R 给出了一个特别好的答案,它涉及到 Linux 历史和技术层面上的几个混淆点。 - Duck
谢谢。当 top 显示线程时,它显示的是 TID(内核 PID),而不是 PID(内核 TGID),这就是为什么每个线程都有一个单独的 TID。现在还是这样吗?我输入 top -H,但无法弄清楚。 - Tim

1
想象一种“元实体”。如果该实体与其父级没有共享任何资源(地址空间、文件描述符等),则它是一个进程;如果该实体共享其父级的所有资源,则它是一个线程。甚至可以有一些介于进程和线程之间的东西(例如,某些资源共享,某些资源不共享)。查看“clone()”系统调用(例如http://linux.die.net/man/2/clone),您会发现这就是Linux在内部处理事情的方式。
现在将其隐藏在某种抽象背后,使一切看起来像进程或线程。如果抽象是完美无缺的,您永远不会知道“实体”和“进程和线程”之间的区别。然而,这种抽象并不完美——您看到的PID实际上是一个“实体ID”。

抱歉,你的回答完全没有回答我的问题。我已经看过clone()函数的手册了。正是因为进程和线程之间有抽象的区分,才使我提出这个问题。很容易就能说,因为我选择使用少于50%共享资源来调用clone(),所以我应该得到一个线程而不是一个进程,反之亦然。 - Doddy

0
在Linux上,每个线程都有一个线程ID。主线程的线程ID兼具作为进程ID的双重职责(在用户界面中相当知名)。线程ID是Linux的实现细节,与POSIX ID无关。要了解更多详情,请参考gettid系统调用(由于它是系统特定的,因此在纯Python中不可用)。

我不确定“主线程的线程ID是否兼具进程ID”的说法是否正确。当我运行上述代码时,主线程的线程ID与PID不同。 - codeforester

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接