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.如果无法打破条件,产生死锁后如何解除有以下方式:

检测死锁:当死锁发生时,系统能够检测到并采取措施修复,如重启服务或终止线程。

超时重试:设置超时机制,当进程等待资源超时时,释放已占有的资源。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐