文章目录
- 力推操作系统的三门神课
- 操作系统的作用和功能
- 线程、进程和协程的区别
- 并行与并发的区别
- 什么是文件描述符
- 操作系统内核态和用户态的区别
- 用户态切换到内核态的方式
- 大内核和微内核的区别
- 用户级线程和内核级线程的区别
- 线程的七态模型
- 进程调度算法有哪些
- 进程间通信的七种方式
- 进程间同步与互斥的区别
- 什么是僵尸进程和孤儿进程
- linux底层的零拷贝技术
- linux的各种IO模型
- 详解IO多路复用 & epoll原理
- 虚拟内存解决了什么问题?
- 动态链接库与静态链接库的区别
力推操作系统的三门神课
标题 | 链接 |
---|---|
清华《操作系统原理》 | 视频链接 |
哈工大 李治军《操作系统原理》 | 视频链接 |
南大 蒋炎岩《操作系统概述》 | 视频链接 |
操作系统的作用和功能
定义:
- 很难有一个公认、精确的定义,一般是根据功能特点来定义。总的来说,是一个承上(应用程序)启下(计算机硬件资源)的系统软件。
- 对上,操作系统可以管理程序,给应用程序提供服务,确保它们有序、有效地执行。
- 对下,操作系统是计算机资源(cpu、内存、磁盘、外设)的管理者(实现了资源的抽象)。
功能:
- CPU调度器
- 进程和线程管理:进程线程的状态、控制、同步互斥、通信调度等
- 内存管理:物理内存、虚拟内存、分配/回收、地址转换、存储保护等
- 文件管理:文件目录、文件操作、磁盘空间、文件存取控制
线程、进程和协程的区别
(1)进程(process)
程序在数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是系统进行资源分配和调度的基本单位。
进程一般由程序
、数据集
、进程控制块
三部分组成。我们编写的程序
用来描述进程要完成哪些功能以及如何完成;数据集
则是程序在执行过程中所需要使用的资源;进程控制块(PCB)
用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
(2)线程(thread)
线程包含在进程中,是进程中一个单一顺序的控制流,像“线”一样。它是CPU调度的最小单元,一个进程的多个线程在执行不同任务的同时共享进程的系统资源(如虚拟地址空间
,文件描述符
等)。线程由线程ID
、程序计数器(PC)
、堆栈寄存器
和线程控制块
组成。
线程的出现是为了减少任务切换的消耗,提高系统的并发性,实现让一个进程也能执行多个任务。例如一个浏览器网页需要加载TechGuide网站内容、显示文本信息和图片信息。如果使用多个进程来执行这些任务,需要频繁的进行上下文切换和进程间通信。考虑到这些任务是相互关联且共享资源的(它们都要用到网站资源),用一个进程中的多个线程来执行可以减少上下文切换和进程间通信的消耗。
(3)协程(Coroutines)
协程是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理(内核不可见),而完全是由用户程序所控制的,用户空间线程。
当出现IO阻塞的时候,由协程的调度器进行调度,通过将数据流立刻yield()
掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑。协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除Context Switch
上的开销。
链接:https://www.cnblogs.com/Survivalist/p/11527949.html
(4)总结
- 资源
线程是任务调度的最小单位,而进程是操作系统分配资源的最小单位。进程是拥有系统资源的独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源:内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。这里稍微扩展下线程共享和独占的资源,加深理解。
线程共享的环境包括:
- 进程代码段
- 进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)
- 进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。
线程独立的资源包括:
-
线程ID:每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。
-
寄存器组的值:由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
-
线程的堆栈:堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影响。
-
错误返回码:由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。
-
线程的信号屏蔽码:由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
-
线程的优先级:由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
- 系统开销
在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈
和局部变量
,但线程没有单独的地址空间
,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
- 管理方式
进程的实现只能由操作系统内核来实现,而不存在用户态实现的情况。但是对于线程就不同了,线程的管理者可以是用户也可以是操作系统本身(分为用户级和内核级)。
并行与并发的区别
- 并发性(concurrency)
是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生,现在主流的os都是时间片轮询的抢占式调度,同一时刻,只有获得时间片的线程执行。【可以理解为共同出发】 - 并行(parallelism)
是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行。比如,对单核CPU,因为一个CPU一次只能执行一条指令,是无法做到并行,只能做到并发【可以理解为同时进行】
什么是文件描述符
文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表
。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符
。
每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。
inode号是用来储存文件的元信息的区域,比如文件的创建者、文件的创建日期、文件的大小等等,可以使用ls -i
命令查看当前目录中的inode号。
链接:https://blog.csdn.net/cywosp/article/details/38965239
操作系统内核态和用户态的区别
先介绍下为什么会有内核态和用户态,系统的资源是固定且有限的,例如内存、CPU、磁盘、网络端口等,所以需要操作系统对资源进行有效的利用。如果某个应用程序过分的访问这些资源,就会造成整个系统的资源无序混乱的使用,如果不对这种行为进行限制和区分,就会导致资源访问的冲突,甚至出错。所以,Linux内核设计的初衷是,给不同的操作给与不同的权限,用户态与内核态只是不同权限的资源范围。
CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。Intel的CPU将特权级别分为4个级别:RING0(内核)、RING1、RING2、RING3(用户级)。Linux只用到了RING0和RING3。
-
内核态执行内核空间的代码,具有RING0保护级别,有对硬件的所有操作权限,可以执行所有cpu指令集,访问任意地址的内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机。
-
用户态下,具有RING3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接口(System Call)来达到访问硬件和内存。
链接:https://juejin.cn/post/6923863670132850701
用户态切换到内核态的方式
- 系统调用(主动)
用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用。系统调用本身就是一种软件中断,通过操作系统为用户开放的一个中断来实现。
补充说下中断原理(面试装b必备),中断有两个属性,一个称为中断号
(从0开始),一个称为中断处理程序(Interrupt Service Routine, ISR)
。不同的中断具有不同的中断号,并且一一对应。在内核中,有一个数组称为中断向量表(Interrupt vector table)
,这个数组的第n项包含了指向第n号中断的中断处理程序的指针。当中断到来时,CPU会暂时中断当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成以后,CPU会继续执行之前的代码。
中断有两种类型,一种称为硬件中断,这种中断来自于硬件的异常或其他事件的发生,如电源掉电、键盘被按下等;另一种称为软中断,软件中断通常是一条执行int+中断号的指令,使用这条指令可以主动触发某个中断,并执行其中断处理函数。例如在i386下,int 0x80
这条指令会调用第0x80号中断的处理程序。
由于中断号是有限的,操作系统不舍得用一个中断号来对应一个系统调用,而倾向于用一个或少数几个中断号对应所有的系统调用。例如,i386下Windows里绝大多数系统调用都是由int 0x2e
来触发的,而linux则使用int 0x80
来触发所有的系统调用。
那么问题来了,对于同一个中断号,操作系统如何知道是哪一个系统调用要被调用呢?
和中断一样,系统调用都有一个系统调用号,每个系统调用号都唯一对应一个系统调用处理函数。例如Linux里fork
的系统调用号是2,这个系统调用号在执行int指令前会被放置在某个固定的寄存器里,对应的中断代码会受到这个系统调用号,并且调用正确的函数。
链接:
用户态&内核态:https://juejin.cn/post/6923863670132850701
中断原理:https://blog.csdn.net/kang___xi/article/details/80556633
- 异常(被动)
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常
。
什么是缺页异常?CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、IO设备等等,但从CPU发出的访问地址并非是这些外设在地址总线上的物理地址,而是一个虚拟地址,由MMU
将虚拟地址转换成物理地址再从地址总线上发出,CPU给MMU的虚拟地址,在快表和页表都没有找到对应的物理页帧,就会产生一个缺页异常。
缺页异常实际上并不一定是一种错误,而是用虚拟内存来增加程序可用内存空间的一种机制,只有程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才可能进行分配,这就是内存的惰性(延时)分配机制。(这部分涉及到的连续内存分配、非连续内存分配中的分段、分页、段页式,以及虚拟内存、地址翻译、内存换入、换出的部分没有详细展开,后面有部分论述,以后有机会再更新。)
当发生缺页异常时,如果是物理内存中没有对应的页帧,需要CPU打开磁盘设备读取到物理内存中,再让MMU建立VA和PA的映射。
更复杂的情况是,如果操作系统物理内存中恰好也没有空闲页面了,则操作系统必须在物理内存选择一个页面将其移出,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。OPT(最佳,不可能实现)、FIFO(性能差)、LRU(接近OPT,但是开销大)、CLOCK(√)算法
链接:
缺页异常:https://cloud.tencent.com/developer/article/1807351
页面置换算法:https://www.cnblogs.com/fkissx/p/4712959.html
- 硬件中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。
大内核和微内核的区别
- 大内核系统
将操作系统的主要功能模块都作为一个紧密联系的整体运行在核心态,从而为应用提供高性能的系统服务。因为各管理模块之间共享信息,能有效利用相互之间的有效特性,所以具有无可比拟的性能优势。
- 微内核
将内核中最基本的功能(如进程管理等)保留在内核,而将那些不需要在核心态执行的功能移到用户态执行,从而降低了内核的设计复杂性。而那些移出内核的操作系统代码根据分层的原则被划分成若干服务程序,它们的执行相互独立,交互则都借助于微内核进行通信。
微内核结构有效地分离了内核与服务、服务与服务,使得它们之间的接口更加清晰,维护的代价大大降低,各部分可以独立地优化和演进,从而保证了操作系统的可靠性。
但是微内核结构的最大问题是性能问题,因为需要频繁地在核心态和用户态之间进行切换,操作系统的执行开销偏大。因此有的操作系统将那些频繁使用的系统服务又移回内核,从而保证系统性能。
用户级线程和内核级线程的区别
- 用户级线程
是指不需要内核支持而在用户程序中实现的线程,它的线程切换是由用户态程序自己控制线程的切换(yield),不需要内核的干涉。
举个栗子,由于用户线程的透明性,操作系统是不能主动切换线程的,换句话讲,如果 A,B 是同一个进程的两个线程的话, A 正在运行的时候,线程 B 想要运行的话,只能等待 A 主动放弃 CPU,也就是主动调用 pthread_yield 函数。
- 内核级线程
线程切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态。它的优势是可以很好的运用多核CPU。为了实现内核级线程,内核里就需要有用来记录系统里所有线程的线程表。当需要创建一个新线程的时候,就需要进行一个系统调用,然后由操作系统进行线程表的更新。开销会更大。
线程的七态模型
线程的生命周期模型可以从三态 到五态 再到七态逐步扩充,递进介绍下:
- 三态
运行态→等待态:往往是由于等待外设,等待主存等资源分配
或等待人工干预
而引起的。
等待态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
运行态→就绪态 不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器
等。
就绪态→运行态:系统按某种调度策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。
- 五态
五态在三态的基础上引入了新建态和终止态,描述一个进程创建和终结的过程。
NULL→新建态:执行一个程序,创建一个子进程,fork系统调用。
新建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
等待态/就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
终止态→NULL:完成资源回收。
- 七态
七态在五态的基础上引入了挂起就绪态
(ready suspend)和挂起等待态
(blocked suspend)。挂起就绪态
表明进程具备运行条件,但目前在外存中,只有它被对换到内存才能被调度执行。 挂起等待态
表明进程正在等待某一个事件发生且在外存中。其产生的原因是,当系统资源(主要是内存资源)已经不能满足进程运行的要求时,必须把某些进程挂起,对换到磁盘对换区中,释放它占有的某些资源。
等待态→挂起等待态
:操作系统根据当前资源状况和性能要求,可以决定把等待态进程对换到磁盘成为挂起等待态。
挂起等待态→挂起就绪态
:引起进程等待的事件发生之后,相应的挂起等待态进程将转换为挂起就绪态
挂起就绪态→就绪态
:当内存中没有就绪态进程,或者挂起就绪态进程具有比就绪态进程更高的优先级,系统将把挂起就绪态进程转换成就绪态。
就绪态→挂起就绪态
:操作系统根据当前资源状况和性能要求,也可以决定把就绪态进程对换到磁盘成为挂起就绪态。
挂起等待态→等待态
:当一个进程等待一个事件时,原则上不需要把它调入内存。但是在下面一种情况下,这一状态变化是可能的。当一个进程退出后,主存已经有了一大块自由空间,而某个挂起等待态进程具有较高的优先级并且操作系统已经得知导致它阻塞的事件即将结束,此时便发生了这一状态变化。
运行态→挂起就绪态
:当一个具有较高优先级的挂起等待态进程的等待事件结束后,它需要抢占 CPU,而此时主存空间不够,从而可能导致正在运行的进程转化为挂起就绪态。另外处于运行态的进程也可以自己挂起自己。
- 进程挂起原因
- 终端用户的请求
当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来(比如手动调用wait()方法
)。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。
- 父进程的请求
有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各子进程间的活动。
- 负荷调节的需要
当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
- 操作系统的需要
操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
- 对换的需要
常见原因,为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。
进程调度算法有哪些
- 先来先服务(FCFS,first come first served)
该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法
时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配CPU,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃cpu。
- 最短作业优先(SJF,Shortest Job First)
短作业优先又称为短进程优先,这是对FCFS算法
的改进,目的是减少平均周转时间。对预计执行时间短的进程优先分派处理器。如果一个进程正在执行,也就是执行时间越短的进程越先被执行,而且后来的短进程通常不会打断它。
- 最高响应比优先(HRRN,Hight Response Ratio Next)
最高响应比优先算法是对FCFS和SJF
的一种平衡算法。FCFS
只考虑了进程等待的时间长短,而SJF
考虑了进程执行的时间长短,因此这两种算法在某种程度上会降低系统调度性能。HRRN
这种算法既会考虑每个进程的等待时间长短,也会考虑进程预计执行时间长短从中选出响应比最高的进程执行。这种算法是介于FCFS算法
和SJF算法
中间的一种这种算法。
- 时间片轮转(RR,Round-Robin)
该算法采用剥夺策略。每个进程都被分配好一个时间段,也就是它的时间片,这就是该进程允许允许的时间。如果该进程超过了时间片的时间,就会发生时间中断
,调度程序暂停当前进程的执行,将其送到就绪队列的末尾,并通过上下文切换执行当前的队首进程。进程可以未使用完一个时间片,就让出CPU(如阻塞)。
- 抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止(也有在基于事件中断的)当前进程(原优先权最高的进程)的执行,重新将cpu分配给新到的优先权最高的进程。
进程间通信的七种方式
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信,具体实现方式有下面几种:
- 匿名管道
(1)父进程调用pipe()函数
创建管道,得到两个文件描述符fd[0]、fd[1]
,指向管道的读端和写端。
(2)父进程fork
出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
(3)父进程关闭fd[0]
,子进程关闭fd[1]
,即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
限制:
(1)只支持单向数据流,管道只允许单向通信
(2)只能用于具有亲缘关系的进程之间
(3)没有名字
(4)管道的缓冲区是有限的
(5)管道内部保证同步机制,从而保证访问数据的一致性
- 有名管道
有名管道不同于匿名管道之处在于它提供了一个路径名
与之关联,以有名管道的文件形式存在于文件系统
中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。
- 消息队列
(1)消息队列是由消息组成的链表,存放在内核中并由消息队列标识符标识。
(2)消息队列允许一个或多个进程向它写入与读取消息.
(3)管道和消息队列的通信数据都是先进先出的原则。
(4)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比FIFO更有优势。
(5)消息队列克服了信号承载信息量少,只能承载无格式字节流,以及缓冲区大小受限等缺点。
- 信号
信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
(1)不可靠信号
也称为非实时信号,不支持排队,信号可能会丢失,比如发送多次相同的信号,进程只能收到一次,信号值取值区间为1~31;
(2)可靠信号
也称为实时信号,支持排队,信号不会丢失,发多少次,就可以收到多少次,信号值取值区间为32~64。
(3)硬件方式产生信号
用户输入:比如在终端上按下组合键ctrl+C
,产生SIGINT
信号;
硬件异常:CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程
(4)软件方式产生信号
通过系统调用,发送signal信号,例如 kill(),raise(),sigqueue(),alarm(),setitimer(),abort()
补充:信号处理流程
(1)信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统
(2)操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
(3)目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。
常用信号:
(1)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C
键将产生该信号。
(2)SIGKILL:用户终止进程执行信号。shell下执行kill -9
发送该信号。
(3)SIGTERM:结束进程信号。shell下执行kill 进程pid
发送该信号。
(4)The SIGTERM can also be referred as soft kill because the process that receives the SIGTERM signal may choose to ignore it. ------------kill <process_id>
The SIGKILL is used for immediate termination of a process. This signal cannot be ignored or blocked. ------------------kill -9 <process_id>
链接:
https://blog.csdn.net/violet_echo_0908/article/details/51201278
http://gityuan.com/2015/12/20/signal/
- 信号量
信号量本质上是一个计数器,用于多进程对共享数据对象的读取,主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv)。
- P(semaphore):如果semaphore的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
- V(semaphore):如果有其他进程因等待semaphore而被挂起,就让它恢复运行,如果没有进程因等待semaphore而挂起,就给它加1
- 共享内存
-
使得多个进程可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
-
为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
-
由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
补充:为什么共享内存效率高
因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
- 套接字socket
一切皆为文件,网络连接也是一个文件,它也有文件描述符。
socket直接翻译为插座,很形象,建立网络连接就像把插头插在这个插座上,创建一个Socket实例开始监听后,这个电话插座就时刻监听着消息的传入,谁拨通我这个“IP地址和端口”,我就接通谁。Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信。这里涉及到计算机网络,我们不全部展开,可以查看计算机网络篇。
socket 可以让不在同一台计算机上的进程进行通信。 socket 编程,是站在传输层的基础上,所以可以使用 TCP/UDP 协议
,但是不能干诸如「访问网页」这样的事情,因为访问网页所需要的 http 协议位于应用层。
补充:socket通信步骤
服务器端:
(1)首先服务器应用程序用系统调用socket()
来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。
(2)然后,服务器进程会给套接字起个名字,我们使用系统调用bind()
来给套接字绑定一个专门的监听端口,比如serverSocket.bind(new InetSocketAddress(host, port));
。然后服务器进程就开始等待客户连接到这个套接字。
(3)接下来,系统调用listen()
来创建一个队列,并将其用于存放来自客户的进入连接。
(4)最后,服务器通过系统调用accept
来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接(建立客户端和服务端的用于通信的流,进行通信)。比如while ((socket = serverSocket.accept()) != null)
阻塞监听。
客户端:
(1)客户应用程序首先调用socket
来创建一个未命名的套接字,然后将服务器的命名套接字作为一个地址来调用connect
与服务器建立连接。
(2)一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信(通过流进行数据传输)
链接:https://zhuanlan.zhihu.com/p/109826876
进程间同步与互斥的区别
进程互斥、同步的概念都是并发进程下存在的概念,有了并发进程,就产生了资源的竞争与协作,从而就要通过进程的互斥、同步、通信来解决资源竞争与同步协作的问题。
- 进程互斥
实际上是进程同步的一种特殊情况,即逐次使用互斥共享资源,也是对进程使用资源次序上的一种协调。进程互斥是进程间竞争共享资源的使用权,例如,若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源。
- 进程同步
把异步环境下的一组并发进程,当其中一个到达协调点后,在尚未得到其他合作进程发来的消息或信号之前应阻塞自己,直到其他合作进程发来协调信号或消息后,方被唤醒并继续执行。这种各进程互相协作,走走停停的过程称为进程间的同步。
什么是僵尸进程和孤儿进程
先介绍下背景,僵尸进程的概念和成因就清晰了。unix提供了一种机制可以保证父进程肯定可以获取到子进程结束时的状态信息。这种机制就是:在每个进程退出(调用exit()
系统调用)的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息,包括进程号、退出状态、CPU运行时间等,这个阶段称为僵尸进程(Zombie),这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不代表子进程不经过僵尸状态。
僵尸状态直到父进程通过wait() / waitpid() 系统调用来取时才释放/结束。如果父进程在子进程结束之前退出,则子进程将由init接管。这时候并不会产生什么危害,因为init进程将会以父进程的身份对僵尸状态的子进程进行处理。
所以问题的根源是 产生出大量僵尸进程的那个父进程。如果这个糟糕的父进程不调用wait() / waitpid() 系统调用的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
因此,当我们寻求如何消灭系统中大量的僵尸进程时,答案就是把产生大量僵尸进程的那个元凶枪毙掉(也就是通过kill
发送SIGTERM
或者SIGKILL
信号啦)。枪毙了元凶进程之后,它产生的僵尸进程就变成了孤儿进程(孤儿进程并没有什么危害),这些孤儿进程会被init进程
接管,init进程
会wait()
这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵尸的孤儿进程就能瞑目而去了。
当然也要谨慎使用SIGKILL系统调用,With SIGTERM, a process gets the time to send the information to its parent and child processes. It’s child processes are handled by init.
Use of SIGKILL may lead to the creation of a zombie process because the killed process doesn’t get the chance to tell its parent process that it has received a kill signal.
ok,总结下上面说的,僵尸进程和孤儿进程的概念。
- 孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程
(进程号为1)所收养,并由init进程
对它们完成状态收集工作,所以并不会有什么危害。
- 僵尸进程
一个进程使用fork
创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid
获取子进程的状态信息(不负责任),那么子进程的进程id等仍然保存在系统中。这种进程称之为僵尸进程。
linux底层的零拷贝技术
零拷贝(Zero-copy)技术指将数据从文件系统移动到网络接口的过程中,不需要将其从内核空间复制到用户空间,从而可以减少上下文切换以及 CPU 的拷贝时间。
零拷贝技术可以减少(CPU)数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载。实现零拷贝用到的最主要技术是 DMA 数据传输技术
和内存区域映射技术
。
Linux 提供了轮询、I/O 中断以及 DMA 传输这 3 种磁盘与主存之间的数据传输机制。其中轮询方式是基于死循环对 I/O 端口进行不断检测。I/O 中断方式是指当数据到达时,磁盘主动向 CPU 发起中断请求,由 CPU 自身负责数据的传输过程。
传统IO中断数据拷贝
- 用户进程向 CPU 发起
read
系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。 - CPU 在接收到指令以后对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区。
- 数据准备完成以后,磁盘向 CPU 发起 I/O 中断。
- CPU 收到 I/O 中断以后将磁盘缓冲区中的数据拷贝到内核缓冲区,然后再从内核缓冲区拷贝到用户缓冲区。
- 用户进程由内核态切换回用户态,解除阻塞状态,然后等待 CPU 的下一个执行时间钟。
基于IO中断的DMA传输
DMA
(Direct Memory Access)传输则在 I/O 中断的基础上引入了 DMA 磁盘控制器,由 DMA 磁盘控制器负责数据的传输,降低了 I/O 中断操作对 CPU 资源的大量消耗。DMA是一种允许外围设备(硬件子系统)直接访问系统主内存的机制。也就是说,基于 DMA 访问方式,系统主内存与硬盘或网卡之间的数据传输可以绕开 CPU 的全程调度。
有了 DMA 磁盘控制器接管数据读写请求以后,CPU 从繁重的 I/O 操作中解脱。
数据读取操作的流程如下:
- 用户进程向 CPU 发起
read
系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。 - CPU 在接收到指令以后对 DMA 磁盘控制器发起调度指令。
- DMA 磁盘控制器对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区,CPU 全程不参与此过程。
- 数据读取完成后,DMA 磁盘控制器会接受到磁盘的通知,将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
- DMA 磁盘控制器向 CPU 发出数据读完的信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区。
- 用户进程由内核态切换回用户态,解除阻塞状态,然后等待 CPU 的下一个执行时间钟。
传统基于IO操作的DMA传输(4次)
以下模拟了一次读取文件并通过socket发送到网络的过程。
在 Linux 系统中,传统的访问方式是通过 write()
和 read()
两个系统调用实现的,通过 read()
函数读取文件到到缓存区中,然后通过 write()
方法把缓存中的数据输出到网络端口,整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝总共 4 次拷贝,以及 4 次上下文切换。
零拷贝技术
-
用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
-
减少数据拷贝次数:在数据传输过程中,避免数据在用户缓冲区和内核缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前主流零拷贝技术的实现思路。
-
写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。
(1)mmap + write(3次)
mmap + write
代替read + write
,使用 mmap
的目的是将内核读缓冲区的地址与用户缓冲区进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区拷贝到用户缓冲区的过程,然而内核读缓冲区仍需将数据写到内核中的socket 缓冲区。整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
问题
- mmap 处理小文件可能会导致碎片空间的浪费,因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存。
- mmap虽然减少了 1 次cpu拷贝,但也存在问题。当
mmap
一个文件时,如果这个文件被另一个进程所截获,那么write
系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,服务器可能因此被终止。
(2)sendfile系统调用(3次)
sendfile
系统调用在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了数据在内核缓冲区和用户缓冲区之间的拷贝。
sendfile
系统调用的引入,不仅减少了 CPU 拷贝的次数,还减少了上下文切换的次数(只有一次系统调用)。通过 sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
问题
sendfile() 系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,所以 sendfile() 只是适用于应用程序地址空间不需要对所访问数据进行处理的情况。因为 sendfile
传输的数据没有越过用户应用程序 / 操作系统内核的边界线,所以也极大地减少了存储管理的开销。
链接:https://zhuanlan.zhihu.com/p/20768200
(3)sendfile + DMA gather copy(2次DMA拷贝,0次CPU拷贝)
DMA 拷贝
引入了 gather
操作。它将内核读缓冲区中的数据描述信息(内存地址
、地址偏移量
)记录到相应的socket缓冲区中(cpu拷贝量很小,可忽略),由 DMA 根据socket缓冲区的内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中,本质上是虚拟内存映射的思路,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作。整个拷贝过程会发生 2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝。
问题
sendfile + DMA gather copy 拷贝方式同样存在用户程序不能对数据进行修改的问题,而且需要硬件的支持,它只适用于将数据从文件拷贝到 socket 套接字上的传输过程。
(4)splice系统调用
splice
系统调用,不仅不需要硬件支持,还通过可以在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道
(pipeline),从而避免了两者之间的 CPU 拷贝操作,实现了两个文件描述符之间的数据零拷贝。整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝。
问题
splice
拷贝方式也同样存在用户程序不能对数据进行修改的问题。除此之外,它使用了 Linux 的管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是它的两个文件描述符参数中有一个必须是管道设备。
链接:
零拷贝技术:https://juejin.cn/post/6844903949359644680
linux的各种IO模型
缓存IO
第一阶段,数据会先被拷贝到操作系统的内核缓冲区中,第二阶段才会从操作系统内核缓冲区拷贝到应用程序的地址空间。
同步I/O与异步I/O
最基本的定义,是否在I/O操作过程中阻塞用户线程。
-
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes
-
An asynchronous I/O operation does not cause the requesting process to be blocked
(1)阻塞 I/O(blocking IO)
当用户进程调用了recvfrom
这个系统调用,kernel就开始了IO的第一个阶段:准备数据
(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝
到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
blocking IO的特点就是在IO执行的两个阶段都被block了
(2)非阻塞 I/O(nonblocking IO)
当用户进程发出read
操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read
操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read
操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
nonblocking IO的特点是用户进程需要不断的主动询问kernel数据是否已准备好,第一阶段用户线程是可以选择做其他事情的。
(3)I/O 多路复用( IO multiplexing)
select/epoll
的优势在于单个process可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll
这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read
操作,将数据从kernel拷贝到用户进程。
I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()
函数就可以返回。
以上三种都是同步IO,他们在第二阶段都是阻塞的。
(4)异步 I/O(asynchronous IO)
用户进程发起read
操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read
操作完成了。
异步IO的特点是IO两个阶段都不阻塞,真正的“不阻塞”!另外三种第二阶段都阻塞在了将数据从内核缓冲区拷贝到用户缓冲区的IO操作上,而真正的异步IO的过程只有 发起和用户进程被通知,中间是没有任何阻塞的。
链接:https://blog.csdn.net/qq_36573828/article/details/89149057
详解IO多路复用 & epoll原理
为什么能高效管理数百万连接,弄懂ET模式和LT模式。
(1)select
select
函数监视的文件描述符分3类,分别是writefds
、readfds
、和exceptfds
。调用后select
函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有exceptions),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
问题:
-
fd集合从用户态到内核态的拷贝开销
-
内核遍历所有fd的开销
-
单个进程能够监视的文件描述符的数量存在最大限制,默认1024,可更改,但性能随着数量上升会降低。
(2)poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* file descriptor /
short events; / requested events to watch /
short revents; / returned events witnessed */
};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制,但是数量过大后性能也是会下降。 和select
函数一样,poll
通过遍历文件描述符来获取已经就绪的socket。
综上,poll
本质上和select
没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。
以上两种方式会产生大量无效的遍历,并不是最优的解法。
(3)epoll
-
调用
epoll_create
()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源) -
调用
epoll_ctl
向epoll对象中添加这100万个连接的套接字 -
调用
epoll_wait
收集发生的事件的连接
int epoll_create(int size);
//创建一个epoll的句柄epfd,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//函数是对指定描述符fd执行op操作(添加、删除和修改对fd的监听事件),epoll_event告诉内核需要监听什么事,fd:是告诉内核需要监听什么socket。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//等待epfd上的io事件,该函数返回需要处理的事件数目,最多maxevents个事件。
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
epoll
的高效在于,当我们调用epoll_ctl塞入百万个句柄fd时,epoll_wait仍然可以快速返回,并有效的将发生事件的句柄fd给我们用户,怎么做到的?
这是由于在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。
通常情况下即使我们要监控百万计的句柄fd,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄fd到用户态而已。所以,epoll_wait
非常高效。
新的问题又来了,如何维护这个list链表的?
这是因为当我们执行epoll_ctl
时,除了把socket放到epoll
文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数
,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后,就来把socket插入到准备就绪链表里了。
总的来说,红黑树是为了高效查找句柄,就绪链表是为了保存已就绪的句柄,方便返回,而链表的维护是依靠中断处理函数的回调。执行epoll_create
时,创建了红黑树和就绪链表,执行epoll_ctl
时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait
时立刻返回准备就绪链表里的数据即可。
epoll优势
(1)监视的描述符数量不受限制
(2)IO的效率不会随着监视fd的数量的增长而下降
(3)利用mmap()
文件映射内存加速与内核空间的消息传递
链接:
https://zhuanlan.zhihu.com/p/39970630
https://www.cnblogs.com/aspirant/p/9166944.html
LT模式和ET模式
epoll
对文件描述符的操作有两种模式:LT
(level trigger)和ET
(edge trigger)。LT
模式与ET
模式的区别如下:
LT
模式
当epoll_wait
检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait
时,会再次响应应用程序并通知此事件。
使用LT模式(默认)意味着只要fd处于可读或者可写状态,每次epoll_wait都会返回该fd,这样的话会带来很大的系统开销,且处理时候每次都需要把这些fd轮询一遍,如果fd的数量巨大,不管有没有事件发生,epoll_wait都会触发这些fd的轮询判断。
ET
模式
当epoll_wait
检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait
时,不会再次响应应用程序并通知此事件,需要用户自己保证可靠。
ET/LT模式的处理逻辑几乎完全相同,差别仅在于 LT
模式在 event 发生时不会将其从 ready list 中移除。
综上,在 select/poll
中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll
事先通过epoll_ctl
()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速把这个句柄放入list中,当进程调用epoll_wait
() 时便得到通知,快速返回。
链接:https://segmentfault.com/a/1190000003063859
补充epoll底层数据结构
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个epoll
对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl
方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback
,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
链表:https://www.cnblogs.com/developing/articles/10849288.html
虚拟内存解决了什么问题?
一句话概括,虚拟内存最大的意义,是为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(似乎拥有了一片连续完整的内存空间)。
(1)分页
优点是页长固定,因而便于构造页表、易于管理,且不存在外碎片。但分页方式的缺点是页长与程序的逻辑大小不相关。例如,某个时刻一个子程序可能有一部分在主存中,另一部分则在磁盘中。这不利于编程时的独立性,并给换入换出处理、存储保护和存储共享等操作造成麻烦。
(2)分段
段是按照程序的自然分界划分的长度可以动态改变的区域。通常,程序员把子程序、操作数和常数等不同类型的数据划分到不同的段中,并且每个程序可以有多个相同类型的段。
(3)段页式
是分段式和分页式结合的存储组织方法,这样可充分利用分段管理和分页管理的优点,对用户是分段,对系统是分页。虚拟内存是连接分段分页的桥梁。用户角度,虚拟地址分成与程序逻辑相关的不同的段(段表),实际执行时,段又会分成不同的页,分布在内存或磁盘中,涉及到换入换出,这个过程由MMU
操作页表(PCB
中)完成由逻辑地址向物理地址的转换(地址翻译)。
现代处理器使用的是一种称为虚拟寻址(Virtual Addressing)的寻址方式。使用虚拟寻址,CPU需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。CPU中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件,它的功能是将虚拟地址转换为物理地址。MMU需要借助存放在内存中的页表来动态翻译虚拟地址,该页表由操作系统管理。
用户进程申请并访问物理内存(或磁盘存储空间)的过程
- 用户进程向操作系统发出内存申请请求
- 系统会检查进程的虚拟地址空间是否被用完,如果有剩余,给进程分配虚拟地址
- 系统为这块虚拟地址创建的内存映射(Memory Mapping),并将它放进该进程的页表(Page Table)
- 系统返回虚拟地址给用户进程,用户进程开始访问该虚拟地址
- CPU 根据虚拟地址在此进程的页表(Page Table)中找到了相应的内存映射(Memory Mapping),但是这个内存映射(Memory Mapping)没有和物理内存关联,于是产生缺页中断
- 操作系统收到缺页中断后,分配真正的物理内存并将它关联到页表相应的内存映射(Memory Mapping)。中断处理完成后 CPU 就可以访问内存了
- 当然缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才用的着,也即很多时候在上面的第 3 步系统会分配真正的物理内存并和内存映射(Memory Mapping)进行关联。
在用户进程和物理内存(磁盘存储器)之间引入虚拟内存的优点:
- 地址空间:提供更大的地址空间,并且地址空间是连续的,使得程序编写、链接更加简单
- 进程隔离:不同进程的虚拟地址之间没有关系,所以一个进程的操作不会对其它进程造成影响
- 数据保护:每块虚拟内存都有相应的读写属性,这样就能保护程序的代码段不被修改,数据块不能被执行等,增加了系统的安全性
- 内存映射:有了虚拟内存之后,可以直接映射磁盘上的文件(可执行文件或动态库)到虚拟地址空间。这样可以做到物理内存延时分配,只有在需要读相应的文件的时候,才将它真正的从磁盘上加载到内存中来,而在内存吃紧的时候又可以将这部分内存清空掉,提高物理内存利用效率,并且所有这些对应用程序是都透明的
- 共享内存:比如动态库只需要在内存中存储一份,然后将它映射到不同进程的虚拟地址空间中,让进程觉得自己独占了这个文件。进程间的内存共享也可以通过映射同一块物理内存到进程的不同虚拟地址空间来实现共享
- 物理内存管理:物理地址空间全部由操作系统管理,进程无法直接分配和回收,从而系统可以更好的利用内存,平衡进程间对内存的需求
链接:https://juejin.cn/post/6844903507594575886
动态链接库与静态链接库的区别
库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库
(.a、.lib)和动态库
(.so、.dll)。
静态库
在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库存在的问题如下:
空间浪费
。- 另一个问题是静态库对程序的
更新、部署和发布带来麻烦
。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于使用者来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新
)。
动态库
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入
。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费
问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布也会带来麻烦的问题。用户只需要更新动态库即可,增量更新
。【拓展】