1. 进程和线程的区别
- 概念
- 进程:
对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;- 线程:
进程的子任务,是CPU调度和分派的基本单位,实现进程内部的并发;
线程是操作系统可识别的最小执行和调度单位。
每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
- 区别
- 拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。- 调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。- 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。- 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
2. 什么是死锁
- 产生条件
- 互斥:
每个资源要么已经分配给了一个进程,要么就是可用的。- 占有和等待:
已经得到了某个资源的进程可以再请求新的资源。- 不可抢占:
已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。- 环路等待:
有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。- 解决策略
- 鸵鸟策略
当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,直接忽略死锁- 死锁预防
通过破坏死锁产生的四个必要条件中的一个或多个,以避免发生死锁。
- 破坏互斥:
不让资源被一个进程独占,可通过假脱机技术允许多个进程同时访问资源;- 破坏占有和等待:有两种方案
- 已拥有资源的进程不能再去请求其他资源。一种实现方法是要求进程在开始执行前请求需要的所有资源。
- 要求进程请求资源时,先暂时释放其当前拥有的所有资源,再尝试一次获取所需的全部资源。
- 破坏不可抢占:
有些资源可以通过虚拟化方式实现可抢占;- 破坏循环等待:有两种方案
- 一种方法是保证每个进程在任何时刻只能占用一个资源,如果要请求另一个资源,必须先释放第一个资源;
- 另一种方法是将所有资源进行统一编号,进程可以在任何时刻请求资源,但要求进程必须按照顺序请求资源。
- 死锁避免
它允许三个必要条件,但通过算法判断资源请求是否可能导致循环等待的形成并相应决策,来避免死锁点的产生。因此,其前提是知道当前资源使用的整体情况,以及申请资源线程本身所占有的资源细节。
- 线程启动拒绝:如果一个线程的请求会引发死锁,则不允许其启动。
- 资源分配拒绝:如果一个线程增加的资源请求会导致死锁,则不允许此申请。
3. 进程调度算法
- 批处理系统
保证吞吐量和周转时间(从提交到终止的时间)
- 先来先服务 first-come first-serverd(FCFS)
非抢占式的调度算法,按照请求的顺序进行调度。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。- 短作业优先 shortest job first(SJF)
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。- 最短剩余时间优先 shortest remaining time next(SRTN)
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
- 交互式系统
快速地进行响应
- 时间片轮转
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。- 优先级调度
为每个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。- 多级反馈队列
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
4. 时间片轮转算法
见第3题
5. 进程间的通信方式,进程和线程的通信
- 管道
- 消息队列
- 信号量
- 信号
- 共享内存
- 套接字SOCKET
- 线程间的同步方式(线程锁)
- 信号量
它只取自然数值,并且只支持两种操作:
P(SV): 如果信号量SV大于0,将它减一;如果SV值为0,则挂起该线程。
V(SV): 如果有其他进程因为等待SV而挂起,则唤醒,然后将SV+1;否则直接将SV+1。- 互斥量
互斥量又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步。当进入临界区 时,需要获得互斥锁并且加锁;当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。- 条件变量
条件变量,又称条件锁,用于在线程之间同步共享数据的值。条件变量提供一种线程间通信机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的一个/多个线程。
- 线程间通信
- 使用全局变量
主要由于多个线程可能更改全局变量,因此全局变量最好声明为volatile- 使用消息实现通信
在Windows程序设计中,每一个线程都可以拥有自己的消息队列(UI线程默认自带消息队列和消息循环,工作线程需要手动实现消息循环),因此可以采用消息进行线程间通信sendMessage,postMessage。- 使用事件CEvent类实现线程间通信
Event对象有两种状态:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。
6. 弱引用和虚引用的区别
【todo】
7. 堆和方法区的区别
堆和栈的区别
- 大小限制:
栈底的地址和栈的最大容量是系统预先规定好的(2M/1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间, stack overflow。因此,能从栈获得的空间较小。堆是用链表来存储的不连续内存区域,大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。- 申请效率:
栈由系统自动分配,速度较快。但程序员是无法控制的。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.- 存储内容:
栈存储返回地址,参数,局部变量。堆在这块内存空间中的首地址处记录本次分配的大小,具体内容由程序员安排。- 数据访问:
存储在堆中的对象是全局可以被访问的,然而栈内存不能被其他线程所访问,且遵循LIFO原则。- 生命周期:
栈内存的生命周期很短,而堆内存的生命周期从程序的运行开始到运行结束。
8. 什么时候会发生内存溢出
【todo】
9. linux如何查看cpu占用率
【todo】top
10. io多路复用了解吗
【todo放link】见其他文章
11. 深拷贝、浅拷贝
【todo放link】见其他文章
12. 锁的粒度,什么时候使用
【todo】
13. 共享锁、排它所
【todo】
14. Linux命令,查看打印日志;查看某端口被占用的进程;查看某文件夹下包含某个字符的文件有哪些;查看磁盘;查看CPU使用情况
【todo】
15. linux软连接
【todo】
16. linux修改个人权限
【todo】
17. 管道
【todo】
18. 协程
【todo】
19. linux修改文件名命令,杀死一个名为a的进程
【todo】
20. 内存泄露和内存溢出的区别
- 内存溢出和内存泄漏
- 内存溢出
指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误
- 原因:
内存中加载的数据量过于庞大,如一次从数据库取出过多数据 集合类中有对对象的引用,使用完后未清空,使得不能回收 代码中存在死循环或循环产生过多重复的对象实体 使用的第三方软件中的BUG 启动参数内存值设定的过小
- 内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
Java 内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。
- 场景:
- 静态集合类引起内存泄漏:静态成员的生命周期是整个程序运行期间。比如:Map 是在堆上动态分配的对象,正常情况下使用完毕后,会被 gc 回收。而如果 Map 被 static 修饰,且没有删除机制,静态成员是不会被回收的,所以会导致这个很大的 Map 一直停留在堆内存中。懒初始化 static 变量,且尽量避免使用。
- 当集合里面的对象属性被修改后,再调用 remove()方法时不起作用:修改 hashset 中对象的参数值,且参数是计算哈希值的字段。当一个对象被存储进 HashSet 集合中以后, 就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初 存储进 HashSet 集合中时的哈希值就不同了。
- 各种连接对象( IO 流对象、数据库连接对象、网络连接对象)使用后未关闭:因为每个流 在操作系统层面都对应了打开的文件句柄,流没有关闭,会导致操作系统的文件句柄一 直处于打开状态,而jvm会消耗内存来跟踪操作系统打开的文件句柄。
- 监听器的使用:在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。
不正确使用单例模式是引起内存泄漏:单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。
- 解决措施
- 尽量减少使用静态变量,类的静态变量的生命周期和类同步的。
- 声明对象引用之前,明确内存对象的有效作用域,尽量减小对象的作用域,将类的成员 变量改写为方法内的局部变量;
- 减少长生命周期的对象持有短生命周期的引用;
- 使用 StringBuilder 和 StringBuffer 进行字符串连接,Sting 和 StringBuilder 以及 StringBuffer 等都可以代表字符串,其中 String 字符串代表的是不可变的字符串,后两者表示 可变的字符串。如果使用多个 String 对象进行字符串连接运算,在运行时可能产生大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。
- 对于不需要使用的对象手动设置 null 值,不管 GC 何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象;
- 各种连接(数据库连接,网络连接,IO 连接)操作,务必显示调用 close 关闭。
21. 进程的死锁
【todo】
22. 生产者消费者模型怎么解决生产速度过快的问题
【todo】
23. 为什么线程比进程要快
【todo】
24. 操作系统的多进程和多线程
【todo】
25. 进程、线程相关(切换,内存空间、调度,创建销毁)
进程状态转换
- 5个基本状态
创建状态:进程正在被创建。
就绪状态:进程被加入到就绪队列中等待CPU调度运行。
执行状态:进程正在被运行。
等待阻塞状态:进程因为某种原因,比如等待I/O,等待设备,而暂时不能运行。
终止状态:进程运行完毕。- 交换技术
当多个进程竞争内存资源时,会造成内存资源紧张,并且,如果此时没有就绪进程,处理机会空闲,I/0速度比处理机速度慢得多,可能出现全部进程阻塞等待I/O。
- 交换技术:换出一部分进程到外存,腾出内存空间。
在交换技术上,将内存暂时不能运行的进程,或者暂时不用的数据和程序,换出到外存,来腾出足够的内存空间,把已经具备运行条件的进程,或进程所需的数据和程序换入到内存。从而出现了进程的挂起状态:进程被交换到外存,进程状态就成为了挂起状态。
- 虚拟存储技术:每个进程只能装入一部分程序和数据。
- 进程的内存空间
进程的内存模型
26. 在一个文件中查找某个字符在哪一行
【todo】
27. 僵尸进程是什么
- 进程关系
进程通常不是凭空独立的出现的,在类Unix系统中,所有的其他进程都是从 进程0 fork 出来的,每个进程都会拥有多个子进程。
- fork()
- fork给父进程返回子进程pid,给其拷贝出来的子进程返回0,
- 这也是他的特点之一,一次调用,两次返回。
- 实质是在子进程的栈中构造好数据后,子进程从栈中获取到的返回值。
由于现代操作系统的写时复制机制,即使我们知道每个进程都拥有自己独立的地址空间,其实子进程指向的物理内存是和父进程相同的(代码段,数据段,堆栈都指向父亲的物理空间),只有子进程修改了其中的某个值时(通常会先调度运行子进程),才会给子进程分配新的物理内存,并根据情况把新的值或原来的值复制给子进程的内存。
- 孤儿进程
- 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。
- 孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
- 由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
- 僵尸进程
- 一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。
- 僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。
- 系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
- 要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。