进程与线程
进程和线程
进程
基本概念
- 进程:程序动态执行的过程,包括创建、调度和消亡。
- 程序:存放在外存中的一段数据的集合。
进程创建
进程空间分布:
每个进程运行后,操作系统为其分配0-4GB的虚拟内存空间。
进程空间分为用户空间和内核空间(不允许用户访问)。用户空间组成:
文本段(代码区):存放程序代码和指令。
数据段(数据区):
- 字符串常量
- 已初始化的全局变量/静态变量
- 未初始化的全局变量/静态变量(默认值为0)
特点: - 编译时分配空间
- 程序结束时回收
系统数据段:
- 堆区:通过
malloc
申请空间,free
释放空间。- 需避免内存泄漏
- 栈区:存放局部变量和函数运行时数据。
特点:- 未初始化时为随机值
- 变量定义时分配空间
- 超出作用域后自动回收
- 堆区:通过
2. 虚拟地址和物理地址:
- 1. 虚拟地址: 所有能够被用户看到的地址均为虚拟地址,表示用户可以寻址的范围
- 2. 物理地址: 内存存放数据对应的实际硬件物理地址
- 3. MMU: 实际地址和物理地址之间的映射由内存映射单元(MMU)完成
3. 多进程存储:
1. 存储方式:
- 多个进程空间在操作系统中存储时,空间是独立的(物理地址是独立的)
- 多个进程在操作系统中共用同一个虚拟内存空间(虚拟地址是共享的)
4. 多进程的调度:
1. 常见的进程调度算法:
- 1. 先来先执行,后来后执行
- 2. 高优先级调度算法
- 3. 时间片轮转调度算法
2. 多进程调度本质:
宏观并行,微观串行
宏观分析:
一个CPU同时执行多个进程任务
微观分析:
一个CPU在多个任务中高速切换保障多任务执行
5. 进程相关的命令:
1. top
2. ps -ef
3. ps -aux
4. 后台执行进程任务
5. jobs
6. fg
7. nice/renice
8. kill/killall
6. 进程的状态:
3.进程相关函数接口
1. fork
注意:
- 子进程拷贝父进程文本段、数据段、系统数据段
- 进程的PID不一样
- fork的返回值不一样,父进程中返回子进程的PID,子进程中返回0
- PID:一定是 > 0
示例
#include "../head.h"int main(void)
{pid_t pid1;pid_t pid2;pid1 = fork();if (-1 == pid1){perror("fail to fork");return -1;}if (0 == pid1){printf("子进程1 PID:%d, PPID:%d\n", getpid(), getppid());}else if (pid1 > 0){pid2 = fork();if (-1 == pid2){perror("fail to fork");return -1;}if (0 == pid2){printf("子进程2 PID:%d, PPID:%d\n", getpid(), getppid());}else if (pid2 > 0){printf("父进程 PID:%d, child1PID:%d, child2PID:%d\n", getpid(), pid1, pid2);}}return 0;
}
2. getpid和getppid
示例
#include "../head.h"int main(void)
{pid_t pid1;pid_t pid2;pid1 = fork();if (-1 == pid1){perror("fail to fork");return -1;}if (0 == pid1){printf("子进程1 PID:%d, PPID:%d\n", getpid(), getppid());}else if (pid1 > 0){pid2 = fork();if (-1 == pid2){perror("fail to fork");return -1;}if (0 == pid2){printf("子进程2 PID:%d, PPID:%d\n", getpid(), getppid());}else if (pid2 > 0){printf("父进程 PID:%d, child1PID:%d, child2PID:%d\n", getpid(), pid1, pid2);}}return 0;
}
3. exit与_exit
注意:
- 在主函数中调用exit和return功能保持一致
- return在函数内部将结束该函数
- exit在函数内部会将进程结束
- exit会在结束前刷新缓存区
- _exit不会刷新缓存区
4. 进程回收:
wait
注意:
- wait具有阻塞等待功能,等到有子进程结束才会回收子进程继续向下执行
- wait可以实现父子进程任务的同步
waitpid
注意:
- waitpid可以非阻塞回收子进程空间
- waitpid可以回收指定子进程空间
4.进程消亡
1. 孤儿进程:
父进程先结束,子进程会成为孤儿进程,被init进程收养
2. 僵尸进程:
是每个进程结束必然会经历的阶段
产生原因:
子进程结束后,父进程没有回收子进程空间,导致进程执行结束,空间依然被占用的状 态,称为僵尸进程
如何避免产生僵尸进程?
- 父进程先结束,子进程会成为孤儿进程,孤儿进程被init进程收养,子进程再结束,init 进程回收进程空间
- 子进程结束,父进程回收子进程空间,避免产生僵尸进程
5.exec函数族
1. exec函数族
- 利用进程空间执行另一份代码
- exec常搭配fork使用,fork负责创建新的子进程,exec负责让子进程执行自己的代码
#include "../head.h"int main(void)
{char *parg[5] = {"./hello","how","are","you",NULL,};printf("execl上面\n");
// execl("./hello", "./hello", "how", "are", "you", NULL);execv(parg[0], parg);printf("看到我表示exec出错了\n");printf("execl下面\n");return 0;
}
2. 主函数传参
1. 主函数形式:
3. system函数:
#include "../head.h"void mysystem(void)
{pid_t pid;pid = fork();if(-1 == pid){perror("fail to fork");return;}if (0 == pid){execlp("ls", "ls", "-l", NULL);}wait(NULL);return;
}int main(void)
{printf("system上面\n");mysystem();printf("system下面\n");return 0;
}
int main() { | if (fork() == 0) { /子进程/ } | |
pthread_t tid; | else { /父进程/ } | |
pthread_create(&tid, NULL, task, NULL); | } | |
} | ``` | |
``` |
关键差异说明
性能差异
多线程的创建和切换速度更快,适合频繁创建的任务。多进程由于独立的地址空间,更适合需要高安全性和稳定性的场景。
资源共享
线程默认共享堆、全局变量和文件描述符,进程需要显式共享(如通过 shmget
或 mmap
)。
调试难度
多线程因共享数据可能导致竞态条件(Race Condition),调试复杂度较高;多进程的隔离性使得问题更容易定位。
线程
1.基本概念:
- 线程是一个轻量级的进程
- 线程本质就是一个进程
- 线程和进程不完全一致,轻量指空间,进程空间和线程空间管理方法不同
2.进程和线程区别:
1. 线程本质
是进程,线程是任务创建、调度、回收的过程
2. 进程空间:
文本段 + 数据段 + 系统数据段
3. 线程空间:
- 线程必须位于进程空间内部,没有进程,线程无法独立存在
- 一个进程中的所有线程共享文本段+数据段+堆区,独享栈区
- 线程独享的栈区默认为8M
- 一个进程中的多个线程切换调度任务时,资源开销比较小
4. 进程和线程区别:
- 线程是CPU任务调度的最小单元
- 进程是操作系统资源分配的最小单元
3.多进程和多线程的优缺点
1. 多线程和多进程对比:
多线程 vs 多进程对比(C语言)
特性 | 多线程(Thread) | 多进程(Process) |
---|---|---|
创建方式 | 使用 pthread_create (POSIX线程) | 使用 fork 或 exec 系列函数 |
资源开销 | 轻量级,共享同一进程的内存空间 | 重量级,独立内存空间,复制父进程资源(写时复制) |
通信机制 | 共享全局变量、互斥锁(pthread_mutex_t )、条件变量 | 管道(pipe )、共享内存、信号、套接字、消息队列 |
同步机制 | 互斥锁、信号量、读写锁 | 文件锁、信号量、信号 |
数据隔离性 | 线程共享数据,需同步避免竞争 | 进程数据默认隔离,共享需显式操作 |
崩溃影响 | 线程崩溃可能导致整个进程终止 | 单个进程崩溃不影响其他进程 |
上下文切换成本 | 低(共享地址空间) | 高(需切换内存映射表) |
适用场景 | CPU密集型任务(需并行计算)、高响应需求 | 需要高稳定性、隔离性的任务(如服务隔离) |
示例代码 | ```c | ```c |
#include <pthread.h> | #include <unistd.h> | |
void* task(void* arg) { /.../ } | int main() { |
跨平台兼容性
多进程(如 fork
)在 Unix/Linux 系统支持良好,Windows 需使用 CreateProcess
;多线程的 pthread
需注意平台实现差异。
4.线程的调度:
- 调度保持一致
- 宏观并行,微观串行
5.线程的消亡:
线程结束需要回收线程空间,否则产生僵尸线程
6.线程的函数接口
1. 函数接口:
2. pthread_create
3. pthread_self
4. pthread_exit
5. pthread_join
注意:
tid对应的线程只要不退出,pthread_join阻塞等待结束回收线程空间 pthread_join具备同步功能
7.线程传参
1. pthread_create
可以通过pthread_create第四个参数实现对线程内部的传参
#include "../head.h"/* 线程参数类型 */typedef struct pthread_arg{pthread_t tid;
char threadname[32];
int sleeptime;
}pthread_arg_t;//存放线程ID//线程名称
//睡眠时间
void *thread(void *arg){pthread_arg_t *parg = arg;printf("%s(TID:%#lx)开始执行\n", parg->threadname, parg->tid);while (1){printf("%s正在执行\n", parg->threadname);sleep(parg->sleeptime);}return NULL;}int main(void){
int i = 0;pthread_arg_t args[4] = {{0, "采集线程", 1},{0, "存储线程", 2},{0, "显示线程", 5},{0, "日志线程", 10},};for (i = 0; i < 4; i++){pthread_create(&args[i].tid, NULL, thread, &args[i]);}for (i = 0; i < 4; i++){pthread_join(args[i].tid, NULL);}return 0;}
8.线程属性
1. 线程属性:
加入属性:
线程结束需要pthread_join手动回收
分离属性:
线程结束后系统自动回收线程空间
2. 函数接口:
1. pthread_attr_init
2.pthread_attr_setdetachstate
3. pthread_attr_destroy
3. 区别:
1. 分离属性:
- 线程结束后,操作系统自动回收空间
- 不需要手动调用pthread_join回收线程空间
2. 加入属性:
- 可以回收到线程结束的状态
- 可以完成线程间的同步
3.线程间通信:
1.概念:
多个线程间传递信息
2.方式:
采用全局变量
原因:
- 进程是操作系统资源分配的最小单元
- 每个进程空间独立的,包含文本段+数据段(全局变量)+系统数据段
- 一个进程中的多个线程独享栈空间,文本段、数据段、堆区进程多线程共享
注意:
多线程同时操作共享空间会引发资源竞争,需要加上互斥锁解决资源竞争问题
3.互斥锁:
1. 概念
- 解决资源竞争的一种方式,可以看成是一种资源。
- 只能加锁一次,加锁期间不能再次加锁,也不能强制占有一个已经加锁的锁资源,必须等待锁 资源释放,也就是解锁后才能继续操作该锁
- 加锁和解锁中间的代码称为临界代码,也称为临界区
- 只能防止多个线程对资源的竞争,不能决定代码的先后执行顺序
- 原子操作:CPU执行原子操作时无法切换调度任务
2. 使用方式:
- 1. 定义互斥锁(全局变量)
- 2. 对锁初始化
- 3. 操作全局资源前先加锁
- 4. 如果加锁成功则完成对全局资源操作
- 5. 如果加锁失败则表示有人占用资源,必须等待其余人释放锁资源才能加锁成功
- 6. 直到加锁成功使用该全局资源
3. 函数接口:
1. pthread_mutex_init
2. pthread_mutex_lock
3. pthread_mutex_unlock
4. pthread_mutex_destroy
4.死锁
1. 概念:
多线程由于加锁解锁错误导致程序无法继续向下运行的状态称为死锁状态,简称为死锁
2. 死锁产生的四个必要条件:
- 1. 互斥条件
- 2. 不可剥夺条件
- 3. 请求保持条件
- 4. 循环等待条件
3. 如何避免死锁:
- 1. 加锁顺序保持一致
- 2. 使用pthread_mutex_trylock替换pthread_mutex_lock
5.信号量
1. 概念:
- 信号量是一种资源
- 信号量只能完成四种操作:初始化、销毁、申请、释放
- 如果信号量资源数为0,申请资源会阻塞等待,直到占用资源的任务释放资源,资源数不为0时 才能申请到资源并继续向下执行
- 释放资源不会阻塞
2. 函数接口:
1. sem_init
2. sem_destroy
3. sem_wait
注意:
- 申请信号量会让信号量资源数-1
- 如果信号量资源数为0,则会阻塞等待,直到有任务释放资源,才能拿到资源并继续向下执行
4. sem_post
进程间通信(IPC)概述
进程间通信(Inter-Process Communication, IPC)是Linux系统中多个进程共享数据或同步操作的机制。Linux提供了多种IPC方式,包括管道、消息队列、共享内存、信号量、套接字等。每种方式适用于不同场景,需根据需求选择。
管道(Pipe)
管道是半双工的通信方式,数据单向流动,通常用于父子进程或兄弟进程间的通信。
匿名管道
通过pipe()
系统调用创建,返回两个文件描述符:fd[0]
(读端)和fd[1]
(写端)。
代码示例
#include <unistd.h>
#include <stdio.h>int main() {int fd[2];pid_t pid;char buf[256];if (pipe(fd) == -1) {perror("pipe");return 1;}pid = fork();if (pid == 0) { // 子进程close(fd[1]); // 关闭写端read(fd[0], buf, sizeof(buf));printf("Child received: %s\n", buf);close(fd[0]);} else { // 父进程close(fd[0]); // 关闭读端write(fd[1], "Hello from parent", 18);close(fd[1]);}return 0;
}
命名管道(FIFO)
通过mkfifo
命令或mkfifo()
函数创建,允许无亲缘关系的进程通信。
代码示例
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {mkfifo("/tmp/myfifo", 0666);int fd = open("/tmp/myfifo", O_WRONLY);write(fd, "Hello FIFO", 11);close(fd);return 0;
}
消息队列(Message Queue)
消息队列是内核维护的链表,进程通过唯一标识符(key
)访问队列,支持不同类型消息。
常用函数
msgget()
:创建或获取队列。msgsnd()
:发送消息。msgrcv()
:接收消息。msgctl()
:控制队列(如删除)。
代码示例
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>struct msg_buffer {long msg_type;char msg_text[100];
};int main() {key_t key = ftok("progfile", 65);int msgid = msgget(key, 0666 | IPC_CREAT);struct msg_buffer message;message.msg_type = 1;strcpy(message.msg_text, "Hello Message Queue");msgsnd(msgid, &message, sizeof(message), 0);printf("Sent: %s\n", message.msg_text);msgrcv(msgid, &message, sizeof(message), 1, 0);printf("Received: %s\n", message.msg_text);msgctl(msgid, IPC_RMID, NULL);return 0;
}
共享内存(Shared Memory)
共享内存是最快的IPC方式,允许多个进程访问同一块内存区域。
常用函数
shmget()
:创建或获取共享内存段。shmat()
:附加到进程地址空间。shmdt()
:分离共享内存。shmctl()
:控制共享内存(如删除)。
代码示例
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>int main() {key_t key = ftok("shmfile", 65);int shmid = shmget(key, 1024, 0666 | IPC_CREAT);char *str = (char*)shmat(shmid, NULL, 0);printf("Write data: ");fgets(str, 1024, stdin);printf("Data written: %s\n", str);shmdt(str);shmctl(shmid, IPC_RMID, NULL);return 0;
}
信号量(Semaphore)
信号量用于同步进程对共享资源的访问,避免竞态条件。
常用函数
semget()
:创建或获取信号量集。semop()
:执行原子操作(如P/V操作)。semctl()
:控制信号量(如初始化或删除)。
代码示例
#include <sys/sem.h>
#include <stdio.h>
#include <unistd.h>union semun {int val;struct semid_ds *buf;unsigned short *array;
};void p(int semid) {struct sembuf op = {0, -1, SEM_UNDO};semop(semid, &op, 1);
}void v(int semid) {struct sembuf op = {0, 1, SEM_UNDO};semop(semid, &op, 1);
}int main() {key_t key = ftok("semfile", 65);int semid = semget(key, 1, 0666 | IPC_CREAT);union semun arg;arg.val = 1;semctl(semid, 0, SETVAL, arg);pid_t pid = fork();if (pid == 0) {p(semid);printf("Child enters critical section\n");sleep(2);printf("Child leaves critical section\n");v(semid);} else {p(semid);printf("Parent enters critical section\n");sleep(2);printf("Parent leaves critical section\n");v(semid);}semctl(semid, 0, IPC_RMID);return 0;
}
套接字(Socket)
套接字支持跨网络通信,也可用于本地进程间通信(AF_UNIX域)。
本地套接字示例
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main() {int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);struct sockaddr_un addr;addr.sun_family = AF_UNIX;strcpy(addr.sun_path, "/tmp/socket");bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));listen(sockfd, 5);int client = accept(sockfd, NULL, NULL);char buf[256];read(client, buf, sizeof(buf));printf("Server received: %s\n", buf);write(client, "Hello from server", 17);close(client);close(sockfd);unlink("/tmp/socket");return 0;
}
信号(Signal)
信号是异步通知机制,用于进程间简单事件通知(如终止、中断)。
常用函数
kill()
:发送信号。signal()
:注册信号处理函数。
代码示例
#include <signal.h>
#include <stdio.h>
#include <unistd.h>void handler(int sig) {printf("Received signal: %d\n", sig);
}int main() {signal(SIGINT, handler);printf("Press Ctrl+C to trigger signal\n");pause(); // 等待信号return 0;
}
对比与选择建议
IPC方式 | 适用场景 | 特点 |
---|---|---|
管道 | 父子进程简单通信 | 单向,容量有限 |
消息队列 | 无亲缘关系进程结构化通信 | 支持消息类型,内核持久化 |
共享内存 | 高频大数据量通信 | 无需拷贝,需同步机制 |
信号量 | 资源访问同步 | 计数器,避免竞态 |
套接字 | 跨网络或复杂通信 | 灵活,支持多种协议 |
信号 | 异步事件通知 | 简单,不可靠 |
根据需求选择:
- 高性能:共享内存 + 信号量。
- 结构化数据:消息队列。
- 简单通知:信号或管道。
- 跨机器通信:套接字。
注意事项
- 同步问题:共享内存和消息队列需配合信号量避免竞态。
- 资源释放:显式删除IPC对象(如
msgctl
、shmctl
)。 - 权限控制:设置正确的访问权限(如
0666
)。 - 错误处理:检查系统调用返回值(如
-1
表示失败)。