Unix源码分析

霍林老师是我在大学中给我影响也相当大的一位老师。在给我们上数据结构和操作系统的时候,她展现了对于计算机的透彻理解,她的教学目的是让我们把所学知识融会贯通,并且将思想融入到各个领域。霍老师曾用战场比喻计算机技术,战车、兵马、枪炮是数据结构,而高效利用它们取得胜利的则是战术,也就是算法。
在我阅读ATT&CK一书后,昔日恩师所说的话冒上心头,感慨计算机确实是万变不离其宗。在霍老师的汇报上,老师向我们介绍了我国信息安全产品,也给我们灌输了信安的观念,从底层原理展开描述了老师对安全的理解。我思考,不想做脚本小子,要依靠自己的能力构建安全长城,就要真正从底层理清楚框架,从学习操作系统开始。
我选择学习Unix源码,他是众多os的鼻祖,尽管距离它出现已过去几十年,技术也趋于老旧,但是如今的linux、macos等等都是基于unix实现的,因此学习它很有必要。霍老师打趣道,现在给我们上课的内容还是以前她学过的内容,越是底层的东西越难发生大的变化。而且unix源码10000行左右,我有信心读完,这篇文章既是学习笔记也是备忘录,更是自己主动学习的有力证明。

我先做的,是先通读《unix内核源码剖析》

可以查看源码的网站

一、unix v6

unix诞生在1969年,V6在1975年发布

unix v6内核提供的功能:
①管理运行的进程
②内存管理
③文件系统
④文件和周边设备共享I/O
⑤中断
⑥支持终端处理

在学习操作系统的时候,对于进程管理、内存管理、文件系统等就有所学习,不过距离现在时间过得久了,印象不深了,现在就是重学习的时候。

构成unix v6运行环境的硬件:
①PDP-11:这是一个处理装置,书中以PDP-11/40这个16位计算机作为示例。也就是说它以16bit为单位对数据进行处理。处理器处理数据的单位是:字(word),一个字=16bit

PDP-11/40通过unibus进行数据的输入输出,使用内存映射(Memory Mapped I/O)人们可以对操作系统的寄存器进行操作。注意,这些寄存器被映射到内存最高位的8KB空间,内存地址是从上到下看的,最上端是0,最下端是最高位。

②PSW状态字寄存器
PSW有16位。从0到15代表不同的状态

③通用寄存器
有R0到R7一共八个通用寄存器

④MMU内存管理单元
用于地址变换以及访问权限管理

⑤内存
PDP-11/40的内存被称为磁芯内存(Magnetic Core Memory)地址长度18bit,容量为218=256KB

⑥块设备
磁盘或者磁带

⑦行式打印机

⑧终端

二、进程

进程是程序资源分配的最小单位,线程是cpu调用的最小单位。进程由多个线程构成,切换进程开销大,而且资源不共用(可能会死锁),切换线程开销小,因为共用资源。在java中多线程用thread,我曾经写过的。
进程跟线程都可以分为五个状态:创建、阻塞、就绪、执行、终止。

内核先把程序读入内存,然后把这个内存区域分给进程,所以进程拥有独立的虚拟地址空间,可以先使用虚拟内存的地址,再由MMU内存控制单元映射到实际的物理地址上。
回忆进程的组成:PCB进程控制块(包含进程ID)、数据段、正文段。

进程是并行执行的,unix v6允许多名用户同时使用,所以在任意时刻,系统中可能有多个进程。回想学习操作系统和计算机组成与原理的时候,cpu会分时调度的,只需要以人不能感知的速度反复切换进程,就能产生同时操作的效果。分时系统(Time Sharing System,TSS)

用户模式和内核模式
处理器具有两种模式:用户模式、内核模式。处理器通过PSW来进行切换(PSW的13-12表示先前、15-14表示当前,00表示内核模式,11表示用户模式)
虚拟地址在用户模式的时候会映射到用户程序的内存区域;在内核模式会映射到内核程序的区域。内存映射的实现依靠MMU内存管理单元。内核程序在系统启动的时候就已经被读取到内存中
用户程序无法访问加载内核程序的内存空间,要想访问内核功能,必须通过系统调用(system call)提出访问请求(这是强制的)。当内核处理结束又会返回用户模式。
书中提到几个用于用户空间与内核空间之间读写数据的函数:
fubute()、fuibyte()、fuword()、fuiword()、subyte()、suibyte()、suword()、suiword()

交换处理
为了防止进程过多而瘫痪内存,内核会定期休眠,将重要度低的进程从内存转移到交换空间(swap out,换出),或者将交换空间中已经处于执行状态的进程重新恢复到内存(swap in,换入),这个过程称为交换处理,由系统启动时生成的进程执行。在操作系统学习的时候,对内存管理比如动态分区,有:
·首次适应性算法
·循环首次适应算法
·最佳适应算法
·最坏适应算法

proc结构体和user结构体
这俩结构体管理进程的控制信息和状态信息,PROC结构体常驻内存,USER可能会被移至交换空间
PROC结构体
由proc结构体组成的数组proc[],里面每个元素都对应一个进程。这个结构体管理着在 进程状态、执行优先级等与进程相关的信息中需要经常被内核访问的那部分信息
每当进程要切换的时候,内核会首先检查所有进程的状态,所以为了保证效率,proc结构体常驻内存。proc[]的长度决定了在系统中可以同时存在的进程的上限,proc[]的长度由常量NPROC定义。
proc[]中的一些元素,可以与学习操作系统时的知识串在一起,比如P_time是进程在内存或者交换空间存在的时间(秒),对缺页中断的置换算法:最佳置换(opt)、先入先出置换(FIFO)、最近最久未使用置换(last recently used,LRU),进程在内存中获得资源的时间就会被作为依据进行置换算法的判定。对于一些根据优先级判断的算法,proc[]中的P_pri越小优先级越高,用户可以通过修改P_nice的值来自定义优先级,默认为0。

【进程图像】:包含两个部分,一个是常驻内存图像,如PROC[];另一部分是可交换图像(swappable image),如PPDA、数据区域、栈区域等,可以被交换到磁盘上

USER结构体
用来管理进程打开的文件或者目录等信息。不像PROC结构体常驻内存,内核只需要当前正在执行的进程的USER结构体,所以它会跟着进程切换而发生改变,比如被移出内存。
书本给出一些名词:
口口口口 实效用户、实效组、实际用户、实际组
也被称为 有效用户、有效组、真实用户、真实组
我的理解是,当前登录的用户是真实用户,但是它的权限可以发生变化,比如su命令提高权限,它的实效就会变高,但是实际用户没有发生变化。

内核可以根据全局变量”U“来访问 进程的user结构体

进程内存分配
进程的构成除了PCB还有数据段和代码段。数据段和代码段是作为两个连续的物理内存区域被分配给进程,进程通过虚拟地址访问被分配的物理内存区域。
代码段:
代码段是只读的,用来存放程序指令的机器代码。用于复用代码,提高效率。它通过数组text[]进行管理,长度由user.u_tsize表示。
数据段:
存放程序使用的变量等数据,它不能被其他进程共享

数据段

就如上图所表示,数据段的物理地址和长度分别由proc.p_addr和proc.p_size表示。数据段从上到下(地址从低到高)表示成三部分:
①PPDA(per process data area)进程数据区:
·user结构体和内核栈区域构成
·长度为USIZE×64byte=1KB,从用户空间无法访问
·内核栈区域是内核处理时的临时工作区域,每个进程都有供内核模式使用的工作区域
②数据区域:
由存放全局变量或BSS等变量的区域和进程来动态管理内存的堆区域构成。
【bss段】用来存放未初始化的全局变量和静态变量。
如果要扩展堆区域,要使用系统调用完成,从虚拟地址的低位到高位方向进行(从上到下),长度由user.u_dsize表示。
③栈区域:
用来暂时存放函数的参数或者局部数据,长度会自动扩展。栈区域的扩展是从虚拟地址的高位往地位低位方向(从下到上)进行,长度由user.u_ssize表示。

虚拟地址空间
进程拥有64KB的虚拟地址空间,通过长度为16bit的虚拟地址访问物理内存,虚拟地址由MMU内存控制单元转换为长度18bit的物理地址。
也就是说本来虚拟地址为216=64KB,转换为物理地址为218=256KB
代码段位于虚拟地址最低位的地址,往后是数据段,在往后是栈区域,栈区域在虚拟地址空间最高位。
每个进程都有独立的虚拟地址空间,但是也会有共i想的代码段。

使用虚拟地址的好处:

①程序可以使用 以任意地址为起点的内存空间:
不需要考虑该虚拟地址和物理地址的对应关系,MMU内存管理单元会解决这个问题。
②实现对内存访问的管理:
不同于直接操作实际地址可能会误操作,使用虚拟地址如果越权访问会通过MMU触发异常,终止进程处理。
③提高内存使用率:
通过映射有效利用分段的碎片的物理地址。
【但】对于unix v6,代码段和数据段是与物理地址存在对应关系的

变换地址
MMU内存管理单元通过APR(Active Page Register)寄存器将虚拟地址转换成物理地址。
一个APR由一个PAR(page address register)寄存器和一个PDR(page description register)寄存器构成。
用户进程的APR的值保存在USER结构体中,当该进程进入执行态,会将保存在USER结构体中的值设置到用户进程的APR中。
APR从0到7一共有8组,PDP-11/40对于用户模式和内核模式都有对应的APR,通过切换PSW的当前模式(00或者11)切换APR。
psw:一共16位;左边是最高位(15、14),右边是最低位(0)。根据15、14的情况判断模式。
进程的虚拟地址空间是以 segment段或page页来管理的。1组APR对应一页。PAR用来保存与各页物理地址有关的信息,PDR用来保存各页的块(64byte为单位)的数量以及是否允许访问等信息。每一页最多分配128个块(8KB)
虚拟地址最左边的高3bit决定对应的页(APR),PAR(16位)的11-0bit决定了物理地址的基址地址的块地址,加上虚拟地址的12-6bit的物理内存的块地址,再加上虚拟地址的5-0bit作为块内偏移地址,得到最后的物理地址。

说人话就是:
①拿到一个虚拟地址,先看15、14、13bit找到对应的APR[i];
②再根据虚拟地址的12、11、10、9、8、7、6这7位得到APR[i]中的PAR块的编号;
③将该块的11、10、9、8、7、6、5、4、3、2、1、0这12bit作为物理地址的基址地址;
④虚拟地址的5、4、3、2、1、0这6位作为块内偏移量(也就是物理块的块内偏移量);
⑤这样得到17-6的物理地址基址地址+5-0的物理地址块内偏移量,得到一共18bit的物理地址

在之前操作系统的学习中关于分页管理学过的,有一个页表,页表里面是页号和块号的对应方式,一页是1024,如果逻辑地址是1011,页表中页号0对应块号2,那么转换为物理地址的过程为:1011÷1024=0,0对应块号为2,那么物理地址基址为块号×1024即2×1024=2048,偏移量为1011,所以对应的物理地址为2048+1011=3059

【内核之所以可以通过全局变量U(0140000)来访问执行进程的USER结构体,是因为内核对供内核使用的APR做了相对应的设定】内核将内核模式使用的编号为6(也就是APR[6]/PAR[6])的PAR设为被执行进程的数据段的物理地址(proc.p_addr)
这是由于地址0140000(oct八进制)的高位3bit是6(十进制),如下图

140000的进制

[书中统一将内核模式使用的APR成为内核APR,用户模式使用的就是用户APR]

三、进程管理

1.进程的生命周期:
①父进程通过 系统调用 fork 创建子进程(子进程复制了父进程的数据);
②父进程执行 系统调用 wait,开始休眠等待子进程处理结束;
③子进程获得控制权,然后通过 系统调用exec,将程序读取到内存并且执行;
④子进程对应的程序执行完后,会通过 系统调用exit 结束自身运行进入僵尸状态,将控制权归还父进程;
⑤父进程取得子进程的结果,并且清理子进程。

2.进程创建:
①子进程复制父进程的数据
·复制proc[]数组元素,并且子进程的proc.p_ppid指向父进程的proc.p_pid
·复制数据段。包括PPDA(per process data area);子进程的user.u_procp指向proc[]中代表子进程的元素;子进程和父进程共享text[]中的相同元素

②父进程与子进程的联系:
·父进程只能通过遍历所有proc.p_ppid指向父进程的进程才能找到所有它的子进程。
·父进程会回收子进程结束后释放的资源
·子进程继承父进程打开的文件和当前目录等数据
·父进程和子进程共享代码段。但是如果子进程执行其他程序,这种共享关系会解除。
·父子进程互不干扰

【unix v6允许父进程介入子进程处理的跟踪机制,用管道,也就是“|”来实现父子进程通信】
【面试常问进程通信:管道、信号、信号量、共享内存、sokect】

③系统调用 fork
fork是依靠内核进程处理系统调用

——到这里暂时看不懂了,看看汇编—–

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注