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

深入了解linux系统—— 线程控制

POSIX线程库

在了解过进程概念之后,我们知道在Linux操作系统中,线程是用进程来模拟实现的;

内核中的task_struct结构体对象被称为轻量级进程,所以操作系统所提供的系统调用接口都是关于轻量级进程的。

而我们想要创建线程,就要使用到pthread库。

使用pthread库,要包含头文件<pthread.h>

pthread是第三方库,在使用g++/gcc编译时要带-lpthread选项。

pthread库中,绝大多数函数接口都是以pthread_开头的;例如pthread_create创建线程、pthread_join等待线程。

线程控制

线程创建

要创建一个线程,就要调用pthread_create函数

在这里插入图片描述

可以看到pthread_create函数有4个参数:

  • 第一个参数是一个输出型参数,传递pthread_t*类型的指针,创建线程成功之后将线程ID带出来。
  • 第二个参数attr用来设置线程相关属性的,传nullptr默认设置属性。
  • 第三个参数,start_routine表示要创建的线程的入口函数(该函数的返回值和参数类型都是void*类型)
  • 第四个参数,arg表示线程在执行自己的入口函数时,要传递的参数。

返回值:

如果调用pthread_create创建线程成功,就返回0;如果创建失败,就返回对应的错误码。

在这里插入图片描述

使用pthread_create创建一个线程:

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
void *func(void *arg)
{std::string name = static_cast<char *>(arg);int cnt = 3;while (cnt--){std::cout << name << std::endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, func, (void *)"thread-1");// 创建线程执行func函数 , func函数的参数(void *)"thread-1"if (n != 0){std::cerr << "pthread_create : " << std::endl;return 1;}return 0;
}

线程终止

我们能够使用pthread_create创建一个线程,那一个线程如何退出呢?

return函数返回

线程要执行对应的函数,当函数执行完成时,线程就退出了;

注意:这里调用exit是让进程退出,线程如果调用exit,其进程就会退出

pthread_exit线程退出

线程在执行时不能调用exit来终止;可以调用pthread_exit来终止进程

在这里插入图片描述

其参数void* retval,就是指返回值;当我们调用pthread_exit终止线程时就可以通过参数将退出信息返回。

pthread_cancel取消线程

上述的return返回、pthread_exit终止线程都是线程自己终止;此外,我们也可以调用pthread_cancel来主动取消线程。

在这里插入图片描述

参数:pthrad_t thread表示要取消线程的id(pthread_create拿到的线程id)

返回值:如果调用pthread_cancel取消线程成功,返回0;失败则返回错误码

补充:pthread_self获取当前线程ID。

主线程(main)调用pthread_create创建新线程,可以获得新线程ID;main线程可以调用pthread_self获取自己的线程ID;

新线程也可以调用pthread_cancel来取消自己。(可以调用pthread_self来获取当前线程的ID)

void *func(void *arg)
{std::string name = static_cast<char *>(arg);int cnt = 3;while (cnt--){std::cout << name << std::endl;sleep(1);}// 线程退出pthread_exit((void *)"pthread_exit");// 线程调用pthread_cancel取消pthread_cancel(pthread_self());// 函数returnreturn (void *)"return";
}
int main()
{pthread_t tid;//创建新线程int n = pthread_create(&tid, nullptr, func, (void *)"thread1");if (n != 0){std::cerr << "pthread_create" << std::endl;return -1;}sleep(1);// main线程取消新线程pthread_cancel(tid);return 0;
}

线程等待

pthread_join

我们创建出来线程,让线程执行指定的函数;

就和创建子进程一样,创建出来要去执行某种任务,那我们要知道任务执行的结果吧;并且如果不处理子进程退出还会造成僵尸进程,那线程呢?

线程也是如此,我们需要获取新线程执行的结果,并且回收线程防止资源泄露问题。

pthread_join等待某个线程。

在这里插入图片描述

int pthread_join(pthread_t thread, void **retval);

参数pthread_join存在两个参数

  • pthread_t thread:要等待线程的ID
  • void **retval:输出型参数,获取线程退出时的返回值(return或者pthread_exit的参数)

返回值:如果等待线程成功就返回0,失败则返回对应的错误码

Linux系统中,ps指令-L选项显示线程的关键参数、-f选项显示完整信息、-T选项显示线程

在这里插入图片描述

对于void **retval可以获取线程退出时的返回值,获取return返回值:

void* func(void* msg)
{std::string name = static_cast<char*>(msg);int cnt = 3;while(cnt--){std::cout<<name<<std::endl;sleep(1);}return (void*)"thread return";
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,func,(void*)"thread-1");char* ret = nullptr;pthread_join(tid, (void**)&ret);std::cout<<ret<<std::endl;return 0;
}

在这里插入图片描述

此外,如果线程是调用pthread_exit退出的,pthread_join获取的就是pthread_exit的返回值

void *func(void *msg)
{std::string name = static_cast<char *>(msg);int cnt = 3;while (cnt--){std::cout << name << std::endl;sleep(1);}pthread_exit((void *)"thread pthread_exit");// return (void*)"thread return";
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, func, (void *)"thread-1");sleep(5);char *ret = nullptr;pthread_join(tid, (void **)&ret);std::cout << ret << std::endl;return 0;
}

在这里插入图片描述

线程分离

pthread_detach

在创建子进程时,就要通过父进程调用wait/waitpid来回收子进程;

如果我们不需要子进程的退出信息,就可以将进程对SIGCHLD(17号信号)的处理方式设置为SIGCHLD,这样子进程在退出时,系统给父进程发送SIGCHLD号信号,父进程忽视处理,就不需要调用wait/waitpid子进程就会被回收。

那线程呢?如果我们不需要线程的退出信息,也不想要调用pthread_join来回收新线程,那我们就可以调用pthread_detach设置线程分离状态;分离状态下的线程,退出后不能获取其退出信息(内核数据结构在线程退出后就回收了)

void *func(void *msg)
{std::string name = static_cast<char *>(msg);int cnt = 3;while (cnt--){std::cout << name << std::endl;sleep(1);}pthread_exit((void *)"thread pthread_exit");// return (void*)"thread return";
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, func, (void *)"thread-1");pthread_detach(tid);sleep(5);char *ret = nullptr;int n = pthread_join(tid, (void **)&ret);if (n == 0)std::cout << ret << std::endl;elsestd::cout << "thread detach" << std::endl;return 0;
}

在这里插入图片描述

线程ID

在上述使用pthread库,进行线程控制的过程中,貌似都是通过线程ID来对线程进行相关的操作;但是线程ID是什么呢?在Linux系统中不是没有线程这一概念吗,线程ID是轻量级进程ID吗?ps -aL显示的线程属性中LWP又是什么呢?

在这里插入图片描述

很显然,ps -aL显示的线程属性中,PID指的是进程ID,LWP指的是内核轻量级进程ID;

那线程ID是LWP吗?

void *func(void *msg)
{std::cout << "new thread id : " << pthread_self() << std::endl;sleep(1);return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, func, nullptr);sleep(3);std::cout << "main thread id : " << pthread_self() << std::endl;pthread_join(tid, nullptr);return 0;
}

这里输出线程ID 看是否和LWP相等

在这里插入图片描述

可以看到线程ID和内核中LWP是不一样的。

输出出来的线程ID是一个很大的数,对于目前Linux实现的NPTL而言,pthread_t类型的的线程ID,本质上就是一个进程地址空间上的一个地址。

在上述创建线程的过程中,我们可以发现当线程运行结束,无论main线程是否调用pthread_join等待新线程,新线程对应的轻量级进程都会被回收;那不等待新线程造成内存泄露,丢失的是哪一部分内存呢?线程的返回值又是如何拿到的呢?

我们程序可以调用pthread_create创建线程,其他程序也可以;那在内存中就存在非常多的线程,有的刚刚创建、有的正在运行、有的即将释放,那肯定要将这些线程管理起来;管理:先描述再组织

所以说,在内存中也存在描述线程相关的数据结构,这些数据结构被管理起来。

那这些数据结构在哪里呢?

mmap动态映射区/共享区。这里就像C语言文件操作那样,在库中构建了一个FILE类型返回给上层。

在这里插入图片描述

线程栈

对于线程,虽然说在linux系统中,进程和线程的统一使用task_struct,但是访问进程地址空间还是存在区别的。

我们知道进程地址中,只存在一个栈区,线程要执行自己的代码,那肯定是要有自己对应的栈的。

对于Linux进程(主线程),就是main函数的栈;在fork时,本质上就是复制了父进程的stack空间地址,然后进行写时拷贝以及动态增长;通过扩充超出该上限就会栈溢出,(段错误)发送段错误信号给进程。

对于子线程 ,其stack不再是向下增长的,是事先固定下来的;线程栈一般是调用glibc/uclibcpthread库接口pthread_create创建的线程,在共享区。

mem = mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); 

这里mmap调用中的size参数比较复杂,可以自己传入stack的大小,也可以使用默认的,一般默认就是8M.

这种stack不能动态增长,用尽之后就没有了;glibc中调用mmap获得栈后,底层会调用sys_clone

int sys_clone(struct pt_regs *regs)
{unsigned long clone_flags;unsigned long newsp;int __user *parent_tidptr, *child_tidptr;clone_flags = regs->bx;// 获取了mmap得到的线程的stack指针newsp = regs->cx;parent_tidptr = (int __user *)regs->dx;child_tidptr = (int __user *)regs->di;if (!newsp)newsp = regs->sp;return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}

所以,对于子线程的栈,本质上是在进程地址空间中map出的一块内存区域;而main线程栈就是进程地址空间上的栈区域。

到这里本篇文章内容就结束了,感谢支持

http://www.dtcms.com/a/335831.html

相关文章:

  • IDE/去读懂STM32CubeMX 时钟配置图(有源/无源晶振、旁路/晶振模式、倍频/分频)
  • 三、k8s 1.29 之 安装2
  • 重温k8s基础概念知识系列三(工作负载)
  • 什么是GD库?PHP中7大类64个GD库函数用法详解
  • Kafka 面试题及详细答案100道(23-35)-- 核心机制2
  • 基础IO_系统文件IO | 重定向【Linux】
  • 《程序员修炼之道》第三四章读书笔记
  • 《算法导论》第 27 章 - 多线程算法
  • scikit-learn/sklearn学习|套索回归Lasso解读
  • Ansible 核心功能进阶:自动化任务的灵活控制与管理
  • 自由职业数据科学:从细分定位到规模化的实战路线
  • 记忆翻牌游戏 greenfoot 开发
  • 机器人经验学习1 杂记
  • 电子电气架构 --- 自动驾驶汽车的下一步发展是什么?
  • Python自学10-常用数据结构之字符串
  • 机器学习算法篇(十三)------词向量转化的算法思想详解与基于词向量转换的文本数据处理的好评差评分类实战(NPL基础实战)
  • 深度解析 Tomcat ProtocolHandler 工作原理
  • 安装 Docker 支持 NVIDIA 显卡的依赖
  • AI Search进化论:从RAG到DeepSearch的智能体演变全过程
  • JavaScript性能优化实战(三):DOM操作性能优化
  • 计算机网络 HTTP1.1、HTTP2、HTTP3 的核心对比及性能分析
  • 【LLM】文献阅读-ISOLATE GPT:基于大语言模型的执行隔离架构
  • 第16节:自定义几何体 - 从顶点构建3D世界
  • 检查xrdp远程连接桌面卡顿的问题(附解决sh脚本)
  • Oracle查看历史会话信息视图介绍
  • 【大语言模型 04】Cross-Attention vs Self-Attention实战对比:解码器中的双重注意力机制
  • NumPy 库介绍:核心 API 详解
  • MYSQL-175. 组合两个表
  • Java 学习笔记(基础篇4)
  • Java学习笔记:IDEA简单使用技巧