平面设计资源网站,广告营销案例100例,湖南住房与城乡建设部网站,公司网站建设需要多少钱目录
一、认识线程
1.1 线程概念
1.2 页表
1.3 线程的优缺点
1.3.1 优点
1.3.2 缺点
1.4 线程异常
二、进程 VS 线程
三、Linux线程控制
3.1 POSIX线程库
3.1 线程创建
3.3 线程等待
3.4 线程终止
3.4.1 return退出
3.4.2 pthread_exit()
3.4.3 pthread_cancel…目录
一、认识线程
1.1 线程概念
1.2 页表
1.3 线程的优缺点
1.3.1 优点
1.3.2 缺点
1.4 线程异常
二、进程 VS 线程
三、Linux线程控制
3.1 POSIX线程库
3.1 线程创建
3.3 线程等待
3.4 线程终止
3.4.1 return退出
3.4.2 pthread_exit()
3.4.3 pthread_cancel()
3.5 线程分离
3.6 线程ID与进程地址空间布局 一、认识线程
1.1 线程概念
之前讲过创建一个进程伴随着其进程控制块task_struct、进程地址空间mm_struct以及页表等的创建虚拟地址和物理地址就是通过页表建立映射的 但在创建线程时只需创建task_struct创建出来的task_struct和主task_struct共享进程地址空间和页表等 进程里的一个执行路线就是线程thread。即线程是一个进程内部的控制序列(执行分支)所有进程至少都有一个执行线程线程在进程内部运行本质是在进程地址空间内运行即曾经这个进程申请的所有资源几乎都是被所有线程共享的在Linux系统中CPU眼中看到的PCB都要比传统的进程PCB更轻量化也称为轻量级进程通过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流重新理解进程 下面用蓝色框起来的就是进程 进程并不是通过task_struct来衡量的除了task_struct之外一个进程还要有进程地址空间、文件、信号集等等合起来称之为一个进程
站在内核角度来理解进程承担分配系统资源的基本实体被称为进程
当创建进程时是创建一个task_struct、创建地址空间、维护页表然后在物理内存当中开辟空间、构建映射打开进程默认打开的相关文件、注册信号对应的处理方案等等。
之前接触到的进程都只有一个task_struct也就是该进程内部只有一个执行流即单执行流进程反之内部有多个执行流的进程叫做多执行流进程 Linux系统中CPU是否能区分进程和线程 在Linux系统中CPU并不能区分进程与线程因为CPU只关心一个一个的独立执行流。无论进程内部只有一个执行流还是有多个执行流CPU都是以task_struct为单位进行调度的。即线程是CPU调度的最小单位
单执行流进程被调度 多执行流进程被调度 Linux中并不存在真正意义上的多线程而是进程模拟的 操作系统中存在大量的进程一个进程中又存在一个或多个线程因此线程的数量一定比进程的数量多很明显线程的执行粒度要比进程更细。
若一款操作系统要真正意义上支持线程那么就需要对线程进行管理。比如创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等所有的这一套相比较进程都需要另起炉灶搭建一套线程管理模块。
因此若要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来描述线程的控制块和描述进程的控制块是类似的因此Linux并没有重新为线程设计管理模块而是直接复用了进程控制块即Linux中的所有执行流都是轻量级进程
但也有支持真正线程的操作系统譬如Windows操作系统就存在专门描述线程的控制块因此Windows操作系统系统的实现逻辑一定比Linux操作系统更为复杂 Linux中并没有真正意义上的线程系统调用 在Linux中没有真正意义上的线程那么也就没有真正意义上的线程相关的系统调用。但Linux提供了创建轻量级进程的接口其中最典型的代表就是vfork函数
pid_t vfork(void);
vfork函数的功能就是创建轻量级进程只创建task_struct父子进程共享资源
返回值
给父进程返回子进程的PID给子进程返回0
#include iostream
#include cstdlib
#include sys/types.h
#include unistd.h
using namespace std;int g_val 100;
int main()
{pid_t id vfork();if (id 0) { //childg_val 200;cout child:PID: getpid() PPID: getppid() g_val: g_val endl;exit(0);}//fathersleep(3);cout father:PID: getpid() g_val: g_val endl;return 0;
} 父进程读取到g_val的值是子进程修改后的值也证明了vfork创建的子进程与父进程是共享地址空间的 1.2 页表
在32位平台下一共有个地址也就意味着有个地址需要被映射。若页表就只是单纯的一张表那么就需要建立个虚拟地址和物理地址之间的映射关系即这张表一共有232个映射表项。
每一个表项中除了要有虚拟地址和与其映射的物理地址以外实际还需要有一些权限相关的信息比如我们所说的用户级页表和内核级页表实际就是通过权限进行区分的。 每个应表项中存储一个物理地址和一个虚拟地址就需要8个字节考虑到还需要包含权限相关的各种信息这里每一个表项就按10个字节计算。若有个表项也就意味着存储这张页表需要用 * 10个字节即40GB。显而易见内存中并存储不了这么大的一张表。
以32位平台为例其页表的映射过程如下
选择虚拟地址的前10个bit位在页目录(一级页表)当中进行查找找到对应的二级页表再选择虚拟地址的10个bit位在二级页表中查找找到物理内存中对应页框的起始地址最后将虚拟地址中剩下的12个bit位作为偏移量从对应页框的起始地址处向后进行偏移找到物理内存中某一个对应的地址
页框、页帧
物理内存实际上是被划分成一个个4KB大小的页框的操作系统完成而磁盘上的程序也是被划分成许多4KB大小的页帧的编译器编译时完成当内存和磁盘进行数据交换时IO就是以4KB大小为单位进行加载和保存的4KB就是个字节即一个页框中有个字节且访问内存的最小大小是1字节。因此一个页框中就有个地址正好使用剩下的12个bit位作为偏移量可以找到页框中任意一个字节 每一个表项还是按10字节计算一级页表的表项有个那么表的大小就是 * 10个字节即10KB。而一级页表有个表项也就意味着二级页表有个即一级页表有1张二级页表有张总共算下来就是10MB左右内存消耗并不高。而且实际运行中并不会使用所有的地址因此页表也比预估的更小。
上面所说的所有映射过程都是由MMUMemoryManagementUnit这个硬件完成的该硬件是集成在CPU内的。页表是一种软件映射MMU是一种硬件映射所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式。
注意 在Linux中32位平台下用的是二级页表而64位平台下使用的是多级页表 1.3 线程的优缺点
1.3.1 优点
创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要更少CPU中存在寄存器和L1 ~ L3级缓存进程切换会导致寄存器和缓存中的数据失效且需要重新加载但线程切换并不需要线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速IO操作结束的同时程序可执行其他的计算任务计算密集型应用在多处理器系统上运行可将计算分解到多个线程中实现从而提高执行效率IO密集型应用为了提高性能可将IO操作重叠使得线程可以同时等待不同的IO操作概念说明 计算密集型(CPU密集型)执行流的大部分任务主要以计算为主。如加密解密、大数据查找等IO密集型执行流的大部分任务主要以IO为主。如刷盘、访问数据库、访问网络等1.3.2 缺点
性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。若计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失即增加了额外的同步和调度开销而可用的资源不变健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的即线程之间是缺乏保护的缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响编程难度提高 编写与调试一个多线程程序比单线程程序困难得多。
若有水平较高的程序编写者其实上述这些缺点都可以避免的 1.4 线程异常
单个线程如果出现除零、野指针等问题导致线程崩溃进程也会随着崩溃线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出二、进程 VS 线程
线程共用同一个地址空间因此代码段Text Segment、数据段Data Segment等都是共享的
若定义一个函数在各线程中都可以调用若定义一个全局变量在各线程中都可以访问到
除此之外各线程还共享以下进程资源和环境
文件描述符表进程打开一个文件后其他线程也能够看到每种信号的处理方式SIG_IGN、SIG_DFL或者自定义的信号处理函数当前工作目录cwd用户ID和组ID
进程是承担分配系统资源的基本实体线程是CPU调度的基本单位。线程共享进程数据但也拥有自己的一部分数据
线程ID一组寄存器存储每个线程的上下文信息栈每个线程都有临时的数据需要压栈出栈errnoC语言提供的全局变量但每个线程都有自己的信号屏蔽字调度优先级三、Linux线程控制
3.1 POSIX线程库
在Linux中站在内核角度上看并没有真正意义上线程相关的接口。但站在用户角度当用户想创建一个线程时更期望使用thread_create这样类似的接口而不是vfork函数因此系统在应用层提供了原生线程库pthread。原生线程库实际就是对轻量级进程的系统调用进行了封装在用户层模拟实现了一套线程相关的接口
应用层指的是这个线程库并不是操作系统直接提供的而是由第三方使用系统接口编写的原生指的是大部分Linux系统都会默认带上该线程库与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以pthread_开头要使用pthread库要引入头文件pthreaad.h链接pthread库时要在编译时要使用-lpthread选项
注意
传统的函数是成功返回0失败返回-1并且对全局变量errno设置以指示错误。pthread函数出错时并不会设置全局变量errno而大部分POSIX函数会这样做而是将错误信息通过返回值返回pthread同样也提供了线程内的errno变量以支持其他使用errno的代码。但对于pthread函数的错误建议通过返回值来判定因为读取返回值要比读取线程内的errno变量的开销更小且线程的errno是各线程独占的3.1 线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数
thread获取创建成功的线程ID该参数是一个输出型参数attr用于设置创建线程的属性传入NULL表示使用默认属性start_routine该参数是一个函数地址表示线程例程即线程启动后要执行的函数arg传给线程例程的参数即传给start_routine的形参
返回值
线程创建成功返回0失败返回错误码
使用案例
让主线程调用pthread_create函数创建一个新线程此后新线程就会跑去执行自己的新例程而主线程则继续执行后续代码
#include iostream
#include pthread.h
#include unistd.h
using namespace std;void* Routine(void* args)
{while (1) {cout I am (char*)args endl;sleep(1);}
}
int main()
{pthread_t tid;pthread_create(tid, NULL, Routine, (void*)thread 1);while (1) {cout I am main thread! endl;sleep(2);}return 0;
} 使用 ps -aL 命令可以显示当前的轻量级进程不带 -L 选项默认显示进程 LWPLight Weight Process就是轻量级进程的ID可以看到显示的两个轻量级进程的PID是相同的因为它们属于同一个进程。
在Linux中应用层的线程与内核的LWP是对应的实际上操作系统调度时使用的是LWP而并非PID。单线程进程时PID和LWP是相等的所以对于单线程进程而言调度时采用PID和LWP是一样的多线程进程时PID与主线程LWP相同。 3.3 线程等待
线程如同进程一般也是需要被等待的。若主线程不对新线程进行等待那么新线程的资源不会被回收会发生类似于僵尸进程的问题即内存泄漏。
使用pthread_join()可以进行线程等待
int pthread_join(pthread_t thread, void **retval);
参数
thread被等待线程的IDretval线程退出时的退出码信息
返回值
线程等待成功返回0失败返回错误码
调用该函数的线程将阻塞到ID为thread的线程终止。thread线程以不同的方法终止通过pthread_join得到的终止状态是不同的
若thread线程通过return返回retval所指向的单元里存放的是线程的返回值若thread线程被别的线程调用pthread_cancel()异常终止掉retval所指向的单元里存放的是宏PTHREAD_CANCELED即(void*)-1) 若thread线程是自行调用pthread_exit()终止的retval所指向的单元存放的是传给pthread_exit的参数若对thread线程的终止状态不感兴趣可传NULL给retval参数
使用案例
#include iostream
#include pthread.h
#include unistd.husing namespace std;void* Routine(void *args)
{cout (char*)args endl;sleep(3);return (void*)0;
}
int main()
{pthread_t tid;pthread_create(tid, nullptr, Routine, (void*)new thread);void* ret nullptr;int n pthread_join(tid,ret);if(n 0) {cout 等待成功 endl;cout 返回信息为: (long long)ret endl;}else {cout 等待失败 endl;}return 0;
} 3.4 线程终止
3.4.1 return退出
在创建线程时指定的例程中使用return代表当前线程退出但在main函数中使用return代表整个进程退出即主线程退出了那么整个进程就退出了。
3.4.2 pthread_exit()
void pthread_exit(void *retval);
参数retval线程退出时的退出信息
注意
pthread_exit()或者return返回的指针所指向的内存单元必须是全局的或者堆区开辟的不能在线程函数的栈上分配因为当其他线程得到这个返回指针时线程已经退出了线程退出不能使用exit()函数其作用是退出整个进程任何一个线程调用都是如此
3.4.3 pthread_cancel()
int pthread_cancel(pthread_t thread);
参数thread被取消线程的ID
返回值线程取消成功返回0失败返回错误码
线程是可以取消自己的使用pthread_self()函数。也可以让新线程取消主线程但不建议这么使用一般都是使用主线程去控制新线程的。
取消成功的线程的退出码一般是宏PTHREAD_CANCELED即(void*)-1) 3.5 线程分离
默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成内存泄漏。但若本身并不关心线程的返回值那么join也是一种负担此时可将该线程进行分离后续当线程退出时就会自动释放线程资源线程若被分离了这个线程依旧使用该进程的资源且依旧在该进程内运行甚至这个线程崩溃了一定会影响整个进程只不过这个线程退出时不再需要主线程去join了当这个线程退出时系统会自动回收该线程所对应的资源可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离joinable和分离是冲突的一个线程不能既是joinable又是分离的
使用pthread_detach()函数进程分离线程
int pthread_detach(pthread_t thread);
参数thread被分离线程的ID
返回值线程分离成功返回0失败返回错误码 3.6 线程ID与进程地址空间布局
pthread_create函数会产生一个线程ID存放在第一个参数指向的地址中但该线程ID和内核中的LWP并不是一回事内核中的LWP属于CPU调度的范畴因为线程其实就是轻量级进程是操作系统调度器的最小单位所以需要一个数值来唯一表示该线程pthread_create()函数第一个参数指向一个虚拟内存单元该内存单元的地址即为新创建线程的线程ID这个ID属于NPTL线程库的范畴线程库的后续操作就是根据该线程ID来操作线程的线程库NPTL提供的pthread_self()函数获取的线程ID和pthread_create()函数第一个参数获取的线程ID是一样的线程ID到底是什么 可以将线程ID打印出来看看
#include iostream
#include pthread.h
#include unistd.husing namespace std;void* Routine(void* args) {cout (char*)args : pthread_self() endl;return (void*)0;
}
int main()
{pthread_t tid;pthread_create(tid,nullptr,Routine,(void*)new thread);sleep(1);cout main thread : pthread_self() endl;pthread_join(tid,nullptr);return 0;
} 可以发现这个线程ID数值特别大并不是LWP那么这个线程ID到底是什么呢
Linux系统中不提供真正的线程ID只提供LWP即操作系统只需通过LWP对轻量级进程进行管理而供用户使用的线程接口等其他数据由线程库来管理因此管理线程时的先描述再组织就应该在线程库中完成
使用 lld 命令可以看到线程库实际上是一个动态库(默认使用动态库) 进程运行时动态库被加载到内存然后通过页表映射到进程地址空间中的共享区此时进程内的所有线程是共享这个动态库的 之前提到每个线程都有独占的栈其中主线程采用的栈是进程地址空间中原生的栈而其余线程采用的栈就是在共享区中开辟的。除此之外每个线程都有各自的struct pthread当中包含了对应线程的各种属性每个线程还有自己的线程局部存储当中包含了对应线程被切换时的上下文数据。
每一个新线程在共享区都有一个struct pthread对其进行描述因此要找到一个用户级线程只需要找到该线程内存块的起始地址然后就可以获取到该线程的各种信息 上面讲述的各种线程函数本质上都是在库内部对线程属性进行的各种操作即线程数据的管理本质是在共享区的进行的
至于pthread_t到底是什么类型取决于实现但对于Linux目前实现的NPTL线程库来说线程ID本质就是进程地址空间共享区上的一个虚拟地址同一个进程中所有的虚拟地址都是不同的因此可以用它来唯一区分每一个线程