进程与线程

线程是 CPU 调度的最小单位,进程是资源分配的最小单位。

进程(Process)

  • 资源分配的基本单位

  • 每个进程拥有自己独立的地址空间(代码段、数据段、堆、栈)和文件描述符表。

  • 进程之间不能直接访问彼此的内存空间,只能通过 IPC(如 pipe、shm、socket)通信。

  • 切换进程涉及完整的上下文切换,开销相对较大。

一个进程挂掉不会影响其他进程

线程(Thread)

  • CPU 调度的基本单位

  • 一个线程属于某个进程,多个线程共享进程的地址空间、堆、文件等资源。

  • 每个线程有自己独立的栈空间和寄存器上下文。

  • 使用线程可以更轻量地实现并发(上下文切换比进程快)。

线程之间通信无需系统调用,效率高;但一个线程崩溃可能拖垮整个进程。

协程(coroutine)

协程是在用户态调度的更轻量级的执行单元,涉及系统调用或上下文切换,因此更快,但不适合多核并行。

线程通信 vs 进程通信

线程之间通过共享内存通信,进程之间通信必须借助系统提供的 IPC 机制。

线程通信:基于共享内存 + 同步控制

线程间共享内存,不需要 IPC,但必须通过同步机制保证线程安全。
并发读写数据会引发竞态

通信原理:

  • 所有线程共享同一个进程的地址空间,因此可以直接通过全局变量或堆区共享数据
  • 为避免并发冲突,需使用同步原语(锁)保证一致性。

常见同步机制:

机制 作用
pthread_mutex_t 互斥锁,保证同一时间只有一个线程访问资源
pthread_rwlock_t 读写锁,提高读多写少场景的效率
pthread_cond_t 条件变量,配合互斥锁实现线程间的等待与唤醒
原子操作 无锁同步,如 __sync_add_and_fetch

进程通信 IPC

类别 通信方式 是否支持非亲缘 是否内核参与 是否支持双向
管道类 pipe、FIFO FIFO 支持 pipe 单向,socketpair 双向
消息类 消息队列、信号
共享内存类 shmgetmmap 部分(数据直接进用户态)
同步控制类 信号量、futex 是/否 -
套接字类 UNIX Socket、TCP

Linux 中的进程间通信(IPC)

IPC(Inter-Process Communication)是为了解决不同进程间因内存隔离导致的数据交换障碍,Linux 提供了多种机制供程序选择。

管道类通信(pipe / FIFO)

匿名管道 pipe()

  • 只能用于父子进程。

  • 单向通信,适用于输入输出流传递。

    1
    2
    3
    4
    int pipefd[2];
    pipe(pipefd);
    fork();
    // 子写 pipefd[1],父读 pipefd[0]

    Q:为什么只能父子?
    A:文件描述符继承性决定。

文件描述符

文件描述符表是进程访问 I/O 资源的索引表,是用户空间与内核空间连接的桥梁。

FD 含义 对应文件流
0 标准输入 stdin 输入(键盘)
1 标准输出 stdout 输出(终端)
2 标准错误 stderr 错误输出
1
2
3
4
5
用户进程
└─ 文件描述符表(fd=0,1,2,...) ← 每个进程独立
└─ 文件表(file structure) ← 多进程可共享
└─ inode 表(inode structure) ← 表示具体文件对象

命名管道 FIFO

  • 文件系统中创建,支持无血缘进程通信。
  • 单向读写。

    FIFO 是系统中真实文件,支持多进程;匿名 pipe 不行。

消息类通信

System V 消息队列(msgget、msgsnd、msgrcv)

消息队列是消息的链表,存放在内核中并由消息队列标识符标识。

msgget: 创建消息队列,key 值唯一标识该消息队列
msgctl: 控制消息队列
msgsnd: 发送消息
msgrcv: 接收消息

  • 基于 key_t 标识符。
  • 可以传结构化数据,支持优先级。

Q:消息队列最大消息大小?
A:通常是系统设置,默认 8192

信号(kill、signal、sigaction)

1
kill(pid, SIGINT);

Q:信号能携带数据吗?
A:标准信号不能,sigqueue 可以。

共享内存类(最高效)

原理
将一段物理内存映射到两个或多个进程的地址空间中,使得进程可以通过这段内存直接访问彼此的数据。

shmget + shmat(System V)

  • 多个进程映射同一物理页,速度最快(零拷贝)。
  • 需要使用同步机制(如semget信号量)防止并发冲突。

mmap(POSIX)

1、使用shm_open 创建或打开共享内存对象
2、使用ftruncate设置共享内存的大小
3、使用 mmap 函数将共享内存映射到进程地址空间
4、通过映射的指针直接访问数据
5、使用 munmapshm_unlink解除映射与删除共享内存对象

同步控制类:信号量 / futex

信号量(semget、semop)

  • 对共享资源的访问控制标志。
  • 一般配合共享内存使用,用于并发控制。

futex(Fast Userspace Mutex)

  • 现代 Linux 下线程锁和高性能锁的底层机制。
  • 用户态加锁,只有在冲突时才陷入内核。

套接字类通信(socket)

  • 用于本机两个进程之间的全功能通信。
  • 与网络 socket 使用方式一致,支持 select、epoll 等机制。

进程与线程的创建和调度机制

fork():创建子进程

1
pid_t pid = fork();
  • 作用:复制当前进程,创建一个几乎完全相同的子进程。

  • 父子进程拥有独立的地址空间。

  • 子进程会继承父进程的大部分资源(如文件描述符、堆栈等)。

返回值:

  • 父进程:fork() 返回子进程的 PID。
  • 子进程:fork() 返回 0。
  • 错误时返回 -1。

    注意:父子进程地址空间是复制的,但彼此独立(写时复制机制COW)。

多线程进程调用fork()时

仅会将发起调用的线程复制到子进程中。子进程中该线程的线程ID与父进程中发起fork()调用线程的线程ID相一致。
其他线程均在子进程中消失,也不会为这些线程调用清理函数以及针对线程特有数据的解构函数。

fork的两个典型用法

(1) 一个进程创建一个自身的副本

  • 网络服务器的典型用法。

(2) 一个进程想要执行另一个程序。首先调用fork创建一个自身的副本,然后其中一个子进程调用exec把自身替换成新的程序。

  • shell之类程序的典型用法。

pid 与 ppid

pid(Process ID):进程的唯一标识,由内核分配

ppid(Parent PID):父进程的 pid,在 task_struct 中通过 real_parent 字段引用。

当父进程结束,子进程的 ppid 会被自动改为 1,即 init(或 systemd)接管,防止出现孤儿进程。

exec*():执行新程序(替换当前进程映像)

父进程需要等待子进程执行完毕,以回收其资源(否则子进程可能变成僵尸进程,Z状态)。
作用:用一个新程序替换当前进程空间,保留 PID。
不会返回:除非出错,否则 exec() 成功调用后,不会返回原程序。

wait()waitpid():进程回收

  • wait()`:阻塞当前父进程,直到有子进程退出。
    返回值:已结束子进程的 PID。

  • waitpid(pid, &status, options)

    • 可等待特定 PID;
    • 可设置非阻塞(如 WNOHANG);
    • status 中可解码退出状态。

僵尸进程出现的原因:

  • 子进程退出后,仍保留其 task_struct 中的一部分(如退出码)供父进程读取。
  • 如果父进程从不调用 wait(),子进程退出后就会一直残留在 Z 状态。

Linux 内核会通过 do_exit() → release_task() 来清理资源。

clone():线程的基础

1
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

作用:底层的线程/进程创建系统调用,比 fork() 更灵活。
pthread_create()、vfork() 等高级接口的实现。
参数:传入一堆 flag 控制资源共享方式(内存空间、文件描述符、信号处理等)。

  • CLONE_VM:共享内存空间;
  • CLONE_FS:共享文件系统信息;
  • CLONE_FILES:共享文件描述符;
  • CLONE_SIGHAND: 共享信号处理器
  • CLONE_THREAD:标记为同一线程组。

clone() 的关键点:

  • flags 参数决定子进程与父进程共享哪些资源。
  • 创建“线程”的关键是设置合适的 flags

Q:Linux 中线程是如何实现的?
A:线程是通过 clone() 系统调用创建的轻量级进程,多个线程共享同一 task 的资源。