前言
进一步理解地址空间和页表
地址空间是进程能看到的资源窗口
页表决定进程真正能拥有的资源
合理地对地址空间和页表进行资源划分,就可以对一个进程的所有资源分类
页表的结构:
页表中的每一行都是一个结构体,保存着相应的属性;再通过某种数据结构连接在一起
虚拟地址空间的地址有2^32个,页表如果也是
2^32个,就需要相当大的空间;因此,页表的结构并非如此
虚拟地址以10,10,12个比特位分为三份;第一份作为页目录,第二份作为页表,第三分作为偏移量
物理空间按照4KB的大小进行划分,通过结构体struct Page进行描述,再通过某种数据结构进行组织,每一份物理空间称作页框;在之前的学习中磁盘中的程序每次同样是以4KB的大小加载到物理空间的
读取文件信息时,进程先通过虚拟地址的前10位找到对应的页目录,再通过后10位找到对应的页表,通过页表中的地址找到物理空间中页框的起始地址,加上12位的偏移量,读取对应的数据
线程
概念
线程是进程内的一个执行流
我们知道在创建子进程时,只会创建子进程的进程控制块,父进程将自己资源的一部分直接给子进程,比如虚拟地址空间中的代码段和页表;现在有一个想法,就是类似创建子进程一样,给某一个进程创建多个进程控制块,同时指向同一个虚拟地址空间,共用一个页表
图解:
将进程的代码区分为多份,同时创建多个进程控制块指向同一虚拟地址;这些进程控制块分配着不同的系统资源,承担着不同的任务;通过虚拟地址空间+页表的方式对进程进行资源划分,使得单个进程的执行力度一定比之前的进程更细
这里多创建的进程其实就是线程,在Linux中由于线程和进程有许多重叠处,所以直接复用进程控制块来表示线程;线程是CPU调度的基本单位
有了线程之后,再一次解释什么是进程:承担分配系统资源的基本实体;之前的进程也是承担系统资源的基本实体,只不过内部只有一个执行流,这里的进程内部可以有多个执行流
线程在进程内部运行,线程在进程的虚拟地址空间内运行,拥有该进程的一部分资源
Linux内核中没有真正意义上的线程,是使用进程控制块进行模拟的
在CPU角度,每个进程控制块都可以称之为轻量级进程
Linux线程是CPU调度的基本单位;进程是承担分配系统资源的基本单位
进程用来申请资源,线程向进程索要资源
线程的好处:简单,维护成本降低
为了理解线程和进程的关系,举个栗子
在家庭中每个成员就是一个线程,整个大家庭便是一个进程;每个成员所承担的责任都不同,但都有一个目的:朝着大家庭和睦的方向发展
代码实现来证明线程
先介绍创建线程函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
- pthread_t *thread:输出型参数,线程id
- pthread_attr_t *attr:线程属性,默认为空
- void *(*start_routine) (void *):函数指针;当线程创建完毕之后,让其去执行这个函数
- void *arg:填入函数指针中的参数
mysignal:mysignal.cpp g++ -o $@ $^ -lpthread -std=c++11 .PHONY:clean clean: rm -f mysignal
由于操作系统只认识线程,用户也只认识线程;Linux没有提供创建线程的系统调用接口,只提供了创建轻量级进程的接口;在任何Linux操作系统中都有用户级线程库,在两者之间,线程库向上提供创建线程的接口,向下把对库进行的操作转化成对轻量级进程的操作;所以在创建线程时,需要链接上线程库
#include<iostream> #include<unistd.h> #include<pthread.h> #include<cassert> using namespace std; void *thread_routine(void *args) { const char*name=(const char*)args; while(true) { cout<<"我是新线程,我正在运行!name:"<<name<<endl; sleep(1); } } int main() { pthread_t tid; int n=pthread_create(&tid,nullptr,thread_routine,(void*)"thread one"); assert(n==0); while(true) { char tidbuffer[64]; snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",tid); cout<<"我是主线程,我正在运行!我创建出的线程的tid:"<<tidbuffer<<endl; sleep(1); } return 0; }
运行结果也证实了,存在两个执行流;线程id的结果貌似是个地址,这里先保留个疑问后面会进行揭晓
查看两执行流的相关信息
两线程拥有同一个进程id;LWP代表light weight process轻量级进程,两线程的LWP不同,所以CPU调度时,是以LWP为标识符表示特定的执行流
优点
创建一个新线程比创建一个进程代价要小的多
线程间切换需要操作系统做的工作少很多:切换PCB和上下文切换;线程切换高速缓存cache不用全部更新,进程切换高速缓存cache全部更新
线程占用的资源比进程少很多
线程一旦被创建,所有资源都被线程共享
缺点
健壮性降低
#include<iostream> #include<unistd.h> #include<pthread.h> #include<cassert> using namespace std; void *thread_routine(void *args) { const char*name=(const char*)args; while(true) { sleep(1); cout<<"我是新线程,我正在运行!name:"<<name<<endl; int*p=nullptr; *p=0; } } int main() { pthread_t tid; int n=pthread_create(&tid,nullptr,thread_routine,(void*)"thread one"); assert(n==0); while(true) { sleep(1); char tidbuffer[64]; snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",tid); cout<<"我是主线程,我正在运行!我创建出的线程的tid:"<<tidbuffer<<endl; } return 0; }
当进程中某一个线程出现异常,进程收到系统的信号进行资源回收时,所有的线程都会退出;进程是承担系统资源的实体,资源都被回收,进程中全部的线程也就会退出
异常
线程是进程的执行分支,线程出现异常,会触发信号机制,终止进程,该进程中的所有线程也随之退出
进程VS线程
线程创建
上面已经展示过如何创建一个线程,这里尝试创建一批线程
void *start_routine(void *args) { string name=(const char*)args; while(true) { sleep(1); cout<<"我是新线程,我正在运行!name:"<<name<<endl; } } int main() { #define NUM 10 for(int i=0;i<NUM;i++) { pthread_t tid; char namebuffer[64]; snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i); pthread_create(&tid,nullptr,start_routine,namebuffer); } while(true) { sleep(1); cout<<"我是主线程,我正在运行!"<<endl; } return 0; }
与进程一样,创建线程时,谁先运行是不确定的;而且创建线程时,将缓冲区 namebuffer的起始地址传给函数 start_routine,每次创建线程,都会刷新缓冲区,结果就导致,线程的编号全都是一样的
进行改进,使得每个线程都拥有自己的缓冲区和编号
class ThreadData { public: int number; pthread_t tid; char namebuffer[64]; }; void *start_routine(void *args) { ThreadData*td=(ThreadData*)args; while(true) { sleep(1); cout<<"我是新线程,name:"<<td->namebuffer<<endl; } } int main() { #define NUM 10 for(int i=0;i<NUM;i++) { ThreadData*td=new ThreadData(); td->number=i+1; char namebuffer[64]; snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",td->number); pthread_create(&td->tid,nullptr,start_routine,td); } while(true) { sleep(1); cout<<"我是主线程,我正在运行!"<<endl; } return 0; }
运行结果也证实了,线程运行的先后是不确定的
当创建10个线程之后,此时的函数start_routine是被10个线程都是执行的,也就是说此时的函数是可重入状态;如果再向函数中添加一个变量,打印其地址结果是否是不一样的呢???
结果与预期是一致的,在函数内部定义的变量具有临时性,也就是证实了每一个线程都有自己独立的栈结构
线程终止
进程退出有两种方式:return nullptr;void pthread_exit(void *retval);参数默认是空
void *start_routine(void *args) { ThreadData*td=(ThreadData*)args; int cnt=10; while(cnt) { sleep(1); cnt--; cout<<"我是新线程,name:"<<td->namebuffer<<endl; } pthread_exit(nullptr); }
线程退出时,是将所有新建线程退出,只剩余主线程;其实线程和进程一样,退出之后也是要进行等待资源回收的
int pthread_join(pthread_t thread, void **retval);
回收线程资源时,需要将线程的id传入第一个参数中,后一个参数先设置为空
int main() { vector<ThreadData*> threads; #define NUM 10 for(int i=0;i<NUM;i++) { ThreadData*td=new ThreadData(); td->number=i+1; char namebuffer[64]; snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",td->number); pthread_create(&td->tid,nullptr,start_routine,td); threads.push_back(td); } for(auto& e:threads) { int n=pthread_join(e->tid,nullptr); assert(n==0); cout<<"join "<<e->namebuffer<<endl; } cout<<"main thread quit"<<endl; return 0; }
线程退出时返回的参数是 void*,退出函数pthread_exit(nullptr)的参数和线程等待的函数pthread_join( ,nullptr)的第二个参数也相同,并且前一个参数的类型是一级指针,后者的参数类型是二级指针,这其中是不是有什么关联呢???
退出函数将退出信息保存至void* retval进行返回,等待函数对退出信息进行取址void** retval,获取退出信息并进行打印;在主线程中可以设置一个void*变量将退出信息写到该变量中
for(auto& e:threads) { void*ret=nullptr; int n=pthread_join(e->tid,&ret); assert(n==0); cout<<"join "<<e->namebuffer<<" "<<(long long)ret<<endl; }
0代表线程正常退出;线程退出时没有对应的信号,pthread_join默认会调用成功,如果失败,整个进程直接退出
线程取消
线程取消也是线程终止的一种方法;线程要被取消,前提是该线程已经运行起来,通过线程取消函数,将正在运行的线程取消,观察其返回结果
int pthread_cancel(pthread_t thread);
for(int i=0;i<threads.size()/2;i++) { pthread_cancel(threads[i]->tid); cout<<"pthread cancel :"<<threads[i]->namebuffer<<"success"<<endl; }
线程如果被取消,线程退出的结果是-1
线程分离
默认情况下,新创建的线程是可等待的,线程退出后,需要对其进行等待操作,否则无法释放资源,从而造成系统泄漏;如果不关心线程的返回值,等待就变成了一种负担,所以可以在线程退出时,告知系统自动释放线程资源,也就是线程分离;线程分离之后,不能进行等待,否则会报错
线程分离函数:
int pthread_detach(pthread_t thread);
获取待分离线程自己的线程id
pthread_t pthread_self(void);
代码展示
由于新线程创建之后,主线程和新线程谁先执行是不确定的,所以最好在新线程执行之后再进行线程分离
string changeid(const pthread_t& thread_id) { char tid[64]; snprintf(tid,sizeof(tid),"0x%x",thread_id); return tid; } void* start_routine(void* args) { string threadname=(const char*)args; int cnt=3; while(cnt--) { cout<<threadname<<"running ..."<<changeid(pthread_self())<<endl; sleep(2); } return nullptr; } int main() { pthread_t tid; pthread_create(&tid,nullptr,start_routine,(void*)"thread one"); string main_id =changeid(pthread_self()); pthread_detach(tid); cout<<"mian thread running ... new thread id "<<changeid(tid)<<"main thread id "<<main_id<<endl; int n=pthread_join(tid,nullptr); cout<<"result "<<n<<strerror(n)<<endl; return 0; }
由于线程已经进行了分离,再次进行等待编译器进行报错