Linux 线程概念与控制:从底层原理到实战应用

目录

一、Linux线程概念

1.1什么是线程(与进程一起理解)

1.2分页式存储管理

1.2.1虚拟地址和页表的由来

1.2.2物理内存管理

1.2.3页表

1.2.4页目录结构

1.2.5两级页表的地址转换

1.2.6缺页异常

1.3线程的优点

1.4线程的缺点

1.5线程异常

1.6线程用途

[二、进程 VS 线程](#二、进程 VS 线程)

2.1进程和线程

2.2进程的多个线程共享

2.3关于进程线程的问题

三、Linux线程控制

3.1POSIX线程库

3.2创建线程

3.3线程终止

3.4线程等待

3.5分离进程

四、线程ID及进程地址空间布局

五、线程封装


一、Linux线程概念

1.1什么是线程(与进程一起理解)

  • 进程 = 内核数据结构 + 代码和数据(执行流)。所以进程要干很多事情,帮我们打开文件,链接动静态库、信号识别......都是占用内存资源和CPU资源的。总之,进程是承担分配系统资源的基本实体。
  • 线程是进程内部的一个执行分支(执行流),线程是CPU调度的基本单位。

结论:

  1. Linux"线程"可以采用进程来模拟
  2. 对资源的划分,本质是对地址空间虚拟地址(资源的代表)范围的划分
  3. 代码区划分:代码中都是一些函数的调用,函数就是虚拟地址(逻辑地址)空间的集合。运行起来就是让线程未来执行ELF程序的不同函数
  4. 之前我们理解的进程是,内部只有一个线程的进程。单进程就是一种特殊情况
  5. 其他操作系统对于进程的实现和Linux差不多,但对于线程,其他平台是采用了PCB+TCB的方法,这种方法是非常复杂的,而Linux没有为线程单独设计结构(没有设计TCB),复用task_struct,使用进程来模拟线程。所以进程的内核代码全部复用这种方法也更加健壮。
  6. 总之,Linux系统,从软件来看,是一个一个执行流;从硬件CPU角度来看,看到的PCB都要比传统的进程更轻量化(执行流 <= 进程)。线程就是轻量化进程,或者用轻量化进程来模拟实现的。

1.2分页式存储管理

1.2.1虚拟地址和页表的由来

如果在没有虚拟内存和分页机制的情况下,每一个用户程序在物理内存上所对应的空间必须是连续的,如下

因为每⼀个程序的代码、数据长度都是不⼀样的,按照这样的映射方式,物理内存将会被分割成各种离散的、大小不同的块。经过⼀段运行时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎片的形式存在。

我们希望操作系统提供给⽤⼾的空间必须是连续的,但是物理内存最好不要连续。此时虚拟内存和分页便出现了,如下图

把物理内存按照⼀个固定的长度的页框进行分割,有时叫做物理页。每个页框包含⼀个物理页(page)。⼀个页的大小等于页框的大小。大多数32位体系结构支持4KB的页,而64位体系结构⼀般会支持 8KB 的页。

页框是一个存储区域,页是一个数据块,可以存放在任何页框或者磁盘中。

有了这种机制,CPU便并非是直接访问物理内存地址,而是通过虚拟地址空间来间接的访问物理内存地址。所谓的虚拟地址空间,是操作系统为每⼀个正在执行的进程分配的⼀个逻辑地址,在32位机上,其范围从0~4G-1。

操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系,也就是页表,这张表上记录了每⼀对页和页框的映射关系,能让CPU间接的访问物理内存地址。

总结⼀下,其思想是将虚拟内存下的逻辑地址空间分为若干页,将物理内存空间分为若干页框,通过页表便能把连续的虚拟内存,映射到若干个不连续的物理内存页。这样就解决了使用连续的物理内存造成的碎片问题。

1.2.2物理内存管理

假设⼀个可用的物理内存有4GB 的空间。按照⼀个页框的大小4KB 进行划分, 4GB 的空间就是4GB/4KB = 1048576 个页框。有这么多的物理页,操作系统肯定是要将其管理起来的,操作系统 需要知道哪些页正在被使用,哪些页空闲等等。

内核用 struct page结构表示系统中的每个物理页,出于节省内存的考虑, struct page 中使用了大量的联合体union。struct page mem[1048576];

所以,每一个paga都会有下标,每一个page的其实物理地址天然就是知道的(index*4kb),具体物理地址 = 起始物理地址 + 页内(4kb)偏移;

我们申请物理内存实际上就是在,查数组改page,建立内核数据结构的对于关系。

复制代码
/* include/linux/mm_types.h */
struct page {
    /* 原子标志, 有些情况下会异步更新 */
    unsigned long flags;
    union {
        struct {
            /* 换出页列表, 例如由zone->lru_lock保护的active_list */
            struct list_head lru;
            /* 如果最低为为0, 则指向inode
             * address_space, 或为NULL
             * 如果页映射为匿名内存, 最低为置位
             * 而且该指针指向anon_vma对象
             */
            struct address_space* mapping;
            /* 在映射内的偏移量 */
            pgoff_t index;
            /*
             * 由映射私有, 不透明数据
             * 如果设置了PagePrivate, 通常用于buffer_heads
             * 如果设置了PageSwapCache, 则用于swp_entry_t
             * 如果设置了PG_buddy, 则用于表示伙伴系统中的阶
             */
            unsigned long private;
        };
        struct {    /* slab, slob and slub */
            union {
                struct list_head slab_list; /* uses lru */
                struct {    /* Partial pages */
                    struct page* next;
#ifdef CONFIG_64BIT
                    int pages;    /* Nr of pages left */
                    int pobjects; /* Approximate count */
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            struct kmem_cache* slab_cache; /* not slob */
            /* Double-word boundary */
            void* freelist;    /* first free object */
            union {
                void* s_mem;    /* slab: first object */
                unsigned long counters;    /* SLUB */
                struct {            /* SLUB */
                    unsigned inuse : 16;    /* 用于SLUB分配器: 对象的数目 */
                    unsigned objects : 15;
                    unsigned frozen : 1;
                };
            };
        };
    };

    union {
        /* 内存管理子系统中映射的页表项计数, 用于表示页是否已经映射, 还用于限制逆向映射搜索*/
        atomic_t _mapcount;
        unsigned int page_type;
        unsigned int active;    /* SLAB */
        int units;              /* SLOB */
    };

    ...

#if defined(WANT_PAGE_VIRTUAL)
    /* 内核虚拟地址 (如果没有映射则为NULL, 即高端内存) */
    void* virtual;
#endif /* WANT_PAGE_VIRTUAL */

    ...
};

其中一些参数需要重点了解:

1.flags:用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。flag的每⼀位单独表示⼀种状态,所以它至少可以同时表示出32种不同的状态。这些标志定义在 中。其中⼀些比特位非常重要,如PG_locked用于指定页是否锁定, PG_uptodate用于表示页的数据已经从块设备读取并且没有出现错误。

2._mapcount :表示在页表中有多少项指向该页,也就是这一页被引用了多少次。当计数值变 为-1时,就说明当前内核并没有引用这一页,于是在新的分配中就可以使用它。

3.virtual :是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(即所谓的高端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为NULL,需要的时候,必须动态地映射这些页。

要注意的是 struct page 与物理页相关,而并非与虚拟页相关。⽽系统中的每个物理页都要分配⼀ 个这样的结构体,让我们来算算对所有这些页都这么做,到底要消耗掉多少内存

算 struct page 占40个字节的内存吧,假定系统的物理页为 4KB 大小,系统有 4GB 物理内存。 那么系统中共有页面1048576 个(1兆个),所以描述这么多页面的page结构体消耗的内存只不过 40MB ,相对系统 4GB 内存而言,仅是很小的⼀部分罢了。因此,要管理系统中这么多物理页面,这个代价并不算太大。

页的大小对于内存利用和系统开销来说非常重要,页太大,页内必然会剩余较大不能利用的空间(页内碎⽚)。页太小 ,虽然可以减小页内碎⽚的大小,但是页太多,会使得页表太长而占用内存,同时系统频繁地进行页转化,加重系统开销。因此,页的大小应该适中,通常为 512B - 8KB ,windows/Linux系统的页框大小为4KB。

1.2.3页表

页表中的每⼀个表项,指向⼀个物理页的开始地址。在 32 位系统中,虚拟内存的最大空间是 4GB , 这是每⼀个用户程序都拥有的虚拟内存空间。既然需要让 4GB 的虚拟内存全部可用,那么页表中就需要能够表示这所有的 4GB 空间,那么就⼀共需要 4GB/4KB = 1048576 个表项。

虚拟内存看上去被虚线"分割"成⼀个个单元,其实并不是真的分割,虚拟内存仍然是连续的。这个 虚线的单元仅仅表示它与页表中每⼀个表项的映射关系,并最终映射到相同大小的⼀个物理内存页上。

表中的物理地址,与物理内存之间,是随机的映射关系,哪里可用就指向哪里(物理页)。虽然最终使用的物理内存是离散的,但是与虚拟内存对应的线性地址是连续的。处理器在访问数据、获取指令时,使用的都是线性地址,只要它是连续的就可以了,最终都能够通过页表找到实际的物理地址。

1.2.4页目录结构

每⼀个页框都被⼀个页表中的⼀个表项来指向了,那么这 1024 个页表也需要被管理起来。管理页表的表称之为页目录表,形成二级页表。

1.2.5两级页表的地址转换

细节部分:

  • 1.申请内存-->直接查找数组-->找到没用的page->page index-->物理页框地址
  • 2.写实拷贝,缺页中断,内存申请等,背后可能都要重新建立新的页表和建立映射关系的操作
  • 3.进程是,一张页目录表+n张页表构建的映射体系,虚拟地址是索引,物理地址页框是目标,虚拟地址(低12) + 页框地址 = 物理地址
  • 4.为什么是低12位呢,应为页框是4KB,刚好[0,4095];这样的转化还会使地址非常有序化(比如前10位一样的在同一张页表下)

以上其实就是MMU的工作流程。MMU(Memory Manage Unit)是⼀种硬件电路,其速度很快, 主要工作是进行内存管理,地址转换只是它承接的业务之⼀。

其实还有个问题,MMU要先进行两次页表查询确定物理地址,在确认了权限等问题后,MMU再将这个物理地址发送到总线,内存收到之后开始读取对应地址的数据并返回。那么当页表变为N级时, 就变成了N次检索+1次读写。可见,页表级数越多查询的步骤越多,对于CPU来说等待时间越长,效率越低。

单级页表对连续内存要求高,于是引入了多级页表,但是多级页表也是⼀把双刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率。

有没有提升效率的办法呢?计算机科学中的所有问题,都可以通过添加⼀个中间层来解决。 MMU 引入了新武器,江湖人称快表的**TLB (**其实,就是缓存,Translation Lookaside Buffer,学名转译后备缓冲器)

当 CPU 给 MMU 传新虚拟地址之后, MMU 先去问 TLB 那边有没有,如果有就直接拿到物理地址发到 总线给内存,齐活。但 TLB 容量比较小,难免发生 Cache Miss ,这时候 MMU 还有保底的老武器页表,在页表中找到之后 MMU 除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录 ⼀下刷新缓存。

1.2.6缺页异常

设想,CPU给MMU的虚拟地址,在 TLB 和页表都没有找到对应的物理页,该怎么办呢?其实这就是缺页异常 Page Fault ,它是⼀个由硬件中断触发的可以由软件逻辑纠正的错误。

假如目标内存页在物理内存中没有对应的物理页或者存在但无对应权限,CPU就无法获取数据,这种情况下CPU就会报告⼀个缺页错误。

由于CPU没有数据就无法进行计算,CPU罢工了用户进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler 处理。

缺页中断会交给 PageFaultHandler 处理,其根据缺页中断的不同类型会进行不同的处理:

Hard Page Fault 也被称为 Major Page Fault ,翻译为硬缺页错误/主要缺页错误,这时物理内存中没有对应的物理页,需要CPU打开磁盘设备读取到物理内存中,再让MMU建立虚拟地址和物理地址的映射。

Soft Page Fault 也被称为 Minor Page Fault ,翻译为软缺页错误/次要缺页错误,这时物理内存中是存在对应物理页的,只不过可能是其他进程调入的,发出缺页异常的进程不知道而已,此时MMU只需要建立映射即可,无需从磁盘读取写⼊内存,⼀般出现在多进程共享内存区域。

Invalid Page Fault 翻译为无效缺页错误,比如进程访问的内存地址越界访问,又比如对空指针解引用内核就会报 segment fault 错误中断进程直接挂掉。

1.如何理解 new 和 malloc?

malloc直接向OS申请虚拟内存,new(本质是封装了 malloc)申请内存,再调用类的构造函数初始化对象

2.如何理解写时拷贝?

fork() 创建子进程时,父子进程一开始共享父进程的所有内存页; 只有当子进程修改某页数据时,才会触发缺页中断,复制该页到新的物理内存。

3.申请内存,究竟是在干什么?

你写 malloc/new 时,申请的是虚拟内存地址,而不是直接拿物理内存。

整个流程是这样的:

    1. 进程向操作系统申请一段虚拟地址空间(比如 0x1000~0x2000);
    1. 操作系统在页表中记录这段虚拟地址,但此时不分配物理内存;
    1. 当进程第一次读写这段虚拟地址时,会触发缺页中断;
    1. 操作系统收到中断后,才真正分配物理内存,建立虚拟地址到物理地址的映射,再让进程继续执行。

一句话:虚拟地址先占着,物理内存"用到才给"。

4.如何区分缺页了,还是真的越界了?

操作系统通过两步检查来判断:

  1. 页号合法性检查

◦ 先看你访问的虚拟地址的页号,是不是在进程合法的虚拟地址范围内;

◦ 如果页号本身就非法(比如超出了进程地址空间的上限),直接判定为越界访问;

◦ 如果页号合法,但对应的物理页还没分配(页表项中标记为"未存在"),则触发缺页中断。

  1. 内存映射检查

◦ 再检查虚拟地址是否在当前进程的内存映射范围内;

◦ 地址在映射范围内,但物理页未分配 → 缺页中断;

◦ 地址不在映射范围内 → 越界访问(段错误)。

5. 越界了一定会报错吗?

不一定,分两种情况:

• 一定会报错的情况:访问的虚拟地址不在进程的地址空间内,操作系统会直接发送 SIGSEGV(段错误)信号,进程崩溃。

• 不一定报错的情况:访问的虚拟地址在进程地址空间内,但属于其他已分配的内存页:

◦ 比如你越界访问了相邻的合法内存页,只是修改了别人的数据,不会触发崩溃,但会造成数据损坏,这就是典型的"野指针"问题。

◦ 这种情况不会立刻报错,但会导致程序行为异常,而且很难排查。
6. 线程资源划分的真相

只要将虚拟地址空间进行划分,进程资源就天然被划分了。

• 进程的虚拟地址空间是所有线程共享的(堆、全局变量、代码段);

• 每个线程只拥有自己独立的栈空间,栈地址是虚拟地址空间里划分出来的一段;

• 所以线程间通信天然方便,但也容易出现数据竞争问题。

1.3线程的优点

1.创建⼀个新线程的代价要比创建⼀个新进程小得多

2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
  • 另外⼀个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下文,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB (快表)会被全部刷新 ,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache

3.线程占用的资源要比进程少

4.能充分利用多处理器的可并行数量

5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

7.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

1.4线程的缺点

  • 性能损失:⼀个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同⼀个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指 的是增加了额外的同步和调度开销,而可用的资源不变。
  • **健壮性降低:**编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护 的。
  • **缺乏访问控制:**进程是访问控制的基本粒度,在⼀个线程中调用某些OS函数会对整个进程造成影响。
  • **编写难度提高:**编写与调试⼀个多线程程序比单线程程序困难得多。

1.5线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃,这是因为线程是进程的执性分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

1.6线程用途

合理的使用多线程,能提高CPU密集型程序的执性效率;

合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们⼀边写代码⼀边下载开发工具,就是多线程运行的⼀种表现)

二、进程 VS 线程

进程间具有独立性

线程共享地址空间,也就是共享进程资源

2.1进程和线程

进程是资源分配的基本单位,线程是调度的基本单位。

线程共享进程数据,但也拥有自己的⼀部分"私有"数据:

  • 线程ID
  • 一组寄存器,线程的上下文数据
  • 栈(线程是一个动态的概念)
  • errno
  • 信号屏蔽字
  • 调度优先级

2.2进程的多个线程共享

同一地址空间,因此 Text Segment 、 Data Segment 都是共享的,如果定义⼀个函数,在各线程中都可以调用,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和 环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程的关系如下图:

2.3关于进程线程的问题

我们之前学习的单进程,就是一个具有线程执行流的进程

三、Linux线程控制

3.1POSIX线程库

**为什么会有这样的一个库?**Linux系统不存在真正意义上的线程,**他所谓的概念是使用轻量级进程模拟的,OS中只有轻量级进程。**是所以Linux只会提供创建轻量级进程的系统调用。

与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以"pthread_"打头的

3.2创建线程

  • thread:返回线程ID
  • attr:设置线程的属性,attr为NULL表示使用默认属性
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数
  • 返回值:成功返回0;失败返回错误码

注意:统的⼀些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。但pthreads函数出错时不会设置全局变量errno(而⼤部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。

cpp 复制代码
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>

void* threadrun(void* arg)
{
    std::string name = (const char*)arg;
    while(true)
    {
        std::cout << "我是新线程" << name << std::endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t thread;
    pthread_create(&thread,nullptr,threadrun,(void*)"thread-1");

    while(true)
    {
        std::cout << "我是主线程"<< std::endl;
        sleep(1);
    }

    return 0;
}
cpp 复制代码
#include <pthread.h>
// 获取线程ID 
pthread_t pthread_self(void);

怎么理解这个"ID"呢?这个"ID"是pthread库给每个线程定义的进程内唯一标识,是pthread库维持的。由于每个进程有自己独⽴的内存空间,故此"ID"的作用域是进程级而非系统级(内核不认识)。

其实pthread库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯⼀的"ID"来唯⼀标识这个线程。

**LWP是什么呢?**LWP得到的是真正的线程ID。之前使用pthread_self 得到的这个数实际上是⼀个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线 程ID,线程栈,寄存器等属性。

在ps -aL 得到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟 地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。

3.3线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  • 线程可以调用pthread_exit终止自己。
  • ⼀个线程可以调用pthread_cancel终止同⼀进程中的另⼀个线程。
  • value_ptr:value_ptr不要指向⼀个局部变量。
  • 返回值: 无返回值,跟进程⼀样,线程结束的时候无法返回到它的调用者(自身)

注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的, 不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

  • thread:线程ID
  • 返回值:成功返回0;失败返回错误码

3.4线程等待

为什么需要进程等待?会有僵尸问题,内存泄漏。而且创建新的线程不会复用刚才退出线程的地址空间。

  • thread:线程ID
  • value_ptr:它指向⼀个指针,后者指向线程的返回值
  • 返回值:成功返回0;失败返回错误码
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>


void *thread1( void *arg )
{
 printf("thread 1 returning ... \n");
 int *p = (int*)malloc(sizeof(int));
 *p = 1;
 return (void*)p;
}

void *thread2( void *arg )
{
 printf("thread 2 exiting ...\n");
 int *p = (int*)malloc(sizeof(int));
 *p = 2;
 pthread_exit((void*)p);
}
void *thread3( void *arg )
{
 while ( 1 ){ // 
 printf("thread 3 is running ...\n");
 sleep(1);
 }
 return NULL;
}
int main( void )
{
 pthread_t tid;
 void *ret;
 // thread 1 return
 pthread_create(&tid, NULL, thread1, NULL);
 pthread_join(tid, &ret);
 printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
 free(ret);
 // thread 2 exit
 pthread_create(&tid, NULL, thread2, NULL);
 pthread_join(tid, &ret);
 printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
 free(ret);
 
 // thread 3 cancel by other
 pthread_create(&tid, NULL, thread3, NULL);
 sleep(3);
 pthread_cancel(tid);
 pthread_join(tid, &ret);
 if ( ret == PTHREAD_CANCELED )
   printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
 else
   printf("thread return, thread id %X, return code:NULL\n", tid);
}
cpp 复制代码
运⾏结果:
 [root@localhost linux]# ./a.out
 thread 1 returning ... 
 thread return, thread id 5AA79700, return code:1
 thread 2 exiting ...
 thread return, thread id 5AA79700, return code:2
 thread 3 is running ...
 thread 3 is running ...
 thread 3 is running ...
 thread return, thread id 5AA79700, return code:PTHREAD_CANCELED

3.5分离进程

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。 如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,joinable和分离是冲突的,⼀个线程不能既是joinable又是分离的。

四、线程ID及进程地址空间布局

pthread_create函数会产生⼀个线程ID,存放在第⼀个参数指向的地址中。该线程ID和前面说的线程ID不是⼀回事。前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位, 所以需要⼀个数值来唯⼀表示该线程。pthread_create函数第⼀个参数指向⼀个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

函数pthread_self,可以获得线程自身的ID:

进程自己的代码区可以访问到pthread库内部的函数或者数据。

所以线程的创建过程是

五、线程封装

lesson22

相关推荐
网络工程小王1 小时前
【LangChain 大模型6大调用指南】调用大模型篇
linux·运维·服务器·人工智能·学习
wangbing11251 小时前
各linux版本的包管理命令
linux·运维·服务器
Joseph Cooper2 小时前
Linux/Android 跟踪技术:ftrace、TRACE_EVENT、atrace、systrace 与 perfetto 入门
android·linux·运维
比昨天多敲两行3 小时前
Linux基础开发工具(下)
linux·运维·服务器
feng14564 小时前
OpenSREClaw - 故障复盘和变更评审双 Agent 案例
运维·人工智能
linux修理工4 小时前
chrome官方下载地址
运维·服务器
light blue bird4 小时前
工序路径工站物料 BOM 协同组件
jvm
无忧智库4 小时前
IT运维正在经历一场真正的范式革命:从告警风暴到AIOps自主自愈的完整工程解构(WORD)
运维
笨笨饿4 小时前
69_如何给自己手搓一个串口
linux·c语言·网络·单片机·嵌入式硬件·算法·个人开发