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()保证子进程先运行,在子进程调用exec或exit之前,父进程会被阻塞。
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 平台继承而来的,主要包括四大类:
- Unix 早期 IPC:半双工管道、FIFO(命名管道)、信号
- System V IPC:消息队列、信号量、共享内存
- POSIX IPC:POSIX 消息队列、POSIX 信号量、POSIX 共享内存
- 基于 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;
}
注意事项:
SIGSTOP和SIGKILL是两个不能被忽略、不能被捕获的信号。kill(pid, sig)函数可以向指定进程发送信号。raise(sig)函数可以向当前进程发送信号。
2.7 IPC 方式对比总结
| IPC方式 | 特点 | 适用场景 |
|---|---|---|
| 管道(pipe) | 半双工,亲缘进程间 | 父子进程通信 |
| 命名管道(FIFO) | 可用于任意进程 | 无亲缘关系的进程通信 |
| 消息队列 | 有格式,可按类型接收 | 需要消息分类的场景 |
| 共享内存 | 速度最快 | 大量数据传输 |
| 信号量 | 计数器,同步互斥 | 资源访问控制 |
| 信号 | 异步通知 | 事件通知、异常处理 |
共享内存是进程间通信最高效的方法,但需要信号量配合实现同步控制。
三、线程基础
3.1 线程的概念与优势
线程是进程内的一个执行单元,是CPU调度的基本单位 。Linux 下的多线程遵循 POSIX 标准,因此称为 pthread(POSIX thread)。
线程的优点:
- 系统资源消耗低:线程共享进程的代码段、数据段、堆等资源。
- 速度快:线程创建和切换的开销远小于进程。
- 共享容易:线程之间的内存和变量共享比进程间简单得多。
进程与线程的核心区别:
- 进程是资源分配的基本单位,拥有完整的虚拟空间。
- 线程是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, ¶m);
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, ¶m);
pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr);
六、进程与线程对比
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU调度的基本单位 |
| 地址空间 | 独立的虚拟地址空间 | 共享进程的地址空间 |
| 创建开销 | 大(需要复制地址空间) | 小(共享大部分资源) |
| 通信方式 | 管道/FIFO/消息队列/共享内存/信号/Socket | 共享变量/互斥锁/条件变量 |
| 健壮性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程退出 |
| 切换开销 | 大(需要切换地址空间) | 小(共享地址空间) |
| 编程复杂度 | IPC编程较复杂 | 同步控制需小心处理 |
| 适用场景 | 需要隔离性、稳定性的场景 | 需要高并发、低开销的场景 |
选择建议:
- 如果需要高稳定性和隔离性,选择多进程。
- 如果需要高并发性能和资源共享的便捷性,选择多线程。
- 实际项目中经常混合使用:主进程 fork 多个子进程,每个子进程内部创建多个线程。
总结
本文从进程基础到线程同步,系统地介绍了 Linux 下进程与线程编程的核心技术:
- 进程基础 :掌握了
fork()/exec/wait系列函数的用法,理解了进程的创建、替换和回收机制。 - 进程间通信:了解了管道、命名管道、消息队列、共享内存、信号量和信号六大 IPC 机制的特点和适用场景,其中共享内存效率最高但需要信号量配合。
- 线程编程:学会了使用 pthread 库创建、管理和控制线程,理解了线程与进程的本质区别。
- 线程同步:深入理解了互斥锁、条件变量、读写锁、自旋锁和信号量五种同步机制的使用方法和适用场景。
- 编程模式:了解了线程池、并发服务器等常见的多线程/多进程编程模式。
进程与线程编程是系统编程的基石,掌握好这些知识,将为后续学习网络编程、数据库开发、高性能服务器设计等打下坚实基础。
原始笔记来源:frasight/上课笔记.c、jdah/StudyC.c