对页表的再次理解(以32位为例)
并不是只有一个页表, 地址的前10位对应一级页表,地址的第11-20位对应二级页表,后12位为页内偏移,其实内存和磁盘中的文件,都被分成了以4KB为单位的区域,只不过磁盘中的4KB单元叫"页帧",内存中的4KB单元叫"页框"。
4KB = 2^12 Byte,即只要通过一级页表和二级页表找到内存中对应的页框后,根据页内偏移就能找到对应的资源。
线程在进程内部执行,是OS调度的基本单位。
如何理解线程?
上图中,每一个task_struct就是一个线程,红色方框内是属于一个进程。
创建线程,不用构建新的进程地址空间,页表,所以创建线程比创建进程更轻量化。
创建线程,只需要利用主线程(原进程)的地址空间,页表等资源。
在CPU看来,并不关心执行流是进程还是线程,只认task_struct,此时,可以说Linux没有真正意义上的线程结构,是用进程task_struct模拟线程的,Linux下的进程,统称为:轻量级进程!!!
如何理解进程?
用户视角:内核数据结构+对应的代码和数据!
内核视角:承担分配系统资源的实体!
线程的优点
创建一个新线程的代价要比创建一个新进程小得多;
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多;
线程占用的资源要比进程少很多;
能充分利用多处理器的可并行数量;
在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务;
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现;
I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。
线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型,线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高编写与调试一个多线程程序比单线程程序困难得多
线程异常
因为创建的新线程,与主线程共用地址空间,页表等,所以新线程出现异常就会引起整个线程组异常。
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出 。
线程用途
合理的使用多线程,能提高 CPU 密集型程序的执行效率
合理的使用多线程,能提高 IO 密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
进程VS线程
进程是资源分配的基本单位;
线程是调度的基本单位;
线程共享进程数据,但也拥有自己的一部分数据:
线程 ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
关于主线程和新线程对栈的使用
主线程和子线程用的栈不在同一个区域,主线程用的栈就在地址空间的栈区,但是新线程用的栈区是在pthread库中对应的区域。
那么,怎么区分各个新线程的栈?
cpp
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
cout<<pthread_self()<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void *)"new thread");
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
}
输出结果:
我们发现,新线程的ID值非常大,我们在哪里见到过这么大的值那?--------虚拟地址!!!
其实不然,新线程的ID值,就是用来标识该线程在pthread库中对应的存储信息的起始位置,可以结合前边的图看到。
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式 (SIG_ IGN 、 SIG_ DFL 或者自定义的信号处理函数 )
当前工作目录
用户 id 和组 id
主线程与新线程共用全局变量
cpp
void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
while(true)
{
cout << (char*)args << " : " << g_val << " &: " << &g_val << endl;
g_val++;
sleep(1);
}
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
while(true)
{
cout << "main thread" << " : " << g_val << " &: " << &g_val << endl;
sleep(1);
}
return 0;
}
部分打印结果: (我们发现,新线程修改全局变量,主线程的也会改变,而且地址是一样的!)
如果用 __thread 来修饰全局变量的话,会发现新线程修改全局变量,主线程看到的值并不会发生改变,而且二者对应的地址不同。
在C++中,
__thread
是一个特定于某些编译器(如GCC)的关键字,用于声明线程局部存储(Thread-Local Storage,TLS)的变量。这意味着每个线程都有其自己的该变量的副本,而不是所有线程共享同一个变量。当你在一个全局变量或静态变量前使用
__thread
修饰符时,你告诉编译器这个变量是线程局部的。每个线程在访问这个变量时,实际上是在访问它自己的私有副本,而不是共享的内存区域。
进程ID和线程ID
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符( task_struct )与之对应。进程描述符结构体中的pid ,表面上看对应的是进程 ID ,其实不然,它对应的是线程 ID;进程描述 符中的 tgid ,含义是 Thread Group ID, 该值对应的是用户层面的进程 ID。
认识进程ID和线程ID
cpp
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{}
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
pthread_create(tid + i, nullptr, threadRun, (void *)"");
}
while (true)
{}
}
ps 命令中的 -L 选项,会显示如下信息:
LWP: 线程 ID ,既 gettid() 系统调用的返回值。
NLWP: 线程组内线程的个数
-f
选项表示使用完整格式输出,包括UID, PID, PPID, C, STIME, TTY, TIME, CMD等字段。
其中,PID值和LWP值相同的是主线程,其余的都是新线程。
pthread_ create 函数会产生一个线程 ID ,存放在第一个参数指向的地址中。
因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID ,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程 ID 来操作线程的。
线程库 NPTL 提供了 pthread_ self 函数,可以获得线程自身的 ID :
pthread_t pthread_self(void);
线程控制
POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的"-lpthread"选项
创建线程
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)
(void*), void *arg);
参数
thread:返回线程 ID
attr:设置线程的属性, attr 为 NULL 表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回 0 ;失败返回错误码
线程终止
如果需要只终止某个线程而不终止整个进程 , 可以有三种方法 :
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程 ID
返回值:成功返回 0 ;失败返回错误码
线程等待
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程 ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回 0 ;失败返回错误码
调用该函数的线程将挂起等待 , 直到 id 为 thread 的线程终止。 thread 线程以不同的方法终止 , 通过 pthread_join 得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
分离线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离 :
pthread_detach(pthread_self()) ;
关于新线程退出结果
cpp
void *threadRoutine(void *args)
{
while(true)
{
pthread_testcancel();
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
int count = 0;
while (true)
{
count++;
if (count >= 5)
break;
}
int n= pthread_cancel(tid);
cout <<n<<endl;
cout << "pthread cancel: " << tid << endl;
int *ret = nullptr;
pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
cout << "main thread wait done ... main quit ...: new thead quit : " << (long long)ret << "\n";
return 0;
}
如果调用pthread_cancel来终止线程,则线程的退出码是 -1。
当然,如果新线程指定返回退出结果,可通过(void*)进行强转,然后主线程再(long long)进行强转,就可获得。