Linux操作系统分析总结
0.前言
Linux是一个相当复杂的系统软件,学习起来可以说是十分困难。在阅读完部分孟宁老师的《庖丁解牛Linux》后,对内核的实现有了进一步的理解,但仍有部分细节一知半解,于是结合网上哈工大李治军的课程,对具体的实现代码进行剖析,用断断续续几个月的时间写了下面的总结。
1.Linux操作系统概览
1.1 自由软件江湖里的码头和规矩
-
自由软件运动
-
Linux发行版本
- Linux内核
- GNU的基础库及应用软件
-
GNU
- 一个类unix的自由软件
- 完全免费
- 等价于GNU is Not Unix
-
GPL与商业软件
- 新增代码采用相同的许可证
- 类似于共产主义,各取所需
- 商业软件类似于资本主义
-
GPLV2 and GPLV3
- 只解决版权问题,不解决专利问题
- GPLV3解决了这一问题
1.2 与Linux的一次亲密接触
-
为什么学习linux
- 应用广泛
- 具有悠久的历史、稳定的接口
- 服务器开发和嵌入式开发应用广泛
-
什么是linux
- 类unix系统
- 可移植
- 通常指内核
-
黑客 vs 骇客
- 黑客 -> 创造者
- 骇客 -> 破坏者
-
linux目录文件结构
1.3 常用linux命令
-
系统查看
- uname:打印系统信息
- logname:打印用户名
- df:列出磁盘情况
- env:查看环境变量
- getconf:获取系统信息
-
用户与用户组
- root用户
- 虚拟用户
- 普通用户
-
文件操作
- cd、ls、mkdir、mv、cp、rm、pwd、find
- grep
- tar
- ssh
- sshd
-
练习
tar | find /etc -name '*.conf' grep ubuntu *.conf > a.txt
2.计算机系统的基本原理
2.1 存储程序
-
冯诺依曼结构
-
将程序和数据放在存储器中
-
不断地取址执行
-
-
哈弗结构
- 将程序和指令分开存储
- 数据放在存储器中
- 程序放在ROM中
2.2 复杂指令集和精简指令集
- CISC 复杂指令集
- x86
- RISC 精简指令集
- MIPS ARM64 RISCV
2.3 计算机存储系统
- 寄存器
- 缓存
- 内存
- 固态硬盘
- 硬盘
- 分布式存储
2.4 x86汇编基础
-
cpu寄存器
- 16位
- AX,BX,CX,DX: 数据寄存器
- SP,BP,SI,DI : 指针寄存器
- IP,FLAGS : 控制寄存器
- CS,DS,SS,ES : 段寄存器
- 32位
- EAX,EBX,ECX,EDX
- ESI,EDI,EBP,ESP
- ES,CS,SS,DS,FS,GS
- EIP
- EFLAGS
- 64位
- 新增R8-R15
- 由E变成了R
- 16位
-
寻址方式
- 寄存器寻址
- 立即寻址
- 直接寻址
- 间接寻址
- 变址寻址
-
堆栈操作
-
函数调用和返回
2.5 内嵌汇编
2.6 指令乱序和线程安全
-
三个层次
- 多线程业务逻辑层面函数可重入和线程安全
- 编译器编译优化导致指令乱序
- cpu乱序执行指令的问题
-
可重入函数:
- 可以由多一个任务并发而不担心数据错误
- 可在任何时间中断
-
线程安全
- 多个线程运行代码和单个线程是一样的
-
线程安全和可重入函数
- 可重入不一定是线程安全的
- 不同的可重入函数在多个线程并发使用会有安全问题
- 不可重入一定不是线程安全的
- 可重入不一定是线程安全的
2.7 编写一个最精简的内核
-
虚拟一个x86-64的cpu硬件平台
- 虚拟一个x86-64的cpu
- 用linux内核源代码把cou初始化配置好时钟和程序入口
-
如何在精简内核上实现进程切换?
- 线程的定义
struct Thread { unsigned long ip; unsigned long sp; }; // 每个进程一个线程 typedef struct PCB{ int pid; volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ char stack[KERNEL_STACK_SIZE]; // 线程 入口 下一个PCB struct Thread thread; unsigned long task_entry; struct PCB *next; }tPCB; void my_schedule(void);
- 初始化进程
void __init my_start_kernel(void) { int pid = 0; int i; /* Initialize process 0*/ task[pid].pid = pid; task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; task[pid].next = &task[pid]; /*fork more process */ for(i=1;i<MAX_TASK_NUM;i++) { memcpy(&task[i],&task[0],sizeof(tPCB)); task[i].pid = i; task[i].state = -1; task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; task[i].next = task[i-1].next; task[i-1].next = &task[i]; } /* start process 0 by task[0] */ pid = 0; my_current_task = &task[pid]; asm volatile( "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */ "pushq %1\n\t" /* push rbp */ "pushq %0\n\t" /* push task[pid].thread.ip */ "ret\n\t" /* pop task[pid].thread.ip to rip */ : : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ );
- 自己的进程代码
void my_process(void) { int i = 0; while(1) { i++; if(i%10000000 == 0) { printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); if(my_need_sched == 1) { my_need_sched = 0; my_schedule(); } printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); } } } // 每过1000个tick进行一次线程切换 void my_timer_handler(void) { if(time_count%1000 == 0 && my_need_sched != 1) { printk(KERN_NOTICE ">>>my_timer_handler here<<<\n"); my_need_sched = 1; } time_count ++ ; return; }
-
调度函数
void my_schedule() { printk(KERN_NOTICE ">>>my_schedule<<<\n"); /* schedule */ next = my_current_task->next; prev = my_current_task; if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ { my_current_task = next; printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); /* switch to next process */ asm volatile( "pushq %%rbp\n\t" /* save rbp of prev */ "movq %%rsp,%0\n\t" /* save rsp of prev */ "movq %2,%%rsp\n\t" /* restore rsp of next */ "movq $1f,%1\n\t" /* save rip of prev */ "pushq %3\n\t" "ret\n\t" /* restore rip of next */ "1:\t" /* next process start here */ "popq %%rbp\n\t" : "=m" (prev->thread.sp),"=m" (prev->thread.ip) : "m" (next->thread.sp),"m" (next->thread.ip) ); }
3.Linux内核源码阅读、调试、启动过程
3.1 Linux内核源代码描述
- dirver:驱动 56%
- arch: cpu体系 架构 16%
- init:初始化,存放内核初始化代码
- main.c中start_kernel是起点
- kernel:包含有内核代码
- mm内存管理
- fs文件系统
- ipc进程通信
- net网络
3.2 编译配置安装Linux内核的步骤
- 安装开发工具
- 下载内核源码
- 准备配置文件
- 配置内核选项
- 编译内核
- 安装模块
- 安装bzlmage
- 生成根文件系统
- 编辑bootloader启用新内核
4.深入理解系统调用
4.1 三个地址的转换
逻辑地址 -> 线性地址 -> 物理地址
4.2 系统调用是如何实现的?
-
用户为何不能随意调用数据,也不能随意的jmp
- 这样做,用户可以看到root密码并修改它
- 别人可以看到word的内容
-
将内核程序和用户程序分离
- 0代表核心态 1,2代表OS服务 3代表用户段
- DPL(当前特权级) >= CPL
- 0代表核心态 1,2代表OS服务 3代表用户段
-
如何进入内核?中断。系统调用的核心是什么
- 用户程序中一段包含有int指令的代码
- 操作系统写中断处理,获取想调用程序的编号
- 操作系统根据编号执行代码.
4.3 系统调用是如何实现的?
-
用户调用printf -> 调用库函数printf() -> 调用write函数
-
-
printf展开成int 0x80
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HBIByjQs-1657077112481)(https://gitee.com/fragile_xia/git_test/raw/master/7.6/4.png)]
-
中断处理system_call
-
查表sys_call_table __NR_write = 4
-
sys_write 系统调用(文件系统)
4.4 whoami如何实现?
- 应用程序调用whoami
- 设置函数表索引72,
- 宏替换后,进入80中断,输入返回值和输入值,设置DPL=3
- 进入set_system_gate(0x80, &system_call),
- 将system_call的函数地址填写到0x80对应的中断描述符中
- 根据索引eax找出系统调用函数sys_whoami
- printk(a, b)实现打印函数
5.进程的创建描述和切换
5.1 进程是什么?和静态的程序有何区别?
- 正在执行的程序
- 进程走走停停,程序不动
- 进程记录ax、bx 程序不用
- 。。。
5.2 多进程如何组织?
设置多个队列

- 就绪队列
- 运行队列
- 阻塞队列
- 新建态
- 终止态
5.3 进程如何切换?
5.4 多进程同时出现在内存中会有什么问题?
- 访问同一空间,造成数据读写错误
- 如何解决?
- 内存管理!内存映射
5.5 多进程如何合作?
- 进程同步和互斥?
- 信号量
- 。。。
5.6 什么是线程?
-
进程 = 资源 + 指令执行序列
-
在切换时,能不能不切换整个PCB,只切换PC和寄存器?
-
线程有没有用?
- 一个线程从服务器接收数据
- 一个线程显示文本
- 一个线程处理图片
- 一个线程显示图片
-
如果都由进程执行,可能需要等待较多时间
-
多个线程需要共享资源
5.7 线程如何切换
-
两个线程不能共享一个栈
-
线程如何切换? 保存栈指针,
- void yield() {TCB2.esp = esp, esp = TCB1.esp} 只需要切换栈即可。
- 函数调用后自动会弹出esp的内容到cs中 **
-
线程创建
- TCB
- 栈存放线程起始地址
- 栈指针
void ThreadCreate(A) { TCB *tcb = malloc(); // 为TCB分配空间 *stack = malloc(); // 栈的分配空间 *stack = A; /// 起始地址 tcp.esp = stack; }
5.8 多核与多cpu的区别
-
多核共用一个MMU和cache,多cpu各有各的MMU和cache
-
多cpu相当于多台电脑,例如分布式系统
-
多个线程可以使用多个核上(只有核心级线程)
-
一个系统中既有进程、用户级线程、核心级线程(进程只有核心态)
-
只有核心级线程能够使用多个核
-
操作系统看不到用户级线程
5.9 用户级线程和核心级线程
- 前者 TCB切换,根据TCB切换用户栈
- 后者 TCB切换 根据TCB切换一套栈(用户栈和内核栈相关联)
5.10 什么时候启用内核栈
- 中断时通过硬件寄存器,找到内核栈
- 内核栈内容,ss,sp,eflags,pc,cs
5.11 内核栈切换内容
void ThreadCreate() {
TCB tcb = get_free_page();
*krlstack = ....;
*userstack传入;
填写两个stack
tcb.esp = krlstack;
tcb.状态 = 就绪
tcb入队
}
5.12 用户级线程和核心级线程区别
5.13 内核级进程核心代码
进程 = 内核级线程 + 映射表
-
中断进入时,自动保护现场
-
但中断返回时,应该手动恢复现场
5.14 Linux 0.11线程切换方法
-
宏替换 + TSS的切换
-
但效果不好,速度过慢
5.15 fork()的执行过程
-
应用程序执行fork(),压入返回地址
-
系统调用,引起0x80中断
-
将SS:SP ELAGS PC CS压入内核栈
-
执行system call,仍然把用户态的执行现场保存到内核中
-
call sys_fork中间可能引起切换(判断状态是否阻塞,时间片用完)
-
从sys_fork()开始CreateThread
-
call copy_process (拷贝所有的参数,)
- 创建TCB
- 创建内核栈和用户栈(与父进程共用用户栈)
- 初始化TSS
- 关联栈和TCB
-
子进程返回0 mov res,%eax
-
-
在切换前将ret_from_sys_call地址放入栈中、
-
返回后,pop xxx, xxx,、
-
iret弹出内核栈中所有的内容,返回用户态
6.设备驱动及文件系统
6.1 怎么让外设工作起来
- 向控制器发指令 写端口
- 外设完成后 发出中断
- cpu进行中断处理
- 需要查寄存器地址 内容格式和语义
- 操作系统要给用户提供一个文件试图
6.2 printf的执行
=
-
open
-
文件视图
-
字符设备
-
tty字符设备
-
写到缓冲区
-
调用函数 out xxx
6.3 什么是设备驱动
- 写出核心的out指令
- 将函数注册到表上
- 创建一个dev/xxx文件 对应设备注册表
6.4 键盘是如何起作用的?
6.5 磁盘的管理
- 磁道
- 扇区
- 控制器 -> 寻道 -> 旋转 -> 传输
- 只要往控制器中写柱面©、磁头(H)、扇区(S)、缓存位置
- DMA技术
- 进程得到盘块号 得到扇区号
- 用扇区号磁盘缓存,用电梯算法放入请求队列中
- 进程sleep_on
- 磁盘中断处理
- do_hd_request算法C,H,S
- hd_out调用out完成端口写
- 唤醒进程
6.6 从生磁盘到文件
- 普通用户使用生磁盘:很多人连扇区都不知道
- 需要在盘块号上引入更高层次的抽象概念->文件
- 磁盘上的文件->一块一块
- 用户眼中的文件:字符流
- 建立字符流到盘块的映射关系
6.7 映射方法
-
连续结构存放文件
- 存放文件的起始块号 和 块数
- 需要很大的连续的空间
- 不适合修改
- 适合顺序读取
-
链式结构存放文件
- 在文件最后存放下一个块的地址
- 存放起始盘块号的块号
-
索引方式存放文件(Inode)
- 专门用一个块存取文件的所有盘块号
- 存放索引块的块号
- 可随机访问 文件容易删减
6.8 实际磁盘方法
6.9 设备文件的inode
6.10 根据路径名文件名 如何找到对应的inode?
- 一个文件对应一个盘块集合
- 许多文件怎么组织?
- 引入目录树
- 目录也是一个文件 存放目录下所有文件的FCB
- 但FCB过大,读入所有的FCB需要消耗巨大的时间
- 因此目录可以只存放 目录项 + FCB指针 (FCB数组 + 数据盘块号的读写)
6.11 完成全部映射下的磁盘使用
6.12 目录解析代码的实现? 如何解析open?
- 通过iget读到1号块inode
- 读入根目录的FCB放入PCB中
- 解析到叶子结点的FCB
7.参考资料
-
庖丁解牛Linux操作系统分析https://gitee.com/mengning997/linuxkernel
-
哈工大李治军老师操作系统课程https://www.bilibili.com/video/BV1d4411v7u7?spm_id_from=333.337.search-card.all.click