Linux专题七:进程、线程与同步互斥

一、基础概念区分(进程、线程、程序)

1. 核心定义

概念 本质说明 关键特性
程序 存储在磁盘上的代码指令集合(静态文件,如.c源文件、.exe可执行文件) 无执行状态,不占用系统资源(CPU、内存等),仅为静态存储载体
进程 正在运行的程序实例(动态实体),操作系统进行资源分配和调度的基本单位 1. 拥有独立的内存空间、文件描述符等系统资源;2. 进程间相互独立,通信需依赖 IPC 机制;3. 开销大,创建 / 销毁 / 切换成本高
线程 进程内部的一条执行路径(轻量级进程),CPU 调度和执行的基本单位 1. 共享所属进程的资源(内存、文件描述符等);2. 线程间切换开销小,创建效率高;3. 一个进程至少包含一个主线程(main 函数执行路径)
主线程 程序启动后默认创建的线程,对应main()函数的执行流程 1. 是进程的入口线程,负责初始化进程资源;2. 主线程退出会导致整个进程终止(除非设置分离线程);3. 可通过pthread库创建其他子线程(副线程)

2. 形象类比

  • 进程 = 工厂(提供生产环境、厂房、设备等资源)
  • 线程 = 工厂内的工人(真正执行生产任务,共享工厂资源)
  • 主线程 = 工厂厂长(负责统筹启动,默认最先运行)
  • 程序 = 生产手册(静态文档,指导工人如何操作)

二、进程间通信(IPC)方式汇总

通信方式 核心特性 适用场景
管道 1. 无名管道:仅支持父子 / 兄弟进程,内核级无文件实体;2. 有名管道:支持任意进程,以文件形式存在于文件系统;3. 半双工通信,数据先进先出 简单的字节流传输,如父子进程间批量数据传递
信号量 1. 特殊非负整数,仅支持 P 操作(减 1,获取资源)和 V 操作(加 1,释放资源);2. 原子操作,用于解决临界资源竞争;3. 不存储数据,仅做同步互斥控制 多进程 / 线程间的同步互斥,如共享资源访问控制
共享内存 1. 多个进程映射到同一块物理内存,直接读写无需数据拷贝,速度最快;2. 本身无同步机制,需配合信号量 / 互斥锁使用;3. 临界资源,存在数据一致性问题 大量数据高速传输,如大数据量共享场景
消息队列 1. 内核中的消息链表,以结构体形式存储带类型的消息;2. 支持按类型读取消息,无需随进程持续运行;3. 数据独立于进程,进程退出消息仍保留 需分类传输的数据,如多进程间指令 / 数据分发
套接字(Socket) 1. 支持跨主机、跨进程通信,兼顾本地和网络 IPC;2. 全双工通信,支持 TCP/UDP 两种协议;3. 通用性强,底层封装了复杂的网络协议 网络通信(如客户端 - 服务器架构)、本地跨进程高可靠通信

三、线程核心知识点(基于 Linux pthread 库)

1. 线程基础特性

(1)线程创建与执行特性
  • 线程创建后不一定立即执行,由系统线程调度器决定执行时机,与创建顺序无关;
  • 线程调度依赖优先级、时间切片算法,可能出现 "先创建后执行" 的情况;
  • 主线程与副线程(子线程)同步并发执行,互不阻塞(除非使用pthread_join()等同步函数);
  • 线程退出:pthread_exit()终止当前线程(释放自身资源,不影响其他线程);主线程退出会导致整个进程终止。
(2)线程分类(按管理级别)
线程类型 创建 / 管理者 开销 核心特性
用户级线程 用户态库(如 pthread) 1. 内核无感知,调度由用户库完成;2. 切换无需进入内核,效率高;3. 无法利用多处理器并行,一个线程阻塞会导致整个进程阻塞
内核级线程 操作系统内核 1. 内核直接管理,每个线程对应内核调度实体;2. 支持多处理器并行执行;3. 线程阻塞不会影响其他线程,Linux 系统默认使用该类型
混合级线程 用户库 + 内核 中等 1. 多个用户级线程映射到一个内核级线程;2. 兼顾用户级的高效和内核级的并行能力;3. 复杂场景下的最优选择
(3)并发与并行的区别
概念 本质说明 依赖条件
并发 多个任务交替执行(宏观上同时进行,微观上串行切换) 无需多处理器,单处理器通过时间切片实现(如单 CPU 同时运行多个软件)
并行 多个任务同时执行(宏观 + 微观均为同时进行) 必须依赖多处理器 / 多核 CPU,是 "特殊的并发"
核心关联 1. 并行是并发的子集,所有并行都是并发,但并发不一定是并行;2. 单处理器仅支持并发,多处理器可同时支持并发与并行 -

2. 线程核心函数(pthread 库)

(1)头文件与编译要求
  • 头文件:#include <pthread.h>(线程函数、锁、读写锁均包含在此头文件)
  • 编译命令:必须添加-lpthread参数(指定链接 pthread 线程库),示例:
bash 复制代码
gcc thread_demo.c -o thread_demo -lpthread
(2)核心函数说明与示例
1. 线程创建:pthread_create()
  • 函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • 参数说明:
    • thread:输出参数,存储创建后的线程 ID;
    • attr:线程属性(通常设为 NULL,使用默认属性);
    • start_routine:线程函数指针(返回值void*,参数void*);
    • arg:传递给线程函数的参数(无参数时设为 NULL);
  • 返回值:成功返回 0,失败返回非 0 错误码。
2. 线程等待:pthread_join()
  • 函数原型:int pthread_join(pthread_t thread, void **retval);
  • 功能:阻塞主线程,等待指定副线程执行完毕,可接收副线程的返回值;
  • 参数说明:
    • thread:要等待的线程 ID;
    • retval:二级指针,存储线程退出时的返回值(pthread_exit()的参数);
  • 返回值:成功返回 0,失败返回非 0 错误码。
3. 线程退出:pthread_exit()
  • 函数原型:void pthread_exit(void *retval);
  • 功能:终止当前线程,释放线程资源,返回指定状态码,不影响其他线程;
  • 参数:retval:线程退出状态指针,可通过pthread_join()获取。
4. 基础线程示例(创建 + 等待 + 退出)
cpp 复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

// 线程函数(副线程执行逻辑)
void* thread_fun(void* arg) {
    int num = *(int*)arg; // 接收传递的参数
    printf("副线程:ID=%lu,接收参数=%d,开始执行\n", (unsigned long)pthread_self(), num);
    
    sleep(2); // 模拟业务耗时
    int* ret = malloc(sizeof(int));
    *ret = num * 2; // 线程返回结果
    pthread_exit((void*)ret); // 终止线程,返回结果
}

int main() {
    // 主线程:默认执行路径
    printf("主线程:ID=%lu,开始运行\n", (unsigned long)pthread_self());
    
    pthread_t tid1, tid2; // 线程ID
    int arg1 = 10, arg2 = 20; // 传递给线程的参数
    
    // 创建两个副线程
    if (pthread_create(&tid1, NULL, thread_fun, &arg1) != 0) {
        perror("创建线程1失败");
        return 1;
    }
    if (pthread_create(&tid2, NULL, thread_fun, &arg2) != 0) {
        perror("创建线程2失败");
        return 1;
    }
    
    // 等待副线程执行完毕,获取返回值
    void* ret1 = NULL;
    void* ret2 = NULL;
    pthread_join(tid1, &ret1);
    pthread_join(tid2, &ret2);
    
    printf("主线程:线程1返回结果=%d,线程2返回结果=%d\n", *(int*)ret1, *(int*)ret2);
    
    // 释放线程返回值的内存
    free(ret1);
    free(ret2);
    
    printf("主线程:执行完毕,退出\n");
    return 0;
}

编译运行:

bash 复制代码
gcc thread_basic.c -o thread_basic -lpthread
./thread_basic
  • 运行结果:主线程创建两个副线程,等待其执行完毕后获取返回值,最终退出。

3. 线程安全问题

(1)核心概念
  • 线程安全:多线程环境下,函数 / 代码段能保证执行结果的正确性,不会出现数据竞争、值被覆盖等问题;
  • 线程不安全原因:
    1. 共享资源未加保护,多个线程同时读写;
    2. 非原子操作(如i++,分为 "读取 - 修改 - 写入" 三步,中间可能被其他线程打断);
    3. 函数内部使用静态变量 / 全局变量(如strtok()的光标指针,会被多个线程覆盖)。
(2)线程不安全示例(i++非原子操作)
cpp 复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int g_count = 0; // 全局共享变量(临界资源)

// 线程函数:对全局变量执行10000次i++
void* count_fun(void* arg) {
    for (int i = 0; i < 10000; i++) {
        g_count++; // 非原子操作,存在数据竞争
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, count_fun, NULL);
    pthread_create(&tid2, NULL, count_fun, NULL);
    
    // 等待两个线程执行完毕
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    // 预期结果:20000,实际结果大概率小于20000(数据竞争导致)
    printf("最终计数结果:%d\n", g_count);
    return 0;
}
(3)线程安全解决方案
  • 使用同步互斥机制(信号量、互斥锁等)保护临界资源;
  • 使用线程安全版本的函数(如strtok_r()替代strtok())。
线程安全函数示例(strtok_r()字符串分割)
cpp 复制代码
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

// 线程函数:使用strtok_r进行字符串分割(线程安全)
void* str_split_fun(void* arg) {
    char* str = (char*)arg;
    char* saveptr; // 存储分割上下文(每个线程独立拥有,避免覆盖)
    char* token = strtok_r(str, " ", &saveptr);
    
    printf("线程%lu:分割结果:\n", (unsigned long)pthread_self());
    while (token != NULL) {
        printf("  %s\n", token);
        token = strtok_r(NULL, " ", &saveptr);
    }
    pthread_exit(NULL);
}

int main() {
    char str1[] = "Hello Thread Safe Function";
    char str2[] = "pthread strtok_r is secure";
    
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, str_split_fun, str1);
    pthread_create(&tid2, NULL, str_split_fun, str2);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

四、同步互斥机制(解决临界资源竞争)

1. 信号量(线程 / 进程通用)

(1)核心概念
  • 本质 :特殊非负整数,仅支持 P 操作(sem_wait(),减 1,获取资源,可能阻塞)和 V 操作(sem_post(),加 1,释放资源,唤醒阻塞线程);
  • 原子操作:P/V 操作不可被打断,保证多线程 / 进程下操作的唯一性;
  • 线程信号量头文件#include <semaphore.h>(进程信号量为sys/sem.h);
  • 核心特性:信号量不与特定线程 / 进程绑定,仅通过值判断资源是否可用,值为 1 时允许访问,值为 0 时阻塞(系统自动休眠,无需手动编写阻塞代码)。
(2)线程信号量核心函数
函数名 功能说明
sem_init() 初始化线程信号量,原型:int sem_init(sem_t *sem, int pshared, unsigned int value);pshared=0表示线程信号量,value为初始值)
sem_wait() P 操作,信号量减 1,若值 < 0 则阻塞,原型:int sem_wait(sem_t *sem);
sem_post() V 操作,信号量加 1,若值≤0 则唤醒阻塞线程,原型:int sem_post(sem_t *sem);
sem_destroy() 销毁信号量,释放资源,原型:int sem_destroy(sem_t *sem);
(3)线程信号量示例(解决i++线程安全问题)
cpp 复制代码
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>

int g_count = 0;
sem_t sem; // 线程信号量

// 线程函数:加锁后执行i++
void* count_fun(void* arg) {
    for (int i = 0; i < 10000; i++) {
        sem_wait(&sem); // P操作:获取资源,进入临界区
        g_count++; // 临界区操作(保护共享变量)
        sem_post(&sem); // V操作:释放资源,退出临界区
    }
    pthread_exit(NULL);
}

int main() {
    // 初始化信号量:初始值1(互斥访问),线程私有
    sem_init(&sem, 0, 1);
    
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, count_fun, NULL);
    pthread_create(&tid2, NULL, count_fun, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    printf("最终计数结果:%d\n", g_count); // 预期结果:20000
    
    // 销毁信号量
    sem_destroy(&sem);
    return 0;
}

编译运行:

(4)形象类比:景点检票
  • 线程 / 进程 = 游客;
  • 信号量值 = 门票数量(1 张表示仅允许 1 人进入,0 张表示无票);
  • sem_wait() = 检票员:检查门票(信号量值),有票则撕票(减 1)允许进入,无票则让游客排队阻塞;
  • sem_post() = 游客离开景区:归还门票(加 1),唤醒排队的游客重新检票。

2. 互斥锁(线程专用,高效互斥)

(1)核心概念
  • 本质:实现 "独占式访问",只有持有锁的线程才能访问临界资源,其他线程需等待解锁;
  • 特性:轻量级开销小,仅支持 "加锁" 和 "解锁" 操作,适用于短时间临界资源访问;
  • 核心函数 :均定义在pthread.h中。
(2)互斥锁核心函数
函数名 功能说明
pthread_mutex_init() 初始化互斥锁,原型:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);attr=NULL使用默认属性)
pthread_mutex_lock() 加锁:若锁未被占用则获取锁,若已被占用则阻塞等待
pthread_mutex_unlock() 解锁:释放锁,唤醒等待该锁的线程
pthread_mutex_destroy() 销毁互斥锁,释放资源
(3)互斥锁示例(保护共享变量)
cpp 复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int g_count = 0;
pthread_mutex_t mutex; // 互斥锁

void* count_fun(void* arg) {
    for (int i = 0; i < 10000; i++) {
        pthread_mutex_lock(&mutex); // 加锁:进入临界区
        g_count++;
        pthread_mutex_unlock(&mutex); // 解锁:退出临界区
    }
    pthread_exit(NULL);
}

int main() {
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, count_fun, NULL);
    pthread_create(&tid2, NULL, count_fun, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    printf("最终计数结果:%d\n", g_count); // 正确结果:20000
    
    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

3. 读写锁(线程专用,区分读写场景)

(1)核心概念
  • 特性:允许多个线程同时读操作,写操作需独占(读 - 读并行,读 - 写互斥,写 - 写互斥);
  • 适用场景:读操作远多于写操作的场景(如配置文件读取、数据查询),比互斥锁效率更高。
(2)读写锁核心函数
函数名 功能说明
pthread_rwlock_init() 初始化读写锁,原型:int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
pthread_rwlock_rdlock() 加读锁:无写锁时可获取,允许多个线程同时持有读锁
pthread_rwlock_wrlock() 加写锁:无读锁 / 写锁时可获取,独占资源
pthread_rwlock_unlock() 解锁:释放读锁或写锁,唤醒等待线程
pthread_rwlock_destroy() 销毁读写锁,释放资源
cpp 复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int g_data = 100; // 共享数据
pthread_rwlock_t rwlock; // 读写锁

// 读线程函数:多次读取共享数据
void* read_fun(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 5; i++) {
        pthread_rwlock_rdlock(&rwlock); // 加读锁
        printf("读线程%d:读取g_data=%d\n", id, g_data);
        sleep(1); // 模拟读耗时
        pthread_rwlock_unlock(&rwlock); // 解读锁
        sleep(1);
    }
    pthread_exit(NULL);
}

// 写线程函数:修改共享数据
void* write_fun(void* arg) {
    for (int i = 0; i < 2; i++) {
        pthread_rwlock_wrlock(&rwlock); // 加写锁
        g_data += 50;
        printf("【写线程】:修改g_data=%d\n", g_data);
        sleep(2); // 模拟写耗时
        pthread_rwlock_unlock(&rwlock); // 解写锁
        sleep(1);
    }
    pthread_exit(NULL);
}

int main() {
    pthread_rwlock_init(&rwlock, NULL);
    
    pthread_t tid_read1, tid_read2, tid_write;
    int id1 = 1, id2 = 2;
    
    // 创建2个读线程,1个写线程
    pthread_create(&tid_read1, NULL, read_fun, &id1);
    pthread_create(&tid_read2, NULL, read_fun, &id2);
    pthread_create(&tid_write, NULL, write_fun, NULL);
    
    // 等待所有线程执行完毕
    pthread_join(tid_read1, NULL);
    pthread_join(tid_read2, NULL);
    pthread_join(tid_write, NULL);
    
    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}
  • 运行结果:两个读线程可同时读取数据,写线程执行时会阻塞所有读 / 写线程,体现 "读 - 读并行,读 - 写互斥" 特性。

4. 条件变量(线程专用,实现线程间同步)

(1)核心概念
  • 本质:用于线程间的 "条件等待",当条件不满足时,线程阻塞等待;当条件满足时,被其他线程唤醒继续执行;
  • 搭配使用:必须与互斥锁配合(防止条件判断与阻塞操作之间被打断)。
(2)条件变量核心函数
函数名 功能说明
pthread_cond_init() 初始化条件变量
pthread_cond_wait() 阻塞线程,等待条件满足,同时释放互斥锁(原子操作)
pthread_cond_signal() 唤醒一个等待该条件变量的线程
pthread_cond_broadcast() 唤醒所有等待该条件变量的线程
pthread_cond_destroy() 销毁条件变量

五、进程与线程的特殊场景

1. fork()与线程的关系

  • fork()创建子进程时,子进程仅复制父进程中调用fork()的线程(通常是主线程),其他副线程不会被复制;
  • 子进程会复制父进程的锁(互斥锁、读写锁)及其状态,但父子进程的锁相互独立,各自管理所属进程的资源;
  • 若父进程在持有锁时调用fork(),子进程会继承锁的 "已锁定" 状态,但无法解锁(因为锁的持有者是父进程的线程),可能导致死锁。

2. 线程调度的资源类型

线程调度器分配的核心资源包括:

  1. CPU 时间片(决定线程执行的先后顺序和时长);
  2. 内存空间(共享进程内存,无需单独分配);
  3. 系统设备(如打印机、显卡,通过进程文件描述符访问);
  4. 网络带宽(通过进程的套接字资源共享);
  5. 信号量与锁(同步互斥工具,控制临界资源访问);
  6. 文件描述符(共享进程的文件、管道等资源句柄)。

3. 线程同步的核心原则

  • 临界区代码尽可能简短,将非临界区代码(如休眠、无关计算)放在同步锁之外,提高并发效率;
  • 避免嵌套锁(防止死锁);
  • 优先使用轻量级同步机制(互斥锁 > 信号量);
  • 保证共享资源的访问唯一性,所有线程必须通过同步机制访问临界资源。

六、补充说明

1. 编译与运行通用要求

功能场景 头文件 编译参数 运行方式
基础线程操作 pthread.h -lpthread 直接运行可执行文件,如./thread_demo
线程信号量 semaphore.hpthread.h -lpthread 同上
互斥锁 / 读写锁 pthread.h -lpthread 同上
进程信号量 sys/sem.h 无需额外参数 同上
共享内存 sys/shm.h 无需额外参数 多进程需分别运行,如./shm_write./shm_read

2. 关键注意事项

  • 主线程应最后退出,避免提前终止导致副线程被强制销毁;
  • 动态分配的线程返回值需手动释放(防止内存泄漏);
  • 线程退出优先使用pthread_exit(),而非exit()exit()会终止整个进程);
  • 同步机制使用完毕后必须销毁(如sem_destroy()pthread_mutex_destroy()),释放系统资源。
相关推荐
舰长1152 小时前
ubuntu挂载盘报错
linux·运维·ubuntu
p@nd@2 小时前
测试linux页缓存对磁盘IO的影响
linux·性能测试·页缓存
Hard but lovely2 小时前
Linux: posix标准:线程互斥&& 互斥量的原理&&抢票问题
linux·开发语言
漫漫求2 小时前
ubuntu设置软件开机自启动
linux·运维·ubuntu
Scholar With Saber2 小时前
kali Linux安装教程,ISO镜像安装(物理机,虚拟机皆可)kali安装2025最新,0基础可用,保姆级图文
linux·运维·网络安全
网硕互联的小客服2 小时前
哪些外在因素条件会导致服务器的延迟过高?
linux·运维·服务器·数据库·安全
wregjru2 小时前
【操作系统】2.用户和权限
linux·服务器·unix
甘韦2 小时前
CentOS 7更换阿里云的源
linux·阿里云·centos
写代码的学渣2 小时前
nmon下载安装使用方法
linux·运维