关于本文 本文是 Java 面试总结系列的第4️⃣篇文章,该专栏将整理和梳理笔者作为 Java 后端程序猿在日常工作以及面试中遇到的实际问题,通过这些问题的系统学习,也帮助笔者顺利拿到阿里、字节、华为、快手等Offer,也祝愿大家能够早日斩获自己心仪的Offer。由于笔者能力有限,如若有错误或疏忽还望各位大佬们不吝指出…
标题 地址 MySQL数据库面试题总结(2022版) Redis面试题总结(2022版) 计算机网络面试题总结(2022版) 操作系统面试题总结(2022版) Linux面试题总结(2022版) Spring面试题总结(2022版) Java基础面试题总结(2022版) Java集合面试题总结(2022版) Java并发面试题总结(2022版)从用户态切换到内核态的常见方法:
进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。通俗点讲,进程是一段程序的执行过程,是个动态概念。
僵尸进程 是子进程先于父进程退出后,子进程的 PCB 需要其父进程释放,但是父进程并没有释放子进程的 PCB,这样的子进程就称为僵尸进程。僵尸进程实际上是一个已经死掉但并未释放 PBC 的进程。
清楚了僵尸进程的定义,我们再来了解一下它的产生。
一个进程在调用 exit 命令结束自己的生命周期时,它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie) 的数据结构(系统调用 exit 的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。在 Linux 进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。这个僵尸进程需要它的父进程来为它收尸,如果他的父进程没有处理这个僵尸进程的措施,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。
如果有大量的僵尸进程驻在系统之中,必然消耗大量的系统资源。但是系统资源是有限的,因此当僵尸进程达到一定数目时,系统因缺乏资源而导致奔溃。所以在实际编程中,避免和防范僵尸进程的产生显得尤为重要。
1)父进程通过wait和waitpid等函数等待子进程结束;
2)使用signal函数为SIGCHLD安装handler;
3)父进程不关心子进程的结束,则交给内核处理;
4)fork两次,回收子进程,并将孙进程交给1号进程(initi进程)。
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(pid=1)所收养,并由init进程对它们完成状态收集工作。
子进程死亡需要父进程来处理,那么意味着正常的进程应该是子进程先于父进程死亡。当父进程先于子进程死亡时,子进程死亡时没父进程处理,这个死亡的子进程就是孤儿进程。
但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了。不会像僵尸进程那样占用 ID,损害运行系统。
有个形象的例子区分进程和线程:车间和工人或者高速路和车道。
进程/线程只在某个cpu核上运行,避免因切换带来的CPU的L1/L2 cache失效。
协程是用户态的轻量级线程,是一种比线程更加轻量级的存在,协程不被操作系统内核管理,完全由程序控制。
协程的优点
1)没有线程切换开销;
2)单线程无需对共享资源加锁机制。
其缺点也显而易见,就是无法利用多核资源。协程本质还是单线程,无法直接利用多核处理器,需要配合进程实现。
想知道死锁的产生,首先需要了解死锁的定义:多个进行相互等待对方资源,在得到所有资源继续运行之前,都不会释放自己已有的资源,这样造成了循环等待的现象,称为死锁。
每个资源要么已经分配给了一个进程,要么是可用的,只有这两种状态,资源不可以被共享使用,所以所谓的互斥是指:资源不共享,如果被使用,只能被一个进程使用。
已经得到资源的进程还能继续请求新的资源,所以个人觉得叫占有并请求也许更好理解。
当一个资源分配给了一个进程后,其它需要该资源的进程不能强制性获得该资源,除非该资源的当前占有者显示地释放该资源。
死锁发生时,系统中一定有由两个或两个以上的进程组成的一条环路,环路上的每个进程都在等待下一个进程所占有的资源。
破坏三个条件(互斥是非共享设备特性,无法更改)。
避免指的是在资源动态分配过程中防止系统进入不安全状态,如银行家算法、系统安全状态法。
资源分配图、死锁定理、死锁解除。
调度算法是指根据系统的资源分配策略所规定的资源分配算法。
基于优先级的时间片轮转抢占式调度。
1)管道 2)信号量(计数器) 3)信号(事件)
4)消息队列 5)共享内存 6)本地套接字
1)互斥量 2)信号量 3)事件
创建一个和当前进程映像一样的进程可以通过 fork() 系统调用。
成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。
最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。
PS:通过 pthread_create() 函数创建线程。
fork() 函数是创建一个新进程, 它的返回值分别是子进程中返回 0,父进程返回子进程的进程号 (PID)。我们就可以通过 fork()函数的返回值识别父子进程。
其实,并不是一个函数有两个返回值,而是调用 fork 函数后,在内存中一个进程被复制成两个进程,分别被 cpu 执行. 那么执行出来肯定就会有两个结果。
首先这三个函数都是 IO 多路复用的实现,IO多路复用就是多路 Socket 连接复用一个 IO 线程,即单个线程可以处理多个 IO 请求。其优点是系统不必创建和维护多进程/线程,从而大大减小了系统的开销。
虚拟地址是指由程序产生的与段相关的偏移地址;
物理地址是指加载到内存地址寄存器中的地址,内存单元的真正地址。
虚拟内存是操作系统提供的一种内存管理技术,使得程序认为拥有连续可用的内存空间,而实际被分配到不同的物理内存碎片中。
如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。
一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问。
内存管理可以简单的分为连续分配管理方式和非连续分配管理方式这两种。
连续分配管理方式是指为一个用户程序分配一个连续的内存空间,比如块式管理。非连续分配管理方式允许一个程序内存分散,比如页式管理、段式管理和段页式管理。
PS:此时可能会让动手实现 LRU 算法,两种思路:
1)双链表 + Hash;
2)LinkedHashMap。