【Linux】多线程:POSIX库、线程管理、线程ID

2024-09-06 02:44

本文主要是介绍【Linux】多线程:POSIX库、线程管理、线程ID,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、POSIX线程库

二、线程ID

三、动态库加载

四、再谈线程ID


一、POSIX线程库

原生库:指的是操作系统自带的库,如POSIX线程库,在类Unix系统中通常是原生支持的。这些库是操作系统的一部分,提供了系统级的线程管理功能。

【了解】兼容性和标准化

  • POSIX 标准:POSIX(Portable Operating System Interface)是一个由 IEEE 制定的标准,旨在提供一个一致的操作系统接口,以提高程序的可移植性。POSIX 线程库(pthreads)是 POSIX 标准的一部分,它定义了一组线程相关的 API。Linux 提供这一标准的实现,以确保与其他遵循 POSIX 标准的系统(如 UNIX 和其他类 UNIX 系统)兼容,使得在不同操作系统之间移植代码变得更加容易。

  • 跨平台开发:许多应用程序和库依赖于 POSIX 标准,以便在多个操作系统上进行开发和运行。提供 POSIX 原生库使得这些应用程序可以更容易地移植到 Linux 平台,同时保持一致的接口和行为。

通过前面的学习我们了解到在Linux操作系统中,实际上并不存在真实的线程,Linux内核是以轻量级进程(LWP)这一机制来实现的。在Linux中,线程并不是像在一些其他操作系统中那样的独立实体。相反,线程实际上是被实现为轻量级进程(LWP)。每个线程都被内核视为一个独立的LWP。

同时,Linux操作系统也为创建线程提供了一系列的系统调用,如clone函数,供操作者来控制线程。我们对此仅作了解。

clone 系统调用的基本原型

int clone(unsigned long flags, void *child_stack, int newtls, int *parent_tid, int *child_tid);

使用场景:

  • 进程创建:通过设置适当的 flagsclone 可以用来创建一个新的进程(如通过 CLONE_VM 标志共享内存空间)。

  • 线程创建:通过设置合适的标志(如 CLONE_VMCLONE_FSCLONE_FILES 和 CLONE_SIGHAND),clone 可以用来创建线程,这些线程与父进程共享大部分资源。

我们知道,不同的操作系统中,实现线程的方式并不一致,这就造成了在Linux系统中使用系统调用编写的多线程程序在其他操作系统中可能并不适用。同时,通过clone函数我们可以发现,此类系统调用函数的使用较为复杂,在使用时需要程序员自身为线程的手动分配栈等内存空间并进行管理,这无疑加大了编程的复杂度。而POSIX库的出现恰好解决了这一系列的问题。

  • 简化开发:POSIX 线程库提供了一个一致的编程接口,封装了线程创建、管理、同步等操作。这使得开发者可以使用统一的 API 来处理线程相关任务,而不必依赖于特定操作系统的特性或接口。

  • 功能丰富:POSIX 线程库包含了多种功能,如线程创建、线程同步(互斥锁、条件变量)、线程局部存储等。这些功能的标准化和一致性使得编写多线程程序变得更加高效和可靠。

  • 直接支持:Linux 内核提供了对 POSIX 线程库的原生支持,通过 clone 系统调用实现线程的创建和管理。这样,线程操作可以直接由内核处理,减少了额外的抽象层,从而提高了性能。

  • 内核和用户空间的分离:Linux 的设计哲学是将内核和用户空间的功能分开,POSIX 线程库为用户空间提供了一套清晰的线程管理接口,而内核通过 clone 等系统调用来实现这些功能。这样的设计使得系统的功能划分更加明确,易于维护和扩展。

简而言之,POSIX线程库封装了一系列创建线程的系统调用,并为用户提供了更为简洁的编程接口,隐藏了诸如栈管理和线程资源的共享问题等底层细节,同时增加了程序的可移植性,减少了多线程编程的复杂度。 

二、线程ID

在进程章节的学习中,我们知道每个进程都有一个自己唯一的PCB(在Linux系统中是task_struct),也就是进程控制块。而操作系统为了便于对进程进行唯一标识,则为每个PCB分配了一个进程描述符——PID,也就是进程ID,存储在每个进程对应的PCB中。我们可以使用ps -ajx命令组合来查看系统中正在运行的进程。

那既然进程作为一个独立的单位,拥有自身的唯一标识。那么线程作为分派和调度的独立单位,是否也一样拥有自身唯一的标识符呢?答案是肯定的。

在Linux操作系统中,线程实际上是“轻量级进程”,依然属于进程的范畴,task_struct是Linux内核中用于表示和管理每个进程(包括线程)的核心数据结构。每个进程和线程都有一个task_struct实例,它包含了进程或线程的所有必要信息。

虽说线程实际上是轻量级进程,但是线程存在于进程的地址空间之中,他无法拥有独立的进程ID,而是拥有独立的线程ID。而线程ID在Linux内核和POSIX库中的表示又是各不相同的。这也正对应了Linux的核心思想:内核态与用户态的分离

  • 内核态:在内核态中,线程ID和进程ID的管理是由内核直接处理的。内核使用线程ID来跟踪和调度线程,同时为每个轻量级进程(线程)分配一个唯一的ID。这个ID在内核内部是唯一的,并用于各种内部操作,如上下文切换和调度。

  • 用户态:在用户态中,POSIX线程库(pthread库)提供了一种线程的抽象,允许程序员在应用层进行线程管理。POSIX线程库中的线程ID(如pthread_t类型)与内核中的线程ID不同。POSIX线程ID是一个用户级的抽象,用于简化线程的管理和操作。

接下来,我们先见一见线程:

【注意:1、要使用这些函数库,要通过引入头文<pthread.h>; 2、链接这些线程函数库时要使用编译器命令的“-lpthread”选项】

pthread_create函数是POSIX线程(pthreads)库中的一个核心函数,用于在用户空间创建新的线程。这个函数的主要功能是启动一个新线程,使其并行执行指定的函数。

函数原型

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

参数说明

  1. pthread_t *thread

            指向pthread_t类型的指针,用于存储新创建线程的ID。pthread_t是一个线程标识符类型,用于在后续操作中引用该线程。
  2. const pthread_attr_t *attr

            指向pthread_attr_t类型的指针,指定线程的属性。可以为NULL,这时线程将使用默认的属性。如果需要自定义线程的属性,可以通过pthread_attr_init函数初始化pthread_attr_t结构体,并设置相关属性。
  3. void *(*start_routine)(void*)

            指向函数的指针,这个函数将在线程中执行。该函数的参数是一个void*类型的指针,返回值也是void*类型。start_routine函数是线程的入口点。
  4. void *arg

            传递给start_routine函数的参数。arg是一个void*类型的指针,可以传递任意类型的数据。在线程开始执行时,arg将被作为参数传递给start_routine函数。

返回值

  • 成功:返回0。
  • 失败:返回一个错误码,指示失败的原因。
  • pthread_ create 函数会产生一个线程 ID, 存放在第一个参数指向的地址中。
  • 前面讲的线程 ID 属于进程调度的范畴。 因为线程是轻量级进程, 是操作系统
    调度器的最小单位, 所以需要一个数值来唯一表示该线程。
  • pthread_ create 函数第一个参数指向一个虚拟内存单元, 该内存单元的地址即为新创建线程的线程 ID, 属于 NPTL 线程库的范畴。 线程库的后续操作,就是根据该线程 ID 来操作线程的。
  • 线程库 NPTL 提供了 pthread_ self 函数, 可以获得线程自身的 ID:
#include <pthread.h>pthread_t pthread_self(void);
  • 返回值:返回一个 pthread_t 类型的线程标识符,表示当前调用线程的唯一标识符。
  • pthread_t 到底是什么类型呢? 取决于实现。 对于 Linux 目前实现的 NPTL 实现而言, pthread_t 类型的线程 ID, 本质就是一个进程地址空间上的一个地址。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>// 线程函数
void *thread_function(void *arg)
{printf("%s Process ID is: %d\n", (const char *)arg, getpid());printf("%s Thread ID is: %ld\n", (const char *)arg, pthread_self());sleep(5);pthread_exit(NULL); // 结束线程
}int main()
{pthread_t thread;const char *arg = "Thread -1";// 创建线程int ret = pthread_create(&thread, NULL, thread_function, (void *)arg);if (ret != 0){fprintf(stderr, "Error creating thread: %d\n", ret);exit(EXIT_FAILURE);}printf("My Process ID is: %d\n", getpid());printf("Main Thread ID is: %ld\n", pthread_self());// 等待线程结束,回收线程资源pthread_join(thread, NULL);printf("Thread has finished.\n");return 0;
}

在上述程序中,我们创建了一个线程,让该线程打印出该线程所属的进程和它的线程ID。在主线程中,也打印出主线程所属的进程和线程ID。在程序执行的过程中,我们可以通过如下命令来观察线程的状态:

ps -aL  //获得系统中的所有进程和线程的详细列表

我们能够观察到,这两个线程同属于一个进程之中,并且有一个线程(轻量级进程)的ID与进程的ID相同,这就是我们所说的“主线程”,主线程并不需要我们手动创建,当一个进程启动时,操作系统会自动创建一个线程,这个线程被称为“主线程”或“初始线程”。而ID为812435的线程则是我们通过pthread_create系统调用所创建的额外线程。

但同时,我们也印证了之前的说法,程序中所打印出的线程ID与内核中的线程ID并不相同。这是因为Linux中的线程是用户级线程,由POSIX库对其进行管理,所以我们在程序中所打印出来的线程ID实际是库给我们所分配的线程唯一标识。实际上,用户级线程ID是对线程库管理的一个抽象,而内核中的线程ID(LWP)则是真正用于系统调度和管理的标识。

三、动态库加载

在使用动态库(也称为共享库)时,动态库的加载和方法解析涉及到几个重要的步骤:

1. 动态库的加载

动态库的加载通常在程序运行时发生,这一过程包括以下几个步骤:

  • 延迟加载(Lazy Loading):动态库通常是在程序运行时根据需要进行加载的。这意味着只有在程序首次调用动态库中的函数时,动态库才会被实际加载到内存中。在 Linux 系统中,动态库加载通常是由 dlopen 函数完成的,该函数会将指定的动态库加载到进程的地址空间中。

  • 立即加载(Eager Loading):某些情况下,动态库可能在程序启动时就被加载。这通常由编译器在程序启动时通过运行时链接器(如 ld-linux.so)自动处理,或者由程序员在链接时指定。此时,动态库在程序启动时就已经被映射到内存中。

2. 页表映射

  • 内存映射:在动态库被加载到内存中时,操作系统会将动态库的内容映射到进程的虚拟地址空间。这是通过操作系统的虚拟内存管理机制实现的。操作系统将动态库的文件映射到进程的虚拟地址空间中,并更新进程的页表以确保对动态库内存的访问是有效的。

  • 动态链接:动态库的加载和页表映射涉及到动态链接过程。在 Linux 上,这个过程由动态链接器 ld.so 完成。它负责将动态库的符号(即库中的函数和变量)解析到实际的内存地址。

3. 方法的解析

  • 符号解析:当程序调用动态库中的函数时,程序需要知道这些函数在内存中的实际地址。动态链接器会解析动态库中的符号,找出每个符号(如函数或变量)在内存中的地址。这些符号信息通常保存在动态库的符号表中。

  • 符号表:动态库的符号表包括所有导出的函数和变量的信息。动态链接器会在动态库加载时处理这些符号表,并将它们与程序中对应的引用进行匹配。程序中的每个符号引用(例如,函数调用)会被替换为实际的内存地址。

  • 重定位:动态链接器会更新程序的内部结构(如跳转表或函数指针)以使用动态库中函数的实际地址。这使得程序可以在运行时正确地调用动态库中的函数。

也就是说,在我们使用pthread_create函数创建线程之前,排除其他情况,这时在该进程的虚拟地址空间中并不包含线程库,它依然暂存在磁盘中。该程序启动时,程序的代码和数据从磁盘中加载进内存当中,映射到操作系统为该进程所创建的虚拟地址空间当中。当使用pthread_create方法时,此时该进程的地址空间中并没有pthread库。所以此时pthread库将会从磁盘加载至内存当中,并通过页表与进程的虚拟地址空间建立映射。因为函数是有地址的,所以在该进程的地址空间内,可以成功找到pthread_create函数所处的位置,进而执行该方法。

四、再谈线程ID

POSIX库是用什么手段来确保线程ID的唯一性的呢?POSIX库所生成的线程ID本质就是一个进程地址空间上的一个虚拟地址!而地址本身就具有唯一性!!!

实际上,pthread库中维护着用户级线程的基本属性。每个线程的属性集合(即pthread_attr_t 结构体)在地址空间中占用一块内存空间。而用户级线程ID实际上是pthread_attr_t 结构体的起始地址。因此,用户级线程ID也确保了它的唯一性!此后,当我们想要获取线程内部的属性时,只需要拿到该线程的TID—即线程控制块的起始地址,即可对线程控制块的内部属性进行访问!

 

这篇关于【Linux】多线程:POSIX库、线程管理、线程ID的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1140780

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n