OS对内存进行管理不是根据字节为单位,以字节为单位效率过低,是以内存块为单位的,一个内存块的大小一般为4kb,系统和磁盘IO的基本单位是4kb--8个扇区 文件在磁盘中存储的时候都是有自己的datablock的,每个datablock的大小都是4kb,所以内存管理时,加载就是把程序的数据块加载到指定的内存块中。
为了方便进行表述,4kb的空间+内容有一个名字,叫页框或页帖,在内核中有一个struct page来管理每一个页框,内存看成是数组,第一个page的起始地址就是数组下标,*4==0 ,第二个page的起始地址是数组下标*4==4。所以每个page都有了下标。
那我们之前所说的虚拟地址到底是如何转化成物理地址的呢?
虚拟地址的前十个比特位索引页目录,中间十个比特位索引页表,页表指向对应页框的起始地址,虚拟地址的低12位+页框的起始地址就能找到页框内的任意一个字节了。 这种页表也叫二级页表,用来搜索页框。 虚拟地址本质是一种资源。
线程:在进程内部运行,是CPU调度的基本单位 进程:承担分配系统资源的基本实体 我们以前讲的都是进程内部只有一个执行流的进程,Windows系统里有struct tcb结构体描述线程,Linux系统选择复用struct pcb结构体。所以Linux是用进程模拟的线程。 Linux中CPU不区分task_struct 是进程还是线程,都看做执行流。 CPU看到的执行流<=进程。 Linux中的执行流叫:轻量级进程。
这个函数的功能是创建一个新的线程: 参数
返回值:成功返回0 失败返回错误码
看一下我们的代码:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
void *routine(void *args)
{
while (true)
{
std::cout << "new thread runing...." << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void *)"pthread-1");
while (true)
{
sleep(1);
std::cout << "main thread runing..." << std::endl;
}
return 0;
}
这里我们记得不可以直接编译,直接编译会出现说直接创建线程是未定义的行为,所以我们要在Makefile中加上
才可以
这个操作的意思是在编译时引入pthread库
运行之后看到:
运行程序, 因为主次线程里都是死循环打印,结果主次线程都有打印,说明有多执行流,即线程创建成功了。 打印出他们的pid,可以看到主次线程的pid都是一样的,因为这两个线程他们都属于同一个进程内部,所以对应的进程pid是一样的。
如果想查看线程,可以通过指令 ps -aL 。他们的pid都是一样的。LWP就是Light Weight Process,即轻量级进程,就是线程的id。 我们把pid和lwp都相等的执行流叫主线程。
进程中的多个线程共享一个地址空间,除此之外,各线程还共享以下的进程资源和环境
进程和线程的关系图如下:
前面已经简单介绍了pthread_create的使用。在创建完成后,主线程会继续向下执行代码,新线程会去执行参数3所指向的函数。此时执行流就一分为二了。
功能:等待线程结束 参数: thread:线程ID retval:它指向一个指针,指向线程的返回值(输出型参数) 参数2的类型是void**,用来接收新线程函数的返回值,因为新线程函数的返回值类型是void*。未来要拿到新线程的返回值void*,放到void* retval中时,这里的参数就得传&retval。 返回值:成功返回0;失败返回错误码
这是线程等待的演示代码,下面是结果:
如上图,为pthread_create和pthread_join的简单使用。pthread_t类型由库提供。主线程和新线程谁先运行,这是不确定的。
我们把tid以字符串的形式打印出来,发现是一个虚拟地址,他跟线程id不一样。
线程函数传参,可以传任意类型,一定要记住还可以传类对象的地址。 有了这个,就意味着可以给线程传递多个参数,甚至方法了。 上面的td对象是在主线程的栈上的,新线程访问了主线程栈上的临时变量,我们不推荐这种做法。因为如果main函数有第二个对象,他们在读取时没有影响,但其中一个对象在修改时,另一个也会跟着修改。推荐做法如下图:
我们建议在堆上申请一段空间,未来需要第二个对象时,再重新new一个对象,这样多线程就不会互相干扰了。
完整代码:
const int num=10;
void* threadrun(void* args)
{
std::string name=static_cast<const char*>(args);
while(true)
{
std::cout<<name<<" is running "<<std::endl;
sleep(1);
break;
}
return args;
}
int main()
{
std::vector<pthread_t> tids;
for(int i=0;i<num;i++)
{
//1.线程的id
pthread_t tid;
//2.线程的名字
char* name=new char[128];
snprintf(name,128,"thread-%d",i+1);
pthread_create(&tid,nullptr,threadrun,name);
//3.保存所有线程的id信息
tids.push_back(tid);
//tids.emplace_back(tid);
}
for(auto tid:tids)
{
void* name=nullptr;
pthread_join(tid,&name);
// std::cout<<PrintToHex(tid)<<"quit ..."<<std::endl;
std::cout<<(const char* )name<<" quit..."<<std::endl;
delete (const char* )name;
}
return 0;
}
运行结果为什么线程名是乱的?因为即使我们的线程是按顺序创建的,但他们不是按顺序启动的。而且上面的name,属于main函数栈上的空间,即main函数栈空间上的公共区传给了每一个线程,所以线程名会被不断覆盖。所以上面这么写是有问题的,要在堆在开辟空间。
运行后,发现主线程没有打印quit语句。因为exit是专门用来终止进程的,不能用来终止线程。任意一个线程调用exit,都可能会导致进程退出。
除了用return结束线程,pthread_exit是专门用来终止一个线程的,使用如下图:
下面是终止线程的另一种方法:
主线程调用pthread_cancel取消新线程。取消一个线程的前提是线程得存在。
线程取消一个就join一个。由上图可知,线程被取消后,线程的退出结果是-1。 -1对应pthread库中的一个宏
作用:哪个线程调用该接口,就返回他自己的线程id。相当于以前的getpid。
void* threadrun(void* args)
{
pthread_detach(pthread_self());
std::string name=static_cast<const char*>(args);
while(true)
{
std::cout<<name<<" is running "<<std::endl;
sleep(3);
break;
}
pthread_exit(args);//专门终止一个线程
}
int main()
{
std::vector<pthread_t> tids;
for(int i=0;i<num;i++)
{
//1.线程的id
pthread_t tid;
//2.线程的名字
char* name=new char[128];
snprintf(name,128,"thread-%d",i+1);
pthread_create(&tid,nullptr,threadrun,name);
tids.emplace_back(tid);
}
for(auto tid:tids)
{
void* result=nullptr; //线程被取消,线程的退出结果是-1
int n=pthread_join(tid,&result);
std::cout<<(long long int)result<<" quit... ,n: "<<n<<std::endl;
}
return 0;
}
运行上面代码,程序直接挂掉了。因为新线程已经分离,主线程不会卡在join,而是会继续往后走,主线程结束了,整个进程就结束了,新线程可能还没起来就死亡了。 所以分离线程后,主线程就可以做自己的事了,不用管新线程。 即使新线程分离,只要分离的线程异常了,还是会影响整个进程。
除了可以让新线程自己分离,也可以由主线程进行分离。
C++11里使用多线程,创建时是支持可变参数的。大致用法跟前文讲的差不多。
我们把makefile文件里的 -lpthread 去掉然后编译。 编译后,报错了,链接时报错。所以C++语言在Linux中要编译支持多线程,也要加 -lpthread。 C++11的多线程本质:就是对原生线程库接口的封装。 Linux中,C++11要支持多线程,底层必须封装Linux环境的pthread库,编译的时候都得带。 在Windows下要编译多线程程序不用带-lpthread。