命名管道 FIFO
上面介绍的管道也称为匿名管道,只能用于亲缘关系的进程间通信。为了克服这个缺点,出现了有名管道 FIFO 。有名管道提供了一个路径名与之关联,以文件形式存在于文件系统中,这样即使不存在亲缘关系的进程,只要可以访问该路径也能相互通信。
命名管道支持同一台计算机的不同进程之间,可靠的、单向或双向的数据通信。
信号 Signal
信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,无需知道该进程的状态。如果该进程当前不是执行态,内核会暂时保存信号,当进程恢复执行后传递给它。
如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。
信号在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:
消息队列 Message Queue
消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示, 只有在内核重启或主动删除时,该消息队列才会被删除。
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 另外,某个进程往一个消息队列写入消息之前,并不需要另外读进程在该队列上等待消息的到达。
共享内存 Shared memory
共享内存是一个进程把地址空间的一段,映射到能被其他进程所访问的内存,一个进程创建、多个进程可访问,进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
共享内存使得多个进程可以可以直接读写同一块内存空间,是最快的可用 IPC 形式,是针对其他通信机制运行效率较低而设计的。共享内存往往与其他通信机制,如信号量配合使用,来实现进程间的同步和互斥通信。
套接字 Socket
套接字你可能没听过这个名字,但绝对是接触的最多的一种进程间通信方式。因为我们熟悉的 TCP/IP 协议栈,也是建立在 socket 通信之上,TCP/IP 构建起了当前的互联网通信网络。
它是一种通信机制,凭借这种机制,既可以在本机进程间通信,也可以跨网络通过,因为,套接字通过网络接口将数据发送到本机的不同进程或远程计算机的进程。
多线程服务模型
线程概念
线程是操作操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位,一个进程内可以包含多个线程,线程是资源调度的最小单位。
多线程模型
启动多个相同功能的进程能提高服务处理能力,但由于各个进程的地址空间相互隔离,通信不便。
于是,多线程服务模型出场。通过前面的学习我们知道,一个进程内的多个线程可以共享进程的全部系统资源。进程内创建的多个线程都可以访问进程内的全局变量。
当然没有免费的午餐,线程虽然能方便的访问进程资源,但也带来了额外的问题。比如多线程访公共资源带来的同步与互斥问题,不同线程访问资源的先后顺序会相互影响,如果不做好同步和互斥会产生预期之外的结果,甚至死锁。
什么是多线程同步
多线程同步是线程之间的一种直接制约关系,一个线程的执行依赖另一个线程的通知,当它没有得到另一个线程的通知时必须等待,直到消息到达时才被唤醒,即有很强的执行先后关系。
比如你搭建了一个商城服务。这个服务的下单流程是这样的:第一步必须要先挑选商品加入购物车,第二步才能结账计算订单金额,假设这两个步骤的操作分别由两个线程去完成,则这两个线程的操作顺序很重要,必须是先下单再结账,这就是线程同步。
什么是多线程互斥
多线程互斥指的是多线程对资源访问的排他性。所谓排他性,就是当有多个线程都要使用某一共享资源时,任何时刻最多只允许一个线程获得对这个共享资源的使用权,当共享资源被其中一个线程占有时,其他未获得资源的线程必须等待,直到占用资源的线程释放资源。
打个比方,你们班只有一台投影仪,当一个同学在上面放电影的时候,如果老师进来上课要用这个投影仪,那就只能由这个同学放弃投影仪的使用权,交给老师上课投影使用,对,教室里唯一的投影仪是共享资源,具有排他性,老师和学生比作是两个线程的话,那这两个线程是互斥的访问共享资源(投影仪)。
多线程同步和互斥方法
Linux 系统提供以下几种方法来解决多线程的同步和互斥问题,分别是:互斥锁、条件变量、读写锁、自旋锁、条件变量。
互斥锁(同步)
互斥锁的作用是对临界区加以保护,以使任意时刻只有一个线程能够执行临界区的代码,实现了多线程对临界资源的互斥访问。
互斥锁接口函数:
条件变量(同步)
条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。适合多个线程等待某个条件的发生,不使用条件变量,那么每个线程就不断尝试互斥锁并检测条件是否发生,浪费系统资源。
通常条件变量和互斥锁同时使用。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件,可以用来实现线程间的同步。
条件变量系统 API 如下:
读写锁(同步)
互斥量要么是加锁状态,要么是不加锁状态,而且一次只有一个线程对其进行加锁。读写锁可以有3种状态:读加锁状态、写加锁状态和不加锁状态。
一次只有一个线程可以占有写模式读写锁,但是可以有多个线程同时占有读模式的读写锁。因此,读写锁适合于对数据结构的读次数比写次数多得多的情况,且读写锁比互斥量具有更高的并行性。
读写锁加锁规则
1:如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁;
2:如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。
读写锁系统 API
自旋锁(同步)
互斥锁得不到锁时,线程会进入休眠,引发任务上下文切换,任务切换涉及一系列耗时的操作,因此用互斥锁一旦遇到阻塞切换代价是十分昂贵的。
而自旋锁阻塞后不会引发上下文切换,当锁被其他线程占有时,获取锁的线程便会进入自旋,不断检测自旋锁的状态,直到得到锁,所谓的自旋就是循环等待的意思。
自旋锁在用户态使用的比较少,在内核使用的比较多。自旋锁适用于临界区代码比较短,锁的持有时间比较短的场景,否则会让其他线程一直等待造成饥饿现象。
自旋锁 API 接口
信号量(同步与互斥)
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
信号量是一个特殊类型的变量,它可以被增加或者减少。可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。但对其的访问被保证是原子操作,即使在一个多线程程序中也是如此。
信号量类型:
信号量 API
协程服务模型
什么是协程
什么是协程呢?协程 Coroutines
是一种比线程更加轻量级的微线程。类比一个进程可以拥有多个线程,一个线程也可以拥有多个协程,因此协程又称微线程和纤程。
可以粗略的把协程理解成子程序调用,每个子程序都可以在一个单独的协程内执行。
协程服务模型
为了说明什么是协程模型,先用多线程下的生产者消费者模型举个栗子。
启动两个线程分别执行两个函数 Do_some_IO
和 Do_some_process
,第一个做耗时的 IO 处理操作,第二个对 IO 操作结果做快速的处理计算工作。伪代码如下:
多线程执行过程是这样的:
可以看到,多线程模型为了保证各个线程并行工作,需要额外做很多线程间的同步和通知工作,而且线程频繁的在阻塞和唤醒间切换,我们知道 Linux 下线程是轻量级线程 LWP
,每次线程切换涉及用户态和内核态的切换,还是很消耗性能的。
同样的场景在协程模型里是怎么处理的呢?还是用前面的例子,说明协程模型的执行流程。
Do_some_IO() // IO处理协程
Do_some_process() // 计算处理协程
协程优势
硬件提升性能
前面讲的多线程、多进程、协程都还只是软件层面的提高服务处理能力。真正硬核的是从硬件层面提高处理能力,增加 CPU 物理核心数目,当然硬件都是有成本的,所以只有软件层面已经充分榨干性能才会考虑增加硬件。
不过,老板有钱买最好最贵的服务器另说,这是人民币玩家和穷逼玩家的区别了,软件工程师留下了贫困的泪水。
增加机器核心数
CPU领域有一条摩尔定律:大概 18 个月会将芯片的性能提高一倍。现在这个定律变的越来越难以突破,CPU 晶体管密度工作频率很难再提高,转而通过增加 CPU 核心数目的方式提高处理器性能。
目前商用服务器架构基本都是多核处理器,多核的处理器能够真正做到程序并行运行,处理效率大幅度提升,那该如何查看 CPU 核心数目呢?
对于 Windows 操作系统,打开任务管理器,通过界面的「内核」和「逻辑处理器」能看到。
查看 cpu 核心数
对于 Linux 操作系统,通过下面 2 种方式查看 CPU 核心相关信息。
1. 通过cpuinfo文件查看
使用cat /proc/cpuinfo
查看 cpu 核心信息,如下两个信息:
cpuinfo
输出示例:
2. 通过编程接口查看
除了上面以文件的形式查看 cpu 核心信息之外,系统还提供了编程接口可以查询,系统 API 如下。
CPU亲和性
CPU 亲和性是绑定某一进程或线程到特定的 CPU 或 CPU 集合,从而使得该进程或线程只能被调度运行在绑定的 CPU或 CPU 集合上。
假如某些进程或线程是 CPU 密集型的,不希望被频繁调度,又或者你有其他特殊需求,不希望进程或线程被调度在不同 CPU 之间频繁切换,则可以将该进程或线程绑定到特定的 CPU 上 ,可以在特定场景下优化程序性能。
绑定进程
在多进程模型中,绑定进程到特定的核心,下面是绑定进程的系统 API
绑定线程
在多线程模型中,绑定线程到特定的核心,下面是绑定线程的系统 API
总总结结
本文从程序任务类型出发,区分任务为 CPU 密集型和 IO 密集型两大类。接着分别说明提高基于这两类任务的服务性能方法,分为软件层面的方法和硬件层面的方法,其中软件层面主要讲述利用多进程、多线程以及协程模型,当然现有的技术还有 IO 多路复用、异步 IO 、池化技术等方案。讲到多线程和多进程,顺势说明了进程间通信和线程间同步互斥技术。
第二部分,讲解了从硬件层面提高服务性能:提高机器核心数,并教你如何查看 CPU 核心数的方法。最后,还可以通过软硬结合的方式,把硬件核心绑定到指定进程或者线程执行,最大程度的利用 CPU 性能。
希望通过本文的学习,读者对高性能服务模型有个初步的了解,并能对服务优化的方法和利弊举例一二,就是本文的价值所在。
再聊两句(求个三连)
感谢各位的阅读,文章的目的是分享对知识的理解,技术类文章我都会反复求证以求最大程度保证准确性,若文中出现明显纰漏也欢迎指出,我们一起在探讨中学习。
如果觉得文章写的还行,对你有所帮助,不要白票 lemon,动动手指「点赞」「三连」是对我持续创作的最大支持。
今天的技术分享就到这里,我们下期再见。