编辑推荐: |
本文主要介绍了linux下的线程、页表理解——虚拟到物理地址之间的转化、线程的接口、用户级线程概念、分离线程和线程互斥。希望对您的学习有所帮助。
本文来自一条长河,由火龙果软件Linda编辑、推荐。 |
|
一.linux下的线程
1.linux下的线程概念
(1)教材上粗略的 线程 定义
1.在进程内部运行的执行流(线程在进程的虚拟地址空间中运行)
2.线程比进程力度更细调度成本更低
3.线程是CPU调度的基本单位
(2)线程的引入
fork之后,父子是共享代码的
可以通过if else判断,让父子进程执行不同的代码块——>引出不同的执行流,可以做到进行对特定资源的划分
(3)线程真正定义 以及 示意图
线程(执行流)是系统调度的基本单位!linux下没有真正的线程,Linux的线程是用进程模拟的,他叫做
轻量级进程。线程执行力度比进程更细,调度成本更低(进程切换时不需要切换页表,电地址空间等,只需要切换线程的上下文数据),因为他执行的是进程的一部分,访问的是进程的一部分资源,使用进程的一部分数据
(4)linux 和 windows等其他操作系统的线程对比
Linux下认为:
进程和线程在概念上没有区别,他们都叫做执行流。
Linux的线程是用进程模拟的(实际上用进程的PCB模拟的)
linux下的tcb就是pcb,因为他们的逻辑结构是一样的
其他操作系统(例如windows)认为:
进程和线程在执行流层面是不一样的,新增了TCB这个结构体,会导致维护成本变高
线程:进程=n:1 进程——PCB;线程——TCB(thread control block)
现在CPU看到的所有的task_ struct都是一个执行流(线程)
2.重新定义进程
曾经:进程——内核数据结构+进程对应的代码和数据
现在:进程——内核视角:承担分配系统资源的基本实体 (进程的基座属性),即:向系统申请资源的基本单位!
内部只有一个执行流 task_struct 的进程——单执行流进程
内部有多个执行流 task_struct 的进程——多执行流进程
线程(执行流)是调度的基本单位!
下面紫色框起来的进程PCB,虚拟内存,页表,内存中的数据和代码,这一组资源的集合叫做一个进程。
轻量级进程解释:
task_ struct <= 传统的进程PCB,如果是单执行流的进程(只有一个task_ struct时),task_
struct = 传统的进程PCB;多执行流的进程task_ struct < 传统的进程PCB
(1)线程的优点
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
(2)线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
(3)线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该
进程内的所有线程也就随即退出
(4)线程用途
合理的使用多线程,能提高CPU 密集型程序的执行效率
合理的使用多线程,能提高 IO 密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是
多线程运行的一种表现)
3.线程和进程的共享/私有资源
进程的多个线程共享 同一地址空间 , 因此 Text Segment 、 Data Segment
都是共享的 , 如果定义一个函数 , 在各线程中都可以调用, 如果定义一个全局变量 , 在各线程中都可以访问到
, 除此之外 ,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式 (SIG_ IGN 、 SIG_ DFL 或者自定义的信号处理函数 )
当前工作目录
用户 id 和组 id
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己私有的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
4.进程和线程的关系如下图:
二.页表理解——虚拟到物理地址之间的转化
1.页表理解
虚拟地址在被转化的过程中,不是直接转化的
虚拟地址是32位的:32bit 分成 10+10+12
0101 0101 00 0100 0111 11 0000 1110 0101
XXXX XXXX xx yyyy yyyy yy zzzz zzzz zzzz
虚拟地址的前10位在一级页表——页目录中找对应的二级页表;找到对应的二级页表后,中间10位在二级页表中找对应的page的起始地址(物理内存);找到对应的page的起始地址后,后12位作为偏移量在物理内存中的一个page(4KB)中找对应数据的地址,因为后12位有2^12=4096字节=4KB,正好物理内存管理单位是一个page,一个page是4KB,则后12位正好可以覆盖一个page的所有的地址。找到地址后CPU读取物理内存的数据
2.页表的好处
(1)进程虚拟地址管理和内存管理,通过页表+page进行解耦
(2)节省空间:分页机制+按需创建页表
页表也要占据内存,页表分离了,可以实现页表的按需创建,比如页目录的第3个地址从来没使用过,就可以不创建对应的二级目录,需要时再创建。一个页表大小是2^32/2^12=2^20字节(页目录和二级页表)
虚拟地址到物理地址的转化——硬件MMU做的,软(页表)硬(MMu)件结合的方式
三.线程的接口
1.pthread_create 创建线程
创建一个新线程的接口,不是系统接口,是linux带的 原生线程库,所以他是第三方库函数,需要在makefile中链接此库,g++
-o $@ $^ -lpthread -std=c++11
int pthread_create(pthread_t *thread, const pthread_attr_t
*attr, void *(*start_routine) (void *), void *arg);
(pthread_t 就是unsigned long int)
thread:输出型参数,线程id。attr:线程属性,现在不考虑,设成nullptr。start_routine:线程执行时的回调函数(该线程要执行的函数方法)。arg:要创建的线程叫什么名称,会传给start_routine函数的参数
返回值:成功返回0;失败返回错误码errno
例如:int n = pthread_create(&tid, nullptr, startRoutine,
(void *)"thread1"); 新线程会从startRoutine函数进入执行,主线程会拿到n继续执行下面的代码
ps -aL (all light)查看所有的轻量级进程
LWP(类比PID)——light wait process:轻量级进程编号。LWP=PID的执行流是主线程,俗称进程
makefile:
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f mythread
mythread.cc
return 退出演示
#include<cstdio>
#include<unistd.h>
#include<pthread.h>
#include<iostream>
using namespace std;
void printTid(const char* name,const pthread_t
&tid)//引用
{
printf("%s 正在运行,thread id:0x%x\n",name,tid);
}
void* startRoutine(void* args)
{
const char* name=static_cast<const char*>(args);
int cnt=5;
while(true)
{
printTid(name,pthread_self());
sleep(1);
if(!(cnt--))
break;
}
cout<<"新线程退出…………"<<endl;
return nullptr;
}
int main()
{
pthread_t tid;
int n=pthread_create(&tid,nullptr,startRoutine,(void*)"thread
1");
sleep(10);
pthread_join(tid,nullptr);
while(true)
{
printTid("main thread",pthread_self());
sleep(1);
}
return 0;
} |
2.pthread_self
man 3 pthread_self
pthread_t pthread_self(void);
线程获取自己的线程id
3.thread_join
man 3 pthread_join
int pthread_join(pthread_t thread, void **retval);
thread:线程id。retval:输出型参数,线程退出的退出码。(join 不需要退出信号)
线程退出的时候,一般必须要进行join等待,如果不进行join,就会造成类似于进程那样的内存泄露问题。(即:作用是:释放线程资源——前提是线程退出了。并获取线程对应的退出码)
成功返回0;错误返回错误码
4.pthread_exit
终止线程。线程终止--只考虑正常终止
(1)pthread_exit 对比 exit
exit(1):代表退出进程,任何一个 主/新线程调用exit,都表示整个进程退出。pthread_exit()仅仅是代表退出线程。
(2)线程退出有3种:
1. 线程退出的方式,return —— return (void*)111;
2. 线程退出的方式,pthread_exit —— pthread_exit((void*)1111);
3. 线程退出的方式:线程取消请求,pthread_cancel ——
void pthread_exit(void *retval); retval:线程退出码
pthread_exit
退出演示
#include<cstdio>
#include<unistd.h>
#include<pthread.h>
#include<iostream>
using namespace std;
void printTid(const char* name,const pthread_t
&tid)//引用
{
printf("%s 正在运行,thread id:0x%x\n",name,tid);
}
void* startRoutine(void* args)
{
const char* name=static_cast<const char*>(args);
int cnt=5;
while(true)
{
printTid(name,pthread_self());
sleep(1);
if(!(cnt--))
break;
}
cout<<"新线程退出…………"<<endl;
//return nullptr;
pthread_exit((void*)1111);
}
int main()
{
pthread_t tid;
int n=pthread_create(&tid,nullptr,startRoutine,(void*)"thread
1");
sleep(10);
pthread_join(tid,nullptr);
while(true)
{
printTid("main thread",pthread_self());
sleep(1);
}
return 0;
} |
5.pthread_cancel
取消一个线程
int pthread_cancel(pthread_t thread); thread:线程id
线程退出的方式,给线程发送取消请求, 如果线程是被取消的,退出结果是:-1
pthread_ cancel(tid); 退出
#include<cstdio>
#include<unistd.h>
#include<pthread.h>
#include<iostream>
using namespace std;
void printTid(const char* name,const pthread_t
&tid)//引用
{
printf("%s 正在运行,thread id:0x%x\n",name,tid);
}
void* startRoutine(void* args)
{
const char* name=static_cast<const char*>(args);
int cnt=5;
while(true)
{
printTid(name,pthread_self());
sleep(1);
if(!(cnt--))
break;
}
cout<<"新线程退出…………"<<endl;
//return nullptr;
//pthread_exit((void*)1111);
}
int main()
{
pthread_t tid;
int n=pthread_create(&tid,nullptr,startRoutine,(void*)"thread
1");
sleep(10);
pthread_cancel(tid);
(void)n;
cout<<"new thread been canceled"<<endl;
void* ret=nullptr; //void* -> 64 -> 8byte
->空间
pthread_join(tid,&ret); //void **retval是
一个输出型参数
cout<<"main thread join success,*ret:"<<(long
long)ret<<endl;
sleep(3);
return 0;
} |
四.用户级线程概念
1.线程异常了怎么办?—线程健壮性问题
线程异常了——>整个进程整体异常退出。 线程异常==进程异常
线程会影响其他线程的运行一新线程会影响主线程main thread 一健壮性/鲁棒性 较低,
2.理解 pthread_ t
是一个地址
1.线程是一个独立的执行流
2.线程一定会在自己的运行过程中,产生临时数据(调用函数,定义局部变量等)在新线程中修改全局变量后,新线程和主线程都能看到被修改后的结果
3.线程一定需要有自己的独立的栈结构
3.线程栈
(1)代码区有三类代码
我们使用的线程库,用户级线程库,库的名字叫pthread
代码区有三类代码:
①你自己写的代码。
②库的接口代码。(例如动态库libpthread. so会写入内存,通过页表映射到进程的共享区,代码区的库接口代码通过跳转到共享区执行完库中的代码,然后再跳转回代码区继续执行)
③系统接口代码。(通过身份切换 用户—>内核 执行代码)
所有的代码执行,都是在进程的地址空间当中进行执行的
(2)解释 pthread_create 创建线程的返回值pthread_t
用户要用线程,但是OS没有线程的概念,libpthread. so线程库起承上启下的作用。
共享区内:
线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。库可以创建多个线程->库也要管理线程->管理:先描述,在组织
struct thread_ info
{
pthread_ t tidh .
void *stack; //私有栈
……
}
libpthread. so线程库映射进共享区中。创建线程时,线程库中也会创建一个 结构体struct
thread_ info叫做 线程控制块, 线程控制块内部是描述线程的信息,内部有一个指针指向mm_struct用户空间的一块空间——线程栈。创建线程成功后,返回一个pthread_t类型的地址,pthread_t类型的地址保存着我们共享区中对应的用户级线程的线程控制块的起始地址!
结论:主线程的独立栈结构,用的就是地址空间中的栈区;新线程用的栈结构,用的是库中提供的栈结构(这个线程栈是库维护的,空间还是用户提供的)
Linux中,线程库用户级线程库,和内核的LWP是1:1(LWP(类比PID)——light wait
process:轻量级进程编号。LWP=PID的执行流是主线程,俗称进程)
(3)线程局部存储
线程库中的结构体struct thread_ info,内部是描述线程的信息,struct thread_
info中还有一个叫做线程局部存储的区域。作用:可以把全局变量私有化
正常情况全局变量是多个线程可以同时修改的:
加上__thread,把全局变量拷贝给每个进程各一份,使全局变量私有化,各自修改自己的全局变量
五.分离线程
1.概念
①默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
线程分离:如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离 :
pthread_detach(pthread_self());
一个线程如果分离了就不能再join,joinable和分离是冲突的,一个线程不能既是 joinable又是分离的。
int pthread_detach(pthread_t thread);
2.示例
(1)pthread_ detach(pthread_ self()); 新线程自我分离。
如果没有sleep(1),就会出现新线程一直循环的情况。因为有可能主线程先执行 int n = pthread_
join(tid1, nullptr); 此时新线程还没有pthread_ detach分离,则主线程会一直阻塞在pthread_
join这里,不会返回。
当有sleep(1)时,新线程会先pthread_ detach自我分离。主线程后执行 int n
= pthread_ join(tid1, nullptr); 此时新线程已经pthread_ detach分离,则主线程会直接pthread_
join返回错误码,证明了一个线程如果分离了就不能再join
(2)pthread_ detach(tid1);主线程分离新线程
建议主线程分离新线程,因为这样主线程一定是先分离了新线程,再pthread_ join。
3.线程分离可以理解为线程退出的第四种方式
(1)线程分离分为立即分离,延后分离,要保证线程还活着。线程分离意味着,我们不在关心这个线程的死活。线程分离可以理解为线程退出的第四种方式——延后退出
(2)新线程分离,但是主线程先退出就是代表进程退出,所有线程都会退出—— 一般我们分离线程,对应的main
thread不要退出(常驻内存的进程)
六.线程互斥
1.三个概念
1.临界资源:多个执行流都能看到并能访问的资源,临界资源
2.临界区:多个执行流代码中有不同的代码,访问临界资源的代码,我们称之为临界区
3.互斥:当我们访问某种资源的时候,任何时刻。都只有一个执行流在进行访问,这个就叫做:互斥特性
2.互斥
没有互斥时,以抢票,票数减1: int tickets; tickets--;为例
(1)在执行语句的任何地方,线程可能被切换走
int tickets;
tickets--;tickets--是由3条语句完成的:
tickets--:有三步
① load tickets to reg
② reg-- ;
③ write reg to tickets
(2)CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文;线程被换回的时候,需要恢复上下文。
(3)抢票中的问题
线程A先抢到一张票时,寄存器中tickets 10000——>9999, 还未写回内存就被切走执行线程B了;线程B也抢票,直接抢了9950张,还剩50张,此时又切回线程A,又把9999写入内存,就错误了。
(4)解决方案
原子性:一件事要么不做,要么全做完
把tickets--这个临界区设为原子的,使不想被打扰,加锁
3.加锁
(1)加锁介绍
加锁范围:临界区,只要对临界区加锁,而且加锁的粒度约细越好
加锁本质:加锁的本质是让线程执行临界区代码串行化
加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加
锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!那这把锁,本身不就也是临界资源吗?锁的设计者早就想到了
pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!申请锁的过程不会中断,不会被打扰。
难度在加锁的临界区里面,就没有线程切换了吗????
(2)定义/释放 互斥锁
man pthread_mutex_init
① pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
定义全局/静态的互斥锁,可以用这个宏初始化
② int pthread_mutex_init(pthread_mutex_t *restrict
mutex, const pthread_mutexattr_t *restrict attr);
mutex:锁的地址。attr:锁的属性设为空
③ int pthread_mutex_destroy(pthread_mutex_t *mutex);
释放锁
(3)加锁、解锁
man pthread_mutex_lock
① int pthread_mutex_lock(pthread_mutex_t *mutex);
阻塞式申请的锁
线程1正在用这个阻塞锁,那线程2就要阻塞式等待线程1用完才能申请这个锁
② int pthread_mutex_trylock(pthread_mutex_t *mutex);
非阻塞式申请的锁
线程1正在用这个非阻塞锁,那线程2就直接返回,只有没有别的线程用这个锁,自己才能用
③ int pthread_mutex_unlock(pthread_mutex_t *mutex);
解锁
(4)使用锁代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h> // 仅仅是了解
// __thread int global_value = 100;
// void *startRoutine(void *args)
// {
// // pthread_detach(pthread_self());
// // cout << "线程分离....." <<
endl;
// while (true)
// {
// // 临界区,不是所有的线程代码都是临界区
// cout << "thread " <<
pthread_self() << " global_value:
"
// << global_value << " &global_value:
" << &global_value
// << " Inc: " << global_value++
<< " lwp: " << ::syscall(SYS_gettid)<<endl;
// sleep(1);
// break;
// }
// // 退出进程,任何一个线程调用exit,都表示整个进程退出
// //exit(1);
// // pthread_exit()
// }
using namespace std;
// int 票数计数器
// 临界资源
int tickets = 10000; // 临界资源,可能会因为共同访问,可能会造成数据不一致问题。
pthread_mutex_t mutex;
void *getTickets(void *args)
{
const char *name = static_cast<const char
*>(args);
while (true)
{
// 临界区,只要对临界区加锁,而且加锁的粒度约细越好
// 加锁的本质是让线程执行临界区代码串行化
// 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加
// 锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
// 这把锁,本身不就也是临界资源吗?锁的设计者早就想到了
// pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!
// 难度在加锁的临界区里面,就没有线程切换了吗????
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(1000);
cout << name << " 抢到了票, 票的编号:
" << tickets << endl;
tickets--;
pthread_mutex_unlock(&mutex);
//other code
usleep(123); //模拟其他业务逻辑的执行
}
else
{
// 票抢到几张,就算没有了呢?0
cout << name << "] 已经放弃抢票了,因为没有了..."
<< endl;
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
// 如何理解exit?
int main()
{
pthread_mutex_init(&mutex, nullptr);
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1, nullptr, getTickets,
(void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets,
(void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets,
(void *)"thread 3");
pthread_create(&tid4, nullptr, getTickets,
(void *)"thread 4");
// sleep(1);
// 倾向于:让主线程,分离其他线程
// pthread_detach(tid1);
// pthread_detach(tid2);
// pthread_detach(tid3);
// 1. 立即分离,延后分离 -- 线程活着 -- 意味着,我们不在关心这个线程的死活。4.
线程退出的第四种方式,延后退出
// 2. 新线程分离,但是主线程先退出(进程退出) --- 一般我们分离线程,对应的main
thread一般不要退出(常驻内存的进程)
// sleep(1);
int n = pthread_join(tid1, nullptr);
cout << n << ":" <<
strerror(n) << endl;
n = pthread_join(tid2, nullptr);
cout << n << ":" <<
strerror(n) << endl;
n = pthread_join(tid3, nullptr);
cout << n << ":" <<
strerror(n) << endl;
n = pthread_join(tid4, nullptr);
cout << n << ":" <<
strerror(n) << endl;
pthread_mutex_destroy(&mutex);
return 0;
} |
|