Linux操作系统分析学习总结

本文是Linux操作系统的分析总结。先介绍了Linux概览、常用命令,接着阐述计算机系统原理,包括存储程序、指令集等。还涉及Linux内核源码阅读、调试、启动过程,深入分析系统调用、进程创建与切换,最后讲解设备驱动及文件系统相关知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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目录文件结构

image-20220705191339863

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
  • 寻址方式

    • 寄存器寻址
    • 立即寻址
    • 直接寻址
    • 间接寻址
    • 变址寻址
  • 堆栈操作

  • 函数调用和返回

2.5 内嵌汇编

image-20220705210320828

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
  • 如何进入内核?中断。系统调用的核心是什么

    • 用户程序中一段包含有int指令的代码
    • 操作系统写中断处理,获取想调用程序的编号
    • 操作系统根据编号执行代码.
4.3 系统调用是如何实现的?
  • 用户调用printf -> 调用库函数printf() -> 调用write函数

  • image-20220303100414369

  • printf展开成int 0x80

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HBIByjQs-1657077112481)(https://gitee.com/fragile_xia/git_test/raw/master/7.6/4.png)]

    image-20220303100752634

  • 中断处理system_call

    image-20220303105751929

  • 查表sys_call_table __NR_write = 4

    image-20220303110645537

  • sys_write 系统调用(文件系统)

4.4 whoami如何实现?
  • image-20220303112949366
  • 应用程序调用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 多进程如何组织?

​ 设置多个队列

image-20220304152903587
  • 就绪队列
  • 运行队列
  • 阻塞队列
  • 新建态
  • 终止态
5.3 进程如何切换?

image-20220304153517306

5.4 多进程同时出现在内存中会有什么问题?
  • 访问同一空间,造成数据读写错误
  • 如何解决?
  • 内存管理!内存映射
5.5 多进程如何合作?
  • 进程同步和互斥?
  • 信号量
  • 。。。
5.6 什么是线程?
  • 进程 = 资源 + 指令执行序列

  • 在切换时,能不能不切换整个PCB,只切换PC和寄存器?

  • 线程有没有用?

    • 一个线程从服务器接收数据
    • 一个线程显示文本
    • 一个线程处理图片
    • 一个线程显示图片
  • 如果都由进程执行,可能需要等待较多时间

  • 多个线程需要共享资源

5.7 线程如何切换

image-20220304161017174

  • 两个线程不能共享一个栈

  • 线程如何切换? 保存栈指针,

    • 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;
    }
    

    image-20220304161017174

5.8 多核与多cpu的区别
  • 多核共用一个MMU和cache,多cpu各有各的MMU和cache

  • 多cpu相当于多台电脑,例如分布式系统

  • 多个线程可以使用多个核上(只有核心级线程)

  • 一个系统中既有进程、用户级线程、核心级线程(进程只有核心态)

  • 只有核心级线程能够使用多个核

  • 操作系统看不到用户级线程

5.9 用户级线程和核心级线程
  • 前者 TCB切换,根据TCB切换用户栈
  • 后者 TCB切换 根据TCB切换一套栈(用户栈和内核栈相关联)
5.10 什么时候启用内核栈
  • 中断时通过硬件寄存器,找到内核栈
  • 内核栈内容,ss,sp,eflags,pc,cs
5.11 内核栈切换内容

image-20220304161017174

image-20220304195919303

image-20220304200357065

void ThreadCreate() {
    TCB tcb = get_free_page();
    *krlstack = ....;
    *userstack传入;
    填写两个stack
    tcb.esp = krlstack;
    tcb.状态 = 就绪
    tcb入队
}
5.12 用户级线程和核心级线程区别
image-20220306170317858
5.13 内核级进程核心代码

​ 进程 = 内核级线程 + 映射表

image-20220304210412220

  • 中断进入时,自动保护现场

  • 但中断返回时,应该手动恢复现场

    image-20220304211839028

image-20220304211851949

5.14 Linux 0.11线程切换方法

image-20220304213136220

  • 宏替换 + TSS的切换

  • 但效果不好,速度过慢

    image-20220304213158201

image-20220304213342618

image-20220304214230819

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的执行

image-20220414125001577

image-20220414125034654=image-20220414125525164

image-20220414125839952

image-20220414130216085

image-20220414132359621

image-20220414132456211

image-20220414132717895

image-20220414132929327

  • open

  • 文件视图

  • 字符设备

  • tty字符设备

  • 写到缓冲区

  • 调用函数 out xxx

    image-20220414134117571

6.3 什么是设备驱动
  • 写出核心的out指令
  • 将函数注册到表上
  • 创建一个dev/xxx文件 对应设备注册表
6.4 键盘是如何起作用的?

image-20220414134912063

image-20220414135109250

image-20220414135302673

image-20220414135416272

image-20220414135443131

image-20220414135520780

6.5 磁盘的管理
  • 磁道
  • 扇区
  • 控制器 -> 寻道 -> 旋转 -> 传输
  • 只要往控制器中写柱面©、磁头(H)、扇区(S)、缓存位置
  • DMA技术

image-20220414230540765

image-20220414230637964

image-20220414231219902

image-20220414231627282

image-20220414231831384

image-20220414232556853

  • 进程得到盘块号 得到扇区号
  • 用扇区号磁盘缓存,用电梯算法放入请求队列中
  • 进程sleep_on
  • 磁盘中断处理
  • do_hd_request算法C,H,S
  • hd_out调用out完成端口写
  • 唤醒进程
6.6 从生磁盘到文件
  • 普通用户使用生磁盘:很多人连扇区都不知道
  • 需要在盘块号上引入更高层次的抽象概念->文件
  • 磁盘上的文件->一块一块
  • 用户眼中的文件:字符流
  • 建立字符流到盘块的映射关系
6.7 映射方法
  • 连续结构存放文件

    • 存放文件的起始块号 和 块数
    • 需要很大的连续的空间
    • 不适合修改
    • 适合顺序读取
  • 链式结构存放文件

    • 在文件最后存放下一个块的地址
    • 存放起始盘块号的块号
  • 索引方式存放文件(Inode)

    • 专门用一个块存取文件的所有盘块号
    • 存放索引块的块号
    • 可随机访问 文件容易删减

image-20220414234942187

6.8 实际磁盘方法

image-20220414235200177

image-20220414235408443

image-20220414235452375

image-20220414235654852

image-20220414235928297

6.9 设备文件的inode

image-20220415000451691

image-20220415000754994

image-20220415000850707

6.10 根据路径名文件名 如何找到对应的inode?
  • 一个文件对应一个盘块集合
  • 许多文件怎么组织?
  • 引入目录树
  • 目录也是一个文件 存放目录下所有文件的FCB
  • 但FCB过大,读入所有的FCB需要消耗巨大的时间
  • 因此目录可以只存放 目录项 + FCB指针 (FCB数组 + 数据盘块号的读写)
6.11 完成全部映射下的磁盘使用

image-20220415101746292

image-20220415100342476

image-20220415100650418

6.12 目录解析代码的实现? 如何解析open?

image-20220415103720254

image-20220415104119222

image-20220415104202769

image-20220415104658376

image-20220415104731229

image-20220415104913631

  • 通过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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值