本人志在持续更新计算机系统、计算机网络、C语言的核心知识点的系列合集以易懂、全面的方式讲解底层知识。对于正在准备面试八股的朋友来说本系列涵盖了本人面试中遇到的所有考点以及许多相关拓展知识读完后能帮助你从容面对大部分面试拷打对于想要深入学习计算机知识的朋友来说本系列比较系统地介绍了操作系统和网络等重点内容也举了不少例子大大有助于你从底层的视角去理解计算机系统。 先说明本系列恐怕不是计算机小白或是想速通期末的朋友们的目标它需要一定系统和语言基础也并不是面向教材和考试要求去讲解所以更适合那些实操过代码、了解一些计算机系统知识、并且想要深入底层和扎实基础的朋友们去耐心学习。如果你是这样的人欢迎阅读该系列文章并分享自己的理解或提出文章中的模糊、错误的地方不排除有。 想要阅读系列中其他内容或想要持续关注本系列更新可移步https://github.com/feiyangyang11/Cpp-Core-CS-Interview-Guide.git。进程进程的基本定义进程的经典定义就是系统中正在运行的一个程序实例或者说操作系统对一个正在运行的程序的抽象这里面有三个需要注意的关键词程序硬盘上存储的静态文件如main.exe、python.exe等运行程序被加载进内存CPU 开始执行它的指令抽象操作系统给这个运行中的程序包装出一套“独立运行环境”让它感觉自己独占 CPU、内存等资源举例你打开两个浏览器窗口底层把浏览器程序加载到内存运行出两个浏览器进程为什么需要进程CPU只有有限的核心不可能让所有程序一直同时跑所以就需要把每个程序包装成一套单独上下文包含程序代码、数据、栈、程序计数器……作为CPU调度的单位也就是进程所以进程至少承担了三件事资源分配单位CPU调度单位之一程序的隔离边界进程的组成部分一个进程的上下文至少包括程序代码段数据段包含bss和data堆段栈段CPU 上下文打开的文件虚拟地址空间进程控制块 PCB进程的用户态虚拟地址空间分区低地址0x00000000│ │ 代码段 text存放程序机器指令只读、可执行 │ ├──────────────────── │ 只读数据段 rodata存放字符串常量、const全局常量等 │ ├──────────────────── │ 已初始化数据段 data存放已初始化的全局变量、静态变量 │ ├──────────────────── │ 未初始化数据段 bss存放未初始化或初始化为0的全局变量、静态变量 │ ├──────────────────── │ 堆 heap malloc/new申请的动态内存向高地址增长 ↓ │ │ │ 共享库/mmap 区域 动态库、文件映射、匿名映射等 │ │ ├──────────────────── │ 栈 stack局部变量、函数参数、返回地址、保存的寄存器向低地址增长 ↑ │ ├──────────────────── │ 命令行参数、环境变量argc/argv/envp 等 │ 高地址进程的完整虚拟地址分区用户态虚拟地址是面试常考的部分但除了用户态进程还有内核态的虚拟地址空间以下列出的是进程的完整虚拟地址空间虚拟地址从低到高 ──────────────────────────────────────────── 0x00000000 │ │ 用户空间 User Space │ 每个进程相对独立 │ │ ┌────────────────────────────────────┐ │ │ NULL / 保留区 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ text 代码段 │ │ │ 程序机器指令 │ │ │ PC/RIP 的值通常指向这里 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ rodata 只读数据段 │ │ │ 字符串常量、只读常量 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ data 已初始化数据段 │ │ │ 已初始化全局变量、静态变量 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ bss 未初始化数据段 │ │ │ 未初始化或置 0 的全局/静态变量 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ heap 堆 │ │ │ 用 malloc/new 动态申请内存 │ │ │ 向高地址增长 ↓ │ │ └────────────────────────────────────┘ │ │ 空闲区域 │ │ ┌────────────────────────────────────┐ │ │ mmap 映射区 │ │ │ 动态库、文件映射、匿名映射、共享内存 │ │ └────────────────────────────────────┘ │ │ 空闲区域 │ │ ┌────────────────────────────────────┐ │ │ user stack 用户栈 │ │ │ 局部变量、函数参数、返回地址、栈帧 │ │ │ 向低地址增长 ↑ │ │ │ RSP/ESP 用户态时通常指向这里 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ argv / envp / auxv │ │ │ 命令行参数、环境变量、辅助向量 │ │ └────────────────────────────────────┘ │ ├──────────────────────────────────────────── │ │ 内核空间 Kernel Space │ 普通用户程序不能直接访问 │ │ ┌────────────────────────────────────┐ │ │ 内核代码段 │ │ │ 操作系统内核自己的机器指令 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ 内核数据段 │ │ │ 内核全局数据结构 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ PCB / task_struct │ │ │ 进程控制块 │ │ │ 保存 pid、状态、调度信息、内存信息等 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ kernel stack 内核栈 │ │ │ 进程进入内核态后使用 │ │ │ 系统调用、中断、异常处理时使用 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ 页表相关结构 │ │ │ 维护虚拟地址到物理地址的映射 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ 文件描述符表 / file 对象 / socket │ │ │ open/read/write/socket 等内核资源 │ │ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ 调度队列、信号结构、定时器等 │ │ │ 操作系统管理进程所需的其他结构 │ │ └────────────────────────────────────┘ │ 0xffffffff ──────────────────────────────────────────── 高地址有几个地方需要注意只读数据段rodata是程序加载时就确定好的静态区域为什么需要它为了防止常量被错误修改、让多个位置只共享同一份只读数据以优化性能例如下述代码会报错Segmentation fault因为hello这个字符串字面量放在rodata段而rodata段的内存页权限是只读的char*phello;//正确p[0]H;//报错再例如下述代码不会报错因为这里是用字符串字面量hello初始化一个数组因此修改的也是数组副本里的字符chararr[]hello;arr[0]H;内核数据段内核数据段全局只有一份实例它被所有进程共享不是每个进程创建时拷贝一份内核数据段存的是内核的全局状态 系统运行中创建的一堆内核数据结构如CPU核数、系统时间、调度队列、物理内存入口、文件系统入口等举个例子每个进程就像教室里的学生内核数据段就像教室里的黑板黑板记录着值日名单、课程列表等所有学生通过这块统一的黑板知道班级的总体状况。整个教室只有一个黑板实例学生自己不持有黑板但知道黑板在哪以及怎么看黑板进程控制块PCBPCB实际是一个概念名而不是一块具体的内核内存空间它本身放在内核空间的动态内存区这个在内存管理篇会细讲。所以上面内核虚拟地址空间的图略不准确主要是为了便于理解进程主要结构用于描述和管理进程的核心数据结构保存了一个进程运行所需的全部上下文信息直接或间接保存它保存几类信息CPU上下文、进程标识信息、进程调度信息、进程资源信息保存方式或结构有两种直接保存保存资源本身直接保存了进程信息、内核栈指针等间接保存保存资源引用虚拟内存空间指针、页表基址、文件描述符表基址等内核栈kernel stack内核栈 这个进程在内核态执行时的函数调用历史 CPU现场保存区 临时变量一般存放用户态 RIP返回地址、用户态 RSP、通用寄存器保存区、内核函数调用栈帧、文件描述符、锁状态等进程内核态工作流程如下进程运行在用户态发生系统调用/中断进入内核态切换栈关键令RSP 当前进程的 kernel_stack_top保存用户态现场将RIP、RSP、通用寄存器等压入内核栈执行内核函数如read()等每次函数调用都在内核栈上push / pop返回用户态恢复寄存器、RIP、RSP等回到用户态现场页表它的实际位置和PCB一样在内核空间的动态内存区系统以页通常4KB为基本单位拆分物理内存和虚拟内存然后用页表建立虚拟地址 → 物理地址映射。地址的高位作为页号用来定位页的位置而低位作为页内偏移量用来定位页内数据具体位置每个进程有自己独立的页表简化后的访存流程拿到虚拟地址 -取虚拟地址的高位作为虚拟页号去页表查到对应的页表项含物理页号、状态位等 -如果查到用物理页号虚拟地址低位组合成物理地址 -根据物理地址去查物理内存中的具体数据实际系统访存是比较复杂的会先查TLBTLB失效了才查页表实际页表也通常设计为多级页表、按需创建否则单张页表太大塞不下进程能做到地址空间隔离和扩展就是靠页表机制简单来说每个进程有自己的页表因此在它看来自己就拥有了整片内存空间可以访问内存的所有位置。但这些地址是虚拟地址要经过页表转换成物理地址才能去访问物理内存。而实际物理内存也不会把所有空间提供给一个进程而是在某进程访问这片空间时才准备一页物理空间并放好它需要的数据。举个不恰当的例子物理内存就像房子进程就像租户有个租户白天上班晚上睡觉有个租户白天睡觉晚上上班两个租户不会同时住在这个房子里就像两个进程不能同时被一个CPU核心运行并访问内存房东告诉两个租户你们都是这房子唯一租户两个租户也都以为这个房子任何时间都只属于自己一个人。而实际上居住情况并非如此但是居住效果是一样的文件描述符表FD table它的实际位置和PCB一样在内核空间的动态内存区文件描述符表是每个进程在内核空间中维护的一张“索引表”用来实现 **fd整数 → file内核对象**的映射进程打开或创建一个文件时会返回一个文件描述符fd它表示进程能以fd为下标去进程的文件描述符表中访问对应的文件对象伪代码类似FILE file fd_table[fd]file 是一个数据结构表示该文件在该进程中的实例或状态记录文件当前读写位置、文件打开模式、inode指针等inode 是文件本体的元数据即文件自身属性记录文件大小、文件类型、磁盘位置等进程访问文件过程就是fd-file-inode-data(文件的数据块可能在磁盘或已经加载到内存中)举个例子进程是校长文件是小明fd表示小明的全校排名fd_table就是成绩榜单file是小明的学号、班级、姓名等学生信息inode就是小明本人和个人信息小明家就是文件的数据。校长说要去家访全校第一名但他不知道全校第一是谁于是他去成绩榜单看谁是第一个学生看到了小明的信息按照信息找到了小明本人问了小明个人基本信息然后去和小明去他家里家访用户的堆/mmap会在其他篇讲解用户态与内核态为了更好更安全的进程抽象CPU和操作系统为应用程序提供了两种运行模式用户态与内核态。用来限制应用程序可以执行的指令和可以访问的地址空间范围。CPU通过某个控制寄存器中的一个模式位来描述进程所处的模式当设置了模式位时进程运行在内核模式可以执行指令集中任何指令可以访问系统中任何内存位置未设置模式位时进程运行在用户模式不允许执行特权指令不允许发起I/O也不允许直接引用地址空间中内核区的代码和数据。必须通过系统调用接口间接访问内核代码和数据应用程序的进程初始是运行在用户态中的进程从用户态变为内核态唯一方法是通过中断、故障、陷入系统调用就是一种陷入等异常机制。异常发生时传递到异常处理程序CPU将用户态变为内核态进行处理返回应用程序代码时CPU把运行模式又变回用户态CPU内部有各种寄存器CPU 内部 ┌──────────────────────────────┐ │ PC / RIP │ │ 保存进程下一条要执行的指令地址 │ │ 它的值通常指向 text 段 │ ├──────────────────────────────┤ │ SP / RSP │ │ 用户态时指向 user stack │ │ 内核态时指向 kernel stack │ ├──────────────────────────────┤ │ BP / RBP │ │ 指向当前进程的栈帧基址 │ ├──────────────────────────────┤ │ 通用寄存器 │ │ RAX、RBX、RCX、RDX... │ ├──────────────────────────────┤ │ 状态寄存器 │ │ 保存当前进程的条件码、中断状态等│ └──────────────────────────────┘进程的状态创建态进程刚被创建开始准备PCB等资源就绪态进程准备就绪放入操作系统的就绪队列等待CPU调度运行态进程在CPU上执行。一个CPU核心同时只能运行一个进程阻塞态进程因等待某事件磁盘IO、网络数据、锁、sleep……而暂停执行退出CPU终止态进程执行完毕或被杀死操作系统回收资源存在以下转换规则新建态 - 就绪态 就绪态 - 运行态 运行态 - 就绪态 运行态 - 阻塞态 阻塞态 - 就绪态 运行态 - 终止态进程创建的过程创建流程内核分配 PCB填写如pid、状态、优先级、父进程等基本信息创建内核栈建立用户虚拟地址空间继承/复制页表、文件描述符表等资源把进程加入就绪队列Linux系统中通过fork()创建进程系统调用pid_t fork()用于创建当前进程的子进程返回值是子进程PIDfork()出的子进程复制了父进程的运行环境包括代码段、数据段、堆、栈、文件描述符表等但有不同的PCB、内核栈、调度信息等最关键的是fork 之后父子进程从 fork 返回的位置继续向下执行值得注意的一点是现代操作系统为了节省开销在fork后通常不会立刻复制出一份独立的内存空间给子进程而是父子进程共享同一物理内存页并且页只标记为只读。当某一方尝试写入这一页后会触发缺页异常操作系统再复制这个页面让父子进程拥有自己独立的物理内存页副本Linux系统中通过exec()运行新程序exec()不创建新进程而是用一个新程序替换当前进程的地址空间代码、数据、堆、栈……shell中运行程序shell命令就是forkexecwait的具体实践shell本身是一个进程等待用户输入。用户通过shell输入一个可执行目标文件或一个命令的的名字比如ls。那么shell就会调用fork()创建一个子进程然后调用execl(/bin/ls, ls, -l, NULL);让这个子进程执行/bin/ls这个可执行文件。而父进程shell在fork后会执行wait()等待这个命令执行完毕并回收子进程什么是进程上下文切换什么是进程的CPU上下文指的是一个进程在 CPU 上运行到某一刻时CPU 里保存的那一整套“执行现场”操作系统必须保存进程的 CPU 上下文这样若进程被切换走再切换回来时把这些值恢复回 CPU进程就能像没被打断一样继续执行常见上下文包括1. PC / RIP / EIP程序计数器 2. SP / RSP / ESP栈指针 3. BP / RBP / EBP栈帧基址指针 4. 通用寄存器 5. 状态寄存器 / 标志寄存器 6. 页表相关寄存器 7. 内核态相关寄存器1.PC程序计数器Program Counter保存进程下一条要执行的指令地址。2.SP栈指针Stack Pointer保存当前栈顶位置它会随着局部变量、函数的出栈入栈而不断变化以调用某个函数func()举一个简单例子调用func -进入funcSP中存func的最初地址 -定义局部变量a变量a入栈SP向低地址增长 -函数返回整个func函数栈弹出SP指向调用func前的栈顶相关的崩溃问题栈溢出stack overflowSP不断向低地址增长到了非法内存访问野指针函数返回局部变量指针外部访问了该指针指向的内存但该指针指向内存已失效未定义行为访问已经结束生命周期的对象属于未定义行为——函数返回后尝试访问已失效的内存可能得到残留旧数据可能产生数据覆盖也可能报错Segmentation fault3.BP栈帧基址指针Base Pointer保存当前函数栈帧基址BP是相对固定的只有产生函数调用/返回时才发生变化依旧以调用某个函数func()举例在main中运行PC指向main代码指令BP指向main基址SP指向当前栈顶 -调用func将返回地址压入栈下一条main指令 -进入func把旧BP入栈保存旧BP让BP指向当前SP的位置建立新的函数栈帧 -执行funcPC指向func代码段指令SP不断变化为局部变量分配空间 -返回func恢复栈顶让SP指向当前BP指向的地址让BP指向旧BP指向的地址PC指向返回地址对应的指令 此时只是栈寄存器指向的栈地址发生了变化func函数栈帧中仍有旧数据但已经不可访问4.通用寄存器用于保存计算过程中的临时数据、函数参数、返回值等5.状态寄存器 / 标志寄存器存 CPU 当前的一些状态比如是否进位、是否溢出、CPU 权限等6.页表寄存器CPU 访问虚拟地址时通过页表翻译成真正的内存物理地址页表寄存器存的就是当前页表的物理基址页表本体在内存中进程上下文切换的流程进程运行时进程的CPU上下文在CPU内部的寄存器和执行单元中其他资源则在内存中切换进程时会发生1.CPU进入内核态系统调用发生CPU进入内核态2.换出当前进程的CPU上下文CPU上下文就是上述的寄存器等。发生切换时系统会把这些寄存器里的信息拷贝到进程的内核地址空间中主要是PCB和内核栈中PCB存放进程状态、调度位置、内核栈指针等进程的管理信息内核栈存放PC、BP、SP、通用寄存器等寄存器现场过程参见前述内核栈的内容3.更新进程状态running - ready/blocking4.调度器选择下一个进程5.恢复新进程的CPU上下文从新进程内核空间中取出task_struct / kernel stack恢复RIP、RSP、RBP等寄存器现场6.CPU返回用户态执行新进程进程上下文切换的开销有哪些上下文切换本身不做业务计算但会消耗 CPU 时间。频繁上下文切换会降低系统性能上下文切换设计1. 保存当前进程寄存器 2. 恢复另一个进程寄存器 3. 切换内核栈 4. 切换页表 5. 可能导致 TLB 失效 6. CPU cache 命中率下降 7. 调度器本身也要执行代码 8. ……主要的开销1.页表切换与TLB失效TLBTranslation Lookaside Buffer是虚拟地址翻译的高速缓存它把最近用过的地址翻译结果缓存起来可视作CPU内部的小型页表CPU需要访问内存时先尝试使用TLB翻译虚拟地址如果无法查到才去页表寄存器里获取页表基址再去内存里查页表当进程切换后页表寄存器的内容也发生了切换导致新进程运行时TLB可能频繁失效增加了数据访问开销2.CPU cache 命中率下降CPU cache是CPU内部存储热点数据的高速缓存以cache line64B内存块为基本单位页通常是4KBCPU要访问内存时先得到实际物理地址然后去cache中检查是否命中如果命中直接使用该行数据否则再去访存当进程切换后由于cache存储的大部分是旧进程的热点数据所以cache命中率大大下降增加了数据访问开销3.寄存器切换寄存器切换开销主要有两点当前进程的 CPU 执行现场寄存器状态保存到内存再从内存恢复另一个进程的寄存器状态所带来的直接开销。代价较小CPU 执行状态重置如流水线清空、TLB失效等。代价较高4.内核栈切换进程间的通信方式进程间通信 IPC就是让两个或多个进程在彼此独立的地址空间之间交换数据、同步状态、协同完成任务。比如同主机的跨进程通信、前后端通信、后端与数据库通信、不同后端服务间的通信……由于每个进程的用户地址空间是隔离的所以必须通过内核提供的机制进行通信常见 IPC 分类 ├── 管道 pipe ├── 命名管道 FIFO ├── 消息队列 message queue ├── 共享内存 shared memory ├── 信号 signal ├── 信号量 semaphore ├── 套接字 socket └── 文件 / mmap 等间接方式pipe 和 FIFOpipepipe管道是内核维护的一块**“环形缓冲区”**。之所以用引号是因为这块缓冲区并非严格意义上的环形而是通过首尾双指针模拟环形通过pipe()函数创建。创建 pipe 后内核会准备好一段缓冲区并返回两个 fd fd[0] 和 fd[1]它们实际作为了该 pipe 的读写入口——fd[0] 是读端fd[1] 是写端可以理解为进程A 内核 进程B │ │ ├── fd[1]→ FILE对象 → 写入 →[pipe buffer]→ 读取 ← FILE对象 ← fd[0]//pipe是单向字节流即读端只能读写端只能写且没有数据边界需要注意的是pipe只能用于父子进程通信因为 fork 出的子进程和父进程的文件描述符表中那两个 fd 会指向同一个内核 pipe 缓冲区pipe 还具有阻塞机制缓冲区满时写端write()阻塞缓冲区空时读端read()阻塞FIFOFIFO命名管道顾名思义相比 pipe 唯一的区别就是有具体路径名字能让无亲缘关系的进程通信在操作系统的文件系统中FIFO被视为一个文件如 /tmp/myfifo类型是FIFO且读写权限为prw-r--r--通过mkfifo(myfifo, 0666)函数创建第一个参数是管道名称第二个参数是管道文件权限。由于 FIFO 本身相当于一个文件所以不同进程可以通过打开/读取/写入文件的方式来进行通信。但它仍然和 pipe 一样是单向字节流只维护内核缓冲区不会落到磁盘二者区别pipefd → file → pipe_buffer没有文件系统入口 FIFOfd → file → inode文件系统节点 ↓ pipe_buffer怎么使用intfd[2];pipe(fd);//创建pipewrite(fd[1]...)//写入read(fd[0]...)//读出//FIFO用法差不多只是创建补充struct file借此契机讲一下 struct file 到底是什么角色在涉及文件处理时我们常会看到它Unix/Linux把进程能操作的东西都统一抽象成文件文件描述符 fd 和 struct file 就是具体实现。fd不细讲了就是进程层面对一个文件的标识仅仅是一个整数file 是一个文件层面的通用数据结构代表着“一次打开”比如一个文件在一个进程中被打开两次就代表着两个文件实体在内核中表现为fd1 → file对象1 → inode(a.txt) fd2 → file对象2 → inode(a.txt)而 file 对象本身记录的是这一次打开的状态比如文件引用计数有多少fd指向了这个file对象、操作权限能读还是写、文件指针当前在文件的哪个位置读写、操作函数表引用指向底层对象的操作函数集合的指针、具体底层对象引用指向底层对象的指针等等这些都是通用状态不管打开什么东西都需要维护这些信息因此抽象出来放到 struct file非通用的是操作函数表引用和具体底层对象引用以 pipe 为例fd → file ├── f_op → pipe_file_ops └── private_data → pipe对象pipe_file_ops 是指针指向了一个函数表里面提供了操作 pipe 对象的所有函数。当上层调用read(fd)时file 会实际调用 pipe 对象自己的操作函数表里的read()private_data是指针指向了内核创建的那个 pipe 对象里面就是具体的数据或者说就是 pipe_buffer消息队列 mq内核维护的一条“按消息为单位组织的队列”message queue进程 A 往队列里放消息进程 B 从队列里取消息与 pipe 比较的话pipe 是字节流传输缓冲区内的多次写入会一并被读出而 mq 一次写只能写入一条消息一次读也只能读出一条消息即每一条消息是完整独立的单位不会被拆也不会粘连需要注意的是消息被读取后就被消费掉了即从消息队列中移除mq 在内存中本身是一个链表连接着多个 msg 节点。当它被创建时内核会赋予它一个 msgid并在内核数据结构的全局 IPC 表中建立一个msgid - struct msg_queue的映射。后续程序操作这个消息队列就通过 msgid 去查表访问key_t keyftok(file1,1);//利用文件名为该消息队列创建一个内核中的唯一编号作为可被多个进程识别的公共keyintmsgidmsgget(key,0666|IPC_CREAT);// 创建消息队列设定创建权限和创建选项msgsnd(msgid,msg,sizeof(msg.mtext),0);// 写入消息msg结构体第一个成员必须是long long类型表示消息type后续的 成员数量和类型都可以自定义msgrcv(msgid,msg,sizeof(msg.mtext),1,0);// 读出第一条类型为 1 的消息这里的msg是接收缓冲区msgctl(msgid,IPC_RMID,NULL);// 删除消息队列局限性性能不高涉及两次或更多拷贝写入拷贝、读出拷贝pipe 也有这个问题队列大小和消息大小有限吞吐率低不支持分布式共享内存多个进程的虚拟地址映射到同一段物理内存页实现通过内存直接通信不涉及数据拷贝是性能最高的一类 IPC简单说就是弄了一块可以被多个进程直接共用的物理内存创建机制和 mq 很像。当它被创建时内核赋予它一个 shmid这是操作系统层面对共享内存的唯一标识而 key 则是应用程序层面的唯一标识同时全局 IPC 表中维护一个shmid - shm 对象的映射shm对象又保存这块共享内存的元信息和指针但是映射和使用阶段不一样。映射阶段依旧使用shmid去查 shm 对象然后建立虚拟地址-共享内存的页表项再返回该虚拟地址给应用程序使用阶段直接通过 MMU 和页表翻译虚拟地址把这块共享内存当自己的内存使用shmat()这才是共享内存中的核心。它做的事情是通过 shmid 查询 IPC 表从而找到 shm 对象保存 shm 的元信息进行权限检查等操作并获取 shm 的物理页集合因为共享内存的物理页通常不是一整段物理内存然后在当前进程用户虚拟地址空间找一段空闲区域通常在堆和栈中间的 mmap 区建立一段虚拟内存 VMA然后在页表中建立虚拟内存-shm物理内存的映射表项可能立即建立也可能后续通过缺页异常延迟按需建立最后把这块虚拟内存起址作为返回值进行返回怎么使用intkeyftok(shmfile,1);//按照文件名生成唯一keyintshmidshmget(key,1024,0666|IPC_CREAT);// 创建共享内存char*addr(char*)shmat(shmid,NULL,0);// 把内核中已经创建好的共享内存段映射到当前进程的用户虚拟地址空间 中并返回一个可以直接读写的用户态地址strcpy(addr,hello);// 写入共享内存printf(%s\n,addr);// 读取共享内存shmdt(addr);// 解除映射shmctl(shmid,IPC_RMID,NULL);// 删除共享内存可能的问题不像 mq 和 pipe 读写时依靠内核函数write() 和 read()能保证数据同步、防止并发写坏共享内存由于相当于进程访问自己的用户地址多进程可能同时访问一块共享内存会有并发安全问题所以一般不会裸用而是由程序员自行设计搭配信号量、互斥锁 、条件变量等机制来进行同步以最经典的共享内存信号量生产者/消费者模型为例为共享内存设计一个数据结构当shmat()返回共享内存的指针时强转类型为SharedData*把这片内存当成一个结构体进行操作补充一点sem_wait()是 P 操作表示在只有信号量当前值大于 0 时将它 -1否则阻塞sem_post()是 V 操作将信号量1并唤醒一个阻塞的 P 操作structSharedData{sem_t empty;// 表示缓冲区是否为空可以写。empty 1一开始缓冲区是空的可以写sem_t full;// 表示缓冲区是否有数据可以读。full 0一开始没有数据不能读sem_t mutex;// 互斥锁保护共享数据区。mutex 1锁未被占用一次只允许一个进程操作共享数据intlen;charbuffer[BUFFER_SIZE];};写进程怎么做sem_wait(shm-empty);// 等待可写即等待缓冲区为空sem_wait(shm-mutex);// 缓冲区为空后立刻加锁// 写共享内存strcpy(shm-buffer,hello);//写入数据shm-lenstrlen(shm-buffer);//记录写入数据的长度sem_post(shm-mutex);// 解锁sem_post(shm-full);// 通知有数据可读读进程逻辑相反差不多略套接字SocketSocket 是最通用的 IPC方式之一它是内核提供的通信端点进程通过它发送和接收数据。它既能用于本机进程通信也能用于不同机器之间通信可以理解为socket 就是两个进程的一对邮箱发消息前先放到自己的邮箱再发到对方邮箱对方也从邮箱读取消息它在内核中的链路是socket_fd → file → socket对象 → 协议栈缓冲区socket_fd 通过进程的文件描述符表定位到 file 对象进而定位到socket 对象进而使用读/写缓冲区TCP socket这块主要是计算机网络的内容会在Socket篇细讲这里只从操作系统层面进行理解TCP socket 是面向连接的套接字对应的地址族通常是 AF_INETipv4、通信方式是 SOCK_STREAM 字节流它在内核中的核心对象有struct socket包含通信模式、socket状态和指向struct sock的指针、struct sock包含地址四元组、读/写缓冲区、序号seq、窗口大小、拥塞控制状态、重传定时器、协议操作函数……统称为 socket 对象服务端socketsocket()//设定地址族和通信模式内核创建一个socket对象返回fd它在应用程序层面的唯一标识↓bind()//将socket与本地某个 ip port 进行绑定表示占用本机的某个端口等待别人连接。内核也会维护一个记录socket-port的表↓listen()//设定监听队列大小让socket进入监听状态。队列有两类半连接队列正在握手、全连接队列连接已建立↓accept()//找到socket的监听队列从全连接队列中取出一个已经建立的连接为这个连接建立新的file和fd若无连接则默认阻塞↓ read/write 或 recv/send//以写为例拷贝数据到内核发送缓冲区-根据 MSS、窗口等切分数据-构造TCP报文-IP层-网卡驱动发送↓close()//file引用计数-1socketfd引用计数-1引用计数归零就触发四次挥手、回收资源客户端socketsocket()//设定地址族和通信模式内核创建一个socket对象返回fd它在应用程序层面的唯一标识↓connect()//根据sockfd找到socket若socket没有bind本地端口和ip那么内核自动分配。根据传入的远端ip和端口发起三次握手↓ read/write 或 recv/send//以写为例拷贝数据到内核发送缓冲区-根据 MSS、窗口等切分数据-构造TCP报文-IP层-网卡驱动发送↓close()//file引用计数-1socketfd引用计数-1引用计数归零就触发四次挥手、回收资源UDP socket是无连接的套接字地址族通常是 AF_INETipv4、通信方式是 SOCK_DGRAM 数据包不同于TCP它是有消息边界的使用的读写函数是sendto/recvfrom表示一包一包写/一包一包读并且 UDP 没有连接状态机、没有三次握手、没有可靠重传、没有按序保证其内核 socket 对象单纯维护端口、地址、缓冲区等信息没有复杂连接信息UNIX domain socket对应地址族是 AF_UNIX通信模式为字节流或数据包非常常用它看起来像网络 socket但不经过 IP 和网卡以内核统一视角看三者结构TCP socket 是fd → file → socket → sock → TCP 协议栈 → IP 层 → 网卡 UDP socket 是fd → file → socket → sock → UDP 协议栈 → IP 层 → 网卡 UNIX domain socket 是fd → file → socket → unix_sock一个特殊的 inode → 本机内核 socket 缓冲区流程和 TCP 类似//服务端↓socket()bind(fd,/tmp/server.sock)//绑定的不是 IP:port而是文件路径若没有就创建并把路径对应的 inode 与unix_sock 绑定listen()accept() send/recv//客户端↓socket()connect(fd,/tmp/server.sock);send/recv简单来讲原先创建了 fd 和 socket 时进程可以通过 fd 查询自己的文件描述符表找到 socket 对象但无法跨进程查使用 bind 创建一个 socket 类型的文件并把它和 socket 对象绑定其他进程就可以通过文件路径查目录查到文件的 inode而 inode 指向的就是这个 socket 对象。既能使用 fd - file - socket 对象来定位又能使用 path 目录 - inode -socket 对象来定位mmap / 文件 / 信号 / 信号量mmap不细讲了它可以看成基于文件的共享内存不过它是靠 fd → file → inode → page cache → 映射到多个进程也需要同步机制具体机制是mmap(fd)-把文件内容映射到进程虚拟地址空间char*pmmap(fd)-进程直接通过指针访问文件最朴素的IPC也就是普通的文件打开和读写但是实时性差、效率低、复杂不细说了信号信号是一种事件通知一般不是为了传数据它是内核给进程发送的一个异步事件通知主要特点是轻量、异步具体机制是进程 Akill(pid,sig)//sig是信号值通常就是一个整数↓ 进入内核 ↓ 找到目标进程 PCB/task_struct ↓ 在目标进程的 pending signal 集合里标记该信号//如果blocking signal中标记了该信号表明进程屏蔽了这个信号信号直接失效↓ 目标进程从内核态返回用户态前内核安排执行 signal handler//触发信号处理函数如果进程没有注册直接触发进程退出信号量不是用来传数据而是用来同步和互斥通常搭配共享内存使用参见讲共享内存时的举例完结请等待后续更多知识更新