5 内核原理
约 3961 字大约 13 分钟
2025-06-18
进程的状态的分类:
- 初始态
- 就绪态
- 运行态
- 阻塞态
- 终止态
- 僵死态
Linux系统中对进程状态的标记
标记 | 对应的状态 |
---|---|
D | 不可中断睡眠状态(通常是在进行IO操作) |
I | 空闲的内核线程,使用ps -ef查看的进程中带尖括号的 |
R | 运行或可运行状态(在运行队列中),即抽象模型的运行态和就绪态,程序在操作系统中在就绪态和运行态高速切换的,如果使用两个标记分别标记切换速度会非常快,标记也就失去了作用 |
S | 可中断的睡眠状态(等待事件完成被唤醒) |
T | 由工作控制信号停止 |
t | 在跟踪过程中由调试器停止 |
W | 分页(2.6.xx内核版本开始弃用了) |
X | 死亡状态,PCB被清理. 由于非常短暂,不可能被看到 |
Z | 僵死态,已经终止但未被父进程回收 |
二者的对应 |
抽象理论 | Linux实现 |
---|---|
初始态 | 这个阶段通常很短暂,不对应于Linux的特定进程状态。 |
就绪态 | 对应于R |
运行态 | 对应于R |
阻塞态 | D、S、T、t均属于阻塞态 |
僵死态 | Z |
异常和中断
- 内核态: 内核态是CPU的一种运行模式,具有执行所有指令和访问所有硬件资源的权限。在这种模式下,操作系统内核执行其核心功能。所有与硬件交互的操作都必须在内核态下执行。由于具有完全的系统控制权,任何在内核态执行的代码都必须是高度可靠的,以避免系统崩溃或安全漏洞。
- 用户态: 用户态是CPU的另一种运行模式,权限受限。应用程序在用户态下运行,不能直接执行特权指令或访问受保护的内存区域。
- 特权指令: 特权指令是指只有在内核态下才可以执行的指令。这些指令提供了对硬件和关键系统资源的直接控制能力,因此它们的执行被严格限制在操作系统内核中,以防止恶意软件或错误的程序代码破坏系统的稳定性和安全性。
中断和异常的分类
大类 | 子类 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|---|
中断(Interrupt) | 中断 | 来自硬件的信号 | 异步 | 总是返回到下一条指令 |
异常(Exception) | 陷阱 | 进程主动触发的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 | |
终止 | 不可恢复的错误 | 同步 | 不会返回 | |
中断(Intermupt)通常是IO设备或时钟触发的,信号来自处理器外部,不是由任何条指令造成的,从这个角度讲,它是异步的。中断处理完毕后总是执行下一条指令。 |
陷阱是由进程执行陷入指令(可以切换封内核不态的指含)主动触发的,如:在调用系统调用时,主动触发,从用户态切换到内核态,暂停当前代码的执行. 是同步的,执行完
中断和异常区别在于:中断处理例程被调用时,CPU会清除EFLAGS 寄存器中的IF(Interrupt Enable)位,避免其它中断干扰当前中断处理例程的执行。而异常处理例程被调用时IF不会被清除。
fork底层执行
PCB的具体实现
struct task_struct{
struct thread_info thread_info; // 执行环境的必要信息
...
void* stack; // 内核栈
...
unsigned intflags; // 标志位信息菜
...
/* Filesystem information: */
struct fs_struct* fs; // 文件系统信息
...
struct mm_struct* mm; // 内存管理信息(包含指向内存区描述符的指针
...
/* Openfile information: */
struct files_struct* files; // 包含指向打开的文件描述符表的指针
...
pid_t pid; // 进程ID
pid_t tgid; // 线程组ID
/* objectiveandrealsubjectivetaskcredentials(cow): */
const struct cred __rcu* real_cred;//真正的证书,包含用户和用户组的信息
/* Effective(overridable)subjective task credentials (cow): */
const struct credrcu* cred; //当前有效的证书
...
/* Signal handlers: */
struct signal_struct* signal; //与信号处理相关的信息,关注信号状态管理
struct sighand_struct_rcu* sighand; //与信号处理函数相关的信息
父进程在调用fork创建子进程时子进程和父进程的虚拟地址和物理地址都是一样的如: thread_info实例进程基本信息, mm_struct实例记录地址空间信息, files_struct实例文件描述相关信息, signal_struct实例信号控制信息等信息 它们都是指向同一个地址,只有在一方对数据进行更改的适合才会把数据再复制一份.
写时复制机制(Copy on WriteCOW)可以提高进程创建效率。子进程完整地复制了父进程的地址空间,此时父子进程的虚拟内存空间映射到相同的物理内存空间。只有当二者之一执行了写入操作才会复制写入区域的内容,为父子进程维护不同的物理页帧
execve底层执行
execve首先在用户态对参数进行检验
- 参数和环境准备 内核检查传递给execve()的参数,包括可执行文件的路径、环境变量和命令行参数,以确保它们的有效性和安全性。这个阶段内核会在内核空间中准备一份新程序需要的命令行参数和环境变量的备份。
- 打开和验证可执行文件 打开指定的二进制文件,验证其格式是否支持(例如,ELF格式),并检查执行权限。如果这一步找不到可执行文件的路径,就会直接终止。
这两步参数校验成功后才会切换到内核态开始执行以下步骤 3. 创建新的内存映射 清除进程当前的内存映射,包括用户空间中的代码、数据、堆和栈。根据新的程序建立新的代码段、数据段、堆和栈等。 内存映射不包含内核空间,内核空间的映射是由操作系统内核管理的,对所有进程是共享的。execve 切换的只是用户空间。也就导致它的pid没有变化 4. 复制参数和环境变量 在新的地址空间中为命令行参数和环境变量分配空间,并将内核中它们的备份复制到新的位置。 5. 初始化进程上下文 设置新的程序计数器、栈指针等,以便新程序可以正确执行。清理和重设进程的各种内核资源,如文件描述符表。根据文件描述符的 close-on-exec标志(FD_CLOEXEC)进行处理,如果有该标志,则文件描述符被关闭。 6. 更新task_struct和其他内核结构 更新 task_struct 中关于进程地址空间、堆栈、命令行参数、环境变量的指针。重置信号控制信息到默认状态。清理进程的各种内核状态,如未处理的信号、定时器等。
每个进程组都有一个进程组组长,其进程ID(PID)与进程组ID(PGID)相同。进程组组长是最先创建的进程,但它退出并不意味着进程组的消失。只要进程组中还有其他进程,该进程组就会继续存在。 可以向一个进程组中的所有进程发送信号,而不是单独向每个进程发送
进程切换
- 进程切换的场景 如果进程的运行不会被打断,那么操作系统内核想要回CPU的控制权就只能寄希望于进程主动归还,或者强制重启计算机。现代计算机提供了中断和异常机制,二者都可以打断正在执行的进程,将CPU的控制权交还给内核。进程的切换需要内核介入,必然要通过中断或异常来实现。进程切换主要在以下几种情况下发生。
- 时钟中断触发,被中断的进程获得的CPU时间片耗尽,操作系统决定切换进程。
- 当前进程发生故障,内核夺回CPU控制权,如果故障无法被修复,则内核终止该进程,切换至其它进程。
- 时钟中断触发,当前进程在等待IO操作,为避免资源浪费,切换至其他进程。
- 时钟中断触发,高优先级进程处于就绪状态,内核将CPU使用权由当前进程转交给高优先级进程。 进程执行io操作是交由内核线程来执行的, 但是在它进行io是cpu不会去操作io相关的用户态的代码,所以说io操作是阻塞的
系统调用
通过系统调用可以和系统内核进行交互,系统调用就是一种异常(陷入)由用户态切换到内核态 用户在程序中执行系统调用,CPU会执行陷入指令切换到内核态,执行相应的异常处理程序,实现用户进程对于内核功能的调用。 系统调用运行在内核态,具有访问硬件和管理系统资源的权限。涉及用户态到内核态的切换,执行开销较大。
库函数通常是用高级语言编写,库函数存储在函数库中,其开销相比于系统调用是比较小的 库函数也是封装了系统调用和一些用户态的代码, 其效率和系统调用相差无几,用户态的代码对效率影响很小,主要开销都是在系统调用时由用户态切换到内核态, 但库函数功能更加强大也更灵活
线程 线程被称为轻量级的进程,它的创建和上下文切换的开销小于进程 lwp:LowWeightProcessID,轻量级线程ID,Linux中等同于线程ID。 tid:Thread ID,线程ID。 tgid::ThreadGroupID,线程组ID。
在Linux中,线程等同于轻量级进程,二者都有独立的task_struct结构体实例。线程创建和进程创建在技术上是完全等同的, fork()和进程创建函数pthread_create()底层都调用了系统调用clone()。
创建进程 当我们调用fork()时,等同于调用
clone(SIGCHLD,0)
,SIGCHLD
标志的作用是告诉操作系统:当子进程终止时,父进程应当接收到SIGCHLD
信号。这个信号是默认的方式,用于通知父进程其子进程已经结束。这样一来,父进程就可以在子进程退出后执行清理操作。创建线程 创建线程时,底层会调用
clone(CLONE_VM丨CLONE_FS丨CLONE_FILES丨CLONE_SIGHAND,0)
,这些Flag 的含义如下。
CLONE_VM
:共享地址空间。从技术上将被创建的线程和创建者的struct task_struct
实例中struct mm_struct
指针类型的字段mm
和active_mm
指向相同的实例。CLONE_FS
:共享文件系统信息。从技术上将,被创建的线程和创建者的struct task_struct
实例中struct fs_struct
指针类型的字段fs指向相同的实例。CLONE_FILES
:共享打开的文件描述符表,从技术上讲,被创建的线程和创建者的struct task_struct
实例中struct files_struct
指针类型的字段files
指向相同的实例。CLONE_SIGHAND
:共享信号处理函数表。从技术上讲,被创建的线程和创建者的struct task_struct
实例中struct signal_struct
指针类型的字段和struct sighand_struct
指针类型的字段指向相同的实例。
从 clone()系统调用的角度,我们可以得出结论:如果多个进程共享了地址空间、文件系统信息、打开的文件信息、信号处理信息,那么他们就是同属于一个进程的线程。
task_struct结构体中的pid实际上表示的是进程或线程ID,tgid字段表示的是线程组ID,等同于传统意义上的线程ID。对于单线程的进程,pid字段和tgid字段相同。对于多线程进程,主线程的 pid等于 tgid,pid此时可以理解为主线程的线程 ID 或者进程 ID,普通线程的tgid等于主线程的pid和tgid,即当前线程所属的线程组 ID 和进程 ID,而 pid字段此时相当于线程ID。
内核空间
32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间; 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
内核空间与用户空间的区别: 进程在用户态时,只能访问用户空间内存; 只有进入内核态后,才可以访问内核空间的内存;
用户空间内存从低到高分别是 6 种不同的内存段:
- 代码段,包括二进制可执行代码;
- 数据段,包括已初始化的静态常量和全局变量;
- BSS 段,包括未初始化的静态变量和全局变量;
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关 (opens new window));
- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;
这种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。 64位系统的寻址空间比较大,所以仍然沿用了32位的经典布局,但是加上了随机的mmap起始地址,以防止溢出攻击
malloc申请内存原理
malloc() 并不是系统调用,而是 C 库里的函数,用于动态分配内存。
malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。
- 通过 brk() 系统调用从堆分配内存
- 通过 mmap() 系统调用在文件映射区域分配内存; 方式一实现的方式很简单,就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间。如下图:
方式二通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。如下图:
malloc() 源码里默认定义了一个阈值: 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存; 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存; 注意,不同的 glibc 版本定义的阈值也是不同的。
malloc(1) 会分配多大的虚拟内存 malloc() 在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。 具体会预分配多大的空间,跟 malloc 使用的内存管理器有关系
贡献者
版权所有
版权归属:PinkDopeyBug