Linux内核原理理解

648 阅读22分钟

操作系统

​ 操作系统,英文名称Operating System,简称OS,是计算机系统中必不可少的基础系统软件,它是应用程序运行以及用户操作必备的基础环境支撑,是计算机系统的核心。

​ 操作系统的作用是管理和控制计算机系统中的硬件和软件资源,例如,它负责直接管理计算机系统的各种硬件资源,如对CPU、内存、磁盘等的管理,同时对系统资源所需的优先次序进行管理。操作系统还可以控制设备的输入、输出以及操作网络与管理文件系统等事务。同时,它也负责对计算机系统中各类软件资源的管理。例如各类应用软件的安装、设置运行环境等。

  • 内存管理
  • 进程管理
  • 设备管理
  • 文件管理

内存管理

地址映射

l 我们程序所使用的内存地址叫做虚拟内存地址

Virtual Memory Address

l 实际存在硬件里面的空间地址叫物理内存地址

Physical Memory Address

把进程所使用的地址「隔离」开来,即让操作系统为每个进程分配独立的一套「虚拟地址」,进程间互不干涉,虚拟地址到物理地址的转化是通过mmu硬件单元实现的。

页帧

​ 作为内存管理的基本单元,页的许多状态需要被记录下来(比如,内核需要知道什么时候可以被回收),因此内核为内核中的每个页都以页描述符表示struct page{}.系统在初始化时根据物理内存的大小建立起一个page结构数组mem_map,作为物理页面的“仓库”

​ 记录物理内存的信息,一个页帧一般是4k大小

页表

​ 用来将虚拟地址空间映射到物理地址空间的数据结构称为页表。实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空间中的每一页,都分配一个数组项。该数组项指向与之关联的页帧,但有一个问题。例如,IA-32体系结构使用4 KiB页,在虚拟地址空间为4 GiB的前提下,则需要包含100万项的数组。在64位体系结构上,情况会更糟糕。每个进程都需要自身的页表,因此系统的所有内存都要用来保存页表,也就是说这个方法是不切实际的。

​ 因为虚拟地址空间的大部分区域都没有使用,因而也没有关联到页帧,那么就可以使用功能相同但内存用量少得多的模型:多级分页

多级分页在有物理内存未分配的情况下,减少页表的占用空间

image-20210517113721374

TLB(快表)

​ 上面介绍到,MMU的输入是page table,而page table又存在内存里,和cpu的cache相比,内存的速度很慢,为了进一步加快虚拟地址到物理地址的转换速度,出现了TLB快表,它存在于cpu的L1cache里面,用来缓存已经找到的虚拟地址和物理地址的映射,这样下次转换前先排查一下TLB,如果已经在里面了就不需要使用MMU进行转换了

物理页分配
  • 伙伴系统

    为分配一组连续的页框而建立的一种健壮、高效的分配策略,最小单位未page frame:分割/合并

  • slab分配

    根据对象大小动态分配内存,分配空间可以小于page frame

虚拟空间

​ 在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。32 位的系统如下所示:

  • 进程在用户态时,只能访问用户空间内存
  • 只有进入内核态后,才能访问内核空间内存

image-20210517114635008

​ 用户态和内核态,本质上是cpu上有一个标识位,用来表示cpu的权限,一般有0、1、2、3这四个级别,Linux用了其中的两个,主要的区别就是执行指令的权限,直观的看就是在户态的时候的不能访问内核空间,这样就保证了应用程序写的代码只能操作自己进程的那块地址,而不影响操作系统代码的运行。

区分用户态/内核态作用

  1. 安全性,不能修改操作系统内核核心代码/数据

进程管理

进程控制块PCB( Task结构)
  • 进程描述信息:

    • 进程标识符:PID,每个进程都有一个并且唯一的标识符;
    • 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
  • 进程控制和管理信息:

    • 进程当前状态;
    • 进程优先级;
  • 资源分配清单:

    • 有关内存地址空间或虚拟地址空间的信息;
    • 所打开文件的列表和所使用的 I/O 设备信息。
  • CPU 相关信息**:**

    • CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
1.struct task_struct {    
2.    ...    
3.    volatile long state;    
4.    /* 内核栈*/    
5.    void *stack;    
6.    pid_t pid;  
7.    pid_t tgid;  
8.    uid_t uid;  
9.    gid_t gid;  
10.      
11.    int prio, static_prio, normal_prio;  
12.    unsigned int time_slice;  
13.      
14.    struct task_struct *parent;  
15.    struct list_head children;  
16.      
17.    /* 进程地址空间 */  
18.    struct mm_struct *mm, *active_mm;  
19.  
20.    /* 文件系统信息 */  
21.    struct fs_struct *fs;  
22.    /* 打开文件信息 */  
23.    struct files_struct *files;  
24.      
25.    /* 进程CPU的状态信息 */  
26.    struct thread_struct  
27.      
28.    ...    
29.};    
进程虚拟地址

32位系统例子

image-20210517115425099

  • 程序文件段,包括二进制可执行代码;

  • 已初始化数据段,包括静态常量;

  • 未初始化数据段,包括未初始化的静态变量;

  • 堆段,包括动态分配的内存,从低地址开始向上增长;

  • 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关)

  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;

​ 在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc()就可以分别在堆或者文件映射段动态分配内存。

进程内存数据结构
1.struct mm_struct {  
2.    ...  
3.    unsigned long mmap_base; /* mmap区域的基地址 */  
4.    unsigned long task_size; /* 进程虚拟内存空间的长度 */  
5.     
6.    unsigned long start_code, end_code, start_data, end_data;  
7.    unsigned long start_brk, brk, start_stack;  
8.    unsigned long arg_start, arg_end, env_start, env_end;  
9.      
10.    struct vm_area_struct * mmap; /* 虚拟内存区域列表 */  
11.    struct vm_area_struct * mmap_cacge; /* 上次查找结果 */  
12. 	   pgd_t  *pgd; /* 页表目录 */  
13.
14.    ...  
15.}  

image-20210517115619646

进程内核栈

​ 在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。内核态运行时先保存用户态的上下文(到内核栈?),然后使用内核栈。

​ task_struct结构存的是进程的通用信息部分,thread_info保存了特定于体系结构的汇编语言代码需要访问的那部分,该结构的定义因不同的处理器而不同。

image-20210517115807822

进程的创建

fork()

vfork()

clone()

进程和线程

​ 进程是操作系统资源分配的基本单位,线程是cpu调度的基本单位

​ 在linux中,进程和线程都是用同一个数据结构Task,每个进程有自己独立的虚拟内存地址,内存是每个进程隔离的。线程是进程的一部分,一个进程可以有多个线程。线程和进程共享地址空间(页表一样),不过线程有自己独立的栈空间,一个进程中所有的线程的TGID都相同,都等于第一个进程的PID。

主要区别:是否共享内存地址

  • 进程切换:进程间内存不共享,切换(内核栈和)硬件上下文,切换页表(内存地址空间)

  • 线程切换:线程共享内存地址,所以线程间切换只需切换(内核栈和)硬件上下文

切换开销:

  1. 是否切换内存地址空间
  2. 隐藏的消耗,切换地址空间之后,很多缓存都失效(比如TLB),所以运行相对来说慢了,

如何避免:

  1. cas不切换上下文
  2. 使用最少线程
  3. 协程
进程间通信方式

进程用户态之间进行同步和数据交换

linux里面进程之间的通信方式

  • 管道(进程之间的生产者消费者模式,一些进程写数据,一些进程读数据)

    • 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系

    • 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

    ls | less 命令

  • 信号量(同步方式):控制多个进程对共享资源的访问

  • 消息(消息队列):消息队列是由消息的链表组成

  • 共享内存区(有一块地址,所有进程可以看到):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,效率高,快

    mmap 内存映射

    shmat 建立共享内存

  • 套接字(socket):可以在不同计算机的进程之间交互数据,也可以在同一计算机里进程交互

  • 信号(singal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

线程间通信方式

java中线程的通信方式

包含上面进程间的通信方式

  • 共享变量 volatile

  • 锁方式

    • synchronized 里面Object的wait(),notify(),notifyAll()方法
    • ReentrantLock和Condition结合使用
  • 并发工具

    • LockSuport park() unpark()
    • CountDownLatch CyclicBarrier
  • Thread join() future

中断

​ 中断是一种可以使CPU和硬件设备进行通信的技术。

​ 中断控制的主要优点是只有在I/O需要服务时才能得到处理器的响应,而不需要处理器不断地进行查询。由此,最初的中断全部是对外部设备而言的,即称为外部中断。程序内部也可以发生中断,称为软中断。

​ 中断号:一般系统将所有的中断信号统一进行了编号,这个号称为中断号

​ 中断向量表:中断类型号与相应中断源的中断处理程序入口地址之间的连接表;

​ 中断服务程序:发生中断时所执行的中断代码

image-20210519110422839

系统调用

image-20210519111201082

​ 系统调用是应用程序与内核交互的一种方式。系统调用作为一种接口,通过系统调用,应用程序能够进入操作系统内核,从而使用内核提供的各种资源,比如操作硬件,开关中断,改变特权模式等等。首先,系统调用是一个软中断,既然是中断那么一般就具有中断号和中断处理程序两个属性,Linux 使用 0x80 号中断作为系统调用的入口。

主要作用:

  • 系统调用在底层硬件和用户空间之间建立了一个抽象的接口
  • 系统调用让操作系统变得更加稳定和安全
  • 系统调用便于实现系统虚拟化
  1. 内核实现了很多不同的系统调用。

  2. 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数

  3. 使用eax寄存器传递系统调用号。

缺页中断

​ 在Linux中,进程和内核都是通过页表PTE访问一个物理页面的,如果无法访问到正确的地址,将产生page fault(缺页异常)。

常见处理方式:

  • 页表相关索引(PTE)不存在:分配物理页,修改页表,建立虚拟地址和页帧的关系
  • 页表相关索引(PTE)存在,页不在内存中:swap,页面置换,把硬盘中的内存数据置换到内存中
  • 标记写时复制:写时复制处理逻辑
swap技术

​ 系统将内存暂时不需要的部分写入到硬盘,实现了对于扩展内存容量,叫虚拟内存,使得应用程序可以使用比实际物理内存更大的内存空间。内存数据切入切出基于页面置换算法

常见置换算法

  • 先进先出计算

  • 第二次机会算法

    为了避免FIFO算法将重要的页换出内存,Second Chance算法提供了一些改进。Second Chance算法在将页面换出内存前检查其使用位,如果其使用位为1,证明此页最近有被使用,于是不把它置换出内存,但是把其使用位置为0,随后检查下一个页面,直到发现某页的使用位为0,将此页置换出内存

  • LRU最近最少使用算法

    每次使用过的数据放在数据块的最前端,淘汰表尾的数据

进程调度

​ 发生在内核态,主要作用就是从就绪队列中挑选出一个进程用来执行

常见调度算法

  • 先来先服务调度算法
  • 短作业优先调度调度算法
  • 高优先权调度算法
  • 时间片轮转调度算法
进程切换

image-20210519144134037

  • 地址空间切换

    进程地址空间使用mm_struct结构体来描述,这个结构体被嵌入到进程描述符task_struct中,mm_struct结构体将各个vma组织起来进行管理,其中有一个成员pgd至关重要,地址空间切换中最重要的是pgd的设置。

    image-20210519144836816

  • 硬件上下文切换

    硬件上下文切换的时候,保存相关寄存器的值到cpu_context结构中。当进程再次被调度回来的时候,通过cpu_context中保存的ip回到了cpu_switch_to的下一条指令继续执行,而由于cpu_context中保存的sp导致当前进程回到自己的内核栈,经过一系列的内核栈的出栈处理,最后将原来保存在pt_regs中的通用寄存器的值恢复到了通用寄存器,这样进程回到用户空间就可以继续沿着被中断打断的下一条指令开始执行,用户栈也回到了被打断之前的位置,而进程访问的指令数据做地址转化也都是从自己的pgd开始进行一切对用户来说就好像没有发生一样。

    image-20210519144852995

进程切换过程描述

  1. 用户态进程X正在运行。

  2. 发生中断(包括硬件中断,异常和系统调用),此时硬件完成以下动作:

    首先找到被中断进程的内核栈,加载内核栈的ss:esp到cpu的ss和esp寄存器。

    将当前cpu的上下文(ss:esp(用户态堆栈)/eflags/cs:eip)压入被中断进程X的内核堆栈。

  3. SAVE_ALL,按pt_regs数据结构保存现场。此时就完成了中断上下文的切换,即从进程X的用户态切换到了进程X的内核态。

  4. 在中断处理过程中或中断返回前调用了schedule函数,其中的switch_to做了关键的进程上下文的切换。将当前用户进程X的内核栈切换到用户进程Y的内核栈,并完成eip等寄存器的切换

  5. schedule()函数执行完以后,使用restore_all恢复现场。注意此时运行的是用户进程Y调用schedule函数的那个中断的代码。

  6. 接着执行iret,硬件恢复现场,从Y进程的内核堆栈中弹出2中硬件完成的压栈内容,此时完成了中断上下文的切换,即从进程Y的内核态返回到进程Y的用户态。

  7. 继续运行用户态进程Y

用户态进入内核态时,使用的内核栈就是当前进程的内核栈

文件系统

image-20210519172035418

文件类型
  • - 常规文件
  • d 目录文件
  • b block device 块设备文件,如硬盘;支持以block为单位进行随机访问
  • c character device 字符设备文件,如键盘;支持以character为单位进行线性访问
  • l symbolic link 符号链接文件,又称软链接
  • p pipe 命名管道文件
  • s socket 套接字文件
文件系统类型
  1. 基于磁盘的文件系统

    ext xfs

  2. 基于内存的文件系统

    proc sysfs

  3. 基于网络的文件系统

    nfs

虚拟文件系统

​ VFS即虚拟文件系统是Linux文件系统中的一个抽象软件层;因为它的支持,众多不同的实际文件系统才能在Linux中共存,跨文件系统操作才能实现。 VFS借助它四个主要的数据结构即超级块、索引节点、目录项和文件对象以及一些辅助的数据结构,向Linux中不管是普通的文件还是目录、设备、套接字等 都提供同样的操作界面,如打开、读写、关闭等。只有当把控制权传给实际的文件系统时,实际的文件系统才会做出区分,对不同的文件类型执行不同的操作。由此 可见,正是有了VFS的存在,跨文件系统操作才能执行,Unix/Linux中的“一切皆是文件”的口号才能够得以实现。

  • 超级块 super block
  • 索引结点 inode
  • 目录项
  • 文件对象

image-20210519173828517

超级块

​ 存放系统中已安装的文件系统的所有信息。对于基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块。超级块时文件系统更多心脏,它记录的信息主要有:block与inode的总量、使用量、剩余量,文件系统的挂载时间,最近一次写入数据的时间等。

1.struct super_block {  
2.    .....
3.    unsigned long   s_blocksize;  
4.    unsigned char   s_blocksize_bits;  
5.    unsigned char   s_dirt;  
6.    unsigned long long  s_maxbytes; /* 最大的文件长度 */  
7.    struct file_system_type *s_type;  
8.    struct super_operations *s_op;  
9.    struct dentry   *s_root;  
10.    struct list_head    s_inodes;   /* 所有inode的链表 */  
11. .....
12.};  
索引结点

​ 索引节点对象存储了文件的相关信息,代表了存储设备上的一个实际的物理文件。当一个 文件首次被访问时,内核会在内存中组装相应的索引节点对象,以便向内核提供对一个文件进行操作时所必需的全部信息;保存的其实是实际的数据的一些信息,这些信息称为“元数据”(也就是对文件属性的描述)。例如:文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,指向存储该内容的磁盘区块的指针,文件分类等等。这些信息一部分存储在磁盘特定位置,另外一部分是在加载时动态填充的。

1.struct inode {  
2.    .....
3.    struct hlist_node    i_hash;     /* 散列表,用于快速查找inode */  
4.    struct list_head    i_list;        /* 索引节点链表 */  
5.    struct list_head    i_sb_list;  /* 超级块链表超级块  */  
6.    struct list_head    i_dentry;   /* 目录项链表 */  
7.    unsigned long        i_ino;      /* 节点号 */  
8.    atomic_t        i_count;        /* 引用计数 */  
9.    unsigned int        i_nlink;    /* 硬链接数 */  
10.    uid_t            i_uid;          /* 使用者id */  
11.    gid_t            i_gid;          /* 使用组id */  
12.    umode_t          i_mode;            /* 文件访问权限 */
13.    struct timespec        i_atime;    /* 最后访问时间 */  
14.    struct timespec        i_mtime;    /* 最后修改时间 */  
15.    struct timespec        i_ctime;    /* 最后改变时间 */  
16.    const struct inode_operations    *i_op;  /* 索引节点操作函数 */  
17.    const struct file_operations    *i_fop;    /* 缺省的索引节点操作 */  
18.    struct super_block    *i_sb;              /* 相关的超级块 */  
19.    struct address_space    *i_mapping;     /* 相关的地址映射 */  
20.    struct address_space    i_data;         /* 设备地址映射 */  
21.    .....
}; 
目录项

​ 引入目录项的概念主要是出于方便查找文件的目的。一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。如,在路径/home/source/test.c中,目录 /, home, source和文件 test.c都对应一个目录项对象。不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,VFS在遍历路径名的过程中现场将它们逐个地解析成目录项对象。文件的层级表示关系。

1.struct dentry {//目录项结构  
2.  
3.    struct inode *d_inode;           /*相关的索引节点*/  
4.    struct dentry *d_parent;         /*父目录的目录项对象*/  
5.    struct qstr d_name;              /*目录项的名字*/  
6.   
7.    struct list_head d_subdirs;      /*子目录*/  
8.    struct dentry_operations *d_op;  /*目录项操作表*/  
9.    struct super_block *d_sb;        /*文件超级块*/  
10.   ...
11.};  
12.  
13.struct dentry_operations {  
14.    //判断目录项是否有效;  
15.    int (*d_revalidate)(struct dentry *, struct nameidata *);  
16.    //为目录项生成散列值;  
17.    int (*d_hash) (struct dentry *, struct qstr *);  
18.    ……  
19.};  
文件对象

​ 文件对象是已打开的文件在内存中的表示,主要用于建立进程和磁盘上的文件的对应关系。它由sys_open() 现场创建,由sys_close()销毁。文件对象和物理文件的关系有点像进程和程序的关系一样。当我们站在用户空间来看待VFS,我们像是只需与文件对象打交道,而无须关心超级块,索引节点或目录项。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已经打开的文件,它反过来指向目录项对象。一个文件对应的文件对象可能不是唯一的,但是其对应的索引节点和目录项对象是唯一的。

1.struct file {  
2.    ……  
3.    struct list_head        f_list;        /*文件对象链表*/  
4.    struct dentry          *f_dentry;       /*相关目录项对象*/  
5.    struct vfsmount        *f_vfsmnt;       /*相关的安装文件系统*/  
6.    struct file_operations *f_op;           /*文件操作表*/  
7.    ……  
8.};  
9.   
10.struct file_operations {  
11.    ……  
12.    //文件读操作  
13.    ssize_t (*read) (struct file *, char __user *, size_tloff_t *);  
14.    //文件写操作  
15.    ssize_t (*write) (struct file *, const char __user *, size_tloff_t *); 
16.    int (*readdir) (struct file *, void *, filldir_t);  
17.    //文件打开操作  
18.    int (*open) (struct inode *, struct file *);  
19.    ……  
20.};  
文件与进程的关系
1.struct task_struct {  
2.    ......  
3./* filesystem information */  
4.    struct fs_struct *fs;               /* 建立进程与文件系统的关系 */  
5./* open file information */  
6.    struct files_struct *files;         /* 打开的文件集 */  
7.    ......  
8.}  

image-20210519180222159

读取文件流程

​ 对文件进行读操作时,需要先打开它。打开一个文件时,会在内存组装一个文件对象,希望对该文件执行的操作方法已在文件对象设置好。所以 对文件进行读操作时,VFS在做了一些简单的转换后(由文件描述符得到其对应的文件对象;其核心思想是返回 current->files->fd[fd]所指向的文件对象),就可以通过语句 file->f_op->read(file, buf, count, pos)轻松调用实际文件系统的相应方法对文件进行读操作了。

image-20210519180423925

EXT2 文件系统

​ 磁盘读写的最小单位是扇区,扇区的大小为512B ,如果要读写大量文件,那这读写的效率会非常低,所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块,块大小一般为扇区的2的n次方倍,这将大大提高了磁盘的读写的效率。

image-20210519181804457

  • 超级块,包含的是文件系统的重要信息,比如 inode 总个数、块总个数、每个块组的inode个数、每个块组的块个数等等。

  • 块组描述符,包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息」。

  • 数据块位图和 inode 位图, 用于表示对应的数据块或 inode 是空闲的还是被使用中。

  • inode 列表,包含了块组中所有的 inode,inode 用于保存文件系统中与各个文件和目录相关的元数据和指向属于该文件数据块的指针。

  • 数据块,包含文件的数据。

数据的存储:

  1. 对于常规文件,文件的数据正常存储在数据块中。

  2. 对于目录,该目录下的所有文件和一级子目录的目录名存储在数据块中

image-20210519182044340

读取/var/message文件过程:

  1. 找出"/"根目录inode指向的data block。

  2. 在data block里找到var目录的inode号,通过inode号在inode表中找到var目录inode的内容。

  3. 根据var目录inode内容找到data block,在data block里找到message文件的inode号。

  4. 根据message文件的inode号找到inode,在inode里面找到数据块。

硬链接和软链接

硬链接:是给文件一个副本,同时建立两者之间的链接关系。修改其中一个,与其链接的文件同时被修改。如果删除其中任意一个,其余的文件将不受影响。

软链接:也叫符号连接,他只是对源文件在新的位置建立一个“快捷方式(借用一下wondows常用词)”,所以,当源文件删除时,符号链接的就仅仅剩下个文件名了,找不到原来的文件,当然删除这个链接,也不会影响到源文件。

  1. 硬链接原文件和新文件的inode编号一致。而软链接不一样。

  2. 对原文件删除,会导致软链接不可用,而硬链接不受影响。

  3. 对原文件的修改,软、硬链接文件内容也一样的修改,因为都是指向同一个文件内容的。

image-20210519182904562