进程线程拟面试题
1.进程与线程的区别,以及实际什么场景运用它们
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 资源分配的基本单位,拥有独立地址空间、文件描述符等 | 调度执行的基本单位,共享所属进程的资源 |
| 上下文切换 | 开销大(需切换页表、内存空间) | 开销小(仅保存少量寄存器) |
| 通信方式 | IPC(管道、消息队列、共享内存等) | 直接读写进程全局变量即可(需同步) |
| 安全性 | 进程间隔离,一个进程崩溃不影响其他进程 | 线程间不隔离,一个线程崩溃会导致整个进程退出 |
(场景)
进程:需要高隔离性的场景,如多用户服务、独立插件、需要崩溃隔离的任务。
线程:轻量级并发场景,如Web服务器、数据库连接池、实时数据处理。
2.fork 函数如何使用?为何 fork 的返回值如此设计?
使用:pid_t fork(void);,调用后创建一个与父进程几乎完全相同的子进程,子进程从 fork 调用处继续执行。
返回值设计:
父进程:返回子进程的 PID(>0)
子进程:返回 0
失败:返回 - 1
设计原因:
父子进程需要区分身份,执行不同逻辑(如父进程等待子进程、子进程执行新程序)。
子进程没有父进程 PID 的概念,用 0 表示 “当前进程是子进程”;父进程通过子进程 PID 管理子进程。
pid_t fork(void);
是UNIX或类UNIX的系统调用,用于创建子进程。若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID(PID);否则,出错返回-1。UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。
如此返回的原因是任何子进程都只有一个父进程,而且子进程总是可以通过getppid函数获取父进程ID,相反一个父进程可以有多个子进程,而且无法获取各子进程ID。如果父进程想跟踪所有的子进程,则需要记录每次调用fork函数的返回值。
execl(
"/bin/ls", // 1. 要执行的程序的 绝对路径
"ls", // 2. 新程序的名字(argv[0])
"-l", // 3. 传给 ls 的参数(argv[1])
NULL // 4. 必须以 NULL 结尾!
);
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// fork失败
perror("fork failed");
return 1;
}
else if (pid == 0) {
// 子进程执行的代码
printf("我是子进程,PID=%d,父进程PID=%d\n",
getpid(), getppid());
// 通常子进程会调用exec族函数执行新程序
execl("/bin/ls", "ls", "-l", NULL);
}
else {
// 父进程执行的代码
printf("我是父进程,PID=%d,子进程PID=%d\n",
getpid(), pid);
// 父进程通常等待子进程结束
wait(NULL);
}
return 0;
}
3.什么是虚拟地址?有什么作用?
定义:操作系统为每个进程提供的独立、连续的逻辑地址空间,与物理地址通过页表 / 段式映射。(页表的作用就是告诉硬件:程序的某一段虚拟地址,究竟对应着真实物理内存的哪个位置。)
作用:
地址隔离:每个进程地址空间独立,互不干扰,避免非法访问。
内存管理:支持虚拟内存(如交换分区),让进程可使用超过物理内存的地址空间。
内存保护:通过页表设置权限(读 / 写 / 执行),防止进程越权访问。
简化开发:进程无需关心物理内存布局,直接使用连续的虚拟地址。
1、保护物理地址,不让程序直接访问
2、把多个零散的物理地址,映射成一个连续的虚拟地址
3、当内存条不够用,把磁盘的一段空间当成内存使用
虚拟地址是操作系统通过MMU硬件为每个进程提供的独立连续的从零开始的连续地址空间。
它的核心作用有四点:
1. 隔离保护:每个进程有独立空间,无法互相干扰
2. 连续视图:进程看到连续内存,实际物理内存可分散
3. 内存扩展:通过虚拟内存运行比物理内存大的程序
4. 高效共享:代码段、动态库可在进程间共享物理页
5.内存保护:通过页表设置权限(读 / 写 / 执行),防止进程越权访问。
这解决了直接操作物理内存的三大难题:地址冲突、内存碎片、安全问题,是现代操作系统的基石。”
4.简述写时拷贝技术与计数技术
在早期Linux内核设计时,每创建一个新的子进程则会直接复制父进程的地址空间,如果创建子进程数量过多会极大的消耗掉内存空间,在3.0之后版本的内核中引入了写实拷贝技术,该技术在创建子进程时并不会立即复制整个父进程地址空间,此时子进程与父进程是共享同一个地址空间,直到其中某个进程执行“写”操作时,才会创建一个新的地址空间。
在创建子进程时,所有由父进程打开的文件描述符也会被复制到子进程中,此时会有两个文件描述符在内核中指向同一个file结构体,file结构体中包含一个引用计数器,该计数器会自增,当其中一个进程调用close()关闭文件时,计数器会自减,只有减到0的时候才会真正关闭该文件。
5.为何进程需要有僵尸态?描述几种僵尸子进程回收的方法
定义:进程退出后,父进程尚未调用wait()/waitpid()回收其 PCB(进程控制块)资源,此时进程处于僵尸态。
存在原因:父进程需要获取子进程的退出状态(如返回值、终止信号)。 内核需要保留子进程的 PID 和退出状态,直到父进程读取。
回收僵尸子进程可使用 pid_t wait(int *status); 函数,功能为回收僵尸态子进程,如果子进程还未结束则阻塞等待,如果没有子进程则立刻返回。
或使用pid_t waitpid(pid_t pid, int *status, int options);(第一个参数:pid决定等哪个子进程函数;*status传出参数;0:阻塞等待(最常用)//WNOHANG:非阻塞,没有子进程退出就立刻返回 0),功能为按照指定的方式探测指定子进程状态改变,回收僵尸态子进程。可通过配置options参数改成非阻塞方式等待。
6.线程间有哪些资源是共享的?哪些资源是独有的?
共享资源(同一进程内的所有线程共享):
进程的地址空间(代码段、数据段、堆)
文件描述符表、信号处理方式、当前工作目录
用户 ID、组 ID、进程 ID
独有资源(每个线程独立拥有):
线程 ID(TID)、线程栈(局部变量、函数调用栈)
寄存器上下文(PC、SP 等)、errno 变量
信号掩码、调度优先级、线程私有数据(TSD)
共享资源:所属进程的地址空间(代码段、数据段、堆)、文件描述符、信号处理方式、当前工作目录、用户 ID / 组 ID。
独有资源:线程 ID(TID)、线程栈(局部变量)、寄存器上下文(PC、SP 等)、errno 变量、信号屏蔽字、调度优先级。
| 关系 | 共享内容 | 独享内容 |
|---|---|---|
| 进程 vs 进程 | 内核;靠 IPC 主动通信 | 全部用户地址空间、栈、堆、PID、文件描述符 |
| 父子进程 fork | 文件描述符、内核、信号属性 | PID、地址空间 (写时复制)、栈、执行位置 |
| 同进程多线程 | 代码、全局 / 静态、堆、文件描述符、PID | 线程栈、局部变量、寄存器、线程 ID |
7.进程间通信有哪些?各有哪些特点?
| 通信方式 | 特点 | 适用场景 |
|---|---|---|
| 管道(Pipe) | 半双工、单向通信,仅支持父子 / 兄弟进程,无格式 | 简单的进程间单向数据传输 |
| 命名管道(FIFO) | 半双工,支持无亲缘关系的进程,以文件形式存在 | 跨进程的稳定单向通信 |
| 消息队列 | 内核维护消息链表,支持双向通信、按类型收发,有边界 | 进程间结构化数据传输,支持异步 |
| 共享内存 | 多个进程映射同一块物理内存,速度最快,需同步 | 大数据量、高频次数据交换 |
| 信号量 | 计数器,用于进程 / 线程间同步互斥,不传输数据 | 控制多个进程访问共享资源 |
| 信号(Signal) | 异步通信,仅传递信号类型,数据量极小 | 进程间的通知、中断处理 |
| 套接字(Socket) | 支持本地(Unix 域)和网络通信,跨主机,双向 | 本地进程通信或跨主机通信 |
管道(Pipe):单向通信,适用于有亲缘关系的进程间通信。
命名管道(Named Pipe):允许无亲缘关系的进程间进行通信。
共享内存(Shared Memory):多个进程共享同一块内存区域,实现高效的数据交换。
消息队列(Message Queue):消息的链表,允许多个进程向同一个队列发送消息。
信号(Signal):用于通知进程发生了某个事件,如中断、异常等
信号量(Semaphore):用于进程间同步和互斥访问共享资源,控制进程对共享资源的访问。
套接字(Socket):用于不同主机间的进程通信,可以实现网络通信
8.守护进程的功能是什么?如何创建守护进程?
功能:
长期在后台运行,不与终端关联,不受终端退出影响。
常用于系统服务,如日志服务、网络服务、定时任务服务等。
创建步骤:
fork () 创建子进程,父进程退出:让子进程成为孤儿进程,脱离终端控制。
setsid () 创建新会话:让子进程成为会话首进程,脱离原会话、进程组和终端。
忽略SIGHUP信号signal(SIGHUP,SIG_IGN)
再次 fork ():防止进程重新打开终端设备,确保进程无法再关联终端。
修改工作目录:将当前工作目录改为/或其他稳定目录,避免占用挂载点(避免占用挂载点是指在Linux或Unix系统中,确保某个目录(通常是空的)不被其他进程或文件占用,以便将其作为挂载点(mount point)使用)。
重设文件权限掩码:调用umask(0),避免继承父进程的权限掩码,保证文件创建权限可控。
关闭不需要的文件描述符:关闭标准输入、输出、错误文件描述符,或重定向到/dev/null。

9.线程同步实现方法及代码编写,两个线程交替打印AB
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
sem_t read_sem, write_sem; // 定义两个信号量
void *read_thread(void *arg)
{
for (int i = 0; i < 10000; ++i) {
sem_wait(&read_sem); // 从 read_sem 中申请资源(P操作),无资源则阻塞
printf("AAAA\n");
sem_post(&write_sem); // 释放资源到 write_sem(V操作),唤醒等待线程
}
}
void *write_thread(void *arg)
{
for (int i = 0; i < 10000; ++i) {
sem_wait(&write_sem); // 从 write_sem 中申请资源(P操作),无资源则阻塞
printf("BBBB\n");
sem_post(&read_sem); // 释放资源到 read_sem(V操作),唤醒等待线程
}
}
int main(int argc, const char *argv[])
{
// 初始化信号量:第二个参数0表示线程间共享,第三个参数为初始资源数
sem_init(&read_sem, 0, 1);
sem_init(&write_sem, 0, 0);
pthread_t tid[2];
int ret = pthread_create(&tid[0], NULL, read_thread, NULL);
if (ret != 0) {
printf("create thread err! errcode: %d\n", ret);
exit(1);
}
ret = pthread_create(&tid[1], NULL, write_thread, NULL);
if (ret != 0) {
printf("create thread err! errcode: %d\n", ret);
exit(1);
}
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
sem_destroy(&read_sem);
sem_destroy(&write_sem);
return 0;
}
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
// 标记:0 轮到A,1 轮到B
int flag = 0;
// 线程1 打印 A
void *printA(void *arg)
{
for (int i = 0; i < 10; i++)
{
pthread_mutex_lock(&mutex);
// 不是自己的轮次,就等待
while (flag != 0)
{
pthread_cond_wait(&cond, &mutex);
}
printf("A ");
// 切换轮到B
flag = 1;
// 唤醒另一个线程
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
// 线程2 打印 B
void *printB(void *arg)
{
for (int i = 0; i < 10; i++)
{
pthread_mutex_lock(&mutex);
while (flag != 1)
{
pthread_cond_wait(&cond, &mutex);
}
printf("B ");
flag = 0;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_create(&t1, NULL, printA, NULL);
pthread_create(&t2, NULL, printB, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
10.简述线程池的概念和作用
概念:
线程池是一种管理和复用线程的机制,它包含一组预先创建的线程,用于执行任务队列中的任务。
线程池的作用包括:
提高性能:减少线程创建和销毁的开销,避免频繁创建线程带来的性能损耗,提高系统的响应速度和吞吐量。
控制并发度:通过限制线程池中线程的数量,可以控制系统的并发度,避免因线程过多导致系统资源耗尽或性能下降。
管理线程:线程池可以统一管理线程的生命周期,包括线程的创建、销毁、复用和调度,简化了线程管理的复杂性。
提高系统稳定性:线程池可以有效地管理系统资源,避免因线程过多导致系统崩溃或资源耗尽的情况,提高系统的稳定性和可靠性。
减少资源消耗:线程池可以重复利用线程,减少线程创建和销毁的开销,降低系统资源消耗,提高系统的效率和性能。
11.说出进程的状态,以及状态字符缩写,并描述各状态之间是如何转换的?
| 状态 | 缩写 | 含义 |
|---|---|---|
| 运行态 | R | 正在 CPU 上运行,或在运行队列中等待调度 |
| 可中断睡眠态 | S | 等待事件,可被信号唤醒 |
| 不可中断睡眠态 | D | 等待 IO 完成,不可被信号唤醒 |
| 停止态 | T | 被信号暂停(如 SIGSTOP) |
| 僵尸态 | Z | 进程已退出,等待父进程回收 |
| 死亡态 | X | 进程已被回收,仅 PCB 残留 |
状态转换:
新建→就绪:进程创建完成,进入就绪队列等待调度。
就绪→运行:调度器选中进程,分配 CPU 时间片,进入运行态。
运行→就绪:时间片用完,或被更高优先级进程抢占 CPU,回到就绪队列。
运行→睡眠(S/D):进程等待 IO 或资源,主动放弃 CPU,进入睡眠态。
睡眠→就绪:等待的事件发生(如 IO 完成、信号到达),进程被唤醒,进入就绪队列。
运行→停止:收到 SIGSTOP 等信号,进程暂停执行,进入停止态。
停止→就绪:收到 SIGCONT 信号,进程恢复执行,进入就绪队列。
运行 / 就绪 / 睡眠 / 停止→僵尸:进程执行完毕或收到退出信号,退出并进入僵尸态,等待父进程回收。
僵尸→死亡:父进程调用 wait () 回收子进程,进程 PCB 被释放,进入死亡态。

12.线程的属性有哪些?他们有什么区别?如何设置线程的属性?
| 属性 | 含义 | 可选值 |
|---|---|---|
| 分离状态 | 线程退出后是否需要其他线程回收资源 | 分离态(PTHREAD_CREATE_DETACHED)、可结合态(PTHREAD_CREATE_JOINABLE) |
| 调度策略 | 线程的调度方式 | SCHED_FIFO、SCHED_RR、SCHED_OTHER |
| 调度优先级 | 线程的调度优先级 | 对应策略下的优先级范围 |
| 栈大小 | 线程栈的大小 | 自定义大小(默认由系统决定) |
| 栈地址 | 线程栈的起始地址 | 自定义地址(需手动分配内存) |
区别:
分离态线程退出后自动释放资源,无需其他线程 join;可结合态线程需被其他线程 join 才能释放资源,否则会成为 “线程僵尸”。
调度策略影响线程被 CPU 调度的方式,实时策略(FIFO/RR)优先级高于普通策略(OTHER)。
设置方法:
定义pthread_attr_t变量,调用pthread_attr_init(&attr)初始化。
调用对应设置函数,如pthread_attr_setdetachstate()、pthread_attr_setschedpolicy()、pthread_attr_setstacksize()等。
创建线程时传入&attr参数,如pthread_create(&tid, &attr, func, arg)。
调用pthread_attr_destroy(&attr)销毁属性对象。
13.死锁是如何产生的?怎么避免死锁的产生
死锁是指两个或多个进程(线程)在互相等待对方释放资源,导致所有相关进程都无法继续执行的状态。
1.死锁的产生条件必须满足以下四个:
互斥条件:资源在同一时刻只能被一个进程或线程使用。
请求与保持条件:一个进程在等待资源时,会保持已经获得的资源,同时继续请求其他资源。
不剥夺条件:已经获得的资源在未使用完之前,不能被剥夺或抢占。
循环等待条件:存在一个进程资源请求的环形链,即一个进程等待另一个进程占用的资源,而后者又在等待前者占用的资源。
2.避免死锁的产生只需要打破以上任意一个条件即可,其中第一个条件不可打破。
打破请求与保持条件:资源分配策略上采取措施,给资源设定优先级,申请顺序必有由高到低,或者将多个资源整合成一个资源。
打破不剥夺条件:使用可剥夺资源,允许关键资源共享。
打破循环等待条件:使用顺序锁或轮询锁,确保资源正确获取和释放。
3.如果无法打破条件,产生死锁后如何解除有以下方式:
检测死锁:当死锁发生时,系统能够检测到并采取措施修复,如重启服务或终止线程。
超时重试:设置超时机制,当进程等待资源超时时,释放已占有的资源。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)