【秋招】嵌入式面试八股文 - 操作系统 篇
本文为 第六章操作系统 部分,具体整篇目录可以看前言!
第一部分(纯八股)
1 什么是进程?什么是线程?
进程(Process):
- 操作系统资源分配的基本单位
- 拥有独立的内存空间和系统资源
- 包含代码、数据、堆栈等
线程(Thread):
- CPU调度的基本单位
- 共享所属进程的内存空间和资源
- 只拥有必要的运行资源(如程序计数器、寄存器和栈)
主要区别:
资源占用 |
占用独立内存空间 |
共享进程内存空间 |
通信开销 |
较大(需IPC机制) |
较小(直接访问共享数据) |
创建开销 |
较大 |
较小 |
切换开销 |
较大(需保存/恢复更多上下文) |
较小 |
安全性 |
较高(相互独立) |
较低(共享内存可能导致冲突) |
并发性 |
多进程并发 |
多线程并发 |
2 内核线程和用户线程的区别?
- 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
- 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
- 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
- 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
- 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。
3 进程和线程有什么区别?
3.1 根本区别
- 进程是资源分配的基本单位,线程是程序执行的最小单位
3.2 资源开销
- 进程有自己独立地址空间(代码空间和数据空间),每启动一个进程,系统会为它分配地址空间。
- 线程没有自己独立的地址空间,线程共享进程中的数据,使用相同的地址空间。每个线程都有自己的堆栈。
- 线程切换的资源开销要比进程小。涉及频繁切换,就选择线程。
- 线程开销小,但是不利于进行资源包含;进程开销大,但是有利于资源保护。
3.3 关于通信
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。(ftok函数)
- 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也跟着死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
3.4 执行过程
- 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
4 何时使用多进程,何时使用多线程?
- 对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
- 要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
5 进程的几种状态?
关于IO:
- 用户程序进行IO的读写,会用到read&write两大系统调用。read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。
同步IO:
- 是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。
- 是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。
- R (Running/Runnable):正在运行或就绪状态
- S (Sleeping):可中断睡眠,等待某事件完成
- D (Disk Sleep):不可中断睡眠,通常是等待I/O
- Z (Zombie):僵尸状态,进程已终止但父进程未回收
- T (Stopped):进程被暂停,如收到SIGSTOP信号
- X (Dead):进程即将被销毁
状态转换:
- R → S:进程等待资源或事件
- S → R:等待的事件发生,进程被唤醒
- R → D:进程请求不可中断的操作,如磁盘I/O
- D → R:不可中断操作完成
- R → Z:进程终止,但父进程未调用wait()
- R/S → T:进程收到SIGSTOP信号
- T → R:进程收到SIGCONT信号
6 进程创建的方式?
6.1 使用fork创建
fork()
是Unix/Linux中创建进程的传统方法:
#include <unistd.h> pid_t fork(void);
特点:
- 创建调用进程的副本(子进程)
- 子进程获得父进程数据空间、堆、栈的副本
- 父子进程共享代码段(只读)
- 采用写时复制(Copy-On-Write)技术优化内存使用
- 返回值:父进程中返回子进程PID,子进程中返回0,失败返回-1
示例:
#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid < 0) { // 创建失败 perror("fork failed"); return 1; } else if (pid == 0) { // 子进程 printf("子进程,PID: %d,父进程PID: %d\n", getpid(), getppid()); } else { // 父进程 printf("父进程,PID: %d,子进程PID: %d\n", getpid(), pid); } return 0; }
6.2 vfork系统调用
vfork()
是fork()
的一种特殊形式:
#include <unistd.h> pid_t vfork(void);
特点:
- 创建进程的目的是执行exec系列函数
- 子进程与父进程共享地址空间(不使用写时复制)
- 父进程会被挂起,直到子进程调用exec或exit
- 子进程必须小心不修改共享的变量
- 现代系统中,fork的性能已经很好,vfork使用较少
示例:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = vfork(); if (pid < 0) { // 创建失败 perror("vfork failed"); return 1; } else if (pid == 0) { // 子进程 printf("子进程,PID: %d\n", getpid()); // 子进程必须调用exec或exit _exit(0); // 注意使用_exit而非exit } else { // 父进程 printf("父进程,PID: %d,子进程PID: %d\n", getpid(), pid); } return 0; }
6.3 clone系统调用
clone()
是Linux特有的系统调用,提供更细粒度的控制:
#define _GNU_SOURCE #include <sched.h> int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);
特点:
- 允许选择父子进程间共享的资源(如文件描述符、信号处理等)
- 是实现线程的基础(pthread库在底层使用clone)
- 通过flags参数控制共享程度
- 常用flags: CLONE_FILES:共享文件描述符表CLONE_FS:共享文件系统信息CLONE_VM:共享内存空间CLONE_SIGHAND:共享信号处理函数
示例:
#define _GNU_SOURCE #include <stdio.h> #include <sched.h> #include <stdlib.h> #include <unistd.h> #define STACK_SIZE (1024 * 1024) // 1MB栈空间 // 子进程执行的函数 int child_func(void *arg) { printf("子进程,PID: %d\n", getpid()); return 0; } int main() { // 分配栈空间,栈向下增长 void *stack = malloc(STACK_SIZE); if (!stack) { perror("malloc failed"); return 1; } // 创建子进程 pid_t pid = clone(child_func, (char*)stack + STACK_SIZE, CLONE_VM | SIGCHLD, NULL); if (pid < 0) { perror("clone failed"); free(stack); return 1; } printf("父进程,PID: %d,子进程PID: %d\n", getpid(), pid); // 等待子进程结束 waitpid(pid, NULL, 0); free(stack); return 0; }
6.2 fork和vfork的区别?
- void exit( int status) 结束当前进程并将status返回;exit结束进程会刷新缓冲区
- void _exit(int status) 这个不会进行缓冲区刷新
注意:
- 内存共享: fork():子进程获得父进程内存的副本(写时复制)vfork():子进程与父进程共享内存空间
- 执行顺序: fork():父子进程执行顺序不确定vfork():父进程挂起,直到子进程调用exec或exit
- 使用场景: fork():通用进程创建vfork():创建进程后立即执行exec
- 安全性: vfork()更危险,子进程可能破坏父进程的数据
- 性能: 历史上vfork()更高效,现代系统中差异不大
6.3 为什么子进程中使用exit()可能导致问题,而应该使用_exit()?
exit()
会执行清理操作,包括刷新标准IO缓冲区和调用atexit()注册的函数- 在vfork()创建的子进程中,由于共享地址空间,使用exit()可能会: 清空父进程的IO缓冲区执行父进程注册的atexit()函数关闭父进程需要的文件描述符
_exit()
直接调用系统调用退出,不执行用户空间清理操作- 因此,vfork()后的子进程应使用_exit()或exec(),避免干扰父进程
6.4 调度策略和优先级
Linux支持多种调度策略:
- SCHED_OTHER(CFS):普通进程的默认策略
- SCHED_BATCH:批处理进程,不需要交互
- SCHED_IDLE:优先级最低的进程
- SCHED_FIFO:实时进程,先进先出,不会被抢占(除非被更高优先级进程)
- SCHED_RR:实时进程,时间片轮转
优先级系统:
- nice值:范围-20到19,值越小优先级越高,默认为0
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【秋招】嵌入式八股文最全总结 文章被收录于专栏
双非本,211硕。本硕均为机械工程,自学嵌入式,在校招过程中拿到小米、格力、美的、比亚迪、海信、海康、大华、江波龙等offer。八股文本质是需要大家理解,因此里面的内容一定要详细、深刻!这个专栏是我个人的学习笔记总结,是对很多面试问题进行的知识点分析,专栏保证高质量,让大家可以高效率理解与吸收里面的知识点!掌握这里面的知识,面试绝对无障碍!