进程
程序是一个静态的概念,它就是一些预先编译好的指令和数据集合的一个文件,一般程序都存储在硬盘当中。进程则是正在运行的程序的实例,是一个动态的概念,它描述了程序在运行时的各种状态,往往被加载到内存之中。而进程是不能使用资源的,进程调度的资源是由进程创建的线程使用,一个进程可以创建多个线程。
进程和线程的区别
- 地址空间和其他资源:进程间相互独立,同一进程的各线程间共享,两个进程之间的线程互不可见
- 通信:进程间通信(IPC)通过管道,信号量,共享内存,信息队列;线程可以直接通过进程的数据段来通信
- 在多线程OS中,进程不是可执行的实体
- 调度和切换:线程上下文切换比进程上下文切换快
为什么切换(同个进程内的)线程比进程快呢
上下文切换:内核重写启动一个被抢占的进程所需的状态,包括:通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种数据结构
虚拟内存和虚拟内存地址
在早期的计算机中,程序是直接运行在物理内存上的,也就是说,程序在运行时所访问的地址都是物理地址,这种情况下只要程序所需要的内存空间不超过物理内存的大小就不会有问题。但是大多数情况下我们必须同时运行多个程序这样必定会造成内存空间的重叠现象,并且程序去直接操作物理内存也是十分危险的,那么我们如何将计算机有限的物理内存分配给多个程序使用呢?
我们在这里加入了一个中间层,即使用一种间接的地址访问方法。我们把程序给出的地址看作是一种虚拟地址,然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这就多个程序可以同时运行且各个程序之间能够访问的物理内存区域不重叠,也杜绝了程序直接操作地址的现象,同时也提高物理地址的使用效率。这种呈现出比实际拥有的地址空间大得多的内存我们叫做虚拟内存。
虚拟内存:将数据映射到物理内存地址上,也就是地址空间映射,也就是页表
进程与虚拟地址
32位系统下每个进程都会分配4G的虚拟内存空间,而其实所有进程都共享着同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。每次访问内存空间的某个地址,都需要把地址翻译为实际物理内存地址。
这时我们需要一个东西它就是MMU(内存管理单元),它的主要作用就是完成地址的映射,也就是页表的建立、映射过程。页表就是用来记录进程中哪些内存地址上的数据在物理内存上以及它们所在的位置的一个结构。每个进程都有一个页表,当进程需要访问某个虚拟地址时,就会去访问页表,页表实现从页号到物理块号的地址映射。存储在硬盘上的可执行程序被操作系统装载为进程,放入虚拟内存空间。当需要访问内存空间的某个地址时,再通过MMU实现虚拟内存对物理地址的映射,达到访问物理地址的目的。
所以每个进程都有自己的私有的连续的虚拟内存地址空间,作用是把虚拟地址转换为物理地址,即查找页表。
页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB(translation Lookaside Buffer,我们不需要关心这个名字只需要知道TLB本质上就是一个cache,是用来加速页表查找的)。
由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。
进程间通信
-
管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
-
命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
-
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。Linux中可以使用kill -12 进程号,像当前进程发送信号,但前提是发送信号的进程要注册该信号。
example: OperateSignal operateSignalHandler = new OperateSignal(); Signal sig = new Signal("USR2"); Signal.handle(sig, operateSignalHandler);
-
消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺限。
-
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
-
内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。Java 中有类 MappedByteBuffer实现内存映射
-
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
-
套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
线程之间的共享资源以及独占资源
共享资源
- 进程申请的堆内存
- 进程打开的文件描述符
- 进程的全局数据(可用于线程之间通信)
- 进程ID、进程组ID
- 进程目录
- 信号处理器
独占资源
- 线程ID
同一进程中每个线程拥有唯一的线程ID。 - 寄存器组的值
由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线 程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。 - 线程堆栈
线程可以进行函数调用,必然会使用大函数堆栈。 - 错误返回码
线程执行出错时,必须明确是哪个线程出现何种错误,因此不同的线程应该拥有自己的错误返回码变量。 - 信号屏蔽码
由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。 - 线程的优先级
由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。