目录
概述
本篇主要讲解线程相关知识和概念、线程的相关函数使用、二级页表的讲解。
一. 线程概念
1 什么是线程
之前我们学过进程,而什么是线程呢??
是LWP(轻量级进程),是内核级线程,由操作系统直接管理和调度 。
1、在一个程序里的一个执行路线就叫做线程。更准确的定义是:线程是“一个进程内部的控制序列”
2、一切进程都至少有一个执行线程
3、线程在进程内部运行,本质是在进程地址空间内运行
4、在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化
5、透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
LWP==内核级线程,即具备进程的调度能力,又有线程的资源共享优势,常用于高并发,低开销的场景。
上图中的一个task_struct就是一个线程,由此可见, linux中不存在真正的线程,而是由进程模拟实现的。
Linux中都没有真正意义上的线程了,自然也就没有真正意义上的与线程相关的系统调用了。但是Linux提供了创建轻量级进程的接口,也就是创建进程,共享空间,比如vfork函数。
![]()
vfork可以创建子进程,但是父子共享进程地址空间。
vfork的返回值与fork函数返回值相同,都是给父进程返回子进程PID,给子进程返回0。vfork创建的子进程与父进程共享进程地址空间,下面用代码进行演示,父进程使用vfork函数创建子进程,子进程改变全局变量,父进程读取全局变量,看看读取的全局变量是否经过修改。
线程库pthread
Linux中,在内核角度没有真正意义上与线程有关的接口,但是站在用户角度,系统为用户封装并提供了原生线程库pthread。
pthread库其实就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口。
因此对于我们来讲,我们要在Linux下学习线程实际上就是学习用户层的这一套接口,而非操作系统的接口。
2、进程VS线程
进程是一个执行起来的程序,是承担分配系统资源的基本实体
线程是进程中的一个执行流,执行粒度比进程更细,是OS调度的基本单位
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有⾃⼰的⼀部分数据,如:
◦ 线程ID
◦ ⼀组寄存器
◦ 栈
◦ errno
◦ 信号屏蔽字
◦ 调度优先级
3、线程的优点
创建一个新线程的代价比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作更少线程占用的资源比进程少很多
能充分利用多处理器的可并行数量
在等待慢速IO操作结束的同时,程序可以执行其他的计算任务
计算密集型应用,为了能在多处理器上运行,将计算分解到多个线程中实现
IO密集型应用,为了提高性能,将IO操作重叠,线程可以同时等待不同的IO操作
4、线程的缺点
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程需要更全面深入的考虑,在一个多线程程序里,因时间分配上的细微差别或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说,线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:编写与调试一个多线程程序比单线程程度困难得多。
5、 线程异常
单个线程如果出现除0,野指针等问题导致线程崩溃,进程也会随着崩溃。
线程是进程的执行分支,线程出现异常,就类似进程出现异常,会除法信号机制,终止进程,进程终止之后该进程内所有线程也会随即退出。
6、线程用途
合理地使用多线程,能提高CPU密集型程序的执行效率。
合理地使用多线程,能提高IO密集型程序的用户体验。
二、线程控制
上面我们知道了上面是线程和相关概念,下面让我们看看任何使用线程来完成对应任务。
1. 线程创建
pthread_creat创建的进程,是线程库层的抽象,无需陷入内核,需要映射到LWP中才能使用。
pthread_create函数用于创建一个新的线程。其函数原型如下:
#include<pthread.h> pthread_t thread; int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数含义:
pthread_t
类型的变量,能够存储系统分配给每个线程的唯一标识符。thread:指向pthread_t类型变量的指针,用于存储新创建线程的标识符。通过这个标识符,后续可以对该线程进行各种控制操作。
attr:指向线程属性对象的指针。线程属性可以用于设置线程的栈大小、调度策略等特性,不过在简单场景下,使用默认属性即可满足需求。如果为NULL,则使用默认的线程属性
start_routine:指向任务函数的指针。新创建的线程将从这个函数开始执行,该函数的返回值类型为void*,并且接受一个void*类型的参数。
arg:传递给线程函数start_routine的参数。由于该参数类型为void*,因此可以传递任何类型的数据指针,在传递时需要进行类型转换。
返回值:成功时返回0,失败时返回一个非零的错误码,不同的错误码对应不同的错误情况,例如EAGAIN表示系统资源不足,无法创建新线程。
2、获得当前的线程ID
pthread_self函数,用于获得当前线程的ID(不是轻量级进程LWP)
#include<pthread.h> pthread_t pthread_self(void)
返回值:获得当前进程的线程ID,用法与getpid用法类似。
3. 线程等待
pthread_join函数用于等待指定线程终止,并获取其返回值。
#include<pthread.h> int pthread_join(pthread_t thread, void **retval);
thread:要等待的线程标识符,该标识符通常是在调用pthread_create函数创建线程时获取的。
retval:指向void*类型指针的指针。如果该参数不为NULL,pthread_join函数会将等待线程的返回值存储到*retval指向的位置。通过这种方式,可以获取线程函数执行完毕后的结果。
如果不关心可以直接设成NULL。
退出码信息:
return -> 返回值
pthread_cancel -> 常数(PTHREAD_CANCEL)
pthread_exit -> 参数
返回值:成功时返回0,失败时返回非零的错误码,常见的错误码如EINVAL表示指定的线程标识符无效。
4. 线程分离
pthread_detach函数用于将线程设置为分离状态。处于分离状态的线程在终止时,系统会自动回收其资源,无需其他线程调用pthread_join来等待。
#include<pthread.h> int pthread_detach(pthread_t thread);
参数含义:thread为要设置为分离状态的线程标识符。
返回值:成功时返回0,失败时返回非零的错误码,例如EINVAL表示线程标识符无效。
5. 线程终止
pthread_exit函数用于终止当前线程。
#include<pthread.h> void pthread_exit(void *retval);
参数含义:retval是当前线程的返回值,该返回值可以被其他调用pthread_join等待该线程的线程获取。同样,由于retval类型为void*,可以传递任何类型的数据指针。
返回值:该函数没有返回值,它直接终止当前线程的执行。
6、线程取消
pthread_cancel
函数用于请求取消一个线程的执行。其函数原型如下:#include<pthread.h> int pthread_cancel(pthread_t thread);
- 参数含义:
thread
:要取消的线程标识符,该标识符通常是在调用pthread_create
函数创建线程时获取的。- 返回值:成功时返回
0
,失败时返回非零的错误码,例如ESRCH
表示指定的线程不存在。- 新线程也可以取消老线程(不推荐)
7、用例
下面是一个简单用例
#include <stdio.h> #include <pthread.h> #include <stdlib.h> // 线程函数 void* thread_function(void* arg) { int num = *(int*)arg; printf("线程开始执行,参数值为:%d\n", num); // 模拟线程执行一些任务 for (int i = 0; i < 5; i++) { printf("线程执行中...\n"); } int result = num * 2; // 线程执行完毕,返回结果 return (void*)&result; } int main() { pthread_t thread; int arg = 10; int *result; // 创建新线程 if (pthread_create(&thread, NULL, thread_function, (void*)&arg) != 0) { perror("pthread_create"); return 1; } // 等待线程终止并获取返回值 if (pthread_join(thread, (void**)&result) != 0) { perror("pthread_join"); return 1; } printf("线程执行完毕,返回值为:%d\n", *result); return 0; }
在这个示例中:
- 首先定义了一个线程函数thread_function,该函数接收一个整数参数,执行一些模拟任务后,返回该参数的两倍。
- 在main函数中,使用pthread_create创建一个新线程,并传递参数arg。
- 然后调用pthread_join等待新线程终止,并获取其返回值。
- 最后输出线程的返回值,程序结束。
三、二级页表
这就是我们所谓的二级页表,其中页目录项是一级页表,页表项是二级页表。
每一个表项还是按10字节计算,页目录和页表的表项都是210个,因此一个表的大小就是210 * 10个字节,也就是10KB。而页目录有210个表项也就意味着页表有210个,也就是说一级页表有1张,二级页表有210张,总共算下来就是10MB,内存消耗并不高,Linux实际上就是这样映射的。
上面所说的所有映射过程,都是由MMU这个硬件完成的,该硬件是集成在CPU内的。页表是一种软件映射,MMU是一种硬件映射,所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式。
还是以32位平台为例,其页表的映射如下:
选择虚拟地址的前10个比特位在页目录下进行查找,找到对应的页表
再选择虚拟地址的10个比特位在对应的页表当中进行查找,找到物理内存中对应页框的起始地址
最后将虚拟地址中剩下的12个比特位作为偏移量从对应页框的起始地址处向后偏移,找到物理内存中某一个对应的字节数据
————————————————
本篇讲解到此结束