网做 网站有哪些,辽宁省工程招投标信息网,网站谁做的比较好看,wordpress 修改目录 1.背景知识
再谈地址空间#xff1a;
关于页表#xff08;32bit机器上#xff09; 2.线程的概念和Linux中线程的实现
概念部分#xff1a;
代码部分#xff1a;
问题#xff1a;
3.关于线程的有点与缺点
4.进程VS线程 1.背景知识 再谈地址空间#xff1a…目录 1.背景知识
再谈地址空间
关于页表32bit机器上 2.线程的概念和Linux中线程的实现
概念部分
代码部分
问题
3.关于线程的有点与缺点
4.进程VS线程 1.背景知识 再谈地址空间 我们都知道系统和磁盘文件进行IO的基本单位是内存块4KB--8个扇区。我们以4GB大小的物理内存为例物理内存被分为一个一个的页框一个页框的大小也就是4KB那么我们也就清楚了磁盘加载到物理内存操作系统会从磁盘中读取该页面并将其加载到物理内存中的一个页框/页帧中。 当我们谈及操作系统对内存的管理工作基本单位也是4KB 现在有一个问题在父子进程进行共享内存的全局变量int只占四个字节我对他写入时要发生写时拷贝写时拷贝的本质就让操作系统重新申请内存那么拷贝的时候是拷贝四个字节还是4kb呢 对于全局变量int的写入操作通常不会触发写时拷贝。全局变量是在进程的地址空间中分配的每个进程都有自己的全局变量副本除非它们通过某种形式的共享内存机制显式地共享。当你修改一个全局int变量时你只是在当前进程的地址空间中修改了该变量的4个字节。 如果全局变量是通过某种形式的共享内存在不同的进程之间共享的并且你在这些进程之一中修改了该变量这时一般会触发写时拷贝写时拷贝也不会仅仅拷贝4个字节相反它会拷贝包含该变量的整个页框即4KB。如果操作系统在每次修改共享内存中的变量时都只拷贝变量的实际大小那么这将大大增加管理的复杂性并可能导致内存碎片化。通过以页面为单位进行拷贝操作系统可以简化内存管理减少内存碎片并提高内存访问的效率。 那么操作系统是如何对物理内存做管理的呢 首先物理内存是被划分为一个一个的页框的若物理内存的大小为4GB那么页框的数量就有1048576个那么操作系统就要知道这些页框的使用状态那么操作系统是如何管理这些页框的呢 操作系统由对应的结构体struct page 其中int flag变量就是管理页框是否被占有是否有脏页是否被锁定的还会包含mode权限等等。 struct page memory[1048576]把内存管理起来用下标转化为每一个页框的起始地址。 关于页表32bit机器上 我们都知道虚拟地址是32个比特位组成的一共有2^32个。 虚拟地址是如何转化为物理地址的呢 我们都知道虚拟地址转化为物理地址都是要通过页表映射关键就在于页表。页表并不是简单的一一映射他是有多级结构的以32bit机器为例 在32位系统中虚拟地址的32个比特位通常按照以下方式划分以多级页表为例 页目录索引高位的比特位用于索引页目录。页目录是一个包含多个页表项的数组每个页表项指向一个页表。页目录索引的位数决定了页目录中页表项的数量进而影响页目录的大小。 页表索引紧接着页目录索引之后的比特位用于索引页表。页表也是一个包含多个页表项的数组每个页表项包含物理页帧的起始地址和其他信息如访问权限。页表索引的位数决定了页表中页表项的数量进而影响页表的大小。 页内偏移最低位的比特位用于在物理页帧内定位数据。页内偏移的位数决定了页帧的大小通常是固定的如4KB。 具体划分示例 以常见的32位系统为例虚拟地址的32个比特位可能被划分为10-10-12的形式 高10位作为页目录索引可以索引到最多10242^10个页表。中间10位作为页表索引每个页表可以包含最多10242^10个页表项。低12位作为页内偏移用于在4KB2^12字节的页帧内定位数据。一个页帧的大小刚好是4KB也就是说页内偏移量可以定位到每一个字节。那么我们也就知道了前20位的作用就是定位到页框号本质就是搜索页框后12那就是用来定位页框内的如何一个字节。这个方案就叫二级页表。这大大的节省了空间1024个页表*2KB2MB4kb页目录这是在拉满的情况下在这种方式下只要知道取的数据是什么类型就知道要取几个字节就能获取数据了。 CPU想要通过页表获取物理地址首先就是要找到页表那么页表在哪里呢 CR3控制寄存器3也被称为PDBR页目录基址寄存器用于存储页目录表的物理地址。通过改变CR3寄存器的值可以实现不同虚拟地址空间之间的切换。 MMU接收到CPU发出的虚拟地址后会根据当前CR3寄存器中存储的页目录表物理地址以及虚拟地址的结构如页目录索引、页表索引、页内偏移等在页目录表和页表中查找对应的物理地址。最后从CPU中出来的直接就是虚拟地址。 2.线程的概念和Linux中线程的实现 概念部分 线程在进程内部运行是cpu调度的基本单位。 初步理解在下面一个一个的tesk_struct就是一个一个的执行流地址空间的正文代码也会被分为4部分让每一个执行流去执行这一个一个的执行流就是Linux中的线程这是我们对线程的初步理解一个进程中可以并发多个线程每条线程并行执行不同的任务。 在学习进程的时候我们得出结论进程内核数据结构进程的代码和数据。 现在我们从内核观点给出进程的定义进程是承担分配系统资源的基本实体 对比以前对进程的理解区别在于内部只有一个执行流的进程。 OS关于线程的设计 在windows系统下线程是真实存在的有自己的控制结构体与调度算法 从内核的角度来看Linux并没有线程这个概念。Linux的线程通常被当作一种特殊的进程是进程模拟的来实现。每个线程都拥有自己独立的task_struct内核数据结构对象但在进程内部多个线程共享进程的地址空间和其他资源。 对于CPU来说调度一个task_struct进程因为task_struct可能只是一个进程的一个执行流。那么CPU要不要区分task_struct是进程还是线程 当然不必区分对于CPU来说都叫做执行流所以之前与进程有关的知识在Linux下仍然适用因为线程就是一个特殊的进程。CPU看到的执行流进程。因此我们称Linux中的执行流轻量级进程 代码部分 先见一见 引入函数pthread_create用于在程序中创建一个新的线程 参数说明 thread指向 pthread_t 类型的指针用于存储新创建的线程的标识符。成功调用后这个标识符可以用来引用该线程。attr指向 pthread_attr_t 类型的指针用于设置线程的属性如线程栈的大小、调度策略等。如果传递 NULL则使用默认属性。start_routine线程将要执行的函数的指针。这个函数应该接受一个 void* 类型的参数并返回一个 void* 类型的值。这个函数是线程开始执行时调用的函数。arg传递给 start_routine 函数的参数。这个参数的类型是 void*这意味着你可以传递任何类型的指针。 主线程和新创建的线程会并行执行直到新线程完成其任务。 eg:两个执行流同时跑死循环 在进行线程的编译时要引入第三方库pthread它提供了一套创建和管理线程的API。这些API使得在多种UNIX系统上编写多线程程序成为可能同时也增强了程序的可移植性。 编译时要带-lpthread链接pthread库 test1:test.ccg -o $ $^ -stdc11 -lpthread
.PHONY:clean
clean:rm -rf test1 代码 #include iostream
#include unistd.h//新进程
void *threadStart(void *args)
{while (true){sleep(1);std::cout new thread running... std::endl;}
}int main()
{pthread_t tid1;pthread_create(tid1, nullptr, threadStart, (void *)thread-new);//主线程while(true){sleep(1);std::cout main thread running... std::endl;}return 0;
} 同时执行两个死循环这就是一个多线程的代码。 这时候你查询系统中的进程时发现只有一个进程 更改代码后让它们打出各自的pid果然都一样 原因是这两个线程属于一个进程内部。 但也是可以通过命令看到有几个线程的ps -aL,我们可以看到LWPLightweight Process轻量级进程OS进行调度的时候看的就是LWP而不是PIDLWP才是标识一个 执行流的概念LWP和PID相等的执行流我们称之为主线程特殊情况多进程单进程调度时看OS根据PID来区分这不矛盾因为在这两种情况下PIDLWP 每个线程都有自己要执行的代码每行代码都有自己的地址在逻辑上只要每个线程拿到自己代码所对应的那部分页表就能找到自己执行代码的地址了就能执行代码了。 问题 1.已经有多进程了为什么要有多线程呢 创建 首先进程创建的成本是非常高的进程是系统资源分配的基本单位每个进程都拥有独立的地址空间、内存、文件描述符等资源。而创建线程1.创建PCB 2.将进程已有的资源获取就好了。 运行线程调度成本低 删除一个线程的成本也是低的 2. 线程这么好为什么要有进程呢 由于线程共享进程的内存空间因此一个线程中的错误可能会影响到进程中的其他线程。例如如果一个线程发生段错误如访问了非法地址则可能导致整个进程崩溃进而影响到该进程内的所有线程。相比之下进程间的独立性使得一个进程的崩溃不会影响到其他进程。健壮性降低当然还有其它方面进程和线程都有自己的不可取代性。 3.线程调度的成本为什么低 CPU为了加速访存会存在一个cache的硬件它会遵循局部性原理将执行代码的前几行和后几行全都加载到cache当中这一部分我们称为进程执行的热数据。当CPU执行到某行代码的时候如果这部分缓存命中了则直接从cache中读取如果没命中再从内存中缓存重新置换到cache当中。 这意味着如果是AB进程间要进行切换除了pcb地址空间页表要切A和B要执行的任务肯定是不一样的进程Acache缓存的热数据进程B用不上这意味着进程B要重新cache这就慢了。但线程进行切换的时候由于线程共享进程的地址空间和资源因此缓存中的内容仍然有效无需进行替换。这减少了缓存失效的次数和缓存加载的时间从而降低了调度的成本。主要矛盾 3.关于线程的有点与缺点 优点 创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。 缺点 性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。 健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。 eg我们写了一段代码 我们发现创建出3个线程加上一个主线程只要有一个线程出问题了其它的线程就都受影响终止了一个线程出问题OS就是识别到整个进程出问题OS就会给进程发信号每个线程都要处理。 #include iostream
#include unistd.h
#include ctime// 新线程void *threadStart(void *args)
{while (true){int x rand() % 5;std::cout new thread running... , pid: getpid(): x std::endl;sleep(1);if(x 0){int *p nullptr;*p 100; // 野指针}}
}int main()
{srand(time(nullptr));pthread_t tid1;pthread_create(tid1, nullptr, threadStart, (void *)thread-new);pthread_t tid2;pthread_create(tid2, nullptr, threadStart, (void *)thread-new);pthread_t tid3;pthread_create(tid3, nullptr, threadStart, (void *)thread-new);// 主线程while(true){sleep(1);std::cout main thread running... ,pidgetpid()std::endl;}return 0;
} 缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。 eg我们发现只要主线程更改了全局变量gvall的值其它线程都是会受影响的因为线程大部分的资源都是共享的 #include iostream
#include unistd.h
#include ctimeint gval 100;// 新线程
void *threadStart(void *args)
{while (true){sleep(1);std::cout new thread running... , pid: getpid() , gval: gval , gval: gval std::endl;}
}int main()
{srand(time(nullptr));pthread_t tid1;pthread_create(tid1, nullptr, threadStart, (void *)thread-new);pthread_t tid2;pthread_create(tid2, nullptr, threadStart, (void *)thread-new);pthread_t tid3;pthread_create(tid3, nullptr, threadStart, (void *)thread-new);// 主线程while (true){std::cout main thread running... , pid: getpid() , gval: gval , gval: gval std::endl;gval; // 修改sleep(1);}return 0;
} 编程难度提高 编写与调试一个多线程程序比单线程程序困难得多 4.进程VS线程 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境 文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id 进程和线程的关系如下图: 进程是资源分配的基本单位 线程是调度的基本单位线程共享进程数据但也拥有自己的一部分数据: 线程ID一组寄存器与硬件上下文数据有关--线程是在动态运行的栈线程在运行的时候本质是在运行一个函数会形成各种临时变量临时变量会被每个线程保存在自己的栈区errno信号屏蔽字调度优先级