《深入理解Linux内核》 读书笔记

深入理解Linux内核 读书笔记

一、概论

操作系统基本概念

  • 多用户系统
    • 允许多个用户登录系统,不同用户之间的有私有的空间
  • 用户和组
    • 每个用于属于一个组,组的权限和其他人的权限,和拥有者的权限不一样。对应的是Linux的文件权限系统
  • 进程
    • 和程序的区别。几个进程能并发执行同一个程序,一个进程能顺序执行几个程序
    • 程序更像是代码片段,进程是执行代码的容器
    • linux是抢占式操作系统,也就是一个进程只能占用CPU一段时间。非抢占式系统中,进程如果不释放CPU,可以一直占用
  • 内核体系结构
    • Linux是单块内核,同时提供模块(module)功能
    • 模块是指:例如一个程序,引用了一个系统模块,这个系统模块不会是这个进程单独拥有,当其他程序也需要这个模块时,内核会把这个模块链接到其他程序。这样可以节省内存,也就是这个模块只会在内存中存在一份。模块就是一组函数,或者一段代码。

文件系统

  • 文件
    • 文件是以字节序列组成的信息载体(container)
    • 文件目录是树结构
    • 每个进程都有一个工作目录,通过pwdx 进程ID 命令可以查看
  • 硬链接和软连接
    • 链接类似window的快捷方式,创建一个文件,指向另一个文件
    • ln p1 p2 就是创建一个文件p2,指向p1
    • 硬链接只能指向文件,不能指向目录,因为会导致循环指向
    • 硬链接只能指向同一个文件系统的文件(文件系统是物理划分,例如不同硬盘)
    • 软链接没有硬链接这些限制,创建方法是加-s参数
  • 文件类型
    • 普通文件
    • 目录
    • 符号链接
    • 面向块的设备文件
    • 面向字符的设备文件
    • 管道和命名管道(pipe named pipe)
    • 套接字(socket)
  • 文件描述符与索引节点
    • 每个文件都有一个索引节点(inode)的数据结构,用来存储文件的描述信息,和文件的内容是区分开的。
      • inode有(通过ll命令看到的):
        • 文件类型
        • 硬链接个数
        • 文件长度
        • 文件拥有者的uid
        • 用户组的id
        • 修改时间等
        • 访问权限
  • 访问权限和文件模式
    • 拥有者,组,其他人,各有读写执行3种权限
  • 文件操作
    • 打开文件
    • 移动光标
    • 关闭

Unix内核概述

  • 进程/内核模式
    • 进程有用户态和内核态
    • 用户态不能访问内核的数据结构和内核程序
    • 两种态会经常切换,例如在时刻A,进程在用户态,在时刻B,进程在内核态
    • 从用户态切换到内核态的情况:
      • 调用系统调用
      • 执行进程的CPU发送异常
      • 外围设备向CPU发出中断
      • 内核线程被执行
  • 进程
    • 每个进程有一个进程ID,pid
    • 内核切换执行的进程时,会保存旧进程的信息,包括:
      • 程序计数器和栈指针寄存器
      • 通用寄存器
      • 浮点寄存器
      • CPU状态
      • 内存管理寄存器
  • 可重入内核
    • unix内核都是可重入的
    • 可重入是指,可以被重复进入,也就是可以同时有多个进程处于内核态
  • 进程地址空间
    • 每个进程有自己私有的地址空间
  • 同步和临界区
    • 类似锁
    • linux是抢占式内核,所以需要同步
    • 信号量
      • 每个资源都有一个信号量,类似int类型,初始值是1
      • 每个进程访问资源,调用down方法,信号量减1,如果减1后,信号量小于0,进程被加入到访问队列中。如果大于等于0,进程可以访问资源
      • 每个进程访问完资源,调用up方法,信号量加1,如果信号量大于等于0,激活访问队列的第一个进程
      • 进程锁,线程锁的机制,应该都是这样的
      • 这里要保证down和up的操作都是原子性的,不能并发
      • 要防止死锁
      • 锁里面的区域就是临界区,也就是acquire和release之间的代码
  • 信号和进程间通信
    • 信号和信号量是不一样的
    • linux有20多种不同的信号,例如kill -9 中的 9就是一种信号
    • 进程收到信号后,可以
      • 忽略
      • 异步执行指定程序(新开一个线程?),这种需要事先定义信号处理函数。
    • 内核收到信号后,可以
      • 终止进程(例如kill - 9)
      • 忽略信号
      • 挂起进程
      • 恢复进程
    • 进程间通信(IPC)
      • 信号
      • 消息(msgget(),msgsnd())两个系统调用,发信息和收信息,Python里面的进程间Queue应该就是用这个实现的
      • 共享内存(shmget shmdt)两个系统调用
  • 进程管理
    • fork来启动一个子进程,一般在启动的时候复制父进程的数据和代码,但是这样效率较低,所以会使用写时复制,也就是一开始父子进程共享内存,当其中一个进程需要修改数据时,才执行复制操作
    • exec用于启动子进程
    • exit用于结束子进程
    • wait4用于父进程等待子进程结束
  • 内存管理
    • 虚拟内存,在物理内存(MMU)和程序之间的抽象,相当于访问内存的代理。
    • 内核内存分配器,KMA,用于管理内存
    • 高速缓存 由于内存比硬盘快很多,所以从硬盘读取得数据会缓存在内存,使下次可以快速访问
    • 二、内存寻址

  • 内存地址
    • 内存地址有3种
      • 逻辑地址,由一个段(segment)和偏移量(offset)组成,用来指明一个操作数,或者一条指令的地址
      • 线性地址。是一个32位无符号整数(在32位系统中是这样),从0x00000000到0xffffffff。内存相当于一个超大的列表,下标(地址)是一个32位整数,值就是内存的内容,值得大小是1字节
      • 物理地址。内存芯片级的地址
    • 逻辑地址,经过分段单元,转换为线性地址,线性地址,经过分页单元,转换为物理地址
  • 分段单元(用于把逻辑地址,转换为线性地址)
    • 概念
      • 段选择符,也叫段标识符,也就是上面说的段,程序传入给分段单元。有字段:
        • index,表示段描述符在GDT或者LDT中下标
        • TI,表示段描述符在GDT中还是LDT中
        • RPL,特权级
      • 段描述符,8字节,存放在GDT或者LDT中,有字段
        • Base表示段在内存中首字节的线性地址
        • S,0表示系统段,1表示普通段
        • DPL,特权级,0表示只有内核态才能访问,3表示内核态和用户态都能访问。(cs寄存器中,有一个两位的字段,指明CPU的当前特权级,0表示内核级,3表示用户级。所以通过这个机制,可以限制用户态的进程不能访问内核态的内存数据
        • D或者B,表示这是代码段,还是数据段
      • GDT,是全局段列表,item是段描述符
      • LDT,是局部段列表,item是段描述符
    • 转换流程
      1. 传入逻辑地址给分段单元,逻辑地址包含段选择符和偏移量
      2. 查看段选择符的TI字段,决定是从GDT中还是LDT中获取段描述符,假如是GDT
      3. 查看段选择符的index字段,假如是2,从gdtr寄存器中获取GDT列表的首字节地址,假如是0x00002000,计算段描述符的位置=0x00002000+2*8,=0x00002016 (每个段描述符8字节),所以段描述符在内存的0x00002016-0x00002024位置
      4. 查看段描述符的Base字段,假如是0x00003000,加上偏移量,假如是100,得到线性地址是0x00003100

三、进程

进程,轻量级进程(LWP)和线程

  • 进程是程序执行时的一个实例
  • 线程 是进程里面的一个执行流,线程的切换时在用户态进行的。但是这样就不能做到并发了
  • 轻量级进程,类似线程,但是切换时在内核态进行

所以Linux的做法是(TODO 这一块还不是很明白)

  • 把线程和轻量级进程关联起来,所以线程和轻量级进程是等价的
  • 对内核来说,进程和LWP是一样的,使用同样的调度方法
  • LWP之间可以共享部分数据

进程描述符

  1. 进程描述符是一个数据结构(c的struct,类似Python的字典)

  2. 进程描述符有字段:
    1. state 状态
      1. 可运行状态(TASK_RUNNING),要么在运行,要么准备运行
      2. 可中断的等待状态(TASK_INTERRUPTIBLE)进程被挂起(睡眠),表示它在等待一个事件的发生,例如等待某个系统资源。当这个系统资源可用,内核会产生一个硬件中断,来唤醒进程
      3. 不可中断的等待状态(TASK_UNINTERRUPTIBLE),和可中断的等待状态类似,这个状态较少用到
      4. 暂停状态(TASK_TOPPED)进程被暂停执行,当进程收到信号SIGSTOP,SIGSTP,SIGTTIN SIGTTOU信号后,会进入暂停状态
      5. 跟踪状态(TASK_TRACED)当进程被另一个进程跟踪,例如执行ptrace命令,
      6. 僵死状态(EXIT_ZOMBIE)进程的执行被终止,但是父进程还没有发布wait4或者waitpid命令来获取进程信息。这时内核不会自动丢弃进程的信息,因为父进程可能还需要这些信息
        10.僵死撤销状态
    2. thread_info 进程的基本信息
    3. fs_struct 当前目录
    4. signal_struct 收到的信号
    5. pid 进程的ID。顺序递增,最大是32767,超过后,从1开始获取闲置的PID值。进程里面的线程,也拥有自己的pid,同时每个线程有一个tgid(thread group id),表示线程组ID,这个ID等于进程中第一个线程的pid。
      1. 一个进程里面至少有一个线程

进程链表

  • 一个进程描述符表示一个进程
  • Linux把所有进程放在一个双向链表里面,每个item是一个进程描述符
  • TASK_RUNNING状态的进程链表
    • 由于CPU在进行进程切换时,需要快速知道下一个执行的进程是什么,所以Linux把所有可以执行的进程都放在一个单独的链表。
    • 由于不同进程有不同的优先级,所以linux的做法是
      • 由于有140种优先级(优先级用prio表示,0-139),所以用140个链表来保存
      • 用一个140长度的位图(bitmap)来表示140个链接中,哪些有数据
      • 所以获取下一个优先级最高的进程的做法是:
        • 查看位图,看第一个=1的位的下标是多少,例如是15
        • 访问第15个链表,queue[15],获取第一个元素

进程间的关系

进程描述符里面有特定的字段,记录每个进程的父进程,兄弟进程和子进程

  • real_parent 父进程的描述符指针,如果父进程不存在,指向进程1
  • parent 当前父进程,通常和real_parent一致,指引当进程被追踪时不一致
  • children 链表,记录所有子进程
  • sibling 有prev和next两个元素,表示上一个兄弟进程,和下一个兄弟进程

pidhash

有时候内核需要根据pid来获取进程描述符
所以内核会保存一个pidhash数据结构,是个hash表(c里面的hash表的实现和redis的hash表实现类似),key是pid,value是进程描述符

进程切换

进程切换,任务切换,上下文切换是一样的

每个进程都有自己的地址空间(在内存),但是进程之间是共享寄存器的,所以进程的切换需要(硬件上下文是寄存器的数据):

  • 保存prev进程的硬件上下文
  • 用next硬件上下文替换prev

上面的操作使用一个switch_to宏来实现,传入参数prev,next,prev。传入两次prev是怕切换上下文后,把第一个prev丢了。

创建进程

Linux进程的特性:

  • 写时复制
  • 轻量级进程允许父子进程共享很多数据结构

创建进程的系统调用:

  • close()

    • fn 子进程创建后执行的函数,函数结束,子进程终止
    • arg 传给函数的数据
    • 其他还有很多参数
  • fork close函数的封装
  • vfork close函数的封装

内核进程

内核进程是一直运行在内核态的

进程0
进程0是linux启动后的第一个进程,由它创建进程1
进程1
进程1也叫init进程,进程1会一直运行知道linux关闭

撤销进程

进程执行完指定的代码后,就会终止,这时必须通知内核回收进程的资源。
一般是exit系统调用,c编译程序会自己动把exit函数插入到main函数最后
内核可以强迫整个线程组死掉(例如收到kill -9)

进程删除
当进程终止后,进程会进入僵死状态,直到父进程调用wait4来获取进程的状态数据,然后进程就会被删除。
如果父进程已经不存在,进程会交给init进程托管,init进程会定期执行wait4命令来查看进程的状态,如果进程已经终止,就会删除这个进程

上一篇:8个新鲜的PHP常用代码


下一篇:创建论坛Discuz