Linux进程与线程编程详解

Linux进程与线程编程详解

简介:进程和线程是操作系统中最核心的概念之一,也是系统编程的基础。无论你是开发高性能服务器、嵌入式系统,还是日常的命令行工具,理解进程与线程的工作原理都是必不可少的。本文将从进程基础、进程间通信(IPC)、线程编程、线程同步机制等多个维度,结合大量代码示例,带你全面掌握Linux下的进程与线程编程技术。内容涵盖 fork/exec/wait 家族、管道/共享内存/消息队列/信号量/信号、pthread 线程库、互斥锁/条件变量/读写锁/自旋锁等核心知识点。


一、进程基础

1.1 进程与程序的区别

进程和程序是两个容易混淆但又截然不同的概念:

  • 程序是静态的,是一堆指令的集合,存储在磁盘上,本身没有运行的含义。
  • 进程是动态的,是程序在计算机上的一次执行活动,有一定的生命周期。
  • 一个进程只能对应一个程序,但一个程序可以对应多个进程。

进程是操作系统进行资源分配的基本单位,每个进程拥有独立的虚拟地址空间、文件描述符表、信号处理等资源。进程号(PID)是进程的唯一标识。

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("当前进程PID: %d\n", getpid());
    printf("父进程PID: %d\n", getppid());
    return 0;
}

1.2 fork() -- 创建子进程

fork() 是 Linux 中创建新进程最基本的方式。它会以父进程为蓝本复制一个子进程,子进程获得父进程数据空间、堆和栈的副本。

fork() 最显著的特点是执行一次,返回两次:在父进程中返回子进程的 PID,在子进程中返回 0。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid;

    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程
        printf("我是子进程,PID = %d,父进程PID = %d\n",
               getpid(), getppid());
    } else {
        // 父进程
        printf("我是父进程,PID = %d,子进程PID = %d\n",
               getpid(), pid);
    }

    return 0;
}

vfork() 与 fork() 的区别

  • fork() 创建的子进程会复制父进程的地址空间。
  • vfork() 创建的子进程并不将父进程地址空间完全复制,而是与父进程共享地址空间
  • vfork() 保证子进程先运行,在子进程调用 execexit 之前,父进程会被阻塞。

1.3 exec 函数族 -- 替换进程映像

exec 函数族用新的程序替换当前进程的映像。调用 exec 后,进程的 PID 不变,但代码段、数据段、堆栈等都被新程序替换。

c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程中执行 ls 命令
        execlp("ls", "ls", "-l", NULL);
        // 如果 exec 成功,下面的代码不会执行
        perror("exec failed");
        exit(1);
    } else if (pid > 0) {
        wait(NULL);  // 等待子进程结束
        printf("子进程已执行完毕\n");
    }
    return 0;
}

exec 函数族共有 6 个函数,其中只有 execve 是真正的系统调用,其余都是基于它的库函数封装:

函数 路径/文件名 参数形式 环境变量
execl 路径名 列表 继承
execlp 文件名 列表 继承
execle 路径名 列表 自定义
execv 路径名 数组 继承
execvp 文件名 数组 继承
execve 文件名 数组 自定义

1.4 wait / waitpid -- 回收子进程

父进程必须主动回收子进程的资源,否则子进程会变成僵尸进程(Zombie)。僵尸进程虽然已经终止,但其进程描述符仍然保留在内核中。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("子进程运行中,PID = %d\n", getpid());
        sleep(2);
        exit(0);
    } else if (pid > 0) {
        int status;
        // WNOHANG: 非阻塞方式,如果没有子进程退出则立即返回0
        pid_t ret = waitpid(-1, &status, WNOHANG);
        if (ret == 0) {
            printf("子进程尚未退出,父进程继续做其他工作...\n");
        }
        // 阻塞等待子进程退出
        ret = waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

关键要点

  • wait() 会阻塞等待任意一个子进程结束。
  • waitpid(-1, NULL, WNOHANG) 是非阻塞版本,常用于并发服务器中回收子进程。
  • 在并发服务器中,不能使用 wait() 来回收子进程,因为 wait() 会阻塞,必须使用 waitpid() 配合 WNOHANG 选项。

1.5 system() 函数

system() 函数封装了 fork()execve()waitpid() 的调用过程,可以直接执行 shell 命令:

c 复制代码
#include <stdlib.h>
#include <stdio.h>

int main() {
    int ret = system("ls -l /tmp");
    if (ret == -1) {
        perror("system() failed");
    } else if (WIFEXITED(ret)) {
        printf("命令执行完毕,退出码: %d\n", WEXITSTATUS(ret));
    }
    return 0;
}

二、进程间通信(IPC)

Linux 的 IPC 机制基本上都是从 Unix 平台继承而来的,主要包括四大类:

  1. Unix 早期 IPC:半双工管道、FIFO(命名管道)、信号
  2. System V IPC:消息队列、信号量、共享内存
  3. POSIX IPC:POSIX 消息队列、POSIX 信号量、POSIX 共享内存
  4. 基于 Socket 的 IPC

2.1 管道(Pipe)

管道是最古老的 IPC 方式,本质上是内核中的一块缓冲区。管道是半双工的,数据只能单向流动。

特点

  • 只有当管道写满时 write 才会阻塞
  • 当管道没有数据时 read 会阻塞
  • 管道中的数据按顺序读取(先进先出)
  • 只能用于有亲缘关系的进程之间通信(通常是父子进程)
c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];
    pid_t pid;
    char buf[128];

    if (pipe(fd) < 0) {
        perror("pipe failed");
        return 1;
    }

    pid = fork();
    if (pid == 0) {
        // 子进程 -- 写端
        close(fd[0]);  // 关闭读端
        char *msg = "Hello from child process!";
        write(fd[1], msg, strlen(msg) + 1);
        close(fd[1]);
    } else {
        // 父进程 -- 读端
        close(fd[1]);  // 关闭写端
        read(fd[0], buf, sizeof(buf));
        printf("父进程收到: %s\n", buf);
        close(fd[0]);
        wait(NULL);
    }
    return 0;
}

如果不希望管道阻塞,可以通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置非阻塞模式。

2.2 命名管道(FIFO)

命名管道是一种特殊文件,存在于文件系统中,允许无亲缘关系的进程之间进行通信。与匿名管道不同,FIFO 可以实现全双工通信。

c 复制代码
/* 创建命名管道 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define FIFO_PATH "/tmp/myfifo"

int main() {
    mkfifo(FIFO_PATH, 0666);  // 创建命名管道文件

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程 -- 写入数据
        int fd = open(FIFO_PATH, O_WRONLY);
        char *msg = "Hello via FIFO!";
        write(fd, msg, strlen(msg) + 1);
        close(fd);
    } else {
        // 父进程 -- 读取数据
        int fd = open(FIFO_PATH, O_RDONLY);
        char buf[128];
        read(fd, buf, sizeof(buf));
        printf("收到消息: %s\n", buf);
        close(fd);
        unlink(FIFO_PATH);  // 删除管道文件
    }
    return 0;
}

FIFO 总是处于阻塞状态,如果设置了读权限则一直阻塞到有进程写入数据,反之亦然。可以在 open 时设置 O_NONBLOCK 选项取消阻塞。

2.3 消息队列(Message Queue)

消息队列是内核空间中的内部链表,通过 Linux 内核在各个进程之间传递内容。每个消息队列由一个唯一的 IPC 标识符区分。

c 复制代码
/* 消息队列示例 -- 发送端 */
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>
#include <sys/ipc.h>

struct msgbuf {
    long mtype;         // 消息类型(必须大于0)
    char mtext[128];    // 消息数据
};

int main() {
    key_t key = ftok("/tmp", 'A');       // 生成键值
    int msgid = msgget(key, IPC_CREAT | 0666);  // 创建消息队列

    struct msgbuf msg;
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello Message Queue!");

    msgsnd(msgid, &msg, sizeof(msg.mtext), 0);  // 发送消息
    printf("消息已发送\n");
    return 0;
}
c 复制代码
/* 消息队列示例 -- 接收端 */
#include <stdio.h>
#include <sys/msg.h>
#include <sys/ipc.h>

struct msgbuf {
    long mtype;
    char mtext[128];
};

int main() {
    key_t key = ftok("/tmp", 'A');
    int msgid = msgget(key, IPC_CREAT | 0666);

    struct msgbuf msg;
    // 接收类型为1的消息
    msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
    printf("收到消息: %s\n", msg.mtext);

    // 删除消息队列
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

消息队列的一个重要特性是可以通过 mtype 来筛选接收不同类型的消息,实现灵活的消息路由。

2.4 共享内存(Shared Memory)

共享内存是最高效的 IPC 方式,因为进程直接读写同一块内存,没有数据拷贝的中间过程。管道、消息队列等都需要建立中间机制,而共享内存只需对某段内存进行映射。但是,共享内存本身不提供同步机制,需要配合信号量使用。

c 复制代码
/* 共享内存示例 -- 写入端 */
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/ipc.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp", 'B');
    // 创建共享内存
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);

    // 将共享内存映射到当前进程的地址空间
    char *shmaddr = (char *)shmat(shmid, NULL, 0);

    printf("写入数据到共享内存...\n");
    strcpy(shmaddr, "Hello Shared Memory!");

    // 解除映射
    shmdt(shmaddr);
    return 0;
}
c 复制代码
/* 共享内存示例 -- 读取端 */
#include <stdio.h>
#include <sys/shm.h>
#include <sys/ipc.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp", 'B');
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);

    char *shmaddr = (char *)shmat(shmid, NULL, SHM_RDONLY);
    printf("从共享内存读到: %s\n", shmaddr);

    shmdt(shmaddr);
    // 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

2.5 信号量(Semaphore)

信号量本质上是一个计数器,用来控制对共享资源的访问,常用于实现进程间的同步与互斥 。经典的应用场景是生产者-消费者问题(PV操作)。

  • P操作:申请资源,信号量减1。如果信号量值小于0则阻塞等待。
  • V操作:释放资源,信号量加1。如果有阻塞的进程则唤醒。
c 复制代码
/* 信号量示例 -- P/V操作 */
#include <stdio.h>
#include <sys/sem.h>
#include <sys/ipc.h>

// 在某些系统上需要自定义 union semun
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int main() {
    key_t key = ftok("/tmp", 'C');
    int semid = semget(key, 1, IPC_CREAT | 0666);

    // 初始化信号量值为1
    union semun arg;
    arg.val = 1;
    semctl(semid, 0, SETVAL, arg);

    // P操作 -- 申请资源
    struct sembuf sop_p = {0, -1, SEM_UNDO};
    semop(semid, &sop_p, 1);

    printf("进入临界区,访问共享资源\n");
    // ... 临界区操作 ...

    // V操作 -- 释放资源
    struct sembuf sop_v = {0, +1, SEM_UNDO};
    semop(semid, &sop_v, 1);

    // 删除信号量
    semctl(semid, 0, IPC_RMID);
    return 0;
}

2.6 信号(Signal)

信号是一种异步通信机制,类似于单片机中的中断。进程收到信号后会跳转到对应的信号处理函数执行。

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signum) {
    printf("收到信号 %d\n", signum);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, sig_handler);   // Ctrl+C
    signal(SIGTERM, sig_handler);  // kill 命令

    printf("进程PID: %d,等待信号...\n", getpid());
    while (1) {
        sleep(1);
    }
    return 0;
}

注意事项

  • SIGSTOPSIGKILL 是两个不能被忽略、不能被捕获的信号。
  • kill(pid, sig) 函数可以向指定进程发送信号。
  • raise(sig) 函数可以向当前进程发送信号。

2.7 IPC 方式对比总结

IPC方式 特点 适用场景
管道(pipe) 半双工,亲缘进程间 父子进程通信
命名管道(FIFO) 可用于任意进程 无亲缘关系的进程通信
消息队列 有格式,可按类型接收 需要消息分类的场景
共享内存 速度最快 大量数据传输
信号量 计数器,同步互斥 资源访问控制
信号 异步通知 事件通知、异常处理

共享内存是进程间通信最高效的方法,但需要信号量配合实现同步控制。


三、线程基础

3.1 线程的概念与优势

线程是进程内的一个执行单元,是CPU调度的基本单位 。Linux 下的多线程遵循 POSIX 标准,因此称为 pthread(POSIX thread)。

线程的优点:

  1. 系统资源消耗低:线程共享进程的代码段、数据段、堆等资源。
  2. 速度快:线程创建和切换的开销远小于进程。
  3. 共享容易:线程之间的内存和变量共享比进程间简单得多。

进程与线程的核心区别

  • 进程是资源分配的基本单位,拥有完整的虚拟空间。
  • 线程是CPU调度的基本单位,共享进程的资源(除栈以外)。
  • 多个线程共享代码段、数据段,但不共享堆栈。

3.2 pthread_create -- 创建线程

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* thread_func(void *arg) {
    int *num = (int *)arg;
    printf("线程正在执行,参数: %d\n", *num);
    for (int i = 0; i < 5; i++) {
        printf("线程工作中... %d\n", i);
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    int param = 42;

    // 创建线程
    int ret = pthread_create(&tid, NULL, thread_func, &param);
    if (ret != 0) {
        perror("pthread_create failed");
        return 1;
    }

    printf("主线程继续执行,创建的线程ID: %lu\n", tid);

    // 等待线程结束
    pthread_join(tid, NULL);
    printf("线程已结束\n");

    return 0;
}

编译时需要链接 pthread 库:gcc -o program program.c -lpthread

3.3 pthread_join / pthread_detach

  • pthread_join :阻塞等待指定线程结束,并获取其返回值。类似于进程中的 wait()
  • pthread_detach :将线程设置为分离状态,线程结束后系统自动回收资源,无需其他线程调用 pthread_join
c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* thread_func(void *arg) {
    printf("分离线程开始执行\n");
    sleep(2);
    printf("分离线程执行完毕\n");
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);

    // 设置为分离状态
    pthread_detach(tid);

    // 主线程无需等待分离线程
    printf("主线程继续执行其他工作...\n");
    sleep(3);  // 确保分离线程有时间执行完毕

    return 0;
}

3.4 线程退出与取消

c 复制代码
// 线程主动退出
void pthread_exit(void *retval);

// 取消指定线程
int pthread_cancel(pthread_t thread);

线程可以通过 pthread_exit() 主动退出,也可以被其他线程通过 pthread_cancel() 取消。


四、线程同步机制

当多个线程同时访问共享资源时,如果不进行同步控制,会导致数据不一致或竞争条件。Linux 提供了多种线程同步机制。

4.1 互斥锁(Mutex)

互斥锁是最常用的线程同步方式,用于保护临界区,确保同一时刻只有一个线程能访问共享资源。

c 复制代码
#include <stdio.h>
#include <pthread.h>

int shared_counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* thread_increment(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);     // 加锁
        shared_counter++;
        pthread_mutex_unlock(&mutex);   // 解锁
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, thread_increment, NULL);
    pthread_create(&tid2, NULL, thread_increment, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("最终计数器值: %d\n", shared_counter);
    pthread_mutex_destroy(&mutex);
    return 0;
}

互斥锁的关键函数

函数 说明
pthread_mutex_init() 初始化互斥锁
pthread_mutex_lock() 加锁(阻塞)
pthread_mutex_trylock() 尝试加锁(非阻塞版本)
pthread_mutex_unlock() 解锁
pthread_mutex_destroy() 销毁互斥锁

pthread_mutex_lock 是阻塞的:当尝试锁定一个已被锁定的资源时,调用线程会被阻塞。而 pthread_mutex_trylock 不会阻塞,尝试加锁失败时立即返回错误码。

也可以使用宏 PTHREAD_MUTEX_INITIALIZER 进行静态初始化。

4.2 条件变量(Condition Variable)

条件变量用于线程间的通知-等待机制,通常与互斥锁配合使用。一个线程等待某个条件成立,另一个线程在条件满足时发出通知。

c 复制代码
#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

void* consumer(void *arg) {
    pthread_mutex_lock(&mutex);
    while (!ready) {
        printf("消费者等待条件满足...\n");
        pthread_cond_wait(&cond, &mutex);  // 等待信号,自动释放mutex
    }
    printf("消费者: 条件已满足,开始处理\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* producer(void *arg) {
    pthread_mutex_lock(&mutex);
    ready = 1;
    printf("生产者: 条件已设置,发出通知\n");
    pthread_cond_signal(&cond);  // 唤醒等待的线程
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, consumer, NULL);
    sleep(1);  // 确保消费者先运行
    pthread_create(&tid2, NULL, producer, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

注意事项

  • pthread_cond_wait() 会自动释放互斥锁并进入等待状态,被唤醒时重新获取锁。
  • 条件检查应使用 while 循环而非 if,防止虚假唤醒。

4.3 读写锁(Read-Write Lock)

读写锁允许多个线程同时读取共享资源,但写操作是独占的。适用于读多写少的场景。

c 复制代码
#include <stdio.h>
#include <pthread.h>

pthread_rwlock_t rwlock;
int shared_data = 0;

void* reader(void *arg) {
    pthread_rwlock_rdlock(&rwlock);  // 获取读锁
    printf("读者线程: 读取数据 = %d\n", shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

void* writer(void *arg) {
    pthread_rwlock_wrlock(&rwlock);  // 获取写锁
    shared_data++;
    printf("写者线程: 更新数据 = %d\n", shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

int main() {
    pthread_t tids[5];
    pthread_rwlock_init(&rwlock, NULL);

    // 创建多个读者和写者
    for (int i = 0; i < 3; i++)
        pthread_create(&tids[i], NULL, reader, NULL);
    for (int i = 3; i < 5; i++)
        pthread_create(&tids[i], NULL, writer, NULL);

    for (int i = 0; i < 5; i++)
        pthread_join(tids[i], NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

4.4 自旋锁(Spinlock)

自旋锁与互斥锁类似,但不会使线程睡眠 ,而是在获取锁失败时不断循环("自旋")等待。适用于临界区非常短的场景,避免了线程切换的开销。

c 复制代码
#include <pthread.h>

pthread_spinlock_t spinlock;
int counter = 0;

void* thread_func(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_spin_lock(&spinlock);
        counter++;
        pthread_spin_unlock(&spinlock);
    }
    return NULL;
}

int main() {
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, thread_func, NULL);
    pthread_create(&tid2, NULL, thread_func, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("最终值: %d\n", counter);
    pthread_spin_destroy(&spinlock);
    return 0;
}

自旋锁 vs 互斥锁

  • 自旋锁在等待期间一直占用 CPU,适用于锁持有时间极短的场景。
  • 互斥锁在等待时线程会被挂起,CPU 可以执行其他任务,适用于锁持有时间较长的场景。

4.5 线程信号量

线程间的信号量与进程间信号量类似,但可以高效地完成基于线程的资源计数。

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;
int buffer[10];
int index = 0;

void* producer(void *arg) {
    for (int i = 0; i < 10; i++) {
        sem_wait(&sem);      // P操作,信号量减1
        buffer[index++] = i;
        printf("生产: %d\n", i);
        sem_post(&sem);      // V操作,信号量加1
    }
    return NULL;
}

void* consumer(void *arg) {
    for (int i = 0; i < 10; i++) {
        sem_wait(&sem);
        if (index > 0) {
            int val = buffer[--index];
            printf("消费: %d\n", val);
        }
        sem_post(&sem);
    }
    return NULL;
}

int main() {
    sem_init(&sem, 0, 1);  // 初始值为1

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, producer, NULL);
    pthread_create(&tid2, NULL, consumer, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    sem_destroy(&sem);
    return 0;
}

4.6 同步机制对比

机制 特点 适用场景
互斥锁 独占访问,阻塞等待 保护临界区
条件变量 等待-通知机制 生产者-消费者模型
读写锁 读共享,写独占 读多写少场景
自旋锁 忙等待,不睡眠 极短临界区
信号量 计数器机制 资源计数、流控

五、多线程编程模式

5.1 线程池模式

线程池预先创建一组线程,任务到来时分配给空闲线程处理,避免频繁创建和销毁线程的开销。

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_NUM 4
#define TASK_NUM   10

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int task_queue[TASK_NUM];
int task_count = 0;
int task_index = 0;
int stop = 0;

void* worker(void *arg) {
    int id = *(int *)arg;
    while (1) {
        pthread_mutex_lock(&mutex);
        while (task_count == 0 && !stop) {
            pthread_cond_wait(&cond, &mutex);
        }
        if (stop && task_count == 0) {
            pthread_mutex_unlock(&mutex);
            break;
        }
        int task = task_queue[task_index - task_count];
        task_count--;
        pthread_mutex_unlock(&mutex);

        printf("线程 %d 处理任务 %d\n", id, task);
        sleep(1);  // 模拟任务处理
    }
    printf("线程 %d 退出\n", id);
    return NULL;
}

int main() {
    pthread_t threads[THREAD_NUM];
    int ids[THREAD_NUM];

    // 创建工作线程
    for (int i = 0; i < THREAD_NUM; i++) {
        ids[i] = i + 1;
        pthread_create(&threads[i], NULL, worker, &ids[i]);
    }

    // 添加任务
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < TASK_NUM; i++) {
        task_queue[task_index++] = i + 100;
        task_count++;
    }
    pthread_cond_broadcast(&cond);  // 唤醒所有工作线程
    pthread_mutex_unlock(&mutex);

    sleep(5);  // 等待任务完成

    // 停止线程池
    pthread_mutex_lock(&mutex);
    stop = 1;
    pthread_cond_broadcast(&cond);
    pthread_mutex_unlock(&mutex);

    for (int i = 0; i < THREAD_NUM; i++)
        pthread_join(threads[i], NULL);

    return 0;
}

5.2 并发服务器模式(多进程版)

在并发服务器中,每接收一个用户请求,就 fork 一个子进程来处理。服务器主进程必须主动回收子进程,否则会产生僵尸进程。

c 复制代码
/* 并发TCP服务器框架 */
while (1) {
    int cfd = accept(listenfd, NULL, NULL);
    pid_t pid = fork();
    if (pid == 0) {
        close(listenfd);   // 子进程关闭监听套接字
        // 处理客户端请求
        handle_client(cfd);
        close(cfd);
        exit(0);
    } else {
        close(cfd);        // 父进程关闭连接套接字
        // 非阻塞回收子进程
        waitpid(-1, NULL, WNOHANG);
    }
}

5.3 线程属性设置

c 复制代码
pthread_attr_t attr;
pthread_attr_init(&attr);

// 设置分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

// 设置栈大小
pthread_attr_setstacksize(&attr, 1024 * 1024);  // 1MB

// 设置调度优先级
struct sched_param param;
param.sched_priority = 50;
pthread_attr_setschedparam(&attr, &param);

pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr);

六、进程与线程对比

对比维度 进程 线程
定义 资源分配的基本单位 CPU调度的基本单位
地址空间 独立的虚拟地址空间 共享进程的地址空间
创建开销 大(需要复制地址空间) 小(共享大部分资源)
通信方式 管道/FIFO/消息队列/共享内存/信号/Socket 共享变量/互斥锁/条件变量
健壮性 一个进程崩溃不影响其他进程 一个线程崩溃可能导致整个进程退出
切换开销 大(需要切换地址空间) 小(共享地址空间)
编程复杂度 IPC编程较复杂 同步控制需小心处理
适用场景 需要隔离性、稳定性的场景 需要高并发、低开销的场景

选择建议

  • 如果需要高稳定性和隔离性,选择多进程
  • 如果需要高并发性能和资源共享的便捷性,选择多线程
  • 实际项目中经常混合使用:主进程 fork 多个子进程,每个子进程内部创建多个线程。

总结

本文从进程基础到线程同步,系统地介绍了 Linux 下进程与线程编程的核心技术:

  1. 进程基础 :掌握了 fork()/exec/wait 系列函数的用法,理解了进程的创建、替换和回收机制。
  2. 进程间通信:了解了管道、命名管道、消息队列、共享内存、信号量和信号六大 IPC 机制的特点和适用场景,其中共享内存效率最高但需要信号量配合。
  3. 线程编程:学会了使用 pthread 库创建、管理和控制线程,理解了线程与进程的本质区别。
  4. 线程同步:深入理解了互斥锁、条件变量、读写锁、自旋锁和信号量五种同步机制的使用方法和适用场景。
  5. 编程模式:了解了线程池、并发服务器等常见的多线程/多进程编程模式。

进程与线程编程是系统编程的基石,掌握好这些知识,将为后续学习网络编程、数据库开发、高性能服务器设计等打下坚实基础。


原始笔记来源:frasight/上课笔记.cjdah/StudyC.c

相关推荐
我星期八休息2 小时前
IT疑难杂症诊疗室:AI时代工程师Superpowers进化论
linux·开发语言·数据结构·人工智能·python·散列表
切糕师学AI2 小时前
深入解析 Zsh 与 Oh-My-Zsh:打造高效现代化终端
linux·终端·zsh
A7bert7772 小时前
【YOLOv8pose部署至RDK X5】模型训练→转换bin→Sunrise 5部署
c++·python·深度学习·yolo·目标检测
li1670902703 小时前
第二十七章:智能指针
c语言·数据结构·c++·visual studio
切糕师学AI3 小时前
Ubuntu 下 Git 完全使用指南
linux·git·ubuntu
浪客灿心3 小时前
Linux网络传输层协议
linux·运维·网络
王老师青少年编程4 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【贪心与二分判定】:数列分段 Section II
c++·算法·贪心·csp·信奥赛·二分判定·数列分段 section ii
zh_xuan4 小时前
libcurl调用https接口
c++·libcurl
就叫飞六吧4 小时前
QT写一个桌面程序exe并动态打包基本流程(c++)
开发语言·c++