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

深刻理解Linux内核 读书笔记

1、概论

操做系统基本概念

  • 多用户系统
    • 容许多个用户登陆系统,不一样用户之间的有私有的空间
  • 用户和组
    • 每一个用于属于一个组,组的权限和其余人的权限,和拥有者的权限不同。对应的是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,用于管理内存
    • 高速缓存 因为内存比硬盘快不少,因此从硬盘读取得数据会缓存在内存,使下次能够快速访问
    • 2、内存寻址

  • 内存地址
    • 内存地址有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

3、进程

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

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

因此Linux的作法是(TODO 这一块还不是很明白)node

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

进程描述符

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

  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],获取第一个元素

进程间的关系

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

  • 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进程的特性:socket

  • 写时复制
  • 轻量级进程容许父子进程共享不少数据结构

建立进程的系统调用:函数

  • close()ui

    • 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命令来查看进程的状态,若是进程已经终止,就会删除这个进程