后端面试38讲_02_01丨程序运行原理程序是如何运行又是如何崩溃的
你好,我是李智慧。
今天呢是专栏正式更新的第一天,让我们从程序的运行一崩快开始说起。
我们都知道呢,软件的核心载体是程序代码,软件开发的主要工作产出也是代码。
但是代码呢被存储在磁盘上本身是没有任何价值的,软件要想实现价值,代码就必须运行起来。
那么代码是如何运行的呢?在运行中可能会出现什么问题呢?首先我们要知道程序是如何运行起来的。
软件被开发出来是文本格式的代码。
这些代码通常不能直接运行,需要使用编译器、编译成操作系统或者虚拟机可以运行的代码,文本格式的代码和可执行代码都被存储在文件系统中。
不管是文本格式的代码还是可执行的代码,都被称为程序。
但是呢程序是静态的,安静的,待在硬盘上什么也干不了。
要想让程序处理数据完成计算任务,就必须把程序从外部设备加载到内存中,并在操作系统的管理调度下交给CPU去执行、去运行起来,才能真正发挥软件的作用。
不是运行起来以后呢,就被称为进程。
进程呢除了包含可执行的程序代码,还包括进程在运行期使用的内存堆空间、占空间以及供操作系统管理用的数据结构。
操作系统呢把可执行代码加载到内存中,生成相应的数据结构和内存空间。
然后就从可执行代码的起始位置读取指令交给CPU顺序执行。
指令执行的时候啊,可能会遇到一条跳转指令。
比如说CPU要执行的下一条指令,不是内存中可执行代码顺序的下一条指令,我们在编程中使用的循环for啊while啊if else啊,最后都被编译成跳转指令。
程序运行的时候呢,如果需要创建数组等数据结构,操作系统就会在自己的堆空间申请一块相应的内存空间,并把这块内存空间的首地址记录在进程的栈中。
我们都知道堆是一块无序的数据结构,任何时候呢进程需要申请存储空间,都会从堆空间中分配,分配到的内存地址就记录在栈中了。
说起栈啊,栈是一个严格的后进先出的数据结构,输程的栈有操作系统维护,主要用来记录函数内部的局部变量堆空间分配的内存空间地址等等。
为了方便你理解,我在文稿中呢改了一代码,这段代码就描述述函函数调过过程中的的操作过程。
你可可以看一每次函函调调操操系系统,当f函函数创建一个战正正在执行的函数参数中。
战变中呢申申请的内存地址都在当前的战战中,也就是堆栈的顶部的战中,中文中中有张图图。
我解释一下下张图图内容。
当f函函执执行的时候,当函函数就在站点的战争中战争中呢,重储着f函数的局部变量输参参数等等。
F函数调用基函数的时候,当前执执函函数就变成了奇函数操作系统呢就会奇函数创创建个战战,并放置在战点正在函数基调,用结程序返回f函数数函函对应的战战出战点,顶部的战争又变成f函数了,继续执行f函数的代码。
也就是说呀,真正执行的函数永远在站点,而且因为战争是隔离的,不同函数可以定义相同的变量,也就不会发生混乱了。
我们平常用的PC计算机啊,通常是一核或者两核的CPU部署。
应用程序的服务器虽然有更多的CPU核心通常也不过几核或者几十核。
但是呢我们的PC计算机可以同时编程听音乐,还能够执行下载任务。
而服务器则可以同时处理数以百计,甚至数以千计的并发用户请求。
那么为什么一台计算机服务器可以同时处理这么多的计算任务呢?其实啊主要依靠的是操作系统的CPU分时共享技术。
如果同时有多个进程在执行,那么操作系统就会将CPU的执行时间分成很多份进程,按照某种策略轮留在CPU上运行。
现在CPU的计算能力实在是太强大。
虽然每个进程都被执行了很短的时间,但是外部看起来却好像所有的进程都在同时执行,每个进程似乎都独占一个CPU执行,那么呢很容易就可以理解。
虽然从外部看起来多个进程在同时运行,但是呢在实际物理上进程并不总是在CPU上运行。
一方面进程共享CPU,所以需要等待CPU运行。
另一方面呢,进程在执行IO操作的时候,也就是访问磁盘了。
读写网卡什么的时候也不需要CPU运行进程在生命周期中主要有三种状态,用行就绪、阻塞。
我来分别解释一下这三种状态。
当一个进程在CPU上运行的时候呢,这个进程就处于运行状态,处于运行状态的进程的数目啊要小于等于CPU的数目。
当一个进程获得了除CPU以外的一切所需资源之后,只要得到CPU就可以运行了。
这个时候啊,我们说这个进程处于就绪状态,就绪状态,有时候也被称为等待运行状态,阻塞也被成为等待或者睡眠状态。
当一个进程正在等待某一时件发生暂时停止运行的时候,就算把CPU分配给进程也无法运行,那么这个进程就处于阻塞状态。
我们要知道,不同进程轮留在CPU上执行,每次都要进行进程键的CPU切换,这个代价就很高了。
实际上啊,现代服务器应用程序中每个用户请求对应的不是一个进程,而是一个线程。
我们可以把线程理解为轻量级的进程,线程在进程内创建并拥有自己的线程站在CPU上进行线程切换的代价也更小。
线程在运行时和进程一样,也有三种主要状态。
从逻辑上看呢,进程的主要概念都可以套用到线程上。
我们在进行服务器应用开发的时候,通常都是多线程开发。
因此啊理解线程对于我们设计开发软件更有价值。
那么服务器程序为什么会变慢?又为什么会崩溃呢?现在的服务器软件系统呢主要使用多线程技术进行多任务计算,完成对很多用户的并发请求处理。
也就是说呀我们开发的应用程序通常以一个进程的方式在操作系统中启动,然后在进程中创建很多线程,每个线程处理一个用户请求。
我们以java的部开发为例,好像我们编程的时候并不需要自己创建和启动线程啊。
那么程序是如何被多线程并发执行,同时处理多个用户请求的呢?实际上呀,启动多线程为每个用户请求当配一个处理线程的工作是在web容器中执行的。
比如常用的tomcat容器。
Tomcat启动多个线程,为每个用户请求分配一个线程调用和请求UIL路径相应的sercat容码,完成用户请求操作。
而tom cat在GYM虚拟机进程中,GYM虚拟机被操作系统当做一个独立的进程管理,真正完成最终计算的是CPU内存。
这些服务器硬件、操作系统将这些硬件进行分时分片管理,虚拟化成一个个的独享资源,让JVM进程在提上运行。
以上就是一个java web应用运行时的主要架构,我们也叫做它架构过程思头。
这里需要注意一点啊,有很多web开发者容易忽略的事情。
那就不管是你是否有意识,你开发的web程序都是被多线程执行的。
Web开发天然就是多线程开发,CPU以线程为单位进行分时共享执行。
可以想象到呢,代码被加载到内存空间以后,有多个线程在这些代码上执行这些线程。
从逻辑上看呢,是同时在运行的。
每个线程有自己的线程栈,所有的线程栈都是完全隔离的。
也就是说啊每个方法的参数和方法内部的几乎变量都是隔离的一个线程无法访问到其他线程的栈类数据。
但是当某些代码修改到内存堆里的数据的时候,如果有多个线程在同时执行,就可能会出现同时修改数据的情况。
比如说啊两个线程,同时对一个堆中的数据执行加一操作,最终这个数据只会被加一次。
这就是人们常说的线程安全问题。
实际上呢线程的结果应该是一次加一,最终结果应该是加二多个县城。
访问共享资源的这段代码被称为临界区。
解决县城安全问题的主要方法是使用锁将临界区的代码加锁,只有获得锁的线程,才能够执行临界区的代码。
如果当前线程执行到第一行获得锁的代码的时候,锁已经被其他县程获取,并没有释放,那么这个线程就会进入阻塞状态,等待前面线程释放锁的时候将自己唤醒,获得锁并继续执行。
我们要知道,锁会引起线程阻塞,如果有很多线程同时在运行,那么就会出现线程排队等待锁的情况,线程无法并行执行,系统响应速度就会变慢。
另外呢IO操作也会引起阻塞,对数据库连接的获取也可能会引起阻塞。
目前典型的web应用呢都是基于RDBMS关系数据库的web应用。
要想访问数据库必须获得数据库连接。
而受数据库资源的限制,每个web应用能够建立的数据库连接呢都是有限的。
如果并发线程数超过了数据库的连接数,那么就会有部分线程无法获得连接,从而进入阻塞,等待其他线程释放连接后才能够访问数据库,并发的线程数越多,等待连接的时间也越多。
从web请求者角度看,响应的时间变长,系统变慢,被阻塞的线程越多,占据的系统资源也越多。
这些被阻塞的线程,既不能继续执行,也不能释放。
当前已经被占的资源在系统中一边等待,一边消耗资源。
这果阻塞的线程数超过了某个系统资源的极限,就会导致系统宕机。
应用崩溃解决系统因为高并发而导致的响应变慢。
应用崩溃的主要手段是使用分布式系统架构,应用更多的服务器构成一个集群,以便共同处理用户的并发请求,保证每台服务器的并发负载不会太高。
此外,必要的时候呢还需要在请求入口处进行限流。
此外,系统的并发请求数目,还有呢在应用内进行业务降级,减小现成的资源消耗。
有关高并发系统架构方案,我将在专栏的第三个模块中进一步探讨。
事实上呢,现代CPU和操作系统的设计远比这篇文章讲的要复杂的多。
但是基础的原理大致就是如此了。
为了让程序能够更好的被执行,软件开发的时候呢,要考虑很多情况。
为了让软件能够更好的发挥效能,需要在部署上进行规划和架构。
为了是如何运行的,应该是软件工程师和架构师的常识。
事设计你开发软件的时候,应该时刻从操作系统原理的常识去审视自己的工作,保证软件开发在正确的方向上进行现程安全的临界区需要依赖锁。
而锁的获取也必须要保证自己是现程安全的,也就是说不能够出现两个线程同时得到锁的情况。
那么锁是如何保证自己是现程安全的呢?或者说在操作系统以及CPU层面锁是如何实现的?你不妨思考一下这个问题,把你的思考写在下面的评论区里,我会和你一起交流,也要欢迎把这篇文章分享给你的朋友,或者同事一起交流吧。