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

深入了解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.xdnf.cn/news/17992.html

相关文章:

  • TCP和UCP的区别
  • 密码学系列 - 零知识证明(ZKP) - 多种承诺方案
  • docker常用命令详解
  • Rust Async 异步编程(一):入门
  • BEVFormer论文速读
  • Day07 缓存商品 购物车
  • 编程算法实例-求一个整数的所有因数
  • 【Jenkins】01 - Jenkins安装
  • 【远程桌面】从RustDesk服务器看UDP对比WebRTC
  • 文本邮箱提取工具
  • gin结合minio来做文件存储
  • 3.逻辑回归:从分类到正则化
  • 快速了解均值滤波处理
  • 基础IO_系统文件IO | 重定向【Linux】
  • 一周学会Matplotlib3 Python 数据可视化-多子图及布局实现
  • 弱类型语言(Strong Typing)与强类型语言(Weak Typing)(描述语言对变量类型处理的严格程度)
  • 7.Ansible自动化之-实施任务控制
  • 工具测试 - marker (Convert PDF to markdown + JSON quickly with high accuracy)
  • 本地处理不上传!隐私安全的PDF转换解决方案
  • 【Netty核心解密】Channel与ChannelHandlerContext:网络编程的双子星
  • 最优化:建模、算法与理论|02 Optimization Modeling and Typical Examples(1)
  • ReID/OSNet 算法模型量化转换实践
  • 芋道RBAC实现介绍
  • 基于Node.js+Express的电商管理平台的设计与实现/基于vue的网上购物商城的设计与实现/基于Node.js+Express的在线销售系统
  • css: word pacing属性
  • 【原理】C#构造函数可以标记为Static吗
  • Oracle Undo Tablespace 使用率暴涨案例分析
  • Java 方法引用详解
  • Vue.js 路由/redirect重定向刷新机制详解
  • 新的“MadeYouReset”方法利用 HTTP/2 进行隐秘的 DoS 攻击