2025年春招-Linux面经
进程和线程
进程和线程的区别与联系?
进程是操作系统资源分配和调度的基本单位,进程有自己独立的内存空间和系统资源。进程间是相互独立运行的,一个进程崩溃不会影响其他进程。进程的创建需要操作系统分配内存和CPU等资源,销毁时也要进行相应的回收,所以进程的管理开销很大,线程的管理开销则很小。
线程是CPU分配和调度的基本单位,同一进程下的多个线程之间是共享内存和资源的。线程的创建和销毁比进程快。
线程是依附于进程的,换句话说就是线程是在进程里跑的,没有进程就没有线程。一个进程下可能有多个线程,但是一个线程只能属于一个进程。
比如说,有个图像编辑软件,这就是一个进程。软件里可以同时进行多个操作,像调整颜色、裁剪图片啥的,这些不同的操作可以由不同的线程来完成。这样可以提高软件的效率。
而且,线程之间可以很方便地通信和同步。但是也得小心处理同步问题,不然就可能会出现数据不一致或者冲突的情况。
线程池
线程池呢比如说,有个服务器程序,要同时处理好多客户端的请求。要是没有线程池,每次来个请求就新建一个线程,那很快系统资源就被耗尽了,而且创建线程也很费时间。但有了线程池,一开始就创建好一些线程放在那儿,来请求了就分配一个线程去处理,处理完了这个线程又回到线程池里等着下一个任务。
线程池还能控制并发的数量,不会让任务一下子太多把系统压垮。而且管理起来也方便,比如可以设置线程池的大小、调整线程的优先级啥的。
epoll 呢,是一种高效的 I/O 多路复用技术。它能同时监控很多个文件描述符,看看哪个有事件发生,比如有数据可读可写啥的。
把 epoll 和线程池结合起来,那就更厉害了。在一个网络服务器程序里,用 epoll 来监控大量的网络连接,一旦有连接有数据来了,epoll 就会通知程序。这时候线程池里的线程就可以被派出去处理这个事件。
这样做有一些好处。一方面,epoll 能快速地检测到事件,不会像传统的轮询方式那样浪费资源。另一方面,线程池可以高效地处理这些事件,不会因为大量的并发连接而忙得不可开交。线程池里的线程可以重复利用,处理完一个任务马上就可以去处理下一个,大大提高了程序的性能和响应速度。
进程之间的通信方式有哪些?
管道:管道分为匿名管道和命名管道,管道本质上是一个内核中的一个缓存,当进程创建管道后会返回两个文件描述符,一个写入端一个输出端。缺点:半双工通信,一个管道只能一个进程写,一个进程读。不适合进程间频繁的交换数据
消息队列:可以边发边收,但是每个消息体都有最大长度限制,队列所包含的消息体的总数量也有上限并且在通信过程中存在用户态和内核态之间的数据拷贝问题
共享内存:解决了消息队列存在的内核态和用户态之间的数据拷贝问题。
信号量:本质上是一个计数器,当使用共享内存的通信方式时,如果有多个进程同时往共享内存中写入数据,有可能先写的进程的内容被其他进程覆盖了,信号量就用于实现进程间的互斥和同步PV操作不限于信号量+-1,而且可以任意加减正整数
套接字:一种通用的通信机制,可以在不同的主机上的进程之间进行通信。不仅可以实现进程间通信,还可以实现网络间通信。 进程通过创建套接字 绑定地址 监听连接 接受连接 和发送数据等操作来进行通信。套接字可以分为流套接字TCP和数据报套接字UDP
如何实现多进程?/创建线程
在Linux中C++使用fork函数来创建进程
而windows中C++使用createprocess来创建进程
socket
Socket 就像是在网络世界中建立连接的桥梁。它可以让不同的设备在网络上进行通信。Socket 分为客户端和服务器端。客户端发起连接请求,服务器端接收请求并建立连接。一旦连接建立,双方就可以通过 Socket 进行数据的发送和接收。
在 C++ 中,可以使用各种网络编程库来操作 Socket。通过设置 IP 地址和端口号,可以准确地找到要通信的目标设备。
Socket 通信可以是基于 TCP 的,这种方式保证数据的可靠传输,就像我们寄快递一样,确保包裹能准确无误地送到目的地。也可以是基于 UDP 的,这种方式速度快,但不保证数据一定能到达,有点像发个短信,发出去了但不确定对方一定能收到。
在实际应用中,还需要考虑网络延迟、数据丢失等问题,通过一些技术手段来保证通信的稳定性和可靠性。
什么时候用多线程 什么时候用多进程?
多线程进程通常适用于处理的任务相关性较强 会这是需要进行频繁数据共享和通信时。多线程可以让程序更高效的利用CPU,特别是在多核处理器上,可以实现真正的并行处理。频繁的创建和销毁数据优先使用多线程
使用多线程的好处?
并行处理多个操作,节省时间
提高CPU利用率,当一个线程的I/O阻塞,CPu可以切换到另一个线程执行计算,从而保证CPU在高负荷下运行,避免空闲。
可以简化对负责任务的操作,可以为每一个客户端请求创建一个新的线程,这样服务器可以同时处理多个客户端发出的请求。
所有线程可以访问相同的地址空间,实现资源共享。
进程的状态转换
-
新建到就绪:
-
当进程被创建后,初始化完成,获得除CPU外的所有必要资源,它就被放入就绪队列,等待调度运行。
-
-
就绪到运行:
-
当操作系统的调度器从就绪队列中选取进程并为其分配CPU时
-
-
运行到就绪:
-
当进程的时间片用完但仍未完成时,它会被操作系统重新放回就绪队列,等待再次被调度。
-
-
运行到阻塞:
-
如果进程在执行过程中需要等待某个事件(如输入/输出操作),它会从运行状态转为阻塞状态,直到等待的事件被满足。
-
-
阻塞到就绪:
-
当阻塞进程等待的事件发生(如I/O完成),它会被再次放入就绪队列,等待CPU调度。
-
-
运行到终止:
-
当进程完成所有执行任务或因错误或其他操作系统干预需要被终止时,它将从运行状态转为终止状态。
-
孤儿进程和僵尸进程
-
孤儿进程:
孤儿进程是指一个父进程在子进程尚未结束时就已经终止了,此时这些子进程就会成为孤儿进程。为了防止这些孤儿进程失控或占用系统资源,操作系统会将它们重新归属给 **init**
进程(在 Linux 中是 **PID**
为 1 的进程),由 **init**
进程来接管并等待它们的终止。因此,孤儿进程在被 **init**
进程收养后,仍然可以正常运行和终止,不会对系统造成太大的影响。init进程收养并完成状态收集
-
僵尸进程:
僵尸进程是指一个子进程已经终止了,但它的父进程还没有调用 **wait()**
或 **waitpid()**
系统调用来获取子进程的终止状态(也就是回收子进程的资源)。在这种情况下,子进程的描述符依然保留在系统中(子进程就成了僵尸进程),以便父进程能够读取子进程的退出状态。僵尸进程不会占用系统的资源(因为它的内存和大部分资源已经被释放了),但它仍然在进程表中占据一个条目。如果父进程不及时回收,僵尸进程会累积,导致系统的进程表项耗尽。
进程和进程表
进程是正在运行程序的实例,像Web程序 shell都是一个进程,文章编译器typora也是一个进程
操作系统负责管理所有正在运行的进程,操作系统会为每个进程分配特定的时间来占用CPU,操作系统还会为每个进程分配特定的资源。
操作系统为了跟踪每一个进程的活动状态,维护了一个进程表。进程表内部列出每个进程的状态以及每个进程的使用资源。
上下文切换
上下文切换是一种将CPU资源从一个进程分配给另一个进程的机制。计算机并行多个进程,是操作系统通过上下文切换造成的结果。操作系统会存储当前进程的状态,再读取下一个进程的状态。
饥饿
饥饿是指在多任务和并发编程中,一个或多个进程由于种种原因无法获取必需的系统资源。通常这发生在某些进程持续地被其他拥有更高优先级的进程抢占资源导致它们长时间等待,无法继续执行的情况。
简单来说,饥饿就像是你在餐厅等位,但是由于其他顾客不断地插队或者优先被安排,结果你就一直等不到位置。在操作系统中,我们通过改进调度算法或调整优先级来避免或解决饥饿问题,确保所有进程都有机会公平地访问资源。
进程终止的方式
正常退出
错误推出
严重错误
被其他进程杀死
死锁
产生原因
死锁通常是因为多个进程相互等待对方持有的资源而无法继续执行,形成了一个闭环。
四个必要条件同时满足可能发生死锁:
互斥条件:一个资源每次只能被一个进程占用
请求与保持条件:一个进程因请求资源而被阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源在未使用完之前,不能被其他资源强行夺走
循环等待条件:若干进程之间形成资源的循环等待,形成闭环
检测死锁
利用资源分配图(可显示哪些进程请求或占用哪些资源),如果这个图中找到循环依赖情况,存在死锁
解决死锁
-
预防:在程序和系统设计时就尽量避免死锁的四个必要条件之一或多个。比如,使用顺序资源分配策略来避免循环等待。
-
避免:通过算法动态检查资源分配状态,避免系统进入不安全状态。银行家算法就是一个经典的死锁避免算法。
-
检测与恢复:运行时通过检测工具发现死锁后,采取措施解决。比如,暂停部分进程,回滚或者重新分配资源。
恢复死锁
当系统检测到死锁时,通常的恢复方法是终止一些进程或者强制回收一些资源。我们可以选择终止占用资源最多或优先级最低的进程(运行时间最短的进程或者是资源需求量最小的进程),这样可以尽量减少对系统整体运行的影响。如果系统支持,还可以将进程回滚到之前的某个安全状态,让它们从那个点重新开始运行,尽量避免再次发生死锁。
一个 mutex 互斥锁怎么实现?
如果要我实现一个互斥锁,我会用一个变量来表示锁的状态,比如一个简单的布尔值。在锁定操作中,我会检查这个变量,如果没被占用,就把它设置为**true**
,表示锁已经被占用了。如果已经是**true**
,说明锁被别的线程占用了,那当前线程就需要等待。
对于解锁操作,只需要把这个变量设置回**false**
就行了,表示资源已经释放,其他线程可以使用这个锁了。
这是最基本的实现,但在真实应用中,这种方式可能会导致很高的CPU消耗,因为等待锁的线程会在循环中不断检查锁的状态。所以在更复杂的实现中,我们可能会用到操作系统提供的更高级的同步机制,比如信号量或者其他等待/通知机制,来有效减少CPU的负担。
怎样区分内核空间和用户空间
内核空间和用户空间的区分主要在于操作系统对内存的管理方式,以及不同程序或进程对系统资源的访问权限。
内核空间是操作系统核心代码运行的区域。它拥有对所有硬件资源的完全访问权限,能够执行特权指令和管理系统资源。所有的设备驱动程序、内存管理、文件系统等关键服务都在内核空间运行。由于它处理的是系统级任务,内核空间对内存的访问是非常敏感的,任何错误都会导致整个系统崩溃。
用户空间则是普通应用程序运行的区域。应用程序在这个空间里运行时,只能访问被分配给它的内存,不能直接操作硬件或系统资源。用户空间程序通过系统调用与内核进行交互,让内核代表它们执行一些需要特权操作的任务。这种设计确保了即使一个用户空间的程序出现问题,也不会直接影响到整个系统的稳定性。
同一个进程内的线程会共享什么资源?
进程的内存地址 全局变量 堆内存等 使得线程间共享数据和状态信息
文件描述符 打开的文件 打开的数据库等
所有线程可以执行相同的程序代码
进程ID 环境变量 也共享
线程的栈空间是自己独有的
在Linux下栈空间通常是 8M Windows下通常是 1M
虚拟内存
进程中只能访问虚拟内存地址,操作系统会把虚拟内存地址翻译成真实的内存地址。这种内存管理方式,称为虚拟内存。 进程如果访问的是真实的物理地址,多个进程之间会互相干扰。例如一个程序在一个物理地址上写入了一个新值,可能将另一个程序在这个位置的内容擦掉,程序就会崩溃。操作系统为每个进程分配一套独立的虚拟地址,每个进程访问自己的虚拟地址,互不干涉,操作系统会提供将虚拟内存地址和物理内存地址映射的机制。
说一下协程?
协程是一种比线程更加轻量级的执行单元,它允许在一个线程内执行多个任务。协程就是子程序在执行时中断去执行其他的子程序,在适当时候又返回来执行。这种子程序之间的跳转不是函数调用,也不是多线程执行,所以省去了线程切换的开销,效率很高,还不需要线程之间的锁机制,不会发生变量写冲突。
协程的底层如何实现?怎么使用协程?
协程进行中断跳转时会将函数的上下文存放到其他位置,而不是存放在函数的堆栈中,当处理完其他事情跳转回来时,取回上下文继续执行原来的函数。
在执行 malloc 申请内存的时候,操作系统是怎么做的?/内存分配的原理说一下 /malloc 函数底层是怎么实现的?/进程是怎么分配内存的?
**Linux 的 I/0 模型介绍以及同步异步阻塞非阻塞的区别
查询进程占用cpu的命令
top 查看Linux负载
uptime查看Linux负载
w查看Linux负载
vmstat查看Linux负载
top---查看所有进程的资源使用情况,类似于Windows系统中的任务管理器
jobs
---命令来查看当前控制台的后台进程。
<font style="color:rgb(13, 13, 13);">ps</font>
---查看后台进程
<font style="color:rgb(13, 13, 13);">pstree</font>
---命令以树形结构显示进程,这有助于看到进程之间的父子关系
软硬链接的区别?
硬链接和软链接都是文件系统中的一个概念。他们的工作方式不同。硬链接实际上就是文件的别名,与原文件共享一个inode,所以他们的内容和属性一样,修改一个链接的内容其他链接的内容也会改变。硬链接不能跨文件系统也不能连接到目录。(如果硬链接可以指向目录,就可能创建循环,即一个目录内的链接指向其自身或其父目录。)
当你删除一个硬链接时,inode的引用计数会减少。只有当所有指向该inode的链接都被删除,引用计数降到零时,文件系统才会释放该文件占用的空间。
软链接相当于一个快捷方式,相当于一个独立的文件,里面保存的是另一个文件的路径。删除源文件的话,软连接也会失效,因为它指向的路径不存在了。但是软连接可以链接到目录,也可以跨文件系统。
软连接使用绝对路径(存相对路径的话源文件一旦移动就找不到了)
对软链接进行读写操作,系统会自动转换为对源文件的操作(删除链接文件时,删除的是链接文件,不是源文件)
操作系统
操作系统是管理计算机硬件和软件资源的一种应用程序,是用户与计算机硬件之间的接口。
通常情况下计算机上会运行着许多程序,他们需要对内存和cpu进行交互,操作系统的目的是为了保证这些访问能够准确无误的进行。
为什么Linux下的应用程序不直接在Windows下运行?
Linux和Windows系统的格式不同,格式就是协议,就是在固定位置上有意义的数据。
Linux和Windows系统的API不同,Linux中的API被称为系统调用,Windows中的API是开发人员所说的DLL,这是一个库,里面包含代码和数据。Linux中的可执行程序获得系统资源的方法和Windows不一样,所以显然不能在Windows中运行。
-
Linux和Windows使用不同的程序二进制格式。Linux通常使用ELF(可执行和链接格式),而Windows使用PE(可执行的便携式格式)。这意味着即使是相同的源代码,在两个系统上编译后也会生成不同格式的二进制文件,它们不能互相兼容。
用户态和内核态
处于用户态下CPU只能访问内存,不允许访问外围设备。
处于内核态下的CPU可以访问任意数据。
用户态想要到内核态需要通过系统调用,而能够执行系统调用的只有操作系统。一般用户态到内核态的转换称为trap进内核,也成为陷阱指令。
内核
在操作系统中,内核是一个计算机程序,是操作系统的核心,可以控制操作系统中的所有内容。
boot loader
实时系统通常分为两类:
-
硬实时系统(Hard Real-Time Systems):
-
这类系统要求必须严格遵守预定的时间限制。任何任务的延迟或错过截止时间都可能导致系统失败或灾难性后果。例如,汽车安全气囊控制系统、航空控制系统。
-
-
软实时系统(Soft Real-Time Systems):
-
软实时系统对时间的要求较为宽松,偶尔的延迟不会导致严重后果,但可能会降低系统的性能或服务质量。例如,视频播放系统或音频处理系统,偶尔的延迟或跳帧通常是可以接受的。
-
讲一下单例模式、策略模式、工厂模式、观察者模式
单例模式就是确保一个类在整个程序运行过程中只有一个实例。比如,你有一个全局的配置管理器,整个应用程序都要用它来获取配置。如果你用单例模式,这个配置管理器的实例就只有一个,大家都用同一个实例,既能节省资源,也能确保全局状态的一致性。
策略模式是用来定义一系列算法或者行为,然后在运行时选择其中一个去执行。举个例子吧,假设你有一个计算打折的系统,不同的客户群体享受不同的打折策略,你可以用策略模式,把不同的打折逻辑封装成独立的策略类,在运行时选择合适的策略来计算最终价格。这样如果以后有新的打折策略,只需要加一个新的策略类就行了,不用改现有的代码,扩展性非常好。
工厂模式是用来创建对象的。它把创建对象的过程封装起来,提供一个工厂类来负责生产具体的对象,而不是直接在代码里用**new**
去创建。这样做的好处是,你可以在不修改调用代码的情况下更换对象的创建方式,甚至可以创建不同的对象实例。比如,假如你有不同种类的日志系统,可以用工厂模式根据配置或者运行时的需求,动态地创建相应的日志对象。
观察者模式是一种用于对象之间一对多关系的模式。当一个对象的状态发生变化时,所有依赖它的对象都会自动收到通知并更新。常见的例子就是事件监听器,比如你点击按钮后,按钮的状态发生变化,会通知所有注册的监听器去执行相应的操作。这种模式在GUI应用或者订阅发布系统里特别常见。