Skip to content

内核

约 4233 个字 23 张图片 预计阅读时间 14 分钟

内核的职责: - 进程调度:对于多任务操作系统,内核负责对多个进程进行管理和调度,达到多任务同时运行的效果; - 内存管理:内核提供虚拟内存管理机制可以让有限的RAM同时运行更多的软件,也可以让不同进程之间、进程与内核之间的内存隔离; - 文件系统:提供文件系统供应用程序使用; - 设备管理:屏蔽各类设备的实现差异,封装简单的接口供应用程序使用; - 提供系统调用API:供应用程序开发各类功能;

内核态和用户态: 执行硬件指令可使CPU在两种状态之间切换,与之对应虚拟内存空间也划分为用户态空间和内核态空间。 - CPU处于用户态时,CPU只能访问标记为用户空间的内存,试图访问属于内核空间的内存会引发硬件异常; - CPU处于内核态时,CPU既能访问用户空间的内存,也能访问内核空间内存;

文件系统

linux文件类型: - 普通文件 - 设备 - 管道 - 套接字 - 目录 - 符号链接 unix系统I/O模型通过一套系统调用(open/read/write/close)适用于所有的文件类型。就其本质而言,内核只提供一种文件类型:字节流序列,在处理磁盘文件、或磁带设备时,可通过lseek系统调用来随机访问。unix系统没有文件结束符的概念,读取文件时如无数据返回,便会认为抵达文件末尾。

/proc文件系统 - 是一种虚拟文件系统,/proc 下的目录和文件可以分为两类:进程相关目录系统全局信息文件 / 目录。 - 系统全局信息:这类文件 / 目录提供整个系统的硬件、内核、资源等全局信息,与具体进程无关。 - 进程相关信息:系统中每个运行的进程,都会在 /proc 下对应一个以 PID 命名的目录;

系统调用 - 所有系统调用进入内核的方式相同,内核是通过系统调用编号区分 - 系统调用是依赖软中断完成用户态到内核态的切换,在x86-64架构下,通过syscall指令触发软中断,CPU收到软中断信号后,保存用户态上下文,切换到内核态。每个进程都有用户栈和内核栈。进程执行用户代码 → 用用户栈;进程触发系统调用 / 软中断 / 硬中断 → 切换到内核态 → 用内核栈。 - 硬中断(如网卡、磁盘中断)的处理函数,不一定使用当前进程的内核栈:如果中断发生时,CPU 正在执行用户态代码 → 使用当前进程的内核栈;如果中断发生时,CPU 正在执行其他中断的内核代码 → 使用内核预分配的中断栈,避免破坏原有的内核栈上下文。

文件描述符与打开文件之间的关系: - 进程A的fd0和进程B的fd3虽指向不同的文件句柄,但是最终指向相同的i-node表项,可能是不同进程打开了同一个文件,实际上同一个进程两次打开同一个文件也是这种现象,即对应不同fd以及不同的句柄,拥有各自的文件偏移量; - 进程A中,fd1和fd20指向同一个文件句柄,可能是调用dup、dup2、fcntl形成的; - 进程A的fd2和进程B的fd2虽属不同进程,但指向同一个文件句柄,可能是调用fork后出现的;
image.png

文件的三层映射架构,中间的系统级文件表主要承载进程对文件操作的动态信息,而系统级i-node表主要承载文件本身静态信息,为什么一定要设计中间层?可以将这层合并到进程级的文件描述符表中吗?答案是如果这样设计,就没法做成多进程共享操作同一个文件,例如nginx的master进程在open打开日志文件后,通过fork出不同的子进程,使得不同子进程的fd指向同一个系统级文件表项,共享同一个文件偏移量,即不同worker进程都可以有序往同一个日志文件追加信息。 另外,为什么nginx多进程往同一个文件写日志,不会出现重叠或覆写?Nginx 打开日志文件时,会指定O_APPEND标志(对应追加模式),Linux 内核保证:每次write()系统调用会先将文件指针移到末尾,再写入数据,这两步是原子操作,不会被其他进程打断。 image.png

有了上面的理解,其实经常能见到的shell语句2&1,旨在将标准错误的fd2重定向到标准输出的fd1,其底层原理是将fd2指向的文件句柄指向fd1,达到2个fd共享同一个文件偏移操作同一个i-node的效果,该能力可以通过调用dup完成。

I/O缓冲区: 下图概括了 stdio 函数库和内核所采用的缓冲(针对输出文件),以及对各种缓冲类型的控制机制。从图中自上而下,首先是通过 stdio 库将用户数据传递到 stdio 缓冲区,该缓冲区位于用户态内存区。当缓冲区填满时,stdio 库会调用 write()系统调用,将数据传递到内核高速缓冲区(位于内核态内存区)。最终,内核发起磁盘操作,将数据传递到磁盘。图中左侧所示为可于任何时刻显式强制刷新各类缓冲区的调用。图右侧所示为促使刷新自动化的调用:一是通过禁用 stdio 库的缓冲,二是在文件输出类的系统调用中启用同步,从而使每个 write()调用立刻刷新到磁盘。 注意:write()会将数据立即传入内核高速缓冲区,不涉及stdio提供的stdio缓冲区这一层。

image.png image.png

如下例子提供理解: image.png

直接 I/O:绕过缓冲区高速缓存 始于内核 2.4,Linux 允许应用程序在执行磁盘 I/O 时绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备。有时也称此为直接 I/O(direct I/O)或者裸 I/O(raw I/O)。有时会将直接 I/O 误认为获取快速 I/O 性能的一种手段。然而,对于大多数应用而言,使用直接 I/O 可能会大大降低性能。这是因为为了提高 I/O 性能,内核针对缓冲区高速缓存做了不少优化,其中包括:按顺序预读取,在成簇(clusters)磁盘块上执行 I/O,允许访问同一文件的多个进程共享高速缓存的缓冲区。应用如使用了直接 I/O 将无法受益于这些优化举措。直接 I/O 只适用于有特定 I/O 需求的应用。例如数据库系统,其高速缓存和 I/O 优化机制均自成一体,无需内核消耗 CPU 时间和内存去完成相同任务。

文件系统、虚拟文件系统VFS、虚拟内存文件系统: TODO

进程

  • 内存布局:文本段(代码段,物理内存只读可共享)、数据段(静态变量)、堆、栈
  • 内核通过父进程fork创建子进程,后者会从前着继承数据段、栈段、堆段的副本,在写时拷贝(COW)保存父子进程的内存空间隔离;实际上大多是请求,创建子进程后,子进程会调用execve系统调用加载并执行一个全新程序,其效果是销毁继承的文本段、数据段、栈段以及堆段,至于环境变量是否继承可以通过evecve入参控制。
  • init进程:是系统启动后第一个被内核创建的用户空间进程,PID始终是1,PPID为0(由内核直接创建),作为所有用户空间进程的“祖先”,① 负责启动系统服务,处理 shutdownreboot 等信号,响应关机、重启请求;②管理孤儿进程等,当一个进程的父进程退出后,该进程会成为“孤儿”,此时 init(PID=1)会自动收养它,并在其终止时回收资源(调用 wait() 防止僵尸进程)。
  • 僵尸进程:进程已经执行完毕、资源(如内存、文件描述符)已被释放,但它的**进程控制块(PCB,每个进程都有一个task_struct结构体,记录了进程的状态、PID、父进程 PID、退出状态等关键信息)** 仍保留在系统的进程表中,内核保留 PCB 的目的,是让父进程通过 wait()/waitpid() 等系统调用获取子进程的退出状态。如果父进程**没有调用这些回收函数**,子进程就会一直以僵尸状态存在于进程表中。wait是阻塞调用,waitpid可以搭配SIGCHLD使用达到非阻塞回收的效果。
  • 僵尸进程跟孤儿进程是两码事,前者是子进程已经exit了但是PCB没有被父进程调用wait回收,后者是是父进程提取exit了子进程处于孤儿进程会被init进程托管。 image.png

内存映射:

静态库和共享库 - 静态库:优点是模块化开发,缺点是编译链接时静态库内容会被拷贝到各个可执行文件中,存在冗余且静态库修改后需要重新链接编译; - 共享库:共享库不会将目前模块复制到可执行文件中,而是在可执行文件中写入一条记录,表明在运行时需要使用该共享库;共享库的代码段和数据段都都位于可执行文件的mmap区,其中代码段对应的物理内存是只读可执行、且支持进程共享以及地址无关,而数据段时可读可写不可执行,且进程私有写时拷贝,即共享库在可执行文件中虚拟内存布局位于mmap区,不论是代码段还是数据段其虚拟内存空间都是进程独立的,但是代码段在物理内存空间是多进程共享的,两个段不与可执行文件自身的代码段或数据段耦合。共享库支持进程启动时动态加载和运行过程中通过libdl提供的dlopen等api进行运行时加载,dlopen内部通过open、mmap等系统调用完成共享库加载。

进程间通信IPC - 信号:也成为软中断,信号本质是异步打断机制:会暂停进程的当前执行流,处理完信号后进程通常会恢复运行。信号从产生直至送达进程期间,一直处于挂起状态,通常系统会在接收进程下次获得调度时将处于挂起状态的信号同时送达,如果接收进程正在运行,则会立即将信号送达。 - 管道 - 套接字 - 信号量 - 共享内存

进程的内存布局: - .data段:包含显式初始化的全局变量和局部变量 - .bss段:包含未显示初始化的全局变量和局部变量 将.data和.bss分开存放,主要原因是考虑减少可执行文件的大小,.data 段会把变量的初始值实实在在地存在二进制文件里,占用磁盘空间;而 .bss 段只记录 “需要预留多大空间”,程序加载到内存时,操作系统才会按这个大小分配内存,并自动将这些内存初始化为 0(整数 0、浮点 0、指针 NULL 等)。

虚拟内存技术很大程度是利用了程序访问局部性这一特征(空间局部性、时间局部性),虚拟内存将内存切割成大小固定的页,同样物理内存页也是相同的大小。任意时刻,每个程序仅有部分页需要驻留在物理内存页帧中,这些页构成了驻留集(resident set),程序未使用的页拷贝保存在交换区(swap area)内,当进程欲访问的页面未驻留在物理内存中,将会发生page fault,内核即刻挂起进程的执行,同时从磁盘将该页面载入内存。 内核通过cat /proc/sys/vm/swappiness控制swap页面置换的门限,默认为60,即当物理内存使用率超过40%,内核遂开始启用页面置换算法(例如LRU)决定将一些不活跃页换出。 swap是基于磁盘的因此IO开销大,当swap也被用尽,则会触发OOM Killer机制,OOM Killer 会根据进程的内存占用、优先级(oom_score),自动杀死占用内存最多的进程(通常是导致内存溢出的程序),以释放内存。

brk/sbrk/malloc/free: - program break 是进程虚拟地址空间中堆区的 “结束地址(堆顶)” —— 它是内核为进程划分的 “已分配堆内存” 和 “未分配虚拟内存” 的边界; - 在堆上申请和释放内存,本质上是调整program break指向的内存水位,linux提供了brk和sbrk来修改其指向; - 仅依靠program break机制使用堆内存,无可避免会出现内存碎片问题,而brk和sbrk自身也不拒绝解决内存碎片的能力,glibc提供了malloc和free接口封装了brk/sbrk,malloc等内存分配器通过内存池、空间块链表、块合并和复用策略、堆顶空闲空间回收等机制来解决内存碎片问题; - sbrk是无状态的系统调用,无法感知堆内存是已分配还是已释放,即其完全由入参传入的调整量来执行操作;类似于malloc/free分配器才会感知管理内存状态以及大小; image.png image.png image.png

clone接口: clone() 是 Linux 内核提供的创建轻量级进程 / 线程的底层接口,是 fork()、vfork() 甚至 pthread 线程库的实现基础,它允许精细控制父子进程(或线程)之间共享的资源(如内存空间、文件描述符、信号量等),是 Linux 进程 / 线程模型的核心底层工具。 image.png image.png image.png

inotify机制: inotify 是 Linux 内核提供的异步文件系统事件监控机制,用于让用户态程序实时感知文件 / 目录的变化(如创建、删除、修改、移动等),替代了传统低效的 poll/select 轮询方式,是实现文件监控、热更新、自动备份等功能的核心技术。 image.png

信号

信号是事件发生时对进程的通知机制。有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程。 kill命令这是管理进程的核心工具,本质是向指定进程 / 进程组发送信号(其他进程、当前进程、进程组),而非直接 “杀死” 进程(仅当发送终止类信号时才会终止进程)。 linux也提供相应的kill系统调用。raise系统调用仅针对当前进程自己,底层原理是封装了kill系统调用,即raise(sig) 等价于 kill(getpid(), sig)。 每个进程都具有一个信号掩码,代表阻塞一组信号。可以通过sigpending系统调用查看当前处于阻塞的信号集,由于该信号集其实也是一个掩码只能表征哪些信号阻塞了并不能表征这些信号期间阻塞的次数,即同一个阻塞的信号后续只会被处理一次; image.png

sigsuspend() 系统调用 —— 这是 Linux 中安全的信号等待机制,核心作用是:临时替换进程的信号阻塞集,然后阻塞进程直到收到一个未被阻塞的信号(并处理该信号),处理完成后恢复原阻塞集。它解决了 pause() + sigprocmask() 组合的 “竞态条件” 问题,是信号处理中更可靠的阻塞等待方式。 image.png

并发机制

image.png image.png image.png image.png

互斥量

image.png

TODO:底层原理是什么?如何阻塞?如何通知的?

条件变量

互斥量解决的是多线程并发读写的安全问题,条件变量解决的是多线程下协作的通知和等待问题。两者是配合关系,解决的不同问题。条件变量典型应用在生产消费问题上。 image.png image.png

自旋锁

image.png