线程的学习

1. 线程

  1. 线程是一个进程内部的控制序列

  2. 线程在进程内部运行,本质是在进程地址空间内运行

  3. 进程:承担分配系统资源的基本实体

线程:CPU调度的基本单位

  1. 线程在进程地址空间内运行

进程访问的大部分资源都是通过地址空间访问的

  1. 在硬件CPU视角:线程是轻量级进程

Linux操作系统视角:执行流

执行流 <= 进程

  1. 将资源合理分配给每一个执行流,就形成了线程执行流

  2. 一切进程至少都有一个执行线程


总结:

  1. 线程可以采用进程来模拟

  2. 对资源的划分本质是对地址空间虚拟地址范围的划分。虚拟地址就是资源的代表

  3. 函数就是虚拟地址(逻辑地址)空间的集合,就是让线程未来执行ELF程序的不同函数

  4. linux的线程就是轻量级进程,或者用轻量级进程模拟实现的

  5. 如果把家庭比作进程,那么家庭的每个成员就都是线程

进程强调独占,部分共享(通信的时候)

线程强调共享,部分独占


4KB内存与页框:

物理内存以4KB为单位被划分成一个一个的页框

在进行I/O操作时,数据也是以4KB为单位在内存和磁盘间交换(程序需要读取磁盘数据时也是以4KB大小的块来读取)

要管理这些4KB的页框,也是先描述再组织

申请物理内存是在做什么?

1.查数组,改page(页)

2.建立内核数据结构的对应关系

struck page是一个自定义描述物理内存中页的结构体

struct page mem[1048576];

声明了一个包含1048576个类型为struct page的元素的mem数组,每个page都有下标

4GB=4*1024*1024 KB

4*1024*1024KB/4KB = 1048576

每个page的起始物理地址就在独立,具体物理地址=起始物理地址+页(4KB)内偏移

没有使用的page标志位为0


划分地址空间本质就是划分虚拟地址

在cpu视角全部都是轻量级进程

OS管理的基本单位是4KB


页表(本质是一张虚拟到物理的地图)的地址转换

虚拟地址(逻辑地址) 转化为物理地址

32位的数字

0000000000 0000000000 000000000000

0,1024) \[0,1024) \[0,4096

CR3寄存器读取页目录起始位置,根据一级页号查页目录表

前10个bit位的缩影查到页目录

页目录里存储的是下一级页表的地址,每一项对应一个二级页表,定位到下一级页表的位置

页目录中的项可以理解为一种指针

二级页表里面存储的是物理页框的地址,用于实现虚拟地址和物理地址间映射的关键

低12位为页内偏移

4KB页面大小意味着每个页面有4096个字节单位,12位二进制数可表示为2^12 = 4096 个不同地址,可以用低12位去充分覆盖一个页框的整个范围,可唯一标识页面内的每个字节单元

先查到虚拟地址对应的页框,根据虚拟地址的低12位作为页内偏移访问具体字节


一些细节:

  1. 内存申请->查找数组->找到没有被使用的page(标志位为0)->page

index(索引)->物理页框地址

  1. 写实拷贝,缺页中断,内存申请等,背后都可能要重新建立新的页表和建立映射关系的操作

  2. 进程,一张页目录+n张页表构建的映射关系,虚拟地址是索引,物理地址页框是目标

物理地址=页框地址+虚拟地址(低12位)


线程的深刻理解

执行流看到的资源是在合法情况下拥有的合法虚拟地址,虚拟地址就是资源的代表

虚拟地址空间本质:进行资源的统计数据还和整体数据

资源划分:本质就是地址空间划分

资源共享:本质就是虚拟地址的共享

线程进行资源划分:本质是划分地址空间,获得一定范围的合法虚拟地址,在本质,就是划分页表

线程进行资源共享:本质是对地址空间的共享,在本质就是对页表条目的共享


申请内存也就是申请地址空间

越界不一定报错


优点:

线程切换:

线程之间的切换需要OS做的工作比进程要少很多

线程切换虚拟地址空间依然是相同的

线程切换时不用对CR3寄存器进行保存

线程切换不会导致缓存失效

进程切换:

指针指向我们选中的进程,OS想知道当前进程是谁,找到该指针,优化到cpu寄存器中

进程切换=>cpu硬件上下文切换

会导致TLB和Cache失效,下次运行,需要重新缓存

线程占用的资源比进程少?线程拿到的资源本身就是进程的一部分,线程是更轻量化的


2. 进程VS线程

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

线程是调度的基本单位

线程共享进程数据,但也拥有自己的一部分数据

1.线程ID

2.一组寄存器,线程的上下文数据

3.栈

4.erno

5.信号屏蔽字

6.调度优先级


3. linux 线程控制

创建线程

bash 复制代码
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);


//thread :返回线程id
//attr:设置线程属性
//start_routine:是个函数地址,线程启动后要执行的函数
//arg:传给线程启动函数的参数

线程创建好之后,新线程要被主线程等待(类似僵尸进程的问题,内存泄漏)

代码:

PID:进程ID

LWP: 轻量级进程ID

这意味着进程内有多个线程,每个线程对应一个LWP号

CPU调度的时候,看轻量级进程lwp

1.关于调度的时间片问题:时间等分给不同的线程

2.任何一个线程崩溃,都会导致整个进程崩溃

线程tid:不直接暴露lwp概念

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

void showtid(pthread_t &tid)
{
    printf("tid: 0x%lx\n",tid);
}
std::string FormatId(const pthread_t &tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}
void *routine(void *args)
{
    std::string name = static_cast<const char*>(args);
    pthread_t tid = pthread_self();
    int cnt = 3;
    while(cnt)
    {
        std::cout << "我是一个新线程: my name: main thread "  << " 我的Id:  " << FormatId(tid) << std::endl;
        sleep(1);
        cnt--; 
    }
    return nullptr;
}

int main()
{
    pthread_t tid;//tid变量用于存储新创建进程的标识符
    int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");
    (void)n;
    showtid(tid);
    int cnt = 3;
    while(cnt)
    {
        std::cout << "我是main线程: my name: main thread " << " 我的Id:  " << FormatId(pthread_self()) << std::endl;
        sleep(1);
        cnt--; 
    }
    pthread_join(tid,nullptr);//等待进程结束
    return 0;
}

main函数也有自己的线程id


pthread库,把创建轻量级进程封装起来,给用户提供一批创建线程的接口

linux线程实现是在用户层实现的,我们称之为用户级线程

pthread:原生线程库

C++的多线程,在linux下,本质是封装了pthread库,在windows下封装windows创建线程的接口

linux系统,不存在真正意义上的线程,他所谓的概念,使用轻量级进程模拟的,但OS中,只有轻量级进程,所谓的模拟线程是我们的说法,linux只会给我们提供创建轻量级进程的系统调用


pthread_exit函数

线程终止


pthread_cancel

取消一个执行中的线程

取消的时候一定要保证线程已经启动


pthread_join

等待线程结束

资源回收,线程终止时,系统不会自动回收线程资源,直到有其他线程调用该函数,目标线程的资源会被彻底释放。

//thread:要等待的线程id

//retval:二级指针,用于存储目标线程的返回值

pthread_join() 函数必须由其他线程调用,用于回收目标终止线程的资源

只能由当前线程以外的其他线程调用。例如:

主线程可以调用 pthread_join() 回收子线程的资源

子线程 A 可以调用 pthread_join() 回收子线程 B 的资源

通过函数参数指定要回收的目标线程 ID(tid)


线程分离

线程分离是一种管理线程资源的机制,当线程被设置为分离状态时,它终止后会自动释放所有资源,不需要有其他线程调用pthread_join来回收资源

线程的状态:1.Joinable 可结合的 新创建的线程是可结合的,需要对其进行pthread_join操作来回 收资源,避免资源泄露

2.Detached 分离的


linux没有真正的线程,他是用轻量级进程模拟的

os提供的接口,不会直接提供线程接口

在用户层,封装轻量级进程形成原生线程库(用户级别的库)

linux所有线程,都在库中

线程的概念是在库中维护的,在库内部就一定会存在多个被创建好的线程,库管理线程也是先描述再组织

pthread_create()

struct tcb

//线程应该有的属性

线程状态

线程id

线程独立的栈结构

线程栈大小

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

linux所有线程都在库中


显示器文件本身就是共享资源

拿到新线程的退出信息

线程测试

线程能够执行进程的一部分

创建多线程

为什么是9?给每个线程第一传id,大家的地址都一样,所有线程参数指向的空间都是同一个,每创建一个进程都要覆盖式改这个id,创建的这个id会让所有线程都看到,拿到的都是同一个地址,指向的是同一个64位的空间,所以进行对应的写入时就把上一次的覆盖了

每一次循环要给每一个线程申请一段堆空间,这个堆空间虽然也是共享的,但只有改=该线程知道空间的起始地址

创建访问线程的本质就是访问用户级别的库


描述线程的管理块

pthread库在内存中

只需要描述我们线程有关的id信息

创建一个描述线程的管理块,有三部分构成,返回时,返回的id地址就是这个块的起始地址

需要jion,因为线程结束时,只是函数结束了,但是在库中线程管理块并没有结束

由tid(退出的线程管理块的起始地址/线程在库中,对应的管理快块的虚拟地址)和ret(曾经线程退出时的结果)就拿到整个线程退出时的退出信息,再把该线程的管理块全部释放,得到返回结果同时解决内存泄漏问题

在自己的代码区里调create(),其实是在动态库内部创建描述该线程的管理块,管理块的开头是线程tcb,里面包含了线程的相关信息,tcb里包含了void *ret字段。当当前线程运行的时候,运行结束会把返回值拷贝到自己线程控制块的void *ret。新主线程都共享地址空间,只要拿到起始虚拟地址,就可以拿到退出线程的控制块

每个线程都有自己独立的栈空间


clone是用于创建进程/线程的函数,可以看作是fork的升级版

bash 复制代码
 #define _GNU_SOURCE
 #include <sched.h>

 int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
               /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

fn:子进程/线程的入口函数

linux所有线nn

linux用户级线程:内核级LWP = 1:1

主线程和新线程谁先运行是不确定的

相关推荐
tq10864 分钟前
值类:Kotlin中的零成本抽象
java·linux·前端
SimonKing23 分钟前
集合的处理:JDK和Guava孰强孰弱?
java·后端·算法
爱装代码的小瓶子32 分钟前
字符操作函数续上
android·c语言·开发语言·数据结构·算法
haokan_Jia38 分钟前
【java中使用stream处理list数据提取其中的某个字段,并由List<String>转为List<Long>】
java·windows·list
蓝胖子不会敲代码42 分钟前
跟着AI学习C# Day12
学习·microsoft·c#
码破苍穹ovo1 小时前
回溯----5.括号生成
java·数据结构·力扣·递归
软件2051 小时前
【Java树形菜单系统设计与实现】
java
麓殇⊙1 小时前
操作系统期末复习--操作系统初识以及进程与线程
java·大数据·数据库
编程乐趣1 小时前
C#实现图片文字识别
开发语言·c#
坏柠1 小时前
C++ 进阶:深入理解虚函数、继承与多态
java·jvm·c++