1. 线程
1. 线程是一个进程内部的控制序列
2. 线程在进程内部运行,本质是在进程地址空间内运行
3. 进程:承担分配系统资源的基本实体
线程:CPU调度的基本单位
4. 线程在进程地址空间内运行
进程访问的大部分资源都是通过地址空间访问的
5. 在硬件CPU视角:线程是轻量级进程
Linux操作系统视角:执行流
执行流 <= 进程
6. 将资源合理分配给每一个执行流,就形成了线程执行流
7. 一切进程至少都有一个执行线程
总结:
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(索引)->物理页框地址
2. 写实拷贝,缺页中断,内存申请等,背后都可能要重新建立新的页表和建立映射关系的操作
3. 进程,一张页目录+n张页表构建的映射关系,虚拟地址是索引,物理地址页框是目标
物理地址=页框地址+虚拟地址(低12位)
线程的深刻理解
执行流看到的资源是在合法情况下拥有的合法虚拟地址,虚拟地址就是资源的代表
虚拟地址空间本质:进行资源的统计数据还和整体数据
资源划分:本质就是地址空间划分
资源共享:本质就是虚拟地址的共享
线程进行资源划分:本质是划分地址空间,获得一定范围的合法虚拟地址,在本质,就是划分页表
线程进行资源共享:本质是对地址空间的共享,在本质就是对页表条目的共享
申请内存也就是申请地址空间
越界不一定报错
优点:
线程切换:
线程之间的切换需要OS做的工作比进程要少很多
线程切换虚拟地址空间依然是相同的
线程切换时不用对CR3寄存器进行保存
线程切换不会导致缓存失效
进程切换:
指针指向我们选中的进程,OS想知道当前进程是谁,找到该指针,优化到cpu寄存器中
进程切换=>cpu硬件上下文切换
会导致TLB和Cache失效,下次运行,需要重新缓存
线程占用的资源比进程少?线程拿到的资源本身就是进程的一部分,线程是更轻量化的
2. 进程VS线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据
1.线程ID
2.一组寄存器,线程的上下文数据
3.栈
4.erno
5.信号屏蔽字
6.调度优先级
3. linux 线程控制
创建线程
#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概念
#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的升级版
#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
主线程和新线程谁先运行是不确定的