目录
推荐阅读《程序是怎样跑起来的》,感觉写得不错,没基础的可以读一下
本文参照了百度百科、网上内容、加上自己理解整理而成
一、程序是怎么跑起来的
1.1 什么叫程序
我们写的代码叫程序,这种是源代码只适合人类阅读的不能直接执行,我们称这种代码叫源代码,也称源程序。
当我们写的源代码,经过IDE工具“编译”之后,变成了适合电脑可阅读的二进制的可执行的文件,叫可执行程序。
PS:
IDE:集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具。集成了代码编写功能、分析功能、编译功能、调试功能等一体化的开发软件服务套。
编译:简单来说就是把人类阅读的源代码变成电脑能识别的二进制可执行程序的过过程。
二进制:就是只有0和1组成的,人类根本看不懂的。
1.2 程序的执行
程序是静态的,程序是一个没有生命的实体。一般放在硬盘上。要想让程序处理数据,完成计算任务,必须把程序从外部设备加载到内存中,并在操作系统的管理调度下交给 CPU 去执行,让其运行起来,才能真正发挥软件的作用,它才能成为一个活动的实体,我们称其为进程。
进程除了包含可执行的程序代码,还包括进程在运行期使用的内存堆空间、栈空间、供操作
系统管理用的数据结构。如下图所示:
二、一台电脑如何处理数以百计的操作
2.1 分时技术及进行切换
要说好这个问题,你得先看一下简单看一下百度百科的《操作系统发展史》,其中有一个分时系统,解释如下:
由于CPU速度不断提高和采用分时技术,一台计算机可同时连接多个用户终端,而每个用户可在自己的终端上联机使用计算机,好象自己独占机器一样。
分时技术:把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。
若某个作业在分配给它的时间片内不能完成其计算,则该作业暂时中断,把处理机让给另一作业使用,等待下一轮时再继续其运行。由于计算机速度很快,作业运行轮转得很快,给每个用户的印象是,好象他独占了一台计算机。而每个用户可以通过自己的终端向系统发出各种操作控制命令,在充分的人机交互情况下,完成作业的运行。
...
多用户分时系统是当今计算机操作系统中最普遍使用的一类操作系统。
我们主要看上面的分时技术,知道我们电脑能处理这么多操作,其实就是采用时间片来实现的。
从上面看我们之所以感觉到自己CPU可以同时处理很多情况,是因为采用了分时技术,因为CPU速度太快了,让我们感觉不出来。
电脑上有计以百计的进程都不能占着一个CPU吧,时间片了到就是要让出来给其它进程使用,那么就会产生进程切换。在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的 切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程 的上下文是存储在进程的私有堆栈中的。
这里我得说一下上下文的概念,尤其运维学开发的时候会发现上下文的概念出现的次数太多了!什么“servet上下文”、“spring 上下文”..直接就晕了
我们小学读书的时候,就说结合上下文(或语景)在横线填写适合的语句,读初中英文了也说结合上下文(或语境)的完型填写,或选择正确的单词。
个人理解:上下文粗暴点就是环境,有初始化信息、配置信息、
所谓的上下文就是指语境,每一段程序都有很多的外部变量。只有想Add这种简单的函数才是没有外部变量的。一旦写的一段程序中有了外部变量,这段程序就是不完整的,不能独立运行,要想让他运行,就必须把所有的外部变量的值一个一个的全部传进去,这些值的集合就叫上下文。
说的通俗一点就是一段程序的执行需要依赖于外部的一些环境(外部变量等等),如果没有这些外部环境,这段程序是运行不起来的。
子程序之于程序,进程之于操作系统,甚至app的一屏之于app,都是一个道理。
当程序执行了一部分,要跳转到其他的地方,而跳转到的地方需要之前程序的一些结果(包括但不限于外部变量,外部对象等等)。
app点击一个按钮进入一个新的界面,也要保存你是在那个屏幕跳过来的等等信息,以便你点击返回的时候能正确跳回
上面这些都是上下文的典型例子,所以把“上下文”理解为环境就可以了。(而且上下文虽然是上下文,但是程序里面一般都只有上文而已,只是叫的好听叫上下文。进程中断在操作系统中是有上有下的)。
所以说,通俗一点理解就是,当程序从一个位置调到另一个位置的时候,这个时候就叫上下文的切换(因为他要保存现场,各种的压栈,出栈等等),进程之间切换也叫上下文切换,因为也要保存现场,以便切换回之前的进程。
2.2 进度的三种状态
所以虽然从外部看起来,多个进程在同时运行,但是在实际物理上,进程并不总是在 CPU上运行的,一方面进程共享 CPU,所以需要等待 CPU 运行,另一方面,进程在执行 I/O操作的时候,也不需要 CPU 运行。进程在生命周期中,主要有三种状态,运行、就绪、阻塞。
1)就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪状态有时候也被称为等待运行状态。
2)运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
3)阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
三、线程
3.1 线程的出现
从上面看出,进程之间切换代码是很大的,还要一系列的“保存现场”操作,方便回切回来的时候能接着之前没完成的工作。
既然代替这么大,要不要让进程尽可能处理更多的操作呢,于是对进程再进行细分,产生了“线程”。
60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。
当下推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
进程:资源分配的基础单位。拥有一个完整的虚拟空间地址,并不依赖线程而独立存在。
线程:程序执行的基础单位。线程程独立于它们的父进程,竞争使用处理器资源
系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。所以说进程是资源分配的
对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。而一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。
PS:线程是进程的一部分, 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
3.2 进程与线程的区别
进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。
另外,进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。
与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。
线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其他线程的相关变量。
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。因而近年来推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
线程与进程的区别可以归纳为以下4点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程OS中,进程不是一个可执行的实体。
我们按照多个不同的维度,来看看多线程和多进程的对比:
对比维度 | 多进程 | 多线程 | 总结 |
数据共享、同步 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU利用率低 | 占用内存少,切换简单,CPU利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 线程占优 |
编程、调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会互相影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程占优 |
四、 子进程
4.1 什么是子进程
在使用linux的时候会见到fork制作,他使用的是子进程,所以得提一下
子进程指的是由另一进程(对应称之为父进程)所创建的进程。
子进程继承了对应的父进程的大部分属性,如文件描述符。在Unix中,子进程通常为系统调用fork的产物。在此情况下,子进程一开始就是父进程的副本,而在这之后,根据具体需要,子进程可以借助exec调用来链式加载另一程序。
4.2 子进程相关概念
4.2.1 孤儿进程
孤儿进程,顾名思义,子进程还在世的时候父进程却结束了。那么孤儿进程没了父进程,是不是就被孤立了呢?不会的,我们还需要了解到1号进程——init进程,在初始化unix系统的时候,会创建一个init进程。然后由init进程创建终端,而终端进程随着用户的接入,会启动更多的进程,以此类推。在这整个系统中,所有的进程都属于以init为根的一棵树。当某个父进程终止,子进程就会被init进程收养。在这些孤儿进程结束时,init进程会回收他们的退出信息,保证他们不一直成为僵尸进程。
4.2.2 子进程创建过程
在Unix,子进程是父进程的拷贝,其地址空间是父进程地址空间的副本,不可写的内存部分是共享的,例如程序代码。被修改的变量等通过写时复制进行修改。父子进程拥有相同的内存映像、打开的文件描述符,环境变量等。
子进程执行fork后的程序代码。所以multiprocessing里的进程创建后设置执行的内容,就是在fork之后,程序计数器值为fork 后一句的语句编号。
4.2.3 孤儿进程总结
父进程被终止,子进程转为孤儿进程, 结束整组进程可以杀死孤儿进程。或者等待子进程自己结束,结束后的数据回收由init进程接管,所以孤儿进程不会对系统造成过多问题。
ps:此时如果你用ctrl+c,是无法结束子进程的,因为他的终端已经成了1号进程,必须找到其进程号,kill 进程号,来结束。
4.2.3 僵尸进程
(有爹,爹不管)
父进程创建,由父进程创建子进程,当子进程退出以后,大部分的资源被释放,但是还是会有例如pid, 存在时间的记录等资源没有被释放。所以当子进程退出后,子进程会先变成僵尸进程,然后由父进程进行剩余的清理工作。当父进程没有对子进程进行清理工作的话,子进程就会维持僵尸进程的状态。
过多僵尸进程会 pid 不够用。。以及系统资源会一直被占用。
僵尸进程可以用 ps aux 这个命令来观察。
[root@linux ~]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 1740 540 ? S Jul25 0:01 init [3]
root 2 0.0 0.0 0 0 ? SN Jul25 0:00 [ksoftirqd/0]
root 3 0.0 0.0 0 0 ? S< Jul25 0:00 [events/0]
.....中間省略.....
root 5881 0.0 0.3 5212 1204 pts/0 S 10:22 0:00 su
root 5882 0.0 0.3 5396 1524 pts/0 S 10:22 0:00 bash
root 6142 0.0 0.2 4488 916 pts/0 R+ 11:45 0:00 ps aux
STAT:该程序目前的状态,主要的状态有:
R :该程序目前正在运作,或者是可被运作;
S :该程序目前正在睡眠当中(可说是idle 状态啦!),但可被某些讯号(signal) 唤醒。
T :该程序目前正在侦测或者是停止了;
Z :该程序应该已经终止,但是其父程序却无法正常的终止他,造成zombie (疆尸) 程序的状态
4.2.4 守护进程 (自动孤儿)
在linux里,守护进程其实就是服务对应的 默默的在后台跑着的程序。
一般来说 守护进程没有任何存在的父进程(即PPID=1),成为守护进程的方式是父进程创建完子进程以后,立即退出,由init接管子进程(碰瓷init, init内心也是崩溃的)。上述也叫脱壳。
总结: 子进程 vs 父进程
- 当父进程意外退出时,子进程会如何
子进程会变成孤儿进程,被init接管,子进程退出后的clean工作也由init进程完成。 - 当唯一的子进程退出时,父进程会如何
父进程清理子进程退出后的资源。如果父进程是阻塞等待的话,那么父进程会解除阻塞,继续执行。 - 当python multiprocessing 里以守护进程运行的时候,父进程退出,子进程会如何
会被kill
4.3 子进程与线程的区别
相同点:
- 二者都具有ID,一组寄存器,状态,优先级以及所要遵循的调度策略;
- 每个进程都有一个进程控制块,线程也拥有一个线程控制块;
- 线程和子进程共享父进程中的资源;线程和子进程独立于它们的父进程,竞争使用处理器资源;线程和子进程的创建者可以在线程和子进程上实行某些控制,比如,创建者可以取消、挂起、继续和修改线程和子进程的优先级;线程和子进程可以改变其属性并创建新的资源;
不同点:
- 线程是进程的一部分, 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的;
- 启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间;
- 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。而一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便;
- 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表中少多了;
- 进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线程而独立存在
举例而言,进程和线程的区别在于粒度不同, 进程之间的变量(或者说是内存)是不能直接互相访问的, 而线程可以, 线程一定会依附在某一个进程上执行.我举个例子, 你在Windows下开一个IE浏览器, 这个IE浏览器是一个进程. 你用浏览器去打开一个pdf, IE就去调用Acrobat去打开, 这时Acrobat是一个独立的进程, 就是IE的子进程.而IE自己本身同时用同一个进程开了2个网页, 并且同时在跑两个网页上的脚本, 这两个网页的执行就是IE自己通过两个线程实现的.值得注意的是, 线程仍然是IE的内容, 而子进程Acrobat严格来说就不属于IE了, 是另外一个程序.之所以是IE的子进程, 只是受IE调用而启动的而已.
Linux系统的实现打破了纯粹的进程与纯粹的线程之间的差异。在Linux系统下二者是本质一致的。
PS:windows下,当你设计一个应用程序时,你可能想使用一些需要长时间运行的代码,而又不中继当前正在进行的工作。一个方法是使用线程,将这个工作交由一个线程去执行,如果这些代码在运行过程中发生了错误,它可能会影响所在进程空间的所有线程。第二个方法是建立一个子进程,由这个子进程完成所需工作,这样子进程代码的错误不会影响到父进程的执行。子进程与父进程之间可以通过动态数据交换(DDE)、OLE、管道、邮件槽等进行通信,使用内存映射文件是最便利的方法之一。当前子进程终止后,子进程句柄变为有信号,父进程可使用 WaitForSingleObject 来等待子进程退出,这样父进程就可使用 GetExitCodeProcess 来获得子进程的退出码。