特点:并发性、共享性、虚拟性、异步性。
Windows 和 Linux 内核差异
对于内核的架构⼀般有这三种类型:
● 宏内核,包含多个模块,整个内核像⼀个完整的程序;
● 微内核,有⼀个最⼩版本的内核,⼀些模块和服务则由⽤户态管理;
● 混合内核,是宏内核和微内核的结合体,内核中抽象出了微内核的概念,也就是内核中会有⼀个⼩型的内核,其他模块就在这个基础上搭建,整个内核是个完整的程序;
Linux 的内核设计是采⽤了宏内核,Window 的内核设计则是采⽤了混合内核。
Linux 可执⾏⽂件格式叫作 ELF,Windows 可执⾏⽂件格式叫作 PE。
键盘敲⼊字⺟时,期间发⽣了什么?
● 当⽤户输⼊了键盘字符,键盘控制器就会产⽣扫描码数据,并将其缓冲在键盘控制器的寄存器中,
紧接着键盘控制器通过总线给 CPU 发送中断请求。
● CPU 收到中断请求后,操作系统会保存被中断进程的 CPU 上下⽂,然后调⽤键盘的中断处理程
序。
● 键盘的中断处理程序是在键盘驱动程序初始化时注册的,其功能就是从键盘控制器的寄存器的缓冲
区读取扫描码,再根据扫描码找到⽤户在键盘输⼊的字符,如果输⼊的字符是显示字符,那就会把
扫描码翻译成对应显示字符的 ASCII 码,⽐如⽤户在键盘输⼊的是字⺟ A,是显示字符,于是就会
把扫描码翻译成 A 字符的 ASCII 码。
● 得到了显示字符的 ASCII 码后,就会把 ASCII 码放到「读缓冲区队列」,接下来就是要把该字符显
示屏幕了,显示设备的驱动程序会定时从「读缓冲区队列」读取数据放到「写缓冲区队列」,最后
把[写缓冲区队列]的数据⼀个⼀个写⼊到显示设备控制器的寄存器中的数据缓冲区,最后将这些
数据显示在屏幕⾥。
● 显示出结果后,恢复被中断进程的上下⽂。
3.1 ⽤户态 和 内核态
陷⼊内核态⽅式:
1. 系统调⽤
系统调⽤是⽤户进程主动发起的操作。发起系统调⽤,陷⼊ 内核,由操作系统执⾏系统调⽤,然后再返回到进程。系统调⽤实质上就是函数调⽤,只不过调⽤的是系统函数,处于内核态⽽已。 ⽤户在调⽤系统调⽤时会向内核传递⼀个系统调⽤号,然后系统调⽤处理程序通过此号从系统调⽤表中找到相应的内核函数执⾏,最后返回。
2. 异常
异常包括程序运算引起的各种错误如除 0、缓冲区溢出、缺⻚等。异常是⼀种错误情况,是执⾏当前指令的结果,可能被错误处理程序修正,也可能直接终⽌应⽤程序。异常是同步的。
3. 中断
中断包括 I/O 设备发出的 I/O 中断、各种定时器引起的时钟中断、调试程序中设置的断点等引起的调试中断等。中断由处理器外部的 硬件 产⽣,不是执⾏某条指令的结果,也⽆法预测发⽣时机。由于中断独⽴于当前执⾏的程序,因此中断是异步事件。
执⾏系统调⽤时 OS 状态
1. ⽤户运⾏库函数(系统调⽤的封装),函数⾥⾯其实是执⾏的 int 0x80 指令。系统调⽤先把系统调⽤号保存在 eax 寄存器中,然后执⾏ int0x80 指令。
2. int 0x80 指令先进⾏切换堆栈,找到进程的堆栈,将寄存器值压⼊到内核栈中
3. 然后查找相应 中断向量表 的中断处(system_call)并调⽤
4. 随后 system_call 从 系统调⽤表 中找到相应的系统调⽤进⾏执⾏,调⽤结束后从 system_call 中返回。
CPU 中断
CPU中断是什么
1. 计算机处于执⾏期间,系统内发⽣了⾮预期的急需处理事件
2. CPU暂时中断当前正在执⾏的程序⽽转去执⾏相应的事件处理程序
3. 处理完毕后返回原来被中断处继续执⾏
CPU中断的作⽤
1. 可以使CPU和外设同时⼯作,使系统可以 及时地响应 外部事件
2. 可以允许多个外设同时⼯作,⼤⼤提⾼CPU的 利⽤率
3. 可以使CPU及时处理各种软硬件故障
零拷⻉
零拷⻉指的是,从⼀个存储区域到另⼀个存储区域的 拷⻉任务没有CPU参与。零拷⻉通常⽤于⽹络⽂件 传输,以减少CPU消耗和内存带宽占⽤,减少 ⽤户空间(⽤户可以操作的内存缓存区域)与CPU内核 空间(CPU可以操作的内存缓存区域及寄存器)的 拷⻉过程,减少 ⽤户上下⽂(⽤户状态环境)与 CPU内核上下⽂(CPU内核状态环境)间的 切换,提⾼系统效率。 [1]
传统拷⻉⽅式:发⽣4次空间切换(1、4、5、7),发⽣4次copy(3、4、5、6),其中有2次CPU (4、5)参与。
零拷⻉⽅式:零拷⻉是通过 sendfile 系统调⽤ 实现的。发⽣2次空间切换(1、6),发⽣3次copy(3、4、5),其中有0次CPU参与。(mmap零拷⻉在此基础上增加⽤户空间与内核空间中内核缓冲区的共享,会多2次空间切换)DMA全称Direct Memory Access(直接内存存取),它允许不同速度的硬件装置来沟通,⽽不需要依赖于 CPU 的⼤量中断负载。DMA主要是解决外围设备可以直接访问内存,从中减少CPU参与。DMA⽅式(优先级⾼于中断)主要适⽤于⼀些⾼速的 I/O设备 。
传输⽂件的时候,我们要根据⽂件的⼤⼩来使⽤不同的⽅式:
● 传输⼤⽂件的时候,使⽤「异步 I/O + 直接 I/O」。因为可能由于 PageCache 被⼤⽂件占据,⽽
导致「热点」⼩⽂件⽆法利⽤到 PageCache,并且⼤⽂件的缓存命中率不⾼,这时就需要使⽤
「异步 IO + 直接 IO 」的⽅式。
● 传输⼩⽂件的时候,则使⽤「零拷⻉技术」;
3.2 进程、线程、协程
惊群效应:是指多进程(多线程)在同时阻塞等待同⼀个事件的时候(休眠状态),如果等待的这个事件发⽣,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有⼀个进程(线程)获得这个事件的“控制权”,对该事件进⾏处理,⽽其他进程(线程)只能重新进⼊休眠状态,这种现象和性能浪费就叫做惊群效应。
PCB 存储信息:
● 进程id。系统中每个进程有唯⼀的id,⾮负整数。
● 进程的状态。有就绪,运⾏,挂起,停⽌等状态
● 进程切换时需要保存和恢复的⼀些CPU寄存器
● 描述虚拟地址空间的信息。
● 描述控制终端的信息。
● 当前⼯作⽬录
● umask掩码
● ⽂件描述符表,包含很多指向结构体的指针
● 和信号相关的信息
● ⽤户id和组id。
● 会话(Session)和进程组
● 进程可以使⽤的资源上限(Resource Limit)
进程与线程区别
⼀个进程最多可以创建多少个线程?
进程的虚拟内存空间上限,因为创建⼀个线程,操作系统需要为其分配⼀个栈空间,如果线程数量
越多,所需的栈空间就要越⼤,那么虚拟内存就会占⽤的越多。
系统参数限制,虽然 Linux 并没有内核参数来控制单个进程创建的最⼤线程个数,但是有系统级别
的参数来控制整个系统的最⼤线程个数。
线程的实现⽅式
在引⼊线程的操作系统中,进程是资源分配的基本单位,线程是独⽴调度的基本单位。线程也像进程⼀样有多个状态:运⾏、就绪、阻塞… 从 Linux 内核的⻆度来看,线程和进程并没有被区别对待。⽆论线程还是进程,都是⽤ task_struct 结构表示的。线程分为以下两种实现⽅式:
1. ⽤户级线程实现
⽤户线程多⻅于⼀些历史悠久的操作系统,例如Unix操作系统。有关线程管理的所有⼯作都由应⽤程序完成,内核意识不到多线程的存在。⽤户级线程仅存在于⽤户空间中,此类线程的创建、撤销、线程之间的同步与通信功能,都⽆法利⽤系统调⽤来实现。 应⽤程序需要通过使⽤线程库来控制线程。 通常,应⽤程序从单线程起始,在该线程中开始运⾏,在其运⾏的任何时刻,可以通过调⽤线程库中的派⽣创建⼀个在相同进程中运⾏的新线程。由于线程在进程 内切换的规则远⽐进程调度和切换的规则简单,不需要进⾏⽤户态/核⼼态切换,所以切换速度快。因为⽤户级线程驻留在⽤户空间,且管理和控制它们的线程也在⽤户空间,每个线程并不具有⾃身的线程上下⽂,所以它们对于操作系统是不可⻅的,这也就是它⽆法被调度到处理器内核的原因。
⽤户线程的优点
可以在不⽀持线程的操作系统中实现。
创建和销毁线程、线程切换代价等线程管理的代价⽐内核线程少, 因为保存线程状态的过程和调⽤ 程序都只是本进程空间的操作允许每个进程定制⾃⼰的调度算法,线程管理⽐较灵活。线程能够利⽤的表空间和堆栈空间⽐内核级线程多不需要trap,不需要上下⽂切换(context switch),也不需要对内存⾼速缓存进⾏刷新,使得线程调⽤⾮常快捷线程的调度不需要内核直接参与,控制简单。
⽤户线程的缺点
如果线程发⽣I/O等阻塞从⽽引起系统调⽤时,由于内核不知道有多线程的存在,会阻塞整个进程进⽽阻塞所有线程, 因此同⼀进程中只能同时有⼀个线程在运⾏。⼀个单独的进程内部,没有时钟中断,所以不可能⽤轮转调度的⽅式调度线程。资源调度按照进程进⾏,多个处理机下,同⼀个进程中的线程只能在同⼀个处理机下分时复⽤,因此对于多线程并不能被多核系统加速。
2. 内核级线程实现
内核线程建⽴和销毁都是在内核的⽀持下运⾏,由操作系统负责管理,通过系统调⽤完成的。线程管理 的所有⼯作由内核完成,应⽤程序没有进⾏线程管理的代码,只有⼀个到内核级线程的编程接⼝。内核为进程及其内部的每个线程维护上下⽂信息,调度也是在内核基于线程架构的基础上完成。内核线程驻留在内核空间,它们是内核对象。操作系统调度器管理、调度并分派这些线程。运⾏时库为每个⽤户级线程请求⼀个内核级线程,将⽤户进程映射或绑定到上⾯。⽤户线程在其⽣命期内都会绑定到该内核线程。⼀旦⽤户线程终⽌,两个线程都将离开系统。这被称作”⼀对⼀”线程映射。内核空间内为每⼀个内核⽀持线程设置了⼀个线程控制块(TCB),内核根据该控制块,感知线程的存在,并进⾏控制。
内核线程的特点
● 当某个线程希望创建⼀个新线程或撤销⼀个已有线程时,它进⾏⼀个系统调⽤。
● 多处理器系统中,内核能够并⾏执⾏同⼀进程内的多个线程。
● 如果进程中的⼀个线程被阻塞,能够切换同⼀进程内的其他线程继续执⾏(⽤户级线程不具备)。
● 所有能够阻塞线程的调⽤都以系统调⽤的形式实现,代价较⼤。
⽤户级线程和内核级线程的区别
● 内核级线程是OS内核可感知的,⽽⽤户级线程是OS内核不可感知的。
● ⽤户级线程的创建、撤消和调度不需要OS内核的⽀持;⽽内核级线程创建、撤消和调度都需OS内核提供⽀持,⽽且与进程的创建、撤消和调度⼤体是相同的。
● ⽤户级线程执⾏系统调⽤指令时将导致其所属进程被中断,⽽内核级线程执⾏系统调⽤指令时,只导致该线程被中断。
● 在只有⽤户级线程的系统内,CPU调度还是以进程为单位,处于运⾏状态的进程中的多个线程,由⽤户程序控制线程的轮换运⾏;在有内核级线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
● ⽤户级线程的程序实体是运⾏在⽤户态下的程序,⽽内核级线程的程序实体则是可以运⾏在任何状态下的程序。
线程安全
线程安全:如果代码所在的进程中有多个线程在同时运⾏,⽽这些线程可能会同时运⾏这段代码。如果每次运⾏结果和单线程运⾏的结果是⼀样的,⽽且其他的变量值也和预期的是⼀样的,就是线程安全的。线程安全问题都是由 全局变量 和 静态变量 引起的
实现线程安全的⽅式
1. 加锁。如标准库中的 mutex
2. 原⼦操作 atomic。在多线程并发执⾏时,原⼦操作是不会被线程打断的执⾏⽚段。
线程的同步⽅式
线程同步 是指多线程通过特定的设置来控制线程之间的执⾏顺序。主要有以下四种⽅式:
1. 临界区:通过多线程的串⾏化来控制对公共资源的访问,速度快。在任意⼀个时刻只允许⼀个线程对共享资源进⾏访问。
2. 互斥对象:只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有⼀个,所以能保证公共资源不会被多个线程同时访问。
3. 信号量:它允许多个线程在同⼀时刻访问同⼀资源,但是需要限制在同⼀时刻访问此资源的最⼤线程数⽬。
4. 事件对象:利⽤通知的⽅式来保持线程的同步,还可以⽅便实现对多个线程的优先级⽐较操作。
进程状态
“挂起”是指将暂不执⾏的进程换出到外存,节省内存空间。“挂起”和“阻塞”都是进程暂停执⾏的状态,但是这是两个维度的概念:
● 阻塞表示进程正在等待⼀个事件的发⽣,阻塞状态下收到信号会切换为就绪状态
● 挂起表示进程被换出到外存,挂起状态下被激活时会被载⼊到内存,切换为⾮挂起状态
综上所属,挂起状态的进程按照是否阻塞可以分为:
● 挂起就绪状态:进程在外存中,但是只要被载⼊内存就可以执⾏
● 挂起阻塞状态:进程在外存中并等待⼀个事件,即使被载⼊内存也⽆法运⾏
Linux 将进程的 阻塞状态 进⼀步细分为:暂停、浅睡眠、深睡眠。
其中,若不需要等待资源,则切换为“暂停”;若需要等待资源,切换为“睡眠”;如果睡眠状态能被信号唤醒,则是“浅睡眠”,否则是“深睡眠”。
进程/CPU调度算法
进程的上下⽂切换不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
1. 先来先服务(First Come First Serverd,FCFS):优点是易于理解,便于实现,只需⼀个就绪队列。缺点是对短作业不公平;对 I/O 密集型进程不利,⻓时间等待设备;响应时间不确定
2. 最短作业优先(Shortest Job First,SJF):优点是提⾼平均周转时间。缺点是对⻓作业不公平;可能导致饥饿问题
3. 最⾼响应⽐优先算法(Highest Response Ratio Next,HRRN):作业运⾏所需时间越⼩、作业
等待时间越⻓,响应⽐越⼤。优点:同时考虑了等待时间和执⾏时间,既优先考虑短作业,也防⽌
⻓作业⽆限等待的饥饿.
4. 时间⽚轮转(Round Robin,RR):优点:没有饥饿问题。问题:若时间⽚⼩,进程切换频繁,吞吐量低;若时间⽚⻓,则响应时间过⻓,实时性得不到保证
5. 最⾼优先级调度
6. 多级反馈队列(Multilevel Feedback Queue,MFQ):优先级⾼的队列先执⾏;优先级越⾼,时间⽚越短;如果⼀个进程在当前队列规定的时间⽚内⽆法执⾏完毕,则移动到下⼀个队列的队尾。缺点:也有可能出现饥饿问题,⽐如不断有新的更⾼优先级的进程加⼊。可在⼀定时间后全部提升到优先级⾼的队列解决。
7. 最早截⽌时间优先算法:先把截⽌时间早的任务给完成,否则这个任务如果在截⽌时间后才完成,就没有意义了。
8. 最短剩余时间优先(Shortest Remaining Time Next,SRTN):最短作业优先的抢占式版本,如果新作业⽐正在执⾏的作业剩余时间短,则它优先执⾏。缺点是对⻓作业不公平;可能导致饥饿问 题。
9. 彩票法:向进程提供各种系统资源的彩票。调度时随机抽取彩票,拥有该彩票的进程得到资源。可 给重要的进程更多的彩票;协作进程可以交换彩票
10. 公平分享法:为每⽤户分配⼀定⽐例的 CPU 时间,⽽不是按照进程。各⽤户之间按照⽐例挑选进程.
进程的分类
僵⼫进程
概念:僵⼫进程是指终⽌但还未被回收的进程。如果⼦进程退出,⽽⽗进程并没有调⽤ wait() 或
waitpid() 来回收,那么就会产⽣僵⼫进程。僵⼫进程是⼀个已经死亡的进程,但是其进程描述符仍然保存在系统的进程表中。
危害:占⽤进程号,系统所能使⽤的进程号是有限的,可能导致不能产⽣新的进程;占⽤⼀定的内存。 如何避免产⽣僵⼫进程:
1. ⽗进程调⽤ wait 或者 waitpid 等待⼦进程结束
2. ⼦进程结束时,内核会发送 SIGCHLD 信号给⽗进程。⽗进程可以注册⼀个信号处理函数,在该函数中调⽤ waitpid,等待所有结束的⼦进程;也可以⽤ signal(SIGCLD, SIG_IGN) 忽略 SIGCHLD信号,那么⼦进程结束后,内核会进⾏回收
3. 杀死⽗进程,僵⼫进程就会变成孤⼉进程,由 Init 进程接管并处理
孤⼉进程
如果某个进程的⽗进程先结束了,那么它的⼦进程会成为孤⼉进程。每个进程结束的时候,系统都会扫描是否存在⼦进程,如果有则⽤ Init 进程(pid = 1)接管,并由 Init 进程调⽤ wait 等待其结束,完成状态收集⼯作。孤⼉进程不会对系统造成危害。
守护进程
守护进程是⼀种在后台执⾏的电脑程序。此类程序会被以进程的形式初始化。
创建守护进程:
1. 在⽗进程中执⾏ fork 并退出⽗进程,⼦进程继续
2. 在⼦进程中调⽤ setsid 函数创建新的会话。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3. 在⼦进程中调⽤ chdir 函数,让根⽬录 "/" 成为⼦进程的⼯作⽬录。进程活动时,其⼯作⽬录所在的⽂件系统不能卸下,⼀般需要将⼯作⽬录改变到根⽬录
4. 在⼦进程中关闭任何不需要的⽂件描述符。进程从创建它的⽗进程那⾥继承了打开的⽂件描述符。如不关闭,将会浪费系统资源,造成进程所在的⽂件系统⽆法卸下以及引起⽆法预料的错误。
5. 在⼦进程中调⽤ umask 函数,设置进程的 umask 为0。进程从创建它的⽗进程那⾥继承了⽂件创 建掩模。它可能修改守护进程所创建的⽂件的存取位。为防⽌这⼀点,将⽂件创建掩模清除.
进程间的通信⽅式
1. 信号 Signal
信号是系统为响应某些条件⽽产⽣的⼀个事件,接收到该信号的进程可以采取事先⾃定义的⾏为。这是⼀种“订阅-发布”的模式。
信号来源:
1. 硬件来源。如按下 CTRL+C、除 0、⾮法内存访问等等
2. 软件来源。如 Kill 命令等等
进程如何发送信号:
1. 使⽤系统调⽤将信号放到⽬标进程的 信号队列 中
2. 如果⽬标进程处于未执⾏状态,则该信号就由内核保存起来,直到该进程恢复执⾏并传递给它为
⽌;如果⼀个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进
程 进程如何接收信号:
1. 把接收到的信息放⼊信号队列中;
2. 执⾏中的进程会在特定时刻(如从系统空间返回到⽤户空间之前)检查并处理⾃⼰的信号队列。
2. 信号量 Semaphore
信号量是⼀种特殊的变量,对它的操作都是原⼦的(屏蔽中断),有两种操作:V(signal())和 P
(wait())。V 操作会增加信号量 S 的数值,P 操作会减少它。(信号量 S 的值相当于记录资源的个数)底层实现:通过硬件提供的原⼦指令,如 Test And Set(上锁并检查)、Compare And Swap(⽐较并交换,仅当预期值与内存值相等时才修改,否则不操作) 等。
种类:整型信号量(通过PV操作访问)、记录型信号量(让权等待策略)、AND型信号量(资源⼀次性分配)、信号量集(⼀次申请多个资源,每次分配前都要测试资源数⽬,根据可分配下限值决定是否分配)
3. 管道 Pipe
管道是⼀种半双⼯的通信⽅式,数据只能单向流动。如果想实现双向通信,那么需要建⽴两个管道。管道适合于传输⼤量信息,传输的内容是没有格式的字节流。
创建管道:通过 pipe() 系统调⽤来创建并打开⼀个管道,当最后⼀个使⽤它的进程关闭对他的引⽤时,pipe 将⾃动撤销。通过 pipe() 创建的是匿名管道,只能⽤于具有亲缘关系的进程之间(⽗⼦进程或兄弟进程)。
管道的实现:管道是⼀个由内核管理的缓冲区,缓冲区被设计为环形的数据结构,以便管道可被循环利⽤。管道的同步:管道是⼀个具有特定⼤⼩的缓冲区,操作系统会通过加锁保证读写进程的同步,下游进程或者上游进程需要等另⼀⽅释放锁后才能操作管道。当管道为空时,下游进程读阻塞;当管道满时,上游进程写阻塞。
4. 命名管道 FIFO
命名管道可⽤于任何有访问权的进程通过⽂件名将其打开和进⾏读写。Pipe 和 FIFO 除了建⽴、打开、
删除的⽅式不同外,⼆者⼏乎⼀模⼀样。
FIFO的实现:通过 mkfifo() 函数建⽴命名管道。命名管道实质上也是通过内核缓冲区来实现数据传
输。建⽴命名管道时,会在磁盘中创建⼀个索引节点,命名管道的名字就相当于索引节点的⽂件名。索
引节点设置了进程的访问权限,有访问权限的进程,可以通过磁盘的索引节点来读写这块缓冲区。当不
再被任何进程使⽤时,命名管道在内存中释放,但磁盘节点仍然存在。
5. 消息队列 Message Queue
消息队列是消息组成的链表,保存在内核中。消息队列中的消息是⼀个具有特定格式的数据块。操作系统中可以存在多个消息队列,每个消息队列有唯⼀的 key 进⾏标识。
优缺点:和信号相⽐,消息队列能够传递更多的信息。与管道相⽐,消息队列提供了有格式的数据。消息队列允许⼀个或多个进程向它写⼊与读取消息,消息队列是异步的。
实现:操作系统提供创建消息队列、取消息、发消息等系统调⽤。操作系统负责读写同步:若消息队列已满,则写消息进程排队等待;若取消息进程没有找到需要的消息,则在消息队列中轮询。
6. 共享内存 Shared Memory
共享内存允许多个进程映射同⼀段物理空间,然后像访问普通内存⼀样访问它,并交换数据。
优点:
● 访问共享内存区域和访问进程独有的内存区域⼀样快,原因是不需要系统调⽤,不涉及⽤户态到内
核态的转换,也不需要对数据过多复制。
● ⽐如管道和消息队列,需要在内核和⽤户空间进⾏四次的数据拷⻉(读输⼊⽂件、写到管道;读管 道、写到输出⽂件),⽽共享内存则只拷⻉两次:⼀次从输⼊⽂件到共享内存区,另⼀次从共享内
存到输出⽂件。
● 消息队列的实现需要系统调⽤,也就经常需要⽤户态和内核态互相转换;⽽共享内存只在建⽴时需要系统调⽤,之后所有访问都可作为常规内存访问,⽆需借助内核。
缺点:存在并发问题,有可能出现多个进程修改同⼀块内存,因此共享内存⼀般与信号量结合使⽤。实现:mmap() 不是专⻔⽤来共享内存的,mmap() 系统调⽤的主要作⽤是将普通⽂件映射到进程的地址空间,然后可以像访问普通内存⼀样对⽂件进⾏访问,不必再调⽤ read(),write() 等操作。因此多个进程可以通过 mmap() 映射同⼀个普通⽂件,来实现共享内存。
7. 套接字 Socket
不同的计算机的进程之间通过 socket 通信,也可⽤于同⼀台计算机的不同进程。需要通信的进程之间⾸先要各⾃创建⼀个 socket,内容包括主机地址与端⼝号。进程通过 socket 把消息发送到⽹络层中,⽹络层通过主机地址将其发到⽬的主机,⽬的主机通过端⼝号发给对应进程。实现:操作系统提供创建 socket、发送、接收的系统调⽤,为每个 socket 设置发送缓冲区、接收缓冲区。
协程
协程是⼀个⽤户态的线程,⽤户在堆上模拟出协程的栈空间。当需要进⾏协程上下⽂切换的时候,主线程只需要交换栈空间和恢复协程的⼀些相关的寄存器的状态,就可以实现上下⽂切换。没有了从⽤户态转换到内核态的切换成本,协程的执⾏也就更加⾼效。和传统的线程不同的是:线程是抢占式执⾏,当发⽣系统调⽤或者中断的时候,交由OS调度执⾏;⽽协程是通过 yield 主动让出cpu所有权,切换到其他协程执⾏。
3.3 锁
类型
● 互斥锁 加锁失败时,会⽤「线程切换」来应对,当加锁失败的线程再次加锁成功后的这⼀过程,会
有两次线程上下⽂切换的成本,性能损耗⽐较⼤。
● ⾃旋锁 加锁失败时,并不会主动产⽣线程切换,⽽是⼀直忙等待,直到获取到锁。如果被锁住的代
码执⾏时间很短,那这个忙等待的时间相对应也很短。
● 读写锁 允许多个读线程同时持有读锁,提⾼了读的并发性。根据偏袒读⽅还是写⽅,可以分为读优
先锁和写优先锁,读优先锁并发性很强,但是写线程会被饿死,⽽写优先锁会优先服务写线程,读
线程也可能会被饿死,那为了避免饥饿的问题,于是就有了公平读写锁,它是⽤队列把请求锁的线
程排队,并保证先⼊先出的原则来对线程加锁,这样便保证了某种线程不会被饿死,通⽤性也更
好。互斥锁和⾃旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁中的⼀个进⾏实现。
● 互斥锁、⾃旋锁、读写锁都属于 悲观锁,悲观锁认为并发访问共享资源时,冲突概率可能⾮常⾼,
所以在访问共享资源前,都需要先加锁。
● 如果并发访问共享资源时,冲突概率⾮常低的话,就可以使⽤ 乐观锁,它的⼯作⽅式是,在访问共
享资源时,不⽤先加锁,修改完共享资源后,再验证这段时间内有没有发⽣冲突,如果没有其他线
85 程修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。但是,
⼀旦冲突概率上升,就不适合使⽤乐观锁,因为它解决冲突的重试成本⾮常⾼。
CAS 不是乐观锁吗,为什么基于 CAS 实现的⾃旋锁是悲观锁?
CAS 是乐观锁,但是基于 CAS 的⾃旋锁加了while 或者睡眠 CPU 的操作⽽产⽣⾃旋的效果,加锁失
败会忙等待直到拿到锁,即需要先拿到锁才能修改数据,所以算悲观锁。
死锁
死锁是指多个进程循环等待其他进程占有的资源⽽⽆限期地僵持下去的局⾯。当两个或两个以上的进程同时对多个互斥资源请求使⽤时,有可能导致死锁。
造成死锁必要条件
1. 互斥条件。即某个资源在⼀段时间内只能由⼀个进程占有,不能同时被两个或两个以上的进程占
有。
2. 不可抢占条件。进程所获得的资源在未使⽤完毕之前,资源申请者不能强⾏地从资源占有者⼿中夺取资源,⽽只能由该资源占有者进程⾃⾏释放。
3. 占有且申请条件。进程⾄少已占有⼀个资源,但⼜申请新资源;由于该资源已被另外进程占有,此时该进程阻塞;但是它在等待新资源的同时,仍继续占⽤已有的资源。
4. 循环等待条件。存在⼀个进程等待序列{P1, P2, P3..., Pn},其中P1等待P2所占有的某⼀资源,
等待P3占有的某⼀资源...,⽽Pn等待P1所占有的某⼀资源,形成⼀个进程循环等待环。
死锁预防
死锁预防是保证系统不进⼊死锁状态的⼀种策略,基本思想是要求进程申请资源时遵循某种协议,从⽽
打破产⽣死锁的必要条件中的⼀个或⼏个,保证系统不进⼊死锁状态。
1. 打破互斥条件。即允许进程同时访问某些资源
2. 打破不可抢占条件。即允许进程强⾏从占有者那⾥夺取某些资源,就是说当⼀个进程已经占有了某些资源,⽽⼜申请新的资源,但⼜不能被⽴即满⾜时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其他进程,这就相当于该进程占有的资源被隐蔽地抢占了。该⽅法实现起来困难,会降低系统性能
3. 打破占有且申请条件。可以实⾏资源预先分配策略,即进程在运⾏前⼀次性地向系统申请它所需要的全部资源,如果某个进程所需的全部资源得不到满⾜,则不分配资源,此进程暂不运⾏。只有当系统能够满⾜当前进程的全部资源需求时,才⼀次性地将所申请的资源全部分配给该进程。
4. 打破循环等待条件。实⾏资源有序分配策略,即把资源事先编号,按号分配。所有进程对资源的请 求必须严格按资源序号递增的顺序提出,进程占⽤了⼩号资源,才能申请⼤号资源,就不会产⽣环路。在数据库层⾯,有两种设置参数策略通过「打破循环等待条件」来解除死锁状态:
● 设置事务等待锁的超时时间。当⼀个事务的等待时间超过该值后,就对这个事务进⾏回滚,于是锁就释放了。
● 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某⼀个事务,让其他事务得以继续执⾏。
死锁避免:银⾏家算法
基本思想是分配资源之前,判断系统是否是安全的,若是则分配。即已知当前所有进程对各个资源的需求,以及现在已占⽤的资源,还有系统可⽤资源Available。
1. 求出每个进程当前还需要多少资源Need
2. 选取 Need <= Availadble 的某进程分配,若⽆则等待更多资源释放到系统
3. 执⾏安全性检测算法,若安全则进⾏分配,否则到第2步找其他进程
4. 回收原进程已有资源和已分配的资源,找其他进程分配