当前位置: 首页 > article >正文

Linux_线程概念

线程相关概念

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

pthread_create函数

参数说明:

  • pthread_t *thread:一个指向 pthread_t 类型的指针,用于获取新创建线程的标识符。这个标识符可以在其他线程函数中被引用,以便进行线程间的同步或等待其他线程结束。
  • const pthread_attr_t *attr:这是一个指向 pthread_attr_t 类型的指针,用于设置线程属性。在大多数情况下,这个参数被设置为 nullptr,使用默认的线程属性。
  • void *(*start_routine) (void *):这是一个指向线程函数的指针,当新线程被创建时,这个函数将被调用。这个函数返回一个 void * 类型的指针,并且接受一个 void * 类型的参数(这个参数就是arg)。
  • void *arg:传递给线程函数的参数。它可以是任何数据类型(变量、数字、对象),但通常会被强制转换为 void * 类型。在线程函数内部,你可以将其转换回原来的类型。

函数返回值:

  • 如果成功,pthread_create 将返回 0。
  • 如果失败,它将返回一个错误码,你可以使用 strerror 或 perror 函数来获取关于这个错误码的详细信息。

Demo代码

// 新线程
void *run(void *args)
{
    while(true)
    {
        std::cout << "new thread, pid: " << getpid() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::cout << "我是一个进程: " << getpid() << std::endl;
    pthread_t tid;
    pthread_create(&tid, nullptr, run, (void*)"thread-1");
    // 主线程
    while(true)
    {
        std::cout << "main thread, pid: "<< getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

进程地址空间(页表模块)

为什么要有虚拟地址和页表

既然要谈为什么要有虚拟地址和页表,那我们就假设没有虚拟内存和分页机制,这样的话,每⼀个用户程序都会直接映射到物理内存上,并且所对应的空间是连续的。而我们运行的程序不可能都是一样的,因为每一个程序的代码、数据长度都是不一样的,如果按照这样的映射方式,物理内存将会被分割成各种离散的、大小不同的块。

经过一段运行时间之后,有些程序会退出,那么它们占据的物理内存空间就会被回收,导致这些物理内存 “千疮百孔” 即造成了很多的内存碎片

我们希望操作系统提供给用户的空间必须是连续的,但是物理内存最好不要连续。因此虚拟内存和分页的存在就使这些问题得到了完美的解决!

虚拟地址和页表存在的必要性

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

页表

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

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

但是在 32 位系统中,地址的长度是 4 个字节,那么页表中的每一个表项就是占用 4 个字节。所以页表占据的总空间大小就是: 1048576 * 4 = 4MB 的大小。也就是说映射表自己本身,就需要占用 4MB / 4KB = 1024 个物理页。这也太不合理了,于是我们提出了多级页表的思想。

就像每一本书前面都有一个目录索引一样,这里就有一个页目录表,每个表项里面存放着每张页表的起始地址。所以操作系统在加载用户程序的时候,不仅仅需要为程序内容来分配物理内存,还需要为用来保存程序的页目录和页表分配物理内存。

// ps. 页表也是有权限的
// 页表的32个比特位中,前20位是页框的地址,后12位是权限位!
// 例:
char *msg = "hello";
*msg = 'H';
// 上面两句代码会报错,因为msg是放在字符常量区的,不允许被修改,进而程序崩溃
// 为什么?因为在虚拟在物理地址转换的时候,查找页表发现该操作映射的地址只有读权限
// 而你的操作涉及到 写权限,所以MMU就会报错
// 页表到底是什么?
typedef struct { unsigned long pte; } pte_t; //页表项
typedef struct { unsigned long pgd; } pgd_t; //页全局目录项
// 就是一个unsigned long(32位)类型的数组!

虚拟地址与物理地址的转换

我们都知道虚拟地址是32个比特位组成的,一共有2^32个。

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

  1. 在32位处理器中,采用4KB的页大小,则虚拟地址中低12位为页偏移,剩下⾼20位给页表,分成两级,每个级别占10个bit(10+10)。
  2. CR3 寄存器 读取页目录起始地址,再根据一级页号查页目录表,找到下一级页表在物理内存中存放位置。
  3. 根据二级页号查表,找到最终想要访问的内存块号。
  4.  结合页内偏移量得到物理地址。

进入CPU的都是虚拟地址,在CPU内部完成虚拟到物理的转换,是怎么做的呢?

其实在CPU转换物理地址时,会访问CR3寄存器(也被称为PDBR,页目录基址寄存器),获取页目录起始地址;CPU中还有一个叫做EIP的寄存器(PC),里面存放着当前进程的入口虚拟地址。当MMU(CPU中的寄存器)接收到CR3中的页目录起始地址EIP中的入口虚拟地址后,再去页表中查找与入口虚拟地址相映射的物理地址,这样就会将虚拟地址转换为物理地址。最后,从CPU中出来的直接就是物理地址。ps. 虚拟地址转换为物理地址是硬件(MMU)自主完成的!

当然,为了提高虚拟地址与物理地址在CPU中的转换的效率,CUP中还存在一个 TLB (快表、硬件)的缓存。当 CPU 给 MMU 传新虚拟地址之后, MMU 先去问 TLB 那边有没有,如果有就直接拿到物理地址发到总线给内存;如果没有或者没有命中,这时候 MMU 再去查找页表,在页表中找到之后, MMU 除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录一下刷新缓存。

缺页中断

如果CPU 给 MMU 的虚拟地址,而MMU在 TLB 和页表都没有找到对应的物理页(物理地址),这候就是发生缺页异常 (Page Fault) ,它是一个由硬件中断触发的可以由软件逻辑纠正的错误。比如,当程序首次访问某个数据或代码段时,由于其对应的页面还未被加载到内存,就会产生缺页中断。
再比如,目标内存页在物理内存中存在没有对应权限,CPU 也无法获取数据,CPU 没有数据就无法进行计算,CPU就会报错,即缺页错误。从而用户进程就会出现缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler 处理。

线程的优缺点

优点

  • 创建一个新线程的代价要比创建一个新进程小得多。
  • 线程占用的资源要比进程少很。
  • 能充分利用多处理器的可并行数量。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多:
    • 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
    • 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache(缓存代码和数据块)

缺点

  1. 性能损失
  2. 健壮性降低
  3. 缺乏访问控制
  4. 编程难度提高

Linux进程VS线程

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

  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    • 线程ID
    • 一组寄存器(线程具有独立的上下文数据)
    • 栈(独立栈)
    • errno
    • 信号屏蔽字
    • 调度优先级

进程的多个线程共享

线程共享以下进程资源和环境:

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

http://www.kler.cn/a/513521.html

相关文章:

  • C++ 类与对象(上)
  • Mac 使用 GVM 管理多版本 Go 环境
  • 前端开发Web
  • Python基础学习(六)unittest 框架
  • leetcode347.前k个高频元素
  • MySQL中日期和时间戳的转换:字符到DATE和TIMESTAMP的相互转换
  • CentOS 7 下安装RabbitMQ教程_centos启动rabbitmq
  • 分享源代码防泄露实战经验
  • Three.js实战项目01:vue3+three.js实现圣诞动画贺卡项目
  • 99.9 金融难点通俗解释:总资产收益率(ROA)
  • Spingboot整合Netty,简单示例
  • HJ108 求最小公倍数(Java版本)
  • Nim游戏算法问题(Java)
  • 颜色分配问题
  • 深入理解 Java 的数据类型与运算符
  • Cannot resolve symbol ‘XXX‘ Maven 依赖问题的解决过程
  • 55.命名、驼峰式、帕斯卡式 C#例子
  • MySQL表创建分区键
  • 37.构造回文字符串问题|Marscode AI刷题
  • PHP语言的网络编程
  • 深度学习 · 手撕 DeepLearning4J ,用Java实现手写数字识别 (附UI效果展示)
  • 【BUUCTF】[RCTF2015]EasySQL1
  • AT9880U-B-F8N-23北斗多频导航芯片车规级数据手册
  • Docker入门学习
  • cf<contest/1950>练习-python版
  • Django学习笔记(安装和环境配置)-01