目录
线程概念
真-线程概念:
历史上:
示意图:
Linux线程的原理:
重新理解进程的概念:
疑问:
线程优缺点:略了
进程vs线程
线程如何看待进程内部的资源:
验证每个线程有独立的信号屏蔽字:
为什么线程切换的成本更低,而进程切换成本更高?
线程控制
pthread库介绍:
pthread线程库API:
线程创建:pthread_create
线程等待:pthread_join
线程终止:pthread_exit
线程分离:pthread_detach
补充
线程id,线程的独立栈结构,线程的局部存储,
线程id的本质
线程的独立栈结构
clone系统调用:
__thread :线程的局部存储
浅谈C++线程库
补充:
OS是可以做到使进程进行资源的细粒度划分的。
早期,操作系统中是没有线程的,也就是只有进程这个概念,一个进程内只有一个执行流。:60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
线程有着自己的特点,比如执行粒度更细,更轻量化,调度切换的开销更小等等,而不同操作系统设计线程时有着不同的方案,Windows对于线程,设立了全新的数据结构,进程和线程划分的很清晰,这是比较复杂的。而因为线程创建,执行,切换,销毁等等很多行为都和进程有着很大的相似性,因此Linux采用了用进程模拟线程的设计方案(实现了进程内核代码的复用),这样的设计方案虽然没有为线程设计全新的数据结构,但是最终设计出的“轻量级进程”依旧符合线程的要求。
上图为学习线程之前,进程加载运行的示意图,每个进程都有一个task_struct(Linux),即进程PCB。
Linux线程原理:OS内,如果我们创建“进程”时,不创建新的地址空间,用户级页表,不进行IO将程序的代码和数据加载到内存,只创建task_struct,让这个新的PCB指向旧的PCB(创建此新线程的主线程)指向的地址空间mm_struct,再通过一定的技术手段,将当前进程的资源合理划分给不同的task_struct,此时,这里的每一个task_struct,就称为一个线程。
如上红色区域即进程全部。
从用户视角来说:进程=内核数据结构+进程对应的代码和数据(内核数据结构中PCB的数量>= 1)
从内核视角来说:进程:承担分配系统资源的基本实体。(因为在进程创建时,系统给这个进程分配资源。而线程是使用创建此线程的进程的部分资源,进程进行资源分配,分配给线程。故进程才是承担分配系统资源的基本实体)
对于线程这块我有个疑惑,既然CPU和OS调度的基本单位是线程,且一个进程内至少有一个执行线程(一个执行流),那能不能说CPU和OS只能调度线程呢,也就是理解为CPU调度一个单执行流进程时,本质上也是调度此进程内的一个执行线程?
cpu调度的最小单位_为什么说线程是CPU调度的基本单位?_兮辞之曰的博客-CSDN博客
调度是一方面,另一方面是调度的目的是什么。如果调度的目的是为了让执行流去执行,那肯定是让线程去跑。而如果目的是以进程整体为单位进行资源分配,则OS也是可以做到调度整个进程的。对于CPU来说,特别是Linux下,CPU并不关心线程还是进程,它只关心task_struct,因为在Linux下,只存在轻量级进程,不存在线程。只是用轻量级进程去模拟线程。
(这块实在有些抽象,且学的不是很多,之后再慢慢理解吧)
不想弄,不想弄,不想弄。
进程是资源分配的基本单位,线程是调度的基本单位。
进程内的所有线程共享同一个进程地址空间,则其中的代码区,全局数据区,共享区,命令行参数和环境变量,内核区都是共享的。而对于堆区和栈区,根本上来说是共享的,因为一个线程可以将栈帧内的局部数据或堆区开辟空间的地址通过全局数据的方式传递给其他线程,其他线程也可以访问。但是一般情况下我们不会这样做,所以也可以认为栈区和堆区是线程私有的。
除此之外,各线程还共享进程内的文件描述符,各种信号的处理方式(SIG_IGN,SIG_DFL或者自定义的信号处理函数(代码区)),当前工作目录,用户id和组id等...
以上为线程共享进程内的部分数据,也有属于线程自己的一部分数据:
线程id,errno,信号屏蔽字,调度优先级。
最重要的是:每个线程私有 一组寄存器和栈。
一组寄存器:线程是CPU调度的基本单位,每个线程一定有自己的上下文。在线程被CPU调度时,上下文数据就会保存在CPU内的一组寄存器中。
栈:每个线程运行时要调用函数,一定有出栈入栈的行为,形成的临时变量会保存在栈内,故每个线程必须有自己的私有栈。
一组寄存器和栈能体现出线程的动态属性。
#include
#include
#include
#include
#include
#include
using namespace std;
void showSigblock(sigset_t* curSigset);
void *routine(void *arg)
{
int cnt = 0;
while (true)
{
cnt++;
sleep(1);
cout << "新线程的信号屏蔽字为 : ";
sigset_t curSigset;
sigprocmask(SIG_BLOCK, nullptr, &curSigset);
showSigblock(&curSigset);
if(cnt == 5) break;
}
string *p = new string("新线程执行完毕");
pthread_exit(reinterpret_cast(p));
}
void showSigblock(sigset_t* curSigset)
{
for (int i = 1; i <= 31; ++i)
{
if (sigismember(curSigset, i))
cout << "1";
else
cout << "0";
}
cout << endl;
}
int main()
{
// 主线程
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, 8);
sigaddset(&sigset, 1);
sigaddset(&sigset, 11);
sigprocmask(SIG_BLOCK, &sigset, nullptr);
sigset_t curSigset;
sigprocmask(SIG_BLOCK, nullptr, &curSigset);
cout << "主线程此时的信号屏蔽字为 : ";
showSigblock(&curSigset);
pthread_t pid;
pthread_create(&pid, nullptr, routine, nullptr);
sleep(3);
sigaddset(&sigset, 3);
sigprocmask(SIG_BLOCK, &sigset, nullptr);
cout << "主线程成功将3号信号进行屏蔽 : ";
sigprocmask(SIG_BLOCK, nullptr, &curSigset);
showSigblock(&curSigset);
void* retThread = nullptr;
pthread_join(pid, &retThread);
cout << *(string*)retThread << endl;
delete (string*)retThread;
return 0;
}
.[yzl@VM-4-5-centos Thread]$ ./mythread
主线程此时的信号屏蔽字为 : 1000000100100000000000000000000
新线程的信号屏蔽字为 : 1000000100100000000000000000000
新线程的信号屏蔽字为 : 1000000100100000000000000000000
主线程成功屏蔽3号信号: 1010000100100000000000000000000
新线程的信号屏蔽字为 : 1000000100100000000000000000000
新线程的信号屏蔽字为 : 1000000100100000000000000000000
新线程的信号屏蔽字为 : 1000000100100000000000000000000
新线程执行完毕
进程内的线程之间切换时,进程地址空间和页表不需要切换(尽管它们在CPU内就是一个寄存器值),CPU内的进程范畴的状态寄存器也不需要切换。这些成本不是很高,故不是最重要的。
重点是:CPU内有L1~L3的cache(高速缓存),对内存的代码和数据,根据局部性原理,预读到CPU内部。若进程切换,则cache立即失效,切换为新进程之后,cache需要进行预热和重新缓存。这个的影响更大。
GPT:线程切换的成本比进程切换的成本低的原因是:线程是轻量级的,共享父进程的资源,切换时需要保存的状态信息相对较少;而进程是独立的,拥有自己的资源,切换时需要保存的状态信息相对较多。因此线程切换的速度比进程切换的速度快。
Linux操作系统并没有设计真正的线程,而是用轻量级进程来模拟线程。故Linux无法直接提供针对线程的系统调用(如线程创建,终止...),最多只能提供轻量级进程的系统接口。但是Linux必须满足用户对于线程的使用需求。最终,在用户层提供了一个pthread库,这里面包含了用户对于线程的使用接口。严格来说,这个pthread库是第三方库,并不是系统库和语言库。但是所有版本的Linux操作系统都直接提供了这个线程库在系统默认路径中。(上图为动静态库和头文件)
因为这个库严格来说并不是系统库和语言库,而是第三方库。故用g++编译时要加上-lpthread指令说明要链接这个库。(g++默认使用动态库进行动态链接,头文件在源文件中已经声明了,而因为libpthread-2.17.so已经在系统默认搜索路径下了,故编译好之后可以直接运行可执行,不会出现链接错误)
pthread_create - create a new thread
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
pthread_join - join with a terminated thread
int pthread_join(pthread_t thread, void **retval);
pthread_exit - terminate calling thread
void pthread_exit(void *retval);
pthread_cancel - send a cancellation request to a thread
int pthread_cancel(pthread_t thread);
pthread_detach - detach a thread
int pthread_detach(pthread_t thread);
pthread_self - obtain ID of the calling thread
pthread_t pthread_self(void);
线程id地址(本质是一个unsigned long int类型),输出型参数。线程属性设置,直接设为nullptr即可。新线程的执行函数,函数指针类型,参数和返回值必须为void*(一般是通过强转进行的)。线程执行方法的参数。
①:线程id,②:用void**接收线程执行的start_routine函数的返回值void*。这里传void**接收,是C语言典型的输出型参数指针传参问题...
线程等待的原因:
若不关心一个新线程的退出结果,在新线程终止后也不想join回收新线程。则可以调用pthread_detach分离新线程。作用类似于signal(SIGCHLD, SIG_IGN);
在Linux下,调用pthread_detach函数将线程分离,使得该线程独立运行,不再需要由任何其他线程等待其终止。分离的线程不需要被回收,内存资源在线程终止时自动回收。
#include
#include
#include
#include
#include
#include
using namespace std;
#define N 10
void* routine(void* arg)
{
long long num = 0;
for(int i = 0; i < N; ++i)
{
if(((int*)arg)[i] % 2 == 0) num++;
}
pthread_exit((void*)num);
// return (void*)num; // 偶数个数
}
int main()
{
int* p = new int[N];
srand(time(nullptr));
for(int i = 0; i < N; ++i)
{
p[i] = rand()%100;
}
pthread_t pid = 0;
pthread_create(&pid, nullptr, routine, (void*)p);
void* retVal;
pthread_join(pid, &retVal);
cout << "共" << (long long)retVal << "个偶数" << endl;
return 0;
}
前面说了,Linux操作系统只设计提供了轻量级进程,而用户需要的是线程。于是有了用户层的pthread线程库。
因此,对于线程的管理,OS承担一部分,pthread库承担一部分。OS承担的是对于轻量级进程的创建调度等和对于内核数据结构的管理。而库需要进一步包装描述内核的轻量级进程,需要提供一些线程相关的属性字段,比如线程id。
OS完成的是对轻量级进程的调度,管理工作。而线程的用户层属性是pthread库管理的,管理就要先描述再组织。
线程id就是这个线程在pthread库中的属性集合的起始地址。pthread库,如动态库是会加载到进程地址空间中的共享区的,因此线程id本质就是共享区的一个地址。
之前我们说过,线程有两个很重要的私有数据:一组寄存器和栈。
进程内的所有线程共享一个进程地址空间,进程地址空间中代码区,全局数据区,堆区,共享区,内核区都没有问题,可是如何保证每个线程有自己独立的栈结构?所有线程共享一个栈空间是不可以的。
新线程的栈空间是用户层pthread库提供并维护的。
如上图所示,主线程使用的是进程地址空间中内核级的栈区,而新线程使用的是pthread库提供的用户级栈区。从而保证每个线程有自己独立的栈结构。
上图为验证线程id的程序,发现线程id转化为地址之后确实在堆栈之间的共享区中,因为是动态链接的pthread动态库。暂时遇到一些问题,没法链接静态库,故无法进一步验证。
Linux提供了一个clone系统调用,作用是创建一个轻量级进程,和所属进程共享一个进程地址空间,而这个轻量级进程独立的栈结构就可以通过第二个参数void* child_stack传递。pthread_create内部可能就调用了这个clone系统调用。
GPT:
在Linux中,进程对不同线程进行资源划分的方法是:通过Clone系统调用创建线程。Clone系统调用允许父进程在创建子进程时,指定如何共享资源。
每个线程都有一个进程控制块(PCB),该PCB记录了该线程的信息,如线程ID、执行状态等。线程之间可以共享进程的一些资源,如文件描述符表、地址空间、进程ID等。同时,每个线程还有一些独有的资源,如线程栈、寄存器状态等。
因此,在Linux中,线程可以通过Clone系统调用进行资源划分,使得多个线程可以共享一些公共资源,同时又有独立的资源。这种资源划分方法有助于提高程序的效率,同时又保证了线程之间的隔离性。
全局变量在全局数据区,是所有线程共享的。如果希望每个线程都有一份独立的某全局变量,则可以用__thread修饰该全局变量。这称为线程的局部存储。
其实本质上就是存储在了pthread库中每个线程属性集合内的线程局部存储空间中(看上方图)
GPT :__thread可以用于修饰全局变量和局部变量,它表示线程本地存储(Thread Local Storage),即每个线程都有自己独立的存储空间,不与其他线程共享。使用__thread修饰的变量,在同一线程中可以直接访问,不需要加锁,效率更高。
#include
#include
#include
#include
#include
#include
using namespace std;
// 验证线程id的本质
__thread int num = 6;
void* routine(void* arg) {}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, nullptr);
int a = 10;
int *p = new int(10);
printf("栈区 : %p\n", &a);
printf("tid : %p\n", tid);
printf("堆区 : %p\n", p);
printf("num : %p\n", &num);
return 0;
}
.[yzl@VM-4-5-centos Thread]$ ./mythread
栈区 : 0x7ffdf25d43cc
tid : 0x7f0b310f8700
堆区 : 0x1f3d260
num : 0x60105c
[yzl@VM-4-5-centos Thread]$ make
g++ -o mythread mythread.cc -std=c++11 -lpthread
.[yzl@VM-4-5-centos Thread]$ ./mythread
栈区 : 0x7ffed17feccc
tid : 0x7f56c3fb1700
堆区 : 0x67a270
num : 0x7f56c4fce77c
第一次运行时num没有被__thread修饰
#include
#include
#include
#include
#include
// #include
#include
// using namespace std;
// C++线程库
void routine()
{
std::cout << "haha" << std::endl;
}
int main()
{
std::thread t1(routine);
t1.join();
return 0;
}
[yzl@VM-4-5-centos Thread]$ ldd mythread
linux-vdso.so.1 => (0x00007ffdf5fbd000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fe163154000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fe162e4c000)
libm.so.6 => /lib64/libm.so.6 (0x00007fe162b4a000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fe162934000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe162566000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe163370000)
如上所示,虽然这个程序没有包pthread.h头文件,但是这个可执行依旧链接了pthread动态库。因为使用了C++线程库。
故,C++线程库底层一定使用了原生线程库。(虽然pthread库不是系统调用,但对于Linux用户来说,地位上几乎已经和系统调用等价了),任何一个语言,要想在Linux下跑,一定要使用原生线程库。
语言上的线程库,底层必须使用原生线程库,一定是对原生线程库进行封装,目的是让用户更好地使用。类似于C语言的FILE结构体内部一定包含文件描述符fd字段。
Linux通过内存管理机制和进程管理机制实现了进程对内存资源的细粒度划分。
vm_area_struct是内核内存管理的一个数据结构,它表示一个进程的虚拟内存地址空间的一个连续区域(例如代码段、堆、栈等)。通过这个数据结构,系统可以对进程的内存资源进行管理和分配。(例如将代码段中的某连续区域或堆区的某块区域划分给不同的线程)
因此,vm_area_struct与进程的资源细粒度划分具有密切关系,它是实现进程内存资源管理的关键数据结构。(进程/线程在堆区不断开辟空间,进程地址空间中堆区只有一个start和end,是无法准确管理每次申请的堆区空间的,底层就是使用vm_area_struct对每一段空间进行管理)
vm_area_struct结构包含了以下关键字段:
这些字段是vm_area_struct的核心内容,它们共同描述了进程的虚拟内存地址空间,以及如何管理和使用这些虚拟内存。
(和进程对不同线程进行资源划分联系起来)
这个bCSDN不知道为什么不能上传图片,之后再说吧。