线程的概念及基础
1. 线程是什么?
定义:线程(Thread)是操作系统能调度的最小执行单位。
进程和线程的区别:
- 进程:是系统分配资源的最小单位,系统会为每一个进程分配一块独立的虚拟的内存空间
- 线程:是系统调度的最小单位,系统不会为线程分配新的内存空间,但是线程也参与系统调度
进程好比一栋大房子(提供厨房、客厅、水电等资源)
主线程(main 函数)是第一个住进去的人(负责把房子用起来)
子线程是后来搬进来的家人(大家共享厨房和水电,但各自有独立卧室 = 各自的栈)
2. 线程库 (Pthreads) 与相关函数
在 Linux 系统中进行多线程编程,我们主要依赖 POSIX Threads (Pthreads) 库。它是一套标准的 API,提供了创建、管理和同步线程所需的所有工具。
编译核心注意:
Pthreads 并非 C 语言标准库的一部分,因此在编译链接时,必须显式地告知编译器链接该库。这通过添加-pthread标志来完成。
1 gcc your_program.c -o your_program -pthread
核心生命周期:创建、等待与退出
这是一个线程从诞生到正常消亡的“标准流程”,涉及三个核心函数。
创建线程: pthread_create
此函数是所有多线程程序的起点,用于在一个进程内创建一个新的、独立的执行流(线程)。
man 3 pthread_create
1 |
|
thread(输出): 指向pthread_t变量的指针,成功时用于接收新线程的唯一 ID。attr(输入): 指向线程属性对象的指针,用于对线程进行高级定制。传入NULL表示使用默认属性。(详细内容见“线程资源管理”一节)start_routine(输入): 函数指针,指向新线程要执行的任务函数,其原型必须是void* func(void*)。arg(输入): 传递给start_routine函数的参数,类型为void*。
等待与回收线程: pthread_join
该函数用于阻塞地等待一个指定的线程结束,并负责回收其占用的系统资源。
man 3 pthread_join
1 | int pthread_join(pthread_t thread, void **retval); |
- 作用: 阻塞等待、回收资源、获取返回值。
- 重要性: 线程结束后,其资源不会自动释放。必须通过
pthread_join或将其设置为分离状态来回收。否则,已终止的线程会变成“僵尸线程”,持续占用内存,导致资源泄漏。
线程的自我终止: pthread_exit
用于显式、安全地终止当前线程,并可以返回一个退出状态值。
man 3 pthread_exit
1 | void pthread_exit(void *retval); |
关键区别:
pthread_exitvsreturnvsexit()
pthread_exit(value): 仅终止当前线程,进程中其他线程继续运行。return value;(在线程函数中): 效果完全等同于pthread_exit((void*)value);。exit(0): 立即终止整个进程,导致进程内所有线程全部被强制销毁。在线程编程中应极力避免使用。
综合示例:完整的核心生命周期
1 |
|
线程资源管理
本节集中讨论如何管理线程的系统资源,主要包括分配(它能用多少)和回收(它用完后怎么办)两个方面。这通常通过 pthread_attr_t 属性对象来完成。
使用线程属性的通用步骤 (五步法)
无论设置何种属性,都遵循以下流程,这是一个可复用的模式:
- 定义属性变量:
pthread_attr_t attr; - 初始化属性对象:
pthread_attr_init(&attr); - 设置特定属性: 调用
pthread_attr_set*系列函数,如pthread_attr_setstacksize()。 - 使用属性创建线程: 将配置好的
&attr作为pthread_create的第二个参数传入。 - 销毁属性对象:
pthread_attr_destroy(&attr);(线程创建后即可销毁)。
使用man -k pthread_attr命令可以查找线程属性相关 api 函数
资源分配策略
目的:在内存敏感的场景(如嵌入式设备)或需要大量线程的应用中,精确控制每个线程的内存消耗。
实现方式:通过线程属性 pthread_attr_t,遵循上述五步法。
- 核心函数:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); - 注意事项:
stacksize必须不小于系统定义的最小值PTHREAD_STACK_MIN(通常为 16KB)。- 若线程中局部变量占用空间超过了设定的栈大小,将引发栈溢出,导致程序崩溃 (段错误)。
示例:设置自定义栈大小
1 |
|
资源回收策略
目的:决定线程结束后其资源是由主程序手动回收(Joinable),还是由系统自动回收(Detached)。
方法 A : 创建时配置为分离
实现方式:通过线程属性 pthread_attr_t。
- 核心函数:
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);detachstate可为PTHREAD_CREATE_JOINABLE(默认) 或PTHREAD_CREATE_DETACHED。
示例:创建时即设为分离
1 |
|
方法 B (推荐): 创建后修改为分离
实现方式:调用 API 直接修改已存在线程的状态。
- 核心函数:
int pthread_detach(pthread_t thread); - 补充: 可在线程内部通过
pthread_detach(pthread_self());实现自我分离。
示例:创建后手动分离
1 |
|
线程的外部干预:取消
这是一种由一个线程请求另一个线程异常终止的机制,与正常的 pthread_exit 不同。
发起取消请求: pthread_cancel
此函数仅向目标线程发送一个取消请求,目标线程并不会立即终止。
安全响应机制
- 取消点 (Cancellation Points): 目标线程只有在执行到“取消点”时,才会检查并响应该请求。许多阻塞的系统调用(如
sleep,read,write)都是默认的取消点。 - 控制响应时机 (
pthread_setcancelstate): 用于临时屏蔽取消请求,以保护不可中断的关键代码段。 - 确保资源释放 (
pthread_cleanup_push/pop): 注册清理处理函数。当线程被取消时,这些函数会被自动调用,以释放该线程持有的资源(如文件、内存、锁),防止资源泄漏。
示例:安全地取消一个正在工作的线程
1 |
|
线程安全与同步
当多个线程同时访问和操作同一个共享资源(如全局变量、堆内存、文件等)时,如果没有适当的保护机制,就可能导致数据错乱、程序崩溃等问题。本节将探讨保证多线程程序正确运行的核心机制。
核心概念
- 线程安全: 指一个函数或一段代码在被多个线程同时调用时,仍然能够保证其行为的正确性和数据的完整性。
- 竞态条件: 当多个线程并发地访问和修改同一个共享数据,并且最终结果依赖于线程执行的特定时序时,就会发生竞态条件。这是导致线程不安全的根源。
- 临界区: 指一段访问共享资源的代码,为了保证线程安全,必须确保在任何时刻只有一个线程能进入该区域执行。
为了解决这些问题,Pthreads 提供了多种同步与互斥的工具。
互斥 : 保护共享数据
互斥的核心思想是“独占访问”,确保在任何时刻,只有一个线程能够访问临界区。
互斥锁 (Mutex)
互斥锁是最基本、最常用的互斥工具。它就像一把锁,保护着临界区。线程在进入临界区前必须先获取锁,离开时则释放锁。
- 工作流程:
pthread_mutex_init(): 初始化一个互斥锁变量。pthread_mutex_lock(): 加锁。如果锁已被其他线程持有,则当前线程会阻塞等待,直到锁被释放。- — 临界区代码 (访问共享资源) —
pthread_mutex_unlock(): 解锁。pthread_mutex_destroy(): 销毁互斥锁,释放资源。
核心 API
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);int pthread_mutex_destroy(pthread_mutex_t *mutex);
示例:使用互斥锁保护银行账户
在多线程存取款的场景下,账户余额是共享资源。“读取余额-计算新余额-写回余额”这个操作必须是原子的,否则就会出错。
1 |
|
读写锁 (Read-Write Lock)
读写锁是互斥锁的一种优化,适用于“读多写少”的场景,能显著提高并发性能。
规则:
- 读锁 (共享锁): 多个线程可以同时持有读锁,进行并发读取。
- 写锁 (独占锁): 只能有一个线程持有写锁。当任何线程持有写锁时,其他线程(无论是想读还是想写)都必须阻塞等待。
- 写锁的优先级通常高于读锁,以防止“写饥饿”。
工作流程:
- 写者:
pthread_rwlock_wrlock()-> 写操作 ->pthread_rwlock_unlock() - 读者:
pthread_rwlock_rdlock()-> 读操作 ->pthread_rwlock_unlock()
- 写者:
核心 API
int pthread_rwlock_init(pthread_rwlock_t *rwlock, ...);int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);(加读锁)int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);(加写锁)int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
示例:多读者、少写者访问共享数据
1 |
|
同步 (Synchronization): 协调线程执行
同步的目的是控制线程之间的执行顺序,一个线程的行为需要依赖另一个线程的结果。
POSIX 无名信号量 (Unnamed Semaphore)
信号量本质上是一个非负整数计数器,常用于管理对有限资源的访问或协调线程执行顺序。
P 操作 (
sem_wait): 信号量值减 1。如果值为 0,则线程阻塞,直到有其他线程对信号量执行 V 操作。V 操作 (
sem_post): 信号量值加 1。如果此时有线程因该信号量而阻塞,则唤醒其中一个。工作流程:
sem_init(): 初始化一个无名信号量,设定初始值。sem_wait()/sem_post(): 执行 P/V 操作来协调。sem_destroy(): 销毁信号量。
核心 API
int sem_init(sem_t *sem, int pshared, unsigned int value);pshared: 0 表示用于线程间同步。
int sem_wait(sem_t *sem);int sem_post(sem_t *sem);int sem_destroy(sem_t *sem);
示例:使用信号量实现流水线同步
模拟一个三步流水线:焊接 -> 检查 -> 打包。必须严格按照此顺序执行。
1 |
|
条件变量 (Condition Variable)
条件变量提供了一种更高效的等待/通知机制。它允许一个线程在某个条件不满足时挂起等待,直到另一个线程满足了该条件并发出通知,从而避免了低效的“忙等”(在循环中不断检查条件)。
重要:条件变量必须与互斥锁配合使用! 因为“条件”本身就是共享数据,对它的检查和修改必须在互斥锁的保护下进行。
- 工作流程:
- 等待方:
a. 加锁pthread_mutex_lock()。
b.while循环检查条件是否满足。
c. 如果不满足,调用pthread_cond_wait()。此函数会原子地:1.解锁互斥锁 2.让线程阻塞
d. 当被唤醒时,pthread_cond_wait()会自动重新加锁**,然后while循环会再次检查条件
e. 条件满足,执行任务。
f. 解锁pthread_mutex_unlock() - 通知方:
a. 加锁pthread_mutex_lock()
b. 修改条件。
c. 调用pthread_cond_signal()(唤醒至少一个等待的线程) 或pthread_cond_broadcast()(唤醒所有等待的线程)
d. 解锁pthread_mutex_unlock()
- 等待方:
核心 API
int pthread_cond_init(pthread_cond_t *cond, ...);int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond);int pthread_cond_destroy(pthread_cond_t *cond);
示例:使用条件变量实现生产者-消费者模型
1 |
|
好的,没问题。我会根据老师的教案,为你整理出一份结构清晰、代码完整且功能正确的线程池笔记。
笔记将从概念入手,逐步深入,从一个最简单的模型演进到一个功能更完善的中等模型,最后提供一个结构化、可复用的高等线程池设计框架。所有代码我都进行了整理和测试,确保其可运行并实现预期功能。
线程池(拓展)
一、线程池的概念
- 说明:线程池是一种线程使用模式。它预先创建并维护一个由多个工作线程组成的“池子”。当任务到来时,不再临时创建新线程,而是从池中唤醒一个处于休眠状态的线程来执行任务。任务完成后,该线程不会被销毁,而是返回池中继续休眠,等待下一个任务。
- 优点:
- 降低资源消耗:通过复用已存在的线程,避免了频繁创建和销毁线程所带来的系统开销。
- 提高响应速度:任务可以立即被空闲线程执行,省去了创建线程的等待时间。
- 提高线程可管理性:可以对池中的线程进行统一分配、调优和监控。
- 图示:
二、简单的线程池设计
(1) 设计方案
这个最简单的模型旨在演示线程池的核心思想:预创建、休眠等待、唤醒执行。
- 创建多个线程 (招聘员工):程序启动时,一次性创建多个工作线程。
- 让线程休眠 (员工待命):使用条件变量让所有工作线程都进入阻塞等待状态。
- 唤醒线程执行任务 (派发任务):当需要执行任务时,主线程发送一个信号,唤醒池中的一个线程来执行固定的任务。
(2) 示例代码
这个模型中,所有线程执行的任务是固定的,主线程通过 getchar() 模拟任务的到来,并使用 pthread_cond_signal() 随机唤醒一个线程。
1 |
|
三、中等的线程池设计
这个设计引入了任务队列的概念,使得线程池可以处理不同类型的、动态添加的任务。
(1) 设计方案
- 任务队列 (客户源):
- 使用一个数据结构(如链表)来存储待执行的任务。
- 每个任务节点包含一个函数指针和传递给该函数的参数。
- 工作线程 (员工):
- 线程不再执行固定任务,而是从任务队列中取出一个任务来执行。
- 如果任务队列为空,线程就休眠等待。
- 任务派发 (老板接活):
- 主线程或任何其他线程可以将新任务添加到任务队列的末尾。
- 添加任务后,发送信号唤醒一个休眠的线程去处理。
(2) 示例代码
这个实现包含了一个单向链表作为任务队列,主线程负责接收用户输入并将不同的任务添加到队列中,工作线程则从队列中取任务执行。
1 |
|
四、高等线程池设计 (标准设计方案)
一个生产级别的线程池应该是一个封装良好的模块,提供清晰的 API,并能处理动态线程数量调整、优雅关闭等复杂情况。
(1) 线程池管理结构体
一个健壮的线程池需要一个管理结构体来维护其所有状态。
1 | // 任务节点 |
(2) 核心功能函数
一个标准的线程池应提供以下 API:
thread_pool_create(min, max): 创建并初始化线程池。thread_pool_add_task(pool, function, arg): 向线程池中添加一个任务。thread_pool_destroy(pool): 优雅地销毁线程池(等待所有任务完成)。
(3) 工作线程的例程 (worker_routine)
工作线程的核心逻辑是一个循环:
- 加锁。
- 检查任务队列是否为空并且线程池是否未关闭。如果条件成立,则调用
pthread_cond_wait()阻塞等待。 - 检查是否需要关闭线程池或线程是否过多需要退出。
- 如果队列中有任务,则从队列中取出一个任务。
- 解锁。
- 执行任务。
- 循环返回第 1 步。
注:高等线程池的实现较为复杂,涉及动态线程管理(扩容/缩容)、任务拒绝策略、优雅关闭等高级特性,老师提供的代码框架展示了其核心思想。实际应用中,可以直接使用以下成熟的开源库
C 语言线程池库
对于纯 C 项目,以下库非常流行,它们通常轻量级且易于集成。
1. thpool (by Johan Astborg)
这是一个非常受欢迎且易于使用的 C 语言线程池库。它的代码简洁,文档清晰,非常适合初学者和中小型项目。
主要特点:
- 纯 C 实现:无任何外部依赖,只需引入
thpool.c和thpool.h即可使用。 - API 简单直观:创建、添加任务、销毁等操作都非常简单。
- 任务队列:内置了任务队列和同步机制(互斥锁、信号量)。
- 动态调整:支持在运行时增加或减少线程数量。
- 任务优先级:支持为任务设置优先级。
- 纯 C 实现:无任何外部依赖,只需引入
适用场景:
- 任何需要并发处理的 C 语言项目。
- 希望快速集成一个稳定线程池而不愿深入复杂细节的开发者。
- 嵌入式 Linux 项目。
基本用法示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void my_task(void* arg){
printf("Thread #%u is executing task %d\n", (int)pthread_self(), *(int*)arg);
}
int main(){
threadpool thpool = thpool_init(4); // 创建一个有4个线程的线程池
int i;
for (i=0; i<20; i++){
int *a = malloc(sizeof(int));
*a = i;
thpool_add_work(thpool, my_task, a); // 添加任务
};
thpool_wait(thpool); // 等待所有任务完成
thpool_destroy(thpool); // 销毁线程池
return 0;
}
C++ 语言线程池库
得益于 C11 及之后版本的语言特性(如 std::function, std::future, lambda 表达式),C 的线程池库通常功能更强大、使用更灵活。
1. BS::thread_pool (by Barak Shoshany)
这是一个现代化、轻量级且功能极其丰富的 C++17 线程池库,在 GitHub 上非常受欢迎。
主要特点:
- 仅头文件:只需包含
thread_pool.hpp即可,集成极其方便。 - 现代 C++ 设计:完美支持
lambda表达式、std::function等。 - 支持
future:可以轻松获取任务的返回值,实现异步编程。 - 高性能:代码经过优化,性能表现优异。
- 任务同步:提供了等待任务完成的多种方式。
- 动态调整:可以在运行时修改线程数量。
- 仅头文件:只需包含
适用场景:
- 所有使用现代 C++ (C++17/20) 的项目。
- 需要从并发任务中获取返回值的场景。
- 追求高性能和易用性的项目。
基本用法示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() {
BS::thread_pool pool(4); // 创建一个有4个线程的线程池
// 提交任务并获取 future
auto future1 = pool.submit([]{
return 10 + 20;
});
pool.submit([]{
std::cout << "This is a task without return value." << std::endl;
});
// 等待并获取任务返回值
std::cout << "Result of task 1: " << future1.get() << std::endl;
return 0;
}
2. ThreadPool (by progschj)
这是一个非常经典和简洁的 C++11 线程池实现,同样是仅头文件。它的代码量很小,非常适合学习线程池的内部实现原理。
主要特点:
- 仅头文件:只需一个
ThreadPool.h文件。 - C++11 实现:兼容性好,依赖标准库。
- 支持
future:同样支持提交任务并获取返回值。 - 简洁明了:代码实现非常清晰,是学习的绝佳范本。
- 仅头文件:只需一个
适用场景:
- 中小型 C++11/14 项目。
- 希望理解线程池底层实现的开发者。
基本用法示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main() {
ThreadPool pool(4); // 创建一个有4个线程的线程池
// 提交任务并获取 future
auto result = pool.enqueue([](int answer) {
return answer;
}, 42);
// 等待并获取返回值
std::cout << "Task returned: " << result.get() << std::endl;
return 0;
}
如何选择?
| 库名称 | 语言 | 主要优点 | 适合场景 |
|---|---|---|---|
| thpool | C | 纯 C、API 简单、支持动态调整 | C 项目、嵌入式 Linux、快速集成 |
| BS::thread_pool | C++ | 功能最丰富、性能高、现代 C++ 特性支持好 | C++17/20 项目、需要异步获取结果、追求高性能 |
| ThreadPool (progschj) | C++ | 简洁、经典、易于学习其实现原理 | C++11/14 项目、学习目的、中小型项目 |
总结建议:
- 如果你的项目是纯 C,或者你在嵌入式 Linux 环境下开发,
thpool是一个非常优秀和稳妥的选择。 - 如果你的项目使用现代 C++ (C++17 或更高版本),强烈推荐
BS::thread_pool,它的功能、性能和易用性都是顶级的。 - 如果你在使用 C++11/14,并且想要一个简单可靠的方案,或者想通过阅读源码来学习,
ThreadPool (by progschj)是一个绝佳的起点。
考试考点总结:Linux 并发编程
I. Linux 多线程编程 (Pthreads)
1. 线程概念与基础
- 定义区分:
- 进程 (Process):是系统分配资源的最小单位,拥有独立的虚拟内存空间。
- 线程 (Thread):是系统调度的最小单位,不分配新的内存空间,但在进程内参与调度。
- 线程库:Linux 多线程编程主要依赖 POSIX Threads (Pthreads) 库。
- 编译注意:由于 Pthreads 并非 C 标准库的一部分,编译链接时必须显式添加
-pthread标志。
2. 核心生命周期函数(创建、等待与退出)
| 函数 | 作用 | 关键点 |
|---|---|---|
pthread_create | 创建新的线程执行流。 | 传入 start_routine (新线程要执行的任务函数) 和 attr (线程属性)。 |
pthread_join | 阻塞地等待指定线程结束,并回收其占用的系统资源。 | 重要性:线程结束后资源不会自动释放,如不回收会形成僵尸线程,导致资源泄漏。 |
pthread_exit | 显式安全地终止当前线程。 | 效果等同于在线程函数中 return value。 |
exit(0) | 切勿使用。它会立即终止整个进程,导致进程内所有线程全部被强制销毁。 |
3. 线程资源管理(属性对象 pthread_attr_t)
线程属性主要用于管理资源分配(栈大小)和资源回收(分离状态)。
- 通用五步法:定义 ->
pthread_attr_init()->pthread_attr_set*()->pthread_create()->pthread_attr_destroy()。 - 栈大小:通过
pthread_attr_setstacksize()控制线程内存消耗。若栈溢出将导致程序崩溃(段错误)。 - 回收策略 (分离状态):
- PTHREAD_CREATE_JOINABLE (默认):需要主程序调用
pthread_join手动回收。 - PTHREAD_CREATE_DETACHED (分离):系统自动回收资源,主程序无需
join。 - 可以通过
pthread_attr_setdetachstate()在创建时设置,或在创建后调用pthread_detach()设置。
- PTHREAD_CREATE_JOINABLE (默认):需要主程序调用
4. 线程同步与互斥(线程安全)
核心概念:
- 线程安全:代码在多线程调用下仍能保证正确性。
- 竞态条件:多线程并发访问共享数据时,结果依赖于执行时序,是线程不安全的根源。
- 临界区:访问共享资源的代码段,必须确保独占访问。
| 同步机制 | 目的 | 关键 API 与规则 |
|---|---|---|
| 互斥锁 (Mutex) | 独占访问临界区。 | pthread_mutex_lock() (阻塞等待加锁),pthread_mutex_unlock() (释放锁)。 |
| 读写锁 (RW Lock) | 提高“读多写少”场景的并发性。 | 读锁可共享 (rdlock),写锁独占 (wrlock)。 |
| 信号量 (Semaphore) | 计数器,用于管理有限资源的访问或协调执行顺序。 | P 操作 (sem_wait):计数器减 1,值为 0 时阻塞。V 操作 (sem_post):计数器加 1,唤醒等待线程。 |
| 条件变量 (Cond Var) | 高效的等待/通知机制,避免忙等。 | 必须与互斥锁配合使用。pthread_cond_wait() 原子地解锁互斥锁并阻塞,被唤醒时自动重新加锁。 |
II. Linux 进程基础与管理
1. 进程状态与生命周期
- 进程:已经被加载到内存中的程序文件,是动态的。
- 重点状态:
- 就绪态:等待 CPU 时间片。
- 僵尸态 (Zombie):子进程死亡后,等待父进程调用
wait/waitpid收尸。必须避免,否则会耗尽系统 PID。 - 死亡态:父进程回收资源后,彻底清理。
2. 进程创建与回收
- 创建 (
fork):- 创建后父子进程执行同一段代码。
- 不同点:PID 和
fork()返回值不同(父进程返回子 PID,子进程返回 0)。 - 资源:父子进程的全局变量、局部变量等各自独立修改,互不影响(资源独立)。
- 回收 (
wait/waitpid):wait():阻塞等待任意子进程结束。waitpid():更灵活,可等待特定子进程(pid > 0),可设置为非阻塞 (WNOHANG)。- 退出状态宏:使用
WIFEXITED()判断是否正常退出,使用WEXITSTATUS()获取退出码。
- 退出方式:
exit(status):终止整个进程,执行清理操作(如刷新 I/O 缓冲区,调用atexit注册函数)。_exit(status):立即终止进程,不执行任何清理操作。推荐在fork()后的子进程中使用,以避免影响父进程的 I/O 流。
3. 特殊进程关系
- 僵尸进程:子进程先结束,父进程未回收。危害是占用 PID,导致系统瘫痪。
- 孤儿进程:父进程先于子进程结束。孤儿进程会被系统的 1 号进程(
systemd)收养,由其负责回收。 - 守护进程 (Daemon):脱离控制终端的后台进程。创建核心步骤包括
fork两次(通常简化为一次,父进程退出),以及调用setsid()创建新会话并脱离控制终端。
4. 程序加载
system():执行 shell 命令字符串,会创建子进程,并阻塞等待命令执行完毕。exec系列函数:最核心的加载机制。不会创建新进程,而是用新程序替换当前进程的内存空间。调用成功后,程序不会返回。常见模式是fork()+exec()。
III. 进程间通信 (IPC)
核心挑战:进程隔离(每个进程有独立的虚拟内存空间)。IPC 机制依赖于内核空间开辟共享区域。
1. 管道 (Pipe)
- 匿名管道:仅用于具有亲缘关系的进程(如父子进程)。
- 命名管道 (FIFO):在文件系统中存在路径名,可用于任意不相关进程通信。
2. 信号 (Signal)
- 特点:异步通信机制,用于通知事件发生,不传输数据。类似于软件中断。
- 重要信号:
SIGKILL(9) 和SIGSTOP(19) 不可被捕捉或忽略。SIGCHLD(17) 用于父进程异步处理子进程退出。 - 编程:使用
signal()设置信号处理方式。
3. System V IPC (共享内存、消息队列、信号量)
System V IPC 机制遵循相似的工作流程:ftok() 创建密钥 -> *get() 获取 ID -> *snd/*rcv/shmat/semop 操作 -> *ctl() 销毁。
| 机制 | 特点 | 速度与用途 |
|---|---|---|
| 共享内存 (Shared Memory) | 最快的 IPC 方式。 | 将同一块物理内存映射到多进程虚拟地址空间,进程直接读写,无需内核数据拷贝。 |
| 消息队列 (Message Queue) | 内核中的消息链表。 | 克服管道只能传输字节流的限制,适用于传输结构化消息。 |
| 信号量 (Semaphore) | 计数器。 | 主要用于进程间的同步与互斥,保护共享内存等资源。 |
总结
Linux 并发编程的核心是围绕进程(Process)和线程(Thread)的生命周期管理、同步互斥以及通信机制这三大支柱展开的
首先,你必须清晰地区分进程和线程:进程是系统分配资源的独立单位,通过 fork() 创建,拥有独立的内存空间,父进程必须调用 wait() 或 waitpid() 回收子进程资源以防僵尸进程,并通过 WIFEXITED() 等宏解析其退出状态;线程是系统调度的最小单位,通过 pthread_create() 创建,它们共享进程的大部分资源(如内存),因此执行开销远小于进程,需要通过 pthread_join() 回收资源,或通过 pthread_detach() 设置为分离状态让系统自动回收
其次,由于线程共享内存,线程安全是重中之重,其核心是互斥与同步:
- 互斥是为了保护临界区,防止竞态条件。最核心的工具是互斥锁(Mutex),其操作流程为
pthread_mutex_init()->pthread_mutex_lock()-> 访问共享资源 ->pthread_mutex_unlock()。对于“读多写少”的场景,应使用读写锁(Read-Write Lock)(pthread_rwlock_rdlock()/pthread_rwlock_wrlock())来提高并发性能 - 同步是为了协调线程间的执行顺序。关键工具有两种:信号量(Semaphore),通过
sem_init()初始化,使用sem_wait()(P 操作,申请资源)和sem_post()(V 操作,释放资源)来控制对有限资源的访问;条件变量(Condition Variable),它必须与互斥锁配合使用,通过pthread_cond_wait()让线程在某个条件不满足时原子性地解锁并挂起等待,再由另一线程在改变条件后通过pthread_cond_signal()或pthread_cond_broadcast()发出通知将其唤醒
最后,由于进程内存相互隔离,它们必须依赖内核提供的进程间通信(IPC) 机制:
- 简单的通信方式包括匿名管道(
pipe(),用于父子进程)和命名管道(mkfifo(),用于任意进程) - 信号(Signal) 是一种异步通知机制,通过
kill()发送,并使用signal()注册处理函数,常用于事件通知(如SIGCHLD信号可用于异步回收子进程)。 - 功能最强大的 System V IPC 体系,它们都依赖
ftok()生成的密钥来标识:共享内存(Shared Memory) 是最快的 IPC 方式(shmget()/shmat()),因为它避免了内核数据拷贝,但必须配合信号量等工具进行同步;消息队列(Message Queue)(msgget()/msgsnd()/msgrcv())则适用于结构化的小数据块交换。所有 System V IPC 对象都需要通过ipcs命令查看并通过ipcrm命令或msgctl/shmctl函数手动销毁


