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

Linux:线程

我恨死大物实验、模电实验、嵌入式实验、概率论和满实验室飞的蚊子了!!!

前言:

OS进行内存管理,是以内存块为单位管理的,一个块默认是4kb,一个块里有八个扇区

查看当前系统下的块的大小:

getconf PAGESIZE

系统和磁盘进行IO的基本单位都是4kb,页(虚拟)是系统的最小单位,块是磁盘的最小单位

4kb也可以被称为页框/页帧,OS对内存的管理工作的基本单位也是4kb

拷贝(立即拷贝)的时候也是把所有的页框拷贝过去,尽管并不是所有数据都是立即用到,但是这样可以防止在复制的时候发生缺页中断,但是会占用内存(空间换时间)

struct page
{
    int flag;     //是否被占用,是否是脏页,是否被锁定
    int mode;
    
    ...
}

*脏页:修改文件数据的时候不包含马上同步到磁盘,会缓存在内存的page cache中,这种和磁盘数据不一样的页为脏页

大概就像这样,没有上传更改的时候就是脏页

组织一个页表需要用数组来管理

struct page memory[1048576]

下标就是每个内存页框的地址

MMU(内存管理单元)会将虚拟地址映射成物理地址

页表也是有等级的

一级页表:简单的单层页表,每个虚拟地址直接映射到物理地址

多级页表:每一级都进一步细分为虚拟地址,避免单一页表占据过多空间(就像一级、二级目录一样)

也就是说一级页表会映射多个页(一个一级目录底下有很多各个目录下的详细内容)

一级页表有1024(2的10次方)个条目,也就是说它的前十位用于索引(索引1024个页表,存的是他们的页表起始地址)其余地址部分用于后续级别的页表索引或页内偏移

举个栗子:

一个32位地址+二级页表

一个典型的二级页表是这样划分虚拟地址的:前十位作为高10位,用于索引一级页表(有2的10次方个目录);中十位用于索引二级页表(也有2的10次方个目录);低十二位用于业内偏移(每页大小为2的12次方=4kb=4096bit)

一个地址为0x12345678,前20位(0x12345)指的是第几个页表条目,页表中存放的起始地址,而后12位(0x678)就是页内偏移,来确定在这个页内的具体字节位置

假如这个0x12345678映射的物理页框是0xABC00000,那么物理地址就是0xABC00000+0x678==0xABC00678

在代码中,一块函数里有很多行代码,每行代码都有自己的地址,每行的地址在一个函数内都是连续的;一个函数对应一批连续的虚拟地址,函数是连续地址构成的代码块

线程的概念/Linux下线程的实现

线程俗称是“进程的进程”,是CPU调度的基本单位(进程是资源分配和调度的基本单位)

我们还是讨论task_struct,地址空间,页表,物理内存的关系

偷偷放别人的图

每个进程都有自己独立的地址空间,进程之间切换和通信有比较大的工作量;但是线程在同一个进程内可以共享进程的地址空间,切换的工作量比较小

所以在底层上,我们要执行一个新的task的时候,创建一个新的task_struct的时候,不额外创建地址空间和页表,而是指向我们之前创建的地址空间和页表,正文代码不同的部分交给不同的线程去执行,所以这些线程共享这个进程的地址空间。

这张图是偷贩卖纯净水的,将进程资源分配给每个执行流,形成线程执行流

一切进程里至少有一个执行线程,线程在进程内部执行,本质上是在进程的地址空间内运行

我们之前说到进程=进程内核数据结构+进程的代码和数据

进程是承担分配系统资源的基本实体

之前我们学的进程是内部只有一个执行流的进程

而多线程进程就是内部有多个执行流的进程

如果OS要设计一个线程,那个这个线程是不是要经历新建、暂停、销毁、调度(具有和进程相同的部分)

那既然线程是进程的进程,那么我们的线程肯定也要和进程扯上关系,怎么扯上关系?

进程有进程的控制块(PCB),线程有线程的控制块

在Windows上,真的有提供给线程的线程控制块

而在Linux上,线程是直接复用进程的PCB格式,毕竟他们也都需要新建、暂停、销毁、调度

那么CPU怎么区分task_struct是进程还是线程?

答案是完全没必要区分!因为对于一个CPU来说,他们都是执行流

CPU眼里的进程都叫执行流,都是轻量级进程

线程在进程内部运行,是CPU调度的基本单位,进程是承担分配资源的基本实体

来看看创建线程的接口函数

第一个参数,pthread_t *thread:类似于获取进程pid的getpid()

第二个参数,const pthread_attr_t *attr:定制线程的类型,一般设置为NULL,可用作制作默认属性的线程

第三个参数,void *(*start_routine) (void *):一个函数,线程会执行这个函数,新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg

第四个参数,void *arg:执行函数中中参数。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构体中,然后把这个结构的地址作为arg参数传入

pth.c

#include<stdio.h>
#include<pthread.h>
void* gopthread(void *arg){
    while(1){
        sleep(1); 
        printf("this is a pthread,tid==%d\n",getpid());

    }
}
int main(){
    pthread_t tid;
    pthread_create(&tid,NULL,gopthread,(void*)"thread-new");
    while(1){
        sleep(1);
        printf("main going\n");
    }

    return 0;
}

可以看见两个执行流在交替执行,但是只有一个进程

怎么查看线程?

ps -aL

上图的LWP是什么呢?

叫轻量级进程(Light Weight Process),也就是线程

OS进行调度的时候是看的LWP,而不是pid,因为LWP是线程

那么LWP和tie什么关系?答案是他们都是线程的id,但是tid在系统提供的接口函数中更常用

上图的pid和lwp的数值有的一样,有的不一样,一样的是主线程

多线程的必要性

我们既然可以创建很多个进程,那么为什么还要有线程的概念

我们之前也提到线程创建的成本比进程的成本低多了,因为线程不需要额外创建页表、地址空间等等,而且删除也很方便

我们明白了创建成本为什么低,为什么说线程的调度成本低?

不同线程的地址空间是共享的,是同一张页表;CPU 在调度进程的时候,不同系统对进程和线程的实现是不一样的,CPU内存有着cache,我们的代码会预先加载到cache里,这叫热数据,cache是CPU中的硬件

你怎么知道的?

lscpu//主要是用来显示CPU结构相关信息

如果是进程间切换调度的时候,cache里的数据都要重新换;带上线程就不需要,因为cache上的数据是可能被用上的

那如果进程里执行的线程就是一个操作系统呢?

那就是虚拟机啊!(一个进程挂掉不会影响另一个进程)

通过进程的虚拟地址空间,可以看到进程的大部分资源,再将资源合理分配给每个执行流,也就是线程;而线程也不是越多越好,合适的才是最好的,双核CPU创建两个线程比较好

线程的优点

创建成本低

切换工作少

占用资源少

能充分利用多处理器的可并行数量

等待慢速I/O操作结束的同时,程序可以执行其他计算任务

计算密集型应用,可以分解到多个线程实现(协程是交替实现)

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

缺点

性能损失:一个密集型应用的线程无法与其他线程共享多个处理器,如果密集型线程的数量比可用的CPU多,那么就会造成较大的性能损失(性能损失:增加了额外的同步和调度开销,但是可用资源不变)

健壮性降低:一个多线程的任务中,如果因为时间上的分配导致的细微偏差或者因共享了不该共享的变量(野指针、除以0等)而造成的不良影响蛮大的,也就是说线程和线程之间是没有保护机制的

缺乏访问控制:进程是访问的基本粒度,在线程里调用进程的OS函数会对整个进程造成影响

编程难度高: 编写与调试一个多线程程序比单线程程序困难得多

我们来写一个代码证明线程健壮性低:

pth.c:

#include<stdio.h>
#include<pthread.h>
void* gopthread(void *arg){
    while(1){
        sleep(5); 
        printf("this is a pthread,pid==%d\n",getpid());
        int x=rand()%5;
        if(x==0){
            int *p=NULL;
            *p=100;//段错误,访问空指针
        }

    }
}
int main(){
    srand(time(NULL));
    pthread_t tid1;
    pthread_create(&tid1,NULL,gopthread,(void*)"thread-new");
    pthread_t tid2;
    pthread_create(&tid2,NULL,gopthread,(void*)"thread-new");
    pthread_t tid3;
    pthread_create(&tid3,NULL,gopthread,(void*)"thread-new");
    while(1){
        sleep(5);
        printf("main going,pid==%d\n",getpid());
    }

    return 0;
}

忘了说了,这是查询的脚本:

while :; do ps -aL |head -1 &&ps -aL |grep 'pth.exe'; sleep 1; done

可以看见所有的线程都挂起了(顺便一说,TTY是伪终端)

来看看证明线程的缺乏访问控制:

pth.c:

#include<stdio.h>
#include<pthread.h>
int gval=100;
void* gopthread(void *arg){
    while(1){
        sleep(5); 
        printf("this is a pthread,pid==%d\n",getpid());
        int x=rand()%5;
        if(x==0){
            int *p=NULL;
            *p=100;//段错误,访问空指针
        }

    }
}
int main(){
    srand(time(NULL));
    pthread_t tid1;
    pthread_create(&tid1,NULL,gopthread,(void*)"thread-new");
    pthread_t tid2;
    pthread_create(&tid2,NULL,gopthread,(void*)"thread-new");
    pthread_t tid3;
    pthread_create(&tid3,NULL,gopthread,(void*)"thread-new");
    while(1){
        sleep(5);
        printf("main thread running...,pid==%d\n",getpid());
        printf("new thread running...,pid==%d, gval==%d,&gval==%p\n",getpid(),gval,&gval);
    }

    return 0;
}

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

用途

虽然线程一直是在共享数据,但是也有一部分自己的资源

线程中私有的部分包括:一组寄存器(硬件上下文数据---线性可以动态运行)、栈(线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区)、线程ID、errno、信号屏蔽字、调度优先级

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

文件描述符表

每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

当前工作目录

用户id和组id

为什么我们编译的时候要有-pthread选项?

因为我们的进程和线程共用的是一样的PCB,于是我们的操作系统就给线程设定了一个专门的轻量级进程(也就是线程)封装库,封装后交给用户

Linux下的线程是用户级线程,而Windows下的线程是内核级线程


http://www.kler.cn/news/359035.html

相关文章:

  • 详解Oracle审计(二)
  • 什么是SCRM?为什么企业要做SCRM?
  • 5种边界填充
  • 代码随想录第一天|704.二分查找 27.移除元素
  • 【论文精读】把一切转成mesh!MeshAnything和MeshAnythingV2论文解析
  • 使用centos8在docker环境下编译ceph reef并使用s3cmd与awscli测试
  • 【某农业大学计算机网络实验报告】实验五 TCP 运输连接管理
  • RFC2616 超文本传输协议 HTTP/1.1
  • 修改pq_default.ini禁用降噪,解决S905X3电视盒硬解视频画质模糊、严重涂抹得像油画、水彩画的问题
  • 冲击美团!已成功 OC
  • 重磅!望繁信科技与德勤中国签署战略合作协议
  • OneThink ThinkCMS think.cms
  • 如何基于 Python 快速搭建 QQ 开放平台 QQ 群官方机器人详细教程(更新中)
  • C语言[数组作函数参数]
  • 论文《Text2SQL is Not Enough: Unifying AI and Databases with TAG》
  • Java AI赋能MJ绘画系统开启创意绘图新纪元
  • Spring AI Alibaba 接入国产大模型通义千问
  • C:浅谈数组指针的声明
  • CODESYS面向对象编程:方法/动作/属性的使用
  • MyBatis 动态 SQL 详解