欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > 【Linux】初识线程

【Linux】初识线程

2025/4/8 10:00:00 来源:https://blog.csdn.net/s_little_monster/article/details/145716007  浏览:    关键词:【Linux】初识线程

在这里插入图片描述

个人主页~


初识线程

  • 一、线程的概念
    • 线程和进程的切换问题
  • 二、再谈进程地址空间
    • 1、多级页表
    • 2、二级页表
      • (一)外层页表(页目录)
      • (二)内层页表(二级页表)
      • (三)页内偏移
  • 三、线程创建
    • 1、库函数
    • 2、测试

一、线程的概念

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位

  • 定义与基本特征

    • 轻量级实体:线程是比进程更小的可独立运行的基本单位,也被称为轻量级进程,一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等,但每个线程都有自己独立的程序计数器、寄存器和栈等
    • 并发执行:同一进程内的多个线程可以并发执行,操作系统会为每个线程分配CPU时间片,使得它们在宏观上看起来是同时运行的,通过这种方式,线程可以实现程序的并发处理,提高系统资源的利用率和程序的执行效率
    • 共享与独立:线程可以共享进程的大部分资源,这使得线程之间的通信和数据共享相对容易实现,同时,每个线程又有自己独立的执行路径和上下文,能够独立地进行运算调度,互不干扰
  • 线程的状态

    • 就绪状态:线程已经具备了运行的条件,等待操作系统分配CPU资源,一旦获得CPU时间片,就可以立即执行
    • 运行状态:线程正在CPU上执行,正在执行相应的任务代码
    • 阻塞状态:线程因等待某些事件的发生而暂时无法继续执行,比如等待I/O操作完成、等待获取锁、等待其他线程的通知等,当所等待的事件发生后,线程会从阻塞状态转换为就绪状态,重新进入就绪队列等待CPU调度
  • 作用与优势

    • 提高程序响应性:在图形界面应用程序中,通常会有一个主线程用于处理界面的显示和更新,同时还可以创建其他线程来处理后台任务,如文件读取、网络数据下载等,这样,即使后台任务比较耗时,也不会阻塞主线程,从而保证界面能够及时响应用户的操作,提高用户体验
    • 充分利用多核处理器:在多核处理器的环境下,多个线程可以同时在不同的核心上运行,实现真正的并行处理,从而充分发挥多核处理器的性能优势,提高程序的执行效率
    • 简化程序结构:将一个复杂的任务分解为多个线程来执行,可以使程序的结构更加清晰,每个线程负责一个相对独立的子任务,便于代码的编写、调试和维护
  • 与进程的关系

    • 包含关系:进程是资源分配的基本单位,而线程是进程内的执行单元,一个进程可以包含一个或多个线程,线程是进程的组成部分,不能独立于进程而存在
    • 资源共享与独立:进程拥有独立的地址空间和系统资源,不同进程之间的资源相互隔离,而同一进程内的线程共享进程的资源,但每个线程有自己独立的栈空间和寄存器等
    • 调度与切换:进程的调度和切换相对复杂,需要保存和恢复大量的上下文信息,开销较大,而线程的调度和切换相对简单,因为线程共享进程的资源,所以在切换时只需保存和恢复少量的寄存器等信息,开销较小
  • 线程的缺点

    • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器,如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,因为这里增加了额外的同步和调度的开销,但是可用的资源是不变的
    • 健壮性降低:一个多线程的程序中,多线程共享的资源是占大部分的,如果线程之间时间分配上出现了细微的偏差或者共享了不该共享的变量就很有可能会造成不良影响,所以进程间缺乏保护
    • 线程异常:线程一旦出现除零或野指针这样的错误,导致线程崩溃,进程也会跟着崩溃

在操作系统中,线程是实现并发编程和提高系统性能的重要手段,广泛应用于各种类型的程序开发中

这里我们举一个简单的例子帮助大家理解,我们都知道,盖一栋大楼需要土木的同学画图纸,建筑工人打地基,吊车工人运送原料,设计的同学设计室内,货车司机运水泥等等,他们共同在做的就是盖大楼,但他们每个人都在做自己的事情,这些事情是无法分开的,但合起来就是盖楼的一个过程,这就是我们进程和线程之间的关系,每个人做的工作都是线程,所有人做的工作合起来就是进程

我们以前说的进程就是单线程进程,开个玩笑说一个人把楼给盖起来的

所以我们要理解的是,进程和线程真正的概念,进程是承担分配系统资源的基本实体,线程是我们进程内部的执行流资源,简单来说由进程来帮我们申请空间分配资源,线程在进程分配的资源中进行线程的执行

线程和进程的切换问题

线程作为一个轻量级的进程,在CPU中也是要进行切换的,一个进程中的线程共享这个进程的时间片,描述线程的数据结构也是task_struct结构,当然其中有标识它是线程的标识符id,在我们CPU执行进程的时候,会将缓存数据加载到寄存器cache中,在线程进行切换的时候,由于共享数据的特性,cache中的数据不用进行更换,效率高,时间片结束后,进程切换时,才进行cache数据更换

二、再谈进程地址空间

今天我们借线程这个话题再谈进程地址空间,线程是进程的细分,这个细分也要由页表找到物理内存,它是怎么找到的呢,这里我们就不得不讲解一下页表的实现结构了

实际上,页表并不像我们前面说的一列是虚拟内存地址,另一列是物理内存地址,它们一一对应,这只是我们抽象出来的,忽略了页表自身结构,而保留它的功能的一个抽象的概念,有一个很简单的问题,如果页表的实现是这样的话,以32位计算机为例,它的一个地址就需要页表中4个字节来存储,而我们的虚拟地址中有页的概念,一页4KB,虚拟内存一共有多少页那页表就有多少虚拟地址,已知虚拟内存共4GB,也就是4*1024*1024÷4个地址,那光虚拟内存就要4*1024*1024bytes = 4MB连续的物理内存,并且大部分进程实际使用的虚拟地址空间只是其中一小部分,这就意味着页表中大量的页表项是无效的,却依然占用着物理内存,造成了极大的浪费,所以我们就要引出真正的页表结构了

1、多级页表

我们页表的结构就是多级页表,上面所说的这种页表结构叫做一级页表,这样的页表我们也看到了,会占用大量连续的物理内存,并且很多情况下大部分页表项可能都是无效的,造成内存浪费,是不可行的,多级页表结构通过将页表分级,仅在需要时才分配和使用各级页表,从而有效减少内存占用

2、二级页表

虚拟地址需要32个比特位来存储,我们将这32个比特位分开存储,前十个,中间十个,最后十二个共三个部分,第一部分叫做外层页表,第二部分叫做内层页表,第三部分叫做页内偏移

(一)外层页表(页目录)

前十个比特位叫做外层页表(页目录),页目录中的每个条目都指向相对应的一个内层页表,把前十位看做一个十进制数字,这个数字的大小就是页目录的下标,页目录占用空间的大小为2^10*4 bytes = 4KB,页目录的条目数和内层页表的条目数相同,是一一对应的

(二)内层页表(二级页表)

中间十个比特位叫做内层页表(二级页表),二级页表中的每个条目都指向物理地址空间的一个页,把中间十位看做一个十进制数字,这个数字的大小,就是该地址在物理地址空间的页码下标,一个二级页表的大小也是4KB,但是二级页表一般都是不全的,申请才有,不申请就没有

(三)页内偏移

最后十二个比特位叫做页内偏移(偏移量),前面已经指向了物理地址空间的一个页了,我们知道页的大小就是4KB,也就是2^12字节,而最后剩下十二个比特位正是用来形容它的,我们知道每个字节都有一个地址,这里页内偏移就是地址相对于这个页起始地址的偏移量

在这里插入图片描述

三、线程创建

1、库函数

pthread_create用于创建一个新线程

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

返回值:成功返回0,失败返回非零错误码

thread:这是一个指向 pthread_t 类型变量的指针,pthread_t 是一个线程标识符类型,用于唯一标识一个线程,当 pthread_create 成功创建一个新线程时,会将该线程的标识符存储在 *thread 所指向的内存位置

attr:这是一个指向 pthread_attr_t类型的常量指针,用于指定新线程的属性,如果将其设置为 NULL,则表示使用默认的线程属性,pthread_attr_t 类型定义了一系列线程的属性,如线程的栈大小、调度策略、分离状态等。可以使用 pthread_attr_init 函数初始化一个 pthread_attr_t 对象,并使用其他相关函数来设置具体的属性,但是我们一般用不到这些属性,我们线程用来就是实现多任务调度的

start_routine:这是一个函数指针,指向新线程开始执行时要调用的函数,该函数必须接受一个 void * 类型的参数,并返回一个 void * 类型的值,新线程从这个函数开始执行,直到该函数返回或者线程被取消

arg:这是传递给 start_routine 函数的参数,由于 start_routine 函数的参数类型是 void*,因此可以将任意类型的数据指针转换为 void* 类型传递给该函数,在 start_routine 函数内部,需要将其转换回原来的类型,这个我们下一篇文章再谈,这里就简单使用一下

2、测试

下面是一个简单的测试线程创建的代码主线程和新线程的任务都是循环打印自己的pid

#include <iostream>
#include <pthread.h>
#include <unistd.h>void *threadRun(void* args)
{while(1){std::cout << "new thread: " << getpid() << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, nullptr);while(1){std::cout << "main thread: " << getpid() << std::endl;sleep(1);}
}

在这里插入图片描述
注意这里的makefile文件,我们将所对应的库写上了,这个库叫做POSIX线程库,它并不是操作系统原生自带,但在Linux系统中都会有这个库,因为Linux内核提供的clone()函数调用更加复杂,clone是Linux内核用来创建轻量级进程的函数,我们的pthread_create()就是基于它封装的,使用方便

简单看一下clone函数,确实是要比pthread_create()要难用的

#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, .../* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

在这里插入图片描述
我们查看打印结果,虽然因为进程调度的原因偶尔会出现重叠乱打的情况,但是大部分时间还是正常的,从这个乱打的情况下我们可以发现,线程之间是不存在同步和互斥的概念的,我们还可以通过打印结果发现一个特点,它们的进程pid是相同的,即一个进程中的多个线程共享一个进程pid

在进程执行的过程中,我们可以通过命令ps -aL查看线程资源,我们线程也有自己的唯一标识符LWP表示light weight process也就是轻量级进程,就是线程,我们发现两个线程的PID相同这我们上面说了,其中一个线程LWPPID相同,这个线程就是主线程
在这里插入图片描述


今日分享就到这了~

在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词