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


65

我正在阅读 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

在我的Ubuntu 16.04中是:

main thread pid is 3615
child thread pid is 3615

ps 输出支持这一点。

我猜在2001年到现在之间一定有所改变。

下一页的下一个子章节4.5.1 Signal Handling建立在上一条语句的基础上:

信号和线程之间的交互行为在一个类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
我不明白你困惑的根源是什么。线程被实现为与其父级共享地址空间的进程。
JohanMyréen's

2
@JohanMyréen那么线程pid为何相等?
Tomasz

啊,现在我明白了。是的,情况确实发生了变化。请参阅@ilkkachu的答案。
JohanMyréen's

5
线程仍被实现为进程-但是现在getpid返回所谓的线程组ID,并为您需要使用的进程获取唯一的ID gettid。但是,除了内核以外,大多数人和工具都将线程组称为进程,并将进程称为线程,以与其他系统保持一致。
immibis

并不是的。一种方法都有它自己的内存和文件描述符,这是从来没有被称为一个线程,这样做会与其他系统相一致。
reinierpost

Answers:


50

我认为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年发布,因此新闻没有印出也就不足为奇了。


2
可能碰巧拥有一些共享资源的独立进程,例如虚拟内存或文件描述符。在您提到的问题已经清除之后,Linux线程的工作方式仍然差不多。我要说的是,将内核中使用的调度单元称为“线程”或“进程”确实无关紧要。他们在Linux上被称为“进程”的事实并不意味着它们就是现在。
安德鲁·亨利

@AndrewHenle,是的,有点编辑。我希望能抓住您的想法,尽管我似乎措辞很难。(如果需要,可以继续进行编辑。)我已经了解到,其他一些类Unix的操作系统在线程和进程之间有更明显的分离,Linux是一个例外,只有真正提供一种类型的服务两种功能。但是我对其他系统了解不多,没有方便的资源,因此很难说出具体的东西。
ilkkachu

@tomas请注意,此答案说明了Linux现在的工作方式。正如ilkkachu所暗示的那样,本书撰写时的工作方式有所不同。FooF的答案解释了当时的Linux是如何工作的。
吉尔斯(Gilles)

38

没错,确实是“从2001年到现在,某些事情已经改变了”。您正在阅读的书根据Linux上POSIX线程的第一个历史实现描述了世界,这称为LinuxThreads(有关某些信息,另请参阅Wikipedia文章)。

LinuxThreads与POSIX标准存在一些兼容性问题-例如,线程不共享PID-以及其他一些严重问题。为了解决这些缺陷,红帽(Red Hat)率先提出了另一种称为NPTL(本机POSIX线程库)的实现,以添加必要的内核和用户空间库支持,以达到更好的POSIX遵从性(从IBM另一个竞争的重新实现项目中取名为NGPT(“下一代Posix线程”),请参阅NPTL上的Wikipedia文章。添加到clone(2)系统调用中的其他标志(特别CLONE_THREAD@ikkkachu他的回答中指出)可能是内核修改中最明显的部分。工作的用户空间部分最终被合并到GNU C库中。

不过现在一些嵌入式Linux的软件开发工具包使用原有的LinuxThreads实现,因为他们使用的libc内存占用较小版本,称为uClibc的(也称为μClibc) ,并花了多年大量的来自GNU libc中的NPTL用户空间实现之前被移植,并假定为默认情况下,POSIX是默认的POSIX线程实现,因此,这些特殊的平台并不会以闪电般的速度努力遵循最新的潮流。通过注意到与POSIX标准指定的不同,这些平台上不同线程的PID确实也有所不同,可以观察到这一点-就像您正在阅读的书所描述的那样。其实一旦你打电话pthread_create(),您突然将进程数从一增加到了三,因为需要更多的过程来使混乱保持在一起。

Linux pthreads(7)手册页提供了两者之间差异的全面而有趣的概述。另一种启发,虽然外的日期的差异描述是这样的乌尔里希Depper和英格·蒙内关于NPTL设计。

我建议您不要过于重视本书的这一部分。相反,我推荐Butenhof的Programming POSIX thread和有关该主题的POSIX和Linux手册页。关于该主题的许多教程都不准确。


22

(用户空间)线程没有在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

3
@tomas einonm是正确的。不管这本书怎么说,这真是令人困惑。邓诺不知道作者想传达什么想法,但他失败了。因此,在Linux中,您具有内核线程和用户空间线程。内核线程本质上是完全没有用户空间的进程。用户空间线程是普通的POSIX线程。用户空间进程共享文件描述符,可以共享代码段,但是位于完全独立的虚拟地址空间中。进程内的用户空间线程共享代码段,静态内存和堆(动态内存),但具有单独的处理器寄存器集和堆栈。
鲍里斯·伯科夫

8

基本上,由于Linux中线程的糟糕实现历史,本书中的信息在历史上是准确的。我对SO上一个相关问题的回答也可以回答您的问题:

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

这些混乱的根源在于,内核开发人员最初持有一种不合理和错误的观点,即只要内核提供了一种使它们共享内存和文件描述符的方法,就可以使用内核进程作为原语在用户空间中几乎完全实现线程。 。这导致了臭名昭著的POSIX线程的LinuxThreads糟糕的实现,这是一个错误的称呼,因为它没有提供任何与POSIX线程语义相似的东西。最终,LinuxThreads被NPTL取代(但由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进程)。


8

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

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

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

与用户空间工具提供的进程和线程世界观相比,Linux中任务的实现更加流畅。


链接到的论文 @FooF描述了内核必须将进程和线程视为独立实体的许多点(例如,信号处理和exec()),因此在阅读之后,我不会真的说“没有这样的东西。是Linux内核中的进程或线程。”
ilkkachu

5

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;
}

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

使用top该按钮,您将看到25个“ threads2”程序实例。但是kidna很无聊。的输出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

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


正确,线程只是轻量级进程(LWP)
fpmurphy

3

当涉及到Linux进程和线程是那种同样的事情。也就是说,它们是使用相同的系统调用创建的:clone

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.