程序是如何运行,又是如何崩溃的?
正如标题所言,今天我们来聊聊程序是如何执行的?以及又是如何崩溃的?我们哼哧哼哧写的代码并不是程序,本质上不过是一个文本文件。即便我们将我们写的代码通过编译生成的可执行文件,本质也没有变,最多不过是一个优点特殊的文本文件。如果要程序真正的运行起来,就需要操作系统将我们的可执行文件加载到内存中,除此之外,操作系统还会额外的创建堆空间,栈空间以及供操作系统管理需要的数据结构,比如PCB。完成了上述的过程,我们写的代码才能被叫做程序,也就是我们熟知的进程。一个进程占用的内存主要分为4个部分:
-
代码空间
-
堆空间
-
栈空间
-
相关的数据结构需要的空间
在程序运行的过程中,内存占用不断变化的空间有两个一个是堆空间,一个是栈空间。那么两者有何不同呢?
两者的共同点就是都是用来存储程序运行过程中产生的临时数据的,只不过数据的生命周期不同罢了。在堆空间中,数据的生命周期是可以跨函数的,一般用于共享的场景,关于堆内存的管理,不同的语言有不同的管理策略,比如,C语言对堆内存的管理完全交给程序员自己管理,Java对堆内存的管理是交由垃圾回收器管理。而栈空间的数据是无法跨函数的,可见范围只能在一个函数内部,典型的代表就是我们熟知的局部变量。针对栈空间的管理,是基于一定的规则的,栈这种数据结构有一个特点,先进后出。每一次函数调用,就会在栈空间中创建一个栈帧,这个栈帧里面存放的数据就是当前这个函数中用到的局部变量。当一个函数执行完毕,就会将对应的栈帧弹出。由于栈的特点,就保证了正在执行的程序的栈帧总是位于栈顶。
上述描述的不过是,进程静态的样子,接下来描述进程如何被操作系统调度。进程由三种状态:
-
就绪态
-
运行态
-
阻塞态
当进程处于就绪态时,意味着一切准备就绪,只需要等待操作系统的调度就可以执行。运行态就是进程正在使用CPU执行指令,阻塞态就是因为需要等待某些资源,可能是IO,可能是锁,而暂时不被操作系统调度,一旦等待的资源就绪,就会进入就绪态,这个时候就可以被操作系统调度。
不过现代的操作系统,调度单元可不是进程,而是线程,但是线程不过是共享了大部分数据的进程而已。也常常被别人称为轻量级进程。具体怎么回事呢?创建一个线程,其实就是创建一个进程,但是我们知道创建进程需要分配代码空间,堆空间,栈空间,以及相关的数据结构需要的空间。而线程做的事情是复用父进程的代码空间,堆空间,创建一个额外的栈空间,以及供操作系统管理的线程相关的数据结构 -- TCB。从这个角度看,我们的线程确实要比进程轻量一些。
讲完了程序是如何运行的,我们来看看程序会因为什么原因导致程序崩溃。主要有两个原因,一个原因是自己写的代码有BUG,一种就是计算机资源不够。当代码有Bug时,当CPU执行了这段有代码的Bug时,可能立即崩溃。也可能因为堆内存的管理不当,导致堆内存不断扩大,达到操作系统的限制,导致程序崩溃,这种情况叫做内存泄漏。计算机的资源总是有限的,比如内存只有那么多,当我们的程序运行过程中,使用的内存超过了限制,也会导致程序崩溃。
相信通过这部分比较浅显的介绍你一定知道了程序是如何运行的,以及程序是如何崩溃的了,但是这段描述不够细节,如果你需要更深入的学习,可以看看《程序员的自我修养 -- 链接、装载与库》