Linux线程实现

前言

前面提到进程和线程的区别,进程是资源分配的基本单位,线程是程序执行的基本单位。线程都属于某个进程,而同一个进程下的不同线程分别有共享和独享的数据,这里再列举一下:

同一进程内的所有线程除了共享全局变量外还共享:

  • 进程指令
  • 大多数数据
  • 打开的文件(即描述符)
  • 信号处理函数和信号处置
  • 当前工作目录
  • 用户ID和组ID

不过每个线程有各自的:

  • 线程ID
  • 寄存器集合,包括程序计数器和栈指针
  • errno
  • 信号掩码
  • 优先级

linux是遵循POSIX标准的操作系统,所以linux也需要提供遵循POSIX标准的线程实现。而最初linux系统中的线程机制则是LinuxThreads,在2.6版本之后又增加了NPTL(Native POSIX Thread Library)。

内核线程和用户线程

对于线程的实现机制来说,通常可以选择在内核内或者内核外实现,这两种方式的区别在于线程是在核内还是核外调度。核内调度更利于并发使用多处理器的资源,内核可以将同一个进程的不同线程调度到不同处理器上执行,当某个线程阻塞时,内核可以将处理器调度到同一个进程的另一个线程。而核外调度的上下文切换开销更低,因为线程的切换不用陷入内核态。

进程-线程模型

当内核既支持进程也支持线程时,就可以实现线程-进程的"多对多"模型,即一个进程的某个线程由核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行。这样既可满足多处理机系统的需要,也可以最大限度的减小调度开销。

在内核外实现的线程又可以分为"一对一"、"多对一"两种模型,前者用一个内核进程对应一个线程,将线程调度等同于进程调度,交给内核完成,而后者则完全在核外实现多线程,调度也在用户态完成。后者就是前面提到的单纯的用户级线程模型的实现方式,显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,但同时因为内核信号都是以进程为单位的,因而无法定位到线程,所以这种实现方式不能用于多处理器系统。

linux的轻量级进程

linux内核只提供了轻量进程的支持,限制了更高效的线程模型的实现,但linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。目前linux的线程机制都采用的线程-进程"一对一"模型,调度交给内核,而在用户级实现一个包括信号处理在内的线程管理机制。

linux内核在2.0.x版本就已经实现了轻量进程,应用程序可以通过一个统一的clone系统调用接口,用不同的参数指定创建轻量进程还是普通进程。在内核中,clone调用经过参数传递和解释后会调用do_fork,这个核内函数同时也是fork、vfork系统调用的最终实现:

intdo_fork(unsignedlongclone_flags,unsignedlongstack_start,structpt_regs*regs,unsignedlongstack_size);

在do_fork中,不同的clone_flags将导致不同的行为(共享不同的资源),下面列举几个flag的作用。

CLONE_VM 如果do_fork时指定了CLONE_VM开关,创建的轻量级进程的内存空间将会和父进程指向同一个地址,即创建的轻量级进程将与父进程共享内存地址空间。

CLONE_FS

如果do_fork时指定了CLONE_FS开关,对于轻量级进程则会与父进程共享相同的所在文件系统的根目录和当前目录信息。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前目录、根目录等信息都将直接影响到其他线程。

CLONE_FILES

如果do_fork时指定了CLONE_FILES开关,创建的轻量级进程与父进程将会共享已经打开的文件。这一共享使得任何线程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程。

CLONE_SIGHAND

如果do_fork时指定了CLONE_FILES开关,轻量级进程与父进程将会共享对信号的处理方式。也就是说,子进程与父进程的信号处理方式完全相同,而且可以相互更改。

尽管linux支持轻量级进程,但并不能说它就支持内核线程,因为linux的"线程"和"进程"实际上处于一个调度层次,共享一个进程标识符空间,这种限制使得不可能在linux上实现完全意义上的POSIX线程机制,因此众多的linux线程库实现尝试都只能尽可能实现POSIX的绝大部分语义,并在功能上尽可能逼近。

LinuxThreads的线程机制

LinuxThreads是linux平台上使用过的一个线程库。它所实现的就是基于内核轻量级进程的"一对一"线程模型,一个线程实体对应一个核心轻量级进程,而线程之间的管理在核外函数库中实现。对于LinuxThreads,它使用(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND)参数来调用clone创建"线程",表示共享内存、共享文件系统访问计数、共享文件描述符表,以及共享信号处理方式。

管理线程

LinuxThreads最初的设计相信相关进程之间的上下文切换速度很快,因此每个内核线程足以处理很多相关的用户级线程。LinuxThreads非常出名的一个特性就是管理线程(manager thread)。在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create创建一个线程的时候就会创建并启动管理线程。

在一个进程空间内,管理线程与其他线程之间通过一对"管理管道(manager_pipe[2])"来通讯,该管道在创建管理线程之前创建,在成功启动了管理线程之后,管理管道的读端和写端分别赋给两个全局变量__pthread_manager_reader和__pthread_manager_request,之后,每个用户线程都通过__pthread_manager_request向管理线程发请求,但管理线程本身并没有直接使用__pthread_manager_reader,管道的读端(manager_pipe[0])是作为__clone()的参数之一传给管理线程的,管理线程的工作主要就是监听管道读端,并对从中取出的请求作出反应。

管理线程在进行一系列初始化工作后,进入while(1)循环。在循环中,线程以2秒为timeout查询(__poll())管理管道的读端。在处理请求前,检查其父线程是否已退出,如果已退出就退出整个进程。如果有退出的子线程需要清理,则进行清理。然后才是读取管道中的请求,根据请求类型执行相应操作(switch-case)。

每个LinuxThreads线程都同时具有线程id和进程id,其中进程id就是内核所维护的进程号,而线程id则由LinuxThreads分配和维护。

LinuxThreads的局限性

LinuxThreads的设计通常都可以很好地工作;但是在压力很大的应用程序中,它的性能、可伸缩性和可用性都会存在问题。下面让我们来看一下LinuxThreads设计的一些局限性:

  • 进程id问题:linux内核并不支持真正意义上的线程,LinuxThreads是用与普通进程具有同样内核调度视图的轻量级进程来实现线程支持的。这些轻量级进程拥有独立的进程id,在进程调度、信号处理、IO等方面享有与普通进程一样的能力。在源码阅读者看来,就是linux内核的clone没有实现对CLONE_PID参数的支持。按照POSIX定义,同一进程的所有线程应该共享一个进程id和父进程id,这在目前的"一对一"模型下是无法实现的。
  • 管理线程容易成为瓶颈,这是这种结构的通病;同时,管理线程又负责用户线程的清理工作,因此,尽管管理线程已经屏蔽了大部分的信号,但一旦管理线程死亡,用户线程就不得不手工清理了,而且用户线程并不知道管理线程的状态,之后的线程创建等请求将无人处理。
  • 信号用来实现同步原语,这会影响操作的响应时间。另外,将信号发送到主进程的概念也并不存在。因此,这并不遵守POSIX中处理信号的方法。
  • LinuxThreads中对信号的处理是按照每线程的原则建立的,而不是按照每进程的原则建立的,这是因为每个线程都有一个独立的进程ID。由于信号被发送给了一个专用的线程,因此信号是串行化的——也就是说,信号是透过这个线程再传递给其他线程的。这与POSIX标准对线程进行并行处理的要求形成了鲜明的对比。例如,在LinuxThreads中,通过kill()所发送的信号被传递到一些单独的线程,而不是集中整体进行处理。这意味着如果有线程阻塞了这个信号,那么LinuxThreads就只能对这个线程进行排队,并在线程开放这个信号时在执行处理,而不是像其他没有阻塞信号的线程中一样立即处理这个信号。
  • 由于LinuxThreads中的每个线程都是一个进程,因此用户和组ID的信息可能对单个进程中的所有线程来说都不是通用的。例如,一个多线程的setuid()/setgid()进程对于不同的线程来说可能都是不同的。
  • 由于每个线程都是一个单独的进程,因此/proc目录中会充满众多的进程项,而这实际上应该是线程。
  • 由于每个线程都是一个进程,因此对每个应用程序只能创建有限数目的线程。
  • 由于计算线程本地数据的方法是基于堆栈地址的位置的,因此对于这些数据的访问速度都很慢。另外一个缺点是用户无法可信地指定堆栈的大小,因为用户可能会意外地将堆栈地址映射到本来要为其他目的所使用的区域上了。按需增长(growondemand)的概念(也称为浮动堆栈的概念)是在2.4.10版本的linux内核中实现的。在此之前,LinuxThreads使用的是固定堆栈。

NPTL

NPTL(Native POSIX Thread Library)是linux线程的一个新实现,它克服了LinuxThreads的缺点,同时也符合POSIX的需求。与LinuxThreads相比,它在性能和稳定性方面都提供了重大的改进。与LinuxThreads一样,NPTL也实现了一对一的模型。

NPTL出现的一部分原因是对LinuxThreads进行改进,它设计目标如下:

  • 这个新线程库应该兼容POSIX标准。
  • 这个线程实现应该在具有很多处理器的系统上也能很好地工作。
  • 为一小段任务创建新线程应该具有很低的启动成本。
  • NPTL线程库应该与LinuxThreads是二进制兼容的。
  • 这个新线程库应该可以利用NUMA支持的优点。

NPTL的优点

NPTL总的来说采用了LinuxThreads类似的解决办法,内核看到的依然是一个进程,新线程是通过clone()系统调用产生的。与LinuxThreads相比,NPTL具有很多优点:

  • NPTL没有使用管理线程。管理线程的一些需求,例如向作为进程一部分的所有线程发送终止信号,是并不需要的;因为内核本身就可以实现这些功能。内核还会处理每个线程堆栈所使用的内存的回收工作。它甚至还通过在清除父线程之前进行等待,从而实现对所有线程结束的管理,这样可以避免僵尸进程的问题。
  • 由于NPTL没有使用管理线程,因此其线程模型在NUMA和SMP系统上具有更好的可伸缩性和同步机制。
  • 使用NPTL线程库与新内核实现,就可以避免使用信号来对线程进行同步了。为了这个目的,NPTL引入了一种名为futex的新机制。futex在共享内存区域上进行工作,因此可以在进程之间进行共享,这样就可以提供进程间POSIX同步机制。我们也可以在进程之间共享一个futex。这种行为使得进程间同步成为可能。实际上,NPTL包含了一个PTHREAD_PROCESS_SHARED宏,使得开发人员可以让用户级进程在不同进程的线程之间共享互斥锁。
  • 由于NPTL是POSIX兼容的,因此它对信号的处理是按照每进程的原则进行的;getpid()会为所有的线程返回相同的进程ID。例如,如果发送了SIGSTOP信号,那么整个进程都会停止;使用LinuxThreads,只有接收到这个信号的线程才会停止。这样可以在基于NPTL的应用程序上更好地利用调试器,例如GDB。
  • 由于在NPTL中所有线程都具有一个父进程,因此对父进程汇报的资源使用情况(例如CPU和内存百分比)都是对整个进程进行统计的,而不是对一个线程进行统计的。
  • NPTL线程库所引入的一个实现特性是对ABI(应用程序二进制接口)的支持。这帮助实现了与LinuxThreads的向后兼容性。这个特性是通过使用LD_ASSUME_KERNEL实现的。

futex

futex(Fast Userspace muTexes)意为快速用户区互斥,它是linux提供的一种同步(互斥)机制,特点是对于条件的判断是发生在用户空间的,在竞争不激烈的情况下能有更好的性能表现。futex在2.6.x系列稳定版内核中出现。

futex由一块能够被多个进程共享的内存空间(一个对齐后的整型变量)组成;这个整型变量的值能够通过汇编语言调用CPU提供的原子操作指令来增加或减少,并且一个进程可以等待直到那个值变成正数。Futex 的操作几乎全部在用户空间完成;只有当操作结果不一致从而需要仲裁时,才需要进入操作系统内核空间执行。这种机制允许使用 futex 的锁定原语有非常高的执行效率:由于绝大多数的操作并不需要在多个进程之间进行仲裁,所以绝大多数操作都可以在应用程序空间执行,而不需要使用(相对高代价的)内核系统调用。

粉丝福利, 免费领取C/C++ 开发学习资料包、技术视频/项目代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发/音视频开发/Qt开发/游戏开发/Linuxn内核等进阶学习资料和最佳学习路线)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

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

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

相关文章

Python位操作指南:从基础到应用

前言 位操作允许直接在二进制层面上直接操作整数的各个位,使用位操作解决问题能降低很多时间和空间复杂度,以很低的成本很优雅的解决问题,不过有着一定的学习成本。 正文 负数和二进制表示 知识补充: 在计算机中,…

LeetCode-统计完全连通分量的数量

题目要求: 给你一个整数 n 。现有一个包含 n 个顶点的 无向 图,顶点按从 0 到 n - 1 编号。给你一个二维整数数组 edges 其中 edges[i] [ai, bi] 表示顶点 ai 和 bi 之间存在一条 无向 边。 返回图中 完全连通分量 的数量。 如果在子图中任意两个顶点…

ChatGPT引领量化交易革命:AI在金融创新的浪潮中崭露头角

随着科技的飞速发展,金融领域正迎来一场前所未有的创新浪潮。在这场变革中,ChatGPT凭借其卓越的自然语言处理能力和深度学习能力,正引领量化交易进入新时代。 量化交易,作为现代金融领域的一种重要交易方式,依赖于复杂的数学模型和大量的历史数据来制定交易策略。然而,传…

揭秘速成软件书:彩虹之下的真相

在这个信息爆炸的时代,我们常常被诱惑性的标题所吸引:“三天掌握Python编程”,“一周精通Photoshop”,书架上堆满了各种各样的速成指南,这些声称能迅速提升技能的书籍,真的能做到它们所承诺的吗&#xff1f…

C++与C语言

C之所以是C,和面向过程的C语言相比,它加了一个类,还有一个是模板。 引入 C语言这种面向过程的编译语言可以将待解的问题分解成若干个子问题,面向对象程序设计则是建立在结构化程序设计方法的基础上,完全避免了结构化程…

前任在代码里下毒,支付下单居然没加幂等?

首先蜗牛和大家从以下几个方面好好剖析一下接口幂等吧。 什么是接口幂等 比较专业的术语:其任意多次执行所产生的影响均与第一次执行的影响相同。 也就是多次调用的情况下,接口最终得到的结果是一致的。 那么为什么需要幂等呢? 那么哪些接…

中科院自动化所实习总结(完)

实习单位 中国科学院自动化所 工作内容 项目涉密,不便介绍 负责内容 负责完善文档,画流程图,UML类图之类的写小模块的代码 实习感悟 大概的整个过程 其实在这段实习中,我得到的最多的并不是技术上的成长,而是业…

数据结构03:栈、队列和数组 队习题01[C++]

考研笔记整理~🥝🥝 之前的博文链接在此:数据结构03:栈、队列和数组_-CSDN博客~🥝🥝 本篇作为链表的代码补充,供小伙伴们参考~🥝🥝 第1版:王道书的课后习题…

实战-后台管理系统SQL注入漏洞

对于edu来说,是新人挖洞较好的平台,本次记录一次走运的捡漏0x01 前景 在进行fofa盲打站点的时候,来到了一个后台管理处看到集市二字,应该是edu站点 确认目标身份(使用的quake进行然后去ipc备案查询) 网…

Qt实现Kermit协议(一)

1 概述 Kermit文件运输协议提供了一条从大型计算机下载文件到微机的途径。它已被用于进行公用数据传输。 其特性如下: Kermit文件运输协议是一个半双工的通信协议。它支持7位ASCII字符。数据以可多达96字节长度的可变长度的分组形式传输。对每个被传送分组需要一个确认。Kerm…

LeetCode刷题笔记之hot 100(二)

1. 322【零钱兑换】- 动态规划 题目: 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回…

关于视场角,你需要知道这些!

视场角在光学工程中又称视场,视场角的大小决定了光学仪器的视野范围。视场角又可用FOV(Field of view)表示,其与焦距的关系如下:像高 EFL*tan (半FOV);EFL为焦距;FOV为视场角。即以入瞳位置为顶…

一个包一条命令,我实现了对整个前端项目代码的校验

在现代前端开发中,代码校验与风格统一不仅是良好编程习惯的体现,更是提升项目质量、保障代码可维护性与减少潜在bug的关键环节。然而,面对诸如ESLint、Commitlint、Stylelint等多样化的校验工具,以及针对React、Vue等不同前端框架…

笔记本电脑上部署LLaMA-2中文模型

尝试在macbook上部署LLaMA-2的中文模型的详细过程。 (1)环境准备 MacBook Pro(M2 Max/32G); VMware Fusion Player 版本 13.5.1 (23298085); Ubuntu 22.04.2 LTS; 给linux虚拟机分配8*core CPU 16G RAM。 我这里用的是16bit的量化模型,…

java线程(一)--进程,多线程,synchronized和lock锁,JUC,JUnit

Java线程入门 单核CPU和多核CPU的理解 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过&#xf…

LeetCode226:反转二叉树

题目描述 给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。 解题思想 使用前序遍历和后序遍历比较方便 代码 class Solution { public:TreeNode* invertTree(TreeNode* root) {if (root nullptr) return root;swap(root->left, root…

nginx 常用功能

添加白名单配置 if ($clientRealIp ~ "192.157.34.245|17.213.126.21") {rewrite ^.*$ /403.html last;break; } 添加站点配置信息 nginx.conf 文件最后一行添加 并新建vhost 目录 include /usr/local/nginx/conf/vhost/*.conf;include vhost/*.conf;

什么是ISP住宅IP?相比于普通IP它的优势是什么?

什么是ISP住宅IP? ISP住宅IP是指由互联网服务提供商(ISP)分配给住宅用户的IP地址。它是用户在家庭网络环境中连接互联网的标识符,通常用于上网浏览、数据传输等活动。ISP住宅IP可以是动态分配的,即每次连接时都可能会…

【DevOps工具篇】 OpenLDAP的LDAP服务器(slapd)是什么?

目录 OpenLAP的LDAP服务器(slapd)是什么基本功能安全性管理性可靠性和可扩展性调优OpenLDAP的服务器基本功能简单身份验证和SASL身份验证LDAP模式OpenLDAP服务器管理LDAP服务器配置LDAP数据备份和还原slapcatslapaddslapindex

C++类复习

C类 1. 类内成员函数隐式声明为inline class Str {int x;int y 3; public:inline void fun(){std::cout<<"pf,yes!"<<std::endl;} };这段代码不会报错&#xff0c;但是类内的成员函数隐式声明为inline函数&#xff0c;不需要单独写在前面。因此将成员…