1 进程和线程
- 进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
- 线程:进程的一个执行单元,是CPU调度和分派的基本单位,也被称为称为轻量级进程。
关联:
- 一个进程可以由多个线程或单个线程组成
- 线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
- 二者均可并发执行。
区别:
- 地址空间:进程拥有独立的地址空间; 线程共享本进程的地址空间。
- 资源拥有: 进程是拥有系统资源的一个独立单位,而线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),和其他线程共享本进程的相关资源如内存、I/O、cpu等。
- 独立性: 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 系统开销:在进程切换时,涉及到整个当前进程CPU环境的保存环境的设置以及新被调度运行的CPU环境的设置,而线程切换只需保存和设置少量的寄存器的内容,并不涉及存储器管理方面的操作,可见,进程切换的开销也远大于线程切换的开销。
- 执行过程: 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
2 进程之间如何通信
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
IPC的方式通常有
- 管道(包括无名管道和命名管道)
- 消息队列
- 信号量
- 信号
- 消息队列
- 共享内存
- 套接字( socket )、
- 远程过程调用:RPC等。其中 套接字( socket )和远程过程调用:RPC支持不同主机上的两个进程IPC。
1、管道( pipe )
既可在程序中使用,也可在shell中使用。
管道是一种半双工的通信方式,数据只能单向流动。
管道的问题在于他们没有名字,只能在具有亲缘关系(父子进程间)的进程间使用。
扩展:
管道由pipe函数创建,提供一个单向数据流。当需要一个双向数据流时,我们必须创建两个管道,每个方向一个。这也就是全双工管道的实现原理:由两个半双工管道构成。
2、命名管道 (named pipe) :即FIFO
命名管道也是半双工的通信方式。提供单向数据流。
克服了管道没有名字的限制,因此允许无亲缘关系的进程间通信,解决了管道的上述问题。
扩展:
管道和FIFO都是使用通常的read和write函数访问。
FIFO由mkfifo函数创建。FIFO不同于管道的是,每个FIFO有一个路径名与之关联,从而允许无亲缘关系的进程访问同一FIFO。
FIFO的真正优势表现在服务器可以是一个长期运行的进程(如守护进程),而且与其客户可以无亲缘关系。
3、信号量( semophore )
主要作为进程间以及同一进程内不同线程之间的同步手段。
进程间通信处理同步互斥的机制。信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
4、信号 ( sinal )
是一种处理异步事件的方式。
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。除了用于进程外,还可以发送信号给进程本身。
信号和信号量是不同的,他们虽然都可用来实现同步和互斥,但前者是使用信号处理器来进行的,后者是使用P,V操作来实现的。
5、消息队列( message queue )
消息队列是消息的链表,包括Posix消息队列systemV消息队列。
有足够权限的进程都可以向队列中添加消息,有足够读权限的进程都可以读走队列中的消息。
消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
扩展:
消息队列具有随内核的持续性,这跟管道和FIFO不一样。当一个管道或FIFO的最后一次关闭发生时,仍在该管道或FIFO上的数据将被丢弃。
两种消息队列:都不使用真正的描述符,因此在消息队列上使用select或poll是困难的。两种消息队列:在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。
SystemV消息队列的问题之一是无法通知一个进程何时在某个队列中放置了一个消息,也就是无法窥探一个消息。而Posix消息队列允许异步事件通知,以告知何时有一个消息放置到了某个空消息队列中。
Posix消息队列缺失的主要特性是从队列中读出指定优先级消息的能力。
使用管道和FIFO时,为在两个方向上交换数据,需要两个IPC通道。而使用消息队列时单个队列就够用,由每个消息的类型来标识该消息是从客户到服务器还是从服务器到客户。
Posix消息队列消息链表的链表头中含有当前队列的两个属性:队列中允许的最大消息数和每个消息的最大大小。
Posix消息队列消息队列的2个限制:一个进程能同时拥有打开着的消息队列的最大数目;任意消息的最大优先级。
6、共享内存( shared memory )
是由一个进程创建,但多个进程都可以访问的同一块内存空间。是最快的可用IPC形式(因为共享内存区中的单个数据副本对于共享该内存的所有线程或进程都是可用的)。
是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同
步和通信。
一般IPC形式(管道、FIFO、消息队列)的问题在于,两个进程要交换信息,这些信息必须经由内核传递。而进程间共享内存时,交换数据就不再涉及内核。这些进程必须协调或同步对该共享内存区的使用。
将共享内存区用于客户-服务器文件复制:该共享内存区同时存在于客户和服务器的地址空间。
7、套接字( socket )
更为一般的进程间通信机制,可用于不同机器之间的进程间通信。
8、远程过程调用:RPC
构筑一个应用程序时,如果所有进程都在同一台主机,那么可以使用前面的各种IPC方式;如果进程不在同一个主机上,那就要求进程间使用某种形式的网络通信,RPC提供了这样一种工具,它属于隐式网络编程的范畴。
RPC是用于从一个系统(客户主机)上某个程序调用另一个系统上(服务器主机)某个函数的一种方法。
它的调用进程和被调用进程可在不同主机上执行,客户和服务器运行在不同主机上,而且过程调用中涉及网络I/O,对于程序员是透明的。
另外,RPC也可用于同一主机上的客户和服务器之间,因此也可认为是另一种形式的消息传递。
3 进程有哪几种状态?
就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源
运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数
阻塞状态: 进程等待某种条件,在条件满足之前无法执行
4 多个线程之间可以共享那些数据
- 进程代码段
- 进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、
- 进程打开的文件描述符
- 信号的处理器
- 进程的当前目录
- 进程用户ID与进程组ID。
5 多线程的锁,也是多线程的同步方式
- 互斥锁
- 自旋锁
- 信号量
- 读写锁
- 递归锁
互斥锁:
用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
自旋锁:
与互斥量类似,它不是通过休眠使进程阻塞,而是在获取锁之前一直处于循环检测保持者是否已经释放了锁(自旋)的状态。用在以下情况:锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。自旋锁与互斥锁的区别:线程在申请自旋锁的时候,线程不会被挂起,而是处于忙等的状态。
信号量:
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
读写锁(rwlock):
高级别锁,区分读和写,符合条件时允许多个线程访问对象。处于读锁操作时可以允许其他线程和本线程的读锁, 但不允许写锁, 处于写锁时则任何锁操作都会睡眠等待;常见的操作系统会在写锁等待时屏蔽后续的读锁操作以防写锁被无限孤立而等待,在操作系统不支持情况下可以用引用计数加写优先等待来用互斥锁实现。 读写锁适用于大量读少量写的环境,但由于其特殊的逻辑使得其效率相对普通的互斥锁和自旋锁要慢一个数量级;值得注意的一点是按POSIX标准在线程申请读锁并未释放前本线程申请写锁是成功的,但运行后的逻辑结果是无法预测
递归锁(recursivelock):
严格上讲递归锁只是互斥锁的一个特例,同样只能有一个线程访问该对象,但允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作; windows下的临界区默认是支持递归锁的,而linux下的互斥量则需要设置参数PTHREAD_MUTEX_RECURSIVE_NP,默认则是不支持
6 什么是死锁?死锁产生的条件?
在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。
死锁产生的四个条件(有一个条件不成立,则不会产生死锁)
- 互斥条件:一个资源一次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
- 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系
7 什么是缓冲区溢出?有什么危害?其原因是什么?
缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
危害有以下两点:
- 程序崩溃,导致拒绝额服务
- 跳转并且执行一段恶意代码
造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。
8 分页和分段有什么区别?
段是信息的逻辑单位,它是根据用户的需要划分的,因此段对用户是可见的 ;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。
段的大小不固定,有它所完成的功能决定;页大大小固定,由系统决定
段向用户提供二维地址空间;页向用户提供的是一维地址空间
段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制。
9 虚拟内存空间是什么,为什么要有虚拟内存空间。
虚拟地址空间:是指应用程序自己认为,自己所处的地址空间。它区别于物理地址空间。后者是真实存在的,比如电脑有一根8G的内存条,物理地址空间就是0~8Gb。CPU的MMU(内存管理单元)负责把虚拟地址转换成物理地址。
引入虚拟地址的好处:
- 程序员不再关心真实的物理内存空间是什么样的,理论上来说(64位CPU的虚拟内存为2^64),程序员有几乎无限大的虚拟内存空间可用,最后只要建立虚拟地址和物理地址的对应关系即可。
- 操作系统屏蔽了物理内存空间的细节,进程无法访问到操作系统禁止访问的物理地址,也不能访问到别的进程的地址空间,这大大增强了程序安全性。
- 由虚拟地址空间引申出来的分页(Paging)技术,大大提高了内存的使用效率。要想运行一个程序,不再需要把整个程序都放入内存中执行,我们只要保证将要执行的页在内存中即可,如果不存在则导致页错误。
10 操作系统内核态,用户态
- 内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
- 用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
为什么要有用户态和内核态
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 -- 用户态 和 内核态
用户态与内核态的切换
所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.
这时需要一个这样的机制: 用户态程序切换到内核态, 但是不能控制在内核态中执行的指令
这种机制叫系统调用, 在CPU中的实现称之为陷阱指令(Trap Instruction)
他们的工作流程如下:
- 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈(stack frame), 以此表明需要操作系统提供的服务.
- 用户态程序执行陷阱指令
- CPU切换到内核态, 并跳到位于内存指定位置的指令, 这些指令是操作系统的一部分, 他们具有内存保护, 不可被用户态程序访问
- 这些指令称之为陷阱(trap)或者系统调用处理器(system call handler). 他们会读取程序放入内存的数据参数, 并执行程序请求的服务
- 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果
11 栈和堆
栈: 由系统自动分配的内存区域, 由系统自动释放。例如,在函数中声明一个局部变量 int b; 系统自动在栈中为b开辟空间
堆: 需要程序员自己申请的内存区域,并指明大小,并且需要主动进行释放。 例如: 在c中malloc函数,如p1 = (char*)malloc(10);在C++中用new运算符如p2 = new char[10];
参考文章
https://www.jianshu.com/p/b2ff3117faf1
https://zhuanlan.zhihu.com/p/23755202
https://www.cnblogs.com/z-sm/p/6254103.html