目录
页表详解
简单创建线程
线程可谓是大名鼎鼎了,它的重要性不亚于我们之前学的进程,之所以说进程重要,那是因为进程就代表着用户,用户所作的操作都是以进程为载体的,而今天要说的线程甚至比进程还要重要。那么什么是线程呢?
线程是进程内部的一个执行分支,线程是CPU调度的基本单位,当然这对于支持线程的操作系统来说是正确的,我们后面会说到,Linux操作系统并不直接支持线程,而是利用轻量级进程封装出的线程
加载到内存中的程序叫做进程,当然这个解释抽象不具体。所以我们一般说进程=内核数据结构+程序的代码和数据
我们之前创建一个进程,它的代码的执行顺序都是串行(xing)执行的,我们当然可以创建子进程让代码并行执行,但是进程的创建的时间和空间成本都太大了,我们应该如何减小创建的成本呢?
后来我们发现,其实我们想要的只是多执行流,而不是多进程,只要能实现多执行流就可以,所以,与其创建进程,不如将一个程序的代码分份,然后让不同的线程去执行不同的代码,这样我们创建的成本就变小了
那我们应该如何设计线程呢?是不是还要像进程一样创建线程单独的TCB(Thread Control Block)呢?后来Linux的设计者就认为不如直接让线程复用进程的代码,这样会使代码更加稳健,可维护性更强。所以我们的Linux就是使用的进程来模拟的线程,所以有关线程的信息也是存放在task_struct这个结构体中。
我们之前学的进程就是内部只有一个线程,而从今以后就是进程内部至少有一个线程。说到这里,那再聊一聊什么是进程,从资源的角度,进程是承担分配系统资源的基本实体,而线程仅仅是进程内部的一个执行分支。
从CPU的角度,CPU不需要关心task_struct是进程的还是线程的,它还是只负责使用进程调度的那一套算法去调度线程。
所以,执行流有时可能是一个进程,有时可能是一个线程,总体下来就是在它们之间,所以我们叫做轻量级进程,这也就解释了我上面说的Linux是用轻量级进程封装出的线程
有了上面的一些理解,我们就不得不说一个进程是如何进行代码划分成多个线程的呢?
其实在磁盘上的程序中的各行代码都是有地址的,不同的线程执行不同的函数,每个进程的虚拟地址经过页表的映射就拿到了只属于它自己的代码和数据,这样就划分好了,下面我们具体的来说明一下这个过程
首先OS肯定是要管理内存的,比如内存有4GB,OS会把内存分成4KB为一个基本单位(所以我们写时拷贝或者new,malloc都是以4KB为基本单位的),这样4GB可以分成1048576份,并且我们说过,磁盘中的文件管理也是以4KB为基本单位的,可执行程序只要写到磁盘中就是按4KB去分块的,我们就把4KB这一个基本单位叫做页框或者页帧。
我们要管理内存,就要对4KB进行描述和组织,我们可以创建struct page用来描述这4KB的空间,同时用一个数组来组织这1048576个4KB,比如叫struct page memory[1048576],并且这个数组也占不了多少空间,大概就几~十几MB,这样对于内存的管理就变成了对于数组的增删查改
页表详解
其次,我们之前对于页表的理解也是有误的,我们可以简单的算一下,进程地址空间有4GB的地址,就是32个比特位最多可以表示2^32-1个虚拟地址,比如一个地址占四个字节,页表中要存这么多的虚拟地址到物理地址的映射,这需要2^32*4*2=32GB这显然是不可能的。
所以真实的页表是这样的:
一个地址有32个比特位,分成三组,10,10和12
在查地址的时候,我们首先取前十位,查页目录,十位可以表示0-1023,一共1024,所以页目录中要存1024个页表的地址。所以最多有1024个页表
找到页表后我们再取中间的十位去页表中找地址,一个页表最多也是存1024个地址。找到了页表中的地址,我们就找到了物理内存中的页框,最后取后12位(就是4096==4KB)作为页框的偏移量就能定位到一个页框中的唯一地址。
用图来表示就是:
这样我们再算一算页目录和页表需要多少空间
1024*1025*4=4MB,并且一个程序很小,根本用不到1024张页表
所以给不同的线程分配不同的区域,本质就是让不同的线程看到全部页表的子集
简单创建线程
下面我们就来简单的写个线程创建的代码,我们要用到的接口是
man 3 pthread_create
第一个参数是一个输出型参数,就是告诉我们这个线程的对于用户而言的唯一标识符
第二个参数是线程的一些属性,我们给nullptr就行
第三个参数是让新线程执行的函数
第四个参数是给第三个参数传的参数
我们简单来用一下
//thread.cc #include<iostream> #include<pthread.h> #include<unistd.h> #include<sys/types.h> using namespace std; void* newthreadrun(void*args) {while(1){cout<<"I am new thread"<<" pid: "<<getpid()<<endl;sleep(1);} } int main() {pthread_t tid;pthread_create(&tid,nullptr,newthreadrun,nullptr);while(1){cout<<"I am main thread"<<" pid: "<<getpid()<<endl;sleep(1);}return 0; } //makefile thread:thread.ccg++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean:rm -f thread
我们可以看到结果中主线程和新线程的pid是一样的,证明它们确实是一个进程。那如何区分主和新线程呢?我们可以用ps -aL,这个命令就是查看轻量级进程的意思
我们可以看到同样的pid,LWP(light weight process轻量级进程)是不同的,并且主线程的pid和LWP是一样的
所以操作系统在线程调度时,拿的就是LWP
OS没有直接提供像getpid一样直接获取LWP的系统调用
并且我们可以看到在makefile中要加上-lpthread,意思就是要显示链接上Linux的原生线程库。