线程
一、线程概念
1. 知识支持及回顾
在我们一开始学习进程的时候。我们总说进程在内部执行时,是OS操作系统调度的基本单位。其实并不严谨,今天,我们要重新完善这个说法——线程在进程内部运行,线程是OS操作系统调度的基本单位,进程的操作系统资源分配管理的基本单位。(这是Linux特有的方案,Linux没有真正的线程数据结构,而是用轻量级进程来模拟线程)
在学习线程之前,我们先通过图来了解一下什么是线程。并且回顾一下进程地址空间的知识。
知识支持:
线程:
进程地址空间用户区和内核区:
堆区: 堆区较其他区比较特别,他是零散化使用的。零散化使用就会有很多个内存块,那么就需要管理——用Vm_area_struct,是继进程PCB,页表,struct page后的第四个内核数据结构。
<mm_types.h>
struct mm_struct {
struct vm_area_struct * mmap; /* 虚拟内存区域列表 */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* 上一次find_vma的结果 */
...
}
页帧struct_page 管理 磁盘、物理内存:我们之前学习文件系统时,知道在管理磁盘的文件时是以4KB(扇区*8)为基本单位的。物理内存也同样是以4KB为基本单位的,假设物理内存是4GB,那么就会有100W多个4KB,那么我们也需要将这些4KB管理起来——先描述再组织。为得知这些4KB内存在不在内存,有没有被占用等等的属性,我们就需要用到继进程PCB结构体和页表之后的第三个内核数据结构——页帧struct_page。
如果我们要清空物理内存,难道要操作系统直接清空这么多个4KB吗。不是的,其实是操作系统通过struct_page进行管理,struct_page中有许多的标记位,可以标识各种信息。
页框: 物理内存上的4KB分块叫做页框,由页帧把物理内存上的4KB和磁盘上的4KB映射起来。
MMU: 是一种集成在CPU内的硬件。采取软硬结合的方式,用CPU的硬件映射 与 页表的软件映射 共同维护 虚拟地址空间 与 物理内存 的映射关系。
2. 什么是线程
- 在一个程序里的一个执行路线(执行流)就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
- 一切进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
3. 缺页中断
磁盘和物理内存上都是以4KB为单位划分好了。当程序运行起来之后,需要将磁盘中的可执行程序文件的内容映射到物理内存当中。
-
首先是通过页表的K-V映射(即进程地址空间和物理内存的映射)的标记位来判断是否映射成功。
-
若没有映射成功。操作系统会先申请truct page,并且向物理内存上申请空间,申请成功之后会把磁盘中对应的文件内容填入物理内存当中。
-
并且更改页表中的K-V映射标记位,使其标记位由进程地址空间指向磁盘文件转为由进程地址空间指向物理内存上对应的内存。!
-
缺页中断对于用户来说是零感知的,是透明的,用户无法察觉这个过程。
4.页表的大小、页表的组成
页表由多个表项组成,一个表项对应一个映射。
3个字节:进程地址空间到页表映射。
3个字节:标记位,是否映射成功。
3个字节:页表到物理内存映射。
1~2个字节:各种权限信息。
那么一个表项(用于一个映射)大概就是9~10字节。这里我们假设为9字节。
表项维护一个进程地址空间和物理内存的映射关系假设需要3+3+3+1=9个字节,那么进程地址空间有2^32个地址需要和物理内存映射,那么我们可以粗略算下来需要9字节 * 2^32 =9 * 4GB=36GB。但由于页表并不是像MMU集成在CPU上,而是在物理内存中的。那么光维护映射关系就需要36GB,在32位平台下物理内存可能只有4GB,想要存储36GB的页表这显然是不现实的,所以并是一个表项对应一个字节数据的映射关系。下面我们就来理解下页表是如何建立映射的。
页目录:一级页表,用于维护虚拟内存与页表项映射关系。
页表项:二级页表,用于存放物理内存块(页框)的起始地址。
以32位平台为例,页表的映射过程:
- 选择虚拟地址的前10个比特位,用于从页目录中索引到页表。(存放的是一级页表的虚拟地址)
- 选择虚拟地址的中间10个比特位,用于从页表中索引到对应每个物理内存块(页框)的起始地址。(存放的是二级页表的虚拟地址)
- 选择虚拟地址的后12个比特位,用于存储物理内存块(页框)的偏移量,用 起始地址+偏移量 即可找到对应的字节数据。(存放的是偏移量数据的虚拟地址)
注:在Linux中,32位平台下用的是二级页表,而64位平台下则是多级也表。
所以算下来,一个表还是假设为9个字节。在Linux32位平台下,页目录和页表都只使用了10个比特位,所以他们的表项都为2^10个(一个比特位对应一个表项,一个表项对应一个映射)。那么一个表张(由表项组成)大小为 2^10* 9字节=9KB.并且由于页目录只有一个,而且二级页表有2^10个。则整体算下来大小约为9KB *2^10 =9MB,内存消耗并大,所以Linux采取了这种方法。
这种方法好处在于==,不需要一个表项对应一个映射,只需要一个表项对应一个页框==,因为页框只有100W+个,比2^32远远小。然后使用偏移量数据,再加上这100W个+页框的起始地址,即可找到 2^32个字节数据。
5.线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
6.线程的缺点
-
性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
-
健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
-
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
-
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
7.线程异常
-
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
-
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
8.线程的用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
9.面试题
面试题:
1. 为什么线程的切换成本更低?
答:因为CPU获取数据时有缓存命中存在,所以每次加载进程数据到上下文数据,上下文数据在CPU内命中的多个线程,就可以直接使用线程,不需要重新IO从外设把进程的数据加载到CPU。减少了IO,提高了效率。(CPU内部是有L1~L3 cache寄存器,根据局部性原理,预读到CPU内部,如果进程切换,cache会立即失效,因为新进程过来,要重新缓存。这个局部性原理预读,就导致了缓存命中的存在。)
2. 线程越多效率越高吗?
答:并不是的,在CPU核数(并行处理执行流的个数)与 创建线程数相等时,效率是最高的。如果线程数<CPU核数,则CPU核数没有得到充分利用;如果线程数>CPU核数,则会导致线程切换过多,成本过大。
二、进程VS线程
1.区别
用户视角:进程是内核数据结构+整体的代码和数据,线程就是一个struct pcb和对应分配的代码和数据。
内核视角:进程是资源分配的基本单位,线程是调度的基本单位。
2.线程的私有数据与共享数据:
线程共享进程数据,但也拥有自己的一部分数据**(私有数据)**:
-
线程ID
-
一组寄存器
-
栈
-
errno
-
信号屏蔽字
-
调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境**(共享数据)😗*
-
文件描述符表
-
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
-
当前工作目录
-
用户id和组id
3.进程线程的关系图:
之前我们的进程,是内部只有一个执行流的进程。
今天我们的进程,是内部有多个执行流的进程。
之前的进程是今天进程的子集。
注:单线程进程的PCB叫做进程,多线程进程的PCB叫做线程。
CPU调度的是PCB(task struct),并不关心你是线程还是进程。可能是进程的PCB,也可能是线程的PCB。
不同平台下的线程:
CPU视角下,Linux平台下的PCB <= 其他平台下的PCB 量级。单线程进程时PCB=其他平台下的PCB,多线程进程时PCB=其他平台下的PCB。Linux中并没有线程专门的数据结构,只能用轻量级进程来模拟线程。并不能给我们提供线程相关结构,只能给我们提供轻量级进程接口。这样一来只要学习了进程的接口,就学会了线程的接口,降低了线程学习的成本。Linux在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用。(pthread线程库–原生线程库)在用别人库时
三、线程控制
0. POSIX线程库(原生线程库)
由于线程的接口函数是用户层的库函数,是第三方的,不在默认库函数内,系统不会自动搜寻,所以我们使用该库的时候需要链接。
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11 # 格外注意 //-lpthread去掉前缀后缀就是pthread,就是库的名字
.PHONY:clean
clean:
rm -f mythread
-
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
-
要使用这些函数库,要通过引入头文<pthread.h>
-
链接这些线程函数库时要使用编译器命令的“-lpthread”选项
错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回。
- pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。
1.线程创建
创建线程的函数 pthread_create(用户层接口):
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数说明:
-
thread:获取创建成功的线程ID,该参数是一个输出型参数。
-
attr:用于设置创建线程的属性,传入NULL表示使用默认属性。
-
start_routine:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数。
-
arg:传给线程例程的参数。
返回值说明:
- 线程创建成功返回0,失败返回错误码
功能说明:
- 创建一个执行start_routine函数的新线程
测试:创建一个新线程
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int x = 100;
void show(const string &name)
{
cout <<"线程name:"<< name << ", 进程pid: " << getpid() << " " <<"x="<< x << "\n"
<< endl;
}
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
show(name);
sleep(1);
}
}
int main()
{
pthread_t tid;
char name[64]="hello, i am thread 1";
pthread_create(&tid,nullptr,threadRun,name);//创建新线程
//维持主线程
while(true)
{
printf("i am main thread, 进程pid:%d\n",getpid());
sleep(1);
}
return 0;
}
主线程一旦退出,新线程也会跟着退出,进程也跟着退出,所以要维持主线程的生命。
测试:创建一批线程
在创建一个线程的基础上利用for循环或者手动创建多个即可。
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int x = 100;
void show(const string &name)
{
cout <<"线程name:"<< name << ", 进程pid: " << getpid() << " " <<"x="<< x << "\n"
<< endl;
}
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
show(name);
sleep(1);
}
}
int main()
{
pthread_t tid[5];//存放了线程的tid
char name[64];
for (int i = 0; i < 5; i++)
{
snprintf(name, sizeof name, "%s-%d", "thread", i);
pthread_create(tid + i, nullptr, threadRun, (void *)name);
sleep(1); // 缓解传参的bug
}
//主线程部分
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(3);
}
}
查看进程与线程:
- ps axj 查看进程命令
while :;do ps -aL | head -1&&ps -aL | grep mythread; sleep 1;done
//必须head -1在前,grep mytread在后
- ps -aL 查看轻量级进程(线程)命令(ps -aL查看多个线程,ps -a查看主线程,ps -L查看新线程)
while :; do ps -aL | head -1&&ps -aL | grep mythread ;sleep 1;done
//必须head -1在前,grep mytread在后
我们可以看到产生了两个线程,但是只有一个进程,说明多个线程共享一个进程。
查看线程的tid(用户层面tid,操作系统层面LWP)
- 利用pthread_create中的输出型参数tid,打印查看。
int main()
{
pthread_t tid[5];
char name[64];
for (int i = 0; i < 5; i++)
{
snprintf(name, sizeof name, "%s-%d", "thread", i);
pthread_create(tid + i, nullptr, threadRun, (void *)name);
sleep(1); // 缓解传参的bug
}
//主线程部分
while (true)
{
cout << "main thread,tid:"<<pthread_self()<< " pid: " << getpid() << endl;
for(int i=0;i<5;i++)
{
printf("获取 %d 号线程 tid: %ld,pid:%d\n",i,tid[i],getpid());
sleep(1);
}
sleep(3);
}
}
- 利用ps -aL命令行查看LWP。
- 利用pthread_self(),在线程内部使用该函数获取该线程的tid。(类似用于查看进程pid的getpid())
tid与LWP区别:
为什么tid的数值这么大?其实这些都是地址,因为pthread的接口都是用户层面的,唯一的标识符是虚拟地址。而LWP是操作系统层面的唯一标识符。tid与LWP是一一对应的。
2.线程等待
线程如果创建出来,主线程不退出,进程不退出,导致新线程无法被回收的话,会产生类似于僵尸进程一样的线程,僵尸进程可以用ps ajx观察到,但是僵尸线程无法用ps -aL观察到。但这个问题真实存在。那如何解决呢?
等待线程的函数接口 pthread_join(用户接口)
int pthread_join(pthread_t thread, void **retval);
参数说明:
- thread:被等待线程的ID。
- retval:线程退出时的退出码信息。
返回值说明:
- 线程等待成功返回0,失败返回错误码。
功能说明:
- 等待,回收一个线程。
总结:
- 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。返回给pthread_join的args输出型参数。
- 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
- 返回给pthread_join的args输出型参数。
int pthread_cancel(pthread_t thread);
- 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。返回给pthread_join的args输出型参数。
void pthread_exit(void *retval);
- 如果不关心thread线程的终止状态,可以传NULL给retval参数。
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int x = 100;
void show(const string &name)
{
cout << "线程name:" << name << ", 进程pid: " << getpid() << " "
<< "x=" << x << "\n"
<< endl;
}
struct messge
{
public:
messge(int i,char* name):i_(i),name_(name)
{
;
}
public:
int i_;
char* name_;
};
void *threadRun(void *args)
{
int idex = ((messge*)args)->i_;
printf("thread_create sucess,i am %s\n", ((messge*)args)->name_);//线程创建成功
sleep(10);
//线程退出
switch (idex)
{
case 0:
{
printf("thread 0退出\n");
return 0;
}
case 1:
{
printf("thread 1以return方式退出\n");
return (void *)10;
}
case 2:
{
pthread_t tid = pthread_self();
printf("thread 2以pthread_cancel方式退出\n");
pthread_cancel(tid);
break;
}
case 3:
{
pthread_t tid = pthread_self();
printf("thread 3以pthread_exit方式退出\n");
int n=14;
pthread_exit((void*)14);
break;
}
}
return nullptr;
}
int main()
{
char name[64];
pthread_t tid[4]; // 创建一批线程用于测试线程等待
for (int i = 0; i < 4; i++)
{
snprintf(name,sizeof name,"thread %d",i);
messge mess(i,name);
pthread_create(tid + i, nullptr, threadRun, (void*)(&mess));
sleep(1);
}
//等待线程退出,阻塞式等待
for (int i = 0; i < 4; i++)
{
int *args=nullptr;
switch (i)
{
case 0:
{
pthread_join(tid[i], nullptr);
printf("不关心线程返回值\n");
break;
}
case 1:
{
pthread_join(tid[i], (void**)&args);
printf("线程被回收,return返回值:%lld\n",(long long)args);
break;
}
case 2:
{
pthread_join(tid[i],(void**)&args);
printf("线程被回收,pthread_cancel退出信息:%lld (PTHREAD_CANCELED)\n",(long long)args);
break;
}
case 3:
{
pthread_join(tid[i],(void**)&args);
printf("线程被回收,pthread_exit退出参数:%lld\n",(long long)args);
break;
}
}
sleep(1);
}
while (true)
{
printf("i am main thread\n");
sleep(1);
}
return 0;
}
注:"PTHREAD_CANCELED 是/usr/include的 库里面的函数,"用grep -ER “PTHREAD_CANCELED” /usr/include/ 可以发现这个常数是一个宏,且值为-1 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t4F4elTI-1673976996514)(C:/Users/20366/AppData/Roaming/Typora/typora-user-images/image-20230117123636160.png)]
3.线程终止
终止线程方式有三种:
- 在当前线程中用return;
代码:
printf("thread 1以return方式退出\n");
return (void *)10;
- 在当前线程或者其他线程中用pthread_cancel;(用其他线程pthread_cancel(tid,(void**)&args);才能获取到退出线程的退出信息)
函数:
int pthread_cancel(pthread_t thread);
参数:
- thread :要取消的线程的tid(tid的获取方式参考线程创建)
返回值:
- 成功返回0,失败返回错误码。
代码:
printf("thread 2以pthread_cancel方式退出\n");
int n=pthread_cancel(tid);
printf("pthread_cancel获取返回值%d",n);
- 在当前线程中用pthread_exit();
函数:
void pthread_exit(void *retval);
参数:
- retval:线程退出的退出信息。
代码:
printf("thread 3以pthread_exit方式退出\n");
int n=14;
pthread_exit((void*)14);
注:以上都是线程的终止方式。如果终止的是新线程,则不会影响其他线程;如果终止的是主线程,那么其他线程,包括整个进程都会退出。exit()是终止进程的接口函数,不论是哪个线程使用exit(),都会导致其他线程包括主线程,以及整个进程都退出在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行。主线程调用pthread_exit只是退出主线程,并不会导致进程的退出。
详情请参考进程等待的代码及图片。
4.分离线程
-
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
-
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
参数说明:
- thread:被分离线程的ID。
返回值说明:
- 线程分离成功返回0,失败返回错误码。
功能:让一个线程分离,不需要join等待,会自动回收该线程。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。意思就是只能是 可等待态 和 已分离态 二者之一。
若在分离之后又进行等待,那么就会等待失败,返回错误码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg )//新线程执行分离线程的操作
{
pthread_detach(pthread_self());
printf("%s\n", (char*)arg);
return NULL;
}
int main( void )
{
pthread_t tid;
if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 ) {
printf("create thread error\n");
return 1;
}
int ret = 0;
sleep(1);//很重要,要让线程先分离,再等待
int n= pthread_join(tid, NULL );
if (n==0 ) {
printf("pthread wait success\n");
ret = 0;
} else {
printf("pthread wait failed,errorn:%d , sterror(n):%s\n",n,strerror(n));
ret = 1;
}
return ret;
}
5.线程ID及进程地址空间布局
- pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和内核中的LWP不是一回事。
- 内核中的LWP属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,这个ID属于NPTL线程库的范畴,线程库的后续操作就是根据该线程ID来操作线程的。
- 线程库NPTL提供的pthread_self函数,获取的线程ID和pthread_create函数第一个参数获取的线程ID是一样的。
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。转到宏定义我们发现,pthread_t其实是unsigned long int无符号长整数。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址,只不过是把地址的指针形式转化成了长整数形式。
我们可以用pthread_self()或pthread_create()获取pthread_t tid,并打印一下,可以发现正如地址般的大小长度。
线程之间是独立享有自己的栈结构的。当是单线程单进程时,该单线程就是独自享有整个地址空间的栈结构;而当两个及以上的线程,也就是多线程单进程时,这些线程也会独自享有自己的栈结构。那么这些栈结构哪来的呢?其实是库提供的,库提供了(1.struct_pthread轻量级进程数据结构-pcb。2.线程局部存储-用于存储上下文数据。3.线程栈-用于存储变量。)组成的块。整个动态库里面可以存放有很多个这种为线程存储结构及代码与数据的块。每个块对应每个线程,且都是被线程独自享有的。每个线程可以通过线程的起始地址进行访问这些块,也就是获取pthread_t tid用户层标识符——虚拟地址即可访问这些块,从而获取线程的各种信息。
用lld命令可以查询该库。
ldd pthread //用于查询可执行程序的链接情况
去掉前缀lib,去掉后缀.so,剩下的就是链接的库的名字。我们可以看到链接的是动态库,pthread。struct_pthread正是pthread库提供的。
6.面试题
面试问题:
- 请简述什么是LWP?
答:LWP是Linux下线程的用户层面的唯一标识符,是虚拟地址。Linux平台下是没有专门的线程的数据机构的,而是用轻量级进程来模拟线程。因为有缓存命中存在,所以每次加载进程数据到上下文数据,上下文数据在CPU内命中多个线程,就可以直接使用线程,不需要重新IO从外设把进程的数据加载到CPU,所以Linux的线程叫做轻量级进程。进程是资源分配的基本单位,线程是系统调度的基本单位。线程的内核层面的tid和用户层面的LWP是一一对应的。
- 请简述LWP与pthread_create创建的线程之间的关系
答:pthread_create是一个用户层面的库函数。phread_create的tid是输出型参数,pthread_create功能是创建一个新线程,并且会在tid处返回新线程的tid,而tid是内核层面的线程的唯一标识符,LWP是用户层面的线程的唯一标识符,LWP是虚拟地址,LWP和tid是一一对应的。
- 简述轻量级进程ID与进程ID之间的区别
答:轻量级进程的ID,是一个pcb的id;进程的ID是内核数据结构+全部代码和数据的id,轻量级进程是系统调度的基本单位,进程是资源分配的基本单位。多个轻量级进程共享一个进程,其上的数据来自于进程。当单线程单进程时,该线程-轻量级进程的上的栈是进程虚拟地址空间的栈。如果是多线程单进程时,这些线程都有字节独立的栈结构,这些栈结构是库函数里面的栈。多个轻量级进程的id对应一个进程的id。进程是由多个线程组合在一起的线程组。