【Linux系统】线程概念

再谈地址空间

理解页框、页帧

OS进行内存管理,不是以字节为单位而是以内存块为单位的,默认大小是4kb。系统和磁盘文件进行IO的基本单位是4kb --- 8个扇区,而操作系统对内存的管理工作,基本单位也是4kb,所以内存与磁盘文件之间的IO的基本单位就是4kb!!!对内存这每一个4kb的管理,这每一个4kb内存块,也叫页框、页帧,我们前面讲过父子进程共享同一块内存空间,当发生写入时要进行写时拷贝,那写时拷贝的基本单位其实就是4kb!!符合局部性原理!!对页框要进行管理,那就要先描述,再组织,用一个结构体描述,flag标志位该页框是否被占用,是否是脏页,是否被锁定,权限mode等等...所以内存中有很多页框,假如我们的内存是4GB大小,所以4*1024*1024*1024 / 4 * 1024 = 1048576,对应的有1MB的page存储大概有十几个字节,也就是存储struct page大概也就十几MB,其中也有引用计数,写时拷贝时就用到了,所以就有1048576个页框,用一个结构体数组管理起来,所以每一个页框就有了下标,每一个下标对应的地址就是页框的首地址,所以我们对页框的管理就转化为对数组的增删查改!!!我们可以理解页框是内存块,页帧是文件数据块。

理解虚拟地址

一个进程有task_struct, 地址空间, 页表, 假设地址空间为2^32即4GB,那么页表是不是应该有2^32行,每行有虚拟地址4字节,物理地址4字节,权限1字节,4 * (4 + 4 + 1)= 36GB???

这页表能这么大吗?不能!

那么真实的页表是什么样的?虚拟地址是如何转化为物理地址呢?

 我们的内存中的数据是要被唤入CPU中的,我们已经知道虚拟地址经过MMU转为了物理地址,那是如何转的呢?我们把虚拟地址看做32位比特位,其中前10位作为第一张叫做页目录的索引,一共10个比特位,所以有1024个索引,所以页目录大小是1024,其实页表有很多张,而页目录正是来索引1024张页表的,而虚拟地址的中间10位能够作为每张页表内每个条目的索引,而每个条目可以指向每个页框的起始地址,虚拟地址后12位为4096个字节,可以用来索引每个页框的页内偏移,毕竟每个页框刚好对应4kb大小,也就是4096个字节单位,用[0, 4095]索引刚刚好,所以虚拟地址& 0xfff不就是页框号吗。

最后我们来算算帐,页目录为1024项,每张页表为1024项,每项2字节(因为用前20位比特位即可表示每个页框起始地址,所以大约2字节即可),每张页表即2kb,故页目录与页表占用空间:1024*2kb=2MB + 4kb(每个页目录存放的是页表的地址,也就是每个条目占4字节,所以一共1024项即4kb),其实实际可能会比这个数值小。

我们已经明白了虚拟地址如何转化的了,那页表如何找到呢?其实在CPU内部还有一个寄存器叫CR3,里面存放的就是页目录的地址,方便CPU去索引页表,那么我们的虚拟地址 + MMU + CR3

 + cr2是页故障线性地址寄存器就可以把我们的虚拟地址转化为物理地址了!!!

页目录存放的是页表的地址,页表也叫二级页表,用来搜索页框!!!

理解文件缓冲区

不就是几个页框组成的文件缓冲区吗,与struct file建立关系,唤入唤出。

理解代码数据划分的本质,理解虚拟地址的本质?

地址空间中的正文代码部分,如果正文部分有20个函数,拆分一下,技术上可行吗?所有的正文代码一定是限定了一批虚拟地址空间的范围,依靠页表才能看到,函数有地址啊,函数的地址就是函数的入口地址,而且函数内部每行代码都有地址,而且我们认为同一个函数地址是连续的,那么函数是什么?连续的代码地址构成代码块,一个函数对应一批函数地址!!那么拆分函数,也就是A函数,B函数,C函数,拆分函数不也就是拆分页表吗!!!所以虚拟地址的本质是一种资源!!!

理解线程的概念和Linux中线程的实现

理解线程概念

线程:在进程内部运行,是CPU调度的基本单位。

我们先抛出线程的概念,我们怎么理解在进程内部运行

 在之前我们父子进程共享同一块内存,写入时进行写时拷贝,所以进程是独立的,但今天要讲线程,线程其实是多个执行流共享同一块地址空间,线程是执行流?我们知道进程=内核数据结构+进程的代码和数据,我们今天重新理解一下进程的定义-- 内核观点:进程:承担分配系统资源的基本实体!!!

我们以前的进程其实是内部只有一个执行流的进程,而现在要讲的是内部有多个执行流的进程!!!所以线程其实是在PCB中运行的!!!

Linux其实是用进程模拟的线程,为什么?

OS要单独设计线程,那就要新建?暂停?销毁?调度?线程要不要和进程产生关联?等等

要管理线程,先描述,在组织 struct TCB{ // 线程的ID,优先级,状态,上下文,连接属性...}这些属性PCB内也有啊,同时还要创建一个PCB管理线程,Windows就是这么做的!!!

Windows提供了真实的线程控制块!

 我们为什么要单独设计一个数据结构,来表示线程???执行流,进程执行流!

我能不能复用PCB,用PCB统一表示执行流,这样的话,我们就不需要为线程单独设计数据结构和调度算法了!!!

往后CPU看到的task_struct <= 进程的!CPU在概念上可能就是一个进程的一个PCB或一个执行流!所以在宏观上要不要区分task_struct是进程还是线程呢?在CPU看来不做区分,都叫做执行流!所以从此往后,CPU看到的执行流 <= 进程 

Linux中的执行流:轻量级进程!!!

往后我们执行代码在多个线程下同时执行,由原来的串行执行,到今天的并发执行!!!

所以线程是CPU调度的基本单位,是因为在CPU看来调度的都是由task_struct为基本实体进行调度的!!!

线程创建接口与命令(见一个)

pthread_create

 创建一个线程,参数1是线程id,参数二是线程的属性(后续不谈),参数三是新线程要执行的函数,参数三是函数要传入的参数。

以后我们把第一个执行流叫主线程,往后创建的执行流叫新线程

测试代码:

#include <iostream>
#include <unistd.h>
#include <pthread.h>

//新线程
void *threadStart(void *args)
{
    while (true)
    {
        std::cout << "new thread running...pid:" << getpid() << std::endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadStart, (void *)"thread-new");

    //主线程
    while (true)
    {
        sleep(1);
        std::cout << "main thread running...pid:" << getpid() << std::endl;
    }
    return 0;
}

 pid为什么一样呢?两个线程同在一个进程内部!

ps -aL

 LWP(Light Weight Process轻量级进程):就是线程的id

当PID == LWP时就是主线程!

已经有多进程了,为什么要有多线程?

优点:

启动或者是创建时,进程要有自己独立的pcb,地址空间,页表构造映射关系,而线程只创建一份地址空间和页表,所以进程创建成本非常高,创建线程成本非常低!

运行时,进程切换时要不断维护各自的上下文数据,线程则只需维护一份即可,线程调度成本低!

死亡删除时,删除一个进程要把每个进程的pcb,地址空间,页表,代码和数据都要删除,成本太高了!

缺点:

健壮性差,一个线程出问题崩溃导致所有的线程都会出问题崩溃!!!

不同系统对于进程和线程的实现都不一样,为什么OS的课本讲的内容基本类似?

因为基本原理(指导思想)基本都相同,只是具体的实现不同!!! 

线程调度成本更低? 为什么?(面试题)

 因为在硬件上CPU内部有cache高速缓冲存储器,每次都会把内存中的数据加载到cache中,发现是否名字cache对应的数据就会把相应数据加载到内部,等需要使用时在拿出来,但如果我们有多个进程每个进程都有自己独立的代码和数据,那A进程加载到cache中的数据,B进程是用不到的,要加载新的数据,原来的数据清空,这样调度成本不是很高吗,而线程共享地址空间、页表、代码和数据,唤入换出都没有影响,所以线程调度成本低!!!

总结

什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
     

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现(谨慎的创建合适的线程)
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作(系统允许可以创建更多的线程)
     

线程的缺点

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

线程异常

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

线程用途

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

线程的共享和私有

进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:

线程私有

  • 线程ID
  • 一组寄存器(重要)
  • 栈(重要)
  • errno
  • 信号屏蔽字
  • 调度优先级

为什么一组寄存器重要?硬件保存的上下文数据,说明线程可以动态运行

为什么栈重要?线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区。

线程共享 --- 除了地址空间、页表。代码和数据,全局变量...

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

进程与线程的关系

  • 30
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花影随风_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值