C++ Linux系统编程基础面试题

1. Linux进程和线程的区别?如何创建?

答案:

本质区别:

  • 进程是资源分配的基本单位,线程是CPU调度的基本单位
  • 进程拥有独立的地址空间,而线程共享所属进程的地址空间
  • 进程间切换开销大(需要切换页表、刷新TLB等),线程切换开销小
  • 进程间通信需要特殊的IPC机制,线程间可以直接读写进程数据段(如全局变量)

资源占用:

  • 进程有独立的代码段、数据段、堆、栈
  • 同一进程的线程共享代码段、数据段、堆,但每个线程有独立的栈和寄存器
  • 创建进程需要分配独立的内存空间,开销大
  • 创建线程只需要分配栈空间,开销小得多

通信方式:

  • 进程间通信:管道、消息队列、共享内存、信号量、Socket等
  • 线程间通信:直接访问共享变量,配合互斥锁、条件变量等同步机制

创建方式:

  • 进程:使用fork()系统调用,子进程是父进程的副本
  • 线程:使用pthread_create(),新线程与创建者共享地址空间

fork的写时复制机制:

  • fork后父子进程共享物理内存页
  • 只有在某个进程写入时才真正复制该页
  • 大大提高了fork的效率

实际应用场景:

  • 多进程:需要隔离性、稳定性高的场景(如Nginx的多进程模型)
  • 多线程:需要频繁通信、共享数据的场景(如Web服务器处理请求)

2. Linux进程间通信(IPC)有哪些方式?各有什么特点?

答案:

Linux提供了多种进程间通信方式,各有特点和适用场景:

1. 管道(Pipe)

  • 特点:半双工通信,数据只能单向流动;只能用于有亲缘关系的进程(父子进程)
  • 原理:内核维护一个缓冲区,一个进程写入,另一个进程读取
  • 优点:简单易用,由内核管理
  • 缺点:只能单向通信,需要双向通信要创建两个管道;只能用于父子进程
  • 应用场景:Shell命令管道(如 ls | grep)

2. 命名管道(FIFO)

  • 特点:有文件名的管道,存在于文件系统中
  • 优点:可以用于任意两个进程间通信,不要求有亲缘关系
  • 缺点:仍然是半双工;读写需要同步
  • 应用场景:不相关进程间的简单通信

3. 消息队列(Message Queue)

  • 特点:消息的链表,存放在内核中
  • 优点:可以按消息类型接收;不需要同步;消息有边界
  • 缺点:有大小限制;不适合大数据量传输
  • 应用场景:需要按类型处理消息的场景

4. 共享内存(Shared Memory)

  • 特点:多个进程映射到同一块物理内存
  • 优点:最快的IPC方式,不需要数据拷贝
  • 缺点:需要额外的同步机制(如信号量);容易出现竞态条件
  • 应用场景:大量数据交换、高性能要求的场景
  • 注意:必须配合信号量或互斥锁使用,否则会有数据竞争

5. 信号量(Semaphore)

  • 特点:计数器,用于进程同步
  • 作用:主要用于保护共享资源,控制访问顺序
  • 操作:P操作(等待/减1)、V操作(释放/加1)
  • 应用场景:配合共享内存使用,实现进程同步

6. 信号(Signal)

  • 特点:异步通知机制,软件层面的中断
  • 优点:简单,可以通知进程发生了某个事件
  • 缺点:不能传递复杂数据,只能传递信号类型
  • 应用场景:进程控制(如Ctrl+C发送SIGINT)、异常处理

7. 套接字(Socket)

  • 特点:可用于网络通信,也可用于本地通信(Unix Domain Socket)
  • 优点:功能强大,支持网络通信;全双工
  • 缺点:相对复杂,开销较大
  • 应用场景:网络通信、客户端-服务器模型

性能对比:

  • 共享内存 > 管道 > 消息队列 > Socket
  • 共享内存最快是因为不需要数据拷贝,直接访问内存
  • 其他方式都需要在用户空间和内核空间之间拷贝数据

选择建议:

  • 简单的父子进程通信:管道
  • 大量数据、高性能:共享内存+信号量
  • 需要消息类型:消息队列
  • 网络通信或复杂通信:Socket
  • 进程控制和通知:信号

3. 什么是僵尸进程和孤儿进程?如何避免?

答案:

僵尸进程(Zombie Process):

定义和产生原因:

  • 子进程已经终止,但父进程还没有调用wait()或waitpid()来回收它
  • 进程虽然已死,但进程表项(PCB)还在,占用系统资源
  • 在ps命令中状态显示为Z(Zombie)
  • 无法通过kill命令杀死,因为它已经死了

为什么会产生:

  • 父进程太忙,没来得及调用wait()
  • 父进程编程错误,忘记调用wait()
  • 父进程故意不调用wait()(设计问题)

危害:

  • 占用进程表项,系统进程数有限
  • 大量僵尸进程会导致无法创建新进程
  • 浪费系统资源(虽然不占用内存,但占用PID)

如何避免:

  1. 父进程调用wait()或waitpid():及时回收子进程资源
  2. 信号处理:捕获SIGCHLD信号,在信号处理函数中调用wait()
  3. 两次fork:让子进程立即退出,孙进程由init收养
  4. 忽略SIGCHLD信号:某些系统会自动回收(不推荐)

孤儿进程(Orphan Process):

定义:

  • 父进程先于子进程终止,子进程成为孤儿进程
  • 孤儿进程会被init进程(PID=1)或systemd收养
  • init进程会自动调用wait()回收孤儿进程

特点:

  • 不会造成资源浪费,因为init会负责回收
  • 是正常现象,不是问题
  • 守护进程就是特殊的孤儿进程

区别总结:

  • 僵尸进程:子进程死了,父进程还活着但不回收 → 有害
  • 孤儿进程:父进程死了,子进程还活着 → 无害,由init收养

实际案例:

  • 如果一个长期运行的服务器程序不处理SIGCHLD信号,每次fork出的子进程结束后都会变成僵尸进程
  • 时间长了会积累大量僵尸进程,最终导致系统无法创建新进程
  • 这是很多服务器程序的常见bug

4. 如何创建守护进程?守护进程有什么特点?

答案:

守护进程(Daemon)的定义和特点:

什么是守护进程:

  • 在后台运行的特殊进程,不与任何终端关联
  • 生命周期很长,通常从系统启动一直运行到系统关闭
  • 不受用户登录注销的影响
  • 通常以超级用户权限运行
  • 命名通常以d结尾(如httpd、sshd、mysqld)

为什么需要守护进程:

  • 提供系统服务(如Web服务器、数据库服务器)
  • 执行定期任务(如日志清理、备份)
  • 监控系统状态
  • 不需要用户交互

创建守护进程的步骤:

  1. fork并退出父进程目的:让子进程在后台运行如果是从Shell启动,父进程退出后Shell认为命令已完成
  2. 调用setsid()创建新会话成为新会话的首进程脱离原来的控制终端成为新进程组的组长此时进程没有控制终端
  3. 再次fork并退出父进程(可选但推荐)目的:确保进程不是会话首进程防止进程重新打开控制终端提高安全性
  4. 改变工作目录到根目录使用chdir("/")防止占用可卸载的文件系统避免当前目录被删除导致问题
  5. 重设文件权限掩码使用umask(0)清除继承的文件权限掩码让守护进程创建的文件有明确的权限
  6. 关闭不需要的文件描述符关闭从父进程继承的打开文件特别是标准输入、输出、错误(0、1、2)释放资源,避免占用
  7. 重定向标准输入输出到/dev/null防止守护进程意外读写终端避免输出信息丢失或造成错误

守护进程的管理:

  • 通常将PID写入文件(如/var/run/xxx.pid)
  • 通过信号控制(SIGTERM终止、SIGHUP重新加载配置)
  • 使用systemd或init脚本管理
  • 日志输出到syslog或专门的日志文件

常见的守护进程:

  • sshd:SSH服务
  • httpd/nginx:Web服务器
  • mysqld:MySQL数据库
  • cron:定时任务
  • syslogd:系统日志

5. Linux文件I/O:系统调用和标准I/O的区别?

答案:

系统调用(System Call)- 无缓冲I/O:

特点:

  • 直接调用内核提供的系统调用(open、read、write、close等)
  • 每次调用都会陷入内核态,有上下文切换开销
  • 没有用户空间缓冲,数据直接在内核缓冲区和用户空间之间传输
  • 返回文件描述符(int类型)
  • 更底层,更灵活,但使用相对复杂

优点:

  • 可以精确控制I/O操作
  • 适合对性能要求高的场景
  • 可以使用特殊标志(如O_DIRECT、O_SYNC)
  • 适合二进制文件操作

缺点:

  • 频繁的系统调用开销大
  • 需要手动管理缓冲
  • 代码相对复杂

标准I/O(Standard I/O)- 缓冲I/O:

特点:

  • C标准库提供的I/O函数(fopen、fread、fwrite、fclose等)
  • 在用户空间维护缓冲区,减少系统调用次数
  • 返回文件指针(FILE*类型)
  • 更高层,使用简单,但灵活性较低

缓冲类型:

  1. 全缓冲:缓冲区满了才进行实际I/O操作(普通文件)
  2. 行缓冲:遇到换行符才刷新缓冲区(终端设备)
  3. 无缓冲:立即进行I/O操作(标准错误stderr)

优点:

  • 减少系统调用次数,提高效率
  • 使用简单,API丰富(格式化输入输出)
  • 自动管理缓冲区
  • 可移植性好

缺点:

  • 缓冲可能导致数据延迟写入
  • 不适合需要精确控制的场景
  • 额外的内存开销

性能对比:

  • 小数据量频繁读写:标准I/O更快(减少系统调用)
  • 大块数据读写:系统调用可能更快(减少拷贝)
  • 需要立即写入:系统调用更合适(如日志)

文件描述符的特殊值:

  • 0:标准输入(STDIN_FILENO)
  • 1:标准输出(STDOUT_FILENO)
  • 2:标准错误(STDERR_FILENO)
  • 这三个在进程启动时自动打开

何时使用哪种:

  • 文本文件、格式化I/O:标准I/O
  • 二进制文件、网络编程:系统调用
  • 需要精确控制、高性能:系统调用
  • 简单应用、可移植性:标准I/O

混合使用注意:

  • 不要对同一个文件同时使用系统调用和标准I/O
  • 如果必须混用,需要使用fflush()刷新缓冲区
  • 可以使用fileno()将FILE*转换为文件描述符

6. select、poll、epoll的区别?为什么epoll性能最好?

答案:

这三个都是Linux下的I/O多路复用机制,用于同时监控多个文件描述符的状态。

select:

原理:

  • 将要监控的文件描述符集合从用户空间拷贝到内核空间
  • 内核遍历整个集合,检查是否有就绪的文件描述符
  • 将结果拷贝回用户空间
  • 用户程序再次遍历找出就绪的文件描述符

限制:

  • 最大监控1024个文件描述符(FD_SETSIZE限制)
  • 每次调用都需要拷贝整个fd_set
  • 需要遍历所有文件描述符,时间复杂度O(n)
  • 不能跨平台修改限制

优点:

  • 跨平台,几乎所有Unix系统都支持
  • 超时精度高(微秒级)

缺点:

  • 性能差,不适合大量连接
  • 有最大文件描述符限制

poll:

改进:

  • 使用pollfd结构数组,没有最大文件描述符数量限制
  • 通过events和revents分离输入输出,不会修改原数组
  • 仍然需要遍历所有文件描述符,O(n)复杂度

优点:

  • 没有最大连接数限制
  • 不会修改原数组

缺点:

  • 仍需要拷贝整个pollfd数组
  • 仍需要遍历所有文件描述符
  • 性能仍然不够好

epoll(Linux特有):

革命性改进:

  • 使用事件驱动机制,不需要遍历
  • 内核维护就绪列表,只返回就绪的文件描述符
  • 使用红黑树管理文件描述符,增删改查O(log n)
  • 使用mmap减少内存拷贝

工作模式:

  1. 水平触发(LT,Lev

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

不愿透露姓名的神秘牛友
03-19 10:38
实力求职者:真的绷不住了,第一张霸总人设,第二张求生欲拉满
点赞 评论 收藏
分享
评论
3
8
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务