在上一篇文章中,主要介绍了怎样从编程言语一步步到可履行程序,介绍了编译器,链接器,虚拟内存,笼统的概念。在这篇文章中,即将把要点放在程序运转起来之后,来介绍程序运转的时分发生了什么,其间就有在大学的操作系统课上耳熟能详的进程,线程,协程,同步,异步,堵塞,非堵塞的概念,还会介绍高并发,高性能的服务器是怎样完结的。

1 CPU 在干什么?

咱们在上一篇文章中介绍到:

CPU是个笨蛋,笨到只会把数据从一个地方搬到别的一个地方,进行简略的计算后,再把数据搬回去。

CPU 只知道两件作业:

  1. 从内存中取出指令
  2. 履行指令,再回到第一步

那么在第一步,CPU 依据什么来取出指令呢,答案是PC寄存器,也便是程序计数器。寄存器能够了解是一种内存,可是速度更快,容量也更小,关于内存的详细内容,也将会鄙人一篇文章中介绍。

PC寄存器中寄存的是指令在内存中的地址是CPU 即将履行的下一条指令。PC 寄存器的初始值来自内存,内存中的指令是从磁盘中保存的可履行文件里加载过来的,而磁盘中的可履行文件是由源文件经过编译器,链接器生成的,这又回到了上一章介绍的知识。

除此之外,PC寄存器的地址默认加1,也便是默认状况下,CPU 按次序一条一条履行指令,假定遇到了if-else 或许函数调用时,PC寄存器的只也会依据CPU 的履行成果动态改动里面要跳转的地址。

计算机底层2 程序在运行时发生了什么

咱们都知道,main 函数是程序的进口,程序启动时,会先找到main 函数的对应的第一条机器指令,然后将其地址写入PC寄存器,这样CPU 就会开端运转这条指令,再一条条履行下去,程序就运转起来了。

在上面的进程中,咱们发现,假定想让CPU 运转程序,就要:

  1. 在内存中找到一块巨细适宜的区域装入程序
  2. CPU 寄存器初始化后,找到函数的进口,设置PC寄存器

能够看到,把程序加载到内存,这是需求手艺完结的,进程繁琐,重复性高,而且,一次性只能运转一个程序,效率极低,无法支撑多使命编程,一起,要给每个程序手动链接硬件驱动

可是咱们发现,现在的程序员如同并没有去手动把程序加载到内存,多使命编程十分常见,也不需求给每个程序装置硬件驱动,那是谁帮咱们做了上面的这些作业呢?答案便是现代的操作系统

2 操作系统

总结咱们上面说到的一些咱们不期望手动处理,可是期望操作系统这个“程序”能够完结的作业:

  1. 自动加载程序
  2. 完结多使命功用的进程办理
  3. 办理软硬件资源,为程序供给服务

自从操作系统诞生后,程序员再也不必手动加载可履行文件,也不必手动保护程序的运转,一切交给操作系统即可。

2.1 进程

咱们常说:程序进程,便是由于在程序运转起来的时分,程序以进程的办法被办理起来,一个程序便是一个进程,咱们上面说,操作系统帮助咱们完结多使命功用的进程办理,这儿的”多使命办理“,是不是便是一起运转多个程序进程呢?要怎样完结呢?

2.1.1 多使命编程

咱们知道,CPU 一次只能做一件作业,那要怎样让进程A 和进程B 一起运转呢?

还记得在上一篇文章中,咱们说到,CPU 很笨,可是它有一件人脑难以超越的绝对优势:。由于快,CPU 就能够做到在几个线程中”来回穿梭“,先运转一会进程A,再马上跳去运转进程B,只需CPU 足够快,看起来便是进程A 和进程B 一起在运转,也便是多使命编程。

计算机底层2 程序在运行时发生了什么

当然,上面是单核CPU 的状况,假定是多核,那么每个CPU 都能得到充分运用,每个CPU 都能履行使命,完结并发

计算机底层2 程序在运行时发生了什么

2.1.2 多进程的好与坏

在上一篇文章展现了进程的内存地址空间散布:

计算机底层2 程序在运行时发生了什么

能够看到,每个进程都有自己的进程地址空间,进程间是独立的

这便是多进程编程的其间一个长处:各个进程的地址空间彼此阻隔,因而一个进程溃散后,不会影响到其他进程

可是这个长处一起也是多线程编程的缺陷。

现在假定咱们有两个函数A和B。别离交给进程A 和进程B 去履行,最后再把在进程B中履行完结的函数B 的运转成果加到进程A 中,能够知道,这个场景触及到了进程间的通讯

可是咱们刚刚说了,每个进程都有自己的进程地址空间,进程间是独立的,假定跨进程进行编程,那么在编程上是比较复杂的。

略微总结一下:

多线程编程的长处

  1. 编程简略,易于了解
  2. 各个进程的地址空间彼此阻隔,因而一个进程溃散后,不会影响到其他进程
  3. 能够充分运用多核资源

缺陷

  1. 各个进程都有自己的地址空间,彼此阻隔,进程间通讯在编程上比较复杂
  2. 创立线程的开支大,假定频繁地创立,毁掉线程无疑会加重系统的担负

因而,有了线程。

2.2 线程

线程能让多个CPU 来履行同一个进程中的机器指令。

计算机底层2 程序在运行时发生了什么

这样,就不存在进程间通讯的开支了,由于都是在同个进程中。

有了线程的概念,咱们只需求开启一个进程,而且创立多个线程,就能够让一切的CPU 都作业起来,充分运用多核,这才是高并发,高性能的底子所在。

当然,并不是只要多核才能进行多线程,单核相同能够,由于线程是操作系统层面的完结,和有多个中心是没有关系的。在单核中,便是像上面多使命编程一样,CPU 在不同的线程之间切换履行,然后伪并行地履行多个使命

举个比方,假定有一个使命A,耗时3分钟,还有一个使命B,耗时4分钟,假定有线程A 和线程B 别离履行使命A 和使命B,假定在多核中,总耗时取决于耗时最长的那个使命(使命B),所以耗时4分钟。但假定是单核,单个CPU 在两个线程中切换履行使命,总耗时仍是3 + 4 = 7分钟。 当今大部分的计算机,移动设备都是多核系统。

2.2.1 多线程的内存布局

咱们知道,函数在履行时,所依赖的信息,包括函数参数,部分变量,回来值等信息,都被保存在相应的中。

在有了多线程后,一个进程中就有多个程序的进口,也便是说,有多个履行流,那么,这个进程中也不应该只要一个栈帧,应该是每个线程(履行流)都有属于自己的栈帧。

计算机底层2 程序在运行时发生了什么

2.2.2 线程池

线程尽管方便,可是假定来一个恳求或许使命就创立一个线程,特别是关于比较简略,耗时短的使命,就会有以下的缺陷:

  1. 很多的创立和毁掉线程是耗时的
  2. 每个线程都会有自己独立的栈区,当创立很多的线程时,会耗费过多的内存等系统资源
  3. 很多线程之间的切换使得线程间切换的开支添加

因而,为了防止这些由于重复屡次创立,毁掉线程带来的开支和性能的下降,发生了线程池

线程池的概念很简略,便是创立一批线程,有使命就交给它们处理,因而不需求频繁的创立和毁掉。其实,也便是复用的思维。

2.3 线程间同享资源

在大学的操作系统课上,咱们现已把进程和线程的概念和它们之间的关系背的滚瓜烂熟了:进程是资源调度的最小单位,线程是CPU 调度的最小单位,一个进程能够包含多个线程,进程是线程的容器,线程是进程的实践履行者,线程之间同享进程资源

那么,线程究竟同享了哪些进程的资源呢?

首先,咱们来看看什么是线程私有的资源:

2.3.1 线程私有资源

函数的运转时信息保存在栈帧中,栈帧组成了栈区,栈帧中保存了函数的回来值、函数参数、回来值、部分变量以及该函数运用的寄存器信息

计算机底层2 程序在运行时发生了什么

此外,CPU 履行机器指令时其内部寄存器的值也属于当时线程的履行状况:

如PC 寄存器,保存的是下一条被履行指令的地址,栈指针,指向着栈顶。这些寄存器信息也是线程私有的。

所以,线程的栈区,PC 寄存器,栈指针,履行函数运用的寄存器信息,都是线程私有的。

2.3.2 线程同享资源

2.3.2.1 代码区

咱们在第一篇文章的时分说过,代码区寄存的是程序员写的代码生成的机器指令,线程之间同享代码区,一切的线程都能够履行代码区的代码。

除此之外,代码区的代码指令在编译完结后,便是只读(read only) 的,任何线程都不能修正代码区的代码,因而,尽管代码区能够被一切进程内的线程同享,可是不会呈现线程安全问题。

2.3.2.2 数据区

数据区寄存的是全局变量,相同,一切的线程都能访问到。

2.3.2.3 栈区

咱们刚方才说到,线程有自己的栈区,这是线程私有的,可是事实上,同一个进程的线程栈帧并不是像进程与进程一样相互隔绝地址空间的。因而,假定一个线程能够拿到来自别的一个线程的指针,那么该线程能够直接读写别的一个线程的栈区。

计算机底层2 程序在运行时发生了什么

这种线程间没有保护的机制有时分便当了线程间通讯,可是有时分会带来难以觉察的bug,由于一个线程有或许会无意间修正了属于其他线程的私有数据,而且不知道什么时分会爆雷,有或许其他线程正在平稳运转,突然就出bug,或许溃散,这个时分,呈现问题的代码距离真实的bug 或许现已很远了

2.3.2.4 动态链接库与文件

咱们在上一篇文章中介绍了在生成可履行文件中的静态链接和动态链接。

假定是静态链接,那么在程序加载前,所依赖的库就现已悉数打包到可履行程序中,这类程序在启动时不需求额定的作业。

而动态链接是可履行程序中并不包含依赖库的代码和数据,当程序加载或许运转时,才完结链接进程,也便是先找到所依赖库的代码和数据,然后放到进程的地址空间中。

那么放到进程地址空间的哪里呢?

答案是放到了栈区和堆区中间的那部分闲暇区域中:

计算机底层2 程序在运行时发生了什么

这一部分的地址空间也是被一切的线程同享的。

除此之外,假定程序在运转进程中翻开了一些文件,那么进程地址空间中还保存有翻开的文件信息,进程翻开的文件信息也能够被一切的线程运用,也成为了线程的同享资源。

2.3.3 线程部分存储 TLS

线程部分存储是指一个变量在每个线程中都有一个副本

寄存在该线程区域的变量有两个意义:

  1. 能够被一切线程访问到
  2. 尽管一切线程都能够访问到这个变量,但这个变量只属于一个线程,一个线程对这个线程的修正对其他线程是不行见的

假定现在有这样一段代码:

int a = 1;
void func() {
    a++;
    printf("%d\n", a);
}
void main() {
    thread t1(run);
    t1.join();
    thread t2(run);
    t2.join();
}

很简略想到,这个运转成果应该是:

2
3

由于两个线程都能同享数据区的资源,线程t1 先读写,a = 2,线程t2 后读写,a = 3

可是假定运用TLS,即int a = 1; 变成__thread int a = 1; 这样程序的运转成果会变成这样:

2
2

这便是刚刚说到的:TLS 线程部分存储的作用:使这个变量在每个线程中都有一个副本,使得这个线程对变量对其他线程不行见

计算机底层2 程序在运行时发生了什么

了解线程私有资源和同享资源是为了写出线程安全的代码。

2.4 线程安全

为什么在买火车票时,几万部手机一起购票,也不会呈现一张票被好几部手机一起抢到的状况呢,为什么每一部手机都能正确显示剩下的票数呢?这便是线程安全。

刚刚咱们介绍了线程的私有资源和同享资源,很明显,线程的私有资源能够完结线程安全,假定是同享资源,在不影响其他线程的束缚下,也能完结线程安全。就比方代码区是只读read only 的,就不影响其他线程,就像在卖高铁票的时分,假定仅仅检查剩下的票数,便是线程安全的。

这样的话,咱们简略知道,有线程不安全风险的就只要堆区和数据区的数据。

堆区是用于动态分配内存,也便是malloc/new/alloc这样在堆区恳求的内存。数据区寄存的是全局变量。

那么,应该怎样完结线程安全呢?

  1. 运用线程部分贮存TLS

    刚刚也说明了,TLS 是一个变量在每个线程中都有副本,一个线程对变量的改动对其他线程是不行见的。所以TLS 是线程安全的。

  2. 只读readonly

    假定需求运用全局变量,那是不是能够让这种资源仅仅可读的?readonly不会发生线程安全问题。

  3. 原子操作atomic

    一个操作在履行进程中不会被中止或干扰,要么履行完毕,要么底子不履行,不存在中间状况。

  4. 同步互斥

    在这一步,程序员不得已手动保护线程访问同享资源的次序,比方互斥锁自旋锁信号量和其他同步互斥机制都能够抵达这个目的。

自旋锁

假定同享的数据先被其他线程运用了,那么该线程就会以死循环的办法等候锁,一旦访问的资源被解锁,则等候的线程会被当即履行

自旋锁适合于:

  1. 预计等候的时刻很短
  2. 临界区(加锁的资源区)经常被调用,可是竞赛状况很少发生
  3. 多核处理器
  4. CPU 的资源不严重

互斥锁 与自旋锁不同,在互斥锁的状况下,假定同享的数据被其他线程占用了,那么该线程就会以休眠的办法等候锁,一旦访问的资源被解锁,则等候资源的线程就会被唤醒

自旋锁适合于:

  1. 预计等候时刻很长
  2. 临界区竞赛激烈
  3. 单核处理器
  4. CPU 资源严重

信号量

在Objective-C 的GCD(iOS 开发中用于多线程编程的比较好的计划),dispatch_semaphore 是一个用于完结信号量(Semaphore)的API。在dispatch_semaphore 中,运用计数来完结这个功用

计数小于 0 时需求等候,不行经过。

计数为 0 或大于 0 时,不必等候,可经过。

计数大于 0 且计数减 1 时不必等候,可经过。

经过加减计数,抵达加锁(不行经过)和解锁(可经过)的作用

到现在为止,线程的创立,毁掉,调度,都是由操作系统帮咱们完结的,那么咱们能够在不依赖操作系统的状况下自己完结线程吗?

协程能够做到。

3 协程

协程和普通函数在形式上没有差别,只不过协程有一项和线程很相似的身手:暂停与康复

CPU 之所以能够在线程之间切换,履行多使命编程,靠的便是能够先暂停一个线程,再康复它的运转,这其间最重要的便是记住线程的状况,便于后续康复履行。

这儿用于“记住”线程状况的便是上下文Context,当一个线程被暂停了,就会把它现在的状况保存到Context 中,后边再依据Context 来康复程序的运转。

那么协程能够做到相似的身手:能够保存自身的履行状况,从协程回来后还能从上一个暂停点(挂起点)持续履行

而函数假定要暂停,就只能return,可是return后边的代码就再也无法被履行了。

就比方在python 中,运用yield来完结协程:

def func():
  print("a")
  yield
  print("b")
  yield
  print("c")

运用:

def funcUse():
  co = func() # 得到该协程
  next(co)    # 调用该协程
  print("in functionA") # 完结其他操作
  next(co)    # 持续该协程

调用成果:

a
in functionA
b

能够看到协程能够做到暂停与康复。

让咱们用图的办法检查函数与协程的区别:

计算机底层2 程序在运行时发生了什么
计算机底层2 程序在运行时发生了什么

能够看到,funcA 函数在运转了一段时刻后,调用协程,协程开端运转,直到第一个挂起点,随后回来到funcA,funcA 在运转一段时刻后,再次调用协程,此刻,协程从上次的挂起点之后开端履行而不是从头开端,直到第二个挂起点,随后再次回来到funcA。

这个进程,就像是操作系统对线程的分配,有了协程,程序员也能够扮演操作系统相似的角色了,协程的调度权在程序员自己手上。除此之外,其实,函数也只不过是没有挂起点的协程罢了

协程的技能比线程更早呈现,后来有了线程,直接由操作系统分配,愈加方便,协程逐步淡出程序员的视野。在近几年,尤其是移动互联网时代的到来,服务端需求处理很多的用户恳求,程序员发现协程在完结高并发,高性能的服务器上有着优势,所以,协程再一次回到程序员的视野中。

4 回调函数

回调函数是指一段以参数的形式传递给其他代码的可履行代码

比方:

void funcOther(func f) {
    ...
    f();
    ...
}

4.1 同步调用和异步调用

在一般状况下,程序员调用函数的思维是这样的:

  1. 调用某个函数,获取成果
  2. 处理取得的成果
res = request();
handle(res);

这便是函数的同步调用。

可是假定,咱们把函数以参数传递给调用的函数,在调用的函数中处理,就像这样:

request(handle);

这样,咱们不关怀handle函数什么时分才会被调用,这是request 需求关怀的。

这两种办法用图这样表达:

计算机底层2 程序在运行时发生了什么

4.2 同步回调和异步回调

首先是同步回调,或许叫堵塞式回调,这是咱们最了解的回调办法。假定要调用函数A,并传递了回调函数作为参数,那么在函数A 回来前,回调函数会被履行。

异步回调,或许叫延迟回调。那么异步回调和同步回调不同的是,在调用完函数A 后,函数A 的调用会马上完结,主线程就开端完结其他使命,一段时刻后,回调函数开端履行,也便是说,在异步调用下,主线程和回调函数的履行或许在一起进行。一般状况下,主线程和回调函数的履行位于不同的线程或许进程中。

咱们在写项目时,经常会用到第三方库,咱们有时分就会以回调函数的办法运用第三方库:

咱们给第三方库指定回调函数,由于第三方库的编写方并不知道咱们要在某些特定节点应该实行什么操作,所以就无法针对具体完结来编写代码,所以会对外供给一个参数,而由咱们运用方来完结函数并把它作为参数传递给第三方库,第三方库只需求在特定的时刻节点去调用该回调函数即可

比方,在网络恳求接纳网络数据文件传输完结后这样的时刻节点,咱们期望能调用一段函数来处理或许告诉,这个时分回调函数就能够派上用场,因而,回调函数也适合于事情驱动型编程

计算机底层2 程序在运行时发生了什么

从图中能够看出,异步回调要比同步回调更能充分运用多核资源,同步回调会有一段“闲暇时刻”,而异步回调则是CPU 一直在干活,能够充分运用CPU 资源。

5 同步与异步

5.1 同步调用

同步调用是咱们比较了解的调用办法,比方:

void funcA() {
    ...
    funcB();
    ...
}

函数A 调用函数B,在函数B 完结前,函数A 后边的代码都不会履行。如图:

计算机底层2 程序在运行时发生了什么

一般来说,同步调用都运转在同一个线程中,可是有一些特别操作,比方I/O 操作,运用read来读取文件,这个时分,便是在内核的线程中履行读取文件操作的。当然,这也是同步调用,不过不属于同一个线程。

不过咱们也能够看出,同步调用尽管易于编程,可是并不高效,由于调用方需求等候。

5.2 异步调用

异步调用不会堵塞调用方,而且一般会开启新的线程。

可是在异步调用的状况下,咱们怎样才能得知履行成果而且处理成果呢?

这就分成了两种状况:

  1. 调用方不关怀调用成果
  2. 调用方需求知道履行成果

第一种状况的完结办法能够运用刚刚讲到的异步回调

计算机底层2 程序在运行时发生了什么

第二种状况的完结办法是运用告诉机制,也便是使命完结后,发送信号或许音讯来告诉调用方使命完结。

计算机底层2 程序在运行时发生了什么

6 堵塞与非堵塞

刚刚咱们说的同步与异步,是多使命处理的办法,也便是说,同步与异步不只呈现在计算机科学范畴,别的范畴比方通讯也有这个概念。

而咱们现在要说的堵塞与非堵塞,在编程语境中一般用在函数调用上。

6.1 堵塞式调用

假定现在有两个函数A 和B,函数A 调用函数B,当函数A 的线程因调用函数B被操作系统挂起暂停运转时,咱们就说函数B 的调用是堵塞式的。

计算机底层2 程序在运行时发生了什么

能够看到,堵塞式调用的关键在于线程或许进程被暂停运转

那么在什么状况下,会由于调用函数导致线程被操作系统暂停运转呢?

一般状况下,堵塞几乎都与I/O 操作有关。

6.2 堵塞与I/O 操作

以磁盘为例,咱们都知道磁盘寻道的I/O 恳求耗时比CPU 的作业频率要低得多,CPU 能在这个期间履行很多机器指令,假定是堵塞式I/O 恳求,那么CPU 时刻就被浪费了,因而,在该线程或许进程触及到I/O 操作时,就应该把CPU 时刻从该进程上拿走,去分配给其他能够运转的线程或许进程,当I/O 操作完结后,再将CPU 再次分配该线程或许进程,在此之前,该线程或许进程一直是被堵塞而停止运转的

计算机底层2 程序在运行时发生了什么

那有没有一种计划既能够建议I/O 操作,又不会导致调用线程被暂停运转呢?

非堵塞式调用能够。

6.3 非堵塞式调用

非堵塞式调用有几种办法能够完结:

  1. 运用成果查询函数 经过调用成果查询函数,咱们能够知道是否接纳到了数据
  2. 告诉机制 就像刚刚说到的异步调用,接纳到数据后,告诉调用的线程
  3. 回调函数 把收到数据后对数据的处理逻辑封装成回调函数,在被调用的线程中调用回调函数

6.4 同步与堵塞

刚刚咱们说过,同步异步是多使命处理的办法,而堵塞与非堵塞是函数调用的办法。

也便是说,同步不一定是堵塞的(可是堵塞一定是同步的)

同步不一定是堵塞的:有或许在一个线程中有funcA 函数,它同步调用函数B:

void funcA() {
    ...
    funcB();
    ...
}
计算机底层2 程序在运行时发生了什么

可是,这并不意味着funcA 所在的线程被堵塞而暂停运转,可是,假定一个函数是堵塞式调用的,那肯定是同步的。

6.5 异步与非堵塞

非堵塞不一定就意味着是异步的

举个比方,假定有这样一个函数funcR,是非堵塞调用的,用于获取网络数据,还有一个函数handle,用于处理funcR恳求的来的网络数据,还有一个函数check,用于检测funcR是否有网络数据传来。

假定要写一个异步非堵塞功用的代码,只需求创立两个线程,一个线程用于履行funcR函数来获取网络数据。另一个线程中运用一些事情驱动的机制,如回调函数、音讯行列、等,来等候网络数据的抵达,在有数据时调用handle函数来处理数据。在funcR函数来获取网络数据期间,该线程还能够做别的作业。

计算机底层2 程序在运行时发生了什么

可是事实上,非堵塞并不意味着是异步的

比方这样略微改动:仍是创立两个线程,一个线程用于履行funcR函数来获取网络数据。另一个线程中运用check函数来事情循环来检测funcR是否有网络数据传来,并在有数据时调用handle函数来处理数据。

也便是:

while (true) {
    funcR(res);  // 获取网络数据,调用后直接回来,不堵塞,获取后的数据放入res
    while (!check()) {  // 循环检测
        ...
    }
    handle(res);  // 处理网络成果
}

能够看到,咱们用了while循环不断检测究竟有没有网络数据传来,该线程中并没有履行其他使命,CPU 一直浪费在while循环中,这样的代码十分低效。实践上仍是同步,所以上面这段代码是同步非堵塞的。

所以,同步并不意味着堵塞,非堵塞也不意味着是异步的,要看具体的代码完结。同步与异步是多使命处理上的,堵塞与非堵塞是函数调用上的,堵塞与非堵塞的关键是要看线程或许进程是否被堵塞

7 高并发,高性能的服务器是怎样完结的

咱们现在学了进程,线程,协程,回调函数,同步,异步,堵塞,非堵塞,这些技能合理运用,能够让我完结高并发,高性能的服务器。

7.1 事情循环与事情驱动

到现在为止,说到“并行”,就会想到进程和线程,可是事实上,并行编程并不只要这两项技能,在服务器编程中,还有事情驱动型编程。

事情驱动编程技能需求两种东西:

  1. 事情(event),比方网络数据到来,文件是否可读
  2. 处理事情的函数(handler())

这个进程能够简略成:事情event 连绵不断到来,当事情到来后,检查一下事情的类型,并依据该类型找到对应的事情处理函数handle(),然后直接调用

计算机底层2 程序在运行时发生了什么

可是,咱们要需求处理两个问题:

  1. 事情来历问题
  2. 处理事情的handler 函数要不要和事情循环函数同一个线程?

7.2 事情来历与I/O 多路复用,多线程

一切皆文件,咱们的程序都是经过文件描述符来进行I/O 的,咱们应该怎样一起处理多个文件描述符呢,这就有了I/O 多路复用机制,比方Linux 中的epoll,也便是告诉epoll需求处理的一些文件描述符号,假定事情发生,就进行处理。

epoll是为事情循环而生的,I/O 多路复用技能就成为了事情循环的发动机,连绵不断地供给事情,这样,事情来历的问题就处理了。

计算机底层2 程序在运行时发生了什么

假定事情处理函数具有以下特色:

  1. 不触及I/O操作
  2. 处理函数比较简略,耗时少

那么这个时分咱们能够让事情循环函数和事情处理函数放在同一个线程中。这种状况下,恳求是串行处理的,那么假定处理用户恳求需求耗费很多的CPU 时刻呢?

这个时分,就应该采用多线程并行处理。

计算机底层2 程序在运行时发生了什么

那假定在处理恳求的进程中,一起触及I/O 操作,这儿就需求留意,在事情循环中,一定不能调用任何堵塞式接口,否则会导致事情循环线程被暂停履行,这个时分,事情循环这台发动机就熄火了,整个系统都不能持续向前推动,可是能够把堵塞式I/O 调用的使命交给作业线程,即使某个作业线程被堵塞,也不会阻碍其他作业线程

7.3 协程:以同步的办法进行异步编程

前面说过,协程最大的特色便是能够暂停与康复,而且协程挂起后,并不会堵塞作业线程当协程被挂起后,作业线程将转去履行其他准备就绪的协程,当完结恳求回来处理成果后,自动暂停的协程将再次具有可履行的条件,而且等候调度履行,此后该协程会在上一次挂起点持续运转下去

计算机底层2 程序在运行时发生了什么

所以添加协程后,服务器的事情循环环节接纳到恳求后,将handle办法封装成协程而且分发给各个作业线程,供他们调度履行,作业线程拿到线程后,开端履行其进口函数,也便是handler函数,当某个协程由于RPC恳求自动释放CPU 后,该作业线程将去找到下一个具有运转条件的协程,这样,在协程中建议堵塞式RPC 调用就不会堵塞作业线程,抵达高效运用CPU 资源的目的。

计算机底层2 程序在运行时发生了什么

CPU,线程,协程是在不同层面上的:CPU 履行机器指令驱动计算机运转,而线程是内核创立调度的,线程是CPU 资源调度的最小单位,而协程对内核来说是不行见的,内核是依照线程来分配CPU 时刻片的,在线程被分配到的时刻片内,程序员能够自行决定运转哪些协程

计算机底层2 程序在运行时发生了什么
计算机底层2 程序在运行时发生了什么

协程本质上是线程CPU时刻片在用户态的二次分配,因而,协程也被称为用户态线程。

8 虚拟化技能

在上一篇文章中,咱们说到笼统是计算机中十分重要的概念,内存虚拟化让每个进程都以为自己独占一整块内存,而CPU 也能够虚拟化,CPU 的虚拟化让每个进程以为自己独占CPU

虚拟化技能是一种计算机技能,它答应在一台物理计算机上创立多个虚拟环境,每个虚拟环境都能够运转独立的操作系统和应用程序。这些虚拟环境被称为虚拟机(VMs),它们是在物理硬件上的软件仿真,为用户供给了一种将多个虚拟计算机运转在同一台物理计算机上的办法。

这也便是CPU 以及操作系统被笼统成了虚拟机。

9 总结

在这一篇文章中,咱们了解到了操作系统,进程,线程,协程,回调函数,同步,异步,堵塞,非堵塞的概念,也因而了解到了高并发,高性能的服务器的基本架构

10 下一篇文章

计算机底层3 内存

11 参考资料

  • 陆小风.计算机底层的隐秘. 电子工业出版社, 2023.
  • 计算机底层的隐秘 gitbook