一、细粒度划分
1、堆区细粒度划分
在语言中,我们知道,用户自己申请的空间是存在于地址空间的堆区上的。可是,堆区是一整块空间,我们每次申请只是申请了其中的一小块,并且我们只是说明了申请空间的大小,拿到的是空间的起始地址。如果,我们多次申请了空间,那么我们怎么知道第一次申请的空间是从堆区哪里到哪里呢,第二次申请的空间是从堆区哪里到哪里呢?
于是os就必须对堆区进行更加精细的管理。在Linux下,每次从堆区申请一块空间,os就会创建结构体 struct vm_area_struct ,该结构体中的数据就是用户申请的每一块空间的相关属性,其中就有空间的起始位置和结束位置。os对于用户申请的空间的管理就成了对结构体链表的管理。os通过这样的精细划分,能够更好地管理用户申请的每一块空间。
当然,链表的整个范围也一定是在堆区的范围之间的。
上图就是os对堆区的细粒度划分的大概内容。
2、物理内存和可执行程序细粒度划分
根据下图,我们来说明。
首先,我们需要知道:1、a.exe这个可执行程序是一个文件。2、a.exe等可执行程序在磁盘上已经按地址空间的方式编译好了。3、并且a.exe等可执行程序已经被分成了若干个4KB大小的小块,我们称之为页帧。
当a.exe刚开始运行,一个进程刚开始运行的时候,os首先创建进程的PCB,地址空间,页表,但是还没有通过页表建立虚拟地址到物理地址的联系,而是通过某种硬件方式直接让地址空间找到磁盘上的可执行程序。
在页表中有一列的数据表示是否在内存中(下图,页表绿色的部分)。当执行具体的代码时,进程必定会通过页表访问物理内存。检测页表时,发现数据不在内存中,于是地址空间不再直接与磁盘上的可执行程序建立联系,os将a.exe加载到物理内存中,通过页表与地址空间建立联系。这叫做缺页中断。
其实,物理内存也被分成了若干个4KB大小的小空间,我们称之为页框。os将a.exe加载到物理内存中的具体过程就是通过IO,将4KB大小的页帧放进4KB大小的页框中。
当然,对于这些若干个页框,os也必须对其进行管理。每一个页框都有一个struct page结构体,里面都是页框的各种属性,该结构体中有一个成员flag,表示该页框是否被占用。然后将这些结构体存储到一个 struct page men[ ] 数组中,数组的下标就是该页框的编号。
3、虚拟地址到物理地址的转化
我们结合下图进行说明。
我们早就知道虚拟地址和物理地址之间的联系是通过页表建立的。但是,真正的转化是不止通过一个页表来实现的。
我们平时所说的地址就是虚拟地址,它一共有32个比特位,页表是一种key,value结构的数据结构。我们通过前10个比特位找到一级页表中的key,然后通过value值找到二级页表,然后通过虚拟地址的第二组10个比特位,找到二级页表的key值位置,接着根据value值找到物理内存中某个页的起始地址。最后,根据虚拟地址的最后12个比特位,进行数值计算,最后找到数据在该页的准确位置。
二、线程的概念
1、基本概念
1、在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的控制序列,一切进程至少都有一个执行线程。
2、线程在进程内部运行,本质是在进程地址空间内运行。
3、透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
每个进程都有自己独立的地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性那么如果我们在创建“进程”时,只创建PCB,并要求新创建出来的PCB与第一次创建的PCB共享地址空间,页表等资源,那么就是下图的情况:
每个线程都是当前进程里的一个执行流,线程在进程内部运行,准确来说线程在进程的地址空间内运行,拥有并使用该进程的一部分资源。
所以说,进程从内核的角度来说就是承担分配系统资源的基本实体,因为线程都是直接从进程处拿到各种资源的。当然,这与我们之前所理解的进程的概念并不矛盾,我们之前的进程都只有一个PCB,也就是该进程内部只有一个执行流,即单执行流进程。从今天开始,我们就会讲到一个进程有多个执行流,即多执行流进程。
从用户的角度来说,进程就是包括一个或多个PCB(执行流)、地址空间、页表等内核数据结构以及内存中的代码和数据。
线程是CPU调度的基本单位。
前面,我们说,CPU去处理一个进程的时候,最先拿到的是它的PCB,来决定调度谁。那么,现在我们知道了,准确来说,CPU拿到的是一个线程,因为CPU以task_struct为单位进行调度。
我们给CPU的task_struct是小于等于过去所说的task_strcut的,比之前的更轻量化了(Linux下的进程也叫做轻量级进程)。因为每一个task_struct只管理着一个进程的一部分资源。它是进程下的一个执行流。
Linux下的线程是用进程PCB模拟的。
在Linux下,设计者并没有单独为线程设计一个像进程的PCB那样的数据结构, 因为,线程所需要的属性和进程非常相似,所以我们直接复用PCB,用PCB来表示Linux内部的“线程”。但是,如果os真的要专门设计“线程”概念,os那就需要管理线程了:先描述在组织,这样就提高了os的维护成本。
Linux内核中有没有真正意义的线程,Linux是用进程PCB来模拟线程的,这种设计方法是Linux特有的。
2、线程的优点
1、创建一个新线程的代价要比创建一个新进程小得多。
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
3、线程占用的资源要比进程少很多。
4、能充分利用多处理器的可并行数量。
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
3、线程的缺点
1、性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2、健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
4、线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出 。
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
using namespace std;
void *thread_run(void *argc)
{
while(true)
{
sleep(1);
int a = 10;
a /= 0;
}
}
int main()
{
pthread_t tid;
pthread_create(tid, nullptr, thread_run, (void *)"thread1");
while (true)
{
cout << "main thread pid: " << getpid() << endl;
sleep(3);
}
return 0;
}
5、线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
三、Linux下的进程和线程
1、进程是资源分配的基本单位。
2、线程是调度的基本单位。
3、线程共享进程数据,但也拥有自己的一部分数据。
线程共享的进程资源:
1、文件描述符表
2、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
3、当前工作目录
4、用户id和组id
线程需要自己私有的资源:
1、线程ID
2、一组寄存器(线程必须要有自己的上下文)
3、栈(独立的栈结构,能够保存自己的临时变量)
4、errno
5、信号屏蔽字
6、调度优先级
版权归原作者 dbln 所有, 如有侵权,请联系我们删除。