• 深入Linux内核架构笔记


    一.简介和概述

    主要版本:2.6.24

    1.1内核任务

    • 抽象计算机:抽象计算机硬件
    • 资源管理:分配资源和保证完整性
    • 库:系统调用

    1.2实现策略

    1. 微内核
    2. 宏内核

    1.3内核的组成部分

    1.3.1 进程、进程切换、调度

    通过虚拟技术,进程感觉自己是唯一的,内核保障这一点

    内核保存、恢复进程状态,在切换时

    内核保障进程共享CPU时间

    1.3.2UNIX进程

    init是第一个进程,负责进一步的系统初始化

    新进程创建,两种方式:

    1. fork,cow,继承父进程内存,需要写的时候再复制内存
    2. exec,新程序,刷掉原程序内存页

    1.线程

    两种程序执行形式

    • 重量级进程:UNIX进程
    • 线程:轻量级线程

    线程是与主程序并行的函数或者例程

    clone方法创建线程

    检查资源,哪些是线程独占哪些是与父进程共享

    2.命名空间

    全局资源分到不同的组

    每个命名空间包含特定PID集合

    通过成为容器的命名空间提供多个系统视图,多个容器实例虚拟化物理计算机(一个容器看起来就是一个真正的计算机,实际上是个假的虚拟计算机)

    不是所有内核子系统都支持命名空间

    1.3.3 地址空间与特权级别

    用户空间虚拟地址范围:0~TASK_SIZE

    1.特权级别:

    禁止访问某些内存地址和使用某些汇编指令

    通过系统调用完成状态转换,由系统检查权限后代替用户态应用执行某些操作

    中断也可以完成状态转换,在中断上下文中执行,不能睡眠,不能访问普通进程的内存

    内核线程也是运行在内核态,可睡眠但是不可访问用户数据

    内核线程名字存在于方括号【】中,名字斜线后的数字表明内核线程绑定运行的CPU

    2.虚拟和物理地址空间

    页表将虚拟地址映射到物理地址

    虚拟地址划分空间,物理地址寻找内存

    • 页帧:物理内存
    • 页:虚拟内存

    1.3.4页表

    页表:用来将虚拟地址空间映射为物理地址空间的数据结构

    有些内存不需要页表项,压缩页表项数量大小,页表要分级

     Linux用4级页表

    优点:不需要映射的地址没有页表项

    缺点:多级转换效率低

    优化:MMU硬件优化和TLB缓存专门存放最频繁转换的地址群

    页表内容变化要是TLB无效,使用CPU专用指令,硬件完成就是空指令

    1.与CPU交互

    CPU不支持多层页表需要Linux进行空页表仿真

    2.内存映射

    可以将任意内容映射到虚拟地址空间

    1.3.5物理内存的分配

    分配页帧速度要快,是大块分配

    用户空间标准库进行小段分配

    1.伙伴系统

    为了分配连续页帧而存在

    地址相邻空闲内存块两两作为伙伴,使用数组标记相同页帧数目的内存块。有空闲的时候两个伙伴就合并,链接到上一个大小数组项

    伙伴系统用来控制内存碎片化

    2.slab缓存

    内核没有标准库管理内存,要用slab缓存管理伙伴系统的内存,分配小对象

     slab提供频繁使用对象的缓存和一般使用小对象的缓存(类似kmalloc和kfree)

    3.页面交换和页面回收

    页面交换:不常用内存放在外存,特殊标记,用到了产生缺页异常,内核自动载入

    页面回收:被映射的内存与块设备同步。同步完了可以将该内存用作他用,需要再重新分配

    1.3.6 计时

    jiffies按照恒定的时间间隔自动递增

    定时器中断更改jiffies数值

    HZ决定jiffies递增数值

    1.3.7 系统调用

    系统调用大致分为以下组别:

    • 进程管理
    • 信号
    • 文件
    • 目录和文件系统
    • 保护机制
    • 定时器函数

    有些功能只能内核做,所以没有用户空间库

    1.3.8 设备驱动程序、块设备和字符设备

    设备驱动通过设备文件完成用户空间程序与设备硬件通信

    • 字符设备:顺序读写,按照字符流式读写,不能随机读取
    • 块设备:按块读,可以随机寻址,不支持按照字符寻址

    块设备设计缓存机制,复杂很多

    1.3.9 网络

    网卡不能利用设备文件直接访问,涉及到层层协议的封装、打包、解包

    使用socket抽象作为应用、文件、内核实现间代理

    1.3.10 文件系统

    存储使用了层次式文件系统

    使用目录结构组织存储的数据,将元数据与数据本身通过关联

    ext2中,文件inode存储数据地址和元信息,目录inode存储文件inode指针,层次由此形成

    VFS是中间层,将文件系统和应用隔离开

    所有文件系统要实现VFS的接口,应用程序通过VFS调用到底层文件系统

    1.3.11 模块和热插拔

    不膨胀内核,动态增加、删除驱动、协议

    1.3.12 缓存

    缓存按页组织,称为页缓存

    页缓存替代了块缓存

    缓存可以绕过低俗设备的限制

    1.3.13 链表处理

    双链表将任何类型的数据彼此链接起来

    要被链接的数据类型,其内部要被嵌入一个叫做list_head的成员变量

     LIST_HEAD 宏声明初始化链表头,链表头也是一个list_head类型

    常用API:

    • list_add
    • list_add_tail
    • list_empty
    • list_entry
    • list_del
    • list_splice
    • list_for_each

    1.3.14 对象管理和引用计数

    一般性的内核对象机制,进行以下操作:

    • 引用技术
    • 集合对象链表管理、
    • 对象加锁
    • 对象属性通过sysfs导出到用户空间

    各个子系统中的对象通用

    1.一般性内核对象

    kobject对象嵌入替他对象,即可提供对象管理功能

    kref管理引用计数

    操作API:

    • kobject_put/get
    • kobject_init
    • kobject_cleanup
    • kobject_add
    • kobject_registger/unregistger

    2.对象集合

    kset结构将不同对象通过链表链接在一起,提供sysfs共用操作,以及自身操作指针函数,本身也通过kobject管理

    kobj_type提供sysfs接口

    3.引用计数

    提供kobject_init,kobject_put,kobject_get等原子操作

    1.3.15 数据类型

    1.类型定义

    为适配不同体系结构,用typedef自定义类型,并且类型数据不能直接操作,要用提供的函数,防止没搞清类型误操作

    2.字节序

    cpu_to_le64和le64_to_cpu将数据转为小端,小端转为大端,本身是小端则为空操作

    3.per_cpu变量

    每个CPU都分配的一个变量

    DEFINE_PER_CPU定义该变量

    get_cpu获取该变量

    smp_processor_id获取cpu id

    多核同时读取一个变量,可以不用在缓存间转移

    4.访问用户空间

    __user标记指针指向的是用户空间的虚拟地址,不要轻易访问

    1.3.16 本书的局限性

    只是讲解了共性问题

    1.4 为什么内核是特别的

    使用非常干净的抽象设计,保证内核的模块化和易管理性

    为了性能、兼容性、并发性,有很多magic代码

    1.5 行文注记

    自顶向下介绍,先概念,再数据结构,最后代码

    代码分析省略细节、次要任务,阐述主题

    1.6 小结

    已给出内核全局图景,包括各部职责分配、所处理问题、组件交互

    二. 进程管理和调度

    内核和处理器建立多任务的错觉,是通过在短时间内切换任务执行做到的

    进程管理带来两个问题:

    • 隔离资源
    • 调度公平

    调度主要有两个方面

    • 调度策略,与平台无关
    • 进程场景切换

    2.1 进程优先级

    分为实时进程与非实时进程

    • 硬实时进程

              为保证响应时间,以下策略:

      • 可抢占的内核机制
      • 实时互斥量
      • 全完公平调度器
    • 软实时进程
    • 普通进程,有优先级

    抢占式多任务处理:

     要考虑进程状态是否是可运行的,进程分为是否实时,是否可抢占

    完全公平调度器取代了之前的调度器,也可以先分配用户时间再分配进程时间

    2.2 进程生命周期

     进程如果在等待外部事件,则调度运行也没有意义

    进程状态有:

    • 运行
    • 等待
    • 睡眠(分为可否被信号中断)
    • 终止
    • 僵尸(只收到信号SIGTERM、SIGKILL,但是父进程没有调用wait4向内核确认删除,所以还有进程表项)

     进程还有两种状态

    • 用户态
    • 核心态

    通过系统调用、中断转变状态

    可抢断进程调度分三层:

    • 普通进程,可以被随便抢占
    • 在处理系统调用的内核状态(重要内核操作可以禁止中断发生),中断可以抢占
    • 中断,执行时间要短

    内核抢占选项:支持系统调用内核态被普通进程抢占,进程执行更平滑

    2.3 进程表示

    linux/sched.h里,task_struct用来表示进程

    进程和其他子系统的联系基本都在这个数据结构中

    包含的信息很多,主要可以分类如下:

    • 状态和执行信息,比如PID,待决信号,父进程(还有其他相关进程),优先级,文件格式,CPU使用时间等
    • 已分配的虚拟内存信息
    • 进程身份凭据,比如UID、GID、权限
    • 所处理文件的文件系统信息、二进制执行相关文件信息
    • 线程信息(与绑定的CPU有关)
    • IPC信息
    • 信号处理相关

    state指定进程状态,有以下值:

    TASK_RUNNING:可运行状态

    TAST_INTERRUPTIBLE:等待外部事件或资源而睡眠,可被唤醒

    TASK_UNINTERRUPTIBLE:被内核主动沉睡,不可唤醒,只能内核自己唤醒

    TASK_STOPPED:停止运行,比如因调试而暂停

    TASK_TRACED:用来区分被ptrace跟踪的进程和普通停止运行进程

    EXIT_ZOMBIE:僵尸态进程,没有父进程调用wait4

    EXIT_DEAD:wait4已发出,等待被释放的进程。主要应对多个wait4调用

    Resouce Limit:限制进程相关资源,放在一个数组里,索引就是资源项ID

     分为软限制和硬限制,current是软限制,max是硬限制

     无限制为RLIM_INFINITY

    init进程的限制为INIT_RLIMITS

    /proc/self/limits可查看本进程限制

    2.3.1 进程类型

    典型的包括:二进制代码、线程、资源(文件、内存、信号量、锁等)

    复制进程API:

    fork:当前进程副本,cow技术优化

    exec:不会新增进程,替换代码

    clone:不是独立的进程,有些资源共享,可用来创建线程

    2.3.2 命名空间

    1.概念

    轻量级虚拟化技术,一些资源隔离的就像在不同计算机,一些资源共享(比如文件)

    一个内核就可以完成虚拟资源配置,不同用户跑在同一个内核上却都以为自己是独一份

    命名空间抽象全局资源

    命名空间建立了系统的不同视图

    一组进程可以放进一个容器中,每个同层级的容器是互相隔离的,互相不知道,就像一台真实的计算机,但是其在父空间中是共存的,在子空间中重新分配

     除了在全局空间外,其他资源在子空间中有一个自己的ID,可能与其他子空间的重复

    命名空间比较简单的话可以是没有层级关系的,比如chroot

    两种方法创建新的命名空间:

    • 1.fork、clone添加参数
    • 2.unshare将自己从父命名空间剥离

    2.实现

     struct nsproxy汇集了所有可以感知ns的子系统的命名空间指针,子系统包括:

    • uts_ns
    • ipc_ns
    • mnt_ns
    • pid_ns
    • user_ns
    • net_ns

    要实现两个任务:

    • 将全局资源映射到各个ns
    • 将进程关联到各个ns

    以下标志用来在fork的时候标识怎么创建命名空间

    task_struct中包含到ns_proxy的指针

     使用了指针,多个进程共享一个命名空间

    int_nsproxy是初始的全局命名空间

     UTS命名空间

    包含了简单的系统信息

     

     初始信息在init_unt_ns里

    copy_utsname生成新的uts_namespace,这样保证了隔离,各个进程修改各自的,不会互相影响

    用户命名空间

    包括了用户相关进程的统计综合数据

     每个用户空间都是统计自己相关用户的信息,互相不影响

     

    2.3.3 进程ID号

     fork和clone的时候生成pid

    1.进程ID

    id有:

    1. pid:标志进程
    2. tgid:线程组,一个组的id都是主线程的
    3. pgrp:进程组,一个组的id都是进程组长的,方便了发送信号
    4. session id:进程组会话id

    各种id在各个命名空间可能重复,但是在全局空间、初始空间中唯一

    定义位置:

    全局pid和tgid直接在task_struct

    session id在task_struct->signal->__session

    pgrd id在task_struct->signal->__pgrp

    2. 管理id

    内核提供小型系统分配pid,并提供pid与task_struct查找功能与内核表现形式和用户空间可见形式转换的辅助函数

    数据结构

    pid命名空间定义如下:

     child_repeater是子空间的init进程,level是子空间层级优先级,parent是父命名空间

    struct upid:特定的命名空间中可见信息

     struct pid:pid的内核表示

     

     numbers形式上是一个元素,但是因为在尾部,可以无限扩容

    共享同意ID的task_struct实例都挂在一个hlist散列表上

     

     

     task_struct和pid发生关联建立双向连接

    函数

    通过task_struct找到id:

     

     

     

    通过id找到task_struct

     

    所有其他类型ID都可以映射到PID

    3.生成唯一PID

     分配PID就是在位图里找到第一个为0的然后置1

     

      

    每一层ns都要生成一个pid,更新到相应upid中

    每一层的 upid都要更新,并重新hash加入相应pid_hash中

    2.3.4 进程关系

    除了ID连接关系,还有父子、兄弟关系

     

    2.4 进程管理相关的系统调用

    内核是由C标准库使用的工具库。

    fork、exec系列系统调用一般有由C标准库调用

    2.4.1 进程复制

    3个api:

    1. fork:重量级复制,cow技术
    2. vfork:共享数据
    3. clone:精确控制复制

    1.写时复制

    进程经常访问的页的集合成为工作区

    cow避免浪费内存和时间

    只拷贝页表,设为只读

    不能随便写彼此的页

    父子进程写允许写的页报缺页错误

    2.执行系统调用

    fork、vfork、clonde的系统调用入口点sys_fork,sys_vfork,sys_clone最后都会调用do_fork函数

     

     cow会分配栈副本

    SIGCHLD标志:子进程结束向父进程发送进信号,fork默认标志

    vork默认标志:CLONE_VFORK,CLONE_VM

    clone根据用户标志传递

    3.do_fork实现

    与结构体系无关 

    4.复制线程

    5.创建线程时的特别问题

    2.4.2 内核线程

    2.4.3 启动新程序

    2.4.4 退出进程

    2.5 调度器的实现

    2.6 完全公平调度类

    2.7 实时调度类

    2.8 调度器增强

    2.9 小结

  • 相关阅读:
    jFinal基于maven简单的demo
    quartz定时任务
    cors解决跨越问题
    poi导出excel
    layui富文本编译器后台获取图片路径
    HttpClient短信接口
    js分页
    El表达式日期处理
    【传智播客】Libevent学习笔记(二):创建event_base
    【传智播客】Libevent学习笔记(一):简介和安装
  • 原文地址:https://www.cnblogs.com/cascle/p/15578721.html
Copyright © 2020-2023  润新知