1.简介和概述
2021/10/18 6:09:48
本文主要是介绍1.简介和概述,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
线程
进程并不是内核支持的唯一一种程序执行形式。除了重量级进程(有时也称为UNIX进程)之外, 还有一种形式是线程(有时也称为轻量级进程)。线程也已经出现相当长的一段时间,本质上一个进 程可能由若干线程组成,这些线程共享同样的数据和资源,但可能执行程序中不同的代码路径。简而 言之,进程可以看作一个正在执行的程序,而线程则是与主程序并行运行的程序函数或例程。该特性是有用的, 例如在浏览器需要并行加载若干图像时。通常浏览器只好执行几次fork和exec调用,以此创建若干并行的进程 实例。这些进程负责加载图像,并使用某种通信机制将接收的数据提供给主程序。在使用线程时,这种情况更容易处理一些 。(问题1) 浏览器定义了一个例程来加载图像,可以将例程作为线程启动,使用参数不同的多个线程即可。由于 线程和主程序共享同样的地址空间,主程序自动就可以访问接收到的数据。因此除了为防止线程访问 同一内存区而采取的互斥机制外,就不需要什么通信了。图1-2说明了有和没有线程的程序之间的差别。地址空间与特权级别
地址空间的最大长度与实际可用的物理内存数量无关,因此被称为虚拟地址空间。使用该术语的 另一个理由是,从系统中每个进程的角度来看,地址空间中只有自身一个进程,而无法感知到其他进 程的存在。应用程序无需关注其他程序的存在,好像计算机中只有一个进程一样。 Linux将虚拟地址空间划分为两个部分,分别称为内核空间和用户空间,如图1-3所示。
系统中每个用户进程都有自身的虚拟地址范围,从0到TASK_SIZE。用户空间之上的区域(从 TASK_SIZE到232或264)保留给内核专用,用户进程不能访问。TASK_SIZE是一个特定于计算机体系结 构的常数,把地址空间按给定比例划分为两部分。例如在IA-32系统中,地址空间在3 GiB处划分,因 此每个进程的虚拟地址空间是3 GiB。由于虚拟地址空间的总长度是4 GiB,所以内核空间有1 GiB可用。 尽管实际的数字依不同的计算机体系结构而不同,但一般概念都是相同的。
特权级别
内核把虚拟地址空间划分为两个部分,因此能够保护各个系统进程,使之彼此隔离。所有的现代 CPU都提供了几种特权级别,进程可以驻留在某一特权级别。每个特权级别都有各种限制,例如对执 行某些汇编语言指令或访问虚拟地址空间某一特定部分的限制。IA-32体系结构使用4种特权级别构成 的系统,各级别可以看作是环。内环能够访问更多的功能,外环则较少,如图1-4所示。
尽管英特尔处理器区分4种特权级别,但Linux只使用两种不同的状态:核心态和用户状态。两种 状态的关键差别在于对高于TASK_SIZE的内存区域的访问。简而言之,在用户状态禁止访问内核空间。 用户进程不能操作或读取内核空间中的数据,也无法执行内核空间中的代码。这是内核的专用领域。 这种机制可防止进程无意间修改彼此的数据而造成相互干扰。 从用户状态到核心态的切换通过系统调用的特定转换手段完成,且系统调用的执行因具体系统而 不同。如果普通进程想要执行任何影响整个系统的操作(例如操作输入/输出装置),则只能借助于系 统调用向内核发出请求。内核首先检查进程是否允许执行想要的操作,然后代表进程执行所需的操作, 接下来返回到用户状态。 除了代表用户程序执行代码之外,内核还可以由异步硬件中断激活,然后在中断上下文中运行。 与在进程上下文中运行的主要区别是,在中断上下文中运行不能访问虚拟地址空间中的用户空间部 分。因为中断可能随机发生,中断发生时可能是任一用户进程处于活动状态,由于该进程基本上与中 断的原因无关,因此内核无权访问当前用户空间的内容。在中断上下文中运行时,内核必须比正常情 况更加谨慎,例如,不能进入睡眠状态。(问题2)在编写中断处理程序时需要特别注意这些,图1-5概述了不同的执行上下文。
页表
用来将虚拟地址空间映射到物理地址空间的数据结构称为页表。实现两个地址空间的关联最容易 的方法是使用数组,对虚拟地址空间中的每一页,都分配一个数组项。该数组项指向与之关联的页帧, 但有一个问题。例如,IA-32体系结构使用4 KiB页,在虚拟地址空间为4 GiB的前提下,则需要包含100 万项的数组。在64位体系结构上,情况会更糟糕。每个进程都需要自身的页表,因此系统的所有内存 都要用来保存页表,也就是说这个方法是不切实际的。因为虚拟地址空间的大部分区域都没有使用, 因而也没有关联到页帧,那么就可以使用功能相同但内存用量少得多的模型:多级分页。 为减少页表的大小并容许忽略不需要的区域,计算机体系结构的设计会将虚拟地址划分为多个部 分,如图1-7所示。(具体在地址字的哪些位区域进行划分,可能依不同的体系结构而异,但这与现在 我们讨论的内容不相关)。在例子中,我将虚拟地址划分为4部分,这样就需要一个三级的页表。大多 数体系结构都是这样的做法。但有一些采用了四级的页表,而Linux也采用了四级页表。为简化场景, 我在这里会一直用三级页表阐述。虚拟地址的第一部分称为全局页目录(Page Global Directory,PGD)。PGD用于索引进程中的一 个数组(每个进程有且仅有一个),该数组是所谓的全局页目录或PGD。PGD的数组项指向另一些数 组的起始地址,这些数组称为中间页目录(Page Middle Directory,PMD)。 虚拟地址中的第二个部分称为PMD,在通过PGD中的数组项找到对应的PMD之后,则使用PMD 来索引PMD。PMD的数组项也是指针,指向下一级数组,称为页表或页目录。 虚拟地址的第三个部分称为PTE(Page Table Entry,页表数组),用作页表的索引。虚拟内存页和 页帧之间的映射就此完成,因为页表的数组项是指向页帧的。 虚拟地址最后的一部分称为偏移量。它指定了页内部的一个字节位置。归根结底,每个地址都指 向地址空间中唯一定义的某个字节。 页表的一个特色在于,对虚拟地址空间中不需要的区域,不必创建中间页目录或页表。与前述使 用单个数组的方法相比,多级页表节省了大量内存。 当然,该方法也有一个缺点。每次访问内存时,必须逐级访问多个数组才能将虚拟地址转换为物 理地址。CPU试图用下面两种方法加速该过程。 (1) CPU中有一个专门的部分称为MMU(Memory Management Unit,内存管理单元),该单元优 化了内存访问操作。 (2) 地址转换中出现最频繁的那些地址,保存到称为地址转换后备缓冲器(Translation Lookaside Buffer,TLB)的CPU高速缓存中。无需访问内存中的页表即可从高速缓存直接获得地址数据,因而 大大加速了地址转换。 在许多体系结构中高速缓存的运转是透明的,但某些体系结构则需要内核专门处理。这更意味着 每当页表的内容变化时必须使TLB高速缓存无效。内核中凡涉及操作页表之处都必须调用相应的指 令。
内存映射
内存映射是一种重要的抽象手段。在内核中大量使用,也可以用于用户应用程序。映射方法可以 将任意来源的数据传输到进程的虚拟地址空间中。作为映射目标的地址空间区域,可以像普通内存那 样用通常的方法访问。但任何修改都会自动传输到原数据源。这样就可以使用相同的函数来处理完全 不同的目标对象。例如,文件的内容可以映射到内存中。处理只需读取相应的内存即可访问文件内容, 或向内存写入数据来修改文件的内容。内核将保证任何修改都会自动同步到文件中。 内核在实现设备驱动程序时直接使用了内存映射。外设的输入/输出可以映射到虚拟地址空间的 区域中。对相关内存区域的读写会由系统重定向到设备,因而大大简化了驱动程序的实现。物理内存的分配
在内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域。由 于内存分配和释放非常频繁,内核还必须保证相关操作尽快完成。内核可以只分配完整的页帧。将内 存划分为更小的部分的工作,则委托给用户空间中的标准库。标准库将来源于内核的页帧拆分为小的 区域,并为进程分配内存。1. 伙伴系统
内核中很多时候要求分配连续页。为快速检测内存中的连续区域,内核采用了一种古老而历经检 验的技术:伙伴系统。 系统中的空闲内存块总是两两分组,每组中的两个内存块称作伙伴。伙伴的分配可以是彼此独立 的。但如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块 的伙伴。图1-8示范了该系统,图中给出了一对伙伴,初始大小均为8页。
内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理。 各有8页的一对伙伴也在相应的列表中。 如果系统现在需要8个页帧,则将16个页帧组成的块拆分为两个伙伴。其中一块用于满足应用程 序的请求,而剩余的8个页帧则放置到对应8页大小内存块的列表中。 如果下一个请求只需要2个连续页帧,则由8页组成的块会分裂成2个伙伴,每个包含4个页帧。其 中一块放置回伙伴列表中,而另一个再次分裂成2个伙伴,每个包含2页。其中一个回到伙伴系统,另 一个则传递给应用程序。 在应用程序释放内存时,内核可以直接检查地址,来判断是否能够创建一组伙伴,并合并为一个 更大的内存块放回到伙伴列表中,这刚好是内存块分裂的逆过程。这提高了较大内存块可用的可能性。 在系统长期运行时,服务器运行几个星期乃至几个月是很正常的,许多桌面系统也趋向于长期开 机运行,那么会发生称为碎片的内存管理问题。频繁的分配和释放页帧可能导致一种情况:系统中有 若干页帧是空闲的,但却散布在物理地址空间的各处。换句话说,系统中缺乏连续页帧组成的较大的 内存块,而从性能上考虑,却又很需要使用较大的连续内存块。通过伙伴系统可以在某种程度上减少 这种效应,但无法完全消除。如果在大块的连续内存中间刚好有一个页帧分配出去,很显然这两块空 闲的内存是无法合并的。
2.slab缓存
对于只有一个字节的内存需求也最少需要分配一个页框大小的块,而且页内的其余内存不能被其他进程使用,这就形成了很大的内部碎片。为了应对内部碎片,内核又提供了slab机制。在分配与释放内存时需要操作很多数据结构,slab机制利用了CPU三级缓存机制,将这些结构体存放在缓存cache中,分配的时候从缓存获取,释放的时候又交还给缓存。这样就提高了内存分配与释放的速度,同时在cache中使用空闲链表,所以这些缓存会连续存放不会导致碎片。Slab分配器有以下几个基本原则:
1:频繁使用的数据结构应该被缓存起来;
2:频繁使用的数据结构会导致内存碎片,可以使用空闲链表将缓存连续存放不会产生碎片;
3:缓存可以减少CPU访问内存的次数;
3. 页面交换和页面回收
经常使用的页可以写入硬盘。如果再需要访问相关数据,内核会将相应的页切换回内存。通过缺页异
常机制,这种切换操作对应用程序是透明的。换出的页可以通过特别的页表项标识。在进程试图访问
此类页帧时,CPU则启动一个可以被内核截取的缺页异常。此时内核可以将硬盘上的数据切换到内存
中。接下来用户进程可以恢复运行。由于进程无法感知到缺页异常,所以页的换入和换出对进程是完
全不可见的。页面回收用于将内存映射被修改的内容与底层的块设备同步,为此有时也简称为数据回写。数据
刷出后,内核即可将页帧用于其他用途(类似于页面交换)。内核的数据结构包含了与此相关的所有
信息,当再次需要该数据时,可根据相关信息从硬盘找到相应的数据并加载。
文件系统
Linux系统由数以千计乃至百万计的文件组成,其数据存储在硬盘或其他块设备(例如ZIP驱动、 软驱、光盘等)。存储使用了层次式文件系统。文件系统使用目录结构组织存储的数据,并将其他元 信息(例如所有者、访问权限等)与实际数据关联起来。Linux支持许多不同的文件系统:标准的Ext2 和Ext3文件系统、ReiserFS、XFS、VFAT(为兼容DOS),还有很多其他文件系统。不同文件系统所基 于的概念抽象,在某种程度上可以说是南辕北辙。Ext2基于inode,即它对每个文件都构造了一个单独 的管理结构,称为inode,并存储到磁盘上。inode包含了文件所有的元信息,以及指向相关数据块的 指针。目录可以表示为普通文件,其数据包括了指向目录下所有文件的inode的指针,因而层次结构得 以建立。相比之下,ReiserFS广泛应用了树形结构来提供同样的功能。 内核必须提供一个额外的软件层,将各种底层文件系统的具体特性与应用层(和内核自身)隔离 开来。该软件层称为VFS(Virtual Filesystem或Virtual Filesystem Switch,虚拟文件系统或虚拟文件系 统交换器)。VFS既是向下的接口(所有文件系统都必须实现该接口),同时也是向上的接口(用户进 程通过系统调用最终能够访问文件系统功能)。如图1-10所示
问题1思考:
这里对为什么使用线程更容易说说自己的看法,首先是要明白为什么我们强调的是进程通信,线程同步,而不是进程同步,线程通信呢?
我们知道,内核为每个进程分配了地址空间,各个进程的地址空间是独立的,进程不会意识到彼此的存在,从进程的角度来看,它会认为
自己是系统中唯一的进程。内核是如何办到的?说白了,就是通过不同进程维护不同的页表来让进程互相之间不会产生干扰。页表是干嘛的?
我上面有介绍,现在只需知道CPU在通过页表转换之前的地址都是虚拟地址,既然是虚拟的,那么它多大都无所谓,对于32位系统来说
它可以是0~4GB范围中的任何一个,当然,一般内核在3GB以上,用户在3GB以下,我们平常运行的程序位于用户态,即3GB以下,一般虚拟
地址也就在这范围。而页表就是将虚拟地址转换为物理地址的结构,每个进程的页表是不同的,理解了这一点,我们可以看看在系统刚开始时假
设有A,B2个进程,初始时,假设A,B2个的页表是空的,即页表什么映射都没做(实际是不可能的),现在假设A要访问0这个地址(仅仅假设,一
般虚拟地址都不会为0),首先在页表转换前地址都是虚拟地址,CPU必须要获得物理地址才能进行相应的指令执行,于是CPU用虚拟地址0查询A
的页表(正在执行的进程)看看相对应的物理地址是否存在,发现不存在,便触发缺页异常,从磁盘获取相对应的块,现在只需要进行页表的映射
即可完成对虚拟地址0的访问了,即获得空闲未使用的一块物理地址然后把它加入到A页表虚拟地址0到物理地址的映射中,再重新执行访问虚拟地址
0的那条指令即可。如何获取未使用的物理地址?内核肯定是记录了整个物理块的使用情况,毕竟每个进程的虚拟地址都是4GB(32位系统),但物
理地址就只有一个,你物理地址是多少就是多少,CPU就在该地址上做相应的访问或修改,不会再进行任何转换了。现在假设所有物理地址都是可用
的,同时物理地址分配策略也是非常简单的首次适配,那么物理地址0页就被成功分配给A了(虚拟地址和物理地址可以相同,这无所谓),那么A现在
的页表中就会有记录了,虚拟地址0映射到物理地址0页。同理对B进程,它也访问虚拟地址0,同样触发缺页异常,但这时物理地址0页已经分配了,那么B
就只能获得物理地址1页了,这时B的页表就是虚拟地址0映射到物理地址1页了。我们可以看到,为什么各个进程的地址空间是独立的,进程不会意识到彼此的
存在?A访问虚拟地址0,但映射到的是物理页0,B却是物理页1,也就是说即使A,B虚拟地址相同,但是A,B页表的不同使得A,B访问到了不同的物理地址了,
这就防止A,B进程互相干扰,若A,B进程映射到的是同一个物理地址的话,那么假设A首先对虚拟地址0设置为1,在这时,进行了进程调度,切换到B了,B也对虚
拟地址0进行设置为2,那么等到下次A再运行时,再访问虚拟地址0映射到的物理地址的话,它的值就是B设置的值,也就是2了。
综上:进程间强调通信是因为进程的页表不同(进程的虚拟地址可以为0到最大值的任一一个,物理地址却不行),也就是即使虚拟地址相同它映射到的物理地址也不
同,也就各自独立,不会互相干扰了,你修改你的物理地址,我修改我的物理地址。但如果2个进程要通信,则必须让2个进程访问到同一个物理地址,这需要对页
表进行设置,而页表位于内核,所以需要通过系统调用让用户态陷入内核态执行。最终的结果是A的任一虚拟地址映射到的物理地址与B的任一虚拟地址映射到的物
理地址相同,那么A在虚拟地址上对应的物理地址存储信息,B通过对应的虚拟地址也访问到该物理地址,也就实现了进程间的通信了。那么对于线程来说,为什么
不强调通信呢?因为同一个进程的线程的页表是相同的,你A线程的虚拟地址x映射到物理地址y,B也同样对虚拟地址x映射到物理地址y,(必须是同一个进程下的线程),
因此,线程的通信是极其简单的,在C语言中,2个线程最简单的通信方式就是通过全局变量了,既没有函数调用也没有陷入内核的开销,性能极高。而进程就不行了,
你如果实验过就可以看到,线程操作同一全局变量都是可见的,但父进程和子进程操作全局变量是互不影响的,虽然2个进程操作的全局变量地址(虚拟地址)相同,原因
就在此处。线程更强调的是同步,即因为对公共资源的访问,所以必须保证原子操作,这里不再讲述。
问题2思考:
Linux中断中不可睡眠的原因:
《操作系统真相还原》P305中有这样一句话:当通过中断门进入中断后,标志寄存器中的eflags中的IF位自动置0,也就是在进入中断后,自动把中断关闭,避免中断嵌套。
那么按照这个思路,若这时你在中断中休眠的话,内核会选择新的线程运行,这时由于是在关中断下线程运行,此时不会响应时钟等大部分中断,则其它线程无法再次获得运行机会。
(上述言论仅代表个人看法,难免会有错误,目前只是在起始阶段,等看到后面若发现有不对地方再来修改。有些地方不严谨,比如初始时页表什么映射都没做(这是错误的,进程页表最起码初始时
映射了整个内核),任一虚拟地址等用词不恰当(内核虚拟地址一般在高地址,用户空间中的虚拟地址一般处于内核以下部分),这里仅仅是为了更好理解)。
这篇关于1.简介和概述的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-14动态路由项目实战:从入门到上手
- 2024-11-14函数组件项目实战:从入门到简单应用
- 2024-11-14获取参数项目实战:新手教程与案例分析
- 2024-11-14可视化开发项目实战:新手入门教程
- 2024-11-14可视化图表项目实战:从入门到实践
- 2024-11-14路由懒加载项目实战:新手入门教程
- 2024-11-14路由嵌套项目实战:新手入门教程
- 2024-11-14全栈低代码开发项目实战:新手入门指南
- 2024-11-14全栈项目实战:新手入门教程
- 2024-11-14useRequest教程:新手快速入门指南