目录

前言:

一、进程与线程

二、线程初体验

三、分页式存储管理初谈

总结:


前言:

大家好啊,今天我们就要开始翻阅我们linux操作系统的另外一座大山:线程了。

对于线程,大体结构上我们是划分为两部分,一部分是线程的概念与控制,另外一部分是线程的同步与互斥的相关内容。

本篇文章我将会为大家介绍线程的一些基础知识,加上对我们之前所学内容与线程之间的联系。

一、进程与线程

我们之前学过进程。

当时我们说:进程是一个执行起来的程序,进程=内核数据结构+代码与数据。

而什么是线程呢?

线程是一个执行流,执行粒度比进程更细,是进程内部的一个执行分支。

也就是说,一个进程可以包含多个线程,而这些线程共享进程的资源(如内存空间,我们后面会解释),但各自拥有独立的执行上下文(如栈、寄存器)

我们之前所说的进程,只有一个执行流,这种“单线程进程”其实是多线程模型的一种特例,如今更常见的是多线程程序,以提高并发性和资源利用率。

我们现在要更新一下对于线程进程的概念。

对于进程来说,进程是分配系统资源的基本实体。

对于线程来说,线程是OS调度的基本单位。


进程都需要被管理起来,那么比进程执行粒度更细的线程呢?

自然也要被管理起来,提到管理,就不得不说出那六个字:先描述,再组织!!

那我们就应该类似管理进程一样,专门弄出一个类似于PCB的结构来管理线程?

那我们的操作系统未免也太复杂了吧。

所以,linux的设计者也考虑到了这一点,于是linux的设计者就决定,我们可不可以让PCB(task_struct)近似的拿去管理线程呢?

所以线程,实际上也是通过PCB来进行管理的,没错,你没有听错,线程,也是通过PCB来进行管理的。

一个程序是一个进程,但是这个进程不一定只有一个PCB,我们从来没说过一个进程只能有一个PCB。所以有着多个PCB的进程,这多出来的,就是一个一个的执行流,就是一个一个的线程。线程也是task_struct描述起来的。

我们之前的模型,都是单线程进程,这唯一一个PCB,代表着这个进程的主执行流。

该进程唯一的 task_struct(PCB)即代表其主执行流,二者是等价的。此时 “进程”=“线程”,因为只有一个执行流,无需区分概念。


而我们之前讲进程的PCB的时候说过,PCB中包含很多数据结构,包括页表,mm_struct,vm_area_struct list。那现在的多线程进程中,我们有多个PCB,这里面的每一个PCB都有着这些结构吗?

当然,要不然为什么他们都是由PCB管理起来的呢?

那他们的数据也是一样的吗?

是,也不是。

一个进程中可以含多个PCB,我们就以主执行流的PCB数据为准,其他执行流(线程)的PCB,里面的mm_struct,vm_area_struct list这些结构数据,其实是共享的,他们之间共享相同的地址空间和资源,但是他们的寄存器和用户栈空间是每个线程各自独立的:

struct task_struct {pid_t pid;    // 线程ID(内核视角)pid_t tgid;   // 线程组ID(用户视角的进程ID)struct mm_struct *mm; // 指向共享的内存描述符// 每个线程有独立的:struct thread_struct thread;  // 寄存器状态void *stack;                 // 内核栈
};

我们可以理解为每一个线程存储的大部分数据都是一样的,在执行代码时,由于我们的执行上下文不同,各自维护独立的执行状态,所以我们可以并发的执行不同的代码。


所以我们今天就有了更清楚的概念:一个PCB(task_struct)<= 进程

我们也不在区分执行流到底是线程还是进程,转而把linux执行流统一称为:轻量级进程(LWP) 

linux系统中没有真正意义上的“线程”,只有 “共享资源的轻量级进程”。

值得提的是,Windows是真的有线程的专属数据结构,所以它的内核代码极其复杂。


二、线程初体验

 我们接下来写一下简单的测试代码,让大家体验一下线程的概念:

在linux中,我们一般用pthread_create函数来创建一个线程。

值得注意的是,这个函数并不是一个系统调用,而是我们glibc封装的一个函数。

他有四个参数,第一个参数是一个指针,函数成功返回后,会将新线程的 ID 写入该地址。所以我们在使用这个函数前,一般会创建一个pthread_t类型的id。

第二个参数指定线程的属性(如栈大小、调度策略等),如果是NULL,则使用默认属性。

第三个参数是一个函数指针,代表值这个线程将要执行的方法,而第四个参数表示传递给线程函数的参数。

#include <pthread.h>
#include <iostream>
#include <unistd.h>void* func(void *argv)
{while(true){std::cout<<"I am func pthread,my pid :"<<getpid()<<std::endl;   sleep(1);}
}
int main()
{pthread_t tid;int i = 100;pthread_create(&tid, nullptr, func, (void *)i);while (true){std::cout << "I am main pthread,my pid :" << getpid() << std::endl;sleep(1);}return 0;
}

在编译这个代码时应该注意,我们的编译指令应该是 g++ test.cc -o test -lpthread ,因为我们需要需要链接 pthread 库。

可以看见,如果只有一个执行流,是不能同时执行两个while循环的。 

 我们再次运行代码,通过新的bash输入以上两个命令可以查看进程运行信息。

可以看见,操作系统中叫做test的进程只有这一个,我们可以使用ps -aL来查看线程信息。

这里就多出来一个叫做LWP的东西。这个就是表示轻量级进程。

我们LWP与PID相同的,就是主执行流,如果光看PID的话,我们是区分不出来两个执行流的。所以,我们可以通过LWP来区分执行流唯一性,我们的OS真实调度时,也是用的LWP而不是PID。


三、分页式存储管理初谈

如果在没有虚拟内存和分页机制的情况下,每一个用户程序在物理内存上所对应的空间必须是连续的,如下图:

因为每一个程序的代码、数据长度都是不一样的,按照这样的映射方式,物理内存将会被分割成各种离散的、大小不同的块。结果一段时间运行后,有些程序会退出,那么他们占据的物理内存空间就会被回收,导致这些物理内存都是以很多碎片的形式存在的。

我们希望操作系统提供给用户的空间必须是连续的,但是物理内存又不连续,该怎么办呢?

所以此时虚拟内存与分页就出现了。

 我们这张图中牵涉到了页框的概念,那么什么是页框呢?

同学们,还记得我们学过的物理内存管理吗?
我们当时说过块的概念,我们可以把一定数量的扇区,划分为一个块。一个块的数据大小为4kb,也就是八个扇区。(ext2下)

我们说:块是文件系统管理数据的最小单位,一个块的大小是4kb。

这里的块是文件系统读写磁盘的最小逻辑单位,类似的,我们的一个页框的大小也是固定为4kb,他是操作系统管理物理内存的最小单位。

我们把物理内存按照一个固定的长度的页框进行分割,有时候叫做物理页。每一个页框包含一个物理页(page)。一个页的大小等于页框的大小,32位大多支持4kb,64为8kb。

我们需要区分页与页框:

页是一个数据块,可以存放在任何页框或者磁盘中,而页框是一个存储区域!

有了这种机制,CPU便并非是直接访问物理内存地址,而是通过虚拟地址空间来间接访问物理内存地址。所谓的虚拟空间,是操作系统为每一个正在执行的进程分配的一个逻辑地址。操作系统将虚拟地址空间与物理内存地址之间建立映射关系,也就是页表,这张表上记录了每一页和页框的映射关系。

总结一下,其思想就是将虚拟内存下的逻辑地址空间分为若干页,将物理内存空间划分为若干页框,通过页表就能把连续的虚拟内存,映射到若干个不同的物理内存页,就解决了碎片问题。


但是假设⼀个可用的物理内存有 4GB 的空间。按照⼀个页框的大小 4KB 进行划分4GB 的空间就是 4GB/4KB = 1048576 页框。有这么多的物理页,操作系统肯定是要将其管理起来的,操作系统需要知道哪些页正在被使用,哪些页空闲等等。
内核用 struct page 结构表示系统中的每个物理页,出于节省内存的考虑, struct page 中是用来大量的联合体union。
/* include/linux/mm_types.h */
struct page
{/* 原⼦标志,有些情况下会异步更新 */unsigned long flags;union{struct{/* 换出⻚列表,例如由zone->lru_lock保护的active_list */struct list_head lru;/* 如果最低为为0,则指向inode* address_space,或为NULL* 如果⻚映射为匿名内存,最低为置位* ⽽且该指针指向anon_vma对象*/struct address_space *mapping;/* 在映射内的偏移量 */pgoff_t index;/** 由映射私有,不透明数据* 如果设置了PagePrivate,通常⽤于buffer_heads* 如果设置了PageSwapCache,则⽤于swp_entry_t如果设置了PG_buddy,则⽤于表⽰伙伴系统中的阶*/unsigned long private;};struct{ /* slab, slob and slub */union{struct list_head slab_list; /* uses lru */struct{ /* Partial pages */struct page *next;
#ifdef CONFIG_64BITint pages;    /* Nr of pages left */int pobjects; /* Approximate count */
#elseshort int pages;short int pobjects;
#endif};};struct kmem_cache *slab_cache; /* not slob *//* Double-word boundary */void *freelist; /* first free object */union{void *s_mem;            /* slab: first object */unsigned long counters; /* SLUB */struct{                        /* SLUB */unsigned inuse : 16; /* ⽤于SLUB分配器:对象的数⽬ */unsigned objects : 15;unsigned frozen : 1;};};};...};union{/* 内存管理⼦系统中映射的⻚表项计数,⽤于表⽰⻚是否已经映射,还⽤于限制逆向映射搜索*/atomic_t _mapcount;unsigned int page_type;unsigned int active; /* SLAB */int units;           /* SLOB */};...
#if defined(WANT_PAGE_VIRTUAL)/* 内核虚拟地址(如果没有映射则为NULL,即⾼端内存) */void *virtual;
#endif /* WANT_PAGE_VIRTUAL */...
}

    系统启动时,内核会根据检测到的物理内存大小,为每个物理页框(page frame)分配一个对应的 struct page 结构体。

    在 Linux 内核中, struct page  是用于管理物理内存页(页框)的核心数据结构,每个物理页都对应一个这样的结构体。该结构体包含几个关键字段:

    1. flags:这是一个多功能的标志位字段,用于记录页的各种状态。每位代表一种独立的状态,可同时表示32种不同的状态(定义在<linux/page-flags.h>中)。其中重要的标志位包括:

      • PG_locked:表示页是否被锁定在内存中

      • PG_uptodate:表示页数据已从块设备正确读取

    2. _mapcount:这个计数器记录有多少个页表项指向该物理页,即页的引用计数。当值为-1时,表示内核不再引用该页,可以被重新分配使用。

    3. virtual:存储页的虚拟地址。对于常规内存,这就是页在虚拟地址空间中的映射地址;而对于高端内存(不永久映射到内核地址空间的部分),此字段为NULL,需要时再动态映射。

    值得注意的是, struct page 描述的是物理页而非虚拟页。以典型的4KB页大小和4GB物理内存为例,系统需要管理约1百万个物理页(4GB/4KB=1M)。假设每个 struct page 占用40字节,则总内存开销约为40MB(1M*40B),仅占系统总内存的1%,这个管理开销是相当合理的。


    我们之所以扯到这里,主要是想帮助大家理解虚拟地址空间与线程之间的关系,同一进程的所有线程共享同一个 mm_struct(内存描述符),因此它们看到的是完全相同的虚拟地址空间.

    我们可以把struct page当做数组来理解,Linux 内核通过一个名为 mem_map 的全局数组(元素类型为 struct page)管理所有物理页。而当作数组,就有了下标,我们就可以快速转化为物理地址,一个页框是4kb,所以物理地址就等于=下标*4kb


    总结:

    由于时间原因,我们今天就讲到这里。

    但是我们的分页式存储管理还是没有讲完,我们还没用深刻理解页表的分级存储。

    所以明天我们将会讲解页表的分级存储,之后会继续讲解线程的概念!

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
    如若转载,请注明出处:http://www.pswp.cn/pingmian/89184.shtml
    繁体地址,请注明出处:http://hk.pswp.cn/pingmian/89184.shtml
    英文地址,请注明出处:http://en.pswp.cn/pingmian/89184.shtml

    如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    windows利用wsl安装qemu

    首先需要安装wsl,然后在swl中启动一个子系统。这里我启动一个ubuntu22.04。 接下来的操作全部为在子系统中的操作。 检查虚拟化 在开始安装之前,让我们检查一下你的机器是否支持虚拟化。 要做到这一点,请使用以下命令: sean@DESKTOP-PPNPJJ3:~$ LC_ALL=C lscpu | grep …

    如何使用 OpenCV 打开指定摄像头

    在计算机视觉应用中&#xff0c;经常需要从特定的摄像头设备获取视频流。例如&#xff0c;在多摄像头环境中&#xff0c;当使用 OpenCV 的 cv::VideoCapture 类打开摄像头时&#xff0c;如果不指定摄像头的 ID&#xff0c;可能会随机打开系统中的某个摄像头&#xff0c;或者按照…

    JAVA面试宝典 -《分布式ID生成器:Snowflake优化变种》

    &#x1f680; 分布式ID生成器&#xff1a;Snowflake优化变种 一场订单高峰&#xff0c;一次链路追踪&#xff0c;一条消息投递…你是否想过&#xff0c;它们背后都依赖着一个“低调却关键”的存在——唯一ID。本文将带你深入理解分布式ID生成器的核心原理与工程实践&#xff0…

    苹果的机器学习框架将支持英伟达的CUDA平台

    苹果专为Apple Silicon设计的MLX机器学习框架即将迎来CUDA后端支持&#xff0c;这意义重大。原因如下。 这项开发工作由GitHub开发者zcbenz主导&#xff08;据AppleInsider报道&#xff09;&#xff0c;他于数月前开始构建CUDA支持的原型。此后他将项目拆分为多个模块&#xff…

    golang语法-----变量、常量

    变量1、声明与初始化&#xff08;1&#xff09;标准声明 (先声明&#xff0c;后赋值)var age int // 声明一个 int 类型的变量 age&#xff0c;此时 age 的值是 0 fmt.Println(age) // 输出: 0age 30 // 给 age 赋值 fmt.Println(age) // 输出: 30//int 的零…

    Jenkins+Docker(docker-compose、Dockerfile)+Gitee实现自动化部署

    项目目录结构 project-root/ ├── pom.xml ├── docker │ ├── copy.sh │ ├── file │ │ ├── jar │ │ │ └── 存放执行copy.sh以后jar包的位置 │ │ └── Dockerfile │ └── docker-compose.yml ├── docker-only-test │ ├─…

    TASK01【datawhale组队学习】地瓜机器人具身智能概述

    https://github.com/datawhalechina/ai-hardware-robotics 参考资料地址 具身智能&#xff08;Embodied AI&#xff09; 具身智能 智能的大脑 行动的身体。 比例&#xff08;Proportional&#xff09;、积分&#xff08;Integral&#xff09;、微分&#xff08;Derivative&a…

    uni-app 配置华为离线推送流程

    1、首先需要创建一个华为开发者账号&#xff0c;我这个是个人开发账号 申请开发者账号 2、去AppGallery Connect登陆我们刚刚创建好的账号&#xff0c;点击页面的APP进入到如下3 AppGallery Connect ‎‎‎‎‎ ‎3、在AppGallery Connect 网站中创建一个 Android应用、点击…

    当下主流摄像头及其核心参数详解

    &#x1f4d6; 推荐阅读&#xff1a;《Yocto项目实战教程:高效定制嵌入式Linux系统》 &#x1f3a5; 更多学习视频请关注 B 站&#xff1a;嵌入式Jerry 当下主流摄像头及其核心参数详解 一、摄像头发展概述 摄像头作为现代智能设备&#xff08;如手机、安防、车载、工业等&am…

    下载了docker但是VirtualBox突然启动不了了

    今天下docker后发现 eNSP 路由器&#xff0c;防火墙启动不了了去virtualbox检查的时候发现无法启动&#xff1a;报错&#xff1a;不能为虚拟电脑 AR_Base 打开一个新任务.Raw-mode is unavailable courtesy of Hyper-V. (VERR_SUPDRV_NO_RAW_MODE_HYPER_V_ROOT).返回代码: E_F…

    C++11之lambda表达式与包装器

    lambda与包装器lambda语法捕捉列表lambda的应用lambda的原理包装器functionbindlambda语法 lambda 表达式本质是⼀个匿名函数对象&#xff0c;跟普通函数不同的是他可以定义在函数内部。 lambda 表达式语法使⽤层⽽⾔没有类型&#xff0c;所以我们⼀般是⽤auto或者模板参数定义…

    有痛呻吟!!!

    XiTuJueJin:YYDS 分盘 有些平台吃相太难看&#xff0c;同样的文章&#xff0c;我还先选择现在这里发布&#xff0c;TMD. 莫名其妙将我的文章设置为仅VIP可见&#xff0c;还是今天才发现&#xff0c;之前只是将一两篇设置为仅VIP可见&#xff0c;今天突然发现这种标识的都自动…

    2025年7-9月高含金量数学建模竞赛清单

    2025年7-9月高含金量数学建模竞赛 ——“高教社杯”国赛 & “华为杯”研赛作为过来人&#xff0c;真心觉得参加数学建模比赛是我本科阶段做的最值的事之一。 它锻炼的那种把实际问题转化成模型求解的思维&#xff0c;对做研究、写论文甚至以后工作都帮助很大。我当时就是靠…

    SpringBoot为什么使用new RuntimeException() 来获取调用栈?

    为什么不直接使用 Thread.currentThread().getStackTrace()&#xff1f;这确实看起来有点“奇怪”或者“绕”&#xff0c;但其实这是 Java 中一种非常常见、巧妙且合法的技巧&#xff0c;用于在运行时动态获取当前代码的调用栈信息。Spring 选择用 new RuntimeException().getS…

    小白成长之路-haproxy负载均衡

    文章目录一、概述1、HAProxy简介2、HAProxy特点和优点&#xff1a;3、HAProxy保持会话的三种解决方法4、HAProxy的balance 8种负载均衡算法1&#xff09;RR&#xff08;Round Robin&#xff09;2&#xff09;LC&#xff08;Least Connections&#xff09;3&#xff09;SH&#…

    Kafka 与 RocketMQ 消息确认机制对比分析

    目录 生产者消息确认机制 Kafka 生产者 ACK 机制 RocketMQ 生产者确认机制 消费者消息确认机制 Kafka 消费者确认机制 RocketMQ 消费者确认机制 核心差异对比 选型建议 消息确认机制是分布式消息中间件的核心功能之一&#xff0c;它直接关系到消息传递的可靠性和系统性能…

    C/C++---rdbuf()函数

    在C中&#xff0c;rdbuf() 是I/O流库中的一个核心成员函数&#xff0c;主要用于访问和操作流对象的缓冲区。这个函数在底层数据处理、流重定向以及自定义流操作等场景中应用广泛。下面将从多个方面详细解析 rdbuf() 函数。 基本概念与函数原型 rdbuf() 是 std::basic_ios 类的成…

    【LLM】从零到一构建一个小型LLM--MiniGPT

    从零到一构建一个小型LLM (Small Language Model)暂时起名为MiniGPT。这个模型将专注于因果语言建模 (Causal Language Modeling)&#xff0c;这是许多现代LLM&#xff08;如GPT系列&#xff09;的核心预训练任务。模型设计&#xff1a; 我们设计的模型是一个仅包含解码器 (Dec…

    网络安全威胁下的企业困境与破局技术实践

    前言&#xff1a;网络安全威胁下的企业困境 在数字化转型的浪潮中&#xff0c;企业对信息技术的依赖程度日益加深&#xff0c;但随之而来的网络安全威胁也愈发严峻。据统计&#xff0c;全球每年因网络安全事件造成的经济损失高达数万亿美元&#xff0c;其中中小企业更是成为了网…

    [RAG system] 信息检索器 | BM25 Vector | Pickle格式 | HybridRetriever重排序

    第六章&#xff1a;信息检索器 在上一章中&#xff0c;我们成功完成了知识库摄入流程。这是巨大的进步~ 我们精心准备了文档"块"&#xff08;类似独立的索引卡&#xff09;&#xff0c;并将其存储在两套智能归档系统中&#xff1a;向量数据库&#xff08;用于基于含…