操作系统总结
Linux操作系统
一.基础知识
1.Linux系统启动过程
(1)内核的引导:开机之后首先是BIOS开机自检,按照BIOS中设置的启动设备(通常为硬盘)来启动.操作系统接管硬件之后,首先读入/boot目录下的内核文件
(2)运行init(init进程为系统所有进程的起点,没有这个进程,系统中任何进程都不会启动)
- Linux中为守护进程,init进程的一大任务,就是去运行这些开机启动的程序
- Linux允许为不同的场合,分配不同的开机启动程序,为运行级别
(3)系统初始化:激活交换分区,检查磁盘,加载硬件模块以及其他一些需要优先执行的任务(BIOS会检测磁盘的第0磁头第0磁道第1扇区的内容是否以0x55aa结尾,如果是,就认为第一扇区中存放的为boot引导程序,顺便把它复制到物理内存0x7c00处,接着跳到此处开始执行)
(4)建立终端:显示一个文本登录界面,在界面中提示用户输入用户名,而用户输入的用户将作为参数传给login程序来进行身份验证
(5)用户登录系统
- 命令行登陆
- ssh登陆
- 用户界面登陆
2.Linux系统目录结构
ls:
系统启动需要的: /boot:启动linux的时候使用的一些核心文件,包括连接文件以及镜像文件 /etc:存放所有的系统管理所需要的配置文件和子目录,一旦删除可能导致系统不能正常启动 /lib:系统中最基本的动态链接共享库 /sys:内核设备树的直观反映,当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中. 指令集合: /bin:存放着经常使用的命令 /sbin:系统管理员使用的系统管理程序 外部文件管理: /dev:Linux的外部设备 /media:系统会自动识别一些设备,当识别了之后,linux会把识别的设备挂载到这个目录下 /mnt:为了让用户临时的挂载别的文件系统的 临时文件: /run:临时文件,存储系统启动以来的信息.当系统重启的时候,这个目录下的文件应该被删掉或者是清除 /lost+found:当系统非法关机的时候,这个文件中会存一点东西 /tmp:存放一些临时文件 账户: /root:系统管理员的用户主目录 /home:用户的主目录 /usr:很多应用程序和文件都放在这个目录下 /usr/bin:系统用户使用的应用程序 /usr/sbin:超级用户使用的比较高级的管理程序和系统守护程序(root) /usr/src:内核源代码默认的放置目录 运行过程中需要使用: /var:放一些经常被修改的东西,包括日志文件(/var/log) /proc:虚拟的目录,它是系统内存的映射,可以直接访问这个目录来获取系统信息.管理内存空间. 扩展用的: /opt:给主机额外安装软件所摆放的目录(默认为空) /srv:一些服务启动之后需要提取的数据(不用服务器就是空) 如果一个目录或者是文件以一个.开始,表示这是一个隐藏目录或者是文件.即以默认方式查找的时候,不显示该目录或者是文件.
3.文件基本属性
Linux中第一个字符代表这个文件时目录,文件或者是链接文件等等
- 若为[d]则为目录
- 若为[-]ze为文件
- 若为[l]则为链接文档
- 若为[b]则为装配文件中的可供储存的接口设备
- 若为[c]则为装置文件中的串行端口设备
[rwx]为三个参数(读,写,执行)
属主--属组--其他
-R:递归的变更,连同此目录虾的所有文件都会变更
4.Linux文件与目录管理
Linux的目录为树状结构,最顶级的目录为根目录,其他的目录通过挂载可以将它们添加到树中,通过接触挂载就可以移除它们
pwd:显示当前目录
mkdir:创建一个新的目录
rmdir:删除一个空的目录(只能删除空的目录)
rm:删除文件或者是目录(-f为强制删除,-r为递归删除)
cp:复制文件或者是目录
man:查看各个命令的使用文档
文件内容查看
cat:由第一行开始显示文件内容
tac:由最后一行开始显示
nl:显示的时候输出行号
链接概念:
硬链接:通过索引节点来进行连接.删除其中任何一个都不影响另外一个的访问(ln命令)
软链接:类似于Windows的快捷方式
Linux磁盘管理:
df
df:查看文件系统的磁盘空间占用情况
df -h:将容量结果以易读的格式显示出来
df -aT:将系统内的所有特殊文件格式以及名称都列出来
df -h /etc:将/etc底下的可用磁盘容量以易读的容量格式显示
du:列出当前目录下的所有文件夹容量
fdisk:
fdisk -l:列出所有分区的信息
fdisk /:列出系统中的根目录所在的磁盘
fdisk /etc:查询该磁盘内的相关信息
fsck:检查和维护不一致的文件系统
mount:磁盘挂载(将一个设备[通常是存储设备]挂接到一个已经存在的目录上.访问这个目录就是访问该存储设备,挂载就是建立一个软件层面目录和硬件层面分区的映射关系,只要是系统可以看到,那么就已经挂载了,只是很多时候你没有看到)
二.Geek_time_Linux操作系统
1.入门准备
(1)学好操作系统的前置知识:
1)态度,不要等一切都准备好了再开始
2)C语言+数据结构与算法+编译原理+计算机组成
3)方法:三遍学习法+PPT笔记法
读薄:理解原理
读厚:遇到不会的地方,搜索解决,一旦了解了,就回到主线,然后功课下一个知识点
读薄:将知识变为自己的东西
4)做好练习,用好音频:认真做课后题,思考题目背后的知识点
附:
学习一遍,回忆一遍,用的时候:搜索一遍
(2)Linux的学习路径
1)抛弃旧的思维习惯,熟练使用Linux命令行(鸟哥的linux私房菜)
2)通过系统调用或者是glibc,学会自己进行程序设计(UNIX环境高级编程)
3)了解Linux内核机制,反复研习重点突破(深入理解Linux内核+庖丁解牛Linux内核分析)
4)阅读Linux内核代码,聚焦核心逻辑和场景(Linux内核源代码场景分析)
5)实验定制化Linux组件
6)实践(并行与并发,锁与保护,扩展性和兼容性)
(3)Linux内核体系(系统调用子系统)
1)进程管理子系统(/kernel,/arch)
(1 调度
(2 任务管理
(3 同步
(4 CPU
2)内存管理子系统(/mm)
(1 虚拟内存
(2 页表
(3 swap
(4 伙伴
(5 slab
(6 物理内存
(7 内存映射
3)文件子系统(/fs)
(1 VFS
(2 缓存
(3 硬件系统
4)设备子系统(/driver,/char,/block)
(1字符设备
(2块设备
5)网络子系统
套接字
协议栈
网络设备
2.操作系统综述
(1)系统调用
1)fork
(1 父进程调用fork创建进程,子进程将各个子系统为父进程创建的数据结构都拷贝了一遍,甚至连程序代码也是拷贝的
(2 如果当前进程为子进程,则返回值为0,如果为父进程,则返回值是子进程的进程号
(3 如果为父进程,则继续做之前做的是;如果是子进程,需要请求另一个系统调用execve来执行另一个程序
(4 操作系统启动的时候创建一个"祖宗进程"
(5 系统调用waitpid,父进程可以调用它,将子进程的进程号作为参数传给他
2)内存管理
(1代码段:放代码
(2数据段:放数据
(3只有真正的写入数据的时候,发现没有对应的内存,才会触发一个中断,分配物理内存
(4堆分配内存的系统调用:
brk:当分配的内存数量比较小的时候,会和原来的堆的数据连在一起
mmap:当办
中间一段数据丢失了!!
补
3.进程&线程
(1)调度
1)一个CPU上有一个队列,CFS的队列是一颗红黑树,树上的每一个节点都是一个sched_entity,每一个sched_entity,每一个sched_entity都属于一个task_struct,task_struct里面有指针指向这个进程属于哪一个调度类,在调度的时候,依次调用调度类的函数,从CPU的队列中取出下一个进程。
2)进程和线程都是task,一起调度
3)上下文切换做的事情:
(1)切换进程空间(虚拟内存)
(2)切换寄存器和CPU上下文
上下文相关:
当切换任务的时候,需要记录任务当前的状态和获取下一任务的信息和地址(指针),因此上下文是指某一时间点CPU寄存器和程序计数器的内容,广义上还包括内存中进程的虚拟地址映射信息
进程上下文切换:
- 内核空间态包括内核的堆栈,寄存器等;用户态资源包括虚拟内存,栈,变量,正文
- 系统调用是在内核态完成的。需要进行两次CPU上下文切换
线程上下文切换:
- 不同进程之间线程的切换,过程和进程上下文大致相同
- 进程内部的线程上下文切换。不需要切换进程的用户资源,只需要切换线程私有的数据和寄存器等。会比进程上下文切换所消耗的资源少,所以多线程比多进程更有优势
中断上下文切换:
快速响应硬件的事件,切换过程类似于系统调用
进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存,栈,全局变量等用户空间的资源,还包括了内核堆栈,寄存器等内核空间的状态
内核中的任务调用,实际上的调度对象
4)所谓的进程切换就是将某个进程的thread_struct里面的寄存器的值,写到CPU的TR指向的tss_struct,对于CPU来说,就是完成了切换
5)创建进程的时候,调用的系统调用是fork,在copy_process函数里面,会将file_struct,fs_struct,sighand_struct,mm_struct都复制一遍,从此父进程和子进程各用各自的数据结构。而创建线程的时候,调用的是系统调用clone,在copy_process函数里面,五大结构仅仅是引用计数加1,也即线程共享进程的数据结构。[x86架构下,fork、vfork、和__clone的库函数最终调用的都是clone系统调用]
6)
进程和线程
为什么协程切换的代价比线程切换低
Linux 中 mmap() 函数的内存映射问题理解
Linux的线程模型
7)内核页表和linux的伙伴系统是不是有冲突?
分配是针对物理地址而言,而映射是针对逻辑地址而言。
上一个问题的另一个解答
8)应用程序性能优化
- 编译器优化
- 算法优化:使用复杂度更低的算法
- 异步优化:避免程序因为等待某个资源而一直等待,提高程序的并发能力
- 多线程代替多进程:线程的上下文切换并不切换进程的地址空间
- 使用缓存
系统优化:
- CPU绑定:将进程绑定到一个或多个CPU上
- CPU独占:不允许其他的进程来使用这些CPU
- 为进程设置资源限制:使用linux cgroups来设置进程的CPU上限
- NUMA优化:支持NUMA的处理器会被划分为多个node
- 优先级的调整
- 中断负载均衡:将中断处理过程自动负载均衡到多个CPU中
4.内存管理
(1)内存管理部分
1)对于内存的访问,用户态的进程使用虚拟地址。内核态基本也是虚拟地址。
2)到了内核之后,虽然内核栈是分离的,但是无论是从哪一个进程来的,看到的都是一个同一个内核空间
3)内核代码部分也是ELF格式的
4)内存管理需要做的事
- 虚拟内存空间的管理
- 物理内存的管理
- 内存映射
5)查看进程内存空间的布局
cat /proc/$pid/maps
6)Linux线程为共享资源的进程
7)对于32位系统,最大可以寻址4G,其中用户态虚拟地址空间事3G,内核态是1G
对于64位系统,虚拟地址只使用了48位,就相当于48位地址空间一半的位置,共128T。同样,内核空间也是128T,内核空间和用户空间之间隔着很大的空隙,以此来进行隔离
8)32位的内核态虚拟地址空间一共就1G,占绝大部分的前896M,我们称为直接映射区。这一块和物理内存是非常简单的映射关系,其实就是虚拟地址减去3G,就可以得到物理内存的位置。
9)一个进程要运行起来需要的内存结构
用户态: - 代码段,全局变量,BSS
- 函数栈
- 堆
- 内存映射区
内核态: - 内核的代码,全局变量,BSS
- 内核数据结构例如task_struct
- 内核栈
- 内核中动态分配的内存
5)内存的分配与回收
- malloc()是C标准库提供的内存分配函数,对应到系统调用上,有两种实现方式:brk()和mmap()
- 对于小块内存(小于128k),C标准库使用brk()来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放之后不会立即归还系统,而是被缓存起来,这样可以重复使用。brk()方式可以减少缺页异常的发生,提高内存访问效率。不过,由于内存没有归还系统,在内存繁忙的时候,繁忙的内存分配和释放会造成内存碎片。
- 对于大块的内存(大于128K),直接使用内存映射mmap()来分配,也就是在文件映射段找一块空闲的内存分配出去。由于mmap方式分配的内存,会在释放的时候直接归还系统,所以每次都会发生缺页异常。在内存繁忙的时候,繁忙的内存分配会导致大量的缺页异常,使内核的管理负担加重。这也是malloc只对大块的内存进行分配的原因。
在用户空间,malloc通过brk()分配的内存,在释放的时候并不立即归还系统,而是缓存起来重复的使用。在内核空间,Linux则通过slab分配器来管理小内存。可以将slab看做构建在伙伴系统上的一个缓存,主要的作用就是分配并释放内核中的小对象。对于内核来说,如果只是分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。
系统回收内存的三种方式:
- 回收缓存:使用LRU算法回收最近最少使用的内存页面
- 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中
- 杀死进程,内存紧张的时候会直接杀掉占用大量内存的进程
Buffer是对磁盘数据的缓存,而Cache是文件数据的缓存,它们既会用在读请求中,也会用在写请求中
(2)物理内存管理
1)访问方式
- CPU是通过总线区访问内存的,这是最经典的内存使用方式。在这种模式中,CPU会有多个,在总线的另一侧,所有的CPU访问内存都要经过总线,而且距离是一样的,这种模式叫做SMP(对称多处理器)
- NUMA(非一致内存访问),在这种模式下,内存不是一整块。每一个CPU都有自己的本地内存,CPU访问本地内存不用经过总线,因而速度要快很多,每一个CPU和内存在一起,称为一个NUMA节点。但是在本地内存不足的时候,每个COU可以去另外的NUMA节点申请内存,这个时候访问延时就会比较长
浅解NUMA机制
Linux控制NUMA的命令是numactl,NUMA往往是非连续内存模型。而非连续内存模型不一定就是NUMA,有时候一大片内存的情况下,也会有物理内存地址不连续的情况
深挖NUMA
2)为了满足操作系统中小内存块的需要,Linux系统采用了一种称为slab allocator的技术,用于分配称为slab的一小块内存。它的基本原理是从内存管理模块申请一整块页,然后划分为多个小块的存储池,用复杂的队列来维护这些小块的状态
2)伙伴系统 - 伙伴系统:首先在循环中看当前节点的zone,如果找不到空闲页,则再看备用节点的zone,每一个zone,都有伙伴系统维护的各种大小的队列。找到合适大小的队列,把页面取下来。
- 从当前的order,也即指数开始,在伙伴系统的free_area找2^order大小的页块。如果链表的第一个不为空,就找到了;如果为空,就到更大的order的页块链表里面去找。找到以后,除了将页块从链表中取下来,还要把多余的部分放到其他页块链表里面。expand就是干这个事情的。
(3)内核态内存映射
1)如果要申请小块内存,就用brk。如果要申请一大块内存,就用mmap。对于堆得申请,mmap就是映射内存空间到物理内存
2)有的异常会产生中断,有的异常时应用层的,可以不产生中断
3) - 物理内存根据NUMA架构分结点,每一个节点里面分区域。每一个区域里面再分页
- 物理内存通过伙伴系统(劫富济贫)进行分配。
- 对于内存的分配需求,可能是来自内核态,也可能是来自用户态
- 对于内核态,再分配大内存的时候,直接使用伙伴系统,分配后转换为虚拟内存,访问的时候需要通过内核页表进行映射;对于小内存而言,使用slub分配器,将伙伴系统分配出来的大内存切成小内存进行分配
- 对于用户态的内存分配,直接调用mmap系统调用分配或者是调用malloc。调用malloc的时候,如果分配小的内存,就用sys_brk系统调用;如果分配大的内存,还是用sys_mmap系统调用。正常情况下用户态的内存都是可以换出的,然而一旦发现内存中不存在,就会调用do_page_fault
4)Linux中的伙伴系统和slab机制
how the slub work?
5)
5.文件系统
(1)文件系统
1)对文件系统提出的要求
- 文件系统要有严格的组织形式,使得文件能够以块为单位进行存储
- 文件系统中也要有索引区,用来查找一个文件分成的多个块都在什么位置
- 对于经常被读取和写入的部分,文件系统应该有缓存层
- 文件应该以文件夹的形式组织起来,方便管理和查询
- Linux内核要在自己的内存中维护一套数据结构,用来保存哪一些文件被哪一些进程打开和使用
2)文件操作
格式化相关
Windows中,常见的格式为NTFS。在Linux下面,常用的是ext3或者ext4
Linux中,格式化之后的硬盘需要挂在某个目录下面,才能作为普通的文件系统进行访问
Linux中的文件标识
- d表示文件夹
- c表示字符设备文件
- b表示块设备文件
- s表示套接字socket文件
- l表示符号链接,即软链接
(2)硬盘文件系统&&虚拟文件系统&&文件缓存
1)虚拟文件系统
在内核,整个进程都需要为打开的文件,维护一定的数据结构
在内核,整个系统打开的文件,也需要维护一定的数据结构
(3)VFS
1)分类
- 基于磁盘的文件系统,也就是直接把数据直接存储在计算机本地挂载的磁盘中,常见的Ext4,XFS,OverlayFS都是这一类系统
- 基于内存的文件系统,不需要任何磁盘分配空间,但是会占用内存。经常用到的/proc文件系统就是一种常见的虚拟文件系统
- 网络文件系统,也就是可以访问其他计算机数据的文件系统。例如NFS,SMB,iSCSI等
(4)epoll
1)性能
- epoll使用红黑树,在内核中管理文件描述符的集合,这样就不需要应用程序在每次操作的时候传入,传出这个集合
- epoll使用事件驱动的机制,只关注有I/O事件发生的文件描述符,不需要轮询扫描整个集合
6.输入输出系统
(1)磁盘
- 在Linux中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。每一个块设备都会被赋予两个设备号,分别是主,次设备。主设备号用在驱动程序中,用来区分设备类型;而次设备号是用来给多个同类设备编号。
三.面试题部分
1.程序什么时候应该使用线程?什么时候单线程效率高?
- 无论什么时候只要是能用单线程就不用多线程,只有在需要相应时间要求比较高的地方才用多线程。某操作允许并发而且该操作有可能阻塞的时候,用多线程
- 多线程的目的:吞吐量+伸缩性
- 多线程:可以抢占更多的系统资源,加快可以分割为独立执行单元的程序段运行;提供良好的操作感受;相应多个并行的请求
- 什么时候单线程效率高:对于处理时间短的服务或者启动频率高的要使用单线程,相反使用多线程
2.惊群现象
多进程(多线程)在同时阻塞等待同一个事件的时候,如果等待的事件发生,那么他就会唤醒等待的所有进程,但是最终只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应
为了确保只有一个进程获得资源,需要对资源操作进行加锁保护
3.线程安全
一般来说,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用的时候,它会一直产生正确的结果
一个不论运行时如何调度都不需要调用方提供额外的同步和协调机制还能正确运行的类是线程安全的
线程安全的级别:
- 不可变:string,integer,long这些都是final类型的类,任何一个线程都改变不了它的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程下使用
- 绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施
- 相对线程安全:像vector,add,remove都是原子的操作,不会被打断。如果有一个线程在遍历vector,同时有一个线程在add这个vector,会出现ConcurrentModificationException
- 线程非安全:ArrayList,LinkedList,HashMap都是线程非安全的类
死锁&&活锁&&饥饿
- 死锁:多个线程互相占用对方的资源的锁,而又相互等待对方释放锁,如果没有外力干预,这些线程将一直处理阻塞的假死状态,形成死锁
- 活锁:多线程中出现了相互谦让,都主动将资源让给别的线程使用,这样这个资源在多个线程之间跳动得不到执行
- 饥饿:优先级高的线程一直抢占优先级低的线程的资源,导致低优先级无法得到执行
- 无锁:所有的线程都可以访问并修改同一个资源,但是同时只有一个线程能够修改成功
Java实现线程的方式:
- 继承Thread类实现多线程
- 实现Runnable接口方式实现多线程
- 使用 ExecutorService、Callable、Future 实现有返回结果的多线程
- 通过线程池创建线程
多线程的同步:
- synchronized关键字
- Lock锁实现
- 分布式锁
守护线程就是守护用户线程
Synchronized的用法
- 锁类
- 锁方法
- 锁代码块
乐观锁&&悲观锁
自旋锁:让当前线程不停地在循环体之内执行实现的,当循环的条件被其他线程改变的额时候才进入临界区
4.其他问题部分
Linux中线程是如何切换的?
- 从Linux内核角度来说,“线程”是作为进程来实现的,“线程”的调度方式和进程一样“
多线程和多进程
对比维度 多进程 多线程 总结 数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势 内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优 创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优 编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优 可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优 分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优 - CPU密集型适合用多线程(切换的时候耗费资源较少)
malloc和free是线程安全的(需要加锁),但是不可以重入(使用信号的时候需要注意);多核的情况会严重的影响效率
Linux是如何避免内存碎片的?
- 伙伴系统,用于管理物理内存,避免内存碎片
- 高速缓存Slab层用于管理内核分配内存,避免碎片
Kernel的内存管理是2层分层系统,从上到下为:
- 第一层全部为物理内存:管理器是伙伴系统,最小管理单位为page
- 第二层为slab page:管理器为slab/slub,最小管理单位是2的m次幂的字节块
共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存。当一个进程改变了这块地址中的内容的时候,其他进程都会察觉到这个更改
- 可重入的概念只是和函数访问的变量类型有关,和是否使用锁是没有关系的
- 线程安全和锁的使用关系密切,很多时候线程安全是靠锁来实现的
原子操作:操作一旦开始,就一直运行到结束,中间不会有任何的context switch