Linux:线程概念与控制

✨✨✨学习的道路很枯燥,希望我们能并肩走下来!

文章目录

目录

文章目录

前言

[一 Linux线程概念](#一 Linux线程概念)

[1.1 什么是线程](#1.1 什么是线程)

[1.2 分页式存储管理](#1.2 分页式存储管理)

[1.2.1 虚拟地址和⻚表的由来](#1.2.1 虚拟地址和⻚表的由来)

[1.2.2 物理内存管理](#1.2.2 物理内存管理)

[1.2.3 ⻚表](#1.2.3 ⻚表)

[1.2.4 页目录结构](#1.2.4 页目录结构)

[1.2.5 两级⻚表的地址转换](#1.2.5 两级⻚表的地址转换)

[1.2.6 缺⻚异常](#1.2.6 缺⻚异常)

​编辑

[1.2.7 进程运行时,CPU和物理内存交互的过程](#1.2.7 进程运行时,CPU和物理内存交互的过程)

[1.2.8 小问题解答](#1.2.8 小问题解答)

[1.3 线程的优点](#1.3 线程的优点)

[1.4 线程的缺点](#1.4 线程的缺点)

[​编辑 1.5 线程异常](#编辑 1.5 线程异常)

[​编辑 1.6 线程⽤途](#编辑 1.6 线程⽤途)

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

[2.1 进程和线程](#2.1 进程和线程)

[2.2 进程的多个线程共享](#2.2 进程的多个线程共享)

[​编辑三 Linux线程控制](#编辑三 Linux线程控制)

[3.1 POSIX线程库](#3.1 POSIX线程库)

[3.2 创建线程](#3.2 创建线程)

[3.3 线程终止](#3.3 线程终止)

[​编辑3.4 线程等待](#编辑3.4 线程等待)

[3.5 分离线程](#3.5 分离线程)

[四 线程ID及进程地址空间布局](#四 线程ID及进程地址空间布局)

[五 线程封装](#五 线程封装)

[5.1 Thread.hpp](#5.1 Thread.hpp)

[5.2 Main.cc](#5.2 Main.cc)


前言

本篇详细介绍了进一步介绍Linux的线程概念与控制,让使用者有更加深刻的认知,而不是仅仅停留在表面,更好的模拟,为了更好的使用. 文章可能出现错误,如有请在评论区指正,让我们一起交流,共同进步!


一 Linux线程概念

1.1 什么是线程

• 在⼀个程序⾥的⼀个执⾏路线就叫做线程(thread)。更准确的定义是:线程是"⼀个进程内部 的控制序列"

• ⼀切进程⾄少都有⼀个执⾏线程

• 线程在进程内部运⾏,本质是在进程地址空间内运⾏

• 在Linux系统中,在CPU眼中,看到的PCB都要⽐传统的进程更加轻量化

• 透过进程虚拟地址空间,可以看到进程的⼤部分资源,将进程资源合理分配给每个执⾏流,就形成了线程执⾏流

不同的线程可以看到同一份进程地址空间

1.2 分页式存储管理

1.2.1 虚拟地址和⻚表的由来

思考⼀下,如果在没有虚拟内存和分⻚机制的情况下,每⼀个⽤⼾程序在物理内存上所对应的空间必须是连续的,如下图:

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

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

把物理内存按照⼀个固定的⻓度的页框 进⾏分割,有时叫做物理⻚。每个⻚框包含⼀个物理⻚(page)。⼀个⻚的⼤⼩等于⻚框的⼤⼩。⼤多数 构⼀般会⽀持 32 位 体系结构⽀持 4KB 的⻚,⽽ 64 位体系结构一般会支持 8KB 的⻚。区分⼀⻚和⼀个⻚框是很重要的:

• ⻚框是⼀个存储区域;

• ⽽⻚是⼀个数据块,可以存放在任何⻚框或磁盘中。

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

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

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

1.2.2 物理内存管理

下标和物理地址的转换

1.2.3 ⻚表

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

为了解决这个问题,可以把这个单⼀⻚表拆分成 1024 个体积更⼩的映射表。如下图所⽰。这样⼀ 来,1024(每个表中的表项个数)*1024(表的个数),仍然可以覆盖 4GB 的物理内存空间。

这⾥的每⼀个表,就是真正的⻚表,所以⼀共有 1024 个⻚表,⼀个⻚表⾃⾝占⽤ 4kb,那么1024 个⻚表⼀共就占⽤了4MB 的物理内存空间,和之前没差别啊?

从总数上看是这样,但是⼀个应⽤程序是不可能完全使⽤全部的4GB空间的,也许只要⼏⼗个⻚表就 可以了。例如:⼀个⽤⼾程序的代码段、数据段、栈段,⼀共就需要 10 MB 的空间,那么使⽤ 3 个 ⻚表就⾜够了。

1.2.4 页目录结构

到⽬前为⽌,每⼀个⻚框都被⼀个⻚表中的⼀个表项来指向了,那么这 1024 个⻚表也需要被管理起来。管理⻚表的表称之为**⻚⽬录表**,形成⼆级⻚表。如下图所⽰:

• 所有⻚表的物理地址被⻚⽬录表项指向

• ⻚⽬录的物理地址被CR3 寄存器指向,这个寄存器中,保存了当前正在执⾏任务的⻚⽬录地址。

所以操作系统在加载⽤⼾程序的时候,不仅仅需要为程序内容来分配物理内存,还需要为⽤来保存程 序的⻚⽬录和⻚表分配物理内存。

1.2.5 两级⻚表的地址转换

下⾯以⼀个逻辑地址为例。将逻辑地址 ( 0000000000,0000000001,11111111111 )转换为物理地址的过程:

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

让我们现在总结⼀下:单级⻚表对连续内存要求⾼,于是引⼊了多级⻚表,但是多级⻚表也是⼀把双 刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率。

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

1.2.6 缺⻚异常

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

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

由于CPU没有数据就⽆法进⾏计算,CPU罢⼯了⽤⼾进程也就出现了缺⻚中断,进程会从⽤⼾态切换到内核态,并将缺⻚中断交给内核的Page Fault Handler处理。

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

1.2.7 进程运行时,CPU和物理内存交互的过程

物理内存有地址就可以访问,没有那么复杂,权限只是在软件层面的概念

每一种设备中都有寄存器,包括磁盘,网卡,内存,寄存器包括数据寄存器和地址寄存器

块加载到内存里

操作系统有全局变量

task_struct* current指向当前进程,CR3指向页表,

之后得到的物理地址通过地址总线,放入到内存的地址寄存器上,以及要对内存操作的指令放入寄存器,这样CPU就把操作需求给了物理内存

之后内存就把你要的虚拟地址通过地址总线,放入ir指令寄存器中,同时

此时,EIP就更新到下一条指令,之后cpu进行解码,对ir里的虚拟地址重复上述操作

1.2.8 小问题解答

1. 仅仅只是在虚拟地址空间申请,把页表的关系维护起来,等到要使用时,触发缺页中断,才加载到内存,构建页表映射

  1. 写时拷贝,页表共享?把权限改为只读,触发缺页中断

用户进程和内存管理进行解耦

1.3 线程的优点

硬件cache 是将数据缓存起来,当cpu要使用数据时,可以先去cache中找是否存在,若不存在,则再进行页表查询

1.4 线程的缺点

1.5 线程异常

1.6 线程⽤途

二 Linux进程VS线程

2.1 进程和线程

• 进程是资源分配的基本单位

• 线程是调度的基本单位

• 线程共享进程数据,但也拥有⾃⼰的⼀部分数据:

◦ 线程ID

◦ ⼀组寄存器 ------寄存器的上下文数据

◦ 栈

◦ errno

◦ 信号屏蔽字

◦ 调度优先级

2.2 进程的多个线程共享

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

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

三 Linux线程控制

3.1 POSIX线程库

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

• 要使⽤这些函数库,要通过引⼊头文件 <pthread.h>

• 链接这些线程函数库时要使⽤编译器命令的"-lpthread"选项

3.2 创建线程

错误检查:

• 传统的⼀些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指⽰错误

• pthreads函数出错时不会设置全局变量errno(⽽⼤部分其他POSIX函数会这样做)。⽽是将错 误代码通过返回值返回

• pthreads同样也提供了线程内的errno变量,以⽀持其它使⽤errno的代码。对于pthreads函数的 错误,建议通过返回值业判定,因为读取返回值要⽐读取线程内的errno变量的开销更⼩

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

打印出来的tid是通过pthread库中有函数 pthread_self得到的,它返回⼀个pthread_t类型的 变量,指代的是调⽤pthread_self函数的线程的"ID"。

怎么理解这个"ID"呢?这个"ID"是pthread库给每个线程定义的进程内唯⼀标识,是pthread库维持的。

由于每个进程有⾃⼰独⽴的内存空间,故此"ID"的作⽤域是进程级⽽⾮系统级(内核不认识)

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

使⽤PS命令查看线程信息

运⾏代码后执⾏:

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

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

3.3 线程终止

如果需要只终⽌某个线程⽽不终⽌整个进程,可以有三种⽅法:

  1. 从线程函数return。这种⽅法对主线程不适⽤,从main函数return相当于调⽤exit。

  2. 线程可以调⽤pthread_exit终⽌⾃⼰。

  3. ⼀个线程可以调⽤pthread_cancel终⽌同⼀进程中的另⼀个线程。

pthread_exit函数:

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

pthread_cancel函数

3.4 线程等待

为什么需要线程等待?

• 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。

• 创建新的线程不会复⽤刚才退出线程的地址空间。

调⽤该函数的线程将挂起等待,直到id为thread的线程终⽌。thread线程以不同的⽅法终⽌,通过 pthread_join得到的终⽌状态是不同的,总结如下:

1. 如果thread线程通过return返回,value_ptr所指向的单元⾥存放的是thread线程函数的返回值。

2. 如果thread线程被别的线程调⽤pthread_cancel异常终掉,value_ptr所指向的单元⾥存放的是常数PTHREAD_CANCELED((void*)-1)。

3. 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元存放的是传给 pthread_exit的参数。

4. 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ptr参数

3.5 分离线程

默认情况下,新创建的线程是joinable的线程退出后,需要对其进⾏pthread_join操作,否则 ⽆法释放资源,从⽽造成系统泄漏。

• 如果不关⼼线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃动释放线程资源。

可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程⾃⼰分离:

cpp 复制代码
pthread_detach(pthread_self())

joinable和分离是冲突的,⼀个线程不能既是joinable⼜是分离的。

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

pthread_t pthread_self(void);

pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现⽽⾔,pthread_t类 型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。

五 线程封装

5.1 Thread.hpp

cpp 复制代码
#ifndef _THREAD_HPP__
#define _THREAD_HPP__

#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

namespace ThreadModule
{
    using func_t = function<void()>;
    static int number = 1;
    enum class TSTATUS
    {
        NEW,
        RUNNING,
        STOP
    };

    class Thread
    {
    private:
        static void *Routine(void *args)
        {
            Thread *t = static_cast<Thread *>(args);
            t->_func();
            t->_status = TSTATUS::RUNNING;
            return nullptr;
        }
        void Enablejoin()
        {
            _joinable = false;
        }

    public:
        Thread(func_t func)
            : _func(func), _status(TSTATUS::NEW), _joinable(true)
        {
            _name = "Thread-" + to_string(number++);
            _pid = getpid();
        }

        bool Start()
        {
            if (_status != TSTATUS::RUNNING)
            {
                int n = pthread_create(&_tid, nullptr, Routine, this);
                if (n != 0)
                {
                    cout << "pthread_create error" << endl;
                    return false;
                }
                return true;
            }
            return false;
        }

        bool Stop()
        {
            if (_status == TSTATUS::RUNNING)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
                    cout << "pthread_cancel error" << endl;
                    return false;
                }
                _status = TSTATUS::STOP;
                return true;
            }
            return false;
        }
        bool Join()
        {
            if (_joinable)
            {
                int n = pthread_join(_tid, nullptr);
                if (n < 0)
                {
                    cout << "pthread_join error" << endl;
                    return false;
                }
                _status = TSTATUS::STOP;
                return true;
            }
            return false;
        }
        void Detach()
        {
            Enablejoin();
            pthread_detach(_tid);
        }
        bool IsJoinable()
        {
            return _joinable;
        }
        string Name()
        {
            return _name;
        }
        ~Thread()
        {
        }

    private:
        string _name;
        pthread_t _tid;
        pid_t _pid;
        TSTATUS _status;
        bool _joinable;
        func_t _func;
    };
}

#endif

5.2 Main.cc

cpp 复制代码
#include <iostream>
#include <vector>
#include "Thread.hpp"

using namespace ThreadModule;

#define NUM 4

int ticketnum = 10000; // 共享资源

void Ticket()
{
    while (true)
    {
        if (ticketnum > 0)
        {
            usleep(1000);
            printf("get a new ticket, id: %d\n", ticketnum--);
        }
        else
        {
            break;
        }
    }
}

int main()
{
    // 1. 构建线程对象
    std::vector<Thread> threads;
    for (int i = 0; i < NUM; i++)
    {
        threads.emplace_back(Ticket);
    }

    // 2. 启动线程
    for (auto &thread : threads)
    {
        thread.Start();
    }

    // 3. 等待线程
    for (auto &thread : threads)
    {
        thread.Join();
    }
}

总结

✨✨✨各位读友,本篇分享到内容是否更好的让你理解线程概念与控制,如果对你有帮助给个👍赞鼓励一下吧!!
🎉🎉🎉世上没有绝望的处境,只有对处境绝望的人。
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!

相关推荐
剑神一笑1 小时前
Linux chown 命令详解:从 inode 到实战
linux·运维·服务器
学代码的真由酱1 小时前
Docker基础
运维·docker·容器
MIXLLRED1 小时前
随笔——在 Ubuntu 22.04 中查看 Markdown (.md) 文件
linux·运维·ubuntu·markdown
STDD1 小时前
Linux cgroup v2 资源控制实战:限制进程 CPU/内存/IO,systemd slice 管理
linux·运维·服务器
Latticy2 小时前
内网渗透-横向移动-密码喷洒攻击和域内用(kerbrute使用)
运维·服务器·网络·内网渗透·内网
元直数字电路验证2 小时前
云计算实验笔记(四):容器编排(Container Orchestration)
运维·笔记·docker·云计算
kukubuzai2 小时前
Docker Note
linux·运维·docker
Ltd Pikashu3 小时前
insmod 加载内核模块 —— sys_init_module 源码剖析
linux·kernel·insmod
大貔貅喝啤酒3 小时前
pip 国内镜像源大全【测试 / 自动化开发常备】
运维·自动化·pip·国内镜像源