• Linux多线程


    文章目录

    一. 线程概念

    以往创建进程会创建 task_struct,mm_struct,页表等,但如果创建 “进程” 只能创建 task_struct,不能创建 mm_struct,页表等,且创建出来的 task_struct 和父进程共享进程地址空间,页表等,这些"进程"被称作线程

    在Linux中,站在CPU的角度,不能识别 task_struct 是进程还是线程,因为CPU只关心一个一个的独立执行流

    进程 : 站在内核角度,承担分配系统资源的基本实体

    我们之前所认识的进程都是只有一个 task_struct,也就是说进程只有一个执行流

    Linux下,并不存在真正的多线程,如果支持真的线程,当线程足够多的时候,OS一定是要管理线程的,这样会提高设计OS的复杂程度,所以线程直接复用了进程的 task_struct ,即线程是用进程模拟出来的,Linux下的所有执行流,都叫做轻量级进程

    Linux并没有真正意义上的线程,所以Linux也没有线程相关的系统调用,但会提供创建线程的接口(vfork() : 创建进程,父子共享空间),在用户角度,可以使用原生线程库来解决

    分页存储 :

    将内存空间分为一个个大小相等的分区(例如每个分区4KB),每个分区为一个页框/页帧/内存块/物理块/物理页面,每个页框有一个编号,即页框号/页帧号/内存块号/物理块号/物理页号,页框号从0开始

    进程地址空间也分为与页框大小相等的一个个部分,每一部分称为页/页面,每个页面也有一个编号,即页号,页号也从0开始

    页表 : 由一个个页表项组成,每个页表项由"页号"和"块号"组成

    单级页表将逻辑地址转换成物理地址的步骤 (页面大小为4KB,页表项为4B):

    (1). 逻辑地址/4KB得到逻辑地址所处的页面

    (2). 逻辑地址 % 4KB 得到页内偏移量

    (3). 查询页表得到对应的内存块号,内存块号 * 4KB + 页内偏移量得到物理地址

    单级页表存在的问题 :

    某计算机系统按字节寻址,支持32位的逻辑地址,采用分页存储管理,因为页面的大小固定为 4KB = 2^12B,所以前12位表示页内偏移量,后20位为页号,系统中最多有2 ^20个页面,页表项为4B,则页表所需要的内存空间为 2 ^20 * 4B = 2 ^22B = 4MB

    二级页表 :

    系统最多有2^20个页面,对应页表项有2 ^20个,每个页表项为4B,我们可以将2 ^10个页表项为一组,这样一组页表项的大小为 2 ^10 * 4B = 2 ^12B = 4KB,正好可以放到一个页框中,将其分为2 ^

    10组,维护一个页目录表,用来记录每组的内存块号,页目录表的大小 2 ^10 * 4B = 2^12B = 4KB

    二级页表将逻辑地址转换成物理地址的步骤 (页面大小为4KB,页表项为4B):

    (1). 将逻辑地址分为3部分,根据后10位找到对应内存块的二级页表

    (2). 根据中10位找到逻辑地址对应物理地址的起始地址

    (3). 根据前12位得到偏移量,由第二步得到的起始地址 + 偏移量得到最终的物理地址

    1. // 为什么不能改?
    2. const char* msg = "hello world\n";
    3. *msg = 'H';

    页表里还有是否命中,权限等选项,前面说过页表也分为内核级页表和用户级页表,但不是物理上分开,而是通过权限分开的,页表中有RWX(读写可执行)权限,访问字符串时,需要访问页表得到物理地址,但RWX权限是只R的,写入会触发MMU硬件错误,操作系统识别到该硬件错误向该进程发送信号使进程终止

    缺页中断 : 如果在访问期间,目标资源不在内存中,则会触发缺页中断,进行内存分配,页面换入,建立映射,在调用malloc,new时,只是在堆区空间开辟出来,也就是只给了你虚拟地址,当访问时,发现物理内存还没分配,操作系统会申请一块内存,然后进行虚拟地址和物理地址的映射,这种现象就叫做由于缺页中断引起的内存分配的现象

    线程的优点 :

    (1). 创建一个新线程的代价要比创建一个新进程小得多

    创建进程要创建task_struct,mm_struct,页表等数据结构(所以创建进程本质上是系统不断的分配资源),而创建线程只需要创建出一个task_struct以及少量的数据结构,只需要分配进程的资源即可

    (2). 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

    进程的切换需要涉及到切换task_struct,mm_struct,页表,上下文信息等等,而线程切换只需要切换上下文即可

    (3). 线程占用的资源要比进程少很多

    (4). 在等待慢速IO操作结束的同时,程序可执行其他的计算任务(迅雷边下边播的功能就是用多线程实现的)

    (5). 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

    计算密集型 : 执行流的大部分任务,主要以计算为主 : 加密解密,排序查找

    (6). I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

    I/O密集型 : 执行流的大部分任务,是以I/O为主的 : 刷磁盘,访问数据库,访问网络(百度网盘上传和下载)

    线程的缺点 :

    (1). 性能损失

    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型

    线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的

    同步和调度开销,而可用的资源不变。

    (2). 健壮性降低

    因为进程运行是具有独立性的,所以一个进程挂了不会影响另一个进程,而多线程地址空间共享,也就意味着通过地址空间,多线程看到的资源大部分是相同的,一个线程崩溃了,其他线程也会崩溃,进程也就随之崩溃了

    (3). 缺乏访问控制

    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

    进程 vs 线程

    (1). 进程是资源分配的基本单位,线程是调度的基本单位

    (2). 线程共享进程数据,但也有自己的一部分数据

    线程ID

    一组寄存器(线程上下文数据)

    errno

    信号屏蔽字

    调度优先级

    寄存器保存线程的上下文信息

    线程运行时会产生各种临时数据,需要将数据进行压栈

    进程的多个线程共享

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

    文件描述符表

    每种信号的处理方式(SIG_IGN,SIG_DFL,或自定义处理函数)

    当前工作目录

    用户id和组id

    1. 功能:创建一个新的线程
    2. 原型 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
    3. 参数:
    4. pthread_t : 无符号长整型
    5. thread:返回线程ID
    6. attr:设置线程的属性,attr为NULL表示使用默认属性
    7. start_routine:是个函数地址,线程启动后要执行的函数
    8. arg:传给线程启动函数的参数
    9. 返回值:成功返回0,失败返回错误码
    1. #include
    2. #include
    3. #include
    4. #include
    5. void* Routine(void* arg)
    6. {
    7. while(1)
    8. {
    9. printf(" %s : pid : %d,ppid : %d\n ",(char*)arg,getpid(),getppid());
    10. sleep(1);
    11. }
    12. }
    13. int main()
    14. {
    15. pthread_t tid;
    16. pthread_create(&tid,NULL,Routine,(void*)"thread 1");
    17. while(1)
    18. {
    19. printf("main thread , pid : %d,ppid : %d\n",getpid(),getppid());
    20. sleep(1);
    21. }
    22. return 0;
    23. }

    主线程和新创建的线程进程pid和ppid都是一样的,可以看出虽然这是两个线程,但是这两个线程同属于一个进程

    我们可以使用 ps -aL 命令来查看线程

    1. ps -aL | head -1 && ps -aL | grep mythread
    2. -L : 显示当前的轻量级进程

    所以实际上,操作系统调度的时候,根本看的不是进程pid,而是LWP(light weight process)

    1. pthread_t pthread_self(void);
    2. 功能 : 可以获得线程自身的ID

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. void* Routine(void* arg)
    7. {
    8. while(1)
    9. {
    10. printf(" %s : pid : %d,ppid : %d,tid : %lu\n ",(char*)arg,getpid(),getppid(),pthread_self());
    11. sleep(1);
    12. }
    13. }
    14. int main()
    15. {
    16. pthread_t tid[5];
    17. for(int i = 0;i < 5;i++)
    18. {
    19. char* buffer = malloc(64);
    20. sprintf(buffer,"thread %d",i);
    21. pthread_create(&tid[i],NULL,Routine,(void*)buffer);
    22. printf("%s tid : %lu\n",buffer,tid[i]);
    23. }
    24. while(1)
    25. {
    26. printf("main thread , pid : %d,ppid : %d,tid : %lu\n",getpid(),getppid(),pthread_self());
    27. sleep(1);
    28. }
    29. return 0;
    30. }

    pthread_self()获得的是用户级原生线程库的线程 id,LWP是内核的轻量级进程id,这两个id是有对应关系的(1 : 1),一个用户级原生线程库的线程 id 对应 LWP 的某个值

    1. 原型 int pthread_join(pthread_t thread, void **value_ptr);
    2. 功能:等待线程结束(阻塞式等待)
    3. 参数
    4. thread:线程ID
    5. value_ptr:它指向一个指针,后者指向线程的返回值
    6. 返回值:成功返回0;失败返回错误码
    7. #include<stdio.h>
    8. #include<stdlib.h>
    9. #include<sys/types.h>
    10. #include<unistd.h>
    11. #include<pthread.h>
    12. void* Routine(void* arg)
    13. {
    14. int count = 0;
    15. while(count < 5)
    16. {
    17. printf("%s : pid : %d,ppid : %d,tid : %lu\n ",(char*)arg,getpid(),getppid(),pthread_self());
    18. sleep(1);
    19. count++;
    20. }
    21. return (void*)0; // (void*)10
    22. }
    23. int main()
    24. {
    25. pthread_t tid[5];
    26. for(int i = 0;i < 5;i++)
    27. {
    28. char* buffer = (char*)malloc(64);
    29. sprintf(buffer,"thread %d",i);
    30. pthread_create(&tid[i],NULL,Routine,(void*)buffer);
    31. printf("%s tid : %lu\n",buffer,tid[i]);
    32. }
    33. printf("main thread , pid : %d,ppid : %d,tid : %lu\n",getpid(),getppid(),pthread_self());
    34. for(int i = 0;i < 5;i++)
    35. {
    36. void* ret = NULL;
    37. pthread_join(tid[i],&ret);
    38. printf("thread %d[%lu]....quit,code : %d\n",i,tid[i],(int)ret);
    39. }
    40. return 0;
    41. }

    线程终止 :

    (1). return XXX 终止线程,但如果在main函数中return XXX,会导致进程退出,所有线程退出

    (2). 在线程中使用 pthread_exit(void *value_ptr)࿰

  • 相关阅读:
    deepin安装MySQL5.7
    【Mysql】大量数据查询时的优化相关知识
    CF750C (1600)
    C++深入学习part_2
    2022.10.1
    面向对象进阶第三天
    2022寒假字节跳动前端训练营笔记
    使用 Redis 实现分布式锁,解决分布式锁原子性、死锁、误删、可重入、自动续期等问题(使用SpringBoot环境实现)
    java毕业生设计疫苗药品批量扫码识别追溯系统计算机源码+系统+mysql+调试部署+lw
    leetcode第311场周赛
  • 原文地址:https://blog.csdn.net/java_lujj/article/details/126949311