关于后端:程序运行原理

56次阅读

共计 3544 个字符,预计需要花费 9 分钟才能阅读完成。

程序是如何运行起来的

软件被开发进去,是文本格式的代码,这些代码通常不能间接运行,须要应用编译器编译成操作系统或者虚拟机能够运行的代码,即可执行代码,它们都被存储在文件系统中。不论是文本格式的代码还是可执行的代码,都被称为 程序 ,程序是动态的,宁静地呆在磁盘上,什么也干不了。要想让程序处理数据,实现计算工作,必须把程序从外部设备加载到内存中,并在操作系统的治理调度下交给 CPU 去执行,去运行起来,能力真正施展软件的作用,程序运行起来当前,被称作 过程

过程除了蕴含可执行的程序代码,还包含过程在运行期应用的内存堆空间、栈空间、供操作系统治理用的数据结构。如下图所示:

操作系统把可执行代码加载到内存中,生成相应的数据结构和内存空间后,就从可执行代码的起始地位读取指令交给 CPU 程序执行。指令执行过程中,可能会遇到一条跳转指令,即 CPU 要执行的下一条指令不是内存中可执行代码程序的下一条指令。编程中应用的循环 for…,while…和 if…else…最初都被编译成跳转指令。

程序运行时如果须要创立数组等数据结构,操作系统就会在过程的 堆空间 申请一块相应的内存空间,并把这块内存的首地址信息记录在过程的栈中。堆是一块无序的内存空间,任何时候过程须要申请内存,都会从堆空间中调配,调配到的内存地址则记录在栈中。

栈是严格的一个后进先出的数据结构,同样由操作系统保护,次要用来记录函数外部的局部变量、堆空间调配的内存空间地址等。

咱们以如下代码示例,形容函数调用过程中,栈的操作过程:

void f(){int x = g(1);
  x++; // g 函数返回,以后堆栈顶部为 f 函数栈帧,在以后栈帧继续执行 f 函数的代码。}
int g(int x){return x + 1;}

每次函数调用,操作系统都会在栈中创立一个栈帧(stack frame)。正在执行的函数参数、局部变量、申请的内存地址等都在以后栈帧中,也就是堆栈的顶部栈帧中。如下图所示:

当 f 函数执行的时候,f 函数就在栈顶,栈帧中存储着 f 函数的局部变量,输出参数等等。当 f 函数调用 g 函数,以后执行函数就变成 g 函数,操作系统会为 g 函数创立一个栈帧并搁置在栈顶。当函数 g()调用完结,程序返回 f 函数,g 函数对应的栈帧出栈,顶部栈帧变又为 f 函数,继续执行 f 函数的代码,也就是说,真正执行的函数永远都在栈顶。而且因为栈帧是隔离的,所以不同函数能够定义雷同的变量而不会产生凌乱。

计算机如何同时解决数以百计的工作

咱们本人日常应用的 PC 计算机通常只是一核或者两核的 CPU,咱们部署应用程序的服务器尽管有更多的 CPU 外围,通常也不过几核或者几十核。然而咱们的 PC 计算机能够同时编程、听音乐,而且还能执行下载工作,而服务器则能够同时解决数以百计甚至数以千计的 并发 用户申请。

那么为什么一台计算机服务器能够同时解决数以百计,以千计的计算工作呢?这里次要依附的是操作系统的 CPU 分时共享技术。如果同时有很多个过程在执行,操作系统会将 CPU 的执行工夫分成很多份,过程依照某种策略轮流在 CPU 上运行。因为古代 CPU 的计算能力十分弱小,尽管每个过程都只被执行了很短一个工夫,然而在内部看来却如同是所有的过程都在同时执行,每个过程仿佛都独占一个 CPU 执行。

所以尽管从内部看起来,多个过程在同时运行,然而在理论物理上,过程并不总是在 CPU 上运行的,一方面过程共享 CPU,所以须要期待 CPU 运行,另一方面,过程在执行 I / O 操作的时候,也不须要 CPU 运行。过程在生命周期中,次要有三种状态,运行、就绪、阻塞。

  • 运行:当一个过程在 CPU 上运行时,则称该过程处于运行状态。处于运行状态的过程的数目小于等于 CPU 的数目。
  • 就绪:当一个过程取得了除 CPU 以外的所有所需资源,只有失去 CPU 即可运行,则称此过程处于就绪状态,就绪状态有时候也被称为期待运行状态。
  • 阻塞:也称为期待或睡眠状态,当一个过程正在期待某一事件产生(例如期待 I / O 实现,期待锁……)而临时进行运行,这时即便把 CPU 调配给过程也无奈运行,故称该过程处于阻塞状态。

不同过程轮流在 CPU 上执行,每次都要进行过程间 CPU 切换,代价是十分大的,实际上,每个用户申请对应的不是一个过程,而是一个线程。线程能够了解为轻量级的过程,在过程内创立,领有本人的线程栈,在 CPU 上进行线程切换的代价也更小。线程在运行时,和过程一样,也有三种次要状态,从逻辑上看,过程的次要概念都能够套用到线程上。咱们在进行服务器利用开发的时候,通常都是多线程开发,了解线程对咱们设计、开发软件更有价值。

零碎为什么会变慢,为什么会解体

当初的服务器软件零碎次要应用多线程技术实现多任务处理,实现对很多用户的并发申请解决。也就是咱们开发的应用程序通常以一个过程的形式在操作系统中启动,而后在过程中创立很多线程,每个线程解决一个用户申请。

以 Java 的 web 开发为例,仿佛咱们编程的时候通常并不需要本人创立和启动线程,那么咱们的程序是如何被多线程并发执行,同时解决多个用户申请的呢?理论中,启动多线程,为每个用户申请调配一个解决线程的工作是在 web 容器中实现的,比方罕用的 Tomcat 容器。

如下图所示:

Tomcat 启动多个线程,为每个用户申请调配一个线程,调用和申请 URL 门路绝对应的 Servlet(或者 Controller)代码,实现用户申请解决。而 Tomcat 则在 JVM 虚拟机过程中,JVM 虚拟机则被操作系统当做一个独立过程治理。真正实现最终计算的,是 CPU、内存等服务器硬件,操作系统将这些硬件进行分时(CPU)、分片(内存)治理,虚拟化成一个独享资源让 JVM 过程在其上运行。

以上就是一个 Java web 利用运行时的次要 架构 ,有时也被称作 架构过程视图 。须要留神的是,这里有个很多 web 开发者容易疏忽的事件,那就是 不论你是否无意识,你开发的 web 程序都是被多线程执行的,web 开发人造就是多线程开发

CPU 以线程为单位进行分时共享执行,能够设想代码被加载到内存空间后,有多个线程在这些代码上执行,这些线程从逻辑上看,是同时在运行的,每个线程有本人的线程栈,所有的线程栈都是齐全隔离的,也就是每个办法的参数和办法内的局部变量都是隔离的,一个线程无法访问到其余线程的栈内数据。

然而当某些代码批改内存堆里的数据的时候,如果有多个线程在同时执行,就可能会呈现同时批改数据的状况,比方,两个线程同时对一个堆中的数据执行 + 1 操作,最终这个数据只会被加一次,这就是人们常说的 线程平安 问题,实际上线程的后果应该是顺次加一,即最终的后果应该是 +2。

多个线程访问共享资源的这段代码被称为 临界区,解决线程平安问题的次要办法是应用锁,将临界区的代码加锁,只有取得锁的线程能力执行临界区代码,如下:

lock.lock();  // 线程取得锁
i++;  // 临界区代码,i 位于堆中
lock.unlock();  // 线程开释锁

如果以后线程执行到第一行,取得锁的代码的时候,锁曾经被其余线程获取并没有开释,那么这个线程就会进入阻塞状态,期待后面开释锁的线程将本人唤醒从新取得锁。

锁会引起线程阻塞,如果有很多线程同时在运行,那么就会呈现线程排队期待锁的状况,线程无奈并行执行,零碎响应速度就会变慢。此外 I / O 操作也会引起阻塞,对数据库连贯的获取也可能会引起阻塞。目前典型的 web 利用都是基于 RDBMS 关系数据库的,web 利用要想拜访数据库,必须取得数据库连贯,而受数据库资源限度,每个 web 利用能建设的数据库的连贯是无限的,如果并发线程数超过了连接数,那么就会有局部线程无奈取得连贯而进入阻塞,期待其余线程开释连贯后能力拜访数据库,并发的线程数越多,期待连贯的工夫也越多,从 web 请求者角度看,响应工夫变长,零碎变慢

被阻塞的线程越多,占据的系统资源也越多,这些被阻塞的线程既不能继续执行,也不能开释以后曾经占据的资源,在零碎中一边期待一边耗费资源,如果阻塞的线程数超过了某个系统资源的极限,就会导致系统宕机,利用解体

解决零碎因高并发而导致的响应变慢、利用解体的次要伎俩是应用 分布式系统架构 ,用更多的服务器形成一个集群,以便独特解决用户的并发申请,保障每台服务器的并发负载不会太高。此外必要时还须要在申请入口处进行 限流 ,减小零碎的并发申请数;在利用内进行业务 降级,减小线程的资源耗费。

总结

事实上,古代 CPU 和操作系统的设计远比这篇文章讲的要简单得多,然而根底原理大抵就是如此。为了让程序能很好地被执行,软件开发的时候要思考很多状况,为了让软件能更好地施展效力,须要在部署上进行布局和架构。软件是如何运行的,应该是软件工程师和架构师的常识,在设计开发软件的时候,应该时刻以常识去扫视本人的工作,保障软件开发在正确的方向上后退。

本文由 mdnice 多平台公布

正文完
 0