线程是否在 Linux 上实现为进程?

74 linux process c thread linux-kernel

我正在阅读这本书,Mark Mitchell、Jeffrey Oldham 和 Alex Samuel 合着的Advanced Linux Programming。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
Run Code Online (Sandbox Code Playgroud)

在我的 Ubuntu 16.04 中,它是:

main thread pid is 3615
child thread pid is 3615
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

ilk*_*chu 57

我认为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)。CLONE_THREAD并且进程 ID (*)和线程 ID的分离使 Linux 的行为看起来更像其他系统,更像 POSIX 要求在这个意义上。尽管从技术上讲,操作系统仍然没有线程和进程的单独实现。

信号处理是旧实现的另一个有问题的领域,这在@FooF在他们的回答中引用的论文中有更详细的描述。

正如评论中所指出的,Linux 2.4 也于 2001 年发布,与本书同年发布,因此该消息没有得到印刷也就不足为奇了。

  • *可能碰巧拥有一些共享资源的单独进程,例如虚拟内存或文件描述符。* Linux 线程几乎仍然是这样工作的,您提到的问题已被清理。我会说调用内核中使用的调度单元“线程”或“进程”真的无关紧要。他们在 Linux 上开始被称为“进程”这一事实并不意味着他们现在就是这样。 (2认同)

Foo*_*ooF 44

你是对的,确实“从 2001 年到现在,肯定发生了一些变化”。您正在阅读的这本书根据 POSIX 线程在 Linux 上的第一个历史实现来描述世界,称为LinuxThreads(另见维基百科文章)。

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),并且在移植假设来自 GNU LibC 的 NPTL 用户空间实现之前花了很多年作为默认的 POSIX 线程实现,一般来说,这些特殊平台并不努力以闪电般的速度追随最新潮流。LinuxThreads 实现在操作中的使用可以通过注意到,实际上,这些平台上不同线程的 PID 与 POSIX 标准指定的不同 - 就像您正在阅读的书所描述的那样。实际上,一旦你打电话pthread_create(),您突然将进程数从 1 增加到 3,因为需要额外的进程来保持混乱。

Linux pthreads(7)手册页对两者之间的差异进行了全面而有趣的概述。另一个有启发性但过时的差异描述是Ulrich Depper 和 Ingo Molnar 关于 NPTL 设计的这篇论文

我建议你不要把这本书的那部分看得太重。相反,我推荐 Butenhof 的 Programming POSIX 线程以及有关该主题的 POSIX 和 Linux 手册页。许多关于该主题的教程是不准确的。


ein*_*onm 23

(用户空间)线程在 Linux 上没有作为进程实现,因为它们没有自己的私有地址空间,它们仍然共享父进程的地址空间。

但是,这些线程被实现为使用内核进程记帐系统,因此分配了它们自己的线程 ID (TID),但被赋予与父进程相同的 PID 和“线程组 ID”(TGID) - 这与一个fork,在这里创建了一个新的TGID和PID,TID与PID相同。

因此,最近的内核似乎有一个单独的 TID 可以查询,这对于线程来说是不同的,在上面的每个 main() thread_function() 中显示这一点的合适代码片段是:

    long tid = syscall(SYS_gettid);
    printf("%ld\n", tid);
Run Code Online (Sandbox Code Playgroud)

所以整个代码是这样的:

#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;                                                                                                                                                 
} 
Run Code Online (Sandbox Code Playgroud)

给出一个示例输出:

main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
Run Code Online (Sandbox Code Playgroud)

  • @tomas einonm 是对的。不用管书上说的,真是太乱了。不知道作者想传达什么想法,但他失败了。因此,在 Linux 中,您有内核线程和用户空间线程。内核线程本质上是根本没有用户空间的进程。用户空间线程是普通的 POSIX 线程。用户空间进程共享文件描述符,可以共享代码段,但生活在完全独立的虚拟地址空间中。进程内的用户空间线程共享代码段、静态内存和堆(动态内存),但具有单独的处理器寄存器集和堆栈。 (3认同)

Lie*_*yan 11

在内部,Linux 内核中没有进程或线程之类的东西。进程和线程主要是用户空间的概念,内核本身只看到“任务”,它们是一个可调度的对象,可能与其他任务不共享任何资源、部分资源或全部资源。线程是已配置为与父任务共享其大部分资源(地址空间、mmap、管道、打开文件处理程序、套接字等)的任务,进程是已配置为与父任务共享最少资源的任务.

当您直接使用 Linux API(clone(),而不是fork()pthread_create())时,您可以更灵活地定义共享或不共享多少资源,并且您可以创建既不完全是进程也不完全是一个线程。如果您直接使用这些低级调用,还可以创建一个具有新 TGID 的任务(因此被大多数用户空间工具视为进程),该任务实际上与父任务共享其所有资源,反之亦然,以创建具有共享 TGID 的任务(因此被大多数用户空间工具视为线程),不与其父任务共享资源。

虽然 Linux 2.4 实现了 TGID,但这主要是为了资源记账。许多用户和用户空间工具发现能够将相关任务组合在一起并报告它们的资源使用情况很有用。

Linux 中任务的实现比用户空间工具呈现的进程和线程世界观要流畅得多。


R..*_*ICE 8

基本上,您书中的信息在历史上是准确的,因为 Linux 上线程的实现历史可耻。我对 SO 相关问题的回答也可以作为对您问题的回答:

/sf/ask/640827001/#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 进程)。


iva*_*van 6

Linus Torvalds 在 1996 年的内核邮件列表帖子中表示“线程和进程都被视为'执行上下文'”,它“只是该 CoE 的所有状态的集合......包括 CPU 之类的东西状态、MMU 状态、权限和各种通信状态(打开文件、信号处理程序等)”。

// simple program to create threads that simply sleep
// compile in debian jessie with apt-get install build-essential
// and then g++ -O4 -Wall -std=c++0x -pthread threads2.cpp -o threads2
#include <string>
#include <iostream>
#include <thread>
#include <chrono>

// how many seconds will the threads sleep for?
#define SLEEPTIME 100
// how many threads should I start?
#define NUM_THREADS 25

using namespace std;

// The function we want to execute on the new thread.
void threadSleeper(int threadid){
    // output what number thread we've created
    cout << "task: " << threadid << "\n";
    // take a nap and sleep for a while
    std::this_thread::sleep_for(std::chrono::seconds(SLEEPTIME));
}

void main(){
    // create an array of thread handles
    thread threadArr[NUM_THREADS];
    for(int i=0;i<NUM_THREADS;i++){
        // spawn the threads
        threadArr[i]=thread(threadSleeper, i);
    }
    for(int i=0;i<NUM_THREADS;i++){
        // wait for the threads to finish
        threadArr[i].join();
    }
    // program done
    cout << "Done\n";
    return;
}
Run Code Online (Sandbox Code Playgroud)

如您所见,该程序将同时产生 25 个线程,每个线程将休眠 100 秒,然后再次加入主程序。在所有 25 个线程重新加入程序后,程序完成并退出。

使用top您将能够看到“threads2”程序的 25 个实例。但是有点无聊。的输出ps auwx更不有趣......但ps -eLf变得有点令人兴奋。

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
debian     689   687   689  0    1 14:52 ?        00:00:00 sshd: debian@pts/0  
debian     690   689   690  0    1 14:52 pts/0    00:00:00 -bash
debian    6217   690  6217  0    1 15:04 pts/0    00:00:00 screen
debian    6218  6217  6218  0    1 15:04 ?        00:00:00 SCREEN
debian    6219  6218  6219  0    1 15:04 pts/1    00:00:00 /bin/bash
debian    6226  6218  6226  0    1 15:04 pts/2    00:00:00 /bin/bash
debian    6232  6219  6232  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6233  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6234  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6235  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6236  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6237  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6238  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6239  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6240  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6241  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6242  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6243  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6244  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6245  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6246  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6247  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6248  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6249  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6250  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6251  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6252  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6253  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6254  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6255  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6256  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6257  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6260  6226  6260  0    1 15:04 pts/2    00:00:00 ps -eLf
Run Code Online (Sandbox Code Playgroud)

您可以在此处看到thread2程序创建的所有 26 个 CoE 。它们都共享相同的进程 ID (PID) 和父进程 ID (PPID),但每个进程都有不同的 LWP ID(轻量级进程),并且 LWP (NLWP) 的数量表示共有 26 个 CoE——主程序和主程序。它产生了 25 个线程。


Dmi*_*ich 5

对于 Linux 来说,进程和线程是同一件事。也就是说,它们是使用相同的系统调用创建的:clone

如果您考虑一下,线程和进程之间的区别在于子进程和父进程将共享哪些内核对象。对于进程来说,这并不是很多:打开的文件描述符、尚未写入的内存段,可能还有一些我一时想不起来的东西。对于线程来说,共享更多的对象,但不是全部。

Linux 中使线程和对象更接近的是unshare系统调用。开始共享的内核对象可以在线程创建后取消共享。因此,例如,您可以在同一进程中拥有两个具有不同文件描述符空间的线程(通过在创建线程后撤销文件描述符的共享)。unshare您可以通过创建一个线程,调用两个线程,然后关闭所有文件并在两个线程中打开新文件、管道或对象来自行测试。然后查看/proc/your_proc_fd/task/*/fd,您将看到每个task(您作为线程创建的)都有不同的 fd。

事实上,新线程和新进程的创建都是库例程,它们在底层调用并指定新创建的 process-thread-thingamajig (即)将与调用进程/线程共享 clone哪些内核对象。task