进程中的程序
==In UNIX, everything is a file== 文件——有"名字"的数据对象
字节流
字节序列
File Descriptor 文件描述符
struct file
的数组==,而fd
则是这里的idx。而struct file
则是存储文件信息的结构体struct file
中的f_pos(文件偏移量),读取偏移count个字节offset 文件偏移量
Handle,Windows下的FD - 在Windows中,handle默认是不继承的(面向工程的设计,=="最小权限原则"==)
pipe() 管道
Unix中的管道符" | "实现
FD适合什么
In UNIX, everything is a file
终端原理
历史上的终端——电子打字机VT100
今天的伪终端
[!NOTE] 终端模拟器 & Shell应用 两者的关系类似硬件和软件,终端模拟器负责键盘输入和屏幕显示。而Shell应用负责指令处理。 在对终端输入指令的时候,终端会把收到的信息传到Shell中,Shell处理后的信息会发送到终端上(也就是Pipe在此处的作用,对接两者的FD stdin和stdout)
终端与应用的配对 终端与应用并不是天然固定连接的(比如终端就是Bash这样),而是负责一个屏幕和键盘的IO功能(输入/输出),也就是可以像一个屏幕连个键盘一样,在上面打开各种应用程序(Bash,Vim),并进行输入
常用的伪终端
在我们对当前终端上的前台应用使用Ctrl + C强制关闭时,我们需要关闭前台应用的所有进程,但是又不能影响后台的进程。也就是说,我们需要一个区分应用进程的方法——==Session和Process Group==
![[Pasted image 20250401093521.png]]
“非必要不实现”(“机制与策略分离”、最小完备性原则) 第一级抽象
对于ELF,本质上是可执行文件的一个==数据载体==,通过在系统内核中的加载器执行
[!quote] ELF 是 Executable and Linkable Format 的缩写,是 Linux 系统上的一种可执行文件格式。
ELF文件中主要包含的信息
ELF内容的数据结构
Core Dump(核心转储)
学前读物,宝宝读了都说好(迫真)结合CSAPP的内容——[[静态链接与加载]]
[!tip] #### More than “可执行文件”
- E = Excutable
- L = Linkable ->
- F = File
链接到加载的三要素
需要的工具
[!tip] 小彩蛋 UNIX系统中通过注释
#!
加载和执行脚本语言
对比维度 | 静态链接 | 动态链接 |
---|---|---|
链接时间 | 编译时完成(编译阶段一次性链接所有依赖库) | 运行时完成(程序启动时或函数调用时动态加载库) |
文件大小 | 可执行文件较大(包含所有库代码) | 可执行文件较小(仅包含库引用,依赖外部库) |
内存占用 | 高(每个进程独立加载库代码,无法共享) | 低(多个进程共享同一份库代码,仅数据段独立) |
启动时间 | 快(无需运行时解析和加载库) | 较慢(需加载和解析库,但可通过延迟绑定优化) |
符号解析 | 编译时完成符号解析和重定位 | 运行时动态解析符号(如Linux的PLT/GOT机制,Windows的延迟绑定) |
依赖管理 | 无外部依赖(自包含,无需额外库文件) | 依赖外部库文件(需确保库版本兼容性和存在性) |
程序更新维护 | 需重新编译整个程序才能更新库代码(维护成本高) | 可独立更新库文件,无需重新编译程序(维护灵活,但需处理版本兼容性) |
安全性 | 更安全(无外部依赖,避免库劫持风险) | 存在风险(如DLL劫持、路径劫持、版本冲突等) |
适用场景 | 嵌入式系统、资源受限环境、需要独立部署的程序(如关键基础设施、恢复工具) | 通用应用程序、服务器环境、需要频繁更新的程序(如Web服务、容器化应用) |
代码共享性 | 代码不共享(每个程序独立携带库代码) | 代码共享(所有进程共享库代码段,节省内存) |
版本兼容性 | 无版本冲突(库代码固定在可执行文件中) | 可能引发版本冲突(需依赖系统库的兼容性管理,如Windows的Side-by-Side机制) |
性能优化 | 无运行时开销(符号解析已完成) | 存在轻微运行时开销(动态加载和解析,但可通过缓存优化) |
[[动态链接]]
读jyy的ld代码
共享库的加载
[!tip]
- 一个动态链接的程序,不能直接从他的a.out开始执行,因为如果从a.out开始执行就需要执行库函数了
- a.out执行的时候,libc还没有加载
libc
的加载步骤
libc
的机制[!important] 多进程共享
libc
的机制 在libc
加载到内存后,多线程共享使用libc
的两种数据,只读数据和可读写数据
- 只读数据 -> 只读共享
- 可读写数据 -> 写时复制(Copy-On-Write) 两者原理详见[[虚拟化#操作系统的Tricks]]
[!example]
- 对于一个100GB的==可读写数组==,在fork时,子进程很难快速复制一个
- 因此,对于这个==可读写的数组==,子进程中对应的fd,会指向父进程的==这个数组==,但只设置为只读
- 当子进程中需要对==这个数组==进行写时,这个数组才会在子进程的空间中被==拷贝==一份
- 而在拷贝后,这个进程之后的==这个数组==读写操作,都在这个==新的拷贝==上进行
[!hint] 共享内存回收
- 页表项中有一个标志位,存储着一些页表信息
- 只读位(Read-Only):标记页面是否可写。
- COW 标志位:标识该页面是否参与 COW 机制(部分系统实现)。
- 引用计数(Reference Count):记录共享页面的引用次数,用于释放物理页。 对于上文中fork()时,父子进程共同指向的物理内存 当这个块内存没有被任何进程使用时才会释放,而不是在父进程关闭后释放
引用计数机制:
- 初始共享:父进程和子进程的共享页引用计数设为 2。
- 写操作触发复制后:新页的引用计数为 1,原页的计数减 1(变为 1)。
- 释放物理页:当引用计数为 0 时,回收物理页。
基本的命令工具,系统内核
先启动内核初始化到初始状态
创建设备文件
加载磁盘