【八股】操作系统篇

本文深入探讨操作系统中的核心概念,包括进程、线程和协程的定义、区别及其应用场景。讨论了线程的状态、同步方式,以及进程通信的方法。还涉及用户态与内核态的切换成本、中断处理的信息保存,以及Linux系统的基本操作,如命令、文件系统和进程管理。
摘要由CSDN通过智能技术生成

进程、线程、协程

进程是一个程序执行的实例,是操作系统资源分配【虚拟内存、文件句柄、信号量等】的基本单位,比如说打开一个XX软件。线程是任务调度和执行的基本单位,是轻量级的进程,比进程更小的执行单位。一个进程可以有多个线程;协程是一种轻量级的线程,它由用户空间的库函数实现,不依赖于操作系统的线程调度机制。

进程,线程,协程的区别是什么

内存空间、切换开销、稳定性和安全性。

  • 首先,我们来谈谈进程。进程是操作系统中进行资源分配和调度的基本单位,它拥有自己的独立内存空间和系统资源。每个进程都有独立的堆和栈,不与其他进程共享。进程间通信需要通过特定的机制,如管道、消息队列、信号量等。由于进程拥有独立的内存空间,因此其稳定性和安全性相对较高,但同时上下文切换的开销也较大,因为需要保存和恢复整个进程的状态。
  • 线程是进程内的一个执行单元,也是CPU调度和分派的基本单位。与进程不同,线程共享进程的内存空间,包括堆和全局变量。线程之间通信更加高效,因为它们可以直接读写共享内存。线程的上下文切换开销较小,因为只需要保存和恢复线程的上下文,而不是整个进程的状态。然而,由于多个线程共享内存空间,因此存在数据竞争和线程安全的问题,需要通过同步和互斥机制来解决。
  • 最后是协程。协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。协程的切换开销非常小,因为只需要保存和恢复协程的上下文,而无需进行内核级的上下文切换。这使得协程在处理大量并发任务时具有非常高的效率。然而,协程需要程序员显式地进行调度和管理,相对于线程和进程来说,其编程型更为复杂。

协程和线程的区别

  1. 线程是并发执行的,多个线程可以同时执行,每个线程有自己的执行路径和资源。协程在单线程中运行,只有一个执行线程,但是可以在逻辑上实现并发,因为协程可以在执行过程中暂停和恢复,从而可以处理多个任务。
  2. 线程的创建和销毁通常会伴随着一定的系统开销,包括内存分配、上下文切换等。协程的开销相对较小,因为它们在用户空间中运行,并且不需要操作系统的线程切换。
  3. 线程之间的并发控制通常使用锁、信号量、条件变量等机制来实现。协程通常使用协作式调度来实现并发控制,通过显式地在协程之间进行 yield 和 resume 操作来控制执行顺序和并发访问。

进程切换和线程切换的区别,线程切换为什么比进程块

  1. 进程切换涉及到更多的内容,如整个进程的地址空间、全局变量、文件描述符等,开销通常较大
  2. 线程切换只涉及到线程的堆栈、寄存器和程序计数器等,不涉及进程级别的资源。线程共享同一进程的地址空间和资源,无需切换地址空间、内存映射表、开销通常较小

有了进程为什么还需要线程

因为进程切换的开销很大,而线程执行的成本比较低;
同一个进程里的线程是共享内存和文件的,他们之间的相互通信无需调用内核;
而且一个进程里可以有多个线程,可以并发的完成许多任务。

为什么进程崩溃不会对其他进程产生很大影响

这是取决于进程本身的隔离性和独立性:每个进程都有自己独立的内存空间,当一个进程崩溃时,其内存空间会被操作系统回收不会影响其他进程的内存空间。这种进程间的隔离性保证了一个进程崩溃不会直接影响其他进程的执行。而且每个进程都是独立运行的,它们之间不会共享资源,如文件、网络连接等。

进程有哪些状态

new 创建状态
ready 进程已经获得了除CPU外的一切资源,只要等到CPU分配的时间片就可以开始运行
running 收到了调度器调度后开始运行,如果时间片结束了会回到ready状态
waiting 阻塞状态,可能是在等待IO传输完成或者是因为某个事件暂停
terminated 进程正常结束 或者是因为某些原因中断退出运行

进程的通信方式

  • 管道:匿名/有名
    匿名用于父子或兄弟进程(只存在内存中),有名用于没有亲缘关系的(以磁盘文件的方式存在)FIFO。不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持lseek 之类的文件定位操作。缺点是只能承载无格式字节流以及缓存区大小受限等缺点
    • 匿名管道顾名思义,没有名字标识,是特殊文件只存在于内存,没有存在于文件系统中,shell命令中的竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
    • 命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为p的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。
  • 消息队列
    克服管道和信号的缺点,存放在内核中,实现信息的随机查询,而不是FIFO。消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体。接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程
  • 共享内存
    最有用的,让多个进程共享同一个内存区间。可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。缺点是多进程竞争同个共享资源会造成数据的错乱。
  • 信号量
    用于进程间同步和互斥的机制,允许多个进程对共享资源进行访问控制。互斥访问是指确保任何时刻只能有一个进程访问共享资源。进程间的同步是说信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是P操作和V操作。
  • 信号
    用于向进程发送异步通知。可以通过系统调用 kill() 或者 signal() 来发送信号,进程可以通过注册信号处理函数来接收和处理信号。通知进程某个事件已经发生,缺点是承载信息量少。信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+c)和软件来源(如 kil 命令),一旦有信号发生,进程有三种方式响应信号1. 执行默认操作、2.捕捉信号、3.忽略信号。有两个信号是应用进程无法捕捉和忽略的,即SIGKILLSIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
  • 套接字
    网络编程中常用的通信方式,允许不同主机上的进程进行通信,比如客户端和服务端网络通信。前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

进程共享内存是怎么实现的

拿出一块虚拟地址,映射到相同的物理内存中。这样一个进程写入的东西另外一个进程马上可以看到,提高了通信速度。在 Linux 中,可以通过 shmget 系列函数或者 POSIX 的共享内存接口来实现,同时 mmap 也可以作为一种变通的共享内存手段。

优势:高效【数据在共享内存中,不需要在进程之间复制。相比于管道、消息队列等方式,效率更高,特别是当需要交换大数据量时,优势明显】、灵活【可以在共享内存中存储任意类型的数据结构,不像消息队列或管道只能传递字节流】、持久性【某些操作系统(如 Linux)允许共享内存区域在所有进程退出后仍然存在,直到手动释放】
缺点:同步问题【由于多个进程同时访问共享内存,容易引发同步问题(如竞态条件),需要额外的机制来保证数据的一致性。】、共享内存需要手动分配和释放、安全性问题【进程可以直接访问共享内存中的数据,可能会造成数据的意外修改或破坏】
使用场景:高速数据交换:适合在多个进程间快速交换大量数据,比如在图像处理、多媒体应用等需要大量数据的场景。跨进程缓存:可以用于多个进程共享缓存数据,以减少重复的计算或数据传输。进程间通信:多个进程可以使用共享内存作为数据共享的手段,减少上下文切换和数据拷贝。

进程的调度算法

  1. 先到先服务(适合CPU繁忙型,不适合I/O繁忙型)
  2. 短作业优先
  3. 时间片轮转(太短会导致过多切换,降低CPU效率,太长会引起对短作业进程的响应时间变长,一般20ms~50ms)
  4. 优先级(分为静态和动态,静态再创建进程时就确定优先级了不会变化,动态会根据进程运行时间或者等待时间调整优先级)(还分为抢占式和非抢占式,区别在于当就绪队列中出现优先级更高的进程A时,是运行完当前进程再运行A还是挂起当前进程直接运行A。缺点是优先级低的进程可能永远不会运行。
  5. 多级反馈队列(时间片轮转+优先级),「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短,「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先
    级高的队列。新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定
    的时间片没运行完成,则将其转入到第二级队列的未尾,以此类推,直至完成。当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列未尾,接着让较高优先级的进程运行。这种方式能够让短作业很快的在第一级队列被处理完

进程退出了内存里的数据有影响吗、机器重启有影响吗

进程退出:数据不会立即丢失,除非所有进程都断开共享内存段,且共享内存段被手动删除。
机器重启:对于 System V 共享内存,数据会丢失;对于 POSIX 共享内存,如果映射到文件系统,数据可以在重启后恢复,否则数据同样会丢失。

线程有哪些状态

new 新建线程:线程对象被创建出来,但没有执行start()。
runnable 可运行,线程调用start()方法
blocked 阻塞:没有获取锁则进入阻塞状态,获得锁再切换为可执行状态
waiting 等待:线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态。
timed_waiting 计时等待:线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态。
teminated 终止:该线程已经运行完毕。

多线程比单线程的优势,劣势?

优势:提高程序的运行效率,可以充分利用多核处理器的资源,同时处理多个任务加快程序的执行速度。
劣势:存在多线程数据竞争访问的问题,需要通过锁机制来保证线程安全,增加了加锁的开销,并且还会有死锁的风险。多线程会消耗更多系统资源,如CPU和内存,导致系统负载过高,某个线程崩溃后,可能会导致进程崩溃。

线程切换的详细步骤

  1. 上下文保存:当操作系统决定切换到另一个线程时,它首先会保存当前线程的上下文信息。上下文信息包括寄存器状态、程序计数器、堆栈指针等,用于保存线程的执行状态。
  2. 切换到调度器:操作系统将执行权切换到调度器(Scheduler)。调度器负责选择下一个要执行的线程,并根据调度算法做出决策。
  3. 上下文恢复:调度器选择了下一个要执行的线程后,它会从该线程保存的上下文信息中恢复线程的执行状态。
  4. 切换到新线程:调度器将执行权切换到新线程,使其开始执行。

上下文信息的保存通常由操作系统负责管理,具体保存在哪里取决于操作系统的实现方式。一般情况下上下文信息会保存在线程的控制块(Thread Control Block,TCB)中。TCB是操作系统用于管理线程的数据结构,包含了线程的状态、寄存器的值、堆栈信息等。当发生线程切换时,操作系统会通过切换TCB来保存和恢复线程的上下文信息。

什么是线程同步,线程同步的方式(通讯的方式)【各种锁】

👉线程同步是两个或者两个以上的共享关键资源的线程的并发执行,访问这些关键资源时应该保持同步,防止产生冲突

  • 互斥锁:只有拥有互斥锁对象的线程可以访问公共资源,因为互斥锁对象只有一个,所以在某一时刻访问公共资源的线程也只有一个。sychronized和lock关键字都是这种机制
  • 自旋锁(Spinlock):通过 CPU 提供的 CAS 函数(Compare And swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。加锁过程包含两个步骤:1. 查看锁的状态,如果锁是空闲的,则执行第二步。2. 将锁设置为当前线程持有。使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。这里的「忙等待可以用 while 循环等待实现,不过最好是使用 CPU 提供的 PAUSE 指令来实现「忙等待」,因为可以减少循环等待时的耗电量。【在单核CPU上需要抢占式的调度去,否则自旋的线程永远不会放弃CPU】
  • 读写锁:允许多个线程读取资源,但只允许一个线程对资源进行改写。原理:当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。读写锁在读多写少的场景,能发挥出优势。
  • 信号量:它允许同一时刻多个线程访问一个资源,但是会控制访问线程最大数量。可以是命名的(有名信号量)或无名的(仅限于当前进程内的线程)。通常信号量表示资源的数量,对应的变量是一个整型(sem)变量。另外,还有两个原子操作的系统调用函数来控制信号量的,分别是:P操作:将 sem 减 1,相减后,如果 sem<0,则进程/线程进入阻塞等待,否则继续,表明P操作可能会阻塞;V操作:将 sem加 1,相加后,如果 sem<=0,唤醒一个等待中的进程/线程,表明V操作不会阻塞。
  • 屏障:就像一面墙一样,等多个线程都运行到屏障处以后再一起执行,比如CyclicBarrier
  • 事件:可以比较多线程的优先级,通过通知操作的方式
  • 条件变量:实现“等待–>唤醒“逻辑常用的方法。它是利用线程间共享的全局变量进行同步,主要包括两个动作:一个线程等待“条件变量的条件成立"而挂起;另一个线程使“条件成立”。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量,函数pthread_cond_wait把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。在函数返回时,互斥量再次被锁住。

自旋锁和互斥锁的区别

加锁失败是,互斥锁采用线程切换应对,互斥锁采用忙等待应对。如果能确定被锁住的代码执行时间很短,就用自旋锁

协程需要加锁吗

在单线程环境中,通常不需要使用锁,因为在单线程中,协程的执行是顺序的,不会出现并发访问的情况。协程之间的切换是由协程调度器控制的,不会存在并发访问共享资源的情况。
而在多线程环境中,如果协程之间存在并发访问共享资源的情况,就需要考虑使用锁来保护资源的访问,否则可能会发生竞态条件(Race Condition)等并发访问问题。

【竞态条件】程序的输出结果会随着线程或进程的执行顺序而变化,而这种变化是不可预测的

并行与并发的区别

以多核CPU为例,并行是指同一时间做多件事情的能力,比如4个CPU内核执行4个线程;
并发是指同一时间应对多件事情的能力,比如1个CPU被多个线程轮流使用。
单核CPU的思想就是宏观并行,微观并发

PCB是什么,有哪些信息

PCB是进程控制块,相当于进程的大脑,是用来管理和跟踪进程的一种数据结构。每个进程在创建的时候CPU就会自动的分配一个唯一的进程ID,并创建一个PCB。
信息:描述信息、调度信息、对资源的需求情况、打开的文件信息

操作系统里面堆栈分别是什么概念?是分配给线程还是进程

概念后进先出的内存结构,用于存储函数调用的上下文,包括局部变量、函数参数和返回地址。用于动态分配内存的区域,允许程序在运行时按需分配和释放内存。它用于存储动态创建的对象和变量,生命周期不受函数调用的限制。
管理是由编译器自动管理的,当函数调用时,栈空间会分配,函数返回时,栈空间会自动释放。堆内存是非结构化的,由程序员手动管理,可能会产生内存泄漏或碎片问题。
分配分配给线程,每个线程有自己的独立栈,栈的大小通常是在线程创建时分配的,并且是固定的。分配给进程,堆是进程级的内存区域,整个进程的所有线程共享同一个堆空间。进程可以通过调用系统函数(如 mallocnewfree 等)来动态管理堆内存。

用户态和内核态

它们的主要区别在于权限和可执行的操作。

  • 内核态(Kernel Mode):在内核态下,CPU可以执行所有的指令和访问所有的硬件资源。这种模式下的操作具有更高的权限,主要用于操作系统内核的运行。底层操作主要包括:内存管理、进程管理、设备驱动程序控制、系统调用等。
  • 用户态(User Mode):在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。这种模式下的操作权限较低,主要用于运行用户程序。

分为内核态和用户态的原因

  1. 安全性:通过对权限的划分,用户程序无法直接访问硬件资源,从而避免了恶意程序对系统资源的破坏。
  2. 稳定性:用户态程序出现问题时,不会影响到整个系统,避免了程序故障导致系统崩溃的风险。
  3. 隔离性:内核态和用户态的划分使得操作系统内核与用户程序之间有了明确的边界,有利于系统的模块化和维护。

执行什么操作的时候,用户态会进入到内核态。

通常是为了执行某些需要特权的操作,这些操作涉及到硬件访问、系统资源管理、以及其他需要高权限的任务。举几个具体的例子:

  1. 用户态程序通过系统调用请求内核提供的服务,比如文件操作、进程管理、内存分配、网络通信等。文件操作:open(), read(), write(), close();进程管理:fork(), exec(), wait();内存管理:mmap(), brk();网络通信:socket(), connect(), send(), recv()
  2. 发生硬件或者软件中断时,例如外设(键盘、鼠标、网络设备等)发出中断信号,CPU 响应中断并切换到内核态执行中断处理程序。 用户态程序可以通过软中断指令(如 int 指令)请求内核服务,这通常是实现系统调用的一种方式。
  3. 发生异常或者错误的时候,比如除零错误、非法内存访问、缺页异常(进程访问的内存页不在内存中)等,处理这些异常需要切换到内核态,由内核来决定如何处理。
  4. 当用户态程序需要访问硬件设备(如磁盘、网络接口、图形卡等)时,会通过系统调用与设备驱动程序进行通信,这会切换到内核态执行。
  5. 当用户态进程接收到信号(如终止信号、定时器信号)时,内核会切换到内核态来处理这些信号,可能涉及进程的暂停、终止或其他信号处理。
  6. 用户态程序尝试执行特权指令(例如直接访问 I/O 端口、修改关键寄存器)会导致陷入内核态,因为这些指令只能在内核态下执行。
  • 系统调用的实现

    1. 调用过程: 用户态程序通过调用 C 库函数(如 printf()fopen() 等),这些库函数会进一步调用系统调用接口(如 write()open())。

    2. 系统调用陷入: 通过软中断指令或 CPU 提供的系统调用指令(如 syscall 指令),将控制权从用户态切换到内核态。

    3. 内核处理: 内核态的系统调用处理程序执行相应的操作,如文件读写、进程调度等。

    4. 返回用户态: 操作完成后,内核将控制权返回给用户态程序,继续执行。

内核态用户态切换哪些操作比较耗时

  1. 上下文切换:当进程或线程从用户态切换到内核态时,需要保存当前的用户态上下文(如程序计数器、堆栈指针等),并加载内核态的上下文(如内核栈、内核态堆栈等)。这里就涉及到寄存器的保存和恢复,内存的访问等操作,因此是内核态用户态切换的主要耗时操作之一。
  2. 权限检查和切换:内核态和用户态拥有不同的权限级别,当切换时,需要进行权限检查和切换,以确保进程或线程具有执行所需操作的权限。可能会访问控制列表(ACL)、权限表、内存映射等操作,因此也会带来一定的开销。
  3. TLB(Translation Lookaside Buffer)刷新:TLB 是用于虚拟地址到物理地址的转换的高速缓存,当进程或线程切换时,TLB 中存储的地址映射可能会失效,需要进行刷新操作,这可能导致额外的内存访问和缓存失效,从而增加了内核态用户态切换的开销。

中断需要保存哪些信息

  1. 程序计数器(PC):保存了当前正在行的指令的地址,以便在中断处理完成后能够正确地恢复执行现场。
  2. 寄存器状态:保存当前线程或进程的寄存器状态,包括通用寄存器(如AX、BX、CX等)、指令指针寄存器(如EIP)、堆栈指针寄存器(如ESP)、段寄存器等。
  3. 栈指针(Stack Pointer):栈指针指向当前线程或进程的栈顶。
  4. 状态寄存器(Flags Register):状态寄存器保存了一些标志位,用于标识处理器状态和条件码。

I/O多路复用

允许程序同时监视多个文件描述符(文件、套接字等)的 I/O 事件,并在其中任意一个文件描述符准备好进行 I/O 操作时通知程序。I/O 多路复用广泛应用于网络服务器【如 nginx、Redis,使用 epoll 处理大量并发连接时,可以显著提高系统的 I/O 效率】、数据库【管理多个客户端的查询请求】、分布式系统等需要高效处理大量 I/O 请求的场景。

I/O 多路复用的工作流程

应用程序通过调用 select、poll 或 epoll 等函数,将多个文件描述符注册到内核中。
内核监视这些文件描述符,当有事件发生(如可读、可写)时,内核通知应用程序。
应用程序接收到通知后,处理相应的 I/O 操作。

select,poll,epoll

selectpollepoll 是Linux下用于多路复用I/O(I/O Multiplexing)的系统调用,它们允许一个程序同时监控多个文件描述符(如套接字)以确定哪些文件描述符可以进行读写操作。select: 简单且兼容性好,但在处理大量文件描述符时效率低下,适合小规模应用。poll: 更灵活,没有文件描述符限制,但在大规模应用中性能不佳。epoll: 针对Linux优化,适合大规模并发连接的高性能服务器应用,是处理大量文件描述符的首选

  1. select() 是一种较老的多路复用I/O机制
    👉工作原理:select 允许程序指定一组文件描述符,并阻塞等待这些文件描述符中的任意一个变为可读、可写或发生异常。调用时,程序提供三个文件描述符集合,分别监控读、写和异常事件。
    👉优点:简单且广泛支持,几乎所有的Unix系统和Windows都支持select。
    👉缺点:文件描述符限制: select 受限于单个进程能打开的文件描述符数量(通常是1024)。效率低: select 每次调用都需要重新设置文件描述符集合,且在处理大量文件描述符时性能会显著下降,因为它采用线性扫描方式。
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(sock, &readfds);
    select(sock + 1, &readfds, NULL, NULL, NULL);
    if (FD_ISSET(sock, &readfds)) {
        // 数据可读
    }
    
  2. poll() p是select的改进版本,消除了select的一些限制。
    工作原理:poll 使用一个包含文件描述符及其事件掩码的结构体数组来监控I/O事件。poll 的参数是一个文件描述符数组,它能够监视读、写、和异常事件,并且与select不同,不受文件描述符数量的限制(受限于系统的最大打开文件数)。
    优点:没有文件描述符限制: poll 能处理超过select上限的文件描述符数量。灵活性高: poll 直接支持对任意文件描述符事件的监控。
    缺点:效率问题: poll 依然需要遍历整个文件描述符数组来检查事件,与select类似,在大量文件描述符的情况下性能不佳。
    struct pollfd fds[2];
    fds[0].fd = sock;
    fds[0].events = POLLIN;
    poll(fds, 2, -1);
    if (fds[0].revents & POLLIN) {
        // 数据可读
    }
    
  3. epoll() 是Linux特有的多路复用I/O机制,设计用于高效处理大量文件描述符。
    工作原理:epoll 使用一个文件描述符来管理多个被监控的文件描述符集合。epoll 提供epoll_create、epoll_ctl和epoll_wait三个系统调用。有两种工作模式:水平触发(Level-Triggered, LT) 和 边缘触发(Edge-Triggered, ET),ET模式通常用于更高效的I/O处理。
    优点:高效: epoll 只在文件描述符的状态发生变化时通知用户,避免了select和poll的线性扫描。无上限: 与select和poll不同,epoll可以高效地处理数以万计的文件描述符。动态管理: epoll允许动态添加或删除被监控的文件描述符。
    缺点:Linux专有: 仅在Linux系统上支持,不具备跨平台性。
    int epfd = epoll_create(1);
    struct epoll_event ev, events[10];
    ev.events = EPOLLIN;
    ev.data.fd = sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
    int nfds = epoll_wait(epfd, events, 10, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == sock) {
            // 数据可读
        }
    }
    

I/O 多路复用与多线程/多进程的对比

I/O 多路复用是一种基于事件驱动的并发处理模式,它避免了使用多线程或多进程的开销(如上下文切换、线程同步等)。相比之下,I/O 多路复用更加轻量且适合处理大量 I/O 密集型任务,而多线程或多进程适合 CPU 密集型任务。

高级语言在写完代码后在电脑上运行分别经过了哪些过程

  • Java: 编写.java源代码 👉使用编译器javac编译为字节码,扩展名为.class,编译完成后会生成一个或者多个.class文件,每个文件对应一个Java类 👉 JVM通过类加载器将.class文件加载到内存中,类加载器负责查找、加载和验证字节码。👉 JVM 中的解释器会逐条解释字节码,将其转换为机器代码,然后在 CPU 上执行。这一过程可以使用两种方式:1. 解释执行:逐行解释字节码并执行(效率较低)。2. 即时编译(JIT):为了提高性能,JVM 会使用即时编译器(JIT Compiler)将热点代码(被频繁执行的字节码)编译为本地机器代码,以提高执行速度。JIT 编译的机器代码可以直接在 CPU 上运行。
  • Python:编写.py源代码 👉 使用解释器CPython生成字节码,扩展名为.pyc 👉 Python虚拟机PVM解释字节码并执行
  • C++:编写.cpp.h源代码 👉 编译:使用 C++ 编译器(如 GCC、Clang、MSVC)将源代码编译为目标文件(机器代码),通常扩展名为.o.obj。(编译器执行的步骤包括预处理(处理宏和头文件)、编译(将源代码转换为汇编代码)、汇编(将汇编代码转换为目标代码)等) 👉 链接:编译器的链接器将所有目标文件以及所需的库文件链接在一起,生成一个可执行文件(如 .exe 或无扩展名的 ELF 可执行文件)。负责将代码中的函数调用和变量引用解析为实际的内存地址。👉 执行:最终生成的可执行文件可以直接在操作系统上运行,执行速度快。由于 C++ 生成的是机器码,可执行文件与具体平台紧密相关(不同平台的编译结果不可互通)。

容器、虚拟机、docker之间的关系

用于软件隔离和部署的技术,但它们在实现方式、性能、资源利用和应用场景方面有所不同。

虚拟机容器docker
定义通过虚拟化技术在物理服务器上模拟多个独立的计算环境。每个虚拟机运行自己的操作系统,并与其他虚拟机隔离。轻量级虚拟化技术,它在操作系统层面上实现虚拟化,多个容器共享宿主机的内核,但彼此之间隔离。是容器化平台,它提供了一种标准化的方式来创建、部署和管理容器化应用程序。
工作原理依赖于虚拟机管理程序(Hypervisor),如 VMware、Hyper-V、KVM。
虚拟机管理程序在物理硬件上运行,提供虚拟硬件给每个虚拟机。
每个虚拟机包含完整的操作系统、副本、应用程序等。
依赖于宿主操作系统的内核,通过操作系统的内核特性(如 cgroups 和 namespaces)实现隔离。
每个容器包含应用程序及其所有依赖,但不包含操作系统内核。 容器是进程级别的虚拟化,在宿主操作系统上直接运行。
使用容器技术,利用宿主机的内核,通过 Docker 引擎来管理容器的生命周期。
提供 Docker 镜像,作为创建容器的模板,镜像包含应用程序及其运行所需的所有依赖。
提供工具和命令行接口来创建、运行、停止和管理容器。
优点高度隔离,安全性强。
支持多种操作系统和应用程序的运行。
资源管理灵活,可以动态分配CPU、内存等资源。
启动速度快,通常几秒内启动,因为不需要加载完整的操作系统。
资源利用率高,共享宿主机内核,减少了系统资源开销。
便于持续集成和持续部署(CI/CD),容易迁移和扩展。
简化了容器的管理,具有强大的生态系统和广泛的社区支持。
通过 Docker Hub 等镜像仓库,可以方便地分发和共享应用。
提供跨平台的兼容性,支持在不同环境中一致地运行应用。
缺点启动速度慢,需要启动完整的操作系统。
资源占用多,每个虚拟机都有自己的操作系统,占用较多内存和存储。
虚拟机之间的通信开销较高。
隔离性不如虚拟机,依赖于宿主操作系统的安全性。
通常只能运行与宿主机相同类型的操作系统(如 Linux 容器只能在 Linux 上运行)。
对 Windows 支持较差,通常性能最佳的场景是 Linux 系统。 需要学习 Docker 工具和命令。
应用场景高度隔离和安全性极高:金融、医疗
多操作系统支持
资源调度灵活,可以动态分配CPU、内存等资源
微服务架构
持续集成和持续部署:需要快速开发、测试和部署的
资源利用率高
保持开发、测试和生产环境一致的运行环境
关系虚拟机和容器都是虚拟化技术,目的是隔离应用程序和资源。 Docker 是一种实现容器技术的工具,而容器是 Docker 的基础。区别隔离级别:虚拟机通过虚拟化硬件提供强隔离,容器通过内核隔离提供较轻的隔离。
资源利用:虚拟机需要完整的操作系统,资源消耗大;容器共享内核,资源利用率高。
启动时间:虚拟机启动慢,容器启动快。
使用场景:虚拟机适合需要高度隔离的多操作系统环境,容器适合微服务、CI/CD、快速部署等场景。

Linux

终端和黑洞文件

终端是/dev/tty文件夹下的文件,是用户与操作系统进行交互的界面,用户可以通过终端执行命令、操作文件和程序等。终端提供了一个命令行界面(CLI),用户可以在其中输入命令并接收系统的输出。
黑洞文件 /dev/null 是一种特殊的文件,用于接收数据但不存储数据,它会立即丢弃所有写入它的数据。当你将数据写入 /dev/null 时,它会立即被丢弃,对数据不做任何处理。黑洞文件在某些情况下非常有用,例如在脚本中屏蔽不需要的输出或将输出重定向到一个不需要的地方。

命令

常用命令

清屏:clear
退出当前命令:ctrl+c 彻底退出
当前目录和上层目录:./ ../
主目录:~/
切换目录:cd
列出指定目录中的目录,以及文件:ls-a所有文件,详细信息,包括字节数和可读写权限 】
创建目录:mkdir ;创建文件:touchvi; 复制文件:cp

查看当前进程:ps
查看当前路径:pwd
查看当前用户 id:id
执行睡眠 :ctrl+z 挂起当前进程
执行退出:exit

vi 文件名 #编辑方式查看,可修改 【写文件命令】
cat 文件名 #显示全部文件内容
more 文件名 #分页显示文件内容
less 文件名 #与 more 相似,更好的是可以往前翻页
tail 文件名 #仅查看尾部,还可以指定行数
head 文件名 #仅查看头部,还可以指定行数

查看网络接口和IP地址:ifconfig
测试网络连接:ping
配置静态地址: /etc/network/interfaces

  • 查看系统资源使用情况:tophtop
    【类似于任务管理器,显示当前正在运行的进程列表。默认情况下,top命令会每隔一段时间自动刷新显示,可以通过按下<space>键手动刷新。<Shift> + <P>键按CPU使用率排序,<Shift>+<M>键按内存使用率排序。可以按下k键杀死选定的进程。然后输入要杀死的进程ID,并按下回车键确认。可以通过按下<Shift>+<F>键或者输入o来选择要显示的列。例如,可以选择显示PID、CPU使用率、内存占用等信息。按下q键可以退出top命令。】
    查看最新的系统日志消息:/var/log
  • 查看最新的系统日志消息:/var/log
  • 查看系统启动消息和硬件故障:dmesg
  • 显示Linux整体内存使用:free;新进程可使用内存 = 未使用内存 + 可回收缓存,看available的值
  • 仅文件拥有者有读写权限,其他人无权限(即权限为 rw-------,数值为 600):
chmod 600 file.txt

三种基本权限属性:

  • 属主(User):属主是指文件或目录的所有者,也就是创建该文件或目录的用户。属主拥有最高的权限,可以控制文件或目录的读取、写入和执行权限。属主可以是系统中的任何用户。
  • 所在组(Group):每个文件或目录都与一个所在组相关联。这个所在组通常与创建该文件或目录的用户的默认所在组相同。所在组权限指的是文件或目录所属的组中其他用户对其的访问权限。通常,组成员具有与其他用户相同的权限。
  • 其他用户(Others):除了属主和所在组之外的所有其他系统用户都被归为“其他用户”。其他用户权限指的是系统中所有非属主和非所在组成员的用户对文件或目录的访问权限。

文件权限修改:chmod

  • chmod u+x file 给 file 的属主增加执行权限
  • chmod 751 file 给 file 的属主分配读4、写2、执行1 (7)的权限,给 file 的所在组分配读、执行(5)的权限,给其他用户分配执行(1)的权限
  • chmod u=rwx,g=rx,o=x file 上例的另一种形式
  • chmod =r file 为所有用户分配读权限
  • chmod 444 file 同上例
  • chmod a-wx,a+r file同上例
  • chmod -R u+r directory 递归地给 directory 目录下所有文件和子目录的属主分配读的权限
  • chmod 600 /path/to/file_or_directory 根用户可读可写,普通用户不可读写

统计文件中json文件的数量(wc和grep)、给txt文件修改权限

😀使用 grepwc 来统计指定目录下 .json 文件的数量。

ls /path/to/directory/*.json | wc -l
  • ls /path/to/directory/*.json:列出该目录下所有 .json 文件。
  • wc -l:统计文件的行数,也就是 .json 文件的数量。

如果需要递归统计所有子目录下的 .json 文件,可以使用 find

find /path/to/directory -name "*.json" | wc -l
  • find /path/to/directory -name "*.json":递归查找该目录及其子目录下的所有 .json 文件。

😀修改权限可以使用 chmod 命令

  • 给所有用户读写权限(即权限为 rw-rw-rw-,数值为 666):
chmod 666 file.txt
  • 给所有用户读、写、执行权限(即权限为 rwxrwxrwx,数值为 777):
chmod 777 file.txt
  • 给文件拥有者读写执行权限,其他人只读权限(即权限为 rwxr-xr-x,数值为 755):
chmod 755 file.txt

软链接与硬链接

👉软链接的功能是某一文件在另外一个位置建立一个同步的链接,相当于C语言中的指针,建立的链接直接指向源文件所在的地址,软链接不会另外占用资源,当同一文件需要在多个位置被用到的时候,就会使用到软连接。
👉硬链接在是另外一个位置创建源文件的链接文件,相当于复制了一份,占用资源会倍增。硬链接一旦创建,源文件和链接文件任何一方修改文件都会同步修改。
【相同点】文件都会同步改变
【不同点】软链接指向源文件地址,不占用资源;硬链接复制
【创建方式】将x1目录下的target.py文件链接到x2目录下的新文件。软链接:ln -s ./x1/target.py ./x2/newFile,硬链接就是不加-s

与Window的区别

Linux使用Linux内核,而Windows使用Windows内核。此外,Linux是基于Unix设计的,因此具有Unix风格的文件系统和命令行接口。

创建子进程用什么系统调用

fork() 系统调用会创建一个与父进程几乎完全相同的子进程,包括代码段、数据段、堆栈等,并且在子进程中返回0,在父进程中返回子进程的进程ID。

将前台进程变为子进程怎么做

ctrl + z暂停该进程后,使用 shell 的内置命令 bg 。或者直接在命令后加&号,这样可以使得进程在后台运行而不阻塞 shell 的输入输出。

Shell 是一个命令行界面,以文本方式输入命令并获取输出结果的方式,大多数Linux常用的shell就是Bash

setid的作用

用于创建一个新的会话(session)并设置进程组 ID,也就是创建守护进程(daemon process)或者在后台运行的进程。它可以确保这些进程不再受控于任何终端,并且能够独立地运行和管理。假设我们有一个程序 my_program,我们希望将其作为守护进程在后台运行,不受终端的影响。可以通过 在启动指令的头部加入setsid来实现。

磁盘空间占满问题快速定位并解决

dflsofdu

  1. df -h 查看是哪个挂在目录满了,常常是根目录/占满
[root@test ~]# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 25G 22G 54% /
tmpfs 7.8G 0 7.8G 0% /dev/shm
  1. 快速定位一下应用日志大小情况
    比如tomcat日志,应用系统自己的日志等。

  2. 如果能直观地看到日志文件过大,则酌情进行删除。有时候删除日志文件之后再df -h查看空间依然被占满,继续排查。
    lsof file_name 查看文件占用进程情况,如果删除的日志正在被某个进程占用,则必须重启或者kill掉进程。

[root@test ~]# lsof /usr/local/apache-tomcat-7.0.54/logs/catalina.out
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 7053 root 1w REG 202,1 958123 1180888 /usr/local/apache-tomcat-7.0.54/logs/catalina.out
java 7053 root 2w REG 202,1 958123 1180888 /usr/local/apache-tomcat-7.0.54/logs/catalina.out
  1. 如果不能直观地排除出是某个日志多大的原因,就需要看一下指定目录下的文件和子目录大小情况,使用du命令。
    du -h --max-depth=1 path | sort -hr #查看目录大小并按照大小倒序展示
[root@test ~]# du -h --max-depth=1 /usr/local/ | sort -hr
2.6G    /usr/local/
1.1G    /usr/local/mysql
358M    /usr/local/jdk1.8.0_121
......

内存泄漏排查

使用vmstat查看一下当前系统的内存使用情况,如果出现buffcache两列的内容是基本保持不变的,free 列的数据是递减的。这种情况下,貌似是发生了内存泄漏,但是也不敢肯定,因为内存的开辟存在开辟回收的情况,可能是其他进程在申请内存。所以用memleak工具,它会跟踪系统或者指定进程的内存分配、释放请求,根据它显示的堆栈信息就可以定位到是哪个进程的哪个地方出现了内存泄漏。

一般把程序的虚拟内存分为以下几个部分:堆栈、只读段、数据段以及内存映射段,其中堆、内存映射段可能回发生内存泄漏。

  • 堆:堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要库函数去调用free()自己释放,如果没有释放,就会造成内存泄漏。
  • 内存映射段:包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。所以,在程序分配后忘了回收,就会导致跟堆内存类似的泄漏问题。

CPU过高排查

jps+top 定位应用进程 pid -> top -Hp {pid}找到线程 tid -> 将 tid 转换成十六进制 printf “%x\n” {tid} -> 打印堆栈信息 jstack {tid} | grep 3a3c -A10
堆栈信息会显示在哪一行出现幺蛾子,进入代码验证即可

网络编程常用API

  1. Socket API 创建一个新的套接字(socket),返回一个套接字描述符。
    int socket(int domain, int type, int protocol);
    domain: 协议族,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(Unix域套接字)。
    type: 套接字类型,如 SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。
    protocol: 通常设置为 0,系统根据 domain 和 type 自动选择合适的协议。
  2. Bind API 将套接字绑定到一个本地地址(IP 地址和端口)。
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    sockfd: 由 socket() 创建的套接字描述符。
    addr: 本地协议地址,通常是一个 sockaddr_in 结构体。
    addrlen: 地址的长度。
  3. Listen API 将一个套接字设为被动监听模式,准备接受传入连接。
    int listen(int sockfd, int backlog);
    sockfd: 套接字描述符。
    backlog: 等待队列的最大长度。
  4. Accept API 从监听的套接字队列中接受一个传入的连接。
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    sockfd: 监听套接字描述符。
    addr: 客户端的协议地址。
    addrlen: 地址结构体的大小。
  5. Connect API 将客户端套接字连接到一个远程服务器。
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    sockfd: 客户端套接字描述符。
    addr: 远程服务器的协议地址。
    addrlen: 地址结构体的大小。
  6. Send/Recv API 发送数据到已连接的套接字。
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    recv(): 从已连接的套接字接收数据。
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    sockfd: 已连接的套接字描述符。
    buf: 指向数据缓冲区的指针。
    len: 缓冲区的大小。
    flags: 发送或接收数据时的标志位。
  7. Sendto/Recvfrom API (用于UDP通信)
    发送数据到指定地址的套接字。
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    recvfrom(): 接收来自指定地址的数据。
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  8. Close API 关闭套接字。
    int close(int sockfd);
    sockfd: 套接字描述符。
  9. Shutdown API 关闭套接字的部分或全部连接。
    int shutdown(int sockfd, int how);
    sockfd: 套接字描述符。
    how: 关闭方式,如 SHUT_RD(停止读操作)、SHUT_WR(停止写操作)、SHUT_RDWR(停止读写操作)。
  10. Setsockopt/Getsockopt API 设置套接字选项。
    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    getsockopt(): 获取套接字选项。
    int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
    level: 选项的协议级别(如 SOL_SOCKET)。
    optname: 选项名,如 SO_REUSEADDR、SO_KEEPALIVE。
    optval: 指向选项值的指针。
    optlen: 选项值的大小。
  11. Select/Poll/epoll API 监视多个文件描述符,等待其中任何一个文件描述符变为可读、可写或出现异常。
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    poll(): 类似select(),但支持更大范围的文件描述符监控。
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    epoll: 更高效的多路复用I/O监控机制,适用于高并发网络服务器。
    epoll_create()、epoll_ctl()、epoll_wait() 是epoll的关键API。
  12. inet_ntop/inet_pton API: 将网络字节序的IP地址转换为点分十进制的字符串形式。
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    inet_pton(): 将点分十进制的IP地址字符串转换为网络字节序的地址。
    int inet_pton(int af, const char *src, void *dst);
  13. getaddrinfo/freeaddrinfo API
  14. 提供更灵活的域名解析功能,支持IPv4和IPv6。
    int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
    freeaddrinfo(): 释放getaddrinfo()分配的地址信息链表。
    void freeaddrinfo(struct addrinfo *res);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值