linux c语言进阶 - 进程,通信方式
一. 进程
概述:
进程(Process)是操作系统中资源分配和调度的基本单位,是正在运行的程序的实例。每个进程拥有独立的地址空间、代码、数据和系统资源(如打开的文件、内存、CPU时间等)。进程之间相互独立,通常通过操作系统提供的机制(如进程间通信IPC)进行交互。进程的生命周期包括创建、运行、等待、就绪和终止等状态。
简而言之:进程是操作系统对正在运行程序的管理方式,概括为进程是运行的程序
C语言中:
c语言中,main函数运行后就是一个进程,程序中所有的调度都是通过main来实现。
windows进程:(win+alt+.)
linux进程
ps aux / ps -ef
top / htop
二. 进程的操作:
1. 创建进程
特性:
特性 fork()
vfork()
内存复制 采用写时复制 (Copy-On-Write) 完全不复制父进程地址空间 执行顺序 父子进程执行顺序不确定 父进程阻塞直到子进程退出或 exec 地址空间 子进程获得独立地址空间 子进程共享父进程地址空间 性能开销 较高(需复制页表等元数据) 极低(无内存复制开销) 安全性 安全(内存隔离) 危险(子进程可破坏父进程内存)
特点:
函数 标准 特点 最佳场景 fork()
POSIX 安全但开销大 通用进程创建 vfork()
POSIX 高效但危险 立即exec的极端优化场景 posix_spawn()
POSIX.1d 安全高效的exec封装 便携式高效进程创建 clone()
Linux 可定制共享资源的轻量级进程 线程/特殊IPC场景 pthread_create()
POSIX 纯用户态线程创建 多线程并发
差异:
维度 fork()
vfork()
调度顺序 父子进程并行执行
• 谁先运行由调度器决定严格串行化
• 父进程被强制挂起
• 子进程完全执行完毕(或调用exec
/exit
)后父进程才恢复退出要求 子进程可自由退出( exit
或return
)子进程必须立即调用 exec()
或_exit()
• 禁止从函数返回(会破坏父进程栈)
• 禁止使用exit()
(会刷新共享I/O缓冲区)阻塞行为 父进程不会被阻塞 父进程在 vfork()
调用处阻塞等
1. fork
#include #include #include #include #include // 添加头文件以使用 strlen 函数int main(int argc, char const *argv[]){ int id = getpid(); // 获取当前进程的ID // 1.fork() 创建子进程 pid_t pid = fork(); if (pid < 0) { // pid<0,代表创建失败 printf(\"Fork failed.\\n\"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 printf(\"这是子进程,子id = %d,父id = %d\\n\", id, getppid(), getppid()); // 获取父进程ID } else { // 父进程 printf(\"这是父进程%d\\n\", id); } return 0;}
2. vfork
#include #include #include #include #include // 添加头文件以使用 strlen 函数int main(int argc, char const *argv[]){ // 1.vfork() 创建子进程 pid_t pid = vfork(); if (pid < 0) { // pid<0,代表创建失败 printf(\"vFork failed.\\n\"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 printf(\"这是子进程,子id = %d,父id = %d\\n\", getpid(), getppid()); // 获取父进程ID _exit(0); // 使用_exit()而不是exit(),避免影响父进程的状态 } else { // 父进程 printf(\"这是父进程%d\\n\", getpid()); // 获取当前进程的ID } return 0;}
3. clone / posix_spawn(不讲,有时间写)
2. 生命周期
1. 流程图
2. 概述
1. 创建状态 (New)
本质:进程正在被创建
触发动作:系统调用(如
fork()
,vfork()
)关键操作:
分配进程控制块(PCB)
初始化进程数据结构
分配初始资源(PID、优先级等)
持续时间:瞬时状态(微秒级)
2. 就绪状态 (Ready)
本质:具备运行条件,等待CPU分配
进入条件:
进程创建完成
阻塞状态结束(如I/O完成)
时间片到期被剥夺CPU
核心特征:
位于就绪队列排队
只缺CPU资源
可随时被调度器选中
3. 运行状态 (Running)
本质:正在CPU上执行指令
触发条件:被调度器选中
关键行为:
占用CPU执行代码
可能触发状态转换:
时间片用完 → 回到就绪态
请求资源 → 进入阻塞态
执行结束 → 进入终止态
持续时间:纳秒到毫秒级(取决于时间片)
4. 阻塞状态 (Blocked/Waiting)
本质:等待外部事件完成
常见触发原因:
I/O操作请求(磁盘/网络)
获取互斥锁失败
等待信号量
定时等待(如
sleep()
)核心特征:
主动让出CPU
移出就绪队列
资源满足后自动回就绪态
5. 终止状态 (Terminated)
本质:进程执行结束
触发条件:
正常结束(执行完毕)
异常终止(收到终止信号)
被父进程终止
关键操作:
释放所有资源(内存、文件、设备)
PCB保留退出状态(供父进程查询)
最终从系统移除
3. 代码展示
#include #include #include #include int main(int argc, char const *argv[]){ // 进程的生命周期 // 1.创建状态 pid_t pid = vfork(); // 使用 vfork 创建子进程 printf(\"创建状态.............\"); // 2.就绪状态 printf(\"就绪状态.............\"); if (pid == 0) { // 3.运行状态 printf(\"运行状态.............\"); printf(\"这是子进程,子id = %d,父id = %d\\n\", getpid(), getppid()); // 4.阻塞状态 printf(\"阻塞状态.............\"); sleep(5); // 模拟阻塞状态,等待5秒 // 5.结束状态(销毁) printf(\"子进程结束.............\"); _exit(0); // 使用 _exit() 结束子进程,避免影响父进程的状态 } else if (pid > 0) { // 父进程 printf(\"父进程.............\"); } else { // 创建失败 perror(\"vfork failed\"); exit(EXIT_FAILURE); } return 0;}
3. 进程替换 (不太明白)
进程替换(Process Replacement)是 Unix/Linux 系统中一种核心机制,它允许正在运行的进程完全替换自身,转而执行一个全新的程序。这一机制通过
exec
系列函数实现,是操作系统动态性的关键体现。即,此进程完全复制前一个进程,但保留此进程的pid。
1. 本质与特点
特性 说明 原地替换 不创建新进程,保留原进程的 PID、父进程关系、文件描述符等属性 内存重构 完全替换地址空间(代码段、数据段、堆栈) 无返回 成功执行后永不返回原程序(函数无返回值) 效率优势 避免创建新进程的开销(无需复制页表/内存)
2. exec 函数家族
函数 参数风格 环境变量 PATH搜索 典型使用场景 execl()
参数列表 继承 ❌ 固定参数的可执行程序 execv()
指针数组 继承 ❌ 动态生成参数的场景 execlp()
参数列表 继承 ✅ Shell命令执行 execvp()
指针数组 继承 ✅ 执行用户输入的命令 execle()
参数列表 自定义 ❌ 需要特定环境变量的程序 execve()
指针数组 自定义 ❌ 系统级编程(实际系统调用) fexecve()
指针数组 自定义 ❌ 通过文件描述符指定程序(Linux特有) 📌 底层真相:所有函数最终都调用
execve()
系统调用(内核入口)
3. 代码解释
#include #include #include #include #include int main(int argc, char const *argv[]){ // 进程替换 pid_t pid = fork(); if (pid == 0) { // 子进程 execlp(\"ls\", \"ls\", \"-l\", NULL); perror(\"替换失败\"); _exit(1); } else if (pid > 0) { // 父进程 wait(NULL); } else { // 创建失败 perror(\"创建失败\"); exit(EXIT_FAILURE); } return 0;}
总结:进程替换的本质
进程替换不是创建新进程,而是进程的重生:
保留外壳(PID、资源)
替换核心(程序代码)
重置状态(信号、寄存器)
重获新生(从新入口执行)
4. 进程通信
管道
本质与类型
-
无名管道:
int pipe(int fd[2])
-
内核中的循环缓冲区(默认 64KB)
-
单向数据流:
fd[0]
只读,fd[1]
只写 -
生命周期随进程结束
-
-
有名管道:
mkfifo()
-
文件系统节点(inode 标识)
-
允许无亲缘关系进程通信
-
特性 无名管道 有名管道 创建方式 pipe()
mkfifo()
文件系统路径 无 有(如 /tmp/fifo
)进程关系 必须亲缘关系 任意进程 生命周期 随进程结束销毁 显式调用 unlink()
删除打开行为 直接使用文件描述符 需 open()
,可能阻塞持久性 临时 持久(除非删除) 典型用途 Shell 管道、父子进程通信 无亲缘关系进程间通信
区别于IO流
特性 有名管道 (FIFO) 普通文件I/O流 物理存储 无磁盘存储
数据仅在内核缓冲区中数据持久化存储到磁盘 数据生命周期 读取后立即消失 永久存储,可重复读取 访问方式 严格遵循FIFO顺序 支持随机访问( lseek
)打开行为 需要成对进程打开(读写端)否则阻塞 单进程可独立读写 存储机制 内核维护的循环缓冲区 文件系统分配的磁盘块 最大容量 管道缓冲区大小(默认64KB) 仅受磁盘空间限制 原子性保证 写操作≤ PIPE_BUF
(4KB)时原子无内置原子性保证
1. 无名管道
使用
pipe(int pipefd[2])
系统调用创建无文件系统路径,仅通过文件描述符(
pipefd[0]
读端,pipefd[1]
写端)访问
int pipefd[2];pipe(pipefd); // 创建无名管道
#include #include #include #include #include // 添加头文件以使用 strlen 函数#include /** * pipe 管道,用于进程之间通信(父子进程) */int main(int argc, char const *argv[]){ setlocale(LC_ALL, \"\"); // 使程序使用系统 locale,支持中文utf-8 // 1.创建无名管道 int fd[2]; // 创建管道 if (pipe(fd) == -1) { perror(\"pipe\"); exit(EXIT_FAILURE); } // 2.创建子进程 pid_t pid = fork(); if (pid < 0) { perror(\"fork\"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 close(fd[1]); // 关闭写端 char buffer[100]; read(fd[0], buffer, sizeof(buffer)); // 从管道读取数据 printf(\"Child process received: %s\\n\", buffer); close(fd[0]); // 关闭读端 exit(EXIT_SUCCESS); } else { // 父进程 close(fd[0]); // 关闭读端 const char *message = \"这是父进程发送的消息\"; write(fd[1], message, strlen(message) + 1); // 向管道写入实际字符串长度+1(包含结尾\\0) close(fd[1]); // 关闭写端 } return 0;}
2.有名管道
使用
mkfifo(const char *pathname, mode_t mode)
创建在文件系统中有一个路径名(如
/tmp/myfifo
),像普通文件一样存在
mkfifo(\"/tmp/myfifo\", 0666); // 创建有名管道
#include #include #include #include #include #include #include /*有名管道*/int main(int argc, char const *argv[]){ // 1.创建有名管道 const char *fifo_path = \"/tmp/my_fifo\"; mkfifo(fifo_path, 0666); // 2.创建子进程 pid_t pid = fork(); if (pid == 0) { // 子进程:写入数据 int fd = open(fifo_path, O_WRONLY); write(fd, \"Hello from child\", 17); close(fd); exit(0); } // 3.父进程:读取数据 char buffer[100]; int fd = open(fifo_path, O_RDONLY); read(fd, buffer, sizeof(buffer)); printf(\"父进程读取到:%s\\n\", buffer); close(fd); // 4.清理 unlink(fifo_path); return 0;}
信号(Signal)
本质与类型
软件中断:异步事件通知机制
标准信号:如
SIGINT
(Ctrl+C),SIGKILL
(强制终止)实时信号:支持排队
system v ipc
消息队列
信号量
即,信号量即是一种状态,我如果让他,一次性等于五,他会让五个进程去操作内存,如果一共,有十个,剩余的五个则等待,进入的五个进程依次退出,剩下的五个依次进入.
-
计数器: 信号量本质上是一个非负整数的计数器。
-
操作: 对信号量的操作主要是两个原子操作(Atomic Operation):
-
P 操作 (Wait/Sleep/Down):
-
尝试将信号量的值减 1。
-
如果信号量值大于 0,则减 1 并立即返回(进程获得资源)。
-
如果信号量值等于 0,则进程(或线程)会被阻塞(休眠),直到信号量值变为大于 0(有别的进程释放资源),然后它才能成功执行减 1 操作并继续执行。
-
-
V 操作 (Signal/Post/Up):
-
将信号量的值加 1。
-
如果有进程因为在该信号量上执行 P 操作而被阻塞,V 操作会唤醒其中一个(或多个,取决于实现)被阻塞的进程。
-
-
-
目的: 信号量用来表示可用资源的数量。P 操作代表申请一个资源,V 操作代表释放一个资源。当信号量初始化为 1 时,它就变成了一个互斥锁(Mutex),用于保证同一时刻只有一个进程可以访问临界区。
-
进程间信号量: 我们讨论的信号量驻留在内核中,由内核维护其状态和阻塞队列。因此,不同的进程可以通过同一个信号量的标识符(key 或 name)来访问和操作它,实现跨进程的同步。
#include #include #include #include #include #include #define TOTAL_PROCS 10#define ALLOWED_AT_ONCE 5#define SEM_NAME \"/my_semaphore_example\"void child_process(int id){ // 打开已存在的信号量 sem_t *sem = sem_open(SEM_NAME, O_RDWR); if (sem == SEM_FAILED) { perror(\"子进程信号量打开失败\"); exit(1); } printf(\"子进程 %d (PID:%d) 等待进入...\\n\", id, getpid()); // P操作:等待通行证 sem_wait(sem); // 临界区开始 printf(\"✅ 子进程 %d (PID:%d) 进入临界区\\n\", id, getpid()); sleep(2); // 模拟工作 printf(\"🚪 子进程 %d (PID:%d) 离开临界区\\n\", id, getpid()); // 临界区结束 // V操作:归还通行证 sem_post(sem); sem_close(sem); exit(0);}int main(){ // 创建信号量 (初始5张通行证) sem_t *sem = sem_open(SEM_NAME, O_CREAT | O_RDWR, 0666, ALLOWED_AT_ONCE); if (sem == SEM_FAILED) { perror(\"信号量创建失败\"); exit(1); } printf(\"主进程 (PID:%d) 启动,创建 %d 个子进程,信号量容量: %d\\n\", getpid(), TOTAL_PROCS, ALLOWED_AT_ONCE); // 创建子进程 for (int i = 0; i < TOTAL_PROCS; i++) { pid_t pid = fork(); if (pid == 0) { child_process(i + 1); // 子进程执行 exit(0); } } // 等待所有子进程结束 for (int i = 0; i < TOTAL_PROCS; i++) { wait(NULL); } // 清理 sem_close(sem); sem_unlink(SEM_NAME); // 删除信号量 printf(\"\\n所有子进程完成!信号量已清理\\n\"); return 0;}