操作系统与 Linux

1. 进程、线程和协程

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

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

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

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

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

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

2. 进程通信的手段

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

  1. 管道

Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。

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

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

  1. 消息队列

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

  1. 共享内存

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

  1. 信号量

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

  1. 信号

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

  1. Socket

前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

以上,就是进程间通信的主要机制了。你可能会问了,那线程通信间的方式呢?

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:

互斥的方式,可保证任意时刻只有一个线程访问共享资源; 同步的方式,可保证线程 A 应在线程 B 之前执行;

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 算法,也就是优先回收不常访问的内存。回收内存的操作基本都会发生磁盘 I/O 的,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多,这个过程势必会影响系统的性能。

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. 操作系统的锁机制

当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。

读写锁适用于能明确区分读操作和写操作的场景,读写锁在读多写少的场景,能发挥出优势。

另外,互斥锁、自旋锁、读写锁都属于悲观锁,悲观锁认为并发访问共享资源时,冲突概率可能非常高,所以在访问共享资源前,都需要先加锁。

相反的,如果并发访问共享资源时,冲突概率非常低的话,就可以使用乐观锁,它的工作方式是,在访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

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

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

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

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

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

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

13. 调度算法合集

进程调度

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

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

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

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

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

页面置换

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

14. 零拷贝

每个 I/O 设备都有自己的 DMA 控制器,通过这个 DMA 控制器,CPU 只需要告诉 DMA 控制器,我们要传输什么数据,从哪里来,到哪里去,就可以放心离开了。后续的实际数据传输工作,都会由 DMA 控制器来完成,CPU 不需要参与数据传输的工作。

传统 IO 的工作方式,从硬盘读取数据,然后再通过网卡向外发送,我们需要进行 4 上下文切换,和 4 次数据拷贝,其中 2 次数据拷贝发生在内存里的缓冲区和对应的硬件设备之间,这个是由 DMA 完成,另外 2 次则发生在内核态和用户态之间,这个数据搬移工作是由 CPU 完成的。

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

Kafka 和 Nginx 都有实现零拷贝技术,这将大大提高文件传输的性能。

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

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

15. 怎么传输大文件

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

在 Nginx 里,可以通过配置,设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝。

16. I/O 多路复用

最基础的 TCP 的 Socket 编程,它是阻塞 I/O 模型,基本上只能一对一通信,那为了服务更多的客户端,我们需要改进网络 I/O 模型。

比较传统的方式是使用多进程/线程模型,每来一个客户端连接,就分配一个进程/线程,然后后续的读写都在对应的进程/线程,这种方式处理 100 个客户端没问题,但是当客户端增大到 10000 个时,10000 个进程/线程的调度、上下文切换以及它们占用的内存,都会成为瓶颈。

为了解决上面这个问题,就出现了 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)系统调用的时候,实际上是将文件数据写到了内核的 page cache,它是文件系统中用于缓存文件数据的缓冲,所以即使进程崩溃了,文件数据还是保留在内核的 page cache,我们读数据的时候,也是从内核的 page cache 读取,因此还是依然读的进程崩溃前写入的数据。

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

19. 进程内存结构

内核区域

栈(从上到下分配)

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

堆内存(从下到上分配)

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

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

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

20. Linux fork 和 exec 的区别

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

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

内核态和用户态是计算机操作系统中的两种运行级别。用户态是指程序在执行时,所处的特权级较低,只能访问自己的内存空间和 CPU 寄存器,不能直接访问操作系统的资源。而内核态是指程序在执行时,所处的特权级较高,可以访问所有的内存空间和 CPU 寄存器,可以直接访问操作系统的资源。

当程序运行在 3 级特权级上时,可以称之为运行在用户态,当程序运行在 0 级特权级上时,称之为运行在内核态。

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. 什么情况下从用户态陷入内核态

当 CPU 正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。

此外,当应用程序使用操作系统提供的接口调用内核功能或者当外围设备完成用户的请求操作后,会向 CPU 发出中断信号,此时,CPU 就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。

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

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

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

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

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

如果您在本地读取一个文件并通过网络发送到另一端,中间涉及的拷贝次数取决于使用的传输协议和工具。例如,如果使用 scp 命令,它将在本地计算机和远程计算机之间进行 3 次拷贝:本地计算机到本地 ssh 进程、本地 ssh 进程到远程 ssh 进程、远程 ssh 进程到远程计算机。如果使用 socket 编程,它将涉及多个内存拷贝。

28. 如何优化磁盘 I/O

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

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

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

29. 线程通信的方式

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

30. socket 可读的情况

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

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

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

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 过滤

Last updated