免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Linux系统与系统编程(14)——线程概念与线程控制

Linux系统与系统编程(14)——线程概念与线程控制 前言欢迎观看Linux系列文章第14篇主要讲述了线程概念、线程控制接口、线程ID与进程地址空间的关系、线程栈等。Linux线程初步概念进程与线程进程Process是操作系统资源分配的基本单位。它是程序的一次动态执行拥有自己独立的内存空间代码、数据、堆栈、文件描述符等资源。比如你打开一个浏览器、一个Word文档系统就会分别创建一个进程。线程Thread是操作系统任务调度CPU执行的最小单位。它是进程内部的一条执行路径不独立拥有系统资源而是与同属一个进程的其他线程共享进程的资源如内存、文件等但它拥有自己独立的程序计数器、寄存器和栈空间用于记录执行到哪里和局部变量。·线程在操作系统内部也要先描述再管理进程是PCB而线程是TCBTCB的源代码数据结构是对PCB的复用。所以在操作系统的实力叫来看两者都是一样的都看作是执行流从硬件CPU来说也一样不用区别进程和线程统一叫做轻量级进程。·进程访问的大部分资源都是通过地址空间访问的。每一个进程都有独立的地址空间。·如果我们 让若干个进程共享同一个地址空间将该地址空间的资源分配给不同的进程那就是用进程模拟出来线程了。所以Linux的线程就是轻量级的进程或者是轻量级进程模拟实现的。·对资源的划分本质是对地址空间范围的划分。 虚拟地址就是资源的代表。·函数就是虚拟地址逻辑地址空间的集合就是让线程未来执行ELF程序的不同的函数即可。分页式内存管理我们在申请物理内存时实际上就是1.查数组改page。struct file{ ······ struct address_space;//地址空间 };2.建立内核数据结构的对应关系。struct address_space{ ······ struct radix_tree_root page_tree;//以基数树为数据结构保存所有的page可查找 };struct radix_tree_node{ ······ unsigned int count; void *slots[RADIX_TREE_MAP_SIZE]; unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; ······ };映射方式在32位平台下虚拟地址为32位的数字0000 0000 0000 0000 0000 0000 0000 0000。其中把前10位、第11到20位、第21到32位看作三个数字取值范围分别是[0, 1023]、[0, 1023]、[0, 4095]。第一个数字在页目录中查找到页表项的地址第二个数字在页表中找到页的地址第三个数字是页内偏移访问具体的字节。这就是两级页表的地址转换。写时拷贝缺页中断内存申请订单背后都可能要重新建立新的页表和建立映射关系的操作。以上基本就是MMU的工作流程MMU是一种硬件电路速度很快主要工作是进行内存管理地址转换是他承接的业务之一。TLB缓存快表由于单级页表对连续内存的要求高所以才引入了多级页表但是多级页表在减少了连续存储要求且减少存储空间的同时降低了查询效率。为了提高效率MMU引入了TLB来解决。TLB是MMU内部的高速缓存。当CPU给MMU传送新地址之后MMU先去TLB查询有没有如果有就直接拿到物理地址然后从总线发送到内存中。因为比较容量小TLB难免会发生cache未命中的情况这时再去页表中查找。找到之后除了把地址发送到总线传给内存还会把这层内存关系给到TLB让他记录一下刷新缓存。缺页异常Page Fault当CPU给到的虚拟地址在TLB和页表都没找到对应的物理页就会由硬件触发缺页中断。缺页中断不同类型会有不同的处理方法Hard Page FaultMajor Page Fault称为硬缺页错误/主要缺页错误这时物理内存中没有对应的物理页需要CPU打开磁盘设备读取到物理内存中再让MMU建立地址和物理地址的映射。Soft Page FaultMinor Page Fault称为软缺页错误/次要缺页错误这时物理内存存在对应的物理页不过可能是其他进程调入的发生缺页中断的进程不知道。此时MMU只要建立起映射即可如需打开磁盘设备读取到内存。主要发生在多进程共享内存区。Invalid Page Fault称为无效缺页错误比如进程访问的内存地址越界又比如对空指针解引用内核就会报segment fault错误中断程序然后挂掉。线程的深刻理解在一个程序里的一个执行线路就叫做线程。一个进程至少有一个执行线程。线程在进程内部运行本质是在进程地址空间内运行。线程进行资源划分本质是划分地址空间获得一定范围的合法虚拟地址其实就是在划分页表。线程进行资源共享本质是对地址空间的共享就是对页表条目的共享。优点❶线程切换时要做的工作要少得多。主要区别是线程的切换虚拟内存依然是相同的但是进程切换是不同的也就是线程不需要切换页表不用切换出寄存器中的内存。其次线程上下文切换不会扰乱处理器的缓存机制。当进程切换时已经缓存的内存地址会全部作废还有TLB缓冲也会被刷新这就会导致内存在一段时间内变得低效cache刷新后都无法命中要重新映射。❷线程占用的资源比进程少❸能充分利用多处理器的可并行数量❹在等待慢速I/O操作结束的同时程序可以执行其他计算任务❺计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现。❻I/O密集型应用为了提高性能将I/O操作重叠线程可以同时等待不同的I/O操作。➐创建一个新线程的代价比一个新进程要小得多缺点❶性能缺失线程过多时会增加额外的同步和调度开销而可用的资源却不变。在很少被外部时间阻塞的计算密集型线程中比较明显。❷健壮性降低多线程程序里因为时间分配上的细微偏差或者因为共享了不该共享的变量而造成的不良影响是很大的。也就是说多线程之间是缺乏保护的。❸缺乏访问控制进程是访问控制的基本粒度在一个线程中调用系统调用函数会影响到整个进程。❹编程难度提高由于以上原因编写和调试一个多线程程序要比单线程程序要难的多。线程异常线程因为错误崩溃就会连带着进程崩溃。线程是进程的执行的分支线程出异常就类似进程出异常进而触发信号禁止终止进程进程终止则其内部的所有线程也会退出。Linux进程与线程进程和线程⭕️进程是资源分配的基本单位。⭕️线程是调度的基本单位。⭕️线程共享进程数据但也拥有自己的一部分“私有”数据线程ID、线程独立的上下文数据从而才能被独立调度、栈结构、errno错误码、信号屏蔽字、调度优先级。进程的多个线程共享同一个地址空间Text Segment、Data Segment都是共享的如果定义一个函数在个线程中都可以调用如果定义一个全局变量多个线程中都可以访问到。各线程间还共享一下资源和环境文件描述符表、信号处理方式信号处理函数、当前工作目录、用户id和组id。简单的说各线程之间绝大多数数据都是共享的。Linux线程控制posix线程库·与线程有关的函数几乎都以“pthread_”开头。·要使用这些函数要引用头文件pthread.h。·在连接这些线程函数库时要使用编译器命令的“-lphread”选项新版的编译器可以不加因为已经把该库的功能合并到主C库中也就是默认自带。线程控制接口pthread_create——创建多线程#includepthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void* (*start_routine)(void *), void* arg);参数thread线程ID输出型参数创建成功后会把线程IID保存到指向的变量中。attr线程属性指针输入型参数用于设置线程属性。如果传入nullptr就以默认属性设置。start_routine线程启动函数。线程启动后将立刻启用该函数。arg作为线程启动函数的参数传入start_routine中。返回值创建成功返回0反之返回错误码。演示代码void *threadrun(void *args) { string name (const char *)args; while (true) { cout 我是新线程——name name endl; sleep(1); } return nullptr; } int main() { pthread_t tid; pthread_create(tid, nullptr, threadrun, (void *)thread-1); while (true) { cout 我是主线程 endl; sleep(1); } return 0; }运行上方代码后输入下方指令可以查看线程运行情况ps -aLLWPlight weght process轻量级进程。PID和LWP相同时说明该线程为主线程。在CPU调度时只看LWP。不同的线程之间对时间片是等分的也就是说时间片也是共享的。不同线程输出消息时可能会混杂在一起就要添加锁这个后面会讲。注意该函数非系统调用函数pthread线程库是对系统调用函数封装的一个软件层。在Linux系统中没有真正意义的线程OS只认轻量级进程。pthread库把创建的轻量级进程封装起来并且给用户提供创建线程的接口。所以linux线程的实现是在用户层实现的称为用户级进程。pthread又叫做原生线程库。pthread_create函数封装的是系统调用函数clone作用是创建一个和父进程共享内存空间的子进程。此处的LWP并不是线程ID线程库封装线程时把线程ID也进行了封装线程ID不会以这种方式暴露出来。pthread_join——主线程等待回收线程资源#include pthread.h int pthread_join(pthread_t thread, void **retval);参数thread等待回收的线程ID。retval输出型参数用于接收线程函数的返回值如果不需要或者不关心就传入nullptr。返回值成功返回0失败不会设置错误码根据情况返回一个正数的错误码。ESRCH指定的 thread 不存在例如已经 detached 或从未创建。EINVAL该线程是不可等待的例如已被分离 detached或已有其他线程在等待它。EDEADLK发生了死锁例如线程试图等待自己。作用类似于进程回收子进程避免内存泄漏。线程默认是需要被等待的即joinable状态。如果不想让主线程等待新线程希望新线程结束之后自己退出就要设置为分离即detach状态具体做法看下文。线程分离可以主动分离也可以被动分离。但是即使分离了线程依旧存在于进程的地址空间里依旧访问和操作进程的资源。已分离的新线程再去jion等待他就会报错。pthread_detach——分离线程#include pthread.h int pthread_detach(pthread_t tid);参数tid需要分离的目标线程。当主线程不关心新线程的时候在join函数去等待它其实是在浪费资源。所以要把新资源分离出来。他会自行释放资源。pthread_self——获取线程ID#include pthread.h int pthread_self();无参数返回线程ID。放在线程函数中使用。进程中的全局变量、函数、STL容器线程都是可以共享的。这一个线程ID是由pthread库来维持的所以这个ID的作用域只在进程内内核是无法理解这个线程ID的。这个ID实际上是我们在创建线程时创建的描述线程的线程控制块的起始地址。pthread_exit——终止线程#include pthread.h int pthread_self(void* retval);让线程即时主动终止运行。线程终止最常见的就是线程的入口函数return了。线程终止用exit()会直接终止进程要谨慎使用。参数retval接收线程的退出状态pthread_cancel——取消线程#include pthread.h int pthread_self(pthread_t thread);向目标线程jionable状态发送取消请求请求响应后目标线程终止。参数thread目标线程ID。响应机制目标线程是否以及何时响应取消请求取决于它自身的两个属性取消状态Cancelability State可以是启用ENABLE 或禁用DISABLE。默认是启用。如果禁用取消请求会一直排队直到线程重新启用取消功能。取消类型Cancelability Type可以是延迟Deferred 或异步Asynchronous。默认是延迟。延迟取消取消请求会延迟直到线程下一次调用一个取消点Cancellation Point 函数如 printf, sleep, read 等。这是最常用、最安全的模式。异步取消线程可以在任何时刻被取消。这种模式非常危险应谨慎使用因为它可能导致资源泄漏或数据不一致。线程ID及进程地址空间Linux下没有真正的线程他是用轻量级进程模拟的。因此OS提供的接口没有关于线程的接口。所以就要在用户层封装轻量级进程形成原生线程库。这个用户级别的库就是pthread库。进程自己的代码区可以访问到pthread库内部和库内部的函数和数据。线程的概念是在库中被维护的在库内部就一定会存在多个被被创建好的线程。库需要对这些线程进行管理。因此又需要进行先描述后组织struct tcb { 线程应该有的属性 线程状态 线程ID 线程独立的栈结构 线程栈大小 ······ }LWP与用户线程为了不泄露线程的数据每个线程都要有独立的栈结构。创建线程时线程库中会创建线程控制的管理块而在内核中会创建轻量级进程这里会调用系统调用方法clone。int clone (int(* fn)(void*), void* stack, int flags, void* args, ... /* pid_t* parent_tid, void* tls, pid_t* child_tid*/);参数fn就是线程的routine函数stack就是线程独立的栈结构。通过这个系统调用创建出一个PCB其他线程的属性就由线程库自行维护在底层运行处理完线程之后把线程传回去库中的void* ret变量中然后可以通过pthread_join回收他。线程栈虽然对于内核来说进程和线程都统一为了task_struct但是对待其地址空间的stack还是有些区别的。pthread_create创建线程会在共享区调用mmap系统调用开辟空间来作为线程的独立栈空间。线程的栈不能动态增长一旦用尽就没了这是和生成进程的fork不同的地方。因此对于子线程的 stack 它其实是在进程的地址空间中map出来的一块内存区域原则上是线程私有的。线程封装以下代码简单演示了线程如何封装演示代码#includeThread.hpp #includeunistd.h using namespace ThreadMod; void Count(int cnt) { while(cnt--) { std::cout我是一个新线程std::endl; sleep(1); } } int main() { int cnt 10; Threadint t(Count, cnt); /* Thread t ([](){ while(true) { std::cout我是一个新线程std::endl; sleep(1); } }); t.Start(); t.Detach(); sleep(5); t.Stop(); sleep(5); t.Join(); return 0; */ }#ifndef THREAD_H_ #define THREAD_H_ #include iostream #include string #include pthread.h #include cstring #include cstdio #include functional namespace ThreadMod { static u_int32_t number 1; templatetypename T class Thread { using func_t std::functionvoid(T); private: void EnableDetach() { std::cout 线程已分离 std::endl; _isdetach true; } public: Thread(func_t func, T data) : _tid(0), _isdetach(false), _isrunning(false), res(nullptr), _func(func), _data(data) { _name thread- std::to_string(number); } void Detach() { if (_isdetach) return; if (_isrunning) pthread_detach(_tid); EnableDetach(); } void EnableRunnning() { _isrunning true; } static void *Routine(void *args) { ThreadT *self static_castThreadT *(args); self-EnableRunnning(); if (self-_isdetach) { self-Detach(); } self-_func(self-_data); return nullptr; } bool Start() { int n pthread_create(_tid, nullptr, Routine, this); if (n ! 0) { std::cerr create thread error: strerror(n) std::endl; return false; } else { std::cout _name create success! std::endl; return true; } } bool Stop() { if (_isrunning) { int n pthread_cancel(_tid); if (n ! 0) { std::cerr cancel thread error: strerror(n) std::endl; return false; } else { std::cout _name stop std::endl; _isrunning false; } } return false; } void Join() { if (_isdetach) { std::cout 你的线程已分离无法再进行Join std::endl; return; } int n pthread_join(_tid, res); if (n ! 0) { std::cerr join thread error: strerror(n) std::endl; } else { std::cout join success! std::endl; } } ~Thread() { } private: pthread_t _tid; std::string _name; bool _isdetach; bool _isrunning; void *res; func_t _func; T _data; }; } #endif❤~~本文完结感谢观看接下来更精彩欢迎来我博客做客~~❤
返回列表