1. 什么是僵尸进程?
一般在有父子关系的进程中会存在。在子进程退出后,如果父进程没有调用wait()
等方式,子进程将不会主动退出,从而成为僵尸进程。
2. 什么是孤儿进程?
当父进程退出后,子进程还没有退出,这些子进程会成为孤儿进程。此时这些子进程将过继给init根进程,并由 init 进程对它们完成状态收集工作。
3. 进程有哪些调度算法?
- 先来先服务算法
- 优先级调度算法
- 时间片轮转算法
- 短作业优先算法
- 最短剩余时间优先算法
4. 进程有哪些通信方式?
- 管道:所谓的管道就是内核中的一串缓存,从管道的一端写入数据,就是缓存在了内核里,另一端读取,也是从内核中读取这段数据。管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;命名管道是双向的,可以实现本机任意两个进程通信。
- 消息队列:消息队列就是保存在内核中的消息链表,包括Posix消息队列和System V消息队列。
- 共享内存:就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写⼊的东西,另外的进程⻢上就能看到。共享内存是最快的 IPC 方式,
- 信号量:它本质上是一个整数计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
- 信号:信号可以理解成一种电报,发送方发送内容,指定接收进程,然后发出特定的软件中断,操作系统接到中断请求后,找到接收进程,通知接收进程处理信号。比如
kill -9 1050
就是一种信号。 - socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。
5. 线程有哪些实现方式?
内核态线程实现、⽤户态线程实现、混合线程实现。
现代操作系统基本都是将两种方式结合起来使用。用户态的执行系统负责进程内部线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换。即我们同时实现内核态和用户态线程管理。其中内核态线程数量较少,而用户态线程数量较多。每个内核态线程可以服务一个或多个用户态线程。
6. 什么是快表?
同样利用了局部性原理
,即在⼀段时间内,整个程序的执行仅限于程序中的某⼀部分。相应地,执行所访问的存储空间也局限于某个内存区域。
利用这⼀特性,把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了⼀个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。
7. 页面置换算法有哪些?
- 先进先出置换算法:
- 最佳页面置换算法:
- 最近最久未使用置换算法:
- 时钟页面置换算法:这个算法的思路是,把所有的页面都保存在⼀个类似钟面的环形链表中,⼀个表针指向最老的页面。当发生缺页中断时,算法首先检查表针指向的页面:如果它的访问位位是 0 就淘汰该页面,并把新的页面插入这个位置,然后把表针前移⼀个位置;如果访问位是 1 就清除访问位,并把表针前移⼀个位置,重复这个过程直到找到了⼀个访问位为 0 的页面为止;

- 最近未使用置换算法:
8. 硬链接和软链接有什么区别?
硬链接与原文件在磁盘上实际上指向同一个数据块,因此修改其中一个文件,另一个文件也会被修改。而软链接只是一个指向原文件的路径名,因此修改原文件不会影响软链接,软链接也可以指向其他路径的文件。
9. 介绍一下零拷贝技术?
在数据传输过程中,普通拷贝一般有四步,涉及到四次用户态和内核态之间的切换,开销较大。
所以为了减少用户态和核心态之间的切换以及内存拷贝的次数,使用了零拷贝技术。
![传统文件传输示意图-来源参考[3]](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/os-1e595664-6585-4d56-8939-08b7ce510218.png)
- mmap + write
本技术是将内核缓冲区里的数据“映射”到用户空间,这样,操作系统内核和用户空间就不需要再进行任何数据拷贝。
![mmap示意图-来源参考[3]](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/os-6dc49f9d-0bc3-4956-a650-7c7236f234a2.png)
- sendfile
在 Linux 内核版本 2.1 中,提供了⼀个专门发送文件的系统调⽤函数 sendfile() 。用这个函数代替read() 和 write() 两个函数,可以减少一次系统调用,也就是减少了两次上下文切换开销。
另外,可以把内核缓冲区内的数据直接拷贝到socket缓冲区,而不是先拷贝到用户态中。这样总共就只有两次线程上下文切换的开销。
![sendfile示意图-来源参考[3]](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/os-0b087b8a-8d51-4aad-898d-d99c38d36592.png)
在Kafka和Rocket中,都有这些零拷贝技术的运用。
10. 讲讲几种I/O模型?
I/O是面向流的,NIO是面向缓冲区的
内核利用文件描述符来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
- 阻塞I/O:阻塞I/O模型是最常见的I/O模型,它的特点是当应用程序进行I/O操作时,操作系统会一直等待I/O操作完成,直到返回结果,期间应用程序会被阻塞,无法进行其他操作。这种模型在面对高并发和大量I/O操作时可能导致性能下降。

- 非阻塞I/O:非阻塞I/O模型是一种优化阻塞I/O的方式,它的特点是当应用程序进行I/O操作时,操作系统不会一直等待I/O操作完成,而是立即返回一个错误码,表示操作正在进行中。这样应用程序可以不停地进行轮询,直到I/O操作完成。这种模型可以提高应用程序的响应速度和吞吐量。(多个调用可以同时执行,没好的话只是都返回错误码,提高吞吐量)

- 基于非阻塞的I/O复用:基于非阻塞I/O的多路复用是一种更高效的I/O模型,它的特点是通过一个系统调用同时监听多个文件描述符的读写状态,一旦有数据可读或可写,就会通知应用程序进行处理。这种模型避免了不必要的轮询,提高了应用程序的效率和性能。

无论是阻塞 I/O、还是非阻塞 I/O、非阻塞I/O多路复用,都是同步调用。因为它们在read调用时,内核将数据从内核空间拷贝到应用程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read调用就会在这个同步过程中等待比较长的时间。
异步I/O:真正的异步 I/O 是
内核数据准备好
和数据从内核态拷贝到⽤户态
这两个过程都不用等待。发起 aio_read 之后,就立即返回,内核自动将数据从内核空间拷贝到应用程序空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。

11. 详细讲讲I/O多路复用?
I/O多路复用,指的是一个进程维护多个socket,也就是多个连接复用一个进程/线程。
I/O多路复用有三种实现机制
- select
select执行过程如下:
假如进程A启动时,要监听的socket三个文件描述符为3、4、5,此时网卡还没有接收到有数据到达,进程A便会让出CPU,阻塞线程。此时select会将三个socket连接从用户态拷贝进内核态的等待队列。
等到有数据到达时,网卡通过中断信号告诉CPU,执行中断程序,中断程序做了以下几件事:
- 将数据写入到对应的socket数据连接
- 唤醒进程A,重新放入CPU的运行队列
数据到达后,select执行结束,文件描述符集合拷贝到用户态。

select缺点如下:
- 开销过大。刚开始时select会将文件描述符集从用户态拷贝到内核态,收到数据,select执行完成后又会将其从内核态拷贝到用户态。有两次用户态和内核态之间的上下文切换。(epoll优化为不拷贝)
- 每次调用select都需要在内核遍历传递进来的所有fd_set。进程被唤醒后,不知道哪些连接已就绪(收到了数据),需要遍历文件描述符集。(epoll优化为异步事件通知)
- select只返回就绪文件的个数,具体哪个可读还要遍历。(epoll优化为返回就绪的文件描述符)
- 同时能够监听的连接数太少,在编译内核时就确定并且无法修改,一般是 32 位操作系统是 1024,64 位是 2048。(poll、epoll 优化为适应链表方式)
- poll
poll主要解决的就是文件描述符集合数量限制的问题,它采用链表进行存储。
但是在使用过程中还是需要遍历整个链表获取哪些socket可读以及哪些socket就绪,时间复杂度太高。
- epoll
epoll解决了select的“性能开销大”和“文件描述符数量少”这两个缺点,是性能最高的多路复用实现方式,能支持的并发量也是最大。解决的方法具体如下:
通过维护一个红黑树结构存储所有待检测的文件描述符,在每次有新的socket连接时,通过函数调用将其加入红黑树内。而不是像select/poll一样每次都传入一个集合,减少了用户空间和内核之间的大量数据拷贝和内存分配。
epoll 使用事件驱动的机制,在内核态维护一个链表记录就绪事件,当socket有事件发生时,通过回调函数,内核会将其加入就绪事件列表。而不是进程唤醒后,每次去遍历去找哪个可读,然后标记。
当用户态需要时会调用epoll_wait()
函数,会返回有事件发生的文件描述符。而不是只返回就绪文件个数,然后一个个去遍历找哪个可读。
假设一个 work process 处理了 1000 个连接,但其中只有 10 个 IO 完成了,并可以继续往下执行,select/poll 的做法是遍历这 1000 个 FD(File Description,可以理解成每个建立了连接的一个标识),找到那 10 个就绪状态的,并把没做完的事情继续做完,这样检索的效率明显很低。所以 epoll 的做法是当这 10 个 IO 准备就绪时,通过系统的回调函数将 FD 放到一个专门的就绪列表中,这样系统只需要去找这个就绪列表就可以了,这大大提高了系统的响应效率。

可以看讲的较为细的文章:https://mp.weixin.qq.com/s/5xj42JPKG8o5T7hjXIKywg