操作系统与 Linux

1. 进程、线程和协程

  1. 线程是进程的子集,一个进程中可以包含多个线程,每条线程执行不同的任务,协程是线程的抽象单位,减少了线程切换过程中的资源代价。

  2. 进程和线程切换时,需要切换进程和线程的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,耗费资源较大,效率要差一些。

  3. 不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。

  4. 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈。

  5. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  6. 协程是用户态轻量级线程,Go 语言中在函数前加上 go 关键字就能实现并发。一个 goroutine 会以一个很小的栈启动 2KB 或 4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个 goroutine 同时启动。

2. 进程通信的手段

由于每个进程的用户空间都是独立的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享一个内核空间。

  1. 管道

管道分为「匿名管道」和「命名管道」。

匿名管道没有名字标识,是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道。匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道需要在文件系统创建一个类型为 p 的设备文件,毫无关系的进程可以通过这个设备文件进行通信。

不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则。

  1. 消息队列

消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程,附件也有大小限制。

  1. 共享内存

共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度。

  1. 信号量

共享内存通信中,多进程竞争同个共享资源会造成数据的错乱。需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。

  1. 信号

信号是唯一的异步通信机制,可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。

  1. Socket

如果要与不同主机的进程间通信,需要 Socket 通信。它不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

线程通信间的方式呢?

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题。

3. 线程数据如何同步(互斥与同步的实现与使用)

任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。

分为忙等待锁(自旋锁)和无忙等待锁,其中忙等待锁不能在单核 CPU 中使用。

  1. 信号量

通常信号量表示资源的数量,对应的变量是一个整型变量(sem)。有两个原子操作的系统调用函数来控制信号量的,分别是:

  • P 操作:将 sem 减 1,相减后,如果 sem < 0,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞;

  • V 操作:将 sem 加 1,相加后,如果 sem <= 0,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞;

互斥

  • 如果互斥信号量为 1,表示没有线程进入临界区;

  • 如果互斥信号量为 0,表示有一个线程进入临界区;

  • 如果互斥信号量为 -1,表示一个线程进入临界区,另一个线程等待进入。

通过互斥信号量的方式,就能保证临界区任何时刻只有一个线程在执行,就达到了互斥的效果。

同步

同步的方式是设置一个信号量,其初值为 0。

4. 死锁和如何避免死锁

死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件

  • 持有并等待条件

  • 不可剥夺条件

  • 环路等待条件

所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。

线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。

5. 操作系统是如何管理虚拟地址与物理地址之间的关系?

主要有两种方式,分别是内存分段和内存分页。对于虚拟地址与物理地址的映射关系,可以有分段和分页的方式,同时两者结合。

内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致外部内存碎片和内存交换效率低的问题。

于是,就出现了内存分页,把虚拟空间和物理空间分成大小固定的页,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片,解决了内存分段的外部内存碎片问题。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。

为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。

6. 虚拟内存有什么用

  • 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经到的常使用内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。

  • 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。

7. 内存满了,会发生什么?

如果空闲物理内存不够,那么就会进行内存回收的工作,主要有两种方式:

  • 后台内存回收:在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。

  • 直接内存回收:如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。

可被回收的内存类型有文件页和匿名页:

  • 文件页的回收:对于干净页是直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘 I/O 的,这个操作是会影响系统性能的。

  • 匿名页的回收:如果开启了 Swap 机制,那么 Swap 机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换入到内存中,这个操作是会影响系统性能的。

文件页和匿名页的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。

8. 在 4GB 物理内存的机器上,申请 8G 内存会怎么样?

在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。

在 64 位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:

  • 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);

  • 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;

9. 如何避免预读失效和缓存污染的问题?

为了避免「预读失效」造成的影响,Linux 和 MySQL 对传统的 LRU 链表做了改进:

  • Linux 操作系统实现两个了 LRU 链表:活跃 LRU 链表(active list)和非活跃 LRU 链表(inactive list)。

  • MySQL Innodb 存储引擎是在一个 LRU 链表上划分来 2 个区域:young 区域 和 old 区域。

为了避免「缓存污染」造成的影响,Linux 操作系统和 MySQL Innodb 存储引擎分别提高了升级为热点数据的门槛:

  • Linux 操作系统:在内存页被访问第二次的时候,才将页从 inactive list 升级到 active list 里。

  • MySQL Innodb:在内存页被访问第二次的时候,并不会马上将该页从 old 区域升级到 young 区域,因为还要进行停留在 old 区域的时间判断:

    • 如果第二次的访问时间与第一次访问的时间在 1 秒内(默认值),那么该页就不会被从 old 区域升级到 young 区域;

    • 如果第二次的访问时间与第一次访问的时间超过 1 秒,那么该页就会从 old 区域升级到 young 区域;

通过提高了进入 active list (或者 young 区域)的门槛后,就很好了避免缓存污染带来的影响。

10. 操作系统的锁机制

自旋锁

自旋锁是一种忙等待锁,主要用于短时间的临界区。其实现方式是通过不断循环检查锁的状态,直到获得锁为止。在 Linux 中,自旋锁的实现通常涉及对一个锁变量的原子操作,具体过程为:

  1. 线程在尝试获取锁时,会不断地对锁变量进行减 1 操作。

  2. 当锁变量的值为 1 时,表示锁可用,线程可以成功获取锁。

  3. 释放锁时,只需将锁变量的值设置为 0 即可。

自旋锁的优点是简单且开销小,但由于其忙等待的特性,长时间持有锁会导致 CPU 资源的浪费,因此适合于临界区执行时间较短的情况。

互斥锁

互斥锁是一种睡眠等待型锁,适用于需要长时间持有的临界区。其底层实现主要依赖于 futex(fast user-space mutex),一个用户态和内核态混合的同步机制。互斥锁的实现步骤如下:

  1. 当线程调用 pthread_mutex_lock 时,如果锁已被其他线程持有,当前线程将进入休眠状态,释放 CPU 资源。

  2. 当锁被释放时,内核会唤醒等待该锁的线程,使其重新竞争锁。

  3. 互斥锁的状态通过原子操作进行管理,确保在多线程环境下的安全性。

这种机制确保了在多线程环境中对共享资源的独占性访问,有效避免了竞态条件。

信号量

信号量是一种用于控制对共享资源访问的同步机制,通常用于进程间的同步。其底层实现依赖于内核的消息传递机制。信号量的基本操作包括:

  1. sem_wait:当信号量的值大于 0 时,减 1 并继续执行;当值为 0 时,线程进入等待状态。

  2. sem_post:增加信号量的值并唤醒等待的线程。

信号量的实现使得多个线程或进程能够安全地访问共享资源,而不必担心数据不一致的问题。

11. 一个进程最多可以创建多少个线程?

  • 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。

  • 64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

12. 线程崩溃了,进程也会崩溃吗?

各个线程的地址空间是共享的,既然是共享,那么某个线程对地址的非法访问就会导致内存的不确定性,进而可能会影响到其他线程,这种操作是危险的,操作系统会认为这很可能导致一系列严重的后果,于是干脆让整个进程崩溃。

但如果进程觉得罪不致死,那么它也可以选择自定义一个信号处理函数,这样的话它就可以做一些自定义的逻辑,比如记录 crash 信息等。

13. 调度算法合集

进程调度

系统调度需要考虑的因素(原因)

  1. CPU利用率:IO 请求阻塞时,CPU 要从就绪队列运行一个进程

  2. 吞吐率:单位时间完成进程数

  3. 等待时间:就绪队列中进程的等待时间

  4. 响应时间:对于交互性应用(鼠标键盘)所考虑

页面置换

将页面从磁盘 调入 物理内存

14. 零拷贝

为了提高文件传输的性能,出现了零拷贝技术,它通过一次系统调用(sendfile 方法)合并了磁盘读取与网络发送两个操作,降低了上下文切换次数。另外,拷贝数据都是发生在内核中的,天然就降低了数据拷贝的次数。

零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能,同时,为了解决机械硬盘寻址慢的问题,它还协助 I/O 调度算法实现了 IO 合并与预读,这也是顺序读比随机读性能好的原因。这些优势,进一步提升了零拷贝的性能。

需要注意的是,零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送。

  1. 减少了 CPU 拷贝数据的次数,提高了文件传输的性能

  2. 减少了上下文切换的次数,降低了 CPU 的开销

  3. 利用 DMA 技术,CPU 不需要参与数据拷贝,可以做其他的事情

15. 怎么传输大文件

当传输大文件时,不能使用零拷贝,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,并且大文件的缓存命中率不高,这时就需要使用「异步 IO + 直接 IO 」的方式。

16. I/O 多路复用

为了解决 C10K 问题,出现了 I/O 的多路复用,可以只在一个进程里处理多个文件的 I/O,Linux 下有三种提供 I/O 多路复用的 API,分别是:select、poll、epoll。

select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。

在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。

很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。

epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。

  • epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。

  • epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。

而且,epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。

17. 负载均衡与一致性哈希

哈希算法虽然能建立数据和节点的映射关系,但是每次在节点数量发生变化的时候,最坏情况下所有数据都需要迁移,这样太麻烦了,所以不适用节点数量变化的场景。

为了减少迁移的数据量,就出现了一致性哈希算法。

一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响。

但是一致性哈希算法不能够均匀的分布节点,会出现大量请求都集中在一个节点的情况,在这种情况下进行容灾与扩容时,容易出现雪崩的连锁反应。

为了解决一致性哈希算法不能够均匀的分布节点的问题,就需要引入虚拟节点,对一个真实节点做多个副本。不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。

18. 进程写文件时,进程发生了崩溃,已写入的数据会丢失吗?

不会,因为进程在执行 write (使用缓冲 IO)系统调用的时候,实际上是将文件数据写到了内核的 PageCache,它是文件系统中用于缓存文件数据的缓冲,所以即使进程崩溃了,文件数据还是保留在内核的 PageCache,我们读数据的时候,也是从内核的 PageCache 读取,因此还是依然读的进程崩溃前写入的数据。

内核会找个合适的时机,将 PageCache 中的数据持久化到磁盘。但是如果 PageCache 里的文件数据,在持久化到磁盘化到磁盘之前,系统发生了崩溃,那这部分数据就会丢失了。

19. 进程内存结构

内核区域

栈(从上到下分配)

文件映射匿名内存区(动态库、共享内存,从低地址开始向上增长)

堆内存(从下到上分配)

BSS 段(包括未初始化的静态变量和全局变量)

数据段(包括已初始化的静态常量和全局变量)

代码段(包括二进制可执行代码)

20. Linux fork 和 exec 的区别

fork 主要是 Linux 用来建立新的进程(线程)而设计的,exec 系列函数则是用来用指定的程序替换当前进程的全部内容。

21. 内核态和用户态的区别

  • 用户态是指程序在执行时,所处的特权级较低,只能访问自己的内存空间和 CPU 寄存器,不能直接访问操作系统的资源。

  • 内核态是指程序在执行时,所处的特权级较高,可以访问所有的内存空间和 CPU 寄存器,可以直接访问操作系统的资源。

22. epoll 的边沿触发与水平触发

  • 水平触发是指只要缓冲区中还有数据可读或可写,就会不断地通知应用程序

  • 边缘触发是指只有在缓冲区状态发生变化时才会通知应用程序,这样可以减少不必要的事件通知

在水平触发模式下,如果文件描述符已经就绪可以非阻塞的执行 IO 操作了,此时会触发通知。允许在任意时刻重复检测 IO 的状态。select 和 poll 就属于水平触发。而在边缘触发模式下,如果文件描述符自上次状态改变后有新的 IO 活动到来,此时会触发通知。在收到通知后,应用程序需要尽可能多地读取或写入数据,直到返回 EAGAIN 错误为止。

23. CPU 缓存

CPU 缓存是位于 CPU 与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。CPU 缓存分为 3 个部分: L1、L2、L3。其中,L1 (一般 32KB,一个缓存行,cacheLine 一般是 64Byte)、L2(一般 256KB) 是 CPU 独享的,不和其他 CPU 的缓存数据共享,而 L3 (一般 2MB)缓存是所有 CPU 共享的。

24. 什么情况下从用户态陷入内核态

  1. 系统调用:这是用户程序主动请求切换到内核态的方式。用户程序通过系统调用接口请求操作系统执行特权操作,如文件读写、进程管理等。系统调用的实现通常涉及软中断机制。

  2. 异常:当 CPU 在执行用户态程序时发生异常(如缺页异常、非法操作码等),CPU 会自动切换到内核态,以便操作系统处理这些异常情况。这种切换是自动的,不需要用户程序的干预。

  3. 中断:外部设备发生中断事件时,CPU 也会自动切换到内核态,以便操作系统处理这些中断。中断可以是来自硬件设备(如键盘、鼠标等)的信号,操作系统会根据中断类型进行相应的处理。

25. 用户态向内核态切换时的资源消耗主要是什么资源

当用户态进程需要访问内核态资源时,就会发生用户态到内核态的切换。这个切换过程需要保存用户态的现场,包括寄存器、用户栈等,同时需要复制用户态参数,将用户栈切换到内核栈,进入内核态。这个过程会消耗一定的资源,包括 CPU 时间、内存空间等。

26. 为什么内核态的上下文切换开销会大?

因为在内核态下,需要保存和恢复更多的寄存器和状态信息,而这些操作都需要耗费时间。此外,内核态的上下文切换还需要进行内存映射和页表切换等操作,这些操作也会增加开销。

27. 从本地读取一个文件通过网络发送到另一端,中间涉及几次拷贝?

  1. 第一次拷贝:操作系统从磁盘读取文件数据到内核缓冲区。这一步通常通过文件系统来完成。

  2. 第二次拷贝:应用程序从内核缓冲区中获取数据并拷贝到应用程序的缓冲区。这一步通常通过系统调用(如 read() 或 recv())来完成。

  3. 第三次拷贝:应用程序将数据从其缓冲区写入到内核的网络堆栈缓冲区。这一步通常通过系统调用(如 write() 或 send())来完成。

  4. 第四次拷贝:网络堆栈将数据从其缓冲区发送到网络接口(如网卡)。这一步为驱动程序负责。

28. 如何优化磁盘 I/O

  • 在顺序读比较多的场景中,可以增大磁盘的预读数据。

  • 可以对应用程序的数据,进行磁盘级别的隔离。比如可以为日志、数据库等 I/O 压力比较重的应用,配置单独的磁盘。

  • 可以使用缓存 IO,充分利用系统缓存,降低实际 IO 的次数。

29. 线程通信的方式

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量

30. socket 可读的情况

  • socket 接收缓冲区中已经接收的数据的字节数大于等于 socket 接收缓冲区低潮限度的当前值。

  • 连接的读一半关闭 (即接收到对方发过来的 FIN 的 TCP 连接),并且返回 0。

  • socket 收到了对方的 connect 请求已经完成的连接数为非 0。这样的 socket 处于可读状态。

31. 查看一个文件最新更新的 100 行怎么做

查看文件 100 行到 200 行:

head -n 200 filename | tail -n 100

如果您只想查看文件的最后 100 行,可以使用以下命令:

tail -n 100 filename

32. 改变文件夹所有权

chown -R

33. 当一个服务器 CPU 打满了如何排查

  • 使用 top 命令查看,是否有进程占用 CPU 过高。可以按 shift+p 按照 CPU 排序,找到占用 CPU 过高的进程的 PID。

  • 使用 top -H -p [进程 ID] 找到进程中消耗资源最高的线程的 ID。

34. 当一个服务器内存打满了如何排查

  • 使用 top 命令查看,是否有进程占用内存过高。可以按 shift+m 按照内存排序,找到占用内存过高的进程的 PID。

  • 使用 top -H -m [进程 ID] 找到进程中消耗资源最高的线程的 ID。

35. 文件权限 755

7 代表文件所有者的权限,5 代表组用户和其他用户的权限。具体来说,755 权限将读、写、执行权限分配给文件所有者,将读、执行权限分配给组用户和其他用户

36. Linux 中,如何查看系统负载?

可以使用 top 命令来查看系统负载。top 命令会显示当前系统的进程列表,并按照 CPU 占用率或内存占用率进行排序。你可以使用 top 命令来查看系统的负载情况,包括 CPU 使用率、内存使用率、交换分区使用率等.

也可以使用 uptime 命令来查看系统的负载情况。uptime 命令会显示系统的运行时间、当前登录用户数以及系统的平均负载情况。平均负载是指在过去 1 分钟、5 分钟和 15 分钟内,系统处于等待 CPU 或 I/O 的进程数的平均值.

37. 什么是平均负载

平均负载(Load Average)是一段时间内系统的平均负载,这个一段时间一般取 1 分钟、5 分钟、15 分钟。它是指在这段时间内,系统处于等待 CPU 或 I/O 的进程数的平均值。如果平均负载是 1,表示系统的 CPU 在这段时间内被占用了 100%。如果平均负载是 2,表示系统的 CPU 在这段时间内被占用了 200%。如果平均负载是 0.5,表示系统的 CPU 在这段时间内被占用了 50%。

38. git pull 和 git fetch 的区别

git pull 是 git fetch + git merge

git fetch 只是将远程仓库的最新的版本下载到本地,但是不会自动 merge,相当于工作区中的文件并没有更新

git pull 会从远程仓库获取到最新的版本并 merge 到本地。

git fetch 更保险一些,git pull 操作更简单

39. git merge 和 git rebase 的区别

  1. 工作方式:

    • git merge:将一个分支的更改合并到另一个分支时,会创建一个新的合并提交,将两个分支的更改整合在一起。这个合并提交会保留两个分支的完整历史记录,并创建一个新的合并节点。

    • git rebase:将一个分支的更改合并到另一个分支时,会将当前分支的提交复制到目标分支的顶部,然后将当前分支指向复制后的提交。这个过程会“变基”当前分支的提交,使其基于目标分支的最新提交。这样可以创建一个更线性的提交历史。

  2. 提交历史:

    • git merge:合并操作会保留两个分支的完整提交历史,包括合并提交。这样可以清楚地看到每个分支的贡献和合并点。

    • git rebase:变基操作会修改当前分支的提交历史,将其放在目标分支的顶部。这样可以创建一个更线性、干净的提交历史。但是,由于提交历史被修改,可能会导致其他开发者在共享分支上的问题。

  3. 冲突处理:

    • git merge:当合并操作存在冲突时,Git 会将所有冲突一次性显示给开发者,需要手动解决所有冲突后才能完成合并。

    • git rebase:当变基操作存在冲突时,Git 会在每个提交应用时逐个显示冲突,需要在每个提交上手动解决冲突。这样可以更容易地处理冲突,因为每个提交的冲突范围更小。

  4. 适用场景:

    • git merge:适用于在合并分支时保留完整的提交历史记录,并且不需要修改提交的顺序。

    • git rebase:适用于创建一个干净、线性的提交历史,并且不介意修改提交的顺序。特别适用于个人分支或私有分支,不适合在共享分支上使用。

40. 什么是 git cherry-pick?

命令 git cherry-pick 通常用于把特定提交从存储仓库的一个分支引入到其他分支中。常见的用途是从维护的分支到开发分支进行向前或回滚提交。

41. 操作系统层面,CAS 操作是怎么做的?

通过硬件的支持来实现的。现代的处理器通常提供了一些原子操作的指令,用于执行 CAS 操作。这些指令可以在单个指令周期内完成读取、比较和写入操作,从而保证了操作的原子性。

下面是 CAS 操作的一般过程:

  1. 读取共享变量的当前值。

  2. 将当前值与预期值进行比较。

  3. 如果当前值等于预期值,则将新值写入共享变量。

  4. 如果当前值不等于预期值,则说明其他线程已经修改了共享变量,操作失败。

如果 CAS 操作失败,通常需要重新执行整个过程,直到操作成功为止。这种方式可以避免使用锁或其他同步机制,提高并发性能。

42. Protobuf fixed32 类型和 int32 类型有什么区别

  1. 存储大小: fixed32 类型在内存中占据固定的 4 个字节(32 位),无论存储的值的大小如何。而 int32 类型则使用变长编码,根据存储的实际值的大小来决定所占用的字节数。通常情况下,int32 类型需要使用 1-5 个字节来存储。

  2. 数值范围: fixed32 类型的取值范围是从 0 到 2^32-1(约 42 亿),而 int32 类型的取值范围是从 -2^31 到 2^31-1(约 -21 亿到 21 亿)。

  3. 符号性: fixed32 类型是无符号的,即只能表示非负整数。而 int32 类型是有符号的,可以表示正数、负数和零。

43. Linux 系统的 8080 端口有多少个 TCP 连接

lsof -i :8080 | grep TCP | wc -l
netstat -anp | grep 8080 | grep ESTABLISHED | wc -l

44. 共享内存的方式如何保证并发安全

  • 互斥锁

  • 读写锁

  • 原子操作(CAS)

  • 同步机制(信号量)

45. 用户态转到内核态的方式

  1. 系统调用:用户态程序通过系统调用请求操作系统提供的服务。系统调用是一种特殊的软件中断,当用户态程序调用系统调用时,会触发一个软件中断,将控制权转移到内核态的系统调用处理程序。

  2. 异常:当用户态程序发生异常事件时,如访问非法内存、除零等,会触发异常,导致用户态程序切换到内核态的异常处理程序。

  3. 外围设备中断:当外围设备完成用户请求的操作后,会向 CPU 发送中断信号,这时 CPU 会转去处理对应的中断处理程序。

46. 什么是中断

中断是计算机系统中的一种机制,用于在程序执行过程中暂停当前任务的执行,并转而处理某个特定事件或请求。当发生中断时,处理器会立即停止当前正在执行的指令,保存当前的执行状态,并跳转到预定义的中断处理程序去处理中断事件。中断可以分为硬件中断和软件中断两种类型。

47. 孤儿进程和僵尸进程

孤儿进程是指父进程已经终止,而子进程仍在运行的情况。此时,子进程的父进程 ID 变成 1 (即 init 进程) ,该进程接管孤儿进程的控制,并进行状态收集工作,防止孤儿进程一直运行并占用资源。孤儿进程被 init 进程接管后,通常不会对系统造成危害,因为 init 进程会负责清理孤儿进程并释放它们所占用的资源。

僵尸进程是指子进程已经终止,但其父进程尚未获取子进程的终止状态信息。在这种情况下,子进程的进程描述符仍然保存在系统中,尽管它不再运行,但仍占用系统资源,如进程表项和一些系统资源。如果大量僵尸进程积累,可能导致系统资源耗尽,甚至系统崩溃。

僵尸进程可以通过父进程调用 wait() 或 waitpid() 函数来处理,以避免产生大量的僵尸进程。孤儿进程则由 init 进程自动接管并清理。

48. 如果我输入某个域名,想让他不访问这个 ip 要怎么办

  • 编辑本地 Hosts 文件

  • 使用 DNS 过滤

49. 硬链接和软链接

  1. 共享 inode:硬链接共享 inode,软链接则不共享。

  2. 删除文件的影响:删除源文件不会影响硬链接的访问,而删除源文件会导致软链接失效。

  3. 创建限制:硬链接不能跨文件系统或链接目录,而软链接可以。

  4. 文件类型:硬链接是文件的另一个入口,软链接是一个独立的文件,类似于 Windows 的快捷方式。

50. 取一个文件操作系统层面中间执行过的所有系统

  1. 用户请求

当用户通过应用程序(如文本编辑器或文件管理器)请求打开一个文件时,操作系统接收到这个请求。

  1. 系统调用

操作系统使用系统调用接口来处理文件操作。用户进程会调用如 open() 的系统调用来请求打开文件。这时,操作系统会进行以下操作:

  • 路径解析:操作系统会解析用户提供的文件路径,确定文件在磁盘上的位置。

  • 文件控制块(FCB):操作系统会在内存中为该文件创建一个文件控制块,FCB 中包含文件的元数据,如文件大小、权限、位置等信息。

  1. 文件系统操作

  • 查找目录:操作系统会在文件系统的目录结构中查找文件。文件系统通常以树状结构组织文件,根目录下可能有多个子目录。

  • 读取磁盘信息:一旦找到文件,操作系统会读取与该文件相关的磁盘块信息,包括文件的起始块号和数据块的位置。

  1. 磁盘 I/O 操作

  • 磁头定位:操作系统控制磁盘的磁头移动到正确的柱面和扇区,以便读取文件数据。这个过程包括将磁头移动到指定的柱面,激活对应的盘面,然后在磁盘旋转时读取数据。

  • 数据传输:一旦磁头定位到正确的位置,操作系统会将数据块从磁盘读取到内存中。这个过程可能涉及多个 I/O 操作,特别是当文件数据分散在多个磁盘块上时。

  1. 内存管理

  • 文件缓冲:读取的数据会被存储在内存中的缓冲区,以便快速访问。操作系统会维护一个文件描述符表,跟踪打开的文件及其状态。

  • 访问权限检查:在打开文件之前,操作系统还会检查当前进程对该文件的访问权限,确保没有未授权的访问。

  1. 完成操作

  • 返回文件句柄:操作系统成功打开文件后,会返回一个文件句柄(或文件描述符)给用户进程,用户可以通过这个句柄进行后续的读写操作。

  • 关闭文件:当用户完成文件操作后,应用程序会调用 close() 系统调用,操作系统会更新文件的状态,并释放相关资源。

51. 线程创建和进程创建在操作系统层面有什么区别

  • 进程创建:创建一个新进程通常涉及复制父进程的所有资源,包括内存空间、文件描述符等,这一过程开销较大。操作系统需要为新进程分配独立的内存和资源,通常使用 fork() 系统调用来实现,这会导致较高的性能开销。

  • 线程创建:线程的创建则相对简单,通常通过调用 pthread_create() 等函数来实现。新线程共享其父进程的资源,避免了大量的内存复制,因此创建速度快,开销小。

52. 缺页中断

当程序试图访问一个尚未分配物理内存空间的虚拟地址时会发生缺页中断。这种中断会导致当前执行的指令被暂停,然后操作系统会接管控制权,并负责处理这种异常情况。

缺页中断属于内中断,由当前指令发出。中断后程序会阻塞,等待中断程序结束后再执行。中断程序首先判断内存中是否有空闲内存块:

  • 如果有,就调入该内存块,并修改页表项

  • 如果没有,则启动调度算法选择一个页面淘汰,并将所需页面调入内存。如果被淘汰页面被修改过,需要先写回外存。

页面置换算法

当发生缺页中断但内存无空闲物理块时,需要根据页面置换算法从内存中选择一页淘汰到磁盘。常见算法有:

  • FIFO(先进先出): 淘汰最先调入内存的页面,但会淘汰经常访问的页面

  • LRU(最近最少使用): 淘汰最长时间未访问的页面,实现复杂

  • OPT(最佳置换): 淘汰以后不再被访问或最迟才被访问的页面,但无法实现

53. Linux 的一台服务器,怎么样去找到某一个程序它对应的进程是多少

  • ps -ef: 显示所有进程的完整信息,包括进程号和父进程号

  • ps aux: 以用户为主的格式显示进程状况

  • ps -C <程序名>: 显示指定程序名的进程信息

例如,查找 tomcat 进程:ps -ef | grep tomcat

找到了这个进程号,然后我想进一步去找这个进程对应的有哪些线程怎么找 ps -T -p <pid>

54. 频繁磁盘 I/O 怎么解决

  1. 使用内存映射(mmap)

    • mmap 将文件映射到进程的虚拟内存空间,允许程序像访问内存一样直接访问文件内容,减少了传统 I/O 操作中的多次数据拷贝过程,从而降低了 I/O 延迟

  2. 增加缓存

    • 通过增加内存缓存,可以减少对磁盘的直接访问频率。数据首先被读入内存中,后续操作优先从内存中获取。

  3. 异步 I/O

    • 采用异步 I/O 可以使得程序在等待 I/O 操作完成时继续执行其他任务,从而提高整体效率。

  4. 优化文件访问模式

    • 通过批量读取或写入数据,减少频繁的小规模 I/O 请求。

55. 文件系统的 inode 有什么信息

  • 文件类型:指示该 inode 所对应的对象是文件、目录、设备文件等。

  • 所有者信息

    • 用户 ID (User ID):表示文件的拥有者。

    • 组 ID (Group ID):表示文件所属的用户组。

  • 访问权限:包括对文件的读、写和执行权限。

  • 时间戳

    • 创建时间 (ctime):inode 最后一次变动的时间。

    • 修改时间 (mtime):文件内容最后一次变动的时间。

    • 访问时间 (atime):文件最后一次被访问的时间。

  • 链接数:指向该 inode 的硬链接数量,即有多少个文件名指向这个 inode。

  • 文件大小:以字节为单位,表示文件的总字节数。

  • 数据块位置:记录该文件数据存储在磁盘上的具体块(block)位置。

56. 为什么频繁系统调用会降低系统性能

  1. 上下文切换开销:每次系统调用都会导致用户空间与内核空间之间的上下文切换。这一过程需要保存和恢复寄存器、栈等状态信息,因此引入了性能开销。当系统调用频繁时,程序会花费大量时间在这些切换上,从而减少了实际的计算时间。

  2. CPU 资源消耗:系统调用不仅需要进行上下文切换,还涉及到权限检查和参数传递等额外操作。这些操作比普通函数调用的开销要大得多,导致 CPU 需要处理更多的任务,从而影响整体性能。例如,系统调用可能会导致缓存失效,增加了 CPU 访问内存的时间。

Last updated