申小林

  • 2025-02-23
  • 发表了主题帖: 《奔跑吧Linux内核1:基础架构》-05-内存高级管理

    今天搞点不一样的,就是内存高级管理,说实话自己还是不理解,所以整理了一些资料给大家分享,这些资料均来自于网络,大家可以一起学习进步。   1. 什么是 Linux 内核内存管理中的 page? 在 Linux 内核内存管理中,page 是内存管理的基本单位。系统物理内存被划分为大小相等的 page,常见大小为 4KB(可依系统配置调整)。内核通过 struct page 结构体管理每个物理页面,结构体包含页面状态信息(如是否被占用、是否为脏页)、引用计数及与其他内存管理组件的关联等重要信息。在页面分配与回收时,内核依据 page 结构体状态判断页面是否可用,已占用页面不能分配给其他进程。 2. PMAP 是什么,有什么作用? PMAP 即 Process Memory Map(进程内存映射),描述进程虚拟地址空间与物理内存的映射关系。进程虚拟地址空间划分为多个区域,每个区域由 vm_area_struct 结构体表示,包括代码段、数据段、堆、栈及内存映射区域等。通过 pmap 命令(如 pmap ),可查看进程内存映射情况,输出进程各虚拟地址区间、对应权限(可读、可写、可执行)、映射文件(若有)及物理内存使用等详细信息,有助于了解进程内存布局与使用状况,排查内存相关问题(如内存泄漏、非法内存访问)。 3. 什么是匿名页,它在进程内存管理中有何作用? 匿名页是特殊内存页面,无对应文件映射,常用于进程的堆、栈及通过 mmap 系统调用创建的匿名映射区域。进程运行时,动态分配内存(如调用 malloc 函数),内核会分配匿名页。以堆内存分配为例,进程调用 malloc,glibc 库通过系统调用向内核请求内存,内核在合适内存区域(如堆所在虚拟地址空间)找到空闲匿名页,映射到进程虚拟地址空间,该匿名页归进程所有,进程在生命周期内可读写操作。内存回收时,系统内存紧张,内核采用交换(swap)方式,将匿名页数据暂存磁盘交换分区释放物理内存,进程再次访问时触发缺页异常,内核从交换分区读取数据,重新加载到物理内存并更新页表映射关系。 4. 页面迁移的目的和实现过程是怎样的? 页面迁移指将物理内存页面从一个位置移到另一个位置,同时保证进程对页面访问不受影响。其最早为 NUMA(Non - Uniform Memory Access)系统设计,提升进程内存访问性能。在 NUMA 系统中,不同 CPU 节点访问本地内存快于其他节点内存,通过页面迁移,可将进程频繁访问页面移到进程所在 CPU 节点本地内存,减少内存访问延迟,提高系统整体性能。例如在多 CPU 节点 NUMA 系统中,若进程在 CPU24 - CPU47 运行,访问的内存页面在非本地内存节点,系统检测到性能瓶颈,尝试将页面迁移到 CPU24 - CPU47 所在节点本地内存,迁移后进程访问速度显著提升。页面迁移实现过程:内核先分配新目标页面(new_page),解除原有页面(old_page)映射关系,将旧页面数据复制到新页面,最后建立新页面映射关系,确保进程能正确访问迁移后的页面。页面迁移中,若应用程序访问正在迁移的页面,内核根据页面迁移状态处理:旧页面页表未解除映射,应用程序正常访问原页面;旧页面已解除映射但新页面未建立映射,应用程序访问被阻塞,直到新页面建立映射且数据复制完成;页面迁移完成,应用程序正常访问新页面。 5. 内存规整的作用是什么,在 Linux 内核中是如何实现的? 内存规整是解决内存碎片化问题的重要手段。系统运行时,内存分配与释放导致内存碎片化,虽有足够空闲内存,但分散成小块,无法满足连续大块内存分配请求。内存规整目的是移动内存页面,合并分散空闲内存块为连续大块内存,满足高阶内存分配需求。在 Linux 内核中,内存规整与页面分配器紧密相关。页面分配器在慢速路径分配页面失败(分配水位线为 min),回收内存后仍无法满足需求时,判断是否满足特定条件(如允许调用直接页面回收机制、存在高成本分配需求且分配不可迁移的多个连续物理页面等),满足则尝试调用__alloc_pages_direct_compact () 函数进行内存规整。__alloc_pages_direct_compact () 函数主要调用 try_to_compact_pages () 函数遍历内存节点中的所有区域(zone),对每个区域进行内存规整。内存规整过程中,调用 compact_zone_order () 函数尝试分配连续内存。若内存规整成功,捕获到可用页面,使用 prep_new_page () 函数准备使用该页面;若未成功获取页面,则调用 get_page_from_freelist () 从空闲链表获取页面。通过内存规整,系统整理碎片化内存,提高内存利用率,更好满足连续大块内存分配请求。 6. KSM 是什么,它是如何节省物理内存的? KSM(Kernel Same - Page Merging)是内核机制,用于合并多个进程中相同内容的内存页面,节省物理内存。多进程系统中,不同进程可能加载相同共享库或拥有相同匿名数据(如 fork 子进程后,父子进程在写时复制(Copy - On - Write,COW)机制下共享相同页面)。KSM 通过定期扫描内存,查找相同内容页面,合并为一个共享页面,多个进程通过页表映射到该共享页面。例如多个进程加载同一个动态链接库,其代码段在各进程中相同,无 KSM 时,每个进程物理内存有一份代码段副本;启用 KSM 后,内核识别相同代码段页面,合并为共享页面,相关进程页表都指向该共享页面,节省大量物理内存空间。KSM 实现依赖内核页表管理、内存扫描算法等机制。内核通过后台线程(通常为 ksmd)定期扫描内存,比较页面内容,发现相同页面时,修改相关进程页表,指向共享页面,并管理共享页面引用计数,确保所有相关进程不再使用该页面时能正确回收内存。 7. Linux 内核如何管理内存碎片化问题? 内存碎片化分为内部碎片化和外部碎片化。内部碎片化是分配内存时,分配内存块大小与实际需求不匹配,导致内存块有未使用部分;外部碎片化是内存频繁分配与释放,系统出现大量分散、难以利用的小空闲内存块。Linux 内核采用多种策略管理内存碎片化。对于外部碎片化,采用内存规整,移动内存页面合并空闲内存块为连续大块内存,满足高阶内存分配需求。对于内部碎片化,在内核内存分配算法上优化,如 slab 分配器,根据不同对象大小需求,预先创建多个不同大小的 slab 缓存,每个缓存专门分配特定大小范围内的对象,进程请求内存时,内核选择最合适的 slab 缓存分配,减少内部碎片化。此外,内核监控和统计内存使用情况,分析内存分配和释放模式,动态调整内存管理策略,降低内存碎片化程度,提高内存使用效率和系统整体性能。

  • 发表了主题帖: 《Linux内核深度解析》-06-Linux文件系统

              在 Linux 的世界里,文件系统犹如一座大厦的基石,承载着整个系统的数据存储与管理重任。它不仅仅是简单地将数据存储在磁盘上,更是构建了一个有序的体系,让用户和应用程序能够高效地访问和操作数据。从系统启动时加载的根文件系统,到日常使用中各种数据文件、配置文件的存储,文件系统贯穿于 Linux 系统运行的始终。 对于开发者而言,深入理解 Linux 文件系统是进行高效开发的必备技能。在开发过程中,无论是编写应用程序读写文件,还是进行系统级的编程,都离不开对文件系统的操作。例如,在开发一个日志管理系统时,需要精确地控制日志文件的存储位置、大小限制以及读写权限,这就要求开发者对文件系统的特性和机制有深入的了解。而对于系统管理员来说,文件系统的管理更是日常工作的核心之一。从文件系统的挂载与卸载,到磁盘空间的管理、文件权限的设置,每一个环节都直接影响着系统的稳定性和安全性。 Linux 文件系统的基本概念 (一)“一切皆文件” 的理念 在 Linux 的世界里,有一个深入人心的理念 ——“一切皆文件” 。这意味着,无论是硬件设备、进程信息,还是普通的数据文件,在 Linux 系统中都被抽象成文件的形式进行管理。比如,硬盘设备通常对应着/dev/sda这样的设备文件,通过对这个文件的读写操作,就可以实现对硬盘的访问。再如,每个正在运行的进程在/proc目录下都有一个对应的文件夹,里面包含了该进程的各种信息,如进程 ID、内存使用情况等,这些信息都以文件的形式呈现。这种理念极大地简化了系统的设计和管理,为用户和开发者提供了统一的操作接口,使得对各种资源的访问变得更加便捷和高效。 二)文件与目录结构 Linux 的文件和目录结构采用了从根目录/开始的树状结构。这种结构清晰明了,就像一棵大树,根目录是树干,各个子目录是树枝,文件则是树叶。在这个结构中,有一些常见的子目录,它们各自承担着重要的职责。 /bin目录存放着系统的基本命令,如ls、cp、mv等,这些命令是系统运行和用户日常操作不可或缺的工具;/etc目录是系统配置文件的聚集地,各种应用程序和服务的配置文件都存储在这里,比如网络配置文件、用户信息文件等,对系统的正常运行起着关键的作用;/home目录用于存放普通用户的主目录,每个用户都有一个以自己用户名命名的子目录,用户可以在其中存储个人文件和配置信息 。 (三)常见文件系统类型 EXT 系列:EXT 系列文件系统是 Linux 中非常经典且广泛使用的文件系统。其中,EXT4 是 EXT3 的后继者,它具有出色的稳定性和可靠性,适用于大多数日常使用场景,如个人计算机、笔记本电脑以及中小型服务器等。它支持最大 1 EiB 的文件系统和 16 TiB 的单个文件,并且提供了日志功能,这使得在系统崩溃时能够快速恢复数据,确保数据的完整性。同时,它还支持延迟分配,有效优化了磁盘空间的利用率。 XFS:XFS 是一种高性能的文件系统,以其卓越的性能和可扩展性而闻名。它特别适合处理大文件和高并发访问的场景,在大规模存储系统、高性能计算(HPC)环境以及企业级服务器中得到了广泛应用。XFS 支持最大 8 EiB 的文件系统和文件大小,文件及目录索引采用 B + 树结构,这使得查询与分配存储空间的速度非常快,并且其性能不受目录及文件数量的限制。此外,XFS 还具备强大的日志功能和实时数据写入优化,能够在线调整文件系统大小。 Btrfs:Btrfs 是一个现代化的文件系统,它支持许多高级功能,如快照、子卷、压缩和在线扩展等。这些特性使得 Btrfs 在大规模存储系统和云存储环境中表现出色。例如,快照功能可以为用户提供数据备份和恢复的便捷方式,子卷则类似于独立的文件系统,可以单独进行管理。Btrfs 还专门对 SSD 进行了优化,通过其写入时复制(COW)技术和针对 SSD 的块空间分配策略优化,能够有效提高 IO 性能 。 文件系统的核心要素 一)索引节点(inode) inode,即索引节点,是文件系统中极为关键的概念,它就像是文件的 “身份证”,记录着文件的各种元数据信息 。每个文件都有一个唯一对应的 inode,这些元数据涵盖了文件的诸多重要属性,如文件的类型(是普通文件、目录、符号链接还是设备文件等)、文件的权限(读、写、执行权限分别赋予哪些用户或用户组)、文件所有者和所属组的信息、文件的大小(以字节为单位)、文件的创建时间、修改时间以及访问时间等。此外,inode 还包含指向存储文件数据的实际磁盘块的指针,通过这些指针,系统能够快速定位到文件在磁盘上的具体存储位置。 inode 与文件是一一对应的关系,并且它和文件内容一样,都会被持久化存储到磁盘中,这意味着 inode 也会占用一定的磁盘空间。在 Linux 系统中,可以使用ls -i命令来查看文件的 inode 编号,使用stat命令则能获取文件更详细的 inode 信息,包括文件大小、时间戳等。每个文件系统在创建时,都会预先设定好 inode 的数量。当 inode 耗尽时,即便磁盘还有剩余空间,也无法创建新的文件或目录,这种情况在创建大量小文件时较为常见。 (二)目录项(dentry) dentry,也就是目录项,用于记录文件的名字、索引节点指针以及与其他目录项的关联关系 。当我们在终端中输入一个文件路径时,系统会依据路径查找相应的 dentry,然后通过 dentry 找到对应的 inode,最终实现对文件或目录的访问和操作。多个相互关联的目录项共同构成了文件系统的目录结构,它就像一张地图,指引着我们在文件系统的 “迷宫” 中找到所需的文件。 值得注意的是,dentry 是由内核维护的一个内存数据结构,因此通常也被称为目录项缓存。这意味着 dentry 并不像 inode 那样会被持久化存储到磁盘上,而是在系统运行过程中,根据需要动态地在内存中创建和管理。dentry 与 inode 之间存在着多对一的关系,这是因为一个文件可能会有多个文件名,即存在硬链接的情况。在这种情况下,不同的 dentry 可以指向同一个 inode,它们共享相同的文件数据。 (三)逻辑块与数据存储 在磁盘存储中,扇区是最小的物理存储单位,但文件系统并不会直接以扇区为单位来管理数据。为了提高数据管理和访问的效率,文件系统会将连续的多个扇区组合成一个逻辑块,然后以逻辑块为基本单位进行数据的存储和管理 。 每个逻辑块都有一个对应的逻辑块编号,文件系统通过维护逻辑块与文件内容的映射关系,实现对文件数据的高效读写。例如,当一个文件被写入磁盘时,文件系统会根据文件的大小和当前磁盘的空闲空间情况,为其分配若干个逻辑块,并将文件的数据依次存储到这些逻辑块中。同时,文件系统会在相应的数据结构中记录下文件与逻辑块之间的映射关系,以便在后续读取文件时能够快速准确地找到文件数据所在的逻辑块。逻辑块的大小通常是固定的,不同的文件系统可能会采用不同大小的逻辑块,常见的逻辑块大小有 4KB、8KB 等。合理选择逻辑块的大小对于文件系统的性能有着重要影响,较大的逻辑块适合存储大文件,能够减少文件系统的元数据开销,但在存储小文件时可能会造成磁盘空间的浪费;较小的逻辑块则更适合存储小文件,能够提高磁盘空间的利用率,但对于大文件的读写性能可能会有所影响。 (四)超级块(superblock) 超级块可以说是文件系统的 “心脏”,它存储着整个文件系统的全局信息和状态 。超级块通常位于文件系统的特定位置,一般是在文件系统的开头部分,并且在磁盘中占据固定的空间。 超级块中包含了众多关键信息,如文件系统的大小、每个块的大小、inode 表的大小、文件系统的布局结构、空闲块位图(用于记录哪些块是空闲的,哪些块已经被占用)、inode 位图(用于记录哪些 inode 是空闲的,哪些 inode 已经被分配使用)等。这些信息对于操作系统正确识别文件系统的类型、结构和状态,以及进行文件的读取、写入和管理至关重要。当文件系统挂载到系统中时,操作系统会首先读取超级块的信息,并将其加载到内存中,后续对文件系统的各种操作都需要依赖超级块中的信息来进行。超级块还包含一些文件系统的元数据信息,如文件系统的名称、创建时间、最近修改时间等,以及文件系统的状态信息,如挂载状态、读写状态、错误状态等。这些信息有助于操作系统和用户对文件系统进行管理和维护,及时发现和处理文件系统中可能出现的问题。 虚拟文件系统(VFS)   一)VFS 的作用与架构 在 Linux 内核中,虚拟文件系统(VFS)扮演着极为关键的角色,它就像是一个智能的 “翻译官”,为不同的文件系统提供了统一的抽象接口。在 Linux 系统中,存在着各种各样的文件系统,如 EXT4、XFS、Btrfs 以及网络文件系统 NFS 等,它们各自有着不同的实现方式和特性。而 VFS 的出现,使得用户进程和内核中的其他子系统无需关心底层具体文件系统的细节,只需要通过 VFS 提供的统一接口进行交互,就可以实现对各种文件系统的操作。 VFS 的架构设计精妙,它与系统调用、文件系统、缓存、块存储之间存在着紧密的关系。从系统调用的角度来看,当应用程序发起文件操作的系统调用时,首先会进入 VFS 层。VFS 会根据具体的操作类型和参数,调用相应的文件系统操作函数。例如,当应用程序调用open系统调用来打开一个文件时,VFS 会根据文件的路径,查找对应的文件系统,并调用该文件系统的open操作函数来完成文件的打开。 在文件系统方面,VFS 管理着各种不同类型的文件系统,每个文件系统都需要向 VFS 注册自己的操作函数,以便 VFS 能够在需要时调用。例如,EXT4 文件系统会向 VFS 注册自己的文件读写、目录操作等函数。缓存方面,VFS 与页缓存密切合作,通过缓存文件数据和元数据,减少对磁盘的直接访问,提高文件系统的访问性能。块存储则是文件系统数据的最终存储介质,VFS 通过块设备驱动与块存储进行交互,实现数据的读写操作。 二)VFS 的数据结构 超级块对象:超级块对象(super_block)是 VFS 中用于描述整个文件系统的关键数据结构。它包含了丰富的文件系统信息,如文件系统的类型(是 EXT4、XFS 还是其他类型)、文件系统的大小、每个块的大小、空闲块位图、inode 位图等。这些信息对于操作系统正确识别和管理文件系统至关重要。超级块对象以链表的形式组织,所有已挂载文件系统的超级块都被链接在一起,方便系统进行统一管理。超级块与具体文件系统的信息紧密关联,它是文件系统在 VFS 中的全局代表,通过超级块,VFS 可以获取到文件系统的各种特性和状态信息。 索引节点对象:索引节点对象(inode)记录了文件的详细元数据,包括文件的大小、权限、所有者、时间戳、链接数等关键属性。每个文件在文件系统中都有唯一对应的 inode,inode 就像是文件的 “身份证”,存储着文件的关键信息。当内核操作文件或目录时,需要依赖 inode 中的信息来进行各种操作,如文件的读写、权限检查等。inode 与文件操作密切相关,例如,在读取文件时,内核会根据 inode 中的数据块指针,找到文件数据所在的磁盘块,从而读取文件内容。 文件对象:文件对象(file)表示进程已打开的文件,它记录了进程在打开文件时的状态信息,如当前读写位置、打开模式(只读、只写、读写等)、文件访问权限等。在进程与打开文件的交互过程中,file 结构起着关键作用。当进程对文件进行读写操作时,会根据 file 结构中的信息来确定操作的位置和方式。需要注意的是,文件指针(即当前读写位置)就存储在 file 结构中,它记录了进程在文件中的当前位置,每次读写操作后,文件指针会相应地移动,以便下次操作能够正确定位到文件中的数据。 目录项对象:目录项对象(dentry)用于描述文件的逻辑属性,它在逻辑文件表示中起着重要作用。dentry 主要记录了文件的名字、索引节点指针以及与其他目录项的关联关系,多个相互关联的 dentry 构成了文件系统的目录结构。dentry 与 file 结构和 inode 结构存在紧密的关系。一方面,dentry 通过 inode 指针与 inode 结构相连,从而可以获取到文件的元数据信息;另一方面,当文件被打开时,会创建一个对应的 file 结构,而 dentry 则是连接 file 结构与文件系统目录结构的桥梁,通过 dentry,进程可以在文件系统的目录结构中找到对应的文件,并进行相应的操作。 (三)VFS 的系统调用 VFS 为应用程序提供了一系列丰富的系统调用,这些系统调用是应用程序与文件系统交互的重要接口。常见的系统调用包括open、read、write、close、lseek、mkdir、rmdir、unlink等。 以open系统调用为例,当应用程序需要打开一个文件时,会调用open系统调用,并传入文件的路径和打开模式等参数。VFS 接收到这个系统调用后,会根据文件路径查找对应的 dentry,进而找到对应的 inode,然后根据 inode 中的信息和打开模式,创建一个 file 结构,并返回一个文件描述符给应用程序。应用程序通过这个文件描述符,就可以对打开的文件进行后续的操作。 read系统调用用于从打开的文件中读取数据。应用程序调用read系统调用,并传入文件描述符、数据缓冲区和读取的字节数等参数。VFS 根据文件描述符找到对应的 file 结构,然后根据 file 结构中的当前读写位置,从文件中读取指定字节数的数据,并将数据存储到应用程序提供的数据缓冲区中。在读取数据的过程中,VFS 会根据需要从磁盘中读取数据,并通过页缓存进行数据的缓存和管理,以提高数据读取的效率。 write系统调用则用于向打开的文件中写入数据。应用程序调用write系统调用,并传入文件描述符、数据缓冲区和写入的字节数等参数。VFS 根据文件描述符找到对应的 file 结构,然后将数据缓冲区中的数据写入到文件中,更新文件的内容和大小,并将 file 结构中的当前读写位置相应地移动。在写入数据时,VFS 会将数据先写入到页缓存中,然后根据一定的策略将页缓存中的数据同步到磁盘上,以确保数据的持久化存储。   文件系统的挂载与操作 一)挂载机制 挂载,是 Linux 文件系统中一个极为关键的概念,它就像是一座桥梁,将文件系统与目录树紧密地连接在一起,使得文件系统能够成为系统目录结构的一部分,从而实现对文件系统中文件和目录的访问。简单来说,挂载就是将一个存储设备(如硬盘、U 盘、光盘等)上的文件系统连接到 Linux 系统的目录树上的特定目录(即挂载点)。例如,当我们插入一个 U 盘时,需要将 U 盘的文件系统挂载到系统的某个目录下,如/media/usb,之后通过访问/media/usb目录,就可以对 U 盘中的文件进行操作。 在系统启动的过程中,自动挂载起着至关重要的作用,它确保了系统能够顺利访问各种文件系统,保障系统的正常运行。而这一过程,主要依赖于/etc/fstab文件 。/etc/fstab文件就像是一个详细的文件系统挂载配置清单,它包含了系统在启动时需要自动挂载的文件系统的各项信 息,如设备名称(如/dev/sda1)、挂载点(如/、/home等)、文件系统类型(如ext4、xfs等)以及挂载选项(如rw表示可读写、ro表示只读等)。系统启动时,会按照/etc/fstab文件中的配置,依次对各个文件系统进行挂载操作。 下面,我们以一个简单的例子来详细说明/etc/fstab文件的配置和使用方法。假设我们有一个新的硬盘分区/dev/sdb1,文件系统类型为ext4,我们希望将其挂载到/data目录下,并且在系统启动时自动挂载。那么,我们需要在/etc/fstab文件中添加如下一行配置:   /dev/sdb1 /data ext4 defaults 0 0 在这行配置中,/dev/sdb1是要挂载的设备名称,/data是挂载点,ext4是文件系统类型,defaults表示使用默认的挂载选项,最后的两个0分别表示是否使用dump命令进行备份(0表示不备份)和是否在开机时进行文件系统检查(0表示不检查)。 (二)文件和目录操作命令 在 Linux 系统中,文件和目录操作是日常使用和开发中最为频繁的操作之一,而熟练掌握常用的文件和目录操作命令,是高效使用 Linux 系统的基础。这些命令就像是我们在 Linux 世界中的得力工具,帮助我们轻松地创建、删除、移动和管理文件与目录。 touch命令:touch命令主要用于创建新的空文件,或者更新文件的时间戳。当我们需要创建一个新的文件时,只需在终端中输入touch命令,后跟要创建的文件名即可。例如,要创建一个名为test.txt的文件,可以执行touch test.txt。如果test.txt文件已经存在,touch命令会更新该文件的访问时间和修改时间。 mkdir命令:mkdir命令用于创建新的目录。它的使用方法非常简单,在终端中输入mkdir,然后跟上要创建的目录名。例如,要创建一个名为my_directory的目录,执行mkdir my_directory。如果需要创建多级目录,可以使用-p选项,例如mkdir -p parent_directory/child_directory,这样就可以一次性创建parent_directory及其子目录child_directory。 rm命令:rm命令用于删除文件或目录。当删除文件时,直接使用rm后跟文件名,如rm test.txt,即可删除test.txt文件。删除目录时,需要使用-r选项,以递归删除目录及其下的所有文件和子目录。例如,要删除my_directory目录及其所有内容,执行rm -r my_directory。需要特别注意的是,rm命令删除文件或目录后,数据通常无法恢复,因此在使用时要格外小心,避免误删重要数据。 mv命令:mv命令有两个主要功能,一是移动文件或目录,二是重命名文件或目录。当移动文件或目录时,mv命令的语法为mv source destination,其中source是要移动的文件或目录的路径,destination是目标路径。例如,要将test.txt文件移动到my_directory目录中,可以执行mv test.txt my_directory。当重命名文件或目录时,mv命令的语法为mv old_name new_name,例如,要将my_directory目录重命名为new_directory,执行mv my_directory new_directory。 (三)文件权限与所有权管理 在 Linux 文件系统中,文件权限和所有权管理是保障系统安全和数据完整性的重要机制。通过合理设置文件的权限和所有权,可以控制不同用户对文件的访问级别,确保只有授权的用户能够对文件进行相应的操作。 Linux 文件系统中定义了三种基本权限,分别是读(r)、写(w)和执行(x) 。读权限允许用户读取文件的内容,对于目录来说,读权限允许用户列出目录中的文件和子目录;写权限允许用户修改文件的内容,对于目录,写权限允许用户在目录中创建、删除和重命名文件及子目录;执行权限允许用户执行可执行文件,对于目录,执行权限允许用户进入该目录。 Linux 系统将用户分为三种类型,分别是文件所有者(owner)、所属组(group)和其他用户(others) 。文件所有者是创建文件的用户,对文件拥有最高的控制权;所属组是文件所有者所在的用户组,组内的用户可以根据设置的权限对文件进行操作;其他用户则是除了文件所有者和所属组用户之外的所有用户。 为了实现对文件权限的灵活管理,Linux 系统提供了chmod命令 。chmod命令可以用于改变文件或目录的权限。它有两种常用的使用方式,一种是使用数字表示权限,另一种是使用符号表示权限。 (四)硬链接与软链接 在 Linux 文件系统中,硬链接和软链接是两种特殊的文件链接方式,它们为用户提供了更加灵活的文件管理方式。 硬链接是指在同一个文件系统中,多个文件名指向同一个文件数据块和 inode 。这意味着,硬链接和原始文件实际上是同一个文件的不同名称,它们共享相同的文件内容和 inode 信息。当我们对硬链接进行修改时,原始文件也会相应地被修改,反之亦然。硬链接的主要特点是不能跨文件系统创建,并且不能为目录创建硬链接。例如,我们有一个文件original_file,要创建它的硬链接hard_link,可以使用以下命令:   ln original_file hard_link   软链接,也称为符号链接,类似于 Windows 系统中的快捷方式 。软链接是一个独立的文件,它包含了指向原始文件的路径信息。当我们访问软链接时,系统会根据软链接中记录的路径,找到原始文件并进行访问。软链接与原始文件拥有不同的 inode,它们是两个不同的文件。软链接的优点是可以跨文件系统创建,并且可以为目录创建软链接。例如,要创建一个指向original_file的软链接soft_link,可以使用以下命令:   ln -s original_file soft_link 硬链接和软链接在实际使用中有着不同的应用场景。硬链接常用于防止重要文件被误删,因为只要还有一个硬链接存在,文件的数据就不会丢失。而软链接则常用于方便地访问不同位置的文件或目录,或者在不同的目录结构中共享同一个文件。例如,在开发过程中,我们可能会将一些常用的配置文件创建软链接到不同的项目目录中,这样在修改配置文件时,所有相关的项目都能使用到最新的配置。          在本次对 Linux 文件系统的深度探索中,我们从其基本概念出发,深入了解了 “一切皆文件” 这一独特理念,以及基于树状结构的文件与目录体系。同时,也认识了 EXT4、XFS、Btrfs 等多种常见文件系统类型,它们各自凭借出色的稳定性、高性能和丰富的高级特性,在不同的应用场景中发挥着关键作用。 文件系统的核心要素,如 inode、dentry、逻辑块和超级块,是支撑文件系统高效运行的关键。inode 记录着文件的元数据,是文件的 “身份标识”;dentry 构建了文件的逻辑结构,方便用户和系统进行文件查找;逻辑块作为数据存储的基本单位,合理的大小设置能显著提升文件系统的性能;超级块则存储着文件系统的全局信息,是文件系统的 “控制中心”。 虚拟文件系统(VFS)作为 Linux 文件系统的核心架构,为不同的文件系统提供了统一的抽象接口,使得用户和应用程序能够以一致的方式访问和操作各种文件系统。通过超级块对象、索引节点对象、文件对象和目录项对象等数据结构,VFS 实现了对文件系统的高效管理和调度。同时,VFS 提供的系统调用,如open、read、write等,为应用程序与文件系统的交互提供了便捷的途径。     在文件系统的操作与管理方面,我们学习了挂载与卸载机制,以及touch、mkdir、rm、mv等常用的文件和目录操作命令。同时,深入了解了文件权限与所有权管理,以及硬链接和软链接的概念与应用。这些知识和技能对于系统管理员和开发者来说,是进行系统维护和开发的必备工具。 此外,我们还探讨了文件系统的高级特性,如磁盘配额、文件系统检查与修复、日志文件系统等。这些特性为文件系统的性能优化、数据安全和可靠性提供了有力保障。在实际应用中,合理运用这些高级特性,能够显著提升文件系统的运行效率和稳定性。 最后,我们介绍了fdisk、parted、mkfs、mount、umount、du、df等多种 Linux 文件系统管理工具,以及文件系统性能优化策略和常见问题的诊断与解决方法。这些工具和方法能够帮助我们更好地管理和维护文件系统,确保系统的高效运行。    

  • 发表了主题帖: 《奔跑吧Linux内核1:基础架构》--04--进程管理

            在 Linux 系统的广袤天地里,进程堪称最为关键的角色之一,特别是在嵌入式软件领域,进程的重要性更是不言而喻。进程,简单来说,就是正在执行的程序实例。它如同现实生活中的各种任务,具有独立的执行环境和资源。想象一下,你的电脑就像一个繁忙的工厂,而进程则是工厂里的不同生产线,每个生产线都在执行特定的任务,有的负责处理文档,有的负责播放音乐,还有的负责网络通信。这些生产线(进程)相互协作,共同维持着工厂(系统)的正常运转。在嵌入式系统中,进程同样承担着至关重要的职责。以智能手环为例,它内部运行着多个进程,如心率监测进程实时采集用户的心率数据,运动追踪进程记录用户的运动步数和轨迹,显示驱动进程则负责将这些数据展示在手环屏幕上。这些进程协同工作,使得智能手环能够为用户提供各种便捷的功能。进程不仅是程序执行的载体,还拥有自己独立的地址空间、寄存器和堆栈等资源。这意味着不同进程之间相互隔离,一个进程的错误通常不会影响到其他进程的正常运行。就好比工厂里的各个生产线,它们各自独立运作,一条生产线出现故障,不会波及到其他生产线的生产。这种独立性和隔离性,为系统的稳定性和可靠性提供了有力保障。   (一)进程诞生记     在 Linux 系统中,进程的创建是通过系统调用实现的,其中最常用的系统调用便是fork。fork函数就像是一个神奇的 “分身术”,它能从已存在的进程中创建一个新的进程,新进程被称为子进程,而原来的进程则是父进程。从原理上讲,当一个进程调用fork时,系统会进行一系列复杂而有序的操作。首先,内核会为子进程分配新的内存块和内核数据结构。这就好比为新出生的 “小生命” 准备好独立的 “生活空间” 和 “成长手册”。然后,内核将父进程的部分数据结构内容拷贝至子进程,就像是把父进程的 “经验” 和 “知识” 复制给子进程。接着,子进程被添加到系统进程列表当中,正式成为系统中的一员。最后,fork返回,开始调度器调度,决定父子进程谁先执行。 fork函数的返回值非常独特,它会返回两次。在子进程中,返回值为 0,这就像是给子进程发放了一张 “身份标识卡”,表明它是子进程;而在父进程中,返回值是子进程的 ID,方便父进程识别和管理自己的 “孩子”。fork有着广泛的应用场景。比如在网络服务器中,当有客户端请求到来时,父进程可以通过fork创建子进程来处理请求,这样父进程就可以继续监听其他请求,提高系统的并发处理能力。又比如在一些需要并行处理任务的场景中,一个进程可以通过fork创建多个子进程,分别执行不同的任务,从而提高整体的处理效率。 除了fork,Linux 还提供了vfork和clone等系统调用用于创建进程。vfork与fork类似,但它有一些特殊之处。vfork创建子进程时,不会将父进程的地址空间复制到子进程,而是让子进程和父进程共享地址空间,直到子进程调用exec或exit。这就像是子进程和父进程暂时住在同一个 “房间” 里,等子进程有了自己的 “新家”(调用exec执行新程序)或者离开(调用exit)时,才会分开。clone则更加灵活,它可以通过参数指定子进程与父进程共享的资源,比如文件描述符、信号处理函数等,就像是一个定制化的进程创建工具,可以根据不同的需求创建出不同 “特性” 的子进程。 (二)进程的谢幕         进程如同世间万物一样,有诞生就有终结。进程的终止分为正常终止和异常终止两种情况。正常终止通常有以下几种方式:一是从main函数返回,当main函数执行完毕并返回时,进程也就正常结束了,这就像是一场演出顺利落幕,演员们完成表演后有序退场。二是调用exit函数,exit是标准库函数,定义在<stdlib.h>中。当调用exit时,它会在终止程序之前进行一系列清理工作,比如调用通过atexit注册的清理函数,这些函数就像是演出结束后的 “善后人员”,负责完成一些收尾工作;刷新标准 I/O 缓冲区,确保输出流中的内容被写入到文件或终端,避免数据丢失;关闭所有已打开的文件,保证数据的完整性。三是调用_exit或_Exit函数,它们是系统调用,定义在<unistd.h>中,与exit不同的是,它们会直接使程序立即终止,不会执行任何清理工作,就像是演出突然中断,演员们直接离场,不做任何后续处理 。异常终止则通常是由于调用abort函数,或者进程接到了一些未处理的信号,如SIGSEGV(段错误)、SIGFPE(浮点运算错误)等。abort函数用于异常终止进程,它会向系统发送SIGABRT信号,导致进程异常终止,就像是演出过程中突然发生意外状况,不得不紧急中断。当进程终止时,内核会进行一系列资源回收操作。其中,wait和waitpid函数起着关键作用。wait函数是一个系统调用,定义在<sys/wait.h>中,其主要功能是让父进程等待任意一个子进程结束。当调用wait时,父进程会暂停执行,进入阻塞状态,直到有一个子进程结束。wait返回结束子进程的 PID,同时将子进程的退出状态存储在提供的指针参数中。     进程调度:CPU 资源的分配艺术 (一)调度器与调度策略       在 Linux 系统中,进程调度器就像是一位繁忙的交通警察,负责管理 CPU 资源的分配,决定哪个进程可以在 CPU 上运行。它的主要作用是确保系统的高效运行和公平性,使各个进程都能合理地获得 CPU 时间。Linux 内核中最为重要的调度器之一是完全公平调度器(CFS,Completely Fair Scheduler)。CFS 主要负责普通进程的调度,它的设计理念是为每个进程提供公平的 CPU 时间分配。CFS 的核心思想是基于虚拟运行时间(vruntime)。每个进程都有一个自己的 vruntime,它代表了该进程在 CPU 上的虚拟运行时间。CFS 会优先调度 vruntime 最小的进程,因为它认为这个进程获得的 CPU 时间相对较少,需要给予更多的运行机会。例如,在一个多进程的系统中,进程 A 和进程 B 同时运行,进程 A 的 vruntime 增长速度较慢,说明它之前获得的 CPU 时间较少,那么 CFS 就会优先调度进程 A,让它在 CPU 上运行一段时间,以保证公平性。        除了 CFS,Linux 还支持实时调度策略,主要包括 SCHED_FIFO(先进先出调度)和 SCHED_RR(时间片轮转调度)。SCHED_FIFO 适用于对时间要求严格、需要立即执行的实时任务。一旦一个 SCHED_FIFO 级的进程处于可执行状态,它就会一直执行,直到它自己受阻塞或显式地释放处理器为止。例如,在工业自动化控制系统中,一些控制设备的实时任务需要快速响应,SCHED_FIFO 调度策略就能确保这些任务优先执行,避免因延迟而导致设备故障。        SCHED_RR 则与 SCHED_FIFO 大体相同,只是 SCHED_RR 级的进程在耗尽事先分配给它的时间片后就不能再继续执行了。这是一种实时轮流调度算法,当 SCHED_RR 任务耗尽它的时间片时,同一优先级的其他实时进程会被轮流调度。例如,在多媒体播放系统中,音频和视频的解码任务可以使用 SCHED_RR 调度策略,保证它们能够按照一定的时间顺序依次执行,从而实现流畅的播放效果。        不同的调度策略适用于不同的场景。CFS 适用于大多数普通的交互式和批处理任务,它能在保证公平性的前提下,提高系统的整体性能。而实时调度策略则适用于对时间要求苛刻的实时任务,如工业控制、航空航天等领域,确保关键任务能够及时得到处理 。 (二)进程优先级与权重        进程优先级和权重是影响进程调度决策的重要因素。在 Linux 系统中,每个进程都有一个优先级,它决定了进程在系统中使用 CPU 资源的相对顺序。优先级越高的进程,越有可能被调度器优先选中执行。进程优先级可以分为静态优先级和动态优先级。静态优先级是由进程的 nice 值确定的,nice 值的范围通常在 - 20 到 + 19 之间,其中 - 20 表示最高优先级,+19 表示最低优先级。较低的 nice 值表示更高的优先级。动态优先级则会考虑进程最近的行为,例如 CPU 使用情况等。如果一个进程长时间占用 CPU,它的动态优先级可能会降低,以便让其他进程也有机会执行。权重与优先级密切相关,它用于确保每个进程都能公平地获得 CPU 时间。权重通常由进程的 nice 值决定,nice 值越低,权重越高,进程获得的 CPU 时间份额就越大。例如,一个 nice 值为 - 5 的进程,其权重比 nice 值为 5 的进程要高,在调度时会获得更多的 CPU 时间。        在实际应用中,可以通过一些命令来调整进程优先级。renice命令可以用于调整运行中进程的 nice 值,从而改变其优先级。例如,renice +5 PID将增加指定进程的 nice 值,使其优先级降低;renice -5 PID则会降低指定进程的 nice 值,提高其优先级。对于实时进程,可以使用chrt命令来设置实时优先级和调度策略。例如,chrt -f 99 PID将把指定进程设置为 SCHED_FIFO 调度策略,并将其实时优先级设置为 99,这是一个非常高的优先级,该进程将在系统中具有很强的抢占能力 。 (三)就绪队列与进程切换        就绪队列是进程调度中的一个重要数据结构,它就像是一个等待上车的乘客队列,存储着所有准备好运行的进程。当一个进程被创建后,它会被加入到就绪队列中;当进程正在运行时,如果它被其他更高优先级的进程抢占,或者它的时间片用完,它也会被放回就绪队列。在 Linux 内核中,就绪队列通常使用允许高效插入和删除进程的数据结构来表示,常见的是链表或红黑树。以链表为例,每个进程控制块(PCB)中都有一个指针,指向下一个进程的 PCB,这样就形成了一个链表。当有新进程加入就绪队列时,只需要将其 PCB 插入到链表的合适位置即可;当进程被调度执行时,从链表中取出相应的 PCB。当调度器决定切换进程时,就会发生进程切换。进程切换的核心是上下文切换,这是一个复杂而精细的过程。首先,需要保存当前进程的上下文信息,包括 CPU 寄存器的值,这些寄存器记录了进程运行时的各种状态和数据,如程序计数器(PC)、通用寄存器等。保存这些寄存器的值,就像是为当前进程的运行状态拍了一张 “快照”,以便下次恢复运行时能够继续从上次的位置开始。接着,需要保存内存映射信息。每个进程都有自己独立的地址空间,内存映射信息定义了进程的虚拟地址如何映射到物理内存上。保存内存映射信息,确保在进程切换后,新进程能够正确地访问自己的内存空间。         然后,调度器会从就绪队列中选择下一个要执行的进程,并加载该进程的上下文信息。这包括恢复新进程的 CPU 寄存器值,将程序计数器设置为新进程的起始地址,使其能够从正确的位置开始执行。同时,加载新进程的内存映射信息,让新进程能够访问自己的内存资源。上下文切换的过程需要消耗一定的时间和资源,因为保存和恢复寄存器、内存映射等操作都需要进行内存读写和 CPU 指令执行。因此,在设计调度算法时,需要尽量减少不必要的上下文切换,以提高系统的性能。例如,通过合理调整进程优先级和调度策略,让重要的进程能够连     在 Linux 系统中,进程并不是孤立存在的,它们之间常常需要进行通信和协作,以完成各种复杂的任务。进程间通信(IPC,Inter-Process Communication)就像是进程之间的 “桥梁”,使得不同进程能够交换数据、传递信息,从而实现系统的整体功能。在嵌入式软件中,进程间通信尤为重要,它可以协调不同功能模块之间的工作,提高系统的性能和稳定性。下面将介绍几种常见的进程间通信方式:信号、管道、共享内存和消息队列。 (一)信号:简单的通知         信号是一种简单的进程间通信方式,它可以用于通知进程发生了某种特定事件。信号就像是一个简短的 “消息”,当某个事件发生时,系统会向相应的进程发送信号,进程接收到信号后,可以根据预先设定的处理方式来做出响应。在 Linux 系统中,有许多常见的信号,每个信号都有一个对应的编号和名称。例如,SIGINT(信号编号为 2)是用户通过按下Ctrl+C组合键发送的中断信号,常用于终止正在运行的进程。当我们在终端中运行一个程序时,如果按下Ctrl+C,系统就会向该程序对应的进程发送SIGINT信号,进程收到信号后,默认会终止运行。SIGKILL(信号编号为 9)是一个强制终止进程的信号,它不能被捕获或忽略,一旦进程收到SIGKILL信号,就会立即被终止。这在某些情况下非常有用,比如当一个进程出现异常,无法正常结束时,可以使用kill -9命令发送SIGKILL信号来强制终止它 。发送信号的方式有多种。可以使用kill命令向进程发送信号,例如kill -SIGINT PID,其中PID是目标进程的 ID,这条命令会向指定 ID 的进程发送SIGINT信号。在程序中,可以使用kill函数来发送信号,其函数原型为int kill(pid_t pid, int sig);,其中pid是目标进程的 ID,sig是要发送的信号编号。 进程处理信号的方式有三种:一是忽略信号,即进程对收到的信号不做任何处理,就像没有收到信号一样。二是执行默认操作,每个信号都有一个默认的处理方式,例如SIGINT的默认操作是终止进程,SIGQUIT的默认操作是终止进程并生成核心转储文件。三是捕获信号,进程可以通过注册信号处理函数来捕获信号,并在信号处理函数中执行自定义的操作。在 C 语言中,可以使用signal函数来注册信号处理函数, (二)管道:单向的信息流      管道是一种单向的进程间通信方式,它允许一个进程将数据发送给另一个进程。管道就像是一条 “单向通道”,数据只能从一端写入,从另一端读出。管道分为匿名管道和命名管道。匿名管道是一种临时的通信机制,它只能在具有亲缘关系(如父子进程)的进程之间使用。匿名管道的创建非常简单,在 Linux 系统中,可以使用pipe函数来创建匿名管道,其函数原型为int pipe(int pipefd[2]);,该函数会创建一个管道,并返回两个文件描述符,pipefd[0]用于读取管道数据,pipefd[1]用于写入管道数据。在这个例子中,首先使用pipe函数创建了一个匿名管道,然后通过fork函数创建了子进程。在子进程中,关闭管道的写端,从读端读取数据并打印;在父进程中,关闭管道的读端,向写端写入数据,然后等待子进程结束。          命名管道则克服了匿名管道只能在亲缘关系进程间使用的限制,它可以在任意两个进程之间进行通信。命名管道在文件系统中以文件的形式存在,使用mkfifo函数来创建,函数原型为int mkfifo(const char *pathname, mode_t mode);,其中pathname是命名管道的路径名,mode是文件权限。创建好命名管道后,进程可以像操作普通文件一样打开、读写命名管道。 管道的优点是简单易用,在处理一些简单的数据流传递时非常方便。在 Linux 系统的命令行中,经常使用管道来连接多个命令,如ls | grep.txt,将ls命令的输出作为grep命令的输入,实现文件过滤功能。然而,管道也有一些缺点,它的通信是单向的,数据只能从一端流向另一端;而且管道的缓冲区大小有限,如果写入的数据量超过缓冲区大小,写入操作可能会阻塞,直到有数据被读取 。 (三)共享内存:高效的数据共享       共享内存是一种高效的进程间通信方式,它允许多个进程共享同一块物理内存区域,从而实现数据的快速交换和共享。共享内存就像是一个公共的 “数据仓库”,多个进程可以直接访问和修改其中的数据。共享内存的原理是,多个进程将同一块物理内存映射到各自的虚拟地址空间中。这样,当一个进程对共享内存中的数据进行修改时,其他进程可以立即看到这些变化,因为它们访问的是同一块物理内存。在 Linux 系统中,使用shmget函数来创建共享内存段,函数原型为int shmget(key_t key, size_t size, int shmflg);,其中key是共享内存的键值,用于标识共享内存段,size是共享内存的大小,shmflg是标志位,用于指定创建方式和权限。创建好共享内存段后,使用shmat函数将共享内存段映射到进程的地址空间中,函数原型为void *shmat(int shmid, const void *shmaddr, int shmflg);,其中shmid是共享内存段的 ID,shmaddr是指定的映射地址,通常设为NULL表示由系统自动分配,shmflg是标志位。 使用共享内存进行进程间通信时,需要注意同步问题,因为多个进程可能同时访问和修改共享内存中的数据,如果不加控制,就会导致数据不一致的问题。为了解决这个问题,可以使用互斥锁、信号量等同步机制。互斥锁是一种简单的同步工具,它就像是一把 “锁”,一次只能允许一个进程进入临界区(即访问共享内存的代码段)。在 C 语言中,可以使用pthread_mutex_t类型的变量来创建互斥锁,使用pthread_mutex_init函数初始化互斥锁,使用pthread_mutex_lock函数加锁,使用pthread_mutex_unlock函数解锁。 (四)消息队列:消息的传递 消息队列是一种进程间通信方式,它允许进程以消息的形式发送和接收数据。消息队列就像是一个 “信箱”,进程可以将消息放入信箱中,其他进程可以从信箱中取出消息。 消息队列的使用相对简单,在 Linux 系统中,使用msgget函数创建消息队列,函数原型为int msgget(key_t key, int msgflg);,其中key是消息队列的键值,msgflg是标志位。创建好消息队列后,使用msgsnd函数发送消息,函数原型为int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);,其中msqid是消息队列的 ID,msgp是指向消息结构体的指针,msgsz是消息的大小,msgflg是标志位。使用msgrcv函数接收消息,函数原型为ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);,其中msgtyp是指定接收的消息类型 。 在嵌入式软件中,消息队列常用于任务之间的通信和协作。在一个智能家居控制系统中,不同的任务(如温度监测任务、灯光控制任务、窗帘控制任务等)可以通过消息队列来传递控制指令和状态信息。当温度监测任务检测到室内温度过高时,可以向空调控制任务发送一条消息,通知它开启制冷模式;空调控制任务收到消息后,执行相应的操作,并将操作结果通过消息队列反馈给温度监测任务。这样,各个任务之间通过消息队列实现了有效的通信和协作,共同完成智能家居系统的功能。消息队列的优势在于它可以实现不同进程之间的异步通信,提高系统的响应速度和并发处理能力 。           进程管理作为 Linux 内核的核心功能之一,在嵌入式软件领域扮演着举足轻重的角色。从进程的创建与终止,到进程调度、进程间通信,每一个环节都紧密关联,共同构建起一个稳定、高效的系统运行环境。通过对《奔跑吧 Linux 内核 1:基础架构》中进程管理内容的深入探讨,我们不仅了解了进程管理的基本概念和原理,还通过实际案例分析,掌握了如何在嵌入式软件中应用进程管理技术来解决实际问题。随着嵌入式技术的不断发展,未来进程管理技术也将面临新的挑战和机遇。一方面,随着物联网、人工智能等技术的兴起,嵌入式系统的应用场景越来越复杂,对进程管理的实时性、可靠性和安全性提出了更高的要求。另一方面,硬件技术的不断进步,如多核处理器的广泛应用,也为进程管理技术的发展提供了新的契机。未来,我们可以期待更加智能、高效的进程调度算法,更加安全、可靠的进程间通信机制,以及更加便捷、灵活的进程管理工具的出现。对于嵌入式软件开发者来说,深入学习和掌握进程管理技术是提升自身技能的关键。希望本文能为大家提供一个学习和探索进程管理的起点,鼓励大家在实际项目中不断实践和创新,将进程管理技术运用得更加娴熟,为嵌入式软件的发展贡献自己的力量。  

  • 2025-02-21
  • 回复了主题帖: 颁奖:验证并选择心仪MOSFET,探寻选型奥秘!注册、体验双重好礼等你拿~

    确认个人信息无误

  • 2025-02-06
  • 发表了主题帖: 奔跑吧Linux内核1:基础架构》-03-物理内存与虚拟内存

    在 Linux 系统的庞大体系中,内存管理无疑是最为关键的部分之一,它就像是整个系统的基石,支撑着系统的稳定运行与高效工作。内存管理肩负着多项重任,比如合理地分配内存资源,确保各个进程都能获得所需的内存空间,同时还要高效地回收不再使用的内存,避免内存的浪费。此外,它还得对内存进行有效的保护,防止进程之间非法访问内存,从而保障系统的安全性和稳定性。要是内存管理出现了问题,整个系统就可能陷入混乱,出现各种异常情况,比如进程崩溃、系统死机等。 物理内存的本质     物理内存,是计算机硬件实实在在提供的内存,是我们可以在电脑中直接看到的内存条所提供的内存空间。它就像是计算机的 “临时仓库”,在计算机运行时,为操作系统和各种程序提供临时的存储场所 ,程序运行时的代码和数据都临时存储在这个仓库中。当我们打开一个应用程序,如浏览器时,浏览器的程序代码以及我们浏览网页时产生的数据,就会被加载到物理内存中,这样 CPU 就可以快速地读取和处理这些数据,让我们能够流畅地浏览网页。一旦计算机关机,物理内存中存储的数据就会全部丢失,因为它只是临时存储数据,并不具备长期保存数据的能力。   物理内存的工作方式     为了更高效地管理和使用物理内存,操作系统采用了分页机制。简单来说,分页就是把物理内存划分成一个个固定大小的小块,每一块被称为一个页。在 Linux 系统中,常见的页大小是 4KB。这样做的好处是可以更灵活地分配内存,提高内存的利用率。每一个物理页都有一个唯一的编号,叫做物理页编号(PFN,Page Frame Number)。通过这个编号,操作系统可以快速地定位和管理每一个物理页。内核中使用一个全局数组 mem_map 来管理物理内存页,数组的下标就是 PFN,通过这种方式,操作系统可以方便地对物理内存进行分配、回收和管理。 当一个进程需要访问内存时,它会向操作系统请求内存。操作系统会根据进程的需求,从空闲的物理页中选择合适的页分配给进程。例如,一个新启动的进程需要 100KB 的内存,操作系统会将 25 个 4KB 的物理页分配给这个进程,进程就可以在这些分配到的物理页上存储和处理数据。 尽管物理内存是计算机运行不可或缺的部分,但它的容量是有限的。在 32 位的系统中,由于地址总线的限制,最多只能访问 4GB 的物理内存。而即使在 64 位系统中,虽然理论上可以支持非常大的内存容量,但受到硬件和成本等因素的限制,实际安装的物理内存也往往是有限的。     在多进程并发运行的场景下,物理内存的不足问题就会更加凸显。例如,当我们同时打开多个大型应用程序,如游戏、视频编辑软件和浏览器等,这些程序都需要占用大量的物理内存。如果物理内存不够,就会导致系统运行缓慢,甚至出现卡顿、死机等情况。在处理大数据量的任务时,如大数据分析、科学计算等,也可能会因为物理内存不足而无法完成任务。 所以,为了解决物理内存不足的问题,虚拟内存应运而生。   虚拟内存的概念 虚拟内存是一种内存管理技术,它就像是计算机内存管理中的 “魔法”,通过利用磁盘空间来扩展物理内存。在计算机运行过程中,当物理内存不够用时,操作系统会把一部分暂时不用的数据从物理内存移动到磁盘上的交换空间中,等需要使用这些数据时,再把它们从磁盘交换回物理内存。这样一来,每个进程都仿佛拥有了一个连续的、很大的内存空间,就像每个进程都有自己独立的 “大仓库”,可以自由地存储和访问数据,而不必担心物理内存的实际大小限制。 虚拟内存的出现,巧妙地解决了物理内存不足的问题,让计算机可以运行比物理内存大得多的程序。比如,在运行大型游戏时,游戏程序的代码和数据量可能远远超过了物理内存的大小,但借助虚拟内存技术,游戏依然可以流畅运行。同时,虚拟内存还解决了多进程环境下的地址冲突问题,每个进程都有自己独立的虚拟地址空间,它们之间互不干扰,保证了系统的稳定性和安全性。 虚拟内存的工作原理 地址空间:在 Linux 系统中,每个进程都拥有一个独立的虚拟地址空间,这个空间就像是进程的 “私人领地”,进程可以在其中自由地访问和管理内存。在 32 位系统中,虚拟地址空间的大小通常为 4GB,而在 64 位系统中,虚拟地址空间更是大得惊人,达到了 16EB(1EB = 1024PB,1PB = 1024TB ,1TB = 1024GB )。这个巨大的虚拟地址空间为进程提供了广阔的内存使用范围,使得进程可以轻松地处理大规模的数据和复杂的任务。 分页机制:为了实现虚拟内存,操作系统采用了分页机制。虚拟内存和物理内存都被划分为固定大小的页,在 Linux 系统中,常见的页大小也是 4KB。每个虚拟页都可以映射到一个物理页上,这种映射关系就像是一把钥匙对应一把锁,通过这种对应关系,操作系统可以将虚拟地址转换为物理地址,从而实现进程对物理内存的访问。 页表:页表是虚拟内存管理中非常重要的数据结构,它就像是一本 “地址字典”,存储着虚拟页到物理页的映射关系。每个进程都有自己的页表,页表中的每一个条目被称为页表项(PTE,Page Table Entry)。页表项记录了虚拟页对应的物理页的物理页号、访问权限、修改位等信息。通过页表,操作系统可以快速地找到虚拟页对应的物理页,实现虚拟地址到物理地址的转换。在 32 位系统中,由于虚拟地址空间为 4GB,页大小为 4KB,所以需要 1024 * 1024 个页表项来映射整个虚拟地址空间,每个页表项通常占用 4 个字节,因此整个页表的大小为 4MB。在 64 位系统中,由于虚拟地址空间更大,页表的结构和管理更加复杂,可能会采用多级页表来减少页表占用的内存空间。 内存交换:当物理内存不足时,操作系统会将一些不活跃的进程或者内存页移动到磁盘上的交换空间中,这个过程就像是把暂时不用的物品从仓库的显眼位置搬到仓库的角落里存放起来。当这些被交换出去的进程或内存页需要再次被访问时,操作系统会将它们从交换空间重新加载回物理内存中。在这个过程中,操作系统会根据一定的算法来选择需要交换出去的内存页,比如最近最少使用(LRU,Least Recently Used)算法,该算法会优先选择那些最近最少被访问的内存页进行交换,以保证物理内存中存放的都是最常用的数据,提高系统的运行效率。 TLB 缓存:为了加快虚拟地址到物理地址的转换速度,CPU 中引入了 TLB(Translation Lookaside Buffer)缓存,它就像是一个 “快速查询助手”。TLB 缓存中存储了最近使用的页表项,当 CPU 需要进行地址转换时,会首先在 TLB 中查找对应的页表项,如果找到了,就可以直接获取物理地址,大大提高了地址转换的速度。如果在 TLB 中没有找到对应的页表项,才会去内存中查找页表,这个过程会相对较慢。由于 TLB 的容量有限,所以操作系统会不断地更新 TLB 中的内容,以保证 TLB 中存储的都是最常用的页表项。 多级页表:在 64 位系统中,由于虚拟地址空间非常大,如果采用单级页表,页表的大小会非常巨大,占用大量的物理内存。为了解决这个问题,操作系统采用了多级页表。以 Linux 系统为例,通常采用三级页表。虚拟地址会被分成多个部分,每个部分用于索引不同级别的页表。通过这种方式,可以大大减少每个进程页表的大小,提高内存管理的效率。比如,在一个采用三级页表的系统中,虚拟地址的高 10 位用于索引一级页表,中间 10 位用于索引二级页表,低 12 位用于表示页内偏移。通过这种分级索引的方式,可以快速地找到虚拟页对应的物理页,同时减少了页表占用的内存空间 。 虚拟内存与物理内存的映射关系 在 Linux 内核中,虚拟内存与物理内存之间的映射关系是通过页表来实现的。页表是一个非常关键的数据结构,它就像是一座桥梁,连接着虚拟内存和物理内存。在 32 位系统中,虚拟地址空间通常为 4GB,而页大小一般为 4KB,这就意味着需要 1024 * 1024 个页表项来完成整个虚拟地址空间的映射。每个页表项记录了虚拟页到物理页的映射信息,以及一些控制位,如访问权限、是否被修改等。通过这些信息,操作系统可以准确地将虚拟地址转换为物理地址,同时还能对内存访问进行有效的控制和管理 。 以一个简单的例子来说明,假设我们有一个进程需要访问虚拟地址 0x12345678。首先,系统会根据虚拟地址的高 20 位(在 32 位系统中,通常将虚拟地址分为高 20 位的页号和低 12 位的页内偏移),在页表中查找对应的页表项。如果找到了对应的页表项,就可以从页表项中获取到该虚拟页对应的物理页号。然后,将物理页号与虚拟地址的低 12 位(页内偏移)相结合,就可以得到最终的物理地址。例如,假设通过查找页表,得到物理页号为 0x987654,那么最终的物理地址就是 0x987654000 + 0x5678 = 0x9876545678 。         物理内存和虚拟内存是 Linux 内存管理中不可或缺的两个部分,它们相互协作,为系统的稳定运行和高效性能提供了坚实的保障。物理内存作为计算机硬件提供的实际内存,直接存储着正在运行的程序和数据,其访问速度快,是计算机运行的关键支撑。而虚拟内存则通过巧妙的技术手段,利用磁盘空间扩展了内存容量,解决了物理内存不足的问题,同时为每个进程提供了独立的地址空间,增强了系统的安全性和稳定性。     随着计算机技术的不断发展,内存管理技术也在持续演进。未来,我们有望看到更加高效的内存管理算法和技术的出现。例如,新兴的内存类型如 MRAM(磁阻随机存取存储器)、FRAM(铁电随机存取存储器)等,它们在速度、功耗和容量等方面展现出了优异的特性,有可能为内存管理带来新的突破。人工智能和机器学习技术也可能被应用到内存管理中,使系统能够根据应用程序的运行模式和内存使用习惯,更加智能地分配和管理内存资源,进一步提升系统的性能和效率。对于 Linux 内存管理的研究和探索,将不断推动计算机技术的发展,为我们带来更加高效、强大的计算体验。    

  • 2025-01-25
  • 发表了主题帖: 《奔跑吧Linux内核1:基础架构》-02-ARM64在Linux内核的实现与内存管理

    一、ARM64 架构与 Linux 内核的邂逅 在当今数字化时代,计算技术飞速发展,各种架构与系统不断涌现,ARM64 架构凭借其独特优势,在众多领域崭露头角。它是 ARM 架构的 64 位版本,相比传统的 32 位架构,有着显著的提升,如更大的内存寻址空间,理论上可支持高达 2^64 字节的内存 ,这为处理大规模数据和复杂应用提供了坚实基础。同时,ARM64 在能效比上表现出色,其精简指令集(RISC)设计,让处理器在执行指令时更高效,能耗更低,这使得它在移动设备、嵌入式系统中备受青睐。从智能手机到物联网设备,ARM64 架构无处不在,成为推动这些领域发展的重要力量。 Linux 内核作为开源操作系统的核心,以其高度的可定制性、稳定性和强大的功能,在服务器、嵌入式系统等众多场景广泛应用。它犹如一个庞大而精密的机器,协调着计算机硬件与软件之间的交互。而 ARM64 架构与 Linux 内核的结合,更是碰撞出了耀眼的火花。研究 ARM64 在 Linux 内核中的实现及内存管理,有着多方面的重要意义。从技术发展角度看,这有助于深入理解现代计算机系统底层原理,推动计算机体系结构的发展。随着物联网、人工智能等新兴技术的兴起,对计算设备的性能和能效提出了更高要求,ARM64 与 Linux 内核的优化结合,能更好地满足这些需求。在实际应用中,对于开发人员来说,掌握这方面知识,能够更高效地进行基于 ARM64 平台的软件开发与系统优化,提高产品性能和稳定性 。无论是开发高性能的服务器应用,还是低功耗的嵌入式设备程序,都离不开对 ARM64 在 Linux 内核实现及内存管理的深入理解。 二、ARM64 在 Linux 内核的实现   (一)ARM64 架构特点 ARM64 架构基于 ARMv8 指令集,拥有诸多独特优势。其最显著的特点便是 64 位寻址能力,这意味着它能够访问远超 32 位架构的内存空间,为运行大规模数据处理和复杂应用程序提供了广阔的内存空间。例如,在处理大型数据库或进行深度学习模型训练时,充足的内存寻址范围能确保数据的高效读取与存储,避免因内存不足导致的性能瓶颈。 在寄存器组方面,ARM64 拥有 31 个 64 位的通用寄存器,相比 ARM32 增加了不少。这些寄存器在数据处理和运算中发挥着关键作用,能够快速存储和读取数据,减少对内存的访问次数,从而提高了处理器的执行效率。以矩阵运算为例,多个通用寄存器可以并行存储矩阵元素,加速运算过程。 ARM64 的指令集也进行了优化,引入了新的指令,如高级 SIMD(NEON)指令集,这极大地增强了其在多媒体处理、信号处理等领域的能力。在处理高清视频解码时,NEON 指令集可以并行处理多个数据元素,快速完成视频的解码工作,为用户带来流畅的观看体验。 (二)Linux 内核适配 ARM64 的关键改动 为了使 Linux 内核能够在 ARM64 架构上高效运行,开发者们对内核进行了一系列关键改动。在中断处理方面,ARM64 采用了全新的通用中断控制器(GIC)架构,如 GICv3 和 GICv4。这些新架构支持更多的中断源和更高的中断处理效率,能够更好地满足现代多核处理器的需求。在多核 ARM64 处理器中,GIC 可以将中断均匀地分配到各个核心上,避免某个核心因中断过多而负载过重,确保系统的整体性能稳定。 Linux 内核针对 ARM64 的中断处理流程也进行了优化。当中断发生时,内核能够快速地响应并跳转到相应的中断处理函数,减少中断响应时间。在处理网络中断时,内核可以迅速将网络数据包从硬件缓冲区读取到内存中,并进行后续的处理,保证网络通信的及时性。 在系统调用方面,ARM64 有着独特的实现方式。系统调用是用户态程序与内核态交互的重要途径,ARM64 通过特定的寄存器传递系统调用参数,如 x0 - x7 寄存器用于传递前 8 个参数,x8 寄存器用于存放系统调用号。这种方式简洁高效,能够快速地实现用户态到内核态的切换。当用户态程序调用 open 函数打开文件时,通过这些寄存器传递文件名、打开模式等参数,内核接收到这些参数后,执行相应的文件打开操作。 (三)编译与构建:ARM64 内核之旅 在 Linux 环境下编译和构建适用于 ARM64 架构的内核,需要精心准备。首先是工具链的准备,常用的工具链如 Linaro 工具链,它提供了一套完整的编译工具,包括编译器、链接器等。可以从 Linaro 官方网站下载适合的工具链版本,下载完成后,解压工具链文件,并将其 bin 目录添加到系统的 PATH 环境变量中,确保系统能够找到相关的编译工具。 接着是配置选项的设置,这一步至关重要。进入内核源代码目录,执行 “make ARCH=arm64 defconfig” 命令,该命令会加载 ARM64 架构的默认配置。如果需要定制内核功能,如添加特定的驱动支持、启用或禁用某些内核特性,可以执行 “make ARCH=arm64 menuconfig” 命令,进入图形化配置界面。在这个界面中,可以像在菜单中选择菜品一样,根据项目需求勾选或取消相应的选项。如果要添加对 USB 设备的驱动支持,在 “Device Drivers” 选项中找到 “USB support” 并勾选;如果要禁用不必要的调试功能以减小内核体积,在 “Kernel hacking” 选项中取消相关调试选项的勾选。 完成配置后,就可以开始编译内核了。执行 “make -jN” 命令,其中 N 表示并行编译的线程数,一般根据计算机的 CPU 核心数来设置,例如 “make -j8” 表示使用 8 个线程并行编译,这样可以大大缩短编译时间。编译过程中,系统会输出详细的编译信息,显示各个模块的编译进度和结果。当编译完成后,在 arch/arm64/boot 目录下会生成 Image 文件,这就是编译好的 ARM64 内核镜像,它就像一个精心打造的 “武器”,等待着在 ARM64 架构的设备上发挥作用。 三、ARM64 在 Linux 内核中的内存管理 (一)内存布局探秘 在 ARM64 架构下,Linux 内核的内存布局犹如一幅精心绘制的蓝图,严谨而有序。ARM64 架构的物理地址拥有 48 位,理论上最大寻址空间可达 256TB ,虚拟地址同样最大支持 48 位寻址。Linux 内核巧妙地将这庞大的地址空间划分为内核空间和用户空间。 用户空间的寻址范围是从 0x0000_0000_0000_0000 至 0x0000_ffff_ffff_ffff,这里是用户进程的活动天地。各类应用程序,如手机上的游戏、办公软件等,在运行时所占用的内存都处于用户空间。当我们在手机上打开一款游戏时,游戏的代码、数据以及运行过程中产生的临时变量等,都存储在用户空间的内存中。 内核空间则从 0xffff_0000_0000_0000 延伸至 0xffff_ffff_ffff_ffff,它是操作系统内核的专属领域。内核负责管理系统的各种资源,如 CPU、内存、设备驱动等,这些关键的操作都在内核空间中进行。当设备驱动程序与硬件设备进行交互时,如网卡驱动接收网络数据包,这个过程就发生在内核空间。内核空间的稳定性和安全性至关重要,它为整个系统的正常运行提供了坚实的保障。 (二)页表管理机制 ARM64 的页表结构是实现内存管理的关键所在,它就像一个精密的导航系统,指引着虚拟地址与物理地址之间的转换。ARM64 支持最多 4 级页表,以 48 位地址总线位宽为例,虚拟地址被划分为不同的部分,用于在各级页表中查找对应的物理地址。 当虚拟地址的最高位 bit [63] 为 1 时,该地址用于内核空间,页表的基地址寄存器使用 TTBR1_EL1;若 bit [63] 等于 0,则虚拟地址属于用户空间,页表基地址寄存器为 TTBR0。以 4KB 大小页面、48 位地址宽度、4 级映射为例,L0 页表中有 512 个表项,以虚拟地址的 bit [47:39] 作为索引值在 L0 页表中查找相应的表项,每个表项包含下一级页表(L1 页表,即 PUD)的基地址。接着,在 PUD 页表中,以虚拟地址的 bit [38:30] 为索引值查找,每个表项又含有下一级页表(L2 页表,即 PMD)的基地址。在 PMD 页表中,以虚拟地址的 bit [29:21] 为索引值查找,得到下一级页表(L3 页表,即 PTE)的基地址。最后,在 PTE 页表中,以虚拟地址的 bit [20:12] 为索引值查找,每个 PTE 表项中含有最终的物理地址的 bit [47:12],再和虚拟地址中 bit [11:0] 合并,从而完成地址翻译过程。 这种页表管理方式,使得 ARM64 能够高效地管理内存,实现虚拟地址到物理地址的准确转换,同时也为内存的保护和隔离提供了支持。不同进程拥有各自独立的页表,彼此的地址空间相互隔离,提高了系统的安全性和稳定性。 (三)内存分配与回收策略 Linux 内核针对 ARM64 提供了一系列完善的内存分配与回收策略,以确保系统内存的高效利用。在内存分配方面,常用的函数如 kmalloc,用于在内核空间分配内存。kmalloc 函数根据所需内存的大小,从内核的内存池中分配一块连续的内存块。当内核需要创建一个新的内核数据结构时,就可以使用 kmalloc 来分配内存。它的分配过程相对高效,能够快速满足内核的内存需求。 除了 kmalloc,还有 vmalloc 函数,它用于分配非连续的内存空间。在一些情况下,内核需要分配较大的内存块,但连续的内存空间可能不足,这时 vmalloc 就派上了用场。它通过在虚拟地址空间中建立映射,将分散的物理内存块映射为连续的虚拟地址空间,从而满足内核对于大内存块的需求。不过,由于需要进行额外的地址映射操作,vmalloc 的分配效率相对较低。 在内存回收方面,Linux 内核采用了多种机制。当系统内存不足时,会触发页面回收机制。其中,kswapd 内核线程负责周期性地回收内存。它会扫描系统中的内存页面,将那些长时间未被访问的页面回收,释放内存供其他进程使用。当系统中某个进程占用了大量内存,而其他进程又急需内存时,kswapd 线程就会被唤醒,开始回收内存。 直接页面回收机制也会在内存分配时发挥作用。当调用页面分配接口函数 alloc_pages () 分配物理页面时,如果系统内存短缺,不能满足分配请求,内核会直接自陷到页面回收机制,尝试回收内存来解决当前的燃眉之急。这种直接回收机制是同步进行的,会阻塞调用者进程的执行,直到回收足够的内存或者确定无法满足分配需求。 这些内存分配与回收策略相互配合,使得 Linux 内核在 ARM64 架构上能够灵活、高效地管理内存,确保系统在不同负载情况下都能稳定运行。  

  • 2025-01-21
  • 回复了主题帖: 2025新年花灯节,来场花式点灯秀吧!

    Linux+FPAG+MCU+NPU远程视觉识别神经网络点灯。

  • 2025-01-17
  • 回复了主题帖: 《奔跑吧Linux内核1:基础架构》-01-总体介绍

    freebsder 发表于 2025-1-17 16:29 新活动?好像24年Linux的读书活动不少呢。 回炉的,就申请了一下。

  • 回复了主题帖: 《Linux内核深度解析》-05-Linux内核互斥技术说明

    秦天qintian0303 发表于 2025-1-17 13:53 采用互斥技术是不是就可以确定内容使用的唯一性?  我的理解是操作的唯一性,内容应该也是唯一的,只是有可能内存部分没更新导致不同步。

  • 发表了主题帖: 《奔跑吧Linux内核1:基础架构》-01-总体介绍

    在操作系统领域,Linux 内核以其开源、高效、稳定等特性占据着重要地位。本书作为深入了解 Linux 内核基础架构的佳作,为读者打开了一扇通往内核世界的大门。对于渴望深入探究操作系统底层原理、提升技术能力的技术爱好者和开发者而言,阅读此书是一次宝贵的学习之旅。本书开篇介绍了 Linux 内核的发展历程,从最初的版本不断演进,逐步成长为如今功能强大、应用广泛的内核。接着深入讲解了内核的基本组成部分,包括进程管理、内存管理、文件系统等。通过生动的比喻和详细的图示,让读者对内核的整体架构有了清晰的认识。例如,将进程管理类比为工厂中的生产流程,每个进程如同一条生产线,形象地展现了进程的创建、调度和终止等过程。          内核数据结构与算法,在数据结构与算法部分,书中详细阐述了内核中常用的数据结构,如链表、树等。这些数据结构在实现内核的各种功能中发挥着关键作用。以链表为例,在进程管理中,链表用于组织进程控制块,方便对进程进行遍历和操作。同时,介绍了相关的算法,如内存分配算法,详细解释了如何高效地管理内存资源,确保系统的稳定运行。 深入探讨了内核的各种机制,如中断处理机制。当外部设备产生中断信号时,内核如何快速响应并进行处理,书中给出了详细的流程和代码实现分析。在系统调用方面,解释了用户空间如何通过系统调用进入内核空间,获取内核提供的服务,这是应用程序与内核交互的重要方式。   整个目录看起来是基于ARM64 的方面比较多,同是还介绍了ARMV8架构进行了对比说明,     通过阅读本书,我对 Linux 内核的理解从表面深入到了底层。深入掌握了进程管理的原理,明白了进程调度算法如何根据不同的系统需求合理分配 CPU 资源,这对于优化系统性能至关重要。在内存管理方面,学会了如何高效地管理内存,避免内存泄漏和碎片问题。这些知识不仅丰富了我的技术储备,也为今后从事系统开发和优化工作打下了坚实的基础。阅读过程中,我深刻体会到了 Linux 内核开发者严谨的思维方式。内核的设计和实现需要考虑各种复杂的情况,确保系统的稳定性和高效性。这让我在面对自己的编程任务时,也开始更加注重代码的健壮性和可扩展性。学会了从系统的角度去思考问题,而不仅仅局限于功能的实现,这种思维方式的转变将对我今后的技术工作产生深远的影响。         《奔跑吧 Linux 内核 1:基础架构》是一本极具价值的技术书籍。通过阅读本书,我在 Linux 内核的知识领域取得了显著的进步,不仅掌握了丰富的技术知识,还实现了思维方式的转变,深刻感悟到了开源精神。同时,本书的知识对我的实际工作和学习有着重要的启发和指导作用。未来,我将继续深入学习 Linux 内核的相关知识,阅读后续的系列书籍,进一步探索内核的奥秘。同时,希望能够将所学的知识应用到实际项目中,为开源社区贡献自己的力量。我相信,在 Linux 内核的学习道路上不断前行,将会为我的技术生涯带来更多的机遇和挑战,让我在技术领域不断成长和进步。      

  • 发表了主题帖: 《Linux内核深度解析》-05-Linux内核互斥技术说明

    在 Linux 内核的复杂环境中,多进程并发执行是常态 。当多个进程同时访问共享资源时,就如同多个线程同时对一个共有的变量进行加 1 操作,由于并行运行,可能导致本该被加两次的变量只被加了一次。这就是所谓的竞态条件,会造成数据的不一致和错误 。 为了避免这种情况,就需要引入互斥技术,保证在同一时刻,只有一个进程能够访问共享资源,从而确保数据的完整性和一致性。互斥技术就像是一把锁,当一个进程获取到这把锁时,其他进程必须等待,直到锁被释放,才能有机会访问共享资源。 (一)并发与竞态        在 Linux 内核的运行环境中,并发是指多个执行单元(如进程、线程或中断处理程序)在同一时间段内同时执行 。想象一下,在一个繁忙的火车站,多个乘客同时在售票窗口买票,这就类似于并发的场景。而竞态则是当这些并发执行的单元同时访问和修改共享资源时,由于它们的执行顺序不确定,导致最终结果出现不可预测的情况。例如,多个进程同时对一个共享的计数器进行加 1 操作,如果没有适当的同步机制,可能会出现计数器的值增加的次数少于预期的情况。 (二)临界区       临界区是指访问共享资源的代码段,这段代码在同一时间只能被一个执行单元执行,以避免竞态条件的发生。继续以上述火车站为例,售票窗口的工作人员处理每个乘客的购票请求时,这个处理过程就相当于临界区,同一时间只能为一位乘客服务,否则就会出现混乱。临界区的存在是为了确保共享资源的一致性,它需要被严格保护,防止多个执行单元同时进入。 (一)中断屏蔽       中断屏蔽的原理是在进入临界区之前,通过特定的指令将 CPU 的中断响应功能暂时关闭 ,使得在临界区代码执行期间,不会被外部中断所打断。在 Linux 内核中,提供了一系列用于中断屏蔽的函数,如local_irq_disable()用于禁止本地中断,local_irq_enable()用于使能本地中断 。以一个简单的示例来说明,如果有一个共享资源是一个全局变量shared_variable,当一个进程要对其进行修改时,为了避免在修改过程中被中断干扰,可以这样使用中断屏蔽:   local_irq_disable(); // 访问和修改共享资源 shared_variable = shared_variable + 1; local_irq_enable(); 中断屏蔽的优点在于它能够非常有效地保证在临界区内的代码执行不会被中断,从而避免了由于中断导致的竞态条件。由于中断与进程调度紧密相关,屏蔽中断也限制了系统进程的并发,进一步降低了竞态发生的可能性 。然而,它的缺点也很明显。Linux 内核中许多重要的操作,如异步 I/O 等都依赖于中断,如果长时间屏蔽中断,会导致这些操作无法正常进行,可能会对整个系统的性能和稳定性造成严重影响。中断屏蔽只能解决单 CPU 内部的竞态问题,对于多 CPU 系统(SMP),由于其他 CPU 的中断仍然可能发生,所以无法解决 SMP 多 CPU 引发的竞态 。因此,中断屏蔽通常适用于临界区代码非常简短的场景,并且在单 CPU 环境下使用更为合适。   (二)原子操作       原子操作指的是在执行过程中不会被中断的操作,要么全部执行成功,要么全部不执行,具有不可分割性 。在单处理器系统中,由于不存在多个处理器同时访问共享资源的情况,原子操作相对容易实现。一些简单的指令,如对单个变量的赋值操作,本身就是原子的,因为在单处理器上,指令是顺序执行的,不会被其他处理器干扰 。而在多处理器系统中,情况就变得复杂起来。不同的处理器体系结构采用了不同的方法来实现原子操作。在 x86 架构中,通过在指令前加上 “LOCK” 前缀来实现原子的读 - 修改 - 写操作。当一个处理器执行带有 “LOCK” 前缀的指令时,它会锁住总线,使得其他处理器在该指令执行期间无法访问内存,从而保证了操作的原子性 。在 ARM 架构中,使用 “load exclusive” 和 “store exclusive” 指令对来实现原子操作。“load exclusive” 指令会加载内存值,并标记该内存位置为独占访问,“store exclusive” 指令会尝试存储值,并检查该内存位置是否仍处于独占状态,如果是则存储成功,否则失败。这种机制确保了在多处理器环境下,对共享资源的操作能够以原子的方式进行。 (三)自旋锁       自旋锁是一种用于实现多处理器环境下互斥访问的机制 。其工作原理是,当一个进程试图获取自旋锁时,如果锁当前处于可用状态(即未被其他进程持有),则该进程立即获得锁并继续执行;如果锁已被其他进程持有,那么该进程会在原地不断循环检查锁的状态,直到锁被释放 。自旋锁的结构体在 Linux 内核中定义为spinlock_t,相关的操作函数有spin_lock()用于获取自旋锁,spin_unlock()用于释放自旋锁 。例如:   spinlock_t lock; spin_lock_init(&lock); // 尝试获取自旋锁 spin_lock(&lock); // 访问临界区 // 释放自旋锁 spin_unlock(&lock); 自旋锁适用于临界区执行时间较短的场景,因为在等待锁的过程中,进程不会睡眠,而是一直占用 CPU 资源进行自旋,所以如果临界区执行时间过长,会浪费大量的 CPU 资源 。当一个处理器长时间持有自旋锁,而其他多个处理器在不断自旋等待时,会导致系统的整体性能下降。 (四)信号量       信号量是一个整型变量,它通过一个计数器来控制对共享资源的访问 。信号量分为二值信号量和计数信号量,二值信号量相当于一把锁,只有 0 和 1 两种状态,用于实现互斥访问;计数信号量则可以有多个值,用于控制对多个相同资源的访问 。当一个进程试图获取信号量时,如果信号量的值大于 0,则将其值减 1 并成功获取;如果信号量的值为 0,则该进程会被阻塞,放入等待队列中,直到有其他进程释放信号量 。在 Linux 内核中,信号量的结构体为struct semaphore,相关操作函数有down()用于获取信号量,up()用于释放信号量 。比如:   struct semaphore sem; sema_init(&sem, 1); // 获取信号量 down(&sem); // 访问临界区 // 释放信号量 up(&sem);       信号量与自旋锁的主要区别在于,当无法获取信号量时,进程会被阻塞并进入睡眠状态,让出 CPU 资源;而自旋锁在无法获取锁时,进程会自旋等待,一直占用 CPU 。因此,信号量适用于临界区执行时间较长的场景,这样可以避免自旋等待浪费 CPU 资源。 互斥锁是一种用于实现进程或线程间互斥访问的同步机制,它的核心作用是确保在同一时刻,只有一个进程或线程能够进入临界区 。与信号量相比,互斥锁更为纯粹地实现了互斥功能,信号量可以通过设置初始值来控制多个资源的访问,而互斥锁就像一把严格的 “独占锁”,只允许一个进程进入临界区 。例如,在一个多线程的数据库访问程序中,多个线程可能同时请求对数据库进行写操作,如果不加以控制,就会导致数据的混乱。使用互斥锁,当一个线程获取到锁后,其他线程必须等待,直到该线程完成数据库写操作并释放锁,这样就能保证数据库数据的一致性。       在 Linux 内核中,互斥锁的主要操作函数包括用于获取互斥锁的mutex_lock()和用于释放互斥锁的mutex_unlock() 。mutex_lock()函数用于获取互斥锁,如果锁当前未被持有,调用该函数的进程将立即获得锁并继续执行;如果锁已被其他进程持有,那么调用该函数的进程会进入睡眠状态,直到锁被释放 。mutex_unlock()函数则用于释放互斥锁,当一个进程完成对临界区的访问后,必须调用该函数来释放锁,以便其他进程有机会获取锁进入临界区 。在使用互斥锁的操作函数时,需要注意以下几点:一定要确保在获取锁后,在合适的时机释放锁,否则会导致死锁。获取锁的时间应尽可能短,以提高系统的并发性能。例如,在对一个共享的链表进行操作时,在获取互斥锁后,应尽快完成链表的插入、删除等操作,然后释放锁,避免其他进程长时间等待。          Linux 内核的互斥技术是确保系统在多进程并发环境下稳定运行的关键 。从基本的中断屏蔽到复杂的自旋锁、信号量以及互斥锁,每一种技术都有其独特的应用场景和优缺点 。中断屏蔽简单直接,但长时间使用会影响系统性能;原子操作为基本数据操作提供了原子性保障;自旋锁适用于短时间临界区,而信号量和互斥锁则在更广泛的场景中发挥作用 。 随着技术的不断发展,Linux 内核互斥技术也在持续演进 。未来,我们可以期待更高效、更智能的互斥机制出现,以满足不断增长的系统性能需求 。希望本文能为你深入理解 Linux 内核互斥技术提供有益的帮助,也鼓励你在实际的编程实践中不断探索和应用这些技术,提升自己的技术能力 。          

  • 2025-01-13
  • 发表了主题帖: 《Linux内核深度解析》-04-中断、异常、系统调用

    说实话,自己对于LInux的内核的运行机制了解的还是太少,对于这个中断。异常、系统调用的还是了解的不清楚,即使读了书中的介绍说明,但是理解还是不是很多, 这里借用一下网图来和大家说一下自己的见解吧,大家不要喷我,仅代表个人意见。 中断        在 Linux 系统的复杂架构中,中断、异常和系统调用犹如精密齿轮组中的关键部件,各自发挥着独特且不可或缺的作用,共同确保系统的高效稳定运行。 中断作为连接硬件与操作系统的桥梁,使得硬件设备能够及时 “告知” CPU 需要处理的事件 ,像键盘的敲击、硬盘数据传输完成等,都能通过中断迅速传递给 CPU,让系统迅速响应。        异常则是系统在执行指令过程中遇到特殊情况时的 “预警信号”,比如除零操作、内存访问越界等错误,它促使系统进行相应的错误处理,避免程序崩溃。        系统调用为用户空间的程序提供了访问内核功能的 “合法通道”,程序借此实现文件读写、进程创建等关键操作,同时也保障了内核的安全性和稳定性,防止用户程序随意访问内核资源。 深入理解这三者的原理与机制,对于 Linux 系统的开发、调试与优化至关重要。接下来,让我们逐步揭开它们的神秘面纱。        中断,简单来说,是指计算机在执行程序过程中,当遇到急需处理的事件时,暂停当前正在运行的程序,转去执行有关服务程序,处理完后自动返回原程序的过程。其概念的提出,源于计算机系统需要高效处理硬件设备与 CPU 之间的异步事件 。在早期计算机中,CPU 与外部设备的交互效率低下,CPU 需要不断查询设备状态,浪费大量时间。中断机制的出现,使得硬件设备能主动 “通知” CPU,大大提高了系统效率。        在 Linux 系统中,中断主要分为硬件中断和软件中断。硬件中断由硬件设备产生,比如键盘按键、硬盘读写完成等操作都会触发硬件中断 。以键盘为例,当用户按下一个按键时,键盘控制器会向 CPU 发送一个中断请求信号,通知 CPU 有按键事件需要处理。硬件中断具有异步性,它的发生不受 CPU 控制,随时可能出现。        软件中断则是通过软件指令触发的中断,常用于系统调用和异常处理。例如,在用户程序需要请求内核服务时,会通过执行特定的软中断指令来实现。软中断通常与特定的功能号相关联,CPU 根据功能号来调用相应的内核函数。       中断处理流程严谨而有序。当硬件设备有事件发生时,会首先向中断控制器发送中断请求信号 。中断控制器负责收集和管理这些请求,并根据优先级决定将哪个请求发送给 CPU。 CPU 接收到中断请求后,会暂停当前正在执行的程序,将程序的上下文(如寄存器的值、程序计数器等)保存到栈中,以便后续恢复。接着,CPU 根据中断向量表找到对应的中断处理程序入口地址。中断向量表是一个存储中断类型与处理程序对应关系的表格,通过它,CPU 能快速定位到处理特定中断的程序。随后,CPU 跳转到中断处理程序开始执行。在处理过程中,会根据中断的具体类型进行相应操作,比如读取硬件设备的数据、更新设备状态等。处理完成后,CPU 会从栈中恢复之前保存的程序上下文,继续执行被中断的程序。 中断在 Linux 系统中扮演着举足轻重的角色。它使得系统能够实时响应硬件设备的各种事件,保障了硬件与系统的高效协同工作 。例如,在网络通信中,网卡收到数据包时会触发中断,Linux 系统能够及时响应并处理数据包,确保网络通信的顺畅。        同时,中断也是实现多任务并发处理的关键。通过中断机制,CPU 可以在不同任务之间快速切换,看似同时处理多个任务,极大提高了系统的资源利用率和整体性能 。没有中断机制,Linux 系统将难以实现高效的硬件管理和多任务处理,无法满足现代计算机系统的复杂需求。   异常      异常,是指在程序执行过程中,由于出现了一些特殊情况或错误,导致程序的正常执行流程被打断的事件。它就像是程序运行过程中的 “小插曲”,但如果处理不当,可能会引发严重问题。在 Linux 系统中,常见的异常类型多种多样。例如,除零异常,当程序尝试进行除以零的运算时,就会触发该异常 ,因为数学规则中,除数不能为零,这是一种明显的错误操作。再如,内存访问越界异常,当程序试图访问超出其分配内存范围的地址时,便会产生此异常。这就好比你在自己房间里活动是正常的,但如果未经允许闯入别人的房间,就会引发问题。还有非法指令异常,当 CPU 遇到无法识别或执行的指令时,会触发该异常,这通常意味着程序代码存在错误。 Linux 系统具备一套完善的异常处理机制。当异常发生时,CPU 会首先检测到这一特殊情况 。它会暂停当前正在执行的指令,将程序的相关上下文信息,如寄存器的值、程序计数器等,保存到特定的内存区域,以便后续恢复程序执行。       接着,系统会根据异常的类型,查找对应的异常处理程序。与中断向量表类似,Linux 系统中有一张异常向量表,存储着各种异常类型与处理程序的对应关系。通过这张表,系统能够迅速定位到合适的处理程序。异常处理程序会对异常进行相应的处理。对于一些可恢复的异常,如缺页异常(当程序访问的页面不在内存中时引发),处理程序会尝试从磁盘中读取所需页面到内存,然后让程序继续执行。而对于一些不可恢复的严重异常,如除零异常,处理程序可能会终止相关程序的执行,并向用户返回错误信息,防止错误进一步扩大影响系统稳定性。 异常处理机制对系统稳定性起着至关重要的作用。它能够及时捕获并处理程序中的错误,避免因一个小错误导致整个系统崩溃 ,确保系统在面对各种异常情况时,仍能保持相对稳定的运行状态。       异常与中断在多个方面存在明显区别。从产生源来看,中断主要由硬件设备产生,是外部设备向 CPU 发出的请求信号;而异常则是由 CPU 在执行指令过程中,因内部出现特殊情况或错误而产生的 。例如,键盘敲击产生中断,而程序中的除零操作引发异常。      在处理流程上,虽然两者都需要保存程序上下文、跳转至相应处理程序,但中断处理更侧重于对硬件设备事件的响应,处理完后通常会快速返回被中断的程序继续执行;而异常处理则更关注对程序错误或特殊情况的处理,可能会根据异常类型进行不同的操作,如恢复程序执行、终止程序等。 尽管存在差异,它们之间也有着紧密联系。异常和中断都属于系统对外部或内部特殊事件的响应机制,都需要暂停当前程序的执行,进行相应处理后再恢复或结束程序 。并且,在某些情况下,异常也可能会引发中断,比如当发生硬件故障导致的异常时,可能会触发相应的中断通知系统进行处理   系统调用 系统调用,简单来说,是用户程序与内核之间进行交互的桥梁。它为用户空间的程序提供了一种安全、受控的方式来访问内核所提供的各种服务 。内核作为操作系统的核心,掌控着硬件资源的管理与分配等关键任务,但出于系统安全性和稳定性的考虑,用户程序不能随意直接访问内核资源。系统调用就解决了这一问题,它允许用户程序通过特定的接口,向内核发起请求,让内核代为执行一些需要特权级别的操作。 其作用体现在多个方面。在硬件操作抽象方面,它为用户程序屏蔽了底层硬件的复杂性 。比如,当用户程序需要从硬盘读取数据时,无需了解硬盘的具体物理结构和读写原理,只需通过系统调用(如 read 系统调用),内核就能完成从硬盘读取数据的复杂操作,并将数据返回给用户程序。 在资源管理领域,系统调用发挥着关键作用。它使得用户程序能够方便地请求和释放系统资源 。例如,通过 open 系统调用打开文件,内核会为该文件分配相应的资源,并返回一个文件描述符供用户程序后续操作使用;而 close 系统调用则用于释放这些资源。在进程管理中,fork 系统调用可创建新进程,exit 系统调用用于终止当前进程,这些都依赖系统调用对资源进行合理的分配与回收。 从进程间通信的角度看,系统调用提供了支持 。像管道、共享内存等进程间通信机制,都通过系统调用实现。不同进程可以借助这些系统调用,安全有效地交换数据,实现协同工作。 安全性也是系统调用的重要考量。通过系统调用,内核能够严格控制用户程序对系统资源的访问 ,防止用户程序因错误操作或恶意行为而破坏系统的稳定性和安全性。例如,用户程序不能直接访问内核的内存空间,但可以通过系统调用请求内核分配和管理内存,从而保障系统的安全运行。         在 Linux 内核中,系统调用的实现机制涉及多个关键步骤。当用户程序需要执行系统调用时,首先会执行一条特殊的指令,这条指令被称为 “陷入指令”(例如在 x86 架构中,使用 int 0x80 或 syscall 指令) 。这条指令的执行会引发一个异常,使得 CPU 从用户态切换到内核态,这是进入内核执行系统调用服务程序的关键一步。 在切换到内核态后,系统会根据系统调用号来确定具体要执行的内核函数。每个系统调用都被分配了一个唯一的系统调用号,就像每个人都有一个独特的身份证号码一样。系统调用号会被存储在特定的寄存器(如 eax 寄存器)中传递给内核。内核中有一个系统调用表,该表存储了系统调用号与对应的内核函数之间的映射关系 。内核通过查找系统调用表,就能迅速找到与该系统调用号对应的内核函数,从而进入相应的处理流程。       关于参数传递,由于系统调用是从用户态到内核态的特殊函数调用,不能像普通函数调用那样直接通过栈来传递参数。在 Linux 中,系统调用主要通过 CPU 寄存器来传递参数 。一般来说,前几个参数会依次存储在 ebx、ecx、edx、esi、edi 等寄存器中。如果参数较多,超过了寄存器的承载能力,会采用其他方式,比如将参数存储在用户空间的一块内存区域,然后将该内存区域的地址通过寄存器传递给内核。内核在执行完系统调用对应的服务程序后,会将结果返回给用户程序 。这个过程通常是将返回值存储在特定的寄存器(如 eax 寄存器)中,然后 CPU 从内核态切换回用户态,用户程序继续执行后续指令。 总结:     中断、异常和系统调用对 Linux 系统的性能、稳定性和安全性有着深远影响。高效的中断处理机制确保了系统能够快速响应硬件设备的请求,减少了 CPU 的等待时间,提高了系统的整体性能。例如,在高速网络通信场景下,快速的中断响应能够及时处理大量的网络数据包,保障网络通信的流畅性。异常处理机制则是系统稳定性的重要保障。它能够及时捕获并处理程序中的错误,防止错误扩散导致系统崩溃。在大型服务器应用中,稳定的异常处理机制可以确保长时间运行的服务不会因程序内部的小错误而中断,提高了系统的可用性 。系统调用的安全受控特性,保障了内核资源不被用户程序随意访问,维护了系统的安全性。在多用户、多任务的 Linux 环境中,系统调用的严格控制能够防止恶意程序对系统资源的非法获取和破坏,保护了系统和用户数据的安全 。       在异常处理方面,随着软件复杂度的增加,需要更智能、更全面的异常检测和处理机制。例如,通过人工智能技术辅助异常诊断和修复,提高系统应对复杂异常情况的能力 。 对于系统调用,随着云计算、大数据等新兴技术的发展,对系统调用的功能和性能也会有新的需求。未来可能会出现更多针对特定应用场景的系统调用,以满足不同领域对系统资源管理和利用的需求。同时,在保障安全性的前提下,进一步优化系统调用的性能,减少用户态与内核态切换的开销,也是重要的发展方向 。总之,中断、异常和系统调用将持续在 Linux 系统的发展中扮演核心角色,不断演进以适应技术的进步和应用的需求。                      

  • 2025-01-08
  • 回复了主题帖: 【测评入围名单(最后1批)】年终回炉:FPGA、AI、高性能MCU、书籍等65个测品邀你来~

    个人信息无误,确认可以完成测评计划

  • 2025-01-05
  • 回复了主题帖: 【 AI挑战营(进阶)】2.踩坑记录1 模型验证

    iexplore123 发表于 2025-1-5 14:46 嗷,我忘了贴仓库地址了Danbinabo/insighrface,这个仓库适合学习整个人脸项目的流程,不过不太适合这次 ... 感谢大佬!

  • 回复了主题帖: 【 AI挑战营(进阶)】2.踩坑记录1 模型验证

    牛逼牛逼,出个更详细的教程让我复刻一下嘛

  • 2025-01-04
  • 发表了主题帖: 【嵌入式AI挑战营 】-05-摄像头测试

    今天先给大家测试一下摄像头的东西,前段时间一直不知道如何入门,我觉得首先应该动起来,还没动就说放弃了那才是真的死的太憋屈了,按照网上的流程,我们首先将摄像头,网线,调试线接好,由于自己是使用的自己做的底板,所以相对来说连接比较方便一点, 首先下载镜像,完成系统的更新,我这里直接使用的是出厂自带的镜像, 然后中端查看IP,通常情况下只需要在同一网段基本没有问题。 查看到了开发板的IP,需要记住当前的IP, 然后下载VLC Media Player,网上都有破解版的,开源的,大家不要被广告骗了, 然后我们在VLC做设置   说明一下,这里的IP是你的开发板的IP,需要根据实际情况做修改。 点击播放就可以看到摄像头里面的东西 了   好了,今天的测试先到这里了,后面有了新的测试再做分享。      

  • 发表了主题帖: 【嵌入式AI挑战营 】-04-RKNN推理基础知识介绍说明

    前段时间一直没有推进,主要是和自己的知识面有关系,因为自己对于AI这一块的东西了解的太少了,然后后面发现对于RK1106的wiki页面有不少的知识,这里我就搬过来和大家一起来分享一下,也算是对于自己学习的一个交代。 具体的教程连接如下:   LuckFox 教程连接: 连接 1. RKNPU 简介​ NPU(Nerual Processing Unit)是一种专门用于加速神经网络计算的处理器。为了满足人工智能的需求,瑞芯微逐渐将NPU集成到其处理器中,这种内置于瑞芯微处理器的NPU被称为RKNPU。LuckFox Pico 系列开发板了搭载瑞芯微 RV1103/RV1106 芯片,内置瑞芯微自研第4代NPU。该NPU具有高运算精度,支持int4、int8、int16混合量化。其中,int8算力为0.5TOPs,int4算力可达1.0TOPs。RKNPU4.0被划分为RKNPU2,因此要使用RKNPU2的SDK和工具套件。 2. rknn-Toolkit2 简介​ RKNN-Toolkit2 工具在 PC 平台上提供 C 或 Python 接口,简化模型的部署和运行。用户可以通过该工具轻松完成以下功能:模型转换、量化、推理、性能和内存评估、量化精度分析以及模型加密。RKNN 软件栈可以帮助用户快速的将 AI 模型部署到 Rockchip 芯片。整体的框架如下: 为了使用 RKNPU,用户需要首先在计算机上运行 RKNN-Toolkit2 工具,将训练好的模型转换为 RKNN 格式模型,之后使用 RKNN C API 或 Python API 在开发板上进行部署。本节介绍用户如何快速在Luckfox Pico系列板子上使用 RKNN Toolkit2 和 RKNPU2 工具转换 yolo5s.onnx 模型为 yolov5s.rknn 模型并进行板端推理。 3 rknn-Toolkit2 安装(PC ubuntu22.04) sudo apt-get update sudo apt-get install python3 python3-dev python3-pip sudo apt-get install libxslt1-dev zlib1g zlib1g-dev libglib2.0-0 libsm6 libgl1-mesa-glx libprotobuf-dev gcc 4 安装RKNN-ToolKit2依赖包 pip3 install -r rknn-toolkit2/packages/requirements_cpxx-1.6.0.txt # such as: pip3 install -r rknn-toolkit2/packages/requirements_cp310-1.6.0.txt 5.安装RKNN-ToolKit2 pip3 install rknn-toolkit2/packages/rknn_toolkit2-x.x.x+xxxxxxxx-cpxx-cpxx-linux_x86_64.whl # such as: pip3 install rknn-toolkit2/packages/rknn_toolkit2-1.6.0+81f21f4d-cp310-cp310-linux_x86_64.whl 包名格式为:rknn_toolkit2-{版本号}+{commit 号}-cp{Python 版本}-cp{Python 版本}-linux_x86_64.whl,根据不同的Python版本,选择安装对应的安装包:     具体的详细的安装步骤,可以直接按照原厂的连接去搭建,这里就只做介绍,毕竟也是原厂搬过来的。      

  • 回复了主题帖: 【嵌入式AI挑战营 】-03-RV1106自制底板的使用

    freebsder 发表于 2024-12-24 15:02 谢谢分享,评测活动自己打板的还是不太多呢。 就是自己弄个底板,调试的时候懒得接线,方便一些。

  • 2025-01-03
  • 发表了主题帖: 《Linux内核深度解析》-03-内存管理

          在LInux系统中,内存管理是一个非常重要的环节,烧友不慎就还导致系统崩溃,所以内存管理是系统稳定运行的基础,也是非常重要的一环。在 Linux 系统的庞大架构里,内存管理无疑是一块关键基石。它肩负着保障系统稳定运行、实现资源高效利用以及提升应用程序性能等多重重任,犹如一位幕后英雄,默默支撑着整个系统的运转。当我们同时开启多个应用程序,比如一边听音乐、一边浏览网页,还后台运行着文件下载任务,此时内存管理就要像一位精明的管家,合理分配内存资源,让每个程序都能顺畅运行,互不干扰。这背后靠的就是内存管理对进程内存空间的精细划分与调度,确保每个进程都有专属的 “内存领地”,避免数据混乱与冲突。再者,对于资源有限的嵌入式设备,如智能手环、智能家居控制器等,高效的内存管理更是决定设备性能优劣的关键。通过优化内存使用,系统能够快速响应操作指令,避免卡顿,为用户带来流畅体验。毫不夸张地说,深入探究 Linux 内核内存管理机制,是解锁系统潜能、优化应用性能的必经之路   1 物理地址         在 Linux 内核的底层世界里,物理内存有着一套严谨且精妙的组织架构。内核通常以页框(Page Frame)作为管理物理内存的基本单位,这就好比将一片广阔的土地划分成规整的小块,每一块都能独立管理与分配。一个页框通常对应着固定大小的内存空间,常见的页框大小为 4KB,当然,在不同架构和配置下,也可能出现 8KB、16KB 甚至更大的页框规格。 为了进一步适配不同硬件特性与系统需求,内核又将物理内存划分成多个区域,也就是常说的区(Zone)。其中,ZONE_DMA 区专门用于满足那些需要直接内存访问(DMA)的设备,这类设备往往对内存访问有特殊要求,只能在特定低地址、物理连续的内存范围内操作,一般涵盖内存起始的 16MB 空间。ZONE_NORMAL 区则是内核与普通进程频繁交互的 “主战场”,涵盖 16MB 到 896MB 的内存段,这片区域的内存能够被内核直接线性映射,访问起来高效便捷。而对于 32 位系统中物理内存超过 896MB 的部分,会被纳入 ZONE_HIGHMEM 区,由于这部分内存无法直接被内核线性映射,需要借助一些特殊的映射机制来访问,像是动态映射技术,不过这也使得访问速度相对较慢。再往宏观层面看,在 NUMA(非一致内存访问)架构系统里,内存还会依据节点(Node)来划分。每个节点关联着一定数量的 CPU 核心以及对应的本地内存,这种架构设计充分考虑到了多处理器系统中,不同 CPU 访问不同内存区域的速度差异,旨在优化整体内存访问性能,让数据获取与处理更加高效。        当系统需要分配物理内存时,针对不同规模的内存需求,内核有着不同的应对策略。对于大块内存的申请,伙伴系统(Buddy System)就会挺身而出。伙伴系统的核心原理犹如一场精妙的拼图游戏,它按照 2 的幂次大小对内存块进行分块管理,从最小的 1 个页框(4KB)开始,逐步扩展到 2 个、4 个、8 个…… 直至 2 的若干次幂个页框的连续内存块。这些大小各异的内存块如同不同规格的拼图碎片,被有序组织在各个链表之中。当有内存分配请求到来,系统就会在相应链表中寻找合适大小的 “拼图碎片”。要是找不到恰好匹配的,它还会尝试将更大的内存块进行拆分,直到满足需求;而当内存被释放回系统时,伙伴系统又会施展 “合并魔法”,将相邻且大小相同的空闲内存块重新组合,回归到更大规格的内存块链表中,时刻保持内存的规整与高效利用,避免碎片化问题的加剧。 与之相对,对于那些频繁出现的小内存分配需求,slub 分配器则扮演着关键角色。它像是一位精明的 “内存管家”,在系统初始化阶段,就预先从伙伴系统中获取一批内存页框,然后将这些页框精细划分成一个个小的内存对象,再把这些对象串联成缓存链表。当进程或内核模块需要小内存时,无需再大费周章地向伙伴系统申请,直接从 slub 分配器的缓存链表中快速获取即可,用完之后归还到链表,以供后续复用。这一过程极大地减少了小内存分配时的碎片化风险,同时借助缓存机制,显著提升了内存分配与回收的速度,确保系统在面对海量小内存需求时,依然能够保持流畅运行。   虚拟地址空间   一)用户态虚拟地址空间构成 在 Linux 系统的进程运行天地里,用户态虚拟地址空间犹如一片专属 “自留地”,为进程的数据与代码存储提供了多样的 “分区”。这片空间起始于地址 0,大小依据处理器架构而定,在 32 位系统中,通常是 3GB 的范围。 代码段(Text Segment),作为这片空间的 “智慧中枢”,承载着程序执行的关键 ——CPU 执行的机器指令。它如同一位严谨的指挥官,只读不写,确保指令的稳定性与安全性,防止程序运行时误操作对代码的篡改,让程序沿着既定的逻辑轨道顺畅前行。 紧挨着代码段的是数据段(Data Segment),这里存放着程序中已初始化且初值不为 0 的全局变量和静态变量,犹如一个装满宝藏的仓库,为程序运行提供各类初始数据,随时待命供程序取用。与之相邻的 BSS 段(Block Started by Symbol),则专门收纳那些未初始化以及初始值为 0 的全局变量和静态变量。在程序加载之际,操作系统会贴心地将这片区域初始化为零或空值,既节省了目标文件的存储空间,又确保变量有初始的 “栖息之所”。 再往高地址方向探索,便是充满活力的堆(Heap)区域,它像是一片可拓展的 “建筑工地”,用于存放进程运行时动态分配的内存。当程序调用 malloc ()、new () 等函数时,如同建筑工人在此添砖加瓦,新分配的内存单元不断在堆上 “拔地而起”;而 free ()、delete () 函数的调用,则如同拆除废弃建筑,释放的内存回归堆中,等待下次被重新利用。堆向高地址扩展,不过由于其内存分配与回收的随机性,就像建筑工地的布局常变,容易产生内存碎片问题。 与堆相反,栈(Stack)区域则是从高地址向低地址 “生长”,它如同一个高效的 “数据管家”,负责存储函数内部声明的非静态局部变量、函数参数以及函数返回地址等关键信息。函数调用时,栈帧如同一个个收纳盒,层层堆叠,保存着当前函数的上下文;函数返回时,栈帧又依次弹出,恢复之前的状态。栈内存由编译器自动分配释放,其操作迅速高效,确保函数调用的流畅进行,不过一旦使用不当,比如递归过深或局部变量占用空间过大,就可能引发栈溢出的风险,如同管家的收纳盒堆满溢出现象。 最后,还有内存映射区(mmap),这是一片神奇的 “连接地带”,通过 mmap () 系统调用,它能将磁盘文件的内容直接映射到内存之中,让程序可以像操作内存一样便捷地读写文件内容,极大提升文件操作效率;同时,动态链接库在加载时也会入驻这片区域,为程序运行提供丰富的功能拓展,如同为程序开启一扇通往外部资源的大门。 (二)内核态虚拟地址空间剖析 相较于用户态虚拟地址空间,内核态虚拟地址空间更像是一片 “管控中枢”,掌控着系统的核心资源与关键操作。在 32 位系统中,它占据着虚拟地址空间的 3GB 到 4GB 这 1GB 范围,虽然看似不大,却有着极高的 “权限” 与精妙的布局。 其中,直接映射区是内核态空间的一大 “基石”,占据了前 896MB 的区域。它如同一条坚实的 “纽带”,将内核空间与物理内存中的 ZONE_DMA 和 ZONE_NORMAL 区域紧密相连,建立起一种直接、固定的映射关系。这种映射方式简单而高效,内核只需通过简单的数学运算,将虚拟地址减去特定偏移量,就能精准定位到对应的物理地址,如同在地图上凭借坐标快速找到目的地,使得内核可以迅速访问关键的物理内存资源,满足日常运行需求。 而对于高端内存的管理,内核态引入了动态映射区。由于内核需要掌控所有物理内存,但受限于 1GB 的虚拟地址空间,当物理内存超过 896MB 时,直接映射已无法满足全部需求。此时,动态映射区就像一位 “灵活的调度员” 挺身而出,它位于内核态虚拟地址空间的高端部分,通过一系列复杂而巧妙的页表操作,能够按需将虚拟地址映射到物理内存中的高端内存区域(ZONE_HIGHMEM)。无论是临时的数据缓存,还是内核模块加载到高端内存,动态映射区都能灵活调配,确保内核在面对多样的内存需求时,始终游刃有余,维持系统的稳定高效运行。 内存映射:虚拟与物理的 “桥梁”    (一)分页机制:精准映射的实现 在 Linux 内核的内存管理 “蓝图” 里,分页机制宛如一位神奇的 “空间架构师”,将虚拟内存和物理内存这两大 “空间领域”,精心划分成一个个固定大小的 “单元积木”—— 也就是页。常见的页大小为 4KB,这一规格如同标准的积木尺寸,在不同硬件架构与系统配置下,虽可能有所调整,如变为 8KB、16KB 等,但都遵循着统一、规整的划分原则。 如此一来,虚拟内存中的每一页,都能依据一套精密的 “映射蓝图”—— 页表,精准找到与之对应的物理内存页。页表,就像是一座连接虚拟与物理世界的 “桥梁”,它的每一项记录(页表项)都承载着关键信息,如同桥梁上的 “指示牌”,清晰指引着虚拟页通往物理页的路径。以 32 位系统为例,虚拟地址通常被划分为两部分:高 20 位作为虚拟页面号(VPN),宛如街区编号,用于在页表中精准定位到对应的页表项;低 12 位则是页内偏移量,如同房间号,能在找到的物理页帧内迅速定位到具体的数据存储位置。 这种分页模式带来的优势显著非凡。一方面,它极大地削减了内存管理的复杂度,内核无需再对每一个字节的内存 “斤斤计较”,只需聚焦于这些固定大小的页,如同城市规划师只需着眼于一个个规整的街区,管理负担大幅减轻。另一方面,内存的分配与回收效率得到飞跃提升。当进程申请内存时,内核能迅速从空闲页链表中挑选出若干连续或离散的页进行分配;进程结束后,回收这些页也如同清理闲置街区般轻松,有效规避了内存碎片的滋生,确保内存空间始终井然有序。 更为关键的是,分页机制为内存的保护与共享筑牢了坚实根基。每个进程配备的独立页表,恰似一把把专属 “钥匙”,赋予进程访问自身内存页的权限,严禁越界访问,有力保障了系统的安全性,防止进程间的内存数据 “相互串门”、引发混乱。同时,借助巧妙的页表设置,多个进程能够共享同一段物理内存,就像不同的租客可以共享公寓的公共区域,实现了内存资源的高效复用,极大提升了系统的整体性能,让有限的物理内存发挥出更大的价值。 (二)MMU 与 TLB:加速访问的 “利器” 在内存管理的 “高速通道” 上,内存管理单元(MMU)无疑是一位核心 “调度员”,肩负着将虚拟地址迅速转换为物理地址的重任,保障 CPU 能精准、高效地访问内存数据。当 CPU 发出内存访问指令,携带的虚拟地址信息就如同一份 “快递收件地址”,MMU 会依据进程专属的页表,快速解读这份地址,将其转换为对应的物理地址,如同快递员依据地址簿找到收件人的实际住址,整个过程精准且迅速,确保数据能及时送达。 为了进一步加快地址转换的速度,提升系统运行效率,MMU 内部还配备了一个强大的 “缓存助手”—— 转换后备缓冲器(TLB),也就是常说的快表。TLB 宛如一个 “高速快递分拣站”,专门缓存近期频繁使用的页表项,这些页表项如同提前分拣好、等待派送的快递包裹。当 CPU 发起内存访问时,MMU 首先会在 TLB 中 “查找包裹”,若幸运命中,就能直接获取物理地址,省去了访问内存中庞大页表的时间,如同快递员直接从分拣站取走包裹派送,效率极高;若不幸 TLB 未命中,才会 “前往” 内存中的页表进行完整的地址转换查找,不过此时找到的页表项也会被 “顺手” 存入 TLB,为后续的访问加速。 在实际运行场景中,TLB 的命中率对于系统性能起着关键作用。由于程序运行通常具有良好的局部性原理,即在一段时间内,CPU 往往频繁访问相邻或相关的内存区域,这使得 TLB 有很大几率缓存到这些热点区域的页表项,命中率常常能达到 90% 以上。像日常办公软件,在文档编辑过程中,CPU 频繁读写文档内容、样式数据所在的内存页,这些页表项大概率会驻留在 TLB 中,使得操作响应迅速;又如图形渲染场景,渲染数据、纹理内存页在一段时间内被密集访问,TLB 的高效缓存也能显著减少地址转换延迟,让渲染流程更加流畅,为用户带来顺滑的使用体验。 内存分配与回收   (一)伙伴系统算法:大块内存的高效分配 伙伴系统算法作为 Linux 内核管理大块内存的得力 “干将”,其核心原理基于一种巧妙的分组策略。它把物理内存中的空闲页框,按照 2 的幂次大小进行分组,如同将积木按照不同规格整齐归类。从最小的 1 个页框(4KB)开始,逐步形成包含 2 个、4 个、8 个…… 直至 2 的若干次幂个连续页框的组,每组构成一个链表,这些链表就像是不同规格积木的收纳盒,有序存放着相应大小的内存块资源。 当系统收到内存分配请求时,比如某个进程需要 128KB(即 32 个 4KB 页框)的连续内存空间,伙伴系统首先会在对应 128KB 大小的链表中寻找空闲块。若该链表中有可用资源,直接取出交付给进程使用;要是此链表为空,它便会向更大一级的链表(如 256KB 链表)“求助”。一旦找到合适的大内存块,就将其一分为二,一半分配给进程,另一半则插入到 128KB 链表中备用,确保后续同规格内存需求能快速满足。 而在内存回收阶段,伙伴系统的 “合并魔法” 就开始施展。当进程释放一块内存时,系统会检查其 “伙伴” 内存块状态,这里的 “伙伴” 需满足三个严苛条件:一是大小相同;二是物理地址连续;三是起始地址符合特定对齐规则,即起始物理地址是对应内存块大小的整数倍。只有同时满足这些条件,两块内存才能合并成更大的内存块,回归到上一级链表之中,就像两个相同规格的积木重新拼接成更大组件,放回对应的收纳盒,时刻保持内存的规整性,有效避免外部碎片的产生。 举个例子,假设系统初始有一块 1MB(256 个 4KB 页框)的连续空闲内存,以 2 的幂次划分为多个子块并链接管理。当依次有进程 A、B、C 分别申请 64KB、128KB、32KB 内存时,系统会精准分配,并按需拆分更大内存块,剩余部分妥善归位到相应链表。待进程结束释放内存,伙伴系统又会依据 “伙伴” 规则迅速合并可用内存,让内存布局始终处于高效有序状态。不过,这种以 2 的幂次分配内存的方式,偶尔也会产生一些内部碎片。例如进程申请 70KB 内存,系统只能分配 128KB 内存块,就会有 58KB 空间暂时闲置,造成一定的内存浪费,这也是在享受伙伴系统高效管理带来便利时,需要权衡考量的小 “代价”。 (二)Slab 分配器:小对象的贴心管家 在 Linux 内核的日常运作里,频繁会有对小内存块的分配需求,像进程描述符(task_struct)、文件描述符等这些内核常用的数据结构,往往只需占用几十字节到几百字节的空间。此时,slab 分配器就宛如一位贴心管家登场,专为管理此类小对象而生。 slab 分配器的精妙之处在于,它为不同类型的内核对象量身定制了专属的内存缓存。内核初始化阶段,就依据各类对象的常见使用频率与大小,提前从伙伴系统 “批发” 来大块内存,再将这些大块内存精细切分成一个个与对象大小适配的小块,同时配备相应的管理数据结构,共同构成一个个高速缓存组。这些缓存组如同一个个分类明晰的 “小宝箱”,针对不同内核对象,有着不同规格的 “内存格子”,随时准备为内核分配小内存块。 以常见的内核对象为例,对于文件描述符,slab 分配器会创建专门的高速缓存。这个缓存中,每个 slab 通常由一个或多个连续物理页组成,再进一步将其划分成多个文件描述符大小的对象。内核需要新的文件描述符时,无需大费周章向伙伴系统申请大块内存,直接从对应的文件描述符高速缓存中快速取出一个空闲对象即可,整个过程如同从装满文件描述符 “模板” 的小宝箱里顺手拿出一个,迅速且便捷;用完之后,归还到缓存,等待下次复用,避免频繁的内存申请与释放操作带来的性能开销。 从操作细节看,创建 slab 缓存时,内核通过 kmem_cache_create () 函数,依据对象大小、对齐要求等参数,构建起 kmem_cache 结构体,它如同宝箱的 “总管家”,掌控着缓存内对象的分配、回收等诸多事宜。分配对象时,首选 kmem_cache_cpu 结构体管理的快速通道,这里缓存着当前 CPU 本地的空闲对象,通过 freelist 指针迅速定位获取;若本地无空闲,则向 kmem_cache_node 结构体管理的共享缓存 “求助”;实在不够,才向伙伴系统申请新内存扩充缓存。释放对象时,对象会依据其状态,在 slabs_full、slabs_partial、slabs_empty 这三个链表间灵活转移,确保内存高效利用。    对比 kmalloc () 和 vmalloc () 等内存分配函数,slab 分配器的优势尽显。kmalloc () 虽能分配连续物理内存,但对于频繁小对象分配,易产生内存碎片;vmalloc () 虽可分配大块虚拟内存,但物理内存可能不连续,访问效率相对较低。而 slab 分配器基于对象类型精准管理内存,不仅减少碎片,还利用缓存机制加速分配,成为内核小对象内存管理的不二之选,保障内核在复杂任务处理中始终高效运行。         至此,我们一同深入探究了 Linux 内核内存管理的诸多关键层面,从物理内存的底层架构搭建,到虚拟地址空间的精巧划分;从内存映射的精准桥梁构建,再到内存分配与回收的动态智慧施展,乃至嵌入式驱动开发场景中的实战应用,每一处细节都彰显着 Linux 内核内存管理的精妙与强大。 它不仅是系统稳定运行的根基,更是开发者挖掘系统潜能、优化应用性能的核心工具。深入理解这些机制,无论是对于系统编程高手雕琢大型软件架构,还是嵌入式开发者优化硬件资源利用,都有着不可估量的指引作用,能助力大家在面对复杂系统挑战时,游刃有余地设计、调试与优化。        Linux 内核内存管理领域仍在持续演进,新技术、新优化不断涌现。希望各位读者以此为起点,保持探索热情,深入研读内核源码,紧跟内核开发社区动态,不断实践积累,去解锁更多前沿技术奥秘,为 Linux 系统性能提升、创新应用落地添砖加瓦,在开源世界里留下属于自己的智慧印记。  

  • 2025-01-02
  • 发表了主题帖: 《Linux内核深度解析》-02-关于设备的初始化,进程管理

    首先还是图片镇楼       在嵌入式系统开发领域,Linux 内核无疑是一颗璀璨的明珠,其重要性不言而喻。对于众多开发者而言,深入了解 Linux 内核的引导初始化、进程管理以及内存管理等核心环节,犹如掌握了开启高效、稳定嵌入式系统大门的钥匙。当嵌入式设备上电启动,引导初始化过程率先登场,它精心为系统的运行搭建基础环境,确保内核能够顺利加载并执行。进程管理则如同交通枢纽,精准调度系统中的众多进程,让它们有条不紊地运行,充分利用系统资源,提升整体性能。而内存管理恰似一位智慧的管家,精心统筹着有限的内存资源,合理分配、高效回收,保证系统流畅运行,避免资源浪费。本次测评,我将以一个长期专注于嵌入式开发的从业者视角,深入探究 Linux 内核在这三个关键领域的卓越表现,结合实际开发案例与经验,为大家呈现其精妙之处,希望能给同行们带来一些有益的参考与启发。 我们首先来说一下,关于内核的启动以及引导 Linux 内核引导初始化剖析 (一)引导程序概览 在嵌入式设备加电的瞬间,引导程序就如同一位尽职的领航员,率先开启系统启动之旅。以广泛应用的 U-Boot(Universal Bootloader)为例,它承担着初始化硬件环境的重任,从最基础的 CPU 时钟设置,确保系统运行节奏稳定,到内存初始化,为后续内核运行开辟空间,再到存储设备的准备,让内核镜像能够顺利读取,每一步都至关重要。当硬件环境就绪,U-Boot 精准地将 Linux 内核加载到内存指定位置,为内核的启动搭建好舞台,随后把控制权优雅地移交,让内核开启后续精彩表演。 在开源的嵌入式世界里,还有不少类似的引导程序明星。像 Redboot,它凭借出色的跨平台特性,适配多种硬件架构,为开发者提供了极大的便利,无论是 ARM 还是 x86 架构的设备,都能轻松驾驭;而 Barebox 则以高度可定制化著称,开发者可以根据项目需求,灵活调整引导流程、添加专属功能,宛如为项目量身定制的启动神器。这些引导程序各自闪耀,凭借独特优势,助力嵌入式开发者在不同硬件平台上乘风破浪,开启系统开发之旅。 (二)内核初始化流程 当内核接过引导程序的接力棒,首先映入眼帘的是汇编语言编写的启动代码,这堪称内核启动的基石。它小心翼翼地设置内核运行的初始状态,从关键寄存器的初始化,精准设定系统运行的初始参数,到中断向量表的精心构建,为系统应对各类中断事件筑牢根基,每一个细节都关乎后续的稳定运行。紧接着,内核无缝切换到 C 语言编写的初始化函数,开启一场盛大的系统初始化狂欢。内存管理子系统率先登场,它精心规划内存布局,将物理内存巧妙划分为内核区、用户区等不同区域,宛如城市规划师精心布局城市功能区;同时构建页表,实现虚拟内存与物理内存的高效映射,让进程访问内存如同在地图上精准导航,快速且准确。进程调度子系统不甘示弱,初始化进程调度器,为后续多进程的和谐共处制定规则,确保每个进程都能在合适的时机获得 CPU 资源,公平竞争,高效运行。设备驱动子系统也忙碌起来,逐一识别并初始化各类硬件设备,从常见的网卡、硬盘,到特定的传感器,让内核能够与硬件设备顺畅沟通,协同工作。当这一系列初始化工作圆满完成,内核会启动 init 进程,这是系统迈向用户空间的关键一步,它如同开启一扇通往丰富多彩应用世界的大门,后续的用户进程都将在它的引领下有序诞生与运行,为整个系统注入无限生机与活力。 Linux 内核进程管理探究   (一)进程的诞生与家族树 在 Linux 内核的进程世界里,进程的创建方式多种多样,其中 fork、vfork 和 clone 系统调用堪称三把 “利器”。fork 系统调用如同复印机,它会全面复制父进程的资源,为子进程打造一个全新且独立的运行环境。从代码段、数据段到堆栈,无一遗漏,子进程初始时几乎与父进程 “一模一样”,宛如克隆体,随后二者便可各自独立发展,互不干扰。vfork 系统调用则像是一位 “急性子”,它追求创建的高效性,直接让子进程共享父进程的虚拟地址空间,连页面的复制步骤都省略了,父子进程仿佛共用一个 “空间”,不过这也要求子进程尽快执行 exec 系列函数,开启新的程序之旅,以免影响父进程后续运行。clone 系统调用宛如一位 “定制大师”,它最为灵活,通过不同的参数组合,开发者能精准定制子进程对父进程资源的共享程度,既可以共享文件系统、信号处理表,也能单独设定线程运行的起点,满足各种复杂的进程创建需求。 以一个简单的嵌入式 Web 服务器项目为例,主进程负责监听网络端口,一旦有新的 HTTP 请求到来,它便调用 fork 系统调用创建子进程。子进程继承了主进程的网络相关资源配置,随后专注于处理该请求,读取网页文件、生成响应数据,而主进程则继续监听端口,随时迎接新请求,二者并行不悖,高效协同,确保服务器能够流畅地服务众多客户端。 在内核中,进程之间还存在着类似家族树的层级关系,每个进程都有自己的父进程,除了 0 号进程(init 进程)作为系统所有进程的 “始祖”。当一个进程创建子进程时,便形成了一条父子相连的分支,众多分支交织成庞大复杂的进程家族树,内核通过这棵树状结构巧妙管理进程的生命周期、资源分配以及调度顺序,确保整个系统有条不紊地运行。 此外,Linux 内核还支持内核线程与用户线程两种线程模型。内核线程如同系统的 “幕后英雄”,由内核直接创建和管理,独立运行在内核空间,专注于执行内核函数,如内存同步、延时管理等关键系统任务,为系统的稳定运行默默付出。而用户线程则是在用户空间由线程库创建并调度,它们依托于进程,共享进程的用户地址空间,对于普通应用开发者更为熟悉,开发者可以利用线程库灵活地在应用程序中创建多个用户线程,实现并发执行,提升程序性能,比如在图形渲染程序中,多个用户线程分别负责图像加载、绘制、特效处理等任务,让画面呈现更加流畅迅速。 (二)调度的艺术:确保公平与高效 Linux 内核中的完全公平调度算法(CFS)犹如一位公正的裁判,致力于让每个进程都能在 CPU 资源分配上得到公平对待。它为每个进程引入了虚拟运行时间(vruntime)的概念,巧妙地结合进程权重,实现了精细的资源分配。想象一下,在一个多任务的嵌入式系统中,既有实时性要求较高的传感器数据采集进程,需要频繁地与硬件交互、快速处理数据,又有后台数据同步进程,对时间敏感性稍低,但数据量较大。CFS 算法会根据它们的权重,精准分配 CPU 时间片。对于传感器数据采集进程,赋予较高权重,使其在单位调度周期内获得相对更多的运行时间,确保数据的及时性;而后台数据同步进程权重稍低,在系统较为空闲时充分利用 CPU 资源进行数据传输,避免过度占用关键进程的时间。 在实际运行过程中,进程切换时机的把握至关重要。内核通过时钟中断机制,周期性地检查当前运行进程的时间片使用情况。当进程的时间片耗尽,或者有更高优先级的进程进入可运行状态时,内核便迅速启动进程切换流程。以下是一段简化的进程切换关键代码片段:   // 进程调度函数 void schedule(void) {     struct task_struct *prev, *next;     // 获取当前运行进程     prev = current;     // 根据调度算法选择下一个运行进程     next = pick_next_task();     if (likely(prev!= next)) {         // 上下文切换准备工作         context_switch(prev, next);     } } // 上下文切换函数 void context_switch(struct task_struct *prev, struct task_struct *next) {     // 切换内存管理相关上下文     switch_mm(prev->active_mm, next->active_mm, next);     // 切换寄存器状态,完成进程切换     switch_to(prev, next, prev); } 这段代码清晰地展示了从调度决策到实际上下文切换的关键步骤。首先,schedule 函数确定当前运行进程 prev 和下一个要运行的进程 next,若二者不同,则调用 context_switch 函数。在 context_switch 中,先处理内存管理相关的切换,确保进程能访问正确的内存空间,再通过 switch_to 宏完成寄存器状态的切换,这一过程如同瞬间切换舞台上的演员,新进程无缝衔接,继续执行,保证系统高效运行。不仅如此,内核还会根据进程的行为动态调整优先级。当一个进程长时间处于等待 I/O 完成的阻塞状态,内核会适当提升其优先级,待其重新就绪后,能更快地获得 CPU 时间,补偿其等待的损失;而对于占用 CPU 时间过长的进程,内核则会降低其优先级,避免它 “霸占” 资源,让其他进程也有机会一展身手,如此这般,整个系统的响应速度与资源利用率都得到了显著优化。    

统计信息

已有249人来访过

  • 芯积分:437
  • 好友:2
  • 主题:50
  • 回复:160

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言