程序:完成特定任务的一系列指令集合,储存在硬盘的静态⽂件;
程序 = 代码段 + 数据段。
进程:程序运行时被加载到内存,就成了进程(即正在进行中的程序);
1、用户角度: 进程是程序的一次动态执行过程;
2、操作系统: 进程是操作系统分配资源的基本单位,也是最小实体。
程序与进程的区别:
(1)静态 / 动态;
(2)硬盘 / 内存(永久 / 暂时)。
程序与进程的联系:
一个程序可以对应多个进程,同一个程序可以在不同的数据集合上运行,因而构成若干个不同的进程。
几个进程能并发地执行相同的程序代码,而同一个进程能顺序地执行几个程序。
作业是用户需要计算机完成的某项任务。
一个作业的完成要经过作业提交、作业收容、作业执行和作业完成四个阶段。
进程和作业的区别主要为:
七态模型:新建态、就绪挂起态、就绪态、运行态、阻塞态、阻塞挂起态、终止态
(1)执行:进程分到CPU时间片,正在执行
(2)就绪:进程已经就绪,只要分配到CPU时间片,随时可以执行
(3)阻塞:有IO事件或者等待其他资源(请求I/O,申请缓冲空间等);阻塞态的进程占⽤着物理内存。
(4)新建:进程刚被创建时的状态,尚未进入就绪队列。创建步骤包括:申请空白的 PCB,向 PCB 中填写一些控制和管理信息,系统向进程分配运行时所需的资源。
(5)终止:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所被终止时所处的状态。
(6-7)挂起:把阻塞的进程置换到磁盘中,此时进程未占用物理内存,我们称之为挂起;挂起不仅仅可能是物理内存不足,比如sleep系统调用,或用户执行Ctrl+Z也可能导致挂起。
–(6)就绪挂起:进程在外存(硬盘),但只要进入内存,马上运⾏。
–(7)阻塞挂起:进程在外存(硬盘)并等待某个事件的出现(进入就绪挂起态)。
进程控制:创建和撤销进程,分配资源、资源回收,控制进程运行过程中的状态转换。
进程同步:多进程运行进行协调–进程互斥(临界资源上锁)、进程同步。
进程通信:实现相互合作之间的进程的信息交换。
调度:作业调度,进程调度。
死锁(deadlock) 是指在多道程序系统中,一组进程中的每一个进程都无限期等待被该组进程中的另一个进程所占有且永远不会释放的资源。
死锁的发生必须同时满足四个条件:互斥,持有/等待,非抢占, 形成等待环。
饥饿(starvation) 是指系统不能保证某个进程的等待时间上界,从而使该进程长时间等待,当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿。
饿死(starve to death) 即是当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义。
讲解Linux内核操作系统——进程状态与转换
操作系统的进程管理软件
阻塞和挂起的区别和联系
进程的挂起状态解析
在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位,是原子操作。
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。
对于通常的进程,其创建、撤销以及要求由系统设备完成的I/O操作都是利用系统调用而进入内核,再由内核中相应处理程序予以完成的。进程切换同样是在内核的支持下实现的,因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
进程表加一项, 申请PCB并初始化,生成标识, 建立映像, 分配资源, 移入就绪队列:
由于父子进程之间共享 fork 之前打开的文件描述符, 并且父子进程共用文件读写偏移量,所以, 在执行逻辑上, fork 之前打开的文件, 要 close 两次!
进入终止状态的进程以后不能再执行,但在操作系统中保存状态码和一些计时统计数据供其他进程收集。
引起进程终止的事件主要有:
进程终止 / 撤销的过程如下:(从队列中移除, 归还资源, 撤销标识,回收PCB, 移除进程表项)
正在执行的进程,由于需要的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或等待新任务的到达等,则由系统自动执行阻塞原语,使自己由运行状态变为阻塞状态。
进程的阻塞是进程自身的一种主动行为,只有处于运行状态的进程,才可能将其自身转为阻塞状态。
进程阻塞(Block) 的执行过程如下:(保存现场信息, 修改PCB, 移入等待队列, 调度其他进程执行)
当被阻塞进程所需要的事件发生时,如 I/O 操作已完成或其所需要的数据已到达,则由相关进程(例如,提供数据的进程)执行唤醒原语,将等待该事件的进程唤醒。
进程的唤醒是一种被动行为,需要其他进程触发。
进程唤醒(Wakeup) 的执行过程如下:(等待队列中移出, 修改PCB, 移入就绪队列)
阻塞原语是由被阻塞进程自我调用实现的,而唤醒原语则是由一个与被唤醒进程相合作或被其他相关的进程调用实现的。
当系统资源紧张的时候,操作系统会对在内存中的资源进行更合理的安排,这时就会将将某些优先级不高的进程设为挂起状态,并将其移到外存,一段时间内不对其进行任何操作;当条件允许的时候(调试结束、被调度进程选中需要重新执行),会被操作系统再次激活调回内存。
进程挂起(Suspend) 的执行过程 如下:(修改状态并出入相关队列, 收回内存等资源送至对换区)
引起进程挂起 的几种情况:
进程挂起的状态转换:
运行状态->就绪挂起状态
:对于抢先式分时系统(即可抢占CPU资源),当有高优先级阻塞进程因事件出现而进入就绪状态时,系统可能会把运行进程转到就绪挂起状态。
就绪状态->就绪挂起状态
:为了让优先级更高的进程得到更多的资源运行。
通常,操作系统更倾向于挂起阻塞态进程而不是就绪态进程,因为就绪态进程可以立即执行,而阻塞态进程占用了内存空间但不能执行。但如果释放内存以得到足够空间的唯一方法是挂起一个就绪态进程,那么这种转换也是必需的。并且,如果操作系统确信高优先级的阻塞态进程很快就会就绪,那么它可能选择挂起一个低优先级的就绪态进程,而不是一个高优先级的阻塞态进程。
阻塞状态->阻塞挂起状态
:为了提交新的进程或为运行就绪状态的进程提供更多资源。
当内存空间比较紧缺的时候,将存在内存中的阻塞状态进程进入阻塞挂起状态,PCB等数据存入外存,让更需要内存的程序占用内存。
阻塞挂起状态->就绪挂起状态
:当阻塞状态等待的IO事件或其他事件到来的时候状态发生改变。
挂起状态和阻塞状态的区别:
进程激活(Active) 的执行过程如下:(分配内存, 修改状态并出入相关队列)
进程激活的状态转换(及条件):
就绪挂起状态->就绪状态
:如果内存中没有就绪态进程,操作系统需要调入一个进程继续执行;此外,就绪挂起进程的优先级高于就绪进程,这种情况的产生是由于操作系统设计者规定,调入高优先级的进程比减少交换量更重要。阻塞挂起状态->阻塞状态
:当一个进程释放足够的内存时,OS会把一个高优先级阻塞挂起进程转为阻塞进程。进程切换是指处理机从一个进程的运行转到另一个进程上运行,这个过程中,进程的运行环境产生了实质性的变化。
进程切换实质上就是被中断运行进程与待运行进程的上下文切换。
进程切换的过程如下:(保存被中断进程的上下文,转向进程调度,恢复待运行进程的上下文)
进程切换的发生时机:
进程切换一定发生在中断/异常/系统调用处理过程中, 常见的情况是:
把虚拟地址转换为物理地址需要查找页表,页表查找是⼀个很慢的过程(至少访问2次内存),因此通常使用Cache来缓存常⽤的地址映射,这样可以加速叶表查找,这个cache就是TLB(快表)。
由于每个进程都有自己的虚拟地址空间,那么每个进程都有自己的页表,而进程切换涉及虚拟地址空间的切换,那么页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来就是程序运行会变慢。
而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程切换不涉及虚拟地址空间的转换,主要是栈空间和代码段的切换。
进程切换必须在操作系统内核模式下完成, 这就需要模式切换;
模式切换又称处理器状态切换, 包括:
进程切换与处理机模式切换是不同的,模式切换时,处理机逻辑上可能还在同一进程中运行。如果进程因中断或异常进入到核心态运行,执行完后又回到用户态刚被中断的程序运行,则操作系统只需恢复进程进入内核时所保存的CPU现场,无需改变当前进程的环境信息。但若要切换进程,当前运行进程改变了,则当前进程的环境信息也需要改变。
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC, Inter Processes Communication)。
进程间通信的目的:
Linux 进程间通信的方式:
ls | wc –l
,为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc。管道的特点:
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
匿名管道的使用:
◼ 创建匿名管道
#include
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
◼ 查看管道缓冲大小命令
ulimit –a
◼ 查看管道缓冲大小函数
#include
long fpathconf(int fd, int name); // name 可填宏参数 _PC_PIPE_BUF
利用匿名管道,在 fork() 之前创建管道,才能共享内核区数据(fd),父进程发送数据给子进程,子进程读取到数据输出:
管道是半双工的,两个进程无法同时读和写,因此父进程关闭管道读端fd[0],子进程关闭写端fd[1],保证数据的稳定性和有效性。
匿名管道的通信本质是文件的读写,可利用 fcntl()
修改为非阻塞I/O。
lseek()
等文件定位操作。有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,区别在于:
有名管道的使用:
创建有名管道 mkfifo
(命令 / 函数)
1.通过命令: mkfifo 名字
2.通过函数:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 管道名称的路径
- mode: 文件的权限 和 open 的 mode 是一样的
是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号
不同进程间的有名管道通信:
有名管道的注意事项:
1.一个为只读权限打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道;
2.一个为只写权限打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道。(都阻塞在open语句处)
匿名 / 有名管道的读写特点(假设都是阻塞I/O操作):
读管道:
1. 管道中有数据,read 返回实际读到的字节数。
2. 管道中无数据:
写端被全部关闭(写端引用计数等于0),read 返回0(相当于读到文件的末尾)
写端没有完全关闭(写端引用计数大于0),read 阻塞等待
写管道:
1. 管道读端全部被关闭(读端引用计数等于0),进程异常终止(进程收到SIGPIPE信号 [13] )
2. 管道读端没有全部关闭(读端引用计数大于0):
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
内存映射(Memory-mapped I/O,mmap)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件(在原文件大小范围内)。(实现进程间通信,直接对内存进行操作,效率相对较高)
普通磁盘I/O读写文件要先把内容拷贝到页缓存(内核空间)中,然后再拷贝到用户空间中供使用,有2次拷贝。
unix访问文件的传统方法使用open打开他们,如果有多个进程访问一个文件,则每一个进程在再记得地址空间都包含有该文件的副本,这不必要地浪费了存储空间。下面说明了两个进程同时读一个文件的同一页的情形,系统要将该页从磁盘读到高速缓冲区中,每个进程再执行一个内存期内的复制操作将数据从高速缓冲区读到自己的地址空间。
而mmap读写文件是利用缺页异常把文件内容从磁盘换到用户空间中,只有1次拷贝,因此效率较高。
进程A和进程B都将该页映射到自己的地址空间,当进程A第一次访问该页中的数据时,它生成一个缺页中断,内核此时读入这一页到内存并更新页表使之指向它,以后,当进程B访问同一页面而出现缺页中断时,该页已经在内存,内核只需要将进程B的页表登记项指向此页即可。
(缺页中断就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。在这个时候,被内存映射的文件实际上成了一个分页交换文件。)
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read、write等操作。
更多内容参考链接
#include
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void *addr: NULL, 由内核指定
- length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
获取文件的长度:stat lseek
(最少为自动调整为系统分页大小的整数倍)
- prot : 对申请的内存映射区的操作权限
-PROT_EXEC :可执行的权限
-PROT_READ :读权限
-PROT_WRITE :写权限
-PROT_NONE :没有权限
要操作映射内存,必须要有读的权限。
e.g. PROT_READ、PROT_READ|PROT_WRITE
- flags :
- MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
- MAP_ANONYMOUS:匿名映射,不需要文件实体
- fd: 需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
prot: PROT_READ open:只读 / 读写
prot: PROT_READ | PROT_WRITE open:读写
- offset:偏移量,一般不用。必须指定的是4k(1024)的整数倍,0表示不偏移。
- 返回值:返回创建的内存的首地址
失败返回MAP_FAILED,(void *) -1
int munmap(void *addr, size_t length);
- 功能:释放内存映射,映射的内存在用户区堆和栈之间的共享库中,进程结束后也会自动释放。
- 参数:
- addr : 要释放的内存的首地址
- length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(…);
ptr++; 可以对其进行++操作
munmap(ptr, len); // 错误,要保存地址
2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。
3.如果文件偏移量为1000会怎样?
偏移量必须是4K(1024)的整数倍,返回MAP_FAILED
4.mmap什么情况下会调用失败?
- 第二个参数:length = 0时
- 第三个参数:prot
— 只指定了写权限
— prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
— lseek(),write()
— truncate()
6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open(“XXX”);
mmap(,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。
7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,);
4K
越界操作操作的是非法的内存 -> 段错误
内存空间的大小限制了文件拷贝的内容
思路:
1.对原始的文件进行内存映射
2.创建一个新文件(拓展该文件)
3.把新文件的数据映射到内存中
4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
5.释放资源
文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件。
匿名映射:不需要文件实体,没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,没有数据源。
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
- 对于前台进程(占用操作终端),用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
- 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
- 运行 kill 命令或调用 kill 函数。
使用信号的两个主要目的是:
– 让进程知道已经发生了一个特定的事情。
– 强迫进程执行它自己代码中的信号处理程序。
信号的特点:
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
查看系统定义的信号列表:kill –l
,前 31 个信号为常规信号,其余为实时信号。
信号的几种状态:产生、未决、递达
SIGKILL
和 SIGSTOP
信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
信号集
sigset_t
。信号处理流程:
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集**默认不阻塞任何的信号**
- 如果想要阻塞某些信号需要用户调用**系统的API**
4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 如果没有阻塞,这个信号就被处理
- 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
两次相同的信号先后递送,在第一次相关回调函数执行完后,才回执行下一次的回调函数。
SIGCHLD
信号(避免僵尸进程)SIGCHLD信号产生的3个条件:
1.子进程结束
2.子进程接收到 SIGSTOP 信号暂停时
3.子进程处在停止态,接受到 SIGCONT 后唤醒时
以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号
使用SIGCHLD
信号解决僵尸进程的问题:
父进程捕捉到SIGCHLD
信号后,才中断调用 waitpid()
进行进程回收,可以避免父进程循环 wait()
占用资源。
共享内存的实现方式分为两种,分别是基于物理内存实现和基于内存映射实现。
4.3 已讲到内存映射需要将磁盘文件的数据映射到内存,用户通过修改内存来修改磁盘文件,进而实现共享了这个磁盘文件映射的进程进行通信,但是涉及到磁盘文件的读写;虽然匿名映射不需要指定文件,但是只能用于具有亲缘关系的进程间通信。
共享内存和内存映射的区别:
此处介绍基于物理内存实现的共享内存。
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段),每个进程可以将自身的虚拟地址映射到物理内存中的特定区域,共享内存段会成为进程用户空间的一部分,因此这种 IPC 机制无需内核介入。
共享内存是最高效的进程间通信的方式。
共享内存只需要两次拷贝即可实现,即数据从进程A的用户空间到内存,再从内存到进程B的用户空间。
A用户空间 -> 内存 -> B用户空间
而管道等要求进程需要通过用户空间和内核内存间进行数据拷贝的做法则需要四次拷贝:首先将数据从进程A的用户空间拷贝到进程A的内核空间,其次将数据从进程A的内核空间拷贝到内存中,之后数据又从内存被拷贝到进程B的内核空间,最后数据从进程B的内核空间拷贝到进程B的用户空间中。
A用户空间 -> A内核空间 -> 内存 -> B内核空间-> B用户空间
共享内存没有进程间同步与互斥机制。例如,进程A对共享内存执行写操作,在A的写入结束之前,进程B就可以从共享内存区读取数据,并无某种自动的机制来阻止进程B的读操作。一般为了实现进程同步和互斥,常常将共享内存和信号量配合使用。
共享内存使用步骤:
shmget()
创建一个新共享内存段或获取一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。shmat()
来关联共享内存段,即使该段成为调用进程的虚拟内存的一部分。shmat()
调用返回的 addr
值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。shmdt()
来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。shmctl()
来删除共享内存段。只有当当前所有关联内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体 struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数
终端执行命令 ipcs -m
问题2:可不可以对共享内存进行多次删除 shmctl ?
可以的
因为 shmctl 标记删除共享内存,不是直接删除
- 什么时候真正删除呢?
当和共享内存关联的进程数为 0 的时候,就真正被删除
- 当共享内存的 key为 0 的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
什么是消息队列?
深入消息队列MQ
消息队列(Message Queue,简称MQ),指保存消息的一个容器,本质是个队列(先进先出)。
消息(Message)是指在应用之间传送的数据,消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。
下图便是消息队列的基本模型,向消息队列中存放数据的叫做生产者,从消息队列中获取数据的叫做消费者。
消息队列中间件是分布式系统中重要的组件,主要解决异步处理、应用解耦、流量削锋等问题,实现高性能、高可用、可伸缩和最终一致性架构。
引入消息队列,将非主要的业务逻辑进行异步处理,主要目的是减少请求响应时间,实现非核心流程异步化,提高系统响应性能。
场景说明:用户注册后,需要发注册邮件和注册短信提醒。传统的做法有两种 1.串行方式;2.并行方式
假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。
消息队列在异步的典型场景就是将比较耗时而且不需要即时(同步)返回结果的操作,通过消息队列来实现异步化。
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件、发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了2倍。
如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,耦合性低,这样系统的可扩展性无疑更好一些。
使用了消息队列后,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦。
每个成员不必受其他成员影响,可以更独立自主,只通过消息队列MQ来联系,典型的上下游解耦如下图所示:
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列,相当于做了一次缓冲。
a、可以控制活动的人数
b、可以缓解短时间内高流量压垮应用
用户的请求,服务器接收后,首先写入消息队列;
假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面;
秒杀业务根据消息队列中的请求信息,再做后续处理。
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下:
日志采集客户端:负责日志数据采集,定时写受写入Kafka队列;
Kafka消息队列:负责日志数据的接收,存储和转发;
日志处理应用:订阅并消费kafka队列中的日志数据。
消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。
以上实际是消息队列的两种消息模式,点对点和发布订阅模式。
每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中);
发送者和接收者之间在时间上没有依赖性;
接收者在成功接收消息之后需向队列应答成功。
每个消息可以有多个消费者:和点对点方式不同,发布消息可以被所有订阅者消费;
发布者和订阅者之间有时间上的依赖性;
针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息;
为了消费消息,订阅者必须保持运行的状态。
信号量(semaphore)是操作系统用来解决并发中的(进程或者线程)互斥和同步问题的一种方法。
对于信号量的值 n(允许进入临界区的线程/进程数):
n > 0:当前有可用资源,可用资源数量为 n
n = 0:资源都被占用,可用资源数量为 0
n < 0:资源都被占用,并且还有 n 个进程正在排队
信号量可以但不一定实现互斥(不是说不能,一种情况是不存在共享临界区,谈不上互斥,另一种情况是允许共同进入临界区,比如读操作),肯定实现了同步。
可用于进程间共享内存的进程同步问题。
sem_t sem; // 信号量的类型
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 【0 用在线程间 ,非 0 用在进程间】
- value : 信号量中的值
int sem_destroy(sem_t *sem);
- 释放资源
int sem_wait(sem_t *sem);
- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
- 对信号量解锁,调用一次对信号量的值+1
int sem_getvalue(sem_t *sem, int *sval);
用于网络中不同主机之间的进程间通信
操作系统的调度算法
操作系统——进程调度
时间片轮转(RR)、优先级调度算法以及多级反馈队列调度算法
进程调度的目的:在进程间切换CPU,最大化CPU利用率,通过操作系统的调度使得计算机资源分配和使用更加高效。
需要CPU调度的4种情况:
运行状态->阻塞状态
(例如I/O请求,或wait()调用)运行状态->就绪状态
(例如当出现中断)阻塞状态->就绪状态
(例如I/O完成)进程终止
调度方案分为两种:
(1)非抢占式调度(nonpreemptive)或协作(cooperative):
一旦某个进程分配到CPU,该进程会一直使用CPU,直到它终止或切换到阻塞状态。
(2)抢占式调度(preemptive):
允许其他进程抢占运行中程序的CPU资源,这中间可能涉及进程共享数据的一致性问题、进程同步问题,同时带来了更多的切换次数,这会造成更高的上下文切换的成本。
为了比较不同的CPU调度算法,采用一些比较准则来评价CPU调度算法的特性,具体的一些比较准则包括:
批处理是指用户将一批作业提交给操作系统后就不再干预,由操作系统控制它们自动运行。
批处理操作系统的目的是提高系统资源的利用率,不具有交互性。
通过FIFO队列实现,按照作业到达任务队列的顺序调度:
当一个进程进入就绪队列中的时候,它的PCB会被链接到队列尾部;当CPU空闲时,它会分配给位于队列头部的进程,并且这个进程从队列中移去。
每次从队列里选择预计时间最短的作业运行。
首先按照作业的服务时间挑选最短的作业运行,在该作业运行期间,一旦有新作业到达系统,并且该新作业的服务时间比当前运行作业的剩余服务时间短,则发生抢占;否则,当前作业继续运行。
该算法考虑到了有IO 阻塞 / 唤醒的情况,确保一旦新的短作业或短进程进入系统,能够很快得到处理。
HRN调度策略同时考虑每个作业的等待时间 W 和估计需要的执行时间 T ,从中选出响应比 R = (W+T)/T = 1+W/T 最高的作业投入执行,是介于FCFS和SJF之间的一种折中算法。
分时操作系统是利用分时技术的一种联机的多用户交互式操作系统,每个用户可以通过自己的终端向系统发出各种操作控制命令,完成作业的运行。分时是指把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。
系统将CPU处理时间划分为若干个时间片(q),进程按照到达先后顺序排列。每次调度选择队首的进程,执行完1个时间片 q 后,计时器发出时钟中断请求,发生抢占,该进程移至队尾,并通过上下文切换执行当前的队首进程;进程可以未使用完一个时间片,就出让CPU(如阻塞)。
RR算法的性能很大程度上取决于时间片的大小:
时间片太小,频繁的进程上下文切换耗费大量资源;
时间片太大,RR退化成FCFS。一般根据经验,80%的CPU执行应小于时间片。
为每个进程关联一个优先级,具有最高优先级的进程会分到CPU;具有相同优先级的进程按照FCFS的顺序调度。
SJF算法是一个简单的优先级算法,其优先级(p)为下次(预测的)CPU执行时间的倒数。
解决低优先级进程的无穷等待的方案之一:老化(aging),即逐渐增加在系统中等待时间很长的进程的优先级。
将就绪队列分成多个单独的队列,根据进程属性(如内存大小、进程优先级、进程类型等),一个进程被分到其中一个队列(不再改变),每个队列有自己的调度算法,并且以最高优先级选择进程。
多级队列调度算法示例:
允许进程在多级队列之间迁移,该算法规则为:
多级反馈队列调度示例:
Q1:表示时间周期为8毫秒的 Round Robin 队列;
Q2:表示时间周期为16毫秒的 Round Robin 队列;
Q3:先到先服务队列。
当进程进入Q1 时,它被允许执行,如果它在 8 毫秒内未完成则转移到 Q2 队尾;如果它在16 毫秒内还未完成,它将再次被转移到 Q3 队尾。