重新理解进程
页表有许多条目。32位系统下,物理内存是4G即2^32字节,即有2^32个地址。其中物理内存中被划分为许多页框(或者叫块),页框大小4KB。相应的磁盘也被划分为许多页帧,页帧大小也是4KB,这样OS将数据从磁盘加载到内存或内存保存到磁盘上就是以4KB为单位。回到内存,内存有2^32个地址,那么就有2^32个地址需要被映射。页表就需要建立2^32个逻辑地址与物理地址的映射。
在页表中,每个条目不仅仅要有2^32位数字作为映射,还要有各种权限标志位,比如U/K标志位(User or Kernel)用来判断进程是处于用户态还是内核态。
页表中存储物理地址和逻辑地址就需要8个字节(232个bit)另外加各种标志位一起算10个字节。那么一个页表就需要2^32 10字节即40G内存大小,很显然在32位系统下内存大小远远不够。所以就需要用到多级页表来进行映射。
一级页表被称为页目录,页目录的每个条目映射二级页表,二级页表可以称为页表,页表的每个条目映射物理的地址。
页目录拿着虚拟地址的高10位来映射页表,可以映射2^10个页表。页表内有页号标识,页目录能通过虚拟地址的高十位找到相应的页表。页表拿着虚拟地址的中10位来映射物理地址的起始位置,找到物理地址的起始位置后,再拿着虚拟地址的低12位作为物理地址的偏移量。前面提到物理内存被划分为一个个4KB大小的页框,2^12bit刚好为4KB,也就是刚好在页框内通过偏移量找到数据。
每个页表的条目还是10字节,那么在二级页表的情况下,页表大小为12^10 10byte=1M,即一个页表有1个页目录,2^10个二级页表,每个二级页表10字节大小。实际上Linux下的页表也是这样映射的。
注意:对于32位的机器,采用二级页表是合适的;但对于64位的机器,采用二级页表是不合适的,因此必须采用多级页表。
另外多级页表还解决程序和数据无需连续存储空间的问题。若是一个页表内包含全部内容,那么在加载页表的时候除了所占空间巨大外,还需要开辟一段连续空间给页表,显然消耗是巨大的。而多级页表就能让页表分散化,无需占用大块的连续空间。
上面所说的所有映射过程,都是由MMU(MemoryManagementUnit)这个硬件完成的,该硬件是集成在CPU内的。页表是一种软件映射,MMU是一种硬件映射,所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式。
在页表视角看待段错误
当我们需要修改字符串常量时,通过页表找到相应字符串存在的页框,然而该进程对于该字符串的权限是只读,进程对该字符串做修改时违背了权限,OS识别到该进程发生了错误,立刻发送信号进行终止。
无需建立线程有关的数据结构,杜绝了在同一个进程中造成线程与进程地址空间,页表,物理空间之间的映射和进程与进程地址空间,页表,物理空间之间的映射,避免了这两套映射之间带来的强耦合性。因此OS也无法提供给我们线程创建的相关接口,但能提供给我们轻量级线程创建的接口。
函数原型如下
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
需要注意的是,pthread并非是Linux系统的默认库,需要手动连接线程库 -lpthread。而且线程库在根目录底下的lib64路径底下。实际上这个线程库是系统安装自带的,被称为原生线程库
makefile文件
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread
PHONY:clean
clean:
rm -rf mythread
mythread.cc
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;
void* start_rontine(void* args)
{
string str=static_cast<const char*>(args);//static_cast<const char*>的作用是参数args进行从void*转变为const char*的类型转化时进行检查,一般用于相近类型的安全转化
while(true)
{
cout<<"我是一个新线程,我的参数是:"<<str<<endl;
sleep(1);
}
}
int main()
{
pthread_t id;//创建一个新的线程id
int n=pthread_create(&id,nullptr,start_rontine,(void*)"new_pthread");
assert(n==0);
while(true)
{
cout<<"我是主线程"<<endl;
sleep(1);
}
return 0;
}
可以看到,本来是只有main函数一个执行流即主线程,然后创建了一个新线程
实际上pthread_create底层调用的是系统调用clone
clone创建子进程,函数原型:
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg);
需要注意的是,该子进程是指轻量级进程。另外函数fork创建的子进程并不和父进程共享进程地址空间,函数vfork创建的子进程与父进程共享进程地址空间
前面提到线程能够访问进程内的资源,线程能够共享进程的资源有代码段、数据段、文件描述符表、信号的处理方式、当前工作目录、用户id和组id等
这里我设置了一个全局变量g_val和一个fun函数,可以看到两个线程都能访问g_val和fun函数
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;
int g_val=0;
string fun()
{
return "我是一个fun函数";
}
void* start_rontine(void* args)
{
string str=static_cast<const char*>(args);
while(true)
{
cout<<"我是一个新线程,我的参数是:"<<str<<" g_val:"<<g_val<<" "<<fun()<<endl;
sleep(1);
}
}
int main()
{
pthread_t id;//创建一个新的线程id
int n=pthread_create(&id,nullptr,start_rontine,(void*)"new_pthread");
assert(n==0);
while(true)
{
cout<<"我是主线程"<<" g_val:"<<g_val++<<" "<<fun()<<endl;
sleep(1);
}
return 0;
}
需要注意的是,线程共享进程数据,但也有私有部分:
线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
using namespace std;
class threadData//建立结构体
{
public:
int _num;
char _buffer[64];
};
void* start_rontine(void* args)
{
threadData* td=static_cast<threadData*>(args);
int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
while(cnt--)
{
cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<endl;
sleep(1);
}
}
#define NUM 3
int main()
{
for(int i=0;i<NUM;i++)
{
pthread_t id;//创建一个新的线程id
threadData* ta=new threadData();
ta->_num=i;
snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
int n=pthread_create(&id,nullptr,start_rontine,(void*)ta);
assert(n==0);
}
while(true)
{
cout<<"我是主线程,"<<endl;
sleep(1);
}
return 0;
}
一是在切换进程时,需要切换进程的task_struct,页表,并且在CPU中切换进程的上下文。而切换线程时,只需要切换线程的task_struct,在CPU中切换线程上下文
二是在CPU中有一个区域叫做cache,即硬件级缓存,该区域的加载速度比CPU慢,但比内存快。当进程或线程需要被CPU调度时,OS会将热点数据(可能多次被调度处理的数据)加载到cache中。CPU调度数据,会先去cache中寻找,指定数据存在,即直接调度。若不存在,OS就到内存中将指定数据加载到cache中再调度。若当前是线程切换,除了切换线程上下文外,cache中的热点数据不会失效,继续供新切换进来的线程调度;若当前是进程切换,那么cache中热点数据会失效,除了切换进程上下文外,还需要切换cache中的热点数据。这个是线程切换比进程切换消耗更少的主要原因。
计算型密集型应用:主要是表现为调度CPU,如文件的加密解密,算法等
I/O密集型应用:主要表现为访问外设资源,如访问磁盘,显示器,网络等
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;
void* start_rontine(void* args)
{
string str=static_cast<const char*>(args);
while(true)
{
cout<<"我是一个新线程,"<<endl;
int* ptr=NULL;
*ptr=0;//空指针解引用--报错
sleep(1);
}
}
int main()
{
pthread_t id;//创建一个新的线程id
int n=pthread_create(&id,nullptr,start_rontine,(void*)"new_pthread");
assert(n==0);
while(true)
{
cout<<"我是主线程,"<<endl;
sleep(1);
}
return 0;
}
我是主线程, 我是一个新线程, Segmentation fault
这里在新线程发生了空指针解引用。空指针一般指向进程的最小的地址,通常这个值为0,当进程试图通过空指针对该数据进行访问,将会发生空指针解引用段错误。值得注意,新线程引发段错误,OS向新线程所在的进程发送信号来终止,那么新线程和主线程赖以利用的资源将会被进程回收,以至于线程都被终止了。
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编写与调试一个多线程程序比单线程程序困难得多
函数原型:
#include <pthread.h>
void pthread_exit(void *retval);
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
using namespace std;
class threadData//建立结构体
{
public:
int _num;
char _buffer[64];
};
void* start_rontine(void* args)
{
threadData* td=static_cast<threadData*>(args);
int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
while(cnt--)
{
cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<endl;
sleep(1);
delete td;//释放指向的结构体资源
pthread_exit(nullptr);//线程终止
}
}
#define NUM 3
int main()
{
for(int i=0;i<NUM;i++)
{
pthread_t id;//创建一个新的线程id
threadData* ta=new threadData();
ta->_num=i;
snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
int n=pthread_create(&id,nullptr,start_rontine,(void*)ta);
assert(n==0);
}
while(true)
{
cout<<"我是主线程,"<<endl;
sleep(1);
}
return 0;
}
另外还有两种方法可以只终止某个线程而不终止整个进程
实际上线程也是需要被等待回收的,否则会造成类似僵尸进程问题,引发内存泄漏
函数原型
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
//#include"mythread.hpp"
using namespace std;
class threadData//建立结构体
{
public:
pthread_t _pid;//线程id
int _num;
char _buffer[64];
};
void* start_rontine(void* args)
{
// string str=static_cast<const char*>(args);
threadData* td=static_cast<threadData*>(args);
int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
while(cnt--)
{
cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<"&cnt:"<<&cnt<<endl;
sleep(1);
// pthread_exit(nullptr);
}
}
#define NUM 3
int main()
{
vector<threadData*> threads;
for(int i=0;i<NUM;i++)
{
pthread_t id;//创建一个新的线程id
threadData* ta=new threadData();
ta->_num=i;
snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
threads.push_back(ta);
int n=pthread_create(&ta->_pid,nullptr,start_rontine,(void*)ta);
assert(n==0);
}
for(const auto &it:threads)
{
int n= pthread_join(it->_pid,nullptr);//等待回收线程
assert(n==0);
cout<<"newpthread "<<it->_num<<" join success"<<endl;
delete it;
}
while(true)
{
cout<<"我是主线程,"<<endl;
sleep(1);
}
return 0;
}
线程回收的作用:一是获取新线程的退出信息,二是回收新线程对应PCB等内核资源,防止内存泄漏
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
using namespace std;
class threadData//建立结构体
{
public:
pthread_t _pid;//线程id
int _num;
char _buffer[64];
};
void* start_rontine(void* args)
{
threadData* td=static_cast<threadData*>(args);
int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
while(cnt--)
{
cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<"&cnt:"<<&cnt<<endl;
sleep(1);
}
return (void*)520;
}
#define NUM 3
int main()
{
vector<threadData*> threads;
for(int i=0;i<NUM;i++)
{
pthread_t id;//创建一个新的线程id
threadData* ta=new threadData();
ta->_num=i;
snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
threads.push_back(ta);
int n=pthread_create(&ta->_pid,nullptr,start_rontine,(void*)ta);
assert(n==0);
}
void* ret;//初始化
for(const auto &it:threads)
{
int n= pthread_join(it->_pid,&ret);//等待回收线程
assert(n==0);
cout<<"newpthread "<<it->_num<<" join success"<<"return val:"<<(long long)ret<<endl;//Linux下void*大小为8个字节,所以强转为整形要用到long long
delete it;
}
while(true)
{
cout<<"我是主线程,"<<endl;
sleep(1);
}
return 0;
}
在前面的pthread_join函数都提到了线程退出的返回值。可以看到返回值类型是void*。实际上线程调用的函数返回值是void ,线程库中有一个专门接收该返回值的变量ret类型也是void ,当线程退出时,OS会将线程退出的返回值拷贝给线程库中的ret变量;但由于我们无法直接获取存在于线程库的变量ret,我们需要对ret取地址,然后解引用才能拿到该数据,对void 取地址的类型为void * *。
可以看到新线程的返回值给pthread_join函数接收并打印出来了。实际上返回值是对象也能接收,只需要在退出的线程处先强转成void*类型,然后在外部接收的函数处做相应的强转即可
函数原型
#include <pthread.h>
int pthread_cancel(pthread_t thread);
线程要取消,前提是线程已经开始执行了,若线程被取消了,则退出码为-1,那么在回收线程时接收的返回值就为-1
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
//#include"mythread.hpp"
using namespace std;
class threadData//建立结构体
{
public:
pthread_t _pid;//线程id
int _num;
char _buffer[64];
};
void* start_rontine(void* args)
{
threadData* td=static_cast<threadData*>(args);
int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
while(cnt--)
{
cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<"&cnt:"<<&cnt<<endl;
sleep(1);
}
return (void*)520;
}
#define NUM 3
int main()
{
vector<threadData*> threads;
for(int i=0;i<NUM;i++)
{
pthread_t id;//创建一个新的线程id
threadData* ta=new threadData();
ta->_num=i;
snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
threads.push_back(ta);
int n=pthread_create(&ta->_pid,nullptr,start_rontine,(void*)ta);
assert(n==0);
}
sleep(2);//让新线程执行,两秒后取消新线程
for(const auto& it:threads)
{
int n=pthread_cancel(it->_pid);
assert(n==0);
cout<<"new thread "<<it->_num<<"cancel success"<<endl;
}
void* ret;//初始化
for(const auto &it:threads)
{
int n= pthread_join(it->_pid,&ret);//等待回收线程
assert(n==0);
cout<<"newpthread "<<it->_num<<" join success"<<"return val:"<<(long long)ret<<endl;//Linux下void*大小为8个字节,所以强转为整形要用到long long
delete it;
}
while(true)
{
cout<<"我是主线程,"<<endl;
sleep(1);
}
return 0;
}
一般线程取消是给主线程来控制新线程的,而取消新线程是不会影响主线程的
函数原型
#include <pthread.h>
int pthread_detach(pthread_t thread);
void* start_rontine(void* arg)
{
string threadname=static_cast<const char*> (arg);
cout<<"new thread name:"<<threadname<<endl;
pthread_detach(pthread_self());//新线程自己让自己分离
int cnt=3;
while(cnt--)
{
cout<<"new thread run..."<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t id;
int n=pthread_create(&id,nullptr,start_rontine,(void*)"thread1");//创建新线程
assert(n==0);
int m=pthread_join(id,nullptr);//阻塞等待回收线程
assert(m==0);
cout<<"new thread ret:"<<n<<":"<<strerror(n)<<endl;//若新线程分离成功则主线程回收新线程失败
while(true)
{
cout<<"main thread"<<endl;
sleep(1);
}
return 0;
}
因此在回收新线程之前需要一定的时间,让新线程先执行到pthread_detach函数,使得新线程分离成功
正确写法
void* start_rontine(void* arg)
{
string threadname=static_cast<const char*> (arg);
int cnt=3;
while(cnt--)
{
cout<<"new thread name:"<<threadname<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t id;
int n=pthread_create(&id,nullptr,start_rontine,(void*)"thread1");//创建新线程
assert(n==0);
pthread_detach(id);//线程分离--在主线程对新线程进行分离
int m=pthread_join(id,nullptr);//阻塞等待回收线程
cout<<"new thread ret:"<<m<<":"<<strerror(m)<<endl;//若新线程分离成功则主线程回收新线程失败
while(true)
{
cout<<"main thread"<<endl;
sleep(1);
}
return 0;
}
函数原型
#include <pthread.h>
pthread_t pthread_self(void);
Linux不提供线程的内核数据结构,只提供LWP,意味着Linux只需要对LWP对应的执行流进行调度或管理,而提供给用户使用的用户级数据由线程库提供,即由线程库来管理。
我们也可以通过地址的方式将线程id进行打印:
string changeID(const pthread_t&thread_id)
{
char id[128];
snprintf(id,sizeof id,"0x%x",thread_id);//以十六进制的方式将参数打印进id缓冲区并返回
return id;
}
void* start_rontine(void* arg)
{
string threadname=static_cast<const char*> (arg);
int cnt=3;
while(cnt--)
{
cout<<"pthread_self get newthreadid: "<<threadname<<changeID(pthread_self())<<endl;//pthread_self获取的线程id
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t id;
int n=pthread_create(&id,nullptr,start_rontine,(void*)"thread1");//创建新线程
assert(n==0);
char newthreadid[128];
snprintf(newthreadid,sizeof newthreadid,"0x%x",id);
cout<<"new threadid: "<<newthreadid<<endl;//pthread_create获取的线程id
pthread_detach(id);//线程分离
int m=pthread_join(id,nullptr);//阻塞等待回收线程
cout<<"new thread ret:"<<m<<":"<<strerror(m)<<endl;//若新线程分离成功则主线程回收新线程失败
while(true)
{
cout<<"main thread"<<endl;
sleep(1);
}
return 0;
}
实际上任何语音都能在Linux下实现多线程,前提是要使用线程原生库pthread。C++的多线程,本质是对pthread线程库的封装。
在Linux下实现简单的C++多线程
makefile
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread
PHONY:clean
clean:
rm -rf mythread
thread.cc
#include<iostream>
#include<unistd.h>
#include<thread>
using namespace std;
void thread_tun()
{
while(true)
{
cout<<"我是新线程"<<endl;
sleep(1);
}
}
int main()
{
thread t1(thread_tun);
while(true)
{
cout<<"我是主线程"<<endl;
sleep(1);
}
t1.join();
return 0;
}
mythread.hpp
#include<iostream>
#include<pthread.h>
#include<string.h>
#include<functional>
using namespace std;
class thread;//声明
class Context
{
public:
thread* _this;//this指针
void* _args;//函数参数
public:
Context()
:_this(nullptr)
,_args(nullptr)
{}
~Context()
{}
};
class thread
{
public:
typedef function<void* (void*)> func_t;//包装器构建返回值类型为void* 参数类型为void* 的函数类型
const int num=1024;
thread(func_t func,void* args,int number=0)//构造函数
: fun_(func)
,args_(args)
{
char namebuffer[num];
snprintf(namebuffer,sizeof namebuffer,"threa--%d",number);//缓冲区内保存线程的名字即几号线程
Context* ctx=new Context();//
ctx->_this=this;
ctx->_args=args_;
int n=pthread_create(&pid_,nullptr,start_rontine,ctx);//因为调用函数start_rontine是类内函数,具有缺省参数this指针,在后续解包参数包会出问题,所以需要一个类来直接获取函数参数
assert(n==0);
(void)n;
}
static void* start_rontine(void* args)
{
Context* ctx=static_cast<Context*>(args);
void *ret= ctx->_this->run(ctx->_args);//调用外部函数
delete ctx;
return ret;
}
void* run(void* args)
{
return fun_(args);//调用外部函数
}
void join()
{
int n= pthread_join(pid_,nullptr);
assert(n==0);
(void)n;
}
~thread()
{
//
}
private:
string name_;//线程的名字
pthread_t pid_;//线程id
func_t fun_;//线程调用的函数对象
void* args_;//线程调用的函数的参数
};
thread.cc
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
#include"mythread.hpp"
using namespace std;
void* getticket(void*args)
{
string username=static_cast<const char*> (args);
int cnt=3;
while(cnt--)
{
cout<<"User name:"<<username<<"get tickets ing..."<<endl;
sleep(1);
}
}
int main()
{
unique_ptr<thread> thread1(new thread(getticket,(void*)"user1",1));
unique_ptr<thread> thread2(new thread(getticket,(void*)"user2",2));
unique_ptr<thread> thread3(new thread(getticket,(void*)"user3",3));
thread1->join();
thread2->join();
thread3->join();
return 0;
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。