文章目录
- 1.前导知识
- 1.1 虚拟地址空间的堆区
- 1.2 缺页中断
- 1.3ELF文件格式
- 1.4页/页框/页帧/页表/MMU
- 1.5虚拟地址到物理地址
- 2.初识Linux线程
- 2.1之前所学的进程
- 2.2线程的引入
- 2.3如何理解线程
- 2.4如何理解轻量级进程
- 3.创建线程
- 3.1pthread_create()函数
- 3.2程序测试
- 3.3Makefile怎么写
- 4. 总结线程
- 4.1如何理解线程共享进程的资源
- 4.2线程的优缺点
- 4.3线程的异常
1.前导知识
1.1 虚拟地址空间的堆区
在Linux内核中,struct vm_area_struct(通常简称为vma)是一个核心的数据结构,用于表示进程的虚拟内存区域(Virtual Memory Area)。它描述了进程地址空间中的一个连续区间,并包含了与该区间相关的所有信息,如该区间的起始和结束地址、访问权限、所关联的页表项等。
struct vm_area_struct的定义位于Linux内核源代码的mm/mmap.h文件中。其字段可能因内核版本的不同而有所变化,但通常包含以下一些关键信息:
Linux内核源代码
struct vm_area_struct
{
struct mm_struct *vm_mm; /* 关联的mm_struct */
unsigned long vm_start; /* 起始地址 */
unsigned long vm_end; /* 结束地址 */
unsigned long vm_flags; /* 该VMA的标志,例如可读写、可执行等*/
struct rb_node vm_rb; /* 红黑树节点 */
struct list_head vm_list; /* mm中的VMA双向链表 */
struct vm_area_struct* vm_next, * vm_prev; /*双向链表中的前驱、后继节点*/
pgprot_t vm_page_prot; /* 页面保护标志 */
struct vm_operations_struct *vm_ops; /* VMA操作,用于处理堆区的操作*/
unsigned long vm_pgoff; /* 文件/设备内的偏移量 */
struct file *vm_file; /* 关联的文件 */
void *vm_private_data; /* VMA的私有数据 */
};
vm_start 和 vm_end:表示该虚拟内存区域的起始和结束地址。
vm_flags:一个位掩码,表示该区域的属性,如是否可读、可写、可执行,是否私有等。
vm_page_prot:表示该区域中页面的保护权限。
vm_mm:指向该vma所属的mm_struct的指针,mm_struct表示进程的虚拟地址空间。
vm_next 和 vm_prev:用于将多个vma链接成一个双向链表,这样内核可以遍历进程的整个地址空间。
vm_file:如果该区域与一个文件相关联(例如,通过mmap系统调用映射的文件),则此字段指向该文件的file结构。
vm_pgoff:如果与文件关联,此字段表示文件内的偏移量,即该vma在文件中的起始位置。
vm_area_ops:指向一个操作集合的指针,该集合包含了用于操作该vma的函数指针。
此外,还有许多其他字段用于处理更复杂的场景,如共享内存、匿名内存等。
在Linux内核中,虚拟内存管理是一个复杂的主题,涉及到许多不同的数据结构和机制。struct vm_area_struct是这些机制中的一个关键组成部分,它使得内核能够高效地管理进程的地址空间,并处理与内存相关的各种操作。
vm_area_struct(VMA)是Linux内核中的一个数据结构,表示进程虚拟地址空间中堆区的一个内存区域。用于跟踪关于堆区内存区域的各种属性和信息,如起始和结束地址、权限、标志以及关联的文件或设备。在进程的虚拟内存空间中,堆区通常是一个连续的内存区域,但可能会被分成多个vm_area_struct(VMA)结构来管理。==> 堆空间被划分成多个VMA结构进行管理和使用。
当进程使用malloc()等函数分配内存时,内核会根据需要创建一个或多个vm_area_struct结构来管理这些分配的内存区域。每个vm_area_struct结构对应于堆区中的一个内存段,它包含了该内存段的起始地址、结束地址、标志等信息。
这种分割堆区的方式可以提高内存管理的灵活性和效率。例如,当进程释放部分内存时,可以通过修改VMA结构中的vm_start和vm_end字段来缩小内存段的范围,以反映释放后的内存空间,而不需要重新映射整个堆区。如果释放的内存块与内存段的边界对齐,可能会合并相邻的VMA结构,以减少内存碎片。
通过vm_area_struct结构,内核可以有效地管理进程的堆区,包括分配和释放内存、保护内存区域、处理内存映射等操作。
当创建一个新的vm_area_struct结构时,内核会将其插入到mm_rb红黑树中,以便可以通过起始地址快速查找和访问对应的VMA。同时,内核还会将其插入到mmap双向链表的末尾,以保持VMA的创建顺序。
图解堆区的管理
OS对进程进行了更细粒度的资源划分
1.2 缺页中断
缺页中断是Linux操作系统中内存管理的一个重要机制。下面是对缺页中断的详细介绍:
在Linux中,虚拟内存技术被广泛应用,使得程序的地址空间可以远大于实际的物理内存。当程序试图访问一个尚未加载到物理内存中的页面时,就会发生缺页中断。这是Linux内核为了处理虚拟内存和物理内存之间的差异而引入的一种机制。
具体来说,当程序启动时,Linux内核会检查CPU的缓存和物理内存,看所需的数据是否已存在。如果数据已经存在于内存中,那么内核就会直接使用该数据。但如果所需的数据不在物理内存中,内核就会触发一个缺页中断。
缺页中断的处理过程包括:
中断触发:当程序试图访问一个不在物理内存中的页面时,CPU会发出一个中断信号给内核。
查找页面:内核接收到中断后,会开始在硬盘上查找缺失的页面。这通常涉及到文件系统的操作,因为页面数据通常存储在硬盘上的某个文件中。
加载页面:一旦找到缺失的页面,内核会将其从硬盘加载到物理内存中。加载的过程可能涉及将其他页面从内存中移出,以便为新的页面腾出空间。
恢复执行:页面加载完成后,内核会恢复程序的执行,并将缺失的页面映射到程序的虚拟地址空间中。
缺页中断的作用在于实现了物理内存的按需分配,即只有在真正需要时,页面才会被加载到物理内存中。这种机制大大提高了内存的利用率,使得Linux能够支持运行比物理内存更大的程序。
此外,缺页中断还可以分为主缺页中断和次缺页中断。主缺页中断是指需要从磁盘读取数据而产生的中断,而次缺页中断则是当数据已经被读入内存并被缓存起来后,从内存缓存区而不是直接从硬盘中读取数据而产生的中断。这种分类有助于更精确地分析和管理内存访问的性能。
总之,缺页中断是Linux内存管理中的一个重要机制,它使得程序能够在有限的物理内存条件下运行大型程序或处理大量数据,并通过优化数据交换和缓存来提高性能。
缺页中断是Linux中内存管理的一个重要机制,特别是在处理虚拟内存和物理内存之间的关系时。下面是对缺页中断的介绍,包括其重要原理和功能:
原理:
虚拟内存技术:Linux通过虚拟内存技术极大地扩展了程序的地址空间。即使程序的体积超过了物理内存的实际容量,它仍然可以运行,因为Linux会在内存和硬盘之间进行数据的交换。
数据检查与缓存:当程序启动时,Linux内核首先会检查CPU的缓存和物理内存,看所需的数据是否已存在。如果数据已经存在于内存中,那么内核就会忽略它。
缺页的产生:但如果所需的数据不在物理内存中,那么就会触发一个缺页中断。这意味着内核需要在硬盘上找到这个缺失的页面(或称为“页”),然后将其加载到物理内存中。
功能:
内存管理优化:缺页中断允许Linux更有效地管理内存。它确保只有真正需要的数据才会被加载到物理内存中,从而提高了内存的利用率。
扩大程序地址空间:通过缺页中断和虚拟内存的结合,程序可以拥有比实际物理内存更大的地址空间。这使得大型程序或需要处理大量数据的程序能够在有限的物理内存条件下运行。
数据交换与缓存:当发生缺页中断时,内核不仅会从硬盘读取缺失的页面,还会将其缓存到物理内存中。这样,在将来需要这些数据时,就可以直接从内存中获取,而不需要再次从硬盘读取,从而提高了数据访问的速度。
此外,缺页中断还可以进一步分为主缺页中断和次缺页中断。主缺页中断是指从磁盘读取数据而产生的中断,而次缺页中断则是当数据已经被读入内存并被缓存起来后,从内存缓存区而不是直接从硬盘中读取数据而产生的中断。这种分类有助于更精确地了解和管理内存访问的性能和效率。
总之,缺页中断是Linux内存管理中的一个关键机制,它使得程序能够在有限的物理内存条件下运行大型程序或处理大量数据,并通过优化数据交换和缓存来提高性能。
1.3ELF文件格式
ELF(Executable and Linkable Format)是一种用于应用程序、库和操作系统的二进制文件格式。它是目前主流的可执行文件格式,在大多数现代操作系统中广泛使用,特别是Linux平台。Windows平台则采用PE/COFF格式。ELF文件格式定义了二进制文件的组织结构和加载方式,使得操作系统能够正确加载和运行程序。
ELF文件格式包含了程序的代码、数据、符号表、重定位表等信息。它主要用于存储编译后的程序,以便在运行时由操作系统加载执行。ELF文件可以是可执行文件、可重定位文件(如.o文件)、共享目标文件(如.so文件)或核心转储文件等。
结构上,ELF文件格式可以分为三个部分:ELF头部、程序头部表和节头部表。这些部分共同定义了文件的整体结构以及各部分的具体内容。
在编译过程中,链接阶段会用到ELF格式的文件。编译生成的中间文件,如库文件、可执行文件以及编译中间文件(如.a、.o、.s文件)等,都是ELF格式的文件。在Android应用运行时,ELF文件的大部分内容会被映射到内存中,以供操作系统加载执行。
总的来说,ELF文件格式为操作系统提供了一种标准化的方式来加载和运行编译后的程序,使得不同的编译器和链接器可以生成兼容的二进制文件。
可执行程序本身就是按照逻辑地址的方式进行编译的。最终形成的ELF文件将可执行文件分为多个段(segment),包括代码段、数据段、符号表等。每个段都有自己的属性和对应的内存区域。
运行程序时将代码和数据加载到内存,实际加载的就是ELF文件。
ELF(Executable and Linkable Format)是一种用于可执行文件、目标代码、共享库和核心转储(core dump)的标准文件格式,常用于类Unix系统,如Linux和Mac OS X等。ELF格式具有高度的灵活性和可扩展性,并且支持跨平台使用。它支持不同的字节序和地址范围,因此不会不兼容某一特定的CPU或指令架构。这使得ELF格式能够被运行于众多不同平台的各种操作系统所广泛采纳。
ELF文件通常分为三种类型:
可重定位目标文件:这类文件保存着代码和适当的数据,用于和其他目标文件一起创建可执行文件或共享目标文件。例如,编译的中间产物(.o文件)就属于此类。
可执行目标文件:这是一个可直接执行的文件,它规定了exec如何创建一个程序的进程映像。
共享目标文件:这类文件保存着代码和合适的数据,用于被链接编辑器和动态链接器链接。例如,Linux下的.so文件就是共享库文件。
ELF文件的格式和结构非常复杂,包含了多种节(section)和段(segment),每个节和段都有其特定的用途和含义。这些节和段共同构成了ELF文件的基本框架,使得操作系统能够正确地加载和执行其中的代码和数据。
在文化层面,ELF格式作为计算机科学和软件工程领域的一部分,体现了人们对于软件可移植性、可维护性和可扩展性的追求。通过采用标准化的文件格式,ELF使得不同平台上的软件能够更容易地实现互操作性,从而推动了软件技术的发展和普及。
请注意,对于ELF格式的深入学习和理解通常需要具备计算机科学、软件工程或相关领域的专业知识。如果你对这方面感兴趣,建议查阅相关的技术文档、书籍或在线教程,以获取更详细和准确的信息。
1.4页/页框/页帧/页表/MMU
在Linux操作系统中,页、页框、页帧、页表和MMU(内存管理单元)是内存管理中的核心概念。以下是对这些概念的简述以及它们之间的联系:
页(Page):
概念:页是内存管理的基本单位。在Linux中,物理内存被划分为固定大小的连续内存块,每个块称为一个页。这种分页机制有助于实现虚拟内存,使得操作系统能够更有效地管理和分配内存资源。常见的页大小是4KB、8KB或者更大。物理内存被划分为一系列的页,每个页都有一个唯一的物理地址。
struct Page{ };
在Linux内核中,struct page是用来描述物理页框的数据结构。每个物理页框都对应一个struct page对象,用于管理和跟踪该页框的状态和属性。包括页框是否被占用、页框的引用计数、页框所属的地址空间等。struct page是物理内存的元数据结构。
struct page中的struct list_head lru字段用于将页框链接到LRU(Least Recently Used)链表中,以实现页面置换算法。通过lru字段,可以将页框链接到活跃链表或非活跃链表中。
在Linux内核中,struct page是用于管理物理内存页的数据结构。Linux内核将整个物理内存按照页对齐方式划分成成千上万个页进行管理,而struct page就是为了描述和跟踪这些页的状态及其他属性而存在的。
每个物理页都会对应一个struct page结构体。这些结构体也会占用实际的物理内存,因此内核会采用一些优化手段,如使用联合体(union)来减少内存的使用。struct page结构体中包含了大量的字段,用于表示页的各种属性,如页是否空闲、页是否被锁定、页的类型、页所在的物理地址等。
由于struct page是内存管理的基本单位,很多与内存管理相关的操作都是以页为单位进行的,比如内存中的page in/page out、swap in/swap out、reclaim和mapping等操作。因此,struct page是Linux内核内存管理中使用频率非常高的结构体。
总的来说,struct page在Linux内核中起到了至关重要的作用,它使得内核能够有效地管理和使用物理内存资源。
页框(Page Frame):
概念:页框通常与页的概念相似,它指的是物理内存中的一个固定大小的块。页框是物理内存的划分单位,用于存储页的内容,在Linux内存管理中,页框是用来表示物理内存页的数据结构。整个物理内存的页框通常由mem_map数组表示。
页帧(Page Frame):
实际上,“页帧”和“页框”在大多数上下文中是可以互换使用的术语,它们都指的是物理内存中的一个固定大小的块。在某些文档或讨论中,可能会根据上下文或特定目的使用其中一个术语,但它们在功能上是相同的。可执行程序存储空间(磁盘空间)的划分单位,与页的大小相同。其实就是文件系统中一个数据块的大小(block),操作系统和磁盘进行IO操作的基本单位是4KB。 源代码在编译的时候就会以特定的格式(elf)被编译成以4KB为单位的二进制可执行程序。
页表(Page Table):
概念:页表是一种数据结构,用于存储虚拟地址和物理地址之间的映射关系。每个进程都有自己的页表,这样操作系统可以将进程的虚拟地址转换为物理地址,从而实现对物理内存的访问。页表的存在使得虚拟内存管理成为可能,允许程序在有限的物理内存空间中运行较大的程序。页表并不是直接记录虚拟地址和物理地址的一一映射关系,而是通过多级页表记录虚拟地址和物理页的映射关系(最终需要通过虚拟地址中的页内偏移定位到物理地址)。
MMU(内存管理单元):
概念:MMU是一种硬件组件,负责在程序运行时执行虚拟地址到物理地址的转换。它是计算机体系结构中的关键部分,通常位于中央处理器(CPU)内部。当程序尝试访问一个虚拟地址时,MMU会查找页表,找到对应的物理地址,并将其传递给系统总线,从而实现虚拟内存的透明访问。MMU是内存管理单元(Memory Management Unit)的缩写,是计算机系统中的一个硬件组件。MMU负责虚拟地址到物理地址的转换,以及内存访问权限的控制。
MMU通常集成在CPU或者独立的芯片中,它是操作系统进行内存管理的重要组成部分。MMU的主要功能包括:
地址转换:MMU根据虚拟地址的高位部分(页表索引)查找页表,将虚拟地址转换为物理地址。这个过程通常包括页表的查找和页内偏移的计算。
内存保护:MMU根据页表中的权限位,对访问的虚拟地址进行权限检查。如果访问权限不符合要求,MMU会产生一个异常,中断程序的执行。
缺页处理:当程序访问的虚拟页不在物理内存中时,MMU会产生一个缺页异常。操作系统会根据异常处理程序将缺失的页从磁盘读取到物理内存中,并更新页表的映射关系。
页面置换:当物理内存不足时,MMU会根据页面置换算法(如LRU)将部分页从物理内存置换到磁盘上,以释放内存空间。
MMU的存在使得操作系统可以将虚拟地址空间映射到物理内存,提供了更大的地址空间和更高的灵活性。同时,MMU也起到了保护内存的作用,防止程序越界访问或者非法访问内存。
联系:
页和页框(页帧)是物理内存的基本单位,用于表示和管理内存资源。
页表则是建立虚拟地址和物理地址之间映射关系的数据结构,使得操作系统和程序能够透明地访问内存。
MMU则是实现虚拟地址到物理地址转换的硬件机制,它依赖于页表来执行这一转换。
综上所述,这些概念在Linux内存管理中是相互关联、相互依存的。它们共同构成了Linux虚拟内存管理的基础,使得系统能够更有效地管理和使用内存资源。
1.5虚拟地址到物理地址
- 虚拟地址到物理内存的映射是由操作系统的内存管理单元(MMU)完成的。MMU负责将虚拟地址转换为物理地址。
- 虚拟地址到物理地址的转换过程通常由硬件和操作系统共同完成。下面是一个常见的转换过程:
进程访问虚拟地址:当一个进程需要访问内存时,它使用的是虚拟地址。这个虚拟地址包括两部分:高位部分是页表号,低位部分是偏移量。页表号用于找到对应的页表项,而偏移量表示在该页表项指向的页中的偏移位置。
页表查找:当进程访问虚拟地址时,硬件通过页表查找来确定对应的物理页框。操作系统维护了每个进程的页表,其中记录了虚拟页号与物理页框的映射关系。
物理地址计算:通过页表查找,找到对应的页表项后,可以得到物理页框号。然后,将物理页框号与偏移量组合起来,得到真正的物理地址。
访问物理地址:通过得到的物理地址,进程可以直接访问对应的物理内存。
需要注意的是,虚拟地址的页表查找是一个相对耗时的操作,为了提高访问速度,通常会采用高速缓存(如TLB,Translation Lookaside Buffer)来加速地址转换过程。
这个转换过程确保了不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,从而实现了内存地址空间的隔离。这是现代操作系统实现多任务处理和内存保护的关键机制之一。
虚拟地址到物理地址的映射过程
- 当程序访问虚拟地址时,CPU将虚拟地址发送给MMU。
- MMU根据虚拟地址的高位部分(页表索引)查找页表(page table)。
- 页表中存储了虚拟页号到物理页框号的映射关系。MMU根据虚拟页号找到对应的物理页框号。
- MMU将物理页框号与虚拟地址的低位部分(页内偏移)组合,得到物理地址。
- MMU将物理地址发送给内存控制器,从物理内存中读取或写入数据。
虚拟地址空间可以大于物理内存空间吗?
可以!这种情况下会使用页面置换算法(如LRU)将部分虚拟页置换到磁盘上,以释放物理内存空间。当程序访问被置换到磁盘上的虚拟页时,会触发缺页异常,操作系统会将该虚拟页从磁盘读取到物理内存中,并更新页表的映射关系。
为什么这么设计?
提示:虚拟地址的低位部分(页内偏移)刚好是12位,2^12 = 4KB,页内偏移码刚好能够覆盖整个页框。
看到这儿,是否对计算机的设计人员产生了敬佩?而我只能说一句握草!
2.初识Linux线程
2.1之前所学的进程
2.2线程的引入
- 进程是操作系统进行资源分配的基本单位
- 每个进程都有自己的地址空间、内存、文件描述符等资源
- 线程是进程中的一个执行流 它们共享进程的资源
- 线程是操作系统调度(CPU执行)的基本单位
- 每一个task struct,可以称之为线程 ⇒ Linux特有的方案!
- 通过一定的技术手段,将当前进程的”资源“,以一定的方式划分给不同的task struct!
- 用户视角:内核数据结构 + 该进程对应的代码和数据
- 内核视角:进程是承担分配系统资源的基本实体!
- 线程在进程内部执行 ⇒ 线程在进程的地址空间内运行
- 线程是OS调度的基本单位 ⇒ CPU其实不关心执行流是进程还是线程,只关心PCB
- 我们之前理解的“进程”是单线程进程即内部只有一个执行流的进程。,现在我们要学的是多线程进程即内部具有多个执行流的进程。
- 在Linux中,线程是进程的一部分,被称为轻量级进程(LWP,Lightweight Process)。Linux使用了一种称为"多线程共享同一进程地址空间"的模型,即多个线程共享同一个进程的资源,如内存、文件描述符等。
进程vs线程
之前学习的单进程 ⇒ 具有一个线程执行流的进程
进程是资源分配的基本单位,线程是调度的基本单位
进程是指计算机中正在运行的程序的实例。每个进程都有自己的地址空间、内存块、文件描述符等资源(内核数据结构+内存块),同时还包括所有的线程。进程是操作系统进行资源分配的基本单位。进程之间相互独立,通过进程间通信(IPC)机制来进行数据交换和协作。
线程是进程中的一个执行流(或执行单元),是进程中的实际工作单位。一个进程至少要有一个线程,也可以包含多个线程,它们共享进程的资源,如内存、文件等。线程是操作系统调度(CPU执行)的基本单位。线程之间可以并发执行,提高了程序的并发性和响应性。线程之间通过共享内存来进行通信和同步。
进程和线程的区别主要有以下几点:
**资源开销:**进程之间的切换开销较大,需要保存和恢复整个进程的上下文信息;而线程之间的切换开销较小,只需要保存和恢复线程的上下文信息。
**独立性:**进程是独立的执行实体,拥有独立的地址空间和资源;而线程是进程的子集,共享进程的资源。
**通信和同步:**进程之间通信和同步需要使用进程间通信(IPC)机制,如管道、消息队列、共享内存等;而线程之间通信和同步可以直接通过共享内存来实现,更加方便和高效。
**创建和管理:**在用户空间,进程和线程可以通过不同的API(如fork()和pthread_create())来创建和管理。
2.3如何理解线程
在Linux系统中,进程和线程的结构是相同的。在内核中,进程和线程都是通过task_struct结构体来表示。
task_struct结构体包含了进程或线程的各种属性和状态信息,如进程ID(PID)、父进程ID(PPID)、进程状态、进程优先级、进程的地址空间、文件描述符表、进程的线程组、进程的信号处理结构等。它还包含了一些指针,用于连接进程或线程的相关数据结构,如进程的子进程链表、进程的线程链表等。
在Linux中,线程是进程的一部分,多个线程共享同一个进程的资源,包括地址空间、文件描述符等。同时,从内核的角度来看,进程和线程的结构是相同的,都是通过task_struct结构体来表示。所以Linux系统中的线程也被称为轻量级进程(LWP)。
在Linux系统中,线程和进程之间的区别相对较小,所以Linux并不直接给我们提供线程相关的系统调用,而是统一提供轻量级进程的接口。但是为了降低用户的使用学习难度,Linux在用户层封装了一套多线程方案,以库的形式提供给用户进行使用。【windows有自己真正的“线程”,他对于线程又单独搞了一套数据结构来描述和组织,在Windows系统中,进程和线程是两个不同的概念,并且在结构上也有一些区别。每个进程都有一个独立的进程控制块(Process Control Block,PCB),每个线程也有一个独立的线程控制块(Thread Control Block,TCB)】
所以,Linux下的PCB <= 其他OS的PCB 。Linux没有真正意义上的线程结构,Linux是用进程pcb模拟线程。Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口
在Linux中,线程的创建和管理可以使用pthread库(又叫POSIX线程库、原生线程库)。
总结
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
2.4如何理解轻量级进程
创建时轻量化
线程创建时,只需要创建task_struct结构即可。不需要创建地址空间、页表、文件描述符表等内核数据结构,也不需要加载内存块。进程资源的创建和申请是在进程创建时进行的,多线程共享进程的资源。
调度时轻量化
-
线程切换的成本较低是因为线程共享同一个进程的资源,包括地址空间、页表等。相比于进程切换,线程切换不需要切换地址空间和页表等资源的上下文(寄存器),因此开销较小。
-
线程切换的成本较低的另一个重要原因:在程序运行期间,CPU会根据局部性原理,将内存中的代码和数据预读到CPU高速缓存(L1~L3 catch)。如果是进程切换,CPU高速缓存就会立即失效,需要重新缓存热点数据。而如果是线程切换,则高速缓存的命中率更高,不需要重新缓存数据。
删除时轻量化
线程在删除时,也只需要删除其task_struct结构,不需要释放进程的资源。进程资源的释放和回收是在进程退出时进行的。
3.创建线程
3.1pthread_create()函数
#include <pthread.h>
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);
pthread_create 是 POSIX 线程(也称为 Pthreads)库中的一个函数,用于在程序中创建新的线程。以下是 pthread_create 函数的参数和返回值的详细解释:
参数
pthread_t *thread:
这是一个指向 pthread_t 类型变量的指针,用于存储新创建线程的线程标识符。在函数成功返回后,这个变量会被设置为新线程的标识符。
const pthread_attr_t *attr:
这是一个指向 pthread_attr_t 类型变量的指针,用于指定新线程的属性。这些属性可以包括线程的调度策略、优先级、栈大小等。如果此参数为 NULL,则使用默认属性创建线程。
void *(*start_routine) (void *):
这是一个函数指针,指向新线程开始执行时调用的函数。这个函数通常被称为线程的启动例程(start routine)。它接受一个 void * 类型的参数,并返回一个 void * 类型的值。
void *arg:
这是传递给线程启动例程的参数。它可以是任何类型的指针,并在启动例程中转换为适当的类型。
返回值
如果函数成功,则返回 0。
如果函数失败,则返回一个错误码。
工作原理
当调用 pthread_create 时,它会请求操作系统为新线程分配所需的资源(如栈空间)。如果请求成功,新线程会开始执行传递给 start_routine 的函数,并带有 arg 指定的参数。
在调用 pthread_create 之后,原始线程(即调用 pthread_create 的线程)和新线程会并发执行。这意味着它们可以几乎同时执行,并且执行顺序是不确定的。
注意事项
线程的属性(通过 attr 参数指定)可以影响线程的行为和性能。例如,通过调整线程的栈大小,可以优化内存使用或处理特定的应用需求。
线程启动例程(start_routine)应该是一个返回 void * 并接受 void * 参数的函数。这允许启动例程接收任何类型的参数,并返回任何类型的值(通过转换为 void *)。
线程标识符(thread)是一个用于识别和操作线程的值。它可以用于函数如 pthread_join 或 pthread_detach,以等待线程结束或释放线程的资源。
总之,pthread_create 函数是 POSIX 线程库中用于创建新线程的关键函数,它允许程序员在程序中实现并发执行。
3.2程序测试
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int x = 100;
void show(const string &threadName)
{
cout << threadName << ", pid: " << getpid() << " global var x= " << x << endl
<< endl;
}
void *threadRun(void *args)
{
const string threadName = (char *)args;
while (true)
{
show(threadName);
sleep(1);
}
}
#define THREADNUM 5
int main()
{
// typedef unsigned long int pthread_t;
pthread_t tid[THREADNUM];
char threadName[64];
for (int i = 0; i < THREADNUM; i++)
{
snprintf(threadName, sizeof threadName, "%s%d", "thread", i + 1);
pthread_create(tid + i, nullptr, threadRun, (void *)threadName);
sleep(1); // 缓解传参的bug
}
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(3);
}
}
- 主线程和新线程的PID相同,证明线程是进程的一部分,是进程的一个执行流(执行单元)。
- 进程监视窗口(ps axj)只出现一个mythread进程,证明这6个线程属于同一个进程。
- 轻量级进程监视窗口(ps -aL)出现了一共6个线程,1个主线程(PID和LWP相同),5个新线程(PID和LWP不同)。
- 向进程发送9号信号,所有的线程都终止了。进程是正在运行的程序的实例,是OS进行资源分配的基本单位。所有线程共享进程的资源。所以进程退出,线程必须退出。
3.3Makefile怎么写
不加
-lpthread
查看进程/线程状态
查看线程库
4. 总结线程
4.1如何理解线程共享进程的资源
各线程共享进程的地址空间
- 代码区数据(定义一个函数,在各线程中都可以调用)
- 静态区数据(定义一个全局变量,在各线程中都可以访问)
- 堆区数据(堆空间的指针可以在各线程间传递,也可以选择私有堆空间)
- 共享区数据(动态库和共享内存通信) 命令行参数和环境变量
各线程还共享进程的资源和环境
- 文件描述符表
- 信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录 用户id和组id
线程也拥有独属于自己的一部分数据
- 线程ID(线程属性结构)
- 独立栈结构(线程属性结构):线程独立执行的重要依据
- errno错误码(线程局部变量,线程属性结构)
- 线程上下文(一组寄存器,PCB数据):线程独立调度的重要依据
- 信号屏蔽字(PCB数据)
- 调度优先级(PCB数据)
两项重要的私有数据:线程上下文数据(线程调度)和栈结构数据(调用函数,开辟栈帧空间,存储临时数据),体现了线程的动态属性。
4.2线程的优缺点
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量,一般进程创建的线程数量和CPU的核数相同。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
【不同冲击下选择更好的方案,并不是线程越多越好,如果只有单一的工作,你创建了很多线程,本来单一的工作循环去做不会花费太多时间,此时由于你交给了很多线程,此时花费时间的是线程的来回切换】合理的使用多线程,能提高计算密集型程序的执行效率,能提高IO密集型程序的用户体验(例如边下边播功能,就是多线程运行的一种表现)
线程的缺点
**性能损失:**一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
**健壮性降低:**编写多线程程序需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
**缺乏访问控制:**在多线程编程中,存在访问控制的挑战。由于多个线程可以同时访问共享的数据和资源,因此需要采取适当的措施来确保线程之间的安全访问。
**编程难度提高:**编写与调试一个多线程程序比单线程程序困难得多
实际上,线程的缺点大部分是可以被避免的,只要程序员能力到位,上述缺点大都可以避免,毕竟,设计者创造“线程”这一概念的初衷是为了提高整机效率
4.3线程的异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出